diff --git a/.github/workflows/tokens.yml b/.github/workflows/tokens.yml new file mode 100644 index 0000000..ee6337a --- /dev/null +++ b/.github/workflows/tokens.yml @@ -0,0 +1,36 @@ +name: Tokens + +on: + pull_request: + push: + branches: + - main + +jobs: + staleness: + name: Generated artifacts are current + runs-on: ubuntu-latest + + steps: + - name: Check out repository + uses: actions/checkout@v4 + + - name: Set up pnpm + uses: pnpm/action-setup@v4 + with: + version: 10.12.1 + + - name: Set up Node.js + uses: actions/setup-node@v4 + with: + node-version: 22 + cache: pnpm + + - name: Install dependencies + run: pnpm install --frozen-lockfile + + - name: Regenerate token artifacts + run: node build + + - name: Check generated artifacts are committed + run: git diff --exit-code -- src/tokens.web.css src/tokens.ts src/tokens.host-map.md diff --git a/.gitignore b/.gitignore index e43b0f9..9daa824 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,2 @@ .DS_Store +node_modules diff --git a/.plans/kilo-design-system-execution-checklist.md b/.plans/kilo-design-system-execution-checklist.md index b0b4a38..ad8da85 100644 --- a/.plans/kilo-design-system-execution-checklist.md +++ b/.plans/kilo-design-system-execution-checklist.md @@ -43,11 +43,11 @@ that file is the *why*. ## Step 2 / M1 — Tokens + fix the cloud styles > Repo was clean-slated (Option A): only `README.md`, `CONTEXT.md`, `docs/adr/`, `.plans/` remain. > Old token values recoverable from git commit `acdcc60`. -- `☐` **T1.1 Author `tokens.json`** (hand-authored **hex** source). `primary` === `brand` === +- `☑` **T1.1 Author `tokens.json`** (hand-authored **hex** source). `primary` === `brand` === `#EDFF00`, `--primary-foreground` = `#1F1F1F`; split into **brand+status (mode-agnostic)** vs **dark surface neutrals**; include status domain→color map, type scale (Inter + Roboto Mono), spacing, radius. _(ADR 0003, 0005, 0009.)_ _DoD: `tokens.json` exists and matches the ADRs._ -- `☐` **T1.2 Build the generator** (`build/` + `culori`): emits `tokens.web.css` (OKLCH, web), +- `☑` **T1.2 Build the generator** (`build/` + `culori`): emits `tokens.web.css` (OKLCH, web), `tokens.ts` (hex, portable), `tokens.host-map.md` (incl. the `--vscode-*` mapping); own `package.json` + a **CI staleness check**. _(ADR 0003, 0010.)_ _DoD: `node build` regenerates all three; CI fails if artifacts drift from source._ diff --git a/README.md b/README.md index a2718f1..fd10ffe 100644 --- a/README.md +++ b/README.md @@ -2,11 +2,13 @@ Canonical source of truth for Kilo's design system across products including Cloud, Landing, Console, VS Code, JetBrains, CLI, and Mobile. -This repository currently contains the first implementation slice of the design system: the hand-authored token contract and a local playground for reviewing and tuning it visually. +This repository currently contains the first implementation slice of the design system: the hand-authored token contract, generated token artifacts, and a local playground for reviewing and tuning tokens visually. ## What Lives Here - `tokens.json` is the canonical design token source for the current dark-first Kilo design language. +- `src/` contains generated token artifacts for product consumption. +- `build/` contains the token generator used by humans, the playground, and CI. - `playground/` is a local Next.js app for previewing, editing, and saving `tokens.json`. - `CONTEXT.md` defines the shared glossary for the design-system architecture. - `docs/adr/` records the locked architecture decisions behind the system. @@ -29,14 +31,29 @@ Current token groups include: The current primary brand action color is `#F7F586`. The system is dark-only by decision; there is no light-mode token set in this repo. +## Generated Artifacts + +Run the generator from the repository root: + +```bash +node build +``` + +This regenerates: + +- `src/tokens.web.css`: OKLCH CSS variables for browser products. +- `src/tokens.ts`: hex token values and flattened CSS-variable names for portable JavaScript/TypeScript consumers. +- `src/tokens.host-map.md`: generated host-environment mapping notes for VS Code, JetBrains, and CLI/ANSI usage. + +CI runs the same command and fails if regenerated artifacts differ from the committed files. That keeps `tokens.json` and `src/` in sync. + ## Run The Playground From the repository root: ```bash -cd playground pnpm install -pnpm dev +pnpm --dir playground dev ``` Then open: @@ -48,16 +65,16 @@ http://localhost:8731 If dependencies are already installed: ```bash -cd playground && pnpm dev +pnpm --dir playground dev ``` Useful playground scripts: ```bash -pnpm dev # Start the local playground on port 8731 -pnpm build # Build smoke test into .next-build -pnpm start # Start the production build on port 8731 -pnpm lint # Next lint command +pnpm --dir playground dev # Start the local playground on port 8731 +pnpm --dir playground build # Build smoke test into .next-build +pnpm --dir playground start # Start the production build on port 8731 +pnpm --dir playground lint # Next lint command ``` ## Edit Tokens @@ -67,7 +84,9 @@ The playground loads `../tokens.json` at startup and applies the values as live - Edit color tokens from the preview swatches. - Edit radius, spacing, typography, and status-domain mappings from the left control rail. - Inspect the serialized output in the right JSON rail. -- Use `Save to tokens.json` to write the current playground state back to the repo root token file. +- Use `Save Tokens` to write the current playground state back to the source file. +- Use `Undo last save` if a saved experiment should be restored to the previous source state. +- Open the dropdown next to `Save Tokens` and choose `Generate Tokens` to regenerate `src/` from the saved source file. This action is disabled while there are unsaved edits. - Use `Cmd+.` or `Ctrl+.` to collapse or expand both side rails together. The write-back API is local-development only and refuses token reads or writes when `NODE_ENV` is `production`. diff --git a/build/index.js b/build/index.js new file mode 100644 index 0000000..e71c0c2 --- /dev/null +++ b/build/index.js @@ -0,0 +1,356 @@ +#!/usr/bin/env node + +import { promises as fs } from "node:fs"; +import path from "node:path"; +import { fileURLToPath } from "node:url"; +import { clampChroma, oklch, parse } from "culori"; + +const __dirname = path.dirname(fileURLToPath(import.meta.url)); +const REPO_ROOT = path.resolve(__dirname, ".."); +const TOKENS_PATH = path.join(REPO_ROOT, "tokens.json"); +const OUTPUT_DIR = path.join(REPO_ROOT, "src"); + +const OUTPUTS = { + webCss: path.join(OUTPUT_DIR, "tokens.web.css"), + portableTs: path.join(OUTPUT_DIR, "tokens.ts"), + hostMap: path.join(OUTPUT_DIR, "tokens.host-map.md"), +}; + +const COLOR_BUCKETS = ["brand", "status", "surface", "foreground", "border", "syntax", "diff"]; +const REQUIRED_TYPE_FIELDS = ["fontFamily", "fontSize", "fontWeight", "lineHeight"]; + +const GENERATED_HEADER = [ + "Generated from tokens.json by `node build`.", + "Do not edit this file by hand.", +].join(" "); + +function isRecord(value) { + return typeof value === "object" && value !== null && !Array.isArray(value); +} + +function isMetaKey(key) { + return key.startsWith("$"); +} + +function cssVarName(...parts) { + return `--${parts.join("-")}`; +} + +function assertRecord(value, name) { + if (!isRecord(value)) { + throw new Error(`${name} must be an object.`); + } +} + +function assertStringMap(value, name) { + assertRecord(value, name); + for (const [key, val] of Object.entries(value)) { + if (isMetaKey(key)) continue; + if (typeof val !== "string") { + throw new Error(`${name}.${key} must be a string.`); + } + } +} + +function assertColorMap(value, name) { + assertStringMap(value, name); + for (const [key, val] of Object.entries(value)) { + if (isMetaKey(key)) continue; + if (!/^#(?:[0-9a-fA-F]{3,4}|[0-9a-fA-F]{6}|[0-9a-fA-F]{8})$/.test(val)) { + throw new Error(`${name}.${key} must be a hex color.`); + } + } +} + +function validateTokens(tokens) { + assertRecord(tokens, "tokens"); + + if (typeof tokens.version !== "string") { + throw new Error("tokens.version must be a string."); + } + + assertRecord(tokens.color, "tokens.color"); + for (const bucket of COLOR_BUCKETS) { + assertColorMap(tokens.color[bucket], `tokens.color.${bucket}`); + } + + assertStringMap(tokens.statusDomain, "tokens.statusDomain"); + for (const [domain, family] of Object.entries(tokens.statusDomain)) { + if (isMetaKey(domain)) continue; + if (!tokens.color.status[`${family}500`]) { + throw new Error(`tokens.statusDomain.${domain} points to missing status family "${family}".`); + } + } + + assertRecord(tokens.typography, "tokens.typography"); + for (const [role, def] of Object.entries(tokens.typography)) { + if (isMetaKey(role)) continue; + assertRecord(def, `tokens.typography.${role}`); + for (const field of REQUIRED_TYPE_FIELDS) { + if (!(field in def)) { + throw new Error(`tokens.typography.${role}.${field} is required.`); + } + } + if (typeof def.fontFamily !== "string") throw new Error(`tokens.typography.${role}.fontFamily must be a string.`); + if (typeof def.fontSize !== "string") throw new Error(`tokens.typography.${role}.fontSize must be a string.`); + if (typeof def.fontWeight !== "number") throw new Error(`tokens.typography.${role}.fontWeight must be a number.`); + if (typeof def.lineHeight !== "number") throw new Error(`tokens.typography.${role}.lineHeight must be a number.`); + if ("letterSpacing" in def && typeof def.letterSpacing !== "string") { + throw new Error(`tokens.typography.${role}.letterSpacing must be a string.`); + } + if ("textTransform" in def && typeof def.textTransform !== "string") { + throw new Error(`tokens.typography.${role}.textTransform must be a string.`); + } + } + + assertStringMap(tokens.radius, "tokens.radius"); + assertStringMap(tokens.spacing, "tokens.spacing"); +} + +function toOklchCss(value, tokenPath) { + const parsed = parse(value); + if (!parsed) { + throw new Error(`Could not parse ${tokenPath} as a color.`); + } + const converted = oklch(parsed); + if (!converted) { + throw new Error(`Could not convert ${tokenPath} to OKLCH.`); + } + const clamped = clampChroma(converted, "oklch", "rgb"); + return formatOklchCss(clamped); +} + +function formatNumber(value, places) { + const rounded = Math.abs(value) < Number.EPSILON ? 0 : value; + return rounded.toFixed(places).replace(/\.?0+$/, ""); +} + +function formatOklchCss(color) { + const lightness = formatNumber(color.l * 100, 3); + const chromaValue = Math.abs(color.c ?? 0) < 0.00005 ? 0 : color.c; + const chroma = formatNumber(chromaValue, 4); + const hue = chromaValue === 0 ? "0" : formatNumber(((color.h ?? 0) + 360) % 360, 2); + const alpha = color.alpha === undefined || color.alpha >= 0.9995 ? "" : ` / ${formatNumber(color.alpha, 3)}`; + return `oklch(${lightness}% ${chroma} ${hue}${alpha})`; +} + +function flattenToCssVars(tokens, { colorFormat }) { + const out = {}; + + for (const bucket of COLOR_BUCKETS) { + for (const [name, value] of Object.entries(tokens.color[bucket])) { + if (isMetaKey(name)) continue; + const varName = cssVarName(bucket, name); + out[varName] = colorFormat === "oklch" ? toOklchCss(value, `color.${bucket}.${name}`) : value; + } + } + + for (const [name, value] of Object.entries(tokens.radius)) { + if (isMetaKey(name)) continue; + out[cssVarName("radius", name)] = value; + } + + for (const [name, value] of Object.entries(tokens.spacing)) { + if (isMetaKey(name)) continue; + out[cssVarName("spacing", name)] = value; + } + + for (const [role, def] of Object.entries(tokens.typography)) { + if (isMetaKey(role)) continue; + out[cssVarName("type", role, "family")] = def.fontFamily; + out[cssVarName("type", role, "size")] = def.fontSize; + out[cssVarName("type", role, "weight")] = String(def.fontWeight); + out[cssVarName("type", role, "leading")] = String(def.lineHeight); + if (def.letterSpacing) out[cssVarName("type", role, "tracking")] = def.letterSpacing; + if (def.textTransform) out[cssVarName("type", role, "transform")] = def.textTransform; + } + + return out; +} + +function generateWebCss(tokens) { + const vars = flattenToCssVars(tokens, { colorFormat: "oklch" }); + const lines = Object.entries(vars).map(([name, value]) => ` ${name}: ${value};`); + return [ + `/* ${GENERATED_HEADER} */`, + ":root,", + '[data-theme="kilo-dark"] {', + " color-scheme: dark;", + ...lines, + "}", + "", + ].join("\n"); +} + +function stringifyConst(value) { + return JSON.stringify(value, null, 2); +} + +function generatePortableTs(tokens) { + const cssVars = flattenToCssVars(tokens, { colorFormat: "hex" }); + return [ + `// ${GENERATED_HEADER}`, + "", + `export const tokenVersion = ${JSON.stringify(tokens.version)} as const;`, + "", + `export const tokens = ${stringifyConst(tokens)} as const;`, + "", + `export const cssVars = ${stringifyConst(cssVars)} as const;`, + "", + "export type Tokens = typeof tokens;", + "export type CssVars = typeof cssVars;", + "export type CssVarName = keyof CssVars;", + "", + "export default tokens;", + "", + ].join("\n"); +} + +function table(headers, rows) { + return [ + `| ${headers.join(" | ")} |`, + `| ${headers.map(() => "---").join(" | ")} |`, + ...rows.map((row) => `| ${row.join(" | ")} |`), + ].join("\n"); +} + +function tokenRef(name) { + return `\`${name}\``; +} + +function tsRef(pathParts) { + return `\`tokens.${pathParts.join(".")}\``; +} + +const VSCODE_WEBVIEW_MAP = [ + ["--surface-background", "`--vscode-editor-background`", "Default Kilo webview canvas."], + ["--surface-raised", "`--vscode-sideBar-background`", "Raised panels inside the webview."], + ["--surface-inset", "`--vscode-input-background`", "Inputs, code wells, and recessed areas."], + ["--surface-overlay", "`--vscode-dropdown.background`, `--vscode-quickInput.background`", "Floating surfaces."], + ["--surface-hover", "`--vscode-list-hoverBackground`", "Hover rows and quiet interactive fills."], + ["--surface-selected", "`--vscode-list-activeSelectionBackground`", "Selected rows and active fills."], + ["--foreground-default", "`--vscode-foreground`", "Primary text and icons."], + ["--foreground-muted", "`--vscode-descriptionForeground`", "Secondary text."], + ["--foreground-subtle", "`--vscode-disabledForeground`", "Tertiary text."], + ["--border-default", "`--vscode-widget-border`", "Default separators and outlines."], + ["--border-strong", "`--vscode-focusBorder`", "Strong outlines and focus-adjacent borders."], + ["--brand-primary", "`--vscode-focusBorder` in high contrast only", "Kilo-owned brand action color in normal dark mode."], + ["--diff-addSurface", "`--vscode-diffEditor.insertedTextBackground`", "Inserted-line background."], + ["--diff-deleteSurface", "`--vscode-diffEditor.removedTextBackground`", "Deleted-line background."], + ["--diff-addText", "`--vscode-gitDecoration-addedResourceForeground`", "Inserted text accent."], + ["--diff-deleteText", "`--vscode-gitDecoration-deletedResourceForeground`", "Deleted text accent."], +]; + +const JETBRAINS_MAP = [ + ["surface.background", "`Panel.background`", "Default Kilo-dark panel fill."], + ["surface.raised", "`ToolWindow.background`", "Raised tool-window areas."], + ["surface.inset", "`TextField.background`", "Inputs and recessed code wells."], + ["surface.hover", "`List.hoverBackground`", "Hover rows."], + ["surface.selected", "`List.selectionBackground`", "Selected rows."], + ["foreground.default", "`Label.foreground`", "Primary text."], + ["foreground.muted", "`ContextHelp.foreground`", "Secondary text."], + ["border.default", "`Component.borderColor`", "Default component outlines."], + ["brand.primary", "Kilo-owned accent", "Primary action and brand focus roles."], +]; + +const ANSI_BY_FAMILY = { + blue: "blue", + purple: "magenta", + teal: "cyan", + gray: "brightBlack", + orange: "yellow", + green: "green", + yellow: "yellow", + red: "red", +}; + +function generateHostMap(tokens) { + const domainRows = Object.entries(tokens.statusDomain).map(([domain, family]) => [ + `\`${domain}\``, + `\`${family}\``, + tokenRef(`--status-${family}500`), + tsRef(["color", "status", `${family}500`]), + `\`${ANSI_BY_FAMILY[family] ?? "default"}\``, + ]); + + return [ + "# Kilo Token Host Map", + "", + `> ${GENERATED_HEADER}`, + "", + "This file documents how the generated token artifacts map into host environments. Product skills can add stricter per-surface recipes, but they should not redefine these token values.", + "", + "## Generated Artifacts", + "", + "- `src/tokens.web.css`: OKLCH CSS variables for browser surfaces such as Cloud and Landing.", + "- `src/tokens.ts`: hex source values and flattened CSS-variable names for portable targets such as webviews, React Native, CLI, and native integrations.", + "- `src/tokens.host-map.md`: this generated host mapping reference.", + "", + "## VS Code Webview", + "", + "Kilo webviews render Kilo-dark content inside VS Code chrome. They do not follow host light themes; only high-contrast host adaptations should override Kilo-dark values.", + "", + table(["Kilo token", "Closest `--vscode-*` host token", "Use"], VSCODE_WEBVIEW_MAP.map((row) => [tokenRef(row[0]), row[1], row[2]])), + "", + "## JetBrains", + "", + "JetBrains surfaces should read the native UIManager role where host chrome owns the surface, then use Kilo tokens for Kilo-owned content. Host-specific details stay in `kilo-jetbrains/AGENTS.md`.", + "", + table(["Kilo role", "UIManager role", "Use"], JETBRAINS_MAP.map((row) => [tokenRef(row[0]), row[1], row[2]])), + "", + "## CLI / ANSI", + "", + "Terminal output should keep structure and contrast first. Use ANSI colors as the nearest semantic family, not as exact color matching.", + "", + table(["Domain", "Status family", "Base token", "Portable token", "ANSI role"], domainRows), + "", + "## Notes", + "", + "- `brand.primary` is Kilo-owned. Do not remap it to arbitrary host accents unless a high-contrast mode requires it.", + "- `foreground` means on-surface content, not a product brand color.", + "- `border.inputBg` is a fill-like input token kept in the border bucket for compatibility with the current token contract.", + "", + ].join("\n"); +} + +export async function readTokens(tokensPath = TOKENS_PATH) { + const raw = await fs.readFile(tokensPath, "utf8"); + const tokens = JSON.parse(raw); + validateTokens(tokens); + return tokens; +} + +export async function generateTokenArtifacts({ tokensPath = TOKENS_PATH, outputDir = OUTPUT_DIR } = {}) { + const tokens = await readTokens(tokensPath); + const outputs = { + webCss: path.join(outputDir, "tokens.web.css"), + portableTs: path.join(outputDir, "tokens.ts"), + hostMap: path.join(outputDir, "tokens.host-map.md"), + }; + + await fs.mkdir(outputDir, { recursive: true }); + await Promise.all([ + fs.writeFile(outputs.webCss, generateWebCss(tokens), "utf8"), + fs.writeFile(outputs.portableTs, generatePortableTs(tokens), "utf8"), + fs.writeFile(outputs.hostMap, generateHostMap(tokens), "utf8"), + ]); + + return outputs; +} + +const cliTarget = process.argv[1] ? path.resolve(process.argv[1]) : ""; +const isCli = cliTarget === fileURLToPath(import.meta.url) || cliTarget === __dirname; + +if (isCli) { + try { + const outputs = await generateTokenArtifacts(); + for (const filePath of Object.values(outputs)) { + console.log(path.relative(REPO_ROOT, filePath)); + } + } catch (error) { + console.error(error instanceof Error ? error.message : error); + process.exitCode = 1; + } +} + +export { OUTPUTS, flattenToCssVars, generateHostMap, generatePortableTs, generateWebCss, validateTokens }; diff --git a/build/package.json b/build/package.json new file mode 100644 index 0000000..b2c40b7 --- /dev/null +++ b/build/package.json @@ -0,0 +1,12 @@ +{ + "name": "@kilo-design/token-generator", + "private": true, + "type": "module", + "main": "index.js", + "scripts": { + "build": "node ." + }, + "dependencies": { + "culori": "^4.0.1" + } +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..61b527e --- /dev/null +++ b/package.json @@ -0,0 +1,11 @@ +{ + "name": "kilo-design", + "private": true, + "packageManager": "pnpm@10.12.1", + "scripts": { + "build:tokens": "node build", + "dev:playground": "pnpm --dir playground dev", + "build:playground": "pnpm --dir playground build", + "typecheck:playground": "pnpm --dir playground exec tsc --noEmit" + } +} diff --git a/playground/README.md b/playground/README.md index 8b9b578..232faf1 100644 --- a/playground/README.md +++ b/playground/README.md @@ -9,15 +9,16 @@ A small local Next.js tool for editing and previewing the canonical Kilo Design - Lets you edit color tokens directly from the swatches in the preview. - Keeps non-color controls, such as radius, spacing, typography, and status-domain mappings, in the left control rail. - Shows the current JSON output in the right rail. -- Can copy, download, reset, reload, or write changes back to `tokens.json` during local development. +- Can copy, download, reset, or write changes back to `tokens.json` during local development. +- Can regenerate the committed `../src/` token artifacts from the saved source file. ## Running Locally -From this directory: +From the repository root: ```bash pnpm install -pnpm dev +pnpm --dir playground dev ``` Then open: @@ -29,10 +30,10 @@ http://localhost:8731 Useful scripts: ```bash -pnpm dev # Start the local playground on port 8731 -pnpm build # Build smoke test into .next-build, so it does not clobber a running dev server -pnpm start # Start the .next-build production output on port 8731 -pnpm lint # Next lint command +pnpm --dir playground dev # Start the local playground on port 8731 +pnpm --dir playground build # Build smoke test into .next-build, so it does not clobber a running dev server +pnpm --dir playground start # Start the .next-build production output on port 8731 +pnpm --dir playground lint # Next lint command ``` ## Editing Tokens @@ -46,24 +47,26 @@ The playground starts with the current contents of `../tokens.json`. Changes are local to the browser state until you choose one of the actions in the header: -- `Save to tokens.json` writes the current token state back to `../tokens.json`. +- `Save Tokens` writes the current token state back to `../tokens.json`. +- `Undo last save` restores the previous `../tokens.json` state after an accidental save. +- `Generate Tokens` lives in the dropdown next to `Save Tokens`. It runs `node build` to regenerate `../src/tokens.web.css`, `../src/tokens.ts`, and `../src/tokens.host-map.md` from the saved source file. It is disabled while there are unsaved edits. - `Reset edits` restores the last loaded source state. -- `Reload source` re-reads `../tokens.json` from disk. -- `Download` saves the current token state as a JSON file. +- `Download` lives in the dropdown next to `Save Tokens` and saves the current token state as a JSON file. - `Copy JSON` in the JSON preview rail copies the current serialized token state. ## Safety Notes The write-back API is guarded for local development only. `app/api/tokens/route.ts` refuses token read/write requests when `NODE_ENV` is `production`, so the playground should be treated as a local authoring tool rather than a deployed editor. -The page itself also reads `../tokens.json` from disk, so run commands from the `playground` directory to keep path resolution correct. +The page itself also reads `../tokens.json` from disk. Use `pnpm --dir playground dev` from the repo root so the dev server runs with `playground/` as its working directory. ## Implementation Map - `app/page.tsx` reads the canonical token file and passes it into the client app. -- `app/PlaygroundClient.tsx` owns token state, dirty state, save/reset/reload actions, sidebar state, and live CSS variable injection. +- `app/PlaygroundClient.tsx` owns token state, dirty state, save/reset actions, sidebar state, and live CSS variable injection. - `app/Gallery.tsx` renders the preview surface and color swatch controls. - `app/Controls.tsx` renders non-color token controls. - `app/JsonPreview.tsx` renders the syntax-highlighted JSON rail. - `lib/tokens.ts` defines token types and flattens token data into CSS variables. - `lib/oklch.ts` contains color conversion helpers used by the playground utilities. +- `../build/index.js` owns the artifact generator invoked after local saves. diff --git a/playground/app/PlaygroundClient.tsx b/playground/app/PlaygroundClient.tsx index 557bd42..6f5cc57 100644 --- a/playground/app/PlaygroundClient.tsx +++ b/playground/app/PlaygroundClient.tsx @@ -19,12 +19,16 @@ export function PlaygroundClient({ initialTokens }: { initialTokens: Tokens }) { const [dirty, setDirty] = useState(false); const [status, setStatus] = useState(null); const [saving, setSaving] = useState(false); - const [confirmingReload, setConfirmingReload] = useState(false); + const [restoringSave, setRestoringSave] = useState(false); + const [generating, setGenerating] = useState(false); + const [artifactsStale, setArtifactsStale] = useState(false); + const [undoSaveSnapshot, setUndoSaveSnapshot] = useState(null); + const [tokenMenuOpen, setTokenMenuOpen] = useState(false); const [railCollapsed, setRailCollapsed] = useState(false); const [jsonCollapsed, setJsonCollapsed] = useState(false); const tokenVars = useMemo(() => flattenToCssVars(tokens) as CSSProperties, [tokens]); - const confirmRef = useRef(null); + const tokenMenuRef = useRef(null); // Drawers overlay content on narrow viewports; start collapsed there so the // editing surface is visible first. @@ -40,24 +44,6 @@ export function PlaygroundClient({ initialTokens }: { initialTokens: Tokens }) { const serialize = () => JSON.stringify(tokens, null, 2) + "\n"; - const reload = async () => { - setConfirmingReload(false); - try { - const res = await fetch("/api/tokens", { cache: "no-store" }); - const data = await res.json(); - if (!res.ok) { setStatus({ tone: "error", text: data.error ?? "Reload failed" }); return; } - setTokens(data.tokens); setOriginal(data.tokens); setDirty(false); - setStatus({ tone: "success", text: "Reloaded from tokens.json" }); - } catch { - setStatus({ tone: "error", text: "Reload failed — is the dev server running?" }); - } - }; - - const requestReload = () => { - if (dirty) { setConfirmingReload(true); return; } - void reload(); - }; - const reset = () => { setTokens(original); setDirty(false); setStatus({ tone: "neutral", text: "Edits discarded" }); }; const copy = async () => { @@ -78,7 +64,16 @@ export function PlaygroundClient({ initialTokens }: { initialTokens: Tokens }) { setStatus({ tone: "success", text: "Downloaded tokens.json" }); }; + const downloadFromMenu = () => { + setTokenMenuOpen(false); + download(); + }; + const save = async () => { + if (!dirty) { + setStatus({ tone: "muted", text: "No token changes to save" }); + return; + } setSaving(true); setStatus(null); try { @@ -89,8 +84,10 @@ export function PlaygroundClient({ initialTokens }: { initialTokens: Tokens }) { }); const data = await res.json(); if (!res.ok) { setStatus({ tone: "error", text: data.error ?? "Save failed" }); return; } + setUndoSaveSnapshot(original); setOriginal(tokens); setDirty(false); - setStatus({ tone: "success", text: "Saved to tokens.json" }); + setArtifactsStale(true); + setStatus({ tone: "success", text: "Saved Tokens; undo remains available" }); } catch { setStatus({ tone: "error", text: "Save failed — is the dev server running?" }); } finally { @@ -98,6 +95,45 @@ export function PlaygroundClient({ initialTokens }: { initialTokens: Tokens }) { } }; + const undoLastSave = async () => { + if (!undoSaveSnapshot) return; + setRestoringSave(true); + setStatus(null); + try { + const res = await fetch("/api/tokens", { + method: "PUT", + headers: { "content-type": "application/json" }, + body: JSON.stringify({ tokens: undoSaveSnapshot }), + }); + const data = await res.json(); + if (!res.ok) { setStatus({ tone: "error", text: data.error ?? "Undo save failed" }); return; } + setTokens(undoSaveSnapshot); setOriginal(undoSaveSnapshot); setDirty(false); + setArtifactsStale(true); setUndoSaveSnapshot(null); + setStatus({ tone: "success", text: "Restored previous tokens.json" }); + } catch { + setStatus({ tone: "error", text: "Undo save failed — is the dev server running?" }); + } finally { + setRestoringSave(false); + } + }; + + const generate = async () => { + setTokenMenuOpen(false); + setGenerating(true); + setStatus(null); + try { + const res = await fetch("/api/tokens", { method: "POST" }); + const data = await res.json(); + if (!res.ok) { setStatus({ tone: "error", text: data.error ?? "Generate failed" }); return; } + setArtifactsStale(false); + setStatus({ tone: "success", text: "Generated Tokens" }); + } catch { + setStatus({ tone: "error", text: "Generate failed — is the dev server running?" }); + } finally { + setGenerating(false); + } + }; + // Transient messages fade back to the standing dirty/synced state. useEffect(() => { if (!status || status.tone === "progress") return; @@ -120,24 +156,36 @@ export function PlaygroundClient({ initialTokens }: { initialTokens: Tokens }) { return () => window.removeEventListener("keydown", onKey); }, [railCollapsed, jsonCollapsed]); - // Move focus into the reload confirmation and restore the standing state on Escape. useEffect(() => { - if (!confirmingReload) return; - confirmRef.current?.focus(); - const onKey = (e: KeyboardEvent) => { if (e.key === "Escape") setConfirmingReload(false); }; + if (!tokenMenuOpen) return; + const onPointerDown = (e: PointerEvent) => { + if (tokenMenuRef.current?.contains(e.target as Node)) return; + setTokenMenuOpen(false); + }; + const onKey = (e: KeyboardEvent) => { if (e.key === "Escape") setTokenMenuOpen(false); }; + window.addEventListener("pointerdown", onPointerDown); window.addEventListener("keydown", onKey); - return () => window.removeEventListener("keydown", onKey); - }, [confirmingReload]); + return () => { + window.removeEventListener("pointerdown", onPointerDown); + window.removeEventListener("keydown", onKey); + }; + }, [tokenMenuOpen]); const closeDrawers = () => { setRailCollapsed(true); setJsonCollapsed(true); }; const drawersOpen = !railCollapsed || !jsonCollapsed; const pill: StatusMessage = saving - ? { tone: "progress", text: "Saving…" } + ? { tone: "progress", text: "Saving tokens…" } + : restoringSave + ? { tone: "progress", text: "Restoring previous save…" } + : generating + ? { tone: "progress", text: "Generating Tokens…" } : status ? status : dirty ? { tone: "neutral", text: "Unsaved edits" } + : artifactsStale + ? { tone: "neutral", text: "Generated Tokens need update" } : { tone: "muted", text: "Synced" }; const pillToneClass: Record = { @@ -170,22 +218,72 @@ export function PlaygroundClient({ initialTokens }: { initialTokens: Tokens }) {
- + {undoSaveSnapshot && ( + + )} +
+
+ - - +
+ + {tokenMenuOpen && ( +
+ + +
+ )} +
-
@@ -203,25 +301,6 @@ export function PlaygroundClient({ initialTokens }: { initialTokens: Tokens }) { - - - - - )} ); } diff --git a/playground/app/api/tokens/route.ts b/playground/app/api/tokens/route.ts index ec5ff70..eef1c4f 100644 --- a/playground/app/api/tokens/route.ts +++ b/playground/app/api/tokens/route.ts @@ -3,11 +3,17 @@ // write surface on a deployed app. import { NextResponse } from "next/server"; +import { execFile } from "node:child_process"; import { promises as fs } from "node:fs"; import path from "node:path"; +import { promisify } from "node:util"; -// playground/app/api/tokens -> repo root is four levels up. -const TOKENS_PATH = path.resolve(process.cwd(), "..", "tokens.json"); +const execFileAsync = promisify(execFile); + +const REPO_ROOT = path.resolve(process.cwd(), ".."); +const TOKENS_PATH = path.join(REPO_ROOT, "tokens.json"); +const BUILD_PATH = path.join(REPO_ROOT, "build"); +const GENERATED_ARTIFACTS = ["src/tokens.web.css", "src/tokens.ts", "src/tokens.host-map.md"]; function guard() { if (process.env.NODE_ENV === "production") { @@ -51,3 +57,23 @@ export async function PUT(request: Request) { ); } } + +export async function POST() { + const blocked = guard(); + if (blocked) return blocked; + + try { + await execFileAsync(process.execPath, [BUILD_PATH], { cwd: REPO_ROOT, maxBuffer: 1024 * 1024 }); + return NextResponse.json({ + ok: true, + generated: GENERATED_ARTIFACTS, + }); + } catch (err) { + const error = err as Error & { stderr?: string; stdout?: string }; + const details = [error.stderr, error.stdout, error.message].filter(Boolean).join("\n").trim(); + return NextResponse.json( + { error: `Could not generate token artifacts: ${details}` }, + { status: 500 }, + ); + } +} diff --git a/playground/app/layout.tsx b/playground/app/layout.tsx index f94d748..60c77cd 100644 --- a/playground/app/layout.tsx +++ b/playground/app/layout.tsx @@ -1,6 +1,5 @@ import type { Metadata } from "next"; import { Inter, Roboto_Mono } from "next/font/google"; -import { Agentation } from "agentation"; import "./globals.css"; const inter = Inter({ subsets: ["latin"], variable: "--font-inter", display: "swap" }); @@ -16,7 +15,6 @@ export default function RootLayout({ children }: { children: React.ReactNode }) {children} - {process.env.NODE_ENV === "development" && } ); diff --git a/playground/app/playground.module.css b/playground/app/playground.module.css index 10a9633..547c3fb 100644 --- a/playground/app/playground.module.css +++ b/playground/app/playground.module.css @@ -4,8 +4,6 @@ --topbar-h: var(--spacing-12, 48px); --z-scrim: 35; --z-drawer: 40; - --z-confirm-scrim: 55; - --z-confirm: 60; display: grid; grid-template-columns: var(--rail-width) minmax(0, 1fr) var(--json-width); grid-template-rows: var(--topbar-h) 1fr; @@ -95,6 +93,8 @@ /* ---- action buttons ---- */ .actions { display: flex; align-items: center; gap: var(--spacing-2, 8px); flex: none; min-width: 0; } .secondaryActions { display: flex; align-items: center; gap: var(--spacing-2, 8px); min-width: 0; } +.tokenMenu { position: relative; display: flex; flex: none; } +.saveGroup { display: inline-flex; align-items: stretch; flex: none; } .btn { display: inline-flex; align-items: center; gap: var(--spacing-2, 8px); font-size: var(--type-label-size, 0.75rem); font-weight: var(--type-label-weight, 500); @@ -112,6 +112,45 @@ .primary:hover:not(:disabled) { background: var(--brand-primaryHover, #e6e475); border-color: transparent; } .primary:disabled { opacity: .45; } .primary:focus-visible { box-shadow: 0 0 0 2px var(--surface-raised, #202020), 0 0 0 4px var(--brand-primary, #f7f586); } +.saveMain { border-top-right-radius: 0; border-bottom-right-radius: 0; } +.splitTrigger { + min-width: 34px; + justify-content: center; + padding-inline: var(--spacing-2, 8px); + border-left: 1px solid color-mix(in srgb, var(--brand-foreground, #1f1f1f) 16%, transparent); + border-top-left-radius: 0; + border-bottom-left-radius: 0; +} +.splitTrigger:hover:not(:disabled) { border-left-color: color-mix(in srgb, var(--brand-foreground, #1f1f1f) 20%, transparent); } +.chevron { display: block; width: 14px; height: 14px; color: var(--brand-foreground, #1f1f1f); opacity: .72; } +.menu { + position: absolute; + z-index: 50; + top: calc(100% + var(--spacing-2, 8px)); + right: 0; + min-width: 220px; + padding: var(--spacing-1, 4px); + border: 1px solid var(--border-strong, #ffffff2e); + border-radius: var(--radius-md, 8px); + background: var(--surface-overlay, #333333); + box-shadow: 0 16px 40px #00000080; +} +.menuItem { + width: 100%; + display: flex; align-items: center; justify-content: flex-start; gap: var(--spacing-4, 16px); + border: 0; + border-radius: var(--radius-sm, 4px); + padding: var(--spacing-2, 8px) var(--spacing-3, 12px); + background: transparent; + color: var(--foreground-default, #fafafa); + font-size: var(--type-label-size, 0.75rem); + font-weight: var(--type-label-weight, 500); + text-align: left; + cursor: pointer; +} +.menuItem:hover:not(:disabled) { background: var(--surface-hover, #3A3A3A); } +.menuItem:disabled { opacity: .45; cursor: not-allowed; } +.menuItem:focus-visible { outline: none; box-shadow: 0 0 0 3px var(--brand-primaryRing, #f7f58659); } .danger { color: var(--status-red400, #f87171); border-color: color-mix(in srgb, var(--status-red500, #ef4444) 50%, transparent); } .danger:hover:not(:disabled) { background: color-mix(in srgb, var(--status-red500, #ef4444) 16%, transparent); @@ -130,46 +169,7 @@ /* ---- drawer scrim (narrow viewports only) ---- */ .scrim { display: none; } -/* ---- reload confirmation ---- */ -.confirmScrim { - position: fixed; inset: 0; z-index: var(--z-confirm-scrim); - border: 0; background: color-mix(in srgb, #000 55%, transparent); - animation: scrimIn .14s ease; -} -.confirm { - position: fixed; z-index: var(--z-confirm); - top: 50%; left: 50%; transform: translate(-50%, -50%); - width: min(92vw, 340px); - padding: var(--spacing-5, 20px); - border-radius: var(--radius-lg, 10px); - border: 1px solid var(--border-strong, #ffffff2e); - background: var(--surface-overlay, #333333); - box-shadow: 0 16px 48px #00000099; - animation: confirmIn .16s cubic-bezier(0.16, 1, 0.3, 1); -} -.confirmTitle { - margin: 0 0 var(--spacing-2, 8px); - font-size: var(--type-body-size, 0.875rem); font-weight: 600; - color: var(--foreground-default, #fafafa); -} -.confirmBody { - margin: 0 0 var(--spacing-4, 16px); - font-size: var(--type-label-size, 0.75rem); line-height: var(--type-body-leading, 1.5); - color: var(--foreground-muted, #a3a3a3); -} -.confirmBody code { - font-family: var(--type-code-family, "Roboto Mono"), var(--font-roboto-mono), monospace; - font-size: 0.6875rem; color: var(--foreground-default, #fafafa); - background: var(--border-inputBg, #ffffff0a); border: 1px solid var(--border-default, #ffffff1a); - border-radius: var(--radius-sm, 4px); padding: 1px 5px; -} -.confirmActions { display: flex; justify-content: flex-end; gap: var(--spacing-2, 8px); } - @keyframes scrimIn { from { opacity: 0; } to { opacity: 1; } } -@keyframes confirmIn { - from { opacity: 0; transform: translate(-50%, calc(-50% + 8px)); } - to { opacity: 1; transform: translate(-50%, -50%); } -} /* ---- narrow viewports: panels become overlay drawers ---- */ @media (max-width: 720px) { @@ -212,6 +212,6 @@ @media (prefers-reduced-motion: reduce) { .app, .statusPill, .statusDot, .btn { transition: none; } .spinner, .spinnerDark { animation: none; border-top-color: currentColor; } - .confirm, .confirmScrim, .scrim { animation: none; } + .scrim { animation: none; } .rail { transition: none; } } diff --git a/playground/package.json b/playground/package.json index 4b296a8..1257fc8 100644 --- a/playground/package.json +++ b/playground/package.json @@ -10,7 +10,6 @@ "lint": "next lint" }, "dependencies": { - "agentation": "^3.0.2", "culori": "^4.0.1", "next": "^15.1.6", "react": "^19.0.0", diff --git a/playground/pnpm-lock.yaml b/pnpm-lock.yaml similarity index 91% rename from playground/pnpm-lock.yaml rename to pnpm-lock.yaml index e711592..a5d0c57 100644 --- a/playground/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -6,11 +6,16 @@ settings: importers: - .: + .: {} + + build: + dependencies: + culori: + specifier: ^4.0.1 + version: 4.0.2 + + playground: dependencies: - agentation: - specifier: ^3.0.2 - version: 3.0.2(react-dom@19.2.7(react@19.2.7))(react@19.2.7) culori: specifier: ^4.0.1 version: 4.0.2 @@ -29,7 +34,7 @@ importers: version: 2.1.1 '@types/node': specifier: ^22.10.7 - version: 22.19.20 + version: 22.19.21 '@types/react': specifier: ^19.0.7 version: 19.2.17 @@ -42,8 +47,8 @@ importers: packages: - '@emnapi/runtime@1.10.0': - resolution: {integrity: sha512-ewvYlk86xUoGI0zQRNq/mC+16R1QeDlKQy21Ki3oSYXNgLb45GV1P6A0M+/s6nyCuNDqe5VpaY84BzXGwVbwFA==} + '@emnapi/runtime@1.11.1': + resolution: {integrity: sha512-vgj7R3y3Wgx24IQaGPA/R6YFXLHVMOZ0uVEyIQPaWs+rd1AzfEMXlAC22FYwO1XkKR6NPsq7mUandH8oIRdZFw==} '@img/colour@1.1.0': resolution: {integrity: sha512-Td76q7j57o/tLVdgS746cYARfSyxk8iEfRxewL9h4OMzYhbW4TAcppl0mT4eyqXddh6L/jwoM75mo7ixa/pCeQ==} @@ -75,105 +80,89 @@ packages: resolution: {integrity: sha512-excjX8DfsIcJ10x1Kzr4RcWe1edC9PquDRRPx3YVCvQv+U5p7Yin2s32ftzikXojb1PIFc/9Mt28/y+iRklkrw==} cpu: [arm64] os: [linux] - libc: [glibc] '@img/sharp-libvips-linux-arm@1.2.4': resolution: {integrity: sha512-bFI7xcKFELdiNCVov8e44Ia4u2byA+l3XtsAj+Q8tfCwO6BQ8iDojYdvoPMqsKDkuoOo+X6HZA0s0q11ANMQ8A==} cpu: [arm] os: [linux] - libc: [glibc] '@img/sharp-libvips-linux-ppc64@1.2.4': resolution: {integrity: sha512-FMuvGijLDYG6lW+b/UvyilUWu5Ayu+3r2d1S8notiGCIyYU/76eig1UfMmkZ7vwgOrzKzlQbFSuQfgm7GYUPpA==} cpu: [ppc64] os: [linux] - libc: [glibc] '@img/sharp-libvips-linux-riscv64@1.2.4': resolution: {integrity: sha512-oVDbcR4zUC0ce82teubSm+x6ETixtKZBh/qbREIOcI3cULzDyb18Sr/Wcyx7NRQeQzOiHTNbZFF1UwPS2scyGA==} cpu: [riscv64] os: [linux] - libc: [glibc] '@img/sharp-libvips-linux-s390x@1.2.4': resolution: {integrity: sha512-qmp9VrzgPgMoGZyPvrQHqk02uyjA0/QrTO26Tqk6l4ZV0MPWIW6LTkqOIov+J1yEu7MbFQaDpwdwJKhbJvuRxQ==} cpu: [s390x] os: [linux] - libc: [glibc] '@img/sharp-libvips-linux-x64@1.2.4': resolution: {integrity: sha512-tJxiiLsmHc9Ax1bz3oaOYBURTXGIRDODBqhveVHonrHJ9/+k89qbLl0bcJns+e4t4rvaNBxaEZsFtSfAdquPrw==} cpu: [x64] os: [linux] - libc: [glibc] '@img/sharp-libvips-linuxmusl-arm64@1.2.4': resolution: {integrity: sha512-FVQHuwx1IIuNow9QAbYUzJ+En8KcVm9Lk5+uGUQJHaZmMECZmOlix9HnH7n1TRkXMS0pGxIJokIVB9SuqZGGXw==} cpu: [arm64] os: [linux] - libc: [musl] '@img/sharp-libvips-linuxmusl-x64@1.2.4': resolution: {integrity: sha512-+LpyBk7L44ZIXwz/VYfglaX/okxezESc6UxDSoyo2Ks6Jxc4Y7sGjpgU9s4PMgqgjj1gZCylTieNamqA1MF7Dg==} cpu: [x64] os: [linux] - libc: [musl] '@img/sharp-linux-arm64@0.34.5': resolution: {integrity: sha512-bKQzaJRY/bkPOXyKx5EVup7qkaojECG6NLYswgktOZjaXecSAeCWiZwwiFf3/Y+O1HrauiE3FVsGxFg8c24rZg==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [arm64] os: [linux] - libc: [glibc] '@img/sharp-linux-arm@0.34.5': resolution: {integrity: sha512-9dLqsvwtg1uuXBGZKsxem9595+ujv0sJ6Vi8wcTANSFpwV/GONat5eCkzQo/1O6zRIkh0m/8+5BjrRr7jDUSZw==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [arm] os: [linux] - libc: [glibc] '@img/sharp-linux-ppc64@0.34.5': resolution: {integrity: sha512-7zznwNaqW6YtsfrGGDA6BRkISKAAE1Jo0QdpNYXNMHu2+0dTrPflTLNkpc8l7MUP5M16ZJcUvysVWWrMefZquA==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [ppc64] os: [linux] - libc: [glibc] '@img/sharp-linux-riscv64@0.34.5': resolution: {integrity: sha512-51gJuLPTKa7piYPaVs8GmByo7/U7/7TZOq+cnXJIHZKavIRHAP77e3N2HEl3dgiqdD/w0yUfiJnII77PuDDFdw==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [riscv64] os: [linux] - libc: [glibc] '@img/sharp-linux-s390x@0.34.5': resolution: {integrity: sha512-nQtCk0PdKfho3eC5MrbQoigJ2gd1CgddUMkabUj+rBevs8tZ2cULOx46E7oyX+04WGfABgIwmMC0VqieTiR4jg==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [s390x] os: [linux] - libc: [glibc] '@img/sharp-linux-x64@0.34.5': resolution: {integrity: sha512-MEzd8HPKxVxVenwAa+JRPwEC7QFjoPWuS5NZnBt6B3pu7EG2Ge0id1oLHZpPJdn3OQK+BQDiw9zStiHBTJQQQQ==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [x64] os: [linux] - libc: [glibc] '@img/sharp-linuxmusl-arm64@0.34.5': resolution: {integrity: sha512-fprJR6GtRsMt6Kyfq44IsChVZeGN97gTD331weR1ex1c1rypDEABN6Tm2xa1wE6lYb5DdEnk03NZPqA7Id21yg==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [arm64] os: [linux] - libc: [musl] '@img/sharp-linuxmusl-x64@0.34.5': resolution: {integrity: sha512-Jg8wNT1MUzIvhBFxViqrEhWDGzqymo3sV7z7ZsaWbZNDLXRJZoRGrjulp60YYtV4wfY8VIKcWidjojlLcWrd8Q==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [x64] os: [linux] - libc: [musl] '@img/sharp-wasm32@0.34.5': resolution: {integrity: sha512-OdWTEiVkY2PHwqkbBI8frFxQQFekHaSSkUIJkwzclWZe64O1X4UlUjqqqLaPbUpMOQk6FBu/HtlGXNblIs0huw==} @@ -218,28 +207,24 @@ packages: engines: {node: '>= 10'} cpu: [arm64] os: [linux] - libc: [glibc] '@next/swc-linux-arm64-musl@15.5.19': resolution: {integrity: sha512-5xTOE0lDlDCSSfp+BAif7j17VRRCjWp//ZPZy6NI0QpdrhxtQnsZguSx0xAAZ0c9XZLrLLwCe/XVe5YPrRilKw==} engines: {node: '>= 10'} cpu: [arm64] os: [linux] - libc: [musl] '@next/swc-linux-x64-gnu@15.5.19': resolution: {integrity: sha512-LTxRmMgqqMv05Had879W00Fm53quiJd3Zuz8h1JSNJ3nGSlbZ/7Tjs1tKyScgN3Au3t3MyPsjPlq60fMmSHLsg==} engines: {node: '>= 10'} cpu: [x64] os: [linux] - libc: [glibc] '@next/swc-linux-x64-musl@15.5.19': resolution: {integrity: sha512-eoNQSpA5PQfB9wBO4RA47MTDXWz1fizy9Y3Z6e4DetYIF3dvjuu8sj7aIGn/bFCU6lnFzTK34NtCaffP4NsQ7Q==} engines: {node: '>= 10'} cpu: [x64] os: [linux] - libc: [musl] '@next/swc-win32-arm64-msvc@15.5.19': resolution: {integrity: sha512-6UNt2dFuCHOe446sm/Kp69nUe8/wIhnh9bm6Xcqw4qEWCOppLMOvhTBVgvM7invVUNr4SPpP6NOQsACtn2IN9Q==} @@ -259,8 +244,8 @@ packages: '@types/culori@2.1.1': resolution: {integrity: sha512-NzLYD0vNHLxTdPp8+RlvGbR2NfOZkwxcYGFwxNtm+WH2NuUNV8785zv1h0sulFQ5aFQ9n/jNDUuJeo3Bh7+oFA==} - '@types/node@22.19.20': - resolution: {integrity: sha512-6tELRwSDYWW9EdZhbeZmYGZ1/7Djkt+Ah3/ScEYT9cDord7UJzasR/4D3VONg9tQI5CDp+/CZC1AXj2pCFOvpw==} + '@types/node@22.19.21': + resolution: {integrity: sha512-VMeFBSCKQKmm2swI2kW51SFusDqekC6q9trBCvJ/JliDchFSuoYYKN7yVNjPthP1HKZcx3U1gI/wTcEBjEFKTA==} '@types/react-dom@19.2.3': resolution: {integrity: sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ==} @@ -270,19 +255,8 @@ packages: '@types/react@19.2.17': resolution: {integrity: sha512-MXfmqaVPEVgkBT/aY0aGCkRWWtByiYQXo3xdQ8r5RzuFrPiRn8Gar2tQdXSUQ2GKV3bkXckek89V8wQBY2Q/Aw==} - agentation@3.0.2: - resolution: {integrity: sha512-iGzBxFVTuZEIKzLY6AExSLAQH6i6SwxV4pAu7v7m3X6bInZ7qlZXAwrEqyc4+EfP4gM7z2RXBF6SF4DeH0f2lA==} - peerDependencies: - react: '>=18.0.0' - react-dom: '>=18.0.0' - peerDependenciesMeta: - react: - optional: true - react-dom: - optional: true - - caniuse-lite@1.0.30001797: - resolution: {integrity: sha512-l8xKG+gwAIExZGl9FrF7KUwuOmk6wbEPC9Xoy/RtnWv1XG0Q4LFlagaLpUv3Kiza3W/wm27zy0yWJEieYKAP6w==} + caniuse-lite@1.0.30001799: + resolution: {integrity: sha512-hG1bReV+OUU+MOqK4t/ZWI0tZOyz3rqS9XuhOUz1cIcbwBKjOyJEJuw9ER5JuNyqxNk8u/JUVbGibBOL1yrjFw==} client-only@0.0.1: resolution: {integrity: sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA==} @@ -343,8 +317,8 @@ packages: scheduler@0.27.0: resolution: {integrity: sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==} - semver@7.8.2: - resolution: {integrity: sha512-c8jsqUZm3omBOI66G90z1Dyw5z622G8oLG+omfsHBJf3CWQTlOcwOjvOG6wtiNfW6anKm/eA39LMwMtMez2TiQ==} + semver@7.8.4: + resolution: {integrity: sha512-rUCObTnP32Q08R2uuIrt7r9PlEonuTmtuXYcW6s5kjdlj3xbnwe+21yXptAUYcMAABLkYYTtnmzb3w3EDZfueA==} engines: {node: '>=10'} hasBin: true @@ -382,7 +356,7 @@ packages: snapshots: - '@emnapi/runtime@1.10.0': + '@emnapi/runtime@1.11.1': dependencies: tslib: 2.8.1 optional: true @@ -472,7 +446,7 @@ snapshots: '@img/sharp-wasm32@0.34.5': dependencies: - '@emnapi/runtime': 1.10.0 + '@emnapi/runtime': 1.11.1 optional: true '@img/sharp-win32-arm64@0.34.5': @@ -516,7 +490,7 @@ snapshots: '@types/culori@2.1.1': {} - '@types/node@22.19.20': + '@types/node@22.19.21': dependencies: undici-types: 6.21.0 @@ -528,12 +502,7 @@ snapshots: dependencies: csstype: 3.2.3 - agentation@3.0.2(react-dom@19.2.7(react@19.2.7))(react@19.2.7): - optionalDependencies: - react: 19.2.7 - react-dom: 19.2.7(react@19.2.7) - - caniuse-lite@1.0.30001797: {} + caniuse-lite@1.0.30001799: {} client-only@0.0.1: {} @@ -550,7 +519,7 @@ snapshots: dependencies: '@next/env': 15.5.19 '@swc/helpers': 0.5.15 - caniuse-lite: 1.0.30001797 + caniuse-lite: 1.0.30001799 postcss: 8.4.31 react: 19.2.7 react-dom: 19.2.7(react@19.2.7) @@ -586,14 +555,14 @@ snapshots: scheduler@0.27.0: {} - semver@7.8.2: + semver@7.8.4: optional: true sharp@0.34.5: dependencies: '@img/colour': 1.1.0 detect-libc: 2.1.2 - semver: 7.8.2 + semver: 7.8.4 optionalDependencies: '@img/sharp-darwin-arm64': 0.34.5 '@img/sharp-darwin-x64': 0.34.5 diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml new file mode 100644 index 0000000..2bebd9b --- /dev/null +++ b/pnpm-workspace.yaml @@ -0,0 +1,3 @@ +packages: + - build + - playground diff --git a/src/tokens.host-map.md b/src/tokens.host-map.md new file mode 100644 index 0000000..b09bdef --- /dev/null +++ b/src/tokens.host-map.md @@ -0,0 +1,71 @@ +# Kilo Token Host Map + +> Generated from tokens.json by `node build`. Do not edit this file by hand. + +This file documents how the generated token artifacts map into host environments. Product skills can add stricter per-surface recipes, but they should not redefine these token values. + +## Generated Artifacts + +- `src/tokens.web.css`: OKLCH CSS variables for browser surfaces such as Cloud and Landing. +- `src/tokens.ts`: hex source values and flattened CSS-variable names for portable targets such as webviews, React Native, CLI, and native integrations. +- `src/tokens.host-map.md`: this generated host mapping reference. + +## VS Code Webview + +Kilo webviews render Kilo-dark content inside VS Code chrome. They do not follow host light themes; only high-contrast host adaptations should override Kilo-dark values. + +| Kilo token | Closest `--vscode-*` host token | Use | +| --- | --- | --- | +| `--surface-background` | `--vscode-editor-background` | Default Kilo webview canvas. | +| `--surface-raised` | `--vscode-sideBar-background` | Raised panels inside the webview. | +| `--surface-inset` | `--vscode-input-background` | Inputs, code wells, and recessed areas. | +| `--surface-overlay` | `--vscode-dropdown.background`, `--vscode-quickInput.background` | Floating surfaces. | +| `--surface-hover` | `--vscode-list-hoverBackground` | Hover rows and quiet interactive fills. | +| `--surface-selected` | `--vscode-list-activeSelectionBackground` | Selected rows and active fills. | +| `--foreground-default` | `--vscode-foreground` | Primary text and icons. | +| `--foreground-muted` | `--vscode-descriptionForeground` | Secondary text. | +| `--foreground-subtle` | `--vscode-disabledForeground` | Tertiary text. | +| `--border-default` | `--vscode-widget-border` | Default separators and outlines. | +| `--border-strong` | `--vscode-focusBorder` | Strong outlines and focus-adjacent borders. | +| `--brand-primary` | `--vscode-focusBorder` in high contrast only | Kilo-owned brand action color in normal dark mode. | +| `--diff-addSurface` | `--vscode-diffEditor.insertedTextBackground` | Inserted-line background. | +| `--diff-deleteSurface` | `--vscode-diffEditor.removedTextBackground` | Deleted-line background. | +| `--diff-addText` | `--vscode-gitDecoration-addedResourceForeground` | Inserted text accent. | +| `--diff-deleteText` | `--vscode-gitDecoration-deletedResourceForeground` | Deleted text accent. | + +## JetBrains + +JetBrains surfaces should read the native UIManager role where host chrome owns the surface, then use Kilo tokens for Kilo-owned content. Host-specific details stay in `kilo-jetbrains/AGENTS.md`. + +| Kilo role | UIManager role | Use | +| --- | --- | --- | +| `surface.background` | `Panel.background` | Default Kilo-dark panel fill. | +| `surface.raised` | `ToolWindow.background` | Raised tool-window areas. | +| `surface.inset` | `TextField.background` | Inputs and recessed code wells. | +| `surface.hover` | `List.hoverBackground` | Hover rows. | +| `surface.selected` | `List.selectionBackground` | Selected rows. | +| `foreground.default` | `Label.foreground` | Primary text. | +| `foreground.muted` | `ContextHelp.foreground` | Secondary text. | +| `border.default` | `Component.borderColor` | Default component outlines. | +| `brand.primary` | Kilo-owned accent | Primary action and brand focus roles. | + +## CLI / ANSI + +Terminal output should keep structure and contrast first. Use ANSI colors as the nearest semantic family, not as exact color matching. + +| Domain | Status family | Base token | Portable token | ANSI role | +| --- | --- | --- | --- | --- | +| `cloud` | `blue` | `--status-blue500` | `tokens.color.status.blue500` | `blue` | +| `vscode` | `purple` | `--status-purple500` | `tokens.color.status.purple500` | `magenta` | +| `cli` | `gray` | `--status-gray500` | `tokens.color.status.gray500` | `brightBlack` | +| `slack` | `teal` | `--status-teal500` | `tokens.color.status.teal500` | `cyan` | +| `agentManager` | `orange` | `--status-orange500` | `tokens.color.status.orange500` | `yellow` | +| `success` | `green` | `--status-green500` | `tokens.color.status.green500` | `green` | +| `warning` | `yellow` | `--status-yellow500` | `tokens.color.status.yellow500` | `yellow` | +| `destructive` | `red` | `--status-red500` | `tokens.color.status.red500` | `red` | + +## Notes + +- `brand.primary` is Kilo-owned. Do not remap it to arbitrary host accents unless a high-contrast mode requires it. +- `foreground` means on-surface content, not a product brand color. +- `border.inputBg` is a fill-like input token kept in the border bucket for compatibility with the current token contract. diff --git a/src/tokens.ts b/src/tokens.ts new file mode 100644 index 0000000..c6f1d50 --- /dev/null +++ b/src/tokens.ts @@ -0,0 +1,288 @@ +// Generated from tokens.json by `node build`. Do not edit this file by hand. + +export const tokenVersion = "0.2.0" as const; + +export const tokens = { + "version": "0.2.0", + "color": { + "brand": { + "primary": "#F7F586", + "primaryHover": "#E6E475", + "primaryRing": "#F7F58659", + "foreground": "#1F1F1F" + }, + "status": { + "blue300": "#93C5FD", + "blue400": "#60A5FA", + "blue500": "#3B82F6", + "blue600": "#2563EB", + "purple300": "#D8B4FE", + "purple400": "#C084FC", + "purple500": "#A855F7", + "purple600": "#9333EA", + "teal300": "#4CE7D7", + "teal400": "#00D4C2", + "teal500": "#00BAA9", + "teal600": "#009689", + "gray300": "#D4D4D8", + "gray400": "#A1A1AA", + "gray500": "#71717A", + "gray600": "#52525B", + "orange300": "#FDBA74", + "orange400": "#FB923C", + "orange500": "#F97316", + "orange600": "#EA580C", + "green300": "#86EFAC", + "green400": "#4ADE80", + "green500": "#22C55E", + "green600": "#16A34A", + "yellow300": "#FDD94A", + "yellow400": "#FBC51C", + "yellow500": "#F0A900", + "yellow600": "#D28100", + "red300": "#FCA5A5", + "red400": "#F87171", + "red500": "#EF4444", + "red600": "#DC2626" + }, + "surface": { + "inset": "#101010", + "background": "#151515", + "raised": "#202020", + "overlay": "#333333", + "hover": "#3A3A3A", + "selected": "#454545" + }, + "foreground": { + "default": "#FAFAFA", + "muted": "#A3A3A3", + "subtle": "#7A7A7A", + "onSecondary": "#FAFAFA", + "onDestructive": "#FFFFFF" + }, + "border": { + "default": "#FFFFFF1A", + "strong": "#FFFFFF2E", + "inputBg": "#FFFFFF0A" + }, + "syntax": { + "plain": "#E8E8E8", + "comment": "#7A7A7A", + "keyword": "#FF9AE2", + "string": "#ECF58C", + "number": "#F2B36B", + "function": "#93E9F6", + "type": "#00CEB9", + "property": "#9CDCFE", + "constant": "#C792EA", + "operator": "#A3A3A3" + }, + "diff": { + "addText": "#9BCD97", + "addSurface": "#1A2919", + "deleteText": "#FC533A", + "deleteSurface": "#42120B" + } + }, + "statusDomain": { + "cloud": "blue", + "vscode": "purple", + "cli": "gray", + "slack": "teal", + "agentManager": "orange", + "success": "green", + "warning": "yellow", + "destructive": "red" + }, + "typography": { + "title": { + "fontFamily": "Inter", + "fontSize": "1.5rem", + "fontWeight": 600, + "lineHeight": 1.2, + "letterSpacing": "-0.015em" + }, + "heading": { + "fontFamily": "Inter", + "fontSize": "1.125rem", + "fontWeight": 600, + "lineHeight": 1.25, + "letterSpacing": "-0.01em" + }, + "bodyLg": { + "fontFamily": "Inter", + "fontSize": "1rem", + "fontWeight": 400, + "lineHeight": 1.5 + }, + "body": { + "fontFamily": "Inter", + "fontSize": "0.875rem", + "fontWeight": 400, + "lineHeight": 1.5 + }, + "label": { + "fontFamily": "Inter", + "fontSize": "0.75rem", + "fontWeight": 500, + "lineHeight": 1.4 + }, + "eyebrow": { + "fontFamily": "Inter", + "fontSize": "0.6875rem", + "fontWeight": 500, + "lineHeight": 1.2, + "letterSpacing": "0.08em", + "textTransform": "uppercase" + }, + "code": { + "fontFamily": "Roboto Mono", + "fontSize": "0.875rem", + "fontWeight": 400, + "lineHeight": 1.5 + } + }, + "radius": { + "none": "0", + "sm": "4px", + "md": "8px", + "lg": "10px", + "xl": "14px", + "full": "9999px" + }, + "spacing": { + "1": "4px", + "2": "8px", + "3": "12px", + "4": "16px", + "5": "20px", + "6": "24px", + "8": "32px", + "10": "40px", + "12": "48px", + "0_5": "2px", + "1_5": "6px" + } +} as const; + +export const cssVars = { + "--brand-primary": "#F7F586", + "--brand-primaryHover": "#E6E475", + "--brand-primaryRing": "#F7F58659", + "--brand-foreground": "#1F1F1F", + "--status-blue300": "#93C5FD", + "--status-blue400": "#60A5FA", + "--status-blue500": "#3B82F6", + "--status-blue600": "#2563EB", + "--status-purple300": "#D8B4FE", + "--status-purple400": "#C084FC", + "--status-purple500": "#A855F7", + "--status-purple600": "#9333EA", + "--status-teal300": "#4CE7D7", + "--status-teal400": "#00D4C2", + "--status-teal500": "#00BAA9", + "--status-teal600": "#009689", + "--status-gray300": "#D4D4D8", + "--status-gray400": "#A1A1AA", + "--status-gray500": "#71717A", + "--status-gray600": "#52525B", + "--status-orange300": "#FDBA74", + "--status-orange400": "#FB923C", + "--status-orange500": "#F97316", + "--status-orange600": "#EA580C", + "--status-green300": "#86EFAC", + "--status-green400": "#4ADE80", + "--status-green500": "#22C55E", + "--status-green600": "#16A34A", + "--status-yellow300": "#FDD94A", + "--status-yellow400": "#FBC51C", + "--status-yellow500": "#F0A900", + "--status-yellow600": "#D28100", + "--status-red300": "#FCA5A5", + "--status-red400": "#F87171", + "--status-red500": "#EF4444", + "--status-red600": "#DC2626", + "--surface-inset": "#101010", + "--surface-background": "#151515", + "--surface-raised": "#202020", + "--surface-overlay": "#333333", + "--surface-hover": "#3A3A3A", + "--surface-selected": "#454545", + "--foreground-default": "#FAFAFA", + "--foreground-muted": "#A3A3A3", + "--foreground-subtle": "#7A7A7A", + "--foreground-onSecondary": "#FAFAFA", + "--foreground-onDestructive": "#FFFFFF", + "--border-default": "#FFFFFF1A", + "--border-strong": "#FFFFFF2E", + "--border-inputBg": "#FFFFFF0A", + "--syntax-plain": "#E8E8E8", + "--syntax-comment": "#7A7A7A", + "--syntax-keyword": "#FF9AE2", + "--syntax-string": "#ECF58C", + "--syntax-number": "#F2B36B", + "--syntax-function": "#93E9F6", + "--syntax-type": "#00CEB9", + "--syntax-property": "#9CDCFE", + "--syntax-constant": "#C792EA", + "--syntax-operator": "#A3A3A3", + "--diff-addText": "#9BCD97", + "--diff-addSurface": "#1A2919", + "--diff-deleteText": "#FC533A", + "--diff-deleteSurface": "#42120B", + "--radius-none": "0", + "--radius-sm": "4px", + "--radius-md": "8px", + "--radius-lg": "10px", + "--radius-xl": "14px", + "--radius-full": "9999px", + "--spacing-1": "4px", + "--spacing-2": "8px", + "--spacing-3": "12px", + "--spacing-4": "16px", + "--spacing-5": "20px", + "--spacing-6": "24px", + "--spacing-8": "32px", + "--spacing-10": "40px", + "--spacing-12": "48px", + "--spacing-0_5": "2px", + "--spacing-1_5": "6px", + "--type-title-family": "Inter", + "--type-title-size": "1.5rem", + "--type-title-weight": "600", + "--type-title-leading": "1.2", + "--type-title-tracking": "-0.015em", + "--type-heading-family": "Inter", + "--type-heading-size": "1.125rem", + "--type-heading-weight": "600", + "--type-heading-leading": "1.25", + "--type-heading-tracking": "-0.01em", + "--type-bodyLg-family": "Inter", + "--type-bodyLg-size": "1rem", + "--type-bodyLg-weight": "400", + "--type-bodyLg-leading": "1.5", + "--type-body-family": "Inter", + "--type-body-size": "0.875rem", + "--type-body-weight": "400", + "--type-body-leading": "1.5", + "--type-label-family": "Inter", + "--type-label-size": "0.75rem", + "--type-label-weight": "500", + "--type-label-leading": "1.4", + "--type-eyebrow-family": "Inter", + "--type-eyebrow-size": "0.6875rem", + "--type-eyebrow-weight": "500", + "--type-eyebrow-leading": "1.2", + "--type-eyebrow-tracking": "0.08em", + "--type-eyebrow-transform": "uppercase", + "--type-code-family": "Roboto Mono", + "--type-code-size": "0.875rem", + "--type-code-weight": "400", + "--type-code-leading": "1.5" +} as const; + +export type Tokens = typeof tokens; +export type CssVars = typeof cssVars; +export type CssVarName = keyof CssVars; + +export default tokens; diff --git a/src/tokens.web.css b/src/tokens.web.css new file mode 100644 index 0000000..6ce5aa0 --- /dev/null +++ b/src/tokens.web.css @@ -0,0 +1,118 @@ +/* Generated from tokens.json by `node build`. Do not edit this file by hand. */ +:root, +[data-theme="kilo-dark"] { + color-scheme: dark; + --brand-primary: oklch(94.895% 0.1337 107.78); + --brand-primaryHover: oklch(89.795% 0.1339 107.89); + --brand-primaryRing: oklch(94.895% 0.1337 107.78 / 0.349); + --brand-foreground: oklch(23.929% 0 0); + --status-blue300: oklch(80.907% 0.0956 251.81); + --status-blue400: oklch(71.374% 0.1434 254.62); + --status-blue500: oklch(62.308% 0.188 259.81); + --status-blue600: oklch(54.615% 0.2152 262.88); + --status-purple300: oklch(82.676% 0.1082 306.38); + --status-purple400: oklch(72.169% 0.1767 305.5); + --status-purple500: oklch(62.685% 0.2325 303.9); + --status-purple600: oklch(55.754% 0.2525 302.32); + --status-teal300: oklch(84.389% 0.1298 185.14); + --status-teal400: oklch(78.171% 0.1383 184.12); + --status-teal500: oklch(70.856% 0.1259 183.43); + --status-teal600: oklch(60.487% 0.107 184.17); + --status-gray300: oklch(87.111% 0.0055 286.29); + --status-gray400: oklch(71.181% 0.0129 286.07); + --status-gray500: oklch(55.166% 0.0138 285.94); + --status-gray600: oklch(44.186% 0.0146 285.79); + --status-orange300: oklch(83.657% 0.1165 66.29); + --status-orange400: oklch(75.764% 0.159 55.93); + --status-orange500: oklch(70.487% 0.1867 47.6); + --status-orange600: oklch(64.607% 0.1943 41.12); + --status-green300: oklch(87.116% 0.1363 154.45); + --status-green400: oklch(80.035% 0.1821 151.71); + --status-green500: oklch(72.275% 0.192 149.58); + --status-green600: oklch(62.705% 0.1699 149.21); + --status-yellow300: oklch(89.152% 0.1591 94.55); + --status-yellow400: oklch(84.811% 0.1687 87.91); + --status-yellow500: oklch(78.264% 0.1629 78.37); + --status-yellow600: oklch(67.377% 0.1489 66.94); + --status-red300: oklch(80.769% 0.1035 19.57); + --status-red400: oklch(71.063% 0.1661 22.22); + --status-red500: oklch(63.683% 0.2078 25.33); + --status-red600: oklch(57.71% 0.2152 27.33); + --surface-inset: oklch(17.304% 0 0); + --surface-background: oklch(19.573% 0 0); + --surface-raised: oklch(24.353% 0 0); + --surface-overlay: oklch(32.109% 0 0); + --surface-hover: oklch(34.846% 0 0); + --surface-selected: oklch(39.042% 0 0); + --foreground-default: oklch(98.51% 0 0); + --foreground-muted: oklch(71.547% 0 0); + --foreground-subtle: oklch(57.951% 0 0); + --foreground-onSecondary: oklch(98.51% 0 0); + --foreground-onDestructive: oklch(100% 0 0); + --border-default: oklch(100% 0 0 / 0.102); + --border-strong: oklch(100% 0 0 / 0.18); + --border-inputBg: oklch(100% 0 0 / 0.039); + --syntax-plain: oklch(93.1% 0 0); + --syntax-comment: oklch(57.951% 0 0); + --syntax-keyword: oklch(81.189% 0.1484 338.44); + --syntax-string: oklch(94.112% 0.1292 112.88); + --syntax-number: oklch(81.068% 0.1155 68.65); + --syntax-function: oklch(88.394% 0.0849 207.93); + --syntax-type: oklch(76.398% 0.1367 182.11); + --syntax-property: oklch(86.321% 0.0801 232.08); + --syntax-constant: oklch(74.275% 0.1348 311.06); + --syntax-operator: oklch(71.547% 0 0); + --diff-addText: oklch(79.908% 0.0914 142.83); + --diff-addSurface: oklch(26.201% 0.0357 142.99); + --diff-deleteText: oklch(67.191% 0.2092 31.31); + --diff-deleteSurface: oklch(26.148% 0.0761 31); + --radius-none: 0; + --radius-sm: 4px; + --radius-md: 8px; + --radius-lg: 10px; + --radius-xl: 14px; + --radius-full: 9999px; + --spacing-1: 4px; + --spacing-2: 8px; + --spacing-3: 12px; + --spacing-4: 16px; + --spacing-5: 20px; + --spacing-6: 24px; + --spacing-8: 32px; + --spacing-10: 40px; + --spacing-12: 48px; + --spacing-0_5: 2px; + --spacing-1_5: 6px; + --type-title-family: Inter; + --type-title-size: 1.5rem; + --type-title-weight: 600; + --type-title-leading: 1.2; + --type-title-tracking: -0.015em; + --type-heading-family: Inter; + --type-heading-size: 1.125rem; + --type-heading-weight: 600; + --type-heading-leading: 1.25; + --type-heading-tracking: -0.01em; + --type-bodyLg-family: Inter; + --type-bodyLg-size: 1rem; + --type-bodyLg-weight: 400; + --type-bodyLg-leading: 1.5; + --type-body-family: Inter; + --type-body-size: 0.875rem; + --type-body-weight: 400; + --type-body-leading: 1.5; + --type-label-family: Inter; + --type-label-size: 0.75rem; + --type-label-weight: 500; + --type-label-leading: 1.4; + --type-eyebrow-family: Inter; + --type-eyebrow-size: 0.6875rem; + --type-eyebrow-weight: 500; + --type-eyebrow-leading: 1.2; + --type-eyebrow-tracking: 0.08em; + --type-eyebrow-transform: uppercase; + --type-code-family: Roboto Mono; + --type-code-size: 0.875rem; + --type-code-weight: 400; + --type-code-leading: 1.5; +}