@widgetic/editor 4.0.2 → 4.0.4

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.
Files changed (35) hide show
  1. package/dist/components/MockWidget.svelte +1 -1
  2. package/dist/components/WidgetPreview.svelte +347 -97
  3. package/dist/components/WidgetPreview.svelte.d.ts +5 -0
  4. package/dist/components/editors/EmbedEditor.svelte +1 -1
  5. package/dist/components/editors/PropsEditor.svelte +173 -148
  6. package/dist/components/ic/BrowserInputController.svelte +14 -14
  7. package/dist/components/ic/CheckboxInputController.svelte +1 -1
  8. package/dist/components/ic/ColorPickerInputController.svelte +37 -37
  9. package/dist/components/ic/DatePickerInputController.svelte +6 -6
  10. package/dist/components/ic/DropdownInputController.svelte +1 -1
  11. package/dist/components/ic/FontInputController.svelte +136 -63
  12. package/dist/components/ic/FontInputController.svelte.d.ts +3 -1
  13. package/dist/components/ic/NumberInputController.svelte +37 -37
  14. package/dist/components/ic/RadioGroupInputController.svelte +1 -1
  15. package/dist/components/ic/SliderRangeInputController.svelte +1 -1
  16. package/dist/components/ic/SwitchInputController.svelte +1 -1
  17. package/dist/components/ic/TextAreaInputController.svelte +23 -23
  18. package/dist/components/ic/TextAreaPreviewInputController.svelte +2 -2
  19. package/dist/components/ic/VectorInputController.svelte +34 -34
  20. package/dist/components/ic/shared/CategoryTabs.svelte +1 -0
  21. package/dist/components/ic/shared/RecentItemsSection.svelte +21 -6
  22. package/dist/components/ic/shared/RecentItemsSection.svelte.d.ts +1 -0
  23. package/dist/stores/userSession.d.ts +1 -1
  24. package/dist/stores/userSession.js +17 -2
  25. package/dist/utils/FontManager.d.ts +1 -1
  26. package/dist/utils/FontManager.js +75 -102
  27. package/dist/utils/VectorManager.d.ts +3 -1
  28. package/dist/utils/VectorManager.js +30 -70
  29. package/dist/utils/colorTransparency.js +3 -3
  30. package/dist/utils/fontUtils.js +1 -1
  31. package/dist/utils/passiveEvents.d.ts +5 -0
  32. package/dist/utils/passiveEvents.js +9 -18
  33. package/dist/utils/recentColors.js +15 -15
  34. package/dist/utils/vectorUtils.js +1 -1
  35. package/package.json +1 -1
@@ -30,7 +30,7 @@
30
30
  };
31
31
 
32
32
  function handleVote(index: number) {
33
- console.log("Vote cast for:", pollData.options[index].label);
33
+ // console.log("Vote cast for:", pollData.options[index].label);
34
34
  // In a real implementation, this would call an API
35
35
  }
36
36
  </script>
@@ -59,6 +59,12 @@
59
59
  export let mobileViewportWidth: number = 390;
60
60
  export let mobileViewportHeight: number = 667;
61
61
  export let enableResize: boolean = false;
62
+ /** Drag only via the preview chrome header (keeps iframe interactive). */
63
+ export let enableDrag: boolean = false;
64
+ /** W×H number inputs in the chrome header, live-synced with resize. */
65
+ export let showDimensionInputs: boolean = false;
66
+ /** Initial / reset placement inside the stage. */
67
+ export let defaultStageAlign: 'center' | 'right-half' = 'center';
62
68
  /** Observe stage size and fill it (desktop modal) */
63
69
  export let autoFitStage: boolean = false;
64
70
  /** No padding/border on preview-content-container — matches widget-creator Edit panel */
@@ -87,32 +93,163 @@
87
93
  const MIN_WIDTH = 120;
88
94
  const MIN_HEIGHT = 80;
89
95
  const HANDLE_MARGIN = 6;
90
- /** Inset padding so resize handles (-4px) and dimension badge stay inside bounds */
91
- const RESIZE_PAD_X = 8;
92
- const RESIZE_PAD_TOP = 8;
93
- const RESIZE_PAD_BOTTOM = 22;
96
+ /** Inset padding so resize handles stay inside bounds */
97
+ const RESIZE_PAD_X = 12;
98
+ const RESIZE_PAD_TOP = 12;
99
+ const RESIZE_PAD_BOTTOM = 12;
100
+ const PREVIEW_CHROME_HEADER_H = 36;
101
+ const PREVIEW_STROKE = '#3b82f6';
102
+ const PREVIEW_STROKE_ALT = '#f97316';
94
103
  let isResizing = false;
104
+ let isDraggingPreview = false;
105
+ let isSymmetricalResize = false;
106
+ let previewShellLeft = 0;
107
+ let previewShellTop = 0;
108
+ let previewPositionReady = false;
109
+ let widthInputValue = '';
110
+ let heightInputValue = '';
95
111
  let contentContainerEl: HTMLElement | null = null;
