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 @@ - + - + - +