@motion-proto/live-tokens 0.53.0 → 0.54.1

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.
@@ -11,7 +11,7 @@ You translate the request into a small ops file; the CLI resolves each matching
11
11
 
12
12
  1. Write the ops file to a temp path (not the project tree), e.g. `/tmp/adjust-ops.json`.
13
13
  2. Run `npx live-tokens adjust /tmp/adjust-ops.json`. It writes `component-configs/<id>/_working.json` for every component the ops change, which is the buffer the page already runs. `--dry-run` prints the report without writing.
14
- 3. Read the report card: every changed alias old → new, plus skips (raw value, off the ladder, already at the ladder end, pill preserved). Exit 1 means the run was rejected; the message names the offending op or the missing input, so fix it and re-run.
14
+ 3. Read the report card: every changed alias old → new, plus skips (raw value, off the ladder, already at the ladder end, pill preserved). Exit 1 means the run was rejected; the message names the offending op or the missing input, so fix it and re-run. Read where the controls landed, not only that the run succeeded: a button, badge, input, or tab padding sitting at `--space-6` is on its floor, and one that also carries `--radius-full` wants a targeted lift.
15
15
  4. Tell the user to reload the app and look. Offer the inverse op as the undo, and say the edit is unsaved until they save the open theme.
16
16
 
17
17
  Each run reads the LIVE config (buffer, else the open theme, else the shipped default), so "a bit more" and "back one" compound naturally.
@@ -40,7 +40,7 @@ Targeted, absolute:
40
40
 
41
41
  | Request | Ops |
42
42
  |---|---|
43
- | pill, capsule | radius `set: "--radius-full"` |
43
+ | pill, capsule | radius `set: "--radius-full"`, plus the padding the pill needs (see below) |
44
44
  | sharp, square corners | radius `set: "--radius-none"`, or `--radius-sm` for "mostly sharp" |
45
45
  | rounded (a named component) | radius `shift: 2` |
46
46
  | softer, rounder (global) | radius `shift: 1` to `2`, no `full` |
@@ -52,12 +52,30 @@ Targeted, absolute:
52
52
 
53
53
  Magnitude words: "slightly" or "a bit" is 1 step, unqualified is 1 to 2, "much", "way", or "really" is 2 to 3. Mood words often mean both axes: "softer" is rounder plus airier, "compact" is tighter padding plus smaller gaps.
54
54
 
55
+ ## Controls squeeze before containers
56
+
57
+ A global op spends the same number of steps everywhere, but a step costs a control far more than a container. `padding shift: -2` takes a card from a 16px inset to 10px and it is still a card. It takes a button from 8 to 4, doubled to 8px at each end, around an 18px line. The button stops reading as a button.
58
+
59
+ So a global compaction is `shift: -1`. When the brief wants more, spend the extra steps on the containers by name (`card`, `dialog`, `panel`, `sidenavigation`, `table`, `codesnippet`) and leave the controls alone. Loosening is not symmetric: airier is safe globally, because nothing breaks by growing.
60
+
61
+ A pill needs the room most. `--radius-full` bends the corner in over the first and last glyph, so a capsule wants more horizontal inset than a square-cornered control, never less. `--space-8` is the floor for a large-text pill, which is where compact Midnight Study sits; the roomier pill presets (Ocean, Sunset, Royal Velvet) run `--space-10` to `--space-12`. Pair the radius op with a padding `set` on the same target, placed after any global compaction so it wins outright:
62
+
63
+ ```json
64
+ { "ops": [
65
+ { "kind": "padding", "shift": -1 },
66
+ { "target": "button", "kind": "radius", "set": "--radius-full" },
67
+ { "target": "button", "kind": "padding", "set": "--space-10" }
68
+ ] }
69
+ ```
70
+
55
71
  ## Ladders
56
72
 
57
73
  Radius runs `none, sm, md, lg, xl, 2xl, 3xl, 4xl`, with `full` as the gated ninth rung. Space (padding and gap) is the editor picker's subset: `0, 2, 4, 6, 8, 10, 12, 16, 20, 24, 32, 48`, so every written value stays re-editable by hand. Border width is the full `--border-width-*` scale. `set` values must be on the ladder (`--space-64` is rejected).
58
74
 
59
75
  Content insets stop at `--space-4`. Below it the text sits against its own edge, so `--space-0` and `--space-2` are destinations a person picks on purpose, not ones a relative "tighter" hands you. Both stay available through the editor picker and through `set`. An alias already below the floor still moves up, and a shift that would push one under `--space-4` reports as clamped and writes nothing.
60
76
 
77
+ Padding that wraps a line of type stops a rung higher, at `--space-6`. The engine spots it in the config itself: a variant that also declares a `-text-font-size` is holding text, and the components that hold text double their padding horizontally, so `--space-4` there is 4px over an 18px line and 8px at each end. No shipped default puts text below `--space-6`.
78
+
61
79
  The floor guards `-padding` only. Outer space is exempt, because a 2px gap between an icon and its label, or a 2px margin under a bar, is ordinary design rather than a mistake. Note that `-margin` rides the `padding` kind, so a padding op moves margins too; it just does not floor them.
62
80
 
63
81
  An alias sitting off the subset spends its first step reaching the rung the shift points at, so `--space-2` with `shift: 1` lands on `--space-4` rather than jumping past it.
