@motion-proto/live-tokens 0.91.1 → 0.92.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude/skills/live-tokens-create-component/references/sketch-mode.md +7 -4
- package/.claude/skills/live-tokens-create-page/SKILL.md +10 -3
- package/.claude/skills/live-tokens-set-type/SKILL.md +3 -3
- package/CHANGELOG.md +153 -0
- package/bin/lib/tokenVocabulary.mjs +2 -2
- package/bin/rules/testRuns.mjs +1 -1
- package/bin/rules/tokens.mjs +3 -3
- package/dist-plugin/chunk-JOSSYGR2.js +1634 -0
- package/dist-plugin/index.cjs +932 -15
- package/dist-plugin/index.js +1 -1
- package/dist-plugin/setType/index.cjs +23 -2
- package/dist-plugin/setType/index.js +23 -2
- package/dist-plugin/tokensCssMigrations/index.cjs +936 -15
- package/dist-plugin/tokensCssMigrations/index.d.cts +23 -3
- package/dist-plugin/tokensCssMigrations/index.d.ts +23 -3
- package/dist-plugin/tokensCssMigrations/index.js +5 -1
- package/package.json +1 -1
- package/src/app/site.css +243 -9
- package/src/demo/TestingLoops.svelte +9 -9
- package/src/editor/core/fonts/weightCoverage.ts +31 -2
- package/src/editor/core/palettes/tokenRegistry.ts +36 -37
- package/src/editor/core/sketch/sketchLayer.ts +23 -17
- package/src/editor/core/themes/parsers/mediaBlocks.ts +46 -0
- package/src/editor/core/typeScale/typeScale.ts +341 -0
- package/src/editor/skill-atlas/SourcePane.svelte +3 -3
- package/src/editor/skill-atlas/skillSources.generated.ts +3 -3
- package/src/editor/skill-atlas/trees/create-page.ts +14 -14
- package/src/editor/skill-atlas/trees/set-type.ts +2 -2
- package/src/editor/ui/UIFontFamilySelector.svelte +5 -1
- package/src/editor/ui/UIFontWeightSelector.svelte +4 -0
- package/src/editor/ui/UILetterSpacingSelector.svelte +4 -0
- package/src/editor/ui/UILineHeightSelector.svelte +4 -0
- package/src/editor/ui/UITextTransformSelector.svelte +4 -0
- package/src/editor/ui/UIVariantSelector.svelte +6 -8
- package/src/editor/ui/VariablesTab.svelte +4 -4
- package/src/editor/ui/sections/textStyles.ts +44 -92
- package/src/editor/ui/text-styles/ControlBody.svelte +100 -0
- package/src/editor/ui/text-styles/ResetIcon.svelte +60 -0
- package/src/editor/ui/text-styles/StepRow.svelte +330 -0
- package/src/editor/ui/text-styles/StyleControl.svelte +128 -0
- package/src/editor/ui/text-styles/TextStylesSection.svelte +551 -0
- package/src/editor/ui/text-styles/textStyleEdits.ts +308 -0
- package/src/editor/ui/variantScales.ts +16 -0
- package/src/system/styles/tokens.css +412 -48
- package/src/testing-js/page-compliance.contract.js +233 -80
- package/src/testing-js/page-compliance.contract.js.map +1 -1
- package/dist-plugin/chunk-GWMEYBZ2.js +0 -715
- package/src/editor/ui/TextStylesSection.svelte +0 -171
|
@@ -13,11 +13,11 @@ export const skillDocs: Record<string, Record<string, string[]>> = {
|
|
|
13
13
|
"references/fixed-overlays.md": ["# Fixed overlays must portal to body","","Any `position: fixed` layer (modal, lightbox, full-screen backdrop) is trapped, clipped or painted under other chrome, by a transformed / `isolation` / `contain` / `will-change` ancestor. Real consumer pages and the editor's own preview pane both have one, so this is the normal case rather than the edge.","","Render the layer with `use:portal` from `src/system/internal/portal.ts` so it escapes to `<body>`. `use:portal={enabled}` keeps an in-flow preview variant where it is; `Dialog` is the worked example. `check:overlay-portal` fails the build when a component sets `position: fixed` without it. Anchored popovers are exempt: `Tooltip` is `position: absolute` against its trigger and belongs in the flow.","","Moving to `<body>` costs two things:","","- DOM events from the layer no longer bubble to a consumer ancestor, so pass component callbacks the way `Dialog` does.","- A subtree-scoped CSS-variable theme no longer reaches it. This library themes via `:root`, so nothing breaks here.","","A modal also needs `role=\"dialog\"` with `aria-modal`, focus moved in on open and restored on close, and `Tab` trapped inside. `ImageLightbox` is the worked example."],
|
|
14
14
|
"references/intrinsics.md": ["# Extension: intrinsics","","Some components expose **structural or display choices** that are not token values: an alignment (start / center), an element's visibility (show / hide), a layout position. These ride a custom `<select>` or checkbox authored in an editor snippet, outside the generic token grid, so they do not belong in `allTokens`. Toggle and most components have none. SectionDivider is the worked example (alignment, hairline position, eyebrow / description visibility).","","An intrinsic still cascades through a CSS custom property with a default in the runtime `:global(:root)`. The trap: that default now lives in two places, the runtime `:global(:root)` AND the editor's read-back getter. When they disagree the control displays a state the page never renders, and a native `<select>` won't even fire `onchange` to write the \"change\" the user thinks they made. `:global(:root)` is the source of truth.","","Declare intrinsics so the editor and the contract test stay honest:","","1. **Runtime `:global(:root)`** carries the per-variant default like any other variable:",""," ```css"," --mywidget-lg-align: start;"," --mywidget-lg-eyebrow-display: block;"," ```","","2. **Editor `<script module>`** exports `intrinsics: IntrinsicSpec[]`, one entry per structural property, each `default` mirroring `:global(:root)` per variant:",""," ```ts"," import type { IntrinsicSpec } from '@motion-proto/live-tokens/component-editor';",""," export const intrinsics: IntrinsicSpec[] = ["," {"," key: 'align',"," variants: ['lg', 'md', 'sm'],"," variable: (v) => `--mywidget-${v}-align`,"," values: ['start', 'center'],"," default: { lg: 'start', md: 'start', sm: 'start' },"," },"," ];"," ```","","3. **Read-back getters fall back to the spec default**, never a hard-coded constant. This is the rule that keeps the control's displayed default in step with what an unedited instance renders:",""," ```ts"," const byKey = new Map(intrinsics.map((i) => [i.key, i]));"," function readIntrinsic(key: string, v: string): string {"," const spec = byKey.get(key)!;"," const raw = readLiteral(spec.variable(v)) ?? spec.default[v]; // store override, else runtime default"," return spec.normalize ? spec.normalize(raw) : raw;"," }"," function getAlign(v: string) {"," return readIntrinsic('align', v) === 'center' ? 'center' : 'start';"," }"," ```",""," Writes go through `setComponentAlias(component, spec.variable(v), { kind: 'literal', value })` so the choice cascades to `:root` like any token.","","4. **Put `intrinsics` on the registry entry** so the contract test can see it. That is the same entry the recipe passes to `bootLiveTokens`, with one more field:",""," ```ts"," bootLiveTokens(App, '#app', {"," components: [{"," id: 'mywidget',"," // ...label, icon, sourceFile, editorComponent, schema..."," intrinsics: myWidgetIntrinsics,"," }],"," });"," ```","","Use `normalize` only when two raw values render identically and the dropdown lists one (SectionDivider folds `above-description` into `below-label`). Properties that resemble intrinsics but are not: preview-only props with no persistence (a size selector that only changes the demo), `setComponentConfig` editor metadata (Dialog's button variants), and token-valued selects (a control choosing between two tokens). None carry a duplicated runtime default, so none need an `IntrinsicSpec`."],
|
|
15
15
|
"references/linked-siblings.md": ["# Extension: linked siblings","","Read this when the component has more than one variant and those variants share base properties (surface, radius, padding) that should move together. Toggle and SectionDivider have no linked tokens and skip all of it.","","Toggle's tokens are flat per state. Most multi-variant components (Badge, Card, SegmentedControl) share base properties across variants and surface that equality via a *linked block*: one edit propagates to every variant, while per-variant properties stay independent. Five additions to the Toggle pattern; see `BadgeEditor.svelte` in `node_modules` for the full file.","","1. **Mark linkable tokens** with `canBeLinked: true` + a `groupKey`. Peers sharing a `groupKey` form a link set across variants.",""," ```ts"," function variantBaseTokens(v: Variant): Token[] {"," return ["," { label: 'padding', canBeLinked: true, groupKey: 'padding', variable: `--badge-${v}-padding` },"," { label: 'corner radius', canBeLinked: true, groupKey: 'radius', variable: `--badge-${v}-radius` },"," ];"," }"," // Colors omit canBeLinked. Per-variant by design."," function variantColorTokens(v: Variant): Token[] {"," return ["," { label: 'surface color', groupKey: 'surface', variable: `--badge-${v}-surface` },"," { label: 'text color', groupKey: 'text', variable: `--badge-${v}-text` },"," ];"," }"," ```","","2. **Build a `linkableContexts: Map<variable, contextLabel>`** in `<script module>`. The label (e.g. `\"success base\"`) is how the LinkageChart row identifies this variable. Plain literal Map, no helper needed.",""," ```ts"," const linkableContexts = new Map<string, string>("," variants.flatMap((v) =>"," variantBaseTokens(v)"," .filter((t) => t.canBeLinked)"," .map((t) => [t.variable, `${v} base`] as [string, string]),"," ),"," );"," ```","","3. **Compute `linked` and mask currently-linked rows** out of per-state lists, so they render once inside the LinkedBlock instead of twice.",""," ```ts"," import { editorState } from '@motion-proto/live-tokens';"," import { computeLinkedBlock, withLinkedDisabled, buildSiblings }"," from '@motion-proto/live-tokens/component-editor';",""," let linked = $derived(computeLinkedBlock(component, linkableContexts, allTokens, $editorState));"," let visibleVariantStates = $derived((v: Variant) => Object.fromEntries("," Object.entries(variantStates(v)).map(([name, list]) => [name, withLinkedDisabled(list, linked.varSet)]),"," ));"," ```","","4. **Pass `{linked}` to `ComponentEditorBase`** so the LinkedBlock renders above the variant groups.","","5. **Multi-variant editors iterate VariantGroups** with `buildSiblings` so cross-variant link rows resolve to their peers.",""," ```svelte"," <ComponentEditorBase {component} title=\"Badge\" tokens={allTokens} {linked} variants={variantOptions}>"," {#each variants as v}"," <VariantGroup"," name={v}"," title={v}"," states={visibleVariantStates(v)}"," {component}"," siblings={buildSiblings(variants, v, variantStates)}"," >"," ...preview snippet"," </VariantGroup>"," {/each}"," </ComponentEditorBase>"," ```","","Single-variant components with multi-state linked tokens still set `canBeLinked` + `linkableContexts`, but skip `buildSiblings` and the `{#each}` loop. Components with no linked tokens (Toggle, SectionDivider) skip all five steps; `ComponentEditorBase` renders without a `{linked}` prop."],
|
|
16
|
-
"references/sketch-mode.md": ["# Joining the sketch layer","","Sketch mode blanks each part's real background and border and repaints them onto","`::before`/`::after` through a shared noise field. It draws a fixed set of","selectors: the shipped components, plus four classes reserved for everyone else.","A component is skipped until it opts in, so it stays crisp while the page","around it goes hand-drawn.","","The whole contract is CSS. There is nothing to import and no function to call:","the layer exports no runtime API, and a component joins it by carrying a class","and naming five custom properties.","","## What the layer takes over","","An opted-in element is no longer painting itself. On every drawn part the layer","forces:","","| It forces | So the component must |","|----------------------------------------------|----------------------------------------------------------|","| `background: transparent
|
|
16
|
+
"references/sketch-mode.md": ["# Joining the sketch layer","","Sketch mode blanks each part's real background and border and repaints them onto","`::before`/`::after` through a shared noise field. It draws a fixed set of","selectors: the shipped components, plus four classes reserved for everyone else.","A component is skipped until it opts in, so it stays crisp while the page","around it goes hand-drawn.","","The whole contract is CSS. There is nothing to import and no function to call:","the layer exports no runtime API, and a component joins it by carrying a class","and naming five custom properties.","","## What the layer takes over","","An opted-in element is no longer painting itself. On every drawn part the layer","forces:","","| It forces | So the component must |","|----------------------------------------------|----------------------------------------------------------|","| `background: transparent` | Name the fill again as `--sketch-fill` |","| `border-color: transparent` | Name the outline again as `--sketch-stroke` |","| `box-shadow: none` | Name the shadow again as `--sketch-shadow` |","| `overflow: visible` | Never put the class on a box whose clip carries meaning |","| `position: relative` | Never put the class on an absolutely-positioned root |","| `z-index: 0` | Expect a new stacking context on that element |","| `::before` (the fill), `::after` (the stroke) | Never own either pseudo-element on that element |","","`::after` survives on `sketch-rule` alone, which draws no outline. `::before` is","claimed on all four.","","The layer wins each of these at the weight of one ID selector. Select the","component's elements by class: a selector with an ID can outweigh the layer.","","## Opting in","","Put one class on the runtime component's root, chosen by **size, not by kind**.","A card and a modal are both containers; a badge and a pill are both chips.","","| Class | For |","|---------------------|-------------------------------------------------------------|","| `sketch-surface` | A box. The default treatment. |","| `sketch-container` | A large box. Tilts less, so the type inside stays readable. |","| `sketch-chip` | A small box. Finer fill mask, more rotation, less travel. |","| `sketch-rule` | A line rather than a box. No rotation, no rounded ends. |","","The class opts the component in and nothing more. It names no colour, so the layer emits","no rule for it and whatever the component declares survives.","","```svelte","<div class=\"mywidget sketch-container {variant}\">…</div>","","<style>"," .mywidget {"," background: var(--mywidget-surface);"," border: var(--mywidget-border-width) solid var(--mywidget-border);"," border-radius: var(--mywidget-radius);"," box-shadow: var(--mywidget-shadow);",""," /* The layer hides all four above. These are what it draws instead. */"," --sketch-fill: var(--mywidget-surface);"," --sketch-stroke: var(--mywidget-border);"," --sketch-hatch-color: var(--mywidget-border);"," --sketch-radius: var(--mywidget-radius);"," --sketch-shadow: var(--mywidget-shadow);"," }","</style>","```","","State all five. `--sketch-radius` is registered as an inheriting `<length>`, and","the other four inherit as ordinary custom properties, so a part that states","nothing is drawn with its **ancestor's** value: a badge inside a hatched card","stripes itself in the card's ink, and a square header inside a rounded card","picks up the card's corners.","","## Variants, states and inner parts","","Nothing competes for these values, so every case is one more","declaration at the specificity already in use.","","```css",".mywidget.danger { --sketch-fill: var(--mywidget-danger-surface); }",".mywidget:hover,",".mywidget.force-hover { --sketch-stroke: var(--mywidget-hover-border); }","```","","Pair every `:hover` with `.force-hover`, as elsewhere: that is the editor's","preview of the hover state, and a hover the sketch layer cannot paint reads as","no hover at all once the real background is transparent.","","An inner part that carries its own surface (a header strip, a footer) takes its","own class and its own five values. The class is easy to forget, because the part","already has its own values and looks finished without it. A part carrying only","the values is left crisp, and reads as a hard-edged rectangle dropped inside a","drawn box. No checker sees it. Where such a part draws no outline, bind the","hatch ink to the ink its **parent** is outlined in, so the component reads as one","drawing rather than a shaded panel dropped into a box:","","```svelte","<span class=\"mywidget-header sketch-chip\">{label}</span>","```","","```css",".mywidget-header {"," --sketch-fill: var(--mywidget-header-surface);"," --sketch-stroke: transparent;"," --sketch-hatch-color: var(--mywidget-border);"," --sketch-radius: 0px;","}","```","","A part with a visible stroke needs no `--sketch-hatch-color`; it falls back to","the stroke and follows it into hover. A part with no fill wants none, because","there is no surface there to shade.","","A gradient is a valid fill. The `background` shorthand's last layer takes a","colour or an image, so `--sketch-fill` accepts either.","","## Where the class does not go","","- **A positioned root.** The layer forces `position: relative` on parts that sit"," in flow, which drops an absolutely or fixed-positioned element back to its"," flow position. Put the class on an inner box instead.","- **A box that clips something real.** `overflow` is forced visible so the ink"," can travel past the border box, and there is no consumer opt-out. A scroller,"," a fill bar held to its track, or a picture held to its frame keeps its clip by"," keeping the class off that element and carrying it on a wrapper.","- **An element that owns `::before` or `::after`.** The layer claims both. A"," shimmer, a caret or a decorative arrow on the opted-in element is gone.","- **A shipped part's selector** (`.card`, `.panel`). Borrowing one to get drawn"," works, but it hands the component that part's colours and its damping, and it"," is package-internal. The reserved classes are the contract.","","## Rules, which are not boxes","","A `border` cannot be displaced: the effect moves boxes, and a border is not one.","Make the rule an element, give it `sketch-rule`, and name its ink as the fill.","","```svelte","<span class=\"mywidget-rule sketch-rule\" aria-hidden=\"true\"></span>","```","```css",".mywidget-rule {"," height: var(--border-width-2);"," background: var(--mywidget-hairline-color);"," --sketch-fill: var(--mywidget-hairline-color);","}","```","","## Media inside the component","","A drawn part's `overflow` is visible so the fill and outline can travel past the","box. A background that bleeds is the effect working. An image that bleeds is","not, since it keeps square corners while the part around it turns. Media running","to the component's edge has to carry the corners itself:","","```css",".mywidget-cover {"," overflow: hidden;"," border-top-left-radius: var(--sketch-radius, var(--mywidget-radius));"," border-top-right-radius: var(--sketch-radius, var(--mywidget-radius));","}","```","","`--sketch-radius` is the radius the layer drew and it inherits, so the fallback","covers the effect being off. Corner spread is per-corner and per-instance, so at","high spread the crop is the mean rather than an exact trace of the drawn edge.","","## Icons and SVG","","Icons and inline SVG take the wobble directly, since a glyph has no box to","redraw. Body type is left alone deliberately: an icon is a shape and survives a","wobble, a paragraph is not. Nothing opts into this; it applies to every","`[class*=\"fa-\"]` and every `svg` under the scope.","","`--sketch-icon-off` names what a subtree's glyphs are drawn with instead. It","inherits, so one declaration covers everything under it:","","```css","/* Crisp. Chrome, a logo, anything that has to stay exact. */",".mywidget-toolbar { --sketch-icon-off: none; }","","/* Drawn back rather than off, at a third of the travel. Small artwork, and"," type set as an SVG, which the layer reads as one large glyph. */",".mywidget-mark { --sketch-icon-off: var(--sketch-icon-soft); }","```","","Travel is stated in px against a glyph whose size the layer cannot know, so the","dial that suits a card's worth of artwork tears a 16px icon apart. Reach for the","soft bank before reaching for `none`.","","## First-party components","","A component authored inside the package does not use the reserved classes. Add a","`PartSpec` row to `PART_SPECS` in `src/editor/core/sketch/sketchLayer.ts`","instead, which is keyed to the component's own token stem and gets the shipped","damping. `sketchPartTokens.test.ts` then holds the component to it: every colour the layer","paints must be one the component itself assigns to that same element, checked","against the compiled `<style>` block.","","## Verify","","Switch Sketch mode on from the editor's **Sketchstyle** view, then check the","component in place:","","- [ ] Drawn, not crisp, in every variant.","- [ ] Wearing its own colours, not its parent's, including inner parts.","- [ ] Hover repaints. The wobble holds still while it does.","- [ ] Hatched fill uses ink that belongs to the component.","- [ ] Media at the component's edge turns with the drawn corners.","- [ ] Nothing that has to stay exact is torn: icons, clipped content, overlays.","- [ ] Switch it off. Every trace is gone and the component is unchanged."],
|
|
17
17
|
"references/token-naming.md": ["# Suffix vocabulary","","The editor picker is chosen by suffix. There is no per-token override; if a","token renders with the wrong picker, rename it to one of these.","","`KIND_RULES` in `src/editor/core/components/aliasKinds.ts` is authoritative, and","`check-component` fails on a suffix outside it. `check:skills` holds this file","to that list, so the two cannot drift apart.","","## Color and surface","","| Suffix | Meaning |","|-------------|---------------------------------------------------------------|","| `-surface` | Fill / background color |","| `-border` | Border color |","| `-text` | Text color |","| `-icon` | Icon color |","| `-label` | Label text color |","| `-fill` | Inner fill (distinct from outer surface) |","| `-color` | A hairline rule's colour, or a color no role word above names |","| `-shadow` | Box-shadow |","| `-blur` | Backdrop or filter blur radius |","| `-tint` | A wash over the surface, aliasing a `--tint-*` stop |","| `-indicator` | The colour of the bar or stripe that marks an item |","| `-thumb` | A scrollbar or slider thumb's colour |","| `-title` | Title text colour |","| `-body` | Body text colour |","| `-eyebrow` | Eyebrow text colour |","| `-description` | Description text colour |","| `-hint` | Hint text colour |","| `-error` | Error text colour |","| `-placeholder` | Placeholder text colour |","| `-value` | A displayed value's colour |","","## Geometry","","| Suffix | Meaning |","|-----------------|---------------------------------------------------------------|","| `-radius` | Corner radius |","| `-border-width` | Stroke thickness (used even when CSS uses `outline:`) |","| `-indicator-width` | An indicator's thickness |","| `-hairline-width` | A hairline rule's thickness |","| `-dot-size` | A dot indicator's diameter |","| `-hairline-inset` | Inset trimmed from a stretched hairline |","| `-track-height` | A track's height (progress bar, slider) |","| `-icon-size` | An icon's rendered size |","| `-thumb-size` | A thumb's rendered size |","| `-height` | A measured height (a track, a panel) |","| `-margin` | Outer spacing, moved on the same scale as `-padding` |","| `-duration` | Motion duration |","| `-easing` | Motion easing curve |","| `-scale` | A transform scale factor |","| `-width` | Width dimension |","| `-size` | Square / uniform dimension |","| `-padding` | Internal spacing |","| `-gap` | Spacing between sibling elements |","","`-width`, `-height` and `-size` are the fall-through: any dimension with no","more specific name behind it. They read the `--space-*` scale through the same","picker `-gap` uses, and they match last, so `-border-width`, `-hairline-width`,","`-icon-size` and the rest claim their token first. Reach for the specific name","when one fits; a stroke is `-border-width` even where the CSS says `outline:`.","","## Typography","","| Suffix | Meaning |","|--------------------|--------------------------|","| `-font-family` | Font family reference |","| `-font-weight` | Font weight reference |","| `-font-size` | Font size reference |","| `-line-height` | Line height |","| `-letter-spacing` | Letter spacing |","","A suffix that is not here is either a rename away from one that is, or","an issue against `@motion-proto/live-tokens`. Inventing one costs the token its","picker: the editor falls back to a plain text input."],
|
|
18
18
|
},
|
|
19
19
|
"create-page": {
|
|
20
|
-
"SKILL.md": ["---","name: live-tokens-create-page","description: Create a page in a @motion-proto/live-tokens project from the shipped components at their defaults and the theme's text styles. Use when the user asks for a page or a route. Use when the user asks to change the layout of a page. Edits page files and the route table. For a choice between two components, read live-tokens-pick-component. For a component the catalogue lacks, read live-tokens-create-component. For a theme change, read live-tokens-create-theme.","---","","# Creating a page in a live-tokens project","","Assemble the page from the shipped components at their defaults and the theme's text styles. A change to a component is made in the components editor at `/live-tokens/components` and reaches every page. A change to the theme is made with **live-tokens-create-theme**.","","## Workflow","","1. Read the project first: the existing pages and where they live, how `App.svelte` wires routes, `--columns-count` in `tokens.css`, and the catalogue from `npx live-tokens components`.","2. Read the page top to bottom and name each section by its purpose. Take each section's column spans from the Page layouts table.","3. Build the page grid and place each section on it. Separate the sections with the smallest difference that separates them.","4. Give each section its container. `npx live-tokens components <id>` prints a component's constraints.","5. Match a shipped component to each need. When two could fit, read **live-tokens-pick-component**. When nothing in the catalogue fits, read **live-tokens-create-component**.","6. Write the page CSS in design tokens.","7. Set the hierarchy: one text style per element, the shipped size on every control, one primary action, and one space step per position.","8. Add the route, with a lazy import and the source path.","9. Run `npx live-tokens check-page <file> --tests --strict --json` until it exits 0, then check the rendered page.","10. Reply with the sections and the layout each took, the components placed, the route, and the check-page result.","","## Layout","","### Page layouts","","Decide the sections before the columns. Read the page top to bottom and name each section by its purpose: what the user reads, types into, or presses. Each section is a row of the page grid. Take the column spans from the layout that matches the reader's task.","","| Layout | Use when | Column spans |","|---|---|---|","| Stacked sections | The reader moves top to bottom: an opening, one section per topic, a close; or a stage, its inputs, and a toolbar. | Each section spans all columns. Copy spans half (6 of 12). |","| Main with a supporting pane | One region is the work and the other adjusts or describes it. | Main two thirds (8), pane one third (4). |","| List with detail | The reader picks an item from a list and inspects it. | List one third (4), detail two thirds (8). |","| Grid of equals | The reader compares or scans items of one kind. | Equal spans. Up to seven per section. |","| Single column | The reader fills a form or reads at length. | Half the columns (6), centered. |","","The stage is the canvas, player, or strip the work is about. Stretch a section's containers to one height (`align-items: stretch`) so their bottom edges align.","","### Grid","","The page is the column grid: `display: grid`, `grid-template-columns: repeat(var(--columns-count), 1fr)`, `column-gap: var(--columns-gutter)`, `max-width: var(--columns-max-width)`, `margin: 0 auto`. Each section spans it with `grid-column: 1 / -1`.","","To place a section's children at page-column positions:","","1. Read `--columns-count` in the project's `tokens.css`.","2. Span the parent grid with `grid-column: 1 / -1`.","3. Redeclare `repeat(var(--columns-count), 1fr)` with `column-gap: var(--columns-gutter)`.","4. Place each child by page-column numbers.","","A grid that follows the page columns takes `var(--columns-count)` or a `calc()` of it as its count, so it stays in step with `ColumnsOverlay`. A local grid of two or three equal columns writes its own count. A column number in `grid-column` is fixed to the count read in step 1.","","No scaffold collapses the page grid on a phone: the theme's own column gutter alone exceeds a phone's width at the full column count, so every page writes its own `@media (max-width: 767px)` rule setting `grid-template-columns: 1fr` and `column-gap: 0`, with each section's children spanning `grid-column: 1 / -1`. A section's columns then stack in reading order.","","### Separation","","The page shows one thing, and every other element stays out of its way. Each element that is not content costs attention: a hairline, a border, a header bar, a shadow. Keep an element only when it serves a purpose no other element serves.","","Separate elements with the smallest difference that separates them: space first, then a hairline, then a second surface. Use one separator at a time. Two heavy borders side by side make a third shape between them, and a section of containers with borders and header bars reads as a set of posters.","","Color each element by its layer.","","| Layer | Color |","|---|---|","| Content | `--text-primary`, or the color `site.css` gives the element |","| Label | `--text-secondary` |","| Chrome | `--border-neutral` |","| Overlay on content, such as a grid or a selection | `--border-brand`, which stays visible on any pixel |","","Show related items side by side when the width permits. A line of copy runs 45 to 90 characters; the copy span in Page layouts holds that at body size. On a tool page the stage is the content and each control is chrome, so the stage takes the space.","","`references/layout-sources.md` names the sources for these laws.","","## Components","","- Use a shipped component when one fits. Import it from `@motion-proto/live-tokens/components/<Name>.svelte`.","- `npx live-tokens components <id>` prints a component's declared props, the values each union accepts, and its catalogue entry, including its constraints and when not to use it. `--json` prints the same as data. The list includes the project's own components.","- Pass only the props a component declares.","- A shipped component fills its parent. To size one, size the element the page wraps it in.","- A native element with no chrome of its own needs no component: an `<input type=\"file\">` behind a Button, a `<canvas>`, an `<img>` inside a stage.","- Text inside a `Card` or a `CollapsibleSection` takes the container's type on nested `p`, `ul`, `ol`, and `li`. When the page owns that type, as full-bleed media does, pass `prose={false}`.","- An empty stage shows a heading that names the condition and one `secondary` Button that fills it. An error goes in a `Callout variant=\"danger\"`.","- A container in a tool UI labels itself: `Card variant=\"bare\"` with the label in the body as `--body-sm-*` in `--text-secondary`.","- A row of fields is a flex row with `gap: var(--space-20)`; each field's wrapper takes `flex: 1`.","- A toolbar is a flex row of Buttons on the section's bottom edge, with no container around it. Group the Buttons left and right with `justify-content: space-between`. A `danger` Button sits apart from the group it could be mistaken for. A vertical stack of Buttons sets `fullWidth` on each Button; a row omits it.","- For a `MenuSelect` picker, toggle it from a Button with a trailing chevron (`icon=\"fa-solid fa-chevron-down\" iconPosition=\"right\"`) and position the list under the Button at `top: 100%` with a `--space-*` margin.","","## Rules the checker enforces","","`check-page` fixes what it can and reports the rest by rule id.","","- `multiple-primary`","- `danger-without-dialog`","- `control-size`","- `native-control`","","## Tokens","","- When a design token exists for a value, page CSS takes the token as `var(--token)`. That holds in the `<style>` block, an inline `style=` attribute, and a `style:` directive.","- A width is a span of page columns. The Layout section gives the grid.","- A height follows the content. A stage's `minHeight` is the one fixed height, set from what the stage must show.","- A value that comes from data, such as a sheet's padding in pixels or a chart's scale, is set through a `{}` expression.","","## Hierarchy","","### Type","","One text style per element. A text style has five axes: `-font-family`, `-font-size`, `-font-weight`, `-line-height`, and `-letter-spacing`. Set all five from the one style.","","| Element | Style |","|---|---|","| Page title | `h1` in `--heading-xl-*` |","| Section title | `h2` in `--heading-lg-*`, or `SectionDivider variant=\"sm\"` |","| Card title | the Card `title` prop |","| Label above a group | `--body-sm-*` in `--text-secondary` |","| Body | `p` in `--body-md-*` |","| Secondary line | `--body-sm-*` in `--text-secondary` |","| Count, status, read-out | `--body-sm-*` in `--text-primary` |","| Command or value | `code` in `--code-*` |","","Use the semantic element for each place: one `h1`, an `h2` for each section, `h3` inside a section, `p` for copy. Heading levels run in order with no skipped level. `site.css` types bare `h1` to `h4`, `p`, `code`, `pre`, and list items from these styles, so the tag carries the style. Type an element only when the table gives its tag a different style. A weight alone, on `strong` or a list marker, is the one axis a page sets by itself. A page shows at most two weights.","","### Size","","Omit `size` on every control and container. `check-page` reports a `size` attribute on a shipped component as `control-size`.","","### Emphasis","","`npx live-tokens components button` names each `variant` and the role it carries.","","In a row of actions the primary sits last, on the right. Up to four actions are individual Buttons. Five or more collapse into a `MenuSelect` behind one Button.","","### Spacing","","Each position takes one step of the `--space-*` scale. Space inside a group is smaller than space between groups. A shipped component carries its own inner spacing; the table names the space the page draws.","","| Position | Step |","|---|---|","| Between controls in a row | `--space-8` |","| Inside a wrapper the page draws | `--space-16` |","| Between fields in a form | `--space-20` |","| Between containers in a section | `--columns-gutter` across, `--space-24` down |","| Between sections | `--space-16` above a hairline |","| Page title to first section | `--space-24`, no hairline |","| Page margin | `--space-32` |","","Every section after the first opens with a hairline: `padding-top: var(--space-16)` and `border-top: var(--border-width-1) solid var(--border-neutral)`. The hairline separates, so the gap between sections is smaller than the gap between the containers inside them. A section's edge is the hairline alone.","","## Routing","","Add the route the way `App.svelte` already wires routes.","","- `<LiveTokensRouter pages={...}>`: add a `pages` entry with `lazy: () => import('./YourPage.svelte')` and `source: 'src/...'`. Add `label` and `icon` to show the page in the nav rail; omit `label` to keep the route reachable by URL alone. For a `/:id`, a path prefix, or a gated page, add `resolve(path) => RouteEntry | null` beside `pages`. The entry fields are the same.","- Manual `<LiveEditorOverlay>`: dispatch with `$derived.by(() => import(...))` and register the route's source in `pageSources={...}`.","","Import the page with `lazy`, so page CSS stays off the editor routes. Import `site.css` from each page's `<script>` block for the same reason. `source` is what makes Page Source work. A page route sits outside `/live-tokens/*`, the namespace of the package's own routes, where `Editor` and `ComponentEditorPage` mount.","","```svelte","const pages = {"," '/pricing': {"," lazy: () => import('./pages/Pricing.svelte'),"," source: 'src/pages/Pricing.svelte',"," label: 'Pricing',"," icon: 'fa-tag',"," },","};","```","","## Verify","","Run `npx live-tokens check-page <file> --tests --strict --json`. It applies every `auto` repair, runs the Playwright suite against the page's own route, and returns the fixes it applied and the findings that remain. The suite proves what only a rendered page can: the cascade leaves every component painting from its semantic properties, every run of text sits in one shipped text style, every text and surface pair meets AA, sections sit on the page grid, and nothing overflows. `--off=<rule>` silences a rule for one run, and `--no-fix` reports without editing.","","Each remaining finding carries a rule id, a line, and its `guidance`. Make each remaining repair from its guidance, and run the command again until it exits 0.","","The checkers cannot see a layout. Open the page at the width it is built for and check each line below.","","- The first section holds what the user came for.","- One `h1`. Heading levels run in order with no skipped level.","- No label is larger than the page's body copy.","- A line of copy runs 45 to 90 characters.","- The containers in a section align at the bottom.","- The actions sit where the eye goes last, with the one primary at the end.","- Every row of actions holds an action that leaves without committing.","- Every action that destroys saved work meets the Button constraint `danger-without-dialog`.","- An action that runs longer than a moment shows progress in a `ProgressBar` or a `Notification`.","- Every field has a default, and Reset restores it.","- Secondary settings sit in a `CollapsibleSection`. Every control is in view.","- Labels use the user's words, such as \"Export slices\".","- Every `img` has `alt` text. Focus order follows the reading order.","","`references/interaction-sources.md` names the sources for these checks.","","Then read the page from a distance: the sections and their edges are the only shapes that show. Then read it closely. For each border, header bar, and container, ask whether the page loses information when the element is removed. When the answer is no, remove the element. Find the element a reader sees first, second, and third, and confirm that is the reading order the page needs."],
|
|
20
|
+
"SKILL.md": ["---","name: live-tokens-create-page","description: Create a page in a @motion-proto/live-tokens project from the shipped components at their defaults and the theme's text styles. Use when the user asks for a page or a route. Use when the user asks to change the layout of a page. Edits page files and the route table. For a choice between two components, read live-tokens-pick-component. For a component the catalogue lacks, read live-tokens-create-component. For a theme change, read live-tokens-create-theme.","---","","# Creating a page in a live-tokens project","","Assemble the page from the shipped components at their defaults and the theme's text styles. A change to a component is made in the components editor at `/live-tokens/components` and reaches every page. A change to the theme is made with **live-tokens-create-theme**.","","## Workflow","","1. Read the project first: the existing pages and where they live, how `App.svelte` wires routes, `--columns-count` in `tokens.css`, and the catalogue from `npx live-tokens components`.","2. Read the page top to bottom and name each section by its purpose. Take each section's column spans from the Page layouts table.","3. Build the page grid and place each section on it. Separate the sections with the smallest difference that separates them.","4. Give each section its container. `npx live-tokens components <id>` prints a component's constraints.","5. Match a shipped component to each need. When two could fit, read **live-tokens-pick-component**. When nothing in the catalogue fits, read **live-tokens-create-component**.","6. Write the page CSS in design tokens.","7. Set the hierarchy: one text style per element, the shipped size on every control, one primary action, and one space step per position.","8. Add the route, with a lazy import and the source path.","9. Run `npx live-tokens check-page <file> --tests --strict --json` until it exits 0, then check the rendered page.","10. Reply with the sections and the layout each took, the components placed, the route, and the check-page result.","","## Layout","","### Page layouts","","Decide the sections before the columns. Read the page top to bottom and name each section by its purpose: what the user reads, types into, or presses. Each section is a row of the page grid. Take the column spans from the layout that matches the reader's task.","","| Layout | Use when | Column spans |","|---|---|---|","| Stacked sections | The reader moves top to bottom: an opening, one section per topic, a close; or a stage, its inputs, and a toolbar. | Each section spans all columns. Copy spans half (6 of 12). |","| Main with a supporting pane | One region is the work and the other adjusts or describes it. | Main two thirds (8), pane one third (4). |","| List with detail | The reader picks an item from a list and inspects it. | List one third (4), detail two thirds (8). |","| Grid of equals | The reader compares or scans items of one kind. | Equal spans. Up to seven per section. |","| Single column | The reader fills a form or reads at length. | Half the columns (6), centered. |","","The stage is the canvas, player, or strip the work is about. Stretch a section's containers to one height (`align-items: stretch`) so their bottom edges align.","","### Grid","","The page is the column grid: `display: grid`, `grid-template-columns: repeat(var(--columns-count), 1fr)`, `column-gap: var(--columns-gutter)`, `max-width: var(--columns-max-width)`, `margin: 0 auto`. Each section spans it with `grid-column: 1 / -1`.","","To place a section's children at page-column positions:","","1. Read `--columns-count` in the project's `tokens.css`.","2. Span the parent grid with `grid-column: 1 / -1`.","3. Redeclare `repeat(var(--columns-count), 1fr)` with `column-gap: var(--columns-gutter)`.","4. Place each child by page-column numbers.","","A grid that follows the page columns takes `var(--columns-count)` or a `calc()` of it as its count, so it stays in step with `ColumnsOverlay`. A local grid of two or three equal columns writes its own count. A column number in `grid-column` is fixed to the count read in step 1.","","No scaffold collapses the page grid on a phone: the theme's own column gutter alone exceeds a phone's width at the full column count, so every page writes its own `@media (max-width: 767px)` rule setting `grid-template-columns: 1fr` and `column-gap: 0`, with each section's children spanning `grid-column: 1 / -1`. A section's columns then stack in reading order.","","### Separation","","The page shows one thing, and every other element stays out of its way. Each element that is not content costs attention: a hairline, a border, a header bar, a shadow. Keep an element only when it serves a purpose no other element serves.","","Separate elements with the smallest difference that separates them: space first, then a hairline, then a second surface. Use one separator at a time. Two heavy borders side by side make a third shape between them, and a section of containers with borders and header bars reads as a set of posters.","","Color each element by its layer.","","| Layer | Color |","|---|---|","| Content | `--text-primary`, or the color `site.css` gives the element |","| Label | `--text-secondary` |","| Chrome | `--border-neutral` |","| Overlay on content, such as a grid or a selection | `--border-brand`, which stays visible on any pixel |","","Show related items side by side when the width permits. A line of copy runs 45 to 90 characters; the copy span in Page layouts holds that at body size. On a tool page the stage is the content and each control is chrome, so the stage takes the space.","","`references/layout-sources.md` names the sources for these laws.","","## Components","","- Use a shipped component when one fits. Import it from `@motion-proto/live-tokens/components/<Name>.svelte`.","- `npx live-tokens components <id>` prints a component's declared props, the values each union accepts, and its catalogue entry, including its constraints and when not to use it. `--json` prints the same as data. The list includes the project's own components.","- Pass only the props a component declares.","- A shipped component fills its parent. To size one, size the element the page wraps it in.","- A native element with no chrome of its own needs no component: an `<input type=\"file\">` behind a Button, a `<canvas>`, an `<img>` inside a stage.","- Text inside a `Card` or a `CollapsibleSection` takes the container's type on nested `p`, `ul`, `ol`, and `li`. When the page owns that type, as full-bleed media does, pass `prose={false}`.","- An empty stage shows a heading that names the condition and one `secondary` Button that fills it. An error goes in a `Callout variant=\"danger\"`.","- A container in a tool UI labels itself: `Card variant=\"bare\"` with the label in the body as `.body-sm` in `--text-secondary`.","- A row of fields is a flex row with `gap: var(--space-20)`; each field's wrapper takes `flex: 1`.","- A toolbar is a flex row of Buttons on the section's bottom edge, with no container around it. Group the Buttons left and right with `justify-content: space-between`. A `danger` Button sits apart from the group it could be mistaken for. A vertical stack of Buttons sets `fullWidth` on each Button; a row omits it.","- For a `MenuSelect` picker, toggle it from a Button with a trailing chevron (`icon=\"fa-solid fa-chevron-down\" iconPosition=\"right\"`) and position the list under the Button at `top: 100%` with a `--space-*` margin.","","## Rules the checker enforces","","`check-page` fixes what it can and reports the rest by rule id.","","- `multiple-primary`","- `danger-without-dialog`","- `control-size`","- `native-control`","","## Tokens","","- When a design token exists for a value, page CSS takes the token as `var(--token)`. That holds in the `<style>` block, an inline `style=` attribute, and a `style:` directive.","- A width is a span of page columns. The Layout section gives the grid.","- A height follows the content. A stage's `minHeight` is the one fixed height, set from what the stage must show.","- A value that comes from data, such as a sheet's padding in pixels or a chart's scale, is set through a `{}` expression.","","## Hierarchy","","### Type","","One text style per element. A text style has five axes: `-font-family`, `-font-size`, `-font-weight`, `-line-height`, and `-letter-spacing`. Set all five from the one style.","","The text styles fall into five usages, each a scale of seven steps from `2xs` to `2xl`: display for type outside the page outline, heading for the outline, body for interface text and short copy, editorial for long reading, and code. Each step is a style named `--{usage}-{step}-*`, such as `--body-sm-*`. Headings use the whole set. The other usages mostly take `md` and `sm`.","","| Element | Style |","|---|---|","| Page title | `h1` in `--heading-xl-*` |","| Section title | `h2` in `--heading-lg-*`, or `SectionDivider variant=\"sm\"` |","| Title inside a section | `h3` to `h6` in `--heading-md-*` to `--heading-2xs-*` |","| Card title | the Card `title` prop |","| Hero title | `--display-xl-*` |","| Big number | `--display-md-*` |","| Label above a group | `--body-sm-*` in `--text-secondary` |","| Body | `p` in `--body-md-*` |","| Secondary line | `--body-sm-*` in `--text-secondary` |","| Count, status, read-out | `--body-sm-*` in `--text-primary` |","| Article copy | `p` in `--editorial-md-*` |","| Code block | `pre` in `--code-md-*` |","| Command or value in a line | `code`, at `0.875em` of the text around it |","","Use the semantic element for each place: one `h1`, an `h2` for each section, `h3` inside a section, `p` for copy. Heading levels run in order with no skipped level. `site.css` types bare `h1` to `h6`, `p`, `small`, `code`, `pre`, and list items from these styles, so the tag carries the style. It also gives every step a class of the same name, such as `.display-md` or `.body-sm`, that sets all five axes. Type an element only when the table gives its tag a different style, and type it with the step's class. A weight alone, on `strong` or a list marker, is the one axis a page sets by itself. A page shows at most two weights.","","### Size","","Omit `size` on every control and container. `check-page` reports a `size` attribute on a shipped component as `control-size`.","","### Emphasis","","`npx live-tokens components button` names each `variant` and the role it carries.","","In a row of actions the primary sits last, on the right. Up to four actions are individual Buttons. Five or more collapse into a `MenuSelect` behind one Button.","","### Spacing","","Each position takes one step of the `--space-*` scale. Space inside a group is smaller than space between groups. A shipped component carries its own inner spacing; the table names the space the page draws.","","| Position | Step |","|---|---|","| Between controls in a row | `--space-8` |","| Inside a wrapper the page draws | `--space-16` |","| Between fields in a form | `--space-20` |","| Between containers in a section | `--columns-gutter` across, `--space-24` down |","| Between sections | `--space-16` above a hairline |","| Page title to first section | `--space-24`, no hairline |","| Page margin | `--space-32` |","","Every section after the first opens with a hairline: `padding-top: var(--space-16)` and `border-top: var(--border-width-1) solid var(--border-neutral)`. The hairline separates, so the gap between sections is smaller than the gap between the containers inside them. A section's edge is the hairline alone.","","## Routing","","Add the route the way `App.svelte` already wires routes.","","- `<LiveTokensRouter pages={...}>`: add a `pages` entry with `lazy: () => import('./YourPage.svelte')` and `source: 'src/...'`. Add `label` and `icon` to show the page in the nav rail; omit `label` to keep the route reachable by URL alone. For a `/:id`, a path prefix, or a gated page, add `resolve(path) => RouteEntry | null` beside `pages`. The entry fields are the same.","- Manual `<LiveEditorOverlay>`: dispatch with `$derived.by(() => import(...))` and register the route's source in `pageSources={...}`.","","Import the page with `lazy`, so page CSS stays off the editor routes. Import `site.css` from each page's `<script>` block for the same reason. `source` is what makes Page Source work. A page route sits outside `/live-tokens/*`, the namespace of the package's own routes, where `Editor` and `ComponentEditorPage` mount.","","```svelte","const pages = {"," '/pricing': {"," lazy: () => import('./pages/Pricing.svelte'),"," source: 'src/pages/Pricing.svelte',"," label: 'Pricing',"," icon: 'fa-tag',"," },","};","```","","## Verify","","Run `npx live-tokens check-page <file> --tests --strict --json`. It applies every `auto` repair, runs the Playwright suite against the page's own route, and returns the fixes it applied and the findings that remain. The suite proves what only a rendered page can: the cascade leaves every component painting from its semantic properties, every run of text sits in one shipped text style, every text and surface pair meets AA, sections sit on the page grid, and nothing overflows. `--off=<rule>` silences a rule for one run, and `--no-fix` reports without editing.","","Each remaining finding carries a rule id, a line, and its `guidance`. Make each remaining repair from its guidance, and run the command again until it exits 0.","","The checkers cannot see a layout. Open the page at the width it is built for and check each line below.","","- The first section holds what the user came for.","- One `h1`. Heading levels run in order with no skipped level.","- No label is larger than the page's body copy.","- A line of copy runs 45 to 90 characters.","- The containers in a section align at the bottom.","- The actions sit where the eye goes last, with the one primary at the end.","- Every row of actions holds an action that leaves without committing.","- Every action that destroys saved work meets the Button constraint `danger-without-dialog`.","- An action that runs longer than a moment shows progress in a `ProgressBar` or a `Notification`.","- Every field has a default, and Reset restores it.","- Secondary settings sit in a `CollapsibleSection`. Every control is in view.","- Labels use the user's words, such as \"Export slices\".","- Every `img` has `alt` text. Focus order follows the reading order.","","`references/interaction-sources.md` names the sources for these checks.","","Then read the page from a distance: the sections and their edges are the only shapes that show. Then read it closely. For each border, header bar, and container, ask whether the page loses information when the element is removed. When the answer is no, remove the element. Find the element a reader sees first, second, and third, and confirm that is the reading order the page needs."],
|
|
21
21
|
"references/interaction-sources.md": ["# Interaction sources","","Read this when a Verify check or an emphasis rule in SKILL.md needs its reason.","Each principle below is held by one of three parties. A shipped component","holds it when the component's own design answers it, so the page's rule is","the component and pick-component names the test. The page holds it when only","the page can get it right, so SKILL.md states a read or the checker a rule.","The product holds it when no page decision touches it, and the row says so","to keep a later edit from reopening it.","","## Jakob Nielsen, ten usability heuristics","","| Heuristic | Holder | Rule in SKILL.md, or the component |","|---|---|---|","| Visibility of system status | Page | Verify: an action that runs longer than a moment shows progress in a `ProgressBar` or a `Notification`. |","| Match between system and the real world | Page | Verify: labels use the user's words. |","| User control and freedom | Page | Verify: every row of actions holds an action that leaves without committing. Emphasis: `multiple-primary`. |","| Consistency and standards | Page | One size, one primary action, one text style per element. The components carry the rest. |","| Error prevention | Page | Verify: `danger-without-dialog`. Verify: every field has a default and Reset restores it. |","| Recognition rather than recall | Component | `MenuSelect` lists the options; `Input` carries its label and hint; `Tooltip` defines in place. |","| Flexibility and efficiency of use | Product | Shortcuts and customisation are product decisions. |","| Aesthetic and minimalist design | Page | Layout: each element serves a purpose no other element serves. Verify: secondary settings sit in a `CollapsibleSection`. |","| Help users recognise, diagnose, and recover from errors | Component | `Input` carries the error line; `Callout variant=\"danger\"` carries a section's. |","| Help and documentation | Product | Contextual help is a product decision. |","","The complex-application version of the ten (Kaley, Nielsen Norman Group)","describes a tool with a stage and controls, which is the tool page SKILL.md","lays out. Its additions that the page holds: a wait past ten seconds shows","steps done and steps left, and the stage is the live preview of every","control. Undo, version history, and autosave are the product's.","","## Bruce Tognazzini, first principles of interaction design","","| Principle | Holder | Rule in SKILL.md, or the component |","|---|---|---|","| Anticipation | Page | Layout: show related items side by side. |","| Colour | Component | `Callout`, `Badge`, and `Notification` carry an icon or text beside the colour. |","| Consistency | Page | One size, one primary action. |","| Defaults | Page | Verify: every field has a default and Reset restores it. |","| Discoverability | Page | Verify: every control is in view. |","| Explorable interfaces | Page | Verify: every row of actions holds an action that leaves without committing. |","| Fitts's law | Component | The shipped default is the large target; SKILL.md's one-size rule keeps it. A toolbar sits on the section's bottom edge. |","| Protect users' work | Page | Verify: `danger-without-dialog`. |","| Readability | Component | live-tokens-set-colors gates every text pair at WCAG AA. |","| Simplicity | Page | Verify: secondary settings sit in a `CollapsibleSection`; no capability is removed for the sake of simplicity. |","| Visible navigation | Component | `SideNavigation` follows the current path. |","| Aesthetics, Autonomy, Efficiency of the user, Human-interface objects, Latency reduction, Learnability, Metaphors, State | Product | Measured, engineered, or researched outside a page. |","","Sources:","","- Nielsen, 10 Usability Heuristics for User Interface Design (1994, updated 2024). https://www.nngroup.com/articles/ten-usability-heuristics/","- Kaley, 10 Usability Heuristics Applied to Complex Applications. https://www.nngroup.com/articles/usability-heuristics-complex-applications/","- Tognazzini, First Principles of Interaction Design (revised 2014). https://asktog.com/atc/principles-of-interaction-design/","","## W3C Web Accessibility Initiative","","| Guidance | Holder | Rule in SKILL.md, or the component |","|---|---|---|","| Headings: one `h1`, levels in order, no skipped level | Page | Type: use the semantic element for each place. Verify: one `h1`, levels in order. |","| Images: text alternatives | Page | Verify: every `img` has `alt` text. |","| Focus order follows the reading order | Page | Verify: focus order follows the reading order. |","| Form labels and error messages | Component | `Input` carries its label, hint, and error line. |","","https://www.w3.org/WAI/tutorials/page-structure/headings/","https://www.w3.org/WAI/tutorials/images/","https://www.w3.org/WAI/WCAG22/Understanding/focus-order.html"],
|
|
22
22
|
"references/layout-sources.md": ["# Layout sources","","Read this when a layout decision in SKILL.md needs its reason. Each law in the","Layout section comes from one of these sources. The sources are for layout and","hierarchy only. Do not take color or type opinions from them: the theme owns","those.","","## Edward Tufte","","Tufte wrote about information graphics. His laws apply to a page because a page","is an information display with controls on it.","","| Law | Statement | Rule in SKILL.md |","|---|---|---|","| Smallest effective difference | Make all visual distinctions as subtle as possible, but still clear and effective. | Separate with space first, then a hairline rule, then a second surface. |","| 1+1=3 | Two heavy marks side by side make a third mark: the space between them. | A section of containers with borders and header bars reads as a set of posters. |","| Layering and separation | Put data on top, labels next, and scaffolding faintest. | Content, labels, and chrome each take their own token. |","| Administrative debris | The metaphor for the interface is the information. Remove the chrome that the tool adds for itself. | The stage takes the space. Controls take their shipped default. |","| Spatial over temporal | Show information adjacent in space. A control that hides it stacks it in time. | Show related items side by side. Frames in a strip are small multiples. |","| Erase non-data ink | Remove each mark that carries no information. | The Verify question: does the page lose information when this element is removed? |","| Micro and macro readings | A good display reads at a distance and up close. | Verify from a distance, then closely. |","","Sources:","","- Envisioning Information (1990): layering and separation, small multiples, micro and macro. https://www.edwardtufte.com/book/envisioning-information/","- The Visual Display of Quantitative Information (1983): data-ink, chartjunk.","- iPhone interface design, edwardtufte.com notebook: administrative debris, spatial over temporal. https://www.edwardtufte.com/notebook/iphone-interface-design/","","## Josef Müller-Brockmann","","Grid Systems in Graphic Design (1981) is the discipline behind the page column","grid. The grid does the separating, so an element needs no border to show","where it sits. His stated aim is compact planning, intelligibility, and","clarity. That is Tufte's aim in a typographer's words.","","## Refactoring UI","","Adam Wathan and Steve Schoger, Refactoring UI (2018), turns both into working","rules for product screens:","","- Put more space around a group than within it.","- Start with too much white space, then remove some.","- Use fewer borders. Separate with space, a shadow, or a second background.","- Emphasize by de-emphasizing the secondary content.","- Labels are a last resort.","- Keep a spacing scale where no two steps are closer than a quarter. The `--space-*` scale is that scale.","","https://www.refactoringui.com/","","## Material Design 3, canonical layouts","","The Page layouts table takes its rows from Material's canonical layouts.","","| Layout | Statement | Row in SKILL.md |","|---|---|---|","| Supporting pane | The primary area takes about two thirds of the window; the secondary pane takes the rest. At compact width the pane moves below the main content. | Main with a supporting pane, two thirds and one third; the stacking sentence. |","| List-detail | The list and the detail of the selected item sit side by side at expanded width. | List with detail. |","| Feed | Equivalent items in an adaptive grid. | Grid of equals. |","","https://m3.material.io/foundations/layout/canonical-examples/overview","https://developer.android.com/develop/adaptive-apps/guides/canonical-layouts","","## Cloudscape patterns","","| Pattern | Statement | Rule in SKILL.md |","|---|---|---|","| Dashboard | Three areas top to bottom: overview, data, support. \"Consider seven as the limit number for data representation.\" | Verify: the first section holds what the user came for. Grid of equals: up to seven per section. |","| Single-page create | One container; the essential fields first and few; secondary inputs in an expandable section; cancel then submit at the bottom. | Containers: a form. Single column. |","| Details page | The title with its actions, then a summary, then related blocks. | Stacked sections. |","| Empty state | A heading, an optional line, and one action. \"Always provide an action.\" Errors go elsewhere. | Containers: an empty stage. |","","https://cloudscape.design/patterns/general/service-dashboard/static-dashboard/","https://cloudscape.design/patterns/resource-management/create/single-page-create/","https://cloudscape.design/patterns/resource-management/details/details-page/","https://cloudscape.design/patterns/general/empty-states/","","## Matthew Butterick, line length","","\"45 to 90 characters per line, including spaces.\" The Separation paragraph and the Verify check carry the measure, and the half-width copy span holds it at body size.","","https://practicaltypography.com/line-length.html","","## Nielsen Norman Group, proximity","","\"Proximity is one of the most important grouping principles and can overpower competing visual cues such as similarity of color or shape.\" Space inside a group is smaller than space between groups. An unrelated action inside a group is camouflaged, so a `danger` Button sits apart from the toolbar group.","","https://www.nngroup.com/articles/gestalt-proximity/"],
|
|
23
23
|
},
|
|
@@ -37,7 +37,7 @@ export const skillDocs: Record<string, Record<string, string[]>> = {
|
|
|
37
37
|
"references/geometry-anchors.md": ["# Geometry anchors: feelings, idioms, and genres","","Read this when the geometry intent names one of these. An anchor overrides the","Idioms table in SKILL.md, because it is tuned to the same direction the color","came from, and a style's geometry is often targeted rather than global.","","Entries are written in the ops vocabulary. \"radius +2\" is a radius shift of 2.","\"borders +1\" is a border-width shift of 1. \"padding +1\" is a padding and gap","shift of 1. \"hairline borders\" is border-width `set: \"--border-width-1\"`, and","\"no borders\" is `set: \"--border-width-0\"`. \"hairline rules\" is hairline-width","`set: \"--border-width-1\"`. A named component means a targeted op. Controls squeeze before containers,","so a compaction of more than one step still spends its extra steps on","containers by name.","","An occasion (Christmas, Autumn, Ocean) fixes color only. Take its geometry from","the feeling it implies, or leave geometry alone.","","## Feelings","","| Anchor | Geometry |","|---|---|","| Joyful, exuberant, energetic | radius +2, padding +1, pill buttons |","| Playful, whimsical | pill buttons, rounder cards, wide gaps |","| Optimistic, hopeful | radius +1 |","| Confident, bold | radius -1, borders +1, tight gaps |","| Serene, tranquil | soft radius, padding +1, no borders |","| Tender, gentle, romantic | rounder, hairline borders |","| Cozy, comforting | radius +1, padding +1 |","| Wistful, nostalgic, vintage, faded | unchanged, hairline rules |","| Earthy, grounded, natural | radius +1, padding +1 |","| Clinical, sterile, precise | radius sm, tight gaps, hairline borders |","| Contemplative, focused | padding +1, minimal borders |","| Urgent, alarming | radius -2, borders +2, tight padding |","| Tense, anxious | tight gaps, radius sm |","| Defiant, rebellious, loud | radius none, borders +3 |","| Melancholy, moody, sad | padding +1, hairline borders |","| Somber, grave, mournful | sharp, tight gaps, hairline rules |","| Ominous, dramatic, haunted | sharp, borders +2 |","| Austere, severe, cold | radius none, tight padding, hairline borders |","","## Idioms, eras, and genres","","| Anchor | Geometry |","|---|---|","| Swiss, International | radius none, tight gaps, hairline borders |","| Bauhaus | radius none on containers, radius full on buttons alone, so the circle reads as a decision |","| Mid-century modern | radius +1 to +2, padding +1, no borders |","| Art deco, opulent, luxurious | sharp, padding +1, thin borders |","| Terminal, phosphor | radius none, tight padding, a 1px border on everything |","| Cyberpunk, neon noir, futuristic | sharp, tight gaps |","| Vaporwave | rounder, airier |","| Y2K, bubble | radius full on buttons with the padding a pill needs, generous spacing |","| Blueprint | radius none, 1px borders, tight grid |","| Scandinavian, hygge | soft radius, padding +1, hairline borders |","| Japandi, wabi-sabi | radius sm, padding +2, no borders |","| Cottagecore, botanical | rounder, airier |","| Editorial, magazine | sharp, padding +1, hairline rules |","| Newsprint, broadsheet | radius none, tight gaps, hairline rules |","| Risograph, zine | radius none, borders +2 |","| Corporate, professional, trustworthy | leave it alone |","| Brutalist | radius none, borders +2 to +3, tight padding |","| Memphis, postmodern | targeted rather than global: pill buttons against radius-none cards |","| Industrial, workshop, gritty | radius sm, thick borders, tight padding |"],
|
|
38
38
|
},
|
|
39
39
|
"set-type": {
|
|
40
|
-
"SKILL.md": ["---","name: live-tokens-set-type","description: Set a live-tokens theme's type: a Google Fonts pairing for the shipped --font-* stacks. The CLI verifies each family for the weights it ships. Called with an anchor and a type intent by live-tokens-create-theme, or with the user's request directly. Use when the user asks to pair fonts, pick a typeface, or set the fonts. Use when the user describes type by voice: editorial, friendlier, technical, elegant, less generic. Use when the user names a face for a role: a serif for headings, a display font. Changes type only. For a request that also names color or geometry, read live-tokens-create-theme.","---","","# Setting a theme's type","","Choose the families. The CLI verifies each against Google Fonts, builds the URL from the weights the family has, and writes the result to the buffer. Never hand-author font JSON or edit the data tree. Google Fonts is the pool because it is freely licensable and loads by URL. Other sources go in through the editor's Project fonts section.","","The result is on screen as soon as the run finishes. The three set skills write the same buffer, so color, type, and geometry compose in any order. When the user accepts the result, run `save-theme` to keep it as a theme. Loading a theme in the editor discards it.","","## Workflow","","1. Read the type intent and any anchor live-tokens-create-theme passed. When either names an anchor (a feeling, an idiom, or a genre), read its entry in `references/type-anchors.md`; it overrides the Voice table below.","2. Choose the pairing and write it to `scratch/font-pairing.json`.","3. Run `npx live-tokens set-type scratch/font-pairing.json`. It prints each stack that moved, each family's weights and URL, and, under Weight coverage, the weights the typography tokens ask for that the family lacks.","4. Read the report. Name a missing weight and offer an alternative only when it matters: a body face without 400,
|
|
40
|
+
"SKILL.md": ["---","name: live-tokens-set-type","description: Set a live-tokens theme's type: a Google Fonts pairing for the shipped --font-* stacks. The CLI verifies each family for the weights it ships. Called with an anchor and a type intent by live-tokens-create-theme, or with the user's request directly. Use when the user asks to pair fonts, pick a typeface, or set the fonts. Use when the user describes type by voice: editorial, friendlier, technical, elegant, less generic. Use when the user names a face for a role: a serif for headings, a display font. Changes type only. For a request that also names color or geometry, read live-tokens-create-theme.","---","","# Setting a theme's type","","Choose the families. The CLI verifies each against Google Fonts, builds the URL from the weights the family has, and writes the result to the buffer. Never hand-author font JSON or edit the data tree. Google Fonts is the pool because it is freely licensable and loads by URL. Other sources go in through the editor's Project fonts section.","","The result is on screen as soon as the run finishes. The three set skills write the same buffer, so color, type, and geometry compose in any order. When the user accepts the result, run `save-theme` to keep it as a theme. Loading a theme in the editor discards it.","","## Workflow","","1. Read the type intent and any anchor live-tokens-create-theme passed. When either names an anchor (a feeling, an idiom, or a genre), read its entry in `references/type-anchors.md`; it overrides the Voice table below.","2. Choose the pairing and write it to `scratch/font-pairing.json`.","3. Run `npx live-tokens set-type scratch/font-pairing.json`. It prints each stack that moved, each family's weights and URL, and, under Weight coverage, the weights the typography tokens ask for that the family lacks.","4. Read the report. Name a missing weight and offer an alternative only when it matters: a body face without 400, 600, or italic matters, and a display face without 300 does not. A family not on Google Fonts fails the run. Fix the spelling and re-run. A pairing the stacks already hold prints \"Nothing to change\" and writes nothing. A pairing equal to the open theme's discards the buffer, and the report says so.","5. Reply with the two families, the form model behind each, the matrix verdict, and any missing weight worth naming.","","Flags: `--dry-run` reports without writing. `--no-verify` skips the network and requires a URL per family; use it only offline.","","## The pairing file","","```json","{ \"display\": \"Fraunces\", \"body\": \"Nunito Sans\" }","```","","Every slot is optional; an omitted slot keeps its family. `display` is `--font-display`, the face of the display and heading usages (`--display-font-family`, `--heading-font-family`). `body` is `--font-sans`, the face of the body usage (`--body-font-family`) and of `h4` to `h6`. `mono` is `--font-mono`, the face of the code usage (`--code-font-family`). `serif` exists when a theme needs it. `editorial` is `--font-editorial`, the long-reading face behind the editorial usage (`--editorial-font-family`). An omitted `editorial` keeps its family, so set it only when essays and articles need a face of their own. Weight coverage is reported for `display`, `body`, `serif`, and `mono`. A family bound to `editorial` gets no coverage line. A slot may be `{ \"name\": \"...\", \"url\": \"...\" }` to pin a URL. A pinned URL is not probed, so the report shows no weights for it and coverage skips it. Spell families as Google does; the CLI reports the canonical spelling.","","## Choose the body face first","","The body face is the anchor. It carries most of the words, and text faces survive small sizes where display faces do not. Pick it against the type intent, then pick the display face against it. A body face has regular, bold, and italic; low to moderate stroke contrast; open apertures; and a large x-height. A face missing any of these is a display face, whatever its name says.","","The display and heading usages ask the display face for 600 through `--display-font-weight` and `--heading-font-weight`. The body usage asks the body face for 400 through `--body-font-weight`, and `h4` to `h6` ask it for 600. The code usage asks the mono face for 400 through `--code-font-weight`. `strong` asks for 600, and `em` adds italic. Screen candidates against those weights before running.","","## The font matrix","","Classify each candidate by form model and by stroke contrast and serifs.","","| Form model | Construction | Reads as |","|---|---|---|","| **Dynamic** | diagonal stress, open apertures, written origin | open, warm, humane, timeless |","| **Rational** | vertical stress, closed apertures, drawn not written | orderly, reserved, elegant, authoritative |","| **Geometric** | monolinear, circle-and-line | technical, modern, systematic, sober |","","- One form model with different stroke contrast or serifs pairs reliably. Helvetica and Bodoni are both rational, one a linear sans and one a high-contrast serif.","- Different form models with the same stroke contrast and serifs fail. The two look alike and fight underneath. Two arbitrary sans serifs clash for this reason.","- Different on both counts works. An unmistakable difference reads as a decision.","","Many faces sit between columns. When one straddles, say so and lean on the Voice table and the x-height check.","","## Voice","","| The intent says | Type voice |","|---|---|","| editorial, literary, considered | dynamic serif display over a humanist sans body |","| elegant, luxurious, formal | rational high-contrast serif display; keep the body quiet |","| friendly, warm, approachable | dynamic sans on both sides, or a soft serif display |","| technical, systematic, precise | geometric or neo-grotesque sans; a mono for code |","| playful, informal | an expressive display face over a plain workhorse body |","| serious, institutional, trustworthy | rational sans body, rational serif display |","| quiet, minimal, unbranded | one superfamily across both slots |","","The table covers an intent that names no anchor. An anchor's row in `references/type-anchors.md` wins.","","Match the type to the design direction the color came from. A warm autumn palette under a cold geometric sans reads as two projects.","","## Shortcuts","","Use these when the request is vague or the type should stay quiet.","","- **A superfamily.** A Google Fonts family with sans and serif siblings: Alegreya, Ancizar, IBM Plex, Inria, Merriweather, Noto, PT, Roboto, Source. The catalogue moves and this list does not; `set-type` fails on a family that is gone.","- **One family across weights.**","- **Same designer or foundry.**","- **Serif display over sans body** when nothing else decides it.","","## Watch for","","- **x-height parity.** Both faces share one size scale, so a small-x-height display face over a large-x-height body face gives a heading weaker than its own body text. Check it on the rendered page.","- **Print faces at small sizes.** Delicate serifs and high stroke contrast turn to mud below 16px.","- **Every family is a download.** Two is the target; three needs a reason.","- **Sets of themes.** No two share a display face or a body face.","","## Scope","","Type only. Color, component aliases, shape, and the type scale are untouched: `set-type` writes the font entries in the buffer and carries every other value forward. `save-theme` keeps the result; Adopt ships it and rewrites `fonts.css`, which is how a build without the editor loads the family.","","## Verify","","- The CLI exits 0 and names each stack that moved, before and after.","- Each URL matches the family's weights: a range for a variable family, an enumeration for a static one, a bare URL for a single-weight face. A pinned URL is written as given.","- The app shows the new type, and the editor's Fonts section lists both families with their fallbacks.","- To revert, run the previous pairing file, or load the open theme to discard the buffer."],
|
|
41
41
|
"references/type-anchors.md": ["# Type anchors: feelings, idioms, and genres","","Read this when the type intent names one of these. An anchor overrides the","Voice table in SKILL.md, because it is tuned to the same direction the color","came from.","","An entry names a form model and a role, never a family. Take the families from","the font matrix and the body-face rule in SKILL.md, and screen them against the","weights the shipped text styles ask for.","","An occasion (Christmas, Autumn, Ocean) fixes color only. Take its type from the","feeling it implies: Autumn reads cozy, Halloween reads ominous, Spring reads","tender.","","## Feelings","","| Anchor | Type voice |","|---|---|","| Joyful, exuberant, energetic | dynamic sans in both slots |","| Playful, whimsical | expressive display over a plain workhorse |","| Optimistic, hopeful | humanist sans, low contrast |","| Confident, bold | heavy rational display, plain body |","| Serene, tranquil | dynamic sans, one family |","| Tender, gentle, romantic | soft serif display over humanist sans, light weights |","| Cozy, comforting | dynamic serif over dynamic sans |","| Wistful, nostalgic, vintage, faded | rational serif display, quiet body |","| Earthy, grounded, natural | dynamic serif over humanist sans |","| Clinical, sterile, precise | one neo-grotesque, tightly set |","| Contemplative, focused | one quiet superfamily |","| Urgent, alarming | condensed grotesque, heavy |","| Tense, anxious | neo-grotesque, tightly set |","| Defiant, rebellious, loud | heavy display; a mono body works |","| Melancholy, moody, sad | rational serif display over quiet sans |","| Somber, grave, mournful | rational serif in both slots |","| Ominous, dramatic, haunted | heavy high-contrast display |","| Austere, severe, cold | one rational family |","","## Idioms, eras, and genres","","| Anchor | Type voice |","|---|---|","| Swiss, International | one rational neo-grotesque across both slots |","| Bauhaus | geometric sans, heavy display |","| Mid-century modern | dynamic sans, or a geometric display over a dynamic body |","| Art deco, opulent, luxurious | rational high-contrast serif display, quiet body |","| Terminal, phosphor | mono in both slots |","| Cyberpunk, neon noir, futuristic | wide geometric display, neutral body |","| Vaporwave | wide retro display, serif welcome |","| Y2K, bubble | geometric sans, heavy display |","| Blueprint | mono, or a technical grotesque; a mono body works here and almost nowhere else |","| Scandinavian, hygge | dynamic sans on both sides |","| Japandi, wabi-sabi | rational serif display over a quiet humanist body |","| Cottagecore, botanical | dynamic serif display over humanist sans |","| Editorial, magazine | high-contrast serif display over a text serif or humanist sans |","| Newsprint, broadsheet | rational serif in both slots |","| Risograph, zine | expressive display over a plain workhorse |","| Corporate, professional, trustworthy | rational sans body with a rational serif or same-family display |","| Brutalist | neo-grotesque or mono at heavy weight |","| Memphis, postmodern | geometric display, plain body |","| Industrial, workshop, gritty | condensed grotesque display over a plain body |"],
|
|
42
42
|
},
|
|
43
43
|
};
|
|
@@ -2,7 +2,7 @@ import type { SkillTree } from '../types';
|
|
|
2
2
|
|
|
3
3
|
export const createPage: SkillTree = {
|
|
4
4
|
"id": "live-tokens-create-page",
|
|
5
|
-
"digest": "sha256:
|
|
5
|
+
"digest": "sha256:dac3be326461ce44",
|
|
6
6
|
"title": "create-page",
|
|
7
7
|
"tagline": "Create a Page Using Live Tokens",
|
|
8
8
|
"nodes": [
|
|
@@ -164,19 +164,19 @@ export const createPage: SkillTree = {
|
|
|
164
164
|
},
|
|
165
165
|
{
|
|
166
166
|
"label": "Type",
|
|
167
|
-
"lines": [105,
|
|
167
|
+
"lines": [105, 127],
|
|
168
168
|
"anchor": "### Type",
|
|
169
169
|
"anchorEnd": "Use the semantic element for each place: one `h1`, an `h2` f"
|
|
170
170
|
},
|
|
171
171
|
{
|
|
172
172
|
"label": "Size and emphasis",
|
|
173
|
-
"lines": [
|
|
173
|
+
"lines": [129, 137],
|
|
174
174
|
"anchor": "### Size",
|
|
175
175
|
"anchorEnd": "In a row of actions the primary sits last, on the right. Up "
|
|
176
176
|
},
|
|
177
177
|
{
|
|
178
178
|
"label": "Spacing",
|
|
179
|
-
"lines": [
|
|
179
|
+
"lines": [139, 153],
|
|
180
180
|
"anchor": "### Spacing",
|
|
181
181
|
"anchorEnd": "Every section after the first opens with a hairline: `paddin"
|
|
182
182
|
}
|
|
@@ -193,12 +193,12 @@ export const createPage: SkillTree = {
|
|
|
193
193
|
"chips": [
|
|
194
194
|
{
|
|
195
195
|
"label": "LiveTokensRouter",
|
|
196
|
-
"lines": [
|
|
196
|
+
"lines": [159, 159],
|
|
197
197
|
"anchor": "`<LiveTokensRouter pages={...}>`: add a `pages` entry with `"
|
|
198
198
|
},
|
|
199
199
|
{
|
|
200
200
|
"label": "LiveEditorOverlay",
|
|
201
|
-
"lines": [
|
|
201
|
+
"lines": [160, 160],
|
|
202
202
|
"anchor": "Manual `<LiveEditorOverlay>`: dispatch with `$derived.by(() "
|
|
203
203
|
}
|
|
204
204
|
]
|
|
@@ -209,7 +209,7 @@ export const createPage: SkillTree = {
|
|
|
209
209
|
"kind": "cli",
|
|
210
210
|
"title": "Run check-page",
|
|
211
211
|
"desc": "check-page --tests --strict --json applies every auto repair, proves the rendered page at its own route, and returns the fixes beside the remaining findings.",
|
|
212
|
-
"lines": [
|
|
212
|
+
"lines": [177, 177],
|
|
213
213
|
"anchor": "Run `npx live-tokens check-page <file> --tests --strict --js"
|
|
214
214
|
},
|
|
215
215
|
{
|
|
@@ -218,7 +218,7 @@ export const createPage: SkillTree = {
|
|
|
218
218
|
"kind": "gate",
|
|
219
219
|
"title": "Repair from guidance",
|
|
220
220
|
"desc": "Each remaining finding carries its guidance. Make each repair, then run check-page again.",
|
|
221
|
-
"lines": [
|
|
221
|
+
"lines": [179, 179],
|
|
222
222
|
"anchor": "Each remaining finding carries a rule id, a line, and its `g"
|
|
223
223
|
},
|
|
224
224
|
{
|
|
@@ -227,31 +227,31 @@ export const createPage: SkillTree = {
|
|
|
227
227
|
"kind": "chipset",
|
|
228
228
|
"title": "Check the page in the browser",
|
|
229
229
|
"desc": "The checkers cannot see a layout. Open the page at its width and check each line.",
|
|
230
|
-
"lines": [
|
|
230
|
+
"lines": [181, 197],
|
|
231
231
|
"anchor": "The checkers cannot see a layout. Open the page at the width",
|
|
232
232
|
"anchorEnd": "`references/interaction-sources.md` names the sources for th",
|
|
233
233
|
"chips": [
|
|
234
234
|
{
|
|
235
235
|
"label": "Structure",
|
|
236
|
-
"lines": [
|
|
236
|
+
"lines": [183, 187],
|
|
237
237
|
"anchor": "The first section holds what the user came for.",
|
|
238
238
|
"anchorEnd": "The containers in a section align at the bottom."
|
|
239
239
|
},
|
|
240
240
|
{
|
|
241
241
|
"label": "Actions",
|
|
242
|
-
"lines": [
|
|
242
|
+
"lines": [188, 191],
|
|
243
243
|
"anchor": "The actions sit where the eye goes last, with the one primar",
|
|
244
244
|
"anchorEnd": "An action that runs longer than a moment shows progress in a"
|
|
245
245
|
},
|
|
246
246
|
{
|
|
247
247
|
"label": "Fields",
|
|
248
|
-
"lines": [
|
|
248
|
+
"lines": [192, 193],
|
|
249
249
|
"anchor": "Every field has a default, and Reset restores it.",
|
|
250
250
|
"anchorEnd": "Secondary settings sit in a `CollapsibleSection`. Every cont"
|
|
251
251
|
},
|
|
252
252
|
{
|
|
253
253
|
"label": "Words and access",
|
|
254
|
-
"lines": [
|
|
254
|
+
"lines": [194, 195],
|
|
255
255
|
"anchor": "Labels use the user's words, such as \"Export slices\".",
|
|
256
256
|
"anchorEnd": "Every `img` has `alt` text. Focus order follows the reading "
|
|
257
257
|
}
|
|
@@ -263,7 +263,7 @@ export const createPage: SkillTree = {
|
|
|
263
263
|
"kind": "step",
|
|
264
264
|
"title": "Read the page twice",
|
|
265
265
|
"desc": "From a distance only the sections show. Up close, every border and bar earns its place or goes.",
|
|
266
|
-
"lines": [
|
|
266
|
+
"lines": [199, 199],
|
|
267
267
|
"anchor": "Then read the page from a distance: the sections and their e"
|
|
268
268
|
},
|
|
269
269
|
{
|
|
@@ -2,7 +2,7 @@ import type { SkillTree } from '../types';
|
|
|
2
2
|
|
|
3
3
|
export const setType: SkillTree = {
|
|
4
4
|
"id": "live-tokens-set-type",
|
|
5
|
-
"digest": "sha256:
|
|
5
|
+
"digest": "sha256:51d5983b67b548cd",
|
|
6
6
|
"title": "set-type",
|
|
7
7
|
"tagline": "Set Typefaces from Google Fonts",
|
|
8
8
|
"nodes": [
|
|
@@ -38,7 +38,7 @@ export const setType: SkillTree = {
|
|
|
38
38
|
"label": "Choose the body face first",
|
|
39
39
|
"lines": [30, 34],
|
|
40
40
|
"anchor": "## Choose the body face first",
|
|
41
|
-
"anchorEnd": "The
|
|
41
|
+
"anchorEnd": "The display and heading usages ask the display face for 600 "
|
|
42
42
|
},
|
|
43
43
|
{
|
|
44
44
|
"label": "The font matrix",
|
|
@@ -15,6 +15,8 @@
|
|
|
15
15
|
disabled?: boolean;
|
|
16
16
|
selectionsLocked?: boolean;
|
|
17
17
|
onchange?: () => void;
|
|
18
|
+
/** Routes writes to the caller in place of the DOM; see `UITokenSelector`. */
|
|
19
|
+
onwrite?: (value: string | null) => void;
|
|
18
20
|
}
|
|
19
21
|
|
|
20
22
|
let {
|
|
@@ -24,6 +26,7 @@
|
|
|
24
26
|
disabled = false,
|
|
25
27
|
selectionsLocked = false,
|
|
26
28
|
onchange,
|
|
29
|
+
onwrite,
|
|
27
30
|
}: Props = $props();
|
|
28
31
|
|
|
29
32
|
const SOURCE_KIND_LABELS: Record<FontSourceKind, string> = {
|
|
@@ -191,6 +194,7 @@
|
|
|
191
194
|
dropdownMinWidth="14rem"
|
|
192
195
|
onreset={handleReset}
|
|
193
196
|
onvarChange={initFromCurrent}
|
|
197
|
+
{onwrite}
|
|
194
198
|
>
|
|
195
199
|
{#snippet triggerTitle()}{activeLabel}{/snippet}
|
|
196
200
|
{#snippet triggerMeta()}{displayFamily || '—'}{/snippet}
|
|
@@ -198,7 +202,7 @@
|
|
|
198
202
|
{#snippet children({ close })}
|
|
199
203
|
|
|
200
204
|
<UIOptionList>
|
|
201
|
-
{#each options as opt}
|
|
205
|
+
{#each options as opt (opt.key)}
|
|
202
206
|
<UIOptionItem
|
|
203
207
|
active={chosenKey === opt.key}
|
|
204
208
|
onclick={() => selectOption(opt.key, close)}
|
|
@@ -16,6 +16,8 @@
|
|
|
16
16
|
disabled?: boolean;
|
|
17
17
|
selectionsLocked?: boolean;
|
|
18
18
|
onchange?: () => void;
|
|
19
|
+
/** Routes writes to the caller in place of the DOM; see `UITokenSelector`. */
|
|
20
|
+
onwrite?: (value: string | null) => void;
|
|
19
21
|
}
|
|
20
22
|
|
|
21
23
|
let {
|
|
@@ -25,6 +27,7 @@
|
|
|
25
27
|
disabled = false,
|
|
26
28
|
selectionsLocked = false,
|
|
27
29
|
onchange,
|
|
30
|
+
onwrite,
|
|
28
31
|
}: Props = $props();
|
|
29
32
|
|
|
30
33
|
const options = [
|
|
@@ -87,6 +90,7 @@
|
|
|
87
90
|
varPrefix="--font-weight-"
|
|
88
91
|
{options}
|
|
89
92
|
{onchange}
|
|
93
|
+
{onwrite}
|
|
90
94
|
>
|
|
91
95
|
{#snippet option({ opt, active, select })}
|
|
92
96
|
{@const unsupported = availability !== null && !availability.weights.has(Number(opt.value))}
|
|
@@ -9,6 +9,8 @@
|
|
|
9
9
|
disabled?: boolean;
|
|
10
10
|
selectionsLocked?: boolean;
|
|
11
11
|
onchange?: () => void;
|
|
12
|
+
/** Routes writes to the caller in place of the DOM; see `UITokenSelector`. */
|
|
13
|
+
onwrite?: (value: string | null) => void;
|
|
12
14
|
}
|
|
13
15
|
|
|
14
16
|
let {
|
|
@@ -18,6 +20,7 @@
|
|
|
18
20
|
disabled = false,
|
|
19
21
|
selectionsLocked = false,
|
|
20
22
|
onchange,
|
|
23
|
+
onwrite,
|
|
21
24
|
}: Props = $props();
|
|
22
25
|
|
|
23
26
|
const options = [
|
|
@@ -38,6 +41,7 @@
|
|
|
38
41
|
varPrefix="--letter-spacing-"
|
|
39
42
|
{options}
|
|
40
43
|
{onchange}
|
|
44
|
+
{onwrite}
|
|
41
45
|
>
|
|
42
46
|
{#snippet option({ opt, active, select })}
|
|
43
47
|
<UIOptionItem {active} onclick={select}>
|
|
@@ -9,6 +9,8 @@
|
|
|
9
9
|
disabled?: boolean;
|
|
10
10
|
selectionsLocked?: boolean;
|
|
11
11
|
onchange?: () => void;
|
|
12
|
+
/** Routes writes to the caller in place of the DOM; see `UITokenSelector`. */
|
|
13
|
+
onwrite?: (value: string | null) => void;
|
|
12
14
|
}
|
|
13
15
|
|
|
14
16
|
let {
|
|
@@ -18,6 +20,7 @@
|
|
|
18
20
|
disabled = false,
|
|
19
21
|
selectionsLocked = false,
|
|
20
22
|
onchange,
|
|
23
|
+
onwrite,
|
|
21
24
|
}: Props = $props();
|
|
22
25
|
|
|
23
26
|
const options = [
|
|
@@ -39,6 +42,7 @@
|
|
|
39
42
|
varPrefix="--line-height-"
|
|
40
43
|
{options}
|
|
41
44
|
{onchange}
|
|
45
|
+
{onwrite}
|
|
42
46
|
>
|
|
43
47
|
{#snippet option({ opt, active, select })}
|
|
44
48
|
|
|
@@ -12,6 +12,8 @@
|
|
|
12
12
|
disabled?: boolean;
|
|
13
13
|
selectionsLocked?: boolean;
|
|
14
14
|
onchange?: () => void;
|
|
15
|
+
/** Routes writes to the caller in place of the DOM; see `UITokenSelector`. */
|
|
16
|
+
onwrite?: (value: string | null) => void;
|
|
15
17
|
}
|
|
16
18
|
|
|
17
19
|
let {
|
|
@@ -21,6 +23,7 @@
|
|
|
21
23
|
disabled = false,
|
|
22
24
|
selectionsLocked = false,
|
|
23
25
|
onchange,
|
|
26
|
+
onwrite,
|
|
24
27
|
}: Props = $props();
|
|
25
28
|
|
|
26
29
|
// text-transform is a literal CSS keyword, not a scale alias — the picker
|
|
@@ -78,6 +81,7 @@
|
|
|
78
81
|
dropdownMinWidth="12rem"
|
|
79
82
|
onreset={handleReset}
|
|
80
83
|
onvarChange={readResolved}
|
|
84
|
+
{onwrite}
|
|
81
85
|
>
|
|
82
86
|
{#snippet triggerTitle()}{activeLabel}{/snippet}
|
|
83
87
|
{#snippet triggerMeta()}{current || '—'}{/snippet}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
<script lang="ts" generics="T extends { key: string; label?: string; value?: string }">
|
|
2
2
|
import type { Snippet } from 'svelte';
|
|
3
3
|
import { resolveAliasChain } from '../core/palettes/tokenRegistry';
|
|
4
|
+
import { parseVariantRef } from './variantScales';
|
|
4
5
|
import UITokenSelector from './UITokenSelector.svelte';
|
|
5
6
|
import UIOptionList from './UIOptionList.svelte';
|
|
6
7
|
import UIOptionItem from './UIOptionItem.svelte';
|
|
@@ -28,6 +29,8 @@
|
|
|
28
29
|
/** Trailing dropdown content (e.g. action buttons). */
|
|
29
30
|
extras?: Snippet<[{ close: () => void }]>;
|
|
30
31
|
onchange?: () => void;
|
|
32
|
+
/** Routes writes to the caller in place of the DOM; see `UITokenSelector`. */
|
|
33
|
+
onwrite?: (value: string | null) => void;
|
|
31
34
|
}
|
|
32
35
|
|
|
33
36
|
let {
|
|
@@ -46,6 +49,7 @@
|
|
|
46
49
|
option: callerOption,
|
|
47
50
|
extras: callerExtras,
|
|
48
51
|
onchange,
|
|
52
|
+
onwrite,
|
|
49
53
|
}: Props<T> = $props();
|
|
50
54
|
|
|
51
55
|
let selector: UITokenSelector;
|
|
@@ -53,16 +57,9 @@
|
|
|
53
57
|
let currentValue: string = $state('');
|
|
54
58
|
|
|
55
59
|
let validKeys = $derived(new Set(options.map((o) => o.key)));
|
|
56
|
-
let refMatcher = $derived.by(() => {
|
|
57
|
-
const escaped = varPrefix.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
58
|
-
return new RegExp(`var\\((${escaped}[^)\\s]+)\\)`);
|
|
59
|
-
});
|
|
60
60
|
|
|
61
61
|
function parseRef(value: string): string | null {
|
|
62
|
-
|
|
63
|
-
if (!m) return null;
|
|
64
|
-
const key = m[1].slice(varPrefix.length);
|
|
65
|
-
return validKeys.has(key) ? key : null;
|
|
62
|
+
return parseVariantRef(value, varPrefix, validKeys);
|
|
66
63
|
}
|
|
67
64
|
|
|
68
65
|
function readResolved() {
|
|
@@ -137,6 +134,7 @@
|
|
|
137
134
|
{dropdownMaxWidth}
|
|
138
135
|
onreset={handleReset}
|
|
139
136
|
onvarChange={initFromCurrent}
|
|
137
|
+
{onwrite}
|
|
140
138
|
>
|
|
141
139
|
{#snippet triggerTitle()}
|
|
142
140
|
{#if callerTriggerTitle}{@render callerTriggerTitle({ activeOption })}{:else}{activeOption?.label ?? ''}{/if}
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
import { PALETTE_SPECS } from '../core/palettes/paletteDerivation';
|
|
5
5
|
import FontStackEditor from './FontStackEditor.svelte';
|
|
6
6
|
import ProjectFontsSection from './ProjectFontsSection.svelte';
|
|
7
|
-
import TextStylesSection from './TextStylesSection.svelte';
|
|
7
|
+
import TextStylesSection from './text-styles/TextStylesSection.svelte';
|
|
8
8
|
import ColumnsSection from './sections/ColumnsSection.svelte';
|
|
9
9
|
import TokenScaleTable from './sections/TokenScaleTable.svelte';
|
|
10
10
|
import WashesSection from './sections/WashesSection.svelte';
|
|
@@ -100,7 +100,6 @@
|
|
|
100
100
|
</div>
|
|
101
101
|
|
|
102
102
|
<div class="typography-group text-styles-group">
|
|
103
|
-
<h3 class="group-title">Text Styles</h3>
|
|
104
103
|
<TextStylesSection />
|
|
105
104
|
</div>
|
|
106
105
|
|
|
@@ -254,10 +253,11 @@
|
|
|
254
253
|
gap: var(--ui-space-32);
|
|
255
254
|
}
|
|
256
255
|
|
|
257
|
-
/*
|
|
258
|
-
|
|
256
|
+
/* Each step row holds three size inputs, a sample and four pickers, so the
|
|
257
|
+
group takes the full row. */
|
|
259
258
|
.text-styles-group {
|
|
260
259
|
grid-column: 1 / -1;
|
|
260
|
+
gap: var(--ui-space-16);
|
|
261
261
|
}
|
|
262
262
|
|
|
263
263
|
/* Utility Tokens */
|