@widgetic/creator 0.3.59 → 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;
@@ -448,6 +464,7 @@
448
464
  import SelectorDropdown from './components/SelectorDropdown.svelte';
449
465
  import type { DropdownItem } from './components/SelectorDropdown.svelte';
450
466
  import WidgetShapeLivePreviews from './components/WidgetShapeLivePreviews.svelte';
467
+ import HoverTooltip from './components/HoverTooltip.svelte';
451
468
 
452
469
  // WIDGET DETAILS COMPONENT — lazy-loaded to reduce peak build memory
453
470
  let WidgetDetails: any;
@@ -558,6 +575,18 @@
558
575
  // Delete canvas dialog
559
576
  let showDeleteCanvasDialog = false;
560
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 = '';
561
590
 
562
591
  // Widgets state
563
592
  interface WidgetSummary {
@@ -890,7 +919,10 @@
890
919
  contextType: options?.source === 'preview-screenshot' ? 'browser-ic' : 'widget-codegen',
891
920
  source: options?.source || 'chat-upload',
892
921
  widgetId: persistWidgetId && !isDraftConvertId(persistWidgetId) ? persistWidgetId : undefined,
893
- conversationId: currentChatConversationId || undefined,
922
+ conversationId:
923
+ currentChatConversationId && !isLocalOnlyConversationId(currentChatConversationId)
924
+ ? currentChatConversationId
925
+ : undefined,
894
926
  },
895
927
  { onProgress: options?.onProgress },
896
928
  );
@@ -1485,40 +1517,164 @@
1485
1517
  // console.log('[WidgetCreator] Cleared prompt draft for widget:', widgetId.substring(0, 8));
1486
1518
  }
1487
1519
 
1488
- /** Screenshot: capture this panel's widget preview iframe and return data URL */
1489
- 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 {
1490
1522
  const targetId = widgetId || focusedPanelWidgetId || selectedWidgetId;
1491
- const panelRef = getWidgetDetailsRef(targetId);
1492
- const iframe =
1493
- (panelRef?.getPreviewIframe?.() as HTMLIFrameElement | null) ||
1494
- (document.querySelector('iframe[title="Widget preview"]') as HTMLIFrameElement | null);
1495
-
1496
- if (!iframe?.contentWindow) {
1497
- // console.log('[Chat→Screenshot] Preview iframe not ready', {
1498
- // targetId: targetId?.substring(0, 8),
1499
- // hasValidPreview: panelRef?.hasValidPreview?.() ?? false,
1500
- // });
1501
- 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);
1502
1543
  }
1544
+ }
1503
1545
 
1504
- // console.log('[Chat→Screenshot] Requesting screenshot from preview iframe', targetId?.substring(0, 8));
1505
- 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
+ }
1506
1555
  const timeout = setTimeout(() => {
1507
1556
  window.removeEventListener('message', handler);
1508
- console.warn('[Chat→Screenshot] Timed out waiting for widgetic:screenshot');
1509
- resolve(null);
1510
- }, 8000);
1557
+ resolve({ data: null, error: 'timeout' });
1558
+ }, timeoutMs);
1511
1559
 
1512
1560
  function handler(event: MessageEvent) {
1513
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;
1514
1564
  clearTimeout(timeout);
1515
1565
  window.removeEventListener('message', handler);
1516
- 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
+ });
1517
1570
  }
1518
1571
  window.addEventListener('message', handler);
1519
- iframe.contentWindow!.postMessage({ type: 'widgetic:request-screenshot' }, '*');
1572
+ iframe.contentWindow.postMessage({ type: 'widgetic:request-screenshot' }, '*');
1520
1573
  });
1521
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),
1675
+ });
1676
+ return null;
1677
+ }
1522
1678
 
