@motion-proto/live-tokens 0.82.0 → 0.83.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.
Files changed (79) hide show
  1. package/.claude/skills/live-tokens-create-component/SKILL.md +16 -4
  2. package/.claude/skills/live-tokens-create-page/SKILL.md +19 -17
  3. package/.claude/skills/live-tokens-create-page/references/interaction-sources.md +3 -3
  4. package/.claude/skills/live-tokens-pick-component/SKILL.md +10 -83
  5. package/CHANGELOG.md +53 -0
  6. package/README.md +1 -1
  7. package/bin/cli.mjs +21 -8
  8. package/bin/lib/catalogue.mjs +140 -41
  9. package/bin/rules/componentStructure.mjs +12 -4
  10. package/dist-plugin/{chunk-REBHE3ZM.js → chunk-6JKUYCPL.js} +29 -9
  11. package/dist-plugin/{chunk-D4WRIKEZ.js → chunk-LBZISJPG.js} +1 -1
  12. package/dist-plugin/index.cjs +33 -13
  13. package/dist-plugin/index.js +2 -2
  14. package/dist-plugin/migrateData/index.cjs +29 -9
  15. package/dist-plugin/migrateData/index.js +2 -2
  16. package/dist-plugin/setColors/index.cjs +29 -9
  17. package/dist-plugin/setColors/index.js +1 -1
  18. package/dist-plugin/setGeometry/index.cjs +29 -9
  19. package/dist-plugin/setGeometry/index.js +1 -1
  20. package/package.json +3 -1
  21. package/src/editor/component-editor/ImageLightboxEditor.svelte +1 -1
  22. package/src/editor/component-editor/scaffolding/types.ts +13 -8
  23. package/src/editor/core/sketch/sketchLayer.ts +1 -1
  24. package/src/editor/core/themes/migrations/2026-09-20-imagelightbox-scrim.ts +28 -0
  25. package/src/editor/core/themes/migrations/index.ts +2 -0
  26. package/src/editor/docs/content/creating-components.md +3 -0
  27. package/src/editor/docs/content.generated.ts +1 -1
  28. package/src/editor/skill-atlas/SkillAtlas.svelte +65 -38
  29. package/src/editor/skill-atlas/SkillValue.svelte +202 -0
  30. package/src/editor/skill-atlas/evalResults.ts +46 -0
  31. package/src/editor/skill-atlas/skillSources.generated.ts +4 -4
  32. package/src/editor/skill-atlas/trees/create-component.ts +18 -18
  33. package/src/editor/skill-atlas/trees/create-page.ts +28 -32
  34. package/src/editor/skill-atlas/trees/pick-component.ts +66 -196
  35. package/src/live-tokens/data/themes/autumn.json +3 -3
  36. package/src/live-tokens/data/themes/halloween.json +3 -3
  37. package/src/live-tokens/data/themes/midnight-study.json +3 -3
  38. package/src/live-tokens/data/themes/ocean.json +3 -3
  39. package/src/live-tokens/data/themes/royal-velvet.json +3 -3
  40. package/src/live-tokens/data/themes/sketchy.json +3 -3
  41. package/src/live-tokens/data/themes/spring-meadow.json +3 -3
  42. package/src/live-tokens/data/themes/sunset.json +3 -3
  43. package/src/system/components/Badge.svelte +5 -2
  44. package/src/system/components/Button.svelte +10 -2
  45. package/src/system/components/Callout.svelte +5 -2
  46. package/src/system/components/Card.svelte +8 -3
  47. package/src/system/components/CodeSnippet.svelte +6 -2
  48. package/src/system/components/CollapsibleSection.svelte +6 -2
  49. package/src/system/components/CornerBadge.svelte +5 -2
  50. package/src/system/components/Dialog.svelte +5 -2
  51. package/src/system/components/IconButton.svelte +5 -2
  52. package/src/system/components/Image.svelte +5 -2
  53. package/src/system/components/ImageLightbox.svelte +47 -4
  54. package/src/system/components/InlineEditActions.svelte +5 -2
  55. package/src/system/components/Input.svelte +6 -2
  56. package/src/system/components/MenuSelect.svelte +9 -2
  57. package/src/system/components/Notification.svelte +5 -2
  58. package/src/system/components/Panel.svelte +5 -2
  59. package/src/system/components/ProgressBar.svelte +5 -2
  60. package/src/system/components/RadioButton.svelte +5 -2
  61. package/src/system/components/SectionDivider.svelte +5 -2
  62. package/src/system/components/SegmentedControl.svelte +5 -2
  63. package/src/system/components/SideNavigation.svelte +5 -2
  64. package/src/system/components/Slider.svelte +6 -2
  65. package/src/system/components/TabBar.svelte +5 -2
  66. package/src/system/components/Table.svelte +4 -2
  67. package/src/system/components/Toggle.svelte +5 -2
  68. package/src/system/components/Tooltip.svelte +5 -2
  69. package/src/testing-js/{chunk-ZMZQZ33J.js → chunk-RDHCBQ6I.js} +2 -2
  70. package/src/testing-js/chunk-RDHCBQ6I.js.map +1 -0
  71. package/src/testing-js/{chunk-Q3YIAAG3.js → chunk-XV5CYADO.js} +2 -2
  72. package/src/testing-js/component-behavior.contract.js +1 -1
  73. package/src/testing-js/component-editor.contract.js +1 -1
  74. package/src/testing-js/component-render.contract.js +1 -1
  75. package/src/testing-js/index.js +2 -2
  76. package/src/testing-js/page-compliance.contract.js +1 -1
  77. package/src/testing-js/vitest.js +2 -2
  78. package/src/testing-js/chunk-ZMZQZ33J.js.map +0 -1
  79. /package/src/testing-js/{chunk-Q3YIAAG3.js.map → chunk-XV5CYADO.js.map} +0 -0
