@widgetic/creator 0.3.59 → 0.3.60
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/dist/CreatorApp.svelte +1412 -284
- package/dist/components/WidgetDetails.svelte +10 -34
- package/dist/components/WidgetDetails.svelte.d.ts +1 -0
- package/dist/components/WidgetShapeLivePreviews.svelte +414 -43
- package/dist/components/WidgetShapeLivePreviews.svelte.d.ts +53 -29
- package/package.json +1 -1
|
@@ -3496,9 +3496,15 @@
|
|
|
3496
3496
|
{/if}
|
|
3497
3497
|
|
|
3498
3498
|
<div
|
|
3499
|
-
class="widget-details-
|
|
3499
|
+
class="widget-details-header-actions relative z-[1] ml-auto flex items-center gap-2 shrink-0"
|
|
3500
3500
|
bind:this={widgetDetailsDebugButtonsEl}
|
|
3501
3501
|
>
|
|
3502
|
+
<div class="widget-details-header-meta flex items-center gap-2 shrink-0 min-w-0">
|
|
3503
|
+
<slot name="header-meta" />
|
|
3504
|
+
<div class="widget-info-version-history-slot relative z-20 shrink-0">
|
|
3505
|
+
<slot name="version-history" />
|
|
3506
|
+
</div>
|
|
3507
|
+
</div>
|
|
3502
3508
|
{#if SHOW_PANEL_PREVIEW_COLUMN}
|
|
3503
3509
|
<HoverTooltip text={previewColumnCollapsed ? 'Open live preview' : 'Close live preview'}>
|
|
3504
3510
|
<button
|
|
@@ -3533,7 +3539,7 @@
|
|
|
3533
3539
|
</div>
|
|
3534
3540
|
|
|
3535
3541
|
<!-- Widget Details Content: Two-column layout -->
|
|
3536
|
-
<div class="widget-details-content-wrapper relative m-
|
|
3542
|
+
<div class="widget-details-content-wrapper relative m-0 min-w-[460px] min-h-0 flex-1 overflow-hidden">
|
|
3537
3543
|
<div class="widget-details-two-col flex h-full min-h-0 min-w-[460px] gap-0 overflow-hidden">
|
|
3538
3544
|
|
|
3539
3545
|
<!-- ═══ LEFT COLUMN: Step Content (scrollable) ═══ -->
|
|
@@ -3541,43 +3547,13 @@
|
|
|
3541
3547
|
class="widget-details-left-col flex flex-col min-w-[460px] min-h-0 overflow-hidden relative"
|
|
3542
3548
|
style="width: {previewColumnCollapsed ? 100 : leftColPercent}%; flex: 0 0 {previewColumnCollapsed ? 100 : leftColPercent}%;"
|
|
3543
3549
|
>
|
|
3544
|
-
<div class="widget-details-left-border absolute inset-y-0 left-0 right-0 border border-gray-300 rounded-
|
|
3550
|
+
<div class="widget-details-left-border absolute inset-y-0 left-0 right-0 border border-gray-300 rounded-none pointer-events-none z-10"></div>
|
|
3545
3551
|
|
|
3546
3552
|
{#if selectedWidgetId}
|
|
3547
3553
|
<div class="widget-details-left-stack relative z-0 flex flex-col h-full min-h-0 min-w-[460px] overflow-hidden">
|
|
3548
|
-
<!-- Sticky Widget Info Bar (always visible above scrollable content) -->
|
|
3549
|
-
<div class="widget-info-sticky-bar relative flex items-center justify-between px-3 py-1.5 border-b border-gray-200 bg-gray-50 rounded-t-lg shrink-0 gap-2 min-w-0 z-20 overflow-visible">
|
|
3550
|
-
<div class="widget-info-name flex items-center min-w-0 gap-1 text-sm text-gray-700">
|
|
3551
|
-
<!-- svelte-ignore a11y-click-events-have-key-events -->
|
|
3552
|
-
<!-- svelte-ignore a11y-no-static-element-interactions -->
|
|
3553
|
-
<span
|
|
3554
|
-
class="widget-id-label text-[11px] font-mono text-gray-500 cursor-pointer hover:text-gray-700 truncate"
|
|
3555
|
-
title="Click to copy widget ID"
|
|
3556
|
-
onclick={() => { navigator.clipboard.writeText(selectedWidgetId); showToast('success', 'Widget ID copied', { duration: 2000 }); }}
|
|
3557
|
-
>Widget id: {selectedWidgetId}</span>
|
|
3558
|
-
</div>
|
|
3559
|
-
<div class="widget-info-meta flex items-center gap-2 shrink-0">
|
|
3560
|
-
{#if saveStatus === 'saving'}
|
|
3561
|
-
<span class="text-[10px] px-1.5 py-0.5 rounded bg-blue-100 text-blue-700 animate-pulse">Saving...</span>
|
|
3562
|
-
{:else if saveStatus === 'saved'}
|
|
3563
|
-
<span class="text-[10px] px-1.5 py-0.5 rounded bg-green-100 text-green-700">Saved</span>
|
|
3564
|
-
{:else if saveStatus === 'error'}
|
|
3565
|
-
<span class="text-[10px] px-1.5 py-0.5 rounded bg-red-100 text-red-700">Save Error</span>
|
|
3566
|
-
{/if}
|
|
3567
|
-
{#if debugMode}
|
|
3568
|
-
<span class="text-[10px] text-gray-500">
|
|
3569
|
-
Commit: <span class="font-mono font-medium text-gray-600" title={lastCommitId || ''}>{lastCommitId ? lastCommitId.substring(0, 8) : '—'}</span>
|
|
3570
|
-
</span>
|
|
3571
|
-
{/if}
|
|
3572
|
-
<div class="widget-info-version-history-slot relative z-20 shrink-0">
|
|
3573
|
-
<slot name="version-history" />
|
|
3574
|
-
</div>
|
|
3575
|
-
</div>
|
|
3576
|
-
</div>
|
|
3577
|
-
|
|
3578
3554
|
<!-- Scrollable step content -->
|
|
3579
3555
|
<div
|
|
3580
|
-
class="widget-details-left-content flex flex-col flex-1 min-h-0 relative rounded-
|
|
3556
|
+
class="widget-details-left-content flex flex-col flex-1 min-h-0 relative rounded-none overflow-hidden overflow-x-hidden"
|
|
3581
3557
|
bind:this={contentContainerEl}
|
|
3582
3558
|
>
|
|
3583
3559
|
<div class="actions-panel widget-details-actions-panel relative flex flex-col flex-1 min-h-0">
|
|
@@ -3,30 +3,98 @@
|
|
|
3
3
|
* Live iframe + chrome over Fabric WidgetShapes (W3).
|
|
4
4
|
* Body drag is locked on the shape; move via the top drag bar so the iframe stays interactive.
|
|
5
5
|
*/
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
6
|
+
import { onMount } from 'svelte';
|
|
7
|
+
|
|
8
|
+
interface VersionOption {
|
|
9
|
+
id: string;
|
|
10
|
+
/** Full list label, e.g. "Version 9". */
|
|
11
|
+
label: string;
|
|
12
|
+
/** Compact trigger label, e.g. "v9". */
|
|
13
|
+
shortLabel?: string;
|
|
14
|
+
/** Published date/time shown in the list. */
|
|
15
|
+
dateLabel?: string;
|
|
16
|
+
/** True when this option is the live published version. */
|
|
17
|
+
isLive?: boolean;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
interface Props {
|
|
21
|
+
class?: string;
|
|
22
|
+
style?: string;
|
|
23
|
+
enabled?: boolean;
|
|
24
|
+
/** Screen-space boxes for widget shapes currently on the canvas. */
|
|
25
|
+
getRects?:
|
|
26
|
+
| (() => Array<{ widgetId: string; left: number; top: number; width: number; height: number }>)
|
|
27
|
+
| null;
|
|
28
|
+
/** Resolve a live preview URL for a widget (CDN published or Worker preview). */
|
|
29
|
+
resolveUrl?: ((widgetId: string) => string | null) | null;
|
|
30
|
+
/** Currently selected WidgetShape id (highlights chrome). */
|
|
31
|
+
selectedWidgetId?: string | null;
|
|
32
|
+
/** Translate shape by screen-pixel delta. */
|
|
33
|
+
onTranslate?: ((widgetId: string, dxScreen: number, dyScreen: number) => void) | null;
|
|
34
|
+
/** Fired once when a chrome drag ends (mark canvas dirty / history). */
|
|
35
|
+
onTranslateEnd?: ((widgetId: string) => void) | null;
|
|
36
|
+
/** Select the Fabric WidgetShape (e.g. when pressing the drag bar). */
|
|
37
|
+
onSelectShape?: ((widgetId: string) => void) | null;
|
|
38
|
+
/** Open Widget Details / chat for this widget. */
|
|
39
|
+
onOpenChat?: ((widgetId: string) => void) | null;
|
|
40
|
+
/** Resolve display title for the chrome bar. */
|
|
41
|
+
resolveTitle?: ((widgetId: string) => string | null) | null;
|
|
42
|
+
/** Current version label, e.g. "v3" or "Preview". */
|
|
43
|
+
resolveVersionLabel?: ((widgetId: string) => string | null) | null;
|
|
44
|
+
/** Available version options for the select (null/empty → show label only). */
|
|
45
|
+
resolveVersionOptions?: ((widgetId: string) => Array<VersionOption> | null) | null;
|
|
46
|
+
/** Currently selected version option id. */
|
|
47
|
+
resolveVersionSelectedId?: ((widgetId: string) => string | null) | null;
|
|
48
|
+
onVersionChange?: ((widgetId: string, versionId: string) => void) | null;
|
|
49
|
+
/** Promote a published version to Live (same as chat Set live). */
|
|
50
|
+
onSetLive?: ((widgetId: string, version: number) => void) | null;
|
|
51
|
+
/** Version currently being set live (disables that row's button). */
|
|
52
|
+
settingLiveVersion?: number | null;
|
|
53
|
+
/** Optional notify after local iframe reload (does not capture screenshots). */
|
|
54
|
+
onRefresh?: ((widgetId: string) => void) | null;
|
|
55
|
+
/** Bump when widget names / publish state change so chrome re-renders. */
|
|
56
|
+
titleRevision?: number;
|
|
57
|
+
/** Whether this widget has unpublished codegen ahead of Live (show Publish). */
|
|
58
|
+
resolveCanPublish?: ((widgetId: string) => boolean) | null;
|
|
59
|
+
/** Publish HEAD as a new live version (shape bar Publish button). */
|
|
60
|
+
onPublish?: ((widgetId: string) => void) | null;
|
|
61
|
+
/** True while publish is in flight for this widget. */
|
|
62
|
+
resolveIsPublishing?: ((widgetId: string) => boolean) | null;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
let {
|
|
66
|
+
class: className = '',
|
|
67
|
+
style = '',
|
|
68
|
+
enabled = true,
|
|
69
|
+
getRects = null,
|
|
70
|
+
resolveUrl = null,
|
|
71
|
+
selectedWidgetId = null,
|
|
72
|
+
onTranslate = null,
|
|
73
|
+
onTranslateEnd = null,
|
|
74
|
+
onSelectShape = null,
|
|
75
|
+
onOpenChat = null,
|
|
76
|
+
resolveTitle = null,
|
|
77
|
+
resolveVersionLabel = null,
|
|
78
|
+
resolveVersionOptions = null,
|
|
79
|
+
resolveVersionSelectedId = null,
|
|
80
|
+
onVersionChange = null,
|
|
81
|
+
onSetLive = null,
|
|
82
|
+
settingLiveVersion = null,
|
|
83
|
+
onRefresh = null,
|
|
84
|
+
titleRevision = 0,
|
|
85
|
+
resolveCanPublish = null,
|
|
86
|
+
onPublish = null,
|
|
87
|
+
resolveIsPublishing = null,
|
|
88
|
+
}: Props = $props();
|
|
89
|
+
|
|
90
|
+
const DRAG_BAR_H = 30;
|
|
29
91
|
const BAR_SHAPE_GAP = 4;
|
|
92
|
+
const SHOW_TITLE_MIN_WIDTH = 160;
|
|
93
|
+
/** Minimum chrome width when expanded so version + Live/Publish controls fit. */
|
|
94
|
+
const MIN_EXPANDED_CHROME_WIDTH = 220;
|
|
95
|
+
const IFRAME_INSET_PX = 2;
|
|
96
|
+
/** ~5 version rows before the menu scrolls (matches chat history panel). */
|
|
97
|
+
const VERSION_MENU_MAX_H = '12.5rem';
|
|
30
98
|
|
|
31
99
|
type ShapeRect = {
|
|
32
100
|
widgetId: string;
|
|
@@ -37,8 +105,15 @@
|
|
|
37
105
|
src: string;
|
|
38
106
|
};
|
|
39
107
|
|
|
40
|
-
let overlays
|
|
41
|
-
let draggingWidgetId
|
|
108
|
+
let overlays = $state<ShapeRect[]>([]);
|
|
109
|
+
let draggingWidgetId = $state<string | null>(null);
|
|
110
|
+
let resizingWidgetId = $state<string | null>(null);
|
|
111
|
+
/** Per-widget cache-bust tokens for Refresh (iframe reload only). */
|
|
112
|
+
let reloadTokens = $state<Record<string, number>>({});
|
|
113
|
+
/** Which shape chrome version menu is open. */
|
|
114
|
+
let openVersionMenuWidgetId = $state<string | null>(null);
|
|
115
|
+
/** Hide Fabric placeholder flash — show loader until iframe fires load. */
|
|
116
|
+
let iframeLoadedByWidgetId = $state<Record<string, boolean>>({});
|
|
42
117
|
|
|
43
118
|
export function sync(): void {
|
|
44
119
|
if (!enabled || !getRects || !resolveUrl) {
|
|
@@ -46,10 +121,15 @@
|
|
|
46
121
|
return;
|
|
47
122
|
}
|
|
48
123
|
const next: ShapeRect[] = [];
|
|
124
|
+
const nextLoaded = { ...iframeLoadedByWidgetId };
|
|
49
125
|
for (const rect of getRects() || []) {
|
|
50
126
|
if (!rect?.widgetId) continue;
|
|
51
127
|
const src = resolveUrl(rect.widgetId);
|
|
52
128
|
if (!src) continue;
|
|
129
|
+
const prev = overlays.find((o) => o.widgetId === rect.widgetId);
|
|
130
|
+
if (!prev || prev.src !== src) {
|
|
131
|
+
nextLoaded[rect.widgetId] = false;
|
|
132
|
+
}
|
|
53
133
|
next.push({
|
|
54
134
|
widgetId: rect.widgetId,
|
|
55
135
|
left: rect.left,
|
|
@@ -59,20 +139,34 @@
|
|
|
59
139
|
src,
|
|
60
140
|
});
|
|
61
141
|
}
|
|
142
|
+
iframeLoadedByWidgetId = nextLoaded;
|
|
62
143
|
overlays = next;
|
|
63
144
|
}
|
|
64
145
|
|
|
146
|
+
function buildIframeSrc(baseSrc: string, widgetId: string): string {
|
|
147
|
+
const token = reloadTokens[widgetId];
|
|
148
|
+
if (!token) return baseSrc;
|
|
149
|
+
try {
|
|
150
|
+
const url = new URL(baseSrc, typeof window !== 'undefined' ? window.location.href : 'http://localhost');
|
|
151
|
+
url.searchParams.set('_t', String(token));
|
|
152
|
+
return url.toString();
|
|
153
|
+
} catch (err) {
|
|
154
|
+
console.warn('[WidgetShapeLivePreviews] buildIframeSrc failed', err);
|
|
155
|
+
const cleaned = baseSrc.replace(/([?&])_t=\d+/g, '$1').replace(/[?&]$/, '');
|
|
156
|
+
const sep = cleaned.includes('?') ? '&' : '?';
|
|
157
|
+
return `${cleaned}${sep}_t=${token}`;
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
|
|
65
161
|
function startShapeDrag(event: PointerEvent, widgetId: string) {
|
|
66
162
|
const target = event.target as HTMLElement | null;
|
|
67
|
-
if (target?.closest('button, a, input')) return;
|
|
163
|
+
if (target?.closest('button, a, input, select, label')) return;
|
|
68
164
|
event.preventDefault();
|
|
69
165
|
event.stopPropagation();
|
|
70
166
|
onSelectShape?.(widgetId);
|
|
71
167
|
draggingWidgetId = widgetId;
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
let lastX = startX;
|
|
75
|
-
let lastY = startY;
|
|
168
|
+
let lastX = event.clientX;
|
|
169
|
+
let lastY = event.clientY;
|
|
76
170
|
|
|
77
171
|
function onMove(e: PointerEvent) {
|
|
78
172
|
const dx = e.clientX - lastX;
|
|
@@ -90,6 +184,7 @@
|
|
|
90
184
|
document.removeEventListener('pointercancel', endDrag);
|
|
91
185
|
document.body.style.removeProperty('user-select');
|
|
92
186
|
document.body.style.removeProperty('cursor');
|
|
187
|
+
onTranslateEnd?.(widgetId);
|
|
93
188
|
console.log('[WidgetShapeLivePreviews] Drag end', widgetId.substring(0, 8));
|
|
94
189
|
}
|
|
95
190
|
|
|
@@ -107,6 +202,102 @@
|
|
|
107
202
|
onSelectShape?.(widgetId);
|
|
108
203
|
onOpenChat?.(widgetId);
|
|
109
204
|
}
|
|
205
|
+
|
|
206
|
+
function handleRefreshClick(event: MouseEvent, widgetId: string) {
|
|
207
|
+
event.preventDefault();
|
|
208
|
+
event.stopPropagation();
|
|
209
|
+
onSelectShape?.(widgetId);
|
|
210
|
+
iframeLoadedByWidgetId = { ...iframeLoadedByWidgetId, [widgetId]: false };
|
|
211
|
+
reloadTokens = { ...reloadTokens, [widgetId]: Date.now() };
|
|
212
|
+
onRefresh?.(widgetId);
|
|
213
|
+
console.log('[WidgetShapeLivePreviews] Refresh iframe', widgetId.substring(0, 8));
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
function markIframeLoaded(widgetId: string) {
|
|
217
|
+
if (iframeLoadedByWidgetId[widgetId]) return;
|
|
218
|
+
iframeLoadedByWidgetId = { ...iframeLoadedByWidgetId, [widgetId]: true };
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
function getTitle(widgetId: string): string {
|
|
222
|
+
return resolveTitle?.(widgetId)?.trim() || 'Widget';
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
function getVersionOptions(widgetId: string): VersionOption[] {
|
|
226
|
+
return resolveVersionOptions?.(widgetId) || [];
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
function resolveSelectedVersionId(widgetId: string, options: VersionOption[]): string {
|
|
230
|
+
const fromParent = resolveVersionSelectedId?.(widgetId)?.trim();
|
|
231
|
+
if (fromParent && options.some((o) => o.id === fromParent)) return fromParent;
|
|
232
|
+
const liveOpt = options.find((o) => o.isLive);
|
|
233
|
+
return liveOpt?.id || options[0]?.id || '';
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
function getSelectedOption(widgetId: string, options: VersionOption[]): VersionOption | null {
|
|
237
|
+
if (!options.length) return null;
|
|
238
|
+
const id = resolveSelectedVersionId(widgetId, options);
|
|
239
|
+
return options.find((o) => o.id === id) || options[0] || null;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
function triggerLabelForOption(option: VersionOption | null, fallback: string): string {
|
|
243
|
+
if (!option) return fallback;
|
|
244
|
+
return (option.shortLabel || option.label || fallback).trim();
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
function toggleVersionMenu(event: MouseEvent, widgetId: string) {
|
|
248
|
+
event.preventDefault();
|
|
249
|
+
event.stopPropagation();
|
|
250
|
+
onSelectShape?.(widgetId);
|
|
251
|
+
openVersionMenuWidgetId = openVersionMenuWidgetId === widgetId ? null : widgetId;
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
function selectVersion(event: MouseEvent, widgetId: string, versionId: string) {
|
|
255
|
+
event.preventDefault();
|
|
256
|
+
event.stopPropagation();
|
|
257
|
+
openVersionMenuWidgetId = null;
|
|
258
|
+
if (!versionId) return;
|
|
259
|
+
onVersionChange?.(widgetId, versionId);
|
|
260
|
+
console.log('[WidgetShapeLivePreviews] Version change', widgetId.substring(0, 8), versionId);
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
function handleSetLiveClick(event: MouseEvent, widgetId: string, versionId: string) {
|
|
264
|
+
event.preventDefault();
|
|
265
|
+
event.stopPropagation();
|
|
266
|
+
const version = versionId.startsWith('v:') ? parseInt(versionId.slice(2), 10) : NaN;
|
|
267
|
+
if (!Number.isFinite(version)) return;
|
|
268
|
+
onSetLive?.(widgetId, version);
|
|
269
|
+
openVersionMenuWidgetId = null;
|
|
270
|
+
console.log('[WidgetShapeLivePreviews] Set live', widgetId.substring(0, 8), version);
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
function handleDocumentPointerDown(event: PointerEvent) {
|
|
274
|
+
if (!openVersionMenuWidgetId) return;
|
|
275
|
+
const target = event.target as HTMLElement | null;
|
|
276
|
+
if (target?.closest?.(`[data-version-menu-for="${openVersionMenuWidgetId}"]`)) return;
|
|
277
|
+
openVersionMenuWidgetId = null;
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
function handleDocumentKeydown(event: KeyboardEvent) {
|
|
281
|
+
if (event.key === 'Escape' && openVersionMenuWidgetId) {
|
|
282
|
+
openVersionMenuWidgetId = null;
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
onMount(() => {
|
|
287
|
+
document.addEventListener('pointerdown', handleDocumentPointerDown, true);
|
|
288
|
+
document.addEventListener('keydown', handleDocumentKeydown);
|
|
289
|
+
const onShapePointerBlock = (event: Event) => {
|
|
290
|
+
const detail = (event as CustomEvent<{ widgetId?: string | null; active?: boolean }>).detail;
|
|
291
|
+
if (!detail) return;
|
|
292
|
+
resizingWidgetId = detail.active && detail.widgetId ? detail.widgetId : null;
|
|
293
|
+
};
|
|
294
|
+
window.addEventListener('widgetic:widget-shape-pointer-block', onShapePointerBlock);
|
|
295
|
+
return () => {
|
|
296
|
+
document.removeEventListener('pointerdown', handleDocumentPointerDown, true);
|
|
297
|
+
document.removeEventListener('keydown', handleDocumentKeydown);
|
|
298
|
+
window.removeEventListener('widgetic:widget-shape-pointer-block', onShapePointerBlock);
|
|
299
|
+
};
|
|
300
|
+
});
|
|
110
301
|
</script>
|
|
111
302
|
|
|
112
303
|
{#if enabled && overlays.length > 0}
|
|
@@ -114,57 +305,237 @@
|
|
|
114
305
|
{#each overlays as overlay (overlay.widgetId)}
|
|
115
306
|
{@const isSelected = selectedWidgetId === overlay.widgetId}
|
|
116
307
|
{@const isDragging = draggingWidgetId === overlay.widgetId}
|
|
308
|
+
{@const isResizing = resizingWidgetId === overlay.widgetId}
|
|
309
|
+
{@const blockIframePointer = isDragging || isResizing}
|
|
117
310
|
{@const chromeTop = overlay.top - DRAG_BAR_H - BAR_SHAPE_GAP}
|
|
311
|
+
{@const showExpandedChrome = isSelected || overlay.width >= SHOW_TITLE_MIN_WIDTH}
|
|
312
|
+
{@const chromeWidth = showExpandedChrome
|
|
313
|
+
? Math.max(overlay.width, MIN_EXPANDED_CHROME_WIDTH)
|
|
314
|
+
: overlay.width}
|
|
315
|
+
{@const titleText = (titleRevision, getTitle(overlay.widgetId))}
|
|
316
|
+
{@const versionMenuOpen = openVersionMenuWidgetId === overlay.widgetId}
|
|
317
|
+
{@const canPublish = (titleRevision, !!resolveCanPublish?.(overlay.widgetId))}
|
|
318
|
+
{@const isPublishing = (titleRevision, !!resolveIsPublishing?.(overlay.widgetId))}
|
|
319
|
+
{@const versionOptions = (titleRevision, getVersionOptions(overlay.widgetId))}
|
|
320
|
+
{@const selectedVersion = getSelectedOption(overlay.widgetId, versionOptions)}
|
|
321
|
+
{@const versionTriggerText = triggerLabelForOption(
|
|
322
|
+
selectedVersion,
|
|
323
|
+
resolveVersionLabel?.(overlay.widgetId)?.trim() || '—',
|
|
324
|
+
)}
|
|
118
325
|
<div
|
|
119
326
|
class="widget-shape-live-chrome-ct fixed"
|
|
120
|
-
style="left: {overlay.left}px; top: {chromeTop}px; width: {
|
|
327
|
+
style="left: {overlay.left}px; top: {chromeTop}px; width: {chromeWidth}px; height: {DRAG_BAR_H + BAR_SHAPE_GAP + overlay.height}px; z-index: 1;"
|
|
121
328
|
>
|
|
122
329
|
<!-- Drag bar sits between rotation control and the shape top stroke -->
|
|
123
330
|
<!-- svelte-ignore a11y_no_static_element_interactions -->
|
|
124
331
|
<div
|
|
125
|
-
class="widget-shape-drag-bar pointer-events-auto absolute left-0 right-0 top-0 flex
|
|
332
|
+
class="widget-shape-drag-bar pointer-events-auto absolute left-0 right-0 top-0 flex items-center gap-1 rounded-t-md border border-b-0 bg-white/95 px-1.5 shadow-sm select-none
|
|
126
333
|
{isSelected ? 'border-blue-400' : 'border-gray-300'}"
|
|
127
|
-
style="height: {DRAG_BAR_H}px; cursor: grab;"
|
|
334
|
+
style="height: {DRAG_BAR_H}px; cursor: grab; min-width: {showExpandedChrome ? MIN_EXPANDED_CHROME_WIDTH : 0}px;"
|
|
128
335
|
title="Drag widget"
|
|
129
336
|
onpointerdown={(e) => startShapeDrag(e, overlay.widgetId)}
|
|
130
337
|
>
|
|
131
338
|
<span class="widget-shape-drag-grip shrink-0 text-xs leading-none text-gray-400" aria-hidden="true">⠿</span>
|
|
132
|
-
|
|
339
|
+
|
|
340
|
+
{#if showExpandedChrome}
|
|
341
|
+
<span
|
|
342
|
+
class="widget-shape-title-el min-w-0 flex-1 truncate text-[11px] font-medium text-gray-700"
|
|
343
|
+
title={titleText}
|
|
344
|
+
>{titleText}</span>
|
|
345
|
+
|
|
346
|
+
{#if versionOptions.length > 0}
|
|
347
|
+
<div
|
|
348
|
+
class="widget-shape-version-ct relative flex shrink-0 items-center gap-0.5"
|
|
349
|
+
data-version-menu-for={overlay.widgetId}
|
|
350
|
+
>
|
|
351
|
+
<button
|
|
352
|
+
type="button"
|
|
353
|
+
class="widget-shape-version-trigger flex min-w-[2.25rem] max-w-[4rem] cursor-pointer items-center gap-0.5 rounded border border-emerald-300 bg-emerald-50 px-1.5 py-0.5 text-[10px] font-semibold tabular-nums text-emerald-800 hover:bg-emerald-100"
|
|
354
|
+
title="Open version list — Live is what visitors see on your website"
|
|
355
|
+
aria-haspopup="listbox"
|
|
356
|
+
aria-expanded={versionMenuOpen}
|
|
357
|
+
onclick={(e) => toggleVersionMenu(e, overlay.widgetId)}
|
|
358
|
+
onpointerdown={(e) => e.stopPropagation()}
|
|
359
|
+
>
|
|
360
|
+
{versionTriggerText}
|
|
361
|
+
<span class="widget-shape-version-caret text-emerald-500" aria-hidden="true">{versionMenuOpen ? '▲' : '▼'}</span>
|
|
362
|
+
</button>
|
|
363
|
+
{#if canPublish}
|
|
364
|
+
<button
|
|
365
|
+
type="button"
|
|
366
|
+
class="widget-shape-publish-btn shrink-0 inline-flex items-center justify-center min-w-[4.75rem] rounded border border-emerald-600 bg-emerald-600 px-1.5 py-0.5 text-[9px] font-semibold uppercase tracking-wide text-white hover:bg-emerald-700 disabled:opacity-50 cursor-pointer"
|
|
367
|
+
disabled={isPublishing}
|
|
368
|
+
title="Publish the latest generated code as the Live version"
|
|
369
|
+
onclick={(e) => {
|
|
370
|
+
e.stopPropagation();
|
|
371
|
+
onPublish?.(overlay.widgetId);
|
|
372
|
+
}}
|
|
373
|
+
onpointerdown={(e) => e.stopPropagation()}
|
|
374
|
+
>{isPublishing ? '...' : 'Publish'}</button>
|
|
375
|
+
{:else if selectedVersion?.isLive}
|
|
376
|
+
<span
|
|
377
|
+
class="widget-shape-live-btn shrink-0 inline-flex items-center justify-center min-w-[4.75rem] rounded bg-emerald-100 px-1.5 py-0.5 text-[9px] font-semibold uppercase tracking-wide text-emerald-600 opacity-80"
|
|
378
|
+
title="This version is Live"
|
|
379
|
+
>Live</span>
|
|
380
|
+
{:else if selectedVersion && onSetLive}
|
|
381
|
+
<button
|
|
382
|
+
type="button"
|
|
383
|
+
class="widget-shape-set-live-btn shrink-0 inline-flex items-center justify-center min-w-[4.75rem] rounded border border-emerald-300 px-1.5 py-0.5 text-[9px] font-semibold uppercase tracking-wide text-emerald-700 hover:bg-emerald-100 disabled:opacity-50 cursor-pointer"
|
|
384
|
+
disabled={settingLiveVersion === Number(selectedVersion.id.slice(2))}
|
|
385
|
+
title="Publish this version so visitors see it on your website"
|
|
386
|
+
onclick={(e) => handleSetLiveClick(e, overlay.widgetId, selectedVersion.id)}
|
|
387
|
+
onpointerdown={(e) => e.stopPropagation()}
|
|
388
|
+
>{settingLiveVersion === Number(selectedVersion.id.slice(2)) ? '...' : 'Set live'}</button>
|
|
389
|
+
{/if}
|
|
390
|
+
{#if versionMenuOpen}
|
|
391
|
+
<div
|
|
392
|
+
class="widget-shape-version-menu absolute right-0 top-full z-30 mt-0.5 w-max max-w-[min(22rem,calc(100vw-16px))] overflow-y-auto overflow-x-hidden rounded-md border border-emerald-200 bg-white py-0.5 shadow-lg"
|
|
393
|
+
style="max-height: {VERSION_MENU_MAX_H}; scrollbar-width: thin; scrollbar-color: rgba(0,0,0,0.2) transparent;"
|
|
394
|
+
role="listbox"
|
|
395
|
+
>
|
|
396
|
+
{#each versionOptions as option (option.id)}
|
|
397
|
+
<!-- svelte-ignore a11y_no_static_element_interactions -->
|
|
398
|
+
<div
|
|
399
|
+
role="option"
|
|
400
|
+
aria-selected={selectedVersion?.id === option.id}
|
|
401
|
+
tabindex="0"
|
|
402
|
+
class="widget-shape-version-option flex w-max max-w-full items-center gap-2 px-2 py-1.5 text-left text-[10px] cursor-pointer
|
|
403
|
+
{selectedVersion?.id === option.id ? 'bg-emerald-50 text-emerald-800' : 'text-gray-700 hover:bg-gray-50'}"
|
|
404
|
+
onclick={(e) => selectVersion(e, overlay.widgetId, option.id)}
|
|
405
|
+
onkeydown={(e) => {
|
|
406
|
+
if (e.key === 'Enter' || e.key === ' ') {
|
|
407
|
+
e.preventDefault();
|
|
408
|
+
selectVersion(e as unknown as MouseEvent, overlay.widgetId, option.id);
|
|
409
|
+
}
|
|
410
|
+
}}
|
|
411
|
+
onpointerdown={(e) => e.stopPropagation()}
|
|
412
|
+
>
|
|
413
|
+
<span class="widget-shape-version-option-label shrink-0 font-semibold tabular-nums">{option.shortLabel || option.label}</span>
|
|
414
|
+
<span class="widget-shape-version-option-date shrink-0 whitespace-nowrap tabular-nums text-slate-400" style="padding-right: 4px;">{option.dateLabel || '—'}</span>
|
|
415
|
+
{#if option.isLive}
|
|
416
|
+
<span class="widget-shape-version-live-badge shrink-0 inline-flex items-center justify-center min-w-[4.75rem] rounded bg-emerald-100 px-1.5 py-0.5 text-[9px] font-semibold uppercase tracking-wide text-emerald-600">live</span>
|
|
417
|
+
{:else if onSetLive}
|
|
418
|
+
<button
|
|
419
|
+
type="button"
|
|
420
|
+
class="widget-shape-set-live-btn shrink-0 inline-flex items-center justify-center min-w-[4.75rem] rounded border border-emerald-300 px-1.5 py-0.5 text-[9px] font-medium text-emerald-700 hover:bg-emerald-100 disabled:opacity-50 cursor-pointer"
|
|
421
|
+
disabled={settingLiveVersion === Number(option.id.slice(2))}
|
|
422
|
+
onclick={(e) => handleSetLiveClick(e, overlay.widgetId, option.id)}
|
|
423
|
+
onpointerdown={(e) => e.stopPropagation()}
|
|
424
|
+
>{settingLiveVersion === Number(option.id.slice(2)) ? '...' : 'Set live'}</button>
|
|
425
|
+
{/if}
|
|
426
|
+
</div>
|
|
427
|
+
{/each}
|
|
428
|
+
</div>
|
|
429
|
+
{/if}
|
|
430
|
+
</div>
|
|
431
|
+
{:else if versionTriggerText && versionTriggerText !== '—'}
|
|
432
|
+
<span
|
|
433
|
+
class="widget-shape-version-label shrink-0 rounded bg-gray-100 px-1.5 py-0.5 text-[10px] font-medium text-gray-500"
|
|
434
|
+
title="Current version"
|
|
435
|
+
>{versionTriggerText}</span>
|
|
436
|
+
{/if}
|
|
437
|
+
{:else if isSelected}
|
|
133
438
|
<span
|
|
134
439
|
class="widget-shape-edit-mode-badge shrink-0 rounded-full bg-amber-200 px-1.5 py-0.5 text-[9px] font-semibold uppercase tracking-wide text-amber-900"
|
|
135
440
|
>Edit mode</span>
|
|
136
441
|
{/if}
|
|
137
|
-
|
|
442
|
+
|
|
443
|
+
<div class="widget-shape-drag-bar-actions ml-auto flex shrink-0 items-center gap-0.5">
|
|
138
444
|
<button
|
|
139
445
|
type="button"
|
|
140
|
-
class="widget-shape-open-chat-
|
|
446
|
+
class="widget-shape-open-chat-btn flex h-6 w-6 items-center justify-center rounded text-gray-500 hover:bg-gray-100 hover:text-forest-green cursor-pointer"
|
|
141
447
|
title="Open chat"
|
|
448
|
+
aria-label="Open chat"
|
|
142
449
|
onclick={(e) => handleOpenChat(e, overlay.widgetId)}
|
|
143
450
|
onpointerdown={(e) => e.stopPropagation()}
|
|
144
451
|
>
|
|
145
|
-
|
|
452
|
+
<svg
|
|
453
|
+
class="widget-shape-chat-icon h-4 w-4"
|
|
454
|
+
viewBox="0 0 24 24"
|
|
455
|
+
fill="none"
|
|
456
|
+
stroke="currentColor"
|
|
457
|
+
stroke-width="2"
|
|
458
|
+
stroke-linecap="round"
|
|
459
|
+
stroke-linejoin="round"
|
|
460
|
+
aria-hidden="true"
|
|
461
|
+
>
|
|
462
|
+
<path d="M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z" />
|
|
463
|
+
</svg>
|
|
464
|
+
<span class="sr-only">Chat</span>
|
|
465
|
+
</button>
|
|
466
|
+
<button
|
|
467
|
+
type="button"
|
|
468
|
+
class="widget-shape-refresh-btn flex h-6 w-6 items-center justify-center rounded text-gray-500 hover:bg-gray-100 hover:text-forest-green cursor-pointer"
|
|
469
|
+
title="Refresh preview"
|
|
470
|
+
aria-label="Refresh preview"
|
|
471
|
+
onclick={(e) => handleRefreshClick(e, overlay.widgetId)}
|
|
472
|
+
onpointerdown={(e) => e.stopPropagation()}
|
|
473
|
+
>
|
|
474
|
+
<svg class="widget-shape-refresh-icon h-3.5 w-3.5" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
|
|
475
|
+
<polyline points="23 4 23 10 17 10"/>
|
|
476
|
+
<path d="M20.49 15a9 9 0 1 1-2.12-9.36L23 10"/>
|
|
477
|
+
</svg>
|
|
146
478
|
</button>
|
|
147
479
|
</div>
|
|
148
480
|
</div>
|
|
149
481
|
|
|
150
482
|
<div
|
|
151
|
-
class="widget-shape-live-preview-frame pointer-events-none absolute left-0
|
|
483
|
+
class="widget-shape-live-preview-frame pointer-events-none absolute left-0 overflow-visible
|
|
152
484
|
{isSelected ? 'ring-2 ring-blue-400/80 rounded-sm' : ''}"
|
|
153
|
-
style="top: {DRAG_BAR_H + BAR_SHAPE_GAP}px; height: {overlay.height}px;"
|
|
485
|
+
style="top: {DRAG_BAR_H + BAR_SHAPE_GAP}px; height: {overlay.height}px; width: {overlay.width}px;"
|
|
154
486
|
>
|
|
155
487
|
<!-- Inset so Fabric corner/rotate handles stay clickable around the edges -->
|
|
156
488
|
<iframe
|
|
157
489
|
class="widget-shape-live-preview-iframe pointer-events-auto absolute border-0 bg-white"
|
|
158
|
-
class:pointer-events-none={
|
|
159
|
-
|
|
490
|
+
class:pointer-events-none={blockIframePointer}
|
|
491
|
+
class:opacity-0={!iframeLoadedByWidgetId[overlay.widgetId]}
|
|
492
|
+
style="left: {IFRAME_INSET_PX}px; top: {IFRAME_INSET_PX}px; right: {IFRAME_INSET_PX}px; bottom: {IFRAME_INSET_PX}px; width: calc(100% - {IFRAME_INSET_PX * 2}px); height: calc(100% - {IFRAME_INSET_PX * 2}px);"
|
|
160
493
|
title="Live widget preview"
|
|
161
|
-
src={overlay.src}
|
|
494
|
+
src={buildIframeSrc(overlay.src, overlay.widgetId)}
|
|
495
|
+
data-widget-id={overlay.widgetId}
|
|
162
496
|
tabindex="-1"
|
|
163
|
-
loading="
|
|
497
|
+
loading="eager"
|
|
164
498
|
sandbox="allow-scripts allow-same-origin allow-forms"
|
|
499
|
+
onload={() => markIframeLoaded(overlay.widgetId)}
|
|
165
500
|
></iframe>
|
|
501
|
+
{#if !iframeLoadedByWidgetId[overlay.widgetId]}
|
|
502
|
+
<div
|
|
503
|
+
class="widget-shape-preview-loader pointer-events-none absolute inset-0 z-[1] flex items-center justify-center rounded-sm bg-white/90"
|
|
504
|
+
aria-hidden="true"
|
|
505
|
+
>
|
|
506
|
+
<div class="widget-shape-preview-spinner h-6 w-6 animate-spin rounded-full border-2 border-gray-300 border-t-emerald-600"></div>
|
|
507
|
+
</div>
|
|
508
|
+
{/if}
|
|
509
|
+
{#if blockIframePointer}
|
|
510
|
+
<!-- Transparent blue shield so drag/resize isn't stolen by the iframe -->
|
|
511
|
+
<div
|
|
512
|
+
class="widget-shape-iframe-pointer-shield pointer-events-auto absolute inset-0 z-[2] rounded-sm bg-blue-400/20"
|
|
513
|
+
aria-hidden="true"
|
|
514
|
+
></div>
|
|
515
|
+
{/if}
|
|
166
516
|
</div>
|
|
167
517
|
</div>
|
|
168
518
|
{/each}
|
|
169
519
|
</div>
|
|
170
520
|
{/if}
|
|
521
|
+
|
|
522
|
+
<style>
|
|
523
|
+
/* Suppress default Tab focus ring on chrome toolbar controls */
|
|
524
|
+
.widget-shape-drag-bar button:focus,
|
|
525
|
+
.widget-shape-drag-bar button:focus-visible {
|
|
526
|
+
outline: none;
|
|
527
|
+
box-shadow: none;
|
|
528
|
+
}
|
|
529
|
+
.widget-shape-version-menu::-webkit-scrollbar {
|
|
530
|
+
width: 8px;
|
|
531
|
+
height: 8px;
|
|
532
|
+
background: transparent;
|
|
533
|
+
}
|
|
534
|
+
.widget-shape-version-menu::-webkit-scrollbar-track {
|
|
535
|
+
background: transparent;
|
|
536
|
+
}
|
|
537
|
+
.widget-shape-version-menu::-webkit-scrollbar-thumb {
|
|
538
|
+
background: rgba(0, 0, 0, 0.2);
|
|
539
|
+
border-radius: 4px;
|
|
540
|
+
}
|
|
541
|
+
</style>
|