@motion-proto/live-tokens 0.1.1 → 0.3.2
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/README.md +168 -21
- package/dist-plugin/index.cjs +823 -336
- package/dist-plugin/index.d.cts +9 -7
- package/dist-plugin/index.d.ts +9 -7
- package/dist-plugin/index.js +822 -335
- package/package.json +46 -20
- package/src/assets/newspaper.webp +0 -0
- package/src/assets/offering.webp +0 -0
- package/src/component-editor/BadgeEditor.svelte +170 -0
- package/src/component-editor/CalloutEditor.svelte +103 -0
- package/src/component-editor/CardEditor.svelte +184 -0
- package/src/component-editor/CollapsibleSectionEditor.svelte +167 -0
- package/src/component-editor/CornerBadgeEditor.svelte +207 -0
- package/src/component-editor/DialogEditor.svelte +172 -0
- package/src/component-editor/ImageEditor.svelte +72 -0
- package/src/component-editor/InlineEditActionsEditor.svelte +83 -0
- package/src/component-editor/NotificationEditor.svelte +160 -0
- package/src/component-editor/ProgressBarEditor.svelte +124 -0
- package/src/component-editor/RadioButtonEditor.svelte +140 -0
- package/src/component-editor/SectionDividerEditor.svelte +263 -0
- package/src/component-editor/SegmentedControlEditor.svelte +154 -0
- package/src/component-editor/StandardButtonsEditor.svelte +178 -0
- package/src/component-editor/TabBarEditor.svelte +137 -0
- package/src/component-editor/TableEditor.svelte +128 -0
- package/src/component-editor/TooltipEditor.svelte +122 -0
- package/src/component-editor/editorTokens.test.ts +93 -0
- package/src/component-editor/groupKeySlots.test.ts +67 -0
- package/src/component-editor/groupKeySnapshot.test.ts +52 -0
- package/src/component-editor/index.ts +5 -0
- package/src/component-editor/registry.ts +246 -0
- package/src/component-editor/scaffolding/AngleDial.svelte +185 -0
- package/src/component-editor/scaffolding/ComponentEditorBase.svelte +96 -0
- package/src/component-editor/scaffolding/ComponentFileManager.svelte +682 -0
- package/src/component-editor/scaffolding/ComponentFileMenu.svelte +312 -0
- package/src/component-editor/scaffolding/ComponentsTab.svelte +69 -0
- package/src/component-editor/scaffolding/CopyFromMenu.svelte +246 -0
- package/src/component-editor/scaffolding/DemoHeader.svelte +21 -0
- package/src/component-editor/scaffolding/DividerEditor.svelte +81 -0
- package/src/component-editor/scaffolding/FieldsetWrapper.svelte +46 -0
- package/src/component-editor/scaffolding/GradientCard.svelte +291 -0
- package/src/component-editor/scaffolding/LinkageChart.svelte +297 -0
- package/src/component-editor/scaffolding/LinkedBlock.svelte +418 -0
- package/src/component-editor/scaffolding/NonStylableConfig.svelte +57 -0
- package/src/component-editor/scaffolding/SaveAsDialog.svelte +177 -0
- package/src/component-editor/scaffolding/ShadowBackdrop.svelte +25 -0
- package/src/component-editor/scaffolding/ShadowBackdropControls.svelte +56 -0
- package/src/component-editor/scaffolding/StateBlock.svelte +115 -0
- package/src/component-editor/scaffolding/TokenLayout.svelte +511 -0
- package/src/component-editor/scaffolding/TypeEditor.svelte +82 -0
- package/src/component-editor/scaffolding/VariantGroup.svelte +277 -0
- package/src/component-editor/scaffolding/buildTypeGroupTokens.ts +97 -0
- package/src/component-editor/scaffolding/componentSectionType.ts +8 -0
- package/src/component-editor/scaffolding/componentSources.ts +9 -0
- package/src/component-editor/scaffolding/defaultSections.ts +16 -0
- package/src/component-editor/scaffolding/editorContext.ts +44 -0
- package/src/component-editor/scaffolding/linkedBlock.ts +226 -0
- package/src/component-editor/scaffolding/siblings.ts +33 -0
- package/src/component-editor/scaffolding/types.ts +39 -0
- package/src/components/Badge.svelte +231 -42
- package/src/components/Button.svelte +324 -124
- package/src/components/Callout.svelte +145 -0
- package/src/components/Card.svelte +123 -25
- package/src/components/CollapsibleSection.svelte +213 -35
- package/src/components/CornerBadge.svelte +224 -0
- package/src/components/Dialog.svelte +137 -114
- package/src/components/Image.svelte +43 -0
- package/src/components/InlineEditActions.svelte +74 -14
- package/src/components/Notification.svelte +184 -163
- package/src/components/ProgressBar.svelte +216 -22
- package/src/components/RadioButton.svelte +110 -40
- package/src/components/SectionDivider.svelte +428 -74
- package/src/components/SegmentedControl.svelte +203 -0
- package/src/components/TabBar.svelte +146 -21
- package/src/components/Table.svelte +102 -0
- package/src/components/Tooltip.svelte +45 -19
- package/src/components/types.ts +51 -0
- package/src/data/google-fonts.json +75 -0
- package/src/lib/ColumnsOverlay.svelte +20 -7
- package/src/lib/LiveEditorOverlay.svelte +257 -78
- package/src/lib/columnsOverlay.ts +21 -17
- package/src/lib/componentConfig.test.ts +204 -0
- package/src/lib/componentConfigKeys.ts +19 -0
- package/src/lib/componentConfigService.ts +88 -0
- package/src/lib/copyPopover.ts +30 -0
- package/src/lib/cssVarSync.ts +59 -7
- package/src/lib/editorConfigStore.ts +0 -10
- package/src/lib/editorCore.ts +402 -0
- package/src/lib/editorKeybindings.ts +52 -0
- package/src/lib/editorPersistence.ts +106 -0
- package/src/lib/editorRenderer.ts +74 -0
- package/src/lib/editorStore.test.ts +328 -0
- package/src/lib/editorStore.ts +412 -0
- package/src/lib/editorTypes.ts +100 -0
- package/src/lib/editorViewStore.ts +55 -0
- package/src/lib/files/versionedFileResource.ts +140 -0
- package/src/lib/fontLoader.ts +130 -0
- package/src/lib/fontMigration.ts +140 -0
- package/src/lib/fontParse.ts +168 -0
- package/src/lib/index.ts +48 -30
- package/src/lib/lazyConfig.test.ts +54 -0
- package/src/lib/migrations/2026-04-24-component-prefix-and-suffix-renames.ts +64 -0
- package/src/lib/migrations/2026-04-24-legacy-keys-and-bg-to-canvas.ts +71 -0
- package/src/lib/migrations/2026-04-27-segmentedcontrol-disabled-flatten.ts +43 -0
- package/src/lib/migrations/2026-05-08-collapsiblesection-frame-and-cleanup.ts +68 -0
- package/src/lib/migrations/2026-05-08-collapsiblesection-variant-namespace.ts +35 -0
- package/src/lib/migrations/2026-05-10-sectiondivider-gradient-stops.ts +50 -0
- package/src/lib/migrations/2026-05-13-primary-to-brand.ts +90 -0
- package/src/lib/migrations/index.ts +93 -0
- package/src/lib/migrations/migrations.test.ts +341 -0
- package/src/lib/navLinkTypes.ts +1 -0
- package/src/lib/overlayState.ts +3 -0
- package/src/lib/paletteDerivation.ts +300 -0
- package/src/lib/parentRouteStore.ts +42 -0
- package/src/lib/parsers/globalRootBlock.ts +32 -0
- package/src/lib/presetService.ts +94 -0
- package/src/lib/router.ts +42 -10
- package/src/lib/scrollSection.ts +45 -0
- package/src/lib/slices/columns.ts +59 -0
- package/src/lib/slices/components.ts +362 -0
- package/src/lib/slices/domainVars.ts +15 -0
- package/src/lib/slices/fonts.ts +30 -0
- package/src/lib/slices/gradients.ts +153 -0
- package/src/lib/slices/overlays.ts +132 -0
- package/src/lib/slices/palettes.ts +26 -0
- package/src/lib/slices/shadows.ts +123 -0
- package/src/lib/storage.ts +88 -0
- package/src/lib/themeInit.ts +74 -0
- package/src/lib/themeService.ts +101 -0
- package/src/lib/themeTypes.ts +146 -0
- package/src/lib/tokenRegistry.ts +148 -0
- package/src/pages/ComponentEditorPage.svelte +384 -0
- package/src/pages/ComponentEditorPage.svelte.d.ts +2 -0
- package/src/pages/Editor.svelte +98 -0
- package/src/pages/Editor.svelte.d.ts +2 -0
- package/src/pages/EditorShell.svelte +348 -0
- package/src/styles/_padding.scss +34 -0
- package/src/styles/fonts/Fraunces/Fraunces-italic-latin-ext.woff2 +0 -0
- package/src/styles/fonts/Fraunces/Fraunces-italic-latin.woff2 +0 -0
- package/src/styles/fonts/Fraunces/Fraunces-roman-latin-ext.woff2 +0 -0
- package/src/styles/fonts/Fraunces/Fraunces-roman-latin.woff2 +0 -0
- package/src/styles/fonts/Manrope/Manrope-latin-ext.woff2 +0 -0
- package/src/styles/fonts/Manrope/Manrope-latin.woff2 +0 -0
- package/src/styles/fonts.css +22 -10
- package/src/styles/form-controls.css +14 -16
- package/src/styles/tokens.css +1322 -0
- package/src/styles/ui-editor.css +126 -0
- package/src/{showcase → ui}/BezierCurveEditor.svelte +14 -14
- package/src/{showcase → ui}/ColorEditPanel.svelte +42 -36
- package/src/ui/EditorViewSwitcher.svelte +180 -0
- package/src/ui/FontStackEditor.svelte +360 -0
- package/src/ui/GradientEditor.svelte +461 -0
- package/src/ui/GradientStopPicker.svelte +74 -0
- package/src/ui/PaletteEditor.svelte +1590 -0
- package/src/ui/PaletteEditor.test.ts +108 -0
- package/src/ui/PresetFileManager.svelte +567 -0
- package/src/ui/ProjectFontsSection.svelte +645 -0
- package/src/{showcase → ui}/SurfacesTab.svelte +39 -39
- package/src/{showcase → ui}/TextTab.svelte +27 -27
- package/src/{showcase/TokenFileManager.svelte → ui/ThemeFileManager.svelte} +196 -112
- package/src/ui/Toggle.svelte +108 -0
- package/src/ui/UICopyPopover.svelte +78 -0
- package/src/{showcase/EditorDialog.svelte → ui/UIDialog.svelte} +66 -25
- package/src/ui/UIFontFamilySelector.svelte +309 -0
- package/src/ui/UIFontSizeSelector.svelte +165 -0
- package/src/ui/UIFontWeightSelector.svelte +52 -0
- package/src/ui/UILineHeightSelector.svelte +47 -0
- package/src/ui/UILinkToggle.svelte +60 -0
- package/src/ui/UIOptionItem.svelte +74 -0
- package/src/ui/UIOptionList.svelte +27 -0
- package/src/ui/UIPaddingSelector.svelte +661 -0
- package/src/ui/UIPaletteSelector.svelte +1084 -0
- package/src/ui/UIRadio.svelte +72 -0
- package/src/ui/UIRadioGroup.svelte +59 -0
- package/src/ui/UIRelinkConfirmPopover.svelte +235 -0
- package/src/ui/UITokenSelector.svelte +509 -0
- package/src/ui/UIVariantSelector.svelte +145 -0
- package/src/ui/VariablesTab.svelte +252 -0
- package/src/ui/index.ts +31 -0
- package/src/ui/keepInViewport.ts +84 -0
- package/src/ui/palette/GradientStopEditor.svelte +482 -0
- package/src/ui/palette/OverridesPanel.svelte +526 -0
- package/src/ui/palette/PaletteBase.svelte +165 -0
- package/src/ui/palette/ScaleCurveEditor.svelte +38 -0
- package/src/ui/palette/paletteEditorState.ts +89 -0
- package/src/ui/sections/ColumnsSection.svelte +273 -0
- package/src/ui/sections/GradientsSection.svelte +147 -0
- package/src/ui/sections/OverlaysSection.svelte +670 -0
- package/src/ui/sections/ShadowsSection.svelte +1250 -0
- package/src/ui/sections/TokenScaleTable.svelte +332 -0
- package/src/ui/sections/tokenScales.ts +81 -0
- package/src/ui/variantScales.ts +108 -0
- package/src/components/DetailNav.svelte +0 -78
- package/src/components/Toggle.svelte +0 -86
- package/src/lib/tokenInit.ts +0 -29
- package/src/lib/tokenService.ts +0 -144
- package/src/lib/tokenTypes.ts +0 -45
- package/src/pages/Admin.svelte +0 -100
- package/src/pages/ShowcasePage.svelte +0 -144
- package/src/showcase/BackupBrowser.svelte +0 -617
- package/src/showcase/ComponentsTab.svelte +0 -105
- package/src/showcase/PaletteEditor.svelte +0 -2579
- package/src/showcase/PaletteSelector.svelte +0 -627
- package/src/showcase/TokenMap.svelte +0 -54
- package/src/showcase/VariablesTab.svelte +0 -2655
- package/src/showcase/VisualsTab.svelte +0 -231
- package/src/showcase/demos/BadgeDemo.svelte +0 -56
- package/src/showcase/demos/CardDemo.svelte +0 -50
- package/src/showcase/demos/ChoiceButtonsDemo.svelte +0 -192
- package/src/showcase/demos/CollapsibleSectionDemo.svelte +0 -54
- package/src/showcase/demos/DialogDemo.svelte +0 -42
- package/src/showcase/demos/InlineEditActionsDemo.svelte +0 -25
- package/src/showcase/demos/NotificationDemo.svelte +0 -147
- package/src/showcase/demos/ProgressBarDemo.svelte +0 -54
- package/src/showcase/demos/RadioButtonDemo.svelte +0 -56
- package/src/showcase/demos/SectionDividerDemo.svelte +0 -77
- package/src/showcase/demos/StandardButtonsDemo.svelte +0 -455
- package/src/showcase/demos/TabBarDemo.svelte +0 -58
- package/src/showcase/demos/TooltipDemo.svelte +0 -52
- package/src/showcase/editor.css +0 -93
- package/src/showcase/index.ts +0 -17
- package/src/styles/fonts/Domine/Domine-VariableFont_wght.ttf +0 -0
- package/src/styles/fonts/Domine/OFL.txt +0 -97
- package/src/styles/fonts/Domine/README.txt +0 -66
- /package/src/{showcase → ui}/curveEngine.ts +0 -0
|
@@ -0,0 +1,1590 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import { onMount, onDestroy, tick } from 'svelte';
|
|
3
|
+
import { hexToOklch, oklchToHex, gamutClamp } from '../lib/oklch';
|
|
4
|
+
import { type CurveAnchor, makeAnchor, sampleCurve, lightnessCurveConfig, saturationCurveConfig, textLightnessCurveConfig } from './curveEngine';
|
|
5
|
+
import ColorEditPanel from './ColorEditPanel.svelte';
|
|
6
|
+
import OverridesPanel from './palette/OverridesPanel.svelte';
|
|
7
|
+
import GradientStopEditor from './palette/GradientStopEditor.svelte';
|
|
8
|
+
import ScaleCurveEditor from './palette/ScaleCurveEditor.svelte';
|
|
9
|
+
import PaletteBase from './palette/PaletteBase.svelte';
|
|
10
|
+
import { type EditingState, idleState, BASE_KEY, isEditingBase as isBaseEdit } from './palette/paletteEditorState';
|
|
11
|
+
import type { PaletteConfig, GradientStyle, GradientStop } from '../lib/themeTypes';
|
|
12
|
+
import { editorState, mutate, setPaletteConfig, beginSliderGesture, beginScope, commitScope, cancelScope, type Scope } from '../lib/editorStore';
|
|
13
|
+
import { scaleToCssVar } from '../lib/paletteDerivation';
|
|
14
|
+
import { showCopyPopover } from '../lib/copyPopover';
|
|
15
|
+
import { get } from 'svelte/store';
|
|
16
|
+
|
|
17
|
+
/** Mid-gray fallback used when no base colour or computed gray-500 is available. */
|
|
18
|
+
const GRAY_FALLBACK = '#808080';
|
|
19
|
+
|
|
20
|
+
export let label: string;
|
|
21
|
+
export let displayLabel: string | null = null;
|
|
22
|
+
export let initialColor: string = GRAY_FALLBACK;
|
|
23
|
+
export let mode: 'chromatic' | 'gray' = 'chromatic';
|
|
24
|
+
export let cssNamespace: string | null = null;
|
|
25
|
+
export let emptySelector: boolean = false;
|
|
26
|
+
|
|
27
|
+
// --- Store-sourced config (single source of truth) ---
|
|
28
|
+
//
|
|
29
|
+
// All persistent palette state lives in `$editorState.palettes[label]`.
|
|
30
|
+
// Local `$:` derivations below pull named fields with defaults; every
|
|
31
|
+
// handler writes via `edit()` / `patchPalette()` so the store is the only
|
|
32
|
+
// writer. No `let` mirrors, no round-trip sync reactives.
|
|
33
|
+
//
|
|
34
|
+
// The defaults fall back only when palettes[label] is undefined (brand-new
|
|
35
|
+
// install, never seeded). Production seeds via themeInit → seedPalettesFromTheme.
|
|
36
|
+
$: paletteConfig = $editorState.palettes[label];
|
|
37
|
+
$: baseColor = paletteConfig?.baseColor ?? initialColor;
|
|
38
|
+
$: tintHue = paletteConfig?.tintHue ?? 240;
|
|
39
|
+
$: tintChroma = paletteConfig?.tintChroma ?? DEFAULT_TINT_CHROMA;
|
|
40
|
+
$: lightnessCurve = paletteConfig?.lightnessCurve ?? DEFAULT_PALETTE_LIGHTNESS();
|
|
41
|
+
$: saturationCurve = paletteConfig?.saturationCurve ?? DEFAULT_PALETTE_SATURATION();
|
|
42
|
+
$: grayLightnessCurve = paletteConfig?.grayLightnessCurve ?? DEFAULT_GRAY_LIGHTNESS();
|
|
43
|
+
$: graySaturationCurve = paletteConfig?.graySaturationCurve ?? DEFAULT_GRAY_SATURATION();
|
|
44
|
+
$: scaleCurves = paletteConfig?.scaleCurves ?? defaultScaleCurvesObject();
|
|
45
|
+
$: curveOffset = paletteConfig?.curveOffset ?? { lightness: 0, saturation: 0 };
|
|
46
|
+
$: overrides = paletteConfig?.overrides ?? {};
|
|
47
|
+
$: snappedScales = new Set(paletteConfig?.snappedScales ?? []);
|
|
48
|
+
$: anchorToBase = paletteConfig?.anchorToBase ?? true;
|
|
49
|
+
$: emptyMode = paletteConfig?.emptyMode ?? 'solid';
|
|
50
|
+
$: emptyStep = paletteConfig?.emptyStep ?? '850';
|
|
51
|
+
$: gradientStyle = paletteConfig?.gradientStyle ?? 'linear';
|
|
52
|
+
$: gradientAngle = paletteConfig?.gradientAngle ?? 180;
|
|
53
|
+
$: gradientReverse = paletteConfig?.gradientReverse ?? false;
|
|
54
|
+
$: gradientStops = paletteConfig?.gradientStops ?? [
|
|
55
|
+
{ position: 0, paletteLabel: '800' },
|
|
56
|
+
{ position: 100, paletteLabel: '950' },
|
|
57
|
+
];
|
|
58
|
+
$: gradientSize = paletteConfig?.gradientSize ?? 'page';
|
|
59
|
+
|
|
60
|
+
function defaultPaletteConfig(): PaletteConfig {
|
|
61
|
+
return {
|
|
62
|
+
baseColor: initialColor,
|
|
63
|
+
tintHue: 240,
|
|
64
|
+
tintChroma: DEFAULT_TINT_CHROMA,
|
|
65
|
+
lightnessCurve: DEFAULT_PALETTE_LIGHTNESS(),
|
|
66
|
+
saturationCurve: DEFAULT_PALETTE_SATURATION(),
|
|
67
|
+
grayLightnessCurve: DEFAULT_GRAY_LIGHTNESS(),
|
|
68
|
+
graySaturationCurve: DEFAULT_GRAY_SATURATION(),
|
|
69
|
+
scaleCurves: defaultScaleCurvesObject(),
|
|
70
|
+
curveOffset: { lightness: 0, saturation: 0 },
|
|
71
|
+
overrides: {},
|
|
72
|
+
snappedScales: [],
|
|
73
|
+
anchorToBase: true,
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function defaultScaleCurvesObject() {
|
|
78
|
+
return {
|
|
79
|
+
Surfaces: { lightness: defaultScaleCurves.Surfaces.lightness(), saturation: defaultScaleCurves.Surfaces.saturation() },
|
|
80
|
+
Borders: { lightness: defaultScaleCurves.Borders.lightness(), saturation: defaultScaleCurves.Borders.saturation() },
|
|
81
|
+
Text: { lightness: defaultScaleCurves.Text.lightness(), saturation: defaultScaleCurves.Text.saturation() },
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function edit<K extends keyof PaletteConfig>(field: K, value: PaletteConfig[K]): void {
|
|
86
|
+
mutate(`${label}: ${String(field)}`, (s) => {
|
|
87
|
+
if (!s.palettes[label]) s.palettes[label] = defaultPaletteConfig();
|
|
88
|
+
(s.palettes[label] as any)[field] = value;
|
|
89
|
+
});
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function patchPalette(patch: Partial<PaletteConfig>, historyLabel: string): void {
|
|
93
|
+
mutate(`${label}: ${historyLabel}`, (s) => {
|
|
94
|
+
if (!s.palettes[label]) s.palettes[label] = defaultPaletteConfig();
|
|
95
|
+
Object.assign(s.palettes[label], patch);
|
|
96
|
+
});
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// --- Transient UI state (not persisted; not in PaletteConfig) ---
|
|
100
|
+
let lockedLightnessIdx: number | null = null;
|
|
101
|
+
let lockedSaturationIdx: number | null = null;
|
|
102
|
+
|
|
103
|
+
// Handle for the open palette edit scope: a clipping scope (clipUndoFloor:
|
|
104
|
+
// true) bracketing one panel-open → confirm/cancel cycle. Held at component
|
|
105
|
+
// scope so the inline header-swatch handlers and the function handlers
|
|
106
|
+
// share the same handle for commitScope/cancelScope.
|
|
107
|
+
let paletteEditScope: Scope | null = null;
|
|
108
|
+
|
|
109
|
+
function stopColor(stop: GradientStop, pc: typeof paletteComputed): string {
|
|
110
|
+
const ps = pc?.find(p => p.label === stop.paletteLabel);
|
|
111
|
+
return ps ? ps.effective : '#000000';
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
let gradientColorStops = '';
|
|
115
|
+
let gradientCssValue = '';
|
|
116
|
+
let gradientBarPreview = '';
|
|
117
|
+
|
|
118
|
+
function onEmptyModeChange(e: Event) {
|
|
119
|
+
edit('emptyMode', (e.currentTarget as HTMLInputElement).checked ? 'gradient' : 'solid');
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// --- Gray mode ---
|
|
123
|
+
|
|
124
|
+
interface GrayStep {
|
|
125
|
+
label: string;
|
|
126
|
+
hue: number;
|
|
127
|
+
saturation: number;
|
|
128
|
+
lightness: number;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
const graySteps: GrayStep[] = [
|
|
132
|
+
{ label: '100', hue: 240, saturation: 5, lightness: 92 },
|
|
133
|
+
{ label: '200', hue: 220, saturation: 13, lightness: 84 },
|
|
134
|
+
{ label: '300', hue: 216, saturation: 12, lightness: 72 },
|
|
135
|
+
{ label: '400', hue: 240, saturation: 5, lightness: 61 },
|
|
136
|
+
{ label: '500', hue: 240, saturation: 5, lightness: 50 },
|
|
137
|
+
{ label: '600', hue: 240, saturation: 5, lightness: 42 },
|
|
138
|
+
{ label: '700', hue: 240, saturation: 5, lightness: 34 },
|
|
139
|
+
{ label: '800', hue: 240, saturation: 10, lightness: 25 },
|
|
140
|
+
{ label: '850', hue: 229, saturation: 20, lightness: 18 },
|
|
141
|
+
{ label: '900', hue: 240, saturation: 30, lightness: 10 },
|
|
142
|
+
{ label: '950', hue: 229, saturation: 34, lightness: 3 },
|
|
143
|
+
];
|
|
144
|
+
|
|
145
|
+
let grayEditorOpen = false;
|
|
146
|
+
let showDerived = false;
|
|
147
|
+
|
|
148
|
+
// --- Palette curve editors (lightness + saturation) ---
|
|
149
|
+
let paletteEditorOpen = false;
|
|
150
|
+
|
|
151
|
+
// Default curve anchors (used for initial state and reset)
|
|
152
|
+
const DEFAULT_PALETTE_LIGHTNESS = () => [makeAnchor(0, 95, 5), makeAnchor(100, 8, 5)];
|
|
153
|
+
const DEFAULT_PALETTE_SATURATION = () => [makeAnchor(0, 100, 30), makeAnchor(100, 100, 30)];
|
|
154
|
+
const DEFAULT_GRAY_LIGHTNESS = () => [makeAnchor(0, 92, 5), makeAnchor(100, 3, 5)];
|
|
155
|
+
const DEFAULT_GRAY_SATURATION = () => [makeAnchor(0, 20, 30), makeAnchor(100, 20, 30)];
|
|
156
|
+
|
|
157
|
+
function setLightnessCurve(a: CurveAnchor[]) { edit('lightnessCurve', a); }
|
|
158
|
+
function setSaturationCurve(a: CurveAnchor[]) { edit('saturationCurve', a); }
|
|
159
|
+
function setGrayLightnessCurve(a: CurveAnchor[]) { edit('grayLightnessCurve', a); }
|
|
160
|
+
function setGraySaturationCurve(a: CurveAnchor[]) { edit('graySaturationCurve', a); }
|
|
161
|
+
|
|
162
|
+
// --- Curve offset + clipboard (shared across all curve editors) ---
|
|
163
|
+
|
|
164
|
+
function handleOffset(key: string, value: number) {
|
|
165
|
+
edit('curveOffset', { ...curveOffset, [key]: value });
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
// Gray step index to curve x-position
|
|
169
|
+
function grayStepToX(index: number): number {
|
|
170
|
+
return graySteps.length > 1 ? (index / (graySteps.length - 1)) * 100 : 50;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
// Base chroma for gray tinting (editable via the color panel's chroma slider)
|
|
174
|
+
const DEFAULT_TINT_CHROMA = 0.04;
|
|
175
|
+
|
|
176
|
+
// --- Editing-state machine (M4 fold) ---
|
|
177
|
+
//
|
|
178
|
+
// Single discriminated union replaces five independent `let` decls
|
|
179
|
+
// (`editingKey`, `editingSnapshot`, `editingDraft`, `snapshotTintHue`,
|
|
180
|
+
// `snapshotTintChroma`). The compatibility `$:` derivations below preserve
|
|
181
|
+
// existing read sites while writes go through `editing = { kind: ... }`.
|
|
182
|
+
let editing: EditingState = idleState;
|
|
183
|
+
|
|
184
|
+
// Read-side compat: existing `editingKey === ...` etc. comparisons keep
|
|
185
|
+
// working. New code should narrow on `editing.kind` directly.
|
|
186
|
+
$: editingKey = editing.kind === 'idle' ? null : editing.kind === 'editingBase' ? BASE_KEY : editing.stepKey;
|
|
187
|
+
$: editingDraft = editing.kind === 'editingStep' ? editing.draft : null;
|
|
188
|
+
$: editingSnapshot = editing.kind === 'idle' ? null : editing.kind === 'editingBase' ? editing.snapshotHex : editing.snapshot;
|
|
189
|
+
$: snapshotTintHue = editing.kind === 'editingBase' ? editing.snapshotTintHue : null;
|
|
190
|
+
$: snapshotTintChroma = editing.kind === 'editingBase' ? editing.snapshotTintChroma : null;
|
|
191
|
+
|
|
192
|
+
function computeGrayColor(index: number, hue: number, chroma: number = tintChroma): string {
|
|
193
|
+
const xPos = grayStepToX(index);
|
|
194
|
+
const lOff = curveOffset['gray-lightness'] ?? 0;
|
|
195
|
+
const sOff = curveOffset['gray-saturation'] ?? 0;
|
|
196
|
+
|
|
197
|
+
const targetL = Math.max(0, Math.min(100, sampleCurve(grayLightnessCurve, xPos) + lOff)) / 100;
|
|
198
|
+
const satMul = Math.max(0, Math.min(2, (sampleCurve(graySaturationCurve, xPos) + sOff) / 100));
|
|
199
|
+
const targetC = chroma * satMul;
|
|
200
|
+
|
|
201
|
+
const clamped = gamutClamp(targetL, targetC, hue);
|
|
202
|
+
return oklchToHex(clamped.l, clamped.c, clamped.h);
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
function grayStepKey(label: string): string {
|
|
206
|
+
return `gray-${label}`;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
// Reactive map of computed gray colors
|
|
210
|
+
$: grayComputed = (() => {
|
|
211
|
+
const _gl = grayLightnessCurve, _gs = graySaturationCurve, _co = curveOffset, _tc = tintChroma, _th = tintHue;
|
|
212
|
+
return graySteps.map((step, index) => ({
|
|
213
|
+
step,
|
|
214
|
+
index,
|
|
215
|
+
key: grayStepKey(step.label),
|
|
216
|
+
hex: computeGrayColor(index, _th, _tc),
|
|
217
|
+
}));
|
|
218
|
+
})();
|
|
219
|
+
|
|
220
|
+
$: grayEffective = (() => {
|
|
221
|
+
const _ed = editingDraft, _ek = editingKey, _ov = overrides;
|
|
222
|
+
return grayComputed.map(g => ({
|
|
223
|
+
...g,
|
|
224
|
+
effective: (_ek === g.key && _ed !== null) ? _ed : (g.key in _ov) ? _ov[g.key] : g.hex,
|
|
225
|
+
}));
|
|
226
|
+
})();
|
|
227
|
+
|
|
228
|
+
// Gray-500 hex — always the computed (curve-derived) value so derived
|
|
229
|
+
// scales (surfaces, borders, text) update in realtime when tint changes.
|
|
230
|
+
$: gray500Hex = mode === 'gray'
|
|
231
|
+
? (grayComputed.find(g => g.step.label === '500')?.hex ?? GRAY_FALLBACK)
|
|
232
|
+
: baseColor;
|
|
233
|
+
|
|
234
|
+
|
|
235
|
+
// --- Chromatic palette steps ---
|
|
236
|
+
|
|
237
|
+
const paletteStepLightness = [
|
|
238
|
+
{ label: '100', lightness: 95 },
|
|
239
|
+
{ label: '200', lightness: 88 },
|
|
240
|
+
{ label: '300', lightness: 78 },
|
|
241
|
+
{ label: '400', lightness: 68 },
|
|
242
|
+
{ label: '500', lightness: 57 },
|
|
243
|
+
{ label: '600', lightness: 49 },
|
|
244
|
+
{ label: '700', lightness: 41 },
|
|
245
|
+
{ label: '800', lightness: 32 },
|
|
246
|
+
{ label: '850', lightness: 25 },
|
|
247
|
+
{ label: '900', lightness: 17 },
|
|
248
|
+
{ label: '950', lightness: 8 },
|
|
249
|
+
];
|
|
250
|
+
|
|
251
|
+
function paletteStepKey(label: string): string {
|
|
252
|
+
return `Palette-${label}`;
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
function stepIndexToX(index: number): number {
|
|
256
|
+
return (index / (paletteStepLightness.length - 1)) * 100;
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
// --- Locked anchor management ---
|
|
260
|
+
|
|
261
|
+
let injectedLightness = false;
|
|
262
|
+
let injectedSaturation = false;
|
|
263
|
+
|
|
264
|
+
function injectLockedAnchor(curve: CurveAnchor[], x: number, y: number): { curve: CurveAnchor[], idx: number, injected: boolean } {
|
|
265
|
+
const existing = curve.findIndex(a => Math.abs(a.x - x) < 0.5);
|
|
266
|
+
if (existing >= 0) {
|
|
267
|
+
if (curve[existing].x === x && Math.abs(curve[existing].y - y) < 0.01) return { curve, idx: existing, injected: false };
|
|
268
|
+
return { curve: curve.map((a, i) => i === existing ? { ...a, x, y } : a), idx: existing, injected: false };
|
|
269
|
+
}
|
|
270
|
+
let insertAt = curve.findIndex(a => a.x > x);
|
|
271
|
+
if (insertAt < 0) insertAt = curve.length;
|
|
272
|
+
return { curve: [...curve.slice(0, insertAt), makeAnchor(x, y, 15), ...curve.slice(insertAt)], idx: insertAt, injected: true };
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
function removeLockedAnchor(curve: CurveAnchor[], idx: number | null): CurveAnchor[] {
|
|
276
|
+
if (idx === null || idx === 0 || idx === curve.length - 1) return curve;
|
|
277
|
+
return curve.filter((_, i) => i !== idx);
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
/**
|
|
281
|
+
* Toggle anchorToBase: inject (or remove) the locked 500 anchor in both
|
|
282
|
+
* curves atomically with the flag flip, so one undo reverses the whole
|
|
283
|
+
* thing. Transient `injectedLightness` / `injectedSaturation` remember
|
|
284
|
+
* whether we created the anchor (vs it already existed) so toggle-off
|
|
285
|
+
* doesn't destroy a user-authored anchor. Not persisted — acceptable drift
|
|
286
|
+
* on reload is that a pre-existing anchor would be preserved on toggle-off.
|
|
287
|
+
*/
|
|
288
|
+
function setAnchorToBase(next: boolean) {
|
|
289
|
+
if (next === anchorToBase) return;
|
|
290
|
+
if (next) {
|
|
291
|
+
const x500 = stepIndexToX(4);
|
|
292
|
+
const lResult = injectLockedAnchor(lightnessCurve, x500, hexToOklch(baseColor).l * 100);
|
|
293
|
+
const sResult = injectLockedAnchor(saturationCurve, x500, 100);
|
|
294
|
+
injectedLightness = lResult.injected;
|
|
295
|
+
injectedSaturation = sResult.injected;
|
|
296
|
+
patchPalette({
|
|
297
|
+
anchorToBase: true,
|
|
298
|
+
lightnessCurve: lResult.curve,
|
|
299
|
+
saturationCurve: sResult.curve,
|
|
300
|
+
}, 'anchor on');
|
|
301
|
+
} else {
|
|
302
|
+
const lCurr = lightnessCurve;
|
|
303
|
+
const sCurr = saturationCurve;
|
|
304
|
+
const nextL = injectedLightness ? removeLockedAnchor(lCurr, lockedLightnessIdx) : lCurr;
|
|
305
|
+
const nextS = injectedSaturation ? removeLockedAnchor(sCurr, lockedSaturationIdx) : sCurr;
|
|
306
|
+
injectedLightness = false;
|
|
307
|
+
injectedSaturation = false;
|
|
308
|
+
patchPalette({
|
|
309
|
+
anchorToBase: false,
|
|
310
|
+
lightnessCurve: nextL,
|
|
311
|
+
saturationCurve: nextS,
|
|
312
|
+
}, 'anchor off');
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
// Derive locked anchor indices from curve shape — no writes to state.
|
|
317
|
+
$: {
|
|
318
|
+
if (anchorToBase) {
|
|
319
|
+
const x500 = stepIndexToX(4);
|
|
320
|
+
const lIdx = lightnessCurve.findIndex(a => Math.abs(a.x - x500) < 0.5);
|
|
321
|
+
lockedLightnessIdx = lIdx >= 0 ? lIdx : null;
|
|
322
|
+
const sIdx = saturationCurve.findIndex(a => Math.abs(a.x - x500) < 0.5);
|
|
323
|
+
lockedSaturationIdx = sIdx >= 0 ? sIdx : null;
|
|
324
|
+
} else {
|
|
325
|
+
lockedLightnessIdx = null;
|
|
326
|
+
lockedSaturationIdx = null;
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
/**
|
|
331
|
+
* Keep the locked lightness anchor y in sync with baseColor. Idempotent —
|
|
332
|
+
* only writes when the curve's anchor y differs from the baseColor-derived
|
|
333
|
+
* target. During a baseColor drag (inside a slider transaction) this
|
|
334
|
+
* additional curve edit merges into the same history entry. On undo/redo
|
|
335
|
+
* the curve already has the correct y (they're saved together), so this
|
|
336
|
+
* is a no-op.
|
|
337
|
+
*/
|
|
338
|
+
$: if (anchorToBase && lockedLightnessIdx !== null && baseColor) {
|
|
339
|
+
const targetY = hexToOklch(baseColor).l * 100;
|
|
340
|
+
if (lightnessCurve[lockedLightnessIdx] && Math.abs(lightnessCurve[lockedLightnessIdx].y - targetY) > 0.01) {
|
|
341
|
+
edit('lightnessCurve', lightnessCurve.map((a, i) => i === lockedLightnessIdx ? { ...a, y: targetY } : a));
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
function computePaletteColor(index: number, base: string): string {
|
|
346
|
+
const { c: baseC, h } = hexToOklch(base);
|
|
347
|
+
const xPos = stepIndexToX(index);
|
|
348
|
+
|
|
349
|
+
const targetL = Math.max(0, Math.min(100, sampleCurve(lightnessCurve, xPos) + (curveOffset['lightness'] ?? 0))) / 100;
|
|
350
|
+
const satMul = Math.max(0, Math.min(2, (sampleCurve(saturationCurve, xPos) + (curveOffset['saturation'] ?? 0)) / 100));
|
|
351
|
+
const targetC = baseC * satMul;
|
|
352
|
+
|
|
353
|
+
const clamped = gamutClamp(targetL, targetC, h);
|
|
354
|
+
return oklchToHex(clamped.l, clamped.c, clamped.h);
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
$: paletteComputed = (() => {
|
|
358
|
+
const _bc = baseColor, _lc = lightnessCurve, _sc = saturationCurve, _co = curveOffset, _ed = editingDraft, _ek = editingKey, _ov = overrides, _ab = anchorToBase;
|
|
359
|
+
return paletteStepLightness.map((ps, index) => {
|
|
360
|
+
const k = paletteStepKey(ps.label);
|
|
361
|
+
const hex = computePaletteColor(index, baseColor);
|
|
362
|
+
const effective = (_ek === k && _ed !== null) ? _ed : (k in _ov) ? _ov[k] : hex;
|
|
363
|
+
return {
|
|
364
|
+
label: ps.label,
|
|
365
|
+
lightness: ps.lightness,
|
|
366
|
+
index,
|
|
367
|
+
key: k,
|
|
368
|
+
hex,
|
|
369
|
+
effective,
|
|
370
|
+
};
|
|
371
|
+
});
|
|
372
|
+
})();
|
|
373
|
+
|
|
374
|
+
// Gradient reactives — must follow paletteComputed
|
|
375
|
+
$: {
|
|
376
|
+
const pc = paletteComputed;
|
|
377
|
+
const sorted = [...gradientStops].sort((a, b) => gradientReverse ? b.position - a.position : a.position - b.position);
|
|
378
|
+
gradientColorStops = sorted.map(s => `${stopColor(s, pc)} ${s.position}%`).join(', ');
|
|
379
|
+
gradientBarPreview = `linear-gradient(to right, ${gradientColorStops})`;
|
|
380
|
+
if (emptySelector && emptyMode === 'gradient') {
|
|
381
|
+
switch (gradientStyle) {
|
|
382
|
+
case 'radial': gradientCssValue = `radial-gradient(circle, ${gradientColorStops})`; break;
|
|
383
|
+
case 'conic': gradientCssValue = `conic-gradient(from ${gradientAngle}deg, ${gradientColorStops})`; break;
|
|
384
|
+
default: gradientCssValue = `linear-gradient(${gradientAngle}deg, ${gradientColorStops})`;
|
|
385
|
+
}
|
|
386
|
+
} else {
|
|
387
|
+
gradientCssValue = '';
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
function startBaseEdit() {
|
|
392
|
+
if (editing.kind === 'editingBase') { confirmEdit(); return; }
|
|
393
|
+
editing = mode === 'gray'
|
|
394
|
+
? { kind: 'editingBase', snapshotHex: gray500Hex, snapshotTintHue: tintHue, snapshotTintChroma: tintChroma }
|
|
395
|
+
: { kind: 'editingBase', snapshotHex: baseColor, snapshotTintHue: null, snapshotTintChroma: null };
|
|
396
|
+
paletteEditScope = beginScope({ label: 'palette session', collapseToOne: true, clipUndoFloor: true });
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
function handlePaletteClick(ps: { label: string; lightness: number; index: number }) {
|
|
400
|
+
const k = paletteStepKey(ps.label);
|
|
401
|
+
if (editingKey === k) {
|
|
402
|
+
confirmEdit();
|
|
403
|
+
return;
|
|
404
|
+
}
|
|
405
|
+
const current = (k in overrides) ? overrides[k] : computePaletteColor(ps.index, baseColor);
|
|
406
|
+
editing = { kind: 'editingStep', stepKey: k, snapshot: current, draft: current };
|
|
407
|
+
paletteEditScope = beginScope({ label: 'palette session', collapseToOne: true, clipUndoFloor: true });
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
// --- Scale types ---
|
|
411
|
+
|
|
412
|
+
interface Step {
|
|
413
|
+
name: string;
|
|
414
|
+
position: number;
|
|
415
|
+
lightness?: number;
|
|
416
|
+
saturation?: number;
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
interface Scale {
|
|
420
|
+
title: string;
|
|
421
|
+
isText: boolean;
|
|
422
|
+
steps: Step[];
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
const scales: Scale[] = [
|
|
426
|
+
{
|
|
427
|
+
title: 'Surfaces',
|
|
428
|
+
isText: false,
|
|
429
|
+
steps: [
|
|
430
|
+
{ name: 'lowest', position: -1 },
|
|
431
|
+
{ name: 'lower', position: -2/3 },
|
|
432
|
+
{ name: 'low', position: -1/3 },
|
|
433
|
+
{ name: 'default', position: 0 },
|
|
434
|
+
{ name: 'high', position: 1/3 },
|
|
435
|
+
{ name: 'higher', position: 2/3 },
|
|
436
|
+
{ name: 'highest', position: 1 },
|
|
437
|
+
]
|
|
438
|
+
},
|
|
439
|
+
{
|
|
440
|
+
title: 'Borders',
|
|
441
|
+
isText: false,
|
|
442
|
+
steps: [
|
|
443
|
+
{ name: 'faint', position: -1 },
|
|
444
|
+
{ name: 'subtle', position: -0.5 },
|
|
445
|
+
{ name: 'default', position: 0 },
|
|
446
|
+
{ name: 'medium', position: 0.5 },
|
|
447
|
+
{ name: 'strong', position: 1 },
|
|
448
|
+
]
|
|
449
|
+
},
|
|
450
|
+
{
|
|
451
|
+
title: 'Text',
|
|
452
|
+
isText: true,
|
|
453
|
+
steps: [
|
|
454
|
+
{ name: 'primary', position: 0 },
|
|
455
|
+
{ name: 'secondary', position: 0 },
|
|
456
|
+
{ name: 'tertiary', position: 0 },
|
|
457
|
+
{ name: 'muted', position: 0 },
|
|
458
|
+
{ name: 'disabled', position: 0 },
|
|
459
|
+
]
|
|
460
|
+
}
|
|
461
|
+
];
|
|
462
|
+
|
|
463
|
+
// Scales to render in gray mode (varies by namespace)
|
|
464
|
+
$: grayScales = mode === 'gray' ? scales.filter(scale => {
|
|
465
|
+
if (scale.title === 'Surfaces') return true;
|
|
466
|
+
if (scale.title === 'Borders') return true;
|
|
467
|
+
if (scale.title === 'Text') return true;
|
|
468
|
+
return false;
|
|
469
|
+
}) : [];
|
|
470
|
+
|
|
471
|
+
// --- Per-scale curve state (Surfaces & Borders) ---
|
|
472
|
+
|
|
473
|
+
const defaultScaleCurves: Record<string, { lightness: () => CurveAnchor[]; saturation: () => CurveAnchor[] }> = {
|
|
474
|
+
Surfaces: {
|
|
475
|
+
lightness: () => [makeAnchor(0, 15, 5), makeAnchor(100, 47, 5)],
|
|
476
|
+
saturation: () => [makeAnchor(0, 100, 30), makeAnchor(100, 100, 30)],
|
|
477
|
+
},
|
|
478
|
+
Borders: {
|
|
479
|
+
lightness: () => [makeAnchor(0, 25, 5), makeAnchor(100, 80, 5)],
|
|
480
|
+
saturation: () => [makeAnchor(0, 100, 30), makeAnchor(100, 100, 30)],
|
|
481
|
+
},
|
|
482
|
+
Text: {
|
|
483
|
+
lightness: () => [makeAnchor(0, 120, 30), makeAnchor(100, 55, 30)],
|
|
484
|
+
saturation: () => [makeAnchor(0, 100, 30), makeAnchor(100, 15, 30)],
|
|
485
|
+
},
|
|
486
|
+
};
|
|
487
|
+
|
|
488
|
+
let scaleEditorOpen: Record<string, boolean> = { Surfaces: false, Borders: false, Text: false };
|
|
489
|
+
|
|
490
|
+
function toggleScaleEditor(title: string) {
|
|
491
|
+
scaleEditorOpen[title] = !scaleEditorOpen[title];
|
|
492
|
+
scaleEditorOpen = scaleEditorOpen;
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
function setScaleCurve(title: string, channel: 'lightness' | 'saturation', a: CurveAnchor[]) {
|
|
496
|
+
const cur = scaleCurves[title] ?? { lightness: defaultScaleCurves[title].lightness(), saturation: defaultScaleCurves[title].saturation() };
|
|
497
|
+
edit('scaleCurves', { ...scaleCurves, [title]: { ...cur, [channel]: a } });
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
function getScaleCurveKey(scaleTitle: string, channel: 'lightness' | 'saturation'): string {
|
|
501
|
+
return `${scaleTitle}-${channel}`;
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
interface ScaleConfig {
|
|
505
|
+
lightnessLow: number;
|
|
506
|
+
lightnessHigh: number;
|
|
507
|
+
saturation: number;
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
function configForScale(title: string): ScaleConfig {
|
|
511
|
+
return { lightnessLow: 0, lightnessHigh: 100, saturation: 100 };
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
|
|
515
|
+
function stepKey(scaleTitle: string, stepName: string): string {
|
|
516
|
+
return `${scaleTitle}-${stepName}`;
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
$: curveVersion = JSON.stringify(scaleCurves) + JSON.stringify(curveOffset) + gray500Hex;
|
|
520
|
+
|
|
521
|
+
function derivedHex(step: Step, base: string, scaleTitle: string, _version?: string): string {
|
|
522
|
+
return computeDerivedColor(step, base, configForScale(scaleTitle), scaleTitle);
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
/**
|
|
526
|
+
* Closure factories used by `<OverridesPanel>` so the panel doesn't have to
|
|
527
|
+
* know about `baseColor` / `gray500Hex` / `curveVersion`. These keep
|
|
528
|
+
* reactivity intact (the parent's `$:` blocks still drive re-render).
|
|
529
|
+
*
|
|
530
|
+
* Note: `effectiveColor` itself always uses `gray500Hex` for non-override
|
|
531
|
+
* derivation (pre-existing); the chromatic vs gray distinction here is
|
|
532
|
+
* only for the `derivedHex` (the "Ag" preview / border-color base).
|
|
533
|
+
*/
|
|
534
|
+
$: derivedHexForBase = (step: Step, scaleTitle: string) => derivedHex(step, baseColor, scaleTitle, curveVersion);
|
|
535
|
+
$: derivedHexForGray = (step: Step, scaleTitle: string) => derivedHex(step, gray500Hex, scaleTitle, curveVersion);
|
|
536
|
+
$: effectiveHexAny = (k: string, step: Step, scaleTitle: string) => effectiveColor(k, step, scaleTitle, curveVersion);
|
|
537
|
+
|
|
538
|
+
// --- Reactive editing state ---
|
|
539
|
+
|
|
540
|
+
$: isEditingBase = isBaseEdit(editing);
|
|
541
|
+
|
|
542
|
+
$: editingColor = isEditingBase
|
|
543
|
+
? (mode === 'gray' ? gray500Hex : baseColor)
|
|
544
|
+
: editingDraft;
|
|
545
|
+
|
|
546
|
+
$: editingStepInfo = (() => {
|
|
547
|
+
if (!editingKey || isEditingBase) return null;
|
|
548
|
+
if (mode === 'gray') {
|
|
549
|
+
const gs = graySteps.find(s => grayStepKey(s.label) === editingKey);
|
|
550
|
+
if (gs) return { scale: 'Gray', step: gs.label };
|
|
551
|
+
}
|
|
552
|
+
const ps = paletteStepLightness.find(p => paletteStepKey(p.label) === editingKey);
|
|
553
|
+
if (ps) return { scale: 'Palette', step: ps.label };
|
|
554
|
+
for (const scale of scales) {
|
|
555
|
+
for (const step of scale.steps) {
|
|
556
|
+
if (stepKey(scale.title, step.name) === editingKey) {
|
|
557
|
+
return { scale: scale.title, step: step.name };
|
|
558
|
+
}
|
|
559
|
+
}
|
|
560
|
+
}
|
|
561
|
+
return null;
|
|
562
|
+
})();
|
|
563
|
+
|
|
564
|
+
$: panelOpen = editingKey !== null && (isEditingBase || (editingDraft !== null && editingStepInfo !== null));
|
|
565
|
+
|
|
566
|
+
$: editPanelTitle = isEditingBase
|
|
567
|
+
? 'Base Color'
|
|
568
|
+
: editingStepInfo
|
|
569
|
+
? `${editingStepInfo.scale} \u203A ${editingStepInfo.step}`
|
|
570
|
+
: null;
|
|
571
|
+
|
|
572
|
+
// --- Compute derived color via OKLCH ---
|
|
573
|
+
|
|
574
|
+
function scaleStepToX(step: Step, scale: Scale): number {
|
|
575
|
+
const idx = scale.steps.indexOf(step);
|
|
576
|
+
return scale.steps.length > 1 ? (idx / (scale.steps.length - 1)) * 100 : 50;
|
|
577
|
+
}
|
|
578
|
+
|
|
579
|
+
function computeDerivedColor(step: Step, base: string, config: ScaleConfig, scaleTitle: string): string {
|
|
580
|
+
const { l: baseL, c: baseC, h: baseH } = hexToOklch(base);
|
|
581
|
+
const scale = scales.find(s => s.title === scaleTitle)!;
|
|
582
|
+
const xPos = scaleStepToX(step, scale);
|
|
583
|
+
|
|
584
|
+
const lCurve = scaleCurves[scaleTitle]?.lightness ?? [];
|
|
585
|
+
const sCurve = scaleCurves[scaleTitle]?.saturation ?? [];
|
|
586
|
+
const lKey = getScaleCurveKey(scaleTitle, 'lightness');
|
|
587
|
+
const sKey = getScaleCurveKey(scaleTitle, 'saturation');
|
|
588
|
+
const lOff = curveOffset[lKey] ?? 0;
|
|
589
|
+
const sOff = curveOffset[sKey] ?? 0;
|
|
590
|
+
|
|
591
|
+
let targetL: number;
|
|
592
|
+
if (scale.isText) {
|
|
593
|
+
// Text: lightness curve is a multiplier (100 = 1x base lightness)
|
|
594
|
+
const lMul = Math.max(0, Math.min(2, (sampleCurve(lCurve, xPos) + lOff) / 100));
|
|
595
|
+
targetL = Math.max(0, Math.min(1, baseL * lMul));
|
|
596
|
+
} else {
|
|
597
|
+
targetL = Math.max(0, Math.min(100, sampleCurve(lCurve, xPos) + lOff)) / 100;
|
|
598
|
+
}
|
|
599
|
+
|
|
600
|
+
const satMul = Math.max(0, Math.min(2, (sampleCurve(sCurve, xPos) + sOff) / 100));
|
|
601
|
+
const targetC = baseC * satMul;
|
|
602
|
+
|
|
603
|
+
const clamped = gamutClamp(targetL, targetC, baseH);
|
|
604
|
+
return oklchToHex(clamped.l, clamped.c, clamped.h);
|
|
605
|
+
}
|
|
606
|
+
|
|
607
|
+
// --- Interaction handlers ---
|
|
608
|
+
|
|
609
|
+
function handleColorChange(hex: string) {
|
|
610
|
+
if (isEditingBase) {
|
|
611
|
+
// Gray mode's base is derived from tintHue/tintChroma (onHueChromaChange
|
|
612
|
+
// writes those). The raw hex path only applies to chromatic palettes.
|
|
613
|
+
if (mode === 'chromatic') edit('baseColor', hex);
|
|
614
|
+
return;
|
|
615
|
+
}
|
|
616
|
+
if (editing.kind === 'editingStep') {
|
|
617
|
+
editing = { ...editing, draft: hex };
|
|
618
|
+
if (editing.stepKey in overrides) {
|
|
619
|
+
edit('overrides', { ...overrides, [editing.stepKey]: hex });
|
|
620
|
+
}
|
|
621
|
+
}
|
|
622
|
+
}
|
|
623
|
+
|
|
624
|
+
function resetOverride(k: string) {
|
|
625
|
+
if (!(k in overrides)) return;
|
|
626
|
+
const { [k]: _, ...rest } = overrides;
|
|
627
|
+
edit('overrides', rest);
|
|
628
|
+
}
|
|
629
|
+
|
|
630
|
+
function handleOverrideClick(k: string, step: Step, scaleTitle: string) {
|
|
631
|
+
if (editingKey === k) {
|
|
632
|
+
confirmEdit();
|
|
633
|
+
return;
|
|
634
|
+
}
|
|
635
|
+
const current = (k in overrides) ? overrides[k] : computeDerivedColor(step, gray500Hex, configForScale(scaleTitle), scaleTitle);
|
|
636
|
+
editing = { kind: 'editingStep', stepKey: k, snapshot: current, draft: current };
|
|
637
|
+
paletteEditScope = beginScope({ label: 'palette session', collapseToOne: true, clipUndoFloor: true });
|
|
638
|
+
}
|
|
639
|
+
|
|
640
|
+
function handleGrayClick(gStep: GrayStep, index: number) {
|
|
641
|
+
const k = grayStepKey(gStep.label);
|
|
642
|
+
if (editingKey === k) {
|
|
643
|
+
confirmEdit();
|
|
644
|
+
return;
|
|
645
|
+
}
|
|
646
|
+
const current = (k in overrides) ? overrides[k] : computeGrayColor(index, tintHue, tintChroma);
|
|
647
|
+
editing = { kind: 'editingStep', stepKey: k, snapshot: current, draft: current };
|
|
648
|
+
paletteEditScope = beginScope({ label: 'palette session', collapseToOne: true, clipUndoFloor: true });
|
|
649
|
+
}
|
|
650
|
+
|
|
651
|
+
async function confirmEdit() {
|
|
652
|
+
if (editingKey) {
|
|
653
|
+
// Accumulate all override changes into one patch so the session commit
|
|
654
|
+
// sees a single final state (no intermediate reactive round-trips).
|
|
655
|
+
let nextOverrides = { ...overrides };
|
|
656
|
+
if (editingDraft !== null) {
|
|
657
|
+
const computed = computedValueForKey(editingKey);
|
|
658
|
+
if (computed !== null && editingDraft !== computed) {
|
|
659
|
+
nextOverrides[editingKey] = editingDraft;
|
|
660
|
+
} else if (computed !== null && editingKey in nextOverrides && editingDraft === computed) {
|
|
661
|
+
delete nextOverrides[editingKey];
|
|
662
|
+
}
|
|
663
|
+
}
|
|
664
|
+
for (const scale of scales.filter(s => !s.isText)) {
|
|
665
|
+
if (snappedScales.has(scale.title) && scale.steps.some(s => stepKey(scale.title, s.name) === editingKey)) {
|
|
666
|
+
const assigned = snapScaleToPalette(scale);
|
|
667
|
+
nextOverrides = { ...nextOverrides, ...assigned };
|
|
668
|
+
break;
|
|
669
|
+
}
|
|
670
|
+
}
|
|
671
|
+
if (JSON.stringify(nextOverrides) !== JSON.stringify(overrides)) {
|
|
672
|
+
edit('overrides', nextOverrides);
|
|
673
|
+
}
|
|
674
|
+
}
|
|
675
|
+
editing = idleState;
|
|
676
|
+
// tick() lets the store-derived reactives flush before session commit
|
|
677
|
+
await tick();
|
|
678
|
+
if (paletteEditScope) { commitScope(paletteEditScope); paletteEditScope = null; }
|
|
679
|
+
}
|
|
680
|
+
|
|
681
|
+
function cancelEdit() {
|
|
682
|
+
editing = idleState;
|
|
683
|
+
// Restoring the session snapshot in the store fires the sync reactive,
|
|
684
|
+
// which pulls baseColor/tintHue/tintChroma/overrides/… back to pre-open.
|
|
685
|
+
if (paletteEditScope) { cancelScope(paletteEditScope); paletteEditScope = null; }
|
|
686
|
+
}
|
|
687
|
+
|
|
688
|
+
function removeOverride(k: string) {
|
|
689
|
+
const { [k]: _, ...rest } = overrides;
|
|
690
|
+
edit('overrides', rest);
|
|
691
|
+
if (editingKey === k) { editing = idleState; }
|
|
692
|
+
}
|
|
693
|
+
|
|
694
|
+
function computedValueForKey(key: string): string | null {
|
|
695
|
+
const ps = paletteStepLightness.find(p => paletteStepKey(p.label) === key);
|
|
696
|
+
if (ps) {
|
|
697
|
+
const idx = paletteStepLightness.indexOf(ps);
|
|
698
|
+
return computePaletteColor(idx, baseColor);
|
|
699
|
+
}
|
|
700
|
+
const gi = graySteps.findIndex(g => grayStepKey(g.label) === key);
|
|
701
|
+
if (gi >= 0) return computeGrayColor(gi, tintHue, tintChroma);
|
|
702
|
+
for (const scale of scales) {
|
|
703
|
+
for (const step of scale.steps) {
|
|
704
|
+
if (stepKey(scale.title, step.name) === key) {
|
|
705
|
+
return computeDerivedColor(step, gray500Hex, configForScale(scale.title), scale.title);
|
|
706
|
+
}
|
|
707
|
+
}
|
|
708
|
+
}
|
|
709
|
+
return null;
|
|
710
|
+
}
|
|
711
|
+
|
|
712
|
+
function effectiveColor(k: string, step: Step, scaleTitle: string, _version?: string): string {
|
|
713
|
+
if (editingKey === k && editingDraft !== null) return editingDraft;
|
|
714
|
+
return (k in overrides) ? overrides[k] : computeDerivedColor(step, gray500Hex, configForScale(scaleTitle), scaleTitle);
|
|
715
|
+
}
|
|
716
|
+
|
|
717
|
+
// --- Brightness/Saturation gradient helpers for scale editor ---
|
|
718
|
+
|
|
719
|
+
function lightnessGrad(base: string): string {
|
|
720
|
+
const { c, h } = hexToOklch(base);
|
|
721
|
+
const points: string[] = [];
|
|
722
|
+
for (let i = 0; i <= 8; i++) {
|
|
723
|
+
const l = i / 8;
|
|
724
|
+
const clamped = gamutClamp(l, c, h);
|
|
725
|
+
points.push(`${oklchToHex(clamped.l, clamped.c, clamped.h)} ${Math.round((i / 8) * 100)}%`);
|
|
726
|
+
}
|
|
727
|
+
return `linear-gradient(to right, ${points.join(', ')})`;
|
|
728
|
+
}
|
|
729
|
+
|
|
730
|
+
function saturationGrad(base: string): string {
|
|
731
|
+
const { l, c, h } = hexToOklch(base);
|
|
732
|
+
const midL = Math.max(0.3, Math.min(0.7, l));
|
|
733
|
+
const points: string[] = [];
|
|
734
|
+
for (let i = 0; i <= 8; i++) {
|
|
735
|
+
const scale = (i / 8) * 2;
|
|
736
|
+
const targetC = c * scale;
|
|
737
|
+
const clamped = gamutClamp(midL, targetC, h);
|
|
738
|
+
points.push(`${oklchToHex(clamped.l, clamped.c, clamped.h)} ${Math.round((i / 8) * 100)}%`);
|
|
739
|
+
}
|
|
740
|
+
return `linear-gradient(to right, ${points.join(', ')})`;
|
|
741
|
+
}
|
|
742
|
+
|
|
743
|
+
let copiedKey: string | null = null;
|
|
744
|
+
function copyHex(k: string, hex: string, event?: MouseEvent) {
|
|
745
|
+
navigator.clipboard.writeText(hex);
|
|
746
|
+
copiedKey = k;
|
|
747
|
+
showCopyPopover(hex, event?.currentTarget ?? null);
|
|
748
|
+
setTimeout(() => { copiedKey = null; }, 1500);
|
|
749
|
+
}
|
|
750
|
+
|
|
751
|
+
let copiedLabelKey: string | null = null;
|
|
752
|
+
function copyVarName(k: string, varName: string, event?: MouseEvent) {
|
|
753
|
+
navigator.clipboard.writeText(varName);
|
|
754
|
+
copiedLabelKey = k;
|
|
755
|
+
showCopyPopover(varName, event?.currentTarget ?? null);
|
|
756
|
+
setTimeout(() => { copiedLabelKey = null; }, 1500);
|
|
757
|
+
}
|
|
758
|
+
|
|
759
|
+
// --- Snap-all: constrain an entire scale to unique palette steps ---
|
|
760
|
+
|
|
761
|
+
function snapScaleToPalette(scale: Scale): Record<string, string> {
|
|
762
|
+
const cfg = configForScale(scale.title);
|
|
763
|
+
const n = scale.steps.length;
|
|
764
|
+
|
|
765
|
+
const stepL = scale.steps.map(step => {
|
|
766
|
+
const derived = computeDerivedColor(step, baseColor, cfg, scale.title);
|
|
767
|
+
return hexToOklch(derived).l;
|
|
768
|
+
});
|
|
769
|
+
|
|
770
|
+
const palL = paletteComputed.map(ps => hexToOklch(ps.hex).l);
|
|
771
|
+
|
|
772
|
+
const palDarkFirst = [...paletteComputed].reverse();
|
|
773
|
+
const palLDarkFirst = [...palL].reverse();
|
|
774
|
+
|
|
775
|
+
let bestStart = 0;
|
|
776
|
+
let bestCost = Infinity;
|
|
777
|
+
for (let start = 0; start <= palDarkFirst.length - n; start++) {
|
|
778
|
+
let cost = 0;
|
|
779
|
+
for (let i = 0; i < n; i++) {
|
|
780
|
+
const d = stepL[i] - palLDarkFirst[start + i];
|
|
781
|
+
cost += d * d;
|
|
782
|
+
}
|
|
783
|
+
if (cost < bestCost) {
|
|
784
|
+
bestCost = cost;
|
|
785
|
+
bestStart = start;
|
|
786
|
+
}
|
|
787
|
+
}
|
|
788
|
+
|
|
789
|
+
const assigned: Record<string, string> = {};
|
|
790
|
+
for (let i = 0; i < n; i++) {
|
|
791
|
+
const k = stepKey(scale.title, scale.steps[i].name);
|
|
792
|
+
assigned[k] = palDarkFirst[bestStart + i].hex;
|
|
793
|
+
}
|
|
794
|
+
return assigned;
|
|
795
|
+
}
|
|
796
|
+
|
|
797
|
+
function toggleSnapAll(scale: Scale) {
|
|
798
|
+
if (snappedScales.has(scale.title)) {
|
|
799
|
+
snapPickerKey = null;
|
|
800
|
+
const nextOverrides = { ...overrides };
|
|
801
|
+
for (const step of scale.steps) {
|
|
802
|
+
delete nextOverrides[stepKey(scale.title, step.name)];
|
|
803
|
+
}
|
|
804
|
+
const nextSnapped = [...snappedScales].filter(s => s !== scale.title);
|
|
805
|
+
patchPalette({ snappedScales: nextSnapped, overrides: nextOverrides }, 'unsnap scale');
|
|
806
|
+
if (editingKey && scale.steps.some(s => stepKey(scale.title, s.name) === editingKey)) {
|
|
807
|
+
editing = idleState;
|
|
808
|
+
}
|
|
809
|
+
} else {
|
|
810
|
+
const assigned = snapScaleToPalette(scale);
|
|
811
|
+
const nextSnapped = [...snappedScales, scale.title];
|
|
812
|
+
patchPalette({ snappedScales: nextSnapped, overrides: { ...overrides, ...assigned } }, 'snap scale');
|
|
813
|
+
}
|
|
814
|
+
}
|
|
815
|
+
|
|
816
|
+
function clearPaletteOverrides() {
|
|
817
|
+
const next = { ...overrides };
|
|
818
|
+
if (mode === 'gray') {
|
|
819
|
+
for (const step of graySteps) delete next[grayStepKey(step.label)];
|
|
820
|
+
} else {
|
|
821
|
+
for (const ps of paletteStepLightness) delete next[paletteStepKey(ps.label)];
|
|
822
|
+
}
|
|
823
|
+
edit('overrides', next);
|
|
824
|
+
}
|
|
825
|
+
|
|
826
|
+
function scaleHasOverrides(scale: Scale): boolean {
|
|
827
|
+
return scale.steps.some(s => stepKey(scale.title, s.name) in overrides);
|
|
828
|
+
}
|
|
829
|
+
|
|
830
|
+
function clearScaleOverrides(scale: Scale) {
|
|
831
|
+
snapPickerKey = null;
|
|
832
|
+
const nextOverrides = { ...overrides };
|
|
833
|
+
for (const step of scale.steps) {
|
|
834
|
+
delete nextOverrides[stepKey(scale.title, step.name)];
|
|
835
|
+
}
|
|
836
|
+
const nextSnapped = [...snappedScales].filter(s => s !== scale.title);
|
|
837
|
+
patchPalette({ snappedScales: nextSnapped, overrides: nextOverrides }, 'clear scale overrides');
|
|
838
|
+
if (editingKey && scale.steps.some(s => stepKey(scale.title, s.name) === editingKey)) {
|
|
839
|
+
editing = idleState;
|
|
840
|
+
}
|
|
841
|
+
}
|
|
842
|
+
|
|
843
|
+
let snapPickerKey: string | null = null;
|
|
844
|
+
|
|
845
|
+
function handleDocClick(e: MouseEvent) {
|
|
846
|
+
if (!snapPickerKey) return;
|
|
847
|
+
const target = e.target as HTMLElement;
|
|
848
|
+
if (target.closest('.override-slot-wrapper')) return;
|
|
849
|
+
snapPickerKey = null;
|
|
850
|
+
}
|
|
851
|
+
onMount(() => document.addEventListener('click', handleDocClick, true));
|
|
852
|
+
onDestroy(() => document.removeEventListener('click', handleDocClick, true));
|
|
853
|
+
|
|
854
|
+
function selectSnapValue(k: string, paletteHex: string, _scaleTitle: string) {
|
|
855
|
+
edit('overrides', { ...overrides, [k]: paletteHex });
|
|
856
|
+
snapPickerKey = null;
|
|
857
|
+
}
|
|
858
|
+
|
|
859
|
+
function handleSnappedClick(k: string) {
|
|
860
|
+
if (snapPickerKey === k) {
|
|
861
|
+
snapPickerKey = null;
|
|
862
|
+
} else {
|
|
863
|
+
snapPickerKey = k;
|
|
864
|
+
}
|
|
865
|
+
}
|
|
866
|
+
|
|
867
|
+
function resnapScales() {
|
|
868
|
+
if (snappedScales.size === 0) return;
|
|
869
|
+
let changed = false;
|
|
870
|
+
const next = { ...overrides };
|
|
871
|
+
for (const scale of scales.filter(s => !s.isText)) {
|
|
872
|
+
if (!snappedScales.has(scale.title)) continue;
|
|
873
|
+
const assigned = snapScaleToPalette(scale);
|
|
874
|
+
for (const [k, hex] of Object.entries(assigned)) {
|
|
875
|
+
if (next[k] !== hex) {
|
|
876
|
+
next[k] = hex;
|
|
877
|
+
changed = true;
|
|
878
|
+
}
|
|
879
|
+
}
|
|
880
|
+
}
|
|
881
|
+
if (changed) edit('overrides', next);
|
|
882
|
+
}
|
|
883
|
+
|
|
884
|
+
$: baseColor, scaleCurves, lightnessCurve, saturationCurve, curveOffset, snappedScales, resnapScales();
|
|
885
|
+
|
|
886
|
+
// CSS-var emission lives in `paletteDerivation` → `editorRenderer`; the store
|
|
887
|
+
// is the single source of truth for palette config and the renderer
|
|
888
|
+
// subscription writes the derived `--color-*` / `--surface-*` / `--border-*`
|
|
889
|
+
// / `--text-*` / `--page-bg` variables to :root.
|
|
890
|
+
|
|
891
|
+
// --- Load external config ---
|
|
892
|
+
//
|
|
893
|
+
// External file loads come through editorStore.loadFromFile, which
|
|
894
|
+
// overwrites $editorState.palettes — no component-side mirroring needed.
|
|
895
|
+
// This export is kept only for callers that want to push a config in
|
|
896
|
+
// directly.
|
|
897
|
+
export function loadConfig(config: PaletteConfig) {
|
|
898
|
+
setPaletteConfig(label, config);
|
|
899
|
+
}
|
|
900
|
+
|
|
901
|
+
|
|
902
|
+
</script>
|
|
903
|
+
|
|
904
|
+
<div class="palette-editor" style="--editor-base: {mode === 'gray' ? gray500Hex : baseColor}">
|
|
905
|
+
<PaletteBase
|
|
906
|
+
{label}
|
|
907
|
+
{displayLabel}
|
|
908
|
+
{mode}
|
|
909
|
+
{baseColor}
|
|
910
|
+
{gray500Hex}
|
|
911
|
+
{tintHue}
|
|
912
|
+
{tintChroma}
|
|
913
|
+
{anchorToBase}
|
|
914
|
+
{isEditingBase}
|
|
915
|
+
{panelOpen}
|
|
916
|
+
{editingColor}
|
|
917
|
+
{editPanelTitle}
|
|
918
|
+
{copiedKey}
|
|
919
|
+
onStartEdit={startBaseEdit}
|
|
920
|
+
onConfirm={confirmEdit}
|
|
921
|
+
onCancel={cancelEdit}
|
|
922
|
+
onColorChange={handleColorChange}
|
|
923
|
+
onTintChange={(h, c) => patchPalette({ tintHue: h, tintChroma: c }, 'tint hue/chroma')}
|
|
924
|
+
onAnchorToBaseChange={setAnchorToBase}
|
|
925
|
+
onCopyBaseHex={copyHex}
|
|
926
|
+
/>
|
|
927
|
+
|
|
928
|
+
{#if mode === 'chromatic'}
|
|
929
|
+
<!-- Palette + Text row -->
|
|
930
|
+
<div class="scales-row">
|
|
931
|
+
<div class="scale-section">
|
|
932
|
+
<div class="scale-header">
|
|
933
|
+
<h4 class="scale-title">Palette</h4>
|
|
934
|
+
{#if emptySelector}
|
|
935
|
+
<label class="empty-mode-toggle">
|
|
936
|
+
<input
|
|
937
|
+
type="checkbox"
|
|
938
|
+
checked={emptyMode === 'gradient'}
|
|
939
|
+
on:change={onEmptyModeChange}
|
|
940
|
+
/>
|
|
941
|
+
<span>Gradient</span>
|
|
942
|
+
</label>
|
|
943
|
+
{/if}
|
|
944
|
+
<button class="edit-toggle" type="button" on:click={clearPaletteOverrides}>Clear Overrides</button>
|
|
945
|
+
<button
|
|
946
|
+
class="edit-toggle"
|
|
947
|
+
type="button"
|
|
948
|
+
on:click={() => paletteEditorOpen = !paletteEditorOpen}
|
|
949
|
+
>{paletteEditorOpen ? 'Close' : 'Edit'}</button>
|
|
950
|
+
</div>
|
|
951
|
+
<div class="swatch-grid" style="--swatch-cols: {paletteStepLightness.length + 2}">
|
|
952
|
+
<div class="step-column">
|
|
953
|
+
<button class="step-label copyable-label" class:copied={copiedLabelKey === 'palette-white'} type="button" on:click={(e) => copyVarName('palette-white', `--color-${cssNamespace}-white`, e)}>
|
|
954
|
+
{copiedLabelKey === 'palette-white' ? 'copied!' : 'white'}
|
|
955
|
+
</button>
|
|
956
|
+
<div class="swatch gray-swatch bookend" style="background: #ffffff"></div>
|
|
957
|
+
</div>
|
|
958
|
+
{#each paletteComputed as ps}
|
|
959
|
+
<div class="step-column">
|
|
960
|
+
<button class="step-label copyable-label" class:copied={copiedLabelKey === ps.key} type="button" on:click={(e) => copyVarName(ps.key, `--color-${cssNamespace}-${ps.label}`, e)}>
|
|
961
|
+
{copiedLabelKey === ps.key ? 'copied!' : ps.label}
|
|
962
|
+
</button>
|
|
963
|
+
<!-- svelte-ignore a11y-no-noninteractive-tabindex -->
|
|
964
|
+
<div
|
|
965
|
+
class="swatch gray-swatch"
|
|
966
|
+
class:active={editingKey === ps.key}
|
|
967
|
+
class:overridden={ps.key in overrides}
|
|
968
|
+
style="background: {ps.effective}"
|
|
969
|
+
on:click={() => handlePaletteClick({ label: ps.label, lightness: ps.lightness, index: ps.index })}
|
|
970
|
+
role="button"
|
|
971
|
+
tabindex="0"
|
|
972
|
+
on:keydown={(e) => e.key === 'Enter' && handlePaletteClick({ label: ps.label, lightness: ps.lightness, index: ps.index })}
|
|
973
|
+
>
|
|
974
|
+
{#if ps.key in overrides}
|
|
975
|
+
<span class="override-dot" title="Palette override"></span>
|
|
976
|
+
{/if}
|
|
977
|
+
{#if emptySelector && emptyMode === 'solid'}
|
|
978
|
+
<input
|
|
979
|
+
type="checkbox"
|
|
980
|
+
class="empty-check"
|
|
981
|
+
checked={emptyStep === ps.label}
|
|
982
|
+
on:click|stopPropagation={() => edit('emptyStep', ps.label)}
|
|
983
|
+
on:keydown|stopPropagation
|
|
984
|
+
title="Page background"
|
|
985
|
+
/>
|
|
986
|
+
{/if}
|
|
987
|
+
</div>
|
|
988
|
+
<button
|
|
989
|
+
class="step-hex"
|
|
990
|
+
class:copied={copiedKey === ps.key}
|
|
991
|
+
type="button"
|
|
992
|
+
on:click={(e) => copyHex(ps.key, ps.effective, e)}
|
|
993
|
+
>{copiedKey === ps.key ? 'copied!' : ps.effective}</button>
|
|
994
|
+
</div>
|
|
995
|
+
{/each}
|
|
996
|
+
<div class="step-column">
|
|
997
|
+
<button class="step-label copyable-label" class:copied={copiedLabelKey === 'palette-black'} type="button" on:click={(e) => copyVarName('palette-black', `--color-${cssNamespace}-black`, e)}>
|
|
998
|
+
{copiedLabelKey === 'palette-black' ? 'copied!' : 'black'}
|
|
999
|
+
</button>
|
|
1000
|
+
<div class="swatch gray-swatch bookend" style="background: #000000"></div>
|
|
1001
|
+
</div>
|
|
1002
|
+
{#if paletteEditorOpen}
|
|
1003
|
+
<div class="curve-grid-span" style="grid-column: 2 / {paletteStepLightness.length + 2}">
|
|
1004
|
+
<ScaleCurveEditor
|
|
1005
|
+
curveKey="lightness"
|
|
1006
|
+
anchors={lightnessCurve}
|
|
1007
|
+
cfg={lightnessCurveConfig}
|
|
1008
|
+
stepCount={paletteStepLightness.length}
|
|
1009
|
+
defaults={DEFAULT_PALETTE_LIGHTNESS()}
|
|
1010
|
+
offset={curveOffset['lightness'] ?? 0}
|
|
1011
|
+
lockedAnchorIndex={lockedLightnessIdx}
|
|
1012
|
+
onAnchorsChange={setLightnessCurve}
|
|
1013
|
+
onOffsetChange={handleOffset}
|
|
1014
|
+
/>
|
|
1015
|
+
<ScaleCurveEditor
|
|
1016
|
+
curveKey="saturation"
|
|
1017
|
+
anchors={saturationCurve}
|
|
1018
|
+
cfg={saturationCurveConfig}
|
|
1019
|
+
stepCount={paletteStepLightness.length}
|
|
1020
|
+
defaults={DEFAULT_PALETTE_SATURATION()}
|
|
1021
|
+
offset={curveOffset['saturation'] ?? 0}
|
|
1022
|
+
lockedAnchorIndex={lockedSaturationIdx}
|
|
1023
|
+
onAnchorsChange={setSaturationCurve}
|
|
1024
|
+
onOffsetChange={handleOffset}
|
|
1025
|
+
/>
|
|
1026
|
+
</div>
|
|
1027
|
+
{/if}
|
|
1028
|
+
</div>
|
|
1029
|
+
|
|
1030
|
+
{#if emptySelector && emptyMode === 'gradient'}
|
|
1031
|
+
<GradientStopEditor
|
|
1032
|
+
{gradientStyle}
|
|
1033
|
+
{gradientAngle}
|
|
1034
|
+
{gradientSize}
|
|
1035
|
+
{gradientReverse}
|
|
1036
|
+
{gradientStops}
|
|
1037
|
+
{gradientBarPreview}
|
|
1038
|
+
{paletteComputed}
|
|
1039
|
+
onSetGradientStyle={(v) => edit('gradientStyle', v)}
|
|
1040
|
+
onSetGradientSize={(v) => edit('gradientSize', v)}
|
|
1041
|
+
onSetGradientAngle={(v) => edit('gradientAngle', v)}
|
|
1042
|
+
onSetGradientReverse={(v) => edit('gradientReverse', v)}
|
|
1043
|
+
onSetGradientStops={(v) => edit('gradientStops', v)}
|
|
1044
|
+
/>
|
|
1045
|
+
{/if}
|
|
1046
|
+
</div>
|
|
1047
|
+
|
|
1048
|
+
</div>
|
|
1049
|
+
|
|
1050
|
+
<button class="derived-toggle" type="button" on:click={() => showDerived = !showDerived}>
|
|
1051
|
+
<i class="fas" class:fa-chevron-right={!showDerived} class:fa-chevron-down={showDerived}></i>
|
|
1052
|
+
<span>Text, Surfaces & Borders</span>
|
|
1053
|
+
</button>
|
|
1054
|
+
|
|
1055
|
+
{#if showDerived}
|
|
1056
|
+
<div class="scales-row">
|
|
1057
|
+
{#each scales.filter(s => s.isText) as scale}
|
|
1058
|
+
<OverridesPanel
|
|
1059
|
+
{scale}
|
|
1060
|
+
editorOpen={scaleEditorOpen[scale.title] ?? false}
|
|
1061
|
+
snapped={snappedScales.has(scale.title)}
|
|
1062
|
+
supportsSnap={true}
|
|
1063
|
+
{cssNamespace}
|
|
1064
|
+
{scaleCurves}
|
|
1065
|
+
{curveOffset}
|
|
1066
|
+
{defaultScaleCurves}
|
|
1067
|
+
{overrides}
|
|
1068
|
+
{editingKey}
|
|
1069
|
+
{snapPickerKey}
|
|
1070
|
+
{copiedKey}
|
|
1071
|
+
{copiedLabelKey}
|
|
1072
|
+
{paletteComputed}
|
|
1073
|
+
derivedHexFor={derivedHexForBase}
|
|
1074
|
+
effectiveHexFor={effectiveHexAny}
|
|
1075
|
+
stepKeyFor={stepKey}
|
|
1076
|
+
scaleCurveKeyFor={getScaleCurveKey}
|
|
1077
|
+
onToggleSnap={toggleSnapAll}
|
|
1078
|
+
onClearScaleOverrides={clearScaleOverrides}
|
|
1079
|
+
onToggleEditor={toggleScaleEditor}
|
|
1080
|
+
onResetOverride={resetOverride}
|
|
1081
|
+
onOverrideClick={handleOverrideClick}
|
|
1082
|
+
onSnappedClick={handleSnappedClick}
|
|
1083
|
+
onSelectSnapValue={selectSnapValue}
|
|
1084
|
+
onCopyHex={copyHex}
|
|
1085
|
+
onCopyVarName={copyVarName}
|
|
1086
|
+
onSetScaleCurve={setScaleCurve}
|
|
1087
|
+
onOffsetChange={handleOffset}
|
|
1088
|
+
/>
|
|
1089
|
+
{/each}
|
|
1090
|
+
</div>
|
|
1091
|
+
|
|
1092
|
+
<!-- Surfaces & Borders — per-scale editors -->
|
|
1093
|
+
<div class="scales-row">
|
|
1094
|
+
{#each scales.filter(s => !s.isText) as scale}
|
|
1095
|
+
<OverridesPanel
|
|
1096
|
+
{scale}
|
|
1097
|
+
editorOpen={scaleEditorOpen[scale.title] ?? false}
|
|
1098
|
+
snapped={snappedScales.has(scale.title)}
|
|
1099
|
+
supportsSnap={true}
|
|
1100
|
+
{cssNamespace}
|
|
1101
|
+
{scaleCurves}
|
|
1102
|
+
{curveOffset}
|
|
1103
|
+
{defaultScaleCurves}
|
|
1104
|
+
{overrides}
|
|
1105
|
+
{editingKey}
|
|
1106
|
+
{snapPickerKey}
|
|
1107
|
+
{copiedKey}
|
|
1108
|
+
{copiedLabelKey}
|
|
1109
|
+
{paletteComputed}
|
|
1110
|
+
derivedHexFor={derivedHexForBase}
|
|
1111
|
+
effectiveHexFor={effectiveHexAny}
|
|
1112
|
+
stepKeyFor={stepKey}
|
|
1113
|
+
scaleCurveKeyFor={getScaleCurveKey}
|
|
1114
|
+
onToggleSnap={toggleSnapAll}
|
|
1115
|
+
onClearScaleOverrides={clearScaleOverrides}
|
|
1116
|
+
onToggleEditor={toggleScaleEditor}
|
|
1117
|
+
onResetOverride={resetOverride}
|
|
1118
|
+
onOverrideClick={handleOverrideClick}
|
|
1119
|
+
onSnappedClick={handleSnappedClick}
|
|
1120
|
+
onSelectSnapValue={selectSnapValue}
|
|
1121
|
+
onCopyHex={copyHex}
|
|
1122
|
+
onCopyVarName={copyVarName}
|
|
1123
|
+
onSetScaleCurve={setScaleCurve}
|
|
1124
|
+
onOffsetChange={handleOffset}
|
|
1125
|
+
/>
|
|
1126
|
+
{/each}
|
|
1127
|
+
</div>
|
|
1128
|
+
{/if}
|
|
1129
|
+
|
|
1130
|
+
{:else}
|
|
1131
|
+
<!-- Gray mode: palette + text row -->
|
|
1132
|
+
<div class="scales-row">
|
|
1133
|
+
<div class="scale-section">
|
|
1134
|
+
<div class="scale-header">
|
|
1135
|
+
<h4 class="scale-title">{displayLabel ?? label}</h4>
|
|
1136
|
+
<button class="edit-toggle" type="button" on:click={clearPaletteOverrides}>Clear Overrides</button>
|
|
1137
|
+
<button
|
|
1138
|
+
class="edit-toggle"
|
|
1139
|
+
type="button"
|
|
1140
|
+
on:click={() => grayEditorOpen = !grayEditorOpen}
|
|
1141
|
+
>{grayEditorOpen ? 'Close' : 'Edit'}</button>
|
|
1142
|
+
</div>
|
|
1143
|
+
<div class="swatch-grid" style="--swatch-cols: {graySteps.length + 2}">
|
|
1144
|
+
<div class="step-column">
|
|
1145
|
+
<button class="step-label copyable-label" class:copied={copiedLabelKey === 'gray-white'} type="button" on:click={(e) => copyVarName('gray-white', `--color-${cssNamespace}-white`, e)}>
|
|
1146
|
+
{copiedLabelKey === 'gray-white' ? 'copied!' : 'white'}
|
|
1147
|
+
</button>
|
|
1148
|
+
<div class="swatch gray-swatch bookend" style="background: #ffffff"></div>
|
|
1149
|
+
</div>
|
|
1150
|
+
{#each grayEffective as g}
|
|
1151
|
+
<div class="step-column">
|
|
1152
|
+
<button class="step-label copyable-label" class:copied={copiedLabelKey === g.key} type="button" on:click={(e) => copyVarName(g.key, `--color-${cssNamespace}-${g.step.label}`, e)}>
|
|
1153
|
+
{copiedLabelKey === g.key ? 'copied!' : g.step.label}
|
|
1154
|
+
</button>
|
|
1155
|
+
<!-- svelte-ignore a11y-no-noninteractive-tabindex -->
|
|
1156
|
+
<div
|
|
1157
|
+
class="swatch gray-swatch"
|
|
1158
|
+
class:active={editingKey === g.key}
|
|
1159
|
+
class:overridden={g.key in overrides}
|
|
1160
|
+
style="background: {g.effective}"
|
|
1161
|
+
on:click={() => handleGrayClick(g.step, g.index)}
|
|
1162
|
+
role="button"
|
|
1163
|
+
tabindex="0"
|
|
1164
|
+
on:keydown={(e) => e.key === 'Enter' && handleGrayClick(g.step, g.index)}
|
|
1165
|
+
>
|
|
1166
|
+
{#if g.key in overrides}
|
|
1167
|
+
<span class="override-dot" title="Palette override"></span>
|
|
1168
|
+
{/if}
|
|
1169
|
+
</div>
|
|
1170
|
+
<button
|
|
1171
|
+
class="step-hex"
|
|
1172
|
+
class:copied={copiedKey === g.key}
|
|
1173
|
+
type="button"
|
|
1174
|
+
on:click={(e) => copyHex(g.key, g.effective, e)}
|
|
1175
|
+
>{copiedKey === g.key ? 'copied!' : g.effective}</button>
|
|
1176
|
+
</div>
|
|
1177
|
+
{/each}
|
|
1178
|
+
<div class="step-column">
|
|
1179
|
+
<button class="step-label copyable-label" class:copied={copiedLabelKey === 'gray-black'} type="button" on:click={(e) => copyVarName('gray-black', `--color-${cssNamespace}-black`, e)}>
|
|
1180
|
+
{copiedLabelKey === 'gray-black' ? 'copied!' : 'black'}
|
|
1181
|
+
</button>
|
|
1182
|
+
<div class="swatch gray-swatch bookend" style="background: #000000"></div>
|
|
1183
|
+
</div>
|
|
1184
|
+
{#if grayEditorOpen}
|
|
1185
|
+
<div class="curve-grid-span" style="grid-column: 2 / {graySteps.length + 2}">
|
|
1186
|
+
<ScaleCurveEditor
|
|
1187
|
+
curveKey="gray-lightness"
|
|
1188
|
+
anchors={grayLightnessCurve}
|
|
1189
|
+
cfg={lightnessCurveConfig}
|
|
1190
|
+
stepCount={graySteps.length}
|
|
1191
|
+
defaults={DEFAULT_GRAY_LIGHTNESS()}
|
|
1192
|
+
offset={curveOffset['gray-lightness'] ?? 0}
|
|
1193
|
+
onAnchorsChange={setGrayLightnessCurve}
|
|
1194
|
+
onOffsetChange={handleOffset}
|
|
1195
|
+
/>
|
|
1196
|
+
<ScaleCurveEditor
|
|
1197
|
+
curveKey="gray-saturation"
|
|
1198
|
+
anchors={graySaturationCurve}
|
|
1199
|
+
cfg={saturationCurveConfig}
|
|
1200
|
+
stepCount={graySteps.length}
|
|
1201
|
+
defaults={DEFAULT_GRAY_SATURATION()}
|
|
1202
|
+
offset={curveOffset['gray-saturation'] ?? 0}
|
|
1203
|
+
onAnchorsChange={setGraySaturationCurve}
|
|
1204
|
+
onOffsetChange={handleOffset}
|
|
1205
|
+
/>
|
|
1206
|
+
</div>
|
|
1207
|
+
{/if}
|
|
1208
|
+
</div>
|
|
1209
|
+
</div>
|
|
1210
|
+
</div>
|
|
1211
|
+
|
|
1212
|
+
<button class="derived-toggle" type="button" on:click={() => showDerived = !showDerived}>
|
|
1213
|
+
<i class="fas" class:fa-chevron-right={!showDerived} class:fa-chevron-down={showDerived}></i>
|
|
1214
|
+
<span>Text, Surfaces & Borders</span>
|
|
1215
|
+
</button>
|
|
1216
|
+
|
|
1217
|
+
{#if showDerived}
|
|
1218
|
+
<div class="scales-row">
|
|
1219
|
+
{#each grayScales.filter(s => s.isText) as scale}
|
|
1220
|
+
<OverridesPanel
|
|
1221
|
+
{scale}
|
|
1222
|
+
editorOpen={scaleEditorOpen[scale.title] ?? false}
|
|
1223
|
+
snapped={snappedScales.has(scale.title)}
|
|
1224
|
+
supportsSnap={true}
|
|
1225
|
+
{cssNamespace}
|
|
1226
|
+
{scaleCurves}
|
|
1227
|
+
{curveOffset}
|
|
1228
|
+
{defaultScaleCurves}
|
|
1229
|
+
{overrides}
|
|
1230
|
+
{editingKey}
|
|
1231
|
+
{snapPickerKey}
|
|
1232
|
+
{copiedKey}
|
|
1233
|
+
{copiedLabelKey}
|
|
1234
|
+
{paletteComputed}
|
|
1235
|
+
derivedHexFor={derivedHexForGray}
|
|
1236
|
+
effectiveHexFor={effectiveHexAny}
|
|
1237
|
+
stepKeyFor={stepKey}
|
|
1238
|
+
scaleCurveKeyFor={getScaleCurveKey}
|
|
1239
|
+
onToggleSnap={toggleSnapAll}
|
|
1240
|
+
onClearScaleOverrides={clearScaleOverrides}
|
|
1241
|
+
onToggleEditor={toggleScaleEditor}
|
|
1242
|
+
onResetOverride={resetOverride}
|
|
1243
|
+
onOverrideClick={handleOverrideClick}
|
|
1244
|
+
onSnappedClick={handleSnappedClick}
|
|
1245
|
+
onSelectSnapValue={selectSnapValue}
|
|
1246
|
+
onCopyHex={copyHex}
|
|
1247
|
+
onCopyVarName={copyVarName}
|
|
1248
|
+
onSetScaleCurve={setScaleCurve}
|
|
1249
|
+
onOffsetChange={handleOffset}
|
|
1250
|
+
/>
|
|
1251
|
+
{/each}
|
|
1252
|
+
</div>
|
|
1253
|
+
<!-- Surfaces & Borders for gray mode -->
|
|
1254
|
+
<div class="scales-row">
|
|
1255
|
+
{#each grayScales.filter(s => !s.isText) as scale}
|
|
1256
|
+
<OverridesPanel
|
|
1257
|
+
{scale}
|
|
1258
|
+
editorOpen={scaleEditorOpen[scale.title] ?? false}
|
|
1259
|
+
snapped={false}
|
|
1260
|
+
supportsSnap={false}
|
|
1261
|
+
{cssNamespace}
|
|
1262
|
+
{scaleCurves}
|
|
1263
|
+
{curveOffset}
|
|
1264
|
+
{defaultScaleCurves}
|
|
1265
|
+
{overrides}
|
|
1266
|
+
{editingKey}
|
|
1267
|
+
{snapPickerKey}
|
|
1268
|
+
{copiedKey}
|
|
1269
|
+
{copiedLabelKey}
|
|
1270
|
+
{paletteComputed}
|
|
1271
|
+
derivedHexFor={derivedHexForGray}
|
|
1272
|
+
effectiveHexFor={effectiveHexAny}
|
|
1273
|
+
stepKeyFor={stepKey}
|
|
1274
|
+
scaleCurveKeyFor={getScaleCurveKey}
|
|
1275
|
+
onToggleSnap={toggleSnapAll}
|
|
1276
|
+
onClearScaleOverrides={clearScaleOverrides}
|
|
1277
|
+
onToggleEditor={toggleScaleEditor}
|
|
1278
|
+
onResetOverride={resetOverride}
|
|
1279
|
+
onOverrideClick={handleOverrideClick}
|
|
1280
|
+
onSnappedClick={handleSnappedClick}
|
|
1281
|
+
onSelectSnapValue={selectSnapValue}
|
|
1282
|
+
onCopyHex={copyHex}
|
|
1283
|
+
onCopyVarName={copyVarName}
|
|
1284
|
+
onSetScaleCurve={setScaleCurve}
|
|
1285
|
+
onOffsetChange={handleOffset}
|
|
1286
|
+
/>
|
|
1287
|
+
{/each}
|
|
1288
|
+
</div>
|
|
1289
|
+
{/if}
|
|
1290
|
+
{/if}
|
|
1291
|
+
|
|
1292
|
+
<!-- Color Edit Panel (non-base edits) -->
|
|
1293
|
+
{#if !isEditingBase && panelOpen && editingColor}
|
|
1294
|
+
<ColorEditPanel
|
|
1295
|
+
color={editingColor}
|
|
1296
|
+
title={editPanelTitle}
|
|
1297
|
+
showRemoveOverride={!!editingKey}
|
|
1298
|
+
mode={'hsl'}
|
|
1299
|
+
hue={tintHue}
|
|
1300
|
+
chroma={tintChroma}
|
|
1301
|
+
onHueChromaChange={(h, c) => patchPalette({ tintHue: h, tintChroma: c }, 'tint hue/chroma')}
|
|
1302
|
+
onColorChange={handleColorChange}
|
|
1303
|
+
onConfirm={confirmEdit}
|
|
1304
|
+
onCancel={cancelEdit}
|
|
1305
|
+
onRemoveOverride={() => editingKey && removeOverride(editingKey)}
|
|
1306
|
+
onSliderStart={() => beginSliderGesture(`edit ${label} ${editingKey ?? 'color'}`)}
|
|
1307
|
+
/>
|
|
1308
|
+
{/if}
|
|
1309
|
+
</div>
|
|
1310
|
+
|
|
1311
|
+
<style>
|
|
1312
|
+
.palette-editor {
|
|
1313
|
+
display: flex;
|
|
1314
|
+
flex-direction: column;
|
|
1315
|
+
gap: var(--ui-space-20);
|
|
1316
|
+
padding: var(--ui-space-16) var(--ui-space-16) var(--ui-space-24);
|
|
1317
|
+
background: none;
|
|
1318
|
+
border: none;
|
|
1319
|
+
border-bottom: 1px solid var(--ui-border-faint);
|
|
1320
|
+
font-family: var(--ui-font-sans);
|
|
1321
|
+
min-width: 0;
|
|
1322
|
+
}
|
|
1323
|
+
|
|
1324
|
+
.palette-editor:last-child {
|
|
1325
|
+
border-bottom: none;
|
|
1326
|
+
}
|
|
1327
|
+
|
|
1328
|
+
/* Scale header with edit button */
|
|
1329
|
+
|
|
1330
|
+
.scale-header {
|
|
1331
|
+
display: flex;
|
|
1332
|
+
align-items: center;
|
|
1333
|
+
gap: var(--ui-space-8);
|
|
1334
|
+
}
|
|
1335
|
+
|
|
1336
|
+
.edit-toggle {
|
|
1337
|
+
font-size: var(--ui-font-size-md);
|
|
1338
|
+
color: var(--ui-text-tertiary);
|
|
1339
|
+
background: none;
|
|
1340
|
+
border: 1px solid var(--ui-border-subtle);
|
|
1341
|
+
border-radius: var(--ui-radius-sm);
|
|
1342
|
+
padding: var(--ui-space-2) var(--ui-space-6);
|
|
1343
|
+
cursor: pointer;
|
|
1344
|
+
}
|
|
1345
|
+
|
|
1346
|
+
.edit-toggle:hover {
|
|
1347
|
+
color: var(--ui-text-primary);
|
|
1348
|
+
border-color: var(--ui-border-medium);
|
|
1349
|
+
}
|
|
1350
|
+
|
|
1351
|
+
.derived-toggle {
|
|
1352
|
+
display: flex;
|
|
1353
|
+
align-items: center;
|
|
1354
|
+
gap: var(--ui-space-8);
|
|
1355
|
+
padding: var(--ui-space-6) var(--ui-space-4);
|
|
1356
|
+
background: none;
|
|
1357
|
+
border: none;
|
|
1358
|
+
color: var(--ui-text-tertiary);
|
|
1359
|
+
font-size: var(--ui-font-size-sm);
|
|
1360
|
+
font-weight: var(--ui-font-weight-semibold);
|
|
1361
|
+
cursor: pointer;
|
|
1362
|
+
transition: color var(--ui-transition-fast);
|
|
1363
|
+
text-transform: uppercase;
|
|
1364
|
+
letter-spacing: 0.04em;
|
|
1365
|
+
}
|
|
1366
|
+
|
|
1367
|
+
.derived-toggle:hover {
|
|
1368
|
+
color: var(--ui-text-secondary);
|
|
1369
|
+
}
|
|
1370
|
+
|
|
1371
|
+
.derived-toggle i {
|
|
1372
|
+
font-size: var(--ui-font-size-xs);
|
|
1373
|
+
width: 0.75rem;
|
|
1374
|
+
text-align: center;
|
|
1375
|
+
}
|
|
1376
|
+
|
|
1377
|
+
/* Scale layout */
|
|
1378
|
+
|
|
1379
|
+
.scales-row {
|
|
1380
|
+
display: flex;
|
|
1381
|
+
gap: 3rem;
|
|
1382
|
+
flex-wrap: wrap;
|
|
1383
|
+
min-width: 0;
|
|
1384
|
+
}
|
|
1385
|
+
|
|
1386
|
+
.scale-section {
|
|
1387
|
+
display: flex;
|
|
1388
|
+
flex-direction: column;
|
|
1389
|
+
gap: var(--ui-space-6);
|
|
1390
|
+
min-width: 0;
|
|
1391
|
+
max-width: 100%;
|
|
1392
|
+
}
|
|
1393
|
+
|
|
1394
|
+
.scale-title {
|
|
1395
|
+
font-size: var(--ui-font-size-md);
|
|
1396
|
+
font-weight: var(--ui-font-weight-semibold);
|
|
1397
|
+
color: var(--ui-text-tertiary);
|
|
1398
|
+
margin: 0;
|
|
1399
|
+
text-transform: uppercase;
|
|
1400
|
+
letter-spacing: 0.05em;
|
|
1401
|
+
}
|
|
1402
|
+
|
|
1403
|
+
/* Step columns */
|
|
1404
|
+
|
|
1405
|
+
.step-column {
|
|
1406
|
+
display: flex;
|
|
1407
|
+
flex-direction: column;
|
|
1408
|
+
align-items: stretch;
|
|
1409
|
+
justify-self: stretch;
|
|
1410
|
+
gap: var(--ui-space-2);
|
|
1411
|
+
width: auto;
|
|
1412
|
+
min-width: 0;
|
|
1413
|
+
overflow: visible;
|
|
1414
|
+
}
|
|
1415
|
+
|
|
1416
|
+
.step-label {
|
|
1417
|
+
font-size: var(--ui-font-size-sm);
|
|
1418
|
+
color: var(--ui-text-secondary);
|
|
1419
|
+
text-align: center;
|
|
1420
|
+
line-height: 1;
|
|
1421
|
+
height: var(--ui-font-size-xs);
|
|
1422
|
+
display: flex;
|
|
1423
|
+
align-items: flex-end;
|
|
1424
|
+
}
|
|
1425
|
+
|
|
1426
|
+
.step-label.copyable-label {
|
|
1427
|
+
background: none;
|
|
1428
|
+
border: none;
|
|
1429
|
+
padding: 0;
|
|
1430
|
+
cursor: pointer;
|
|
1431
|
+
font: inherit;
|
|
1432
|
+
font-size: var(--ui-font-size-sm);
|
|
1433
|
+
justify-content: center;
|
|
1434
|
+
transition: color var(--ui-transition-fast);
|
|
1435
|
+
}
|
|
1436
|
+
|
|
1437
|
+
.step-label.copyable-label:hover {
|
|
1438
|
+
color: var(--ui-text-primary);
|
|
1439
|
+
}
|
|
1440
|
+
|
|
1441
|
+
.step-label.copyable-label.copied {
|
|
1442
|
+
color: var(--ui-text-accent, var(--ui-text-primary));
|
|
1443
|
+
}
|
|
1444
|
+
|
|
1445
|
+
/* Swatches */
|
|
1446
|
+
|
|
1447
|
+
.swatch {
|
|
1448
|
+
width: 100%;
|
|
1449
|
+
height: 2rem;
|
|
1450
|
+
border-radius: var(--ui-radius-sm);
|
|
1451
|
+
border: 1px solid var(--ui-border-faint);
|
|
1452
|
+
}
|
|
1453
|
+
|
|
1454
|
+
/* Step hex values */
|
|
1455
|
+
|
|
1456
|
+
.step-hex {
|
|
1457
|
+
font-size: var(--ui-font-size-xs);
|
|
1458
|
+
color: var(--ui-text-secondary);
|
|
1459
|
+
font-family: var(--ui-font-mono);
|
|
1460
|
+
cursor: pointer;
|
|
1461
|
+
padding: 1px var(--ui-space-2);
|
|
1462
|
+
border-radius: var(--ui-radius-sm);
|
|
1463
|
+
white-space: nowrap;
|
|
1464
|
+
background: none;
|
|
1465
|
+
border: none;
|
|
1466
|
+
text-align: center;
|
|
1467
|
+
min-width: 0;
|
|
1468
|
+
overflow: hidden;
|
|
1469
|
+
text-overflow: ellipsis;
|
|
1470
|
+
}
|
|
1471
|
+
|
|
1472
|
+
.step-hex:hover {
|
|
1473
|
+
background: var(--ui-surface-highest);
|
|
1474
|
+
color: var(--ui-text-primary);
|
|
1475
|
+
}
|
|
1476
|
+
|
|
1477
|
+
.step-hex.copied {
|
|
1478
|
+
color: var(--ui-text-accent);
|
|
1479
|
+
}
|
|
1480
|
+
|
|
1481
|
+
/* Swatch grid */
|
|
1482
|
+
|
|
1483
|
+
.swatch-grid {
|
|
1484
|
+
display: grid;
|
|
1485
|
+
grid-template-columns: repeat(var(--swatch-cols), minmax(0, 1fr));
|
|
1486
|
+
gap: var(--ui-space-4) var(--swatch-gap, var(--ui-space-4));
|
|
1487
|
+
align-items: start;
|
|
1488
|
+
justify-content: start;
|
|
1489
|
+
min-width: 0;
|
|
1490
|
+
max-width: calc(var(--swatch-cols) * 4rem + (var(--swatch-cols) - 1) * var(--swatch-gap, var(--ui-space-4)));
|
|
1491
|
+
}
|
|
1492
|
+
|
|
1493
|
+
.curve-grid-span {
|
|
1494
|
+
display: flex;
|
|
1495
|
+
flex-direction: column;
|
|
1496
|
+
gap: var(--ui-space-8);
|
|
1497
|
+
}
|
|
1498
|
+
|
|
1499
|
+
.swatch.gray-swatch {
|
|
1500
|
+
width: 100%;
|
|
1501
|
+
height: calc(4rem + var(--ui-space-2));
|
|
1502
|
+
cursor: pointer;
|
|
1503
|
+
position: relative;
|
|
1504
|
+
}
|
|
1505
|
+
|
|
1506
|
+
.swatch.gray-swatch:hover {
|
|
1507
|
+
border-color: var(--ui-border-medium);
|
|
1508
|
+
}
|
|
1509
|
+
|
|
1510
|
+
.swatch.gray-swatch.active {
|
|
1511
|
+
border-color: var(--ui-border-strong);
|
|
1512
|
+
outline: 2px solid var(--ui-border-medium);
|
|
1513
|
+
outline-offset: 1px;
|
|
1514
|
+
}
|
|
1515
|
+
|
|
1516
|
+
.override-dot {
|
|
1517
|
+
position: absolute;
|
|
1518
|
+
top: 3px;
|
|
1519
|
+
right: 3px;
|
|
1520
|
+
width: 6px;
|
|
1521
|
+
height: 6px;
|
|
1522
|
+
border-radius: 50%;
|
|
1523
|
+
background: var(--ui-text-primary);
|
|
1524
|
+
border: 1px solid rgba(255, 255, 255, 0.6);
|
|
1525
|
+
}
|
|
1526
|
+
|
|
1527
|
+
.empty-mode-toggle {
|
|
1528
|
+
display: flex;
|
|
1529
|
+
align-items: center;
|
|
1530
|
+
gap: var(--ui-space-6);
|
|
1531
|
+
font-size: var(--ui-font-size-md);
|
|
1532
|
+
color: var(--ui-text-secondary);
|
|
1533
|
+
cursor: pointer;
|
|
1534
|
+
user-select: none;
|
|
1535
|
+
}
|
|
1536
|
+
|
|
1537
|
+
.empty-mode-toggle input {
|
|
1538
|
+
margin: 0;
|
|
1539
|
+
cursor: pointer;
|
|
1540
|
+
}
|
|
1541
|
+
|
|
1542
|
+
.empty-check {
|
|
1543
|
+
-webkit-appearance: none;
|
|
1544
|
+
appearance: none;
|
|
1545
|
+
position: absolute;
|
|
1546
|
+
bottom: 3px;
|
|
1547
|
+
right: 3px;
|
|
1548
|
+
margin: 0;
|
|
1549
|
+
cursor: pointer;
|
|
1550
|
+
width: 14px;
|
|
1551
|
+
height: 14px;
|
|
1552
|
+
background: white;
|
|
1553
|
+
border: 1px solid rgba(0, 0, 0, 0.3);
|
|
1554
|
+
border-radius: 2px;
|
|
1555
|
+
opacity: 0.5;
|
|
1556
|
+
}
|
|
1557
|
+
|
|
1558
|
+
.empty-check:checked {
|
|
1559
|
+
opacity: 1;
|
|
1560
|
+
}
|
|
1561
|
+
|
|
1562
|
+
.empty-check:checked::after {
|
|
1563
|
+
content: '\2713';
|
|
1564
|
+
display: flex;
|
|
1565
|
+
align-items: center;
|
|
1566
|
+
justify-content: center;
|
|
1567
|
+
width: 100%;
|
|
1568
|
+
height: 100%;
|
|
1569
|
+
font-size: var(--ui-font-size-md);
|
|
1570
|
+
font-weight: bold;
|
|
1571
|
+
color: black;
|
|
1572
|
+
line-height: 1;
|
|
1573
|
+
}
|
|
1574
|
+
|
|
1575
|
+
/* Narrow desktop: tighten palette editor spacing */
|
|
1576
|
+
@media (max-width: 1280px) {
|
|
1577
|
+
.palette-editor {
|
|
1578
|
+
padding: var(--ui-space-12) var(--ui-space-12) var(--ui-space-20);
|
|
1579
|
+
}
|
|
1580
|
+
.scales-row {
|
|
1581
|
+
gap: var(--ui-space-24);
|
|
1582
|
+
}
|
|
1583
|
+
}
|
|
1584
|
+
|
|
1585
|
+
@media (max-width: 1024px) {
|
|
1586
|
+
.scales-row {
|
|
1587
|
+
gap: var(--ui-space-16);
|
|
1588
|
+
}
|
|
1589
|
+
}
|
|
1590
|
+
</style>
|