@widgetic/creator 0.3.66 → 0.3.68
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 +52 -98
- package/dist/CreatorApp.svelte +512 -147
- package/dist/components/WidgetDetails.svelte +32 -3
- package/dist/components/WidgetDetails.svelte.d.ts +2 -0
- package/dist/components/WidgetShapeLivePreviews.svelte +133 -46
- package/dist/components/WidgetShapeLivePreviews.svelte.d.ts +2 -0
- package/dist/stores/websocketStore.d.ts +1 -1
- package/dist/stores/websocketStore.js +1 -1
- package/dist/utils/creatorDraftStorage.d.ts +5 -0
- package/dist/utils/creatorDraftStorage.js +41 -4
- package/dist/utils/freezeWatchdog.js +38 -7
- package/package.json +4 -5
|
@@ -806,6 +806,11 @@
|
|
|
806
806
|
// console.log('[WidgetDetails] Codegen preview marked fresh — skipping GitLab rebuilds for 30s');
|
|
807
807
|
}
|
|
808
808
|
|
|
809
|
+
/** Restore Checkpoint must compile GitLab HEAD even inside the post-codegen window. */
|
|
810
|
+
export function clearCodegenPreviewFresh(): void {
|
|
811
|
+
codegenPreviewFreshUntil = 0;
|
|
812
|
+
}
|
|
813
|
+
|
|
809
814
|
/** Timestamp of generation start — set by the parent when the operation begins. */
|
|
810
815
|
let codegenStartedAt: number | null = null;
|
|
811
816
|
/** Parent calls this when a generation starts for THIS widget's panel. */
|
|
@@ -2394,6 +2399,10 @@
|
|
|
2394
2399
|
if (previewHealthCheckHandle) { clearInterval(previewHealthCheckHandle); previewHealthCheckHandle = null; }
|
|
2395
2400
|
previewFallbackHandle = setTimeout(() => {
|
|
2396
2401
|
if ((previewLoading || previewHydrating) && (directPreviewUrl || iframeSrc)) {
|
|
2402
|
+
if (previewWidgetReady && !previewHydrating) {
|
|
2403
|
+
previewLoading = false;
|
|
2404
|
+
return;
|
|
2405
|
+
}
|
|
2397
2406
|
console.warn('[WidgetDetails] Preview fallback timeout — revealing iframe');
|
|
2398
2407
|
previewLoading = false;
|
|
2399
2408
|
previewError = null;
|
|
@@ -3172,6 +3181,11 @@
|
|
|
3172
3181
|
}
|
|
3173
3182
|
|
|
3174
3183
|
let lastWsPreviewSignature: string | null = null;
|
|
3184
|
+
let pendingWorkerPreviewFromWs: {
|
|
3185
|
+
workerUrl: string;
|
|
3186
|
+
buildTimeMs?: number | null;
|
|
3187
|
+
eventTimestamp?: string | null;
|
|
3188
|
+
} | null = null;
|
|
3175
3189
|
|
|
3176
3190
|
async function applyWorkerPreviewFromEvent(
|
|
3177
3191
|
workerUrl: string,
|
|
@@ -3188,6 +3202,16 @@
|
|
|
3188
3202
|
if (signature === lastWsPreviewSignature) return;
|
|
3189
3203
|
lastWsPreviewSignature = signature;
|
|
3190
3204
|
|
|
3205
|
+
if (isGeneratingCode) {
|
|
3206
|
+
pendingWorkerPreviewFromWs = { workerUrl, buildTimeMs, eventTimestamp };
|
|
3207
|
+
dynamicWorkerPreviewUrl = fetchUrl;
|
|
3208
|
+
directPreviewUrl = toWorkerPreviewDisplayUrl(fetchUrl);
|
|
3209
|
+
console.log('[WidgetDetails] Defer Worker iframe remount until codegen finishes');
|
|
3210
|
+
return;
|
|
3211
|
+
}
|
|
3212
|
+
|
|
3213
|
+
pendingWorkerPreviewFromWs = null;
|
|
3214
|
+
|
|
3191
3215
|
// console.log('[WidgetDetails] Worker preview ready — force reloading iframe', {
|
|
3192
3216
|
// widgetId: selectedWidgetId,
|
|
3193
3217
|
// previewUrl: fetchUrl,
|
|
@@ -3215,6 +3239,13 @@
|
|
|
3215
3239
|
);
|
|
3216
3240
|
}
|
|
3217
3241
|
|
|
3242
|
+
$: if (!isGeneratingCode && pendingWorkerPreviewFromWs) {
|
|
3243
|
+
const pending = pendingWorkerPreviewFromWs;
|
|
3244
|
+
pendingWorkerPreviewFromWs = null;
|
|
3245
|
+
lastWsPreviewSignature = null;
|
|
3246
|
+
void applyWorkerPreviewFromEvent(pending.workerUrl, pending.buildTimeMs, pending.eventTimestamp);
|
|
3247
|
+
}
|
|
3248
|
+
|
|
3218
3249
|
// Process queued actions
|
|
3219
3250
|
$: if (queuedActions.length > 0) {
|
|
3220
3251
|
void processQueuedActions();
|
|
@@ -3910,9 +3941,7 @@
|
|
|
3910
3941
|
|
|
3911
3942
|
<style>
|
|
3912
3943
|
.widget-details-drag-bar button:focus,
|
|
3913
|
-
.widget-details-drag-bar button:focus-visible
|
|
3914
|
-
.widget-details-drag-bar a:focus,
|
|
3915
|
-
.widget-details-drag-bar a:focus-visible {
|
|
3944
|
+
.widget-details-drag-bar button:focus-visible {
|
|
3916
3945
|
outline: none;
|
|
3917
3946
|
box-shadow: none;
|
|
3918
3947
|
}
|
|
@@ -108,6 +108,7 @@ declare const WidgetDetails: $$__sveltets_2_IsomorphicComponent<{
|
|
|
108
108
|
showPublishedArtifact?: (url: string | null) => void;
|
|
109
109
|
finishCompositionHydration?: () => void;
|
|
110
110
|
markCodegenPreviewFresh?: () => void;
|
|
111
|
+
clearCodegenPreviewFresh?: () => void;
|
|
111
112
|
markCodegenStarted?: () => void;
|
|
112
113
|
hasLiveWorkerPreview?: () => boolean;
|
|
113
114
|
getLatestRuntimeLogs?: (maxLines?: number) => string[];
|
|
@@ -208,6 +209,7 @@ declare const WidgetDetails: $$__sveltets_2_IsomorphicComponent<{
|
|
|
208
209
|
showPublishedArtifact: (url: string | null) => void;
|
|
209
210
|
finishCompositionHydration: () => void;
|
|
210
211
|
markCodegenPreviewFresh: () => void;
|
|
212
|
+
clearCodegenPreviewFresh: () => void;
|
|
211
213
|
markCodegenStarted: () => void;
|
|
212
214
|
hasLiveWorkerPreview: () => boolean;
|
|
213
215
|
getLatestRuntimeLogs: (maxLines?: number) => string[];
|
|
@@ -130,17 +130,36 @@
|
|
|
130
130
|
}: Props = $props();
|
|
131
131
|
|
|
132
132
|
const DRAG_BAR_H = 30;
|
|
133
|
-
const BAR_SHAPE_GAP = 4;
|
|
134
133
|
const SHOW_TITLE_MIN_WIDTH = 160;
|
|
135
134
|
/** Minimum chrome width when expanded so version + Live/Publish controls fit. */
|
|
136
135
|
const MIN_EXPANDED_CHROME_WIDTH = 220;
|
|
136
|
+
/** Keeps Fabric handles clickable. The strip is inside the shape and outside the iframe. */
|
|
137
137
|
const IFRAME_INSET_PX = 2;
|
|
138
|
+
/**
|
|
139
|
+
* Negative so the bar covers that inset. Otherwise the strip is a dead hit-zone:
|
|
140
|
+
* the pointer falls through to the canvas and selects the widget underneath.
|
|
141
|
+
*/
|
|
142
|
+
const BAR_SHAPE_GAP = -IFRAME_INSET_PX;
|
|
138
143
|
/** ~5 version rows before the menu scrolls (matches chat history panel). */
|
|
139
144
|
/** Row height ≈2.1rem (py-1.5 + text) + 0.6rem vertical padding for the menu box. */
|
|
140
145
|
const VERSION_MENU_ROW_REM = 2.1;
|
|
141
146
|
const VERSION_MENU_PAD_REM = 0.6;
|
|
142
147
|
/** Menu shows at most 5 rows before scrolling. */
|
|
143
148
|
const VERSION_MENU_MAX_ROWS = 5;
|
|
149
|
+
/** Fabric corner handles sit on the shape corners and must stay clickable through the bar. */
|
|
150
|
+
const HANDLE_NOTCH_PX = 14;
|
|
151
|
+
|
|
152
|
+
function headerNotchClip(barWidth: number, shapeWidth: number): string {
|
|
153
|
+
const notch = HANDLE_NOTCH_PX;
|
|
154
|
+
const height = DRAG_BAR_H;
|
|
155
|
+
const y = height - notch;
|
|
156
|
+
const shapeRight = Math.min(barWidth, Math.max(shapeWidth, notch * 2));
|
|
157
|
+
if (shapeRight >= barWidth - 1) {
|
|
158
|
+
return `polygon(0px 0px, ${barWidth}px 0px, ${barWidth}px ${y}px, ${barWidth - notch}px ${y}px, ${barWidth - notch}px ${height}px, ${notch}px ${height}px, ${notch}px ${y}px, 0px ${y}px)`;
|
|
159
|
+
}
|
|
160
|
+
const rightInner = Math.max(notch + 1, shapeRight - notch);
|
|
161
|
+
return `polygon(0px 0px, ${barWidth}px 0px, ${barWidth}px ${height}px, ${shapeRight}px ${height}px, ${shapeRight}px ${y}px, ${rightInner}px ${y}px, ${rightInner}px ${height}px, ${notch}px ${height}px, ${notch}px ${y}px, 0px ${y}px)`;
|
|
162
|
+
}
|
|
144
163
|
|
|
145
164
|
type ShapeRect = {
|
|
146
165
|
widgetId: string;
|
|
@@ -200,6 +219,7 @@
|
|
|
200
219
|
const coalescedReloadTimers = new Map<string, ReturnType<typeof setTimeout>>();
|
|
201
220
|
|
|
202
221
|
export function requestReload(widgetId: string): void {
|
|
222
|
+
if (isGenerating) return;
|
|
203
223
|
const now = Date.now();
|
|
204
224
|
const sinceLast = now - (lastReloadAtByWidgetId[widgetId] || 0);
|
|
205
225
|
if (sinceLast < RELOAD_COALESCE_MS) {
|
|
@@ -222,12 +242,38 @@
|
|
|
222
242
|
lastReloadAtByWidgetId[widgetId] = now;
|
|
223
243
|
cancelPendingRefresh(widgetId);
|
|
224
244
|
const token = Math.max(now, (reloadTokens[widgetId] || 0) + 1);
|
|
225
|
-
pendingRefreshTokens
|
|
245
|
+
// Do not set pendingRefreshTokens — that path runs html2canvas (explicit Refresh only).
|
|
226
246
|
reloadTokens = { ...reloadTokens, [widgetId]: token };
|
|
227
247
|
iframeLoadedByWidgetId = { ...iframeLoadedByWidgetId, [widgetId]: false };
|
|
228
248
|
sync();
|
|
229
249
|
}
|
|
230
250
|
|
|
251
|
+
/** Drop coalesced overlay reloads and pending captures when a generation starts. */
|
|
252
|
+
export function cancelReloads(widgetId?: string): void {
|
|
253
|
+
if (widgetId) {
|
|
254
|
+
const coalesced = coalescedReloadTimers.get(widgetId);
|
|
255
|
+
if (coalesced) {
|
|
256
|
+
clearTimeout(coalesced);
|
|
257
|
+
coalescedReloadTimers.delete(widgetId);
|
|
258
|
+
}
|
|
259
|
+
cancelPendingRefresh(widgetId);
|
|
260
|
+
return;
|
|
261
|
+
}
|
|
262
|
+
for (const timer of coalescedReloadTimers.values()) clearTimeout(timer);
|
|
263
|
+
coalescedReloadTimers.clear();
|
|
264
|
+
for (const id of [...pendingRefreshTokens.keys()]) cancelPendingRefresh(id);
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
export function setSelectedVersion(widgetId: string, versionId: string | null): void {
|
|
268
|
+
if (!widgetId) return;
|
|
269
|
+
if (!versionId) {
|
|
270
|
+
const { [widgetId]: _dropped, ...rest } = localSelectedVersionByWidgetId;
|
|
271
|
+
localSelectedVersionByWidgetId = rest;
|
|
272
|
+
return;
|
|
273
|
+
}
|
|
274
|
+
localSelectedVersionByWidgetId = { ...localSelectedVersionByWidgetId, [widgetId]: versionId };
|
|
275
|
+
}
|
|
276
|
+
|
|
231
277
|
/** Which shape chrome version menu is open. */
|
|
232
278
|
let openVersionMenuWidgetId = $state<string | null>(null);
|
|
233
279
|
/** Local manual version selection override per widget (ensures trigger updates immediately). */
|
|
@@ -291,17 +337,32 @@
|
|
|
291
337
|
draggingWidgetId = widgetId;
|
|
292
338
|
let lastX = event.clientX;
|
|
293
339
|
let lastY = event.clientY;
|
|
340
|
+
let pendingDx = 0;
|
|
341
|
+
let pendingDy = 0;
|
|
342
|
+
let dragFrame = 0;
|
|
343
|
+
|
|
344
|
+
function flushDrag() {
|
|
345
|
+
dragFrame = 0;
|
|
346
|
+
const dx = pendingDx;
|
|
347
|
+
const dy = pendingDy;
|
|
348
|
+
pendingDx = 0;
|
|
349
|
+
pendingDy = 0;
|
|
350
|
+
if (dx === 0 && dy === 0) return;
|
|
351
|
+
onTranslate?.(widgetId, dx, dy);
|
|
352
|
+
}
|
|
294
353
|
|
|
295
354
|
function onMove(e: PointerEvent) {
|
|
296
|
-
|
|
297
|
-
|
|
355
|
+
pendingDx += e.clientX - lastX;
|
|
356
|
+
pendingDy += e.clientY - lastY;
|
|
298
357
|
lastX = e.clientX;
|
|
299
358
|
lastY = e.clientY;
|
|
300
|
-
if (
|
|
301
|
-
|
|
359
|
+
if (dragFrame) return;
|
|
360
|
+
dragFrame = requestAnimationFrame(flushDrag);
|
|
302
361
|
}
|
|
303
362
|
|
|
304
363
|
function endDrag() {
|
|
364
|
+
if (dragFrame) cancelAnimationFrame(dragFrame);
|
|
365
|
+
flushDrag();
|
|
305
366
|
draggingWidgetId = null;
|
|
306
367
|
document.removeEventListener('pointermove', onMove);
|
|
307
368
|
document.removeEventListener('pointerup', endDrag);
|
|
@@ -415,11 +476,24 @@
|
|
|
415
476
|
return (option.shortLabel || option.label || fallback).trim();
|
|
416
477
|
}
|
|
417
478
|
|
|
479
|
+
let versionMenuBox = $state<{ widgetId: string; top: number; right: number } | null>(null);
|
|
480
|
+
|
|
418
481
|
function toggleVersionMenu(event: MouseEvent, widgetId: string) {
|
|
419
482
|
event.preventDefault();
|
|
420
483
|
event.stopPropagation();
|
|
421
484
|
onSelectShape?.(widgetId);
|
|
422
|
-
|
|
485
|
+
if (openVersionMenuWidgetId === widgetId) {
|
|
486
|
+
openVersionMenuWidgetId = null;
|
|
487
|
+
versionMenuBox = null;
|
|
488
|
+
return;
|
|
489
|
+
}
|
|
490
|
+
const rect = (event.currentTarget as HTMLElement).getBoundingClientRect();
|
|
491
|
+
versionMenuBox = {
|
|
492
|
+
widgetId,
|
|
493
|
+
top: rect.bottom + 2,
|
|
494
|
+
right: window.innerWidth - rect.right,
|
|
495
|
+
};
|
|
496
|
+
openVersionMenuWidgetId = widgetId;
|
|
423
497
|
}
|
|
424
498
|
|
|
425
499
|
function selectVersion(event: MouseEvent, widgetId: string, versionId: string) {
|
|
@@ -428,7 +502,6 @@
|
|
|
428
502
|
openVersionMenuWidgetId = null;
|
|
429
503
|
if (!versionId) return;
|
|
430
504
|
onVersionChange?.(widgetId, versionId);
|
|
431
|
-
// The parent logs the selected version and resolved commit once the build completes.
|
|
432
505
|
}
|
|
433
506
|
|
|
434
507
|
function handleSetLiveClick(event: MouseEvent, widgetId: string, versionId: string) {
|
|
@@ -586,6 +659,13 @@
|
|
|
586
659
|
{@const bodyWidth = overlay.localWidth ?? overlay.width}
|
|
587
660
|
{@const bodyHeight = overlay.localHeight ?? overlay.height}
|
|
588
661
|
{@const bodyMatrix = overlay.matrix ?? [1, 0, 0, 1, overlay.left, overlay.top]}
|
|
662
|
+
{@const frameScaleX = Math.hypot(bodyMatrix[0], bodyMatrix[1]) || 1}
|
|
663
|
+
{@const frameScaleY = Math.hypot(bodyMatrix[2], bodyMatrix[3]) || 1}
|
|
664
|
+
{@const frameRotation = Math.atan2(bodyMatrix[1], bodyMatrix[0])}
|
|
665
|
+
{@const frameCos = Math.cos(frameRotation)}
|
|
666
|
+
{@const frameSin = Math.sin(frameRotation)}
|
|
667
|
+
{@const frameWidth = Math.max(1, bodyWidth * frameScaleX)}
|
|
668
|
+
{@const frameHeight = Math.max(1, bodyHeight * frameScaleY)}
|
|
589
669
|
{@const showExpandedChrome = isSelected || overlay.width >= SHOW_TITLE_MIN_WIDTH}
|
|
590
670
|
{@const chromeWidth = showExpandedChrome
|
|
591
671
|
? Math.max(overlay.width, MIN_EXPANDED_CHROME_WIDTH)
|
|
@@ -621,10 +701,16 @@
|
|
|
621
701
|
<div
|
|
622
702
|
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
|
|
623
703
|
{isPrimary ? 'bg-red-100' : 'bg-white'} {isSelected ? 'border-blue-400' : 'border-gray-300'}"
|
|
624
|
-
style="height: {DRAG_BAR_H}px; cursor: grab; min-width: {showExpandedChrome ? MIN_EXPANDED_CHROME_WIDTH : 0}px;"
|
|
704
|
+
style="height: {DRAG_BAR_H}px; cursor: grab; min-width: {showExpandedChrome ? MIN_EXPANDED_CHROME_WIDTH : 0}px; clip-path: {headerNotchClip(chromeWidth, overlay.width)};"
|
|
625
705
|
title="Drag widget"
|
|
626
706
|
onpointerdown={(e) => startShapeDrag(e, overlay.widgetId)}
|
|
627
707
|
>
|
|
708
|
+
{#if isPrimary}
|
|
709
|
+
<div
|
|
710
|
+
class="widget-shape-primary-stroke pointer-events-none absolute z-20 bg-[#DC2626]"
|
|
711
|
+
style="left: 0; width: {overlay.width}px; top: {DRAG_BAR_H - IFRAME_INSET_PX - 1}px; height: 2px; clip-path: inset(0 {HANDLE_NOTCH_PX}px 0 {HANDLE_NOTCH_PX}px);"
|
|
712
|
+
></div>
|
|
713
|
+
{/if}
|
|
628
714
|
<span class="widget-shape-drag-grip shrink-0 text-xs leading-none text-gray-400" aria-hidden="true">⠿</span>
|
|
629
715
|
|
|
630
716
|
{#if showExpandedChrome}
|
|
@@ -733,42 +819,6 @@
|
|
|
733
819
|
</HoverTooltip>
|
|
734
820
|
{/if}
|
|
735
821
|
{/if}
|
|
736
|
-
{#if versionMenuOpen}
|
|
737
|
-
{@const hasScroll = versionOptions.length > 5}
|
|
738
|
-
<div
|
|
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;"
|
|
741
|
-
role="listbox"
|
|
742
|
-
>
|
|
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>
|
|
770
|
-
</div>
|
|
771
|
-
{/if}
|
|
772
822
|
</div>
|
|
773
823
|
{:else if versionTriggerText && versionTriggerText !== '—'}
|
|
774
824
|
<span
|
|
@@ -829,7 +879,7 @@
|
|
|
829
879
|
<div
|
|
830
880
|
class="widget-shape-live-preview-frame pointer-events-none fixed left-0 top-0 origin-top-left overflow-visible"
|
|
831
881
|
data-widget-id={overlay.widgetId}
|
|
832
|
-
style="height: {
|
|
882
|
+
style="height: {frameHeight}px; width: {frameWidth}px; transform: matrix({frameCos}, {frameSin}, {-frameSin}, {frameCos}, {bodyMatrix[4]}, {bodyMatrix[5]}); z-index: {isSelected ? 60 : 1};"
|
|
833
883
|
>
|
|
834
884
|
<!-- A new reload gets its own window so old load/screenshot replies cannot win. -->
|
|
835
885
|
{#if overlay.src}
|
|
@@ -880,6 +930,43 @@
|
|
|
880
930
|
></div>
|
|
881
931
|
{/if}
|
|
882
932
|
</div>
|
|
933
|
+
{#if versionMenuOpen && versionMenuBox?.widgetId === overlay.widgetId}
|
|
934
|
+
{@const hasScroll = versionOptions.length > 5}
|
|
935
|
+
<div
|
|
936
|
+
class="widget-shape-version-menu pointer-events-auto fixed z-[400] w-max rounded-md border border-emerald-200 bg-white p-1 shadow-lg"
|
|
937
|
+
data-version-menu-for={overlay.widgetId}
|
|
938
|
+
style="top: {versionMenuBox.top}px; right: {versionMenuBox.right}px;"
|
|
939
|
+
role="listbox"
|
|
940
|
+
>
|
|
941
|
+
<div
|
|
942
|
+
class="widget-shape-version-scroll flex flex-col gap-1 overflow-x-hidden {hasScroll ? 'overflow-y-auto' : 'overflow-y-visible'}"
|
|
943
|
+
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;"
|
|
944
|
+
>
|
|
945
|
+
{#each versionOptions as option (option.id)}
|
|
946
|
+
{@const optionVersion = Number(option.id.slice(2))}
|
|
947
|
+
{@const optionBusy = (void titleRevision, !!resolveIsPublishing?.(overlay.widgetId))}
|
|
948
|
+
{@const optionPublishDisabled = !resolveCanPublishDraft?.(overlay.widgetId, optionVersion)
|
|
949
|
+
|| !!resolvePublishDisabled?.(overlay.widgetId, optionVersion)
|
|
950
|
+
|| (selectedVersion?.id === option.id && selectedPreviewBlocked)}
|
|
951
|
+
{@const optionPublishReason = (void titleRevision, resolvePublishDisabledReason?.(overlay.widgetId, optionVersion))}
|
|
952
|
+
<VersionDropdownRow
|
|
953
|
+
versionLabel={option.shortLabel || option.label}
|
|
954
|
+
dateLabel={option.dateLabel}
|
|
955
|
+
isLive={option.isLive === true}
|
|
956
|
+
isDraft={isDraftVersion(option)}
|
|
957
|
+
selected={selectedVersion?.id === option.id}
|
|
958
|
+
busy={optionBusy}
|
|
959
|
+
publishDisabled={optionPublishDisabled}
|
|
960
|
+
publishDisabledReason={optionPublishReason || 'Build this draft version and make it the Live version'}
|
|
961
|
+
setLiveDisabled={settingLiveVersion === optionVersion}
|
|
962
|
+
onselect={() => selectVersionFromMenu(overlay.widgetId, option.id)}
|
|
963
|
+
onpublish={() => handlePublishDraftClickFromMenu(overlay.widgetId, option.id)}
|
|
964
|
+
onsetlive={() => handleSetLiveClickFromMenu(overlay.widgetId, option.id)}
|
|
965
|
+
/>
|
|
966
|
+
{/each}
|
|
967
|
+
</div>
|
|
968
|
+
</div>
|
|
969
|
+
{/if}
|
|
883
970
|
</div>
|
|
884
971
|
{/each}
|
|
885
972
|
</div>
|
|
@@ -95,6 +95,8 @@ interface Props {
|
|
|
95
95
|
}
|
|
96
96
|
declare const WidgetShapeLivePreviews: import("svelte").Component<Props, {
|
|
97
97
|
requestReload: (widgetId: string) => void;
|
|
98
|
+
cancelReloads: (widgetId?: string) => void;
|
|
99
|
+
setSelectedVersion: (widgetId: string, versionId: string | null) => void;
|
|
98
100
|
sync: () => void;
|
|
99
101
|
}, "">;
|
|
100
102
|
type WidgetShapeLivePreviews = ReturnType<typeof WidgetShapeLivePreviews>;
|
|
@@ -24,7 +24,7 @@ export interface CodeGenerationStatus {
|
|
|
24
24
|
error: string | null;
|
|
25
25
|
timestamp: string | null;
|
|
26
26
|
}
|
|
27
|
-
/** Agent harness repair lines pushed during codegen (
|
|
27
|
+
/** Agent harness repair lines pushed during codegen (codegen:output). */
|
|
28
28
|
export interface AgentHarnessOutput {
|
|
29
29
|
output: string;
|
|
30
30
|
timestamp: string;
|
|
@@ -222,7 +222,7 @@ function setupSocketListeners(sock) {
|
|
|
222
222
|
// =========================================================================
|
|
223
223
|
// Dynamic Worker Preview
|
|
224
224
|
// =========================================================================
|
|
225
|
-
sock.on('
|
|
225
|
+
sock.on('codegen:output', (data) => {
|
|
226
226
|
const output = typeof data?.output === 'string' ? data.output.trim() : '';
|
|
227
227
|
if (!output)
|
|
228
228
|
return;
|
|
@@ -12,6 +12,11 @@ export declare function readAllWidgetDrafts(): Promise<Record<string, WidgetDraf
|
|
|
12
12
|
export declare function copyWidgetDraft(sourceWidgetId: string, targetWidgetId: string): Promise<void>;
|
|
13
13
|
export declare function saveCanvasState(canvasId: string | null, json: string): void;
|
|
14
14
|
export declare function readCanvasState(canvasId: string | null): Promise<string | null>;
|
|
15
|
+
/** False before an owner is known — nothing can be persisted yet. */
|
|
16
|
+
export declare function canPersistShapeThumbnails(): boolean;
|
|
17
|
+
export declare function saveShapeThumbnail(ref: string, dataUrl: string): void;
|
|
18
|
+
export declare function readShapeThumbnail(ref: string): Promise<string | null>;
|
|
19
|
+
export declare function deleteShapeThumbnail(ref: string): Promise<void>;
|
|
15
20
|
export declare function saveActiveGeneration(widgetId: string, operationId: string): void;
|
|
16
21
|
export declare function readActiveGeneration(widgetId: string): Promise<string | null>;
|
|
17
22
|
export declare function clearActiveGeneration(widgetId: string): Promise<void>;
|
|
@@ -11,6 +11,7 @@ import { deleteEntity, getCacheOwner, readAllEntitiesForKind, readEntity, writeE
|
|
|
11
11
|
const DRAFT_KIND = 'creatorDrafts';
|
|
12
12
|
const CANVAS_KIND = 'creatorCanvasState';
|
|
13
13
|
const GENERATION_KIND = 'creatorActiveGeneration';
|
|
14
|
+
const SHAPE_THUMBNAIL_KIND = 'creatorShapeThumbnails';
|
|
14
15
|
function ownerIdOrNull() {
|
|
15
16
|
return getCacheOwner();
|
|
16
17
|
}
|
|
@@ -56,7 +57,9 @@ export function saveCanvasState(canvasId, json) {
|
|
|
56
57
|
const ownerId = ownerIdOrNull();
|
|
57
58
|
if (!ownerId)
|
|
58
59
|
return;
|
|
59
|
-
|
|
60
|
+
// Wrapped: readEntity discards non-object payloads, so a bare string never
|
|
61
|
+
// came back and this durable fallback silently read as empty.
|
|
62
|
+
void writeEntity(ownerId, CANVAS_KIND, canvasId ?? 'default', { json }).catch((error) => {
|
|
60
63
|
console.warn('[creatorDraftStorage] Failed to persist canvas state:', error);
|
|
61
64
|
});
|
|
62
65
|
}
|
|
@@ -64,20 +67,54 @@ export async function readCanvasState(canvasId) {
|
|
|
64
67
|
const ownerId = ownerIdOrNull();
|
|
65
68
|
if (!ownerId)
|
|
66
69
|
return null;
|
|
67
|
-
|
|
70
|
+
const record = await readEntity(ownerId, CANVAS_KIND, canvasId ?? 'default');
|
|
71
|
+
return record?.json ?? null;
|
|
72
|
+
}
|
|
73
|
+
// ─── Widget shape thumbnails ─────────────────────────────────────
|
|
74
|
+
//
|
|
75
|
+
// Widget screenshots used to be serialized inline in the canvas JSON, which
|
|
76
|
+
// made every autosave stringify megabytes of base64 on the main thread. They
|
|
77
|
+
// live here instead, keyed by the reference the canvas puts on the shape.
|
|
78
|
+
/** False before an owner is known — nothing can be persisted yet. */
|
|
79
|
+
export function canPersistShapeThumbnails() {
|
|
80
|
+
return !!ownerIdOrNull();
|
|
81
|
+
}
|
|
82
|
+
export function saveShapeThumbnail(ref, dataUrl) {
|
|
83
|
+
const ownerId = ownerIdOrNull();
|
|
84
|
+
if (!ownerId || !ref || !dataUrl)
|
|
85
|
+
return;
|
|
86
|
+
// Wrapped in an object: readEntity only hands back object payloads, so a bare
|
|
87
|
+
// string round-trips as null.
|
|
88
|
+
void writeEntity(ownerId, SHAPE_THUMBNAIL_KIND, ref, { dataUrl }).catch((error) => {
|
|
89
|
+
console.warn('[creatorDraftStorage] Failed to persist shape thumbnail:', ref, error);
|
|
90
|
+
});
|
|
91
|
+
}
|
|
92
|
+
export async function readShapeThumbnail(ref) {
|
|
93
|
+
const ownerId = ownerIdOrNull();
|
|
94
|
+
if (!ownerId || !ref)
|
|
95
|
+
return null;
|
|
96
|
+
const record = await readEntity(ownerId, SHAPE_THUMBNAIL_KIND, ref);
|
|
97
|
+
return record?.dataUrl ?? null;
|
|
98
|
+
}
|
|
99
|
+
export async function deleteShapeThumbnail(ref) {
|
|
100
|
+
const ownerId = ownerIdOrNull();
|
|
101
|
+
if (!ownerId || !ref)
|
|
102
|
+
return;
|
|
103
|
+
await deleteEntity(ownerId, SHAPE_THUMBNAIL_KIND, ref);
|
|
68
104
|
}
|
|
69
105
|
// ─── Active generation id ────────────────────────────────────────
|
|
70
106
|
export function saveActiveGeneration(widgetId, operationId) {
|
|
71
107
|
const ownerId = ownerIdOrNull();
|
|
72
108
|
if (!ownerId)
|
|
73
109
|
return;
|
|
74
|
-
void writeEntity(ownerId, GENERATION_KIND, widgetId, operationId);
|
|
110
|
+
void writeEntity(ownerId, GENERATION_KIND, widgetId, { operationId });
|
|
75
111
|
}
|
|
76
112
|
export async function readActiveGeneration(widgetId) {
|
|
77
113
|
const ownerId = ownerIdOrNull();
|
|
78
114
|
if (!ownerId)
|
|
79
115
|
return null;
|
|
80
|
-
|
|
116
|
+
const record = await readEntity(ownerId, GENERATION_KIND, widgetId);
|
|
117
|
+
return record?.operationId ?? null;
|
|
81
118
|
}
|
|
82
119
|
export async function clearActiveGeneration(widgetId) {
|
|
83
120
|
const ownerId = ownerIdOrNull();
|
|
@@ -29,7 +29,7 @@ let reported = false;
|
|
|
29
29
|
let lastStatus = '';
|
|
30
30
|
let breadcrumbs = [];
|
|
31
31
|
const PING_MS = 500;
|
|
32
|
-
const STALL_MS =
|
|
32
|
+
const STALL_MS = 1500;
|
|
33
33
|
|
|
34
34
|
function describeBreadcrumbs() {
|
|
35
35
|
if (breadcrumbs.length === 0) return 'no instrumented task over 200ms — the block is somewhere not yet measured';
|
|
@@ -45,12 +45,16 @@ setInterval(() => {
|
|
|
45
45
|
if (!freezeStartedAt) freezeStartedAt = now - STALL_MS;
|
|
46
46
|
if (!reported) {
|
|
47
47
|
reported = true;
|
|
48
|
-
|
|
49
|
-
'ms. Last status: "' + (lastStatus || '—') + '"'
|
|
48
|
+
const msg = '[FreezeReport] Main thread unresponsive ' + Math.round(now - freezeStartedAt) +
|
|
49
|
+
'ms. Last status: "' + (lastStatus || '—') + '". Select the top page in the console frame picker (not the widget iframe).';
|
|
50
|
+
console.warn(msg);
|
|
51
|
+
postMessage({ type: 'freeze-report', phase: 'start', message: msg });
|
|
50
52
|
}
|
|
51
53
|
} else if (freezeStartedAt) {
|
|
52
|
-
|
|
53
|
-
'ms. Last status: "' + (lastStatus || '—') + '". Blocking work: ' + describeBreadcrumbs()
|
|
54
|
+
const msg = '[FreezeReport] Main thread recovered after ' + Math.round(now - freezeStartedAt) +
|
|
55
|
+
'ms. Last status: "' + (lastStatus || '—') + '". Blocking work: ' + describeBreadcrumbs();
|
|
56
|
+
console.warn(msg);
|
|
57
|
+
postMessage({ type: 'freeze-report', phase: 'recovery', message: msg });
|
|
54
58
|
freezeStartedAt = null;
|
|
55
59
|
reported = false;
|
|
56
60
|
}
|
|
@@ -128,11 +132,38 @@ export function installFreezeWatchdog() {
|
|
|
128
132
|
URL.revokeObjectURL(url);
|
|
129
133
|
let lastStatus = '';
|
|
130
134
|
let pendingBreadcrumbs = [];
|
|
135
|
+
let lastLongTaskSentAt = performance.now();
|
|
136
|
+
function longTasksSince(since) {
|
|
137
|
+
if (typeof performance === 'undefined' || typeof performance.getEntriesByType !== 'function')
|
|
138
|
+
return [];
|
|
139
|
+
const found = [];
|
|
140
|
+
for (const entry of performance.getEntriesByType('longtask')) {
|
|
141
|
+
const endedAt = entry.startTime + entry.duration;
|
|
142
|
+
if (endedAt < since || entry.duration < BREADCRUMB_MIN_MS)
|
|
143
|
+
continue;
|
|
144
|
+
const attribution = entry.attribution?.[0];
|
|
145
|
+
const source = attribution?.containerSrc || attribution?.containerName || attribution?.containerType;
|
|
146
|
+
found.push({ label: 'longtask', duration: entry.duration, source });
|
|
147
|
+
}
|
|
148
|
+
return found;
|
|
149
|
+
}
|
|
131
150
|
worker.onmessage = (event) => {
|
|
132
|
-
|
|
133
|
-
|
|
151
|
+
const data = event.data;
|
|
152
|
+
if (data?.type === 'ping') {
|
|
153
|
+
const breadcrumbs = pendingBreadcrumbs.concat(longTasksSince(lastLongTaskSentAt));
|
|
154
|
+
lastLongTaskSentAt = performance.now();
|
|
155
|
+
worker.postMessage({ type: 'pong', status: lastStatus, breadcrumbs });
|
|
134
156
|
pendingBreadcrumbs = [];
|
|
135
157
|
}
|
|
158
|
+
else if (data?.type === 'freeze-report' && data.message) {
|
|
159
|
+
console.warn(data.message);
|
|
160
|
+
try {
|
|
161
|
+
window.top?.console.warn(data.message);
|
|
162
|
+
}
|
|
163
|
+
catch {
|
|
164
|
+
/* cross-origin embed */
|
|
165
|
+
}
|
|
166
|
+
}
|
|
136
167
|
};
|
|
137
168
|
let stopLongTaskObserver = null;
|
|
138
169
|
const watchdog = {
|
package/package.json
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@widgetic/creator",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.68",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"peerDependencies": {
|
|
6
6
|
"@sveltejs/kit": "^2.20.0",
|
|
7
7
|
"@widgetic/api-sdk": "^1.0.10",
|
|
8
8
|
"@widgetic/cache-layer": "^0.1.2",
|
|
9
9
|
"@widgetic/canvas": "^0.5.4",
|
|
10
|
-
"@widgetic/chat": "^0.1.
|
|
10
|
+
"@widgetic/chat": "^0.1.13",
|
|
11
11
|
"@widgetic/design-system": "^0.5.8",
|
|
12
12
|
"@widgetic/editor": "^4.0.1",
|
|
13
13
|
"@widgetic/file-browser": "^0.5.17",
|
|
@@ -43,9 +43,8 @@
|
|
|
43
43
|
"dev": "npm run stop-dev && npm run dev-sv",
|
|
44
44
|
"predev-sv": "node scripts/suppress-postcss-from-warning.js",
|
|
45
45
|
"dev-sv": "VITE_APP_VERSION=$(node -p \"require('./package.json').version\") VITE_BUILD_SHA=$(git rev-parse --short HEAD 2>/dev/null || echo local) NODE_ENV=development vite dev --port 5174",
|
|
46
|
-
"lps": "volta run npm link --save @widgetic/design-system && volta run npm link --save @widgetic/api-sdk && volta run npm link --save @widgetic/canvas",
|
|
46
|
+
"lps": "volta run npm link --save @widgetic/design-system && volta run npm link --save @widgetic/api-sdk && volta run npm link --save @widgetic/canvas && volta run npm link --save @widgetic/chat",
|
|
47
47
|
"llinks": "ls -la node_modules/@widgetic",
|
|
48
|
-
"ldt": "volta run npm link --save @widgetic/dev-tools",
|
|
49
48
|
"clean-npm-cache": "npm cache clean --force",
|
|
50
49
|
"clean-svelte-cache": "rm -rf .svelte-kit && npx svelte-kit sync",
|
|
51
50
|
"clean-vite-cache": "rm -rf node_modules/.vite",
|
|
@@ -72,7 +71,7 @@
|
|
|
72
71
|
"@widgetic/api-sdk": "^1.0.10",
|
|
73
72
|
"@widgetic/cache-layer": "^0.1.2",
|
|
74
73
|
"@widgetic/canvas": "^0.5.4",
|
|
75
|
-
"@widgetic/chat": "^0.1.
|
|
74
|
+
"@widgetic/chat": "^0.1.13",
|
|
76
75
|
"@widgetic/design-system": "^0.5.8",
|
|
77
76
|
"@widgetic/dev-tools": "^0.2.0",
|
|
78
77
|
"@widgetic/editor": "^4.0.1",
|