@widgetic/creator 0.3.51 → 0.3.54

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.
@@ -111,6 +111,21 @@
111
111
 
112
112
  function notifyHostWidgetName(widget: { id?: string | null; name?: string | null } | null | undefined): void {
113
113
  if (!embedded || !widget?.id) return;
114
+ const primaryId = (canvasPrimaryWidgetId || '').trim();
115
+ if (!primaryId) {
116
+ console.log('[CreatorApp] Skip host title — primary widget unknown');
117
+ emitCanvasWidgetsList('skip-host-title');
118
+ return;
119
+ }
120
+ if (widget.id !== primaryId) {
121
+ console.log(
122
+ '[CreatorApp] Skip host title — not primary',
123
+ widget.id.substring(0, 8),
124
+ primaryId.substring(0, 8)
125
+ );
126
+ emitCanvasWidgetsList('skip-host-title');
127
+ return;
128
+ }
114
129
  const name = (widget.name || '').trim() || 'Untitled Widget';
115
130
  console.log('[CreatorApp] Notify host widget name:', widget.id, name);
116
131
  onWidgetRenamed?.({ widgetId: widget.id, name });
@@ -142,9 +157,14 @@
142
157
  const HOST_OPEN_WIDGET_DETAILS_EVENT = 'widgetic:host-open-widget-details';
143
158
  const HOST_DUPLICATE_WIDGET_EVENT = 'widgetic:host-duplicate-widget';
144
159
  const HOST_DELETE_WIDGET_EVENT = 'widgetic:host-delete-widget';
160
+ const HOST_SAVE_CANVAS_EVENT = 'widgetic:host-save-canvas';
161
+ const HOST_SET_PRIMARY_WIDGET_EVENT = 'widgetic:host-set-primary-widget';
162
+ const HOST_REQUEST_CANVAS_WIDGETS_EVENT = 'widgetic:host-request-canvas-widgets';
163
+ const HOST_MOVE_CANVAS_FOLDER_EVENT = 'widgetic:host-move-canvas-folder';
145
164
  const CREATOR_DEBUG_CHANGED_EVENT = 'widgetic:creator-debug-changed';
146
165
  const CREATOR_WIREFRAME_CHANGED_EVENT = 'widgetic:creator-wireframe-changed';
147
166
  const CREATOR_ADVANCED_CHANGED_EVENT = 'widgetic:creator-advanced-changed';
167
+ const CREATOR_CANVAS_WIDGETS_EVENT = 'widgetic:creator-canvas-widgets';
148
168
 
149
169
  /** Site AppHeader rename — embed is a second Svelte runtime, so mount props do not update. */
150
170
  async function applyHostWidgetName(widgetId: string | null | undefined, newName: string): Promise<void> {
@@ -246,7 +266,7 @@
246
266
  /**
247
267
  * WIDGETIC EDITOR — PropsEditor for design/content property editing
248
268
  */
249
- import { PropsEditor, setFileBrowserUploadConfig } from '@widgetic/editor';
269
+ import { PropsEditor, setFileBrowserUploadConfig, setGoogleFontsApiKey } from '@widgetic/editor';
250
270
 
251
271
  // API root: embedded hosts pass apiUrl; standalone uses VITE_API_URL / PUBLIC_API_URL.
252
272
  // If unset, @widgetic/api-sdk falls back to BASE_PATH = dev-api.widgetic.com (OpenAPI default).
@@ -549,6 +569,7 @@
549
569
  let linkedWidgetIds: Set<string> = new Set();
550
570
  function refreshLinkedWidgetIds() {
551
571
  linkedWidgetIds = canvasRef?.getLinkedWidgetIds?.() ?? new Set();
572
+ emitCanvasWidgetsList('linked-ids');
552
573
  }
553
574
 
554
575
  // Delete Widget Shape confirmation dialog state
@@ -606,6 +627,7 @@
606
627
  if (!widgetsClient || !hasValidToken) return;
607
628
  try {
608
629
  console.log('[WidgetCreator] Deleting widget from DB:', widgetId);
630
+ closeWidgetDetailsPanel(widgetId);
609
631
  await widgetsClient.deleteWidget({ id: widgetId });
610
632
  widgets = widgets.filter((w: any) => w.id !== widgetId);
611
633
  if (selectedWidgetId === widgetId) {
@@ -734,6 +756,9 @@
734
756
  updateLastUserMessageAttachments?: (attachments: Array<{ id?: string; url: string; file_type?: string; file_name?: string | null }>) => void;
735
757
  } | null = null;
736
758
  let chatMethodsByWidgetId: Record<string, NonNullable<typeof chatMethods>> = {};
759
+ /** Conversation `completed` flag per widget — ActionBar Complete / Completed. */
760
+ let conversationCompletedByWidgetId: Record<string, boolean> = {};
761
+ let conversationsByWidgetId: Record<string, any[]> = {};
737
762
  let pendingChatDraftText = '';
738
763
  let chatDraftRestoredForWidgetId: string | null = null;
739
764
  /**
@@ -951,15 +976,8 @@
951
976
  file_size: attachment.fileSize,
952
977
  display_order: i + 1,
953
978
  }));
954
- if (persistFileAttachments.length > 0) {
955
- await ensureCodegenUserMessagePersisted(
956
- codegenWidgetId,
957
- content,
958
- undefined,
959
- persistFileAttachments,
960
- );
961
- }
962
- // Pass upload IDs / images explicitly — clearDraft already wiped Chat's local attachments.
979
+ // Chat.onSaveMessage already persisted this turn. generateCode keeps one
980
+ // ensureCodegenUserMessagePersisted after imagesToSend is finalized.
963
981
  await generateCode({
964
982
  prompt: content,
965
983
  fromChat: true,
@@ -1046,6 +1064,86 @@
1046
1064
  currentChatConversationId = conversationId;
1047
1065
  }
1048
1066
  console.log('[Chat] Active conversation changed for', widgetId.substring(0, 8), ':', conversationId || 'none');
1067
+ const conv = (conversationsByWidgetId[widgetId] || []).find((c) => c.id === conversationId);
1068
+ if (conv) {
1069
+ const completed = !!(conv.completed ?? conv.is_completed ?? conv.isCompleted);
1070
+ conversationCompletedByWidgetId = { ...conversationCompletedByWidgetId, [widgetId]: completed };
1071
+ }
1072
+ }
1073
+
1074
+ function isConversationRecordCompleted(conversation: any): boolean {
1075
+ return !!(conversation?.completed ?? conversation?.is_completed ?? conversation?.isCompleted);
1076
+ }
1077
+
1078
+ function isUuid(value: string | null | undefined): value is string {
1079
+ return !!value && /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(value);
1080
+ }
1081
+
1082
+ function resolvePersistableConversationId(widgetId: string, preferredId?: string | null): string | null {
1083
+ const knownIds = (conversationsByWidgetId[widgetId] || [])
1084
+ .map((c) => c.id)
1085
+ .filter((id) => isUuid(id));
1086
+ const fromChat = getChatMethodsForWidget(widgetId)?.getConversationId?.();
1087
+ const fromFocus = widgetId === focusedPanelWidgetId ? currentChatConversationId : null;
1088
+ const preferredKnown = [preferredId, fromChat, fromFocus].find((id) => id && knownIds.includes(id));
1089
+ if (preferredKnown) return preferredKnown;
1090
+ if (knownIds[0]) return knownIds[0];
1091
+ if (isUuid(preferredId)) return preferredId;
1092
+ if (isUuid(fromChat)) return fromChat;
1093
+ return isUuid(fromFocus) ? fromFocus : null;
1094
+ }
1095
+
1096
+ async function handleChatCompletionToggle(
1097
+ widgetId: string,
1098
+ detail?: { conversationId?: string | null; completed?: boolean },
1099
+ ) {
1100
+ const next = typeof detail?.completed === 'boolean'
1101
+ ? detail.completed
1102
+ : !conversationCompletedByWidgetId[widgetId];
1103
+ conversationCompletedByWidgetId = { ...conversationCompletedByWidgetId, [widgetId]: next };
1104
+ let conversationId = resolvePersistableConversationId(widgetId, detail?.conversationId);
1105
+ console.log('[Chat] Complete toggle', widgetId.substring(0, 8), next ? 'completed' : 'active', conversationId);
1106
+ if (!conversationsClient) {
1107
+ console.warn('[Chat] Complete toggle skipped persist — missing conversations client');
1108
+ return;
1109
+ }
1110
+ try {
1111
+ if (!conversationId) {
1112
+ const created = await handleCreateConversation(widgetId, 'Widget conversation');
1113
+ conversationId = created?.id || null;
1114
+ console.log('[Chat] Complete created conversation for persist', conversationId);
1115
+ }
1116
+ if (!conversationId || !isUuid(conversationId)) {
1117
+ throw new Error('No persistable conversation id');
1118
+ }
1119
+ try {
1120
+ await conversationsClient.updateConversation({
1121
+ conversationId,
1122
+ updateConversationRequest: { completed: next },
1123
+ });
1124
+ } catch (updateError) {
1125
+ const listed = await handleLoadConversations(widgetId);
1126
+ const fallbackId = (listed || []).map((c) => c.id).find((id) => isUuid(id) && id !== conversationId);
1127
+ if (!fallbackId) throw updateError;
1128
+ console.warn('[Chat] Complete retry with loaded conversation', fallbackId);
1129
+ conversationId = fallbackId;
1130
+ await conversationsClient.updateConversation({
1131
+ conversationId,
1132
+ updateConversationRequest: { completed: next },
1133
+ });
1134
+ }
1135
+ conversationsByWidgetId = {
1136
+ ...conversationsByWidgetId,
1137
+ [widgetId]: (conversationsByWidgetId[widgetId] || []).map((c) =>
1138
+ c.id === conversationId ? { ...c, completed: next } : c,
1139
+ ),
1140
+ };
1141
+ console.log('[Chat] Conversation marked', next ? 'completed' : 'active', conversationId);
1142
+ } catch (error) {
1143
+ console.error('[Chat] Failed to persist conversation completed:', error);
1144
+ showToast('error', 'Could not update conversation status');
1145
+ conversationCompletedByWidgetId = { ...conversationCompletedByWidgetId, [widgetId]: !next };
1146
+ }
1049
1147
  }
1050
1148
 
1051
1149
  /** Sync unsent chat draft + attachments to the durable per-widget draft (or clear when empty). */
@@ -1196,12 +1294,19 @@
1196
1294
  if (compressedPersistImages.length === 0 && persistFileItems.length === 0) {
1197
1295
  console.warn('[Codegen] Persist user message has no attachment URLs for widget', widgetId?.substring(0, 8));
1198
1296
  }
1199
- const imagePersistItems = compressedPersistImages.map((url, i) => ({
1200
- url,
1201
- file_type: guessAttachmentFileType(url, 'design-screenshot.png', 'image/png'),
1202
- file_name: 'design-screenshot.png',
1203
- display_order: i + 1,
1204
- }));
1297
+ const fileHasImage = persistFileItems.some((att) => {
1298
+ const type = String(att.file_type || '');
1299
+ const url = String(att.url || '');
1300
+ return type.startsWith('image/') || url.startsWith('data:image/') || /\.(png|jpe?g|gif|webp|svg)(\?|$)/i.test(url);
1301
+ });
1302
+ const imagePersistItems = fileHasImage
1303
+ ? []
1304
+ : compressedPersistImages.map((url, i) => ({
1305
+ url,
1306
+ file_type: guessAttachmentFileType(url, 'design-screenshot.png', 'image/png'),
1307
+ file_name: 'design-screenshot.png',
1308
+ display_order: i + 1,
1309
+ }));
1205
1310
  const persistAttachments = [...persistFileItems, ...imagePersistItems].map((att, i) => ({
1206
1311
  ...att,
1207
1312
  display_order: i + 1,
@@ -1227,8 +1332,11 @@
1227
1332
  .map((a: { url?: string }) => a.url)
1228
1333
  .filter((url: string | undefined): url is string => !!url),
1229
1334
  );
1335
+ const existingHasRemoteImage = [...existingUrls].some((url) => /^https?:\/\//i.test(url));
1230
1336
  const missingPersist = persistAttachments.filter((att) => {
1231
1337
  if (att.url && existingUrls.has(att.url)) return false;
1338
+ if (existingHasRemoteImage && typeof att.url === 'string' && att.url.startsWith('data:')) return false;
1339
+ if (existingHasRemoteImage && String(att.file_type || '').startsWith('image/')) return false;
1232
1340
  return true;
1233
1341
  });
1234
1342
  let savedAttachmentCount = targetUser?.attachments?.length || 0;
@@ -1366,6 +1474,13 @@
1366
1474
  });
1367
1475
  const json = await response.raw.json();
1368
1476
  const conversations = normalizeConversationRecords(json);
1477
+ conversationsByWidgetId = { ...conversationsByWidgetId, [contextId]: conversations };
1478
+ if (conversations[0]) {
1479
+ conversationCompletedByWidgetId = {
1480
+ ...conversationCompletedByWidgetId,
1481
+ [contextId]: isConversationRecordCompleted(conversations[0]),
1482
+ };
1483
+ }
1369
1484
  console.log('[Chat→Backend] Loaded conversations for widget:', contextId, conversations.length);
1370
1485
  return conversations;
1371
1486
  } catch (error) {
@@ -1481,6 +1596,15 @@
1481
1596
  const conversation = json?.data || json;
1482
1597
  const conversationId = conversation?.id || conversation?.conversation_id || conversation?.conversationId;
1483
1598
  console.log('[Chat→Backend] Created conversation:', conversationId);
1599
+ if (conversationId && contextId) {
1600
+ conversationsByWidgetId = {
1601
+ ...conversationsByWidgetId,
1602
+ [contextId]: [
1603
+ { ...conversation, id: conversationId },
1604
+ ...(conversationsByWidgetId[contextId] || []).filter((c) => c.id !== conversationId),
1605
+ ],
1606
+ };
1607
+ }
1484
1608
  return conversationId ? { ...conversation, id: conversationId } : conversation;
1485
1609
  } catch (error) {
1486
1610
  console.error('[Chat→Backend] Failed to create conversation:', error);
@@ -1763,6 +1887,11 @@
1763
1887
  let publishingWidgetId: string | null = null;
1764
1888
  let publishWidgetStatus: string | null = null;
1765
1889
 
1890
+ $: if (isPublishing && !publishingWidgetId) {
1891
+ console.warn('[Publish] Clearing stuck isPublishing without publishingWidgetId');
1892
+ isPublishing = false;
1893
+ }
1894
+
1766
1895
  // Cross-tab operation guards — detect when another tab is generating/publishing on the same widget
1767
1896
  let isGeneratingInOtherTab = false;
1768
1897
  let isPublishingInOtherTab = false;
@@ -1979,6 +2108,50 @@
1979
2108
  return null;
1980
2109
  }
1981
2110
 
2111
+ /** If the widget only has titleFont, expose subtitleFont in Edit (same FontIC). */
2112
+ function ensureSubtitleFontInDesignSchema(schema: unknown): unknown {
2113
+ if (!schema) return schema;
2114
+ try {
2115
+ const clone = JSON.parse(JSON.stringify(schema));
2116
+ const groups = Array.isArray(clone) ? clone : clone.componentGroups;
2117
+ if (!Array.isArray(groups) || groups.length === 0) return schema;
2118
+ const allProps = groups.flatMap((group: { components?: Array<{ properties?: unknown[] }> }) =>
2119
+ (group.components || []).flatMap((component) => component.properties || [])
2120
+ );
2121
+ const hasSubtitle = allProps.some((prop: { name?: string; id?: string }) =>
2122
+ /subtitleFont/i.test(String(prop?.name || prop?.id || ''))
2123
+ );
2124
+ if (hasSubtitle) return clone;
2125
+ const titleProp = allProps.find((prop: { name?: string; id?: string; inputController?: { type?: string } }) => {
2126
+ const key = String(prop?.name || prop?.id || '');
2127
+ const type = String(prop?.inputController?.type || '').toLowerCase();
2128
+ return type === 'font' && /titleFont|^font$/i.test(key);
2129
+ });
2130
+ if (!titleProp || typeof titleProp !== 'object') return clone;
2131
+ const firstComponent = groups[0]?.components?.[0];
2132
+ if (!firstComponent) return clone;
2133
+ firstComponent.properties = Array.isArray(firstComponent.properties) ? firstComponent.properties : [];
2134
+ const subtitleProp = {
2135
+ ...JSON.parse(JSON.stringify(titleProp)),
2136
+ id: 'subtitleFont',
2137
+ name: 'subtitleFont',
2138
+ description: 'Subtitle Font',
2139
+ };
2140
+ const titleIndex = firstComponent.properties.findIndex(
2141
+ (prop: { name?: string; id?: string }) =>
2142
+ (prop?.name || prop?.id) === ((titleProp as { name?: string; id?: string }).name
2143
+ || (titleProp as { name?: string; id?: string }).id)
2144
+ );
2145
+ if (titleIndex >= 0) firstComponent.properties.splice(titleIndex + 1, 0, subtitleProp);
2146
+ else firstComponent.properties.push(subtitleProp);
2147
+ console.log('[CompositionEditor] Added subtitleFont FontIC next to titleFont');
2148
+ return Array.isArray(schema) ? clone : clone;
2149
+ } catch (error) {
2150
+ console.warn('[CompositionEditor] subtitleFont schema inject skipped:', error);
2151
+ return schema;
2152
+ }
2153
+ }
2154
+
1982
2155
  function normalizeContentSchemaFromApiResponse(contentRes: unknown): any {
1983
2156
  if (!contentRes) return null;
1984
2157
  const payload = (contentRes as { data?: unknown })?.data ?? contentRes;
@@ -2108,14 +2281,20 @@
2108
2281
  }
2109
2282
 
2110
2283
  /** Load design & content schemas for a widget — per-panel; never wipes other panels. */
2111
- async function loadWidgetSchemas(widgetId: string): Promise<void> {
2284
+ async function loadWidgetSchemas(widgetId: string, options?: { force?: boolean }): Promise<void> {
2112
2285
  if (!widgetsClient || !hasValidToken) {
2113
2286
  patchPanelSnapshot(widgetId, { compositionSchemasLoading: false, schemaLoadStartedAt: null });
2114
2287
  return;
2115
2288
  }
2116
2289
 
2290
+ if (options?.force) {
2291
+ widgetSchemaCache.delete(widgetId);
2292
+ cancelPanelSchemaLoad(widgetId);
2293
+ abandonSchemaLoadAttempt(widgetId);
2294
+ }
2295
+
2117
2296
  const existingLoad = schemaLoadPromises.get(widgetId);
2118
- if (existingLoad) {
2297
+ if (existingLoad && !options?.force) {
2119
2298
  const snap = getPanelState(widgetId);
2120
2299
  if (!isStalePanelSchemaLoading(widgetId, snap)) return existingLoad;
2121
2300
  cancelPanelSchemaLoad(widgetId);
@@ -2131,7 +2310,7 @@
2131
2310
  || existing.compositionContentSchema
2132
2311
  );
2133
2312
 
2134
- if (hasCachedSchemas) {
2313
+ if (hasCachedSchemas && !options?.force) {
2135
2314
  const designSchema = cached?.design ?? existing.compositionDesignSchema;
2136
2315
  const contentSchema = cached?.content ?? existing.compositionContentSchema;
2137
2316
  const schemaError = cached?.error ?? existing.compositionSchemasError;
@@ -2226,7 +2405,9 @@
2226
2405
  'Content schema',
2227
2406
  );
2228
2407
 
2229
- designSchema = normalizeDesignSchemaFromApiResponse(designRes);
2408
+ designSchema = ensureSubtitleFontInDesignSchema(
2409
+ normalizeDesignSchemaFromApiResponse(designRes)
2410
+ );
2230
2411
  contentSchema = normalizeContentSchemaFromApiResponse(contentRes);
2231
2412
 
2232
2413
  if (!designSchema && !contentSchema) {
@@ -2330,7 +2511,7 @@
2330
2511
  } else {
2331
2512
  patchPanelSnapshot(widgetId, { liveDesignValues: defaults });
2332
2513
  }
2333
- getWidgetDetailsRef(widgetId)?.sendMessageToPreview({ type: 'widgetic:update', design: defaults });
2514
+ getWidgetDetailsRef(widgetId)?.sendMessageToPreview({ type: 'widgetic:update', design: flattenDesignValuesForPreview(defaults) });
2334
2515
  console.log('[Create Preview] Applied design schema defaults:', Object.keys(defaults));
2335
2516
  }
2336
2517
  const contentSchema = getPanelDisplayState(widgetId).compositionContentSchema;
@@ -2468,15 +2649,22 @@
2468
2649
  const versionedUrl = `${cdnBase}/widgets/${widgetId}/v${version}/widget.html`;
2469
2650
 
2470
2651
  if (jsPath) {
2652
+ const trimmed = jsPath.replace(/\/+$/, '');
2653
+ let resolved = trimmed
2654
+ .replace(/\/widget\.js$/i, '/widget.html')
2655
+ .replace(/\.js$/i, '.html');
2656
+ if (!/\.html$/i.test(resolved)) {
2657
+ resolved = `${resolved}/widget.html`;
2658
+ }
2659
+ if (resolved.startsWith('/')) {
2660
+ resolved = `${cdnBase}${resolved}`;
2661
+ }
2471
2662
  const jsPathVersionMatch = jsPath.match(/\/v(\d+)(?:\/|$)/);
2472
2663
  const jsPathVersion = jsPathVersionMatch ? parseInt(jsPathVersionMatch[1], 10) : null;
2473
- if (jsPathVersion === version) {
2474
- const trimmed = jsPath.replace(/\/+$/, '');
2475
- let resolved = /\.html$/i.test(trimmed) ? trimmed : `${trimmed}/widget.html`;
2476
- // Bare pathname (e.g. from older saves) must be resolved against CDN, not localhost
2477
- if (resolved.startsWith('/')) {
2478
- resolved = `${cdnBase}${resolved}`;
2479
- }
2664
+ const jsPathWidgetId = (resolved.match(/\/widgets\/([^/]+)\//i) || [])[1] || null;
2665
+ const pointsAtOtherWidget = !!(jsPathWidgetId && jsPathWidgetId !== widgetId);
2666
+ if (pointsAtOtherWidget || jsPathVersion === version || /\/widgets\//i.test(resolved)) {
2667
+ console.log('[Publish] Using js_path artifact URL', resolved, pointsAtOtherWidget ? '(source widget)' : '');
2480
2668
  return resolved;
2481
2669
  }
2482
2670
  }
@@ -2507,6 +2695,68 @@
2507
2695
  let suppressNextBuildFromRepoChatMessage = false;
2508
2696
  let _isRestoringCheckpoint = false;
2509
2697
 
2698
+ function mapFontWeightToCss(weight: string | undefined): string {
2699
+ const name = String(weight || 'Regular').toLowerCase().replace(/[\s_-]/g, '');
2700
+ const table: Record<string, string> = {
2701
+ thin: '100', hairline: '100',
2702
+ extralight: '200', ultralight: '200',
2703
+ light: '300',
2704
+ regular: '400', normal: '400',
2705
+ medium: '500',
2706
+ semibold: '600', demibold: '600',
2707
+ bold: '700',
2708
+ extrabold: '800', ultrabold: '800',
2709
+ black: '900', heavy: '900',
2710
+ };
2711
+ if (/^\d{3}$/.test(name)) return name;
2712
+ return table[name] || '400';
2713
+ }
2714
+
2715
+ function mapAspectRatioToCss(raw: string): string {
2716
+ const key = raw.trim().toLowerCase();
2717
+ const table: Record<string, string> = {
2718
+ square: '1/1',
2719
+ '1:1': '1/1',
2720
+ '1/1': '1/1',
2721
+ landscape: '4/3',
2722
+ '4:3': '4/3',
2723
+ '4/3': '4/3',
2724
+ wide: '16/9',
2725
+ '16:9': '16/9',
2726
+ '16/9': '16/9',
2727
+ portrait: '3/4',
2728
+ '3:4': '3/4',
2729
+ '3/4': '3/4',
2730
+ };
2731
+ return table[key] || raw;
2732
+ }
2733
+
2734
+ /** CSS custom properties for widgetic:update — FontIC objects and dropdown {value} become strings. */
2735
+ function flattenDesignValuesForPreview(design: Record<string, unknown>): Record<string, string> {
2736
+ const out: Record<string, string> = {};
2737
+ for (const [key, value] of Object.entries(design || {})) {
2738
+ if (value && typeof value === 'object' && 'family' in (value as Record<string, unknown>)) {
2739
+ const font = value as { family?: string; weight?: string };
2740
+ const family = String(font.family || 'Inter');
2741
+ out[key] = `"${family}", sans-serif`;
2742
+ out[`${key}Family`] = family;
2743
+ out[`${key}Weight`] = mapFontWeightToCss(font.weight);
2744
+ continue;
2745
+ }
2746
+ if (value && typeof value === 'object' && 'value' in (value as Record<string, unknown>)) {
2747
+ out[key] = mapAspectRatioToCss(String((value as { value?: unknown }).value ?? ''));
2748
+ continue;
2749
+ }
2750
+ if (typeof value === 'string') {
2751
+ out[key] = /aspect/i.test(key) ? mapAspectRatioToCss(value) : value;
2752
+ continue;
2753
+ }
2754
+ if (value == null) continue;
2755
+ out[key] = String(value);
2756
+ }
2757
+ return out;
2758
+ }
2759
+
2510
2760
  /** Handle property change from PropsEditor — send to widget preview iframe */
2511
2761
  function handleEditorPropertyChange(event: CustomEvent<{ schema: string; propertyId: string; value: unknown; opacity?: number; itemId?: string }>) {
2512
2762
  const { schema, propertyId, value, itemId } = event.detail;
@@ -2516,7 +2766,7 @@
2516
2766
  liveDesignValues = { ...liveDesignValues, [propertyId]: value };
2517
2767
  getWidgetDetailsRef(focusedPanelWidgetId)?.sendMessageToPreview({
2518
2768
  type: 'widgetic:update',
2519
- design: liveDesignValues
2769
+ design: flattenDesignValuesForPreview(liveDesignValues)
2520
2770
  });
2521
2771
  } else if (schema === 'content') {
2522
2772
  if (itemId) {
@@ -2699,11 +2949,16 @@
2699
2949
 
2700
2950
  /** Push composition overlay to a specific panel preview iframe. */
2701
2951
  function applyPanelCompositionToPreview(widgetId: string): void {
2702
- const ps = getPanelDisplayState(widgetId);
2703
2952
  const panelRef = getWidgetDetailsRef(widgetId);
2704
2953
  if (!panelRef) return;
2954
+ const step = widgetId === focusedPanelWidgetId ? widgetDetailsStep : 'create';
2955
+ if (step === 'create') {
2956
+ console.log('[Preview] Skip composition overlay on Create step', widgetId?.substring(0, 8));
2957
+ return;
2958
+ }
2959
+ const ps = getPanelDisplayState(widgetId);
2705
2960
  if (Object.keys(ps.liveDesignValues).length > 0) {
2706
- panelRef.sendMessageToPreview({ type: 'widgetic:update', design: ps.liveDesignValues });
2961
+ panelRef.sendMessageToPreview({ type: 'widgetic:update', design: flattenDesignValuesForPreview(ps.liveDesignValues as Record<string, unknown>) });
2707
2962
  }
2708
2963
  if (Object.keys(ps.liveContentValues).length > 0) {
2709
2964
  panelRef.sendMessageToPreview({ type: 'widgetic:update', content: ps.liveContentValues });
@@ -2965,6 +3220,10 @@
2965
3220
  /** Send current composition design/content values to the preview iframe via postMessage.
2966
3221
  * Called on: composition select, iframe ready, and step switch to Edit/Embed. */
2967
3222
  function applyCompositionToPreview() {
3223
+ if (widgetDetailsStep === 'create') {
3224
+ console.log('[Preview] Skip composition overlay on Create step');
3225
+ return;
3226
+ }
2968
3227
  if (Object.keys(liveDesignValues).length > 0) {
2969
3228
  widgetDetails?.sendMessageToPreview({ type: 'widgetic:update', design: liveDesignValues });
2970
3229
  }
@@ -3438,19 +3697,21 @@
3438
3697
  }
3439
3698
 
3440
3699
  async function handleHostDuplicateWidgetEvent(): Promise<void> {
3441
- const widgetId = resolveHostTargetWidgetId();
3442
- if (!widgetId || !canvasRef) {
3700
+ const widgetId = canvasPrimaryWidgetId || resolveHostTargetWidgetId();
3701
+ if (!widgetId || isDraftConvertId(widgetId)) {
3443
3702
  showToast('warning', 'Select a widget first', {
3444
- description: 'Click the widget on the canvas, then Duplicate.',
3703
+ description: 'Duplicate from the header copies the widget in the title.',
3445
3704
  });
3446
3705
  return;
3447
3706
  }
3448
- canvasRef.selectWidgetShape?.(widgetId);
3449
- if (typeof canvasRef.duplicateSelectedObjects === 'function') {
3450
- await canvasRef.duplicateSelectedObjects();
3451
- return;
3707
+ console.log('[CreatorApp] Host Duplicate — primary widget', widgetId.substring(0, 8));
3708
+ if (canvasRef?.selectWidgetShape && typeof canvasRef.duplicateSelectedObjects === 'function') {
3709
+ if (canvasRef.selectWidgetShape(widgetId)) {
3710
+ await canvasRef.duplicateSelectedObjects();
3711
+ return;
3712
+ }
3452
3713
  }
3453
- showToast('error', 'Cannot duplicate widget', { description: 'Canvas duplicate is not available' });
3714
+ await handleWidgetShapeDuplicated(widgetId, null);
3454
3715
  }
3455
3716
 
3456
3717
  async function handleHostDeleteWidgetEvent(): Promise<void> {
@@ -3470,6 +3731,15 @@
3470
3731
  }
3471
3732
  }
3472
3733
 
3734
+ function emitCreatorSaveState(dirty: boolean): void {
3735
+ if (typeof window === 'undefined' || !embedded) return;
3736
+ window.dispatchEvent(new CustomEvent('widgetic:creator-save-state', { detail: { dirty } }));
3737
+ }
3738
+
3739
+ function handleHostSaveCanvasEvent(): void {
3740
+ canvasRef?.saveNow?.();
3741
+ }
3742
+
3473
3743
  async function persistPrimaryWidgetId(widgetId: string) {
3474
3744
  if (!widgetId) return;
3475
3745
  canvasPrimaryWidgetId = widgetId;
@@ -3485,7 +3755,6 @@
3485
3755
  }
3486
3756
  }
3487
3757
  const widget = getWidgetById(widgetId);
3488
- if (widget) notifyHostWidgetName(widget);
3489
3758
  if (embedded && typeof window !== 'undefined') {
3490
3759
  try {
3491
3760
  const url = new URL(window.location.href);
@@ -3497,12 +3766,110 @@
3497
3766
  console.warn('[CreatorApp] Failed to sync primary widget_id in URL:', err);
3498
3767
  }
3499
3768
  }
3769
+ if (widget) notifyHostWidgetName(widget);
3770
+ emitCanvasWidgetsList('primary-changed');
3771
+ }
3772
+
3773
+ function emitCanvasWidgetsList(reason: string) {
3774
+ if (!embedded || typeof window === 'undefined') return;
3775
+ const ids = canvasRef?.getLinkedWidgetIds?.() ?? linkedWidgetIds ?? new Set<string>();
3776
+ const items: { id: string; name: string }[] = [];
3777
+ for (const id of ids) {
3778
+ const widget = getWidgetById(id);
3779
+ items.push({
3780
+ id,
3781
+ name: (widget?.name || '').trim() || 'Untitled Widget'
3782
+ });
3783
+ }
3784
+ if (canvasPrimaryWidgetId && !items.some((item) => item.id === canvasPrimaryWidgetId)) {
3785
+ const primary = getWidgetById(canvasPrimaryWidgetId);
3786
+ if (primary?.id) {
3787
+ items.unshift({
3788
+ id: primary.id,
3789
+ name: (primary.name || '').trim() || 'Untitled Widget'
3790
+ });
3791
+ }
3792
+ }
3793
+ const primaryWidgetId = canvasPrimaryWidgetId || '';
3794
+ console.log('[CreatorApp] Emit canvas widgets for header', reason, items.length, primaryWidgetId);
3795
+ window.dispatchEvent(new CustomEvent(CREATOR_CANVAS_WIDGETS_EVENT, {
3796
+ detail: { widgets: items, primaryWidgetId }
3797
+ }));
3798
+ }
3799
+
3800
+ function handleHostRequestCanvasWidgetsEvent() {
3801
+ emitCanvasWidgetsList('host-request');
3802
+ }
3803
+
3804
+ function handleHostSetPrimaryWidgetEvent(event: Event) {
3805
+ const widgetId = (event as CustomEvent).detail?.widgetId;
3806
+ if (!widgetId || typeof widgetId !== 'string') {
3807
+ console.warn('[CreatorApp] Host set primary ignored — missing widgetId');
3808
+ return;
3809
+ }
3810
+ console.log('[CreatorApp] Host set primary widget', widgetId.substring(0, 8));
3811
+ canvasRef?.setPrimaryWidgetShape?.(widgetId);
3812
+ canvasRef?.selectWidgetShape?.(widgetId);
3813
+ void persistPrimaryWidgetId(widgetId);
3500
3814
  }
3501
3815
 
3502
3816
  function handleSetPrimaryWidget(widgetId: string) {
3503
3817
  void persistPrimaryWidgetId(widgetId);
3504
3818
  }
3505
3819
 
3820
+ async function hydrateCanvasPrimaryWidget(linkedIds?: string[]): Promise<string | null> {
3821
+ const targetCanvasId = (typeof canvasId === 'string' && canvasId.trim()) ? canvasId.trim() : null;
3822
+ if (!targetCanvasId) return canvasPrimaryWidgetId || null;
3823
+ let primaryId = await resolvePrimaryWidgetIdForCanvas(targetCanvasId);
3824
+ if (!primaryId) {
3825
+ const fromLinks = (linkedIds || []).filter((id) => id && !isDraftConvertId(id) && !String(id).startsWith('draft'));
3826
+ const fromList = widgets
3827
+ .filter((w: any) => {
3828
+ const cid = w.canvasId || w.canvas_id;
3829
+ return cid === targetCanvasId && w.id && !isDraftConvertId(w.id);
3830
+ })
3831
+ .map((w: any) => w.id);
3832
+ primaryId = fromLinks[0] || fromList[0] || null;
3833
+ if (primaryId) {
3834
+ console.log('[CreatorApp] Healing empty primary_widget_id →', primaryId.substring(0, 8));
3835
+ await persistPrimaryWidgetId(primaryId);
3836
+ }
3837
+ }
3838
+ if (primaryId) {
3839
+ const widget = getWidgetById(primaryId);
3840
+ if (widget) notifyHostWidgetName(widget);
3841
+ emitCanvasWidgetsList('hydrate-primary');
3842
+ }
3843
+ return primaryId || canvasPrimaryWidgetId || null;
3844
+ }
3845
+
3846
+ async function handleHostMoveCanvasFolderEvent(event: Event) {
3847
+ const folderId = (event as CustomEvent).detail?.folderId;
3848
+ if (!folderId || typeof folderId !== 'string' || !widgetsClient) return;
3849
+ const targetCanvasId = (typeof canvasId === 'string' && canvasId.trim()) ? canvasId.trim() : null;
3850
+ let primaryId = canvasPrimaryWidgetId || (targetCanvasId ? await resolvePrimaryWidgetIdForCanvas(targetCanvasId) : null);
3851
+ if (!primaryId) {
3852
+ primaryId = await hydrateCanvasPrimaryWidget();
3853
+ }
3854
+ const ids = new Set<string>();
3855
+ if (primaryId) ids.add(primaryId);
3856
+ for (const w of widgets) {
3857
+ const cid = (w as any).canvasId || (w as any).canvas_id;
3858
+ if (targetCanvasId && cid === targetCanvasId && w.id) ids.add(w.id);
3859
+ }
3860
+ console.log('[CreatorApp] Move canvas widgets to folder', folderId, [...ids].map((id) => id.substring(0, 8)));
3861
+ for (const id of ids) {
3862
+ try {
3863
+ await widgetsClient.updateWidget({
3864
+ id,
3865
+ updateWidgetRequest: { projectId: folderId, projectFolder: folderId }
3866
+ });
3867
+ } catch (err) {
3868
+ console.warn('[CreatorApp] Failed to move widget to folder', id.substring(0, 8), err);
3869
+ }
3870
+ }
3871
+ }
3872
+
3506
3873
  // Preview Modal State Variables
3507
3874
  let previewIframe: HTMLIFrameElement | null = null;
3508
3875
  const { DEV: isDevMode } = import.meta.env;
@@ -4007,18 +4374,11 @@
4007
4374
  const { loaded, widgetIds: widgetIdsOnCanvas } = await tryLoadCanvasState();
4008
4375
  await syncWidgetsWithCanvas(widgetIdsOnCanvas);
4009
4376
  await maybeAutoRestoreWidgetShapes();
4010
- // Backfill widget name + thumbnail on shapes that were saved before those
4011
- // fields existed on WidgetShape (fixes "Widget" placeholder on old canvases).
4012
4377
  backfillWidgetShapeMetadata();
4013
- if (loaded && canvasRef) {
4014
- const primaryId =
4015
- canvasPrimaryWidgetId ||
4016
- canvasRef.getPrimaryWidgetId?.() ||
4017
- selectedWidgetId;
4018
- if (primaryId && !isConvertOrDraftPanelOpen()) {
4019
- canvasRef.setPrimaryWidgetShape?.(primaryId);
4020
- canvasRef.selectWidgetShape(primaryId);
4021
- }
4378
+ const primaryId = await hydrateCanvasPrimaryWidget(widgetIdsOnCanvas);
4379
+ if (loaded && canvasRef && primaryId && !isConvertOrDraftPanelOpen()) {
4380
+ canvasRef.setPrimaryWidgetShape?.(primaryId);
4381
+ canvasRef.selectWidgetShape(primaryId);
4022
4382
  }
4023
4383
  } catch (error) {
4024
4384
  console.error('Widget Creator: Load canvases error:', error);
@@ -5282,20 +5642,19 @@
5282
5642
  }
5283
5643
  }
5284
5644
 
5285
- /** After hydrate, cross-check GitLab HEAD if we don't have a fresh local commit.
5286
- * Widget: if HEAD sha differs from the local lastCommitId, promote it so the Edit banner
5287
- * ("Newer code than Live") can detect unpublished code even without recent codegen locally. */
5645
+ /** After hydrate, probe GitLab HEAD only when this widget has no codegen commit yet.
5646
+ * Do not promote HEAD over lastCommitId — that marks a new publish version on every Details open. */
5288
5647
  async function reconcileWidgetHeadCommit(widgetId: string): Promise<void> {
5289
5648
  const currentPanelLastCommit = getPanelState(widgetId).lastCommitId;
5290
- const publishSha = getWidgetLastPublishCommitSha(widgetId);
5291
- // If we already have a lastCommitId that differs from the publish SHA, no probe needed.
5292
- if (currentPanelLastCommit && publishSha && !commitsMatch(currentPanelLastCommit, publishSha)) {
5649
+ if (currentPanelLastCommit) {
5650
+ console.log(
5651
+ `[Version] Skip HEAD probe for "${widgetId}": lastCommitId already ${currentPanelLastCommit.substring(0, 8)}`
5652
+ );
5293
5653
  return;
5294
5654
  }
5295
5655
  const headSha = await fetchRepoHeadCommitSha(widgetId);
5296
5656
  if (!headSha) return;
5297
- if (currentPanelLastCommit && commitsMatch(currentPanelLastCommit, headSha)) return;
5298
- console.log(`[Version] Repo HEAD probe for "${widgetId}": ${headSha.substring(0, 8)} (was ${currentPanelLastCommit?.substring(0, 8) ?? 'null'})`);
5657
+ console.log(`[Version] Repo HEAD probe for "${widgetId}": ${headSha.substring(0, 8)} (was null)`);
5299
5658
  patchPanelSnapshot(widgetId, { lastCommitId: headSha, hasGeneratedCode: true });
5300
5659
  saveCommitIdForWidget(widgetId, headSha);
5301
5660
  if (widgetId === focusedPanelWidgetId) {
@@ -5568,9 +5927,11 @@
5568
5927
  // Ensure we have an id
5569
5928
  const newWidget = response.data || response; // handle envelope or direct
5570
5929
  if (newWidget?.id) {
5571
- // Optimistically add to list and select
5572
5930
  widgets = [{ ...newWidget }, ...widgets];
5573
5931
  selectWidget(newWidget);
5932
+ if (!canvasPrimaryWidgetId) {
5933
+ void persistPrimaryWidgetId(newWidget.id);
5934
+ }
5574
5935
  authStatus = 'Widget created.';
5575
5936
  } else {
5576
5937
  authStatus = 'Widget created but response missing id.';
@@ -6553,16 +6914,6 @@ Requirements:
6553
6914
  : 'Preparing code generation...',
6554
6915
  });
6555
6916
 
6556
- // Persist chat attachments before repo/codegen so reopen + failed GLM still show the file.
6557
- if (!options.compileFixOnly) {
6558
- await ensureCodegenUserMessagePersisted(
6559
- generationWidgetId,
6560
- prompt,
6561
- options.promptImagesOverride,
6562
- options.persistFileAttachments,
6563
- );
6564
- }
6565
-
6566
6917
  // Keep preview tab active so user sees the "Generating..." overlay.
6567
6918
  // Console logs are still accessible via the Console tab button.
6568
6919
  getWidgetDetailsRef(generationWidgetId)?.setInlinePanelView('preview');
@@ -6929,6 +7280,11 @@ Requirements:
6929
7280
  }
6930
7281
  })();
6931
7282
  }
7283
+ if (generationWidgetId) {
7284
+ console.log('[CompositionEditor] Reloading schemas after codegen', generationWidgetId.substring(0, 8));
7285
+ void loadWidgetSchemas(generationWidgetId, { force: true });
7286
+ }
7287
+
6932
7288
  const completedRepoId = resolveRepositoryIdForWidget(generationWidgetId);
6933
7289
  if (completedRepoId) {
6934
7290
  stampWidgetRepositoryId(generationWidgetId, completedRepoId);
@@ -7160,7 +7516,11 @@ Requirements:
7160
7516
  commitSha?: string | null
7161
7517
  ): void {
7162
7518
  const jsPath = resolvePublishedJsPath(widgetId, version, artifactUrl);
7163
- const normalizedCommit = commitSha ? normalizeCommitSha(commitSha) : null;
7519
+ const prevSnap = getPanelState(widgetId);
7520
+ const normalizedCommit =
7521
+ normalizeCommitSha(commitSha)
7522
+ || normalizeCommitSha(prevSnap.lastCommitId)
7523
+ || normalizeCommitSha(loadCommitIdForWidget(widgetId));
7164
7524
 
7165
7525
  widgets = widgets.map((w) =>
7166
7526
  w.id === widgetId
@@ -7177,7 +7537,6 @@ Requirements:
7177
7537
  : w
7178
7538
  );
7179
7539
 
7180
- const prevSnap = getPanelState(widgetId);
7181
7540
  const optimisticHistoryEntry = normalizedCommit
7182
7541
  ? {
7183
7542
  version,
@@ -7208,6 +7567,9 @@ Requirements:
7208
7567
  saveCommitIdForWidget(widgetId, normalizedCommit);
7209
7568
  }
7210
7569
 
7570
+ console.log('[CompositionEditor] Reloading schemas after publish', widgetId.substring(0, 8));
7571
+ void loadWidgetSchemas(widgetId, { force: true });
7572
+
7211
7573
  if (selectedWidgetId === widgetId) {
7212
7574
  lastPublishedVersion = version;
7213
7575
  selectedWidgetJsPath = jsPath;
@@ -7402,6 +7764,16 @@ Requirements:
7402
7764
 
7403
7765
  // Restore session on mount with expiration check
7404
7766
  onMount(() => {
7767
+ const googleFontsKey =
7768
+ (typeof import.meta !== 'undefined' &&
7769
+ (import.meta.env?.PUBLIC_GOOGLE_FONTS_API_KEY || import.meta.env?.VITE_GOOGLE_FONTS_API_KEY)) ||
7770
+ '';
7771
+ if (googleFontsKey) {
7772
+ setGoogleFontsApiKey(String(googleFontsKey));
7773
+ console.log('[CreatorApp] Google Fonts API key set for FontIC');
7774
+ } else {
7775
+ console.warn('[CreatorApp] No PUBLIC_GOOGLE_FONTS_API_KEY / VITE_GOOGLE_FONTS_API_KEY — FontIC uses fallback catalog');
7776
+ }
7405
7777
  setWebsocketApiUrl(apiRoot ?? null);
7406
7778
  if (embedded) {
7407
7779
  syncSessionFromSiteAuth();
@@ -7551,6 +7923,10 @@ Requirements:
7551
7923
  window.removeEventListener(HOST_OPEN_WIDGET_DETAILS_EVENT, handleHostOpenWidgetDetailsEvent);
7552
7924
  window.removeEventListener(HOST_DUPLICATE_WIDGET_EVENT, handleHostDuplicateWidgetEvent);
7553
7925
  window.removeEventListener(HOST_DELETE_WIDGET_EVENT, handleHostDeleteWidgetEvent);
7926
+ window.removeEventListener(HOST_SAVE_CANVAS_EVENT, handleHostSaveCanvasEvent);
7927
+ window.removeEventListener(HOST_SET_PRIMARY_WIDGET_EVENT, handleHostSetPrimaryWidgetEvent);
7928
+ window.removeEventListener(HOST_REQUEST_CANVAS_WIDGETS_EVENT, handleHostRequestCanvasWidgetsEvent);
7929
+ window.removeEventListener(HOST_MOVE_CANVAS_FOLDER_EVENT, handleHostMoveCanvasFolderEvent);
7554
7930
  for (const wId of Object.keys(generatingWidgetIds)) {
7555
7931
  setCrossTabGenerating(false, wId);
7556
7932
  }
@@ -7580,9 +7956,14 @@ Requirements:
7580
7956
  window.addEventListener(HOST_OPEN_WIDGET_DETAILS_EVENT, handleHostOpenWidgetDetailsEvent);
7581
7957
  window.addEventListener(HOST_DUPLICATE_WIDGET_EVENT, handleHostDuplicateWidgetEvent);
7582
7958
  window.addEventListener(HOST_DELETE_WIDGET_EVENT, handleHostDeleteWidgetEvent);
7959
+ window.addEventListener(HOST_SAVE_CANVAS_EVENT, handleHostSaveCanvasEvent);
7960
+ window.addEventListener(HOST_SET_PRIMARY_WIDGET_EVENT, handleHostSetPrimaryWidgetEvent);
7961
+ window.addEventListener(HOST_REQUEST_CANVAS_WIDGETS_EVENT, handleHostRequestCanvasWidgetsEvent);
7962
+ window.addEventListener(HOST_MOVE_CANVAS_FOLDER_EVENT, handleHostMoveCanvasFolderEvent);
7583
7963
  emitCreatorDebugChanged();
7584
7964
  emitCreatorWireframeChanged();
7585
7965
  emitCreatorAdvancedChanged();
7966
+ emitCreatorSaveState(false);
7586
7967
  });
7587
7968
 
7588
7969
  // Capture-phase Escape: close lightbox before Canvas can deselect → close WidgetDetails
@@ -7620,8 +8001,6 @@ Requirements:
7620
8001
  if (file) {
7621
8002
  const base64 = await processImageFile(file);
7622
8003
  if (base64) {
7623
- promptImages = [...promptImages, base64];
7624
- savePromptImagesToSession();
7625
8004
  chatMethods?.addImageAttachment?.(base64, file.name || 'pasted-image.png');
7626
8005
  showToast('success', 'Image pasted to prompt', { duration: 2000 });
7627
8006
  }
@@ -8425,14 +8804,20 @@ Requirements:
8425
8804
  }
8426
8805
 
8427
8806
  function positionOpenDetailsPanel(widgetId: string): void {
8428
- let rect = canvasRef?.getWidgetShapeScreenRect?.(widgetId) ?? null;
8429
- if (!rect) {
8430
- const objectId = draftConvertObjectIds.get(widgetId);
8431
- if (objectId && canvasRef?.getObjectScreenRect) {
8432
- rect = canvasRef.getObjectScreenRect(objectId);
8807
+ const apply = () => {
8808
+ let rect = canvasRef?.getWidgetShapeScreenRect?.(widgetId) ?? null;
8809
+ if (!rect) {
8810
+ const objectId = draftConvertObjectIds.get(widgetId);
8811
+ if (objectId && canvasRef?.getObjectScreenRect) {
8812
+ rect = canvasRef.getObjectScreenRect(objectId);
8813
+ }
8433
8814
  }
8815
+ getWidgetDetailsRef(widgetId)?.positionBesideShape?.(rect);
8816
+ };
8817
+ apply();
8818
+ if (typeof requestAnimationFrame === 'function') {
8819
+ requestAnimationFrame(apply);
8434
8820
  }
8435
- getWidgetDetailsRef(widgetId)?.positionBesideShape?.(rect);
8436
8821
  }
8437
8822
 
8438
8823
  async function openWidgetDetailsPanel(widgetId: string): Promise<void> {
@@ -9235,20 +9620,11 @@ Requirements:
9235
9620
  const effectiveCanvasId = getEffectiveCanvasId();
9236
9621
  const panelIsPublishing = isPublishing && publishingWidgetId === widgetId;
9237
9622
  const anotherPanelIsPublishing = isPublishing && publishingWidgetId !== null && publishingWidgetId !== widgetId;
9238
- const stuckGlobalPublishing = isPublishing && !publishingWidgetId;
9239
9623
  const nextVersion = (ps.lastPublishedVersion ?? 0) + 1;
9240
9624
 
9241
9625
  if (panelIsPublishing) {
9242
9626
  return { disabled: true, label: 'Publishing...', reason: 'Publishing in progress…', showDisabledHint: false };
9243
9627
  }
9244
- if (stuckGlobalPublishing) {
9245
- return {
9246
- disabled: true,
9247
- label: getPublishButtonLabelFallback(ps),
9248
- reason: 'Publish state is busy — try again in a moment.',
9249
- showDisabledHint: true,
9250
- };
9251
- }
9252
9628
  if (anotherPanelIsPublishing) {
9253
9629
  return {
9254
9630
  disabled: true,
@@ -9257,7 +9633,7 @@ Requirements:
9257
9633
  showDisabledHint: true,
9258
9634
  };
9259
9635
  }
9260
- if (isPublishingInOtherTab) {
9636
+ if (checkCrossTabPublishing(widgetId)) {
9261
9637
  return {
9262
9638
  disabled: true,
9263
9639
  label: getPublishButtonLabelFallback(ps),
@@ -9319,6 +9695,16 @@ Requirements:
9319
9695
  const unpublishedAhead = hasUnpublishedChangesForPanel(widgetId)
9320
9696
  || !!(ps.lastCommitId && liveSha && !commitsMatch(ps.lastCommitId, liveSha));
9321
9697
 
9698
+ // Known up-to-date: HEAD matches live. Must win over leftover pendingPublish flags.
9699
+ if (headSha && liveSha && commitsMatch(headSha, liveSha)) {
9700
+ return {
9701
+ disabled: true,
9702
+ label: 'No new version to publish',
9703
+ reason: `Latest commit (${formatCommitShaShort(headSha)}) is already live at v${ps.lastPublishedVersion}.`,
9704
+ showDisabledHint: false,
9705
+ };
9706
+ }
9707
+
9322
9708
  if (pendingPublish || unpublishedAhead) {
9323
9709
  console.log('[Publish] Gate: unpublished HEAD — enable', widgetId.substring(0, 8), {
9324
9710
  pendingPublish,
@@ -9335,16 +9721,6 @@ Requirements:
9335
9721
  };
9336
9722
  }
9337
9723
 
9338
- // Known up-to-date: HEAD matches live.
9339
- if (headSha && liveSha && commitsMatch(headSha, liveSha)) {
9340
- return {
9341
- disabled: true,
9342
- label: 'No new version to publish',
9343
- reason: `Latest commit (${formatCommitShaShort(headSha)}) is already live at v${ps.lastPublishedVersion}.`,
9344
- showDisabledHint: false,
9345
- };
9346
- }
9347
-
9348
9724
  // After a live version exists, enable vN+1 when HEAD is proven different.
9349
9725
  if (headSha && liveSha && !commitsMatch(headSha, liveSha)) {
9350
9726
  return {
@@ -10178,8 +10554,8 @@ $: if (codeGenerationPrompt && selectedWidgetId) {
10178
10554
  }
10179
10555
  }
10180
10556
 
10181
- // Keep address-bar widget_id aligned with the shape (prod embed props are frozen at mount).
10182
- if (typeof window !== 'undefined' && embedded) {
10557
+ // Keep address-bar widget_id on the canvas primary (duplicate/select must not steal it).
10558
+ if (typeof window !== 'undefined' && embedded && canvasPrimaryWidgetId && widgetId === canvasPrimaryWidgetId) {
10183
10559
  try {
10184
10560
  const url = new URL(window.location.href);
10185
10561
  if (url.searchParams.get('widget_id') !== widgetId) {
@@ -10229,7 +10605,7 @@ $: if (codeGenerationPrompt && selectedWidgetId) {
10229
10605
  id: sourceWidgetId,
10230
10606
  duplicateWidgetRequest: { canvas_id: canvasId } as any
10231
10607
  });
10232
- const newWidget = response?.data;
10608
+ let newWidget = response?.data;
10233
10609
 
10234
10610
  if (!newWidget?.id) {
10235
10611
  console.error('[WidgetCreator] duplicateWidget returned no data', response);
@@ -10255,37 +10631,94 @@ $: if (codeGenerationPrompt && selectedWidgetId) {
10255
10631
  }
10256
10632
 
10257
10633
  // Re-link the cloned canvas shape to the new widget (also clears _isDuplicating on clone)
10258
- canvasRef?.linkObjectToWidget(clonedObject, newWidget.id, newWidget.name ?? null);
10634
+ if (clonedObject) {
10635
+ canvasRef?.linkObjectToWidget(clonedObject, newWidget.id, newWidget.name ?? null);
10636
+ } else if (canvasRef?.addWidgetShape) {
10637
+ canvasRef.addWidgetShape(newWidget.id, newWidget.name, 0, 1, null);
10638
+ }
10259
10639
  setTimeout(() => {
10260
10640
  const json = canvasRef?.saveCanvas();
10261
10641
  if (json) handleCanvasAutoSave(json);
10262
10642
  }, 500);
10263
10643
 
10264
- // Add to beginning of list (newest first) and auto-select
10265
10644
  widgets = [{ ...newWidget }, ...widgets];
10266
10645
 
10267
- // Set flag BEFORE selectWidget so loadWidget skips the build-from-repo attempt
10268
- // (the fork isn't ready yet — we handle it below after a delay)
10269
- pendingDuplicateBuildWidgetId = newWidget.id;
10270
- selectWidget(newWidget);
10646
+ const sourceWidget = getWidgetById(sourceWidgetId);
10647
+ const sourceJsPath =
10648
+ ((sourceWidget as { jsPath?: string | null; js_path?: string | null } | null)?.jsPath
10649
+ ?? (sourceWidget as { js_path?: string | null } | null)?.js_path)
10650
+ ?? null;
10651
+ const duplicateJsPath =
10652
+ sourceJsPath
10653
+ ?? ((newWidget as { jsPath?: string | null; js_path?: string | null }).jsPath
10654
+ ?? (newWidget as { js_path?: string | null }).js_path);
10655
+ if (sourceWidget && duplicateJsPath) {
10656
+ Object.assign(newWidget, {
10657
+ jsPath: duplicateJsPath,
10658
+ js_path: duplicateJsPath,
10659
+ version: (newWidget as { version?: number }).version ?? (sourceWidget as { version?: number }).version,
10660
+ isDraft: (newWidget as { isDraft?: boolean }).isDraft
10661
+ ?? (sourceWidget as { isDraft?: boolean; is_draft?: boolean }).isDraft
10662
+ ?? (sourceWidget as { is_draft?: boolean }).is_draft,
10663
+ lastCpgCommitSha:
10664
+ (newWidget as { lastCpgCommitSha?: string | null }).lastCpgCommitSha
10665
+ ?? (sourceWidget as { lastCpgCommitSha?: string | null }).lastCpgCommitSha,
10666
+ lastPublishCommitSha:
10667
+ (newWidget as { lastPublishCommitSha?: string | null }).lastPublishCommitSha
10668
+ ?? (sourceWidget as { lastPublishCommitSha?: string | null; last_publish_commit_sha?: string | null }).lastPublishCommitSha
10669
+ ?? (sourceWidget as { last_publish_commit_sha?: string | null }).last_publish_commit_sha,
10670
+ });
10671
+ widgets = widgets.map((w) => (w.id === newWidget.id ? { ...newWidget } : w));
10672
+ }
10673
+
10674
+ const sourceCommit = loadCommitIdForWidget(sourceWidgetId);
10675
+ if (sourceCommit) {
10676
+ saveCommitIdForWidget(newWidget.id, sourceCommit);
10677
+ }
10678
+
10679
+ const sourceHasCode =
10680
+ !!normalizeRepositoryId(newWidget)
10681
+ || !!((newWidget as { jsPath?: string | null; js_path?: string | null }).jsPath
10682
+ ?? (newWidget as { js_path?: string | null }).js_path)
10683
+ || resolvePublishedVersionFromWidget(newWidget) !== null
10684
+ || !!sourceCommit;
10685
+
10686
+ if (!canvasPrimaryWidgetId) {
10687
+ canvasPrimaryWidgetId = sourceWidgetId;
10688
+ }
10689
+
10690
+ // Duplicate stays a sibling on the canvas — do not make it the primary widget.
10691
+ await selectWidget(newWidget, { skipPlaceShape: true });
10692
+ notifyHostWidgetName(getWidgetById(canvasPrimaryWidgetId) ?? getWidgetById(sourceWidgetId));
10271
10693
 
10272
- const newRepoId = normalizeRepositoryId(newWidget);
10273
10694
  showToast('success', 'Widget duplicated', {
10274
- description: `"${newWidget.name}" created — compiling code…`
10695
+ description: sourceHasCode
10696
+ ? `"${newWidget.name}" created with existing code`
10697
+ : `"${newWidget.name}" created`
10275
10698
  });
10276
10699
 
10277
- // Re-fetch widget from API to ensure repo_id and other fields are current
10278
10700
  setTimeout(() => handleWidgetDataChanged(newWidget.id), 1500);
10279
10701
 
10280
- // Wait for GitLab fork to complete, then compile from repo
10281
- // GitLab forks are async — files need time to be copied
10702
+ if (sourceHasCode) {
10703
+ await openWidgetDetailsPanel(newWidget.id);
10704
+ await tick();
10705
+ getWidgetDetailsRef(newWidget.id)?.startPreviewLoadingPublic();
10706
+ try {
10707
+ await getWidgetDetailsRef(newWidget.id)?.retryPreviewIfEmpty?.();
10708
+ } catch (previewErr) {
10709
+ console.warn('[WidgetCreator] Duplicate preview fallback failed:', previewErr);
10710
+ }
10711
+ return;
10712
+ }
10713
+
10714
+ pendingDuplicateBuildWidgetId = newWidget.id;
10282
10715
  setTimeout(async () => {
10283
10716
  if (selectedWidgetId !== newWidget.id) return;
10284
10717
  pendingDuplicateBuildWidgetId = null;
10285
10718
  try {
10286
10719
  await handleWidgetDataChanged(newWidget.id);
10287
10720
  const freshWidget = getWidgetById(newWidget.id);
10288
- let repoId = freshWidget ? normalizeRepositoryId(freshWidget) : newRepoId;
10721
+ let repoId = freshWidget ? normalizeRepositoryId(freshWidget) : normalizeRepositoryId(newWidget);
10289
10722
 
10290
10723
  if (!repoId) {
10291
10724
  console.log('[WidgetCreator] Duplicate has no repo after re-fetch, creating one...');
@@ -10333,6 +10766,8 @@ $: if (codeGenerationPrompt && selectedWidgetId) {
10333
10766
 
10334
10767
  if (!result.proceed) return false;
10335
10768
 
10769
+ closeWidgetDetailsPanel(widgetId);
10770
+
10336
10771
  if (result.deleteFromDb) {
10337
10772
  await deleteWidgetFromDb(widgetId);
10338
10773
  } else {
@@ -10360,7 +10795,8 @@ $: if (codeGenerationPrompt && selectedWidgetId) {
10360
10795
  if (selectedWidget?.id === widgetId) {
10361
10796
  selectedWidget = { ...selectedWidget, name: newName };
10362
10797
  }
10363
- onWidgetRenamed?.({ widgetId, name: newName });
10798
+ notifyHostWidgetName({ id: widgetId, name: newName });
10799
+ emitCanvasWidgetsList('widget-renamed');
10364
10800
  showToast('success', `Widget renamed to "${newName}"`);
10365
10801
  } catch (error) {
10366
10802
  console.error('[WidgetCreator] Failed to rename widget:', error);
@@ -10407,10 +10843,7 @@ $: if (codeGenerationPrompt && selectedWidgetId) {
10407
10843
  }
10408
10844
  await syncWidgetsWithCanvas(widgetIdsOnCanvas);
10409
10845
  await maybeAutoRestoreWidgetShapes();
10410
- const primaryId =
10411
- canvasPrimaryWidgetId ||
10412
- canvasRef.getPrimaryWidgetId?.() ||
10413
- selectedWidgetId;
10846
+ const primaryId = await hydrateCanvasPrimaryWidget(widgetIdsOnCanvas);
10414
10847
  if (primaryId && canvasRef && !isConvertOrDraftPanelOpen()) {
10415
10848
  canvasRef.setPrimaryWidgetShape?.(primaryId);
10416
10849
  const found = canvasRef.selectWidgetShape(primaryId);
@@ -10607,6 +11040,7 @@ $: if (codeGenerationPrompt && selectedWidgetId) {
10607
11040
  const widgetIdsOnCanvas = extractWidgetIdsFromCanvasContent(parsed);
10608
11041
  await canvasRef.loadCanvas(saved);
10609
11042
  markCanvasStateLoaded();
11043
+ canvasApiLoadError = null;
10610
11044
  isLoadCooldownActive = true;
10611
11045
  setTimeout(() => { isLoadCooldownActive = false; }, 3000);
10612
11046
  lastSavedCanvasObjectCount = objectCount;
@@ -10690,6 +11124,7 @@ $: if (codeGenerationPrompt && selectedWidgetId) {
10690
11124
 
10691
11125
  lastSavedCanvasObjectCount = objectCount;
10692
11126
  try { sessionStorage.setItem(CANVAS_OBJ_COUNT_KEY, String(objectCount)); } catch {}
11127
+ emitCreatorSaveState(false);
10693
11128
 
10694
11129
  // Save to durable L1 fallback (always, even with 0 objects for legitimate clears)
10695
11130
  saveCanvasState(canvasId, json);
@@ -10928,6 +11363,13 @@ $: if (codeGenerationPrompt && selectedWidgetId) {
10928
11363
  // Add to widgets list so getWidgetById resolves the real widget from now on.
10929
11364
  widgets = [{ ...newWidget }, ...widgets.filter((w) => w.id !== draftId)];
10930
11365
 
11366
+ if (canvasId) {
11367
+ const existingPrimary = await resolvePrimaryWidgetIdForCanvas(canvasId);
11368
+ if (!existingPrimary) {
11369
+ await persistPrimaryWidgetId(widgetId);
11370
+ }
11371
+ }
11372
+
10931
11373
  // Replace the grouped sketch with a WidgetShape screenshot. On failure
10932
11374
  // fall back to linking the sketch object to the widget id.
10933
11375
  if (canvasRef && canvasObjectId) {
@@ -11560,6 +12002,7 @@ $: if (codeGenerationPrompt && selectedWidgetId) {
11560
12002
  }}
11561
12003
  onConvertToWidget={handleConvertToWidget}
11562
12004
  onWidgetImageClick={handleWidgetImageClick}
12005
+ onCanvasDirty={() => emitCreatorSaveState(true)}
11563
12006
  onWidgetShapeDuplicated={handleWidgetShapeDuplicated}
11564
12007
  onWidgetShapeDeleting={handleWidgetShapeDeleting}
11565
12008
  onWidgetRename={handleWidgetRename}
@@ -11998,21 +12441,15 @@ $: if (codeGenerationPrompt && selectedWidgetId) {
11998
12441
  detail.repoHeadCommitSha ||
11999
12442
  detail.repo_head_commit_sha ||
12000
12443
  null;
12001
- const hasSha = typeof repoHead === 'string' && repoHead.length > 0;
12002
- if (hasSha) {
12003
- saveCommitIdForWidget(detail.widgetId, repoHead);
12004
- }
12005
- // Always persist hasGeneratedCode on the snapshot so Publish re-renders
12006
- // even when Worker cache hits without a SHA and the panel is unfocused.
12444
+ // Rebuild is the same saved tree — do not promote GitLab HEAD into lastCommitId
12445
+ // or Publish will offer vN+1 without a new codegen commit.
12007
12446
  patchPanelSnapshot(detail.widgetId, {
12008
12447
  panelPreviewCompileError: null,
12009
12448
  hasGeneratedCode: true,
12010
- ...(hasSha ? { lastCommitId: repoHead } : {}),
12011
12449
  });
12012
12450
  if (detail.widgetId === panelWidgetId) {
12013
12451
  hasGeneratedCode = true;
12014
- if (hasSha) lastCommitId = repoHead;
12015
- if (!hasSha) {
12452
+ if (!getPanelState(detail.widgetId).lastCommitId && !loadCommitIdForWidget(detail.widgetId)) {
12016
12453
  void reconcileWidgetHeadCommit(detail.widgetId);
12017
12454
  }
12018
12455
  if (isPanelFocused && generateCodeStatus?.includes('preview build failed')) {
@@ -12273,6 +12710,8 @@ $: if (codeGenerationPrompt && selectedWidgetId) {
12273
12710
  onRetryMessage={(content) => handleChatRetryMessage(content, panelWidgetId)}
12274
12711
  onUploadChatFile={handleChatUploadFile}
12275
12712
  onTakeScreenshot={() => handleChatScreenshot(panelWidgetId)}
12713
+ isCompleted={!!conversationCompletedByWidgetId[panelWidgetId]}
12714
+ onCompletionToggle={(detail) => handleChatCompletionToggle(panelWidgetId, detail)}
12276
12715
  onLoadConversations={handleLoadConversations}
12277
12716
  onLoadMessages={(conversationId) => handleLoadMessages(conversationId, panelWidgetId)}
12278
12717
  onCreateConversation={handleCreateConversation}
@@ -12384,8 +12823,8 @@ class={`publish-widget-btn flex justify-center items-center border text-white w-
12384
12823
  ${isPublishing && publishingWidgetId === panelWidgetId
12385
12824
  ? 'bg-emerald-500 border-emerald-500 opacity-80 cursor-not-allowed'
12386
12825
  : isPublishDisabledForPanel(panelWidgetId)
12387
- ? 'bg-emerald-600 border-emerald-600 opacity-35 cursor-not-allowed'
12388
- : 'bg-emerald-600 border-emerald-600 hover:bg-emerald-700 hover:border-emerald-700 cursor-pointer'
12826
+ ? 'bg-emerald-600 border-emerald-600 opacity-40 cursor-not-allowed'
12827
+ : '!opacity-100 bg-emerald-600 border-emerald-600 hover:bg-emerald-700 hover:border-emerald-700 cursor-pointer'
12389
12828
  } rounded-lg`}
12390
12829
  >
12391
12830
  {#if isPublishing && publishingWidgetId === panelWidgetId && publishWidgetStatus === 'Building...'}
@@ -12408,9 +12847,9 @@ class={`publish-widget-btn flex justify-center items-center border text-white w-
12408
12847
  {/if}
12409
12848
  </Button>
12410
12849
 
12411
- {#if isPublishDisabledForPanel(panelWidgetId) && shouldShowPublishDisabledHintForPanel(panelWidgetId)}
12850
+ {#if isPublishDisabledForPanel(panelWidgetId)}
12412
12851
  <p class="publish-widget-disabled-hint text-[10px] text-center text-emerald-800/80 leading-snug px-1">
12413
- {getPublishDisabledReasonForPanel(panelWidgetId)}
12852
+ {getPublishDisabledReasonForPanel(panelWidgetId) || getPublishButtonLabelForPanel(panelWidgetId)}
12414
12853
  </p>
12415
12854
  {/if}
12416
12855