From 24b9ada48e80d6af2da675029f78c085e27e5c83 Mon Sep 17 00:00:00 2001 From: Doxa Maps Date: Sun, 27 Sep 2026 05:14:34 +0000 Subject: [PATCH] Plugin Studio and the gallery split production from experimental, from each bundle's own stage The Driver: a public plugin that is not yet a production map shows under Experimental until it is. A profile's .meta.json declares "stage": "production" | "experimental" (default experimental; a bundle is production if any profile says so) and "heavy": true for a live WebGL owner. generate-tree carries both into manifest.json, so the Studio and the gallery read the same field and neither keeps a list. The seven production bundles are marked: dashboard-plus-grid, research-map-plus-grid, regions-dashboard, my-upg-100-list, doxa-simple-map, cities-internet, gospel-resources. The Studio rail keeps its capability sections for them and puts every experimental bundle (gospel-internet among them) in one final Experimental section. The gallery's auto-discovered Experimental folder skips a production bundle. gospel-internet-board joins HEAVY_ELEMENTS (it can own two WebGL contexts). check:studio-rail asserts the split. --- internal/check-studio-rail.mjs | 27 ++++++++++++++----- internal/generate-tree.mjs | 20 ++++++++++++++ library/i18n/locales/ar/studio.json | 1 + library/i18n/locales/de/studio.json | 1 + library/i18n/locales/en/studio.json | 1 + library/i18n/locales/es/studio.json | 1 + library/i18n/locales/fr/studio.json | 1 + library/i18n/locales/hi/studio.json | 1 + library/i18n/locales/it/studio.json | 1 + library/i18n/locales/pt/studio.json | 1 + library/i18n/locales/ro/studio.json | 1 + library/i18n/locales/ru/studio.json | 1 + library/i18n/locales/zh/studio.json | 1 + .../connectivity-map.meta.json | 1 + .../dashboard-plus-grid.meta.json | 1 + .../doxa-maps-spa/doxa-maps-spa.vue | 2 +- .../doxa-simple-map/doxa-simple-map.meta.json | 4 +++ .../gospel-dashboard.meta.json | 1 + .../my-upg-100-list/upg-100-mfe.meta.json | 1 + .../plugin-studio/plugin-studio.vue | 24 +++++++++++++---- .../regions-dashboard/regions-mfe.meta.json | 1 + .../research-map-plus-grid.meta.json | 1 + 22 files changed, 82 insertions(+), 12 deletions(-) create mode 100644 plugin-designs/doxa-simple-map/doxa-simple-map.meta.json diff --git a/internal/check-studio-rail.mjs b/internal/check-studio-rail.mjs index 3bf716d0f..57bd5ec0c 100644 --- a/internal/check-studio-rail.mjs +++ b/internal/check-studio-rail.mjs @@ -5,7 +5,9 @@ * The Driver's ask (2026-09-26): one FOLDER per BUNDLE with its ELEMENTS inside, bundles sorted by * capability, highest first. Runs the studio's REAL treeSections / compositionTrees / rowsFromManifest * (loaded through Vite, no browser) against the BUILT manifest and asserts: - * - sections run CAP 5 → 1, and each bundle sits in the section of its HIGHEST capability, once; + * - PRODUCTION bundles (manifest bundle.stage, from their .meta.json) sit in capability sections that run + * CAP 5 → 1, each in the section of its HIGHEST capability, once; every EXPERIMENTAL bundle sits in one + * final Experimental section, highest capability first (the Driver, 2026-09-27); * - bundles within a section are in name order; * - a bundle with several elements is ONE folder holding all of them; a single-element bundle is its element; * - every studio row has exactly ONE canonical node (one identity per plugin; parts only target it). @@ -36,7 +38,11 @@ try { ctx.resolveRef = (ref, fb) => comp.methods.resolveRef.call(ctx, ref, fb) ctx.compositionTrees = comp.computed.compositionTrees.call(ctx) const secs = comp.computed.treeSections.call(ctx) - const caps = secs.map((s) => s.cap) + const stageOf = new Map(manifest.bundles.map((b) => [b.bundle, b.stage === 'production' ? 'production' : 'experimental'])) + const expIdx = secs.findIndex((s) => s.id === 'stage-experimental') + if (expIdx !== -1 && expIdx !== secs.length - 1) fail('the Experimental section is not last') + const capSecs = secs.filter((s) => s.id !== 'stage-experimental') + const caps = capSecs.map((s) => s.cap) if (caps.join() !== [...caps].sort((a, b) => b - a).join()) fail(`sections not CAP 5 → 1: ${caps.join(',')}`) const byBundle = new Map() for (const r of ctx.rows) { if (!byBundle.has(r.bundle)) byBundle.set(r.bundle, []); byBundle.get(r.bundle).push(r) } @@ -44,13 +50,21 @@ try { for (const s of secs) { const tops = s.nodes.filter((n) => n.depth === 0) const names = tops.map((n) => (n.folder ? n.label : ctx.rows.find((r) => r.key === n.target)?.bundle)) - if (names.join() !== [...names].sort((a, b) => a.localeCompare(b)).join()) fail(`CAP ${s.cap}: bundles not in name order (${names.join(', ')})`) + const isExp = s.id === 'stage-experimental' + const topCap = (b) => Math.max(...(byBundle.get(b) || []).map((r) => r.cap)) + const want = isExp ? [...names].sort((a, b) => topCap(b) - topCap(a) || a.localeCompare(b)) : [...names].sort((a, b) => a.localeCompare(b)) + if (names.join() !== want.join()) fail(`${isExp ? 'Experimental' : 'CAP ' + s.cap}: bundles not in order (${names.join(', ')})`) + for (const b of names) { + const st = stageOf.get(b) + if (isExp && st === 'production') fail(`production bundle ${b} is filed under Experimental`) + if (!isExp && st !== 'production') fail(`experimental bundle ${b} is filed under CAP ${s.cap}, not Experimental`) + } for (const [i, n] of tops.entries()) { const b = names[i]; const rows = byBundle.get(b) || [] if (seen.has(b)) fail(`bundle ${b} appears twice (CAP ${seen.get(b)} and CAP ${s.cap})`) seen.set(b, s.cap) const top = Math.max(...rows.map((r) => r.cap)) - if (top !== s.cap) fail(`bundle ${b} (highest CAP ${top}) filed under CAP ${s.cap}`) + if (!isExp && top !== s.cap) fail(`bundle ${b} (highest CAP ${top}) filed under CAP ${s.cap}`) if (rows.length > 1) { if (!n.folder) fail(`bundle ${b} has ${rows.length} elements but is not a folder`) const kids = s.nodes.filter((k) => k.parent === n.id && k.canonical).map((k) => k.target).sort() @@ -61,7 +75,8 @@ try { for (const b of byBundle.keys()) if (!seen.has(b)) fail(`bundle ${b} is missing from the rail`) const canon = secs.flatMap((s) => s.nodes.filter((n) => n.canonical)) for (const r of ctx.rows) { const c = canon.filter((n) => n.target === r.key).length; if (c !== 1) fail(`${r.key} has ${c} canonical nodes (want 1)`) } - console.log(`STUDIO-RAIL: ${byBundle.size} bundle(s), ${ctx.rows.length} element(s), ${secs.length} capability section(s); folders: ${secs.flatMap((s) => s.nodes.filter((n) => n.folder).map((n) => n.label + '(' + n.kids + ')')).join(', ')}`) + const nProd = [...stageOf.values()].filter((v) => v === 'production').length + console.log(`STUDIO-RAIL: ${byBundle.size} bundle(s) (${nProd} production, ${byBundle.size - nProd} experimental), ${ctx.rows.length} element(s), ${secs.length} section(s); folders: ${secs.flatMap((s) => s.nodes.filter((n) => n.folder).map((n) => n.label + '(' + n.kids + ')')).join(', ')}`) } finally { await server.close() } if (bad) { console.log(`STUDIO-RAIL: FAIL — ${bad} problem(s).`); process.exit(1) } -console.log('STUDIO-RAIL: PASS — one folder per bundle with its elements inside, bundles grouped by their highest capability (5 → 1) and in name order, one identity per plugin.') +console.log('STUDIO-RAIL: PASS — one folder per bundle with its elements inside; production bundles grouped by their highest capability (5 → 1) in name order, experimental bundles in one final Experimental section; one identity per plugin.') diff --git a/internal/generate-tree.mjs b/internal/generate-tree.mjs index e5f37c57a..fbe521730 100644 --- a/internal/generate-tree.mjs +++ b/internal/generate-tree.mjs @@ -301,6 +301,22 @@ function profileReachesAMap(dir, file) { } return false } +/** + * STAGE and HEAVY, read from .meta.json and carried into manifest.json (the Driver, 2026-09-27: + * a public-but-unfinished plugin shows under EXPERIMENTAL in the Plugin Studio and the gallery until it works + * as a production map). Both surfaces read the MANIFEST, so they cannot disagree, and neither keeps a list. + * "stage": "production" | "experimental" default experimental; a bundle is production if ANY of its + * profiles says so (a bundle is one folder in the studio) + * "heavy": true the element holds live WebGL contexts, so a host must destroy + * it when it leaves the screen (Plugin Studio's HEAVY rows) + */ +function metaStageHeavy(dir, profileName) { + try { + const meta = JSON.parse(readFileSync(join(dir, `${profileName}.meta.json`), 'utf8')) + return { stage: meta?.stage === 'production' ? 'production' : 'experimental', heavy: meta?.heavy === true } + } catch { return { stage: 'experimental', heavy: false } } +} + function metaIsMap(dir, profileName) { try { const meta = JSON.parse(readFileSync(join(dir, `${profileName}.meta.json`), 'utf8')) @@ -1286,8 +1302,11 @@ function scanBundles() { ? composesAMapLeaf(dir, profileName, file) : (metaIsMap(dir, profileName) ?? profileReachesAMap(dir, file)) + const { stage: profileStage, heavy: profileHeavy } = metaStageHeavy(dir, profileName) return { profile: profileName, + stage: profileStage, + ...(profileHeavy ? { heavy: true } : {}), file, description: description || fileTag || '', element: tag, @@ -1341,6 +1360,7 @@ function scanBundles() { integrity, elements: bundleElements, ...(composesUnique.length ? { composes: composesUnique } : {}), + stage: profiles.some((p) => p.stage === 'production') ? 'production' : 'experimental', profiles, }) } diff --git a/library/i18n/locales/ar/studio.json b/library/i18n/locales/ar/studio.json index 6221f7373..220c19ad5 100644 --- a/library/i18n/locales/ar/studio.json +++ b/library/i18n/locales/ar/studio.json @@ -21,6 +21,7 @@ "cap3": "متعدد العروض", "cap2": "إعداد مسبق", "cap1": "مفرد", + "stageExperimental": "تجريبي", "toggleList": "إظهار القائمة أو إخفاؤها", "openViews": "فتح قائمة العروض", "expandList": "توسيع القائمة", diff --git a/library/i18n/locales/de/studio.json b/library/i18n/locales/de/studio.json index 3ff841aea..10508304f 100644 --- a/library/i18n/locales/de/studio.json +++ b/library/i18n/locales/de/studio.json @@ -21,6 +21,7 @@ "cap3": "Mehrfachansicht", "cap2": "Voreinstellung", "cap1": "Einzeln", + "stageExperimental": "Experimentell", "toggleList": "Liste ein-/ausblenden", "openViews": "Ansichtsliste öffnen", "expandList": "Liste ausklappen", diff --git a/library/i18n/locales/en/studio.json b/library/i18n/locales/en/studio.json index 404ed5f1f..9a8bad225 100644 --- a/library/i18n/locales/en/studio.json +++ b/library/i18n/locales/en/studio.json @@ -21,6 +21,7 @@ "cap3": "Multi-view", "cap2": "Preset", "cap1": "Single", + "stageExperimental": "Experimental", "toggleList": "Toggle list", "openViews": "Open the list of views", "expandList": "Expand list", diff --git a/library/i18n/locales/es/studio.json b/library/i18n/locales/es/studio.json index c45204989..9d058f3f4 100644 --- a/library/i18n/locales/es/studio.json +++ b/library/i18n/locales/es/studio.json @@ -21,6 +21,7 @@ "cap3": "Multivista", "cap2": "Preajuste", "cap1": "Individual", + "stageExperimental": "Experimental", "toggleList": "Mostrar u ocultar la lista", "openViews": "Abrir la lista de vistas", "expandList": "Expandir la lista", diff --git a/library/i18n/locales/fr/studio.json b/library/i18n/locales/fr/studio.json index c5fdf6e42..966dc9347 100644 --- a/library/i18n/locales/fr/studio.json +++ b/library/i18n/locales/fr/studio.json @@ -21,6 +21,7 @@ "cap3": "Multivue", "cap2": "Préréglage", "cap1": "Simple", + "stageExperimental": "Expérimental", "toggleList": "Afficher ou masquer la liste", "openViews": "Ouvrir la liste des vues", "expandList": "Déplier la liste", diff --git a/library/i18n/locales/hi/studio.json b/library/i18n/locales/hi/studio.json index 5fedb2291..1c4bbb837 100644 --- a/library/i18n/locales/hi/studio.json +++ b/library/i18n/locales/hi/studio.json @@ -21,6 +21,7 @@ "cap3": "बहु-दृश्य", "cap2": "प्रीसेट", "cap1": "एकल", + "stageExperimental": "प्रायोगिक", "toggleList": "सूची दिखाएँ या छिपाएँ", "openViews": "दृश्यों की सूची खोलें", "expandList": "सूची फैलाएँ", diff --git a/library/i18n/locales/it/studio.json b/library/i18n/locales/it/studio.json index af44ef3b1..229779033 100644 --- a/library/i18n/locales/it/studio.json +++ b/library/i18n/locales/it/studio.json @@ -21,6 +21,7 @@ "cap3": "Multivista", "cap2": "Preimpostato", "cap1": "Singolo", + "stageExperimental": "Sperimentale", "toggleList": "Mostra o nascondi l’elenco", "openViews": "Apri l’elenco delle viste", "expandList": "Espandi l’elenco", diff --git a/library/i18n/locales/pt/studio.json b/library/i18n/locales/pt/studio.json index b8118eceb..e41b6650e 100644 --- a/library/i18n/locales/pt/studio.json +++ b/library/i18n/locales/pt/studio.json @@ -21,6 +21,7 @@ "cap3": "Multivisualização", "cap2": "Predefinição", "cap1": "Único", + "stageExperimental": "Experimental", "toggleList": "Mostrar ou ocultar a lista", "openViews": "Abrir a lista de visualizações", "expandList": "Expandir a lista", diff --git a/library/i18n/locales/ro/studio.json b/library/i18n/locales/ro/studio.json index 91f83c5cb..c7c0d7beb 100644 --- a/library/i18n/locales/ro/studio.json +++ b/library/i18n/locales/ro/studio.json @@ -21,6 +21,7 @@ "cap3": "Vizualizări multiple", "cap2": "Presetat", "cap1": "Simplu", + "stageExperimental": "Experimental", "toggleList": "Afișează sau ascunde lista", "openViews": "Deschide lista de vizualizări", "expandList": "Extinde lista", diff --git a/library/i18n/locales/ru/studio.json b/library/i18n/locales/ru/studio.json index e1e4508a0..264b84d72 100644 --- a/library/i18n/locales/ru/studio.json +++ b/library/i18n/locales/ru/studio.json @@ -21,6 +21,7 @@ "cap3": "Несколько видов", "cap2": "Предустановка", "cap1": "Одиночный", + "stageExperimental": "Экспериментальные", "toggleList": "Показать или скрыть список", "openViews": "Открыть список представлений", "expandList": "Развернуть список", diff --git a/library/i18n/locales/zh/studio.json b/library/i18n/locales/zh/studio.json index f36efb8ce..cec7b3f99 100644 --- a/library/i18n/locales/zh/studio.json +++ b/library/i18n/locales/zh/studio.json @@ -21,6 +21,7 @@ "cap3": "多视图", "cap2": "预设", "cap1": "单一", + "stageExperimental": "实验性", "toggleList": "显示或隐藏列表", "openViews": "打开视图列表", "expandList": "展开列表", diff --git a/plugin-designs/cities-internet/connectivity-map.meta.json b/plugin-designs/cities-internet/connectivity-map.meta.json index 4c4bab939..f1997e1a1 100644 --- a/plugin-designs/cities-internet/connectivity-map.meta.json +++ b/plugin-designs/cities-internet/connectivity-map.meta.json @@ -1,4 +1,5 @@ { + "stage": "production", "type": "single", "label": "Cities with Internet — connectivity map", "config": { diff --git a/plugin-designs/dashboard-plus-grid/dashboard-plus-grid.meta.json b/plugin-designs/dashboard-plus-grid/dashboard-plus-grid.meta.json index dd8b375ae..be6aa40d1 100644 --- a/plugin-designs/dashboard-plus-grid/dashboard-plus-grid.meta.json +++ b/plugin-designs/dashboard-plus-grid/dashboard-plus-grid.meta.json @@ -1,4 +1,5 @@ { + "stage": "production", "_comment": "Capability-4 composite that BAKES the research-dashboard + people-groups-grid into ONE self-contained element (both components imported directly + compiled into a single bundle, NO runtime bundle injection — unlike stack-demo which composes via src). Rendered stacked via the shared StackLayout so you scroll through both. This is the 'two elements baked into one' element the Driver asked for.", "type": "nested-elements-mfe", "composes": ["research-dashboard", "people-groups-grid"], diff --git a/plugin-designs/doxa-maps-spa/doxa-maps-spa.vue b/plugin-designs/doxa-maps-spa/doxa-maps-spa.vue index 3bccdb640..1341bb7d4 100644 --- a/plugin-designs/doxa-maps-spa/doxa-maps-spa.vue +++ b/plugin-designs/doxa-maps-spa/doxa-maps-spa.vue @@ -591,7 +591,7 @@ function experimentalTabsFromManifest(manifest) { const out = [] for (const b of bundles) { const id = b && b.bundle - if (!id || placed.has(id) || NOT_EXPERIMENTAL.has(id)) continue + if (!id || placed.has(id) || NOT_EXPERIMENTAL.has(id) || b.stage === 'production') continue // stage: the bundle's .meta.json, same field the Plugin Studio reads // Pick the bundle's entry profile: prefer a real MAP profile, else the profile named like the // bundle, else simply the first one. A bundle contributes exactly ONE rail row (see dedupeRows), // so this chooses which of its profiles that row opens on. diff --git a/plugin-designs/doxa-simple-map/doxa-simple-map.meta.json b/plugin-designs/doxa-simple-map/doxa-simple-map.meta.json new file mode 100644 index 000000000..6d3ae69b7 --- /dev/null +++ b/plugin-designs/doxa-simple-map/doxa-simple-map.meta.json @@ -0,0 +1,4 @@ +{ + "stage": "production", + "_comment": "Prayer Progress is a production map: the Plugin Studio and the gallery list it outside Experimental (stage is read into manifest.json by generate-tree)." +} diff --git a/plugin-designs/gospel-resources/gospel-dashboard.meta.json b/plugin-designs/gospel-resources/gospel-dashboard.meta.json index 015c63227..4343a3c4f 100644 --- a/plugin-designs/gospel-resources/gospel-dashboard.meta.json +++ b/plugin-designs/gospel-resources/gospel-dashboard.meta.json @@ -1,4 +1,5 @@ { + "stage": "production", "_comment": "Capability-4 composite that BAKES this bundle's own map and tables into ONE element, stacked to scroll. Both children are imported directly and compiled into the single gospel-resources bundle — no runtime injection, no second script, and no cross-bundle version skew: the tables you scroll to are built from the same pin index the map above them draws. This is the 'gospel resources dashboard' the Driver asked for.", "type": "nested-elements-mfe", "composes": ["resources-map", "data-tables"], diff --git a/plugin-designs/my-upg-100-list/upg-100-mfe.meta.json b/plugin-designs/my-upg-100-list/upg-100-mfe.meta.json index dac849d36..fba2b8bf7 100644 --- a/plugin-designs/my-upg-100-list/upg-100-mfe.meta.json +++ b/plugin-designs/my-upg-100-list/upg-100-mfe.meta.json @@ -1,4 +1,5 @@ { + "stage": "production", "type": "nested-elements-mfe", "label": "UPG-100 — Map + Prayer Lists", "composes": ["my-upg-100-list/upg-100-list", "my-upg-100-list/poster-wall"], diff --git a/plugin-designs/plugin-studio/plugin-studio.vue b/plugin-designs/plugin-studio/plugin-studio.vue index e86531ec9..28d8e0cec 100644 --- a/plugin-designs/plugin-studio/plugin-studio.vue +++ b/plugin-designs/plugin-studio/plugin-studio.vue @@ -386,7 +386,10 @@ const HEAVY_ELEMENTS = new Set([ 'my-upg-100-list-map', 'doxa-map', 'doxa-maps-spa', // composites that NEST a map, so they own a context too (measured, see above) 'dashboard-plus-grid', 'research-dashboard-map', 'demo-map', 'all-people-groups', - 'country-posters-map', 'key-cities-map', 'research-map-plus-grid' + 'country-posters-map', 'key-cities-map', 'research-map-plus-grid', + // gospel-internet-board owns up to TWO contexts (its map and the language-tree whiteboard; go-int). + // A profile can also declare "heavy": true in its .meta.json, which the manifest carries (below). + 'gospel-internet-board' ]) // SUPERSEDED BUNDLES — built, still embeddable, but NOT shown in the studio. big-picture is a dark @@ -574,9 +577,17 @@ export default defineComponent({ const treeOf = new Map(this.compositionTrees.map(t => [t.nodes[0].selects, t])) const byBundle = new Map() for (const r of this.rows) { if (!byBundle.has(r.bundle)) byBundle.set(r.bundle, []); byBundle.get(r.bundle).push(r) } - const bundles = [...byBundle.entries()].map(([bundle, rows]) => ({ bundle, rows, cap: Math.max(...rows.map(r => r.cap)) })) - return [5, 4, 3, 2, 1].map(cap => { - const group = bundles.filter(b => b.cap === cap).sort((x, y) => x.bundle.localeCompare(y.bundle)) + // ★ PRODUCTION vs EXPERIMENTAL (the Driver, 2026-09-27): a public plugin that does not yet work as a + // production map is listed under Experimental until it does. The stage is the bundle's own + // .meta.json "stage", carried by the manifest, so this file keeps no list and the gallery reads the + // same field. Production bundles keep their capability sections; every experimental bundle goes in + // one Experimental section at the end, highest capability first. + const bundles = [...byBundle.entries()].map(([bundle, rows]) => ({ bundle, rows, cap: Math.max(...rows.map(r => r.cap)), stage: rows[0].stage })) + const experimental = bundles.filter(b => b.stage !== 'production').sort((x, y) => y.cap - x.cap || x.bundle.localeCompare(y.bundle)) + const production = bundles.filter(b => b.stage === 'production') + const groups = [5, 4, 3, 2, 1].map(cap => ({ cap, group: production.filter(b => b.cap === cap).sort((x, y) => x.bundle.localeCompare(y.bundle)) })) + groups.push({ cap: 'experimental', group: experimental }) + return groups.map(({ cap, group }) => { if (!group.length) return null const nodes = [] const pushElement = (r, depth, parent) => { @@ -610,6 +621,7 @@ export default defineComponent({ }) for (const r of rows) pushElement(r, 1, fid) } + if (cap === 'experimental') return { id: 'stage-experimental', cap: 0, label: this.tr('stageExperimental'), title: this.tr('stageExperimental'), icon: 'layers', nodes } return { id: 'cap-' + cap, cap, label: this.tr('cap' + cap), title: this.tr('cap' + cap) + ' · ' + (MACHINE_TYPE[cap] || ''), icon: CAP_ICON[cap] || 'embed', nodes } }).filter(Boolean) }, @@ -935,10 +947,12 @@ export default defineComponent({ const cap = capFor(p.type || 'single') // A heavy row owns a live WebGL context, so it is torn down on deactivate rather than // hidden. Keyed by tag OR bundle so a bundle whose profiles differ still resolves. - const heavy = HEAVY_ELEMENTS.has(tag) || HEAVY_ELEMENTS.has(b.bundle) + const heavy = HEAVY_ELEMENTS.has(tag) || HEAVY_ELEMENTS.has(b.bundle) || p.heavy === true out.push({ key: b.bundle + '/' + p.profile, bundle: b.bundle, + // production | experimental, from the bundle's .meta.json via the manifest (never a list here) + stage: b.stage === 'production' ? 'production' : 'experimental', label: DISPLAY_NAME[b.bundle + '/' + p.profile] || p.profile, // The REAL profile id, handed to the element in profile-config. Never the label: a display // name would mount with profile "Research Gallery", which it does not have. diff --git a/plugin-designs/regions-dashboard/regions-mfe.meta.json b/plugin-designs/regions-dashboard/regions-mfe.meta.json index 7c0413465..a65327f07 100644 --- a/plugin-designs/regions-dashboard/regions-mfe.meta.json +++ b/plugin-designs/regions-dashboard/regions-mfe.meta.json @@ -1,4 +1,5 @@ { + "stage": "production", "type": "nested-elements-mfe", "label": "Regions — List + Dashboards", "composes": [ diff --git a/plugin-designs/research-map-plus-grid/research-map-plus-grid.meta.json b/plugin-designs/research-map-plus-grid/research-map-plus-grid.meta.json index 26f52799c..f9e2897c9 100644 --- a/plugin-designs/research-map-plus-grid/research-map-plus-grid.meta.json +++ b/plugin-designs/research-map-plus-grid/research-map-plus-grid.meta.json @@ -1,4 +1,5 @@ { + "stage": "production", "_comment": "Capability-4 composite: the monolithic doxa-research-map (cap-3) stacked above the people-groups-grid (cap-2), scrolled as one element. THE MOBILE SIBLING of dashboard-plus-grid — the Driver's 2026-09-22 ruling was two separate cap-4 plugins rather than one element that swaps its top half by aspect ratio ('that would be really complex'); the SPA picks which one to show. Composes by TAG + BUNDLE URL (the stack-demo element/src contract) rather than baking the children in, because importing the research map's .vue directly would bypass doxa-research-map/index.js and silently drop its bundle-private colour strategies, its profileModules registry, its Pinia/i18n pair and the RTL plugin. The grid's own search + filters are ON here (unlike the desktop sibling, where the dashboard header owns them).", "type": "nested-elements-mfe", "composes": ["doxa-research-map", "people-groups-grid"],