@motion-proto/live-tokens 0.3.7 → 0.5.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 +1 -1
- package/package.json +11 -9
- package/src/component-editor/BadgeEditor.svelte +24 -22
- package/src/component-editor/CalloutEditor.svelte +3 -3
- package/src/component-editor/CardEditor.svelte +25 -21
- package/src/component-editor/CollapsibleSectionEditor.svelte +27 -25
- package/src/component-editor/CornerBadgeEditor.svelte +37 -35
- package/src/component-editor/DialogEditor.svelte +26 -24
- package/src/component-editor/ImageEditor.svelte +11 -9
- package/src/component-editor/InlineEditActionsEditor.svelte +17 -15
- package/src/component-editor/NotificationEditor.svelte +32 -30
- package/src/component-editor/ProgressBarEditor.svelte +3 -3
- package/src/component-editor/RadioButtonEditor.svelte +31 -29
- package/src/component-editor/SectionDividerEditor.svelte +30 -28
- package/src/component-editor/SegmentedControlEditor.svelte +29 -25
- package/src/component-editor/StandardButtonsEditor.svelte +42 -38
- package/src/component-editor/TabBarEditor.svelte +20 -18
- package/src/component-editor/TableEditor.svelte +4 -4
- package/src/component-editor/TooltipEditor.svelte +11 -9
- package/src/component-editor/registry.ts +2 -2
- package/src/component-editor/scaffolding/AngleDial.svelte +20 -19
- package/src/component-editor/scaffolding/ComponentEditorBase.svelte +44 -20
- package/src/component-editor/scaffolding/ComponentFileManager.svelte +260 -37
- package/src/component-editor/scaffolding/ComponentFileMenu.svelte +41 -29
- package/src/component-editor/scaffolding/ComponentsTab.svelte +7 -3
- package/src/component-editor/scaffolding/CopyFromMenu.svelte +21 -12
- package/src/component-editor/scaffolding/DemoHeader.svelte +13 -4
- package/src/component-editor/scaffolding/DividerEditor.svelte +27 -14
- package/src/component-editor/scaffolding/FieldsetWrapper.svelte +10 -4
- package/src/component-editor/scaffolding/GradientCard.svelte +25 -20
- package/src/component-editor/scaffolding/LinkageChart.svelte +43 -34
- package/src/component-editor/scaffolding/LinkedBlock.svelte +24 -21
- package/src/component-editor/scaffolding/NonStylableConfig.svelte +6 -1
- package/src/component-editor/scaffolding/SaveAsDialog.svelte +39 -35
- package/src/component-editor/scaffolding/ShadowBackdrop.svelte +21 -9
- package/src/component-editor/scaffolding/ShadowBackdropControls.svelte +8 -3
- package/src/component-editor/scaffolding/StateBlock.svelte +30 -13
- package/src/component-editor/scaffolding/TokenLayout.svelte +46 -30
- package/src/component-editor/scaffolding/TypeEditor.svelte +52 -26
- package/src/component-editor/scaffolding/VariantGroup.svelte +81 -48
- package/src/component-editor/scaffolding/componentSectionType.ts +2 -2
- package/src/components/Badge.svelte +45 -26
- package/src/components/Button.svelte +44 -21
- package/src/components/Callout.svelte +17 -12
- package/src/components/Card.svelte +23 -11
- package/src/components/CollapsibleSection.svelte +56 -27
- package/src/components/CornerBadge.svelte +32 -18
- package/src/components/Dialog.svelte +55 -31
- package/src/components/Image.svelte +14 -5
- package/src/components/InlineEditActions.svelte +22 -10
- package/src/components/Notification.svelte +39 -19
- package/src/components/ProgressBar.svelte +27 -17
- package/src/components/RadioButton.svelte +27 -10
- package/src/components/SectionDivider.svelte +34 -26
- package/src/components/SegmentedControl.svelte +23 -9
- package/src/components/TabBar.svelte +23 -10
- package/src/components/Table.svelte +8 -3
- package/src/components/Tooltip.svelte +15 -5
- package/src/lib/ColumnsOverlay.svelte +3 -3
- package/src/lib/LiveEditorOverlay.svelte +73 -36
- package/src/pages/ComponentEditorPage.svelte +17 -13
- package/src/pages/EditorShell.svelte +24 -20
- package/src/styles/form-controls.css +2 -2
- package/src/styles/tokens.css +59 -81
- package/src/ui/BezierCurveEditor.svelte +59 -43
- package/src/ui/ColorEditPanel.svelte +71 -44
- package/src/ui/EditorViewSwitcher.svelte +9 -5
- package/src/ui/FontStackEditor.svelte +16 -15
- package/src/ui/GradientEditor.svelte +42 -33
- package/src/ui/GradientStopPicker.svelte +18 -29
- package/src/ui/PaletteEditor.svelte +238 -212
- package/src/ui/PresetFileManager.svelte +20 -18
- package/src/ui/ProjectFontsSection.svelte +30 -30
- package/src/ui/SurfacesTab.svelte +3 -3
- package/src/ui/TextTab.svelte +2 -2
- package/src/ui/ThemeFileManager.svelte +38 -35
- package/src/ui/Toggle.svelte +11 -9
- package/src/ui/UICopyPopover.svelte +19 -15
- package/src/ui/UIDialog.svelte +48 -30
- package/src/ui/UIFontFamilySelector.svelte +104 -78
- package/src/ui/UIFontSizeSelector.svelte +38 -20
- package/src/ui/UIFontWeightSelector.svelte +33 -13
- package/src/ui/UILineHeightSelector.svelte +33 -13
- package/src/ui/UILinkToggle.svelte +7 -6
- package/src/ui/UIOptionItem.svelte +21 -7
- package/src/ui/UIOptionList.svelte +9 -3
- package/src/ui/UIPaddingSelector.svelte +108 -82
- package/src/ui/UIPaletteSelector.svelte +186 -161
- package/src/ui/UIRadio.svelte +23 -8
- package/src/ui/UIRadioGroup.svelte +9 -8
- package/src/ui/UIRelinkConfirmPopover.svelte +26 -16
- package/src/ui/UITokenSelector.svelte +112 -68
- package/src/ui/UIVariantSelector.svelte +79 -57
- package/src/ui/VariablesTab.svelte +15 -15
- package/src/ui/palette/GradientStopEditor.svelte +45 -26
- package/src/ui/palette/OverridesPanel.svelte +85 -49
- package/src/ui/palette/PaletteBase.svelte +60 -32
- package/src/ui/palette/ScaleCurveEditor.svelte +25 -10
- package/src/ui/sections/ColumnsSection.svelte +13 -13
- package/src/ui/sections/GradientsSection.svelte +12 -9
- package/src/ui/sections/OverlaysSection.svelte +50 -47
- package/src/ui/sections/ShadowsSection.svelte +110 -104
- package/src/ui/sections/TokenScaleTable.svelte +38 -22
- package/src/ui/sections/tokenScales.ts +2 -2
|
@@ -5,16 +5,17 @@
|
|
|
5
5
|
* gradient convention (0deg = pointing up, increasing clockwise) so the
|
|
6
6
|
* displayed line orients the way the gradient axis will paint.
|
|
7
7
|
*/
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
8
|
+
interface Props {
|
|
9
|
+
value?: number;
|
|
10
|
+
label?: string;
|
|
11
|
+
size?: number;
|
|
12
|
+
onchange?: (payload: { value: number }) => void;
|
|
13
|
+
}
|
|
13
14
|
|
|
14
|
-
|
|
15
|
+
let { value = $bindable(0), label = 'Angle', size = 44, onchange }: Props = $props();
|
|
15
16
|
|
|
16
|
-
let dialEl: HTMLDivElement;
|
|
17
|
-
let dragging = false;
|
|
17
|
+
let dialEl: HTMLDivElement | undefined = $state();
|
|
18
|
+
let dragging = $state(false);
|
|
18
19
|
|
|
19
20
|
function normalize(deg: number): number {
|
|
20
21
|
const r = Math.round(deg) % 360;
|
|
@@ -25,11 +26,11 @@
|
|
|
25
26
|
const n = normalize(next);
|
|
26
27
|
if (n === value) return;
|
|
27
28
|
value = n;
|
|
28
|
-
|
|
29
|
+
onchange?.({ value: n });
|
|
29
30
|
}
|
|
30
31
|
|
|
31
32
|
function angleFromEvent(e: PointerEvent): number {
|
|
32
|
-
const rect = dialEl
|
|
33
|
+
const rect = dialEl!.getBoundingClientRect();
|
|
33
34
|
const cx = rect.left + rect.width / 2;
|
|
34
35
|
const cy = rect.top + rect.height / 2;
|
|
35
36
|
const dx = e.clientX - cx;
|
|
@@ -41,7 +42,7 @@
|
|
|
41
42
|
|
|
42
43
|
function onPointerDown(e: PointerEvent) {
|
|
43
44
|
dragging = true;
|
|
44
|
-
dialEl
|
|
45
|
+
dialEl!.setPointerCapture(e.pointerId);
|
|
45
46
|
emit(angleFromEvent(e));
|
|
46
47
|
}
|
|
47
48
|
function onPointerMove(e: PointerEvent) {
|
|
@@ -51,7 +52,7 @@
|
|
|
51
52
|
function onPointerUp(e: PointerEvent) {
|
|
52
53
|
if (!dragging) return;
|
|
53
54
|
dragging = false;
|
|
54
|
-
dialEl
|
|
55
|
+
dialEl!.releasePointerCapture(e.pointerId);
|
|
55
56
|
}
|
|
56
57
|
|
|
57
58
|
function onInputChange(e: Event) {
|
|
@@ -59,7 +60,7 @@
|
|
|
59
60
|
if (Number.isFinite(v)) emit(v);
|
|
60
61
|
}
|
|
61
62
|
|
|
62
|
-
|
|
63
|
+
let indicatorTransform = $derived(`rotate(${value}deg)`);
|
|
63
64
|
</script>
|
|
64
65
|
|
|
65
66
|
<div class="angle-dial-row">
|
|
@@ -71,17 +72,17 @@
|
|
|
71
72
|
class="dial"
|
|
72
73
|
class:dragging
|
|
73
74
|
style="width: {size}px; height: {size}px;"
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
75
|
+
onpointerdown={onPointerDown}
|
|
76
|
+
onpointermove={onPointerMove}
|
|
77
|
+
onpointerup={onPointerUp}
|
|
78
|
+
onpointercancel={onPointerUp}
|
|
78
79
|
role="slider"
|
|
79
80
|
aria-valuemin="0"
|
|
80
81
|
aria-valuemax="360"
|
|
81
82
|
aria-valuenow={value}
|
|
82
83
|
aria-label={label}
|
|
83
84
|
tabindex="0"
|
|
84
|
-
|
|
85
|
+
onkeydown={(e) => {
|
|
85
86
|
if (e.key === 'ArrowLeft' || e.key === 'ArrowDown') { e.preventDefault(); emit(value - 1); }
|
|
86
87
|
else if (e.key === 'ArrowRight' || e.key === 'ArrowUp') { e.preventDefault(); emit(value + 1); }
|
|
87
88
|
}}
|
|
@@ -96,7 +97,7 @@
|
|
|
96
97
|
max="360"
|
|
97
98
|
step="1"
|
|
98
99
|
value={value}
|
|
99
|
-
|
|
100
|
+
onchange={onInputChange}
|
|
100
101
|
/>
|
|
101
102
|
<span class="suffix">°</span>
|
|
102
103
|
</div>
|
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
|
+
import { run } from 'svelte/legacy';
|
|
3
|
+
|
|
2
4
|
import DemoHeader from './DemoHeader.svelte';
|
|
3
5
|
import NonStylableConfig from './NonStylableConfig.svelte';
|
|
4
6
|
import LinkedBlock from './LinkedBlock.svelte';
|
|
@@ -6,36 +8,58 @@
|
|
|
6
8
|
import type { Token } from './types';
|
|
7
9
|
import type { LinkedBlockResult } from './linkedBlock';
|
|
8
10
|
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
interface Props {
|
|
15
|
+
component: string;
|
|
16
|
+
title: string;
|
|
17
|
+
description?: string;
|
|
18
|
+
/** Token list used to drive the reset action in the header. The editor itself
|
|
13
19
|
is responsible for calling `registerComponentSchema` synchronously. */
|
|
14
|
-
|
|
15
|
-
|
|
20
|
+
tokens?: Token[];
|
|
21
|
+
/** Optional linked-block result. When provided, the LinkedBlock is rendered
|
|
16
22
|
and hover highlights propagate to VariantGroup children via context. */
|
|
17
|
-
|
|
18
|
-
|
|
23
|
+
linked?: LinkedBlockResult | null;
|
|
24
|
+
/** Canonical {value,label} list of variants in display order. When provided
|
|
19
25
|
with 2+ entries, a single variant tab strip is rendered that drives which
|
|
20
26
|
VariantGroup is focused. */
|
|
21
|
-
|
|
27
|
+
variants?: { value: string; label: string }[];
|
|
28
|
+
config?: import('svelte').Snippet;
|
|
29
|
+
children?: import('svelte').Snippet<[any]>;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
let {
|
|
33
|
+
component,
|
|
34
|
+
title,
|
|
35
|
+
description = '',
|
|
36
|
+
tokens = [],
|
|
37
|
+
linked = null,
|
|
38
|
+
variants = [],
|
|
39
|
+
config,
|
|
40
|
+
children
|
|
41
|
+
}: Props = $props();
|
|
22
42
|
|
|
23
43
|
const ctx = createEditorContext();
|
|
24
44
|
const { focusedVariant } = ctx;
|
|
25
45
|
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
46
|
+
run(() => {
|
|
47
|
+
ctx._linkedOrder.set(linked?.linkedOrder ?? null);
|
|
48
|
+
});
|
|
49
|
+
let showVariantTabs = $derived(variants.length >= 2);
|
|
50
|
+
run(() => {
|
|
51
|
+
if (showVariantTabs && ($focusedVariant === null || !variants.some((v) => v.value === $focusedVariant))) {
|
|
52
|
+
focusedVariant.set(variants[0].value);
|
|
53
|
+
}
|
|
54
|
+
});
|
|
55
|
+
let resetVariables = $derived(tokens.map((t) => t.variable));
|
|
32
56
|
</script>
|
|
33
57
|
|
|
34
58
|
<div class="demo-block">
|
|
35
59
|
<DemoHeader {component} {title} {description} {resetVariables} />
|
|
36
|
-
{#if
|
|
60
|
+
{#if config}
|
|
37
61
|
<NonStylableConfig>
|
|
38
|
-
|
|
62
|
+
{@render config?.()}
|
|
39
63
|
</NonStylableConfig>
|
|
40
64
|
{/if}
|
|
41
65
|
{#if showVariantTabs}
|
|
@@ -47,14 +71,14 @@
|
|
|
47
71
|
class:active={opt.value === $focusedVariant}
|
|
48
72
|
role="tab"
|
|
49
73
|
aria-selected={opt.value === $focusedVariant}
|
|
50
|
-
|
|
74
|
+
onclick={() => focusedVariant.set(opt.value)}
|
|
51
75
|
>{opt.label}</button>
|
|
52
76
|
{/each}
|
|
53
77
|
</div>
|
|
54
78
|
{/if}
|
|
55
|
-
|
|
79
|
+
{@render children?.({ focusedVariant: $focusedVariant, })}
|
|
56
80
|
{#if linked}
|
|
57
|
-
<LinkedBlock {component} {linked}
|
|
81
|
+
<LinkedBlock {component} {linked} />
|
|
58
82
|
{/if}
|
|
59
83
|
</div>
|
|
60
84
|
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
<script
|
|
1
|
+
<script module lang="ts">
|
|
2
2
|
declare const __PROJECT_ROOT__: string | undefined;
|
|
3
3
|
</script>
|
|
4
4
|
|
|
@@ -27,18 +27,25 @@
|
|
|
27
27
|
import ComponentFileMenu from './ComponentFileMenu.svelte';
|
|
28
28
|
import SaveAsDialog from './SaveAsDialog.svelte';
|
|
29
29
|
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
interface Props {
|
|
34
|
+
/** Which component this manager controls (e.g. "button"). */
|
|
35
|
+
component: string;
|
|
36
|
+
/** Display name shown at the start of the bar (e.g. "Segmented Control"). */
|
|
37
|
+
title?: string;
|
|
38
|
+
/** When provided, renders a Reset button that reverts the component to its
|
|
35
39
|
currently-loaded config file (discarding unsaved edits). To switch
|
|
36
40
|
configs or return to default, use the File menu. */
|
|
37
|
-
|
|
41
|
+
resetVariables?: string[] | null;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
let { component, title = '', resetVariables = null }: Props = $props();
|
|
38
45
|
|
|
39
46
|
const projectRoot: string =
|
|
40
47
|
typeof __PROJECT_ROOT__ !== 'undefined' ? (__PROJECT_ROOT__ ?? '') : '';
|
|
41
|
-
|
|
48
|
+
let sourceFile = $derived(componentSourceFile(component));
|
|
42
49
|
|
|
43
50
|
type SaveStatus = 'idle' | 'saving' | 'saved' | 'error';
|
|
44
51
|
let saveStatus: SaveStatus = 'idle';
|
|
@@ -49,15 +56,58 @@
|
|
|
49
56
|
saveStatus = state;
|
|
50
57
|
setTimeout(() => (saveStatus = 'idle'), 2000);
|
|
51
58
|
}
|
|
52
|
-
let files: ComponentConfigMeta[] = [];
|
|
53
|
-
let activeFileName = 'default';
|
|
54
|
-
let currentDisplayName = 'Default';
|
|
55
|
-
let saveAsDialog = false;
|
|
59
|
+
let files: ComponentConfigMeta[] = $state([]);
|
|
60
|
+
let activeFileName = $state('default');
|
|
61
|
+
let currentDisplayName = $state('Default');
|
|
62
|
+
let saveAsDialog = $state(false);
|
|
56
63
|
|
|
57
|
-
let productionInfo
|
|
64
|
+
let productionInfo = $state<ComponentProductionInfo | null>(null);
|
|
58
65
|
type ProductionStatus = 'idle' | 'updating' | 'done' | 'error';
|
|
59
|
-
let productionUpdateStatus: ProductionStatus = 'idle';
|
|
60
|
-
let adoptFeedback = '';
|
|
66
|
+
let productionUpdateStatus: ProductionStatus = $state('idle');
|
|
67
|
+
let adoptFeedback = $state('');
|
|
68
|
+
|
|
69
|
+
let infoOpen = $state(false);
|
|
70
|
+
let infoBtnEl = $state<HTMLButtonElement | undefined>(undefined);
|
|
71
|
+
let infoPopoverEl = $state<HTMLDivElement | undefined>(undefined);
|
|
72
|
+
let infoPopoverReady = $state(false);
|
|
73
|
+
|
|
74
|
+
/** Anchor the fixed-position popover centered below the info button.
|
|
75
|
+
Uses position: fixed so it escapes the sticky header's stacking
|
|
76
|
+
context (which was letting the side panels paint over it). */
|
|
77
|
+
function positionInfoPopover(): void {
|
|
78
|
+
const btn = infoBtnEl;
|
|
79
|
+
const pop = infoPopoverEl;
|
|
80
|
+
if (!btn || !pop) return;
|
|
81
|
+
const br = btn.getBoundingClientRect();
|
|
82
|
+
const pr = pop.getBoundingClientRect();
|
|
83
|
+
const margin = 8;
|
|
84
|
+
let left = br.left + br.width / 2 - pr.width / 2;
|
|
85
|
+
const vw = window.innerWidth;
|
|
86
|
+
if (left < margin) left = margin;
|
|
87
|
+
if (left + pr.width > vw - margin) left = vw - margin - pr.width;
|
|
88
|
+
pop.style.left = `${left}px`;
|
|
89
|
+
pop.style.top = `${br.bottom + margin}px`;
|
|
90
|
+
infoPopoverReady = true;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
$effect(() => {
|
|
94
|
+
if (!infoOpen) {
|
|
95
|
+
infoPopoverReady = false;
|
|
96
|
+
return;
|
|
97
|
+
}
|
|
98
|
+
// Two rAFs: first so Svelte mounts the popover and the bind: ref is set,
|
|
99
|
+
// second so its rendered width is measurable before we anchor it.
|
|
100
|
+
let raf1 = requestAnimationFrame(() => {
|
|
101
|
+
raf1 = requestAnimationFrame(positionInfoPopover);
|
|
102
|
+
});
|
|
103
|
+
window.addEventListener('scroll', positionInfoPopover, true);
|
|
104
|
+
window.addEventListener('resize', positionInfoPopover);
|
|
105
|
+
return () => {
|
|
106
|
+
cancelAnimationFrame(raf1);
|
|
107
|
+
window.removeEventListener('scroll', positionInfoPopover, true);
|
|
108
|
+
window.removeEventListener('resize', positionInfoPopover);
|
|
109
|
+
};
|
|
110
|
+
});
|
|
61
111
|
|
|
62
112
|
/** Same idle-after-2s pattern for the production-update flash. */
|
|
63
113
|
function flashProductionStatus(state: Exclude<ProductionStatus, 'idle'>) {
|
|
@@ -68,9 +118,9 @@
|
|
|
68
118
|
}, 2000);
|
|
69
119
|
}
|
|
70
120
|
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
121
|
+
let compDirty = $derived($componentDirty[component] ?? false);
|
|
122
|
+
let isApplied = $derived(!!productionInfo && productionInfo.fileName === activeFileName && !compDirty);
|
|
123
|
+
let resetDirty = $derived(!!resetVariables && compDirty);
|
|
74
124
|
|
|
75
125
|
async function refreshFiles() {
|
|
76
126
|
// safeFetch returns null on dev-server unavailable / non-2xx — silently
|
|
@@ -99,16 +149,28 @@
|
|
|
99
149
|
await refreshFiles();
|
|
100
150
|
await refreshProduction();
|
|
101
151
|
window.addEventListener('keydown', handleKeydown);
|
|
152
|
+
document.addEventListener('mousedown', handleDocumentMousedown, true);
|
|
102
153
|
});
|
|
103
154
|
|
|
104
155
|
onDestroy(() => {
|
|
105
156
|
window.removeEventListener('keydown', handleKeydown);
|
|
157
|
+
document.removeEventListener('mousedown', handleDocumentMousedown, true);
|
|
106
158
|
});
|
|
107
159
|
|
|
108
160
|
function handleKeydown(e: KeyboardEvent) {
|
|
109
161
|
if ((e.metaKey || e.ctrlKey) && e.key === 's') {
|
|
110
162
|
e.preventDefault();
|
|
111
163
|
handleSave();
|
|
164
|
+
} else if (e.key === 'Escape' && infoOpen) {
|
|
165
|
+
infoOpen = false;
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
function handleDocumentMousedown(e: MouseEvent) {
|
|
170
|
+
if (!infoOpen) return;
|
|
171
|
+
const target = e.target as Element | null;
|
|
172
|
+
if (target && !target.closest('.cfm-info-btn, .cfm-info-popover')) {
|
|
173
|
+
infoOpen = false;
|
|
112
174
|
}
|
|
113
175
|
}
|
|
114
176
|
|
|
@@ -166,8 +228,8 @@
|
|
|
166
228
|
saveAsDialog = true;
|
|
167
229
|
}
|
|
168
230
|
|
|
169
|
-
async function confirmSaveAs(
|
|
170
|
-
const { displayName, fileName } =
|
|
231
|
+
async function confirmSaveAs(detail: { displayName: string; fileName: string }) {
|
|
232
|
+
const { displayName, fileName } = detail;
|
|
171
233
|
saveStatus = 'saving';
|
|
172
234
|
try {
|
|
173
235
|
await persist(fileName, displayName);
|
|
@@ -178,8 +240,7 @@
|
|
|
178
240
|
}
|
|
179
241
|
}
|
|
180
242
|
|
|
181
|
-
async function handleLoad(
|
|
182
|
-
const file = e.detail;
|
|
243
|
+
async function handleLoad(file: ComponentConfigMeta) {
|
|
183
244
|
// Multi-step service flow (load + set-active) — if any network call
|
|
184
245
|
// fails, the dialog is already closed and the local state stays on the
|
|
185
246
|
// previous selection. Silent by design; the same boot resilience that
|
|
@@ -195,8 +256,7 @@
|
|
|
195
256
|
}
|
|
196
257
|
}
|
|
197
258
|
|
|
198
|
-
async function handleDelete(
|
|
199
|
-
const file = e.detail;
|
|
259
|
+
async function handleDelete(file: ComponentConfigMeta) {
|
|
200
260
|
if (file.fileName === 'default') return;
|
|
201
261
|
// Multi-step service flow (delete + reload-default-on-active-removal).
|
|
202
262
|
// Silent by design — see handleLoad.
|
|
@@ -268,7 +328,7 @@
|
|
|
268
328
|
title="Open {sourceFile} in VS Code"
|
|
269
329
|
>
|
|
270
330
|
<i class="fas fa-code"></i>
|
|
271
|
-
<span>
|
|
331
|
+
<span>Show component source</span>
|
|
272
332
|
</a>
|
|
273
333
|
{/if}
|
|
274
334
|
</div>
|
|
@@ -300,16 +360,16 @@
|
|
|
300
360
|
{component}
|
|
301
361
|
{files}
|
|
302
362
|
{activeFileName}
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
363
|
+
onsave={handleSave}
|
|
364
|
+
onsaveAs={openSaveAs}
|
|
365
|
+
onopenLoad={refreshFiles}
|
|
366
|
+
onload={handleLoad}
|
|
367
|
+
ondelete={handleDelete}
|
|
308
368
|
/>
|
|
309
369
|
{#if resetVariables}
|
|
310
370
|
<button
|
|
311
371
|
class="cfm-btn reset-btn"
|
|
312
|
-
|
|
372
|
+
onclick={handleReset}
|
|
313
373
|
disabled={!resetDirty}
|
|
314
374
|
title="Revert unsaved changes to {currentDisplayName}"
|
|
315
375
|
>
|
|
@@ -342,7 +402,7 @@
|
|
|
342
402
|
class:saving={productionUpdateStatus === 'updating'}
|
|
343
403
|
class:saved={productionUpdateStatus === 'done'}
|
|
344
404
|
class:error={productionUpdateStatus === 'error'}
|
|
345
|
-
|
|
405
|
+
onclick={handleUpdateProduction}
|
|
346
406
|
disabled={productionUpdateStatus === 'updating' || !productionInfo || (productionInfo.fileName === activeFileName && !compDirty)}
|
|
347
407
|
title={!productionInfo
|
|
348
408
|
? ''
|
|
@@ -359,6 +419,50 @@
|
|
|
359
419
|
{#if productionUpdateStatus === 'idle'}Adopt{:else if productionUpdateStatus === 'updating'}Adopting{:else if productionUpdateStatus === 'done'}Adopted{:else}Error{/if}
|
|
360
420
|
</span>
|
|
361
421
|
</button>
|
|
422
|
+
<button
|
|
423
|
+
type="button"
|
|
424
|
+
class="cfm-info-btn"
|
|
425
|
+
aria-label="About Save and Adopt"
|
|
426
|
+
aria-expanded={infoOpen}
|
|
427
|
+
bind:this={infoBtnEl}
|
|
428
|
+
onclick={() => (infoOpen = !infoOpen)}
|
|
429
|
+
>
|
|
430
|
+
<i class="fas fa-circle-info"></i>
|
|
431
|
+
</button>
|
|
432
|
+
{#if infoOpen}
|
|
433
|
+
<div
|
|
434
|
+
class="cfm-info-popover"
|
|
435
|
+
class:ready={infoPopoverReady}
|
|
436
|
+
role="dialog"
|
|
437
|
+
aria-label="About Save and Adopt"
|
|
438
|
+
bind:this={infoPopoverEl}
|
|
439
|
+
>
|
|
440
|
+
<header class="cfm-info-header">
|
|
441
|
+
<span class="cfm-info-title">Component Configuration</span>
|
|
442
|
+
<button
|
|
443
|
+
type="button"
|
|
444
|
+
class="cfm-info-close"
|
|
445
|
+
aria-label="Close"
|
|
446
|
+
onclick={() => (infoOpen = false)}
|
|
447
|
+
>
|
|
448
|
+
<i class="fas fa-xmark"></i>
|
|
449
|
+
</button>
|
|
450
|
+
</header>
|
|
451
|
+
<div class="cfm-info-body">
|
|
452
|
+
<p>
|
|
453
|
+
Editor and Prod both use a saved file. When they share the
|
|
454
|
+
<em>same</em> file, <strong>Saved changes</strong> go to into production
|
|
455
|
+
immediately. They are sharing the configuration.
|
|
456
|
+
</p>
|
|
457
|
+
<p>
|
|
458
|
+
To experiment without changing production,<strong>Save As</strong> a new file first.
|
|
459
|
+
</p>
|
|
460
|
+
<p>
|
|
461
|
+
When ready, click <strong>Adopt</strong> to use the new file on prod.
|
|
462
|
+
</p>
|
|
463
|
+
</div>
|
|
464
|
+
</div>
|
|
465
|
+
{/if}
|
|
362
466
|
{#if adoptFeedback}
|
|
363
467
|
<span class="cfm-feedback" aria-live="polite">{adoptFeedback}</span>
|
|
364
468
|
{/if}
|
|
@@ -371,7 +475,7 @@
|
|
|
371
475
|
bind:show={saveAsDialog}
|
|
372
476
|
{currentDisplayName}
|
|
373
477
|
{files}
|
|
374
|
-
|
|
478
|
+
onsave={confirmSaveAs}
|
|
375
479
|
/>
|
|
376
480
|
|
|
377
481
|
<style>
|
|
@@ -413,13 +517,15 @@
|
|
|
413
517
|
.source-link {
|
|
414
518
|
display: inline-flex;
|
|
415
519
|
align-items: center;
|
|
416
|
-
gap: var(--ui-space-
|
|
417
|
-
|
|
520
|
+
gap: var(--ui-space-6);
|
|
521
|
+
height: 26px;
|
|
522
|
+
padding: 0 14px;
|
|
418
523
|
font-size: var(--ui-font-size-xs);
|
|
524
|
+
font-weight: 500;
|
|
419
525
|
color: var(--ui-text-secondary);
|
|
420
526
|
text-decoration: none;
|
|
421
527
|
border: 1px solid var(--ui-border-default);
|
|
422
|
-
border-radius:
|
|
528
|
+
border-radius: 999px;
|
|
423
529
|
transition: all var(--ui-transition-fast);
|
|
424
530
|
}
|
|
425
531
|
|
|
@@ -553,8 +659,10 @@
|
|
|
553
659
|
}
|
|
554
660
|
|
|
555
661
|
/* actions cluster — sits directly next to the filename pill so the
|
|
556
|
-
buttons stay near the input and don't drift under the open editor panel
|
|
662
|
+
buttons stay near the input and don't drift under the open editor panel.
|
|
663
|
+
position: relative anchors the info popover below the cluster. */
|
|
557
664
|
.cfm-actions {
|
|
665
|
+
position: relative;
|
|
558
666
|
display: flex;
|
|
559
667
|
align-items: center;
|
|
560
668
|
gap: var(--ui-space-6);
|
|
@@ -674,6 +782,121 @@
|
|
|
674
782
|
50% { box-shadow: 0 0 0 5px color-mix(in srgb, var(--ui-highlight) 10%, transparent); }
|
|
675
783
|
}
|
|
676
784
|
|
|
785
|
+
/* info button — naked icon, no chrome. The icon itself carries the
|
|
786
|
+
affordance; hover/active simply brighten its color. */
|
|
787
|
+
.cfm-info-btn {
|
|
788
|
+
display: inline-flex;
|
|
789
|
+
align-items: center;
|
|
790
|
+
justify-content: center;
|
|
791
|
+
padding: var(--ui-space-2) var(--ui-space-4);
|
|
792
|
+
background: transparent;
|
|
793
|
+
border: 0;
|
|
794
|
+
color: var(--ui-text-tertiary);
|
|
795
|
+
font-size: 1.15rem;
|
|
796
|
+
line-height: 1;
|
|
797
|
+
cursor: pointer;
|
|
798
|
+
transition: color var(--ui-transition-fast);
|
|
799
|
+
}
|
|
800
|
+
|
|
801
|
+
.cfm-info-btn:hover,
|
|
802
|
+
.cfm-info-btn[aria-expanded='true'] {
|
|
803
|
+
color: var(--ui-text-primary);
|
|
804
|
+
}
|
|
805
|
+
|
|
806
|
+
.cfm-info-popover {
|
|
807
|
+
/* Fixed positioning escapes the sticky header's stacking context,
|
|
808
|
+
so the popover paints over the side panels. JS in this file
|
|
809
|
+
anchors it centered below the info button. */
|
|
810
|
+
position: fixed;
|
|
811
|
+
top: 0;
|
|
812
|
+
left: 0;
|
|
813
|
+
width: 22rem;
|
|
814
|
+
max-width: calc(100vw - var(--ui-space-24));
|
|
815
|
+
padding: 0;
|
|
816
|
+
background: var(--ui-surface-higher);
|
|
817
|
+
border: 1px solid var(--ui-border-medium);
|
|
818
|
+
border-radius: var(--ui-radius-lg);
|
|
819
|
+
box-shadow: var(--ui-shadow-lg);
|
|
820
|
+
z-index: 1000;
|
|
821
|
+
color: var(--ui-text-secondary);
|
|
822
|
+
font-family: var(--ui-font-family, system-ui, sans-serif);
|
|
823
|
+
overflow: hidden;
|
|
824
|
+
visibility: hidden;
|
|
825
|
+
animation: cfm-info-in 140ms ease-out;
|
|
826
|
+
}
|
|
827
|
+
|
|
828
|
+
.cfm-info-popover.ready {
|
|
829
|
+
visibility: visible;
|
|
830
|
+
}
|
|
831
|
+
|
|
832
|
+
.cfm-info-header {
|
|
833
|
+
display: flex;
|
|
834
|
+
align-items: center;
|
|
835
|
+
justify-content: space-between;
|
|
836
|
+
gap: var(--ui-space-8);
|
|
837
|
+
padding: var(--ui-space-10) var(--ui-space-12) var(--ui-space-10) var(--ui-space-16);
|
|
838
|
+
border-bottom: 1px solid var(--ui-border-subtle);
|
|
839
|
+
}
|
|
840
|
+
|
|
841
|
+
.cfm-info-title {
|
|
842
|
+
color: var(--ui-text-primary);
|
|
843
|
+
font-size: var(--ui-font-size-sm);
|
|
844
|
+
font-weight: var(--ui-font-weight-semibold);
|
|
845
|
+
letter-spacing: -0.01em;
|
|
846
|
+
line-height: 1.2;
|
|
847
|
+
}
|
|
848
|
+
|
|
849
|
+
.cfm-info-close {
|
|
850
|
+
display: inline-flex;
|
|
851
|
+
align-items: center;
|
|
852
|
+
justify-content: center;
|
|
853
|
+
width: var(--ui-space-24);
|
|
854
|
+
height: var(--ui-space-24);
|
|
855
|
+
padding: 0;
|
|
856
|
+
background: transparent;
|
|
857
|
+
border: 0;
|
|
858
|
+
border-radius: var(--ui-radius-sm);
|
|
859
|
+
color: var(--ui-text-tertiary);
|
|
860
|
+
font-size: var(--ui-font-size-xs);
|
|
861
|
+
line-height: 1;
|
|
862
|
+
cursor: pointer;
|
|
863
|
+
transition: color var(--ui-transition-fast), background var(--ui-transition-fast);
|
|
864
|
+
}
|
|
865
|
+
|
|
866
|
+
.cfm-info-close:hover {
|
|
867
|
+
color: var(--ui-text-primary);
|
|
868
|
+
background: var(--ui-hover);
|
|
869
|
+
}
|
|
870
|
+
|
|
871
|
+
.cfm-info-body {
|
|
872
|
+
padding: var(--ui-space-16);
|
|
873
|
+
}
|
|
874
|
+
|
|
875
|
+
.cfm-info-popover p {
|
|
876
|
+
margin: 0 0 var(--ui-space-12) 0;
|
|
877
|
+
font-size: var(--ui-font-size-xs);
|
|
878
|
+
line-height: 1.55;
|
|
879
|
+
}
|
|
880
|
+
|
|
881
|
+
.cfm-info-popover p:last-child {
|
|
882
|
+
margin-bottom: 0;
|
|
883
|
+
}
|
|
884
|
+
|
|
885
|
+
.cfm-info-popover strong {
|
|
886
|
+
color: var(--ui-text-primary);
|
|
887
|
+
font-weight: var(--ui-font-weight-semibold);
|
|
888
|
+
}
|
|
889
|
+
|
|
890
|
+
.cfm-info-popover em {
|
|
891
|
+
font-style: italic;
|
|
892
|
+
color: var(--ui-text-primary);
|
|
893
|
+
}
|
|
894
|
+
|
|
895
|
+
@keyframes cfm-info-in {
|
|
896
|
+
from { opacity: 0; transform: translateY(-3px); }
|
|
897
|
+
to { opacity: 1; transform: translateY(0); }
|
|
898
|
+
}
|
|
899
|
+
|
|
677
900
|
/* narrow viewports: hide button text, keep icons visible */
|
|
678
901
|
@media (max-width: 640px) {
|
|
679
902
|
.cfm-btn span { display: none; }
|