@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
package/src/{component-editor → editor/component-editor}/scaffolding/buildTypeGroupTokens.ts
RENAMED
|
@@ -7,6 +7,7 @@ export const TYPE_FONT_PROPS = [
|
|
|
7
7
|
{ key: 'sizeVariable', label: 'font size', defaultGroupKey: 'font-size' },
|
|
8
8
|
{ key: 'weightVariable', label: 'font weight', defaultGroupKey: 'font-weight' },
|
|
9
9
|
{ key: 'lineHeightVariable', label: 'line height', defaultGroupKey: 'line-height' },
|
|
10
|
+
{ key: 'letterSpacingVariable', label: 'letter spacing', defaultGroupKey: 'letter-spacing' },
|
|
10
11
|
] as const satisfies ReadonlyArray<{
|
|
11
12
|
key: keyof TypeGroupConfig;
|
|
12
13
|
label: string;
|
|
@@ -3,37 +3,47 @@ import { writable, type Writable, type Readable } from 'svelte/store';
|
|
|
3
3
|
|
|
4
4
|
const KEY = Symbol('editor-context');
|
|
5
5
|
|
|
6
|
+
export type VariantOption = { value: string; label: string };
|
|
7
|
+
|
|
6
8
|
export type EditorContext = {
|
|
7
|
-
/** Per-variable rank
|
|
9
|
+
/** Per-variable rank for TokenLayout row alignment; null when no linked block. */
|
|
8
10
|
linkedOrder: Readable<Map<string, number> | null>;
|
|
9
|
-
/**
|
|
11
|
+
/** Focused preview variant when siblings exist. */
|
|
10
12
|
focusedVariant: Writable<string | null>;
|
|
11
|
-
/**
|
|
12
|
-
|
|
13
|
-
|
|
13
|
+
/** Variants for this editor; empty when single-variant. Focused VariantGroup renders the tab strip itself. */
|
|
14
|
+
variants: Readable<VariantOption[]>;
|
|
15
|
+
/** Cross-group state-tab hint; VariantGroups whose stateNames contain it adopt as activeTab. */
|
|
14
16
|
focusedState: Writable<string | null>;
|
|
15
|
-
/**
|
|
16
|
-
Linked-properties block. Bidirectional cue: a hover in one surface lights up
|
|
17
|
-
the matching row in the other so the user can see the linkage at a glance. */
|
|
17
|
+
/** Hovered variable in Properties grid or Linked block; bidirectional highlight. */
|
|
18
18
|
hoveredLinkedVariable: Writable<string | null>;
|
|
19
|
+
/** Copy-from "Preserve color families" toggle, scoped to this editor. Lives
|
|
20
|
+
on context (not local to CopyFromMenu) so it survives VariantGroup remounts
|
|
21
|
+
when the user changes which variant is focused. */
|
|
22
|
+
preserveColorFamily: Writable<boolean>;
|
|
19
23
|
};
|
|
20
24
|
|
|
21
|
-
/**
|
|
25
|
+
/** Mutable handle for ComponentEditorBase. */
|
|
22
26
|
export type EditorContextInternal = EditorContext & {
|
|
23
27
|
_linkedOrder: Writable<Map<string, number> | null>;
|
|
28
|
+
_variants: Writable<VariantOption[]>;
|
|
24
29
|
};
|
|
25
30
|
|
|
26
31
|
export function createEditorContext(): EditorContextInternal {
|
|
27
32
|
const _linkedOrder = writable<Map<string, number> | null>(null);
|
|
33
|
+
const _variants = writable<VariantOption[]>([]);
|
|
28
34
|
const focusedVariant = writable<string | null>(null);
|
|
29
35
|
const focusedState = writable<string | null>(null);
|
|
30
36
|
const hoveredLinkedVariable = writable<string | null>(null);
|
|
37
|
+
const preserveColorFamily = writable<boolean>(false);
|
|
31
38
|
const ctx: EditorContextInternal = {
|
|
32
39
|
linkedOrder: _linkedOrder,
|
|
33
40
|
focusedVariant,
|
|
41
|
+
variants: _variants,
|
|
34
42
|
focusedState,
|
|
35
43
|
hoveredLinkedVariable,
|
|
44
|
+
preserveColorFamily,
|
|
36
45
|
_linkedOrder,
|
|
46
|
+
_variants,
|
|
37
47
|
};
|
|
38
48
|
setContext(KEY, ctx);
|
|
39
49
|
return ctx;
|
|
@@ -3,8 +3,8 @@ import {
|
|
|
3
3
|
isComponentPropertyLinked,
|
|
4
4
|
getComponentPropertySiblings,
|
|
5
5
|
editorState,
|
|
6
|
-
} from '../../
|
|
7
|
-
import type { CssVarRef } from '../../
|
|
6
|
+
} from '../../core/store/editorStore';
|
|
7
|
+
import type { CssVarRef } from '../../core/store/editorTypes';
|
|
8
8
|
import type { Token } from './types';
|
|
9
9
|
|
|
10
10
|
function aliasKey(ref: CssVarRef | undefined): string {
|
|
@@ -13,6 +13,26 @@ export type Token = {
|
|
|
13
13
|
/** When the linked block collapses several same-label same-value rows into one,
|
|
14
14
|
the surviving row carries the other groupKey leads here so writes co-propagate. */
|
|
15
15
|
mergeVariables?: string[];
|
|
16
|
+
/** When false, a padding-shaped token renders only the single-value control
|
|
17
|
+
(no split-to-sides affordance). For tokens consumed via a one-axis CSS
|
|
18
|
+
property like `padding-bottom: var(--x)`, splitting yields side values
|
|
19
|
+
that have nowhere to render — hide the toggle so users can't get into
|
|
20
|
+
that state. Defaults to true for padding-shaped tokens. */
|
|
21
|
+
splittable?: boolean;
|
|
22
|
+
/** Optional element grouping (e.g. 'frame', 'header', 'body'). When a state
|
|
23
|
+
has tokens or type-groups tagged with two or more distinct elements,
|
|
24
|
+
StateBlock partitions the panel into labeled subsections — typography
|
|
25
|
+
and properties for each element render together. */
|
|
26
|
+
element?: string;
|
|
27
|
+
/** Hint to the editor that this token's alias is a structured payload
|
|
28
|
+
(currently only `kind: 'gradient'`). Drives Copy-from's per-kind
|
|
29
|
+
branch — gradient aliases need family-swap of in-family stop colors
|
|
30
|
+
rather than a verbatim ref copy. */
|
|
31
|
+
kind?: 'gradient';
|
|
32
|
+
/** Color-family slug for this token's owning variant (e.g. `brand`,
|
|
33
|
+
`accent`). Set on gradient-kind tokens so Copy-from's family-swap
|
|
34
|
+
can compute the src→dst family substitution. */
|
|
35
|
+
family?: string;
|
|
16
36
|
};
|
|
17
37
|
|
|
18
38
|
/** Editor type-group: a fieldset containing a coordinated set of typography tokens
|
|
@@ -32,8 +52,13 @@ export type TypeGroupConfig = {
|
|
|
32
52
|
weightLabel?: string;
|
|
33
53
|
lineHeightVariable?: string;
|
|
34
54
|
lineHeightLabel?: string;
|
|
55
|
+
letterSpacingVariable?: string;
|
|
56
|
+
letterSpacingLabel?: string;
|
|
35
57
|
outlineWidthVariable?: string;
|
|
36
58
|
outlineWidthLabel?: string;
|
|
37
59
|
outlineColorVariable?: string;
|
|
38
60
|
outlineColorLabel?: string;
|
|
61
|
+
/** See `Token.element` — when present, StateBlock groups this fieldset under
|
|
62
|
+
the matching element subsection. */
|
|
63
|
+
element?: string;
|
|
39
64
|
};
|
|
@@ -16,4 +16,12 @@
|
|
|
16
16
|
export const KNOWN_COMPONENT_CONFIG_KEYS: ReadonlySet<string> = new Set([
|
|
17
17
|
'--dialog-confirm-variant',
|
|
18
18
|
'--dialog-cancel-variant',
|
|
19
|
+
// SectionDivider per-variant `color-family` is editor metadata that drives
|
|
20
|
+
// the family-swap rewrite on aliases. It is not a runtime CSS value, so it
|
|
21
|
+
// stays in the config bucket. The other intrinsics (align, hairline,
|
|
22
|
+
// eyebrow/description visibility, eyebrow text-transform) now flow through
|
|
23
|
+
// the alias bucket as cascading CSS vars — see the 2026-05-22 migration.
|
|
24
|
+
'--sectiondivider-lg-color-family',
|
|
25
|
+
'--sectiondivider-md-color-family',
|
|
26
|
+
'--sectiondivider-sm-color-family',
|
|
19
27
|
]);
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import type { ComponentConfig, ComponentConfigMeta } from '
|
|
2
|
-
import { versionedFileResource } from '
|
|
1
|
+
import type { AliasDiskValue, ComponentConfig, ComponentConfigMeta } from '../themes/themeTypes';
|
|
2
|
+
import { versionedFileResource } from '../storage/files/versionedFileResourceClient';
|
|
3
3
|
|
|
4
4
|
/**
|
|
5
5
|
* REST client for per-component config files. Parallel to `themeService.ts`
|
|
@@ -23,7 +23,7 @@ export interface ComponentSummary {
|
|
|
23
23
|
export interface ComponentProductionInfo {
|
|
24
24
|
fileName: string;
|
|
25
25
|
name: string;
|
|
26
|
-
aliases: Record<string,
|
|
26
|
+
aliases: Record<string, AliasDiskValue>;
|
|
27
27
|
}
|
|
28
28
|
|
|
29
29
|
export interface ComponentConfigList {
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { get } from 'svelte/store';
|
|
2
|
-
import type { ComponentConfig } from '
|
|
3
|
-
import { editorState, markComponentSaved } from '
|
|
4
|
-
import type { CssVarRef } from '
|
|
5
|
-
import { CURRENT_COMPONENT_SCHEMA_VERSION } from '
|
|
2
|
+
import type { AliasDiskValue, ComponentConfig } from '../themes/themeTypes';
|
|
3
|
+
import { editorState, markComponentSaved } from '../store/editorStore';
|
|
4
|
+
import type { CssVarRef } from '../store/editorTypes';
|
|
5
|
+
import { CURRENT_COMPONENT_SCHEMA_VERSION } from '../themes/migrations';
|
|
6
6
|
import {
|
|
7
7
|
listComponentConfigs,
|
|
8
8
|
saveComponentConfig,
|
|
@@ -12,7 +12,7 @@ import {
|
|
|
12
12
|
/**
|
|
13
13
|
* Save the current in-memory state of a component to its active file. Mirrors
|
|
14
14
|
* the `persist` flow inside `ComponentFileManager.svelte` so callers without a
|
|
15
|
-
* file-manager instance (e.g.
|
|
15
|
+
* file-manager instance (e.g. an unsaved-components dialog upstream)
|
|
16
16
|
* can save a dirty component without duplicating the schema-version + aliases
|
|
17
17
|
* stringification logic.
|
|
18
18
|
*
|
|
@@ -24,8 +24,10 @@ export type SaveActiveComponentResult =
|
|
|
24
24
|
| { ok: true; fileName: string; displayName: string }
|
|
25
25
|
| { ok: false; reason: 'default' | 'no-state' | 'error'; error?: unknown };
|
|
26
26
|
|
|
27
|
-
function
|
|
28
|
-
|
|
27
|
+
function refToDiskValue(ref: CssVarRef): AliasDiskValue {
|
|
28
|
+
if (ref.kind === 'token') return ref.name;
|
|
29
|
+
if (ref.kind === 'literal') return ref.value;
|
|
30
|
+
return { kind: 'gradient', value: ref.value };
|
|
29
31
|
}
|
|
30
32
|
|
|
31
33
|
export async function saveActiveComponentConfig(
|
|
@@ -43,8 +45,8 @@ export async function saveActiveComponentConfig(
|
|
|
43
45
|
const displayName = active?.name ?? fileName;
|
|
44
46
|
|
|
45
47
|
const now = new Date().toISOString();
|
|
46
|
-
const aliases: Record<string,
|
|
47
|
-
for (const [k, ref] of Object.entries(slice.aliases)) aliases[k] =
|
|
48
|
+
const aliases: Record<string, AliasDiskValue> = {};
|
|
49
|
+
for (const [k, ref] of Object.entries(slice.aliases)) aliases[k] = refToDiskValue(ref);
|
|
48
50
|
|
|
49
51
|
const data: ComponentConfig = {
|
|
50
52
|
name: displayName,
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Centralised "flash a transient status, then revert to idle" helper for the
|
|
3
|
+
* file managers (theme / component / manifest). Each pulse — saved, error,
|
|
4
|
+
* adopted — shares the same timing so the UI stays in sync.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
export const FLASH_DURATION_MS = 2000;
|
|
8
|
+
|
|
9
|
+
export interface FlashOptions<S extends string> {
|
|
10
|
+
/** State to revert to after the pulse. Defaults to `'idle'`. */
|
|
11
|
+
idleState?: S;
|
|
12
|
+
/** How long the transient state is shown before reverting. */
|
|
13
|
+
durationMs?: number;
|
|
14
|
+
/** Side effect fired alongside the revert (e.g. clear an inline message). */
|
|
15
|
+
onIdle?: () => void;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function flashStatus<S extends string>(
|
|
19
|
+
set: (state: S) => void,
|
|
20
|
+
transientState: S,
|
|
21
|
+
options: FlashOptions<S> = {},
|
|
22
|
+
): void {
|
|
23
|
+
const idleState = options.idleState ?? ('idle' as S);
|
|
24
|
+
const durationMs = options.durationMs ?? FLASH_DURATION_MS;
|
|
25
|
+
set(transientState);
|
|
26
|
+
setTimeout(() => {
|
|
27
|
+
set(idleState);
|
|
28
|
+
options.onIdle?.();
|
|
29
|
+
}, durationMs);
|
|
30
|
+
}
|
|
@@ -5,8 +5,8 @@ import type {
|
|
|
5
5
|
FontStackSlot,
|
|
6
6
|
FontStackVariable,
|
|
7
7
|
SystemCascadePreset,
|
|
8
|
-
} from '
|
|
9
|
-
import { setCssVar, getSyncedDocuments } from '
|
|
8
|
+
} from '../themes/themeTypes';
|
|
9
|
+
import { setCssVar, getSyncedDocuments } from '../cssVarSync';
|
|
10
10
|
|
|
11
11
|
export const SYSTEM_CASCADES: Record<SystemCascadePreset, string> = {
|
|
12
12
|
'system-ui-sans':
|
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
import type { FontFamily, FontSource, FontStack, Theme } from '
|
|
2
|
-
import frauncesRomanLatin from '
|
|
3
|
-
import frauncesItalicLatin from '
|
|
4
|
-
import manropeLatin from '
|
|
1
|
+
import type { FontFamily, FontSource, FontStack, Theme } from '../themes/themeTypes';
|
|
2
|
+
import frauncesRomanLatin from '../../../system/styles/fonts/Fraunces/Fraunces-roman-latin.woff2?url';
|
|
3
|
+
import frauncesItalicLatin from '../../../system/styles/fonts/Fraunces/Fraunces-italic-latin.woff2?url';
|
|
4
|
+
import manropeLatin from '../../../system/styles/fonts/Manrope/Manrope-latin.woff2?url';
|
|
5
5
|
|
|
6
6
|
function makeId(prefix: string): string {
|
|
7
7
|
return `${prefix}_${Math.random().toString(36).slice(2, 10)}`;
|
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
import type { Manifest, ManifestMeta, ManifestBundle, Theme, ComponentConfig } from '../themes/themeTypes';
|
|
2
|
+
import { versionedFileResource } from '../storage/files/versionedFileResourceClient';
|
|
3
|
+
import { listComponents } from '../components/componentConfigService';
|
|
4
|
+
import { getActiveTheme } from '../themes/themeService';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* REST client for manifest files. A manifest references one theme file +
|
|
8
|
+
* one config file per component (by basename). The active manifest is the
|
|
9
|
+
* single live snapshot: theme and component Adopts patch its refs server-side.
|
|
10
|
+
*
|
|
11
|
+
* `default` is the protected baseline — cannot be overwritten or deleted, and
|
|
12
|
+
* Adopts return 409 ACTIVE_IS_PROTECTED while it is active.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
const manifestsResource = versionedFileResource<Manifest, ManifestMeta, never>({
|
|
16
|
+
baseUrl: '/api/manifests',
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
export const listManifests = async (): Promise<ManifestMeta[]> => {
|
|
20
|
+
const data = await manifestsResource.list();
|
|
21
|
+
return data.files;
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
export const loadManifest = (fileName: string): Promise<Manifest> =>
|
|
25
|
+
manifestsResource.load(fileName);
|
|
26
|
+
export const saveManifest = (fileName: string, data: Manifest): Promise<void> =>
|
|
27
|
+
manifestsResource.save(fileName, data);
|
|
28
|
+
export const deleteManifest = (fileName: string): Promise<void> =>
|
|
29
|
+
manifestsResource.remove(fileName);
|
|
30
|
+
export const getActiveManifest = (): Promise<Manifest | null> => manifestsResource.getActive();
|
|
31
|
+
export const setActiveManifest = (fileName: string): Promise<void> =>
|
|
32
|
+
manifestsResource.setActive(fileName);
|
|
33
|
+
|
|
34
|
+
export interface ApplyManifestResult {
|
|
35
|
+
ok: boolean;
|
|
36
|
+
manifest: Manifest;
|
|
37
|
+
theme: Theme;
|
|
38
|
+
componentConfigs: Record<string, ComponentConfig>;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Server-side atomic apply: validate every referenced file exists, flip the
|
|
43
|
+
* theme + each component's `_active.json` and `_production.json` pointers,
|
|
44
|
+
* sync tokens.css/fonts.css from the new theme, mark the manifest active, and
|
|
45
|
+
* return the resolved theme + component configs in one payload. Clients
|
|
46
|
+
* usually follow with a full page reload — manifest load is a "blow up the
|
|
47
|
+
* world" action.
|
|
48
|
+
*/
|
|
49
|
+
export async function applyManifest(fileName: string): Promise<ApplyManifestResult> {
|
|
50
|
+
const res = await fetch(`/api/manifests/${encodeURIComponent(fileName)}/apply`, {
|
|
51
|
+
method: 'PUT',
|
|
52
|
+
});
|
|
53
|
+
if (!res.ok) {
|
|
54
|
+
const err = await res.json().catch(() => ({ error: 'Apply failed' }));
|
|
55
|
+
throw new Error(err.error || 'Apply failed');
|
|
56
|
+
}
|
|
57
|
+
return res.json();
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Snapshot the currently-active theme + component-config file pointers into
|
|
62
|
+
* a new manifest file and set it active. Used by the manifest panel's Save As
|
|
63
|
+
* action and by the SaveAs-then-Adopt recovery flow when active is `default`.
|
|
64
|
+
*/
|
|
65
|
+
export async function saveAsManifest(
|
|
66
|
+
fileName: string,
|
|
67
|
+
displayName: string,
|
|
68
|
+
): Promise<void> {
|
|
69
|
+
const activeTheme = await getActiveTheme();
|
|
70
|
+
if (!activeTheme || !activeTheme._fileName) {
|
|
71
|
+
throw new Error('No active theme on disk to capture');
|
|
72
|
+
}
|
|
73
|
+
const components = await listComponents();
|
|
74
|
+
const componentConfigs: Record<string, string> = {};
|
|
75
|
+
for (const c of components) {
|
|
76
|
+
componentConfigs[c.name] = c.activeFile || 'default';
|
|
77
|
+
}
|
|
78
|
+
const now = new Date().toISOString();
|
|
79
|
+
const manifest: Manifest = {
|
|
80
|
+
name: displayName,
|
|
81
|
+
createdAt: now,
|
|
82
|
+
updatedAt: now,
|
|
83
|
+
theme: activeTheme._fileName,
|
|
84
|
+
componentConfigs,
|
|
85
|
+
};
|
|
86
|
+
await saveManifest(fileName, manifest);
|
|
87
|
+
await setActiveManifest(fileName);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Re-snapshot the editor's current active pointers into the *currently active*
|
|
92
|
+
* manifest file. Used by the manifest panel's Save action. Server rejects with
|
|
93
|
+
* 403 if active is `default` (protected).
|
|
94
|
+
*/
|
|
95
|
+
export async function saveActiveManifest(displayName?: string): Promise<void> {
|
|
96
|
+
const active = await getActiveManifest();
|
|
97
|
+
if (!active || !active._fileName) {
|
|
98
|
+
throw new Error('No active manifest');
|
|
99
|
+
}
|
|
100
|
+
const activeTheme = await getActiveTheme();
|
|
101
|
+
if (!activeTheme || !activeTheme._fileName) {
|
|
102
|
+
throw new Error('No active theme on disk');
|
|
103
|
+
}
|
|
104
|
+
const components = await listComponents();
|
|
105
|
+
const componentConfigs: Record<string, string> = {};
|
|
106
|
+
for (const c of components) {
|
|
107
|
+
componentConfigs[c.name] = c.activeFile || 'default';
|
|
108
|
+
}
|
|
109
|
+
const manifest: Manifest = {
|
|
110
|
+
name: displayName ?? active.name,
|
|
111
|
+
createdAt: active.createdAt,
|
|
112
|
+
updatedAt: new Date().toISOString(),
|
|
113
|
+
theme: activeTheme._fileName,
|
|
114
|
+
componentConfigs,
|
|
115
|
+
};
|
|
116
|
+
await saveManifest(active._fileName, manifest);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
export interface ImportManifestResult {
|
|
120
|
+
ok: boolean;
|
|
121
|
+
/** Final manifest filename (may be renamed if it collided with an existing one). */
|
|
122
|
+
manifest: string;
|
|
123
|
+
/** Keyed `theme:<orig>` / `componentConfig:<comp>/<orig>` / `manifest:<orig>` → final name. */
|
|
124
|
+
renames: Record<string, string>;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* Fetch the manifest as a self-contained `ManifestBundle` and trigger a
|
|
129
|
+
* browser download. Hidden-anchor trick — no infrastructure beyond the
|
|
130
|
+
* existing GET `/api/manifests/:name/export` endpoint.
|
|
131
|
+
*
|
|
132
|
+
* See temp/manifest-robustness-plan.md §11.
|
|
133
|
+
*/
|
|
134
|
+
export async function exportManifest(fileName: string): Promise<void> {
|
|
135
|
+
const res = await fetch(`/api/manifests/${encodeURIComponent(fileName)}/export`);
|
|
136
|
+
if (!res.ok) {
|
|
137
|
+
const err = await res.json().catch(() => ({ error: 'Export failed' }));
|
|
138
|
+
throw new Error(err.error || 'Export failed');
|
|
139
|
+
}
|
|
140
|
+
const blob = await res.blob();
|
|
141
|
+
const url = URL.createObjectURL(blob);
|
|
142
|
+
try {
|
|
143
|
+
const a = document.createElement('a');
|
|
144
|
+
a.href = url;
|
|
145
|
+
a.download = `${fileName}.bundle.json`;
|
|
146
|
+
document.body.appendChild(a);
|
|
147
|
+
a.click();
|
|
148
|
+
a.remove();
|
|
149
|
+
} finally {
|
|
150
|
+
URL.revokeObjectURL(url);
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/**
|
|
155
|
+
* POST a `ManifestBundle` to the import endpoint. Server materialises the
|
|
156
|
+
* inlined theme + component configs as fresh files (renaming on collision),
|
|
157
|
+
* rewrites the manifest's pointers, and returns the final manifest name plus
|
|
158
|
+
* the rename map so the UI can surface what got renamed.
|
|
159
|
+
*/
|
|
160
|
+
export async function importManifest(bundle: ManifestBundle): Promise<ImportManifestResult> {
|
|
161
|
+
const res = await fetch('/api/manifests/import', {
|
|
162
|
+
method: 'POST',
|
|
163
|
+
headers: { 'Content-Type': 'application/json' },
|
|
164
|
+
body: JSON.stringify(bundle),
|
|
165
|
+
});
|
|
166
|
+
if (!res.ok) {
|
|
167
|
+
const err = await res.json().catch(() => ({ error: 'Import failed' }));
|
|
168
|
+
throw new Error(err.error || 'Import failed');
|
|
169
|
+
}
|
|
170
|
+
return res.json();
|
|
171
|
+
}
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Family-aware token rewrites.
|
|
3
|
+
*
|
|
4
|
+
* Color tokens encode their palette family as a hyphen segment in the slug
|
|
5
|
+
* (e.g. `--border-canvas-strong` → `canvas`, `--surface-accent-low` →
|
|
6
|
+
* `accent`). Text tokens are special-cased: `--text-primary` carries no
|
|
7
|
+
* family, `--text-{family}` and `--text-{family}-{step}` do, and `--text-{step}`
|
|
8
|
+
* resolves to the neutral text scale.
|
|
9
|
+
*
|
|
10
|
+
* These helpers let editor flows (variant family swap, per-stop monochrome
|
|
11
|
+
* snap) rewrite a token slug without re-implementing the parser each time.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
export const KNOWN_FAMILIES = [
|
|
15
|
+
'neutral', 'alternate', 'canvas', 'brand',
|
|
16
|
+
'accent', 'special', 'success', 'warning', 'info', 'danger',
|
|
17
|
+
] as const;
|
|
18
|
+
|
|
19
|
+
export type FamilyName = (typeof KNOWN_FAMILIES)[number];
|
|
20
|
+
|
|
21
|
+
export const TEXT_STEPS = ['primary', 'secondary', 'tertiary', 'muted', 'disabled'] as const;
|
|
22
|
+
|
|
23
|
+
export type TextStep = (typeof TEXT_STEPS)[number];
|
|
24
|
+
|
|
25
|
+
/** Parse a text-scale token name into family + step. Returns null for any
|
|
26
|
+
* non-`--text-*` slug or one whose parts don't resolve to a known step. */
|
|
27
|
+
export function parseTextToken(colorRef: string): { family: FamilyName; step: TextStep } | null {
|
|
28
|
+
if (!colorRef.startsWith('--text-')) return null;
|
|
29
|
+
const rest = colorRef.slice('--text-'.length);
|
|
30
|
+
const parts = rest.split('-');
|
|
31
|
+
if (parts.length === 1) {
|
|
32
|
+
const p = parts[0];
|
|
33
|
+
if ((TEXT_STEPS as readonly string[]).includes(p)) return { family: 'neutral', step: p as TextStep };
|
|
34
|
+
if ((KNOWN_FAMILIES as readonly string[]).includes(p)) return { family: p as FamilyName, step: 'primary' };
|
|
35
|
+
return null;
|
|
36
|
+
}
|
|
37
|
+
if (parts.length === 2) {
|
|
38
|
+
const [fam, step] = parts;
|
|
39
|
+
if (
|
|
40
|
+
(KNOWN_FAMILIES as readonly string[]).includes(fam)
|
|
41
|
+
&& (TEXT_STEPS as readonly string[]).includes(step)
|
|
42
|
+
) {
|
|
43
|
+
return { family: fam as FamilyName, step: step as TextStep };
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
return null;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/** Inverse of parseTextToken — assemble a `--text-*` slug from family + step.
|
|
50
|
+
* `neutral` family flattens to `--text-{step}`; `primary` step on a named
|
|
51
|
+
* family flattens to `--text-{family}` (matches authoring conventions). */
|
|
52
|
+
export function buildTextToken(family: FamilyName | string, step: TextStep | string): string {
|
|
53
|
+
if (family === 'neutral') return `--text-${step}`;
|
|
54
|
+
return step === 'primary' ? `--text-${family}` : `--text-${family}-${step}`;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/** Pull the family marker out of a token slug, or null if none of the
|
|
58
|
+
* hyphen-delimited parts is a known family. */
|
|
59
|
+
export function detectFamily(colorRef: string): FamilyName | null {
|
|
60
|
+
if (!colorRef.startsWith('--')) return null;
|
|
61
|
+
const parts = colorRef.slice(2).split('-');
|
|
62
|
+
for (const p of parts) {
|
|
63
|
+
if ((KNOWN_FAMILIES as readonly string[]).includes(p)) return p as FamilyName;
|
|
64
|
+
}
|
|
65
|
+
return null;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/** Rewrite the family marker in a slug from `oldFamily` to `newFamily`.
|
|
69
|
+
* Returns the input unchanged when the slug doesn't reference `oldFamily`
|
|
70
|
+
* (so unrelated tokens, literals, and out-of-family stops fall through). */
|
|
71
|
+
export function swapTokenFamily(name: string, oldFamily: string, newFamily: string): string {
|
|
72
|
+
if (!name.startsWith('--')) return name;
|
|
73
|
+
const text = parseTextToken(name);
|
|
74
|
+
if (text) {
|
|
75
|
+
if (text.family !== oldFamily) return name;
|
|
76
|
+
return buildTextToken(newFamily, text.step);
|
|
77
|
+
}
|
|
78
|
+
const parts = name.slice(2).split('-');
|
|
79
|
+
const idx = parts.indexOf(oldFamily);
|
|
80
|
+
if (idx < 0) return name;
|
|
81
|
+
parts[idx] = newFamily;
|
|
82
|
+
return '--' + parts.join('-');
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/** Coerce a slug to the given target family. Unlike swapTokenFamily this
|
|
86
|
+
* doesn't need to know the source family — it autodetects, then rewrites.
|
|
87
|
+
* Returns the input unchanged if no family is present (e.g. literal colors,
|
|
88
|
+
* `transparent`, or a `--text-primary` neutral). */
|
|
89
|
+
export function snapTokenToFamily(name: string, targetFamily: string): string {
|
|
90
|
+
if (!name.startsWith('--')) return name;
|
|
91
|
+
const text = parseTextToken(name);
|
|
92
|
+
if (text) {
|
|
93
|
+
if (text.family === targetFamily) return name;
|
|
94
|
+
return buildTextToken(targetFamily, text.step);
|
|
95
|
+
}
|
|
96
|
+
const fromFamily = detectFamily(name);
|
|
97
|
+
if (!fromFamily || fromFamily === targetFamily) return name;
|
|
98
|
+
return swapTokenFamily(name, fromFamily, targetFamily);
|
|
99
|
+
}
|
|
@@ -14,8 +14,8 @@
|
|
|
14
14
|
*/
|
|
15
15
|
|
|
16
16
|
import { hexToOklch, oklchToHex, gamutClamp } from './oklch';
|
|
17
|
-
import { type CurveAnchor, sampleCurve, makeAnchor } from '
|
|
18
|
-
import type { PaletteConfig } from '
|
|
17
|
+
import { type CurveAnchor, sampleCurve, makeAnchor } from '../../ui/curveEngine';
|
|
18
|
+
import type { PaletteConfig } from '../themes/themeTypes';
|
|
19
19
|
|
|
20
20
|
export type PaletteMode = 'chromatic' | 'gray';
|
|
21
21
|
|
|
@@ -298,3 +298,72 @@ export function palettesToVars(palettes: Record<string, PaletteConfig>): Record<
|
|
|
298
298
|
}
|
|
299
299
|
return out;
|
|
300
300
|
}
|
|
301
|
+
|
|
302
|
+
const HEX_RE = /^#[0-9a-f]{6}$/i;
|
|
303
|
+
|
|
304
|
+
/**
|
|
305
|
+
* Reconcile palette typed state against the catch-all `cssVariables` bag and
|
|
306
|
+
* report the set of variable names the typed slice now owns. Two operations:
|
|
307
|
+
*
|
|
308
|
+
* - **Snap** (gated by `_imported`): for any palette whose `_imported` flag
|
|
309
|
+
* is true, the imported `--color-{ns}-500` value is treated as the
|
|
310
|
+
* authoritative anchor. Chromatic palettes get `baseColor` snapped to it;
|
|
311
|
+
* gray palettes (Neutral/Alternate) get `tintHue` + `tintChroma` derived
|
|
312
|
+
* from it via OKLCH. The flag is then cleared. Editor-authored palettes
|
|
313
|
+
* (no `_imported`) are left untouched — see `temp/manifest-robustness-plan.md`
|
|
314
|
+
* §9 for why "snap on any divergence" was wrong: it would have flipped
|
|
315
|
+
* `themes/default.json`'s accent from teal to olive on first read.
|
|
316
|
+
*
|
|
317
|
+
* - **Consume** (always): every variable the palette's derivation produces
|
|
318
|
+
* is reported in `consumed` so the caller can strip it from
|
|
319
|
+
* `cssVariables`. The renderer (`editorRenderer.ts:42`) overlays
|
|
320
|
+
* `palettesToVars(palettes)` on top of `cssVariables` regardless, so
|
|
321
|
+
* stripped keys were dead data anyway. Stripping makes the file
|
|
322
|
+
* invariant explicit: catch-all carries only tokens no typed slice owns.
|
|
323
|
+
*
|
|
324
|
+
* Returns the updated palette map plus the two sets. Pure: no DOM, no I/O,
|
|
325
|
+
* no module state. Idempotent on second call with the same input (no anchor
|
|
326
|
+
* to snap to after first call's strip).
|
|
327
|
+
*/
|
|
328
|
+
export function reconcilePalettesFromCssVars(
|
|
329
|
+
palettes: Record<string, PaletteConfig>,
|
|
330
|
+
cssVars: Record<string, string>,
|
|
331
|
+
): {
|
|
332
|
+
palettes: Record<string, PaletteConfig>;
|
|
333
|
+
consumed: ReadonlySet<string>;
|
|
334
|
+
snapped: ReadonlySet<string>;
|
|
335
|
+
} {
|
|
336
|
+
const next: Record<string, PaletteConfig> = structuredClone(palettes);
|
|
337
|
+
const consumed = new Set<string>();
|
|
338
|
+
const snapped = new Set<string>();
|
|
339
|
+
|
|
340
|
+
for (const spec of PALETTE_SPECS) {
|
|
341
|
+
const current = next[spec.label];
|
|
342
|
+
if (current === undefined) continue;
|
|
343
|
+
|
|
344
|
+
if (current._imported === true) {
|
|
345
|
+
const anchorHex = cssVars[`--color-${spec.cssNamespace}-500`];
|
|
346
|
+
if (anchorHex && HEX_RE.test(anchorHex.trim())) {
|
|
347
|
+
const hex = anchorHex.trim();
|
|
348
|
+
if (spec.mode === 'gray') {
|
|
349
|
+
const { c, h } = hexToOklch(hex);
|
|
350
|
+
next[spec.label] = { ...current, tintHue: h, tintChroma: c, _imported: false };
|
|
351
|
+
} else {
|
|
352
|
+
next[spec.label] = { ...current, baseColor: hex, _imported: false };
|
|
353
|
+
}
|
|
354
|
+
snapped.add(spec.label);
|
|
355
|
+
} else {
|
|
356
|
+
// No anchor in cssVariables to snap to — flag has nothing to do; clear
|
|
357
|
+
// it so subsequent calls don't keep checking. Safe because the renderer
|
|
358
|
+
// is going to overlay palettesToVars anyway.
|
|
359
|
+
next[spec.label] = { ...current, _imported: false };
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
for (const k of Object.keys(derivePaletteVars(spec, next[spec.label]))) {
|
|
364
|
+
consumed.add(k);
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
return { palettes: next, consumed, snapped };
|
|
369
|
+
}
|
|
@@ -19,17 +19,18 @@
|
|
|
19
19
|
*/
|
|
20
20
|
|
|
21
21
|
import { derived, type Readable } from 'svelte/store';
|
|
22
|
-
import tokensCss from '
|
|
23
|
-
import { editorState } from '
|
|
24
|
-
import type { EditorState } from '
|
|
25
|
-
import { extractGlobalRootBody } from '
|
|
22
|
+
import tokensCss from '../../../system/styles/tokens.css?raw';
|
|
23
|
+
import { editorState } from '../store/editorStore';
|
|
24
|
+
import type { EditorState } from '../store/editorTypes';
|
|
25
|
+
import { extractGlobalRootBody } from '../themes/parsers/globalRootBlock';
|
|
26
|
+
import { formatGradientValue } from '../themes/slices/gradients';
|
|
26
27
|
|
|
27
28
|
// Re-exported for tests and downstream consumers that previously imported it
|
|
28
29
|
// from this module. The canonical implementation lives in `./parsers/globalRootBlock`
|
|
29
30
|
// so the dev-server vite plugin can share it.
|
|
30
31
|
export { extractGlobalRootBody };
|
|
31
32
|
|
|
32
|
-
const componentSources = import.meta.glob('
|
|
33
|
+
const componentSources = import.meta.glob('../../../system/components/*.svelte', {
|
|
33
34
|
query: '?raw',
|
|
34
35
|
import: 'default',
|
|
35
36
|
eager: true,
|
|
@@ -111,7 +112,9 @@ function buildOverlayRegistry(
|
|
|
111
112
|
const overrides = new Map<string, string>();
|
|
112
113
|
for (const slice of Object.values(components)) {
|
|
113
114
|
for (const [varName, ref] of Object.entries(slice.aliases)) {
|
|
114
|
-
|
|
115
|
+
if (ref.kind === 'token') overrides.set(varName, `var(${ref.name})`);
|
|
116
|
+
else if (ref.kind === 'literal') overrides.set(varName, ref.value);
|
|
117
|
+
else overrides.set(varName, formatGradientValue(ref.value));
|
|
115
118
|
}
|
|
116
119
|
}
|
|
117
120
|
const getDeclared = (v: string): string | null =>
|