@@ -70,5 +88,6 @@ Every value written is an existing token; nothing new is minted. `tokens.css`, s
70
88
 
71
89
  - The CLI exits 0 and the report card lists the changes you expected, with no surprising skips.
72
90
  - The app (dev server running) shows the new shape on each changed component after a reload.
91
+ - Buttons still read as buttons: the label has room at both ends, and a pill has more of it than a square-cornered control had.
73
92
  - `component-configs/<id>/_working.json` exists for every component the report listed. That buffer is the whole change: it stays until the open theme is saved or another theme is loaded.
74
93
  - To revert, run the inverse ops, or load a theme in the Theme panel to discard every unsaved edit.
@@ -130,6 +130,21 @@ A sky needs a committed canvas: when the Canvas seed anchors at the ramp edge
130
130
  side and the engine skips the sky, saying so in the report. An atmospheric
131
131
  brief that wants one should already be at canvas commitment level 2–3.
132
132
 
133
+ ## Shadow weight follows the canvas
134
+
135
+ The engine sets the opacity of the `--shadow-*` scale from the Canvas seed's
136
+ lightness and carries each shadow's geometry and color through untouched. A
137
+ near-black shadow at 0.9 opacity is what a dark ground needs to show any
138
+ shadow at all, and it punches a hole in paper, so opacity holds at 0.9 up to
139
+ canvas L 0.5 and eases to 0.2 by L 0.9. There is nothing to choose: the report
140
+ prints the derived value, and every regeneration re-derives it, so a brief
141
+ iterated from a light canvas to a dark one gets its weight back.
142
+
143
+ When the user says the shadows read heavy, muddy, or dirty, the canvas is the
144
+ lever. Raise the Canvas seed's lightness and the shadows lighten with it. A
145
+ one-off override lives in the editor's Shadows tab and survives regeneration
146
+ in everything except opacity.
147
+
133
148
  ## Named themes (canonical OKLCH anchors)
134
149
 
135
150
  Holiday briefs are statement briefs — default to commitment level 2–3 above, not cream. The named colors go on the *ground*, not just the buttons.
@@ -143,7 +158,7 @@ Holiday briefs are statement briefs — default to commitment level 2–3 above,
143
158
 
144
159
  ## Scope
145
160
 
146
- Fonts are never touched (they carry forward from the live look, as do shadows and component aliases). Swatch gradients (`--gradient-1..4`) carry forward when user-tuned; if they are absent or still stock, the engine rebuilds them from the new theme's families — you never author them. Radius is out of scope for generation. Shipping the theme stays a human action: Adopt, in the editor.
161
+ Fonts are never touched (they carry forward from the live look, as do component aliases and shadow geometry; shadow opacity is derived, see above). Swatch gradients (`--gradient-1..4`) carry forward when user-tuned; if they are absent or still stock, the engine rebuilds them from the new theme's families — you never author them. Radius is out of scope for generation. Shipping the theme stays a human action: Adopt, in the editor.
147
162
 
148
163
  ## Verify
149
164
 
package/CHANGELOG.md CHANGED
@@ -1,5 +1,52 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.54.1 — Palette editing opens on a curve
4
+
5
+ ### Changed
6
+
7
+ - **The base-color panel opens with the rest of the palette editor.** The panel
8
+ above the swatch grid appeared and vanished in one frame while the swatch
9
+ grid and the derived row animated, so opening a base color read as a jump. It
10
+ now reveals on the same height transition as its neighbours.
11
+
12
+ - **The floating token pills read as polished chrome.** The gradient was tuned
13
+ against a dark ground and sat too low in the scale for a light page, where it
14
+ looked tarnished. The bands keep their spacing and move up the scale, the
15
+ border becomes a dark hairline, and the strings drop to 0.3 opacity on light
16
+ pages, where a black stroke at 0.5 reads far heavier than the white stroke it
17
+ mirrors. The darkest stop stays at `#8f9090`, so the black label keeps AA.
18
+
19
+ - **The demo hero labels its theme picker.** The trigger showed the open
20
+ theme's name with nothing saying what it picked. A "Theme" label sits above
21
+ it, and the actions row bottom-aligns so the picker and the buttons sit on
22
+ one line.
23
+
24
+ ## 0.54.0 — Shadow weight and text insets follow their context
25
+
26
+ ### Fixed
27
+
28
+ - **Padding that holds text stops at `--space-6`.** One floor for every inset
29
+ treated a card and a button alike, but the components that hold text double
30
+ their padding horizontally, so `--space-4` on a button is 4px over an 18px
31
+ line and 8px at each end. `adjust` now reads which floor applies off the
32
+ config itself: a variant that also declares a `-text-font-size` is holding
33
+ type. Containers keep the `--space-4` floor, `-margin` and `-gap` stay
34
+ exempt, and both rungs stay reachable by `set` and by the editor picker.
35
+ Midnight Study shipped with every text inset on the old floor, its pill
36
+ buttons worst of all; its controls move up, and the six full-size button
37
+ variants to `--space-8` so the capsule has room for its label.
38
+
39
+ ### Changed
40
+
41
+ - **Shadow weight follows the canvas.** The `--shadow-*` scale was one fixed
42
+ near-black at 0.9 opacity in every theme, which is what a dark ground needs
43
+ and what puts a smudge under every card on a light one. `generate-theme` now
44
+ derives the opacity from the Canvas seed's lightness: 0.9 holds up to L 0.5
45
+ and eases to 0.2 by L 0.9. Geometry and color carry forward untouched, so a
46
+ hand-tuned elevation ramp survives regeneration. The three light presets
47
+ (Ocean, Spring Meadow, Autumn) ship with the derived values; the dark presets
48
+ and the default theme are unchanged.
49
+
3
50
  ## 0.53.0 — Themes are complete documents
