From 7823560b73f91e868f328ff754103bfbcfd083aa Mon Sep 17 00:00:00 2001 From: Taylor Ho Date: Sat, 17 Oct 2026 22:01:23 -0700 Subject: [PATCH 01/20] feat(design-system): add selectable plugin color families Co-authored-by: Carl Signed-off-by: Taylor Ho --- docs/design-system.md | 13 ++ src/shared/design-system/styles/tokens.css | 150 ++++++++++++++++++ src/shared/design-system/tokens/brand.test.ts | 33 ++++ src/shared/design-system/tokens/brand.ts | 31 ++++ tests/fixtures/design-system/styles.css | 29 ++++ .../design-system/ui/BrandColors.test.ts | 44 +++++ .../fixtures/design-system/ui/BrandColors.tsx | 117 ++++++++++++++ tests/fixtures/design-system/ui/ColorPage.tsx | 4 + .../design-system/ui/ColorTablePage.tsx | 15 ++ 9 files changed, 436 insertions(+) create mode 100644 src/shared/design-system/tokens/brand.test.ts create mode 100644 src/shared/design-system/tokens/brand.ts create mode 100644 tests/fixtures/design-system/ui/BrandColors.test.ts create mode 100644 tests/fixtures/design-system/ui/BrandColors.tsx diff --git a/docs/design-system.md b/docs/design-system.md index da6efd6473..8072db98da 100644 --- a/docs/design-system.md +++ b/docs/design-system.md @@ -424,3 +424,16 @@ icons, subtitles, disabled states, and required-status text. Standard Button sizes use the 14px `text-label-sm` role while retaining their existing minimum heights. The extra-small capsule keeps its caption role. + +## Plugin color-family exploration + +The Color viewer includes twelve fixed OKLCH families with 50–950 stops. +`--brand--500` preserves each exact gradient endpoint from plugin branding +(`src/app/Settings.module.css` on `tho/plugin-icons`, revision `834b8ea4b`). +The six original gradient pairs are recorded in `tokens/brand.ts`; the values +live in `styles/tokens.css`. Lightness descends from 98% to 19%; chroma tapers +and is capped to sRGB outside the exact source stops. These are exploratory +artwork colors, independent of theme, not new semantic roles or appearance choices. +Select a swatch to inspect its actual CSS value and copy its variable. Product +controls continue to consume semantic roles; the plugin branch is not a dependency +of this viewer and no plugin artwork is imported. diff --git a/src/shared/design-system/styles/tokens.css b/src/shared/design-system/styles/tokens.css index 29a1259e69..34b47ecc92 100644 --- a/src/shared/design-system/styles/tokens.css +++ b/src/shared/design-system/styles/tokens.css @@ -1613,3 +1613,153 @@ --spacing-chip-inset: var(--space-chip-inset); --spacing-chip-gap: var(--space-chip-gap); } + +/* Plugin artwork families: fixed light-to-dark scales, independent of mode. + 500 preserves the exact source endpoint; other stops reduce chroma to sRGB. + Decorative palette only; product controls continue to use semantic roles. */ +:root { + /* cyan */ + --brand-cyan-50: oklch(98% 0.0055 220); + --brand-cyan-100: oklch(95% 0.0136 220); + --brand-cyan-200: oklch(89% 0.03 220); + --brand-cyan-300: oklch(81% 0.0518 220); + --brand-cyan-400: oklch(71% 0.079 220); + --brand-cyan-500: oklch(60% 0.109 220); + --brand-cyan-600: oklch(51% 0.0927 220); + --brand-cyan-700: oklch(43% 0.0781 220); + --brand-cyan-800: oklch(35% 0.0636 220); + --brand-cyan-900: oklch(27% 0.0491 220); + --brand-cyan-950: oklch(19% 0.0345 220); + /* blue */ + --brand-blue-50: oklch(98% 0.007 255); + --brand-blue-100: oklch(95% 0.0175 255); + --brand-blue-200: oklch(89% 0.0385 255); + --brand-blue-300: oklch(81% 0.0665 255); + --brand-blue-400: oklch(71% 0.1015 255); + --brand-blue-500: oklch(60% 0.14 255); + --brand-blue-600: oklch(51% 0.119 255); + --brand-blue-700: oklch(43% 0.1003 255); + --brand-blue-800: oklch(35% 0.0817 255); + --brand-blue-900: oklch(27% 0.063 255); + --brand-blue-950: oklch(19% 0.0443 255); + /* violet */ + --brand-violet-50: oklch(98% 0.007 295); + --brand-violet-100: oklch(95% 0.0175 295); + --brand-violet-200: oklch(89% 0.0385 295); + --brand-violet-300: oklch(81% 0.0665 295); + --brand-violet-400: oklch(71% 0.1015 295); + --brand-violet-500: oklch(60% 0.14 295); + --brand-violet-600: oklch(51% 0.119 295); + --brand-violet-700: oklch(43% 0.1003 295); + --brand-violet-800: oklch(35% 0.0817 295); + --brand-violet-900: oklch(27% 0.063 295); + --brand-violet-950: oklch(19% 0.0443 295); + /* indigo */ + --brand-indigo-50: oklch(98% 0.007 260); + --brand-indigo-100: oklch(95% 0.0175 260); + --brand-indigo-200: oklch(89% 0.0385 260); + --brand-indigo-300: oklch(81% 0.0665 260); + --brand-indigo-400: oklch(71% 0.1015 260); + --brand-indigo-500: oklch(60% 0.14 260); + --brand-indigo-600: oklch(51% 0.119 260); + --brand-indigo-700: oklch(43% 0.1003 260); + --brand-indigo-800: oklch(35% 0.0817 260); + --brand-indigo-900: oklch(27% 0.063 260); + --brand-indigo-950: oklch(19% 0.0443 260); + /* fuchsia */ + --brand-fuchsia-50: oklch(98% 0.007 335); + --brand-fuchsia-100: oklch(95% 0.0175 335); + --brand-fuchsia-200: oklch(89% 0.0385 335); + --brand-fuchsia-300: oklch(81% 0.0665 335); + --brand-fuchsia-400: oklch(71% 0.1015 335); + --brand-fuchsia-500: oklch(60% 0.14 335); + --brand-fuchsia-600: oklch(51% 0.119 335); + --brand-fuchsia-700: oklch(43% 0.1003 335); + --brand-fuchsia-800: oklch(35% 0.0817 335); + --brand-fuchsia-900: oklch(27% 0.063 335); + --brand-fuchsia-950: oklch(19% 0.0443 335); + /* purple */ + --brand-purple-50: oklch(98% 0.007 300); + --brand-purple-100: oklch(95% 0.0175 300); + --brand-purple-200: oklch(89% 0.0385 300); + --brand-purple-300: oklch(81% 0.0665 300); + --brand-purple-400: oklch(71% 0.1015 300); + --brand-purple-500: oklch(60% 0.14 300); + --brand-purple-600: oklch(51% 0.119 300); + --brand-purple-700: oklch(43% 0.1003 300); + --brand-purple-800: oklch(35% 0.0817 300); + --brand-purple-900: oklch(27% 0.063 300); + --brand-purple-950: oklch(19% 0.0443 300); + /* coral */ + --brand-coral-50: oklch(98% 0.007 375); + --brand-coral-100: oklch(95% 0.0175 375); + --brand-coral-200: oklch(89% 0.0385 375); + --brand-coral-300: oklch(81% 0.0665 375); + --brand-coral-400: oklch(71% 0.1015 375); + --brand-coral-500: oklch(60% 0.14 375); + --brand-coral-600: oklch(51% 0.119 375); + --brand-coral-700: oklch(43% 0.1003 375); + --brand-coral-800: oklch(35% 0.0817 375); + --brand-coral-900: oklch(27% 0.063 375); + --brand-coral-950: oklch(19% 0.0443 375); + /* pink */ + --brand-pink-50: oklch(98% 0.007 340); + --brand-pink-100: oklch(95% 0.0175 340); + --brand-pink-200: oklch(89% 0.0385 340); + --brand-pink-300: oklch(81% 0.0665 340); + --brand-pink-400: oklch(71% 0.1015 340); + --brand-pink-500: oklch(60% 0.14 340); + --brand-pink-600: oklch(51% 0.119 340); + --brand-pink-700: oklch(43% 0.1003 340); + --brand-pink-800: oklch(35% 0.0817 340); + --brand-pink-900: oklch(27% 0.063 340); + --brand-pink-950: oklch(19% 0.0443 340); + /* scarlet */ + --brand-scarlet-50: oklch(98% 0.007 380); + --brand-scarlet-100: oklch(95% 0.0175 380); + --brand-scarlet-200: oklch(89% 0.0385 380); + --brand-scarlet-300: oklch(81% 0.0665 380); + --brand-scarlet-400: oklch(71% 0.1015 380); + --brand-scarlet-500: oklch(60% 0.14 380); + --brand-scarlet-600: oklch(51% 0.119 380); + --brand-scarlet-700: oklch(43% 0.1003 380); + --brand-scarlet-800: oklch(35% 0.0817 380); + --brand-scarlet-900: oklch(27% 0.063 380); + --brand-scarlet-950: oklch(19% 0.0443 380); + /* orange */ + --brand-orange-50: oklch(98% 0.007 415); + --brand-orange-100: oklch(95% 0.0175 415); + --brand-orange-200: oklch(89% 0.0385 415); + --brand-orange-300: oklch(81% 0.0665 415); + --brand-orange-400: oklch(71% 0.1015 415); + --brand-orange-500: oklch(60% 0.14 415); + --brand-orange-600: oklch(51% 0.119 415); + --brand-orange-700: oklch(43% 0.1003 415); + --brand-orange-800: oklch(35% 0.0817 415); + --brand-orange-900: oklch(27% 0.063 415); + --brand-orange-950: oklch(19% 0.0443 415); + /* gold */ + --brand-gold-50: oklch(98% 0.007 420); + --brand-gold-100: oklch(95% 0.0175 420); + --brand-gold-200: oklch(89% 0.0385 420); + --brand-gold-300: oklch(81% 0.0665 420); + --brand-gold-400: oklch(71% 0.1015 420); + --brand-gold-500: oklch(60% 0.14 420); + --brand-gold-600: oklch(51% 0.119 420); + --brand-gold-700: oklch(43% 0.1003 420); + --brand-gold-800: oklch(35% 0.0817 420); + --brand-gold-900: oklch(27% 0.063 420); + --brand-gold-950: oklch(19% 0.0443 420); + /* lime */ + --brand-lime-50: oklch(98% 0.0062 455); + --brand-lime-100: oklch(95% 0.0154 455); + --brand-lime-200: oklch(89% 0.0338 455); + --brand-lime-300: oklch(81% 0.0584 455); + --brand-lime-400: oklch(71% 0.0892 455); + --brand-lime-500: oklch(60% 0.123 455); + --brand-lime-600: oklch(51% 0.1046 455); + --brand-lime-700: oklch(43% 0.0882 455); + --brand-lime-800: oklch(35% 0.0718 455); + --brand-lime-900: oklch(27% 0.0554 455); + --brand-lime-950: oklch(19% 0.039 455); +} diff --git a/src/shared/design-system/tokens/brand.test.ts b/src/shared/design-system/tokens/brand.test.ts new file mode 100644 index 0000000000..fef1132bcd --- /dev/null +++ b/src/shared/design-system/tokens/brand.test.ts @@ -0,0 +1,33 @@ +import { readFileSync } from "node:fs"; +import { expect, it } from "vitest"; +import { BRAND_FAMILIES, BRAND_GRADIENTS, BRAND_SHADES } from "./brand"; + +it("preserves the gradient endpoints inside complete descending shade ranges", () => { + const css = readFileSync( + new URL("../styles/tokens.css", import.meta.url), + "utf8", + ); + for (const family of BRAND_FAMILIES) { + let previousLightness = 100; + for (const shade of BRAND_SHADES) { + const match = css.match( + new RegExp( + `--brand-${family.id}-${shade}: oklch\\(([\\d.]+)% ([\\d.]+) ([\\d.]+)\\)`, + ), + ); + expect(match, `${family.id} ${shade}`).not.toBeNull(); + if (!match) throw new Error(`Missing ${family.id} ${shade}`); + const [, lightness, chroma, hue] = match; + expect(Number(lightness)).toBeLessThan(previousLightness); + previousLightness = Number(lightness); + expect(Number(hue)).toBe(family.hue); + if (shade === 500) { + expect(Number(lightness)).toBe(60); + expect(Number(chroma)).toBe(family.chroma); + } + } + } + expect( + new Set(BRAND_GRADIENTS.flatMap(({ from, to }) => [from, to])), + ).toEqual(new Set(BRAND_FAMILIES.map(({ id }) => id))); +}); diff --git a/src/shared/design-system/tokens/brand.ts b/src/shared/design-system/tokens/brand.ts new file mode 100644 index 0000000000..83534eb141 --- /dev/null +++ b/src/shared/design-system/tokens/brand.ts @@ -0,0 +1,31 @@ +/** Decorative artwork palette. Fixed scales, not theme-aware semantic roles. + * Source: plugin branding Settings.module.css, revision 834b8ea4b. + * Each 500 stop preserves one exact OKLCH gradient endpoint, including its hue. + * Other stops vary lightness and taper chroma within sRGB; review before adoption. + */ +export const BRAND_SHADES = [ + 50, 100, 200, 300, 400, 500, 600, 700, 800, 900, 950, +] as const; +export const BRAND_FAMILIES = [ + { id: "cyan", hue: 220, chroma: 0.109 }, + { id: "blue", hue: 255, chroma: 0.14 }, + { id: "violet", hue: 295, chroma: 0.14 }, + { id: "indigo", hue: 260, chroma: 0.14 }, + { id: "fuchsia", hue: 335, chroma: 0.14 }, + { id: "purple", hue: 300, chroma: 0.14 }, + { id: "coral", hue: 375, chroma: 0.14 }, + { id: "pink", hue: 340, chroma: 0.14 }, + { id: "scarlet", hue: 380, chroma: 0.14 }, + { id: "orange", hue: 415, chroma: 0.14 }, + { id: "gold", hue: 420, chroma: 0.14 }, + { id: "lime", hue: 455, chroma: 0.123 }, +] as const; + +export const BRAND_GRADIENTS = [ + { name: "Communication", from: "cyan", to: "blue" }, + { name: "Agents", from: "violet", to: "indigo" }, + { name: "Agent tools", from: "fuchsia", to: "purple" }, + { name: "Developer tools & integrations", from: "coral", to: "pink" }, + { name: "People & communities", from: "scarlet", to: "orange" }, + { name: "Productivity", from: "gold", to: "lime" }, +] as const; diff --git a/tests/fixtures/design-system/styles.css b/tests/fixtures/design-system/styles.css index fe58714884..b8b811f37f 100644 --- a/tests/fixtures/design-system/styles.css +++ b/tests/fixtures/design-system/styles.css @@ -758,3 +758,32 @@ .component-specimen-group > h2 { scroll-margin-block-start: var(--space-8); } + +/* Viewer-only palette layout; shared Button retains interaction and focus. */ +.brand-gradient-grid { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(min(100%, 9rem), 1fr)); + gap: var(--space-4); +} + +.brand-color-selection { + display: flex; + align-items: center; + flex-wrap: wrap; + gap: var(--space-3); +} + +.brand-color-ramp { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(3rem, 1fr)); + gap: var(--space-1); +} + +.brand-color-ramp .buzz-button { + height: auto; + padding: var(--space-1); +} + +.brand-color-ramp .buzz-button-label { + width: 100%; +} diff --git a/tests/fixtures/design-system/ui/BrandColors.test.ts b/tests/fixtures/design-system/ui/BrandColors.test.ts new file mode 100644 index 0000000000..8f7e177ad1 --- /dev/null +++ b/tests/fixtures/design-system/ui/BrandColors.test.ts @@ -0,0 +1,44 @@ +// @vitest-environment jsdom +import { createElement } from "react"; +import { afterEach, expect, it, vi } from "vitest"; +import { cleanup, fireEvent, render, screen } from "@testing-library/react"; +import "@testing-library/jest-dom/vitest"; +import { BrandColors } from "./BrandColors"; + +const clipboardDescriptor = Object.getOwnPropertyDescriptor( + navigator, + "clipboard", +); +afterEach(() => { + cleanup(); + if (clipboardDescriptor) + Object.defineProperty(navigator, "clipboard", clipboardDescriptor); + else Reflect.deleteProperty(navigator, "clipboard"); +}); + +it("selects a shade and copies its CSS reference, with a manual fallback", async () => { + const writeText = vi.fn().mockResolvedValue(undefined); + Object.defineProperty(navigator, "clipboard", { + configurable: true, + value: { writeText }, + }); + render(createElement(BrandColors)); + fireEvent.click(screen.getByRole("button", { name: "violet 700" })); + expect(screen.getByRole("button", { name: "cyan 500" })).toHaveAttribute( + "aria-pressed", + "false", + ); + expect(screen.getByRole("button", { name: "violet 700" })).toHaveAttribute( + "aria-pressed", + "true", + ); + fireEvent.click(screen.getByRole("button", { name: "Copy CSS" })); + expect(await screen.findByText("Copied CSS variable.")).toBeInTheDocument(); + expect(writeText).toHaveBeenCalledWith("var(--brand-violet-700)"); + fireEvent.click(screen.getByRole("button", { name: "lime 950" })); + expect(screen.getByRole("status")).toBeEmptyDOMElement(); + writeText.mockRejectedValueOnce(new Error("denied")); + fireEvent.click(screen.getByRole("button", { name: "Copy CSS" })); + expect(await screen.findByText(/Could not copy/)).toBeInTheDocument(); + expect(screen.getByText("var(--brand-lime-950)")).toBeInTheDocument(); +}); diff --git a/tests/fixtures/design-system/ui/BrandColors.tsx b/tests/fixtures/design-system/ui/BrandColors.tsx new file mode 100644 index 0000000000..11e6972d2e --- /dev/null +++ b/tests/fixtures/design-system/ui/BrandColors.tsx @@ -0,0 +1,117 @@ +import { useState } from "react"; +import { + BRAND_FAMILIES, + BRAND_GRADIENTS, + BRAND_SHADES, +} from "../../../../src/shared/design-system/tokens/brand"; +import { Button } from "../../../../src/shared/design-system/ui/Button"; +import { useResolvedTokens } from "../useResolvedToken"; +import { Section } from "./primitives"; + +const variables = BRAND_FAMILIES.flatMap((family) => + BRAND_SHADES.map((shade) => `--brand-${family.id}-${shade}`), +); + +export function BrandColors() { + const [selected, setSelected] = useState("--brand-cyan-500"); + const [copyStatus, setCopyStatus] = useState(""); + const resolved = useResolvedTokens(variables); + const value = resolved.get(selected)?.value ?? ""; + + async function copy() { + try { + await navigator.clipboard.writeText(`var(${selected})`); + setCopyStatus("Copied CSS variable."); + } catch { + setCopyStatus( + "Could not copy. Select the CSS variable below to copy it manually.", + ); + } + } + + return ( +
+
+ {BRAND_GRADIENTS.map((gradient) => ( +
+
+ {gradient.name} + + {gradient.from} → {gradient.to} + +
+ ))} +
+

