Poster Design: stored previews show at once, and one card for each poster kind

Styles drew every preview live: 27.7 s to the last of 16 cards on the pilot,
~9 s per UPG100 zone row. A configuration whose exact thumbnail is already
stored (same format, same sample, made by the library's generator) now
shows it at once: the country 3 x 3 (Nepal), the UPG100 3 x 3 zone poster
and the UPG100 packet's page 1. The live render stays one click away.

Three poster kinds that exist only as stored posters get a card each, from
their stored poster: the city poster (key-cities, Hyderabad), the India
state poster (Uttar Pradesh) and the UPG100 prayer cards. Each names its
source plugin, its renderer and its stored file; its print is that stored
poster on a page.

check:poster-design-catalogue arm 9: every preview is a file a plugin ships
(poisoned with a missing one). Keys in 11 locales; three ledger names.
This commit is contained in:
Doxa Maps 2026-10-03 05:57:09 +00:00
parent 789d90eb2f
commit 6dbdd71826
17 changed files with 153 additions and 38 deletions

View file

@ -42,7 +42,8 @@
"mapPrint": "طباعة الخريطة",
"plugins": "من إضافات أخرى",
"experimental": "تجريبية (قيد العمل)",
"upgZone": "ملصقات مناطق UPG100"
"upgZone": "ملصقات مناطق UPG100",
"cityPoster": "ملصقات المدن"
},
"tpl": {
"mapSheetCountry": "صفحة خريطة الدولة",
@ -53,7 +54,10 @@
"blessPanels": "ألواح جدار البركة",
"mapPrint": "طباعة الخريطة",
"upgZonePoster": "ملصق منطقة UPG100",
"upgZonePacket": "حزمة صلاة UPG100، الصفحة 1"
"upgZonePacket": "حزمة صلاة UPG100، الصفحة 1",
"cityPoster": "ملصق مدينة",
"indiaStatePoster": "ملصق ولاية هندية",
"upgZoneCards": "بطاقات صلاة UPG100"
},
"missing": {
"bless": "الأوراق وشرائط اللصق ومواضع صفوف البطاقات مأخوذة من هندسة الألواح المشتركة؛ وتُعرض البطاقات كتخطيط، لأن جدار البركة يرسم بطاقاته في إضافته الخاصة.",
@ -71,7 +75,8 @@
"renderer": "المولّد",
"legacy": "مقاس قديم: ما زال يُطبع، لكن أداة اختيار الطباعة لم تعد تعرضه.",
"forAgents": "لاستخدام هذا القالب، قل للوكيل: استخدم الإعداد {name}.",
"openPage": "افتح صفحته"
"openPage": "افتح صفحته",
"storedPreview": "المعاينة المحفوظة"
},
"templateCount": "لا قوالب | قالب واحد | قالبان | {n} قوالب | {n} قالبًا | {n} قالب",
"fromPlugin": "تصنعه إضافته الخاصة. اعرض كل التفاصيل لتراه مباشرة."

View file

@ -42,7 +42,8 @@
"mapPrint": "Kartendruck",
"plugins": "Aus anderen Plugins",
"experimental": "Experimentell (in Arbeit)",
"upgZone": "UPG100-Gebietsplakate"
"upgZone": "UPG100-Gebietsplakate",
"cityPoster": "Stadtplakate"
},
"tpl": {
"mapSheetCountry": "Länder-Kartenblatt",
@ -53,7 +54,10 @@
"blessPanels": "Tafeln der Segnen-Wand",
"mapPrint": "Kartendruck",
"upgZonePoster": "UPG100-Gebietsplakat",
"upgZonePacket": "UPG100-Gebetsmappe, Seite 1"
"upgZonePacket": "UPG100-Gebetsmappe, Seite 1",
"cityPoster": "Stadtplakat",
"indiaStatePoster": "Plakat eines indischen Bundesstaats",
"upgZoneCards": "UPG100-Gebetskarten"
},
"missing": {
"bless": "Die Bögen, die Klebestreifen und die Lage der Kartenreihen stammen aus der gemeinsamen Tafelgeometrie; die Karten sind als Layout dargestellt, weil die Segnen-Wand ihre Karten in ihrem eigenen Plugin zeichnet.",
@ -71,7 +75,8 @@
"renderer": "Generator",
"legacy": "Alte Größe: Sie wird noch gedruckt, aber die Druckauswahl bietet sie nicht mehr an.",
"forAgents": "Um diese Vorlage zu verwenden, sagen Sie einem Agenten: verwende die Konfiguration {name}.",
"openPage": "Seite öffnen"
"openPage": "Seite öffnen",
"storedPreview": "Gespeicherte Vorschau"
},
"templateCount": "{n} Vorlage | {n} Vorlagen",
"fromPlugin": "Wird von seinem eigenen Plugin erstellt. „Alle Details anzeigen“ zeigt es live."

