@motion-proto/live-tokens 0.91.2 → 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.
Files changed (46) hide show
  1. package/.claude/skills/live-tokens-create-page/SKILL.md +10 -3
  2. package/.claude/skills/live-tokens-set-type/SKILL.md +3 -3
  3. package/CHANGELOG.md +140 -0
  4. package/bin/lib/tokenVocabulary.mjs +2 -2
  5. package/bin/rules/testRuns.mjs +1 -1
  6. package/bin/rules/tokens.mjs +3 -3
  7. package/dist-plugin/chunk-JOSSYGR2.js +1634 -0
  8. package/dist-plugin/index.cjs +932 -15
  9. package/dist-plugin/index.js +1 -1
  10. package/dist-plugin/setType/index.cjs +23 -2
  11. package/dist-plugin/setType/index.js +23 -2
  12. package/dist-plugin/tokensCssMigrations/index.cjs +936 -15
  13. package/dist-plugin/tokensCssMigrations/index.d.cts +23 -3
  14. package/dist-plugin/tokensCssMigrations/index.d.ts +23 -3
  15. package/dist-plugin/tokensCssMigrations/index.js +5 -1
  16. package/package.json +1 -1
  17. package/src/app/site.css +243 -9
  18. package/src/demo/TestingLoops.svelte +9 -9
  19. package/src/editor/core/fonts/weightCoverage.ts +31 -2
  20. package/src/editor/core/palettes/tokenRegistry.ts +36 -37
  21. package/src/editor/core/themes/parsers/mediaBlocks.ts +46 -0
  22. package/src/editor/core/typeScale/typeScale.ts +341 -0
  23. package/src/editor/skill-atlas/SourcePane.svelte +3 -3
  24. package/src/editor/skill-atlas/skillSources.generated.ts +2 -2
  25. package/src/editor/skill-atlas/trees/create-page.ts +14 -14
  26. package/src/editor/skill-atlas/trees/set-type.ts +2 -2
  27. package/src/editor/ui/UIFontFamilySelector.svelte +5 -1
  28. package/src/editor/ui/UIFontWeightSelector.svelte +4 -0
  29. package/src/editor/ui/UILetterSpacingSelector.svelte +4 -0
  30. package/src/editor/ui/UILineHeightSelector.svelte +4 -0
  31. package/src/editor/ui/UITextTransformSelector.svelte +4 -0
  32. package/src/editor/ui/UIVariantSelector.svelte +6 -8
  33. package/src/editor/ui/VariablesTab.svelte +4 -4
  34. package/src/editor/ui/sections/textStyles.ts +44 -92
  35. package/src/editor/ui/text-styles/ControlBody.svelte +100 -0
  36. package/src/editor/ui/text-styles/ResetIcon.svelte +60 -0
  37. package/src/editor/ui/text-styles/StepRow.svelte +330 -0
  38. package/src/editor/ui/text-styles/StyleControl.svelte +128 -0
  39. package/src/editor/ui/text-styles/TextStylesSection.svelte +551 -0
  40. package/src/editor/ui/text-styles/textStyleEdits.ts +308 -0
  41. package/src/editor/ui/variantScales.ts +16 -0
  42. package/src/system/styles/tokens.css +412 -48
  43. package/src/testing-js/page-compliance.contract.js +233 -80
  44. package/src/testing-js/page-compliance.contract.js.map +1 -1
  45. package/dist-plugin/chunk-GWMEYBZ2.js +0 -715
  46. package/src/editor/ui/TextStylesSection.svelte +0 -171
@@ -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:e9eafa612a3b863b",
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, 120],
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": [122, 130],
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": [132, 146],
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": [152, 152],
196
+ "lines": [159, 159],
197
197
  "anchor": "`<LiveTokensRouter pages={...}>`: add a `pages` entry with `"
198
198
  },
