diff --git a/internal/check-i18n-never-translated.mjs b/internal/check-i18n-never-translated.mjs index 98dccf6b5..4e3619995 100644 --- a/internal/check-i18n-never-translated.mjs +++ b/internal/check-i18n-never-translated.mjs @@ -81,6 +81,14 @@ const ALLOWED = new Map([ 'DRIVER-GATED: needs the field\'s term per locale, not an invented acronym — see DECIDE-UUPGS-COLUMN-HEADER'], ['prayerLaps.json:upgs', 'DRIVER-GATED: same term as legend.json:columns.upgs — see DECIDE-UUPGS-COLUMN-HEADER'], + // ⚠ SETTLED, NOT A BOOKMARK — unlike the two above, this one is not waiting on a ruling; it IS + // the ruling. "UPG 100" is the product's chosen NAME for the prayer list, so it is a brand + // designation and correctly identical in every locale, exactly like a version number. The + // surrounding copy carries the meaning in each language (export.packetExplain is translated per + // locale and explains what a UPG 100 list is), so a reader does not meet a bare acronym with + // nothing to anchor it. Do not "resolve" this entry by inventing eleven names for one product. + ['legend.json:export.packet', + 'BRAND: the product name for the prayer list; export.packetExplain carries the meaning per locale'], ]) // Below this many locales the comparison is not meaningful — a key present in two files being the diff --git a/library/components/legends/SemanticTreeLegend.vue b/library/components/legends/SemanticTreeLegend.vue index fc0439034..f5f64b4f4 100644 --- a/library/components/legends/SemanticTreeLegend.vue +++ b/library/components/legends/SemanticTreeLegend.vue @@ -221,6 +221,10 @@ const localSelected = ref(null) const panelOpen = ref(props.open !== null ? props.open : true) const showInfoForTab = ref(null) const exportMenuOpen = ref(false) // top-right Export dropdown (behind SHOW_EXPORT_BUTTON) +// Whether the UPG-100 item has revealed its two variants. Separate from exportMenuOpen so closing +// the whole menu also collapses the submenu — see the watch below; a submenu left open would +// re-appear expanded the next time the reader opens Export, having forgotten why it was open. +const packetOpen = ref(false) const { scrimRef, scrimStyle } = useViewportScrim(exportMenuOpen) // Feature flag: show the Export affordance. Gated here in the SHARED legend so // one switch covers every bundle. It was false by an earlier coder decision; @@ -562,15 +566,21 @@ function toggleExportMenu(e) { e?.stopPropagation() exportMenuOpen.value = !exportMenuOpen.value } -// Fire the chosen export kind ('poster' | 'packet') up to the profile, then -// close the menu. The profile renders + downloads the PDF. +// Fire the chosen export kind up to the profile, then close the menu. The profile does the render. +// ⚠ THE PACKET KINDS NOW CARRY THEIR VARIANT — 'packet:full' / 'packet:lightweight' — while 'poster' +// and a bare 'packet' are unchanged, so an older consumer that only understands 'poster' | 'packet' +// keeps working and a new one can split on the colon. Widening the EXISTING string beats adding a +// second emit: one event means a consumer cannot handle half the menu. function emitExport(kind) { exportMenuOpen.value = false + packetOpen.value = false emit('export', kind) } // The Export button only exists while a row is selected; if the selection is // cleared (deselect / tab switch / nodes change) the menu must not linger. -watch(selectedId, id => { if (!id) exportMenuOpen.value = false }) +watch(selectedId, id => { if (!id) { exportMenuOpen.value = false; packetOpen.value = false } }) +// Closing the menu collapses the submenu, so it never re-opens pre-expanded. +watch(exportMenuOpen, open => { if (!open) packetOpen.value = false }) // Bring the selected row into view when the selection changes (e.g. a People-Group // search result highlights a row that may be scrolled off-screen). @@ -1110,6 +1120,27 @@ useShadowStyles(` font: 600 11px system-ui, sans-serif; } .stl-export-item[disabled] { opacity: 0.55; } +/* ★ THE SUBMENU FLOWS BELOW ITS TRIGGER, IT IS NOT POSITIONED OVER IT. .stl-export-menu is a flex + COLUMN, so a block placed after the packet button sits underneath it by document order — no + absolute positioning, nothing to overlap. This matters because the equivalent disclosure on the + card deck did cover its own label on mobile: that one hangs at top:100% out of its card, where a + short viewport leaves it nowhere to go. A panel that grows its parent cannot cover its parent. */ +.stl-export-caret { flex-shrink: 0; font-size: 9px; opacity: 0.7; transition: transform 0.15s; } +.stl-export-caret.is-open { transform: rotate(180deg); } +.stl-export-sub { + display: flex; flex-direction: column; gap: 2px; + margin: 2px 0 2px 6px; padding-inline-start: 6px; + border-inline-start: 1px solid #30363d; /* inline-start, so it mirrors under RTL */ +} +/* The explainer is the only place a reader meets the idea, so it wraps rather than truncating — + a clipped definition is worse than none. */ +.stl-export-note { + margin: 2px 0 4px; padding: 0 2px; + font: 400 10px/1.45 system-ui, sans-serif; color: #8b949e; white-space: normal; +} +.stl-export-subitem { font-weight: 500; padding-block: 5px; } +.stl-panel[data-theme="light"] .stl-export-sub { border-inline-start-color: #d0d7de; } +.stl-panel[data-theme="light"] .stl-export-note { color: #57606a; } .stl-export-lock { color: #6e7681; flex-shrink: 0; } .stl-export-item-label { flex: 1; min-width: 0; } .stl-export-soon { @@ -1240,14 +1271,36 @@ useShadowStyles(` {{ t('legend.export.poster') }} - +
+

{{ t('legend.export.packetExplain') }}

+ + +
diff --git a/library/i18n/locales/ar/legend.json b/library/i18n/locales/ar/legend.json index 3d4fe4ae8..fc1a834c5 100644 --- a/library/i18n/locales/ar/legend.json +++ b/library/i18n/locales/ar/legend.json @@ -70,7 +70,11 @@ }, "export": { "poster": "ملصق", - "packet": "كُتيّب" + "packet": "UPG 100", + "packetExplain": "قائمة صلاة من نحو ٥٠ إلى ١٠٠ شعب غير مبلَّغ — يستطيع شخص واحد أن يحمل واحدة.", + "packetAria": "تفاصيل حزمة الصلاة", + "packetFull": "تفاصيل كاملة", + "packetLightweight": "مختصرة" }, "noDataAtThisLevel": "لا توجد بيانات في هذا المستوى", "exportButton": "تصدير", diff --git a/library/i18n/locales/de/legend.json b/library/i18n/locales/de/legend.json index 68db51d17..541e0cea0 100644 --- a/library/i18n/locales/de/legend.json +++ b/library/i18n/locales/de/legend.json @@ -70,7 +70,11 @@ }, "export": { "poster": "Plakat", - "packet": "Heft" + "packet": "UPG 100", + "packetExplain": "Eine Gebetsliste von etwa 50 bis 100 unerreichten Volksgruppen — eine Person kann eine tragen.", + "packetAria": "Detailgrad des Gebetspakets", + "packetFull": "Vollständig", + "packetLightweight": "Kompakt" }, "noDataAtThisLevel": "Keine Daten auf dieser Ebene", "exportButton": "Exportieren", diff --git a/library/i18n/locales/en/legend.json b/library/i18n/locales/en/legend.json index b7ab49362..9568142aa 100644 --- a/library/i18n/locales/en/legend.json +++ b/library/i18n/locales/en/legend.json @@ -70,7 +70,11 @@ }, "export": { "poster": "Poster", - "packet": "Packet" + "packet": "UPG 100", + "packetExplain": "A prayer list of about 50 to 100 unreached people groups — one person can carry one.", + "packetAria": "Prayer packet detail", + "packetFull": "Full detail", + "packetLightweight": "Lightweight" }, "noDataAtThisLevel": "No data at this level", "exportButton": "Export", diff --git a/library/i18n/locales/es/legend.json b/library/i18n/locales/es/legend.json index 782350045..f97f21aea 100644 --- a/library/i18n/locales/es/legend.json +++ b/library/i18n/locales/es/legend.json @@ -70,7 +70,11 @@ }, "export": { "poster": "Cartel", - "packet": "Cuadernillo" + "packet": "UPG 100", + "packetExplain": "Una lista de oración de unos 50 a 100 pueblos no alcanzados: una persona puede llevar una.", + "packetAria": "Detalle del paquete de oración", + "packetFull": "Detalle completo", + "packetLightweight": "Ligero" }, "noDataAtThisLevel": "No hay datos en este nivel", "exportButton": "Exportar", diff --git a/library/i18n/locales/fr/legend.json b/library/i18n/locales/fr/legend.json index 12b18990f..0a346d941 100644 --- a/library/i18n/locales/fr/legend.json +++ b/library/i18n/locales/fr/legend.json @@ -70,7 +70,11 @@ }, "export": { "poster": "Affiche", - "packet": "Livret" + "packet": "UPG 100", + "packetExplain": "Une liste de prière d’environ 50 à 100 peuples non atteints : une personne peut en porter une.", + "packetAria": "Niveau de détail du dossier de prière", + "packetFull": "Détail complet", + "packetLightweight": "Allégé" }, "noDataAtThisLevel": "Aucune donnée à ce niveau", "exportButton": "Exporter", diff --git a/library/i18n/locales/hi/legend.json b/library/i18n/locales/hi/legend.json index 320996f35..85229349e 100644 --- a/library/i18n/locales/hi/legend.json +++ b/library/i18n/locales/hi/legend.json @@ -70,7 +70,11 @@ }, "export": { "poster": "पोस्टर", - "packet": "पुस्तिका" + "packet": "UPG 100", + "packetExplain": "लगभग 50 से 100 अनपहुँचे जनसमूहों की एक प्रार्थना सूची — एक व्यक्ति एक ले सकता है।", + "packetAria": "प्रार्थना पैकेट विवरण", + "packetFull": "पूर्ण विवरण", + "packetLightweight": "संक्षिप्त" }, "noDataAtThisLevel": "इस स्तर पर कोई डेटा नहीं", "exportButton": "निर्यात", diff --git a/library/i18n/locales/it/legend.json b/library/i18n/locales/it/legend.json index 2f2435021..3b78f1779 100644 --- a/library/i18n/locales/it/legend.json +++ b/library/i18n/locales/it/legend.json @@ -70,7 +70,11 @@ }, "export": { "poster": "Manifesto", - "packet": "Libretto" + "packet": "UPG 100", + "packetExplain": "Una lista di preghiera di circa 50-100 popoli non raggiunti: una persona può portarne una.", + "packetAria": "Dettaglio del pacchetto di preghiera", + "packetFull": "Dettaglio completo", + "packetLightweight": "Leggero" }, "noDataAtThisLevel": "Nessun dato a questo livello", "exportButton": "Esporta", diff --git a/library/i18n/locales/pt/legend.json b/library/i18n/locales/pt/legend.json index 8538bda32..73cdac70d 100644 --- a/library/i18n/locales/pt/legend.json +++ b/library/i18n/locales/pt/legend.json @@ -70,7 +70,11 @@ }, "export": { "poster": "Cartaz", - "packet": "Caderno" + "packet": "UPG 100", + "packetExplain": "Uma lista de oração de cerca de 50 a 100 povos não alcançados: uma pessoa pode levar uma.", + "packetAria": "Detalhe do pacote de oração", + "packetFull": "Detalhe completo", + "packetLightweight": "Leve" }, "noDataAtThisLevel": "Sem dados neste nível", "exportButton": "Exportar", diff --git a/library/i18n/locales/ro/legend.json b/library/i18n/locales/ro/legend.json index 93509fa97..f38af783a 100644 --- a/library/i18n/locales/ro/legend.json +++ b/library/i18n/locales/ro/legend.json @@ -70,7 +70,11 @@ }, "export": { "poster": "Afiș", - "packet": "Broșură" + "packet": "UPG 100", + "packetExplain": "O listă de rugăciune de aproximativ 50-100 de popoare neatinse — o persoană poate purta una.", + "packetAria": "Nivelul de detaliu al pachetului de rugăciune", + "packetFull": "Detaliu complet", + "packetLightweight": "Redus" }, "noDataAtThisLevel": "Nu există date la acest nivel", "exportButton": "Exportă", diff --git a/library/i18n/locales/ru/legend.json b/library/i18n/locales/ru/legend.json index 6d67c10ef..312daa30a 100644 --- a/library/i18n/locales/ru/legend.json +++ b/library/i18n/locales/ru/legend.json @@ -70,7 +70,11 @@ }, "export": { "poster": "Плакат", - "packet": "Буклет" + "packet": "UPG 100", + "packetExplain": "Молитвенный список примерно из 50–100 неохваченных народов — один человек может взять один.", + "packetAria": "Детализация молитвенного пакета", + "packetFull": "Полная версия", + "packetLightweight": "Краткая версия" }, "noDataAtThisLevel": "На этом уровне нет данных", "exportButton": "Экспорт", diff --git a/library/i18n/locales/zh/legend.json b/library/i18n/locales/zh/legend.json index 98a08c22b..26e2f8afb 100644 --- a/library/i18n/locales/zh/legend.json +++ b/library/i18n/locales/zh/legend.json @@ -70,7 +70,11 @@ }, "export": { "poster": "海报", - "packet": "祷告册" + "packet": "UPG 100", + "packetExplain": "约 50 到 100 个未得之民的祷告名单——一个人就能领一份。", + "packetAria": "祷告手册详细程度", + "packetFull": "完整版", + "packetLightweight": "精简版" }, "noDataAtThisLevel": "此层级没有数据", "exportButton": "导出", diff --git a/plugin-designs/my-upg-100-list/upg-100-list.vue b/plugin-designs/my-upg-100-list/upg-100-list.vue index 154077bfe..d44dad290 100644 --- a/plugin-designs/my-upg-100-list/upg-100-list.vue +++ b/plugin-designs/my-upg-100-list/upg-100-list.vue @@ -1315,13 +1315,18 @@ provide('onLegendExport', onLegendExport) async function onLegendExport(kind) { const zoneId = selectedKey.value; if (!zoneId) return; // the button only shows with a selection + // ★ THE KIND NOW CARRIES ITS VARIANT: 'packet:full' | 'packet:lightweight'. A bare 'packet' still + // works and still means the full packet, so nothing that emits the old string breaks — the legend + // WIDENED its existing event rather than adding a second one, because two events would let a + // consumer handle half the menu and look fine. + const [base, variant] = String(kind).split(':'); // Guard re-entry the way the HUD does: a second click while a print is building would run two // renders over one print frame. if (batchBusy.value) return; batchBusy.value = true; - busyAction.value = kind === 'packet' ? 'packet' : 'poster'; + busyAction.value = base === 'packet' ? 'packet' : 'poster'; try { - if (kind === 'packet') { await printPrayerPacket(zoneId); return; } + if (base === 'packet') { await printPrayerPacket(zoneId, variant || 'full'); return; } // The legend selects by KEY, while the print actions label by item — hand over the zone // object so the print dialog's title says the list's name rather than its slug. // Same resolution printPrayerPacket uses, so both print actions name the list identically. @@ -1435,7 +1440,10 @@ function flashRef(target, text, ms) { } const flashStatus = (text, ms) => flashRef(status, text, ms); -async function printPrayerPacket(zoneId) { +// `variant` selects WHICH packet — 'full' (map, life calendar, %-evangelical grid, triangle, +// profiles, sets, points, wheel) or 'lightweight' (map, triangle, profiles, points, wheel). +// Defaults to 'full', so every caller that does not pass one prints exactly what it printed before. +async function printPrayerPacket(zoneId, variant = 'full') { const zone = allLists().find((z) => z.id === zoneId); if (!zone) return; // The map svg needs the renderer's own mapCss/W, so it is resolved here and @@ -1447,6 +1455,7 @@ async function printPrayerPacket(zoneId) { ]); const R = createPacketRenderer({ strings, lang: packetLang() }); await packetPrints.printPacket(zone, { + variant, mapSvg: await packetMapSvg(zoneId, R), funnel: await packetFunnel(), });