@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,1084 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import { createEventDispatcher } from 'svelte';
|
|
3
|
+
import { slide } from 'svelte/transition';
|
|
4
|
+
import { cubicOut, cubicIn } from 'svelte/easing';
|
|
5
|
+
import { resolveAliasChain } from '../lib/tokenRegistry';
|
|
6
|
+
import { editorState } from '../lib/editorStore';
|
|
7
|
+
import { formatGradientStops } from '../lib/slices/gradients';
|
|
8
|
+
import type { GradientToken } from '../lib/editorTypes';
|
|
9
|
+
import UITokenSelector from './UITokenSelector.svelte';
|
|
10
|
+
|
|
11
|
+
/** Honor prefers-reduced-motion: `t()` zeroes durations when the OS asks for
|
|
12
|
+
* less motion, so the detail-row enter/exit slides skip outright. Mirrors
|
|
13
|
+
* the pattern used in UIPaddingSelector for split↔merge transitions. */
|
|
14
|
+
const reduceMotion = typeof window !== 'undefined'
|
|
15
|
+
&& window.matchMedia?.('(prefers-reduced-motion: reduce)').matches === true;
|
|
16
|
+
const t = (ms: number) => (reduceMotion ? 0 : ms);
|
|
17
|
+
|
|
18
|
+
/** Tokens that match the surface/fill suffix but live in <color>-only CSS contexts
|
|
19
|
+
(color-mix, box-shadow color slot) where a gradient would invalidate the declaration. */
|
|
20
|
+
const GRADIENT_DENYLIST = new Set<string>([
|
|
21
|
+
'--radiobutton-default-surface',
|
|
22
|
+
'--radiobutton-hover-surface',
|
|
23
|
+
'--radiobutton-active-surface',
|
|
24
|
+
]);
|
|
25
|
+
|
|
26
|
+
/** Slot kinds where a gradient is a renderable assignment. */
|
|
27
|
+
function acceptsGradient(name: string): boolean {
|
|
28
|
+
if (GRADIENT_DENYLIST.has(name)) return false;
|
|
29
|
+
return name.endsWith('-surface') || name.endsWith('-fill');
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const dispatch = createEventDispatcher();
|
|
33
|
+
|
|
34
|
+
export let variable: string;
|
|
35
|
+
export let component: string | undefined = undefined;
|
|
36
|
+
export let canBeLinked: boolean = false;
|
|
37
|
+
export let disabled: boolean = false;
|
|
38
|
+
export let selectionsLocked: boolean = false;
|
|
39
|
+
|
|
40
|
+
type Category = 'palette' | 'surface' | 'border' | 'text';
|
|
41
|
+
|
|
42
|
+
const families = [
|
|
43
|
+
{ name: 'neutral', label: 'Neutral' },
|
|
44
|
+
{ name: 'alternate', label: 'Alternate' },
|
|
45
|
+
{ name: 'canvas', label: 'Canvas' },
|
|
46
|
+
{ name: 'brand', label: 'Brand' },
|
|
47
|
+
{ name: 'accent', label: 'Accent' },
|
|
48
|
+
{ name: 'special', label: 'Special' },
|
|
49
|
+
{ name: 'success', label: 'Success' },
|
|
50
|
+
{ name: 'warning', label: 'Warning' },
|
|
51
|
+
{ name: 'info', label: 'Info' },
|
|
52
|
+
{ name: 'danger', label: 'Danger' },
|
|
53
|
+
];
|
|
54
|
+
|
|
55
|
+
const familyNames = families.map(f => f.name);
|
|
56
|
+
|
|
57
|
+
const paletteSteps = ['100', '200', '300', '400', '500', '600', '700', '800', '850', '900', '950'];
|
|
58
|
+
|
|
59
|
+
const surfaceSteps = [
|
|
60
|
+
{ key: 'lowest', label: 'Lowest' },
|
|
61
|
+
{ key: 'lower', label: 'Lower' },
|
|
62
|
+
{ key: 'low', label: 'Low' },
|
|
63
|
+
{ key: '', label: 'Default' },
|
|
64
|
+
{ key: 'high', label: 'High' },
|
|
65
|
+
{ key: 'higher', label: 'Higher' },
|
|
66
|
+
{ key: 'highest', label: 'Highest' },
|
|
67
|
+
];
|
|
68
|
+
|
|
69
|
+
const borderSteps = [
|
|
70
|
+
{ key: 'faint', label: 'Faint' },
|
|
71
|
+
{ key: 'subtle', label: 'Subtle' },
|
|
72
|
+
{ key: '', label: 'Default' },
|
|
73
|
+
{ key: 'medium', label: 'Medium' },
|
|
74
|
+
{ key: 'strong', label: 'Strong' },
|
|
75
|
+
];
|
|
76
|
+
|
|
77
|
+
const textSteps = [
|
|
78
|
+
{ key: 'primary', label: 'Primary' },
|
|
79
|
+
{ key: 'secondary', label: 'Secondary' },
|
|
80
|
+
{ key: 'tertiary', label: 'Tertiary' },
|
|
81
|
+
{ key: 'muted', label: 'Muted' },
|
|
82
|
+
{ key: 'disabled', label: 'Disabled' },
|
|
83
|
+
];
|
|
84
|
+
|
|
85
|
+
const surfaceStepKeys = surfaceSteps.map(s => s.key);
|
|
86
|
+
const borderStepKeys = borderSteps.map(s => s.key);
|
|
87
|
+
const textStepKeys = textSteps.map(s => s.key);
|
|
88
|
+
|
|
89
|
+
const familiesWithText = ['neutral', 'canvas', 'brand', 'accent', 'special', 'success', 'warning', 'info', 'danger'];
|
|
90
|
+
|
|
91
|
+
const allCategories: { id: Category; label: string }[] = [
|
|
92
|
+
{ id: 'palette', label: 'Palette' },
|
|
93
|
+
{ id: 'surface', label: 'Surface' },
|
|
94
|
+
{ id: 'border', label: 'Border' },
|
|
95
|
+
{ id: 'text', label: 'Text' },
|
|
96
|
+
];
|
|
97
|
+
|
|
98
|
+
let selector: UITokenSelector;
|
|
99
|
+
let selectedFamily: string | null = null;
|
|
100
|
+
let selectedTab: Category = 'palette';
|
|
101
|
+
|
|
102
|
+
/** Compass-rose layout for the orientation grid. Angles follow the CSS
|
|
103
|
+
* linear-gradient convention (0° points up, 90° points right). */
|
|
104
|
+
const directionGrid: { angle: number; glyph: string; col: number; row: number; label: string }[] = [
|
|
105
|
+
{ angle: 315, glyph: '↖', col: 1, row: 1, label: 'top-left' },
|
|
106
|
+
{ angle: 0, glyph: '↑', col: 2, row: 1, label: 'top' },
|
|
107
|
+
{ angle: 45, glyph: '↗', col: 3, row: 1, label: 'top-right' },
|
|
108
|
+
{ angle: 270, glyph: '←', col: 1, row: 2, label: 'left' },
|
|
109
|
+
{ angle: 90, glyph: '→', col: 3, row: 2, label: 'right' },
|
|
110
|
+
{ angle: 225, glyph: '↙', col: 1, row: 3, label: 'bottom-left' },
|
|
111
|
+
{ angle: 180, glyph: '↓', col: 2, row: 3, label: 'bottom' },
|
|
112
|
+
{ angle: 135, glyph: '↘', col: 3, row: 3, label: 'bottom-right' },
|
|
113
|
+
];
|
|
114
|
+
|
|
115
|
+
/** Numeric input value. Kept in sync with chosenAngle/token-default by the
|
|
116
|
+
* reactive block below; user edits flow back through `applyOrientation`. */
|
|
117
|
+
let angleInput: number = 0;
|
|
118
|
+
|
|
119
|
+
let chosenCategory: Category | null = null;
|
|
120
|
+
let chosenFamily: string | null = null;
|
|
121
|
+
let chosenStep: string | null = null;
|
|
122
|
+
let chosenNone: boolean = false;
|
|
123
|
+
let chosenGradient: string | null = null;
|
|
124
|
+
/** Per-slot angle override on the chosen linear gradient. Null means
|
|
125
|
+
* "no override" — the slot writes `var(--gradient-N)` and inherits the
|
|
126
|
+
* token's natural angle. Non-null means the slot writes a materialized
|
|
127
|
+
* `linear-gradient(<angle>, <token's stops>)` so the angle is locally
|
|
128
|
+
* pinned while stop colors keep flowing from the token's `var()` refs. */
|
|
129
|
+
let chosenAngle: number | null = null;
|
|
130
|
+
let opacity: number = 100;
|
|
131
|
+
let selfDefaultHex: string = '';
|
|
132
|
+
|
|
133
|
+
$: gradientsAllowed = acceptsGradient(variable);
|
|
134
|
+
$: gradientTokens = $editorState.gradients.tokens;
|
|
135
|
+
|
|
136
|
+
function captureSelfDefault() {
|
|
137
|
+
const root = document.documentElement;
|
|
138
|
+
const inline = root.style.getPropertyValue(variable);
|
|
139
|
+
if (inline) root.style.removeProperty(variable);
|
|
140
|
+
selfDefaultHex = rgbToHex(getComputedStyle(root).getPropertyValue(variable).trim());
|
|
141
|
+
if (inline) root.style.setProperty(variable, inline);
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
function previewBg(category: Category, family: string, step: string): string {
|
|
145
|
+
const varName = getVarName(category, family, step);
|
|
146
|
+
if (varName === variable) return selfDefaultHex || `var(${varName})`;
|
|
147
|
+
return `var(${varName})`;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
function rgbToHex(value: string): string {
|
|
151
|
+
const m = value.match(/rgba?\s*\(\s*(\d+)\s*,\s*(\d+)\s*,\s*(\d+)/i);
|
|
152
|
+
if (!m) return value;
|
|
153
|
+
const toHex = (n: number) => Math.max(0, Math.min(255, n)).toString(16).padStart(2, '0');
|
|
154
|
+
return `#${toHex(parseInt(m[1]))}${toHex(parseInt(m[2]))}${toHex(parseInt(m[3]))}`;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
function getVarName(category: Category, family: string, stepKey: string): string {
|
|
158
|
+
switch (category) {
|
|
159
|
+
case 'palette':
|
|
160
|
+
return `--color-${family}-${stepKey}`;
|
|
161
|
+
case 'surface':
|
|
162
|
+
return stepKey ? `--surface-${family}-${stepKey}` : `--surface-${family}`;
|
|
163
|
+
case 'border':
|
|
164
|
+
return stepKey ? `--border-${family}-${stepKey}` : `--border-${family}`;
|
|
165
|
+
case 'text':
|
|
166
|
+
if (family === 'neutral') return `--text-${stepKey}`;
|
|
167
|
+
if (stepKey === 'primary') return `--text-${family}`;
|
|
168
|
+
return `--text-${family}-${stepKey}`;
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
function parseTextVarName(varName: string): { family: string; step: string } | null {
|
|
173
|
+
if (varName === '--text-primary') return { family: 'neutral', step: 'primary' };
|
|
174
|
+
if (varName === '--text-secondary') return { family: 'neutral', step: 'secondary' };
|
|
175
|
+
if (varName === '--text-tertiary') return { family: 'neutral', step: 'tertiary' };
|
|
176
|
+
if (varName === '--text-muted') return { family: 'neutral', step: 'muted' };
|
|
177
|
+
if (varName === '--text-disabled') return { family: 'neutral', step: 'disabled' };
|
|
178
|
+
const m = varName.match(/^--text-([a-z]+)(?:-([a-z]+))?$/);
|
|
179
|
+
if (m) {
|
|
180
|
+
const fam = m[1];
|
|
181
|
+
const hier = m[2] || 'primary';
|
|
182
|
+
if (familiesWithText.includes(fam) && fam !== 'neutral' && textStepKeys.includes(hier)) {
|
|
183
|
+
return { family: fam, step: hier };
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
return null;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
function parseRef(value: string): { category: Category; family: string; step: string } | null {
|
|
190
|
+
const varMatch = value.match(/var\((--[a-z0-9-]+)\)/);
|
|
191
|
+
if (!varMatch) return null;
|
|
192
|
+
const varName = varMatch[1];
|
|
193
|
+
|
|
194
|
+
const paletteM = varName.match(/^--color-([a-z]+)-(\d{3})$/);
|
|
195
|
+
if (paletteM && familyNames.includes(paletteM[1]) && paletteSteps.includes(paletteM[2])) {
|
|
196
|
+
return { category: 'palette', family: paletteM[1], step: paletteM[2] };
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
const surfaceM = varName.match(/^--surface-([a-z]+)(?:-([a-z]+))?$/);
|
|
200
|
+
if (surfaceM && familyNames.includes(surfaceM[1])) {
|
|
201
|
+
const step = surfaceM[2] || '';
|
|
202
|
+
if (surfaceStepKeys.includes(step)) {
|
|
203
|
+
return { category: 'surface', family: surfaceM[1], step };
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
const borderM = varName.match(/^--border-([a-z]+)(?:-([a-z]+))?$/);
|
|
208
|
+
if (borderM && familyNames.includes(borderM[1])) {
|
|
209
|
+
const step = borderM[2] || '';
|
|
210
|
+
if (borderStepKeys.includes(step)) {
|
|
211
|
+
return { category: 'border', family: borderM[1], step };
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
const textResult = parseTextVarName(varName);
|
|
216
|
+
if (textResult) {
|
|
217
|
+
return { category: 'text', ...textResult };
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
return null;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
function parseOpacity(raw: string): { inner: string; opacity: number } | null {
|
|
224
|
+
const m = raw.match(/^color-mix\(in srgb,\s*(var\(--[a-z0-9-]+\))\s+(\d+)%,\s*transparent\)$/);
|
|
225
|
+
if (!m) return null;
|
|
226
|
+
return { inner: m[1], opacity: parseInt(m[2]) };
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
function buildValue(varName: string): string | null {
|
|
230
|
+
if (varName === variable && opacity >= 100) return null;
|
|
231
|
+
if (opacity >= 100) return varName;
|
|
232
|
+
return `color-mix(in srgb, var(${varName}) ${opacity}%, transparent)`;
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
function applyOpacity() {
|
|
236
|
+
opacity = Math.max(0, Math.min(100, Math.round(opacity)));
|
|
237
|
+
if (chosenCategory === null || chosenFamily === null || chosenStep === null) return;
|
|
238
|
+
const varName = getVarName(chosenCategory, chosenFamily, chosenStep);
|
|
239
|
+
selector.writeOverride(buildValue(varName));
|
|
240
|
+
dispatch('change');
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
/** Apply (or clear) a per-slot angle override on the chosen linear gradient.
|
|
244
|
+
* When the requested angle matches the gradient token's own natural angle
|
|
245
|
+
* we drop back to the cleaner `var(--gradient-N)` form — that way dialing
|
|
246
|
+
* back to default and pressing the reset button arrive at the same value. */
|
|
247
|
+
function applyOrientation(nextAngle: number) {
|
|
248
|
+
if (chosenGradient === null) return;
|
|
249
|
+
const token = getGradientToken(chosenGradient);
|
|
250
|
+
if (!token || token.type !== 'linear') return;
|
|
251
|
+
const normalized = ((Math.round(nextAngle) % 360) + 360) % 360;
|
|
252
|
+
chosenAngle = normalized;
|
|
253
|
+
if (normalized === token.angle) {
|
|
254
|
+
selector.writeOverride(chosenGradient);
|
|
255
|
+
} else {
|
|
256
|
+
selector.writeOverride(materializeGradient(token, normalized));
|
|
257
|
+
}
|
|
258
|
+
dispatch('change');
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
function resetOrientation() {
|
|
262
|
+
if (chosenGradient === null) return;
|
|
263
|
+
chosenAngle = null;
|
|
264
|
+
selector.writeOverride(chosenGradient);
|
|
265
|
+
dispatch('change');
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
function isGradientToken(name: string): boolean {
|
|
269
|
+
return gradientTokens.some((g) => g.variable === name);
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
function getGradientToken(name: string): GradientToken | undefined {
|
|
273
|
+
return gradientTokens.find((g) => g.variable === name);
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
/** Normalize whitespace so a slot's stored linear-gradient string compares
|
|
277
|
+
* equal to the canonical `formatGradientStops` output regardless of how it
|
|
278
|
+
* was last serialized through the DOM / persistence layer. */
|
|
279
|
+
function normStops(s: string): string {
|
|
280
|
+
return s.replace(/\s+/g, ' ').trim();
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
/** Match a materialized linear-gradient back to its source gradient token by
|
|
284
|
+
* comparing the inlined stop list. We only need to identify linear tokens
|
|
285
|
+
* here because angle override doesn't apply to radial. */
|
|
286
|
+
function identifyMaterializedGradient(raw: string): { variable: string; angle: number } | null {
|
|
287
|
+
const m = raw.match(/^linear-gradient\(\s*([\d.]+)deg\s*,\s*(.+)\)\s*$/);
|
|
288
|
+
if (!m) return null;
|
|
289
|
+
const angle = parseFloat(m[1]);
|
|
290
|
+
const stops = normStops(m[2]);
|
|
291
|
+
for (const t of gradientTokens) {
|
|
292
|
+
if (t.type !== 'linear') continue;
|
|
293
|
+
if (normStops(formatGradientStops(t)) === stops) {
|
|
294
|
+
return { variable: t.variable, angle };
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
return null;
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
function materializeGradient(token: GradientToken, angle: number): string {
|
|
301
|
+
return `linear-gradient(${angle}deg, ${formatGradientStops(token)})`;
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
function initFromCurrent() {
|
|
305
|
+
const raw = document.documentElement.style.getPropertyValue(variable).trim();
|
|
306
|
+
|
|
307
|
+
if (raw === 'transparent') {
|
|
308
|
+
chosenNone = true;
|
|
309
|
+
chosenCategory = null;
|
|
310
|
+
chosenFamily = null;
|
|
311
|
+
chosenStep = null;
|
|
312
|
+
chosenGradient = null;
|
|
313
|
+
chosenAngle = null;
|
|
314
|
+
opacity = 100;
|
|
315
|
+
return;
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
chosenNone = false;
|
|
319
|
+
|
|
320
|
+
if (gradientsAllowed) {
|
|
321
|
+
const gradMatch = raw.match(/^var\((--[a-z0-9-]+)\)$/);
|
|
322
|
+
if (gradMatch && isGradientToken(gradMatch[1])) {
|
|
323
|
+
chosenGradient = gradMatch[1];
|
|
324
|
+
chosenCategory = null;
|
|
325
|
+
chosenFamily = null;
|
|
326
|
+
chosenStep = null;
|
|
327
|
+
chosenAngle = null;
|
|
328
|
+
opacity = 100;
|
|
329
|
+
return;
|
|
330
|
+
}
|
|
331
|
+
// Materialized form: `linear-gradient(<angle>, <token's stops>)`
|
|
332
|
+
// — an angle override pinned locally while stops still carry var() refs
|
|
333
|
+
// back to palette tokens.
|
|
334
|
+
const materialized = identifyMaterializedGradient(raw);
|
|
335
|
+
if (materialized) {
|
|
336
|
+
chosenGradient = materialized.variable;
|
|
337
|
+
chosenCategory = null;
|
|
338
|
+
chosenFamily = null;
|
|
339
|
+
chosenStep = null;
|
|
340
|
+
chosenAngle = materialized.angle;
|
|
341
|
+
opacity = 100;
|
|
342
|
+
return;
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
chosenGradient = null;
|
|
346
|
+
chosenAngle = null;
|
|
347
|
+
|
|
348
|
+
const opacityParsed = parseOpacity(raw);
|
|
349
|
+
if (opacityParsed) {
|
|
350
|
+
const parsed = parseRef(opacityParsed.inner);
|
|
351
|
+
if (parsed) {
|
|
352
|
+
chosenCategory = parsed.category;
|
|
353
|
+
chosenFamily = parsed.family;
|
|
354
|
+
chosenStep = parsed.step;
|
|
355
|
+
opacity = opacityParsed.opacity;
|
|
356
|
+
return;
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
const parsed = raw ? parseRef(raw) : null;
|
|
361
|
+
if (parsed) {
|
|
362
|
+
chosenCategory = parsed.category;
|
|
363
|
+
chosenFamily = parsed.family;
|
|
364
|
+
chosenStep = parsed.step;
|
|
365
|
+
opacity = 100;
|
|
366
|
+
return;
|
|
367
|
+
}
|
|
368
|
+
for (const alias of resolveAliasChain(variable)) {
|
|
369
|
+
const aliasParsed = parseRef(`var(${alias})`);
|
|
370
|
+
if (aliasParsed) {
|
|
371
|
+
chosenCategory = aliasParsed.category;
|
|
372
|
+
chosenFamily = aliasParsed.family;
|
|
373
|
+
chosenStep = aliasParsed.step;
|
|
374
|
+
opacity = 100;
|
|
375
|
+
return;
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
chosenCategory = null;
|
|
379
|
+
chosenFamily = null;
|
|
380
|
+
chosenStep = null;
|
|
381
|
+
opacity = 100;
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
function handleReset() {
|
|
385
|
+
opacity = 100;
|
|
386
|
+
chosenNone = false;
|
|
387
|
+
initFromCurrent();
|
|
388
|
+
selectedFamily = null;
|
|
389
|
+
dispatch('change');
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
function handleClose() {
|
|
393
|
+
selectedFamily = null;
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
function selectFamily(name: string) {
|
|
397
|
+
selectedFamily = name;
|
|
398
|
+
if (name === chosenFamily && chosenCategory) {
|
|
399
|
+
selectedTab = chosenCategory;
|
|
400
|
+
}
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
function backToFamilies() {
|
|
404
|
+
selectedFamily = null;
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
function selectNone(close: () => void) {
|
|
408
|
+
chosenNone = true;
|
|
409
|
+
chosenCategory = null;
|
|
410
|
+
chosenFamily = null;
|
|
411
|
+
chosenStep = null;
|
|
412
|
+
chosenGradient = null;
|
|
413
|
+
chosenAngle = null;
|
|
414
|
+
opacity = 100;
|
|
415
|
+
selector.writeOverride('transparent');
|
|
416
|
+
selectedFamily = null;
|
|
417
|
+
close();
|
|
418
|
+
dispatch('change');
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
function selectSwatch(category: Category, step: string, close: () => void) {
|
|
422
|
+
const varName = getVarName(category, selectedFamily!, step);
|
|
423
|
+
chosenNone = false;
|
|
424
|
+
chosenGradient = null;
|
|
425
|
+
chosenAngle = null;
|
|
426
|
+
chosenCategory = category;
|
|
427
|
+
chosenFamily = selectedFamily;
|
|
428
|
+
chosenStep = step;
|
|
429
|
+
selector.writeOverride(buildValue(varName));
|
|
430
|
+
selectedFamily = null;
|
|
431
|
+
close();
|
|
432
|
+
dispatch('change');
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
// Picking any gradient is a fresh start: any prior angle override is
|
|
436
|
+
// discarded and the slot adopts the new token's natural orientation by
|
|
437
|
+
// writing `var(--gradient-N)`. The orientation control then displays the
|
|
438
|
+
// token's default angle but with no local override active.
|
|
439
|
+
function selectGradient(gradientVar: string, close: () => void) {
|
|
440
|
+
chosenNone = false;
|
|
441
|
+
chosenCategory = null;
|
|
442
|
+
chosenFamily = null;
|
|
443
|
+
chosenStep = null;
|
|
444
|
+
chosenGradient = gradientVar;
|
|
445
|
+
chosenAngle = null;
|
|
446
|
+
opacity = 100;
|
|
447
|
+
selector.writeOverride(gradientVar);
|
|
448
|
+
selectedFamily = null;
|
|
449
|
+
close();
|
|
450
|
+
dispatch('change');
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
// Re-derive trigger state when the bound `variable` changes (e.g. when a
|
|
454
|
+
// VariantGroup tabs view reuses the same selector instance across states).
|
|
455
|
+
// The wrapper UITokenSelector forwards `var-change` only for the currently
|
|
456
|
+
// bound variable, so prop swaps wouldn't otherwise refresh `chosenCategory`
|
|
457
|
+
// / `chosenFamily` / `chosenStep` and the meta label drifts from the swatch.
|
|
458
|
+
let lastSeenVariable: string | null = null;
|
|
459
|
+
$: if (variable !== lastSeenVariable) {
|
|
460
|
+
lastSeenVariable = variable;
|
|
461
|
+
initFromCurrent();
|
|
462
|
+
captureSelfDefault();
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
$: chosenGradientToken = chosenGradient ? getGradientToken(chosenGradient) : undefined;
|
|
466
|
+
$: isLinearGradientChosen = !!chosenGradientToken && chosenGradientToken.type === 'linear';
|
|
467
|
+
$: effectiveAngle = chosenGradientToken
|
|
468
|
+
? (chosenAngle ?? chosenGradientToken.angle)
|
|
469
|
+
: 0;
|
|
470
|
+
$: angleInput = effectiveAngle;
|
|
471
|
+
|
|
472
|
+
$: triggerMeta = chosenNone
|
|
473
|
+
? 'none'
|
|
474
|
+
: chosenGradient
|
|
475
|
+
? chosenGradient.replace(/^--/, '') + (chosenAngle !== null ? ` (${effectiveAngle}°)` : '')
|
|
476
|
+
: (chosenCategory && chosenFamily && chosenStep !== null
|
|
477
|
+
? getVarName(chosenCategory, chosenFamily, chosenStep).replace(/^--/, '') + (opacity < 100 ? ` (${opacity}%)` : '')
|
|
478
|
+
: '');
|
|
479
|
+
|
|
480
|
+
$: availableTabs = selectedFamily
|
|
481
|
+
? allCategories.filter(c => c.id !== 'text' || familiesWithText.includes(selectedFamily!))
|
|
482
|
+
: allCategories;
|
|
483
|
+
|
|
484
|
+
$: if (selectedFamily && !availableTabs.find(t => t.id === selectedTab)) {
|
|
485
|
+
selectedTab = 'palette';
|
|
486
|
+
}
|
|
487
|
+
</script>
|
|
488
|
+
|
|
489
|
+
<UITokenSelector
|
|
490
|
+
bind:this={selector}
|
|
491
|
+
{variable}
|
|
492
|
+
{component}
|
|
493
|
+
{canBeLinked}
|
|
494
|
+
{disabled}
|
|
495
|
+
{selectionsLocked}
|
|
496
|
+
dropdownMinWidth="14rem"
|
|
497
|
+
dropdownMaxWidth="calc(100vw - 2rem)"
|
|
498
|
+
hideDefaultHeader={!!selectedFamily}
|
|
499
|
+
on:reset={handleReset}
|
|
500
|
+
on:close={handleClose}
|
|
501
|
+
on:var-change={initFromCurrent}
|
|
502
|
+
>
|
|
503
|
+
<div slot="trigger-preview" class="swatch-wrap">
|
|
504
|
+
<div class="swatch" style="background: var({variable});"></div>
|
|
505
|
+
</div>
|
|
506
|
+
<div slot="subheader" class="opacity-control" class:hidden={chosenGradient !== null}>
|
|
507
|
+
<span class="opacity-label">opacity</span>
|
|
508
|
+
<input type="range" min="0" max="100" bind:value={opacity} class="opacity-slider" on:input={applyOpacity} />
|
|
509
|
+
<input type="number" min="0" max="100" bind:value={opacity} class="opacity-input" on:change={applyOpacity} />
|
|
510
|
+
<span class="opacity-unit">%</span>
|
|
511
|
+
</div>
|
|
512
|
+
<svelte:fragment slot="trigger-meta">{triggerMeta}</svelte:fragment>
|
|
513
|
+
|
|
514
|
+
<svelte:fragment let:close>
|
|
515
|
+
{#if selectedFamily === null}
|
|
516
|
+
<div class="family-list">
|
|
517
|
+
<button class="family-item" class:active={chosenNone} on:click={() => selectNone(close)}>
|
|
518
|
+
<div class="family-swatches">
|
|
519
|
+
<div class="none-swatch"></div>
|
|
520
|
+
</div>
|
|
521
|
+
<span class="family-label">None</span>
|
|
522
|
+
</button>
|
|
523
|
+
{#each families as fam}
|
|
524
|
+
<button class="family-item" class:active={!chosenNone && chosenFamily === fam.name} on:click={() => selectFamily(fam.name)}>
|
|
525
|
+
<div class="family-swatches">
|
|
526
|
+
<div class="mini-swatch" style="background: var(--color-{fam.name}-300);"></div>
|
|
527
|
+
<div class="mini-swatch" style="background: var(--color-{fam.name}-500);"></div>
|
|
528
|
+
<div class="mini-swatch" style="background: var(--color-{fam.name}-700);"></div>
|
|
529
|
+
</div>
|
|
530
|
+
<span class="family-label">{fam.label}</span>
|
|
531
|
+
<i class="fas fa-chevron-right family-arrow"></i>
|
|
532
|
+
</button>
|
|
533
|
+
{/each}
|
|
534
|
+
{#if gradientsAllowed && gradientTokens.length > 0}
|
|
535
|
+
<div class="family-divider">Gradients</div>
|
|
536
|
+
{#each gradientTokens as g}
|
|
537
|
+
<button class="family-item" class:active={chosenGradient === g.variable} on:click={() => selectGradient(g.variable, close)}>
|
|
538
|
+
<div class="family-swatches">
|
|
539
|
+
<div class="gradient-swatch" style="background: var({g.variable});"></div>
|
|
540
|
+
</div>
|
|
541
|
+
<span class="family-label">Gradient {g.variable.replace(/^--gradient-/, '')}</span>
|
|
542
|
+
</button>
|
|
543
|
+
{/each}
|
|
544
|
+
{/if}
|
|
545
|
+
</div>
|
|
546
|
+
{:else}
|
|
547
|
+
<button class="dropdown-back" on:click={backToFamilies}>
|
|
548
|
+
<i class="fas fa-chevron-left"></i>
|
|
549
|
+
<span>{families.find(f => f.name === selectedFamily)?.label}</span>
|
|
550
|
+
</button>
|
|
551
|
+
|
|
552
|
+
<div class="tab-bar">
|
|
553
|
+
{#each availableTabs as tab}
|
|
554
|
+
<button
|
|
555
|
+
class="tab-btn"
|
|
556
|
+
class:selected={selectedTab === tab.id}
|
|
557
|
+
class:assigned={chosenCategory === tab.id && chosenFamily === selectedFamily}
|
|
558
|
+
on:click={() => selectedTab = tab.id}
|
|
559
|
+
>{tab.label}</button>
|
|
560
|
+
{/each}
|
|
561
|
+
</div>
|
|
562
|
+
|
|
563
|
+
{#if selectedTab === 'palette'}
|
|
564
|
+
<div class="step-grid">
|
|
565
|
+
{#each paletteSteps as step}
|
|
566
|
+
<button
|
|
567
|
+
class="step-item"
|
|
568
|
+
class:active={chosenCategory === 'palette' && chosenFamily === selectedFamily && chosenStep === step}
|
|
569
|
+
on:click={() => selectSwatch('palette', step, close)}
|
|
570
|
+
>
|
|
571
|
+
<div class="step-swatch" style="background: var(--color-{selectedFamily}-{step});"></div>
|
|
572
|
+
<span class="step-label">{step}</span>
|
|
573
|
+
</button>
|
|
574
|
+
{/each}
|
|
575
|
+
</div>
|
|
576
|
+
{:else if selectedTab === 'surface'}
|
|
577
|
+
<div class="step-grid">
|
|
578
|
+
{#each surfaceSteps as step}
|
|
579
|
+
<button
|
|
580
|
+
class="step-item"
|
|
581
|
+
class:active={chosenCategory === 'surface' && chosenFamily === selectedFamily && chosenStep === step.key}
|
|
582
|
+
on:click={() => selectSwatch('surface', step.key, close)}
|
|
583
|
+
>
|
|
584
|
+
<div class="step-swatch" style="background: {previewBg('surface', selectedFamily, step.key)};"></div>
|
|
585
|
+
<span class="step-label">{step.label}</span>
|
|
586
|
+
</button>
|
|
587
|
+
{/each}
|
|
588
|
+
</div>
|
|
589
|
+
{:else if selectedTab === 'border'}
|
|
590
|
+
<div class="step-grid">
|
|
591
|
+
{#each borderSteps as step}
|
|
592
|
+
<button
|
|
593
|
+
class="step-item"
|
|
594
|
+
class:active={chosenCategory === 'border' && chosenFamily === selectedFamily && chosenStep === step.key}
|
|
595
|
+
on:click={() => selectSwatch('border', step.key, close)}
|
|
596
|
+
>
|
|
597
|
+
<div class="step-swatch" style="background: {previewBg('border', selectedFamily, step.key)};"></div>
|
|
598
|
+
<span class="step-label">{step.label}</span>
|
|
599
|
+
</button>
|
|
600
|
+
{/each}
|
|
601
|
+
</div>
|
|
602
|
+
{:else if selectedTab === 'text'}
|
|
603
|
+
<div class="step-grid">
|
|
604
|
+
{#each textSteps as step}
|
|
605
|
+
<button
|
|
606
|
+
class="step-item"
|
|
607
|
+
class:active={chosenCategory === 'text' && chosenFamily === selectedFamily && chosenStep === step.key}
|
|
608
|
+
on:click={() => selectSwatch('text', step.key, close)}
|
|
609
|
+
>
|
|
610
|
+
<div class="step-swatch" style="background: {previewBg('text', selectedFamily, step.key)};"></div>
|
|
611
|
+
<span class="step-label">{step.label}</span>
|
|
612
|
+
</button>
|
|
613
|
+
{/each}
|
|
614
|
+
</div>
|
|
615
|
+
{/if}
|
|
616
|
+
{/if}
|
|
617
|
+
</svelte:fragment>
|
|
618
|
+
</UITokenSelector>
|
|
619
|
+
|
|
620
|
+
<!--
|
|
621
|
+
Inline orientation row. When a linear gradient is the chosen value the
|
|
622
|
+
palette row "grows" — UITokenSelector occupies row 1 (cols 2-3 of the
|
|
623
|
+
parent token-row's subgrid via its default `grid-column: span 2`); this
|
|
624
|
+
block lands on row 2, also cols 2-3, sitting directly under the swatch
|
|
625
|
+
trigger. Same pattern padding-sides uses to expand into row 2 of the
|
|
626
|
+
parent token-row, just lighter (no link/merge header chrome).
|
|
627
|
+
-->
|
|
628
|
+
{#if isLinearGradientChosen}
|
|
629
|
+
<div
|
|
630
|
+
class="palette-detail-row"
|
|
631
|
+
in:slide|local={{ duration: t(280), delay: t(60), easing: cubicOut }}
|
|
632
|
+
out:slide|local={{ duration: t(220), easing: cubicIn }}
|
|
633
|
+
>
|
|
634
|
+
<span class="detail-label">orientation</span>
|
|
635
|
+
<div class="orientation-body">
|
|
636
|
+
<div class="dir-grid">
|
|
637
|
+
{#each directionGrid as d}
|
|
638
|
+
<button
|
|
639
|
+
type="button"
|
|
640
|
+
class="dir-btn"
|
|
641
|
+
class:active={d.angle === ((effectiveAngle % 360) + 360) % 360}
|
|
642
|
+
style="grid-column: {d.col}; grid-row: {d.row};"
|
|
643
|
+
on:click={() => applyOrientation(d.angle)}
|
|
644
|
+
title="{d.label} ({d.angle}°)"
|
|
645
|
+
>{d.glyph}</button>
|
|
646
|
+
{/each}
|
|
647
|
+
</div>
|
|
648
|
+
<div class="angle-input-wrap">
|
|
649
|
+
<input
|
|
650
|
+
type="number"
|
|
651
|
+
min="0"
|
|
652
|
+
max="359"
|
|
653
|
+
class="angle-input"
|
|
654
|
+
bind:value={angleInput}
|
|
655
|
+
on:change={() => applyOrientation(angleInput)}
|
|
656
|
+
/>
|
|
657
|
+
<span class="angle-unit">°</span>
|
|
658
|
+
</div>
|
|
659
|
+
<button
|
|
660
|
+
type="button"
|
|
661
|
+
class="orientation-reset"
|
|
662
|
+
class:active={chosenAngle !== null}
|
|
663
|
+
on:click={resetOrientation}
|
|
664
|
+
disabled={chosenAngle === null}
|
|
665
|
+
title="Reset to gradient default"
|
|
666
|
+
>
|
|
667
|
+
<i class="fas fa-rotate-left"></i>
|
|
668
|
+
</button>
|
|
669
|
+
</div>
|
|
670
|
+
</div>
|
|
671
|
+
{/if}
|
|
672
|
+
|
|
673
|
+
<style>
|
|
674
|
+
.swatch-wrap {
|
|
675
|
+
align-self: stretch;
|
|
676
|
+
flex: 1;
|
|
677
|
+
border-radius: var(--ui-radius-sm);
|
|
678
|
+
border: 1px solid var(--ui-border-faint);
|
|
679
|
+
background-image: linear-gradient(45deg, var(--ui-border-subtle) 25%, transparent 25%),
|
|
680
|
+
linear-gradient(-45deg, var(--ui-border-subtle) 25%, transparent 25%),
|
|
681
|
+
linear-gradient(45deg, transparent 75%, var(--ui-border-subtle) 75%),
|
|
682
|
+
linear-gradient(-45deg, transparent 75%, var(--ui-border-subtle) 75%);
|
|
683
|
+
background-size: 8px 8px;
|
|
684
|
+
background-position: 0 0, 0 4px, 4px -4px, -4px 0;
|
|
685
|
+
overflow: hidden;
|
|
686
|
+
}
|
|
687
|
+
|
|
688
|
+
.swatch {
|
|
689
|
+
width: 100%;
|
|
690
|
+
height: 100%;
|
|
691
|
+
}
|
|
692
|
+
|
|
693
|
+
.opacity-control {
|
|
694
|
+
display: flex;
|
|
695
|
+
align-items: center;
|
|
696
|
+
gap: var(--ui-space-6);
|
|
697
|
+
padding: var(--ui-space-6) var(--ui-space-8);
|
|
698
|
+
border-bottom: 1px solid var(--ui-border-faint);
|
|
699
|
+
}
|
|
700
|
+
|
|
701
|
+
.opacity-control.hidden {
|
|
702
|
+
display: none;
|
|
703
|
+
}
|
|
704
|
+
|
|
705
|
+
.opacity-label {
|
|
706
|
+
font-size: var(--ui-font-size-xs);
|
|
707
|
+
color: var(--ui-text-secondary);
|
|
708
|
+
flex-shrink: 0;
|
|
709
|
+
}
|
|
710
|
+
|
|
711
|
+
.opacity-slider {
|
|
712
|
+
flex: 1;
|
|
713
|
+
height: 4px;
|
|
714
|
+
-webkit-appearance: none;
|
|
715
|
+
appearance: none;
|
|
716
|
+
background: var(--ui-border-default);
|
|
717
|
+
border-radius: 2px;
|
|
718
|
+
outline: none;
|
|
719
|
+
cursor: pointer;
|
|
720
|
+
}
|
|
721
|
+
|
|
722
|
+
.opacity-slider::-webkit-slider-thumb {
|
|
723
|
+
-webkit-appearance: none;
|
|
724
|
+
width: 12px;
|
|
725
|
+
height: 12px;
|
|
726
|
+
border-radius: 50%;
|
|
727
|
+
background: var(--ui-text-primary);
|
|
728
|
+
cursor: pointer;
|
|
729
|
+
}
|
|
730
|
+
|
|
731
|
+
.opacity-input {
|
|
732
|
+
width: 3rem;
|
|
733
|
+
padding: var(--ui-space-2) var(--ui-space-4);
|
|
734
|
+
background: var(--ui-surface-lowest);
|
|
735
|
+
border: 1px solid var(--ui-border-subtle);
|
|
736
|
+
border-radius: var(--ui-radius-sm);
|
|
737
|
+
color: var(--ui-text-primary);
|
|
738
|
+
font-size: var(--ui-font-size-xs);
|
|
739
|
+
font-family: var(--ui-font-mono);
|
|
740
|
+
text-align: right;
|
|
741
|
+
-moz-appearance: textfield;
|
|
742
|
+
}
|
|
743
|
+
|
|
744
|
+
.opacity-input::-webkit-inner-spin-button,
|
|
745
|
+
.opacity-input::-webkit-outer-spin-button {
|
|
746
|
+
-webkit-appearance: none;
|
|
747
|
+
margin: 0;
|
|
748
|
+
}
|
|
749
|
+
|
|
750
|
+
.opacity-unit {
|
|
751
|
+
font-size: var(--ui-font-size-xs);
|
|
752
|
+
color: var(--ui-text-muted);
|
|
753
|
+
}
|
|
754
|
+
|
|
755
|
+
.dropdown-back {
|
|
756
|
+
display: flex;
|
|
757
|
+
align-items: center;
|
|
758
|
+
gap: var(--ui-space-6);
|
|
759
|
+
width: 100%;
|
|
760
|
+
padding: var(--ui-space-8) var(--ui-space-10);
|
|
761
|
+
background: none;
|
|
762
|
+
border: none;
|
|
763
|
+
border-bottom: 1px solid var(--ui-border-faint);
|
|
764
|
+
color: var(--ui-text-secondary);
|
|
765
|
+
font-size: var(--ui-font-size-sm);
|
|
766
|
+
font-weight: var(--ui-font-weight-medium);
|
|
767
|
+
cursor: pointer;
|
|
768
|
+
transition: background var(--ui-transition-fast);
|
|
769
|
+
}
|
|
770
|
+
|
|
771
|
+
.dropdown-back:hover {
|
|
772
|
+
background: var(--ui-hover);
|
|
773
|
+
}
|
|
774
|
+
|
|
775
|
+
.dropdown-back i {
|
|
776
|
+
font-size: 0.5rem;
|
|
777
|
+
}
|
|
778
|
+
|
|
779
|
+
.tab-bar {
|
|
780
|
+
display: flex;
|
|
781
|
+
border-bottom: 1px solid var(--ui-border-faint);
|
|
782
|
+
}
|
|
783
|
+
|
|
784
|
+
.tab-btn {
|
|
785
|
+
flex: 1;
|
|
786
|
+
padding: var(--ui-space-4) var(--ui-space-4);
|
|
787
|
+
background: none;
|
|
788
|
+
border: 1px solid transparent;
|
|
789
|
+
border-radius: 0;
|
|
790
|
+
color: var(--ui-text-muted);
|
|
791
|
+
font-size: var(--ui-font-size-xs);
|
|
792
|
+
font-weight: var(--ui-font-weight-medium);
|
|
793
|
+
cursor: pointer;
|
|
794
|
+
transition: all var(--ui-transition-fast);
|
|
795
|
+
text-align: center;
|
|
796
|
+
}
|
|
797
|
+
|
|
798
|
+
.tab-btn:hover {
|
|
799
|
+
color: var(--ui-text-secondary);
|
|
800
|
+
background: var(--ui-hover);
|
|
801
|
+
}
|
|
802
|
+
|
|
803
|
+
.tab-btn.assigned {
|
|
804
|
+
border-color: var(--ui-border-default);
|
|
805
|
+
}
|
|
806
|
+
|
|
807
|
+
.tab-btn.selected {
|
|
808
|
+
color: var(--ui-text-primary);
|
|
809
|
+
box-shadow: inset 0 -2px 0 var(--ui-text-accent);
|
|
810
|
+
background: var(--ui-hover);
|
|
811
|
+
}
|
|
812
|
+
|
|
813
|
+
.family-list {
|
|
814
|
+
display: flex;
|
|
815
|
+
flex-direction: column;
|
|
816
|
+
max-height: 20rem;
|
|
817
|
+
overflow-y: auto;
|
|
818
|
+
}
|
|
819
|
+
|
|
820
|
+
.family-item {
|
|
821
|
+
display: flex;
|
|
822
|
+
align-items: center;
|
|
823
|
+
gap: var(--ui-space-8);
|
|
824
|
+
width: 100%;
|
|
825
|
+
padding: var(--ui-space-6) var(--ui-space-10);
|
|
826
|
+
background: none;
|
|
827
|
+
border: none;
|
|
828
|
+
cursor: pointer;
|
|
829
|
+
transition: background var(--ui-transition-fast);
|
|
830
|
+
}
|
|
831
|
+
|
|
832
|
+
.family-item:hover {
|
|
833
|
+
background: var(--ui-hover);
|
|
834
|
+
}
|
|
835
|
+
|
|
836
|
+
.family-item.active {
|
|
837
|
+
background: var(--ui-hover-high);
|
|
838
|
+
box-shadow: inset 3px 0 0 var(--ui-text-accent);
|
|
839
|
+
}
|
|
840
|
+
|
|
841
|
+
.family-item.active .family-label {
|
|
842
|
+
color: var(--ui-text-accent);
|
|
843
|
+
}
|
|
844
|
+
|
|
845
|
+
.family-swatches {
|
|
846
|
+
display: flex;
|
|
847
|
+
gap: 2px;
|
|
848
|
+
}
|
|
849
|
+
|
|
850
|
+
.mini-swatch {
|
|
851
|
+
width: 0.75rem;
|
|
852
|
+
height: 0.75rem;
|
|
853
|
+
border-radius: 2px;
|
|
854
|
+
}
|
|
855
|
+
|
|
856
|
+
.none-swatch {
|
|
857
|
+
width: 2.5rem;
|
|
858
|
+
height: 0.75rem;
|
|
859
|
+
border-radius: 2px;
|
|
860
|
+
border: 1px solid var(--ui-border-subtle);
|
|
861
|
+
position: relative;
|
|
862
|
+
overflow: hidden;
|
|
863
|
+
}
|
|
864
|
+
|
|
865
|
+
.gradient-swatch {
|
|
866
|
+
width: 2.5rem;
|
|
867
|
+
height: 0.75rem;
|
|
868
|
+
border-radius: 2px;
|
|
869
|
+
border: 1px solid var(--ui-border-subtle);
|
|
870
|
+
}
|
|
871
|
+
|
|
872
|
+
.family-divider {
|
|
873
|
+
margin-top: var(--ui-space-6);
|
|
874
|
+
padding: var(--ui-space-4) var(--ui-space-8);
|
|
875
|
+
font-size: var(--ui-font-size-xs);
|
|
876
|
+
font-family: var(--ui-font-mono);
|
|
877
|
+
color: var(--ui-text-tertiary);
|
|
878
|
+
text-transform: uppercase;
|
|
879
|
+
letter-spacing: 0.04em;
|
|
880
|
+
border-top: 1px solid var(--ui-border-faint);
|
|
881
|
+
}
|
|
882
|
+
|
|
883
|
+
.none-swatch::after {
|
|
884
|
+
content: '';
|
|
885
|
+
position: absolute;
|
|
886
|
+
top: -1px;
|
|
887
|
+
left: -1px;
|
|
888
|
+
right: -1px;
|
|
889
|
+
bottom: -1px;
|
|
890
|
+
background: repeating-linear-gradient(
|
|
891
|
+
-45deg,
|
|
892
|
+
transparent,
|
|
893
|
+
transparent 3px,
|
|
894
|
+
var(--ui-border-subtle) 3px,
|
|
895
|
+
var(--ui-border-subtle) 4px
|
|
896
|
+
);
|
|
897
|
+
}
|
|
898
|
+
|
|
899
|
+
.family-label {
|
|
900
|
+
flex: 1;
|
|
901
|
+
font-size: var(--ui-font-size-sm);
|
|
902
|
+
color: var(--ui-text-primary);
|
|
903
|
+
text-align: left;
|
|
904
|
+
}
|
|
905
|
+
|
|
906
|
+
.family-arrow {
|
|
907
|
+
font-size: 0.5rem;
|
|
908
|
+
color: var(--ui-text-muted);
|
|
909
|
+
}
|
|
910
|
+
|
|
911
|
+
.step-grid {
|
|
912
|
+
display: grid;
|
|
913
|
+
grid-template-columns: repeat(4, 1fr);
|
|
914
|
+
gap: var(--ui-space-4);
|
|
915
|
+
padding: var(--ui-space-8);
|
|
916
|
+
}
|
|
917
|
+
|
|
918
|
+
.step-item {
|
|
919
|
+
display: flex;
|
|
920
|
+
flex-direction: column;
|
|
921
|
+
align-items: center;
|
|
922
|
+
gap: var(--ui-space-2);
|
|
923
|
+
padding: var(--ui-space-4);
|
|
924
|
+
background: none;
|
|
925
|
+
border: 1px solid transparent;
|
|
926
|
+
border-radius: var(--ui-radius-sm);
|
|
927
|
+
cursor: pointer;
|
|
928
|
+
transition: all var(--ui-transition-fast);
|
|
929
|
+
}
|
|
930
|
+
|
|
931
|
+
.step-item:hover {
|
|
932
|
+
background: var(--ui-hover);
|
|
933
|
+
border-color: var(--ui-border-default);
|
|
934
|
+
}
|
|
935
|
+
|
|
936
|
+
.step-item.active {
|
|
937
|
+
border-color: var(--ui-text-accent);
|
|
938
|
+
border-width: 2px;
|
|
939
|
+
background: var(--ui-hover-high);
|
|
940
|
+
padding: 3px;
|
|
941
|
+
}
|
|
942
|
+
|
|
943
|
+
.step-item.active .step-label {
|
|
944
|
+
color: var(--ui-text-accent);
|
|
945
|
+
font-weight: var(--ui-font-weight-semibold);
|
|
946
|
+
}
|
|
947
|
+
|
|
948
|
+
.step-swatch {
|
|
949
|
+
width: 2rem;
|
|
950
|
+
height: 1.5rem;
|
|
951
|
+
border-radius: var(--ui-radius-sm);
|
|
952
|
+
border: 1px solid var(--ui-border-faint);
|
|
953
|
+
}
|
|
954
|
+
|
|
955
|
+
.step-label {
|
|
956
|
+
font-size: var(--ui-font-size-xs);
|
|
957
|
+
color: var(--ui-text-secondary);
|
|
958
|
+
font-family: var(--ui-font-mono);
|
|
959
|
+
}
|
|
960
|
+
|
|
961
|
+
/* Inline detail row, sibling of the UITokenSelector inside the parent
|
|
962
|
+
token-row's 3-col subgrid. Lands in cols 2-3 of row 2, directly under
|
|
963
|
+
the swatch trigger — same column anchoring the value/contexts strip
|
|
964
|
+
uses, so the orientation block reads as a continuation of the row. */
|
|
965
|
+
.palette-detail-row {
|
|
966
|
+
grid-column: 2 / -1;
|
|
967
|
+
display: flex;
|
|
968
|
+
align-items: center;
|
|
969
|
+
gap: var(--ui-space-8);
|
|
970
|
+
padding-top: var(--ui-space-4);
|
|
971
|
+
min-width: 0;
|
|
972
|
+
}
|
|
973
|
+
|
|
974
|
+
.detail-label {
|
|
975
|
+
font-size: var(--ui-font-size-xs);
|
|
976
|
+
color: var(--ui-text-secondary);
|
|
977
|
+
flex-shrink: 0;
|
|
978
|
+
}
|
|
979
|
+
|
|
980
|
+
.orientation-reset {
|
|
981
|
+
display: inline-flex;
|
|
982
|
+
align-items: center;
|
|
983
|
+
justify-content: center;
|
|
984
|
+
width: 1.25rem;
|
|
985
|
+
height: 1.25rem;
|
|
986
|
+
padding: 0;
|
|
987
|
+
background: none;
|
|
988
|
+
border: 1px solid transparent;
|
|
989
|
+
border-radius: var(--ui-radius-sm);
|
|
990
|
+
color: var(--ui-text-muted);
|
|
991
|
+
font-size: 0.625rem;
|
|
992
|
+
cursor: pointer;
|
|
993
|
+
transition: all var(--ui-transition-fast);
|
|
994
|
+
flex-shrink: 0;
|
|
995
|
+
}
|
|
996
|
+
|
|
997
|
+
.orientation-reset.active {
|
|
998
|
+
color: var(--ui-link-broken, var(--ui-text-secondary));
|
|
999
|
+
border-color: var(--ui-border-subtle);
|
|
1000
|
+
}
|
|
1001
|
+
|
|
1002
|
+
.orientation-reset:hover:not(:disabled) {
|
|
1003
|
+
background: var(--ui-hover);
|
|
1004
|
+
color: var(--ui-text-primary);
|
|
1005
|
+
}
|
|
1006
|
+
|
|
1007
|
+
.orientation-reset:disabled {
|
|
1008
|
+
cursor: default;
|
|
1009
|
+
opacity: 0.4;
|
|
1010
|
+
}
|
|
1011
|
+
|
|
1012
|
+
.orientation-body {
|
|
1013
|
+
display: flex;
|
|
1014
|
+
align-items: center;
|
|
1015
|
+
gap: var(--ui-space-8);
|
|
1016
|
+
flex: 1;
|
|
1017
|
+
min-width: 0;
|
|
1018
|
+
}
|
|
1019
|
+
|
|
1020
|
+
.dir-grid {
|
|
1021
|
+
display: grid;
|
|
1022
|
+
grid-template-columns: repeat(3, 1.25rem);
|
|
1023
|
+
grid-template-rows: repeat(3, 1.25rem);
|
|
1024
|
+
gap: 2px;
|
|
1025
|
+
flex-shrink: 0;
|
|
1026
|
+
}
|
|
1027
|
+
|
|
1028
|
+
.dir-btn {
|
|
1029
|
+
display: inline-flex;
|
|
1030
|
+
align-items: center;
|
|
1031
|
+
justify-content: center;
|
|
1032
|
+
padding: 0;
|
|
1033
|
+
background: var(--ui-surface-lowest);
|
|
1034
|
+
border: 1px solid var(--ui-border-subtle);
|
|
1035
|
+
border-radius: var(--ui-radius-sm);
|
|
1036
|
+
color: var(--ui-text-secondary);
|
|
1037
|
+
font-size: 0.875rem;
|
|
1038
|
+
line-height: 1;
|
|
1039
|
+
cursor: pointer;
|
|
1040
|
+
transition: all var(--ui-transition-fast);
|
|
1041
|
+
}
|
|
1042
|
+
|
|
1043
|
+
.dir-btn:hover {
|
|
1044
|
+
background: var(--ui-hover);
|
|
1045
|
+
border-color: var(--ui-border-default);
|
|
1046
|
+
color: var(--ui-text-primary);
|
|
1047
|
+
}
|
|
1048
|
+
|
|
1049
|
+
.dir-btn.active {
|
|
1050
|
+
background: var(--ui-hover-high);
|
|
1051
|
+
border-color: var(--ui-text-accent);
|
|
1052
|
+
color: var(--ui-text-accent);
|
|
1053
|
+
}
|
|
1054
|
+
|
|
1055
|
+
.angle-input-wrap {
|
|
1056
|
+
display: flex;
|
|
1057
|
+
align-items: center;
|
|
1058
|
+
gap: var(--ui-space-2);
|
|
1059
|
+
}
|
|
1060
|
+
|
|
1061
|
+
.angle-input {
|
|
1062
|
+
width: 3rem;
|
|
1063
|
+
padding: var(--ui-space-2) var(--ui-space-4);
|
|
1064
|
+
background: var(--ui-surface-lowest);
|
|
1065
|
+
border: 1px solid var(--ui-border-subtle);
|
|
1066
|
+
border-radius: var(--ui-radius-sm);
|
|
1067
|
+
color: var(--ui-text-primary);
|
|
1068
|
+
font-size: var(--ui-font-size-xs);
|
|
1069
|
+
font-family: var(--ui-font-mono);
|
|
1070
|
+
text-align: right;
|
|
1071
|
+
-moz-appearance: textfield;
|
|
1072
|
+
}
|
|
1073
|
+
|
|
1074
|
+
.angle-input::-webkit-inner-spin-button,
|
|
1075
|
+
.angle-input::-webkit-outer-spin-button {
|
|
1076
|
+
-webkit-appearance: none;
|
|
1077
|
+
margin: 0;
|
|
1078
|
+
}
|
|
1079
|
+
|
|
1080
|
+
.angle-unit {
|
|
1081
|
+
font-size: var(--ui-font-size-xs);
|
|
1082
|
+
color: var(--ui-text-muted);
|
|
1083
|
+
}
|
|
1084
|
+
</style>
|