@widgetic/creator 0.3.54 → 0.3.56

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.
@@ -32,6 +32,13 @@
32
32
  import { userSession as userSessionStore } from '../stores/userSession.js';
33
33
 
34
34
  import { WidgetPreview } from '@widgetic/editor';
35
+ import HoverTooltip from './HoverTooltip.svelte';
36
+ import EditableName from './EditableName.svelte';
37
+
38
+ const HEADER_ELEMENTS_GAP_PX = 6;
39
+ const HEADER_PADDING_PX = HEADER_ELEMENTS_GAP_PX + 2;
40
+ const HEADER_NAME_MIN_WIDTH_PX = 22;
41
+ const DETAILS_DRAG_THRESHOLD_PX = 5;
35
42
 
36
43
  const dispatch = createEventDispatcher();
37
44
 
@@ -278,6 +285,14 @@
278
285
  // ═══════════════════════════════════════════════════════════════════════
279
286
  type WidgetStep = 'create' | 'edit' | 'embed';
280
287
  export let currentStep: WidgetStep = 'create';
288
+ /**
289
+ * Canvas Creator is code-only (chat + preview). Edit/Embed header steps stay
290
+ * off until composition editing lives on /composition. Keep the Edit/Embed
291
+ * panels mounted (CSS-hidden) so we can turn this back on without ripping slots.
292
+ */
293
+ export let showCompositionSteps: boolean = false;
294
+ /** After Set live, Create preview stays on that published artifact until the next Worker build. */
295
+ let pinnedPublishedPreviewUrl: string | null = null;
281
296
 
282
297
  const STEPS: { id: WidgetStep; label: string; number: number }[] = [
283
298
  { id: 'create', label: 'Create', number: 1 },
@@ -299,8 +314,15 @@
299
314
  previewError = null;
300
315
  if (!iframeSrc) previewLoading = true;
301
316
  }
302
- $: canEditStep = lastPublishedVersion !== null && !isGeneratingCode;
303
- $: canEmbedStep = lastPublishedVersion !== null && !isGeneratingCode;
317
+ $: canEditStep = showCompositionSteps && lastPublishedVersion !== null && !isGeneratingCode;
318
+ $: canEmbedStep = showCompositionSteps && lastPublishedVersion !== null && !isGeneratingCode;
319
+ $: activeWorkflowStep = showCompositionSteps ? currentStep : 'create';
320
+
321
+ $: if (!showCompositionSteps && currentStep !== 'create') {
322
+ currentStep = 'create';
323
+ dispatch('stepChange', { step: 'create' });
324
+ switchPreviewUrlForStep('create');
325
+ }
304
326
 
305
327
  // Auto-switch back to Create when code generation starts
306
328
  let _justFinishedGenerating = false;
@@ -314,6 +336,7 @@
314
336
  previewError = null;
315
337
  previewHasRuntimeError = false;
316
338
  previewRuntimeErrorStatus = null;
339
+ pinnedPublishedPreviewUrl = null;
317
340
  }
318
341
 
319
342
  // Default step on widget switch: always open on Create
@@ -326,6 +349,7 @@
326
349
  }
327
350
  currentStep = 'create';
328
351
  dispatch('stepChange', { step: 'create' });
352
+ pinnedPublishedPreviewUrl = null;
329
353
  const createBase = normalizePreviewBaseUrl(
330
354
  toPreviewFetchUrl(resolvePreviewUrlForStep('create') ?? '')
331
355
  );
@@ -347,6 +371,7 @@
347
371
 
348
372
  function canNavigateToStep(step: WidgetStep): boolean {
349
373
  if (step === 'create') return true;
374
+ if (!showCompositionSteps) return false;
350
375
  if (step === 'edit') return canEditStep;
351
376
  if (step === 'embed') return canEmbedStep;
352
377
  return false;
@@ -399,6 +424,10 @@
399
424
  dynamicWorkerPreviewUrl = workerUrl;
400
425
  syncPreviewDisplayUrlForStep('create');
401
426
  void forceReloadPreviewIframe(workerUrl);
427
+ } else if ((step === 'edit' || step === 'embed') && nextResolved) {
428
+ // Always remount Edit/Embed on the published URL — skip-reload left the previous
429
+ // live version in the iframe after auto-publish on Create.
430
+ void forceReloadPreviewIframe(nextResolved);
402
431
  } else {
403
432
  switchPreviewUrlForStep(step, { forceReload: crossingPreviewFamily });
404
433
  }
@@ -422,7 +451,7 @@
422
451
  /** Resolve the canonical preview URL for a workflow step (without cache buster). */
423
452
  function resolvePreviewUrlForStep(step: WidgetStep): string | null {
424
453
  if (step === 'create') {
425
- return dynamicWorkerPreviewUrl;
454
+ return pinnedPublishedPreviewUrl || dynamicWorkerPreviewUrl;
426
455
  }
427
456
  // Use published URL unless probe already confirmed it missing.
428
457
  // Waiting for publishedArtifactReachable === true left Edit/Embed on the idle
@@ -454,6 +483,16 @@
454
483
  }, 3000);
455
484
  }
456
485
 