@@ -1,14 +1,19 @@
1
- /** A component's catalogue entry: what it is, what to use it for, what not
2
- to use it for, and what any guidance-bearing prop's values mean. Exported
3
- as `catalogue` from the runtime file's `<script module>` block — the
4
- Svelte compiler drops a leading HTML comment before it reaches the
5
- running editor, so this is the one form every reader (CLI, registry,
6
- consumer) can read. */
1
+ /** A component's catalogue entry: what it is, what to use it for, what to use
2
+ instead, the rules of use, and what any guidance-bearing prop's values
3
+ mean. Exported as `catalogue` from the runtime file's `<script module>`
4
+ block — the Svelte compiler drops a leading HTML comment before it
5
+ reaches the running editor, so this is the one form every reader (CLI,
6
+ registry, consumer) can read. */
7
7
  export type CatalogueEntry = {
8
8
  /** One sentence: what the component is. */
9
9
  description: string;
10
- useFor: string;
11
- notFor: string;
10
+ /** The condition that makes this component the right one. */
11
+ whenToUse: string;
12
+ /** Each row rules the component out. `use` is the component id
13
+ (`table`, never `Table`) that fits instead. */
14
+ whenNotToUse: Array<{ when: string; use?: string }>;
15
+ /** Rules of use, one sentence each. */
16
+ constraints?: string[];
12
17
  /** Keyed by prop name; the value explains that prop's values. */
13
18
  props?: Record<string, string>;
14
19
  };
