@widgetic/creator 0.3.58 → 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.
@@ -0,0 +1,541 @@
1
+ <script lang="ts">
2
+ /**
3
+ * Live iframe + chrome over Fabric WidgetShapes (W3).
4
+ * Body drag is locked on the shape; move via the top drag bar so the iframe stays interactive.
5
+ */
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;
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';
98
+
99
+ type ShapeRect = {
100
+ widgetId: string;
101
+ left: number;
102
+ top: number;
103
+ width: number;
104
+ height: number;
105
+ src: string;
106
+ };
107
+
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>>({});
117
+
118
+ export function sync(): void {
119
+ if (!enabled || !getRects || !resolveUrl) {
120
+ overlays = [];
121
+ return;
122
+ }
123
+ const next: ShapeRect[] = [];
124
+ const nextLoaded = { ...iframeLoadedByWidgetId };
125
+ for (const rect of getRects() || []) {
126
+ if (!rect?.widgetId) continue;
127
+ const src = resolveUrl(rect.widgetId);
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
+ }
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
+ });
141
+ }
142
+ iframeLoadedByWidgetId = nextLoaded;
143
+ overlays = next;
144
+ }
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
+
161
+ function startShapeDrag(event: PointerEvent, widgetId: string) {
162
+ const target = event.target as HTMLElement | null;
163
+ if (target?.closest('button, a, input, select, label')) return;
164
+ event.preventDefault();
165
+ event.stopPropagation();
166
+ onSelectShape?.(widgetId);
167
+ draggingWidgetId = widgetId;
168
+ let lastX = event.clientX;
169
+ let lastY = event.clientY;
170
+
171
+ function onMove(e: PointerEvent) {
172
+ const dx = e.clientX - lastX;
173
+ const dy = e.clientY - lastY;
174
+ lastX = e.clientX;
175
+ lastY = e.clientY;
176
+ if (dx === 0 && dy === 0) return;
177
+ onTranslate?.(widgetId, dx, dy);
178
+ }
179
+
180
+ function endDrag() {
181
+ draggingWidgetId = null;
182
+ document.removeEventListener('pointermove', onMove);
183
+ document.removeEventListener('pointerup', endDrag);
184
+ document.removeEventListener('pointercancel', endDrag);
185
+ document.body.style.removeProperty('user-select');
186
+ document.body.style.removeProperty('cursor');
187
+ onTranslateEnd?.(widgetId);
188
+ console.log('[WidgetShapeLivePreviews] Drag end', widgetId.substring(0, 8));
189
+ }
190
+
191
+ document.body.style.userSelect = 'none';
192
+ document.body.style.cursor = 'grabbing';
193
+ document.addEventListener('pointermove', onMove);
194
+ document.addEventListener('pointerup', endDrag);
195
+ document.addEventListener('pointercancel', endDrag);
196
+ console.log('[WidgetShapeLivePreviews] Drag start', widgetId.substring(0, 8));
197
+ }
198
+
199
+ function handleOpenChat(event: MouseEvent, widgetId: string) {
200
+ event.preventDefault();
201
+ event.stopPropagation();
202
+ onSelectShape?.(widgetId);
203
+ onOpenChat?.(widgetId);
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
+ });
301
+ </script>
302
+
303
+ {#if enabled && overlays.length > 0}
304
+ <div class="widget-shape-live-previews-ct pointer-events-none {className}" {style}>
305
+ {#each overlays as overlay (overlay.widgetId)}
306
+ {@const isSelected = selectedWidgetId === overlay.widgetId}
307
+ {@const isDragging = draggingWidgetId === overlay.widgetId}
308
+ {@const isResizing = resizingWidgetId === overlay.widgetId}
309
+ {@const blockIframePointer = isDragging || isResizing}
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
+ )}
325
+ <div
326
+ class="widget-shape-live-chrome-ct fixed"
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;"
328
+ >
329
+ <!-- Drag bar sits between rotation control and the shape top stroke -->
330
+ <!-- svelte-ignore a11y_no_static_element_interactions -->
331
+ <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 bg-white/95 px-1.5 shadow-sm select-none
333
+ {isSelected ? 'border-blue-400' : 'border-gray-300'}"
334
+ style="height: {DRAG_BAR_H}px; cursor: grab; min-width: {showExpandedChrome ? MIN_EXPANDED_CHROME_WIDTH : 0}px;"
335
+ title="Drag widget"
336
+ onpointerdown={(e) => startShapeDrag(e, overlay.widgetId)}
337
+ >
338
+ <span class="widget-shape-drag-grip shrink-0 text-xs leading-none text-gray-400" aria-hidden="true">⠿</span>
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}
438
+ <span
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"
440
+ >Edit mode</span>
441
+ {/if}
442
+
443
+ <div class="widget-shape-drag-bar-actions ml-auto flex shrink-0 items-center gap-0.5">
444
+ <button
445
+ type="button"
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"
447
+ title="Open chat"
448
+ aria-label="Open chat"
449
+ onclick={(e) => handleOpenChat(e, overlay.widgetId)}
450
+ onpointerdown={(e) => e.stopPropagation()}
451
+ >
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>
478
+ </button>
479
+ </div>
480
+ </div>
481
+
482
+ <div
483
+ class="widget-shape-live-preview-frame pointer-events-none absolute left-0 overflow-visible
484
+ {isSelected ? 'ring-2 ring-blue-400/80 rounded-sm' : ''}"
485
+ style="top: {DRAG_BAR_H + BAR_SHAPE_GAP}px; height: {overlay.height}px; width: {overlay.width}px;"
486
+ >
487
+ <!-- Inset so Fabric corner/rotate handles stay clickable around the edges -->
488
+ <iframe
489
+ class="widget-shape-live-preview-iframe pointer-events-auto absolute border-0 bg-white"
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);"
493
+ title="Live widget preview"
494
+ src={buildIframeSrc(overlay.src, overlay.widgetId)}
495
+ data-widget-id={overlay.widgetId}
496
+ tabindex="-1"
497
+ loading="eager"
498
+ sandbox="allow-scripts allow-same-origin allow-forms"
499
+ onload={() => markIframeLoaded(overlay.widgetId)}
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}
516
+ </div>
517
+ </div>
518
+ {/each}
519
+ </div>
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>
@@ -0,0 +1,64 @@
1
+ interface VersionOption {
2
+ id: string;
3
+ /** Full list label, e.g. "Version 9". */
4
+ label: string;
5
+ /** Compact trigger label, e.g. "v9". */
6
+ shortLabel?: string;
7
+ /** Published date/time shown in the list. */
8
+ dateLabel?: string;
9
+ /** True when this option is the live published version. */
10
+ isLive?: boolean;
11
+ }
12
+ interface Props {
13
+ class?: string;
14
+ style?: string;
15
+ enabled?: boolean;
16
+ /** Screen-space boxes for widget shapes currently on the canvas. */
17
+ getRects?: (() => Array<{
18
+ widgetId: string;
19
+ left: number;
20
+ top: number;
21
+ width: number;
22
+ height: number;
23
+ }>) | null;
24
+ /** Resolve a live preview URL for a widget (CDN published or Worker preview). */
25
+ resolveUrl?: ((widgetId: string) => string | null) | null;
26
+ /** Currently selected WidgetShape id (highlights chrome). */
27
+ selectedWidgetId?: string | null;
28
+ /** Translate shape by screen-pixel delta. */
29
+ onTranslate?: ((widgetId: string, dxScreen: number, dyScreen: number) => void) | null;
30
+ /** Fired once when a chrome drag ends (mark canvas dirty / history). */
31
+ onTranslateEnd?: ((widgetId: string) => void) | null;
32
+ /** Select the Fabric WidgetShape (e.g. when pressing the drag bar). */
33
+ onSelectShape?: ((widgetId: string) => void) | null;
34
+ /** Open Widget Details / chat for this widget. */
35
+ onOpenChat?: ((widgetId: string) => void) | null;
36
+ /** Resolve display title for the chrome bar. */
37
+ resolveTitle?: ((widgetId: string) => string | null) | null;
38
+ /** Current version label, e.g. "v3" or "Preview". */
39
+ resolveVersionLabel?: ((widgetId: string) => string | null) | null;
40
+ /** Available version options for the select (null/empty → show label only). */
41
+ resolveVersionOptions?: ((widgetId: string) => Array<VersionOption> | null) | null;
42
+ /** Currently selected version option id. */
43
+ resolveVersionSelectedId?: ((widgetId: string) => string | null) | null;
44
+ onVersionChange?: ((widgetId: string, versionId: string) => void) | null;
45
+ /** Promote a published version to Live (same as chat Set live). */
46
+ onSetLive?: ((widgetId: string, version: number) => void) | null;
47
+ /** Version currently being set live (disables that row's button). */
48
+ settingLiveVersion?: number | null;
49
+ /** Optional notify after local iframe reload (does not capture screenshots). */
50
+ onRefresh?: ((widgetId: string) => void) | null;
51
+ /** Bump when widget names / publish state change so chrome re-renders. */
52
+ titleRevision?: number;
53
+ /** Whether this widget has unpublished codegen ahead of Live (show Publish). */
54
+ resolveCanPublish?: ((widgetId: string) => boolean) | null;
55
+ /** Publish HEAD as a new live version (shape bar Publish button). */
56
+ onPublish?: ((widgetId: string) => void) | null;
57
+ /** True while publish is in flight for this widget. */
58
+ resolveIsPublishing?: ((widgetId: string) => boolean) | null;
59
+ }
60
+ declare const WidgetShapeLivePreviews: import("svelte").Component<Props, {
61
+ sync: () => void;
62
+ }, "">;
63
+ type WidgetShapeLivePreviews = ReturnType<typeof WidgetShapeLivePreviews>;
64
+ export default WidgetShapeLivePreviews;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@widgetic/creator",
3
- "version": "0.3.58",
3
+ "version": "0.3.60",
4
4
  "type": "module",
5
5
  "peerDependencies": {
6
6
  "@sveltejs/kit": "^2.20.0",