486
+ /** Load a published CDN artifact (Set live) even on Create — pin until next Worker build. */
487
+ export function showPublishedArtifact(url: string | null): void {
488
+ if (!url) return;
489
+ pinnedPublishedPreviewUrl = url;
490
+ publishedArtifactReachable = true;
491
+ _lastEditPreviewBase = null;
492
+ _lastEmbedPreviewBase = null;
493
+ void forceReloadPreviewIframe(url);
494
+ }
495
+
457
496
  export function finishCompositionHydration(): void {
458
497
  if (compositionHydrationTimeout) {
459
498
  clearTimeout(compositionHydrationTimeout);
@@ -532,9 +571,12 @@
532
571
  }
533
572
  directPreviewUrl = toPreviewDisplayUrl(fetchUrl);
534
573
  let nextFull = fetchUrl;
574
+ const isImmutableCdnUrl = /cdn\.widgetic\.com/i.test(fetchUrl) && /\/v\d+\//i.test(fetchUrl);
535
575
  try {
536
576
  const parsed = new URL(fetchUrl, typeof window !== 'undefined' ? window.location.origin : 'http://localhost');
537
- parsed.searchParams.set('_t', (cacheBuster ?? Date.now()).toString());
577
+ if (!isImmutableCdnUrl || cacheBuster !== undefined) {
578
+ parsed.searchParams.set('_t', (cacheBuster ?? Date.now()).toString());
579
+ }
538
580
  nextFull = parsed.toString();
539
581
  } catch {
540
582
  nextFull = fetchUrl;
@@ -666,8 +708,7 @@
666
708
  const editBase = editSource ? normalizePreviewBaseUrl(toPreviewFetchUrl(editSource)) : null;
667
709
  if (editBase && editBase !== _lastEditPreviewBase) {
668
710
  _lastEditPreviewBase = editBase;
669
- // Prop-driven URL changes must apply even right after step switch (parallel panels)
670
- switchPreviewUrlForStep('edit', { forceReload: !_stepSwitchInProgress });
711
+ void forceReloadPreviewIframe(editSource);
671
712
  }
672
713
  }
673
714
 
@@ -753,7 +794,7 @@
753
794
  export function markCodegenPreviewFresh(): void {
754
795
  codegenPreviewFreshUntil = Date.now() + 30_000;
755
796
  widgetNotFoundAutoRebuildAttempts = 1;
756
- console.log('[WidgetDetails] Codegen preview marked fresh — skipping GitLab rebuilds for 30s');
797
+ // console.log('[WidgetDetails] Codegen preview marked fresh — skipping GitLab rebuilds for 30s');
757
798
  }
758
799
 
759
800
  /** True when the iframe is already showing this widget's Worker preview (not about:blank / error). */
@@ -812,6 +853,8 @@
812
853
 
813
854
  function handleWidgetConsoleMessage(event: MessageEvent) {
814
855
  if (!event.data || typeof event.data !== 'object') return;
856
+ const activeWin = previewIframe?.contentWindow;
857
+ if (!activeWin || event.source !== activeWin) return;
815
858
  const { type, level, message, args } = event.data;
816
859
  if (type !== 'widgetic:console') return;
817
860
  const entry: ConsoleLogEntry = {
@@ -869,19 +912,19 @@
869
912
  $: detailsPanelTopOffset = panelTopOffset ?? legacyTopOffset;
870
913
  $: detailsPanelBottomMargin = panelBottomMargin ?? legacyBottomMargin;
871
914
 
872
- const DETAILS_PANEL_DEFAULT_WIDTH = 800;
873
- const DETAILS_PANEL_MAX_WIDTH = 800;
915
+ const DETAILS_PANEL_DEFAULT_WIDTH = 1000;
916
+ const DETAILS_PANEL_MAX_WIDTH = 1200;
874
917
  const DETAILS_PANEL_MAX_HEIGHT = 800;
875
- const DETAILS_PANEL_MIN_WIDTH = 320;
918
+ const DETAILS_PANEL_MIN_WIDTH = 480;
876
919
  const DETAILS_PANEL_MIN_HEIGHT = 400;
877
920
  const DETAILS_PANEL_FILL_INSET = 16;
878
921
  const DETAILS_PANEL_SHAPE_GAP = 12;
879
- /** Left column share of the two-col layout (chat / PropsEditor). */
880
- const LEFT_COL_DEFAULT_PERCENT = 58;
922
+ /** Left column share of the two-col layout (chat / PropsEditor). Preview gets the rest. */
923
+ const LEFT_COL_DEFAULT_PERCENT = 50;
881
924
  const LEFT_COL_MIN_PERCENT = 32;
882
- const LEFT_COL_MAX_PERCENT = 72;
925
+ const LEFT_COL_MAX_PERCENT = 62;
883
926
  const COL_RESIZER_PX = 10;
884
- const PREVIEW_COL_MIN_PX = 240;
927
+ const PREVIEW_COL_MIN_PX = 420;
885
928
  /** Chat-first layout: preview column starts collapsed and opens on generate. */
886
929
  let previewColumnCollapsed = true;
887
930
  let lastAutoExpandForGenerating = false;
@@ -924,36 +967,33 @@
924
967
 
925
968
  function collapsePreviewColumn() {
926
969
  previewColumnCollapsed = true;
927
- if (widthBeforePreviewExpand > 0) {
928
- detailsPanelWidth = widthBeforePreviewExpand;
929
- widthBeforePreviewExpand = 0;
930
- leftColPercent = LEFT_COL_DEFAULT_PERCENT;
931
- }
932
- console.log('[WidgetDetails] Preview column minimized, restored width', detailsPanelWidth);
970
+ widthBeforePreviewExpand = 0;
971
+ leftColPercent = LEFT_COL_DEFAULT_PERCENT;
972
+ detailsPanelWidth = DETAILS_PANEL_MIN_WIDTH;
973
+ console.log('[WidgetDetails] Close Preview — panel width', detailsPanelWidth);
933
974
  }
934
975
 
935
976
  function expandPreviewColumn() {
936
977
  if (!previewColumnCollapsed) return;
937
- widthBeforePreviewExpand = detailsPanelWidth;
938
978
  previewColumnCollapsed = false;
939
- const extraWidth = 420;
940
- const maxRight = typeof window !== 'undefined' ? window.innerWidth - 8 : detailsPanelWidth;
941
- const expandedWidth = widthBeforePreviewExpand + extraWidth;
942
- const fittedWidth = Math.min(expandedWidth, Math.max(widthBeforePreviewExpand, maxRight - detailsPanelX));
943
- detailsPanelWidth = Math.max(widthBeforePreviewExpand, fittedWidth);
944
- const usable = Math.max(1, detailsPanelWidth);
945
- leftColPercent = clampLeftColPercent((widthBeforePreviewExpand / usable) * 100);
946
- console.log('[WidgetDetails] Preview column expanded', {
947
- from: widthBeforePreviewExpand,
948
- to: detailsPanelWidth,
949
- leftColPercent,
950
- });
979
+ leftColPercent = clampLeftColPercent(LEFT_COL_DEFAULT_PERCENT);
980
+ detailsPanelWidth = clampPanelWidth(DETAILS_PANEL_DEFAULT_WIDTH);
981
+ if (typeof window !== 'undefined' && detailsPanelX + detailsPanelWidth > window.innerWidth - 8) {
982
+ detailsPanelX = Math.max(8, window.innerWidth - 8 - detailsPanelWidth);
983
+ }
984
+ console.log('[WidgetDetails] Open Preview — panel width', detailsPanelWidth);
951
985
  tick().then(() => requestAnimationFrame(() => fitPreviewToArea()));
952
986
  }
953
987
 
954
988
  let detailsPanelX = -1;
955
989
  let detailsPanelY = -1;
956
990
  let isDraggingDetails = false;
991
+ let pendingDetailsDrag = false;
992
+ let detailsDragBarEl: HTMLElement | null = null;
993
+ let widgetDetailsNameCtEl: HTMLElement | null = null;
994
+ let widgetDetailsStepsRowEl: HTMLElement | null = null;
995
+ let widgetDetailsDebugButtonsEl: HTMLElement | null = null;
996
+ let headerNameMaxWidthPx = 96;
957
997
  let detailsDragStartX = 0;
958
998
  let detailsDragStartY = 0;
959
999
  let detailsInitialX = 0;
@@ -998,7 +1038,7 @@
998
1038
  const pageOrigin = typeof window !== 'undefined' ? window.location.origin : '';
999
1039
  const isApiProxy = !!(apiBaseUrl && fetchUrl.startsWith(apiBaseUrl));
1000
1040
  if (pageOrigin && absolute.origin !== pageOrigin && !isApiProxy) {
1001
- console.log('[WidgetDetails] Skipping cross-origin HEAD probe (CORS); loading in iframe:', absolute.origin);
1041
+ // console.log('[WidgetDetails] Skipping cross-origin HEAD probe (CORS); loading in iframe:', absolute.origin);
1002
1042
  return true;
1003
1043
  }
1004
1044
  } catch {
@@ -1057,11 +1097,11 @@
1057
1097
  if (iframeSrc && previewLoadedForWidgetId === widgetId) {
1058
1098
  previewBuildDeferred = false;
1059
1099
  previewLoading = false;
1060
- console.log('[WidgetDetails] Preview already loaded, skipping pipeline:', widgetId.substring(0, 8));
1100
+ // console.log('[WidgetDetails] Preview already loaded, skipping pipeline:', widgetId.substring(0, 8));
1061
1101
  return;
1062
1102
  }
1063
1103
  if (shouldDeferPreviewNetworkWork()) {
1064
- console.log('[WidgetDetails] Deferring preview load (background panel):', widgetId.substring(0, 8));
1104
+ // console.log('[WidgetDetails] Deferring preview load (background panel):', widgetId.substring(0, 8));
1065
1105
  previewBuildDeferred = true;
1066
1106
  previewLoading = false;
1067
1107
  previewError = null;
@@ -1092,10 +1132,10 @@
1092
1132
  }
1093
1133
 
1094
1134
  if (probeResult === 'not-found') {
1095
- console.log('[WidgetDetails] Widget not on Worker — published artifact or build-from-repo', {
1096
- widgetId: capturedWidgetId.substring(0, 8),
1097
- hasJwt: !!getAuthJwt(),
1098
- });
1135
+ // console.log('[WidgetDetails] Widget not on Worker — published artifact or build-from-repo', {
1136
+ // widgetId: capturedWidgetId.substring(0, 8),
1137
+ // hasJwt: !!getAuthJwt(),
1138
+ // });
1099
1139
  if (isGeneratingCode) {
1100
1140
  previewLoading = true;
1101
1141
  previewError = null;
@@ -1108,13 +1148,13 @@
1108
1148
  if (!hasRepo) {
1109
1149
  const fallbackUrl = publishedHtmlUrlFromJsPath(widgetJsPath);
1110
1150
  if (fallbackUrl) {
1111
- console.log('[WidgetDetails] No repo yet — loading js_path artifact', fallbackUrl);
1151
+ // console.log('[WidgetDetails] No repo yet — loading js_path artifact', fallbackUrl);
1112
1152
  previewError = null;
1113
1153
  applyPreviewUrlToIframe(fallbackUrl);
1114
1154
  previewLoading = false;
1115
1155
  return;
1116
1156
  }
1117
- console.log('[WidgetDetails] No repository yet — skipping build-from-repo');
1157
+ // console.log('[WidgetDetails] No repository yet — skipping build-from-repo');
1118
1158
  previewLoading = false;
1119
1159
  previewError = 'NO_CODE_YET';
1120
1160
  return;
@@ -1127,7 +1167,7 @@
1127
1167
  directPreviewUrl = workerPreviewUrl;
1128
1168
  previewLoading = true;
1129
1169
  switchPreviewUrlForStep(currentStep);
1130
- console.log('[WidgetDetails] Loading preview iframe:', workerPreviewUrl, 'step:', currentStep);
1170
+ // console.log('[WidgetDetails] Loading preview iframe:', workerPreviewUrl, 'step:', currentStep);
1131
1171
  requestAnimationFrame(() => {
1132
1172
  setTimeout(fitPreviewToArea, 150);
1133
1173
  });
@@ -1139,7 +1179,7 @@
1139
1179
  if (!previewBuildDeferred || !selectedWidgetId) return;
1140
1180
  if (iframeSrc && previewLoadedForWidgetId === selectedWidgetId) {
1141
1181
  previewBuildDeferred = false;
1142
- console.log('[WidgetDetails] Skip deferred preview — already loaded for', selectedWidgetId.substring(0, 8));
1182
+ // console.log('[WidgetDetails] Skip deferred preview — already loaded for', selectedWidgetId.substring(0, 8));
1143
1183
  return;
1144
1184
  }
1145
1185
  previewBuildDeferred = false;
@@ -1199,18 +1239,18 @@
1199
1239
 
1200
1240
  await tick();
1201
1241
 
1202
- console.log('WidgetDetails: loadWidget completed:', {
1203
- widgetId,
1204
- name: selectedWidget?.name,
1205
- currentRepositoryId,
1206
- });
1242
+ // console.log('WidgetDetails: loadWidget completed:', {
1243
+ // widgetId,
1244
+ // name: selectedWidget?.name,
1245
+ // currentRepositoryId,
1246
+ // });
1207
1247
 
1208
1248
  dispatch('widgetReady', { widgetId: selectedWidgetId });
1209
1249
 
1210
1250
  // If skipBasePreview is set, the caller will handle compiling via triggerBuildFromRepo
1211
1251
  // (e.g. duplicates waiting for GitLab fork, or converts waiting for code generation)
1212
1252
  if (skipBasePreview) {
1213
- console.log('[WidgetDetails] skipBasePreview is set — deferring preview load to caller');
1253
+ // console.log('[WidgetDetails] skipBasePreview is set — deferring preview load to caller');
1214
1254
  previewLoading = true;
1215
1255
  previewError = null;
1216
1256
  return;
@@ -1222,11 +1262,11 @@
1222
1262
 
1223
1263
  const hasRepo = !!(normalizeRepositoryId(selectedWidget) || currentRepositoryId);
1224
1264
  if (!lastCommitId && lastPublishedVersion === null && !widgetJsPath && !hasRepo) {
1225
- console.log('[WidgetDetails] No code yet — skipping Worker probe/build-from-repo', {
1226
- widgetId: widgetId?.substring(0, 8),
1227
- hasRepo,
1228
- repositorySetupStatus,
1229
- });
1265
+ // console.log('[WidgetDetails] No code yet — skipping Worker probe/build-from-repo', {
1266
+ // widgetId: widgetId?.substring(0, 8),
1267
+ // hasRepo,
1268
+ // repositorySetupStatus,
1269
+ // });
1230
1270
  previewLoading = false;
1231
1271
  previewError = 'NO_CODE_YET';
1232
1272
  return;
@@ -1251,7 +1291,7 @@
1251
1291
 
1252
1292
  export async function refreshPreview(opts: { withRetry?: boolean; skipIfAlreadyShowing?: boolean } = {}): Promise<void> {
1253
1293
  if (opts.skipIfAlreadyShowing && hasLiveWorkerPreview()) {
1254
- console.log('[WidgetDetails] refreshPreview skipped — Worker preview already showing');
1294
+ // console.log('[WidgetDetails] refreshPreview skipped — Worker preview already showing');
1255
1295
  previewLoading = false;
1256
1296
  previewHydrating = false;
1257
1297
  return;
@@ -1275,11 +1315,11 @@
1275
1315
  middleware: cfg.middleware,
1276
1316
  headers: cfg.headers,
1277
1317
  });
1278
- console.log('[WidgetDetails] buildWidgetFromRepo', {
1279
- widgetId: widgetId.substring(0, 8),
1280
- builderBasePath,
1281
- sdkPath: '/v1/agent/widget-builder/build-from-repo/{widgetId}',
1282
- });
1318
+ // console.log('[WidgetDetails] buildWidgetFromRepo', {
1319
+ // widgetId: widgetId.substring(0, 8),
1320
+ // builderBasePath,
1321
+ // sdkPath: '/v1/agent/widget-builder/build-from-repo/{widgetId}',
1322
+ // });
1283
1323
  return builder.buildWidgetFromRepo({ widgetId });
1284
1324
  }
1285
1325
 
@@ -1287,13 +1327,13 @@
1287
1327
  const forceRebuild = options?.force === true;
1288
1328
  if (!selectedWidgetId) return;
1289
1329
  if (shouldDeferPreviewNetworkWork(forceRebuild)) {
1290
- console.log('[WidgetDetails] triggerBuildFromRepo deferred (background panel)');
1330
+ // console.log('[WidgetDetails] triggerBuildFromRepo deferred (background panel)');
1291
1331
  previewBuildDeferred = true;
1292
1332
  return;
1293
1333
  }
1294
1334
  const hasRepo = !!(normalizeRepositoryId(selectedWidget) || currentRepositoryId);
1295
1335
  if (!hasRepo) {
1296
- console.log('[WidgetDetails] triggerBuildFromRepo skipped — widget has no repository yet');
1336
+ // console.log('[WidgetDetails] triggerBuildFromRepo skipped — widget has no repository yet');
1297
1337
  previewLoading = false;
1298
1338
  previewError = 'NO_CODE_YET';
1299
1339
  return;
@@ -1301,6 +1341,7 @@
1301
1341
  const capturedWidgetId = selectedWidgetId;
1302
1342
  const runId = ++buildFromRepoRunId;
1303
1343
  if (forceRebuild) isRebuildingPreview = true;
1344
+ pinnedPublishedPreviewUrl = null;
1304
1345
  try {
1305
1346
  const workerPreviewUrl = buildWorkerPreviewUrl(capturedWidgetId);
1306
1347
  const workerPreviewDisplayUrl = buildWorkerPreviewUrl(capturedWidgetId, { forDisplay: true });
@@ -1309,7 +1350,7 @@
1309
1350
  const freshProbe = await probeWorkerReachable(workerPreviewUrl);
1310
1351
  if (selectedWidgetId !== capturedWidgetId || runId !== buildFromRepoRunId) return;
1311
1352
  if (freshProbe === 'ok') {
1312
- console.log('[WidgetDetails] triggerBuildFromRepo: skipping GitLab rebuild — codegen preview is fresh');
1353
+ // console.log('[WidgetDetails] triggerBuildFromRepo: skipping GitLab rebuild — codegen preview is fresh');
1313
1354
  dynamicWorkerPreviewUrl = workerPreviewUrl;
1314
1355
  directPreviewUrl = workerPreviewDisplayUrl;
1315
1356
  inlinePanelView = 'preview';
@@ -1349,7 +1390,7 @@
1349
1390
  const probeResult = await probeWorkerReachable(workerPreviewUrl);
1350
1391
  if (isStaleRun()) return;
1351
1392
  if (!forceRebuild && probeResult === 'ok') {
1352
- console.log('[WidgetDetails] triggerBuildFromRepo: Worker already has widget, loading preview directly');
1393
+ // console.log('[WidgetDetails] triggerBuildFromRepo: Worker already has widget, loading preview directly');
1353
1394
  dispatch('buildFromRepoSuccess', { widgetId: capturedWidgetId });
1354
1395
  loadPreviewFromWorker();
1355
1396
  return;
@@ -1363,7 +1404,7 @@
1363
1404
 
1364
1405
  // Skip build-from-repo while codegen is running — WS / refreshPreview will load when ready
1365
1406
  if (isGeneratingCode) {
1366
- console.log('[WidgetDetails] triggerBuildFromRepo: codegen in progress — deferring build-from-repo');
1407
+ // console.log('[WidgetDetails] triggerBuildFromRepo: codegen in progress — deferring build-from-repo');
1367
1408
  previewLoading = true;
1368
1409
  previewError = null;
1369
1410
  return;
@@ -1381,7 +1422,7 @@
1381
1422
  const buildData = await requestBuildFromRepo(capturedWidgetId);
1382
1423
  if (isStaleRun()) return;
1383
1424
 
1384
- console.log('[WidgetDetails] triggerBuildFromRepo succeeded', { buildTimeMs: buildData?.data?.buildTimeMs ?? buildData?.buildTimeMs, attempt });
1425
+ // console.log('[WidgetDetails] triggerBuildFromRepo succeeded', { buildTimeMs: buildData?.data?.buildTimeMs ?? buildData?.buildTimeMs, attempt });
1385
1426
  previewError = null;
1386
1427
  previewLoading = false;
1387
1428
  dispatch('widgetDataChanged', { widgetId: capturedWidgetId });
@@ -1409,7 +1450,7 @@
1409
1450
  lastBuildErrorMsg = sanitizePreviewBuildError(lastBuildErrorMsg);
1410
1451
 
1411
1452
  if (buildErrorCode === 'NO_CODE_YET') {
1412
- console.log('[WidgetDetails] triggerBuildFromRepo: repo exists but has no code yet — skipping retries');
1453
+ // console.log('[WidgetDetails] triggerBuildFromRepo: repo exists but has no code yet — skipping retries');
1413
1454
  lastBuildErrorMsg = 'NO_CODE_YET';
1414
1455
  break;
1415
1456
  }
@@ -1417,7 +1458,7 @@
1417
1458
  const status = buildErr?.response?.status ?? buildErr?.status;
1418
1459
  const isRetryable = status === 404 || status === 500 || !status;
1419
1460
  if (isRetryable && attempt < maxRetries) {
1420
- console.log(`[WidgetDetails] triggerBuildFromRepo attempt ${attempt}/${maxRetries} failed (${lastBuildErrorMsg}), retrying in ${retryDelayMs}ms...`);
1461
+ // console.log(`[WidgetDetails] triggerBuildFromRepo attempt ${attempt}/${maxRetries} failed (${lastBuildErrorMsg}), retrying in ${retryDelayMs}ms...`);
1421
1462
  await new Promise(r => setTimeout(r, retryDelayMs));
1422
1463
  continue;
1423
1464
  }
@@ -1439,14 +1480,14 @@
1439
1480
 
1440
1481
  // Preview may have loaded via codegen/WS while build-from-repo was in flight
1441
1482
  if (allowStalePreviewRecovery && iframeSrc && !previewHasRuntimeError) {
1442
- console.log('[WidgetDetails] triggerBuildFromRepo: build failed but preview iframe already loaded — keeping preview');
1483
+ // console.log('[WidgetDetails] triggerBuildFromRepo: build failed but preview iframe already loaded — keeping preview');
1443
1484
  previewError = null;
1444
1485
  previewLoading = false;
1445
1486
  return;
1446
1487
  }
1447
1488
 
1448
1489
  if (allowStalePreviewRecovery && (await tryRecoverPreviewFromWorker())) {
1449
- console.log('[WidgetDetails] triggerBuildFromRepo: recovered preview from Worker after build-from-repo failure');
1490
+ // console.log('[WidgetDetails] triggerBuildFromRepo: recovered preview from Worker after build-from-repo failure');
1450
1491
  return;
1451
1492
  }
1452
1493
 
@@ -1454,7 +1495,7 @@
1454
1495
  // For published widgets, try loading via API proxy (handles both R2 and CDN)
1455
1496
  const sourceArtifactUrl = publishedHtmlUrlFromJsPath(widgetJsPath);
1456
1497
  if (sourceArtifactUrl) {
1457
- console.log('[WidgetDetails] triggerBuildFromRepo: using js_path artifact (not new-id proxy)', sourceArtifactUrl);
1498
+ // console.log('[WidgetDetails] triggerBuildFromRepo: using js_path artifact (not new-id proxy)', sourceArtifactUrl);
1458
1499
  const reachable = await probePublishedArtifactUrl(sourceArtifactUrl);
1459
1500
  if (reachable) {
1460
1501
  previewError = null;
@@ -1586,7 +1627,7 @@
1586
1627
  /** Duplicate/fork fallback: keep published iframe if it loaded; otherwise compile from repo. */
1587
1628
  export async function retryPreviewIfEmpty(): Promise<void> {
1588
1629
  if (iframeSrc && !previewError) {
1589
- console.log('[WidgetDetails] retryPreviewIfEmpty skipped — preview already loaded');
1630
+ // console.log('[WidgetDetails] retryPreviewIfEmpty skipped — preview already loaded');
1590
1631
  return;
1591
1632
  }
1592
1633
  previewLoading = true;
@@ -1595,7 +1636,7 @@
1595
1636
  if (await loadPublishedPreviewForCurrentStep()) return;
1596
1637
  const hasRepo = !!(normalizeRepositoryId(selectedWidget) || currentRepositoryId);
1597
1638
  if (hasRepo) {
1598
- console.log('[WidgetDetails] retryPreviewIfEmpty — compiling from repository');
1639
+ // console.log('[WidgetDetails] retryPreviewIfEmpty — compiling from repository');
1599
1640
  await triggerBuildFromRepo();
1600
1641
  return;
1601
1642
  }
@@ -1664,10 +1705,9 @@
1664
1705
  // Widget is alive — never keep a runtime-error overlay over a working preview.
1665
1706
  clearPendingRuntimeErrorOverlay();
1666
1707
  if (previewHasRuntimeError) {
1667
- console.log('[WidgetDetails] Clearing runtime error overlay — widgetic:ready');
1668
1708
  previewHasRuntimeError = false;
1669
1709
  previewRuntimeErrorStatus = null;
1670
- if (previewError && /runtime error|HTTP 500|compilation/i.test(previewError)) {
1710
+ if (previewError && /did not start|runtime error|HTTP 500|compilation/i.test(previewError)) {
1671
1711
  previewError = null;
1672
1712
  }
1673
1713
  notifyPreviewCompileErrorChanged();
@@ -1684,6 +1724,17 @@
1684
1724
  return typeof value === 'string' && /<[a-z][\s\S]*>/i.test(value);
1685
1725
  }
1686
1726
 
1727
+ function contentOverlaySelectors(key: string): string {
1728
+ const exact = `[data-content="${CSS.escape(key)}"]`;
1729
+ if (/subtitle/i.test(key)) {
1730
+ return `${exact}, [data-content="widgetSubtitle"], [data-content="gallerySubtitle"], [data-content="subtitle"], .widget-subtitle, .gallery-subtitle`;
1731
+ }
1732
+ if (/^(widgetTitle|galleryTitle|title|heading)$/i.test(key)) {
1733
+ return `${exact}, [data-content="widgetTitle"], [data-content="galleryTitle"], h1, .widget-title`;
1734
+ }
1735
+ return exact;
1736
+ }
1737
+
1687
1738
  /** Same-origin preview iframes (Vite /__widget_builder__ proxy): restore bold/italic after widgets set textContent. */
1688
1739
  function applyRichTextInPreviewIframe(message: Record<string, unknown>): void {
1689
1740
  if (message.type !== 'widgetic:update') return;
@@ -1694,10 +1745,16 @@
1694
1745
  const content = message.content as Record<string, unknown> | undefined;
1695
1746
  if (content) {
1696
1747
  for (const [key, value] of Object.entries(content)) {
1697
- if (!looksLikeHtmlMarkup(value)) continue;
1698
- doc.querySelectorAll(`[data-content="${CSS.escape(key)}"]`).forEach((el) => {
1699
- if (el instanceof HTMLImageElement || el instanceof HTMLInputElement) return;
1700
- el.innerHTML = value;
1748
+ if (typeof value !== 'string') continue;
1749
+ const selectors = contentOverlaySelectors(key);
1750
+ doc.querySelectorAll(selectors).forEach((el) => {
1751
+ if (el instanceof HTMLImageElement) {
1752
+ el.src = value;
1753
+ return;
1754
+ }
1755
+ if (el instanceof HTMLInputElement || el instanceof HTMLAnchorElement) return;
1756
+ if (looksLikeHtmlMarkup(value)) el.innerHTML = value;
1757
+ else el.textContent = value;
1701
1758
  });
1702
1759
  }
1703
1760
  }
@@ -1737,25 +1794,63 @@
1737
1794
  }
1738
1795
 
1739
1796
  function ensureGoogleFontInPreviewDoc(doc: Document, family: string): void {
1740
- if (!family || /^(Inter|system-ui|sans-serif|serif)$/i.test(family)) return;
1797
+ if (!family || /^(system-ui|sans-serif|serif)$/i.test(family)) return;
1741
1798
  const id = `widgetic-gf-${family.replace(/[^a-z0-9]+/gi, '-').toLowerCase()}`;
1742
1799
  if (doc.getElementById(id)) return;
1743
1800
  const link = doc.createElement('link');
1744
1801
  link.id = id;
1745
1802
  link.rel = 'stylesheet';
1746
- link.href = `https://fonts.googleapis.com/css2?family=${encodeURIComponent(family)}:ital,wght@0,400;0,500;0,600;0,700;1,400&display=swap`;
1803
+ link.href = `https://fonts.googleapis.com/css2?family=${encodeURIComponent(family)}:ital,wght@0,100;0,200;0,300;0,400;0,500;0,600;0,700;0,800;0,900;1,100;1,200;1,300;1,400;1,500;1,600;1,700;1,800;1,900&display=swap`;
1747
1804
  doc.head?.appendChild(link);
1748
- console.log('[WidgetDetails] Loaded Google Font in preview iframe:', family);
1805
+ // console.log('[WidgetDetails] Loaded Google Font in preview iframe:', family);
1806
+ }
1807
+
1808
+ function parseOverlayFontFace(weight: string | undefined): { cssWeight: string; cssStyle: string } {
1809
+ const raw = String(weight || 'Regular');
1810
+ const cssStyle = /italic|oblique/i.test(raw) ? 'italic' : 'normal';
1811
+ const name = raw.toLowerCase().replace(/italic|oblique/g, '').replace(/[\s_-]/g, '');
1812
+ const table: Record<string, string> = {
1813
+ thin: '100', hairline: '100',
1814
+ extralight: '200', ultralight: '200',
1815
+ light: '300',
1816
+ regular: '400', normal: '400', '': '400',
1817
+ medium: '500',
1818
+ semibold: '600', demibold: '600',
1819
+ bold: '700',
1820
+ extrabold: '800', ultrabold: '800',
1821
+ black: '900', heavy: '900',
1822
+ };
1823
+ if (/^\d{3}$/.test(name)) return { cssWeight: name, cssStyle };
1824
+ return { cssWeight: table[name] || '400', cssStyle };
1825
+ }
1826
+
1827
+ function flattenDesignOverlay(raw: Record<string, unknown> | undefined): Record<string, string> | null {
1828
+ if (!raw) return null;
1829
+ const out: Record<string, string> = {};
1830
+ for (const [key, value] of Object.entries(raw)) {
1831
+ if (value && typeof value === 'object' && 'family' in (value as Record<string, unknown>)) {
1832
+ const font = value as { family?: string; weight?: string };
1833
+ const family = String(font.family || 'Inter');
1834
+ const face = parseOverlayFontFace(font.weight);
1835
+ out[key] = `"${family}", sans-serif`;
1836
+ out[`${key}Family`] = family;
1837
+ out[`${key}Weight`] = face.cssWeight;
1838
+ out[`${key}Style`] = face.cssStyle;
1839
+ continue;
1840
+ }
1841
+ if (typeof value === 'string' && value) out[key] = value;
1842
+ }
1843
+ return Object.keys(out).length ? out : null;
1749
1844
  }
1750
1845
 
1751
1846
  /** Same-origin preview: apply CSS vars, Google Fonts, title/subtitle, tile aspect-ratio. */
1752
1847
  function applyDesignOverlayInPreviewIframe(message: Record<string, unknown>): void {
1753
1848
  if (message.type !== 'widgetic:update') return;
1754
- const design = message.design as Record<string, string> | undefined;
1849
+ const design = flattenDesignOverlay(message.design as Record<string, unknown> | undefined);
1755
1850
  if (!design) return;
1756
1851
  const doc = previewIframe?.contentDocument;
1757
1852
  if (!doc?.documentElement) {
1758
- console.log('[WidgetDetails] Design overlay skipped — preview iframe not same-origin');
1853
+ // console.log('[WidgetDetails] Design overlay skipped — preview iframe not same-origin');
1759
1854
  return;
1760
1855
  }
1761
1856
  const apply = () => {
@@ -1776,46 +1871,69 @@
1776
1871
  }
1777
1872
  families.forEach((family) => ensureGoogleFontInPreviewDoc(doc, family));
1778
1873
 
1779
- const titleFamily =
1780
- design.titleFontFamily ||
1781
- extractCssFontFamily(design.titleFont) ||
1782
- design.headingFontFamily ||
1783
- extractCssFontFamily(design.headingFont);
1784
- const titleWeight = design.titleFontWeight || design.headingFontWeight || '';
1785
- const subtitleFamily =
1786
- design.subtitleFontFamily ||
1787
- extractCssFontFamily(design.subtitleFont) ||
1788
- design.descriptionFontFamily ||
1789
- extractCssFontFamily(design.descriptionFont);
1790
- const subtitleWeight = design.subtitleFontWeight || design.descriptionFontWeight || '';
1791
-
1792
- if (titleFamily || titleWeight) {
1793
- const titleStack = titleFamily ? `"${titleFamily}", sans-serif` : '';
1794
- doc.querySelectorAll(
1795
- 'h1, h2, h3, .widget-title, [data-content="title"], [data-content="widgetTitle"], [data-content="heading"]'
1796
- ).forEach((el) => {
1797
- if (!(el instanceof HTMLElement)) return;
1798
- if (titleStack) el.style.setProperty('font-family', titleStack, 'important');
1799
- if (titleWeight) el.style.setProperty('font-weight', titleWeight, 'important');
1874
+ const applyFace = (family: string, weight: string, style: string, selectors: string) => {
1875
+ if (!family && !weight && !style) return;
1876
+ const stack = family ? `"${family}", sans-serif` : '';
1877
+ doc.querySelectorAll(selectors).forEach((node) => {
1878
+ if (!(node instanceof HTMLElement)) return;
1879
+ if (stack) node.style.setProperty('font-family', stack, 'important');
1880
+ if (weight) node.style.setProperty('font-weight', weight, 'important');
1881
+ if (style) node.style.setProperty('font-style', style, 'important');
1800
1882
  });
1883
+ };
1884
+
1885
+ const cssChunks: string[] = [];
1886
+ const fontPrefixes = new Set<string>();
1887
+ for (const key of Object.keys(design)) {
1888
+ const match = key.match(/^(.*)Font(?:Family|Weight|Style)?$/i);
1889
+ if (match) fontPrefixes.add(match[1] || '');
1801
1890
  }
1802
- if (subtitleFamily || subtitleWeight) {
1803
- const subStack = subtitleFamily ? `"${subtitleFamily}", sans-serif` : '';
1804
- const subtitleEls = doc.querySelectorAll(
1805
- '.widget-subtitle, .gallery-subtitle, [data-content="subtitle"], [data-content="description"], [data-content="widgetSubtitle"]'
1806
- );
1807
- const applySub = (el: HTMLElement) => {
1808
- if (subStack) el.style.setProperty('font-family', subStack, 'important');
1809
- if (subtitleWeight) el.style.setProperty('font-weight', subtitleWeight, 'important');
1810
- };
1811
- if (subtitleEls.length > 0) {
1812
- subtitleEls.forEach((el) => {
1813
- if (el instanceof HTMLElement) applySub(el);
1814
- });
1815
- } else {
1816
- const firstParagraph = doc.querySelector('p');
1817
- if (firstParagraph instanceof HTMLElement) applySub(firstParagraph);
1891
+
1892
+ for (const prefix of fontPrefixes) {
1893
+ const family =
1894
+ design[`${prefix}FontFamily`] ||
1895
+ extractCssFontFamily(design[`${prefix}Font`]) ||
1896
+ '';
1897
+ const weight = design[`${prefix}FontWeight`] || '';
1898
+ const style = design[`${prefix}FontStyle`] || '';
1899
+ const isTitleLike =
1900
+ /^(title|heading|tite)?$/i.test(prefix) ||
1901
+ /title|heading|tite/i.test(prefix);
1902
+ const isSubtitleLike = /subtitle|description|caption|label/i.test(prefix);
1903
+ const varFamily = `--${prefix}Font`;
1904
+ const varWeight = `--${prefix}FontWeight`;
1905
+ const varStyle = `--${prefix}FontStyle`;
1906
+ if (isSubtitleLike) {
1907
+ cssChunks.push(
1908
+ `[data-content="caption"], [data-content="subtitle"], [data-content="description"], [data-content="widgetSubtitle"], .caption-bottom p, [class*="caption"], [class*="subtitle"] { font-family: var(${varFamily}, inherit) !important; font-weight: var(${varWeight}, inherit) !important; font-style: var(${varStyle}, normal) !important; }`
1909
+ );
1910
+ applyFace(
1911
+ family,
1912
+ weight,
1913
+ style,
1914
+ '.widget-subtitle, .gallery-subtitle, .gallery-caption, .caption-bottom p, [data-content="subtitle"], [data-content="description"], [data-content="widgetSubtitle"], [data-content="caption"], [class*="caption"], [class*="subtitle"]'
1915
+ );
1916
+ } else if (isTitleLike || fontPrefixes.size === 1) {
1917
+ cssChunks.push(
1918
+ `h1, h2, h3, [data-content="widgetTitle"], [data-content="title"], [data-content="heading"], .widget-title, .gallery-title, [class*="title"] { font-family: var(${varFamily}, inherit) !important; font-weight: var(${varWeight}, inherit) !important; font-style: var(${varStyle}, normal) !important; }`
1919
+ );
1920
+ applyFace(
1921
+ family,
1922
+ weight,
1923
+ style,
1924
+ 'h1, h2, h3, .widget-title, .gallery-title, [data-content="title"], [data-content="widgetTitle"], [data-content="heading"], [class*="title"]'
1925
+ );
1926
+ }
1927
+ }
1928
+
1929
+ if (cssChunks.length) {
1930
+ let sheet = doc.getElementById('widgetic-font-overlay') as HTMLStyleElement | null;
1931
+ if (!sheet) {
1932
+ sheet = doc.createElement('style');
1933
+ sheet.id = 'widgetic-font-overlay';
1934
+ doc.head.appendChild(sheet);
1818
1935
  }
1936
+ sheet.textContent = cssChunks.join('\n');
1819
1937
  }
1820
1938
 
1821
1939
  const aspectEntry = Object.entries(design).find(([key, value]) =>
@@ -1832,7 +1950,7 @@
1832
1950
  el.style.height = 'auto';
1833
1951
  }
1834
1952
  });
1835
- console.log('[WidgetDetails] Applied tile aspect-ratio overlay:', cssRatio);
1953
+ // console.log('[WidgetDetails] Applied tile aspect-ratio overlay:', cssRatio);
1836
1954
  }
1837
1955
  } catch (error) {
1838
1956
  console.warn('[WidgetDetails] Design overlay skipped:', error);
@@ -1899,7 +2017,7 @@
1899
2017
 
1900
2018
  function startPreviewTokenAutoRefresh() {
1901
2019
  stopPreviewTokenAutoRefresh();
1902
- console.log('[TokenAutoRefresh] Started - checking every', TOKEN_CHECK_INTERVAL_MS / 1000, 'seconds');
2020
+ // console.log('[TokenAutoRefresh] Started - checking every', TOKEN_CHECK_INTERVAL_MS / 1000, 'seconds');
1903
2021
  previewTokenRefreshInterval = setInterval(async () => {
1904
2022
  const isPreviewVisible = showPreviewModal || (showWidgetDetails && inlinePanelView === 'preview');
1905
2023
  if (!isPreviewVisible || !lastPreviewUrlFetchTime || !directPreviewUrl) return;
@@ -2008,7 +2126,7 @@
2008
2126
  const retryCount = opts._retryCount || 0;
2009
2127
  const withRetry = opts.withRetry || false;
2010
2128
 
2011
- console.log('WidgetDetails: Triggering preview refresh via src manipulation', retryCount > 0 ? `(retry ${retryCount})` : '');
2129
+ // console.log('WidgetDetails: Triggering preview refresh via src manipulation', retryCount > 0 ? `(retry ${retryCount})` : '');
2012
2130
  const newCacheBuster = Date.now();
2013
2131
  previewCacheBuster = newCacheBuster;
2014
2132
 
@@ -2018,7 +2136,7 @@
2018
2136
  const workerPreviewDisplayUrl = buildWorkerPreviewUrl(selectedWidgetId, { forDisplay: true });
2019
2137
  dynamicWorkerPreviewUrl = workerPreviewUrl;
2020
2138
  directPreviewUrl = workerPreviewDisplayUrl;
2021
- console.log('[WidgetDetails] refreshPreview: initialized worker preview URL');
2139
+ // console.log('[WidgetDetails] refreshPreview: initialized worker preview URL');
2022
2140
  }
2023
2141
 
2024
2142
  // After publish (or any time the CDN artifact may have changed), edit/embed steps must
@@ -2044,7 +2162,7 @@
2044
2162
  // If not reachable and we have retry budget, retry after a short delay (CDN propagation).
2045
2163
  if (withRetry && retryCount < 3) {
2046
2164
  const delay = (retryCount + 1) * 2000;
2047
- console.log(`[WidgetDetails] Published artifact not reachable yet, retrying in ${delay}ms (attempt ${retryCount + 1}/3)`);
2165
+ // console.log(`[WidgetDetails] Published artifact not reachable yet, retrying in ${delay}ms (attempt ${retryCount + 1}/3)`);
2048
2166
  setTimeout(() => refreshPreviewIframe({ withRetry: true, _retryCount: retryCount + 1 }), delay);
2049
2167
  return;
2050
2168
  }
@@ -2073,14 +2191,14 @@
2073
2191
  if (probeResult === 'not-found') {
2074
2192
  if (withRetry && retryCount < 3) {
2075
2193
  const delay = (retryCount + 1) * 2000;
2076
- console.log(`[WidgetDetails] Preview not found, retrying in ${delay}ms (attempt ${retryCount + 1}/3)`);
2194
+ // console.log(`[WidgetDetails] Preview not found, retrying in ${delay}ms (attempt ${retryCount + 1}/3)`);
2077
2195
  previewLoading = true;
2078
2196
  setTimeout(() => refreshPreviewIframe({ withRetry: true, _retryCount: retryCount + 1 }), delay);
2079
2197
  return;
2080
2198
  }
2081
2199
  // Worker cache miss after codegen — compile from GitLab repo
2082
2200
  if (lastCommitId && !isGeneratingCode && !shouldDeferPreviewNetworkWork()) {
2083
- console.log('[WidgetDetails] Preview not on Worker after retries — trying build-from-repo');
2201
+ // console.log('[WidgetDetails] Preview not on Worker after retries — trying build-from-repo');
2084
2202
  await triggerBuildFromRepo();
2085
2203
  if (iframeSrc) return;
2086
2204
  }
@@ -2196,9 +2314,16 @@
2196
2314
  if (!previewWidgetReady) {
2197
2315
  if (previewReadyFallbackTimeout) clearTimeout(previewReadyFallbackTimeout);
2198
2316
  previewReadyFallbackTimeout = setTimeout(() => {
2199
- console.warn('[WidgetDetails] Create step widgetic:ready fallback — applying overlay');
2200
- markPreviewWidgetReady();
2201
- }, 1500);
2317
+ if (previewWidgetReady) return;
2318
+ console.warn('[WidgetDetails] Create step: iframe loaded but widget did not signal ready');
2319
+ previewLoading = false;
2320
+ previewHydrating = false;
2321
+ previewHasRuntimeError = true;
2322
+ previewRuntimeErrorStatus = 500;
2323
+ previewError =
2324
+ 'Preview loaded but the widget did not start. Open in a new tab or tap Rebuild. If the new tab is also blank, the generated code likely failed at runtime.';
2325
+ notifyPreviewCompileErrorChanged();
2326
+ }, 4000);
2202
2327
  }
2203
2328
  scheduleFitPreviewToArea();
2204
2329
  }
@@ -2376,8 +2501,13 @@
2376
2501
  return Math.max(DETAILS_PANEL_MIN_HEIGHT, Math.min(DETAILS_PANEL_MAX_HEIGHT, available));
2377
2502
  }
2378
2503
 
2504
+ function getDetailsPanelMinWidth(): number {
2505
+ if (previewColumnCollapsed) return DETAILS_PANEL_MIN_WIDTH;
2506
+ return DETAILS_PANEL_MIN_WIDTH + COL_RESIZER_PX + 240;
2507
+ }
2508
+
2379
2509
  function clampPanelWidth(w: number): number {
2380
- return Math.max(DETAILS_PANEL_MIN_WIDTH, Math.min(w, DETAILS_PANEL_MAX_WIDTH));
2510
+ return Math.max(getDetailsPanelMinWidth(), Math.min(w, DETAILS_PANEL_MAX_WIDTH));
2381
2511
  }
2382
2512
 
2383
2513
  function clampPanelHeight(h: number): number {
@@ -2420,7 +2550,14 @@
2420
2550
  }
2421
2551
  const cascade = panelCascadeIndex * 32;
2422
2552
  detailsPanelX = Math.max(8, Math.round((window.innerWidth - detailsPanelWidth) / 2) + cascade);
2423
- detailsPanelY = detailsPanelTopOffset + cascade;
2553
+ const availableTop = detailsPanelTopOffset;
2554
+ detailsPanelY = Math.max(
2555
+ availableTop,
2556
+ Math.round(availableTop + (window.innerHeight - availableTop - detailsPanelHeight) / 2) + cascade,
2557
+ );
2558
+ if (detailsPanelY + detailsPanelHeight > window.innerHeight - 8) {
2559
+ detailsPanelY = Math.max(availableTop, window.innerHeight - detailsPanelHeight - 8);
2560
+ }
2424
2561
  }
2425
2562
 
2426
2563
  /** Keep the panel on-screen after a browser resize. Does not reset to default 600×800. */
@@ -2446,8 +2583,7 @@
2446
2583
  }
2447
2584
 
2448
2585
  /**
2449
- * Park the panel to the right of a canvas widget shape. May overlay the
2450
- * shape Props panel. Can hang halfway outside the viewport.
2586
+ * Center Widget Details in the canvas viewport (X and Y), clamped on-screen.
2451
2587
  */
2452
2588
  export function positionBesideShape(shapeRect: {
2453
2589
  left: number;
@@ -2464,24 +2600,28 @@
2464
2600
  _hasBeenOpenedOnce = true;
2465
2601
  if (!shapeRect) {
2466
2602
  centerDetailsPanel();
2467
- console.log('[WidgetDetails] No shape rect — centered floating panel', detailsPanelWidth, detailsPanelHeight);
2603
+ // console.log('[WidgetDetails] No shape rect — centered floating panel', detailsPanelWidth, detailsPanelHeight);
2468
2604
  return;
2469
2605
  }
2470
2606
 
2471
- const gap = DETAILS_PANEL_SHAPE_GAP;
2472
- detailsPanelWidth = clampPanelWidth(DETAILS_PANEL_MAX_WIDTH);
2607
+ detailsPanelWidth = clampPanelWidth(DETAILS_PANEL_DEFAULT_WIDTH);
2473
2608
  detailsPanelHeight = clampPanelHeight(DETAILS_PANEL_MAX_HEIGHT);
2474
2609
 
2475
- const x = shapeRect.left + shapeRect.width + gap;
2476
- const pos = clampDetailsPanelPosition(x, Math.max(detailsPanelTopOffset, shapeRect.top));
2610
+ const viewportLeft = shapeRect.viewportLeft ?? 0;
2611
+ const viewportWidth = shapeRect.viewportWidth || window.innerWidth;
2612
+ const viewportTop = shapeRect.viewportTop ?? detailsPanelTopOffset;
2613
+ const viewportHeight = shapeRect.viewportHeight || (window.innerHeight - viewportTop);
2614
+
2615
+ const x = Math.round(viewportLeft + (viewportWidth - detailsPanelWidth) / 2);
2616
+ const y = Math.round(viewportTop + Math.max(0, (viewportHeight - detailsPanelHeight) / 2));
2617
+ const pos = clampDetailsPanelPosition(x, y);
2477
2618
  detailsPanelX = pos.x;
2478
2619
  detailsPanelY = pos.y;
2479
- console.log('[WidgetDetails] Positioned beside shape', {
2620
+ console.log('[WidgetDetails] Centered in canvas viewport', {
2480
2621
  x: Math.round(detailsPanelX),
2481
2622
  y: Math.round(detailsPanelY),
2482
2623
  w: detailsPanelWidth,
2483
2624
  h: detailsPanelHeight,
2484
- shapeLeft: Math.round(shapeRect.left),
2485
2625
  });
2486
2626
  }
2487
2627
 
@@ -2546,7 +2686,7 @@
2546
2686
 
2547
2687
  if (selectedWidgetId) {
2548
2688
  if (iframeSrc && previewLoadedForWidgetId === selectedWidgetId) {
2549
- console.log('[WidgetDetails] Reopened with existing preview, no reload needed');
2689
+ // console.log('[WidgetDetails] Reopened with existing preview, no reload needed');
2550
2690
  return;
2551
2691
  }
2552
2692
  loadWidget(selectedWidgetId);
@@ -2570,7 +2710,7 @@
2570
2710
  function handleCloseButtonClick(e: MouseEvent) {
2571
2711
  e.preventDefault();
2572
2712
  e.stopPropagation();
2573
- console.log('[WidgetDetails] Close button clicked');
2713
+ // console.log('[WidgetDetails] Close button clicked');
2574
2714
  closeWidgetDetails();
2575
2715
  }
2576
2716
 
@@ -2620,7 +2760,7 @@
2620
2760
  if (!isResizingDetails) return;
2621
2761
  const deltaX = e.clientX - detailsResizeStartX;
2622
2762
  const deltaY = e.clientY - detailsResizeStartY;
2623
- const minW = DETAILS_PANEL_MIN_WIDTH;
2763
+ const minW = getDetailsPanelMinWidth();
2624
2764
  const minH = DETAILS_PANEL_MIN_HEIGHT;
2625
2765
  const maxRight = window.innerWidth - 8;
2626
2766
  const maxBottom = window.innerHeight - 8;
@@ -2692,23 +2832,83 @@
2692
2832
  };
2693
2833
  }
2694
2834
 
2835
+ function updateHeaderNameMaxWidth() {
2836
+ if (!widgetDetailsNameCtEl) return;
2837
+ const nameRect = widgetDetailsNameCtEl.getBoundingClientRect();
2838
+ const rightEl = showCompositionSteps && widgetDetailsStepsRowEl
2839
+ ? widgetDetailsStepsRowEl
2840
+ : widgetDetailsDebugButtonsEl;
2841
+ if (!rightEl) return;
2842
+ const rightRect = rightEl.getBoundingClientRect();
2843
+ const available = Math.floor(rightRect.left - nameRect.left - HEADER_ELEMENTS_GAP_PX);
2844
+ const next = Math.max(HEADER_NAME_MIN_WIDTH_PX, available);
2845
+ if (next !== headerNameMaxWidthPx) {
2846
+ headerNameMaxWidthPx = next;
2847
+ }
2848
+ }
2849
+
2850
+ function trackHeaderNameMaxWidth(node: HTMLElement) {
2851
+ detailsDragBarEl = node;
2852
+ const resizeObserver = new ResizeObserver(() => {
2853
+ requestAnimationFrame(updateHeaderNameMaxWidth);
2854
+ });
2855
+ resizeObserver.observe(node);
2856
+ requestAnimationFrame(updateHeaderNameMaxWidth);
2857
+ return {
2858
+ destroy() {
2859
+ resizeObserver.disconnect();
2860
+ }
2861
+ };
2862
+ }
2863
+
2695
2864
  function startDetailsDrag(e: PointerEvent) {
2696
2865
  const target = e.target as HTMLElement | null;
2697
2866
  if (target?.closest('.step-button, .widget-details-close-bt, .widget-details-debug-buttons, button, a, input, select, textarea')) {
2698
2867
  return;
2699
2868
  }
2700
- handlePanelFocusRequest();
2701
- e.preventDefault();
2702
- e.stopPropagation();
2703
- isDraggingDetails = true;
2704
2869
  detailsDragStartX = e.clientX;
2705
2870
  detailsDragStartY = e.clientY;
2706
2871
  detailsInitialX = detailsPanelX < 0 ? 0 : detailsPanelX;
2707
2872
  detailsInitialY = detailsPanelY < 0 ? detailsPanelTopOffset : detailsPanelY;
2873
+ detailsDragBarEl = e.currentTarget as HTMLElement | null;
2874
+ const fromName = !!target?.closest('.widget-details-name-ct');
2875
+ if (fromName) {
2876
+ pendingDetailsDrag = true;
2877
+ window.addEventListener('pointermove', onPendingDetailsDrag);
2878
+ window.addEventListener('pointerup', cancelPendingDetailsDrag);
2879
+ window.addEventListener('pointercancel', cancelPendingDetailsDrag);
2880
+ return;
2881
+ }
2882
+ beginDetailsDrag(e);
2883
+ }
2884
+
2885
+ function onPendingDetailsDrag(e: PointerEvent) {
2886
+ if (!pendingDetailsDrag) return;
2887
+ const moved = Math.hypot(e.clientX - detailsDragStartX, e.clientY - detailsDragStartY);
2888
+ if (moved < DETAILS_DRAG_THRESHOLD_PX) return;
2889
+ pendingDetailsDrag = false;
2890
+ window.removeEventListener('pointermove', onPendingDetailsDrag);
2891
+ window.removeEventListener('pointerup', cancelPendingDetailsDrag);
2892
+ window.removeEventListener('pointercancel', cancelPendingDetailsDrag);
2893
+ beginDetailsDrag(e);
2894
+ }
2895
+
2896
+ function cancelPendingDetailsDrag() {
2897
+ pendingDetailsDrag = false;
2898
+ window.removeEventListener('pointermove', onPendingDetailsDrag);
2899
+ window.removeEventListener('pointerup', cancelPendingDetailsDrag);
2900
+ window.removeEventListener('pointercancel', cancelPendingDetailsDrag);
2901
+ }
2902
+
2903
+ function beginDetailsDrag(e: PointerEvent) {
2904
+ handlePanelFocusRequest();
2905
+ e.preventDefault();
2906
+ e.stopPropagation();
2907
+ isDraggingDetails = true;
2708
2908
  document.body.style.cursor = 'grabbing';
2709
2909
  document.body.style.userSelect = 'none';
2710
2910
  try {
2711
- (e.currentTarget as HTMLElement | null)?.setPointerCapture?.(e.pointerId);
2911
+ detailsDragBarEl?.setPointerCapture?.(e.pointerId);
2712
2912
  } catch {
2713
2913
  /* ignore capture failures */
2714
2914
  }
@@ -2735,6 +2935,7 @@
2735
2935
  }
2736
2936
 
2737
2937
  function stopDetailsDrag() {
2938
+ cancelPendingDetailsDrag();
2738
2939
  if (!isDraggingDetails) return;
2739
2940
  isDraggingDetails = false;
2740
2941
  document.body.style.cursor = '';
@@ -2883,7 +3084,7 @@
2883
3084
  if (currentToken) setWsAuthToken(currentToken);
2884
3085
  const userId = userSession?.user?.id;
2885
3086
  if (userId) {
2886
- console.log('[WidgetDetails] Connecting WebSocket for user events');
3087
+ // console.log('[WidgetDetails] Connecting WebSocket for user events');
2887
3088
  connectWebSocket(userId);
2888
3089
  }
2889
3090
  }
@@ -2891,17 +3092,18 @@
2891
3092
  let lastWsPreviewSignature: string | null = null;
2892
3093
 
2893
3094
  async function applyWorkerPreviewFromEvent(workerUrl: string, buildTimeMs?: number | null): Promise<void> {
3095
+ if (pinnedPublishedPreviewUrl) return;
2894
3096
  const fetchUrl = toWorkerPreviewFetchUrl(workerUrl);
2895
3097
  const signature = `${fetchUrl}:${buildTimeMs ?? 'na'}:${selectedWidgetId ?? ''}`;
2896
3098
  if (signature === lastWsPreviewSignature) return;
2897
3099
  lastWsPreviewSignature = signature;
2898
3100
 
2899
- console.log('[WidgetDetails] Worker preview ready — force reloading iframe', {
2900
- widgetId: selectedWidgetId,
2901
- previewUrl: fetchUrl,
2902
- displayUrl: toWorkerPreviewDisplayUrl(fetchUrl),
2903
- buildTimeMs,
2904
- });
3101
+ // console.log('[WidgetDetails] Worker preview ready — force reloading iframe', {
3102
+ // widgetId: selectedWidgetId,
3103
+ // previewUrl: fetchUrl,
3104
+ // displayUrl: toWorkerPreviewDisplayUrl(fetchUrl),
3105
+ // buildTimeMs,
3106
+ // });
2905
3107
 
2906
3108
  dynamicWorkerPreviewUrl = fetchUrl;
2907
3109
  directPreviewUrl = toWorkerPreviewDisplayUrl(fetchUrl);
@@ -2947,25 +3149,29 @@
2947
3149
  // Worker-built widgets signal readiness via widgetic:ready (not sveltekit-hydration-complete)
2948
3150
  const handleWidgeticReady = (event: MessageEvent) => {
2949
3151
  if (event.data?.type !== 'widgetic:ready') return;
2950
- console.log('[WidgetDetails] widgetic:ready received — applying queued composition updates');
3152
+ const activeWin = previewIframe?.contentWindow;
3153
+ if (!activeWin || event.source !== activeWin) return;
3154
+ // console.log('[WidgetDetails] widgetic:ready received — applying queued composition updates');
2951
3155
  markPreviewWidgetReady();
2952
3156
  };
2953
3157
  window.addEventListener('message', handleWidgeticReady);
2954
3158
 
2955
3159
  // Listen for SvelteKit hydration completion signal (legacy VM / published SvelteKit artifacts)
2956
3160
  const handleHydrationSignal = (event: MessageEvent) => {
2957
- if (event.data?.type === 'sveltekit-hydration-complete') {
2958
- previewHydrating = false;
2959
- hydrationRetryCount = 0;
2960
- if (hydrationSafetyTimeout) { clearTimeout(hydrationSafetyTimeout); hydrationSafetyTimeout = null; }
2961
- markPreviewWidgetReady();
2962
- }
3161
+ if (event.data?.type !== 'sveltekit-hydration-complete') return;
3162
+ const activeWin = previewIframe?.contentWindow;
3163
+ if (!activeWin || event.source !== activeWin) return;
3164
+ previewHydrating = false;
3165
+ if (hydrationSafetyTimeout) { clearTimeout(hydrationSafetyTimeout); hydrationSafetyTimeout = null; }
3166
+ markPreviewWidgetReady();
2963
3167
  };
2964
3168
  window.addEventListener('message', handleHydrationSignal);
2965
3169
 
2966
3170
  // Listen for runtime error signals from the preview iframe
2967
3171
  const handleIframeRuntimeError = (event: MessageEvent) => {
2968
3172
  if (event.data?.type !== 'sveltekit-runtime-error') return;
3173
+ const activeWin = previewIframe?.contentWindow;
3174
+ if (!activeWin || event.source !== activeWin) return;
2969
3175
 
2970
3176
  const runtimeMsg = String(event.data?.message || event.data?.error || '').trim();
2971
3177
  const runtimeSource = event.data?.source ? String(event.data.source) : '';
@@ -2976,16 +3182,10 @@
2976
3182
  console.warn('[WidgetDetails] Ignoring runtime error during code generation:', runtimeMsg);
2977
3183
  return;
2978
3184
  }
2979
- if (isCodegenPreviewFresh()) {
3185
+ if (isCodegenPreviewFresh() && !looksLikeHardCompileFailure(runtimeMsg)) {
2980
3186
  console.warn('[WidgetDetails] Ignoring runtime error during codegen preview window:', runtimeMsg);
2981
3187
  return;
2982
3188
  }
2983
- // Stale iframe (previous build) must not poison the active preview
2984
- const activeWin = previewIframe?.contentWindow;
2985
- if (activeWin && event.source && event.source !== activeWin) {
2986
- console.warn('[WidgetDetails] Ignoring runtime error from inactive iframe');
2987
- return;
2988
- }
2989
3189
  if (isBenignPreviewRuntimeError(runtimeMsg)) {
2990
3190
  console.warn('[WidgetDetails] Ignoring benign preview runtime error:', runtimeMsg);
2991
3191
  return;
@@ -3036,20 +3236,21 @@
3036
3236
 
3037
3237
  // Listen for expired preview token signal
3038
3238
  const handleTokenExpired = async (event: MessageEvent) => {
3039
- if (event.data?.type === 'preview-token-expired') {
3040
- directPreviewUrl = null;
3041
- lastPreviewUrlFetchTime = 0;
3042
- try {
3043
- const freshUrl = await fetchDirectPreviewUrl();
3044
- if (freshUrl) {
3045
- const cacheBuster = Date.now();
3046
- const url = new URL(freshUrl);
3047
- url.searchParams.set('_t', cacheBuster.toString());
3048
- iframeSrc = url.toString();
3049
- previewCacheBuster = cacheBuster;
3050
- }
3051
- } catch (err) { console.error('Error refreshing preview token:', err); }
3052
- }
3239
+ if (event.data?.type !== 'preview-token-expired') return;
3240
+ const activeWin = previewIframe?.contentWindow;
3241
+ if (!activeWin || event.source !== activeWin) return;
3242
+ directPreviewUrl = null;
3243
+ lastPreviewUrlFetchTime = 0;
3244
+ try {
3245
+ const freshUrl = await fetchDirectPreviewUrl();
3246
+ if (freshUrl) {
3247
+ const cacheBuster = Date.now();
3248
+ const url = new URL(freshUrl);
3249
+ url.searchParams.set('_t', cacheBuster.toString());
3250
+ iframeSrc = url.toString();
3251
+ previewCacheBuster = cacheBuster;
3252
+ }
3253
+ } catch (err) { console.error('Error refreshing preview token:', err); }
3053
3254
  };
3054
3255
  window.addEventListener('message', handleTokenExpired);
3055
3256
 
@@ -3059,12 +3260,14 @@
3059
3260
  // "Rebuild" whenever we lose the L1/L2 cache. (Reset happens on widget switch.)
3060
3261
  const handleWidgetNotFound = async (event: MessageEvent) => {
3061
3262
  if (event.data?.type !== 'widgetic:widget-not-found') return;
3263
+ const activeWin = previewIframe?.contentWindow;
3264
+ if (!activeWin || event.source !== activeWin) return;
3062
3265
  if (isCodegenPreviewFresh()) {
3063
- console.log('[WidgetDetails] Ignoring widget-not-found during codegen preview window');
3266
+ // console.log('[WidgetDetails] Ignoring widget-not-found during codegen preview window');
3064
3267
  return;
3065
3268
  }
3066
3269
  if (dwLoadTimeoutHandle) { clearTimeout(dwLoadTimeoutHandle); dwLoadTimeoutHandle = null; }
3067
- console.log('[WidgetDetails] Worker reports widget not found — checking if repo has code');
3270
+ // console.log('[WidgetDetails] Worker reports widget not found — checking if repo has code');
3068
3271
  dynamicWorkerPreviewUrl = null;
3069
3272
  directPreviewUrl = null;
3070
3273
  iframeSrc = null;
@@ -3075,7 +3278,7 @@
3075
3278
  const hasRepo = !!currentRepositoryId;
3076
3279
  if (hasRepo && capturedWidgetId && widgetNotFoundAutoRebuildAttempts === 0) {
3077
3280
  widgetNotFoundAutoRebuildAttempts = 1;
3078
- console.log('[WidgetDetails] Widget missing from Worker — auto-triggering build-from-repo');
3281
+ // console.log('[WidgetDetails] Widget missing from Worker — auto-triggering build-from-repo');
3079
3282
  previewLoading = true;
3080
3283
  try {
3081
3284
  await triggerBuildFromRepo({ force: true });
@@ -3105,6 +3308,7 @@
3105
3308
  if (contentContainerEl) {
3106
3309
  hasScrollbar = contentContainerEl.scrollHeight > contentContainerEl.clientHeight;
3107
3310
  }
3311
+ updateHeaderNameMaxWidth();
3108
3312
  });
3109
3313
 
3110
3314
  onDestroy(() => {
@@ -3133,13 +3337,13 @@
3133
3337
  onclick={handleCloseButtonClick}
3134
3338
  onmousedown={(e) => e.stopPropagation()}
3135
3339
  onpointerdown={(e) => e.stopPropagation()}
3136
- class="widget-details-close-bt pointer-events-auto absolute -top-2.5 -right-2.5 z-30 w-7 h-7 flex items-center justify-center rounded-full bg-white border border-gray-300 shadow-md hover:bg-red-50 hover:border-red-300 hover:text-red-600 text-gray-500 cursor-pointer transition-colors text-sm"
3340
+ class="widget-details-close-bt pointer-events-auto absolute -top-3 -right-3 z-40 w-7 h-7 flex items-center justify-center rounded-full bg-white border border-gray-300 shadow-md hover:bg-red-50 hover:border-red-300 hover:text-red-600 text-gray-500 cursor-pointer transition-colors text-sm"
3137
3341
  title="Close widget details"
3138
3342
  >
3139
3343
  ✕
3140
3344
  </button>
3141
3345
  <div
3142
- class="widget-details-floating-panel pointer-events-auto relative flex flex-col h-full w-full
3346
+ class="widget-details-floating-panel pointer-events-auto relative flex flex-col h-full w-full min-w-[480px]
3143
3347
  {fillViewport
3144
3348
  ? 'bg-white rounded-none border-x border-b border-t-0 ring-0 shadow-none overflow-hidden'
3145
3349
  : 'bg-white/95 backdrop-blur-sm shadow-2xl rounded-xl border overflow-hidden'}
@@ -3151,23 +3355,44 @@
3151
3355
  <!-- Widget Details Top Bar — title, steps, and close on one row -->
3152
3356
  <div class="widget-details-header-ct flex flex-col shrink-0 bg-gray-100 border-b border-gray-200">
3153
3357
  <div
3154
- class="widget-details-drag-bar relative flex items-center justify-end gap-2 px-3 py-1.5 cursor-grab active:cursor-grabbing select-none min-h-[40px] touch-none flex-nowrap overflow-hidden"
3358
+ class="widget-details-drag-bar relative flex items-center py-1.5 cursor-grab active:cursor-grabbing select-none min-h-[40px] touch-none flex-nowrap min-w-0"
3359
+ style="--header-elements-gap: {HEADER_ELEMENTS_GAP_PX}px; gap: {HEADER_ELEMENTS_GAP_PX}px; padding-left: {HEADER_PADDING_PX}px; padding-right: {HEADER_PADDING_PX}px;"
3360
+ bind:this={detailsDragBarEl}
3361
+ use:trackHeaderNameMaxWidth
3155
3362
  onpointerdown={startDetailsDrag}
3156
3363
  >
3157
- <span
3158
- class="widget-details-name pointer-events-none absolute left-3 top-1/2 -translate-y-1/2 max-w-[28%] truncate text-sm font-bold text-gray-800"
3159
- title={selectedWidget?.name || ''}
3364
+ <!-- svelte-ignore a11y_no_static_element_interactions -->
3365
+ <div
3366
+ class="widget-details-name-ct relative z-[1] min-w-0 w-fit overflow-hidden"
3367
+ bind:this={widgetDetailsNameCtEl}
3368
+ style="min-width: {HEADER_NAME_MIN_WIDTH_PX}px; max-width: {headerNameMaxWidthPx}px;"
3160
3369
  >
3161
- {selectedWidget?.name || (isDraftConvertPanel ? 'Widget Draft' : 'Widget')}
3162
- </span>
3163
- <!-- Step Indicator — centered; name is compact on the left (read-only, no rename) -->
3164
- <div class="widget-details-step-indicator-row pointer-events-none absolute left-1/2 -translate-x-1/2 flex justify-center px-1">
3165
- <div class="widget-details-step-indicator pointer-events-auto flex items-center gap-1 flex-nowrap justify-center whitespace-nowrap">
3370
+ <EditableName
3371
+ class="widget-details-name min-w-0 truncate text-sm font-bold text-gray-800"
3372
+ name={selectedWidget?.name || ''}
3373
+ placeholder={isDraftConvertPanel ? 'Widget Draft' : 'Widget'}
3374
+ tooltipText="Double-click to rename"
3375
+ on:rename={(e) => {
3376
+ if (!selectedWidgetId) return;
3377
+ dispatch('widgetRename', { widgetId: selectedWidgetId, newName: e.detail.newName });
3378
+ }}
3379
+ />
3380
+ </div>
3381
+ {#if showCompositionSteps}
3382
+ <div
3383
+ class="widget-details-step-indicator-row pointer-events-none absolute left-1/2 top-1/2 z-0 flex -translate-x-1/2 -translate-y-1/2 justify-center"
3384
+ bind:this={widgetDetailsStepsRowEl}
3385
+ >
3386
+ <div
3387
+ class="widget-details-step-indicator pointer-events-auto flex items-center flex-nowrap justify-center whitespace-nowrap"
3388
+ style="gap: {HEADER_ELEMENTS_GAP_PX}px;"
3389
+ >
3390
+ <HoverTooltip text="Create widget code" class="inline-flex shrink-0">
3166
3391
  <!-- svelte-ignore a11y_consider_explicit_label -->
3167
3392
  <button
3168
3393
  type="button"
3169
3394
  onclick={() => goToStep('create')}
3170
- class="step-button step-button-create flex items-center gap-1 px-2 py-0.5 rounded-md text-xs font-medium transition-all
3395
+ class="step-button step-button-create flex items-center gap-0.5 px-1 py-0.5 rounded-md text-xs font-medium transition-all
3171
3396
  {currentStep === 'create'
3172
3397
  ? 'bg-blue-100 text-blue-700 border border-blue-300 cursor-pointer'
3173
3398
  : 'text-gray-600 hover:bg-gray-200 border border-transparent cursor-pointer'}"
@@ -3175,15 +3400,16 @@
3175
3400
  <span class="step-number inline-flex items-center justify-center w-5 h-5 rounded-full text-[11px] font-bold leading-none {currentStep === 'create' ? 'bg-blue-600 text-white' : 'bg-gray-300 text-gray-700'}">1</span>
3176
3401
  <span class="step-label">Create</span>
3177
3402
  </button>
3178
- <div class="step-connector w-4 h-px {canEditStep ? 'bg-gray-400' : 'bg-gray-200'}"></div>
3403
+ </HoverTooltip>
3404
+ <span class="step-connector text-sm font-semibold leading-none {canEditStep ? 'text-gray-500' : 'text-gray-300'}" aria-hidden="true">→</span>
3405
+ <HoverTooltip text={canEditStep ? 'Edit composition' : 'Publish the widget to edit composition'} class="inline-flex shrink-0">
3179
3406
  <!-- svelte-ignore a11y_consider_explicit_label -->
3180
3407
  <button
3181
3408
  type="button"
3182
3409
  onclick={() => goToStep('edit')}
3183
3410
  tabindex={canEditStep ? 0 : -1}
3184
3411
  aria-disabled={!canEditStep}
3185
- title={canEditStep ? 'Edit composition' : 'Publish the widget to edit composition'}
3186
- class="step-button step-button-edit flex items-center gap-1 px-2 py-0.5 rounded-md text-xs font-medium transition-all
3412
+ class="step-button step-button-edit flex items-center gap-0.5 px-1 py-0.5 rounded-md text-xs font-medium transition-all
3187
3413
  {currentStep === 'edit'
3188
3414
  ? 'bg-blue-100 text-blue-700 border border-blue-300 cursor-pointer'
3189
3415
  : canEditStep
@@ -3193,15 +3419,16 @@
3193
3419
  <span class="step-number inline-flex items-center justify-center w-5 h-5 rounded-full text-[11px] font-bold leading-none {currentStep === 'edit' ? 'bg-blue-600 text-white' : canEditStep ? 'bg-gray-300 text-gray-700' : 'bg-gray-200 text-gray-400'}">2</span>
3194
3420
  <span class="step-label">Edit</span>
3195
3421
  </button>
3196
- <div class="step-connector w-4 h-px {canEmbedStep ? 'bg-gray-400' : 'bg-gray-200'}"></div>
3422
+ </HoverTooltip>
3423
+ <span class="step-connector text-sm font-semibold leading-none {canEmbedStep ? 'text-gray-500' : 'text-gray-300'}" aria-hidden="true">→</span>
3424
+ <HoverTooltip text={canEmbedStep ? 'Embed the published widget' : 'Publish the widget to get embed code'} class="inline-flex shrink-0">
3197
3425
  <!-- svelte-ignore a11y_consider_explicit_label -->
3198
3426
  <button
3199
3427
  type="button"
3200
3428
  onclick={() => goToStep('embed')}
3201
3429
  tabindex={canEmbedStep ? 0 : -1}
3202
3430
  aria-disabled={!canEmbedStep}
3203
- title={canEmbedStep ? 'Embed the published widget' : 'Publish the widget to get embed code'}
3204
- class="step-button step-button-embed flex items-center gap-1 px-2 py-0.5 rounded-md text-xs font-medium transition-all
3431
+ class="step-button step-button-embed flex items-center gap-0.5 px-1 py-0.5 rounded-md text-xs font-medium transition-all
3205
3432
  {currentStep === 'embed'
3206
3433
  ? 'bg-blue-100 text-blue-700 border border-blue-300 cursor-pointer'
3207
3434
  : canEmbedStep
@@ -3211,41 +3438,61 @@
3211
3438
  <span class="step-number inline-flex items-center justify-center w-5 h-5 rounded-full text-[11px] font-bold leading-none {currentStep === 'embed' ? 'bg-blue-600 text-white' : canEmbedStep ? 'bg-gray-300 text-gray-700' : 'bg-gray-200 text-gray-400'}">3</span>
3212
3439
  <span class="step-label">Embed</span>
3213
3440
  </button>
3441
+ </HoverTooltip>
3214
3442
  </div>
3215
3443
  </div>
3444
+ {/if}
3216
3445
 
3217
- <div class="widget-details-debug-buttons flex items-center gap-1.5 shrink-0 pr-1">
3446
+ <div
3447
+ class="widget-details-debug-buttons relative z-[1] ml-auto flex items-center shrink-0"
3448
+ bind:this={widgetDetailsDebugButtonsEl}
3449
+ >
3450
+ <HoverTooltip text={previewColumnCollapsed ? 'Open live preview' : 'Close live preview'}>
3218
3451
  <button
3219
3452
  type="button"
3220
3453
  onclick={() => previewColumnCollapsed ? expandPreviewColumn() : collapsePreviewColumn()}
3221
3454
  onmousedown={(e) => e.stopPropagation()}
3222
3455
  onpointerdown={(e) => e.stopPropagation()}
3223
- class="widget-details-preview-toggle-bt h-6 min-w-[6.75rem] px-2 flex items-center justify-center rounded-lg text-[11px] font-medium cursor-pointer transition-colors whitespace-nowrap
3456
+ aria-label={previewColumnCollapsed ? 'Open live preview' : 'Close live preview'}
3457
+ class="widget-details-preview-toggle-bt h-6 w-6 flex items-center justify-center rounded-lg cursor-pointer transition-colors
3224
3458
  {previewColumnCollapsed
3225
3459
  ? 'bg-white border border-gray-300 text-gray-600 hover:bg-gray-100'
3226
3460
  : 'bg-blue-50 border border-blue-300 text-blue-700 hover:bg-blue-100'}"
3227
- title={previewColumnCollapsed ? 'Open live preview column' : 'Close live preview column'}
3228
3461
  >
3229
- {previewColumnCollapsed ? 'Open Preview' : 'Close Preview'}
3462
+ {#if previewColumnCollapsed}
3463
+ <svg class="w-3.5 h-3.5" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true">
3464
+ <rect x="3" y="4" width="18" height="16" rx="2"/>
3465
+ <path d="M14 4v16"/>
3466
+ <path d="M17 9l3 3-3 3"/>
3467
+ </svg>
3468
+ {:else}
3469
+ <svg class="w-3.5 h-3.5" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true">
3470
+ <rect x="3" y="4" width="18" height="16" rx="2"/>
3471
+ <path d="M14 4v16"/>
3472
+ <path d="M10 15L7 12l3-3"/>
3473
+ </svg>
3474
+ {/if}
3230
3475
  </button>
3476
+ </HoverTooltip>
3231
3477
  </div>
3232
3478
  </div>
3233
3479
  </div>
3234
3480
 
3235
3481
  <!-- Widget Details Content: Two-column layout -->
3236
- <div class="widget-details-content-wrapper relative m-2.5 min-w-0 min-h-0 flex-1 overflow-x-hidden">
3237
- <div class="widget-details-two-col flex h-full min-h-0 min-w-0 gap-0 overflow-x-hidden">
3482
+ <div class="widget-details-content-wrapper relative m-2.5 min-w-[460px] min-h-0 flex-1 overflow-hidden">
3483
+ <div class="widget-details-two-col flex h-full min-h-0 min-w-[460px] gap-0 overflow-hidden">
3238
3484
 
3239
3485
  <!-- ═══ LEFT COLUMN: Step Content (scrollable) ═══ -->
3240
3486
  <div
3241
- class="widget-details-left-col flex flex-col min-w-0 min-h-0 relative"
3487
+ class="widget-details-left-col flex flex-col min-w-[460px] min-h-0 overflow-hidden relative"
3242
3488
  style="width: {previewColumnCollapsed ? 100 : leftColPercent}%; flex: 0 0 {previewColumnCollapsed ? 100 : leftColPercent}%;"
3243
3489
  >
3244
3490
  <div class="widget-details-left-border absolute inset-y-0 left-0 right-0 border border-gray-300 rounded-lg pointer-events-none z-10"></div>
3245
3491
 
3246
3492
  {#if selectedWidgetId}
3493
+ <div class="widget-details-left-stack relative z-0 flex flex-col h-full min-h-0 min-w-[460px] overflow-hidden">
3247
3494
  <!-- Sticky Widget Info Bar (always visible above scrollable content) -->
3248
- <div class="widget-info-sticky-bar flex items-center justify-between px-3 py-1.5 border-b border-gray-200 bg-gray-50 rounded-t-lg shrink-0 gap-2 min-w-0 z-5">
3495
+ <div class="widget-info-sticky-bar relative flex items-center justify-between px-3 py-1.5 border-b border-gray-200 bg-gray-50 rounded-t-lg shrink-0 gap-2 min-w-0 z-20 overflow-visible">
3249
3496
  <div class="widget-info-name flex items-center min-w-0 gap-1 text-sm text-gray-700">
3250
3497
  <!-- svelte-ignore a11y-click-events-have-key-events -->
3251
3498
  <!-- svelte-ignore a11y-no-static-element-interactions -->
@@ -3263,12 +3510,14 @@
3263
3510
  {:else if saveStatus === 'error'}
3264
3511
  <span class="text-[10px] px-1.5 py-0.5 rounded bg-red-100 text-red-700">Save Error</span>
3265
3512
  {/if}
3513
+ {#if debugMode}
3266
3514
  <span class="text-[10px] text-gray-500">
3267
3515
  Commit: <span class="font-mono font-medium text-gray-600" title={lastCommitId || ''}>{lastCommitId ? lastCommitId.substring(0, 8) : '—'}</span>
3268
3516
  </span>
3269
- {#if lastPublishedVersion !== null}
3270
- <a href="/test-widget?widgetId={selectedWidgetId}&version={lastPublishedVersion}" target="_blank" rel="noopener noreferrer" class="text-[10px] px-1.5 py-0.5 rounded bg-emerald-600 border border-emerald-700 text-white hover:bg-emerald-700 transition-colors whitespace-nowrap cursor-pointer" title="Open published widget v{lastPublishedVersion}">Live: v{lastPublishedVersion} ↗</a>
3271
3517
  {/if}
3518
+ <div class="widget-info-version-history-slot relative z-20 shrink-0">
3519
+ <slot name="version-history" />
3520
+ </div>
3272
3521
  </div>
3273
3522
  </div>
3274
3523
 
@@ -3280,23 +3529,34 @@
3280
3529
  <div class="actions-panel widget-details-actions-panel relative flex flex-col flex-1 min-h-0">
3281
3530
 
3282
3531
  <!-- Keep all steps mounted — switching tabs must not destroy chat / PropsEditor state -->
3283
- <div class="widget-details-create-step flex flex-col flex-1 min-h-0 overflow-hidden overflow-x-hidden {currentStep === 'create' ? '' : 'hidden'}">
3284
- <div class="widget-details-create-chat-ct flex flex-col flex-1 min-h-0 overflow-y-auto overflow-x-hidden scrollbar-thin gap-2 p-2">
3285
- <slot name="code-generation" />
3532
+ <div class="widget-details-create-step flex flex-col flex-1 min-h-0 overflow-hidden overflow-x-hidden {activeWorkflowStep === 'create' ? '' : 'hidden'}">
3533
+ <div class="widget-details-create-models-ct shrink-0 empty:hidden">
3534
+ <slot name="code-generation-models" />
3535
+ </div>
3536
+ <div
3537
+ class="widget-details-create-chat-ct flex flex-col flex-1 min-h-0 overflow-hidden overflow-x-hidden"
3538
+ >
3539
+ <div class="widget-details-create-status-slot shrink-0 empty:hidden">
3540
+ <slot name="code-generation-status" />
3541
+ </div>
3542
+ <div class="widget-details-create-chat-body flex flex-col flex-1 min-h-0 overflow-hidden">
3543
+ <slot name="code-generation" />
3544
+ </div>
3286
3545
  </div>
3287
- <div class="widget-details-create-publish-ct shrink-0 px-2 pb-2">
3546
+ <div class="widget-details-create-publish-ct shrink-0 empty:hidden">
3288
3547
  <slot name="publish-widget" />
3289
3548
  </div>
3290
3549
  </div>
3291
- <div class="widget-details-edit-step flex flex-col h-full min-h-0 overflow-y-auto overflow-x-hidden scrollbar-thin {currentStep === 'edit' ? '' : 'hidden'}">
3550
+ <div class="widget-details-edit-step flex flex-col flex-1 min-h-0 overflow-hidden {activeWorkflowStep === 'edit' ? '' : 'hidden'}">
3292
3551
  <slot name="composition-editor" />
3293
3552
  </div>
3294
- <div class="widget-details-embed-step flex flex-col gap-2 p-2 {currentStep === 'embed' ? '' : 'hidden'}">
3553
+ <div class="widget-details-embed-step flex flex-col flex-1 min-h-0 overflow-y-auto overflow-x-hidden scrollbar-thin gap-2 p-2 {activeWorkflowStep === 'embed' ? '' : 'hidden'}">
3295
3554
  <slot name="embed-section" />
3296
3555
  </div>
3297
3556
 
3298
3557
  </div> <!-- end of actions-panel -->
3299
3558
  </div> <!-- end of widget-details-left-content -->
3559
+ </div> <!-- end of widget-details-left-stack -->
3300
3560
  {/if}
3301
3561
 
3302
3562
  </div> <!-- end of widget-details-left-col -->
@@ -3581,14 +3841,38 @@
3581
3841
  {/if}
3582
3842
 
3583
3843
  <style>
3584
- .widget-details-left-content {
3585
- overflow: hidden;
3586
- overflow-x: hidden;
3844
+ .widget-details-drag-bar button:focus,
3845
+ .widget-details-drag-bar button:focus-visible,
3846
+ .widget-details-drag-bar a:focus,
3847
+ .widget-details-drag-bar a:focus-visible {
3848
+ outline: none;
3849
+ box-shadow: none;
3850
+ }
3851
+ .widget-details-create-models-ct :global(button:focus),
3852
+ .widget-details-create-models-ct :global(button:focus-visible),
3853
+ .widget-details-create-models-ct :global(select:focus),
3854
+ .widget-details-create-models-ct :global(select:focus-visible) {
3855
+ outline: none;
3856
+ box-shadow: none;
3857
+ }
3858
+ .widget-details-create-chat-ct :global(.prompt-chat-messages) {
3859
+ overflow-y: auto;
3587
3860
  scrollbar-width: thin;
3588
- scrollbar-color: rgba(0, 0, 0, 0.15) transparent;
3861
+ scrollbar-color: rgba(0, 0, 0, 0.22) transparent;
3862
+ }
3863
+ .widget-details-create-chat-ct :global(.prompt-chat-messages::-webkit-scrollbar) {
3864
+ width: 6px;
3865
+ background: transparent;
3866
+ }
3867
+ .widget-details-create-chat-ct :global(.prompt-chat-messages::-webkit-scrollbar-track) {
3868
+ background: transparent;
3869
+ }
3870
+ .widget-details-create-chat-ct :global(.prompt-chat-messages::-webkit-scrollbar-thumb) {
3871
+ background: rgba(0, 0, 0, 0.2);
3872
+ border-radius: 4px;
3589
3873
  }
3590
3874
  .widget-details-left-content::-webkit-scrollbar {
3591
- width: 10px;
3875
+ width: 6px;
3592
3876
  background: transparent;
3593
3877
  }
3594
3878
  .widget-details-left-content::-webkit-scrollbar-track {