From 1717ab3016140ea643a0b8122ce16917c3370442 Mon Sep 17 00:00:00 2001 From: Doxa Maps Date: Mon, 28 Sep 2026 19:11:20 +0000 Subject: [PATCH] The UPG-100 poster list gets the per-row control, with an Auto option Round 2 item 6. The control was already built in PosterWall -- state, persistence, grid binding, markup -- and gated behind an opt-in prop that only the regions dashboard passed. So most of this is enabling it rather than writing it, which is what "reuse the pattern, don't re-derive" asked for. I checked that before building anything, because the last time I added a feature without checking which surface already had it, it landed somewhere nobody looks. Auto is genuinely new: the choices were 2 to 5, always forcing a count, and the brief asks for Auto meaning today's CSS with overrides on top. gridStyle already returned {} for a non-positive value -- the comment at that line even says so -- so Auto needed no new layout branch. It needed to become REACHABLE: a 0 in the choice list, a label, a read path and a write path. It extends the earlier min-2/max-5/default-2 ruling rather than contradicting it, since those bound the counts and Auto is the option that declines to pick one. Round-tripping the storage values, rather than reading the code, caught a bug I had just written: getItem returns null when nothing is stored, and Number(null) is 0 -- which is exactly Auto now that 0 means something. So every first-time reader would have silently got Auto instead of the default, and merely ADDING an option would have changed the layout for everyone who never touched the control. A missing key now returns null and falls back to the default; a stored 0 reads as Auto. Auto is also persisted, which needed its own clause: the range check rejects 0, so without it the one value that is not a count would be the one value that never survived a reload. A preference that forgets only some of its values is worse than one that forgets all of them, because the reader cannot tell it is broken. --- library/components/poster/PosterWall.vue | 25 ++++++++++++++++--- library/i18n/locales/ar/buttons.json | 3 ++- library/i18n/locales/de/buttons.json | 3 ++- library/i18n/locales/en/buttons.json | 3 ++- library/i18n/locales/es/buttons.json | 3 ++- library/i18n/locales/fr/buttons.json | 3 ++- library/i18n/locales/hi/buttons.json | 3 ++- library/i18n/locales/it/buttons.json | 3 ++- library/i18n/locales/pt/buttons.json | 3 ++- library/i18n/locales/ro/buttons.json | 3 ++- library/i18n/locales/ru/buttons.json | 3 ++- library/i18n/locales/zh/buttons.json | 3 ++- .../my-upg-100-list/poster-wall.vue | 1 + 13 files changed, 45 insertions(+), 14 deletions(-) diff --git a/library/components/poster/PosterWall.vue b/library/components/poster/PosterWall.vue index 0fcda8c3e..fe3097e40 100644 --- a/library/components/poster/PosterWall.vue +++ b/library/components/poster/PosterWall.vue @@ -173,11 +173,23 @@ const props = defineProps({ // before the reader has said otherwise; once they choose, their choice is the default. const perRow = ref(readStoredPerRow() ?? (props.columns || PER_ROW_DEFAULT)) watch(() => props.columns, (n) => { if (readStoredPerRow() == null) perRow.value = n || PER_ROW_DEFAULT }) -watch(perRow, (n) => { if (Number.isFinite(n) && n >= PER_ROW_MIN && n <= PER_ROW_MAX) storePerRow(n) }) +// ⚠ PER_ROW_AUTO (0) IS STORED TOO, so "Auto" survives a reload like any other choice. Without this +// clause the range check below rejects 0, nothing is written, and the next visit silently falls back +// to the default — a preference that forgets only ONE of its values is worse than one that forgets +// them all, because the reader cannot tell it is broken. +watch(perRow, (n) => { + if (n === PER_ROW_AUTO || (Number.isFinite(n) && n >= PER_ROW_MIN && n <= PER_ROW_MAX)) storePerRow(n) +}) // The choices the control offers. Two is the Driver's default; the ceiling is the wall's own // auto-fill behaviour, offered as "fit" so the knob can always get back to where it started. // Driver: "poster per row should have a minimum of 2 and a max of 5 and a default of 2". -const PER_ROW_CHOICES = [2, 3, 4, 5] +// ★ AUTO IS 0, AND 0 IS NOT A COUNT — it is the absence of an override. gridStyle already returns +// {} for a non-positive value, leaving the stylesheet's auto-fill rule in charge, so Auto needed no +// new branch in the layout: it needed to become REACHABLE from the control and from storage. +// This extends the earlier ruling (min 2, max 5, default 2) rather than contradicting it: those +// bound the COUNTS, and Auto is the option that declines to pick one. +const PER_ROW_AUTO = 0 +const PER_ROW_CHOICES = [PER_ROW_AUTO, 2, 3, 4, 5] const PER_ROW_MIN = 2, PER_ROW_MAX = 5, PER_ROW_DEFAULT = 2 // ...and "it needs to honour local storage so if you change the default number it is saved in local // storage so you can reload and your default you set is persistent". One key for the whole wall, @@ -188,7 +200,14 @@ const PER_ROW_KEY = 'doxa-maps:poster-wall:per-row' function readStoredPerRow() { try { const raw = window.localStorage.getItem(PER_ROW_KEY) + // ⚠ A MISSING KEY MUST NOT READ AS AUTO. getItem returns null when nothing is stored, and + // Number(null) is 0 — which is exactly PER_ROW_AUTO now that 0 means something. Without this + // guard every first-time reader would silently get Auto instead of the default, i.e. adding an + // option would have CHANGED the layout for everyone who never touched the control. Caught by + // round-tripping the storage values rather than by reading the code. + if (raw == null || raw === '') return null const n = Number(raw) + if (n === PER_ROW_AUTO) return PER_ROW_AUTO return Number.isFinite(n) && n >= PER_ROW_MIN && n <= PER_ROW_MAX ? n : null } catch (_) { return null } } @@ -1061,7 +1080,7 @@ useShadowStyles(
diff --git a/library/i18n/locales/ar/buttons.json b/library/i18n/locales/ar/buttons.json index 0d6ee375a..8ecbe7177 100644 --- a/library/i18n/locales/ar/buttons.json +++ b/library/i18n/locales/ar/buttons.json @@ -46,5 +46,6 @@ "packetVariantAria": "تفاصيل حزمة الصلاة", "packetFull": "تفاصيل كاملة", "packetLightweight": "مختصرة", - "working": "جارٍ العمل…" + "working": "جارٍ العمل…", + "postersPerRowAuto": "تلقائي" } diff --git a/library/i18n/locales/de/buttons.json b/library/i18n/locales/de/buttons.json index 4de464659..c7b28da1c 100644 --- a/library/i18n/locales/de/buttons.json +++ b/library/i18n/locales/de/buttons.json @@ -46,5 +46,6 @@ "packetVariantAria": "Detailgrad des Gebetspakets", "packetFull": "Vollständig", "packetLightweight": "Kompakt", - "working": "Wird bearbeitet…" + "working": "Wird bearbeitet…", + "postersPerRowAuto": "Automatisch" } diff --git a/library/i18n/locales/en/buttons.json b/library/i18n/locales/en/buttons.json index 17abca39d..0ac1496d4 100644 --- a/library/i18n/locales/en/buttons.json +++ b/library/i18n/locales/en/buttons.json @@ -46,5 +46,6 @@ "clearSelection": "Clear selection", "noOptions": "No options found", "renderingLocal": "Rendering a print-quality image in your browser — nothing is uploaded.", - "postersPerRow": "Posters per row" + "postersPerRow": "Posters per row", + "postersPerRowAuto": "Auto" } diff --git a/library/i18n/locales/es/buttons.json b/library/i18n/locales/es/buttons.json index a03761824..093c3f8c3 100644 --- a/library/i18n/locales/es/buttons.json +++ b/library/i18n/locales/es/buttons.json @@ -46,5 +46,6 @@ "packetVariantAria": "Detalle del paquete de oración", "packetFull": "Detalle completo", "packetLightweight": "Ligero", - "working": "Procesando…" + "working": "Procesando…", + "postersPerRowAuto": "Automático" } diff --git a/library/i18n/locales/fr/buttons.json b/library/i18n/locales/fr/buttons.json index 444cefbc0..2e0a11d48 100644 --- a/library/i18n/locales/fr/buttons.json +++ b/library/i18n/locales/fr/buttons.json @@ -46,5 +46,6 @@ "packetVariantAria": "Niveau de détail du dossier de prière", "packetFull": "Détail complet", "packetLightweight": "Allégé", - "working": "Traitement…" + "working": "Traitement…", + "postersPerRowAuto": "Auto" } diff --git a/library/i18n/locales/hi/buttons.json b/library/i18n/locales/hi/buttons.json index 49c29d793..412fe4e1b 100644 --- a/library/i18n/locales/hi/buttons.json +++ b/library/i18n/locales/hi/buttons.json @@ -46,5 +46,6 @@ "packetVariantAria": "प्रार्थना पैकेट विवरण", "packetFull": "पूर्ण विवरण", "packetLightweight": "संक्षिप्त", - "working": "कार्य जारी…" + "working": "कार्य जारी…", + "postersPerRowAuto": "स्वतः" } diff --git a/library/i18n/locales/it/buttons.json b/library/i18n/locales/it/buttons.json index ed7ada37e..84bc80391 100644 --- a/library/i18n/locales/it/buttons.json +++ b/library/i18n/locales/it/buttons.json @@ -46,5 +46,6 @@ "packetVariantAria": "Dettaglio del pacchetto di preghiera", "packetFull": "Dettaglio completo", "packetLightweight": "Leggero", - "working": "Elaborazione…" + "working": "Elaborazione…", + "postersPerRowAuto": "Automatico" } diff --git a/library/i18n/locales/pt/buttons.json b/library/i18n/locales/pt/buttons.json index dc5d638f9..1efb6a214 100644 --- a/library/i18n/locales/pt/buttons.json +++ b/library/i18n/locales/pt/buttons.json @@ -46,5 +46,6 @@ "packetVariantAria": "Detalhe do pacote de oração", "packetFull": "Detalhe completo", "packetLightweight": "Leve", - "working": "Processando…" + "working": "Processando…", + "postersPerRowAuto": "Automático" } diff --git a/library/i18n/locales/ro/buttons.json b/library/i18n/locales/ro/buttons.json index c3725bc2d..620ad6dd4 100644 --- a/library/i18n/locales/ro/buttons.json +++ b/library/i18n/locales/ro/buttons.json @@ -46,5 +46,6 @@ "packetVariantAria": "Nivelul de detaliu al pachetului de rugăciune", "packetFull": "Detaliu complet", "packetLightweight": "Redus", - "working": "Se procesează…" + "working": "Se procesează…", + "postersPerRowAuto": "Automat" } diff --git a/library/i18n/locales/ru/buttons.json b/library/i18n/locales/ru/buttons.json index 28400bc0b..4937fd097 100644 --- a/library/i18n/locales/ru/buttons.json +++ b/library/i18n/locales/ru/buttons.json @@ -46,5 +46,6 @@ "packetVariantAria": "Детализация молитвенного пакета", "packetFull": "Полная версия", "packetLightweight": "Краткая версия", - "working": "Выполняется…" + "working": "Выполняется…", + "postersPerRowAuto": "Авто" } diff --git a/library/i18n/locales/zh/buttons.json b/library/i18n/locales/zh/buttons.json index 4c5cda93d..b5ff59862 100644 --- a/library/i18n/locales/zh/buttons.json +++ b/library/i18n/locales/zh/buttons.json @@ -46,5 +46,6 @@ "packetVariantAria": "祷告手册详细程度", "packetFull": "完整版", "packetLightweight": "精简版", - "working": "处理中…" + "working": "处理中…", + "postersPerRowAuto": "自动" } diff --git a/plugin-designs/my-upg-100-list/poster-wall.vue b/plugin-designs/my-upg-100-list/poster-wall.vue index b1ef47a8c..8dd3def9a 100644 --- a/plugin-designs/my-upg-100-list/poster-wall.vue +++ b/plugin-designs/my-upg-100-list/poster-wall.vue @@ -293,5 +293,6 @@ const wallTag = computed(() => t('messages.upg100WallTag')) preview-action="print" :hide-prayer-list-when-soon="true" download-prefix="doxa-upg-100" + :columns-control="true" />