@widgetic/editor 4.0.10 โ†’ 4.1.0

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 (84) hide show
  1. package/README.md +242 -0
  2. package/dist/components/WidgetPreview.svelte +123 -86
  3. package/dist/components/WidgetPreview.svelte.d.ts +8 -0
  4. package/dist/components/color-select/ColorSelect.svelte +4 -4
  5. package/dist/components/editors/CompositionEditorApp.svelte +562 -0
  6. package/dist/components/editors/CompositionEditorApp.svelte.d.ts +95 -0
  7. package/dist/components/editors/EmbedEditor.svelte +33 -22
  8. package/dist/components/editors/EmbedEditor.svelte.d.ts +1 -1
  9. package/dist/components/editors/PropsEditor.svelte +267 -850
  10. package/dist/components/editors/PropsEditor.svelte.d.ts +10 -3
  11. package/dist/components/ic/AngleInputController.svelte +1 -1
  12. package/dist/components/ic/BrowserInputController.svelte +58 -31
  13. package/dist/components/ic/ColorPickerInputController.svelte +55 -21
  14. package/dist/components/ic/DateFieldInputController.svelte +34 -14
  15. package/dist/components/ic/DatePickerInputController.svelte +37 -21
  16. package/dist/components/ic/DropdownInputController.svelte +21 -9
  17. package/dist/components/ic/DropdownInputController.svelte.d.ts +1 -1
  18. package/dist/components/ic/FixedPositionInputController.svelte +12 -6
  19. package/dist/components/ic/FontInputController.svelte +100 -76
  20. package/dist/components/ic/InputControllerHeader.svelte +16 -9
  21. package/dist/components/ic/NumberControls.svelte +4 -4
  22. package/dist/components/ic/NumberInputController.svelte +6 -4
  23. package/dist/components/ic/PositionInputController.svelte +23 -15
  24. package/dist/components/ic/RangeCalendarInputController.svelte +36 -20
  25. package/dist/components/ic/RoundedCornersInputController.svelte +39 -26
  26. package/dist/components/ic/SliderInputController.svelte +7 -2
  27. package/dist/components/ic/SwitchInputController.svelte +12 -3
  28. package/dist/components/ic/TagsInputController.svelte +19 -4
  29. package/dist/components/ic/TextAreaInputController.svelte +79 -51
  30. package/dist/components/ic/TextAreaPreviewInputController.svelte +27 -72
  31. package/dist/components/ic/TextInputController.svelte +17 -1
  32. package/dist/components/ic/TimeInputController.svelte +11 -4
  33. package/dist/components/ic/VectorInputController.svelte +101 -70
  34. package/dist/components/ic/shared/ItemGrid.svelte +12 -4
  35. package/dist/components/ic/shared/RecentItemsSection.svelte +16 -11
  36. package/dist/components/ic/shared/RecentItemsSection.svelte.d.ts +1 -1
  37. package/dist/composition/compositionApi.d.ts +95 -0
  38. package/dist/composition/compositionApi.js +289 -0
  39. package/dist/composition/compositionEditorController.d.ts +263 -0
  40. package/dist/composition/compositionEditorController.js +746 -0
  41. package/dist/composition/compositionHelpers.d.ts +74 -0
  42. package/dist/composition/compositionHelpers.js +405 -0
  43. package/dist/i18n/index.d.ts +4 -0
  44. package/dist/i18n/index.js +4 -0
  45. package/dist/i18n/labels.d.ts +15 -0
  46. package/dist/i18n/labels.js +57 -0
  47. package/dist/i18n/language.d.ts +228 -0
  48. package/dist/i18n/language.js +34 -0
  49. package/dist/i18n/languageKit.d.ts +54 -0
  50. package/dist/i18n/languageKit.js +102 -0
  51. package/dist/i18n/messages.d.ts +241 -0
  52. package/dist/i18n/messages.js +253 -0
  53. package/dist/i18n/translate.d.ts +6 -0
  54. package/dist/i18n/translate.js +8 -0
  55. package/dist/index.d.ts +4 -0
  56. package/dist/index.js +8 -0
  57. package/dist/mediaLibraryContext.d.ts +10 -0
  58. package/dist/mediaLibraryContext.js +30 -0
  59. package/dist/utils/FontManager.d.ts +21 -5
  60. package/dist/utils/FontManager.js +62 -67
  61. package/dist/utils/VectorManager.d.ts +0 -1
  62. package/dist/utils/VectorManager.js +10 -12
  63. package/dist/utils/dockPlacement.d.ts +12 -0
  64. package/dist/utils/dockPlacement.js +13 -0
  65. package/dist/utils/editorStorage.d.ts +23 -0
  66. package/dist/utils/editorStorage.js +95 -0
  67. package/dist/utils/fieldDirection.d.ts +13 -0
  68. package/dist/utils/fieldDirection.js +25 -0
  69. package/dist/utils/fontUtils.js +5 -3
  70. package/dist/utils/htmlSafety.d.ts +21 -0
  71. package/dist/utils/htmlSafety.js +38 -0
  72. package/dist/utils/itemReorder.d.ts +90 -0
  73. package/dist/utils/itemReorder.js +354 -0
  74. package/dist/utils/recentColors.js +4 -2
  75. package/dist/utils/richTextPreview.d.ts +38 -0
  76. package/dist/utils/richTextPreview.js +207 -0
  77. package/dist/utils/scrollActivity.d.ts +14 -0
  78. package/dist/utils/scrollActivity.js +63 -0
  79. package/dist/utils/shadowDom.d.ts +67 -0
  80. package/dist/utils/shadowDom.js +211 -0
  81. package/dist/utils/vectorUtils.js +5 -3
  82. package/dist/utils/visibilityCondition.d.ts +40 -0
  83. package/dist/utils/visibilityCondition.js +535 -0
  84. package/package.json +7 -6
