@widgetic/creator 0.3.50 → 0.3.53

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.
@@ -14,7 +14,8 @@
14
14
  import { get } from 'svelte/store';
15
15
  import { Button, showToast, GenerateLoader } from '@widgetic/design-system/components';
16
16
  import {
17
- AgentApi
17
+ AgentApi,
18
+ WidgetBuilderApi
18
19
  } from '@widgetic/api-sdk';
19
20
  import { maxCharsFromId } from '../constants.js';
20
21
  import {
@@ -182,6 +183,21 @@
182
183
  return `${base}/preview/${widgetId}`;
183
184
  }
184
185
 
186
+ /** CDN/js_path may point at the source widget after duplicate — never rewrite the id. */
187
+ function publishedHtmlUrlFromJsPath(jsPath: string | null | undefined): string | null {
188
+ if (!jsPath) return null;
189
+ let resolved = String(jsPath).replace(/\/+$/, '')
190
+ .replace(/\/widget\.js$/i, '/widget.html')
191
+ .replace(/\.js$/i, '.html');
192
+ if (!/\.html$/i.test(resolved)) {
193
+ resolved = `${resolved}/widget.html`;
194
+ }
195
+ if (resolved.startsWith('/')) {
196
+ resolved = `${cdnPublicBaseUrl}${resolved}`;
197
+ }
198
+ return resolved;
199
+ }
200
+
185
201
  /** Map public preview URLs to same-origin fetch paths in dev (iframe embed). */
186
202
  function toPreviewFetchUrl(url: string): string {
187
203
  if (!import.meta.env.DEV) return url;
@@ -269,7 +285,7 @@
269
285
  { id: 'embed', label: 'Embed', number: 3 },
270
286
  ];
271
287
 
272
- $: hasWidgetCode = !!lastCommitId || lastPublishedVersion !== null;
288
+ $: hasWidgetCode = !!lastCommitId || lastPublishedVersion !== null || !!widgetJsPath;
273
289
 
274
290
  /** Convert UX v2: draft panels (synthetic ids) have no widget/repo yet —
275
291
  * show a friendly draft hint instead of the repo-setup loader. */
@@ -278,14 +294,10 @@
278
294
  previewError = 'DRAFT_PANEL';
279
295
  }
280
296
 
281
- /** Published widgets must never show the draft "No Code Yet" empty state. */
297
+ /** Duplicate still has source code — keep the loader, never flash "No Code Yet" as a compile failure. */
282
298
  $: if (hasWidgetCode && previewError === 'NO_CODE_YET') {
283
- previewError = lastPublishedVersion !== null
284
- ? 'Published version is not available. Use Rebuild code or republish.'
285
- : 'Failed to compile widget code from repository.';
286
- if (lastPublishedVersion !== null) {
287
- publishedArtifactReachable = false;
288
- }
299
+ previewError = null;
300
+ if (!iframeSrc) previewLoading = true;
289
301
  }
290
302
  $: canEditStep = lastPublishedVersion !== null && !isGeneratingCode;
291
303
  $: canEmbedStep = lastPublishedVersion !== null && !isGeneratingCode;
@@ -647,7 +659,7 @@
647
659
 
648
660
  function isCdnArtifactMissingError(message: string | null): boolean {
649
661
  if (!message) return false;
650
- return /artifact not found|artifact missing|not available|CDN|404 Object not found|not publicly accessible/i.test(message);
662
+ return /artifact not found|artifact missing|not available|CDN|404|not found|requested resource|not publicly accessible/i.test(message);
651
663
  }
652
664
  $: if (currentStep === 'edit') {
653
665
  const editSource = resolvePreviewUrlForStep('edit') ?? '';
@@ -857,8 +869,8 @@
857
869
  $: detailsPanelTopOffset = panelTopOffset ?? legacyTopOffset;
858
870
  $: detailsPanelBottomMargin = panelBottomMargin ?? legacyBottomMargin;
859
871
 
860
- const DETAILS_PANEL_DEFAULT_WIDTH = 600;
861
- const DETAILS_PANEL_MAX_WIDTH = 600;
872
+ const DETAILS_PANEL_DEFAULT_WIDTH = 800;
873
+ const DETAILS_PANEL_MAX_WIDTH = 800;
862
874
  const DETAILS_PANEL_MAX_HEIGHT = 800;
863
875
  const DETAILS_PANEL_MIN_WIDTH = 320;
864
876
  const DETAILS_PANEL_MIN_HEIGHT = 400;
@@ -868,6 +880,8 @@
868
880
  const LEFT_COL_DEFAULT_PERCENT = 58;
869
881
  const LEFT_COL_MIN_PERCENT = 32;
870
882
  const LEFT_COL_MAX_PERCENT = 72;
883
+ const COL_RESIZER_PX = 10;
884
+ const PREVIEW_COL_MIN_PX = 240;
871
885
  /** Chat-first layout: preview column starts collapsed and opens on generate. */
872
886
  let previewColumnCollapsed = true;
873
887
  let lastAutoExpandForGenerating = false;
@@ -891,6 +905,23 @@
891
905
  expandPreviewColumn();
892
906
  }
893
907
 
