The Styles tab called generateCountrySheet directly and built the country without its region, so the sample would have printed in the old flat teal while the Country Posters wall prints it region-hued. It now draws through the one shared path (library/packet/countrySheet.js), which carries the region and computes the series the way the wall does; the inline series arithmetic is gone. check:country-sheet-one-path passes.
145 lines
8 KiB
Vue
145 lines
8 KiB
Vue
<script setup>
|
||
/**
|
||
* poster-styles.vue: every poster template the library prints, one card per configuration.
|
||
*
|
||
* @element poster-design
|
||
* @description Every poster template in one grid (map sheets, the prayer packet, prayer cards, the wall
|
||
* panels, the map print), each card drawn from the library with sample data and named by its
|
||
* configuration, with "Show full details" for what an agent needs to reuse it.
|
||
*
|
||
* The cards ARE the poster wall's cards: this profile renders the library's PosterWall and changes two
|
||
* things through its host seams. headerAction turns the card's "Show on map" sticker into "Show full
|
||
* details" (there is no map here), and previewSrc draws each thumbnail from the library at view time
|
||
* instead of reading a stored image, so a change to a template shows up here the next time it opens.
|
||
* The catalogue itself is src/templates.js.
|
||
*/
|
||
import { computed, inject, ref } from 'vue'
|
||
import PosterWall from '@map/components/poster/PosterWall.vue'
|
||
import { useOptionalT } from '@map/composables/useOptionalT.js'
|
||
import { resolveAssetsBase } from '@map/utils/embedBase.js'
|
||
import { useShadowStyles } from '@map/composables/useShadowStyles.js'
|
||
import { createPeopleGroupRecords } from '@map/lists/peopleGroupRecords.js'
|
||
import { getApiBaseUrl } from '@map/api/apiBaseUrl.js'
|
||
import { getRuntimeLocale, detectLocale } from '@map/i18n/index.js'
|
||
import countryData from '@map/data/country-posters.json'
|
||
import { makeCountrySheet } from '@map/packet/countrySheet.js'
|
||
import { listConfigurations, FAMILY_KEYS, SAMPLE_COUNTRY_ID } from './src/templates.js'
|
||
import FullDetailsDialog from './src/FullDetailsDialog.vue'
|
||
|
||
const profileConfig = inject('profileConfig', ref(null))
|
||
const instanceId = computed(() => profileConfig.value?.instanceId || 'poster-design-styles')
|
||
const assetsBase = computed(() => resolveAssetsBase(profileConfig.value))
|
||
const t = useOptionalT('')
|
||
|
||
const configs = listConfigurations()
|
||
const byName = new Map(configs.map((c) => [c.name, c]))
|
||
|
||
// A card's title: the template's name, then its size. Card stocks name the stock the way the wall's own
|
||
// stock picker does (the noun translates, the size is printed as a printer's box prints it).
|
||
function stockLabel(s) {
|
||
if (!s.noun) return s.label
|
||
const noun = s.noun === 'postcard' ? t('wall.stockPostcard') : t('wall.stockCard')
|
||
const dims = `${s.w} × ${s.h} in`
|
||
return s.side ? `${noun} ${dims} (${t('wall.stockLandscape')})` : `${noun} ${dims}`
|
||
}
|
||
const labelOf = (c) => (c.stock ? `${t(c.labelKey)}: ${stockLabel(c.stock)}` : `${t(c.labelKey)}: ${c.size}`)
|
||
|
||
// No `count`: these are templates, not lists of people, so the sticker reads "Show full details" alone.
|
||
const items = computed(() => configs.map((c) => ({ id: c.name, label: labelOf(c) })))
|
||
const groupBy = (item) => t(FAMILY_KEYS[byName.get(item.id)?.family] || FAMILY_KEYS.mapSheet)
|
||
|
||
// ── drawing a configuration from the library ──────────────────────────────────────────────────
|
||
const sample = (countryData.countries || []).find((c) => c.id === SAMPLE_COUNTRY_ID)
|
||
const recordsFor = createPeopleGroupRecords({
|
||
apiBase: () => profileConfig.value?.apiBaseUrl || getApiBaseUrl(),
|
||
lang: () => getRuntimeLocale() || detectLocale(),
|
||
profileConfig,
|
||
})
|
||
// The ONE path every surface draws a country sheet through (the Country Posters wall, the Regions tab),
|
||
// so the sample carries its region hue and its "map N of M" exactly as the wall prints it.
|
||
const sheetFor = makeCountrySheet({ records: recordsFor, assetsBase: () => assetsBase.value, countries: countryData.countries || [] })
|
||
const countrySheet = (format) => (sample ? sheetFor(sample, format) : Promise.resolve(''))
|
||
const ctx = { countrySheet }
|
||
|
||
// The printed document's size, measured when it is drawn: what "file size" means in the details.
|
||
const fileBytes = ref({})
|
||
async function renderDocument(name) {
|
||
const c = byName.get(name)
|
||
if (!c || !c.render) return ''
|
||
const html = await c.render(ctx)
|
||
if (html) fileBytes.value = { ...fileBytes.value, [name]: new Blob([html]).size }
|
||
return html || ''
|
||
}
|
||
|
||
// previewSrc: the thumbnail, rasterised from the drawn document at a quarter scale (a 1500px sheet
|
||
// becomes ~375px, the card's size). A render that comes out blank is refused, so the card shows
|
||
// "preview unavailable" rather than a black rectangle.
|
||
async function previewSrc(item) {
|
||
const html = await renderDocument(item.id)
|
||
if (!html) return null
|
||
const { rasterPoster, looksRendered } = await import('@map/packet/poster-raster.js')
|
||
const blob = await rasterPoster(html, { scale: 0.25 })
|
||
if (!(await looksRendered(blob))) return null
|
||
return URL.createObjectURL(blob)
|
||
}
|
||
|
||
// The card's print button prints THIS configuration: the format the wall asks for is ignored on
|
||
// purpose, because the card already names its size.
|
||
const sheet = (item) => renderDocument(item.id)
|
||
|
||
// ── "Show full details" ───────────────────────────────────────────────────────────────────────
|
||
const open = ref(null)
|
||
const openLabel = computed(() => (open.value ? labelOf(open.value) : ''))
|
||
function showDetails(item) { open.value = byName.get(item.id) || null }
|
||
|
||
const heading = computed(() => t('posterDesign.styles.heading'))
|
||
// PosterWall renders the intro as HTML, so plain catalogue text is escaped first.
|
||
const escHtml = (s) => String(s).replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>')
|
||
const intro = computed(() => escHtml(t('posterDesign.styles.intro')))
|
||
|
||
// Child components' <style> blocks are dropped by defineCustomElement, so this profile carries the
|
||
// dialog's CSS too. The floor sits on :host, where a shell that sizes its pane can override it.
|
||
useShadowStyles(`
|
||
:host{display:block;width:100%;height:100%;min-height:420px}
|
||
.pd-styles{position:absolute;inset:0;min-height:0}
|
||
.pd-dialog{box-sizing:border-box;border:1px solid #d1d4cd;border-radius:12px;padding:20px 22px;max-width:min(560px,calc(100vw - 32px));
|
||
width:100%;color:#3b463d;background:#fff;font:14px/1.45 -apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,system-ui,sans-serif;
|
||
box-shadow:0 12px 40px rgba(8,12,16,.25)}
|
||
.pd-dialog::backdrop{background:rgba(8,12,16,.55)}
|
||
.pd-dialog__title{margin:0 0 14px;font-size:18px;font-weight:700;line-height:1.3}
|
||
.pd-dialog__list{display:grid;grid-template-columns:max-content 1fr;gap:8px 16px;margin:0}
|
||
.pd-dialog__list dt{color:#6d766e;font-weight:600}
|
||
.pd-dialog__list dd{margin:0;min-width:0;overflow-wrap:anywhere}
|
||
.pd-dialog__list code{font:12.5px/1.4 ui-monospace,SFMono-Regular,Menlo,monospace;background:#f3f3f1;border-radius:4px;padding:1px 5px}
|
||
.pd-dialog__name{font-weight:700}
|
||
.pd-dialog__copy,.pd-dialog__close{font:inherit;border-radius:8px;border:1px solid #d1d4cd;background:#f3f3f1;color:#3b463d;
|
||
padding:4px 10px;cursor:pointer;margin-inline-start:8px}
|
||
.pd-dialog__note{margin:14px 0 0;color:#6d766e}
|
||
.pd-dialog__agent{margin:14px 0 0;padding:10px 12px;border-radius:8px;background:#f3f3f1;border-inline-start:4px solid #92b195}
|
||
.pd-dialog__actions{display:flex;justify-content:flex-end;margin:16px 0 0}
|
||
.pd-dialog__close{margin:0;background:#92b195;border-color:#92b195;color:#fff;font-weight:600;padding:6px 16px}
|
||
@media (max-width:480px){.pd-dialog__list{grid-template-columns:1fr}.pd-dialog__list dd{margin-bottom:6px}}
|
||
`, 'poster-design-styles')
|
||
</script>
|
||
|
||
<template>
|
||
<div class="pd-styles">
|
||
<PosterWall
|
||
:items="items"
|
||
:group-by="groupBy"
|
||
:instance-id="instanceId"
|
||
:assets-base="assetsBase"
|
||
:heading="heading"
|
||
:intro="intro"
|
||
unit=""
|
||
:collapsible="true"
|
||
:header-action="{ labelKey: 'posterDesign.showFullDetails', ariaKey: 'posterDesign.showFullDetailsAria' }"
|
||
:preview-src="previewSrc"
|
||
:sheet="sheet"
|
||
:hide-prayer-list-when-soon="true"
|
||
download-prefix="doxa-poster-design"
|
||
@header-action="showDetails"
|
||
/>
|
||
<FullDetailsDialog :config="open" :label="openLabel" :file-bytes="open ? (fileBytes[open.name] ?? null) : null" @close="open = null" />
|
||
</div>
|
||
</template>
|