@widgetic/creator 0.3.53 → 0.3.55

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,8 @@
278
285
  // ═══════════════════════════════════════════════════════════════════════
279
286
  type WidgetStep = 'create' | 'edit' | 'embed';
280
287
  export let currentStep: WidgetStep = 'create';
288
+ /** After Set live, Create preview stays on that published artifact until the next Worker build. */
289
+ let pinnedPublishedPreviewUrl: string | null = null;
281
290
 
282
291
  const STEPS: { id: WidgetStep; label: string; number: number }[] = [
283
292
  { id: 'create', label: 'Create', number: 1 },
@@ -314,6 +323,7 @@
314
323
  previewError = null;
315
324
  previewHasRuntimeError = false;
316
325
  previewRuntimeErrorStatus = null;
326
+ pinnedPublishedPreviewUrl = null;
317
327
  }
318
328
 
319
329
  // Default step on widget switch: always open on Create
@@ -326,6 +336,7 @@
326
336
  }
327
337
  currentStep = 'create';
328
338
  dispatch('stepChange', { step: 'create' });
339
+ pinnedPublishedPreviewUrl = null;
329
340
  const createBase = normalizePreviewBaseUrl(
330
341
  toPreviewFetchUrl(resolvePreviewUrlForStep('create') ?? '')
331
342
  );
@@ -399,6 +410,10 @@
399
410
  dynamicWorkerPreviewUrl = workerUrl;
400
411
  syncPreviewDisplayUrlForStep('create');
401
412
  void forceReloadPreviewIframe(workerUrl);
413
+ } else if ((step === 'edit' || step === 'embed') && nextResolved) {
414
+ // Always remount Edit/Embed on the published URL — skip-reload left the previous
415
+ // live version in the iframe after auto-publish on Create.
416
+ void forceReloadPreviewIframe(nextResolved);
402
417
  } else {
403
418
  switchPreviewUrlForStep(step, { forceReload: crossingPreviewFamily });
404
419
  }
@@ -422,7 +437,7 @@
422
437
  /** Resolve the canonical preview URL for a workflow step (without cache buster). */
423
438
  function resolvePreviewUrlForStep(step: WidgetStep): string | null {
424
439
  if (step === 'create') {
425
- return dynamicWorkerPreviewUrl;
440
+ return pinnedPublishedPreviewUrl || dynamicWorkerPreviewUrl;
426
441
  }
427
442
  // Use published URL unless probe already confirmed it missing.
428
443
  // Waiting for publishedArtifactReachable === true left Edit/Embed on the idle
@@ -454,6 +469,16 @@
454
469
  }, 3000);
455
470
  }
456
471
 
472
+ /** Load a published CDN artifact (Set live) even on Create — pin until next Worker build. */
473
+ export function showPublishedArtifact(url: string | null): void {
474
+ if (!url) return;
475
+ pinnedPublishedPreviewUrl = url;
476
+ publishedArtifactReachable = true;
477
+ _lastEditPreviewBase = null;
478
+ _lastEmbedPreviewBase = null;
479
+ void forceReloadPreviewIframe(url);
480
+ }
481
+
457
482
  export function finishCompositionHydration(): void {
458
483
  if (compositionHydrationTimeout) {
459
484
  clearTimeout(compositionHydrationTimeout);
@@ -532,9 +557,12 @@
532
557
  }
533
558
  directPreviewUrl = toPreviewDisplayUrl(fetchUrl);
534
559
  let nextFull = fetchUrl;
560
+ const isImmutableCdnUrl = /cdn\.widgetic\.com/i.test(fetchUrl) && /\/v\d+\//i.test(fetchUrl);
535
561
  try {
536
562
  const parsed = new URL(fetchUrl, typeof window !== 'undefined' ? window.location.origin : 'http://localhost');
537
- parsed.searchParams.set('_t', (cacheBuster ?? Date.now()).toString());
563
+ if (!isImmutableCdnUrl || cacheBuster !== undefined) {
564
+ parsed.searchParams.set('_t', (cacheBuster ?? Date.now()).toString());
565
+ }
538
566
  nextFull = parsed.toString();
539
567
  } catch {
540
568
  nextFull = fetchUrl;
@@ -666,8 +694,7 @@
666
694
  const editBase = editSource ? normalizePreviewBaseUrl(toPreviewFetchUrl(editSource)) : null;
667
695
  if (editBase && editBase !== _lastEditPreviewBase) {
668
696
  _lastEditPreviewBase = editBase;
669
- // Prop-driven URL changes must apply even right after step switch (parallel panels)
670
- switchPreviewUrlForStep('edit', { forceReload: !_stepSwitchInProgress });
697
+ void forceReloadPreviewIframe(editSource);
671
698
  }
672
699
  }
673
700
 
@@ -753,7 +780,7 @@
753
780
  export function markCodegenPreviewFresh(): void {
754
781
  codegenPreviewFreshUntil = Date.now() + 30_000;
755
782
  widgetNotFoundAutoRebuildAttempts = 1;
756
- console.log('[WidgetDetails] Codegen preview marked fresh — skipping GitLab rebuilds for 30s');
783
+ // console.log('[WidgetDetails] Codegen preview marked fresh — skipping GitLab rebuilds for 30s');
757
784
  }
758
785
 
759
786
  /** True when the iframe is already showing this widget's Worker preview (not about:blank / error). */
@@ -812,6 +839,8 @@
812
839
 
813
840
  function handleWidgetConsoleMessage(event: MessageEvent) {
814
841
  if (!event.data || typeof event.data !== 'object') return;
842
+ const activeWin = previewIframe?.contentWindow;
843
+ if (!activeWin || event.source !== activeWin) return;
815
844
  const { type, level, message, args } = event.data;
816
845
  if (type !== 'widgetic:console') return;
817
846
  const entry: ConsoleLogEntry = {
@@ -869,19 +898,19 @@
869
898
  $: detailsPanelTopOffset = panelTopOffset ?? legacyTopOffset;
870
899
  $: detailsPanelBottomMargin = panelBottomMargin ?? legacyBottomMargin;
871
900
 
872
- const DETAILS_PANEL_DEFAULT_WIDTH = 800;
873
- const DETAILS_PANEL_MAX_WIDTH = 800;
901
+ const DETAILS_PANEL_DEFAULT_WIDTH = 1000;
902
+ const DETAILS_PANEL_MAX_WIDTH = 1200;
874
903
  const DETAILS_PANEL_MAX_HEIGHT = 800;
875
- const DETAILS_PANEL_MIN_WIDTH = 320;
904
+ const DETAILS_PANEL_MIN_WIDTH = 480;
876
905
  const DETAILS_PANEL_MIN_HEIGHT = 400;
877
906
  const DETAILS_PANEL_FILL_INSET = 16;
878
907
  const DETAILS_PANEL_SHAPE_GAP = 12;
879
- /** Left column share of the two-col layout (chat / PropsEditor). */
880
- const LEFT_COL_DEFAULT_PERCENT = 58;
908
+ /** Left column share of the two-col layout (chat / PropsEditor). Preview gets the rest. */
909
+ const LEFT_COL_DEFAULT_PERCENT = 50;
881
910
  const LEFT_COL_MIN_PERCENT = 32;
882
- const LEFT_COL_MAX_PERCENT = 72;
911
+ const LEFT_COL_MAX_PERCENT = 62;
883
912
  const COL_RESIZER_PX = 10;
884
- const PREVIEW_COL_MIN_PX = 240;
913
+ const PREVIEW_COL_MIN_PX = 420;
885
914
  /** Chat-first layout: preview column starts collapsed and opens on generate. */
886
915
  let previewColumnCollapsed = true;
887
916
  let lastAutoExpandForGenerating = false;
@@ -924,36 +953,32 @@
924
953
 
925
954
  function collapsePreviewColumn() {
926
955
  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);
956
+ widthBeforePreviewExpand = 0;
957
+ leftColPercent = LEFT_COL_DEFAULT_PERCENT;
958
+ detailsPanelWidth = DETAILS_PANEL_MIN_WIDTH;
959
+ console.log('[WidgetDetails] Close Preview — panel width', detailsPanelWidth);
933
960
  }
934
961
 
935
962
  function expandPreviewColumn() {
936
963
  if (!previewColumnCollapsed) return;
937
- widthBeforePreviewExpand = detailsPanelWidth;
938
964
  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
- });
965
+ leftColPercent = clampLeftColPercent(LEFT_COL_DEFAULT_PERCENT);
966
+ detailsPanelWidth = clampPanelWidth(DETAILS_PANEL_DEFAULT_WIDTH);
967
+ if (typeof window !== 'undefined' && detailsPanelX + detailsPanelWidth > window.innerWidth - 8) {
968
+ detailsPanelX = Math.max(8, window.innerWidth - 8 - detailsPanelWidth);
969
+ }
970
+ console.log('[WidgetDetails] Open Preview — panel width', detailsPanelWidth);
951
971
  tick().then(() => requestAnimationFrame(() => fitPreviewToArea()));
952
972
  }
953
973
 
954
974
  let detailsPanelX = -1;
955
975
  let detailsPanelY = -1;
956
976
  let isDraggingDetails = false;