@@ -186,7 +186,7 @@ const PART_SPECS: readonly PartSpec[] = [
186
186
  { sel: '.image-lightbox-thumb', stem: 'imagelightbox-tile', positioned: true, clips: true },
187
187
  // A fixed full-viewport scrim behind the open modal; no border of its own.
188
188
  {
189
- sel: '.image-lightbox-overlay', fill: 'var(--imagelightbox-overlay-surface)',
189
+ sel: '.image-lightbox-overlay', fill: 'var(--imagelightbox-scrim-surface)',
190
190
  stroke: 'transparent', positioned: true, unmasked: true,
191
191
  },
192
192
  // Close and the two nav chevrons all carry this class; each is fixed and
@@ -0,0 +1,28 @@
1
+ import type { Migration } from './index';
2
+
3
+ /**
4
+ * One word for the layer that dims the page (2026-09-20).
5
+ *
6
+ * Dialog called it a scrim and read the `--scrim-*` scale; ImageLightbox called
7
+ * the same layer an overlay and mixed its own colour. The token scale settles
8
+ * the word, so the key reads `-scrim-surface`. The chrome tokens keep their own
9
+ * names: they paint the toolbar, not the layer behind it.
10
+ */
11
+ const RENAMES: Record<string, string> = {
12
+ '--imagelightbox-overlay-surface': '--imagelightbox-scrim-surface',
13
+ };
14
+
15
+ export const componentMigration_2026_09_20_imagelightboxScrim: Migration = {
16
+ id: '2026-09-20-imagelightbox-scrim',
17
+ fromVersion: 35,
18
+ toVersion: 36,
19
+ appliesTo: 'component-config',
20
+ apply(rawVars, meta) {
21
+ if (meta.component !== 'imagelightbox') return { ...rawVars };
22
+ const out: Record<string, string> = {};
23
+ for (const [key, value] of Object.entries(rawVars)) {
24
+ out[RENAMES[key] ?? key] = value;
25
+ }
26
+ return out;
27
+ },
28
+ };
@@ -79,6 +79,7 @@ import { componentMigration_2026_09_13_selectedState } from './2026-09-13-select
79
79
  import { componentMigration_2026_09_13_hairline } from './2026-09-13-hairline';
80
80
  import { componentMigration_2026_09_13_indicator } from './2026-09-13-indicator';
81
81
  import { componentMigration_2026_09_13_sectiondividerSurface } from './2026-09-13-sectiondivider-surface';
82
+ import { componentMigration_2026_09_20_imagelightboxScrim } from './2026-09-20-imagelightbox-scrim';
82
83
  import { componentMigration_2026_09_13_cornerbadgePrefix } from './2026-09-13-cornerbadge-prefix';
83
84
  import { componentMigration_2026_09_13_toggleLabel } from './2026-09-13-toggle-label';
84
85
  import { componentMigration_2026_09_13_collapsiblesectionOpen } from './2026-09-13-collapsiblesection-open';
@@ -132,6 +133,7 @@ export const MIGRATIONS: Migration[] = [
132
133
  componentMigration_2026_09_13_toggleLabel,
133
134
  componentMigration_2026_09_13_collapsiblesectionOpen,
134
135
  componentMigration_2026_09_13_badgeBrand,
136
+ componentMigration_2026_09_20_imagelightboxScrim,
135
137
  ];
136
138
 
137
139
  function countFor(kind: 'colors-and-type' | 'component-config'): number {
@@ -46,6 +46,9 @@ component in the editor and confirm everything works.
46
46
 
47
47
  - A runtime component whose editable properties default to your theme tokens.
48
48
  - An editor entry that appears under **Custom** in the `/live-tokens/components` view.
49
+ - A catalogue entry in the runtime file that says what the component is for and
50
+ what to use instead. `npx live-tokens components` prints it, and the skills
51
+ read it when they choose a component for a page.
49
52
  - The naming and wiring handled for you, so the component fits the system.
50
53
 
51
54
  Advanced authors who want to write a component by hand can read the naming and
@@ -3,7 +3,7 @@
3
3
 
4
4
  export const docContent: Record<string, string> = {
5
5
  "01-overview": "# Overview\n\nLiveTokens is a design system for building Svelte microsites quickly. You\nstyle your site by editing tokens and components in a live editor. When it looks right, you save the theme and ship it.\n\n## How it works\n\n- The editor runs in your dev server, on top of your real pages. You style in\n context, not in a separate sandbox.\n- Every change updates a CSS variable, so the page repaints instantly. No\n reload, no build step.\n- Saving writes a small JSON file into your project. Shipping bakes your chosen\n theme into a plain CSS file that the build bundles.\n- The editor is dev-only. Production ships plain CSS variables and the\n components you used, nothing else.\n\n## What you can edit\n\n- **Tokens**: the design-system primitives, colour palettes, type, spacing,\n radius, shadow, and gradients, that apply across your whole site.\n- **Components**: the package ships about 25 editable components (Button,\n IconButton, Card, Dialog, Table, and more). You style components by changing\n the tokens assigned to each property.\n\n## Where to go next\n\n- **[Getting started](getting-started.md)**: scaffold a project and make your\n first edit.\n- **[Editing tokens](editing-tokens.md)**: a tour of the editor.\n- **[Sketch mode](sketch-mode.md)**: redraw the page by hand.\n- **[Themes](themes-workflow.md)**: save, switch, and ship.\n- **[Creating components](creating-components.md)**: make your own components\n editable.\n",
6
- "creating-components": "# Creating components\n\nThe package ships about 25 editable components. When you need one it doesn't\nhave, you can make your own Svelte component editable, so anyone using the\neditor can re-point its colours, type, and spacing without touching code.\n\nThe simplest way is to ask Claude. The package bundles a Claude Code skill that\nknows the conventions, writes the files, and checks the result for you.\n\n## Install the skills\n\n```bash\nnpx @motion-proto/live-tokens setup-claude\n```\n\nThis copies the bundled skills into your project's `.claude/skills/`. Once\nthey're there, Claude Code picks them up automatically.\n\n## Browse the skill atlas\n\nThe package also ships a Skill Atlas component: a diagram of the reasoning\nbehind each bundled skill, with its `SKILL.md` and reference files alongside\nso you can see exactly which lines drive which step. Import it from\n`@motion-proto/live-tokens/skill-atlas` and mount it on a route of your own:\n\n```js\n'/skills': {\n lazy: () => import('@motion-proto/live-tokens/skill-atlas'),\n},\n```\n\n## Ask for a component\n\nDescribe what you want in plain English. Phrases like these trigger the skill:\n\n- \"Add a Toggle component to live-tokens\"\n- \"Make this Svelte component editable in the live-tokens editor\"\n- \"Create a Stat component with a value and a label\"\n\nClaude asks any clarifying questions it needs (which variants, which states,\nwhich parts), then writes the component, registers it with the editor, and runs\nits verification checklist. When it finishes, open `/live-tokens/components` to see your new\ncomponent in the editor and confirm everything works.\n\n## What you get\n\n- A runtime component whose editable properties default to your theme tokens.\n- An editor entry that appears under **Custom** in the `/live-tokens/components` view.\n- The naming and wiring handled for you, so the component fits the system.\n\nAdvanced authors who want to write a component by hand can read the naming and\nstate-model conventions shipped in the package\n(`src/system/styles/CONVENTIONS.md` and the skill's own `SKILL.md`).\n",
6
+ "creating-components": "# Creating components\n\nThe package ships about 25 editable components. When you need one it doesn't\nhave, you can make your own Svelte component editable, so anyone using the\neditor can re-point its colours, type, and spacing without touching code.\n\nThe simplest way is to ask Claude. The package bundles a Claude Code skill that\nknows the conventions, writes the files, and checks the result for you.\n\n## Install the skills\n\n```bash\nnpx @motion-proto/live-tokens setup-claude\n```\n\nThis copies the bundled skills into your project's `.claude/skills/`. Once\nthey're there, Claude Code picks them up automatically.\n\n## Browse the skill atlas\n\nThe package also ships a Skill Atlas component: a diagram of the reasoning\nbehind each bundled skill, with its `SKILL.md` and reference files alongside\nso you can see exactly which lines drive which step. Import it from\n`@motion-proto/live-tokens/skill-atlas` and mount it on a route of your own:\n\n```js\n'/skills': {\n lazy: () => import('@motion-proto/live-tokens/skill-atlas'),\n},\n```\n\n## Ask for a component\n\nDescribe what you want in plain English. Phrases like these trigger the skill:\n\n- \"Add a Toggle component to live-tokens\"\n- \"Make this Svelte component editable in the live-tokens editor\"\n- \"Create a Stat component with a value and a label\"\n\nClaude asks any clarifying questions it needs (which variants, which states,\nwhich parts), then writes the component, registers it with the editor, and runs\nits verification checklist. When it finishes, open `/live-tokens/components` to see your new\ncomponent in the editor and confirm everything works.\n\n## What you get\n\n- A runtime component whose editable properties default to your theme tokens.\n- An editor entry that appears under **Custom** in the `/live-tokens/components` view.\n- A catalogue entry in the runtime file that says what the component is for and\n what to use instead. `npx live-tokens components` prints it, and the skills\n read it when they choose a component for a page.\n- The naming and wiring handled for you, so the component fits the system.\n\nAdvanced authors who want to write a component by hand can read the naming and\nstate-model conventions shipped in the package\n(`src/system/styles/CONVENTIONS.md` and the skill's own `SKILL.md`).\n",
7
7
  "editing-tokens": "# Editing tokens\n\nA tour of the editor. The page behind it repaints on every change; saving\nwrites a theme file you can reload later.\n\nThe editor has four views:\n\n- **Tokens**: the design-system primitives (colour, type, spacing, and so on).\n They apply everywhere your site uses them.\n- **Color Wheel**: the harmony wheel, the palette curves, and the story your colours\n tell across a page.\n- **Components**: per-component editors. Re-Assign what tokens a component uses\n without changing the underlying system.\n- **Sketchstyle**: an effect layer that redraws the page by hand. See\n [Sketch mode](sketch-mode.md).\n\nThis page covers **Tokens**. For components, see\n[Creating components](creating-components.md).\n\n## Palettes\n\nMost colour work happens here. Each palette (Brand, Accent, Neutral, Canvas,\nSuccess, Warning, Info, Danger, and a few more) has:\n\n- **Base colour.** Pick a hex; the palette derives an 11-step ramp (100 to 950)\n from it.\n- **Curves.** Three curves shape the ramp, in stack order: Hue, Saturation,\n Lightness. Drag the handles to bias it warmer or cooler, more or less\n saturated, darker or lighter. Hue drifts the ramp's temperature without\n moving contrast, because OKLCH hue rotation is close to lightness-preserving.\n It holds ±45 degrees; a bigger shift belongs on the base colour or the\n harmony axis.\n- **Overrides.** Lock a single step to a hand-picked hex when the curve doesn't\n land where you want.\n\nEditing a palette base ripples through every colour that depends on it, in real\ntime. Colours use OKLCH, so the ramp stays perceptually even across hues\nwithout muddy mid-tones.\n\n## Type\n\n- **Fonts.** Add sources from Google Fonts, Adobe (Typekit), a CSS URL, or an\n inline `@font-face`. The font loads in the page as soon as you add it.\n- **Stacks.** Named font cascades you reference by token, such as a display\n stack and a body stack.\n- **Sizes and weights.** A t-shirt scale (xs, sm, md, lg, xl, 2xl…) for size and\n a numeric scale (100 to 900) for weight.\n\n## Spacing, radius, shadow\n\nNumeric scales with a slider per step.\n\n- **Spacing**: the padding, gap, and margin scale.\n- **Radius**: none through full.\n- **Shadow**: colour, offset, blur, spread, and opacity per step, with stacked\n shadows supported.\n\nChange a step and every element using it repaints.\n\n## Washes and gradients\n\n- **Scrims** are translucent layers that dim what sits behind them, like the\n one a dialog draws over the page. Set a colour and opacity per stop.\n- **Tints** are the opposite operation: they shade the surface they sit on\n rather than dimming what is behind it, which is what a hover needs.\n- **Gradients** are reusable gradient tokens with a stop list and direction, for\n hero panels and accent backgrounds.\n\n## Columns\n\nThe page-grid overlay. Set column count, gutter, and outer margin, and toggle\nthe visual guide with `Cmd/Ctrl+G`. Pages built on the column system reflow\nlive.\n\n## Saving\n\nThe editor saves to your browser continuously, so work survives a reload\nmid-edit. Writing a file is a separate step: the **Theme** panel at the foot of\nthe sidebar has **Save**, **Save As**, and **Load**, and each theme is one JSON\nfile under `src/live-tokens/data/themes/`.\n\nThe header gives you undo/redo (`Cmd/Ctrl+Z`, `Cmd/Ctrl+Shift+Z`). You can keep\nmany themes side by side; one is open at a time, and only **Adopt** publishes\none. See [Themes](themes-workflow.md) for the full lifecycle.\n",
8
8
  "getting-started": "# Getting started\n\nScaffold a live token site in a moments. You need Node 20 or later, a\npackage manager (npm, pnpm, or yarn), and a browser. Open claude code in your repo and start building.\n\n## Scaffold a new app\n\n```bash\nnpm create @motion-proto/live-tokens@latest my-app\ncd my-app\nnpm install\nnpm run dev\n```\n\nOpen the URL Vite prints (usually `http://localhost:5173`). You get a\none-page Svelte + Vite app that depends on the published package, with the\neditor wired up and the full component set ready to import.\n\n`npx @motion-proto/live-tokens create my-app` runs the same scaffold without\nthe initialiser package.\n\n### What the scaffold gives you\n\nEvery editable file lives under `src/` and is committed, so `npm install` and\nversion upgrades never touch your styles. The package code stays in\n`node_modules`.\n\n| Path | What it is |\n|------|------------|\n| `src/pages/Home.svelte` | The starter page. Replace it with your own content. |\n| `src/App.svelte` | Your routes. `<LiveTokensRouter>` adds dev-only routes under a reserved `/live-tokens/*` namespace: `/live-tokens/editor`, `/live-tokens/components`, and `/live-tokens/docs`. |\n| `src/system/styles/tokens.css` | Your base token vocabulary, hand-authored. |\n| `src/styles/site.css` | Themed page typography, yours to edit. |\n\n## Your first edit\n\n1. Run `npm run dev` and open the home page.\n2. Click **Open Token Editor**, or visit `/live-tokens/editor`. The editor opens beside\n the page.\n3. Open **Palettes**, pick **Brand**, and change the base hex. The page\n repaints as you type.\n4. In the **Theme** panel at the foot of the sidebar, choose **Save As**. Your\n theme appears as JSON under `src/live-tokens/data/themes/`.\n5. Reload. The editor reopens on your theme, so the page returns as you left\n it.\n\n## What you just changed\n\nEvery edit sets a CSS custom property on `:root`. Your components read those\nproperties through `var(--...)`. There is no token build step and no\npreprocessor rewriting your code: the page renders against plain CSS variables\nthe editor swaps live.\n\nTo ship, click **Adopt** in the Theme panel. That saves the open theme and bakes\nit into `src/live-tokens/data/tokens.generated.css`, which your build bundles\nalongside `tokens.css`. Adopt is the only action that changes what your site\nships, so try any theme you like first. The editor itself never reaches\nproduction.\n\nAlready have a Svelte 5 + Vite app? The\n[README](https://github.com/motionproto/live-tokens#readme) covers installing\ninto an existing project.\n\n## Where to go next\n\n- **[Editing tokens](editing-tokens.md)**: a tour of the editor.\n- **[Themes](themes-workflow.md)**: save, switch, and ship.\n- **[Creating components](creating-components.md)**: make your own component\n editable.\n",
9
9
  "light-and-dark": "# Light and dark\n\nSome things on a page cannot be written as a token. A wordmark drawn in white\ndisappears on a pale theme. Ink that multiplies onto paper vanishes on a dark\none. A photograph behind a headline is dark no matter what the palette says.\n\nEach of those needs the same fact first: which way does the surface behind this\nthing lean? One attribute carries it.\n\n## The attribute\n\n`data-backdrop` is either `light` or `dark`, and it does two things at once: it\nselects, so a rule can key on it, and it sets `color-scheme`, so every\n`light-dark()` under it resolves the half that reads.\n\n```css\n.title {\n color: light-dark(var(--color-black), var(--color-white));\n}\n```\n\nThat line is right on both sides of the theme, and it is right inside a dark\nsection on a pale page, because the nearest `color-scheme` wins.\n\n## Stating it\n\nPut it in the markup when the surface knows its own tone — a hero over a\nphotograph, a plate that stays pale in every theme:\n\n```svelte\n<div class=\"hero-panel\" data-backdrop=\"dark\">\n```\n\nA stated tone beats any measurement, and it inherits, so everything inside the\npanel resolves against it.\n\n## Measuring it\n\nWhere the tone is a property of the theme rather than of the markup, let it be\nmeasured:\n\n```svelte\n<script>\n import { backdrop } from '@motion-proto/live-tokens/backdrop';\n</script>\n\n<section use:backdrop>\n```\n\nThe action reads whatever actually paints behind the element — the nearest\nancestor with an opaque fill, averaged across its gradient stops, falling back\nto the theme's `--page-bg` — and stamps the answer. It re-reads when the theme\nchanges, which the editor does by rewriting custom properties with no reload,\nso the stamp follows a live edit.\n\nThe page itself is stamped for you: the build bakes the production theme's\npolarity into `tokens.generated.css`, so the first paint is already right, and\n`syncDocumentBackdrop()` keeps `<html>` current as themes switch.\n\n```ts\nimport { syncDocumentBackdrop } from '@motion-proto/live-tokens/backdrop';\n\nsyncDocumentBackdrop();\n```\n\n## Reading it from JavaScript\n\nAnything that paints outside CSS — a canvas, a WebGL uniform, an `<img>` that\ncomes in two versions — asks the same question through the same module:\n\n```ts\nimport { isLightBackdrop, watchBackdrop, cssColorToHex } from '@motion-proto/live-tokens/backdrop';\n\nconst stop = watchBackdrop(logoEl, {\n stamp: false,\n onChange: (polarity) => (src = polarity === 'light' ? darkMark : lightMark),\n});\n```\n\n`isLightBackdrop(el)` answers once. `watchBackdrop` keeps answering and returns\na stop function. `cssColorToHex` resolves any CSS colour — including the\n`oklch()` a token holds — to a hex a non-CSS consumer can take.\n\n## What it does not do\n\nPolarity is a property of a surface, not of a component, so nothing is stamped\nfor you below `<html>`: a section that needs an answer either states one or asks\nfor one. And a measurement reads the paint at the moment it runs — an element\nthat scrolls from a pale section onto a dark one keeps the answer it was given.\nState the tone on each section instead.\n",
@@ -3,6 +3,7 @@
3
3
  import { navigate } from '../core/routing/router';
4
4
  import Button from '../../system/components/Button.svelte';
5
5
  import TabBar from '../../system/components/TabBar.svelte';
6
+ import SkillValue from './SkillValue.svelte';
6
7
  import SourcePane from './SourcePane.svelte';
7
8
  import TreeCanvas from './TreeCanvas.svelte';
8
9
  import { linkHash, linkTargets, resolveLink } from './atlasLink';
@@ -12,23 +13,35 @@
12
13
 
13
14
  // `#set-type` opens that skill and `#set-type/write-the-font-pairing/voice`
14
15
  // also selects that block, so a link can hand someone one step of one tree.
16
+ const VALUE_TAB = 'measured-value';
17
+ const firstSkill = Object.keys(skillTrees)[0];
18
+
19
+ function isValueHash(hash: string) {
20
+ return hash === `#${VALUE_TAB}`;
21
+ }
22
+
15
23
  const linked = resolveLink(window.location.hash, skillTrees);
16
- let active = $state(linked?.skill ?? Object.keys(skillTrees)[0]);
24
+ let active = $state(
25
+ isValueHash(window.location.hash) ? VALUE_TAB : (linked?.skill ?? firstSkill),
26
+ );
17
27
  let selection: Selection | null = $state(linked?.target ?? null);
18
28
  /** Which of the skill's documents the source pane shows. */
19
29
  let doc: string = $state(SKILL_DOC);
20
30
 
21
- let tree = $derived(skillTrees[active]);
22
- let docs = $derived(skillDocs[active]);
31
+ // The value tab has no tree; the panes it hides keep reading a real skill.
32
+ let skill = $derived(active === VALUE_TAB ? firstSkill : active);
33
+ let tree = $derived(skillTrees[skill]);
34
+ let docs = $derived(skillDocs[skill]);
23
35
  let lines = $derived(docs[doc] ?? docs[SKILL_DOC]);
24
36
  let siblings = $derived(Object.keys(docs).filter((name) => name !== SKILL_DOC));
25
37
 
26
- let tabs = $derived(
27
- Object.entries(skillTrees).map(([id, t]) => ({
38
+ let tabs = $derived([
39
+ ...Object.entries(skillTrees).map(([id, t]) => ({
28
40
  id,
29
41
  label: `${t.title}\n${skillDocs[id][SKILL_DOC].length} lines`,
30
42
  })),
31
- );
43
+ { id: VALUE_TAB, label: 'Measured value\n1 eval' },
44
+ ]);
32
45
 
33
46
  let docTabs = $derived(
34
47
  Object.keys(docs).map((name) => ({
@@ -52,6 +65,10 @@
52
65
  }
53
66
 
54
67
  function shareSelection() {
68
+ if (active === VALUE_TAB) {
69
+ history.replaceState(null, '', `${window.location.pathname}#${VALUE_TAB}`);
70
+ return;
71
+ }
55
72
  const target = linkTargets(tree).find((t) => t.key === selection?.key) ?? null;
56
73
  history.replaceState(null, '', `${window.location.pathname}${linkHash(active, target)}`);
57
74
  }
@@ -94,6 +111,12 @@
94
111
  }
95
112
 
96
113
  function openLink(hash: string) {
114
+ if (isValueHash(hash)) {
115
+ active = VALUE_TAB;
116
+ selection = null;
117
+ doc = SKILL_DOC;
118
+ return;
119
+ }
97
120
  const link = resolveLink(hash, skillTrees);
98
121
  if (!link) return;
99
122
  active = link.skill;
@@ -134,38 +157,42 @@
134
157
  <TabBar {tabs} value={active} onchange={changeTab} />
135
158
  </div>
136
159
 
137
- <div class="split">
138
- <section class="pane" aria-label="{tree.id} decision tree">
139
- <div class="pane-head">
140
- <span class="pane-title">{tree.id}</span>
141
- <span class="pane-note">{tree.nodes.length} steps</span>
142
- </div>
143
- <div class="pane-body" bind:this={treePane}>
144
- <h2 class="tagline">{tree.tagline}</h2>
145
-
146
- <TreeCanvas {tree} selected={selection?.key ?? null} onselect={selectTarget} onopen={openDoc} />
147
- </div>
148
- </section>
149
-
150
- <section class="pane pane-source" aria-label="{tree.id} source">
151
- <div class="pane-head">
152
- <span class="pane-title">{tree.id}/{doc}</span>
153
- <span class="pane-note">{lines.length} lines</span>
154
- </div>
155
- <div class="doc-tabs">
156
- <TabBar tabs={docTabs} value={doc} onchange={openDoc} />
157
- </div>
158
- <div class="pane-body" bind:this={sourcePane}>
159
- <SourcePane
160
- {lines}
161
- {siblings}
162
- highlight={doc === SKILL_DOC ? (selection?.lines ?? null) : null}
163
- onpick={selectFromLine}
164
- onopen={openDoc}
165
- />
166
- </div>
167
- </section>
168
- </div>
160
+ {#if active === VALUE_TAB}
161
+ <SkillValue />
162
+ {:else}
163
+ <div class="split">
164
+ <section class="pane" aria-label="{tree.id} decision tree">
165
+ <div class="pane-head">
166
+ <span class="pane-title">{tree.id}</span>
167
+ <span class="pane-note">{tree.nodes.length} steps</span>
168
+ </div>
169
+ <div class="pane-body" bind:this={treePane}>
170
+ <h2 class="tagline">{tree.tagline}</h2>
171
+
172
+ <TreeCanvas {tree} selected={selection?.key ?? null} onselect={selectTarget} onopen={openDoc} />
173
+ </div>
174
+ </section>
175
+
176
+ <section class="pane pane-source" aria-label="{tree.id} source">
177
+ <div class="pane-head">
178
+ <span class="pane-title">{tree.id}/{doc}</span>
179
+ <span class="pane-note">{lines.length} lines</span>
180
+ </div>
181
+ <div class="doc-tabs">
182
+ <TabBar tabs={docTabs} value={doc} onchange={openDoc} />
183
+ </div>
184
+ <div class="pane-body" bind:this={sourcePane}>
185
+ <SourcePane
186
+ {lines}
187
+ {siblings}
188
+ highlight={doc === SKILL_DOC ? (selection?.lines ?? null) : null}
189
+ onpick={selectFromLine}
190
+ onopen={openDoc}
191
+ />
192
+ </div>
193
+ </section>
194
+ </div>
195
+ {/if}
169
196
  </div>
170
197
 
171
198
  <style>
@@ -0,0 +1,202 @@
1
+ <script lang="ts">
2
+ import Table from '../../system/components/Table.svelte';
3
+ import { pickComponentEval } from './evalResults';
4
+
5
+ const result = pickComponentEval;
6
+ const [withSkills, without] = result.arms;
7
+ const difference = (withSkills.score - without.score).toFixed(2);
8
+ </script>
9
+
10
+ <article class="skill-value" aria-labelledby="skill-value-title">
11
+ <div class="measure">
12
+ <p class="eyebrow">Measured value</p>
13
+ <h2 id="skill-value-title">The skills make an agent faster. The component entries make it right.</h2>
14
+ <p class="lead">
15
+ An eval asked an agent to choose a component for twelve requirements, three times with the
16
+ skills loaded and three times with none. Every run that finished chose all twelve correctly,
17
+ in both arms. The skills changed the cost of getting there.
18
+ </p>
19
+ </div>
20
+
21
+ <div class="results">
22
+ <Table>
23
+ <table>
24
+ <thead>
25
+ <tr>
26
+ <th scope="col"><span class="visually-hidden">Arm</span></th>
27
+ <th scope="col">Score</th>
28
+ <th scope="col">Requirements right</th>
29
+ <th scope="col">Ran the catalogue command</th>
30
+ <th scope="col">Turns</th>
31
+ <th scope="col">Seconds</th>
32
+ </tr>
33
+ </thead>
34
+ <tbody>
35
+ {#each result.arms as arm (arm.label)}
36
+ <tr>
37
+ <td class="arm">{arm.label}</td>
38
+ <td>{arm.score.toFixed(2)}</td>
39
+ <td>{arm.rowsRight}</td>
40
+ <td>{arm.ranCatalogue}</td>
41
+ <td>{arm.turns}</td>
42
+ <td>{arm.seconds}</td>
43
+ </tr>
44
+ {/each}
45
+ </tbody>
46
+ </table>
47
+ </Table>
48
+ </div>
49
+
50
+ <div class="measure">
51
+ <h3>Accuracy comes from the component</h3>
52
+ <p>
53
+ Each component file carries its own entry: <code>whenToUse</code> states the condition that
54
+ makes it right, and <code>whenNotToUse</code> lists the conditions that rule it out, each with
55
+ the component to use in its place. An agent with no skills opened the component files, found
56
+ those entries, and made the same twelve choices, including the two requirements nothing
57
+ shipped fits and the destructive action that belongs in a Dialog.
58
+ </p>
59
+
60
+ <h3>Efficiency comes from the skill</h3>
61
+ <p>
62
+ The pick-component skill sends the agent to one command,
63
+ <code>npx live-tokens components --json</code>, which returns every entry at once. With the
64
+ skills the agent answered in {withSkills.turns} turns and under a minute. Without them it
65
+ never found the command, read files one at a time for {without.turns} turns, and one of its
66
+ three runs reached the five minute limit before it answered.
67
+ </p>
68
+
69
+ <h3>Method</h3>
70
+ <ul>
71
+ <li>Case <code>{result.id}</code>, run on {result.date}.</li>
72
+ <li>Twelve requirements: ten with one right component, two that nothing shipped fits.</li>
73
+ <li>Three runs per arm. The runner adds the arm without skills on its own.</li>
74
+ <li>
75
+ Each requirement has its own pattern grader, so a failure names its row. One more grader
76
+ checks that the agent ran the catalogue command.
77
+ </li>
78
+ <li>
79
+ The score is the share of graders that pass. The difference of {difference} comes from one
80
+ timeout and from the catalogue-command grader. No finished run chose a wrong component.
81
+ </li>
82
+ </ul>
83
+
84
+ <h3>Limits</h3>
85
+ <p>
86
+ Three runs per arm is a small sample, and one timeout moves the score a long way. The twelve
87
+ requirements each have a clear answer in the entries. A harder case, where two components
88
+ both fit, has not been measured.
89
+ </p>
90
+ </div>
91
+ </article>
92
+
93
+ <style>
94
+ /* The atlas locks to the viewport on desktop, so the report scrolls itself. */
95
+ .skill-value {
96
+ flex: 1 1 auto;
97
+ min-height: 0;
98
+ overflow-y: auto;
99
+ padding-bottom: var(--space-48);
100
+ color: var(--text-secondary);
101
+ }
102
+
103
+ .measure {
104
+ max-width: 70ch;
105
+ }
106
+
107
+ .results {
108
+ max-width: 110ch;
109
+ margin: var(--space-32) 0 var(--space-40);
110
+ }
111
+
112
+ .eyebrow {
113
+ margin: 0 0 var(--space-8);
114
+ font-family: var(--eyebrow-font-family);
115
+ font-size: var(--eyebrow-font-size);
116
+ font-weight: var(--eyebrow-font-weight);
117
+ line-height: var(--eyebrow-line-height);
118
+ letter-spacing: var(--eyebrow-letter-spacing);
119
+ text-transform: var(--eyebrow-text-transform);
120
+ color: var(--text-tertiary);
121
+ }
122
+
123
+ h2 {
124
+ margin: 0 0 var(--space-24);
125
+ font-family: var(--heading-lg-font-family);
126
+ font-size: var(--heading-lg-font-size);
127
+ font-weight: var(--heading-lg-font-weight);
128
+ line-height: var(--heading-lg-line-height);
129
+ letter-spacing: var(--heading-lg-letter-spacing);
130
+ color: var(--text-primary);
131
+ }
132
+
133
+ h3 {
134
+ margin: var(--space-40) 0 var(--space-12);
135
+ font-family: var(--heading-sm-font-family);
136
+ font-size: var(--heading-sm-font-size);
137
+ font-weight: var(--heading-sm-font-weight);
138
+ line-height: var(--heading-sm-line-height);
139
+ letter-spacing: var(--heading-sm-letter-spacing);
140
+ color: var(--text-primary);
141
+ }
142
+
143
+ .measure > h3:first-child {
144
+ margin-top: 0;
145
+ }
146
+
147
+ p,
148
+ li {
149
+ font-family: var(--body-md-font-family);
150
+ font-size: var(--body-md-font-size);
151
+ font-weight: var(--body-md-font-weight);
152
+ line-height: var(--body-md-line-height);
153
+ letter-spacing: var(--body-md-letter-spacing);
154
+ }
155
+
156
+ p {
157
+ margin: 0 0 var(--space-16);
158
+ }
159
+
160
+ .lead {
161
+ margin-bottom: 0;
162
+ color: var(--text-primary);
163
+ }
164
+
165
+ ul {
166
+ margin: 0;
167
+ padding-left: var(--space-24);
168
+ }
169
+
170
+ li + li {
171
+ margin-top: var(--space-8);
172
+ }
173
+
174
+ code {
175
+ font-family: var(--code-font-family);
176
+ font-size: var(--code-font-size);
177
+ font-weight: var(--code-font-weight);
178
+ line-height: var(--code-line-height);
179
+ letter-spacing: var(--code-letter-spacing);
180
+ background: var(--tint-low);
181
+ padding-inline: var(--space-4);
182
+ border-radius: var(--radius-sm);
183
+ /* The code face joins `--` into one dash, and a line may break between the
184
+ two hyphens. Either misprints a CLI flag. */
185
+ font-variant-ligatures: none;
186
+ white-space: nowrap;
187
+ }
188
+
189
+ .results td.arm {
190
+ color: var(--text-primary);
191
+ white-space: nowrap;
192
+ }
193
+
194
+ .visually-hidden {
195
+ position: absolute;
196
+ width: 1px;
197
+ height: 1px;
198
+ overflow: hidden;
199
+ clip-path: inset(50%);
200
+ white-space: nowrap;
201
+ }
202
+ </style>
@@ -0,0 +1,46 @@
1
+ /** One arm of an eval: the same case run with the skills loaded, or without them. */
2
+ export interface EvalArm {
3
+ label: string;
4
+ score: number;
5
+ rowsRight: string;
6
+ ranCatalogue: string;
7
+ turns: string;
8
+ seconds: string;
9
+ }
10
+
11
+ export interface EvalResult {
12
+ id: string;
13
+ date: string;
14
+ packageVersion: string;
15
+ runsPerArm: number;
16
+ requirements: number;
17
+ arms: [withSkills: EvalArm, without: EvalArm];
18
+ }
19
+
20
+ // Recorded by hand from `npm run eval` in the live-tokens repository. The
21
+ // evals do not ship, so the page cannot read a result file at runtime.
22
+ export const pickComponentEval: EvalResult = {
23
+ id: 'outcome-pick-component',
24
+ date: '2026-09-20',
25
+ packageVersion: '0.82.0, plus the unreleased catalogue entry',
26
+ runsPerArm: 3,
27
+ requirements: 12,
28
+ arms: [
29
+ {
30
+ label: 'With the skills',
31
+ score: 1,
32
+ rowsRight: '12 of 12 in all 3 runs',
33
+ ranCatalogue: '3 of 3 runs',
34
+ turns: '9 to 10',
35
+ seconds: '42 to 58',
36
+ },
37
+ {
38
+ label: 'Without',
39
+ score: 0.62,
40
+ rowsRight: '12 of 12 in the 2 runs that finished',
41
+ ranCatalogue: '0 of 3 runs',
42
+ turns: '15 to 16',
43
+ seconds: '70 to 72, and one run timed out at 300',
44
+ },
45
+ ],
46
+ };