@motion-proto/live-tokens 0.40.0 → 0.41.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (89) hide show
  1. package/CHANGELOG.md +115 -0
  2. package/dist-plugin/{chunk-D77VD4Z6.js → chunk-H4TRUINI.js} +102 -13
  3. package/dist-plugin/index.cjs +288 -67
  4. package/dist-plugin/index.js +171 -55
  5. package/dist-plugin/tokensCssMigrations/index.cjs +102 -13
  6. package/dist-plugin/tokensCssMigrations/index.js +1 -1
  7. package/package.json +3 -2
  8. package/src/app/site.css +97 -20
  9. package/src/editor/core/palettes/colorHarmony.ts +228 -0
  10. package/src/editor/core/palettes/contrast.ts +128 -0
  11. package/src/editor/core/palettes/oklch.ts +8 -0
  12. package/src/editor/core/palettes/paletteDerivation.ts +267 -72
  13. package/src/editor/core/palettes/recommendText.ts +216 -0
  14. package/src/editor/core/palettes/solveTextContrast.ts +251 -0
  15. package/src/editor/core/store/editorCore.ts +2 -1
  16. package/src/editor/core/store/editorPersistence.ts +18 -1
  17. package/src/editor/core/store/editorStore.ts +20 -1
  18. package/src/editor/core/store/editorTypes.ts +4 -0
  19. package/src/editor/core/store/editorViewStore.ts +7 -3
  20. package/src/editor/core/themes/migrations/2026-06-05-palette-unification.ts +11 -9
  21. package/src/editor/core/themes/migrations/2026-07-21-palette-oklch-basis.ts +51 -0
  22. package/src/editor/core/themes/migrations/2026-07-24-base-anchor-placement.ts +37 -0
  23. package/src/editor/core/themes/migrations/2026-07-25-background-spot-to-base.ts +40 -0
  24. package/src/editor/core/themes/themeTypes.ts +23 -3
  25. package/src/editor/docs/CodeBlock.svelte +1 -1
  26. package/src/editor/docs/Docs.svelte +30 -13
  27. package/src/editor/pages/EditorShell.svelte +31 -3
  28. package/src/editor/ui/ColorEditPanel.svelte +22 -16
  29. package/src/editor/ui/EditorViewSwitcher.svelte +43 -9
  30. package/src/editor/ui/FontStackEditor.svelte +0 -1
  31. package/src/editor/ui/PaletteEditor.svelte +117 -155
  32. package/src/editor/ui/ProjectFontsSection.svelte +0 -3
  33. package/src/editor/ui/TextStylesSection.svelte +173 -0
  34. package/src/editor/ui/UILineHeightSelector.svelte +6 -5
  35. package/src/editor/ui/UIPaletteSelector.svelte +6 -1
  36. package/src/editor/ui/UITextTransformSelector.svelte +106 -0
  37. package/src/editor/ui/VariablesTab.svelte +23 -10
  38. package/src/editor/ui/colors/ColorReadouts.svelte +123 -0
  39. package/src/editor/ui/colors/ColorStory.svelte +375 -0
  40. package/src/editor/ui/colors/ColorWheel.svelte +740 -0
  41. package/src/editor/ui/colors/ColorsTab.svelte +508 -0
  42. package/src/editor/ui/colors/HarmonyAxesList.svelte +320 -0
  43. package/src/editor/ui/colors/LightnessBar.svelte +261 -0
  44. package/src/editor/ui/colors/PaletteStepStrip.svelte +142 -0
  45. package/src/editor/ui/colors/colorWheelMath.ts +9 -0
  46. package/src/editor/ui/colors/harmony-icons/analogous.svg +13 -0
  47. package/src/editor/ui/colors/harmony-icons/complementary.svg +10 -0
  48. package/src/editor/ui/colors/harmony-icons/compound.svg +15 -0
  49. package/src/editor/ui/colors/harmony-icons/custom.svg +10 -0
  50. package/src/editor/ui/colors/harmony-icons/monochromatic.svg +11 -0
  51. package/src/editor/ui/colors/harmony-icons/shades.svg +9 -0
  52. package/src/editor/ui/colors/harmony-icons/split-complementary.svg +13 -0
  53. package/src/editor/ui/colors/harmony-icons/square.svg +13 -0
  54. package/src/editor/ui/colors/harmony-icons/tetradic.svg +15 -0
  55. package/src/editor/ui/colors/harmony-icons/triadic.svg +13 -0
  56. package/src/editor/ui/colors/harmonyModeIcons.ts +23 -0
  57. package/src/editor/ui/colors/paletteBaseColor.ts +214 -0
  58. package/src/editor/ui/index.ts +0 -1
  59. package/src/editor/ui/palette/OverridesPanel.svelte +6 -3
  60. package/src/editor/ui/palette/PaletteBase.svelte +16 -8
  61. package/src/editor/ui/palette/dockMagnify.ts +22 -0
  62. package/src/editor/ui/palette/paletteEditorState.ts +5 -4
  63. package/src/editor/ui/palette/paletteMath.ts +93 -181
  64. package/src/editor/ui/sections/textStyles.ts +82 -0
  65. package/src/editor/ui/sections/tokenScales.ts +3 -3
  66. package/src/live-tokens/data/themes/default.json +3 -3
  67. package/src/live-tokens/data/tokens.generated.css +96 -95
  68. package/src/system/components/Badge.svelte +11 -11
  69. package/src/system/components/Button.svelte +7 -7
  70. package/src/system/components/Callout.svelte +8 -8
  71. package/src/system/components/Card.svelte +2 -2
  72. package/src/system/components/CodeSnippet.svelte +1 -1
  73. package/src/system/components/CollapsibleSection.svelte +7 -7
  74. package/src/system/components/CornerBadge.svelte +1 -1
  75. package/src/system/components/Dialog.svelte +2 -2
  76. package/src/system/components/FloatingTokenTags.css +6 -5
  77. package/src/system/components/Input.svelte +6 -6
  78. package/src/system/components/MenuSelect.svelte +4 -4
  79. package/src/system/components/Notification.svelte +8 -8
  80. package/src/system/components/ProgressBar.svelte +2 -2
  81. package/src/system/components/RadioButton.svelte +4 -4
  82. package/src/system/components/SectionDivider.svelte +6 -6
  83. package/src/system/components/SegmentedControl.svelte +5 -5
  84. package/src/system/components/SideNavigation.svelte +12 -12
  85. package/src/system/components/TabBar.svelte +4 -4
  86. package/src/system/components/Table.svelte +2 -2
  87. package/src/system/components/Tooltip.svelte +1 -1
  88. package/src/system/styles/tokens.css +59 -6
  89. package/src/editor/ui/TextTab.svelte +0 -203
