@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.
- package/CHANGELOG.md +115 -0
- package/dist-plugin/{chunk-D77VD4Z6.js → chunk-H4TRUINI.js} +102 -13
- package/dist-plugin/index.cjs +288 -67
- package/dist-plugin/index.js +171 -55
- package/dist-plugin/tokensCssMigrations/index.cjs +102 -13
- package/dist-plugin/tokensCssMigrations/index.js +1 -1
- package/package.json +3 -2
- package/src/app/site.css +97 -20
- package/src/editor/core/palettes/colorHarmony.ts +228 -0
- package/src/editor/core/palettes/contrast.ts +128 -0
- package/src/editor/core/palettes/oklch.ts +8 -0
- package/src/editor/core/palettes/paletteDerivation.ts +267 -72
- package/src/editor/core/palettes/recommendText.ts +216 -0
- package/src/editor/core/palettes/solveTextContrast.ts +251 -0
- package/src/editor/core/store/editorCore.ts +2 -1
- package/src/editor/core/store/editorPersistence.ts +18 -1
- package/src/editor/core/store/editorStore.ts +20 -1
- package/src/editor/core/store/editorTypes.ts +4 -0
- package/src/editor/core/store/editorViewStore.ts +7 -3
- package/src/editor/core/themes/migrations/2026-06-05-palette-unification.ts +11 -9
- package/src/editor/core/themes/migrations/2026-07-21-palette-oklch-basis.ts +51 -0
- package/src/editor/core/themes/migrations/2026-07-24-base-anchor-placement.ts +37 -0
- package/src/editor/core/themes/migrations/2026-07-25-background-spot-to-base.ts +40 -0
- package/src/editor/core/themes/themeTypes.ts +23 -3
- package/src/editor/docs/CodeBlock.svelte +1 -1
- package/src/editor/docs/Docs.svelte +30 -13
- package/src/editor/pages/EditorShell.svelte +31 -3
- package/src/editor/ui/ColorEditPanel.svelte +22 -16
- package/src/editor/ui/EditorViewSwitcher.svelte +43 -9
- package/src/editor/ui/FontStackEditor.svelte +0 -1
- package/src/editor/ui/PaletteEditor.svelte +117 -155
- package/src/editor/ui/ProjectFontsSection.svelte +0 -3
- package/src/editor/ui/TextStylesSection.svelte +173 -0
- package/src/editor/ui/UILineHeightSelector.svelte +6 -5
- package/src/editor/ui/UIPaletteSelector.svelte +6 -1
- package/src/editor/ui/UITextTransformSelector.svelte +106 -0
- package/src/editor/ui/VariablesTab.svelte +23 -10
- package/src/editor/ui/colors/ColorReadouts.svelte +123 -0
- package/src/editor/ui/colors/ColorStory.svelte +375 -0
- package/src/editor/ui/colors/ColorWheel.svelte +740 -0
- package/src/editor/ui/colors/ColorsTab.svelte +508 -0
- package/src/editor/ui/colors/HarmonyAxesList.svelte +320 -0
- package/src/editor/ui/colors/LightnessBar.svelte +261 -0
- package/src/editor/ui/colors/PaletteStepStrip.svelte +142 -0
- package/src/editor/ui/colors/colorWheelMath.ts +9 -0
- package/src/editor/ui/colors/harmony-icons/analogous.svg +13 -0
- package/src/editor/ui/colors/harmony-icons/complementary.svg +10 -0
- package/src/editor/ui/colors/harmony-icons/compound.svg +15 -0
- package/src/editor/ui/colors/harmony-icons/custom.svg +10 -0
- package/src/editor/ui/colors/harmony-icons/monochromatic.svg +11 -0
- package/src/editor/ui/colors/harmony-icons/shades.svg +9 -0
- package/src/editor/ui/colors/harmony-icons/split-complementary.svg +13 -0
- package/src/editor/ui/colors/harmony-icons/square.svg +13 -0
- package/src/editor/ui/colors/harmony-icons/tetradic.svg +15 -0
- package/src/editor/ui/colors/harmony-icons/triadic.svg +13 -0
- package/src/editor/ui/colors/harmonyModeIcons.ts +23 -0
- package/src/editor/ui/colors/paletteBaseColor.ts +214 -0
- package/src/editor/ui/index.ts +0 -1
- package/src/editor/ui/palette/OverridesPanel.svelte +6 -3
- package/src/editor/ui/palette/PaletteBase.svelte +16 -8
- package/src/editor/ui/palette/dockMagnify.ts +22 -0
- package/src/editor/ui/palette/paletteEditorState.ts +5 -4
- package/src/editor/ui/palette/paletteMath.ts +93 -181
- package/src/editor/ui/sections/textStyles.ts +82 -0
- package/src/editor/ui/sections/tokenScales.ts +3 -3
- package/src/live-tokens/data/themes/default.json +3 -3
- package/src/live-tokens/data/tokens.generated.css +96 -95
- package/src/system/components/Badge.svelte +11 -11
- package/src/system/components/Button.svelte +7 -7
- package/src/system/components/Callout.svelte +8 -8
- package/src/system/components/Card.svelte +2 -2
- package/src/system/components/CodeSnippet.svelte +1 -1
- package/src/system/components/CollapsibleSection.svelte +7 -7
- package/src/system/components/CornerBadge.svelte +1 -1
- package/src/system/components/Dialog.svelte +2 -2
- package/src/system/components/FloatingTokenTags.css +6 -5
- package/src/system/components/Input.svelte +6 -6
- package/src/system/components/MenuSelect.svelte +4 -4
- package/src/system/components/Notification.svelte +8 -8
- package/src/system/components/ProgressBar.svelte +2 -2
- package/src/system/components/RadioButton.svelte +4 -4
- package/src/system/components/SectionDivider.svelte +6 -6
- package/src/system/components/SegmentedControl.svelte +5 -5
- package/src/system/components/SideNavigation.svelte +12 -12
- package/src/system/components/TabBar.svelte +4 -4
- package/src/system/components/Table.svelte +2 -2
- package/src/system/components/Tooltip.svelte +1 -1
- package/src/system/styles/tokens.css +59 -6
- 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 {
|
|
22
|
+
import { oklchToHex, gamutClamp } from '../../palettes/oklch';
|
|
23
23
|
import { DEFAULT_PALETTE_SATURATION } from '../../palettes/paletteDerivation';
|
|
24
|
-
import type {
|
|
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 =
|
|
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):
|
|
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):
|
|
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,
|
|
81
|
-
): Record<string,
|
|
82
|
-
const out: Record<string,
|
|
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
|
-
|
|
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,
|
|
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
|
|
@@ -398,17 +398,19 @@
|
|
|
398
398
|
margin: 0;
|
|
399
399
|
}
|
|
400
400
|
.title-block .eyebrow {
|
|
401
|
-
font-
|
|
402
|
-
font-
|
|
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-
|
|
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-
|
|
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-
|
|
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-
|
|
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-
|
|
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
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
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
|
-
|
|
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
|
|
25
|
-
|
|
26
|
-
|
|
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
|
|
35
|
-
title={$editorView
|
|
36
|
-
onclick={
|
|
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
|
|
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"
|