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 <profile>.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.
This commit is contained in:
parent
078378199a
commit
9bd2177687
24 changed files with 469 additions and 210 deletions
|
|
@ -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. */
|
||||
|
|
|
|||
|
|
@ -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": "تصنعه إضافته الخاصة. اعرض كل التفاصيل لتراه مباشرة."
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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."
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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."
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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."
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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."
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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": "यह अपने ही प्लगइन से बनता है। पूरा विवरण दिखाएँ इसे लाइव दिखाता है।"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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."
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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."
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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."
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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": "Создаётся собственным плагином. «Показать все подробности» покажет его вживую."
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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": "由其自身插件生成。点击“显示全部详情”可实时查看。"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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/<plugin>/<profile>.meta.json: family, name, size, mobileFriendly, optional labelKey and instance), keeps names unique across templates.js and all blocks, and fails when a name in the names ledger (src/catalogue-names.json) was renamed or removed.",
|
||||
"check:poster-design-catalogue:self-test": "an invalid posterDesign block, a name that clashes with templates.js, a renamed ledger name 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/<id>.webp answered 200 text/html, and export handed that page to the print dialog as the image. Asserts library/packet/poster-fetch.js accepts a raster only if it is an image (by type or by its first bytes) and a source only if it is a poster document (class=\"stage\"), and that neither usePosterPrints nor PosterWall fetches posters/ directly. Node only, no server.",
|
||||
"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/<id>.html instead of opening on \"Poster not available yet\". Needs a served tree built with posterSources:true; NOT MEASURED otherwise.",
|
||||
|
|
|
|||
|
|
@ -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')
|
||||
|
|
|
|||
|
|
@ -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/<plugin>/<profile>.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: "<area>.<path>" in library/i18n/locales/en/<area>.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(', ')}` : ''}`)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 <dialog> 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() {
|
|||
<button type="button" class="pd-dialog__copy" @click="copyName">{{ t('posterDesign.details.copyName') }}</button></dd>
|
||||
<dt>{{ t('posterDesign.details.size') }}</dt>
|
||||
<dd dir="ltr">{{ config.size }}<span v-if="config.px"> · {{ config.px[0] }} × {{ config.px[1] }} px</span></dd>
|
||||
<template v-if="!config.pageUrl">
|
||||
<dt>{{ t('posterDesign.details.fileSize') }}</dt>
|
||||
<dd>{{ fileSize }}</dd>
|
||||
</template>
|
||||
<dt>{{ t('posterDesign.details.settings') }}</dt>
|
||||
<dd dir="ltr">{{ settings }}</dd>
|
||||
<dt>{{ t('posterDesign.details.sample') }}</dt>
|
||||
|
|
@ -72,6 +75,10 @@ async function copyName() {
|
|||
<dt>{{ t('posterDesign.details.renderer') }}</dt>
|
||||
<dd dir="ltr"><code>{{ config.renderer }}</code></dd>
|
||||
</dl>
|
||||
<div v-if="config.pageUrl" class="pd-dialog__live">
|
||||
<iframe class="pd-dialog__frame" :src="config.pageUrl" :title="label" loading="lazy"></iframe>
|
||||
<a class="pd-dialog__open" :href="config.pageUrl" target="_blank" rel="noopener">{{ t('posterDesign.details.openPage') }}</a>
|
||||
</div>
|
||||
<p v-if="config.legacy" class="pd-dialog__note">{{ t('posterDesign.details.legacy') }}</p>
|
||||
<p v-if="config.missing" class="pd-dialog__note">{{ t(config.missing) }}</p>
|
||||
<p class="pd-dialog__agent">{{ agentLine }}</p>
|
||||
|
|
|
|||
21
plugin-designs/poster-design/src/catalogue-names.json
Normal file
21
plugin-designs/poster-design/src/catalogue-names.json
Normal file
|
|
@ -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"
|
||||
]
|
||||
}
|
||||
76
plugin-designs/poster-design/src/pluginDesigns.js
Normal file
76
plugin-designs/poster-design/src/pluginDesigns.js
Normal file
|
|
@ -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 <profile>.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: [] }
|
||||
}
|
||||
|
|
@ -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": "<iframe src=\"./poster-design/poster-design-mfe/index.html?utm_source=YOUR_SOURCE\" width=\"100%\" height=\"600\" style=\"border:none;border-radius:12px;\" allowfullscreen></iframe>",
|
||||
"script": "<script>window.MAP_APP_API_URL=\"https://pray.doxa.life\";</script>\n<script src=\"./poster-design/poster-design.js\" integrity=\"sha384-oWAaZ9tHeKpnEKXJMio2ps1j7FLkvKZK/cG9ubEkDNz20LJqB/zMd1NnzDCfxD9W\" crossorigin=\"anonymous\"></script>\n<!-- provider: \"maplibre\" is the default; change it to \"mapbox\" and fill tk to use Mapbox. -->\n<poster-design profile-config='{\"profile\":\"poster-design-mfe\",\"provider\":\"maplibre\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></poster-design>"
|
||||
"script": "<script>window.MAP_APP_API_URL=\"https://pray.doxa.life\";</script>\n<script src=\"./poster-design/poster-design.js\" integrity=\"sha384-U1pLdFLmYorU+hLPzhBnjnu7jCaKtjUlRB/4UMoAF9MFhAPWU6YZhv7471EXDiEv\" crossorigin=\"anonymous\"></script>\n<!-- provider: \"maplibre\" is the default; change it to \"mapbox\" and fill tk to use Mapbox. -->\n<poster-design profile-config='{\"profile\":\"poster-design-mfe\",\"provider\":\"maplibre\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></poster-design>"
|
||||
}
|
||||
},
|
||||
{
|
||||
|
|
@ -1156,7 +1156,7 @@
|
|||
"scriptUrl": "./poster-design/poster-design.js",
|
||||
"embed": {
|
||||
"iframe": "<iframe src=\"./poster-design/poster-sizes/index.html?utm_source=YOUR_SOURCE\" width=\"100%\" height=\"600\" style=\"border:none;border-radius:12px;\" allowfullscreen></iframe>",
|
||||
"script": "<script>window.MAP_APP_API_URL=\"https://pray.doxa.life\";</script>\n<script src=\"./poster-design/poster-design.js\" integrity=\"sha384-oWAaZ9tHeKpnEKXJMio2ps1j7FLkvKZK/cG9ubEkDNz20LJqB/zMd1NnzDCfxD9W\" crossorigin=\"anonymous\"></script>\n<!-- provider: \"maplibre\" is the default; change it to \"mapbox\" and fill tk to use Mapbox. -->\n<poster-design profile-config='{\"profile\":\"poster-sizes\",\"provider\":\"maplibre\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></poster-design>"
|
||||
"script": "<script>window.MAP_APP_API_URL=\"https://pray.doxa.life\";</script>\n<script src=\"./poster-design/poster-design.js\" integrity=\"sha384-U1pLdFLmYorU+hLPzhBnjnu7jCaKtjUlRB/4UMoAF9MFhAPWU6YZhv7471EXDiEv\" crossorigin=\"anonymous\"></script>\n<!-- provider: \"maplibre\" is the default; change it to \"mapbox\" and fill tk to use Mapbox. -->\n<poster-design profile-config='{\"profile\":\"poster-sizes\",\"provider\":\"maplibre\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></poster-design>"
|
||||
}
|
||||
},
|
||||
{
|
||||
|
|
@ -1172,7 +1172,7 @@
|
|||
"scriptUrl": "./poster-design/poster-design.js",
|
||||
"embed": {
|
||||
"iframe": "<iframe src=\"./poster-design/poster-styles/index.html?utm_source=YOUR_SOURCE\" width=\"100%\" height=\"600\" style=\"border:none;border-radius:12px;\" allowfullscreen></iframe>",
|
||||
"script": "<script>window.MAP_APP_API_URL=\"https://pray.doxa.life\";</script>\n<script src=\"./poster-design/poster-design.js\" integrity=\"sha384-oWAaZ9tHeKpnEKXJMio2ps1j7FLkvKZK/cG9ubEkDNz20LJqB/zMd1NnzDCfxD9W\" crossorigin=\"anonymous\"></script>\n<!-- provider: \"maplibre\" is the default; change it to \"mapbox\" and fill tk to use Mapbox. -->\n<poster-design profile-config='{\"profile\":\"poster-styles\",\"provider\":\"maplibre\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></poster-design>"
|
||||
"script": "<script>window.MAP_APP_API_URL=\"https://pray.doxa.life\";</script>\n<script src=\"./poster-design/poster-design.js\" integrity=\"sha384-U1pLdFLmYorU+hLPzhBnjnu7jCaKtjUlRB/4UMoAF9MFhAPWU6YZhv7471EXDiEv\" crossorigin=\"anonymous\"></script>\n<!-- provider: \"maplibre\" is the default; change it to \"mapbox\" and fill tk to use Mapbox. -->\n<poster-design profile-config='{\"profile\":\"poster-styles\",\"provider\":\"maplibre\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></poster-design>"
|
||||
}
|
||||
}
|
||||
]
|
||||
|
|
|
|||
|
|
@ -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": "<iframe src=\"./poster-design/poster-design-mfe/index.html?utm_source=YOUR_SOURCE\" width=\"100%\" height=\"600\" style=\"border:none;border-radius:12px;\" allowfullscreen></iframe>",
|
||||
"script": "<script>window.MAP_APP_API_URL=\"https://pray.doxa.life\";</script>\n<script src=\"./poster-design/poster-design.js\" integrity=\"sha384-oWAaZ9tHeKpnEKXJMio2ps1j7FLkvKZK/cG9ubEkDNz20LJqB/zMd1NnzDCfxD9W\" crossorigin=\"anonymous\"></script>\n<!-- provider: \"maplibre\" is the default; change it to \"mapbox\" and fill tk to use Mapbox. -->\n<poster-design profile-config='{\"profile\":\"poster-design-mfe\",\"provider\":\"maplibre\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></poster-design>"
|
||||
"script": "<script>window.MAP_APP_API_URL=\"https://pray.doxa.life\";</script>\n<script src=\"./poster-design/poster-design.js\" integrity=\"sha384-U1pLdFLmYorU+hLPzhBnjnu7jCaKtjUlRB/4UMoAF9MFhAPWU6YZhv7471EXDiEv\" crossorigin=\"anonymous\"></script>\n<!-- provider: \"maplibre\" is the default; change it to \"mapbox\" and fill tk to use Mapbox. -->\n<poster-design profile-config='{\"profile\":\"poster-design-mfe\",\"provider\":\"maplibre\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></poster-design>"
|
||||
}
|
||||
},
|
||||
{
|
||||
|
|
@ -1156,7 +1156,7 @@
|
|||
"scriptUrl": "./poster-design/poster-design.js",
|
||||
"embed": {
|
||||
"iframe": "<iframe src=\"./poster-design/poster-sizes/index.html?utm_source=YOUR_SOURCE\" width=\"100%\" height=\"600\" style=\"border:none;border-radius:12px;\" allowfullscreen></iframe>",
|
||||
"script": "<script>window.MAP_APP_API_URL=\"https://pray.doxa.life\";</script>\n<script src=\"./poster-design/poster-design.js\" integrity=\"sha384-oWAaZ9tHeKpnEKXJMio2ps1j7FLkvKZK/cG9ubEkDNz20LJqB/zMd1NnzDCfxD9W\" crossorigin=\"anonymous\"></script>\n<!-- provider: \"maplibre\" is the default; change it to \"mapbox\" and fill tk to use Mapbox. -->\n<poster-design profile-config='{\"profile\":\"poster-sizes\",\"provider\":\"maplibre\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></poster-design>"
|
||||
"script": "<script>window.MAP_APP_API_URL=\"https://pray.doxa.life\";</script>\n<script src=\"./poster-design/poster-design.js\" integrity=\"sha384-U1pLdFLmYorU+hLPzhBnjnu7jCaKtjUlRB/4UMoAF9MFhAPWU6YZhv7471EXDiEv\" crossorigin=\"anonymous\"></script>\n<!-- provider: \"maplibre\" is the default; change it to \"mapbox\" and fill tk to use Mapbox. -->\n<poster-design profile-config='{\"profile\":\"poster-sizes\",\"provider\":\"maplibre\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></poster-design>"
|
||||
}
|
||||
},
|
||||
{
|
||||
|
|
@ -1172,7 +1172,7 @@
|
|||
"scriptUrl": "./poster-design/poster-design.js",
|
||||
"embed": {
|
||||
"iframe": "<iframe src=\"./poster-design/poster-styles/index.html?utm_source=YOUR_SOURCE\" width=\"100%\" height=\"600\" style=\"border:none;border-radius:12px;\" allowfullscreen></iframe>",
|
||||
"script": "<script>window.MAP_APP_API_URL=\"https://pray.doxa.life\";</script>\n<script src=\"./poster-design/poster-design.js\" integrity=\"sha384-oWAaZ9tHeKpnEKXJMio2ps1j7FLkvKZK/cG9ubEkDNz20LJqB/zMd1NnzDCfxD9W\" crossorigin=\"anonymous\"></script>\n<!-- provider: \"maplibre\" is the default; change it to \"mapbox\" and fill tk to use Mapbox. -->\n<poster-design profile-config='{\"profile\":\"poster-styles\",\"provider\":\"maplibre\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></poster-design>"
|
||||
"script": "<script>window.MAP_APP_API_URL=\"https://pray.doxa.life\";</script>\n<script src=\"./poster-design/poster-design.js\" integrity=\"sha384-U1pLdFLmYorU+hLPzhBnjnu7jCaKtjUlRB/4UMoAF9MFhAPWU6YZhv7471EXDiEv\" crossorigin=\"anonymous\"></script>\n<!-- provider: \"maplibre\" is the default; change it to \"mapbox\" and fill tk to use Mapbox. -->\n<poster-design profile-config='{\"profile\":\"poster-styles\",\"provider\":\"maplibre\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></poster-design>"
|
||||
}
|
||||
}
|
||||
]
|
||||
|
|
|
|||
|
|
@ -73,7 +73,7 @@
|
|||
|
||||
<script>window.MAP_APP_API_URL = "https://pray.doxa.life";</script>
|
||||
<!-- Bundle IIFE — registers the <poster-design> custom element. -->
|
||||
<script src="../poster-design.js" integrity="sha384-oWAaZ9tHeKpnEKXJMio2ps1j7FLkvKZK/cG9ubEkDNz20LJqB/zMd1NnzDCfxD9W" crossorigin="anonymous"></script>
|
||||
<script src="../poster-design.js" integrity="sha384-U1pLdFLmYorU+hLPzhBnjnu7jCaKtjUlRB/4UMoAF9MFhAPWU6YZhv7471EXDiEv" crossorigin="anonymous"></script>
|
||||
|
||||
<script>
|
||||
// Mapbox token resolution — the A3 ladder, in precedence order:
|
||||
|
|
|
|||
File diff suppressed because one or more lines are too long
|
|
@ -67,7 +67,7 @@
|
|||
|
||||
<script>window.MAP_APP_API_URL = "https://pray.doxa.life";</script>
|
||||
<!-- Bundle IIFE — registers the <poster-design> custom element. -->
|
||||
<script src="../poster-design.js" integrity="sha384-oWAaZ9tHeKpnEKXJMio2ps1j7FLkvKZK/cG9ubEkDNz20LJqB/zMd1NnzDCfxD9W" crossorigin="anonymous"></script>
|
||||
<script src="../poster-design.js" integrity="sha384-U1pLdFLmYorU+hLPzhBnjnu7jCaKtjUlRB/4UMoAF9MFhAPWU6YZhv7471EXDiEv" crossorigin="anonymous"></script>
|
||||
|
||||
<script>
|
||||
// Mapbox token resolution — the A3 ladder, in precedence order:
|
||||
|
|
|
|||
|
|
@ -67,7 +67,7 @@
|
|||
|
||||
<script>window.MAP_APP_API_URL = "https://pray.doxa.life";</script>
|
||||
<!-- Bundle IIFE — registers the <poster-design> custom element. -->
|
||||
<script src="../poster-design.js" integrity="sha384-oWAaZ9tHeKpnEKXJMio2ps1j7FLkvKZK/cG9ubEkDNz20LJqB/zMd1NnzDCfxD9W" crossorigin="anonymous"></script>
|
||||
<script src="../poster-design.js" integrity="sha384-U1pLdFLmYorU+hLPzhBnjnu7jCaKtjUlRB/4UMoAF9MFhAPWU6YZhv7471EXDiEv" crossorigin="anonymous"></script>
|
||||
|
||||
<script>
|
||||
// Mapbox token resolution — the A3 ladder, in precedence order:
|
||||
|
|
|
|||
Loading…
Reference in a new issue