@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
@@ -55,6 +55,11 @@
55
55
  import { SearchInput } from '@widgetic/design-system/components';
56
56
  import CategoryTabs from './shared/CategoryTabs.svelte';
57
57
  import { RecentItemsSection, ItemGrid } from './shared';
58
+ import { useEditorLanguage } from '../../i18n/language';
59
+ import { categoryLabel } from '../../i18n/labels';
60
+
61
+ // The words of the editor in the language of the page.
62
+ const { t } = useEditorLanguage();
58
63
 
59
64
  // Utils imports
60
65
  import {
@@ -66,6 +71,9 @@
66
71
  } from '../../utils/vectorUtils';
67
72
  import type { VectorData } from '../../utils/VectorManager';
68
73
  import VectorManager from '../../utils/VectorManager';
74
+ import { escapeHtml } from '../../utils/htmlSafety';
75
+ import { listenForScroll, queryInTreeOf } from '../../utils/shadowDom';
76
+ import { readEditorStorageValue, writeEditorStorageValue } from '../../utils/editorStorage';
69
77
 
70
78
  // Constants
71
79
  const VECTOR_FILTER_KEY = (id: string) => `vector-specific-filter-state-${id}`;
@@ -109,6 +117,8 @@
109
117
 
110
118
  // Vector loading optimization
111
119
  let vectorListContainer: HTMLElement;
120
+ // The control's own element: it says which tree (the page or a module's shadow root) to listen for scrolling in
121
+ let controllerElement: HTMLDivElement | undefined;
112
122
  let vectorObserver = new IntersectionObserver(() => {});
113
123
 
114
124
  // Initialize VectorManager instance
@@ -175,6 +185,15 @@
175
185
  `;
176
186
  }
177
187
 
188
+ /**
189
+ * The sticky header of the vector list. It is looked up in the tree the list lives in (inside a module that is a
190
+ * shadow root, which `document.querySelector` cannot see into), and the id is escaped so that one with dots or
191
+ * brackets in it is found too.
192
+ */
193
+ function findStickyHeader(): Element | null {
194
+ return queryInTreeOf(vectorListContainer, `#${CSS.escape(`vector-header-${controllerId}`)}`);
195
+ }
196
+
178
197
  // Function to update scroll state
