[Mappe·Shared] Card primitive with mint accent variants (§7) #858

Open
opened 2026-06-16 10:53:47 +02:00 by marcel · 0 comments
Owner

Shared component · Story 4. Part of #853.

Context

The card pattern (rounded-sm border border-line bg-surface shadow-sm) is re-implemented inline across domains, and zero cards use the 3px mint top-border archival signature the system calls for. 73 inline call-sites exist — well past the rule-of-three threshold, so extracting a primitive is justified.

Scope

  • Create $lib/shared/primitives/Card.svelte. Props: accent: 'top' | 'left' | 'none' (default 'top'), padding: 'sm' | 'md' (default 'md', mapping to §7 padding: 20–24 px). Base bg-surface border border-line shadow-sm rounded-sm; top adds a 3px top border; left adds a 3px left border (inline/resume strips); none adds no accent.
  • Accent implementation — decided: drive the accent off var(--c-accent) only (i.e. border-top: 3px solid var(--c-accent)) — never a raw hex or inline style. This ensures the dark-mode token flip (navy-mint → turquoise) that the epic gates on works automatically. Raw inline borders break the dark-mode token flip and the epic's "zero raw color" gate.
  • Children rendering — decided: the component renders children via {@render children()} (Svelte 5 snippet slot). It never applies {@html} to passed content. All content flows through Svelte's default escaping. This is a hard rule per §2.5 of the constitution; document it in the component's JSDoc so downstream adopters in PII-bearing domains (person/document/transcription) inherit the safe default.
  • Accent is decorative only: the mint accent border must never be the sole carrier of status or meaning (WCAG 1.4.1 / §1 "never carries text/meaning"). Any status meaning must come from a StatusDot + label, not the border color.
  • Section-caption helper: text content displayed via the helper must be supplied by adopters as Paraglide i18n keys — the helper must not hard-code any string literals. Per epic #853: "every visible string is a Paraglide key (German first; en/es stubs)."
  • Provide the primitive + adopt on 1–2 surfaces here; broad per-domain adoption happens in the page issues (don't blanket-refactor every card in this issue).

Acceptance

  • AC-1: Three accent variants (top / left / none) render per §7; rounded-sm (2px) default radius; accent driven by var(--c-accent) token only (no raw hex/inline)
  • AC-2: Section-caption helper documented (Montserrat 12px/700 .12em UPPERCASE text-ink-3); any caption text is supplied by adopters as Paraglide i18n keys
  • AC-3: Adopted on ≥1 real page; visual diff passes at 320 / 768 / 1440 px in light AND dark (verify on the adoption surface and at least one other card-bearing page); padding variant does not force a fixed width on mobile (Critical reader path)
  • AC-4: Card.svelte.spec.ts (vitest-browser-svelte) written red-first before implementation; asserts all three accent variants, padding values, 2px radius, section-caption helper; invalid/unknown accent falls back to 'top'; dark-mode token correctness verified

Security note (implementer)

The primitive renders children via {@render children()} with Svelte's default escaping. {@html} must never appear in this component. Cards wrap user/import-derived content (names, transcription excerpts, story intros) — the safe-rendering contract must hold for all adopters.

Out of Scope

  • Blanket refactor of all 73 inline call-sites — broad adoption happens in the per-domain page issues
  • Backend, database, migration, env vars, CI gate changes
  • ADR entry — extracting a presentational primitive is reversible and uncontentious
  • E2E Playwright scenarios — covered by adopting page issues

Depends on: token close-out for --c-accent / --radius-sm (already present in layout.css — unblocked). Refs: DESIGN_RULES §1/§2/§7, _AUTHORING_KIT.md §4.

**Shared component · Story 4.** Part of #853. ## Context The card pattern (`rounded-sm border border-line bg-surface shadow-sm`) is re-implemented inline across domains, and **zero cards** use the 3px mint top-border archival signature the system calls for. 73 inline call-sites exist — well past the rule-of-three threshold, so extracting a primitive is justified. ## Scope - Create `$lib/shared/primitives/Card.svelte`. Props: `accent: 'top' | 'left' | 'none'` (default `'top'`), `padding: 'sm' | 'md'` (default `'md'`, mapping to §7 `padding: 20–24 px`). Base `bg-surface border border-line shadow-sm rounded-sm`; `top` adds a 3px top border; `left` adds a 3px left border (inline/resume strips); `none` adds no accent. - **Accent implementation — decided:** drive the accent off `var(--c-accent)` only (i.e. `border-top: 3px solid var(--c-accent)`) — never a raw hex or inline style. This ensures the dark-mode token flip (navy-mint → turquoise) that the epic gates on works automatically. Raw inline borders break the dark-mode token flip and the epic's "zero raw color" gate. - **Children rendering — decided:** the component renders children via `{@render children()}` (Svelte 5 snippet slot). It never applies `{@html}` to passed content. All content flows through Svelte's default escaping. This is a hard rule per §2.5 of the constitution; document it in the component's JSDoc so downstream adopters in PII-bearing domains (person/document/transcription) inherit the safe default. - **Accent is decorative only:** the mint accent border must never be the sole carrier of status or meaning (WCAG 1.4.1 / §1 "never carries text/meaning"). Any status meaning must come from a `StatusDot` + label, not the border color. - **Section-caption helper:** text content displayed via the helper must be supplied by adopters as Paraglide i18n keys — the helper must not hard-code any string literals. Per epic #853: "every visible string is a Paraglide key (German first; en/es stubs)." - Provide the primitive + adopt on 1–2 surfaces here; broad per-domain adoption happens in the page issues (don't blanket-refactor every card in this issue). ## Acceptance - [ ] AC-1: Three accent variants (`top` / `left` / `none`) render per §7; `rounded-sm` (2px) default radius; accent driven by `var(--c-accent)` token only (no raw hex/inline) - [ ] AC-2: Section-caption helper documented (Montserrat 12px/700 `.12em` UPPERCASE `text-ink-3`); any caption text is supplied by adopters as Paraglide i18n keys - [ ] AC-3: Adopted on ≥1 real page; visual diff passes at 320 / 768 / 1440 px in light AND dark (verify on the adoption surface and at least one other card-bearing page); `padding` variant does not force a fixed width on mobile (Critical reader path) - [ ] AC-4: `Card.svelte.spec.ts` (vitest-browser-svelte) written red-first before implementation; asserts all three accent variants, `padding` values, 2px radius, section-caption helper; invalid/unknown `accent` falls back to `'top'`; dark-mode token correctness verified ## Security note (implementer) The primitive renders children via `{@render children()}` with Svelte's default escaping. `{@html}` must never appear in this component. Cards wrap user/import-derived content (names, transcription excerpts, story intros) — the safe-rendering contract must hold for all adopters. ## Out of Scope - Blanket refactor of all 73 inline call-sites — broad adoption happens in the per-domain page issues - Backend, database, migration, env vars, CI gate changes - ADR entry — extracting a presentational primitive is reversible and uncontentious - E2E Playwright scenarios — covered by adopting page issues **Depends on:** token close-out for `--c-accent` / `--radius-sm` (already present in `layout.css` — unblocked). **Refs:** `DESIGN_RULES §1/§2/§7`, `_AUTHORING_KIT.md §4`.
marcel added this to the Mappe Visual Redesign milestone 2026-06-16 10:53:47 +02:00
marcel added the P2-mediumfeatureredesign-mappeui labels 2026-06-16 11:06:20 +02:00
Sign in to join this conversation.
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: marcel/familienarchiv#858