1523
1679
  function normalizeConversationRecords(json: any): any[] {
1524
1680
  const raw = json?.data ?? json?.conversations ?? json;
@@ -1606,6 +1762,8 @@
1606
1762
  /** Load messages for a conversation from backend */
1607
1763
  async function handleLoadMessages(conversationId: string, widgetIdForSync?: string | null): Promise<any[]> {
1608
1764
  try {
1765
+ // Client-only conversations (draft convert panels) have no DB row.
1766
+ if (isLocalOnlyConversationId(conversationId)) return [];
1609
1767
  if (!messagesClient) {
1610
1768
  console.warn('[Chat→Backend] messagesClient not initialized');
1611
1769
  return [];
@@ -1669,7 +1827,17 @@
1669
1827
  try {
1670
1828
  if (!conversationsClient) {
1671
1829
  console.warn('[Chat→Backend] conversationsClient not initialized');
1672
- 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 };
1673
1841
  }
1674
1842
  const response = await conversationsClient.createConversationForWidgetRaw({
1675
1843
  createConversationForWidgetRequest: {
@@ -1693,7 +1861,8 @@
1693
1861
  return conversationId ? { ...conversation, id: conversationId } : conversation;
1694
1862
  } catch (error) {
1695
1863
  console.error('[Chat→Backend] Failed to create conversation:', error);
1696
- return { id: crypto.randomUUID(), title };
1864
+ // Never invent a local UUID — saving messages against it yields 403 and stops codegen.
1865
+ return null;
1697
1866
  }
1698
1867
  }
1699
1868
 
@@ -1810,6 +1979,11 @@
1810
1979
  persistWidgetId?: string | null,
1811
1980
  ): Promise<any> {
1812
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
+ }
1813
1987
  if (!messagesClient) {
1814
1988
  console.warn('[Chat→Backend] messagesClient not initialized');
1815
1989
  return null;
@@ -1865,7 +2039,11 @@
1865
2039
  }
1866
2040
 
1867
2041
  /** Restore conversation to a checkpoint message via backend (deletes messages + git rollback) */
1868
- 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> {
1869
2047
  try {
1870
2048
  if (!messagesClient) {
1871
2049
  console.warn('[Chat→Backend] messagesClient not initialized');
@@ -1882,16 +2060,22 @@
1882
2060
  const data = json?.data || json;
1883
2061
  // console.log('[Chat→Backend] Checkpoint restored:', data);
1884
2062
 
1885
- // Refresh the widget preview after restore — the backend rebuilds the widget
1886
- // from the reverted code, so we just need to reload the iframe.
1887
- // Check both camelCase (after responseTransformMiddleware) and snake_case paths.
1888
2063
  const gitRollback = data?.gitRollback || data?.git_rollback || data?.data?.gitRollback || data?.data?.git_rollback;
1889
2064
  const undoMode = data?.undoMode ?? data?.undo_mode ?? false;
1890
2065
  const targetCommit = gitRollback?.targetCommit ?? gitRollback?.target_commit;
1891
- // 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 });
1892
2072
  if (gitRollback?.success) {
1893
2073
  if (targetCommit) {
1894
2074
  lastCommitId = targetCommit;
2075
+ if (panelWidgetId) {
2076
+ patchPanelSnapshot(panelWidgetId, { lastCommitId: targetCommit });
2077
+ patchCodegenUiState(panelWidgetId, {});
2078
+ }
1895
2079
  }
1896
2080
  showToast('success', undoMode ? 'Change undone' : 'Checkpoint restored', {
1897
2081
  description: undoMode
@@ -1905,20 +2089,27 @@
1905
2089
  compositionDesignSchema = null;
1906
2090
  compositionContentSchema = null;
1907
2091
  pendingCreatePreviewDefaultsWidgetId = null;
1908
- // Rollback commit may take a moment to be visible from the Worker's clone; retry
1909
- // build-from-repo with escalating delays to survive GitLab propagation lag.
1910
2092
  _isRestoringCheckpoint = true;
2093
+ const panelRef = panelWidgetId ? getWidgetDetailsRef(panelWidgetId) : widgetDetails;
1911
2094
  const scheduleBuildRetry = async () => {
1912
2095
  const delays = [2500, 5000, 8000];
1913
2096
  for (let i = 0; i < delays.length; i++) {
1914
2097
  await new Promise((r) => setTimeout(r, delays[i]));
1915
2098
  if (i === 0) suppressNextBuildFromRepoChatMessage = true;
1916
2099
  try {
1917
- await widgetDetails?.triggerBuildFromRepo({ force: true });
1918
- const err = widgetDetails?.getPreviewError?.() ?? null;
2100
+ const activeRef = panelWidgetId ? getWidgetDetailsRef(panelWidgetId) : panelRef;
2101
+ await activeRef?.triggerBuildFromRepo({ force: true });
2102
+ const err = activeRef?.getPreviewError?.() ?? null;
1919
2103
  if (!err) {
1920
- // console.log('[Restore] triggerBuildFromRepo succeeded on attempt', i + 1);
1921
2104
  _isRestoringCheckpoint = false;
2105
+ if (panelWidgetId) {
2106
+ shapePreviewSourceByWidgetId = {
2107
+ ...shapePreviewSourceByWidgetId,
2108
+ [panelWidgetId]: 'preview',
2109
+ };
2110
+ syncWidgetShapeLivePreviews();
2111
+ setTimeout(() => handleRefreshScreenshot(panelWidgetId, { quiet: true }), 2000);
2112
+ }
1922
2113
  return;
1923
2114
  }
1924
2115
  console.warn('[Restore] triggerBuildFromRepo attempt', i + 1, 'has error:', err);
@@ -1941,14 +2132,16 @@
1941
2132
  pendingCreatePreviewDefaultsWidgetId = null;
1942
2133
  setTimeout(() => {
1943
2134
  suppressNextBuildFromRepoChatMessage = true;
1944
- widgetDetails?.triggerBuildFromRepo({ force: true });
2135
+ const activeRef = panelWidgetId ? getWidgetDetailsRef(panelWidgetId) : widgetDetails;
2136
+ void activeRef?.triggerBuildFromRepo({ force: true });
1945
2137
  }, 1500);
1946
2138
  }
1947
-
1948
2139
  return data;
1949
2140
  } catch (error) {
1950
2141
  console.error('[Chat→Backend] Failed to restore checkpoint:', error);
1951
- _isRestoringCheckpoint = false;
2142
+ showToast('error', 'Failed to restore checkpoint', {
2143
+ description: error instanceof Error ? error.message : 'Unknown error',
2144
+ });
1952
2145
  return null;
1953
2146
  }
1954
2147
  }
@@ -2123,14 +2316,15 @@
2123
2316
  let editingCompositionName = '';
2124
2317
  let deletingCompositionId: string | null = null;
2125
2318
 
2126
- /** Explicit chat box height (px). Drag grows this and pushes Publish down. */
2127
- const CHAT_DEFAULT_HEIGHT = 550;
2128
- 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;
2129
2322
  const CHAT_MAX_HEIGHT = 2000;
2130
2323
 
2131
- function getChatContainerHeightForPanel(widgetId: string): number {
2324
+ function getChatContainerHeightForPanel(widgetId: string): number | null {
2132
2325
  const stored = getPanelState(widgetId).chatContainerHeight;
2133
- 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;
2134
2328
  return Math.max(CHAT_MIN_HEIGHT, Math.min(CHAT_MAX_HEIGHT, stored));
2135
2329
  }
2136
2330
 
@@ -2138,9 +2332,12 @@
2138
2332
  const measured = document
2139
2333
  .querySelector(`[data-widget-chat-height="${widgetId}"]`)
2140
2334
  ?.getBoundingClientRect().height;
2141
- const current = measured && measured > 0 ? measured : getChatContainerHeightForPanel(widgetId);
2335
+ const current =
2336
+ measured && measured > 0
2337
+ ? measured
2338
+ : (getChatContainerHeightForPanel(widgetId) ?? CHAT_DEFAULT_HEIGHT);
2142
2339
  const next = Math.max(CHAT_MIN_HEIGHT, Math.min(CHAT_MAX_HEIGHT, current + deltaPx));
2143
- // 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);
2144
2341
  patchPanelSnapshot(widgetId, { chatContainerHeight: next });
2145
2342
  }
2146
2343
 
@@ -2293,6 +2490,11 @@
2293
2490
  }
2294
2491
 
2295
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();
2296
2498
  const cached = widgetSchemaCache.get(widgetId);
2297
2499
  if (cached?.design || cached?.content) {
2298
2500
  applyWidgetSchemaCacheToSnapshot(widgetId);
@@ -2462,6 +2664,9 @@
2462
2664
  }
2463
2665
 
2464
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;
2465
2670
  schemaLoadInFlight.add(widgetId);
2466
2671
 
2467
2672
  try {
@@ -2760,7 +2965,6 @@
2760
2965
  // Only reuse js_path when it already points at this widget + this version.
2761
2966
  // `/widgets/` alone used to win and keep Edit on the previous CDN version after publish.
2762
2967
  if (!pointsAtOtherWidget && jsPathVersion === version) {
2763
- console.log('[Publish] Using js_path artifact URL', resolved);
2764
2968
  return resolved;
2765
2969
  }
2766
2970
  }
@@ -3883,63 +4087,162 @@
3883
4087
  }
3884
4088
 
3885
4089
  function handleHostSaveCanvasEvent(): void {
3886
- 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 });
3887
4098
  }
3888
4099
 
3889
- async function persistPrimaryWidgetId(widgetId: string) {
3890
- 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
+ }
3891
4130
  canvasPrimaryWidgetId = widgetId;
4131
+ saveSelectedWidgetId(widgetId);
3892
4132
  // console.log('[CreatorApp] Set primary widget:', widgetId);
3893
- if (canvasId && canvasesClient) {
4133
+ if (targetCanvasId && canvasesClient) {
3894
4134
  try {
3895
4135
  await canvasesClient.updateCanvas({
3896
- id: canvasId,
4136
+ id: targetCanvasId,
3897
4137
  updateCanvasRequest: { primaryWidgetId: widgetId }
3898
4138
  });
3899
4139
  } catch (err) {
3900
4140
  console.warn('[CreatorApp] Failed to save primary_widget_id:', err);
3901
4141
  }
3902
4142
  }
3903
- const widget = getWidgetById(widgetId);
3904
- if (embedded && typeof window !== 'undefined') {
4143
+ // Adopt orphans AND cross-canvas strays whose shape lives here (explicit user intent).
4144
+ if (targetCanvasId && widgetsClient && shouldAdoptToThisCanvas) {
3905
4145
  try {
3906
- const url = new URL(window.location.href);
3907
- if (url.searchParams.get('widget_id') !== widgetId) {
3908
- url.searchParams.set('widget_id', widgetId);
3909
- window.history.replaceState({}, '', url.pathname + url.search + url.hash);
3910
- }
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
+ );
3911
4161
  } catch (err) {
3912
- console.warn('[CreatorApp] Failed to sync primary widget_id in URL:', err);
4162
+ console.warn('[CreatorApp] Failed to heal widget canvas_id:', err);
3913
4163
  }
3914
4164
  }
4165
+ if (embedded) {
4166
+ syncUrlWidgetId(widgetId);
4167
+ }
3915
4168
  if (widget) notifyHostWidgetName(widget);
3916
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;
3917
4178
  }
3918
4179
 
3919
4180
  function emitCanvasWidgetsList(reason: string) {
3920
4181
  if (!embedded || typeof window === 'undefined') return;
4182
+ const hostCanvasId = resolveHostCanvasId();
3921
4183
  const ids = canvasRef?.getLinkedWidgetIds?.() ?? linkedWidgetIds ?? new Set<string>();
3922
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.
3923
4205
  for (const id of ids) {
4206
+ if (!id || isDraftConvertId(id)) continue;
3924
4207
  const widget = getWidgetById(id);
3925
- items.push({
3926
- id,
3927
- name: (widget?.name || '').trim() || 'Untitled Widget'
3928
- });
3929
- }
3930
- if (canvasPrimaryWidgetId && !items.some((item) => item.id === canvasPrimaryWidgetId)) {
3931
- const primary = getWidgetById(canvasPrimaryWidgetId);
3932
- 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) {
3933
4223
  items.unshift({
3934
4224
  id: primary.id,
3935
4225
  name: (primary.name || '').trim() || 'Untitled Widget'
3936
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);
3937
4238
  }
3938
4239
  }
3939
- const primaryWidgetId = canvasPrimaryWidgetId || '';
4240
+ if (!primaryWidgetId && items[0]?.id) {
4241
+ primaryWidgetId = items[0].id;
4242
+ }
3940
4243
  // console.log('[CreatorApp] Emit canvas widgets for header', reason, items.length, primaryWidgetId);
3941
4244
  window.dispatchEvent(new CustomEvent(CREATOR_CANVAS_WIDGETS_EVENT, {
3942
- detail: { widgets: items, primaryWidgetId }
4245
+ detail: { widgets: items, primaryWidgetId: primaryWidgetId || '' }
3943
4246
  }));
3944
4247
  }
3945
4248
 
@@ -3953,10 +4256,19 @@
3953
4256
  console.warn('[CreatorApp] Host set primary ignored — missing widgetId');
3954
4257
  return;
3955
4258
  }
4259
+ const previousPrimary = canvasPrimaryWidgetId;
4260
+ // Optimistic — so host-request emits during persist cannot resurrect the previous primary.
4261
+ canvasPrimaryWidgetId = widgetId;
4262
+ saveSelectedWidgetId(widgetId);
3956
4263
  // console.log('[CreatorApp] Host set primary widget', widgetId.substring(0, 8));
3957
4264
  canvasRef?.setPrimaryWidgetShape?.(widgetId);
3958
4265
  canvasRef?.selectWidgetShape?.(widgetId);
3959
- 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
+ });
3960
4272
  }
3961
4273
 
3962
4274
  function handleSetPrimaryWidget(widgetId: string) {
@@ -3966,7 +4278,33 @@
3966
4278
  async function hydrateCanvasPrimaryWidget(linkedIds?: string[]): Promise<string | null> {
3967
4279
  const targetCanvasId = (typeof canvasId === 'string' && canvasId.trim()) ? canvasId.trim() : null;
3968
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
+
3969
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
+ }
3970
4308
  if (!primaryId) {
3971
4309
  const fromLinks = (linkedIds || []).filter((id) => id && !isDraftConvertId(id) && !String(id).startsWith('draft'));
3972
4310
  const fromList = widgets
@@ -3975,7 +4313,11 @@
3975
4313
  return cid === targetCanvasId && w.id && !isDraftConvertId(w.id);
3976
4314
  })
3977
4315
  .map((w: any) => w.id);
3978
- 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;
3979
4321
  if (primaryId) {
3980
4322
  // console.log('[CreatorApp] Healing empty primary_widget_id →', primaryId.substring(0, 8));
3981
4323
  await persistPrimaryWidgetId(primaryId);
@@ -3989,6 +4331,12 @@
3989
4331
  return primaryId || canvasPrimaryWidgetId || null;
3990
4332
  }
3991
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
+
3992
4340
  async function handleHostMoveCanvasFolderEvent(event: Event) {
3993
4341
  const folderId = (event as CustomEvent).detail?.folderId;
3994
4342
  if (!folderId || typeof folderId !== 'string' || !widgetsClient) return;
@@ -4441,12 +4789,46 @@
4441
4789
  }, delayMs);
4442
4790
  }
