@widgetic/creator 0.3.49 → 0.3.50

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.
@@ -65,8 +65,8 @@
65
65
  ondblclick={() => startEditing()}
66
66
  >
67
67
  <input
68
- class="editable-name-input font-semibold rounded px-1 py-0.5 text-sm outline-none transition-colors {isEditing ? 'border border-blue-400 bg-white focus:ring-1 focus:ring-blue-500' : 'border border-transparent bg-transparent cursor-default'}"
69
- style="min-width: 60px; flex: 1 1 auto; {!isEditing ? 'pointer-events: none;' : ''}"
68
+ class="editable-name-input min-w-0 w-full overflow-hidden text-ellipsis whitespace-nowrap font-semibold rounded px-1 py-0.5 text-sm outline-none transition-colors {isEditing ? 'border border-blue-400 bg-white focus:ring-1 focus:ring-blue-500' : 'border border-transparent bg-transparent cursor-default'}"
69
+ style="flex: 1 1 0%; {!isEditing ? 'pointer-events: none;' : ''}"
70
70
  type="text"
71
71
  value={isEditing ? inputValue : (name || placeholder)}
72
72
  readonly={!isEditing}
@@ -31,7 +31,6 @@
31
31
  import { userSession as userSessionStore } from '../stores/userSession.js';
32
32
 
33
33
  import { WidgetPreview } from '@widgetic/editor';
34
- import EditableName from './EditableName.svelte';
35
34
 
36
35
  const dispatch = createEventDispatcher();
37
36
 
@@ -272,6 +271,13 @@
272
271
 
273
272
  $: hasWidgetCode = !!lastCommitId || lastPublishedVersion !== null;
274
273
 
274
+ /** Convert UX v2: draft panels (synthetic ids) have no widget/repo yet —
275
+ * show a friendly draft hint instead of the repo-setup loader. */
276
+ $: isDraftConvertPanel = !!selectedWidgetId?.startsWith('draft_convert_');
277
+ $: if (isDraftConvertPanel && previewError === 'NO_CODE_YET') {
278
+ previewError = 'DRAFT_PANEL';
279
+ }
280
+
275
281
  /** Published widgets must never show the draft "No Code Yet" empty state. */
276
282
  $: if (hasWidgetCode && previewError === 'NO_CODE_YET') {
277
283
  previewError = lastPublishedVersion !== null
@@ -851,14 +857,71 @@
851
857
  $: detailsPanelTopOffset = panelTopOffset ?? legacyTopOffset;
852
858
  $: detailsPanelBottomMargin = panelBottomMargin ?? legacyBottomMargin;
853
859
 
854
- const DETAILS_PANEL_DEFAULT_WIDTH = 1100;
855
- const DETAILS_PANEL_MIN_WIDTH = 720;
856
- const DETAILS_PANEL_MIN_HEIGHT = 500;
860
+ const DETAILS_PANEL_DEFAULT_WIDTH = 600;
861
+ const DETAILS_PANEL_MAX_WIDTH = 600;
862
+ const DETAILS_PANEL_MAX_HEIGHT = 800;
863
+ const DETAILS_PANEL_MIN_WIDTH = 320;
864
+ const DETAILS_PANEL_MIN_HEIGHT = 400;
857
865
  const DETAILS_PANEL_FILL_INSET = 16;
866
+ const DETAILS_PANEL_SHAPE_GAP = 12;
858
867
  /** Left column share of the two-col layout (chat / PropsEditor). */
859
868
  const LEFT_COL_DEFAULT_PERCENT = 58;
860
869
  const LEFT_COL_MIN_PERCENT = 32;
861
870
  const LEFT_COL_MAX_PERCENT = 72;
871
+ /** Chat-first layout: preview column starts collapsed and opens on generate. */
872
+ let previewColumnCollapsed = true;
873
+ let lastAutoExpandForGenerating = false;
874
+ let widthBeforePreviewExpand = 0;
875
+ $: if (isGeneratingCode && !lastAutoExpandForGenerating) {
876
+ lastAutoExpandForGenerating = true;
877
+ if (previewColumnCollapsed) expandPreviewColumn();
878
+ }
879
+ $: if (!isGeneratingCode) {
880
+ lastAutoExpandForGenerating = false;
881
+ }
882
+ /** Open the live preview column once when this widget already has code (or a loaded iframe). */
883
+ $: if (
884
+ showWidgetDetails
885
+ && !isDraftConvertPanel
886
+ && previewColumnCollapsed
887
+ && !hasAutoOpenedPreview
888
+ && (hasWidgetCode || !!iframeSrc)
889
+ ) {
890
+ hasAutoOpenedPreview = true;
891
+ expandPreviewColumn();
892
+ }
893
+
894
+ function collapsePreviewColumn() {
895
+ previewColumnCollapsed = true;
896
+ if (widthBeforePreviewExpand > 0) {
897
+ detailsPanelWidth = widthBeforePreviewExpand;
898
+ widthBeforePreviewExpand = 0;
899
+ leftColPercent = LEFT_COL_DEFAULT_PERCENT;
900
+ }
901
+ console.log('[WidgetDetails] Preview column minimized, restored width', detailsPanelWidth);
902
+ }
903
+
904
+ function expandPreviewColumn() {
905
+ if (!previewColumnCollapsed) return;
906
+ widthBeforePreviewExpand = detailsPanelWidth;
907
+ previewColumnCollapsed = false;
908
+ const extraWidth = 420;
909
+ const maxRight = typeof window !== 'undefined' ? window.innerWidth - 8 : detailsPanelWidth;
910
+ const expandedWidth = widthBeforePreviewExpand + extraWidth;
911
+ const fittedWidth = Math.min(expandedWidth, Math.max(widthBeforePreviewExpand, maxRight - detailsPanelX));
912
+ detailsPanelWidth = Math.max(widthBeforePreviewExpand, fittedWidth);
913
+ 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
+ );
918
+ console.log('[WidgetDetails] Preview column expanded', {
919
+ from: widthBeforePreviewExpand,
920
+ to: detailsPanelWidth,
921
+ leftColPercent,
922
+ });
923
+ tick().then(() => requestAnimationFrame(() => fitPreviewToArea()));
924
+ }
862
925
 
