@svgrid/grid 2.2.31 → 2.2.33

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 (196) hide show
  1. package/README.md +1 -1
  2. package/dist/DockNodeView.svelte +1 -1
  3. package/dist/GridFooter.svelte +0 -40
  4. package/dist/GridMenus.svelte +0 -77
  5. package/dist/SvAvatar.svelte +1 -1
  6. package/dist/SvCheckBox.svelte +2 -1
  7. package/dist/SvCircularProgress.svelte +1 -1
  8. package/dist/SvDateRangeInput.svelte +1 -1
  9. package/dist/SvGrid.controller.svelte.d.ts +20 -0
  10. package/dist/SvGrid.controller.svelte.js +358 -18
  11. package/dist/SvGrid.css +103 -3
  12. package/dist/SvGrid.svelte +164 -35
  13. package/dist/SvGrid.types.d.ts +81 -0
  14. package/dist/SvGridChart.svelte +2 -10
  15. package/dist/SvGridDropdown.svelte +38 -6
  16. package/dist/SvGridDropdown.svelte.d.ts +4 -0
  17. package/dist/SvGridSelect.svelte +12 -5
  18. package/dist/SvGridSelect.svelte.d.ts +6 -0
  19. package/dist/SvGroupCell.svelte +9 -2
  20. package/dist/SvKbd.svelte +1 -1
  21. package/dist/SvList.svelte +1 -1
  22. package/dist/SvMenubar.svelte +1 -1
  23. package/dist/SvMultiSelect.svelte +12 -4
  24. package/dist/SvMultiSelect.svelte.d.ts +6 -0
  25. package/dist/SvOtpInput.svelte +1 -1
  26. package/dist/SvPasswordInput.svelte +1 -1
  27. package/dist/SvProgress.svelte +1 -1
  28. package/dist/SvRating.svelte +1 -1
  29. package/dist/SvResult.svelte +1 -1
  30. package/dist/SvRichText.svelte +35 -16
  31. package/dist/SvRichText.svelte.d.ts +9 -0
  32. package/dist/SvSwitchButton.svelte +2 -1
  33. package/dist/SvToaster.svelte +1 -1
  34. package/dist/SvTour.svelte +4 -0
  35. package/dist/SvTree.svelte +9 -2
  36. package/dist/SvTree.svelte.d.ts +5 -0
  37. package/dist/SvTreeSelect.svelte +6 -4
  38. package/dist/build-api.js +16 -0
  39. package/dist/cdn/{GridMenus-Ds3OpzZT.js → GridMenus-B7XhhqdN.js} +34 -36
  40. package/dist/cdn/{GridMenus-Da_p7SKS.js → GridMenus-BxmIvCJh.js} +35 -37
  41. package/dist/cdn/{SvDateTimePicker-DB0aIEk8.js → SvDateTimePicker-ClqtVIj3.js} +2 -1
  42. package/dist/cdn/{SvDateTimePicker-B8GopjhC.js → SvDateTimePicker-o94wfSUa.js} +3 -2
  43. package/dist/cdn/{SvGridChart-BcLCYvw9.js → SvGridChart-Cr_90pPD.js} +1 -1
  44. package/dist/cdn/{SvGridChartPanel-DvjwxLvK.js → SvGridChartPanel-ICbxiUvK.js} +4 -4
  45. package/dist/cdn/{SvGridChartView-PwqI4s02.js → SvGridChartView-Bx42PMvf.js} +2 -2
  46. package/dist/cdn/{SvGridDropdown-hpnHvOGt.js → SvGridDropdown-CUOdm7YA.js} +93 -88
  47. package/dist/cdn/{SvGridDropdown-DjALc_4f.js → SvGridDropdown-D0VdjeR8.js} +129 -123
  48. package/dist/cdn/{disclose-version-DoD9AFD_.js → disclose-version-DljhZohK.js} +45 -45
  49. package/dist/cdn/{src-DYDCiC0D.js → src-erjBRYfX.js} +5952 -5615
  50. package/dist/cdn/{src-BLJfdyL0.js → src-ktbQVOk-.js} +4190 -3851
  51. package/dist/cdn/svgrid.js +9 -9
  52. package/dist/cdn/svgrid.svelte-external.js +8 -8
  53. package/dist/cell-render.d.ts +1 -0
  54. package/dist/cell-render.js +115 -1
  55. package/dist/cell-values.js +5 -0
  56. package/dist/chart.d.ts +2 -0
  57. package/dist/chart.js +1 -4
  58. package/dist/core.d.ts +80 -9
  59. package/dist/core.js +89 -0
  60. package/dist/createMenu.svelte.js +0 -1
  61. package/dist/createPopoverSelect.svelte.d.ts +2 -2
  62. package/dist/editing.js +1 -1
  63. package/dist/editor-contract.d.ts +12 -5
  64. package/dist/editor-registry.d.ts +18 -7
  65. package/dist/editor-registry.js +10 -1
  66. package/dist/export-format.js +3 -0
  67. package/dist/group-display.d.ts +39 -0
  68. package/dist/group-display.js +52 -0
  69. package/dist/index.d.ts +2 -2
  70. package/dist/index.js +2 -2
  71. package/dist/keyboard-handlers.js +16 -0
  72. package/dist/recurrence.js +3 -2
  73. package/dist/selection.js +0 -1
  74. package/dist/svgrid-wrapper.types.d.ts +8 -2
  75. package/dist/themes/index.d.ts +10 -0
  76. package/dist/themes/index.js +26 -0
  77. package/package.json +3 -2
  78. package/src/DockNodeView.svelte +1 -1
  79. package/src/GridFooter.svelte +0 -40
  80. package/src/GridMenus.svelte +0 -77
  81. package/src/SvAccordion.test.ts +5 -5
  82. package/src/SvAvatar.svelte +1 -1
  83. package/src/SvButtonGroup.test.ts +6 -6
  84. package/src/SvCalendar.test.ts +1 -1
  85. package/src/SvCheckBox.svelte +2 -1
  86. package/src/SvCircularProgress.svelte +1 -1
  87. package/src/SvDateRangeInput.svelte +1 -1
  88. package/src/SvFileUpload.test.ts +1 -1
  89. package/src/SvGrid.controller.svelte.ts +367 -22
  90. package/src/SvGrid.css +103 -3
  91. package/src/SvGrid.svelte +164 -35
  92. package/src/SvGrid.types.ts +81 -1
  93. package/src/SvGridChart.svelte +2 -10
  94. package/src/SvGridDropdown.svelte +38 -6
  95. package/src/SvGridSelect.svelte +12 -5
  96. package/src/SvGroupCell.svelte +9 -2
  97. package/src/SvKbd.svelte +1 -1
  98. package/src/SvList.svelte +1 -1
  99. package/src/SvMenubar.svelte +1 -1
  100. package/src/SvMultiSelect.svelte +12 -4
  101. package/src/SvOtpInput.svelte +1 -1
  102. package/src/SvPasswordInput.svelte +1 -1
  103. package/src/SvProgress.svelte +1 -1
  104. package/src/SvRating.svelte +1 -1
  105. package/src/SvResult.svelte +1 -1
  106. package/src/SvRichText.svelte +35 -16
  107. package/src/SvSwitchButton.svelte +2 -1
  108. package/src/SvToaster.svelte +1 -1
  109. package/src/SvTour.svelte +4 -0
  110. package/src/SvTree.svelte +9 -2
  111. package/src/SvTreeSelect.svelte +6 -4
  112. package/src/ai.test.ts +1 -1
  113. package/src/build-api.ts +15 -105
  114. package/src/builtin-editors.test.ts +3 -1
  115. package/src/cell-formatting.test.ts +1 -1
  116. package/src/cell-render.test.ts +27 -3
  117. package/src/cell-render.ts +122 -95
  118. package/src/cell-values.ts +4 -0
  119. package/src/chart-export.test.ts +2 -2
  120. package/src/chart.ts +1 -2
  121. package/src/clipboard.test.ts +0 -2
  122. package/src/clipboard.ts +0 -102
  123. package/src/columns.ts +0 -115
  124. package/src/core.ts +157 -8
  125. package/src/createMenu.svelte.ts +0 -1
  126. package/src/editing.test.ts +1 -1
  127. package/src/editing.ts +2 -105
  128. package/src/editor-contract.ts +12 -5
  129. package/src/editor-registry.grid.test.ts +48 -2
  130. package/src/editor-registry.test.ts +3 -1
  131. package/src/editor-registry.ts +23 -7
  132. package/src/export-format.ts +3 -0
  133. package/src/facet-buckets.test.ts +33 -33
  134. package/src/features.ts +0 -90
  135. package/src/group-display.test.ts +91 -1
  136. package/src/group-display.ts +83 -0
  137. package/src/index.ts +9 -0
  138. package/src/keyboard-handlers.coverage.test.ts +1 -1
  139. package/src/keyboard-handlers.ts +17 -120
  140. package/src/list-power.test.ts +2 -2
  141. package/src/menus.test.ts +4 -5
  142. package/src/menus.ts +0 -112
  143. package/src/named-views.coverage.test.ts +3 -3
  144. package/src/new-features.test.ts +3 -1
  145. package/src/recurrence.ts +3 -2
  146. package/src/row-drag.test.ts +1 -1
  147. package/src/row-resize.test.ts +41 -42
  148. package/src/scheduler-model.ts +1 -1
  149. package/src/scroll-sync.test.ts +4 -7
  150. package/src/scroll-sync.ts +0 -122
  151. package/src/selection.ts +0 -109
  152. package/src/server-data-source.coverage.test.ts +1 -1
  153. package/src/server-group-model.test.ts +8 -8
  154. package/src/spreadsheet.test.ts +0 -1
  155. package/src/summaries.ts +0 -112
  156. package/src/svgrid-wrapper.types.ts +11 -9
  157. package/src/svgrid.async-editor-options.test.ts +271 -0
  158. package/src/svgrid.auto-row-height.test.ts +204 -0
  159. package/src/svgrid.behavior.test.ts +0 -1
  160. package/src/svgrid.charting.test.ts +4 -4
  161. package/src/svgrid.column-reorder.test.ts +0 -8
  162. package/src/svgrid.comments-autocomplete.test.ts +8 -2
  163. package/src/svgrid.context-menu.test.ts +25 -9
  164. package/src/svgrid.filter-depth.test.ts +163 -0
  165. package/src/svgrid.filter-menu-listbox.svelte.test.ts +16 -2
  166. package/src/svgrid.filter-menu-scroll.test.ts +10 -2
  167. package/src/svgrid.grand-total.test.ts +188 -0
  168. package/src/svgrid.group-display-mode.test.ts +171 -0
  169. package/src/svgrid.group-footers.test.ts +121 -0
  170. package/src/svgrid.group-pagination.test.ts +153 -0
  171. package/src/svgrid.tree-data.test.ts +186 -0
  172. package/src/svgriddropdown.async-panel.svelte.test.ts +195 -0
  173. package/src/themes/index.ts +42 -0
  174. package/src/tree-edit.test.ts +4 -4
  175. package/src/tree-row-model.test.ts +168 -0
  176. package/src/ui-localization.test.ts +113 -0
  177. package/themes/ag-alpine.css +22 -0
  178. package/themes/antd.css +22 -0
  179. package/themes/atlassian.css +22 -0
  180. package/themes/bootstrap.css +22 -0
  181. package/themes/carbon.css +22 -0
  182. package/themes/catppuccin.css +22 -0
  183. package/themes/dracula.css +22 -0
  184. package/themes/ember.css +22 -0
  185. package/themes/excel.css +22 -0
  186. package/themes/fluent.css +22 -0
  187. package/themes/github.css +22 -0
  188. package/themes/linear.css +22 -0
  189. package/themes/material.css +22 -0
  190. package/themes/nord.css +22 -0
  191. package/themes/notion.css +22 -0
  192. package/themes/salesforce.css +22 -0
  193. package/themes/sap.css +22 -0
  194. package/themes/shadcn.css +22 -0
  195. package/themes/tailwind.css +22 -0
  196. package/themes/vercel.css +22 -0
