@widgetic/creator 0.3.60 → 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 +597 -112
- 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/WidgetShapeLivePreviews.svelte +397 -124
- package/dist/components/WidgetShapeLivePreviews.svelte.d.ts +43 -11
- package/dist/utils/versionPreviewError.d.ts +9 -0
- package/dist/utils/versionPreviewError.js +19 -0
- package/package.json +1 -1
|
@@ -4,6 +4,10 @@
|
|
|
4
4
|
* Body drag is locked on the shape; move via the top drag bar so the iframe stays interactive.
|
|
5
5
|
*/
|
|
6
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';
|
|
7
11
|
|
|
8
12
|
interface VersionOption {
|
|
9
13
|
id: string;
|
|
@@ -15,20 +19,32 @@
|
|
|
15
19
|
dateLabel?: string;
|
|
16
20
|
/** True when this option is the live published version. */
|
|
17
21
|
isLive?: boolean;
|
|
22
|
+
/** True when this version has never been published. */
|
|
23
|
+
isDraft?: boolean;
|
|
18
24
|
}
|
|
19
25
|
|
|
20
26
|
interface Props {
|
|
21
27
|
class?: string;
|
|
22
28
|
style?: string;
|
|
23
29
|
enabled?: boolean;
|
|
24
|
-
/**
|
|
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. */
|
|
25
33
|
getRects?:
|
|
26
|
-
| (() => Array<
|
|
34
|
+
| (() => Array<Omit<ShapeRect, 'src'>>)
|
|
27
35
|
| null;
|
|
28
36
|
/** Resolve a live preview URL for a widget (CDN published or Worker preview). */
|
|
29
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;
|
|
30
42
|
/** Currently selected WidgetShape id (highlights chrome). */
|
|
31
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;
|
|
32
48
|
/** Translate shape by screen-pixel delta. */
|
|
33
49
|
onTranslate?: ((widgetId: string, dxScreen: number, dyScreen: number) => void) | null;
|
|
34
50
|
/** Fired once when a chrome drag ends (mark canvas dirty / history). */
|
|
@@ -45,30 +61,46 @@
|
|
|
45
61
|
resolveVersionOptions?: ((widgetId: string) => Array<VersionOption> | null) | null;
|
|
46
62
|
/** Currently selected version option id. */
|
|
47
63
|
resolveVersionSelectedId?: ((widgetId: string) => string | null) | null;
|
|
48
|
-
onVersionChange?: ((widgetId: string, versionId: string) => void) | null;
|
|
64
|
+
onVersionChange?: ((widgetId: string, versionId: string, buildPreview?: boolean) => void) | null;
|
|
49
65
|
/** Promote a published version to Live (same as chat Set live). */
|
|
50
66
|
onSetLive?: ((widgetId: string, version: number) => void) | null;
|
|
51
67
|
/** Version currently being set live (disables that row's button). */
|
|
52
68
|
settingLiveVersion?: number | null;
|
|
53
|
-
/**
|
|
54
|
-
onRefresh?: ((widgetId: string) => void) | null;
|
|
69
|
+
/** Capture callback after an explicit reload has loaded and settled. */
|
|
70
|
+
onRefresh?: ((widgetId: string, iframe: HTMLIFrameElement, isCurrent: () => boolean) => void | Promise<void>) | null;
|
|
55
71
|
/** Bump when widget names / publish state change so chrome re-renders. */
|
|
56
72
|
titleRevision?: number;
|
|
57
73
|
/** Whether this widget has unpublished codegen ahead of Live (show Publish). */
|
|
58
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;
|
|
59
81
|
/** Publish HEAD as a new live version (shape bar Publish button). */
|
|
60
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;
|
|
61
85
|
/** True while publish is in flight for this widget. */
|
|
62
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;
|
|
63
89
|
}
|
|
64
90
|
|
|
65
91
|
let {
|
|
66
92
|
class: className = '',
|
|
67
93
|
style = '',
|
|
68
94
|
enabled = true,
|
|
95
|
+
portalTarget = 'floating-panels-overlay',
|
|
69
96
|
getRects = null,
|
|
70
97
|
resolveUrl = null,
|
|
98
|
+
resolvePreviewState = null,
|
|
99
|
+
onRetryPreview = null,
|
|
71
100
|
selectedWidgetId = null,
|
|
101
|
+
primaryWidgetId = null,
|
|
102
|
+
onSetPrimary = null,
|
|
103
|
+
resolvePublishDisabled = null,
|
|
72
104
|
onTranslate = null,
|
|
73
105
|
onTranslateEnd = null,
|
|
74
106
|
onSelectShape = null,
|
|
@@ -83,8 +115,12 @@
|
|
|
83
115
|
onRefresh = null,
|
|
84
116
|
titleRevision = 0,
|
|
85
117
|
resolveCanPublish = null,
|
|
118
|
+
resolveCanPublishDraft = null,
|
|
86
119
|
onPublish = null,
|
|
120
|
+
onPublishDraft = null,
|
|
87
121
|
resolveIsPublishing = null,
|
|
122
|
+
/** Per-widget publish-gate reason for a Draft row (tooltip on Publish). */
|
|
123
|
+
resolvePublishDisabledReason = null,
|
|
88
124
|
}: Props = $props();
|
|
89
125
|
|
|
90
126
|
const DRAG_BAR_H = 30;
|
|
@@ -102,14 +138,39 @@
|
|
|
102
138
|
top: number;
|
|
103
139
|
width: number;
|
|
104
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;
|
|
105
146
|
src: string;
|
|
106
147
|
};
|
|
107
148
|
|
|
108
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);
|
|
109
153
|
let draggingWidgetId = $state<string | null>(null);
|
|
110
154
|
let resizingWidgetId = $state<string | null>(null);
|
|
111
155
|
/** Per-widget cache-bust tokens for Refresh (iframe reload only). */
|
|
112
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
|
+
}
|
|
113
174
|
/** Which shape chrome version menu is open. */
|
|
114
175
|
let openVersionMenuWidgetId = $state<string | null>(null);
|
|
115
176
|
/** Hide Fabric placeholder flash — show loader until iframe fires load. */
|
|
@@ -117,6 +178,7 @@
|
|
|
117
178
|
|
|
118
179
|
export function sync(): void {
|
|
119
180
|
if (!enabled || !getRects || !resolveUrl) {
|
|
181
|
+
for (const widgetId of [...pendingRefreshTokens.keys()]) cancelPendingRefresh(widgetId);
|
|
120
182
|
overlays = [];
|
|
121
183
|
return;
|
|
122
184
|
}
|
|
@@ -124,22 +186,24 @@
|
|
|
124
186
|
const nextLoaded = { ...iframeLoadedByWidgetId };
|
|
125
187
|
for (const rect of getRects() || []) {
|
|
126
188
|
if (!rect?.widgetId) continue;
|
|
127
|
-
const src = resolveUrl(rect.widgetId);
|
|
128
|
-
|
|
189
|
+
const src = resolveUrl(rect.widgetId) || '';
|
|
190
|
+
const previewState = resolvePreviewState?.(rect.widgetId);
|
|
191
|
+
if (!src && !previewState?.loading && !previewState?.error) continue;
|
|
129
192
|
const prev = overlays.find((o) => o.widgetId === rect.widgetId);
|
|
130
193
|
if (!prev || prev.src !== src) {
|
|
194
|
+
cancelPendingRefresh(rect.widgetId);
|
|
131
195
|
nextLoaded[rect.widgetId] = false;
|
|
132
196
|
}
|
|
133
|
-
next.push({
|
|
134
|
-
widgetId: rect.widgetId,
|
|
135
|
-
left: rect.left,
|
|
136
|
-
top: rect.top,
|
|
137
|
-
width: rect.width,
|
|
138
|
-
height: rect.height,
|
|
139
|
-
src,
|
|
140
|
-
});
|
|
197
|
+
next.push({ ...rect, src });
|
|
141
198
|
}
|
|
142
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
|
+
}
|
|
206
|
+
}
|
|
143
207
|
overlays = next;
|
|
144
208
|
}
|
|
145
209
|
|
|
@@ -159,6 +223,7 @@
|
|
|
159
223
|
}
|
|
160
224
|
|
|
161
225
|
function startShapeDrag(event: PointerEvent, widgetId: string) {
|
|
226
|
+
if (event.button !== 0 || draggingWidgetId || resizingWidgetId) return;
|
|
162
227
|
const target = event.target as HTMLElement | null;
|
|
163
228
|
if (target?.closest('button, a, input, select, label')) return;
|
|
164
229
|
event.preventDefault();
|
|
@@ -207,15 +272,42 @@
|
|
|
207
272
|
event.preventDefault();
|
|
208
273
|
event.stopPropagation();
|
|
209
274
|
onSelectShape?.(widgetId);
|
|
275
|
+
cancelPendingRefresh(widgetId);
|
|
276
|
+
const token = Math.max(Date.now(), (reloadTokens[widgetId] || 0) + 1);
|
|
277
|
+
pendingRefreshTokens.set(widgetId, token);
|
|
210
278
|
iframeLoadedByWidgetId = { ...iframeLoadedByWidgetId, [widgetId]: false };
|
|
211
|
-
reloadTokens = { ...reloadTokens, [widgetId]:
|
|
212
|
-
onRefresh?.(widgetId);
|
|
279
|
+
reloadTokens = { ...reloadTokens, [widgetId]: token };
|
|
213
280
|
console.log('[WidgetShapeLivePreviews] Refresh iframe', widgetId.substring(0, 8));
|
|
214
281
|
}
|
|
215
282
|
|
|
216
|
-
function markIframeLoaded(widgetId: string) {
|
|
217
|
-
if (
|
|
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;
|
|
218
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));
|
|
219
311
|
}
|
|
220
312
|
|
|
221
313
|
function getTitle(widgetId: string): string {
|
|
@@ -233,6 +325,17 @@
|
|
|
233
325
|
return liveOpt?.id || options[0]?.id || '';
|
|
234
326
|
}
|
|
235
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
|
+
|
|
236
339
|
function getSelectedOption(widgetId: string, options: VersionOption[]): VersionOption | null {
|
|
237
340
|
if (!options.length) return null;
|
|
238
341
|
const id = resolveSelectedVersionId(widgetId, options);
|
|
@@ -257,7 +360,7 @@
|
|
|
257
360
|
openVersionMenuWidgetId = null;
|
|
258
361
|
if (!versionId) return;
|
|
259
362
|
onVersionChange?.(widgetId, versionId);
|
|
260
|
-
|
|
363
|
+
// The parent logs the selected version and resolved commit once the build completes.
|
|
261
364
|
}
|
|
262
365
|
|
|
263
366
|
function handleSetLiveClick(event: MouseEvent, widgetId: string, versionId: string) {
|
|
@@ -270,6 +373,84 @@
|
|
|
270
373
|
console.log('[WidgetShapeLivePreviews] Set live', widgetId.substring(0, 8), version);
|
|
271
374
|
}
|
|
272
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
|
+
|
|
273
454
|
function handleDocumentPointerDown(event: PointerEvent) {
|
|
274
455
|
if (!openVersionMenuWidgetId) return;
|
|
275
456
|
const target = event.target as HTMLElement | null;
|
|
@@ -284,6 +465,15 @@
|
|
|
284
465
|
}
|
|
285
466
|
|
|
286
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
|
+
});
|
|
287
477
|
document.addEventListener('pointerdown', handleDocumentPointerDown, true);
|
|
288
478
|
document.addEventListener('keydown', handleDocumentKeydown);
|
|
289
479
|
const onShapePointerBlock = (event: Event) => {
|
|
@@ -293,6 +483,8 @@
|
|
|
293
483
|
};
|
|
294
484
|
window.addEventListener('widgetic:widget-shape-pointer-block', onShapePointerBlock);
|
|
295
485
|
return () => {
|
|
486
|
+
cancelAnimationFrame(initialSyncFrame);
|
|
487
|
+
for (const widgetId of [...pendingRefreshTokens.keys()]) cancelPendingRefresh(widgetId);
|
|
296
488
|
document.removeEventListener('pointerdown', handleDocumentPointerDown, true);
|
|
297
489
|
document.removeEventListener('keydown', handleDocumentKeydown);
|
|
298
490
|
window.removeEventListener('widgetic:widget-shape-pointer-block', onShapePointerBlock);
|
|
@@ -300,7 +492,8 @@
|
|
|
300
492
|
});
|
|
301
493
|
</script>
|
|
302
494
|
|
|
303
|
-
{#if enabled && overlays.length > 0}
|
|
495
|
+
{#if enabled && overlays.length > 0 && portalHost}
|
|
496
|
+
<Portal target={portalHost} class="widget-shape-units-portal" style="display: contents;">
|
|
304
497
|
<div class="widget-shape-live-previews-ct pointer-events-none {className}" {style}>
|
|
305
498
|
{#each overlays as overlay (overlay.widgetId)}
|
|
306
499
|
{@const isSelected = selectedWidgetId === overlay.widgetId}
|
|
@@ -308,29 +501,44 @@
|
|
|
308
501
|
{@const isResizing = resizingWidgetId === overlay.widgetId}
|
|
309
502
|
{@const blockIframePointer = isDragging || isResizing}
|
|
310
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]}
|
|
311
507
|
{@const showExpandedChrome = isSelected || overlay.width >= SHOW_TITLE_MIN_WIDTH}
|
|
312
508
|
{@const chromeWidth = showExpandedChrome
|
|
313
509
|
? Math.max(overlay.width, MIN_EXPANDED_CHROME_WIDTH)
|
|
314
510
|
: overlay.width}
|
|
315
|
-
{@const titleText = (titleRevision, getTitle(overlay.widgetId))}
|
|
511
|
+
{@const titleText = (void titleRevision, getTitle(overlay.widgetId))}
|
|
316
512
|
{@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))}
|
|
513
|
+
{@const canPublish = (void titleRevision, !!resolveCanPublish?.(overlay.widgetId))}
|
|
514
|
+
{@const isPublishing = (void titleRevision, !!resolveIsPublishing?.(overlay.widgetId))}
|
|
515
|
+
{@const versionOptions = (void titleRevision, getVersionOptions(overlay.widgetId))}
|
|
320
516
|
{@const selectedVersion = getSelectedOption(overlay.widgetId, versionOptions)}
|
|
321
517
|
{@const versionTriggerText = triggerLabelForOption(
|
|
322
518
|
selectedVersion,
|
|
323
519
|
resolveVersionLabel?.(overlay.widgetId)?.trim() || '—',
|
|
324
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;'}>
|
|
325
531
|
<div
|
|
326
|
-
class="widget-shape-live-chrome-ct fixed"
|
|
327
|
-
|
|
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;"
|
|
328
536
|
>
|
|
329
|
-
<!--
|
|
537
|
+
<!-- Attached horizontal chrome wins hits where its own rotation stem overlaps. -->
|
|
330
538
|
<!-- svelte-ignore a11y_no_static_element_interactions -->
|
|
331
539
|
<div
|
|
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
|
|
333
|
-
{isSelected ? 'border-blue-400' : 'border-gray-300'}"
|
|
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'}"
|
|
334
542
|
style="height: {DRAG_BAR_H}px; cursor: grab; min-width: {showExpandedChrome ? MIN_EXPANDED_CHROME_WIDTH : 0}px;"
|
|
335
543
|
title="Drag widget"
|
|
336
544
|
onpointerdown={(e) => startShapeDrag(e, overlay.widgetId)}
|
|
@@ -343,49 +551,105 @@
|
|
|
343
551
|
title={titleText}
|
|
344
552
|
>{titleText}</span>
|
|
345
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>
|
|
346
564
|
{#if versionOptions.length > 0}
|
|
347
565
|
<div
|
|
348
566
|
class="widget-shape-version-ct relative flex shrink-0 items-center gap-0.5"
|
|
349
567
|
data-version-menu-for={overlay.widgetId}
|
|
350
568
|
>
|
|
351
|
-
<
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
aria-haspopup="listbox"
|
|
356
|
-
aria-expanded={versionMenuOpen}
|
|
357
|
-
onclick={(e) => toggleVersionMenu(e, overlay.widgetId)}
|
|
358
|
-
onpointerdown={(e) => e.stopPropagation()}
|
|
569
|
+
<HoverTooltip
|
|
570
|
+
text="Switch version"
|
|
571
|
+
maxWidth={120}
|
|
572
|
+
class="inline-flex shrink-0"
|
|
359
573
|
>
|
|
360
|
-
{versionTriggerText}
|
|
361
|
-
<span class="widget-shape-version-caret text-emerald-500" aria-hidden="true">{versionMenuOpen ? '▲' : '▼'}</span>
|
|
362
|
-
</button>
|
|
363
|
-
{#if canPublish}
|
|
364
574
|
<button
|
|
365
575
|
type="button"
|
|
366
|
-
class="widget-shape-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
onclick={(e) =>
|
|
370
|
-
e.stopPropagation();
|
|
371
|
-
onPublish?.(overlay.widgetId);
|
|
372
|
-
}}
|
|
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)}
|
|
373
580
|
onpointerdown={(e) => e.stopPropagation()}
|
|
374
|
-
>
|
|
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>
|
|
375
603
|
{:else if selectedVersion?.isLive}
|
|
376
|
-
<
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
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}
|
|
389
653
|
{/if}
|
|
390
654
|
{#if versionMenuOpen}
|
|
391
655
|
<div
|
|
@@ -394,36 +658,26 @@
|
|
|
394
658
|
role="listbox"
|
|
395
659
|
>
|
|
396
660
|
{#each versionOptions as option (option.id)}
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
}
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
{
|
|
416
|
-
|
|
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>
|
|
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
|
+
/>
|
|
427
681
|
{/each}
|
|
428
682
|
</div>
|
|
429
683
|
{/if}
|
|
@@ -441,10 +695,25 @@
|
|
|
441
695
|
{/if}
|
|
442
696
|
|
|
443
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">
|
|
444
714
|
<button
|
|
445
715
|
type="button"
|
|
446
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"
|
|
447
|
-
title="Open chat"
|
|
448
717
|
aria-label="Open chat"
|
|
449
718
|
onclick={(e) => handleOpenChat(e, overlay.widgetId)}
|
|
450
719
|
onpointerdown={(e) => e.stopPropagation()}
|
|
@@ -463,32 +732,27 @@
|
|
|
463
732
|
</svg>
|
|
464
733
|
<span class="sr-only">Chat</span>
|
|
465
734
|
</button>
|
|
466
|
-
|
|
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>
|
|
478
|
-
</button>
|
|
735
|
+
</HoverTooltip>
|
|
479
736
|
</div>
|
|
480
737
|
</div>
|
|
481
738
|
|
|
739
|
+
</div>
|
|
740
|
+
|
|
482
741
|
<div
|
|
483
|
-
class="widget-shape-live-preview-frame pointer-events-none
|
|
484
|
-
|
|
485
|
-
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};"
|
|
486
745
|
>
|
|
487
|
-
<!--
|
|
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. -->
|
|
488
752
|
<iframe
|
|
489
753
|
class="widget-shape-live-preview-iframe pointer-events-auto absolute border-0 bg-white"
|
|
490
|
-
class:pointer-events-none={blockIframePointer}
|
|
491
|
-
class:opacity-0={
|
|
754
|
+
class:pointer-events-none={blockIframePointer || selectedPreviewBlocked}
|
|
755
|
+
class:opacity-0={selectedPreviewBlocked}
|
|
492
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);"
|
|
493
757
|
title="Live widget preview"
|
|
494
758
|
src={buildIframeSrc(overlay.src, overlay.widgetId)}
|
|
@@ -496,14 +760,28 @@
|
|
|
496
760
|
tabindex="-1"
|
|
497
761
|
loading="eager"
|
|
498
762
|
sandbox="allow-scripts allow-same-origin allow-forms"
|
|
499
|
-
|
|
763
|
+
use:trackIframeSelection={overlay.widgetId}
|
|
764
|
+
onload={(event) => markIframeLoaded(overlay.widgetId, event.currentTarget)}
|
|
500
765
|
></iframe>
|
|
501
|
-
{
|
|
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}
|
|
502
778
|
<div
|
|
503
|
-
class="widget-shape-preview-loader pointer-events-none absolute inset-0 z-[1] flex items-center justify-center rounded-sm bg-white
|
|
504
|
-
|
|
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"
|
|
505
782
|
>
|
|
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>
|
|
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>
|
|
507
785
|
</div>
|
|
508
786
|
{/if}
|
|
509
787
|
{#if blockIframePointer}
|
|
@@ -517,15 +795,10 @@
|
|
|
517
795
|
</div>
|
|
518
796
|
{/each}
|
|
519
797
|
</div>
|
|
798
|
+
</Portal>
|
|
520
799
|
{/if}
|
|
521
800
|
|
|
522
801
|
<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
802
|
.widget-shape-version-menu::-webkit-scrollbar {
|
|
530
803
|
width: 8px;
|
|
531
804
|
height: 8px;
|