@motion-proto/live-tokens 0.6.2 → 0.8.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +14 -13
- package/dist-plugin/index.cjs +854 -226
- package/dist-plugin/index.d.cts +2 -1
- package/dist-plugin/index.d.ts +2 -1
- package/dist-plugin/index.js +852 -225
- package/package.json +26 -40
- package/src/{styles → app}/site.css +1 -1
- package/src/{component-editor → editor/component-editor}/BadgeEditor.svelte +8 -82
- package/src/{component-editor → editor/component-editor}/CalloutEditor.svelte +4 -4
- package/src/{component-editor → editor/component-editor}/CardEditor.svelte +28 -76
- package/src/{component-editor → editor/component-editor}/CollapsibleSectionEditor.svelte +37 -30
- package/src/{component-editor → editor/component-editor}/CornerBadgeEditor.svelte +31 -93
- package/src/{component-editor → editor/component-editor}/DialogEditor.svelte +60 -57
- package/src/editor/component-editor/ImageEditor.svelte +30 -0
- package/src/{component-editor → editor/component-editor}/InlineEditActionsEditor.svelte +6 -4
- package/src/editor/component-editor/MenuSelectEditor.svelte +160 -0
- package/src/{component-editor → editor/component-editor}/NotificationEditor.svelte +67 -38
- package/src/{component-editor → editor/component-editor}/ProgressBarEditor.svelte +5 -4
- package/src/{component-editor → editor/component-editor}/RadioButtonEditor.svelte +3 -3
- package/src/editor/component-editor/SectionDividerEditor.svelte +565 -0
- package/src/{component-editor → editor/component-editor}/SegmentedControlEditor.svelte +2 -2
- package/src/{component-editor → editor/component-editor}/StandardButtonsEditor.svelte +29 -21
- package/src/{component-editor → editor/component-editor}/TabBarEditor.svelte +9 -14
- package/src/{component-editor → editor/component-editor}/TableEditor.svelte +9 -18
- package/src/{component-editor → editor/component-editor}/TooltipEditor.svelte +11 -47
- package/src/editor/component-editor/editors.d.ts +10 -0
- package/src/{component-editor → editor/component-editor}/registry.ts +28 -18
- package/src/{component-editor → editor/component-editor}/scaffolding/AngleDial.svelte +54 -15
- package/src/{component-editor → editor/component-editor}/scaffolding/ComponentEditorBase.svelte +3 -51
- package/src/{component-editor → editor/component-editor}/scaffolding/ComponentFileManager.svelte +151 -424
- package/src/{component-editor → editor/component-editor}/scaffolding/ComponentFileMenu.svelte +18 -170
- package/src/{component-editor → editor/component-editor}/scaffolding/ComponentsTab.svelte +2 -2
- package/src/{component-editor → editor/component-editor}/scaffolding/CopyFromMenu.svelte +44 -4
- package/src/{component-editor → editor/component-editor}/scaffolding/FieldsetWrapper.svelte +1 -1
- package/src/{component-editor → editor/component-editor}/scaffolding/LinkageChart.svelte +6 -6
- package/src/{component-editor → editor/component-editor}/scaffolding/LinkedBlock.svelte +6 -12
- package/src/editor/component-editor/scaffolding/NonStylableConfig.svelte +38 -0
- package/src/editor/component-editor/scaffolding/RadialShapePad.svelte +483 -0
- package/src/{component-editor → editor/component-editor}/scaffolding/SaveAsDialog.svelte +66 -12
- package/src/editor/component-editor/scaffolding/ShadowBackdrop.svelte +85 -0
- package/src/editor/component-editor/scaffolding/ShadowBackdropControls.svelte +132 -0
- package/src/editor/component-editor/scaffolding/StateBlock.svelte +345 -0
- package/src/{component-editor → editor/component-editor}/scaffolding/TokenLayout.svelte +17 -12
- package/src/{component-editor → editor/component-editor}/scaffolding/TypeEditor.svelte +13 -1
- package/src/editor/component-editor/scaffolding/VariantGroup.svelte +858 -0
- package/src/{component-editor → editor/component-editor}/scaffolding/buildTypeGroupTokens.ts +1 -0
- package/src/{component-editor → editor/component-editor}/scaffolding/editorContext.ts +19 -9
- package/src/{component-editor → editor/component-editor}/scaffolding/linkedBlock.ts +2 -2
- package/src/{component-editor → editor/component-editor}/scaffolding/types.ts +25 -0
- package/src/{lib → editor/core/components}/componentConfigKeys.ts +8 -0
- package/src/{lib → editor/core/components}/componentConfigService.ts +3 -3
- package/src/{lib → editor/core/components}/componentPersist.ts +11 -9
- package/src/editor/core/flashStatus.ts +30 -0
- package/src/{lib → editor/core/fonts}/fontLoader.ts +2 -2
- package/src/{lib → editor/core/fonts}/fontMigration.ts +4 -4
- package/src/{lib → editor/core/fonts}/fontParse.ts +1 -1
- package/src/editor/core/manifests/manifestService.ts +171 -0
- package/src/editor/core/palettes/familySwap.ts +99 -0
- package/src/{lib → editor/core/palettes}/paletteDerivation.ts +71 -2
- package/src/{lib → editor/core/palettes}/tokenRegistry.ts +9 -6
- package/src/editor/core/productionPulse.ts +37 -0
- package/src/{lib → editor/core/routing}/router.ts +1 -1
- package/src/{lib/files/versionedFileResource.ts → editor/core/storage/files/versionedFileResourceClient.ts} +8 -1
- package/src/{lib → editor/core/store}/editorCore.ts +24 -8
- package/src/{lib → editor/core/store}/editorPersistence.ts +3 -3
- package/src/{lib → editor/core/store}/editorRenderer.ts +2 -2
- package/src/{lib → editor/core/store}/editorStore.ts +222 -28
- package/src/{lib → editor/core/store}/editorTypes.ts +56 -13
- package/src/editor/core/store/gradientSource.ts +192 -0
- package/src/editor/core/themes/migrations/2026-05-19-collapsiblesection-drop-frame-surface.ts +28 -0
- package/src/editor/core/themes/migrations/2026-05-19-sectiondivider-rich-gradient.ts +35 -0
- package/src/editor/core/themes/migrations/2026-05-20-sectiondivider-slim-variants.ts +82 -0
- package/src/editor/core/themes/migrations/2026-05-21-sectiondivider-spacing-to-padding.ts +24 -0
- package/src/editor/core/themes/migrations/2026-05-22-sectiondivider-intrinsics-to-css.ts +81 -0
- package/src/{lib → editor/core/themes}/migrations/index.ts +10 -0
- package/src/{lib → editor/core/themes}/slices/columns.ts +2 -2
- package/src/{lib → editor/core/themes}/slices/components.ts +20 -6
- package/src/{lib → editor/core/themes}/slices/fonts.ts +1 -1
- package/src/{lib → editor/core/themes}/slices/gradients.ts +89 -14
- package/src/{lib → editor/core/themes}/slices/overlays.ts +1 -1
- package/src/{lib → editor/core/themes}/slices/palettes.ts +1 -1
- package/src/{lib → editor/core/themes}/slices/shadows.ts +3 -3
- package/src/{lib → editor/core/themes}/themeInit.ts +8 -8
- package/src/{lib → editor/core/themes}/themeService.ts +6 -6
- package/src/{lib → editor/core/themes}/themeTypes.ts +67 -8
- package/src/editor/index.ts +69 -0
- package/src/{lib → editor/overlay}/ColumnsOverlay.svelte +0 -1
- package/src/{lib → editor/overlay}/LiveEditorOverlay.svelte +80 -129
- package/src/{lib → editor/overlay}/columnsOverlay.ts +2 -2
- package/src/{pages → editor/pages}/ComponentEditorPage.svelte +12 -12
- package/src/{pages → editor/pages}/Editor.svelte +4 -4
- package/src/{pages → editor/pages}/EditorShell.svelte +18 -36
- package/src/{styles → editor/styles}/ui-editor.css +43 -22
- package/src/{styles → editor/styles}/ui-form-controls.css +23 -24
- package/src/{ui → editor/ui}/BezierCurveEditor.svelte +119 -68
- package/src/{ui → editor/ui}/ColorEditPanel.svelte +13 -13
- package/src/{ui → editor/ui}/EditorViewSwitcher.svelte +7 -6
- package/src/editor/ui/FileLoadList.svelte +367 -0
- package/src/editor/ui/FilePill.svelte +80 -0
- package/src/editor/ui/FontStackEditor.svelte +499 -0
- package/src/editor/ui/GradientEditor.svelte +690 -0
- package/src/{ui → editor/ui}/GradientStopPicker.svelte +12 -4
- package/src/editor/ui/ManifestFileManager.svelte +438 -0
- package/src/{ui → editor/ui}/PaletteEditor.svelte +180 -673
- package/src/editor/ui/ProjectFontsSection.svelte +638 -0
- package/src/{ui → editor/ui}/SurfacesTab.svelte +3 -3
- package/src/{ui → editor/ui}/TextTab.svelte +3 -3
- package/src/editor/ui/ThemeFileManager.svelte +783 -0
- package/src/{ui → editor/ui}/UICopyPopover.svelte +4 -4
- package/src/{ui → editor/ui}/UIFontFamilySelector.svelte +6 -7
- package/src/{ui → editor/ui}/UIFontSizeSelector.svelte +4 -1
- package/src/editor/ui/UIInfoPopover.svelte +243 -0
- package/src/editor/ui/UILetterSpacingSelector.svelte +65 -0
- package/src/{ui → editor/ui}/UILineHeightSelector.svelte +5 -5
- package/src/{ui → editor/ui}/UILinkToggle.svelte +2 -2
- package/src/{ui → editor/ui}/UIPaddingSelector.svelte +6 -6
- package/src/{ui → editor/ui}/UIPaletteSelector.svelte +57 -30
- package/src/editor/ui/UIPillButton.svelte +168 -0
- package/src/{ui → editor/ui}/UIRadio.svelte +2 -2
- package/src/{ui → editor/ui}/UIRelinkConfirmPopover.svelte +4 -4
- package/src/editor/ui/UISegmentedControl.svelte +114 -0
- package/src/editor/ui/UISquareButton.svelte +172 -0
- package/src/{ui → editor/ui}/UITokenSelector.svelte +14 -11
- package/src/{ui → editor/ui}/UIVariantSelector.svelte +1 -1
- package/src/{ui → editor/ui}/VariablesTab.svelte +46 -17
- package/src/{ui → editor/ui}/palette/GradientStopEditor.svelte +13 -13
- package/src/{ui → editor/ui}/palette/OverridesPanel.svelte +24 -47
- package/src/{ui → editor/ui}/palette/PaletteBase.svelte +11 -8
- package/src/{ui → editor/ui}/palette/paletteEditorState.ts +1 -1
- package/src/editor/ui/palette/paletteMath.ts +275 -0
- package/src/{ui → editor/ui}/sections/ColumnsSection.svelte +137 -18
- package/src/{ui → editor/ui}/sections/GradientsSection.svelte +8 -8
- package/src/{ui → editor/ui}/sections/OverlaysSection.svelte +18 -18
- package/src/{ui → editor/ui}/sections/ShadowsSection.svelte +23 -23
- package/src/{ui → editor/ui}/sections/TokenScaleTable.svelte +3 -3
- package/src/{components → system/components}/Badge.svelte +0 -36
- package/src/{components → system/components}/Button.svelte +2 -2
- package/src/{components → system/components}/Card.svelte +34 -60
- package/src/{components → system/components}/CollapsibleSection.svelte +25 -2
- package/src/{components → system/components}/CornerBadge.svelte +8 -24
- package/src/{components → system/components}/Dialog.svelte +1 -1
- package/src/system/components/FloatingTokenTags.css +275 -0
- package/src/system/components/FloatingTokenTags.svelte +543 -0
- package/src/{components → system/components}/InlineEditActions.svelte +6 -4
- package/src/system/components/MenuSelect.svelte +229 -0
- package/src/{components → system/components}/Notification.svelte +8 -1
- package/src/{components → system/components}/ProgressBar.svelte +29 -11
- package/src/system/components/SectionDivider.svelte +560 -0
- package/src/{components → system/components}/SegmentedControl.svelte +49 -43
- package/src/{components → system/components}/TabBar.svelte +81 -65
- package/src/{components → system/components}/Table.svelte +17 -3
- package/src/{components → system/components}/Tooltip.svelte +6 -4
- package/src/system/styles/CONVENTIONS.md +178 -0
- package/src/system/styles/fonts.css +20 -0
- package/src/system/styles/tokens.css +601 -0
- package/src/system/styles/tokens.generated.css +544 -0
- package/src/component-editor/ImageEditor.svelte +0 -74
- package/src/component-editor/SectionDividerEditor.svelte +0 -265
- package/src/component-editor/scaffolding/DividerEditor.svelte +0 -94
- package/src/component-editor/scaffolding/GradientCard.svelte +0 -296
- package/src/component-editor/scaffolding/NonStylableConfig.svelte +0 -62
- package/src/component-editor/scaffolding/ShadowBackdrop.svelte +0 -37
- package/src/component-editor/scaffolding/ShadowBackdropControls.svelte +0 -61
- package/src/component-editor/scaffolding/StateBlock.svelte +0 -132
- package/src/component-editor/scaffolding/VariantGroup.svelte +0 -310
- package/src/components/SectionDivider.svelte +0 -483
- package/src/data/google-fonts.json +0 -75
- package/src/lib/index.ts +0 -68
- package/src/lib/presetService.ts +0 -214
- package/src/lib/productionPulse.ts +0 -32
- package/src/styles/fonts.css +0 -30
- package/src/styles/tokens.css +0 -1324
- package/src/ui/FontStackEditor.svelte +0 -361
- package/src/ui/GradientEditor.svelte +0 -470
- package/src/ui/PresetFileManager.svelte +0 -1116
- package/src/ui/ProjectFontsSection.svelte +0 -645
- package/src/ui/ThemeFileManager.svelte +0 -1020
- package/src/ui/UnsavedComponentsDialog.svelte +0 -315
- /package/src/{component-editor → editor/component-editor}/index.ts +0 -0
- /package/src/{component-editor → editor/component-editor}/scaffolding/DemoHeader.svelte +0 -0
- /package/src/{component-editor → editor/component-editor}/scaffolding/componentSectionType.ts +0 -0
- /package/src/{component-editor → editor/component-editor}/scaffolding/componentSources.ts +0 -0
- /package/src/{component-editor → editor/component-editor}/scaffolding/defaultSections.ts +0 -0
- /package/src/{component-editor → editor/component-editor}/scaffolding/siblings.ts +0 -0
- /package/src/{lib → editor/core}/cssVarSync.ts +0 -0
- /package/src/{lib → editor/core/palettes}/oklch.ts +0 -0
- /package/src/{lib → editor/core/routing}/navLinkTypes.ts +0 -0
- /package/src/{lib → editor/core/routing}/parentRouteStore.ts +0 -0
- /package/src/{lib → editor/core/storage}/storage.ts +0 -0
- /package/src/{lib → editor/core/store}/editorConfig.ts +0 -0
- /package/src/{lib → editor/core/store}/editorConfigStore.ts +0 -0
- /package/src/{lib → editor/core/store}/editorKeybindings.ts +0 -0
- /package/src/{lib → editor/core/store}/editorViewStore.ts +0 -0
- /package/src/{lib → editor/core/themes}/migrations/2026-04-24-component-prefix-and-suffix-renames.ts +0 -0
- /package/src/{lib → editor/core/themes}/migrations/2026-04-24-legacy-keys-and-bg-to-canvas.ts +0 -0
- /package/src/{lib → editor/core/themes}/migrations/2026-04-27-segmentedcontrol-disabled-flatten.ts +0 -0
- /package/src/{lib → editor/core/themes}/migrations/2026-05-08-collapsiblesection-frame-and-cleanup.ts +0 -0
- /package/src/{lib → editor/core/themes}/migrations/2026-05-08-collapsiblesection-variant-namespace.ts +0 -0
- /package/src/{lib → editor/core/themes}/migrations/2026-05-10-sectiondivider-gradient-stops.ts +0 -0
- /package/src/{lib → editor/core/themes}/migrations/2026-05-13-primary-to-brand.ts +0 -0
- /package/src/{lib → editor/core/themes}/parsers/globalRootBlock.ts +0 -0
- /package/src/{lib → editor/core/themes}/slices/domainVars.ts +0 -0
- /package/src/{lib → editor/overlay}/overlayState.ts +0 -0
- /package/src/{pages → editor/pages}/ComponentEditorPage.svelte.d.ts +0 -0
- /package/src/{pages → editor/pages}/Editor.svelte.d.ts +0 -0
- /package/src/{ui → editor/ui}/Toggle.svelte +0 -0
- /package/src/{ui → editor/ui}/UIDialog.svelte +0 -0
- /package/src/{ui → editor/ui}/UIFontWeightSelector.svelte +0 -0
- /package/src/{ui → editor/ui}/UIOptionItem.svelte +0 -0
- /package/src/{ui → editor/ui}/UIOptionList.svelte +0 -0
- /package/src/{ui → editor/ui}/UIRadioGroup.svelte +0 -0
- /package/src/{lib → editor/ui}/copyPopover.ts +0 -0
- /package/src/{ui → editor/ui}/curveEngine.ts +0 -0
- /package/src/{ui → editor/ui}/index.ts +0 -0
- /package/src/{ui → editor/ui}/keepInViewport.ts +0 -0
- /package/src/{ui → editor/ui}/palette/ScaleCurveEditor.svelte +0 -0
- /package/src/{lib → editor/ui}/scrollSection.ts +0 -0
- /package/src/{ui → editor/ui}/sections/tokenScales.ts +0 -0
- /package/src/{ui → editor/ui}/variantScales.ts +0 -0
- /package/src/{assets → system/assets}/newspaper.webp +0 -0
- /package/src/{assets → system/assets}/offering.webp +0 -0
- /package/src/{components → system/components}/Callout.svelte +0 -0
- /package/src/{components → system/components}/Image.svelte +0 -0
- /package/src/{components → system/components}/RadioButton.svelte +0 -0
- /package/src/{components → system/components}/types.ts +0 -0
- /package/src/{styles → system/styles}/_padding.scss +0 -0
- /package/src/{styles → system/styles}/fonts/Fraunces/Fraunces-italic-latin-ext.woff2 +0 -0
- /package/src/{styles → system/styles}/fonts/Fraunces/Fraunces-italic-latin.woff2 +0 -0
- /package/src/{styles → system/styles}/fonts/Fraunces/Fraunces-roman-latin-ext.woff2 +0 -0
- /package/src/{styles → system/styles}/fonts/Fraunces/Fraunces-roman-latin.woff2 +0 -0
- /package/src/{styles → system/styles}/fonts/Manrope/Manrope-latin-ext.woff2 +0 -0
- /package/src/{styles → system/styles}/fonts/Manrope/Manrope-latin.woff2 +0 -0
|
@@ -0,0 +1,858 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import { writable } from 'svelte/store';
|
|
3
|
+
import type { Snippet } from 'svelte';
|
|
4
|
+
import TokenLayout from './TokenLayout.svelte';
|
|
5
|
+
import StateBlock from './StateBlock.svelte';
|
|
6
|
+
import CopyFromMenu from './CopyFromMenu.svelte';
|
|
7
|
+
import ShadowBackdrop from './ShadowBackdrop.svelte';
|
|
8
|
+
import ShadowBackdropControls from './ShadowBackdropControls.svelte';
|
|
9
|
+
import { mutate } from '../../core/store/editorStore';
|
|
10
|
+
import { getDeclaredValue } from '../../core/palettes/tokenRegistry';
|
|
11
|
+
import type { CssVarRef } from '../../core/store/editorTypes';
|
|
12
|
+
import { getEditorContext } from './editorContext';
|
|
13
|
+
import type { Token, TypeGroupConfig } from './types';
|
|
14
|
+
import type { Sibling } from './siblings';
|
|
15
|
+
|
|
16
|
+
interface Props {
|
|
17
|
+
name: string;
|
|
18
|
+
title: string;
|
|
19
|
+
tokens?: Token[];
|
|
20
|
+
states?: Record<string, Token[]> | null;
|
|
21
|
+
typeGroups?: Record<string, TypeGroupConfig[]>;
|
|
22
|
+
/** When set, overrides are read from and cleared through the editor store. */
|
|
23
|
+
component?: string | undefined;
|
|
24
|
+
siblings?: Sibling[];
|
|
25
|
+
columns?: number;
|
|
26
|
+
/** Defaults to "Element" because most strips mix structural parts with states. */
|
|
27
|
+
selectorLabel?: string;
|
|
28
|
+
onchange?: () => void;
|
|
29
|
+
children?: Snippet<[{ activeState: string }]>;
|
|
30
|
+
stateActions?: Snippet<[string]>;
|
|
31
|
+
previewActions?: Snippet;
|
|
32
|
+
compositeControls?: Snippet<[string]>;
|
|
33
|
+
/** Each child should be a `.property-row`. Rendered above the token grid
|
|
34
|
+
so per-variant display knobs (alignment, hairline, etc.) lead the list
|
|
35
|
+
before the typography and token rows. */
|
|
36
|
+
extraPropertyRowsTop?: Snippet<[string]>;
|
|
37
|
+
/** Each child should be a `.property-row` to match the token grid above. */
|
|
38
|
+
extraPropertyRows?: Snippet<[string]>;
|
|
39
|
+
/** Extra sections appended below the Background controls inside the canvas
|
|
40
|
+
toolbar. Use `.canvas-toolbar-eyebrow` for section headings to match
|
|
41
|
+
Background. Lets per-instance display knobs (anchor, alignment, etc.)
|
|
42
|
+
live with the canvas rather than in a separate config block above. */
|
|
43
|
+
canvasToolbarExtras?: Snippet;
|
|
44
|
+
/** Per-element Show toggles, forwarded to StateBlock. Keyed by element name. */
|
|
45
|
+
elementToggles?: Record<string, { checked: boolean; label?: string; onchange: (checked: boolean) => void }>;
|
|
46
|
+
/** Explicit order for element-grouped sections, forwarded to StateBlock. */
|
|
47
|
+
elementOrder?: string[];
|
|
48
|
+
/** Per-element extras snippet, forwarded to StateBlock. Receives the
|
|
49
|
+
element name and renders between the section heading and its content. */
|
|
50
|
+
elementExtras?: Snippet<[string]>;
|
|
51
|
+
/** Skip the default centered, padded stage when the editor brings its own backdrop. */
|
|
52
|
+
unboxedPreview?: boolean;
|
|
53
|
+
backdropPadding?: string;
|
|
54
|
+
backdropModes?: ReadonlyArray<'default' | 'image' | 'color'>;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
let {
|
|
58
|
+
name,
|
|
59
|
+
title,
|
|
60
|
+
tokens = [],
|
|
61
|
+
states = null,
|
|
62
|
+
typeGroups = {},
|
|
63
|
+
component = undefined,
|
|
64
|
+
siblings = [],
|
|
65
|
+
columns = 1,
|
|
66
|
+
selectorLabel = 'Element',
|
|
67
|
+
onchange,
|
|
68
|
+
children,
|
|
69
|
+
stateActions,
|
|
70
|
+
previewActions,
|
|
71
|
+
compositeControls,
|
|
72
|
+
extraPropertyRowsTop,
|
|
73
|
+
extraPropertyRows,
|
|
74
|
+
canvasToolbarExtras,
|
|
75
|
+
elementToggles,
|
|
76
|
+
elementOrder,
|
|
77
|
+
elementExtras,
|
|
78
|
+
unboxedPreview = false,
|
|
79
|
+
backdropPadding,
|
|
80
|
+
backdropModes,
|
|
81
|
+
}: Props = $props();
|
|
82
|
+
|
|
83
|
+
let bgMode: 'default' | 'image' | 'color' = $state('default');
|
|
84
|
+
let bgVar = $derived(`--backdrop-${component ?? name}-surface`);
|
|
85
|
+
|
|
86
|
+
const editorCtx = getEditorContext();
|
|
87
|
+
const linkedOrderStore = editorCtx?.linkedOrder ?? writable<Map<string, number> | null>(null);
|
|
88
|
+
const focusedVariantStore = editorCtx?.focusedVariant ?? writable<string | null>(null);
|
|
89
|
+
const focusedStateStore = editorCtx?.focusedState ?? writable<string | null>(null);
|
|
90
|
+
const variantsStore = editorCtx?.variants ?? writable<{ value: string; label: string }[]>([]);
|
|
91
|
+
const preserveColorFamilyStore = editorCtx?.preserveColorFamily ?? writable(false);
|
|
92
|
+
let linkedOrder = $derived($linkedOrderStore ?? undefined);
|
|
93
|
+
|
|
94
|
+
let activeTab: string = $state('');
|
|
95
|
+
|
|
96
|
+
const TYPE_PROPS = ['colorVariable', 'familyVariable', 'sizeVariable', 'weightVariable', 'lineHeightVariable', 'letterSpacingVariable', 'outlineWidthVariable', 'outlineColorVariable'] as const;
|
|
97
|
+
// Carry per-side derived vars so split padding fully transfers; no-op when absent.
|
|
98
|
+
const PADDING_SIDES = ['top', 'right', 'bottom', 'left'] as const;
|
|
99
|
+
|
|
100
|
+
/** A token whose label contains "color" describes a color property. The
|
|
101
|
+
copy-from "Preserve color families" toggle skips these so the destination
|
|
102
|
+
keeps its existing palette family (e.g. button-primary stays on `brand`)
|
|
103
|
+
while still picking up shape/typography from the source variant. */
|
|
104
|
+
function isColorToken(t: Token): boolean {
|
|
105
|
+
return t.label.toLowerCase().includes('color');
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/** Parse the right-hand side of a CSS declaration (e.g. `var(--surface-accent-lowest)`
|
|
109
|
+
or `#ff0000`) into a CssVarRef. Returns null for empty/unparseable input.
|
|
110
|
+
Used by copy-from when the source has no explicit override — we resolve
|
|
111
|
+
its declared default so the destination visually matches the source
|
|
112
|
+
instead of falling back to its own family's default. */
|
|
113
|
+
function declaredToRef(declared: string | null): CssVarRef | null {
|
|
114
|
+
if (!declared) return null;
|
|
115
|
+
const m = declared.match(/^\s*var\((--[a-z0-9-]+)\)\s*$/i);
|
|
116
|
+
if (m) return { kind: 'token', name: m[1] };
|
|
117
|
+
return { kind: 'literal', value: declared };
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/** Extract a transparency percentage from a `color-mix(in srgb, var(--X) N%, transparent)`
|
|
121
|
+
wrapper, optionally itself wrapped in `var(...)`. Returns null if no
|
|
122
|
+
such wrapper is present. */
|
|
123
|
+
const ALPHA_RE = /color-mix\s*\(\s*in\s+srgb\s*,\s*var\(--[a-z0-9-]+\)\s+([\d.]+%)\s*,\s*transparent\s*\)/i;
|
|
124
|
+
function extractAlpha(value: string): string | null {
|
|
125
|
+
const m = value.match(ALPHA_RE);
|
|
126
|
+
return m ? m[1] : null;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/** Extract the inner var() base reference, whether the value is a bare
|
|
130
|
+
`var(--X)`, or wrapped by a transparency color-mix, or both. */
|
|
131
|
+
function extractBaseToken(value: string): string | null {
|
|
132
|
+
const mix = value.match(/color-mix\s*\(\s*in\s+srgb\s*,\s*var\((--[a-z0-9-]+)\)/i);
|
|
133
|
+
if (mix) return mix[1];
|
|
134
|
+
const bare = value.match(/var\((--[a-z0-9-]+)\)/i);
|
|
135
|
+
return bare ? bare[1] : null;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/** TypeGroup props whose values are color tokens; preserve-color-families
|
|
139
|
+
skips these in the typeGroups copy loop. */
|
|
140
|
+
const COLOR_TYPE_PROPS = new Set(['colorVariable', 'outlineColorVariable']);
|
|
141
|
+
|
|
142
|
+
/** True iff `colorRef` is a CSS-var token whose slug contains `family`
|
|
143
|
+
as a hyphen-delimited segment (e.g. `--surface-canvas-low` ∋ `canvas`). */
|
|
144
|
+
function stopMatchesFamily(colorRef: string, family: string): boolean {
|
|
145
|
+
if (!colorRef.startsWith('--')) return false;
|
|
146
|
+
return colorRef.slice(2).split('-').includes(family);
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/** Replace the first `from` segment in a CSS-var token slug with `to`.
|
|
150
|
+
Used to swap stop colors across variant families on gradient copy
|
|
151
|
+
(e.g. `--surface-success-high` + (success → accent) → `--surface-accent-high`). */
|
|
152
|
+
function swapFamilyInToken(colorRef: string, from: string, to: string): string {
|
|
153
|
+
if (!colorRef.startsWith('--')) return colorRef;
|
|
154
|
+
const parts = colorRef.slice(2).split('-');
|
|
155
|
+
const idx = parts.indexOf(from);
|
|
156
|
+
if (idx < 0) return colorRef;
|
|
157
|
+
parts[idx] = to;
|
|
158
|
+
return '--' + parts.join('-');
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
function pickCopySource(toState: string, fromVariant: string, fromState: string) {
|
|
162
|
+
const preserveColorFamily = $preserveColorFamilyStore;
|
|
163
|
+
if (!component || !states) return;
|
|
164
|
+
const isSelfVariant = fromVariant === name;
|
|
165
|
+
const sibling = isSelfVariant ? null : siblings.find((s) => s.name === fromVariant);
|
|
166
|
+
if (!isSelfVariant && !sibling) return;
|
|
167
|
+
const srcTokens = (isSelfVariant ? states[fromState] : sibling!.states[fromState]) ?? [];
|
|
168
|
+
const dstTokens = states[toState] ?? [];
|
|
169
|
+
const srcTypeGroups = (isSelfVariant ? typeGroups[fromState] : sibling!.typeGroups?.[fromState]) ?? [];
|
|
170
|
+
const dstTypeGroups = typeGroups[toState] ?? [];
|
|
171
|
+
|
|
172
|
+
mutate(`copy ${fromVariant}/${fromState} → ${name}/${toState}`, (s) => {
|
|
173
|
+
const slice = s.components[component!] ?? (s.components[component!] = { activeFile: 'default', aliases: {}, config: {} });
|
|
174
|
+
const dstVarsTouched: string[] = [];
|
|
175
|
+
/** Resolve a variable's effective value as a CSS string: the override if
|
|
176
|
+
set, otherwise its declared default. Returns null if neither exists.
|
|
177
|
+
Gradient refs short-circuit to null — the copy path for gradients
|
|
178
|
+
uses `applyGradient`, not the alpha-extract pipeline. */
|
|
179
|
+
const effectiveValue = (varName: string): string | null => {
|
|
180
|
+
const ref = slice.aliases[varName];
|
|
181
|
+
if (!ref) return getDeclaredValue(varName);
|
|
182
|
+
if (ref.kind === 'token') return `var(${ref.name})`;
|
|
183
|
+
if (ref.kind === 'literal') return ref.value;
|
|
184
|
+
return null;
|
|
185
|
+
};
|
|
186
|
+
|
|
187
|
+
const apply = (srcVar: string, dstVar: string) => {
|
|
188
|
+
if (srcVar === dstVar) return;
|
|
189
|
+
if (srcVar in slice.aliases) {
|
|
190
|
+
slice.aliases[dstVar] = slice.aliases[srcVar];
|
|
191
|
+
} else {
|
|
192
|
+
// Src is at its declared default. Materialize that default as dst's
|
|
193
|
+
// override so dst visually picks up src's value — otherwise dst
|
|
194
|
+
// falls back to its OWN family default and the copy is a visual no-op.
|
|
195
|
+
const ref = declaredToRef(getDeclaredValue(srcVar));
|
|
196
|
+
if (ref) slice.aliases[dstVar] = ref;
|
|
197
|
+
else delete slice.aliases[dstVar];
|
|
198
|
+
}
|
|
199
|
+
dstVarsTouched.push(dstVar);
|
|
200
|
+
};
|
|
201
|
+
|
|
202
|
+
/** Preserve-color-families variant of apply: copy src's transparency
|
|
203
|
+
wrapper (if any) over dst's existing base color so dst keeps its
|
|
204
|
+
palette family but picks up src's alpha. Source with no alpha = no-op
|
|
205
|
+
(leaves dst's color untouched). */
|
|
206
|
+
const applyColorPreserve = (srcVar: string, dstVar: string) => {
|
|
207
|
+
const srcVal = effectiveValue(srcVar);
|
|
208
|
+
if (!srcVal) return;
|
|
209
|
+
const alpha = extractAlpha(srcVal);
|
|
210
|
+
if (!alpha) return;
|
|
211
|
+
const dstVal = effectiveValue(dstVar);
|
|
212
|
+
const dstBase = dstVal ? extractBaseToken(dstVal) : null;
|
|
213
|
+
if (!dstBase) return;
|
|
214
|
+
slice.aliases[dstVar] = {
|
|
215
|
+
kind: 'literal',
|
|
216
|
+
value: `color-mix(in srgb, var(${dstBase}) ${alpha}, transparent)`,
|
|
217
|
+
};
|
|
218
|
+
dstVarsTouched.push(dstVar);
|
|
219
|
+
};
|
|
220
|
+
/** Copy a structured gradient ref. Stops carry source's positions +
|
|
221
|
+
opacities verbatim; out-of-family stop colors carry verbatim too.
|
|
222
|
+
With preserveColorFamily on, in-family stop colors swap to the
|
|
223
|
+
destination family — see plan §5 for the worked example. */
|
|
224
|
+
const applyGradient = (
|
|
225
|
+
srcVar: string,
|
|
226
|
+
dstVar: string,
|
|
227
|
+
srcFamily: string | undefined,
|
|
228
|
+
dstFamily: string | undefined,
|
|
229
|
+
) => {
|
|
230
|
+
const srcRef = slice.aliases[srcVar];
|
|
231
|
+
if (!srcRef || srcRef.kind !== 'gradient') {
|
|
232
|
+
// Source has no override — clearing dst returns it to its own CSS
|
|
233
|
+
// default (which is dst's family by design), preserving the
|
|
234
|
+
// "destination keeps its family palette" invariant.
|
|
235
|
+
delete slice.aliases[dstVar];
|
|
236
|
+
dstVarsTouched.push(dstVar);
|
|
237
|
+
return;
|
|
238
|
+
}
|
|
239
|
+
const swapFamilies = preserveColorFamily
|
|
240
|
+
&& !!srcFamily && !!dstFamily
|
|
241
|
+
&& srcFamily !== dstFamily;
|
|
242
|
+
const newStops = srcRef.value.stops.map((s) => {
|
|
243
|
+
if (swapFamilies && stopMatchesFamily(s.color, srcFamily!)) {
|
|
244
|
+
return { ...s, color: swapFamilyInToken(s.color, srcFamily!, dstFamily!) };
|
|
245
|
+
}
|
|
246
|
+
return { ...s };
|
|
247
|
+
});
|
|
248
|
+
slice.aliases[dstVar] = {
|
|
249
|
+
kind: 'gradient',
|
|
250
|
+
value: { type: srcRef.value.type, angle: srcRef.value.angle, stops: newStops },
|
|
251
|
+
};
|
|
252
|
+
dstVarsTouched.push(dstVar);
|
|
253
|
+
};
|
|
254
|
+
|
|
255
|
+
const minLen = Math.min(srcTokens.length, dstTokens.length);
|
|
256
|
+
for (let i = 0; i < minLen; i++) {
|
|
257
|
+
const srcVar = srcTokens[i].variable;
|
|
258
|
+
const dstVar = dstTokens[i].variable;
|
|
259
|
+
if (srcTokens[i].kind === 'gradient' || dstTokens[i].kind === 'gradient') {
|
|
260
|
+
applyGradient(srcVar, dstVar, srcTokens[i].family, dstTokens[i].family);
|
|
261
|
+
continue;
|
|
262
|
+
}
|
|
263
|
+
if (preserveColorFamily && isColorToken(srcTokens[i])) {
|
|
264
|
+
applyColorPreserve(srcVar, dstVar);
|
|
265
|
+
continue;
|
|
266
|
+
}
|
|
267
|
+
apply(srcVar, dstVar);
|
|
268
|
+
if (srcTokens[i].splittable !== false) {
|
|
269
|
+
for (const side of PADDING_SIDES) apply(`${srcVar}-${side}`, `${dstVar}-${side}`);
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
const minTypeGroups = Math.min(srcTypeGroups.length, dstTypeGroups.length);
|
|
273
|
+
for (let g = 0; g < minTypeGroups; g++) {
|
|
274
|
+
const srcType = srcTypeGroups[g];
|
|
275
|
+
const dstType = dstTypeGroups[g];
|
|
276
|
+
for (const prop of TYPE_PROPS) {
|
|
277
|
+
const srcVar = srcType[prop];
|
|
278
|
+
const dstVar = dstType[prop];
|
|
279
|
+
if (!srcVar || !dstVar) continue;
|
|
280
|
+
if (preserveColorFamily && COLOR_TYPE_PROPS.has(prop)) {
|
|
281
|
+
applyColorPreserve(srcVar, dstVar);
|
|
282
|
+
continue;
|
|
283
|
+
}
|
|
284
|
+
apply(srcVar, dstVar);
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
// Copy intent "make this state match" clears stale unlinked markers on touched vars.
|
|
288
|
+
if (slice.unlinked && slice.unlinked.length > 0) {
|
|
289
|
+
const touched = new Set(dstVarsTouched);
|
|
290
|
+
const remaining = slice.unlinked.filter((v) => !touched.has(v));
|
|
291
|
+
if (remaining.length === 0) delete slice.unlinked;
|
|
292
|
+
else slice.unlinked = remaining;
|
|
293
|
+
}
|
|
294
|
+
});
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
let copySources = $derived.by(() => {
|
|
298
|
+
const fromSiblings = siblings.map((s) => ({
|
|
299
|
+
name: s.name,
|
|
300
|
+
label: s.label,
|
|
301
|
+
states: Object.keys(s.states),
|
|
302
|
+
}));
|
|
303
|
+
const ownStates = states ? Object.keys(states) : [];
|
|
304
|
+
if (ownStates.length >= 2) {
|
|
305
|
+
return [{ name, label: title, states: ownStates }, ...fromSiblings];
|
|
306
|
+
}
|
|
307
|
+
return fromSiblings;
|
|
308
|
+
});
|
|
309
|
+
|
|
310
|
+
let stateNames = $derived(states ? Object.keys(states) : []);
|
|
311
|
+
/** Key-name convention: when ANY state name contains " / ", the strip switches
|
|
312
|
+
to two-tier rendering — top row is parts (unique left-hand sides), bottom
|
|
313
|
+
row is the active part's states (right-hand sides). Parts with no slash
|
|
314
|
+
have no sub-states and skip the bottom row when active. */
|
|
315
|
+
const PART_SEP = ' / ';
|
|
316
|
+
let isHierarchical = $derived(stateNames.some((n) => n.includes(PART_SEP)));
|
|
317
|
+
/** Ordered list of unique parts, preserving the order they first appear in `states`. */
|
|
318
|
+
let parts = $derived.by(() => {
|
|
319
|
+
const seen: string[] = [];
|
|
320
|
+
for (const n of stateNames) {
|
|
321
|
+
const part = n.includes(PART_SEP) ? n.split(PART_SEP)[0] : n;
|
|
322
|
+
if (!seen.includes(part)) seen.push(part);
|
|
323
|
+
}
|
|
324
|
+
return seen;
|
|
325
|
+
});
|
|
326
|
+
/** Sub-states of the currently active part (only meaningful in hierarchical mode). */
|
|
327
|
+
let activePart = $derived(activeTab.includes(PART_SEP) ? activeTab.split(PART_SEP)[0] : activeTab);
|
|
328
|
+
let activeSubState = $derived(activeTab.includes(PART_SEP) ? activeTab.split(PART_SEP)[1] : '');
|
|
329
|
+
let partSubStates = $derived(
|
|
330
|
+
isHierarchical
|
|
331
|
+
? stateNames.filter((n) => n.startsWith(activePart + PART_SEP)).map((n) => n.split(PART_SEP)[1])
|
|
332
|
+
: [],
|
|
333
|
+
);
|
|
334
|
+
let tabsStripVisible = $derived(stateNames.length >= 2);
|
|
335
|
+
let subStripVisible = $derived(isHierarchical && partSubStates.length >= 2);
|
|
336
|
+
|
|
337
|
+
/** Switch parts. If the new part has sub-states, jump to its first one; otherwise
|
|
338
|
+
activate the part itself. Keeps activeTab as a single canonical key so the
|
|
339
|
+
downstream property lookup, copy-from menus, and focusedStateStore stay simple. */
|
|
340
|
+
function selectPart(part: string) {
|
|
341
|
+
const firstSub = stateNames.find((n) => n.startsWith(part + PART_SEP));
|
|
342
|
+
activeTab = firstSub ?? part;
|
|
343
|
+
focusedStateStore.set(activeTab);
|
|
344
|
+
}
|
|
345
|
+
function selectSubState(sub: string) {
|
|
346
|
+
activeTab = `${activePart}${PART_SEP}${sub}`;
|
|
347
|
+
focusedStateStore.set(activeTab);
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
$effect(() => {
|
|
351
|
+
if (stateNames.length > 0 && !stateNames.includes(activeTab)) {
|
|
352
|
+
activeTab = stateNames[0];
|
|
353
|
+
}
|
|
354
|
+
});
|
|
355
|
+
// Adopt cross-group state hint from chart row clicks.
|
|
356
|
+
$effect(() => {
|
|
357
|
+
if ($focusedStateStore && stateNames.includes($focusedStateStore) && activeTab !== $focusedStateStore) {
|
|
358
|
+
activeTab = $focusedStateStore;
|
|
359
|
+
}
|
|
360
|
+
});
|
|
361
|
+
|
|
362
|
+
let inFocusMode = $derived(siblings.length > 0);
|
|
363
|
+
let amIFocused = $derived($focusedVariantStore === name);
|
|
364
|
+
let shouldRender = $derived(!inFocusMode || amIFocused);
|
|
365
|
+
let showVariantTabs = $derived(inFocusMode && $variantsStore.length >= 2);
|
|
366
|
+
// Mirror state-tab clicks to the shared store so linked-block + chart track them.
|
|
367
|
+
$effect(() => {
|
|
368
|
+
if (amIFocused && activeTab && stateNames.includes(activeTab) && $focusedStateStore !== activeTab) {
|
|
369
|
+
focusedStateStore.set(activeTab);
|
|
370
|
+
}
|
|
371
|
+
});
|
|
372
|
+
|
|
373
|
+
</script>
|
|
374
|
+
|
|
375
|
+
{#if shouldRender}
|
|
376
|
+
<div class="editor-section-card demo-section variant-group">
|
|
377
|
+
{#if states}
|
|
378
|
+
<div class="tabs-preview">
|
|
379
|
+
<div class="preview-header">
|
|
380
|
+
<span class="editor-section-title">Preview</span>
|
|
381
|
+
{#if showVariantTabs}
|
|
382
|
+
<div class="variant-tabs" role="tablist">
|
|
383
|
+
{#each $variantsStore as opt (opt.value)}
|
|
384
|
+
<button
|
|
385
|
+
type="button"
|
|
386
|
+
class="variant-tab-btn"
|
|
387
|
+
class:active={opt.value === $focusedVariantStore}
|
|
388
|
+
role="tab"
|
|
389
|
+
aria-selected={opt.value === $focusedVariantStore}
|
|
390
|
+
onclick={() => focusedVariantStore.set(opt.value)}
|
|
391
|
+
>{opt.label}</button>
|
|
392
|
+
{/each}
|
|
393
|
+
</div>
|
|
394
|
+
{/if}
|
|
395
|
+
{#if previewActions}
|
|
396
|
+
<div class="preview-actions">
|
|
397
|
+
{@render previewActions?.()}
|
|
398
|
+
</div>
|
|
399
|
+
{/if}
|
|
400
|
+
</div>
|
|
401
|
+
{#if unboxedPreview}
|
|
402
|
+
{@render children?.({ activeState: activeTab })}
|
|
403
|
+
{:else}
|
|
404
|
+
<ShadowBackdrop mode={bgMode} colorVariable={bgVar} padding={backdropPadding}>
|
|
405
|
+
{#snippet controls()}
|
|
406
|
+
<div class="canvas-toolbar">
|
|
407
|
+
<span class="canvas-toolbar-eyebrow">Background</span>
|
|
408
|
+
<ShadowBackdropControls bind:mode={bgMode} colorVariable={bgVar} modes={backdropModes ?? ['default', 'image', 'color']} />
|
|
409
|
+
{@render canvasToolbarExtras?.()}
|
|
410
|
+
</div>
|
|
411
|
+
{/snippet}
|
|
412
|
+
{@render children?.({ activeState: activeTab })}
|
|
413
|
+
</ShadowBackdrop>
|
|
414
|
+
{/if}
|
|
415
|
+
|
|
416
|
+
{#if tabsStripVisible}
|
|
417
|
+
<div class="tabs-states-block">
|
|
418
|
+
<span class="editor-subsection-title">{selectorLabel}</span>
|
|
419
|
+
<div class="tabs-selectors">
|
|
420
|
+
{#if isHierarchical}
|
|
421
|
+
<div class="state-tabs" role="tablist">
|
|
422
|
+
{#each parts as p}
|
|
423
|
+
<button
|
|
424
|
+
type="button"
|
|
425
|
+
class="state-tab-btn"
|
|
426
|
+
class:active={activePart === p}
|
|
427
|
+
role="tab"
|
|
428
|
+
aria-selected={activePart === p}
|
|
429
|
+
onclick={() => selectPart(p)}
|
|
430
|
+
>{p}</button>
|
|
431
|
+
{/each}
|
|
432
|
+
</div>
|
|
433
|
+
{:else}
|
|
434
|
+
<div class="state-tabs" role="tablist">
|
|
435
|
+
{#each stateNames as s}
|
|
436
|
+
<button
|
|
437
|
+
type="button"
|
|
438
|
+
class="state-tab-btn"
|
|
439
|
+
class:active={activeTab === s}
|
|
440
|
+
role="tab"
|
|
441
|
+
aria-selected={activeTab === s}
|
|
442
|
+
onclick={() => { activeTab = s; focusedStateStore.set(s); }}
|
|
443
|
+
>{s}</button>
|
|
444
|
+
{/each}
|
|
445
|
+
</div>
|
|
446
|
+
{/if}
|
|
447
|
+
{#if activeTab}
|
|
448
|
+
{@render stateActions?.(activeTab)}
|
|
449
|
+
{/if}
|
|
450
|
+
</div>
|
|
451
|
+
{#if subStripVisible}
|
|
452
|
+
<div class="tabs-selectors substrip">
|
|
453
|
+
<span class="editor-subsection-title state-eyebrow">State</span>
|
|
454
|
+
<div class="state-tabs" role="tablist">
|
|
455
|
+
{#each partSubStates as s}
|
|
456
|
+
<button
|
|
457
|
+
type="button"
|
|
458
|
+
class="state-tab-btn"
|
|
459
|
+
class:active={activeSubState === s}
|
|
460
|
+
role="tab"
|
|
461
|
+
aria-selected={activeSubState === s}
|
|
462
|
+
onclick={() => selectSubState(s)}
|
|
463
|
+
>{s}</button>
|
|
464
|
+
{/each}
|
|
465
|
+
</div>
|
|
466
|
+
</div>
|
|
467
|
+
{/if}
|
|
468
|
+
</div>
|
|
469
|
+
{/if}
|
|
470
|
+
</div>
|
|
471
|
+
|
|
472
|
+
{#if activeTab && states[activeTab]}
|
|
473
|
+
{@const stateName = activeTab}
|
|
474
|
+
{@render compositeControls?.(stateName)}
|
|
475
|
+
<div class="properties-header">
|
|
476
|
+
<span class="editor-subsection-title properties-title">Properties</span>
|
|
477
|
+
{#if !tabsStripVisible && stateActions}
|
|
478
|
+
{@render stateActions(activeTab)}
|
|
479
|
+
{/if}
|
|
480
|
+
{#if copySources.length > 0}
|
|
481
|
+
<CopyFromMenu
|
|
482
|
+
toState={activeTab}
|
|
483
|
+
variantName={name}
|
|
484
|
+
{copySources}
|
|
485
|
+
onselect={(d) => pickCopySource(activeTab, d.fromVariant, d.fromState)}
|
|
486
|
+
/>
|
|
487
|
+
{/if}
|
|
488
|
+
</div>
|
|
489
|
+
{#if extraPropertyRowsTop}
|
|
490
|
+
<div class="extra-property-rows extra-property-rows--top">
|
|
491
|
+
{@render extraPropertyRowsTop(stateName)}
|
|
492
|
+
</div>
|
|
493
|
+
{/if}
|
|
494
|
+
<StateBlock
|
|
495
|
+
tokens={states[stateName]}
|
|
496
|
+
typeGroups={typeGroups[stateName] ?? []}
|
|
497
|
+
{component}
|
|
498
|
+
{linkedOrder}
|
|
499
|
+
{columns}
|
|
500
|
+
{elementToggles}
|
|
501
|
+
{elementOrder}
|
|
502
|
+
{elementExtras}
|
|
503
|
+
{onchange}
|
|
504
|
+
/>
|
|
505
|
+
{#if extraPropertyRows}
|
|
506
|
+
<div class="extra-property-rows">
|
|
507
|
+
{@render extraPropertyRows(stateName)}
|
|
508
|
+
</div>
|
|
509
|
+
{/if}
|
|
510
|
+
{/if}
|
|
511
|
+
{:else}
|
|
512
|
+
{#if unboxedPreview}
|
|
513
|
+
{@render children?.({ activeState: '' })}
|
|
514
|
+
{:else}
|
|
515
|
+
<ShadowBackdrop mode={bgMode} colorVariable={bgVar} padding={backdropPadding}>
|
|
516
|
+
{@render children?.({ activeState: '' })}
|
|
517
|
+
</ShadowBackdrop>
|
|
518
|
+
{/if}
|
|
519
|
+
<TokenLayout
|
|
520
|
+
title={name}
|
|
521
|
+
tokens={tokens}
|
|
522
|
+
{component}
|
|
523
|
+
{linkedOrder}
|
|
524
|
+
{onchange}
|
|
525
|
+
/>
|
|
526
|
+
{/if}
|
|
527
|
+
|
|
528
|
+
</div>
|
|
529
|
+
{/if}
|
|
530
|
+
|
|
531
|
+
<style>
|
|
532
|
+
/* Card chrome lives on .editor-section-card in ui-editor.css. */
|
|
533
|
+
.variant-group {
|
|
534
|
+
gap: var(--ui-space-12);
|
|
535
|
+
container-type: inline-size;
|
|
536
|
+
container-name: variant-group;
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
/* Pin the preview + state-tab strip to the top of the page scroll so
|
|
540
|
+
property edits stay visually connected to the preview without scrolling.
|
|
541
|
+
The card background extends through the sticky band so the property grid
|
|
542
|
+
scrolls cleanly behind it. The element-grouped property layout (see
|
|
543
|
+
StateBlock) fans out horizontally, which keeps the property section
|
|
544
|
+
short enough that the sticky preview rarely steals usable space. */
|
|
545
|
+
.tabs-preview {
|
|
546
|
+
position: sticky;
|
|
547
|
+
top: 0;
|
|
548
|
+
z-index: 2;
|
|
549
|
+
display: flex;
|
|
550
|
+
flex-direction: column;
|
|
551
|
+
gap: var(--ui-space-20);
|
|
552
|
+
background: var(--ui-surface-low);
|
|
553
|
+
/* Bleed the background up through the card's top padding so content
|
|
554
|
+
scrolling behind doesn't peek between the viewport edge and the
|
|
555
|
+
pinned preview. The matching negative margin restores flow position.
|
|
556
|
+
Border-radius hugs the card's rounded top corners so the unpinned
|
|
557
|
+
state still reads as one continuous panel. */
|
|
558
|
+
margin: calc(-1 * var(--ui-space-20)) calc(-1 * var(--ui-space-20)) 0;
|
|
559
|
+
padding: var(--ui-space-20) var(--ui-space-20) 0;
|
|
560
|
+
border-radius: var(--ui-radius-md) var(--ui-radius-md) 0 0;
|
|
561
|
+
}
|
|
562
|
+
|
|
563
|
+
/* Soft fade at the bottom of the sticky band so property rows scrolling
|
|
564
|
+
up don't sharply cut against the pinned preview. Greyscale (no accent).
|
|
565
|
+
Sits below the preview body, above scrolling content. */
|
|
566
|
+
.tabs-preview::after {
|
|
567
|
+
content: '';
|
|
568
|
+
position: absolute;
|
|
569
|
+
left: 0;
|
|
570
|
+
right: 0;
|
|
571
|
+
bottom: calc(-1 * var(--ui-space-12));
|
|
572
|
+
height: var(--ui-space-12);
|
|
573
|
+
background: linear-gradient(to bottom, var(--ui-surface-low), transparent);
|
|
574
|
+
pointer-events: none;
|
|
575
|
+
}
|
|
576
|
+
|
|
577
|
+
.preview-header {
|
|
578
|
+
display: flex;
|
|
579
|
+
align-items: center;
|
|
580
|
+
gap: var(--ui-space-24);
|
|
581
|
+
flex-wrap: wrap;
|
|
582
|
+
}
|
|
583
|
+
|
|
584
|
+
.preview-actions {
|
|
585
|
+
margin-left: auto;
|
|
586
|
+
display: inline-flex;
|
|
587
|
+
align-items: center;
|
|
588
|
+
gap: var(--ui-space-8);
|
|
589
|
+
}
|
|
590
|
+
|
|
591
|
+
/* Sits in the backdrop's right-rail column (ShadowBackdrop owns the two-column
|
|
592
|
+
split). Own border + raised surface so it reads as a distinct panel inside
|
|
593
|
+
the backdrop, regardless of which backdrop mode is active. */
|
|
594
|
+
.canvas-toolbar {
|
|
595
|
+
width: 11rem;
|
|
596
|
+
height: 100%;
|
|
597
|
+
display: flex;
|
|
598
|
+
flex-direction: column;
|
|
599
|
+
gap: var(--ui-space-12);
|
|
600
|
+
padding: var(--ui-space-10) var(--ui-space-12);
|
|
601
|
+
background: var(--ui-surface-low);
|
|
602
|
+
border: 1px solid var(--ui-border-low);
|
|
603
|
+
border-radius: var(--ui-radius-md);
|
|
604
|
+
color: var(--ui-text-primary);
|
|
605
|
+
box-sizing: border-box;
|
|
606
|
+
}
|
|
607
|
+
|
|
608
|
+
@container variant-group (max-width: 32rem) {
|
|
609
|
+
.canvas-toolbar {
|
|
610
|
+
width: 100%;
|
|
611
|
+
height: auto;
|
|
612
|
+
}
|
|
613
|
+
}
|
|
614
|
+
|
|
615
|
+
.canvas-toolbar :global(.canvas-toolbar-eyebrow) {
|
|
616
|
+
font-size: var(--ui-font-size-xs);
|
|
617
|
+
font-weight: var(--ui-font-weight-medium);
|
|
618
|
+
color: var(--ui-text-tertiary);
|
|
619
|
+
}
|
|
620
|
+
|
|
621
|
+
/* Divider lives *between* sections, not under each eyebrow. First eyebrow
|
|
622
|
+
has no preceding sibling and stays flush. */
|
|
623
|
+
.canvas-toolbar > :global(* + .canvas-toolbar-eyebrow) {
|
|
624
|
+
padding-top: var(--ui-space-12);
|
|
625
|
+
border-top: 1px solid var(--ui-border-low);
|
|
626
|
+
}
|
|
627
|
+
|
|
628
|
+
/* Explicit separator for label-less sections in canvasToolbarExtras. */
|
|
629
|
+
.canvas-toolbar :global(.canvas-toolbar-divider) {
|
|
630
|
+
margin: 0;
|
|
631
|
+
border: 0;
|
|
632
|
+
border-top: 1px solid var(--ui-border-low);
|
|
633
|
+
}
|
|
634
|
+
|
|
635
|
+
/* Native <select> styled to match the property-row trigger chrome
|
|
636
|
+
(UITokenSelector) so toolbar selects don't read as a separate visual
|
|
637
|
+
vocabulary from the rest of the editor. */
|
|
638
|
+
.canvas-toolbar :global(.canvas-toolbar-select) {
|
|
639
|
+
width: 100%;
|
|
640
|
+
box-sizing: border-box;
|
|
641
|
+
appearance: none;
|
|
642
|
+
-webkit-appearance: none;
|
|
643
|
+
padding: 0 var(--ui-space-24) 0 var(--ui-space-8);
|
|
644
|
+
min-height: 1.75rem;
|
|
645
|
+
background-color: var(--ui-surface-low);
|
|
646
|
+
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='10' height='10' viewBox='0 0 12 12'%3E%3Cpath fill='none' stroke='%23999' stroke-width='1.5' stroke-linecap='round' stroke-linejoin='round' d='M3 5l3 3 3-3'/%3E%3C/svg%3E");
|
|
647
|
+
background-repeat: no-repeat;
|
|
648
|
+
background-position: right var(--ui-space-8) center;
|
|
649
|
+
border: 1px solid var(--ui-border);
|
|
650
|
+
border-radius: var(--ui-radius-md);
|
|
651
|
+
color: var(--ui-text-primary);
|
|
652
|
+
font-family: var(--ui-font-sans);
|
|
653
|
+
font-size: var(--ui-font-size-sm);
|
|
654
|
+
cursor: pointer;
|
|
655
|
+
transition: background-color var(--ui-transition-fast), border-color var(--ui-transition-fast);
|
|
656
|
+
}
|
|
657
|
+
.canvas-toolbar :global(.canvas-toolbar-select:hover) {
|
|
658
|
+
background-color: var(--ui-surface-high);
|
|
659
|
+
border-color: var(--ui-border-higher);
|
|
660
|
+
}
|
|
661
|
+
.canvas-toolbar :global(.canvas-toolbar-select:focus-visible) {
|
|
662
|
+
outline: 2px solid var(--ui-highlight);
|
|
663
|
+
outline-offset: 2px;
|
|
664
|
+
}
|
|
665
|
+
|
|
666
|
+
/* Native <input> styled to match the toolbar's select chrome. Long values
|
|
667
|
+
ellipsize when blurred so the input doesn't grow or scroll-bleed past
|
|
668
|
+
the toolbar's 11rem column; focus restores native caret-driven scroll. */
|
|
669
|
+
.canvas-toolbar :global(.canvas-toolbar-input) {
|
|
670
|
+
width: 100%;
|
|
671
|
+
min-width: 0;
|
|
672
|
+
max-width: 100%;
|
|
673
|
+
box-sizing: border-box;
|
|
674
|
+
padding: 0 var(--ui-space-8);
|
|
675
|
+
min-height: 1.75rem;
|
|
676
|
+
background: var(--ui-surface-low);
|
|
677
|
+
border: 1px solid var(--ui-border);
|
|
678
|
+
border-radius: var(--ui-radius-md);
|
|
679
|
+
color: var(--ui-text-primary);
|
|
680
|
+
font-family: var(--ui-font-sans);
|
|
681
|
+
font-size: var(--ui-font-size-sm);
|
|
682
|
+
text-overflow: ellipsis;
|
|
683
|
+
transition: background-color var(--ui-transition-fast), border-color var(--ui-transition-fast);
|
|
684
|
+
}
|
|
685
|
+
.canvas-toolbar :global(.canvas-toolbar-input:hover) {
|
|
686
|
+
border-color: var(--ui-border-higher);
|
|
687
|
+
}
|
|
688
|
+
.canvas-toolbar :global(.canvas-toolbar-input:focus-visible) {
|
|
689
|
+
outline: 2px solid var(--ui-highlight);
|
|
690
|
+
outline-offset: 2px;
|
|
691
|
+
}
|
|
692
|
+
|
|
693
|
+
/* Pill each option row so Default/Image match the swatch trigger's weight.
|
|
694
|
+
One step below the toolbar's --ui-surface-low so option chrome stays visible
|
|
695
|
+
against the panel. */
|
|
696
|
+
.canvas-toolbar :global(.backdrop-option) {
|
|
697
|
+
padding: 0 var(--ui-space-8);
|
|
698
|
+
background: var(--ui-surface-lowest);
|
|
699
|
+
border: 1px solid var(--ui-border-low);
|
|
700
|
+
border-radius: var(--ui-radius-md);
|
|
701
|
+
color: var(--ui-text-secondary);
|
|
702
|
+
}
|
|
703
|
+
.canvas-toolbar :global(.backdrop-option.checked) {
|
|
704
|
+
background: var(--ui-surface-high);
|
|
705
|
+
border-color: var(--ui-border);
|
|
706
|
+
color: var(--ui-text-primary);
|
|
707
|
+
}
|
|
708
|
+
|
|
709
|
+
/* Drop the swatch trigger's frame to avoid a double-border inside the option pill. */
|
|
710
|
+
.canvas-toolbar :global(.backdrop-option .ui-ts-trigger) {
|
|
711
|
+
background: transparent;
|
|
712
|
+
border-color: transparent;
|
|
713
|
+
padding-left: 0;
|
|
714
|
+
padding-right: 0;
|
|
715
|
+
}
|
|
716
|
+
.canvas-toolbar :global(.backdrop-option .ui-ts-trigger:hover) {
|
|
717
|
+
background: var(--ui-hover);
|
|
718
|
+
border-color: transparent;
|
|
719
|
+
}
|
|
720
|
+
|
|
721
|
+
.canvas-toolbar :global(.ui-ts-meta-text) {
|
|
722
|
+
color: var(--ui-text-tertiary);
|
|
723
|
+
}
|
|
724
|
+
|
|
725
|
+
/* Anchor dropdown to the trigger's right edge; the toolbar sits flush against
|
|
726
|
+
the editor's right gutter, so a left-anchored dropdown would clip. */
|
|
727
|
+
.canvas-toolbar :global(.ui-ts-dropdown) {
|
|
728
|
+
left: auto;
|
|
729
|
+
right: 0;
|
|
730
|
+
}
|
|
731
|
+
|
|
732
|
+
.variant-tabs {
|
|
733
|
+
display: inline-flex;
|
|
734
|
+
flex-wrap: wrap;
|
|
735
|
+
gap: var(--ui-space-4);
|
|
736
|
+
padding: var(--ui-space-4);
|
|
737
|
+
background: var(--ui-surface-lowest);
|
|
738
|
+
border: 1px solid var(--ui-border-low);
|
|
739
|
+
border-radius: var(--ui-radius-md);
|
|
740
|
+
}
|
|
741
|
+
|
|
742
|
+
.variant-tab-btn {
|
|
743
|
+
padding: var(--ui-space-6) var(--ui-space-12);
|
|
744
|
+
background: none;
|
|
745
|
+
border: none;
|
|
746
|
+
border-radius: var(--ui-radius-sm);
|
|
747
|
+
color: var(--ui-text-secondary);
|
|
748
|
+
font-size: var(--ui-font-size-md);
|
|
749
|
+
font-weight: var(--ui-font-weight-semibold);
|
|
750
|
+
text-transform: capitalize;
|
|
751
|
+
cursor: pointer;
|
|
752
|
+
transition: color var(--ui-transition-fast), background var(--ui-transition-fast);
|
|
753
|
+
}
|
|
754
|
+
|
|
755
|
+
.variant-tab-btn:hover:not(.active) {
|
|
756
|
+
color: var(--ui-text-primary);
|
|
757
|
+
background: var(--ui-hover);
|
|
758
|
+
}
|
|
759
|
+
|
|
760
|
+
.variant-tab-btn.active {
|
|
761
|
+
color: var(--ui-text-primary);
|
|
762
|
+
background: var(--ui-surface-high);
|
|
763
|
+
box-shadow: 0 0 0 1px var(--ui-border);
|
|
764
|
+
}
|
|
765
|
+
|
|
766
|
+
.tabs-states-block {
|
|
767
|
+
display: flex;
|
|
768
|
+
flex-direction: column;
|
|
769
|
+
gap: var(--ui-space-8);
|
|
770
|
+
}
|
|
771
|
+
|
|
772
|
+
.tabs-selectors {
|
|
773
|
+
display: flex;
|
|
774
|
+
align-items: center;
|
|
775
|
+
gap: var(--ui-space-12);
|
|
776
|
+
}
|
|
777
|
+
|
|
778
|
+
/* Sub-strip sits flush under the parts strip when a part has interaction states.
|
|
779
|
+
The eyebrow label distinguishes it from the parts row above. */
|
|
780
|
+
.tabs-selectors.substrip {
|
|
781
|
+
margin-top: var(--ui-space-6);
|
|
782
|
+
}
|
|
783
|
+
.tabs-selectors.substrip .state-eyebrow {
|
|
784
|
+
color: var(--ui-text-tertiary);
|
|
785
|
+
font-size: var(--ui-font-size-xs);
|
|
786
|
+
min-width: 2.5rem;
|
|
787
|
+
}
|
|
788
|
+
|
|
789
|
+
.state-tabs {
|
|
790
|
+
display: inline-flex;
|
|
791
|
+
flex-wrap: wrap;
|
|
792
|
+
gap: var(--ui-space-4);
|
|
793
|
+
padding: var(--ui-space-4);
|
|
794
|
+
background: var(--ui-surface-lowest);
|
|
795
|
+
border: 1px solid var(--ui-border-low);
|
|
796
|
+
border-radius: var(--ui-radius-md);
|
|
797
|
+
}
|
|
798
|
+
|
|
799
|
+
.state-tab-btn {
|
|
800
|
+
padding: var(--ui-space-6) var(--ui-space-12);
|
|
801
|
+
background: none;
|
|
802
|
+
border: none;
|
|
803
|
+
border-radius: var(--ui-radius-sm);
|
|
804
|
+
color: var(--ui-text-secondary);
|
|
805
|
+
font-size: var(--ui-font-size-sm);
|
|
806
|
+
font-weight: var(--ui-font-weight-medium);
|
|
807
|
+
text-transform: capitalize;
|
|
808
|
+
cursor: pointer;
|
|
809
|
+
transition: color var(--ui-transition-fast), background var(--ui-transition-fast);
|
|
810
|
+
}
|
|
811
|
+
|
|
812
|
+
.state-tab-btn:hover:not(.active) {
|
|
813
|
+
color: var(--ui-text-primary);
|
|
814
|
+
background: var(--ui-hover);
|
|
815
|
+
}
|
|
816
|
+
|
|
817
|
+
.state-tab-btn.active {
|
|
818
|
+
color: var(--ui-text-primary);
|
|
819
|
+
background: var(--ui-surface-high);
|
|
820
|
+
box-shadow: 0 0 0 1px var(--ui-border);
|
|
821
|
+
}
|
|
822
|
+
|
|
823
|
+
/* Direct child of .variant-group; needs its own margin since flex gap doesn't apply across siblings. */
|
|
824
|
+
.properties-header {
|
|
825
|
+
display: flex;
|
|
826
|
+
align-items: center;
|
|
827
|
+
gap: var(--ui-space-16);
|
|
828
|
+
margin-top: var(--ui-space-8);
|
|
829
|
+
margin-bottom: var(--ui-space-8);
|
|
830
|
+
}
|
|
831
|
+
|
|
832
|
+
/* Tighten the title's line-height so its line-box matches its visual glyph
|
|
833
|
+
height — otherwise the inherited body line-height makes the heading sit
|
|
834
|
+
visually above the smaller CopyFromMenu trigger even with align-items:center. */
|
|
835
|
+
.properties-header .properties-title {
|
|
836
|
+
line-height: 1;
|
|
837
|
+
}
|
|
838
|
+
|
|
839
|
+
.extra-property-rows {
|
|
840
|
+
display: flex;
|
|
841
|
+
flex-direction: column;
|
|
842
|
+
gap: var(--ui-space-6);
|
|
843
|
+
}
|
|
844
|
+
|
|
845
|
+
.extra-property-rows :global(.property-row) {
|
|
846
|
+
display: grid;
|
|
847
|
+
grid-template-columns: minmax(8rem, max-content) 1fr;
|
|
848
|
+
column-gap: var(--ui-space-16);
|
|
849
|
+
align-items: center;
|
|
850
|
+
min-height: 1.75rem;
|
|
851
|
+
}
|
|
852
|
+
|
|
853
|
+
.extra-property-rows :global(.property-row > .property-label) {
|
|
854
|
+
font-size: var(--ui-font-size-sm);
|
|
855
|
+
color: var(--ui-text-secondary);
|
|
856
|
+
}
|
|
857
|
+
|
|
858
|
+
</style>
|