@widgetic/editor 4.0.11 → 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 (58) hide show
  1. package/README.md +124 -0
  2. package/dist/components/WidgetPreview.svelte +58 -59
  3. package/dist/components/color-select/ColorSelect.svelte +4 -4
  4. package/dist/components/editors/CompositionEditorApp.svelte +69 -15
  5. package/dist/components/editors/CompositionEditorApp.svelte.d.ts +20 -0
  6. package/dist/components/editors/EmbedEditor.svelte +33 -22
  7. package/dist/components/editors/EmbedEditor.svelte.d.ts +1 -1
  8. package/dist/components/editors/PropsEditor.svelte +74 -58
  9. package/dist/components/editors/PropsEditor.svelte.d.ts +3 -3
  10. package/dist/components/ic/AngleInputController.svelte +1 -1
  11. package/dist/components/ic/BrowserInputController.svelte +37 -29
  12. package/dist/components/ic/ColorPickerInputController.svelte +31 -15
  13. package/dist/components/ic/DateFieldInputController.svelte +27 -11
  14. package/dist/components/ic/DatePickerInputController.svelte +27 -17
  15. package/dist/components/ic/DropdownInputController.svelte +10 -5
  16. package/dist/components/ic/DropdownInputController.svelte.d.ts +1 -1
  17. package/dist/components/ic/FixedPositionInputController.svelte +12 -6
  18. package/dist/components/ic/FontInputController.svelte +32 -27
  19. package/dist/components/ic/InputControllerHeader.svelte +5 -1
  20. package/dist/components/ic/NumberControls.svelte +4 -4
  21. package/dist/components/ic/NumberInputController.svelte +2 -2
  22. package/dist/components/ic/PositionInputController.svelte +9 -4
  23. package/dist/components/ic/RangeCalendarInputController.svelte +26 -16
  24. package/dist/components/ic/RoundedCornersInputController.svelte +22 -17
  25. package/dist/components/ic/SliderInputController.svelte +7 -2
  26. package/dist/components/ic/SwitchInputController.svelte +12 -3
  27. package/dist/components/ic/TagsInputController.svelte +19 -4
  28. package/dist/components/ic/TextAreaInputController.svelte +59 -44
  29. package/dist/components/ic/TextAreaPreviewInputController.svelte +8 -3
  30. package/dist/components/ic/TextInputController.svelte +17 -1
  31. package/dist/components/ic/TimeInputController.svelte +2 -1
  32. package/dist/components/ic/VectorInputController.svelte +21 -16
  33. package/dist/components/ic/shared/ItemGrid.svelte +7 -3
  34. package/dist/components/ic/shared/RecentItemsSection.svelte +16 -11
  35. package/dist/components/ic/shared/RecentItemsSection.svelte.d.ts +1 -1
  36. package/dist/composition/compositionEditorController.d.ts +11 -2
  37. package/dist/composition/compositionEditorController.js +33 -21
  38. package/dist/composition/compositionHelpers.d.ts +3 -1
  39. package/dist/composition/compositionHelpers.js +4 -2
  40. package/dist/i18n/index.d.ts +4 -0
  41. package/dist/i18n/index.js +4 -0
  42. package/dist/i18n/labels.d.ts +15 -0
  43. package/dist/i18n/labels.js +57 -0
  44. package/dist/i18n/language.d.ts +228 -0
  45. package/dist/i18n/language.js +34 -0
  46. package/dist/i18n/languageKit.d.ts +54 -0
  47. package/dist/i18n/languageKit.js +102 -0
  48. package/dist/i18n/messages.d.ts +241 -0
  49. package/dist/i18n/messages.js +253 -0
  50. package/dist/i18n/translate.d.ts +6 -0
  51. package/dist/i18n/translate.js +8 -0
  52. package/dist/index.d.ts +1 -0
  53. package/dist/index.js +4 -0
  54. package/dist/utils/dockPlacement.d.ts +12 -0
  55. package/dist/utils/dockPlacement.js +13 -0
  56. package/dist/utils/fieldDirection.d.ts +13 -0
  57. package/dist/utils/fieldDirection.js +25 -0
  58. package/package.json +2 -2