4443
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
+
4444
4819
  async function loadCurrentCanvas() {
4445
4820
  // console.log('Widget Creator: loadCurrentCanvas():', routeCanvasId, isLoadingCanvasOnce);
4446
4821
  if (isLoadingCanvasOnce) return;
4447
4822
  try {
4448
4823
  isLoadingCanvasOnce = true;
4449
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
+
4450
4832
  // set widgets loading state
4451
4833
  isLoadingWidgets = true;
4452
4834
  widgetsError = null;
@@ -4479,6 +4861,16 @@
4479
4861
  // Prefer canvasId from route (path or query)
4480
4862
  if (routeCanvasId) {
4481
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;
4482
4874
  // console.log('WidgetCreator.loadCurrentCanvas: Loading data for route canvasId:', canvasId);
4483
4875
  }
4484
4876
  // Fallback: get latest canvas from current user's canvases
@@ -4582,10 +4974,16 @@
4582
4974
  const { loaded, widgetIds: widgetIdsOnCanvas } = await parallelCanvasLoadPromise;
4583
4975
  await maybeAutoRestoreWidgetShapes();
4584
4976
  backfillWidgetShapeMetadata();
4585
- const primaryId = await hydrateCanvasPrimaryWidget(widgetIdsOnCanvas);
4586
- if (loaded && canvasRef && primaryId && !isConvertOrDraftPanelOpen()) {
4587
- canvasRef.setPrimaryWidgetShape?.(primaryId);
4588
- 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
+ });
4589
4987
  }
4590
4988
  return;
4591
4989
  }
@@ -4602,10 +5000,16 @@
4602
5000
  await syncWidgetsWithCanvas(widgetIdsOnCanvas);
4603
5001
  await maybeAutoRestoreWidgetShapes();
4604
5002
  backfillWidgetShapeMetadata();
4605
- const primaryId = await hydrateCanvasPrimaryWidget(widgetIdsOnCanvas);
4606
- if (loaded && canvasRef && primaryId && !isConvertOrDraftPanelOpen()) {
4607
- canvasRef.setPrimaryWidgetShape?.(primaryId);
4608
- 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
+ });
4609
5013
  }
4610
5014
  } catch (error) {
4611
5015
  console.error('Widget Creator: Load canvases error:', error);
@@ -4675,6 +5079,11 @@
4675
5079
  sessionCanvasId = entry.id;
4676
5080
  activeCanvasName = entry.name;
4677
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);
4678
5087
 
4679
5088
  // Clear existing widgets and canvas visually
4680
5089
  widgets = [];
@@ -4692,6 +5101,21 @@
4692
5101
  }
4693
5102
  }
4694
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
+
4695
5119
  /** Close canvas dropdown on outside click */
4696
5120
  function handleCanvasDropdownOutsideClick(e: MouseEvent) {
4697
5121
  const target = e.target as HTMLElement;
@@ -4878,7 +5302,15 @@
4878
5302
  }
4879
5303
 
4880
5304
  try {
4881
- 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
+ }
4882
5314
  const createdRaw = await canvasesClient.createCanvas({
4883
5315
  createCanvasRequest: { persistenceKey: newName }
4884
5316
  });
@@ -4991,16 +5423,51 @@
4991
5423
  deleteCanvasTarget = entry;
4992
5424
  showDeleteCanvasDialog = true;
4993
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
+ }
4994
5460
  }
4995
5461
 
4996
5462
  async function handleDeleteCanvasConfirm() {
4997
5463
  if (!deleteCanvasTarget || !canvasesClient) return;
4998
5464
  const targetId = deleteCanvasTarget.id;
4999
5465
  const targetName = deleteCanvasTarget.name;
5000
- showDeleteCanvasDialog = false;
5466
+ deleteCanvasError = '';
5001
5467
  try {
5002
5468
  await canvasesClient.deleteCanvas({ id: targetId });
5003
5469
  // console.log('[WidgetCreator] Canvas deleted:', targetId, targetName);
5470
+ showDeleteCanvasDialog = false;
5004
5471
  userCanvases = userCanvases.filter(c => c.id !== targetId);
5005
5472
  if (canvasId === targetId) {
5006
5473
  const next = userCanvases[0] || null;
@@ -5016,6 +5483,10 @@
5016
5483
  }
5017
5484
  } catch (err) {
5018
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;
5019
5490
  }
5020
5491
  deleteCanvasTarget = null;
5021
5492
  }
@@ -5085,15 +5556,39 @@
5085
5556
  // console.log('Widgets for canvas:', canvasId, ':', widgets);
5086
5557
  syncOrphanedWidgetShapes();
5087
5558
 
5088
- // 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.
5089
5562
  if (embedded && forceCreateNewWidget && !initialWidgetId) {
5090
5563
  // console.log('[WidgetCreator] Skipping auto-select of existing widgets (new widget from site)');
5091
5564
  } else if (widgets.length > 0) {
5092
- const deepLinkedWidget = initialWidgetId
5093
- ? 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)
5094
5571
  : null;
5095
- const savedWidgetId = getSavedSelectedWidgetId();
5096
- 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;
5097
5592
  const widgetToSelect = deepLinkedWidget ?? savedWidget ?? widgets[0];
5098
5593
  if (deepLinkedWidget) {
5099
5594
  // console.log('[WidgetCreator] Selecting widget from embedded deep-link:', deepLinkedWidget.name);
@@ -5360,6 +5855,17 @@
5360
5855
 
5361
5856
  if (missingIds.length === 0) return;
5362
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
+
5363
5869
  try {
5364
5870
  // console.log(
5365
5871
  // '[WidgetCreator] Merging',
@@ -5370,8 +5876,21 @@
5370
5876
  getWidgetsBatchRequest: { widgetIds: missingIds }
5371
5877
  });
5372
5878
  const batch = response as { widgets?: WidgetSummary[]; data?: { widgets?: WidgetSummary[] } };
5373
- const fetched = batch?.widgets ?? batch?.data?.widgets ?? [];
5374
- 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;
5375
5894
 
5376
5895
  const previousById = new Map(
5377
5896
  widgets.filter((w) => w.id).map((w) => [w.id as string, w]),
@@ -5393,9 +5912,26 @@
5393
5912
  widgets.length > 0 &&
5394
5913
  !(embedded && forceCreateNewWidget && !initialWidgetId)
5395
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;
5396
5923
  const savedWidgetId = getSavedSelectedWidgetId();
5397
5924
  const savedWidget = savedWidgetId ? getWidgetById(savedWidgetId) : null;
5398
- 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
+ );
5399
5935
  }