View file

@ -42,7 +42,8 @@
"mapPrint": "Map print",
"plugins": "From other plugins",
"experimental": "Experimental (in progress)",
"upgZone": "UPG100 zone posters"
"upgZone": "UPG100 zone posters",
"cityPoster": "City posters"
},
"tpl": {
"mapSheetCountry": "Country map sheet",
@ -53,7 +54,10 @@
"blessPanels": "Bless wall panels",
"mapPrint": "Map print",
"upgZonePoster": "UPG100 zone poster",
"upgZonePacket": "UPG100 prayer packet, page 1"
"upgZonePacket": "UPG100 prayer packet, page 1",
"cityPoster": "City poster",
"indiaStatePoster": "India state poster",
"upgZoneCards": "UPG100 prayer cards"
},
"missing": {
"bless": "The sheets, the glue strips and where the card rows go come from the shared panel geometry; the cards are shown as a layout, because the Bless wall draws its cards in its own plugin.",
@ -71,7 +75,8 @@
"renderer": "Renderer",
"legacy": "Legacy size: it still prints, but the print picker no longer offers it.",
"forAgents": "To use this template, tell an agent: use configuration {name}.",
"openPage": "Open its page"
"openPage": "Open its page",
"storedPreview": "Stored preview"
},
"templateCount": "{n} templates | {n} template | {n} templates",
"fromPlugin": "Made by its own plugin. Show full details shows it live."

View file

@ -42,7 +42,8 @@
"mapPrint": "Impresión del mapa",
"plugins": "De otros complementos",
"experimental": "Experimentales (en desarrollo)",
"upgZone": "Carteles de zonas UPG100"
"upgZone": "Carteles de zonas UPG100",
"cityPoster": "Carteles de ciudades"
},
"tpl": {
"mapSheetCountry": "Hoja de mapa del país",
@ -53,7 +54,10 @@
"blessPanels": "Paneles del muro Bendecir",
"mapPrint": "Impresión del mapa",
"upgZonePoster": "Cartel de zona UPG100",
"upgZonePacket": "Paquete de oración UPG100, página 1"
"upgZonePacket": "Paquete de oración UPG100, página 1",
"cityPoster": "Cartel de ciudad",
"indiaStatePoster": "Cartel de un estado de la India",
"upgZoneCards": "Tarjetas de oración UPG100"
},
"missing": {
"bless": "Las hojas, las franjas de pegado y el lugar de las filas de tarjetas vienen de la geometría de paneles compartida; las tarjetas se muestran como un diseño, porque el muro Bendecir dibuja sus tarjetas en su propio complemento.",
@ -71,7 +75,8 @@
"renderer": "Generador",
"legacy": "Tamaño heredado: todavía se imprime, pero el selector de impresión ya no lo ofrece.",
"forAgents": "Para usar esta plantilla, dile a un agente: usa la configuración {name}.",
"openPage": "Abrir su página"
"openPage": "Abrir su página",
"storedPreview": "Vista previa guardada"
},
"templateCount": "{n} plantilla | {n} de plantillas | {n} plantillas",
"fromPlugin": "Lo crea su propio complemento. Mostrar todos los detalles lo muestra en vivo."

View file

@ -42,7 +42,8 @@
"mapPrint": "Impression de la carte",
"plugins": "D’autres plugins",
"experimental": "Expérimentaux (en cours)",
"upgZone": "Affiches de zones UPG100"
"upgZone": "Affiches de zones UPG100",
"cityPoster": "Affiches de villes"
},
"tpl": {
"mapSheetCountry": "Feuille de carte du pays",
@ -53,7 +54,10 @@
"blessPanels": "Panneaux du mur Bénir",
"mapPrint": "Impression de la carte",
"upgZonePoster": "Affiche de zone UPG100",
"upgZonePacket": "Dossier de prière UPG100, page 1"
"upgZonePacket": "Dossier de prière UPG100, page 1",
"cityPoster": "Affiche de ville",
"indiaStatePoster": "Affiche d’un État indien",
"upgZoneCards": "Cartes de prière UPG100"
},
"missing": {
"bless": "Les feuilles, les bandes de collage et l'emplacement des rangées de cartes viennent de la géométrie de panneaux partagée ; les cartes sont montrées comme une mise en page, car le mur Bénir dessine ses cartes dans son propre module.",
@ -71,7 +75,8 @@
"renderer": "Générateur",
"legacy": "Format ancien : il s'imprime encore, mais le sélecteur d'impression ne le propose plus.",
"forAgents": "Pour utiliser ce modèle, dites à un agent : utilise la configuration {name}.",
"openPage": "Ouvrir sa page"
"openPage": "Ouvrir sa page",
"storedPreview": "Aperçu enregistré"
},
"templateCount": "{n} modèle | {n} de modèles | {n} modèles",
"fromPlugin": "Créé par son propre plugin. Afficher tous les détails le montre en direct."