@@ -19,23 +19,25 @@
19
19
  */
20
20
 
21
21
  import { type CurveAnchor, sampleCurve } from '../../../ui/curveEngine';
22
- import { hexToOklch, oklchToHex, gamutClamp } from '../../palettes/oklch';
22
+ import { oklchToHex, gamutClamp } from '../../palettes/oklch';
23
23
  import { DEFAULT_PALETTE_SATURATION } from '../../palettes/paletteDerivation';
24
- import type { PaletteConfig } from '../themeTypes';
24
+ import type { PreOklchPaletteConfig } from './2026-07-21-palette-oklch-basis';
25
25
 
26
26
  const NEUTRAL_LABELS = new Set(['Neutral', 'Alternate']);
27
27
 
28
28
  const GRAY_STEP_COUNT = 11;
29
29
  const GRAY_500_X = (4 / (GRAY_STEP_COUNT - 1)) * 100;
30
30
 
31
- /** Legacy shape: the gray fields union'd onto every saved palette config. */
31
+ /** Legacy shape: the gray fields union'd onto every saved palette config. This
32
+ * migration runs before the OKLCH-basis conversion, so color channels here are
33
+ * still hex strings. */
32
34
  type LegacyGrayFields = {
33
35
  tintHue?: number;
34
36
  tintChroma?: number;
35
37
  grayLightnessCurve?: CurveAnchor[];
36
38
  graySaturationCurve?: CurveAnchor[];
37
39
  };
38
- type LegacyPaletteConfig = PaletteConfig & LegacyGrayFields;
40
+ type LegacyPaletteConfig = PreOklchPaletteConfig & LegacyGrayFields;
39
41
 
40
42
  /** Reproduces the deleted `computeGrayColor` at step 500. */
