@widgetic/creator 0.3.51 → 0.3.54

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -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;
@@ -1005,35 +1017,39 @@
1005
1017
 
1006
1018
  /** Load published CDN preview without Worker probe (safe for background panels). */
1007
1019
  async function loadPublishedPreviewForCurrentStep(): Promise<boolean> {
1008
- if (currentStep === 'edit' && editPreviewUrl) {
1009
- const reachable = await probePublishedArtifactUrl(editPreviewUrl);
1010
- publishedArtifactReachable = reachable;
1011
- if (!reachable) {
1012
- previewError = 'Published version is not available. Use Rebuild code.';
1013
- previewLoading = false;
1014
- return false;
1015
- }
1016
- _lastEditPreviewBase = null;
1017
- switchPreviewUrlForStep('edit', { forceReload: true });
1018
- previewBuildDeferred = false;
1019
- previewError = null;
1020
- 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;
1021
1037
  }
1022
- if (currentStep === 'embed' && embedPreviewUrl) {
1023
- const reachable = await probePublishedArtifactUrl(embedPreviewUrl);
1024
- publishedArtifactReachable = reachable;
1025
- if (!reachable) {
1026
- previewError = 'Published version is not available. Use Rebuild code.';
1027
- previewLoading = false;
1028
- return false;
1029
- }
1038
+ if (currentStep === 'embed') {
1030
1039
  _lastEmbedPreviewBase = null;
1031
- applyPreviewUrlToIframe(embedPreviewUrl);
1032
- previewBuildDeferred = false;
1033
- previewError = null;
1034
- 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
+ }
1035
1048
  }
1036
- return false;
1049
+ previewBuildDeferred = false;
1050
+ previewError = null;
1051
+ previewLoading = false;
1052
+ return true;
1037
1053
  }
1038
1054
 
1039
1055
  /** Probe Worker / build-from-repo pipeline without resetting panel state. */
@@ -1076,7 +1092,7 @@
1076
1092
  }
1077
1093
 