977
+ let pendingDetailsDrag = false;
978
+ let detailsDragBarEl: HTMLElement | null = null;
979
+ let widgetDetailsNameCtEl: HTMLElement | null = null;
980
+ let widgetDetailsStepsRowEl: HTMLElement | null = null;
981
+ let headerNameMaxWidthPx = 96;
957
982
  let detailsDragStartX = 0;
958
983
  let detailsDragStartY = 0;
959
984
  let detailsInitialX = 0;
@@ -998,7 +1023,7 @@
998
1023
  const pageOrigin = typeof window !== 'undefined' ? window.location.origin : '';
999
1024
  const isApiProxy = !!(apiBaseUrl && fetchUrl.startsWith(apiBaseUrl));
1000
1025
  if (pageOrigin && absolute.origin !== pageOrigin && !isApiProxy) {
1001
- console.log('[WidgetDetails] Skipping cross-origin HEAD probe (CORS); loading in iframe:', absolute.origin);
1026
+ // console.log('[WidgetDetails] Skipping cross-origin HEAD probe (CORS); loading in iframe:', absolute.origin);
1002
1027
  return true;
1003
1028
  }
1004
1029
  } catch {
@@ -1057,11 +1082,11 @@
1057
1082
  if (iframeSrc && previewLoadedForWidgetId === widgetId) {
1058
1083
  previewBuildDeferred = false;
1059
1084
  previewLoading = false;
1060
- console.log('[WidgetDetails] Preview already loaded, skipping pipeline:', widgetId.substring(0, 8));
1085
+ // console.log('[WidgetDetails] Preview already loaded, skipping pipeline:', widgetId.substring(0, 8));
1061
1086
  return;
1062
1087
  }
1063
1088
  if (shouldDeferPreviewNetworkWork()) {
1064
- console.log('[WidgetDetails] Deferring preview load (background panel):', widgetId.substring(0, 8));
1089
+ // console.log('[WidgetDetails] Deferring preview load (background panel):', widgetId.substring(0, 8));
1065
1090
  previewBuildDeferred = true;
1066
1091
  previewLoading = false;
1067
1092
  previewError = null;
@@ -1092,10 +1117,10 @@
1092
1117
  }
1093
1118
 
1094
1119
  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
- });
1120
+ // console.log('[WidgetDetails] Widget not on Worker — published artifact or build-from-repo', {
1121
+ // widgetId: capturedWidgetId.substring(0, 8),
1122
+ // hasJwt: !!getAuthJwt(),
1123
+ // });
1099
1124
  if (isGeneratingCode) {
1100
1125
  previewLoading = true;
1101
1126
  previewError = null;
@@ -1108,13 +1133,13 @@
1108
1133
  if (!hasRepo) {
1109
1134
  const fallbackUrl = publishedHtmlUrlFromJsPath(widgetJsPath);
1110
1135
  if (fallbackUrl) {
1111
- console.log('[WidgetDetails] No repo yet — loading js_path artifact', fallbackUrl);
1136
+ // console.log('[WidgetDetails] No repo yet — loading js_path artifact', fallbackUrl);
1112
1137
  previewError = null;
1113
1138
  applyPreviewUrlToIframe(fallbackUrl);
1114
1139
  previewLoading = false;
1115
1140
  return;
1116
1141
  }
1117
- console.log('[WidgetDetails] No repository yet — skipping build-from-repo');
1142
+ // console.log('[WidgetDetails] No repository yet — skipping build-from-repo');
1118
1143
  previewLoading = false;
1119
1144
  previewError = 'NO_CODE_YET';
1120
1145
  return;
@@ -1127,7 +1152,7 @@
1127
1152
  directPreviewUrl = workerPreviewUrl;
1128
1153
  previewLoading = true;
1129
1154
  switchPreviewUrlForStep(currentStep);
1130
- console.log('[WidgetDetails] Loading preview iframe:', workerPreviewUrl, 'step:', currentStep);
1155
+ // console.log('[WidgetDetails] Loading preview iframe:', workerPreviewUrl, 'step:', currentStep);
1131
1156
  requestAnimationFrame(() => {
1132
1157
  setTimeout(fitPreviewToArea, 150);
1133
1158
  });
@@ -1139,7 +1164,7 @@
1139
1164
  if (!previewBuildDeferred || !selectedWidgetId) return;
1140
1165
  if (iframeSrc && previewLoadedForWidgetId === selectedWidgetId) {
1141
1166
  previewBuildDeferred = false;
1142
- console.log('[WidgetDetails] Skip deferred preview — already loaded for', selectedWidgetId.substring(0, 8));
1167
+ // console.log('[WidgetDetails] Skip deferred preview — already loaded for', selectedWidgetId.substring(0, 8));
1143
1168
  return;
1144
1169
  }
1145
1170
  previewBuildDeferred = false;
@@ -1199,18 +1224,18 @@
1199
1224
 
1200
1225
  await tick();
1201
1226
 
1202
- console.log('WidgetDetails: loadWidget completed:', {
1203
- widgetId,
1204
- name: selectedWidget?.name,
1205
- currentRepositoryId,
1206
- });
1227
+ // console.log('WidgetDetails: loadWidget completed:', {
1228
+ // widgetId,
1229
+ // name: selectedWidget?.name,
1230
+ // currentRepositoryId,
1231
+ // });
1207
1232
 
1208
1233
  dispatch('widgetReady', { widgetId: selectedWidgetId });
1209
1234
 
1210
1235
  // If skipBasePreview is set, the caller will handle compiling via triggerBuildFromRepo
1211
1236
  // (e.g. duplicates waiting for GitLab fork, or converts waiting for code generation)
1212
1237
  if (skipBasePreview) {
1213
- console.log('[WidgetDetails] skipBasePreview is set — deferring preview load to caller');
1238
+ // console.log('[WidgetDetails] skipBasePreview is set — deferring preview load to caller');
1214
1239
  previewLoading = true;
1215
1240
  previewError = null;
1216
1241
  return;
@@ -1222,11 +1247,11 @@
1222
1247
 
1223
1248
  const hasRepo = !!(normalizeRepositoryId(selectedWidget) || currentRepositoryId);
1224
1249
  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
- });
1250
+ // console.log('[WidgetDetails] No code yet — skipping Worker probe/build-from-repo', {
1251
+ // widgetId: widgetId?.substring(0, 8),
1252
+ // hasRepo,
1253
+ // repositorySetupStatus,
1254
+ // });
1230
1255
  previewLoading = false;
1231
1256
  previewError = 'NO_CODE_YET';
1232
1257
  return;
@@ -1251,7 +1276,7 @@
1251
1276
 
1252
1277
  export async function refreshPreview(opts: { withRetry?: boolean; skipIfAlreadyShowing?: boolean } = {}): Promise<void> {
1253
1278
  if (opts.skipIfAlreadyShowing && hasLiveWorkerPreview()) {
1254
- console.log('[WidgetDetails] refreshPreview skipped — Worker preview already showing');
1279
+ // console.log('[WidgetDetails] refreshPreview skipped — Worker preview already showing');
1255
1280
  previewLoading = false;
1256
1281
  previewHydrating = false;
1257
1282
  return;
@@ -1275,11 +1300,11 @@
1275
1300
  middleware: cfg.middleware,
1276
1301
  headers: cfg.headers,
1277
1302
  });
1278
- console.log('[WidgetDetails] buildWidgetFromRepo', {
1279
- widgetId: widgetId.substring(0, 8),
1280
- builderBasePath,
1281
- sdkPath: '/v1/agent/widget-builder/build-from-repo/{widgetId}',
1282
- });
1303
+ // console.log('[WidgetDetails] buildWidgetFromRepo', {
1304
+ // widgetId: widgetId.substring(0, 8),
1305
+ // builderBasePath,
1306
+ // sdkPath: '/v1/agent/widget-builder/build-from-repo/{widgetId}',
1307
+ // });
1283
1308
  return builder.buildWidgetFromRepo({ widgetId });
1284
1309
  }
1285
1310
 
@@ -1287,13 +1312,13 @@
1287
1312
  const forceRebuild = options?.force === true;
1288
1313
  if (!selectedWidgetId) return;
1289
1314
  if (shouldDeferPreviewNetworkWork(forceRebuild)) {
1290
- console.log('[WidgetDetails] triggerBuildFromRepo deferred (background panel)');
1315
+ // console.log('[WidgetDetails] triggerBuildFromRepo deferred (background panel)');
1291
1316
  previewBuildDeferred = true;
1292
1317
  return;
1293
1318
  }
1294
1319
  const hasRepo = !!(normalizeRepositoryId(selectedWidget) || currentRepositoryId);