@@ -74,6 +74,11 @@
74
74
  import { SearchInput } from '@widgetic/design-system/components';
75
75
  import CategoryTabs from './shared/CategoryTabs.svelte';
76
76
  import { RecentItemsSection, ItemGrid } from './shared';
77
+ import { useEditorLanguage } from '../../i18n/language';
78
+ import { categoryLabel, fontWeightLabel } from '../../i18n/labels';
79
+
80
+ // The words of the editor in the language of the page.
81
+ const { t } = useEditorLanguage();
77
82
 
78
83
 
79
84
  const FONT_FILTER_KEY = (id: string) => `font-specific-filter-state-${id}`;
@@ -1787,7 +1792,7 @@
1787
1792
  toggleDropdown(controllerId);
1788
1793
  }}
1789
1794
  >
1790
- <Popover.Trigger tabindex={-1} class="w-full h-full flex items-center border-0 bg-transparent hover:bg-transparent focus:bg-transparent pl-3 pr-[16px] group"
1795
+ <Popover.Trigger tabindex={-1} class="w-full h-full flex items-center border-0 bg-transparent hover:bg-transparent focus:bg-transparent ps-3 pe-[16px] group"
1791
1796
  onmouseenter={() => {
1792
1797
  // Preload fonts when hovering over the button
1793
1798
  // This reduces the delay when actually clicking to open
@@ -1797,10 +1802,10 @@
1797
1802
  }}
1798
1803
  >
1799
1804
  <span
1800
- class="truncate flex-1 text-left subtext text-grey7"
1805
+ class="truncate flex-1 text-start subtext text-grey7"
1801
1806
  style="font-family: {cssQuotedString(resolvedFont?.family || 'system-ui')}, sans-serif; font-display: swap;"
1802
1807
  >
1803
- {resolvedFont?.family || "Select font..."}
1808
+ {resolvedFont?.family || $t('editor.font.select')}
1804
1809
  </span>
1805
1810
  <IconCaretDownFilled class="w-5 h-5 shrink-0 text-grey5 group-hover:text-grey7" />
1806
1811
  </Popover.Trigger>
@@ -1816,11 +1821,11 @@
1816
1821
  <div class="flex flex-col h-full min-h-0 font-popover-content">
1817
1822
  <!-- Title and upload icon -->
1818
1823
  <div class="font-popover-header shrink-0 relative pt-6 pb-[11px] px-5">
1819
- <div class="h5 text-grey7 widgetic-colors">Fonts</div>
1824
+ <div class="h5 text-grey7 widgetic-colors">{$t('editor.font.title')}</div>
1820
1825
  <button
1821
1826
  type="button"
1822
1827
  tabindex="-1"