1078
1094
  if (probeResult === 'not-found') {
1079
- console.log('[WidgetDetails] Widget not on Worker — build-from-repo', {
1095
+ console.log('[WidgetDetails] Widget not on Worker — published artifact or build-from-repo', {
1080
1096
  widgetId: capturedWidgetId.substring(0, 8),
1081
1097
  hasJwt: !!getAuthJwt(),
1082
1098
  });
@@ -1085,9 +1101,19 @@
1085
1101
  previewError = null;
1086
1102
  return;
1087
1103
  }
1104
+ if (await loadPublishedPreviewForCurrentStep()) {
1105
+ return;
1106
+ }
1088
1107
  const hasRepo = !!(normalizeRepositoryId(selectedWidget) || currentRepositoryId);
1089
1108
  if (!hasRepo) {
1090
- // 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
+ }
1091
1117
  console.log('[WidgetDetails] No repository yet — skipping build-from-repo');
1092
1118
  previewLoading = false;
1093
1119
  previewError = 'NO_CODE_YET';
@@ -1140,10 +1166,7 @@
1140
1166
  previewHydrating = false;
1141
1167
  resetPreviewMessageBridge();
1142
1168
  clearPendingRuntimeErrorOverlay();
1143
- // Keep previewLoading=true if it was set by widget selection (isWidgetLoading)
1144
- if (!isWidgetLoading) {
1145
- previewLoading = false;
1146
- }
1169
+ previewLoading = true;
1147
1170
  isWidgetLoading = false;
1148
1171
  previewError = null;
1149
1172
  previewRetryCount = 0;
@@ -1197,11 +1220,8 @@
1197
1220
  return;
1198
1221
  }
1199
1222
 
1200
- // Brand-new widget (no repo): skip Worker HEAD / build-from-repo.
1201
- // If the widget already has a GitLab repo, compile even without a cached commit SHA
1202
- // (localStorage miss, failed retries, unpublished lastCpgCommitSha).
1203
1223
  const hasRepo = !!(normalizeRepositoryId(selectedWidget) || currentRepositoryId);
1204
- if (!lastCommitId && lastPublishedVersion === null && !hasRepo) {
1224
+ if (!lastCommitId && lastPublishedVersion === null && !widgetJsPath && !hasRepo) {
1205
1225
  console.log('[WidgetDetails] No code yet — skipping Worker probe/build-from-repo', {
1206
1226
  widgetId: widgetId?.substring(0, 8),
1207
1227
  hasRepo,
@@ -1242,6 +1262,27 @@
1242
1262
  /** Trigger build-from-repo with retries — for duplicate/returning widgets.
1243
1263
  * Unlike loadWidget, this doesn't reset state. Just builds and updates preview.
1244
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
+
1245
1286
  export async function triggerBuildFromRepo(options?: { force?: boolean }): Promise<void> {
1246
1287
  const forceRebuild = options?.force === true;
1247
1288
  if (!selectedWidgetId) return;
@@ -1337,9 +1378,7 @@
1337
1378
  for (let attempt = 1; attempt <= maxRetries; attempt++) {
1338
1379
  if (isStaleRun()) return;
1339
1380
  try {
1340
- const buildData = await agentClient.agentWidgetBuilderBuildFromRepoWidgetIdPost({
1341
- widgetId: capturedWidgetId
1342
- });
1381
+ const buildData = await requestBuildFromRepo(capturedWidgetId);
1343
1382
  if (isStaleRun()) return;
1344
1383
 
1345
1384
  console.log('[WidgetDetails] triggerBuildFromRepo succeeded', { buildTimeMs: buildData?.data?.buildTimeMs ?? buildData?.buildTimeMs, attempt });
@@ -1413,24 +1452,17 @@
1413
1452
 
1414
1453
  if (selectedWidgetId === capturedWidgetId) {
1415
1454
  // For published widgets, try loading via API proxy (handles both R2 and CDN)
1416
- if (lastPublishedVersion && widgetJsPath && apiBaseUrl) {
1417
- const proxyUrl = `${apiBaseUrl}/widgets/${capturedWidgetId}/v${lastPublishedVersion}/widget.html`;
1418
- console.log('[WidgetDetails] triggerBuildFromRepo: build failed but widget is published — checking API proxy:', proxyUrl);
1419
- try {
1420
- const probeRes = await fetch(proxyUrl, { method: 'HEAD' });
1421
- if (probeRes.ok) {
1422
- console.log('[WidgetDetails] API proxy returned OK — loading published widget');
1423
- previewError = null;
1424
- previewLoading = true;
1425
- iframeSrc = proxyUrl;
1426
- setupPreviewFallbackTimeout();
1427
- syncPreviewLoadingWithIframe();
1428
- return;
1429
- }
1430
- console.warn('[WidgetDetails] API proxy returned', probeRes.status, '— artifact not on R2, showing generate button');
1431
- } catch (probeErr) {
1432
- 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;
1433
1464
  }
1465
+ console.warn('[WidgetDetails] js_path artifact not reachable — showing Rebuild');
1434
1466
  }
1435
1467
 
1436
1468
  previewLoading = false;
@@ -1551,6 +1583,37 @@
1551
1583
  notifyPreviewCompileErrorChanged();
1552
1584
  }
1553
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
+
1554
1617
  export function showNoCodeYetState(): void {
1555
1618
  previewLoading = false;
1556
1619
  previewError = 'NO_CODE_YET';
@@ -1574,6 +1637,8 @@
1574
1637
  if (!target || pendingPreviewMessages.length === 0) return;
1575
1638
  for (const message of pendingPreviewMessages) {
1576
1639
  target.postMessage(message, '*');
1640
+ applyRichTextInPreviewIframe(message);
1641
+ applyDesignOverlayInPreviewIframe(message);
1577
1642
  }
1578
1643
  pendingPreviewMessages = [];
1579
1644
  }
@@ -1662,11 +1727,129 @@
1662
1727
  });
1663
1728
  }
1664
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
+
1665
1847
  /** Send a postMessage to the widget preview iframe (for live property editing) */
1666
1848
  export function sendMessageToPreview(message: Record<string, unknown>): void {
1667
1849
  if (previewWidgetReady && previewIframe?.contentWindow) {
1668
1850
  previewIframe.contentWindow.postMessage(message, '*');
1669
1851
  applyRichTextInPreviewIframe(message);
1852
+ applyDesignOverlayInPreviewIframe(message);
1670
1853
  return;
1671
1854
  }
1672
1855
  pendingPreviewMessages.push(message);
@@ -2194,9 +2377,7 @@
2194
2377
  }
2195
2378
 
2196
2379
  function clampPanelWidth(w: number): number {
2197
- if (typeof window === 'undefined') return Math.min(DETAILS_PANEL_MAX_WIDTH, w);
2198
- const maxWidth = Math.min(DETAILS_PANEL_MAX_WIDTH, window.innerWidth - 24);
2199
- 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));
2200
2381
  }
2201
2382
 
2202
2383
  function clampPanelHeight(h: number): number {
@@ -2265,8 +2446,8 @@
2265
2446
  }
2266
2447
 
2267
2448
  /**
2268
- * Park the panel beside a canvas widget shape: right if the shape is left of
2269
- * 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.
2270
2451
  */
2271
2452
  export function positionBesideShape(shapeRect: {
2272
2453
  left: number;
@@ -2287,41 +2468,20 @@
2287
2468
  return;
2288
2469
  }
2289
2470
 
2290
- const pad = 8;
2291
- const viewLeft = shapeRect.viewportLeft;
2292
- const viewRight = shapeRect.viewportLeft + shapeRect.viewportWidth;
2293
- const availableWidth = Math.max(DETAILS_PANEL_MIN_WIDTH, shapeRect.viewportWidth - pad * 2);
2294
- detailsPanelWidth = clampPanelWidth(Math.min(DETAILS_PANEL_MAX_WIDTH, availableWidth));
2295
- detailsPanelHeight = clampPanelHeight(
2296
- Math.min(DETAILS_PANEL_MAX_HEIGHT, shapeRect.viewportHeight - pad * 2),
2297
- );
2298
-
2299
- const shapeCenterX = shapeRect.left + shapeRect.width / 2;
2300
- const viewCenterX = shapeRect.viewportLeft + shapeRect.viewportWidth / 2;
2301
- const openOnRight = shapeCenterX < viewCenterX;
2302
- if (openOnRight) {
2303
- detailsPanelX = shapeRect.left + shapeRect.width + DETAILS_PANEL_SHAPE_GAP;
2304
- if (detailsPanelX + detailsPanelWidth > viewRight - pad) {
2305
- detailsPanelX = Math.max(viewLeft + pad, viewRight - pad - detailsPanelWidth);
2306
- }
2307
- } else {
2308
- detailsPanelX = shapeRect.left - DETAILS_PANEL_SHAPE_GAP - detailsPanelWidth;
2309
- if (detailsPanelX < viewLeft + pad) {
2310
- detailsPanelX = viewLeft + pad;
2311
- }
2312
- }
2471
+ const gap = DETAILS_PANEL_SHAPE_GAP;
2472
+ detailsPanelWidth = clampPanelWidth(DETAILS_PANEL_MAX_WIDTH);
2473
+ detailsPanelHeight = clampPanelHeight(DETAILS_PANEL_MAX_HEIGHT);
2313
2474
 
2314
- const maxY = window.innerHeight - detailsPanelHeight - pad;
2315
- detailsPanelY = Math.min(
2316
- maxY,
2317
- Math.max(detailsPanelTopOffset, shapeRect.top),
2318
- );
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;
2319
2479
  console.log('[WidgetDetails] Positioned beside shape', {
2320
- openOnRight,
2321
2480
  x: Math.round(detailsPanelX),
2322
2481
  y: Math.round(detailsPanelY),
2323
2482
  w: detailsPanelWidth,
2324
2483
  h: detailsPanelHeight,
2484
+ shapeLeft: Math.round(shapeRect.left),
2325
2485
  });
2326
2486
  }
2327
2487
 
@@ -2518,14 +2678,16 @@
2518
2678
 
2519
2679
  function clampDetailsPanelPosition(x: number, y: number): { x: number; y: number } {
2520
2680
  if (typeof window === 'undefined') return { x, y };
2521
- const pad = 8;
2522
2681
  const panelH = detailsPanelHeight || getDefaultDetailsPanelHeight();
2523
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);
2524
2685
  const minTop = detailsPanelTopOffset;
2525
- const maxTop = Math.max(minTop, window.innerHeight - Math.min(panelH, window.innerHeight - pad) - pad);
2526
- 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);
2527
2689
  return {
2528
- x: Math.min(maxLeft, Math.max(pad, x)),
2690
+ x: Math.min(maxX, Math.max(minX, x)),
2529
2691
  y: Math.min(maxTop, Math.max(minTop, y)),
2530
2692
  };
2531
2693
  }
@@ -2962,16 +3124,28 @@
2962
3124
  {#if selectedWidgetId}
2963
3125
  <!-- svelte-ignore a11y-no-static-element-interactions -->
2964
3126
  <div
2965
- class="widget-details-floating-panel pointer-events-auto fixed
2966
- {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
2967
3143
  {fillViewport
2968
3144
  ? 'bg-white rounded-none border-x border-b border-t-0 ring-0 shadow-none overflow-hidden'
2969
- : 'bg-white/95 backdrop-blur-sm shadow-2xl rounded-xl border overflow-visible'}
3145
+ : 'bg-white/95 backdrop-blur-sm shadow-2xl rounded-xl border overflow-hidden'}
2970
3146
  {isFocusedPanel
2971
3147
  ? (fillViewport ? 'border-blue-400' : 'border-blue-400 ring-2 ring-blue-200/80')
2972
3148
  : 'border-gray-300 opacity-[0.97]'}"
2973
- style="left: {detailsPanelX}px; top: {detailsPanelY}px; width: {detailsPanelWidth}px; z-index: {panelZIndex};
2974
- height: {detailsPanelHeight ? detailsPanelHeight + 'px' : `calc(100vh - ${detailsPanelTopOffset + detailsPanelBottomMargin}px)`};"
2975
3149
  onmousedown={handlePanelFocusRequest}
2976
3150
  >
2977
3151
  <!-- Widget Details Top Bar — title, steps, and close on one row -->
@@ -3058,17 +3232,6 @@
3058
3232
  </div>
3059
3233
  </div>
3060
3234
 
3061
- <button
3062
- type="button"
3063
- onclick={handleCloseButtonClick}
3064
- onmousedown={(e) => e.stopPropagation()}
3065
- onpointerdown={(e) => e.stopPropagation()}
3066
- class="widget-details-close-bt 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"
3067
- title="Close widget details"
3068
- >
3069
- ✕
3070
- </button>
3071
-
3072
3235
  <!-- Widget Details Content: Two-column layout -->
3073
3236
  <div class="widget-details-content-wrapper relative m-2.5 min-w-0 min-h-0 flex-1 overflow-x-hidden">
3074
3237
  <div class="widget-details-two-col flex h-full min-h-0 min-w-0 gap-0 overflow-x-hidden">
@@ -3104,7 +3267,7 @@
3104
3267
  Commit: <span class="font-mono font-medium text-gray-600" title={lastCommitId || ''}>{lastCommitId ? lastCommitId.substring(0, 8) : '—'}</span>
3105
3268
  </span>
3106
3269
  {#if lastPublishedVersion !== null}
3107
- <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>
3108
3271
  {/if}
3109
3272
  </div>
3110
3273
  </div>
@@ -3117,9 +3280,13 @@
3117
3280
  <div class="actions-panel widget-details-actions-panel relative flex flex-col flex-1 min-h-0">
3118
3281
 
3119
3282
  <!-- Keep all steps mounted — switching tabs must not destroy chat / PropsEditor state -->
3120
- <div class="widget-details-create-step flex flex-col flex-1 min-h-0 justify-start overflow-y-auto overflow-x-hidden scrollbar-thin gap-2 p-2 {currentStep === 'create' ? '' : 'hidden'}">
3121
- <slot name="code-generation" />
3122
- <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>
3123
3290
  </div>
3124
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'}">
3125
3292
  <slot name="composition-editor" />
@@ -3254,7 +3421,7 @@
3254
3421
  <div class="widget-preview-fallback relative flex items-center justify-center h-full rounded-lg bg-gray-50 overflow-hidden">
3255
3422
  {#if isGeneratingCode || previewLoading}
3256
3423
  <GenerateLoader
3257
- title={isGeneratingCode ? 'Generating code…' : 'Building preview…'}
3424
+ title={isGeneratingCode ? 'Generating code…' : 'Loading preview…'}
3258
3425
  subtitle=""
3259
3426
  size="md"
3260
3427
  />
@@ -3410,6 +3577,7 @@
3410
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>
3411
3578
  {/if}
3412
3579
  </div>
3580
+ </div>
3413
3581
  {/if}
3414
3582
 
3415
3583
  <style>
@@ -135,6 +135,7 @@ declare const WidgetDetails: $$__sveltets_2_IsomorphicComponent<{
135
135
  setPreviewLoadingState?: (val: boolean) => void;
136
136
  getPreviewError?: () => string | null;
137
137
  setPreviewBuildError?: (message: string | null) => void;
138
+ retryPreviewIfEmpty?: () => Promise<void>;
138
139
  showNoCodeYetState?: () => void;
139
140
  startPreviewLoadingPublic?: () => void;
140
141
  sendMessageToPreview?: (message: Record<string, unknown>) => void;
@@ -224,6 +225,7 @@ declare const WidgetDetails: $$__sveltets_2_IsomorphicComponent<{
224
225
  setPreviewLoadingState: (val: boolean) => void;
225
226
  getPreviewError: () => string | null;
226
227
  setPreviewBuildError: (message: string | null) => void;
228
+ retryPreviewIfEmpty: () => Promise<void>;
227
229
  showNoCodeYetState: () => void;
228
230
  startPreviewLoadingPublic: () => void;
229
231
  sendMessageToPreview: (message: Record<string, unknown>) => void;
@@ -20,6 +20,9 @@ export function sanitizePreviewBuildError(raw) {
20
20
  if (/failed to fetch|fetch failed/i.test(text)) {
21
21
  return 'Could not reach the preview builder. Try Rebuild.';
22
22
  }
23
+ if (/is not a function/i.test(text)) {
24
+ return 'Preview rebuild is unavailable in this session. Try Rebuild or reload the page.';
25
+ }
23
26
  if (looksLikeHtmlErrorPage(text) || /error code:\s*1042/i.test(text)) {
24
27
  return 'Preview builder is unavailable. The widget code was saved — use Rebuild to compile it.';
25
28
  }
@@ -33,7 +36,7 @@ export function isInfrastructurePreviewError(raw) {
33
36
  return false;
34
37
  if (looksLikeHtmlErrorPage(raw))
35
38
  return true;
36
- return /preview builder is unavailable|could not reach the preview builder|unexpected error page/i.test(sanitizePreviewBuildError(raw));
39
+ return /preview builder is unavailable|could not reach the preview builder|unexpected error page|is not a function|requested resource was not found|rebuild is unavailable/i.test(sanitizePreviewBuildError(raw));
37
40
  }
38
41
  export function normalizeErrorMessage(rawError) {
39
42
  let extracted = '';