View file

@ -42,7 +42,8 @@
"mapPrint": "मानचित्र प्रिंट",
"plugins": "अन्य प्लगइन से",
"experimental": "प्रयोगात्मक (काम जारी)",
"upgZone": "UPG100 क्षेत्र पोस्टर"
"upgZone": "UPG100 क्षेत्र पोस्टर",
"cityPoster": "शहर के पोस्टर"
},
"tpl": {
"mapSheetCountry": "देश का मानचित्र पृष्ठ",
@ -53,7 +54,10 @@
"blessPanels": "आशीष दीवार के पैनल",
"mapPrint": "मानचित्र प्रिंट",
"upgZonePoster": "UPG100 क्षेत्र का पोस्टर",
"upgZonePacket": "UPG100 प्रार्थना पैकेट, पेज 1"
"upgZonePacket": "UPG100 प्रार्थना पैकेट, पेज 1",
"cityPoster": "शहर का पोस्टर",
"indiaStatePoster": "भारतीय राज्य का पोस्टर",
"upgZoneCards": "UPG100 प्रार्थना कार्ड"
},
"missing": {
"bless": "शीटें, चिपकाने की पट्टियाँ और कार्ड-पंक्तियों की जगह साझा पैनल ज्यामिति से आती हैं; कार्ड लेआउट के रूप में दिखाए गए हैं, क्योंकि आशीष दीवार अपने कार्ड अपने प्लगइन में बनाती है।",
@ -71,7 +75,8 @@
"renderer": "जनरेटर",
"legacy": "पुराना आकार: यह अब भी प्रिंट होता है, पर प्रिंट चयनकर्ता अब इसे नहीं दिखाता।",
"forAgents": "यह टेम्पलेट इस्तेमाल करने के लिए किसी एजेंट से कहें: कॉन्फ़िगरेशन {name} इस्तेमाल करो।",
"openPage": "इसका पेज खोलें"
"openPage": "इसका पेज खोलें",
"storedPreview": "सहेजा गया पूर्वावलोकन"
},
"templateCount": "{n} टेम्पलेट | {n} टेम्पलेट",
"fromPlugin": "यह अपने ही प्लगइन से बनता है। पूरा विवरण दिखाएँ इसे लाइव दिखाता है।"

View file

@ -42,7 +42,8 @@
"mapPrint": "Stampa della mappa",
"plugins": "Da altri plugin",
"experimental": "Sperimentali (in corso)",
"upgZone": "Poster delle zone UPG100"
"upgZone": "Poster delle zone UPG100",
"cityPoster": "Poster delle città"
},
"tpl": {
"mapSheetCountry": "Foglio mappa del paese",
@ -53,7 +54,10 @@
"blessPanels": "Pannelli del muro Benedici",
"mapPrint": "Stampa della mappa",
"upgZonePoster": "Poster di zona UPG100",
"upgZonePacket": "Fascicolo di preghiera UPG100, pagina 1"
"upgZonePacket": "Fascicolo di preghiera UPG100, pagina 1",
"cityPoster": "Poster di città",
"indiaStatePoster": "Poster di uno stato indiano",
"upgZoneCards": "Schede di preghiera UPG100"
},
"missing": {
"bless": "I fogli, le strisce da incollare e la posizione delle file di schede vengono dalla geometria dei pannelli condivisa; le schede sono mostrate come impaginazione, perché il muro Benedici disegna le sue schede nel proprio plugin.",
@ -71,7 +75,8 @@
"renderer": "Generatore",
"legacy": "Formato precedente: si stampa ancora, ma il selettore di stampa non lo offre più.",
"forAgents": "Per usare questo modello, di' a un agente: usa la configurazione {name}.",
"openPage": "Apri la sua pagina"
"openPage": "Apri la sua pagina",
"storedPreview": "Anteprima salvata"
},
"templateCount": "{n} modello | {n} di modelli | {n} modelli",
"fromPlugin": "Creato dal suo plugin. Mostra tutti i dettagli lo mostra dal vivo."

