diff --git a/packages/ui/script/colors.txt b/packages/ui/script/colors.txt index 29b553655..d89675bbb 100644 --- a/packages/ui/script/colors.txt +++ b/packages/ui/script/colors.txt @@ -33,15 +33,7 @@ --error-text: #9a2818; --diff-add: rgba(45, 157, 90, 0.1); --diff-del: rgba(210, 74, 58, 0.1); ---shadow-raised: 0 6px 24px rgba(0, 0, 0, 0.06), 0 1px 2px rgba(0, 0, 0, 0.05); ---shadow-floating: 0 8px 24px rgba(0, 0, 0, 0.09), 0 1px 3px rgba(0, 0, 0, 0.07); ---shadow-modal: 0 16px 48px rgba(0, 0, 0, 0.11), 0 4px 12px rgba(0, 0, 0, 0.07); --ring-base: 0 0 0 1px rgba(11, 6, 0, 0.18); ---shadow-xs-border-focus: 0 0 0 1px var(--brand-primary), 0 0 0 3px rgba(255, 89, 16, 0.2); ---shadow-xs-border-select: 0 0 0 1px var(--brand-primary); ---shadow-xs-border-hover: 0 0 0 1px rgba(11, 6, 0, 0.28); ---shadow-xs-border-critical-base: 0 0 0 1px var(--error); ---shadow-xs-border-critical-focus: 0 0 0 1px var(--error), 0 0 0 3px rgba(210, 74, 58, 0.2); --icon-agent-plan-base: #a753ae; --icon-agent-docs-base: #fcb239; --icon-agent-ask-base: #2090f5; @@ -96,7 +88,6 @@ --syntax-diff-add: #3a8437; --syntax-diff-delete: #ca2d17; --syntax-diff-unknown: #ff0000; ---text-mix-blend-mode: multiply; --button-brand-base: #ff5910; --button-brand-hover: #e04e0e; --icon-success-base: #2d9d5a; diff --git a/packages/ui/src/components/checkbox.css b/packages/ui/src/components/checkbox.css index 2e8a25a75..c37b1f015 100644 --- a/packages/ui/src/components/checkbox.css +++ b/packages/ui/src/components/checkbox.css @@ -68,7 +68,7 @@ [data-slot="checkbox-checkbox-description"] { color: var(--fg-base); font-family: var(--font-family-sans); - font-size: 12px; + font-size: var(--font-size-x-small); font-weight: var(--font-weight-regular); line-height: var(--line-height-normal); letter-spacing: var(--letter-spacing-normal); @@ -77,7 +77,7 @@ [data-slot="checkbox-checkbox-error"] { color: var(--error-text); font-family: var(--font-family-sans); - font-size: 12px; + font-size: var(--font-size-x-small); font-weight: var(--font-weight-regular); line-height: var(--line-height-normal); letter-spacing: var(--letter-spacing-normal); diff --git a/packages/ui/src/components/icon.css b/packages/ui/src/components/icon.css index a2ebee30b..f7b519f45 100644 --- a/packages/ui/src/components/icon.css +++ b/packages/ui/src/components/icon.css @@ -3,29 +3,18 @@ align-items: center; justify-content: center; flex-shrink: 0; - /* resize: both; */ aspect-ratio: 1/1; color: var(--icon-base); - &[data-size="small"] { - width: 16px; - height: 16px; - } - - &[data-size="normal"] { - width: 20px; - height: 20px; - } + &[data-color="strong"] { color: var(--icon-strong); } + &[data-color="weak"] { color: var(--icon-weak); } + &[data-color="disabled"] { color: var(--icon-disabled); } - &[data-size="medium"] { - width: 24px; - height: 24px; - } - - &[data-size="large"] { - width: 24px; - height: 24px; - } + /* size="small" is the standard chrome size (16px); normal/medium/large for special contexts */ + &[data-size="small"] { width: 16px; height: 16px; } + &[data-size="normal"] { width: 20px; height: 20px; } + &[data-size="medium"] { width: 24px; height: 24px; } + &[data-size="large"] { width: 24px; height: 24px; } [data-slot="icon-svg"] { width: 100%; diff --git a/packages/ui/src/components/keybind.css b/packages/ui/src/components/keybind.css index 02d186a39..6fb12089e 100644 --- a/packages/ui/src/components/keybind.css +++ b/packages/ui/src/components/keybind.css @@ -9,9 +9,8 @@ background: var(--surface-base); box-shadow: var(--shadow-xxs-border); - /* text-13-regular */ font-family: var(--font-family-sans); - font-size: 12px; + font-size: var(--font-size-x-small); font-weight: var(--font-weight-regular); line-height: 1; color: var(--fg-weak); diff --git a/packages/ui/src/components/message-part.css b/packages/ui/src/components/message-part.css index 46dd14c0e..2b6957d3f 100644 --- a/packages/ui/src/components/message-part.css +++ b/packages/ui/src/components/message-part.css @@ -285,7 +285,7 @@ [data-component="markdown"] { margin-top: 16px; font-style: normal; - font-size: 13px; + font-size: var(--font-size-small); color: var(--fg-weak); strong, @@ -407,7 +407,7 @@ [data-slot="bash-pre"] code { font-family: var(--font-family-mono); font-feature-settings: var(--font-family-mono--font-feature-settings); - font-size: 13px; + font-size: var(--font-size-small); line-height: var(--line-height-large); white-space: pre-wrap; overflow-wrap: anywhere; @@ -1154,7 +1154,7 @@ display: flex; flex-direction: column; gap: 2px; - font-size: 13px; + font-size: var(--font-size-small); [data-slot="question-text"] { color: var(--fg-weak); diff --git a/packages/ui/src/components/radio-group.css b/packages/ui/src/components/radio-group.css index 19339c149..603f91df1 100644 --- a/packages/ui/src/components/radio-group.css +++ b/packages/ui/src/components/radio-group.css @@ -167,7 +167,7 @@ --radio-group-padding: 2px; [data-slot="radio-group-item-label"] { - font-size: 12px; + font-size: var(--font-size-x-small); } [data-slot="radio-group-item-control"] { diff --git a/packages/ui/src/components/switch.css b/packages/ui/src/components/switch.css index 04b476780..e4c0c83f7 100644 --- a/packages/ui/src/components/switch.css +++ b/packages/ui/src/components/switch.css @@ -66,7 +66,7 @@ [data-slot="switch-description"] { color: var(--fg-base); font-family: var(--font-family-sans); - font-size: 12px; + font-size: var(--font-size-x-small); font-weight: var(--font-weight-regular); line-height: var(--line-height-normal); letter-spacing: var(--letter-spacing-normal); @@ -75,7 +75,7 @@ [data-slot="switch-error"] { color: var(--error-text); font-family: var(--font-family-sans); - font-size: 12px; + font-size: var(--font-size-x-small); font-weight: var(--font-weight-regular); line-height: var(--line-height-normal); letter-spacing: var(--letter-spacing-normal); diff --git a/packages/ui/src/styles/tailwind/colors.css b/packages/ui/src/styles/tailwind/colors.css index b131db972..5829a87cc 100644 --- a/packages/ui/src/styles/tailwind/colors.css +++ b/packages/ui/src/styles/tailwind/colors.css @@ -38,15 +38,7 @@ --color-error-text: var(--error-text); --color-diff-add: var(--diff-add); --color-diff-del: var(--diff-del); - --color-shadow-raised: var(--shadow-raised); - --color-shadow-floating: var(--shadow-floating); - --color-shadow-modal: var(--shadow-modal); --color-ring-base: var(--ring-base); - --color-shadow-xs-border-focus: var(--shadow-xs-border-focus); - --color-shadow-xs-border-select: var(--shadow-xs-border-select); - --color-shadow-xs-border-hover: var(--shadow-xs-border-hover); - --color-shadow-xs-border-critical-base: var(--shadow-xs-border-critical-base); - --color-shadow-xs-border-critical-focus: var(--shadow-xs-border-critical-focus); --color-icon-agent-plan-base: var(--icon-agent-plan-base); --color-icon-agent-docs-base: var(--icon-agent-docs-base); --color-icon-agent-ask-base: var(--icon-agent-ask-base); @@ -101,7 +93,6 @@ --color-syntax-diff-add: var(--syntax-diff-add); --color-syntax-diff-delete: var(--syntax-diff-delete); --color-syntax-diff-unknown: var(--syntax-diff-unknown); - --color-text-mix-blend-mode: var(--text-mix-blend-mode); --color-button-brand-base: var(--button-brand-base); --color-button-brand-hover: var(--button-brand-hover); --color-icon-success-base: var(--icon-success-base); diff --git a/packages/ui/src/styles/theme.css b/packages/ui/src/styles/theme.css index 619188806..6ee0fff7d 100644 --- a/packages/ui/src/styles/theme.css +++ b/packages/ui/src/styles/theme.css @@ -84,6 +84,7 @@ --surface-raised: #ffffff; --surface-sunken: #faf9f7; /* light alias → bg-cream; no third neutral tier (L9) */ --sidebar: #faf9f7; + --surface-base-active: rgba(0, 0, 0, 0.051); /* ────────── Light ink · STANDARDS L11 (4 tiers) ────────── */ --fg-strong: #1a1613; @@ -219,9 +220,6 @@ 0 6.38px 14.177px 0 rgba(0, 0, 0, 0.05), 0 3.127px 6.95px 0 rgba(0, 0, 0, 0.06), 0 1.237px 2.748px 0 rgba(0, 0, 0, 0.09); - /* Active-press overlay on base surface */ - --surface-base-active: rgba(0, 0, 0, 0.051); - /* Text mix blend mode utility — controls antialiasing on light surfaces */ --text-mix-blend-mode: multiply; @@ -285,6 +283,7 @@ /* Brand-tinted interactive · dark (warm orange muted) */ --surface-interactive-base: #2b1f19; --surface-interactive-hover: #362619; + --surface-base-active: rgba(255, 255, 255, 0.059); /* Shadow · L18: dark elevation via L10 surfaces; drop shadows forbidden */ --shadow-raised: 0 1px 0 rgba(255, 255, 255, 0.04) inset; @@ -372,7 +371,6 @@ --syntax-diff-delete: #faa494; --syntax-diff-unknown: #ff0000; - --surface-base-active: rgba(255, 255, 255, 0.059); --shadow-xxs-border: 0 0 0 0.5px var(--border-base); --shadow-xs-border-base: 0 0 0 1px var(--border-base), @@ -414,6 +412,7 @@ --surface-interactive-base: #2b1f19; --surface-interactive-hover: #362619; + --surface-base-active: rgba(255, 255, 255, 0.059); --shadow-raised: 0 1px 0 rgba(255, 255, 255, 0.04) inset; --shadow-floating: 0 1px 0 rgba(255, 255, 255, 0.05) inset; @@ -500,7 +499,6 @@ --syntax-diff-delete: #faa494; --syntax-diff-unknown: #ff0000; - --surface-base-active: rgba(255, 255, 255, 0.059); --shadow-xxs-border: 0 0 0 0.5px var(--border-base); --shadow-xs-border-base: 0 0 0 1px var(--border-base), diff --git a/packages/ui/src/theme/themes/pawwork.json b/packages/ui/src/theme/themes/pawwork.json index 2350ce6cb..0b538cbc8 100644 --- a/packages/ui/src/theme/themes/pawwork.json +++ b/packages/ui/src/theme/themes/pawwork.json @@ -25,6 +25,7 @@ "sidebar": "#faf9f7", "surface-interactive-base": "#fdf6f0", "surface-interactive-hover": "#fbece0", + "surface-base-active": "rgba(0, 0, 0, 0.051)", "fg-strong": "#1a1613", "fg-base": "#4a4138", "fg-weak": "#8c817a", @@ -75,6 +76,7 @@ "sidebar": "#15120f", "surface-interactive-base": "#2b1f19", "surface-interactive-hover": "#362619", + "surface-base-active": "rgba(255, 255, 255, 0.059)", "fg-strong": "#f3ede4", "fg-base": "#c5beb4", "fg-weak": "#8a8278", diff --git a/packages/ui/test/colors-generation.test.ts b/packages/ui/test/colors-generation.test.ts new file mode 100644 index 000000000..56802ef0c --- /dev/null +++ b/packages/ui/test/colors-generation.test.ts @@ -0,0 +1,70 @@ +/** + * colors-generation.test.ts + * + * Verifies that tailwind/colors.css is in sync with script/colors.txt. + * For every --X token defined in colors.txt there must be a corresponding + * --color-X: var(--X) entry in colors.css, and vice versa (no orphan entries). + * + * This catches stale artifacts: if colors.txt is updated but colors.css is not + * regenerated, a Tailwind utility like `bg-surface-base-active` would silently + * resolve to the wrong value. + */ + +import { expect, test } from "bun:test" +import { readFileSync } from "fs" +import { join } from "path" + +const ROOT = join(import.meta.dirname, "..") +const COLORS_TXT = readFileSync(join(ROOT, "script/colors.txt"), "utf-8") +const COLORS_CSS = readFileSync(join(ROOT, "src/styles/tailwind/colors.css"), "utf-8") + +/** Extract token names from colors.txt (e.g. "--brand-primary: ..." → "brand-primary") */ +function parseTxtTokens(txt: string): string[] { + return txt + .split("\n") + .filter((l) => l.trim().startsWith("--")) + .map((l) => l.trim().split(":")[0].trim().substring(2)) + .filter(Boolean) +} + +/** Extract token names from colors.css @theme block (e.g. "--color-brand-primary: var(--brand-primary)" → "brand-primary") */ +function parseCssTokens(css: string): string[] { + const entries: string[] = [] + const re = /--color-([^\s:]+)\s*:\s*var\(--\1\)/g + let m: RegExpExecArray | null + while ((m = re.exec(css)) !== null) { + entries.push(m[1]) + } + return entries +} + +const txtTokens = parseTxtTokens(COLORS_TXT) +const cssTokens = parseCssTokens(COLORS_CSS) + +test("colors.txt has no duplicate token names", () => { + const dupes = txtTokens.filter((t, i) => txtTokens.indexOf(t) !== i) + expect(dupes, `duplicate tokens in colors.txt: ${dupes.join(", ")}`).toEqual([]) +}) + +test("colors.css has no duplicate token entries", () => { + const dupes = cssTokens.filter((t, i) => cssTokens.indexOf(t) !== i) + expect(dupes, `duplicate entries in colors.css: ${dupes.join(", ")}`).toEqual([]) +}) + +test("every colors.txt token has a --color-X: var(--X) entry in colors.css", () => { + const cssSet = new Set(cssTokens) + const missing = txtTokens.filter((t) => !cssSet.has(t)) + expect( + missing, + `tokens in colors.txt missing from colors.css: ${missing.join(", ")}`, + ).toEqual([]) +}) + +test("every colors.css entry has a corresponding token in colors.txt", () => { + const txtSet = new Set(txtTokens) + const orphan = cssTokens.filter((t) => !txtSet.has(t)) + expect( + orphan, + `tokens in colors.css not present in colors.txt: ${orphan.join(", ")}`, + ).toEqual([]) +})