+ Original gradient stops, before the icon lighting and artwork overlays. + Family names are exploratory; nearby hues remain separate to preserve + every source color. +

+
+
+

+ {copyStatus} +

+
+ {BRAND_FAMILIES.map((family) => ( +
+

{family.id}

+
+ {BRAND_SHADES.map((shade) => { + const variable = `--brand-${family.id}-${shade}`; + return ( + + ); + })} +
+
+ ))} +
+
+ ); +} diff --git a/tests/fixtures/design-system/ui/ColorPage.tsx b/tests/fixtures/design-system/ui/ColorPage.tsx index 1bd2594ce9..b19dac91f2 100644 --- a/tests/fixtures/design-system/ui/ColorPage.tsx +++ b/tests/fixtures/design-system/ui/ColorPage.tsx @@ -9,6 +9,8 @@ import { type Role, } from "../../../../src/shared/design-system/tokens/registry"; +import { BrandColors } from "./BrandColors"; + import { Note, PageHeader, Section, Swatch } from "./primitives"; /** @@ -84,6 +86,8 @@ export function ColorPage() { intro="Choose colors by their purpose: surface, text, border, or action. Shared roles connect those purposes to the palette in light and dark mode. The examples below come from the token registry." /> + +
Date: Sat, 17 Oct 2026 22:06:32 -0700 Subject: [PATCH 02/20] refactor(design-system): align artwork studies with buzz palette Co-authored-by: Carl Signed-off-by: Taylor Ho --- docs/design-system.md | 8 +- src/shared/design-system/styles/tokens.css | 280 +++++++++--------- src/shared/design-system/tokens/brand.test.ts | 2 +- src/shared/design-system/tokens/brand.ts | 6 +- .../design-system/ui/BrandColors.test.ts | 12 +- .../fixtures/design-system/ui/BrandColors.tsx | 8 +- tests/fixtures/design-system/ui/ColorPage.tsx | 4 +- 7 files changed, 166 insertions(+), 154 deletions(-) diff --git a/docs/design-system.md b/docs/design-system.md index 8072db98da..10dec0b23c 100644 --- a/docs/design-system.md +++ b/docs/design-system.md @@ -427,11 +427,13 @@ existing minimum heights. The extra-small capsule keeps its caption role. ## Plugin color-family exploration -The Color viewer includes twelve fixed OKLCH families with 50–950 stops. -`--brand--500` preserves each exact gradient endpoint from plugin branding +The Color viewer includes twelve fixed OKLCH artwork families with twelve steps, following Buzz’s existing +palette vocabulary. The established palette remains first in the viewer and its +values and semantic mappings are unchanged. +`--brand--9` preserves each exact gradient endpoint from plugin branding (`src/app/Settings.module.css` on `tho/plugin-icons`, revision `834b8ea4b`). The six original gradient pairs are recorded in `tokens/brand.ts`; the values -live in `styles/tokens.css`. Lightness descends from 98% to 19%; chroma tapers +live in `styles/tokens.css`. Lightness descends from 99% to 30%; chroma tapers and is capped to sRGB outside the exact source stops. These are exploratory artwork colors, independent of theme, not new semantic roles or appearance choices. Select a swatch to inspect its actual CSS value and copy its variable. Product diff --git a/src/shared/design-system/styles/tokens.css b/src/shared/design-system/styles/tokens.css index 34b47ecc92..de872398dd 100644 --- a/src/shared/design-system/styles/tokens.css +++ b/src/shared/design-system/styles/tokens.css @@ -1614,152 +1614,164 @@ --spacing-chip-gap: var(--space-chip-gap); } -/* Plugin artwork families: fixed light-to-dark scales, independent of mode. - 500 preserves the exact source endpoint; other stops reduce chroma to sRGB. +/* Plugin artwork families: twelve fixed steps, independent of mode. + Step 9 preserves the exact source endpoint; other stops taper chroma to sRGB. Decorative palette only; product controls continue to use semantic roles. */ :root { /* cyan */ - --brand-cyan-50: oklch(98% 0.0055 220); - --brand-cyan-100: oklch(95% 0.0136 220); - --brand-cyan-200: oklch(89% 0.03 220); - --brand-cyan-300: oklch(81% 0.0518 220); - --brand-cyan-400: oklch(71% 0.079 220); - --brand-cyan-500: oklch(60% 0.109 220); - --brand-cyan-600: oklch(51% 0.0927 220); - --brand-cyan-700: oklch(43% 0.0781 220); - --brand-cyan-800: oklch(35% 0.0636 220); - --brand-cyan-900: oklch(27% 0.0491 220); - --brand-cyan-950: oklch(19% 0.0345 220); + --brand-cyan-1: oklch(99% 0.0027 220); + --brand-cyan-2: oklch(97% 0.0082 220); + --brand-cyan-3: oklch(94% 0.0164 220); + --brand-cyan-4: oklch(90% 0.0272 220); + --brand-cyan-5: oklch(84% 0.0436 220); + --brand-cyan-6: oklch(76% 0.0654 220); + --brand-cyan-7: oklch(68% 0.0872 220); + --brand-cyan-8: oklch(64% 0.0981 220); + --brand-cyan-9: oklch(60% 0.109 220); + --brand-cyan-10: oklch(55% 0.0999 220); + --brand-cyan-11: oklch(45% 0.0818 220); + --brand-cyan-12: oklch(30% 0.0545 220); /* blue */ - --brand-blue-50: oklch(98% 0.007 255); - --brand-blue-100: oklch(95% 0.0175 255); - --brand-blue-200: oklch(89% 0.0385 255); - --brand-blue-300: oklch(81% 0.0665 255); - --brand-blue-400: oklch(71% 0.1015 255); - --brand-blue-500: oklch(60% 0.14 255); - --brand-blue-600: oklch(51% 0.119 255); - --brand-blue-700: oklch(43% 0.1003 255); - --brand-blue-800: oklch(35% 0.0817 255); - --brand-blue-900: oklch(27% 0.063 255); - --brand-blue-950: oklch(19% 0.0443 255); + --brand-blue-1: oklch(99% 0.0035 255); + --brand-blue-2: oklch(97% 0.0105 255); + --brand-blue-3: oklch(94% 0.021 255); + --brand-blue-4: oklch(90% 0.035 255); + --brand-blue-5: oklch(84% 0.056 255); + --brand-blue-6: oklch(76% 0.084 255); + --brand-blue-7: oklch(68% 0.112 255); + --brand-blue-8: oklch(64% 0.126 255); + --brand-blue-9: oklch(60% 0.14 255); + --brand-blue-10: oklch(55% 0.1283 255); + --brand-blue-11: oklch(45% 0.105 255); + --brand-blue-12: oklch(30% 0.07 255); /* violet */ - --brand-violet-50: oklch(98% 0.007 295); - --brand-violet-100: oklch(95% 0.0175 295); - --brand-violet-200: oklch(89% 0.0385 295); - --brand-violet-300: oklch(81% 0.0665 295); - --brand-violet-400: oklch(71% 0.1015 295); - --brand-violet-500: oklch(60% 0.14 295); - --brand-violet-600: oklch(51% 0.119 295); - --brand-violet-700: oklch(43% 0.1003 295); - --brand-violet-800: oklch(35% 0.0817 295); - --brand-violet-900: oklch(27% 0.063 295); - --brand-violet-950: oklch(19% 0.0443 295); + --brand-violet-1: oklch(99% 0.0035 295); + --brand-violet-2: oklch(97% 0.0105 295); + --brand-violet-3: oklch(94% 0.021 295); + --brand-violet-4: oklch(90% 0.035 295); + --brand-violet-5: oklch(84% 0.056 295); + --brand-violet-6: oklch(76% 0.084 295); + --brand-violet-7: oklch(68% 0.112 295); + --brand-violet-8: oklch(64% 0.126 295); + --brand-violet-9: oklch(60% 0.14 295); + --brand-violet-10: oklch(55% 0.1283 295); + --brand-violet-11: oklch(45% 0.105 295); + --brand-violet-12: oklch(30% 0.07 295); /* indigo */ - --brand-indigo-50: oklch(98% 0.007 260); - --brand-indigo-100: oklch(95% 0.0175 260); - --brand-indigo-200: oklch(89% 0.0385 260); - --brand-indigo-300: oklch(81% 0.0665 260); - --brand-indigo-400: oklch(71% 0.1015 260); - --brand-indigo-500: oklch(60% 0.14 260); - --brand-indigo-600: oklch(51% 0.119 260); - --brand-indigo-700: oklch(43% 0.1003 260); - --brand-indigo-800: oklch(35% 0.0817 260); - --brand-indigo-900: oklch(27% 0.063 260); - --brand-indigo-950: oklch(19% 0.0443 260); + --brand-indigo-1: oklch(99% 0.0035 260); + --brand-indigo-2: oklch(97% 0.0105 260); + --brand-indigo-3: oklch(94% 0.021 260); + --brand-indigo-4: oklch(90% 0.035 260); + --brand-indigo-5: oklch(84% 0.056 260); + --brand-indigo-6: oklch(76% 0.084 260); + --brand-indigo-7: oklch(68% 0.112 260); + --brand-indigo-8: oklch(64% 0.126 260); + --brand-indigo-9: oklch(60% 0.14 260); + --brand-indigo-10: oklch(55% 0.1283 260); + --brand-indigo-11: oklch(45% 0.105 260); + --brand-indigo-12: oklch(30% 0.07 260); /* fuchsia */ - --brand-fuchsia-50: oklch(98% 0.007 335); - --brand-fuchsia-100: oklch(95% 0.0175 335); - --brand-fuchsia-200: oklch(89% 0.0385 335); - --brand-fuchsia-300: oklch(81% 0.0665 335); - --brand-fuchsia-400: oklch(71% 0.1015 335); - --brand-fuchsia-500: oklch(60% 0.14 335); - --brand-fuchsia-600: oklch(51% 0.119 335); - --brand-fuchsia-700: oklch(43% 0.1003 335); - --brand-fuchsia-800: oklch(35% 0.0817 335); - --brand-fuchsia-900: oklch(27% 0.063 335); - --brand-fuchsia-950: oklch(19% 0.0443 335); + --brand-fuchsia-1: oklch(99% 0.0035 335); + --brand-fuchsia-2: oklch(97% 0.0105 335); + --brand-fuchsia-3: oklch(94% 0.021 335); + --brand-fuchsia-4: oklch(90% 0.035 335); + --brand-fuchsia-5: oklch(84% 0.056 335); + --brand-fuchsia-6: oklch(76% 0.084 335); + --brand-fuchsia-7: oklch(68% 0.112 335); + --brand-fuchsia-8: oklch(64% 0.126 335); + --brand-fuchsia-9: oklch(60% 0.14 335); + --brand-fuchsia-10: oklch(55% 0.1283 335); + --brand-fuchsia-11: oklch(45% 0.105 335); + --brand-fuchsia-12: oklch(30% 0.07 335); /* purple */ - --brand-purple-50: oklch(98% 0.007 300); - --brand-purple-100: oklch(95% 0.0175 300); - --brand-purple-200: oklch(89% 0.0385 300); - --brand-purple-300: oklch(81% 0.0665 300); - --brand-purple-400: oklch(71% 0.1015 300); - --brand-purple-500: oklch(60% 0.14 300); - --brand-purple-600: oklch(51% 0.119 300); - --brand-purple-700: oklch(43% 0.1003 300); - --brand-purple-800: oklch(35% 0.0817 300); - --brand-purple-900: oklch(27% 0.063 300); - --brand-purple-950: oklch(19% 0.0443 300); + --brand-purple-1: oklch(99% 0.0035 300); + --brand-purple-2: oklch(97% 0.0105 300); + --brand-purple-3: oklch(94% 0.021 300); + --brand-purple-4: oklch(90% 0.035 300); + --brand-purple-5: oklch(84% 0.056 300); + --brand-purple-6: oklch(76% 0.084 300); + --brand-purple-7: oklch(68% 0.112 300); + --brand-purple-8: oklch(64% 0.126 300); + --brand-purple-9: oklch(60% 0.14 300); + --brand-purple-10: oklch(55% 0.1283 300); + --brand-purple-11: oklch(45% 0.105 300); + --brand-purple-12: oklch(30% 0.07 300); /* coral */ - --brand-coral-50: oklch(98% 0.007 375); - --brand-coral-100: oklch(95% 0.0175 375); - --brand-coral-200: oklch(89% 0.0385 375); - --brand-coral-300: oklch(81% 0.0665 375); - --brand-coral-400: oklch(71% 0.1015 375); - --brand-coral-500: oklch(60% 0.14 375); - --brand-coral-600: oklch(51% 0.119 375); - --brand-coral-700: oklch(43% 0.1003 375); - --brand-coral-800: oklch(35% 0.0817 375); - --brand-coral-900: oklch(27% 0.063 375); - --brand-coral-950: oklch(19% 0.0443 375); + --brand-coral-1: oklch(99% 0.0035 375); + --brand-coral-2: oklch(97% 0.0105 375); + --brand-coral-3: oklch(94% 0.021 375); + --brand-coral-4: oklch(90% 0.035 375); + --brand-coral-5: oklch(84% 0.056 375); + --brand-coral-6: oklch(76% 0.084 375); + --brand-coral-7: oklch(68% 0.112 375); + --brand-coral-8: oklch(64% 0.126 375); + --brand-coral-9: oklch(60% 0.14 375); + --brand-coral-10: oklch(55% 0.1283 375); + --brand-coral-11: oklch(45% 0.105 375); + --brand-coral-12: oklch(30% 0.07 375); /* pink */ - --brand-pink-50: oklch(98% 0.007 340); - --brand-pink-100: oklch(95% 0.0175 340); - --brand-pink-200: oklch(89% 0.0385 340); - --brand-pink-300: oklch(81% 0.0665 340); - --brand-pink-400: oklch(71% 0.1015 340); - --brand-pink-500: oklch(60% 0.14 340); - --brand-pink-600: oklch(51% 0.119 340); - --brand-pink-700: oklch(43% 0.1003 340); - --brand-pink-800: oklch(35% 0.0817 340); - --brand-pink-900: oklch(27% 0.063 340); - --brand-pink-950: oklch(19% 0.0443 340); + --brand-pink-1: oklch(99% 0.0035 340); + --brand-pink-2: oklch(97% 0.0105 340); + --brand-pink-3: oklch(94% 0.021 340); + --brand-pink-4: oklch(90% 0.035 340); + --brand-pink-5: oklch(84% 0.056 340); + --brand-pink-6: oklch(76% 0.084 340); + --brand-pink-7: oklch(68% 0.112 340); + --brand-pink-8: oklch(64% 0.126 340); + --brand-pink-9: oklch(60% 0.14 340); + --brand-pink-10: oklch(55% 0.1283 340); + --brand-pink-11: oklch(45% 0.105 340); + --brand-pink-12: oklch(30% 0.07 340); /* scarlet */ - --brand-scarlet-50: oklch(98% 0.007 380); - --brand-scarlet-100: oklch(95% 0.0175 380); - --brand-scarlet-200: oklch(89% 0.0385 380); - --brand-scarlet-300: oklch(81% 0.0665 380); - --brand-scarlet-400: oklch(71% 0.1015 380); - --brand-scarlet-500: oklch(60% 0.14 380); - --brand-scarlet-600: oklch(51% 0.119 380); - --brand-scarlet-700: oklch(43% 0.1003 380); - --brand-scarlet-800: oklch(35% 0.0817 380); - --brand-scarlet-900: oklch(27% 0.063 380); - --brand-scarlet-950: oklch(19% 0.0443 380); + --brand-scarlet-1: oklch(99% 0.0035 380); + --brand-scarlet-2: oklch(97% 0.0105 380); + --brand-scarlet-3: oklch(94% 0.021 380); + --brand-scarlet-4: oklch(90% 0.035 380); + --brand-scarlet-5: oklch(84% 0.056 380); + --brand-scarlet-6: oklch(76% 0.084 380); + --brand-scarlet-7: oklch(68% 0.112 380); + --brand-scarlet-8: oklch(64% 0.126 380); + --brand-scarlet-9: oklch(60% 0.14 380); + --brand-scarlet-10: oklch(55% 0.1283 380); + --brand-scarlet-11: oklch(45% 0.105 380); + --brand-scarlet-12: oklch(30% 0.07 380); /* orange */ - --brand-orange-50: oklch(98% 0.007 415); - --brand-orange-100: oklch(95% 0.0175 415); - --brand-orange-200: oklch(89% 0.0385 415); - --brand-orange-300: oklch(81% 0.0665 415); - --brand-orange-400: oklch(71% 0.1015 415); - --brand-orange-500: oklch(60% 0.14 415); - --brand-orange-600: oklch(51% 0.119 415); - --brand-orange-700: oklch(43% 0.1003 415); - --brand-orange-800: oklch(35% 0.0817 415); - --brand-orange-900: oklch(27% 0.063 415); - --brand-orange-950: oklch(19% 0.0443 415); + --brand-orange-1: oklch(99% 0.0035 415); + --brand-orange-2: oklch(97% 0.0105 415); + --brand-orange-3: oklch(94% 0.021 415); + --brand-orange-4: oklch(90% 0.035 415); + --brand-orange-5: oklch(84% 0.056 415); + --brand-orange-6: oklch(76% 0.084 415); + --brand-orange-7: oklch(68% 0.112 415); + --brand-orange-8: oklch(64% 0.126 415); + --brand-orange-9: oklch(60% 0.14 415); + --brand-orange-10: oklch(55% 0.1283 415); + --brand-orange-11: oklch(45% 0.105 415); + --brand-orange-12: oklch(30% 0.07 415); /* gold */ - --brand-gold-50: oklch(98% 0.007 420); - --brand-gold-100: oklch(95% 0.0175 420); - --brand-gold-200: oklch(89% 0.0385 420); - --brand-gold-300: oklch(81% 0.0665 420); - --brand-gold-400: oklch(71% 0.1015 420); - --brand-gold-500: oklch(60% 0.14 420); - --brand-gold-600: oklch(51% 0.119 420); - --brand-gold-700: oklch(43% 0.1003 420); - --brand-gold-800: oklch(35% 0.0817 420); - --brand-gold-900: oklch(27% 0.063 420); - --brand-gold-950: oklch(19% 0.0443 420); + --brand-gold-1: oklch(99% 0.0035 420); + --brand-gold-2: oklch(97% 0.0105 420); + --brand-gold-3: oklch(94% 0.021 420); + --brand-gold-4: oklch(90% 0.035 420); + --brand-gold-5: oklch(84% 0.056 420); + --brand-gold-6: oklch(76% 0.084 420); + --brand-gold-7: oklch(68% 0.112 420); + --brand-gold-8: oklch(64% 0.126 420); + --brand-gold-9: oklch(60% 0.14 420); + --brand-gold-10: oklch(55% 0.1283 420); + --brand-gold-11: oklch(45% 0.105 420); + --brand-gold-12: oklch(30% 0.07 420); /* lime */ - --brand-lime-50: oklch(98% 0.0062 455); - --brand-lime-100: oklch(95% 0.0154 455); - --brand-lime-200: oklch(89% 0.0338 455); - --brand-lime-300: oklch(81% 0.0584 455); - --brand-lime-400: oklch(71% 0.0892 455); - --brand-lime-500: oklch(60% 0.123 455); - --brand-lime-600: oklch(51% 0.1046 455); - --brand-lime-700: oklch(43% 0.0882 455); - --brand-lime-800: oklch(35% 0.0718 455); - --brand-lime-900: oklch(27% 0.0554 455); - --brand-lime-950: oklch(19% 0.039 455); + --brand-lime-1: oklch(99% 0.0031 455); + --brand-lime-2: oklch(97% 0.0092 455); + --brand-lime-3: oklch(94% 0.0185 455); + --brand-lime-4: oklch(90% 0.0307 455); + --brand-lime-5: oklch(84% 0.0492 455); + --brand-lime-6: oklch(76% 0.0738 455); + --brand-lime-7: oklch(68% 0.0984 455); + --brand-lime-8: oklch(64% 0.1107 455); + --brand-lime-9: oklch(60% 0.123 455); + --brand-lime-10: oklch(55% 0.1128 455); + --brand-lime-11: oklch(45% 0.0922 455); + --brand-lime-12: oklch(30% 0.0615 455); } diff --git a/src/shared/design-system/tokens/brand.test.ts b/src/shared/design-system/tokens/brand.test.ts index fef1132bcd..c217836168 100644 --- a/src/shared/design-system/tokens/brand.test.ts +++ b/src/shared/design-system/tokens/brand.test.ts @@ -21,7 +21,7 @@ it("preserves the gradient endpoints inside complete descending shade ranges", ( expect(Number(lightness)).toBeLessThan(previousLightness); previousLightness = Number(lightness); expect(Number(hue)).toBe(family.hue); - if (shade === 500) { + if (shade === 9) { expect(Number(lightness)).toBe(60); expect(Number(chroma)).toBe(family.chroma); } diff --git a/src/shared/design-system/tokens/brand.ts b/src/shared/design-system/tokens/brand.ts index 83534eb141..3d2df05149 100644 --- a/src/shared/design-system/tokens/brand.ts +++ b/src/shared/design-system/tokens/brand.ts @@ -1,11 +1,9 @@ /** Decorative artwork palette. Fixed scales, not theme-aware semantic roles. * Source: plugin branding Settings.module.css, revision 834b8ea4b. - * Each 500 stop preserves one exact OKLCH gradient endpoint, including its hue. + * Each step 9 preserves one exact OKLCH gradient endpoint, including its hue. * Other stops vary lightness and taper chroma within sRGB; review before adoption. */ -export const BRAND_SHADES = [ - 50, 100, 200, 300, 400, 500, 600, 700, 800, 900, 950, -] as const; +export const BRAND_SHADES = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12] as const; export const BRAND_FAMILIES = [ { id: "cyan", hue: 220, chroma: 0.109 }, { id: "blue", hue: 255, chroma: 0.14 }, diff --git a/tests/fixtures/design-system/ui/BrandColors.test.ts b/tests/fixtures/design-system/ui/BrandColors.test.ts index 8f7e177ad1..c0b0454c03 100644 --- a/tests/fixtures/design-system/ui/BrandColors.test.ts +++ b/tests/fixtures/design-system/ui/BrandColors.test.ts @@ -23,22 +23,22 @@ it("selects a shade and copies its CSS reference, with a manual fallback", async value: { writeText }, }); render(createElement(BrandColors)); - fireEvent.click(screen.getByRole("button", { name: "violet 700" })); - expect(screen.getByRole("button", { name: "cyan 500" })).toHaveAttribute( + fireEvent.click(screen.getByRole("button", { name: "violet 11" })); + expect(screen.getByRole("button", { name: "cyan 9" })).toHaveAttribute( "aria-pressed", "false", ); - expect(screen.getByRole("button", { name: "violet 700" })).toHaveAttribute( + expect(screen.getByRole("button", { name: "violet 11" })).toHaveAttribute( "aria-pressed", "true", ); fireEvent.click(screen.getByRole("button", { name: "Copy CSS" })); expect(await screen.findByText("Copied CSS variable.")).toBeInTheDocument(); - expect(writeText).toHaveBeenCalledWith("var(--brand-violet-700)"); - fireEvent.click(screen.getByRole("button", { name: "lime 950" })); + expect(writeText).toHaveBeenCalledWith("var(--brand-violet-11)"); + fireEvent.click(screen.getByRole("button", { name: "lime 12" })); expect(screen.getByRole("status")).toBeEmptyDOMElement(); writeText.mockRejectedValueOnce(new Error("denied")); fireEvent.click(screen.getByRole("button", { name: "Copy CSS" })); expect(await screen.findByText(/Could not copy/)).toBeInTheDocument(); - expect(screen.getByText("var(--brand-lime-950)")).toBeInTheDocument(); + expect(screen.getByText("var(--brand-lime-12)")).toBeInTheDocument(); }); diff --git a/tests/fixtures/design-system/ui/BrandColors.tsx b/tests/fixtures/design-system/ui/BrandColors.tsx index 11e6972d2e..b06a59611c 100644 --- a/tests/fixtures/design-system/ui/BrandColors.tsx +++ b/tests/fixtures/design-system/ui/BrandColors.tsx @@ -13,7 +13,7 @@ const variables = BRAND_FAMILIES.flatMap((family) => ); export function BrandColors() { - const [selected, setSelected] = useState("--brand-cyan-500"); + const [selected, setSelected] = useState("--brand-cyan-9"); const [copyStatus, setCopyStatus] = useState(""); const resolved = useResolvedTokens(variables); const value = resolved.get(selected)?.value ?? ""; @@ -31,8 +31,8 @@ export function BrandColors() { return (
{BRAND_GRADIENTS.map((gradient) => ( @@ -43,7 +43,7 @@ export function BrandColors() {
{gradient.name} diff --git a/tests/fixtures/design-system/ui/ColorPage.tsx b/tests/fixtures/design-system/ui/ColorPage.tsx index b19dac91f2..33137d52bc 100644 --- a/tests/fixtures/design-system/ui/ColorPage.tsx +++ b/tests/fixtures/design-system/ui/ColorPage.tsx @@ -86,8 +86,6 @@ export function ColorPage() { intro="Choose colors by their purpose: surface, text, border, or action. Shared roles connect those purposes to the palette in light and dark mode. The examples below come from the token registry." /> - -
+ +
Date: Mon, 19 Oct 2026 08:15:11 -0700 Subject: [PATCH 03/20] refactor(design-system): align plugin gradients with accent colorways Co-authored-by: Carl Signed-off-by: Taylor Ho --- docs/design-system.md | 29 +-- src/shared/design-system/styles/tokens.css | 205 ++++-------------- src/shared/design-system/tokens/brand.test.ts | 74 +++++-- src/shared/design-system/tokens/brand.ts | 45 ++-- .../design-system/ui/BrandColors.test.ts | 23 +- .../fixtures/design-system/ui/BrandColors.tsx | 61 ++++-- .../design-system/ui/ColorTablePage.tsx | 13 +- .../design-system/useResolvedToken.ts | 6 +- 8 files changed, 200 insertions(+), 256 deletions(-) diff --git a/docs/design-system.md b/docs/design-system.md index 10dec0b23c..301f2d5cb7 100644 --- a/docs/design-system.md +++ b/docs/design-system.md @@ -425,17 +425,18 @@ icons, subtitles, disabled states, and required-status text. Standard Button sizes use the 14px `text-label-sm` role while retaining their existing minimum heights. The extra-small capsule keeps its caption role. -## Plugin color-family exploration - -The Color viewer includes twelve fixed OKLCH artwork families with twelve steps, following Buzz’s existing -palette vocabulary. The established palette remains first in the viewer and its -values and semantic mappings are unchanged. -`--brand--9` preserves each exact gradient endpoint from plugin branding -(`src/app/Settings.module.css` on `tho/plugin-icons`, revision `834b8ea4b`). -The six original gradient pairs are recorded in `tokens/brand.ts`; the values -live in `styles/tokens.css`. Lightness descends from 99% to 30%; chroma tapers -and is capped to sRGB outside the exact source stops. These are exploratory -artwork colors, independent of theme, not new semantic roles or appearance choices. -Select a swatch to inspect its actual CSS value and copy its variable. Product -controls continue to consume semantic roles; the plugin branch is not a dependency -of this viewer and no plugin artwork is imported. +## Colorways and plugin gradients + +The Color viewer reuses the six existing accent ramps, including their authored +light/dark values. No parallel family palette or saved-preference migration is needed. +Category mapping: Communication → Cobalt, Agents → Iris, Agent tools → Magenta, +Developer tools → Flame, People & communities → Jade, Productivity → Chartreuse. + +Shared `--art--gradient` tokens supply decorative plugin backgrounds. +Their hue centers come from the existing light-mode accent step 9, with an +8-degree sweep either side. Endpoints target relative luminance 0.18, with chroma +capped to sRGB. This equalizes bare-background contrast (white about 4.57:1, +black about 4.60:1), rather than assuming equal OKLCH lightness means equal +contrast. These backgrounds are independent of theme; icon lighting remains +host-owned and must be checked separately. The existing semantic roles and +Appearance choices retain their values. diff --git a/src/shared/design-system/styles/tokens.css b/src/shared/design-system/styles/tokens.css index de872398dd..c2ef5b9405 100644 --- a/src/shared/design-system/styles/tokens.css +++ b/src/shared/design-system/styles/tokens.css @@ -1614,164 +1614,51 @@ --spacing-chip-gap: var(--space-chip-gap); } -/* Plugin artwork families: twelve fixed steps, independent of mode. - Step 9 preserves the exact source endpoint; other stops taper chroma to sRGB. - Decorative palette only; product controls continue to use semantic roles. */ +/* Plugin gradients follow the six existing accent identities. + Bare endpoints target relative luminance 0.18 (white 4.57:1, black 4.60:1). + Hue comes from each family's existing light step 9; a +/-8 degree sweep + keeps adjacent cobalt/iris distinct. Artwork lighting changes final pixels. */ :root { - /* cyan */ - --brand-cyan-1: oklch(99% 0.0027 220); - --brand-cyan-2: oklch(97% 0.0082 220); - --brand-cyan-3: oklch(94% 0.0164 220); - --brand-cyan-4: oklch(90% 0.0272 220); - --brand-cyan-5: oklch(84% 0.0436 220); - --brand-cyan-6: oklch(76% 0.0654 220); - --brand-cyan-7: oklch(68% 0.0872 220); - --brand-cyan-8: oklch(64% 0.0981 220); - --brand-cyan-9: oklch(60% 0.109 220); - --brand-cyan-10: oklch(55% 0.0999 220); - --brand-cyan-11: oklch(45% 0.0818 220); - --brand-cyan-12: oklch(30% 0.0545 220); - /* blue */ - --brand-blue-1: oklch(99% 0.0035 255); - --brand-blue-2: oklch(97% 0.0105 255); - --brand-blue-3: oklch(94% 0.021 255); - --brand-blue-4: oklch(90% 0.035 255); - --brand-blue-5: oklch(84% 0.056 255); - --brand-blue-6: oklch(76% 0.084 255); - --brand-blue-7: oklch(68% 0.112 255); - --brand-blue-8: oklch(64% 0.126 255); - --brand-blue-9: oklch(60% 0.14 255); - --brand-blue-10: oklch(55% 0.1283 255); - --brand-blue-11: oklch(45% 0.105 255); - --brand-blue-12: oklch(30% 0.07 255); - /* violet */ - --brand-violet-1: oklch(99% 0.0035 295); - --brand-violet-2: oklch(97% 0.0105 295); - --brand-violet-3: oklch(94% 0.021 295); - --brand-violet-4: oklch(90% 0.035 295); - --brand-violet-5: oklch(84% 0.056 295); - --brand-violet-6: oklch(76% 0.084 295); - --brand-violet-7: oklch(68% 0.112 295); - --brand-violet-8: oklch(64% 0.126 295); - --brand-violet-9: oklch(60% 0.14 295); - --brand-violet-10: oklch(55% 0.1283 295); - --brand-violet-11: oklch(45% 0.105 295); - --brand-violet-12: oklch(30% 0.07 295); - /* indigo */ - --brand-indigo-1: oklch(99% 0.0035 260); - --brand-indigo-2: oklch(97% 0.0105 260); - --brand-indigo-3: oklch(94% 0.021 260); - --brand-indigo-4: oklch(90% 0.035 260); - --brand-indigo-5: oklch(84% 0.056 260); - --brand-indigo-6: oklch(76% 0.084 260); - --brand-indigo-7: oklch(68% 0.112 260); - --brand-indigo-8: oklch(64% 0.126 260); - --brand-indigo-9: oklch(60% 0.14 260); - --brand-indigo-10: oklch(55% 0.1283 260); - --brand-indigo-11: oklch(45% 0.105 260); - --brand-indigo-12: oklch(30% 0.07 260); - /* fuchsia */ - --brand-fuchsia-1: oklch(99% 0.0035 335); - --brand-fuchsia-2: oklch(97% 0.0105 335); - --brand-fuchsia-3: oklch(94% 0.021 335); - --brand-fuchsia-4: oklch(90% 0.035 335); - --brand-fuchsia-5: oklch(84% 0.056 335); - --brand-fuchsia-6: oklch(76% 0.084 335); - --brand-fuchsia-7: oklch(68% 0.112 335); - --brand-fuchsia-8: oklch(64% 0.126 335); - --brand-fuchsia-9: oklch(60% 0.14 335); - --brand-fuchsia-10: oklch(55% 0.1283 335); - --brand-fuchsia-11: oklch(45% 0.105 335); - --brand-fuchsia-12: oklch(30% 0.07 335); - /* purple */ - --brand-purple-1: oklch(99% 0.0035 300); - --brand-purple-2: oklch(97% 0.0105 300); - --brand-purple-3: oklch(94% 0.021 300); - --brand-purple-4: oklch(90% 0.035 300); - --brand-purple-5: oklch(84% 0.056 300); - --brand-purple-6: oklch(76% 0.084 300); - --brand-purple-7: oklch(68% 0.112 300); - --brand-purple-8: oklch(64% 0.126 300); - --brand-purple-9: oklch(60% 0.14 300); - --brand-purple-10: oklch(55% 0.1283 300); - --brand-purple-11: oklch(45% 0.105 300); - --brand-purple-12: oklch(30% 0.07 300); - /* coral */ - --brand-coral-1: oklch(99% 0.0035 375); - --brand-coral-2: oklch(97% 0.0105 375); - --brand-coral-3: oklch(94% 0.021 375); - --brand-coral-4: oklch(90% 0.035 375); - --brand-coral-5: oklch(84% 0.056 375); - --brand-coral-6: oklch(76% 0.084 375); - --brand-coral-7: oklch(68% 0.112 375); - --brand-coral-8: oklch(64% 0.126 375); - --brand-coral-9: oklch(60% 0.14 375); - --brand-coral-10: oklch(55% 0.1283 375); - --brand-coral-11: oklch(45% 0.105 375); - --brand-coral-12: oklch(30% 0.07 375); - /* pink */ - --brand-pink-1: oklch(99% 0.0035 340); - --brand-pink-2: oklch(97% 0.0105 340); - --brand-pink-3: oklch(94% 0.021 340); - --brand-pink-4: oklch(90% 0.035 340); - --brand-pink-5: oklch(84% 0.056 340); - --brand-pink-6: oklch(76% 0.084 340); - --brand-pink-7: oklch(68% 0.112 340); - --brand-pink-8: oklch(64% 0.126 340); - --brand-pink-9: oklch(60% 0.14 340); - --brand-pink-10: oklch(55% 0.1283 340); - --brand-pink-11: oklch(45% 0.105 340); - --brand-pink-12: oklch(30% 0.07 340); - /* scarlet */ - --brand-scarlet-1: oklch(99% 0.0035 380); - --brand-scarlet-2: oklch(97% 0.0105 380); - --brand-scarlet-3: oklch(94% 0.021 380); - --brand-scarlet-4: oklch(90% 0.035 380); - --brand-scarlet-5: oklch(84% 0.056 380); - --brand-scarlet-6: oklch(76% 0.084 380); - --brand-scarlet-7: oklch(68% 0.112 380); - --brand-scarlet-8: oklch(64% 0.126 380); - --brand-scarlet-9: oklch(60% 0.14 380); - --brand-scarlet-10: oklch(55% 0.1283 380); - --brand-scarlet-11: oklch(45% 0.105 380); - --brand-scarlet-12: oklch(30% 0.07 380); - /* orange */ - --brand-orange-1: oklch(99% 0.0035 415); - --brand-orange-2: oklch(97% 0.0105 415); - --brand-orange-3: oklch(94% 0.021 415); - --brand-orange-4: oklch(90% 0.035 415); - --brand-orange-5: oklch(84% 0.056 415); - --brand-orange-6: oklch(76% 0.084 415); - --brand-orange-7: oklch(68% 0.112 415); - --brand-orange-8: oklch(64% 0.126 415); - --brand-orange-9: oklch(60% 0.14 415); - --brand-orange-10: oklch(55% 0.1283 415); - --brand-orange-11: oklch(45% 0.105 415); - --brand-orange-12: oklch(30% 0.07 415); - /* gold */ - --brand-gold-1: oklch(99% 0.0035 420); - --brand-gold-2: oklch(97% 0.0105 420); - --brand-gold-3: oklch(94% 0.021 420); - --brand-gold-4: oklch(90% 0.035 420); - --brand-gold-5: oklch(84% 0.056 420); - --brand-gold-6: oklch(76% 0.084 420); - --brand-gold-7: oklch(68% 0.112 420); - --brand-gold-8: oklch(64% 0.126 420); - --brand-gold-9: oklch(60% 0.14 420); - --brand-gold-10: oklch(55% 0.1283 420); - --brand-gold-11: oklch(45% 0.105 420); - --brand-gold-12: oklch(30% 0.07 420); - /* lime */ - --brand-lime-1: oklch(99% 0.0031 455); - --brand-lime-2: oklch(97% 0.0092 455); - --brand-lime-3: oklch(94% 0.0185 455); - --brand-lime-4: oklch(90% 0.0307 455); - --brand-lime-5: oklch(84% 0.0492 455); - --brand-lime-6: oklch(76% 0.0738 455); - --brand-lime-7: oklch(68% 0.0984 455); - --brand-lime-8: oklch(64% 0.1107 455); - --brand-lime-9: oklch(60% 0.123 455); - --brand-lime-10: oklch(55% 0.1128 455); - --brand-lime-11: oklch(45% 0.0922 455); - --brand-lime-12: oklch(30% 0.0615 455); + --art-cobalt-start: oklch(56.55547% 0.1 258.6948); + --art-cobalt-end: oklch(56.91558% 0.1 274.6948); + --art-cobalt-gradient: linear-gradient( + to bottom in oklch, + var(--art-cobalt-start), + var(--art-cobalt-end) + ); + --art-iris-start: oklch(56.95356% 0.1 276.502); + --art-iris-end: oklch(57.25707% 0.1 292.502); + --art-iris-gradient: linear-gradient( + to bottom in oklch, + var(--art-iris-start), + var(--art-iris-end) + ); + --art-magenta-start: oklch(57.75818% 0.1 347.6028); + --art-magenta-end: oklch(57.74444% 0.1 363.6028); + --art-magenta-gradient: linear-gradient( + to bottom in oklch, + var(--art-magenta-start), + var(--art-magenta-end) + ); + --art-flame-start: oklch(57.60201% 0.1 26.6181); + --art-flame-end: oklch(57.41753% 0.1 42.6181); + --art-flame-gradient: linear-gradient( + to bottom in oklch, + var(--art-flame-start), + var(--art-flame-end) + ); + --art-jade-start: oklch(55.24652% 0.1 161.9836); + --art-jade-end: oklch(55.20503% 0.1 177.9836); + --art-jade-gradient: linear-gradient( + to bottom in oklch, + var(--art-jade-start), + var(--art-jade-end) + ); + --art-chartreuse-start: oklch(56.26357% 0.1 101.5919); + --art-chartreuse-end: oklch(55.91096% 0.1 117.5919); + --art-chartreuse-gradient: linear-gradient( + to bottom in oklch, + var(--art-chartreuse-start), + var(--art-chartreuse-end) + ); } diff --git a/src/shared/design-system/tokens/brand.test.ts b/src/shared/design-system/tokens/brand.test.ts index c217836168..44d3f9e587 100644 --- a/src/shared/design-system/tokens/brand.test.ts +++ b/src/shared/design-system/tokens/brand.test.ts @@ -1,33 +1,67 @@ import { readFileSync } from "node:fs"; import { expect, it } from "vitest"; -import { BRAND_FAMILIES, BRAND_GRADIENTS, BRAND_SHADES } from "./brand"; +import { BRAND_FAMILIES, BRAND_GRADIENTS } from "./brand"; -it("preserves the gradient endpoints inside complete descending shade ranges", () => { - const css = readFileSync( - new URL("../styles/tokens.css", import.meta.url), - "utf8", +const css = readFileSync( + new URL("../styles/tokens.css", import.meta.url), + "utf8", +); +function rgb(l: number, c: number, h: number) { + const a = c * Math.cos((h * Math.PI) / 180); + const b = c * Math.sin((h * Math.PI) / 180); + const x = (l + 0.3963377774 * a + 0.2158037573 * b) ** 3; + const y = (l - 0.1055613458 * a - 0.0638541728 * b) ** 3; + const z = (l - 0.0894841775 * a - 1.291485548 * b) ** 3; + return [ + 4.0767416621 * x - 3.3077115913 * y + 0.2309699292 * z, + -1.2684380046 * x + 2.6097574011 * y - 0.3413193965 * z, + -0.0041960863 * x - 0.7034186147 * y + 1.707614701 * z, + ] as const; +} + +it("gives six distinct identities comparable contrast across the full gradient", () => { + expect(new Set(BRAND_GRADIENTS.map(({ family }) => family))).toEqual( + new Set(BRAND_FAMILIES.map(({ id }) => id)), ); for (const family of BRAND_FAMILIES) { - let previousLightness = 100; - for (const shade of BRAND_SHADES) { + const readStop = (stop: string) => { const match = css.match( new RegExp( - `--brand-${family.id}-${shade}: oklch\\(([\\d.]+)% ([\\d.]+) ([\\d.]+)\\)`, + `--art-${family.id}-${stop}: oklch\\(([\\d.]+)% ([\\d.]+) ([\\d.]+)\\)`, ), ); - expect(match, `${family.id} ${shade}`).not.toBeNull(); - if (!match) throw new Error(`Missing ${family.id} ${shade}`); - const [, lightness, chroma, hue] = match; - expect(Number(lightness)).toBeLessThan(previousLightness); - previousLightness = Number(lightness); - expect(Number(hue)).toBe(family.hue); - if (shade === 9) { - expect(Number(lightness)).toBe(60); - expect(Number(chroma)).toBe(family.chroma); + if (!match) throw new Error(`Missing ${family.id} ${stop}`); + return [ + Number(match[1]) / 100, + Number(match[2]), + Number(match[3]), + ] as const; + }; + const start = readStop("start"); + const end = readStop("end"); + for (let index = 0; index <= 20; index++) { + const t = index / 20; + const l = start[0] + (end[0] - start[0]) * t; + const c = start[1] + (end[1] - start[1]) * t; + const h = start[2] + (end[2] - start[2]) * t; + const channels = rgb(l, c, h); + for (const channel of channels) { + expect(channel).toBeGreaterThanOrEqual(0); + expect(channel).toBeLessThanOrEqual(1); } + const luminance = + channels[0] * 0.2126 + channels[1] * 0.7152 + channels[2] * 0.0722; + expect(Math.abs(luminance - 0.18)).toBeLessThan(0.002); + expect(1.05 / (luminance + 0.05)).toBeGreaterThanOrEqual(4.5); + expect((luminance + 0.05) / 0.05).toBeGreaterThanOrEqual(4.5); } } - expect( - new Set(BRAND_GRADIENTS.flatMap(({ from, to }) => [from, to])), - ).toEqual(new Set(BRAND_FAMILIES.map(({ id }) => id))); + const hues = BRAND_FAMILIES.map(({ hue }) => hue).sort((a, b) => a - b); + for (const a of hues) + for (const b of hues) { + if (a !== b) + expect( + Math.min(Math.abs(a - b), 360 - Math.abs(a - b)), + ).toBeGreaterThan(16); + } }); diff --git a/src/shared/design-system/tokens/brand.ts b/src/shared/design-system/tokens/brand.ts index 3d2df05149..55aae44a76 100644 --- a/src/shared/design-system/tokens/brand.ts +++ b/src/shared/design-system/tokens/brand.ts @@ -1,29 +1,26 @@ -/** Decorative artwork palette. Fixed scales, not theme-aware semantic roles. - * Source: plugin branding Settings.module.css, revision 834b8ea4b. - * Each step 9 preserves one exact OKLCH gradient endpoint, including its hue. - * Other stops vary lightness and taper chroma within sRGB; review before adoption. - */ +/** Existing accent identities, reused by plugin artwork. */ export const BRAND_SHADES = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12] as const; export const BRAND_FAMILIES = [ - { id: "cyan", hue: 220, chroma: 0.109 }, - { id: "blue", hue: 255, chroma: 0.14 }, - { id: "violet", hue: 295, chroma: 0.14 }, - { id: "indigo", hue: 260, chroma: 0.14 }, - { id: "fuchsia", hue: 335, chroma: 0.14 }, - { id: "purple", hue: 300, chroma: 0.14 }, - { id: "coral", hue: 375, chroma: 0.14 }, - { id: "pink", hue: 340, chroma: 0.14 }, - { id: "scarlet", hue: 380, chroma: 0.14 }, - { id: "orange", hue: 415, chroma: 0.14 }, - { id: "gold", hue: 420, chroma: 0.14 }, - { id: "lime", hue: 455, chroma: 0.123 }, + { id: "cobalt", hue: 266.6948 }, + { id: "iris", hue: 284.502 }, + { id: "magenta", hue: 355.6028 }, + { id: "flame", hue: 34.6181 }, + { id: "jade", hue: 169.9836 }, + { id: "chartreuse", hue: 109.5919 }, ] as const; - export const BRAND_GRADIENTS = [ - { name: "Communication", from: "cyan", to: "blue" }, - { name: "Agents", from: "violet", to: "indigo" }, - { name: "Agent tools", from: "fuchsia", to: "purple" }, - { name: "Developer tools & integrations", from: "coral", to: "pink" }, - { name: "People & communities", from: "scarlet", to: "orange" }, - { name: "Productivity", from: "gold", to: "lime" }, + { name: "Communication", category: "communication", family: "cobalt" }, + { name: "Agents", category: "agents", family: "iris" }, + { name: "Agent tools", category: "agent-tools", family: "magenta" }, + { + name: "Developer tools & integrations", + category: "developer-tools-and-integrations", + family: "flame", + }, + { + name: "People & communities", + category: "people-and-communities", + family: "jade", + }, + { name: "Productivity", category: "productivity", family: "chartreuse" }, ] as const; diff --git a/tests/fixtures/design-system/ui/BrandColors.test.ts b/tests/fixtures/design-system/ui/BrandColors.test.ts index c0b0454c03..941e0b606d 100644 --- a/tests/fixtures/design-system/ui/BrandColors.test.ts +++ b/tests/fixtures/design-system/ui/BrandColors.test.ts @@ -1,10 +1,17 @@ // @vitest-environment jsdom import { createElement } from "react"; -import { afterEach, expect, it, vi } from "vitest"; +import { beforeAll, afterEach, expect, it, vi } from "vitest"; import { cleanup, fireEvent, render, screen } from "@testing-library/react"; import "@testing-library/jest-dom/vitest"; import { BrandColors } from "./BrandColors"; +beforeAll(() => { + Object.defineProperty(window, "matchMedia", { + configurable: true, + value: () => ({ matches: false }), + }); +}); + const clipboardDescriptor = Object.getOwnPropertyDescriptor( navigator, "clipboard", @@ -23,22 +30,24 @@ it("selects a shade and copies its CSS reference, with a manual fallback", async value: { writeText }, }); render(createElement(BrandColors)); - fireEvent.click(screen.getByRole("button", { name: "violet 11" })); - expect(screen.getByRole("button", { name: "cyan 9" })).toHaveAttribute( + fireEvent.click(screen.getByRole("button", { name: "iris 11" })); + expect(screen.getByRole("button", { name: "cobalt 9" })).toHaveAttribute( "aria-pressed", "false", ); - expect(screen.getByRole("button", { name: "violet 11" })).toHaveAttribute( + expect(screen.getByRole("button", { name: "iris 11" })).toHaveAttribute( "aria-pressed", "true", ); fireEvent.click(screen.getByRole("button", { name: "Copy CSS" })); expect(await screen.findByText("Copied CSS variable.")).toBeInTheDocument(); - expect(writeText).toHaveBeenCalledWith("var(--brand-violet-11)"); - fireEvent.click(screen.getByRole("button", { name: "lime 12" })); + expect(writeText).toHaveBeenCalledWith( + '[data-accent="iris"][data-color-mode="light"] { color: var(--accent-11); }', + ); + fireEvent.click(screen.getByRole("button", { name: "jade 12" })); expect(screen.getByRole("status")).toBeEmptyDOMElement(); writeText.mockRejectedValueOnce(new Error("denied")); fireEvent.click(screen.getByRole("button", { name: "Copy CSS" })); expect(await screen.findByText(/Could not copy/)).toBeInTheDocument(); - expect(screen.getByText("var(--brand-lime-12)")).toBeInTheDocument(); + expect(screen.getByText("jade · var(--accent-12)")).toBeInTheDocument(); }); diff --git a/tests/fixtures/design-system/ui/BrandColors.tsx b/tests/fixtures/design-system/ui/BrandColors.tsx index b06a59611c..0dd6ebe3ff 100644 --- a/tests/fixtures/design-system/ui/BrandColors.tsx +++ b/tests/fixtures/design-system/ui/BrandColors.tsx @@ -4,23 +4,27 @@ import { BRAND_GRADIENTS, BRAND_SHADES, } from "../../../../src/shared/design-system/tokens/brand"; +import { useColorScheme } from "../../../../src/shared/design-system/theme/useColorScheme"; import { Button } from "../../../../src/shared/design-system/ui/Button"; import { useResolvedTokens } from "../useResolvedToken"; import { Section } from "./primitives"; -const variables = BRAND_FAMILIES.flatMap((family) => - BRAND_SHADES.map((shade) => `--brand-${family.id}-${shade}`), -); +const variables = BRAND_SHADES.map((shade) => `--accent-${shade}`); export function BrandColors() { - const [selected, setSelected] = useState("--brand-cyan-9"); + const [selected, setSelected] = useState("--accent-9"); + const [family, setFamily] = useState("cobalt"); + const [scope, setScope] = useState(null); + const { scheme } = useColorScheme(); const [copyStatus, setCopyStatus] = useState(""); - const resolved = useResolvedTokens(variables); + const resolved = useResolvedTokens(variables, scope); const value = resolved.get(selected)?.value ?? ""; async function copy() { try { - await navigator.clipboard.writeText(`var(${selected})`); + await navigator.clipboard.writeText( + `[data-accent="${family}"][data-color-mode="${scheme}"] { color: var(${selected}); }`, + ); setCopyStatus("Copied CSS variable."); } catch { setCopyStatus( @@ -31,8 +35,8 @@ export function BrandColors() { return (
{BRAND_GRADIENTS.map((gradient) => ( @@ -43,22 +47,28 @@ export function BrandColors() {
{gradient.name} - {gradient.from} → {gradient.to} + {gradient.family}
))}

- Original gradient stops, before the icon lighting and artwork overlays. - Family names are exploratory; nearby hues remain separate to preserve - every source color. + Gradient endpoints target the same luminance: approximately 4.57:1 + against white and 4.60:1 against black. Lighting and artwork overlays + need a separate visual check; these measurements describe the bare + gradients.

-
+