@dowel-ui/themes 0.7.0 → 0.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -20,7 +20,7 @@ no JavaScript theme object — just CSS custom properties you can read and edit.
20
20
  /* Brings Tailwind, the raw scales and the semantic layer. */
21
21
  @import "@dowel-ui/themes/styles.css";
22
22
 
23
- /* Optional — adds the seven presets. */
23
+ /* Optional — adds the thirteen presets. */
24
24
  @import "@dowel-ui/themes/presets.css";
25
25
  ```
26
26
 
@@ -43,12 +43,17 @@ file.
43
43
  }
44
44
  ```
45
45
 
46
- ## Seven presets, all contrast-audited
46
+ ## Thirteen presets, all contrast-audited
47
47
 
48
- Default · Ocean · Emerald · Violet · Rose · Amber · Monochrome
48
+ Default · Ocean · Emerald · Violet · Rose · Amber · Monochrome · Candy · Indigo ·
49
+ Blue · Red · Orange · Green
50
+
51
+ The last six derive from [SmoothUI](https://smoothui.dev)'s themes, adjusted
52
+ where SmoothUI's exact colour misses a contrast floor — each file says where and
53
+ by how much.
49
54
 
50
55
  Every preset passes **WCAG AA contrast in light and dark**, checked by an audit
51
- that evaluates 322 colour pairs across 14 schemes on every commit. When the
56
+ that evaluates 598 colour pairs across 26 schemes on every commit. When the
52
57
  audit first ran it found 88 failures — including that the amber preset could not
53
58
  carry dark text on its fill at any usable lightness, so amber became an ochre.
54
59
  That is a real trade, made knowingly, rather than a swatch that looks nice and
package/dist/index.d.ts CHANGED
@@ -213,7 +213,7 @@ declare function toDesignTokens(input: DesignTokensInput): DesignTokenGroup;
213
213
  * constants exist so theme switchers, the docs playground and the future CLI
214
214
  * all agree on the same vocabulary.
215
215
  */
216
- declare const THEME_PRESETS: readonly ["default", "ocean", "emerald", "violet", "rose", "amber", "monochrome"];
216
+ declare const THEME_PRESETS: readonly ["default", "ocean", "emerald", "violet", "rose", "amber", "monochrome", "candy", "indigo", "blue", "red", "orange", "green"];
217
217
  type ThemePreset = (typeof THEME_PRESETS)[number];
218
218
  declare const COLOR_MODES: readonly ["light", "dark", "system"];
219
219
  type ColorMode = (typeof COLOR_MODES)[number];
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","names":[],"sources":["../src/colour.ts","../src/preset.ts","../src/figma.ts","../src/index.ts"],"mappings":";;;;;;;;;UASiB;EACf;EACA;EACA;;;iBAIc,WACd;EACG;EAAW;EAAW;EAAW;;;iBAkBtB,iBAAiB,WAAW,WAAW,YAAY;;iBAqBnD,UAAU,KAAK;;;;;;;;iBAWf,UAAU,YAAY,KAAK,eAAe,YAAY,MAAM;iBAQ5D,cAAc,GAAG,KAAK,GAAG;;iBASzB,cAAc,eAAe,OAAO,MAAM;;iBAU1C,WAAW,KAAK;EAAQ;EAAW;EAAW;;;iBAW9C,WAAW,WAAW,WAAW,YAAY;UAS5C;EACf;EACA;EACA;;;;;;;;;;iBAWc,iBAAiB,KAAK,MAAM;;iBAqB5B,WAAW,cAAc;iBAqBzB,aAAa,GAAG,GAAG,KAAK;;iBAOxB,cAAc,GAAG,GAAG,KAAK;;;;cC9I5B;;;;;;;;iBAiBG,cAAc,YAAY,QAAQ;UAOjC;EACf,SAAS;EACT,cAAc;EACd,eAAe;EACf,mBAAmB;;UAGJ;EACf,OAAO;EACP,MAAM;;UAGS;;;;;;;;EAQf;;iBAGc,aAAa,OAAO,OAAO,UAAS,gBAAqB;UA+BxD;EACf;EACA;EACA;EACA;;;;;;;;;iBAUc,YAAY,QAAQ,gBAAgB;;;;;;;;iBA0CpC,aAAa,cAAc,QAAQ;;iBAcnC,QAAQ;;;;;;;;;;;;;;;;;;;KCpKZ,eAAe;;UAGV;EACf;EACA;EACA;;UAGe;GACd,cAAc,cAAc;EAC7B;;;;;;;;;iBAec,cAAc,aAAa,mBAAmB;;;;;;;;;;iBA6C9C,kBAAkB,eAAe,QAAQ;;iBAsBzC,eAAe;;;;;;;;;iBAoBf,cAAc,eAAe;;iBA0B7B,mBAAmB,QAAQ;EACzC,OAAO;EACP,MAAM;;UAWS;;EAEf;;EAEA,OAAO;;EAEP,OAAO;;EAEP,MAAM;;EAEN;IAAW,OAAO;IAAc,MAAM;;;EAEtC;;;;;;;;;iBAgFc,eAAe,OAAO,oBAAoB;;;;;;;;cCvN7C;KAUD,sBAAsB;cAErB;KAED,oBAAoB;;cAGnB;;cAGA;;;;;cAMA;iBAEG,cAAc,gBAAgB,SAAS;iBAIvC,YAAY,gBAAgB,SAAS"}
1
+ {"version":3,"file":"index.d.ts","names":[],"sources":["../src/colour.ts","../src/preset.ts","../src/figma.ts","../src/index.ts"],"mappings":";;;;;;;;;UASiB;EACf;EACA;EACA;;;iBAIc,WACd;EACG;EAAW;EAAW;EAAW;;;iBAkBtB,iBAAiB,WAAW,WAAW,YAAY;;iBAqBnD,UAAU,KAAK;;;;;;;;iBAWf,UAAU,YAAY,KAAK,eAAe,YAAY,MAAM;iBAQ5D,cAAc,GAAG,KAAK,GAAG;;iBASzB,cAAc,eAAe,OAAO,MAAM;;iBAU1C,WAAW,KAAK;EAAQ;EAAW;EAAW;;;iBAW9C,WAAW,WAAW,WAAW,YAAY;UAS5C;EACf;EACA;EACA;;;;;;;;;;iBAWc,iBAAiB,KAAK,MAAM;;iBAqB5B,WAAW,cAAc;iBAqBzB,aAAa,GAAG,GAAG,KAAK;;iBAOxB,cAAc,GAAG,GAAG,KAAK;;;;cC9I5B;;;;;;;;iBAiBG,cAAc,YAAY,QAAQ;UAOjC;EACf,SAAS;EACT,cAAc;EACd,eAAe;EACf,mBAAmB;;UAGJ;EACf,OAAO;EACP,MAAM;;UAGS;;;;;;;;EAQf;;iBAGc,aAAa,OAAO,OAAO,UAAS,gBAAqB;UA+BxD;EACf;EACA;EACA;EACA;;;;;;;;;iBAUc,YAAY,QAAQ,gBAAgB;;;;;;;;iBA0CpC,aAAa,cAAc,QAAQ;;iBAcnC,QAAQ;;;;;;;;;;;;;;;;;;;KCpKZ,eAAe;;UAGV;EACf;EACA;EACA;;UAGe;GACd,cAAc,cAAc;EAC7B;;;;;;;;;iBAec,cAAc,aAAa,mBAAmB;;;;;;;;;;iBA6C9C,kBAAkB,eAAe,QAAQ;;iBAsBzC,eAAe;;;;;;;;;iBAoBf,cAAc,eAAe;;iBA0B7B,mBAAmB,QAAQ;EACzC,OAAO;EACP,MAAM;;UAWS;;EAEf;;EAEA,OAAO;;EAEP,OAAO;;EAEP,MAAM;;EAEN;IAAW,OAAO;IAAc,MAAM;;;EAEtC;;;;;;;;;iBAgFc,eAAe,OAAO,oBAAoB;;;;;;;;cCvN7C;KAgBD,sBAAsB;cAErB;KAED,oBAAoB;;cAGnB;;cAGA;;;;;cAMA;iBAEG,cAAc,gBAAgB,SAAS;iBAIvC,YAAY,gBAAgB,SAAS"}
package/dist/index.js CHANGED
@@ -513,7 +513,13 @@ const THEME_PRESETS = [
513
513
  "violet",
514
514
  "rose",
515
515
  "amber",
516
- "monochrome"
516
+ "monochrome",
517
+ "candy",
518
+ "indigo",
519
+ "blue",
520
+ "red",
521
+ "orange",
522
+ "green"
517
523
  ];
518
524
  const COLOR_MODES = [
519
525
  "light",
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","names":[],"sources":["../src/colour.ts","../src/preset.ts","../src/figma.ts","../src/index.ts"],"sourcesContent":["/**\n * OKLCH → sRGB, and WCAG contrast.\n *\n * The design tokens are authored in OKLCH because its lightness is\n * perceptually even. WCAG contrast, however, is defined on sRGB relative\n * luminance — so checking the palette means actually converting it rather than\n * eyeballing the lightness numbers, which are not the same thing.\n */\n\nexport interface Rgb {\n r: number;\n g: number;\n b: number;\n}\n\n/** Parses `oklch(L C H)` or `oklch(L C H / A)`. L may be a percentage. */\nexport function parseOklch(\n value: string,\n): { l: number; c: number; h: number; alpha: number } | undefined {\n const match = /^oklch\\(\\s*([\\d.%]+)\\s+([\\d.]+)\\s+([\\d.]+)\\s*(?:\\/\\s*([\\d.]+)\\s*)?\\)$/.exec(\n value.trim(),\n );\n if (!match) return undefined;\n\n const rawL = match[1] ?? \"0\";\n const l = rawL.endsWith(\"%\") ? Number.parseFloat(rawL) / 100 : Number.parseFloat(rawL);\n\n return {\n l,\n c: Number.parseFloat(match[2] ?? \"0\"),\n h: Number.parseFloat(match[3] ?? \"0\"),\n alpha: match[4] === undefined ? 1 : Number.parseFloat(match[4]),\n };\n}\n\n/** Linear-light sRGB, before gamma encoding. Luminance is defined on these. */\nexport function oklchToLinearRgb(l: number, c: number, h: number): Rgb {\n const hRad = (h * Math.PI) / 180;\n const a = c * Math.cos(hRad);\n const bb = c * Math.sin(hRad);\n\n const lCube = (l + 0.3963377774 * a + 0.2158037573 * bb) ** 3;\n const mCube = (l - 0.1055613458 * a - 0.0638541728 * bb) ** 3;\n const sCube = (l - 0.0894841775 * a - 1.291485548 * bb) ** 3;\n\n return {\n r: 4.0767416621 * lCube - 3.3077115913 * mCube + 0.2309699292 * sCube,\n g: -1.2684380046 * lCube + 2.6097574011 * mCube - 0.3413193965 * sCube,\n b: -0.0041960863 * lCube - 0.7034186147 * mCube + 1.707614701 * sCube,\n };\n}\n\nfunction clamp(value: number): number {\n return Math.min(1, Math.max(0, value));\n}\n\n/** WCAG 2.x relative luminance. */\nexport function luminance(rgb: Rgb): number {\n return 0.2126 * clamp(rgb.r) + 0.7152 * clamp(rgb.g) + 0.0722 * clamp(rgb.b);\n}\n\n/**\n * Composites a translucent colour over an opaque one.\n *\n * Several tokens are alpha values over a surface — an overlay, a tinted alert\n * background. Measuring them without compositing would report the contrast of a\n * colour nobody ever sees.\n */\nexport function composite(foreground: Rgb, alpha: number, background: Rgb): Rgb {\n return {\n r: foreground.r * alpha + background.r * (1 - alpha),\n g: foreground.g * alpha + background.g * (1 - alpha),\n b: foreground.b * alpha + background.b * (1 - alpha),\n };\n}\n\nexport function contrastRatio(a: Rgb, b: Rgb): number {\n const la = luminance(a);\n const lb = luminance(b);\n const lighter = Math.max(la, lb);\n const darker = Math.min(la, lb);\n return (lighter + 0.05) / (darker + 0.05);\n}\n\n/** `oklch(...)` string to linear RGB, composited over `over` if translucent. */\nexport function resolveColour(value: string, over?: Rgb): Rgb | undefined {\n const parsed = parseOklch(value);\n if (!parsed) return undefined;\n\n const rgb = oklchToLinearRgb(parsed.l, parsed.c, parsed.h);\n if (parsed.alpha >= 1 || !over) return rgb;\n return composite(rgb, parsed.alpha, over);\n}\n\n/** Gamma-encoded sRGB, 0–255, from linear-light sRGB. */\nexport function encodeSrgb(rgb: Rgb): { r: number; g: number; b: number } {\n const encode = (channel: number) => {\n const value = clamp(channel);\n const encoded = value <= 0.0031308 ? value * 12.92 : 1.055 * value ** (1 / 2.4) - 0.055;\n return Math.round(encoded * 255);\n };\n\n return { r: encode(rgb.r), g: encode(rgb.g), b: encode(rgb.b) };\n}\n\n/** Linear-light sRGB from gamma-encoded channels, each 0–255. */\nexport function decodeSrgb(r: number, g: number, b: number): Rgb {\n const decode = (channel: number) => {\n const value = channel / 255;\n return value <= 0.04045 ? value / 12.92 : ((value + 0.055) / 1.055) ** 2.4;\n };\n\n return { r: decode(r), g: decode(g), b: decode(b) };\n}\n\nexport interface Oklch {\n l: number;\n c: number;\n h: number;\n}\n\n/**\n * Linear-light sRGB to OKLCH.\n *\n * The inverse of `oklchToLinearRgb`, needed because people pick colours as hex\n * and the tokens are authored in OKLCH. Round-tripping through this is lossy\n * only where the input is outside the OKLCH gamut the tokens use, which a hex\n * value from a colour input never is.\n */\nexport function linearRgbToOklch(rgb: Rgb): Oklch {\n const lCube = 0.4122214708 * rgb.r + 0.5363325363 * rgb.g + 0.0514459929 * rgb.b;\n const mCube = 0.2119034982 * rgb.r + 0.6806995451 * rgb.g + 0.1073969566 * rgb.b;\n const sCube = 0.0883024619 * rgb.r + 0.2817188376 * rgb.g + 0.6299787005 * rgb.b;\n\n const l_ = Math.cbrt(lCube);\n const m_ = Math.cbrt(mCube);\n const s_ = Math.cbrt(sCube);\n\n const l = 0.2104542553 * l_ + 0.793617785 * m_ - 0.0040720468 * s_;\n const a = 1.9779984951 * l_ - 2.428592205 * m_ + 0.4505937099 * s_;\n const b = 0.0259040371 * l_ + 0.7827717662 * m_ - 0.808675766 * s_;\n\n const c = Math.sqrt(a * a + b * b);\n // atan2 returns (-180, 180]; hues are conventionally [0, 360).\n const h = c < 1e-6 ? 0 : ((Math.atan2(b, a) * 180) / Math.PI + 360) % 360;\n\n return { l, c, h };\n}\n\n/** `#rrggbb` (or `#rgb`) to OKLCH. Undefined for anything else. */\nexport function hexToOklch(hex: string): Oklch | undefined {\n const value = hex.trim().replace(/^#/, \"\");\n const full =\n value.length === 3\n ? value\n .split(\"\")\n .map((digit) => digit + digit)\n .join(\"\")\n : value;\n\n if (!/^[0-9a-f]{6}$/i.test(full)) return undefined;\n\n return linearRgbToOklch(\n decodeSrgb(\n Number.parseInt(full.slice(0, 2), 16),\n Number.parseInt(full.slice(2, 4), 16),\n Number.parseInt(full.slice(4, 6), 16),\n ),\n );\n}\n\nexport function oklchToHex({ l, c, h }: Oklch): string {\n const { r, g, b } = encodeSrgb(oklchToLinearRgb(l, c, h));\n const pair = (channel: number) => channel.toString(16).padStart(2, \"0\");\n return `#${pair(r)}${pair(g)}${pair(b)}`;\n}\n\n/** An OKLCH triple as the tokens write it. */\nexport function formatOklch({ l, c, h }: Oklch): string {\n const round = (value: number, places: number) => Number(value.toFixed(places)).toString();\n return `oklch(${round(l, 3)} ${round(c, 3)} ${round(h, 1)})`;\n}\n","import { contrastRatio, formatOklch, oklchToLinearRgb, type Oklch } from \"./colour\";\n\n/**\n * Deriving a theme preset from a single colour.\n *\n * A preset in this system reassigns four tokens per mode and inherits\n * everything else, so building one is not a palette exercise — it is picking a\n * primary and then answering three questions the shipped presets already\n * answer: what it looks like pressed, what it looks like in dark mode, and what\n * text can be read on it.\n *\n * The deltas below are read off the presets that ship. They are not arbitrary:\n * every one of those passes the contrast audit in both modes, so starting from\n * the same relationships means a derived preset starts somewhere that works.\n */\n\n/** Lightness step from the base colour to its hover state, in light mode. */\nconst LIGHT_HOVER_DELTA = -0.045;\n/** And to its active state, which is a press and reads as further down. */\nconst LIGHT_ACTIVE_DELTA = -0.083;\n\n/**\n * Dark mode raises lightness and drops chroma.\n *\n * A colour that reads as saturated on white reads as glaring on near-black, and\n * one dark enough to sit on white disappears into the background.\n */\nconst DARK_LIGHTNESS_DELTA = 0.115;\nconst DARK_CHROMA_DELTA = -0.015;\nconst DARK_HOVER_DELTA = 0.045;\nconst DARK_ACTIVE_DELTA = -0.045;\n\n/** The near-white the shipped presets use for text on a saturated colour. */\nconst LIGHT_FOREGROUND: Oklch = { l: 0.985, c: 0.002, h: 265 };\n\n/** WCAG 2.2 AA for normal text; a button label is normal text. */\nexport const TEXT_MINIMUM = 4.5;\n\nfunction clampLightness(value: number): number {\n return Math.min(0.99, Math.max(0.01, value));\n}\n\nfunction ratio(a: Oklch, b: Oklch): number {\n return contrastRatio(oklchToLinearRgb(a.l, a.c, a.h), oklchToLinearRgb(b.l, b.c, b.h));\n}\n\n/**\n * Text for a saturated background: near-white, or a dark tint of its own hue.\n *\n * Whichever reads better, rather than always white. A light primary — amber,\n * lime — cannot carry white text at 4.5:1 no matter how it is nudged, and the\n * shipped `amber` preset is dark-on-light for exactly this reason.\n */\nexport function foregroundFor(background: Oklch): Oklch {\n const dark: Oklch = { l: 0.155, c: 0.03, h: background.h };\n return ratio(LIGHT_FOREGROUND, background) >= ratio(dark, background)\n ? LIGHT_FOREGROUND\n : dark;\n}\n\nexport interface PresetMode {\n primary: Oklch;\n primaryHover: Oklch;\n primaryActive: Oklch;\n primaryForeground: Oklch;\n}\n\nexport interface DerivedPreset {\n light: PresetMode;\n dark: PresetMode;\n}\n\nexport interface DeriveOptions {\n /**\n * Lightness of the dark-mode primary.\n *\n * Overridable because it is the one derived value with no single right\n * answer: how bright a brand reads on near-black is a judgement about the\n * brand, not about contrast.\n */\n darkLightness?: number;\n}\n\nexport function derivePreset(input: Oklch, options: DeriveOptions = {}): DerivedPreset {\n // Clamped on the way in as well as on the way out. Every value this returns\n // is one it is responsible for, including the one it was handed: a preset\n // built on pure black is not a preset anyone can use, and passing it through\n // untouched would mean the only unusable value in the output is the one that\n // was never checked.\n const primary: Oklch = { ...input, l: clampLightness(input.l), c: Math.max(0, input.c) };\n\n const light: PresetMode = {\n primary,\n primaryHover: { ...primary, l: clampLightness(primary.l + LIGHT_HOVER_DELTA) },\n primaryActive: { ...primary, l: clampLightness(primary.l + LIGHT_ACTIVE_DELTA) },\n primaryForeground: foregroundFor(primary),\n };\n\n const darkPrimary: Oklch = {\n l: clampLightness(options.darkLightness ?? primary.l + DARK_LIGHTNESS_DELTA),\n c: Math.max(0, primary.c + DARK_CHROMA_DELTA),\n h: primary.h,\n };\n\n const dark: PresetMode = {\n primary: darkPrimary,\n primaryHover: { ...darkPrimary, l: clampLightness(darkPrimary.l + DARK_HOVER_DELTA) },\n primaryActive: { ...darkPrimary, l: clampLightness(darkPrimary.l + DARK_ACTIVE_DELTA) },\n primaryForeground: foregroundFor(darkPrimary),\n };\n\n return { light, dark };\n}\n\nexport interface ContrastCheck {\n label: string;\n ratio: number;\n minimum: number;\n passes: boolean;\n}\n\n/**\n * The pairs a derived preset is responsible for.\n *\n * Only these four per mode: every other pair in the system is inherited from\n * the base tokens, which the audit already covers. Reporting the inherited ones\n * would be reporting on something the person cannot change from here.\n */\nexport function checkPreset(preset: DerivedPreset): ContrastCheck[] {\n const checks: ContrastCheck[] = [];\n\n for (const [mode, values] of [\n [\"Light\", preset.light],\n [\"Dark\", preset.dark],\n ] as const) {\n for (const [state, background] of [\n [\"primary\", values.primary],\n [\"primary-hover\", values.primaryHover],\n [\"primary-active\", values.primaryActive],\n ] as const) {\n checks.push({\n label: `${mode}: primary-foreground on ${state}`,\n ratio: ratio(values.primaryForeground, background),\n minimum: TEXT_MINIMUM,\n passes: ratio(values.primaryForeground, background) >= TEXT_MINIMUM,\n });\n }\n }\n\n return checks;\n}\n\nfunction block(selector: string, mode: PresetMode): string {\n return [\n `${selector} {`,\n ` --primary: ${formatOklch(mode.primary)};`,\n ` --primary-hover: ${formatOklch(mode.primaryHover)};`,\n ` --primary-active: ${formatOklch(mode.primaryActive)};`,\n ` --primary-foreground: ${formatOklch(mode.primaryForeground)};`,\n `}`,\n ].join(\"\\n\");\n}\n\n/**\n * The preset as a stylesheet, in the same shape as the ones that ship.\n *\n * Deliberately the same file format rather than a bespoke export: what comes\n * out of here can be dropped into `packages/themes/src/presets/` unchanged, and\n * is then covered by the same audit as everything else.\n */\nexport function formatPreset(name: string, preset: DerivedPreset): string {\n return [\n `/* Theme preset: ${name}.`,\n ` * Apply with data-theme=\"${name}\" on the <html> element. Only the brand-carrying`,\n ` * tokens are reassigned; every neutral, radius and motion token is inherited. */`,\n \"\",\n block(`[data-theme=\"${name}\"]`, preset.light),\n \"\",\n block(`.dark[data-theme=\"${name}\"]`, preset.dark),\n \"\",\n ].join(\"\\n\");\n}\n\n/** A name usable as a `data-theme` value. */\nexport function slugify(name: string): string {\n const slug = name\n .toLowerCase()\n .replace(/[^a-z0-9]+/g, \"-\")\n .replace(/^-+|-+$/g, \"\");\n return slug.length > 0 ? slug : \"custom\";\n}\n","import { encodeSrgb, oklchToLinearRgb, parseOklch, formatOklch } from \"./colour\";\nimport type { DerivedPreset, PresetMode } from \"./preset\";\n\n/**\n * The tokens, in the shape a design tool reads.\n *\n * The CSS is the source of truth and stays that way — nothing here is a second\n * palette to keep in step. This reads the same `tokens.css`, `base.css` and\n * preset files the components consume, resolves every `var()` the way a\n * browser would, and writes the result as W3C Design Tokens (DTCG): the format\n * Tokens Studio for Figma imports directly, and the one every other design\n * tool is converging on.\n *\n * Colours come out as sRGB hex. Figma has no OKLCH; converting here, with the\n * same maths the contrast audit uses, means the swatch a designer sees is the\n * colour a user gets — rather than whatever a tool makes of an `oklch()` string\n * it cannot parse.\n */\n\n/** A flat map of custom property name (without the leading `--`) to raw value. */\nexport type Declarations = Record<string, string>;\n\n/** A W3C Design Tokens document: nested groups whose leaves carry `$type` and `$value`. */\nexport interface DesignToken {\n $type: \"color\" | \"dimension\" | \"fontFamily\" | \"number\";\n $value: string | number | string[];\n $description?: string;\n}\n\nexport interface DesignTokenGroup {\n [key: string]: DesignToken | DesignTokenGroup | string | undefined;\n $description?: string;\n}\n\n/** Strips block comments, which otherwise hide `--x: y;` pairs inside them from the parser. */\nfunction stripComments(css: string): string {\n return css.replace(/\\/\\*[\\s\\S]*?\\*\\//g, \"\");\n}\n\n/**\n * The declarations inside the first block whose selector is exactly `selector`.\n *\n * Exact, not substring: `.dark` must not match `.dark[data-theme=\"ocean\"]`, and\n * `:root` must not match `:root[dir=\"rtl\"]`. Whitespace inside a value is\n * collapsed, because a font stack written over four lines is one value.\n */\nexport function parseTokenCss(css: string, selector: string): Declarations {\n const clean = stripComments(css);\n const declarations: Declarations = {};\n\n let searchFrom = 0;\n for (;;) {\n const brace = clean.indexOf(\"{\", searchFrom);\n if (brace === -1) break;\n\n // The selector is whatever sits between the previous `}` (or `;`, or the\n // start) and this `{`.\n const start = Math.max(\n clean.lastIndexOf(\"}\", brace),\n clean.lastIndexOf(\";\", brace),\n clean.lastIndexOf(\"{\", brace - 1),\n );\n const candidate = clean.slice(start + 1, brace).trim();\n const end = clean.indexOf(\"}\", brace);\n if (end === -1) break;\n\n if (candidate === selector) {\n const body = clean.slice(brace + 1, end);\n for (const match of body.matchAll(/--([\\w-]+)\\s*:\\s*([^;]+);/g)) {\n const name = match[1];\n const value = match[2];\n if (name && value) declarations[name] = value.replace(/\\s+/g, \" \").trim();\n }\n return declarations;\n }\n\n searchFrom = end + 1;\n }\n\n return declarations;\n}\n\n/**\n * Resolves `var(--x)` references the way the cascade would.\n *\n * `scopes` are searched in order, so a mode's own declarations shadow the root's\n * and the root's shadow the raw scale — which is exactly what `.dark { --x }`\n * over `:root { --x }` over `@theme { --x }` means. A fallback inside the\n * `var()` is used when nothing defines the name, and an unresolvable reference\n * is left as written rather than silently dropped.\n */\nexport function resolveReferences(value: string, scopes: Declarations[]): string {\n let current = value;\n // Bounded, so a cycle terminates with the reference left in place rather\n // than hanging the build.\n for (let depth = 0; depth < 16; depth += 1) {\n const next = current.replace(\n /var\\(\\s*--([\\w-]+)\\s*(?:,\\s*([^()]*(?:\\([^()]*\\))?[^()]*))?\\)/g,\n (whole, name: string, fallback: string | undefined) => {\n for (const scope of scopes) {\n const found = scope[name];\n if (found !== undefined) return found;\n }\n return fallback?.trim() ?? whole;\n },\n );\n if (next === current) return current;\n current = next;\n }\n return current;\n}\n\n/** `oklch(...)` to `#rrggbb`, or `#rrggbbaa` when it carries alpha. */\nexport function cssColourToHex(value: string): string | undefined {\n const parsed = parseOklch(value.trim());\n if (!parsed) return undefined;\n\n const { r, g, b } = encodeSrgb(oklchToLinearRgb(parsed.l, parsed.c, parsed.h));\n const pair = (channel: number) => channel.toString(16).padStart(2, \"0\");\n const rgb = `#${pair(r)}${pair(g)}${pair(b)}`;\n return parsed.alpha >= 1 ? rgb : `${rgb}${pair(Math.round(parsed.alpha * 255))}`;\n}\n\nconst ROOT_FONT_PX = 16;\n\n/**\n * A length in px, for the values the scale is written in.\n *\n * Handles the two shapes the tokens use: a plain `rem`/`px`, and the radius\n * ladder's `calc(<rem> * var(--radius-scale, 1))`, which is resolved with the\n * given scale so an exported theme carries the corner radius it was designed\n * with rather than a formula Figma cannot evaluate.\n */\nexport function cssLengthToPx(value: string, radiusScale = 1): number | undefined {\n const trimmed = value.trim();\n\n const calc = /^calc\\(\\s*([\\d.]+)rem\\s*\\*\\s*var\\(--radius-scale(?:,\\s*[\\d.]+)?\\)\\s*\\)$/.exec(\n trimmed,\n );\n if (calc?.[1]) return round(Number.parseFloat(calc[1]) * ROOT_FONT_PX * radiusScale);\n\n const rem = /^([\\d.]+)rem$/.exec(trimmed);\n if (rem?.[1]) return round(Number.parseFloat(rem[1]) * ROOT_FONT_PX);\n\n const px = /^([\\d.]+)px$/.exec(trimmed);\n if (px?.[1]) return round(Number.parseFloat(px[1]));\n\n return undefined;\n}\n\nfunction round(value: number): number {\n return Math.round(value * 100) / 100;\n}\n\nfunction px(value: number): string {\n return `${String(value)}px`;\n}\n\n/** The four tokens a derived preset owns, as declarations, per mode. */\nexport function presetDeclarations(preset: DerivedPreset): {\n light: Declarations;\n dark: Declarations;\n} {\n const mode = (entry: PresetMode): Declarations => ({\n primary: formatOklch(entry.primary),\n \"primary-hover\": formatOklch(entry.primaryHover),\n \"primary-active\": formatOklch(entry.primaryActive),\n \"primary-foreground\": formatOklch(entry.primaryForeground),\n });\n return { light: mode(preset.light), dark: mode(preset.dark) };\n}\n\nexport interface DesignTokensInput {\n /** Named in the document, e.g. \"ocean\" or a studio preset's slug. */\n name: string;\n /** The `@theme` block of tokens.css: the raw scales. */\n scale: Declarations;\n /** The `:root` block of base.css. */\n light: Declarations;\n /** The `.dark` block of base.css. */\n dark: Declarations;\n /** A preset's overrides, layered over `light` and `dark`. */\n preset?: { light: Declarations; dark: Declarations };\n /** The `--radius-scale` the theme was designed at. */\n radiusScale?: number;\n}\n\nfunction colourGroup(\n declarations: Declarations,\n scopes: Declarations[],\n filter: (name: string) => boolean,\n): DesignTokenGroup {\n const group: DesignTokenGroup = {};\n for (const [name, raw] of Object.entries(declarations)) {\n if (!filter(name)) continue;\n const hex = cssColourToHex(resolveReferences(raw, scopes));\n if (hex) group[name] = { $type: \"color\", $value: hex };\n }\n return group;\n}\n\n/** `--color-neutral-500` → `neutral.500`, nested. */\nfunction scaleColours(scale: Declarations): DesignTokenGroup {\n const group: DesignTokenGroup = {};\n for (const [name, raw] of Object.entries(scale)) {\n const match = /^color-([a-z]+)-(\\d+)$/.exec(name);\n if (!match) continue;\n const [, family, step] = match;\n if (!family || !step) continue;\n const hex = cssColourToHex(raw);\n if (!hex) continue;\n const familyGroup = (group[family] ??= {}) as DesignTokenGroup;\n familyGroup[step] = { $type: \"color\", $value: hex };\n }\n return group;\n}\n\nfunction radii(scale: Declarations, radiusScale: number): DesignTokenGroup {\n const group: DesignTokenGroup = {};\n for (const [name, raw] of Object.entries(scale)) {\n const match = /^radius-([\\w]+)$/.exec(name);\n if (!match?.[1]) continue;\n const length = cssLengthToPx(raw, radiusScale);\n if (length !== undefined) group[match[1]] = { $type: \"dimension\", $value: px(length) };\n }\n return group;\n}\n\nfunction typography(scale: Declarations): DesignTokenGroup {\n const family: DesignTokenGroup = {};\n for (const [name, raw] of Object.entries(scale)) {\n const match = /^font-([a-z]+)$/.exec(name);\n if (!match?.[1]) continue;\n family[match[1]] = {\n $type: \"fontFamily\",\n $value: raw.split(\",\").map((entry) => entry.trim().replace(/^[\"']|[\"']$/g, \"\")),\n };\n }\n\n const size: DesignTokenGroup = {};\n for (const [name, raw] of Object.entries(scale)) {\n const step = /^text-([\\w]+)$/.exec(name)?.[1];\n if (!step) continue;\n const fontSize = cssLengthToPx(raw);\n if (fontSize === undefined) continue;\n const entry: DesignTokenGroup = { size: { $type: \"dimension\", $value: px(fontSize) } };\n const lineHeight = scale[`text-${step}--line-height`];\n const lineHeightPx = lineHeight === undefined ? undefined : cssLengthToPx(lineHeight);\n if (lineHeightPx !== undefined) {\n entry.lineHeight = { $type: \"dimension\", $value: px(lineHeightPx) };\n }\n size[step] = entry;\n }\n\n return { family, text: size };\n}\n\n/**\n * The whole theme, as a design-tokens document.\n *\n * Three sets: `core` (the raw scales, the same in both modes), `light` and\n * `dark` (the semantic colours, resolved). A designer enables `core` plus one\n * mode, which mirrors exactly how the CSS composes.\n */\nexport function toDesignTokens(input: DesignTokensInput): DesignTokenGroup {\n const radiusScale = input.radiusScale ?? 1;\n const light: Declarations = { ...input.light, ...input.preset?.light };\n const dark: Declarations = { ...input.dark, ...input.preset?.dark };\n\n const semantic = (name: string) => name !== \"radius-scale\" && !name.startsWith(\"color-\");\n\n return {\n $description: `Dowel design tokens, preset \"${input.name}\". Generated from the CSS the components use; colours are sRGB hex converted from OKLCH.`,\n core: {\n color: scaleColours(input.scale),\n radius: radii(input.scale, radiusScale),\n font: typography(input.scale),\n },\n light: {\n color: colourGroup(light, [light, input.scale], semantic),\n },\n dark: {\n // Dark declares only what differs; the rest inherits from the root.\n color: colourGroup({ ...light, ...dark }, [dark, light, input.scale], semantic),\n },\n };\n}\n","export {\n checkPreset,\n derivePreset,\n foregroundFor,\n formatPreset,\n slugify,\n TEXT_MINIMUM,\n type ContrastCheck,\n type DerivedPreset,\n type DeriveOptions,\n type PresetMode,\n} from \"./preset\";\n\nexport {\n cssColourToHex,\n cssLengthToPx,\n parseTokenCss,\n presetDeclarations,\n resolveReferences,\n toDesignTokens,\n type Declarations,\n type DesignToken,\n type DesignTokenGroup,\n type DesignTokensInput,\n} from \"./figma\";\n\nexport {\n composite,\n contrastRatio,\n decodeSrgb,\n encodeSrgb,\n formatOklch,\n hexToOklch,\n linearRgbToOklch,\n luminance,\n oklchToHex,\n oklchToLinearRgb,\n parseOklch,\n resolveColour,\n type Oklch,\n type Rgb,\n} from \"./colour\";\n\n/**\n * Typed surface of the theme layer. The CSS is the implementation; these\n * constants exist so theme switchers, the docs playground and the future CLI\n * all agree on the same vocabulary.\n */\n\nexport const THEME_PRESETS = [\n \"default\",\n \"ocean\",\n \"emerald\",\n \"violet\",\n \"rose\",\n \"amber\",\n \"monochrome\",\n] as const;\n\nexport type ThemePreset = (typeof THEME_PRESETS)[number];\n\nexport const COLOR_MODES = [\"light\", \"dark\", \"system\"] as const;\n\nexport type ColorMode = (typeof COLOR_MODES)[number];\n\n/** Attribute set on the <html> element to activate a preset. */\nexport const THEME_ATTRIBUTE = \"data-theme\";\n\n/** Class toggled on the <html> element for dark mode. */\nexport const DARK_CLASS = \"dark\";\n\n/**\n * Custom property that re-proportions every radius token at once.\n * `1` is the designed default; `0` gives fully square corners.\n */\nexport const RADIUS_SCALE_PROPERTY = \"--radius-scale\";\n\nexport function isThemePreset(value: string): value is ThemePreset {\n return (THEME_PRESETS as readonly string[]).includes(value);\n}\n\nexport function isColorMode(value: string): value is ColorMode {\n return (COLOR_MODES as readonly string[]).includes(value);\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"index.js","names":[],"sources":["../src/colour.ts","../src/preset.ts","../src/figma.ts","../src/index.ts"],"sourcesContent":["/**\n * OKLCH → sRGB, and WCAG contrast.\n *\n * The design tokens are authored in OKLCH because its lightness is\n * perceptually even. WCAG contrast, however, is defined on sRGB relative\n * luminance — so checking the palette means actually converting it rather than\n * eyeballing the lightness numbers, which are not the same thing.\n */\n\nexport interface Rgb {\n r: number;\n g: number;\n b: number;\n}\n\n/** Parses `oklch(L C H)` or `oklch(L C H / A)`. L may be a percentage. */\nexport function parseOklch(\n value: string,\n): { l: number; c: number; h: number; alpha: number } | undefined {\n const match = /^oklch\\(\\s*([\\d.%]+)\\s+([\\d.]+)\\s+([\\d.]+)\\s*(?:\\/\\s*([\\d.]+)\\s*)?\\)$/.exec(\n value.trim(),\n );\n if (!match) return undefined;\n\n const rawL = match[1] ?? \"0\";\n const l = rawL.endsWith(\"%\") ? Number.parseFloat(rawL) / 100 : Number.parseFloat(rawL);\n\n return {\n l,\n c: Number.parseFloat(match[2] ?? \"0\"),\n h: Number.parseFloat(match[3] ?? \"0\"),\n alpha: match[4] === undefined ? 1 : Number.parseFloat(match[4]),\n };\n}\n\n/** Linear-light sRGB, before gamma encoding. Luminance is defined on these. */\nexport function oklchToLinearRgb(l: number, c: number, h: number): Rgb {\n const hRad = (h * Math.PI) / 180;\n const a = c * Math.cos(hRad);\n const bb = c * Math.sin(hRad);\n\n const lCube = (l + 0.3963377774 * a + 0.2158037573 * bb) ** 3;\n const mCube = (l - 0.1055613458 * a - 0.0638541728 * bb) ** 3;\n const sCube = (l - 0.0894841775 * a - 1.291485548 * bb) ** 3;\n\n return {\n r: 4.0767416621 * lCube - 3.3077115913 * mCube + 0.2309699292 * sCube,\n g: -1.2684380046 * lCube + 2.6097574011 * mCube - 0.3413193965 * sCube,\n b: -0.0041960863 * lCube - 0.7034186147 * mCube + 1.707614701 * sCube,\n };\n}\n\nfunction clamp(value: number): number {\n return Math.min(1, Math.max(0, value));\n}\n\n/** WCAG 2.x relative luminance. */\nexport function luminance(rgb: Rgb): number {\n return 0.2126 * clamp(rgb.r) + 0.7152 * clamp(rgb.g) + 0.0722 * clamp(rgb.b);\n}\n\n/**\n * Composites a translucent colour over an opaque one.\n *\n * Several tokens are alpha values over a surface — an overlay, a tinted alert\n * background. Measuring them without compositing would report the contrast of a\n * colour nobody ever sees.\n */\nexport function composite(foreground: Rgb, alpha: number, background: Rgb): Rgb {\n return {\n r: foreground.r * alpha + background.r * (1 - alpha),\n g: foreground.g * alpha + background.g * (1 - alpha),\n b: foreground.b * alpha + background.b * (1 - alpha),\n };\n}\n\nexport function contrastRatio(a: Rgb, b: Rgb): number {\n const la = luminance(a);\n const lb = luminance(b);\n const lighter = Math.max(la, lb);\n const darker = Math.min(la, lb);\n return (lighter + 0.05) / (darker + 0.05);\n}\n\n/** `oklch(...)` string to linear RGB, composited over `over` if translucent. */\nexport function resolveColour(value: string, over?: Rgb): Rgb | undefined {\n const parsed = parseOklch(value);\n if (!parsed) return undefined;\n\n const rgb = oklchToLinearRgb(parsed.l, parsed.c, parsed.h);\n if (parsed.alpha >= 1 || !over) return rgb;\n return composite(rgb, parsed.alpha, over);\n}\n\n/** Gamma-encoded sRGB, 0–255, from linear-light sRGB. */\nexport function encodeSrgb(rgb: Rgb): { r: number; g: number; b: number } {\n const encode = (channel: number) => {\n const value = clamp(channel);\n const encoded = value <= 0.0031308 ? value * 12.92 : 1.055 * value ** (1 / 2.4) - 0.055;\n return Math.round(encoded * 255);\n };\n\n return { r: encode(rgb.r), g: encode(rgb.g), b: encode(rgb.b) };\n}\n\n/** Linear-light sRGB from gamma-encoded channels, each 0–255. */\nexport function decodeSrgb(r: number, g: number, b: number): Rgb {\n const decode = (channel: number) => {\n const value = channel / 255;\n return value <= 0.04045 ? value / 12.92 : ((value + 0.055) / 1.055) ** 2.4;\n };\n\n return { r: decode(r), g: decode(g), b: decode(b) };\n}\n\nexport interface Oklch {\n l: number;\n c: number;\n h: number;\n}\n\n/**\n * Linear-light sRGB to OKLCH.\n *\n * The inverse of `oklchToLinearRgb`, needed because people pick colours as hex\n * and the tokens are authored in OKLCH. Round-tripping through this is lossy\n * only where the input is outside the OKLCH gamut the tokens use, which a hex\n * value from a colour input never is.\n */\nexport function linearRgbToOklch(rgb: Rgb): Oklch {\n const lCube = 0.4122214708 * rgb.r + 0.5363325363 * rgb.g + 0.0514459929 * rgb.b;\n const mCube = 0.2119034982 * rgb.r + 0.6806995451 * rgb.g + 0.1073969566 * rgb.b;\n const sCube = 0.0883024619 * rgb.r + 0.2817188376 * rgb.g + 0.6299787005 * rgb.b;\n\n const l_ = Math.cbrt(lCube);\n const m_ = Math.cbrt(mCube);\n const s_ = Math.cbrt(sCube);\n\n const l = 0.2104542553 * l_ + 0.793617785 * m_ - 0.0040720468 * s_;\n const a = 1.9779984951 * l_ - 2.428592205 * m_ + 0.4505937099 * s_;\n const b = 0.0259040371 * l_ + 0.7827717662 * m_ - 0.808675766 * s_;\n\n const c = Math.sqrt(a * a + b * b);\n // atan2 returns (-180, 180]; hues are conventionally [0, 360).\n const h = c < 1e-6 ? 0 : ((Math.atan2(b, a) * 180) / Math.PI + 360) % 360;\n\n return { l, c, h };\n}\n\n/** `#rrggbb` (or `#rgb`) to OKLCH. Undefined for anything else. */\nexport function hexToOklch(hex: string): Oklch | undefined {\n const value = hex.trim().replace(/^#/, \"\");\n const full =\n value.length === 3\n ? value\n .split(\"\")\n .map((digit) => digit + digit)\n .join(\"\")\n : value;\n\n if (!/^[0-9a-f]{6}$/i.test(full)) return undefined;\n\n return linearRgbToOklch(\n decodeSrgb(\n Number.parseInt(full.slice(0, 2), 16),\n Number.parseInt(full.slice(2, 4), 16),\n Number.parseInt(full.slice(4, 6), 16),\n ),\n );\n}\n\nexport function oklchToHex({ l, c, h }: Oklch): string {\n const { r, g, b } = encodeSrgb(oklchToLinearRgb(l, c, h));\n const pair = (channel: number) => channel.toString(16).padStart(2, \"0\");\n return `#${pair(r)}${pair(g)}${pair(b)}`;\n}\n\n/** An OKLCH triple as the tokens write it. */\nexport function formatOklch({ l, c, h }: Oklch): string {\n const round = (value: number, places: number) => Number(value.toFixed(places)).toString();\n return `oklch(${round(l, 3)} ${round(c, 3)} ${round(h, 1)})`;\n}\n","import { contrastRatio, formatOklch, oklchToLinearRgb, type Oklch } from \"./colour\";\n\n/**\n * Deriving a theme preset from a single colour.\n *\n * A preset in this system reassigns four tokens per mode and inherits\n * everything else, so building one is not a palette exercise — it is picking a\n * primary and then answering three questions the shipped presets already\n * answer: what it looks like pressed, what it looks like in dark mode, and what\n * text can be read on it.\n *\n * The deltas below are read off the presets that ship. They are not arbitrary:\n * every one of those passes the contrast audit in both modes, so starting from\n * the same relationships means a derived preset starts somewhere that works.\n */\n\n/** Lightness step from the base colour to its hover state, in light mode. */\nconst LIGHT_HOVER_DELTA = -0.045;\n/** And to its active state, which is a press and reads as further down. */\nconst LIGHT_ACTIVE_DELTA = -0.083;\n\n/**\n * Dark mode raises lightness and drops chroma.\n *\n * A colour that reads as saturated on white reads as glaring on near-black, and\n * one dark enough to sit on white disappears into the background.\n */\nconst DARK_LIGHTNESS_DELTA = 0.115;\nconst DARK_CHROMA_DELTA = -0.015;\nconst DARK_HOVER_DELTA = 0.045;\nconst DARK_ACTIVE_DELTA = -0.045;\n\n/** The near-white the shipped presets use for text on a saturated colour. */\nconst LIGHT_FOREGROUND: Oklch = { l: 0.985, c: 0.002, h: 265 };\n\n/** WCAG 2.2 AA for normal text; a button label is normal text. */\nexport const TEXT_MINIMUM = 4.5;\n\nfunction clampLightness(value: number): number {\n return Math.min(0.99, Math.max(0.01, value));\n}\n\nfunction ratio(a: Oklch, b: Oklch): number {\n return contrastRatio(oklchToLinearRgb(a.l, a.c, a.h), oklchToLinearRgb(b.l, b.c, b.h));\n}\n\n/**\n * Text for a saturated background: near-white, or a dark tint of its own hue.\n *\n * Whichever reads better, rather than always white. A light primary — amber,\n * lime — cannot carry white text at 4.5:1 no matter how it is nudged, and the\n * shipped `amber` preset is dark-on-light for exactly this reason.\n */\nexport function foregroundFor(background: Oklch): Oklch {\n const dark: Oklch = { l: 0.155, c: 0.03, h: background.h };\n return ratio(LIGHT_FOREGROUND, background) >= ratio(dark, background)\n ? LIGHT_FOREGROUND\n : dark;\n}\n\nexport interface PresetMode {\n primary: Oklch;\n primaryHover: Oklch;\n primaryActive: Oklch;\n primaryForeground: Oklch;\n}\n\nexport interface DerivedPreset {\n light: PresetMode;\n dark: PresetMode;\n}\n\nexport interface DeriveOptions {\n /**\n * Lightness of the dark-mode primary.\n *\n * Overridable because it is the one derived value with no single right\n * answer: how bright a brand reads on near-black is a judgement about the\n * brand, not about contrast.\n */\n darkLightness?: number;\n}\n\nexport function derivePreset(input: Oklch, options: DeriveOptions = {}): DerivedPreset {\n // Clamped on the way in as well as on the way out. Every value this returns\n // is one it is responsible for, including the one it was handed: a preset\n // built on pure black is not a preset anyone can use, and passing it through\n // untouched would mean the only unusable value in the output is the one that\n // was never checked.\n const primary: Oklch = { ...input, l: clampLightness(input.l), c: Math.max(0, input.c) };\n\n const light: PresetMode = {\n primary,\n primaryHover: { ...primary, l: clampLightness(primary.l + LIGHT_HOVER_DELTA) },\n primaryActive: { ...primary, l: clampLightness(primary.l + LIGHT_ACTIVE_DELTA) },\n primaryForeground: foregroundFor(primary),\n };\n\n const darkPrimary: Oklch = {\n l: clampLightness(options.darkLightness ?? primary.l + DARK_LIGHTNESS_DELTA),\n c: Math.max(0, primary.c + DARK_CHROMA_DELTA),\n h: primary.h,\n };\n\n const dark: PresetMode = {\n primary: darkPrimary,\n primaryHover: { ...darkPrimary, l: clampLightness(darkPrimary.l + DARK_HOVER_DELTA) },\n primaryActive: { ...darkPrimary, l: clampLightness(darkPrimary.l + DARK_ACTIVE_DELTA) },\n primaryForeground: foregroundFor(darkPrimary),\n };\n\n return { light, dark };\n}\n\nexport interface ContrastCheck {\n label: string;\n ratio: number;\n minimum: number;\n passes: boolean;\n}\n\n/**\n * The pairs a derived preset is responsible for.\n *\n * Only these four per mode: every other pair in the system is inherited from\n * the base tokens, which the audit already covers. Reporting the inherited ones\n * would be reporting on something the person cannot change from here.\n */\nexport function checkPreset(preset: DerivedPreset): ContrastCheck[] {\n const checks: ContrastCheck[] = [];\n\n for (const [mode, values] of [\n [\"Light\", preset.light],\n [\"Dark\", preset.dark],\n ] as const) {\n for (const [state, background] of [\n [\"primary\", values.primary],\n [\"primary-hover\", values.primaryHover],\n [\"primary-active\", values.primaryActive],\n ] as const) {\n checks.push({\n label: `${mode}: primary-foreground on ${state}`,\n ratio: ratio(values.primaryForeground, background),\n minimum: TEXT_MINIMUM,\n passes: ratio(values.primaryForeground, background) >= TEXT_MINIMUM,\n });\n }\n }\n\n return checks;\n}\n\nfunction block(selector: string, mode: PresetMode): string {\n return [\n `${selector} {`,\n ` --primary: ${formatOklch(mode.primary)};`,\n ` --primary-hover: ${formatOklch(mode.primaryHover)};`,\n ` --primary-active: ${formatOklch(mode.primaryActive)};`,\n ` --primary-foreground: ${formatOklch(mode.primaryForeground)};`,\n `}`,\n ].join(\"\\n\");\n}\n\n/**\n * The preset as a stylesheet, in the same shape as the ones that ship.\n *\n * Deliberately the same file format rather than a bespoke export: what comes\n * out of here can be dropped into `packages/themes/src/presets/` unchanged, and\n * is then covered by the same audit as everything else.\n */\nexport function formatPreset(name: string, preset: DerivedPreset): string {\n return [\n `/* Theme preset: ${name}.`,\n ` * Apply with data-theme=\"${name}\" on the <html> element. Only the brand-carrying`,\n ` * tokens are reassigned; every neutral, radius and motion token is inherited. */`,\n \"\",\n block(`[data-theme=\"${name}\"]`, preset.light),\n \"\",\n block(`.dark[data-theme=\"${name}\"]`, preset.dark),\n \"\",\n ].join(\"\\n\");\n}\n\n/** A name usable as a `data-theme` value. */\nexport function slugify(name: string): string {\n const slug = name\n .toLowerCase()\n .replace(/[^a-z0-9]+/g, \"-\")\n .replace(/^-+|-+$/g, \"\");\n return slug.length > 0 ? slug : \"custom\";\n}\n","import { encodeSrgb, oklchToLinearRgb, parseOklch, formatOklch } from \"./colour\";\nimport type { DerivedPreset, PresetMode } from \"./preset\";\n\n/**\n * The tokens, in the shape a design tool reads.\n *\n * The CSS is the source of truth and stays that way — nothing here is a second\n * palette to keep in step. This reads the same `tokens.css`, `base.css` and\n * preset files the components consume, resolves every `var()` the way a\n * browser would, and writes the result as W3C Design Tokens (DTCG): the format\n * Tokens Studio for Figma imports directly, and the one every other design\n * tool is converging on.\n *\n * Colours come out as sRGB hex. Figma has no OKLCH; converting here, with the\n * same maths the contrast audit uses, means the swatch a designer sees is the\n * colour a user gets — rather than whatever a tool makes of an `oklch()` string\n * it cannot parse.\n */\n\n/** A flat map of custom property name (without the leading `--`) to raw value. */\nexport type Declarations = Record<string, string>;\n\n/** A W3C Design Tokens document: nested groups whose leaves carry `$type` and `$value`. */\nexport interface DesignToken {\n $type: \"color\" | \"dimension\" | \"fontFamily\" | \"number\";\n $value: string | number | string[];\n $description?: string;\n}\n\nexport interface DesignTokenGroup {\n [key: string]: DesignToken | DesignTokenGroup | string | undefined;\n $description?: string;\n}\n\n/** Strips block comments, which otherwise hide `--x: y;` pairs inside them from the parser. */\nfunction stripComments(css: string): string {\n return css.replace(/\\/\\*[\\s\\S]*?\\*\\//g, \"\");\n}\n\n/**\n * The declarations inside the first block whose selector is exactly `selector`.\n *\n * Exact, not substring: `.dark` must not match `.dark[data-theme=\"ocean\"]`, and\n * `:root` must not match `:root[dir=\"rtl\"]`. Whitespace inside a value is\n * collapsed, because a font stack written over four lines is one value.\n */\nexport function parseTokenCss(css: string, selector: string): Declarations {\n const clean = stripComments(css);\n const declarations: Declarations = {};\n\n let searchFrom = 0;\n for (;;) {\n const brace = clean.indexOf(\"{\", searchFrom);\n if (brace === -1) break;\n\n // The selector is whatever sits between the previous `}` (or `;`, or the\n // start) and this `{`.\n const start = Math.max(\n clean.lastIndexOf(\"}\", brace),\n clean.lastIndexOf(\";\", brace),\n clean.lastIndexOf(\"{\", brace - 1),\n );\n const candidate = clean.slice(start + 1, brace).trim();\n const end = clean.indexOf(\"}\", brace);\n if (end === -1) break;\n\n if (candidate === selector) {\n const body = clean.slice(brace + 1, end);\n for (const match of body.matchAll(/--([\\w-]+)\\s*:\\s*([^;]+);/g)) {\n const name = match[1];\n const value = match[2];\n if (name && value) declarations[name] = value.replace(/\\s+/g, \" \").trim();\n }\n return declarations;\n }\n\n searchFrom = end + 1;\n }\n\n return declarations;\n}\n\n/**\n * Resolves `var(--x)` references the way the cascade would.\n *\n * `scopes` are searched in order, so a mode's own declarations shadow the root's\n * and the root's shadow the raw scale — which is exactly what `.dark { --x }`\n * over `:root { --x }` over `@theme { --x }` means. A fallback inside the\n * `var()` is used when nothing defines the name, and an unresolvable reference\n * is left as written rather than silently dropped.\n */\nexport function resolveReferences(value: string, scopes: Declarations[]): string {\n let current = value;\n // Bounded, so a cycle terminates with the reference left in place rather\n // than hanging the build.\n for (let depth = 0; depth < 16; depth += 1) {\n const next = current.replace(\n /var\\(\\s*--([\\w-]+)\\s*(?:,\\s*([^()]*(?:\\([^()]*\\))?[^()]*))?\\)/g,\n (whole, name: string, fallback: string | undefined) => {\n for (const scope of scopes) {\n const found = scope[name];\n if (found !== undefined) return found;\n }\n return fallback?.trim() ?? whole;\n },\n );\n if (next === current) return current;\n current = next;\n }\n return current;\n}\n\n/** `oklch(...)` to `#rrggbb`, or `#rrggbbaa` when it carries alpha. */\nexport function cssColourToHex(value: string): string | undefined {\n const parsed = parseOklch(value.trim());\n if (!parsed) return undefined;\n\n const { r, g, b } = encodeSrgb(oklchToLinearRgb(parsed.l, parsed.c, parsed.h));\n const pair = (channel: number) => channel.toString(16).padStart(2, \"0\");\n const rgb = `#${pair(r)}${pair(g)}${pair(b)}`;\n return parsed.alpha >= 1 ? rgb : `${rgb}${pair(Math.round(parsed.alpha * 255))}`;\n}\n\nconst ROOT_FONT_PX = 16;\n\n/**\n * A length in px, for the values the scale is written in.\n *\n * Handles the two shapes the tokens use: a plain `rem`/`px`, and the radius\n * ladder's `calc(<rem> * var(--radius-scale, 1))`, which is resolved with the\n * given scale so an exported theme carries the corner radius it was designed\n * with rather than a formula Figma cannot evaluate.\n */\nexport function cssLengthToPx(value: string, radiusScale = 1): number | undefined {\n const trimmed = value.trim();\n\n const calc = /^calc\\(\\s*([\\d.]+)rem\\s*\\*\\s*var\\(--radius-scale(?:,\\s*[\\d.]+)?\\)\\s*\\)$/.exec(\n trimmed,\n );\n if (calc?.[1]) return round(Number.parseFloat(calc[1]) * ROOT_FONT_PX * radiusScale);\n\n const rem = /^([\\d.]+)rem$/.exec(trimmed);\n if (rem?.[1]) return round(Number.parseFloat(rem[1]) * ROOT_FONT_PX);\n\n const px = /^([\\d.]+)px$/.exec(trimmed);\n if (px?.[1]) return round(Number.parseFloat(px[1]));\n\n return undefined;\n}\n\nfunction round(value: number): number {\n return Math.round(value * 100) / 100;\n}\n\nfunction px(value: number): string {\n return `${String(value)}px`;\n}\n\n/** The four tokens a derived preset owns, as declarations, per mode. */\nexport function presetDeclarations(preset: DerivedPreset): {\n light: Declarations;\n dark: Declarations;\n} {\n const mode = (entry: PresetMode): Declarations => ({\n primary: formatOklch(entry.primary),\n \"primary-hover\": formatOklch(entry.primaryHover),\n \"primary-active\": formatOklch(entry.primaryActive),\n \"primary-foreground\": formatOklch(entry.primaryForeground),\n });\n return { light: mode(preset.light), dark: mode(preset.dark) };\n}\n\nexport interface DesignTokensInput {\n /** Named in the document, e.g. \"ocean\" or a studio preset's slug. */\n name: string;\n /** The `@theme` block of tokens.css: the raw scales. */\n scale: Declarations;\n /** The `:root` block of base.css. */\n light: Declarations;\n /** The `.dark` block of base.css. */\n dark: Declarations;\n /** A preset's overrides, layered over `light` and `dark`. */\n preset?: { light: Declarations; dark: Declarations };\n /** The `--radius-scale` the theme was designed at. */\n radiusScale?: number;\n}\n\nfunction colourGroup(\n declarations: Declarations,\n scopes: Declarations[],\n filter: (name: string) => boolean,\n): DesignTokenGroup {\n const group: DesignTokenGroup = {};\n for (const [name, raw] of Object.entries(declarations)) {\n if (!filter(name)) continue;\n const hex = cssColourToHex(resolveReferences(raw, scopes));\n if (hex) group[name] = { $type: \"color\", $value: hex };\n }\n return group;\n}\n\n/** `--color-neutral-500` → `neutral.500`, nested. */\nfunction scaleColours(scale: Declarations): DesignTokenGroup {\n const group: DesignTokenGroup = {};\n for (const [name, raw] of Object.entries(scale)) {\n const match = /^color-([a-z]+)-(\\d+)$/.exec(name);\n if (!match) continue;\n const [, family, step] = match;\n if (!family || !step) continue;\n const hex = cssColourToHex(raw);\n if (!hex) continue;\n const familyGroup = (group[family] ??= {}) as DesignTokenGroup;\n familyGroup[step] = { $type: \"color\", $value: hex };\n }\n return group;\n}\n\nfunction radii(scale: Declarations, radiusScale: number): DesignTokenGroup {\n const group: DesignTokenGroup = {};\n for (const [name, raw] of Object.entries(scale)) {\n const match = /^radius-([\\w]+)$/.exec(name);\n if (!match?.[1]) continue;\n const length = cssLengthToPx(raw, radiusScale);\n if (length !== undefined) group[match[1]] = { $type: \"dimension\", $value: px(length) };\n }\n return group;\n}\n\nfunction typography(scale: Declarations): DesignTokenGroup {\n const family: DesignTokenGroup = {};\n for (const [name, raw] of Object.entries(scale)) {\n const match = /^font-([a-z]+)$/.exec(name);\n if (!match?.[1]) continue;\n family[match[1]] = {\n $type: \"fontFamily\",\n $value: raw.split(\",\").map((entry) => entry.trim().replace(/^[\"']|[\"']$/g, \"\")),\n };\n }\n\n const size: DesignTokenGroup = {};\n for (const [name, raw] of Object.entries(scale)) {\n const step = /^text-([\\w]+)$/.exec(name)?.[1];\n if (!step) continue;\n const fontSize = cssLengthToPx(raw);\n if (fontSize === undefined) continue;\n const entry: DesignTokenGroup = { size: { $type: \"dimension\", $value: px(fontSize) } };\n const lineHeight = scale[`text-${step}--line-height`];\n const lineHeightPx = lineHeight === undefined ? undefined : cssLengthToPx(lineHeight);\n if (lineHeightPx !== undefined) {\n entry.lineHeight = { $type: \"dimension\", $value: px(lineHeightPx) };\n }\n size[step] = entry;\n }\n\n return { family, text: size };\n}\n\n/**\n * The whole theme, as a design-tokens document.\n *\n * Three sets: `core` (the raw scales, the same in both modes), `light` and\n * `dark` (the semantic colours, resolved). A designer enables `core` plus one\n * mode, which mirrors exactly how the CSS composes.\n */\nexport function toDesignTokens(input: DesignTokensInput): DesignTokenGroup {\n const radiusScale = input.radiusScale ?? 1;\n const light: Declarations = { ...input.light, ...input.preset?.light };\n const dark: Declarations = { ...input.dark, ...input.preset?.dark };\n\n const semantic = (name: string) => name !== \"radius-scale\" && !name.startsWith(\"color-\");\n\n return {\n $description: `Dowel design tokens, preset \"${input.name}\". Generated from the CSS the components use; colours are sRGB hex converted from OKLCH.`,\n core: {\n color: scaleColours(input.scale),\n radius: radii(input.scale, radiusScale),\n font: typography(input.scale),\n },\n light: {\n color: colourGroup(light, [light, input.scale], semantic),\n },\n dark: {\n // Dark declares only what differs; the rest inherits from the root.\n color: colourGroup({ ...light, ...dark }, [dark, light, input.scale], semantic),\n },\n };\n}\n","export {\n checkPreset,\n derivePreset,\n foregroundFor,\n formatPreset,\n slugify,\n TEXT_MINIMUM,\n type ContrastCheck,\n type DerivedPreset,\n type DeriveOptions,\n type PresetMode,\n} from \"./preset\";\n\nexport {\n cssColourToHex,\n cssLengthToPx,\n parseTokenCss,\n presetDeclarations,\n resolveReferences,\n toDesignTokens,\n type Declarations,\n type DesignToken,\n type DesignTokenGroup,\n type DesignTokensInput,\n} from \"./figma\";\n\nexport {\n composite,\n contrastRatio,\n decodeSrgb,\n encodeSrgb,\n formatOklch,\n hexToOklch,\n linearRgbToOklch,\n luminance,\n oklchToHex,\n oklchToLinearRgb,\n parseOklch,\n resolveColour,\n type Oklch,\n type Rgb,\n} from \"./colour\";\n\n/**\n * Typed surface of the theme layer. The CSS is the implementation; these\n * constants exist so theme switchers, the docs playground and the future CLI\n * all agree on the same vocabulary.\n */\n\nexport const THEME_PRESETS = [\n \"default\",\n \"ocean\",\n \"emerald\",\n \"violet\",\n \"rose\",\n \"amber\",\n \"monochrome\",\n \"candy\",\n \"indigo\",\n \"blue\",\n \"red\",\n \"orange\",\n \"green\",\n] as const;\n\nexport type ThemePreset = (typeof THEME_PRESETS)[number];\n\nexport const COLOR_MODES = [\"light\", \"dark\", \"system\"] as const;\n\nexport type ColorMode = (typeof COLOR_MODES)[number];\n\n/** Attribute set on the <html> element to activate a preset. */\nexport const THEME_ATTRIBUTE = \"data-theme\";\n\n/** Class toggled on the <html> element for dark mode. */\nexport const DARK_CLASS = \"dark\";\n\n/**\n * Custom property that re-proportions every radius token at once.\n * `1` is the designed default; `0` gives fully square corners.\n */\nexport const RADIUS_SCALE_PROPERTY = \"--radius-scale\";\n\nexport function isThemePreset(value: string): value is ThemePreset {\n return (THEME_PRESETS as readonly string[]).includes(value);\n}\n\nexport function isColorMode(value: string): value is ColorMode {\n return (COLOR_MODES as readonly string[]).includes(value);\n}\n"],"mappings":"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"}
package/package.json CHANGED
@@ -1,8 +1,8 @@
1
1
  {
2
2
  "name": "@dowel-ui/themes",
3
- "version": "0.7.0",
3
+ "version": "0.8.0",
4
4
  "type": "module",
5
- "description": "Two-tier OKLCH design tokens in plain CSS for Dowel, with seven presets that all pass WCAG AA contrast in light and dark.",
5
+ "description": "Two-tier OKLCH design tokens in plain CSS for Dowel, with thirteen presets that all pass WCAG AA contrast in light and dark.",
6
6
  "keywords": [
7
7
  "css",
8
8
  "design-tokens",
@@ -55,7 +55,7 @@
55
55
  "tsdown": "0.22.14",
56
56
  "typescript": "6.0.3",
57
57
  "vitest": "4.1.10",
58
- "@dowel-ui/config": "0.7.0"
58
+ "@dowel-ui/config": "0.8.0"
59
59
  },
60
60
  "scripts": {
61
61
  "build": "tsdown",
package/src/index.ts CHANGED
@@ -55,6 +55,12 @@ export const THEME_PRESETS = [
55
55
  "rose",
56
56
  "amber",
57
57
  "monochrome",
58
+ "candy",
59
+ "indigo",
60
+ "blue",
61
+ "red",
62
+ "orange",
63
+ "green",
58
64
  ] as const;
59
65
 
60
66
  export type ThemePreset = (typeof THEME_PRESETS)[number];
@@ -0,0 +1,30 @@
1
+ /* Theme preset: blue.
2
+ * Apply with data-theme="blue" on the <html> element. Only the brand-carrying
3
+ * tokens are reassigned; every neutral, radius and motion token is inherited.
4
+ *
5
+ * From SmoothUI's Blue theme (MIT © 2024 Eduardo Calvo): brand
6
+ * oklch(0.67 0.17 257.78), brand-secondary oklch(0.59 0.21 258.02). As in every
7
+ * SmoothUI-derived preset, the brand is --primary and the secondary — the deep
8
+ * end of SmoothUI's gradients — is --primary-active.
9
+ *
10
+ * Hue 258 sits between `ocean` (232, a cyan-leaning blue) and the default
11
+ * (275, an indigo): this is the plain blue neither of them is. */
12
+
13
+ [data-theme="blue"] {
14
+ /* The brand is 2.90:1 under white text and 3.03:1 as text on white. 0.555 is
15
+ the lightest value that clears 4.5:1 for both; hue and chroma are kept.
16
+ The secondary's chroma is capped at 0.175, the most sRGB holds at 0.472. */
17
+ --primary: oklch(0.555 0.17 257.78);
18
+ --primary-hover: oklch(0.51 0.17 257.78);
19
+ --primary-active: oklch(0.472 0.175 258.02);
20
+ --primary-foreground: oklch(0.985 0.002 265);
21
+ }
22
+
23
+ .dark[data-theme="blue"] {
24
+ /* SmoothUI's exact values: 6.5:1 under dark text on the brand, 4.6:1 on the
25
+ secondary. Hover is capped at the chroma sRGB holds at 0.715. */
26
+ --primary: oklch(0.67 0.17 257.78);
27
+ --primary-hover: oklch(0.715 0.145 257.78);
28
+ --primary-active: oklch(0.59 0.21 258.02);
29
+ --primary-foreground: oklch(0.155 0.03 257.78);
30
+ }
@@ -0,0 +1,37 @@
1
+ /* Theme preset: candy.
2
+ * Apply with data-theme="candy" on the <html> element. Only the brand-carrying
3
+ * tokens are reassigned; every neutral, radius and motion token is inherited.
4
+ *
5
+ * From SmoothUI's Candy theme (MIT © 2024 Eduardo Calvo). Candy is two colours,
6
+ * not one: a pink brand, oklch(0.72 0.2 352.53), and a deeper brand-secondary,
7
+ * oklch(0.66 0.21 354.31), that SmoothUI uses as the far end of its gradients.
8
+ * Dowel has one brand colour and no gradient token, so the pair is mapped onto
9
+ * the states it already has: the brand is --primary, and the secondary is
10
+ * --primary-active — pressing moves further down the same gradient. Hover
11
+ * follows the brand hue. */
12
+
13
+ [data-theme="candy"] {
14
+ /* SmoothUI's pink is 2.62:1 under white text and 2.74:1 as text on white —
15
+ short of 4.5:1 twice, and this token is the focus ring too. Lightness is
16
+ brought down to 0.58, the lightest value that clears 4.5:1 for both, with
17
+ hue and chroma untouched. The active state carries brand-secondary's hue
18
+ at the standard step down, its chroma capped at 0.2, the most sRGB holds
19
+ at 0.497. */
20
+ --primary: oklch(0.58 0.2 352.53);
21
+ --primary-hover: oklch(0.535 0.2 352.53);
22
+ --primary-active: oklch(0.497 0.2 354.31);
23
+ --primary-foreground: oklch(0.985 0.002 265);
24
+ }
25
+
26
+ .dark[data-theme="candy"] {
27
+ /* SmoothUI's exact values. Its brand does not change between modes, and on
28
+ near-black both of them already carry dark text: 7.2:1 on the brand, 5.7:1
29
+ on the secondary. Hover is capped at chroma 0.16, the most sRGB holds at
30
+ that lightness. Near `rose`, but not the same colour: rose is a red at hue
31
+ 17 and a dark primary of 0.645; candy is a cooler pink, 25° away, and far
32
+ brighter in dark mode. */
33
+ --primary: oklch(0.72 0.2 352.53);
34
+ --primary-hover: oklch(0.765 0.16 352.53);
35
+ --primary-active: oklch(0.66 0.21 354.31);
36
+ --primary-foreground: oklch(0.155 0.03 352.53);
37
+ }
@@ -0,0 +1,34 @@
1
+ /* Theme preset: green.
2
+ * Apply with data-theme="green" on the <html> element. Only the brand-carrying
3
+ * tokens are reassigned; every neutral, radius and motion token is inherited.
4
+ *
5
+ * From SmoothUI's Green theme (MIT © 2024 Eduardo Calvo): brand
6
+ * oklch(0.7 0.15 162.48), brand-secondary oklch(0.6 0.13 163.23). As in every
7
+ * SmoothUI-derived preset, the brand is --primary and the secondary — the deep
8
+ * end of SmoothUI's gradients — is --primary-active.
9
+ *
10
+ * The closest of the six to an existing preset: `emerald` is at hue 162 too,
11
+ * and in light mode the two are nearly the same colour, because contrast
12
+ * pushes both to the same place. The difference is dark mode, where green is
13
+ * SmoothUI's bright mint at 0.7/0.15 and emerald a deeper 0.645/0.12. */
14
+
15
+ [data-theme="green"] {
16
+ /* The brand is 2.39:1 under white text and 2.50:1 as text on white. 0.54 is
17
+ the lightest value that clears 4.5:1 for both. Green is where sRGB is
18
+ narrowest at that lightness: it holds 0.115 chroma, not SmoothUI's 0.15,
19
+ so that is what is written — the value the screen can show. Hover and
20
+ the secondary are capped the same way, at 0.105 and 0.095. */
21
+ --primary: oklch(0.54 0.115 162.48);
22
+ --primary-hover: oklch(0.495 0.105 162.48);
23
+ --primary-active: oklch(0.457 0.095 163.23);
24
+ --primary-foreground: oklch(0.985 0.002 265);
25
+ }
26
+
27
+ .dark[data-theme="green"] {
28
+ /* SmoothUI's brand exactly: 7.8:1 under dark text. The secondary's chroma is
29
+ 0.125, just inside sRGB where SmoothUI's 0.13 is not; 5.2:1. */
30
+ --primary: oklch(0.7 0.15 162.48);
31
+ --primary-hover: oklch(0.745 0.15 162.48);
32
+ --primary-active: oklch(0.6 0.125 163.23);
33
+ --primary-foreground: oklch(0.155 0.03 162.48);
34
+ }
@@ -5,3 +5,9 @@
5
5
  @import "./rose.css";
6
6
  @import "./amber.css";
7
7
  @import "./monochrome.css";
8
+ @import "./candy.css";
9
+ @import "./indigo.css";
10
+ @import "./blue.css";
11
+ @import "./red.css";
12
+ @import "./orange.css";
13
+ @import "./green.css";
@@ -0,0 +1,36 @@
1
+ /* Theme preset: indigo.
2
+ * Apply with data-theme="indigo" on the <html> element. Only the brand-carrying
3
+ * tokens are reassigned; every neutral, radius and motion token is inherited.
4
+ *
5
+ * From SmoothUI's Indigo theme (MIT © 2024 Eduardo Calvo): brand
6
+ * oklch(0.65 0.22 300.21), brand-secondary oklch(0.54 0.23 286.53). As in every
7
+ * SmoothUI-derived preset, the brand is --primary and the secondary — the deep
8
+ * end of SmoothUI's gradients — is --primary-active.
9
+ *
10
+ * Next to `violet` (hue 302), the brand hue is almost the same: SmoothUI's
11
+ * "indigo" is a violet in OKLCH terms, and the indigo is its secondary, 14°
12
+ * bluer. What separates the two presets is that pressing indigo turns to that
13
+ * true indigo, and that its dark mode keeps the full 0.22 chroma where violet
14
+ * drops to 0.175. */
15
+
16
+ [data-theme="indigo"] {
17
+ /* The brand is 3.41:1 under white text and 3.56:1 as text on white. 0.58 is
18
+ the lightest value that clears 4.5:1 for both; hue and chroma are kept. */
19
+ --primary: oklch(0.58 0.22 300.21);
20
+ --primary-hover: oklch(0.535 0.22 300.21);
21
+ --primary-active: oklch(0.497 0.23 286.53);
22
+ --primary-foreground: oklch(0.985 0.002 265);
23
+ }
24
+
25
+ .dark[data-theme="indigo"] {
26
+ /* The brand is SmoothUI's exact value: 5.5:1 under dark text. The secondary
27
+ is not. One foreground serves every state, so the pressed fill has to
28
+ carry the same dark text, and at 0.54 it reaches only 3.49:1. It is raised
29
+ to 0.605, the first lightness that clears 4.5:1, keeping its hue; its
30
+ chroma is capped at 0.225 and hover's at 0.185, the most sRGB holds at
31
+ those lightnesses. */
32
+ --primary: oklch(0.65 0.22 300.21);
33
+ --primary-hover: oklch(0.695 0.185 300.21);
34
+ --primary-active: oklch(0.605 0.225 286.53);
35
+ --primary-foreground: oklch(0.155 0.03 300.21);
36
+ }
@@ -0,0 +1,34 @@
1
+ /* Theme preset: orange.
2
+ * Apply with data-theme="orange" on the <html> element. Only the brand-carrying
3
+ * tokens are reassigned; every neutral, radius and motion token is inherited.
4
+ *
5
+ * From SmoothUI's Orange theme (MIT © 2024 Eduardo Calvo): brand
6
+ * oklch(0.75 0.17 47.65), brand-secondary oklch(0.68 0.21 40.59). As in every
7
+ * SmoothUI-derived preset, the brand is --primary and the secondary — the deep
8
+ * end of SmoothUI's gradients — is --primary-active.
9
+ *
10
+ * Not `amber`: amber sits at hue 70 and in light mode is an ochre; orange is
11
+ * 22° redder, and in light mode reads as a burnt orange rather than a brown. */
12
+
13
+ [data-theme="orange"] {
14
+ /* The largest adjustment of the six. SmoothUI's orange is 2.28:1 under white
15
+ text and 2.38:1 as text on white; like amber, a bright orange cannot be
16
+ read on a light background at all. 0.565 is the lightest value that clears
17
+ 4.5:1 for both, and at that lightness sRGB holds only 0.15 chroma, so that
18
+ is what is written — the value the screen can show. Hover and the
19
+ secondary are capped the same way, at 0.14 and 0.145. */
20
+ --primary: oklch(0.565 0.15 47.65);
21
+ --primary-hover: oklch(0.52 0.14 47.65);
22
+ --primary-active: oklch(0.482 0.145 40.59);
23
+ --primary-foreground: oklch(0.985 0.002 265);
24
+ }
25
+
26
+ .dark[data-theme="orange"] {
27
+ /* SmoothUI's values, with the brand's chroma at 0.16: 0.17 is just outside
28
+ sRGB at this lightness. 8.4:1 under dark text on the brand, 6.3:1 on the
29
+ secondary. Hover is capped at the chroma sRGB holds at 0.795. */
30
+ --primary: oklch(0.75 0.16 47.65);
31
+ --primary-hover: oklch(0.795 0.125 47.65);
32
+ --primary-active: oklch(0.68 0.21 40.59);
33
+ --primary-foreground: oklch(0.155 0.03 47.65);
34
+ }
@@ -0,0 +1,33 @@
1
+ /* Theme preset: red.
2
+ * Apply with data-theme="red" on the <html> element. Only the brand-carrying
3
+ * tokens are reassigned; every neutral, radius and motion token is inherited.
4
+ *
5
+ * From SmoothUI's Red theme (MIT © 2024 Eduardo Calvo): brand
6
+ * oklch(0.67 0.21 24.28), brand-secondary oklch(0.62 0.25 28.23). As in every
7
+ * SmoothUI-derived preset, the brand is --primary and the secondary — the deep
8
+ * end of SmoothUI's gradients — is --primary-active.
9
+ *
10
+ * Near `rose`, but warmer: rose sits at hue 17 and leans pink; red is at 24
11
+ * and presses towards 28, a vermilion. In dark mode red is also brighter and
12
+ * more saturated — 0.67/0.21 against rose's 0.645/0.17. Worth knowing: the
13
+ * brand is now close to `--destructive`, so a primary and a destructive button
14
+ * side by side are told apart by their labels rather than their colour. */
15
+
16
+ [data-theme="red"] {
17
+ /* The brand is 3.16:1 under white text and 3.30:1 as text on white. 0.58 is
18
+ the lightest value that clears 4.5:1 for both; hue and chroma are kept.
19
+ The secondary's chroma is capped at 0.2, the most sRGB holds at 0.497. */
20
+ --primary: oklch(0.58 0.21 24.28);
21
+ --primary-hover: oklch(0.535 0.21 24.28);
22
+ --primary-active: oklch(0.497 0.2 28.23);
23
+ --primary-foreground: oklch(0.985 0.002 265);
24
+ }
25
+
26
+ .dark[data-theme="red"] {
27
+ /* SmoothUI's exact values: 6.0:1 under dark text on the brand, 4.8:1 on the
28
+ secondary. Hover is capped at the chroma sRGB holds at 0.715. */
29
+ --primary: oklch(0.67 0.21 24.28);
30
+ --primary-hover: oklch(0.715 0.175 24.28);
31
+ --primary-active: oklch(0.62 0.25 28.23);
32
+ --primary-foreground: oklch(0.155 0.03 24.28);
33
+ }