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
51 changes: 46 additions & 5 deletions .agents/skills/codeatlas-ui/API.md
Original file line number Diff line number Diff line change
Expand Up @@ -3831,7 +3831,48 @@ function applyMove<T>(lists: Record<string, T[]>, e: DndDropEvent, key: (item: T
`import { BUILTIN_PRESETS } from "@codeatlasdev/ui"`

```ts
const BUILTIN_PRESETS: BuiltinPresetSource[]
const BUILTIN_PRESETS: {
0: { readonly id: "amber"; readonly label: "Amber"; readonly color: "#c47a15"; readonly hue: 55; reado…;
1: { readonly id: "ocean"; readonly label: "Ocean"; readonly color: "#2b5fd9"; readonly hue: 260; read…;
2: { readonly id: "crimson"; readonly label: "Crimson"; readonly color: "#c00020"; readonly hue: 26; r…;
3: { readonly id: "cyan"; readonly label: "Cyan"; readonly color: "#0094a8"; readonly hue: 193; readon…;
4: { readonly id: "violet"; readonly label: "Violet"; readonly color: "#8b3ec8"; readonly hue: 295; re…;
5: { readonly id: "emerald"; readonly label: "Emerald"; readonly color: "#1a7a50"; readonly hue: 152; …;
6: { readonly id: "rose"; readonly label: "Rose"; readonly color: "#c8405a"; readonly hue: 350; readon…;
7: { readonly id: "slate"; readonly label: "Slate"; readonly color: "#3c5a8a"; readonly hue: 220; read…;
length: 8;
toString: () => string;
toLocaleString: { (): string; (locales: string | string[], options?: Intl.NumberFormatOptions & Intl.DateTimeFormat…;
concat: { (...items: ConcatArray<{ readonly id: "amber"; readonly label: "Amber"; readonly color: "#c47a15"…;
join: (separator?: string) => string;
slice: (start?: number, end?: number) => ({ readonly id: "amber"; readonly label: "Amber"; readonly color:…;
indexOf: (searchElement: { readonly id: "amber"; readonly label: "Amber"; readonly color: "#c47a15"; readonl…;
lastIndexOf: (searchElement: { readonly id: "amber"; readonly label: "Amber"; readonly color: "#c47a15"; readonl…;
every: { <S extends { readonly id: "amber"; readonly label: "Amber"; readonly color: "#c47a15"; readonly h…;
some: (predicate: (value: { readonly id: "amber"; readonly label: "Amber"; readonly color: "#c47a15"; rea…;
forEach: (callbackfn: (value: { readonly id: "amber"; readonly label: "Amber"; readonly color: "#c47a15"; re…;
map: <U>(callbackfn: (value: { readonly id: "amber"; readonly label: "Amber"; readonly color: "#c47a15";…;
filter: { <S extends { readonly id: "amber"; readonly label: "Amber"; readonly color: "#c47a15"; readonly h…;
reduce: { (callbackfn: (previousValue: { readonly id: "amber"; readonly label: "Amber"; readonly color: "#c…;
reduceRight: { (callbackfn: (previousValue: { readonly id: "amber"; readonly label: "Amber"; readonly color: "#c…;
find: { <S extends { readonly id: "amber"; readonly label: "Amber"; readonly color: "#c47a15"; readonly h…;
findIndex: (predicate: (value: { readonly id: "amber"; readonly label: "Amber"; readonly color: "#c47a15"; rea…;
entries: () => ArrayIterator<[number, { readonly id: "amber"; readonly label: "Amber"; readonly color: "#c47…;
keys: () => ArrayIterator<number>;
values: () => ArrayIterator<{ readonly id: "amber"; readonly label: "Amber"; readonly color: "#c47a15"; rea…;
includes: (searchElement: { readonly id: "amber"; readonly label: "Amber"; readonly color: "#c47a15"; readonl…;
flatMap: <U, This = undefined>(callback: (this: This, value: { readonly id: "amber"; readonly label: "Amber"…;
flat: <A, D extends number = 1>(this: A, depth?: D | undefined) => FlatArray<A, D>[];
at: (index: number) => { readonly id: "amber"; readonly label: "Amber"; readonly color: "#c47a15"; read…;
findLast: { <S extends { readonly id: "amber"; readonly label: "Amber"; readonly color: "#c47a15"; readonly h…;
findLastIndex: (predicate: (value: { readonly id: "amber"; readonly label: "Amber"; readonly color: "#c47a15"; rea…;
toReversed: () => ({ readonly id: "amber"; readonly label: "Amber"; readonly color: "#c47a15"; readonly hue: 55…;
toSorted: (compareFn?: ((a: { readonly id: "amber"; readonly label: "Amber"; readonly color: "#c47a15"; reado…;
toSpliced: { (start: number, deleteCount: number, ...items: ({ readonly id: "amber"; readonly label: "Amber"; …;
with: (index: number, value: { readonly id: "amber"; readonly label: "Amber"; readonly color: "#c47a15"; …;
__@iterator@288: () => ArrayIterator<{ readonly id: "amber"; readonly label: "Amber"; readonly color: "#c47a15"; rea…;
readonly __@unscopables@290: { readonly [x: number]: boolean | undefined; readonly length?: boolean | undefined; toString?: bool…;
}
```

### collectPaths
Expand Down Expand Up @@ -4879,8 +4920,8 @@ Types:
| `AvatarTokens` | `interface AvatarTokens extends AvatarFallbackTokens { size?: Partial< Record< "xs" \| "sm" \| "md" \| "lg" \| "xl", Pick<AvatarFallbackTokens, "fallbackSize" \| "fallbackWeight"> > >; }` | |
| `BrandColors` | `interface BrandColors { main: string; light: string; dark: string; contrast: string; }` | |
| `Breakpoint` | `type Breakpoint = "xs" \| keyof typeof SCALE;` | |
| `BuiltinPresetId` | `type BuiltinPresetId = \| "amber" \| "ocean" \| "crimson" \| "cyan" \| "violet" \| "emerald" \| "rose" \| "slate";` | Presets shipped in global.css. |
| `BuiltinPresetSource` | `interface BuiltinPresetSource { id: "amber" \| "ocean" \| "crimson" \| "cyan" \| "violet" \| "emerald" \| "rose" \| "slate"; label: string; color: `#${string}`; hue: number; chroma: number; lightL: number; overrides: { light: TokenMap; dark: TokenMap }; }` | |
| `BuiltinPresetId` | `type BuiltinPresetId = (typeof BUILTIN_PRESETS)[number]["id"];` | Presets shipped in global.css: the ids of `BUILTIN_PRESETS` (theme/builtins.ts). |
| `BuiltinPresetSource` | `interface BuiltinPresetSource { id: string; label: string; color: `#${string}`; hue: number; chroma: number; lightL: number; overrides: { light: TokenMap; dark: TokenMap }; }` | |
| `ButtonSizeTokens` | `interface ButtonSizeTokens { height?: CssValue; paddingX?: CssValue; fontSize?: CssValue; weight?: CssValue; tracking?: CssValue; }` | Typography and box tokens of one Button size. |
| `ButtonTokens` | `interface ButtonTokens extends ButtonSizeTokens { weight?: CssValue; tracking?: CssValue; leading?: CssValue; gap?: CssValue; ghostBorderWidth?: CssValue; disabledBg?: CssValue; touchHeight?: CssValue; sm?: ButtonSizeTokens; md?: ButtonSizeTokens; lg?: ButtonSizeTokens; }` | |
| `CardTokens` | `interface CardTokens { bg?: CssValue; radius?: CssValue; border?: CssValue; shadow?: CssValue; }` | |
Expand Down Expand Up @@ -5013,7 +5054,7 @@ Types:

| Field | Type | Notes |
|---|---|---|
| `id` | `"amber" \| "ocean" \| "crimson" \| "cyan" \| "violet" \| "emerald" \| "rose" \| "slate"` | |
| `id` | `string` | The one declaration of the built-in ids: `BuiltinPresetId` (tokens/presets.ts) is derived from the data below. |
| `label` | `string` | |
| `color` | ``#${string}`` | sRGB swatch for pickers and canvas. |
| `hue` | `number` | |
Expand Down Expand Up @@ -7532,7 +7573,7 @@ Types:
| Type | Declaration | Notes |
|---|---|---|
| `BrandColors` | `interface BrandColors { main: string; light: string; dark: string; contrast: string; }` | |
| `BuiltinPresetId` | `type BuiltinPresetId = \| "amber" \| "ocean" \| "crimson" \| "cyan" \| "violet" \| "emerald" \| "rose" \| "slate";` | Presets shipped in global.css. |
| `BuiltinPresetId` | `type BuiltinPresetId = (typeof BUILTIN_PRESETS)[number]["id"];` | Presets shipped in global.css: the ids of `BUILTIN_PRESETS` (theme/builtins.ts). |
| `ElevationLevel` | `type ElevationLevel = keyof typeof elevation;` | |
| `NeutralRamp` | `interface NeutralRamp { surfaceLight: SurfaceColors; surfaceDark: SurfaceColors; textLight: TextColors; textDark: TextColors; outlineLight: OutlineColors; outlineDark: OutlineColors; }` | |
| `OutlineColors` | `interface OutlineColors { subtle: string; default: string; strong: string; }` | |
Expand Down
10 changes: 7 additions & 3 deletions packages/ui/src/lib/theme/builtins.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,8 @@
import type { TokenMap } from "../utils/palette-generator.js";

export interface BuiltinPresetSource {
id: "amber" | "ocean" | "crimson" | "cyan" | "violet" | "emerald" | "rose" | "slate";
/** The one declaration of the built-in ids: `BuiltinPresetId` (tokens/presets.ts) is derived from the data below. */
id: string;
label: string;
/** sRGB swatch for pickers and canvas. */
color: `#${string}`;
Expand All @@ -13,7 +14,10 @@ export interface BuiltinPresetSource {
overrides: { light: TokenMap; dark: TokenMap };
}

export const BUILTIN_PRESETS: BuiltinPresetSource[] = [
/** Keeps each `id` a literal type (`BuiltinPresetId` is derived from it) while checking the shape. */
const presets = <const T extends readonly BuiltinPresetSource[]>(list: T): T => list;

export const BUILTIN_PRESETS = presets([
{
id: "amber",
label: "Amber",
Expand Down Expand Up @@ -146,4 +150,4 @@ export const BUILTIN_PRESETS: BuiltinPresetSource[] = [
dark: {},
},
},
];
]);
12 changes: 2 additions & 10 deletions packages/ui/src/lib/tokens/presets.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,16 +7,8 @@ import { generatePreset } from "../utils/palette-generator.js";
import type { BrandColors, NeutralRamp, SemanticColors } from "./colors.js";
import { neutralRamp } from "./colors.js";

/** Presets shipped in global.css. */
export type BuiltinPresetId =
| "amber"
| "ocean"
| "crimson"
| "cyan"
| "violet"
| "emerald"
| "rose"
| "slate";
/** Presets shipped in global.css: the ids of `BUILTIN_PRESETS` (theme/builtins.ts). */
export type BuiltinPresetId = (typeof BUILTIN_PRESETS)[number]["id"];

/** A built-in preset or a brand preset registered via `ThemeOptions.presets`. */
export type PresetId = BuiltinPresetId | (string & {});
Expand Down
45 changes: 45 additions & 0 deletions packages/ui/tests/preset-ids-source.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
import { describe, expect, it } from "bun:test";
import { readdirSync, readFileSync } from "node:fs";
import { join, relative } from "node:path";
import { BUILTIN_PRESETS } from "../src/lib/theme/builtins.ts";

/**
* #654: the eight built-in preset ids were written out in builtins.ts (the source), again as the
* `BuiltinPresetId` union in tokens/presets.ts, as a literal array in the theme studio state and in the landing
* theme lab, so adding a preset meant four edits and the three copies drifted silently. `BUILTIN_PRESETS`
* (theme/builtins.ts) is the one place that names them; everything else derives.
*/
const ROOT = join(import.meta.dir, "../../..");
const OWNER = "packages/ui/src/lib/theme/builtins.ts";
// A curated selection of six for the hero, typed `satisfies readonly BuiltinPresetId[]`: it is not a copy of the set.
const CURATED = new Set(["site/src/components/landing/HeroStage.svelte"]);

function* sources(dir: string): Generator<string> {
for (const e of readdirSync(dir, { withFileTypes: true })) {
const p = join(dir, e.name);
if (e.isDirectory()) {
if (
!["node_modules", "dist", ".svelte-kit", "__screenshots__", "__traces__"].includes(e.name)
)
yield* sources(p);
} else if (/\.(ts|svelte|astro)$/.test(e.name) && !/\.test\./.test(e.name)) yield p;
}
}

describe("built-in preset ids are named once (#654)", () => {
it("no module but builtins.ts lists the built-in ids", () => {
const ids = BUILTIN_PRESETS.map((p) => p.id);
// three or more distinct built-in ids as string literals in one run: a hand-written list or union
const quoted = new RegExp(`(?:["'](?:${ids.join("|")})["'][\\s,|]*){3,}`);
const hits: string[] = [];
for (const root of ["packages/ui/src", "packages/ui/scripts", "site/src"])
for (const f of sources(join(ROOT, root)))
if (
relative(ROOT, f) !== OWNER &&
!CURATED.has(relative(ROOT, f)) &&
quoted.test(readFileSync(f, "utf8"))
)
hits.push(relative(ROOT, f));
expect(hits.sort()).toEqual([]);
});
});
6 changes: 0 additions & 6 deletions packages/ui/tests/theme-studio.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,6 @@ import { FONTS, familyOf, stackOf } from "../../../site/src/components/theme/fon
import { LOOKS } from "../../../site/src/components/theme/looks.js";
import { extractCode } from "../../../site/src/components/theme/panels/output-model.js";
import {
BUILTIN_PRESETS_IDS,
COALESCE_MS,
commit,
createHistory,
Expand All @@ -36,7 +35,6 @@ import {
toThemeOptions,
undo,
} from "../../../site/src/components/theme/studio-state.js";
import { BUILTIN_PRESETS } from "../src/lib/theme/builtins.js";
import { defineTheme, themeCSS } from "../src/lib/theme/define.js";
import { DESIGN_STYLES, designStyle } from "../src/lib/theme/styles.js";

Expand All @@ -46,10 +44,6 @@ const edit = (p: Partial<StudioState>): StudioState => ({
});

describe("studio state → ThemeOptions", () => {
it("keeps the built-in preset ids in lockstep with the package", () => {
expect(BUILTIN_PRESETS_IDS).toEqual(BUILTIN_PRESETS.map((p) => p.id));
});

it("maps each control onto its ThemeOptions key and compiles through the real engine", () => {
const s = edit({
brand: "#e8590c",
Expand Down
4 changes: 3 additions & 1 deletion site/src/components/landing/HeroStage.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -17,12 +17,14 @@
Toaster,
colorScheme,
toast,
type BuiltinPresetId,
type SurfaceStyle,
} from "@codeatlasdev/ui";
import { Activity, Boxes, Gauge, Layers, Rocket, Users } from "@lucide/svelte";
import { onMount } from "svelte";

const PRESETS = ["ocean", "violet", "emerald", "rose", "cyan", "amber"] as const;
// A curated order of six built-ins for the hero, not the full set: a rename of one fails the type check.
const PRESETS = ["ocean", "violet", "emerald", "rose", "cyan", "amber"] as const satisfies readonly BuiltinPresetId[];
const STYLES: SurfaceStyle[] = ["glass", "frosted", "solid", "flat"];

let presetIdx = $state(0);
Expand Down
4 changes: 2 additions & 2 deletions site/src/components/landing/ThemeLab.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,10 @@
// Theme chapter: pick any built-in preset or paste a hex; the preview below is a real set of
// package components re-themed on its own wrapper. Brand presets are generated in the browser
// with the package's own palette generator, the same function defineTheme() uses at build time.
import { Button, Chip, Card, Progress, SegmentedControl, SwatchGroup, Switch } from "@codeatlasdev/ui";
import { BUILTIN_PRESETS, Button, Chip, Card, Progress, SegmentedControl, SwatchGroup, Switch } from "@codeatlasdev/ui";
import { generatePreset, hexToOklch, presetVars } from "@codeatlasdev/ui/utils/palette-generator";

const BUILTIN = ["amber", "ocean", "crimson", "cyan", "violet", "emerald", "rose", "slate"] as const;
const BUILTIN = BUILTIN_PRESETS.map((p) => p.id);
const STYLES = ["glass", "frosted", "solid", "flat"] as const;

let preset = $state<string>("ocean");
Expand Down
17 changes: 4 additions & 13 deletions site/src/components/theme/studio-state.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
// `ThemeOptions`; nothing in the studio invents a knob the engine does not have. Pure and DOM-free,
// so the codec, the history and the mapping are unit-testable.

import { BUILTIN_PRESETS } from "../../../../packages/ui/src/lib/theme/builtins";
import type {
ColorScheme,
Density,
Expand All @@ -14,20 +15,10 @@ import type {
} from "./engine";

/**
* Ids of the built-in presets. Kept literal so this module has no runtime import of the package
* barrel (it pulls Svelte components) and stays testable under plain `bun test`; `studio.test.ts`
* asserts it equals the package's own `THEME_PRESETS` ids, so a new built-in cannot drift.
* Ids of the built-in presets, derived from the package's one list. Imported from `theme/builtins` rather than the
* barrel (which pulls Svelte components) so this module stays testable under plain `bun test`.
*/
export const BUILTIN_PRESETS_IDS = [
"amber",
"ocean",
"crimson",
"cyan",
"violet",
"emerald",
"rose",
"slate",
];
export const BUILTIN_PRESETS_IDS: string[] = BUILTIN_PRESETS.map((p) => p.id);

/** Preset id the studio gives the user's own brand. Never clashes with a built-in. */
export const STUDIO_PRESET = "studio";
Expand Down