@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.
Files changed (48) hide show
  1. package/.claude/skills/live-tokens-create-component/references/sketch-mode.md +7 -4
  2. package/.claude/skills/live-tokens-create-page/SKILL.md +10 -3
  3. package/.claude/skills/live-tokens-set-type/SKILL.md +3 -3
  4. package/CHANGELOG.md +153 -0
  5. package/bin/lib/tokenVocabulary.mjs +2 -2
  6. package/bin/rules/testRuns.mjs +1 -1
  7. package/bin/rules/tokens.mjs +3 -3
  8. package/dist-plugin/chunk-JOSSYGR2.js +1634 -0
  9. package/dist-plugin/index.cjs +932 -15
  10. package/dist-plugin/index.js +1 -1
  11. package/dist-plugin/setType/index.cjs +23 -2
  12. package/dist-plugin/setType/index.js +23 -2
  13. package/dist-plugin/tokensCssMigrations/index.cjs +936 -15
  14. package/dist-plugin/tokensCssMigrations/index.d.cts +23 -3
  15. package/dist-plugin/tokensCssMigrations/index.d.ts +23 -3
  16. package/dist-plugin/tokensCssMigrations/index.js +5 -1
  17. package/package.json +1 -1
  18. package/src/app/site.css +243 -9
  19. package/src/demo/TestingLoops.svelte +9 -9
  20. package/src/editor/core/fonts/weightCoverage.ts +31 -2
  21. package/src/editor/core/palettes/tokenRegistry.ts +36 -37
  22. package/src/editor/core/sketch/sketchLayer.ts +23 -17
  23. package/src/editor/core/themes/parsers/mediaBlocks.ts +46 -0
  24. package/src/editor/core/typeScale/typeScale.ts +341 -0
  25. package/src/editor/skill-atlas/SourcePane.svelte +3 -3
  26. package/src/editor/skill-atlas/skillSources.generated.ts +3 -3
  27. package/src/editor/skill-atlas/trees/create-page.ts +14 -14
  28. package/src/editor/skill-atlas/trees/set-type.ts +2 -2
  29. package/src/editor/ui/UIFontFamilySelector.svelte +5 -1
  30. package/src/editor/ui/UIFontWeightSelector.svelte +4 -0
  31. package/src/editor/ui/UILetterSpacingSelector.svelte +4 -0
  32. package/src/editor/ui/UILineHeightSelector.svelte +4 -0
  33. package/src/editor/ui/UITextTransformSelector.svelte +4 -0
  34. package/src/editor/ui/UIVariantSelector.svelte +6 -8
  35. package/src/editor/ui/VariablesTab.svelte +4 -4
  36. package/src/editor/ui/sections/textStyles.ts +44 -92
  37. package/src/editor/ui/text-styles/ControlBody.svelte +100 -0
  38. package/src/editor/ui/text-styles/ResetIcon.svelte +60 -0
  39. package/src/editor/ui/text-styles/StepRow.svelte +330 -0
  40. package/src/editor/ui/text-styles/StyleControl.svelte +128 -0
  41. package/src/editor/ui/text-styles/TextStylesSection.svelte +551 -0
  42. package/src/editor/ui/text-styles/textStyleEdits.ts +308 -0
  43. package/src/editor/ui/variantScales.ts +16 -0
  44. package/src/system/styles/tokens.css +412 -48
  45. package/src/testing-js/page-compliance.contract.js +233 -80
  46. package/src/testing-js/page-compliance.contract.js.map +1 -1
  47. package/dist-plugin/chunk-GWMEYBZ2.js +0 -715
  48. package/src/editor/ui/TextStylesSection.svelte +0 -171
