@motion-proto/live-tokens 0.40.0 → 0.41.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 (89) hide show
  1. package/CHANGELOG.md +115 -0
  2. package/dist-plugin/{chunk-D77VD4Z6.js → chunk-H4TRUINI.js} +102 -13
  3. package/dist-plugin/index.cjs +288 -67
  4. package/dist-plugin/index.js +171 -55
  5. package/dist-plugin/tokensCssMigrations/index.cjs +102 -13
  6. package/dist-plugin/tokensCssMigrations/index.js +1 -1
  7. package/package.json +3 -2
  8. package/src/app/site.css +97 -20
  9. package/src/editor/core/palettes/colorHarmony.ts +228 -0
  10. package/src/editor/core/palettes/contrast.ts +128 -0
  11. package/src/editor/core/palettes/oklch.ts +8 -0
  12. package/src/editor/core/palettes/paletteDerivation.ts +267 -72
  13. package/src/editor/core/palettes/recommendText.ts +216 -0
  14. package/src/editor/core/palettes/solveTextContrast.ts +251 -0
  15. package/src/editor/core/store/editorCore.ts +2 -1
  16. package/src/editor/core/store/editorPersistence.ts +18 -1
  17. package/src/editor/core/store/editorStore.ts +20 -1
  18. package/src/editor/core/store/editorTypes.ts +4 -0
  19. package/src/editor/core/store/editorViewStore.ts +7 -3
  20. package/src/editor/core/themes/migrations/2026-06-05-palette-unification.ts +11 -9
  21. package/src/editor/core/themes/migrations/2026-07-21-palette-oklch-basis.ts +51 -0
  22. package/src/editor/core/themes/migrations/2026-07-24-base-anchor-placement.ts +37 -0
  23. package/src/editor/core/themes/migrations/2026-07-25-background-spot-to-base.ts +40 -0
  24. package/src/editor/core/themes/themeTypes.ts +23 -3
  25. package/src/editor/docs/CodeBlock.svelte +1 -1
  26. package/src/editor/docs/Docs.svelte +30 -13
  27. package/src/editor/pages/EditorShell.svelte +31 -3
  28. package/src/editor/ui/ColorEditPanel.svelte +22 -16
  29. package/src/editor/ui/EditorViewSwitcher.svelte +43 -9
  30. package/src/editor/ui/FontStackEditor.svelte +0 -1
  31. package/src/editor/ui/PaletteEditor.svelte +117 -155
  32. package/src/editor/ui/ProjectFontsSection.svelte +0 -3
  33. package/src/editor/ui/TextStylesSection.svelte +173 -0
  34. package/src/editor/ui/UILineHeightSelector.svelte +6 -5
  35. package/src/editor/ui/UIPaletteSelector.svelte +6 -1
  36. package/src/editor/ui/UITextTransformSelector.svelte +106 -0
  37. package/src/editor/ui/VariablesTab.svelte +23 -10
  38. package/src/editor/ui/colors/ColorReadouts.svelte +123 -0
  39. package/src/editor/ui/colors/ColorStory.svelte +375 -0
  40. package/src/editor/ui/colors/ColorWheel.svelte +740 -0
  41. package/src/editor/ui/colors/ColorsTab.svelte +508 -0
  42. package/src/editor/ui/colors/HarmonyAxesList.svelte +320 -0
  43. package/src/editor/ui/colors/LightnessBar.svelte +261 -0
  44. package/src/editor/ui/colors/PaletteStepStrip.svelte +142 -0
  45. package/src/editor/ui/colors/colorWheelMath.ts +9 -0
  46. package/src/editor/ui/colors/harmony-icons/analogous.svg +13 -0
  47. package/src/editor/ui/colors/harmony-icons/complementary.svg +10 -0
  48. package/src/editor/ui/colors/harmony-icons/compound.svg +15 -0
  49. package/src/editor/ui/colors/harmony-icons/custom.svg +10 -0
  50. package/src/editor/ui/colors/harmony-icons/monochromatic.svg +11 -0
  51. package/src/editor/ui/colors/harmony-icons/shades.svg +9 -0
  52. package/src/editor/ui/colors/harmony-icons/split-complementary.svg +13 -0
  53. package/src/editor/ui/colors/harmony-icons/square.svg +13 -0
  54. package/src/editor/ui/colors/harmony-icons/tetradic.svg +15 -0
  55. package/src/editor/ui/colors/harmony-icons/triadic.svg +13 -0
  56. package/src/editor/ui/colors/harmonyModeIcons.ts +23 -0
  57. package/src/editor/ui/colors/paletteBaseColor.ts +214 -0
  58. package/src/editor/ui/index.ts +0 -1
  59. package/src/editor/ui/palette/OverridesPanel.svelte +6 -3
  60. package/src/editor/ui/palette/PaletteBase.svelte +16 -8
  61. package/src/editor/ui/palette/dockMagnify.ts +22 -0
  62. package/src/editor/ui/palette/paletteEditorState.ts +5 -4
  63. package/src/editor/ui/palette/paletteMath.ts +93 -181
  64. package/src/editor/ui/sections/textStyles.ts +82 -0
  65. package/src/editor/ui/sections/tokenScales.ts +3 -3
  66. package/src/live-tokens/data/themes/default.json +3 -3
  67. package/src/live-tokens/data/tokens.generated.css +96 -95
  68. package/src/system/components/Badge.svelte +11 -11
  69. package/src/system/components/Button.svelte +7 -7
  70. package/src/system/components/Callout.svelte +8 -8
  71. package/src/system/components/Card.svelte +2 -2
  72. package/src/system/components/CodeSnippet.svelte +1 -1
  73. package/src/system/components/CollapsibleSection.svelte +7 -7
  74. package/src/system/components/CornerBadge.svelte +1 -1
  75. package/src/system/components/Dialog.svelte +2 -2
  76. package/src/system/components/FloatingTokenTags.css +6 -5
  77. package/src/system/components/Input.svelte +6 -6
  78. package/src/system/components/MenuSelect.svelte +4 -4
  79. package/src/system/components/Notification.svelte +8 -8
  80. package/src/system/components/ProgressBar.svelte +2 -2
  81. package/src/system/components/RadioButton.svelte +4 -4
  82. package/src/system/components/SectionDivider.svelte +6 -6
  83. package/src/system/components/SegmentedControl.svelte +5 -5
  84. package/src/system/components/SideNavigation.svelte +12 -12
  85. package/src/system/components/TabBar.svelte +4 -4
  86. package/src/system/components/Table.svelte +2 -2
  87. package/src/system/components/Tooltip.svelte +1 -1
  88. package/src/system/styles/tokens.css +59 -6
  89. package/src/editor/ui/TextTab.svelte +0 -203
