@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.
@@ -37,6 +37,8 @@ declare const CreatorApp: $$__sveltets_2_IsomorphicComponent<{
37
37
  style?: string;
38
38
  }, {
39
39
  [evt: string]: CustomEvent<any>;
40
- }, {}, {}, string>;
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-[16px] justify-end';
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
- const COL_ACTION = 'w-[60px]';
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-max text-[10px] cursor-pointer {selected ? 'bg-emerald-50 text-emerald-800' : 'text-gray-700 hover:bg-gray-50'}"
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
- export let hasValidToken: boolean = false;
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 let isCheckingActiveGeneration: boolean = false;
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
- export let debugModeAvailable: boolean = false;
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
- return source.repositoryId || source.repo_id || source.repoId || null;
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(workerUrl: string, buildTimeMs?: number | null): Promise<void> {
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
- const signature = `${fetchUrl}:${buildTimeMs ?? 'na'}:${selectedWidgetId ?? ''}`;
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
- <WidgetPreview
3690
- bind:this={widgetPreviewComponentRef}
3691
- mode="controlled"
3692
- enableResize={true}
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
- const VERSION_MENU_MAX_H = '12.5rem';
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 token = Math.max(Date.now(), (reloadTokens[widgetId] || 0) + 1);
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-[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"
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-[3.75rem] rounded bg-emerald-100 px-1 py-0.5 text-[8px] font-semibold uppercase tracking-wide text-emerald-600 opacity-80"
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-[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"
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-[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"
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-[3.75rem] rounded bg-amber-100 px-1 py-0.5 text-[8px] font-semibold uppercase tracking-wide text-amber-700"
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 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"
670
- style="max-height: {VERSION_MENU_MAX_H}; scrollbar-width: thin; scrollbar-color: rgba(0,0,0,0.2) transparent;"
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
- {#each versionOptions as option (option.id)}
674
- {@const optionVersion = Number(option.id.slice(2))}
675
- {@const optionBusy = (void titleRevision, !!resolveIsPublishing?.(overlay.widgetId))}
676
- {@const optionPublishDisabled = !resolveCanPublishDraft?.(overlay.widgetId, optionVersion)
677
- || !!resolvePublishDisabled?.(overlay.widgetId, optionVersion)
678
- || (selectedVersion?.id === option.id && selectedPreviewBlocked)}
679
- {@const optionPublishReason = (void titleRevision, resolvePublishDisabledReason?.(overlay.widgetId, optionVersion))}
680
- <VersionDropdownRow
681
- versionLabel={option.shortLabel || option.label}
682
- dateLabel={option.dateLabel}
683
- isLive={option.isLive === true}
684
- isDraft={isDraftVersion(option)}
685
- selected={selectedVersion?.id === option.id}
686
- busy={optionBusy}
687
- publishDisabled={optionPublishDisabled}
688
- publishDisabledReason={optionPublishReason || 'Build this draft version and make it the Live version'}
689
- setLiveDisabled={settingLiveVersion === optionVersion}
690
- onselect={() => selectVersionFromMenu(overlay.widgetId, option.id)}
691
- onpublish={() => handlePublishDraftClickFromMenu(overlay.widgetId, option.id)}
692
- onsetlive={() => handleSetLiveClickFromMenu(overlay.widgetId, option.id)}
693
- />
694
- {/each}
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-menu::-webkit-scrollbar {
816
- width: 8px;
817
- height: 8px;
890
+ .widget-shape-version-scroll::-webkit-scrollbar {
891
+ width: 4px;
892
+ height: 4px;
818
893
  background: transparent;
819
894
  }
820
- .widget-shape-version-menu::-webkit-scrollbar-track {
895
+ .widget-shape-version-scroll::-webkit-scrollbar-track {
821
896
  background: transparent;
822
897
  }
823
- .widget-shape-version-menu::-webkit-scrollbar-thumb {
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;