179
198
  function updateScrollState() {
180
199
  if (vectorListContainer) {
@@ -183,7 +202,7 @@
183
202
 
184
203
  // If we're transitioning from scrolled to not scrolled, remove transition
185
204
  if (wasScrolled && !isScrolled) {
186
- const stickyHeader = document.querySelector(`#vector-header-${controllerId}`);
205
+ const stickyHeader = findStickyHeader();
187
206
  if (stickyHeader) {
188
207
  stickyHeader.classList.remove('transition-border');
189
208
  // Force a reflow to ensure the transition class removal takes effect
@@ -191,7 +210,7 @@
191
210
  }
192
211
  } else if (!wasScrolled && isScrolled) {
193
212
  // Add transition class when starting to scroll
194
- const stickyHeader = document.querySelector(`#vector-header-${controllerId}`);
213
+ const stickyHeader = findStickyHeader();
195
214
  if (stickyHeader) {
196
215
  stickyHeader.classList.add('transition-border');
197
216
  }
@@ -314,31 +333,14 @@
314
333
  await handleSearch(event);
315
334
  }, 300); // 300ms debounce time
316
335
 
317
- // Setup global error handler for SVG loading errors
318
- const vectorErrorHandler = (event: Event) => {
319
- if (event instanceof CustomEvent && event.detail) {
320
- const vectorName = event.detail;
321
- // Check if we can get the vector URL from allVectors
322
- const vectorData = allVectors.find(v => v.name === vectorName);
323
- const vectorUrl = vectorData?.url || 'unknown';
324
-
325
- console.error(`❌ [VectorInput] Failed to load SVG: "${vectorName}" from URL: ${vectorUrl}`);
326
- svgLoadErrors.set(vectorName, true);
327
- // Force component update
328
- svgLoadErrors = new Map(svgLoadErrors);
329
- }
330
- };
331
-
332
- // Add the event listener
333
- document.addEventListener('vector-load-error', vectorErrorHandler);
334
-
335
- // Store the handler reference for cleanup in onDestroy
336
- (window as any)['vectorErrorHandler'] = vectorErrorHandler;
336
+ // SVG loading errors are noticed by the watchVectorImageErrors action on the list of pictures. They used to be
337
+ // announced on `document` by an inline onerror handler and caught here; with the editor inside a partner's page
338
+ // that was an event on the partner's document and a handler stored on the partner's `window`.
337
339
 
338
340
  try {
339
341
  if (browser) {
340
342
  // Load saved filter
341
- const savedFilter = localStorage.getItem(VECTOR_FILTER_KEY(controllerId));
343
+ const savedFilter = readEditorStorageValue(VECTOR_FILTER_KEY(controllerId));
342
344
  if (savedFilter) {
343
345
  currentFilter = savedFilter as FilterType;
344
346
  }
@@ -493,7 +495,7 @@
493
495
  lastActiveFilter = currentFilter;
494
496
  currentFilter = 'All'; // Reset to All when starting a search
495
497
  if (browser) {
496
- localStorage.setItem(VECTOR_FILTER_KEY(controllerId), 'All');
498
+ writeEditorStorageValue(VECTOR_FILTER_KEY(controllerId), 'All');
497
499
  }
498
500
  }
499
501
 
@@ -970,16 +972,7 @@
970
972
  }
971
973
 
972
974
  onDestroy(() => {
973
- window.removeEventListener('scroll', handleGlobalScroll, true);
974
- window.removeEventListener('touchstart', () => {});
975
- window.removeEventListener('touchmove', () => {});
976
-
977
- // Remove vector load error listener using the stored reference
978
- const vectorErrorHandler = (window as any)['vectorErrorHandler'];
979
- if (vectorErrorHandler) {
980
- document.removeEventListener('vector-load-error', vectorErrorHandler);
981
- delete (window as any)['vectorErrorHandler'];
982
- }
975
+ stopListeningForScroll();
983
976
 
984
977
  if (vectorObserver) {
985
978
  vectorObserver.disconnect();
@@ -1030,7 +1023,7 @@
1030
1023
  lastActiveFilter = filter; // Remember the last active filter
1031
1024
 
1032
1025
  if (browser) {
1033
- localStorage.setItem(VECTOR_FILTER_KEY(controllerId), filter);
1026
+ writeEditorStorageValue(VECTOR_FILTER_KEY(controllerId), filter);
1034
1027
  }
1035
1028
 
1036
1029
  if (vectorManagerInstance) {
@@ -1104,25 +1097,26 @@
1104
1097
  }
1105
1098
 
1106
1099
  /**
1107
- * Highlights search term matches in vector names
1100
+ * Highlights search term matches in vector names.
1101
+ * The result is meant for {@html}, so every piece of the name is escaped as it is put in.
1108
1102
  */
1109
1103
  function highlightMatch(name: string, term: string) {
1110
- if (!term) return name;
1104
+ if (!term) return escapeHtml(name);
1111
1105
  const nameLower = name.toLowerCase();
1112
1106
  const termLower = term.toLowerCase().trim();
1113
1107
 
1114
1108
  // If the name contains the search term, highlight that part in blue
1115
1109
  if (termLower && nameLower.includes(termLower)) {
1116
1110
  const index = nameLower.indexOf(termLower);
1117
- const beforeMatch = name.slice(0, index);
1118
- const match = name.slice(index, index + termLower.length);
1119
- const afterMatch = name.slice(index + termLower.length);
1111
+ const beforeMatch = escapeHtml(name.slice(0, index));
1112
+ const match = escapeHtml(name.slice(index, index + termLower.length));
1113
+ const afterMatch = escapeHtml(name.slice(index + termLower.length));
1120
1114
 
1121
1115
  return `<span style="color: hsl(var(--grey7))">${beforeMatch}<span style="color: #45BCFE">${match}</span>${afterMatch}</span>`;
1122
1116
  }
1123
1117
 
1124
1118
  // Default case - just return the name in grey7 color
1125
- return `<span style="color: hsl(var(--grey7))">${name}</span>`;
1119
+ return `<span style="color: hsl(var(--grey7))">${escapeHtml(name)}</span>`;
1126
1120
  }
1127
1121
 
1128
1122
 
@@ -1173,15 +1167,16 @@
1173
1167
  }
1174
1168
  }
1175
1169
 
1176
- // Add scroll listener with passive option
1177
- window.addEventListener('scroll', handleGlobalScroll, {
1178
- capture: true,
1179
- passive: true
1170
+ // Add scroll listener with passive option. It listens to the page and, inside a module, to the module's own panels
1171
+ // (a scroll never leaves a shadow root, so a listener on the window alone does not hear about those).
1172
+ let stopListeningForScroll: () => void = () => {};
1173
+ onMount(() => {
1174
+ stopListeningForScroll = listenForScroll(controllerElement, handleGlobalScroll, { passive: true });
1180
1175
  });
1181
1176
 
1182
- // Add touch listeners with passive option
1183
- window.addEventListener('touchstart', () => {}, { passive: true });
1184
- window.addEventListener('touchmove', () => {}, { passive: true });
1177
+ // (Two touchstart/touchmove listeners that did nothing used to be added to the window here, and could never be
1178
+ // removed: each input left two more on the customer's page every time it was shown. They did nothing, so they are
1179
+ // gone.)
1185
1180
 
1186
1181
  // Add to VectorInputController.svelte
1187
1182
  function handleToggle() {
@@ -1299,6 +1294,37 @@
1299
1294
  removeVector(vector.name);
1300
1295
  }
1301
1296
 
1297
+ /**
1298
+ * Svelte action for the list that holds the vector pictures: notices a picture that does not load and shows the
1299
+ * "Failed to load" state for that vector. `error` events do not bubble, so it listens in the capture phase; a
1300
+ * picture is found by its data-vector-name (see renderVectorItem). It replaces an inline onerror attribute that
1301
+ * reported through `document`, which is the partner's page when the editor runs inside one.
1302
+ */
1303
+ function watchVectorImageErrors(node: HTMLElement) {
1304
+ const onImageError = (event: Event) => {
1305
+ const image = event.target;
1306
+ if (!(image instanceof HTMLImageElement)) return;
1307
+ const vectorName = image.dataset.vectorName;
1308
+ if (!vectorName) return;
1309
+ image.style.display = 'none';
1310
+ // Reported once per vector, however many pictures of it are on screen.
1311
+ if (hasSvgLoadError(vectorName)) return;
1312
+ const vectorData = allVectors.find(vector => vector.name === vectorName);
1313
+ console.error(`❌ [VectorInput] Failed to load SVG: "${vectorName}" from URL: ${vectorData?.url || image.src || 'unknown'}`);
1314
+ svgLoadErrors.set(vectorName, true);
1315
+ // Force component update
1316
+ svgLoadErrors = new Map(svgLoadErrors);
1317
+ };
1318
+
1319
+ node.addEventListener('error', onImageError, true);
1320
+
1321
+ return {
1322
+ destroy() {
1323
+ node.removeEventListener('error', onImageError, true);
1324
+ }
1325
+ };
1326
+ }
1327
+
1302
1328
  // Create a Svelte action to handle image error events
1303
1329
  function handleImageError(node: HTMLElement, vectorName: string) {
1304
1330
  const errorHandler = (event: Event) => {
@@ -1345,15 +1371,19 @@
1345
1371
  };
1346
1372
  }
1347
1373
 
1374
+ // The result is used as HTML by RecentItemsSection and ItemGrid: the name and the address of a vector (a custom
1375
+ // one is named after the person's own file) are escaped as they are put in. A picture that does not load is
1376
+ // noticed by watchVectorImageErrors, which finds it by data-vector-name; an inline onerror="..." would not run
1377
+ // under a strict Content-Security-Policy.
1348
1378
  return {
1349
1379
  html: `
1350
1380
  <div class="flex items-center justify-center w-full h-full vector-img-container">
1351
1381
  <img
1352
- src="${vector.url}"
1353
- alt="${vector.name}"
1382
+ src="${escapeHtml(vector.url)}"
1383
+ alt="${escapeHtml(vector.name)}"
1384
+ data-vector-name="${escapeHtml(vector.name)}"
1354
1385
  class="w-[72px] h-[72px] object-contain"
1355
1386
  draggable="false"
1356
- onerror="this.style.display='none'; document.dispatchEvent(new CustomEvent('vector-load-error', {detail: this.alt, bubbles: true})); console.error('❌ [VectorInput] Vector failed to load:', this.alt, 'URL:', this.src);"
1357
1387
  />
1358
1388
  </div>
1359
1389
  `
@@ -1399,7 +1429,7 @@
1399
1429
  lastActiveFilter = currentFilter;
1400
1430
  currentFilter = 'All'; // Reset to All when starting a search
1401
1431
  if (browser) {
1402
- localStorage.setItem(VECTOR_FILTER_KEY(controllerId), 'All');
1432
+ writeEditorStorageValue(VECTOR_FILTER_KEY(controllerId), 'All');
1403
1433
  }
1404
1434
  }
1405
1435
 
@@ -1472,16 +1502,16 @@
1472
1502
  }
1473
1503
  </script>
1474
1504
 
1475
- <div class="flex flex-col gap-2 widgetic-colors">
1505
+ <div bind:this={controllerElement} class="flex flex-col gap-2 widgetic-colors">
1476
1506
  <div class="flex items-center w-full relative">
1477
- <div class="w-[185px] flex-shrink-0 ml-auto">
1507
+ <div class="w-[185px] flex-shrink-0 ms-auto">
1478
1508
  <InputControllerHeader
1479
1509
  label={label}
1480
1510
  helpText={description}
1481
1511
  />
1482
1512
  </div>
1483
1513
 
1484
- <div class="absolute right-0">
1514
+ <div class="absolute end-0">
1485
1515
  <div
1486
1516
  class="h-[48px] w-[185px] rounded-large border border-grey2 flex items-center bg-grey1 transition-colors duration-200 hover:border-forest-green"
1487
1517
  onfocusin={onFocus}
@@ -1527,7 +1557,7 @@
1527
1557
  }}
1528
1558
  >
1529
1559
  <Popover.Trigger
1530
- class="w-full h-full flex items-center border-0 bg-transparent hover:bg-transparent focus:bg-transparent pl-3 pr-[16px] group"
1560
+ class="w-full h-full flex items-center border-0 bg-transparent hover:bg-transparent focus:bg-transparent ps-3 pe-[16px] group"
1531
1561
  onclick={handleButtonClick}
1532
1562
  onkeydown={handleKeyDown}
1533
1563
  onmouseenter={() => {
@@ -1552,8 +1582,8 @@
1552
1582
  <path d="M12 17h.01"></path>
1553
1583
  </svg>
1554
1584
  </div>
1555
- <span class="truncate text-left capitalize subtext text-grey7">
1556
- {value?.name || "Select vector..."}
1585
+ <span class="truncate text-start capitalize subtext text-grey7">
1586
+ {value?.name || $t('editor.vector.select')}
1557
1587
  </span>
1558
1588
  {:else}
1559
1589
  <!-- Use a simple img tag with Svelte action for error handling -->
@@ -1563,13 +1593,13 @@
1563
1593
  class="w-5 h-5 object-contain"
1564
1594
  use:handleImageError={value.name}
1565
1595
  />
1566
- <span class="truncate text-left capitalize subtext text-grey7">
1567
- {value?.name || "Select vector..."}
1596
+ <span class="truncate text-start capitalize subtext text-grey7">
1597
+ {value?.name || $t('editor.vector.select')}
1568
1598
  </span>
1569
1599
  {/if}
1570
1600
  {:else}
1571
- <span class="truncate text-left subtext text-grey7">
1572
- Select vector...
1601
+ <span class="truncate text-start subtext text-grey7">
1602
+ {$t('editor.vector.select')}
1573
1603
  </span>
1574
1604
  {/if}
1575
1605
  </div>
@@ -1580,10 +1610,10 @@
1580
1610
  <div class="flex flex-col h-full vector-popover-content">
1581
1611
  <!-- Title and upload icon -->
1582
1612
  <div class="relative pt-6 pb-[8px] px-4">
1583
- <div class="h5 text-grey7 widgetic-colors">Vectors</div>
1613
+ <div class="h5 text-grey7 widgetic-colors">{$t('editor.vector.title')}</div>
1584
1614
  <button
1585
1615
  type="button"
1586
- class="absolute right-6 top-[26px] bg-transparent border-0 cursor-pointer p-0 rounded-full hover:bg-grey3 transition-all duration-300"
1616
+ class="absolute end-6 top-[26px] bg-transparent border-0 cursor-pointer p-0 rounded-full hover:bg-grey3 transition-all duration-300"
1587
1617
  onclick={() => {
1588
1618
  try {
1589
1619
  openFileBrowser();
@@ -1603,7 +1633,7 @@
1603
1633
  <!-- Search input -->
1604
1634
  <div class="relative px-4">
1605
1635
  <SearchInput
1606
- placeholder="Search vectors..."
1636
+ placeholder={$t('editor.vector.search')}
1607
1637
  bind:value={searchTerm}
1608
1638
  onInput={(e: Event) => {
1609
1639
  // Use debounced search for better performance
@@ -1617,7 +1647,7 @@
1617
1647
  // Restore the previous filter when clearing search
1618
1648
  currentFilter = lastActiveFilter;
1619
1649
  if (browser) {
1620
- localStorage.setItem(VECTOR_FILTER_KEY(controllerId), lastActiveFilter);
1650
+ writeEditorStorageValue(VECTOR_FILTER_KEY(controllerId), lastActiveFilter);
1621
1651
  }
1622
1652
 
1623
1653
  // Reset batch index to start fresh
@@ -1682,6 +1712,7 @@
1682
1712
  categories={filterOptions}
1683
1713
  activeCategory={currentFilter}
1684
1714
  onCategorySelect={handleFilterChange}
1715
+ getCategoryLabel={(category) => categoryLabel(category, $t)}
1685
1716
  filterVisible={!searchTerm}
1686
1717
  />
1687
1718
  </div>
@@ -1691,6 +1722,7 @@
1691
1722
  <div
1692
1723
  class="flex-1 {filteredVectors.length > 0 ? 'overflow-y-auto' : 'overflow-hidden'} min-h-0 vector-list-container"
1693
1724
  bind:this={vectorListContainer}
1725
+ use:watchVectorImageErrors
1694
1726
  onscroll={(e: Event) => {
1695
1727
  e.stopPropagation();
1696
1728
  handleVectorListScroll(e);
@@ -1698,14 +1730,13 @@
1698
1730
  }}
1699
1731
  id={`vector-list-${controllerId}`}
1700
1732
  role="listbox"
1701
- aria-label="Vector list"
1733
+ aria-label={$t('editor.vector.list')}
1702
1734
  >
1703
1735
  <div class="space-y-0">
1704
1736
  <!-- Recently used section - only show when not searching -->
1705
1737
  {#if recentVectors.length > 0 && !searchTerm}
1706
1738
  <RecentItemsSection
1707
1739
  recentItems={recentVectors}
1708
- title="Recently used"
1709
1740
  onItemSelect={handleVectorSelect}
1710
1741
  onItemDelete={handleVectorDelete}
1711
1742
  isItemSelected={isVectorSelected}
@@ -1724,7 +1755,7 @@
1724
1755
  {#if filteredVectors.length > 0}
1725
1756
  <div id="vector-header-{controllerId}" class="sticky top-0 -mt-2 bg-white z-10 pt-2 {isScrolled ? 'border-b border-grey2' : 'border-b border-transparent'} transition-border">
1726
1757
  <h3 class="subtext text-grey7 px-4 pb-2">
1727
- {searchTerm ? 'Search results' : 'More'}
1758
+ {searchTerm ? $t('editor.list.searchResults') : $t('editor.list.more')}
1728
1759
  </h3>
1729
1760
  </div>
1730
1761
  {/if}
@@ -1732,7 +1763,7 @@
1732
1763
  {#if filteredVectors.length === 0}
1733
1764
  <div class="empty-state-wrapper">
1734
1765
  <EmptyState
1735
- message={searchTerm ? 'No vectors found' : 'No vectors available'}
1766
+ message={searchTerm ? $t('editor.vector.noneFound') : $t('editor.vector.noneAvailable')}
1736
1767
  height="auto"
1737
1768
  />
1738
1769
  </div>
@@ -8,9 +8,17 @@
8
8
  * - Handles item selection
9
9
  * - Supports custom item rendering
10
10
  * - Optimized with intersection observer
11
+ *
12
+ * `renderItem` may return a plain string (shown as text) or `{ html }`. The `html` is put on the page as it is, so the
13
+ * function that makes it must escape everything that comes from outside (names, addresses): see escapeHtml.
11
14
  -->
12
15
  <script lang="ts">
13
16
  import { Check } from '@widgetic/design-system/icons';
17
+ import { escapeHtml } from '../../../utils/htmlSafety';
18
+ import { useEditorLanguage } from '../../../i18n/language';
19
+
20
+ // The words of the editor in the language of the page.
21
+ const { t } = useEditorLanguage();
14
22
 
15
23
  export let items: any[] = [];
16
24
  export let containerRef: (node: HTMLElement) => void;
@@ -41,7 +49,7 @@
41
49
  function renderContent(item: any) {
42
50
  const rendered = renderItem(item);
43
51
  if (typeof rendered === 'string') {
44
- return `<span>${rendered}</span>`;
52
+ return `<span>${escapeHtml(rendered)}</span>`;
45
53
  }
46
54
 
47
55
  if (rendered && typeof rendered === 'object' && 'html' in rendered) {
@@ -113,7 +121,7 @@
113
121
  data-item-type="font"
114
122
  >
115
123
  <div class="flex items-center flex-1 min-w-0 group">
116
- <Check class="mr-2 h-6 w-6 flex-shrink-0 {isItemSelected(item) ? 'text-white' : 'text-sage-green opacity-0'}"/>
124
+ <Check class="me-2 h-6 w-6 flex-shrink-0 {isItemSelected(item) ? 'text-white' : 'text-sage-green opacity-0'}"/>
117
125
  <div class="flex-1 overflow-hidden">
118
126
  {@html renderContent(item)}
119
127
  </div>
@@ -132,7 +140,7 @@
132
140
  data-item-type="font"
133
141
  >
134
142
  <div class="flex items-center flex-1 min-w-0 group">
135
- <Check class="mr-2 h-6 w-6 flex-shrink-0 {isItemSelected(item) ? 'text-white' : 'text-sage-green opacity-0'}"/>
143
+ <Check class="me-2 h-6 w-6 flex-shrink-0 {isItemSelected(item) ? 'text-white' : 'text-sage-green opacity-0'}"/>
136
144
  <div class="flex-1 overflow-hidden">
137
145
  {@html renderContent(item)}
138
146
  </div>
@@ -145,7 +153,7 @@
145
153
 
146
154
  {#if loadingMore}
147
155
  <div class="py-4 subtext text-center text-grey5">
148
- Loading more items...
156
+ {$t('editor.list.loadingMore')}
149
157
  </div>
150
158
  {/if}
151
159
  </div>
@@ -13,9 +13,14 @@
13
13
  // Import Check icon from design system
14
14
  import { Check } from '@widgetic/design-system/icons';
15
15
  import icTrash from '../../../assets/icons/ic-browser/trash.svg?url';
16
-
16
+ import { useEditorLanguage } from '../../../i18n/language';
17
+
18
+ // The words of the editor in the language of the page.
19
+ const { t } = useEditorLanguage();
20
+
17
21
  export let recentItems: any[] = [];
18
- export let title = "Recently used";
22
+ /** The title of the section. Without one, the editor's own word, in the language of the page. */
23
+ export let title: string | undefined = undefined;
19
24
  export let onItemSelect: (item: any) => void;
20
25
  export let onItemDelete: (item: any, event: Event) => void;
21
26
  export let isItemSelected: (item: any) => boolean;
@@ -36,7 +41,7 @@
36
41
 
37
42
  {#if recentItems.length > 0}
38
43
  <div class="px-4 pt-4 pb-4 widgetic-colors">
39
- <h5 class="subtext text-grey7">{title}</h5>
44
+ <h5 class="subtext text-grey7">{title ?? $t('editor.list.recentlyUsed')}</h5>
40
45
 
41
46
  {#if displayMode === 'list'}
42
47
  <div class="grid grid-cols-1">
@@ -59,7 +64,7 @@
59
64
  data-item={getItemKey(item)}
60
65
  >
61
66
  <div class="flex items-center flex-1 min-w-0 group">
62
- <Check class="mr-2 h-6 w-6 flex-shrink-0 {selected ? 'text-white' : 'text-sage-green opacity-0'}"/>
67
+ <Check class="me-2 h-6 w-6 flex-shrink-0 {selected ? 'text-white' : 'text-sage-green opacity-0'}"/>
63
68
  <div class="flex-1 overflow-hidden">
64
69
  {#if typeof renderedItem === 'string'}
65
70
  <span class="text-[24px]">{renderedItem}</span>
@@ -78,7 +83,7 @@
78
83
  onItemDelete(item, e);
79
84
  }}
80
85
  >
81
- <img src={icTrash} alt="Delete" class="w-4 h-4" />
86
+ <img src={icTrash} alt={$t('editor.list.deleteItem')} class="w-4 h-4" />
82
87
  </button>
83
88
  {/if}
84
89
  </div>
@@ -97,7 +102,7 @@
97
102
  data-item={getItemKey(item)}
98
103
  >
99
104
  <div class="flex items-center flex-1 min-w-0 group">
100
- <Check class="mr-2 h-6 w-6 flex-shrink-0 {selected ? 'text-white' : 'text-sage-green opacity-0'}"/>
105
+ <Check class="me-2 h-6 w-6 flex-shrink-0 {selected ? 'text-white' : 'text-sage-green opacity-0'}"/>
101
106
  <div class="flex-1 overflow-hidden">
102
107
  {#if typeof renderedItem === 'string'}
103
108
  <span class="text-[24px]">{renderedItem}</span>
@@ -116,7 +121,7 @@
116
121
  onItemDelete(item, e);
117
122
  }}
118
123
  >
119
- <img src={icTrash} alt="Delete" class="w-4 h-4" />
124
+ <img src={icTrash} alt={$t('editor.list.deleteItem')} class="w-4 h-4" />
120
125
  </button>
121
126
  {/if}
122
127
  </div>
@@ -157,14 +162,14 @@
157
162
  {#if isCustomItem(item) && !hideTrashIcon}
158
163
  <button
159
164
  type="button"
160
- class="absolute top-1 right-1 p-1 rounded-full hover:bg-red-100 hover:text-red-500 transition-colors bg-white"
165
+ class="absolute top-1 end-1 p-1 rounded-full hover:bg-red-100 hover:text-red-500 transition-colors bg-white"
161
166
  tabindex="-1"
162
167
  onclick={(e) => {
163
168
  e.stopPropagation();
164
169
  onItemDelete(item, e);
165
170
  }}
166
171
  >
167
- <img src={icTrash} alt="Delete" class="w-3 h-3" />
172
+ <img src={icTrash} alt={$t('editor.list.deleteItem')} class="w-3 h-3" />
168
173
  </button>
169
174
  {/if}
170
175
  </div>
@@ -195,14 +200,14 @@
195
200
  {#if isCustomItem(item) && !hideTrashIcon}
196
201
  <button
197
202
  type="button"
198
- class="absolute top-1 right-1 p-1 rounded-full hover:bg-red-100 hover:text-red-500 transition-colors bg-white"
203
+ class="absolute top-1 end-1 p-1 rounded-full hover:bg-red-100 hover:text-red-500 transition-colors bg-white"
199
204
  tabindex="-1"
200
205
  onclick={(e) => {
201
206
  e.stopPropagation();
202
207
  onItemDelete(item, e);
203
208
  }}
204
209
  >
205
- <img src={icTrash} alt="Delete" class="w-3 h-3" />
210
+ <img src={icTrash} alt={$t('editor.list.deleteItem')} class="w-3 h-3" />
206
211
  </button>
207
212
  {/if}
208
213
  </div>
@@ -13,7 +13,7 @@ interface $$__sveltets_2_IsomorphicComponent<Props extends Record<string, any> =
13
13
  }
14
14
  declare const RecentItemsSection: $$__sveltets_2_IsomorphicComponent<{
15
15
  recentItems?: any[];
16
- title?: string;
16
+ /** The title of the section. Without one, the editor's own word, in the language of the page. */ title?: string | undefined;
17
17
  onItemSelect: (item: any) => void;
18
18
  onItemDelete: (item: any, event: Event) => void;
19
19
  isItemSelected: (item: any) => boolean;
@@ -0,0 +1,95 @@
1
+ /**
2
+ * The calls an embedded composition editor makes to the Widgetic API, over `fetch`.
3
+ *
4
+ * Why not the generated SDK: the editor runs in a customer's browser and may only call a short list of routes
5
+ * (those an editor session allows), the list is fixed, and it must work with whatever token the host page gives it.
6
+ * A client of its own keeps the editor small and independent of the SDK's release cycle, and it keeps the few
7
+ * rules that matter in one place:
8
+ *
9
+ * - the token is asked from the host page before each request (`getAccessToken('initial')`); when the API
10
+ * answers 401 it is asked again (`'expired'`) and the request is sent once more with a token that is new;
11
+ * - a save carries an `Idempotency-Key` (the API requires one on every PUT from a session), kept the same when
12
+ * the request is sent again after a 401, so one save is never applied twice;
13
+ * - only the fields editing changes can be sent (`SaveCompositionBody`); a session is refused any other;
14
+ * - every failure is a `CompositionApiError` with the API's own code, so the host can tell an ended session
15
+ * (`EDITOR_SESSION_EXPIRED`...) from a missing composition (`NOT_FOUND`) from a widget that was never
16
+ * published (`WIDGET_NOT_PUBLISHED`);
17
+ * - a request that gets no answer does not hang the editor: it fails with `TIMEOUT` after a set time.
18
+ */
19
+ import { type JsonRecord } from './compositionHelpers.js';
20
+ /** Why the token is being asked for. `expired` means the API just refused the previous one. */
21
+ export type TokenReason = 'initial' | 'expired';
22
+ /** The fields of a composition an editor may change. Anything else is refused by the API for an editor session. */
23
+ export interface SaveCompositionBody {
24
+ name?: string;
25
+ content?: JsonRecord;
26
+ design?: JsonRecord;
27
+ embedSettings?: JsonRecord;
28
+ }
29
+ /** Where to show a composition: the widget build the preview frame loads (`GET /compositions/{id}/preview`). */
30
+ export interface CompositionPreviewInfo {
31
+ compositionId: string;
32
+ widgetId: string;
33
+ version: number;
34
+ previewUrl: string;
35
+ }
36
+ export interface CompositionApiOptions {
37
+ /** The API's address including the version, like `https://api.widgetic.com/v1`. */
38
+ apiBasePath: string;
39
+ /**
40
+ * Gives the token to call the API with. Called before every request, so a host that keeps its token fresh is
41
+ * always used up to date; called again with `'expired'` when the API answers 401, after which the request is
42
+ * sent once more if (and only if) the token that comes back is a different one. That second call also gets the
43
+ * token that was refused, so a host can tell the refusal of its current token (get a new one) from the late
44
+ * refusal of one it has already replaced (hand out the new one) when several requests are refused together.
45
+ */
46
+ getAccessToken: (reason: TokenReason, refusedToken?: string) => Promise<string> | string;
47
+ /** The `fetch` to use. Default: the page's own. */
48
+ fetch?: typeof fetch;
49
+ /** Makes the key a save is identified by (8 to 50 letters, digits, dashes and underscores). Default: a random UUID. */
50
+ createIdempotencyKey?: () => string;
51
+ /** How long to wait for an answer before giving up with `TIMEOUT`, in milliseconds. Default: 30 seconds. */
52
+ requestTimeoutMs?: number;
53
+ }
54
+ /** Options of one call. */
55
+ export interface CompositionRequestOptions {
56
+ /** Cancels the request: the call then fails with `ABORTED`. */
57
+ signal?: AbortSignal;
58
+ }
59
+ export interface SaveCompositionOptions extends CompositionRequestOptions {
60
+ /** Identifies this save. Give the same key to send the same save again; default: a new one. */
61
+ idempotencyKey?: string;
62
+ }
63
+ export declare const DEFAULT_REQUEST_TIMEOUT_MS = 30000;
64
+ /** Codes the client itself reports (the API's own codes, such as `NOT_FOUND`, are passed through as they are). */
65
+ export type ClientErrorCode = 'NETWORK' | 'TIMEOUT' | 'ABORTED' | 'TOKEN_UNAVAILABLE' | 'INVALID_RESPONSE';
66
+ /** A request to the API that did not give the answer wanted. */
67
+ export declare class CompositionApiError extends Error {
68
+ /** The HTTP status; `0` when no answer arrived (network, timeout, abort, or no token to send). */
69
+ readonly status: number;
70
+ /** The API's error code (`NOT_FOUND`, `EDITOR_SESSION_EXPIRED`...), or one of {@link ClientErrorCode}. */
71
+ readonly code: string;
72
+ /** The API's correlation id for the failed request, to quote in a support request. */
73
+ readonly correlationId?: string;
74
+ constructor(message: string, details: {
75
+ status: number;
76
+ code: string;
77
+ correlationId?: string;
78
+ });
79
+ /** True when the editor session cannot be used any more: the host page has to give a new one. */
80
+ get sessionEnded(): boolean;
81
+ /** True when sending the same request again later may work: nothing was refused, the network or the server failed. */
82
+ get retryable(): boolean;
83
+ }
84
+ /** What a client of the composition routes can do. */
85
+ export interface CompositionApi {
86
+ getComposition(compositionId: string, options?: CompositionRequestOptions): Promise<JsonRecord>;
87
+ getPreview(compositionId: string, options?: CompositionRequestOptions): Promise<CompositionPreviewInfo>;
88
+ getContentSchema(compositionId: string, options?: CompositionRequestOptions): Promise<unknown>;
89
+ getDesignSchema(compositionId: string, options?: CompositionRequestOptions): Promise<unknown>;
90
+ saveComposition(compositionId: string, body: SaveCompositionBody, options?: SaveCompositionOptions): Promise<JsonRecord>;
91
+ }
92
+ /** A key that is unique, not secret: the page's `crypto.randomUUID`, else random bytes, else the clock and `Math.random`. */
93
+ export declare function createRandomIdempotencyKey(): string;
94
+ /** Builds the client. `apiBasePath` and `getAccessToken` are required. */
95
+ export declare function createCompositionApi(options: CompositionApiOptions): CompositionApi;