From de24e633f99f3b1c8e46451ab25dddc91d764e52 Mon Sep 17 00:00:00 2001 From: Doxa Maps Date: Sun, 27 Sep 2026 02:38:53 +0000 Subject: [PATCH] The region card becomes a shared component the plugin consumes MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The Driver's ask, and his negotiating point with the DX-life author depends on it being true: the components cloned from their /regions page must be REUSABLE, PARAMETERIZED components in the shared library, not private to one plugin. He named the failure mode himself — someone opens the regions plugin folder and finds the components are not reusable after all. I checked before building, and for the UI he named he was right: only the DATA and LOGIC had been promoted. So library/components/regions/RegionSectionCard.vue takes title, summary, countries and footerLabel, emits what was clicked, and knows nothing about where regions come from. The plugin version fetched its own data and owned the layout, which is exactly what made it unusable anywhere else. THE DESIGN TRAVELS WITH IT. The card carries the region-card, countries-grid and country-link CSS lifted unchanged — including the RTL chevron rules, where a "forward" chevron must mirror in Arabic while a disclosure caret must not, and where [dir="rtl"] .region-card__icon at 0-2-0 would otherwise beat the 0-1-0 --open modifier. A consumer gets the look, not a lookalike. It needs dir on an ancestor for that mirroring, which is documented in the file as a requirement. ★ THE CARD OWNS THE DISCLOSURE ROW, after the first cut got it wrong. I put the "View country posters" toggle in the consumer's slot — and immediately had to copy the footer-link styling and the rotating caret back into the plugin, re-creating the duplication this extraction exists to remove. A disclosure inside a card is a card feature; only the revealed CONTENT is the consumer's, so PosterWall stays in the slot with its fourteen props and does not become every consumer's API. ★ THE CHEVRON IS INLINE, NOT A . The plugin declared the symbol once and every card referenced it. A shadow-root id is scoped to that root, so the same card mounted elsewhere would reference a symbol that does not exist — and an unresolved renders NOTHING, silently. The plugin's own comment warns about that class for external hrefs; a cross-root id fails the same way. check:regions-uses-library is registered and asserts BOTH halves: the plugin imports from @map/ and renders the component, AND no longer holds the markup or the CSS it delegated. A reusability claim only a human can audit is one that rots; this one fails the day someone re-inlines a copy. Two negative controls keep it honest — the plugin still uses its own .rl-root classes, and the other tab is asserted NOT to use the card yet, so the check describes what is extracted rather than what I hope for. Two of my own mechanical edits produced valid JS and broken content on the way here, both caught by checking rather than by the parser: a line-numbered splice that dropped PosterWall's closing /> (fixed by locating boundaries by CONTENT), and a line-filtered CSS deletion that left 9 orphaned declarations and braces at 12/21 (fixed by removing rules brace-aware, then re-validating the concatenated stylesheet rather than trusting check:sfc-parse, which only proves the JS parses). --- documentation/reference-library-index.md | 2 + .../components/regions/RegionSectionCard.vue | 275 ++++++++++++++++++ package.json | 3 +- .../regions-dashboard/regions-list.vue | 247 +--------------- .../scripts/check-regions-uses-library.mjs | 62 ++++ 5 files changed, 356 insertions(+), 233 deletions(-) create mode 100644 library/components/regions/RegionSectionCard.vue create mode 100644 plugin-designs/regions-dashboard/scripts/check-regions-uses-library.mjs diff --git a/documentation/reference-library-index.md b/documentation/reference-library-index.md index fdeb02027..4ac3f5ec5 100644 --- a/documentation/reference-library-index.md +++ b/documentation/reference-library-index.md @@ -84,6 +84,8 @@ and a change here can break other maps. `import … from '@map/components/poster/slots/TitleSlot.vue'` - **QaCorner** — QaCorner.vue — the embeddable QA corner widget (bottom-right of any map). `import … from '@map/components/qa/QaCorner.vue'` +- **RegionSectionCard** — RegionSectionCard.vue — one region as a card: a heading, a summary line, a grid of its countries, + `import … from '@map/components/regions/RegionSectionCard.vue'` - **FilterDropdown** — FilterDropdown.vue — SHARED (@map/components/search/FilterDropdown.vue). `import … from '@map/components/search/FilterDropdown.vue'` - **SearchBar** — SearchBar.vue — SHARED (@map/components/search/SearchBar.vue). diff --git a/library/components/regions/RegionSectionCard.vue b/library/components/regions/RegionSectionCard.vue new file mode 100644 index 000000000..66ca37dc4 --- /dev/null +++ b/library/components/regions/RegionSectionCard.vue @@ -0,0 +1,275 @@ + + + diff --git a/package.json b/package.json index 52a33b775..8b947bf65 100644 --- a/package.json +++ b/package.json @@ -195,6 +195,7 @@ "check:script-flags": "node internal/check-script-flags-reachable.mjs", "check:settled": "node internal/settled-tree-gate.mjs", "check:sfc-parse": "node internal/check-sfc-script-parses.mjs", + "check:regions-uses-library": "node plugin-designs/regions-dashboard/scripts/check-regions-uses-library.mjs", "check:religion-label-not-value": "node plugin-designs/regions-dashboard/scripts/check-religion-label-not-value.mjs", "check:share-policy": "node internal/check-share-policy.mjs", "check:shared-reached": "node internal/check-shared-component-reached-bundles.mjs", @@ -232,7 +233,7 @@ "dev": "bun x vite", "fix:sri": "node internal/fix-sri.mjs", "gallery": "node internal/generate-gallery.mjs", - "gates:post": "node internal/run-parallel.mjs --label=post -- 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:share-policy check:library-index check:csv-shape check:capability-citations check:catalog-identity check:no-orphan-gates check:quoted-scripts check:done-evidence check:script-flags check:backlog-status 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:backlog-refs 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: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", + "gates:post": "node internal/run-parallel.mjs --label=post -- 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:share-policy check:library-index check:csv-shape check:capability-citations check:catalog-identity check:no-orphan-gates check:quoted-scripts check:done-evidence check:script-flags check:backlog-status 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:backlog-refs 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: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:regions-uses-library", "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", "internet:adm1": "node plugin-designs/cities-internet/scripts/build-adm1-internet.mjs", "library-index": "node internal/generate-library-index.mjs", diff --git a/plugin-designs/regions-dashboard/regions-list.vue b/plugin-designs/regions-dashboard/regions-list.vue index 5af7fc8df..b73e2e6a0 100644 --- a/plugin-designs/regions-dashboard/regions-list.vue +++ b/plugin-designs/regions-dashboard/regions-list.vue @@ -65,6 +65,7 @@ import { DXLIFE_TOKENS } from './dxlife-tokens.js' // labour country-poster-wall.vue states in its own header ("this profile supplies only its // data and taxonomy"). import PosterWall from '@map/components/poster/PosterWall.vue' +import RegionSectionCard from '@map/components/regions/RegionSectionCard.vue' import posterData from '@map/data/country-posters.json' import { resolveAssetsBase } from '@map/utils/embedBase.js' import { onBeforeUnmount } from 'vue' @@ -426,74 +427,6 @@ useShadowStyles( '.rl-summary{ margin:0; color:var(--dx-muted); font-size:var(--font-size-md); }' + '.rl-status{ padding:var(--spacing-xl); text-align:center; color:var(--dx-muted); font-size:var(--font-size-md); }' + '.rl-status.rl-failed{ font-weight:600; }' + - // .region-card: DX-life's --border-radius-xlg (35px), overflow hidden, white body, no border -- - // the dark header band supplies its own edge, unlike our old grey-outlined card. - '.region-card{ flex:0 0 auto; border-radius:var(--dx-radius); overflow:hidden; background:var(--dx-elevated); }' + - // .region-card__header: padding = --spacing-md / clamp(--spacing-md,4vw,--spacing-2xl) -- the clamp - // is KEPT as a clamp (var(--spacing-md) / var(--spacing-2xl) are the resolved ends), not frozen at its desktop maximum. - // gap = --spacing-2xs (7.5px), on --color-surface-brand. - '.region-card__header{ display:flex; flex-direction:column; gap:var(--spacing-2xs);' + - ' padding:var(--spacing-md) clamp(var(--spacing-md), 4cqw, var(--spacing-2xl));' + - ' background:var(--dx-header-bg); color:var(--dx-header-text); }' + - // .region-card__title: font-size clamp(--font-size-xl,4vw,--font-size-3xl) = clamp(var(--font-size-xl),_,var(--font-size-3xl)). - // A phone gets the var(--font-size-xl) FLOOR, as theirs does; it only reaches var(--font-size-3xl) past ~1192px of width. - // DX-life's heading font (Bebas Neue -- a bold condensed display face), regular weight (the - // face itself carries the boldness), all-caps to match the source screenshot exactly. - // Bebas Neue itself is DX-life's own self-hosted font file (doxa.life/assets/fonts/BebasNeue/), - // not served with CORS -- cross-origin @font-face silently fails without it, so it cannot be - // loaded here without shipping a font asset this repo doesn't own. Approximated with the - // standard condensed-display-face fallback stack (bold weight + font-stretch:condensed + tight - // tracking), since Bebas Neue's glyphs read as heavy/compressed even at its nominal "400" weight. - // getComputedStyle() on the live card title reads weight 400 / line-height 1.0 (47.68px on a - // 47.69px font-size) / letter-spacing normal / text-transform none -- Bebas Neue's own glyphs - // are what read as bold+condensed+caps-like, DX-life applies NO transform on top of the font. - // Our fallback stack has no true Bebas-Neue substitute, so font-stretch:condensed stands in for - // the face's natural narrowness, but weight/leading/tracking/case now match the measured values - // exactly rather than compounding extra "boldness" the source doesn't have. - '.region-card__title{ margin:0; font-size:clamp(var(--font-size-xl), 4cqw, var(--font-size-3xl)); line-height:1;' + - ' font-family:"Bebas Neue","Arial Narrow",BlinkMacSystemFont,"Segoe UI","Helvetica Neue",Arial,sans-serif;' + - ' font-weight:400; letter-spacing:normal; }' + - // Their link is an , so it carries their `a` rule's - // font-weight: var(--font-weight-medium) = 500 (_globals.scss:132). Ours is a