@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,499 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import { flip } from 'svelte/animate';
|
|
3
|
+
import { cubicOut } from 'svelte/easing';
|
|
4
|
+
import type {
|
|
5
|
+
FontFamily,
|
|
6
|
+
FontSource,
|
|
7
|
+
FontStack,
|
|
8
|
+
FontStackSlot,
|
|
9
|
+
FontStackVariable,
|
|
10
|
+
GenericFamily,
|
|
11
|
+
SystemCascadePreset,
|
|
12
|
+
} from '../core/themes/themeTypes';
|
|
13
|
+
import { editorState, setFontStacks } from '../core/store/editorStore';
|
|
14
|
+
import { applyFontStacks, SYSTEM_CASCADES } from '../core/fonts/fontLoader';
|
|
15
|
+
|
|
16
|
+
const SYSTEM_PRESETS: SystemCascadePreset[] = ['system-ui-sans', 'system-ui-serif', 'system-ui-mono'];
|
|
17
|
+
// `cursive` and `fantasy` are CSS-spec generics whose rendering varies wildly
|
|
18
|
+
// across OSes (cursive → Comic Sans / Snell Roundhand; fantasy → Impact /
|
|
19
|
+
// Papyrus). They're rarely what a designer means by "fallback," so we don't
|
|
20
|
+
// offer them in the editor.
|
|
21
|
+
const GENERIC_VALUES: GenericFamily[] = ['sans-serif', 'serif', 'monospace'];
|
|
22
|
+
|
|
23
|
+
const STACK_VARIABLES: FontStackVariable[] = [
|
|
24
|
+
'--font-display',
|
|
25
|
+
'--font-sans',
|
|
26
|
+
'--font-serif',
|
|
27
|
+
'--font-mono',
|
|
28
|
+
];
|
|
29
|
+
|
|
30
|
+
// Each stack's terminal fallback (bottom slot). It's locked to system or
|
|
31
|
+
// generic so text always renders even if every project font 404s. The
|
|
32
|
+
// fallback maps by stack variable; chosen to match the stack's purpose.
|
|
33
|
+
const TERMINAL_FALLBACK_BY_VAR: Record<FontStackVariable, GenericFamily> = {
|
|
34
|
+
'--font-display': 'serif',
|
|
35
|
+
'--font-sans': 'sans-serif',
|
|
36
|
+
'--font-serif': 'serif',
|
|
37
|
+
'--font-mono': 'monospace',
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
// The single matching System UI preset paired with each stack — the only two
|
|
41
|
+
// options offered in the terminal slot's dropdown.
|
|
42
|
+
const TERMINAL_SYSTEM_BY_VAR: Record<FontStackVariable, SystemCascadePreset> = {
|
|
43
|
+
'--font-display': 'system-ui-serif',
|
|
44
|
+
'--font-sans': 'system-ui-sans',
|
|
45
|
+
'--font-serif': 'system-ui-serif',
|
|
46
|
+
'--font-mono': 'system-ui-mono',
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
let fontSourcesList = $derived($editorState.fonts.sources);
|
|
50
|
+
let fontStacksList = $derived($editorState.fonts.stacks);
|
|
51
|
+
let allFamilies = $derived((fontSourcesList as FontSource[]).flatMap((s) => s.families.map((f) => ({ ...f, sourceLabel: s.label ?? s.kind }))));
|
|
52
|
+
let familyById = $derived(new Map<string, FontFamily>(allFamilies.map((f) => [f.id, f])));
|
|
53
|
+
|
|
54
|
+
/** Ensure the slot list ends in a system/generic terminal. If it doesn't,
|
|
55
|
+
* append the variable's default generic so the stack is always renderable. */
|
|
56
|
+
function withTerminalFallback(variable: FontStackVariable, slots: FontStackSlot[]): FontStackSlot[] {
|
|
57
|
+
const fallback: FontStackSlot = { kind: 'generic', value: TERMINAL_FALLBACK_BY_VAR[variable] };
|
|
58
|
+
if (slots.length === 0) return [fallback];
|
|
59
|
+
const last = slots[slots.length - 1];
|
|
60
|
+
if (last.kind === 'project') return [...slots, fallback];
|
|
61
|
+
return slots;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function ensureAllStacksPresent(current: FontStack[]): FontStack[] {
|
|
65
|
+
const byVar = new Map(current.map((s) => [s.variable, s]));
|
|
66
|
+
return STACK_VARIABLES.map((v) => {
|
|
67
|
+
const stack = byVar.get(v);
|
|
68
|
+
const slots = withTerminalFallback(v, stack?.slots ?? []);
|
|
69
|
+
return stack ? { ...stack, slots } : { variable: v, slots };
|
|
70
|
+
});
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
let stacks = $derived(ensureAllStacksPresent(fontStacksList));
|
|
74
|
+
|
|
75
|
+
function variableLabel(v: string): string {
|
|
76
|
+
return v.replace(/^--/, '').split('-').map((w) => w.charAt(0).toUpperCase() + w.slice(1)).join(' ');
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function slotKey(slot: FontStackSlot): string {
|
|
80
|
+
if (slot.kind === 'project') return `project:${slot.familyId}`;
|
|
81
|
+
if (slot.kind === 'system') return `system:${slot.preset}`;
|
|
82
|
+
return `generic:${slot.value}`;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function slotFromKey(key: string): FontStackSlot | null {
|
|
86
|
+
const [kind, ...rest] = key.split(':');
|
|
87
|
+
const value = rest.join(':');
|
|
88
|
+
if (kind === 'project') return { kind: 'project', familyId: value };
|
|
89
|
+
if (kind === 'system') return { kind: 'system', preset: value as SystemCascadePreset };
|
|
90
|
+
if (kind === 'generic') return { kind: 'generic', value: value as GenericFamily };
|
|
91
|
+
return null;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function slotDisplayName(slot: FontStackSlot): string {
|
|
95
|
+
if (slot.kind === 'project') return familyById.get(slot.familyId)?.name ?? '(missing)';
|
|
96
|
+
if (slot.kind === 'system') {
|
|
97
|
+
return slot.preset === 'system-ui-sans' ? 'System UI (sans)'
|
|
98
|
+
: slot.preset === 'system-ui-serif' ? 'System UI (serif)'
|
|
99
|
+
: 'System UI (mono)';
|
|
100
|
+
}
|
|
101
|
+
return slot.value;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function slotCssValue(slot: FontStackSlot): string {
|
|
105
|
+
if (slot.kind === 'project') return familyById.get(slot.familyId)?.cssName ?? 'sans-serif';
|
|
106
|
+
if (slot.kind === 'system') return SYSTEM_CASCADES[slot.preset];
|
|
107
|
+
return slot.value;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
function updateStack(variable: FontStackVariable, updater: (slots: FontStackSlot[]) => FontStackSlot[]) {
|
|
111
|
+
const next = stacks.map((s) => (s.variable === variable ? { ...s, slots: updater([...s.slots]) } : s));
|
|
112
|
+
setFontStacks(next);
|
|
113
|
+
applyFontStacks(next, fontSourcesList);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
function handleSelectChange(variable: FontStackVariable, index: number, value: string) {
|
|
117
|
+
const slot = slotFromKey(value);
|
|
118
|
+
if (!slot) return;
|
|
119
|
+
updateStack(variable, (slots) => {
|
|
120
|
+
slots[index] = slot;
|
|
121
|
+
return slots;
|
|
122
|
+
});
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
function onSelectChange(event: Event, variable: FontStackVariable, index: number) {
|
|
126
|
+
const target = event.currentTarget as HTMLSelectElement;
|
|
127
|
+
handleSelectChange(variable, index, target.value);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
function removeSlot(variable: FontStackVariable, index: number) {
|
|
131
|
+
updateStack(variable, (slots) => {
|
|
132
|
+
slots.splice(index, 1);
|
|
133
|
+
return slots;
|
|
134
|
+
});
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
function addSlot(variable: FontStackVariable) {
|
|
138
|
+
const stack = stacks.find((s) => s.variable === variable);
|
|
139
|
+
const generic: GenericFamily =
|
|
140
|
+
variable === '--font-mono' ? 'monospace' : variable === '--font-serif' ? 'serif' : 'sans-serif';
|
|
141
|
+
const existing = new Set((stack?.slots ?? []).map(slotKey));
|
|
142
|
+
let newSlot: FontStackSlot = { kind: 'generic', value: generic };
|
|
143
|
+
if (existing.has(slotKey(newSlot))) {
|
|
144
|
+
const preset: SystemCascadePreset =
|
|
145
|
+
variable === '--font-mono' ? 'system-ui-mono' : variable === '--font-serif' ? 'system-ui-serif' : 'system-ui-sans';
|
|
146
|
+
newSlot = { kind: 'system', preset };
|
|
147
|
+
}
|
|
148
|
+
updateStack(variable, (slots) => {
|
|
149
|
+
// Insert above the terminal fallback (always the last slot) so the
|
|
150
|
+
// terminal stays at the bottom.
|
|
151
|
+
const insertAt = Math.max(0, slots.length - 1);
|
|
152
|
+
slots.splice(insertAt, 0, newSlot);
|
|
153
|
+
return slots;
|
|
154
|
+
});
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/* Drag UX: the source row lifts (opacity, shadow); a white insertion bar
|
|
158
|
+
sits in the gap between rows at the projected drop position. The array
|
|
159
|
+
is only mutated on drop. animate:flip then slides every row to its new
|
|
160
|
+
spot, so the commit doesn't snap.
|
|
161
|
+
|
|
162
|
+
Only the drag handle is `draggable="true"` — putting it on the whole row
|
|
163
|
+
swallowed clicks on the inner <button>/<select> in real browsers because
|
|
164
|
+
mousedown started a drag gesture before `click` could fire. The handle
|
|
165
|
+
starts the drag and calls setDragImage(rowEl, ...) so the visual drag
|
|
166
|
+
image is still the whole row. */
|
|
167
|
+
let dragSource: { variable: FontStackVariable; index: number } | null = $state(null);
|
|
168
|
+
let dragOver: { variable: FontStackVariable; index: number; position: 'before' | 'after' } | null = $state(null);
|
|
169
|
+
|
|
170
|
+
function onDragStart(e: DragEvent, variable: FontStackVariable, index: number) {
|
|
171
|
+
if (!e.dataTransfer) return;
|
|
172
|
+
dragSource = { variable, index };
|
|
173
|
+
e.dataTransfer.effectAllowed = 'move';
|
|
174
|
+
e.dataTransfer.setData('application/x-font-slot', JSON.stringify({ variable, index }));
|
|
175
|
+
const rowEl = (e.currentTarget as HTMLElement).closest('.slot-row') as HTMLElement | null;
|
|
176
|
+
if (rowEl) {
|
|
177
|
+
const rect = rowEl.getBoundingClientRect();
|
|
178
|
+
e.dataTransfer.setDragImage(rowEl, e.clientX - rect.left, e.clientY - rect.top);
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
function onDragOver(e: DragEvent, variable: FontStackVariable, index: number) {
|
|
183
|
+
const types = e.dataTransfer?.types ?? [];
|
|
184
|
+
if (!types.includes('application/x-font-slot')) return;
|
|
185
|
+
e.preventDefault();
|
|
186
|
+
if (e.dataTransfer) e.dataTransfer.dropEffect = 'move';
|
|
187
|
+
const rect = (e.currentTarget as HTMLElement).getBoundingClientRect();
|
|
188
|
+
let position: 'before' | 'after' = e.clientY - rect.top < rect.height / 2 ? 'before' : 'after';
|
|
189
|
+
// Terminal fallback stays at the bottom — never accept a drop after it.
|
|
190
|
+
const stack = stacks.find((s) => s.variable === variable);
|
|
191
|
+
if (stack && index === stack.slots.length - 1 && position === 'after') {
|
|
192
|
+
position = 'before';
|
|
193
|
+
}
|
|
194
|
+
dragOver = { variable, index, position };
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
function onDragLeave() {
|
|
198
|
+
dragOver = null;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
function onDrop(e: DragEvent, variable: FontStackVariable, index: number) {
|
|
202
|
+
e.preventDefault();
|
|
203
|
+
const slotPayload = e.dataTransfer?.getData('application/x-font-slot');
|
|
204
|
+
const position = dragOver?.position ?? 'before';
|
|
205
|
+
dragOver = null;
|
|
206
|
+
if (!slotPayload) return;
|
|
207
|
+
const src = JSON.parse(slotPayload) as { variable: FontStackVariable; index: number };
|
|
208
|
+
if (src.variable !== variable) return;
|
|
209
|
+
if (src.index === index) return;
|
|
210
|
+
updateStack(variable, (slots) => {
|
|
211
|
+
const [moved] = slots.splice(src.index, 1);
|
|
212
|
+
let target = index;
|
|
213
|
+
if (src.index < index) target -= 1;
|
|
214
|
+
if (position === 'after') target += 1;
|
|
215
|
+
slots.splice(target, 0, moved);
|
|
216
|
+
return slots;
|
|
217
|
+
});
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
function onDragEnd() {
|
|
221
|
+
dragSource = null;
|
|
222
|
+
dragOver = null;
|
|
223
|
+
}
|
|
224
|
+
</script>
|
|
225
|
+
|
|
226
|
+
<div class="font-stacks-columns">
|
|
227
|
+
{#each stacks as stack (stack.variable)}
|
|
228
|
+
<div class="font-stack">
|
|
229
|
+
<div class="stack-header">
|
|
230
|
+
<span class="stack-variable">{variableLabel(stack.variable)}</span>
|
|
231
|
+
</div>
|
|
232
|
+
<div class="font-stack-list">
|
|
233
|
+
{#each stack.slots as slot, i (slotKey(slot))}
|
|
234
|
+
{@const isTerminal = i === stack.slots.length - 1}
|
|
235
|
+
<!-- svelte-ignore a11y_no_static_element_interactions -->
|
|
236
|
+
<div
|
|
237
|
+
class="slot-row"
|
|
238
|
+
class:terminal={isTerminal}
|
|
239
|
+
class:drop-before={dragOver?.variable === stack.variable && dragOver?.index === i && dragOver?.position === 'before'}
|
|
240
|
+
class:drop-after={dragOver?.variable === stack.variable && dragOver?.index === i && dragOver?.position === 'after'}
|
|
241
|
+
class:dragging={dragSource?.variable === stack.variable && dragSource?.index === i}
|
|
242
|
+
ondragover={(e) => onDragOver(e, stack.variable, i)}
|
|
243
|
+
ondragleave={onDragLeave}
|
|
244
|
+
ondrop={(e) => onDrop(e, stack.variable, i)}
|
|
245
|
+
ondragend={onDragEnd}
|
|
246
|
+
animate:flip={{ duration: 220, easing: cubicOut }}
|
|
247
|
+
>
|
|
248
|
+
<div class="slot-controls">
|
|
249
|
+
{#if isTerminal}
|
|
250
|
+
<i class="fas fa-lock slot-locked-glyph" aria-hidden="true" title="Final fallback — can't be removed"></i>
|
|
251
|
+
{:else}
|
|
252
|
+
<span
|
|
253
|
+
class="drag-handle"
|
|
254
|
+
aria-hidden="true"
|
|
255
|
+
draggable="true"
|
|
256
|
+
ondragstart={(e) => onDragStart(e, stack.variable, i)}
|
|
257
|
+
>⋮⋮</span>
|
|
258
|
+
{/if}
|
|
259
|
+
<span class="slot-position">{i + 1}.</span>
|
|
260
|
+
<select
|
|
261
|
+
class="ui-form-select slot-select"
|
|
262
|
+
value={slotKey(slot)}
|
|
263
|
+
onchange={(e) => onSelectChange(e, stack.variable, i)}
|
|
264
|
+
>
|
|
265
|
+
{#if isTerminal}
|
|
266
|
+
{@const sys = TERMINAL_SYSTEM_BY_VAR[stack.variable]}
|
|
267
|
+
{@const gen = TERMINAL_FALLBACK_BY_VAR[stack.variable]}
|
|
268
|
+
<option value={`system:${sys}`}>{sys === 'system-ui-sans' ? 'System UI (sans)' : sys === 'system-ui-serif' ? 'System UI (serif)' : 'System UI (mono)'}</option>
|
|
269
|
+
<option value={`generic:${gen}`}>{gen}</option>
|
|
270
|
+
{:else}
|
|
271
|
+
{#if allFamilies.length > 0}
|
|
272
|
+
<optgroup label="Project fonts">
|
|
273
|
+
{#each allFamilies as fam}
|
|
274
|
+
<option value={`project:${fam.id}`}>{fam.name}</option>
|
|
275
|
+
{/each}
|
|
276
|
+
</optgroup>
|
|
277
|
+
{/if}
|
|
278
|
+
<optgroup label="System cascade">
|
|
279
|
+
{#each SYSTEM_PRESETS as p}
|
|
280
|
+
<option value={`system:${p}`}>{p === 'system-ui-sans' ? 'System UI (sans)' : p === 'system-ui-serif' ? 'System UI (serif)' : 'System UI (mono)'}</option>
|
|
281
|
+
{/each}
|
|
282
|
+
</optgroup>
|
|
283
|
+
<optgroup label="Generic">
|
|
284
|
+
{#each GENERIC_VALUES as g}
|
|
285
|
+
<option value={`generic:${g}`}>{g}</option>
|
|
286
|
+
{/each}
|
|
287
|
+
</optgroup>
|
|
288
|
+
{/if}
|
|
289
|
+
</select>
|
|
290
|
+
{#if isTerminal}
|
|
291
|
+
<span class="slot-remove-placeholder" aria-hidden="true"></span>
|
|
292
|
+
{:else}
|
|
293
|
+
<button
|
|
294
|
+
type="button"
|
|
295
|
+
class="slot-remove"
|
|
296
|
+
aria-label="Remove slot"
|
|
297
|
+
title="Remove"
|
|
298
|
+
onclick={() => removeSlot(stack.variable, i)}
|
|
299
|
+
>×</button>
|
|
300
|
+
{/if}
|
|
301
|
+
</div>
|
|
302
|
+
<span
|
|
303
|
+
class="slot-preview"
|
|
304
|
+
style="font-family: {slotCssValue(slot)};{stack.variable === '--font-display' ? ' font-size: var(--ui-font-size-2xl);' : ''}"
|
|
305
|
+
>The quick brown fox jumps over the lazy dog</span>
|
|
306
|
+
</div>
|
|
307
|
+
{/each}
|
|
308
|
+
</div>
|
|
309
|
+
<button type="button" class="add-fallback" onclick={() => addSlot(stack.variable)}>
|
|
310
|
+
+ add fallback
|
|
311
|
+
</button>
|
|
312
|
+
</div>
|
|
313
|
+
{/each}
|
|
314
|
+
</div>
|
|
315
|
+
|
|
316
|
+
<style>
|
|
317
|
+
.font-stacks-columns {
|
|
318
|
+
display: grid;
|
|
319
|
+
grid-template-columns: repeat(2, minmax(0, 1fr));
|
|
320
|
+
gap: var(--ui-space-24);
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
@media (max-width: 720px) {
|
|
324
|
+
.font-stacks-columns {
|
|
325
|
+
grid-template-columns: 1fr;
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
/* Track: each font family is a bordered channel that the slot cards drop into.
|
|
330
|
+
The interior sits a step deeper than the page surface, so the slot cards
|
|
331
|
+
(surface-low) read as raised inside the depression. */
|
|
332
|
+
.font-stack {
|
|
333
|
+
position: relative;
|
|
334
|
+
display: flex;
|
|
335
|
+
flex-direction: column;
|
|
336
|
+
gap: var(--ui-space-12);
|
|
337
|
+
padding: var(--ui-space-20) var(--ui-space-12) var(--ui-space-16);
|
|
338
|
+
background: var(--ui-surface-lowest);
|
|
339
|
+
border: 1px solid var(--ui-border-low);
|
|
340
|
+
border-radius: var(--ui-radius-lg);
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
/* Legend: knock through the top border like a native <fieldset>'s <legend>.
|
|
344
|
+
Editor content bg is solid black; the header paints over the border line
|
|
345
|
+
to cut it. */
|
|
346
|
+
.stack-header {
|
|
347
|
+
position: absolute;
|
|
348
|
+
top: 0;
|
|
349
|
+
left: var(--ui-space-12);
|
|
350
|
+
transform: translateY(-50%);
|
|
351
|
+
display: flex;
|
|
352
|
+
align-items: center;
|
|
353
|
+
padding: 0 var(--ui-space-6);
|
|
354
|
+
background: black;
|
|
355
|
+
line-height: 1;
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
.stack-variable {
|
|
359
|
+
font-size: var(--ui-font-size-lg);
|
|
360
|
+
font-weight: var(--ui-font-weight-bold);
|
|
361
|
+
color: var(--ui-text-primary);
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
.font-stack-list {
|
|
365
|
+
display: flex;
|
|
366
|
+
flex-direction: column;
|
|
367
|
+
gap: var(--ui-space-8);
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
.slot-row {
|
|
371
|
+
display: flex;
|
|
372
|
+
flex-direction: column;
|
|
373
|
+
gap: var(--ui-space-8);
|
|
374
|
+
padding: var(--ui-space-10);
|
|
375
|
+
border: 1px solid var(--ui-border-low);
|
|
376
|
+
border-radius: var(--ui-radius-md);
|
|
377
|
+
background: var(--ui-surface-low);
|
|
378
|
+
position: relative;
|
|
379
|
+
transition:
|
|
380
|
+
opacity var(--ui-transition-fast),
|
|
381
|
+
transform var(--ui-transition-fast),
|
|
382
|
+
border-color var(--ui-transition-fast);
|
|
383
|
+
}
|
|
384
|
+
.slot-row:hover { border-color: var(--ui-border); }
|
|
385
|
+
|
|
386
|
+
.slot-controls {
|
|
387
|
+
display: grid;
|
|
388
|
+
grid-template-columns: auto auto 1fr auto;
|
|
389
|
+
align-items: center;
|
|
390
|
+
gap: var(--ui-space-8);
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
/* Reorder UX: a thin white bar sits in the existing 8px gap between rows
|
|
394
|
+
as the insertion indicator. The bar is absolutely positioned and consumes
|
|
395
|
+
no layout space, so the parent track stays the same height. On drop, the
|
|
396
|
+
array commits and animate:flip slides each row to its new position. */
|
|
397
|
+
.slot-row.drop-before::before,
|
|
398
|
+
.slot-row.drop-after::after {
|
|
399
|
+
content: '';
|
|
400
|
+
position: absolute;
|
|
401
|
+
left: 0;
|
|
402
|
+
right: 0;
|
|
403
|
+
height: 2px;
|
|
404
|
+
background: var(--ui-text-primary);
|
|
405
|
+
border-radius: 1px;
|
|
406
|
+
box-shadow: 0 0 6px rgba(255, 255, 255, 0.45);
|
|
407
|
+
}
|
|
408
|
+
.slot-row.drop-before::before { top: -5px; }
|
|
409
|
+
.slot-row.drop-after::after { bottom: -5px; }
|
|
410
|
+
|
|
411
|
+
.slot-row.dragging {
|
|
412
|
+
opacity: 0.55;
|
|
413
|
+
z-index: 2;
|
|
414
|
+
border-color: var(--ui-border-high);
|
|
415
|
+
box-shadow: 0 6px 16px rgba(0, 0, 0, 0.5);
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
.drag-handle {
|
|
419
|
+
cursor: grab;
|
|
420
|
+
user-select: none;
|
|
421
|
+
color: var(--ui-text-muted);
|
|
422
|
+
font-size: var(--ui-font-size-md);
|
|
423
|
+
line-height: 1;
|
|
424
|
+
}
|
|
425
|
+
.slot-row.dragging .drag-handle { cursor: grabbing; }
|
|
426
|
+
|
|
427
|
+
/* Terminal-row lock glyph sits in the drag-handle's grid track; the row's
|
|
428
|
+
right-hand X column is left empty (see .slot-remove-placeholder) so the
|
|
429
|
+
lock is the only chrome and reads as "this row is fixed." */
|
|
430
|
+
.slot-locked-glyph {
|
|
431
|
+
display: inline-flex;
|
|
432
|
+
align-items: center;
|
|
433
|
+
justify-content: center;
|
|
434
|
+
color: var(--ui-text-muted);
|
|
435
|
+
font-size: var(--ui-font-size-sm);
|
|
436
|
+
opacity: 0.55;
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
/* Empty grid track on terminal rows where the X button sits on others,
|
|
440
|
+
so columns stay aligned. */
|
|
441
|
+
.slot-remove-placeholder {
|
|
442
|
+
width: 1.5rem;
|
|
443
|
+
height: 1.5rem;
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
.slot-position {
|
|
447
|
+
font-size: var(--ui-font-size-md);
|
|
448
|
+
color: var(--ui-text-muted);
|
|
449
|
+
min-width: 1.25rem;
|
|
450
|
+
text-align: right;
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
.slot-preview {
|
|
454
|
+
font-size: var(--ui-font-size-md);
|
|
455
|
+
color: var(--ui-text-primary);
|
|
456
|
+
line-height: var(--ui-line-height-normal);
|
|
457
|
+
overflow: hidden;
|
|
458
|
+
text-overflow: ellipsis;
|
|
459
|
+
white-space: nowrap;
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
.slot-select {
|
|
463
|
+
width: 100%;
|
|
464
|
+
font-family: var(--ui-font-mono);
|
|
465
|
+
font-size: var(--ui-font-size-sm);
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
.slot-remove {
|
|
469
|
+
background: none;
|
|
470
|
+
border: 1px solid var(--ui-border-low);
|
|
471
|
+
color: var(--ui-text-muted);
|
|
472
|
+
font-size: var(--ui-font-size-md);
|
|
473
|
+
line-height: 1;
|
|
474
|
+
width: 1.5rem;
|
|
475
|
+
height: 1.5rem;
|
|
476
|
+
border-radius: var(--ui-radius-sm);
|
|
477
|
+
cursor: pointer;
|
|
478
|
+
}
|
|
479
|
+
.slot-remove:hover:not(:disabled) {
|
|
480
|
+
color: var(--ui-text-primary);
|
|
481
|
+
border-color: var(--ui-border);
|
|
482
|
+
}
|
|
483
|
+
.slot-remove:disabled { opacity: 0.35; cursor: not-allowed; }
|
|
484
|
+
|
|
485
|
+
.add-fallback {
|
|
486
|
+
align-self: flex-start;
|
|
487
|
+
background: none;
|
|
488
|
+
border: 1px dashed var(--ui-border-low);
|
|
489
|
+
color: var(--ui-text-muted);
|
|
490
|
+
font-size: var(--ui-font-size-sm);
|
|
491
|
+
padding: var(--ui-space-4) var(--ui-space-8);
|
|
492
|
+
border-radius: var(--ui-radius-sm);
|
|
493
|
+
cursor: pointer;
|
|
494
|
+
}
|
|
495
|
+
.add-fallback:hover {
|
|
496
|
+
color: var(--ui-text-primary);
|
|
497
|
+
border-color: var(--ui-border);
|
|
498
|
+
}
|
|
499
|
+
</style>
|