@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.
- package/.claude/skills/live-tokens-pick-component/SKILL.md +9 -1
- package/CHANGELOG.md +71 -0
- package/dist-plugin/index.cjs +20 -72
- package/dist-plugin/index.js +20 -72
- package/package.json +1 -1
- package/src/editor/component-editor/IconButtonEditor.svelte +175 -0
- package/src/editor/component-editor/SectionDividerEditor.svelte +1 -1
- package/src/editor/component-editor/registry.ts +11 -0
- package/src/editor/core/palettes/paletteDerivation.ts +28 -83
- package/src/editor/core/store/editorStore.ts +2 -1
- package/src/editor/core/store/gradientSource.ts +49 -9
- package/src/editor/core/themes/migrations/2026-06-05-palette-unification.ts +90 -0
- package/src/editor/core/themes/themeTypes.ts +3 -8
- package/src/editor/docs/content/01-overview.md +3 -2
- package/src/editor/docs/content.generated.ts +1 -1
- package/src/editor/ui/ColorEditPanel.svelte +93 -172
- package/src/editor/ui/PaletteEditor.svelte +46 -207
- package/src/editor/ui/VariablesTab.svelte +2 -2
- package/src/editor/ui/palette/PaletteBase.svelte +29 -37
- package/src/editor/ui/palette/paletteEditorState.ts +12 -25
- package/src/editor/ui/palette/paletteMath.ts +22 -49
- package/src/live-tokens/data/themes/default.json +11 -391
- package/src/live-tokens/data/tokens.generated.css +14 -14
- package/src/system/components/IconButton.svelte +322 -0
|
@@ -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
|
package/dist-plugin/index.cjs
CHANGED
|
@@ -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",
|
|
196
|
-
{ label: "Alternate", cssNamespace: "alternate",
|
|
197
|
-
{ label: "Background", cssNamespace: "canvas",
|
|
198
|
-
{ label: "Brand", cssNamespace: "brand",
|
|
199
|
-
{ label: "Accent", cssNamespace: "accent",
|
|
200
|
-
{ label: "Special", cssNamespace: "special",
|
|
201
|
-
{ label: "Info", cssNamespace: "info",
|
|
202
|
-
{ label: "Success", cssNamespace: "success",
|
|
203
|
-
{ label: "Warning", cssNamespace: "warning",
|
|
204
|
-
{ label: "Danger", cssNamespace: "danger",
|
|
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
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
const
|
|
368
|
-
const
|
|
369
|
-
const
|
|
370
|
-
|
|
371
|
-
|
|
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,
|
|
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
|
-
|
|
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 };
|
package/dist-plugin/index.js
CHANGED
|
@@ -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",
|
|
155
|
-
{ label: "Alternate", cssNamespace: "alternate",
|
|
156
|
-
{ label: "Background", cssNamespace: "canvas",
|
|
157
|
-
{ label: "Brand", cssNamespace: "brand",
|
|
158
|
-
{ label: "Accent", cssNamespace: "accent",
|
|
159
|
-
{ label: "Special", cssNamespace: "special",
|
|
160
|
-
{ label: "Info", cssNamespace: "info",
|
|
161
|
-
{ label: "Success", cssNamespace: "success",
|
|
162
|
-
{ label: "Warning", cssNamespace: "warning",
|
|
163
|
-
{ label: "Danger", cssNamespace: "danger",
|
|
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
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
const
|
|
327
|
-
const
|
|
328
|
-
const
|
|
329
|
-
|
|
330
|
-
|
|
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,
|
|
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
|
-
|
|
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
|
@@ -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="
|
|
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',
|