@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,661 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import { onMount, onDestroy, createEventDispatcher } from 'svelte';
|
|
3
|
+
import { fade, fly, slide } from 'svelte/transition';
|
|
4
|
+
import { cubicOut, cubicIn, cubicInOut } from 'svelte/easing';
|
|
5
|
+
import UITokenSelector from './UITokenSelector.svelte';
|
|
6
|
+
import UIOptionList from './UIOptionList.svelte';
|
|
7
|
+
import UIOptionItem from './UIOptionItem.svelte';
|
|
8
|
+
import { setCssVar, removeCssVar, CSS_VAR_CHANGE_EVENT } from '../lib/cssVarSync';
|
|
9
|
+
import {
|
|
10
|
+
editorState,
|
|
11
|
+
setComponentAlias,
|
|
12
|
+
clearComponentAlias,
|
|
13
|
+
getComponentPropertySiblings,
|
|
14
|
+
isComponentPropertyLinked,
|
|
15
|
+
setComponentAliasLinked,
|
|
16
|
+
unlinkComponentProperty,
|
|
17
|
+
relinkComponentProperty,
|
|
18
|
+
} from '../lib/editorStore';
|
|
19
|
+
import UIRelinkConfirmPopover from './UIRelinkConfirmPopover.svelte';
|
|
20
|
+
import UILinkToggle from './UILinkToggle.svelte';
|
|
21
|
+
|
|
22
|
+
const dispatch = createEventDispatcher();
|
|
23
|
+
|
|
24
|
+
export let variable: string;
|
|
25
|
+
export let component: string | undefined = undefined;
|
|
26
|
+
export let canBeLinked: boolean = false;
|
|
27
|
+
export let disabled: boolean = false;
|
|
28
|
+
export let selectionsLocked: boolean = false;
|
|
29
|
+
/** When 'sides', renders the per-side rows alongside the link/merge header.
|
|
30
|
+
The header occupies cols 2-3 of the parent token-row (sharing row 1 with
|
|
31
|
+
TokenLayout's .token-label) and the side rows fill cols 1-3 of row 2. */
|
|
32
|
+
export let mode: 'single' | 'sides' = 'single';
|
|
33
|
+
/** When false, hide the split-to-sides affordance (e.g. for non-box spacing like gap). */
|
|
34
|
+
export let splittable: boolean = true;
|
|
35
|
+
|
|
36
|
+
type Side = 'top' | 'right' | 'bottom' | 'left';
|
|
37
|
+
const SIDES: readonly Side[] = ['top', 'right', 'bottom', 'left'];
|
|
38
|
+
|
|
39
|
+
/** Honor prefers-reduced-motion: skip the orchestrated split↔merge entry/exit
|
|
40
|
+
when the OS asks for less motion. Read once at module mount. The split
|
|
41
|
+
transition has two phases — old-block fades, then new-block expands; the
|
|
42
|
+
cumulative time is large enough that ignoring this preference would be
|
|
43
|
+
annoying for users who set it. */
|
|
44
|
+
const reduceMotion = typeof window !== 'undefined'
|
|
45
|
+
&& window.matchMedia?.('(prefers-reduced-motion: reduce)').matches === true;
|
|
46
|
+
const t = (ms: number) => (reduceMotion ? 0 : ms);
|
|
47
|
+
|
|
48
|
+
function sideVar(s: Side): string {
|
|
49
|
+
return `${variable}-${s}`;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const options = [
|
|
53
|
+
{ key: '0', label: 'None', size: '0' },
|
|
54
|
+
{ key: '2', label: '2XS', size: '0.125rem' },
|
|
55
|
+
{ key: '4', label: 'XS', size: '0.25rem' },
|
|
56
|
+
{ key: '6', label: 'Small', size: '0.375rem' },
|
|
57
|
+
{ key: '8', label: 'Medium', size: '0.5rem' },
|
|
58
|
+
{ key: '10', label: 'Large', size: '0.625rem' },
|
|
59
|
+
{ key: '12', label: 'XL', size: '0.75rem' },
|
|
60
|
+
{ key: '16', label: '2XL', size: '1rem' },
|
|
61
|
+
{ key: '20', label: '3XL', size: '1.25rem' },
|
|
62
|
+
{ key: '24', label: '4XL', size: '1.5rem' },
|
|
63
|
+
{ key: '32', label: '5XL', size: '2rem' },
|
|
64
|
+
{ key: '48', label: '6XL', size: '3rem' },
|
|
65
|
+
];
|
|
66
|
+
|
|
67
|
+
function tokenForKey(key: string): string {
|
|
68
|
+
return `--space-${key}`;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function readAlias(v: string): string {
|
|
72
|
+
if (component) {
|
|
73
|
+
const ref = $editorState.components[component]?.aliases?.[v];
|
|
74
|
+
return ref?.kind === 'token' ? ref.name : '';
|
|
75
|
+
}
|
|
76
|
+
const inline = document.documentElement.style.getPropertyValue(v).trim();
|
|
77
|
+
const m = inline.match(/var\((--space-[a-z0-9]+)\)/);
|
|
78
|
+
return m ? m[1] : '';
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function parseKey(value: string): string | null {
|
|
82
|
+
if (!value) return null;
|
|
83
|
+
const m = value.match(/^--space-([a-z0-9]+)$/);
|
|
84
|
+
if (!m) return null;
|
|
85
|
+
return options.find((o) => o.key === m[1]) ? m[1] : null;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function readResolved(v: string): string {
|
|
89
|
+
return getComputedStyle(document.documentElement).getPropertyValue(v).trim();
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function writeAlias(v: string, semantic: string | null) {
|
|
93
|
+
if (component) {
|
|
94
|
+
if (semantic) setComponentAlias(component, v, { kind: 'token', name: semantic });
|
|
95
|
+
else clearComponentAlias(component, v);
|
|
96
|
+
return;
|
|
97
|
+
}
|
|
98
|
+
if (semantic) setCssVar(v, `var(${semantic})`);
|
|
99
|
+
else removeCssVar(v);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/** Honor the parent token's linkage on every write the padding selector
|
|
103
|
+
* performs — for the parent var itself and for any per-side child var. When
|
|
104
|
+
* the parent is linked, mirror the write across every linked peer's
|
|
105
|
+
* matching var (parent → peer; parent-side → peer-side); otherwise fall
|
|
106
|
+
* through to the local write. The split topology is "shared" because the
|
|
107
|
+
* parent's groupKey is — peers stay in lockstep on single↔split toggles
|
|
108
|
+
* and per-side picks alike. */
|
|
109
|
+
function writeAliasLinked(targetVar: string, semantic: string | null) {
|
|
110
|
+
if (!component) {
|
|
111
|
+
writeAlias(targetVar, semantic);
|
|
112
|
+
return;
|
|
113
|
+
}
|
|
114
|
+
if (!isComponentPropertyLinked(component, variable)) {
|
|
115
|
+
writeAlias(targetVar, semantic);
|
|
116
|
+
return;
|
|
117
|
+
}
|
|
118
|
+
const peers = getComponentPropertySiblings(component, variable);
|
|
119
|
+
if (peers.length < 2) {
|
|
120
|
+
writeAlias(targetVar, semantic);
|
|
121
|
+
return;
|
|
122
|
+
}
|
|
123
|
+
const slice = $editorState.components[component];
|
|
124
|
+
const unlinked = slice?.unlinked ?? [];
|
|
125
|
+
const suffix = targetVar.slice(variable.length);
|
|
126
|
+
for (const peer of peers) {
|
|
127
|
+
if (unlinked.includes(peer)) continue;
|
|
128
|
+
const peerVar = `${peer}${suffix}`;
|
|
129
|
+
if (semantic) setComponentAlias(component, peerVar, { kind: 'token', name: semantic });
|
|
130
|
+
else clearComponentAlias(component, peerVar);
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
let chosenKey: string | null = null;
|
|
135
|
+
let resolvedSize = '';
|
|
136
|
+
let sideKeys: Record<Side, string | null> = { top: null, right: null, bottom: null, left: null };
|
|
137
|
+
let sideResolved: Record<Side, string> = { top: '', right: '', bottom: '', left: '' };
|
|
138
|
+
|
|
139
|
+
function refreshFromState() {
|
|
140
|
+
chosenKey = parseKey(readAlias(variable));
|
|
141
|
+
resolvedSize = readResolved(variable);
|
|
142
|
+
const nextKeys: Record<Side, string | null> = { top: null, right: null, bottom: null, left: null };
|
|
143
|
+
const nextResolved: Record<Side, string> = { top: '', right: '', bottom: '', left: '' };
|
|
144
|
+
for (const s of SIDES) {
|
|
145
|
+
nextKeys[s] = parseKey(readAlias(sideVar(s)));
|
|
146
|
+
nextResolved[s] = readResolved(sideVar(s));
|
|
147
|
+
}
|
|
148
|
+
sideKeys = nextKeys;
|
|
149
|
+
sideResolved = nextResolved;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
function selectSingle(key: string, close: () => void) {
|
|
153
|
+
writeAliasLinked(variable, tokenForKey(key));
|
|
154
|
+
close();
|
|
155
|
+
dispatch('change');
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
function selectSide(s: Side, key: string, close: () => void) {
|
|
159
|
+
writeAliasLinked(sideVar(s), tokenForKey(key));
|
|
160
|
+
close();
|
|
161
|
+
dispatch('change');
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
function handleResetAll() {
|
|
165
|
+
for (const s of SIDES) writeAliasLinked(sideVar(s), null);
|
|
166
|
+
writeAliasLinked(variable, null);
|
|
167
|
+
dispatch('change');
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
function handleResetSide(s: Side) {
|
|
171
|
+
writeAliasLinked(sideVar(s), null);
|
|
172
|
+
dispatch('change');
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
function splitToSides() {
|
|
176
|
+
if (disabled) return;
|
|
177
|
+
const seed = readAlias(variable) || tokenForKey('4');
|
|
178
|
+
for (const s of SIDES) writeAliasLinked(sideVar(s), seed);
|
|
179
|
+
dispatch('change');
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
function mergeToSingle() {
|
|
183
|
+
if (disabled) return;
|
|
184
|
+
const seed = readAlias(sideVar('top')) || readAlias(variable);
|
|
185
|
+
if (seed && !readAlias(variable)) writeAliasLinked(variable, seed);
|
|
186
|
+
for (const s of SIDES) writeAliasLinked(sideVar(s), null);
|
|
187
|
+
dispatch('change');
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
function handleVarChange() {
|
|
191
|
+
refreshFromState();
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
onMount(() => {
|
|
195
|
+
refreshFromState();
|
|
196
|
+
document.addEventListener(CSS_VAR_CHANGE_EVENT, handleVarChange);
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
onDestroy(() => {
|
|
200
|
+
document.removeEventListener(CSS_VAR_CHANGE_EVENT, handleVarChange);
|
|
201
|
+
});
|
|
202
|
+
|
|
203
|
+
// Track `variable` alongside `$editorState` so a VariantGroup tabs view that
|
|
204
|
+
// reuses this selector across states refreshes when the bound prop swaps.
|
|
205
|
+
$: { variable; if ($editorState) refreshFromState(); }
|
|
206
|
+
|
|
207
|
+
$: activeKey = chosenKey ?? (options.find((o) => o.size === resolvedSize)?.key ?? null);
|
|
208
|
+
$: activeLabel = options.find((o) => o.key === activeKey)?.label ?? '';
|
|
209
|
+
$: sideActiveKey = {
|
|
210
|
+
top: sideKeys.top ?? (options.find((o) => o.size === sideResolved.top)?.key ?? null),
|
|
211
|
+
right: sideKeys.right ?? (options.find((o) => o.size === sideResolved.right)?.key ?? null),
|
|
212
|
+
bottom: sideKeys.bottom ?? (options.find((o) => o.size === sideResolved.bottom)?.key ?? null),
|
|
213
|
+
left: sideKeys.left ?? (options.find((o) => o.size === sideResolved.left)?.key ?? null),
|
|
214
|
+
} as Record<Side, string | null>;
|
|
215
|
+
$: sideLabels = {
|
|
216
|
+
top: options.find((o) => o.key === sideActiveKey.top)?.label ?? '',
|
|
217
|
+
right: options.find((o) => o.key === sideActiveKey.right)?.label ?? '',
|
|
218
|
+
bottom: options.find((o) => o.key === sideActiveKey.bottom)?.label ?? '',
|
|
219
|
+
left: options.find((o) => o.key === sideActiveKey.left)?.label ?? '',
|
|
220
|
+
} as Record<Side, string>;
|
|
221
|
+
|
|
222
|
+
// Linkage state for the parent token. The split fieldset is a single
|
|
223
|
+
// linkage unit — peers either share the parent (and their per-side state
|
|
224
|
+
// mirrors via `writeAliasLinked`) or one peer has detached. Mirrors the
|
|
225
|
+
// pop-bar/lock-toggle vocabulary used by `UITokenSelector` so the visual
|
|
226
|
+
// and interaction language is the same at both scales.
|
|
227
|
+
$: hasParentSiblings = canBeLinked && component && $editorState
|
|
228
|
+
? getComponentPropertySiblings(component, variable).length >= 2
|
|
229
|
+
: false;
|
|
230
|
+
$: showLinkUI = canBeLinked && !!component && hasParentSiblings;
|
|
231
|
+
/** $editorState is referenced directly so this re-evaluates on every store
|
|
232
|
+
* update. isComponentPropertyLinked reads `get(store)` internally — Svelte
|
|
233
|
+
* can't see that as a dependency. Without the explicit reference, an
|
|
234
|
+
* unlink mutation (which leaves siblings count and showLinkUI unchanged)
|
|
235
|
+
* would not re-run this expression: Svelte's $$invalidate skips marking
|
|
236
|
+
* a boolean dirty when the new value equals the old, so the dirty bit
|
|
237
|
+
* never propagates to isLinkedParent. */
|
|
238
|
+
$: isLinkedParent = !!$editorState
|
|
239
|
+
&& showLinkUI
|
|
240
|
+
&& !!component
|
|
241
|
+
&& isComponentPropertyLinked(component, variable);
|
|
242
|
+
|
|
243
|
+
let relinkOpen = false;
|
|
244
|
+
let relinkCandidates: { variable: string; alias: string }[] = [];
|
|
245
|
+
|
|
246
|
+
/** Adopt one peer's entire padding block (parent + four sides) onto every
|
|
247
|
+
* currently-linked peer. Padding is treated as a single shared unit — when
|
|
248
|
+
* peers diverge in split topology (one in single mode, another in split),
|
|
249
|
+
* relinking must converge them, not just mirror the parent var. The source
|
|
250
|
+
* peer's split-or-single state, plus all four side aliases, become the
|
|
251
|
+
* group's canonical state. */
|
|
252
|
+
function adoptBlockFromPeer(sourcePeer: string) {
|
|
253
|
+
if (!component) return;
|
|
254
|
+
const slice = $editorState.components[component];
|
|
255
|
+
if (!slice) return;
|
|
256
|
+
|
|
257
|
+
const sourceParent = slice.aliases[sourcePeer];
|
|
258
|
+
const sourceSides: Record<Side, ReturnType<typeof readSideAlias>> = {
|
|
259
|
+
top: readSideAlias(sourcePeer, 'top'),
|
|
260
|
+
right: readSideAlias(sourcePeer, 'right'),
|
|
261
|
+
bottom: readSideAlias(sourcePeer, 'bottom'),
|
|
262
|
+
left: readSideAlias(sourcePeer, 'left'),
|
|
263
|
+
};
|
|
264
|
+
|
|
265
|
+
if (sourceParent) setComponentAliasLinked(component, variable, sourceParent);
|
|
266
|
+
else relinkComponentProperty(component, variable);
|
|
267
|
+
|
|
268
|
+
// After re-engaging linkage, fan the source's per-side state to every
|
|
269
|
+
// peer not currently opted out. `setComponentAliasLinked` already removed
|
|
270
|
+
// `variable` from `unlinked`; re-read so we don't skip self.
|
|
271
|
+
const next = $editorState.components[component];
|
|
272
|
+
if (!next) return;
|
|
273
|
+
const peers = getComponentPropertySiblings(component, variable);
|
|
274
|
+
const unlinked = new Set(next.unlinked ?? []);
|
|
275
|
+
for (const peer of peers) {
|
|
276
|
+
if (unlinked.has(peer)) continue;
|
|
277
|
+
for (const side of SIDES) {
|
|
278
|
+
const peerSide = `${peer}-${side}`;
|
|
279
|
+
const ref = sourceSides[side];
|
|
280
|
+
if (ref) setComponentAlias(component, peerSide, ref);
|
|
281
|
+
else clearComponentAlias(component, peerSide);
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
function readSideAlias(peer: string, side: Side) {
|
|
287
|
+
if (!component) return undefined;
|
|
288
|
+
const slice = $editorState.components[component];
|
|
289
|
+
return slice?.aliases[`${peer}-${side}`];
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
function toggleLinkPaddingGroup() {
|
|
293
|
+
if (!showLinkUI || !component) return;
|
|
294
|
+
if (isLinkedParent) {
|
|
295
|
+
unlinkComponentProperty(component, variable);
|
|
296
|
+
dispatch('change');
|
|
297
|
+
return;
|
|
298
|
+
}
|
|
299
|
+
const slice = $editorState.components[component];
|
|
300
|
+
if (!slice) return;
|
|
301
|
+
const peers = getComponentPropertySiblings(component, variable);
|
|
302
|
+
if (peers.length < 2) return;
|
|
303
|
+
const linkedPeers = peers.filter(
|
|
304
|
+
(v) => v !== variable && !slice.unlinked?.includes(v),
|
|
305
|
+
);
|
|
306
|
+
if (linkedPeers.length === 0) {
|
|
307
|
+
const currentValue = slice.aliases[variable];
|
|
308
|
+
if (currentValue) {
|
|
309
|
+
adoptBlockFromPeer(variable);
|
|
310
|
+
dispatch('change');
|
|
311
|
+
}
|
|
312
|
+
return;
|
|
313
|
+
}
|
|
314
|
+
const candidates = linkedPeers.map((v) => {
|
|
315
|
+
const ref = slice.aliases[v];
|
|
316
|
+
const alias = ref?.kind === 'token' ? ref.name : '';
|
|
317
|
+
return { variable: v, alias };
|
|
318
|
+
});
|
|
319
|
+
const definedCandidates = candidates.filter((c) => c.alias);
|
|
320
|
+
const distinctValues = new Set(definedCandidates.map((c) => c.alias));
|
|
321
|
+
if (distinctValues.size <= 1) {
|
|
322
|
+
const sourcePeer = definedCandidates.length > 0
|
|
323
|
+
? definedCandidates[0].variable
|
|
324
|
+
: variable;
|
|
325
|
+
adoptBlockFromPeer(sourcePeer);
|
|
326
|
+
dispatch('change');
|
|
327
|
+
return;
|
|
328
|
+
}
|
|
329
|
+
relinkCandidates = candidates;
|
|
330
|
+
relinkOpen = true;
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
function handleRelinkConfirm(e: CustomEvent<{ alias: string }>) {
|
|
334
|
+
if (!component) return;
|
|
335
|
+
// Find the peer whose parent alias matches the chosen one — its full
|
|
336
|
+
// block (split state + side values) becomes canonical.
|
|
337
|
+
const sourcePeer = relinkCandidates.find((c) => c.alias === e.detail.alias)?.variable ?? variable;
|
|
338
|
+
adoptBlockFromPeer(sourcePeer);
|
|
339
|
+
dispatch('change');
|
|
340
|
+
relinkOpen = false;
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
function handleRelinkCancel() {
|
|
344
|
+
relinkOpen = false;
|
|
345
|
+
}
|
|
346
|
+
</script>
|
|
347
|
+
|
|
348
|
+
{#if mode === 'sides'}
|
|
349
|
+
<!--
|
|
350
|
+
Sides mode renders into the parent token-row subgrid, NOT inside a
|
|
351
|
+
self-contained fieldset. TokenLayout puts the .token-label "padding" in
|
|
352
|
+
col 1; we fill cols 2-3 of the same row with the link/merge header, then
|
|
353
|
+
drop the four side rows onto row 2 spanning cols 1-3 (still subgridded so
|
|
354
|
+
each side's dropdown lands in the same trigger column as the surrounding
|
|
355
|
+
properties — border-width, corner-radius, etc.).
|
|
356
|
+
|
|
357
|
+
Both .padding-header and .padding-sides-block carry explicit grid-row so
|
|
358
|
+
auto-placement during the transition can't push them onto extra rows when
|
|
359
|
+
the leaving .padding-single-row briefly co-occupies cols 2-3 row 1; with
|
|
360
|
+
the explicit row anchors the OLD and NEW header content cross-fade in
|
|
361
|
+
the same cell instead of stacking.
|
|
362
|
+
-->
|
|
363
|
+
<div
|
|
364
|
+
class="padding-header"
|
|
365
|
+
in:fade|local={{ duration: t(220), delay: t(200) }}
|
|
366
|
+
out:fade|local={{ duration: t(180), easing: cubicIn }}
|
|
367
|
+
>
|
|
368
|
+
{#if showLinkUI}
|
|
369
|
+
<div class="link-toggle-wrap">
|
|
370
|
+
<UILinkToggle linked={isLinkedParent} on:toggle={toggleLinkPaddingGroup} />
|
|
371
|
+
{#if relinkOpen && component}
|
|
372
|
+
<UIRelinkConfirmPopover
|
|
373
|
+
candidates={relinkCandidates}
|
|
374
|
+
initialVariable={variable}
|
|
375
|
+
prefixToStrip={`--${component}-`}
|
|
376
|
+
on:confirm={handleRelinkConfirm}
|
|
377
|
+
on:cancel={handleRelinkCancel}
|
|
378
|
+
/>
|
|
379
|
+
{/if}
|
|
380
|
+
</div>
|
|
381
|
+
{/if}
|
|
382
|
+
<button
|
|
383
|
+
type="button"
|
|
384
|
+
class="merge-btn"
|
|
385
|
+
on:click={mergeToSingle}
|
|
386
|
+
title="Use the same value for all sides"
|
|
387
|
+
disabled={disabled || selectionsLocked}
|
|
388
|
+
>
|
|
389
|
+
<i class="fas fa-square" aria-hidden="true"></i>
|
|
390
|
+
<span>Merge</span>
|
|
391
|
+
</button>
|
|
392
|
+
</div>
|
|
393
|
+
<div
|
|
394
|
+
class="padding-sides-block"
|
|
395
|
+
class:linked={showLinkUI && isLinkedParent}
|
|
396
|
+
class:unlinked={showLinkUI && !isLinkedParent}
|
|
397
|
+
in:slide|local={{ duration: t(360), delay: t(200), easing: cubicOut }}
|
|
398
|
+
out:slide|local={{ duration: t(320), easing: cubicInOut }}
|
|
399
|
+
>
|
|
400
|
+
{#each SIDES as s}
|
|
401
|
+
<span class="side-label">{s}</span>
|
|
402
|
+
<UITokenSelector
|
|
403
|
+
variable={sideVar(s)}
|
|
404
|
+
{component}
|
|
405
|
+
canBeLinked={false}
|
|
406
|
+
{disabled}
|
|
407
|
+
{selectionsLocked}
|
|
408
|
+
on:reset={() => handleResetSide(s)}
|
|
409
|
+
on:var-change={handleVarChange}
|
|
410
|
+
>
|
|
411
|
+
<svelte:fragment slot="trigger-title">{sideLabels[s] || '—'}</svelte:fragment>
|
|
412
|
+
<svelte:fragment slot="trigger-meta">{sideResolved[s] || '—'}</svelte:fragment>
|
|
413
|
+
|
|
414
|
+
<svelte:fragment let:close>
|
|
415
|
+
<UIOptionList>
|
|
416
|
+
{#each options as opt}
|
|
417
|
+
<UIOptionItem
|
|
418
|
+
active={sideActiveKey[s] === opt.key}
|
|
419
|
+
on:click={() => selectSide(s, opt.key, close)}
|
|
420
|
+
>
|
|
421
|
+
<svelte:fragment slot="label">{opt.label}</svelte:fragment>
|
|
422
|
+
<svelte:fragment slot="meta">{opt.size}</svelte:fragment>
|
|
423
|
+
</UIOptionItem>
|
|
424
|
+
{/each}
|
|
425
|
+
</UIOptionList>
|
|
426
|
+
</svelte:fragment>
|
|
427
|
+
</UITokenSelector>
|
|
428
|
+
{/each}
|
|
429
|
+
</div>
|
|
430
|
+
{:else}
|
|
431
|
+
<!--
|
|
432
|
+
Parent fade carries the row's overall opacity (and through it, the
|
|
433
|
+
nested UITokenSelector trigger's apparent opacity, since opacity
|
|
434
|
+
cascades through the stacking context). The split-button's `fly`
|
|
435
|
+
sets `opacity: 1` to disable fly's built-in fade so we don't
|
|
436
|
+
double-fade it: the parent already handles opacity, fly handles
|
|
437
|
+
only the right-ward translate. Result: as the user clicks split,
|
|
438
|
+
the trigger fades in place while the grid icon glides to the right
|
|
439
|
+
and disappears in lockstep.
|
|
440
|
+
-->
|
|
441
|
+
<div
|
|
442
|
+
class="padding-single-row"
|
|
443
|
+
class:disabled
|
|
444
|
+
in:fade|local={{ duration: t(220), delay: t(320) }}
|
|
445
|
+
out:fade|local={{ duration: t(200), easing: cubicIn }}
|
|
446
|
+
>
|
|
447
|
+
<UITokenSelector
|
|
448
|
+
{variable}
|
|
449
|
+
{component}
|
|
450
|
+
{canBeLinked}
|
|
451
|
+
{disabled}
|
|
452
|
+
{selectionsLocked}
|
|
453
|
+
on:reset={handleResetAll}
|
|
454
|
+
on:var-change={handleVarChange}
|
|
455
|
+
>
|
|
456
|
+
<svelte:fragment slot="trigger-title">{activeLabel || '—'}</svelte:fragment>
|
|
457
|
+
|
|
458
|
+
<svelte:fragment let:close>
|
|
459
|
+
<UIOptionList>
|
|
460
|
+
{#each options as opt}
|
|
461
|
+
<UIOptionItem
|
|
462
|
+
active={activeKey === opt.key}
|
|
463
|
+
on:click={() => selectSingle(opt.key, close)}
|
|
464
|
+
>
|
|
465
|
+
<svelte:fragment slot="label">{opt.label}</svelte:fragment>
|
|
466
|
+
<svelte:fragment slot="meta">{opt.size}</svelte:fragment>
|
|
467
|
+
</UIOptionItem>
|
|
468
|
+
{/each}
|
|
469
|
+
</UIOptionList>
|
|
470
|
+
</svelte:fragment>
|
|
471
|
+
</UITokenSelector>
|
|
472
|
+
{#if splittable}
|
|
473
|
+
<button
|
|
474
|
+
type="button"
|
|
475
|
+
class="split-btn"
|
|
476
|
+
on:click={splitToSides}
|
|
477
|
+
title="Set each side independently"
|
|
478
|
+
disabled={disabled || selectionsLocked}
|
|
479
|
+
in:fly|local={{ x: 24, opacity: 1, duration: t(220), delay: t(320), easing: cubicOut }}
|
|
480
|
+
out:fly|local={{ x: 24, opacity: 1, duration: t(200), easing: cubicIn }}
|
|
481
|
+
>
|
|
482
|
+
<i class="fas fa-border-all" aria-hidden="true"></i>
|
|
483
|
+
</button>
|
|
484
|
+
{/if}
|
|
485
|
+
</div>
|
|
486
|
+
{/if}
|
|
487
|
+
|
|
488
|
+
<style>
|
|
489
|
+
/* Single-mode padding row: trigger + split-toggle, sitting side-by-side.
|
|
490
|
+
The row spans grid columns from the trigger slot onward —
|
|
491
|
+
`--padding-row-start` defaults to column 2 (matches the per-variant
|
|
492
|
+
`[label][trigger][value]` grid) and is overridden to 1 by callers whose
|
|
493
|
+
grid skips the label column (the linked-block layout). The inner
|
|
494
|
+
UITokenSelector switches from its default subgrid to inline-flex so it
|
|
495
|
+
occupies natural width inside this flex row instead of stealing both
|
|
496
|
+
parent columns via `grid-column: span 2`. */
|
|
497
|
+
.padding-single-row {
|
|
498
|
+
display: flex;
|
|
499
|
+
align-items: center;
|
|
500
|
+
gap: var(--ui-space-6);
|
|
501
|
+
grid-column: var(--padding-row-start, 2) / -1;
|
|
502
|
+
/* Pin to row 1 so during a sides→single transition the leaving header
|
|
503
|
+
and the entering single row co-occupy the same cell instead of
|
|
504
|
+
stacking on adjacent grid rows. */
|
|
505
|
+
grid-row: 1;
|
|
506
|
+
min-width: 0;
|
|
507
|
+
}
|
|
508
|
+
.padding-single-row :global(.ui-token-selector) {
|
|
509
|
+
display: inline-flex;
|
|
510
|
+
align-items: center;
|
|
511
|
+
gap: var(--ui-space-8);
|
|
512
|
+
grid-column: auto;
|
|
513
|
+
flex: 0 0 auto;
|
|
514
|
+
}
|
|
515
|
+
/* Pin the trigger to the same width as the surrounding selectors' trigger
|
|
516
|
+
column (`--token-selector-w` on the parent .token-grid, 8rem default).
|
|
517
|
+
Without this, a short token label like "XS" collapses the inline-flex
|
|
518
|
+
trigger to natural width and breaks the column alignment with the
|
|
519
|
+
border-width/corner-radius/etc. rows above and below. */
|
|
520
|
+
.padding-single-row :global(.ui-ts-trigger-wrap) {
|
|
521
|
+
min-width: var(--token-selector-w, 8rem);
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
.split-btn {
|
|
525
|
+
display: inline-flex;
|
|
526
|
+
align-items: center;
|
|
527
|
+
justify-content: center;
|
|
528
|
+
width: 1.75rem;
|
|
529
|
+
height: 1.75rem;
|
|
530
|
+
padding: 0;
|
|
531
|
+
background: var(--ui-surface-low);
|
|
532
|
+
border: 1px solid var(--ui-border-default);
|
|
533
|
+
border-radius: var(--ui-radius-sm);
|
|
534
|
+
color: var(--ui-text-secondary);
|
|
535
|
+
font-size: var(--ui-font-size-sm);
|
|
536
|
+
cursor: pointer;
|
|
537
|
+
transition: all var(--ui-transition-fast);
|
|
538
|
+
flex-shrink: 0;
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
.split-btn:hover:not(:disabled) {
|
|
542
|
+
border-color: var(--ui-border-strong);
|
|
543
|
+
background: var(--ui-surface-high);
|
|
544
|
+
color: var(--ui-text-primary);
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
.split-btn:disabled {
|
|
548
|
+
cursor: not-allowed;
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
/* Header row: link-toggle + Merge button, sharing row 1 with the parent's
|
|
552
|
+
.token-label "padding". Cols 2-3 of the parent subgrid by default; the
|
|
553
|
+
left col stays free for the label TokenLayout already rendered. The
|
|
554
|
+
--padding-row-start hook lets a host (the linked-block, which renders
|
|
555
|
+
the property name as an h4 instead of an in-row label) re-anchor the
|
|
556
|
+
header to col 1 so it spans the full row width. */
|
|
557
|
+
.padding-header {
|
|
558
|
+
grid-column: var(--padding-row-start, 2) / -1;
|
|
559
|
+
grid-row: 1;
|
|
560
|
+
display: flex;
|
|
561
|
+
justify-content: flex-start;
|
|
562
|
+
align-items: center;
|
|
563
|
+
gap: var(--ui-space-6);
|
|
564
|
+
min-width: 0;
|
|
565
|
+
}
|
|
566
|
+
|
|
567
|
+
.link-toggle-wrap {
|
|
568
|
+
position: relative;
|
|
569
|
+
}
|
|
570
|
+
|
|
571
|
+
.merge-btn {
|
|
572
|
+
display: inline-flex;
|
|
573
|
+
align-items: center;
|
|
574
|
+
gap: var(--ui-space-6);
|
|
575
|
+
padding: var(--ui-space-4) var(--ui-space-8);
|
|
576
|
+
height: 1.5rem;
|
|
577
|
+
background: none;
|
|
578
|
+
border: 1px solid var(--ui-border-subtle);
|
|
579
|
+
border-radius: var(--ui-radius-sm);
|
|
580
|
+
color: var(--ui-text-muted);
|
|
581
|
+
font-family: inherit;
|
|
582
|
+
font-size: var(--ui-font-size-sm);
|
|
583
|
+
cursor: pointer;
|
|
584
|
+
transition: all var(--ui-transition-fast);
|
|
585
|
+
}
|
|
586
|
+
|
|
587
|
+
.merge-btn:hover:not(:disabled) {
|
|
588
|
+
background: var(--ui-hover);
|
|
589
|
+
color: var(--ui-text-primary);
|
|
590
|
+
border-color: var(--ui-border-default);
|
|
591
|
+
}
|
|
592
|
+
|
|
593
|
+
.merge-btn:disabled {
|
|
594
|
+
cursor: not-allowed;
|
|
595
|
+
}
|
|
596
|
+
|
|
597
|
+
/* Sides block: spans all three parent columns on its own row, then
|
|
598
|
+
subgrids back to those same columns so each side's label, dropdown, and
|
|
599
|
+
resolved value land in the same tracks as the surrounding properties.
|
|
600
|
+
Position: relative anchors the link-state pop-bar (::before) at the
|
|
601
|
+
left edge of col 1; side labels carry padding-left so they clear the
|
|
602
|
+
bar and read as visually indented under "padding". */
|
|
603
|
+
.padding-sides-block {
|
|
604
|
+
grid-column: 1 / -1;
|
|
605
|
+
grid-row: 2;
|
|
606
|
+
display: grid;
|
|
607
|
+
grid-template-columns: subgrid;
|
|
608
|
+
row-gap: var(--ui-space-6);
|
|
609
|
+
align-items: center;
|
|
610
|
+
margin-top: var(--ui-space-6);
|
|
611
|
+
position: relative;
|
|
612
|
+
}
|
|
613
|
+
|
|
614
|
+
/* Group-scale link-state pop-bar. The bar runs the full height of the
|
|
615
|
+
sides block in both states — the four sides are conceptually one
|
|
616
|
+
property, so the indicator never breaks into a tick. Linked = thick
|
|
617
|
+
teal stripe; unlinked = thinner amber stripe, drifted slightly left
|
|
618
|
+
to mirror the dropdown's "trigger pulls away from indicator" cue. */
|
|
619
|
+
.padding-sides-block.linked::before,
|
|
620
|
+
.padding-sides-block.unlinked::before {
|
|
621
|
+
content: "";
|
|
622
|
+
position: absolute;
|
|
623
|
+
top: 50%;
|
|
624
|
+
left: 0;
|
|
625
|
+
height: 100%;
|
|
626
|
+
transform: translateY(-50%);
|
|
627
|
+
pointer-events: none;
|
|
628
|
+
background: var(--ui-text-primary);
|
|
629
|
+
border-radius: var(--ui-radius-md);
|
|
630
|
+
transition:
|
|
631
|
+
width 220ms cubic-bezier(0.4, 0, 0.2, 1),
|
|
632
|
+
border-radius 220ms cubic-bezier(0.4, 0, 0.2, 1),
|
|
633
|
+
background-color 220ms cubic-bezier(0.4, 0, 0.2, 1),
|
|
634
|
+
left 220ms cubic-bezier(0.4, 0, 0.2, 1);
|
|
635
|
+
}
|
|
636
|
+
.padding-sides-block.linked::before {
|
|
637
|
+
width: 4px;
|
|
638
|
+
}
|
|
639
|
+
.padding-sides-block.unlinked::before {
|
|
640
|
+
background: var(--ui-link-broken);
|
|
641
|
+
width: 2px;
|
|
642
|
+
border-radius: 1px;
|
|
643
|
+
left: -0.25rem;
|
|
644
|
+
}
|
|
645
|
+
@media (prefers-reduced-motion: reduce) {
|
|
646
|
+
.padding-sides-block::before { transition: none; }
|
|
647
|
+
}
|
|
648
|
+
|
|
649
|
+
.side-label {
|
|
650
|
+
grid-column: 1;
|
|
651
|
+
font-size: var(--ui-font-size-sm);
|
|
652
|
+
color: var(--ui-text-secondary);
|
|
653
|
+
text-align: left;
|
|
654
|
+
line-height: 1;
|
|
655
|
+
/* Reserve gutter for the link-state bar so labels don't jostle when the
|
|
656
|
+
indicator changes shape. Indents the side names slightly under
|
|
657
|
+
"padding" — matching the sketch where top/right/bottom/left sit one
|
|
658
|
+
step in from the parent label. */
|
|
659
|
+
padding-left: 0.75rem;
|
|
660
|
+
}
|
|
661
|
+
</style>
|