908
+ function maxLeftColPercent(): number {
909
+ const twoCol =
910
+ typeof document !== 'undefined'
911
+ ? (document.querySelector('.widget-details-two-col') as HTMLElement | null)
912
+ : null;
913
+ const width = twoCol?.clientWidth || detailsPanelWidth || DETAILS_PANEL_DEFAULT_WIDTH;
914
+ if (width <= 0) return LEFT_COL_MAX_PERCENT;
915
+ const maxLeftPx = Math.max(160, width - COL_RESIZER_PX - PREVIEW_COL_MIN_PX);
916
+ return Math.min(LEFT_COL_MAX_PERCENT, (maxLeftPx / width) * 100);
917
+ }
918
+
919
+ function clampLeftColPercent(percent: number): number {
920
+ const maxP = maxLeftColPercent();
921
+ const minP = Math.min(LEFT_COL_MIN_PERCENT, maxP);
922
+ return Math.min(maxP, Math.max(minP, percent));
923
+ }
924
+
894
925
  function collapsePreviewColumn() {
895
926
  previewColumnCollapsed = true;
896
927
  if (widthBeforePreviewExpand > 0) {
@@ -911,10 +942,7 @@
911
942
  const fittedWidth = Math.min(expandedWidth, Math.max(widthBeforePreviewExpand, maxRight - detailsPanelX));
912
943
  detailsPanelWidth = Math.max(widthBeforePreviewExpand, fittedWidth);
913
944
  const usable = Math.max(1, detailsPanelWidth);
914
- leftColPercent = Math.min(
915
- LEFT_COL_MAX_PERCENT,
916
- Math.max(LEFT_COL_MIN_PERCENT, (widthBeforePreviewExpand / usable) * 100),
917
- );
945
+ leftColPercent = clampLeftColPercent((widthBeforePreviewExpand / usable) * 100);
918
946
  console.log('[WidgetDetails] Preview column expanded', {
919
947
  from: widthBeforePreviewExpand,
920
948
  to: detailsPanelWidth,
@@ -989,35 +1017,39 @@
989
1017
 
990
1018
  /** Load published CDN preview without Worker probe (safe for background panels). */
991
1019
  async function loadPublishedPreviewForCurrentStep(): Promise<boolean> {
992
- if (currentStep === 'edit' && editPreviewUrl) {
993
- const reachable = await probePublishedArtifactUrl(editPreviewUrl);
994
- publishedArtifactReachable = reachable;
995
- if (!reachable) {
996
- previewError = 'Published version is not available. Use Rebuild code.';
997
- previewLoading = false;
998
- return false;
999
- }
1000
- _lastEditPreviewBase = null;
1001
- switchPreviewUrlForStep('edit', { forceReload: true });
1002
- previewBuildDeferred = false;
1003
- previewError = null;
1004
- return true;
1020
+ const widgetPath = publishedHtmlUrlFromJsPath(
1021
+ widgetJsPath
1022
+ || ((selectedWidget as { jsPath?: string | null; js_path?: string | null } | null)?.jsPath
1023
+ ?? (selectedWidget as { js_path?: string | null } | null)?.js_path)
1024
+ || null
1025
+ );
1026
+ const publishedUrl =
1027
+ currentStep === 'embed'
1028
+ ? embedPreviewUrl
1029
+ : (widgetPath || editPreviewUrl);
1030
+ if (!publishedUrl) return false;
1031
+ const reachable = await probePublishedArtifactUrl(publishedUrl);
1032
+ publishedArtifactReachable = reachable;
1033
+ if (!reachable) {
1034
+ console.warn('[WidgetDetails] Published artifact not reachable, falling through to repo compile', publishedUrl);
1035
+ previewLoading = false;
1036
+ return false;
1005
1037
  }
1006
- if (currentStep === 'embed' && embedPreviewUrl) {
1007
- const reachable = await probePublishedArtifactUrl(embedPreviewUrl);
1008
- publishedArtifactReachable = reachable;
1009
- if (!reachable) {
1010
- previewError = 'Published version is not available. Use Rebuild code.';
1011
- previewLoading = false;
1012
- return false;
1013
- }
1038
+ if (currentStep === 'embed') {
1014
1039
  _lastEmbedPreviewBase = null;
1015
- applyPreviewUrlToIframe(embedPreviewUrl);
1016
- previewBuildDeferred = false;
1017
- previewError = null;
1018
- return true;
1040
+ applyPreviewUrlToIframe(publishedUrl);
1041
+ } else {
1042
+ _lastEditPreviewBase = null;
1043
+ if (currentStep === 'edit') {
1044
+ switchPreviewUrlForStep('edit', { forceReload: true });
1045
+ } else {
1046
+ applyPreviewUrlToIframe(publishedUrl);
1047
+ }
1019
1048
  }
1020
- return false;
1049
+ previewBuildDeferred = false;
1050
+ previewError = null;
1051
+ previewLoading = false;
1052
+ return true;
1021
1053
  }
1022
1054
 
1023
1055
  /** Probe Worker / build-from-repo pipeline without resetting panel state. */
@@ -1060,7 +1092,7 @@
1060
1092
  }
1061
1093
 
1062
1094
  if (probeResult === 'not-found') {
1063
- console.log('[WidgetDetails] Widget not on Worker — build-from-repo', {
1095
+ console.log('[WidgetDetails] Widget not on Worker — published artifact or build-from-repo', {
1064
1096
  widgetId: capturedWidgetId.substring(0, 8),
1065
1097
  hasJwt: !!getAuthJwt(),
1066
1098
  });
@@ -1069,9 +1101,19 @@
1069
1101
  previewError = null;
1070
1102
  return;
1071
1103
  }
1104
+ if (await loadPublishedPreviewForCurrentStep()) {
1105
+ return;
1106
+ }
1072
1107
  const hasRepo = !!(normalizeRepositoryId(selectedWidget) || currentRepositoryId);
1073
1108
  if (!hasRepo) {
1074
- // New widget: repo is still being created — do not call build-from-repo (400 noise).
1109
+ const fallbackUrl = publishedHtmlUrlFromJsPath(widgetJsPath);
1110
+ if (fallbackUrl) {
1111
+ console.log('[WidgetDetails] No repo yet — loading js_path artifact', fallbackUrl);
1112
+ previewError = null;
1113
+ applyPreviewUrlToIframe(fallbackUrl);
1114
+ previewLoading = false;
1115
+ return;
1116
+ }
1075
1117
  console.log('[WidgetDetails] No repository yet — skipping build-from-repo');
1076
1118
  previewLoading = false;
1077
1119
  previewError = 'NO_CODE_YET';
@@ -1124,10 +1166,7 @@
1124
1166
  previewHydrating = false;
1125
1167
  resetPreviewMessageBridge();
1126
1168
  clearPendingRuntimeErrorOverlay();
1127
- // Keep previewLoading=true if it was set by widget selection (isWidgetLoading)
1128
- if (!isWidgetLoading) {
1129
- previewLoading = false;
1130
- }
1169
+ previewLoading = true;
1131
1170
  isWidgetLoading = false;
1132
1171
  previewError = null;
1133
1172
  previewRetryCount = 0;
@@ -1181,11 +1220,8 @@
1181
1220
  return;
1182
1221
  }
1183
1222
 
1184
- // Brand-new widget (no repo): skip Worker HEAD / build-from-repo.
1185
- // If the widget already has a GitLab repo, compile even without a cached commit SHA
1186
- // (localStorage miss, failed retries, unpublished lastCpgCommitSha).
1187
1223
  const hasRepo = !!(normalizeRepositoryId(selectedWidget) || currentRepositoryId);
1188
- if (!lastCommitId && lastPublishedVersion === null && !hasRepo) {
1224
+ if (!lastCommitId && lastPublishedVersion === null && !widgetJsPath && !hasRepo) {
1189
1225
  console.log('[WidgetDetails] No code yet — skipping Worker probe/build-from-repo', {
1190
1226
  widgetId: widgetId?.substring(0, 8),
1191
1227
  hasRepo,
@@ -1226,6 +1262,27 @@
1226
1262
  /** Trigger build-from-repo with retries — for duplicate/returning widgets.
1227
1263
  * Unlike loadWidget, this doesn't reset state. Just builds and updates preview.
1228
1264
  * When force is true, always recompiles from GitLab (evicts Worker cache) even if preview exists. */
1265
+ async function requestBuildFromRepo(widgetId: string): Promise<any> {
1266
+ const anyClient = agentClient as any;
1267
+ if (typeof anyClient.agentWidgetBuilderBuildFromRepoWidgetIdPost === 'function') {
1268
+ return anyClient.agentWidgetBuilderBuildFromRepoWidgetIdPost({ widgetId });
1269
+ }
1270
+ const cfg = anyClient.configuration || {};
1271
+ const builderBasePath = String(cfg.basePath || '').replace(/\/v1\/?$/, '') || undefined;
1272
+ const builder = new WidgetBuilderApi({
1273
+ basePath: builderBasePath,
1274
+ accessToken: cfg.accessToken,
1275
+ middleware: cfg.middleware,
1276
+ headers: cfg.headers,
1277
+ });
1278
+ console.log('[WidgetDetails] buildWidgetFromRepo', {
1279
+ widgetId: widgetId.substring(0, 8),
1280
+ builderBasePath,
1281
+ sdkPath: '/v1/agent/widget-builder/build-from-repo/{widgetId}',
1282
+ });
1283
+ return builder.buildWidgetFromRepo({ widgetId });
1284
+ }
1285
+
1229
1286
  export async function triggerBuildFromRepo(options?: { force?: boolean }): Promise<void> {
1230
1287
  const forceRebuild = options?.force === true;
1231
1288
  if (!selectedWidgetId) return;
@@ -1321,9 +1378,7 @@
1321
1378
  for (let attempt = 1; attempt <= maxRetries; attempt++) {
1322
1379
  if (isStaleRun()) return;
1323
1380
  try {
1324
- const buildData = await agentClient.agentWidgetBuilderBuildFromRepoWidgetIdPost({
1325
- widgetId: capturedWidgetId
1326
- });
1381
+ const buildData = await requestBuildFromRepo(capturedWidgetId);
1327
1382
  if (isStaleRun()) return;
1328
1383
 
1329
1384
  console.log('[WidgetDetails] triggerBuildFromRepo succeeded', { buildTimeMs: buildData?.data?.buildTimeMs ?? buildData?.buildTimeMs, attempt });
@@ -1397,24 +1452,17 @@
1397
1452
 
1398
1453
  if (selectedWidgetId === capturedWidgetId) {
1399
1454
  // For published widgets, try loading via API proxy (handles both R2 and CDN)
1400
- if (lastPublishedVersion && widgetJsPath && apiBaseUrl) {
1401
- const proxyUrl = `${apiBaseUrl}/widgets/${capturedWidgetId}/v${lastPublishedVersion}/widget.html`;
1402
- console.log('[WidgetDetails] triggerBuildFromRepo: build failed but widget is published — checking API proxy:', proxyUrl);
1403
- try {
1404
- const probeRes = await fetch(proxyUrl, { method: 'HEAD' });
1405
- if (probeRes.ok) {
1406
- console.log('[WidgetDetails] API proxy returned OK — loading published widget');
1407
- previewError = null;
1408
- previewLoading = true;
1409
- iframeSrc = proxyUrl;
1410
- setupPreviewFallbackTimeout();
1411
- syncPreviewLoadingWithIframe();
1412
- return;
1413
- }
1414
- console.warn('[WidgetDetails] API proxy returned', probeRes.status, '— artifact not on R2, showing generate button');
1415
- } catch (probeErr) {
1416
- console.warn('[WidgetDetails] API proxy probe failed:', probeErr);
1455
+ const sourceArtifactUrl = publishedHtmlUrlFromJsPath(widgetJsPath);
1456
+ if (sourceArtifactUrl) {
1457
+ console.log('[WidgetDetails] triggerBuildFromRepo: using js_path artifact (not new-id proxy)', sourceArtifactUrl);
1458
+ const reachable = await probePublishedArtifactUrl(sourceArtifactUrl);
1459
+ if (reachable) {
1460
+ previewError = null;
1461
+ previewLoading = true;
1462
+ applyPreviewUrlToIframe(sourceArtifactUrl);
1463
+ return;
1417
1464
  }
1465
+ console.warn('[WidgetDetails] js_path artifact not reachable — showing Rebuild');
1418
1466
  }
1419
1467
 
1420
1468
  previewLoading = false;
@@ -1461,7 +1509,9 @@
1461
1509
  }
1462
1510
 
1463
1511
  export function hasValidPreview(): boolean {
1464
- return !!iframeSrc && !previewError && !previewLoading;
1512
+ // Iframe can already show a compiled preview while `previewLoading`/`previewError`
1513
+ // still lag (e.g. iteration rebuild). Screenshot only needs a live contentWindow.
1514
+ return !!previewIframe?.contentWindow;
1465
1515
  }
1466
1516
 
1467
1517
  export function getPreviewIframe(): HTMLIFrameElement | null {
@@ -1533,6 +1583,37 @@
1533
1583
  notifyPreviewCompileErrorChanged();
1534
1584
  }
1535
1585
 
1586
+ /** Duplicate/fork fallback: keep published iframe if it loaded; otherwise compile from repo. */
1587
+ export async function retryPreviewIfEmpty(): Promise<void> {
1588
+ if (iframeSrc && !previewError) {
1589
+ console.log('[WidgetDetails] retryPreviewIfEmpty skipped — preview already loaded');
1590
+ return;
1591
+ }
1592
+ previewLoading = true;
1593
+ previewError = null;
1594
+ try {
1595
+ if (await loadPublishedPreviewForCurrentStep()) return;
1596
+ const hasRepo = !!(normalizeRepositoryId(selectedWidget) || currentRepositoryId);
1597
+ if (hasRepo) {
1598
+ console.log('[WidgetDetails] retryPreviewIfEmpty — compiling from repository');
1599
+ await triggerBuildFromRepo();
1600
+ return;
1601
+ }
1602
+ const fallbackUrl = publishedHtmlUrlFromJsPath(widgetJsPath);
1603
+ if (fallbackUrl) {
1604
+ applyPreviewUrlToIframe(fallbackUrl);
1605
+ previewLoading = false;
1606
+ return;
1607
+ }
1608
+ previewLoading = false;
1609
+ previewError = 'NO_CODE_YET';
1610
+ console.warn('[WidgetDetails] retryPreviewIfEmpty — no published artifact and no repo');
1611
+ } catch (err) {
1612
+ previewLoading = false;
1613
+ console.warn('[WidgetDetails] retryPreviewIfEmpty failed:', err);
1614
+ }
1615
+ }
1616
+
1536
1617
  export function showNoCodeYetState(): void {
1537
1618
  previewLoading = false;
1538
1619
  previewError = 'NO_CODE_YET';
@@ -1556,6 +1637,8 @@
1556
1637
  if (!target || pendingPreviewMessages.length === 0) return;
1557
1638
  for (const message of pendingPreviewMessages) {
1558
1639
  target.postMessage(message, '*');
1640
+ applyRichTextInPreviewIframe(message);
1641
+ applyDesignOverlayInPreviewIframe(message);
1559
1642
  }
1560
1643
  pendingPreviewMessages = [];
1561
1644
  }
@@ -1590,19 +1673,183 @@
1590
1673
  notifyPreviewCompileErrorChanged();
1591
1674
  }
1592
1675
  flushPendingPreviewMessages();
1593
- if (currentStep === 'edit' || currentStep === 'embed') {
1594
- dispatch('previewLoaded', { step: currentStep });
1595
- }
1676
+ dispatch('previewLoaded', { step: currentStep });
1596
1677
  // Emit previewReady so the parent app can clear the "…loading preview…" status.
1597
1678
  dispatch('previewReady', { widgetId: selectedWidgetId, step: currentStep });
1598
1679
  // Fill available preview area once the widget confirms it is ready.
1599
1680
  scheduleFitPreviewToArea();
1600
1681
  }
1601
1682
 
1683
+ function looksLikeHtmlMarkup(value: unknown): value is string {
1684
+ return typeof value === 'string' && /<[a-z][\s\S]*>/i.test(value);
1685
+ }
1686
+
1687
+ /** Same-origin preview iframes (Vite /__widget_builder__ proxy): restore bold/italic after widgets set textContent. */
1688
+ function applyRichTextInPreviewIframe(message: Record<string, unknown>): void {
1689
+ if (message.type !== 'widgetic:update') return;
1690
+ const doc = previewIframe?.contentDocument;
1691
+ if (!doc) return;
1692
+ const apply = () => {
1693
+ try {
1694
+ const content = message.content as Record<string, unknown> | undefined;
1695
+ if (content) {
1696
+ 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;
1701
+ });
1702
+ }
1703
+ }
1704
+ const items = message.contentItems as Array<Record<string, unknown>> | undefined;
1705
+ if (Array.isArray(items)) {
1706
+ for (const item of items) {
1707
+ const itemEl = item?.id
1708
+ ? doc.querySelector(`[data-content-item="${CSS.escape(String(item.id))}"]`)
1709
+ : null;
1710
+ const scope = itemEl || doc;
1711
+ for (const [key, value] of Object.entries(item || {})) {
1712
+ if (key === 'id' || !looksLikeHtmlMarkup(value)) continue;
1713
+ scope.querySelectorAll(`[data-content="${CSS.escape(key)}"]`).forEach((el) => {
1714
+ if (el instanceof HTMLImageElement || el instanceof HTMLInputElement) return;
1715
+ el.innerHTML = value;
1716
+ });
1717
+ }
1718
+ }
1719
+ }
1720
+ } catch (error) {
1721
+ console.warn('[WidgetDetails] Rich-text preview overlay skipped:', error);
1722
+ }
1723
+ };
1724
+ requestAnimationFrame(() => {
1725
+ apply();
1726
+ setTimeout(apply, 50);
1727
+ });
1728
+ }
1729
+
1730
+ function extractCssFontFamily(raw: string | undefined): string | null {
1731
+ if (!raw) return null;
1732
+ const quoted = raw.match(/["']([^"']+)["']/);
1733
+ if (quoted?.[1]) return quoted[1].trim();
1734
+ const first = raw.split(',')[0]?.trim();
1735
+ if (!first || first === 'sans-serif' || first === 'serif' || first === 'system-ui') return null;
1736
+ return first.replace(/^["']|["']$/g, '');
1737
+ }
1738
+
1739
+ function ensureGoogleFontInPreviewDoc(doc: Document, family: string): void {
1740
+ if (!family || /^(Inter|system-ui|sans-serif|serif)$/i.test(family)) return;
1741
+ const id = `widgetic-gf-${family.replace(/[^a-z0-9]+/gi, '-').toLowerCase()}`;
1742
+ if (doc.getElementById(id)) return;
1743
+ const link = doc.createElement('link');
1744
+ link.id = id;
1745
+ 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`;
1747
+ doc.head?.appendChild(link);
1748
+ console.log('[WidgetDetails] Loaded Google Font in preview iframe:', family);
1749
+ }
1750
+
1751
+ /** Same-origin preview: apply CSS vars, Google Fonts, title/subtitle, tile aspect-ratio. */
1752
+ function applyDesignOverlayInPreviewIframe(message: Record<string, unknown>): void {
1753
+ if (message.type !== 'widgetic:update') return;
1754
+ const design = message.design as Record<string, string> | undefined;
1755
+ if (!design) return;
1756
+ const doc = previewIframe?.contentDocument;
1757
+ if (!doc?.documentElement) {
1758
+ console.log('[WidgetDetails] Design overlay skipped — preview iframe not same-origin');
1759
+ return;
1760
+ }
1761
+ const apply = () => {
1762
+ try {
1763
+ for (const [key, value] of Object.entries(design)) {
1764
+ if (typeof value !== 'string' || !value) continue;
1765
+ doc.documentElement.style.setProperty(`--${key}`, value);
1766
+ doc.body?.style.setProperty(`--${key}`, value);
1767
+ }
1768
+
1769
+ const families = new Set<string>();
1770
+ for (const [key, value] of Object.entries(design)) {
1771
+ if (typeof value !== 'string') continue;
1772
+ if (/Family$/i.test(key) || (/font/i.test(key) && !/Weight$/i.test(key))) {
1773
+ const family = extractCssFontFamily(value);
1774
+ if (family) families.add(family);
1775
+ }
1776
+ }
1777
+ families.forEach((family) => ensureGoogleFontInPreviewDoc(doc, family));
1778
+
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');
1800
+ });
1801
+ }
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);
1818
+ }
1819
+ }
1820
+
1821
+ const aspectEntry = Object.entries(design).find(([key, value]) =>
1822
+ /aspect/i.test(key) && typeof value === 'string' && /\d+\s*\/\s*\d+/.test(value)
1823
+ );
1824
+ if (aspectEntry) {
1825
+ const cssRatio = aspectEntry[1].replace(/\s+/g, '');
1826
+ doc.querySelectorAll('img, [class*="aspect-"], .gallery-tile, .tile').forEach((el) => {
1827
+ if (!(el instanceof HTMLElement)) return;
1828
+ el.style.aspectRatio = cssRatio;
1829
+ el.style.objectFit = 'cover';
1830
+ if (el instanceof HTMLImageElement) {
1831
+ el.style.width = '100%';
1832
+ el.style.height = 'auto';
1833
+ }
1834
+ });
1835
+ console.log('[WidgetDetails] Applied tile aspect-ratio overlay:', cssRatio);
1836
+ }
1837
+ } catch (error) {
1838
+ console.warn('[WidgetDetails] Design overlay skipped:', error);
1839
+ }
1840
+ };
1841
+ requestAnimationFrame(() => {
1842
+ apply();
1843
+ setTimeout(apply, 50);
1844
+ });
1845
+ }
1846
+
1602
1847
  /** Send a postMessage to the widget preview iframe (for live property editing) */
1603
1848
  export function sendMessageToPreview(message: Record<string, unknown>): void {
1604
1849
  if (previewWidgetReady && previewIframe?.contentWindow) {
1605
1850
  previewIframe.contentWindow.postMessage(message, '*');
1851
+ applyRichTextInPreviewIframe(message);
1852
+ applyDesignOverlayInPreviewIframe(message);
1606
1853
  return;
1607
1854
  }
1608
1855
  pendingPreviewMessages.push(message);
@@ -1944,9 +2191,15 @@
1944
2191
  }
1945
2192
  } else {
1946
2193
  previewHydrating = false;
1947
- previewWidgetReady = true;
1948
2194
  dispatch('previewReady', { widgetId: selectedWidgetId, step: currentStep });
1949
- // Create step: iframe load does not go through markPreviewWidgetReady — still fit-to-area.
2195
+ // Create: wait for widgetic:ready so contentItems overlay is not sent before the widget listens.
2196
+ if (!previewWidgetReady) {
2197
+ if (previewReadyFallbackTimeout) clearTimeout(previewReadyFallbackTimeout);
2198
+ previewReadyFallbackTimeout = setTimeout(() => {
2199
+ console.warn('[WidgetDetails] Create step widgetic:ready fallback — applying overlay');
2200
+ markPreviewWidgetReady();
2201
+ }, 1500);
2202
+ }
1950
2203
  scheduleFitPreviewToArea();
1951
2204
  }
1952
2205
  }
@@ -2124,9 +2377,7 @@
2124
2377
  }
2125
2378
 
2126
2379
  function clampPanelWidth(w: number): number {
2127
- if (typeof window === 'undefined') return Math.min(DETAILS_PANEL_MAX_WIDTH, w);
2128
- const maxWidth = Math.min(DETAILS_PANEL_MAX_WIDTH, window.innerWidth - 24);
2129
- return Math.max(DETAILS_PANEL_MIN_WIDTH, Math.min(w, maxWidth));
2380
+ return Math.max(DETAILS_PANEL_MIN_WIDTH, Math.min(w, DETAILS_PANEL_MAX_WIDTH));
2130
2381
  }
2131
2382
 
2132
2383
  function clampPanelHeight(h: number): number {
@@ -2195,8 +2446,8 @@
2195
2446
  }
2196
2447
 
2197
2448
  /**
2198
- * Park the panel beside a canvas widget shape: right if the shape is left of
2199
- * the canvas viewport center, otherwise left. Caps at 600×800 (or smaller).
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.
2200
2451
  */
2201
2452
  export function positionBesideShape(shapeRect: {
2202
2453
  left: number;
@@ -2217,41 +2468,20 @@
2217
2468
  return;
2218
2469
  }
2219
2470
 
2220
- const pad = 8;
2221
- const viewLeft = shapeRect.viewportLeft;
2222
- const viewRight = shapeRect.viewportLeft + shapeRect.viewportWidth;
2223
- const availableWidth = Math.max(DETAILS_PANEL_MIN_WIDTH, shapeRect.viewportWidth - pad * 2);
2224
- detailsPanelWidth = clampPanelWidth(Math.min(DETAILS_PANEL_MAX_WIDTH, availableWidth));
2225
- detailsPanelHeight = clampPanelHeight(
2226
- Math.min(DETAILS_PANEL_MAX_HEIGHT, shapeRect.viewportHeight - pad * 2),
2227
- );
2228
-
2229
- const shapeCenterX = shapeRect.left + shapeRect.width / 2;
2230
- const viewCenterX = shapeRect.viewportLeft + shapeRect.viewportWidth / 2;
2231
- const openOnRight = shapeCenterX < viewCenterX;
2232
- if (openOnRight) {
2233
- detailsPanelX = shapeRect.left + shapeRect.width + DETAILS_PANEL_SHAPE_GAP;
2234
- if (detailsPanelX + detailsPanelWidth > viewRight - pad) {
2235
- detailsPanelX = Math.max(viewLeft + pad, viewRight - pad - detailsPanelWidth);
2236
- }
2237
- } else {
2238
- detailsPanelX = shapeRect.left - DETAILS_PANEL_SHAPE_GAP - detailsPanelWidth;
2239
- if (detailsPanelX < viewLeft + pad) {
2240
- detailsPanelX = viewLeft + pad;
2241
- }
2242
- }
2471
+ const gap = DETAILS_PANEL_SHAPE_GAP;
2472
+ detailsPanelWidth = clampPanelWidth(DETAILS_PANEL_MAX_WIDTH);
2473
+ detailsPanelHeight = clampPanelHeight(DETAILS_PANEL_MAX_HEIGHT);
2243
2474
 
2244
- const maxY = window.innerHeight - detailsPanelHeight - pad;
2245
- detailsPanelY = Math.min(
2246
- maxY,
2247
- Math.max(detailsPanelTopOffset, shapeRect.top),
2248
- );
2475
+ const x = shapeRect.left + shapeRect.width + gap;
2476
+ const pos = clampDetailsPanelPosition(x, Math.max(detailsPanelTopOffset, shapeRect.top));
2477
+ detailsPanelX = pos.x;
2478
+ detailsPanelY = pos.y;
2249
2479
  console.log('[WidgetDetails] Positioned beside shape', {
2250
- openOnRight,
2251
2480
  x: Math.round(detailsPanelX),
2252
2481
  y: Math.round(detailsPanelY),
2253
2482
  w: detailsPanelWidth,
2254
2483
  h: detailsPanelHeight,
2484
+ shapeLeft: Math.round(shapeRect.left),
2255
2485
  });
2256
2486
  }
2257
2487
 
@@ -2273,10 +2503,7 @@
2273
2503
  const width = twoCol?.clientWidth || detailsPanelWidth || DETAILS_PANEL_DEFAULT_WIDTH;
2274
2504
  if (width <= 0) return;
2275
2505
  const deltaPercent = ((e.clientX - columnResizeStartX) / width) * 100;
2276
- leftColPercent = Math.min(
2277
- LEFT_COL_MAX_PERCENT,
2278
- Math.max(LEFT_COL_MIN_PERCENT, columnResizeStartPercent + deltaPercent)
2279
- );
2506
+ leftColPercent = clampLeftColPercent(columnResizeStartPercent + deltaPercent);
2280
2507
  // Keep preview iframe inside the shrinking right column (avoid clipped "cut off" widgets).
2281
2508
  requestAnimationFrame(() => {
2282
2509
  clampPreviewToArea();
@@ -2363,15 +2590,27 @@
2363
2590
  window.removeEventListener('blur', stopFn);
2364
2591
  }
2365
2592
 
2366
- function startDetailsResize(e: MouseEvent) {
2593
+ let detailsResizeEdge: 'se' | 'sw' | 'e' | 'w' | 's' = 'se';
2594
+ let detailsResizeStartPanelX = 0;
2595
+ let detailsResizeStartPanelY = 0;
2596
+
2597
+ function startDetailsResize(e: MouseEvent, edge: 'se' | 'sw' | 'e' | 'w' | 's' = 'se') {
2367
2598
  e.preventDefault();
2368
2599
  e.stopPropagation();
2369
2600
  isResizingDetails = true;
2601
+ detailsResizeEdge = edge;
2370
2602
  detailsResizeStartX = e.clientX;
2371
2603
  detailsResizeStartY = e.clientY;
2372
2604
  detailsResizeStartWidth = detailsPanelWidth;
2373
2605
  detailsResizeStartHeight = detailsPanelHeight || getDefaultDetailsPanelHeight();
2374
- document.body.style.cursor = 'nwse-resize';
2606
+ detailsResizeStartPanelX = detailsPanelX;
2607
+ detailsResizeStartPanelY = detailsPanelY;
2608
+ const cursor =
2609
+ edge === 'e' || edge === 'w' ? 'ew-resize'
2610
+ : edge === 's' ? 'ns-resize'
2611
+ : edge === 'sw' ? 'nesw-resize'
2612
+ : 'nwse-resize';
2613
+ document.body.style.cursor = cursor;
2375
2614
  document.body.style.userSelect = 'none';
2376
2615
  window.addEventListener('mousemove', onDetailsResize);
2377
2616
  addGlobalStopListeners(stopDetailsResize);
@@ -2381,10 +2620,46 @@
2381
2620
  if (!isResizingDetails) return;
2382
2621
  const deltaX = e.clientX - detailsResizeStartX;
2383
2622
  const deltaY = e.clientY - detailsResizeStartY;
2384
- const maxWidth = window.innerWidth - detailsPanelX - 20;
2385
- const maxHeight = window.innerHeight - detailsPanelY - 20;
2386
- detailsPanelWidth = Math.min(maxWidth, Math.max(DETAILS_PANEL_MIN_WIDTH, detailsResizeStartWidth + deltaX));
2387
- detailsPanelHeight = Math.min(maxHeight, Math.max(DETAILS_PANEL_MIN_HEIGHT, detailsResizeStartHeight + deltaY));
2623
+ const minW = DETAILS_PANEL_MIN_WIDTH;
2624
+ const minH = DETAILS_PANEL_MIN_HEIGHT;
2625
+ const maxRight = window.innerWidth - 8;
2626
+ const maxBottom = window.innerHeight - 8;
2627
+
2628
+ let nextW = detailsResizeStartWidth;
2629
+ let nextH = detailsResizeStartHeight;
2630
+ let nextX = detailsResizeStartPanelX;
2631
+ let nextY = detailsResizeStartPanelY;
2632
+
2633
+ if (detailsResizeEdge === 'e' || detailsResizeEdge === 'se') {
2634
+ nextW = detailsResizeStartWidth + deltaX;
2635
+ }
2636
+ if (detailsResizeEdge === 'w' || detailsResizeEdge === 'sw') {
2637
+ nextW = detailsResizeStartWidth - deltaX;
2638
+ nextX = detailsResizeStartPanelX + deltaX;
2639
+ }
2640
+ if (detailsResizeEdge === 's' || detailsResizeEdge === 'se' || detailsResizeEdge === 'sw') {
2641
+ nextH = detailsResizeStartHeight + deltaY;
2642
+ }
2643
+
2644
+ nextW = Math.max(minW, nextW);
2645
+ nextH = Math.max(minH, nextH);
2646
+ if (detailsResizeEdge === 'w' || detailsResizeEdge === 'sw') {
2647
+ nextX = detailsResizeStartPanelX + (detailsResizeStartWidth - nextW);
2648
+ if (nextX < 8) {
2649
+ nextW -= (8 - nextX);
2650
+ nextX = 8;
2651
+ }
2652
+ }
2653
+ if (nextX + nextW > maxRight) nextW = maxRight - nextX;
2654
+ if (nextY + nextH > maxBottom) nextH = maxBottom - nextY;
2655
+
2656
+ detailsPanelWidth = nextW;
2657
+ detailsPanelHeight = nextH;
2658
+ detailsPanelX = nextX;
2659
+ detailsPanelY = nextY;
2660
+ if (!previewColumnCollapsed) {
2661
+ leftColPercent = clampLeftColPercent(leftColPercent);
2662
+ }
2388
2663
  }
2389
2664
 
2390
2665
  function stopDetailsResize() {
@@ -2403,14 +2678,16 @@
2403
2678
 
2404
2679
  function clampDetailsPanelPosition(x: number, y: number): { x: number; y: number } {
2405
2680
  if (typeof window === 'undefined') return { x, y };
2406
- const pad = 8;
2407
2681
  const panelH = detailsPanelHeight || getDefaultDetailsPanelHeight();
2408
2682
  const panelW = detailsPanelWidth || DETAILS_PANEL_DEFAULT_WIDTH;
2683
+ const minVisibleX = Math.max(120, panelW * 0.5);
2684
+ const minVisibleY = Math.max(160, panelH * 0.5);
2409
2685
  const minTop = detailsPanelTopOffset;
2410
- const maxTop = Math.max(minTop, window.innerHeight - Math.min(panelH, window.innerHeight - pad) - pad);
2411
- const maxLeft = Math.max(pad, window.innerWidth - Math.min(panelW, window.innerWidth - pad) - pad);
2686
+ const minX = minVisibleX - panelW;
2687
+ const maxX = window.innerWidth - minVisibleX;
2688
+ const maxTop = Math.max(minTop, window.innerHeight - minVisibleY);
2412
2689
  return {
2413
- x: Math.min(maxLeft, Math.max(pad, x)),
2690
+ x: Math.min(maxX, Math.max(minX, x)),
2414
2691
  y: Math.min(maxTop, Math.max(minTop, y)),
2415
2692
  };
2416
2693
  }
@@ -2847,16 +3124,28 @@
2847
3124
  {#if selectedWidgetId}
2848
3125
  <!-- svelte-ignore a11y-no-static-element-interactions -->
2849
3126
  <div
2850
- class="widget-details-floating-panel pointer-events-auto fixed overflow-hidden
2851
- {showWidgetDetails ? 'flex flex-col' : 'hidden'}
3127
+ class="widget-details-floating-shell pointer-events-none fixed overflow-visible {showWidgetDetails ? '' : 'hidden'}"
3128
+ style="left: {detailsPanelX}px; top: {detailsPanelY}px; width: {detailsPanelWidth}px; z-index: {panelZIndex};
3129
+ height: {detailsPanelHeight ? detailsPanelHeight + 'px' : `calc(100vh - ${detailsPanelTopOffset + detailsPanelBottomMargin}px)`};"
3130
+ >
3131
+ <button
3132
+ type="button"
3133
+ onclick={handleCloseButtonClick}
3134
+ onmousedown={(e) => e.stopPropagation()}
3135
+ 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"
3137
+ title="Close widget details"
3138
+ >
3139
+ ✕
3140
+ </button>
3141
+ <div
3142
+ class="widget-details-floating-panel pointer-events-auto relative flex flex-col h-full w-full
2852
3143
  {fillViewport
2853
- ? 'bg-white rounded-none border-x border-b border-t-0 ring-0 shadow-none'
2854
- : 'bg-white/95 backdrop-blur-sm shadow-2xl rounded-xl border'}
3144
+ ? 'bg-white rounded-none border-x border-b border-t-0 ring-0 shadow-none overflow-hidden'
3145
+ : 'bg-white/95 backdrop-blur-sm shadow-2xl rounded-xl border overflow-hidden'}
2855
3146
  {isFocusedPanel
2856
3147
  ? (fillViewport ? 'border-blue-400' : 'border-blue-400 ring-2 ring-blue-200/80')
2857
3148
  : 'border-gray-300 opacity-[0.97]'}"
2858
- style="left: {detailsPanelX}px; top: {detailsPanelY}px; width: {detailsPanelWidth}px; z-index: {panelZIndex};
2859
- height: {detailsPanelHeight ? detailsPanelHeight + 'px' : `calc(100vh - ${detailsPanelTopOffset + detailsPanelBottomMargin}px)`};"
2860
3149
  onmousedown={handlePanelFocusRequest}
2861
3150
  >
2862
3151
  <!-- Widget Details Top Bar — title, steps, and close on one row -->
@@ -2866,7 +3155,7 @@
2866
3155
  onpointerdown={startDetailsDrag}
2867
3156
  >
2868
3157
  <span
2869
- class="widget-details-name pointer-events-none absolute left-3 top-1/2 -translate-y-1/2 max-w-[22%] truncate text-xs font-medium text-gray-600"
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"
2870
3159
  title={selectedWidget?.name || ''}
2871
3160
  >
2872
3161
  {selectedWidget?.name || (isDraftConvertPanel ? 'Widget Draft' : 'Widget')}
@@ -2925,7 +3214,7 @@
2925
3214
  </div>
2926
3215
  </div>
2927
3216
 
2928
- <div class="widget-details-debug-buttons flex items-center gap-1.5 shrink-0">
3217
+ <div class="widget-details-debug-buttons flex items-center gap-1.5 shrink-0 pr-1">
2929
3218
  <button
2930
3219
  type="button"
2931
3220
  onclick={() => previewColumnCollapsed ? expandPreviewColumn() : collapsePreviewColumn()}
@@ -2939,23 +3228,13 @@
2939
3228
  >
2940
3229
  {previewColumnCollapsed ? 'Open Preview' : 'Close Preview'}
2941
3230
  </button>
2942
- <button
2943
- type="button"
2944
- onclick={handleCloseButtonClick}
2945
- onmousedown={(e) => e.stopPropagation()}
2946
- onpointerdown={(e) => e.stopPropagation()}
2947
- class="widget-details-close-bt w-6 h-6 flex items-center justify-center rounded-lg hover:bg-gray-200 text-gray-500 hover:text-gray-700 cursor-pointer transition-colors text-sm"
2948
- title="Close widget details"
2949
- >
2950
- ✕
2951
- </button>
2952
3231
  </div>
2953
3232
  </div>
2954
3233
  </div>
2955
3234
 
2956
3235
  <!-- Widget Details Content: Two-column layout -->
2957
- <div class="widget-details-content-wrapper relative m-2.5 min-w-0 min-h-0 flex-1">
2958
- <div class="widget-details-two-col flex h-full min-h-0 gap-0">
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">
2959
3238
 
2960
3239
  <!-- ═══ LEFT COLUMN: Step Content (scrollable) ═══ -->
2961
3240
  <div
@@ -2988,24 +3267,28 @@
2988
3267
  Commit: <span class="font-mono font-medium text-gray-600" title={lastCommitId || ''}>{lastCommitId ? lastCommitId.substring(0, 8) : '—'}</span>
2989
3268
  </span>
2990
3269
  {#if lastPublishedVersion !== null}
2991
- <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-50 border border-emerald-200 text-emerald-600 hover:bg-emerald-100 transition-colors whitespace-nowrap" title="Open published widget v{lastPublishedVersion}">Live: v{lastPublishedVersion} ↗</a>
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>
2992
3271
  {/if}
2993
3272
  </div>
2994
3273
  </div>
2995
3274
 
2996
3275
  <!-- Scrollable step content -->
2997
3276
  <div
2998
- class="widget-details-left-content flex flex-col flex-1 min-h-0 relative rounded-b-lg {currentStep === 'edit' ? 'overflow-hidden' : 'overflow-hidden'}"
3277
+ class="widget-details-left-content flex flex-col flex-1 min-h-0 relative rounded-b-lg overflow-hidden overflow-x-hidden"
2999
3278
  bind:this={contentContainerEl}
3000
3279
  >
3001
3280
  <div class="actions-panel widget-details-actions-panel relative flex flex-col flex-1 min-h-0">
3002
3281
 
3003
3282
  <!-- Keep all steps mounted — switching tabs must not destroy chat / PropsEditor state -->
3004
- <div class="widget-details-create-step flex flex-col flex-1 min-h-0 overflow-hidden gap-2 p-2 {currentStep === 'create' ? '' : 'hidden'}">
3005
- <slot name="code-generation" />
3006
- <slot name="publish-widget" />
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" />
3286
+ </div>
3287
+ <div class="widget-details-create-publish-ct shrink-0 px-2 pb-2">
3288
+ <slot name="publish-widget" />
3289
+ </div>
3007
3290
  </div>
3008
- <div class="widget-details-edit-step flex flex-col h-full min-h-0 overflow-hidden {currentStep === 'edit' ? '' : 'hidden'}">
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'}">
3009
3292
  <slot name="composition-editor" />
3010
3293
  </div>
3011
3294
  <div class="widget-details-embed-step flex flex-col gap-2 p-2 {currentStep === 'embed' ? '' : 'hidden'}">
@@ -3138,7 +3421,8 @@
3138
3421
  <div class="widget-preview-fallback relative flex items-center justify-center h-full rounded-lg bg-gray-50 overflow-hidden">
3139
3422
  {#if isGeneratingCode || previewLoading}
3140
3423
  <GenerateLoader
3141
- title={isGeneratingCode ? 'Generating code…' : 'Building preview…'}
3424
+ title={isGeneratingCode ? 'Generating code…' : 'Loading preview…'}
3425
+ subtitle=""
3142
3426
  size="md"
3143
3427
  />
3144
3428
  {:else if previewError === 'DRAFT_PANEL'}
@@ -3190,6 +3474,17 @@
3190
3474
  </div>
3191
3475
  {/if}
3192
3476
  </div>
3477
+ {:else if lastGenerationFailed}
3478
+ <div class="widget-preview-generation-error flex flex-col items-center justify-center text-center p-4">
3479
+ <div class="text-3xl mb-2">⚠️</div>
3480
+ <div class="font-medium text-sm text-red-600 mb-1">Code generation failed</div>
3481
+ <div class="text-xs max-w-[220px] text-gray-600 mb-3">{lastGenerationError || 'Fix the prompt or attachments, then retry.'}</div>
3482
+ <button
3483
+ type="button"
3484
+ onclick={() => { dispatch('generateNow', { widgetId: selectedWidgetId }); }}
3485
+ class="generate-now-btn px-3 py-1.5 text-xs rounded border border-green-500 text-green-700 bg-white hover:bg-green-50 cursor-pointer font-medium"
3486
+ >▶ Retry</button>
3487
+ </div>
3193
3488
  {:else if isLoadingDirectPreviewUrl || previewLoading}
3194
3489
  <GenerateLoader title="Building Preview…" subtitle="Compiling widget code…" size="md" />
3195
3490
  {:else}
@@ -3252,7 +3547,7 @@
3252
3547
  </div> <!-- end of widget-details-content-wrapper -->
3253
3548
 
3254
3549
  <!-- svelte-ignore a11y-no-static-element-interactions -->
3255
- <div class="absolute bottom-0 right-0 w-4 h-4 cursor-nwse-resize z-10 group" onmousedown={startDetailsResize} title="Resize panel">
3550
+ <div class="widget-details-resize-se absolute bottom-0 right-0 w-4 h-4 cursor-nwse-resize z-20 group" onmousedown={(e) => startDetailsResize(e, 'se')} title="Resize panel">
3256
3551
  <svg class="w-3 h-3 absolute bottom-0.5 right-0.5 text-gray-400 group-hover:text-gray-600 transition-colors" viewBox="0 0 6 6" fill="currentColor">
3257
3552
  <circle cx="5" cy="1" r="0.7"/>
3258
3553
  <circle cx="3" cy="3" r="0.7"/>
@@ -3262,6 +3557,26 @@
3262
3557
  <circle cx="5" cy="5" r="0.7"/>
3263
3558
  </svg>
3264
3559
  </div>
3560
+ {#if !fillViewport}
3561
+ <!-- svelte-ignore a11y-no-static-element-interactions -->
3562
+ <div class="widget-details-resize-sw absolute bottom-0 left-0 w-4 h-4 cursor-nesw-resize z-20 group" onmousedown={(e) => startDetailsResize(e, 'sw')} title="Resize panel">
3563
+ <svg class="w-3 h-3 absolute bottom-0.5 left-0.5 text-gray-400 group-hover:text-gray-600 transition-colors rotate-90" viewBox="0 0 6 6" fill="currentColor">
3564
+ <circle cx="5" cy="1" r="0.7"/>
3565
+ <circle cx="3" cy="3" r="0.7"/>
3566
+ <circle cx="5" cy="3" r="0.7"/>
3567
+ <circle cx="1" cy="5" r="0.7"/>
3568
+ <circle cx="3" cy="5" r="0.7"/>
3569
+ <circle cx="5" cy="5" r="0.7"/>
3570
+ </svg>
3571
+ </div>
3572
+ <!-- svelte-ignore a11y-no-static-element-interactions -->
3573
+ <div class="widget-details-resize-e absolute top-8 bottom-4 right-0 w-1.5 cursor-ew-resize z-20 hover:bg-blue-400/30" onmousedown={(e) => startDetailsResize(e, 'e')} title="Resize width"></div>
3574
+ <!-- svelte-ignore a11y-no-static-element-interactions -->
3575
+ <div class="widget-details-resize-w absolute top-8 bottom-4 left-0 w-1.5 cursor-ew-resize z-20 hover:bg-blue-400/30" onmousedown={(e) => startDetailsResize(e, 'w')} title="Resize width"></div>
3576
+ <!-- svelte-ignore a11y-no-static-element-interactions -->
3577
+ <div class="widget-details-resize-s absolute bottom-0 left-4 right-4 h-1.5 cursor-ns-resize z-20 hover:bg-blue-400/30" onmousedown={(e) => startDetailsResize(e, 's')} title="Resize height"></div>
3578
+ {/if}
3579
+ </div>
3265
3580
  </div>
3266
3581
  {/if}
3267
3582