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.
This commit is contained in:
parent
3adeca1474
commit
1717ab3016
13 changed files with 45 additions and 14 deletions
|
|
@ -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(
|
|||
<div v-if="columnsControl && !isCollapsed(sec.name)" class="perrow">
|
||||
<label class="perrow__label" :for="`perrow-${instanceId}-${sec.name}`">{{ t('buttons.postersPerRow') }}</label>
|
||||
<select :id="`perrow-${instanceId}-${sec.name}`" class="perrow__select" v-model.number="perRow">
|
||||
<option v-for="n in PER_ROW_CHOICES" :key="n" :value="n">{{ n }}</option>
|
||||
<option v-for="n in PER_ROW_CHOICES" :key="n" :value="n">{{ n === PER_ROW_AUTO ? t('buttons.postersPerRowAuto') : n }}</option>
|
||||
</select>
|
||||
</div>
|
||||
<div v-show="!isCollapsed(sec.name)" class="grid" :style="gridStyle">
|
||||
|
|
|
|||
|
|
@ -46,5 +46,6 @@
|
|||
"packetVariantAria": "تفاصيل حزمة الصلاة",
|
||||
"packetFull": "تفاصيل كاملة",
|
||||
"packetLightweight": "مختصرة",
|
||||
"working": "جارٍ العمل…"
|
||||
"working": "جارٍ العمل…",
|
||||
"postersPerRowAuto": "تلقائي"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -46,5 +46,6 @@
|
|||
"packetVariantAria": "Detailgrad des Gebetspakets",
|
||||
"packetFull": "Vollständig",
|
||||
"packetLightweight": "Kompakt",
|
||||
"working": "Wird bearbeitet…"
|
||||
"working": "Wird bearbeitet…",
|
||||
"postersPerRowAuto": "Automatisch"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -46,5 +46,6 @@
|
|||
"packetVariantAria": "Detalle del paquete de oración",
|
||||
"packetFull": "Detalle completo",
|
||||
"packetLightweight": "Ligero",
|
||||
"working": "Procesando…"
|
||||
"working": "Procesando…",
|
||||
"postersPerRowAuto": "Automático"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -46,5 +46,6 @@
|
|||
"packetVariantAria": "प्रार्थना पैकेट विवरण",
|
||||
"packetFull": "पूर्ण विवरण",
|
||||
"packetLightweight": "संक्षिप्त",
|
||||
"working": "कार्य जारी…"
|
||||
"working": "कार्य जारी…",
|
||||
"postersPerRowAuto": "स्वतः"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -46,5 +46,6 @@
|
|||
"packetVariantAria": "Dettaglio del pacchetto di preghiera",
|
||||
"packetFull": "Dettaglio completo",
|
||||
"packetLightweight": "Leggero",
|
||||
"working": "Elaborazione…"
|
||||
"working": "Elaborazione…",
|
||||
"postersPerRowAuto": "Automatico"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -46,5 +46,6 @@
|
|||
"packetVariantAria": "Detalhe do pacote de oração",
|
||||
"packetFull": "Detalhe completo",
|
||||
"packetLightweight": "Leve",
|
||||
"working": "Processando…"
|
||||
"working": "Processando…",
|
||||
"postersPerRowAuto": "Automático"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -46,5 +46,6 @@
|
|||
"packetVariantAria": "Детализация молитвенного пакета",
|
||||
"packetFull": "Полная версия",
|
||||
"packetLightweight": "Краткая версия",
|
||||
"working": "Выполняется…"
|
||||
"working": "Выполняется…",
|
||||
"postersPerRowAuto": "Авто"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -46,5 +46,6 @@
|
|||
"packetVariantAria": "祷告手册详细程度",
|
||||
"packetFull": "完整版",
|
||||
"packetLightweight": "精简版",
|
||||
"working": "处理中…"
|
||||
"working": "处理中…",
|
||||
"postersPerRowAuto": "自动"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
/>
|
||||
</template>
|
||||
|
|
|
|||
Loading…
Reference in a new issue