5400
5936
  } catch (err) {
5401
5937
  console.warn('[WidgetCreator] Failed to merge widgets linked on canvas:', err);
@@ -5404,8 +5940,8 @@
5404
5940
 
5405
5941
  /**
5406
5942
  * Site embeds CreatorApp once; `initialWidgetId` props stay frozen in prod.
5407
- * Always prefer the live `?widget_id=` from the address bar so deep-link
5408
- * 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.
5409
5945
  */
5410
5946
  function resolveEmbeddedDeepLinkWidgetId(): string | null {
5411
5947
  if (typeof window !== 'undefined') {
@@ -5419,6 +5955,84 @@
5419
5955
  return initialWidgetId ?? null;
5420
5956
  }
5421
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
+
5422
6036
  /**
5423
6037
  * Canvas-only embed deep-link (My widgets / canvas without widget_id):
5424
6038
  * read canvases.primary_widget_id so Creator can open that widget panel.
@@ -5435,20 +6049,44 @@
5435
6049
  (record as { primary_widget_id?: unknown })?.primary_widget_id ??
5436
6050
  null;
5437
6051
  const primaryId = typeof raw === 'string' && raw.trim() ? raw.trim() : null;
5438
- if (primaryId) {
5439
- canvasPrimaryWidgetId = primaryId;
5440
- // console.log(
5441
- // '[CreatorApp] Resolved primary widget for canvas',
5442
- // targetCanvasId.substring(0, 8),
5443
- // '→',
5444
- // primaryId.substring(0, 8)
5445
- // );
5446
- } else {
5447
- // console.log(
5448
- // '[CreatorApp] No primary_widget_id on canvas',
5449
- // targetCanvasId.substring(0, 8)
5450
- // );
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
+ }
5451
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;
5452
6090
  return primaryId;
5453
6091
  } catch (err) {
5454
6092
  console.warn(
@@ -5465,11 +6103,6 @@
5465
6103
  syncEmbeddedDeepLinkDismissTracking();
5466
6104
  if (!embedded || !widgetsClient || !hasValidToken) return;
5467
6105
  if (embeddedDeepLinkPanelDismissed) {
5468
- const dismissedId = resolveEmbeddedDeepLinkWidgetId();
5469
- // console.log(
5470
- // '[CreatorApp] Skip embedded deep-link select — user dismissed panel:',
5471
- // dismissedId ? dismissedId.substring(0, 8) : '(none)'
5472
- // );
5473
6106
  return;
5474
6107
  }
5475
6108
  // Select primary widget once per Creator mount. Do not auto-open Widget Details.
@@ -5477,70 +6110,79 @@
5477
6110
  return;
5478
6111
  }
5479
6112
  if (isConvertOrDraftPanelOpen()) {
5480
- // console.log('[CreatorApp] Skip embedded deep-link select — convert/draft panel is open');
5481
6113
  embeddedDeepLinkAutoOpenDone = true;
5482
6114
  return;
5483
6115
  }
5484
6116
 
6117
+ const hostCanvasId = resolveHostCanvasId();
5485
6118
  let deepLinkWidgetId = resolveEmbeddedDeepLinkWidgetId();
5486
- // Canvas-only deep-link: no ?widget_id= / initialWidgetId — select canvases.primary_widget_id.
5487
- if (!deepLinkWidgetId && canvasesClient) {
5488
- const targetCanvasId =
5489
- (typeof canvasId === 'string' && canvasId.trim() ? canvasId.trim() : null) ||
5490
- (typeof sessionCanvasId === 'string' && sessionCanvasId.trim() ? sessionCanvasId.trim() : null) ||
5491
- (typeof routeCanvasId === 'string' && routeCanvasId.trim() ? routeCanvasId.trim() : null);
5492
- if (targetCanvasId) {
5493
- deepLinkWidgetId = await resolvePrimaryWidgetIdForCanvas(targetCanvasId);
5494
- if (embeddedDeepLinkPanelDismissed || embeddedDeepLinkAutoOpenDone) {
5495
- return;
5496
- }
5497
- // Align address-bar widget_id so header / dismiss tracking match the opened panel.
5498
- if (deepLinkWidgetId && typeof window !== 'undefined') {
5499
- try {
5500
- const url = new URL(window.location.href);
5501
- if (url.searchParams.get('widget_id') !== deepLinkWidgetId) {
5502
- url.searchParams.set('widget_id', deepLinkWidgetId);
5503
- window.history.replaceState({}, '', url.pathname + url.search + url.hash);
5504
- syncEmbeddedDeepLinkDismissTracking();
5505
- }
5506
- } catch (err) {
5507
- console.warn('[CreatorApp] Failed to sync primary widget_id in URL:', err);
5508
- }
5509
- }
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);
5510
6135
  }
5511
6136
  }
5512
- 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);
5513
6154
 
5514
6155
  const requestGen = ++embeddedDeepLinkRequestGen;
6156
+ const expectedWidgetId = deepLinkWidgetId;
5515
6157
 
5516
6158
  function deepLinkOpenStillValid(): boolean {
5517
6159
  return (
5518
6160
  requestGen === embeddedDeepLinkRequestGen &&
5519
6161
  !embeddedDeepLinkPanelDismissed &&
5520
- resolveEmbeddedDeepLinkWidgetId() === deepLinkWidgetId
6162
+ canvasPrimaryWidgetId === expectedWidgetId
5521
6163
  );
5522
6164
  }
5523
6165
 
5524
6166
  const alreadyLinked =
5525
- selectedWidgetId === deepLinkWidgetId && !!getWidgetById(deepLinkWidgetId);
6167
+ selectedWidgetId === expectedWidgetId && !!getWidgetById(expectedWidgetId);
5526
6168
  if (alreadyLinked) {
5527
6169
  if (embeddedInitialStepPending && initialStep) {
5528
6170
  widgetDetailsStep = resolveCreatorWorkflowStep(initialStep);
5529
6171
  embeddedInitialStepPending = false;
5530
6172
  }
5531
6173
  embeddedDeepLinkAutoOpenDone = true;
5532
- if (canvasRef && deepLinkWidgetId) {
5533
- canvasRef.selectWidgetShape(deepLinkWidgetId);
5534
- }
6174
+ applyPrimarySelectionOnCanvas(expectedWidgetId);
6175
+ const primaryWidget = getWidgetById(expectedWidgetId);
6176
+ if (primaryWidget) notifyHostWidgetName(primaryWidget);
6177
+ emitCanvasWidgetsList('deeplink-already-linked');
5535
6178
  return;
5536
6179
  }
5537
6180
 
5538
6181
  try {
5539
6182
  const response = await widgetsClient.getWidgetsBatch({
5540
- getWidgetsBatchRequest: { widgetIds: [deepLinkWidgetId] }
6183
+ getWidgetsBatchRequest: { widgetIds: [expectedWidgetId] }
5541
6184
  });
5542
6185
  if (!deepLinkOpenStillValid()) {
5543
- // console.log('[CreatorApp] Stale embedded deep-link fetch — user changed selection or closed panel');
5544
6186
  return;
5545
6187
  }
5546
6188
 
@@ -5550,47 +6192,52 @@
5550
6192
  };
5551
6193
  const target = batch?.widgets?.[0] ?? batch?.data?.widgets?.[0];
5552
6194
  if (!target?.id) {
5553
- console.warn('[CreatorApp] Embedded deep-link widget not found:', deepLinkWidgetId);
6195
+ console.warn('[CreatorApp] Embedded deep-link widget not found:', expectedWidgetId);
5554
6196
  embeddedDeepLinkAutoOpenDone = true;
5555
6197
  return;
5556
6198
  }
5557
6199
 
5558
- const targetCanvasId =
5559
- (target as { canvasId?: string | null; canvas_id?: string | null }).canvasId ??
5560
- (target as { canvasId?: string | null; canvas_id?: string | null }).canvas_id ??
5561
- 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
+ }
5562
6219
 
5563
6220
  if (!getWidgetById(target.id)) {
5564
6221
  widgets = sortWidgetSummaries([...widgets, target]);
5565
6222
  }
5566
6223
  if (!deepLinkOpenStillValid()) {
5567
- // console.log('[CreatorApp] Skip embedded deep-link open — stale after widget fetch');
5568
6224
  return;
5569
6225
  }
5570
6226
 
5571
6227
  embeddedDeepLinkAutoOpenDone = true;
5572
- // Select the primary widget (and its canvas shape) — do not open Widget Details.
6228
+ canvasPrimaryWidgetId = target.id;
5573
6229
  await selectWidget(target, { skipLoadWidget: true, skipPlaceShape: true });
5574
6230
  if (!deepLinkOpenStillValid()) return;
5575
- if (canvasRef) {
5576
- canvasRef.selectWidgetShape(target.id);
5577
- }
6231
+ applyPrimarySelectionOnCanvas(target.id);
6232
+ requestAnimationFrame(() => {
6233
+ setTimeout(() => applyPrimarySelectionOnCanvas(target.id), 80);
6234
+ });
6235
+ notifyHostWidgetName(target);
6236
+ emitCanvasWidgetsList('deeplink-primary');
5578
6237
  if (embeddedInitialStepPending && initialStep) {
5579
6238
  widgetDetailsStep = resolveCreatorWorkflowStep(initialStep);
5580
6239
  embeddedInitialStepPending = false;
5581
6240
  }
5582
-
5583
- if (targetCanvasId && targetCanvasId !== canvasId) {
5584
- // console.log('[CreatorApp] Switching canvas for embedded deep-link:', targetCanvasId);
5585
- routeCanvasId = targetCanvasId;
5586
- canvasId = targetCanvasId;
5587
- saveSelectedCanvasId(canvasId);
5588
- isLoadingCanvasOnce = false;
5589
- isLoadingWidgetsOnce = false;
5590
- lastWidgetsCanvasId = null;
5591
- void loadCurrentCanvas();
5592
- return;
5593
- }
5594
6241
  } catch (err) {
5595
6242
  console.error('[CreatorApp] ensureEmbeddedWidgetDeepLink failed:', err);
5596
6243
  embeddedDeepLinkAutoOpenDone = true;
@@ -6143,6 +6790,11 @@
6143
6790
  }
6144
6791
 
6145
6792
  notifyHostWidgetName({ id: widgetId, name: newName });
6793
+ emitCanvasWidgetsList('widget-renamed');
6794
+ widgetNamesRevision += 1;
6795
+ panelDisplayRevision += 1;
6796
+ backfillWidgetShapeMetadata();
6797
+ syncWidgetShapeLivePreviews();
6146
6798
  // console.log('[WidgetRename] Successfully renamed widget:', widgetId, 'to:', newName);
6147
6799
  showToast('success', `Widget renamed to "${newName}"`);
6148
6800
  } catch (error) {
@@ -6337,26 +6989,37 @@
6337
6989
  * Aborts the polling and resets UI state
6338
6990
  */
6339
6991
  function stopGeneration() {
6340
- // Abort the generation for the currently selected widget
6341
- if (selectedWidgetId) {
6342
- 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);
6343
7003
  if (controller) {
6344
7004
  controller.abort();
6345
- widgetAbortControllers.delete(selectedWidgetId);
7005
+ widgetAbortControllers.delete(widgetId);
6346
7006
  }
6347
- const { [selectedWidgetId]: _, ...restStop } = generatingWidgetIds;
6348
- generatingWidgetIds = restStop;
7007
+ clearActiveOperationId(widgetId);
6349
7008
  }
7009
+
7010
+ generatingWidgetIds = {};
6350
7011
  // Legacy fallback
6351
7012
  if (codeGenerationAbortController) {
6352
7013
  codeGenerationAbortController.abort();
6353
7014
  codeGenerationAbortController = null;
6354
7015
  }
6355
-
7016
+
7017
+ currentGenerationOperationId = null;
6356
7018
  isGeneratingCode = false;
6357
7019
  setCrossTabGenerating(false);
6358
7020
  generateCodeStatus = '⏹ Generation stopped by user';
6359
- getChatMethodsForWidget(selectedWidgetId)?.updateLatestCodegenStatusMessage?.(
7021
+ const statusWidgetId = focusedPanelWidgetId || selectedWidgetId || stopTargetIds[0] || null;
7022
+ getChatMethodsForWidget(statusWidgetId)?.updateLatestCodegenStatusMessage?.(
6360
7023
  'Generation stopped by user. You can Retry this prompt.',
6361
7024
  );
6362
7025
  showToast('info', 'Generation stopped', {
@@ -6866,7 +7529,8 @@
6866
7529
  const shapeDataUrl = canvasRef?.getWidgetShapeDataUrl?.(widgetId);
6867
7530
  if (shapeDataUrl && canvasRef) {
6868
7531
  canvasRef.updateWidgetImage(widgetId, shapeDataUrl);
6869
- void persistWidgetListThumbnail(widgetId, shapeDataUrl);
7532
+ // Do NOT persist Fabric placeholder/icon as project-list thumbnail —
7533
+ // only iframe screenshots (below) represent the real widget.
6870
7534
  } else {
6871
7535
  // console.log('[handleRefreshScreenshot] Skip quiet capture — panel closed:', widgetId.substring(0, 8));
6872
7536
  }
@@ -7659,37 +8323,49 @@ Requirements:
7659
8323
  // cache-busts the same URL and looks like an extra reload.
7660
8324
  // console.log('[CreatorApp] Skipping extra preview remount — Worker iframe already showing');
7661
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
+ });
7662
8330
  } else {
7663
8331
  activePanelRef?.setPreviewLoadingState(true);
7664
8332
  await activePanelRef?.refreshPreview?.({ withRetry: true, skipIfAlreadyShowing: true });
7665
8333
  }
7666
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;
7667
8342
  if (!completedPreviewBuildErr) {
7668
8343
  await applyDesignSchemaDefaultsToPreview(generationWidgetId);
7669
8344
  pendingCreatePreviewDefaultsWidgetId = null;
7670
8345
  panelChat?.updateLatestCodegenStatusMessage?.(
7671
8346
  `Code generation completed!${commitShort ? ` Commit: ${commitShort}` : ''}\nPreview ready.`,
7672
8347
  );
8348
+ patchCodegenUiState(generationWidgetId, {
8349
+ generateCodeStatus: `Code generation completed!${commitShort ? ` Commit: ${commitShort}` : ''} Preview ready.`,
8350
+ });
7673
8351
  setTimeout(async () => {
7674
8352
  await panelChat?.reloadConversationMessages?.();
7675
8353
  panelChat?.markWidgetHeadAtLatestCommit?.();
7676
8354
  }, 2500);
7677
- setTimeout(() => handleRefreshScreenshot(generationWidgetId, { quiet: true }), 3000);
7678
- void (async () => {
7679
- try {
7680
- console.log('[Publish] Auto-publishing after codegen', generationWidgetId.substring(0, 8));
7681
- await executePublishWidget(generationWidgetId, { silent: true, fromCodegen: true });
7682
- panelChat?.updateLatestCodegenStatusMessage?.(
7683
- `Code generation completed!${commitShort ? ` Commit: ${commitShort}` : ''}\nPreview ready. Live published.`,
7684
- );
7685
- } catch (autoPublishErr) {
7686
- console.warn('[Publish] Auto-publish after codegen failed:', autoPublishErr);
7687
- }
7688
- })();
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.
7689
8362
  } else {
7690
8363
  panelChat?.updateLatestCodegenStatusMessage?.(
7691
8364
  `Code saved${commitShort ? ` (${commitShort})` : ''} — preview rebuilt from saved code.`,
7692
8365
  );
8366
+ patchCodegenUiState(generationWidgetId, {
8367
+ generateCodeStatus: `Code saved${commitShort ? ` (${commitShort})` : ''} — preview rebuilt from saved code.`,
8368
+ });
7693
8369
  }
7694
8370
  }, completedPreviewBuildErr ? 400 : 800);
