doxa-maps/library/components/regions/StatCard.vue
Doxa Maps de284359dd
Some checks are pending
Deploy to GitHub Pages / build (push) Waiting to run
Deploy to GitHub Pages / deploy (push) Blocked by required conditions
Update from source f4871401
2026-09-27 04:49:00 +00:00

63 lines
3.6 KiB
Vue

<!--
StatCard.vue — one figure with a caption, on the DX-life card surface.
Their RegionDetail.vue renders a row of these above the map: a big number and a small grey label,
centred on a small-padding card. That is the whole component, and it is the smallest genuinely
reusable piece of the regions dashboard.
★ THE GRID IS NOT IN HERE, AND THAT IS THE POINT. The plugin's `.rd-stats` container
(grid-template-columns: repeat(auto-fit, minmax(180px, 1fr))) stays with the CONSUMER. How many
stats there are and how they wrap is a decision belonging to the page, not to one stat; baking a
grid into a single card would make it impossible to place one on its own, or two side by side, or
in a column. So a consumer supplies its own container and puts as many StatCards in it as it has.
VALUES ARE PRE-FORMATTED BY THE CALLER. `value` is a string or number and is rendered verbatim,
because number formatting is locale-dependent and the caller already knows the locale -- the
regions plugin passes formatNumber(n) through its own i18n. This component installs no i18n and
therefore works on a host that has none.
CONTRACT: the containing shadow root must carry the DX-life token layer (DXLIFE_TOKENS in the
regions plugin) and, for the figure to render in the intended face, the Bebas Neue @font-face --
which is DOCUMENT-scoped and so must be registered on the host document, not in the shadow root.
See useDocumentFont.js. Without the face the declared fallbacks apply; nothing breaks.
-->
<script setup>
import { useShadowStyles } from '../../composables/useShadowStyles.js'
import { useCardSurface } from '../../composables/useCardSurface.js'
defineProps({
/** The figure. Already formatted for the locale by the caller, e.g. "43,045,275". */
value: { type: [String, Number], default: '' },
/** The caption under the figure. Already localized by the caller. */
label: { type: String, default: '' },
})
// .rd-card / --small, shared with GoalCard under one stable label so only one copy is injected.
useCardSurface()
useShadowStyles(
// ── .region-stat: RegionDetail.vue's scoped block ──
'.rd-stat{ text-align:center; }' +
// ★ THE data-len SHRINK IS GONE, AND ITS OWN COMMENT SHOWED WHY. Their .region-stat__value is
// font-family-heading + font-size-3xl + line-height:1 and nothing else -- no such attribute exists
// in their markup. Ours was a shrink-to-fit workaround whose stated premise was "our condensed
// fallback is wider" than Bebas. That is STALE: Bebas is vendored now (assets/fonts/bebas-neue,
// glyph oracle 279 = 279 SAME), so there is no wider fallback to compensate for.
// Its other claim -- "every value DX-life itself renders keeps their size" -- was FALSIFIED by
// measurement: rega found "43,045,275" (10 chars, so it hit the 'long' bucket) rendered at
// 38.15px against their 47.68px on the same-width card, while the other three stats matched
// exactly. A rule whose own invariant is violated in practice is not protecting anything.
// Deleted WITH its mechanism -- the binding and lenBucket() too, not just the rules.
'.rd-stat__value{ margin:0; font-size:var(--font-size-3xl); line-height:1;' +
' font-family:"Bebas Neue","Arial Narrow",BlinkMacSystemFont,"Segoe UI","Helvetica Neue",Arial,sans-serif;' +
' font-weight:400; }' +
'.rd-stat__label{ margin:var(--spacing-2xs) 0 0; font-size:var(--font-size-sm); color:var(--dx-muted); }'
, 'stat-card')
</script>
<template>
<div class="rd-card rd-card--small rd-stat">
<p class="rd-stat__value">{{ value }}</p>
<p class="rd-stat__label">{{ label }}</p>
</div>
</template>