View file

@ -42,7 +42,8 @@
"mapPrint": "Impressão do mapa",
"plugins": "De outros plugins",
"experimental": "Experimentais (em andamento)",
"upgZone": "Cartazes de zonas UPG100"
"upgZone": "Cartazes de zonas UPG100",
"cityPoster": "Cartazes de cidades"
},
"tpl": {
"mapSheetCountry": "Folha de mapa do país",
@ -53,7 +54,10 @@
"blessPanels": "Painéis do mural Abençoar",
"mapPrint": "Impressão do mapa",
"upgZonePoster": "Cartaz de zona UPG100",
"upgZonePacket": "Pacote de oração UPG100, página 1"
"upgZonePacket": "Pacote de oração UPG100, página 1",
"cityPoster": "Cartaz de cidade",
"indiaStatePoster": "Cartaz de um estado da Índia",
"upgZoneCards": "Cartões de oração UPG100"
},
"missing": {
"bless": "As folhas, as faixas de colagem e a posição das fileiras de cartões vêm da geometria de painéis compartilhada; os cartões aparecem como um layout, porque o mural Abençoar desenha os seus cartões no seu próprio plugin.",
@ -71,7 +75,8 @@
"renderer": "Gerador",
"legacy": "Tamanho antigo: ainda imprime, mas o seletor de impressão já não o oferece.",
"forAgents": "Para usar este modelo, diga a um agente: use a configuração {name}.",
"openPage": "Abrir a página"
"openPage": "Abrir a página",
"storedPreview": "Pré-visualização guardada"
},
"templateCount": "{n} modelo | {n} de modelos | {n} modelos",
"fromPlugin": "Feito pelo próprio plugin. Mostrar todos os detalhes o exibe ao vivo."

View file

@ -42,7 +42,8 @@
"mapPrint": "Tipărirea hărții",
"plugins": "Din alte pluginuri",
"experimental": "Experimentale (în lucru)",
"upgZone": "Postere de zone UPG100"
"upgZone": "Postere de zone UPG100",
"cityPoster": "Postere de orașe"
},
"tpl": {
"mapSheetCountry": "Foaie de hartă a țării",
@ -53,7 +54,10 @@
"blessPanels": "Panourile peretelui Binecuvântează",
"mapPrint": "Tipărirea hărții",
"upgZonePoster": "Poster de zonă UPG100",
"upgZonePacket": "Pachet de rugăciune UPG100, pagina 1"
"upgZonePacket": "Pachet de rugăciune UPG100, pagina 1",
"cityPoster": "Poster de oraș",
"indiaStatePoster": "Poster al unui stat indian",
"upgZoneCards": "Cartonașe de rugăciune UPG100"
},
"missing": {
"bless": "Colile, benzile de lipire și locul rândurilor de cartonașe vin din geometria comună a panourilor; cartonașele sunt arătate ca machetă, pentru că peretele Binecuvântează își desenează cartonașele în propriul modul.",
@ -71,7 +75,8 @@
"renderer": "Generator",
"legacy": "Dimensiune veche: încă se tipărește, dar selectorul de tipărire nu o mai oferă.",
"forAgents": "Ca să folosești acest șablon, spune-i unui agent: folosește configurația {name}.",
"openPage": "Deschide pagina"
"openPage": "Deschide pagina",
"storedPreview": "Previzualizare salvată"
},
"templateCount": "{n} șablon | {n} șabloane | {n} de șabloane",
"fromPlugin": "Este realizat de propriul plugin. Afișează toate detaliile îl arată în direct."

View file

@ -42,7 +42,8 @@
"mapPrint": "Печать карты",
"plugins": "Из других плагинов",
"experimental": "Экспериментальные (в работе)",
"upgZone": "Плакаты зон UPG100"
"upgZone": "Плакаты зон UPG100",
"cityPoster": "Плакаты городов"
},
"tpl": {
"mapSheetCountry": "Лист карты страны",
@ -53,7 +54,10 @@
"blessPanels": "Панели стены «Благословить»",
"mapPrint": "Печать карты",
"upgZonePoster": "Плакат зоны UPG100",
"upgZonePacket": "Молитвенный пакет UPG100, страница 1"
"upgZonePacket": "Молитвенный пакет UPG100, страница 1",
"cityPoster": "Плакат города",
"indiaStatePoster": "Плакат штата Индии",
"upgZoneCards": "Молитвенные карточки UPG100"
},
"missing": {
"bless": "Листы, полосы для склейки и места рядов карточек взяты из общей геометрии панелей; карточки показаны как макет, потому что стена «Благословить» рисует свои карточки в собственном плагине.",
@ -71,7 +75,8 @@
"renderer": "Генератор",
"legacy": "Устаревший размер: он ещё печатается, но в выборе печати его больше нет.",
"forAgents": "Чтобы использовать этот шаблон, скажите агенту: используй конфигурацию {name}.",
"openPage": "Открыть страницу"
"openPage": "Открыть страницу",
"storedPreview": "Сохранённый эскиз"
},
"templateCount": "{n} шаблон | {n} шаблона | {n} шаблонов | {n} шаблонов",
"fromPlugin": "Создаётся собственным плагином. «Показать все подробности» покажет его вживую."