@@ -38,6 +38,10 @@ type Props = {
38
38
  onCommit?: () => void;
39
39
  /** Called when the user dismisses (Escape, blur with no change). */
40
40
  onCancel?: () => void;
41
+ /** The option list is still being fetched (async `editorOptions`). Shows a
42
+ * loading line instead of "No options", which would read as "nothing to
43
+ * pick" when the truth is "not here yet". */
44
+ loading?: boolean;
41
45
  };
42
46
  declare const SvGridDropdown: import("svelte").Component<Props, {}, "">;
43
47
  type SvGridDropdown = ReturnType<typeof SvGridDropdown>;
@@ -38,6 +38,12 @@
38
38
  loadOptions?: (query: string) => Promise<Row[]>
39
39
  debounceMs?: number
40
40
  loadingText?: string
41
+ /** Shown when the list is empty / a search matches nothing (localization). */
42
+ emptyText?: string
43
+ /** Placeholder in the panel's search box (localization). */
44
+ searchPlaceholder?: string
45
+ /** Accessible name for the panel's search box (localization). */
46
+ searchLabel?: string
41
47
  }
42
48
 
43
49
  let {
@@ -47,13 +53,14 @@
47
53
  labelField,
48
54
  value = $bindable<string | number | null>(null),
49
55
  onChange,
50
- onCommit,
51
- onCancel,
52
56
  placeholder = 'Select…',
53
57
  searchable = true,
54
58
  loadOptions,
55
59
  debounceMs = 250,
56
60
  loadingText = 'Loading…',
61
+ emptyText = 'No matches',
62
+ searchPlaceholder = 'Search…',
63
+ searchLabel = 'Search options',
57
64
  disabled = false,
58
65
  readonly = false,
59
66
  label,
@@ -168,8 +175,8 @@
168
175
  aria-expanded="true"
169
176
  aria-controls={gs.panelId}
170
177
  aria-activedescendant={gs.activeDescendant()}
171
- aria-label="Search options"
172
- placeholder="Search…"
178
+ aria-label={searchLabel}
179
+ placeholder={searchPlaceholder}
173
180
  value={search}
174
181
  oninput={(e) => { search = e.currentTarget.value; gs.active = 0; if (loadOptions) runLoad(search) }}
175
182
  />
@@ -205,7 +212,7 @@
205
212
  {/each}
206
213
  </div>
207
214
  {:else}
208
- <div class="sv-gs__empty" role="row">{loading ? loadingText : 'No matches'}</div>
215
+ <div class="sv-gs__empty" role="row">{loading ? loadingText : emptyText}</div>
209
216
  {/each}
210
217
  {#if loading && filtered.length}
211
218
  <div class="sv-gs__empty" role="row" aria-live="polite">{loadingText}</div>
@@ -26,6 +26,12 @@ type Props = Pick<SvEditorProps, 'disabled' | 'readonly' | 'label' | 'hint' | 'e
26
26
  loadOptions?: (query: string) => Promise<Row[]>;
27
27
  debounceMs?: number;
28
28
  loadingText?: string;
29
+ /** Shown when the list is empty / a search matches nothing (localization). */
30
+ emptyText?: string;
31
+ /** Placeholder in the panel's search box (localization). */
32
+ searchPlaceholder?: string;
33
+ /** Accessible name for the panel's search box (localization). */
34
+ searchLabel?: string;
29
35
  };
30
36
  declare const SvGridSelect: import("svelte").Component<Props, {}, "value">;
31
37
  type SvGridSelect = ReturnType<typeof SvGridSelect>;
@@ -39,7 +39,12 @@
39
39
  aria-label={meta.expanded ? 'Collapse group' : 'Expand group'}
40
40
  onclick={() => onToggle(row)}
41
41
  >
42
- <span class="sv-group-chev" class:open={meta.expanded} aria-hidden="true">▸</span>
42
+ <!-- Same chevron the grid's own expanders and SvTree draw, so every
43
+ expander in the library looks identical. -->
44
+ <span class="sv-group-chev" class:open={meta.expanded} aria-hidden="true">
45
+ <svg viewBox="0 0 24 24" width="12" height="12" fill="none" stroke="currentColor"
46
+ stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><path d="m9 18 6-6-6-6" /></svg>
47
+ </span>
43
48
  <span class="sv-group-key">{meta.key}</span>
44
49
  {#if meta.loading}<span class="sv-group-load" aria-hidden="true">…</span>{/if}
45
50
  </button>
@@ -76,7 +81,9 @@
76
81
  cursor: pointer;
77
82
  }
78
83
  .sv-group-chev {
79
- display: inline-block;
84
+ display: inline-flex;
85
+ align-items: center;
86
+ justify-content: center;
80
87
  transition: transform 0.15s;
81
88
  color: var(--sg-muted, #64748b);
82
89
  }
package/dist/SvKbd.svelte CHANGED
@@ -21,7 +21,7 @@
21
21
 
22
22
  {#if keys}
23
23
  <span class="sv-kbd-group sv-kbd--{size}">
24
- {#each keys as k, i}
24
+ {#each keys as k, i (i)}
25
25
  {#if i > 0}<span class="sv-kbd__sep" aria-hidden="true">{separator}</span>{/if}
26
26
  <kbd class="sv-kbd">{k}</kbd>
27
27
  {/each}
@@ -37,7 +37,7 @@
37
37
  {#if children}
38
38
  {@render children()}
39
39
  {:else if items}
40
- {#each items as item}<li>{item}</li>{/each}
40
+ {#each items as item, i (i)}<li>{item}</li>{/each}
41
41
  {/if}
42
42
  </svelte:element>
43
43
 
@@ -41,7 +41,7 @@
41
41
  const resolvedDir = $derived(dir === 'ltr' || dir === 'rtl' ? dir : undefined)
42
42
  const rtl = $derived(resolvedDir === 'rtl')
43
43
 
44
- let triggerEls = $state<(HTMLButtonElement | null)[]>([])
44
+ const triggerEls = $state<(HTMLButtonElement | null)[]>([])
45
45
  let panelEl = $state<HTMLDivElement | null>(null)
46
46
  let openIndex = $state(-1)
47
47
  let activeIndex = $state(0)
@@ -37,6 +37,12 @@
37
37
  /** Debounce for remote search. Default 250ms. */
38
38
  debounceMs?: number
39
39
  loadingText?: string
40
+ /** Shown when the list is empty / a search matches nothing (localization). */
41
+ emptyText?: string
42
+ /** Placeholder in the panel's search box (localization). */
43
+ searchPlaceholder?: string
44
+ /** Accessible name for the panel's search box (localization). */
45
+ searchLabel?: string
40
46
  }
41
47
 
42
48
  let {
@@ -44,7 +50,6 @@
44
50
  value = $bindable<ReadonlyArray<string | number>>([]),
45
51
  onChange,
46
52
  onCommit,
47
- onCancel,
48
53
  placeholder = 'Select…',
49
54
  searchable = true,
50
55
  maxTagCount = 3,
@@ -52,6 +57,9 @@
52
57
  loadOptions,
53
58
  debounceMs = 250,
54
59
  loadingText = 'Loading…',
60
+ emptyText = 'No matches',
61
+ searchPlaceholder = 'Search…',
62
+ searchLabel = 'Search options',
55
63
  disabled = false,
56
64
  readonly = false,
57
65
  label,
@@ -193,8 +201,8 @@
193
201
  aria-expanded="true"
194
202
  aria-controls={ms.panelId}
195
203
  aria-activedescendant={ms.activeDescendant()}
196
- aria-label="Search options"
197
- placeholder="Search…"
204
+ aria-label={searchLabel}
205
+ placeholder={searchPlaceholder}
198
206
  value={search}
199
207
  oninput={(e) => { search = e.currentTarget.value; ms.active = 0; if (loadOptions) runLoad(search) }}
200
208
  />
@@ -224,7 +232,7 @@
224
232
  <span class="sv-ms__label">{opt.label}</span>
225
233
  </li>
226
234
  {:else}
227
- <li class="sv-ms__empty" aria-disabled="true">{loading ? loadingText : 'No matches'}</li>
235
+ <li class="sv-ms__empty" aria-disabled="true">{loading ? loadingText : emptyText}</li>
228
236
  {/each}
229
237
  {#if loading && filtered.length}
230
238
  <li class="sv-ms__loading" aria-live="polite">{loadingText}</li>
@@ -25,6 +25,12 @@ type Props = Pick<SvEditorProps, 'disabled' | 'readonly' | 'label' | 'hint' | 'e
25
25
  /** Debounce for remote search. Default 250ms. */
26
26
  debounceMs?: number;
27
27
  loadingText?: string;
28
+ /** Shown when the list is empty / a search matches nothing (localization). */
29
+ emptyText?: string;
30
+ /** Placeholder in the panel's search box (localization). */
31
+ searchPlaceholder?: string;
32
+ /** Accessible name for the panel's search box (localization). */
33
+ searchLabel?: string;
28
34
  };
29
35
  declare const SvMultiSelect: import("svelte").Component<Props, {}, "value">;
30
36
  type SvMultiSelect = ReturnType<typeof SvMultiSelect>;
@@ -52,7 +52,7 @@
52
52
  const autoId = nextEditorId('sv-otp')
53
53
  const uid = $derived(id ?? autoId)
54
54
  const cells = $derived(otpCells(sanitizeOtp(value, length, numeric), length))
55
- let inputs = $state<HTMLInputElement[]>([])
55
+ const inputs = $state<HTMLInputElement[]>([])
56
56
 
57
57
  function commit(next: string) {
58
58
  const clean = sanitizeOtp(next, length, numeric)
@@ -142,7 +142,7 @@
142
142
  .sv-pw__meter { display: flex; gap: 4px; }
143
143
  .sv-pw__bar { flex: 1; height: 4px; border-radius: 2px; background: var(--sg-border, #e2e8f0); transition: background 0.15s; }
144
144
  .sv-pw__bar.on.is-lvl1 { background: var(--sg-danger, #dc2626); }
145
- .sv-pw__bar.on.is-lvl2 { background: var(--sg-warning, #f59e0b); }
145
+ .sv-pw__bar.on.is-lvl2 { background: var(--sg-warning, #d97706); }
146
146
  .sv-pw__bar.on.is-lvl3 { background: #eab308; }
147
147
  .sv-pw__bar.on.is-lvl4 { background: var(--sg-success, #16a34a); }
148
148
  .sv-pw__strength { font-size: 11px; font-weight: 600; color: var(--sg-muted, #64748b); }
@@ -75,7 +75,7 @@
75
75
  display: flex; align-items: center; gap: 10px; width: 100%;
76
76
  }
77
77
  .sv-prog--success { --_c: var(--sg-success, #16a34a); }
78
- .sv-prog--warning { --_c: var(--sg-warning, #f59e0b); }
78
+ .sv-prog--warning { --_c: var(--sg-warning, #d97706); }
79
79
  .sv-prog--danger { --_c: var(--sg-danger, #dc2626); }
80
80
  .sv-prog__track {
81
81
  position: relative; flex: 1; overflow: hidden;
@@ -96,7 +96,7 @@
96
96
  <style>
97
97
  .sv-rating {
98
98
  --_on: var(--sg-rating-on, #f59e0b);
99
- --_off: var(--sg-rating-empty, #d1d5db);
99
+ --_off: var(--sg-rating-empty, #cbd5e1);
100
100
  --_sz: 22px;
101
101
  display: inline-flex; gap: 2px; align-items: center;
102
102
  }
@@ -59,7 +59,7 @@
59
59
  }
60
60
  .sv-result--success { --_c: var(--sg-success, #16a34a); }
61
61
  .sv-result--error { --_c: var(--sg-danger, #dc2626); }
62
- .sv-result--warning { --_c: var(--sg-warning, #f59e0b); }
62
+ .sv-result--warning { --_c: var(--sg-warning, #d97706); }
63
63
  .sv-result--notfound { --_c: var(--sg-muted, #64748b); }
64
64
  .sv-result__icon {
65
65
  display: grid; place-items: center; width: 60px; height: 60px; border-radius: 50%; margin-bottom: 4px;
@@ -19,7 +19,7 @@
19
19
  * <SvRichText bind:value={html} placeholder="Write something…" />
20
20
  * ```
21
21
  */
22
- import { nextEditorId } from './editor-contract'
22
+ import { nextEditorId, resolveMessages } from './editor-contract'
23
23
 
24
24
  const DEFAULT_TOOLS: RichTextTool[] = [
25
25
  'bold', 'italic', 'underline', 'strike', '|',
@@ -29,16 +29,27 @@
29
29
  'link', 'clear', '|', 'undo', 'redo',
30
30
  ]
31
31
 
32
- const META: Record<Exclude<RichTextTool, '|'>, { label: string; glyph: string }> = {
33
- bold: { label: 'Bold', glyph: 'B' }, italic: { label: 'Italic', glyph: 'I' },
34
- underline: { label: 'Underline', glyph: 'U' }, strike: { label: 'Strikethrough', glyph: 'S' },
35
- h1: { label: 'Heading 1', glyph: 'H1' }, h2: { label: 'Heading 2', glyph: 'H2' },
36
- h3: { label: 'Heading 3', glyph: 'H3' }, p: { label: 'Paragraph', glyph: '¶' },
37
- ul: { label: 'Bullet list', glyph: '' }, ol: { label: 'Numbered list', glyph: '1.' },
38
- quote: { label: 'Quote', glyph: '' }, code: { label: 'Code block', glyph: '</>' },
39
- alignLeft: { label: 'Align left', glyph: '' }, alignCenter: { label: 'Align center', glyph: '≣' },
40
- alignRight: { label: 'Align right', glyph: '≡' }, link: { label: 'Link', glyph: '🔗' },
41
- clear: { label: 'Clear formatting', glyph: '⌫' }, undo: { label: 'Undo', glyph: '↶' }, redo: { label: 'Redo', glyph: '↷' },
32
+ /** Toolbar glyphs. Mode-independent and not language-dependent, so they stay
33
+ * fixed; the LABELS beside them are localizable via `messages`. */
34
+ const GLYPH: Record<Exclude<RichTextTool, '|'>, string> = {
35
+ bold: 'B', italic: 'I', underline: 'U', strike: 'S',
36
+ h1: 'H1', h2: 'H2', h3: 'H3', p: '¶',
37
+ ul: '', ol: '1.', quote: '', code: '</>',
38
+ alignLeft: '', alignCenter: '', alignRight: '', link: '🔗',
39
+ clear: '', undo: '', redo: '',
40
+ }
41
+
42
+ /** User-facing strings (localizable via `messages`). Every one is a tooltip +
43
+ * accessible name on a toolbar button, so leaving them hard-coded made the
44
+ * whole toolbar English-only for screen-reader users. */
45
+ type RichTextMessages = Record<Exclude<RichTextTool, '|'>, string> & { linkPrompt: string; toolbar: string }
46
+ const DEFAULT_MESSAGES: RichTextMessages = {
47
+ bold: 'Bold', italic: 'Italic', underline: 'Underline', strike: 'Strikethrough',
48
+ h1: 'Heading 1', h2: 'Heading 2', h3: 'Heading 3', p: 'Paragraph',
49
+ ul: 'Bullet list', ol: 'Numbered list', quote: 'Quote', code: 'Code block',
50
+ alignLeft: 'Align left', alignCenter: 'Align center', alignRight: 'Align right',
51
+ link: 'Link', clear: 'Clear formatting', undo: 'Undo', redo: 'Redo',
52
+ linkPrompt: 'Link URL', toolbar: 'Formatting',
42
53
  }
43
54
 
44
55
  type Props = {
@@ -52,6 +63,8 @@
52
63
  /** Toolbar tools + order; use '|' for a separator. */
53
64
  tools?: RichTextTool[]
54
65
  ariaLabel?: string
66
+ /** Override any toolbar label / prompt (localization). */
67
+ messages?: Partial<RichTextMessages>
55
68
  }
56
69
 
57
70
  let {
@@ -63,8 +76,10 @@
63
76
  minHeight = '160px',
64
77
  tools = DEFAULT_TOOLS,
65
78
  ariaLabel = 'Rich text editor',
79
+ messages,
66
80
  }: Props = $props()
67
81
 
82
+ const M = $derived(resolveMessages(DEFAULT_MESSAGES, messages))
68
83
  const uid = nextEditorId('sv-rt')
69
84
  let editorEl = $state<HTMLDivElement | null>(null)
70
85
  let focused = $state(false)
@@ -75,6 +90,10 @@
75
90
  // (writing innerHTML while focused would collapse the caret).
76
91
  $effect(() => {
77
92
  const v = value ?? ''
93
+ // A contenteditable owns its own DOM: letting Svelte re-render the body
94
+ // would wipe the caret mid-typing, so the value is pushed in by hand and
95
+ // only while the editor is unfocused.
96
+ // eslint-disable-next-line svelte/no-dom-manipulating
78
97
  if (editorEl && !focused && editorEl.innerHTML !== v) editorEl.innerHTML = v
79
98
  if (editorEl) empty = editorEl.textContent!.trim().length === 0
80
99
  })
@@ -109,14 +128,14 @@
109
128
  ul: () => exec('insertUnorderedList'), ol: () => exec('insertOrderedList'),
110
129
  quote: () => exec('formatBlock', 'BLOCKQUOTE'), code: () => exec('formatBlock', 'PRE'),
111
130
  alignLeft: () => exec('justifyLeft'), alignCenter: () => exec('justifyCenter'), alignRight: () => exec('justifyRight'),
112
- link: () => { const url = typeof prompt === 'function' ? prompt('Link URL', 'https://') : null; if (url) exec('createLink', url) },
131
+ link: () => { const url = typeof prompt === 'function' ? prompt(M.linkPrompt, 'https://') : null; if (url) exec('createLink', url) },
113
132
  clear: () => exec('removeFormat'), undo: () => exec('undo'), redo: () => exec('redo'),
114
133
  }
115
134
  </script>
116
135
 
117
136
  <div class="sv-rt" class:is-disabled={disabled} class:is-readonly={readonly}>
118
137
  {#if !readonly}
119
- <div class="sv-rt__toolbar" role="toolbar" aria-label="Formatting" aria-controls={uid}>
138
+ <div class="sv-rt__toolbar" role="toolbar" aria-label={M.toolbar} aria-controls={uid}>
120
139
  {#each tools as t, i (i)}
121
140
  {#if t === '|'}
122
141
  <span class="sv-rt__sep" aria-hidden="true"></span>
@@ -126,13 +145,13 @@
126
145
  class="sv-rt__btn"
127
146
  class:is-on={activeState[t]}
128
147
  data-tool={t}
129
- title={META[t].label}
130
- aria-label={META[t].label}
148
+ title={M[t]}
149
+ aria-label={M[t]}
131
150
  aria-pressed={activeState[t] ? true : undefined}
132
151
  {disabled}
133
152
  onmousedown={(e) => e.preventDefault()}
134
153
  onclick={run[t]}
135
- >{META[t].glyph}</button>
154
+ >{GLYPH[t]}</button>
136
155
  {/if}
137
156
  {/each}
138
157
  </div>
@@ -1,4 +1,11 @@
1
1
  export type RichTextTool = 'bold' | 'italic' | 'underline' | 'strike' | 'h1' | 'h2' | 'h3' | 'p' | 'ul' | 'ol' | 'quote' | 'code' | 'alignLeft' | 'alignCenter' | 'alignRight' | 'link' | 'clear' | 'undo' | 'redo' | '|';
2
+ /** User-facing strings (localizable via `messages`). Every one is a tooltip +
3
+ * accessible name on a toolbar button, so leaving them hard-coded made the
4
+ * whole toolbar English-only for screen-reader users. */
5
+ type RichTextMessages = Record<Exclude<RichTextTool, '|'>, string> & {
6
+ linkPrompt: string;
7
+ toolbar: string;
8
+ };
2
9
  type Props = {
3
10
  /** HTML string (bindable). */
4
11
  value?: string;
@@ -10,6 +17,8 @@ type Props = {
10
17
  /** Toolbar tools + order; use '|' for a separator. */
11
18
  tools?: RichTextTool[];
12
19
  ariaLabel?: string;
20
+ /** Override any toolbar label / prompt (localization). */
21
+ messages?: Partial<RichTextMessages>;
13
22
  };
14
23
  declare const SvRichText: import("svelte").Component<Props, {}, "value">;
15
24
  type SvRichText = ReturnType<typeof SvRichText>;
@@ -53,8 +53,9 @@
53
53
  </script>
54
54
 
55
55
  <SvField id={uid} {label} {hint} {error} {required} {dir}>
56
+ <!-- role="switch" arrives via switchProps(), which the a11y pass cannot see through -->
57
+ <!-- svelte-ignore a11y_role_supports_aria_props_implicit -->
56
58
  <button
57
- role="switch"
58
59
  class="sv-switch sv-switch--{size}"
59
60
  class:is-on={checked}
60
61
  class:is-readonly={readonly}
@@ -135,7 +135,7 @@
135
135
 
136
136
  :global(.sv-toast--success) { --_accent: var(--sg-success, #16a34a); }
137
137
  :global(.sv-toast--error) { --_accent: var(--sg-danger, #dc2626); }
138
- :global(.sv-toast--warning) { --_accent: var(--sg-warning, #f59e0b); }
138
+ :global(.sv-toast--warning) { --_accent: var(--sg-warning, #d97706); }
139
139
  :global(.sv-toast--info) { --_accent: var(--sg-accent, #2563eb); }
140
140
 
141
141
  :global(.sv-toast__icon) { flex: none; width: 18px; height: 18px; display: grid; place-items: center; color: var(--_accent); font-weight: 700; }
@@ -156,6 +156,10 @@
156
156
  >
157
157
  {#if step.image}
158
158
  <div class="sv-tour__media">
159
+ <!-- A step's `image` is either a URL (rendered as an img above) or
160
+ inline SVG markup the app author wrote for its own tour - never
161
+ user input, so rendering it as markup is the intent. -->
162
+ <!-- eslint-disable-next-line svelte/no-at-html-tags -->
159
163
  {#if /^(https?:|data:|\/)/.test(step.image)}<img class="sv-tour__img" src={step.image} alt="" />{:else}{@html step.image}{/if}
160
164
  </div>
161
165
  {/if}
@@ -48,6 +48,11 @@
48
48
  /** Render a built-in search box that drives the filter. */
49
49
  searchable?: boolean
50
50
  searchPlaceholder?: string
51
+ /** Placeholder row shown under a lazy node while its children load
52
+ * (localization). */
53
+ loadingText?: string
54
+ /** Accessible name for the search box's clear button (localization). */
55
+ clearSearchLabel?: string
51
56
  /** Sort siblings by label ('asc'/'desc') or a custom comparator. */
52
57
  sort?: 'asc' | 'desc' | ((a: TreeNode, b: TreeNode) => number)
53
58
  /** Allow inline rename (double-click or F2). Fires onRename. */
@@ -77,6 +82,8 @@
77
82
  filter,
78
83
  searchable = false,
79
84
  searchPlaceholder = 'Search...',
85
+ loadingText = 'Loading...',
86
+ clearSearchLabel = 'Clear search',
80
87
  sort,
81
88
  editable = false,
82
89
  onRename,
@@ -173,7 +180,7 @@
173
180
  if (!node?.lazy || loaded.has(id) || loadingIds.has(id)) return
174
181
  loadingIds = new Set(loadingIds).add(id)
175
182
  // A placeholder row shows immediately while the promise resolves.
176
- loaded = new Map(loaded).set(id, [{ id: `${id}__loading`, label: 'Loading...', disabled: true }])
183
+ loaded = new Map(loaded).set(id, [{ id: `${id}__loading`, label: loadingText, disabled: true }])
177
184
  try {
178
185
  const kids = await loadChildren(node)
179
186
  loaded = new Map(loaded).set(id, kids)
@@ -268,7 +275,7 @@
268
275
  <div class="sv-tree__search">
269
276
  <svg viewBox="0 0 24 24" width="14" height="14" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" aria-hidden="true"><circle cx="11" cy="11" r="7" /><path d="m21 21-4.3-4.3" /></svg>
270
277
  <input class="sv-tree__search-input" type="text" placeholder={searchPlaceholder} bind:value={query} aria-label={searchPlaceholder} />
271
- {#if query}<button type="button" class="sv-tree__search-clear" aria-label="Clear search" onclick={() => (query = '')}>&times;</button>{/if}
278
+ {#if query}<button type="button" class="sv-tree__search-clear" aria-label={clearSearchLabel} onclick={() => (query = '')}>&times;</button>{/if}
272
279
  </div>
273
280
  {/if}
274
281
  <!-- svelte-ignore a11y_no_static_element_interactions -->
@@ -30,6 +30,11 @@ type Props = {
30
30
  /** Render a built-in search box that drives the filter. */
31
31
  searchable?: boolean;
32
32
  searchPlaceholder?: string;
33
+ /** Placeholder row shown under a lazy node while its children load
34
+ * (localization). */
35
+ loadingText?: string;
36
+ /** Accessible name for the search box's clear button (localization). */
37
+ clearSearchLabel?: string;
33
38
  /** Sort siblings by label ('asc'/'desc') or a custom comparator. */
34
39
  sort?: 'asc' | 'desc' | ((a: TreeNode, b: TreeNode) => number);
35
40
  /** Allow inline rename (double-click or F2). Fires onRename. */
@@ -36,8 +36,6 @@
36
36
  nodes,
37
37
  value = $bindable<string | number | null>(null),
38
38
  onChange,
39
- onCommit,
40
- onCancel,
41
39
  placeholder = 'Select…',
42
40
  showPath = false,
43
41
  expandedIds = [],
@@ -186,7 +184,11 @@
186
184
  {...ts.itemProps(i)}
187
185
  >
188
186
  {#if row.hasChildren}
189
- <span class="sv-ts__chev" class:is-open={row.expanded} aria-hidden="true">▸</span>
187
+ <!-- Same chevron as SvTree and the grid's expanders. -->
188
+ <span class="sv-ts__chev" class:is-open={row.expanded} aria-hidden="true">
189
+ <svg viewBox="0 0 24 24" width="12" height="12" fill="none" stroke="currentColor"
190
+ stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><path d="m9 18 6-6-6-6" /></svg>
191
+ </span>
190
192
  {:else}
191
193
  <span class="sv-ts__chev sv-ts__chev--leaf" aria-hidden="true"></span>
192
194
  {/if}
@@ -226,7 +228,7 @@
226
228
  :global(.sv-ts__row.is-active) { background: var(--sg-row-hover-bg, #f1f5f9); }
227
229
  :global(.sv-ts__row.is-selected) { color: var(--sg-accent, #2563eb); font-weight: 600; }
228
230
  :global(.sv-ts__row.is-disabled) { opacity: 0.5; cursor: not-allowed; }
229
- :global(.sv-ts__chev) { width: 14px; flex: none; color: var(--sg-muted, #64748b); transition: transform 0.12s; font-size: 10px; }
231
+ :global(.sv-ts__chev) { width: 14px; flex: none; display: inline-flex; align-items: center; justify-content: center; color: var(--sg-muted, #64748b); transition: transform 0.12s; }
230
232
  :global(.sv-ts__chev.is-open) { transform: rotate(90deg); }
231
233
  :global(.sv-ts__chev--leaf) { visibility: hidden; }
232
234
  :global(.sv-ts__label) { flex: 1; white-space: nowrap; }
package/dist/build-api.js CHANGED
@@ -315,6 +315,22 @@ export function createGridApi(ctx) {
315
315
  clearFilter(columnId) {
316
316
  ctx.clearColumnFilter(columnId);
317
317
  },
318
+ refreshEditorOptions(columnId) {
319
+ // Async `editorOptions` are cached per column (or per column+row for a
320
+ // per-row source) so opening an editor twice does not refetch. This
321
+ // drops those entries, so the next render re-runs the source - use it
322
+ // when the underlying list changed server-side.
323
+ if (columnId == null) {
324
+ ctx.asyncEditorOptions = {};
325
+ return;
326
+ }
327
+ const next = { ...ctx.asyncEditorOptions };
328
+ for (const key of Object.keys(next)) {
329
+ if (key === columnId || key.startsWith(`${columnId}::`))
330
+ delete next[key];
331
+ }
332
+ ctx.asyncEditorOptions = next;
333
+ },
318
334
  clearAllFilters() {
319
335
  // See `setFilter` for why the read-modify-write is untracked.
320
336
  untrack(() => {