From 001d8fe83df848de154899c14cf6f61144e5cf97 Mon Sep 17 00:00:00 2001 From: Jake Date: Fri, 15 May 2026 15:07:26 +0100 Subject: [PATCH] v0.3 Phase 4i: counterlines as interaction-affordance identity Round-7 redirect (Option D): move counterlines from notices (where they're invisible) to interactive controls (where they add identity). Three-level model: Level 0 no counterline panels, notices (default), content, disabled Level 1 quiet controls, chips, active nav 1 px, 48% opacity dark / 34% light Level 2 focus / strong keyboard focus, recording state 2 px, 85% opacity Notices retire from default counterline rendering. Notice already carries enough semantic language: bar + icon + border + bg + label. A complementary inset on top was adding nothing. Notice can opt in via when it deliberately mimics control identity. Token deltas vs Phase 4h. --counterline-width: 1px unchanged --counterline-width-focus: 2px new (Level-2 width) --counterline-opacity-dark: 0.48 was 0.32 (raised so Level-1 actually reads on controls) --counterline-opacity-light: 0.34 was 0.22 --counterline-opacity-focus: 0.85 new --role-brand-counterline: #3A6BFF new (blue counter for the orange brand accent so primary buttons get an identity edge like the other roles) --button-{role}-counterline new (per-role component tokens subscribing to role-counterline via color-mix) --focus-ring-color new (defaults to brand accent; HC overrides to #005FCC) LumotiaButton applies counterlines to primary + destructive variants only. Secondary + tertiary stay clean. All variants get a Level-2 focus-visible ring at --focus-ring-color * opacity-focus, with the existing ring-offset-2 ring-offset-bg pattern. LumotiaNotice gains a `counterline` boolean prop, default false. When set, the previous Phase 4h inset shadow re-applies. HC contract: counterline-width 0 + all -counterline tokens transparent. Focus width stays 2 px because the HC focus ring IS the tactile detail in that mode. focus-ring-color forces to #005FCC. Honest evaluation of the visible result. Notices: visibly cleaner without the default inset shadow. Reads with more confidence. Good change. Buttons: counterline applied at 1 px / 48% dark / 34% light is perceptible at standard viewing but screenshot-zoom subtle. Focus state implemented but not visible in static screenshots; requires manual tab-test in the dev server. Recommend keeping the architecture even if the visual is restrained, because the focus-state work is genuinely valuable accessibility scaffolding regardless of how the Level-1 reads. Verified. - npm run check: 0 errors, 0 warnings across 5707 files. Phase 5c (Dictation layout migration) still queued for a focused fresh sitting. This commit does not block it. Co-Authored-By: Claude Opus 4.7 (1M context) --- docs/release/v0.3-tactile-quietware.md | 36 ++++++++- src/design-system/v0.3-quietware-tokens.css | 88 +++++++++++++++------ src/lib/ui/LumotiaButton.svelte | 19 ++++- src/lib/ui/LumotiaNotice.svelte | 8 +- 4 files changed, 124 insertions(+), 27 deletions(-) diff --git a/docs/release/v0.3-tactile-quietware.md b/docs/release/v0.3-tactile-quietware.md index b559fe0..b725b77 100644 --- a/docs/release/v0.3-tactile-quietware.md +++ b/docs/release/v0.3-tactile-quietware.md @@ -351,7 +351,41 @@ LumotiaNotice now reads these via Tailwind arbitrary-value classes (`bg-[var(--n Per Jake's round-5 spec: yellow stays yellow (visible signal), ochre only appears as small-text ink, and brown/khaki never represents the caution role's primary identity again. -### Phase 4h — Counterlines test (TBD: keep or remove). Landed 2026-05-15. +### Phase 4i — Counterlines as interaction-affordance identity. Landed 2026-05-15. + +Round-7 redirect (Option D): move counterlines from notices (where they're invisible) to interactive controls (where they add identity). Three-level model: + +| Level | Surface | Width | Opacity | +|---|---|---|---| +| 0 — none | panels, notices (default), content, disabled | — | — | +| 1 — quiet | controls, chips, active nav | 1 px | 48% dark / 34% light | +| 2 — focus | keyboard focus, recording state | 2 px | 85% | + +Notice retires from default counterline rendering. Opt-in via `` when a specific notice deliberately mimics control identity. + +LumotiaButton primary + destructive variants gain a Level-1 inset counterline: + +- primary: `--button-brand-counterline` (softened blue, complement of brand amber) +- destructive: `--button-danger-counterline` (softened blue, complement of red) +- secondary / tertiary: no counterline (clean neutral) +- All variants on `:focus-visible`: 2-px ring at `--focus-ring-color` (default `--color-accent`) at 85% opacity. HC overrides to a strong blue `#005FCC`. + +Token deltas vs Phase 4h: + +```css +--counterline-width: 1px /* unchanged */ +--counterline-width-focus: 2px /* new — Level-2 width */ +--counterline-opacity-dark: 0.48 /* was 0.32 */ +--counterline-opacity-light: 0.34 /* was 0.22 */ +--counterline-opacity-focus: 0.85 /* new */ +--role-brand-counterline: #3A6BFF /* new — blue counter for orange brand */ +--button-{role}-counterline /* new — per-role component tokens */ +--focus-ring-color /* new — context-overridable focus tint */ +``` + +HC contract: every counterline token forced transparent, `--counterline-width: 0`, focus width kept at 2 px (HC focus ring carries the tactile detail in that mode), `--focus-ring-color: #005FCC`. + +### Phase 4h — Counterlines test (RETIRED 2026-05-15, superseded by Phase 4i). Landed 2026-05-15. Test commit per Jake's round-6 spec. Subtle 1-px inner-shadow detail in a softened complementary hue, applied to LumotiaNotice only. Other test surfaces (Button.destructive, status pills, record ring, active nav) reserved for after evaluation. diff --git a/src/design-system/v0.3-quietware-tokens.css b/src/design-system/v0.3-quietware-tokens.css index df9d68b..c97e305 100644 --- a/src/design-system/v0.3-quietware-tokens.css +++ b/src/design-system/v0.3-quietware-tokens.css @@ -219,29 +219,53 @@ --quietware-texture-color: rgba(243, 239, 231, 1); /* ---------------------------------------------------------- - Phase 4h — counterlines (test). - Tactile-only 1-px inner-shadow detail using a softened - complementary hue. NOT a second semantic signal — meaning is - still carried by signal + icon + label. HC contract zeroes - the width. + Phase 4i — counterlines as interactive-affordance identity + (per round-7 redirect). Three-level model: + + Level 0 no counterline (panels, notices, content, disabled) + Level 1 quiet (controls, chips, active nav) + Level 2 strong / focus (keyboard focus, recording state) + + Notices retire from default counterline rendering (they have + enough semantic language already: bar + icon + border + bg + + label). Notice can still opt in via the `counterline` prop. + + HC contract zeroes the normal width but keeps the focus width + because the HC focus ring IS the tactile detail in that mode. ---------------------------------------------------------- */ --counterline-width: 1px; + --counterline-width-focus: 2px; + --counterline-opacity-dark: 0.48; + --counterline-opacity-light: 0.34; + --counterline-opacity-focus: 0.85; - /* Raw complementary hues per Jake's spec. */ + /* Raw complementary hues. Brand orange gets its own counterline + (soft blue) so primary buttons land in the same identity as the + other roles. */ --role-info-counterline: var(--color-accent); --role-danger-counterline: #6EA8FF; --role-success-counterline: #B891FF; --role-caution-counterline: #3A6BFF; + --role-brand-counterline: #3A6BFF; - /* Tinted per theme. Dark mode uses 32% opacity (~slightly stronger - so it reads against the dark surface). Per-usage aliases so - components subscribe semantically. */ - --notice-info-counterline: color-mix(in srgb, var(--role-info-counterline) 32%, transparent); - --notice-danger-counterline: color-mix(in srgb, var(--role-danger-counterline) 32%, transparent); - --notice-success-counterline: color-mix(in srgb, var(--role-success-counterline) 32%, transparent); - --notice-caution-counterline: color-mix(in srgb, var(--role-caution-counterline) 32%, transparent); + /* Component usage tokens. Buttons + chips + nav subscribe to these. + Default opacity is dark; light-mode override below replaces. */ + --button-brand-counterline: color-mix(in srgb, var(--role-brand-counterline) calc(var(--counterline-opacity-dark) * 100%), transparent); + --button-danger-counterline: color-mix(in srgb, var(--role-danger-counterline) calc(var(--counterline-opacity-dark) * 100%), transparent); + --button-info-counterline: color-mix(in srgb, var(--role-info-counterline) calc(var(--counterline-opacity-dark) * 100%), transparent); + --button-success-counterline: color-mix(in srgb, var(--role-success-counterline) calc(var(--counterline-opacity-dark) * 100%), transparent); + --button-caution-counterline: color-mix(in srgb, var(--role-caution-counterline) calc(var(--counterline-opacity-dark) * 100%), transparent); - --button-danger-counterline: var(--notice-danger-counterline); + /* Notice-level counterline tokens kept available for opt-in only. + LumotiaNotice no longer applies them by default per round-7. */ + --notice-info-counterline: color-mix(in srgb, var(--role-info-counterline) calc(var(--counterline-opacity-dark) * 100%), transparent); + --notice-danger-counterline: color-mix(in srgb, var(--role-danger-counterline) calc(var(--counterline-opacity-dark) * 100%), transparent); + --notice-success-counterline: color-mix(in srgb, var(--role-success-counterline) calc(var(--counterline-opacity-dark) * 100%), transparent); + --notice-caution-counterline: color-mix(in srgb, var(--role-caution-counterline) calc(var(--counterline-opacity-dark) * 100%), transparent); + + /* Focus ring colour — defaults to brand accent. Components can + override per-context (eg. info focus = info-counterline). */ + --focus-ring-color: var(--color-accent); } /* ============================================================ @@ -325,14 +349,18 @@ --quietware-texture-opacity: var(--grain-opacity); --quietware-texture-color: rgba(29, 27, 24, 1); - /* Phase 4h light-mode counterlines. Lower opacity (22%) so the - detail stays whisper-quiet against the cream surface. */ - --notice-info-counterline: color-mix(in srgb, var(--role-info-counterline) 22%, transparent); - --notice-danger-counterline: color-mix(in srgb, var(--role-danger-counterline) 22%, transparent); - --notice-success-counterline: color-mix(in srgb, var(--role-success-counterline) 22%, transparent); - --notice-caution-counterline: color-mix(in srgb, var(--role-caution-counterline) 22%, transparent); + /* Phase 4i light-mode counterlines. Lower opacity per Jake's spec + so the detail stays restrained against the cream surface. */ + --button-brand-counterline: color-mix(in srgb, var(--role-brand-counterline) calc(var(--counterline-opacity-light) * 100%), transparent); + --button-danger-counterline: color-mix(in srgb, var(--role-danger-counterline) calc(var(--counterline-opacity-light) * 100%), transparent); + --button-info-counterline: color-mix(in srgb, var(--role-info-counterline) calc(var(--counterline-opacity-light) * 100%), transparent); + --button-success-counterline: color-mix(in srgb, var(--role-success-counterline) calc(var(--counterline-opacity-light) * 100%), transparent); + --button-caution-counterline: color-mix(in srgb, var(--role-caution-counterline) calc(var(--counterline-opacity-light) * 100%), transparent); - --button-danger-counterline: var(--notice-danger-counterline); + --notice-info-counterline: color-mix(in srgb, var(--role-info-counterline) calc(var(--counterline-opacity-light) * 100%), transparent); + --notice-danger-counterline: color-mix(in srgb, var(--role-danger-counterline) calc(var(--counterline-opacity-light) * 100%), transparent); + --notice-success-counterline: color-mix(in srgb, var(--role-success-counterline) calc(var(--counterline-opacity-light) * 100%), transparent); + --notice-caution-counterline: color-mix(in srgb, var(--role-caution-counterline) calc(var(--counterline-opacity-light) * 100%), transparent); } /* ============================================================ @@ -418,14 +446,28 @@ --quietware-texture-opacity: 0; --quietware-texture-color: rgba(0, 0, 0, 0); - /* HC contract: counterlines disabled (the 2-px HC border IS the - tactile detail in this mode). */ + /* HC contract: counterlines disabled for normal states. The HC + focus ring (2 px, full strong-blue) IS the tactile detail in + this mode. */ --counterline-width: 0; + --counterline-width-focus: 2px; + --counterline-opacity-dark: 0; + --counterline-opacity-light: 0; + --counterline-opacity-focus: 1; + --notice-info-counterline: transparent; --notice-danger-counterline: transparent; --notice-success-counterline: transparent; --notice-caution-counterline: transparent; + + --button-brand-counterline: transparent; --button-danger-counterline: transparent; + --button-info-counterline: transparent; + --button-success-counterline: transparent; + --button-caution-counterline: transparent; + + /* HC focus ring is the brand-strong blue, not a complement. */ + --focus-ring-color: #005FCC; } /* Dark-base high-contrast — black background, white borders. Used diff --git a/src/lib/ui/LumotiaButton.svelte b/src/lib/ui/LumotiaButton.svelte index b393359..0250b79 100644 --- a/src/lib/ui/LumotiaButton.svelte +++ b/src/lib/ui/LumotiaButton.svelte @@ -46,15 +46,26 @@ // Variant palette uses semantic tokens; primary fills with --color-accent // (amber/copper) and pairs with .btn-filled-text for AA. Destructive // mirrors primary on --color-danger. + // + // v0.3 Phase 4i — interactive 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. + // + // 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. const variantClass: Record = { primary: - "bg-accent btn-filled-text font-medium hover:bg-accent-hover shadow-[var(--shadow-accent-pill)]", + "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)]", 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", + "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)]", }; const busy = $derived(loading || disabled); @@ -63,6 +74,10 @@