@@ -0,0 +1,375 @@
1
+ <script lang="ts">
2
+ import { editorState } from '../../core/store/editorStore';
3
+ import { oklchToHexClamped } from '../../core/palettes/oklch';
4
+ import { palettesToVars, PALETTE_SPECS, type PaletteSpec } from '../../core/palettes/paletteDerivation';
5
+ import { defaultPaletteConfig } from '../palette/paletteMath';
6
+ import { contrastRatio } from '../../core/palettes/contrast';
7
+ import { recommendNeutralText } from '../../core/palettes/recommendText';
8
+ import { applySuggestedNeutralText } from './paletteBaseColor';
9
+ import UIPillButton from '../UIPillButton.svelte';
10
+ import type { PaletteConfig } from '../../core/themes/themeTypes';
11
+
12
+ // 60-30-10 as an area composition: the background surface is the dominant 60%
13
+ // field (the page), and Brand/Accent/Special are the tones shown against it.
14
+ // Shares are flex-grow weights, not exact percentages.
15
+ const TONES = [
16
+ { label: 'Brand', ns: 'brand', share: 30 },
17
+ { label: 'Accent', ns: 'accent', share: 8 },
18
+ { label: 'Special', ns: 'special', share: 2 },
19
+ ] as const;
20
+
21
+ const FUNCTIONAL = [
22
+ { label: 'Info', ns: 'info' },
23
+ { label: 'Success', ns: 'success' },
24
+ { label: 'Warning', ns: 'warning' },
25
+ { label: 'Danger', ns: 'danger' },
26
+ ] as const;
27
+
28
+ // The story is a seed-composition preview: every FILL is a family's base
29
+ // color — exactly what the wheel dot and swatch show — so it tracks edits
30
+ // 1:1. Derived --surface-* tokens deliberately do not appear here: their
31
+ // lightness is curve-owned, which decouples the story from the wheel and
32
+ // reads as broken.
33
+ const SPEC_BY_LABEL: Record<string, PaletteSpec> = Object.fromEntries(PALETTE_SPECS.map((s) => [s.label, s]));
34
+
35
+ function seedHex(label: string): string {
36
+ const { l, c, h } = $editorState.palettes[label]?.baseColor ?? SPEC_BY_LABEL[label].initialColor;
37
+ return oklchToHexClamped(l, c, h);
38
+ }
39
+
40
+ // Text still renders from this store-derived map (text derivation tracks the
41
+ // seed), never from the ambient :root variables — those stay at vendored
42
+ // defaults for families a loaded theme doesn't configure, so a story bound
43
+ // to them silently goes static.
44
+ let full = $derived.by(() => {
45
+ const map: Record<string, PaletteConfig> = {};
46
+ for (const spec of PALETTE_SPECS) {
47
+ map[spec.label] =
48
+ $editorState.palettes[spec.label] ??
49
+ defaultPaletteConfig({ baseColor: spec.initialColor, neutral: spec.neutral ?? false });
50
+ }
51
+ return map;
52
+ });
53
+
54
+ let vars = $derived(palettesToVars(full));
55
+ let useBlackWhite = $state(false);
56
+ let rec = $derived(recommendNeutralText(full, undefined, useBlackWhite));
57
+ let partialCoverage = $derived(rec.suggestions.filter((s) => !s.current.coverage.full));
58
+
59
+ // Shadow the consumed text variables locally with the store-derived values;
60
+ // the page field is the Background family's SEED, not its derived page step.
61
+ let storyVars = $derived(
62
+ ['--text-primary', '--text-secondary', '--text-tertiary', '--text-inverted']
63
+ .concat([...TONES, ...FUNCTIONAL].map((t) => `--text-${t.ns}`))
64
+ .filter((name) => vars[name] !== undefined)
65
+ .map((name) => `${name}: ${vars[name]}`)
66
+ .concat(`--page-bg: ${seedHex('Background')}`)
67
+ .join('; '),
68
+ );
69
+
70
+ // Ratios are measured against the DISPLAYED field — the Background seed —
71
+ // so the numbers always describe what the story shows.
72
+ function ratioOnBg(textVar: string): number | null {
73
+ const text = vars[textVar];
74
+ return typeof text === 'string' ? contrastRatio(text, seedHex('Background')) : null;
75
+ }
76
+
77
+ // Floor, don't round: the readout must never overstate a contrast ratio.
78
+ function fmt(r: number | null): string | null {
79
+ return r === null ? null : `${(Math.floor(r * 10) / 10).toFixed(1)}:1`;
80
+ }
81
+
82
+ let bgRatio = $derived(ratioOnBg('--text-primary'));
83
+
84
+ // Inverted is measured against the fill it sits on (primary), not the page:
85
+ // the pill below IS that pairing, so the number describes what is shown. A
86
+ // mid-lightness primary makes this pairing inherently low-contrast — the
87
+ // readout surfaces that rather than hiding the pill.
88
+ let invertedRatio = $derived.by(() => {
89
+ const inverted = vars['--text-inverted'];
90
+ const primary = vars['--text-primary'];
91
+ return typeof inverted === 'string' && typeof primary === 'string'
92
+ ? contrastRatio(inverted, primary)
93
+ : null;
94
+ });
95
+ </script>
96
+
97
+ <div class="story" style={storyVars}>
98
+ <div class="field">
99
+ <div class="field-head">
100
+ <span class="field-label">Background</span>
101
+ {#if fmt(bgRatio)}<span class="ratio">{fmt(bgRatio)}</span>{/if}
102
+ </div>
103
+ <p class="body">The dominant surface. Body text sits here.</p>
104
+ <p class="secondary">
105
+ Secondary text follows the same field.
106
+ {#if fmt(ratioOnBg('--text-secondary'))}<span class="ratio">{fmt(ratioOnBg('--text-secondary'))}</span>{/if}
107
+ </p>
108
+ <p class="tertiary">
109
+ Tertiary text steps back once more.
110
+ {#if fmt(ratioOnBg('--text-tertiary'))}<span class="ratio">{fmt(ratioOnBg('--text-tertiary'))}</span>{/if}
111
+ </p>
112
+ <p class="inverted-row">
113
+ <span class="inverted-pill">
114
+ Inverted text flips onto primary.
115
+ {#if fmt(invertedRatio)}<span class="ratio">{fmt(invertedRatio)}</span>{/if}
116
+ </span>
117
+ </p>
118
+
119
+ <!-- Always rendered: toggling "Use black and white" changes the
120
+ suggestion, so the toggle must stay reachable even when the current
121
+ values already match. -->
122
+ <div class="suggest-row">
123
+ <span class="suggest-label">Suggested</span>
124
+ {#each rec.suggestions as s (s.step)}
125
+ <span class="suggest-entry" class:dim={!s.differs}>
126
+ <span class="fn-dot" style:--dot={s.suggested.hex}></span>
127
+ <span class="suggest-step">{s.step}</span>
128
+ <span class="ratio">{fmt(contrastRatio(s.suggested.hex, seedHex('Background')))}</span>
129
+ </span>
130
+ {/each}
131
+ <label class="bw-toggle" title="Anchor the suggestion at pure white or black instead of the softened extreme">
132
+ <input type="checkbox" bind:checked={useBlackWhite} />
133
+ <span>Use black and white</span>
134
+ </label>
135
+ {#if rec.anyDiffers}
136
+ <UIPillButton
137
+ icon="fa-arrow-down-short-wide"
138
+ title="Anchor on primary and step secondary/tertiary down by even lightness drops, floored at WCAG AA (one undo)"
139
+ onclick={() => applySuggestedNeutralText(useBlackWhite)}
140
+ >
141
+ Use suggested
142
+ </UIPillButton>
143
+ {/if}
144
+ </div>
145
+
146
+ {#if partialCoverage.length > 0}
147
+ <div
148
+ class="coverage-alert"
149
+ title="WCAG AA (4.5:1) coverage across the neutral surface steps. Informational, nothing is blocked."
150
+ >
151
+ <i class="fa-solid fa-triangle-exclamation"></i>
152
+ <span>
153
+ AA reach: {partialCoverage
154
+ .map((s) => `${s.step} ${s.current.coverage.cleared}/${s.current.coverage.total} surfaces`)
155
+ .join(' · ')}
156
+ </span>
157
+ </div>
158
+ {/if}
159
+
160
+ <div class="functional-row">
161
+ {#each FUNCTIONAL as fn (fn.ns)}
162
+ {@const r = fmt(ratioOnBg(`--text-${fn.ns}`))}
163
+ <span class="fn-chip">
164
+ <span class="fn-dot" style:--dot={seedHex(fn.label)}></span>
165
+ <span class="fn-label" style:color="var(--text-{fn.ns})">{fn.label}</span>
166
+ {#if r}<span class="ratio">{r}</span>{/if}
167
+ </span>
168
+ {/each}
169
+ </div>
170
+ </div>
171
+
172
+ <div class="tones">
173
+ {#each TONES as tone (tone.ns)}
174
+ {@const r = fmt(ratioOnBg(`--text-${tone.ns}`))}
175
+ <div class="tone" style:--tone-fill={seedHex(tone.label)} style:--tone-share={tone.share}>
176
+ <span class="tone-pill" style:color="var(--text-{tone.ns})">
177
+ <span class="tone-name">{tone.label}</span>
178
+ {#if r}<span class="ratio">{r}</span>{/if}
179
+ </span>
180
+ </div>
181
+ {/each}
182
+ </div>
183
+ </div>
184
+
185
+ <style>
186
+ /* The story frame IS the 60% background surface; the tones are the accents
187
+ shown against it. The neutral field shows through as the dominant page. */
188
+ .story {
189
+ display: flex;
190
+ flex-direction: column;
191
+ gap: var(--ui-space-8);
192
+ min-height: 30rem;
193
+ padding: var(--ui-space-12);
194
+ border-radius: var(--ui-radius-md);
195
+ border: 1px solid var(--ui-border-low);
196
+ background: var(--page-bg);
197
+ }
198
+
199
+ /* Transparent: the background surface reads directly, no card over it. */
200
+ .field {
201
+ flex-grow: 60;
202
+ flex-basis: 0;
203
+ display: flex;
204
+ flex-direction: column;
205
+ gap: var(--ui-space-4);
206
+ min-height: 0;
207
+ padding: var(--ui-space-8) var(--ui-space-12);
208
+ color: var(--text-primary);
209
+ }
210
+
211
+ .field-head {
212
+ display: flex;
213
+ align-items: baseline;
214
+ justify-content: space-between;
215
+ gap: var(--ui-space-8);
216
+ }
217
+
218
+ .field-label {
219
+ font-size: var(--ui-font-size-sm);
220
+ font-weight: var(--ui-font-weight-semibold);
221
+ }
222
+
223
+ .body {
224
+ margin: 0;
225
+ font-size: var(--ui-font-size-sm);
226
+ color: var(--text-primary);
227
+ }
228
+
229
+ .secondary {
230
+ margin: 0;
231
+ font-size: var(--ui-font-size-xs);
232
+ color: var(--text-secondary);
233
+ }
234
+
235
+ .tertiary {
236
+ margin: 0;
237
+ font-size: var(--ui-font-size-xs);
238
+ color: var(--text-tertiary);
239
+ }
240
+
241
+ .inverted-row {
242
+ margin: 0;
243
+ }
244
+
245
+ /* The pill's fill IS the primary text color: the flip is shown literally,
246
+ and any illegibility of the pairing is visible right here. */
247
+ .inverted-pill {
248
+ display: inline-flex;
249
+ align-items: baseline;
250
+ gap: var(--ui-space-8);
251
+ padding: var(--ui-space-2) var(--ui-space-8);
252
+ border-radius: var(--ui-radius-full);
253
+ background: var(--text-primary);
254
+ color: var(--text-inverted);
255
+ font-size: var(--ui-font-size-xs);
256
+ }
257
+
258
+ .suggest-row {
259
+ display: flex;
260
+ align-items: center;
261
+ flex-wrap: wrap;
262
+ gap: var(--ui-space-8);
263
+ }
264
+
265
+ .suggest-label {
266
+ font-size: var(--ui-font-size-sm);
267
+ font-weight: var(--ui-font-weight-semibold);
268
+ opacity: 0.6;
269
+ }
270
+
271
+ .suggest-entry {
272
+ display: flex;
273
+ align-items: center;
274
+ gap: var(--ui-space-6);
275
+ }
276
+
277
+ .suggest-entry.dim {
278
+ opacity: 0.45;
279
+ }
280
+
281
+ .suggest-step {
282
+ font-size: var(--ui-font-size-xs);
283
+ }
284
+
285
+ .bw-toggle {
286
+ display: flex;
287
+ align-items: center;
288
+ gap: var(--ui-space-4);
289
+ font-size: var(--ui-font-size-xs);
290
+ cursor: pointer;
291
+ }
292
+
293
+ .bw-toggle input {
294
+ margin: 0;
295
+ cursor: pointer;
296
+ }
297
+
298
+ .coverage-alert {
299
+ display: flex;
300
+ align-items: center;
301
+ gap: var(--ui-space-6);
302
+ font-size: var(--ui-font-size-xs);
303
+ opacity: 0.75;
304
+ }
305
+
306
+ .functional-row {
307
+ display: flex;
308
+ gap: var(--ui-space-8);
309
+ flex-wrap: wrap;
310
+ margin-top: auto;
311
+ padding-top: var(--ui-space-12);
312
+ }
313
+
314
+ .fn-chip {
315
+ display: flex;
316
+ align-items: center;
317
+ gap: var(--ui-space-6);
318
+ }
319
+
320
+ .fn-dot {
321
+ width: 0.75rem;
322
+ height: 0.75rem;
323
+ border-radius: var(--ui-radius-full);
324
+ background: var(--dot);
325
+ border: 1px solid var(--page-bg);
326
+ }
327
+
328
+ .fn-label {
329
+ font-size: var(--ui-font-size-xs);
330
+ font-weight: var(--ui-font-weight-semibold);
331
+ }
332
+
333
+ .tones {
334
+ display: flex;
335
+ flex-direction: column;
336
+ gap: var(--ui-space-8);
337
+ flex-grow: 40;
338
+ flex-basis: 0;
339
+ min-height: 0;
340
+ }
341
+
342
+ .tone {
343
+ flex-grow: var(--tone-share);
344
+ flex-basis: 0;
345
+ display: flex;
346
+ align-items: center;
347
+ min-height: 1.5rem;
348
+ padding: var(--ui-space-8) var(--ui-space-12);
349
+ border-radius: var(--ui-radius-sm);
350
+ background: var(--tone-fill);
351
+ }
352
+
353
+ /* A pill of the background surface sitting on the tone: its label renders the
354
+ family text on the background, so what you see is exactly the reported
355
+ ratio. */
356
+ .tone-pill {
357
+ display: inline-flex;
358
+ align-items: baseline;
359
+ gap: var(--ui-space-8);
360
+ padding: var(--ui-space-2) var(--ui-space-8);
361
+ border-radius: var(--ui-radius-full);
362
+ background: var(--page-bg);
363
+ }
364
+
365
+ .tone-name {
366
+ font-size: var(--ui-font-size-xs);
367
+ font-weight: var(--ui-font-weight-semibold);
368
+ }
369
+
370
+ .ratio {
371
+ font-size: var(--ui-font-size-xs);
372
+ font-family: var(--ui-font-mono);
373
+ opacity: 0.75;
374
+ }
375
+ </style>