From 9bd21776879afc88475b0223e5030e07b0761444 Mon Sep 17 00:00:00 2001 From: Doxa Maps Date: Fri, 2 Oct 2026 08:50:48 +0000 Subject: [PATCH] Poster Design lists every plugin's poster designs, declared in the plugin's own meta A poster plugin now lists its designs in its own .meta.json under posterDesign ({ family, name, size, mobileFriendly, labelKey?, instance? }, one or an array). internal/profile-meta.mjs carries the block into manifest.json only when present, so every existing manifest keeps its bytes (the built and the dev manifest share that function). Poster Design's Styles reads the tree's manifest at runtime and gives each declared design a card under From other plugins: the card says it is made by its own plugin, and Show full details shows that plugin's page (or preset page) live, with a link to open it. No plugin code is imported, and a plugin adding its block does not rebuild Poster Design. check:poster-design-catalogue now also validates every block, keeps names unique across templates.js and all blocks, and keeps a names ledger (src/catalogue-names.json): a shipped name that is renamed or removed fails. Its new self-test (in gates:pre) proves an invalid block, a name clash, a renamed ledger name and a missing preset each fail, and the documented example passes. --- internal/profile-meta.mjs | 8 +- library/i18n/locales/ar/posterDesign.json | 9 +- library/i18n/locales/de/posterDesign.json | 9 +- library/i18n/locales/en/posterDesign.json | 9 +- library/i18n/locales/es/posterDesign.json | 9 +- library/i18n/locales/fr/posterDesign.json | 9 +- library/i18n/locales/hi/posterDesign.json | 9 +- library/i18n/locales/it/posterDesign.json | 9 +- library/i18n/locales/pt/posterDesign.json | 9 +- library/i18n/locales/ro/posterDesign.json | 9 +- library/i18n/locales/ru/posterDesign.json | 9 +- library/i18n/locales/zh/posterDesign.json | 9 +- package.json | 6 +- .../poster-design/poster-styles.vue | 47 ++- .../poster-design/scripts/check-catalogue.mjs | 94 +++++- .../poster-design/src/FullDetailsDialog.vue | 9 +- .../poster-design/src/catalogue-names.json | 21 ++ .../poster-design/src/pluginDesigns.js | 76 +++++ public/dx-maps/doxa-maps-spa/manifest.json | 8 +- public/dx-maps/manifest.json | 8 +- .../poster-design-mfe/index.html | 2 +- public/dx-maps/poster-design/poster-design.js | 297 +++++++++--------- .../poster-design/poster-sizes/index.html | 2 +- .../poster-design/poster-styles/index.html | 2 +- 24 files changed, 469 insertions(+), 210 deletions(-) create mode 100644 plugin-designs/poster-design/src/catalogue-names.json create mode 100644 plugin-designs/poster-design/src/pluginDesigns.js diff --git a/internal/profile-meta.mjs b/internal/profile-meta.mjs index d7e0fbe17..3da0f7b94 100644 --- a/internal/profile-meta.mjs +++ b/internal/profile-meta.mjs @@ -9,6 +9,9 @@ * widget { corner } for a corner widget the Plugin Studio mounts once, fixed * visibility "team" | "public" default team — see VISIBILITY below * mobile "desktop-only" | "larger-screens" absent = phone-ready — see MOBILE below + * posterDesign { family, name, size, mobileFriendly, labelKey?, instance? } or an array of them: the poster + * design(s) this profile shows, listed by Poster Design's Styles. Carried as written, ONLY when present + * (every other manifest keeps its bytes); check:poster-design-catalogue validates it. */ import { readFileSync } from 'node:fs' import { join } from 'node:path' @@ -69,8 +72,9 @@ export function readProfileMeta(dir, profileName) { ? null : String(meta.visibility), mobile: MOBILE.includes(meta?.mobile) ? meta.mobile : null, + posterDesign: meta?.posterDesign && typeof meta.posterDesign === 'object' ? meta.posterDesign : null, } - } catch { return { stage: 'experimental', heavy: false, widget: null, label: '', targets: null, visibility: 'team', visibilityInvalid: null, mobile: null } } + } catch { return { stage: 'experimental', heavy: false, widget: null, label: '', targets: null, visibility: 'team', visibilityInvalid: null, mobile: null, posterDesign: null } } } /** The optional manifest keys for one profile (stage always; heavy / widget only when set). */ @@ -78,7 +82,7 @@ export function profileMetaFields(dir, profileName) { const m = readProfileMeta(dir, profileName) // visibility is emitted ONLY when it is "public": absent means team, so every manifest in the // tree today keeps the bytes it has and nothing becomes visible by upgrading the generator. - return { stage: m.stage, ...(m.visibility === 'public' ? { visibility: 'public' } : {}), ...(m.label ? { label: m.label } : {}), ...(m.targets ? { targets: m.targets } : {}), ...(m.heavy ? { heavy: true } : {}), ...(m.widget ? { widget: m.widget } : {}), ...(m.mobile ? { mobile: m.mobile } : {}) } + return { stage: m.stage, ...(m.visibility === 'public' ? { visibility: 'public' } : {}), ...(m.label ? { label: m.label } : {}), ...(m.targets ? { targets: m.targets } : {}), ...(m.heavy ? { heavy: true } : {}), ...(m.widget ? { widget: m.widget } : {}), ...(m.mobile ? { mobile: m.mobile } : {}), ...(m.posterDesign ? { posterDesign: m.posterDesign } : {}) } } /** A bundle is production if ANY of its profiles says so. */ diff --git a/library/i18n/locales/ar/posterDesign.json b/library/i18n/locales/ar/posterDesign.json index 3cc3aee22..dbf622436 100644 --- a/library/i18n/locales/ar/posterDesign.json +++ b/library/i18n/locales/ar/posterDesign.json @@ -35,7 +35,8 @@ "packet": "حزمة الصلاة", "cards": "بطاقات الصلاة", "wall": "ألواح الجدار", - "mapPrint": "طباعة الخريطة" + "mapPrint": "طباعة الخريطة", + "plugins": "من إضافات أخرى" }, "tpl": { "mapSheetCountry": "صفحة خريطة الدولة", @@ -60,7 +61,9 @@ "sample": "بيانات نموذجية", "renderer": "المولّد", "legacy": "مقاس قديم: ما زال يُطبع، لكن أداة اختيار الطباعة لم تعد تعرضه.", - "forAgents": "لاستخدام هذا القالب، قل للوكيل: استخدم الإعداد {name}." + "forAgents": "لاستخدام هذا القالب، قل للوكيل: استخدم الإعداد {name}.", + "openPage": "افتح صفحته" }, - "templateCount": "لا قوالب | قالب واحد | قالبان | {n} قوالب | {n} قالبًا | {n} قالب" + "templateCount": "لا قوالب | قالب واحد | قالبان | {n} قوالب | {n} قالبًا | {n} قالب", + "fromPlugin": "تصنعه إضافته الخاصة. اعرض كل التفاصيل لتراه مباشرة." } diff --git a/library/i18n/locales/de/posterDesign.json b/library/i18n/locales/de/posterDesign.json index 76fd9c561..c39189fc1 100644 --- a/library/i18n/locales/de/posterDesign.json +++ b/library/i18n/locales/de/posterDesign.json @@ -35,7 +35,8 @@ "packet": "Gebetsmappe", "cards": "Gebetskarten", "wall": "Wandtafeln", - "mapPrint": "Kartendruck" + "mapPrint": "Kartendruck", + "plugins": "Aus anderen Plugins" }, "tpl": { "mapSheetCountry": "Länder-Kartenblatt", @@ -60,7 +61,9 @@ "sample": "Beispieldaten", "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}." + "forAgents": "Um diese Vorlage zu verwenden, sagen Sie einem Agenten: verwende die Konfiguration {name}.", + "openPage": "Seite öffnen" }, - "templateCount": "{n} Vorlage | {n} Vorlagen" + "templateCount": "{n} Vorlage | {n} Vorlagen", + "fromPlugin": "Wird von seinem eigenen Plugin erstellt. „Alle Details anzeigen“ zeigt es live." } diff --git a/library/i18n/locales/en/posterDesign.json b/library/i18n/locales/en/posterDesign.json index dd7fb1f8b..0c8035ad5 100644 --- a/library/i18n/locales/en/posterDesign.json +++ b/library/i18n/locales/en/posterDesign.json @@ -35,7 +35,8 @@ "packet": "Prayer packet", "cards": "Prayer cards", "wall": "Wall panels", - "mapPrint": "Map print" + "mapPrint": "Map print", + "plugins": "From other plugins" }, "tpl": { "mapSheetCountry": "Country map sheet", @@ -60,7 +61,9 @@ "sample": "Sample data", "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}." + "forAgents": "To use this template, tell an agent: use configuration {name}.", + "openPage": "Open its page" }, - "templateCount": "{n} templates | {n} template | {n} templates" + "templateCount": "{n} templates | {n} template | {n} templates", + "fromPlugin": "Made by its own plugin. Show full details shows it live." } diff --git a/library/i18n/locales/es/posterDesign.json b/library/i18n/locales/es/posterDesign.json index a63495e8f..3b7da0427 100644 --- a/library/i18n/locales/es/posterDesign.json +++ b/library/i18n/locales/es/posterDesign.json @@ -35,7 +35,8 @@ "packet": "Paquete de oración", "cards": "Tarjetas de oración", "wall": "Paneles de pared", - "mapPrint": "Impresión del mapa" + "mapPrint": "Impresión del mapa", + "plugins": "De otros complementos" }, "tpl": { "mapSheetCountry": "Hoja de mapa del país", @@ -60,7 +61,9 @@ "sample": "Datos de ejemplo", "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}." + "forAgents": "Para usar esta plantilla, dile a un agente: usa la configuración {name}.", + "openPage": "Abrir su página" }, - "templateCount": "{n} plantilla | {n} de plantillas | {n} plantillas" + "templateCount": "{n} plantilla | {n} de plantillas | {n} plantillas", + "fromPlugin": "Lo crea su propio complemento. Mostrar todos los detalles lo muestra en vivo." } diff --git a/library/i18n/locales/fr/posterDesign.json b/library/i18n/locales/fr/posterDesign.json index ff32e0e40..0e58cf838 100644 --- a/library/i18n/locales/fr/posterDesign.json +++ b/library/i18n/locales/fr/posterDesign.json @@ -35,7 +35,8 @@ "packet": "Dossier de prière", "cards": "Cartes de prière", "wall": "Panneaux muraux", - "mapPrint": "Impression de la carte" + "mapPrint": "Impression de la carte", + "plugins": "D’autres plugins" }, "tpl": { "mapSheetCountry": "Feuille de carte du pays", @@ -60,7 +61,9 @@ "sample": "Données d'exemple", "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}." + "forAgents": "Pour utiliser ce modèle, dites à un agent : utilise la configuration {name}.", + "openPage": "Ouvrir sa page" }, - "templateCount": "{n} modèle | {n} de modèles | {n} modèles" + "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." } diff --git a/library/i18n/locales/hi/posterDesign.json b/library/i18n/locales/hi/posterDesign.json index 802ddc630..38b4624ce 100644 --- a/library/i18n/locales/hi/posterDesign.json +++ b/library/i18n/locales/hi/posterDesign.json @@ -35,7 +35,8 @@ "packet": "प्रार्थना पैकेट", "cards": "प्रार्थना कार्ड", "wall": "दीवार पैनल", - "mapPrint": "मानचित्र प्रिंट" + "mapPrint": "मानचित्र प्रिंट", + "plugins": "अन्य प्लगइन से" }, "tpl": { "mapSheetCountry": "देश का मानचित्र पृष्ठ", @@ -60,7 +61,9 @@ "sample": "नमूना डेटा", "renderer": "जनरेटर", "legacy": "पुराना आकार: यह अब भी प्रिंट होता है, पर प्रिंट चयनकर्ता अब इसे नहीं दिखाता।", - "forAgents": "यह टेम्पलेट इस्तेमाल करने के लिए किसी एजेंट से कहें: कॉन्फ़िगरेशन {name} इस्तेमाल करो।" + "forAgents": "यह टेम्पलेट इस्तेमाल करने के लिए किसी एजेंट से कहें: कॉन्फ़िगरेशन {name} इस्तेमाल करो।", + "openPage": "इसका पेज खोलें" }, - "templateCount": "{n} टेम्पलेट | {n} टेम्पलेट" + "templateCount": "{n} टेम्पलेट | {n} टेम्पलेट", + "fromPlugin": "यह अपने ही प्लगइन से बनता है। पूरा विवरण दिखाएँ इसे लाइव दिखाता है।" } diff --git a/library/i18n/locales/it/posterDesign.json b/library/i18n/locales/it/posterDesign.json index a4c4e5dd7..242456dd3 100644 --- a/library/i18n/locales/it/posterDesign.json +++ b/library/i18n/locales/it/posterDesign.json @@ -35,7 +35,8 @@ "packet": "Fascicolo di preghiera", "cards": "Schede di preghiera", "wall": "Pannelli da parete", - "mapPrint": "Stampa della mappa" + "mapPrint": "Stampa della mappa", + "plugins": "Da altri plugin" }, "tpl": { "mapSheetCountry": "Foglio mappa del paese", @@ -60,7 +61,9 @@ "sample": "Dati di esempio", "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}." + "forAgents": "Per usare questo modello, di' a un agente: usa la configurazione {name}.", + "openPage": "Apri la sua pagina" }, - "templateCount": "{n} modello | {n} di modelli | {n} modelli" + "templateCount": "{n} modello | {n} di modelli | {n} modelli", + "fromPlugin": "Creato dal suo plugin. Mostra tutti i dettagli lo mostra dal vivo." } diff --git a/library/i18n/locales/pt/posterDesign.json b/library/i18n/locales/pt/posterDesign.json index 489b67ae0..a6f43519f 100644 --- a/library/i18n/locales/pt/posterDesign.json +++ b/library/i18n/locales/pt/posterDesign.json @@ -35,7 +35,8 @@ "packet": "Pacote de oração", "cards": "Cartões de oração", "wall": "Painéis de parede", - "mapPrint": "Impressão do mapa" + "mapPrint": "Impressão do mapa", + "plugins": "De outros plugins" }, "tpl": { "mapSheetCountry": "Folha de mapa do país", @@ -60,7 +61,9 @@ "sample": "Dados de exemplo", "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}." + "forAgents": "Para usar este modelo, diga a um agente: use a configuração {name}.", + "openPage": "Abrir a página" }, - "templateCount": "{n} modelo | {n} de modelos | {n} modelos" + "templateCount": "{n} modelo | {n} de modelos | {n} modelos", + "fromPlugin": "Feito pelo próprio plugin. Mostrar todos os detalhes o exibe ao vivo." } diff --git a/library/i18n/locales/ro/posterDesign.json b/library/i18n/locales/ro/posterDesign.json index 7d864df8b..5a8a6fd8c 100644 --- a/library/i18n/locales/ro/posterDesign.json +++ b/library/i18n/locales/ro/posterDesign.json @@ -35,7 +35,8 @@ "packet": "Pachet de rugăciune", "cards": "Cartonașe de rugăciune", "wall": "Panouri de perete", - "mapPrint": "Tipărirea hărții" + "mapPrint": "Tipărirea hărții", + "plugins": "Din alte pluginuri" }, "tpl": { "mapSheetCountry": "Foaie de hartă a țării", @@ -60,7 +61,9 @@ "sample": "Date de exemplu", "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}." + "forAgents": "Ca să folosești acest șablon, spune-i unui agent: folosește configurația {name}.", + "openPage": "Deschide pagina" }, - "templateCount": "{n} șablon | {n} șabloane | {n} de șabloane" + "templateCount": "{n} șablon | {n} șabloane | {n} de șabloane", + "fromPlugin": "Este realizat de propriul plugin. Afișează toate detaliile îl arată în direct." } diff --git a/library/i18n/locales/ru/posterDesign.json b/library/i18n/locales/ru/posterDesign.json index 402a39b75..203b31908 100644 --- a/library/i18n/locales/ru/posterDesign.json +++ b/library/i18n/locales/ru/posterDesign.json @@ -35,7 +35,8 @@ "packet": "Молитвенный пакет", "cards": "Молитвенные карточки", "wall": "Настенные панели", - "mapPrint": "Печать карты" + "mapPrint": "Печать карты", + "plugins": "Из других плагинов" }, "tpl": { "mapSheetCountry": "Лист карты страны", @@ -60,7 +61,9 @@ "sample": "Примерные данные", "renderer": "Генератор", "legacy": "Устаревший размер: он ещё печатается, но в выборе печати его больше нет.", - "forAgents": "Чтобы использовать этот шаблон, скажите агенту: используй конфигурацию {name}." + "forAgents": "Чтобы использовать этот шаблон, скажите агенту: используй конфигурацию {name}.", + "openPage": "Открыть страницу" }, - "templateCount": "{n} шаблон | {n} шаблона | {n} шаблонов | {n} шаблонов" + "templateCount": "{n} шаблон | {n} шаблона | {n} шаблонов | {n} шаблонов", + "fromPlugin": "Создаётся собственным плагином. «Показать все подробности» покажет его вживую." } diff --git a/library/i18n/locales/zh/posterDesign.json b/library/i18n/locales/zh/posterDesign.json index 2003add93..3c31c79ea 100644 --- a/library/i18n/locales/zh/posterDesign.json +++ b/library/i18n/locales/zh/posterDesign.json @@ -35,7 +35,8 @@ "packet": "祷告资料包", "cards": "祷告卡", "wall": "墙面展板", - "mapPrint": "地图打印" + "mapPrint": "地图打印", + "plugins": "来自其他插件" }, "tpl": { "mapSheetCountry": "国家地图页", @@ -60,7 +61,9 @@ "sample": "示例数据", "renderer": "生成器", "legacy": "旧尺寸:仍可打印,但打印选择器已不再提供。", - "forAgents": "要使用此模板,请告诉代理:使用配置 {name}。" + "forAgents": "要使用此模板,请告诉代理:使用配置 {name}。", + "openPage": "打开其页面" }, - "templateCount": "{n} 个模板" + "templateCount": "{n} 个模板", + "fromPlugin": "由其自身插件生成。点击“显示全部详情”可实时查看。" } diff --git a/package.json b/package.json index 0bd87cf01..0170bf34a 100644 --- a/package.json +++ b/package.json @@ -258,6 +258,7 @@ "check:poster-card-fits": "node internal/check-poster-card-fits.mjs", "check:poster-degrades": "node internal/check-poster-degrades-honestly.mjs", "check:poster-design-catalogue": "node plugin-designs/poster-design/scripts/check-catalogue.mjs", + "check:poster-design-catalogue:self-test": "node plugin-designs/poster-design/scripts/check-catalogue.mjs --self-test", "check:poster-fetch-guard": "node internal/check-poster-fetch-guard.mjs", "check:poster-print-fallback": "node internal/check-poster-print-fallback.mjs", "check:posters-exportable": "node internal/check-posters-exportable.mjs", @@ -353,7 +354,7 @@ "fix:sri": "node internal/fix-sri.mjs", "gallery": "node internal/generate-gallery.mjs", "gates:post": "node internal/run-parallel.mjs --label=post -- check:capability-derived check:drop check:parse verify check:sri check:public-hygiene check:label-joins check:facet-cascade check:facet-one-field check:heartbeat check:kpi-unit check:composite-reg check:descriptions check:no-abs-paths check:no-codename check:dep-names check:composite-srcs check:embed-targets check:nested-token check:i18n-parity check:tabs-localized check:packets-all check:packet-strings-parity check:zone-census check:zoning-declared check:share-policy check:library-index check:csv-shape check:capability-citations check:catalog-identity check:no-orphan-gates check:quoted-scripts check:script-flags check:capability-exemplars check:packet-fixture check:packet-browser check:provider-switch check:locale-reachable check:url-resolver check:manifest-covers check:one-palette check:pin-dim-slider check:legend-source check:clustering-modes check:composite-embed-knobs check:composite-locale-leaks check:linked-vocabulary check:wp-embed check:dashboard-geocoder-provider check:leaf-engine-per-provider check:auto-engine check:engine-contract check:map-props-unit check:composites-mount check:composite-interop check:no-free-identifiers check:map-fly-reduced-motion check:mst-clustering check:list-prop-unit check:profiles-mount check:cdn-pins check:no-eval check:popup-escaping check:i18n-treeshake check:pinned-umd check:one-palette:self-test check:no-cross-plugin-imports check:no-cross-plugin-imports:self-test check:map-attribution check:studio-rail check:approx-pin-note check:embedder-keys check:provider-for check:legend-logical-insets check:legend-logical-insets:self-test check:optional-t-equivalence check:text-highlight check:religion-label-not-value check:i18n-stopped-scope check:no-silent-engine check:no-silent-engine:self-test check:regions-uses-library check:shadow-css-parses check:square-map-controls check:preset-config-merge check:instance-config-keys check:frozen-labels check:plural-forms check:region-fill-repaint check:region-paint-single-source check:poster-print-fallback check:posters-exportable check:country-names check:reduced-motion check:no-dead-library check:universal-props check:readme-size check:tree-relative check:leaf-hints check:snippet-engine check:snippet-engine:self-test check:embed-base check:link-coverage check:no-public-binds check:shared-reached check:tree-weight check:no-fleet-names:self-test check:pin-hint-floor:self-test check:poster-sizes:self-test check:shell-header:self-test check:preview-flag-dev-only:self-test", - "gates:pre": "node internal/run-parallel.mjs --label=pre -- check:template-literal check:mjs-parse check:sfc-parse check:tab-radius check:synced-css check:doc-scripts check:setup-order check:package-json-unique-keys check:no-repo-pb-hooks check:no-credentials-omit check:no-credentials-omit:self-test check:shadow-reduced-motion check:poster-action-print check:targets check:targets:self-test check:browser-gates-guarded check:pin-selection check:facet-shape check:hardcoded-english:self-test check:list-selection check:selector-fields check:upg-zone-joins check:gospel-set-charts check:gospel-set-fit check:describe-group check:one-scroll-container check:packet-charts check:composite-token-url check:language-tree-layout check:i18n-keys-exist check:i18n-never-translated check:packet-variants check:profile-table check:element-bootstrap check:embed-url-absolute check:frame-target check:language-tree-model check:map-menu-anchor check:selection-kinds check:tiered-points check:whiteboard-projection check:mercator-slice check:ring-vocabulary check:map-props-unit check:mobile-gating check:provide-shadowing check:legend-top-offset check:cdn-cache check:plural-forms check:select-zone-race check:script-registry check:map-control-overlap check:poster-attribution check:board-map-options check:gospel-whiteboard check:packet-map-missing-loud check:poster-layout-optin check:ported-globals-identical check:ring-bands check:packet-a1-structure check:india-states check:batch-selection-export check:zone-title-tier-a check:evangelical-share check:map-labels check:packet-offline check:help-reachable check:gated-not-in-tree check:window-globals-released check:legend-data-contract check:no-internal-role-nouns check:poster-fetch-guard check:people-group-records check:sentence-slots check:popup-href-escaped check:popup-pan-controls check:country-map-refetch check:pane-config-reforward check:viewport-meta check:viewport-meta:self-test check:label-key-coverage check:poster-browser-equiv check:poster-render-pinned check:country-sheet-one-path check:country-sheet-one-path:self-test check:side-lanes check:side-lanes:self-test check:poster-section check:poster-section:self-test check:posterverse-shelf check:host-locale-fetch check:print-frame-reclaim check:click-sheet check:coverage-consistent:chained check:coverage-consistent:poison check:dashboard-filter-a11y check:filter-multivalue check:hover-channel check:kit-options-reachable check:layout-key-namespaced check:legend-mirrors check:map-unmount-race check:no-conflict-markers check:series-isolation check:snapshot-guard check:stale-sweep-claim check:surfaces-read-the-tier check:translator-contacts check:wall-wysiwyg check:wall-print-watch check:wall-no-bg-photos check:wall-pan check:template-i18n check:list-pattern-doc check:shadow-style-labels-unique check:grid-image-loading check:retry-fetch check:panel-sheets check:poster-wall-host-seams check:poster-design-catalogue check:wall-photo-weight check:no-fleet-names check:pin-hint-floor check:poster-sizes check:upg-count check:shell-header check:pg-name-gate check:preview-flag-dev-only", + "gates:pre": "node internal/run-parallel.mjs --label=pre -- check:template-literal check:mjs-parse check:sfc-parse check:tab-radius check:synced-css check:doc-scripts check:setup-order check:package-json-unique-keys check:no-repo-pb-hooks check:no-credentials-omit check:no-credentials-omit:self-test check:shadow-reduced-motion check:poster-action-print check:targets check:targets:self-test check:browser-gates-guarded check:pin-selection check:facet-shape check:hardcoded-english:self-test check:list-selection check:selector-fields check:upg-zone-joins check:gospel-set-charts check:gospel-set-fit check:describe-group check:one-scroll-container check:packet-charts check:composite-token-url check:language-tree-layout check:i18n-keys-exist check:i18n-never-translated check:packet-variants check:profile-table check:element-bootstrap check:embed-url-absolute check:frame-target check:language-tree-model check:map-menu-anchor check:selection-kinds check:tiered-points check:whiteboard-projection check:mercator-slice check:ring-vocabulary check:map-props-unit check:mobile-gating check:provide-shadowing check:legend-top-offset check:cdn-cache check:plural-forms check:select-zone-race check:script-registry check:map-control-overlap check:poster-attribution check:board-map-options check:gospel-whiteboard check:packet-map-missing-loud check:poster-layout-optin check:ported-globals-identical check:ring-bands check:packet-a1-structure check:india-states check:batch-selection-export check:zone-title-tier-a check:evangelical-share check:map-labels check:packet-offline check:help-reachable check:gated-not-in-tree check:window-globals-released check:legend-data-contract check:no-internal-role-nouns check:poster-fetch-guard check:people-group-records check:sentence-slots check:popup-href-escaped check:popup-pan-controls check:country-map-refetch check:pane-config-reforward check:viewport-meta check:viewport-meta:self-test check:label-key-coverage check:poster-browser-equiv check:poster-render-pinned check:country-sheet-one-path check:country-sheet-one-path:self-test check:side-lanes check:side-lanes:self-test check:poster-section check:poster-section:self-test check:posterverse-shelf check:host-locale-fetch check:print-frame-reclaim check:click-sheet check:coverage-consistent:chained check:coverage-consistent:poison check:dashboard-filter-a11y check:filter-multivalue check:hover-channel check:kit-options-reachable check:layout-key-namespaced check:legend-mirrors check:map-unmount-race check:no-conflict-markers check:series-isolation check:snapshot-guard check:stale-sweep-claim check:surfaces-read-the-tier check:translator-contacts check:wall-wysiwyg check:wall-print-watch check:wall-no-bg-photos check:wall-pan check:template-i18n check:list-pattern-doc check:shadow-style-labels-unique check:grid-image-loading check:retry-fetch check:panel-sheets check:poster-wall-host-seams check:poster-design-catalogue check:poster-design-catalogue:self-test check:wall-photo-weight check:no-fleet-names check:pin-hint-floor check:poster-sizes check:upg-count check:shell-header check:pg-name-gate check:preview-flag-dev-only", "gen:iso3-to-alpha2": "node library/scripts/generate-iso3-to-alpha2.mjs", "internet:adm1": "node plugin-designs/cities-internet/scripts/build-adm1-internet.mjs", "library-index": "node internal/generate-library-index.mjs", @@ -598,7 +599,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.", + "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//.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 and a block naming a preset that does not exist must each make check:poster-design-catalogue fail, and the documented example block 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/.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.", "check:poster-layout-optin": "the composed poster sheet (header, map, legend beside it, footer) is OPT-IN: spec.layout === 'poster' asks for it, and every caller that does not gets the full-page map exactly as before, asserted by object identity, not just by value. Drives the real layout decisions with real spec objects. Headless, no browser.", "check:poster-print-fallback": "clicks the REAL poster print control with the served poster generator and every posters/*.webp aborted, and asserts the preview RASTERISES from posters/.html instead of opening on \"Poster not available yet\". Needs a served tree built with posterSources:true; NOT MEASURED otherwise.", diff --git a/plugin-designs/poster-design/poster-styles.vue b/plugin-designs/poster-design/poster-styles.vue index 0b6961441..217031190 100644 --- a/plugin-designs/poster-design/poster-styles.vue +++ b/plugin-designs/poster-design/poster-styles.vue @@ -11,12 +11,14 @@ * 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. + * The catalogue itself is src/templates.js. Below it, every poster design OTHER plugins declare in their own meta + * ("posterDesign", carried into manifest.json; src/pluginDesigns.js) gets a card in its own group: it is made by its + * own plugin, so the card says so and "Show full details" shows that plugin's page live. */ -import { computed, inject, ref } from 'vue' +import { computed, inject, onMounted, 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 { absoluteEmbedUrl, 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' @@ -29,6 +31,7 @@ import { panelsSvgUrl } from './src/panelsSvg.js' import { resolvePosterSize } from '@map/constants/posterSizes.js' import { listConfigurations, FAMILY_KEYS, SAMPLE_COUNTRY_ID } from './src/templates.js' import FullDetailsDialog from './src/FullDetailsDialog.vue' +import { loadPluginDesigns } from './src/pluginDesigns.js' const profileConfig = inject('profileConfig', ref(null)) const instanceId = computed(() => profileConfig.value?.instanceId || 'poster-design-styles') @@ -48,16 +51,35 @@ function stockLabel(s) { } const labelOf = (c) => (c.stock ? `${t(c.labelKey)}: ${stockLabel(c.stock)}` : `${t(c.labelKey)}: ${c.size}`) +// Designs other plugins declare (their meta's posterDesign, via manifest.json): the built tree's manifest, or the dev +// server's. A name already in templates.js stays the library's (the check refuses the clash; the page never doubles it). +const pluginDesigns = ref([]) +const byPluginName = computed(() => new Map(pluginDesigns.value.map((d) => [d.name, d]))) +onMounted(async () => { + const urls = [absoluteEmbedUrl('manifest.json', profileConfig.value), import.meta.env.DEV ? '/__plugin-gallery/manifest.json' : ''] + const { designs } = await loadPluginDesigns(urls) + pluginDesigns.value = designs.filter((d) => !byName.has(d.name)) +}) +const pluginLabel = (d) => { + const v = d.labelKey ? t(d.labelKey) : '' + return `${v && v !== d.labelKey ? v : d.label}: ${d.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) +const items = computed(() => [ + ...configs.map((c) => ({ id: c.name, label: labelOf(c) })), + ...pluginDesigns.value.map((d) => ({ id: d.name, label: pluginLabel(d) })), +]) +const groupBy = (item) => (byPluginName.value.has(item.id) ? t('posterDesign.family.plugins') : t(FAMILY_KEYS[byName.get(item.id)?.family] || FAMILY_KEYS.mapSheet)) // ── drawing a configuration from the library ────────────────────────────────────────────────── const countryById = (id) => (countryData.countries || []).find((c) => c.id === id) // A design whose sample country is WITHHELD (countrySheet.js: the whole-India poster, for now) shows this sentence on its // card and is never drawn or printed: PosterWall answers `unsupported` before it draws a preview or starts a print. The // designs stay in the catalogue, so they reappear the day the country is no longer withheld. +// A design made by another plugin is not drawn here either: its card says so, and its details show its page live. const withheld = (item) => { + if (byPluginName.value.has(item.id)) return t('posterDesign.fromPlugin') const c = byName.get(item.id) const sample = c && c.sample ? countryById(c.sample) : null return sample && sheetWithheld(sample) ? t('messages.countryPosterTooMany') : null @@ -153,8 +175,16 @@ async function sheet(item) { // ── "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 openLabel = computed(() => (!open.value ? '' : open.value.design ? pluginLabel(open.value.design) : labelOf(open.value))) +// A plugin's design opens in the same dialog, as a configuration: its renderer is the plugin's page, and the dialog +// shows that page live. +function showDetails(item) { + const d = byPluginName.value.get(item.id) + open.value = d + ? { name: d.name, size: d.size, settings: { family: d.family, mobileFriendly: d.mobileFriendly, ...(d.instance ? { preset: d.instance } : {}) }, + renderer: `plugin-designs/${d.bundle}/${d.profile}.vue`, pageUrl: d.pageUrl, design: d } + : 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. @@ -181,6 +211,9 @@ useShadowStyles(` .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__live{margin:14px 0 0} +.pd-dialog__frame{display:block;width:100%;height:min(60vh,520px);border:1px solid #d1d4cd;border-radius:8px;background:#f3f3f1} +.pd-dialog__open{display:inline-block;margin-top:8px;color:#3b463d;font-weight:600} .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') diff --git a/plugin-designs/poster-design/scripts/check-catalogue.mjs b/plugin-designs/poster-design/scripts/check-catalogue.mjs index 73ab72861..01aaa9246 100644 --- a/plugin-designs/poster-design/scripts/check-catalogue.mjs +++ b/plugin-designs/poster-design/scripts/check-catalogue.mjs @@ -12,9 +12,17 @@ * 4. Every catalogue key an entry names (labelKey, missing, the family keys) exists in the English * posterDesign catalogue. * 5. The sample country exists in the country data and can be drawn (has geometry, a sheet tier). - * A catalogue that lists nothing FAILS (vacuous guard). + * 6. PLUGIN DESIGNS: every "posterDesign" block in plugin-designs//.meta.json is valid + * (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. + * 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. + * A catalogue that lists nothing FAILS (vacuous guard). --self-test: an invalid block, a name clash, a renamed + * ledger name and a block naming a missing preset must each FAIL, and the documented example block must PASS. */ -import { readFileSync } from 'node:fs' +import { readFileSync, readdirSync, existsSync } from 'node:fs' import { resolve, dirname, join } from 'node:path' import { fileURLToPath } from 'node:url' @@ -28,9 +36,10 @@ const server = await createServer({ server: { middlewareMode: true, hmr: false, ws: false, watch: null }, resolve: { alias: { '@map': join(ROOT, 'library') } }, }) -let cat, STOCK +let cat, STOCK, PD try { cat = await server.ssrLoadModule('/plugin-designs/poster-design/src/templates.js') + PD = await server.ssrLoadModule('/plugin-designs/poster-design/src/pluginDesigns.js') ;({ STOCK } = await server.ssrLoadModule('/library/packet/card-render.js')) } finally { await server.close() } @@ -55,11 +64,8 @@ else { } // 2. names -const seen = new Set() for (const c of configs) { if (!/^[a-z0-9]+(?:[.-][a-z0-9]+)*$/i.test(c.name)) fail(`'${c.name}' is not a configuration name (words joined by . or -)`) - if (seen.has(c.name)) fail(`two configurations are named '${c.name}'`) - seen.add(c.name) } // 3. stocks @@ -69,6 +75,15 @@ if (JSON.stringify(stockNames) !== JSON.stringify(want)) fail(`card configuratio // 4. keys exist in English const en = JSON.parse(readFileSync(join(ROOT, 'library/i18n/locales/en/posterDesign.json'), 'utf8')) +// any catalogue key, in any area: "." in library/i18n/locales/en/.json +const enKey = (key) => { + const [area, ...parts] = String(key).split('.') + const file = join(ROOT, `library/i18n/locales/en/${area}.json`) + if (!parts.length || !existsSync(file)) return false + let o = JSON.parse(readFileSync(file, 'utf8')) + for (const p of parts) { if (!o || typeof o !== 'object' || !(p in o)) return false; o = o[p] } + return typeof o === 'string' +} const has = (key) => { const parts = String(key).split('.') if (parts.shift() !== 'posterDesign') return false @@ -86,9 +101,74 @@ const s = (data.countries || []).find((c) => c.id === cat.SAMPLE_COUNTRY_ID) if (!s) fail(`sample '${cat.SAMPLE_COUNTRY_ID}' is not in library/data/country-posters.json`) else if (!s.hasGeometry || !s.sheet || s.unsupportedReason) fail(`sample '${s.id}' cannot be drawn (hasGeometry ${s.hasGeometry}, sheet ${s.sheet})`) +// 6-8: plugin designs, unique names, the ledger. Gathered once, judged by one function, so --self-test poisons the +// same function the real run uses. +function gatherBlocks() { + const out = [] + const base = join(ROOT, 'plugin-designs') + for (const plugin of readdirSync(base, { withFileTypes: true }).filter((d) => d.isDirectory()).map((d) => d.name)) { + for (const f of readdirSync(join(base, plugin)).filter((x) => x.endsWith('.meta.json'))) { + let meta + try { meta = JSON.parse(readFileSync(join(base, plugin, f), 'utf8')) } catch { continue } + if (meta.posterDesign === undefined) continue + const profile = f.replace(/\.meta\.json$/, '') + const inst = readdirSync(join(base, plugin)).find((x) => x.endsWith('.instances.json')) + const presets = inst ? ((JSON.parse(readFileSync(join(base, plugin, inst), 'utf8')).instances) || []).map((i) => i.id) : [] + const blocks = PD.blocksOf(meta.posterDesign) + if (!blocks.length) out.push({ where: `${plugin}/${f}`, block: meta.posterDesign, presets }) + for (const block of blocks) out.push({ where: `${plugin}/${profile}.meta.json`, block, presets }) + } + } + return out +} +function designArms({ names, blocks, ledger }) { + const fs = [] + for (const { where, block, presets } of blocks) { + const probs = PD.blockProblems(block) + for (const p of probs) fs.push(`6: ${where} posterDesign ${p}`) + if (probs.length) continue + if (block.labelKey && !enKey(block.labelKey)) fs.push(`6: ${where} labelKey '${block.labelKey}' is not in the English catalogue`) + if (block.instance && !presets.includes(block.instance)) fs.push(`6: ${where} instance '${block.instance}' is not one of its presets`) + } + const all = [...names, ...blocks.filter((b) => !PD.blockProblems(b.block).length).map((b) => b.block.name)] + const seen = new Set() + for (const n of all) { if (seen.has(n)) fs.push(`7: two designs are named '${n}'`); seen.add(n) } + 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)) } +} +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') +const blocks = gatherBlocks() +const real = { names: configs.map((c) => c.name), blocks, ledger } + +if (process.argv.includes('--self-test')) { + const example = { family: 'regionPoster', name: 'region-poster.66x66', size: '66x66', mobileFriendly: true } + const B = (block, presets = []) => ({ where: 'fixture/fixture.meta.json', block, presets }) + const cases = [ + ['an invalid block (mobileFriendly missing)', { ...real, blocks: [...blocks, B({ family: 'regionPoster', name: 'region-poster.x', size: '66x66' })] }, /^6: .*mobileFriendly/], + ['a name clash with templates.js', { ...real, blocks: [...blocks, B({ ...example, name: real.names[0] })] }, /^7: /], + ['a renamed ledger name', { ...real, names: real.names.map((n, i) => (i === 0 ? `${n}-renamed` : n)) }, /^8: /], + ['a block naming a preset that does not exist', { ...real, blocks: [...blocks, B({ ...example, instance: 'nowhere' }, ['africa'])] }, /^6: .*instance/], + ] + let ok = true + for (const [name, input, rx] of cases) { + const { fails } = designArms(input); const hit = fails.find((f) => rx.test(f)) + console.log(hit ? ` ✓ caught: ${name} -> ${hit}` : ` ✗ MISSED: ${name}`) + if (!hit) 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 + console.log(ok ? 'POSTER-DESIGN-CATALOGUE SELF-TEST: PASS' : 'POSTER-DESIGN-CATALOGUE SELF-TEST: FAIL') + process.exit(ok ? 0 : 1) +} +const judged = designArms(real) +judged.fails.forEach(fail) + if (failures.length) { console.error(`POSTER-DESIGN-CATALOGUE: FAIL (${failures.length})\n - ${failures.join('\n - ')}`) process.exitCode = 1 } else { - console.log(`POSTER-DESIGN-CATALOGUE: PASS — ${configs.length} configurations, ${Object.keys(cat.SHEET_FORMATS).length} sheet formats matching poster-render FORMATS, ${want.length} card stocks from STOCK, ${keys.length} catalogue keys present, sample ${cat.SAMPLE_COUNTRY_ID}`) + console.log(`POSTER-DESIGN-CATALOGUE: PASS — ${configs.length} configurations, ${Object.keys(cat.SHEET_FORMATS).length} sheet formats matching poster-render FORMATS, ${want.length} card stocks from STOCK, ${keys.length} catalogue keys present, sample ${cat.SAMPLE_COUNTRY_ID}; ${blocks.length} plugin design block(s) valid; ${ledger.length} ledger names all present${judged.fresh.length ? `; NEW (add to src/catalogue-names.json): ${judged.fresh.join(', ')}` : ''}`) } diff --git a/plugin-designs/poster-design/src/FullDetailsDialog.vue b/plugin-designs/poster-design/src/FullDetailsDialog.vue index 7b82abdf0..06d73ab3a 100644 --- a/plugin-designs/poster-design/src/FullDetailsDialog.vue +++ b/plugin-designs/poster-design/src/FullDetailsDialog.vue @@ -4,7 +4,8 @@ * * Everything a person needs to name the template to an agent, or an agent needs to find it: the * configuration name (the stable id), what it is, its size, the size of the file it prints, where its - * renderer lives, its settings and its sample data. + * renderer lives, its settings and its sample data. A design another plugin makes (config.pageUrl) shows that + * plugin's page live instead of a file size, with a link to open it. * * A NATIVE opened with showModal(), so it sits in the browser's TOP LAYER. A fixed scrim inside * this element would be captured by a host's containing block (VerticalTabsLayout sets contain:layout on @@ -63,8 +64,10 @@ async function copyName() {
{{ t('posterDesign.details.size') }}
{{ config.size }} · {{ config.px[0] }} × {{ config.px[1] }} px
+
{{ t('posterDesign.details.settings') }}
{{ settings }}
{{ t('posterDesign.details.sample') }}
@@ -72,6 +75,10 @@ async function copyName() {
{{ t('posterDesign.details.renderer') }}
{{ config.renderer }}
+

{{ t('posterDesign.details.legacy') }}

{{ t(config.missing) }}

{{ agentLine }}

diff --git a/plugin-designs/poster-design/src/catalogue-names.json b/plugin-designs/poster-design/src/catalogue-names.json new file mode 100644 index 000000000..412cfa362 --- /dev/null +++ b/plugin-designs/poster-design/src/catalogue-names.json @@ -0,0 +1,21 @@ +{ + "//": "Every poster design name ever shipped: templates.js and every plugin's posterDesign block. check:poster-design-catalogue fails when a name here no longer exists, because a name is what people hand an agent (a retired design stays as a legacy entry). Append new names; never edit or remove one.", + "names": [ + "map-sheet.country.3x3", + "map-sheet.country.60x96", + "map-sheet.country.66x66", + "map-sheet.country.36x60", + "map-sheet.country.3x4", + "map-sheet.country.letter", + "map-sheet.packet-page", + "packet.full", + "packet.lightweight", + "cards.poker", + "cards.pokerLandscape", + "cards.postcard", + "cards.postcardLandscape", + "cards.a6", + "wall.bless-panels", + "map-print.dialog" + ] +} diff --git a/plugin-designs/poster-design/src/pluginDesigns.js b/plugin-designs/poster-design/src/pluginDesigns.js new file mode 100644 index 000000000..e4aa168a8 --- /dev/null +++ b/plugin-designs/poster-design/src/pluginDesigns.js @@ -0,0 +1,76 @@ +/** + * pluginDesigns.js — poster designs that OTHER plugins declare, read from the tree's manifest.json. + * + * A poster plugin lists its designs in its OWN .meta.json, under "posterDesign" (one object, or an array): + * + * "posterDesign": { "family": "regionPoster", "name": "region-poster.66x66", "size": "66x66", + * "mobileFriendly": true, "labelKey": "regions.posterTitle", "instance": "africa" } + * + * family a word naming the kind of poster (camelCase or dashes) + * name the design's stable configuration name, the same rule as templates.js: words joined by "." or + * "-". Once shipped it is never renamed or removed (check:poster-design-catalogue's names ledger). + * size what it prints at (a ladder key such as "66x66", or "letter", "a4", "phone" …) + * mobileFriendly true when it reads well on a phone + * labelKey optional: the card's title, a catalogue key the plugin owns; else the profile's own label + * instance optional: a preset id (capability 2) whose page shows this design; else the profile's page + * + * The build carries the block into manifest.json (internal/profile-meta.mjs), so Poster Design lists every plugin's + * designs without importing any plugin's code, and a plugin adding its block never makes Poster Design rebuild. + * Pure (no Vue, no DOM), so check:poster-design-catalogue validates with the same rules the page filters by. + */ +export const NAME_RE = /^[a-z0-9]+(?:[.-][a-z0-9]+)*$/ +const FAMILY_RE = /^[a-z][a-zA-Z0-9-]*$/ +const KEY_RE = /^[a-zA-Z][\w-]*(?:\.[\w-]+)+$/ + +/** The blocks a meta's posterDesign holds, as an array (one object or an array of them). */ +export const blocksOf = (posterDesign) => (Array.isArray(posterDesign) ? posterDesign : posterDesign ? [posterDesign] : []) + +/** What is wrong with one block, as sentences; [] when it is valid. */ +export function blockProblems(b) { + const out = [] + if (!b || typeof b !== 'object' || Array.isArray(b)) return ['is not an object'] + if (typeof b.family !== 'string' || !FAMILY_RE.test(b.family)) out.push(`family ${JSON.stringify(b.family)} is not a word`) + if (typeof b.name !== 'string' || !NAME_RE.test(b.name) || !b.name.includes('.')) out.push(`name ${JSON.stringify(b.name)} is not a configuration name (words joined by "." or "-", with at least one ".")`) + if (typeof b.size !== 'string' || !b.size.trim()) out.push('size is missing') + if (typeof b.mobileFriendly !== 'boolean') out.push('mobileFriendly is not true or false') + if (b.labelKey !== undefined && (typeof b.labelKey !== 'string' || !KEY_RE.test(b.labelKey))) out.push(`labelKey ${JSON.stringify(b.labelKey)} is not a catalogue key`) + if (b.instance !== undefined && (typeof b.instance !== 'string' || !b.instance.trim())) out.push('instance is empty') + const known = new Set(['family', 'name', 'size', 'mobileFriendly', 'labelKey', 'instance']) + for (const k of Object.keys(b)) if (!known.has(k)) out.push(`unknown field "${k}"`) + return out +} + +/** + * Every valid design in a manifest, with the page that shows it as an absolute URL (resolved against the manifest's + * own URL, since the manifest's URLs are tree-relative). An invalid block is skipped here; the check names it. + */ +export function designsFromManifest(manifest, manifestUrl) { + const out = [] + for (const b of (manifest && manifest.bundles) || []) { + for (const p of b.profiles || []) { + for (const d of blocksOf(p.posterDesign)) { + if (blockProblems(d).length) continue + const inst = d.instance ? (p.instances || []).find((i) => i.id === d.instance) : null + if (d.instance && !inst) continue + const rel = inst ? inst.iframeUrl : p.iframeUrl + let pageUrl = rel + try { pageUrl = new URL(rel, manifestUrl).href } catch { /* keep it as written */ } + out.push({ name: d.name, family: d.family, size: d.size, mobileFriendly: d.mobileFriendly, labelKey: d.labelKey || '', + label: (inst && inst.title) || p.label || p.profile, bundle: b.bundle, profile: p.profile, instance: d.instance || '', pageUrl }) + } + } + } + return out +} + +/** The first manifest that answers, from a list of candidate URLs; { designs: [] } when none does. */ +export async function loadPluginDesigns(urls, fetchImpl = globalThis.fetch) { + for (const url of urls.filter(Boolean)) { + try { + const r = await fetchImpl(url) // same-origin credentials (the default): a gated tree still answers + if (!r.ok) continue + return { url, designs: designsFromManifest(await r.json(), url) } + } catch { /* the next candidate */ } + } + return { url: '', designs: [] } +} diff --git a/public/dx-maps/doxa-maps-spa/manifest.json b/public/dx-maps/doxa-maps-spa/manifest.json index b1a849f8b..27a26c638 100644 --- a/public/dx-maps/doxa-maps-spa/manifest.json +++ b/public/dx-maps/doxa-maps-spa/manifest.json @@ -1117,7 +1117,7 @@ "bundle": "poster-design", "scriptUrl": "./poster-design/poster-design.js", "output": "dx-maps/poster-design/poster-design.js", - "integrity": "sha384-oWAaZ9tHeKpnEKXJMio2ps1j7FLkvKZK/cG9ubEkDNz20LJqB/zMd1NnzDCfxD9W", + "integrity": "sha384-U1pLdFLmYorU+hLPzhBnjnu7jCaKtjUlRB/4UMoAF9MFhAPWU6YZhv7471EXDiEv", "elements": [ "poster-design" ], @@ -1140,7 +1140,7 @@ "scriptUrl": "./poster-design/poster-design.js", "embed": { "iframe": "", - "script": "\n\n\n" + "script": "\n\n\n" } }, { @@ -1156,7 +1156,7 @@ "scriptUrl": "./poster-design/poster-design.js", "embed": { "iframe": "", - "script": "\n\n\n" + "script": "\n\n\n" } }, { @@ -1172,7 +1172,7 @@ "scriptUrl": "./poster-design/poster-design.js", "embed": { "iframe": "", - "script": "\n\n\n" + "script": "\n\n\n" } } ] diff --git a/public/dx-maps/manifest.json b/public/dx-maps/manifest.json index b1a849f8b..27a26c638 100644 --- a/public/dx-maps/manifest.json +++ b/public/dx-maps/manifest.json @@ -1117,7 +1117,7 @@ "bundle": "poster-design", "scriptUrl": "./poster-design/poster-design.js", "output": "dx-maps/poster-design/poster-design.js", - "integrity": "sha384-oWAaZ9tHeKpnEKXJMio2ps1j7FLkvKZK/cG9ubEkDNz20LJqB/zMd1NnzDCfxD9W", + "integrity": "sha384-U1pLdFLmYorU+hLPzhBnjnu7jCaKtjUlRB/4UMoAF9MFhAPWU6YZhv7471EXDiEv", "elements": [ "poster-design" ], @@ -1140,7 +1140,7 @@ "scriptUrl": "./poster-design/poster-design.js", "embed": { "iframe": "", - "script": "\n\n\n" + "script": "\n\n\n" } }, { @@ -1156,7 +1156,7 @@ "scriptUrl": "./poster-design/poster-design.js", "embed": { "iframe": "", - "script": "\n\n\n" + "script": "\n\n\n" } }, { @@ -1172,7 +1172,7 @@ "scriptUrl": "./poster-design/poster-design.js", "embed": { "iframe": "", - "script": "\n\n\n" + "script": "\n\n\n" } } ] diff --git a/public/dx-maps/poster-design/poster-design-mfe/index.html b/public/dx-maps/poster-design/poster-design-mfe/index.html index a504e1f19..7b9538d31 100644 --- a/public/dx-maps/poster-design/poster-design-mfe/index.html +++ b/public/dx-maps/poster-design/poster-design-mfe/index.html @@ -73,7 +73,7 @@ - + - + - +