@motion-proto/live-tokens 0.38.0 → 0.40.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.
@@ -11,10 +11,18 @@ For composing a page once you've picked components, see [[live-tokens-build-page
11
11
 
12
12
  ## Catalogue
13
13
 
14
- Action: `Button`. Input: `Input`. Selection: `SegmentedControl`, `TabBar`, `RadioButton`, `MenuSelect`, `Toggle`. Containers: `Card`, `CollapsibleSection`, `Dialog`. Messaging: `Callout`, `Notification`, `Tooltip`, `Badge`, `CornerBadge`. Display: `Table`, `Image`, `ImageLightbox`, `ProgressBar`, `SectionDivider`, `SideNavigation`, `CodeSnippet`.
14
+ Action: `Button`, `IconButton`. Input: `Input`. Selection: `SegmentedControl`, `TabBar`, `RadioButton`, `MenuSelect`, `Toggle`. Containers: `Card`, `CollapsibleSection`, `Dialog`. Messaging: `Callout`, `Notification`, `Tooltip`, `Badge`, `CornerBadge`. Display: `Table`, `Image`, `ImageLightbox`, `ProgressBar`, `SectionDivider`, `SideNavigation`, `CodeSnippet`.
15
15
 
16
16
  `CodeSnippet` is for a single-line command or value the user is meant to copy and paste back into a terminal (install commands, generated keys, ids). Click-to-copy with a brief "Copied" popover. Use it whenever your page asks the reader to *run* something, rather than just *read* it.
17
17
 
18
+ ## Action family: Button vs IconButton
19
+
20
+ Both trigger an action and share the same six variants (primary, secondary, outline, success, danger, warning), three states (default, hover, disabled) and two sizes (default, small). They differ only in content.
21
+
22
+ - `Button` carries a text label, optionally with a leading or trailing icon. Use it whenever the action needs a word to be unambiguous.
23
+ - `IconButton` is icon-only and square. Use it for compact, space-constrained actions whose meaning is obvious from the glyph alone (toolbar controls, close/edit/delete affordances, card overflow menus). It has no text slot, so an `ariaLabel` is required for accessibility.
24
+ - **Don't reach for `IconButton` when the icon's meaning isn't self-evident.** A labelled `Button` (or a `Button` with an icon) avoids the guessing game.
25
+
18
26
  ## Single-selection family: SegmentedControl vs TabBar vs RadioButton vs MenuSelect
19
27
 
20
28
  All four pick one option from a set. The right one depends on **option count**, **whether the selection changes what's rendered below**, and **how much visual weight** you want.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,76 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.40.0 — New IconButton component
4
+
5
+ ### Added
6
+
7
+ - **`IconButton`, an icon-only sibling of `Button`.** It shares Button's six
8
+ variants (primary, secondary, outline, success, danger, warning), three states
9
+ (default, hover, disabled), and two sizes (default, small), but renders a
10
+ single icon with no text. It is square (symmetric padding plus `aspect-ratio`),
11
+ exposes the icon colour as a first-class per-variant, per-state token, and
12
+ drops Button's text-typography properties. Its tokens live in their own
13
+ `--iconbutton-*` namespace, so styling it never affects Button. Because the
14
+ control has no visible text, `ariaLabel` is required. Editable in the editor
15
+ under Components, with the same linked base block (padding, radius, border
16
+ width, icon size) that links across variants.
17
+
18
+ ### Notes
19
+
20
+ - Additive only. No token renames or `tokens.css` migration, so existing
21
+ consumers are unaffected; the new component ships its defaults in its
22
+ `:global(:root)` block like every other.
23
+
24
+ ## 0.39.0 — One unified palette model (no gray "mode")
25
+
26
+ ### Changed (breaking)
27
+
28
+ - **The chromatic/gray palette split collapses into a single OKLCH (Lightness /
29
+ Chroma / Hue) model.** A neutral is no longer a special "mode"; it is an
30
+ ordinary low-chroma palette with calm defaults (a low but non-zero base chroma,
31
+ a wider neutral lightness ramp). One derivation path (`computePaletteColor`)
32
+ now serves every palette, and the Neutral / Alternate editors show the same
33
+ full L/C/H picker, lock-to-500 toggle, and derived-scale snapping as accents.
34
+ `PaletteConfig` drops its gray vocabulary (`tintHue`, `tintChroma`,
35
+ `grayLightnessCurve`, `graySaturationCurve`); `baseColor`, `lightnessCurve`,
36
+ and `saturationCurve` are now universal, and the internal `mode` prop is
37
+ removed from the palette editors.
38
+ - **Token names are unchanged** — this is a theme-config schema change, not a
39
+ `tokens.css` migration, so no consumer CSS or token references are affected.
40
+ The shipped `default.json` was regenerated; the neutral shift is sub-1-LSB per
41
+ channel and every derived `--surface-*` / `--text-*` / `--color-*` token is
42
+ byte-identical.
43
+
44
+ ### Fixed
45
+
46
+ - **A component's surface control no longer vanishes when its fill is a flat
47
+ colour.** `componentGradientSource` returned `undefined` for any non-gradient
48
+ alias and `GradientEditor` renders only `{#if gradient}`, so the surface editor
49
+ disappeared whenever a fill was a plain colour (e.g. SectionDivider's default
50
+ transparent). The editor now synthesizes a none/solid single-stop snapshot from
51
+ a flat (or absent) alias so the type picker always renders, and promotes the
52
+ flat alias to a real gradient on first edit. SectionDivider's section heading is
53
+ renamed "Background" → "Surface" to match Panel and disambiguate it from the
54
+ preview backdrop control.
55
+ - **Palette colour overrides now apply live while you drag.** A new Text /
56
+ Surfaces / Borders override previously reached neither the live page nor the
57
+ preview swatch until commit — `handleColorChange` only wrote to the store when
58
+ the key was already an override. Every drag tick now writes (the open session
59
+ collapses to one undo entry; cancel/confirm clean up no-ops), and the per-step
60
+ hex text tracks the live colour too.
61
+
62
+ ### Migration
63
+
64
+ - **Consumer themes migrate automatically on load.** `unifyGrayPalettes` (run in
65
+ `loadFromFile` after `renamePrimaryPaletteKey`) close-maps existing neutrals to
66
+ the unified form: `baseColor` snaps to the effective step-500 colour (preserving
67
+ the subtle tint and the neutral lightness ramp), the saturation curve becomes
68
+ flat-100, and the palette is locked to base. Every palette drops the four
69
+ vestigial gray fields, and the `gray-lightness` / `gray-saturation` curve-offset
70
+ keys fold into `lightness` / `saturation`. Default and flat-saturation neutrals
71
+ are visually identical; only a hand-shaped gray *saturation* curve migrates
72
+ approximately and may want a quick manual retune.
73
+
3
74
  ## 0.38.0 — Overridable scroll reset for smooth-scroll hosts
