@widgetic/creator 0.3.64 → 0.3.66
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +15 -0
- package/dist/CreatorApp.svelte +650 -176
- package/dist/CreatorApp.svelte.d.ts +3 -1
- package/dist/components/VersionDropdownRow.svelte +5 -3
- package/dist/components/WidgetDetails.svelte +34 -14
- package/dist/components/WidgetDetails.svelte.d.ts +3 -0
- package/dist/components/WidgetShapeLivePreviews.svelte +114 -36
- package/dist/components/WidgetShapeLivePreviews.svelte.d.ts +4 -0
- package/dist/utils/freezeWatchdog.d.ts +38 -0
- package/dist/utils/freezeWatchdog.js +161 -0
- package/dist/utils/persistWidgetThumbnail.d.ts +12 -1
- package/dist/utils/persistWidgetThumbnail.js +14 -7
- package/dist/utils/pinScrollToBottom.d.ts +12 -0
- package/dist/utils/pinScrollToBottom.js +43 -0
- package/package.json +4 -2
|
@@ -37,6 +37,8 @@ declare const CreatorApp: $$__sveltets_2_IsomorphicComponent<{
|
|
|
37
37
|
style?: string;
|
|
38
38
|
}, {
|
|
39
39
|
[evt: string]: CustomEvent<any>;
|
|
40
|
-
}, {}, {
|
|
40
|
+
}, {}, {
|
|
41
|
+
onBack: CreatorAppProps["onBack"];
|
|
42
|
+
}, string>;
|
|
41
43
|
type CreatorApp = InstanceType<typeof CreatorApp>;
|
|
42
44
|
export default CreatorApp;
|
|
@@ -42,14 +42,16 @@
|
|
|
42
42
|
// One fixed width per column — identical on every row and in both dropdowns.
|
|
43
43
|
// Explicit PIXEL widths that hug the content (vN, 7-char SHA, dd/mm/yyyy
|
|
44
44
|
// hh:mm) — no rem sizing, no floating side padding.
|
|
45
|
-
const COL_VERSION = 'w-[
|
|
45
|
+
const COL_VERSION = 'w-[20px] justify-end';
|
|
46
46
|
const COL_COMMIT = 'w-[44px] justify-end';
|
|
47
47
|
const COL_DATE = 'w-[88px]';
|
|
48
|
-
|
|
48
|
+
// Same width as the header Publish/Live button (min-w-[4.75rem] / w-[4.75rem]) so every
|
|
49
|
+
// action button lines up in one column across rows, header and overlay.
|
|
50
|
+
const COL_ACTION = 'w-[4.75rem]';
|
|
49
51
|
</script>
|
|
50
52
|
|
|
51
53
|
<div
|
|
52
|
-
class="version-dropdown-row flex items-center gap-2 rounded px-2 py-1.5 w-
|
|
54
|
+
class="version-dropdown-row flex items-center justify-between gap-2 rounded px-2 py-1.5 w-full text-[10px] cursor-pointer {selected ? 'bg-emerald-50 text-emerald-800' : 'text-gray-700 hover:bg-gray-50'}"
|
|
53
55
|
role="option"
|
|
54
56
|
aria-selected={selected}
|
|
55
57
|
tabindex="0"
|
|
@@ -69,7 +69,9 @@
|
|
|
69
69
|
| { type: 'save' };
|
|
70
70
|
|
|
71
71
|
export let userSession: { jwt?: string; user?: { id?: string } } | null = null;
|
|
72
|
-
|
|
72
|
+
// Passed by CreatorApp but never read here — export const keeps the
|
|
73
|
+
// init-set contract without the unused-reactive warning.
|
|
74
|
+
export const hasValidToken: boolean = false;
|
|
73
75
|
export let selectedWidgetId: string | null = null;
|
|
74
76
|
export let selectedWidget: WidgetSummary | null = null;
|
|
75
77
|
export let isGeneratingCode: boolean = false;
|
|
@@ -81,11 +83,13 @@
|
|
|
81
83
|
/** 'pending' = repo being created (show loader); 'failed' = show Create Repository; 'ready' = normal. */
|
|
82
84
|
export let repositorySetupStatus: 'pending' | 'ready' | 'failed' = 'ready';
|
|
83
85
|
export let generateCodeStatus: string | null = null;
|
|
84
|
-
export
|
|
86
|
+
export const isCheckingActiveGeneration: boolean = false;
|
|
85
87
|
export let lastCommitId: string | null = null;
|
|
86
88
|
export let debugMode: boolean = false;
|
|
87
89
|
export let forceMockReady: boolean = false;
|
|
88
|
-
|
|
90
|
+
// Passed by CreatorApp but never read here — export const keeps the
|
|
91
|
+
// init-set contract without the unused-reactive warning.
|
|
92
|
+
export const debugModeAvailable: boolean = false;
|
|
89
93
|
|
|
90
94
|
// Code gen / save / publish state — passed through to parent's slot content.
|
|
91
95
|
// svelte-ignore export_let_unused — these are consumed by <svelte:fragment> slots in the parent.
|
|
@@ -767,6 +771,7 @@
|
|
|
767
771
|
getResizeLimits?: () => { maxWidth: number; maxHeight: number };
|
|
768
772
|
fitToContainer?: () => void;
|
|
769
773
|
resetPreviewPosition?: () => void;
|
|
774
|
+
nudgeIframePaint?: () => void;
|
|
770
775
|
} | undefined;
|
|
771
776
|
let previewIframe: HTMLIFrameElement | null = null;
|
|
772
777
|
let isLoadingDirectPreviewUrl = false;
|
|
@@ -806,6 +811,14 @@
|
|
|
806
811
|
/** Parent calls this when a generation starts for THIS widget's panel. */
|
|
807
812
|
export function markCodegenStarted(): void {
|
|
808
813
|
codegenStartedAt = Date.now();
|
|
814
|
+
// A pinned published artifact — a version picked from the history dropdown —
|
|
815
|
+
// makes the Worker build event a no-op, so the panel went on showing that
|
|
816
|
+
// older version after the generation finished. The code being generated is
|
|
817
|
+
// what the user just asked to see, so the pin does not survive a generation.
|
|
818
|
+
pinnedPublishedPreviewUrl = null;
|
|
819
|
+
// The build event is judged against the previous generation's signature
|
|
820
|
+
// otherwise, and a Worker cache hit can reproduce it exactly.
|
|
821
|
+
lastWsPreviewSignature = null;
|
|
809
822
|
}
|
|
810
823
|
|
|
811
824
|
/**
|
|
@@ -2046,7 +2059,8 @@
|
|
|
2046
2059
|
|
|
2047
2060
|
function normalizeRepositoryId(source: WidgetSummary | null): string | null {
|
|
2048
2061
|
if (!source) return null;
|
|
2049
|
-
|
|
2062
|
+
const candidate = source.repositoryId || source.repo_id || source.repoId;
|
|
2063
|
+
return typeof candidate === 'string' ? candidate : null;
|
|
2050
2064
|
}
|
|
2051
2065
|
|
|
2052
2066
|
// ═══════════════════════════════════════════════════════════════════════
|
|
@@ -3159,10 +3173,18 @@
|
|
|
3159
3173
|
|
|
3160
3174
|
let lastWsPreviewSignature: string | null = null;
|
|
3161
3175
|
|
|
3162
|
-
async function applyWorkerPreviewFromEvent(
|
|
3176
|
+
async function applyWorkerPreviewFromEvent(
|
|
3177
|
+
workerUrl: string,
|
|
3178
|
+
buildTimeMs?: number | null,
|
|
3179
|
+
eventTimestamp?: string | null,
|
|
3180
|
+
): Promise<void> {
|
|
3163
3181
|
if (pinnedPublishedPreviewUrl) return;
|
|
3164
3182
|
const fetchUrl = toWorkerPreviewFetchUrl(workerUrl);
|
|
3165
|
-
|
|
3183
|
+
// The Worker serves every build of a widget at the same /preview URL, and
|
|
3184
|
+
// buildTimeMs repeats across builds (0 on a cache hit), so url + buildTimeMs
|
|
3185
|
+
// alone made a second generation look like a replay of the first and the
|
|
3186
|
+
// iframe kept the previous version. The per-event timestamp separates them.
|
|
3187
|
+
const signature = `${fetchUrl}:${buildTimeMs ?? 'na'}:${eventTimestamp ?? 'na'}:${selectedWidgetId ?? ''}`;
|
|
3166
3188
|
if (signature === lastWsPreviewSignature) return;
|
|
3167
3189
|
lastWsPreviewSignature = signature;
|
|
3168
3190
|
|
|
@@ -3189,6 +3211,7 @@
|
|
|
3189
3211
|
void applyWorkerPreviewFromEvent(
|
|
3190
3212
|
$dynamicWorkerPreviewStatus.previewUrl,
|
|
3191
3213
|
$dynamicWorkerPreviewStatus.buildTimeMs,
|
|
3214
|
+
$dynamicWorkerPreviewStatus.timestamp,
|
|
3192
3215
|
);
|
|
3193
3216
|
}
|
|
3194
3217
|
|
|
@@ -3685,14 +3708,11 @@
|
|
|
3685
3708
|
<Button onclick={() => { showPreviewModal = false; }} variant="outline" size="sm" class="close-preview-modal-btn shrink-0 w-6 h-6 p-[3px] cursor-pointer border border-black text-md bg-white text-black hover:bg-gray-100 rounded-lg">X</Button>
|
|
3686
3709
|
</div>
|
|
3687
3710
|
</div>
|
|
3688
|
-
{/if}
|
|
3689
|
-
|
|
3690
|
-
|
|
3691
|
-
|
|
3692
|
-
|
|
3693
|
-
resizeMode="concentric"
|
|
3694
|
-
defaultStageAlign="center"
|
|
3695
|
-
autoFitStage={showPreviewModal}
|
|
3711
|
+
{/if} <WidgetPreview
|
|
3712
|
+
bind:this={widgetPreviewComponentRef}
|
|
3713
|
+
mode="controlled"
|
|
3714
|
+
enableResize={true}
|
|
3715
|
+
autoFitStage={showPreviewModal}
|
|
3696
3716
|
src={iframeSrc || getFallbackPreviewUrl()}
|
|
3697
3717
|
displayUrl={directPreviewUrl || iframeSrc}
|
|
3698
3718
|
bind:resizeWidth={previewResizeWidth}
|
|
@@ -201,6 +201,9 @@ declare const WidgetDetails: $$__sveltets_2_IsomorphicComponent<{
|
|
|
201
201
|
'composition-editor': {};
|
|
202
202
|
'embed-section': {};
|
|
203
203
|
}, {
|
|
204
|
+
hasValidToken: boolean;
|
|
205
|
+
isCheckingActiveGeneration: boolean;
|
|
206
|
+
debugModeAvailable: boolean;
|
|
204
207
|
beginCompositionHydration: () => void;
|
|
205
208
|
showPublishedArtifact: (url: string | null) => void;
|
|
206
209
|
finishCompositionHydration: () => void;
|
|
@@ -49,6 +49,8 @@
|
|
|
49
49
|
onTranslate?: ((widgetId: string, dxScreen: number, dyScreen: number) => void) | null;
|
|
50
50
|
/** Fired once when a chrome drag ends (mark canvas dirty / history). */
|
|
51
51
|
onTranslateEnd?: ((widgetId: string) => void) | null;
|
|
52
|
+
/** Tell the canvas a chrome drag started/ended (suppresses hover-select). */
|
|
53
|
+
onOverlayDragChange?: ((active: boolean) => void) | null;
|
|
52
54
|
/** Select the Fabric WidgetShape (e.g. when pressing the drag bar). */
|
|
53
55
|
onSelectShape?: ((widgetId: string) => void) | null;
|
|
54
56
|
/** Open Widget Details / chat for this widget. */
|
|
@@ -86,6 +88,8 @@
|
|
|
86
88
|
resolveIsPublishing?: ((widgetId: string) => boolean) | null;
|
|
87
89
|
/** Publish-disabled reason for a specific Draft row (null = enabled). */
|
|
88
90
|
resolvePublishDisabledReason?: ((widgetId: string, version: number) => string | null) | null;
|
|
91
|
+
/** Whether code generation is running (suppresses background captures / reloads). */
|
|
92
|
+
isGenerating?: boolean;
|
|
89
93
|
}
|
|
90
94
|
|
|
91
95
|
let {
|
|
@@ -103,6 +107,7 @@
|
|
|
103
107
|
resolvePublishDisabled = null,
|
|
104
108
|
onTranslate = null,
|
|
105
109
|
onTranslateEnd = null,
|
|
110
|
+
onOverlayDragChange = null,
|
|
106
111
|
onSelectShape = null,
|
|
107
112
|
onOpenChat = null,
|
|
108
113
|
resolveTitle = null,
|
|
@@ -121,6 +126,7 @@
|
|
|
121
126
|
resolveIsPublishing = null,
|
|
122
127
|
/** Per-widget publish-gate reason for a Draft row (tooltip on Publish). */
|
|
123
128
|
resolvePublishDisabledReason = null,
|
|
129
|
+
isGenerating = false,
|
|
124
130
|
}: Props = $props();
|
|
125
131
|
|
|
126
132
|
const DRAG_BAR_H = 30;
|
|
@@ -130,7 +136,11 @@
|
|
|
130
136
|
const MIN_EXPANDED_CHROME_WIDTH = 220;
|
|
131
137
|
const IFRAME_INSET_PX = 2;
|
|
132
138
|
/** ~5 version rows before the menu scrolls (matches chat history panel). */
|
|
133
|
-
|
|
139
|
+
/** Row height ≈2.1rem (py-1.5 + text) + 0.6rem vertical padding for the menu box. */
|
|
140
|
+
const VERSION_MENU_ROW_REM = 2.1;
|
|
141
|
+
const VERSION_MENU_PAD_REM = 0.6;
|
|
142
|
+
/** Menu shows at most 5 rows before scrolling. */
|
|
143
|
+
const VERSION_MENU_MAX_ROWS = 5;
|
|
134
144
|
|
|
135
145
|
type ShapeRect = {
|
|
136
146
|
widgetId: string;
|
|
@@ -177,15 +187,51 @@
|
|
|
177
187
|
* at the same URL (codegen rebuild). `sync()` only reloads when `src` changes,
|
|
178
188
|
* so without a cache-bust token the overlay keeps showing pre-codegen content.
|
|
179
189
|
* Bumps the reload token → `{#key}` remounts the iframe with a fresh `_t`.
|
|
190
|
+
*
|
|
191
|
+
* Reloads closer together than RELOAD_COALESCE_MS are collapsed into one, so
|
|
192
|
+
* the WS build event and the codegen `completed` handler do not flicker the
|
|
193
|
+
* overlay twice. The extra call is deferred rather than dropped: the later
|
|
194
|
+
* request is the authoritative one (codegen reloads after the build event, and
|
|
195
|
+
* a build-repair loop builds more than once), and dropping it left the overlay
|
|
196
|
+
* showing the version from before the generation.
|
|
180
197
|
*/
|
|
198
|
+
const RELOAD_COALESCE_MS = 1200;
|
|
199
|
+
let lastReloadAtByWidgetId: Record<string, number> = {};
|
|
200
|
+
const coalescedReloadTimers = new Map<string, ReturnType<typeof setTimeout>>();
|
|
201
|
+
|
|
181
202
|
export function requestReload(widgetId: string): void {
|
|
182
|
-
const
|
|
203
|
+
const now = Date.now();
|
|
204
|
+
const sinceLast = now - (lastReloadAtByWidgetId[widgetId] || 0);
|
|
205
|
+
if (sinceLast < RELOAD_COALESCE_MS) {
|
|
206
|
+
if (!coalescedReloadTimers.has(widgetId)) {
|
|
207
|
+
coalescedReloadTimers.set(
|
|
208
|
+
widgetId,
|
|
209
|
+
setTimeout(() => {
|
|
210
|
+
coalescedReloadTimers.delete(widgetId);
|
|
211
|
+
requestReload(widgetId);
|
|
212
|
+
}, RELOAD_COALESCE_MS - sinceLast),
|
|
213
|
+
);
|
|
214
|
+
}
|
|
215
|
+
return;
|
|
216
|
+
}
|
|
217
|
+
const coalesced = coalescedReloadTimers.get(widgetId);
|
|
218
|
+
if (coalesced) {
|
|
219
|
+
clearTimeout(coalesced);
|
|
220
|
+
coalescedReloadTimers.delete(widgetId);
|
|
221
|
+
}
|
|
222
|
+
lastReloadAtByWidgetId[widgetId] = now;
|
|
223
|
+
cancelPendingRefresh(widgetId);
|
|
224
|
+
const token = Math.max(now, (reloadTokens[widgetId] || 0) + 1);
|
|
225
|
+
pendingRefreshTokens.set(widgetId, token);
|
|
183
226
|
reloadTokens = { ...reloadTokens, [widgetId]: token };
|
|
184
227
|
iframeLoadedByWidgetId = { ...iframeLoadedByWidgetId, [widgetId]: false };
|
|
228
|
+
sync();
|
|
185
229
|
}
|
|
186
230
|
|
|
187
231
|
/** Which shape chrome version menu is open. */
|
|
188
232
|
let openVersionMenuWidgetId = $state<string | null>(null);
|
|
233
|
+
/** Local manual version selection override per widget (ensures trigger updates immediately). */
|
|
234
|
+
let localSelectedVersionByWidgetId = $state<Record<string, string>>({});
|
|
189
235
|
/** Hide Fabric placeholder flash — show loader until iframe fires load. */
|
|
190
236
|
let iframeLoadedByWidgetId = $state<Record<string, boolean>>({});
|
|
191
237
|
|
|
@@ -263,11 +309,13 @@
|
|
|
263
309
|
document.body.style.removeProperty('user-select');
|
|
264
310
|
document.body.style.removeProperty('cursor');
|
|
265
311
|
onTranslateEnd?.(widgetId);
|
|
312
|
+
onOverlayDragChange?.(false);
|
|
266
313
|
console.log('[WidgetShapeLivePreviews] Drag end', widgetId.substring(0, 8));
|
|
267
314
|
}
|
|
268
315
|
|
|
269
316
|
document.body.style.userSelect = 'none';
|
|
270
317
|
document.body.style.cursor = 'grabbing';
|
|
318
|
+
onOverlayDragChange?.(true);
|
|
271
319
|
document.addEventListener('pointermove', onMove);
|
|
272
320
|
document.addEventListener('pointerup', endDrag);
|
|
273
321
|
document.addEventListener('pointercancel', endDrag);
|
|
@@ -303,6 +351,11 @@
|
|
|
303
351
|
// An old load event must not mark the replacement loaded or claim its timer.
|
|
304
352
|
if (!matchesFrame()) return;
|
|
305
353
|
iframeLoadedByWidgetId = { ...iframeLoadedByWidgetId, [widgetId]: true };
|
|
354
|
+
// Suppress auto-refresh / screenshot capture when code generation is running.
|
|
355
|
+
if (isGenerating) {
|
|
356
|
+
cancelPendingRefresh(widgetId);
|
|
357
|
+
return;
|
|
358
|
+
}
|
|
306
359
|
const token = pendingRefreshTokens.get(widgetId);
|
|
307
360
|
if (!token || refreshSettleTimers.has(widgetId)) return;
|
|
308
361
|
const isCurrent = () => matchesFrame()
|
|
@@ -332,6 +385,8 @@
|
|
|
332
385
|
}
|
|
333
386
|
|
|
334
387
|
function resolveSelectedVersionId(widgetId: string, options: VersionOption[]): string {
|
|
388
|
+
const local = localSelectedVersionByWidgetId[widgetId];
|
|
389
|
+
if (local && options.some((o) => o.id === local)) return local;
|
|
335
390
|
const fromParent = resolveVersionSelectedId?.(widgetId)?.trim();
|
|
336
391
|
if (fromParent && options.some((o) => o.id === fromParent)) return fromParent;
|
|
337
392
|
const liveOpt = options.find((o) => o.isLive);
|
|
@@ -404,6 +459,7 @@
|
|
|
404
459
|
function selectVersionFromMenu(widgetId: string, versionId: string) {
|
|
405
460
|
openVersionMenuWidgetId = null;
|
|
406
461
|
if (!versionId) return;
|
|
462
|
+
localSelectedVersionByWidgetId = { ...localSelectedVersionByWidgetId, [widgetId]: versionId };
|
|
407
463
|
onVersionChange?.(widgetId, versionId);
|
|
408
464
|
}
|
|
409
465
|
|
|
@@ -438,8 +494,15 @@
|
|
|
438
494
|
*/
|
|
439
495
|
function trackIframeSelection(iframe, widgetId) {
|
|
440
496
|
let focusTimer;
|
|
497
|
+
let pointerIsDown = false;
|
|
498
|
+
const onWindowPointerDown = () => { pointerIsDown = true; };
|
|
499
|
+
const onWindowPointerUp = () => { pointerIsDown = false; };
|
|
441
500
|
function select() {
|
|
442
501
|
if (!enabled || draggingWidgetId || resizingWidgetId || !iframe.isConnected) return;
|
|
502
|
+
// A drag in progress anywhere (Fabric shape drag or overlay chrome drag)
|
|
503
|
+
// must not let hover over this iframe steal the selection — the pointer
|
|
504
|
+
// merely passes over other shapes while carrying the dragged one.
|
|
505
|
+
if (pointerIsDown) return;
|
|
443
506
|
// Keep the menu owner selected while its dropdown is being used.
|
|
444
507
|
if (openVersionMenuWidgetId && openVersionMenuWidgetId !== widgetId) return;
|
|
445
508
|
if (selectedWidgetId !== widgetId) onSelectShape?.(widgetId);
|
|
@@ -450,6 +513,8 @@
|
|
|
450
513
|
if (document.activeElement === iframe) select();
|
|
451
514
|
}, 0);
|
|
452
515
|
}
|
|
516
|
+
window.addEventListener('pointerdown', onWindowPointerDown, true);
|
|
517
|
+
window.addEventListener('pointerup', onWindowPointerUp, true);
|
|
453
518
|
iframe.addEventListener('pointerenter', select);
|
|
454
519
|
iframe.addEventListener('focus', select);
|
|
455
520
|
window.addEventListener('blur', checkFocus);
|
|
@@ -457,6 +522,8 @@
|
|
|
457
522
|
update(nextWidgetId) { widgetId = nextWidgetId; },
|
|
458
523
|
destroy() {
|
|
459
524
|
clearTimeout(focusTimer);
|
|
525
|
+
window.removeEventListener('pointerdown', onWindowPointerDown, true);
|
|
526
|
+
window.removeEventListener('pointerup', onWindowPointerUp, true);
|
|
460
527
|
iframe.removeEventListener('pointerenter', select);
|
|
461
528
|
iframe.removeEventListener('focus', select);
|
|
462
529
|
window.removeEventListener('blur', checkFocus);
|
|
@@ -498,6 +565,8 @@
|
|
|
498
565
|
return () => {
|
|
499
566
|
cancelAnimationFrame(initialSyncFrame);
|
|
500
567
|
for (const widgetId of [...pendingRefreshTokens.keys()]) cancelPendingRefresh(widgetId);
|
|
568
|
+
for (const timer of coalescedReloadTimers.values()) clearTimeout(timer);
|
|
569
|
+
coalescedReloadTimers.clear();
|
|
501
570
|
document.removeEventListener('pointerdown', handleDocumentPointerDown, true);
|
|
502
571
|
document.removeEventListener('keydown', handleDocumentKeydown);
|
|
503
572
|
window.removeEventListener('widgetic:widget-shape-pointer-block', onShapePointerBlock);
|
|
@@ -604,7 +673,7 @@
|
|
|
604
673
|
>
|
|
605
674
|
<button
|
|
606
675
|
type="button"
|
|
607
|
-
class="widget-shape-publish-btn shrink-0 inline-flex items-center justify-center min-w-[
|
|
676
|
+
class="widget-shape-publish-btn shrink-0 inline-flex items-center justify-center min-w-[4.75rem] w-[4.75rem] rounded border border-emerald-600 bg-emerald-600 px-1 py-0.5 text-[10px] font-semibold uppercase tracking-wide text-white hover:bg-emerald-700 disabled:opacity-50 cursor-pointer"
|
|
608
677
|
disabled={isPublishing || selectedPreviewBlocked || !!resolvePublishDisabled?.(overlay.widgetId, selectedVersion ? Number(selectedVersion.id.slice(2)) : null)}
|
|
609
678
|
onclick={(e) => {
|
|
610
679
|
e.stopPropagation();
|
|
@@ -620,7 +689,7 @@
|
|
|
620
689
|
class="inline-flex shrink-0"
|
|
621
690
|
>
|
|
622
691
|
<span
|
|
623
|
-
class="widget-shape-live-btn shrink-0 inline-flex items-center justify-center min-w-[
|
|
692
|
+
class="widget-shape-live-btn shrink-0 inline-flex items-center justify-center min-w-[4.75rem] w-[4.75rem] rounded bg-emerald-100 px-1 py-0.5 text-[10px] font-semibold uppercase tracking-wide text-emerald-600 opacity-80"
|
|
624
693
|
>Live</span>
|
|
625
694
|
</HoverTooltip>
|
|
626
695
|
{:else if selectedVersion && onSetLive && !isDraftVersion(selectedVersion)}
|
|
@@ -631,7 +700,7 @@
|
|
|
631
700
|
>
|
|
632
701
|
<button
|
|
633
702
|
type="button"
|
|
634
|
-
class="widget-shape-set-live-btn shrink-0 inline-flex items-center justify-center min-w-[
|
|
703
|
+
class="widget-shape-set-live-btn shrink-0 inline-flex items-center justify-center min-w-[4.75rem] w-[4.75rem] rounded border border-emerald-300 px-1 py-0.5 text-[10px] font-semibold uppercase tracking-wide text-emerald-700 hover:bg-emerald-100 disabled:opacity-50 cursor-pointer"
|
|
635
704
|
disabled={settingLiveVersion === Number(selectedVersion.id.slice(2))}
|
|
636
705
|
onclick={(e) => handleSetLiveClick(e, overlay.widgetId, selectedVersion.id)}
|
|
637
706
|
onpointerdown={(e) => e.stopPropagation()}
|
|
@@ -646,7 +715,7 @@
|
|
|
646
715
|
>
|
|
647
716
|
<button
|
|
648
717
|
type="button"
|
|
649
|
-
class="widget-shape-publish-btn shrink-0 inline-flex items-center justify-center min-w-[
|
|
718
|
+
class="widget-shape-publish-btn shrink-0 inline-flex items-center justify-center min-w-[4.75rem] w-[4.75rem] rounded border border-emerald-600 bg-emerald-600 px-1 py-0.5 text-[10px] font-semibold uppercase tracking-wide text-white hover:bg-emerald-700 disabled:opacity-50 cursor-pointer"
|
|
650
719
|
disabled={isPublishing || selectedPreviewBlocked || !resolveCanPublishDraft?.(overlay.widgetId, Number(selectedVersion.id.slice(2))) || !!resolvePublishDisabled?.(overlay.widgetId, Number(selectedVersion.id.slice(2)))}
|
|
651
720
|
onclick={(e) => handlePublishDraftClick(e, overlay.widgetId, selectedVersion.id)}
|
|
652
721
|
onpointerdown={(e) => e.stopPropagation()}
|
|
@@ -659,39 +728,45 @@
|
|
|
659
728
|
class="inline-flex shrink-0"
|
|
660
729
|
>
|
|
661
730
|
<span
|
|
662
|
-
class="widget-shape-draft-badge shrink-0 inline-flex items-center justify-center min-w-[
|
|
731
|
+
class="widget-shape-draft-badge shrink-0 inline-flex items-center justify-center min-w-[4.75rem] w-[4.75rem] rounded bg-amber-100 px-1 py-0.5 text-[10px] font-semibold uppercase tracking-wide text-amber-700"
|
|
663
732
|
>Draft</span>
|
|
664
733
|
</HoverTooltip>
|
|
665
734
|
{/if}
|
|
666
735
|
{/if}
|
|
667
736
|
{#if versionMenuOpen}
|
|
737
|
+
{@const hasScroll = versionOptions.length > 5}
|
|
668
738
|
<div
|
|
669
|
-
class="widget-shape-version-menu absolute
|
|
670
|
-
style="
|
|
739
|
+
class="widget-shape-version-menu absolute top-full z-30 mt-0.5 w-max rounded-md border border-emerald-200 bg-white p-1 shadow-lg"
|
|
740
|
+
style="right: -12px;"
|
|
671
741
|
role="listbox"
|
|
672
742
|
>
|
|
673
|
-
|
|
674
|
-
{
|
|
675
|
-
{
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
743
|
+
<div
|
|
744
|
+
class="widget-shape-version-scroll flex flex-col gap-1 overflow-x-hidden {hasScroll ? 'overflow-y-auto' : 'overflow-y-visible'}"
|
|
745
|
+
style="{hasScroll ? 'max-height: 185px; margin-right: -4px; padding-right: 4px;' : 'max-height: none;'} scrollbar-width: thin; scrollbar-color: rgba(0,0,0,0.2) transparent;"
|
|
746
|
+
>
|
|
747
|
+
{#each versionOptions as option (option.id)}
|
|
748
|
+
{@const optionVersion = Number(option.id.slice(2))}
|
|
749
|
+
{@const optionBusy = (void titleRevision, !!resolveIsPublishing?.(overlay.widgetId))}
|
|
750
|
+
{@const optionPublishDisabled = !resolveCanPublishDraft?.(overlay.widgetId, optionVersion)
|
|
751
|
+
|| !!resolvePublishDisabled?.(overlay.widgetId, optionVersion)
|
|
752
|
+
|| (selectedVersion?.id === option.id && selectedPreviewBlocked)}
|
|
753
|
+
{@const optionPublishReason = (void titleRevision, resolvePublishDisabledReason?.(overlay.widgetId, optionVersion))}
|
|
754
|
+
<VersionDropdownRow
|
|
755
|
+
versionLabel={option.shortLabel || option.label}
|
|
756
|
+
dateLabel={option.dateLabel}
|
|
757
|
+
isLive={option.isLive === true}
|
|
758
|
+
isDraft={isDraftVersion(option)}
|
|
759
|
+
selected={selectedVersion?.id === option.id}
|
|
760
|
+
busy={optionBusy}
|
|
761
|
+
publishDisabled={optionPublishDisabled}
|
|
762
|
+
publishDisabledReason={optionPublishReason || 'Build this draft version and make it the Live version'}
|
|
763
|
+
setLiveDisabled={settingLiveVersion === optionVersion}
|
|
764
|
+
onselect={() => selectVersionFromMenu(overlay.widgetId, option.id)}
|
|
765
|
+
onpublish={() => handlePublishDraftClickFromMenu(overlay.widgetId, option.id)}
|
|
766
|
+
onsetlive={() => handleSetLiveClickFromMenu(overlay.widgetId, option.id)}
|
|
767
|
+
/>
|
|
768
|
+
{/each}
|
|
769
|
+
</div>
|
|
695
770
|
</div>
|
|
696
771
|
{/if}
|
|
697
772
|
</div>
|
|
@@ -812,16 +887,19 @@
|
|
|
812
887
|
{/if}
|
|
813
888
|
|
|
814
889
|
<style>
|
|
815
|
-
.widget-shape-version-
|
|
816
|
-
width:
|
|
817
|
-
height:
|
|
890
|
+
.widget-shape-version-scroll::-webkit-scrollbar {
|
|
891
|
+
width: 4px;
|
|
892
|
+
height: 4px;
|
|
818
893
|
background: transparent;
|
|
819
894
|
}
|
|
820
|
-
.widget-shape-version-
|
|
895
|
+
.widget-shape-version-scroll::-webkit-scrollbar-track {
|
|
821
896
|
background: transparent;
|
|
822
897
|
}
|
|
823
|
-
.widget-shape-version-
|
|
898
|
+
.widget-shape-version-scroll::-webkit-scrollbar-thumb {
|
|
824
899
|
background: rgba(0, 0, 0, 0.2);
|
|
825
900
|
border-radius: 4px;
|
|
826
901
|
}
|
|
902
|
+
.widget-shape-version-scroll::-webkit-scrollbar-thumb:hover {
|
|
903
|
+
background: rgba(0, 0, 0, 0.35);
|
|
904
|
+
}
|
|
827
905
|
</style>
|
|
@@ -51,6 +51,8 @@ interface Props {
|
|
|
51
51
|
onTranslate?: ((widgetId: string, dxScreen: number, dyScreen: number) => void) | null;
|
|
52
52
|
/** Fired once when a chrome drag ends (mark canvas dirty / history). */
|
|
53
53
|
onTranslateEnd?: ((widgetId: string) => void) | null;
|
|
54
|
+
/** Tell the canvas a chrome drag started/ended (suppresses hover-select). */
|
|
55
|
+
onOverlayDragChange?: ((active: boolean) => void) | null;
|
|
54
56
|
/** Select the Fabric WidgetShape (e.g. when pressing the drag bar). */
|
|
55
57
|
onSelectShape?: ((widgetId: string) => void) | null;
|
|
56
58
|
/** Open Widget Details / chat for this widget. */
|
|
@@ -88,6 +90,8 @@ interface Props {
|
|
|
88
90
|
resolveIsPublishing?: ((widgetId: string) => boolean) | null;
|
|
89
91
|
/** Publish-disabled reason for a specific Draft row (null = enabled). */
|
|
90
92
|
resolvePublishDisabledReason?: ((widgetId: string, version: number) => string | null) | null;
|
|
93
|
+
/** Whether code generation is running (suppresses background captures / reloads). */
|
|
94
|
+
isGenerating?: boolean;
|
|
91
95
|
}
|
|
92
96
|
declare const WidgetShapeLivePreviews: import("svelte").Component<Props, {
|
|
93
97
|
requestReload: (widgetId: string) => void;
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Main-thread responsiveness watchdog.
|
|
3
|
+
*
|
|
4
|
+
* PerformanceObserver('longtask') only fires AFTER a blocking task completes —
|
|
5
|
+
* an infinite reactive loop, or heavy work running inside a same-origin iframe
|
|
6
|
+
* (html2canvas screenshot captures in the widget preview share the parent's main
|
|
7
|
+
* thread), is never attributed to the page. The result was a frozen site with no
|
|
8
|
+
* [FreezeReport] in the console.
|
|
9
|
+
*
|
|
10
|
+
* This watchdog flips the model: a Web Worker (own thread, unaffected by the
|
|
11
|
+
* blocked main thread) pings every 500ms and the main thread answers. When pongs
|
|
12
|
+
* stop arriving, the worker logs a [FreezeReport] straight to the page console
|
|
13
|
+
* (worker console output shows in DevTools) with the block duration and the last
|
|
14
|
+
* codegen status the main thread reported. It also reports recovery, so each
|
|
15
|
+
* freeze episode produces exactly two lines: block start + recovery.
|
|
16
|
+
*
|
|
17
|
+
* The duration alone never said *what* blocked, which is why the freeze survived
|
|
18
|
+
* several rounds of fixes. So the main thread also keeps breadcrumbs — long tasks
|
|
19
|
+
* seen by PerformanceObserver (with their iframe attribution, since a widget
|
|
20
|
+
* preview blocking the shared main thread looks identical to the page blocking
|
|
21
|
+
* itself) and explicit `measureSync` spans — and ships the ones it has not
|
|
22
|
+
* reported yet on the next pong. The pong that lands on recovery therefore
|
|
23
|
+
* carries the task that caused the freeze, named and measured.
|
|
24
|
+
*/
|
|
25
|
+
export interface FreezeWatchdog {
|
|
26
|
+
/** Main thread reports the current codegen status; included in freeze reports. */
|
|
27
|
+
setStatus(status: string): void;
|
|
28
|
+
/** Record a synchronous span so a freeze report can name it. */
|
|
29
|
+
addBreadcrumb(label: string, duration: number, source?: string): void;
|
|
30
|
+
dispose(): void;
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* Times a synchronous step and hands it to the watchdog when it blocks long
|
|
34
|
+
* enough to matter. Cheap enough to leave on any suspect path permanently: two
|
|
35
|
+
* `performance.now()` reads and, for ordinary fast calls, nothing else.
|
|
36
|
+
*/
|
|
37
|
+
export declare function measureSync<T>(label: string, run: () => T): T;
|
|
38
|
+
export declare function installFreezeWatchdog(): FreezeWatchdog | null;
|