@motion-proto/live-tokens 0.91.0 → 0.91.2
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.
|
@@ -17,10 +17,10 @@ forces:
|
|
|
17
17
|
|
|
18
18
|
| It forces | So the component must |
|
|
19
19
|
|----------------------------------------------|----------------------------------------------------------|
|
|
20
|
-
| `background: transparent
|
|
21
|
-
| `border-color: transparent
|
|
22
|
-
| `box-shadow: none
|
|
23
|
-
| `overflow: visible
|
|
20
|
+
| `background: transparent` | Name the fill again as `--sketch-fill` |
|
|
21
|
+
| `border-color: transparent` | Name the outline again as `--sketch-stroke` |
|
|
22
|
+
| `box-shadow: none` | Name the shadow again as `--sketch-shadow` |
|
|
23
|
+
| `overflow: visible` | Never put the class on a box whose clip carries meaning |
|
|
24
24
|
| `position: relative` | Never put the class on an absolutely-positioned root |
|
|
25
25
|
| `z-index: 0` | Expect a new stacking context on that element |
|
|
26
26
|
| `::before` (the fill), `::after` (the stroke) | Never own either pseudo-element on that element |
|
|
@@ -28,6 +28,9 @@ forces:
|
|
|
28
28
|
`::after` survives on `sketch-rule` alone, which draws no outline. `::before` is
|
|
29
29
|
claimed on all four.
|
|
30
30
|
|
|
31
|
+
The layer wins each of these at the weight of one ID selector. Select the
|
|
32
|
+
component's elements by class: a selector with an ID can outweigh the layer.
|
|
33
|
+
|
|
31
34
|
## Opting in
|
|
32
35
|
|
|
33
36
|
Put one class on the runtime component's root, chosen by **size, not by kind**.
|
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,28 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.91.2 — Sketch mode claims its parts at ID weight
|
|
4
|
+
|
|
5
|
+
### Changed
|
|
6
|
+
|
|
7
|
+
- **Sketch mode claims what it repaints without `!important`.** The layer
|
|
8
|
+
hides a part's own background, border and shadow, lifts its clip, and takes
|
|
9
|
+
over its `::before` and `::after`. Those eight declarations now sit in rules
|
|
10
|
+
that carry the weight of one ID selector in place of `!important`. Svelte
|
|
11
|
+
scopes a component rule by adding a class, so the layer still outranks every
|
|
12
|
+
shipped component rule, and a page can override it at ID weight. A component
|
|
13
|
+
that joins the layer through a `sketch-*` class should select its elements by
|
|
14
|
+
class: an ID in its selector can outweigh the layer.
|
|
15
|
+
|
|
16
|
+
## 0.91.1 — Sketch mode respects bare images
|
|
17
|
+
|
|
18
|
+
### Fixed
|
|
19
|
+
|
|
20
|
+
- **Sketch mode leaves a bare Image unframed.** The layer drew every `.image`
|
|
21
|
+
from the default frame tokens, so `variant="bare"` lost its effect once a
|
|
22
|
+
sketchstyle was on and cut-out art picked up a hand-drawn border and rounded
|
|
23
|
+
clip. A bare image now draws from `--image-bare-border`,
|
|
24
|
+
`--image-bare-radius` and `--image-bare-shadow`.
|
|
25
|
+
|
|
3
26
|
## 0.91.0 — Overlay for ImageLightbox
|
|
4
27
|
|
|
5
28
|
### Added
|
package/package.json
CHANGED
|
@@ -254,6 +254,11 @@ const PART_SPECS: readonly PartSpec[] = [
|
|
|
254
254
|
// Holds the picture inside the frame; without it a zoom spills out square.
|
|
255
255
|
clips: true,
|
|
256
256
|
},
|
|
257
|
+
{
|
|
258
|
+
sel: '.image.bare', fill: 'transparent', stroke: 'var(--image-bare-border)',
|
|
259
|
+
radius: 'var(--image-bare-radius, 0px)', shadow: 'var(--image-bare-shadow, none)',
|
|
260
|
+
clips: true,
|
|
261
|
+
},
|
|
257
262
|
...['single', 'range'].map((v) => ({
|
|
258
263
|
sel: `.slider.${v} .slider-track`,
|
|
259
264
|
fill: `var(--slider-${v}-track-surface)`,
|
|
@@ -379,6 +384,12 @@ const OUTSIDE_CHROME =
|
|
|
379
384
|
':where(:not([data-live-tokens-chrome], [data-live-tokens-chrome] *),' +
|
|
380
385
|
' [data-live-tokens-chrome] [data-sketch] *)';
|
|
381
386
|
|
|
387
|
+
/** Matches every element and weighs one ID. Svelte scopes a rule by adding a
|
|
388
|
+
class, and no shipped component selects by ID, so a rule carrying this
|
|
389
|
+
outweighs any component rule however many states it stacks. It goes only on
|
|
390
|
+
the declarations the layer has to win; the rest keep their ordinary weight. */
|
|
391
|
+
export const OUTWEIGH_COMPONENTS = ':is(*, #_)';
|
|
392
|
+
|
|
382
393
|
export function buildDefsMarkup(s: SketchStyleSettings): string {
|
|
383
394
|
/**
|
|
384
395
|
* `warp` is the shape stage: one wave of noise whose wavelength spans a whole
|
|
@@ -706,6 +717,7 @@ const WARP_FREQUENCY = 0.08;
|
|
|
706
717
|
|
|
707
718
|
export function buildStylesheet(s: SketchStyleSettings): string {
|
|
708
719
|
const on = '[data-sketch]';
|
|
720
|
+
const claim = `${on}${OUTWEIGH_COMPONENTS}`;
|
|
709
721
|
const parts = `:is(${PARTS})${OUTSIDE_CHROME}`;
|
|
710
722
|
const el = `${on} ${parts}`;
|
|
711
723
|
const strokeEl = `${on} :is(${STROKE_PARTS})${OUTSIDE_CHROME}`;
|
|
@@ -863,10 +875,8 @@ export function buildStylesheet(s: SketchStyleSettings): string {
|
|
|
863
875
|
shimmer ::before states `width: 100%` and would otherwise pin the grown box
|
|
864
876
|
back to the element. `background` is a shorthand and resets both box
|
|
865
877
|
properties, so `bleedPaint` follows every declaration of it. */
|
|
866
|
-
const
|
|
867
|
-
|
|
868
|
-
: `inset:-${bleed}px !important;padding:${bleed}px;box-sizing:content-box;` +
|
|
869
|
-
`width:auto !important;height:auto !important;`;
|
|
878
|
+
const bleedClaim = bleed === 0 ? 'inset:0;' : `inset:-${bleed}px;width:auto;height:auto;`;
|
|
879
|
+
const bleedBox = bleed === 0 ? '' : `padding:${bleed}px;box-sizing:content-box;`;
|
|
870
880
|
const bleedPaint = bleed === 0 ? '' : 'background-origin:content-box;background-clip:content-box;';
|
|
871
881
|
|
|
872
882
|
const host =
|
|
@@ -880,12 +890,11 @@ export function buildStylesheet(s: SketchStyleSettings): string {
|
|
|
880
890
|
// but overwrite the one a floating part stacks by: a lightbox scrim or a
|
|
881
891
|
// tooltip at 0 falls under the page's sticky header.
|
|
882
892
|
`isolation:isolate;` +
|
|
883
|
-
// The shadow is re-cast on the fill layer, where it follows the shape
|
|
884
|
-
// that is actually drawn.
|
|
885
|
-
`background:transparent !important;border-color:transparent !important;` +
|
|
886
|
-
`box-shadow:none !important;` +
|
|
887
893
|
`}` +
|
|
888
|
-
|
|
894
|
+
// The shadow is re-cast on the fill layer, where it follows the shape that
|
|
895
|
+
// is actually drawn.
|
|
896
|
+
`${claim} ${parts}{background:transparent;border-color:transparent;box-shadow:none;}` +
|
|
897
|
+
`${claim} :is(${UNCLIPPED})${OUTSIDE_CHROME}{overflow:visible;}` +
|
|
889
898
|
// The five that keep their clip take the drawn radii on the host, so what
|
|
890
899
|
// the clip cuts turns the same way the ink does at the corners. It cannot
|
|
891
900
|
// follow the displacement, which is a filter and has no geometry to clip to.
|
|
@@ -896,15 +905,15 @@ export function buildStylesheet(s: SketchStyleSettings): string {
|
|
|
896
905
|
|
|
897
906
|
// Fill layer. Own seed, own offset, sits behind the content.
|
|
898
907
|
//
|
|
899
|
-
// `inset` and `transition` are
|
|
908
|
+
// `inset` and `transition` are claimed because the layer takes this
|
|
900
909
|
// pseudo-element from whatever the component was using it for. Button drives
|
|
901
910
|
// a hover shimmer off ::before — parked at left:-100%, sliding to left:100%
|
|
902
|
-
// over 0.5s —
|
|
903
|
-
//
|
|
904
|
-
//
|
|
911
|
+
// over 0.5s — from a hover rule as heavy as this one. Left alone, the fill
|
|
912
|
+
// wipes across the button on hover, and again the moment the effect is
|
|
913
|
+
// switched on, because `left` animates from -100% to 0.
|
|
905
914
|
const fill =
|
|
906
915
|
`${el}::before{` +
|
|
907
|
-
`content:'';position:absolute;${bleedBox}
|
|
916
|
+
`content:'';position:absolute;${bleedBox}` +
|
|
908
917
|
`z-index:-1;${fillCorners}` +
|
|
909
918
|
`background:var(--sketch-fill, var(--surface-neutral-lower));` +
|
|
910
919
|
bleedPaint +
|
|
@@ -921,7 +930,8 @@ export function buildStylesheet(s: SketchStyleSettings): string {
|
|
|
921
930
|
`) rotate(calc(var(--sketch-jr, 0) * var(--sketch-jit-rot, 0deg)))` +
|
|
922
931
|
` scale(calc(1 + (var(--sketch-js, 0) + 1) / 2 * var(--sketch-jit-scale, 0)));` +
|
|
923
932
|
`pointer-events:none;` +
|
|
924
|
-
`}
|
|
933
|
+
`}` +
|
|
934
|
+
`${claim} ${parts}::before{${bleedClaim}transition:none;}`;
|
|
925
935
|
|
|
926
936
|
/* The parts that float over page content take the fill whole, and everything
|
|
927
937
|
else the layer does with it. The second `:is` outweighs the rule above. */
|
|
@@ -1010,7 +1020,7 @@ export function buildStylesheet(s: SketchStyleSettings): string {
|
|
|
1010
1020
|
// Outline layer. Own seed, above the content.
|
|
1011
1021
|
const stroke =
|
|
1012
1022
|
`${strokeEl}::after{` +
|
|
1013
|
-
`content:'';position:absolute
|
|
1023
|
+
`content:'';position:absolute;` +
|
|
1014
1024
|
`z-index:1;${corners}` +
|
|
1015
1025
|
`border-style:var(--sketch-stroke-style);` +
|
|
1016
1026
|
`border-color:${ink('var(--border-neutral)')};` +
|
|
@@ -1019,7 +1029,8 @@ export function buildStylesheet(s: SketchStyleSettings): string {
|
|
|
1019
1029
|
`filter:var(--sketch-stroke-filter) var(--sketch-retrace, opacity(1))` +
|
|
1020
1030
|
` var(--sketch-pool, opacity(1));` +
|
|
1021
1031
|
`pointer-events:none;` +
|
|
1022
|
-
`}
|
|
1032
|
+
`}` +
|
|
1033
|
+
`${claim} :is(${STROKE_PARTS})${OUTSIDE_CHROME}::after{inset:0;transition:none;}`;
|
|
1023
1034
|
|
|
1024
1035
|
const seedRotation = SEEDS.map((seed, i) =>
|
|
1025
1036
|
`${el}:nth-child(5n + ${i + 1}){` +
|
|
@@ -13,7 +13,7 @@ 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": {
|