Cards per row on the shared grid, with Auto as the absence of a value
The Driver asked for a top-right dropdown with Auto plus fixed overrides, remembered, and said plainly that the CSS already working must not break. So Auto writes NOTHING. The grid is repeat(auto-fill, minmax(240px, 1fr)), which already gives one column on a phone and three to five on a desktop by measuring its container; Auto leaves the stylesheet alone and only an explicit override sets grid-template-columns. An Auto that computed a number and wrote it back would look identical until the container resized, and then be silently wrong — the same picture, with the responsiveness removed. An override is honoured even when it is a poor idea: five columns on a 390px phone is a 62px card. That is deliberate. The reader asked for it, Auto is the default and one click away, and the control shows which is active. Clamping someone's explicit choice to what we think is sensible is how a control stops being trusted. It is remembered per PROFILE, not per bundle. Two grids on one page are two different lists, and useDashboardLayout already paid for that lesson when a shared key made one caller inherit another's saved layout. Blocked storage is caught and ignored: in private mode the default is the correct outcome, not an error to report. It sits in its own row rather than in #filters, which is gated on useSearch || useFilters — a host turning both off would otherwise lose a control that has nothing to do with either. It modifies the grid, so it lives on the grid. Both strings are authored in all eleven catalogues, in the snake_case this catalogue uses: a camelCase key here would have resolved to nothing and rendered its own name, which is the defect I shipped in a legend earlier today.
This commit is contained in:
parent
594a3bbe53
commit
5880e84616
12 changed files with 80 additions and 12 deletions
|
|
@ -16,5 +16,7 @@
|
|||
"no_results": "لا توجد شعوب مطابقة لهذا التصفية.",
|
||||
"type_to_search": "اكتب للبحث...",
|
||||
"load_failed": "تعذّر تحميل بيانات الشعوب. هذه مشكلة اتصال، وليست مشكلة في التصفية.",
|
||||
"search_people_or_countries": "ابحث عن الشعوب أو الدول…"
|
||||
"search_people_or_countries": "ابحث عن الشعوب أو الدول…",
|
||||
"cards_per_row": "البطاقات في كل صف",
|
||||
"cards_per_row_auto": "تلقائي"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -16,5 +16,7 @@
|
|||
"no_results": "Keine Volksgruppen entsprechen diesem Filter.",
|
||||
"type_to_search": "Zum Suchen tippen...",
|
||||
"load_failed": "Die Daten konnten nicht geladen werden. Das ist ein Verbindungsproblem, nicht Ihr Filter.",
|
||||
"search_people_or_countries": "Volksgruppen oder Länder suchen…"
|
||||
"search_people_or_countries": "Volksgruppen oder Länder suchen…",
|
||||
"cards_per_row": "Karten pro Zeile",
|
||||
"cards_per_row_auto": "Automatisch"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -16,5 +16,7 @@
|
|||
"no_results": "No people groups match this filter.",
|
||||
"type_to_search": "Type to search...",
|
||||
"load_failed": "People-group data could not be loaded. This is a connection problem, not your filter.",
|
||||
"search_people_or_countries": "Search people groups or countries…"
|
||||
"search_people_or_countries": "Search people groups or countries…",
|
||||
"cards_per_row": "Cards per row",
|
||||
"cards_per_row_auto": "Auto"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -16,5 +16,7 @@
|
|||
"no_results": "Ningún pueblo coincide con este filtro.",
|
||||
"type_to_search": "Escribe para buscar...",
|
||||
"load_failed": "No se pudieron cargar los datos de pueblos. Es un problema de conexión, no de tu filtro.",
|
||||
"search_people_or_countries": "Buscar pueblos o países…"
|
||||
"search_people_or_countries": "Buscar pueblos o países…",
|
||||
"cards_per_row": "Tarjetas por fila",
|
||||
"cards_per_row_auto": "Automático"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -16,5 +16,7 @@
|
|||
"no_results": "Aucun peuple ne correspond à ce filtre.",
|
||||
"type_to_search": "Tapez pour rechercher...",
|
||||
"load_failed": "Les données n’ont pas pu être chargées. C’est un problème de connexion, pas votre filtre.",
|
||||
"search_people_or_countries": "Rechercher des peuples ou des pays…"
|
||||
"search_people_or_countries": "Rechercher des peuples ou des pays…",
|
||||
"cards_per_row": "Cartes par ligne",
|
||||
"cards_per_row_auto": "Automatique"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -16,5 +16,7 @@
|
|||
"no_results": "इस फ़िल्टर से कोई जनसमूह मेल नहीं खाता।",
|
||||
"type_to_search": "खोजने के लिए लिखें...",
|
||||
"load_failed": "जनसमूह डेटा लोड नहीं हो सका। यह कनेक्शन की समस्या है, आपके फ़िल्टर की नहीं।",
|
||||
"search_people_or_countries": "जनसमूह या देश खोजें…"
|
||||
"search_people_or_countries": "जनसमूह या देश खोजें…",
|
||||
"cards_per_row": "प्रति पंक्ति कार्ड",
|
||||
"cards_per_row_auto": "स्वचालित"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -16,5 +16,7 @@
|
|||
"no_results": "Nessun popolo corrisponde a questo filtro.",
|
||||
"type_to_search": "Digita per cercare...",
|
||||
"load_failed": "Impossibile caricare i dati dei popoli. È un problema di connessione, non del filtro.",
|
||||
"search_people_or_countries": "Cerca popoli o paesi…"
|
||||
"search_people_or_countries": "Cerca popoli o paesi…",
|
||||
"cards_per_row": "Schede per riga",
|
||||
"cards_per_row_auto": "Automatico"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -16,5 +16,7 @@
|
|||
"no_results": "Nenhum povo corresponde a este filtro.",
|
||||
"type_to_search": "Escreva para pesquisar...",
|
||||
"load_failed": "Não foi possível carregar os dados dos povos. É um problema de ligação, não do seu filtro.",
|
||||
"search_people_or_countries": "Pesquisar povos ou países…"
|
||||
"search_people_or_countries": "Pesquisar povos ou países…",
|
||||
"cards_per_row": "Cartões por linha",
|
||||
"cards_per_row_auto": "Automático"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -16,5 +16,7 @@
|
|||
"no_results": "Niciun popor nu corespunde acestui filtru.",
|
||||
"type_to_search": "Scrie pentru a căuta...",
|
||||
"load_failed": "Datele despre popoare nu au putut fi încărcate. Este o problemă de conexiune, nu a filtrului.",
|
||||
"search_people_or_countries": "Caută popoare sau țări…"
|
||||
"search_people_or_countries": "Caută popoare sau țări…",
|
||||
"cards_per_row": "Carduri pe rând",
|
||||
"cards_per_row_auto": "Automat"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -16,5 +16,7 @@
|
|||
"no_results": "Нет народов, соответствующих этому фильтру.",
|
||||
"type_to_search": "Введите для поиска...",
|
||||
"load_failed": "Не удалось загрузить данные о народах. Это проблема соединения, а не фильтра.",
|
||||
"search_people_or_countries": "Поиск народов или стран…"
|
||||
"search_people_or_countries": "Поиск народов или стран…",
|
||||
"cards_per_row": "Карточек в ряду",
|
||||
"cards_per_row_auto": "Авто"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -16,5 +16,7 @@
|
|||
"no_results": "没有符合此筛选条件的民族。",
|
||||
"type_to_search": "输入以搜索...",
|
||||
"load_failed": "无法加载民族数据。这是连接问题,而不是筛选问题。",
|
||||
"search_people_or_countries": "搜索民族或国家…"
|
||||
"search_people_or_countries": "搜索民族或国家…",
|
||||
"cards_per_row": "每行卡片数",
|
||||
"cards_per_row_auto": "自动"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -312,6 +312,37 @@ const selectUrl = computed(() => props.selectUrl || cfg.value.selectUrl || ''
|
|||
// that installs no i18n plugin — useI18n() there throws, and a thrown setup() removes the subtree
|
||||
// with no build error. Order: host `t` wins (it always did), then the catalogue, then the English
|
||||
// literal already at each call site, so a locale missing a key degrades to today's behaviour.
|
||||
/**
|
||||
* ★ CARDS PER ROW. "Auto" is not a value — it is the ABSENCE of one, and that is the whole design.
|
||||
* The Driver's words were "we don't want to break the CSS that's already working": the grid is
|
||||
* repeat(auto-fill, minmax(240px, 1fr)), which already gives 1 column on a phone and 3-5 on a
|
||||
* desktop by measuring the container. So Auto writes NO inline style and the stylesheet is
|
||||
* untouched; only an explicit override sets grid-template-columns. A "smart" Auto that computed a
|
||||
* number and wrote it back would be the same picture until the container changed size, and then
|
||||
* silently wrong.
|
||||
*
|
||||
* ⚠ AN OVERRIDE IS HONOURED EVEN WHEN IT IS A BAD IDEA — 5 columns on a 390px phone is 62px cards.
|
||||
* That is deliberate: the reader asked. The escape is that Auto is the default and is one click
|
||||
* away, and the control says which one is active. Clamping someone's explicit choice to what we
|
||||
* think is sensible is how a control stops being trusted.
|
||||
*/
|
||||
const PER_ROW_OPTIONS = ['auto', '1', '2', '3', '4', '5']
|
||||
const perRowKey = computed(() => `pgg-cards-per-row:${cfg.value.profile || 'default'}`)
|
||||
const perRow = ref('auto')
|
||||
onMounted(() => {
|
||||
// Per PROFILE, not per bundle: two grids on one page are two different lists, and useDashboardLayout
|
||||
// already paid for that lesson — a shared key made one caller inherit another's saved layout.
|
||||
try {
|
||||
const saved = localStorage.getItem(perRowKey.value)
|
||||
if (saved && PER_ROW_OPTIONS.includes(saved)) perRow.value = saved
|
||||
} catch (_) { /* private mode / blocked storage: the default is correct, not an error */ }
|
||||
})
|
||||
watch(perRow, (v) => { try { localStorage.setItem(perRowKey.value, v) } catch (_) {} })
|
||||
/** Auto contributes NOTHING, so the stylesheet keeps its own rule. */
|
||||
const gridStyle = computed(() => (perRow.value === 'auto'
|
||||
? null
|
||||
: { gridTemplateColumns: `repeat(${perRow.value}, minmax(0, 1fr))` }))
|
||||
|
||||
const t = computed(() => {
|
||||
const host = { ...(cfg.value.t || {}), ...(props.t || {}) }
|
||||
const locale = cfg.value.locale || cfg.value.lang || undefined
|
||||
|
|
@ -1011,7 +1042,18 @@ onMounted(() => {
|
|||
</div>
|
||||
</div>
|
||||
|
||||
<div id="results" class="grid uupgs-list">
|
||||
<!-- Its own row, NOT inside #filters: that block is gated on useSearch || useFilters, so a
|
||||
host that turns both off would lose this control too — and it has nothing to do with
|
||||
either. It modifies the grid, so it sits on the grid. -->
|
||||
<div class="pgg-gridbar">
|
||||
<label class="pgg-perrow">
|
||||
<span>{{ t.cards_per_row }}</span>
|
||||
<select v-model="perRow" :aria-label="t.cards_per_row">
|
||||
<option v-for="o in PER_ROW_OPTIONS" :key="o" :value="o">{{ o === 'auto' ? t.cards_per_row_auto : o }}</option>
|
||||
</select>
|
||||
</label>
|
||||
</div>
|
||||
<div id="results" class="grid uupgs-list" :style="gridStyle">
|
||||
<template v-for="uupg in displayedUUPGs" :key="uupg.slug">
|
||||
<div class="card uupg__card">
|
||||
<img v-if="uupg.image_url" class="uupg__image" :src="uupg.image_url" :alt="uupg.name">
|
||||
|
|
@ -1152,6 +1194,10 @@ onMounted(() => {
|
|||
overflowing. Paired with .uupg__image's aspect-ratio:1/1 this takes the image from a 220x130
|
||||
letterbox to a 240x240 square: ~2x the visible area, and the face is no longer cropped to a band. */
|
||||
.grid.uupgs-list { display: grid; grid-template-columns: repeat(auto-fill, minmax(240px, 1fr)); gap: 16px; }
|
||||
/* Top-right, above the grid it controls. Logical inline-end so it mirrors under RTL. */
|
||||
.pgg-gridbar { display: flex; justify-content: flex-end; margin-bottom: 8px; }
|
||||
.pgg-perrow { display: inline-flex; align-items: center; gap: 6px; font-size: 12px; color: var(--mut); }
|
||||
.pgg-perrow select { font: inherit; color: var(--ink); background: var(--card); border: 1px solid var(--line); border-radius: 6px; padding: 2px 6px; }
|
||||
.card.uupg__card {
|
||||
display: flex; flex-direction: column; background: var(--card); border: 1px solid var(--line);
|
||||
border-radius: 12px; overflow: hidden; box-shadow: 0 1px 2px rgba(0, 0, 0, 0.04);
|
||||
|
|
|
|||
Loading…
Reference in a new issue