96
112
  let resizeStageEl: HTMLElement | null = null;
97
113
  let stageResizeObserver: ResizeObserver | null = null;
98
114
 
115
+ $: showPreviewChrome = enableDrag || showDimensionInputs;
116
+ $: chromeHeaderHeight = showPreviewChrome ? PREVIEW_CHROME_HEADER_H : 0;
117
+ $: if (resizeWidth != null) widthInputValue = String(Math.round(resizeWidth));
118
+ $: if (resizeHeight != null) heightInputValue = String(Math.round(resizeHeight));
119
+ $: previewStrokeColor = isSymmetricalResize ? PREVIEW_STROKE_ALT : PREVIEW_STROKE;
120
+
99
121
  export function getResizeLimits(): { maxWidth: number; maxHeight: number } {
100
122
  const stageEl = resizeStageEl ?? contentContainerEl;
123
+ const chromeH = showPreviewChrome ? PREVIEW_CHROME_HEADER_H : 0;
101
124
  if (stageEl) {
102
125
  return {
103
126
  maxWidth: Math.max(MIN_WIDTH, stageEl.clientWidth),
104
- maxHeight: Math.max(MIN_HEIGHT, stageEl.clientHeight),
127
+ maxHeight: Math.max(MIN_HEIGHT, stageEl.clientHeight - chromeH),
105
128
  };
106
129
  }
107
130
  if (hasFixedBounds && boundsWidth != null && boundsHeight != null) {
108
131
  return {
109
132
  maxWidth: Math.max(MIN_WIDTH, boundsWidth - RESIZE_PAD_X * 2),
110
- maxHeight: Math.max(MIN_HEIGHT, boundsHeight - RESIZE_PAD_TOP - RESIZE_PAD_BOTTOM),
133
+ maxHeight: Math.max(MIN_HEIGHT, boundsHeight - RESIZE_PAD_TOP - RESIZE_PAD_BOTTOM - chromeH),
111
134
  };
112
135
  }
113
136
  return { maxWidth: 720, maxHeight: 420 };
114
137
  }
115
138
 
