@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
@@ -384,6 +384,12 @@ const OUTSIDE_CHROME =
384
384
  ':where(:not([data-live-tokens-chrome], [data-live-tokens-chrome] *),' +
385
385
  ' [data-live-tokens-chrome] [data-sketch] *)';
386
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
+
387
393
  export function buildDefsMarkup(s: SketchStyleSettings): string {
388
394
  /**
389
395
  * `warp` is the shape stage: one wave of noise whose wavelength spans a whole
@@ -711,6 +717,7 @@ const WARP_FREQUENCY = 0.08;
711
717
 
712
718
  export function buildStylesheet(s: SketchStyleSettings): string {
713
719
  const on = '[data-sketch]';
720
+ const claim = `${on}${OUTWEIGH_COMPONENTS}`;
714
721
  const parts = `:is(${PARTS})${OUTSIDE_CHROME}`;
715
722
  const el = `${on} ${parts}`;
716
723
  const strokeEl = `${on} :is(${STROKE_PARTS})${OUTSIDE_CHROME}`;
@@ -868,10 +875,8 @@ export function buildStylesheet(s: SketchStyleSettings): string {
868
875
  shimmer ::before states `width: 100%` and would otherwise pin the grown box
869
876
  back to the element. `background` is a shorthand and resets both box
870
877
  properties, so `bleedPaint` follows every declaration of it. */
871
- const bleedBox = bleed === 0
872
- ? 'inset:0 !important;'
873
- : `inset:-${bleed}px !important;padding:${bleed}px;box-sizing:content-box;` +
874
- `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;`;
875
880
  const bleedPaint = bleed === 0 ? '' : 'background-origin:content-box;background-clip:content-box;';
876
881
 
877
882
  const host =
@@ -885,12 +890,11 @@ export function buildStylesheet(s: SketchStyleSettings): string {
885
890
  // but overwrite the one a floating part stacks by: a lightbox scrim or a
886
891
  // tooltip at 0 falls under the page's sticky header.
887
892
  `isolation:isolate;` +
888
- // The shadow is re-cast on the fill layer, where it follows the shape
889
- // that is actually drawn.
890
- `background:transparent !important;border-color:transparent !important;` +
891
- `box-shadow:none !important;` +
892
893
  `}` +
893
- `${on} :is(${UNCLIPPED})${OUTSIDE_CHROME}{overflow:visible !important;}` +
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;}` +
894
898
  // The five that keep their clip take the drawn radii on the host, so what
895
899
  // the clip cuts turns the same way the ink does at the corners. It cannot
896
900
  // follow the displacement, which is a filter and has no geometry to clip to.
@@ -901,15 +905,15 @@ export function buildStylesheet(s: SketchStyleSettings): string {
901
905
 
902
906
  // Fill layer. Own seed, own offset, sits behind the content.
903
907
  //
904
- // `inset` and `transition` are !important because the layer CLAIMS this
908
+ // `inset` and `transition` are claimed because the layer takes this
905
909
  // pseudo-element from whatever the component was using it for. Button drives
906
910
  // a hover shimmer off ::before — parked at left:-100%, sliding to left:100%
907
- // over 0.5s — and its hover rule outweighs this one on specificity. Left
908
- // alone, the fill wipes across the button on hover, and again the moment the
909
- // effect is switched on, because `left` animates from -100% to 0.
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.
910
914
  const fill =
911
915
  `${el}::before{` +
912
- `content:'';position:absolute;${bleedBox}transition:none !important;` +
916
+ `content:'';position:absolute;${bleedBox}` +
913
917
  `z-index:-1;${fillCorners}` +
914
918
  `background:var(--sketch-fill, var(--surface-neutral-lower));` +
915
919
  bleedPaint +
@@ -926,7 +930,8 @@ export function buildStylesheet(s: SketchStyleSettings): string {
926
930
  `) rotate(calc(var(--sketch-jr, 0) * var(--sketch-jit-rot, 0deg)))` +
927
931
  ` scale(calc(1 + (var(--sketch-js, 0) + 1) / 2 * var(--sketch-jit-scale, 0)));` +
928
932
  `pointer-events:none;` +
929
- `}`;
933
+ `}` +
934
+ `${claim} ${parts}::before{${bleedClaim}transition:none;}`;
930
935
 
931
936
  /* The parts that float over page content take the fill whole, and everything
932
937
  else the layer does with it. The second `:is` outweighs the rule above. */
@@ -1015,7 +1020,7 @@ export function buildStylesheet(s: SketchStyleSettings): string {
1015
1020
  // Outline layer. Own seed, above the content.
1016
1021
  const stroke =
1017
1022
  `${strokeEl}::after{` +
1018
- `content:'';position:absolute;inset:0 !important;transition:none !important;` +
1023
+ `content:'';position:absolute;` +
1019
1024
  `z-index:1;${corners}` +
1020
1025
  `border-style:var(--sketch-stroke-style);` +
1021
1026
  `border-color:${ink('var(--border-neutral)')};` +
@@ -1024,7 +1029,8 @@ export function buildStylesheet(s: SketchStyleSettings): string {
1024
1029
  `filter:var(--sketch-stroke-filter) var(--sketch-retrace, opacity(1))` +
1025
1030
  ` var(--sketch-pool, opacity(1));` +
1026
1031
  `pointer-events:none;` +
1027
- `}`;
1032
+ `}` +
1033
+ `${claim} :is(${STROKE_PARTS})${OUTSIDE_CHROME}::after{inset:0;transition:none;}`;
1028
1034
 
1029
1035
  const seedRotation = SEEDS.map((seed, i) =>
1030
1036
  `${el}:nth-child(5n + ${i + 1}){` +
@@ -0,0 +1,46 @@
1
+ /**
2
+ * `css` with every `@media` block removed. Readers that want a token's declared
3
+ * default go through it, so a breakpoint re-point never stands in for the
4
+ * top-level value.
5
+ *
6
+ * Dependency-free so the vite plugin build can import it, as it does
7
+ * `globalRootBlock`.
8
+ */
9
+ export function stripMediaBlocks(css: string): string {
10
+ const kept: string[] = [];
11
+ let from = 0;
12
+ let i = 0;
13
+ while (i < css.length) {
14
+ if (css.startsWith('/*', i)) {
15
+ i = commentEnd(css, i);
16
+ } else if (css[i] === '@' && /^@media\b/i.test(css.slice(i, i + 7))) {
17
+ kept.push(css.slice(from, i));
18
+ i = from = blockEnd(css, i);
19
+ } else {
20
+ i++;
21
+ }
22
+ }
23
+ kept.push(css.slice(from));
24
+ return kept.join('');
25
+ }
26
+
27
+ function commentEnd(css: string, start: number): number {
28
+ const end = css.indexOf('*/', start + 2);
29
+ return end === -1 ? css.length : end + 2;
30
+ }
31
+
32
+ /** Index just past the brace that closes the first block opened at or after `start`. */
33
+ function blockEnd(css: string, start: number): number {
34
+ let depth = 0;
35
+ let i = start;
36
+ while (i < css.length) {
37
+ if (css.startsWith('/*', i)) {
38
+ i = commentEnd(css, i);
39
+ continue;
40
+ }
41
+ const c = css[i++];
42
+ if (c === '{') depth++;
43
+ else if (c === '}' && --depth === 0) return i;
44
+ }
45
+ return css.length;
46
+ }
@@ -0,0 +1,341 @@
1
+ export const USAGES = ['display', 'heading', 'body', 'editorial', 'code'] as const;
2
+ export type Usage = (typeof USAGES)[number];
3
+
4
+ export const STEPS = ['2xs', 'xs', 'sm', 'md', 'lg', 'xl', '2xl'] as const;
5
+ export type Step = (typeof STEPS)[number];
6
+
7
+ export const EXPONENT: Readonly<Record<Step, number>> = {
8
+ '2xs': -3,
9
+ xs: -2,
10
+ sm: -1,
11
+ md: 0,
12
+ lg: 1,
13
+ xl: 2,
14
+ '2xl': 3,
15
+ };
16
+
17
+ export const VIEWPORTS = ['desktop', 'tablet', 'phone'] as const;
18
+ export type Viewport = (typeof VIEWPORTS)[number];
19
+ export type CompressedViewport = Exclude<Viewport, 'desktop'>;
20
+
21
+ export const MEDIA_QUERY: Readonly<Record<CompressedViewport, string>> = {
22
+ tablet: '(max-width: 768px)',
23
+ phone: '(max-width: 480px)',
24
+ };
25
+
26
+ export interface Interval {
27
+ key: string;
28
+ label: string;
29
+ /** Null for the custom entry, which takes any ratio in `CUSTOM_RATIO_RANGE`. */
30
+ ratio: number | null;
31
+ }
32
+
33
+ export const INTERVALS: readonly Interval[] = [
34
+ { key: 'minor-second', label: 'Minor second', ratio: 1.067 },
35
+ { key: 'major-second', label: 'Major second', ratio: 1.125 },
36
+ { key: 'minor-third', label: 'Minor third', ratio: 1.2 },
37
+ { key: 'major-third', label: 'Major third', ratio: 1.25 },
38
+ { key: 'perfect-fourth', label: 'Perfect fourth', ratio: 1.333 },
39
+ { key: 'augmented-fourth', label: 'Augmented fourth', ratio: 1.414 },
40
+ { key: 'perfect-fifth', label: 'Perfect fifth', ratio: 1.5 },
41
+ { key: 'golden', label: 'Golden ratio', ratio: 1.618 },
42
+ { key: 'custom', label: 'Custom', ratio: null },
43
+ ];
44
+
45
+ export const CUSTOM_RATIO_RANGE = { min: 1.01, max: 2 } as const;
46
+ export const COMPRESSION_RANGE = { min: 0.4, max: 1 } as const;
47
+
48
+ /** Pixels per rem. Bases are rem counts, and the power rule works in px. */
49
+ export const ROOT_PX = 16;
50
+
51
+ export type LineHeight = 'none' | 'tightest' | 'tighter' | 'tight' | 'normal' | 'relaxed';
52
+ export type LetterSpacing = 'tighter' | 'tight' | 'normal' | 'wide' | 'wider';
53
+
54
+ export interface UsageDefaults {
55
+ /** A token name, such as `--font-display`. */
56
+ face: string;
57
+ /** A token name, such as `--font-weight-semibold`. */
58
+ weight: string;
59
+ /** Desktop size of the md step, in rem. */
60
+ base: number;
61
+ ratio: number;
62
+ lineHeight: Readonly<Record<Step, LineHeight>>;
63
+ letterSpacing: Readonly<Record<Step, LetterSpacing>>;
64
+ /** Steps whose family ships unlinked, pinned to this font stack token. */
65
+ familyPins: Readonly<Partial<Record<Step, string>>>;
66
+ /** Elements `site.css` maps to each step. */
67
+ elements: Readonly<Partial<Record<Step, readonly string[]>>>;
68
+ }
69
+
70
+ export interface EyebrowDefaults {
71
+ /** The usage whose face and weight the eyebrow links to. */
72
+ usage: Usage;
73
+ /** The step of that usage whose sizes the eyebrow follows. */
74
+ sizeStep: Step;
75
+ lineHeight: LineHeight;
76
+ letterSpacing: LetterSpacing;
77
+ textTransform: string;
78
+ }
79
+
80
+ const allNormal: Record<Step, 'normal'> = {
81
+ '2xs': 'normal',
82
+ xs: 'normal',
83
+ sm: 'normal',
84
+ md: 'normal',
85
+ lg: 'normal',
86
+ xl: 'normal',
87
+ '2xl': 'normal',
88
+ };
89
+
90
+ /** The tokens.css migrations freeze their own copy of these values; a change here needs one. */
91
+ export const DEFAULTS: {
92
+ compression: Readonly<Record<CompressedViewport, number>>;
93
+ usages: Readonly<Record<Usage, UsageDefaults>>;
94
+ eyebrow: EyebrowDefaults;
95
+ } = {
96
+ compression: { tablet: 0.75, phone: 0.63 },
97
+ usages: {
98
+ display: {
99
+ face: '--font-display',
100
+ weight: '--font-weight-semibold',
101
+ base: 3,
102
+ ratio: 1.2,
103
+ lineHeight: {
104
+ '2xs': 'tighter',
105
+ xs: 'tighter',
106
+ sm: 'tighter',
107
+ md: 'tightest',
108
+ lg: 'tightest',
109
+ xl: 'tightest',
110
+ '2xl': 'tightest',
111
+ },
112
+ letterSpacing: { ...allNormal, lg: 'tight', xl: 'tight', '2xl': 'tight' },
113
+ familyPins: {},
114
+ elements: {},
115
+ },
116
+ heading: {
117
+ face: '--font-display',
118
+ weight: '--font-weight-semibold',
119
+ base: 1.5,
120
+ ratio: 1.25,
121
+ lineHeight: {
122
+ '2xs': 'tight',
123
+ xs: 'tight',
124
+ sm: 'tighter',
125
+ md: 'tighter',
126
+ lg: 'tightest',
127
+ xl: 'tightest',
128
+ '2xl': 'tightest',
129
+ },
130
+ letterSpacing: allNormal,
131
+ familyPins: { '2xs': '--font-sans', xs: '--font-sans', sm: '--font-sans' },
132
+ elements: { '2xs': ['h6'], xs: ['h5'], sm: ['h4'], md: ['h3'], lg: ['h2'], xl: ['h1'] },
133
+ },
134
+ body: {
135
+ face: '--font-sans',
136
+ weight: '--font-weight-normal',
137
+ base: 1,
138
+ ratio: 1.125,
139
+ lineHeight: {
140
+ '2xs': 'tight',
141
+ xs: 'tight',
142
+ sm: 'tight',
143
+ md: 'normal',
144
+ lg: 'tight',
145
+ xl: 'tighter',
146
+ '2xl': 'tighter',
147
+ },
148
+ letterSpacing: allNormal,
149
+ familyPins: {},
150
+ elements: { sm: ['small'], md: ['p', 'li'] },
151
+ },
152
+ editorial: {
153
+ face: '--font-editorial',
154
+ weight: '--font-weight-normal',
155
+ base: 1,
156
+ ratio: 1.125,
157
+ lineHeight: {
158
+ '2xs': 'tighter',
159
+ xs: 'tighter',
160
+ sm: 'tight',
161
+ md: 'normal',
162
+ lg: 'tight',
163
+ xl: 'tighter',
164
+ '2xl': 'tightest',
165
+ },
166
+ letterSpacing: allNormal,
167
+ familyPins: {},
168
+ elements: {},
169
+ },
170
+ code: {
171
+ face: '--font-mono',
172
+ weight: '--font-weight-normal',
173
+ base: 0.875,
174
+ ratio: 1.125,
175
+ lineHeight: {
176
+ '2xs': 'normal',
177
+ xs: 'normal',
178
+ sm: 'normal',
179
+ md: 'normal',
180
+ lg: 'normal',
181
+ xl: 'normal',
182
+ '2xl': 'normal',
183
+ },
184
+ letterSpacing: allNormal,
185
+ familyPins: {},
186
+ elements: { md: ['pre'] },
187
+ },
188
+ },
189
+ eyebrow: {
190
+ usage: 'body',
191
+ sizeStep: 'sm',
192
+ lineHeight: 'normal',
193
+ letterSpacing: 'normal',
194
+ textTransform: 'none',
195
+ },
196
+ };
197
+
198
+ export const EYEBROW_PREFIX = '--eyebrow';
199
+ export type StylePrefix = `--${Usage}-${Step}` | typeof EYEBROW_PREFIX;
200
+ export type UsageSetting = 'font-family' | 'font-weight' | 'scale-base' | 'scale-ratio';
201
+ export type StyleAxis = 'font-family' | 'font-weight' | 'line-height' | 'letter-spacing' | 'text-transform';
202
+
203
+ export const compressionName = (viewport: CompressedViewport): string =>
204
+ `--type-${viewport}-scale-compression`;
205
+ export const usageName = (usage: Usage, setting: UsageSetting): string => `--${usage}-${setting}`;
206
+ export const stepPrefix = (usage: Usage, step: Step): StylePrefix => `--${usage}-${step}`;
207
+ export const axisName = (prefix: StylePrefix, axis: StyleAxis): string => `${prefix}-${axis}`;
208
+ export const sizeName = (prefix: StylePrefix, viewport: Viewport): string =>
209
+ `${prefix}-${viewport}-font-size`;
210
+ /**
211
+ * The structural size. `tokens.css` points it at the desktop size and re-points
212
+ * it at each breakpoint, so the editor never writes it and no theme holds it.
213
+ */
214
+ export const fontSizeName = (prefix: StylePrefix): string => `${prefix}-font-size`;
215
+
216
+ /** Steps in `tokens.css` order, largest first. */
217
+ const STEPS_DOWN: readonly Step[] = [...STEPS].reverse();
218
+
219
+ /** Every text style, in `tokens.css` order: each usage's steps, then the eyebrow. */
220
+ export const STYLE_PREFIXES: readonly StylePrefix[] = [
221
+ ...USAGES.flatMap((usage) => STEPS_DOWN.map((step) => stepPrefix(usage, step))),
222
+ EYEBROW_PREFIX,
223
+ ];
224
+
225
+ export function scalePx(base: number, ratio: number, step: Step): number {
226
+ return base * ROOT_PX * ratio ** EXPONENT[step];
227
+ }
228
+
229
+ export function recommend(desktopPx: number, compression: number): number {
230
+ return Math.min(desktopPx, ROOT_PX * (desktopPx / ROOT_PX) ** compression);
231
+ }
232
+
233
+ /** A number as CSS writes it: at most four decimals, no trailing zeros. */
234
+ export function formatNumber(n: number): string {
235
+ return String(Number(n.toFixed(4)));
236
+ }
237
+
238
+ export function formatRem(rem: number): string {
239
+ return `${formatNumber(rem)}rem`;
240
+ }
241
+
242
+ /** The step's desktop size as a unitless rem count over the usage's scale settings. */
243
+ export function scaleTerm(usage: Usage, step: Step): string {
244
+ const base = `var(${usageName(usage, 'scale-base')})`;
245
+ const n = EXPONENT[step];
246
+ return n === 0 ? base : `${base} * pow(var(${usageName(usage, 'scale-ratio')}), ${n})`;
247
+ }
248
+
249
+ export function desktopScaleExpression(usage: Usage, step: Step): string {
250
+ return `calc(${scaleTerm(usage, step)} * 1rem)`;
251
+ }
252
+
253
+ function recommendationExpression(prefix: StylePrefix, remTerm: string, viewport: CompressedViewport): string {
254
+ return `min(var(${sizeName(prefix, 'desktop')}), calc(pow(${remTerm}, var(${compressionName(viewport)})) * 1rem))`;
255
+ }
256
+
257
+ /** The recommendation while the desktop size follows the scale. */
258
+ export function recommendedScaleExpression(usage: Usage, step: Step, viewport: CompressedViewport): string {
259
+ return recommendationExpression(stepPrefix(usage, step), scaleTerm(usage, step), viewport);
260
+ }
261
+
262
+ /** The recommendation once the desktop size holds an edit of `desktopRem`. */
263
+ export function recommendedEditedExpression(
264
+ prefix: StylePrefix,
265
+ desktopRem: number,
266
+ viewport: CompressedViewport,
267
+ ): string {
268
+ return recommendationExpression(prefix, formatNumber(desktopRem), viewport);
269
+ }
270
+
271
+ export interface TokenDeclaration {
272
+ name: string;
273
+ value: string;
274
+ }
275
+
276
+ const ref = (name: string): string => `var(${name})`;
277
+ const COMPRESSED: readonly CompressedViewport[] = ['tablet', 'phone'];
278
+
279
+ /** The text-style block of `tokens.css`, one entry per declaration, in file order. */
280
+ export function typeScaleDeclarations(): TokenDeclaration[] {
281
+ const out: TokenDeclaration[] = COMPRESSED.map((vp) => ({
282
+ name: compressionName(vp),
283
+ value: formatNumber(DEFAULTS.compression[vp]),
284
+ }));
285
+
286
+ for (const usage of USAGES) {
287
+ const d = DEFAULTS.usages[usage];
288
+ out.push(
289
+ { name: usageName(usage, 'font-family'), value: ref(d.face) },
290
+ { name: usageName(usage, 'font-weight'), value: ref(d.weight) },
291
+ { name: usageName(usage, 'scale-base'), value: formatNumber(d.base) },
292
+ { name: usageName(usage, 'scale-ratio'), value: formatNumber(d.ratio) },
293
+ );
294
+ for (const step of STEPS_DOWN) {
295
+ const prefix = stepPrefix(usage, step);
296
+ const pin = d.familyPins[step];
297
+ out.push(
298
+ { name: axisName(prefix, 'font-family'), value: ref(pin ?? usageName(usage, 'font-family')) },
299
+ { name: sizeName(prefix, 'desktop'), value: desktopScaleExpression(usage, step) },
300
+ ...COMPRESSED.map((vp) => ({
301
+ name: sizeName(prefix, vp),
302
+ value: recommendedScaleExpression(usage, step, vp),
303
+ })),
304
+ { name: fontSizeName(prefix), value: ref(sizeName(prefix, 'desktop')) },
305
+ { name: axisName(prefix, 'font-weight'), value: ref(usageName(usage, 'font-weight')) },
306
+ { name: axisName(prefix, 'line-height'), value: ref(`--line-height-${d.lineHeight[step]}`) },
307
+ { name: axisName(prefix, 'letter-spacing'), value: ref(`--letter-spacing-${d.letterSpacing[step]}`) },
308
+ );
309
+ }
310
+ }
311
+
312
+ const e = DEFAULTS.eyebrow;
313
+ const follows = stepPrefix(e.usage, e.sizeStep);
314
+ out.push(
315
+ { name: axisName(EYEBROW_PREFIX, 'font-family'), value: ref(usageName(e.usage, 'font-family')) },
316
+ ...VIEWPORTS.map((vp) => ({ name: sizeName(EYEBROW_PREFIX, vp), value: ref(sizeName(follows, vp)) })),
317
+ { name: fontSizeName(EYEBROW_PREFIX), value: ref(sizeName(EYEBROW_PREFIX, 'desktop')) },
318
+ { name: axisName(EYEBROW_PREFIX, 'font-weight'), value: ref(usageName(e.usage, 'font-weight')) },
319
+ { name: axisName(EYEBROW_PREFIX, 'line-height'), value: ref(`--line-height-${e.lineHeight}`) },
320
+ { name: axisName(EYEBROW_PREFIX, 'letter-spacing'), value: ref(`--letter-spacing-${e.letterSpacing}`) },
321
+ { name: axisName(EYEBROW_PREFIX, 'text-transform'), value: e.textTransform },
322
+ );
323
+ return out;
324
+ }
325
+
326
+ /** The breakpoint re-points of the structural sizes, in file order. */
327
+ export function typeScaleMediaDeclarations(viewport: CompressedViewport): TokenDeclaration[] {
328
+ return STYLE_PREFIXES.map((prefix) => ({
329
+ name: fontSizeName(prefix),
330
+ value: ref(sizeName(prefix, viewport)),
331
+ }));
332
+ }
333
+
334
+ export const STRUCTURAL_NAMES: ReadonlySet<string> = new Set(STYLE_PREFIXES.map(fontSizeName));
335
+
336
+ /** Every name the editor may write. */
337
+ export const EDITABLE_NAMES: ReadonlySet<string> = new Set(
338
+ typeScaleDeclarations()
339
+ .map((d) => d.name)
340
+ .filter((name) => !STRUCTURAL_NAMES.has(name)),
341
+ );
@@ -174,7 +174,7 @@
174
174
  text-align: right;
175
175
  font-family: var(--code-font-family);
176
176
  font-size: var(--font-size-md);
177
- line-height: var(--code-line-height);
177
+ line-height: var(--code-md-line-height);
178
178
  font-variant-numeric: tabular-nums;
179
179
  color: var(--text-muted);
180
180
  cursor: pointer;
@@ -194,8 +194,8 @@
194
194
  font-family: var(--code-font-family);
195
195
  font-size: var(--font-size-md);
196
196
  font-weight: var(--code-font-weight);
197
- line-height: var(--code-line-height);
198
- letter-spacing: var(--code-letter-spacing);
197
+ line-height: var(--code-md-line-height);
198
+ letter-spacing: var(--code-md-letter-spacing);
199
199
  color: var(--text-secondary);
200
200
  }
201
201