@@ -1,13 +1,20 @@
1
1
  <script lang="ts">
2
2
  import { onMount, onDestroy, createEventDispatcher, tick } from 'svelte';
3
3
  import { GenerateLoader } from '@widgetic/design-system/components';
4
+ import { lockPointerStyle } from '../utils/shadowDom';
5
+ import { useEditorLanguage } from '../i18n/language';
6
+
7
+ // The words of the editor in the language of the page: the editor above this one chose it (see `provideEditorLanguage`).
8
+ const { t } = useEditorLanguage();
4
9
 
5
10
  const dispatch = createEventDispatcher<{
6
11
  load: { src: string | null };
7
12
  error: { src: string | null };
8
13
  refresh: void;
9
14
  openInTab: void;
10
- resize: { width: number; height: number };
15
+ // `source` says where a size came from, so a host can tell a size the user chose ('drag', 'input', 'fit',
16
+ // 'device') from one the box around the preview forced on it ('clamp': the box is smaller than the size asked for).
17
+ resize: { width: number; height: number; source?: 'drag' | 'input' | 'fit' | 'device' | 'clamp' };
11
18
  mobileViewport: { width: number; height: number };
12
19
  desktopViewport: void;
13
20
  deviceView: { mobile: boolean };
@@ -50,6 +57,17 @@
50
57
  export let debugMode: boolean = false;
51
58
  export let iframeReloadKey = 0;
52
59
 
60
+ // โ”€โ”€ What the preview iframe may do โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
61
+ /**
62
+ * The iframe's `sandbox` tokens. `undefined` keeps what each mode always had (controlled: no sandbox; standalone:
63
+ * scripts, popups, forms and its own origin); a string is used exactly as given; `null` means no `sandbox` at all.
64
+ * A page that embeds the editor for its own visitors passes a list without `allow-top-navigation`, so that a
65
+ * widget's code cannot send the visitor's whole tab somewhere else.
66
+ */
67
+ export let sandbox: string | null | undefined = undefined;
68
+ /** The iframe's `allow` attribute: which of the page's permissions the preview may use. */
69
+ export let allow: string = 'clipboard-read; clipboard-write';
70
+
53
71
  // โ”€โ”€ Layout / chrome โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
54
72
  export let width: string = '100%';
55
73
  export let height: string = '100%';
@@ -74,6 +92,7 @@
74
92
  */
75
93
  export let resizeMode: 'concentric' | 'corner' = 'concentric';
76
94
  /** Initial / reset placement inside the stage. */
95
+ // rtl-ok: 'right-half' names the half of the stage by its place on the screen; the composition editor docks the preview itself
77
96
  export let defaultStageAlign: 'center' | 'right-half' = 'center';
78
97
  /** Observe stage size and fill it (desktop modal) */
79
98
  export let autoFitStage: boolean = false;
@@ -91,7 +110,6 @@
91
110
  let internalIframeSrc: string | null = null;
92
111
  let iframeLoaded = false;
93
112
  let iframeError = false;
94
- let errorMessage = '';
95
113
  let isWidgetPublished = false;
96
114
  let triedFallback = false;
97
115
  let pendingWidget: any = null;
@@ -160,6 +178,7 @@
160
178
  ): { left: number; top: number } {
161
179
  const maxLeft = Math.max(RESIZE_PAD_X, stageWidth - shellWidth - RESIZE_PAD_X);
162
180
  const maxTop = Math.max(RESIZE_PAD_TOP, stageHeight - shellHeight - RESIZE_PAD_BOTTOM);
181
+ // rtl-ok: the half of the stage is named by its place on the screen, see the prop
163
182
  if (defaultStageAlign === 'right-half') {
164
183
  const rightHalfCenterX = stageWidth * 0.75;
165
184
  const left = clampDimension(rightHalfCenterX - shellWidth / 2, RESIZE_PAD_X, maxLeft);
@@ -222,11 +241,19 @@
222
241
  heightInputValue = String(Math.round(resizeHeight));
223
242
  }
224
243
  if (resizeWidth != null && resizeHeight != null) {
225
- dispatch('resize', { width: resizeWidth, height: resizeHeight });
244
+ dispatch('resize', { width: resizeWidth, height: resizeHeight, source: 'input' });
226
245
  }
227
246
  console.log('[WidgetPreview] Dimension input', { dimension, value: parsed });
228
247
  }
229
248
 