7695
8371
  } else {
@@ -8132,22 +8808,21 @@ Requirements:
8132
8808
  window.addEventListener('storage', handleCrossTabStorageEvent);
8133
8809
  window.addEventListener('widgetic:session-extended', handleSiteSessionExtended);
8134
8810
 
8135
- // Clean up cross-tab flags when tab is closed/refreshed
8136
- const handleBeforeUnload = () => {
8137
- for (const wId of Object.keys(generatingWidgetIds)) {
8138
- setCrossTabGenerating(false, wId);
8139
- }
8140
- if (isPublishing) setCrossTabPublishing(false);
8141
- };
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.
8142
8813
  window.addEventListener('beforeunload', handleBeforeUnload);
8143
8814
 
8144
- // Read canvas id from route (supports /?canvasId=uuid). Skip in embedded when prop provided.
8145
- 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) {
8146
8819
  try {
8147
8820
  const sub = pageStore.subscribe(($page) => {
8148
- const qId =
8149
- ($page?.url?.searchParams?.get && $page.url.searchParams.get('canvas-id')) || null;
8150
- 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;
8151
8826
  });
8152
8827
  setTimeout(() => {
8153
8828
  if (sub && typeof sub === 'function') sub();
@@ -8439,6 +9114,10 @@ Requirements:
8439
9114
  function formatCodegenProgressStatus(op: OperationStatus): string {
8440
9115
  const stage = getOperationStageMessage(op.metadata as Record<string, unknown> | undefined);
8441
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
+ }
8442
9121
  if (stage) {
8443
9122
  return pct != null && pct > 0 ? `${stage} (${pct}%)` : stage;
8444
9123
  }
@@ -8468,13 +9147,22 @@ Requirements:
8468
9147
  >,
8469
9148
  ): void {
8470
9149
  ensurePanelState(widgetId);
8471
- 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);
8472
9160
  if (widgetId === focusedPanelWidgetId) {
8473
- if (patch.generateCodeStatus !== undefined) generateCodeStatus = patch.generateCodeStatus;
8474
- if (patch.lastGenerationFailed !== undefined) lastGenerationFailed = patch.lastGenerationFailed;
8475
- if (patch.lastGenerationError !== undefined) lastGenerationError = patch.lastGenerationError;
8476
- if (patch.lastCommitId !== undefined) lastCommitId = patch.lastCommitId;
8477
- 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;
8478
9166
  }
8479
9167
  }
8480
9168
 
@@ -8614,13 +9302,15 @@ Requirements:
8614
9302
  compositionSchemasLoading: boolean;
8615
9303
  compositionSchemasError: string | null;
8616
9304
  schemaLoadStartedAt: number | null;
8617
- chatContainerHeight: number;
9305
+ chatContainerHeight: number; // 0 = fill available; >0 = user-resized fixed px
8618
9306
  panelPreviewCompileError: string | null;
8619
9307
  };
8620
9308
 
8621
9309
  let widgetPanelSnapshots: Record<string, WidgetPanelSnapshot> = {};
8622
9310
  /** Bumped on every panel snapshot write so {#each} panels re-read display state. */
8623
9311
  let panelDisplayRevision = 0;
9312
+ /** Bumped on rename so shape chrome / host title re-read widget names. */
9313
+ let widgetNamesRevision = 0;
8624
9314
  const schemaLoadInFlight = new Set<string>();
8625
9315
  const schemaLoadPromises = new Map<string, Promise<void>>();
8626
9316
  const compositionsLoadInFlight = new Set<string>();
@@ -8716,7 +9406,7 @@ Requirements:
8716
9406
  compositionSchemasLoading,
8717
9407
  compositionSchemasError,
8718
9408
  schemaLoadStartedAt: focusedSnap?.schemaLoadStartedAt ?? null,
8719
- chatContainerHeight: focusedSnap?.chatContainerHeight ?? CHAT_DEFAULT_HEIGHT,
9409
+ chatContainerHeight: focusedSnap?.chatContainerHeight ?? 0,
8720
9410
  panelPreviewCompileError: focusedSnap?.panelPreviewCompileError ?? null,
8721
9411
  };
8722
9412
  }
@@ -8782,7 +9472,7 @@ Requirements:
8782
9472
  compositionSchemasLoading: false,
8783
9473
  compositionSchemasError: null,
8784
9474
  schemaLoadStartedAt: null,
8785
- chatContainerHeight: 550,
9475
+ chatContainerHeight: 0,
8786
9476
  panelPreviewCompileError: null,
8787
9477
  };
8788
9478
  }
@@ -8838,7 +9528,7 @@ Requirements:
8838
9528
  : livePublishMatchesPanel
8839
9529
  ? live.publishHistoryResolvedForWidgetId
8840
9530
  : snap.publishHistoryResolvedForWidgetId,
8841
- chatContainerHeight: snap.chatContainerHeight ?? CHAT_DEFAULT_HEIGHT,
9531
+ chatContainerHeight: snap.chatContainerHeight ?? 0,
8842
9532
  compositionDesignSchema: resolvedDesignSchema,
8843
9533
  compositionContentSchema: resolvedContentSchema,
8844
9534
  compositionSchemasLoading: schemaLoading,
@@ -8856,7 +9546,7 @@ Requirements:
8856
9546
  ...snap,
