v0.3 Phase 4g: four-tier semantic tokens + notice usage layer

Round-5 feedback flagged the muddy result of forcing one --color-caution
to be both bright yellow (signal) and accessible text (ochre). The
single-token-per-role model collapsed the visible identity. Caution
notices ended up reading as brown/khaki surfaces because color-mix(
in oklab, var(--color-caution) 8%, transparent) over cream produced
muddy results whatever the source hex.

New architecture: four-tier per-role tokens, hand-tuned -bg surfaces,
plus usage-specific notice tokens on top.

Tier model — each semantic role publishes four tokens:

  signal  Brand-true colour. Left bars, large icons, dots, focus rings.
          Looks like the role at a glance.
  ink     Accessible text variant. Used ONLY when text itself must
          carry the role colour. WCAG AA 4.5:1 on its surface.
  border  Middle value. Visible chip/notice outline; not text-safe.
  bg      Hand-tuned pale tint surface. NOT a color-mix derivation.

Example, light-mode caution:
  --color-caution-signal #FFCD00  (bright yellow)
  --color-caution-ink    #7A5D00  (ochre, for small text only)
  --color-caution-border #D9A900  (mid gold)
  --color-caution-bg     #FFF7D6  (clean pale yellow tint)

--color-{role} retained as a backward-compat alias pointing at -signal.
v0.2 callers keep working; new code reaches for the specific tier.

Usage layer — components subscribe to usage-specific tokens, not
role-tier tokens directly:

  --notice-{role}-bar     left bar, signal-tier
  --notice-{role}-icon    signal in dark, ink in light, signal in HC
  --notice-{role}-border  outer border colour
  --notice-{role}-bg      hand-tuned pale surface

LumotiaNotice reads these via Tailwind arbitrary-value classes
(bg-[var(--notice-info-bg)] etc). v0.2 fallback values defined at
:root (outside quietware blocks) using color-mix from v0.2 --color-
{role} tokens so the Notice keeps rendering sensibly when quietware
is off.

HC contract — every -bg token forces to transparent. HC notices
render text + icon + 2-px border + left bar, no body tint. Brand
atmosphere steps fully aside.

Per Jake's round-5 spec: yellow stays yellow as the visible signal,
ochre only appears as small-text ink, and brown/khaki never
represents the caution role's primary visual identity again. Same
discipline applies to red/blue/green — each role has a clean
hand-tuned -bg pale tint per theme.

Verified — visible result.

  Light caution notice: bright #FFCD00 bar + signal icon + ink-darkened
  small icon + pale #FFF7D6 surface + #D9A900 border. Clean,
  semantically clear, no mud.

  Dark caution notice: bright #FFCD00 bar + signal icon + dark warm
  #29210A surface + bright yellow border. Reads as yellow at a glance.

  HC modes: no body tint, strong bordered notices, semantic colours
  preserved as primary identifier alongside icon + label.

  npm run check: 0 errors, 0 warnings across 5707 files.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-05-15 14:27:46 +01:00
parent 2fcb5aff78
commit 2f11f493c5
3 changed files with 319 additions and 111 deletions

View File

@@ -49,33 +49,43 @@
classes = "",
}: Props = $props();
// v0.3 Phase 4g — notice usage tokens. Each tone reads four CSS
// variables published by v0.3-quietware-tokens.css:
// --notice-{tone}-bar left bar (signal-tier)
// --notice-{tone}-icon icon colour (signal in dark, ink in light)
// --notice-{tone}-border outer border colour
// --notice-{tone}-bg hand-tuned pale tint surface
//
// v0.2 fallback values are defined at :root in the same tokens file
// via color-mix from the v0.2 --color-{role} tokens, so the Notice
// renders sensibly with or without quietware active.
const palette: Record<Tone, { bar: string; bg: string; border: string; iconColor: string; icon: typeof Info }> = {
info: {
bar: "border-l-info",
bg: "bg-info/8",
border: "border-info/40",
iconColor: "text-info",
bar: "border-l-[var(--notice-info-bar)]",
bg: "bg-[var(--notice-info-bg)]",
border: "border-[var(--notice-info-border)]",
iconColor: "text-[var(--notice-info-icon)]",
icon: Info,
},
caution: {
bar: "border-l-caution",
bg: "bg-caution/8",
border: "border-caution/40",
iconColor: "text-caution",
bar: "border-l-[var(--notice-caution-bar)]",
bg: "bg-[var(--notice-caution-bg)]",
border: "border-[var(--notice-caution-border)]",
iconColor: "text-[var(--notice-caution-icon)]",
icon: AlertTriangle,
},
danger: {
bar: "border-l-danger",
bg: "bg-danger/8",
border: "border-danger/40",
iconColor: "text-danger",
bar: "border-l-[var(--notice-danger-bar)]",
bg: "bg-[var(--notice-danger-bg)]",
border: "border-[var(--notice-danger-border)]",
iconColor: "text-[var(--notice-danger-icon)]",
icon: AlertOctagon,
},
success: {
bar: "border-l-success",
bg: "bg-success/8",
border: "border-success/40",
iconColor: "text-success",
bar: "border-l-[var(--notice-success-bar)]",
bg: "bg-[var(--notice-success-bg)]",
border: "border-[var(--notice-success-border)]",
iconColor: "text-[var(--notice-success-icon)]",
icon: CheckCircle,
},
};