1823
- class="absolute right-6 top-[26px] bg-transparent border-0 cursor-pointer p-0 rounded-full hover:bg-grey3 transition-all duration-300"
1828
+ class="absolute end-6 top-[26px] bg-transparent border-0 cursor-pointer p-0 rounded-full hover:bg-grey3 transition-all duration-300"
1824
1829
  onclick={() => {
1825
1830
  try {
1826
1831
  openFileBrowser();
@@ -1840,7 +1845,7 @@
1840
1845
  <!-- Search input -->
1841
1846
  <div class="font-popover-search shrink-0 relative px-4">
1842
1847
  <SearchInput
1843
- placeholder="Search fonts..."
1848
+ placeholder={$t('editor.font.search')}
1844
1849
  bind:value={searchTerm}
1845
1850
  onInput={(e: Event) => {
1846
1851
  // Use debounced search only for typing
@@ -1868,6 +1873,7 @@
1868
1873
  categories={filterOptions}
1869
1874
  activeCategory={currentFilter}
1870
1875
  onCategorySelect={handleFilterChange}
1876
+ getCategoryLabel={(category) => categoryLabel(category, $t)}
1871
1877
  filterVisible={!searchTerm}
1872
1878
  />
1873
1879
  </div>
@@ -1884,14 +1890,13 @@
1884
1890
  }}
1885
1891
  id={`font-list-${controllerId}`}
1886
1892
  role="listbox"
1887
- aria-label="Font list"
1893
+ aria-label={$t('editor.font.list')}
1888
1894
  >
1889
1895
  <div class="space-y-0">
1890
1896
  <!-- Recently used section - only show when not searching -->
1891
1897
  {#if recentFonts.length > 0 && !searchTerm}
1892
1898
  <RecentItemsSection
1893
1899
  recentItems={recentFonts}
1894
- title="Recently used"
1895
1900
  onItemSelect={handleFontSelect}
1896
1901
  onItemDelete={handleFontDelete}
1897
1902
  isItemSelected={isFontSelected}
@@ -1912,9 +1917,9 @@
1912
1917
  <div id="font-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">
1913
1918
  <h3 class="subtext text-grey7 pb-2 flex items-center">
1914
1919
  {#if isInitialLoading}
1915
- <div class="w-4 h-4 border-2 border-t-sage-green border-r-sage-green border-b-grey2 border-l-grey2 rounded-full animate-spin mr-2"></div>
1920
+ <div class="w-4 h-4 border-2 border-t-sage-green border-e-sage-green border-b-grey2 border-s-grey2 rounded-full animate-spin me-2"></div>
1916
1921
  {/if}
1917
- {searchTerm ? 'Search results' : 'More'}
1922
+ {searchTerm ? $t('editor.list.searchResults') : $t('editor.list.more')}
1918
1923
  </h3>
1919
1924
  </div>
1920
1925
  {/if}
@@ -1924,13 +1929,13 @@
1924
1929
  {#if isInitialLoading}
1925
1930
  <!-- Show compact loading indicator while fonts are loading -->
1926
1931
  <div class="flex items-center">
1927
- <div class="text-grey5 subtext mr-2">Loading fonts</div>
1928
- <div class="w-4 h-4 border-2 border-t-sage-green border-r-sage-green border-b-grey2 border-l-grey2 rounded-full animate-spin"></div>
1932
+ <div class="text-grey5 subtext me-2">{$t('editor.font.loading')}</div>
1933
+ <div class="w-4 h-4 border-2 border-t-sage-green border-e-sage-green border-b-grey2 border-s-grey2 rounded-full animate-spin"></div>
1929
1934
  </div>
1930
1935
  {:else}
1931
1936
  <!-- Only show empty state when loading is complete and no fonts were found -->
1932
1937
  <EmptyState
1933
- message={searchTerm ? 'No fonts found' : 'No fonts available'}
1938
+ message={searchTerm ? $t('editor.font.noneFound') : $t('editor.font.noneAvailable')}
1934
1939
  height="auto"
1935
1940
  />
1936
1941
  {/if}
@@ -1953,7 +1958,7 @@
1953
1958
  style="color: {isFontSelected(font) ? '#ffffff' : 'hsl(var(--grey7))'};"
1954
1959
  >
1955
1960
  <div class="flex items-center flex-1 min-w-0 group">
1956
- <Check class="mr-2 h-6 w-6 flex-shrink-0 {isFontSelected(font) ? 'text-white' : 'text-sage-green opacity-0'}"/>
1961
+ <Check class="me-2 h-6 w-6 flex-shrink-0 {isFontSelected(font) ? 'text-white' : 'text-sage-green opacity-0'}"/>
1957
1962
  <span
1958
1963
  class="text-[24px] truncate {index === activeSearchIndex && searchTerm ? 'font-medium' : ''}"
1959
1964
  style="font-family: {cssQuotedString(font.family)}, sans-serif; font-display: swap; color: inherit; -webkit-text-fill-color: currentColor;"
@@ -1965,7 +1970,7 @@
1965
1970
  <div
1966
1971
  role="button"
1967
1972
  tabindex="-1"
1968
- class="ml-auto transition-opacity {resolvedFont?.family === font.family ? 'opacity-0 pointer-events-none' : 'opacity-0 group-hover:opacity-100'}"
1973
+ class="ms-auto transition-opacity {resolvedFont?.family === font.family ? 'opacity-0 pointer-events-none' : 'opacity-0 group-hover:opacity-100'}"
1969
1974
  onclick={(e) => {
1970
1975
  e.stopPropagation();
1971
1976
  e.preventDefault();
@@ -1974,11 +1979,11 @@
1974
1979
  >
1975
1980
  <img
1976
1981
  src={icTrash}
1977
- alt="Remove font"
1982
+ alt={$t('editor.font.remove')}
1978
1983
  class="w-4 h-4 text-grey5 hover:text-error"
1979
1984
  draggable="false"
1980
1985
  />
1981
- <span class="sr-only">Remove font</span>
1986
+ <span class="sr-only">{$t('editor.font.remove')}</span>
1982
1987
  </div>
1983
1988
  {/if}
1984
1989
  </div>
@@ -1997,7 +2002,7 @@
1997
2002
 
1998
2003
  <div class="font-weight-row flex items-center justify-between w-full min-w-0 gap-2" style="min-height: 48px;">
1999
2004
  <div class="font-weight-label-ct flex-1 min-w-0">
2000
- <InputControllerHeader label={label ? 'Font Weight' : ''} />
2005
+ <InputControllerHeader label={label ? $t('editor.font.weight') : ''} />
2001
2006
  </div>
2002
2007
  <div class="font-weight-input-ct h-12 shrink-0 rounded-large border border-grey2 flex items-center bg-grey1 transition-colors duration-200 hover:border-forest-green" style="width: 185px; max-width: 185px; min-width: 185px;">
2003
2008
  <Popover.Root
@@ -2009,15 +2014,15 @@
2009
2014
  }
2010
2015
  }}
2011
2016
  >
2012
- <Popover.Trigger tabindex={-1} class="w-full h-full flex items-center border-0 bg-transparent hover:bg-transparent focus:bg-transparent pl-3 pr-[16px] group">
2013
- <span class="truncate flex-1 text-left subtext text-grey7">
2014
- {displayWeight || 'Regular'}
2017
+ <Popover.Trigger tabindex={-1} class="w-full h-full flex items-center border-0 bg-transparent hover:bg-transparent focus:bg-transparent ps-3 pe-[16px] group">
2018
+ <span class="truncate flex-1 text-start subtext text-grey7">
2019
+ {fontWeightLabel(displayWeight || 'Regular', $t)}
2015
2020
  </span>
2016
2021
  <IconCaretDownFilled class="w-5 h-5 shrink-0 text-grey5 group-hover:text-grey7" />
2017
2022
  </Popover.Trigger>
2018
2023
  <Popover.Content class="font-input-popover p-0 w-[150px]" portalProps={{ to: fontPickerPortalHost }}>
2019
2024
  <Command.Root>
2020
- <Command.List>
2025
+ <Command.List aria-label={$t('editor.font.weightList')}>
2021
2026
  <Command.Group>
2022
2027
  {#each availableWeights as weight}
2023
2028
  <Command.Item
@@ -2026,9 +2031,9 @@
2026
2031
  class="px-2 py-2"
2027
2032
  >
2028
2033
  <Check
2029
- class="mr-1 h-4 w-4 {displayWeight === weight ? 'opacity-100' : 'opacity-0'}"
2034
+ class="me-1 h-4 w-4 {displayWeight === weight ? 'opacity-100' : 'opacity-0'}"
2030
2035
  />
2031
- <span class="truncate">{weight}</span>
2036
+ <span class="truncate">{fontWeightLabel(weight, $t)}</span>
2032
2037
  </Command.Item>
2033
2038
  {/each}
2034
2039
  </Command.Group>
@@ -2064,7 +2069,7 @@
2064
2069
  width: 185px !important;
2065
2070
  max-width: 185px !important;
2066
2071
  min-width: 185px !important;
2067
- margin-left: auto;
2072
+ margin-inline-start: auto;
2068
2073
  flex-shrink: 0;
2069
2074
  position: relative;
2070
2075
  }
@@ -2211,12 +2216,12 @@
2211
2216
 
2212
2217
  /* Style for checkmark icon */
2213
2218
  :global(.checkmark-icon) {
2214
- margin-left: 12px !important;
2219
+ margin-inline-start: 12px !important;
2215
2220
  }
2216
2221
 
2217
2222
  /* Style for font item padding */
2218
2223
  :global(.font-item) {
2219
- padding-left: 48px !important;
2224
+ padding-inline-start: 48px !important;
2220
2225
  }
2221
2226
 
2222
2227
  /* Style for font item height */
@@ -12,6 +12,10 @@ import { Tooltip, Portal } from '@widgetic/design-system/components';
12
12
  // Lucide Icons from design-system
13
13
  import { Info } from '@widgetic/design-system/icons';
14
14
 
15
+ // The way the screen is read: the tip opens at the end of the line, beside the icon
16
+ import { sideAtEnd, useTextDirection } from '@widgetic/design-system/components/textDirection';
17
+ const textDirection = useTextDirection();
18
+
15
19
  // Component props
16
20
  let className = '';
17
21
  export { className as class };
@@ -106,7 +110,7 @@ onDestroy(() => {
106
110
  {#if showHelp && !isScrolling}
107
111
  <Portal target="editor-tooltips">
108
112
  <Tooltip.Content
109
- side="right"
113
+ side={sideAtEnd($textDirection)}
110
114
  sideOffset={5}
111
115
  align="center"
112
116
  alignOffset={-1}
@@ -232,10 +232,10 @@
232
232
  oninput={handleInput}
233
233
  onblur={handleBlur}
234
234
  readonly={isLocked}
235
- class="w-full h-full border-0 bg-transparent subtext text-grey7 placeholder:text-grey3 focus-visible:ring-0 focus-visible:ring-offset-0 pl-4 {customInputClass}"
235
+ class="w-full h-full border-0 bg-transparent subtext text-grey7 placeholder:text-grey3 focus-visible:ring-0 focus-visible:ring-offset-0 ps-4 {customInputClass}"
236
236
  />
237
237
  {#if suffix}
238
- <span class="subtext text-grey7 pointer-events-none -ml-1">
238
+ <span class="subtext text-grey7 pointer-events-none -ms-1">
239
239
  {suffix}
240
240
  </span>
241
241
  {/if}
@@ -279,10 +279,10 @@
279
279
 
280
280
  <style>
281
281
  :global([id^="number-input-"]) {
282
- text-align: left;
282
+ text-align: start;
283
283
  appearance: textfield;
284
284
  -moz-appearance: textfield;
285
- padding-right: 0 !important;
285
+ padding-inline-end: 0 !important;
286
286
  }
287
287
 
288
288
  :global([id^="number-input-"]::-webkit-outer-spin-button),
@@ -373,7 +373,7 @@
373
373
  <InputControllerHeader {label} helpText={helpText} />
374
374
  </div>
375
375
 
376
- <div class="absolute right-0">
376
+ <div class="absolute end-0">
377
377
  <NumberControls
378
378
  {value}
379
379
  {onChange}
@@ -393,7 +393,7 @@
393
393
 
394
394
  <style>
395
395
  :global(#number-input) {
396
- text-align: left;
396
+ text-align: start;
397
397
  appearance: textfield;
398
398
  -moz-appearance: textfield;
399
399
  }
@@ -38,6 +38,10 @@
38
38
 
39
39
  // The cursor of a drag is shown on the module's element, not on the body of the customer's page
40
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();
41
45
 
42
46
  /**
43
47
  * Core state and configuration props
@@ -191,6 +195,7 @@
191
195
  <!-- svelte-ignore a11y_no_static_element_interactions -->
192
196
  <div
193
197
  bind:this={joystickContainer}
198
+ dir="ltr"
194
199
  class="relative w-[185px] h-[185px] bg-grey1 rounded-large border border-grey2 joystick-container transition-colors duration-200 hover:border-forest-green"
195
200
  class:overflow-hidden={shouldMaskThumb}
196
201
  on:mouseenter={handleMouseEnter}
@@ -208,22 +213,22 @@
208
213
  <div
209
214
  bind:this={joystickHandle}
210
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'}"
211
- 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 */"
212
217
  />
213
218
 
214
219
  <!-- Percentage display with dynamic positioning -->
215
220
  <div
216
- 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"
217
222
  class:bottom-2={!shouldShowAtTop}
218
223
  class:top-2={shouldShowAtTop}
219
224
  style="opacity: {isDragging ? 0.5 : 1}"
220
225
  >
221
226
  <div class="flex items-center gap-2 w-[50px]">
222
- <span class="caption text-grey7 w-[12px]">X:</span>
227
+ <span class="caption text-grey7 w-[12px]">{$t('editor.position.x')}</span>
223
228
  <span class="subtext text-grey7 w-[32px]">{formattedX}%</span>
224
229
  </div>
225
230
  <div class="flex items-center gap-2 w-[50px]">
226
- <span class="caption text-grey7 w-[12px]">Y:</span>
231
+ <span class="caption text-grey7 w-[12px]">{$t('editor.position.y')}</span>
227
232
  <span class="subtext text-grey7 w-[32px]">{formattedY}%</span>
228
233
  </div>
229
234
  </div>
@@ -47,6 +47,12 @@
47
47
  // Svelte imports
48
48
  import { onMount, onDestroy } from 'svelte';
49
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();
50
56
 
51
57
  // Shadcn UI Components
52
58
  import { Button, RangeCalendar } from '@widgetic/design-system/components';
@@ -110,26 +116,26 @@
110
116
  * @param range - The date range to format
111
117
  * @returns Formatted date range string
112
118
  */
113
- function formatDateRange(range: DateRange | undefined): string {
114
- if (!range || !range.start || !range.end) return "Pick a date range";
115
-
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
+
116
126
  try {
117
127
  const formatDate = (date: DateValue) => {
118
128
  if (!(date instanceof CalendarDate)) {
119
- console.warn('Invalid date object:', date);
120
- return 'Invalid date';
129
+ console.warn('[Editor] Invalid date object in range control:', date);
130
+ return translate('editor.date.invalid');
121
131
  }
122
- // Format as DD/MM/YY
123
- const day = date.day.toString().padStart(2, '0');
124
- const month = date.month.toString().padStart(2, '0');
125
- const year = date.year.toString().slice(-2);
126
- return `${day}/${month}/${year}`;
132
+ return formatLocaleShortDate({ year: date.year, month: date.month, day: date.day }, languageTag);
127
133
  };
128
134
 
129
135
  return `${formatDate(range.start)} - ${formatDate(range.end)}`;
130
136
  } catch (error) {
131
- console.error('Error formatting date range:', error);
132
- return "Pick a date range";
137
+ console.warn('[Editor] Error formatting date range:', error);
138
+ return translate('editor.date.pickRange');
133
139
  }
134
140
  }
135
141
 
@@ -201,7 +207,7 @@
201
207
  <InputControllerHeader {label} helpText={helpText} />
202
208
  </div>
203
209
 
204
- <div class="absolute right-0">
210
+ <div class="absolute end-0">
205
211
  <div
206
212
  class="h-[48px] w-[185px] rounded-large border border-grey2 flex items-center bg-grey1 transition-colors duration-200 hover:border-forest-green"
207
213
  on:focusin={onFocus}
@@ -212,16 +218,20 @@
212
218
  <Button
213
219
  id="range-calendar"
214
220
  variant="ghost"
215
- 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"
216
222
  >
217
- <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>
218
224
  </Button>
219
225
  </div>
220
226
  </Popover.Trigger>
221
227
 
222
228
  <Popover.Content class="w-auto p-0 z-50" align="start" sideOffset={4}>
223
229
  <RangeCalendar
224
- {...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')}
225
235
  onValueChange={(v: BitsDateRange) => {
226
236
  handleSelect(v)
227
237
  }}
@@ -29,6 +29,10 @@
29
29
  import Unlock from '../icons/unlocked.svelte';
30
30
  import type { InputControllerConfig } from './index';
31
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();
32
36
 
33
37
  // Constants
34
38
  const MIN_RADIUS = 0;
@@ -199,20 +203,21 @@
199
203
  }
200
204
  }
201
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
+
202
215
  // Get corner-specific CSS classes
203
216
  function getCornerClasses(corner: keyof typeof value) {
204
- const isTopLeft = corner === 'topLeft';
205
- const isTopRight = corner === 'topRight';
206
- const isBottomLeft = corner === 'bottomLeft';
207
- const isBottomRight = corner === 'bottomRight';
208
-
209
217
  return {
210
218
  container: `
211
219
  relative
212
- ${isTopLeft ? 'rounded-tl-[15px]' : ''}
213
- ${isTopRight ? 'rounded-tr-[15px]' : ''}
214
- ${isBottomLeft ? 'rounded-bl-[15px]' : ''}
215
- ${isBottomRight ? 'rounded-br-[15px]' : ''}
220
+ ${CORNER_ROUNDING[corner]}
216
221
  ${focusedCorner === corner ? 'bg-sage-green' : 'bg-grey1'}
217
222
  hover:bg-[${focusedCorner === corner ? 'hsl(var(--sage-green))' : 'hsl(var(--grey2))'}]
218
223
  transition-colors
@@ -290,13 +295,13 @@
290
295
  </script>
291
296
 
292
297
  <!-- Main corner radius grid component -->
293
- <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}
294
299
  on:mouseenter={handleMouseEnter}
295
300
  on:mouseleave={handleMouseLeave}
296
301
  on:focusin={onFocus}>
297
302
 
298
- <!-- Corner radius grid -->
299
- <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">
300
305
  <!-- Top Left -->
301
306
  <div class={getCornerClasses('topLeft').container}>
302
307
  <input
@@ -311,7 +316,7 @@
311
316
  on:blur={() => handleInputBlur('topLeft')}
312
317
  on:keydown={(e) => handleKeyDown('topLeft', e)}
313
318
  class={getCornerClasses('topLeft').input}
314
- aria-label="Top left corner radius"
319
+ aria-label={$t('editor.corners.topLeft')}
315
320
  maxlength="3"
316
321
  />
317
322
  </div>
@@ -330,7 +335,7 @@
330
335
  on:blur={() => handleInputBlur('topRight')}
331
336
  on:keydown={(e) => handleKeyDown('topRight', e)}
332
337
  class={getCornerClasses('topRight').input}
333
- aria-label="Top right corner radius"
338
+ aria-label={$t('editor.corners.topRight')}
334
339
  maxlength="3"
335
340
  />
336
341
  </div>
@@ -349,7 +354,7 @@
349
354
  on:blur={() => handleInputBlur('bottomLeft')}
350
355
  on:keydown={(e) => handleKeyDown('bottomLeft', e)}
351
356
  class={getCornerClasses('bottomLeft').input}
352
- aria-label="Bottom left corner radius"
357
+ aria-label={$t('editor.corners.bottomLeft')}
353
358
  maxlength="3"
354
359
  />
355
360
  </div>
@@ -368,7 +373,7 @@
368
373
  on:blur={() => handleInputBlur('bottomRight')}
369
374
  on:keydown={(e) => handleKeyDown('bottomRight', e)}
370
375
  class={getCornerClasses('bottomRight').input}
371
- aria-label="Bottom right corner radius"
376
+ aria-label={$t('editor.corners.bottomRight')}
372
377
  maxlength="3"
373
378
  />
374
379
  </div>
@@ -377,7 +382,7 @@
377
382
  <button
378
383
  class="lock-button"
379
384
  on:click={toggleLock}
380
- aria-label={isLocked ? "Unlock corner radius values" : "Lock corner radius values"}
385
+ aria-label={isLocked ? $t('editor.corners.unlock') : $t('editor.corners.lock')}
381
386
  aria-pressed={isLocked}
382
387
  >
383
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}