8857
9547
  lastCommitId: snap.lastCommitId || loadCommitIdForWidget(widgetId),
8858
9548
  hasGeneratedCode: !!(snap.hasGeneratedCode || snap.lastCommitId || loadCommitIdForWidget(widgetId)),
8859
- chatContainerHeight: snap.chatContainerHeight ?? CHAT_DEFAULT_HEIGHT,
9549
+ chatContainerHeight: snap.chatContainerHeight ?? 0,
8860
9550
  compositionDesignSchema: resolvedDesignSchema,
8861
9551
  compositionContentSchema: resolvedContentSchema,
8862
9552
  compositionSchemasError: resolvedSchemaError,
@@ -9017,27 +9707,226 @@ Requirements:
9017
9707
 
9018
9708
  /**
9019
9709
  * Live preview URL for a canvas WidgetShape overlay.
9020
- * Prefer published CDN when available; else Worker /preview when code exists.
9021
- * Returns null → screenshot-only fallback on the Fabric shape.
9710
+ * Source: 'live' | 'preview' | 'v:{N}' (historical published version).
9711
+ * Publish history is loaded without requiring chat to be open.
9022
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
+
9023
9751
  function resolveWidgetShapeLivePreviewUrl(widgetId: string): string | null {
9024
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
+
9025
9780
  const published = getPanelEditPreviewUrl(widgetId);
9026
- if (published) return published;
9781
+ if (published && source !== 'preview') return published;
9782
+ return workerUrl;
9783
+ }
9027
9784
 
9028
- const widget = getWidgetById(widgetId);
9029
- if (!widget) return null;
9785
+ function resolveWidgetShapeVersionLabel(widgetId: string): string | null {
9786
+ const source = shapePreviewSourceByWidgetId[widgetId] || 'live';
9030
9787
  const ps = getPanelDisplayState(widgetId);
9031
- const hasCode = !!(ps.lastCommitId || loadCommitIdForWidget(widgetId));
9032
- if (!hasCode) return null;
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
+ }
9033
9881
 
9034
- return `${widgetBuilderPreviewBaseUrl()}/preview/${widgetId}`;
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);
9035
9909
  }
9036
9910
 
9037
9911
  function syncWidgetShapeLivePreviews(): void {
9038
9912
  try {
9039
9913
  canvasSelectedWidgetShapeId = canvasRef?.getSelectedWidgetShapeId?.() ?? null;
9040
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
+ }
9041
9930
  } catch (err) {
9042
9931
  console.warn('[CreatorApp] WidgetShape live preview sync failed', err);
9043
9932
  }
@@ -9380,6 +10269,9 @@ Requirements:
9380
10269
  if (detailsSessionWidgetId === fromId) detailsSessionWidgetId = toId;
9381
10270
  if (widgetDetailsOpenTargetId === fromId) widgetDetailsOpenTargetId = toId;
9382
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();
9383
10275
  // console.log(
9384
10276
  // '[CreatorApp] Migrated Widget Details panel',
9385
10277
  // fromId.substring(0, 16),
@@ -9975,6 +10867,8 @@ Requirements:
9975
10867
 
9976
10868
  function hasUnpublishedChangesForPanel(widgetId: string): boolean {
9977
10869
  const ps = getPanelDisplayState(widgetId);
10870
+ if (ps.pendingPublishAfterCodegen) return true;
10871
+ const pendingSha = loadPendingPublishShaForWidget(widgetId);
9978
10872
  const headSha = resolvePublishHeadCommitId(widgetId, ps);
9979
10873
  if (!headSha || ps.lastPublishedVersion === null) return false;
9980
10874
 
@@ -9982,6 +10876,7 @@ Requirements:
9982
10876
  getLivePublishedCommitShaForPanel(widgetId)
9983
10877
  ?? getWidgetLastPublishCommitSha(widgetId);
9984
10878
  if (!liveSha) return false;
10879
+ if (pendingSha && !commitsMatch(pendingSha, liveSha)) return true;
9985
10880
 
9986
10881
  return !commitsMatch(headSha, liveSha);
9987
10882
  }
@@ -10422,6 +11317,10 @@ async function loadPublishHistoryForPanel(widgetId: string, opts: { silent?: boo
10422
11317
  publishHistoryResolved: true,
10423
11318
  publishHistoryResolvedForWidgetId: widgetId,
10424
11319
  });
11320
+ shapePublishHistoryByWidgetId = {
11321
+ ...shapePublishHistoryByWidgetId,
11322
+ [widgetId]: [...history],
11323
+ };
10425
11324
  if (widgetId === focusedPanelWidgetId) {
10426
11325
  publishHistory = history;
10427
11326
  publishHistoryResolved = true;
@@ -10464,8 +11363,8 @@ async function loadPublishHistory(opts: { silent?: boolean } = {}) {
10464
11363
  * Sets a historical version as the current live version without rebuilding.
10465
11364
  * Updates widgets.js_path to point to the older version's artifact URL.
10466
11365
  */
