v0.3 Phase 4j: colour grammar correction — record red, brand orange separate, wirelines bumped

Major correction per round-8 redirect.

Problem.

  - Brown/copper --color-accent #C97845/#9D5F32 was dragging the UI
    back into the warm-brutalist palette and competing with semantic
    yellow.
  - Record button about to become blue (Lumotia primary). Wrong —
    record is universally red across products and OSes.
  - Brand orange conflated with primary action. Orange should be
    brand mark only.

Solution.

  Source tokens — identity-only, never used directly in components:
    --source-red    #FF0700
    --source-yellow #FFCD00
    --source-green  #00FF56
    --source-blue   #000AFF
    --source-orange-brand #F0620A
    --{role}-complement-source for the wireline layer

  Component tokens — what components subscribe to. Hand-tuned hex
  values verified at WCAG AA on the foreground each button pairs with.
  Tonal scales (red-50..red-950 etc.) reserved for a focused future
  commit; component names stay stable through that upgrade:

    --button-record-bg     #DC2626  (red)
    --button-record-fg     #FFFFFF
    --button-record-border #B91C1C
    --button-record-wire   #00D8E0  (cyan, red's complement)

    --button-primary-bg    #2563EB  (blue)
    --button-primary-fg    #FFFFFF
    --button-primary-wire  #F6E600  (yellow, blue's complement)

    --button-danger-bg     same as record (red, fg white, cyan wire)

    --button-success-bg    #15803D dark / #166534 light  (green)
    --button-success-wire  #E600A0  (magenta, green's complement)

    --button-caution-bg    #FACC15  (yellow, fg #1A1500 near-black)
    --button-caution-wire  #1850D8  (blue, yellow's complement)

    --brand-accent          #F0620A (orange, LOGO ONLY)
    --brand-wire            #0A98F0 (cyan-blue, orange's complement)

  --color-accent repointed:
    quietware dark:   #4A7BFF  (was #C97845 copper)
    quietware light:  #1D4ED8  (was #9D5F32 copper)
    HC:               #005FCC  (existing, unchanged)
    v0.2 fallback:    var(--color-accent) preserved → v0.2 stays amber.

  New LumotiaButton variant: "record". Used by DictationPage's
  record control. Existing v0.2 record button class swapped from
  bg-accent (amber) to bg-[var(--button-record-bg)] (red). v0.2
  fallback at :root maps --button-record-bg to var(--color-danger)
  so non-quietware surface keeps its red-when-recording / amber-when-
  idle behaviour.

Wirelines (renamed from counterlines).

    --wire-width 1px / --wire-width-active 2px / --wire-width-focus 2px
    --wire-opacity-dark 0.75 (was 0.32; bumped per round-8 spec)
    --wire-opacity-light 0.6 (was 0.22)
    --wire-opacity-active 0.9
    --wire-opacity-focus 1
    --wire-opacity is a mode-aware resolver (-dark dark / -light light)
    --wire-style solid (dotted reserved for design-preview / drag-drop)

  HC contract: --wire-width 0, --wire-width-focus 2px kept.
  --focus-ring-color #005FCC (strong blue), not a decorative complement.

  Legacy --counterline-* tokens kept as aliases pointing at the new
  --wire-* set so Phase 4h/4i opt-in code still works.

Design-system-v2 preview now shows the new record variant alongside
primary so both red and blue button identities are visible.

Verified.

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

Phase 5c (Dictation layout migration) still queued for the next
focused sitting. This commit gives Phase 5c the correct grammar to
build on.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-05-15 15:39:21 +01:00
parent 001d8fe83d
commit 2c6f5ca230
5 changed files with 294 additions and 86 deletions

View File

