Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions apps/app/src/app.css
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@

* {
scrollbar-width: thin;
scrollbar-color: color-mix(in oklch, var(--foreground) 20%, transparent)
scrollbar-color: color-mix(in oklab, var(--foreground) 20%, transparent)
transparent;
}

Expand All @@ -52,12 +52,12 @@
}

*::-webkit-scrollbar-thumb {
background-color: color-mix(in oklch, var(--foreground) 20%, transparent);
background-color: color-mix(in oklab, var(--foreground) 20%, transparent);
border-radius: 9999px;
}

*::-webkit-scrollbar-thumb:hover {
background-color: color-mix(in oklch, var(--foreground) 40%, transparent);
background-color: color-mix(in oklab, var(--foreground) 40%, transparent);
}

/*
Expand Down
36 changes: 18 additions & 18 deletions apps/app/src/components/ui/theme.css
Original file line number Diff line number Diff line change
Expand Up @@ -348,8 +348,8 @@
* reads consistently on canvas, cards, and recessed surfaces alike. Over the
* canvas these match an opaque ink-over-canvas mix exactly; on elevated
* surfaces they composite for real, visible contrast. */
--state-hover: color-mix(in oklch, var(--ink) 5.9%, transparent);
--state-active: color-mix(in oklch, var(--ink) 11.8%, transparent);
--state-hover: color-mix(in oklab, var(--ink) 5.9%, transparent);
--state-active: color-mix(in oklab, var(--ink) 11.8%, transparent);
--destructive: oklch(0.45 0.19 25.8625);
--destructive-foreground: oklch(1 0 0);
/* Text-only destructive. Light is identical to --destructive (8.04:1); the
Expand Down Expand Up @@ -377,22 +377,22 @@
--ring: var(--primary);
/* Inset panel tints on the ink ramp: recessed a clear sunken step, raised a
* faint lift, both translucent so they read on card or page alike. */
--surface-recessed: color-mix(in oklch, var(--ink) 6%, transparent);
--surface-raised: color-mix(in oklch, var(--ink) 2.5%, transparent);
--surface-scrim: color-mix(in oklch, var(--canvas) 92%, transparent);
--surface-recessed: color-mix(in oklab, var(--ink) 6%, transparent);
--surface-raised: color-mix(in oklab, var(--ink) 2.5%, transparent);
--surface-scrim: color-mix(in oklab, var(--canvas) 92%, transparent);
--surface-destructive: color-mix(
in oklch,
in oklab,
var(--destructive) 6%,
transparent
);
--surface-destructive-border: color-mix(
in oklch,
in oklab,
var(--destructive) 25%,
transparent
);
--surface-selected: color-mix(in oklch, var(--primary) 16%, transparent);
--surface-selected: color-mix(in oklab, var(--primary) 16%, transparent);
--surface-selected-border: color-mix(
in oklch,
in oklab,
var(--primary) 35%,
transparent
);
Expand Down Expand Up @@ -520,8 +520,8 @@
--file-accent: oklch(0.72 0.09 250);
/* Interactive-state fills: translucent ink so the same hover/active step
* reads consistently on canvas, cards, and recessed surfaces alike. */
--state-hover: color-mix(in oklch, var(--ink) 13.8%, transparent);
--state-active: color-mix(in oklch, var(--ink) 22.5%, transparent);
--state-hover: color-mix(in oklab, var(--ink) 13.8%, transparent);
--state-active: color-mix(in oklab, var(--ink) 22.5%, transparent);
--destructive: oklch(0.56 0.19 22.1703);
--destructive-foreground: oklch(1 0 0);
/* Lighter/less-saturated than the fill so error TEXT clears AA (~4.8:1) on the
Expand All @@ -545,22 +545,22 @@
--input: color-mix(in oklch, var(--ink) 32.6%, var(--canvas));
--ring: var(--primary);
/* Same model as light; ink is light here, so these lift the dark canvas. */
--surface-recessed: color-mix(in oklch, var(--ink) 6%, transparent);
--surface-raised: color-mix(in oklch, var(--ink) 2.5%, transparent);
--surface-scrim: color-mix(in oklch, var(--canvas) 92%, transparent);
--surface-recessed: color-mix(in oklab, var(--ink) 6%, transparent);
--surface-raised: color-mix(in oklab, var(--ink) 2.5%, transparent);
--surface-scrim: color-mix(in oklab, var(--canvas) 92%, transparent);
--surface-destructive: color-mix(
in oklch,
in oklab,
var(--destructive) 8%,
transparent
);
--surface-destructive-border: color-mix(
in oklch,
in oklab,
var(--destructive) 30%,
transparent
);
--surface-selected: color-mix(in oklch, var(--primary) 12%, transparent);
--surface-selected: color-mix(in oklab, var(--primary) 12%, transparent);
--surface-selected-border: color-mix(
in oklch,
in oklab,
var(--primary) 35%,
transparent
);
Expand Down
25 changes: 21 additions & 4 deletions apps/app/src/components/ui/theme.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,13 +26,14 @@ function modeBlock(scheme: "light" | "dark"): string {

/**
* token -> ink mix percentage, for tokens derived from the anchors. The base is
* either the canvas (opaque steps) or `transparent` (translucent interactive/
* overlay steps); over the canvas both resolve to the same step, so the mix
* percentage is the comparable "contrast from canvas" either way.
* either the canvas (opaque steps, mixed in oklch) or `transparent` (translucent
* interactive/overlay steps, mixed in oklab — see the guard below); over the
* canvas both resolve to the same step, so the mix percentage is the comparable
* "contrast from canvas" either way.
*/
function rampSteps(block: string): Map<string, number> {
const re =
/--([a-z-]+):\s*color-mix\(in oklch, var\(--ink\) ([\d.]+)%, (?:var\(--canvas\)|transparent)\);/g;
/--([a-z-]+):\s*color-mix\(in okl(?:ch|ab), var\(--ink\) ([\d.]+)%, (?:var\(--canvas\)|transparent)\);/g;
const steps = new Map<string, number>();
for (const match of block.matchAll(re)) {
steps.set(match[1], Number(match[2]));
Expand Down Expand Up @@ -197,6 +198,22 @@ describe("theme.css neutral ramp", () => {
const dark = [...rampSteps(modeBlock("dark")).keys()].sort();
expect(light).toEqual(dark);
});

it("derives translucent (transparent-mixed) tokens in oklab, not oklch", () => {
// Mixing a color with `transparent` in a *polar* space (oklch) drops the
// result hue to `none`, which renders as hue 0 (red). The chroma survives,
// so any palette whose canvas/ink/primary isn't pure gray got a pink-tinted
// header (--surface-scrim), hover, and selection — the default palette only
// escaped because its anchors are chroma-0. Rectangular spaces (oklab) carry
// the hue through, so translucency must mix in oklab. Opaque color->canvas
// mixes can stay oklch. This guard keeps every future palette correct by
// construction, since palettes only set opaque anchors and never touch these
// derived tokens.
const offenders = [
...css.matchAll(/color-mix\(\s*in oklch\b[^;]*?\btransparent\b/g),
].map((match) => match[0].replace(/\s+/g, " "));
expect(offenders).toEqual([]);
});
});

describe("theme.css Cadence text tokens", () => {
Expand Down
Loading