4
51
 
5
52
  ### Fixed
@@ -241,6 +241,7 @@ export function formatGenerateThemeResult(result) {
241
241
  ? '\nGradients: kept your tuned swatch gradients.'
242
242
  : '\nGradients: swatch tokens rebuilt from the theme families.',
243
243
  );
244
+ lines.push(`Shadows: ${result.report.shadows}; carried geometry kept.`);
244
245
  if (result.report.canvasGradient) {
245
246
  lines.push(`Canvas sky: ${result.report.canvasGradient}.`);
246
247
  }
@@ -102,6 +102,7 @@ var SPACE_SETTABLE = [
102
102
  ];
103
103
  var SPACE_RUNGS = SPACE_SETTABLE;
104
104
  var INSET_RUNGS = SPACE_SETTABLE.slice(SPACE_SETTABLE.indexOf("--space-4"));
105
+ var TEXT_INSET_RUNGS = SPACE_SETTABLE.slice(SPACE_SETTABLE.indexOf("--space-6"));
105
106
  var SPACE_FAMILY = [...SPACE_SETTABLE, "--space-40", "--space-64", "--space-96", "--space-128"];
106
107
  var BORDER_WIDTH_RUNGS = [
107
108
  "--border-width-0",
@@ -133,10 +134,13 @@ var LADDERS = {
133
134
  }
134
135
  };
135
136
  var TOKEN_NAME = /^--[a-z0-9-]+$/;
