@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.
@@ -147,18 +147,31 @@
147
147
  const name = (widget.name || '').trim() || 'Untitled Widget';
148
148
  // console.log('[CreatorApp] Notify host widget name:', widget.id, name);
149
149
  onWidgetRenamed?.({ widgetId: widget.id, name });
150
+ // Also broadcast — mount props can be stale across embed remounts.
151
+ if (typeof window !== 'undefined') {
152
+ window.dispatchEvent(
153
+ new CustomEvent('widgetic:creator-primary-renamed', {
154
+ detail: { widgetId: widget.id, name },
155
+ }),
156
+ );
157
+ }
150
158
  }
151
159
 
152
160
  /**
153
161
  * Rewrite the embedded URL so a refresh resumes the current canvas instead of
154
162
  * re-running the new-widget bootstrap (which creates a blank canvas every time).
163
+ *
164
+ * The canvas id goes into the **path** (`/canvas/<canvasId>`), matching the host route.
155
165
  * Uses history.replaceState — no navigation, no host remount.
156
166
  */
157
167
  function syncEmbeddedCanvasUrl(canvasIdToSync: string): void {
158
168
  if (!embedded || !canvasIdToSync) return;
159
169
  try {
160
170
  const url = new URL(window.location.href);
161
- url.searchParams.set('canvas_id', canvasIdToSync);
171
+ url.pathname = `/canvas/${encodeURIComponent(canvasIdToSync)}`;
172
+ url.searchParams.delete('canvas_id');
173
+ url.searchParams.delete('canvas-id');
174
+ url.searchParams.delete('widget_id');
162
175
  url.searchParams.delete('new_widget');
163
176
  window.history.replaceState(window.history.state, '', url.toString());
164
177
  // console.log('[CreatorApp] URL synced to canvas:', canvasIdToSync);
@@ -200,8 +213,11 @@
200
213
  if (selectedWidget?.id === targetId) {
201
214
  selectedWidget = { ...selectedWidget, name: trimmed };
202
215
  }
216
+ widgetNamesRevision += 1;
203
217
  panelDisplayRevision += 1;
204
218
  backfillWidgetShapeMetadata();
219
+ syncWidgetShapeLivePreviews();
220
+ emitCanvasWidgetsList('host-widget-renamed');
205
221
  // console.log('[CreatorApp] Applied host widget rename:', targetId.substring(0, 8), trimmed);
206
222
 
207
223
  if (!widgetsClient) return;
@@ -447,6 +463,8 @@
447
463
  */
448
464
  import SelectorDropdown from './components/SelectorDropdown.svelte';
449
465
  import type { DropdownItem } from './components/SelectorDropdown.svelte';
466
+ import WidgetShapeLivePreviews from './components/WidgetShapeLivePreviews.svelte';
467
+ import HoverTooltip from './components/HoverTooltip.svelte';
450
468
 
451
469
  // WIDGET DETAILS COMPONENT — lazy-loaded to reduce peak build memory
452
470
  let WidgetDetails: any;
@@ -473,6 +491,10 @@
473
491
 
474
492
  let WidgeticCanvas: any;
475
493
  let canvasRef: any;
494
+ /** HTML iframe overlays over Fabric WidgetShapes (W3). */
495
+ let widgetShapeLivePreviewsRef: { sync?: () => void } | null = null;
496
+ /** Selected WidgetShape id for chrome highlight (from Fabric selection). */
497
+ let canvasSelectedWidgetShapeId: string | null = null;
476
498
  let widgetDetailsModulePromise: Promise<any> | null = null;
477
499
 
478
500
  function loadWidgetDetailsModule(): Promise<any> {
@@ -553,6 +575,18 @@
553
575
  // Delete canvas dialog
554
576
  let showDeleteCanvasDialog = false;
555
577
  let deleteCanvasTarget: { id: string; name: string } | null = null;
578
+ /**
579
+ * Widgets the pending canvas deletion would remove, grouped by project.
580
+ * A canvas can hold widgets from more than one project (moving a widget
581
+ * rewrites `project_id` but leaves `canvas_id`), so the dialog states exactly
582
+ * what is about to be lost instead of only reassuring that it is irreversible.
583
+ */
584
+ let deleteCanvasPreview: {
585
+ widgetCount: number;
586
+ groups: Array<{ projectName: string; widgetNames: string[] }>;
587
+ } | null = null;
588
+ let deleteCanvasPreviewLoading = false;
589
+ let deleteCanvasError = '';
556
590
 
557
591
  // Widgets state
558
592
  interface WidgetSummary {
@@ -885,7 +919,10 @@
885
919
  contextType: options?.source === 'preview-screenshot' ? 'browser-ic' : 'widget-codegen',
886
920
  source: options?.source || 'chat-upload',
887
921
  widgetId: persistWidgetId && !isDraftConvertId(persistWidgetId) ? persistWidgetId : undefined,
888
- conversationId: currentChatConversationId || undefined,
922
+ conversationId:
923
+ currentChatConversationId && !isLocalOnlyConversationId(currentChatConversationId)
924
+ ? currentChatConversationId
925
+ : undefined,
889
926
  },
890
927
  { onProgress: options?.onProgress },
891
928
  );
@@ -1480,39 +1517,163 @@
1480
1517
  // console.log('[WidgetCreator] Cleared prompt draft for widget:', widgetId.substring(0, 8));
1481
1518
  }
1482
1519
 
1483
- /** Screenshot: capture this panel's widget preview iframe and return data URL */
1484
- async function handleChatScreenshot(widgetId?: string | null): Promise<string | null> {
1520
+ /** Live WidgetShape iframe only — never the hidden panel preview column. */
1521
+ function resolveLiveWidgetShapeIframe(widgetId: string | null | undefined): HTMLIFrameElement | null {
1485
1522
  const targetId = widgetId || focusedPanelWidgetId || selectedWidgetId;
1486
- const panelRef = getWidgetDetailsRef(targetId);
1487
- const iframe =
1488
- (panelRef?.getPreviewIframe?.() as HTMLIFrameElement | null) ||
1489
- (document.querySelector('iframe[title="Widget preview"]') as HTMLIFrameElement | null);
1490
-
1491
- if (!iframe?.contentWindow) {
1492
- // console.log('[Chat→Screenshot] Preview iframe not ready', {
1493
- // targetId: targetId?.substring(0, 8),
1494
- // hasValidPreview: panelRef?.hasValidPreview?.() ?? false,
1495
- // });
1496
- return null;
1523
+ if (targetId) {
1524
+ const liveById = document.querySelector(
1525
+ `iframe.widget-shape-live-preview-iframe[data-widget-id="${CSS.escape(targetId)}"]`,
1526
+ ) as HTMLIFrameElement | null;
1527
+ if (liveById?.contentWindow) return liveById;
1528
+ }
1529
+ return (
1530
+ (document.querySelector(
1531
+ 'iframe.widget-shape-live-preview-iframe',
1532
+ ) as HTMLIFrameElement | null) || null
1533
+ );
1534
+ }
1535
+
1536
+ function isWorkerPreviewUrl(url: string | null | undefined): boolean {
1537
+ if (!url) return false;
1538
+ try {
1539
+ const u = new URL(url, window.location.origin);
1540
+ return /\/preview\/[^/]+/i.test(u.pathname) || u.pathname.includes('/__widget_builder__/');
1541
+ } catch {
1542
+ return /\/preview\//i.test(url);
1497
1543
  }
1544
+ }
1498
1545
 
1499
- // console.log('[Chat→Screenshot] Requesting screenshot from preview iframe', targetId?.substring(0, 8));
1500
- return new Promise<string | null>((resolve) => {
1546
+ function requestIframeScreenshot(
1547
+ iframe: HTMLIFrameElement,
1548
+ timeoutMs = 6000,
1549
+ ): Promise<{ data: string | null; error: string | null }> {
1550
+ return new Promise((resolve) => {
1551
+ if (!iframe.contentWindow) {
1552
+ resolve({ data: null, error: 'no-content-window' });
1553
+ return;
1554
+ }
1501
1555
  const timeout = setTimeout(() => {
1502
1556
  window.removeEventListener('message', handler);
1503
- console.warn('[Chat→Screenshot] Timed out waiting for widgetic:screenshot');
1504
- resolve(null);
1505
- }, 8000);
1557
+ resolve({ data: null, error: 'timeout' });
1558
+ }, timeoutMs);
1506
1559
 
1507
1560
  function handler(event: MessageEvent) {
1508
1561
  if (event.data?.type !== 'widgetic:screenshot') return;
1562
+ // Prefer replies from this iframe when source is available
1563
+ if (event.source && event.source !== iframe.contentWindow) return;
1509
1564
  clearTimeout(timeout);
1510
1565
  window.removeEventListener('message', handler);
1511
- resolve(event.data.data || null);
1566
+ resolve({
1567
+ data: typeof event.data.data === 'string' ? event.data.data : null,
1568
+ error: typeof event.data.error === 'string' ? event.data.error : null,
1569
+ });
1512
1570
  }
1513
1571
  window.addEventListener('message', handler);
1514
- iframe.contentWindow!.postMessage({ type: 'widgetic:request-screenshot' }, '*');
1572
+ iframe.contentWindow.postMessage({ type: 'widgetic:request-screenshot' }, '*');
1573
+ });
1574
+ }
1575
+
1576
+ /**
1577
+ * Published CDN artifacts often lack the Worker screenshot script.
1578
+ * Capture via a temporary same-path Worker /preview iframe (has html2canvas handler).
1579
+ */
1580
+ async function captureViaHiddenWorkerPreview(widgetId: string): Promise<string | null> {
1581
+ const workerUrl = `${widgetBuilderPreviewBaseUrl()}/preview/${widgetId}`;
1582
+ console.log('[Chat→Screenshot] Capturing via hidden Worker preview', {
1583
+ widgetId: widgetId.substring(0, 8),
1584
+ workerUrl,
1585
+ });
1586
+
1587
+ const host = document.createElement('div');
1588
+ host.className = 'wc-hidden-screenshot-host';
1589
+ host.setAttribute(
1590
+ 'style',
1591
+ 'position:fixed;left:-10000px;top:0;width:800px;height:600px;opacity:0;pointer-events:none;z-index:-1;',
1592
+ );
1593
+ const iframe = document.createElement('iframe');
1594
+ iframe.title = 'Hidden widget screenshot';
1595
+ iframe.setAttribute('sandbox', 'allow-scripts allow-same-origin allow-forms');
1596
+ iframe.style.cssText = 'width:800px;height:600px;border:0;';
1597
+ host.appendChild(iframe);
1598
+ document.body.appendChild(host);
1599
+
1600
+ try {
1601
+ await new Promise<void>((resolve, reject) => {
1602
+ const timeout = setTimeout(() => reject(new Error('worker-preview-load-timeout')), 12000);
1603
+ iframe.onload = () => {
1604
+ clearTimeout(timeout);
1605
+ resolve();
1606
+ };
1607
+ iframe.onerror = () => {
1608
+ clearTimeout(timeout);
1609
+ reject(new Error('worker-preview-load-error'));
1610
+ };
1611
+ iframe.src = workerUrl;
1612
+ });
1613
+ // Give injected screenshot handler + images a beat to settle
1614
+ await new Promise((r) => setTimeout(r, 800));
1615
+ const result = await requestIframeScreenshot(iframe, 12000);
1616
+ if (result.data) return result.data;
1617
+ console.warn('[Chat→Screenshot] Hidden Worker capture failed:', result.error);
1618
+ return null;
1619
+ } catch (err) {
1620
+ console.warn('[Chat→Screenshot] Hidden Worker preview error:', err);
1621
+ return null;
1622
+ } finally {
1623
+ host.remove();
1624
+ }
1625
+ }
1626
+
1627
+ /** Screenshot: capture the live WidgetShape iframe (CDN or Worker), not Fabric wireframe. */
1628
+ async function handleChatScreenshot(widgetId?: string | null): Promise<string | null> {
1629
+ const targetId = widgetId || focusedPanelWidgetId || selectedWidgetId;
1630
+ if (!targetId) {
1631
+ console.warn('[Chat→Screenshot] No widget id');
1632
+ return null;
1633
+ }
1634
+
1635
+ const liveIframe = resolveLiveWidgetShapeIframe(targetId);
1636
+ const liveSrc = liveIframe
1637
+ ? liveIframe.getAttribute('src') || liveIframe.src || ''
1638
+ : '';
1639
+
1640
+ // 1) Always try the visible live iframe first (published HTML from Worker includes screenshot script)
1641
+ if (liveIframe?.contentWindow) {
1642
+ console.log('[Chat→Screenshot] Live WidgetShape iframe', {
1643
+ targetId: targetId.substring(0, 8),
1644
+ isWorker: isWorkerPreviewUrl(liveSrc),
1645
+ src: liveSrc.slice(0, 120),
1646
+ });
1647
+ const fromLive = await requestIframeScreenshot(liveIframe, 12000);
1648
+ if (fromLive.data && fromLive.data.length > 500) {
1649
+ return fromLive.data;
1650
+ }
1651
+ console.warn('[Chat→Screenshot] Live iframe capture failed:', fromLive.error);
1652
+ }
1653
+
1654
+ // 2) Hidden Worker /preview (same capture script; wait for load)
1655
+ const fromWorker = await captureViaHiddenWorkerPreview(targetId);
1656
+ if (fromWorker && fromWorker.length > 500) return fromWorker;
1657
+
1658
+ // 3) Last resort: Fabric thumbnail (often stale / wireframe) — only if nothing else works
1659
+ const shapeDataUrl =
1660
+ typeof canvasRef?.getWidgetShapeDataUrl === 'function'
1661
+ ? canvasRef.getWidgetShapeDataUrl(targetId)
1662
+ : null;
1663
+ if (shapeDataUrl && shapeDataUrl.length > 100) {
1664
+ console.warn(
1665
+ '[Chat→Screenshot] Falling back to Fabric thumbnail (may be stale wireframe)',
1666
+ targetId.substring(0, 8),
1667
+ );
1668
+ return shapeDataUrl;
1669
+ }
1670
+
1671
+ console.warn('[Chat→Screenshot] All capture paths failed', {
1672
+ targetId: targetId.substring(0, 8),
1673
+ hadLiveIframe: !!liveIframe,
1674
+ liveSrc: liveSrc.slice(0, 100),
1515
1675
  });
1676
+ return null;
1516
1677
  }
1517
1678
 
1518
1679
  function normalizeConversationRecords(json: any): any[] {
@@ -1601,6 +1762,8 @@
1601
1762
  /** Load messages for a conversation from backend */
1602
1763
  async function handleLoadMessages(conversationId: string, widgetIdForSync?: string | null): Promise<any[]> {
1603
1764
  try {
1765
+ // Client-only conversations (draft convert panels) have no DB row.
1766
+ if (isLocalOnlyConversationId(conversationId)) return [];
1604
1767
  if (!messagesClient) {
1605
1768
  console.warn('[Chat→Backend] messagesClient not initialized');
1606
1769
  return [];
@@ -1664,7 +1827,17 @@
1664
1827
  try {
1665
1828
  if (!conversationsClient) {
1666
1829
  console.warn('[Chat→Backend] conversationsClient not initialized');
1667
- return { id: crypto.randomUUID(), title };
1830
+ return null;
1831
+ }
1832
+ // Draft convert panels have no DB widget yet, so there is no table row to
1833
+ // attach a conversation to. `@widgetic/chat` requires an object with an
1834
+ // `id` here and treats anything else as a fatal init error, so hand it a
1835
+ // clearly-marked client-only id. `finalizeDraftConversion` never migrates
1836
+ // this id into the real widget's panel state, so messages sent before the
1837
+ // widget exists can never be persisted against a phantom conversation.
1838
+ if (!contextId || isDraftConvertId(contextId) || String(contextId).startsWith('draft')) {
1839
+ const localConversationId = `local_${contextId || 'draft'}_${Date.now()}`;
1840
+ return { id: localConversationId, title, isLocalOnly: true };
1668
1841
  }
1669
1842
  const response = await conversationsClient.createConversationForWidgetRaw({
1670
1843
  createConversationForWidgetRequest: {
@@ -1688,7 +1861,8 @@
1688
1861
  return conversationId ? { ...conversation, id: conversationId } : conversation;
1689
1862
  } catch (error) {
1690
1863
  console.error('[Chat→Backend] Failed to create conversation:', error);
1691
- return { id: crypto.randomUUID(), title };
1864
+ // Never invent a local UUID — saving messages against it yields 403 and stops codegen.
1865
+ return null;
1692
1866
  }
1693
1867
  }
1694
1868
 
@@ -1805,6 +1979,11 @@
1805
1979
  persistWidgetId?: string | null,
1806
1980
  ): Promise<any> {
1807
1981
  try {
1982
+ // Draft convert panels hold a client-only conversation until the widget
1983
+ // exists; there is no `conversations` row to attach messages to yet.
1984
+ if (isLocalOnlyConversationId(conversationId)) {
1985
+ return { id: `local_msg_${Date.now()}`, attachments: attachments ?? [] };
1986
+ }
1808
1987
  if (!messagesClient) {
1809
1988
  console.warn('[Chat→Backend] messagesClient not initialized');
1810
1989
  return null;
@@ -1860,7 +2039,11 @@
1860
2039
  }
1861
2040
 
1862
2041
  /** Restore conversation to a checkpoint message via backend (deletes messages + git rollback) */
1863
- async function handleRestoreCheckpoint(conversationId: string, checkpointMessageId: string): Promise<any> {
2042
+ async function handleRestoreCheckpoint(
2043
+ conversationId: string,
2044
+ checkpointMessageId: string,
2045
+ restoreWidgetId?: string | null,
2046
+ ): Promise<any> {
1864
2047
  try {
1865
2048
  if (!messagesClient) {
1866
2049
  console.warn('[Chat→Backend] messagesClient not initialized');
@@ -1877,16 +2060,22 @@
1877
2060
  const data = json?.data || json;
1878
2061
  // console.log('[Chat→Backend] Checkpoint restored:', data);
1879
2062
 
1880
- // Refresh the widget preview after restore — the backend rebuilds the widget
1881
- // from the reverted code, so we just need to reload the iframe.
1882
- // Check both camelCase (after responseTransformMiddleware) and snake_case paths.
1883
2063
  const gitRollback = data?.gitRollback || data?.git_rollback || data?.data?.gitRollback || data?.data?.git_rollback;
1884
2064
  const undoMode = data?.undoMode ?? data?.undo_mode ?? false;
1885
2065
  const targetCommit = gitRollback?.targetCommit ?? gitRollback?.target_commit;
1886
- // console.log('[Chat→Backend] Git rollback result:', gitRollback, { undoMode, targetCommit });
2066
+ const panelWidgetId =
2067
+ restoreWidgetId ||
2068
+ focusedPanelWidgetId ||
2069
+ selectedWidgetId ||
2070
+ null;
2071
+ // console.log('[Chat→Backend] Git rollback result:', gitRollback, { undoMode, targetCommit, panelWidgetId });
1887
2072
  if (gitRollback?.success) {
1888
2073
  if (targetCommit) {
1889
2074
  lastCommitId = targetCommit;
2075
+ if (panelWidgetId) {
2076
+ patchPanelSnapshot(panelWidgetId, { lastCommitId: targetCommit });
2077
+ patchCodegenUiState(panelWidgetId, {});
2078
+ }
1890
2079
  }
1891
2080
  showToast('success', undoMode ? 'Change undone' : 'Checkpoint restored', {
1892
2081
  description: undoMode
@@ -1900,20 +2089,27 @@
1900
2089
  compositionDesignSchema = null;
1901
2090
  compositionContentSchema = null;
1902
2091
  pendingCreatePreviewDefaultsWidgetId = null;
1903
- // Rollback commit may take a moment to be visible from the Worker's clone; retry
1904
- // build-from-repo with escalating delays to survive GitLab propagation lag.
1905
2092
  _isRestoringCheckpoint = true;
2093
+ const panelRef = panelWidgetId ? getWidgetDetailsRef(panelWidgetId) : widgetDetails;
1906
2094
  const scheduleBuildRetry = async () => {
1907
2095
  const delays = [2500, 5000, 8000];
1908
2096
  for (let i = 0; i < delays.length; i++) {
1909
2097
  await new Promise((r) => setTimeout(r, delays[i]));
1910
2098
  if (i === 0) suppressNextBuildFromRepoChatMessage = true;
1911
2099
  try {
1912
- await widgetDetails?.triggerBuildFromRepo({ force: true });
1913
- const err = widgetDetails?.getPreviewError?.() ?? null;
2100
+ const activeRef = panelWidgetId ? getWidgetDetailsRef(panelWidgetId) : panelRef;
2101
+ await activeRef?.triggerBuildFromRepo({ force: true });
2102
+ const err = activeRef?.getPreviewError?.() ?? null;
1914
2103
  if (!err) {
1915
- // console.log('[Restore] triggerBuildFromRepo succeeded on attempt', i + 1);
1916
2104
  _isRestoringCheckpoint = false;
2105
+ if (panelWidgetId) {
2106
+ shapePreviewSourceByWidgetId = {
2107
+ ...shapePreviewSourceByWidgetId,
2108
+ [panelWidgetId]: 'preview',
2109
+ };
2110
+ syncWidgetShapeLivePreviews();
2111
+ setTimeout(() => handleRefreshScreenshot(panelWidgetId, { quiet: true }), 2000);
2112
+ }
1917
2113
  return;
1918
2114
  }
1919
2115
  console.warn('[Restore] triggerBuildFromRepo attempt', i + 1, 'has error:', err);
@@ -1936,14 +2132,16 @@
1936
2132
  pendingCreatePreviewDefaultsWidgetId = null;
1937
2133
  setTimeout(() => {
1938
2134
  suppressNextBuildFromRepoChatMessage = true;
1939
- widgetDetails?.triggerBuildFromRepo({ force: true });
2135
+ const activeRef = panelWidgetId ? getWidgetDetailsRef(panelWidgetId) : widgetDetails;
2136
+ void activeRef?.triggerBuildFromRepo({ force: true });
1940
2137
  }, 1500);
1941
2138
  }
1942
-
1943
2139
  return data;
1944
2140
  } catch (error) {
1945
2141
  console.error('[Chat→Backend] Failed to restore checkpoint:', error);
1946
- _isRestoringCheckpoint = false;
2142
+ showToast('error', 'Failed to restore checkpoint', {
2143
+ description: error instanceof Error ? error.message : 'Unknown error',
2144
+ });
1947
2145
  return null;
1948
2146
  }
1949
2147
  }
@@ -2118,14 +2316,15 @@
2118
2316
  let editingCompositionName = '';
2119
2317
  let deletingCompositionId: string | null = null;
2120
2318
 
2121
- /** Explicit chat box height (px). Drag grows this and pushes Publish down. */
2122
- const CHAT_DEFAULT_HEIGHT = 550;
2123
- const CHAT_MIN_HEIGHT = 260;
2319
+ /** Explicit chat box height (px). null/0 = fill available; user drag sets a fixed px. */
2320
+ const CHAT_DEFAULT_HEIGHT = 400;
2321
+ const CHAT_MIN_HEIGHT = 400;
2124
2322
  const CHAT_MAX_HEIGHT = 2000;
2125
2323
 
2126
- function getChatContainerHeightForPanel(widgetId: string): number {
2324
+ function getChatContainerHeightForPanel(widgetId: string): number | null {
2127
2325
  const stored = getPanelState(widgetId).chatContainerHeight;
2128
- if (stored == null || stored <= 0) return CHAT_DEFAULT_HEIGHT;
2326
+ // null/0 → fill available space (flex-grow). Explicit resize stores a px value.
2327
+ if (stored == null || stored <= 0) return null;
2129
2328
  return Math.max(CHAT_MIN_HEIGHT, Math.min(CHAT_MAX_HEIGHT, stored));
2130
2329
  }
2131
2330
 
@@ -2133,9 +2332,12 @@
2133
2332
  const measured = document
2134
2333
  .querySelector(`[data-widget-chat-height="${widgetId}"]`)
2135
2334
  ?.getBoundingClientRect().height;
2136
- const current = measured && measured > 0 ? measured : getChatContainerHeightForPanel(widgetId);
2335
+ const current =
2336
+ measured && measured > 0
2337
+ ? measured
2338
+ : (getChatContainerHeightForPanel(widgetId) ?? CHAT_DEFAULT_HEIGHT);
2137
2339
  const next = Math.max(CHAT_MIN_HEIGHT, Math.min(CHAT_MAX_HEIGHT, current + deltaPx));
2138
- // console.log('[Chat] Resize container height:', widgetId?.substring(0, 8), current, '→', next);
2340
+ console.log('[Chat] Resize container height:', widgetId?.substring(0, 8), current, '→', next);
2139
2341
  patchPanelSnapshot(widgetId, { chatContainerHeight: next });
2140
2342
  }
2141
2343
 
@@ -2288,6 +2490,11 @@
2288
2490
  }
2289
2491
 
2290
2492
  function scheduleWidgetSchemaLoad(widgetId: string): Promise<void> {
2493
+ // Draft convert panels have no DB widget yet — `/widgets/:id/schema/*` validates
2494
+ // a UUID, so calling it with a `draft_convert_*` id only produces a 400 and a
2495
+ // console error. The real schemas are loaded once the panel migrates to the
2496
+ // widget id after the first send.
2497
+ if (isDraftConvertId(widgetId)) return Promise.resolve();
2291
2498
  const cached = widgetSchemaCache.get(widgetId);
2292
2499
  if (cached?.design || cached?.content) {
2293
2500
  applyWidgetSchemaCacheToSnapshot(widgetId);
@@ -2457,6 +2664,9 @@
2457
2664
  }
2458
2665
 
2459
2666
  async function executeWidgetSchemaLoad(widgetId: string): Promise<void> {
2667
+ // Defence in depth: callers already skip drafts, but any path that reaches
2668
+ // here with a synthetic id would hit `/widgets/:id/schema/*` and get a 400.
2669
+ if (isDraftConvertId(widgetId)) return;
2460
2670
  schemaLoadInFlight.add(widgetId);
2461
2671
 
2462
2672
  try {
@@ -2755,7 +2965,6 @@
2755
2965
  // Only reuse js_path when it already points at this widget + this version.
2756
2966
  // `/widgets/` alone used to win and keep Edit on the previous CDN version after publish.
2757
2967
  if (!pointsAtOtherWidget && jsPathVersion === version) {
2758
- console.log('[Publish] Using js_path artifact URL', resolved);
2759
2968
  return resolved;
2760
2969
  }
2761
2970
  }
@@ -3878,63 +4087,162 @@
3878
4087
  }
3879
4088
 
3880
4089
  function handleHostSaveCanvasEvent(): void {
3881
- canvasRef?.saveNow?.();
4090
+ // Explicit Save must always persist — bypass soft load-cooldown / not-ready guards.
4091
+ const json = canvasRef?.saveCanvas?.() ?? null;
4092
+ if (!json) {
4093
+ console.warn('[CreatorApp] Host Save ignored — saveCanvas returned null');
4094
+ showToast('error', 'Nothing to save', { description: 'Canvas is not ready yet.' });
4095
+ return;
4096
+ }
4097
+ void handleCanvasAutoSave(json, false, { force: true });
3882
4098
  }
3883
4099
 
3884
- async function persistPrimaryWidgetId(widgetId: string) {
3885
- if (!widgetId) return;
4100
+ async function persistPrimaryWidgetId(widgetId: string): Promise<boolean> {
4101
+ if (!widgetId) return false;
4102
+ const targetCanvasId =
4103
+ (typeof canvasId === 'string' && canvasId.trim() ? canvasId.trim() : null) ||
4104
+ (typeof sessionCanvasId === 'string' && sessionCanvasId.trim() ? sessionCanvasId.trim() : null);
4105
+ const widget = getWidgetById(widgetId);
4106
+ const widgetCanvasId =
4107
+ (widget as { canvasId?: string | null; canvas_id?: string | null } | null)?.canvasId ??
4108
+ (widget as { canvasId?: string | null; canvas_id?: string | null } | null)?.canvas_id ??
4109
+ null;
4110
+ const linkedIds = canvasRef?.getLinkedWidgetIds?.() ?? linkedWidgetIds ?? new Set<string>();
4111
+ const shapeOnThisCanvas = linkedIds instanceof Set ? linkedIds.has(widgetId) : false;
4112
+ // Explicit user intent (picking a primary) is the strongest ownership signal, so a
4113
+ // shape the user drew / sees on THIS canvas always wins over a stale canvas_id.
4114
+ // Legacy canvas duplication left the same shape spread across many canvases; refusing
4115
+ // here made primary unsettable. Adopt (move) instead — never touch the shape itself.
4116
+ const shouldAdoptToThisCanvas =
4117
+ !!targetCanvasId && shapeOnThisCanvas && (!widgetCanvasId || widgetCanvasId !== targetCanvasId);
4118
+ if (targetCanvasId && widgetCanvasId && widgetCanvasId !== targetCanvasId && !shapeOnThisCanvas) {
4119
+ console.warn(
4120
+ '[CreatorApp] Refusing primary_widget_id — widget belongs to another canvas and has no shape here',
4121
+ widgetId.slice(0, 8),
4122
+ 'widgetCanvas',
4123
+ String(widgetCanvasId).slice(0, 8),
4124
+ 'targetCanvas',
4125
+ targetCanvasId.slice(0, 8),
4126
+ );
4127
+ emitCanvasWidgetsList('primary-refused');
4128
+ return false;
4129
+ }
3886
4130
  canvasPrimaryWidgetId = widgetId;
4131
+ saveSelectedWidgetId(widgetId);
3887
4132
  // console.log('[CreatorApp] Set primary widget:', widgetId);
3888
- if (canvasId && canvasesClient) {
4133
+ if (targetCanvasId && canvasesClient) {
3889
4134
  try {
3890
4135
  await canvasesClient.updateCanvas({
3891
- id: canvasId,
4136
+ id: targetCanvasId,
3892
4137
  updateCanvasRequest: { primaryWidgetId: widgetId }
3893
4138
  });
3894
4139
  } catch (err) {
3895
4140
  console.warn('[CreatorApp] Failed to save primary_widget_id:', err);
3896
4141
  }
3897
4142
  }
3898
- const widget = getWidgetById(widgetId);
3899
- if (embedded && typeof window !== 'undefined') {
4143
+ // Adopt orphans AND cross-canvas strays whose shape lives here (explicit user intent).
4144
+ if (targetCanvasId && widgetsClient && shouldAdoptToThisCanvas) {
3900
4145
  try {
3901
- const url = new URL(window.location.href);
3902
- if (url.searchParams.get('widget_id') !== widgetId) {
3903
- url.searchParams.set('widget_id', widgetId);
3904
- window.history.replaceState({}, '', url.pathname + url.search + url.hash);
3905
- }
4146
+ await widgetsClient.updateWidget({
4147
+ id: widgetId,
4148
+ updateWidgetRequest: { canvasId: targetCanvasId },
4149
+ });
4150
+ widgets = widgets.map((w: any) =>
4151
+ w.id === widgetId ? { ...w, canvasId: targetCanvasId, canvas_id: targetCanvasId } : w,
4152
+ );
4153
+ console.log(
4154
+ '[CreatorApp] Adopted widget canvas_id to host canvas (primary pick)',
4155
+ widgetId.slice(0, 8),
4156
+ 'from',
4157
+ widgetCanvasId ? String(widgetCanvasId).slice(0, 8) : 'null',
4158
+ 'to',
4159
+ targetCanvasId.slice(0, 8),
4160
+ );
3906
4161
  } catch (err) {
3907
- console.warn('[CreatorApp] Failed to sync primary widget_id in URL:', err);
4162
+ console.warn('[CreatorApp] Failed to heal widget canvas_id:', err);
3908
4163
  }
3909
4164
  }
4165
+ if (embedded) {
4166
+ syncUrlWidgetId(widgetId);
4167
+ }
3910
4168
  if (widget) notifyHostWidgetName(widget);
3911
4169
  emitCanvasWidgetsList('primary-changed');
4170
+ const anyW = widget as { thumbnailUrl?: string | null; thumbnail_url?: string | null } | null;
4171
+ const hasThumb = !!(anyW?.thumbnailUrl || anyW?.thumbnail_url);
4172
+ if (!hasThumb) {
4173
+ setTimeout(() => {
4174
+ void handleRefreshScreenshot(widgetId, { quiet: true });
4175
+ }, 1200);
4176
+ }
4177
+ return true;
3912
4178
  }
3913
4179
 
3914
4180
  function emitCanvasWidgetsList(reason: string) {
3915
4181
  if (!embedded || typeof window === 'undefined') return;
4182
+ const hostCanvasId = resolveHostCanvasId();
3916
4183
  const ids = canvasRef?.getLinkedWidgetIds?.() ?? linkedWidgetIds ?? new Set<string>();
3917
4184
  const items: { id: string; name: string }[] = [];
4185
+ const seen = new Set<string>();
4186
+ const pushItem = (id: string, name?: string | null) => {
4187
+ if (!id || seen.has(id) || isDraftConvertId(id)) return;
4188
+ seen.add(id);
4189
+ items.push({ id, name: (name || '').trim() || 'Untitled Widget' });
4190
+ };
4191
+ // Canvas widgets are those whose DB canvas_id matches this canvas (shapes may lag behind).
4192
+ if (hostCanvasId) {
4193
+ for (const widget of widgets) {
4194
+ const id = (widget as { id?: string }).id;
4195
+ if (!id) continue;
4196
+ const widgetCanvas = widgetCanvasIdOf(
4197
+ widget as { canvasId?: string; canvas_id?: string } | null,
4198
+ );
4199
+ if (widgetCanvas !== hostCanvasId) continue;
4200
+ pushItem(id, (widget as { name?: string }).name);
4201
+ }
4202
+ }
4203
+ // Linked shapes that live on THIS canvas. An orphan (null canvas_id) drawn here is
4204
+ // legitimately this canvas's widget — only foreign canvas_id ownership is excluded.
3918
4205
  for (const id of ids) {
4206
+ if (!id || isDraftConvertId(id)) continue;
3919
4207
  const widget = getWidgetById(id);
3920
- items.push({
3921
- id,
3922
- name: (widget?.name || '').trim() || 'Untitled Widget'
3923
- });
3924
- }
3925
- if (canvasPrimaryWidgetId && !items.some((item) => item.id === canvasPrimaryWidgetId)) {
3926
- const primary = getWidgetById(canvasPrimaryWidgetId);
3927
- if (primary?.id) {
4208
+ if (!widget?.id) continue;
4209
+ const widgetCanvas = widgetCanvasIdOf(
4210
+ widget as { canvasId?: string; canvas_id?: string } | null,
4211
+ );
4212
+ if (!hostCanvasId || (widgetCanvas && widgetCanvas !== hostCanvasId)) continue;
4213
+ pushItem(id, widget.name);
4214
+ }
4215
+ let primaryWidgetId = (canvasPrimaryWidgetId || '').trim();
4216
+ const primaryOnList = primaryWidgetId && items.some((item) => item.id === primaryWidgetId);
4217
+ if (primaryWidgetId && !primaryOnList) {
4218
+ const primary = getWidgetById(primaryWidgetId);
4219
+ const primaryCanvas = widgetCanvasIdOf(
4220
+ primary as { canvasId?: string; canvas_id?: string } | null,
4221
+ );
4222
+ if (hostCanvasId && primaryCanvas && primaryCanvas === hostCanvasId && primary?.id) {
3928
4223
  items.unshift({
3929
4224
  id: primary.id,
3930
4225
  name: (primary.name || '').trim() || 'Untitled Widget'
3931
4226
  });
4227
+ } else {
4228
+ console.warn(
4229
+ '[CreatorApp] Dropping stale primary from canvas widgets emit',
4230
+ reason,
4231
+ primaryWidgetId.slice(0, 8),
4232
+ 'host',
4233
+ hostCanvasId?.slice(0, 8) || 'none',
4234
+ );
4235
+ primaryWidgetId = items[0]?.id || '';
4236
+ canvasPrimaryWidgetId = primaryWidgetId || null;
4237
+ if (primaryWidgetId) syncUrlWidgetId(primaryWidgetId);
3932
4238
  }
3933
4239
  }
3934
- const primaryWidgetId = canvasPrimaryWidgetId || '';
4240
+ if (!primaryWidgetId && items[0]?.id) {
4241
+ primaryWidgetId = items[0].id;
4242
+ }
3935
4243
  // console.log('[CreatorApp] Emit canvas widgets for header', reason, items.length, primaryWidgetId);
3936
4244
  window.dispatchEvent(new CustomEvent(CREATOR_CANVAS_WIDGETS_EVENT, {
3937
- detail: { widgets: items, primaryWidgetId }
4245
+ detail: { widgets: items, primaryWidgetId: primaryWidgetId || '' }
3938
4246
  }));
3939
4247
  }
3940
4248
 
@@ -3948,10 +4256,19 @@
3948
4256
  console.warn('[CreatorApp] Host set primary ignored — missing widgetId');
3949
4257
  return;
3950
4258
  }
4259
+ const previousPrimary = canvasPrimaryWidgetId;
4260
+ // Optimistic — so host-request emits during persist cannot resurrect the previous primary.
4261
+ canvasPrimaryWidgetId = widgetId;
4262
+ saveSelectedWidgetId(widgetId);
3951
4263
  // console.log('[CreatorApp] Host set primary widget', widgetId.substring(0, 8));
3952
4264
  canvasRef?.setPrimaryWidgetShape?.(widgetId);
3953
4265
  canvasRef?.selectWidgetShape?.(widgetId);
3954
- void persistPrimaryWidgetId(widgetId);
4266
+ void persistPrimaryWidgetId(widgetId).then((ok) => {
4267
+ if (ok) return;
4268
+ canvasPrimaryWidgetId = previousPrimary;
4269
+ if (previousPrimary) saveSelectedWidgetId(previousPrimary);
4270
+ emitCanvasWidgetsList('primary-reverted');
4271
+ });
3955
4272
  }
3956
4273
 
3957
4274
  function handleSetPrimaryWidget(widgetId: string) {
@@ -3961,7 +4278,33 @@
3961
4278
  async function hydrateCanvasPrimaryWidget(linkedIds?: string[]): Promise<string | null> {
3962
4279
  const targetCanvasId = (typeof canvasId === 'string' && canvasId.trim()) ? canvasId.trim() : null;
3963
4280
  if (!targetCanvasId) return canvasPrimaryWidgetId || null;
4281
+
4282
+ // NOTE: `?widget_id=` is deliberately NOT consulted here. The canvas's DB
4283
+ // `primary_widget_id` is the single source of truth for what opens selected/red on
4284
+ // this canvas. A widget_id coming from a link is treated as a one-time intent in
4285
+ // ensureEmbeddedWidgetDeepLink(), which validates it and promotes it to primary —
4286
+ // so a stale id from a previously opened canvas can never win on load again.
4287
+
3964
4288
  let primaryId = await resolvePrimaryWidgetIdForCanvas(targetCanvasId);
4289
+ // Reject cross-canvas primary (stale DB rows mix project cards / header) unless the
4290
+ // widget's shape actually lives on this canvas — then the shape wins over stale metadata.
4291
+ if (primaryId) {
4292
+ const primaryWidget = getWidgetById(primaryId);
4293
+ const primaryCanvasId = widgetCanvasIdOf(
4294
+ primaryWidget as { canvasId?: string | null; canvas_id?: string | null } | null,
4295
+ );
4296
+ const primaryShapeHere = !!canvasRef?.getLinkedWidgetIds?.()?.has(primaryId);
4297
+ if (primaryCanvasId && primaryCanvasId !== targetCanvasId && !primaryShapeHere) {
4298
+ console.warn(
4299
+ '[CreatorApp] Ignoring cross-canvas primary_widget_id',
4300
+ primaryId.slice(0, 8),
4301
+ 'on canvas',
4302
+ targetCanvasId.slice(0, 8),
4303
+ );
4304
+ primaryId = null;
4305
+ canvasPrimaryWidgetId = null;
4306
+ }
4307
+ }
3965
4308
  if (!primaryId) {
3966
4309
  const fromLinks = (linkedIds || []).filter((id) => id && !isDraftConvertId(id) && !String(id).startsWith('draft'));
3967
4310
  const fromList = widgets
@@ -3970,7 +4313,11 @@
3970
4313
  return cid === targetCanvasId && w.id && !isDraftConvertId(w.id);
3971
4314
  })
3972
4315
  .map((w: any) => w.id);
3973
- primaryId = fromLinks[0] || fromList[0] || null;
4316
+ primaryId = fromLinks.find((id) => {
4317
+ const w = getWidgetById(id);
4318
+ const cid = (w as any)?.canvasId || (w as any)?.canvas_id;
4319
+ return !cid || cid === targetCanvasId;
4320
+ }) || fromList[0] || null;
3974
4321
  if (primaryId) {
3975
4322
  // console.log('[CreatorApp] Healing empty primary_widget_id →', primaryId.substring(0, 8));
3976
4323
  await persistPrimaryWidgetId(primaryId);
@@ -3984,6 +4331,12 @@
3984
4331
  return primaryId || canvasPrimaryWidgetId || null;
3985
4332
  }
3986
4333
 
4334
+ function applyPrimarySelectionOnCanvas(primaryId: string | null | undefined): void {
4335
+ if (!primaryId || !canvasRef || isConvertOrDraftPanelOpen()) return;
4336
+ canvasRef.setPrimaryWidgetShape?.(primaryId);
4337
+ canvasRef.selectWidgetShape?.(primaryId);
4338
+ }
4339
+
3987
4340
  async function handleHostMoveCanvasFolderEvent(event: Event) {
3988
4341
  const folderId = (event as CustomEvent).detail?.folderId;
3989
4342
  if (!folderId || typeof folderId !== 'string' || !widgetsClient) return;
@@ -4436,12 +4789,46 @@
4436
4789
  }, delayMs);
4437
4790
  }
4438
4791
 
4792
+ /**
4793
+ * Read the canvas id from the address bar.
4794
+ *
4795
+ * The canvas id lives in the **path** (`/canvas/<canvasId>`), not in a query string.
4796
+ *
4797
+ * `loadCurrentCanvas()` is also triggered by a reactive auth effect, which can run BEFORE
4798
+ * the onMount route subscription populates `routeCanvasId`. In that race, `routeCanvasId`
4799
+ * was still null, so the loader fell through to `getSavedSelectedCanvasId()` and a canvas
4800
+ * remembered from a *previous* session silently replaced the canvas in the URL. Reading the
4801
+ * address bar synchronously here makes the opened canvas authoritative in every entry path.
4802
+ */
4803
+ function readCanvasIdFromAddressBar(): string | null {
4804
+ if (typeof window === 'undefined') return null;
4805
+ try {
4806
+ const pathParts = window.location.pathname.split('/');
4807
+ const canvasIndex = pathParts.indexOf('canvas');
4808
+ const fromPath = canvasIndex >= 0 ? (pathParts[canvasIndex + 1] || '').trim() : '';
4809
+ if (fromPath) return decodeURIComponent(fromPath);
4810
+ // Legacy query string — kept only so an old bookmark still resolves to its canvas.
4811
+ const params = new URLSearchParams(window.location.search);
4812
+ const fromQuery = params.get('canvas_id') || params.get('canvas-id');
4813
+ return fromQuery && fromQuery.trim() ? fromQuery.trim() : null;
4814
+ } catch {
4815
+ return null;
4816
+ }
4817
+ }
4818
+
4439
4819
  async function loadCurrentCanvas() {
4440
4820
  // console.log('Widget Creator: loadCurrentCanvas():', routeCanvasId, isLoadingCanvasOnce);
4441
4821
  if (isLoadingCanvasOnce) return;
4442
4822
  try {
4443
4823
  isLoadingCanvasOnce = true;
4444
4824
 
4825
+ // Resolve the canvas from the address bar before any localStorage fallback — see
4826
+ // readCanvasIdFromAddressBar(). This must happen before initialCandidateCanvasId.
4827
+ if (!routeCanvasId) {
4828
+ const fromAddressBar = readCanvasIdFromAddressBar();
4829
+ if (fromAddressBar) routeCanvasId = fromAddressBar;
4830
+ }
4831
+
4445
4832
  // set widgets loading state
4446
4833
  isLoadingWidgets = true;
4447
4834
  widgetsError = null;
@@ -4474,6 +4861,16 @@
4474
4861
  // Prefer canvasId from route (path or query)
4475
4862
  if (routeCanvasId) {
4476
4863
  canvasListResolved = true;
4864
+ // The route is authoritative — keep the remembered canvas in step with it. Since the
4865
+ // host now passes `canvasId` as a prop, the `!canvasId` guard above can skip the sync
4866
+ // and leave localStorage pointing at the previously opened canvas, which then wins
4867
+ // the next time a bare `/canvas` is opened.
4868
+ if (sessionCanvasId !== routeCanvasId || canvasId !== routeCanvasId) {
4869
+ rememberCanvasId(routeCanvasId);
4870
+ }
4871
+ // New canvas session — clear primary from previous canvas before hydrate.
4872
+ canvasPrimaryWidgetId = null;
4873
+ embeddedDeepLinkAutoOpenDone = false;
4477
4874
  // console.log('WidgetCreator.loadCurrentCanvas: Loading data for route canvasId:', canvasId);
4478
4875
  }
4479
4876
  // Fallback: get latest canvas from current user's canvases
@@ -4577,10 +4974,16 @@
4577
4974
  const { loaded, widgetIds: widgetIdsOnCanvas } = await parallelCanvasLoadPromise;
4578
4975
  await maybeAutoRestoreWidgetShapes();
4579
4976
  backfillWidgetShapeMetadata();
4580
- const primaryId = await hydrateCanvasPrimaryWidget(widgetIdsOnCanvas);
4581
- if (loaded && canvasRef && primaryId && !isConvertOrDraftPanelOpen()) {
4582
- canvasRef.setPrimaryWidgetShape?.(primaryId);
4583
- canvasRef.selectWidgetShape(primaryId);
4977
+ const linkedIds =
4978
+ widgetIdsOnCanvas.length > 0
4979
+ ? widgetIdsOnCanvas
4980
+ : [...(canvasRef?.getLinkedWidgetIds?.() ?? [])];
4981
+ const primaryId = await hydrateCanvasPrimaryWidget(linkedIds);
4982
+ if (loaded) {
4983
+ applyPrimarySelectionOnCanvas(primaryId);
4984
+ requestAnimationFrame(() => {
4985
+ setTimeout(() => applyPrimarySelectionOnCanvas(primaryId), 80);
4986
+ });
4584
4987
  }
4585
4988
  return;
4586
4989
  }
@@ -4597,10 +5000,16 @@
4597
5000
  await syncWidgetsWithCanvas(widgetIdsOnCanvas);
4598
5001
  await maybeAutoRestoreWidgetShapes();
4599
5002
  backfillWidgetShapeMetadata();
4600
- const primaryId = await hydrateCanvasPrimaryWidget(widgetIdsOnCanvas);
4601
- if (loaded && canvasRef && primaryId && !isConvertOrDraftPanelOpen()) {
4602
- canvasRef.setPrimaryWidgetShape?.(primaryId);
4603
- canvasRef.selectWidgetShape(primaryId);
5003
+ const linkedIds =
5004
+ widgetIdsOnCanvas.length > 0
5005
+ ? widgetIdsOnCanvas
5006
+ : [...(canvasRef?.getLinkedWidgetIds?.() ?? [])];
5007
+ const primaryId = await hydrateCanvasPrimaryWidget(linkedIds);
5008
+ if (loaded || isCanvasStateLoaded) {
5009
+ applyPrimarySelectionOnCanvas(primaryId);
5010
+ requestAnimationFrame(() => {
5011
+ setTimeout(() => applyPrimarySelectionOnCanvas(primaryId), 80);
5012
+ });
4604
5013
  }
4605
5014
  } catch (error) {
4606
5015
  console.error('Widget Creator: Load canvases error:', error);
@@ -4670,6 +5079,11 @@
4670
5079
  sessionCanvasId = entry.id;
4671
5080
  activeCanvasName = entry.name;
4672
5081
  saveSelectedCanvasId(canvasId);
5082
+ // Address bar must track the canvas we actually switched to, and must not keep a
5083
+ // widget_id belonging to the previous canvas.
5084
+ routeCanvasId = entry.id;
5085
+ canvasPrimaryWidgetId = null;
5086
+ syncUrlCanvasId(entry.id);
4673
5087
 
4674
5088
  // Clear existing widgets and canvas visually
4675
5089
  widgets = [];
@@ -4687,6 +5101,21 @@
4687
5101
  }
4688
5102
  }
4689
5103
 
5104
+ /**
5105
+ * Clear the cross-tab "generating"/"publishing" flags when the tab closes.
5106
+ *
5107
+ * Lives at component scope (not inside `onMount`) so `onDestroy` can pass the
5108
+ * same reference to `removeEventListener`. When it was declared inside
5109
+ * `onMount`, leaving the canvas threw `handleBeforeUnload is not defined`,
5110
+ * which aborted the rest of the cleanup and blocked the navigation.
5111
+ */
5112
+ function handleBeforeUnload() {
5113
+ for (const wId of Object.keys(generatingWidgetIds)) {
5114
+ setCrossTabGenerating(false, wId);
5115
+ }
5116
+ if (isPublishing) setCrossTabPublishing(false);
5117
+ }
5118
+
4690
5119
  /** Close canvas dropdown on outside click */
4691
5120
  function handleCanvasDropdownOutsideClick(e: MouseEvent) {
4692
5121
  const target = e.target as HTMLElement;
@@ -4873,7 +5302,15 @@
4873
5302
  }
4874
5303
 
4875
5304
  try {
4876
- const newName = `Canvas ${userCanvases.length + 1}`;
5305
+ // Names must be unique per user — duplicate keys ("Canvas 1" x N) made
5306
+ // canvases indistinguishable and contributed to cross-canvas widget mix-ups.
5307
+ const usedNames = new Set(userCanvases.map((c) => (c.name || '').trim()).filter(Boolean));
5308
+ let canvasIndex = userCanvases.length + 1;
5309
+ let newName = `Canvas ${canvasIndex}`;
5310
+ while (usedNames.has(newName)) {
5311
+ canvasIndex += 1;
5312
+ newName = `Canvas ${canvasIndex}`;
5313
+ }
4877
5314
  const createdRaw = await canvasesClient.createCanvas({
4878
5315
  createCanvasRequest: { persistenceKey: newName }
4879
5316
  });
@@ -4986,16 +5423,51 @@
4986
5423
  deleteCanvasTarget = entry;
4987
5424
  showDeleteCanvasDialog = true;
4988
5425
  showCanvasDropdown = false;
5426
+ deleteCanvasError = '';
5427
+ void loadDeleteCanvasPreview(entry.id);
5428
+ }
5429
+
5430
+ /**
5431
+ * Fetch what the deletion would remove so the dialog can name it.
5432
+ * Failure is non-fatal: the dialog still explains the action, just without
5433
+ * the per-project breakdown.
5434
+ */
5435
+ async function loadDeleteCanvasPreview(canvasId: string): Promise<void> {
5436
+ deleteCanvasPreview = null;
5437
+ deleteCanvasPreviewLoading = true;
5438
+ try {
5439
+ const response = await canvasesClient?.previewDeleteCanvas?.({ id: canvasId });
5440
+ const preview = response?.data;
5441
+
5442
+ if (!preview) {
5443
+ // Older SDK build without the delete-preview method — no breakdown.
5444
+ console.warn('[WidgetCreator] Canvas delete preview unavailable');
5445
+ return;
5446
+ }
5447
+
5448
+ deleteCanvasPreview = {
5449
+ widgetCount: preview.widgetCount ?? 0,
5450
+ groups: (preview.groups ?? []).map((group) => ({
5451
+ projectName: group.projectName || 'Unknown project',
5452
+ widgetNames: group.widgetNames ?? []
5453
+ }))
5454
+ };
5455
+ } catch (err) {
5456
+ console.warn('[WidgetCreator] Could not load canvas delete preview:', err);
5457
+ } finally {
5458
+ deleteCanvasPreviewLoading = false;
5459
+ }
4989
5460
  }
4990
5461
 
4991
5462
  async function handleDeleteCanvasConfirm() {
4992
5463
  if (!deleteCanvasTarget || !canvasesClient) return;
4993
5464
  const targetId = deleteCanvasTarget.id;
4994
5465
  const targetName = deleteCanvasTarget.name;
4995
- showDeleteCanvasDialog = false;
5466
+ deleteCanvasError = '';
4996
5467
  try {
4997
5468
  await canvasesClient.deleteCanvas({ id: targetId });
4998
5469
  // console.log('[WidgetCreator] Canvas deleted:', targetId, targetName);
5470
+ showDeleteCanvasDialog = false;
4999
5471
  userCanvases = userCanvases.filter(c => c.id !== targetId);
5000
5472
  if (canvasId === targetId) {
5001
5473
  const next = userCanvases[0] || null;
@@ -5011,6 +5483,10 @@
5011
5483
  }
5012
5484
  } catch (err) {
5013
5485
  console.error('[WidgetCreator] Failed to delete canvas:', targetId, err);
5486
+ // Keep the dialog open so the user can react; a partial failure leaves
5487
+ // the canvas intact on purpose (the API refuses to orphan widgets).
5488
+ deleteCanvasError = 'Could not delete this canvas. Some of its widgets could not be removed, so nothing was deleted.';
5489
+ return;
5014
5490
  }
5015
5491
  deleteCanvasTarget = null;
5016
5492
  }
@@ -5080,15 +5556,39 @@
5080
5556
  // console.log('Widgets for canvas:', canvasId, ':', widgets);
5081
5557
  syncOrphanedWidgetShapes();
5082
5558
 
5083
- // Auto-select: embedded deep-link > localStorage > latest
5559
+ // Auto-select: embedded deep-link > localStorage > latest.
5560
+ // Both sources must belong to THIS canvas — localStorage is shared across canvases,
5561
+ // so a saved id from a previous canvas would otherwise leak in as primary.
5084
5562
  if (embedded && forceCreateNewWidget && !initialWidgetId) {
5085
5563
  // console.log('[WidgetCreator] Skipping auto-select of existing widgets (new widget from site)');
5086
5564
  } else if (widgets.length > 0) {
5087
- const deepLinkedWidget = initialWidgetId
5088
- ? getWidgetById(initialWidgetId)
5565
+ const hostCanvasId = resolveHostCanvasId();
5566
+ // A saved widget is only trustworthy when we can PROVE it belongs here: the
5567
+ // canvas must be known and the widget's canvas_id must match (null canvas_id
5568
+ // is only adopted via an explicit primary pick, not via localStorage).
5569
+ const savedWidgetRaw = getSavedSelectedWidgetId()
5570
+ ? getWidgetById(getSavedSelectedWidgetId() as string)
5089
5571
  : null;
5090
- const savedWidgetId = getSavedSelectedWidgetId();
5091
- const savedWidget = savedWidgetId ? getWidgetById(savedWidgetId) : null;
5572
+ const savedWidget =
5573
+ savedWidgetRaw &&
5574
+ hostCanvasId &&
5575
+ widgetCanvasIdOf(
5576
+ savedWidgetRaw as { canvasId?: string | null; canvas_id?: string | null },
5577
+ ) === hostCanvasId
5578
+ ? savedWidgetRaw
5579
+ : null;
5580
+ if (savedWidgetRaw && !savedWidget) {
5581
+ console.warn(
5582
+ '[CreatorApp] Ignoring saved widget from another canvas',
5583
+ savedWidgetRaw.id?.slice(0, 8),
5584
+ 'host',
5585
+ hostCanvasId?.slice(0, 8) || 'unknown',
5586
+ );
5587
+ saveSelectedWidgetId(null);
5588
+ }
5589
+ // The explicit deep-link is validated by ensureEmbeddedWidgetDeepLink(); a stale
5590
+ // URL id is never used to auto-select a widget from another canvas.
5591
+ const deepLinkedWidget = initialWidgetId ? getWidgetById(initialWidgetId) : null;
5092
5592
  const widgetToSelect = deepLinkedWidget ?? savedWidget ?? widgets[0];
5093
5593
  if (deepLinkedWidget) {
5094
5594
  // console.log('[WidgetCreator] Selecting widget from embedded deep-link:', deepLinkedWidget.name);
@@ -5355,6 +5855,17 @@
5355
5855
 
5356
5856
  if (missingIds.length === 0) return;
5357
5857
 
5858
+ // Scope merge to THIS canvas: a shape can linger on several canvases (legacy canvas
5859
+ // duplication), but a widget belongs to exactly one. Merging foreign widgets is what
5860
+ // made another canvas's widgets show up in this canvas's list/picker.
5861
+ const hostCanvasId = resolveHostCanvasId();
5862
+ const belongsHere = (widget: WidgetSummary): boolean => {
5863
+ const widgetCanvas = widgetCanvasIdOf(
5864
+ widget as { canvasId?: string | null; canvas_id?: string | null },
5865
+ );
5866
+ return !widgetCanvas || !hostCanvasId || widgetCanvas === hostCanvasId;
5867
+ };
5868
+
5358
5869
  try {
5359
5870
  // console.log(
5360
5871
  // '[WidgetCreator] Merging',
@@ -5365,8 +5876,21 @@
5365
5876
  getWidgetsBatchRequest: { widgetIds: missingIds }
5366
5877
  });
5367
5878
  const batch = response as { widgets?: WidgetSummary[]; data?: { widgets?: WidgetSummary[] } };
5368
- const fetched = batch?.widgets ?? batch?.data?.widgets ?? [];
5369
- if (!Array.isArray(fetched) || fetched.length === 0) return;
5879
+ const fetchedRaw = batch?.widgets ?? batch?.data?.widgets ?? [];
5880
+ if (!Array.isArray(fetchedRaw) || fetchedRaw.length === 0) return;
5881
+ const fetched = fetchedRaw.filter(belongsHere);
5882
+ const foreignCount = fetchedRaw.length - fetched.length;
5883
+ if (foreignCount > 0) {
5884
+ console.warn(
5885
+ '[CreatorApp] Ignoring',
5886
+ foreignCount,
5887
+ 'widget(s) linked by shape but owned by another canvas',
5888
+ );
5889
+ // Keep the stale shape but stop treating it as a widget of this canvas.
5890
+ syncOrphanedWidgetShapes();
5891
+ emitCanvasWidgetsList('foreign-shapes');
5892
+ }
5893
+ if (fetched.length === 0) return;
5370
5894
 
5371
5895
  const previousById = new Map(
5372
5896
  widgets.filter((w) => w.id).map((w) => [w.id as string, w]),
@@ -5388,9 +5912,26 @@
5388
5912
  widgets.length > 0 &&
5389
5913
  !(embedded && forceCreateNewWidget && !initialWidgetId)
5390
5914
  ) {
5915
+ const hostCanvasId = resolveHostCanvasId();
5916
+ const primaryOnCanvas =
5917
+ (canvasPrimaryWidgetId && getWidgetById(canvasPrimaryWidgetId)) ||
5918
+ widgets.find((w: any) => {
5919
+ const cid = w.canvasId || w.canvas_id;
5920
+ return hostCanvasId && cid === hostCanvasId && w.id === canvasPrimaryWidgetId;
5921
+ }) ||
5922
+ null;
5391
5923
  const savedWidgetId = getSavedSelectedWidgetId();
5392
5924
  const savedWidget = savedWidgetId ? getWidgetById(savedWidgetId) : null;
5393
- applyInitialWidgetSelectionFromList(savedWidget || widgets[0]);
5925
+ const savedOnThisCanvas =
5926
+ savedWidget &&
5927
+ (!hostCanvasId ||
5928
+ widgetCanvasIdOf(savedWidget as { canvasId?: string; canvas_id?: string }) ===
5929
+ hostCanvasId)
5930
+ ? savedWidget
5931
+ : null;
5932
+ applyInitialWidgetSelectionFromList(
5933
+ primaryOnCanvas || savedOnThisCanvas || widgets[0],
5934
+ );
5394
5935
  }
5395
5936
  } catch (err) {
5396
5937
  console.warn('[WidgetCreator] Failed to merge widgets linked on canvas:', err);
@@ -5399,8 +5940,8 @@
5399
5940
 
5400
5941
  /**
5401
5942
  * Site embeds CreatorApp once; `initialWidgetId` props stay frozen in prod.
5402
- * Always prefer the live `?widget_id=` from the address bar so deep-link
5403
- * reopen / selection match the header (avoids Audio Player header + Image Slider panel).
5943
+ * Prefer live `?widget_id=` only when it belongs to the host canvas — otherwise
5944
+ * a stale widget_id from the previous canvas overwrites the header/primary.
5404
5945
  */
5405
5946
  function resolveEmbeddedDeepLinkWidgetId(): string | null {
5406
5947
  if (typeof window !== 'undefined') {
@@ -5414,6 +5955,84 @@
5414
5955
  return initialWidgetId ?? null;
5415
5956
  }
5416
5957
 
5958
+ function resolveHostCanvasId(): string | null {
5959
+ // Priority: the canvas id in the address bar is authoritative (it is what the user
5960
+ // opened and what a refresh resumes). It lives in the route path (`/canvas/<id>`).
5961
+ // `canvasId`/`sessionCanvasId` can still hold a stale value hydrated from
5962
+ // localStorage on a previous canvas, which is what allowed another canvas's widgets
5963
+ // to leak into this canvas's list/picker.
5964
+ const fromUrl = readCanvasIdFromAddressBar();
5965
+ return (
5966
+ fromUrl ||
5967
+ (typeof canvasId === 'string' && canvasId.trim() ? canvasId.trim() : null) ||
5968
+ (typeof sessionCanvasId === 'string' && sessionCanvasId.trim() ? sessionCanvasId.trim() : null) ||
5969
+ (typeof routeCanvasId === 'string' && routeCanvasId.trim() ? routeCanvasId.trim() : null)
5970
+ );
5971
+ }
5972
+
5973
+ function widgetCanvasIdOf(widget: {
5974
+ canvasId?: string | null;
5975
+ canvas_id?: string | null;
5976
+ } | null | undefined): string | null {
5977
+ if (!widget) return null;
5978
+ const cid = widget.canvasId ?? widget.canvas_id ?? null;
5979
+ return typeof cid === 'string' && cid.trim() ? cid.trim() : null;
5980
+ }
5981
+
5982
+ async function fetchWidgetCanvasId(widgetId: string): Promise<string | null> {
5983
+ const local = getWidgetById(widgetId);
5984
+ const localCanvas = widgetCanvasIdOf(local as { canvasId?: string; canvas_id?: string } | null);
5985
+ if (localCanvas) return localCanvas;
5986
+ if (!widgetsClient) return null;
5987
+ try {
5988
+ const batch = await widgetsClient.getWidgetsBatch({
5989
+ getWidgetsBatchRequest: { widgetIds: [widgetId] },
5990
+ });
5991
+ const row =
5992
+ (batch as { widgets?: Array<{ canvasId?: string; canvas_id?: string }> })?.widgets?.[0] ??
5993
+ (batch as { data?: { widgets?: Array<{ canvasId?: string; canvas_id?: string }> } })?.data
5994
+ ?.widgets?.[0];
5995
+ return widgetCanvasIdOf(row);
5996
+ } catch (err) {
5997
+ console.warn('[CreatorApp] Failed to fetch widget canvas for deep-link', widgetId.slice(0, 8), err);
5998
+ return null;
5999
+ }
6000
+ }
6001
+
6002
+ /**
6003
+ * `canvas_id` + the canvas's DB `primary_widget_id` are the source of truth for
6004
+ * "which widget is open on this canvas". A `widget_id` in the address bar is only a
6005
+ * transient intent read once at mount.
6006
+ *
6007
+ * IMPORTANT: this deliberately does NOT touch `widget_id` any more. Rewriting the URL
6008
+ * here used to (a) persist a stale id from a previously opened canvas as if it were the
6009
+ * current primary, and (b) trigger a host remount because the site keys CreatorApp's
6010
+ * mount on the URL params. Leaving the address bar untouched is what makes the primary
6011
+ * stable; the canvas's DB `primary_widget_id` decides what opens.
6012
+ */
6013
+ function syncUrlWidgetId(_widgetId: string | null): void {
6014
+ if (typeof window === 'undefined' || !embedded) return;
6015
+ syncEmbeddedDeepLinkDismissTracking();
6016
+ }
6017
+
6018
+ /** Ensure the address bar points at the canvas we actually loaded (refresh-safe). */
6019
+ function syncUrlCanvasId(canvasIdToSync: string | null): void {
6020
+ if (typeof window === 'undefined' || !embedded) return;
6021
+ if (!canvasIdToSync || !canvasIdToSync.trim()) return;
6022
+ try {
6023
+ const url = new URL(window.location.href);
6024
+ const nextPath = `/canvas/${encodeURIComponent(canvasIdToSync)}`;
6025
+ if (url.pathname === nextPath && !url.searchParams.has('canvas_id')) return;
6026
+ url.pathname = nextPath;
6027
+ // The canvas id belongs in the path only — never duplicated into the query string.
6028
+ url.searchParams.delete('canvas_id');
6029
+ url.searchParams.delete('canvas-id');
6030
+ window.history.replaceState({}, '', url.pathname + url.search + url.hash);
6031
+ } catch (err) {
6032
+ console.warn('[CreatorApp] Failed to sync canvas_id in URL:', err);
6033
+ }
6034
+ }
6035
+
5417
6036
  /**
5418
6037
  * Canvas-only embed deep-link (My widgets / canvas without widget_id):
5419
6038
  * read canvases.primary_widget_id so Creator can open that widget panel.
@@ -5430,20 +6049,44 @@
5430
6049
  (record as { primary_widget_id?: unknown })?.primary_widget_id ??
5431
6050
  null;
5432
6051
  const primaryId = typeof raw === 'string' && raw.trim() ? raw.trim() : null;
5433
- if (primaryId) {
5434
- canvasPrimaryWidgetId = primaryId;
5435
- // console.log(
5436
- // '[CreatorApp] Resolved primary widget for canvas',
5437
- // targetCanvasId.substring(0, 8),
5438
- // '→',
5439
- // primaryId.substring(0, 8)
5440
- // );
5441
- } else {
5442
- // console.log(
5443
- // '[CreatorApp] No primary_widget_id on canvas',
5444
- // targetCanvasId.substring(0, 8)
5445
- // );
6052
+ if (!primaryId) {
6053
+ return null;
6054
+ }
6055
+ // Validate primary belongs to this canvas (DB has many cross-canvas orphans).
6056
+ let widgetCanvasId: string | null = null;
6057
+ const local = getWidgetById(primaryId);
6058
+ if (local) {
6059
+ widgetCanvasId =
6060
+ (local as { canvasId?: string | null; canvas_id?: string | null }).canvasId ??
6061
+ (local as { canvasId?: string | null; canvas_id?: string | null }).canvas_id ??
6062
+ null;
6063
+ } else if (widgetsClient) {
6064
+ try {
6065
+ const batch = await widgetsClient.getWidgetsBatch({
6066
+ getWidgetsBatchRequest: { widgetIds: [primaryId] },
6067
+ });
6068
+ const row =
6069
+ (batch as { widgets?: Array<{ canvasId?: string; canvas_id?: string }> })?.widgets?.[0] ??
6070
+ (batch as { data?: { widgets?: Array<{ canvasId?: string; canvas_id?: string }> } })?.data
6071
+ ?.widgets?.[0];
6072
+ widgetCanvasId = row?.canvasId ?? row?.canvas_id ?? null;
6073
+ } catch (batchErr) {
6074
+ console.warn('[CreatorApp] Failed to validate primary widget canvas', batchErr);
6075
+ }
5446
6076
  }
6077
+ if (widgetCanvasId && widgetCanvasId !== targetCanvasId) {
6078
+ console.warn(
6079
+ '[CreatorApp] Canvas primary_widget_id points at another canvas — treating as empty',
6080
+ primaryId.slice(0, 8),
6081
+ 'target',
6082
+ targetCanvasId.slice(0, 8),
6083
+ 'widgetCanvas',
6084
+ widgetCanvasId.slice(0, 8),
6085
+ );
6086
+ canvasPrimaryWidgetId = null;
6087
+ return null;
6088
+ }
6089
+ canvasPrimaryWidgetId = primaryId;
5447
6090
  return primaryId;
5448
6091
  } catch (err) {
5449
6092
  console.warn(
@@ -5460,11 +6103,6 @@
5460
6103
  syncEmbeddedDeepLinkDismissTracking();
5461
6104
  if (!embedded || !widgetsClient || !hasValidToken) return;
5462
6105
  if (embeddedDeepLinkPanelDismissed) {
5463
- const dismissedId = resolveEmbeddedDeepLinkWidgetId();
5464
- // console.log(
5465
- // '[CreatorApp] Skip embedded deep-link select — user dismissed panel:',
5466
- // dismissedId ? dismissedId.substring(0, 8) : '(none)'
5467
- // );
5468
6106
  return;
5469
6107
  }
5470
6108
  // Select primary widget once per Creator mount. Do not auto-open Widget Details.
@@ -5472,70 +6110,79 @@
5472
6110
  return;
5473
6111
  }
5474
6112
  if (isConvertOrDraftPanelOpen()) {
5475
- // console.log('[CreatorApp] Skip embedded deep-link select — convert/draft panel is open');
5476
6113
  embeddedDeepLinkAutoOpenDone = true;
5477
6114
  return;
5478
6115
  }
5479
6116
 
6117
+ const hostCanvasId = resolveHostCanvasId();
5480
6118
  let deepLinkWidgetId = resolveEmbeddedDeepLinkWidgetId();
5481
- // Canvas-only deep-link: no ?widget_id= / initialWidgetId — select canvases.primary_widget_id.
5482
- if (!deepLinkWidgetId && canvasesClient) {
5483
- const targetCanvasId =
5484
- (typeof canvasId === 'string' && canvasId.trim() ? canvasId.trim() : null) ||
5485
- (typeof sessionCanvasId === 'string' && sessionCanvasId.trim() ? sessionCanvasId.trim() : null) ||
5486
- (typeof routeCanvasId === 'string' && routeCanvasId.trim() ? routeCanvasId.trim() : null);
5487
- if (targetCanvasId) {
5488
- deepLinkWidgetId = await resolvePrimaryWidgetIdForCanvas(targetCanvasId);
5489
- if (embeddedDeepLinkPanelDismissed || embeddedDeepLinkAutoOpenDone) {
5490
- return;
5491
- }
5492
- // Align address-bar widget_id so header / dismiss tracking match the opened panel.
5493
- if (deepLinkWidgetId && typeof window !== 'undefined') {
5494
- try {
5495
- const url = new URL(window.location.href);
5496
- if (url.searchParams.get('widget_id') !== deepLinkWidgetId) {
5497
- url.searchParams.set('widget_id', deepLinkWidgetId);
5498
- window.history.replaceState({}, '', url.pathname + url.search + url.hash);
5499
- syncEmbeddedDeepLinkDismissTracking();
5500
- }
5501
- } catch (err) {
5502
- console.warn('[CreatorApp] Failed to sync primary widget_id in URL:', err);
5503
- }
5504
- }
6119
+
6120
+ // Stale ?widget_id= from a previous canvas must NOT win over this canvas's primary.
6121
+ if (deepLinkWidgetId && hostCanvasId) {
6122
+ const widgetCanvasId = await fetchWidgetCanvasId(deepLinkWidgetId);
6123
+ if (widgetCanvasId && widgetCanvasId !== hostCanvasId) {
6124
+ console.warn(
6125
+ '[CreatorApp] Ignoring stale URL widget_id from another canvas',
6126
+ deepLinkWidgetId.slice(0, 8),
6127
+ 'widgetCanvas',
6128
+ widgetCanvasId.slice(0, 8),
6129
+ 'host',
6130
+ hostCanvasId.slice(0, 8),
6131
+ );
6132
+ deepLinkWidgetId = null;
6133
+ syncUrlWidgetId(null);
6134
+ saveSelectedWidgetId(null);
5505
6135
  }
5506
6136
  }
5507
- if (!deepLinkWidgetId) return;
6137
+
6138
+ if (!deepLinkWidgetId && hostCanvasId && canvasesClient) {
6139
+ deepLinkWidgetId = await resolvePrimaryWidgetIdForCanvas(hostCanvasId);
6140
+ if (embeddedDeepLinkPanelDismissed || embeddedDeepLinkAutoOpenDone) {
6141
+ return;
6142
+ }
6143
+ }
6144
+
6145
+ if (!deepLinkWidgetId) {
6146
+ embeddedDeepLinkAutoOpenDone = true;
6147
+ return;
6148
+ }
6149
+
6150
+ // Keep URL + localStorage aligned to THIS canvas's primary only.
6151
+ canvasPrimaryWidgetId = deepLinkWidgetId;
6152
+ saveSelectedWidgetId(deepLinkWidgetId);
6153
+ syncUrlWidgetId(deepLinkWidgetId);
5508
6154
 
5509
6155
  const requestGen = ++embeddedDeepLinkRequestGen;
6156
+ const expectedWidgetId = deepLinkWidgetId;
5510
6157
 
5511
6158
  function deepLinkOpenStillValid(): boolean {
5512
6159
  return (
5513
6160
  requestGen === embeddedDeepLinkRequestGen &&
5514
6161
  !embeddedDeepLinkPanelDismissed &&
5515
- resolveEmbeddedDeepLinkWidgetId() === deepLinkWidgetId
6162
+ canvasPrimaryWidgetId === expectedWidgetId
5516
6163
  );
5517
6164
  }
5518
6165
 
5519
6166
  const alreadyLinked =
5520
- selectedWidgetId === deepLinkWidgetId && !!getWidgetById(deepLinkWidgetId);
6167
+ selectedWidgetId === expectedWidgetId && !!getWidgetById(expectedWidgetId);
5521
6168
  if (alreadyLinked) {
5522
6169
  if (embeddedInitialStepPending && initialStep) {
5523
6170
  widgetDetailsStep = resolveCreatorWorkflowStep(initialStep);
5524
6171
  embeddedInitialStepPending = false;
5525
6172
  }
5526
6173
  embeddedDeepLinkAutoOpenDone = true;
5527
- if (canvasRef && deepLinkWidgetId) {
5528
- canvasRef.selectWidgetShape(deepLinkWidgetId);
5529
- }
6174
+ applyPrimarySelectionOnCanvas(expectedWidgetId);
6175
+ const primaryWidget = getWidgetById(expectedWidgetId);
6176
+ if (primaryWidget) notifyHostWidgetName(primaryWidget);
6177
+ emitCanvasWidgetsList('deeplink-already-linked');
5530
6178
  return;
5531
6179
  }
5532
6180
 
5533
6181
  try {
5534
6182
  const response = await widgetsClient.getWidgetsBatch({
5535
- getWidgetsBatchRequest: { widgetIds: [deepLinkWidgetId] }
6183
+ getWidgetsBatchRequest: { widgetIds: [expectedWidgetId] }
5536
6184
  });
5537
6185
  if (!deepLinkOpenStillValid()) {
5538
- // console.log('[CreatorApp] Stale embedded deep-link fetch — user changed selection or closed panel');
5539
6186
  return;
5540
6187
  }
5541
6188
 
@@ -5545,47 +6192,52 @@
5545
6192
  };
5546
6193
  const target = batch?.widgets?.[0] ?? batch?.data?.widgets?.[0];
5547
6194
  if (!target?.id) {
5548
- console.warn('[CreatorApp] Embedded deep-link widget not found:', deepLinkWidgetId);
6195
+ console.warn('[CreatorApp] Embedded deep-link widget not found:', expectedWidgetId);
5549
6196
  embeddedDeepLinkAutoOpenDone = true;
5550
6197
  return;
5551
6198
  }
5552
6199
 
5553
- const targetCanvasId =
5554
- (target as { canvasId?: string | null; canvas_id?: string | null }).canvasId ??
5555
- (target as { canvasId?: string | null; canvas_id?: string | null }).canvas_id ??
5556
- null;
6200
+ const targetCanvasId = widgetCanvasIdOf(
6201
+ target as { canvasId?: string | null; canvas_id?: string | null },
6202
+ );
6203
+ if (hostCanvasId && targetCanvasId && targetCanvasId !== hostCanvasId) {
6204
+ console.warn(
6205
+ '[CreatorApp] Deep-link widget does not belong to host canvas — abort select',
6206
+ target.id.slice(0, 8),
6207
+ 'target',
6208
+ targetCanvasId.slice(0, 8),
6209
+ 'host',
6210
+ hostCanvasId.slice(0, 8),
6211
+ );
6212
+ embeddedDeepLinkAutoOpenDone = true;
6213
+ const healed = await hydrateCanvasPrimaryWidget(
6214
+ [...(canvasRef?.getLinkedWidgetIds?.() ?? [])],
6215
+ );
6216
+ applyPrimarySelectionOnCanvas(healed);
6217
+ return;
6218
+ }
5557
6219
 
5558
6220
  if (!getWidgetById(target.id)) {
5559
6221
  widgets = sortWidgetSummaries([...widgets, target]);
5560
6222
  }
5561
6223
  if (!deepLinkOpenStillValid()) {
5562
- // console.log('[CreatorApp] Skip embedded deep-link open — stale after widget fetch');
5563
6224
  return;
5564
6225
  }
5565
6226
 
5566
6227
  embeddedDeepLinkAutoOpenDone = true;
5567
- // Select the primary widget (and its canvas shape) — do not open Widget Details.
6228
+ canvasPrimaryWidgetId = target.id;
5568
6229
  await selectWidget(target, { skipLoadWidget: true, skipPlaceShape: true });
5569
6230
  if (!deepLinkOpenStillValid()) return;
5570
- if (canvasRef) {
5571
- canvasRef.selectWidgetShape(target.id);
5572
- }
6231
+ applyPrimarySelectionOnCanvas(target.id);
6232
+ requestAnimationFrame(() => {
6233
+ setTimeout(() => applyPrimarySelectionOnCanvas(target.id), 80);
6234
+ });
6235
+ notifyHostWidgetName(target);
6236
+ emitCanvasWidgetsList('deeplink-primary');
5573
6237
  if (embeddedInitialStepPending && initialStep) {
5574
6238
  widgetDetailsStep = resolveCreatorWorkflowStep(initialStep);
5575
6239
  embeddedInitialStepPending = false;
5576
6240
  }
5577
-
5578
- if (targetCanvasId && targetCanvasId !== canvasId) {
5579
- // console.log('[CreatorApp] Switching canvas for embedded deep-link:', targetCanvasId);
5580
- routeCanvasId = targetCanvasId;
5581
- canvasId = targetCanvasId;
5582
- saveSelectedCanvasId(canvasId);
5583
- isLoadingCanvasOnce = false;
5584
- isLoadingWidgetsOnce = false;
5585
- lastWidgetsCanvasId = null;
5586
- void loadCurrentCanvas();
5587
- return;
5588
- }
5589
6241
  } catch (err) {
5590
6242
  console.error('[CreatorApp] ensureEmbeddedWidgetDeepLink failed:', err);
5591
6243
  embeddedDeepLinkAutoOpenDone = true;
@@ -6138,6 +6790,11 @@
6138
6790
  }
6139
6791
 
6140
6792
  notifyHostWidgetName({ id: widgetId, name: newName });
6793
+ emitCanvasWidgetsList('widget-renamed');
6794
+ widgetNamesRevision += 1;
6795
+ panelDisplayRevision += 1;
6796
+ backfillWidgetShapeMetadata();
6797
+ syncWidgetShapeLivePreviews();
6141
6798
  // console.log('[WidgetRename] Successfully renamed widget:', widgetId, 'to:', newName);
6142
6799
  showToast('success', `Widget renamed to "${newName}"`);
6143
6800
  } catch (error) {
@@ -6332,26 +6989,37 @@
6332
6989
  * Aborts the polling and resets UI state
6333
6990
  */
6334
6991
  function stopGeneration() {
6335
- // Abort the generation for the currently selected widget
6336
- if (selectedWidgetId) {
6337
- const controller = widgetAbortControllers.get(selectedWidgetId);
6992
+ // Prefer the focused panel widget (Stop lives in that panel's Chat), then selected, then all in-flight.
6993
+ const stopTargetIds = Array.from(
6994
+ new Set(
6995
+ [focusedPanelWidgetId, selectedWidgetId, ...Object.keys(generatingWidgetIds)].filter(
6996
+ (id): id is string => typeof id === 'string' && id.length > 0,
6997
+ ),
6998
+ ),
6999
+ );
7000
+
7001
+ for (const widgetId of stopTargetIds) {
7002
+ const controller = widgetAbortControllers.get(widgetId);
6338
7003
  if (controller) {
6339
7004
  controller.abort();
6340
- widgetAbortControllers.delete(selectedWidgetId);
7005
+ widgetAbortControllers.delete(widgetId);
6341
7006
  }
6342
- const { [selectedWidgetId]: _, ...restStop } = generatingWidgetIds;
6343
- generatingWidgetIds = restStop;
7007
+ clearActiveOperationId(widgetId);
6344
7008
  }
7009
+
7010
+ generatingWidgetIds = {};
6345
7011
  // Legacy fallback
6346
7012
  if (codeGenerationAbortController) {
6347
7013
  codeGenerationAbortController.abort();
6348
7014
  codeGenerationAbortController = null;
6349
7015
  }
6350
-
7016
+
7017
+ currentGenerationOperationId = null;
6351
7018
  isGeneratingCode = false;
6352
7019
  setCrossTabGenerating(false);
6353
7020
  generateCodeStatus = '⏹ Generation stopped by user';
6354
- getChatMethodsForWidget(selectedWidgetId)?.updateLatestCodegenStatusMessage?.(
7021
+ const statusWidgetId = focusedPanelWidgetId || selectedWidgetId || stopTargetIds[0] || null;
7022
+ getChatMethodsForWidget(statusWidgetId)?.updateLatestCodegenStatusMessage?.(
6355
7023
  'Generation stopped by user. You can Retry this prompt.',
6356
7024
  );
6357
7025
  showToast('info', 'Generation stopped', {
@@ -6861,7 +7529,8 @@
6861
7529
  const shapeDataUrl = canvasRef?.getWidgetShapeDataUrl?.(widgetId);
6862
7530
  if (shapeDataUrl && canvasRef) {
6863
7531
  canvasRef.updateWidgetImage(widgetId, shapeDataUrl);
6864
- void persistWidgetListThumbnail(widgetId, shapeDataUrl);
7532
+ // Do NOT persist Fabric placeholder/icon as project-list thumbnail —
7533
+ // only iframe screenshots (below) represent the real widget.
6865
7534
  } else {
6866
7535
  // console.log('[handleRefreshScreenshot] Skip quiet capture — panel closed:', widgetId.substring(0, 8));
6867
7536
  }
@@ -7654,37 +8323,49 @@ Requirements:
7654
8323
  // cache-busts the same URL and looks like an extra reload.
7655
8324
  // console.log('[CreatorApp] Skipping extra preview remount — Worker iframe already showing');
7656
8325
  activePanelRef?.setPreviewLoadingState(false);
8326
+ // previewReady may not re-fire when we skip remount — clear stuck status.
8327
+ patchCodegenUiState(generationWidgetId, {
8328
+ generateCodeStatus: `Code generation completed!${commitShort ? ` Commit: ${commitShort}` : ''} Preview ready.`,
8329
+ });
7657
8330
  } else {
7658
8331
  activePanelRef?.setPreviewLoadingState(true);
7659
8332
  await activePanelRef?.refreshPreview?.({ withRetry: true, skipIfAlreadyShowing: true });
7660
8333
  }
7661
8334
  activePanelRef?.setPreviewBuildError(null);
8335
+ // Show Worker preview on the canvas shape (not the previous Live CDN artifact).
8336
+ shapePreviewSourceByWidgetId = {
8337
+ ...shapePreviewSourceByWidgetId,
8338
+ [generationWidgetId]: 'preview',
8339
+ };
8340
+ syncWidgetShapeLivePreviews();
8341
+ panelDisplayRevision += 1;
7662
8342
  if (!completedPreviewBuildErr) {
7663
8343
  await applyDesignSchemaDefaultsToPreview(generationWidgetId);
7664
8344
  pendingCreatePreviewDefaultsWidgetId = null;
7665
8345
  panelChat?.updateLatestCodegenStatusMessage?.(
7666
8346
  `Code generation completed!${commitShort ? ` Commit: ${commitShort}` : ''}\nPreview ready.`,
7667
8347
  );
8348
+ patchCodegenUiState(generationWidgetId, {
8349
+ generateCodeStatus: `Code generation completed!${commitShort ? ` Commit: ${commitShort}` : ''} Preview ready.`,
8350
+ });
7668
8351
  setTimeout(async () => {
7669
8352
  await panelChat?.reloadConversationMessages?.();
7670
8353
  panelChat?.markWidgetHeadAtLatestCommit?.();
7671
8354
  }, 2500);
7672
- setTimeout(() => handleRefreshScreenshot(generationWidgetId, { quiet: true }), 3000);
7673
- void (async () => {
7674
- try {
7675
- console.log('[Publish] Auto-publishing after codegen', generationWidgetId.substring(0, 8));
7676
- await executePublishWidget(generationWidgetId, { silent: true, fromCodegen: true });
7677
- panelChat?.updateLatestCodegenStatusMessage?.(
7678
- `Code generation completed!${commitShort ? ` Commit: ${commitShort}` : ''}\nPreview ready. Live published.`,
7679
- );
7680
- } catch (autoPublishErr) {
7681
- console.warn('[Publish] Auto-publish after codegen failed:', autoPublishErr);
7682
- }
7683
- })();
8355
+ setTimeout(() => {
8356
+ void (async () => {
8357
+ await handleRefreshScreenshot(generationWidgetId, { quiet: true });
8358
+ // List thumb only from live iframe capture — never Fabric shape icon.
8359
+ })();
8360
+ }, 3000);
8361
+ // Do NOT auto-publish — user previews first, then clicks Publish / Set live.
7684
8362
  } else {
7685
8363
  panelChat?.updateLatestCodegenStatusMessage?.(
7686
8364
  `Code saved${commitShort ? ` (${commitShort})` : ''} — preview rebuilt from saved code.`,
7687
8365
  );
8366
+ patchCodegenUiState(generationWidgetId, {
8367
+ generateCodeStatus: `Code saved${commitShort ? ` (${commitShort})` : ''} — preview rebuilt from saved code.`,
8368
+ });
7688
8369
  }
7689
8370
  }, completedPreviewBuildErr ? 400 : 800);
7690
8371
  } else {
@@ -8127,22 +8808,21 @@ Requirements:
8127
8808
  window.addEventListener('storage', handleCrossTabStorageEvent);
8128
8809
  window.addEventListener('widgetic:session-extended', handleSiteSessionExtended);
8129
8810
 
8130
- // Clean up cross-tab flags when tab is closed/refreshed
8131
- const handleBeforeUnload = () => {
8132
- for (const wId of Object.keys(generatingWidgetIds)) {
8133
- setCrossTabGenerating(false, wId);
8134
- }
8135
- if (isPublishing) setCrossTabPublishing(false);
8136
- };
8811
+ // Clean up cross-tab flags when tab is closed/refreshed. The handler lives at
8812
+ // component scope so `onDestroy` can remove the same reference.
8137
8813
  window.addEventListener('beforeunload', handleBeforeUnload);
8138
8814
 
8139
- // Read canvas id from route (supports /?canvasId=uuid). Skip in embedded when prop provided.
8140
- if (!canvasId) {
8815
+ // Read canvas id from the route. The route path (`/canvas/<canvasId>`) is the
8816
+ // AUTHORITATIVE canvas (site links, refresh, deep-links); localStorage is only a
8817
+ // fallback and must never win over it.
8818
+ if (!routeCanvasId) {
8141
8819
  try {
8142
8820
  const sub = pageStore.subscribe(($page) => {
8143
- const qId =
8144
- ($page?.url?.searchParams?.get && $page.url.searchParams.get('canvas-id')) || null;
8145
- routeCanvasId = qId;
8821
+ // `readCanvasIdFromAddressBar()` also covers the legacy query string, so an
8822
+ // old bookmark `/canvas?canvas_id=…` still resolves to its canvas.
8823
+ const nextCanvasId = readCanvasIdFromAddressBar();
8824
+ // Never clobber an already-known canvas with null.
8825
+ if (nextCanvasId) routeCanvasId = nextCanvasId;
8146
8826
  });
8147
8827
  setTimeout(() => {
8148
8828
  if (sub && typeof sub === 'function') sub();
@@ -8434,6 +9114,10 @@ Requirements:
8434
9114
  function formatCodegenProgressStatus(op: OperationStatus): string {
8435
9115
  const stage = getOperationStageMessage(op.metadata as Record<string, unknown> | undefined);
8436
9116
  const pct = typeof op.progress === 'number' ? op.progress : null;
9117
+ // User mode: never expose model names / provider in the status line.
9118
+ if (!debugMode) {
9119
+ return pct != null && pct > 0 ? `Generating code... (${pct}%)` : 'Generating code...';
9120
+ }
8437
9121
  if (stage) {
8438
9122
  return pct != null && pct > 0 ? `${stage} (${pct}%)` : stage;
8439
9123
  }
@@ -8463,13 +9147,22 @@ Requirements:
8463
9147
  >,
8464
9148
  ): void {
8465
9149
  ensurePanelState(widgetId);
8466
- patchPanelSnapshot(widgetId, patch);
9150
+ const nextPatch = { ...patch };
9151
+ if (typeof nextPatch.generateCodeStatus === 'string' && !debugMode) {
9152
+ nextPatch.generateCodeStatus = nextPatch.generateCodeStatus
9153
+ .replace(/\s*Commit:\s*[a-f0-9]{7,40}/gi, '')
9154
+ .replace(/\s*\(\s*Commit:\s*[a-f0-9]{7,40}\s*\)/gi, '')
9155
+ .replace(/\s*\(\s*[a-f0-9]{7,40}\s*\)/g, '')
9156
+ .replace(/\s{2,}/g, ' ')
9157
+ .trim();
9158
+ }
9159
+ patchPanelSnapshot(widgetId, nextPatch);
8467
9160
  if (widgetId === focusedPanelWidgetId) {
8468
- if (patch.generateCodeStatus !== undefined) generateCodeStatus = patch.generateCodeStatus;
8469
- if (patch.lastGenerationFailed !== undefined) lastGenerationFailed = patch.lastGenerationFailed;
8470
- if (patch.lastGenerationError !== undefined) lastGenerationError = patch.lastGenerationError;
8471
- if (patch.lastCommitId !== undefined) lastCommitId = patch.lastCommitId;
8472
- if (patch.hasGeneratedCode !== undefined) hasGeneratedCode = patch.hasGeneratedCode;
9161
+ if (nextPatch.generateCodeStatus !== undefined) generateCodeStatus = nextPatch.generateCodeStatus;
9162
+ if (nextPatch.lastGenerationFailed !== undefined) lastGenerationFailed = nextPatch.lastGenerationFailed;
9163
+ if (nextPatch.lastGenerationError !== undefined) lastGenerationError = nextPatch.lastGenerationError;
9164
+ if (nextPatch.lastCommitId !== undefined) lastCommitId = nextPatch.lastCommitId;
9165
+ if (nextPatch.hasGeneratedCode !== undefined) hasGeneratedCode = nextPatch.hasGeneratedCode;
8473
9166
  }
8474
9167
  }
8475
9168
 
@@ -8609,13 +9302,15 @@ Requirements:
8609
9302
  compositionSchemasLoading: boolean;
8610
9303
  compositionSchemasError: string | null;
8611
9304
  schemaLoadStartedAt: number | null;
8612
- chatContainerHeight: number;
9305
+ chatContainerHeight: number; // 0 = fill available; >0 = user-resized fixed px
8613
9306
  panelPreviewCompileError: string | null;
8614
9307
  };
8615
9308
 
8616
9309
  let widgetPanelSnapshots: Record<string, WidgetPanelSnapshot> = {};
8617
9310
  /** Bumped on every panel snapshot write so {#each} panels re-read display state. */
8618
9311
  let panelDisplayRevision = 0;
9312
+ /** Bumped on rename so shape chrome / host title re-read widget names. */
9313
+ let widgetNamesRevision = 0;
8619
9314
  const schemaLoadInFlight = new Set<string>();
8620
9315
  const schemaLoadPromises = new Map<string, Promise<void>>();
8621
9316
  const compositionsLoadInFlight = new Set<string>();
@@ -8711,7 +9406,7 @@ Requirements:
8711
9406
  compositionSchemasLoading,
8712
9407
  compositionSchemasError,
8713
9408
  schemaLoadStartedAt: focusedSnap?.schemaLoadStartedAt ?? null,
8714
- chatContainerHeight: focusedSnap?.chatContainerHeight ?? CHAT_DEFAULT_HEIGHT,
9409
+ chatContainerHeight: focusedSnap?.chatContainerHeight ?? 0,
8715
9410
  panelPreviewCompileError: focusedSnap?.panelPreviewCompileError ?? null,
8716
9411
  };
8717
9412
  }
@@ -8777,7 +9472,7 @@ Requirements:
8777
9472
  compositionSchemasLoading: false,
8778
9473
  compositionSchemasError: null,
8779
9474
  schemaLoadStartedAt: null,
8780
- chatContainerHeight: 550,
9475
+ chatContainerHeight: 0,
8781
9476
  panelPreviewCompileError: null,
8782
9477
  };
8783
9478
  }
@@ -8833,7 +9528,7 @@ Requirements:
8833
9528
  : livePublishMatchesPanel
8834
9529
  ? live.publishHistoryResolvedForWidgetId
8835
9530
  : snap.publishHistoryResolvedForWidgetId,
8836
- chatContainerHeight: snap.chatContainerHeight ?? CHAT_DEFAULT_HEIGHT,
9531
+ chatContainerHeight: snap.chatContainerHeight ?? 0,
8837
9532
  compositionDesignSchema: resolvedDesignSchema,
8838
9533
  compositionContentSchema: resolvedContentSchema,
8839
9534
  compositionSchemasLoading: schemaLoading,
@@ -8851,7 +9546,7 @@ Requirements:
8851
9546
  ...snap,
8852
9547
  lastCommitId: snap.lastCommitId || loadCommitIdForWidget(widgetId),
8853
9548
  hasGeneratedCode: !!(snap.hasGeneratedCode || snap.lastCommitId || loadCommitIdForWidget(widgetId)),
8854
- chatContainerHeight: snap.chatContainerHeight ?? CHAT_DEFAULT_HEIGHT,
9549
+ chatContainerHeight: snap.chatContainerHeight ?? 0,
8855
9550
  compositionDesignSchema: resolvedDesignSchema,
8856
9551
  compositionContentSchema: resolvedContentSchema,
8857
9552
  compositionSchemasError: resolvedSchemaError,
@@ -8996,6 +9691,247 @@ Requirements:
8996
9691
  return resolvePublishedWidgetUrl(widgetId, version, jsPath);
8997
9692
  }
8998
9693
 
9694
+ /** Widget Builder origin for canvas shape iframes (mirrors WidgetDetails). */
9695
+ function widgetBuilderPreviewBaseUrl(): string {
9696
+ const publicBase = (
9697
+ widgetBuilderUrl ||
9698
+ import.meta.env.VITE_WIDGET_BUILDER_URL ||
9699
+ import.meta.env.PUBLIC_WIDGET_BUILDER_URL ||
9700
+ 'https://wbuilder.widgetic.com'
9701
+ ).replace(/\/+$/, '');
9702
+ if (import.meta.env.DEV && publicBase.includes('wbuilder.widgetic.com')) {
9703
+ return '/__widget_builder__';
9704
+ }
9705
+ return publicBase;
9706
+ }
9707
+
9708
+ /**
9709
+ * Live preview URL for a canvas WidgetShape overlay.
9710
+ * Source: 'live' | 'preview' | 'v:{N}' (historical published version).
9711
+ * Publish history is loaded without requiring chat to be open.
9712
+ */
9713
+ let shapePreviewSourceByWidgetId: Record<string, string> = {};
9714
+ let shapePublishHistoryByWidgetId: Record<string, any[]> = {};
9715
+ let shapeHistoryLoadInFlight: Record<string, boolean> = {};
9716
+
9717
+ async function ensureShapePublishHistory(widgetId: string): Promise<void> {
9718
+ if (!widgetId || shapeHistoryLoadInFlight[widgetId]) return;
9719
+ const cached = shapePublishHistoryByWidgetId[widgetId];
9720
+ if (cached && cached.length > 0) return;
9721
+ const fromPanel = getPanelDisplayState(widgetId).publishHistory;
9722
+ if (fromPanel && fromPanel.length > 0) {
9723
+ shapePublishHistoryByWidgetId = {
9724
+ ...shapePublishHistoryByWidgetId,
9725
+ [widgetId]: [...fromPanel],
9726
+ };
9727
+ return;
9728
+ }
9729
+ shapeHistoryLoadInFlight = { ...shapeHistoryLoadInFlight, [widgetId]: true };
9730
+ try {
9731
+ await loadPublishHistoryForPanel(widgetId, { silent: true });
9732
+ const history = getPanelDisplayState(widgetId).publishHistory || [];
9733
+ shapePublishHistoryByWidgetId = {
9734
+ ...shapePublishHistoryByWidgetId,
9735
+ [widgetId]: [...history],
9736
+ };
9737
+ syncWidgetShapeLivePreviews();
9738
+ } finally {
9739
+ const { [widgetId]: _, ...rest } = shapeHistoryLoadInFlight;
9740
+ shapeHistoryLoadInFlight = rest;
9741
+ }
9742
+ }
9743
+
9744
+ function getShapePublishHistory(widgetId: string): any[] {
9745
+ const cached = shapePublishHistoryByWidgetId[widgetId];
9746
+ if (cached?.length) return cached;
9747
+ const fromPanel = getPanelDisplayState(widgetId).publishHistory;
9748
+ return fromPanel?.length ? fromPanel : [];
9749
+ }
9750
+
9751
+ function resolveWidgetShapeLivePreviewUrl(widgetId: string): string | null {
9752
+ if (!widgetId) return null;
9753
+ const source = shapePreviewSourceByWidgetId[widgetId] || 'live';
9754
+ const ps = getPanelDisplayState(widgetId);
9755
+ const hasCode = !!(ps.lastCommitId || loadCommitIdForWidget(widgetId));
9756
+ const workerUrl = hasCode
9757
+ ? `${widgetBuilderPreviewBaseUrl()}/preview/${widgetId}`
9758
+ : null;
9759
+
9760
+ if (source.startsWith('v:')) {
9761
+ const version = parseInt(source.slice(2), 10);
9762
+ if (Number.isFinite(version)) {
9763
+ const record = getShapePublishHistory(widgetId).find(
9764
+ (row) => Number(row.version) === version,
9765
+ );
9766
+ const artifact =
9767
+ record?.artifactUrl ||
9768
+ record?.artifact_url ||
9769
+ (version === ps.lastPublishedVersion ? ps.selectedWidgetJsPath : null);
9770
+ const url = resolvePublishedWidgetUrl(widgetId, version, artifact ?? null);
9771
+ if (url) return url;
9772
+ }
9773
+ }
9774
+
9775
+ // After codegen (unpublished), prefer Worker preview over the Live CDN artifact.
9776
+ if (source === 'preview' || ps.pendingPublishAfterCodegen) {
9777
+ if (workerUrl) return workerUrl;
9778
+ }
9779
+
9780
+ const published = getPanelEditPreviewUrl(widgetId);
9781
+ if (published && source !== 'preview') return published;
9782
+ return workerUrl;
9783
+ }
9784
+
9785
+ function resolveWidgetShapeVersionLabel(widgetId: string): string | null {
9786
+ const source = shapePreviewSourceByWidgetId[widgetId] || 'live';
9787
+ const ps = getPanelDisplayState(widgetId);
9788
+ const liveVersion =
9789
+ ps.lastPublishedVersion ?? resolvePublishedVersionFromWidget(getWidgetById(widgetId));
9790
+ if (source.startsWith('v:')) {
9791
+ const version = source.slice(2);
9792
+ return `v${version}`;
9793
+ }
9794
+ return liveVersion != null ? `v${liveVersion}` : null;
9795
+ }
9796
+
9797
+ function formatPublishHistoryDate(record: {
9798
+ publishedAt?: string;
9799
+ published_at?: string;
9800
+ }): string {
9801
+ const d = new Date(record.publishedAt ?? record.published_at ?? '');
9802
+ if (isNaN(d.getTime())) return '—';
9803
+ const month = String(d.getMonth() + 1).padStart(2, '0');
9804
+ const day = String(d.getDate()).padStart(2, '0');
9805
+ const timePart = d.toLocaleTimeString(undefined, {
9806
+ hour: '2-digit',
9807
+ minute: '2-digit',
9808
+ hour12: false,
9809
+ });
9810
+ return `${month}/${day}/${d.getFullYear()} ${timePart}`;
9811
+ }
9812
+
9813
+ function resolveWidgetShapeVersionOptions(
9814
+ widgetId: string,
9815
+ ): Array<{
9816
+ id: string;
9817
+ label: string;
9818
+ shortLabel?: string;
9819
+ dateLabel?: string;
9820
+ isLive?: boolean;
9821
+ }> | null {
9822
+ const ps = getPanelDisplayState(widgetId);
9823
+ const liveVersion =
9824
+ ps.lastPublishedVersion ?? resolvePublishedVersionFromWidget(getWidgetById(widgetId));
9825
+ const history = [...getShapePublishHistory(widgetId)].sort(
9826
+ (a, b) => Number(b.version) - Number(a.version),
9827
+ );
9828
+ const options: Array<{
9829
+ id: string;
9830
+ label: string;
9831
+ shortLabel?: string;
9832
+ dateLabel?: string;
9833
+ isLive?: boolean;
9834
+ }> = [];
9835
+ for (const record of history) {
9836
+ const version = Number(record.version);
9837
+ if (!Number.isFinite(version)) continue;
9838
+ options.push({
9839
+ id: `v:${version}`,
9840
+ label: `Version ${version}`,
9841
+ shortLabel: `v${version}`,
9842
+ dateLabel: formatPublishHistoryDate(record),
9843
+ isLive: version === liveVersion,
9844
+ });
9845
+ }
9846
+ if (options.length === 0 && liveVersion != null) {
9847
+ options.push({
9848
+ id: `v:${liveVersion}`,
9849
+ label: `Version ${liveVersion}`,
9850
+ shortLabel: `v${liveVersion}`,
9851
+ dateLabel: '—',
9852
+ isLive: true,
9853
+ });
9854
+ }
9855
+ // Kick off history fetch so the dropdown fills even before chat opens
9856
+ if (history.length === 0 && liveVersion != null) {
9857
+ void ensureShapePublishHistory(widgetId);
9858
+ }
9859
+ return options.length > 0 ? options : null;
9860
+ }
9861
+
9862
+ function resolveWidgetShapeVersionSelectedId(widgetId: string): string | null {
9863
+ const source = shapePreviewSourceByWidgetId[widgetId];
9864
+ if (source?.startsWith('v:')) return source;
9865
+ const ps = getPanelDisplayState(widgetId);
9866
+ const liveVersion =
9867
+ ps.lastPublishedVersion ?? resolvePublishedVersionFromWidget(getWidgetById(widgetId));
9868
+ return liveVersion != null ? `v:${liveVersion}` : null;
9869
+ }
9870
+
9871
+ function handleWidgetShapeVersionChange(widgetId: string, versionId: string): void {
9872
+ if (!versionId) return;
9873
+ shapePreviewSourceByWidgetId = {
9874
+ ...shapePreviewSourceByWidgetId,
9875
+ [widgetId]: versionId,
9876
+ };
9877
+ syncWidgetShapeLivePreviews();
9878
+ panelDisplayRevision += 1;
9879
+ console.log('[WidgetShape] Preview source →', versionId, widgetId.substring(0, 8));
9880
+ }
9881
+
9882
+ /** Commit SHA for the version currently selected in the version menu (drives Head-at-commit badge). */
9883
+ function resolveViewingCommitShaForPanel(widgetId: string): string | null {
9884
+ const ps = getPanelDisplayState(widgetId);
9885
+ const source = shapePreviewSourceByWidgetId[widgetId] || 'live';
9886
+ if (source === 'preview') {
9887
+ return normalizeCommitSha(ps.lastCommitId);
9888
+ }
9889
+ let version: number | null = null;
9890
+ if (source.startsWith('v:')) {
9891
+ const parsed = parseInt(source.slice(2), 10);
9892
+ version = Number.isFinite(parsed) ? parsed : null;
9893
+ } else {
9894
+ version = ps.lastPublishedVersion ?? resolvePublishedVersionFromWidget(getWidgetById(widgetId));
9895
+ }
9896
+ if (version == null) return normalizeCommitSha(ps.lastCommitId);
9897
+ const history = ps.publishHistory?.length ? ps.publishHistory : publishHistory;
9898
+ const record = history.find((r) => Number(r.version) === Number(version));
9899
+ const fromHistory = normalizeCommitSha(record?.commitSha ?? record?.commit_sha);
9900
+ if (fromHistory) return fromHistory;
9901
+ // Live version with no history row yet — fall back to published SHA, then HEAD.
9902
+ if (Number(version) === Number(ps.lastPublishedVersion)) {
9903
+ return (
9904
+ normalizeCommitSha(getLivePublishedCommitShaForPanel(widgetId))
9905
+ ?? normalizeCommitSha(ps.lastCommitId)
9906
+ );
9907
+ }
9908
+ return normalizeCommitSha(ps.lastCommitId);
9909
+ }
9910
+
9911
+ function syncWidgetShapeLivePreviews(): void {
9912
+ try {
9913
+ canvasSelectedWidgetShapeId = canvasRef?.getSelectedWidgetShapeId?.() ?? null;
9914
+ widgetShapeLivePreviewsRef?.sync?.();
9915
+ // Hide Fabric screenshots under live iframes (avoids ghost bleed in padding).
9916
+ const rects = canvasRef?.listWidgetShapeScreenRects?.() ?? [];
9917
+ const liveWidgetIds = new Set<string>();
9918
+ for (const rect of rects) {
9919
+ if (!rect?.widgetId) continue;
9920
+ if (!resolveWidgetShapeLivePreviewUrl(rect.widgetId)) continue;
9921
+ liveWidgetIds.add(rect.widgetId);
9922
+ canvasRef?.setWidgetShapeLivePreviewHiddenImage?.(rect.widgetId, true);
9923
+ }
9924
+ const linkedIds = canvasRef?.getLinkedWidgetIds?.() ?? new Set<string>();
9925
+ for (const widgetId of linkedIds) {
9926
+ if (!liveWidgetIds.has(widgetId)) {
9927
+ canvasRef?.setWidgetShapeLivePreviewHiddenImage?.(widgetId, false);
9928
+ }
9929
+ }
9930
+ } catch (err) {
9931
+ console.warn('[CreatorApp] WidgetShape live preview sync failed', err);
9932
+ }
9933
+ }
9934
+
8999
9935
  function getPanelEmbedPreviewUrl(widgetId: string): string | null {
9000
9936
  const base = getPanelEditPreviewUrl(widgetId);
9001
9937
  if (!base) return null;
@@ -9333,6 +10269,9 @@ Requirements:
9333
10269
  if (detailsSessionWidgetId === fromId) detailsSessionWidgetId = toId;
9334
10270
  if (widgetDetailsOpenTargetId === fromId) widgetDetailsOpenTargetId = toId;
9335
10271
  showWidgetDetails = openWidgetPanelIds.length > 0;
10272
+ // Any *other* draft panel still registered would keep a real widget panel
10273
+ // bound to a `draft_convert_*` context id — clear those too.
10274
+ purgeDraftConvertPanels();
9336
10275
  // console.log(
9337
10276
  // '[CreatorApp] Migrated Widget Details panel',
9338
10277
  // fromId.substring(0, 16),
@@ -9928,6 +10867,8 @@ Requirements:
9928
10867
 
9929
10868
  function hasUnpublishedChangesForPanel(widgetId: string): boolean {
9930
10869
  const ps = getPanelDisplayState(widgetId);
10870
+ if (ps.pendingPublishAfterCodegen) return true;
10871
+ const pendingSha = loadPendingPublishShaForWidget(widgetId);
9931
10872
  const headSha = resolvePublishHeadCommitId(widgetId, ps);
9932
10873
  if (!headSha || ps.lastPublishedVersion === null) return false;
9933
10874
 
@@ -9935,6 +10876,7 @@ Requirements:
9935
10876
  getLivePublishedCommitShaForPanel(widgetId)
9936
10877
  ?? getWidgetLastPublishCommitSha(widgetId);
9937
10878
  if (!liveSha) return false;
10879
+ if (pendingSha && !commitsMatch(pendingSha, liveSha)) return true;
9938
10880
 
9939
10881
  return !commitsMatch(headSha, liveSha);
9940
10882
  }
@@ -10375,6 +11317,10 @@ async function loadPublishHistoryForPanel(widgetId: string, opts: { silent?: boo
10375
11317
  publishHistoryResolved: true,
10376
11318
  publishHistoryResolvedForWidgetId: widgetId,
10377
11319
  });
11320
+ shapePublishHistoryByWidgetId = {
11321
+ ...shapePublishHistoryByWidgetId,
11322
+ [widgetId]: [...history],
11323
+ };
10378
11324
  if (widgetId === focusedPanelWidgetId) {
10379
11325
  publishHistory = history;
10380
11326
  publishHistoryResolved = true;
@@ -10417,8 +11363,8 @@ async function loadPublishHistory(opts: { silent?: boolean } = {}) {
10417
11363
  * Sets a historical version as the current live version without rebuilding.
10418
11364
  * Updates widgets.js_path to point to the older version's artifact URL.
10419
11365
  */
10420
- async function setLiveVersion(version: number) {
10421
- if (!selectedWidgetId || !$userSession?.jwt) return;
11366
+ async function setLiveVersionForWidget(widgetId: string, version: number) {
11367
+ if (!widgetId || !$userSession?.jwt) return;
10422
11368
 
10423
11369
  isSettingLiveVersion = version;
10424
11370
  if (!widgetsClient) {
@@ -10428,12 +11374,12 @@ async function setLiveVersion(version: number) {
10428
11374
  }
10429
11375
  try {
10430
11376
  const liveRes = await widgetsClient.setWidgetPublishHistoryLive({
10431
- widgetId: selectedWidgetId,
11377
+ widgetId,
10432
11378
  version
10433
11379
  });
10434
11380
  const nested = liveRes?.data as { data?: Record<string, unknown>; artifactUrl?: string; artifact_url?: string; commitSha?: string; commit_sha?: string } | undefined;
10435
11381
  const liveData = nested?.data ?? nested ?? {};
10436
- const history = getPanelDisplayState(selectedWidgetId).publishHistory;
11382
+ const history = getPanelDisplayState(widgetId).publishHistory;
10437
11383
  const record = history.find((row) => row.version === version);
10438
11384
  const commitSha = (liveData as { commitSha?: string; commit_sha?: string }).commitSha
10439
11385
  || (liveData as { commit_sha?: string }).commit_sha
@@ -10441,15 +11387,23 @@ async function setLiveVersion(version: number) {
10441
11387
  const artifactUrl = (liveData as { artifactUrl?: string; artifact_url?: string }).artifactUrl
10442
11388
  || (liveData as { artifact_url?: string }).artifact_url
10443
11389
  || record?.artifactUrl || record?.artifact_url || null;
10444
- applyPublishSuccessToWidgetState(selectedWidgetId, version, artifactUrl, commitSha);
11390
+ applyPublishSuccessToWidgetState(widgetId, version, artifactUrl, commitSha);
11391
+ shapePreviewSourceByWidgetId = {
11392
+ ...shapePreviewSourceByWidgetId,
11393
+ [widgetId]: `v:${version}`,
11394
+ };
11395
+ syncWidgetShapeLivePreviews();
11396
+ panelDisplayRevision += 1;
10445
11397
  isHistoryOpen = false;
10446
- const liveUrl = resolvePublishedWidgetUrl(selectedWidgetId, version, artifactUrl);
10447
- const detailsRef = getWidgetDetailsRef(selectedWidgetId);
11398
+ const liveUrl = resolvePublishedWidgetUrl(widgetId, version, artifactUrl);
11399
+ const detailsRef = getWidgetDetailsRef(widgetId);
10448
11400
  detailsRef?.showPublishedArtifact?.(liveUrl);
10449
11401
  detailsRef?.beginCompositionHydration?.();
10450
- revealCompositionPreview();
10451
- showToast('success', `Reverted to v${version}`, {
10452
- description: 'Widget is now live at this version. No rebuild needed.',
11402
+ if (widgetId === selectedWidgetId || widgetId === focusedPanelWidgetId) {
11403
+ revealCompositionPreview();
11404
+ }
11405
+ showToast('success', `Live: v${version}`, {
11406
+ description: 'This version is now what visitors see on your sites.',
10453
11407
  duration: 5000
10454
11408
  });
10455
11409
  } catch (err: any) {
@@ -10460,6 +11414,11 @@ async function setLiveVersion(version: number) {
10460
11414
  }
10461
11415
  }
10462
11416
 
11417
+ async function setLiveVersion(version: number) {
11418
+ if (!selectedWidgetId) return;
11419
+ await setLiveVersionForWidget(selectedWidgetId, version);
11420
+ }
11421
+
10463
11422
  /** Toggle publish history panel — reloads data on every open */
10464
11423
  function togglePublishHistory(triggerEl?: HTMLElement | null) {
10465
11424
  isHistoryOpen = !isHistoryOpen;
@@ -11018,18 +11977,10 @@ $: if (codeGenerationPrompt && selectedWidgetId && promptLoadedForWidgetId === s
11018
11977
  }
11019
11978
  }
11020
11979
 
11021
- // Keep address-bar widget_id on the canvas primary (duplicate/select must not steal it).
11022
- if (typeof window !== 'undefined' && embedded && canvasPrimaryWidgetId && widgetId === canvasPrimaryWidgetId) {
11023
- try {
11024
- const url = new URL(window.location.href);
11025
- if (url.searchParams.get('widget_id') !== widgetId) {
11026
- url.searchParams.set('widget_id', widgetId);
11027
- window.history.replaceState({}, '', url.pathname + url.search + url.hash);
11028
- syncEmbeddedDeepLinkDismissTracking();
11029
- }
11030
- } catch (err) {
11031
- console.warn('[WidgetCreator] Failed to sync widget_id in URL:', err);
11032
- }
11980
+ // Clicking a widget shape means "open this widget" — make it the canvas primary so a
11981
+ // refresh reopens it. The URL only carries canvas_id; widget_id is no longer written.
11982
+ if (embedded && widgetId !== canvasPrimaryWidgetId) {
11983
+ void persistPrimaryWidgetId(widgetId);
11033
11984
  }
11034
11985
  // Full select so commit/preview/header match the shape's linked widget (not a stale URL widget).
11035
11986
  if (selectedWidgetId !== widgetId) {
@@ -11261,6 +12212,10 @@ $: if (codeGenerationPrompt && selectedWidgetId && promptLoadedForWidgetId === s
11261
12212
  }
11262
12213
  notifyHostWidgetName({ id: widgetId, name: newName });
11263
12214
  emitCanvasWidgetsList('widget-renamed');
12215
+ widgetNamesRevision += 1;
12216
+ panelDisplayRevision += 1;
12217
+ backfillWidgetShapeMetadata();
12218
+ syncWidgetShapeLivePreviews();
11264
12219
  showToast('success', `Widget renamed to "${newName}"`);
11265
12220
  } catch (error) {
11266
12221
  console.error('[WidgetCreator] Failed to rename widget:', error);
@@ -11298,6 +12253,7 @@ $: if (codeGenerationPrompt && selectedWidgetId && promptLoadedForWidgetId === s
11298
12253
  if (selectedWidget?.id && !didForceCreateNewWidget) {
11299
12254
  placeSelectedWidgetShapeOnCanvas();
11300
12255
  }
12256
+ syncWidgetShapeLivePreviews();
11301
12257
  return;
11302
12258
  }
11303
12259
  const { loaded, widgetIds: widgetIdsOnCanvas } = await tryLoadCanvasState();
@@ -11306,18 +12262,21 @@ $: if (codeGenerationPrompt && selectedWidgetId && promptLoadedForWidgetId === s
11306
12262
  if (!isCanvasStateLoaded) {
11307
12263
  scheduleCanvasHydrateRetry('canvas ready before canvasId');
11308
12264
  }
12265
+ syncWidgetShapeLivePreviews();
11309
12266
  return;
11310
12267
  }
11311
12268
  await syncWidgetsWithCanvas(widgetIdsOnCanvas);
11312
12269
  await maybeAutoRestoreWidgetShapes();
11313
- const primaryId = await hydrateCanvasPrimaryWidget(widgetIdsOnCanvas);
11314
- if (primaryId && canvasRef && !isConvertOrDraftPanelOpen()) {
11315
- canvasRef.setPrimaryWidgetShape?.(primaryId);
11316
- const found = canvasRef.selectWidgetShape(primaryId);
11317
- // console.log('[CreatorApp] Primary widget shape after canvas load:', primaryId, found);
11318
- } else if (isConvertOrDraftPanelOpen()) {
11319
- // console.log('[CreatorApp] Skip primary shape select — convert/draft panel is open');
11320
- }
12270
+ const linkedIds =
12271
+ widgetIdsOnCanvas.length > 0
12272
+ ? widgetIdsOnCanvas
12273
+ : [...(canvasRef?.getLinkedWidgetIds?.() ?? [])];
12274
+ const primaryId = await hydrateCanvasPrimaryWidget(linkedIds);
12275
+ applyPrimarySelectionOnCanvas(primaryId);
12276
+ requestAnimationFrame(() => {
12277
+ setTimeout(() => applyPrimarySelectionOnCanvas(primaryId), 80);
12278
+ });
12279
+ syncWidgetShapeLivePreviews();
11321
12280
  }
11322
12281
 
11323
12282
  /**
@@ -11591,14 +12550,20 @@ $: if (codeGenerationPrompt && selectedWidgetId && promptLoadedForWidgetId === s
11591
12550
  let autosaveSequence = 0;
11592
12551
  let lastResolvedAutosaveSequence = 0;
11593
12552
 
11594
- async function handleCanvasAutoSave(json: string | null, isUnmountSave = false) {
12553
+ async function handleCanvasAutoSave(
12554
+ json: string | null,
12555
+ isUnmountSave = false,
12556
+ opts: { force?: boolean } = {},
12557
+ ) {
11595
12558
  if (!json) {
11596
12559
  if (isUnmountSave) console.warn('[CanvasDebug] onBeforeUnmount: saveCanvas returned null — canvas not initialized');
11597
12560
  return;
11598
12561
  }
11599
12562
 
12563
+ const force = opts.force === true;
12564
+
11600
12565
  // Guard: don't overwrite DB with an empty canvas if state hasn't loaded yet
11601
- if (!isCanvasStateLoaded) {
12566
+ if (!force && !isCanvasStateLoaded) {
11602
12567
  // console.log(
11603
12568
  // '[CanvasDebug] Skipping autosave — canvas state not loaded yet (guard active, avoids overwrite with empty)'
11604
12569
  // );
@@ -11606,7 +12571,7 @@ $: if (codeGenerationPrompt && selectedWidgetId && promptLoadedForWidgetId === s
11606
12571
  }
11607
12572
 
11608
12573
  // Guard: skip autosaves triggered by loadCanvas() object:added events
11609
- if (isLoadCooldownActive && !isUnmountSave) {
12574
+ if (!force && isLoadCooldownActive && !isUnmountSave) {
11610
12575
  // console.log('[CanvasDebug] Skipping autosave — load cooldown active (prevents overwriting freshly loaded data)');
11611
12576
  return;
11612
12577
  }
@@ -11629,11 +12594,17 @@ $: if (codeGenerationPrompt && selectedWidgetId && promptLoadedForWidgetId === s
11629
12594
  // Unmount saves (HMR) are always blocked when going from N→0.
11630
12595
  // Normal autosaves going from N→0 are allowed only if the drop is gradual
11631
12596
  // (user deleted objects one by one) — a sudden jump from many to 0 is suspicious.
12597
+ // Explicit Save (force) still blocks sudden N→0 to avoid wiping the canvas.
11632
12598
  if (objectCount === 0 && lastSavedCanvasObjectCount > 0) {
11633
12599
  if (isUnmountSave || lastSavedCanvasObjectCount > 1) {
11634
12600
  console.warn(
11635
- `[CanvasDebug] BLOCKING save: would overwrite ${lastSavedCanvasObjectCount} shapes with 0 (isUnmount=${isUnmountSave})`
12601
+ `[CanvasDebug] BLOCKING save: would overwrite ${lastSavedCanvasObjectCount} shapes with 0 (isUnmount=${isUnmountSave}, force=${force})`
11636
12602
  );
12603
+ if (force) {
12604
+ showToast('error', 'Save blocked', {
12605
+ description: 'Canvas snapshot looked empty — refresh and try again.',
12606
+ });
12607
+ }
11637
12608
  return;
11638
12609
  }
11639
12610
  // lastSavedCanvasObjectCount === 1 → user deleted the last object, allow it
@@ -11670,12 +12641,25 @@ $: if (codeGenerationPrompt && selectedWidgetId && promptLoadedForWidgetId === s
11670
12641
  return;
11671
12642
  }
11672
12643
  lastResolvedAutosaveSequence = currentSeq;
12644
+ if (force) {
12645
+ showToast('success', 'Canvas saved', { duration: 2000 });
12646
+ }
11673
12647
  // console.log(
11674
12648
  // `[CanvasDebug] Canvas ${canvasId} saved to API: ${objectCount} shapes (seq ${currentSeq})`
11675
12649
  // );
11676
12650
  } catch (err) {
11677
12651
  console.warn('[WidgetCreator] Failed to save canvas to API:', err);
12652
+ if (force) {
12653
+ showToast('error', 'Canvas save failed', {
12654
+ description: 'Kept a local copy — check your connection and retry.',
12655
+ });
12656
+ }
11678
12657
  }
12658
+ } else if (force) {
12659
+ showToast('success', 'Canvas saved locally', {
12660
+ description: canvasId ? 'API client unavailable.' : 'No canvas id yet.',
12661
+ duration: 2500,
12662
+ });
11679
12663
  }
11680
12664
 
11681
12665
  refreshLinkedWidgetIds();
@@ -12019,6 +13003,68 @@ $: if (codeGenerationPrompt && selectedWidgetId && promptLoadedForWidgetId === s
12019
13003
  return !!widgetId && widgetId.startsWith(DRAFT_CONVERT_ID_PREFIX);
12020
13004
  }
12021
13005
 
13006
+ /**
13007
+ * True for conversations that exist only in the browser.
13008
+ *
13009
+ * Draft convert panels need a conversation object before the widget has a DB
13010
+ * row, so `handleCreateConversation` issues a `local_*` id. Message load/save
13011
+ * must treat those as local-only rather than querying the API with a key that
13012
+ * cannot exist there.
13013
+ */
13014
+ function isLocalOnlyConversationId(conversationId: string | null | undefined): boolean {
13015
+ return !!conversationId && conversationId.startsWith('local_');
13016
+ }
13017
+
13018
+ /**
13019
+ * Drop synthetic draft ids from the panel registry.
13020
+ *
13021
+ * `migrateOpenPanelWidgetId` renames every map keyed by widget id, but the
13022
+ * `{#each openWidgetPanelIds}` block is keyed by `panelInstanceKeyByWidgetId`,
13023
+ * so a draft id that survives anywhere in that list can re-render a real
13024
+ * widget's panel while `context.id` still points at the draft. That panel then
13025
+ * asks the API for `draft_convert_*` conversations and schemas — 400s and
13026
+ * "Failed to initialize chat" for a widget whose history is perfectly fine.
13027
+ *
13028
+ * Draft panels are only ever opened through the convert flow, which re-creates
13029
+ * them from `draftConvertObjectIds`, so dropping them here is safe.
13030
+ */
13031
+ function purgeDraftConvertPanels(): void {
13032
+ if (!openWidgetPanelIds.some((id) => isDraftConvertId(id))) return;
13033
+
13034
+ const draftIds = openWidgetPanelIds.filter((id) => isDraftConvertId(id));
13035
+ const remainingIds = openWidgetPanelIds.filter((id) => !isDraftConvertId(id));
13036
+ const nextInstanceKeys = { ...panelInstanceKeyByWidgetId };
13037
+ const nextZIndexes = { ...panelZIndexByWidgetId };
13038
+ const nextRefs = { ...widgetDetailsRefs };
13039
+ const nextChatMethods = { ...chatMethodsByWidgetId };
13040
+ for (const draftId of draftIds) {
13041
+ delete nextInstanceKeys[draftId];
13042
+ delete nextZIndexes[draftId];
13043
+ delete nextRefs[draftId];
13044
+ delete nextChatMethods[draftId];
13045
+ }
13046
+ panelInstanceKeyByWidgetId = nextInstanceKeys;
13047
+ panelZIndexByWidgetId = nextZIndexes;
13048
+ widgetDetailsRefs = nextRefs;
13049
+ chatMethodsByWidgetId = nextChatMethods;
13050
+ openWidgetPanelIds = remainingIds;
13051
+
13052
+ if (focusedPanelWidgetId && isDraftConvertId(focusedPanelWidgetId)) {
13053
+ focusedPanelWidgetId = remainingIds.at(-1) ?? null;
13054
+ }
13055
+ if (selectedWidgetId && isDraftConvertId(selectedWidgetId)) {
13056
+ selectedWidgetId = focusedPanelWidgetId;
13057
+ }
13058
+ if (detailsSessionWidgetId && isDraftConvertId(detailsSessionWidgetId)) {
13059
+ detailsSessionWidgetId = null;
13060
+ }
13061
+ if (widgetDetailsOpenTargetId && isDraftConvertId(widgetDetailsOpenTargetId)) {
13062
+ widgetDetailsOpenTargetId = null;
13063
+ }
13064
+ showWidgetDetails = openWidgetPanelIds.length > 0;
13065
+ console.warn('[CreatorApp] Purged draft convert panels:', draftIds.length);
13066
+ }
13067
+
12022
13068
  function isConvertOrDraftPanelOpen(): boolean {
12023
13069
  return (
12024
13070
  isDraftConvertId(focusedPanelWidgetId)
@@ -12457,13 +13503,14 @@ $: if (codeGenerationPrompt && selectedWidgetId && promptLoadedForWidgetId === s
12457
13503
  <!-- Right: Debug toggle + Auth info (2 lines) + Logout -->
12458
13504
  <div class="wc-top-bar-right flex items-center gap-3 shrink-0">
12459
13505
  {#if debugModeAvailable}
13506
+ <HoverTooltip text={debugMode ? 'Debug mode ON — click to disable (hides console tab, widgets panel)' : 'Debug mode OFF — click to enable'}>
12460
13507
  <button
12461
13508
  onclick={toggleDebugMode}
12462
13509
  class="wc-debug-toggle px-2.5 py-1 cursor-pointer text-xs font-medium rounded-lg border transition-colors {debugMode ? 'bg-red-50 border-red-400 text-red-600 hover:bg-red-100' : 'bg-gray-100 border-gray-300 text-gray-500 hover:bg-gray-200'}"
12463
- title={debugMode ? 'Debug mode ON — click to disable (hides console tab, widgets panel)' : 'Debug mode OFF — click to enable'}
12464
13510
  >
12465
13511
  🐛 Debug {debugMode ? 'ON' : 'OFF'}
12466
13512
  </button>
13513
+ </HoverTooltip>
12467
13514
  {/if}
12468
13515
  <div class="wc-auth-info flex flex-col gap-px items-start">
12469
13516
  <div class="wc-auth-line1 flex items-center gap-2">
@@ -12548,9 +13595,11 @@ $: if (codeGenerationPrompt && selectedWidgetId && promptLoadedForWidgetId === s
12548
13595
  onWidgetShapeDuplicated={handleWidgetShapeDuplicated}
12549
13596
  onWidgetShapeDeleting={handleWidgetShapeDeleting}
12550
13597
  onWidgetRename={handleWidgetRename}
12551
- onRefreshScreenshot={handleRefreshScreenshot}
13598
+ onRefreshScreenshot={null}
12552
13599
  onSetPrimaryWidget={handleSetPrimaryWidget}
13600
+ onAfterRender={syncWidgetShapeLivePreviews}
12553
13601
  getWidgetName={getWidgetNameById}
13602
+ widgetNameRevision={widgetNamesRevision}
12554
13603
  isWidgetKnown={isWidgetKnownById}
12555
13604
  onNonWidgetSelected={() => {
12556
13605
  if (suppressWidgetDetailsClose || isConvertOrDraftPanelOpen()) {
@@ -12574,6 +13623,44 @@ $: if (codeGenerationPrompt && selectedWidgetId && promptLoadedForWidgetId === s
12574
13623
  <!-- FLOATING PANELS OVERLAY (pointer-events-none container, each panel is pointer-events-auto) -->
12575
13624
  <div class="floating-panels-overlay absolute inset-0 z-2003 pointer-events-none">
12576
13625
 
13626
+ <!-- W3: live iframe + drag bar on WidgetShapes (iframe interactive; body drag locked) -->
13627
+ <WidgetShapeLivePreviews
13628
+ bind:this={widgetShapeLivePreviewsRef}
13629
+ getRects={() => canvasRef?.listWidgetShapeScreenRects?.() ?? []}
13630
+ resolveUrl={resolveWidgetShapeLivePreviewUrl}
13631
+ resolveTitle={getWidgetNameById}
13632
+ titleRevision={widgetNamesRevision + panelDisplayRevision}
13633
+ resolveVersionLabel={resolveWidgetShapeVersionLabel}
13634
+ resolveVersionOptions={resolveWidgetShapeVersionOptions}
13635
+ resolveVersionSelectedId={resolveWidgetShapeVersionSelectedId}
13636
+ onVersionChange={handleWidgetShapeVersionChange}
13637
+ onSetLive={(widgetId, version) => {
13638
+ void setLiveVersionForWidget(widgetId, version);
13639
+ }}
13640
+ settingLiveVersion={isSettingLiveVersion}
13641
+ resolveCanPublish={(widgetId) => hasUnpublishedChangesForPanel(widgetId)}
13642
+ resolveIsPublishing={(widgetId) => isPublishing && publishingWidgetId === widgetId}
13643
+ onPublish={(widgetId) => runPublishForPanel(widgetId)}
13644
+ selectedWidgetId={canvasSelectedWidgetShapeId}
13645
+ onTranslate={(widgetId, dx, dy) => {
13646
+ canvasRef?.translateWidgetShapeByScreen?.(widgetId, dx, dy);
13647
+ }}
13648
+ onTranslateEnd={(widgetId) => {
13649
+ canvasRef?.commitWidgetShapeTranslate?.(widgetId);
13650
+ }}
13651
+ onSelectShape={(widgetId) => {
13652
+ canvasRef?.selectWidgetShapeById?.(widgetId);
13653
+ canvasSelectedWidgetShapeId = widgetId;
13654
+ void ensureShapePublishHistory(widgetId);
13655
+ }}
13656
+ onOpenChat={(widgetId) => {
13657
+ void openWidgetDetailsPanel(widgetId);
13658
+ }}
13659
+ onRefresh={(widgetId) => {
13660
+ console.log('[WidgetShape] Iframe refreshed', widgetId.substring(0, 8));
13661
+ }}
13662
+ />
13663
+
12577
13664
  <!-- ═══ PANEL 2: Widgets Panel — debug + showWidgetsList (site passes false) ═══ -->
12578
13665
  {#if debugMode && showWidgetsList}
12579
13666
  <!-- svelte-ignore a11y-no-static-element-interactions -->
@@ -13167,9 +14254,32 @@ $: if (codeGenerationPrompt && selectedWidgetId && promptLoadedForWidgetId === s
13167
14254
  hasAutoOpenedPreview = detail.hasAutoOpenedPreview;
13168
14255
  }}
13169
14256
  >
14257
+ <svelte:fragment slot="header-meta">
14258
+ {#if saveStatus === 'saving' && isPanelFocused}
14259
+ <span class="wc-header-save-status text-[10px] px-1.5 py-0.5 rounded bg-blue-100 text-blue-700 animate-pulse">Saving...</span>
14260
+ {:else if saveStatus === 'saved' && isPanelFocused}
14261
+ <span class="wc-header-save-status text-[10px] px-1.5 py-0.5 rounded bg-green-100 text-green-700">Saved</span>
14262
+ {:else if saveStatus === 'error' && isPanelFocused}
14263
+ <span class="wc-header-save-status text-[10px] px-1.5 py-0.5 rounded bg-red-100 text-red-700">Save Error</span>
14264
+ {/if}
14265
+ {#if debugMode}
14266
+ <span class="wc-header-commit text-[10px] text-gray-500 whitespace-nowrap">
14267
+ Commit:
14268
+ <span class="font-mono font-medium text-gray-600" title={ps.lastCommitId || ''}>
14269
+ {ps.lastCommitId ? ps.lastCommitId.substring(0, 8) : '—'}
14270
+ </span>
14271
+ </span>
14272
+ {/if}
14273
+ </svelte:fragment>
13170
14274
  <svelte:fragment slot="version-history">
13171
14275
  {#if ps.lastPublishedVersion !== null}
13172
- <div class="wc-version-history-ct relative shrink-0">
14276
+ {@const shapeSource = shapePreviewSourceByWidgetId[panelWidgetId] || `v:${ps.lastPublishedVersion}`}
14277
+ {@const viewingVersion = shapeSource.startsWith('v:')
14278
+ ? parseInt(shapeSource.slice(2), 10)
14279
+ : ps.lastPublishedVersion}
14280
+ {@const viewingIsLive = Number(viewingVersion) === Number(ps.lastPublishedVersion)}
14281
+ <div class="wc-version-history-ct relative shrink-0 flex items-center gap-0.5">
14282
+ <HoverTooltip text="Live is the version visitors see on your website. Open the list to preview another version, or Set live to publish it.">
13173
14283
  <button
13174
14284
  type="button"
13175
14285
  onclick={(e) => {
@@ -13177,11 +14287,48 @@ $: if (codeGenerationPrompt && selectedWidgetId && promptLoadedForWidgetId === s
13177
14287
  togglePublishHistory(e.currentTarget);
13178
14288
  }}
13179
14289
  onmousedown={(e) => e.stopPropagation()}
13180
- class="wc-version-history-toggle flex items-center gap-1 text-[10px] font-medium px-1.5 py-0.5 rounded border border-emerald-300 bg-emerald-50 text-emerald-800 hover:bg-emerald-100 cursor-pointer whitespace-nowrap"
14290
+ class="wc-version-history-toggle flex items-center gap-0.5 text-[10px] font-semibold tabular-nums px-1.5 py-0.5 rounded border border-emerald-300 bg-emerald-50 text-emerald-800 hover:bg-emerald-100 cursor-pointer whitespace-nowrap"
13181
14291
  >
13182
- Live: v{ps.lastPublishedVersion}
14292
+ v{Number.isFinite(viewingVersion) ? viewingVersion : ps.lastPublishedVersion}
13183
14293
  <span class="text-emerald-500">{isHistoryOpen && isPanelFocused ? '▲' : '▼'}</span>
13184
14294
  </button>
14295
+ </HoverTooltip>
14296
+ {#if panelHasUnpublishedChanges}
14297
+ <button
14298
+ type="button"
14299
+ onclick={(e) => {
14300
+ e.stopPropagation();
14301
+ if (!isPanelFocused) focusWidgetPanel(panelWidgetId);
14302
+ runPublishForPanel(panelWidgetId);
14303
+ }}
14304
+ disabled={isPublishDisabledForPanel(panelWidgetId) || (isPublishing && publishingWidgetId === panelWidgetId)}
14305
+ title={getPublishDisabledReasonForPanel(panelWidgetId) ?? 'Publish the latest generated code as the Live version'}
14306
+ class="wc-version-publish-btn shrink-0 inline-flex items-center justify-center min-w-[4.75rem] text-[10px] font-semibold uppercase tracking-wide px-1.5 py-0.5 rounded border border-emerald-600 bg-emerald-600 text-white hover:bg-emerald-700 disabled:opacity-50 disabled:cursor-not-allowed cursor-pointer"
14307
+ >
14308
+ {isPublishing && publishingWidgetId === panelWidgetId ? '...' : 'Publish'}
14309
+ </button>
14310
+ {:else if viewingIsLive}
14311
+ <span
14312
+ class="wc-version-live-btn shrink-0 inline-flex items-center justify-center min-w-[4.75rem] text-[10px] font-semibold uppercase tracking-wide px-1.5 py-0.5 rounded bg-emerald-100 text-emerald-600 opacity-80"
14313
+ title="Live — generate new code to Publish a new version"
14314
+ >Live</span>
14315
+ {:else}
14316
+ <button
14317
+ type="button"
14318
+ onclick={(e) => {
14319
+ e.stopPropagation();
14320
+ if (!isPanelFocused) focusWidgetPanel(panelWidgetId);
14321
+ if (Number.isFinite(viewingVersion)) {
14322
+ void setLiveVersionForWidget(panelWidgetId, Number(viewingVersion));
14323
+ }
14324
+ }}
14325
+ disabled={isSettingLiveVersion === Number(viewingVersion)}
14326
+ title="Publish this version so visitors see it on your website"
14327
+ class="wc-version-set-live-btn shrink-0 inline-flex items-center justify-center min-w-[4.75rem] text-[10px] font-semibold uppercase tracking-wide px-1.5 py-0.5 rounded border border-emerald-300 text-emerald-700 hover:bg-emerald-100 disabled:opacity-50 disabled:cursor-not-allowed cursor-pointer"
14328
+ >
14329
+ {isSettingLiveVersion === Number(viewingVersion) ? '...' : 'Set live'}
14330
+ </button>
14331
+ {/if}
13185
14332
  {#if isHistoryOpen && isPanelFocused}
13186
14333
  <Portal target="wc-live-history-portal-{panelWidgetId}">
13187
14334
  <button
@@ -13200,38 +14347,49 @@ $: if (codeGenerationPrompt && selectedWidgetId && promptLoadedForWidgetId === s
13200
14347
  <p class="text-xs text-emerald-400 text-center py-2">No history records yet. Publish to start tracking.</p>
13201
14348
  {:else}
13202
14349
  {#each ps.publishHistory as record (record.id)}
13203
- <div class="publish-history-row flex items-center gap-2 rounded border border-emerald-100 bg-white px-2 py-1.5 w-max">
13204
- <span class="text-xs font-mono font-bold text-emerald-700 shrink-0">v{record.version}</span>
14350
+ {@const rowIsViewing = Number(record.version) === Number(viewingVersion)}
14351
+ {@const rowIsLive = Number(record.version) === Number(ps.lastPublishedVersion)}
14352
+ <!-- svelte-ignore a11y_no_static_element_interactions -->
14353
+ <div
14354
+ class="publish-history-row flex items-center gap-2 rounded border px-2 py-1.5 w-max cursor-pointer
14355
+ {rowIsViewing ? 'border-emerald-400 bg-emerald-50' : 'border-emerald-100 bg-white hover:bg-emerald-50/60'}"
14356
+ role="option"
14357
+ aria-selected={rowIsViewing}
14358
+ tabindex="0"
14359
+ onclick={() => {
14360
+ handleWidgetShapeVersionChange(panelWidgetId, `v:${record.version}`);
14361
+ }}
14362
+ onkeydown={(e) => {
14363
+ if (e.key === 'Enter' || e.key === ' ') {
14364
+ e.preventDefault();
14365
+ handleWidgetShapeVersionChange(panelWidgetId, `v:${record.version}`);
14366
+ }
14367
+ }}
14368
+ >
14369
+ <span class="publish-history-version-label text-xs font-mono font-bold text-emerald-700 shrink-0">v{record.version}</span>
13205
14370
  {#if debugMode && (record.commitSha || record.commit_sha)}
13206
14371
  <span class="text-xs font-mono text-slate-400 shrink-0" title={record.commitSha ?? record.commit_sha}>
13207
14372
  {(record.commitSha ?? record.commit_sha).slice(0, 7)}
13208
14373
  </span>
13209
14374
  {/if}
13210
14375
  <span class="wc-live-history-date text-xs text-slate-400 shrink-0 whitespace-nowrap tabular-nums" style="padding-right: 4px;">
13211
- {(() => {
13212
- const d = new Date(record.publishedAt ?? record.published_at ?? '');
13213
- if (isNaN(d.getTime())) return '—';
13214
- const month = String(d.getMonth() + 1).padStart(2, '0');
13215
- const day = String(d.getDate()).padStart(2, '0');
13216
- const timePart = d.toLocaleTimeString(undefined, { hour: '2-digit', minute: '2-digit', hour12: false });
13217
- return `${month}/${day}/${d.getFullYear()} ${timePart}`;
13218
- })()}
14376
+ {formatPublishHistoryDate(record)}
13219
14377
  </span>
13220
- {#if record.version !== ps.lastPublishedVersion}
14378
+ {#if !rowIsLive}
13221
14379
  <button
13222
- onclick={() => { void setLiveVersion(record.version); }}
14380
+ type="button"
14381
+ onclick={(e) => {
14382
+ e.stopPropagation();
14383
+ void setLiveVersionForWidget(panelWidgetId, record.version);
14384
+ }}
13223
14385
  disabled={isSettingLiveVersion === record.version}
13224
14386
  class="shrink-0 inline-flex items-center justify-center min-w-[4.75rem] text-xs px-1.5 py-0.5 rounded border border-emerald-300 text-emerald-700 hover:bg-emerald-100 disabled:opacity-50 disabled:cursor-not-allowed transition-colors"
13225
- title="Set this version as the live published widget"
14387
+ title="Publish this version so visitors see it on your website"
13226
14388
  >
13227
14389
  {isSettingLiveVersion === record.version ? '...' : 'Set live'}
13228
14390
  </button>
13229
14391
  {:else}
13230
- <button
13231
- type="button"
13232
- class="shrink-0 inline-flex items-center justify-center min-w-[4.75rem] text-xs px-1.5 py-0.5 rounded bg-emerald-100 text-emerald-600 font-medium cursor-pointer"
13233
- onclick={() => { isHistoryOpen = false; }}
13234
- >live</button>
14392
+ <span class="shrink-0 inline-flex items-center justify-center min-w-[4.75rem] text-xs px-1.5 py-0.5 rounded bg-emerald-100 text-emerald-600 font-medium">live</span>
13235
14393
  {/if}
13236
14394
  </div>
13237
14395
  {/each}
@@ -13269,7 +14427,7 @@ $: if (codeGenerationPrompt && selectedWidgetId && promptLoadedForWidgetId === s
13269
14427
  </svelte:fragment>
13270
14428
 
13271
14429
  <svelte:fragment slot="code-generation-status">
13272
- <div class="generate-code-status-ct flex flex-col gap-2 shrink-0" style="padding: 10px; margin: 0px;">
14430
+ <div class="generate-code-status-ct flex flex-col gap-2 shrink-0 border-b border-gray-200 bg-gray-50" style="padding: 10px; margin: 0px; border-radius: 0;">
13273
14431
  <div class="flex items-center justify-between gap-2 min-w-0">
13274
14432
  <div class="generate-code-status-el text-xs flex-1 min-w-0 truncate {ps.lastGenerationFailed ? 'text-red-600' : 'text-gray-500'}">
13275
14433
  {#if ps.lastGenerationFailed}
@@ -13280,7 +14438,7 @@ $: if (codeGenerationPrompt && selectedWidgetId && promptLoadedForWidgetId === s
13280
14438
  </div>
13281
14439
  </div>
13282
14440
  {#if ps.lastGenerationFailed}
13283
- <div class="p-3 bg-red-50 border border-red-200 rounded-lg">
14441
+ <div class="p-3 bg-red-50 border border-red-200" style="border-radius: 0;">
13284
14442
  <p class="text-xs text-red-700 whitespace-pre-wrap">{ps.lastGenerationError}</p>
13285
14443
  <div class="flex gap-2 mt-3 justify-center">
13286
14444
  <Button
@@ -13318,20 +14476,22 @@ $: if (codeGenerationPrompt && selectedWidgetId && promptLoadedForWidgetId === s
13318
14476
 
13319
14477
  <svelte:fragment slot="code-generation">
13320
14478
  <!-- svelte-ignore a11y_no_static_element_interactions -->
13321
- {@const chatUserHeight = (panelDisplayRevision, Math.max(CHAT_MIN_HEIGHT, Math.min(CHAT_MAX_HEIGHT, (ps.chatContainerHeight > 0 ? ps.chatContainerHeight : CHAT_DEFAULT_HEIGHT))))}
14479
+ {@const chatUserHeight = (panelDisplayRevision, getChatContainerHeightForPanel(panelWidgetId))}
13322
14480
  <div
13323
14481
  class="generation-scroll-ct flex flex-col h-full min-h-0 overflow-y-auto border-t border-gray-200"
13324
- style="padding: 10px;"
13325
- use:nativeScrollbarPadding={10}
14482
+ style="padding: 0;"
14483
+ use:nativeScrollbarPadding={0}
13326
14484
  onmousedown={() => { if (!isPanelFocused) focusWidgetPanel(panelWidgetId); }}
13327
14485
  >
13328
14486
  {#key panelInstanceKey}
13329
14487
  <div
13330
- class="code-generation-chat-host w-full min-h-0"
14488
+ class="code-generation-chat-host w-full min-h-0 {chatUserHeight == null ? 'flex-1' : 'shrink-0'}"
13331
14489
  data-widget-chat-height={panelWidgetId}
13332
- style="flex: 1 0 {chatUserHeight}px; min-height: {CHAT_MIN_HEIGHT}px; height: {chatUserHeight}px;"
14490
+ style={chatUserHeight == null
14491
+ ? `flex: 1 1 auto; min-height: ${CHAT_MIN_HEIGHT}px;`
14492
+ : `flex: 0 0 ${chatUserHeight}px; min-height: ${CHAT_MIN_HEIGHT}px; height: ${chatUserHeight}px;`}
13333
14493
  >
13334
- <Chat class="code-generation-chat w-full h-full"
14494
+ <Chat class="code-generation-chat w-full h-full rounded-none border-0"
13335
14495
  style="height: 100%; min-height: {CHAT_MIN_HEIGHT}px;"
13336
14496
  resizeExpandsContainer={true}
13337
14497
  onMessagesResize={(deltaPx) => handleChatMessagesResize(panelWidgetId, deltaPx)}
@@ -13359,7 +14519,7 @@ $: if (codeGenerationPrompt && selectedWidgetId && promptLoadedForWidgetId === s
13359
14519
  autoCodeDetection: false,
13360
14520
  }
13361
14521
  }}
13362
- widgetHeadCommitSha={ps.lastCommitId}
14522
+ widgetHeadCommitSha={resolveViewingCommitShaForPanel(panelWidgetId) || ps.lastCommitId}
13363
14523
  onSendMessage={(content, attachments) => handleChatSendMessage(content, attachments, panelWidgetId)}
13364
14524
  onRetryMessage={(content) => handleChatRetryMessage(content, panelWidgetId)}
13365
14525
  onUploadChatFile={handleChatUploadFile}
@@ -13372,7 +14532,9 @@ $: if (codeGenerationPrompt && selectedWidgetId && promptLoadedForWidgetId === s
13372
14532
  onSaveMessage={(conversationId, content, messageType, attachments) =>
13373
14533
  handleSaveMessage(conversationId, content, messageType, attachments, panelWidgetId)
13374
14534
  }
13375
- onRestoreCheckpointBackend={handleRestoreCheckpoint}
14535
+ onRestoreCheckpointBackend={(conversationId, checkpointMessageId) =>
14536
+ handleRestoreCheckpoint(conversationId, checkpointMessageId, panelWidgetId)
14537
+ }
13376
14538
  onChatReady={(methods) => handleChatReady(methods, panelWidgetId)}
13377
14539
  onDraftChange={() => handleChatDraftChange(panelWidgetId)}
13378
14540
  onConversationChange={(conversationId) => handleChatConversationChange(conversationId, panelWidgetId)}
@@ -13434,7 +14596,7 @@ $: if (codeGenerationPrompt && selectedWidgetId && promptLoadedForWidgetId === s
13434
14596
  {#if isEditStepWithoutPublishForPanel(panelWidgetId)}
13435
14597
  <div class="edit-not-published-banner-ct mx-3 mt-2 px-3 py-2 rounded-md border border-amber-200 bg-amber-50 text-amber-800 text-xs">
13436
14598
  <strong class="font-semibold">Not published yet.</strong>
13437
- Generate on Create — the widget publishes automatically so you can edit the composition.
14599
+ Generate on Create, preview the result, then Publish when you are ready.
13438
14600
  </div>
13439
14601
  {/if}
13440
14602
 
@@ -13924,11 +15086,46 @@ $: if (codeGenerationPrompt && selectedWidgetId && promptLoadedForWidgetId === s
13924
15086
  Are you sure you want to delete <strong>"{deleteCanvasTarget?.name || ''}"</strong>? This action cannot be undone.
13925
15087
  </Dialog.Description>
13926
15088
  </Dialog.Header>
15089
+
15090
+ {#if deleteCanvasPreviewLoading}
15091
+ <p class="confirm-delete-canvas-preview-loading text-sm text-gray-500 pt-2">
15092
+ Checking what will be deleted…
15093
+ </p>
15094
+ {:else if deleteCanvasPreview && deleteCanvasPreview.widgetCount > 0}
15095
+ <div class="confirm-delete-canvas-preview pt-2">
15096
+ <p class="text-sm text-gray-700">
15097
+ This will also delete
15098
+ <strong>{deleteCanvasPreview.widgetCount}</strong>
15099
+ {deleteCanvasPreview.widgetCount === 1 ? 'widget' : 'widgets'} on this canvas:
15100
+ </p>
15101
+ <ul class="confirm-delete-canvas-preview-groups mt-2 space-y-1.5 max-h-40 overflow-y-auto scrollbar-thin text-sm">
15102
+ {#each deleteCanvasPreview.groups as group}
15103
+ <li class="confirm-delete-canvas-preview-group">
15104
+ <span class="font-medium text-gray-800">{group.projectName}</span>
15105
+ <span class="text-gray-500"> — {group.widgetNames.length}</span>
15106
+ <p class="text-gray-500 text-xs truncate" title={group.widgetNames.join(', ')}>
15107
+ {group.widgetNames.join(', ')}
15108
+ </p>
15109
+ </li>
15110
+ {/each}
15111
+ </ul>
15112
+ {#if deleteCanvasPreview.groups.length > 1}
15113
+ <p class="confirm-delete-canvas-preview-warning text-xs text-amber-700 mt-2">
15114
+ These widgets belong to more than one project.
15115
+ </p>
15116
+ {/if}
15117
+ </div>
15118
+ {/if}
15119
+
15120
+ {#if deleteCanvasError}
15121
+ <p class="confirm-delete-canvas-error text-sm text-red-600 pt-2">{deleteCanvasError}</p>
15122
+ {/if}
15123
+
13927
15124
  <div class="confirm-delete-canvas-dialog-actions flex justify-end gap-2 pt-2">
13928
15125
  <button
13929
15126
  type="button"
13930
15127
  class="px-4 py-2 text-sm rounded-lg border border-gray-300 hover:bg-gray-100 cursor-pointer"
13931
- onclick={() => { showDeleteCanvasDialog = false; deleteCanvasTarget = null; }}
15128
+ onclick={() => { showDeleteCanvasDialog = false; deleteCanvasTarget = null; deleteCanvasError = ''; }}
13932
15129
  >Cancel</button>
13933
15130
  <button
13934
15131
  type="button"