199
199
  {
200
200
  "label": "LiveEditorOverlay",
201
- "lines": [153, 153],
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": [170, 170],
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": [172, 172],
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": [174, 190],
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": [176, 180],
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": [181, 184],
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": [185, 186],
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": [187, 188],
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": [192, 192],
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:0c8d26a16313f9ee",
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 shipped text styles ask the display face for 600 and 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
- const m = value.match(refMatcher);
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
- /* The definition table is wide (four pickers + preview per row); give it the
258
- full row so it isn't squeezed into an auto-fill column. */
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 */
@@ -1,110 +1,62 @@
1
- /**
2
- * Registry of the semantic text styles rendered by TextStylesSection.
3
- * Each style is a bundle of alias tokens declared in tokens.css; the prefix
4
- * joins with a kind suffix (`-font-family`, `-font-size`, …) to form each
5
- * axis variable the editor's pickers target.
6
- *
7
- * Family/size/weight/letter-spacing/line-height are editable on every style.
8
- * `hasTextTransform` marks the one style (eyebrow) that also exposes a
9
- * text-transform picker — the only axis that exists on just one style.
10
- */
1
+ import {
2
+ DEFAULTS,
3
+ EYEBROW_PREFIX,
4
+ STEPS,
5
+ USAGES,
6
+ stepPrefix,
7
+ type StylePrefix,
8
+ type Usage,
9
+ } from '../../core/typeScale/typeScale';
11
10
 
12
11
  export interface TextStyle {
13
12
  name: string;
14
13
  label: string;
15
14
  /** Variable prefix, e.g. `--heading-xl`; suffixes form each axis var. */
16
- prefix: string;
17
- /** Element (or class) this style is the default for. */
15
+ prefix: StylePrefix;
16
+ /** The elements `site.css` styles with it, or else its class. */
18
17
  defaultElement: string;
19
18
  preview: string;
20
19
  /** Whether this style exposes an editable `-text-transform` axis. */
21
20
  hasTextTransform?: boolean;
22
21
  }
23
22
 
23
+ const USAGE_LABEL: Readonly<Record<Usage, string>> = {
24
+ display: 'Display',
25
+ heading: 'Heading',
26
+ body: 'Body',
27
+ editorial: 'Editorial',
28
+ code: 'Code',
29
+ };
30
+
31
+ const USAGE_PREVIEW: Readonly<Record<Usage, string>> = {
32
+ display: 'Tokens that move with every edit',
33
+ heading: 'Choosing a palette for the launch page',
34
+ body: 'Body copy for comfortable reading.',
35
+ editorial: 'A pull quote sets the tone for the essay.',
36
+ code: 'const total = sum(items.map(price));',
37
+ };
38
+
39
+ /** Every text style in `tokens.css` order: each usage's steps, largest first, then the eyebrow. */
24
40
  export const TEXT_STYLES: TextStyle[] = [
25
- {
26
- name: 'heading-xl',
27
- label: 'Heading XL',
28
- prefix: '--heading-xl',
29
- defaultElement: 'h1',
30
- preview: 'Page title',
31
- },
32
- {
33
- name: 'heading-lg',
34
- label: 'Heading LG',
35
- prefix: '--heading-lg',
36
- defaultElement: 'h2',
37
- preview: 'Section heading',
38
- },
39
- {
40
- name: 'heading-md',
41
- label: 'Heading MD',
42
- prefix: '--heading-md',
43
- defaultElement: 'h3',
44
- preview: 'Subsection heading',
45
- },
46
- {
47
- name: 'heading-sm',
48
- label: 'Heading SM',
49
- prefix: '--heading-sm',
50
- defaultElement: 'h4',
51
- preview: 'Minor heading',
52
- },
53
- {
54
- name: 'body-md',
55
- label: 'Body MD',
56
- prefix: '--body-md',
57
- defaultElement: 'p',
58
- preview: 'Body copy for comfortable reading.',
59
- },
60
- {
61
- name: 'body-sm',
62
- label: 'Body SM',
63
- prefix: '--body-sm',
64
- defaultElement: 'small',
65
- preview: 'Captions and secondary text.',
66
- },
67
- {
68
- name: 'editorial-xl',
69
- label: 'Editorial XL',
70
- prefix: '--editorial-xl',
71
- defaultElement: 'editorial-xl',
72
- preview: 'Pull quotes and opening statements.',
73
- },
74
- {
75
- name: 'editorial-lg',
76
- label: 'Editorial LG',
77
- prefix: '--editorial-lg',
78
- defaultElement: 'editorial-lg',
79
- preview: 'Standfirsts and section ledes.',
80
- },
81
- {
82
- name: 'editorial-md',
83
- label: 'Editorial MD',
84
- prefix: '--editorial-md',
85
- defaultElement: 'editorial-md',
86
- preview: 'Long-form reading, set in the editorial face.',
87
- },
88
- {
89
- name: 'editorial-sm',
90
- label: 'Editorial SM',
91
- prefix: '--editorial-sm',
92
- defaultElement: 'editorial-sm',
93
- preview: 'Captions, credits, and asides.',
94
- },
95
- {
96
- name: 'code',
97
- label: 'Code',
98
- prefix: '--code',
99
- defaultElement: 'code',
100
- preview: 'const total = sum(items);',
101
- },
41
+ ...USAGES.flatMap((usage) =>
42
+ [...STEPS].reverse().map((step): TextStyle => {
43
+ const name = `${usage}-${step}`;
44
+ const elements = DEFAULTS.usages[usage].elements[step];
45
+ return {
46
+ name,
47
+ label: `${USAGE_LABEL[usage]} ${step.toUpperCase()}`,
48
+ prefix: stepPrefix(usage, step),
49
+ defaultElement: elements ? elements.join(', ') : `.${name}`,
50
+ preview: USAGE_PREVIEW[usage],
51
+ };
52
+ }),
53
+ ),
102
54
  {
103
55
  name: 'eyebrow',
104
56
  label: 'Eyebrow',
105
- prefix: '--eyebrow',
106
- defaultElement: 'eyebrow',
107
- preview: 'Overline label',
57
+ prefix: EYEBROW_PREFIX,
58
+ defaultElement: '.eyebrow',
59
+ preview: 'Release notes',
108
60
  hasTextTransform: true,
109
61
  },
110
62
  ];
@@ -0,0 +1,100 @@
1
+ <script lang="ts">
2
+ import type { Snippet } from 'svelte';
3
+
4
+ interface Props {
5
+ width: string;
6
+ /** Null draws no link bar. */
7
+ linked: boolean | null;
8
+ children: Snippet;
9
+ /** Content after the body, such as a lock or a unit. */
10
+ after?: Snippet;
11
+ }
12
+
13
+ let { width, linked, children, after }: Props = $props();
14
+ </script>
15
+
16
+ {#if linked === null}
17
+ <div class="ctl-body" style:--w={width}>{@render children()}</div>
18
+ {:else}
19
+ <div class="linkwrap" class:is-unlinked={!linked}>
20
+ <div class="ctl-body" style:--w={width}>{@render children()}</div>
21
+ {@render after?.()}
22
+ </div>
23
+ {/if}
24
+
25
+ <style>
26
+ /* The link bar matches the editor's token selectors: a white bar flush
27
+ against a linked control, an amber tick beside an indented unlinked one. */
28
+ .linkwrap {
29
+ position: relative;
30
+ display: flex;
31
+ align-items: center;
32
+ gap: var(--ui-space-6);
33
+ min-width: 0;
34
+ padding-left: 4px;
35
+ }
36
+
37
+ .linkwrap::before {
38
+ content: "";
39
+ position: absolute;
40
+ top: 50%;
41
+ left: 0;
42
+ width: 4px;
43
+ height: 1.75rem;
44
+ background: var(--ui-text-primary);
45
+ border-radius: var(--ui-radius-md) 0 0 var(--ui-radius-md);
46
+ transform: translateY(-50%);
47
+ pointer-events: none;
48
+ transition:
49
+ width 220ms cubic-bezier(0.4, 0, 0.2, 1),
50
+ height 220ms cubic-bezier(0.4, 0, 0.2, 1),
51
+ background-color 220ms cubic-bezier(0.4, 0, 0.2, 1);
52
+ }
53
+
54
+ .linkwrap.is-unlinked {
55
+ padding-left: 10px;
56
+ }
57
+
58
+ .linkwrap.is-unlinked::before {
59
+ width: 2px;
60
+ height: 0.875rem;
61
+ background: var(--ui-link-broken);
62
+ border-radius: 1px;
63
+ }
64
+
65
+ /* Unlinking indents the body by 6px and narrows it by as much, so whatever
66
+ follows holds its position. */
67
+ .ctl-body {
68
+ box-sizing: border-box;
69
+ display: flex;
70
+ align-items: center;
71
+ width: var(--w);
72
+ min-width: 0;
73
+ }
74
+
75
+ .is-unlinked > .ctl-body {
76
+ width: calc(var(--w) - 6px);
77
+ }
78
+
79
+ .ctl-body > :global(*) {
80
+ flex: 1 1 auto;
81
+ min-width: 0;
82
+ }
83
+
84
+ .ctl-body :global(.ui-token-selector) {
85
+ display: block;
86
+ width: 100%;
87
+ }
88
+
89
+ /* The trigger names the value; a resolved-value line under it would push
90
+ the lock off the trigger's centre line. */
91
+ .ctl-body :global(.ui-ts-meta-text) {
92
+ display: none;
93
+ }
94
+
95
+ @media (prefers-reduced-motion: reduce) {
96
+ .linkwrap::before {
97
+ transition: none;
98
+ }
99
+ }
100
+ </style>
@@ -0,0 +1,60 @@
1
+ <script lang="ts">
2
+ import { tick } from 'svelte';
3
+
4
+ interface Props {
5
+ /** Names the value in the button's accessible label. */
6
+ label: string;
7
+ visible: boolean;
8
+ onreset: () => void;
9
+ }
10
+
11
+ let { label, visible, onreset }: Props = $props();
12
+
13
+ // The icon leaves the DOM once its value resets, so focus moves to the
14
+ // control it restored.
15
+ async function reset(e: MouseEvent) {
16
+ const scope = (e.currentTarget as HTMLElement).closest<HTMLElement>('[data-reset-scope]');
17
+ onreset();
18
+ await tick();
19
+ scope?.querySelector<HTMLElement>('input, select, button:not(.reset)')?.focus();
20
+ }
21
+ </script>
22
+
23
+ {#if visible}
24
+ <button
25
+ type="button"
26
+ class="reset"
27
+ aria-label="Reset {label} to default"
28
+ title="Reset to default"
29
+ onclick={reset}
30
+ >
31
+ <i class="fas fa-rotate-left" aria-hidden="true"></i>
32
+ </button>
33
+ {/if}
34
+
35
+ <style>
36
+ .reset {
37
+ flex: none;
38
+ display: inline-grid;
39
+ place-items: center;
40
+ width: 18px;
41
+ height: 18px;
42
+ padding: 0;
43
+ background: none;
44
+ border: none;
45
+ border-radius: var(--ui-radius-md);
46
+ color: var(--ui-text-tertiary);
47
+ font-size: 0.6875rem;
48
+ cursor: pointer;
49
+ }
50
+
51
+ .reset:hover {
52
+ color: var(--ui-text-primary);
53
+ background: var(--ui-surface-higher);
54
+ }
55
+
56
+ .reset:focus-visible {
57
+ outline: 2px solid var(--ui-text-primary);
58
+ outline-offset: 1px;
59
+ }
60
+ </style>