View file

@ -42,7 +42,8 @@
"mapPrint": "地图打印",
"plugins": "来自其他插件",
"experimental": "实验性(进行中)",
"upgZone": "UPG100 区域海报"
"upgZone": "UPG100 区域海报",
"cityPoster": "城市海报"
},
"tpl": {
"mapSheetCountry": "国家地图页",
@ -53,7 +54,10 @@
"blessPanels": "祝福墙展板",
"mapPrint": "地图打印",
"upgZonePoster": "UPG100 区域海报(单张)",
"upgZonePacket": "UPG100 祷告资料包,第 1 页"
"upgZonePacket": "UPG100 祷告资料包,第 1 页",
"cityPoster": "城市海报(单张)",
"indiaStatePoster": "印度邦海报",
"upgZoneCards": "UPG100 祷告卡"
},
"missing": {
"bless": "纸张、粘贴条以及卡片行的位置都来自共享的拼板几何;卡片以版式示意,因为祝福墙在其自己的插件中绘制卡片。",
@ -71,7 +75,8 @@
"renderer": "生成器",
"legacy": "旧尺寸:仍可打印,但打印选择器已不再提供。",
"forAgents": "要使用此模板,请告诉代理:使用配置 {name}。",
"openPage": "打开其页面"
"openPage": "打开其页面",
"storedPreview": "已保存的预览"
},
"templateCount": "{n} 个模板",
"fromPlugin": "由其自身插件生成。点击“显示全部详情”可实时查看。"

View file

@ -620,8 +620,8 @@
"check:print-frame-reclaim": "a print frame is kept until it has printed: openPrintFrame reclaims it 60 s after its load (not after its creation), at afterprint, or at a 10-minute ceiling, and printImageBlob revokes its object URL on the same clock. Fake timers; two-sided (the pre-fix copy fails). The Wall poster loads 132 MB of photos, and the old 60 s-from-creation clock removed the frame before it could print.",
"check:poster-card-fits": "a poster card's print-options row must stay inside its own card. Reported by the repo agent off a 1440px screenshot: 27/27 cards overflowing by 110px, 36 neighbour intrusions. .pfs already had width:82%, so SOURCE READING SAID IT WAS FINE — flex items default to min-width:auto and refuse to shrink. Asserts BOTH no-overlap AND that the controls keep a usable box, because a row whose buttons collapsed to nothing would also \"not overflow\". Sweeps 390/768/1024/1440/1920px and asserts the row sits BELOW the preview too — measuring only left/right edges is how it passed a driver-visible regression. CARD_FIT_WIDTH overrides the list. SKIPs when nothing is served.",
"check:poster-degrades": "with NO stored poster, NO source and NO generator (forced in the browser: every posters/*.webp and *.html answers 404, lib/poster-render.js is aborted), nothing must print and the reader must be told why. Measures any served tree; NOT MEASURED without a wall url.",
"check:poster-design-catalogue": "Poster Design's configuration catalogue (plugin-designs/poster-design/src/templates.js) says only true things about the library: its sheet formats are exactly poster-render.js FORMATS at the same pixel sizes (the generator is a pinned asset, so the table cannot be imported); every configuration name is unique and well formed; the card entries are exactly card-render STOCK; every catalogue key it names exists in en/posterDesign.json; the sample country can be drawn. Six mutants (px drift, a missing format, a duplicate name, a bad key, an undrawable sample, a dropped stock) each FAIL. It also validates every plugin's posterDesign block (plugin-designs/<plugin>/<profile>.meta.json: family, name, size, mobileFriendly, optional labelKey and instance), keeps names unique across templates.js and all blocks, and fails when a name in the names ledger (src/catalogue-names.json) was renamed or removed.",
"check:poster-design-catalogue:self-test": "an invalid posterDesign block, a name that clashes with templates.js, a renamed ledger name a block naming a preset that does not exist, and a sample that exists but cannot be drawn (India with withholding off) must each make check:poster-design-catalogue fail, and the documented example block must pass.",
"check:poster-design-catalogue": "Poster Design's configuration catalogue (plugin-designs/poster-design/src/templates.js) says only true things about the library: its sheet formats are exactly poster-render.js FORMATS at the same pixel sizes (the generator is a pinned asset, so the table cannot be imported); every configuration name is unique and well formed; the card entries are exactly card-render STOCK; every catalogue key it names exists in en/posterDesign.json; the sample country can be drawn. Six mutants (px drift, a missing format, a duplicate name, a bad key, an undrawable sample, a dropped stock) each FAIL. It also validates every plugin's posterDesign block (plugin-designs/<plugin>/<profile>.meta.json: family, name, size, mobileFriendly, optional labelKey and instance), keeps names unique across templates.js and all blocks, and fails when a name in the names ledger (src/catalogue-names.json) was renamed or removed. Every template's stored preview (templates.js `preview`) must be a file some plugin ships under assets/previews (read from the git index).",
"check:poster-design-catalogue:self-test": "an invalid posterDesign block, a name that clashes with templates.js, a renamed ledger name a block naming a preset that does not exist, a sample that exists but cannot be drawn (India with withholding off), and a preview no plugin ships must each make check:poster-design-catalogue fail, and the documented example block must pass.",
"check:upg100-zone-copy": "library/packet/upg100Zones/ is UPG100's zone-poster code copied into the library (so Poster Design draws UPG100 zone posters). COPY mode while UPG100's originals exist: each file equals its original beyond its import specifiers, clusters.json byte for byte. ONE-PATH mode once UPG100 switches and deletes its originals: no plugin file defines the zone code again. Half switched fails.",
"check:upg100-zone-copy:self-test": "a one-character edit in a function body, a changed clusters.json, a missing copy, a half switch, and (one-path) a plugin re-defining makeZoneSheet must each fail; an import-specifier-only change and a clean one-path tree must pass.",
"check:poster-fetch-guard": "a host's 200 fallback page is never taken for a poster. On a drop-in dev host a missing posters/<id>.webp answered 200 text/html, and export handed that page to the print dialog as the image. Asserts library/packet/poster-fetch.js accepts a raster only if it is an image (by type or by its first bytes) and a source only if it is a poster document (class=\"stage\"), and that neither usePosterPrints nor PosterWall fetches posters/ directly. Node only, no server.",

