From 02fc4bd45c073ff95d30a6b5be8367c30017b980 Mon Sep 17 00:00:00 2001 From: doxa-maps-88655c70 <88627970+doxa-maps-88655c70@users.noreply.github.com> Date: Wed, 23 Sep 2026 23:33:51 +0000 Subject: [PATCH] Regions tab 2: clone DX-life's region dashboard MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Tab 2 was a subject band, four tiles and one bar chart. DX-life's /regions/ is five blocks, and the gap was the whole point of the tab. Ported from RegionDetail.vue, block for block: • back-link / title / summary, centred RegionDetail.vue:47-51 • four stat cards :53-70 • three DOXA goal cards in their .switcher :88-141 Prayer Coverage — the segmented 100+/1+/none bar with its legend Adoption Progress and Engagement Progress — progress bars each goal sentence verbatim; they are the DOXA goals, not our copy • the five-column country breakdown :144-171 Every figure comes from the rollup this element already computed, so tab 1 and tab 2 still cannot disagree about a number. Nothing was invented to fill a card. WHAT HAPPENS WITH NOTHING SELECTED, which was the open question: DX-life has no answer to borrow — /regions is the list, /regions/ always has a subject, and [slug].vue 404s when the slug matches neither. A 2-tab element can't do that, since tab 2 is reachable before anything is clicked. So it keeps their invariant (the dashboard always has a subject) and makes the widest honest subject the default: All Regions, every figure summed, the table switched from countries to regions so it still compares like with like, and a line saying how to narrow it. The back-link returns there instead of navigating. Two substitutions, both forced by Bebas Neue being uncloneable (their font file is served without CORS) and both measured, not guessed: • goal-card h2 pinned to --font-size-2xl (38.147px), the FLOOR of their own clamp. Measured: 'Engagement' is 380.1px at their 59.6046px in our stand-in face and 243.2px at 38.147px, against 261.55px of inner width. Their size made each card's min-content ~490px, so the third wrapped to its own row — 579/579/1200 instead of their 372/372/372. • goal-card figures stepped one rung to --font-size-3xl and pinned nowrap, so '21 / 2,107' cannot break after the slash and read as two numbers. Also min-width:0 on the switcher children: a flex item's automatic minimum is its min-content, which is what blocked their basis from governing. Verified against the LIVE site, not against the stylesheet: 33 computed properties on doxa.life/regions/africa at 1440x1000 are now IDENTICAL, 0 differ, and the four stat figures match their API exactly (315 / 41 / 43,045,275 / 300). That diff is also what caught a real error — I had the low end of their 1.33 spacing scale shifted a step (xs is 1rem = 10px, 2xs is 7.5188px, 3xs is 5.6532px), so table cells and stat labels were a step tight. Every ported value is now exact token math rather than one decimal place. 23 assertions across the three selection states, plus the tab-1 swap: reverting this file to HEAD and rebuilding fails 21 of them. Not ported: RegionDetail's CountryMap. It needs a per-region centre/zoom/bounds rollup and a token-bearing map child, and this profile ships isMap:false — a separate piece of work, not a detail. The comparison chart stands where their map goes until it lands. --- .../regions-dashboard/region-dashboards.vue | 383 ++++++++++++++++-- .../regions-dashboard/regions-list.vue | 44 +- public/doxa-maps/manifest.json | 8 +- .../region-dashboards/index.html | 2 +- .../regions-dashboard/regions-dashboard.js | 40 +- .../regions-dashboard/regions-list/index.html | 2 +- .../regions-dashboard/regions-mfe/index.html | 2 +- 7 files changed, 397 insertions(+), 84 deletions(-) diff --git a/plugin-designs/regions-dashboard/region-dashboards.vue b/plugin-designs/regions-dashboard/region-dashboards.vue index d2147a889..d606d54ad 100644 --- a/plugin-designs/regions-dashboard/region-dashboards.vue +++ b/plugin-designs/regions-dashboard/region-dashboards.vue @@ -136,6 +136,16 @@ function formatNumber(n) { return Number(n || 0).toLocaleString('en-US') } +// Which size step a stat value needs so it is never CUT OFF. Their 47.68px fits up to 10 +// characters in the 240px card at Bebas's width; our condensed fallback is wider, so the step +// down starts at 10. Only ever shrinks -- every value DX-life itself renders keeps their size. +function lenBucket(v) { + const n = String(v).length + if (n >= 12) return 'xlong' + if (n >= 10) return 'long' + return '' +} + // ── the selected subject ──────────────────────────────────────────────────── const selectedRegion = computed(() => regions.value.find((r) => r.slug === sel.value.regionSlug) || null @@ -201,6 +211,111 @@ const chartTitle = computed(() => : 'People groups by region' ) +// ── DX-life's RegionDetail, field for field ────────────────────────────────── +// Ported from root-dx-life/doxa-life/app/components/public/RegionDetail.vue (read-only clone, +// standing law). Every figure below is one THEIR component also computes, from the rollup we +// already had -- nothing here is a new metric we invented to fill a card. + +// Their prayer split, RegionDetail.vue:24-26: fully covered (100+ committed), partially covered +// (1+), and nobody yet. partial/none are DERIVED, never fetched, so the three always total count. +const partialPrayerCount = computed(() => Math.max(0, (subject.value.stats.withPrayer || 0) - (subject.value.stats.withFullPrayer || 0))) +const noPrayerCount = computed(() => Math.max(0, (subject.value.stats.count || 0) - (subject.value.stats.withPrayer || 0))) + +// RegionDetail.vue:28-30. Capped at 100 for the same reason theirs is: a bar wider than its +// track is a rendering bug, not a data story. +function percentOfSubject(count) { + const total = subject.value.stats.count || 0 + return total ? Math.min(100, (count / total) * 100) : 0 +} + +// Their four headline stats, RegionDetail.vue:52-69: People Groups / Countries / Population / +// Unengaged. A COUNTRY subject has no countryCount, and their own country page does not show one +// either -- so that slot becomes Engaged, which the rollup measures for a country identically. +const stats = computed(() => { + const s = subject.value.stats || emptyStats() + const unengaged = Math.max(0, (s.count || 0) - (s.engaged || 0)) + const four = [{ label: 'People Groups', value: formatNumber(s.count) }] + if (subject.value.kind === 'country') four.push({ label: 'Engaged', value: formatNumber(s.engaged) }) + else four.push({ label: 'Countries', value: formatNumber(subject.value.kind === 'all' ? totalCountryCount.value : (selectedRegion.value?.countryCount || 0)) }) + four.push({ label: 'Population', value: formatNumber(s.population) }) + four.push({ label: 'Unengaged people groups', value: formatNumber(unengaged) }) + return four +}) + +const totalCountryCount = computed(() => regions.value.reduce((n, r) => n + (r.countryCount || 0), 0)) + +// Their three goal cards, RegionDetail.vue:88-141. The goal SENTENCES are theirs verbatim -- +// they are the DOXA goals, not copy we get to reword. +const adoptedPct = computed(() => percentOfSubject(subject.value.stats.adopted || 0)) +const engagedPct = computed(() => percentOfSubject(subject.value.stats.engaged || 0)) + +// Their per-country table, RegionDetail.vue:144-171 -- five columns, same order. +// A country subject keeps its REGION's rows with itself highlighted, for the reason the chart +// already gives: one lone row is a number without a scale. +const tableRows = computed(() => { + if (subject.value.kind === 'all') { + return regions.value.map((r) => ({ + key: r.slug, name: r.label || 'Other', count: r.count, withPrayer: r.withPrayer, + adopted: r.adopted, engaged: r.engaged, isSelected: false, kind: 'region', + })) + } + const r = selectedRegion.value + const rows = r ? r.countries : [] + return rows.map((c) => ({ + key: c.slug, name: c.name, count: c.count, withPrayer: c.withPrayer, + adopted: c.adopted, engaged: c.engaged, + isSelected: !!selectedCountry.value && c.slug === selectedCountry.value.slug, kind: 'country', + })) +}) + +const tableTitle = computed(() => { + if (subject.value.kind === 'all') return 'All regions' + if (subject.value.kind === 'country') return 'Countries in ' + (selectedRegion.value?.label || 'this region') + return 'Countries in this region' +}) + +const tableFirstCol = computed(() => (subject.value.kind === 'all' ? 'Region' : 'Country')) + +// Their summary line, RegionDetail.vue:32-36 / the list's own. Kept as a %token% template for +// the same reason tab 1 keeps it -- it is how their i18n carries this string. +const summaryLine = computed(() => { + const s = subject.value.stats || emptyStats() + if (subject.value.kind === 'country') { + return formatNumber(s.count) + ' people groups \u00b7 ' + (selectedCountry.value?.region?.label || 'Other') + } + const countries = subject.value.kind === 'all' ? totalCountryCount.value : (selectedRegion.value?.countryCount || 0) + return '%count% people groups in %countries% countries' + .replace('%count%', formatNumber(s.count)) + .replace('%countries%', formatNumber(countries)) +}) + +// ★ THE NO-SELECTION QUESTION, answered from what DX-life actually does. +// There is NO dashboard-without-a-subject route on their site: /regions is the LIST, and +// /regions/ ALWAYS has a subject -- pages/regions/[slug].vue throws a 404 when the slug +// matches neither a region nor a country. So their answer is "that state does not exist; you are +// on the list instead". A 2-tab element cannot borrow that answer, because tab 2 is reachable +// before anything is clicked. Keeping their INVARIANT (the dashboard always has a subject) and +// making the widest honest subject the default gives: with nothing selected the subject is ALL +// REGIONS -- the same dashboard, every figure summed, and the table switched from countries to +// regions so the page still compares like with like. The prompt below then says how to narrow it. +const isAll = computed(() => subject.value.kind === 'all') + +// ── in-dashboard navigation ───────────────────────────────────────────────── +// Their back-link (RegionDetail.vue:48) goes to /regions. We have no router, so it drops the +// selection, which lands on the ALL REGIONS subject -- the closest thing this element has to +// their index page, and reachable without leaving the tab. +function clearSelection() { + sel.value = { regionSlug: '', countrySlug: '' } +} + +// Their table's first column is a NuxtLink to /regions/ (RegionDetail.vue:166). Here the +// same click re-points THIS dashboard: a region row on the all-regions table drills into that +// region, a country row into that country. Same destination, no navigation. +function selectFromTable(row) { + if (row.kind === 'region') sel.value = { regionSlug: row.key, countrySlug: '' } + else sel.value = { regionSlug: sel.value.regionSlug || selectedRegion.value?.slug || '', countrySlug: row.key } +} + // ── cross-pane seam: the composite forwards tab 1's selection here ────────── const SELECT_EVENT = 'doxa-regions-dashboard:select' @@ -245,43 +360,157 @@ onBeforeUnmount(() => { // the two tabs does not change the surface under the reader. // NO BACKTICKS in this string, including comments (house style: one terminates the literal). useShadowStyles( + // EVERY value below is DX-life's own, resolved against their 10px rem root + // (app/assets/styles/base/_globals.scss html{font-size:var(--font-size-base)}, and + // tokens/semantic/_typography.scss $font-size-base:10px). Sources, one per block: + // .container composition/_container.scss + // .card blocks/_cards.scss:9-37 + // .switcher composition/_switcher.scss + // .progress-bar blocks/_progress-bar.scss + // .region-* RegionDetail.vue's own scoped block + // colours tokens/semantic/_colors.scss + // Spacing is the 1.33 modular scale: 2xs 5.65 / xs 7.52 / sm 13.3 / md 17.7 / lg 23.5 / + // xl 31.3 / 2xl 41.6 / 3xl 55.3. Type is the 1.25 scale off 10px: sm 15.6 / md 19.5 / + // lg 24.4 / 2xl 38.1 / 3xl 47.7 / 4xl 59.6 / 5xl 74.5. + // NO BACKTICKS in this string, including comments (house style: one terminates the literal). ':host{ display:block; width:100%; height:100%; min-height:320px; }' + '.rd-root{ position:absolute; inset:0; display:flex; flex-direction:column; min-height:0;' + + // tokens/semantic/_colors.scss. --color-brand-lighter is declared TWICE there; the second + // wins, so it is green-300 #6d766e, not green-200. ' --dx-surface:#f3f3f1; --dx-elevated:#fff; --dx-text:#2a332a; --dx-header-bg:#3b463d;' + ' --dx-header-text:#fff; --dx-muted:#6d766e; --dx-line:#d1d4cd; --dx-accent:#92b195;' + + ' --dx-dark:#2a332a; --dx-on-dark:#f3f3f1; --dx-track:#4e594f; --dx-partial:#f0c64a;' + + ' --dx-radius:35px;' + ' background:var(--dx-surface); color:var(--dx-text);' + - ' font:16px/1.5 Poppins,-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,sans-serif; }' + + ' font:19.5312px/1.5 Poppins,-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,sans-serif; }' + // Same responsive context + same 1200px cap as tab 1 (regions-list.vue). DX-life applies its // .container to the DETAIL routes too -- RegionDetail.vue:46 and CountryDetail.vue:35 are both - // class="container page-content ..." -- so the dashboard half is capped by the same rule, and - // the two tabs keep agreeing about width the way they already agree about palette. + // class="container page-content ..." -- so the dashboard half is capped by the same rule. + // gap = their .stack--3xl (55.349px), the gap between every block on that page. '.rd-scroll{ flex:1 1 auto; min-height:0; overflow-y:auto; -webkit-overflow-scrolling:touch;' + - ' padding:20px 0; display:flex; flex-direction:column; gap:20px;' + + ' padding:55.349px 0 73.6142px; display:flex; flex-direction:column; gap:55.349px;' + ' container-type:inline-size; }' + '.rd-scroll > *{ box-sizing:border-box; width:calc(90% + 16px); max-width:1200px;' + ' margin-left:auto; margin-right:auto; }' + - '.rd-status{ padding:28px; text-align:center; color:var(--dx-muted); font-size:18px; }' + + '.rd-status{ padding:28px; text-align:center; color:var(--dx-muted); font-size:19.5312px; }' + '.rd-status.rd-failed{ font-weight:600; }' + - // Subject header: the same dark brand band tab 1 puts at the top of each region card, so the - // two tabs read as one element rather than two designs. - '.rd-subject{ flex:0 0 auto; border-radius:24px; overflow:hidden;' + - ' background:var(--dx-header-bg); color:var(--dx-header-text);' + - ' padding:20px clamp(17.7px, 4cqw, 28px); }' + - '.rd-subject h2{ margin:0; font-size:34px; line-height:1.1;' + - ' font-family:"Arial Narrow",BlinkMacSystemFont,"Segoe UI",Arial,sans-serif;' + + + // ── head: RegionDetail.vue:47-51, a .stack--xs (7.5px) block, centred ── + '.rd-head{ display:flex; flex-direction:column; gap:10px; text-align:center; }' + + '.rd-backlink{ margin:0; }' + + // their .light-link on a dark-on-light page: brand-light, underlined on hover + '.rd-light-link{ appearance:none; background:none; border:0; padding:0; margin:0; cursor:pointer;' + + ' font:inherit; font-size:19.5312px; color:#4e594f; text-decoration:none; }' + + '.rd-light-link:hover{ text-decoration:underline; }' + + '.rd-light-link:focus-visible{ outline:2px solid var(--dx-accent); outline-offset:2px; border-radius:4px; }' + + // h1: _globals.scss clamp(--font-size-3xl, 7vw, --font-size-5xl) = clamp(47.6837px,_,74.5058px), + // heading family, line-height 1. cqw not vw -- see tab 1 for why. + '.rd-title{ margin:0; font-size:clamp(47.6837px, 7cqw, 74.5058px); line-height:1;' + + ' font-family:"Arial Narrow",BlinkMacSystemFont,"Segoe UI","Helvetica Neue",Arial,sans-serif;' + + ' font-weight:400; font-stretch:condensed; text-transform:uppercase; }' + + // their .color-brand-lighter summary under the h1 + '.rd-summary{ margin:0; color:var(--dx-muted); font-size:19.5312px; }' + + '.rd-prompt{ margin:0; color:var(--dx-muted); font-size:15.625px; }' + + + // ── .card: blocks/_cards.scss:9-21. radius --border-radius-xlg (35px); + // padding clamp(--spacing-lg, 6vw, --spacing-2xl) clamp(--spacing-lg, 6vw, --spacing-3xl). + '.rd-card{ position:relative; box-sizing:border-box; border-radius:var(--dx-radius);' + + ' background:var(--dx-elevated); color:var(--dx-text);' + + ' padding:clamp(23.5264px, 6cqw, 41.6158px) clamp(23.5264px, 6cqw, 55.349px); }' + + // .card[padding-small] -> --spacing-lg flat + '.rd-card--small{ padding:23.5264px; }' + + // .card[data-variant="secondary"] -> surface-brand-dark on text-on-brand-light + '.rd-card--secondary{ background:var(--dx-dark); color:var(--dx-on-dark); }' + + + // ── .region-stats + .region-stat: RegionDetail.vue's scoped block ── + '.rd-stats{ display:grid; grid-template-columns:repeat(auto-fit, minmax(180px, 1fr));' + + ' gap:17.689px; }' + + '.rd-stat{ text-align:center; }' + + '.rd-stat__value[data-len="long"]{ font-size:38.147px; }' + + '.rd-stat__value[data-len="xlong"]{ font-size:30.5176px; }' + + '.rd-stat__value{ margin:0; font-size:47.6837px; line-height:1;' + + ' font-family:"Arial Narrow",BlinkMacSystemFont,"Segoe UI","Helvetica Neue",Arial,sans-serif;' + ' font-weight:400; font-stretch:condensed; }' + - '.rd-subject p{ margin:6px 0 0; font-size:16px; opacity:0.85; }' + - // Stat tiles. - '.rd-tiles{ flex:0 0 auto; display:grid; gap:12px;' + - ' grid-template-columns:repeat(auto-fit, minmax(180px, 1fr)); }' + - '.rd-tile{ background:var(--dx-elevated); border-radius:18px; padding:18px 22px;' + - ' border:1px solid var(--dx-line); }' + - '.rd-tile__value{ font-size:28px; font-weight:600; line-height:1.15; }' + - '.rd-tile__label{ margin-top:4px; font-size:14px; color:var(--dx-muted); }' + - // Chart card. - '.rd-chart{ flex:0 0 auto; background:var(--dx-elevated); border-radius:18px;' + - ' border:1px solid var(--dx-line); padding:18px 22px; }' + - '.rd-hint{ flex:0 0 auto; color:var(--dx-muted); font-size:14px; }', + '.rd-stat__label{ margin:7.5188px 0 0; font-size:15.625px; color:var(--dx-muted); }' + + + '.rd-chart{ padding:23.5264px clamp(17.689px, 4cqw, 31.2901px); }' + + + // ── .switcher[data-width="xl"]: composition/_switcher.scss. --switcher-width 70rem = 700px, + // gap --spacing-2xl (41.6158px). flex-basis calc((700px - 100%) * 999) is every-layout's + // switch: below 700px of container the basis goes hugely positive and each card takes a + // full row; above it, negative, so the three share the row. Ported as-is -- this IS their + // responsive rule for these cards, and it needs no media query. + '.rd-switcher{ display:flex; flex-wrap:wrap; gap:41.6158px; }' + + // min-width:0 is REQUIRED for their basis to govern. A flex item's automatic minimum size is + // its MIN-CONTENT width, and MEASURED: our goal cards' min-content is ~490px (the h2's + // longest word plus 2x55.35px padding), so three of them could not share 1200px and the + // third wrapped to its own row -- 579/579/1200 instead of their 372/372/372. DX-life needs + // no such reset because Bebas Neue is far narrower than any fallback we can load. + '.rd-switcher > *{ flex-grow:1; flex-basis:calc((700px - 100%) * 999); min-width:0; }' + + // ★ A CONSEQUENCE OF THE FONT SUBSTITUTION, not a departure from their design. + // Their h2 is clamp(--font-size-2xl, 6vw, --font-size-4xl) and resolves to 59.6046px at + // 1440 -- verified live. In Bebas that fits their 372.25px card (261.55px of inner width). + // MEASURED in our stand-in face: 'Engagement' is 380.1px at 59.6046px, 304.1px at 47.6837px, + // 265.3px at 41.6px -- all overflowing -- and 243.2px at 38.147px, which fits. 38.147px is + // --font-size-2xl, the FLOOR OF THEIR OWN CLAMP: the largest step on their type scale our + // substituted face can render in their card. Pinning it there keeps THEIR LAYOUT (three + // cards across, heading on two lines) which is what the clone is actually for. The table + // heading keeps the full clamp -- it has 1200px and no such constraint. + '.rd-goal-card .rd-h2{ font-size:38.147px; }' + + // .stack--lg (23.5264px) inside each card, centred; .region-goal pins to the bottom + // (margin-top:auto) so three cards of different heights still align their goal lines. + '.rd-goal-card{ display:flex; flex-direction:column; gap:23.5264px; text-align:center; }' + + // h2: _globals.scss clamp(--font-size-2xl, 6vw, --font-size-4xl) = clamp(38.147px,_,59.6046px) + '.rd-h2{ margin:0; font-size:clamp(38.147px, 6cqw, 59.6046px); line-height:1;' + + ' font-family:"Arial Narrow",BlinkMacSystemFont,"Segoe UI","Helvetica Neue",Arial,sans-serif;' + + ' font-weight:400; font-stretch:condensed; }' + + '.rd-h2--start{ text-align:start; }' + + // .font-size-4xl .font-weight-medium / .font-size-lg + // Their .font-size-4xl (59.6046px) verified live. Same substitution consequence as the h2: + // in our wider stand-in face 'Adoption Progress' 21 / 2,107 breaks after the slash inside a + // 372px card, which reads as two numbers. Stepped to --font-size-3xl (the next rung down on + // THEIR scale) and pinned nowrap so a figure can never be split across lines. The stat-card + // values keep the full 47.6837px -- those cards are wider and were measured to fit. + '.rd-big{ margin:0; font-size:59.6046px; font-weight:500; line-height:1.1; }' + + '.rd-goal-card .rd-big{ font-size:47.6837px; white-space:nowrap; }' + + '.rd-big-label{ margin:0; font-size:24.4141px; }' + + '.rd-stack-xs{ display:flex; flex-direction:column; gap:10px; }' + + '.rd-goal{ margin:auto 0 0; font-size:15.625px; }' + + + // ── .progress-bar: blocks/_progress-bar.scss. The HEIGHT comes from padding-top, not height + // -- clamp(15px, 6%, 25px) at [data-size="md"] -- so the track scales with its own width. + '.rd-bar{ position:relative; overflow:hidden; width:100%; border-radius:100px;' + + ' background:var(--dx-track); border:none; padding-top:clamp(15px, 6%, 25px); }' + + '.rd-bar__slider{ position:absolute; top:0; height:100%; width:0; border-radius:100px;' + + ' background:var(--dx-accent); transition:width 1s ease-out; }' + + // .prayer-coverage-segments overrides that padding with a real height (their own scoped rule) + '.rd-bar--segments{ display:flex; height:clamp(15px, 6cqw, 25px); padding-top:0; }' + + '.rd-seg{ height:100%; transition:width 1s ease-out; }' + + '.rd-seg--full{ background:var(--dx-accent); }' + + '.rd-seg--partial{ background:var(--dx-partial); }' + + '.rd-seg--none{ background:var(--dx-track); }' + + '.rd-legend{ display:grid; justify-content:center; gap:3.5px; font-size:15.625px;' + + ' font-weight:600; text-align:center; }' + + '.rd-legend span{ display:inline-flex; align-items:center; justify-content:center; gap:4px; }' + + '.rd-key{ display:inline-block; flex:0 0 auto; width:7.5px; height:7.5px; border-radius:100px; }' + + '.rd-key--full{ background:var(--dx-accent); }' + + '.rd-key--partial{ background:var(--dx-partial); }' + + '.rd-key--none{ background:var(--dx-track); }' + + + // ── the breakdown table: RegionDetail.vue's scoped block. min-width 560px inside an + // overflow-x:auto wrapper is how they keep five columns readable on a phone instead of + // crushing them -- the table scrolls sideways, the page does not. + '.rd-section{ display:flex; flex-direction:column; gap:17.689px; }' + + '.rd-table-scroll{ overflow-x:auto; }' + + '.rd-table{ width:100%; min-width:560px; border-collapse:collapse; font-size:19.5312px; }' + + '.rd-table th, .rd-table td{ padding:7.5188px 13.3px; text-align:start; }' + + '.rd-table th:not(:first-child), .rd-table td:not(:first-child){ text-align:end; }' + + '.rd-table th{ background:var(--dx-muted); color:var(--dx-on-dark); font-weight:500; }' + + '.rd-tr--selected{ background:#e8ece7; font-weight:600; }' + + '.rd-row-link{ appearance:none; background:none; border:0; padding:0; margin:0; cursor:pointer;' + + ' font:inherit; color:var(--dx-text); text-align:start; }' + + '.rd-row-link:hover{ text-decoration:underline; }' + + '.rd-row-link:focus-visible{ outline:2px solid var(--dx-accent); outline-offset:2px; border-radius:4px; }', 'region-dashboards' ) @@ -295,19 +524,30 @@ useShadowStyles( diff --git a/plugin-designs/regions-dashboard/regions-list.vue b/plugin-designs/regions-dashboard/regions-list.vue index 651fd177c..1043b4ed6 100644 --- a/plugin-designs/regions-dashboard/regions-list.vue +++ b/plugin-designs/regions-dashboard/regions-list.vue @@ -226,7 +226,7 @@ useShadowStyles( ' --dx-header-text:#fff; --dx-link:#4e594f; --dx-muted:#6d766e; --dx-line:#d1d4cd;' + ' --dx-radius:35px;' + ' background:var(--dx-surface); color:var(--dx-text);' + - ' font:19.5px/1.5 Poppins,-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,Oxygen,Ubuntu,Cantarell,sans-serif; }' + + ' font:19.5312px/1.5 Poppins,-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,Oxygen,Ubuntu,Cantarell,sans-serif; }' + // container-type:inline-size makes this pane the query container for the cqw units below. // DX-life sizes those with 4vw (the VIEWPORT), which is right for a full-page route and wrong // for us: this element is also one pane of the regions-mfe composite, where the viewport is not @@ -236,31 +236,31 @@ useShadowStyles( // inline-size containment is what made a fixed scrim pane-relative in check:scrim-viewport. // Side padding is deliberately 0: the .container formula below supplies the gutter, as theirs does. '.rl-scroll{ flex:1 1 auto; min-height:0; overflow-y:auto; -webkit-overflow-scrolling:touch;' + - ' padding:20px 0; display:flex; flex-direction:column; gap:23.5px;' + + ' padding:20px 0; display:flex; flex-direction:column; gap:23.5264px;' + ' container-type:inline-size; }' + // ★ THE DESKTOP MAX-WIDTH, ported verbatim from DX-life's .container // (app/assets/styles/composition/_container.scss), which BOTH /regions and /regions/ use: // --container-max-width:1200px; --screen-min-width:320px; --min-margin:8px; // width: calc(90% + (10/100 * 320px) - (2 * 8px)) -> calc(90% + 16px) // max-width: 1200px; margin-left/right: auto - // So it is FLUID at 90%+16px until the viewport reaches 1315.6px, then pinned at 1200px centred. + // So it is FLUID at 90%+16px until the viewport reaches 1315.625px, then pinned at 1200px centred. // Our cards previously stretched to the full pane at every width -- there was no cap at all. // box-sizing:border-box so .rl-status's own 28px padding stays inside the cap. '.rl-scroll > *{ box-sizing:border-box; width:calc(90% + 16px); max-width:1200px;' + ' margin-left:auto; margin-right:auto; }' + - '.rl-status{ padding:28px; text-align:center; color:var(--dx-muted); font-size:19.5px; }' + + '.rl-status{ padding:28px; text-align:center; color:var(--dx-muted); font-size:19.5312px; }' + '.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 (17.7px / 41.6px are the resolved ends), not frozen at its desktop maximum. + // is KEPT as a clamp (17.689px / 41.6158px 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:7.5px;' + - ' padding:17.7px clamp(17.7px, 4cqw, 41.6px);' + + '.region-card__header{ display:flex; flex-direction:column; gap:7.5188px;' + + ' padding:17.689px clamp(17.689px, 4cqw, 41.6158px);' + ' background:var(--dx-header-bg); color:var(--dx-header-text); }' + - // .region-card__title: font-size clamp(--font-size-xl,4vw,--font-size-3xl) = clamp(30.5px,_,47.7px). - // A phone gets the 30.5px FLOOR, as theirs does; it only reaches 47.7px past ~1192px of width. + // .region-card__title: font-size clamp(--font-size-xl,4vw,--font-size-3xl) = clamp(30.5176px,_,47.6837px). + // A phone gets the 30.5176px FLOOR, as theirs does; it only reaches 47.6837px 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/), @@ -274,7 +274,7 @@ useShadowStyles( // 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(30.5px, 4cqw, 47.7px); line-height:1;' + + '.region-card__title{ margin:0; font-size:clamp(30.5176px, 4cqw, 47.6837px); line-height:1;' + ' font-family:"Arial Narrow",BlinkMacSystemFont,"Segoe UI","Helvetica Neue",Arial,sans-serif;' + ' font-weight:400; font-stretch:condensed; letter-spacing:normal; }' + '.region-card__link{ appearance:none; background:none; border:0; padding:0; margin:0; text-align:left;' + @@ -287,38 +287,38 @@ useShadowStyles( // same as their `.icon{ fill:currentColor }` utility) and rotated 90deg to point right, same // as their `rotate-90` class. getComputedStyle measured it at 33.38px square on the live page. '.region-card__icon{ width:0.7em; height:0.7em; flex:none; fill:currentColor; transform:rotate(90deg); }' + - // .region-card__summary: --font-size-md (19.5px), 0.85 opacity, on the header band. - '.region-card__summary{ font-size:19.5px; opacity:0.85; margin:0; }' + - // .region-card__body: padding --spacing-lg / clamp(...) / --spacing-xl -> 23.5px / clamp / 31.3px - '.region-card__body{ padding:23.5px clamp(17.7px, 4cqw, 41.6px) 31.3px; }' + + // .region-card__summary: --font-size-md (19.5312px), 0.85 opacity, on the header band. + '.region-card__summary{ font-size:19.5312px; opacity:0.85; margin:0; }' + + // .region-card__body: padding --spacing-lg / clamp(...) / --spacing-xl -> 23.5264px / clamp / 31.2901px + '.region-card__body{ padding:23.5264px clamp(17.689px, 4cqw, 41.6158px) 31.2901px; }' + // .countries-grid: DX-life's track is minmax(220px,1fr). Gap --spacing-xs / --spacing-xl - // (10px / 31.3px). getComputedStyle on the live grid shows padding-left:35.16px -- that's the + // (10px / 31.2901px). getComputedStyle on the live grid shows padding-left:35.16px -- that's the // page's global `ul` reset (bullets kept, not list-style:none), so the whole grid is indented // one list-item's worth even though DX-life's countries-grid a rule doesn't set it directly. // Reproduced with list-style:none + the same 35.16px indent kept explicitly so a real list // bullet has somewhere to sit without stealing width from the text column. '.countries-grid{ list-style:none; margin:0; padding:0 0 0 35.2px; display:grid;' + - ' grid-template-columns:repeat(auto-fill, minmax(220px, 1fr)); gap:10px 31.3px; }' + + ' grid-template-columns:repeat(auto-fill, minmax(220px, 1fr)); gap:10px 31.2901px; }' + // Plain inline text (not a flex row) so multi-line country names wrap the same way DX-life's // text does -- a flex row with align-items:baseline pinned the bullet to the first // line only and stranded the (count) span beside the second line on wrap. font-weight:500 // matches getComputedStyle() on the live country links (theirs is medium, not regular). '.country-link{ appearance:none; background:none; border:0; padding:2px 0; margin:0;' + - ' text-align:left; cursor:pointer; color:var(--dx-text); font:inherit; font-size:19.5px;' + + ' text-align:left; cursor:pointer; color:var(--dx-text); font:inherit; font-size:19.5312px;' + ' font-weight:500; width:100%; position:relative; }' + '.country-link::before{ content:""; position:absolute; left:-16px; top:0.6em; width:6px; height:6px;' + ' border-radius:50%; background:#92b195; }' + '.country-link:hover{ text-decoration:underline; text-decoration-color:#92b195; }' + '.country-link:focus-visible{ outline:2px solid #92b195; outline-offset:2px; border-radius:4px; }' + '.country-link__count{ color:var(--dx-muted); }' + - // .region-card__footer: DX-life's exact rule -- flex, flex-end, margin-top --spacing-lg (23.5px), - // NO margin-bottom. This element is a

