156 lines
11 KiB
Vue
156 lines
11 KiB
Vue
<!--
|
|
GoalCard.vue — a headline figure with a goal line pinned to the bottom of the card.
|
|
|
|
The agent named these directly: "the banners are the cards that tell you the prayer progress".
|
|
Their RegionDetail.vue renders three of them in a row -- Prayer Coverage, Adoption Progress,
|
|
Engagement Progress -- identical in structure and different only in their numbers, their bar, and
|
|
one dark variant. That is one component with props, which is why it is here and not three copies.
|
|
|
|
★ NAMED GoalCard, NOT PrayerGoalCard, and the deviation from the name in the order is deliberate:
|
|
nothing in this component's props or CSS is about prayer. The shepherd's name described the first
|
|
instance; a shared component named after one caller mis-describes it for every later one. Flagged
|
|
in the handoff rather than decided silently.
|
|
|
|
★ THE MIDDLE IS A SLOT. What sits between the figure and the goal line differs per instance -- a
|
|
segmented bar with a legend in one, a plain bar in the other two -- so the card does not import a
|
|
bar at all. It knows about a title, a figure, and a goal; the caller supplies the rest. This is the
|
|
same rule RegionSectionCard follows for its poster wall: one caller's feature must not become every
|
|
caller's API.
|
|
|
|
★ margin-top:auto IS THE WHOLE POINT OF THE LAYOUT, not a detail. It is what makes three cards
|
|
holding different amounts of copy align their goal lines flush along the bottom edge. The full
|
|
measurement history is on the rules below; it survived a probe that measured the pair as "same".
|
|
|
|
TWO TEMPLATE DETAILS, documented here rather than in the template, because a comment between nodes
|
|
is a rendered node and this card's DOM is compared against the markup it replaced:
|
|
• the figure and its caption sit in an UNCLASSED <div>. That grouping is what makes the card's
|
|
stack margin fall between the groups rather than between a number and its own label.
|
|
• the space after </strong> in the goal line is INTERPOLATED, not written as template whitespace,
|
|
so it cannot depend on the compiler's whitespace:condense behaviour.
|
|
|
|
CONTRACT, two things the host root must provide, exactly as for RegionSectionCard:
|
|
• the DX-life token layer (--dx-radius, --dx-elevated, --dx-dark, --spacing-*, --font-size-*).
|
|
• for the Cyrillic heading switch, a `lang` attribute on an ancestor INSIDE THE SAME shadow root.
|
|
A [lang] selector cannot see the host document's attribute across a shadow boundary, so a
|
|
consumer that does not set lang on an ancestor gets the default face and nothing else changes.
|
|
No i18n is installed or required: every string arrives pre-localized, including the goal label.
|
|
-->
|
|
<script setup>
|
|
import { useShadowStyles } from '../../composables/useShadowStyles.js'
|
|
import { useCardSurface } from '../../composables/useCardSurface.js'
|
|
|
|
defineProps({
|
|
/** The card heading, already localized. */
|
|
title: { type: String, default: '' },
|
|
/** The headline figure, pre-formatted by the caller, e.g. "21 / 2,107". */
|
|
value: { type: [String, Number], default: '' },
|
|
/** The caption under the figure, already localized. */
|
|
valueLabel: { type: String, default: '' },
|
|
/** The goal sentence, already localized. Pinned to the bottom of the card. */
|
|
goal: { type: String, default: '' },
|
|
/** The bold lead-in to the goal sentence, e.g. "Goal". Omitted entirely when empty. */
|
|
goalLabel: { type: String, default: '' },
|
|
/** Follows goalLabel inside the <strong>. A prop because ':' is not universal punctuation. */
|
|
labelSeparator: { type: String, default: ':' },
|
|
/** 'secondary' is their dark-on-light variant, used by the first of the three cards. */
|
|
variant: { type: String, default: 'default' },
|
|
})
|
|
|
|
// .rd-card / --secondary, shared with StatCard under one stable label.
|
|
useCardSurface()
|
|
|
|
useShadowStyles(
|
|
// ★ THEIR .stack IS margin-top, AND THAT IS LOAD-BEARING — not a stylistic detail.
|
|
// composition/_stack.scss: `.stack > * + * { margin-top: var(--stack-spacing) }`, with
|
|
// `.stack--lg` setting that to --spacing-lg. RegionDetail.vue's scoped
|
|
// `.region-goal { margin-top: auto }` then OVERRIDES the same property on the last child,
|
|
// so the goal line is pushed to the bottom and the 23.526px stack gap above it is GONE.
|
|
// This was ported as `gap`, which margin cannot override: gap AND the auto both applied,
|
|
// so every goal card carried ~23.5px of space their cards do not have. The DRIVER saw it
|
|
// ('spcing is nto ths ame', naming these three cards). Ported as their mechanism.
|
|
'.rd-goal-card{ display:flex; flex-direction:column; text-align:center; }' +
|
|
'.rd-goal-card > * + *{ margin-top:var(--spacing-lg); }' +
|
|
|
|
// ── the heading. h2: _globals.scss clamp(--font-size-2xl, 6vw, --font-size-4xl).
|
|
// ★ THIS IS A SECOND COPY OF THE PLUGIN'S .rd-h2 RULE, under this card's own class name, and the
|
|
// duplication is deliberate rather than overlooked. Using .rd-h2 itself would put two identical
|
|
// selectors in one shadow root from two different files -- the shape that drifts silently the first
|
|
// time one is tuned. The plugin keeps .rd-h2 for the one heading it still owns (its table title).
|
|
// The real fix is a shared ported-globals module beside the token layer, which is a batch of its
|
|
// own; carded, not smuggled in here.
|
|
'.rd-goal-card__title{ margin:0; font-size:clamp(var(--font-size-2xl), 6cqw, var(--font-size-4xl)); line-height:1;' +
|
|
' font-family:"Bebas Neue","Arial Narrow",BlinkMacSystemFont,"Segoe UI","Helvetica Neue",Arial,sans-serif;' +
|
|
' font-weight:400; }' +
|
|
// ★ CYRILLIC HEADINGS SWITCH FACE, as theirs do. base/_globals.scss:35-44:
|
|
// h1,h2,h3 { [lang="ru-RU"] & { font-family: var(--font-family-cyrillic); letter-spacing: -0.02em } }
|
|
// and --font-family-cyrillic is Oswald. The switch is not decoration: BEBAS NEUE HAS NO CYRILLIC
|
|
// GLYPHS, so a Russian heading in it leaves the family entirely and lands on whatever condensed
|
|
// system face the browser picks.
|
|
// BOTH TAG FORMS, deliberately: their site tags the document ru-RU while our i18n locale is `ru`,
|
|
// and a host page embedding this element may set either. A selector that matched only one would
|
|
// work on their site and fail on ours, or the reverse, depending on who set the attribute.
|
|
'[lang="ru"] .rd-goal-card__title, [lang="ru-RU"] .rd-goal-card__title{' +
|
|
' font-family:"Oswald","Arial Narrow",BlinkMacSystemFont,"Segoe UI","Helvetica Neue",Arial,sans-serif;' +
|
|
' letter-spacing:-0.02em; }' +
|
|
|
|
// ── the figure. .font-size-4xl .font-weight-medium / .font-size-lg
|
|
// ★ NO line-height HERE. Theirs is `<p class="font-size-4xl font-weight-medium">` and the
|
|
// .font-size-4xl utility sets ONLY a font-size — so their number inherits body's 1.5. Ours forced
|
|
// 1.1, which was compensating for the base line-height we never ported (now on :host in
|
|
// dxlife-tokens.js). rega measured the cost: 24px of card height, their number ~12px lower and its
|
|
// label ~24px lower. Inheriting is the faithful port.
|
|
// ★ A CLAMP, LIKE THEIRS, AND IT RESOLVES THE NOWRAP WORRY RATHER THAN FIGHTING IT.
|
|
// utilities/_typography.scss gives .font-size-4xl
|
|
// font-size: clamp(var(--font-size-2xl), 6vw, var(--font-size-4xl))
|
|
// and ours was a flat var(--font-size-4xl). Identical at 1440 and above (both sit on the ceiling),
|
|
// but below that ours never shrank -- MEASURED against their resolved values:
|
|
// 390px viewport theirs 38.15 ours 59.60 ours 21.46 too large
|
|
// 768px viewport theirs 46.08 ours 59.60 ours 13.52 too large
|
|
// 1024px and up theirs 59.60 ours 59.60 identical
|
|
// I had reported this as unchangeable because a figure must never split across lines in a 372px
|
|
// card. That was backwards: the clamp makes the figure SMALLER exactly where the wrap was observed.
|
|
// In a 372px container 6cqw is 22.3px, below the 2xl floor, so it resolves to 38.15px against the
|
|
// 59.60px that was breaking. The clamp helps the very problem the flat value was chosen for, and
|
|
// the nowrap stays as a belt.
|
|
// cqw not vw, consistent with every other clamp in this port (the container-vs-viewport
|
|
// substitution the agent is still ruling on); at 768 that lands 42.4 against their 46.08.
|
|
'.rd-big{ margin:0; font-size:clamp(var(--font-size-2xl), 6cqw, var(--font-size-4xl));' +
|
|
' font-weight:500; }' +
|
|
'.rd-goal-card .rd-big{ white-space:nowrap; }' +
|
|
'.rd-big-label{ margin:0; font-size:var(--font-size-lg); }' +
|
|
|
|
// ── the goal line. .region-goal — margin-top:auto, spelled at higher specificity than the stack
|
|
// rule above so the override is unambiguous rather than depending on source order.
|
|
// ★ margin-top:auto, AND IT IS LOAD-BEARING. Theirs is RegionDetail.vue:24 `.region-goal {
|
|
// margin-top: auto }` inside `<div class="stack stack--lg | card | text-center">`, and
|
|
// composition/_stack.scss makes .stack a flex COLUMN with `> * + * { margin-top:
|
|
// var(--stack-spacing) }`. So the auto OVERRIDES that child margin and pushes the goal line to the
|
|
// BOTTOM of the card. Ours was margin:0, which left it directly under the text above it.
|
|
// The visible consequence, and why it matters for a clone: the three goal cards hold different
|
|
// amounts of copy, so theirs align their goal lines flush along the cards' bottom edge while ours
|
|
// floated at three different heights.
|
|
// FOUND BY READING THEIR MARKUP after rega's spacing probe measured this pair as "same" -- which
|
|
// it is, at 1440, because with no free space in the card the auto resolves to 0. An equal computed
|
|
// margin hid a real difference, so the probe was right and incomplete at the same time.
|
|
'.rd-goal{ margin:auto 0 0; font-size:var(--font-size-sm); }' +
|
|
'.rd-goal-card > .rd-goal{ margin-top:auto; }' +
|
|
// base/_globals.scss:200-202 — <strong> is --font-weight-medium (500) on their site, not the UA's
|
|
// bold. The plugin's own `.rd-root strong` STAYS where it is: UupgsList renders a <strong> in the
|
|
// same shadow root and depends on it, so this is a second rule for this card's own subtree, not a
|
|
// move. Checked by reading the whole root rather than this card's markup -- a file-scoped grep
|
|
// would have called the plugin's copy dead and deleted a rule another component needs.
|
|
'.rd-goal strong{ font-weight:500; }'
|
|
, 'goal-card')
|
|
</script>
|
|
|
|
<template>
|
|
<div class="rd-card rd-goal-card" :class="{ 'rd-card--secondary': variant === 'secondary' }">
|
|
<h2 class="rd-goal-card__title">{{ title }}</h2>
|
|
<div>
|
|
<p class="rd-big">{{ value }}</p>
|
|
<p class="rd-big-label">{{ valueLabel }}</p>
|
|
</div>
|
|
<slot />
|
|
<p class="rd-goal"><strong v-if="goalLabel">{{ goalLabel }}{{ labelSeparator }}</strong>{{ goalLabel ? ' ' : '' }}{{ goal }}</p>
|
|
</div>
|
|
</template>
|