@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,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>
@@ -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/src/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}
package/src/SvList.svelte CHANGED
@@ -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>
@@ -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>
@@ -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; }
package/src/SvTour.svelte CHANGED
@@ -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}
package/src/SvTree.svelte CHANGED
@@ -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 -->
@@ -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/src/ai.test.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
1
+ import { afterEach, beforeEach, describe, expect, it } from 'vitest'
2
2
  import {
3
3
  setAIProvider, getAIProvider, hasAIProvider,
4
4
  mockAIProvider,
package/src/build-api.ts CHANGED
@@ -2,52 +2,12 @@
2
2
  // reading/writing controller state via the `ctx` handle; the reactive core
3
3
  // ($state/$derived/$effect) stays in the controller.
4
4
  import {
5
- applyExcelFilter,
6
- normalizeForFilter,
7
- createColumnVirtualizer,
8
- createCoreRowModel,
9
- createExpandedRowModel,
10
- createFilteredRowModel,
11
- createGroupedRowModel,
12
- createPaginatedRowModel,
13
- createSvelteVirtualizer,
14
- createSortedRowModel,
15
- createSvGrid,
16
- filterFns,
17
- getGridCellA11yProps,
18
- getGridCellDomId,
19
- getGridHeaderA11yProps,
20
- getGridRootA11yProps,
21
- getGridRowA11yProps,
22
- parseEditorValue,
23
- normalizeEditorOptions,
24
- sortFns,
25
- tableFeatures,
26
- rowSortingFeature,
27
- columnFilteringFeature,
28
- columnGroupingFeature,
29
- type CellContext,
30
- type EditorContext,
31
- type CellEditorOption,
32
- type CellEditorType,
33
- type CellFormatter,
34
- type CellFormatConfig,
35
- type Column,
36
- type ColumnDef,
37
- type Row,
38
5
  type RowData,
39
6
  type SvGridApi,
40
7
  type TableFeatures,
41
8
  } from "./index";
42
9
  import "./sv-grid-scrollbar";
43
10
  import { untrack } from "svelte";
44
- import type { Snippet } from "svelte";
45
- import { getKeyboardIntent, getNextActiveCell } from "./keyboard";
46
- import {
47
- formatNumericWithConfig,
48
- getDateFormatter,
49
- resolveDatePattern,
50
- } from "./cell-formatting";
51
11
  // The export serializers load lazily (their own chunk) so export-format.ts stays
52
12
  // out of the base bundle - it is fetched only when an export/copy method is
53
13
  // actually invoked (an explicit, already-async user action).
@@ -57,77 +17,12 @@ import type {
57
17
  GridExportOptions,
58
18
  GridClipboardOptions,
59
19
  } from "./export-format";
60
- import {
61
- RenderSnippetConfig,
62
- RenderComponentConfig,
63
- } from "./render-component";
64
- import { buildFillPattern } from "./fill-patterns";
65
- import { buildSparkline, toSparklineValues } from "./sparkline";
66
- import {
67
- resolveCellFormat,
68
- computeColumnStat,
69
- formatsNeedingStats,
70
- type ColumnStat,
71
- type ResolvedCellFormat,
72
- } from "./conditional-formatting";
73
- import SvGridDropdown from "./SvGridDropdown.svelte";
74
20
  import type {
75
- Props,
76
- SelectionPoint,
77
- SelectionRange,
78
- CellEditState,
79
21
  FilterOperator,
80
- FilterOption,
81
- MenuPosition,
82
22
  } from "./SvGrid.types";
83
23
  import {
84
- cfTextStyle,
85
- fmtStat,
86
- getCellKey,
87
- resolveClassList,
88
- toDateInputValue,
89
- toDateTimeLocalInputValue,
90
- getEditableInputValue,
91
- getEditorInputType,
92
- toValueArray,
93
- getOptionLabel,
94
- getOptionColor,
95
- colorfulChipStyle,
96
- getEditorClass,
97
- asDate,
98
- clampMenuX,
99
- cssEscape,
100
- rawToNumber,
101
- formatFacetNumber,
102
- formatFacetDate,
103
- } from "./SvGrid.helpers";
104
- import { createClipboard } from "./clipboard";
105
- import {
106
- filterOperatorOptions,
107
- fallbackOperatorOption,
108
- TEXT_OPERATORS,
109
- NUMBER_OPERATORS,
110
- DATE_OPERATORS,
111
- CHECKBOX_OPERATORS,
112
- operatorOption,
113
- operatorsForColumn,
114
- defaultOperatorFor,
115
- operatorLabelFor,
116
- } from "./filter-operators";
117
- import {
118
- type FacetBucket,
119
- isBucketableColumn,
120
- buildBuckets,
121
- isInBucket,
122
- } from "./facet-buckets";
123
- import {
124
- getColumnBaseValue,
125
24
  isGroupRow,
126
- toolPanelHeaderLabel,
127
- formatSummaryNumeric,
128
25
  getColumnAlign,
129
- getPinnedCellValue,
130
- getColumnAccessorValue,
131
26
  columnDefMatchesId,
132
27
  } from "./cell-values";
133
28
 
@@ -455,6 +350,21 @@ export function createGridApi<
455
350
  clearFilter(columnId) {
456
351
  ctx.clearColumnFilter(columnId);
457
352
  },
353
+ refreshEditorOptions(columnId) {
354
+ // Async `editorOptions` are cached per column (or per column+row for a
355
+ // per-row source) so opening an editor twice does not refetch. This
356
+ // drops those entries, so the next render re-runs the source - use it
357
+ // when the underlying list changed server-side.
358
+ if (columnId == null) {
359
+ ctx.asyncEditorOptions = {};
360
+ return;
361
+ }
362
+ const next = { ...ctx.asyncEditorOptions };
363
+ for (const key of Object.keys(next)) {
364
+ if (key === columnId || key.startsWith(`${columnId}::`)) delete next[key];
365
+ }
366
+ ctx.asyncEditorOptions = next;
367
+ },
458
368
  clearAllFilters() {
459
369
  // See `setFilter` for why the read-modify-write is untracked.
460
370
  untrack(() => {
@@ -9,6 +9,8 @@ import {
9
9
  type CellEditorContext,
10
10
  } from './editor-registry'
11
11
 
12
+ // onCommitAndMove / onRequestClose / inCell are the hooks the grid injects at
13
+ // mount; the registry tests here never exercise them.
12
14
  function ctx(overrides: Partial<CellEditorContext> = {}): CellEditorContext {
13
15
  return {
14
16
  value: null,
@@ -18,7 +20,7 @@ function ctx(overrides: Partial<CellEditorContext> = {}): CellEditorContext {
18
20
  onCommit: () => {},
19
21
  onCancel: () => {},
20
22
  ...overrides,
21
- }
23
+ } as CellEditorContext
22
24
  }
23
25
 
24
26
  afterEach(() => {
@@ -158,7 +158,7 @@ describe('getDateFormatter', () => {
158
158
 
159
159
  describe('formatNumericWithConfig - number', () => {
160
160
  it('formats a basic integer with default locale', () => {
161
- expect(formatNumericWithConfig(1234, { type: 'number' })).toMatch(/1[\.,]?234/)
161
+ expect(formatNumericWithConfig(1234, { type: 'number' })).toMatch(/1[.,]?234/)
162
162
  })
163
163
 
164
164
  it('respects locale grouping and decimal separators', () => {