@@ -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>
@@ -0,0 +1,330 @@
1
+ <script lang="ts">
2
+ import UIFontFamilySelector from '../UIFontFamilySelector.svelte';
3
+ import UIFontWeightSelector from '../UIFontWeightSelector.svelte';
4
+ import UILetterSpacingSelector from '../UILetterSpacingSelector.svelte';
5
+ import UILineHeightSelector from '../UILineHeightSelector.svelte';
6
+ import UITextTransformSelector from '../UITextTransformSelector.svelte';
7
+ import type { TextStyle } from '../sections/textStyles';
8
+ import {
9
+ DEFAULTS,
10
+ VIEWPORTS,
11
+ axisName,
12
+ sizeName,
13
+ usageName,
14
+ type StyleAxis,
15
+ type Viewport,
16
+ } from '../../core/typeScale/typeScale';
17
+ import ControlBody from './ControlBody.svelte';
18
+ import ResetIcon from './ResetIcon.svelte';
19
+ import StyleControl from './StyleControl.svelte';
20
+ import {
21
+ SIZE_RANGE_PX,
22
+ SMALL_SIZE_PX,
23
+ faceLabel,
24
+ formatPx,
25
+ isLinked,
26
+ isStored,
27
+ linkExpression,
28
+ sizesOf,
29
+ valueOf,
30
+ weightLabel,
31
+ type LinkAxis,
32
+ type StyleTarget,
33
+ type TextStyleEdits,
34
+ type TokenView,
35
+ } from './textStyleEdits';
36
+
37
+ interface Props {
38
+ target: StyleTarget;
39
+ style: TextStyle;
40
+ view: TokenView;
41
+ /** The viewport the sample renders at and the sizes highlight. */
42
+ viewport: Viewport;
43
+ edits: TextStyleEdits;
44
+ }
45
+
46
+ let { target, style, view, viewport, edits }: Props = $props();
47
+
48
+ const VIEWPORT_LABEL: Record<Viewport, string> = { desktop: 'Desktop', tablet: 'Tablet', phone: 'Phone' };
49
+
50
+ let sizes = $derived(sizesOf(view, target));
51
+ let prefix = $derived(target.prefix);
52
+ const axis = (a: StyleAxis) => axisName(prefix, a);
53
+ let chip = $derived(
54
+ target.step === null
55
+ ? style.defaultElement
56
+ : DEFAULTS.usages[target.usage].elements[target.step]?.join(', '),
57
+ );
58
+
59
+ let sampleStyle = $derived(
60
+ [
61
+ `font-family: var(${axis('font-family')})`,
62
+ `font-size: var(${sizeName(prefix, viewport)})`,
63
+ `font-weight: var(${axis('font-weight')})`,
64
+ `line-height: var(${axis('line-height')})`,
65
+ `letter-spacing: var(${axis('letter-spacing')})`,
66
+ ...(style.hasTextTransform ? [`text-transform: var(${axis('text-transform')})`] : []),
67
+ ].join('; '),
68
+ );
69
+
70
+ // A typed value that the store leaves unchanged would otherwise stay in the
71
+ // input, so the input re-reads the size the store now resolves.
72
+ function editSize(vp: Viewport, input: HTMLInputElement) {
73
+ edits.setSize(target, vp, input.valueAsNumber);
74
+ input.value = formatPx(sizesOf(edits.view(), target)[vp].px);
75
+ }
76
+
77
+ function link(a: LinkAxis, label: (value: string) => string) {
78
+ return {
79
+ linked: isLinked(view, target, a),
80
+ text: label(valueOf(view, usageName(target.usage, a))),
81
+ title: `Linked to ${linkExpression(target, a)}`,
82
+ ontoggle: () => edits.toggleLink(target, a),
83
+ };
84
+ }
85
+
86
+ const write = (a: StyleAxis) => (value: string | null) => edits.writeToken(axis(a), value);
87
+ const reset = (a: StyleAxis) => () => edits.resetKey(axis(a));
88
+ </script>
89
+
90
+ <div class="step">
91
+ <div class="step-id">
92
+ <code class="step-name">{style.name}</code>
93
+ {#if chip}
94
+ <span class="chip">{chip}</span>
95
+ {/if}
96
+ {#if sizes[viewport].px < SMALL_SIZE_PX}
97
+ <span class="flag">Below {SMALL_SIZE_PX}px</span>
98
+ {/if}
99
+ </div>
100
+
101
+ <div class="sizes">
102
+ {#each VIEWPORTS as vp (vp)}
103
+ {@const size = sizes[vp]}
104
+ <div class="size-row" class:is-current={vp === viewport} data-reset-scope>
105
+ <span class="size-label">{VIEWPORT_LABEL[vp]}</span>
106
+ <ControlBody width="4.75rem" linked={!size.edited}>
107
+ <input
108
+ class="size-input"
109
+ type="number"
110
+ min={SIZE_RANGE_PX.min}
111
+ max={SIZE_RANGE_PX.max}
112
+ step="any"
113
+ value={formatPx(size.px)}
114
+ aria-label="{style.name} {vp} size in px"
115
+ title={size.edited ? `Recommended ${formatPx(size.recommendedPx)}px` : 'Recommended size'}
116
+ onchange={(e) => editSize(vp, e.currentTarget)}
117
+ />
118
+ {#snippet after()}
119
+ <span class="unit">px</span>
120
+ <ResetIcon
121
+ label="{style.name} {vp} size"
122
+ visible={size.edited}
123
+ onreset={() => edits.resetSize(target, vp)}
124
+ />
125
+ {/snippet}
126
+ </ControlBody>
127
+ </div>
128
+ {/each}
129
+ </div>
130
+
131
+ <div class="step-sample" style={sampleStyle}>{style.preview}</div>
132
+
133
+ <div class="step-controls">
134
+ <StyleControl
135
+ label="Face"
136
+ name="{style.name} face"
137
+ width="10rem"
138
+ resetVisible={isStored(view, axis('font-family'))}
139
+ onreset={reset('font-family')}
140
+ link={link('font-family', faceLabel)}
141
+ >
142
+ <UIFontFamilySelector variable={axis('font-family')} onwrite={write('font-family')} />
143
+ </StyleControl>
144
+ <StyleControl
145
+ label="Weight"
146
+ name="{style.name} weight"
147
+ width="8rem"
148
+ resetVisible={isStored(view, axis('font-weight'))}
149
+ onreset={reset('font-weight')}
150
+ link={link('font-weight', weightLabel)}
151
+ >
152
+ <UIFontWeightSelector variable={axis('font-weight')} onwrite={write('font-weight')} />
153
+ </StyleControl>
154
+ <StyleControl
155
+ label="Line height"
156
+ name="{style.name} line height"
157
+ width="8rem"
158
+ resetVisible={isStored(view, axis('line-height'))}
159
+ onreset={reset('line-height')}
160
+ >
161
+ <UILineHeightSelector variable={axis('line-height')} onwrite={write('line-height')} />
162
+ </StyleControl>
163
+ <StyleControl
164
+ label="Letter spacing"
165
+ name="{style.name} letter spacing"
166
+ width="9.5rem"
167
+ resetVisible={isStored(view, axis('letter-spacing'))}
168
+ onreset={reset('letter-spacing')}
169
+ >
170
+ <UILetterSpacingSelector variable={axis('letter-spacing')} onwrite={write('letter-spacing')} />
171
+ </StyleControl>
172
+ {#if style.hasTextTransform}
173
+ <StyleControl
174
+ label="Transform"
175
+ name="{style.name} transform"
176
+ width="8rem"
177
+ resetVisible={isStored(view, axis('text-transform'))}
178
+ onreset={reset('text-transform')}
179
+ >
180
+ <UITextTransformSelector variable={axis('text-transform')} onwrite={write('text-transform')} />
181
+ </StyleControl>
182
+ {/if}
183
+ </div>
184
+ </div>
185
+
186
+ <style>
187
+ .step {
188
+ display: grid;
189
+ grid-template-columns: 7.5rem 11.5rem minmax(0, 1fr);
190
+ column-gap: var(--ui-space-16);
191
+ row-gap: var(--ui-space-10);
192
+ padding: var(--ui-space-12) var(--ui-space-10);
193
+ border-bottom: 1px solid var(--ui-border-low);
194
+ }
195
+
196
+ .step:last-child {
197
+ border-bottom: none;
198
+ }
199
+
200
+ .step-id {
201
+ grid-row: 1 / span 2;
202
+ display: flex;
203
+ flex-direction: column;
204
+ align-items: flex-start;
205
+ gap: var(--ui-space-6);
206
+ min-width: 0;
207
+ }
208
+
209
+ .step-name {
210
+ font-family: var(--ui-font-mono);
211
+ font-size: var(--ui-font-size-sm);
212
+ color: var(--ui-text-primary);
213
+ }
214
+
215
+ .chip {
216
+ padding: 1px var(--ui-space-8);
217
+ border: 1px solid var(--ui-border-low);
218
+ border-radius: var(--ui-radius-full);
219
+ color: var(--ui-text-secondary);
220
+ font-family: var(--ui-font-mono);
221
+ font-size: var(--ui-font-size-xs);
222
+ }
223
+
224
+ .flag {
225
+ padding: 1px var(--ui-space-8);
226
+ border: 1px dashed var(--ui-border-high);
227
+ border-radius: var(--ui-radius-full);
228
+ color: var(--ui-text-primary);
229
+ font-size: var(--ui-font-size-xs);
230
+ white-space: nowrap;
231
+ }
232
+
233
+ .sizes {
234
+ grid-row: 1 / span 2;
235
+ display: grid;
236
+ align-content: start;
237
+ gap: var(--ui-space-6);
238
+ }
239
+
240
+ .size-row {
241
+ display: grid;
242
+ grid-template-columns: 3.5rem auto;
243
+ align-items: center;
244
+ gap: var(--ui-space-4);
245
+ }
246
+
247
+ .size-label {
248
+ color: var(--ui-text-muted);
249
+ font-size: var(--ui-font-size-xs);
250
+ }
251
+
252
+ .size-row.is-current .size-label {
253
+ color: var(--ui-text-primary);
254
+ font-weight: var(--ui-font-weight-semibold);
255
+ }
256
+
257
+ .size-input {
258
+ box-sizing: border-box;
259
+ width: 100%;
260
+ height: 1.75rem;
261
+ padding: 0 var(--ui-space-8);
262
+ background: var(--ui-surface-high);
263
+ border: 1px solid var(--ui-border-low);
264
+ border-radius: var(--ui-radius-md);
265
+ color: var(--ui-text-primary);
266
+ font: inherit;
267
+ font-size: var(--ui-font-size-sm);
268
+ font-variant-numeric: tabular-nums;
269
+ }
270
+
271
+ .size-input:hover {
272
+ border-color: var(--ui-border-high);
273
+ }
274
+
275
+ .size-input:focus-visible {
276
+ outline: 2px solid var(--ui-text-primary);
277
+ outline-offset: 2px;
278
+ }
279
+
280
+ .unit {
281
+ flex: none;
282
+ color: var(--ui-text-muted);
283
+ font-size: var(--ui-font-size-xs);
284
+ }
285
+
286
+ /* Samples take the editor's ink; colour sits outside a text style. */
287
+ .step-sample {
288
+ grid-column: 3;
289
+ min-width: 0;
290
+ max-width: min(100%, 22ch);
291
+ color: var(--ui-text-primary);
292
+ display: -webkit-box;
293
+ -webkit-line-clamp: 2;
294
+ line-clamp: 2;
295
+ -webkit-box-orient: vertical;
296
+ overflow: hidden;
297
+ overflow-wrap: anywhere;
298
+ text-wrap: balance;
299
+ }
300
+
301
+ .step-controls {
302
+ grid-column: 3;
303
+ align-self: end;
304
+ display: flex;
305
+ flex-wrap: wrap;
306
+ gap: var(--ui-space-12);
307
+ }
308
+
309
+ @media (max-width: 720px) {
310
+ .step {
311
+ grid-template-columns: minmax(0, 1fr);
312
+ }
313
+
314
+ .step-id {
315
+ grid-row: auto;
316
+ flex-direction: row;
317
+ align-items: center;
318
+ flex-wrap: wrap;
319
+ }
320
+
321
+ .sizes {
322
+ grid-row: auto;
323
+ }
324
+
325
+ .step-sample,
326
+ .step-controls {
327
+ grid-column: auto;
328
+ }
329
+ }
330
+ </style>