249
+ /**
250
+ * While a drag or a resize lasts the pointer shows the matching cursor and nothing is selected as it moves
251
+ * (`lockPointerStyle`, in `shadowDom.ts`: set on the host of the shadow root the editor lives in, or on <body> when
252
+ * there is none, and put back afterwards). Puts back what the running drag or resize changed: the pointer style,
253
+ * and the listeners on `document`.
254
+ */
255
+ let finishPointerGesture: (() => void) | null = null;
256
+
230
257
  function startPreviewDrag(event: PointerEvent) {
231
258
  if (!enableDrag || isResizing) return;
232
259
  const target = event.target as HTMLElement | null;
@@ -252,22 +279,25 @@
252
279
 
253
280
  function endDrag() {
254
281
  isDraggingPreview = false;
255
- document.removeEventListener('pointermove', onPointerMove);
256
- document.removeEventListener('pointerup', endDrag);
257
- document.removeEventListener('pointercancel', endDrag);
258
- document.body.style.removeProperty('user-select');
259
- document.body.style.removeProperty('cursor');
282
+ finishPointerGesture?.();
260
283
  console.log('[WidgetPreview] Preview drag end', {
261
284
  left: previewShellLeft,
262
285
  top: previewShellTop,
263
286
  });
264
287
  }
265
288
 
266
- document.body.style.userSelect = 'none';
267
- document.body.style.cursor = 'move';
289
+ finishPointerGesture?.();
290
+ const unlockPointerStyle = lockPointerStyle(event.target, 'move');
268
291
  document.addEventListener('pointermove', onPointerMove);
269
292
  document.addEventListener('pointerup', endDrag);
270
293
  document.addEventListener('pointercancel', endDrag);
294
+ finishPointerGesture = () => {
295
+ document.removeEventListener('pointermove', onPointerMove);
296
+ document.removeEventListener('pointerup', endDrag);
297
+ document.removeEventListener('pointercancel', endDrag);
298
+ unlockPointerStyle();
299
+ finishPointerGesture = null;
300
+ };
271
301
  }
272
302
 
273
303
  /** Firefox/Chrome skip painting a cross-origin iframe that started at opacity 0 or inside a CSS filter until a reflow (opening DevTools). */
@@ -308,7 +338,7 @@
308
338
  }
309
339
  resizeWidth = limits.maxWidth;
310
340
  resizeHeight = limits.maxHeight;
311
- dispatch('resize', { width: limits.maxWidth, height: limits.maxHeight });
341
+ dispatch('resize', { width: limits.maxWidth, height: limits.maxHeight, source: 'fit' });
312
342
  previewPositionReady = false;
313
343
  requestAnimationFrame(() => {
314
344
  resetPreviewPosition();
@@ -353,8 +383,8 @@
353
383
  : '';
354
384
  $: iframeTitle =
355
385
  mode === 'controlled'
356
- ? 'Widget preview'
357
- : `Widget preview โ€“ ${widget?.name || 'Widget'}`;
386
+ ? $t('editor.preview.frameTitle')
387
+ : $t('editor.preview.frameTitleNamed', { name: widget?.name || $t('editor.preview.widgetName') });
358
388
 
359
389
  function buildWidgetUrl(w: any): string | null {
360
390
  if (!w) return null;
@@ -395,7 +425,6 @@
395
425
  iframeLoaded = false;
396
426
  iframeError = false;
397
427
  widgetReady = false;
398
- errorMessage = '';
399
428
  pendingMessages = [];
400
429
  isResolvingUrl = false;
401
430
 
@@ -451,7 +480,6 @@
451
480
  isWidgetPublished = false;
452
481
  iframeLoaded = false;
453
482
  iframeError = false;
454
- errorMessage = '';
455
483
  widgetReady = false;
456
484
  isResolvingUrl = false;
457
485
  }
@@ -569,12 +597,14 @@
569
597
  iframeError = true;
570
598
  iframeLoaded = false;
571
599
  widgetReady = false;
572
- errorMessage = 'Failed to load widget preview.';
573
600
  console.error('[WidgetPreview] Widget iframe onerror:', effectiveSrc);
574
601
  dispatch('error', { src: effectiveSrc });
575
602
  }
576
603
 
577
604
  function handleMessage(event: MessageEvent) {
605
+ // Only the preview iframe of this component speaks to it: any other window on the page (a partner's own frames
606
+ // and scripts included) can post messages to this window too.
607
+ if (!iframeEl?.contentWindow || event.source !== iframeEl.contentWindow) return;
578
608
  const type = event.data?.type;
579
609
  if (type === 'widgetic:ready' || type === 'widgetic:pong') {
580
610
  if (mode === 'standalone') {
@@ -597,7 +627,8 @@
597
627
 
598
628
  function handleOpenInTabClick() {
599
629
  const url = effectiveDisplayUrl || effectiveSrc;
600
- if (url) window.open(url, '_blank');
630
+ // noopener: the page that opens must not get a handle on the page that opened it (which can be a partner's page).
631
+ if (url) window.open(url, '_blank', 'noopener,noreferrer');
601
632
  dispatch('openInTab');
602
633
  }
603
634
 
@@ -607,7 +638,7 @@
607
638
  const h = Math.min(mobileViewportHeight, maxHeight);
608
639
  resizeWidth = w;
609
640
  resizeHeight = h;
610
- dispatch('resize', { width: w, height: h });
641
+ dispatch('resize', { width: w, height: h, source: 'device' });
611
642
  dispatch('mobileViewport', { width: w, height: h });
612
643
  }
613
644
 
@@ -629,7 +660,7 @@
629
660
  if (nextW !== resizeWidth || nextH !== resizeHeight) {
630
661
  resizeWidth = nextW;
631
662
  resizeHeight = nextH;
632
- dispatch('resize', { width: nextW, height: nextH });
663
+ dispatch('resize', { width: nextW, height: nextH, source: 'clamp' });
633
664
  }
634
665
  }
635
666
 
@@ -689,35 +720,40 @@
689
720
  }
690
721
 
691
722
  if (resizeWidth != null && resizeHeight != null) {
692
- dispatch('resize', { width: resizeWidth, height: resizeHeight });
723
+ dispatch('resize', { width: resizeWidth, height: resizeHeight, source: 'drag' });
693
724
  }
694
725
  }
