@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,483 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
/**
|
|
3
|
+
* 2D shape control for a radial gradient. Two independent stretch
|
|
4
|
+
* factors (X and Y, both 1 → 8) define the ellipse's semi-axes; rendering
|
|
5
|
+
* uses `base * aspectX` and `base * aspectY` so the data also encodes
|
|
6
|
+
* size (both = 8 is a circle 8× bigger than both = 1).
|
|
7
|
+
*
|
|
8
|
+
* Three concurrent affordances stay synced:
|
|
9
|
+
* - 2D pad with a draggable handle that lives at (aspectX, aspectY).
|
|
10
|
+
* The visible ellipse is drawn with semi-axes equal to the handle's
|
|
11
|
+
* offset from center, so the dot you drag IS the corner of the shape.
|
|
12
|
+
* - Horizontal slider beneath the pad — drags X only.
|
|
13
|
+
* - Vertical slider beside the pad — drags Y only.
|
|
14
|
+
*
|
|
15
|
+
* Range chosen so 1 = "no stretch" and 8 = "8× stretch" — same units the
|
|
16
|
+
* user sees in the W : H ratio readout.
|
|
17
|
+
*/
|
|
18
|
+
import UIPillButton from '../../ui/UIPillButton.svelte';
|
|
19
|
+
interface Props {
|
|
20
|
+
/** Horizontal stretch (1 = unscaled, default). */
|
|
21
|
+
x?: number;
|
|
22
|
+
/** Vertical stretch (1 = unscaled, default). */
|
|
23
|
+
y?: number;
|
|
24
|
+
/** Edge length of the square pad in px. Sliders extend the bounding box. */
|
|
25
|
+
size?: number;
|
|
26
|
+
/** Clamp bounds for each axis. */
|
|
27
|
+
min?: number;
|
|
28
|
+
max?: number;
|
|
29
|
+
onchange?: (payload: { x: number; y: number }) => void;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
let {
|
|
33
|
+
x = $bindable(1),
|
|
34
|
+
y = $bindable(1),
|
|
35
|
+
size = 80,
|
|
36
|
+
min = 1,
|
|
37
|
+
max = 8,
|
|
38
|
+
onchange,
|
|
39
|
+
}: Props = $props();
|
|
40
|
+
|
|
41
|
+
let padEl: HTMLDivElement | undefined = $state();
|
|
42
|
+
let xSliderEl: HTMLDivElement | undefined = $state();
|
|
43
|
+
let ySliderEl: HTMLDivElement | undefined = $state();
|
|
44
|
+
let drag: 'pad' | 'x' | 'y' | null = $state(null);
|
|
45
|
+
|
|
46
|
+
// Quadrant the handle currently lives in. The stored aspect values are
|
|
47
|
+
// always positive (CSS radial-gradient semi-axes must be), but the dot can
|
|
48
|
+
// be dragged into any of the four quadrants. The ellipse is symmetric, so
|
|
49
|
+
// mirroring the handle reads as "this corner" — the rendered shape is
|
|
50
|
+
// identical. Sign is session-local; on reload the dot defaults to top-right.
|
|
51
|
+
let signX = $state(1);
|
|
52
|
+
let signY = $state(1);
|
|
53
|
+
|
|
54
|
+
/** Inset (px) the pad uses to keep the corner handle inside its border.
|
|
55
|
+
* Sliders use a separate inset since their end-labels sit outside the
|
|
56
|
+
* rail entirely now. */
|
|
57
|
+
const PAD_INSET = 6;
|
|
58
|
+
const SLIDER_INSET = 0;
|
|
59
|
+
|
|
60
|
+
function clamp(v: number, lo: number, hi: number): number {
|
|
61
|
+
return Math.max(lo, Math.min(hi, v));
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/** Snap near-integer values so the user can land cleanly on {1,2,…,8}. */
|
|
65
|
+
function snap(v: number): number {
|
|
66
|
+
const rounded = Math.round(v);
|
|
67
|
+
if (Math.abs(v - rounded) < 0.05) return rounded;
|
|
68
|
+
return Math.round(v * 10) / 10;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function emit(nx: number, ny: number) {
|
|
72
|
+
const cx = clamp(snap(nx), min, max);
|
|
73
|
+
const cy = clamp(snap(ny), min, max);
|
|
74
|
+
if (cx === x && cy === y) return;
|
|
75
|
+
x = cx;
|
|
76
|
+
y = cy;
|
|
77
|
+
onchange?.({ x: cx, y: cy });
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/** Pad pointer → (aspectX, aspectY). The pointer's signed distance from
|
|
81
|
+
* center decides the quadrant; magnitude maps linearly into (min, max).
|
|
82
|
+
* signX/signY are updated here so the handle renders in the quadrant the
|
|
83
|
+
* user actually dragged to, even though the returned aspect values stay
|
|
84
|
+
* positive. Clamping at the edges is handled by `emit`. */
|
|
85
|
+
function aspectFromPad(e: PointerEvent): { x: number; y: number } {
|
|
86
|
+
const rect = padEl!.getBoundingClientRect();
|
|
87
|
+
const cx = rect.left + rect.width / 2;
|
|
88
|
+
const cy = rect.top + rect.height / 2;
|
|
89
|
+
const span = rect.width / 2 - PAD_INSET;
|
|
90
|
+
const dx = e.clientX - cx;
|
|
91
|
+
const dy = cy - e.clientY;
|
|
92
|
+
if (dx !== 0) signX = dx >= 0 ? 1 : -1;
|
|
93
|
+
if (dy !== 0) signY = dy >= 0 ? 1 : -1;
|
|
94
|
+
const nx = min + (Math.abs(dx) / span) * (max - min);
|
|
95
|
+
const ny = min + (Math.abs(dy) / span) * (max - min);
|
|
96
|
+
return { x: nx, y: ny };
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function valueFromLinearSlider(el: HTMLDivElement, clientPx: number, axis: 'h' | 'v'): number {
|
|
100
|
+
const rect = el.getBoundingClientRect();
|
|
101
|
+
const len = axis === 'h' ? rect.width - 2 * SLIDER_INSET : rect.height - 2 * SLIDER_INSET;
|
|
102
|
+
const pos = axis === 'h'
|
|
103
|
+
? clientPx - (rect.left + SLIDER_INSET)
|
|
104
|
+
: (rect.bottom - SLIDER_INSET) - clientPx;
|
|
105
|
+
const t = clamp(pos / len, 0, 1);
|
|
106
|
+
return min + t * (max - min);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function onPadDown(e: PointerEvent) {
|
|
110
|
+
drag = 'pad';
|
|
111
|
+
padEl!.setPointerCapture(e.pointerId);
|
|
112
|
+
const { x: nx, y: ny } = aspectFromPad(e);
|
|
113
|
+
emit(nx, ny);
|
|
114
|
+
}
|
|
115
|
+
function onXDown(e: PointerEvent) {
|
|
116
|
+
drag = 'x';
|
|
117
|
+
xSliderEl!.setPointerCapture(e.pointerId);
|
|
118
|
+
emit(valueFromLinearSlider(xSliderEl!, e.clientX, 'h'), y);
|
|
119
|
+
}
|
|
120
|
+
function onYDown(e: PointerEvent) {
|
|
121
|
+
drag = 'y';
|
|
122
|
+
ySliderEl!.setPointerCapture(e.pointerId);
|
|
123
|
+
emit(x, valueFromLinearSlider(ySliderEl!, e.clientY, 'v'));
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
function onMove(e: PointerEvent) {
|
|
127
|
+
if (drag === 'pad') {
|
|
128
|
+
const { x: nx, y: ny } = aspectFromPad(e);
|
|
129
|
+
emit(nx, ny);
|
|
130
|
+
} else if (drag === 'x') {
|
|
131
|
+
emit(valueFromLinearSlider(xSliderEl!, e.clientX, 'h'), y);
|
|
132
|
+
} else if (drag === 'y') {
|
|
133
|
+
emit(x, valueFromLinearSlider(ySliderEl!, e.clientY, 'v'));
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
function onUp(e: PointerEvent) {
|
|
137
|
+
if (!drag) return;
|
|
138
|
+
const target = drag === 'pad' ? padEl! : drag === 'x' ? xSliderEl! : ySliderEl!;
|
|
139
|
+
target.releasePointerCapture(e.pointerId);
|
|
140
|
+
drag = null;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/** Position of the pad handle and the rendered ellipse's bounding corner.
|
|
144
|
+
* Aspect values map to pixel magnitudes; signX/signY pick which of the
|
|
145
|
+
* ellipse's four corners the handle sits on. The ellipse itself is always
|
|
146
|
+
* centered with positive semi-axes (it's symmetric, so the shape reads
|
|
147
|
+
* identically in any quadrant). */
|
|
148
|
+
let pad = $derived.by(() => {
|
|
149
|
+
const span = size / 2 - PAD_INSET;
|
|
150
|
+
const tx = (x - min) / (max - min);
|
|
151
|
+
const ty = (y - min) / (max - min);
|
|
152
|
+
const rx = tx * span;
|
|
153
|
+
const ry = ty * span;
|
|
154
|
+
const center = size / 2;
|
|
155
|
+
return { center, rx, ry, handleX: center + signX * rx, handleY: center - signY * ry };
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
let xThumb = $derived(((x - min) / (max - min)));
|
|
159
|
+
let yThumb = $derived(((y - min) / (max - min)));
|
|
160
|
+
|
|
161
|
+
function parseAxisInput(raw: string): number | null {
|
|
162
|
+
const n = parseFloat(raw.trim());
|
|
163
|
+
return Number.isFinite(n) ? n : null;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
function onXInput(e: Event) {
|
|
167
|
+
const v = parseAxisInput((e.target as HTMLInputElement).value);
|
|
168
|
+
if (v !== null) emit(v, y);
|
|
169
|
+
}
|
|
170
|
+
function onYInput(e: Event) {
|
|
171
|
+
const v = parseAxisInput((e.target as HTMLInputElement).value);
|
|
172
|
+
if (v !== null) emit(x, v);
|
|
173
|
+
}
|
|
174
|
+
</script>
|
|
175
|
+
|
|
176
|
+
<svelte:window onpointermove={onMove} onpointerup={onUp} onpointercancel={onUp} />
|
|
177
|
+
|
|
178
|
+
<div class="shape-pad">
|
|
179
|
+
<div class="pad-body">
|
|
180
|
+
<div class="grid">
|
|
181
|
+
<div
|
|
182
|
+
bind:this={padEl}
|
|
183
|
+
class="pad"
|
|
184
|
+
class:dragging={drag === 'pad'}
|
|
185
|
+
style="width: {size}px; height: {size}px;"
|
|
186
|
+
onpointerdown={onPadDown}
|
|
187
|
+
role="application"
|
|
188
|
+
aria-label="Drag to set the radial gradient's width and height"
|
|
189
|
+
>
|
|
190
|
+
<span class="axis axis-h" aria-hidden="true"></span>
|
|
191
|
+
<span class="axis axis-v" aria-hidden="true"></span>
|
|
192
|
+
<svg class="ellipse" viewBox="0 0 {size} {size}" aria-hidden="true">
|
|
193
|
+
<ellipse
|
|
194
|
+
cx={pad.center}
|
|
195
|
+
cy={pad.center}
|
|
196
|
+
rx={Math.max(3, pad.rx)}
|
|
197
|
+
ry={Math.max(3, pad.ry)}
|
|
198
|
+
fill="none"
|
|
199
|
+
stroke="currentColor"
|
|
200
|
+
stroke-width="1"
|
|
201
|
+
/>
|
|
202
|
+
<line
|
|
203
|
+
x1={pad.center}
|
|
204
|
+
y1={pad.center}
|
|
205
|
+
x2={pad.handleX}
|
|
206
|
+
y2={pad.handleY}
|
|
207
|
+
stroke="currentColor"
|
|
208
|
+
stroke-width="1"
|
|
209
|
+
stroke-dasharray="2 2"
|
|
210
|
+
opacity="0.4"
|
|
211
|
+
/>
|
|
212
|
+
</svg>
|
|
213
|
+
<span
|
|
214
|
+
class="handle"
|
|
215
|
+
class:dragging={drag === 'pad'}
|
|
216
|
+
style="left: {pad.handleX}px; top: {pad.handleY}px;"
|
|
217
|
+
aria-hidden="true"
|
|
218
|
+
></span>
|
|
219
|
+
</div>
|
|
220
|
+
|
|
221
|
+
<div
|
|
222
|
+
bind:this={ySliderEl}
|
|
223
|
+
class="slider slider-v"
|
|
224
|
+
class:dragging={drag === 'y'}
|
|
225
|
+
style="height: {size}px;"
|
|
226
|
+
onpointerdown={onYDown}
|
|
227
|
+
role="slider"
|
|
228
|
+
aria-orientation="vertical"
|
|
229
|
+
aria-valuemin={min}
|
|
230
|
+
aria-valuemax={max}
|
|
231
|
+
aria-valuenow={y}
|
|
232
|
+
aria-label="Vertical stretch"
|
|
233
|
+
tabindex="0"
|
|
234
|
+
onkeydown={(e) => {
|
|
235
|
+
if (e.key === 'ArrowUp') { e.preventDefault(); emit(x, y + 0.5); }
|
|
236
|
+
else if (e.key === 'ArrowDown') { e.preventDefault(); emit(x, y - 0.5); }
|
|
237
|
+
}}
|
|
238
|
+
>
|
|
239
|
+
<span class="track" aria-hidden="true"></span>
|
|
240
|
+
<span class="thumb" style="bottom: calc({yThumb * 100}% - 4px);" aria-hidden="true"></span>
|
|
241
|
+
</div>
|
|
242
|
+
|
|
243
|
+
<div
|
|
244
|
+
bind:this={xSliderEl}
|
|
245
|
+
class="slider slider-h"
|
|
246
|
+
class:dragging={drag === 'x'}
|
|
247
|
+
style="width: {size}px;"
|
|
248
|
+
onpointerdown={onXDown}
|
|
249
|
+
role="slider"
|
|
250
|
+
aria-orientation="horizontal"
|
|
251
|
+
aria-valuemin={min}
|
|
252
|
+
aria-valuemax={max}
|
|
253
|
+
aria-valuenow={x}
|
|
254
|
+
aria-label="Horizontal stretch"
|
|
255
|
+
tabindex="0"
|
|
256
|
+
onkeydown={(e) => {
|
|
257
|
+
if (e.key === 'ArrowLeft') { e.preventDefault(); emit(x - 0.5, y); }
|
|
258
|
+
else if (e.key === 'ArrowRight') { e.preventDefault(); emit(x + 0.5, y); }
|
|
259
|
+
}}
|
|
260
|
+
>
|
|
261
|
+
<span class="track" aria-hidden="true"></span>
|
|
262
|
+
<span class="thumb" style="left: calc({xThumb * 100}% - 4px);" aria-hidden="true"></span>
|
|
263
|
+
</div>
|
|
264
|
+
</div>
|
|
265
|
+
|
|
266
|
+
<div class="readouts">
|
|
267
|
+
<label class="num-field">
|
|
268
|
+
<span class="num-label">W</span>
|
|
269
|
+
<input
|
|
270
|
+
type="number"
|
|
271
|
+
min={min}
|
|
272
|
+
max={max}
|
|
273
|
+
step="0.1"
|
|
274
|
+
value={x}
|
|
275
|
+
onchange={onXInput}
|
|
276
|
+
/>
|
|
277
|
+
</label>
|
|
278
|
+
<label class="num-field">
|
|
279
|
+
<span class="num-label">H</span>
|
|
280
|
+
<input
|
|
281
|
+
type="number"
|
|
282
|
+
min={min}
|
|
283
|
+
max={max}
|
|
284
|
+
step="0.1"
|
|
285
|
+
value={y}
|
|
286
|
+
onchange={onYInput}
|
|
287
|
+
/>
|
|
288
|
+
</label>
|
|
289
|
+
<UIPillButton
|
|
290
|
+
variant="secondary"
|
|
291
|
+
size="compact"
|
|
292
|
+
icon="fa-arrows-rotate"
|
|
293
|
+
title="Reset to circle (1 : 1)"
|
|
294
|
+
disabled={x === 1 && y === 1}
|
|
295
|
+
onclick={() => emit(1, 1)}
|
|
296
|
+
/>
|
|
297
|
+
</div>
|
|
298
|
+
</div>
|
|
299
|
+
</div>
|
|
300
|
+
|
|
301
|
+
<style>
|
|
302
|
+
/* Two-column body: pad + sliders on the left, W/H/Reset on the right.
|
|
303
|
+
The section header ("Gradient shape") is owned by GradientEditor so it
|
|
304
|
+
can sit on the same baseline as the parent's section label. */
|
|
305
|
+
.shape-pad {
|
|
306
|
+
display: inline-flex;
|
|
307
|
+
color: var(--ui-text-secondary);
|
|
308
|
+
font-size: var(--ui-font-size-sm);
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
.pad-body {
|
|
312
|
+
display: inline-flex;
|
|
313
|
+
align-items: flex-start;
|
|
314
|
+
gap: var(--ui-space-12);
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
/* Pad in the top-left, Y slider snugged to its right edge, X slider
|
|
318
|
+
snugged to its bottom edge. Zero gaps so the rails read as extensions
|
|
319
|
+
of the pad's own axis lines rather than separate widgets. */
|
|
320
|
+
.grid {
|
|
321
|
+
display: grid;
|
|
322
|
+
grid-template-columns: max-content max-content;
|
|
323
|
+
grid-template-rows: max-content max-content;
|
|
324
|
+
gap: 0;
|
|
325
|
+
align-items: start;
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
.pad {
|
|
329
|
+
position: relative;
|
|
330
|
+
grid-row: 1;
|
|
331
|
+
grid-column: 1;
|
|
332
|
+
background: var(--ui-surface-lowest);
|
|
333
|
+
border: 1px solid var(--ui-border);
|
|
334
|
+
border-radius: var(--ui-radius-sm);
|
|
335
|
+
cursor: crosshair;
|
|
336
|
+
color: var(--ui-text-primary);
|
|
337
|
+
touch-action: none;
|
|
338
|
+
box-shadow: inset 0 0 0 1px rgba(255, 255, 255, 0.02);
|
|
339
|
+
}
|
|
340
|
+
.pad:focus-visible { outline: 2px solid var(--ui-text-primary); outline-offset: 2px; }
|
|
341
|
+
.pad.dragging { cursor: grabbing; }
|
|
342
|
+
|
|
343
|
+
/* Faint center cross — anchors the gradient origin and shows the user
|
|
344
|
+
where the (1,1) state lives (handle parks right at the cross). */
|
|
345
|
+
.axis {
|
|
346
|
+
position: absolute;
|
|
347
|
+
background: var(--ui-border-low);
|
|
348
|
+
pointer-events: none;
|
|
349
|
+
}
|
|
350
|
+
.axis-h { left: 4px; right: 4px; top: 50%; height: 1px; }
|
|
351
|
+
.axis-v { top: 4px; bottom: 4px; left: 50%; width: 1px; }
|
|
352
|
+
|
|
353
|
+
.ellipse {
|
|
354
|
+
position: absolute;
|
|
355
|
+
inset: 0;
|
|
356
|
+
pointer-events: none;
|
|
357
|
+
color: var(--ui-text-primary);
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
/* Bigger, brighter dot than v1 + a subtle outer ring on hover so it reads
|
|
361
|
+
as "this is the thing you grab." The dashed line back to center makes
|
|
362
|
+
the relationship between handle position and shape semi-axes legible. */
|
|
363
|
+
.handle {
|
|
364
|
+
position: absolute;
|
|
365
|
+
width: 11px;
|
|
366
|
+
height: 11px;
|
|
367
|
+
margin-left: -5.5px;
|
|
368
|
+
margin-top: -5.5px;
|
|
369
|
+
border-radius: 50%;
|
|
370
|
+
background: var(--ui-text-primary);
|
|
371
|
+
border: 2px solid var(--ui-surface-lowest);
|
|
372
|
+
box-shadow: 0 0 0 1px var(--ui-text-primary), 0 0 6px rgba(255, 255, 255, 0.15);
|
|
373
|
+
cursor: grab;
|
|
374
|
+
pointer-events: none;
|
|
375
|
+
transition: box-shadow var(--ui-transition-fast);
|
|
376
|
+
}
|
|
377
|
+
.pad:hover .handle {
|
|
378
|
+
box-shadow: 0 0 0 1px var(--ui-text-primary), 0 0 10px rgba(255, 255, 255, 0.25);
|
|
379
|
+
}
|
|
380
|
+
.handle.dragging,
|
|
381
|
+
.pad.dragging .handle {
|
|
382
|
+
cursor: grabbing;
|
|
383
|
+
box-shadow: 0 0 0 2px var(--ui-text-primary), 0 0 12px rgba(255, 255, 255, 0.35);
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
/* Slider rails. Each rail hugs the pad edge it represents: X under the
|
|
387
|
+
pad's bottom, Y to the right of the pad's right edge. The thumbs are
|
|
388
|
+
small arrow triangles whose tips touch the rail — they read as "this
|
|
389
|
+
points at the value" instead of a dot drifting along the rail. */
|
|
390
|
+
.slider {
|
|
391
|
+
position: relative;
|
|
392
|
+
flex: none;
|
|
393
|
+
cursor: pointer;
|
|
394
|
+
touch-action: none;
|
|
395
|
+
}
|
|
396
|
+
.slider:focus-visible { outline: 2px solid var(--ui-text-primary); outline-offset: 2px; }
|
|
397
|
+
|
|
398
|
+
/* Horizontal slider: rail along the very top (flush against the pad's
|
|
399
|
+
bottom border), arrow thumb sits below pointing up at the rail. */
|
|
400
|
+
.slider-h {
|
|
401
|
+
grid-row: 2;
|
|
402
|
+
grid-column: 1;
|
|
403
|
+
height: 14px;
|
|
404
|
+
}
|
|
405
|
+
/* Vertical slider: rail along the very left (flush against the pad's
|
|
406
|
+
right border), arrow thumb sits to the right pointing left at the
|
|
407
|
+
rail. */
|
|
408
|
+
.slider-v {
|
|
409
|
+
grid-row: 1;
|
|
410
|
+
grid-column: 2;
|
|
411
|
+
width: 14px;
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
.track {
|
|
415
|
+
position: absolute;
|
|
416
|
+
background: var(--ui-border);
|
|
417
|
+
}
|
|
418
|
+
.slider-h .track { left: 0; right: 0; top: 0; height: 1px; border-radius: 1px; }
|
|
419
|
+
.slider-v .track { top: 0; bottom: 0; left: 0; width: 1px; border-radius: 1px; }
|
|
420
|
+
|
|
421
|
+
/* Arrow thumb — CSS triangle. The transparent borders create the legs
|
|
422
|
+
and the colored border creates the tip. We use `border-bottom` to make
|
|
423
|
+
the tip point UP, `border-right` to make it point LEFT. */
|
|
424
|
+
.thumb {
|
|
425
|
+
position: absolute;
|
|
426
|
+
width: 0;
|
|
427
|
+
height: 0;
|
|
428
|
+
pointer-events: none;
|
|
429
|
+
}
|
|
430
|
+
.slider-h .thumb {
|
|
431
|
+
top: 2px;
|
|
432
|
+
border-left: 4px solid transparent;
|
|
433
|
+
border-right: 4px solid transparent;
|
|
434
|
+
border-bottom: 6px solid var(--ui-text-primary);
|
|
435
|
+
}
|
|
436
|
+
.slider-v .thumb {
|
|
437
|
+
left: 2px;
|
|
438
|
+
border-top: 4px solid transparent;
|
|
439
|
+
border-bottom: 4px solid transparent;
|
|
440
|
+
border-right: 6px solid var(--ui-text-primary);
|
|
441
|
+
}
|
|
442
|
+
.slider.dragging .thumb {
|
|
443
|
+
filter: drop-shadow(0 0 2px rgba(255, 255, 255, 0.35));
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
|
|
447
|
+
/* Vertical stack beside the pad: W input, H input, Reset. Right-aligned
|
|
448
|
+
so the input field's right edges and the Reset button's right edge all
|
|
449
|
+
share a single vertical line — reads as a tidy aligned column. */
|
|
450
|
+
.readouts {
|
|
451
|
+
display: inline-flex;
|
|
452
|
+
flex-direction: column;
|
|
453
|
+
align-items: flex-end;
|
|
454
|
+
gap: var(--ui-space-6);
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
.num-field {
|
|
458
|
+
display: inline-flex;
|
|
459
|
+
align-items: center;
|
|
460
|
+
gap: var(--ui-space-6);
|
|
461
|
+
color: var(--ui-text-secondary);
|
|
462
|
+
}
|
|
463
|
+
.num-label {
|
|
464
|
+
font-family: var(--ui-font-mono);
|
|
465
|
+
font-size: var(--ui-font-size-xs);
|
|
466
|
+
color: var(--ui-text-tertiary);
|
|
467
|
+
}
|
|
468
|
+
.num-field input {
|
|
469
|
+
width: 3.25rem;
|
|
470
|
+
padding: var(--ui-space-2) var(--ui-space-6);
|
|
471
|
+
background: var(--ui-surface-lowest);
|
|
472
|
+
border: 1px solid var(--ui-border-low);
|
|
473
|
+
border-radius: var(--ui-radius-sm);
|
|
474
|
+
color: var(--ui-text-primary);
|
|
475
|
+
font-family: var(--ui-font-mono);
|
|
476
|
+
font-size: var(--ui-font-size-sm);
|
|
477
|
+
text-align: right;
|
|
478
|
+
}
|
|
479
|
+
.num-field input::-webkit-outer-spin-button,
|
|
480
|
+
.num-field input::-webkit-inner-spin-button { -webkit-appearance: none; margin: 0; }
|
|
481
|
+
|
|
482
|
+
|
|
483
|
+
</style>
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
2
|
import { run } from 'svelte/legacy';
|
|
3
3
|
|
|
4
|
-
import { sanitizeFileName } from '../../
|
|
4
|
+
import { sanitizeFileName } from '../../core/themes/themeService';
|
|
5
5
|
import UIDialog from '../../ui/UIDialog.svelte';
|
|
6
6
|
|
|
7
7
|
interface Props {
|
|
@@ -11,16 +11,35 @@
|
|
|
11
11
|
currentDisplayName?: string;
|
|
12
12
|
/** Existing files used by the increment helper to find the next available `_NN` suffix.
|
|
13
13
|
* Only `fileName` is read, so this accepts any shape with that field
|
|
14
|
-
* (ComponentConfigMeta,
|
|
14
|
+
* (ComponentConfigMeta, ManifestMeta, …). */
|
|
15
15
|
files?: { fileName: string }[];
|
|
16
16
|
/** Dialog title — defaults to "Save As". Overridable so callers can use
|
|
17
|
-
* context-specific framing (e.g. "Save
|
|
17
|
+
* context-specific framing (e.g. "Save Manifest As"). */
|
|
18
18
|
title?: string;
|
|
19
19
|
/** Placeholder shown in the empty input. */
|
|
20
20
|
placeholder?: string;
|
|
21
21
|
/** Error message shown when the user types the reserved "default" name.
|
|
22
|
-
* Default copy references components;
|
|
22
|
+
* Default copy references components; manifests should override. */
|
|
23
23
|
reservedNameMessage?: string;
|
|
24
|
+
/** Optional one-line explanation rendered above the name input. Use when
|
|
25
|
+
* the dialog opens automatically (e.g. as a recovery prompt) so the user
|
|
26
|
+
* understands what's about to be saved and why the prompt appeared. */
|
|
27
|
+
description?: string;
|
|
28
|
+
/** Seed value to use when branching off the protected `default` file.
|
|
29
|
+
* Without this the dialog falls back to incrementing the default's display
|
|
30
|
+
* name (e.g. "Default Theme_01"), which reads as a derivative of the
|
|
31
|
+
* reserved name. Pass a fresh, neutral suggestion (e.g. "My Theme") so
|
|
32
|
+
* the user's first save isn't named after the slot they can't overwrite. */
|
|
33
|
+
branchFromDefaultName?: string;
|
|
34
|
+
/** Active file's basename. Authoritative source for "are we branching from
|
|
35
|
+
* the protected default?" — the displayName-based check below fails when
|
|
36
|
+
* the default's display name doesn't sanitize to "default" (e.g. themes
|
|
37
|
+
* whose default is named "Default Theme" → "default-theme"). */
|
|
38
|
+
currentFileName?: string;
|
|
39
|
+
/** Display names that already belong to protected/system files. Blocked
|
|
40
|
+
* case-insensitively so a user can't shadow the default by reusing its
|
|
41
|
+
* display label even when the sanitized filename would differ. */
|
|
42
|
+
reservedDisplayNames?: string[];
|
|
24
43
|
onsave?: (payload: { displayName: string; fileName: string }) => void;
|
|
25
44
|
}
|
|
26
45
|
|
|
@@ -31,6 +50,10 @@
|
|
|
31
50
|
title = 'Save As',
|
|
32
51
|
placeholder = 'Config name…',
|
|
33
52
|
reservedNameMessage = 'The name "default" is reserved for the core component definition.',
|
|
53
|
+
description = '',
|
|
54
|
+
branchFromDefaultName = '',
|
|
55
|
+
currentFileName = '',
|
|
56
|
+
reservedDisplayNames = [],
|
|
34
57
|
onsave,
|
|
35
58
|
}: Props = $props();
|
|
36
59
|
|
|
@@ -79,10 +102,27 @@
|
|
|
79
102
|
// ends up focused-and-selected, not the button.
|
|
80
103
|
run(() => {
|
|
81
104
|
if (show) {
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
105
|
+
const trimmedCurrent = currentDisplayName.trim().toLowerCase();
|
|
106
|
+
const isReservedDisplay = reservedDisplayNames.some(
|
|
107
|
+
(n) => n.trim().toLowerCase() === trimmedCurrent,
|
|
108
|
+
);
|
|
109
|
+
// Three paths funnel into the branch suggestion:
|
|
110
|
+
// 1. Active is the literal `default` file.
|
|
111
|
+
// 2. Current displayName itself sanitizes to "default".
|
|
112
|
+
// 3. Current displayName collides with a reserved label (e.g. a stray
|
|
113
|
+
// user file already named "Default Theme") — re-seeding with their
|
|
114
|
+
// old name would trip the validator on submit.
|
|
115
|
+
const isFromDefault =
|
|
116
|
+
currentFileName === 'default' ||
|
|
117
|
+
sanitizeFileName(currentDisplayName) === 'default' ||
|
|
118
|
+
isReservedDisplay;
|
|
119
|
+
if (isFromDefault) {
|
|
120
|
+
saveAsName = branchFromDefaultName
|
|
121
|
+
? branchFromDefaultName
|
|
122
|
+
: nextIncrementName(currentDisplayName).displayName;
|
|
123
|
+
} else {
|
|
124
|
+
saveAsName = currentDisplayName;
|
|
125
|
+
}
|
|
86
126
|
setTimeout(() => saveAsInput?.select(), 0);
|
|
87
127
|
}
|
|
88
128
|
});
|
|
@@ -92,6 +132,10 @@
|
|
|
92
132
|
if (sanitizeFileName(trimmed) === 'default') {
|
|
93
133
|
return reservedNameMessage;
|
|
94
134
|
}
|
|
135
|
+
const lowered = trimmed.toLowerCase();
|
|
136
|
+
if (reservedDisplayNames.some((n) => n.trim().toLowerCase() === lowered)) {
|
|
137
|
+
return reservedNameMessage;
|
|
138
|
+
}
|
|
95
139
|
return '';
|
|
96
140
|
})());
|
|
97
141
|
</script>
|
|
@@ -106,6 +150,9 @@
|
|
|
106
150
|
width="360px"
|
|
107
151
|
>
|
|
108
152
|
<div class="save-as-dialog">
|
|
153
|
+
{#if description}
|
|
154
|
+
<p class="save-as-description">{description}</p>
|
|
155
|
+
{/if}
|
|
109
156
|
<div class="save-as-row">
|
|
110
157
|
<input
|
|
111
158
|
class="save-as-input"
|
|
@@ -149,7 +196,7 @@
|
|
|
149
196
|
min-width: 0;
|
|
150
197
|
padding: var(--ui-space-8) var(--ui-space-10);
|
|
151
198
|
background: var(--ui-surface-lowest);
|
|
152
|
-
border: 1px solid var(--ui-border-
|
|
199
|
+
border: 1px solid var(--ui-border-low);
|
|
153
200
|
border-radius: var(--ui-radius-md);
|
|
154
201
|
color: var(--ui-text-primary);
|
|
155
202
|
font-size: var(--ui-font-size-md);
|
|
@@ -163,7 +210,7 @@
|
|
|
163
210
|
width: 2.25rem;
|
|
164
211
|
padding: 0;
|
|
165
212
|
background: var(--ui-surface-low);
|
|
166
|
-
border: 1px solid var(--ui-border-
|
|
213
|
+
border: 1px solid var(--ui-border-low);
|
|
167
214
|
border-radius: var(--ui-radius-md);
|
|
168
215
|
color: var(--ui-text-secondary);
|
|
169
216
|
font-size: var(--ui-font-size-md);
|
|
@@ -173,12 +220,12 @@
|
|
|
173
220
|
|
|
174
221
|
.save-as-increment:hover {
|
|
175
222
|
background: var(--ui-surface);
|
|
176
|
-
border-color: var(--ui-border
|
|
223
|
+
border-color: var(--ui-border);
|
|
177
224
|
color: var(--ui-text-primary);
|
|
178
225
|
}
|
|
179
226
|
|
|
180
227
|
.save-as-input:focus {
|
|
181
|
-
border-color: var(--ui-border-
|
|
228
|
+
border-color: var(--ui-border-high);
|
|
182
229
|
}
|
|
183
230
|
|
|
184
231
|
.save-as-input.invalid,
|
|
@@ -195,4 +242,11 @@
|
|
|
195
242
|
font-size: var(--ui-font-size-xs);
|
|
196
243
|
color: var(--ui-highlight);
|
|
197
244
|
}
|
|
245
|
+
|
|
246
|
+
.save-as-description {
|
|
247
|
+
margin: 0;
|
|
248
|
+
font-size: var(--ui-font-size-xs);
|
|
249
|
+
line-height: 1.5;
|
|
250
|
+
color: var(--ui-text-secondary);
|
|
251
|
+
}
|
|
198
252
|
</style>
|