4
75
 
5
76
  ### Added
@@ -192,16 +192,16 @@ function sampleCurve(anchors, xPos) {
192
192
 
193
193
  // src/editor/core/palettes/paletteDerivation.ts
194
194
  var PALETTE_SPECS = [
195
- { label: "Neutral", cssNamespace: "neutral", mode: "gray", initialColor: "#808080" },
196
- { label: "Alternate", cssNamespace: "alternate", mode: "gray", initialColor: "#808080" },
197
- { label: "Background", cssNamespace: "canvas", mode: "chromatic", emptySelector: true, initialColor: "#1a1a2e" },
198
- { label: "Brand", cssNamespace: "brand", mode: "chromatic", initialColor: "#c93636" },
199
- { label: "Accent", cssNamespace: "accent", mode: "chromatic", initialColor: "#f49e0b" },
200
- { label: "Special", cssNamespace: "special", mode: "chromatic", initialColor: "#8b5cf6" },
201
- { label: "Info", cssNamespace: "info", mode: "chromatic", initialColor: "#3077e8" },
202
- { label: "Success", cssNamespace: "success", mode: "chromatic", initialColor: "#21c45d" },
203
- { label: "Warning", cssNamespace: "warning", mode: "chromatic", initialColor: "#e66e1a" },
204
- { label: "Danger", cssNamespace: "danger", mode: "chromatic", initialColor: "#e8304f" }
195
+ { label: "Neutral", cssNamespace: "neutral", neutral: true, initialColor: "#70787e" },
196
+ { label: "Alternate", cssNamespace: "alternate", neutral: true, initialColor: "#817b78" },
197
+ { label: "Background", cssNamespace: "canvas", emptySelector: true, initialColor: "#1a1a2e" },
198
+ { label: "Brand", cssNamespace: "brand", initialColor: "#c93636" },
199
+ { label: "Accent", cssNamespace: "accent", initialColor: "#f49e0b" },
200
+ { label: "Special", cssNamespace: "special", initialColor: "#8b5cf6" },
201
+ { label: "Info", cssNamespace: "info", initialColor: "#3077e8" },
202
+ { label: "Success", cssNamespace: "success", initialColor: "#21c45d" },
203
+ { label: "Warning", cssNamespace: "warning", initialColor: "#e66e1a" },
204
+ { label: "Danger", cssNamespace: "danger", initialColor: "#e8304f" }
205
205
  ];
206
206
  var PALETTE_STEPS = [
207
207
  { label: "100" },
@@ -216,19 +216,6 @@ var PALETTE_STEPS = [
216
216
  { label: "900" },
217
217
  { label: "950" }
218
218
  ];
219
- var GRAY_STEPS = [
220
- { label: "100" },
221
- { label: "200" },
222
- { label: "300" },
223
- { label: "400" },
224
- { label: "500" },
225
- { label: "600" },
226
- { label: "700" },
227
- { label: "800" },
228
- { label: "850" },
229
- { label: "900" },
230
- { label: "950" }
231
- ];
232
219
  var SCALES = [
233
220
  {
234
221
  title: "Surfaces",
@@ -268,9 +255,6 @@ var SCALES = [
268
255
  ];
269
256
  var DEFAULT_PALETTE_LIGHTNESS = () => [makeAnchor(0, 95, 5), makeAnchor(100, 8, 5)];
270
257
  var DEFAULT_PALETTE_SATURATION = () => [makeAnchor(0, 100, 30), makeAnchor(100, 100, 30)];
271
- var DEFAULT_GRAY_LIGHTNESS = () => [makeAnchor(0, 92, 5), makeAnchor(100, 3, 5)];
272
- var DEFAULT_GRAY_SATURATION = () => [makeAnchor(0, 20, 30), makeAnchor(100, 20, 30)];
273
- var DEFAULT_TINT_CHROMA = 0.04;
274
258
  var defaultScaleCurves = {
275
259
  Surfaces: {
276
260
  lightness: () => [makeAnchor(0, 15, 5), makeAnchor(100, 47, 5)],
@@ -288,9 +272,6 @@ var defaultScaleCurves = {
288
272
  function paletteStepKey(label) {
289
273
  return `Palette-${label}`;
290
274
  }
291
- function grayStepKey(label) {
292
- return `gray-${label}`;
293
- }
294
275
  function stepKey(scaleTitle, stepName) {
295
276
  return `${scaleTitle}-${stepName}`;
296
277
  }
@@ -306,16 +287,6 @@ function computePaletteColor(index, base, lightnessCurve, saturationCurve, curve
306
287
  const clamped = gamutClamp(targetL, targetC, h);
307
288
  return oklchToHex(clamped.l, clamped.c, clamped.h);
308
289
  }
309
- function computeGrayColor(index, hue, chroma, grayLightnessCurve, graySaturationCurve, curveOffset) {
310
- const xPos = stepIndexToX(index, GRAY_STEPS.length);
311
- const lOff = curveOffset["gray-lightness"] ?? 0;
312
- const sOff = curveOffset["gray-saturation"] ?? 0;
313
- const targetL = Math.max(0, Math.min(100, sampleCurve(grayLightnessCurve, xPos) + lOff)) / 100;
314
- const satMul = Math.max(0, Math.min(2, (sampleCurve(graySaturationCurve, xPos) + sOff) / 100));
315
- const targetC = chroma * satMul;
316
- const clamped = gamutClamp(targetL, targetC, hue);
317
- return oklchToHex(clamped.l, clamped.c, clamped.h);
318
- }
319
290
  function computeDerivedColor(step, base, scaleTitle, scaleCurves, curveOffset) {
320
291
  const scale = SCALES.find((s) => s.title === scaleTitle);
321
292
  const idx = scale.steps.indexOf(step);
@@ -361,36 +332,18 @@ function derivePaletteVars(spec, config) {
361
332
  const overrides = config.overrides ?? {};
362
333
  const curveOffset = config.curveOffset ?? {};
363
334
  const scaleCurves = config.scaleCurves ?? {};
364
- let baseForScales;
365
- if (spec.mode === "gray") {
366
- const grayLightnessCurve = config.grayLightnessCurve ?? DEFAULT_GRAY_LIGHTNESS();
367
- const graySaturationCurve = config.graySaturationCurve ?? DEFAULT_GRAY_SATURATION();
368
- const tintHue = config.tintHue ?? 240;
369
- const tintChroma = config.tintChroma ?? DEFAULT_TINT_CHROMA;
370
- let gray500 = "#808080";
371
- GRAY_STEPS.forEach((step, index) => {
372
- const k = grayStepKey(step.label);
373
- const hex = computeGrayColor(index, tintHue, tintChroma, grayLightnessCurve, graySaturationCurve, curveOffset);
374
- const effective = k in overrides ? overrides[k] : hex;
375
- out[`--color-${spec.cssNamespace}-${step.label}`] = effective;
376
- if (step.label === "500") gray500 = hex;
377
- });
378
- baseForScales = gray500;
379
- } else {
380
- const lightnessCurve = config.lightnessCurve ?? DEFAULT_PALETTE_LIGHTNESS();
381
- const saturationCurve = config.saturationCurve ?? DEFAULT_PALETTE_SATURATION();
382
- PALETTE_STEPS.forEach((ps, index) => {
383
- const k = paletteStepKey(ps.label);
384
- const hex = computePaletteColor(index, baseColor, lightnessCurve, saturationCurve, curveOffset);
385
- const effective = k in overrides ? overrides[k] : hex;
386
- out[`--color-${spec.cssNamespace}-${ps.label}`] = effective;
387
- });
388
- baseForScales = baseColor;
389
- }
335
+ const lightnessCurve = config.lightnessCurve ?? DEFAULT_PALETTE_LIGHTNESS();
336
+ const saturationCurve = config.saturationCurve ?? DEFAULT_PALETTE_SATURATION();
337
+ PALETTE_STEPS.forEach((ps, index) => {
338
+ const k = paletteStepKey(ps.label);
339
+ const hex = computePaletteColor(index, baseColor, lightnessCurve, saturationCurve, curveOffset);
340
+ const effective = k in overrides ? overrides[k] : hex;
341
+ out[`--color-${spec.cssNamespace}-${ps.label}`] = effective;
342
+ });
390
343
  for (const scale of SCALES) {
391
344
  for (const step of scale.steps) {
392
345
  const k = stepKey(scale.title, step.name);
393
- const hex = k in overrides ? overrides[k] : computeDerivedColor(step, baseForScales, scale.title, scaleCurves, curveOffset);
346
+ const hex = k in overrides ? overrides[k] : computeDerivedColor(step, baseColor, scale.title, scaleCurves, curveOffset);
394
347
  const varName = scaleToCssVar(scale.title, step.name, spec.cssNamespace);
395
348
  if (varName) out[varName] = hex;
396
349
  }
@@ -451,12 +404,7 @@ function reconcilePalettesFromCssVars(palettes, cssVars) {
451
404
  const anchorHex = cssVars[`--color-${spec.cssNamespace}-500`];
452
405
  if (anchorHex && HEX_RE.test(anchorHex.trim())) {
453
406
  const hex = anchorHex.trim();
454
- if (spec.mode === "gray") {
455
- const { c, h } = hexToOklch(hex);
456
- next[spec.label] = { ...current, tintHue: h, tintChroma: c, _imported: false };
457
- } else {
458
- next[spec.label] = { ...current, baseColor: hex, _imported: false };
459
- }
407
+ next[spec.label] = { ...current, baseColor: hex, _imported: false };
460
408
  snapped.add(spec.label);
461
409
  } else {
462
410
  next[spec.label] = { ...current, _imported: false };
@@ -151,16 +151,16 @@ function sampleCurve(anchors, xPos) {
151
151
 
152
152
  // src/editor/core/palettes/paletteDerivation.ts
153
153
  var PALETTE_SPECS = [
154
- { label: "Neutral", cssNamespace: "neutral", mode: "gray", initialColor: "#808080" },
155
- { label: "Alternate", cssNamespace: "alternate", mode: "gray", initialColor: "#808080" },
156
- { label: "Background", cssNamespace: "canvas", mode: "chromatic", emptySelector: true, initialColor: "#1a1a2e" },
157
- { label: "Brand", cssNamespace: "brand", mode: "chromatic", initialColor: "#c93636" },
158
- { label: "Accent", cssNamespace: "accent", mode: "chromatic", initialColor: "#f49e0b" },
159
- { label: "Special", cssNamespace: "special", mode: "chromatic", initialColor: "#8b5cf6" },
160
- { label: "Info", cssNamespace: "info", mode: "chromatic", initialColor: "#3077e8" },
161
- { label: "Success", cssNamespace: "success", mode: "chromatic", initialColor: "#21c45d" },
162
- { label: "Warning", cssNamespace: "warning", mode: "chromatic", initialColor: "#e66e1a" },
163
- { label: "Danger", cssNamespace: "danger", mode: "chromatic", initialColor: "#e8304f" }
154
+ { label: "Neutral", cssNamespace: "neutral", neutral: true, initialColor: "#70787e" },
155
+ { label: "Alternate", cssNamespace: "alternate", neutral: true, initialColor: "#817b78" },
156
+ { label: "Background", cssNamespace: "canvas", emptySelector: true, initialColor: "#1a1a2e" },
157
+ { label: "Brand", cssNamespace: "brand", initialColor: "#c93636" },
158
+ { label: "Accent", cssNamespace: "accent", initialColor: "#f49e0b" },
159
+ { label: "Special", cssNamespace: "special", initialColor: "#8b5cf6" },
160
+ { label: "Info", cssNamespace: "info", initialColor: "#3077e8" },
161
+ { label: "Success", cssNamespace: "success", initialColor: "#21c45d" },
162
+ { label: "Warning", cssNamespace: "warning", initialColor: "#e66e1a" },
163
+ { label: "Danger", cssNamespace: "danger", initialColor: "#e8304f" }
164
164
  ];
165
165
  var PALETTE_STEPS = [
166
166
  { label: "100" },
@@ -175,19 +175,6 @@ var PALETTE_STEPS = [
175
175
  { label: "900" },
176
176
  { label: "950" }
177
177
  ];
178
- var GRAY_STEPS = [
179
- { label: "100" },
180
- { label: "200" },
181
- { label: "300" },
182
- { label: "400" },
183
- { label: "500" },
184
- { label: "600" },
185
- { label: "700" },
186
- { label: "800" },
187
- { label: "850" },
188
- { label: "900" },
189
- { label: "950" }
190
- ];
191
178
  var SCALES = [
192
179
  {
193
180
  title: "Surfaces",
@@ -227,9 +214,6 @@ var SCALES = [
227
214
  ];
228
215
  var DEFAULT_PALETTE_LIGHTNESS = () => [makeAnchor(0, 95, 5), makeAnchor(100, 8, 5)];
229
216
  var DEFAULT_PALETTE_SATURATION = () => [makeAnchor(0, 100, 30), makeAnchor(100, 100, 30)];
230
- var DEFAULT_GRAY_LIGHTNESS = () => [makeAnchor(0, 92, 5), makeAnchor(100, 3, 5)];
231
- var DEFAULT_GRAY_SATURATION = () => [makeAnchor(0, 20, 30), makeAnchor(100, 20, 30)];
232
- var DEFAULT_TINT_CHROMA = 0.04;
233
217
  var defaultScaleCurves = {
234
218
  Surfaces: {
235
219
  lightness: () => [makeAnchor(0, 15, 5), makeAnchor(100, 47, 5)],
@@ -247,9 +231,6 @@ var defaultScaleCurves = {
247
231
  function paletteStepKey(label) {
248
232
  return `Palette-${label}`;
249
233
  }
250
- function grayStepKey(label) {
251
- return `gray-${label}`;
252
- }
253
234
  function stepKey(scaleTitle, stepName) {
254
235
  return `${scaleTitle}-${stepName}`;
255
236
  }
@@ -265,16 +246,6 @@ function computePaletteColor(index, base, lightnessCurve, saturationCurve, curve
265
246
  const clamped = gamutClamp(targetL, targetC, h);
266
247
  return oklchToHex(clamped.l, clamped.c, clamped.h);
267
248
  }
268
- function computeGrayColor(index, hue, chroma, grayLightnessCurve, graySaturationCurve, curveOffset) {
269
- const xPos = stepIndexToX(index, GRAY_STEPS.length);
270
- const lOff = curveOffset["gray-lightness"] ?? 0;
271
- const sOff = curveOffset["gray-saturation"] ?? 0;
272
- const targetL = Math.max(0, Math.min(100, sampleCurve(grayLightnessCurve, xPos) + lOff)) / 100;
273
- const satMul = Math.max(0, Math.min(2, (sampleCurve(graySaturationCurve, xPos) + sOff) / 100));
274
- const targetC = chroma * satMul;
275
- const clamped = gamutClamp(targetL, targetC, hue);
276
- return oklchToHex(clamped.l, clamped.c, clamped.h);
277
- }
278
249
  function computeDerivedColor(step, base, scaleTitle, scaleCurves, curveOffset) {
279
250
  const scale = SCALES.find((s) => s.title === scaleTitle);
280
251
  const idx = scale.steps.indexOf(step);
@@ -320,36 +291,18 @@ function derivePaletteVars(spec, config) {
320
291
  const overrides = config.overrides ?? {};
321
292
  const curveOffset = config.curveOffset ?? {};
322
293
  const scaleCurves = config.scaleCurves ?? {};
323
- let baseForScales;
324
- if (spec.mode === "gray") {
325
- const grayLightnessCurve = config.grayLightnessCurve ?? DEFAULT_GRAY_LIGHTNESS();
326
- const graySaturationCurve = config.graySaturationCurve ?? DEFAULT_GRAY_SATURATION();
327
- const tintHue = config.tintHue ?? 240;
328
- const tintChroma = config.tintChroma ?? DEFAULT_TINT_CHROMA;
329
- let gray500 = "#808080";
330
- GRAY_STEPS.forEach((step, index) => {
331
- const k = grayStepKey(step.label);
332
- const hex = computeGrayColor(index, tintHue, tintChroma, grayLightnessCurve, graySaturationCurve, curveOffset);
333
- const effective = k in overrides ? overrides[k] : hex;
334
- out[`--color-${spec.cssNamespace}-${step.label}`] = effective;
335
- if (step.label === "500") gray500 = hex;
336
- });
337
- baseForScales = gray500;
338
- } else {
339
- const lightnessCurve = config.lightnessCurve ?? DEFAULT_PALETTE_LIGHTNESS();
340
- const saturationCurve = config.saturationCurve ?? DEFAULT_PALETTE_SATURATION();
341
- PALETTE_STEPS.forEach((ps, index) => {
342
- const k = paletteStepKey(ps.label);
343
- const hex = computePaletteColor(index, baseColor, lightnessCurve, saturationCurve, curveOffset);
344
- const effective = k in overrides ? overrides[k] : hex;
345
- out[`--color-${spec.cssNamespace}-${ps.label}`] = effective;
346
- });
347
- baseForScales = baseColor;
348
- }
294
+ const lightnessCurve = config.lightnessCurve ?? DEFAULT_PALETTE_LIGHTNESS();
295
+ const saturationCurve = config.saturationCurve ?? DEFAULT_PALETTE_SATURATION();
296
+ PALETTE_STEPS.forEach((ps, index) => {
297
+ const k = paletteStepKey(ps.label);
298
+ const hex = computePaletteColor(index, baseColor, lightnessCurve, saturationCurve, curveOffset);
299
+ const effective = k in overrides ? overrides[k] : hex;
300
+ out[`--color-${spec.cssNamespace}-${ps.label}`] = effective;
301
+ });
349
302
  for (const scale of SCALES) {
350
303
  for (const step of scale.steps) {
351
304
  const k = stepKey(scale.title, step.name);
352
- const hex = k in overrides ? overrides[k] : computeDerivedColor(step, baseForScales, scale.title, scaleCurves, curveOffset);
305
+ const hex = k in overrides ? overrides[k] : computeDerivedColor(step, baseColor, scale.title, scaleCurves, curveOffset);
353
306
  const varName = scaleToCssVar(scale.title, step.name, spec.cssNamespace);
354
307
  if (varName) out[varName] = hex;
355
308
  }
@@ -410,12 +363,7 @@ function reconcilePalettesFromCssVars(palettes, cssVars) {
410
363
  const anchorHex = cssVars[`--color-${spec.cssNamespace}-500`];
411
364
  if (anchorHex && HEX_RE.test(anchorHex.trim())) {
412
365
  const hex = anchorHex.trim();
413
- if (spec.mode === "gray") {
414
- const { c, h } = hexToOklch(hex);
415
- next[spec.label] = { ...current, tintHue: h, tintChroma: c, _imported: false };
416
- } else {
417
- next[spec.label] = { ...current, baseColor: hex, _imported: false };
418
- }
366
+ next[spec.label] = { ...current, baseColor: hex, _imported: false };
419
367
  snapped.add(spec.label);
420
368
  } else {
421
369
  next[spec.label] = { ...current, _imported: false };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@motion-proto/live-tokens",
3
- "version": "0.38.0",
3
+ "version": "0.40.0",
4
4
  "type": "module",
5
5
  "description": "Design token editor with live CSS variable editing. Svelte 5 + Vite 8.",
6
6
  "keywords": [
@@ -0,0 +1,175 @@
1
+ <script module lang="ts">
2
+ import { buildSiblings } from './scaffolding/siblings';
3
+ import type { Token } from './scaffolding/types';
4
+
5
+ export const component = 'iconbutton';
6
+
7
+ const variants = ['primary', 'secondary', 'outline', 'success', 'danger', 'warning'] as const;
8
+ type Variant = typeof variants[number];
9
+ const stateNames = ['default', 'hover', 'disabled'] as const;
10
+ type StateName = typeof stateNames[number];
11
+ function statePrefix(v: Variant, s: StateName): string {
12
+ return s === 'default' ? `--iconbutton-${v}` : `--iconbutton-${v}-${s}`;
13
+ }
14
+
15
+ // Shape. Icon-only, so unlike Button there is no text typography here — the
16
+ // base part carries only frame geometry, which links across variants.
17
+ function variantBaseTokens(v: Variant): Token[] {
18
+ return [
19
+ { label: 'padding', canBeLinked: true, groupKey: 'padding', variable: `--iconbutton-${v}-padding` },
20
+ { label: 'padding-top', canBeLinked: true, groupKey: 'padding-top', variable: `--iconbutton-${v}-padding-top`, hidden: true },
21
+ { label: 'padding-right', canBeLinked: true, groupKey: 'padding-right', variable: `--iconbutton-${v}-padding-right`, hidden: true },
22
+ { label: 'padding-bottom', canBeLinked: true, groupKey: 'padding-bottom', variable: `--iconbutton-${v}-padding-bottom`, hidden: true },
23
+ { label: 'padding-left', canBeLinked: true, groupKey: 'padding-left', variable: `--iconbutton-${v}-padding-left`, hidden: true },
24
+ { label: 'corner radius', canBeLinked: true, groupKey: 'radius', variable: `--iconbutton-${v}-radius` },
25
+ { label: 'border width', canBeLinked: true, groupKey: 'border-width', variable: `--iconbutton-${v}-border-width` },
26
+ { label: 'icon size', canBeLinked: true, groupKey: 'icon-size', variable: `--iconbutton-${v}-icon-size` },
27
+ ];
28
+ }
29
+
30
+ function variantStateTokens(v: Variant, s: StateName): Token[] {
31
+ const iconVar = s === 'default' ? `--iconbutton-${v}-icon` : `--iconbutton-${v}-${s}-icon`;
32
+ return [
33
+ { label: 'surface color', groupKey: 'surface', variable: `${statePrefix(v, s)}-surface` },
34
+ { label: 'border color', groupKey: 'border', variable: `${statePrefix(v, s)}-border` },
35
+ { label: 'icon color', groupKey: 'icon', variable: iconVar },
36
+ ];
37
+ }
38
+
39
+ // Outline is the only variant that paints a surface tint on :active.
40
+ const outlineActiveTokens: Token[] = [
41
+ { label: 'surface color', groupKey: 'surface', variable: '--iconbutton-outline-active-surface' },
42
+ ];
43
+
44
+ function variantStates(v: Variant): Record<string, Token[]> {
45
+ const out: Record<string, Token[]> = {};
46
+ out.base = variantBaseTokens(v);
47
+ out.default = variantStateTokens(v, 'default');
48
+ out.hover = variantStateTokens(v, 'hover');
49
+ if (v === 'outline') out.active = outlineActiveTokens;
50
+ out.disabled = variantStateTokens(v, 'disabled');
51
+ return out;
52
+ }
53
+
54
+ // Small-size schema. One shared spec across all variants (matches the runtime
55
+ // `.small` rule, which is variant-agnostic). Per-side padding rows are hidden
56
+ // and surface only when the editor splits the padding control.
57
+ const smallStates: Record<string, Token[]> = {
58
+ small: [
59
+ { label: 'padding', groupKey: 'small-padding', variable: '--iconbutton-small-padding' },
60
+ { label: 'padding-top', groupKey: 'small-padding-top', variable: '--iconbutton-small-padding-top', hidden: true },
61
+ { label: 'padding-right', groupKey: 'small-padding-right', variable: '--iconbutton-small-padding-right', hidden: true },
62
+ { label: 'padding-bottom', groupKey: 'small-padding-bottom', variable: '--iconbutton-small-padding-bottom', hidden: true },
63
+ { label: 'padding-left', groupKey: 'small-padding-left', variable: '--iconbutton-small-padding-left', hidden: true },
64
+ { label: 'icon size', groupKey: 'small-icon-size', variable: '--iconbutton-small-icon-size' },
65
+ ],
66
+ };
67
+ const smallTokensFlat: Token[] = Object.values(smallStates).flat();
68
+
69
+ export const allTokens: Token[] = [
70
+ ...variants.flatMap((v) => Object.values(variantStates(v)).flat()),
71
+ ...smallTokensFlat,
72
+ ];
73
+
74
+ // Frame geometry lives under each variant's "base" part and links across
75
+ // variants from there. Small tokens stay in their own namespace.
76
+ const linkableContexts = new Map<string, string>(
77
+ variants.flatMap((v) => [
78
+ [`--iconbutton-${v}-padding`, `${v} base`] as const,
79
+ [`--iconbutton-${v}-radius`, `${v} base`] as const,
80
+ [`--iconbutton-${v}-border-width`, `${v} base`] as const,
81
+ [`--iconbutton-${v}-icon-size`, `${v} base`] as const,
82
+ ]),
83
+ );
84
+
85
+ const variantOptions = variants.map((v) => ({ value: v, label: v.charAt(0).toUpperCase() + v.slice(1) }));
86
+
87
+ const previewIcons: Record<Variant, string> = {
88
+ primary: 'fas fa-star',
89
+ secondary: 'fas fa-gear',
90
+ outline: 'fas fa-pen',
91
+ success: 'fas fa-check',
92
+ danger: 'fas fa-trash',
93
+ warning: 'fas fa-triangle-exclamation',
94
+ };
95
+ </script>
96
+
97
+ <script lang="ts">
98
+ import IconButton from '../../system/components/IconButton.svelte';
99
+ import VariantGroup from './scaffolding/VariantGroup.svelte';
100
+ import ComponentEditorBase from './scaffolding/ComponentEditorBase.svelte';
101
+ import { editorState } from '../core/store/editorStore';
102
+ import { computeLinkedBlock, withLinkedDisabled } from './scaffolding/linkedBlock';
103
+
104
+ let previewSize = $state<'default' | 'small'>('default');
105
+
106
+ let linked = $derived(computeLinkedBlock(component, linkableContexts, allTokens, $editorState));
107
+
108
+ let visibleVariantStates = $derived((v: Variant) => Object.fromEntries(
109
+ Object.entries(variantStates(v)).map(([name, list]) => [name, withLinkedDisabled(list, linked.varSet)]),
110
+ ) as Record<string, Token[]>);
111
+
112
+ let visibleSmallStates = $derived(Object.fromEntries(
113
+ Object.entries(smallStates).map(([name, list]) => [name, withLinkedDisabled(list, linked.varSet)]),
114
+ ) as Record<string, Token[]>);
115
+
116
+ // At small size, no variant strip — the small spec is shared across all
117
+ // variants, so the strip would have nothing to navigate between.
118
+ let baseVariantOptions = $derived(previewSize === 'small' ? [] : variantOptions);
119
+ </script>
120
+
121
+ {#snippet sizeAction()}
122
+ <label>
123
+ <span>Size</span>
124
+ <select bind:value={previewSize}>
125
+ <option value="default">Default</option>
126
+ <option value="small">Small</option>
127
+ </select>
128
+ </label>
129
+ {/snippet}
130
+
131
+ <ComponentEditorBase {component} title="Icon Button" description="Icon-only button with the same variants, states and sizes as Button." tokens={allTokens} {linked} variants={baseVariantOptions}>
132
+ {#if previewSize === 'default'}
133
+ {#each variants as v}
134
+ <VariantGroup
135
+ name={v}
136
+ title={v.charAt(0).toUpperCase() + v.slice(1)}
137
+ states={visibleVariantStates(v)}
138
+ {component}
139
+ siblings={buildSiblings(variants, v, variantStates)}
140
+ previewActions={sizeAction}
141
+ >
142
+ {#snippet children({ activeState })}
143
+ {@const forceClass = activeState === 'hover' ? 'force-hover' : ''}
144
+ {@const isDisabled = activeState === 'disabled'}
145
+ <IconButton variant={v} icon={previewIcons[v]} ariaLabel={`${v} action`} disabled={isDisabled} class={forceClass} />
146
+ {/snippet}
147
+ </VariantGroup>
148
+ {/each}
149
+ {:else}
150
+ <VariantGroup
151
+ name="small"
152
+ title="Small"
153
+ states={visibleSmallStates}
154
+ {component}
155
+ previewActions={sizeAction}
156
+ >
157
+ {#snippet children()}
158
+ <div class="small-preview">
159
+ {#each variants as v}
160
+ <IconButton variant={v} size="small" icon={previewIcons[v]} ariaLabel={`${v} action`} />
161
+ {/each}
162
+ </div>
163
+ {/snippet}
164
+ </VariantGroup>
165
+ {/if}
166
+ </ComponentEditorBase>
167
+
168
+ <style>
169
+ .small-preview {
170
+ display: flex;
171
+ flex-wrap: wrap;
172
+ gap: var(--space-12);
173
+ align-items: center;
174
+ }
175
+ </style>
@@ -428,7 +428,7 @@
428
428
  {#snippet compositeControls(_stateName)}
429
429
  <div class="gradient-bg-section">
430
430
  <GradientEditor
431
- sectionLabel="Background"
431
+ sectionLabel="Surface"
432
432
  source={gradientSources[v.key]}
433
433
  stopIdPrefix={`sectiondivider-${v.key}`}
434
434
  familyFilter={getColorFamily(v.key)}
@@ -6,6 +6,7 @@ import BadgeEditor, { allTokens as badgeTokens } from './BadgeEditor.svelte';
6
6
  import CalloutEditor, { allTokens as calloutTokens } from './CalloutEditor.svelte';
7
7
  import CornerBadgeEditor, { allTokens as cornerBadgeTokens } from './CornerBadgeEditor.svelte';
8
8
  import ButtonEditor, { allTokens as buttonTokens } from './ButtonEditor.svelte';
9
+ import IconButtonEditor, { allTokens as iconButtonTokens } from './IconButtonEditor.svelte';
9
10
  import CardEditor, { allTokens as cardTokens, intrinsics as cardIntrinsics } from './CardEditor.svelte';
10
11
  import CodeSnippetEditor, { allTokens as codeSnippetTokens } from './CodeSnippetEditor.svelte';
11
12
  import CollapsibleSectionEditor, { allTokens as collapsibleSectionTokens } from './CollapsibleSectionEditor.svelte';
@@ -31,6 +32,7 @@ import TooltipEditor, { allTokens as tooltipTokens } from './TooltipEditor.svelt
31
32
  type BuiltInComponentId =
32
33
  | 'segmentedcontrol'
33
34
  | 'button'
35
+ | 'iconbutton'
34
36
  | 'notification'
35
37
  | 'dialog'
36
38
  | 'radiobutton'
@@ -108,6 +110,15 @@ const builtInRegistry: Readonly<Record<BuiltInComponentId, RegistryEntry>> = Obj
108
110
  schema: buttonTokens,
109
111
  origin: 'system',
110
112
  },
113
+ iconbutton: {
114
+ id: 'iconbutton',
115
+ label: 'Icon Button',
116
+ icon: 'fas fa-square-plus',
117
+ sourceFile: 'src/system/components/IconButton.svelte',
118
+ editorComponent: IconButtonEditor,
119
+ schema: iconButtonTokens,
120
+ origin: 'system',
121
+ },
111
122
  notification: {
112
123
  id: 'notification',
113
124
  label: 'Notification',