doxa-maps/plugin-designs/poster-design/poster-sizes.vue
Doxa Maps 58c6bb28af Poster Design names sizes, and plugins' designs, in the reader's language
Styles and Sizes now name every size the one way (src/sizeText.js, shared):
templates.js carries each size as data (inches, side lanes, panels, paper),
and posterDesign.sizes gains sideLanes, packetPage1, panels and anySize in
all 11 locales, so Arabic readers no longer see "5 x 8 ft, side lanes" or
"Letter, portrait". A plugin's declared size (a ladder key, letter, a4) is
named through the shared ladder.

A plugin's design title came from its labelKey, but Poster Design's bundle
ships only the catalogue areas its own code names, so a plugin's own area
(upgMap, ...) read English. internal/profile-meta.mjs now resolves each
block's labelKey in every locale at build time and carries the titles in
manifest.json (posterDesignLabels, only beside a posterDesign block), and
Styles reads the reader's language from there.
2026-10-02 13:32:19 +00:00

159 lines
8.2 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<script setup>
/**
* poster-sizes.vue: every poster size, drawn to scale, with the sheets each one prints on.
*
* @element poster-design
* @description Every poster size the print picker offers, from standard paper to 5 × 8 ft, drawn to scale.
* A size that prints on several sheets shows them pulled apart with the red strip where the next sheet is
* glued on, as the Wall of People Groups previews its 5 × 8 ft poster.
*
* The sizes are the ONE shared ladder (library/constants/posterSizes.js POSTER_LADDER, in
* POSTER_LADDER_ORDER), never a second list: a size the ladder gains appears here by itself. Each size's
* `sheets` goes straight into the library's PanelSheets, the same component and the same geometry
* (panelSheets.js) the print uses. What fills the sheets is the 3 × 3 country poster's LAYOUT drawn at that
* size (src/layoutSvg.js), labelled as a layout, because the renderer does not draw the large sizes yet.
*/
import { computed, ref, onMounted, onBeforeUnmount } from 'vue'
import PanelSheets from '@map/components/poster/PanelSheets.vue'
import { planPanels, screenLayout } from '@map/components/poster/panelSheets.js'
import { POSTER_LADDER, POSTER_LADDER_ORDER, paperFor } from '@map/constants/posterSizes.js'
import { useOptionalT, useOptionalI18n } from '@map/composables/useOptionalT.js'
import { useShadowStyles } from '@map/composables/useShadowStyles.js'
import { layoutSvgUrl } from './src/layoutSvg.js'
import { num, dimsText } from './src/sizeText.js'
const t = useOptionalT('')
const { locale } = useOptionalI18n('')
// Sizes are named the one way Poster Design names them (src/sizeText.js, shared with Styles).
const n = num
const dims = (w, h) => dimsText(w, h, t)
// Standard printer paper is Letter or A4 by the reader's region, as the print picker decides it.
const paperIsA4 = computed(() => {
const l = locale && (locale.value ?? locale)
const tag = (typeof navigator !== 'undefined' && navigator.language) || (typeof l === 'string' ? l : '') || 'en'
return paperFor(tag) === 'a4'
})
const sizes = computed(() => POSTER_LADDER_ORDER.map((key) => POSTER_LADDER[key]).filter(Boolean).map((s) => {
const { sheetW, sheetH, count, direction, joinIn } = s.sheets
return {
key: s.id,
w: s.inches.w,
h: s.inches.h,
sheets: { sheetW, sheetH, count, direction, joinIn },
title: s.paper ? t('posterDesign.sizes.paper') : dims(s.inches.w, s.inches.h),
subtitle: s.paper ? t(paperIsA4.value ? 'posterDesign.sizes.paperA4' : 'posterDesign.sizes.paperLetter') : '',
count,
sheetDims: dims(sheetW, sheetH),
overlap: count > 1 ? t('posterDesign.sizes.dimsInOne', { v: n(joinIn) }) : '',
photos: !!s.photos,
shop: s.path === 'pdf',
src: layoutSvgUrl(s.inches.w, s.inches.h),
}
}))
// The box PanelSheets draws, in inches: its own screenLayout of the same plan, so a fit computed here is the fit
// it renders (each sheet whole, pulled apart by the gap, which is taller than the finished poster).
const LINEUP_GAP_IN = 1.5
const CARD_GAP_IN = 0.75
function world(s, gapIn) {
const L = screenLayout(planPanels(s.sheets), { gapIn, pxPerIn: 1 })
return { w: L.worldW, h: L.worldH }
}
// THE LINEUP: every size at ONE scale, standing on one baseline, so 5 × 8 ft looks as big beside paper as it
// is. Each size gets an equal column; the scale is the largest at which the tallest drawing is LINEUP_H and
// the widest still fits its column, so on a phone every size stays in view and stays to scale.
const LINEUP_H = 240 // px for the tallest size, when there is room
const COL_GAP = 12
const rowEl = ref(null)
const rowW = ref(1000)
let ro = null
onMounted(() => {
if (!rowEl.value || typeof ResizeObserver === 'undefined') return
ro = new ResizeObserver(([e]) => { if (e && e.contentRect.width) rowW.value = e.contentRect.width })
ro.observe(rowEl.value)
})
onBeforeUnmount(() => { ro?.disconnect(); ro = null })
const pxPerIn = computed(() => {
const ws = sizes.value.map((s) => world(s, LINEUP_GAP_IN))
if (!ws.length) return 1
const col = (rowW.value - COL_GAP * (ws.length - 1)) / ws.length
return Math.min(LINEUP_H / Math.max(...ws.map((w) => w.h)), col / Math.max(...ws.map((w) => w.w)))
})
const lineupW = (s) => `${Math.max(4, world(s, LINEUP_GAP_IN).w * pxPerIn.value)}px`
useShadowStyles(`
:host{display:block;width:100%;height:100%;min-height:420px}
.pd-sizes{position:absolute;inset:0;overflow-y:auto;padding:24px 20px 40px;background:#f3f3f1;color:#3b463d;
font:14px/1.5 -apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,system-ui,sans-serif;box-sizing:border-box}
.pd-sizes__h{margin:0 0 6px;font-size:22px;font-weight:700}
.pd-sizes__intro{margin:0 0 18px;max-width:70ch;color:#6d766e}
.pd-lineup{background:#fff;border:1px solid #d1d4cd;border-radius:12px;padding:16px 16px 10px;margin:0 0 24px}
.pd-lineup__h{margin:0 0 12px;font-size:14px;font-weight:700;color:#6d766e}
.pd-lineup__row{display:grid;grid-auto-flow:column;grid-auto-columns:minmax(0,1fr);align-items:end;gap:12px;
padding-bottom:6px;border-bottom:2px solid #d1d4cd}
.pd-lineup__item{display:flex;flex-direction:column;align-items:center;gap:6px;min-width:0}
.pd-lineup__label{font-size:12px;font-weight:600;text-align:center;line-height:1.25;overflow-wrap:anywhere}
.pd-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(250px,1fr));gap:18px}
.pd-card{background:#fff;border:1px solid #d1d4cd;border-radius:12px;padding:14px;display:flex;flex-direction:column;gap:10px;min-width:0}
.pd-card__art{display:flex;justify-content:center;align-items:flex-end;height:220px;background:#f3f3f1;border-radius:8px;padding:10px;box-sizing:border-box}
.pd-card__art > .pd-card__fit{height:100%;display:flex;align-items:flex-end;justify-content:center;width:100%}
.pd-card__title{margin:0;font-size:17px;font-weight:700}
.pd-card__sub{margin:-8px 0 0;color:#6d766e;font-size:13px}
.pd-card__rows{display:grid;grid-template-columns:max-content 1fr;gap:4px 12px;margin:0;font-size:13px}
.pd-card__rows dt{color:#6d766e}
.pd-card__rows dd{margin:0}
.pd-note{margin:20px 0 0;max-width:70ch;color:#6d766e;font-size:13px}
`, 'poster-design-sizes')
// A card's art box leaves 200 × 222 px for the drawing; it fits by whichever side binds first.
const ART_H = 200
const ART_W = 222
function artWidth(s) {
const { w, h } = world(s, CARD_GAP_IN)
return `${Math.floor(w * Math.min(ART_H / h, ART_W / w))}px`
}
</script>
<template>
<div class="pd-sizes">
<h2 class="pd-sizes__h">{{ t('posterDesign.sizes.heading') }}</h2>
<p class="pd-sizes__intro">{{ t('posterDesign.sizes.intro') }}</p>
<section class="pd-lineup" :aria-label="t('posterDesign.sizes.lineup')">
<h3 class="pd-lineup__h">{{ t('posterDesign.sizes.lineup') }}</h3>
<div ref="rowEl" class="pd-lineup__row">
<div v-for="s in sizes" :key="s.key" class="pd-lineup__item">
<div :style="{ width: lineupW(s) }">
<PanelSheets v-bind="s.sheets" :gap-in="LINEUP_GAP_IN" :src="s.src" />
</div>
<span class="pd-lineup__label">{{ s.title }}</span>
</div>
</div>
</section>
<div class="pd-grid">
<article v-for="s in sizes" :key="s.key" class="pd-card" :data-size="s.key">
<div class="pd-card__art">
<div class="pd-card__fit">
<div :style="{ width: artWidth(s) }"><PanelSheets v-bind="s.sheets" :gap-in="CARD_GAP_IN" :src="s.src" /></div>
</div>
</div>
<h3 class="pd-card__title">{{ s.title }}</h3>
<p v-if="s.subtitle" class="pd-card__sub">{{ s.subtitle }}</p>
<dl class="pd-card__rows">
<dt>{{ t('posterDesign.sizes.rowSheets') }}</dt><dd>{{ s.count }}</dd>
<template v-if="s.count > 1">
<dt>{{ t('posterDesign.sizes.rowSheet') }}</dt><dd>{{ s.sheetDims }}</dd>
<dt>{{ t('posterDesign.sizes.rowOverlap') }}</dt><dd>{{ s.overlap }}</dd>
</template>
<dt>{{ t('posterDesign.sizes.rowPhotos') }}</dt><dd>{{ s.photos ? t('posterDesign.sizes.yes') : t('posterDesign.sizes.no') }}</dd>
<dt>{{ t('posterDesign.sizes.rowPrint') }}</dt><dd>{{ s.shop ? t('posterDesign.sizes.printShop') : t('posterDesign.sizes.printHome') }}</dd>
</dl>
</article>
</div>
<p class="pd-note">{{ t('posterDesign.sizes.layoutNote') }}</p>
</div>
</template>