View file

@ -180,7 +180,12 @@ async function renderDocument(name) {
// previewSrc: the thumbnail. A map sheet is rasterised at a quarter scale (a 1500px sheet becomes ~375px,
// the card's size), and a render that comes out blank is refused, so the card shows "preview unavailable"
// rather than a black rectangle. A document's first page is drawn as it prints (src/pageThumb.js).
// A configuration with a STORED thumbnail of itself (templates.js `preview`) shows it at once: one small image
// instead of a live draw (the zone posters took ~9 s each, measured). The live render is still one click away.
const storedUrl = (rel) => `${assetsBase.value}/${rel}`
async function previewSrc(item) {
const stored = byName.get(item.id)?.preview
if (stored) return storedUrl(stored)
const out = await renderDocument(item.id)
if (!out) return null
if (out.image) return out.image
@ -196,6 +201,12 @@ async function previewSrc(item) {
// built here only as far as its page 1, so it is NOT offered for printing from this card: printing a
// packet missing its later pages would misrepresent the template.
async function sheet(item) {
// A kind that exists only as STORED posters (no live renderer here): print its stored poster on a page.
const c0 = byName.get(item.id)
if (c0 && !c0.render && c0.preview) {
const src = new URL(storedUrl(c0.preview), document.baseURI).href
return `<!doctype html><meta charset="utf-8"><title>${escHtml(labelOf(c0))}</title><style>@page{size:letter portrait;margin:12mm}html,body{margin:0}img{display:block;width:100%;height:auto}</style><img src="${src}" alt="">`
}
const out = await renderDocument(item.id)
if (!out || out.image || (typeof out === 'object' && out.firstPageOnly)) return ''
return typeof out === 'string' ? out : out.html

View file

@ -18,6 +18,8 @@
* (src/pluginDesigns.js blockProblems: family, name, size, mobileFriendly, optional labelKey / instance), its
* labelKey exists in the English catalogue, and its instance is a preset of that profile.
* 7. Names are unique across templates.js AND every plugin's blocks.
* 9. STORED PREVIEWS: every template's `preview` (a path under the tree's assets/) is a stored file some plugin
* ships (plugin-designs/<plugin>/assets/<path>), read from the git index so a sparse checkout works too.
* 8. THE NAMES LEDGER (src/catalogue-names.json): every name ever shipped. A ledger name that no longer exists
* (renamed or removed) FAILS: a name is what people hand an agent, so it must keep working (a retired size stays
* as a legacy entry, as templates.js does). A new name passes and is reported, to be added to the ledger.
@ -26,6 +28,7 @@
* off) must each FAIL, and the documented example block must PASS.
*/
import { readFileSync, readdirSync, existsSync } from 'node:fs'
import { execFileSync } from 'node:child_process'
import { resolve, dirname, join } from 'node:path'
import { fileURLToPath } from 'node:url'
@ -158,6 +161,14 @@ function designArms({ names, blocks, ledger, drawnFor = new Map() }) {
for (const n of ledger) if (!seen.has(n)) fs.push(`8: '${n}' is in the names ledger and no longer exists: a shipped name is never renamed or removed (keep it, as a legacy entry if retired)`)
return { fails: fs, fresh: [...seen].filter((n) => !ledger.includes(n)) }
}
// 9. every `preview` names a stored file a plugin ships
const SHIPPED = (() => { try { return new Set(execFileSync('git', ['-C', ROOT, 'ls-files', 'plugin-designs'], { encoding: 'utf8' }).split('\n').filter((f) => /\/assets\/previews\//.test(f)).map((f) => f.replace(/^plugin-designs\/[^/]+\/assets\//, ''))) } catch { return null } })()
function previewArm(list, shipped) {
if (!shipped) return ['9: could not read the git index to find the stored previews']
return list.filter((c) => c.preview && !shipped.has(c.preview)).map((c) => `9: '${c.name}' previews assets/${c.preview}, which no plugin ships`)
}
previewArm(configs, SHIPPED).forEach(fail)
const LEDGER_FILE = join(ROOT, 'plugin-designs/poster-design/src/catalogue-names.json')
const ledger = existsSync(LEDGER_FILE) ? JSON.parse(readFileSync(LEDGER_FILE, 'utf8')).names || [] : []
if (!ledger.length) fail('8: the names ledger (src/catalogue-names.json) is missing or empty')
@ -184,6 +195,9 @@ if (process.argv.includes('--self-test')) {
const und = sampleArm(samples, data.countries || [], () => false).find((f) => /cannot be drawn/.test(f))
console.log(und ? ` ✓ caught: a sample that exists but cannot be drawn (withholding off) -> ${und}` : ' ✗ MISSED: a sample that exists but cannot be drawn')
if (!und) ok = false
const pv = previewArm([...configs, { name: 'fixture.missing-preview', preview: 'previews/no-such-poster.webp' }], SHIPPED).find((f) => /no-such-poster/.test(f))
console.log(pv ? ` ✓ caught: a preview no plugin ships -> ${pv}` : ' ✗ MISSED: a preview no plugin ships')
if (!pv) ok = false
const good = designArms({ ...real, blocks: [...blocks, B(example)] })
console.log(good.fails.length ? ` ✗ the documented example block FAILS: ${good.fails[0]}` : ' ✓ the documented example block passes')
if (good.fails.length) ok = false

View file

@ -71,7 +71,11 @@ async function copyName() {
<dt>{{ t('posterDesign.details.settings') }}</dt>
<dd dir="ltr">{{ settings }}</dd>
<dt>{{ t('posterDesign.details.sample') }}</dt>
<dd dir="ltr">{{ config.sample || '—' }}</dd>
<dd dir="ltr">{{ config.sample || config.sampleLabel || '—' }}</dd>
<template v-if="config.preview">
<dt>{{ t('posterDesign.details.storedPreview') }}</dt>
<dd dir="ltr"><code>assets/{{ config.preview }}</code></dd>
</template>
<dt>{{ t('posterDesign.details.renderer') }}</dt>
<dd dir="ltr"><code>{{ config.renderer }}</code></dd>
</dl>

View file

@ -26,6 +26,9 @@
"wall.bless-60x96",
"bible-poster.36x60",
"upg100-territory-packet.page-1",
"bless-india.range-badge-60x96"
"bless-india.range-badge-60x96",
"city-poster.3x3",
"india-state-poster.3x3",
"upg100-territory-cards.poker"
]
}

View file

@ -33,7 +33,10 @@ export const SAMPLE_COUNTRY_ID = 'country-npl'
// show the template, and their details say so (missing: noCountryAtSize). Names never change (src/catalogue-names.json).
export const SHEET_FORMATS = {
// `size` is the printed size as data (src/sizeText.js names it in the reader's language): inches, side lanes, paper.
'3x3': { size: { dims: [36, 36] }, px: [1500, 1500] },
// `preview`: a STORED thumbnail of this exact configuration (same format, same sample), generated by the real
// generator (library/scripts/posters), path under the tree's assets/. Styles shows it instantly; the live render
// stays one click away (Show full details, print). check:poster-design-catalogue proves the file exists.
'3x3': { size: { dims: [36, 36] }, px: [1500, 1500], preview: 'previews/country-npl.webp' },
// experimental: still being designed, or no drawable preview; Styles lists these in a collapsed Experimental group.
'60x96': { size: { dims: [60, 96], lanes: true }, px: [2500, 4000], sample: 'country-ind', experimental: true },
'66x66': { size: { dims: [66, 66], lanes: true }, px: [2750, 2750], sample: 'country-chn', missing: 'posterDesign.missing.noCountryAtSize', experimental: true },
@ -47,6 +50,7 @@ export const SHEET_FORMATS = {
// assembled, so the catalogue checks and the i18n area build can see each one.
export const FAMILY_KEYS = {
mapSheet: 'posterDesign.family.mapSheet',
cityPoster: 'posterDesign.family.cityPoster',
upgZone: 'posterDesign.family.upgZone',
packet: 'posterDesign.family.packet',
cards: 'posterDesign.family.cards',
@ -64,6 +68,7 @@ function sheetEntry(format) {
px: f.px,
legacy: !!f.legacy,
...(f.experimental ? { experimental: true } : {}),
...(f.preview && !f.sample ? { preview: f.preview } : {}),
...(f.missing ? { missing: f.missing } : {}),
renderer: 'library/packet/posterSheets.js generateCountrySheet → library/packet/poster-render.js',
settings: { format },
@ -90,15 +95,38 @@ function zoneEntry(format) {
renderer: ZONE_RENDERER,
settings: { format },
sample: SAMPLE_ZONE_ID,
...(ZONE_PREVIEWS[format] ? { preview: ZONE_PREVIEWS[format] } : {}),
render: (ctx) => ctx.zoneSheet(format, SAMPLE_ZONE_ID),
}
}
// The stored thumbnails of middle-east-core that are exactly these configurations (the 3x3 zone poster and the
// packet's page 1); the letter and 3x4 zone posters have none yet, so they render live.
const ZONE_PREVIEWS = { '3x3': 'previews/middle-east-core.webp', packet: 'previews/examples/middle-east-core/packet-p01.webp' }
// ONE OF EACH KIND that exists only as STORED posters today (their generator is the batch one, library/scripts/
// posters): the card shows the stored poster; the live render is the source plugin's own page. Each carries its
// source plugin (`plugin`), so that plugin's own posterDesign block may share the name.
const STORED_KINDS = [
{ name: 'city-poster.3x3', family: 'cityPoster', labelKey: 'posterDesign.tpl.cityPoster', plugin: 'key-cities',
size: { dims: [36, 36] }, px: [560, 560], sampleLabel: 'Hyderabad, Telangana, India',
preview: 'previews/city-ind-17.4284-78.4492.webp',
renderer: 'library/scripts/posters/generate.mjs (key-cities) → library/packet/poster-render.js' },
{ name: 'india-state-poster.3x3', family: 'mapSheet', labelKey: 'posterDesign.tpl.indiaStatePoster', plugin: 'country-posters',
size: { dims: [36, 36] }, px: [560, 560], sampleLabel: 'Uttar Pradesh',
preview: 'previews/india-up.webp',
renderer: 'library/packet/countrySheet.js makeCountrySheet (indiaState) → library/packet/poster-render.js' },
{ name: 'upg100-territory-cards.poker', family: 'upgZone', labelKey: 'posterDesign.tpl.upgZoneCards', plugin: 'my-upg-100-list',
size: { dims: [2.5, 3.5] }, px: [480, 672], sampleLabel: 'middle-east-core, card 1',
preview: 'previews/examples/middle-east-core/card-01.webp',
renderer: 'library/packet/card-render.js createCardRenderer (STOCK poker), via library/scripts/posters/packet.mjs' },
].map((k) => ({ ...k, settings: {}, sample: null, render: null }))
/** @returns {Array<object>} every configuration, in the gallery's order. */
export function listConfigurations() {
return [
...Object.keys(SHEET_FORMATS).map(sheetEntry),
...['letter', '3x3', '3x4', 'packet'].map(zoneEntry),
...STORED_KINDS,
...['full', 'lightweight'].map((variant) => ({
name: `packet.${variant}`,
family: 'packet',