@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
@@ -35,6 +35,13 @@
35
35
 
36
36
  // Component config
37
37
  import type { InputControllerConfig } from './index';
38
+
39
+ // The cursor of a drag is shown on the module's element, not on the body of the customer's page
40
+ import { lockPointerStyle } from '../../utils/shadowDom';
41
+ import { useEditorLanguage } from '../../i18n/language';
42
+
43
+ // The words of the editor in the language of the page.
44
+ const { t } = useEditorLanguage();
38
45
 
39
46
  /**
40
47
  * Core state and configuration props
@@ -58,6 +65,8 @@
58
65
  let joystickHandle: HTMLDivElement;
59
66
  let isDragging = false;
60
67
  let isHovering = false;
68
+ // Puts back the cursor the page had before the drag (does nothing when no drag is going on)
69
+ let releasePointerStyle: () => void = () => {};
61
70
 
62
71
  /**
63
72
  * Reactive position state
@@ -125,8 +134,9 @@
125
134
  updatePosition(event.clientX, event.clientY);
126
135
  document.addEventListener('mousemove', handleMouseMove);
127
136
  document.addEventListener('mouseup', handleMouseUp);
128
- document.body.style.cursor = 'grabbing';
129
- document.body.classList.add('position-dragging');
137
+ // A second press before the first one was let go must not leave the first lock behind
138
+ releasePointerStyle();
139
+ releasePointerStyle = lockPointerStyle(joystickContainer, 'grabbing');
130
140
  }
131
141
 
132
142
  function handleMouseMove(event: MouseEvent) {
@@ -139,9 +149,7 @@
139
149
  onDragStateChange(false);
140
150
  document.removeEventListener('mousemove', handleMouseMove);
141
151
  document.removeEventListener('mouseup', handleMouseUp);
142
- document.body.style.cursor = '';
143
- document.body.classList.remove('position-dragging');
144
- document.body.style.userSelect = '';
152
+ releasePointerStyle();
145
153
  }
146
154
 
147
155
  /**
@@ -177,17 +185,17 @@
177
185
  });
178
186
 
179
187
  onDestroy(() => {
180
- document.body.classList.remove('position-dragging');
181
- document.body.style.cursor = '';
182
- document.body.style.userSelect = '';
188
+ // Only undoes what this drag did (it used to clear the cursor and the text selection of the whole page)
189
+ releasePointerStyle();
183
190
  });
184
191
  </script>
185
192
 
186
- <div class="position-input-controller widgetic-colors">
193
+ <div class="position-input-controller widgetic-colors" class:position-input-controller-dragging={isDragging}>
187
194
  <!-- Joystick UI -->
188
195
  <!-- svelte-ignore a11y_no_static_element_interactions -->
189
196
  <div
190
197
  bind:this={joystickContainer}
198
+ dir="ltr"
191
199
  class="relative w-[185px] h-[185px] bg-grey1 rounded-large border border-grey2 joystick-container transition-colors duration-200 hover:border-forest-green"
192
200
  class:overflow-hidden={shouldMaskThumb}
193
201
  on:mouseenter={handleMouseEnter}
@@ -205,22 +213,22 @@
205
213
  <div
206
214
  bind:this={joystickHandle}
207
215
  class="absolute w-[24px] h-[24px] bg-white rounded-full -translate-x-1/2 -translate-y-1/2 border-2 border-grey5 z-10 {isDragging ? 'border-grey7 scale-110' : 'hover:border-grey7 hover:scale-110 transition-all duration-150'}"
208
- style="left: {position.x}%; top: {position.y}%"
216
+ style="left: {position.x}%; top: {position.y}%; /* rtl-ok: x counts from the left edge of the pad, which is a left to right island */"
209
217
  />
210
218
 
211
219
  <!-- Percentage display with dynamic positioning -->
212
220
  <div
213
- class="absolute left-4 right-4 flex justify-between items-center transition-all duration-300 ease-in-out"
221
+ class="absolute start-4 end-4 flex justify-between items-center transition-all duration-300 ease-in-out"
214
222
  class:bottom-2={!shouldShowAtTop}
215
223
  class:top-2={shouldShowAtTop}
216
224
  style="opacity: {isDragging ? 0.5 : 1}"
217
225
  >
218
226
  <div class="flex items-center gap-2 w-[50px]">
219
- <span class="caption text-grey7 w-[12px]">X:</span>
227
+ <span class="caption text-grey7 w-[12px]">{$t('editor.position.x')}</span>
220
228
  <span class="subtext text-grey7 w-[32px]">{formattedX}%</span>