863
926
  let detailsPanelX = -1;
864
927
  let detailsPanelY = -1;
@@ -959,6 +1022,12 @@
959
1022
 
960
1023
  /** Probe Worker / build-from-repo pipeline without resetting panel state. */
961
1024
  async function runPreviewLoadPipeline(widgetId: string): Promise<void> {
1025
+ if (iframeSrc && previewLoadedForWidgetId === widgetId) {
1026
+ previewBuildDeferred = false;
1027
+ previewLoading = false;
1028
+ console.log('[WidgetDetails] Preview already loaded, skipping pipeline:', widgetId.substring(0, 8));
1029
+ return;
1030
+ }
962
1031
  if (shouldDeferPreviewNetworkWork()) {
963
1032
  console.log('[WidgetDetails] Deferring preview load (background panel):', widgetId.substring(0, 8));
964
1033
  previewBuildDeferred = true;
@@ -1026,6 +1095,11 @@
1026
1095
  /** Resume preview after a background panel receives focus. */
1027
1096
  export async function resumeDeferredPreviewLoad(): Promise<void> {
1028
1097
  if (!previewBuildDeferred || !selectedWidgetId) return;
1098
+ if (iframeSrc && previewLoadedForWidgetId === selectedWidgetId) {
1099
+ previewBuildDeferred = false;
1100
+ console.log('[WidgetDetails] Skip deferred preview — already loaded for', selectedWidgetId.substring(0, 8));
1101
+ return;
1102
+ }
1029
1103
  previewBuildDeferred = false;
1030
1104
  await runPreviewLoadPipeline(selectedWidgetId);
1031
1105
  }
@@ -1107,10 +1181,11 @@
1107
1181
  return;
1108
1182
  }
1109
1183
 
1110
- // Brand-new widget (no commit, not published): skip Worker HEAD / build-from-repo.
1111
- // Probing localhost:5173/__widget_builder__/preview/... only produces expected 404 noise.
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).
1112
1187
  const hasRepo = !!(normalizeRepositoryId(selectedWidget) || currentRepositoryId);
