@widgetic/creator 0.3.59 → 0.3.61
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 +1952 -339
- package/dist/components/HoverTooltip.svelte +8 -2
- package/dist/components/HoverTooltip.svelte.d.ts +5 -0
- package/dist/components/VersionDropdownRow.svelte +101 -0
- package/dist/components/VersionDropdownRow.svelte.d.ts +29 -0
- package/dist/components/WidgetDetails.svelte +10 -34
- package/dist/components/WidgetDetails.svelte.d.ts +1 -0
- package/dist/components/WidgetShapeLivePreviews.svelte +704 -60
- package/dist/components/WidgetShapeLivePreviews.svelte.d.ts +91 -35
- package/dist/utils/versionPreviewError.d.ts +9 -0
- package/dist/utils/versionPreviewError.js +19 -0
- package/package.json +1 -1
|
@@ -3,30 +3,134 @@
|
|
|
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
|
+
import { Checked } from '@widgetic/design-system/icons';
|
|
8
|
+
import { Portal } from '@widgetic/design-system';
|
|
9
|
+
import HoverTooltip from './HoverTooltip.svelte';
|
|
10
|
+
import VersionDropdownRow from './VersionDropdownRow.svelte';
|
|
11
|
+
|
|
12
|
+
interface VersionOption {
|
|
13
|
+
id: string;
|
|
14
|
+
/** Full list label, e.g. "Version 9". */
|
|
15
|
+
label: string;
|
|
16
|
+
/** Compact trigger label, e.g. "v9". */
|
|
17
|
+
shortLabel?: string;
|
|
18
|
+
/** Published date/time shown in the list. */
|
|
19
|
+
dateLabel?: string;
|
|
20
|
+
/** True when this option is the live published version. */
|
|
21
|
+
isLive?: boolean;
|
|
22
|
+
/** True when this version has never been published. */
|
|
23
|
+
isDraft?: boolean;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
interface Props {
|
|
27
|
+
class?: string;
|
|
28
|
+
style?: string;
|
|
29
|
+
enabled?: boolean;
|
|
30
|
+
/** Shared host: inactive units < active body (60) < controls (70) < active chrome (80). */
|
|
31
|
+
portalTarget?: string;
|
|
32
|
+
/** Bounds park chrome; the local dimensions/matrix transform the live body. */
|
|
33
|
+
getRects?:
|
|
34
|
+
| (() => Array<Omit<ShapeRect, 'src'>>)
|
|
35
|
+
| null;
|
|
36
|
+
/** Resolve a live preview URL for a widget (CDN published or Worker preview). */
|
|
37
|
+
resolveUrl?: ((widgetId: string) => string | null) | null;
|
|
38
|
+
/** Keep chrome visible while a historical build is pending or failed. */
|
|
39
|
+
resolvePreviewState?: ((widgetId: string) => { loading: boolean; error: string | null } | null) | null;
|
|
40
|
+
/** Retry the selected historical version; never refresh shared HEAD. */
|
|
41
|
+
onRetryPreview?: ((widgetId: string) => void) | null;
|
|
42
|
+
/** Currently selected WidgetShape id (highlights chrome). */
|
|
43
|
+
selectedWidgetId?: string | null;
|
|
44
|
+
primaryWidgetId?: string | null;
|
|
45
|
+
onSetPrimary?: ((widgetId: string) => void) | null;
|
|
46
|
+
/** Shared publish gate; explicit versions must not inherit another row's error. */
|
|
47
|
+
resolvePublishDisabled?: ((widgetId: string, version?: number | null) => boolean) | null;
|
|
48
|
+
/** Translate shape by screen-pixel delta. */
|
|
49
|
+
onTranslate?: ((widgetId: string, dxScreen: number, dyScreen: number) => void) | null;
|
|
50
|
+
/** Fired once when a chrome drag ends (mark canvas dirty / history). */
|
|
51
|
+
onTranslateEnd?: ((widgetId: string) => void) | null;
|
|
52
|
+
/** Select the Fabric WidgetShape (e.g. when pressing the drag bar). */
|
|
53
|
+
onSelectShape?: ((widgetId: string) => void) | null;
|
|
54
|
+
/** Open Widget Details / chat for this widget. */
|
|
55
|
+
onOpenChat?: ((widgetId: string) => void) | null;
|
|
56
|
+
/** Resolve display title for the chrome bar. */
|
|
57
|
+
resolveTitle?: ((widgetId: string) => string | null) | null;
|
|
58
|
+
/** Current version label, e.g. "v3" or "Preview". */
|
|
59
|
+
resolveVersionLabel?: ((widgetId: string) => string | null) | null;
|
|
60
|
+
/** Available version options for the select (null/empty → show label only). */
|
|
61
|
+
resolveVersionOptions?: ((widgetId: string) => Array<VersionOption> | null) | null;
|
|
62
|
+
/** Currently selected version option id. */
|
|
63
|
+
resolveVersionSelectedId?: ((widgetId: string) => string | null) | null;
|
|
64
|
+
onVersionChange?: ((widgetId: string, versionId: string, buildPreview?: boolean) => void) | null;
|
|
65
|
+
/** Promote a published version to Live (same as chat Set live). */
|
|
66
|
+
onSetLive?: ((widgetId: string, version: number) => void) | null;
|
|
67
|
+
/** Version currently being set live (disables that row's button). */
|
|
68
|
+
settingLiveVersion?: number | null;
|
|
69
|
+
/** Capture callback after an explicit reload has loaded and settled. */
|
|
70
|
+
onRefresh?: ((widgetId: string, iframe: HTMLIFrameElement, isCurrent: () => boolean) => void | Promise<void>) | null;
|
|
71
|
+
/** Bump when widget names / publish state change so chrome re-renders. */
|
|
72
|
+
titleRevision?: number;
|
|
73
|
+
/** Whether this widget has unpublished codegen ahead of Live (show Publish). */
|
|
74
|
+
resolveCanPublish?: ((widgetId: string) => boolean) | null;
|
|
75
|
+
/**
|
|
76
|
+
* Whether a specific Draft row can be published. Drafts carry their own
|
|
77
|
+
* commit, so they can be promoted without regenerating first — the main
|
|
78
|
+
* `resolveCanPublish` gate is about HEAD vs Live and would say "no".
|
|
79
|
+
*/
|
|
80
|
+
resolveCanPublishDraft?: ((widgetId: string, version: number) => boolean) | null;
|
|
81
|
+
/** Publish HEAD as a new live version (shape bar Publish button). */
|
|
82
|
+
onPublish?: ((widgetId: string) => void) | null;
|
|
83
|
+
/** Publish a specific Draft version (built from that version's own commit). */
|
|
84
|
+
onPublishDraft?: ((widgetId: string, version: number) => void) | null;
|
|
85
|
+
/** True while publish is in flight for this widget. */
|
|
86
|
+
resolveIsPublishing?: ((widgetId: string) => boolean) | null;
|
|
87
|
+
/** Publish-disabled reason for a specific Draft row (null = enabled). */
|
|
88
|
+
resolvePublishDisabledReason?: ((widgetId: string, version: number) => string | null) | null;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
let {
|
|
92
|
+
class: className = '',
|
|
93
|
+
style = '',
|
|
94
|
+
enabled = true,
|
|
95
|
+
portalTarget = 'floating-panels-overlay',
|
|
96
|
+
getRects = null,
|
|
97
|
+
resolveUrl = null,
|
|
98
|
+
resolvePreviewState = null,
|
|
99
|
+
onRetryPreview = null,
|
|
100
|
+
selectedWidgetId = null,
|
|
101
|
+
primaryWidgetId = null,
|
|
102
|
+
onSetPrimary = null,
|
|
103
|
+
resolvePublishDisabled = null,
|
|
104
|
+
onTranslate = null,
|
|
105
|
+
onTranslateEnd = null,
|
|
106
|
+
onSelectShape = null,
|
|
107
|
+
onOpenChat = null,
|
|
108
|
+
resolveTitle = null,
|
|
109
|
+
resolveVersionLabel = null,
|
|
110
|
+
resolveVersionOptions = null,
|
|
111
|
+
resolveVersionSelectedId = null,
|
|
112
|
+
onVersionChange = null,
|
|
113
|
+
onSetLive = null,
|
|
114
|
+
settingLiveVersion = null,
|
|
115
|
+
onRefresh = null,
|
|
116
|
+
titleRevision = 0,
|
|
117
|
+
resolveCanPublish = null,
|
|
118
|
+
resolveCanPublishDraft = null,
|
|
119
|
+
onPublish = null,
|
|
120
|
+
onPublishDraft = null,
|
|
121
|
+
resolveIsPublishing = null,
|
|
122
|
+
/** Per-widget publish-gate reason for a Draft row (tooltip on Publish). */
|
|
123
|
+
resolvePublishDisabledReason = null,
|
|
124
|
+
}: Props = $props();
|
|
125
|
+
|
|
126
|
+
const DRAG_BAR_H = 30;
|
|
29
127
|
const BAR_SHAPE_GAP = 4;
|
|
128
|
+
const SHOW_TITLE_MIN_WIDTH = 160;
|
|
129
|
+
/** Minimum chrome width when expanded so version + Live/Publish controls fit. */
|
|
130
|
+
const MIN_EXPANDED_CHROME_WIDTH = 220;
|
|
131
|
+
const IFRAME_INSET_PX = 2;
|
|
132
|
+
/** ~5 version rows before the menu scrolls (matches chat history panel). */
|
|
133
|
+
const VERSION_MENU_MAX_H = '12.5rem';
|
|
30
134
|
|
|
31
135
|
type ShapeRect = {
|
|
32
136
|
widgetId: string;
|
|
@@ -34,45 +138,100 @@
|
|
|
34
138
|
top: number;
|
|
35
139
|
width: number;
|
|
36
140
|
height: number;
|
|
141
|
+
localWidth?: number;
|
|
142
|
+
localHeight?: number;
|
|
143
|
+
matrix?: number[];
|
|
144
|
+
/** Pointer is over the shape (Fabric mouse:over); keeps chrome visible. */
|
|
145
|
+
hovered?: boolean;
|
|
37
146
|
src: string;
|
|
38
147
|
};
|
|
39
148
|
|
|
40
|
-
let overlays
|
|
41
|
-
|
|
149
|
+
let overlays = $state<ShapeRect[]>([]);
|
|
150
|
+
// Pass the existing element to DS Portal: its string-target ancestor guard
|
|
151
|
+
// otherwise creates a second floating-panels-overlay on document.body.
|
|
152
|
+
let portalHost = $state<HTMLElement | null>(null);
|
|
153
|
+
let draggingWidgetId = $state<string | null>(null);
|
|
154
|
+
let resizingWidgetId = $state<string | null>(null);
|
|
155
|
+
/** Per-widget cache-bust tokens for Refresh (iframe reload only). */
|
|
156
|
+
let reloadTokens = $state<Record<string, number>>({});
|
|
157
|
+
/**
|
|
158
|
+
* Explicit refresh intent: widgetId → token. The parent capture callback
|
|
159
|
+
* (onRefresh) fires only after THAT reload's load event has settled, never
|
|
160
|
+
* for a stale frame, and exactly once per explicit Refresh click.
|
|
161
|
+
*/
|
|
162
|
+
const pendingRefreshTokens = new Map<string, number>();
|
|
163
|
+
/** Settle timers for post-load captures so unmount/sync can cancel them. */
|
|
164
|
+
const refreshSettleTimers = new Map<string, ReturnType<typeof setTimeout>>();
|
|
165
|
+
|
|
166
|
+
function cancelPendingRefresh(widgetId: string) {
|
|
167
|
+
const timer = refreshSettleTimers.get(widgetId);
|
|
168
|
+
if (timer) {
|
|
169
|
+
clearTimeout(timer);
|
|
170
|
+
refreshSettleTimers.delete(widgetId);
|
|
171
|
+
}
|
|
172
|
+
pendingRefreshTokens.delete(widgetId);
|
|
173
|
+
}
|
|
174
|
+
/** Which shape chrome version menu is open. */
|
|
175
|
+
let openVersionMenuWidgetId = $state<string | null>(null);
|
|
176
|
+
/** Hide Fabric placeholder flash — show loader until iframe fires load. */
|
|
177
|
+
let iframeLoadedByWidgetId = $state<Record<string, boolean>>({});
|
|
42
178
|
|
|
43
179
|
export function sync(): void {
|
|
44
180
|
if (!enabled || !getRects || !resolveUrl) {
|
|
181
|
+
for (const widgetId of [...pendingRefreshTokens.keys()]) cancelPendingRefresh(widgetId);
|
|
45
182
|
overlays = [];
|
|
46
183
|
return;
|
|
47
184
|
}
|
|
48
185
|
const next: ShapeRect[] = [];
|
|
186
|
+
const nextLoaded = { ...iframeLoadedByWidgetId };
|
|
49
187
|
for (const rect of getRects() || []) {
|
|
50
188
|
if (!rect?.widgetId) continue;
|
|
51
|
-
const src = resolveUrl(rect.widgetId);
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
189
|
+
const src = resolveUrl(rect.widgetId) || '';
|
|
190
|
+
const previewState = resolvePreviewState?.(rect.widgetId);
|
|
191
|
+
if (!src && !previewState?.loading && !previewState?.error) continue;
|
|
192
|
+
const prev = overlays.find((o) => o.widgetId === rect.widgetId);
|
|
193
|
+
if (!prev || prev.src !== src) {
|
|
194
|
+
cancelPendingRefresh(rect.widgetId);
|
|
195
|
+
nextLoaded[rect.widgetId] = false;
|
|
196
|
+
}
|
|
197
|
+
next.push({ ...rect, src });
|
|
198
|
+
}
|
|
199
|
+
iframeLoadedByWidgetId = nextLoaded;
|
|
200
|
+
// Drop refresh intents for shapes that left the canvas — their iframes
|
|
201
|
+
// will never fire the load event the capture is waiting on.
|
|
202
|
+
for (const pendingWidgetId of [...pendingRefreshTokens.keys()]) {
|
|
203
|
+
if (!next.some((overlay) => overlay.widgetId === pendingWidgetId)) {
|
|
204
|
+
cancelPendingRefresh(pendingWidgetId);
|
|
205
|
+
}
|
|
61
206
|
}
|
|
62
207
|
overlays = next;
|
|
63
208
|
}
|
|
64
209
|
|
|
210
|
+
function buildIframeSrc(baseSrc: string, widgetId: string): string {
|
|
211
|
+
const token = reloadTokens[widgetId];
|
|
212
|
+
if (!token) return baseSrc;
|
|
213
|
+
try {
|
|
214
|
+
const url = new URL(baseSrc, typeof window !== 'undefined' ? window.location.href : 'http://localhost');
|
|
215
|
+
url.searchParams.set('_t', String(token));
|
|
216
|
+
return url.toString();
|
|
217
|
+
} catch (err) {
|
|
218
|
+
console.warn('[WidgetShapeLivePreviews] buildIframeSrc failed', err);
|
|
219
|
+
const cleaned = baseSrc.replace(/([?&])_t=\d+/g, '$1').replace(/[?&]$/, '');
|
|
220
|
+
const sep = cleaned.includes('?') ? '&' : '?';
|
|
221
|
+
return `${cleaned}${sep}_t=${token}`;
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
|
|
65
225
|
function startShapeDrag(event: PointerEvent, widgetId: string) {
|
|
226
|
+
if (event.button !== 0 || draggingWidgetId || resizingWidgetId) return;
|
|
66
227
|
const target = event.target as HTMLElement | null;
|
|
67
|
-
if (target?.closest('button, a, input')) return;
|
|
228
|
+
if (target?.closest('button, a, input, select, label')) return;
|
|
68
229
|
event.preventDefault();
|
|
69
230
|
event.stopPropagation();
|
|
70
231
|
onSelectShape?.(widgetId);
|
|
71
232
|
draggingWidgetId = widgetId;
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
let lastX = startX;
|
|
75
|
-
let lastY = startY;
|
|
233
|
+
let lastX = event.clientX;
|
|
234
|
+
let lastY = event.clientY;
|
|
76
235
|
|
|
77
236
|
function onMove(e: PointerEvent) {
|
|
78
237
|
const dx = e.clientX - lastX;
|
|
@@ -90,6 +249,7 @@
|
|
|
90
249
|
document.removeEventListener('pointercancel', endDrag);
|
|
91
250
|
document.body.style.removeProperty('user-select');
|
|
92
251
|
document.body.style.removeProperty('cursor');
|
|
252
|
+
onTranslateEnd?.(widgetId);
|
|
93
253
|
console.log('[WidgetShapeLivePreviews] Drag end', widgetId.substring(0, 8));
|
|
94
254
|
}
|
|
95
255
|
|
|
@@ -107,64 +267,548 @@
|
|
|
107
267
|
onSelectShape?.(widgetId);
|
|
108
268
|
onOpenChat?.(widgetId);
|
|
109
269
|
}
|
|
270
|
+
|
|
271
|
+
function handleRefreshClick(event: MouseEvent, widgetId: string) {
|
|
272
|
+
event.preventDefault();
|
|
273
|
+
event.stopPropagation();
|
|
274
|
+
onSelectShape?.(widgetId);
|
|
275
|
+
cancelPendingRefresh(widgetId);
|
|
276
|
+
const token = Math.max(Date.now(), (reloadTokens[widgetId] || 0) + 1);
|
|
277
|
+
pendingRefreshTokens.set(widgetId, token);
|
|
278
|
+
iframeLoadedByWidgetId = { ...iframeLoadedByWidgetId, [widgetId]: false };
|
|
279
|
+
reloadTokens = { ...reloadTokens, [widgetId]: token };
|
|
280
|
+
console.log('[WidgetShapeLivePreviews] Refresh iframe', widgetId.substring(0, 8));
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
function markIframeLoaded(widgetId: string, iframe: EventTarget | null) {
|
|
284
|
+
if (!(iframe instanceof HTMLIFrameElement)) return;
|
|
285
|
+
const loadedSource = iframe.src;
|
|
286
|
+
const matchesFrame = () => enabled && iframe.isConnected
|
|
287
|
+
&& iframe.dataset.widgetId === widgetId && iframe.src === loadedSource
|
|
288
|
+
&& overlays.some((overlay) => overlay.widgetId === widgetId
|
|
289
|
+
&& new URL(buildIframeSrc(overlay.src, widgetId), window.location.href).href === loadedSource);
|
|
290
|
+
// An old load event must not mark the replacement loaded or claim its timer.
|
|
291
|
+
if (!matchesFrame()) return;
|
|
292
|
+
iframeLoadedByWidgetId = { ...iframeLoadedByWidgetId, [widgetId]: true };
|
|
293
|
+
const token = pendingRefreshTokens.get(widgetId);
|
|
294
|
+
if (!token || refreshSettleTimers.has(widgetId)) return;
|
|
295
|
+
const isCurrent = () => matchesFrame()
|
|
296
|
+
&& pendingRefreshTokens.get(widgetId) === token && reloadTokens[widgetId] === token;
|
|
297
|
+
// Match the existing screenshot settling delay, but only after this frame's load.
|
|
298
|
+
refreshSettleTimers.set(widgetId, setTimeout(async () => {
|
|
299
|
+
// Retain the intent and timer marker throughout capture: cancellation must
|
|
300
|
+
// invalidate in-flight work, and duplicate load events must not recapture.
|
|
301
|
+
try {
|
|
302
|
+
if (!isCurrent()) return;
|
|
303
|
+
await onRefresh?.(widgetId, iframe, isCurrent);
|
|
304
|
+
} catch (error) {
|
|
305
|
+
console.warn('[WidgetShapeLivePreviews] Refresh capture failed', widgetId, error);
|
|
306
|
+
} finally {
|
|
307
|
+
// An older completion must never consume a newer Refresh click.
|
|
308
|
+
if (pendingRefreshTokens.get(widgetId) === token) cancelPendingRefresh(widgetId);
|
|
309
|
+
}
|
|
310
|
+
}, 800));
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
function getTitle(widgetId: string): string {
|
|
314
|
+
return resolveTitle?.(widgetId)?.trim() || 'Widget';
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
function getVersionOptions(widgetId: string): VersionOption[] {
|
|
318
|
+
return resolveVersionOptions?.(widgetId) || [];
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
function resolveSelectedVersionId(widgetId: string, options: VersionOption[]): string {
|
|
322
|
+
const fromParent = resolveVersionSelectedId?.(widgetId)?.trim();
|
|
323
|
+
if (fromParent && options.some((o) => o.id === fromParent)) return fromParent;
|
|
324
|
+
const liveOpt = options.find((o) => o.isLive);
|
|
325
|
+
return liveOpt?.id || options[0]?.id || '';
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
/**
|
|
329
|
+
* True for a version that has not been published yet.
|
|
330
|
+
*
|
|
331
|
+
* The history is a single sequence of versions; a Draft is one that has no
|
|
332
|
+
* `publishedAt`. It must never offer "Set live" — there is no artifact to
|
|
333
|
+
* promote, and publishing goes through the normal Publish flow.
|
|
334
|
+
*/
|
|
335
|
+
function isDraftVersion(option: { id: string; isDraft?: boolean }): boolean {
|
|
336
|
+
return option.isDraft === true;
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
function getSelectedOption(widgetId: string, options: VersionOption[]): VersionOption | null {
|
|
340
|
+
if (!options.length) return null;
|
|
341
|
+
const id = resolveSelectedVersionId(widgetId, options);
|
|
342
|
+
return options.find((o) => o.id === id) || options[0] || null;
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
function triggerLabelForOption(option: VersionOption | null, fallback: string): string {
|
|
346
|
+
if (!option) return fallback;
|
|
347
|
+
return (option.shortLabel || option.label || fallback).trim();
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
function toggleVersionMenu(event: MouseEvent, widgetId: string) {
|
|
351
|
+
event.preventDefault();
|
|
352
|
+
event.stopPropagation();
|
|
353
|
+
onSelectShape?.(widgetId);
|
|
354
|
+
openVersionMenuWidgetId = openVersionMenuWidgetId === widgetId ? null : widgetId;
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
function selectVersion(event: MouseEvent, widgetId: string, versionId: string) {
|
|
358
|
+
event.preventDefault();
|
|
359
|
+
event.stopPropagation();
|
|
360
|
+
openVersionMenuWidgetId = null;
|
|
361
|
+
if (!versionId) return;
|
|
362
|
+
onVersionChange?.(widgetId, versionId);
|
|
363
|
+
// The parent logs the selected version and resolved commit once the build completes.
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
function handleSetLiveClick(event: MouseEvent, widgetId: string, versionId: string) {
|
|
367
|
+
event.preventDefault();
|
|
368
|
+
event.stopPropagation();
|
|
369
|
+
const version = versionId.startsWith('v:') ? parseInt(versionId.slice(2), 10) : NaN;
|
|
370
|
+
if (!Number.isFinite(version)) return;
|
|
371
|
+
onSetLive?.(widgetId, version);
|
|
372
|
+
openVersionMenuWidgetId = null;
|
|
373
|
+
console.log('[WidgetShapeLivePreviews] Set live', widgetId.substring(0, 8), version);
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
/** Publish a Draft row: select/preview that version first, then promote exactly it. */
|
|
377
|
+
function handlePublishDraftClick(event: MouseEvent, widgetId: string, versionId: string) {
|
|
378
|
+
event.preventDefault();
|
|
379
|
+
event.stopPropagation();
|
|
380
|
+
openVersionMenuWidgetId = null;
|
|
381
|
+
const version = versionId.startsWith('v:') ? parseInt(versionId.slice(2), 10) : NaN;
|
|
382
|
+
if (!Number.isFinite(version)) return;
|
|
383
|
+
if (!resolveCanPublishDraft?.(widgetId, version) || resolvePublishDisabled?.(widgetId, version)) return;
|
|
384
|
+
// Select exactly this draft, without launching a competing preview build.
|
|
385
|
+
onVersionChange?.(widgetId, versionId, false);
|
|
386
|
+
onPublishDraft?.(widgetId, version);
|
|
387
|
+
console.log('[WidgetShapeLivePreviews] Publish draft', widgetId.substring(0, 8), version);
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
/** Row-select from the shared VersionDropdownRow (no event to stop). */
|
|
391
|
+
function selectVersionFromMenu(widgetId: string, versionId: string) {
|
|
392
|
+
openVersionMenuWidgetId = null;
|
|
393
|
+
if (!versionId) return;
|
|
394
|
+
onVersionChange?.(widgetId, versionId);
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
/** Publish-Draft from the shared VersionDropdownRow; guards mirror handlePublishDraftClick. */
|
|
398
|
+
function handlePublishDraftClickFromMenu(widgetId: string, versionId: string) {
|
|
399
|
+
openVersionMenuWidgetId = null;
|
|
400
|
+
const version = versionId.startsWith('v:') ? parseInt(versionId.slice(2), 10) : NaN;
|
|
401
|
+
if (!Number.isFinite(version)) return;
|
|
402
|
+
if (!onPublishDraft) return;
|
|
403
|
+
if (!resolveCanPublishDraft?.(widgetId, version) || resolvePublishDisabled?.(widgetId, version)) return;
|
|
404
|
+
// Select exactly this draft, without launching a competing preview build.
|
|
405
|
+
onVersionChange?.(widgetId, versionId, false);
|
|
406
|
+
onPublishDraft?.(widgetId, version);
|
|
407
|
+
console.log('[WidgetShapeLivePreviews] Publish draft', widgetId.substring(0, 8), version);
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
/** Set-live from the shared VersionDropdownRow. */
|
|
411
|
+
function handleSetLiveClickFromMenu(widgetId: string, versionId: string) {
|
|
412
|
+
const version = versionId.startsWith('v:') ? parseInt(versionId.slice(2), 10) : NaN;
|
|
413
|
+
if (!Number.isFinite(version)) return;
|
|
414
|
+
onSetLive?.(widgetId, version);
|
|
415
|
+
openVersionMenuWidgetId = null;
|
|
416
|
+
console.log('[WidgetShapeLivePreviews] Set live', widgetId.substring(0, 8), version);
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
/**
|
|
420
|
+
* Parent-side iframe selection works across origins without reading child DOM.
|
|
421
|
+
* pointerenter selects on hover and never cancels the child's click. Browsers
|
|
422
|
+
* focus the iframe element in the parent document when its child gains focus;
|
|
423
|
+
* window blur is deferred one task so activeElement has already been updated.
|
|
424
|
+
* No focus() call or transparent click catcher: rating/audio keep the gesture.
|
|
425
|
+
*/
|
|
426
|
+
function trackIframeSelection(iframe, widgetId) {
|
|
427
|
+
let focusTimer;
|
|
428
|
+
function select() {
|
|
429
|
+
if (!enabled || draggingWidgetId || resizingWidgetId || !iframe.isConnected) return;
|
|
430
|
+
// Keep the menu owner selected while its dropdown is being used.
|
|
431
|
+
if (openVersionMenuWidgetId && openVersionMenuWidgetId !== widgetId) return;
|
|
432
|
+
if (selectedWidgetId !== widgetId) onSelectShape?.(widgetId);
|
|
433
|
+
}
|
|
434
|
+
function checkFocus() {
|
|
435
|
+
clearTimeout(focusTimer);
|
|
436
|
+
focusTimer = setTimeout(() => {
|
|
437
|
+
if (document.activeElement === iframe) select();
|
|
438
|
+
}, 0);
|
|
439
|
+
}
|
|
440
|
+
iframe.addEventListener('pointerenter', select);
|
|
441
|
+
iframe.addEventListener('focus', select);
|
|
442
|
+
window.addEventListener('blur', checkFocus);
|
|
443
|
+
return {
|
|
444
|
+
update(nextWidgetId) { widgetId = nextWidgetId; },
|
|
445
|
+
destroy() {
|
|
446
|
+
clearTimeout(focusTimer);
|
|
447
|
+
iframe.removeEventListener('pointerenter', select);
|
|
448
|
+
iframe.removeEventListener('focus', select);
|
|
449
|
+
window.removeEventListener('blur', checkFocus);
|
|
450
|
+
},
|
|
451
|
+
};
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
function handleDocumentPointerDown(event: PointerEvent) {
|
|
455
|
+
if (!openVersionMenuWidgetId) return;
|
|
456
|
+
const target = event.target as HTMLElement | null;
|
|
457
|
+
if (target?.closest?.(`[data-version-menu-for="${openVersionMenuWidgetId}"]`)) return;
|
|
458
|
+
openVersionMenuWidgetId = null;
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
function handleDocumentKeydown(event: KeyboardEvent) {
|
|
462
|
+
if (event.key === 'Escape' && openVersionMenuWidgetId) {
|
|
463
|
+
openVersionMenuWidgetId = null;
|
|
464
|
+
}
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
onMount(() => {
|
|
468
|
+
const existingHost = document.getElementsByClassName(portalTarget.replace(/^#/, ''))[0]
|
|
469
|
+
|| document.getElementById(portalTarget.replace(/^#/, ''));
|
|
470
|
+
portalHost = existingHost instanceof HTMLElement ? existingHost : null;
|
|
471
|
+
// A child-only HMR remount does not trigger Fabric after:render. Bootstrap
|
|
472
|
+
// once after parent bindings/layout settle; subsequent updates stay event-driven.
|
|
473
|
+
const initialSyncFrame = requestAnimationFrame(() => {
|
|
474
|
+
try { sync(); }
|
|
475
|
+
catch (error) { console.warn('[WidgetShapeLivePreviews] Initial sync failed', error); }
|
|
476
|
+
});
|
|
477
|
+
document.addEventListener('pointerdown', handleDocumentPointerDown, true);
|
|
478
|
+
document.addEventListener('keydown', handleDocumentKeydown);
|
|
479
|
+
const onShapePointerBlock = (event: Event) => {
|
|
480
|
+
const detail = (event as CustomEvent<{ widgetId?: string | null; active?: boolean }>).detail;
|
|
481
|
+
if (!detail) return;
|
|
482
|
+
resizingWidgetId = detail.active && detail.widgetId ? detail.widgetId : null;
|
|
483
|
+
};
|
|
484
|
+
window.addEventListener('widgetic:widget-shape-pointer-block', onShapePointerBlock);
|
|
485
|
+
return () => {
|
|
486
|
+
cancelAnimationFrame(initialSyncFrame);
|
|
487
|
+
for (const widgetId of [...pendingRefreshTokens.keys()]) cancelPendingRefresh(widgetId);
|
|
488
|
+
document.removeEventListener('pointerdown', handleDocumentPointerDown, true);
|
|
489
|
+
document.removeEventListener('keydown', handleDocumentKeydown);
|
|
490
|
+
window.removeEventListener('widgetic:widget-shape-pointer-block', onShapePointerBlock);
|
|
491
|
+
};
|
|
492
|
+
});
|
|
110
493
|
</script>
|
|
111
494
|
|
|
112
|
-
{#if enabled && overlays.length > 0}
|
|
495
|
+
{#if enabled && overlays.length > 0 && portalHost}
|
|
496
|
+
<Portal target={portalHost} class="widget-shape-units-portal" style="display: contents;">
|
|
113
497
|
<div class="widget-shape-live-previews-ct pointer-events-none {className}" {style}>
|
|
114
498
|
{#each overlays as overlay (overlay.widgetId)}
|
|
115
499
|
{@const isSelected = selectedWidgetId === overlay.widgetId}
|
|
116
500
|
{@const isDragging = draggingWidgetId === overlay.widgetId}
|
|
501
|
+
{@const isResizing = resizingWidgetId === overlay.widgetId}
|
|
502
|
+
{@const blockIframePointer = isDragging || isResizing}
|
|
117
503
|
{@const chromeTop = overlay.top - DRAG_BAR_H - BAR_SHAPE_GAP}
|
|
504
|
+
{@const bodyWidth = overlay.localWidth ?? overlay.width}
|
|
505
|
+
{@const bodyHeight = overlay.localHeight ?? overlay.height}
|
|
506
|
+
{@const bodyMatrix = overlay.matrix ?? [1, 0, 0, 1, overlay.left, overlay.top]}
|
|
507
|
+
{@const showExpandedChrome = isSelected || overlay.width >= SHOW_TITLE_MIN_WIDTH}
|
|
508
|
+
{@const chromeWidth = showExpandedChrome
|
|
509
|
+
? Math.max(overlay.width, MIN_EXPANDED_CHROME_WIDTH)
|
|
510
|
+
: overlay.width}
|
|
511
|
+
{@const titleText = (void titleRevision, getTitle(overlay.widgetId))}
|
|
512
|
+
{@const versionMenuOpen = openVersionMenuWidgetId === overlay.widgetId}
|
|
513
|
+
{@const canPublish = (void titleRevision, !!resolveCanPublish?.(overlay.widgetId))}
|
|
514
|
+
{@const isPublishing = (void titleRevision, !!resolveIsPublishing?.(overlay.widgetId))}
|
|
515
|
+
{@const versionOptions = (void titleRevision, getVersionOptions(overlay.widgetId))}
|
|
516
|
+
{@const selectedVersion = getSelectedOption(overlay.widgetId, versionOptions)}
|
|
517
|
+
{@const versionTriggerText = triggerLabelForOption(
|
|
518
|
+
selectedVersion,
|
|
519
|
+
resolveVersionLabel?.(overlay.widgetId)?.trim() || '—',
|
|
520
|
+
)}
|
|
521
|
+
<!-- Inactive widgets are whole stacking units, ordered by Fabric's array.
|
|
522
|
+
Active wrapper is transparent to stacking: body 60 < controls 70 < bar 80. -->
|
|
523
|
+
{@const chromeZIndex = isSelected ? 80 : 2}
|
|
524
|
+
{@const isPrimary = primaryWidgetId === overlay.widgetId}
|
|
525
|
+
{@const previewState = (void titleRevision, resolvePreviewState?.(overlay.widgetId))}
|
|
526
|
+
{@const selectedPreviewBlocked = !!(previewState?.loading || previewState?.error) || !iframeLoadedByWidgetId[overlay.widgetId]}
|
|
527
|
+
<!-- Chrome shows on hover or selection; an open menu pins it while the pointer is inside the menu. -->
|
|
528
|
+
{@const chromeVisible = isSelected || overlay.hovered || versionMenuOpen}
|
|
529
|
+
<div class="widget-shape-unit-ct pointer-events-none" data-widget-id={overlay.widgetId}
|
|
530
|
+
style={isSelected ? 'display: contents;' : 'position: absolute; inset: 0; z-index: 1; pointer-events: none;'}>
|
|
118
531
|
<div
|
|
119
|
-
class="widget-shape-live-chrome-ct fixed"
|
|
120
|
-
|
|
532
|
+
class="widget-shape-live-chrome-ct fixed pointer-events-none"
|
|
533
|
+
data-widget-id={overlay.widgetId}
|
|
534
|
+
class:opacity-0={!chromeVisible}
|
|
535
|
+
style="left: {overlay.left}px; top: {chromeTop}px; width: {chromeWidth}px; height: {DRAG_BAR_H}px; z-index: {chromeZIndex}; pointer-events: none; transition: opacity 120ms ease;"
|
|
121
536
|
>
|
|
122
|
-
<!--
|
|
537
|
+
<!-- Attached horizontal chrome wins hits where its own rotation stem overlaps. -->
|
|
123
538
|
<!-- svelte-ignore a11y_no_static_element_interactions -->
|
|
124
539
|
<div
|
|
125
|
-
class="widget-shape-drag-bar pointer-events-auto absolute left-0 right-0 top-0 flex
|
|
126
|
-
{isSelected ? 'border-blue-400' : 'border-gray-300'}"
|
|
127
|
-
style="height: {DRAG_BAR_H}px; cursor: grab;"
|
|
540
|
+
class="widget-shape-drag-bar pointer-events-auto absolute left-0 right-0 top-0 z-10 flex items-center gap-1 rounded-t-md border border-b-0 px-1.5 shadow-sm select-none [&_button:focus]:outline-none [&_button:focus]:shadow-none [&_button:focus-visible]:outline-none [&_button:focus-visible]:shadow-none
|
|
541
|
+
{isPrimary ? 'bg-red-100' : 'bg-white'} {isSelected ? 'border-blue-400' : 'border-gray-300'}"
|
|
542
|
+
style="height: {DRAG_BAR_H}px; cursor: grab; min-width: {showExpandedChrome ? MIN_EXPANDED_CHROME_WIDTH : 0}px;"
|
|
128
543
|
title="Drag widget"
|
|
129
544
|
onpointerdown={(e) => startShapeDrag(e, overlay.widgetId)}
|
|
130
545
|
>
|
|
131
546
|
<span class="widget-shape-drag-grip shrink-0 text-xs leading-none text-gray-400" aria-hidden="true">⠿</span>
|
|
132
|
-
|
|
547
|
+
|
|
548
|
+
{#if showExpandedChrome}
|
|
549
|
+
<span
|
|
550
|
+
class="widget-shape-title-el min-w-0 flex-1 truncate text-[11px] font-medium text-gray-700"
|
|
551
|
+
title={titleText}
|
|
552
|
+
>{titleText}</span>
|
|
553
|
+
|
|
554
|
+
<HoverTooltip text={isPrimary ? 'Primary widget' : 'Set primary'} class="inline-flex shrink-0">
|
|
555
|
+
<button
|
|
556
|
+
type="button"
|
|
557
|
+
class="widget-shape-set-primary-btn flex h-6 w-6 shrink-0 items-center justify-center rounded text-red-700 enabled:hover:bg-red-200 enabled:hover:text-red-800 disabled:text-red-700/40 disabled:pointer-events-none disabled:cursor-default cursor-pointer"
|
|
558
|
+
aria-label={isPrimary ? 'Primary widget' : 'Set primary'}
|
|
559
|
+
disabled={isPrimary || !onSetPrimary}
|
|
560
|
+
onpointerdown={(event) => event.stopPropagation()}
|
|
561
|
+
onclick={(event) => { event.stopPropagation(); onSetPrimary?.(overlay.widgetId); }}
|
|
562
|
+
><Checked class="widget-shape-primary-icon h-4 w-4" /></button>
|
|
563
|
+
</HoverTooltip>
|
|
564
|
+
{#if versionOptions.length > 0}
|
|
565
|
+
<div
|
|
566
|
+
class="widget-shape-version-ct relative flex shrink-0 items-center gap-0.5"
|
|
567
|
+
data-version-menu-for={overlay.widgetId}
|
|
568
|
+
>
|
|
569
|
+
<HoverTooltip
|
|
570
|
+
text="Switch version"
|
|
571
|
+
maxWidth={120}
|
|
572
|
+
class="inline-flex shrink-0"
|
|
573
|
+
>
|
|
574
|
+
<button
|
|
575
|
+
type="button"
|
|
576
|
+
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"
|
|
577
|
+
aria-haspopup="listbox"
|
|
578
|
+
aria-expanded={versionMenuOpen}
|
|
579
|
+
onclick={(e) => toggleVersionMenu(e, overlay.widgetId)}
|
|
580
|
+
onpointerdown={(e) => e.stopPropagation()}
|
|
581
|
+
>
|
|
582
|
+
{versionTriggerText}
|
|
583
|
+
<span class="widget-shape-version-caret text-emerald-500" aria-hidden="true">{versionMenuOpen ? '▲' : '▼'}</span>
|
|
584
|
+
</button>
|
|
585
|
+
</HoverTooltip>
|
|
586
|
+
{#if canPublish && !selectedVersion}
|
|
587
|
+
<HoverTooltip
|
|
588
|
+
text="Publish latest"
|
|
589
|
+
maxWidth={120}
|
|
590
|
+
class="inline-flex shrink-0"
|
|
591
|
+
>
|
|
592
|
+
<button
|
|
593
|
+
type="button"
|
|
594
|
+
class="widget-shape-publish-btn shrink-0 inline-flex items-center justify-center min-w-[3.75rem] rounded border border-emerald-600 bg-emerald-600 px-1 py-0.5 text-[8px] font-semibold uppercase tracking-wide text-white hover:bg-emerald-700 disabled:opacity-50 cursor-pointer"
|
|
595
|
+
disabled={isPublishing || selectedPreviewBlocked || !!resolvePublishDisabled?.(overlay.widgetId, selectedVersion ? Number(selectedVersion.id.slice(2)) : null)}
|
|
596
|
+
onclick={(e) => {
|
|
597
|
+
e.stopPropagation();
|
|
598
|
+
onPublish?.(overlay.widgetId);
|
|
599
|
+
}}
|
|
600
|
+
onpointerdown={(e) => e.stopPropagation()}
|
|
601
|
+
>{isPublishing ? '...' : 'Publish'}</button>
|
|
602
|
+
</HoverTooltip>
|
|
603
|
+
{:else if selectedVersion?.isLive}
|
|
604
|
+
<HoverTooltip
|
|
605
|
+
text="Currently live"
|
|
606
|
+
maxWidth={120}
|
|
607
|
+
class="inline-flex shrink-0"
|
|
608
|
+
>
|
|
609
|
+
<span
|
|
610
|
+
class="widget-shape-live-btn shrink-0 inline-flex items-center justify-center min-w-[3.75rem] rounded bg-emerald-100 px-1 py-0.5 text-[8px] font-semibold uppercase tracking-wide text-emerald-600 opacity-80"
|
|
611
|
+
>Live</span>
|
|
612
|
+
</HoverTooltip>
|
|
613
|
+
{:else if selectedVersion && onSetLive && !isDraftVersion(selectedVersion)}
|
|
614
|
+
<HoverTooltip
|
|
615
|
+
text="Make this version live"
|
|
616
|
+
maxWidth={130}
|
|
617
|
+
class="inline-flex shrink-0"
|
|
618
|
+
>
|
|
619
|
+
<button
|
|
620
|
+
type="button"
|
|
621
|
+
class="widget-shape-set-live-btn shrink-0 inline-flex items-center justify-center min-w-[3.75rem] rounded border border-emerald-300 px-1 py-0.5 text-[8px] font-semibold uppercase tracking-wide text-emerald-700 hover:bg-emerald-100 disabled:opacity-50 cursor-pointer"
|
|
622
|
+
disabled={settingLiveVersion === Number(selectedVersion.id.slice(2))}
|
|
623
|
+
onclick={(e) => handleSetLiveClick(e, overlay.widgetId, selectedVersion.id)}
|
|
624
|
+
onpointerdown={(e) => e.stopPropagation()}
|
|
625
|
+
>{settingLiveVersion === Number(selectedVersion.id.slice(2)) ? '...' : 'Set live'}</button>
|
|
626
|
+
</HoverTooltip>
|
|
627
|
+
{:else if selectedVersion && isDraftVersion(selectedVersion)}
|
|
628
|
+
{#if onPublishDraft}
|
|
629
|
+
<HoverTooltip
|
|
630
|
+
text="Publish this draft"
|
|
631
|
+
maxWidth={130}
|
|
632
|
+
class="inline-flex shrink-0"
|
|
633
|
+
>
|
|
634
|
+
<button
|
|
635
|
+
type="button"
|
|
636
|
+
class="widget-shape-publish-btn shrink-0 inline-flex items-center justify-center min-w-[3.75rem] rounded border border-emerald-600 bg-emerald-600 px-1 py-0.5 text-[8px] font-semibold uppercase tracking-wide text-white hover:bg-emerald-700 disabled:opacity-50 cursor-pointer"
|
|
637
|
+
disabled={isPublishing || selectedPreviewBlocked || !resolveCanPublishDraft?.(overlay.widgetId, Number(selectedVersion.id.slice(2))) || !!resolvePublishDisabled?.(overlay.widgetId, Number(selectedVersion.id.slice(2)))}
|
|
638
|
+
onclick={(e) => handlePublishDraftClick(e, overlay.widgetId, selectedVersion.id)}
|
|
639
|
+
onpointerdown={(e) => e.stopPropagation()}
|
|
640
|
+
>{isPublishing ? '...' : 'Publish'}</button>
|
|
641
|
+
</HoverTooltip>
|
|
642
|
+
{:else}
|
|
643
|
+
<HoverTooltip
|
|
644
|
+
text="No commit to publish"
|
|
645
|
+
maxWidth={130}
|
|
646
|
+
class="inline-flex shrink-0"
|
|
647
|
+
>
|
|
648
|
+
<span
|
|
649
|
+
class="widget-shape-draft-badge shrink-0 inline-flex items-center justify-center min-w-[3.75rem] rounded bg-amber-100 px-1 py-0.5 text-[8px] font-semibold uppercase tracking-wide text-amber-700"
|
|
650
|
+
>Draft</span>
|
|
651
|
+
</HoverTooltip>
|
|
652
|
+
{/if}
|
|
653
|
+
{/if}
|
|
654
|
+
{#if versionMenuOpen}
|
|
655
|
+
<div
|
|
656
|
+
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"
|
|
657
|
+
style="max-height: {VERSION_MENU_MAX_H}; scrollbar-width: thin; scrollbar-color: rgba(0,0,0,0.2) transparent;"
|
|
658
|
+
role="listbox"
|
|
659
|
+
>
|
|
660
|
+
{#each versionOptions as option (option.id)}
|
|
661
|
+
{@const optionVersion = Number(option.id.slice(2))}
|
|
662
|
+
{@const optionBusy = (void titleRevision, !!resolveIsPublishing?.(overlay.widgetId))}
|
|
663
|
+
{@const optionPublishDisabled = !resolveCanPublishDraft?.(overlay.widgetId, optionVersion)
|
|
664
|
+
|| !!resolvePublishDisabled?.(overlay.widgetId, optionVersion)
|
|
665
|
+
|| (selectedVersion?.id === option.id && selectedPreviewBlocked)}
|
|
666
|
+
{@const optionPublishReason = (void titleRevision, resolvePublishDisabledReason?.(overlay.widgetId, optionVersion))}
|
|
667
|
+
<VersionDropdownRow
|
|
668
|
+
versionLabel={option.shortLabel || option.label}
|
|
669
|
+
dateLabel={option.dateLabel}
|
|
670
|
+
isLive={option.isLive === true}
|
|
671
|
+
isDraft={isDraftVersion(option)}
|
|
672
|
+
selected={selectedVersion?.id === option.id}
|
|
673
|
+
busy={optionBusy}
|
|
674
|
+
publishDisabled={optionPublishDisabled}
|
|
675
|
+
publishDisabledReason={optionPublishReason || 'Build this draft version and make it the Live version'}
|
|
676
|
+
setLiveDisabled={settingLiveVersion === optionVersion}
|
|
677
|
+
onselect={() => selectVersionFromMenu(overlay.widgetId, option.id)}
|
|
678
|
+
onpublish={() => handlePublishDraftClickFromMenu(overlay.widgetId, option.id)}
|
|
679
|
+
onsetlive={() => handleSetLiveClickFromMenu(overlay.widgetId, option.id)}
|
|
680
|
+
/>
|
|
681
|
+
{/each}
|
|
682
|
+
</div>
|
|
683
|
+
{/if}
|
|
684
|
+
</div>
|
|
685
|
+
{:else if versionTriggerText && versionTriggerText !== '—'}
|
|
686
|
+
<span
|
|
687
|
+
class="widget-shape-version-label shrink-0 rounded bg-gray-100 px-1.5 py-0.5 text-[10px] font-medium text-gray-500"
|
|
688
|
+
title="Current version"
|
|
689
|
+
>{versionTriggerText}</span>
|
|
690
|
+
{/if}
|
|
691
|
+
{:else if isSelected}
|
|
133
692
|
<span
|
|
134
693
|
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
694
|
>Edit mode</span>
|
|
136
695
|
{/if}
|
|
137
|
-
|
|
696
|
+
|
|
697
|
+
<div class="widget-shape-drag-bar-actions ml-auto flex shrink-0 items-center gap-0.5">
|
|
698
|
+
<HoverTooltip text="Refresh preview" class="inline-flex shrink-0">
|
|
699
|
+
<button
|
|
700
|
+
type="button"
|
|
701
|
+
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"
|
|
702
|
+
aria-label="Refresh preview"
|
|
703
|
+
onclick={(e) => handleRefreshClick(e, overlay.widgetId)}
|
|
704
|
+
onpointerdown={(e) => e.stopPropagation()}
|
|
705
|
+
>
|
|
706
|
+
<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">
|
|
707
|
+
<polyline points="23 4 23 10 17 10"/>
|
|
708
|
+
<path d="M20.49 15a9 9 0 1 1-2.12-9.36L23 10"/>
|
|
709
|
+
</svg>
|
|
710
|
+
<span class="sr-only">Refresh</span>
|
|
711
|
+
</button>
|
|
712
|
+
</HoverTooltip>
|
|
713
|
+
<HoverTooltip text="Open chat" maxWidth={100} class="inline-flex shrink-0">
|
|
138
714
|
<button
|
|
139
715
|
type="button"
|
|
140
|
-
class="widget-shape-open-chat-
|
|
141
|
-
|
|
716
|
+
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"
|
|
717
|
+
aria-label="Open chat"
|
|
142
718
|
onclick={(e) => handleOpenChat(e, overlay.widgetId)}
|
|
143
719
|
onpointerdown={(e) => e.stopPropagation()}
|
|
144
720
|
>
|
|
145
|
-
|
|
721
|
+
<svg
|
|
722
|
+
class="widget-shape-chat-icon h-4 w-4"
|
|
723
|
+
viewBox="0 0 24 24"
|
|
724
|
+
fill="none"
|
|
725
|
+
stroke="currentColor"
|
|
726
|
+
stroke-width="2"
|
|
727
|
+
stroke-linecap="round"
|
|
728
|
+
stroke-linejoin="round"
|
|
729
|
+
aria-hidden="true"
|
|
730
|
+
>
|
|
731
|
+
<path d="M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z" />
|
|
732
|
+
</svg>
|
|
733
|
+
<span class="sr-only">Chat</span>
|
|
146
734
|
</button>
|
|
735
|
+
</HoverTooltip>
|
|
147
736
|
</div>
|
|
148
737
|
</div>
|
|
149
738
|
|
|
739
|
+
</div>
|
|
740
|
+
|
|
150
741
|
<div
|
|
151
|
-
class="widget-shape-live-preview-frame pointer-events-none
|
|
152
|
-
|
|
153
|
-
style="
|
|
742
|
+
class="widget-shape-live-preview-frame pointer-events-none fixed left-0 top-0 origin-top-left overflow-visible"
|
|
743
|
+
data-widget-id={overlay.widgetId}
|
|
744
|
+
style="height: {bodyHeight}px; width: {bodyWidth}px; transform: matrix({bodyMatrix.join(',')}); z-index: {isSelected ? 60 : 1};"
|
|
154
745
|
>
|
|
155
|
-
<!--
|
|
746
|
+
<!-- A new reload gets its own window so old load/screenshot replies cannot win. -->
|
|
747
|
+
{#if overlay.src}
|
|
748
|
+
{#key buildIframeSrc(overlay.src, overlay.widgetId)}
|
|
749
|
+
<!-- Inset so Fabric corner/rotate handles stay clickable around the edges.
|
|
750
|
+
First click inside the widget selects the shape (ring + chrome) via
|
|
751
|
+
focus, while the iframe keeps receiving the pointer events themselves. -->
|
|
156
752
|
<iframe
|
|
157
753
|
class="widget-shape-live-preview-iframe pointer-events-auto absolute border-0 bg-white"
|
|
158
|
-
class:pointer-events-none={
|
|
159
|
-
|
|
754
|
+
class:pointer-events-none={blockIframePointer || selectedPreviewBlocked}
|
|
755
|
+
class:opacity-0={selectedPreviewBlocked}
|
|
756
|
+
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
757
|
title="Live widget preview"
|
|
161
|
-
src={overlay.src}
|
|
758
|
+
src={buildIframeSrc(overlay.src, overlay.widgetId)}
|
|
759
|
+
data-widget-id={overlay.widgetId}
|
|
162
760
|
tabindex="-1"
|
|
163
|
-
loading="
|
|
761
|
+
loading="eager"
|
|
164
762
|
sandbox="allow-scripts allow-same-origin allow-forms"
|
|
763
|
+
use:trackIframeSelection={overlay.widgetId}
|
|
764
|
+
onload={(event) => markIframeLoaded(overlay.widgetId, event.currentTarget)}
|
|
165
765
|
></iframe>
|
|
766
|
+
{/key}
|
|
767
|
+
{/if}
|
|
768
|
+
{#if previewState?.error && !previewState.loading}
|
|
769
|
+
<div class="widget-shape-preview-error pointer-events-auto absolute inset-0 flex flex-col items-center justify-center gap-2 overflow-auto bg-white p-4 text-center text-xs text-red-600 [scrollbar-color:rgba(0,0,0,0.2)_transparent]" role="alert">
|
|
770
|
+
<span>{previewState.error}</span>
|
|
771
|
+
{#if onRetryPreview}
|
|
772
|
+
<button type="button" class="widget-shape-preview-retry pointer-events-auto cursor-pointer rounded border border-gray-300 bg-white px-3 py-1.5 text-gray-700 hover:bg-gray-100 focus:outline-none focus-visible:outline-none focus-visible:bg-gray-100"
|
|
773
|
+
onpointerdown={(event) => event.stopPropagation()}
|
|
774
|
+
onclick={(event) => { event.stopPropagation(); onRetryPreview?.(overlay.widgetId); }}>Retry</button>
|
|
775
|
+
{/if}
|
|
776
|
+
</div>
|
|
777
|
+
{:else if selectedPreviewBlocked}
|
|
778
|
+
<div
|
|
779
|
+
class="widget-shape-preview-loader pointer-events-none absolute inset-0 z-[1] flex flex-col items-center justify-center gap-2 rounded-sm bg-white"
|
|
780
|
+
role="status"
|
|
781
|
+
aria-live="polite"
|
|
782
|
+
>
|
|
783
|
+
<div class="widget-shape-preview-spinner h-6 w-6 animate-spin rounded-full border-2 border-gray-300 border-t-emerald-600" aria-hidden="true"></div>
|
|
784
|
+
<span class="widget-shape-preview-loading-label text-xs text-gray-600">{previewState?.loading ? 'Building...' : 'Loading...'}</span>
|
|
785
|
+
</div>
|
|
786
|
+
{/if}
|
|
787
|
+
{#if blockIframePointer}
|
|
788
|
+
<!-- Transparent blue shield so drag/resize isn't stolen by the iframe -->
|
|
789
|
+
<div
|
|
790
|
+
class="widget-shape-iframe-pointer-shield pointer-events-auto absolute inset-0 z-[2] rounded-sm bg-blue-400/20"
|
|
791
|
+
aria-hidden="true"
|
|
792
|
+
></div>
|
|
793
|
+
{/if}
|
|
166
794
|
</div>
|
|
167
795
|
</div>
|
|
168
796
|
{/each}
|
|
169
797
|
</div>
|
|
798
|
+
</Portal>
|
|
170
799
|
{/if}
|
|
800
|
+
|
|
801
|
+
<style>
|
|
802
|
+
.widget-shape-version-menu::-webkit-scrollbar {
|
|
803
|
+
width: 8px;
|
|
804
|
+
height: 8px;
|
|
805
|
+
background: transparent;
|
|
806
|
+
}
|
|
807
|
+
.widget-shape-version-menu::-webkit-scrollbar-track {
|
|
808
|
+
background: transparent;
|
|
809
|
+
}
|
|
810
|
+
.widget-shape-version-menu::-webkit-scrollbar-thumb {
|
|
811
|
+
background: rgba(0, 0, 0, 0.2);
|
|
812
|
+
border-radius: 4px;
|
|
813
|
+
}
|
|
814
|
+
</style>
|