10467
- async function setLiveVersion(version: number) {
10468
- if (!selectedWidgetId || !$userSession?.jwt) return;
11366
+ async function setLiveVersionForWidget(widgetId: string, version: number) {
11367
+ if (!widgetId || !$userSession?.jwt) return;
10469
11368
 
10470
11369
  isSettingLiveVersion = version;
10471
11370
  if (!widgetsClient) {
@@ -10475,12 +11374,12 @@ async function setLiveVersion(version: number) {
10475
11374
  }
10476
11375
  try {
10477
11376
  const liveRes = await widgetsClient.setWidgetPublishHistoryLive({
10478
- widgetId: selectedWidgetId,
11377
+ widgetId,
10479
11378
  version
10480
11379
  });
10481
11380
  const nested = liveRes?.data as { data?: Record<string, unknown>; artifactUrl?: string; artifact_url?: string; commitSha?: string; commit_sha?: string } | undefined;
10482
11381
  const liveData = nested?.data ?? nested ?? {};
10483
- const history = getPanelDisplayState(selectedWidgetId).publishHistory;
11382
+ const history = getPanelDisplayState(widgetId).publishHistory;
10484
11383
  const record = history.find((row) => row.version === version);
10485
11384
  const commitSha = (liveData as { commitSha?: string; commit_sha?: string }).commitSha
10486
11385
  || (liveData as { commit_sha?: string }).commit_sha
@@ -10488,15 +11387,23 @@ async function setLiveVersion(version: number) {
10488
11387
  const artifactUrl = (liveData as { artifactUrl?: string; artifact_url?: string }).artifactUrl
10489
11388
  || (liveData as { artifact_url?: string }).artifact_url
10490
11389
  || record?.artifactUrl || record?.artifact_url || null;
10491
- 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;
10492
11397
  isHistoryOpen = false;
10493
- const liveUrl = resolvePublishedWidgetUrl(selectedWidgetId, version, artifactUrl);
10494
- const detailsRef = getWidgetDetailsRef(selectedWidgetId);
11398
+ const liveUrl = resolvePublishedWidgetUrl(widgetId, version, artifactUrl);
11399
+ const detailsRef = getWidgetDetailsRef(widgetId);
10495
11400
  detailsRef?.showPublishedArtifact?.(liveUrl);
10496
11401
  detailsRef?.beginCompositionHydration?.();
10497
- revealCompositionPreview();
10498
- showToast('success', `Reverted to v${version}`, {
10499
- 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.',
10500
11407
  duration: 5000
10501
11408
  });
10502
11409
  } catch (err: any) {
@@ -10507,6 +11414,11 @@ async function setLiveVersion(version: number) {
10507
11414
  }
10508
11415
  }
10509
11416
 
11417
+ async function setLiveVersion(version: number) {
11418
+ if (!selectedWidgetId) return;
11419
+ await setLiveVersionForWidget(selectedWidgetId, version);
11420
+ }
11421
+
10510
11422
  /** Toggle publish history panel — reloads data on every open */
10511
11423
  function togglePublishHistory(triggerEl?: HTMLElement | null) {
10512
11424
  isHistoryOpen = !isHistoryOpen;
@@ -11065,18 +11977,10 @@ $: if (codeGenerationPrompt && selectedWidgetId && promptLoadedForWidgetId === s
11065
11977
  }
11066
11978
  }
11067
11979
 
11068
- // Keep address-bar widget_id on the canvas primary (duplicate/select must not steal it).
11069
- if (typeof window !== 'undefined' && embedded && canvasPrimaryWidgetId && widgetId === canvasPrimaryWidgetId) {
11070
- try {
11071
- const url = new URL(window.location.href);
11072
- if (url.searchParams.get('widget_id') !== widgetId) {
11073
- url.searchParams.set('widget_id', widgetId);
11074
- window.history.replaceState({}, '', url.pathname + url.search + url.hash);
11075
- syncEmbeddedDeepLinkDismissTracking();
11076
- }
11077
- } catch (err) {
11078
- console.warn('[WidgetCreator] Failed to sync widget_id in URL:', err);
11079
- }
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);
11080
11984
  }
11081
11985
  // Full select so commit/preview/header match the shape's linked widget (not a stale URL widget).
11082
11986
  if (selectedWidgetId !== widgetId) {
@@ -11308,6 +12212,10 @@ $: if (codeGenerationPrompt && selectedWidgetId && promptLoadedForWidgetId === s
11308
12212
  }
11309
12213
  notifyHostWidgetName({ id: widgetId, name: newName });
11310
12214
  emitCanvasWidgetsList('widget-renamed');
12215
+ widgetNamesRevision += 1;
12216
+ panelDisplayRevision += 1;
12217
+ backfillWidgetShapeMetadata();
12218
+ syncWidgetShapeLivePreviews();
11311
12219
  showToast('success', `Widget renamed to "${newName}"`);
11312
12220
  } catch (error) {
11313
12221
  console.error('[WidgetCreator] Failed to rename widget:', error);
@@ -11359,14 +12267,15 @@ $: if (codeGenerationPrompt && selectedWidgetId && promptLoadedForWidgetId === s
11359
12267
  }
11360
12268
  await syncWidgetsWithCanvas(widgetIdsOnCanvas);
11361
12269
  await maybeAutoRestoreWidgetShapes();
11362
- const primaryId = await hydrateCanvasPrimaryWidget(widgetIdsOnCanvas);
11363
- if (primaryId && canvasRef && !isConvertOrDraftPanelOpen()) {
11364
- canvasRef.setPrimaryWidgetShape?.(primaryId);
11365
- const found = canvasRef.selectWidgetShape(primaryId);
11366
- // console.log('[CreatorApp] Primary widget shape after canvas load:', primaryId, found);
11367
- } else if (isConvertOrDraftPanelOpen()) {
11368
- // console.log('[CreatorApp] Skip primary shape select — convert/draft panel is open');
11369
- }
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
+ });
11370
12279
  syncWidgetShapeLivePreviews();
11371
12280
  }
11372
12281
 
@@ -11641,14 +12550,20 @@ $: if (codeGenerationPrompt && selectedWidgetId && promptLoadedForWidgetId === s
11641
12550
  let autosaveSequence = 0;
11642
12551
  let lastResolvedAutosaveSequence = 0;
11643
12552
 
11644
- async function handleCanvasAutoSave(json: string | null, isUnmountSave = false) {
12553
+ async function handleCanvasAutoSave(
12554
+ json: string | null,
12555
+ isUnmountSave = false,
12556
+ opts: { force?: boolean } = {},
12557
+ ) {
11645
12558
  if (!json) {
11646
12559
  if (isUnmountSave) console.warn('[CanvasDebug] onBeforeUnmount: saveCanvas returned null — canvas not initialized');
11647
12560
  return;
11648
12561
  }
11649
12562
 
12563
+ const force = opts.force === true;
12564
+
11650
12565
  // Guard: don't overwrite DB with an empty canvas if state hasn't loaded yet
11651
- if (!isCanvasStateLoaded) {
12566
+ if (!force && !isCanvasStateLoaded) {
11652
12567
  // console.log(
11653
12568
  // '[CanvasDebug] Skipping autosave — canvas state not loaded yet (guard active, avoids overwrite with empty)'
11654
12569
  // );
@@ -11656,7 +12571,7 @@ $: if (codeGenerationPrompt && selectedWidgetId && promptLoadedForWidgetId === s
11656
12571
  }
11657
12572
 
11658
12573
  // Guard: skip autosaves triggered by loadCanvas() object:added events
11659
- if (isLoadCooldownActive && !isUnmountSave) {
12574
+ if (!force && isLoadCooldownActive && !isUnmountSave) {
11660
12575
  // console.log('[CanvasDebug] Skipping autosave — load cooldown active (prevents overwriting freshly loaded data)');
11661
12576
  return;
11662
12577
  }
@@ -11679,11 +12594,17 @@ $: if (codeGenerationPrompt && selectedWidgetId && promptLoadedForWidgetId === s
11679
12594
  // Unmount saves (HMR) are always blocked when going from N→0.
11680
12595
  // Normal autosaves going from N→0 are allowed only if the drop is gradual
11681
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.
11682
12598
  if (objectCount === 0 && lastSavedCanvasObjectCount > 0) {
11683
12599
  if (isUnmountSave || lastSavedCanvasObjectCount > 1) {
11684
12600
  console.warn(
11685
- `[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})`
11686
12602
  );
12603
+ if (force) {
12604
+ showToast('error', 'Save blocked', {
12605
+ description: 'Canvas snapshot looked empty — refresh and try again.',
12606
+ });
12607
+ }
11687
12608
  return;
11688
12609
  }
11689
12610
  // lastSavedCanvasObjectCount === 1 → user deleted the last object, allow it
@@ -11720,12 +12641,25 @@ $: if (codeGenerationPrompt && selectedWidgetId && promptLoadedForWidgetId === s
11720
12641
  return;
11721
12642
  }
11722
12643
  lastResolvedAutosaveSequence = currentSeq;
12644
+ if (force) {
12645
+ showToast('success', 'Canvas saved', { duration: 2000 });
12646
+ }
11723
12647
  // console.log(
11724
12648
  // `[CanvasDebug] Canvas ${canvasId} saved to API: ${objectCount} shapes (seq ${currentSeq})`
11725
12649
  // );
11726
12650
  } catch (err) {
11727
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
+ }
11728
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
+ });
11729
12663
  }
11730
12664
 
11731
12665
  refreshLinkedWidgetIds();
@@ -12069,6 +13003,68 @@ $: if (codeGenerationPrompt && selectedWidgetId && promptLoadedForWidgetId === s
12069
13003
  return !!widgetId && widgetId.startsWith(DRAFT_CONVERT_ID_PREFIX);
12070
13004
  }
12071
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
+
12072
13068
  function isConvertOrDraftPanelOpen(): boolean {
12073
13069
  return (
12074
13070
  isDraftConvertId(focusedPanelWidgetId)
@@ -12507,13 +13503,14 @@ $: if (codeGenerationPrompt && selectedWidgetId && promptLoadedForWidgetId === s
12507
13503
  <!-- Right: Debug toggle + Auth info (2 lines) + Logout -->
12508
13504
  <div class="wc-top-bar-right flex items-center gap-3 shrink-0">
12509
13505
  {#if debugModeAvailable}
13506
+ <HoverTooltip text={debugMode ? 'Debug mode ON — click to disable (hides console tab, widgets panel)' : 'Debug mode OFF — click to enable'}>
12510
13507
  <button
12511
13508
  onclick={toggleDebugMode}
12512
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'}"
12513
- title={debugMode ? 'Debug mode ON — click to disable (hides console tab, widgets panel)' : 'Debug mode OFF — click to enable'}
12514
13510
  >
12515
13511
  🐛 Debug {debugMode ? 'ON' : 'OFF'}
12516
13512
  </button>
13513
+ </HoverTooltip>
12517
13514
  {/if}
12518
13515
  <div class="wc-auth-info flex flex-col gap-px items-start">
12519
13516
  <div class="wc-auth-line1 flex items-center gap-2">
@@ -12598,10 +13595,11 @@ $: if (codeGenerationPrompt && selectedWidgetId && promptLoadedForWidgetId === s
12598
13595
  onWidgetShapeDuplicated={handleWidgetShapeDuplicated}
12599
13596
  onWidgetShapeDeleting={handleWidgetShapeDeleting}
12600
13597
  onWidgetRename={handleWidgetRename}
12601
- onRefreshScreenshot={handleRefreshScreenshot}
13598
+ onRefreshScreenshot={null}
12602
13599
  onSetPrimaryWidget={handleSetPrimaryWidget}
12603
13600
  onAfterRender={syncWidgetShapeLivePreviews}
12604
13601
  getWidgetName={getWidgetNameById}
13602
+ widgetNameRevision={widgetNamesRevision}
12605
13603
  isWidgetKnown={isWidgetKnownById}
12606
13604
  onNonWidgetSelected={() => {
12607
13605
  if (suppressWidgetDetailsClose || isConvertOrDraftPanelOpen()) {
@@ -12630,17 +13628,37 @@ $: if (codeGenerationPrompt && selectedWidgetId && promptLoadedForWidgetId === s
12630
13628
  bind:this={widgetShapeLivePreviewsRef}
12631
13629
  getRects={() => canvasRef?.listWidgetShapeScreenRects?.() ?? []}
12632
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)}
12633
13644
  selectedWidgetId={canvasSelectedWidgetShapeId}
12634
13645
  onTranslate={(widgetId, dx, dy) => {
12635
13646
  canvasRef?.translateWidgetShapeByScreen?.(widgetId, dx, dy);
12636
13647
  }}
13648
+ onTranslateEnd={(widgetId) => {
13649
+ canvasRef?.commitWidgetShapeTranslate?.(widgetId);
13650
+ }}
12637
13651
  onSelectShape={(widgetId) => {
12638
13652
  canvasRef?.selectWidgetShapeById?.(widgetId);
12639
13653
  canvasSelectedWidgetShapeId = widgetId;
13654
+ void ensureShapePublishHistory(widgetId);
12640
13655
  }}
12641
13656
  onOpenChat={(widgetId) => {
12642
13657
  void openWidgetDetailsPanel(widgetId);
12643
13658
  }}
13659
+ onRefresh={(widgetId) => {
13660
+ console.log('[WidgetShape] Iframe refreshed', widgetId.substring(0, 8));
13661
+ }}
12644
13662
  />
12645
13663
 
12646
13664
  <!-- ═══ PANEL 2: Widgets Panel — debug + showWidgetsList (site passes false) ═══ -->
@@ -13236,9 +14254,32 @@ $: if (codeGenerationPrompt && selectedWidgetId && promptLoadedForWidgetId === s
13236
14254
  hasAutoOpenedPreview = detail.hasAutoOpenedPreview;
13237
14255
  }}
13238
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>
13239
14274
  <svelte:fragment slot="version-history">
13240
14275
  {#if ps.lastPublishedVersion !== null}
13241
- <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.">
13242
14283
  <button
13243
14284
  type="button"
13244
14285
  onclick={(e) => {
@@ -13246,11 +14287,48 @@ $: if (codeGenerationPrompt && selectedWidgetId && promptLoadedForWidgetId === s
13246
14287
  togglePublishHistory(e.currentTarget);
13247
14288
  }}
13248
14289
  onmousedown={(e) => e.stopPropagation()}
13249
- 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"
13250
14291
  >
13251
- Live: v{ps.lastPublishedVersion}
14292
+ v{Number.isFinite(viewingVersion) ? viewingVersion : ps.lastPublishedVersion}
13252
14293
  <span class="text-emerald-500">{isHistoryOpen && isPanelFocused ? '▲' : '▼'}</span>
13253
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}
13254
14332
  {#if isHistoryOpen && isPanelFocused}
13255
14333
  <Portal target="wc-live-history-portal-{panelWidgetId}">
13256
14334
  <button
@@ -13269,38 +14347,49 @@ $: if (codeGenerationPrompt && selectedWidgetId && promptLoadedForWidgetId === s
13269
14347
  <p class="text-xs text-emerald-400 text-center py-2">No history records yet. Publish to start tracking.</p>
13270
14348
  {:else}
13271
14349
  {#each ps.publishHistory as record (record.id)}
13272
- <div class="publish-history-row flex items-center gap-2 rounded border border-emerald-100 bg-white px-2 py-1.5 w-max">
13273
- <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>
13274
14370
  {#if debugMode && (record.commitSha || record.commit_sha)}
13275
14371
  <span class="text-xs font-mono text-slate-400 shrink-0" title={record.commitSha ?? record.commit_sha}>
13276
14372
  {(record.commitSha ?? record.commit_sha).slice(0, 7)}
13277
14373
  </span>
13278
14374
  {/if}
13279
14375
  <span class="wc-live-history-date text-xs text-slate-400 shrink-0 whitespace-nowrap tabular-nums" style="padding-right: 4px;">
13280
- {(() => {
13281
- const d = new Date(record.publishedAt ?? record.published_at ?? '');
13282
- if (isNaN(d.getTime())) return '—';
13283
- const month = String(d.getMonth() + 1).padStart(2, '0');
13284
- const day = String(d.getDate()).padStart(2, '0');
13285
- const timePart = d.toLocaleTimeString(undefined, { hour: '2-digit', minute: '2-digit', hour12: false });
13286
- return `${month}/${day}/${d.getFullYear()} ${timePart}`;
13287
- })()}
14376
+ {formatPublishHistoryDate(record)}
13288
14377
  </span>
13289
- {#if record.version !== ps.lastPublishedVersion}
14378
+ {#if !rowIsLive}
13290
14379
  <button
13291
- onclick={() => { void setLiveVersion(record.version); }}
14380
+ type="button"
14381
+ onclick={(e) => {
14382
+ e.stopPropagation();
14383
+ void setLiveVersionForWidget(panelWidgetId, record.version);
14384
+ }}
13292
14385
  disabled={isSettingLiveVersion === record.version}
13293
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"
13294
- title="Set this version as the live published widget"
14387
+ title="Publish this version so visitors see it on your website"
13295
14388
  >
13296
14389
  {isSettingLiveVersion === record.version ? '...' : 'Set live'}
13297
14390
  </button>
13298
14391
  {:else}
13299
- <button
13300
- type="button"
13301
- 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"
13302
- onclick={() => { isHistoryOpen = false; }}
13303
- >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>
13304
14393
  {/if}
13305
14394
  </div>
13306
14395
  {/each}
@@ -13338,7 +14427,7 @@ $: if (codeGenerationPrompt && selectedWidgetId && promptLoadedForWidgetId === s
13338
14427
  </svelte:fragment>
13339
14428
 
13340
14429
  <svelte:fragment slot="code-generation-status">
13341
- <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;">
13342
14431
  <div class="flex items-center justify-between gap-2 min-w-0">
13343
14432
  <div class="generate-code-status-el text-xs flex-1 min-w-0 truncate {ps.lastGenerationFailed ? 'text-red-600' : 'text-gray-500'}">
13344
14433
  {#if ps.lastGenerationFailed}
@@ -13349,7 +14438,7 @@ $: if (codeGenerationPrompt && selectedWidgetId && promptLoadedForWidgetId === s
13349
14438
  </div>
13350
14439
  </div>
13351
14440
  {#if ps.lastGenerationFailed}
13352
- <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;">
13353
14442
  <p class="text-xs text-red-700 whitespace-pre-wrap">{ps.lastGenerationError}</p>
13354
14443
  <div class="flex gap-2 mt-3 justify-center">
13355
14444
  <Button
@@ -13387,20 +14476,22 @@ $: if (codeGenerationPrompt && selectedWidgetId && promptLoadedForWidgetId === s
13387
14476
 
13388
14477
  <svelte:fragment slot="code-generation">
13389
14478
  <!-- svelte-ignore a11y_no_static_element_interactions -->
13390
- {@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))}
13391
14480
  <div
13392
14481
  class="generation-scroll-ct flex flex-col h-full min-h-0 overflow-y-auto border-t border-gray-200"
13393
- style="padding: 10px;"
13394
- use:nativeScrollbarPadding={10}
14482
+ style="padding: 0;"
14483
+ use:nativeScrollbarPadding={0}
13395
14484
  onmousedown={() => { if (!isPanelFocused) focusWidgetPanel(panelWidgetId); }}
13396
14485
  >
13397
14486
  {#key panelInstanceKey}
13398
14487
  <div
13399
- 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'}"
13400
14489
  data-widget-chat-height={panelWidgetId}
13401
- 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;`}
13402
14493
  >
13403
- <Chat class="code-generation-chat w-full h-full"
14494
+ <Chat class="code-generation-chat w-full h-full rounded-none border-0"
13404
14495
  style="height: 100%; min-height: {CHAT_MIN_HEIGHT}px;"
13405
14496
  resizeExpandsContainer={true}
13406
14497
  onMessagesResize={(deltaPx) => handleChatMessagesResize(panelWidgetId, deltaPx)}
@@ -13428,7 +14519,7 @@ $: if (codeGenerationPrompt && selectedWidgetId && promptLoadedForWidgetId === s
13428
14519
  autoCodeDetection: false,
13429
14520
  }
13430
14521
  }}
13431
- widgetHeadCommitSha={ps.lastCommitId}
14522
+ widgetHeadCommitSha={resolveViewingCommitShaForPanel(panelWidgetId) || ps.lastCommitId}
13432
14523
  onSendMessage={(content, attachments) => handleChatSendMessage(content, attachments, panelWidgetId)}
13433
14524
  onRetryMessage={(content) => handleChatRetryMessage(content, panelWidgetId)}
13434
14525
  onUploadChatFile={handleChatUploadFile}
@@ -13441,7 +14532,9 @@ $: if (codeGenerationPrompt && selectedWidgetId && promptLoadedForWidgetId === s
13441
14532
  onSaveMessage={(conversationId, content, messageType, attachments) =>
13442
14533
  handleSaveMessage(conversationId, content, messageType, attachments, panelWidgetId)
13443
14534
  }
13444
- onRestoreCheckpointBackend={handleRestoreCheckpoint}
14535
+ onRestoreCheckpointBackend={(conversationId, checkpointMessageId) =>
14536
+ handleRestoreCheckpoint(conversationId, checkpointMessageId, panelWidgetId)
14537
+ }
13445
14538
  onChatReady={(methods) => handleChatReady(methods, panelWidgetId)}
13446
14539
  onDraftChange={() => handleChatDraftChange(panelWidgetId)}
13447
14540
  onConversationChange={(conversationId) => handleChatConversationChange(conversationId, panelWidgetId)}
@@ -13503,7 +14596,7 @@ $: if (codeGenerationPrompt && selectedWidgetId && promptLoadedForWidgetId === s
13503
14596
  {#if isEditStepWithoutPublishForPanel(panelWidgetId)}
13504
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">
13505
14598
  <strong class="font-semibold">Not published yet.</strong>
13506
- 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.
13507
14600
  </div>
13508
14601
  {/if}
13509
14602
 
@@ -13993,11 +15086,46 @@ $: if (codeGenerationPrompt && selectedWidgetId && promptLoadedForWidgetId === s
13993
15086
  Are you sure you want to delete <strong>"{deleteCanvasTarget?.name || ''}"</strong>? This action cannot be undone.
13994
15087
  </Dialog.Description>
13995
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
+
13996
15124
  <div class="confirm-delete-canvas-dialog-actions flex justify-end gap-2 pt-2">
13997
15125
  <button
13998
15126
  type="button"
13999
15127
  class="px-4 py-2 text-sm rounded-lg border border-gray-300 hover:bg-gray-100 cursor-pointer"
14000
- onclick={() => { showDeleteCanvasDialog = false; deleteCanvasTarget = null; }}
15128
+ onclick={() => { showDeleteCanvasDialog = false; deleteCanvasTarget = null; deleteCanvasError = ''; }}
14001
15129
  >Cancel</button>
14002
15130
  <button
14003
15131
  type="button"