1113
- if (!lastCommitId && lastPublishedVersion === null) {
1188
+ if (!lastCommitId && lastPublishedVersion === null && !hasRepo) {
1114
1189
  console.log('[WidgetDetails] No code yet — skipping Worker probe/build-from-repo', {
1115
1190
  widgetId: widgetId?.substring(0, 8),
1116
1191
  hasRepo,
@@ -2043,20 +2118,30 @@
2043
2118
  // ═══════════════════════════════════════════════════════════════════════
2044
2119
 
2045
2120
  function getDefaultDetailsPanelHeight(): number {
2046
- if (typeof window === 'undefined') return 600;
2047
- return window.innerHeight - detailsPanelTopOffset - detailsPanelBottomMargin;
2121
+ if (typeof window === 'undefined') return DETAILS_PANEL_MAX_HEIGHT;
2122
+ const available = window.innerHeight - detailsPanelTopOffset - detailsPanelBottomMargin;
2123
+ return Math.max(DETAILS_PANEL_MIN_HEIGHT, Math.min(DETAILS_PANEL_MAX_HEIGHT, available));
2048
2124
  }
2049
2125
 
2050
2126
  function clampPanelWidth(w: number): number {
2051
- if (typeof window === 'undefined') return w;
2052
- const maxWidth = window.innerWidth - 40;
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);
2053
2129
  return Math.max(DETAILS_PANEL_MIN_WIDTH, Math.min(w, maxWidth));
2054
2130
  }
2055
2131
 
2132
+ function clampPanelHeight(h: number): number {
2133
+ if (typeof window === 'undefined') return Math.min(DETAILS_PANEL_MAX_HEIGHT, h);
2134
+ const available = window.innerHeight - detailsPanelTopOffset - 16;
2135
+ return Math.max(
2136
+ DETAILS_PANEL_MIN_HEIGHT,
2137
+ Math.min(h, Math.min(DETAILS_PANEL_MAX_HEIGHT, available)),
2138
+ );
2139
+ }
2140
+
2056
2141
  function resetDetailsPanelSize() {
2057
2142
  if (typeof window === 'undefined') {
2058
2143
  detailsPanelWidth = DETAILS_PANEL_DEFAULT_WIDTH;
2059
- detailsPanelHeight = 0;
2144
+ detailsPanelHeight = DETAILS_PANEL_MAX_HEIGHT;
2060
2145
  return;
2061
2146
  }
2062
2147
  if (fillViewport) {
@@ -2071,7 +2156,7 @@
2071
2156
  return;
2072
2157
  }
2073
2158
  detailsPanelWidth = clampPanelWidth(DETAILS_PANEL_DEFAULT_WIDTH);
2074
- detailsPanelHeight = 0;
2159
+ detailsPanelHeight = getDefaultDetailsPanelHeight();
2075
2160
  }
2076
2161
 
2077
2162
  export function centerDetailsPanel() {
@@ -2083,10 +2168,93 @@
2083
2168
  return;
2084
2169
  }
2085
2170
  const cascade = panelCascadeIndex * 32;
2086
- detailsPanelX = Math.max(0, Math.round((window.innerWidth - detailsPanelWidth) / 2) + cascade);
2171
+ detailsPanelX = Math.max(8, Math.round((window.innerWidth - detailsPanelWidth) / 2) + cascade);
2087
2172
  detailsPanelY = detailsPanelTopOffset + cascade;
2088
2173
  }
2089
2174
 
2175
+ /** Keep the panel on-screen after a browser resize. Does not reset to default 600×800. */
2176
+ export function keepPanelInViewport() {
2177
+ if (typeof window === 'undefined') return;
2178
+ if (fillViewport) {
2179
+ resetDetailsPanelSize();
2180
+ detailsPanelX = DETAILS_PANEL_FILL_INSET;
2181
+ detailsPanelY = detailsPanelTopOffset;
2182
+ return;
2183
+ }
2184
+ const maxW = Math.max(DETAILS_PANEL_MIN_WIDTH, window.innerWidth - 16);
2185
+ const maxH = Math.max(
2186
+ DETAILS_PANEL_MIN_HEIGHT,
2187
+ window.innerHeight - detailsPanelTopOffset - 16,
2188
+ );
2189
+ if (detailsPanelWidth > maxW) detailsPanelWidth = maxW;
2190
+ const currentH = detailsPanelHeight || getDefaultDetailsPanelHeight();
2191
+ if (currentH > maxH) detailsPanelHeight = maxH;
2192
+ const pos = clampDetailsPanelPosition(detailsPanelX, detailsPanelY);
2193
+ detailsPanelX = pos.x;
2194
+ detailsPanelY = pos.y;
2195
+ }
2196
+
2197
+ /**
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).
2200
+ */
2201
+ export function positionBesideShape(shapeRect: {
2202
+ left: number;
2203
+ top: number;
2204
+ width: number;
2205
+ height: number;
2206
+ viewportLeft: number;
2207
+ viewportTop: number;
2208
+ viewportWidth: number;
2209
+ viewportHeight: number;
2210
+ } | null): void {
2211
+ if (typeof window === 'undefined') return;
2212
+ resetDetailsPanelSize();
2213
+ _hasBeenOpenedOnce = true;
2214
+ if (!shapeRect) {
2215
+ centerDetailsPanel();
2216
+ console.log('[WidgetDetails] No shape rect — centered floating panel', detailsPanelWidth, detailsPanelHeight);
2217
+ return;
2218
+ }
2219
+
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
+ }
2243
+
2244
+ const maxY = window.innerHeight - detailsPanelHeight - pad;
2245
+ detailsPanelY = Math.min(
2246
+ maxY,
2247
+ Math.max(detailsPanelTopOffset, shapeRect.top),
2248
+ );
2249
+ console.log('[WidgetDetails] Positioned beside shape', {
2250
+ openOnRight,
2251
+ x: Math.round(detailsPanelX),
2252
+ y: Math.round(detailsPanelY),
2253
+ w: detailsPanelWidth,
2254
+ h: detailsPanelHeight,
2255
+ });
2256
+ }
2257
+
2090
2258
  function startColumnResize(e: MouseEvent) {
2091
2259
  e.preventDefault();
2092
2260
  e.stopPropagation();
@@ -2134,7 +2302,7 @@
2134
2302
  export function openWidgetDetails() {
2135
2303
  if (showWidgetDetails) return;
2136
2304
  resetDetailsPanelSize();
2137
- if (fillViewport || !_hasBeenOpenedOnce || detailsPanelX < 0 || detailsPanelY < 0) {
2305
+ if (fillViewport || (!_hasBeenOpenedOnce && (detailsPanelX < 0 || detailsPanelY < 0))) {
2138
2306
  centerDetailsPanel();
2139
2307
  _hasBeenOpenedOnce = true;
2140
2308
  }
@@ -2152,7 +2320,6 @@
2152
2320
  if (selectedWidgetId) {
2153
2321
  if (iframeSrc && previewLoadedForWidgetId === selectedWidgetId) {
2154
2322
  console.log('[WidgetDetails] Reopened with existing preview, no reload needed');
2155
- dispatch('buildFromRepoSuccess', { widgetId: selectedWidgetId, source: 'cached-preview' });
2156
2323
  return;
2157
2324
  }
2158
2325
  loadWidget(selectedWidgetId);
@@ -2162,6 +2329,10 @@
2162
2329
  export function closeWidgetDetails(options?: { silent?: boolean }) {
2163
2330
  showWidgetDetails = false;
2164
2331
  showPreviewModal = false;
2332
+ // Next open should auto-expand preview again if the widget already has code.
2333
+ hasAutoOpenedPreview = false;
2334
+ previewColumnCollapsed = true;
2335
+ widthBeforePreviewExpand = 0;
2165
2336
  if (!options?.silent) {
2166
2337
  dispatch('widgetDetailsToggle', { open: false, showWidgetDetails: false });
2167
2338
  }
@@ -2691,19 +2862,18 @@
2691
2862
  <!-- Widget Details Top Bar — title, steps, and close on one row -->
2692
2863
  <div class="widget-details-header-ct flex flex-col shrink-0 bg-gray-100 border-b border-gray-200">
2693
2864
  <div
2694
- class="widget-details-drag-bar relative flex items-center gap-2 px-3 py-1.5 cursor-grab active:cursor-grabbing select-none min-h-[40px] touch-none"
2865
+ 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"
2695
2866
  onpointerdown={startDetailsDrag}
2696
2867
  >
2697
- <h3 class="widget-details-title text-sm font-semibold text-gray-700 truncate min-w-0 max-w-[28%] shrink" title={selectedWidget?.name || 'Widget Details'}>
2698
- {selectedWidget?.name || 'Widget Details'}
2699
- {#if !isFocusedPanel}
2700
- <span class="text-gray-400 font-normal"> · bg</span>
2701
- {/if}
2702
- </h3>
2703
-
2704
- <!-- Step Indicator (inline same row as title) -->
2705
- <div class="widget-details-step-indicator-row flex flex-1 justify-center min-w-0 px-1">
2706
- <div class="widget-details-step-indicator flex items-center gap-1 flex-wrap justify-center">
2868
+ <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"
2870
+ title={selectedWidget?.name || ''}
2871
+ >
2872
+ {selectedWidget?.name || (isDraftConvertPanel ? 'Widget Draft' : 'Widget')}
2873
+ </span>
2874
+ <!-- Step Indicator — centered; name is compact on the left (read-only, no rename) -->
2875
+ <div class="widget-details-step-indicator-row pointer-events-none absolute left-1/2 -translate-x-1/2 flex justify-center px-1">
2876
+ <div class="widget-details-step-indicator pointer-events-auto flex items-center gap-1 flex-nowrap justify-center whitespace-nowrap">
2707
2877
  <!-- svelte-ignore a11y_consider_explicit_label -->
2708
2878
  <button
2709
2879
  type="button"
@@ -2713,7 +2883,7 @@
2713
2883
  ? 'bg-blue-100 text-blue-700 border border-blue-300 cursor-pointer'
2714
2884
  : 'text-gray-600 hover:bg-gray-200 border border-transparent cursor-pointer'}"
2715
2885
  >
2716
- <span class="step-number inline-flex items-center justify-center w-5 h-5 rounded-full text-[11px] font-bold {currentStep === 'create' ? 'bg-blue-600 text-white' : 'bg-gray-300 text-gray-700'}">1</span>
2886
+ <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>
2717
2887
  <span class="step-label">Create</span>
2718
2888
  </button>
2719
2889
  <div class="step-connector w-4 h-px {canEditStep ? 'bg-gray-400' : 'bg-gray-200'}"></div>
@@ -2731,7 +2901,7 @@
2731
2901
  ? 'text-gray-600 hover:bg-gray-200 border border-transparent cursor-pointer'
2732
2902
  : 'text-gray-300 border border-transparent cursor-default pointer-events-none'}"
2733
2903
  >
2734
- <span class="step-number inline-flex items-center justify-center w-5 h-5 rounded-full text-[11px] font-bold {currentStep === 'edit' ? 'bg-blue-600 text-white' : canEditStep ? 'bg-gray-300 text-gray-700' : 'bg-gray-200 text-gray-400'}">2</span>
2904
+ <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>
2735
2905
  <span class="step-label">Edit</span>
2736
2906
  </button>
2737
2907
  <div class="step-connector w-4 h-px {canEmbedStep ? 'bg-gray-400' : 'bg-gray-200'}"></div>
@@ -2749,13 +2919,26 @@
2749
2919
  ? 'text-gray-600 hover:bg-gray-200 border border-transparent cursor-pointer'
2750
2920
  : 'text-gray-300 border border-transparent cursor-default pointer-events-none'}"
2751
2921
  >
2752
- <span class="step-number inline-flex items-center justify-center w-5 h-5 rounded-full text-[11px] font-bold {currentStep === 'embed' ? 'bg-blue-600 text-white' : canEmbedStep ? 'bg-gray-300 text-gray-700' : 'bg-gray-200 text-gray-400'}">3</span>
2922
+ <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>
2753
2923
  <span class="step-label">Embed</span>
2754
2924
  </button>
2755
2925
  </div>
2756
2926
  </div>
2757
2927
 
2758
2928
  <div class="widget-details-debug-buttons flex items-center gap-1.5 shrink-0">
2929
+ <button
2930
+ type="button"
2931
+ onclick={() => previewColumnCollapsed ? expandPreviewColumn() : collapsePreviewColumn()}
2932
+ onmousedown={(e) => e.stopPropagation()}
2933
+ onpointerdown={(e) => e.stopPropagation()}
2934
+ 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
2935
+ {previewColumnCollapsed
2936
+ ? 'bg-white border border-gray-300 text-gray-600 hover:bg-gray-100'
2937
+ : 'bg-blue-50 border border-blue-300 text-blue-700 hover:bg-blue-100'}"
2938
+ title={previewColumnCollapsed ? 'Open live preview column' : 'Close live preview column'}
2939
+ >
2940
+ {previewColumnCollapsed ? 'Open Preview' : 'Close Preview'}
2941
+ </button>
2759
2942
  <button
2760
2943
  type="button"
2761
2944
  onclick={handleCloseButtonClick}
@@ -2777,7 +2960,7 @@
2777
2960
  <!-- ═══ LEFT COLUMN: Step Content (scrollable) ═══ -->
2778
2961
  <div
2779
2962
  class="widget-details-left-col flex flex-col min-w-0 min-h-0 relative"
2780
- style="width: {leftColPercent}%; flex: 0 0 {leftColPercent}%;"
2963
+ style="width: {previewColumnCollapsed ? 100 : leftColPercent}%; flex: 0 0 {previewColumnCollapsed ? 100 : leftColPercent}%;"
2781
2964
  >
2782
2965
  <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>
2783
2966
 
@@ -2785,16 +2968,13 @@
2785
2968
  <!-- Sticky Widget Info Bar (always visible above scrollable content) -->
2786
2969
  <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">
2787
2970
  <div class="widget-info-name flex items-center min-w-0 gap-1 text-sm text-gray-700">
2788
- <EditableName
2789
- name={selectedWidget?.name || ''}
2790
- placeholder="Unnamed"
2791
- tooltipText="Double-click to rename"
2792
- class="flex-1 min-w-0"
2793
- on:rename={({ detail }) => dispatch('widgetRename', { widgetId: selectedWidgetId, newName: detail.newName })}
2794
- />
2795
2971
  <!-- svelte-ignore a11y-click-events-have-key-events -->
2796
2972
  <!-- svelte-ignore a11y-no-static-element-interactions -->
2797
- <span class="ml-1 text-[10px] font-mono text-gray-400 cursor-pointer hover:text-gray-600 shrink-0" title="Click to copy widget ID" onclick={() => { navigator.clipboard.writeText(selectedWidgetId); showToast('success', 'Widget ID copied', { duration: 2000 }); }}>(id: {selectedWidgetId.substring(0, 8)}…)</span>
2973
+ <span
2974
+ class="widget-id-label text-[11px] font-mono text-gray-500 cursor-pointer hover:text-gray-700 truncate"
2975
+ title="Click to copy widget ID"
2976
+ onclick={() => { navigator.clipboard.writeText(selectedWidgetId); showToast('success', 'Widget ID copied', { duration: 2000 }); }}
2977
+ >Widget id: {selectedWidgetId}</span>
2798
2978
  </div>
2799
2979
  <div class="widget-info-meta flex items-center gap-2 shrink-0">
2800
2980
  {#if saveStatus === 'saving'}
@@ -2815,13 +2995,13 @@
2815
2995
 
2816
2996
  <!-- Scrollable step content -->
2817
2997
  <div
2818
- class="widget-details-left-content flex flex-col flex-1 min-h-0 relative rounded-b-lg {currentStep === 'edit' ? 'overflow-hidden' : 'overflow-y-auto'}"
2998
+ class="widget-details-left-content flex flex-col flex-1 min-h-0 relative rounded-b-lg {currentStep === 'edit' ? 'overflow-hidden' : 'overflow-hidden'}"
2819
2999
  bind:this={contentContainerEl}
2820
3000
  >
2821
3001
  <div class="actions-panel widget-details-actions-panel relative flex flex-col flex-1 min-h-0">
2822
3002
 
2823
3003
  <!-- Keep all steps mounted — switching tabs must not destroy chat / PropsEditor state -->
2824
- <div class="widget-details-create-step flex flex-col gap-2 p-2 {currentStep === 'create' ? '' : 'hidden'}">
3004
+ <div class="widget-details-create-step flex flex-col flex-1 min-h-0 overflow-hidden gap-2 p-2 {currentStep === 'create' ? '' : 'hidden'}">
2825
3005
  <slot name="code-generation" />
2826
3006
  <slot name="publish-widget" />
2827
3007
  </div>
@@ -2838,6 +3018,18 @@
2838
3018
 
2839
3019
  </div> <!-- end of widget-details-left-col -->
2840
3020
 
3021
+ {#if previewColumnCollapsed}
3022
+ <div class="widget-details-preview-expand-strip flex items-center justify-center w-8 shrink-0 border-l border-gray-200 bg-gray-50">
3023
+ <button
3024
+ type="button"
3025
+ onclick={expandPreviewColumn}
3026
+ class="widget-details-preview-expand-bt h-full w-full text-[10px] font-semibold text-gray-500 hover:text-forest-green hover:bg-gray-100 cursor-pointer"
3027
+ title="Show live preview"
3028
+ >
3029
+ <span class="widget-details-preview-expand-label inline-block rotate-180" style="writing-mode: vertical-rl;">Preview</span>
3030
+ </button>
3031
+ </div>
3032
+ {:else}
2841
3033
  <!-- svelte-ignore a11y-no-static-element-interactions -->
2842
3034
  <div
2843
3035
  class="widget-details-col-resizer w-2.5 shrink-0 cursor-col-resize relative z-20 group"
@@ -2949,7 +3141,14 @@
2949
3141
  title={isGeneratingCode ? 'Generating code…' : 'Building preview…'}
2950
3142
  size="md"
2951
3143
  />
2952
- {:else if previewError === 'NO_CODE_YET' && !hasWidgetCode}
3144
+ {:else if previewError === 'DRAFT_PANEL'}
3145
+ <div class="widget-preview-draft-hint flex flex-col items-center justify-center text-center p-4">
3146
+ <div class="text-3xl mb-2">🎨</div>
3147
+ <div class="font-medium text-sm text-gray-700 mb-1">Sketch draft</div>
3148
+ <div class="text-xs max-w-[240px] text-gray-500 mb-1">Your sketch is still on the canvas — keep editing it freely.</div>
3149
+ <div class="text-xs max-w-[240px] text-gray-500">Send the prompt in chat to create the widget and generate code.</div>
3150
+ </div>
3151
+ {:else if previewError === 'NO_CODE_YET' && !hasWidgetCode}
2953
3152
  <div class="widget-preview-no-code flex flex-col items-center justify-center text-center p-4">
2954
3153
  {#if !normalizeRepositoryId(selectedWidget) && repositorySetupStatus !== 'failed'}
2955
3154
  <GenerateLoader
@@ -3031,6 +3230,7 @@
3031
3230
 
3032
3231
  </div> <!-- end of widget-details-right-content -->
3033
3232
  </div> <!-- end of widget-details-right-col -->
3233
+ {/if}
3034
3234
 
3035
3235
  </div> <!-- end of widget-details-two-col -->
3036
3236
 
@@ -3067,7 +3267,7 @@
3067
3267
 
3068
3268
  <style>
3069
3269
  .widget-details-left-content {
3070
- overflow-y: auto;
3270
+ overflow: hidden;
3071
3271
  overflow-x: hidden;
3072
3272
  scrollbar-width: thin;
3073
3273
  scrollbar-color: rgba(0, 0, 0, 0.15) transparent;
@@ -3124,4 +3324,17 @@
3124
3324
  :global(.widget-details-right-content .widget-preview-wrapper) {
3125
3325
  overflow: visible !important;
3126
3326
  }
3327
+
3328
+ .step-number {
3329
+ line-height: 1;
3330
+ }
3331
+
3332
+ /* Tab focus: hide DS/site orange ring inside Widget Details (buttons + links). */
3333
+ .widget-details-floating-panel :global(button:focus),
3334
+ .widget-details-floating-panel :global(button:focus-visible),
3335
+ .widget-details-floating-panel :global(a:focus),
3336
+ .widget-details-floating-panel :global(a:focus-visible) {
3337
+ outline: none !important;
3338
+ box-shadow: none !important;
3339
+ }
3127
3340
  </style>
@@ -147,12 +147,22 @@ declare const WidgetDetails: $$__sveltets_2_IsomorphicComponent<{
147
147
  startTokenAutoRefreshPublic?: () => void;
148
148
  fetchDirectPreviewUrlPublic?: () => Promise<string | null>;
149
149
  centerDetailsPanel?: () => void;
150
+ keepPanelInViewport?: () => void;
151
+ positionBesideShape?: (shapeRect: {
152
+ left: number;
153
+ top: number;
154
+ width: number;
155
+ height: number;
156
+ viewportLeft: number;
157
+ viewportTop: number;
158
+ viewportWidth: number;
159
+ viewportHeight: number;
160
+ } | null) => void;
150
161
  openWidgetDetails?: () => void;
151
162
  closeWidgetDetails?: (options?: {
152
163
  silent?: boolean;
153
164
  }) => void;
154
165
  }, {
155
- widgetRename: CustomEvent<any>;
156
166
  fixErrors: CustomEvent<any>;
157
167
  retryRepository: CustomEvent<any>;
158
168
  generateNow: CustomEvent<any>;
@@ -226,6 +236,17 @@ declare const WidgetDetails: $$__sveltets_2_IsomorphicComponent<{
226
236
  startTokenAutoRefreshPublic: () => void;
227
237
  fetchDirectPreviewUrlPublic: () => Promise<string | null>;
228
238
  centerDetailsPanel: () => void;
239
+ keepPanelInViewport: () => void;
240
+ positionBesideShape: (shapeRect: {
241
+ left: number;
242
+ top: number;
243
+ width: number;
244
+ height: number;
245
+ viewportLeft: number;
246
+ viewportTop: number;
247
+ viewportWidth: number;
248
+ viewportHeight: number;
249
+ } | null) => void;
229
250
  openWidgetDetails: () => void;
230
251
  closeWidgetDetails: (options?: {
231
252
  silent?: boolean;
@@ -19,6 +19,10 @@ export function streamOperationUpdates(operationId, options) {
19
19
  let abortController = null;
20
20
  let reader = null;
21
21
  const cleanup = () => {
22
+ if (watchdogTimer) {
23
+ clearTimeout(watchdogTimer);
24
+ watchdogTimer = null;
25
+ }
22
26
  try {
23
27
  reader?.cancel();
24
28
  }
@@ -42,6 +46,18 @@ export function streamOperationUpdates(operationId, options) {
42
46
  else
43
47
  reject(value);
44
48
  };
49
+ // Watchdog: if no frame arrives for a while (proxy dropped the stream,
50
+ // server stalled), give up on SSE and let the caller fall back to
51
+ // polling GET /operations/:id — the terminal event then always lands.
52
+ const WATCHDOG_TIMEOUT_MS = 45000;
53
+ let watchdogTimer = null;
54
+ const armWatchdog = () => {
55
+ if (watchdogTimer)
56
+ clearTimeout(watchdogTimer);
57
+ watchdogTimer = setTimeout(() => {
58
+ settle('reject', new Error('Operation stream idle — falling back to polling'));
59
+ }, WATCHDOG_TIMEOUT_MS);
60
+ };
45
61
  const run = async () => {
46
62
  abortController = new AbortController();
47
63
  // Propagate caller aborts into the fetch request.
@@ -68,12 +84,15 @@ export function streamOperationUpdates(operationId, options) {
68
84
  reader = response.body.getReader();
69
85
  const decoder = new TextDecoder();
70
86
  let buffer = '';
87
+ armWatchdog();
71
88
  while (true) {
72
89
  const { done, value } = await reader.read();
73
90
  if (done)
74
91
  break;
92
+ armWatchdog();
75
93
  buffer += decoder.decode(value, { stream: true });
76
- // SSE frames are `data: {...}\n\n`
94
+ // SSE frames are `data: {...}\n\n`; keepalive comment frames carry
95
+ // no data line and are skipped.
77
96
  let frameEnd = buffer.indexOf('\n\n');
78
97
  while (frameEnd !== -1) {
79
98
  const frame = buffer.slice(0, frameEnd);
package/package.json CHANGED
@@ -1,17 +1,17 @@
1
1
  {
2
2
  "name": "@widgetic/creator",
3
- "version": "0.3.49",
3
+ "version": "0.3.50",
4
4
  "type": "module",
5
5
  "peerDependencies": {
6
6
  "@sveltejs/kit": "^2.20.0",
7
7
  "svelte": "^5.25.0",
8
- "@widgetic/api-sdk": "*",
9
- "@widgetic/canvas": "^0.5.3",
10
- "@widgetic/cache-layer": "^0.1.1",
11
- "@widgetic/chat": "^0.1.3",
12
- "@widgetic/design-system": ">=0.3.0",
13
- "@widgetic/editor": "^4.0.0",
14
- "@widgetic/file-browser": "^0.5.16"
8
+ "@widgetic/api-sdk": "^1.0.10",
9
+ "@widgetic/canvas": "^0.5.4",
10
+ "@widgetic/cache-layer": "^0.1.2",
11
+ "@widgetic/chat": "^0.1.4",
12
+ "@widgetic/design-system": "^0.5.8",
13
+ "@widgetic/editor": "^4.0.1",
14
+ "@widgetic/file-browser": "^0.5.17"
15
15
  },
16
16
  "svelte": "./dist/index.js",
17
17
  "module": "./dist/index.js",
@@ -89,13 +89,13 @@
89
89
  "vite": "^6.2.6",
90
90
  "vite-imagetools": "^7.0.4",
91
91
  "vitest": "^2.0.0",
92
- "@widgetic/api-sdk": "*",
93
- "@widgetic/canvas": "^0.5.3",
94
- "@widgetic/cache-layer": "^0.1.1",
95
- "@widgetic/chat": "^0.1.3",
96
- "@widgetic/design-system": ">=0.3.0",
97
- "@widgetic/editor": "^4.0.0",
98
- "@widgetic/file-browser": "^0.5.16"
92
+ "@widgetic/api-sdk": "^1.0.10",
93
+ "@widgetic/canvas": "^0.5.4",
94
+ "@widgetic/cache-layer": "^0.1.2",
95
+ "@widgetic/chat": "^0.1.4",
96
+ "@widgetic/design-system": "^0.5.8",
97
+ "@widgetic/editor": "^4.0.1",
98
+ "@widgetic/file-browser": "^0.5.17"
99
99
  },
100
100
  "dependencies": {
101
101
  "@codesandbox/sandpack-react": "^2.19.9",