136
- function rungsFor(op, variable) {
137
+ function rungsFor(op, variable, aliases) {
137
138
  if (op.kind === "radius") return op.full ? [...RADIUS_RUNGS, RADIUS_FULL] : RADIUS_RUNGS;
138
- if (op.kind === "padding" && stripSide(variable).endsWith("-padding")) return INSET_RUNGS;
139
- return LADDERS[op.kind].rungs;
139
+ if (op.kind !== "padding") return LADDERS[op.kind].rungs;
140
+ const base = stripSide(variable);
141
+ if (!base.endsWith("-padding")) return LADDERS.padding.rungs;
142
+ const variant = base.slice(0, -"-padding".length);
143
+ return `${variant}-text-font-size` in aliases ? TEXT_INSET_RUNGS : INSET_RUNGS;
140
144
  }
141
145
  function spaceStep(token) {
142
146
  return Number(token.slice("--space-".length));
@@ -149,11 +153,11 @@ function snapRung(token, rungs, direction) {
149
153
  }
150
154
  return -1;
151
155
  }
152
- function resolve(op, variable, value) {
156
+ function resolve(op, variable, value, aliases) {
153
157
  if (typeof value !== "string" || !TOKEN_NAME.test(value)) return { skip: "raw-value" };
154
158
  if (!LADDERS[op.kind].family.includes(value)) return { skip: "off-ladder" };
155
159
  if (op.set !== void 0) return { from: value, to: op.set };
156
- const rungs = rungsFor(op, variable);
160
+ const rungs = rungsFor(op, variable, aliases);
157
161
  let index = rungs.indexOf(value);
158
162
  let shift = op.shift;
159
163
  if (index < 0) {
@@ -206,7 +210,7 @@ function adjustAliases(configs, ops, now) {
206
210
  let changed = false;
207
211
  for (const [variable, value] of Object.entries(config.aliases)) {
208
212
  if (!matchesKind(variable, op.kind)) continue;
209
- const outcome = resolve(op, variable, value);
213
+ const outcome = resolve(op, variable, value, config.aliases);
210
214
  if ("skip" in outcome) {
211
215
  reportFor(component).skips.push({ variable, value, reason: outcome.skip });
212
216
  continue;
@@ -70,6 +70,7 @@ var SPACE_SETTABLE = [
70
70
  ];
71
71
  var SPACE_RUNGS = SPACE_SETTABLE;
72
72
  var INSET_RUNGS = SPACE_SETTABLE.slice(SPACE_SETTABLE.indexOf("--space-4"));
73
+ var TEXT_INSET_RUNGS = SPACE_SETTABLE.slice(SPACE_SETTABLE.indexOf("--space-6"));
73
74
  var SPACE_FAMILY = [...SPACE_SETTABLE, "--space-40", "--space-64", "--space-96", "--space-128"];
74
75
  var BORDER_WIDTH_RUNGS = [
75
76
  "--border-width-0",
@@ -101,10 +102,13 @@ var LADDERS = {
101
102
  }
102
103
  };
103
104
  var TOKEN_NAME = /^--[a-z0-9-]+$/;
104
- function rungsFor(op, variable) {
105
+ function rungsFor(op, variable, aliases) {
105
106
  if (op.kind === "radius") return op.full ? [...RADIUS_RUNGS, RADIUS_FULL] : RADIUS_RUNGS;
106
- if (op.kind === "padding" && stripSide(variable).endsWith("-padding")) return INSET_RUNGS;
107
- return LADDERS[op.kind].rungs;
107
+ if (op.kind !== "padding") return LADDERS[op.kind].rungs;
108
+ const base = stripSide(variable);
109
+ if (!base.endsWith("-padding")) return LADDERS.padding.rungs;
110
+ const variant = base.slice(0, -"-padding".length);
111
+ return `${variant}-text-font-size` in aliases ? TEXT_INSET_RUNGS : INSET_RUNGS;
108
112
  }
109
113
  function spaceStep(token) {
110
114
  return Number(token.slice("--space-".length));
@@ -117,11 +121,11 @@ function snapRung(token, rungs, direction) {
117
121
  }
118
122
  return -1;
119
123
  }
120
- function resolve(op, variable, value) {
124
+ function resolve(op, variable, value, aliases) {
121
125
  if (typeof value !== "string" || !TOKEN_NAME.test(value)) return { skip: "raw-value" };
122
126
  if (!LADDERS[op.kind].family.includes(value)) return { skip: "off-ladder" };
123
127
  if (op.set !== void 0) return { from: value, to: op.set };
124
- const rungs = rungsFor(op, variable);
128
+ const rungs = rungsFor(op, variable, aliases);
125
129
  let index = rungs.indexOf(value);
126
130
  let shift = op.shift;
127
131
  if (index < 0) {
@@ -174,7 +178,7 @@ function adjustAliases(configs, ops, now) {
174
178
  let changed = false;
175
179
  for (const [variable, value] of Object.entries(config.aliases)) {
176
180
  if (!matchesKind(variable, op.kind)) continue;
177
- const outcome = resolve(op, variable, value);
181
+ const outcome = resolve(op, variable, value, config.aliases);
178
182
  if ("skip" in outcome) {
179
183
  reportFor(component).skips.push({ variable, value, reason: outcome.skip });
180
184
  continue;
@@ -691,6 +691,39 @@ function makeDefaultGradients() {
691
691
  ];
692
692
  }
693
693
 
694
+ // src/editor/core/themes/parsers/shadow.ts
695
+ var SHADOW_VAR_NAMES = [
696
+ "--shadow-sm",
697
+ "--shadow-md",
698
+ "--shadow-lg",
699
+ "--shadow-xl",
700
+ "--shadow-2xl"
701
+ ];
702
+ function computeAngleDistance(x, y) {
703
+ const distance = Math.round(Math.sqrt(x * x + y * y));
704
+ if (distance === 0) return { angle: 135, distance: 0 };
705
+ let angle = Math.atan2(y, -x) * (180 / Math.PI);
706
+ if (angle < 0) angle += 360;
707
+ return { angle: Math.round(angle), distance };
708
+ }
709
+ function shadowTokenCss(t) {
710
+ return `${t.x}px ${t.y}px ${t.blur}px ${t.spread}px hsla(${t.hue}, ${t.saturation}%, ${t.lightness}%, ${t.opacity})`;
711
+ }
712
+ function parseShadowCss(variable, raw) {
713
+ const m = raw.trim().match(/^(-?\d+)px\s+(-?\d+)px\s+(\d+)px\s+(-?\d+)px\s+hsla\(([\d.]+),\s*([\d.]+)%,\s*([\d.]+)%,\s*([\d.]+)\)$/);
714
+ if (!m) return null;
715
+ const x = parseInt(m[1], 10);
716
+ const y = parseInt(m[2], 10);
717
+ const blur = parseInt(m[3], 10);
718
+ const spread = parseInt(m[4], 10);
719
+ const hue = Math.round(parseFloat(m[5]));
720
+ const saturation = Math.round(parseFloat(m[6]));
721
+ const lightness = Math.round(parseFloat(m[7]));
722
+ const opacity = parseFloat(m[8]);
723
+ const { angle, distance } = computeAngleDistance(x, y);
724
+ return { variable, x, y, blur, spread, opacity, hue, saturation, lightness, angle, distance };
725
+ }
726
+
694
727
  // src/editor/core/palettes/contrast.ts
695
728
  var AA_BODY = 4.5;
696
729
  var AA_LARGE = 3;
@@ -1760,6 +1793,34 @@ function applyCanvasGradient(canvas, scheme) {
1760
1793
  canvas.gradientSize = "window";
1761
1794
  return `on, ${labels[sky]} \u2192 ${labels[anchor]}`;
1762
1795
  }
1796
+ var STOCK_SHADOW_GEOMETRY = {
1797
+ "--shadow-sm": [1, 1, 2],
1798
+ "--shadow-md": [3, 3, 6],
1799
+ "--shadow-lg": [5, 5, 9],
1800
+ "--shadow-xl": [6, 7, 13],
1801
+ "--shadow-2xl": [8, 9, 16]
1802
+ };
1803
+ var STOCK_SHADOW_COLOR = { spread: 0, hue: 237, saturation: 18, lightness: 3, opacity: 0.9 };
1804
+ var SHADOW_OPACITY_DEEP = 0.9;
1805
+ var SHADOW_OPACITY_SOFT = 0.2;
1806
+ var SHADOW_GROUND_DEEP = 0.5;
1807
+ var SHADOW_GROUND_SOFT = 0.9;
1808
+ function shadowOpacityForCanvas(canvasL) {
1809
+ const t = (canvasL - SHADOW_GROUND_DEEP) / (SHADOW_GROUND_SOFT - SHADOW_GROUND_DEEP);
1810
+ const ramp = Math.max(0, Math.min(1, t));
1811
+ const opacity = SHADOW_OPACITY_DEEP + ramp * (SHADOW_OPACITY_SOFT - SHADOW_OPACITY_DEEP);
1812
+ return Math.round(opacity * 100) / 100;
1813
+ }
1814
+ function shadowVars(carried, canvas) {
1815
+ const opacity = shadowOpacityForCanvas(canvas.l);
1816
+ const out = {};
1817
+ for (const variable of SHADOW_VAR_NAMES) {
1818
+ const [x, y, blur] = STOCK_SHADOW_GEOMETRY[variable];
1819
+ const base = parseShadowCss(variable, carried[variable] ?? "") ?? { x, y, blur, ...STOCK_SHADOW_COLOR };
1820
+ out[variable] = shadowTokenCss({ ...base, opacity });
1821
+ }
1822
+ return out;
1823
+ }
1763
1824
  function buildColorsAndTypeFromSeeds(brief, carry = {}, now = (/* @__PURE__ */ new Date()).toISOString()) {
1764
1825
  const { seeds, slug } = validateBrief(brief);
1765
1826
  const configs = {};
@@ -1784,6 +1845,7 @@ function buildColorsAndTypeFromSeeds(brief, carry = {}, now = (/* @__PURE__ */ n
1784
1845
  Object.entries(carry.cssVariables ?? {}).filter(([k]) => !owned.has(k))
1785
1846
  );
1786
1847
  for (const t of gradients) cssVariables[t.variable] = formatGradientValue(t);
1848
+ Object.assign(cssVariables, shadowVars(carry.cssVariables ?? {}, seeds.Canvas));
1787
1849
  const colorsAndType = {
1788
1850
  name: brief.name.trim(),
1789
1851
  createdAt: now,
@@ -1802,6 +1864,7 @@ function buildColorsAndTypeFromSeeds(brief, carry = {}, now = (/* @__PURE__ */ n
1802
1864
  report: {
1803
1865
  ...report,
1804
1866
  gradients: gradients === carry.gradients ? "carried" : "recipes",
1867
+ shadows: `opacity ${shadowOpacityForCanvas(seeds.Canvas.l)} for a canvas at L ${seeds.Canvas.l.toFixed(2)}`,
1805
1868
  ...canvasGradient ? { canvasGradient } : {}
1806
1869
  }
1807
1870
  };
@@ -41,7 +41,8 @@ interface ColorsAndTypeBrief {
41
41
  canvasGradient?: boolean;
42
42
  }
43
43
  /** Non-color content carried forward from the currently active colors and type
44
- * so gradients, shadows, component aliases, and fonts survive regeneration. */
44
+ * so gradients, shadow geometry, component aliases, and fonts survive
45
+ * regeneration. Shadow opacity is derived, not carried — see `shadowVars`. */
45
46
  interface CarryForward {
46
47
  cssVariables?: Record<string, string>;
47
48
  fontSources?: FontSource[];
@@ -68,6 +69,8 @@ interface GenerateColorsAndTypeReport {
68
69
  * 'on, <sky> → <anchor>' or a 'skipped — …' explanation when the Canvas
69
70
  * anchors at the ramp edge and leaves no room. */
70
71
  canvasGradient?: string;
72
+ /** The derived shadow opacity and the canvas lightness it came from. */
73
+ shadows: string;
71
74
  }
72
75
  interface GenerateColorsAndTypeResult {
73
76
  colorsAndType: ColorsAndType;
@@ -41,7 +41,8 @@ interface ColorsAndTypeBrief {
41
41
  canvasGradient?: boolean;
42
42
  }
43
43
  /** Non-color content carried forward from the currently active colors and type
44
- * so gradients, shadows, component aliases, and fonts survive regeneration. */
44
+ * so gradients, shadow geometry, component aliases, and fonts survive
45
+ * regeneration. Shadow opacity is derived, not carried — see `shadowVars`. */
45
46
  interface CarryForward {
46
47
  cssVariables?: Record<string, string>;
47
48
  fontSources?: FontSource[];
@@ -68,6 +69,8 @@ interface GenerateColorsAndTypeReport {
68
69
  * 'on, <sky> → <anchor>' or a 'skipped — …' explanation when the Canvas
69
70
  * anchors at the ramp edge and leaves no room. */
70
71
  canvasGradient?: string;
72
+ /** The derived shadow opacity and the canvas lightness it came from. */
73
+ shadows: string;
71
74
  }
72
75
  interface GenerateColorsAndTypeResult {
73
76
  colorsAndType: ColorsAndType;
@@ -86,6 +86,39 @@ function makeDefaultGradients() {
86
86
  ];
87
87
  }
88
88
 
89
+ // src/editor/core/themes/parsers/shadow.ts
90
+ var SHADOW_VAR_NAMES = [
91
+ "--shadow-sm",
92
+ "--shadow-md",
93
+ "--shadow-lg",
94
+ "--shadow-xl",
95
+ "--shadow-2xl"
96
+ ];
97
+ function computeAngleDistance(x, y) {
98
+ const distance = Math.round(Math.sqrt(x * x + y * y));
99
+ if (distance === 0) return { angle: 135, distance: 0 };
100
+ let angle = Math.atan2(y, -x) * (180 / Math.PI);
101
+ if (angle < 0) angle += 360;
102
+ return { angle: Math.round(angle), distance };
103
+ }
104
+ function shadowTokenCss(t) {
105
+ return `${t.x}px ${t.y}px ${t.blur}px ${t.spread}px hsla(${t.hue}, ${t.saturation}%, ${t.lightness}%, ${t.opacity})`;
106
+ }
107
+ function parseShadowCss(variable, raw) {
108
+ const m = raw.trim().match(/^(-?\d+)px\s+(-?\d+)px\s+(\d+)px\s+(-?\d+)px\s+hsla\(([\d.]+),\s*([\d.]+)%,\s*([\d.]+)%,\s*([\d.]+)\)$/);
109
+ if (!m) return null;
110
+ const x = parseInt(m[1], 10);
111
+ const y = parseInt(m[2], 10);
112
+ const blur = parseInt(m[3], 10);
113
+ const spread = parseInt(m[4], 10);
114
+ const hue = Math.round(parseFloat(m[5]));
115
+ const saturation = Math.round(parseFloat(m[6]));
116
+ const lightness = Math.round(parseFloat(m[7]));
117
+ const opacity = parseFloat(m[8]);
118
+ const { angle, distance } = computeAngleDistance(x, y);
119
+ return { variable, x, y, blur, spread, opacity, hue, saturation, lightness, angle, distance };
120
+ }
121
+
89
122
  // src/editor/core/palettes/contrast.ts
90
123
  var AA_BODY = 4.5;
91
124
  var AA_LARGE = 3;
@@ -410,6 +443,34 @@ function applyCanvasGradient(canvas, scheme) {
410
443
  canvas.gradientSize = "window";
411
444
  return `on, ${labels[sky]} \u2192 ${labels[anchor]}`;
412
445
  }
446
+ var STOCK_SHADOW_GEOMETRY = {
447
+ "--shadow-sm": [1, 1, 2],
448
+ "--shadow-md": [3, 3, 6],
449
+ "--shadow-lg": [5, 5, 9],
450
+ "--shadow-xl": [6, 7, 13],
451
+ "--shadow-2xl": [8, 9, 16]
452
+ };
453
+ var STOCK_SHADOW_COLOR = { spread: 0, hue: 237, saturation: 18, lightness: 3, opacity: 0.9 };
454
+ var SHADOW_OPACITY_DEEP = 0.9;
455
+ var SHADOW_OPACITY_SOFT = 0.2;
456
+ var SHADOW_GROUND_DEEP = 0.5;
457
+ var SHADOW_GROUND_SOFT = 0.9;
458
+ function shadowOpacityForCanvas(canvasL) {
459
+ const t = (canvasL - SHADOW_GROUND_DEEP) / (SHADOW_GROUND_SOFT - SHADOW_GROUND_DEEP);
460
+ const ramp = Math.max(0, Math.min(1, t));
461
+ const opacity = SHADOW_OPACITY_DEEP + ramp * (SHADOW_OPACITY_SOFT - SHADOW_OPACITY_DEEP);
462
+ return Math.round(opacity * 100) / 100;
463
+ }
464
+ function shadowVars(carried, canvas) {
465
+ const opacity = shadowOpacityForCanvas(canvas.l);
466
+ const out = {};
467
+ for (const variable of SHADOW_VAR_NAMES) {
468
+ const [x, y, blur] = STOCK_SHADOW_GEOMETRY[variable];
469
+ const base = parseShadowCss(variable, carried[variable] ?? "") ?? { x, y, blur, ...STOCK_SHADOW_COLOR };
470
+ out[variable] = shadowTokenCss({ ...base, opacity });
471
+ }
472
+ return out;
473
+ }
413
474
  function buildColorsAndTypeFromSeeds(brief, carry = {}, now = (/* @__PURE__ */ new Date()).toISOString()) {
414
475
  const { seeds, slug } = validateBrief(brief);
415
476
  const configs = {};
@@ -434,6 +495,7 @@ function buildColorsAndTypeFromSeeds(brief, carry = {}, now = (/* @__PURE__ */ n
434
495
  Object.entries(carry.cssVariables ?? {}).filter(([k]) => !owned.has(k))
435
496
  );
436
497
  for (const t of gradients) cssVariables[t.variable] = formatGradientValue(t);
498
+ Object.assign(cssVariables, shadowVars(carry.cssVariables ?? {}, seeds.Canvas));
437
499
  const colorsAndType = {
438
500
  name: brief.name.trim(),
439
501
  createdAt: now,
@@ -452,6 +514,7 @@ function buildColorsAndTypeFromSeeds(brief, carry = {}, now = (/* @__PURE__ */ n
452
514
  report: {
453
515
  ...report,
454
516
  gradients: gradients === carry.gradients ? "carried" : "recipes",
517
+ shadows: `opacity ${shadowOpacityForCanvas(seeds.Canvas.l)} for a canvas at L ${seeds.Canvas.l.toFixed(2)}`,
455
518
  ...canvasGradient ? { canvasGradient } : {}
456
519
  }
457
520
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@motion-proto/live-tokens",
3
- "version": "0.53.0",
3
+ "version": "0.54.1",
4
4
  "type": "module",
5
5
  "description": "Design token editor with live CSS variable editing. Svelte 5 + Vite 8.",
6
6
  "keywords": [
@@ -66,6 +66,12 @@ const SPACE_RUNGS = SPACE_SETTABLE;
66
66
  rungs stay pickable by hand and settable by name. */
67
67
  const INSET_RUNGS = SPACE_SETTABLE.slice(SPACE_SETTABLE.indexOf('--space-4'));
68
68
 
69
+ /** Padding that wraps a line of type stops a rung higher. Those components
70
+ double it horizontally (`themed-padding($h: 2)`), so `--space-4` is 4px
71
+ over an 18px line and 8px inside a pill's own corner: squeezed rather than
72
+ compact. No shipped default puts text below `--space-6`. */
73
+ const TEXT_INSET_RUNGS = SPACE_SETTABLE.slice(SPACE_SETTABLE.indexOf('--space-6'));
74
+
69
75
  const SPACE_FAMILY = [...SPACE_SETTABLE, '--space-40', '--space-64', '--space-96', '--space-128'];
70
76
 
71
77
  const BORDER_WIDTH_RUNGS = [
@@ -95,11 +101,16 @@ const TOKEN_NAME = /^--[a-z0-9-]+$/;
95
101
  type Outcome = { from: string; to: string } | { skip: SkipReason };
96
102
 
97
103
  /** `-margin` shares the padding kind, so the inset floor keys off the variable
98
- rather than the kind. */
99
- function rungsFor(op: AdjustOp, variable: string): string[] {
104
+ rather than the kind. Which floor depends on what the padding surrounds,
105
+ and the config says so itself: a variant that also declares a
106
+ `-text-font-size` is holding type. */
107
+ function rungsFor(op: AdjustOp, variable: string, aliases: ComponentConfig['aliases']): string[] {
100
108
  if (op.kind === 'radius') return op.full ? [...RADIUS_RUNGS, RADIUS_FULL] : RADIUS_RUNGS;
101
- if (op.kind === 'padding' && stripSide(variable).endsWith('-padding')) return INSET_RUNGS;
102
- return LADDERS[op.kind].rungs;
109
+ if (op.kind !== 'padding') return LADDERS[op.kind].rungs;
110
+ const base = stripSide(variable);
111
+ if (!base.endsWith('-padding')) return LADDERS.padding.rungs;
112
+ const variant = base.slice(0, -'-padding'.length);
113
+ return `${variant}-text-font-size` in aliases ? TEXT_INSET_RUNGS : INSET_RUNGS;
103
114
  }
104
115
 
105
116
  function spaceStep(token: string): number {
@@ -119,12 +130,17 @@ function snapRung(token: string, rungs: string[], direction: number): number {
119
130
  return -1;
120
131
  }
121
132
 
122
- function resolve(op: AdjustOp, variable: string, value: AliasDiskValue): Outcome {
133
+ function resolve(
134
+ op: AdjustOp,
135
+ variable: string,
136
+ value: AliasDiskValue,
137
+ aliases: ComponentConfig['aliases'],
138
+ ): Outcome {
123
139
  if (typeof value !== 'string' || !TOKEN_NAME.test(value)) return { skip: 'raw-value' };
124
140
  if (!LADDERS[op.kind].family.includes(value)) return { skip: 'off-ladder' };
125
141
  if (op.set !== undefined) return { from: value, to: op.set };
126
142
 
127
- const rungs = rungsFor(op, variable);
143
+ const rungs = rungsFor(op, variable, aliases);
128
144
  let index = rungs.indexOf(value);
129
145
  let shift = op.shift!;
130
146
  if (index < 0) {
@@ -188,7 +204,7 @@ export function adjustAliases(
188
204
 
189
205
  for (const [variable, value] of Object.entries(config.aliases)) {
190
206
  if (!matchesKind(variable, op.kind)) continue;
191
- const outcome = resolve(op, variable, value);
207
+ const outcome = resolve(op, variable, value, config.aliases);
192
208
  if ('skip' in outcome) {
193
209
  reportFor(component).skips.push({ variable, value, reason: outcome.skip });
194
210
  continue;
@@ -18,6 +18,7 @@ import {
18
18
  setCurveAnchor,
19
19
  } from '../palettes/paletteDerivation';
20
20
  import { formatGradientValue, makeDefaultGradients } from './slices/gradients';
21
+ import { SHADOW_VAR_NAMES, parseShadowCss, shadowTokenCss } from './parsers/shadow';
21
22
  import { AA_BODY, AA_LARGE, contrastRatio, findLForContrast } from '../palettes/contrast';
22
23
  import { type HarmonyAxis, type HarmonyMode } from '../palettes/colorHarmony';
23
24
  import { defaultPaletteConfig } from '../../ui/palette/paletteMath';
@@ -39,7 +40,8 @@ export interface ColorsAndTypeBrief {
39
40
  }
40
41
 
41
42
  /** Non-color content carried forward from the currently active colors and type
42
- * so gradients, shadows, component aliases, and fonts survive regeneration. */
43
+ * so gradients, shadow geometry, component aliases, and fonts survive
44
+ * regeneration. Shadow opacity is derived, not carried — see `shadowVars`. */
43
45
  export interface CarryForward {
44
46
  cssVariables?: Record<string, string>;
45
47
  fontSources?: FontSource[];
@@ -68,6 +70,8 @@ export interface GenerateColorsAndTypeReport {
68
70
  * 'on, <sky> → <anchor>' or a 'skipped — …' explanation when the Canvas
69
71
  * anchors at the ramp edge and leaves no room. */
70
72
  canvasGradient?: string;
73
+ /** The derived shadow opacity and the canvas lightness it came from. */
74
+ shadows: string;
71
75
  }
72
76
 
73
77
  export interface GenerateColorsAndTypeResult {
@@ -377,6 +381,49 @@ function applyCanvasGradient(canvas: PaletteConfig, scheme: SchemeDirection): st
377
381
  return `on, ${labels[sky]} → ${labels[anchor]}`;
378
382
  }
379
383
 
384
+ /** The tokens.css scale, the starting point when a theme carries no shadows.
385
+ * Geometry per step; every step shares one color. */
386
+ const STOCK_SHADOW_GEOMETRY: Record<(typeof SHADOW_VAR_NAMES)[number], readonly [number, number, number]> = {
387
+ '--shadow-sm': [1, 1, 2],
388
+ '--shadow-md': [3, 3, 6],
389
+ '--shadow-lg': [5, 5, 9],
390
+ '--shadow-xl': [6, 7, 13],
391
+ '--shadow-2xl': [8, 9, 16],
392
+ };
393
+ const STOCK_SHADOW_COLOR = { spread: 0, hue: 237, saturation: 18, lightness: 3, opacity: 0.9 };
394
+
395
+ /** A near-black shadow needs 0.9 to read at all against a dark ground, and at
396
+ * 0.9 it punches a hole in a light one. Opacity therefore rides the canvas:
397
+ * full weight up to DEEP, easing to SOFT by the time the page is paper. */
398
+ const SHADOW_OPACITY_DEEP = 0.9;
399
+ const SHADOW_OPACITY_SOFT = 0.2;
400
+ const SHADOW_GROUND_DEEP = 0.5;
401
+ const SHADOW_GROUND_SOFT = 0.9;
402
+
403
+ export function shadowOpacityForCanvas(canvasL: number): number {
404
+ const t = (canvasL - SHADOW_GROUND_DEEP) / (SHADOW_GROUND_SOFT - SHADOW_GROUND_DEEP);
405
+ const ramp = Math.max(0, Math.min(1, t));
406
+ const opacity = SHADOW_OPACITY_DEEP + ramp * (SHADOW_OPACITY_SOFT - SHADOW_OPACITY_DEEP);
407
+ return Math.round(opacity * 100) / 100;
408
+ }
409
+
410
+ /** Recolor the scale for this canvas: carried geometry and color ride through
411
+ * (elevation shape belongs to the shape pass), opacity is replaced. Replaced
412
+ * rather than kept-when-untouched, because a brief iterated from a light
413
+ * canvas to a dark one would otherwise hold a shadow too faint to see. The
414
+ * Canvas seed is the page background verbatim, so it is the ground the
415
+ * shadow falls on. */
416
+ function shadowVars(carried: Record<string, string>, canvas: Oklch): Record<string, string> {
417
+ const opacity = shadowOpacityForCanvas(canvas.l);
418
+ const out: Record<string, string> = {};
419
+ for (const variable of SHADOW_VAR_NAMES) {
420
+ const [x, y, blur] = STOCK_SHADOW_GEOMETRY[variable];
421
+ const base = parseShadowCss(variable, carried[variable] ?? '') ?? { x, y, blur, ...STOCK_SHADOW_COLOR };
422
+ out[variable] = shadowTokenCss({ ...base, opacity });
423
+ }
424
+ return out;
425
+ }
426
+
380
427
  export function buildColorsAndTypeFromSeeds(
381
428
  brief: ColorsAndTypeBrief,
382
429
  carry: CarryForward = {},
@@ -417,6 +464,7 @@ export function buildColorsAndTypeFromSeeds(
417
464
  );
418
465
  // Rendered projections of the structured gradients, kept for production CSS.
419
466
  for (const t of gradients) cssVariables[t.variable] = formatGradientValue(t);
467
+ Object.assign(cssVariables, shadowVars(carry.cssVariables ?? {}, seeds.Canvas));
420
468
 
421
469
  const colorsAndType: ColorsAndType = {
422
470
  name: brief.name.trim(),
@@ -437,6 +485,7 @@ export function buildColorsAndTypeFromSeeds(
437
485
  report: {
438
486
  ...report,
439
487
  gradients: gradients === carry.gradients ? 'carried' : 'recipes',
488
+ shadows: `opacity ${shadowOpacityForCanvas(seeds.Canvas.l)} for a canvas at L ${seeds.Canvas.l.toFixed(2)}`,
440
489
  ...(canvasGradient ? { canvasGradient } : {}),
441
490
  },
442
491
  };