139
+ function clampDimension(value: number, min: number, max: number): number {
140
+ return Math.max(min, Math.min(max, value));
141
+ }
142
+
143
+ function computeDefaultShellPosition(
144
+ stageWidth: number,
145
+ stageHeight: number,
146
+ shellWidth: number,
147
+ shellHeight: number
148
+ ): { left: number; top: number } {
149
+ const maxLeft = Math.max(RESIZE_PAD_X, stageWidth - shellWidth - RESIZE_PAD_X);
150
+ const maxTop = Math.max(RESIZE_PAD_TOP, stageHeight - shellHeight - RESIZE_PAD_BOTTOM);
151
+ if (defaultStageAlign === 'right-half') {
152
+ const rightHalfCenterX = stageWidth * 0.75;
153
+ const left = clampDimension(rightHalfCenterX - shellWidth / 2, RESIZE_PAD_X, maxLeft);
154
+ const top = clampDimension((stageHeight - shellHeight) / 2, RESIZE_PAD_TOP, maxTop);
155
+ return { left, top };
156
+ }
157
+ const left = clampDimension((stageWidth - shellWidth) / 2, RESIZE_PAD_X, maxLeft);
158
+ const top = clampDimension((stageHeight - shellHeight) / 2, RESIZE_PAD_TOP, maxTop);
159
+ return { left, top };
160
+ }
161
+
162
+ /** Dock / reset preview shell to defaultStageAlign. */
163
+ export function resetPreviewPosition(): void {
164
+ const stageEl = resizeStageEl ?? contentContainerEl;
165
+ if (!stageEl || resizeWidth == null || resizeHeight == null) return;
166
+ if (stageEl.clientWidth < 40 || stageEl.clientHeight < 40) {
167
+ requestAnimationFrame(() => resetPreviewPosition());
168
+ return;
169
+ }
170
+ const shellWidth = resizeWidth;
171
+ const shellHeight = resizeHeight + chromeHeaderHeight;
172
+ const next = computeDefaultShellPosition(
173
+ stageEl.clientWidth,
174
+ stageEl.clientHeight,
175
+ shellWidth,
176
+ shellHeight
177
+ );
178
+ previewShellLeft = next.left;
179
+ previewShellTop = next.top;
180
+ previewPositionReady = true;
181
+ console.log('[WidgetPreview] Reset preview position', {
182
+ align: defaultStageAlign,
183
+ left: previewShellLeft,
184
+ top: previewShellTop,
185
+ });
186
+ }
187
+
188
+ function ensurePreviewPosition(): void {
189
+ if (previewPositionReady) return;
190
+ resetPreviewPosition();
191
+ }
192
+
193
+ function applyDimensionFromInput(dimension: 'width' | 'height', raw: string) {
194
+ const { maxWidth, maxHeight } = getResizeLimits();
195
+ const parsed = Number.parseInt(raw, 10);
196
+ if (!Number.isFinite(parsed)) return;
197
+ if (dimension === 'width') {
198
+ resizeWidth = clampDimension(parsed, MIN_WIDTH, maxWidth);
199
+ widthInputValue = String(Math.round(resizeWidth));
200
+ } else {
201
+ resizeHeight = clampDimension(parsed, MIN_HEIGHT, maxHeight);
202
+ heightInputValue = String(Math.round(resizeHeight));
203
+ }
204
+ if (resizeWidth != null && resizeHeight != null) {
205
+ dispatch('resize', { width: resizeWidth, height: resizeHeight });
206
+ }
207
+ console.log('[WidgetPreview] Dimension input', { dimension, value: parsed });
208
+ }
209
+
210
+ function startPreviewDrag(event: PointerEvent) {
211
+ if (!enableDrag || isResizing) return;
212
+ const target = event.target as HTMLElement | null;
213
+ if (target?.closest('input, button, a, label')) return;
214
+ event.preventDefault();
215
+ event.stopPropagation();
216
+ isDraggingPreview = true;
217
+ const startX = event.clientX;
218
+ const startY = event.clientY;
219
+ const startLeft = previewShellLeft;
220
+ const startTop = previewShellTop;
221
+ const stageEl = resizeStageEl ?? contentContainerEl;
222
+ const shellWidth = resizeWidth ?? 0;
223
+ const shellHeight = (resizeHeight ?? 0) + chromeHeaderHeight;
224
+
225
+ function onPointerMove(e: PointerEvent) {
226
+ if (!stageEl) return;
227
+ const maxLeft = Math.max(RESIZE_PAD_X, stageEl.clientWidth - shellWidth - RESIZE_PAD_X);
228
+ const maxTop = Math.max(RESIZE_PAD_TOP, stageEl.clientHeight - shellHeight - RESIZE_PAD_BOTTOM);
229
+ previewShellLeft = clampDimension(startLeft + (e.clientX - startX), RESIZE_PAD_X, maxLeft);
230
+ previewShellTop = clampDimension(startTop + (e.clientY - startY), RESIZE_PAD_TOP, maxTop);
231
+ }
232
+
233
+ function endDrag() {
234
+ isDraggingPreview = false;
235
+ document.removeEventListener('pointermove', onPointerMove);
236
+ document.removeEventListener('pointerup', endDrag);
237
+ document.removeEventListener('pointercancel', endDrag);
238
+ document.body.style.removeProperty('user-select');
239
+ document.body.style.removeProperty('cursor');
240
+ console.log('[WidgetPreview] Preview drag end', {
241
+ left: previewShellLeft,
242
+ top: previewShellTop,
243
+ });
244
+ }
245
+
246
+ document.body.style.userSelect = 'none';
247
+ document.body.style.cursor = 'move';
248
+ document.addEventListener('pointermove', onPointerMove);
249
+ document.addEventListener('pointerup', endDrag);
250
+ document.addEventListener('pointercancel', endDrag);
251
+ }
252
+
116
253
  /** Firefox/Chrome skip painting a cross-origin iframe that started at opacity 0 or inside a CSS filter until a reflow (opening DevTools). */
117
254
  let iframePaintNudgeGeneration = 0;
118
255
  export function nudgeIframePaint(): void {
@@ -205,7 +342,7 @@
205
342
  const artifactPath = jsPath.match(/\/widgets\/[^/]+\/v\d+\/.+$/i);
206
343
  if (artifactPath) {
207
344
  const proxyUrl = `${apiBaseUrl}${artifactPath[0]}`;
208
- console.log('[WidgetPreview] Using API proxy URL (R2):', proxyUrl);
345
+ // console.log('[WidgetPreview] Using API proxy URL (R2):', proxyUrl);
209
346
  return proxyUrl;
210
347
  }
211
348
  }
@@ -215,11 +352,11 @@
215
352
 
216
353
  if (widgetBuilderUrl && w.id) {
217
354
  const previewUrl = `${widgetBuilderUrl.replace(/\/+$/, '')}/preview/${w.id}`;
218
- console.log('[WidgetPreview] Using Worker preview URL (unpublished):', previewUrl);
355
+ // console.log('[WidgetPreview] Using Worker preview URL (unpublished):', previewUrl);
219
356
  return previewUrl;
220
357
  }
221
358
 
222
- console.log('[WidgetPreview] Widget not published and no Worker URL:', w.name, w.id);
359
+ // console.log('[WidgetPreview] Widget not published and no Worker URL:', w.name, w.id);
223
360
  return null;
224
361
  }
225
362
 
@@ -228,32 +365,6 @@
228
365
  return `${widgetBuilderUrl.replace(/\/+$/, '')}/preview/${w.id}`;
229
366
  }
230
367
 