695
726
 
696
727
  function endResize() {
697
728
  isResizing = false;
698
729
  // Keep orange stroke while Alt remains held (composition corner mode).
699
- document.removeEventListener('pointermove', onPointerMove);
700
- document.removeEventListener('pointerup', endResize);
701
- document.removeEventListener('pointercancel', endResize);
702
- document.body.style.removeProperty('user-select');
703
- document.body.style.removeProperty('cursor');
730
+ finishPointerGesture?.();
704
731
  if (resizeWidth != null && resizeHeight != null) {
705
- dispatch('resize', { width: resizeWidth, height: resizeHeight });
732
+ dispatch('resize', { width: resizeWidth, height: resizeHeight, source: 'drag' });
706
733
  }
707
734
  }
708
735
 
709
- document.body.style.userSelect = 'none';
710
- document.body.style.cursor =
736
+ finishPointerGesture?.();
737
+ const unlockPointerStyle = lockPointerStyle(
738
+ event.target,
711
739
  signX !== 0 && signY !== 0
712
740
  ? signX * signY > 0
713
741
  ? 'nwse-resize'
714
742
  : 'nesw-resize'
715
743
  : signX !== 0
716
744
  ? 'ew-resize'
717
- : 'ns-resize';
745
+ : 'ns-resize'
746
+ );
718
747
  document.addEventListener('pointermove', onPointerMove);
719
748
  document.addEventListener('pointerup', endResize);
720
749
  document.addEventListener('pointercancel', endResize);
750
+ finishPointerGesture = () => {
751
+ document.removeEventListener('pointermove', onPointerMove);
752
+ document.removeEventListener('pointerup', endResize);
753
+ document.removeEventListener('pointercancel', endResize);
754
+ unlockPointerStyle();
755
+ finishPointerGesture = null;
756
+ };
721
757
  }
722
758
 
723
759
  $: if (enableResize && contentContainerEl && (resizeWidth != null || resizeHeight != null)) {
@@ -762,8 +798,8 @@
762
798
  onDestroy(() => {
763
799
  window.removeEventListener('message', handleMessage);
764
800
  stageResizeObserver?.disconnect();
765
- document.body.style.removeProperty('user-select');
766
- document.body.style.removeProperty('cursor');
801
+ // A drag or resize that was still going when the editor was removed: stop listening and put the style back.
802
+ finishPointerGesture?.();
767
803
  });
768
804
  </script>
769
805
 
@@ -776,7 +812,7 @@
776
812
  <span
777
813
  class="preview-url-text flex-1 rounded border border-gray-300 px-2 py-1 text-xs text-gray-500 min-w-0 font-mono overflow-x-auto whitespace-nowrap"
778
814
  >
779
- {effectiveDisplayUrl || effectiveSrc || 'no URL'}
815
+ {effectiveDisplayUrl || effectiveSrc || $t('editor.preview.noUrl')}
780
816
  </span>
781
817
  <button
782
818
  type="button"
@@ -784,7 +820,7 @@
784
820
  const url = effectiveDisplayUrl || effectiveSrc;
785
821
  if (url) navigator.clipboard.writeText(url);
786
822
  }}
787
- title="Copy URL"
823
+ title={$t('editor.preview.copyUrl')}
788
824
  class="shrink-0 w-7 h-7 flex items-center justify-center rounded border border-gray-300 bg-white hover:bg-gray-100 text-sm cursor-pointer"
789
825
  >
790
826
  <svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="9" y="9" width="13" height="13" rx="2" ry="2"/><path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"/></svg>
@@ -793,14 +829,14 @@
793
829
  <button
794
830
  type="button"
795
831
  onclick={handleRefreshClick}
796
- title="Refresh preview"
832
+ title={$t('editor.preview.refresh')}
797
833
  class="shrink-0 w-7 h-7 flex items-center justify-center rounded border border-gray-300 bg-white hover:bg-gray-100 text-sm cursor-pointer"
798
834
  >๐Ÿ”„</button>
