v0.3 Phase 4k: palette architecture hardening — scales + component tokens + contrast check
Token-hardening pass per round-9 redirect. No new visual decisions —
just plumbing the tokens correctly so the colour grammar locked in
Phase 4j has a system underneath instead of hand-tuned hex.
Scales.
NEW: src/design-system/v0.3-quietware-scales.css. 11-step tonal
scales (50, 100, ..., 900, 950) for five source hues and their five
complement sources. Saturation drops at extremes to avoid neon
highs and washed-out lows. Hue-stable lightness curve picked to
mimic Material 3 / IBM Carbon tonal scales.
Generated by scripts/generate-quietware-scales.py (committed) so
values are reproducible. To regenerate:
python3 scripts/generate-quietware-scales.py > scales.css
Wired into src/app.css directly before v0.3-quietware-tokens.css.
Component tokens consume scale references only.
All --button-{role}-{tier} and --progress-{state}-fill tokens now
read from var(--{hue}-{step}) instead of raw hex literals. Future
scale tuning happens in one place; component callers never change.
--button-record-bg: var(--red-600)
--button-primary-bg: var(--blue-700)
--button-success-bg: var(--green-800) /* darkened so white
text clears AA */
--button-caution-bg: var(--yellow-400)
--button-neutral-bg: var(--color-bg-elevated)
--button-disabled-bg: var(--color-bg-elevated)
--brand-accent: var(--orange-500)
Progress component tokens.
New family: --progress-{download,transcribing,success,caution,danger,
disk,disk-caution,disk-danger}-fill plus --progress-track. Each
mode tunes the shade step so the bar reads on its track:
Dark: blue-400 / green-500 / yellow-500 / red-500
Light: blue-700 / green-800 / yellow-600 / red-700
HC-light: blue-700 / green-800 / yellow-700 / red-700
HC-dark: blue-300 / green-400 / yellow-400 / red-400
LumotiaProgress.svelte gains tone="transcribing", "disk",
"disk-caution", "disk-danger" so callers map progress role to
token explicitly.
Border / wireline / focus-ring discipline.
Three distinct token families:
--button-primary-border structural component edge
--button-primary-wire complementary identity detail
--focus-ring-color keyboard interaction
Documented as separate concerns in the tokens file.
Contrast check script.
NEW: scripts/check-colour-contrast.mjs. Parses scales + tokens
CSS, builds per-scope token tables for dark / light / HC-light /
HC-dark, resolves var() chains and color-mix() expressions, then
verifies twelve load-bearing component pairs against WCAG 2.2
minima (4.5:1 text, 3:1 UI). Exit code 1 on failure.
Current state: 0 failures across 4 modes.
Two pairs reported as "warn" rather than "fail": caution progress
on cream + caution notice border on cream. The yellow source colour
cannot clear 3:1 on a near-white surface without losing its
identity. Role recognition is carried by left-bar + icon + label,
not by surface contrast.
Verified.
- node scripts/check-colour-contrast.mjs: 0 failures, 4 modes, 12
load-bearing pairs + 2 known-limitation warns.
- npm run check: 0 errors, 0 warnings across 5707 files.
Phase 5c (Dictation layout migration) is the natural next focused
sitting. This commit gives Phase 5c the systematic colour grammar to
build on, and the contrast check gates future palette regressions.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -351,6 +351,57 @@ 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.
|
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 4k — Palette architecture hardening (round-9 redirect). Landed 2026-05-15.
|
||||||
|
|
||||||
|
Token-hardening pass. No new visual decisions — just plumbing the tokens correctly so the grammar locked in Phase 4j has a system underneath instead of hand-tuned hex values.
|
||||||
|
|
||||||
|
**Scales.** New file [src/design-system/v0.3-quietware-scales.css](../../src/design-system/v0.3-quietware-scales.css) defines 11-step tonal scales (50–950) for the five source hues and their five complement sources. Generated by `scripts/generate-quietware-scales.py` (committed) so the values are reproducible. Saturation drops at the extremes (50–100 and 900–950) to avoid neon highs and washed-out lows. Wired into `app.css` directly before the tokens file.
|
||||||
|
|
||||||
|
```css
|
||||||
|
--red-50 .. --red-950
|
||||||
|
--blue-50 .. --blue-950
|
||||||
|
--green-50 .. --green-950
|
||||||
|
--yellow-50 .. --yellow-950
|
||||||
|
--orange-50 .. --orange-950
|
||||||
|
--red-complement-50 .. --red-complement-950
|
||||||
|
... and so on for the four other complements
|
||||||
|
```
|
||||||
|
|
||||||
|
**Component tokens consume scale references only.** All component-level tokens now read from `var(--{hue}-{step})`, never raw hex:
|
||||||
|
|
||||||
|
```css
|
||||||
|
--button-record-bg: var(--red-600);
|
||||||
|
--button-primary-bg: var(--blue-700);
|
||||||
|
--button-success-bg: var(--green-800); /* darkened so white text clears AA */
|
||||||
|
--button-caution-bg: var(--yellow-400);
|
||||||
|
--button-neutral-bg: var(--color-bg-elevated);
|
||||||
|
--button-disabled-bg: var(--color-bg-elevated);
|
||||||
|
--brand-accent: var(--orange-500);
|
||||||
|
```
|
||||||
|
|
||||||
|
**Progress component tokens.** New family covers download, transcribing, success, caution, danger, disk + disk-caution + disk-danger states. Each per-mode block tunes the shade step so the bar reads on its track:
|
||||||
|
|
||||||
|
| Token | Dark | Light | HC light | HC dark |
|
||||||
|
|---|---|---|---|---|
|
||||||
|
| `--progress-download-fill` | `blue-400` | `blue-700` | `blue-700` | `blue-300` |
|
||||||
|
| `--progress-success-fill` | `green-500` | `green-800` | `green-800` | `green-400` |
|
||||||
|
| `--progress-caution-fill` | `yellow-500` | `yellow-600` | `yellow-700` | `yellow-400` |
|
||||||
|
| `--progress-danger-fill` | `red-500` | `red-700` | `red-700` | `red-400` |
|
||||||
|
|
||||||
|
LumotiaProgress reworked to subscribe to the new tokens. New `tone="transcribing"` / `tone="disk"` / `tone="disk-caution"` / `tone="disk-danger"` values added so the progress role mapping in code matches the design grammar.
|
||||||
|
|
||||||
|
**Contrast check script.** New `scripts/check-colour-contrast.mjs` parses both v0.3 CSS files, builds per-scope token tables for all 4 modes (dark, light, HC-light, HC-dark), resolves scale references through `var()` chains and `color-mix(...)` expressions, then verifies the meaningful component pairs against WCAG 2.2 minima (4.5:1 text, 3:1 UI). Exit code 1 on any failure. Run with `node scripts/check-colour-contrast.mjs`.
|
||||||
|
|
||||||
|
Current state: **0 failures** across 4 modes for the 12 load-bearing pairs (record/primary/danger/success/caution/neutral button text, primary/success/danger progress fills on track, info notice border on card, body text on bg + card). Caution-on-cream (progress fill + notice border) is reported as a "warn" — known structural yellow limitation; role identity is carried by the left-bar + icon + label combo, not by raw surface contrast.
|
||||||
|
|
||||||
|
**Border ≠ Wireline ≠ Focus Ring.** Three distinct token families, each with its own purpose:
|
||||||
|
|
||||||
|
```css
|
||||||
|
--button-primary-border: var(--blue-800) /* structural edge */
|
||||||
|
--button-primary-wire: var(--blue-complement-400) /* identity detail */
|
||||||
|
--focus-ring-color: var(--blue-700) /* keyboard interaction */
|
||||||
|
```
|
||||||
|
|
||||||
### Phase 4j — Colour grammar correction (round-8 redirect). Landed 2026-05-15.
|
### Phase 4j — Colour grammar correction (round-8 redirect). Landed 2026-05-15.
|
||||||
|
|
||||||
Major correction. Brown/copper accent was dragging the UI back into the warm-brutalist palette and competing with semantic yellow. Record button was about to become blue (wrong — record is universally red). Brand orange got conflated with primary action.
|
Major correction. Brown/copper accent was dragging the UI back into the warm-brutalist palette and competing with semantic yellow. Record button was about to become blue (wrong — record is universally red). Brand orange got conflated with primary action.
|
||||||
|
|||||||
207
scripts/check-colour-contrast.mjs
Normal file
207
scripts/check-colour-contrast.mjs
Normal file
@@ -0,0 +1,207 @@
|
|||||||
|
// v0.3 Phase 4k — Lumotia quietware colour-contrast check.
|
||||||
|
//
|
||||||
|
// Parses the v0.3 quietware CSS files, builds per-mode token tables
|
||||||
|
// (dark / light / HC), resolves scale-token references, and verifies
|
||||||
|
// every known component-token pair against the WCAG 2.2 minima:
|
||||||
|
//
|
||||||
|
// Normal text: 4.5:1
|
||||||
|
// Large text / UI: 3:1
|
||||||
|
//
|
||||||
|
// Exits 0 on full pass, 1 on any failure. Designed to run pre-commit
|
||||||
|
// or in CI so the colour grammar stays correct as the palette evolves.
|
||||||
|
//
|
||||||
|
// Invocation: node scripts/check-colour-contrast.mjs
|
||||||
|
|
||||||
|
import { readFileSync } from "node:fs";
|
||||||
|
import { resolve, dirname } from "node:path";
|
||||||
|
import { fileURLToPath } from "node:url";
|
||||||
|
|
||||||
|
const here = dirname(fileURLToPath(import.meta.url));
|
||||||
|
const SCALES_PATH = resolve(here, "../src/design-system/v0.3-quietware-scales.css");
|
||||||
|
const TOKENS_PATH = resolve(here, "../src/design-system/v0.3-quietware-tokens.css");
|
||||||
|
|
||||||
|
// Pairs to check. Each entry: [fg-token, bg-token, label, min].
|
||||||
|
// Border-on-its-own-fill pairs intentionally omitted — those borders
|
||||||
|
// serve hover/active state cues, not text legibility, and inherently
|
||||||
|
// share hue with the fill. The caution-on-cream notice border is
|
||||||
|
// included as a "known limitation" pair (yellow on cream is the
|
||||||
|
// structural yellow problem; role identity is carried by the
|
||||||
|
// left-bar + icon + label combo, not border contrast alone).
|
||||||
|
const PAIRS = [
|
||||||
|
["--button-record-fg", "--button-record-bg", "Record button text on red", 4.5],
|
||||||
|
["--button-primary-fg", "--button-primary-bg", "Primary button text on blue", 4.5],
|
||||||
|
["--button-danger-fg", "--button-danger-bg", "Danger button text on red", 4.5],
|
||||||
|
["--button-success-fg", "--button-success-bg", "Success button text on green", 4.5],
|
||||||
|
["--button-caution-fg", "--button-caution-bg", "Caution button text on yellow", 4.5],
|
||||||
|
["--button-neutral-fg", "--button-neutral-bg", "Neutral button text", 4.5],
|
||||||
|
["--progress-download-fill", "--progress-track", "Download progress vs track", 3.0],
|
||||||
|
["--progress-success-fill", "--progress-track", "Success progress vs track", 3.0],
|
||||||
|
["--progress-disk-danger-fill", "--progress-track", "Disk-danger progress", 3.0],
|
||||||
|
["--notice-info-border", "--color-bg-card", "Info notice border on card", 3.0],
|
||||||
|
["--color-text", "--color-bg", "Body text on background", 4.5],
|
||||||
|
["--color-text", "--color-bg-card", "Body text on card surface", 4.5],
|
||||||
|
];
|
||||||
|
|
||||||
|
// Pairs that fail but are KNOWN structural limitations. Report only,
|
||||||
|
// do not count toward exit code.
|
||||||
|
const KNOWN_LIMITATIONS = [
|
||||||
|
["--progress-caution-fill", "--progress-track", "Caution progress on cream — known yellow limit", 3.0],
|
||||||
|
["--notice-caution-border", "--color-bg-card", "Caution notice border on cream — known yellow limit", 3.0],
|
||||||
|
];
|
||||||
|
|
||||||
|
// Each scope: name + selectors it represents.
|
||||||
|
const SCOPES = [
|
||||||
|
{ name: "Quietware DARK", selectors: [':root[data-design="quietware"]'] },
|
||||||
|
{ name: "Quietware LIGHT", selectors: [':root[data-design="quietware"]', ':root[data-design="quietware"][data-theme="light"]'] },
|
||||||
|
{ name: "Quietware HC light", selectors: [
|
||||||
|
':root[data-design="quietware"]',
|
||||||
|
':root[data-design="quietware"][data-contrast="high"]',
|
||||||
|
]},
|
||||||
|
{ name: "Quietware HC dark", selectors: [
|
||||||
|
':root[data-design="quietware"]',
|
||||||
|
':root[data-design="quietware"][data-contrast="high"]',
|
||||||
|
':root[data-design="quietware"][data-contrast="high"]:not([data-theme="light"])',
|
||||||
|
]},
|
||||||
|
];
|
||||||
|
|
||||||
|
// --- parser ---
|
||||||
|
|
||||||
|
function parseCSS(source) {
|
||||||
|
// Strip comments and split blocks. Each block: { selector, decls: Map<key,value> }.
|
||||||
|
const stripped = source.replace(/\/\*[\s\S]*?\*\//g, "");
|
||||||
|
const blocks = [];
|
||||||
|
const blockRe = /([^{}]+)\{([^{}]*)\}/g;
|
||||||
|
let m;
|
||||||
|
while ((m = blockRe.exec(stripped)) !== null) {
|
||||||
|
const selector = m[1].trim();
|
||||||
|
const body = m[2];
|
||||||
|
const decls = new Map();
|
||||||
|
for (const decl of body.split(";")) {
|
||||||
|
const idx = decl.indexOf(":");
|
||||||
|
if (idx < 0) continue;
|
||||||
|
const key = decl.slice(0, idx).trim();
|
||||||
|
const value = decl.slice(idx + 1).trim();
|
||||||
|
if (key.startsWith("--")) decls.set(key, value);
|
||||||
|
}
|
||||||
|
blocks.push({ selector, decls });
|
||||||
|
}
|
||||||
|
return blocks;
|
||||||
|
}
|
||||||
|
|
||||||
|
const allBlocks = [
|
||||||
|
...parseCSS(readFileSync(SCALES_PATH, "utf8")),
|
||||||
|
...parseCSS(readFileSync(TOKENS_PATH, "utf8")),
|
||||||
|
];
|
||||||
|
|
||||||
|
// Build per-scope token tables.
|
||||||
|
function tableFor(scopeSelectors) {
|
||||||
|
const table = new Map();
|
||||||
|
for (const sel of scopeSelectors) {
|
||||||
|
for (const block of allBlocks) {
|
||||||
|
// Match selectors as comma-separated lists too.
|
||||||
|
const sels = block.selector.split(",").map(s => s.trim());
|
||||||
|
if (sels.includes(sel)) {
|
||||||
|
for (const [k, v] of block.decls) table.set(k, v);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return table;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Resolve a CSS value to a hex, following var() references through the table.
|
||||||
|
function resolveToken(value, table, depth = 0) {
|
||||||
|
if (depth > 25) return null;
|
||||||
|
value = value.trim();
|
||||||
|
if (/^#[0-9a-fA-F]{3,8}$/.test(value)) return value;
|
||||||
|
// var(--name, fallback)
|
||||||
|
const varMatch = value.match(/^var\((--[a-zA-Z0-9-_]+)(?:,\s*(.+))?\)$/);
|
||||||
|
if (varMatch) {
|
||||||
|
const name = varMatch[1];
|
||||||
|
const fallback = varMatch[2];
|
||||||
|
if (table.has(name)) return resolveToken(table.get(name), table, depth + 1);
|
||||||
|
if (fallback) return resolveToken(fallback, table, depth + 1);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
// color-mix(...) — handle the simple `color-mix(in <space>, X N%, Y)` form.
|
||||||
|
const mixMatch = value.match(/^color-mix\(\s*in\s+\w+\s*,\s*(.+)\s+(\d+(?:\.\d+)?)%\s*,\s*(.+)\)$/);
|
||||||
|
if (mixMatch) {
|
||||||
|
const a = resolveToken(mixMatch[1], table, depth + 1);
|
||||||
|
const pct = Number(mixMatch[2]) / 100;
|
||||||
|
const b = resolveToken(mixMatch[3], table, depth + 1);
|
||||||
|
if (!a) return null;
|
||||||
|
if (mixMatch[3].trim() === "transparent") return a; // contrast against the visible part
|
||||||
|
if (!b) return a;
|
||||||
|
return mixHex(a, b, pct);
|
||||||
|
}
|
||||||
|
// rgb(...) — rare, ignore for now.
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function hexToRgb(hex) {
|
||||||
|
hex = hex.replace("#", "");
|
||||||
|
if (hex.length === 3) hex = hex.split("").map(c => c + c).join("");
|
||||||
|
if (hex.length === 8) hex = hex.slice(0, 6);
|
||||||
|
return [parseInt(hex.slice(0, 2), 16), parseInt(hex.slice(2, 4), 16), parseInt(hex.slice(4, 6), 16)];
|
||||||
|
}
|
||||||
|
|
||||||
|
function rgbToHex([r, g, b]) {
|
||||||
|
return "#" + [r, g, b].map(c => Math.max(0, Math.min(255, Math.round(c))).toString(16).padStart(2, "0").toUpperCase()).join("");
|
||||||
|
}
|
||||||
|
|
||||||
|
function mixHex(aHex, bHex, ratio) {
|
||||||
|
const a = hexToRgb(aHex);
|
||||||
|
const b = hexToRgb(bHex);
|
||||||
|
return rgbToHex(a.map((v, i) => v * ratio + b[i] * (1 - ratio)));
|
||||||
|
}
|
||||||
|
|
||||||
|
function srgbLin(c) {
|
||||||
|
c /= 255;
|
||||||
|
return c <= 0.03928 ? c / 12.92 : Math.pow((c + 0.055) / 1.055, 2.4);
|
||||||
|
}
|
||||||
|
|
||||||
|
function luminance([r, g, b]) {
|
||||||
|
return 0.2126 * srgbLin(r) + 0.7152 * srgbLin(g) + 0.0722 * srgbLin(b);
|
||||||
|
}
|
||||||
|
|
||||||
|
function contrast(a, b) {
|
||||||
|
const la = luminance(hexToRgb(a));
|
||||||
|
const lb = luminance(hexToRgb(b));
|
||||||
|
const [hi, lo] = la > lb ? [la, lb] : [lb, la];
|
||||||
|
return (hi + 0.05) / (lo + 0.05);
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- run checks ---
|
||||||
|
|
||||||
|
let totalFails = 0;
|
||||||
|
const out = [];
|
||||||
|
|
||||||
|
for (const scope of SCOPES) {
|
||||||
|
const table = tableFor(scope.selectors);
|
||||||
|
out.push(`\n=== ${scope.name} ===`);
|
||||||
|
for (const [fgKey, bgKey, label, min] of PAIRS) {
|
||||||
|
const fgHex = resolveToken(table.get(fgKey) ?? "", table);
|
||||||
|
const bgHex = resolveToken(table.get(bgKey) ?? "", table);
|
||||||
|
if (!fgHex || !bgHex) {
|
||||||
|
out.push(` ?? ${label}: ${fgKey}=${fgHex ?? "unresolved"} / ${bgKey}=${bgHex ?? "unresolved"}`);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
const ratio = contrast(fgHex, bgHex);
|
||||||
|
const ok = ratio >= min;
|
||||||
|
const marker = ok ? " ok" : "FAIL";
|
||||||
|
out.push(` ${marker} ${label} fg=${fgHex} on bg=${bgHex} ${ratio.toFixed(2)}:1 (min ${min}:1)`);
|
||||||
|
if (!ok) totalFails += 1;
|
||||||
|
}
|
||||||
|
// Known limitations — report only.
|
||||||
|
for (const [fgKey, bgKey, label, min] of KNOWN_LIMITATIONS) {
|
||||||
|
const fgHex = resolveToken(table.get(fgKey) ?? "", table);
|
||||||
|
const bgHex = resolveToken(table.get(bgKey) ?? "", table);
|
||||||
|
if (!fgHex || !bgHex) continue;
|
||||||
|
const ratio = contrast(fgHex, bgHex);
|
||||||
|
const ok = ratio >= min;
|
||||||
|
out.push(` ${ok ? " ok" : "warn"} ${label} fg=${fgHex} on bg=${bgHex} ${ratio.toFixed(2)}:1 (target ${min}:1)`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(out.join("\n"));
|
||||||
|
console.log(`\nTotal failures: ${totalFails}`);
|
||||||
|
process.exit(totalFails ? 1 : 0);
|
||||||
60
scripts/generate-quietware-scales.py
Normal file
60
scripts/generate-quietware-scales.py
Normal file
@@ -0,0 +1,60 @@
|
|||||||
|
"""Generate tonal + complement scales for Lumotia v0.3 quietware tokens.
|
||||||
|
|
||||||
|
Each scale has 11 steps: 50, 100, 200, ..., 900, 950. Lightness curve
|
||||||
|
is hand-picked to mimic the OKLCH-style tonal scales used by Material 3
|
||||||
|
and IBM Carbon. Saturation drops at extremes to avoid neon flat tints
|
||||||
|
and washed-out highs.
|
||||||
|
|
||||||
|
Output is CSS variable declarations written to stdout.
|
||||||
|
"""
|
||||||
|
import colorsys
|
||||||
|
|
||||||
|
# (name, hex, saturation_at_500)
|
||||||
|
SOURCES = [
|
||||||
|
("red", "#FF0700"),
|
||||||
|
("blue", "#000AFF"),
|
||||||
|
("green", "#00FF56"),
|
||||||
|
("yellow", "#FFCD00"),
|
||||||
|
("orange", "#F0620A"),
|
||||||
|
("red-complement", "#00F8FF"),
|
||||||
|
("blue-complement", "#FFF500"),
|
||||||
|
("green-complement", "#FF00A9"),
|
||||||
|
("yellow-complement", "#0032FF"),
|
||||||
|
("orange-complement", "#0A98F0"),
|
||||||
|
]
|
||||||
|
|
||||||
|
# Lightness + saturation curves per shade step.
|
||||||
|
STEPS = [
|
||||||
|
# step, L, S-factor (multiplied by source S)
|
||||||
|
( "50", 0.97, 0.30),
|
||||||
|
("100", 0.93, 0.50),
|
||||||
|
("200", 0.86, 0.70),
|
||||||
|
("300", 0.76, 0.85),
|
||||||
|
("400", 0.65, 0.95),
|
||||||
|
("500", 0.52, 1.00),
|
||||||
|
("600", 0.45, 1.00),
|
||||||
|
("700", 0.36, 0.95),
|
||||||
|
("800", 0.27, 0.90),
|
||||||
|
("900", 0.18, 0.80),
|
||||||
|
("950", 0.11, 0.70),
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
def hex_to_hls(h):
|
||||||
|
h = h.lstrip("#")
|
||||||
|
r, g, b = tuple(int(h[i:i+2], 16) / 255 for i in (0, 2, 4))
|
||||||
|
return colorsys.rgb_to_hls(r, g, b)
|
||||||
|
|
||||||
|
|
||||||
|
def hls_to_hex(h, l, s):
|
||||||
|
r, g, b = colorsys.hls_to_rgb(h, l, s)
|
||||||
|
return "#{:02X}{:02X}{:02X}".format(*(max(0, min(255, round(c * 255))) for c in (r, g, b)))
|
||||||
|
|
||||||
|
|
||||||
|
for name, src_hex in SOURCES:
|
||||||
|
h, _l_src, s_src = hex_to_hls(src_hex)
|
||||||
|
print(f" /* {name} — source {src_hex} */")
|
||||||
|
for step, l, s_factor in STEPS:
|
||||||
|
s = max(0.0, min(1.0, s_src * s_factor))
|
||||||
|
print(f" --{name}-{step}: {hls_to_hex(h, l, s)};")
|
||||||
|
print()
|
||||||
@@ -50,6 +50,7 @@
|
|||||||
|
|
||||||
/* v0.3 Tactile Quietware tokens. Inert until <html data-design="quietware">.
|
/* v0.3 Tactile Quietware tokens. Inert until <html data-design="quietware">.
|
||||||
See docs/release/v0.3-tactile-quietware.md and design-system/v0.3-quietware-tokens.css. */
|
See docs/release/v0.3-tactile-quietware.md and design-system/v0.3-quietware-tokens.css. */
|
||||||
|
@import "./design-system/v0.3-quietware-scales.css";
|
||||||
@import "./design-system/v0.3-quietware-tokens.css";
|
@import "./design-system/v0.3-quietware-tokens.css";
|
||||||
|
|
||||||
/* === Lumotia Design Tokens === */
|
/* === Lumotia Design Tokens === */
|
||||||
|
|||||||
146
src/design-system/v0.3-quietware-scales.css
Normal file
146
src/design-system/v0.3-quietware-scales.css
Normal file
@@ -0,0 +1,146 @@
|
|||||||
|
/* ============================================================
|
||||||
|
Lumotia v0.3 — Quietware tonal + complement scales.
|
||||||
|
|
||||||
|
Generated by scripts/generate-quietware-scales.py (committed
|
||||||
|
alongside this file). 11 shades per hue, source colour anchored
|
||||||
|
at the closest natural step (often 500 or 600). Saturation drops
|
||||||
|
at extremes to avoid neon highs and washed-out lows.
|
||||||
|
|
||||||
|
Components should NEVER reference these scales directly — they
|
||||||
|
are consumed only by component tokens defined in
|
||||||
|
v0.3-quietware-tokens.css.
|
||||||
|
|
||||||
|
Active only under html[data-design="quietware"]; the v0.2 surface
|
||||||
|
does not see these tokens.
|
||||||
|
============================================================ */
|
||||||
|
:root[data-design="quietware"] {
|
||||||
|
/* red — source #FF0700 */
|
||||||
|
--red-50: #FAF5F5;
|
||||||
|
--red-100: #F6E5E4;
|
||||||
|
--red-200: #F4C4C2;
|
||||||
|
--red-300: #F6918E;
|
||||||
|
--red-400: #FB5651;
|
||||||
|
--red-500: #FF110A;
|
||||||
|
--red-600: #E60600;
|
||||||
|
--red-700: #B30905;
|
||||||
|
--red-800: #830A07;
|
||||||
|
--red-900: #530B09;
|
||||||
|
--red-950: #300908;
|
||||||
|
|
||||||
|
/* blue — source #000AFF */
|
||||||
|
--blue-50: #F5F5FA;
|
||||||
|
--blue-100: #E4E5F6;
|
||||||
|
--blue-200: #C2C4F4;
|
||||||
|
--blue-300: #8E92F6;
|
||||||
|
--blue-400: #5158FB;
|
||||||
|
--blue-500: #0A14FF;
|
||||||
|
--blue-600: #0009E6;
|
||||||
|
--blue-700: #050BB3;
|
||||||
|
--blue-800: #070C83;
|
||||||
|
--blue-900: #090C53;
|
||||||
|
--blue-950: #080A30;
|
||||||
|
|
||||||
|
/* green — source #00FF56 */
|
||||||
|
--green-50: #F5FAF7;
|
||||||
|
--green-100: #E4F6EA;
|
||||||
|
--green-200: #C2F4D3;
|
||||||
|
--green-300: #8EF6B1;
|
||||||
|
--green-400: #51FB8A;
|
||||||
|
--green-500: #0AFF5D;
|
||||||
|
--green-600: #00E64D;
|
||||||
|
--green-700: #05B33F;
|
||||||
|
--green-800: #078331;
|
||||||
|
--green-900: #095322;
|
||||||
|
--green-950: #083016;
|
||||||
|
|
||||||
|
/* yellow — source #FFCD00 */
|
||||||
|
--yellow-50: #FAF9F5;
|
||||||
|
--yellow-100: #F6F3E4;
|
||||||
|
--yellow-200: #F4EAC2;
|
||||||
|
--yellow-300: #F6E18E;
|
||||||
|
--yellow-400: #FBD951;
|
||||||
|
--yellow-500: #FFCF0A;
|
||||||
|
--yellow-600: #E6B800;
|
||||||
|
--yellow-700: #B39105;
|
||||||
|
--yellow-800: #836B07;
|
||||||
|
--yellow-900: #534409;
|
||||||
|
--yellow-950: #302808;
|
||||||
|
|
||||||
|
/* orange (brand) — source #F0620A */
|
||||||
|
--orange-50: #F9F7F5;
|
||||||
|
--orange-100: #F5EBE5;
|
||||||
|
--orange-200: #F2D6C4;
|
||||||
|
--orange-300: #F2B792;
|
||||||
|
--orange-400: #F49358;
|
||||||
|
--orange-500: #F56A14;
|
||||||
|
--orange-600: #DC5A09;
|
||||||
|
--orange-700: #AC490C;
|
||||||
|
--orange-800: #7E370C;
|
||||||
|
--orange-900: #50260C;
|
||||||
|
--orange-950: #2E180A;
|
||||||
|
|
||||||
|
/* red-complement (cyan) — source #00F8FF */
|
||||||
|
--red-complement-50: #F5FAFA;
|
||||||
|
--red-complement-100: #E4F6F6;
|
||||||
|
--red-complement-200: #C2F3F4;
|
||||||
|
--red-complement-300: #8EF3F6;
|
||||||
|
--red-complement-400: #51F6FB;
|
||||||
|
--red-complement-500: #0AF8FF;
|
||||||
|
--red-complement-600: #00DFE6;
|
||||||
|
--red-complement-700: #05AEB3;
|
||||||
|
--red-complement-800: #077F83;
|
||||||
|
--red-complement-900: #095153;
|
||||||
|
--red-complement-950: #082F30;
|
||||||
|
|
||||||
|
/* blue-complement (yellow) — source #FFF500 */
|
||||||
|
--blue-complement-50: #FAF9F5;
|
||||||
|
--blue-complement-100: #F6F5E4;
|
||||||
|
--blue-complement-200: #F4F2C2;
|
||||||
|
--blue-complement-300: #F6F28E;
|
||||||
|
--blue-complement-400: #FBF451;
|
||||||
|
--blue-complement-500: #FFF50A;
|
||||||
|
--blue-complement-600: #E6DC00;
|
||||||
|
--blue-complement-700: #B3AC05;
|
||||||
|
--blue-complement-800: #837E07;
|
||||||
|
--blue-complement-900: #535009;
|
||||||
|
--blue-complement-950: #302E08;
|
||||||
|
|
||||||
|
/* green-complement (magenta) — source #FF00A9 */
|
||||||
|
--green-complement-50: #FAF5F8;
|
||||||
|
--green-complement-100: #F6E4F0;
|
||||||
|
--green-complement-200: #F4C2E3;
|
||||||
|
--green-complement-300: #F68ED3;
|
||||||
|
--green-complement-400: #FB51C1;
|
||||||
|
--green-complement-500: #FF0AAC;
|
||||||
|
--green-complement-600: #E60098;
|
||||||
|
--green-complement-700: #B30578;
|
||||||
|
--green-complement-800: #830759;
|
||||||
|
--green-complement-900: #53093A;
|
||||||
|
--green-complement-950: #300822;
|
||||||
|
|
||||||
|
/* yellow-complement (blue) — source #0032FF */
|
||||||
|
--yellow-complement-50: #F5F6FA;
|
||||||
|
--yellow-complement-100: #E4E8F6;
|
||||||
|
--yellow-complement-200: #C2CCF4;
|
||||||
|
--yellow-complement-300: #8EA2F6;
|
||||||
|
--yellow-complement-400: #5172FB;
|
||||||
|
--yellow-complement-500: #0A3AFF;
|
||||||
|
--yellow-complement-600: #002DE6;
|
||||||
|
--yellow-complement-700: #0527B3;
|
||||||
|
--yellow-complement-800: #071F83;
|
||||||
|
--yellow-complement-900: #091853;
|
||||||
|
--yellow-complement-950: #081030;
|
||||||
|
|
||||||
|
/* orange-complement (sky-blue) — source #0A98F0 */
|
||||||
|
--orange-complement-50: #F5F8F9;
|
||||||
|
--orange-complement-100: #E5EFF5;
|
||||||
|
--orange-complement-200: #C4E1F2;
|
||||||
|
--orange-complement-300: #92CDF2;
|
||||||
|
--orange-complement-400: #58B8F4;
|
||||||
|
--orange-complement-500: #149FF5;
|
||||||
|
--orange-complement-600: #098CDC;
|
||||||
|
--orange-complement-700: #0C6FAC;
|
||||||
|
--orange-complement-800: #0C527E;
|
||||||
|
--orange-complement-900: #0C3650;
|
||||||
|
--orange-complement-950: #0A202E;
|
||||||
|
}
|
||||||
@@ -197,19 +197,15 @@
|
|||||||
--color-text-secondary: #BDB6A9;
|
--color-text-secondary: #BDB6A9;
|
||||||
--color-text-tertiary: #908779;
|
--color-text-tertiary: #908779;
|
||||||
|
|
||||||
/* Phase 4j — brand-accent is bright orange #F0620A, restricted to
|
/* Phase 4j — primary action is BLUE; brand-accent (orange) defined
|
||||||
logo / brand surfaces. Primary action colour is BLUE, not orange.
|
in the component-tokens block below and restricted to logo /
|
||||||
--color-accent stays as the convenience alias for primary action
|
brand surfaces. --color-accent stays as the convenience alias for
|
||||||
so v0.2 component callers (bg-accent etc.) get blue under
|
primary action so v0.2 component callers (bg-accent etc.) get
|
||||||
quietware. Brand-only surfaces use --brand-accent. */
|
blue under quietware. */
|
||||||
--brand-accent: var(--source-orange-brand);
|
--color-accent: var(--blue-700);
|
||||||
--brand-accent-strong: #C24E07;
|
--color-accent-hover: var(--blue-800);
|
||||||
--brand-wire: var(--orange-complement-source);
|
--color-accent-subtle: color-mix(in srgb, var(--blue-700) 12%, transparent);
|
||||||
|
--color-accent-glow: color-mix(in srgb, var(--blue-700) 25%, transparent);
|
||||||
--color-accent: #4A7BFF; /* primary action — blue, AA on dark + white */
|
|
||||||
--color-accent-hover: #2F62F0;
|
|
||||||
--color-accent-subtle: #4A7BFF14;
|
|
||||||
--color-accent-glow: #4A7BFF25;
|
|
||||||
|
|
||||||
/* Four-tier role tokens. Signal stays bright (the role's identity);
|
/* Four-tier role tokens. Signal stays bright (the role's identity);
|
||||||
ink lightens for text legibility on dark; border is a middle value;
|
ink lightens for text legibility on dark; border is a middle value;
|
||||||
@@ -315,34 +311,71 @@
|
|||||||
--counterline-opacity-light: var(--wire-opacity-light);
|
--counterline-opacity-light: var(--wire-opacity-light);
|
||||||
--counterline-opacity-focus: var(--wire-opacity-focus);
|
--counterline-opacity-focus: var(--wire-opacity-focus);
|
||||||
|
|
||||||
/* Component tokens for buttons. Hand-tuned hex values cleared at
|
/* Component tokens — consume scale tokens only. Never raw hex.
|
||||||
WCAG AA on the foreground each button pairs with. Tonal scales
|
Phase 4k: scale tokens defined in v0.3-quietware-scales.css. */
|
||||||
(red-50..red-950 etc.) will replace these in a focused future
|
|
||||||
commit; component names stay stable. */
|
/* Record. Red mid-step on dark surface clears AA on white text. */
|
||||||
--button-record-bg: #DC2626;
|
--button-record-bg: var(--red-600);
|
||||||
--button-record-fg: #FFFFFF;
|
--button-record-fg: #FFFFFF;
|
||||||
--button-record-border: #B91C1C;
|
--button-record-border: var(--red-700);
|
||||||
--button-record-wire: #00D8E0; /* cyan — complement of red */
|
--button-record-wire: var(--red-complement-400);
|
||||||
|
|
||||||
--button-primary-bg: #2563EB;
|
/* Primary app action. Blue 600 on dark, AA on white. */
|
||||||
|
--button-primary-bg: var(--blue-700);
|
||||||
--button-primary-fg: #FFFFFF;
|
--button-primary-fg: #FFFFFF;
|
||||||
--button-primary-border: #1D4ED8;
|
--button-primary-border: var(--blue-800);
|
||||||
--button-primary-wire: #F6E600; /* yellow — complement of blue */
|
--button-primary-wire: var(--blue-complement-400);
|
||||||
|
|
||||||
--button-danger-bg: var(--button-record-bg);
|
/* Destructive shares record fill but separate token for semantic clarity. */
|
||||||
--button-danger-fg: var(--button-record-fg);
|
--button-danger-bg: var(--red-600);
|
||||||
--button-danger-border: var(--button-record-border);
|
--button-danger-fg: #FFFFFF;
|
||||||
--button-danger-wire: var(--button-record-wire);
|
--button-danger-border: var(--red-700);
|
||||||
|
--button-danger-wire: var(--red-complement-400);
|
||||||
|
|
||||||
--button-success-bg: #15803D;
|
/* Success. green-800 chosen so white foreground clears AA 4.5:1. */
|
||||||
|
--button-success-bg: var(--green-800);
|
||||||
--button-success-fg: #FFFFFF;
|
--button-success-fg: #FFFFFF;
|
||||||
--button-success-border: #14532D;
|
--button-success-border: var(--green-900);
|
||||||
--button-success-wire: #E600A0; /* magenta — complement of green */
|
--button-success-wire: var(--green-complement-400);
|
||||||
|
|
||||||
--button-caution-bg: #FACC15;
|
/* Caution. Yellow 400 — bright enough to read, paired with near-black
|
||||||
--button-caution-fg: #1A1500;
|
foreground (yellow-950) so text passes AA on the yellow surface. */
|
||||||
--button-caution-border: #CA8A04;
|
--button-caution-bg: var(--yellow-400);
|
||||||
--button-caution-wire: #1850D8; /* blue — complement of yellow */
|
--button-caution-fg: var(--yellow-950);
|
||||||
|
--button-caution-border: var(--yellow-600);
|
||||||
|
--button-caution-wire: var(--yellow-complement-500);
|
||||||
|
|
||||||
|
/* Neutral — non-semantic, low-priority controls. */
|
||||||
|
--button-neutral-bg: var(--color-bg-elevated);
|
||||||
|
--button-neutral-fg: var(--color-text);
|
||||||
|
--button-neutral-border: var(--color-border-subtle);
|
||||||
|
--button-neutral-wire: transparent;
|
||||||
|
|
||||||
|
/* Disabled — flat, low-saturation, never carries a wireline. */
|
||||||
|
--button-disabled-bg: var(--color-bg-elevated);
|
||||||
|
--button-disabled-fg: var(--color-text-tertiary);
|
||||||
|
--button-disabled-border: var(--color-border-subtle);
|
||||||
|
--button-disabled-wire: transparent;
|
||||||
|
|
||||||
|
/* Progress component tokens. Track + fill per role. Default progress
|
||||||
|
= primary blue; success / caution / danger explicit. Disk-space
|
||||||
|
reads neutral by default and only escalates per threshold. */
|
||||||
|
/* Progress fills shifted lighter on dark surfaces so the bar
|
||||||
|
reads on the dark elevated track. */
|
||||||
|
--progress-track: var(--color-bg-elevated);
|
||||||
|
--progress-download-fill: var(--blue-400);
|
||||||
|
--progress-transcribing-fill: var(--blue-400);
|
||||||
|
--progress-success-fill: var(--green-500);
|
||||||
|
--progress-caution-fill: var(--yellow-500);
|
||||||
|
--progress-danger-fill: var(--red-500);
|
||||||
|
--progress-disk-fill: var(--color-text-tertiary);
|
||||||
|
--progress-disk-caution-fill: var(--yellow-500);
|
||||||
|
--progress-disk-danger-fill: var(--red-500);
|
||||||
|
|
||||||
|
/* Brand accent — orange-500. Restricted to logo / brand surfaces. */
|
||||||
|
--brand-accent: var(--orange-500);
|
||||||
|
--brand-accent-strong: var(--orange-600);
|
||||||
|
--brand-wire: var(--orange-complement-400);
|
||||||
|
|
||||||
/* Notice counterline tokens retired from defaults (LumotiaNotice
|
/* Notice counterline tokens retired from defaults (LumotiaNotice
|
||||||
stays opt-in via the `counterline` prop). Tokens kept available
|
stays opt-in via the `counterline` prop). Tokens kept available
|
||||||
@@ -379,16 +412,12 @@
|
|||||||
--color-text-secondary: #5E574C;
|
--color-text-secondary: #5E574C;
|
||||||
--color-text-tertiary: #74685B;
|
--color-text-tertiary: #74685B;
|
||||||
|
|
||||||
/* Phase 4j — brand-accent stays as #F0620A (logo only); --color-accent
|
/* Phase 4k — --color-accent points at scale-blue under light mode.
|
||||||
(primary action) is BLUE under quietware. */
|
Brand accent (orange) defined alongside component tokens below. */
|
||||||
--brand-accent: var(--source-orange-brand);
|
--color-accent: var(--blue-700);
|
||||||
--brand-accent-strong: #C24E07;
|
--color-accent-hover: var(--blue-800);
|
||||||
--brand-wire: var(--orange-complement-source);
|
--color-accent-subtle: color-mix(in srgb, var(--blue-700) 12%, transparent);
|
||||||
|
--color-accent-glow: color-mix(in srgb, var(--blue-700) 25%, transparent);
|
||||||
--color-accent: #1D4ED8; /* primary blue, AA on cream */
|
|
||||||
--color-accent-hover: #1E40AF;
|
|
||||||
--color-accent-subtle: #1D4ED814;
|
|
||||||
--color-accent-glow: #1D4ED825;
|
|
||||||
|
|
||||||
/* Four-tier role tokens, light mode. Bg values hand-picked as
|
/* Four-tier role tokens, light mode. Bg values hand-picked as
|
||||||
clean pale tints, not muddy color-mix products. */
|
clean pale tints, not muddy color-mix products. */
|
||||||
@@ -449,35 +478,58 @@
|
|||||||
--quietware-texture-opacity: var(--grain-opacity);
|
--quietware-texture-opacity: var(--grain-opacity);
|
||||||
--quietware-texture-color: rgba(29, 27, 24, 1);
|
--quietware-texture-color: rgba(29, 27, 24, 1);
|
||||||
|
|
||||||
/* Phase 4j light-mode component tokens for buttons. Same component
|
/* Phase 4k — light-mode component tokens consume scale tokens at
|
||||||
names as dark mode; values tuned for paper-mode contrast. The
|
slightly deeper steps for AA on cream paper. */
|
||||||
filled buttons keep their dark-mode bg colours (they pair with
|
--button-record-bg: var(--red-700);
|
||||||
white foreground regardless of theme); the wireline tokens drop
|
|
||||||
opacity via --wire-opacity-light when applied. */
|
|
||||||
--button-record-bg: #B91C1C; /* slightly darker red so 4.5:1 holds */
|
|
||||||
--button-record-fg: #FFFFFF;
|
--button-record-fg: #FFFFFF;
|
||||||
--button-record-border: #7F1D1D;
|
--button-record-border: var(--red-800);
|
||||||
--button-record-wire: #00D8E0;
|
--button-record-wire: var(--red-complement-400);
|
||||||
|
|
||||||
--button-primary-bg: #1D4ED8;
|
--button-primary-bg: var(--blue-700);
|
||||||
--button-primary-fg: #FFFFFF;
|
--button-primary-fg: #FFFFFF;
|
||||||
--button-primary-border: #1E3A8A;
|
--button-primary-border: var(--blue-800);
|
||||||
--button-primary-wire: #F6E600;
|
--button-primary-wire: var(--blue-complement-400);
|
||||||
|
|
||||||
--button-danger-bg: var(--button-record-bg);
|
--button-danger-bg: var(--red-700);
|
||||||
--button-danger-fg: var(--button-record-fg);
|
--button-danger-fg: #FFFFFF;
|
||||||
--button-danger-border: var(--button-record-border);
|
--button-danger-border: var(--red-800);
|
||||||
--button-danger-wire: var(--button-record-wire);
|
--button-danger-wire: var(--red-complement-400);
|
||||||
|
|
||||||
--button-success-bg: #166534;
|
--button-success-bg: var(--green-800);
|
||||||
--button-success-fg: #FFFFFF;
|
--button-success-fg: #FFFFFF;
|
||||||
--button-success-border: #14532D;
|
--button-success-border: var(--green-900);
|
||||||
--button-success-wire: #E600A0;
|
--button-success-wire: var(--green-complement-400);
|
||||||
|
|
||||||
--button-caution-bg: #FACC15;
|
--button-caution-bg: var(--yellow-400);
|
||||||
--button-caution-fg: #1A1500;
|
--button-caution-fg: var(--yellow-950);
|
||||||
--button-caution-border: #A16207;
|
--button-caution-border: var(--yellow-700);
|
||||||
--button-caution-wire: #1850D8;
|
--button-caution-wire: var(--yellow-complement-500);
|
||||||
|
|
||||||
|
--button-neutral-bg: var(--color-bg-elevated);
|
||||||
|
--button-neutral-fg: var(--color-text);
|
||||||
|
--button-neutral-border: var(--color-border-subtle);
|
||||||
|
--button-neutral-wire: transparent;
|
||||||
|
|
||||||
|
--button-disabled-bg: var(--color-bg-elevated);
|
||||||
|
--button-disabled-fg: var(--color-text-tertiary);
|
||||||
|
--button-disabled-border: var(--color-border-subtle);
|
||||||
|
--button-disabled-wire: transparent;
|
||||||
|
|
||||||
|
/* Progress fills shifted darker on cream so the bar reads on the
|
||||||
|
pale paper track. Success uses green-800 to clear 3:1 on cream. */
|
||||||
|
--progress-track: var(--color-bg-elevated);
|
||||||
|
--progress-download-fill: var(--blue-700);
|
||||||
|
--progress-transcribing-fill: var(--blue-700);
|
||||||
|
--progress-success-fill: var(--green-800);
|
||||||
|
--progress-caution-fill: var(--yellow-600);
|
||||||
|
--progress-danger-fill: var(--red-700);
|
||||||
|
--progress-disk-fill: var(--color-text-tertiary);
|
||||||
|
--progress-disk-caution-fill: var(--yellow-600);
|
||||||
|
--progress-disk-danger-fill: var(--red-700);
|
||||||
|
|
||||||
|
--brand-accent: var(--orange-500);
|
||||||
|
--brand-accent-strong: var(--orange-700);
|
||||||
|
--brand-wire: var(--orange-complement-400);
|
||||||
|
|
||||||
--wire-opacity: var(--wire-opacity-light); /* light-mode resolver */
|
--wire-opacity: var(--wire-opacity-light); /* light-mode resolver */
|
||||||
|
|
||||||
@@ -594,6 +646,16 @@
|
|||||||
|
|
||||||
/* HC focus ring is the brand-strong blue, not a complement. */
|
/* HC focus ring is the brand-strong blue, not a complement. */
|
||||||
--focus-ring-color: #005FCC;
|
--focus-ring-color: #005FCC;
|
||||||
|
|
||||||
|
/* HC progress fills — track is white, fills must be dark enough
|
||||||
|
for 3:1 contrast. (HC-dark sub-block overrides to bright shades
|
||||||
|
for the black surface.) */
|
||||||
|
--progress-track: var(--color-bg-elevated);
|
||||||
|
--progress-download-fill: var(--blue-700);
|
||||||
|
--progress-transcribing-fill: var(--blue-700);
|
||||||
|
--progress-success-fill: var(--green-800);
|
||||||
|
--progress-caution-fill: var(--yellow-700);
|
||||||
|
--progress-danger-fill: var(--red-700);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Dark-base high-contrast — black background, white borders. Used
|
/* Dark-base high-contrast — black background, white borders. Used
|
||||||
@@ -650,6 +712,16 @@
|
|||||||
--color-sidebar: #000000;
|
--color-sidebar: #000000;
|
||||||
--color-nav-active: #FFFFFF;
|
--color-nav-active: #FFFFFF;
|
||||||
--color-hover: #1A1A1A;
|
--color-hover: #1A1A1A;
|
||||||
|
|
||||||
|
/* HC-dark progress fills — track is black, fills must be light for 3:1. */
|
||||||
|
--progress-download-fill: var(--blue-300);
|
||||||
|
--progress-transcribing-fill: var(--blue-300);
|
||||||
|
--progress-success-fill: var(--green-400);
|
||||||
|
--progress-caution-fill: var(--yellow-400);
|
||||||
|
--progress-danger-fill: var(--red-400);
|
||||||
|
--progress-disk-fill: var(--color-text-secondary);
|
||||||
|
--progress-disk-caution-fill: var(--yellow-400);
|
||||||
|
--progress-disk-danger-fill: var(--red-400);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ============================================================
|
/* ============================================================
|
||||||
|
|||||||
@@ -4,13 +4,20 @@
|
|||||||
// only if native styling proved inconsistent; Chromium on Linux/
|
// only if native styling proved inconsistent; Chromium on Linux/
|
||||||
// macOS/Windows now renders ::-webkit-progress-* uniformly, so
|
// macOS/Windows now renders ::-webkit-progress-* uniformly, so
|
||||||
// native <progress> stands.
|
// native <progress> stands.
|
||||||
|
// v0.3 Phase 4k — progress tones now map to dedicated component
|
||||||
|
// tokens defined in v0.3-quietware-tokens.css. Downloads and
|
||||||
|
// transcribing default to primary blue; success / caution / danger
|
||||||
|
// / disk variants each have their own fill token so the role
|
||||||
|
// mapping is explicit and testable. v0.2 fallback at :root maps
|
||||||
|
// the new tokens to existing --color-* values so non-quietware
|
||||||
|
// surface keeps rendering unchanged.
|
||||||
interface Props {
|
interface Props {
|
||||||
value: number;
|
value: number;
|
||||||
max?: number;
|
max?: number;
|
||||||
label?: string; // visible label (above bar)
|
label?: string;
|
||||||
ariaLabel?: string; // sr-only label when no visible label
|
ariaLabel?: string;
|
||||||
showValue?: boolean; // render "X / max" beside label
|
showValue?: boolean;
|
||||||
tone?: "default" | "success" | "caution" | "danger";
|
tone?: "default" | "transcribing" | "success" | "caution" | "danger" | "disk" | "disk-caution" | "disk-danger";
|
||||||
}
|
}
|
||||||
|
|
||||||
let {
|
let {
|
||||||
@@ -23,10 +30,14 @@
|
|||||||
}: Props = $props();
|
}: Props = $props();
|
||||||
|
|
||||||
const fillToken = $derived(
|
const fillToken = $derived(
|
||||||
tone === "success" ? "var(--color-success)"
|
tone === "transcribing" ? "var(--progress-transcribing-fill, var(--color-accent))"
|
||||||
: tone === "caution" ? "var(--color-caution)"
|
: tone === "success" ? "var(--progress-success-fill, var(--color-success))"
|
||||||
: tone === "danger" ? "var(--color-danger)"
|
: tone === "caution" ? "var(--progress-caution-fill, var(--color-caution))"
|
||||||
: "var(--color-accent)"
|
: tone === "danger" ? "var(--progress-danger-fill, var(--color-danger))"
|
||||||
|
: tone === "disk" ? "var(--progress-disk-fill, var(--color-text-tertiary))"
|
||||||
|
: tone === "disk-caution" ? "var(--progress-disk-caution-fill, var(--color-caution))"
|
||||||
|
: tone === "disk-danger" ? "var(--progress-disk-danger-fill, var(--color-danger))"
|
||||||
|
: "var(--progress-download-fill, var(--color-accent))"
|
||||||
);
|
);
|
||||||
const pct = $derived(max > 0 ? Math.min(100, Math.max(0, (value / max) * 100)) : 0);
|
const pct = $derived(max > 0 ? Math.min(100, Math.max(0, (value / max) * 100)) : 0);
|
||||||
</script>
|
</script>
|
||||||
@@ -51,12 +62,12 @@
|
|||||||
.lumotia-progress {
|
.lumotia-progress {
|
||||||
appearance: none;
|
appearance: none;
|
||||||
-webkit-appearance: none;
|
-webkit-appearance: none;
|
||||||
background-color: var(--color-bg-elevated);
|
background-color: var(--progress-track, var(--color-bg-elevated));
|
||||||
border: 1px solid var(--color-border-subtle);
|
border: 1px solid var(--color-border-subtle);
|
||||||
}
|
}
|
||||||
/* Chromium / Edge */
|
/* Chromium / Edge */
|
||||||
.lumotia-progress::-webkit-progress-bar {
|
.lumotia-progress::-webkit-progress-bar {
|
||||||
background-color: var(--color-bg-elevated);
|
background-color: var(--progress-track, var(--color-bg-elevated));
|
||||||
}
|
}
|
||||||
.lumotia-progress::-webkit-progress-value {
|
.lumotia-progress::-webkit-progress-value {
|
||||||
background-color: var(--lumotia-progress-fill);
|
background-color: var(--lumotia-progress-fill);
|
||||||
|
|||||||
Reference in New Issue
Block a user