Files
Lumotia/src/lib/ui/LumotiaButton.svelte
Jake 2c6f5ca230 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>
2026-05-15 15:39:21 +01:00

113 lines
4.2 KiB
Svelte
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<script lang="ts">
// v0.2 Phase 5 primitive — native <button> + token-driven styling.
// Replaces the ~60 ad-hoc button blocks scattered across pages with
// one grammar (primary | secondary | tertiary | destructive × sm | md | lg).
// Bypasses Bits UI deliberately: <button> is already accessible and
// the headless library would only add weight here.
import type { Snippet } from "svelte";
type Variant = "primary" | "secondary" | "tertiary" | "destructive" | "record";
type Size = "sm" | "md" | "lg";
interface Props {
variant?: Variant;
size?: Size;
type?: "button" | "submit" | "reset";
disabled?: boolean;
loading?: boolean;
fullWidth?: boolean;
title?: string;
ariaLabel?: string;
onclick?: (e: MouseEvent) => void;
children?: Snippet;
classes?: string;
}
let {
variant = "secondary" as Variant,
size = "md" as Size,
type = "button",
disabled = false,
loading = false,
fullWidth = false,
title,
ariaLabel,
onclick,
children,
classes = "",
}: Props = $props();
const sizeClass: Record<Size, string> = {
sm: "btn-sm",
md: "btn-md",
lg: "btn-lg",
};
// 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 4j — colour-grammar correction.
//
// 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-[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-[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);
</script>
<button
{type}
class="rounded-lg font-medium select-none disabled:opacity-50 disabled:cursor-not-allowed
focus-visible:outline-none
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]}
{fullWidth ? 'w-full' : ''}
{classes}"
style="transition-duration: var(--duration-ui)"
disabled={busy}
aria-busy={loading ? "true" : undefined}
aria-label={ariaLabel}
{title}
{onclick}
>
{#if loading}
<span class="inline-block animate-spin" aria-hidden="true"></span>
{/if}
{#if children}{@render children()}{/if}
</button>