799
835
  {#if showViewportButtons && enableResize}
800
836
  <button
801
837
  type="button"
802
838
  onclick={handleFitToAreaClick}
803
- title="Fit preview to panel"
839
+ title={$t('editor.preview.fitToPanel')}
804
840
  class="preview-fit-to-area-btn shrink-0 w-7 h-7 flex items-center justify-center rounded border border-gray-300 bg-white hover:bg-gray-100 cursor-pointer"
805
841
  >
806
842
  <svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M15 3h6v6"/><path d="M9 21H3v-6"/><path d="M21 3l-7 7"/><path d="M3 21l7-7"/></svg>
@@ -808,7 +844,7 @@
808
844
  <button
809
845
  type="button"
810
846
  onclick={applyMobileViewport}
811
- title="Mobile viewport"
847
+ title={$t('editor.preview.mobileViewport')}
812
848
  class="preview-mobile-viewport-btn shrink-0 w-7 h-7 flex items-center justify-center rounded border border-gray-300 bg-white hover:bg-gray-100 cursor-pointer"
813
849
  >
814
850
  <svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="5" y="2" width="14" height="20" rx="2" ry="2"/><line x1="12" y1="18" x2="12.01" y2="18"/></svg>
@@ -816,7 +852,7 @@
816
852
  <button
817
853
  type="button"
818
854
  onclick={handleDesktopViewport}
819
- title="Desktop viewport (full window)"
855
+ title={$t('editor.preview.desktopViewport')}
820
856
  class="preview-desktop-viewport-btn shrink-0 w-7 h-7 flex items-center justify-center rounded border border-gray-300 bg-white hover:bg-gray-100 cursor-pointer"
821
857
  >
822
858
  <svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="2" y="3" width="20" height="14" rx="2" ry="2"/><line x1="8" y1="21" x2="16" y2="21"/><line x1="12" y1="17" x2="12" y2="21"/></svg>
@@ -825,9 +861,9 @@
825
861
  <button
826
862
  type="button"
827
863
  onclick={handleOpenInTabClick}
828
- title="Open in new tab"
864
+ title={$t('editor.preview.openInNewTab')}
829
865
  class="shrink-0 px-2 h-7 flex items-center justify-center rounded border border-gray-300 bg-white hover:bg-gray-100 text-xs cursor-pointer whitespace-nowrap"
830
- >โ†— New tab</button>
866
+ >{$t('editor.preview.newTab')}</button>
831
867
  {/if}
832
868
  </div>
833
869
  {/if}
@@ -846,7 +882,7 @@
846
882
 
847
883
  {:else if mode === 'standalone' && !widget}
848
884
  <div class="widget-preview-select-ct flex items-center justify-center h-full text-sm text-gray-400">
849
- Select a widget to preview
885
+ {$t('editor.preview.selectWidget')}
850
886
  </div>
851
887
 
852
888
  {:else if mode === 'standalone' && widget && !effectiveSrc && !isResolvingUrl && !iframeError}
@@ -858,11 +894,11 @@
858
894
  </svg>
859
895
  </div>
860
896
  <div>
861
- <p class="text-sm font-medium text-gray-600">{widget.name || 'Widget'}</p>
862
- <p class="text-xs text-gray-400 mt-1">Not published yet</p>
897
+ <p class="text-sm font-medium text-gray-600">{widget.name || $t('editor.preview.widgetName')}</p>
898
+ <p class="text-xs text-gray-400 mt-1">{$t('editor.preview.notPublished')}</p>
863
899
  </div>
864
900
  <p class="text-[11px] text-gray-300 max-w-[220px]">
865
- Publish the widget in Widget Creator to preview it here.
901
+ {$t('editor.preview.publishToPreview')}
866
902
  </p>
867
903
  </div>
868
904
  </div>
@@ -875,7 +911,7 @@
875
911
  <div
876
912
  class="preview-iframe-shell relative flex flex-col {enableResize ? 'preview-iframe-shell-positioned' : ''}"
877
913
  class:preview-iframe-shell-symmetrical={showSymmetricalStroke}
878
- style="{enableResize
914
+ style="{enableResize /* rtl-ok: the preview is placed in pixels from the left edge of the stage; the dock of the editor mirrors it */
879
915
  ? `position: absolute; left: ${previewShellLeft}px; top: ${previewShellTop}px; width: ${resizeWidth ? resizeWidth + 'px' : '100%'};`
880
916
  : `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'};"
881
917
  >
@@ -887,18 +923,18 @@
887
923
  onpointerdown={startPreviewDrag}
888
924
  >
889
925
  {#if enableDrag}
890
- <span class="preview-chrome-drag-grip shrink-0 text-gray-400 text-xs leading-none" title="Drag preview" aria-hidden="true">โ ฟ</span>
926
+ <span class="preview-chrome-drag-grip shrink-0 text-gray-400 text-xs leading-none" title={$t('editor.preview.dragHandle')} aria-hidden="true">โ ฟ</span>
891
927
  {/if}
892
- <span class="preview-chrome-title text-[11px] font-semibold uppercase tracking-wide text-gray-500 shrink-0">Preview</span>
928
+ <span class="preview-chrome-title text-[11px] font-semibold uppercase tracking-wide text-gray-500 shrink-0">{$t('editor.preview.title')}</span>
893
929
  {#if showChromeDeviceToggle}
894
930
  <div
895
- class="preview-chrome-device-toggle ml-auto flex shrink-0 items-center overflow-hidden rounded border border-gray-300"
931
+ class="preview-chrome-device-toggle ms-auto flex shrink-0 items-center overflow-hidden rounded border border-gray-300"
896
932
  onpointerdown={(event) => event.stopPropagation()}
897
933
  >
898
934
  <button
899
935
  type="button"
900
936
  class="preview-chrome-device-desktop flex h-6 w-6 items-center justify-center {!deviceMobile ? 'bg-gray-100 text-gray-800' : 'bg-white text-gray-400'} hover:text-gray-800 focus:outline-none focus-visible:outline-none"
901
- aria-label="Desktop preview"
937
+ aria-label={$t('editor.preview.desktop')}
902
938
  aria-pressed={!deviceMobile}
903
939
  onclick={() => dispatch('deviceView', { mobile: false })}
904
940
  onpointerdown={(event) => event.stopPropagation()}
@@ -907,8 +943,8 @@
907
943
  </button>
908
944
  <button
909
945
  type="button"
910
- class="preview-chrome-device-mobile flex h-6 w-6 items-center justify-center border-l border-gray-300 {deviceMobile ? 'bg-gray-100 text-gray-800' : 'bg-white text-gray-400'} hover:text-gray-800 focus:outline-none focus-visible:outline-none"
911
- aria-label="Mobile preview"
946
+ class="preview-chrome-device-mobile flex h-6 w-6 items-center justify-center border-s border-gray-300 {deviceMobile ? 'bg-gray-100 text-gray-800' : 'bg-white text-gray-400'} hover:text-gray-800 focus:outline-none focus-visible:outline-none"
947
+ aria-label={$t('editor.preview.mobile')}
912
948
  aria-pressed={deviceMobile}
913
949
  onclick={() => dispatch('deviceView', { mobile: true })}
914
950
  onpointerdown={(event) => event.stopPropagation()}
@@ -918,9 +954,9 @@
918
954
  </div>
919
955
  {/if}
920
956
  {#if showDimensionInputs}
921
- <div class="preview-chrome-dimensions flex items-center gap-1.5 {showChromeDeviceToggle ? '' : 'ml-auto'}">
957
+ <div class="preview-chrome-dimensions flex items-center gap-1.5 {showChromeDeviceToggle ? '' : 'ms-auto'}">
922
958
  <label class="preview-dim-label flex items-center gap-1 text-[10px] font-semibold text-gray-500">
923
- <span>W</span>
959
+ <span>{$t('editor.preview.widthShort')}</span>
924
960
  <input
925
961
  type="number"
926
962
  min={MIN_WIDTH}
@@ -935,7 +971,7 @@
935
971
  </label>
936
972
  <span class="text-[10px] text-gray-400">ร—</span>
937
973
  <label class="preview-dim-label flex items-center gap-1 text-[10px] font-semibold text-gray-500">
938
- <span>H</span>
974
+ <span>{$t('editor.preview.heightShort')}</span>
939
975
  <input
940
976
  type="number"
941
977
  min={MIN_HEIGHT}
@@ -987,20 +1023,20 @@
987
1023
  <svg class="w-8 h-8 text-gray-300" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="1.5">
988
1024
  <path stroke-linecap="round" stroke-linejoin="round" d="M12 9v3.75m9-.75a9 9 0 11-18 0 9 9 0 0118 0zm-9 3.75h.008v.008H12v-.008z" />
989
1025
  </svg>
990
- <span class="text-sm text-gray-500">{@html errorMessage || 'Failed to load widget.'}</span>
1026
+ <span class="text-sm text-gray-500">{$t('editor.preview.loadFailedPreview')}</span>
991
1027
  </div>
992
1028
  </div>
993
1029
  {:else if mode === 'controlled' && previewError && !isGeneratingCode}
994
1030
  <div class="preview-error-overlay absolute inset-0 z-10 flex h-full flex-col items-center justify-center bg-red-50/95 text-gray-700 p-6">
995
1031
  <div class="text-4xl mb-3">โš ๏ธ</div>
996
- <div class="font-semibold text-lg text-red-600 mb-2">Preview Unavailable</div>
1032
+ <div class="font-semibold text-lg text-red-600 mb-2">{$t('editor.preview.unavailable')}</div>
997
1033
  <div class="text-sm text-center max-w-md whitespace-pre-line text-gray-600 mb-4">{previewError}</div>
998
1034
  <div class="flex gap-3">
999
1035
  <button type="button" onclick={() => dispatch('retry')} class="px-3 py-1.5 text-xs rounded border border-blue-500 text-blue-600 bg-white hover:bg-blue-50 cursor-pointer">
1000
- ๐Ÿ”„ Retry {previewRetryCount > 0 ? `(${previewRetryCount})` : ''}
1036
+ ๐Ÿ”„ {$t('editor.preview.retry')} {previewRetryCount > 0 ? `(${previewRetryCount})` : ''}
1001
1037
  </button>
1002
1038
  <button type="button" onclick={() => dispatch('openInTab')} class="px-3 py-1.5 text-xs rounded border border-gray-300 text-gray-600 bg-white hover:bg-gray-100 cursor-pointer">
1003
- Open in new tab
1039
+ {$t('editor.preview.openInNewTab')}
1004
1040
  </button>
1005
1041
  </div>
1006
1042
  </div>
@@ -1008,21 +1044,17 @@
1008
1044
 
1009
1045
  {#if showStandaloneLoading}
1010
1046
  <div class="widget-preview-loading-ct absolute inset-0 z-10 flex items-center justify-center bg-gray-50/80">
1011
- <GenerateLoader title="Loading widgetโ€ฆ" subtitle="" size="md" />
1047
+ <GenerateLoader title={$t('editor.preview.loadingWidget')} subtitle="" size="md" />
1012
1048
  </div>
1013
1049
  {:else if showControlledLoading && !isGeneratingCode}
1014
1050
  <div class="loading-overlay absolute inset-0 z-20 flex items-center justify-center bg-gray-50">
1015
1051
  <GenerateLoader
1016
- title={!src && isLoadingUrl
1017
- ? 'Loading Previewโ€ฆ'
1018
- : previewHydrating
1019
- ? 'Loading Previewโ€ฆ'
1020
- : 'Loading Previewโ€ฆ'}
1052
+ title={$t('editor.preview.loadingPreview')}
1021
1053
  subtitle={!src && isLoadingUrl
1022
- ? 'Checking widget on Workerโ€ฆ'
1054
+ ? $t('editor.preview.checkingWidget')
1023
1055
  : previewHydrating
1024
- ? 'Applying composition settingsโ€ฆ'
1025
- : 'Connecting to the widget previewโ€ฆ'}
1056
+ ? $t('editor.preview.applyingSettings')
1057
+ : $t('editor.preview.connecting')}
1026
1058
  size="lg"
1027
1059
  />
1028
1060
  </div>
@@ -1033,17 +1065,17 @@
1033
1065
  <div class="text-5xl mb-4">โš ๏ธ</div>
1034
1066
  <div class="font-semibold text-xl mb-2">
1035
1067
  {previewError && /TypeError|Uncaught|is not a function|is not defined/i.test(previewError)
1036
- ? 'Widget runtime error'
1037
- : 'Code Has Compilation Errors'}
1068
+ ? $t('editor.preview.runtimeError')
1069
+ : $t('editor.preview.compilationErrors')}
1038
1070
  </div>
1039
1071
  <div class="text-sm text-center text-red-600 mb-4 max-w-md whitespace-pre-line">
1040
1072
  {previewError
1041
- || `The generated code has syntax or compilation errors (HTTP ${previewRuntimeErrorStatus}).`}
1073
+ || $t('editor.preview.compilationErrorsDetail', { status: previewRuntimeErrorStatus ?? '?' })}
1042
1074
  </div>
1043
1075
  <div class="flex gap-3 flex-wrap justify-center">
1044
- <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>
1045
- <button type="button" onclick={() => dispatch('viewErrors')} class="px-3 py-1.5 text-xs rounded border border-orange-400 text-orange-600 bg-white hover:bg-orange-50 cursor-pointer">๐Ÿ“‹ View Errors</button>
1046
- <button type="button" onclick={() => dispatch('retryPreview')} class="px-3 py-1.5 text-xs rounded border border-red-300 text-red-600 bg-white hover:bg-red-50 cursor-pointer">๐Ÿ”„ Retry Preview</button>
1076
+ <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">๐Ÿ”ง {$t('editor.preview.fixErrors')}</button>
1077
+ <button type="button" onclick={() => dispatch('viewErrors')} class="px-3 py-1.5 text-xs rounded border border-orange-400 text-orange-600 bg-white hover:bg-orange-50 cursor-pointer">๐Ÿ“‹ {$t('editor.preview.viewErrors')}</button>
1078
+ <button type="button" onclick={() => dispatch('retryPreview')} class="px-3 py-1.5 text-xs rounded border border-red-300 text-red-600 bg-white hover:bg-red-50 cursor-pointer">๐Ÿ”„ {$t('editor.preview.retryPreview')}</button>
1047
1079
  </div>
1048
1080
  </div>
1049
1081
  {/if}
@@ -1057,9 +1089,13 @@
1057
1089
  src={mountedPreviewSrc}
1058
1090
  class="widget-preview-iframe h-full w-full border-0"
1059
1091
  tabindex="-1"
1060
- sandbox={mode === 'controlled' ? undefined : 'allow-scripts allow-popups allow-forms allow-same-origin'}
1092
+ sandbox={sandbox !== undefined
1093
+ ? (sandbox ?? undefined)
1094
+ : mode === 'controlled'
1095
+ ? undefined
1096
+ : 'allow-scripts allow-popups allow-forms allow-same-origin'}
1061
1097
  loading="eager"
1062
- allow="clipboard-read; clipboard-write"
1098
+ {allow}
1063
1099
  onload={handleIframeLoad}
1064
1100
  onerror={handleIframeError}
1065
1101
  ></iframe>
@@ -1071,7 +1107,7 @@
1071
1107
  {/if}
1072
1108
 
1073
1109
  {#if mode === 'controlled' && isGeneratingCode}
1074
- <GenerateLoader overlay title="Generating codeโ€ฆ" subtitle="" size="lg" />
1110
+ <GenerateLoader overlay title={$t('editor.preview.generatingCode')} subtitle="" size="lg" />
1075
1111
  {/if}
1076
1112
  </div>
1077
1113
  </div>
@@ -1081,22 +1117,22 @@
1081
1117
  {:else if mode === 'controlled'}
1082
1118
  {#if isLoadingUrl}
1083
1119
  <div class="flex h-full flex-col items-center justify-center p-6">
1084
- <GenerateLoader title="Loading Previewโ€ฆ" subtitle="" size="lg" />
1120
+ <GenerateLoader title={$t('editor.preview.loadingPreview')} subtitle="" size="lg" />
1085
1121
  </div>
1086
1122
  {:else}
1087
1123
  <div class="flex h-full flex-col items-center justify-center p-6 text-center text-sm text-gray-600">
1088
1124
  {#if isGeneratingCode}
1089
- <GenerateLoader title="Generating codeโ€ฆ" subtitle="" size="lg" />
1125
+ <GenerateLoader title={$t('editor.preview.generatingCode')} subtitle="" size="lg" />
1090
1126
  {:else if previewError === 'NO_CODE_YET'}
1091
- <div class="font-semibold text-lg text-gray-700 mb-2">No Code Generated Yet</div>
1127
+ <div class="font-semibold text-lg text-gray-700 mb-2">{$t('editor.preview.noCodeYet')}</div>
1092
1128
  {:else if previewError}
1093
- <div class="font-semibold text-lg text-red-600 mb-2">Preview Unavailable</div>
1129
+ <div class="font-semibold text-lg text-red-600 mb-2">{$t('editor.preview.unavailable')}</div>
1094
1130
  <div class="text-sm text-gray-600 mb-4">{previewError}</div>
1095
- <button type="button" onclick={() => dispatch('retry')} class="px-3 py-1.5 text-xs rounded border border-blue-500 text-blue-600 bg-white hover:bg-blue-50 cursor-pointer">๐Ÿ”„ Retry Build</button>
1131
+ <button type="button" onclick={() => dispatch('retry')} class="px-3 py-1.5 text-xs rounded border border-blue-500 text-blue-600 bg-white hover:bg-blue-50 cursor-pointer">๐Ÿ”„ {$t('editor.preview.retryBuild')}</button>
1096
1132
  {:else if previewLoading}
1097
- <div class="text-lg font-medium text-gray-700">Building Preview...</div>
1133
+ <div class="text-lg font-medium text-gray-700">{$t('editor.preview.building')}</div>
1098
1134
  {:else}
1099
- <div class="text-lg text-gray-600 mb-2">๐Ÿ–ผ๏ธ No Preview Available</div>
1135
+ <div class="text-lg text-gray-600 mb-2">๐Ÿ–ผ๏ธ {$t('editor.preview.noPreview')}</div>
1100
1136
  {/if}
1101
1137
  </div>
1102
1138
  {/if}
@@ -1140,10 +1176,11 @@
1140
1176
  box-shadow: none;
1141
1177
  }
1142
1178
 
1143
- .preview-resize-handle-tl { top: -5px; left: -5px; cursor: nwse-resize; }
1144
- .preview-resize-handle-tr { top: -5px; right: -5px; cursor: nesw-resize; }
1145
- .preview-resize-handle-bl { bottom: -5px; left: -5px; cursor: nesw-resize; }
1146
- .preview-resize-handle-br { bottom: -5px; right: -5px; cursor: nwse-resize; }
1179
+ /* The four corner handles of the preview are on the four corners of the box, a place on the screen: the set is the same in both directions. */
1180
+ .preview-resize-handle-tl { top: -5px; left: -5px; cursor: nwse-resize; } /* rtl-ok: top left corner */
1181
+ .preview-resize-handle-tr { top: -5px; right: -5px; cursor: nesw-resize; } /* rtl-ok: top right corner */
1182
+ .preview-resize-handle-bl { bottom: -5px; left: -5px; cursor: nesw-resize; } /* rtl-ok: bottom left corner */
1183
+ .preview-resize-handle-br { bottom: -5px; right: -5px; cursor: nwse-resize; } /* rtl-ok: bottom right corner */
1147
1184
 
1148
1185
  .preview-resize-handle:hover {
1149
1186
  background: #eff6ff;
@@ -34,6 +34,13 @@ declare const WidgetPreview: $$__sveltets_2_IsomorphicComponent<{
34
34
  forceMockReady?: boolean;
35
35
  debugMode?: boolean;
36
36
  iframeReloadKey?: number;
37
+ /**
38
+ * The iframe's `sandbox` tokens. `undefined` keeps what each mode always had (controlled: no sandbox; standalone:
39
+ * scripts, popups, forms and its own origin); a string is used exactly as given; `null` means no `sandbox` at all.
40
+ * A page that embeds the editor for its own visitors passes a list without `allow-top-navigation`, so that a
41
+ * widget's code cannot send the visitor's whole tab somewhere else.
42
+ */ sandbox?: string | null | undefined;
43
+ /** The iframe's `allow` attribute: which of the page's permissions the preview may use. */ allow?: string;
37
44
  width?: string;
38
45
  height?: string;
39
46
  contentHeight?: string;
@@ -82,6 +89,7 @@ declare const WidgetPreview: $$__sveltets_2_IsomorphicComponent<{
82
89
  resize: CustomEvent<{
83
90
  width: number;
84
91
  height: number;
92
+ source?: "drag" | "input" | "fit" | "device" | "clamp";
85
93
  }>;
86
94
  mobileViewport: CustomEvent<{
87
95
  width: number;
@@ -173,8 +173,10 @@ $: if (rgb && typeof a === 'number') {
173
173
 
174
174
  <!-- svelte-ignore a11y-no-noninteractive-tabindex -->
175
175
  <!-- svelte-ignore a11y-no-static-element-interactions -->
176
+ <!-- The picker is a drawing: saturation grows to the right and hue is a strip next to it, on every screen, so it is a left to right island. -->
176
177
  <div
177
178
  class={$$props.class}
179
+ dir="ltr"
178
180
  tabindex="0"
179
181
  style="width: {width}px; height: {height}px;"
180
182
  on:keydown={onKeydown}
@@ -184,8 +186,7 @@ $: if (rgb && typeof a === 'number') {
184
186
  id="{instanceId}_sv"
185
187
  width={pickerSize}
186
188
  height={pickerSize}
187
- style:top="{border_size}px"
188
- style:left="{border_size}px"
189
+ style="top: {border_size}px; left: {border_size}px; /* rtl-ok: the saturation square is drawn from the left edge of the picker */"
189
190
  use:pointer={update_sv}
190
191
  use:render_main_image={color.h}
191
192
  />
@@ -193,8 +194,7 @@ $: if (rgb && typeof a === 'number') {
193
194
  <canvas
194
195
  width={sliderWidth}
195
196
  height={pickerSize}
196
- style:top="{border_size}px"
197
- style:left="{pickerSize + gap_size}px"
197
+ style="top: {border_size}px; left: {pickerSize + gap_size}px; /* rtl-ok: the hue strip is drawn next to the square, from the left edge of the picker */"
198
198
  use:pointer={update_h}
199
199
  use:render_slider_image
200
200
  />