@@ -6,7 +6,7 @@
// the headless library would only add weight here.
import type { Snippet } from "svelte";
type Variant = "primary" | "secondary" | "tertiary" | "destructive";
type Variant = "primary" | "secondary" | "tertiary" | "destructive" | "record";
type Size = "sm" | "md" | "lg";
interface Props {
@@ -47,25 +47,41 @@
// (amber/copper) and pairs with .btn-filled-text for AA. Destructive
// mirrors primary on --color-danger.
//
// v0.3 Phase 4iinteractive counterlines. primary + destructive
// gain a Level-1 inset counterline (softened complement) under
// quietware. Secondary + tertiary stay clean. HC contract zeroes
// --counterline-width so the effect vanishes in HC.
// v0.3 Phase 4jcolour-grammar correction.
//
// Focus state uses Level-2 width (2 px) + --focus-ring-color via a
// focus-visible ring. Tailwind's ring utility writes to a separate
// box-shadow slot so it composes cleanly with the inset counterline.
// primary blue (was amber, now correct primary action)
// record red (globally recognised recording colour)
// destructive red (same fill as record, distinct semantic)
// secondary neutral
// tertiary transparent
//
// Brand orange (#F0620A) is reserved for the Lumotia logo / brand
// mark only and does NOT appear here. The legacy v0.2 path (bg-accent
// amber) still works for callers that have not migrated; --color-accent
// points at primary blue under quietware via the tokens CSS.
//
// Each filled variant gains a Level-1 wireline (complement of its bg).
// HC contract zeroes --wire-width so the effect vanishes in HC.
//
// Focus state uses Level-2 width via Tailwind's ring utility. Separate
// box-shadow slot, composes cleanly with the inset wireline.
const variantClass: Record<Variant, string> = {
primary:
"bg-accent btn-filled-text font-medium hover:bg-accent-hover " +
"shadow-[var(--shadow-accent-pill),inset_0_0_0_var(--counterline-width,0)_var(--button-brand-counterline,transparent)]",
"bg-[var(--button-primary-bg)] text-[var(--button-primary-fg)] font-medium " +
"hover:bg-[var(--button-primary-border)] " +
"shadow-[inset_0_0_0_var(--wire-width,0)_color-mix(in_srgb,var(--button-primary-wire)_calc(var(--wire-opacity,0)*100%),transparent)]",
record:
"bg-[var(--button-record-bg)] text-[var(--button-record-fg)] font-medium " +
"hover:bg-[var(--button-record-border)] " +
"shadow-[inset_0_0_0_var(--wire-width,0)_color-mix(in_srgb,var(--button-record-wire)_calc(var(--wire-opacity,0)*100%),transparent)]",
secondary:
"bg-bg-elevated text-text border border-border-subtle hover:bg-hover",
tertiary:
"bg-transparent text-text-secondary hover:bg-hover hover:text-text",
destructive:
"bg-danger btn-filled-text font-medium hover:bg-danger/90 " +
"shadow-[inset_0_0_0_var(--counterline-width,0)_var(--button-danger-counterline,transparent)]",
"bg-[var(--button-danger-bg)] text-[var(--button-danger-fg)] font-medium " +
"hover:bg-[var(--button-danger-border)] " +
"shadow-[inset_0_0_0_var(--wire-width,0)_color-mix(in_srgb,var(--button-danger-wire)_calc(var(--wire-opacity,0)*100%),transparent)]",
};
const busy = $derived(loading || disabled);
@@ -75,8 +91,8 @@
{type}
class="rounded-lg font-medium select-none disabled:opacity-50 disabled:cursor-not-allowed
focus-visible:outline-none
focus-visible:ring-[var(--counterline-width-focus,2px)]
focus-visible:ring-[color-mix(in_srgb,var(--focus-ring-color,var(--color-accent))_calc(var(--counterline-opacity-focus,0.85)*100%),transparent)]
focus-visible:ring-[var(--wire-width-focus,2px)]
focus-visible:ring-[color-mix(in_srgb,var(--focus-ring-color,var(--color-accent))_calc(var(--wire-opacity-focus,1)*100%),transparent)]
focus-visible:ring-offset-2 focus-visible:ring-offset-bg
{sizeClass[size]}
{variantClass[variant]}