1295
1320
  if (!hasRepo) {
1296
- console.log('[WidgetDetails] triggerBuildFromRepo skipped — widget has no repository yet');
1321
+ // console.log('[WidgetDetails] triggerBuildFromRepo skipped — widget has no repository yet');
1297
1322
  previewLoading = false;
1298
1323
  previewError = 'NO_CODE_YET';
1299
1324
  return;
@@ -1301,6 +1326,7 @@
1301
1326
  const capturedWidgetId = selectedWidgetId;
1302
1327
  const runId = ++buildFromRepoRunId;
1303
1328
  if (forceRebuild) isRebuildingPreview = true;
1329
+ pinnedPublishedPreviewUrl = null;
1304
1330
  try {
1305
1331
  const workerPreviewUrl = buildWorkerPreviewUrl(capturedWidgetId);
1306
1332
  const workerPreviewDisplayUrl = buildWorkerPreviewUrl(capturedWidgetId, { forDisplay: true });
@@ -1309,7 +1335,7 @@
1309
1335
  const freshProbe = await probeWorkerReachable(workerPreviewUrl);
1310
1336
  if (selectedWidgetId !== capturedWidgetId || runId !== buildFromRepoRunId) return;
1311
1337
  if (freshProbe === 'ok') {
1312
- console.log('[WidgetDetails] triggerBuildFromRepo: skipping GitLab rebuild — codegen preview is fresh');
1338
+ // console.log('[WidgetDetails] triggerBuildFromRepo: skipping GitLab rebuild — codegen preview is fresh');
1313
1339
  dynamicWorkerPreviewUrl = workerPreviewUrl;
1314
1340
  directPreviewUrl = workerPreviewDisplayUrl;
1315
1341
  inlinePanelView = 'preview';
@@ -1349,7 +1375,7 @@
1349
1375
  const probeResult = await probeWorkerReachable(workerPreviewUrl);
1350
1376
  if (isStaleRun()) return;
1351
1377
  if (!forceRebuild && probeResult === 'ok') {
1352
- console.log('[WidgetDetails] triggerBuildFromRepo: Worker already has widget, loading preview directly');
1378
+ // console.log('[WidgetDetails] triggerBuildFromRepo: Worker already has widget, loading preview directly');
1353
1379
  dispatch('buildFromRepoSuccess', { widgetId: capturedWidgetId });
1354
1380
  loadPreviewFromWorker();
1355
1381
  return;
@@ -1363,7 +1389,7 @@
1363
1389
 
1364
1390
  // Skip build-from-repo while codegen is running — WS / refreshPreview will load when ready
1365
1391
  if (isGeneratingCode) {
1366
- console.log('[WidgetDetails] triggerBuildFromRepo: codegen in progress — deferring build-from-repo');
1392
+ // console.log('[WidgetDetails] triggerBuildFromRepo: codegen in progress — deferring build-from-repo');
1367
1393
  previewLoading = true;
1368
1394
  previewError = null;
1369
1395
  return;
@@ -1381,7 +1407,7 @@
1381
1407
  const buildData = await requestBuildFromRepo(capturedWidgetId);
1382
1408
  if (isStaleRun()) return;
1383
1409
 
1384
- console.log('[WidgetDetails] triggerBuildFromRepo succeeded', { buildTimeMs: buildData?.data?.buildTimeMs ?? buildData?.buildTimeMs, attempt });
1410
+ // console.log('[WidgetDetails] triggerBuildFromRepo succeeded', { buildTimeMs: buildData?.data?.buildTimeMs ?? buildData?.buildTimeMs, attempt });
1385
1411
  previewError = null;
1386
1412
  previewLoading = false;
1387
1413
  dispatch('widgetDataChanged', { widgetId: capturedWidgetId });
@@ -1409,7 +1435,7 @@
1409
1435
  lastBuildErrorMsg = sanitizePreviewBuildError(lastBuildErrorMsg);
1410
1436
 
1411
1437
  if (buildErrorCode === 'NO_CODE_YET') {
1412
- console.log('[WidgetDetails] triggerBuildFromRepo: repo exists but has no code yet — skipping retries');
1438
+ // console.log('[WidgetDetails] triggerBuildFromRepo: repo exists but has no code yet — skipping retries');
1413
1439
  lastBuildErrorMsg = 'NO_CODE_YET';
1414
1440
  break;
1415
1441
  }
@@ -1417,7 +1443,7 @@
1417
1443
  const status = buildErr?.response?.status ?? buildErr?.status;
1418
1444
  const isRetryable = status === 404 || status === 500 || !status;
1419
1445
  if (isRetryable && attempt < maxRetries) {
1420
- console.log(`[WidgetDetails] triggerBuildFromRepo attempt ${attempt}/${maxRetries} failed (${lastBuildErrorMsg}), retrying in ${retryDelayMs}ms...`);
1446
+ // console.log(`[WidgetDetails] triggerBuildFromRepo attempt ${attempt}/${maxRetries} failed (${lastBuildErrorMsg}), retrying in ${retryDelayMs}ms...`);
1421
1447
  await new Promise(r => setTimeout(r, retryDelayMs));
1422
1448
  continue;
1423
1449
  }
@@ -1439,14 +1465,14 @@
1439
1465
 
1440
1466
  // Preview may have loaded via codegen/WS while build-from-repo was in flight
1441
1467
  if (allowStalePreviewRecovery && iframeSrc && !previewHasRuntimeError) {
1442
- console.log('[WidgetDetails] triggerBuildFromRepo: build failed but preview iframe already loaded — keeping preview');
1468
+ // console.log('[WidgetDetails] triggerBuildFromRepo: build failed but preview iframe already loaded — keeping preview');
1443
1469
  previewError = null;
1444
1470
  previewLoading = false;
1445
1471
  return;
1446
1472
  }
1447
1473
 
1448
1474
  if (allowStalePreviewRecovery && (await tryRecoverPreviewFromWorker())) {
1449
- console.log('[WidgetDetails] triggerBuildFromRepo: recovered preview from Worker after build-from-repo failure');
1475
+ // console.log('[WidgetDetails] triggerBuildFromRepo: recovered preview from Worker after build-from-repo failure');
1450
1476
  return;
1451
1477
  }
1452
1478
 
@@ -1454,7 +1480,7 @@
1454
1480
  // For published widgets, try loading via API proxy (handles both R2 and CDN)
1455
1481
  const sourceArtifactUrl = publishedHtmlUrlFromJsPath(widgetJsPath);
1456
1482
  if (sourceArtifactUrl) {
1457
- console.log('[WidgetDetails] triggerBuildFromRepo: using js_path artifact (not new-id proxy)', sourceArtifactUrl);
1483
+ // console.log('[WidgetDetails] triggerBuildFromRepo: using js_path artifact (not new-id proxy)', sourceArtifactUrl);
1458
1484
  const reachable = await probePublishedArtifactUrl(sourceArtifactUrl);
1459
1485
  if (reachable) {
1460
1486
  previewError = null;
@@ -1586,7 +1612,7 @@
1586
1612
  /** Duplicate/fork fallback: keep published iframe if it loaded; otherwise compile from repo. */
1587
1613
  export async function retryPreviewIfEmpty(): Promise<void> {
1588
1614
  if (iframeSrc && !previewError) {
1589
- console.log('[WidgetDetails] retryPreviewIfEmpty skipped — preview already loaded');
1615
+ // console.log('[WidgetDetails] retryPreviewIfEmpty skipped — preview already loaded');
1590
1616
  return;
1591
1617
  }
1592
1618
  previewLoading = true;
@@ -1595,7 +1621,7 @@
1595
1621
  if (await loadPublishedPreviewForCurrentStep()) return;
1596
1622
  const hasRepo = !!(normalizeRepositoryId(selectedWidget) || currentRepositoryId);
1597
1623
  if (hasRepo) {
1598
- console.log('[WidgetDetails] retryPreviewIfEmpty — compiling from repository');
1624
+ // console.log('[WidgetDetails] retryPreviewIfEmpty — compiling from repository');
1599
1625
  await triggerBuildFromRepo();
1600
1626
  return;
1601
1627
  }
@@ -1664,10 +1690,9 @@
1664
1690
  // Widget is alive — never keep a runtime-error overlay over a working preview.
1665
1691
  clearPendingRuntimeErrorOverlay();
1666
1692
  if (previewHasRuntimeError) {
1667
- console.log('[WidgetDetails] Clearing runtime error overlay — widgetic:ready');
1668
1693
  previewHasRuntimeError = false;
1669
1694
  previewRuntimeErrorStatus = null;
1670
- if (previewError && /runtime error|HTTP 500|compilation/i.test(previewError)) {
1695
+ if (previewError && /did not start|runtime error|HTTP 500|compilation/i.test(previewError)) {
1671
1696
  previewError = null;
1672
1697
  }
1673
1698
  notifyPreviewCompileErrorChanged();
@@ -1684,6 +1709,17 @@
1684
1709
  return typeof value === 'string' && /<[a-z][\s\S]*>/i.test(value);
1685
1710
  }
1686
1711
 
1712
+ function contentOverlaySelectors(key: string): string {
1713
+ const exact = `[data-content="${CSS.escape(key)}"]`;
1714
+ if (/subtitle/i.test(key)) {
1715
+ return `${exact}, [data-content="widgetSubtitle"], [data-content="gallerySubtitle"], [data-content="subtitle"], .widget-subtitle, .gallery-subtitle`;
1716
+ }
1717
+ if (/^(widgetTitle|galleryTitle|title|heading)$/i.test(key)) {
1718
+ return `${exact}, [data-content="widgetTitle"], [data-content="galleryTitle"], h1, .widget-title`;
1719
+ }
1720
+ return exact;
1721
+ }
1722
+
1687
1723
  /** Same-origin preview iframes (Vite /__widget_builder__ proxy): restore bold/italic after widgets set textContent. */
1688
1724
  function applyRichTextInPreviewIframe(message: Record<string, unknown>): void {
1689
1725
  if (message.type !== 'widgetic:update') return;
@@ -1694,10 +1730,16 @@
1694
1730
  const content = message.content as Record<string, unknown> | undefined;
1695
1731
  if (content) {
1696
1732
  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;
1733
+ if (typeof value !== 'string') continue;
1734
+ const selectors = contentOverlaySelectors(key);
1735
+ doc.querySelectorAll(selectors).forEach((el) => {
1736
+ if (el instanceof HTMLImageElement) {
1737
+ el.src = value;
1738
+ return;
1739
+ }
1740
+ if (el instanceof HTMLInputElement || el instanceof HTMLAnchorElement) return;
1741
+ if (looksLikeHtmlMarkup(value)) el.innerHTML = value;
1742
+ else el.textContent = value;
1701
1743
  });
1702
1744
  }
1703
1745
  }
@@ -1737,25 +1779,63 @@
1737
1779
  }
1738
1780
 
1739
1781
  function ensureGoogleFontInPreviewDoc(doc: Document, family: string): void {
1740
- if (!family || /^(Inter|system-ui|sans-serif|serif)$/i.test(family)) return;
1782
+ if (!family || /^(system-ui|sans-serif|serif)$/i.test(family)) return;
1741
1783
  const id = `widgetic-gf-${family.replace(/[^a-z0-9]+/gi, '-').toLowerCase()}`;
1742
1784
  if (doc.getElementById(id)) return;
1743
1785
  const link = doc.createElement('link');
1744
1786
  link.id = id;
1745
1787
  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`;
1788
+ 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
1789
  doc.head?.appendChild(link);
1748
- console.log('[WidgetDetails] Loaded Google Font in preview iframe:', family);
1790
+ // console.log('[WidgetDetails] Loaded Google Font in preview iframe:', family);
1791
+ }
1792
+
1793
+ function parseOverlayFontFace(weight: string | undefined): { cssWeight: string; cssStyle: string } {
1794
+ const raw = String(weight || 'Regular');
1795
+ const cssStyle = /italic|oblique/i.test(raw) ? 'italic' : 'normal';
1796
+ const name = raw.toLowerCase().replace(/italic|oblique/g, '').replace(/[\s_-]/g, '');
1797
+ const table: Record<string, string> = {
1798
+ thin: '100', hairline: '100',
1799
+ extralight: '200', ultralight: '200',
1800
+ light: '300',
1801
+ regular: '400', normal: '400', '': '400',
1802
+ medium: '500',
1803
+ semibold: '600', demibold: '600',
1804
+ bold: '700',
1805
+ extrabold: '800', ultrabold: '800',
1806
+ black: '900', heavy: '900',
1807
+ };
1808
+ if (/^\d{3}$/.test(name)) return { cssWeight: name, cssStyle };
1809
+ return { cssWeight: table[name] || '400', cssStyle };
1810
+ }
1811
+
1812
+ function flattenDesignOverlay(raw: Record<string, unknown> | undefined): Record<string, string> | null {
1813
+ if (!raw) return null;
1814
+ const out: Record<string, string> = {};
1815
+ for (const [key, value] of Object.entries(raw)) {
1816
+ if (value && typeof value === 'object' && 'family' in (value as Record<string, unknown>)) {
1817
+ const font = value as { family?: string; weight?: string };
1818
+ const family = String(font.family || 'Inter');
1819
+ const face = parseOverlayFontFace(font.weight);
1820
+ out[key] = `"${family}", sans-serif`;
1821
+ out[`${key}Family`] = family;
1822
+ out[`${key}Weight`] = face.cssWeight;
1823
+ out[`${key}Style`] = face.cssStyle;
1824
+ continue;
1825
+ }
1826
+ if (typeof value === 'string' && value) out[key] = value;
1827
+ }
1828
+ return Object.keys(out).length ? out : null;
1749
1829
  }
1750
1830
 
1751
1831
  /** Same-origin preview: apply CSS vars, Google Fonts, title/subtitle, tile aspect-ratio. */
1752
1832
  function applyDesignOverlayInPreviewIframe(message: Record<string, unknown>): void {
1753
1833
  if (message.type !== 'widgetic:update') return;
1754
- const design = message.design as Record<string, string> | undefined;
1834
+ const design = flattenDesignOverlay(message.design as Record<string, unknown> | undefined);
1755
1835
  if (!design) return;
1756
1836
  const doc = previewIframe?.contentDocument;
1757
1837
  if (!doc?.documentElement) {
1758
- console.log('[WidgetDetails] Design overlay skipped — preview iframe not same-origin');
1838
+ // console.log('[WidgetDetails] Design overlay skipped — preview iframe not same-origin');
1759
1839
  return;
1760
1840
  }
1761
1841
  const apply = () => {
@@ -1776,46 +1856,69 @@
1776
1856
  }
1777
1857
  families.forEach((family) => ensureGoogleFontInPreviewDoc(doc, family));
1778
1858
 
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');
1859
+ const applyFace = (family: string, weight: string, style: string, selectors: string) => {
1860
+ if (!family && !weight && !style) return;
1861
+ const stack = family ? `"${family}", sans-serif` : '';
1862
+ doc.querySelectorAll(selectors).forEach((node) => {
1863
+ if (!(node instanceof HTMLElement)) return;
1864
+ if (stack) node.style.setProperty('font-family', stack, 'important');
1865
+ if (weight) node.style.setProperty('font-weight', weight, 'important');
1866
+ if (style) node.style.setProperty('font-style', style, 'important');
1800
1867
  });
1868
+ };
1869
+
1870
+ const cssChunks: string[] = [];
1871
+ const fontPrefixes = new Set<string>();
1872
+ for (const key of Object.keys(design)) {
1873
+ const match = key.match(/^(.*)Font(?:Family|Weight|Style)?$/i);
1874
+ if (match) fontPrefixes.add(match[1] || '');
1801
1875
  }
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);
1876
+
1877
+ for (const prefix of fontPrefixes) {
1878
+ const family =
1879
+ design[`${prefix}FontFamily`] ||
1880
+ extractCssFontFamily(design[`${prefix}Font`]) ||
1881
+ '';
1882
+ const weight = design[`${prefix}FontWeight`] || '';
1883
+ const style = design[`${prefix}FontStyle`] || '';
1884
+ const isTitleLike =
1885
+ /^(title|heading|tite)?$/i.test(prefix) ||
1886
+ /title|heading|tite/i.test(prefix);
1887
+ const isSubtitleLike = /subtitle|description|caption|label/i.test(prefix);
1888
+ const varFamily = `--${prefix}Font`;
1889
+ const varWeight = `--${prefix}FontWeight`;
1890
+ const varStyle = `--${prefix}FontStyle`;
1891
+ if (isSubtitleLike) {
1892
+ cssChunks.push(
1893
+ `[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; }`
1894
+ );
1895
+ applyFace(
1896
+ family,
1897
+ weight,
1898
+ style,
1899
+ '.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"]'
1900
+ );
1901
+ } else if (isTitleLike || fontPrefixes.size === 1) {
1902
+ cssChunks.push(
1903
+ `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; }`
1904
+ );
1905
+ applyFace(
1906
+ family,
1907
+ weight,
1908
+ style,
1909
+ 'h1, h2, h3, .widget-title, .gallery-title, [data-content="title"], [data-content="widgetTitle"], [data-content="heading"], [class*="title"]'
1910
+ );
1911
+ }
1912
+ }
1913
+
1914
+ if (cssChunks.length) {
1915
+ let sheet = doc.getElementById('widgetic-font-overlay') as HTMLStyleElement | null;
1916
+ if (!sheet) {
1917
+ sheet = doc.createElement('style');
1918
+ sheet.id = 'widgetic-font-overlay';
1919
+ doc.head.appendChild(sheet);
1818
1920
  }
1921
+ sheet.textContent = cssChunks.join('\n');
1819
1922
  }
1820
1923
 
1821
1924
  const aspectEntry = Object.entries(design).find(([key, value]) =>
@@ -1832,7 +1935,7 @@
1832
1935
  el.style.height = 'auto';
1833
1936
  }
1834
1937
  });
1835
- console.log('[WidgetDetails] Applied tile aspect-ratio overlay:', cssRatio);
1938
+ // console.log('[WidgetDetails] Applied tile aspect-ratio overlay:', cssRatio);
1836
1939
  }
1837
1940
  } catch (error) {
1838
1941
  console.warn('[WidgetDetails] Design overlay skipped:', error);
@@ -1899,7 +2002,7 @@
1899
2002
 
1900
2003
  function startPreviewTokenAutoRefresh() {
1901
2004
  stopPreviewTokenAutoRefresh();
1902
- console.log('[TokenAutoRefresh] Started - checking every', TOKEN_CHECK_INTERVAL_MS / 1000, 'seconds');
2005
+ // console.log('[TokenAutoRefresh] Started - checking every', TOKEN_CHECK_INTERVAL_MS / 1000, 'seconds');
1903
2006
  previewTokenRefreshInterval = setInterval(async () => {
1904
2007
  const isPreviewVisible = showPreviewModal || (showWidgetDetails && inlinePanelView === 'preview');
1905
2008
  if (!isPreviewVisible || !lastPreviewUrlFetchTime || !directPreviewUrl) return;
@@ -2008,7 +2111,7 @@
2008
2111
  const retryCount = opts._retryCount || 0;
2009
2112
  const withRetry = opts.withRetry || false;
2010
2113
 
2011
- console.log('WidgetDetails: Triggering preview refresh via src manipulation', retryCount > 0 ? `(retry ${retryCount})` : '');
2114
+ // console.log('WidgetDetails: Triggering preview refresh via src manipulation', retryCount > 0 ? `(retry ${retryCount})` : '');
2012
2115
  const newCacheBuster = Date.now();
2013
2116
  previewCacheBuster = newCacheBuster;
2014
2117
 
@@ -2018,7 +2121,7 @@
2018
2121
  const workerPreviewDisplayUrl = buildWorkerPreviewUrl(selectedWidgetId, { forDisplay: true });
2019
2122
  dynamicWorkerPreviewUrl = workerPreviewUrl;
2020
2123
  directPreviewUrl = workerPreviewDisplayUrl;
2021
- console.log('[WidgetDetails] refreshPreview: initialized worker preview URL');
2124
+ // console.log('[WidgetDetails] refreshPreview: initialized worker preview URL');
2022
2125
  }
2023
2126
 
2024
2127
  // After publish (or any time the CDN artifact may have changed), edit/embed steps must
@@ -2044,7 +2147,7 @@
2044
2147
  // If not reachable and we have retry budget, retry after a short delay (CDN propagation).
2045
2148
  if (withRetry && retryCount < 3) {
2046
2149
  const delay = (retryCount + 1) * 2000;
2047
- console.log(`[WidgetDetails] Published artifact not reachable yet, retrying in ${delay}ms (attempt ${retryCount + 1}/3)`);
2150
+ // console.log(`[WidgetDetails] Published artifact not reachable yet, retrying in ${delay}ms (attempt ${retryCount + 1}/3)`);
2048
2151
  setTimeout(() => refreshPreviewIframe({ withRetry: true, _retryCount: retryCount + 1 }), delay);
2049
2152
  return;
2050
2153
  }
@@ -2073,14 +2176,14 @@
2073
2176
  if (probeResult === 'not-found') {
2074
2177
  if (withRetry && retryCount < 3) {
2075
2178
  const delay = (retryCount + 1) * 2000;
2076
- console.log(`[WidgetDetails] Preview not found, retrying in ${delay}ms (attempt ${retryCount + 1}/3)`);
2179
+ // console.log(`[WidgetDetails] Preview not found, retrying in ${delay}ms (attempt ${retryCount + 1}/3)`);
2077
2180
  previewLoading = true;
2078
2181
  setTimeout(() => refreshPreviewIframe({ withRetry: true, _retryCount: retryCount + 1 }), delay);
2079
2182
  return;
2080
2183
  }
2081
2184
  // Worker cache miss after codegen — compile from GitLab repo
2082
2185
  if (lastCommitId && !isGeneratingCode && !shouldDeferPreviewNetworkWork()) {
2083
- console.log('[WidgetDetails] Preview not on Worker after retries — trying build-from-repo');
2186
+ // console.log('[WidgetDetails] Preview not on Worker after retries — trying build-from-repo');
2084
2187
  await triggerBuildFromRepo();
2085
2188
  if (iframeSrc) return;
2086
2189
  }
@@ -2196,9 +2299,16 @@
2196
2299
  if (!previewWidgetReady) {
2197
2300
  if (previewReadyFallbackTimeout) clearTimeout(previewReadyFallbackTimeout);
2198
2301
  previewReadyFallbackTimeout = setTimeout(() => {
2199
- console.warn('[WidgetDetails] Create step widgetic:ready fallback — applying overlay');
2200
- markPreviewWidgetReady();
2201
- }, 1500);
2302
+ if (previewWidgetReady) return;
2303
+ console.warn('[WidgetDetails] Create step: iframe loaded but widget did not signal ready');
2304
+ previewLoading = false;
2305
+ previewHydrating = false;
2306
+ previewHasRuntimeError = true;
2307
+ previewRuntimeErrorStatus = 500;
2308
+ previewError =
2309
+ '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.';
2310
+ notifyPreviewCompileErrorChanged();
2311
+ }, 4000);
2202
2312
  }
2203
2313
  scheduleFitPreviewToArea();
2204
2314
  }
@@ -2376,8 +2486,13 @@
2376
2486
  return Math.max(DETAILS_PANEL_MIN_HEIGHT, Math.min(DETAILS_PANEL_MAX_HEIGHT, available));
2377
2487
  }
2378
2488
 
2489
+ function getDetailsPanelMinWidth(): number {
2490
+ if (previewColumnCollapsed) return DETAILS_PANEL_MIN_WIDTH;
2491
+ return DETAILS_PANEL_MIN_WIDTH + COL_RESIZER_PX + 240;
2492
+ }
2493
+
2379
2494
  function clampPanelWidth(w: number): number {
2380
- return Math.max(DETAILS_PANEL_MIN_WIDTH, Math.min(w, DETAILS_PANEL_MAX_WIDTH));
2495
+ return Math.max(getDetailsPanelMinWidth(), Math.min(w, DETAILS_PANEL_MAX_WIDTH));
2381
2496
  }
2382
2497
 
2383
2498
  function clampPanelHeight(h: number): number {
@@ -2420,7 +2535,14 @@
2420
2535
  }
2421
2536
  const cascade = panelCascadeIndex * 32;
2422
2537
  detailsPanelX = Math.max(8, Math.round((window.innerWidth - detailsPanelWidth) / 2) + cascade);
2423
- detailsPanelY = detailsPanelTopOffset + cascade;
2538
+ const availableTop = detailsPanelTopOffset;
2539
+ detailsPanelY = Math.max(
2540
+ availableTop,
2541
+ Math.round(availableTop + (window.innerHeight - availableTop - detailsPanelHeight) / 2) + cascade,
2542
+ );
2543
+ if (detailsPanelY + detailsPanelHeight > window.innerHeight - 8) {
2544
+ detailsPanelY = Math.max(availableTop, window.innerHeight - detailsPanelHeight - 8);
2545
+ }
2424
2546
  }
2425
2547
 
2426
2548
  /** Keep the panel on-screen after a browser resize. Does not reset to default 600×800. */
@@ -2446,8 +2568,7 @@
2446
2568
  }
2447
2569
 
2448
2570
  /**
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.
2571
+ * Center Widget Details in the canvas viewport (X and Y), clamped on-screen.
2451
2572
  */
2452
2573
  export function positionBesideShape(shapeRect: {
2453
2574
  left: number;
@@ -2464,24 +2585,28 @@
2464
2585
  _hasBeenOpenedOnce = true;
2465
2586
  if (!shapeRect) {
2466
2587
  centerDetailsPanel();
2467
- console.log('[WidgetDetails] No shape rect — centered floating panel', detailsPanelWidth, detailsPanelHeight);
2588
+ // console.log('[WidgetDetails] No shape rect — centered floating panel', detailsPanelWidth, detailsPanelHeight);
2468
2589
  return;
2469
2590
  }
2470
2591
 
2471
- const gap = DETAILS_PANEL_SHAPE_GAP;
2472
- detailsPanelWidth = clampPanelWidth(DETAILS_PANEL_MAX_WIDTH);
2592
+ detailsPanelWidth = clampPanelWidth(DETAILS_PANEL_DEFAULT_WIDTH);
2473
2593
  detailsPanelHeight = clampPanelHeight(DETAILS_PANEL_MAX_HEIGHT);
2474
2594
 
2475
- const x = shapeRect.left + shapeRect.width + gap;
2476
- const pos = clampDetailsPanelPosition(x, Math.max(detailsPanelTopOffset, shapeRect.top));
2595
+ const viewportLeft = shapeRect.viewportLeft ?? 0;
2596
+ const viewportWidth = shapeRect.viewportWidth || window.innerWidth;
2597
+ const viewportTop = shapeRect.viewportTop ?? detailsPanelTopOffset;
2598
+ const viewportHeight = shapeRect.viewportHeight || (window.innerHeight - viewportTop);
2599
+
2600
+ const x = Math.round(viewportLeft + (viewportWidth - detailsPanelWidth) / 2);
2601
+ const y = Math.round(viewportTop + Math.max(0, (viewportHeight - detailsPanelHeight) / 2));
2602
+ const pos = clampDetailsPanelPosition(x, y);
2477
2603
  detailsPanelX = pos.x;
2478
2604
  detailsPanelY = pos.y;
2479
- console.log('[WidgetDetails] Positioned beside shape', {
2605
+ console.log('[WidgetDetails] Centered in canvas viewport', {
2480
2606
  x: Math.round(detailsPanelX),
2481
2607
  y: Math.round(detailsPanelY),
2482
2608
  w: detailsPanelWidth,
2483
2609
  h: detailsPanelHeight,
2484
- shapeLeft: Math.round(shapeRect.left),
2485
2610
  });
2486
2611
  }
2487
2612
 
@@ -2546,7 +2671,7 @@
2546
2671
 
2547
2672
  if (selectedWidgetId) {
2548
2673
  if (iframeSrc && previewLoadedForWidgetId === selectedWidgetId) {
2549
- console.log('[WidgetDetails] Reopened with existing preview, no reload needed');
2674
+ // console.log('[WidgetDetails] Reopened with existing preview, no reload needed');
2550
2675
  return;
2551
2676
  }
2552
2677
  loadWidget(selectedWidgetId);
@@ -2570,7 +2695,7 @@
2570
2695
  function handleCloseButtonClick(e: MouseEvent) {
2571
2696
  e.preventDefault();
2572
2697
  e.stopPropagation();
2573
- console.log('[WidgetDetails] Close button clicked');
2698
+ // console.log('[WidgetDetails] Close button clicked');
2574
2699
  closeWidgetDetails();
2575
2700
  }
2576
2701
 
@@ -2620,7 +2745,7 @@
2620
2745
  if (!isResizingDetails) return;
2621
2746
  const deltaX = e.clientX - detailsResizeStartX;
2622
2747
  const deltaY = e.clientY - detailsResizeStartY;
2623
- const minW = DETAILS_PANEL_MIN_WIDTH;
2748
+ const minW = getDetailsPanelMinWidth();
2624
2749
  const minH = DETAILS_PANEL_MIN_HEIGHT;
2625
2750
  const maxRight = window.innerWidth - 8;
2626
2751
  const maxBottom = window.innerHeight - 8;
@@ -2692,23 +2817,79 @@
2692
2817
  };
2693
2818
  }
2694
2819
 
2820
+ function updateHeaderNameMaxWidth() {
2821
+ if (!widgetDetailsNameCtEl || !widgetDetailsStepsRowEl) return;
2822
+ const nameRect = widgetDetailsNameCtEl.getBoundingClientRect();
2823
+ const stepsRect = widgetDetailsStepsRowEl.getBoundingClientRect();
2824
+ const available = Math.floor(stepsRect.left - nameRect.left - HEADER_ELEMENTS_GAP_PX);
2825
+ const next = Math.max(HEADER_NAME_MIN_WIDTH_PX, available);
2826
+ if (next !== headerNameMaxWidthPx) {
2827
+ headerNameMaxWidthPx = next;
2828
+ }
2829
+ }
2830
+
2831
+ function trackHeaderNameMaxWidth(node: HTMLElement) {
2832
+ detailsDragBarEl = node;
2833
+ const resizeObserver = new ResizeObserver(() => {
2834
+ requestAnimationFrame(updateHeaderNameMaxWidth);
2835
+ });
2836
+ resizeObserver.observe(node);
2837
+ requestAnimationFrame(updateHeaderNameMaxWidth);
2838
+ return {
2839
+ destroy() {
2840
+ resizeObserver.disconnect();
2841
+ }
2842
+ };
2843
+ }
2844
+
2695
2845
  function startDetailsDrag(e: PointerEvent) {
2696
2846
  const target = e.target as HTMLElement | null;
2697
2847
  if (target?.closest('.step-button, .widget-details-close-bt, .widget-details-debug-buttons, button, a, input, select, textarea')) {
2698
2848
  return;
2699
2849
  }
2700
- handlePanelFocusRequest();
2701
- e.preventDefault();
2702
- e.stopPropagation();
2703
- isDraggingDetails = true;
2704
2850
  detailsDragStartX = e.clientX;
2705
2851
  detailsDragStartY = e.clientY;
2706
2852
  detailsInitialX = detailsPanelX < 0 ? 0 : detailsPanelX;
2707
2853
  detailsInitialY = detailsPanelY < 0 ? detailsPanelTopOffset : detailsPanelY;
2854
+ detailsDragBarEl = e.currentTarget as HTMLElement | null;
2855
+ const fromName = !!target?.closest('.widget-details-name-ct');
2856
+ if (fromName) {
2857
+ pendingDetailsDrag = true;
2858
+ window.addEventListener('pointermove', onPendingDetailsDrag);
2859
+ window.addEventListener('pointerup', cancelPendingDetailsDrag);
2860
+ window.addEventListener('pointercancel', cancelPendingDetailsDrag);
2861
+ return;
2862
+ }
2863
+ beginDetailsDrag(e);
2864
+ }
2865
+
2866
+ function onPendingDetailsDrag(e: PointerEvent) {
2867
+ if (!pendingDetailsDrag) return;
2868
+ const moved = Math.hypot(e.clientX - detailsDragStartX, e.clientY - detailsDragStartY);
2869
+ if (moved < DETAILS_DRAG_THRESHOLD_PX) return;
2870
+ pendingDetailsDrag = false;
2871
+ window.removeEventListener('pointermove', onPendingDetailsDrag);
2872
+ window.removeEventListener('pointerup', cancelPendingDetailsDrag);
2873
+ window.removeEventListener('pointercancel', cancelPendingDetailsDrag);
2874
+ beginDetailsDrag(e);
2875
+ }
2876
+
2877
+ function cancelPendingDetailsDrag() {
2878
+ pendingDetailsDrag = false;
2879
+ window.removeEventListener('pointermove', onPendingDetailsDrag);
2880
+ window.removeEventListener('pointerup', cancelPendingDetailsDrag);
2881
+ window.removeEventListener('pointercancel', cancelPendingDetailsDrag);
2882
+ }
2883
+
2884
+ function beginDetailsDrag(e: PointerEvent) {
2885
+ handlePanelFocusRequest();
2886
+ e.preventDefault();
2887
+ e.stopPropagation();
2888
+ isDraggingDetails = true;
2708
2889
  document.body.style.cursor = 'grabbing';
2709
2890
  document.body.style.userSelect = 'none';
2710
2891
  try {
2711
- (e.currentTarget as HTMLElement | null)?.setPointerCapture?.(e.pointerId);
2892
+ detailsDragBarEl?.setPointerCapture?.(e.pointerId);
2712
2893
  } catch {
2713
2894
  /* ignore capture failures */
2714
2895
  }
@@ -2735,6 +2916,7 @@
2735
2916
  }
2736
2917
 
2737
2918
  function stopDetailsDrag() {
2919
+ cancelPendingDetailsDrag();
2738
2920
  if (!isDraggingDetails) return;
2739
2921
  isDraggingDetails = false;
2740
2922
  document.body.style.cursor = '';
@@ -2883,7 +3065,7 @@
2883
3065
  if (currentToken) setWsAuthToken(currentToken);
2884
3066
  const userId = userSession?.user?.id;
2885
3067
  if (userId) {
2886
- console.log('[WidgetDetails] Connecting WebSocket for user events');
3068
+ // console.log('[WidgetDetails] Connecting WebSocket for user events');
2887
3069
  connectWebSocket(userId);
2888
3070
  }
2889
3071
  }
@@ -2891,17 +3073,18 @@
2891
3073
  let lastWsPreviewSignature: string | null = null;
2892
3074
 
2893
3075
  async function applyWorkerPreviewFromEvent(workerUrl: string, buildTimeMs?: number | null): Promise<void> {
3076
+ if (pinnedPublishedPreviewUrl) return;
2894
3077
  const fetchUrl = toWorkerPreviewFetchUrl(workerUrl);
2895
3078
  const signature = `${fetchUrl}:${buildTimeMs ?? 'na'}:${selectedWidgetId ?? ''}`;
2896
3079
  if (signature === lastWsPreviewSignature) return;
2897
3080
  lastWsPreviewSignature = signature;
2898
3081
 
2899
- console.log('[WidgetDetails] Worker preview ready — force reloading iframe', {
2900
- widgetId: selectedWidgetId,
2901
- previewUrl: fetchUrl,
2902
- displayUrl: toWorkerPreviewDisplayUrl(fetchUrl),
2903
- buildTimeMs,
2904
- });
3082
+ // console.log('[WidgetDetails] Worker preview ready — force reloading iframe', {
3083
+ // widgetId: selectedWidgetId,
3084
+ // previewUrl: fetchUrl,
3085
+ // displayUrl: toWorkerPreviewDisplayUrl(fetchUrl),
3086
+ // buildTimeMs,
3087
+ // });
2905
3088
 
2906
3089
  dynamicWorkerPreviewUrl = fetchUrl;
2907
3090
  directPreviewUrl = toWorkerPreviewDisplayUrl(fetchUrl);
@@ -2947,25 +3130,29 @@
2947
3130
  // Worker-built widgets signal readiness via widgetic:ready (not sveltekit-hydration-complete)
2948
3131
  const handleWidgeticReady = (event: MessageEvent) => {
2949
3132
  if (event.data?.type !== 'widgetic:ready') return;
2950
- console.log('[WidgetDetails] widgetic:ready received — applying queued composition updates');
3133
+ const activeWin = previewIframe?.contentWindow;
3134
+ if (!activeWin || event.source !== activeWin) return;
3135
+ // console.log('[WidgetDetails] widgetic:ready received — applying queued composition updates');
2951
3136
  markPreviewWidgetReady();
2952
3137
  };
2953
3138
  window.addEventListener('message', handleWidgeticReady);
2954
3139
 
2955
3140
  // Listen for SvelteKit hydration completion signal (legacy VM / published SvelteKit artifacts)
2956
3141
  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
- }
3142
+ if (event.data?.type !== 'sveltekit-hydration-complete') return;
3143
+ const activeWin = previewIframe?.contentWindow;
3144
+ if (!activeWin || event.source !== activeWin) return;
3145
+ previewHydrating = false;
3146
+ if (hydrationSafetyTimeout) { clearTimeout(hydrationSafetyTimeout); hydrationSafetyTimeout = null; }
3147
+ markPreviewWidgetReady();
2963
3148
  };
2964
3149
  window.addEventListener('message', handleHydrationSignal);
2965
3150
 
2966
3151
  // Listen for runtime error signals from the preview iframe
2967
3152
  const handleIframeRuntimeError = (event: MessageEvent) => {
2968
3153
  if (event.data?.type !== 'sveltekit-runtime-error') return;
3154
+ const activeWin = previewIframe?.contentWindow;
3155
+ if (!activeWin || event.source !== activeWin) return;
2969
3156
 
2970
3157
  const runtimeMsg = String(event.data?.message || event.data?.error || '').trim();
2971
3158
  const runtimeSource = event.data?.source ? String(event.data.source) : '';
@@ -2976,16 +3163,10 @@
2976
3163
  console.warn('[WidgetDetails] Ignoring runtime error during code generation:', runtimeMsg);
2977
3164
  return;
2978
3165
  }
2979
- if (isCodegenPreviewFresh()) {
3166
+ if (isCodegenPreviewFresh() && !looksLikeHardCompileFailure(runtimeMsg)) {
2980
3167
  console.warn('[WidgetDetails] Ignoring runtime error during codegen preview window:', runtimeMsg);
2981
3168
  return;
2982
3169
  }
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
3170
  if (isBenignPreviewRuntimeError(runtimeMsg)) {
2990
3171
  console.warn('[WidgetDetails] Ignoring benign preview runtime error:', runtimeMsg);
2991
3172
  return;
@@ -3036,20 +3217,21 @@
3036
3217
 
3037
3218
  // Listen for expired preview token signal
3038
3219
  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
- }
3220
+ if (event.data?.type !== 'preview-token-expired') return;
3221
+ const activeWin = previewIframe?.contentWindow;
3222
+ if (!activeWin || event.source !== activeWin) return;
3223
+ directPreviewUrl = null;
3224
+ lastPreviewUrlFetchTime = 0;
3225
+ try {
3226
+ const freshUrl = await fetchDirectPreviewUrl();
3227
+ if (freshUrl) {
3228
+ const cacheBuster = Date.now();
3229
+ const url = new URL(freshUrl);
3230
+ url.searchParams.set('_t', cacheBuster.toString());
3231
+ iframeSrc = url.toString();
3232
+ previewCacheBuster = cacheBuster;
3233
+ }
3234
+ } catch (err) { console.error('Error refreshing preview token:', err); }
3053
3235
  };
3054
3236
  window.addEventListener('message', handleTokenExpired);
3055
3237
 
@@ -3059,12 +3241,14 @@
3059
3241
  // "Rebuild" whenever we lose the L1/L2 cache. (Reset happens on widget switch.)
3060
3242
  const handleWidgetNotFound = async (event: MessageEvent) => {
3061
3243
  if (event.data?.type !== 'widgetic:widget-not-found') return;
3244
+ const activeWin = previewIframe?.contentWindow;
3245
+ if (!activeWin || event.source !== activeWin) return;
3062
3246
  if (isCodegenPreviewFresh()) {
3063
- console.log('[WidgetDetails] Ignoring widget-not-found during codegen preview window');
3247
+ // console.log('[WidgetDetails] Ignoring widget-not-found during codegen preview window');
3064
3248
  return;
3065
3249
  }
3066
3250
  if (dwLoadTimeoutHandle) { clearTimeout(dwLoadTimeoutHandle); dwLoadTimeoutHandle = null; }
3067
- console.log('[WidgetDetails] Worker reports widget not found — checking if repo has code');
3251
+ // console.log('[WidgetDetails] Worker reports widget not found — checking if repo has code');
3068
3252
  dynamicWorkerPreviewUrl = null;
3069
3253
  directPreviewUrl = null;
3070
3254
  iframeSrc = null;
@@ -3075,7 +3259,7 @@
3075
3259
  const hasRepo = !!currentRepositoryId;
3076
3260
  if (hasRepo && capturedWidgetId && widgetNotFoundAutoRebuildAttempts === 0) {
3077
3261
  widgetNotFoundAutoRebuildAttempts = 1;
3078
- console.log('[WidgetDetails] Widget missing from Worker — auto-triggering build-from-repo');
3262
+ // console.log('[WidgetDetails] Widget missing from Worker — auto-triggering build-from-repo');
3079
3263
  previewLoading = true;
3080
3264
  try {
3081
3265
  await triggerBuildFromRepo({ force: true });
@@ -3105,6 +3289,7 @@
3105
3289
  if (contentContainerEl) {
3106
3290
  hasScrollbar = contentContainerEl.scrollHeight > contentContainerEl.clientHeight;
3107
3291
  }
3292
+ updateHeaderNameMaxWidth();
3108
3293
  });
3109
3294
 
3110
3295
  onDestroy(() => {
@@ -3133,13 +3318,13 @@
3133
3318
  onclick={handleCloseButtonClick}
3134
3319
  onmousedown={(e) => e.stopPropagation()}
3135
3320
  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"
3321
+ 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
3322
  title="Close widget details"
3138
3323
  >
3139
3324
  ✕
3140
3325
  </button>
3141
3326
  <div
3142
- class="widget-details-floating-panel pointer-events-auto relative flex flex-col h-full w-full
3327
+ class="widget-details-floating-panel pointer-events-auto relative flex flex-col h-full w-full min-w-[480px]
3143
3328
  {fillViewport
3144
3329
  ? 'bg-white rounded-none border-x border-b border-t-0 ring-0 shadow-none overflow-hidden'
3145
3330
  : 'bg-white/95 backdrop-blur-sm shadow-2xl rounded-xl border overflow-hidden'}
@@ -3151,23 +3336,43 @@
3151
3336
  <!-- Widget Details Top Bar — title, steps, and close on one row -->
3152
3337
  <div class="widget-details-header-ct flex flex-col shrink-0 bg-gray-100 border-b border-gray-200">
3153
3338
  <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"
3339
+ 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"
3340
+ 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;"
3341
+ bind:this={detailsDragBarEl}
3342
+ use:trackHeaderNameMaxWidth
3155
3343
  onpointerdown={startDetailsDrag}
3156
3344
  >
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 || ''}
3345
+ <!-- svelte-ignore a11y_no_static_element_interactions -->
3346
+ <div
3347
+ class="widget-details-name-ct relative z-[1] min-w-0 w-fit overflow-hidden"
3348
+ bind:this={widgetDetailsNameCtEl}
3349
+ style="min-width: {HEADER_NAME_MIN_WIDTH_PX}px; max-width: {headerNameMaxWidthPx}px;"
3160
3350
  >
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">
3351
+ <EditableName
3352
+ class="widget-details-name min-w-0 truncate text-sm font-bold text-gray-800"
3353
+ name={selectedWidget?.name || ''}
3354
+ placeholder={isDraftConvertPanel ? 'Widget Draft' : 'Widget'}
3355
+ tooltipText="Double-click to rename"
3356
+ on:rename={(e) => {
3357
+ if (!selectedWidgetId) return;
3358
+ dispatch('widgetRename', { widgetId: selectedWidgetId, newName: e.detail.newName });
3359
+ }}
3360
+ />
3361
+ </div>
3362
+ <div
3363
+ 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"
3364
+ bind:this={widgetDetailsStepsRowEl}
3365
+ >
3366
+ <div
3367
+ class="widget-details-step-indicator pointer-events-auto flex items-center flex-nowrap justify-center whitespace-nowrap"
3368
+ style="gap: {HEADER_ELEMENTS_GAP_PX}px;"
3369
+ >
3370
+ <HoverTooltip text="Create widget code" class="inline-flex shrink-0">
3166
3371
  <!-- svelte-ignore a11y_consider_explicit_label -->
3167
3372
  <button
3168
3373
  type="button"
3169
3374
  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
3375
+ 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
3376
  {currentStep === 'create'
3172
3377
  ? 'bg-blue-100 text-blue-700 border border-blue-300 cursor-pointer'
3173
3378
  : 'text-gray-600 hover:bg-gray-200 border border-transparent cursor-pointer'}"
@@ -3175,15 +3380,16 @@
3175
3380
  <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
3381
  <span class="step-label">Create</span>
3177
3382
  </button>
3178
- <div class="step-connector w-4 h-px {canEditStep ? 'bg-gray-400' : 'bg-gray-200'}"></div>
3383
+ </HoverTooltip>
3384
+ <span class="step-connector text-sm font-semibold leading-none {canEditStep ? 'text-gray-500' : 'text-gray-300'}" aria-hidden="true">→</span>
3385
+ <HoverTooltip text={canEditStep ? 'Edit composition' : 'Publish the widget to edit composition'} class="inline-flex shrink-0">
3179
3386
  <!-- svelte-ignore a11y_consider_explicit_label -->
3180
3387
  <button
3181
3388
  type="button"
3182
3389
  onclick={() => goToStep('edit')}
3183
3390
  tabindex={canEditStep ? 0 : -1}
3184
3391
  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
3392
+ 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
3393
  {currentStep === 'edit'
3188
3394
  ? 'bg-blue-100 text-blue-700 border border-blue-300 cursor-pointer'
3189
3395
  : canEditStep
@@ -3193,15 +3399,16 @@
3193
3399
  <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
3400
  <span class="step-label">Edit</span>
3195
3401
  </button>
3196
- <div class="step-connector w-4 h-px {canEmbedStep ? 'bg-gray-400' : 'bg-gray-200'}"></div>
3402
+ </HoverTooltip>
3403
+ <span class="step-connector text-sm font-semibold leading-none {canEmbedStep ? 'text-gray-500' : 'text-gray-300'}" aria-hidden="true">→</span>
3404
+ <HoverTooltip text={canEmbedStep ? 'Embed the published widget' : 'Publish the widget to get embed code'} class="inline-flex shrink-0">
3197
3405
  <!-- svelte-ignore a11y_consider_explicit_label -->
3198
3406
  <button
3199
3407
  type="button"
3200
3408
  onclick={() => goToStep('embed')}
3201
3409
  tabindex={canEmbedStep ? 0 : -1}
3202
3410
  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
3411
+ 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
3412
  {currentStep === 'embed'
3206
3413
  ? 'bg-blue-100 text-blue-700 border border-blue-300 cursor-pointer'
3207
3414
  : canEmbedStep
@@ -3211,41 +3418,57 @@
3211
3418
  <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
3419
  <span class="step-label">Embed</span>
3213
3420
  </button>
3421
+ </HoverTooltip>
3214
3422
  </div>
3215
3423
  </div>
3216
3424
 
3217
- <div class="widget-details-debug-buttons flex items-center gap-1.5 shrink-0 pr-1">
3425
+ <div class="widget-details-debug-buttons relative z-[1] ml-auto flex items-center shrink-0">
3426
+ <HoverTooltip text={previewColumnCollapsed ? 'Open live preview' : 'Close live preview'}>
3218
3427
  <button
3219
3428
  type="button"
3220
3429
  onclick={() => previewColumnCollapsed ? expandPreviewColumn() : collapsePreviewColumn()}
3221
3430
  onmousedown={(e) => e.stopPropagation()}
3222
3431
  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
3432
+ aria-label={previewColumnCollapsed ? 'Open live preview' : 'Close live preview'}
3433
+ class="widget-details-preview-toggle-bt h-6 w-6 flex items-center justify-center rounded-lg cursor-pointer transition-colors
3224
3434
  {previewColumnCollapsed
3225
3435
  ? 'bg-white border border-gray-300 text-gray-600 hover:bg-gray-100'
3226
3436
  : '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
3437
  >
3229
- {previewColumnCollapsed ? 'Open Preview' : 'Close Preview'}
3438
+ {#if previewColumnCollapsed}
3439
+ <svg class="w-3.5 h-3.5" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true">
3440
+ <rect x="3" y="4" width="18" height="16" rx="2"/>
3441
+ <path d="M14 4v16"/>
3442
+ <path d="M17 9l3 3-3 3"/>
3443
+ </svg>
3444
+ {:else}
3445
+ <svg class="w-3.5 h-3.5" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true">
3446
+ <rect x="3" y="4" width="18" height="16" rx="2"/>
3447
+ <path d="M14 4v16"/>
3448
+ <path d="M10 15L7 12l3-3"/>
3449
+ </svg>
3450
+ {/if}
3230
3451
  </button>
3452
+ </HoverTooltip>
3231
3453
  </div>
3232
3454
  </div>
3233
3455
  </div>
3234
3456
 
3235
3457
  <!-- 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">
3458
+ <div class="widget-details-content-wrapper relative m-2.5 min-w-[460px] min-h-0 flex-1 overflow-hidden">
3459
+ <div class="widget-details-two-col flex h-full min-h-0 min-w-[460px] gap-0 overflow-hidden">
3238
3460
 
3239
3461
  <!-- ═══ LEFT COLUMN: Step Content (scrollable) ═══ -->
3240
3462
  <div
3241
- class="widget-details-left-col flex flex-col min-w-0 min-h-0 relative"
3463
+ class="widget-details-left-col flex flex-col min-w-[460px] min-h-0 overflow-hidden relative"
3242
3464
  style="width: {previewColumnCollapsed ? 100 : leftColPercent}%; flex: 0 0 {previewColumnCollapsed ? 100 : leftColPercent}%;"
3243
3465
  >
3244
3466
  <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
3467
 
3246
3468
  {#if selectedWidgetId}
3469
+ <div class="widget-details-left-stack relative z-0 flex flex-col h-full min-h-0 min-w-[460px] overflow-hidden">
3247
3470
  <!-- 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">
3471
+ <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
3472
  <div class="widget-info-name flex items-center min-w-0 gap-1 text-sm text-gray-700">
3250
3473
  <!-- svelte-ignore a11y-click-events-have-key-events -->
3251
3474
  <!-- svelte-ignore a11y-no-static-element-interactions -->
@@ -3263,12 +3486,14 @@
3263
3486
  {:else if saveStatus === 'error'}
3264
3487
  <span class="text-[10px] px-1.5 py-0.5 rounded bg-red-100 text-red-700">Save Error</span>
3265
3488
  {/if}
3489
+ {#if debugMode}
3266
3490
  <span class="text-[10px] text-gray-500">
3267
3491
  Commit: <span class="font-mono font-medium text-gray-600" title={lastCommitId || ''}>{lastCommitId ? lastCommitId.substring(0, 8) : '—'}</span>
3268
3492
  </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
3493
  {/if}
3494
+ <div class="widget-info-version-history-slot relative z-20 shrink-0">
3495
+ <slot name="version-history" />
3496
+ </div>
3272
3497
  </div>
3273
3498
  </div>
3274
3499
 
@@ -3281,22 +3506,33 @@
3281
3506
 
3282
3507
  <!-- Keep all steps mounted — switching tabs must not destroy chat / PropsEditor state -->
3283
3508
  <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" />
3509
+ <div class="widget-details-create-models-ct shrink-0 empty:hidden">
3510
+ <slot name="code-generation-models" />
3286
3511
  </div>
3287
- <div class="widget-details-create-publish-ct shrink-0 px-2 pb-2">
3512
+ <div
3513
+ class="widget-details-create-chat-ct flex flex-col flex-1 min-h-0 overflow-hidden overflow-x-hidden"
3514
+ >
3515
+ <div class="widget-details-create-status-slot shrink-0 empty:hidden">
3516
+ <slot name="code-generation-status" />
3517
+ </div>
3518
+ <div class="widget-details-create-chat-body flex flex-col flex-1 min-h-0 overflow-hidden">
3519
+ <slot name="code-generation" />
3520
+ </div>
3521
+ </div>
3522
+ <div class="widget-details-create-publish-ct shrink-0 empty:hidden">
3288
3523
  <slot name="publish-widget" />
3289
3524
  </div>
3290
3525
  </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'}">
3526
+ <div class="widget-details-edit-step flex flex-col flex-1 min-h-0 overflow-hidden {currentStep === 'edit' ? '' : 'hidden'}">
3292
3527
  <slot name="composition-editor" />
3293
3528
  </div>
3294
- <div class="widget-details-embed-step flex flex-col gap-2 p-2 {currentStep === 'embed' ? '' : 'hidden'}">
3529
+ <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 {currentStep === 'embed' ? '' : 'hidden'}">
3295
3530
  <slot name="embed-section" />
3296
3531
  </div>
3297
3532
 
3298
3533
  </div> <!-- end of actions-panel -->
3299
3534
  </div> <!-- end of widget-details-left-content -->
3535
+ </div> <!-- end of widget-details-left-stack -->
3300
3536
  {/if}
3301
3537
 
3302
3538
  </div> <!-- end of widget-details-left-col -->
@@ -3581,14 +3817,38 @@
3581
3817
  {/if}
3582
3818
 
3583
3819
  <style>
3584
- .widget-details-left-content {
3585
- overflow: hidden;
3586
- overflow-x: hidden;
3820
+ .widget-details-drag-bar button:focus,
3821
+ .widget-details-drag-bar button:focus-visible,
3822
+ .widget-details-drag-bar a:focus,
3823
+ .widget-details-drag-bar a:focus-visible {
3824
+ outline: none;
3825
+ box-shadow: none;
3826
+ }
3827
+ .widget-details-create-models-ct :global(button:focus),
3828
+ .widget-details-create-models-ct :global(button:focus-visible),
3829
+ .widget-details-create-models-ct :global(select:focus),
3830
+ .widget-details-create-models-ct :global(select:focus-visible) {
3831
+ outline: none;
3832
+ box-shadow: none;
3833
+ }
3834
+ .widget-details-create-chat-ct :global(.prompt-chat-messages) {
3835
+ overflow-y: auto;
3587
3836
  scrollbar-width: thin;
3588
- scrollbar-color: rgba(0, 0, 0, 0.15) transparent;
3837
+ scrollbar-color: rgba(0, 0, 0, 0.22) transparent;
3838
+ }
3839
+ .widget-details-create-chat-ct :global(.prompt-chat-messages::-webkit-scrollbar) {
3840
+ width: 6px;
3841
+ background: transparent;
3842
+ }
3843
+ .widget-details-create-chat-ct :global(.prompt-chat-messages::-webkit-scrollbar-track) {
3844
+ background: transparent;
3845
+ }
3846
+ .widget-details-create-chat-ct :global(.prompt-chat-messages::-webkit-scrollbar-thumb) {
3847
+ background: rgba(0, 0, 0, 0.2);
3848
+ border-radius: 4px;
3589
3849
  }
3590
3850
  .widget-details-left-content::-webkit-scrollbar {
3591
- width: 10px;
3851
+ width: 6px;
3592
3852
  background: transparent;
3593
3853
  }
3594
3854
  .widget-details-left-content::-webkit-scrollbar-track {