41
43
  function effectiveGray500(cfg: LegacyPaletteConfig): string {
@@ -51,13 +53,13 @@ function effectiveGray500(cfg: LegacyPaletteConfig): string {
51
53
  return oklchToHex(clamped.l, clamped.c, clamped.h);
52
54
  }
53
55
 
54
- function stripGrayFields(cfg: LegacyPaletteConfig): PaletteConfig {
56
+ function stripGrayFields(cfg: LegacyPaletteConfig): PreOklchPaletteConfig {
55
57
  const { tintHue, tintChroma, grayLightnessCurve, graySaturationCurve, ...rest } = cfg;
56
58
  return rest;
57
59
  }
58
60
 
59
61
  /** Precondition: `cfg.grayLightnessCurve` is defined (caller-guarded). */
60
- function unifyNeutral(cfg: LegacyPaletteConfig): PaletteConfig {
62
+ function unifyNeutral(cfg: LegacyPaletteConfig): PreOklchPaletteConfig {
61
63
  const {
62
64
  'gray-lightness': grayLightnessOffset,
63
65
  'gray-saturation': _graySaturationOffset,
@@ -77,9 +79,9 @@ function unifyNeutral(cfg: LegacyPaletteConfig): PaletteConfig {
77
79
  }
78
80
 
79
81
  export function unifyGrayPalettes(
80
- editorConfigs: Record<string, PaletteConfig>,
81
- ): Record<string, PaletteConfig> {
82
- const out: Record<string, PaletteConfig> = {};
82
+ editorConfigs: Record<string, PreOklchPaletteConfig>,
83
+ ): Record<string, PreOklchPaletteConfig> {
84
+ const out: Record<string, PreOklchPaletteConfig> = {};
83
85
  for (const [label, cfg] of Object.entries(editorConfigs)) {
84
86
  const legacy = cfg as LegacyPaletteConfig;
85
87
  out[label] = NEUTRAL_LABELS.has(label) && legacy.grayLightnessCurve !== undefined
@@ -0,0 +1,51 @@
1
+ /**
2
+ * Palette color basis: hex → numeric OKLCH.
3
+ *
4
+ * The stored + edited basis moves from 8-bit hex to numeric OKLCH `{ l, c, h }`
5
+ * for every palette's `baseColor` and `overrides`. Lives outside the
6
+ * `runMigrations` framework (whose contract is the flat cssVariables bag)
7
+ * because it transforms the structured `editorConfigs` map — exactly like
8
+ * `renamePrimaryPaletteKey` / `unifyGrayPalettes`, which it composes with in
9
+ * `loadFromFile`. Applied unconditionally; idempotent (an already-numeric
10
+ * channel passes straight through) and self-sunsetting once every saved theme
11
+ * has been resaved in the numeric shape.
12
+ *
13
+ * The `_imported` anchor lives in `cssVariables` (a hex string) and is
14
+ * untouched here; the reconcile parser converts it at its own boundary.
15
+ */
16
+
17
+ import { hexToOklch, type Oklch } from '../../palettes/oklch';
18
+ import type { PaletteConfig } from '../themeTypes';
19
+
20
+ /** A config whose color channels may still be hex strings (pre-basis on disk)
21
+ * or already numeric (resaved). Lets the pre-OKLCH structural migrations stay
22
+ * honestly typed while this one is the boundary that produces `PaletteConfig`. */
23
+ export type PreOklchPaletteConfig = Omit<PaletteConfig, 'baseColor' | 'overrides'> & {
24
+ baseColor: string | Oklch;
25
+ overrides: Record<string, string | Oklch>;
26
+ };
27
+
28
+ const round = (v: number, dp: number): number => {
29
+ const f = 10 ** dp;
30
+ return Math.round(v * f) / f;
31
+ };
32
+
33
+ // Storage precision (decision 5): l, c to 4 decimals, h to 2 — beyond sub-LSB
34
+ // fidelity so the sRGB projection is unmoved, and stable across file diffs.
35
+ function toStoredOklch(value: string | Oklch): Oklch {
36
+ if (typeof value !== 'string') return value;
37
+ const { l, c, h } = hexToOklch(value);
38
+ return { l: round(l, 4), c: round(c, 4), h: round(h, 2) };
39
+ }
40
+
41
+ export function migratePaletteColorsToOklch(
42
+ editorConfigs: Record<string, PreOklchPaletteConfig>,
43
+ ): Record<string, PaletteConfig> {
44
+ const out: Record<string, PaletteConfig> = {};
45
+ for (const [label, cfg] of Object.entries(editorConfigs)) {
46
+ const overrides: Record<string, Oklch> = {};
47
+ for (const [k, v] of Object.entries(cfg.overrides ?? {})) overrides[k] = toStoredOklch(v);
48
+ out[label] = { ...cfg, baseColor: toStoredOklch(cfg.baseColor), overrides };
49
+ }
50
+ return out;
51
+ }
@@ -0,0 +1,37 @@
1
+ /**
2
+ * Base-anchor placement: adopt the legacy locked-500 anchor.
3
+ *
4
+ * The old "Lock base color to position 500" toggle injected a curve anchor at
5
+ * x=40 (step 500) but recorded nothing about it — the editor re-found it by
6
+ * x-position. Placement is now persisted as `anchorPlacement` (any step, not
7
+ * just 500), so configs that carry the legacy anchor adopt it as a placement
8
+ * at step index 4. Configs whose lock was on but inert (flag true, no anchor
9
+ * — the shipped default for most palettes) are left alone; their placement
10
+ * happens on the next base-color edit.
11
+ *
12
+ * Lives outside the `runMigrations` framework (whose contract is the flat
13
+ * cssVariables bag) because it transforms the structured `editorConfigs` map,
14
+ * exactly like `migratePaletteColorsToOklch`, which it composes with in
15
+ * `loadFromFile`. Applied unconditionally; idempotent (skips once
16
+ * `anchorPlacement` exists) and self-sunsetting once every saved theme has
17
+ * been resaved with a placement.
18
+ */
19
+
20
+ import { stepIndexToX } from '../../palettes/paletteDerivation';
21
+ import type { PaletteConfig } from '../themeTypes';
22
+
23
+ const LEGACY_LOCK_STEP = 4;
24
+
25
+ export function adoptLegacyBaseAnchor(
26
+ editorConfigs: Record<string, PaletteConfig>,
27
+ ): Record<string, PaletteConfig> {
28
+ const x500 = stepIndexToX(LEGACY_LOCK_STEP);
29
+ for (const cfg of Object.values(editorConfigs)) {
30
+ if (cfg.anchorToBase === false || cfg.anchorPlacement !== undefined) continue;
31
+ const idx = (cfg.lightnessCurve ?? []).findIndex((a) => Math.abs(a.x - x500) < 0.5);
32
+ if (idx > 0 && idx < cfg.lightnessCurve.length - 1) {
33
+ cfg.anchorPlacement = { step: LEGACY_LOCK_STEP };
34
+ }
35
+ }
36
+ return editorConfigs;
37
+ }
@@ -0,0 +1,40 @@
1
+ /**
2
+ * Background spot → base color: the solid page background used to be a
3
+ * separately selected palette step (`emptyStep`); it now IS the base color.
4
+ * To keep a saved theme's page rendering identical, the base color adopts the
5
+ * color the old spot actually showed (the derived value at that step, curves
6
+ * and overrides included) and `syncBaseAnchor` pins it there — the placement
7
+ * lands back on the old spot's step because the adopted L equals the curve at
8
+ * that x by construction. Gradient-mode configs keep their base untouched.
9
+ * The stray field is deleted either way so a resave drops it.
10
+ *
11
+ * Composes in `loadFromFile` after the OKLCH-basis migration (it reads a
12
+ * numeric baseColor). Idempotent (skips once `emptyStep` is gone) and
13
+ * self-sunsetting once every saved theme has been resaved.
14
+ */
15
+
16
+ import {
17
+ PALETTE_SPECS,
18
+ derivePaletteValues,
19
+ syncBaseAnchor,
20
+ } from '../../palettes/paletteDerivation';
21
+ import type { PaletteConfig } from '../themeTypes';
22
+
23
+ export function adoptBackgroundSpotAsBase(
24
+ editorConfigs: Record<string, PaletteConfig>,
25
+ ): Record<string, PaletteConfig> {
26
+ const spec = PALETTE_SPECS.find((s) => s.emptySelector)!;
27
+ for (const [label, cfg] of Object.entries(editorConfigs)) {
28
+ const legacy = cfg as PaletteConfig & { emptyStep?: string };
29
+ if (legacy.emptyStep === undefined) continue;
30
+ if (label === spec.label && (cfg.emptyMode ?? 'solid') === 'solid') {
31
+ const spot = derivePaletteValues(spec, cfg)[`--color-${spec.cssNamespace}-${legacy.emptyStep}`];
32
+ if (spot?.kind === 'color') {
33
+ cfg.baseColor = { l: spot.l, c: spot.c, h: spot.h };
34
+ syncBaseAnchor(cfg);
35
+ }
36
+ }
37
+ delete legacy.emptyStep;
38
+ }
39
+ return editorConfigs;
40
+ }
@@ -1,4 +1,6 @@
1
1
  import type { CurveAnchor } from '../../ui/curveEngine';
2
+ import type { Oklch } from '../palettes/oklch';
3
+ import type { HarmonyAxis } from '../palettes/colorHarmony';
2
4
 
3
5
  export type GradientStyle = 'linear' | 'radial' | 'conic';
4
6
 
@@ -8,21 +10,32 @@ export interface GradientStop {
8
10
  }
9
11
 
10
12
  export interface PaletteConfig {
11
- baseColor: string;
13
+ /** Numeric OKLCH intent — the single stored basis. Unclamped: the store holds
14
+ * what the user meant; gamut clamping is projection-only (derivation output,
15
+ * swatch/canvas painting, hex/CSS serialization). */
16
+ baseColor: Oklch;
12
17
  lightnessCurve: CurveAnchor[];
13
18
  saturationCurve: CurveAnchor[];
14
19
  scaleCurves: Record<string, { lightness: CurveAnchor[]; saturation: CurveAnchor[] }>;
15
20
  curveOffset: Record<string, number>;
16
- overrides: Record<string, string>;
21
+ overrides: Record<string, Oklch>;
17
22
  snappedScales: string[];
18
23
  emptyMode?: 'solid' | 'gradient';
19
- emptyStep?: string;
20
24
  gradientStyle?: GradientStyle;
21
25
  gradientAngle?: number;
22
26
  gradientReverse?: boolean;
23
27
  gradientStops?: GradientStop[];
24
28
  gradientSize?: 'page' | 'window';
25
29
  anchorToBase?: boolean;
30
+ /**
31
+ * Where the base color sits in the ramp while `anchorToBase` is on: the
32
+ * PALETTE_STEPS index whose curve anchors are pinned to the base color
33
+ * (lightness = base L, saturation = multiplier 100). `displacedL` /
34
+ * `displacedS` remember the y of a pre-existing anchor (endpoint or
35
+ * user-authored) the placement overwrote at that x, so moving the
36
+ * placement or toggling off restores it instead of deleting it.
37
+ */
38
+ anchorPlacement?: { step: number; displacedL?: number; displacedS?: number };
26
39
  /**
27
40
  * Set to true by importers when they overlay `cssVariables[--color-{ns}-*]`
28
41
  * without owning the typed-state curves. The storage-layer reconciler uses
@@ -78,6 +91,13 @@ export interface Theme {
78
91
  cssVariables: Record<string, string>;
79
92
  fontSources?: FontSource[];
80
93
  fontStacks?: FontStack[];
94
+ /**
95
+ * Deprecated legacy field. Read when `harmonyAxes` is absent; written on save
96
+ * for older builds.
97
+ */
98
+ harmonyOrder?: string[];
99
+ /** Four fixed harmony axes, each owning a hue with an optional bound family. */
100
+ harmonyAxes?: HarmonyAxis[];
81
101
  /**
82
102
  * Server-attached file-name marker for round-tripping the file identity
83
103
  * back to the client. Set by `themeFileApi`'s GET handlers; read by
@@ -47,7 +47,7 @@
47
47
  padding: var(--space-16, 1rem) var(--space-20, 1.25rem);
48
48
  overflow-x: auto;
49
49
  font-size: var(--font-size-sm, 0.875rem);
50
- line-height: var(--line-height-md, 1.6);
50
+ line-height: var(--line-height-normal, 1.6);
51
51
  }
52
52
 
53
53
  pre code {
@@ -398,17 +398,19 @@
398
398
  margin: 0;
399
399
  }
400
400
  .title-block .eyebrow {
401
- font-size: var(--font-size-xs, 0.75rem);
402
- font-weight: var(--font-weight-semibold, 600);
401
+ font-family: var(--eyebrow-font-family);
402
+ font-size: var(--eyebrow-font-size);
403
+ font-weight: var(--eyebrow-font-weight);
404
+ line-height: var(--eyebrow-line-height);
405
+ letter-spacing: var(--eyebrow-letter-spacing);
406
+ text-transform: var(--eyebrow-text-transform);
403
407
  color: var(--text-accent, #009d9a);
404
- text-transform: uppercase;
405
- letter-spacing: var(--letter-spacing-wider, 0.12em);
406
408
  margin: 0 0 var(--space-8, 0.5rem);
407
409
  }
408
410
  .title-block h1 {
409
411
  font-family: var(--font-display, var(--font-sans));
410
412
  font-size: var(--font-size-5xl, 3rem);
411
- line-height: var(--line-height-xs, 1.1);
413
+ line-height: var(--line-height-none, 1.1);
412
414
  letter-spacing: var(--letter-spacing-tight, -0.02em);
413
415
  margin: 0 0 var(--space-12, 0.75rem);
414
416
  color: var(--text-primary);
@@ -490,7 +492,7 @@
490
492
 
491
493
  .prose {
492
494
  font-size: var(--font-size-md, 1rem);
493
- line-height: var(--line-height-lg, 1.65);
495
+ line-height: var(--line-height-relaxed, 1.65);
494
496
  }
495
497
  .md-snippet {
496
498
  margin: 0 0 var(--space-20, 1.25rem);
@@ -517,25 +519,40 @@
517
519
  .prose :global(h2),
518
520
  .prose :global(h3),
519
521
  .prose :global(h4) {
520
- font-family: var(--font-display, var(--font-sans));
521
- font-weight: var(--font-weight-semibold, 600);
522
- letter-spacing: var(--letter-spacing-tight, -0.015em);
523
- line-height: var(--line-height-sm, 1.2);
524
522
  color: var(--text-primary);
525
523
  position: relative;
526
524
  }
525
+ .prose :global(h1) {
526
+ font-family: var(--heading-xl-font-family);
527
+ font-size: var(--heading-xl-font-size);
528
+ font-weight: var(--heading-xl-font-weight);
529
+ line-height: var(--heading-xl-line-height);
530
+ letter-spacing: var(--heading-xl-letter-spacing);
531
+ }
527
532
  .prose :global(h2) {
528
- font-size: var(--font-size-2xl, 1.5rem);
533
+ font-family: var(--heading-lg-font-family);
534
+ font-size: var(--heading-lg-font-size);
535
+ font-weight: var(--heading-lg-font-weight);
536
+ line-height: var(--heading-lg-line-height);
537
+ letter-spacing: var(--heading-lg-letter-spacing);
529
538
  margin: var(--space-48, 3rem) 0 var(--space-16, 1rem);
530
539
  padding-top: var(--space-12, 0.75rem);
531
540
  border-top: var(--border-width-1, 1px) solid var(--border-neutral-faint, #1c2327);
532
541
  }
533
542
  .prose :global(h3) {
534
- font-size: var(--font-size-xl, 1.25rem);
543
+ font-family: var(--heading-md-font-family);
544
+ font-size: var(--heading-md-font-size);
545
+ font-weight: var(--heading-md-font-weight);
546
+ line-height: var(--heading-md-line-height);
547
+ letter-spacing: var(--heading-md-letter-spacing);
535
548
  margin: var(--space-32, 2rem) 0 var(--space-12, 0.75rem);
536
549
  }
537
550
  .prose :global(h4) {
538
- font-size: var(--font-size-lg, 1.125rem);
551
+ font-family: var(--heading-sm-font-family);
552
+ font-size: var(--heading-sm-font-size);
553
+ font-weight: var(--heading-sm-font-weight);
554
+ line-height: var(--heading-sm-line-height);
555
+ letter-spacing: var(--heading-sm-letter-spacing);
539
556
  margin: var(--space-24, 1.5rem) 0 var(--space-8, 0.5rem);
540
557
  color: var(--text-secondary);
541
558
  }
@@ -4,6 +4,7 @@
4
4
  import { onMount } from 'svelte';
5
5
  import { get } from 'svelte/store';
6
6
  import VariablesTab from '../ui/VariablesTab.svelte';
7
+ import ColorsTab from '../ui/colors/ColorsTab.svelte';
7
8
  import ThemeFileManager from '../ui/ThemeFileManager.svelte';
8
9
  import EditorViewSwitcher from '../ui/EditorViewSwitcher.svelte';
9
10
  import ComponentsTab from '../component-editor/scaffolding/ComponentsTab.svelte';
@@ -29,6 +30,11 @@
29
30
  { id: 'utility-tokens', label: 'Utility Tokens', icon: 'fas fa-sliders' }
30
31
  ];
31
32
 
33
+ const colorsNavItems = [
34
+ { id: 'colors-wheel', label: 'Wheel', icon: 'fas fa-palette' },
35
+ { id: 'colors-story', label: 'Story', icon: 'fas fa-layer-group' }
36
+ ];
37
+
32
38
  const allComponentNavItems = getComponentRegistryEntries().map(({ id, label, icon, origin }) => ({ id, label, icon, origin }));
33
39
  const systemNavItems = allComponentNavItems.filter((i) => i.origin === 'system');
34
40
  const customNavItems = allComponentNavItems.filter((i) => i.origin === 'custom');
@@ -131,7 +137,27 @@
131
137
 
132
138
  {#if $editorView === 'tokens'}
133
139
  <div class="nav-items">
134
- {#each tokenNavItems as item}
140
+ {#each tokenNavItems as item (item.id)}
141
+ <button
142
+ class="nav-item"
143
+ class:active={selectedTokenSection === item.id}
144
+ onmouseenter={(e) => showHint(item.label, e.currentTarget)}
145
+ onmouseleave={hideHint}
146
+ onclick={() => scrollToSection(item.id)}
147
+ >
148
+ <i class={item.icon}></i>
149
+ <span class="nav-label">{item.label}</span>
150
+ </button>
151
+ {/each}
152
+ </div>
153
+ {#if !condensed}
154
+ <div class="sidebar-footer">
155
+ <ThemeFileManager {saveStatus} onsave={handleSave} onload={handleLoad} />
156
+ </div>
157
+ {/if}
158
+ {:else if $editorView === 'colors'}
159
+ <div class="nav-items">
160
+ {#each colorsNavItems as item (item.id)}
135
161
  <button
136
162
  class="nav-item"
137
163
  class:active={selectedTokenSection === item.id}
@@ -151,7 +177,7 @@
151
177
  {/if}
152
178
  {:else}
153
179
  <div class="nav-items">
154
- {#each systemNavItems as item}
180
+ {#each systemNavItems as item (item.id)}
155
181
  <button
156
182
  class="nav-item"
157
183
  class:active={$selectedComponent === item.id}
@@ -171,7 +197,7 @@
171
197
  <div class="nav-divider">
172
198
  <span class="nav-divider-label">Custom</span>
173
199
  </div>
174
- {#each customNavItems as item}
200
+ {#each customNavItems as item (item.id)}
175
201
  <button
176
202
  class="nav-item"
177
203
  class:active={$selectedComponent === item.id}
@@ -200,6 +226,8 @@
200
226
  <main class="content">
201
227
  {#if $editorView === 'tokens'}
202
228
  <VariablesTab />
229
+ {:else if $editorView === 'colors'}
230
+ <ColorsTab />
203
231
  {:else}
204
232
  <ComponentsTab selectedComponent={$selectedComponent} />
205
233
  {/if}
@@ -9,6 +9,9 @@
9
9
  interface Props {
10
10
  title?: string | null;
11
11
  showRemoveOverride?: boolean;
12
+ /** Hide the confirm/cancel actions for live-apply consumers (Colors view
13
+ * readout) where edits commit through the store as they happen. */
14
+ hideActions?: boolean;
12
15
  onConfirm?: () => void;
13
16
  onCancel?: () => void;
14
17
  onRemoveOverride?: () => void;
@@ -32,6 +35,7 @@
32
35
  let {
33
36
  title = null,
34
37
  showRemoveOverride = false,
38
+ hideActions = false,
35
39
  onConfirm = () => {},
36
40
  onCancel = () => {},
37
41
  onRemoveOverride = () => {},
@@ -154,22 +158,24 @@
154
158
  {/if}
155
159
  <code class="hsl-values">oklch({(lPct / 100).toFixed(2)}, {chroma.toFixed(3)}, {Math.round(hue)})</code>
156
160
  {@render actions?.()}
157
- <div class="hsl-panel-actions">
158
- {#if showRemoveOverride}
159
- <Button
160
- variant="danger"
161
- size="small"
162
- icon="fas fa-trash"
163
- on:click={onRemoveOverride}
164
- >Remove override</Button>
165
- {/if}
166
- <InlineEditActions
167
- onSave={onConfirm}
168
- onCancel={onCancel}
169
- saveTitle="Apply changes"
170
- cancelTitle="Discard changes"
171
- />
172
- </div>
161
+ {#if !hideActions}
162
+ <div class="hsl-panel-actions">
163
+ {#if showRemoveOverride}
164
+ <Button
165
+ variant="danger"
166
+ size="small"
167
+ icon="fas fa-trash"
168
+ on:click={onRemoveOverride}
169
+ >Remove override</Button>
170
+ {/if}
171
+ <InlineEditActions
172
+ onSave={onConfirm}
173
+ onCancel={onCancel}
174
+ saveTitle="Apply changes"
175
+ cancelTitle="Discard changes"
176
+ />
177
+ </div>
178
+ {/if}
173
179
  </div>
174
180
  <div class="hsl-sliders">
175
181
  <div class="hsl-slider-row">
@@ -1,5 +1,5 @@
1
1
  <script lang="ts">
2
- import { editorView } from '../core/store/editorViewStore';
2
+ import { editorView, type EditorView } from '../core/store/editorViewStore';
3
3
  import { parentRoute } from '../core/routing/parentRouteStore';
4
4
  import { DEFAULT_COMPONENTS_PATH } from '../core/routing/ownedRoutes';
5
5
 
@@ -17,13 +17,36 @@
17
17
  // relocated route won't disable the switch.
18
18
  let componentsDisabled = $derived($parentRoute === DEFAULT_COMPONENTS_PATH);
19
19
 
20
- function set(v: 'tokens' | 'components') {
20
+ // Editing flow order: Tokens → Colors → Components. The condensed rail cycles
21
+ // through these; Components is skipped while it's disabled (already on that
22
+ // page) so the cycle never lands on a dead view.
23
+ const CYCLE: readonly EditorView[] = ['tokens', 'colors', 'components'];
24
+ const ICONS: Record<EditorView, string> = {
25
+ tokens: 'fa-sliders',
26
+ colors: 'fa-palette',
27
+ components: 'fa-cubes',
28
+ };
29
+ const LABELS: Record<EditorView, string> = {
30
+ tokens: 'Tokens',
31
+ colors: 'Colors',
32
+ components: 'Components',
33
+ };
34
+
35
+ function set(v: EditorView) {
21
36
  editorView.set(v);
22
37
  }
23
38
 
24
- function toggle() {
25
- if (componentsDisabled) return;
26
- editorView.update((v) => (v === 'tokens' ? 'components' : 'tokens'));
39
+ function cycle() {
40
+ editorView.update((current) => {
41
+ let i = CYCLE.indexOf(current);
42
+ for (let n = 0; n < CYCLE.length; n++) {
43
+ i = (i + 1) % CYCLE.length;
44
+ const next = CYCLE[i];
45
+ if (next === 'components' && componentsDisabled) continue;
46
+ return next;
47
+ }
48
+ return current;
49
+ });
27
50
  }
28
51
  </script>
29
52
 
@@ -31,11 +54,11 @@
31
54
  <button
32
55
  type="button"
33
56
  class="compact"
34
- aria-label={$editorView === 'tokens' ? 'Switch to components' : 'Switch to tokens'}
35
- title={$editorView === 'tokens' ? 'Tokens (click for components)' : 'Components (click for tokens)'}
36
- onclick={toggle}
57
+ aria-label={`Editor view: ${LABELS[$editorView]} (click to cycle)`}
58
+ title={`${LABELS[$editorView]} (click to cycle views)`}
59
+ onclick={cycle}
37
60
  >
38
- <i class="fas {$editorView === 'tokens' ? 'fa-palette' : 'fa-cubes'}"></i>
61
+ <i class="fas {ICONS[$editorView]}"></i>
39
62
  </button>
40
63
  {:else}
41
64
  <div class="seg-group">
@@ -52,6 +75,17 @@
52
75
  <span class="radio" aria-hidden="true"></span>
53
76
  <span>Tokens</span>
54
77
  </button>
78
+ <button
79
+ type="button"
80
+ role="tab"
81
+ class="seg-btn"
82
+ class:active={$editorView === 'colors'}
83
+ aria-selected={$editorView === 'colors'}
84
+ onclick={() => set('colors')}
85
+ >
86
+ <span class="radio" aria-hidden="true"></span>
87
+ <span>Colors</span>
88
+ </button>
55
89
  <button
56
90
  type="button"
57
91
  role="tab"
@@ -381,7 +381,6 @@
381
381
  transform var(--ui-transition-fast),
382
382
  border-color var(--ui-transition-fast);
383
383
  }
384
- .slot-row:hover { border-color: var(--ui-border); }
385
384
 
386
385
  .slot-controls {
387
386
  display: grid;