231
- async function resolveWidgetPreviewUrl(w: any, primaryUrl: string): Promise<string> {
232
- try {
233
- const res = await fetch(primaryUrl, { method: 'GET', mode: 'cors' });
234
- const contentType = res.headers.get('content-type') || '';
235
- const looksLikeErrorPayload =
236
- !res.ok || (contentType.includes('application/json') && !contentType.includes('html'));
237
- if (looksLikeErrorPayload) {
238
- const fallbackUrl = tryFallbackUrl(w);
239
- if (fallbackUrl && !triedFallback) {
240
- triedFallback = true;
241
- console.log('[WidgetPreview] Primary URL failed, trying Worker fallback:', fallbackUrl);
242
- return fallbackUrl;
243
- }
244
- }
245
- } catch (err) {
246
- const fallbackUrl = tryFallbackUrl(w);
247
- if (fallbackUrl && !triedFallback) {
248
- triedFallback = true;
249
- console.log('[WidgetPreview] Primary URL unreachable, trying Worker fallback:', fallbackUrl);
250
- return fallbackUrl;
251
- }
252
- console.warn('[WidgetPreview] URL probe failed:', primaryUrl, err);
253
- }
254
- return primaryUrl;
255
- }
256
-
257
368
  function beginWidgetLoad(w: any, url: string | null) {
258
369
  pendingWidget = w;
259
370
  triedFallback = false;
@@ -262,20 +373,16 @@
262
373
  widgetReady = false;
263
374
  errorMessage = '';
264
375
  pendingMessages = [];
376
+ isResolvingUrl = false;
265
377
 
266
378
  if (!url) {
267
379
  internalIframeSrc = null;
268
- isResolvingUrl = false;
269
380
  return;
270
381
  }
271
382
 
272
- isResolvingUrl = true;
273
- console.log('[WidgetPreview] Resolving widget URL:', url);
274
- void resolveWidgetPreviewUrl(w, url).then(async (resolvedUrl) => {
275
- if (pendingWidget !== w) return;
276
- isResolvingUrl = false;
277
- internalIframeSrc = resolvedUrl;
278
- await tick();
383
+ // console.log('[WidgetPreview] Setting widget URL:', url);
384
+ internalIframeSrc = url;
385
+ void tick().then(() => {
279
386
  requestAnimationFrame(() => {
280
387
  ensureIframeLoadDetected();
281
388
  });
@@ -299,7 +406,7 @@
299
406
 
300
407
  window.setTimeout(() => {
301
408
  if (mode === 'standalone' && !iframeLoaded && !iframeError && iframeEl && effectiveSrc) {
302
- console.log('[WidgetPreview] Fallback: marking iframe loaded after ping timeout');
409
+ // console.log('[WidgetPreview] Fallback: marking iframe loaded after ping timeout');
303
410
  void handleIframeLoad();
304
411
  }
305
412
  }, 400);
@@ -353,12 +460,12 @@
353
460
  contentItems
354
461
  };
355
462
  iframeEl.contentWindow.postMessage(message, '*');
356
- console.log('[WidgetPreview] Sent property update to widget iframe', {
357
- designKeys: design ? Object.keys(design) : [],
358
- genericContentKeys: Object.keys(genericContent),
359
- contentItemsCount: contentItems?.length,
360
- iframeSrc: effectiveSrc?.substring(0, 80)
361
- });
463
+ // console.log('[WidgetPreview] Sent property update to widget iframe', {
464
+ // designKeys: design ? Object.keys(design) : [],
465
+ // genericContentKeys: Object.keys(genericContent),
466
+ // contentItemsCount: contentItems?.length,
467
+ // iframeSrc: effectiveSrc?.substring(0, 80)
468
+ // });
362
469
  } catch (err) {
363
470
  console.warn('[WidgetPreview] Failed to send postMessage:', err);
364
471
  }
@@ -406,7 +513,7 @@
406
513
  iframeLoaded = true;
407
514
  iframeError = false;
408
515
  widgetReady = true;
409
- console.log('[WidgetPreview] iframe onload:', effectiveSrc);
516
+ // console.log('[WidgetPreview] iframe onload:', effectiveSrc);
410
517
 
411
518
  if (iframeEl?.contentWindow) {
412
519
  iframeEl.contentWindow.postMessage({ type: 'widgetic:ping' }, '*');
@@ -426,6 +533,15 @@
426
533
  dispatch('iframeError', event || new Event('error'));
427
534
  return;
428
535
  }
536
+ if (!triedFallback && pendingWidget) {
537
+ const fallbackUrl = tryFallbackUrl(pendingWidget);
538
+ if (fallbackUrl && fallbackUrl !== internalIframeSrc) {
539
+ triedFallback = true;
540
+ // console.log('[WidgetPreview] Primary iframe load failed, trying Worker fallback:', fallbackUrl);
541
+ internalIframeSrc = fallbackUrl;
542
+ return;
543
+ }
544
+ }
429
545
  iframeError = true;
430
546
  iframeLoaded = false;
431
547
  widgetReady = false;
@@ -441,7 +557,7 @@
441
557
  iframeLoaded = true;
442
558
  iframeError = false;
443
559
  widgetReady = true;
444
- console.log('[WidgetPreview] Widget confirmed ready via postMessage');
560
+ // console.log('[WidgetPreview] Widget confirmed ready via postMessage');
445
561
  sendPropertiesToWidget(designValues, contentValues);
446
562
  flushPendingMessages();
447
563
  }
@@ -496,22 +612,64 @@
496
612
  event.preventDefault();
497
613
  event.stopPropagation();
498
614
  isResizing = true;
615
+ isSymmetricalResize = event.altKey;
499
616
  const startX = event.clientX;
500
617
  const startY = event.clientY;
501
-
618
+ const startLeft = previewShellLeft;
619
+ const startTop = previewShellTop;
502
620
  const { maxWidth, maxHeight } = getResizeLimits();
503
621
  const startWidth = resizeWidth ?? maxWidth;
504
622
  const startHeight = resizeHeight ?? maxHeight;
505
623
 
506
624
  function onPointerMove(e: PointerEvent) {
507
- const deltaX = (e.clientX - startX) * signX * 2;
508
- const deltaY = (e.clientY - startY) * signY * 2;
509
- resizeWidth = Math.max(MIN_WIDTH, Math.min(maxWidth, startWidth + deltaX));
510
- resizeHeight = Math.max(MIN_HEIGHT, Math.min(maxHeight, startHeight + deltaY));
625
+ const useAlt = e.altKey;
626
+ isSymmetricalResize = useAlt;
627
+ const dx = e.clientX - startX;
628
+ const dy = e.clientY - startY;
629
+
630
+ if (useAlt) {
631
+ const deltaW = signX !== 0 ? dx * signX * 2 : 0;
632
+ const deltaH = signY !== 0 ? dy * signY * 2 : 0;
633
+ const nextW = clampDimension(startWidth + deltaW, MIN_WIDTH, maxWidth);
634
+ const nextH = clampDimension(startHeight + deltaH, MIN_HEIGHT, maxHeight);
635
+ resizeWidth = nextW;
636
+ resizeHeight = nextH;
637
+ previewShellLeft = startLeft + (startWidth - nextW) / 2;
638
+ previewShellTop = startTop + (startHeight - nextH) / 2;
639
+ } else {
640
+ let nextW = startWidth;
641
+ let nextH = startHeight;
642
+ let nextLeft = startLeft;
643
+ let nextTop = startTop;
644
+
645
+ if (signX === -1) {
646
+ nextW = clampDimension(startWidth - dx, MIN_WIDTH, maxWidth);
647
+ nextLeft = startLeft + (startWidth - nextW);
648
+ } else if (signX === 1) {
649
+ nextW = clampDimension(startWidth + dx, MIN_WIDTH, maxWidth);
650
+ }
651
+
652
+ if (signY === -1) {
653
+ nextH = clampDimension(startHeight - dy, MIN_HEIGHT, maxHeight);
654
+ nextTop = startTop + (startHeight - nextH);
655
+ } else if (signY === 1) {
656
+ nextH = clampDimension(startHeight + dy, MIN_HEIGHT, maxHeight);
657
+ }
658
+
659
+ resizeWidth = nextW;
660
+ resizeHeight = nextH;
661
+ previewShellLeft = nextLeft;
662
+ previewShellTop = nextTop;
663
+ }
664
+
665
+ if (resizeWidth != null && resizeHeight != null) {
666
+ dispatch('resize', { width: resizeWidth, height: resizeHeight });
667
+ }
511
668
  }
512
669
 
513
670
  function endResize() {
514
671
  isResizing = false;
672
+ isSymmetricalResize = false;
515
673
  document.removeEventListener('pointermove', onPointerMove);
516
674
  document.removeEventListener('pointerup', endResize);
517
675
  document.removeEventListener('pointercancel', endResize);
@@ -523,7 +681,14 @@
523
681
  }
524
682
 
525
683
  document.body.style.userSelect = 'none';
526
- document.body.style.cursor = 'nwse-resize';
684
+ document.body.style.cursor =
685
+ signX !== 0 && signY !== 0
686
+ ? signX * signY > 0
687
+ ? 'nwse-resize'
688
+ : 'nesw-resize'
689
+ : signX !== 0
690
+ ? 'ew-resize'
691
+ : 'ns-resize';
527
692
  document.addEventListener('pointermove', onPointerMove);
528
693
  document.addEventListener('pointerup', endResize);
529
694
  document.addEventListener('pointercancel', endResize);
@@ -531,6 +696,7 @@
531
696
 
532
697
  $: if (enableResize && contentContainerEl && (resizeWidth != null || resizeHeight != null)) {
533
698
  clampResizeToLimits();
699
+ ensurePreviewPosition();
534
700
  }
535
701
 
536
702
  $: if (autoFitStage && resizeStageEl) {
@@ -544,6 +710,18 @@
544
710
 
545
711
  onMount(() => {
546
712
  window.addEventListener('message', handleMessage);
713
+ const onAltDown = (e: KeyboardEvent) => {
714
+ if (e.key === 'Alt') isSymmetricalResize = true;
715
+ };
716
+ const onAltUp = (e: KeyboardEvent) => {
717
+ if (e.key === 'Alt' && !isResizing) isSymmetricalResize = false;
718
+ };
719
+ window.addEventListener('keydown', onAltDown);
720
+ window.addEventListener('keyup', onAltUp);
721
+ return () => {
722
+ window.removeEventListener('keydown', onAltDown);
723
+ window.removeEventListener('keyup', onAltUp);
724
+ };
547
725
  });
548
726
 
549
727
  onDestroy(() => {
@@ -657,12 +835,66 @@
657
835
  {:else if effectiveSrc || (mode === 'standalone' && isResolvingUrl)}
658
836
  <div
659
837
  bind:this={resizeStageEl}
660
- class="{enableResize ? 'preview-resize-stage-ct absolute inset-0 overflow-hidden flex items-center justify-center' : 'preview-resize-stage-ct relative flex items-center justify-center w-full h-full'}"
838
+ class="{enableResize ? 'preview-resize-stage-ct absolute inset-0 overflow-hidden' : 'preview-resize-stage-ct relative flex items-center justify-center w-full h-full'}"
661
839
  >
662
840
  <div
663
- class="preview-iframe-wrapper relative"
664
- style="width: {resizeWidth ? resizeWidth + 'px' : '100%'}; height: {resizeHeight ? resizeHeight + 'px' : '100%'}; transition: {isResizing ? 'none' : 'width 0.2s, height 0.2s'};"
841
+ class="preview-iframe-shell relative flex flex-col {enableResize ? 'preview-iframe-shell-positioned' : ''}"
842
+ class:preview-iframe-shell-symmetrical={isSymmetricalResize}
843
+ style="{enableResize
844
+ ? `position: absolute; left: ${previewShellLeft}px; top: ${previewShellTop}px; width: ${resizeWidth ? resizeWidth + 'px' : '100%'};`
845
+ : `width: ${resizeWidth ? resizeWidth + 'px' : '100%'}; height: ${resizeHeight ? resizeHeight + 'px' : '100%'};`} transition: {isResizing || isDraggingPreview ? 'none' : 'width 0.2s, height 0.2s, left 0.15s, top 0.15s'};"
665
846
  >
847
+ {#if showPreviewChrome}
848
+ <!-- svelte-ignore a11y_no_static_element_interactions -->
849
+ <div
850
+ class="preview-chrome-header flex items-center gap-2 h-9 px-2 border border-b-0 rounded-t-md bg-white select-none {enableDrag ? 'cursor-move' : ''}"
851
+ style="border-color: {previewStrokeColor};"
852
+ onpointerdown={startPreviewDrag}
853
+ >
854
+ {#if enableDrag}
855
+ <span class="preview-chrome-drag-grip shrink-0 text-gray-400 text-xs leading-none" title="Drag preview" aria-hidden="true">⠿</span>
856
+ {/if}
857
+ <span class="preview-chrome-title text-[11px] font-semibold uppercase tracking-wide text-gray-500 shrink-0">Preview</span>
858
+ {#if showDimensionInputs}
859
+ <div class="preview-chrome-dimensions ml-auto flex items-center gap-1.5">
860
+ <label class="preview-dim-label flex items-center gap-1 text-[10px] font-semibold text-gray-500">
861
+ <span>W</span>
862
+ <input
863
+ type="number"
864
+ min={MIN_WIDTH}
865
+ class="preview-dim-input w-14 h-6 rounded border border-gray-300 bg-white px-1 text-[11px] text-gray-800 focus:outline-none focus:border-blue-400 focus-visible:outline-none"
866
+ value={widthInputValue}
867
+ oninput={(e) => {
868
+ widthInputValue = e.currentTarget.value;
869
+ applyDimensionFromInput('width', e.currentTarget.value);
870
+ }}
871
+ onpointerdown={(e) => e.stopPropagation()}
872
+ />
873
+ </label>
874
+ <span class="text-[10px] text-gray-400">×</span>
875
+ <label class="preview-dim-label flex items-center gap-1 text-[10px] font-semibold text-gray-500">
876
+ <span>H</span>
877
+ <input
878
+ type="number"
879
+ min={MIN_HEIGHT}
880
+ class="preview-dim-input w-14 h-6 rounded border border-gray-300 bg-white px-1 text-[11px] text-gray-800 focus:outline-none focus:border-blue-400 focus-visible:outline-none"
881
+ value={heightInputValue}
882
+ oninput={(e) => {
883
+ heightInputValue = e.currentTarget.value;
884
+ applyDimensionFromInput('height', e.currentTarget.value);
885
+ }}
886
+ onpointerdown={(e) => e.stopPropagation()}
887
+ />
888
+ </label>
889
+ </div>
890
+ {/if}
891
+ </div>
892
+ {/if}
893
+
894
+ <div
895
+ class="preview-iframe-wrapper relative"
896
+ style="width: 100%; height: {resizeHeight ? resizeHeight + 'px' : '100%'};"
897
+ >
666
898
  {#if showResizeHandles}
667
899
  <!-- svelte-ignore a11y-no-static-element-interactions -->
668
900
  <div class="preview-resize-handle preview-resize-handle-tl" onpointerdown={(e) => startResize(e, -1, -1)}></div>
@@ -672,9 +904,17 @@
672
904
  <div class="preview-resize-handle preview-resize-handle-bl" onpointerdown={(e) => startResize(e, -1, 1)}></div>
673
905
  <!-- svelte-ignore a11y-no-static-element-interactions -->
674
906
  <div class="preview-resize-handle preview-resize-handle-br" onpointerdown={(e) => startResize(e, 1, 1)}></div>
907
+ <!-- svelte-ignore a11y-no-static-element-interactions -->
908
+ <div class="preview-resize-handle preview-resize-handle-t" onpointerdown={(e) => startResize(e, 0, -1)}></div>
909
+ <!-- svelte-ignore a11y-no-static-element-interactions -->
910
+ <div class="preview-resize-handle preview-resize-handle-r" onpointerdown={(e) => startResize(e, 1, 0)}></div>
911
+ <!-- svelte-ignore a11y-no-static-element-interactions -->
912
+ <div class="preview-resize-handle preview-resize-handle-b" onpointerdown={(e) => startResize(e, 0, 1)}></div>
913
+ <!-- svelte-ignore a11y-no-static-element-interactions -->
914
+ <div class="preview-resize-handle preview-resize-handle-l" onpointerdown={(e) => startResize(e, -1, 0)}></div>
675
915
  {/if}
676
916
 
677
- {#if resizeWidth || resizeHeight}
917
+ {#if (resizeWidth || resizeHeight) && !showDimensionInputs}
678
918
  <div
679
919
  class="widget-preview-dimension-badge absolute left-1/2 -translate-x-1/2 text-[10px] bg-black/70 px-1.5 py-px rounded text-gray-300 select-none pointer-events-none z-20 whitespace-nowrap"
680
920
  style="top: calc(100% + 4px);"
@@ -683,7 +923,10 @@
683
923
  </div>
684
924
  {/if}
685
925
 
686
- <div class="preview-iframe-container relative w-full h-full overflow-hidden {compactChrome ? '' : showUrlBar || mode === 'controlled' || enableResize ? 'border border-gray-300' : ''}">
926
+ <div
927
+ class="preview-iframe-container relative w-full h-full overflow-hidden {compactChrome && !enableResize ? '' : showUrlBar || mode === 'controlled' || enableResize ? 'border' : ''}"
928
+ style={enableResize ? `border-color: ${previewStrokeColor}; border-radius: ${showPreviewChrome ? '0 0 6px 6px' : '6px'};` : ''}
929
+ >
687
930
  {#if mode === 'standalone' && iframeError}
688
931
  <div class="widget-preview-error-ct absolute inset-0 flex items-center justify-center bg-white z-10 rounded">
689
932
  <div class="widget-preview-error-content-ct flex flex-col items-center gap-2 text-center px-4">
@@ -734,9 +977,14 @@
734
977
  {#if mode === 'controlled' && previewHasRuntimeError && !isGeneratingCode}
735
978
  <div class="compilation-error-overlay absolute inset-0 z-20 flex flex-col items-center justify-center bg-red-50 text-red-700 p-6">
736
979
  <div class="text-5xl mb-4">⚠️</div>
737
- <div class="font-semibold text-xl mb-2">Code Has Compilation Errors</div>
738
- <div class="text-sm text-center text-red-600 mb-4 max-w-md">
739
- The generated code has syntax or compilation errors (HTTP {previewRuntimeErrorStatus}).
980
+ <div class="font-semibold text-xl mb-2">
981
+ {previewError && /TypeError|Uncaught|is not a function|is not defined/i.test(previewError)
982
+ ? 'Widget runtime error'
983
+ : 'Code Has Compilation Errors'}
984
+ </div>
985
+ <div class="text-sm text-center text-red-600 mb-4 max-w-md whitespace-pre-line">
986
+ {previewError
987
+ || `The generated code has syntax or compilation errors (HTTP ${previewRuntimeErrorStatus}).`}
740
988
  </div>
741
989
  <div class="flex gap-3 flex-wrap justify-center">
742
990
  <button type="button" onclick={() => dispatch('fixErrors')} class="px-4 py-2 text-xs rounded bg-red-600 hover:bg-red-700 text-white cursor-pointer border-0">🔧 Fix Errors</button>
@@ -755,7 +1003,7 @@
755
1003
  src={mountedPreviewSrc}
756
1004
  class="widget-preview-iframe h-full w-full border-0"
757
1005
  tabindex="-1"
758
- sandbox={mode === 'controlled' ? undefined : 'allow-scripts allow-popups allow-forms'}
1006
+ sandbox={mode === 'controlled' ? undefined : 'allow-scripts allow-popups allow-forms allow-same-origin'}
759
1007
  loading="eager"
760
1008
  allow="clipboard-read; clipboard-write"
761
1009
  onload={handleIframeLoad}
@@ -772,6 +1020,7 @@
772
1020
  <GenerateLoader overlay title="Generating code…" subtitle="" size="lg" />
773
1021
  {/if}
774
1022
  </div>
1023
+ </div>
775
1024
  </div>
776
1025
  </div>
777
1026
 
@@ -800,10 +1049,10 @@
800
1049
  {/if}
801
1050
  </div>
802
1051
 
803
- {#if isResizing}
1052
+ {#if isResizing || isDraggingPreview}
804
1053
  <div
805
- class="widget-preview-resize-guard-ct fixed inset-0 z-999999 cursor-nwse-resize"
806
- style="background: transparent;"
1054
+ class="widget-preview-resize-guard-ct fixed inset-0 z-999999"
1055
+ style="background: transparent; cursor: {isDraggingPreview ? 'move' : 'nwse-resize'};"
807
1056
  aria-hidden="true"
808
1057
  ></div>
809
1058
  {/if}
@@ -815,38 +1064,39 @@
815
1064
 
816
1065
  .preview-resize-handle {
817
1066
  position: absolute;
818
- width: 16px;
819
- height: 16px;
1067
+ width: 10px;
1068
+ height: 10px;
820
1069
  z-index: 30;
821
- border-radius: 2px;
822
- }
823
-
824
- .preview-resize-handle::before,
825
- .preview-resize-handle::after {
826
- content: '';
827
- position: absolute;
828
- background: #94a3b8;
1070
+ box-sizing: border-box;
1071
+ background: #ffffff;
1072
+ border: 1.5px solid #3b82f6;
829
1073
  border-radius: 1px;
830
1074
  }
831
1075
 
832
- .preview-resize-handle-tl { top: -4px; left: -4px; cursor: nwse-resize; }
833
- .preview-resize-handle-tl::before { top: 0; left: 0; width: 10px; height: 2px; }
834
- .preview-resize-handle-tl::after { top: 0; left: 0; width: 2px; height: 10px; }
835
-
836
- .preview-resize-handle-tr { top: -4px; right: -4px; cursor: nesw-resize; }
837
- .preview-resize-handle-tr::before { top: 0; right: 0; width: 10px; height: 2px; }
838
- .preview-resize-handle-tr::after { top: 0; right: 0; width: 2px; height: 10px; }
839
-
840
- .preview-resize-handle-bl { bottom: -4px; left: -4px; cursor: nesw-resize; }
841
- .preview-resize-handle-bl::before { bottom: 0; left: 0; width: 10px; height: 2px; }
842
- .preview-resize-handle-bl::after { bottom: 0; left: 0; width: 2px; height: 10px; }
1076
+ .preview-iframe-shell-symmetrical .preview-resize-handle,
1077
+ .preview-iframe-shell-symmetrical .preview-iframe-container {
1078
+ border-color: #f97316 !important;
1079
+ }
843
1080
 
844
- .preview-resize-handle-br { bottom: -4px; right: -4px; cursor: nwse-resize; }
845
- .preview-resize-handle-br::before { bottom: 0; right: 0; width: 10px; height: 2px; }
846
- .preview-resize-handle-br::after { bottom: 0; right: 0; width: 2px; height: 10px; }
1081
+ .preview-chrome-header button:focus,
1082
+ .preview-chrome-header button:focus-visible,
1083
+ .preview-chrome-header input:focus,
1084
+ .preview-chrome-header input:focus-visible {
1085
+ outline: none;
1086
+ box-shadow: none;
1087
+ }
847
1088
 
848
- .preview-resize-handle:hover::before,
849
- .preview-resize-handle:hover::after {
850
- background: #64748b;
1089
+ .preview-resize-handle-tl { top: -5px; left: -5px; cursor: nwse-resize; }
1090
+ .preview-resize-handle-tr { top: -5px; right: -5px; cursor: nesw-resize; }
1091
+ .preview-resize-handle-bl { bottom: -5px; left: -5px; cursor: nesw-resize; }
1092
+ .preview-resize-handle-br { bottom: -5px; right: -5px; cursor: nwse-resize; }
1093
+ .preview-resize-handle-t { top: -5px; left: 50%; margin-left: -5px; cursor: ns-resize; }
1094
+ .preview-resize-handle-b { bottom: -5px; left: 50%; margin-left: -5px; cursor: ns-resize; }
1095
+ .preview-resize-handle-l { left: -5px; top: 50%; margin-top: -5px; cursor: ew-resize; }
1096
+ .preview-resize-handle-r { right: -5px; top: 50%; margin-top: -5px; cursor: ew-resize; }
1097
+
1098
+ .preview-resize-handle:hover {
1099
+ background: #eff6ff;
1100
+ border-color: #2563eb;
851
1101
  }
852
1102
  </style>