221
229
  </div>
222
230
  <div class="flex items-center gap-2 w-[50px]">
223
- <span class="caption text-grey7 w-[12px]">Y:</span>
231
+ <span class="caption text-grey7 w-[12px]">{$t('editor.position.y')}</span>
224
232
  <span class="subtext text-grey7 w-[32px]">{formattedY}%</span>
225
233
  </div>
226
234
  </div>
@@ -228,8 +236,8 @@
228
236
  </div>
229
237
 
230
238
  <style>
231
- /* Make the selector more specific by targeting only elements within position-input-controller */
232
- :global(body.position-dragging .position-input-controller *) {
239
+ /* While dragging, everything inside the controller shows the grabbing cursor (this used to be a class on the page's body) */
240
+ .position-input-controller-dragging :global(*) {
233
241
  cursor: grabbing !important;
234
242
  user-select: none !important;
235
243
  -webkit-user-select: none !important;
@@ -46,6 +46,13 @@
46
46
  <script lang="ts">
47
47
  // Svelte imports
48
48
  import { onMount, onDestroy } from 'svelte';
49
+ import { listenForScroll } from '../../utils/shadowDom';
50
+ import { useEditorLanguage } from '../../i18n/language';
51
+ import type { EditorMessageKey } from '../../i18n/messages';
52
+ import { formatLocaleShortDate, type Translate } from '@widgetic/design-system/i18n';
53
+
54
+ // The words of the editor in the language of the page, and the language itself (how the calendar writes its months).
55
+ const { t, locale } = useEditorLanguage();
49
56
 
50
57
  // Shadcn UI Components
51
58
  import { Button, RangeCalendar } from '@widgetic/design-system/components';
@@ -109,26 +116,26 @@
109
116
  * @param range - The date range to format
110
117
  * @returns Formatted date range string
111
118
  */
112
- function formatDateRange(range: DateRange | undefined): string {
113
- if (!range || !range.start || !range.end) return "Pick a date range";
114
-
119
+ function formatDateRange(
120
+ range: DateRange | undefined,
121
+ languageTag: string,
122
+ translate: Translate<EditorMessageKey>,
123
+ ): string {
124
+ if (!range || !range.start || !range.end) return translate('editor.date.pickRange');
125
+
115
126
  try {
116
127
  const formatDate = (date: DateValue) => {
117
128
  if (!(date instanceof CalendarDate)) {
118
- console.warn('Invalid date object:', date);
119
- return 'Invalid date';
129
+ console.warn('[Editor] Invalid date object in range control:', date);
130
+ return translate('editor.date.invalid');
120
131
  }
121
- // Format as DD/MM/YY
122
- const day = date.day.toString().padStart(2, '0');
123
- const month = date.month.toString().padStart(2, '0');
124
- const year = date.year.toString().slice(-2);
125
- return `${day}/${month}/${year}`;
132
+ return formatLocaleShortDate({ year: date.year, month: date.month, day: date.day }, languageTag);
126
133
  };
127
134
 
128
135
  return `${formatDate(range.start)} - ${formatDate(range.end)}`;
129
136
  } catch (error) {
130
- console.error('Error formatting date range:', error);
131
- return "Pick a date range";
137
+ console.warn('[Editor] Error formatting date range:', error);
138
+ return translate('editor.date.pickRange');
132
139
  }
133
140
  }
134
141
 
@@ -177,25 +184,30 @@
177
184
  closeDropdown();
178
185
  }
179
186
 
180
- // Setup scroll event listener on mount
187
+ // The control's own element, which says which tree (the page or a module's shadow root) to listen for scrolling in
188
+ let controllerElement: HTMLDivElement | undefined;
189
+ let stopListeningForScroll: () => void = () => {};
190
+
191
+ // Setup scroll event listener on mount (the page and, inside a module, the module's own panels: a scroll never
192
+ // leaves a shadow root, so a listener on the window alone does not hear about those)
181
193
  onMount(() => {
182
- window.addEventListener('scroll', handleScroll, true);
194
+ stopListeningForScroll = listenForScroll(controllerElement, handleScroll);
183
195
  });
184
196
 
185
197
  // Cleanup scroll event listener on destroy
186
198
  onDestroy(() => {
187
- window.removeEventListener('scroll', handleScroll, true);
199
+ stopListeningForScroll();
188
200
  });
189
201
  </script>
190
202
 
191
203
  <!-- Range Calendar Container -->
192
- <div class="flex flex-col gap-2 widgetic-colors">
204
+ <div bind:this={controllerElement} class="flex flex-col gap-2 widgetic-colors">
193
205
  <div class="flex items-center w-full relative">
194
206
  <div class="flex-1 min-w-0">
195
207
  <InputControllerHeader {label} helpText={helpText} />
196
208
  </div>
197
209
 
198
- <div class="absolute right-0">
210
+ <div class="absolute end-0">
199
211
  <div
200
212
  class="h-[48px] w-[185px] rounded-large border border-grey2 flex items-center bg-grey1 transition-colors duration-200 hover:border-forest-green"
201
213
  on:focusin={onFocus}
@@ -206,16 +218,20 @@
206
218
  <Button
207
219
  id="range-calendar"
208
220
  variant="ghost"
209
- class="w-full h-full justify-start text-left border-0 bg-transparent hover:bg-transparent focus:bg-transparent p-0"
221
+ class="w-full h-full justify-start text-start border-0 bg-transparent hover:bg-transparent focus:bg-transparent p-0"
210
222
  >
211
- <span class="subtext text-grey7 {!currentValue ? 'text-grey3' : ''}">{formatDateRange(currentValue)}</span>
223
+ <span class="subtext text-grey7 {!currentValue ? 'text-grey3' : ''}">{formatDateRange(currentValue, $locale, $t)}</span>
212
224
  </Button>
213
225
  </div>
214
226
  </Popover.Trigger>
215
227
 
216
228
  <Popover.Content class="w-auto p-0 z-50" align="start" sideOffset={4}>
217
229
  <RangeCalendar
218
- {...currentValue}
230
+ value={currentValue}
231
+ locale={$locale}
232
+ calendarLabel={$t('editor.date.rangeCalendarLabel')}
233
+ previousLabel={$t('editor.date.previousMonth')}
234
+ nextLabel={$t('editor.date.nextMonth')}
219
235
  onValueChange={(v: BitsDateRange) => {
220
236
  handleSelect(v)
221
237
  }}
@@ -28,6 +28,11 @@
28
28
  import Lock from '../icons/locked.svelte';
29
29
  import Unlock from '../icons/unlocked.svelte';
30
30
  import type { InputControllerConfig } from './index';
31
+ import { activeElementIn } from '../../utils/shadowDom';
32
+ import { useEditorLanguage } from '../../i18n/language';
33
+
34
+ // The words of the editor in the language of the page.
35
+ const { t } = useEditorLanguage();
31
36
 
32
37
  // Constants
33
38
  const MIN_RADIUS = 0;
@@ -63,19 +68,22 @@
63
68
 
64
69
  if (isLocked) {
65
70
  // When locked, update all corners to the same value
66
- onChange({
71
+ value = {
67
72
  topLeft: newValue,
68
73
  topRight: newValue,
69
74
  bottomRight: newValue,
70
75
  bottomLeft: newValue
71
- });
76
+ };
72
77
  } else {
73
78
  // When unlocked, update only the specified corner
74
- onChange({
79
+ value = {
75
80
  ...value,
76
81
  [corner]: newValue
77
- });
82
+ };
78
83
  }
84
+ // The fields show the change at once, in every corner it reached: the parent hands the value back only when it
85
+ // builds this control again, and until then the corners that were changed together kept showing the old number
86
+ onChange(value);
79
87
  }
80
88
 
81
89
  // Handler for input changes
@@ -167,9 +175,12 @@
167
175
 
168
176
  // Reset focused corner if we're not focusing another input immediately
169
177
  setTimeout(() => {
170
- if (!document.activeElement ||
171
- !(document.activeElement instanceof HTMLInputElement) ||
172
- !Object.values(inputRefs).includes(document.activeElement)) {
178
+ // The focused element of the tree this control is in: inside a module `document.activeElement` is the module's
179
+ // host element, which is never one of the corners
180
+ const focusedElement = activeElementIn(input);
181
+ if (!focusedElement ||
182
+ !(focusedElement instanceof HTMLInputElement) ||
183
+ !Object.values(inputRefs).includes(focusedElement)) {
173
184
  focusedCorner = null;
174
185
  }
175
186
  }, 0);
@@ -182,29 +193,31 @@
182
193
  // If locking, normalize all values to the first focused corner or top-left
183
194
  if (isLocked && focusedCorner) {
184
195
  const referenceValue = value[focusedCorner];
185
- onChange({
196
+ value = {
186
197
  topLeft: referenceValue,
187
198
  topRight: referenceValue,
188
199
  bottomRight: referenceValue,
189
200
  bottomLeft: referenceValue
190
- });
201
+ };
202
+ onChange(value);
191
203
  }
192
204
  }
193
205
 
206
+ // The outer corner of the picture of the box that each of the four fields draws. The corners of a box are named by
207
+ // their place on the screen, whatever way the screen is read, so these stay physical.
208
+ const CORNER_ROUNDING = {
209
+ topLeft: 'rounded-tl-[15px]', // rtl-ok: the corner of the picture of a box
210
+ topRight: 'rounded-tr-[15px]', // rtl-ok: the corner of the picture of a box
211
+ bottomLeft: 'rounded-bl-[15px]', // rtl-ok: the corner of the picture of a box
212
+ bottomRight: 'rounded-br-[15px]' // rtl-ok: the corner of the picture of a box
213
+ } as const;
214
+
194
215
  // Get corner-specific CSS classes
195
216
  function getCornerClasses(corner: keyof typeof value) {
196
- const isTopLeft = corner === 'topLeft';
197
- const isTopRight = corner === 'topRight';
198
- const isBottomLeft = corner === 'bottomLeft';
199
- const isBottomRight = corner === 'bottomRight';
200
-
201
217
  return {
202
218
  container: `
203
219
  relative
204
- ${isTopLeft ? 'rounded-tl-[15px]' : ''}
205
- ${isTopRight ? 'rounded-tr-[15px]' : ''}
206
- ${isBottomLeft ? 'rounded-bl-[15px]' : ''}
207
- ${isBottomRight ? 'rounded-br-[15px]' : ''}
220
+ ${CORNER_ROUNDING[corner]}
208
221
  ${focusedCorner === corner ? 'bg-sage-green' : 'bg-grey1'}
209
222
  hover:bg-[${focusedCorner === corner ? 'hsl(var(--sage-green))' : 'hsl(var(--grey2))'}]
210
223
  transition-colors
@@ -282,13 +295,13 @@
282
295
  </script>
283
296
 
284
297
  <!-- Main corner radius grid component -->
285
- <div class="corners-container {className}" role="group" aria-label={label || 'Corner radius'} title={helpText}
298
+ <div class="corners-container {className}" role="group" aria-label={label || $t('editor.corners.group')} title={helpText}
286
299
  on:mouseenter={handleMouseEnter}
287
300
  on:mouseleave={handleMouseLeave}
288
301
  on:focusin={onFocus}>
289
302
 
290
- <!-- Corner radius grid -->
291
- <div class="corners-grid">
303
+ <!-- Corner radius grid: four fields placed like the four corners of a box, top left first, so it is a left to right island -->
304
+ <div class="corners-grid" dir="ltr">
292
305
  <!-- Top Left -->
293
306
  <div class={getCornerClasses('topLeft').container}>
294
307
  <input
@@ -303,7 +316,7 @@
303
316
  on:blur={() => handleInputBlur('topLeft')}
304
317
  on:keydown={(e) => handleKeyDown('topLeft', e)}
305
318
  class={getCornerClasses('topLeft').input}
306
- aria-label="Top left corner radius"
319
+ aria-label={$t('editor.corners.topLeft')}
307
320
  maxlength="3"
308
321
  />
309
322
  </div>
@@ -322,7 +335,7 @@
322
335
  on:blur={() => handleInputBlur('topRight')}
323
336
  on:keydown={(e) => handleKeyDown('topRight', e)}
324
337
  class={getCornerClasses('topRight').input}
325
- aria-label="Top right corner radius"
338
+ aria-label={$t('editor.corners.topRight')}
326
339
  maxlength="3"
327
340
  />
328
341
  </div>
@@ -341,7 +354,7 @@
341
354
  on:blur={() => handleInputBlur('bottomLeft')}
342
355
  on:keydown={(e) => handleKeyDown('bottomLeft', e)}
343
356
  class={getCornerClasses('bottomLeft').input}
344
- aria-label="Bottom left corner radius"
357
+ aria-label={$t('editor.corners.bottomLeft')}
345
358
  maxlength="3"
346
359
  />
347
360
  </div>
@@ -360,7 +373,7 @@
360
373
  on:blur={() => handleInputBlur('bottomRight')}
361
374
  on:keydown={(e) => handleKeyDown('bottomRight', e)}
362
375
  class={getCornerClasses('bottomRight').input}
363
- aria-label="Bottom right corner radius"
376
+ aria-label={$t('editor.corners.bottomRight')}
364
377
  maxlength="3"
365
378
  />
366
379
  </div>
@@ -369,7 +382,7 @@
369
382
  <button
370
383
  class="lock-button"
371
384
  on:click={toggleLock}
372
- aria-label={isLocked ? "Unlock corner radius values" : "Lock corner radius values"}
385
+ aria-label={isLocked ? $t('editor.corners.unlock') : $t('editor.corners.lock')}
373
386
  aria-pressed={isLocked}
374
387
  >
375
388
  <div class="flex items-center justify-center w-full h-full {isLocked ? 'text-sage-green' : 'text-grey7'}">
@@ -42,7 +42,11 @@
42
42
  * Shadcn UI components from Widgetic design-system package
43
43
  */
44
44
  import { Slider } from '@widgetic/design-system/components';
45
+ import { useTextDirection } from '@widgetic/design-system/components/textDirection';
45
46
  import { InputControllerHeader } from './index';
47
+
48
+ // The way the screen is read: the slider of the design system starts at the right end on a right to left screen.
49
+ const textDirection = useTextDirection();
46
50
 
47
51
 
48
52
  /**
@@ -186,7 +190,8 @@
186
190
  if (!sliderElement) return;
187
191
 
188
192
  const rect = sliderElement.getBoundingClientRect();
189
- const clickX = mouseEvent.clientX - rect.left;
193
+ // The distance from the end where the track starts: its left end, or its right end when the screen is read from the right
194
+ const clickX = $textDirection === 'rtl' ? rect.right - mouseEvent.clientX : mouseEvent.clientX - rect.left;
190
195
  const trackWidth = rect.width;
191
196
 
192
197
  // Calculate the percentage position of the click
@@ -331,7 +336,7 @@
331
336
  <!-- Main Container with subcomponents -->
332
337
  <div
333
338
  class="slider-main-container flex items-center justify-between rounded-large transition-colors duration-200"
334
- style="height: {height}px; padding-left: 16px; padding-right: 16px; gap: {elementsGap}px; border: 1px solid hsl(var(--grey2)); background: hsl(var(--grey1));"
339
+ style="height: {height}px; padding-inline-start: 16px; padding-inline-end: 16px; gap: {elementsGap}px; border: 1px solid hsl(var(--grey2)); background: hsl(var(--grey1));"
335
340
  onfocusin={onFocus}
336
341
  bind:this={sliderContainer}
337
342
  >
@@ -17,6 +17,10 @@
17
17
  // Shadcn UI Components
18
18
  import { Switch } from '@widgetic/design-system/components';
19
19
  import { InputControllerHeader } from './index';
20
+ import { useEditorLanguage } from '../../i18n/language';
21
+
22
+ // The words of the editor in the language of the page.
23
+ const { t } = useEditorLanguage();
20
24
 
21
25
  // Component props
22
26
  export let label: SwitchInputControllerProps['label'];
@@ -38,7 +42,7 @@
38
42
  <InputControllerHeader {label} />
39
43
  </div>
40
44
 
41
- <div class="absolute right-0">
45
+ <div class="absolute end-0">
42
46
  <!-- svelte-ignore a11y_click_events_have_key_events -->
43
47
  <!-- svelte-ignore a11y_no_static_element_interactions -->
44
48
  <div
@@ -48,7 +52,7 @@
48
52
  onclick={(e) => { if (e.target === e.currentTarget) toggle(); }}
49
53
  >
50
54
  <!-- Off text -->
51
- <span class="subtext select-none pointer-events-none" style="color: hsl(var({!value ? '--grey7' : '--grey3'}));">Off</span>
55
+ <span class="subtext select-none pointer-events-none" style="color: hsl(var({!value ? '--grey7' : '--grey3'}));">{$t('editor.switch.off')}</span>
52
56
 
53
57
  <!-- Switch -->
54
58
  <div class="flex items-center justify-center pointer-events-none widgetic-colors">
@@ -62,7 +66,7 @@
62
66
  </div>
63
67
 
64
68
  <!-- On text -->
65
- <span class="subtext select-none pointer-events-none" style="color: hsl(var({value ? '--grey7' : '--grey3'}));">On</span>
69
+ <span class="subtext select-none pointer-events-none" style="color: hsl(var({value ? '--grey7' : '--grey3'}));">{$t('editor.switch.on')}</span>
66
70
  </div>
67
71
  </div>
68
72
  </div>
@@ -116,4 +120,9 @@
116
120
  transform: translateX(20px) !important;
117
121
  border: 1px solid hsl(var(--grey6)) !important;
118
122
  }
123
+
124
+ /* The thumb starts at the end of the track that the line starts at: on a right to left screen it is at the right, and it travels to the left. */
125
+ :global(.switch-override:dir(rtl) > *[data-state="checked"]) {
126
+ transform: translateX(-20px) !important;
127
+ }
119
128
  </style>
@@ -30,6 +30,14 @@
30
30
  import { onMount, onDestroy } from 'svelte';
31
31
  import InputControllerHeader from './InputControllerHeader.svelte';
32
32
  import { IconX } from '@widgetic/design-system/icons';
33
+ import { useEditorLanguage } from '../../i18n/language';
34
+ import { dirForUserText, useTextDirection } from '@widgetic/design-system/components/textDirection';
35
+
36
+ // The words of the editor in the language of the page.
37
+ const { t } = useEditorLanguage();
38
+
39
+ // The way the screen is read: a tag is what a person typed, so on a right to left screen it is read by its own first letter.
40
+ const textDirection = useTextDirection();
33
41
 
34
42
  // Required props
35
43
  export let label: TagsInputControllerProps['label'];
@@ -44,11 +52,15 @@
44
52
  export { className as class };
45
53
 
46
54
  // Use config values with fallbacks
47
- const placeholder = config.placeholder || 'Add tag...';
55
+ // The hint in the box: the one the widget gave, or the editor's own word in the language of the page.
56
+ $: placeholder = config.placeholder || $t('editor.tags.add');
48
57
 
49
58
  // Component state
50
59
  let inputElement: HTMLInputElement;
51
60
  let tagInputContainer: HTMLDivElement;
61
+ // What is typed in the box right now. An empty box has no first letter to be read by, and reads left to right by itself
62
+ // even on a right to left screen: so it takes the direction of the screen until something is typed (see fieldDirection.ts).
63
+ let typedTag = '';
52
64
 
53
65
  function handleTagRemove(tagToRemove: string) {
54
66
  const newTags = value.filter(tag => tag !== tagToRemove);
@@ -68,6 +80,7 @@
68
80
  onChange(newTags);
69
81
  }
70
82
  input.value = '';
83
+ typedTag = '';
71
84
  }
72
85
  }
73
86
  }
@@ -98,27 +111,29 @@
98
111
  on:focusin={onFocus}
99
112
  >
100
113
  <!-- Input field with specific padding -->
101
- <div class="pt-[12px] pl-[16px] pr-[16px]">
114
+ <div class="pt-[12px] ps-[16px] pe-[16px]">
102
115
  <input
103
116
  bind:this={inputElement}
104
117
  type="text"
105
118
  autocomplete="off"
119
+ dir={typedTag ? dirForUserText($textDirection) : undefined}
106
120
  class="w-full outline-none border-none bg-transparent subtext text-grey7 placeholder:text-grey3 placeholder:subtext"
107
121
  placeholder={placeholder}
122
+ on:input={(event) => (typedTag = event.currentTarget.value)}
108
123
  on:keydown={handleInputKeydown}
109
124
  />
110
125
  </div>
111
126
 
112
127
  <!-- Tags with specific padding -->
113
128
  {#if value && value.length > 0}
114
- <div class="flex flex-wrap gap-1 pl-[16px] pr-[16px] pb-[16px] pt-[8px]">
129
+ <div class="flex flex-wrap gap-1 ps-[16px] pe-[16px] pb-[16px] pt-[8px]">
115
130
  {#each value as tag}
116
131
  <!-- svelte-ignore a11y_click_events_have_key_events -->
117
132
  <div
118
133
  class="tag-element bg-sage-green text-white border-none hover:bg-deep-forest-green cursor-pointer transition-colors flex items-center gap-1"
119
134
  on:click|stopPropagation={() => handleTagRemove(tag)}
120
135
  >
121
- <span class="tag uppercase truncate max-w-[115px]">{tag}</span>
136
+ <span class="tag uppercase truncate max-w-[115px]" dir={dirForUserText($textDirection)}>{tag}</span>
122
137
  <IconX class="w-4 h-4 text-white flex-shrink-0" />
123
138
  </div>
124
139
  {/each}