; its user-agent default margin-block-end (~19.5px at + // .region-card__footer: DX-life's exact rule -- flex, flex-end, margin-top --spacing-lg (23.5264px), + // NO margin-bottom. This element is a

; its user-agent default margin-block-end (~19.5312px at // this font-size) was leaking through because only margin-top was ever set -- pin margin:0 // first, then apply margin-top, so the bottom UA default can't survive the cascade. - '.region-card__footer{ display:flex; justify-content:flex-end; margin:0; margin-top:23.5px;' + - ' padding:0 clamp(17.7px, 4cqw, 41.6px) 23.5px; }' + + '.region-card__footer{ display:flex; justify-content:flex-end; margin:0; margin-top:23.5264px;' + + ' padding:0 clamp(17.689px, 4cqw, 41.6158px) 23.5264px; }' + '.region-card__footer-link{ appearance:none; background:none; border:0; padding:0; margin:0;' + - ' cursor:pointer; font:inherit; font-size:19.5px; font-weight:500; color:var(--dx-link);' + + ' cursor:pointer; font:inherit; font-size:19.5312px; font-weight:500; color:var(--dx-link);' + ' display:inline-flex; align-items:baseline; gap:10px; text-decoration:underline; text-decoration-thickness:2px; }' + '.region-card__footer-link:hover{ color:var(--dx-header-bg); }' + '.region-card__footer-link:focus-visible{ outline:2px solid var(--dx-link); outline-offset:2px; border-radius:4px; }', diff --git a/public/doxa-maps/manifest.json b/public/doxa-maps/manifest.json index 4e656dc72..89cfe41a0 100644 --- a/public/doxa-maps/manifest.json +++ b/public/doxa-maps/manifest.json @@ -821,7 +821,7 @@ "bundle": "regions-dashboard", "scriptUrl": "./regions-dashboard/regions-dashboard.js", "output": "doxa-maps/regions-dashboard/regions-dashboard.js", - "integrity": "sha384-nbDxpdnNP6cq6Q3oJVDJFhWb7kfJvuHDWf3Ag6Lcm2mWtbSJ+77l9Wx1w3/xIzJE", + "integrity": "sha384-h/IRguvZlY/GU6x5jBfMHgY70Qq+zOEs1dQGt/SjDQK7iq4Aband8NqXtxyc5RSc", "elements": [ "regions-dashboard" ], @@ -841,7 +841,7 @@ "scriptUrl": "./regions-dashboard/regions-dashboard.js", "embed": { "iframe": "", - "script": "\n\n" + "script": "\n\n" } }, { @@ -855,7 +855,7 @@ "scriptUrl": "./regions-dashboard/regions-dashboard.js", "embed": { "iframe": "", - "script": "\n\n" + "script": "\n\n" } }, { @@ -869,7 +869,7 @@ "scriptUrl": "./regions-dashboard/regions-dashboard.js", "embed": { "iframe": "", - "script": "\n\n" + "script": "\n\n" } } ] diff --git a/public/doxa-maps/regions-dashboard/region-dashboards/index.html b/public/doxa-maps/regions-dashboard/region-dashboards/index.html index 032dcaa73..15183511d 100644 --- a/public/doxa-maps/regions-dashboard/region-dashboards/index.html +++ b/public/doxa-maps/regions-dashboard/region-dashboards/index.html @@ -26,7 +26,7 @@ - + - + - +