@svgrid/grid 1.2.34 → 2.0.1

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 (444) hide show
  1. package/README.md +6 -5
  2. package/dist/SvAccordion.svelte +109 -0
  3. package/dist/SvAccordion.svelte.d.ts +28 -0
  4. package/dist/SvAlert.svelte +92 -0
  5. package/dist/SvAlert.svelte.d.ts +27 -0
  6. package/dist/SvAutoComplete.svelte +47 -18
  7. package/dist/SvAutoComplete.svelte.d.ts +2 -5
  8. package/dist/SvAvatar.svelte +75 -0
  9. package/dist/SvAvatar.svelte.d.ts +14 -0
  10. package/dist/SvAvatarGroup.svelte +48 -0
  11. package/dist/SvAvatarGroup.svelte.d.ts +16 -0
  12. package/dist/SvBadge.svelte +54 -0
  13. package/dist/SvBadge.svelte.d.ts +23 -0
  14. package/dist/SvBreadcrumb.svelte +88 -0
  15. package/dist/SvBreadcrumb.svelte.d.ts +23 -0
  16. package/dist/SvButtonGroup.svelte +121 -0
  17. package/dist/SvButtonGroup.svelte.d.ts +16 -0
  18. package/dist/SvCalendar.svelte +72 -9
  19. package/dist/SvCalendar.svelte.d.ts +34 -1
  20. package/dist/SvCard.svelte +71 -0
  21. package/dist/SvCard.svelte.d.ts +28 -0
  22. package/dist/SvCarousel.svelte +134 -0
  23. package/dist/SvCarousel.svelte.d.ts +28 -0
  24. package/dist/SvCheckBox.svelte +36 -21
  25. package/dist/SvCheckBox.svelte.d.ts +2 -5
  26. package/dist/SvChip.svelte +88 -0
  27. package/dist/SvChip.svelte.d.ts +31 -0
  28. package/dist/SvCircularProgress.svelte +109 -0
  29. package/dist/SvCircularProgress.svelte.d.ts +31 -0
  30. package/dist/SvColorInput.svelte +54 -27
  31. package/dist/SvColorInput.svelte.d.ts +10 -6
  32. package/dist/SvComboBox.svelte +119 -33
  33. package/dist/SvComboBox.svelte.d.ts +18 -6
  34. package/dist/SvCommand.svelte +197 -0
  35. package/dist/SvCommand.svelte.d.ts +26 -0
  36. package/dist/SvContextMenu.svelte +96 -0
  37. package/dist/SvContextMenu.svelte.d.ts +25 -0
  38. package/dist/SvCountryInput.svelte +56 -21
  39. package/dist/SvCountryInput.svelte.d.ts +9 -5
  40. package/dist/SvDateRangeInput.svelte +256 -0
  41. package/dist/SvDateRangeInput.svelte.d.ts +37 -0
  42. package/dist/SvDateTimePicker.svelte +57 -20
  43. package/dist/SvDateTimePicker.svelte.d.ts +12 -4
  44. package/dist/SvDivider.svelte +61 -0
  45. package/dist/SvDivider.svelte.d.ts +22 -0
  46. package/dist/SvDrawer.svelte +226 -0
  47. package/dist/SvDrawer.svelte.d.ts +43 -0
  48. package/dist/SvDropDownList.svelte +114 -35
  49. package/dist/SvDropDownList.svelte.d.ts +7 -6
  50. package/dist/SvDurationInput.svelte +108 -0
  51. package/dist/SvDurationInput.svelte.d.ts +15 -0
  52. package/dist/SvEmptyState.svelte +59 -0
  53. package/dist/SvEmptyState.svelte.d.ts +25 -0
  54. package/dist/SvField.svelte +85 -0
  55. package/dist/SvField.svelte.d.ts +30 -0
  56. package/dist/SvFileUpload.svelte +169 -0
  57. package/dist/SvFileUpload.svelte.d.ts +24 -0
  58. package/dist/SvForm.svelte +7 -1
  59. package/dist/SvForm.svelte.d.ts +3 -0
  60. package/dist/SvGauge.svelte +7 -1
  61. package/dist/SvGauge.svelte.d.ts +11 -0
  62. package/dist/SvGrid.controller.svelte.d.ts +6 -0
  63. package/dist/SvGrid.controller.svelte.js +64 -9
  64. package/dist/SvGrid.css +33 -0
  65. package/dist/SvGrid.svelte +116 -3
  66. package/dist/SvGrid.types.d.ts +366 -0
  67. package/dist/SvGridBoard.svelte +2281 -0
  68. package/dist/SvGridBoard.svelte.d.ts +31 -0
  69. package/dist/SvGridDropdown.svelte +2 -1
  70. package/dist/SvGridSelect.svelte +255 -0
  71. package/dist/SvGridSelect.svelte.d.ts +32 -0
  72. package/dist/SvGroupCell.svelte +109 -0
  73. package/dist/SvGroupCell.svelte.d.ts +41 -0
  74. package/dist/SvListBox.svelte +123 -36
  75. package/dist/SvListBox.svelte.d.ts +12 -8
  76. package/dist/SvMaskedInput.svelte +53 -13
  77. package/dist/SvMaskedInput.svelte.d.ts +16 -6
  78. package/dist/SvMenu.svelte +101 -0
  79. package/dist/SvMenu.svelte.d.ts +26 -0
  80. package/dist/SvMenuList.svelte +143 -0
  81. package/dist/SvMenuList.svelte.d.ts +24 -0
  82. package/dist/SvModal.svelte +220 -0
  83. package/dist/SvModal.svelte.d.ts +31 -0
  84. package/dist/SvMultiSelect.svelte +277 -0
  85. package/dist/SvMultiSelect.svelte.d.ts +31 -0
  86. package/dist/SvNavPane.svelte +314 -0
  87. package/dist/SvNavPane.svelte.d.ts +59 -0
  88. package/dist/SvNumberInput.svelte +23 -9
  89. package/dist/SvNumberInput.svelte.d.ts +2 -0
  90. package/dist/SvOtpInput.svelte +157 -0
  91. package/dist/SvOtpInput.svelte.d.ts +16 -0
  92. package/dist/SvPagination.svelte +100 -0
  93. package/dist/SvPagination.svelte.d.ts +16 -0
  94. package/dist/SvPasswordInput.svelte +42 -12
  95. package/dist/SvPasswordInput.svelte.d.ts +13 -6
  96. package/dist/SvPhoneInput.svelte +43 -11
  97. package/dist/SvPhoneInput.svelte.d.ts +10 -11
  98. package/dist/SvPopover.svelte +127 -0
  99. package/dist/SvPopover.svelte.d.ts +35 -0
  100. package/dist/SvProgress.svelte +116 -0
  101. package/dist/SvProgress.svelte.d.ts +32 -0
  102. package/dist/SvRadioGroup.svelte +40 -22
  103. package/dist/SvRadioGroup.svelte.d.ts +2 -10
  104. package/dist/SvRating.svelte +51 -30
  105. package/dist/SvRating.svelte.d.ts +10 -7
  106. package/dist/SvRichText.svelte +192 -0
  107. package/dist/SvRichText.svelte.d.ts +16 -0
  108. package/dist/SvRowGroupPanel.svelte +147 -0
  109. package/dist/SvRowGroupPanel.svelte.d.ts +17 -0
  110. package/dist/SvScrollArea.svelte +50 -0
  111. package/dist/SvScrollArea.svelte.d.ts +22 -0
  112. package/dist/SvSkeleton.svelte +76 -0
  113. package/dist/SvSkeleton.svelte.d.ts +25 -0
  114. package/dist/SvSlider.svelte +41 -9
  115. package/dist/SvSlider.svelte.d.ts +4 -4
  116. package/dist/SvSparkline.svelte +77 -0
  117. package/dist/SvSparkline.svelte.d.ts +28 -0
  118. package/dist/SvSplitter.svelte +138 -0
  119. package/dist/SvSplitter.svelte.d.ts +32 -0
  120. package/dist/SvStat.svelte +86 -0
  121. package/dist/SvStat.svelte.d.ts +30 -0
  122. package/dist/SvStepper.svelte +91 -0
  123. package/dist/SvStepper.svelte.d.ts +24 -0
  124. package/dist/SvSwitchButton.svelte +39 -24
  125. package/dist/SvSwitchButton.svelte.d.ts +2 -5
  126. package/dist/SvTabs.svelte +147 -28
  127. package/dist/SvTabs.svelte.d.ts +16 -4
  128. package/dist/SvTagsInput.svelte +59 -22
  129. package/dist/SvTagsInput.svelte.d.ts +9 -4
  130. package/dist/SvTextArea.svelte +113 -0
  131. package/dist/SvTextArea.svelte.d.ts +18 -0
  132. package/dist/SvTextInput.svelte +115 -0
  133. package/dist/SvTextInput.svelte.d.ts +20 -0
  134. package/dist/SvTimePicker.svelte +19 -5
  135. package/dist/SvTimePicker.svelte.d.ts +12 -0
  136. package/dist/SvTimeline.svelte +75 -0
  137. package/dist/SvTimeline.svelte.d.ts +24 -0
  138. package/dist/SvToaster.svelte +124 -0
  139. package/dist/SvToaster.svelte.d.ts +10 -0
  140. package/dist/SvToggleButton.svelte +37 -12
  141. package/dist/SvToggleButton.svelte.d.ts +2 -4
  142. package/dist/SvTooltip.svelte +113 -0
  143. package/dist/SvTooltip.svelte.d.ts +22 -0
  144. package/dist/SvTour.svelte +151 -0
  145. package/dist/SvTour.svelte.d.ts +25 -0
  146. package/dist/SvTree.svelte +286 -44
  147. package/dist/SvTree.svelte.d.ts +26 -9
  148. package/dist/SvTreeSelect.svelte +222 -0
  149. package/dist/SvTreeSelect.svelte.d.ts +24 -0
  150. package/dist/a11y/dismissable.d.ts +32 -0
  151. package/dist/a11y/dismissable.js +54 -0
  152. package/dist/a11y/focus-trap.d.ts +39 -0
  153. package/dist/a11y/focus-trap.js +142 -0
  154. package/dist/a11y/live-region.d.ts +8 -0
  155. package/dist/a11y/live-region.js +60 -0
  156. package/dist/a11y/scroll-lock.d.ts +4 -0
  157. package/dist/a11y/scroll-lock.js +41 -0
  158. package/dist/a11y.d.ts +3 -1
  159. package/dist/a11y.js +1 -1
  160. package/dist/avatar.d.ts +10 -0
  161. package/dist/avatar.js +22 -0
  162. package/dist/builtin-editors.d.ts +5 -0
  163. package/dist/builtin-editors.js +55 -0
  164. package/dist/cdn/svgrid.js +16021 -8234
  165. package/dist/cdn/svgrid.svelte-external.js +14542 -6764
  166. package/dist/columns.js +3 -2
  167. package/dist/core.d.ts +7 -1
  168. package/dist/countries.d.ts +12 -0
  169. package/dist/countries.js +24 -0
  170. package/dist/createAccordion.svelte.d.ts +71 -0
  171. package/dist/createAccordion.svelte.js +99 -0
  172. package/dist/createAutocomplete.svelte.d.ts +13 -4
  173. package/dist/createAutocomplete.svelte.js +11 -1
  174. package/dist/createButtonGroup.svelte.d.ts +62 -0
  175. package/dist/createButtonGroup.svelte.js +154 -0
  176. package/dist/createCalendar.svelte.d.ts +5 -0
  177. package/dist/createCalendar.svelte.js +2 -0
  178. package/dist/createCheckbox.svelte.d.ts +9 -17
  179. package/dist/createCheckbox.svelte.js +28 -1
  180. package/dist/createColorInput.svelte.d.ts +12 -22
  181. package/dist/createColorInput.svelte.js +13 -0
  182. package/dist/createCombobox.svelte.d.ts +16 -5
  183. package/dist/createCombobox.svelte.js +13 -2
  184. package/dist/createCountryInput.svelte.d.ts +14 -3
  185. package/dist/createCountryInput.svelte.js +12 -2
  186. package/dist/createDateTimePicker.svelte.d.ts +17 -2
  187. package/dist/createDateTimePicker.svelte.js +12 -2
  188. package/dist/createDropdownList.svelte.d.ts +14 -5
  189. package/dist/createDropdownList.svelte.js +50 -1
  190. package/dist/createFileUpload.svelte.d.ts +68 -0
  191. package/dist/createFileUpload.svelte.js +122 -0
  192. package/dist/createListbox.svelte.d.ts +10 -1
  193. package/dist/createListbox.svelte.js +48 -1
  194. package/dist/createMaskedInput.svelte.d.ts +11 -2
  195. package/dist/createMaskedInput.svelte.js +11 -1
  196. package/dist/createNumberInput.svelte.d.ts +1 -0
  197. package/dist/createNumberInput.svelte.js +1 -0
  198. package/dist/createPasswordInput.svelte.d.ts +19 -21
  199. package/dist/createPasswordInput.svelte.js +16 -4
  200. package/dist/createPhoneInput.svelte.d.ts +26 -7
  201. package/dist/createPhoneInput.svelte.js +24 -3
  202. package/dist/createPopoverSelect.svelte.d.ts +86 -0
  203. package/dist/createPopoverSelect.svelte.js +196 -0
  204. package/dist/createRadioGroup.svelte.d.ts +9 -0
  205. package/dist/createRadioGroup.svelte.js +11 -1
  206. package/dist/createRating.svelte.d.ts +11 -0
  207. package/dist/createRating.svelte.js +12 -1
  208. package/dist/createSlider.svelte.d.ts +2 -0
  209. package/dist/createSlider.svelte.js +16 -5
  210. package/dist/createSplitter.svelte.d.ts +67 -0
  211. package/dist/createSplitter.svelte.js +99 -0
  212. package/dist/createSwitch.svelte.d.ts +9 -16
  213. package/dist/createSwitch.svelte.js +28 -1
  214. package/dist/createTabs.svelte.d.ts +7 -0
  215. package/dist/createTabs.svelte.js +17 -2
  216. package/dist/createTagsInput.svelte.d.ts +13 -1
  217. package/dist/createTagsInput.svelte.js +12 -3
  218. package/dist/createToggle.svelte.d.ts +9 -0
  219. package/dist/createToggle.svelte.js +12 -1
  220. package/dist/createTree.svelte.d.ts +19 -0
  221. package/dist/createTree.svelte.js +108 -15
  222. package/dist/debounce.d.ts +11 -0
  223. package/dist/debounce.js +30 -0
  224. package/dist/duration.d.ts +15 -0
  225. package/dist/duration.js +51 -0
  226. package/dist/editor-contract.d.ts +62 -7
  227. package/dist/editor-contract.js +45 -5
  228. package/dist/editor-registry.d.ts +67 -0
  229. package/dist/editor-registry.js +38 -0
  230. package/dist/editors/cell-editors.d.ts +1 -1
  231. package/dist/editors/cell-editors.js +5 -0
  232. package/dist/fuzzy.d.ts +3 -0
  233. package/dist/fuzzy.js +41 -0
  234. package/dist/grid-select.d.ts +9 -0
  235. package/dist/grid-select.js +8 -0
  236. package/dist/index.d.ts +79 -6
  237. package/dist/index.js +78 -4
  238. package/dist/keyboard-handlers.js +19 -0
  239. package/dist/list-option.d.ts +36 -0
  240. package/dist/list-option.js +68 -0
  241. package/dist/otp.d.ts +16 -0
  242. package/dist/otp.js +26 -0
  243. package/dist/paginate.d.ts +20 -0
  244. package/dist/paginate.js +40 -0
  245. package/dist/popover.d.ts +15 -1
  246. package/dist/popover.js +29 -3
  247. package/dist/recurrence.d.ts +36 -0
  248. package/dist/recurrence.js +90 -0
  249. package/dist/server-data-source.d.ts +89 -0
  250. package/dist/server-data-source.js +4 -0
  251. package/dist/server-group-model.d.ts +98 -0
  252. package/dist/server-group-model.js +263 -0
  253. package/dist/toast-store.svelte.d.ts +41 -0
  254. package/dist/toast-store.svelte.js +113 -0
  255. package/dist/tree-select.d.ts +19 -0
  256. package/dist/tree-select.js +40 -0
  257. package/dist/ui-app.types.d.ts +80 -0
  258. package/dist/ui-app.types.js +7 -0
  259. package/dist/validators.d.ts +43 -0
  260. package/dist/validators.js +53 -0
  261. package/dist/virtual.d.ts +35 -0
  262. package/dist/virtual.js +34 -0
  263. package/package.json +1 -1
  264. package/src/SvAccordion.svelte +109 -0
  265. package/src/SvAccordion.test.ts +66 -0
  266. package/src/SvAlert.svelte +92 -0
  267. package/src/SvAutoComplete.svelte +47 -18
  268. package/src/SvAvatar.svelte +75 -0
  269. package/src/SvAvatarGroup.svelte +48 -0
  270. package/src/SvBadge.svelte +54 -0
  271. package/src/SvBreadcrumb.svelte +88 -0
  272. package/src/SvButtonGroup.svelte +121 -0
  273. package/src/SvButtonGroup.test.ts +72 -0
  274. package/src/SvCalendar.svelte +72 -9
  275. package/src/SvCard.svelte +71 -0
  276. package/src/SvCarousel.svelte +134 -0
  277. package/src/SvCheckBox.svelte +36 -21
  278. package/src/SvChip.svelte +88 -0
  279. package/src/SvCircularProgress.svelte +109 -0
  280. package/src/SvColorInput.svelte +54 -27
  281. package/src/SvComboBox.svelte +119 -33
  282. package/src/SvCommand.svelte +197 -0
  283. package/src/SvContextMenu.svelte +96 -0
  284. package/src/SvCountryInput.svelte +56 -21
  285. package/src/SvDateRangeInput.svelte +256 -0
  286. package/src/SvDateRangeInput.test.ts +80 -0
  287. package/src/SvDateTimePicker.svelte +57 -20
  288. package/src/SvDivider.svelte +61 -0
  289. package/src/SvDrawer.svelte +226 -0
  290. package/src/SvDrawer.test.ts +125 -0
  291. package/src/SvDropDownList.svelte +114 -35
  292. package/src/SvDurationInput.svelte +108 -0
  293. package/src/SvEmptyState.svelte +59 -0
  294. package/src/SvField.svelte +85 -0
  295. package/src/SvFileUpload.svelte +169 -0
  296. package/src/SvFileUpload.test.ts +59 -0
  297. package/src/SvForm.svelte +7 -1
  298. package/src/SvGauge.svelte +7 -1
  299. package/src/SvGrid.controller.svelte.ts +68 -9
  300. package/src/SvGrid.css +33 -0
  301. package/src/SvGrid.svelte +116 -3
  302. package/src/SvGrid.types.ts +374 -0
  303. package/src/SvGridBoard.svelte +2281 -0
  304. package/src/SvGridDropdown.svelte +2 -1
  305. package/src/SvGridSelect.svelte +255 -0
  306. package/src/SvGridSelect.test.ts +100 -0
  307. package/src/SvGroupCell.svelte +109 -0
  308. package/src/SvListBox.svelte +123 -36
  309. package/src/SvMaskedInput.svelte +53 -13
  310. package/src/SvMenu.svelte +101 -0
  311. package/src/SvMenu.test.ts +73 -0
  312. package/src/SvMenuList.svelte +143 -0
  313. package/src/SvModal.svelte +220 -0
  314. package/src/SvMultiSelect.svelte +277 -0
  315. package/src/SvMultiSelect.test.ts +167 -0
  316. package/src/SvNavPane.svelte +314 -0
  317. package/src/SvNavPane.test.ts +70 -0
  318. package/src/SvNumberInput.svelte +23 -9
  319. package/src/SvOtpInput.svelte +157 -0
  320. package/src/SvPagination.svelte +100 -0
  321. package/src/SvPasswordInput.svelte +42 -12
  322. package/src/SvPhoneInput.svelte +43 -11
  323. package/src/SvPopover.svelte +127 -0
  324. package/src/SvProgress.svelte +116 -0
  325. package/src/SvRadioGroup.svelte +40 -22
  326. package/src/SvRating.svelte +51 -30
  327. package/src/SvRichText.svelte +192 -0
  328. package/src/SvRowGroupPanel.svelte +147 -0
  329. package/src/SvScrollArea.svelte +50 -0
  330. package/src/SvSkeleton.svelte +76 -0
  331. package/src/SvSlider.svelte +41 -9
  332. package/src/SvSparkline.svelte +77 -0
  333. package/src/SvSplitter.svelte +138 -0
  334. package/src/SvSplitter.test.ts +55 -0
  335. package/src/SvStat.svelte +86 -0
  336. package/src/SvStepper.svelte +91 -0
  337. package/src/SvSwitchButton.svelte +39 -24
  338. package/src/SvTabs.svelte +147 -28
  339. package/src/SvTagsInput.svelte +59 -22
  340. package/src/SvTextArea.svelte +113 -0
  341. package/src/SvTextInput.svelte +115 -0
  342. package/src/SvTimePicker.svelte +19 -5
  343. package/src/SvTimeline.svelte +75 -0
  344. package/src/SvToaster.svelte +124 -0
  345. package/src/SvToaster.test.ts +63 -0
  346. package/src/SvToggleButton.svelte +37 -12
  347. package/src/SvTooltip.svelte +113 -0
  348. package/src/SvTour.svelte +151 -0
  349. package/src/SvTree.svelte +286 -44
  350. package/src/SvTreeSelect.svelte +222 -0
  351. package/src/SvTreeSelect.test.ts +87 -0
  352. package/src/a11y/dismissable.test.ts +95 -0
  353. package/src/a11y/dismissable.ts +94 -0
  354. package/src/a11y/focus-trap.test.ts +110 -0
  355. package/src/a11y/focus-trap.ts +134 -0
  356. package/src/a11y/live-region.test.ts +40 -0
  357. package/src/a11y/live-region.ts +65 -0
  358. package/src/a11y/scroll-lock.test.ts +38 -0
  359. package/src/a11y/scroll-lock.ts +41 -0
  360. package/src/a11y.ts +3 -1
  361. package/src/avatar.test.ts +32 -0
  362. package/src/avatar.ts +21 -0
  363. package/src/board.test.ts +927 -0
  364. package/src/builtin-editors.grid.test.ts +85 -0
  365. package/src/builtin-editors.test.ts +58 -0
  366. package/src/builtin-editors.ts +56 -0
  367. package/src/columns.ts +3 -2
  368. package/src/core.ts +10 -0
  369. package/src/countries.ts +25 -0
  370. package/src/createAccordion.svelte.ts +126 -0
  371. package/src/createAutocomplete.svelte.ts +18 -1
  372. package/src/createButtonGroup.svelte.ts +166 -0
  373. package/src/createCalendar.svelte.ts +6 -0
  374. package/src/createCheckbox.svelte.ts +23 -1
  375. package/src/createColorInput.svelte.ts +20 -0
  376. package/src/createCombobox.svelte.ts +22 -2
  377. package/src/createCountryInput.svelte.ts +21 -2
  378. package/src/createDateTimePicker.svelte.ts +23 -2
  379. package/src/createDropdownList.svelte.ts +31 -2
  380. package/src/createFileUpload.svelte.ts +144 -0
  381. package/src/createListbox.svelte.ts +35 -3
  382. package/src/createMaskedInput.svelte.ts +18 -1
  383. package/src/createNumberInput.svelte.ts +2 -0
  384. package/src/createPasswordInput.svelte.ts +31 -5
  385. package/src/createPhoneInput.svelte.ts +38 -4
  386. package/src/createPopoverSelect.svelte.ts +208 -0
  387. package/src/createRadioGroup.svelte.ts +23 -1
  388. package/src/createRating.svelte.ts +26 -1
  389. package/src/createSlider.svelte.ts +14 -3
  390. package/src/createSplitter.svelte.ts +119 -0
  391. package/src/createSwitch.svelte.ts +26 -3
  392. package/src/createTabs.svelte.ts +20 -3
  393. package/src/createTagsInput.svelte.ts +24 -3
  394. package/src/createToggle.svelte.ts +25 -2
  395. package/src/createTree.svelte.ts +104 -14
  396. package/src/debounce.test.ts +50 -0
  397. package/src/debounce.ts +41 -0
  398. package/src/duration.test.ts +41 -0
  399. package/src/duration.ts +51 -0
  400. package/src/editor-contract.ts +87 -8
  401. package/src/editor-registry.grid.test.ts +98 -0
  402. package/src/editor-registry.test.ts +86 -0
  403. package/src/editor-registry.ts +106 -0
  404. package/src/editors/cell-editors.ts +10 -1
  405. package/src/fuzzy.test.ts +40 -0
  406. package/src/fuzzy.ts +39 -0
  407. package/src/grid-select.test.ts +28 -0
  408. package/src/grid-select.ts +20 -0
  409. package/src/index.ts +150 -5
  410. package/src/keyboard-handlers.ts +20 -0
  411. package/src/list-option.ts +74 -0
  412. package/src/list-power.test.ts +71 -0
  413. package/src/otp.test.ts +28 -0
  414. package/src/otp.ts +28 -0
  415. package/src/overlays.test.ts +85 -0
  416. package/src/paginate.test.ts +41 -0
  417. package/src/paginate.ts +61 -0
  418. package/src/phone-valid.test.ts +26 -0
  419. package/src/popover.ts +31 -3
  420. package/src/progress.test.ts +55 -0
  421. package/src/recurrence.test.ts +91 -0
  422. package/src/recurrence.ts +111 -0
  423. package/src/server-data-source.ts +102 -0
  424. package/src/server-group-model.test.ts +294 -0
  425. package/src/server-group-model.ts +370 -0
  426. package/src/svgrid.responsive.test.ts +72 -0
  427. package/src/toast-store.svelte.ts +148 -0
  428. package/src/toast-store.test.ts +86 -0
  429. package/src/tree-edit.test.ts +49 -0
  430. package/src/tree-filter.test.ts +56 -0
  431. package/src/tree-select.test.ts +48 -0
  432. package/src/tree-select.ts +62 -0
  433. package/src/ui-app.test.ts +130 -0
  434. package/src/ui-app.types.ts +88 -0
  435. package/src/ui-display.test.ts +79 -0
  436. package/src/ui-inputs.test.ts +1 -1
  437. package/src/ui-status.test.ts +141 -0
  438. package/src/ui-tier1.test.ts +142 -0
  439. package/src/ui-tier2.test.ts +124 -0
  440. package/src/ui-tier3.test.ts +139 -0
  441. package/src/validators.test.ts +58 -0
  442. package/src/validators.ts +105 -0
  443. package/src/virtual.test.ts +72 -0
  444. package/src/virtual.ts +67 -0
@@ -2,15 +2,19 @@
2
2
  * The shared "editor contract" for the SvGrid UI kit.
3
3
  *
4
4
  * Every value-bearing editor (inputs, selects, pickers) accepts this common set
5
- * of props so forms, validation and accessibility behave IDENTICALLY across the
6
- * kit - the same spirit as the grid's headless core, applied to the editors.
7
- * Components spread their own `value` / options / behavior on top of this base.
5
+ * of props so forms, validation, accessibility, direction (RTL) and localization
6
+ * behave IDENTICALLY across the kit - the same spirit as the grid's headless
7
+ * core, applied to the editors. Components spread their own `value` / options /
8
+ * behavior on top of this base.
8
9
  *
9
10
  * Framework-free (no Svelte, no DOM) so it can be unit-tested and reused.
10
11
  */
11
12
 
12
13
  export type EditorSize = 'sm' | 'md' | 'lg'
13
14
 
15
+ /** Text direction. `'auto'` defers to the surrounding document/CSS. */
16
+ export type EditorDir = 'ltr' | 'rtl' | 'auto'
17
+
14
18
  /** Props shared by every value-bearing editor in the kit. */
15
19
  export type SvEditorProps = {
16
20
  /** Disable interaction + form submission. */
@@ -23,26 +27,72 @@ export type SvEditorProps = {
23
27
  invalid?: boolean
24
28
  /** Error message; when set it is announced via `aria-describedby` and shown. */
25
29
  error?: string
30
+ /** Visible field label, rendered above the control and wired via `for`/`id`. */
31
+ label?: string
32
+ /** Helper text shown under the control and announced via `aria-describedby`. */
33
+ hint?: string
26
34
  /** Control size / density. */
27
35
  size?: EditorSize
36
+ /**
37
+ * Text direction. `'rtl'` mirrors layout for Arabic/Hebrew/etc.; `'auto'` (the
38
+ * default) inherits from the surrounding document so a page-level `dir` wins.
39
+ */
40
+ dir?: EditorDir
28
41
  /** Form field name; the editor emits a hidden input carrying its value. */
29
42
  name?: string
30
- /** Root/control element id (the error-text id derives from it). */
43
+ /** Root/control element id (the label/hint/error ids derive from it). */
31
44
  id?: string
32
- /** Accessible name when there is no visible `<label>`. */
45
+ /** Accessible name when there is no visible `label`. */
33
46
  ariaLabel?: string
34
47
  }
35
48
 
49
+ /**
50
+ * The interaction surface an editor exposes when embedded as a grid cell editor.
51
+ * Standalone usage ignores these; the grid supplies them so every editor shares
52
+ * one commit / cancel / move contract (Enter commits, Escape cancels, Tab moves)
53
+ * instead of each cell-editor re-implementing key handling. Consumed in Phase 2
54
+ * (the editor registry).
55
+ */
56
+ export type EditorInteraction<V = unknown> = {
57
+ /** Commit the current value and stop editing (Enter, option pick, blur-commit). */
58
+ onCommit?: (value: V) => void
59
+ /** Abandon the edit and restore the previous value (Escape). */
60
+ onCancel?: () => void
61
+ /** Commit, then move to the adjacent cell (Tab = 1, Shift+Tab = -1). */
62
+ onCommitAndMove?: (value: V, direction: 1 | -1) => void
63
+ /** Ask the surrounding popover/panel to close without committing. */
64
+ onRequestClose?: () => void
65
+ /** True while mounted inside a grid cell (lets an editor tune autofocus etc.). */
66
+ inCell?: boolean
67
+ }
68
+
36
69
  /** Stable DOM id for an editor's error text, for `aria-describedby` wiring. */
37
70
  export const editorErrorId = (id?: string): string | undefined => (id ? `${id}__error` : undefined)
38
71
 
72
+ /** Stable DOM id for an editor's hint text, for `aria-describedby` wiring. */
73
+ export const editorHintId = (id?: string): string | undefined => (id ? `${id}__hint` : undefined)
74
+
75
+ let _autoId = 0
76
+ /**
77
+ * Generate a stable-per-instance element id. Call ONCE per component instance
78
+ * (e.g. `const uid = provided ?? nextEditorId()`) so label/hint/error wiring
79
+ * always has an anchor even when the consumer passes no `id`.
80
+ */
81
+ export const nextEditorId = (prefix = 'sv-ed'): string => `${prefix}-${_autoId++}`
82
+
39
83
  /** A subset of {@link SvEditorProps} that drives ARIA on the focusable control. */
40
- export type EditorAriaState = Pick<SvEditorProps, 'id' | 'invalid' | 'required' | 'error' | 'ariaLabel'>
84
+ export type EditorAriaState = Pick<
85
+ SvEditorProps,
86
+ 'id' | 'invalid' | 'required' | 'error' | 'hint' | 'ariaLabel'
87
+ >
41
88
 
42
89
  /**
43
90
  * ARIA attributes for the focusable control of an editor, derived from its state.
44
91
  * Spread onto the input/button/combobox:
45
- * `<input {...editorAria({ id, invalid, required, error, ariaLabel })} />`
92
+ * `<input {...editorAria({ id, invalid, required, error, hint, ariaLabel })} />`
93
+ *
94
+ * `aria-describedby` points at the error text (when invalid) and/or the hint
95
+ * text - both ids derive from `id`, so pass a stable `id` (see {@link nextEditorId}).
46
96
  * Undefined values are omitted by Svelte, so unset props add no attributes.
47
97
  */
48
98
  export function editorAria(state: EditorAriaState): {
@@ -51,10 +101,39 @@ export function editorAria(state: EditorAriaState): {
51
101
  'aria-describedby': string | undefined
52
102
  'aria-label': string | undefined
53
103
  } {
104
+ const described = [
105
+ state.error ? editorErrorId(state.id) : undefined,
106
+ state.hint ? editorHintId(state.id) : undefined,
107
+ ].filter(Boolean)
54
108
  return {
55
109
  'aria-invalid': state.invalid ? 'true' : undefined,
56
110
  'aria-required': state.required ? 'true' : undefined,
57
- 'aria-describedby': state.error ? editorErrorId(state.id) : undefined,
111
+ 'aria-describedby': described.length ? described.join(' ') : undefined,
58
112
  'aria-label': state.ariaLabel,
59
113
  }
60
114
  }
115
+
116
+ /**
117
+ * Merge a component's default message strings with a consumer's `messages`
118
+ * override, so every editor can be fully localized from the outside.
119
+ *
120
+ * ```ts
121
+ * const M = { clear: 'Clear', noResults: 'No results' }
122
+ * const messages = resolveMessages(M, props.messages) // Partial override
123
+ * ```
124
+ *
125
+ * Undefined/empty overrides fall back to the default, so a partial `messages`
126
+ * object only replaces the keys it sets.
127
+ */
128
+ export function resolveMessages<T extends Record<string, string>>(
129
+ defaults: T,
130
+ overrides?: Partial<T> | null,
131
+ ): T {
132
+ if (!overrides) return defaults
133
+ const out = { ...defaults }
134
+ for (const k in overrides) {
135
+ const v = overrides[k]
136
+ if (v != null && v !== '') out[k] = v as T[Extract<keyof T, string>]
137
+ }
138
+ return out
139
+ }
@@ -0,0 +1,98 @@
1
+ /**
2
+ * Integration test for the cell-editor registry wiring in SvGrid.svelte: a
3
+ * component registered via `registerCellEditor` is actually mounted when a cell
4
+ * of that custom `editorType` starts editing.
5
+ */
6
+ import { afterEach, describe, expect, it } from 'vitest'
7
+ import { mount, unmount } from 'svelte'
8
+ import SvGrid from './SvGrid.svelte'
9
+ import SvRating from './SvRating.svelte'
10
+ import {
11
+ registerCellEditor,
12
+ unregisterCellEditor,
13
+ createCoreRowModel,
14
+ createFilteredRowModel,
15
+ createPaginatedRowModel,
16
+ createSortedRowModel,
17
+ sortFns,
18
+ tableFeatures,
19
+ columnFilteringFeature,
20
+ rowSortingFeature,
21
+ } from './index'
22
+ import type { ColumnDef, SvGridApi } from './index'
23
+
24
+ type Row = { id: number; name: string; score: number }
25
+
26
+ const features = tableFeatures({ columnFilteringFeature, rowSortingFeature })
27
+ const rows: Row[] = [
28
+ { id: 1, name: 'Ada', score: 3 },
29
+ { id: 2, name: 'Alan', score: 5 },
30
+ ]
31
+ const cols: ColumnDef<typeof features, Row>[] = [
32
+ { field: 'name', header: 'Name', width: 200, editorType: 'text' },
33
+ // Custom editor type resolved through the registry:
34
+ { field: 'score', header: 'Score', width: 160, editorType: 'stars' },
35
+ ]
36
+
37
+ const tick = () => new Promise<void>((r) => queueMicrotask(r))
38
+
39
+ function mountGrid() {
40
+ return new Promise<{ api: SvGridApi<typeof features, Row>; target: HTMLElement; destroy: () => void }>(
41
+ (res, rej) => {
42
+ const target = document.createElement('div')
43
+ document.body.appendChild(target)
44
+ const app = mount(SvGrid, {
45
+ target,
46
+ props: {
47
+ data: rows,
48
+ columns: cols,
49
+ features,
50
+ _rowModels: {
51
+ coreRowModel: createCoreRowModel(),
52
+ filteredRowModel: createFilteredRowModel(),
53
+ sortedRowModel: createSortedRowModel(sortFns),
54
+ paginatedRowModel: createPaginatedRowModel(),
55
+ },
56
+ rowHeight: 36,
57
+ containerHeight: 300,
58
+ virtualization: false,
59
+ enableInlineEditing: true,
60
+ onApiReady(api: SvGridApi<typeof features, Row>) {
61
+ res({ api, target, destroy: () => { unmount(app); target.remove() } })
62
+ },
63
+ } as any,
64
+ })
65
+ queueMicrotask(() => rej.length && undefined)
66
+ },
67
+ )
68
+ }
69
+
70
+ afterEach(() => {
71
+ unregisterCellEditor('stars')
72
+ })
73
+
74
+ describe('registry wiring in SvGrid', () => {
75
+ it('mounts a registered component when a custom-editorType cell edits', async () => {
76
+ let committed: unknown = undefined
77
+ registerCellEditor('stars', {
78
+ component: SvRating,
79
+ props: (ctx) => ({
80
+ value: ctx.value,
81
+ onChange: (v: number) => ctx.onCommit(v),
82
+ }),
83
+ })
84
+
85
+ const { api, target, destroy } = await mountGrid()
86
+ try {
87
+ api.startEditing(0, 'score')
88
+ await tick()
89
+ await tick()
90
+ // The registered SvRating editor is mounted in the editing overlay.
91
+ const rating = target.querySelector('.sv-rating')
92
+ expect(rating).not.toBeNull()
93
+ void committed
94
+ } finally {
95
+ destroy()
96
+ }
97
+ })
98
+ })
@@ -0,0 +1,86 @@
1
+ import { afterEach, describe, expect, it, vi } from 'vitest'
2
+ import type { Component } from 'svelte'
3
+ import {
4
+ registerCellEditor,
5
+ getCellEditor,
6
+ hasCellEditor,
7
+ unregisterCellEditor,
8
+ registeredCellEditorTypes,
9
+ defaultEditorProps,
10
+ resolveEditorProps,
11
+ type CellEditorContext,
12
+ } from './editor-registry'
13
+
14
+ // A stand-in component (the registry never invokes it; the grid mounts it).
15
+ const Fake = (() => {}) as unknown as Component<any>
16
+ const Fake2 = (() => {}) as unknown as Component<any>
17
+
18
+ function ctx(overrides: Partial<CellEditorContext> = {}): CellEditorContext {
19
+ return {
20
+ value: 42,
21
+ rowId: 'r1',
22
+ columnId: 'score',
23
+ onChange: () => {},
24
+ onCommit: () => {},
25
+ onCancel: () => {},
26
+ ...overrides,
27
+ }
28
+ }
29
+
30
+ afterEach(() => {
31
+ for (const t of registeredCellEditorTypes()) unregisterCellEditor(t)
32
+ })
33
+
34
+ describe('editor-registry', () => {
35
+ it('registers a bare component with the default mapping', () => {
36
+ registerCellEditor('stars', Fake)
37
+ expect(hasCellEditor('stars')).toBe(true)
38
+ const reg = getCellEditor('stars')!
39
+ expect(reg.component).toBe(Fake)
40
+ expect(reg.props).toBeUndefined()
41
+ })
42
+
43
+ it('registers a full registration with custom prop mapping', () => {
44
+ const props = (c: CellEditorContext) => ({ v: c.value })
45
+ registerCellEditor('slider', { component: Fake, props, autoOpen: true })
46
+ const reg = getCellEditor('slider')!
47
+ expect(reg.component).toBe(Fake)
48
+ expect(reg.autoOpen).toBe(true)
49
+ expect(reg.props).toBe(props)
50
+ })
51
+
52
+ it('re-registering replaces the previous entry', () => {
53
+ registerCellEditor('x', Fake)
54
+ registerCellEditor('x', Fake2)
55
+ expect(getCellEditor('x')!.component).toBe(Fake2)
56
+ })
57
+
58
+ it('unregister + registeredCellEditorTypes reflect the map', () => {
59
+ registerCellEditor('a', Fake)
60
+ registerCellEditor('b', Fake)
61
+ expect(registeredCellEditorTypes().sort()).toEqual(['a', 'b'])
62
+ unregisterCellEditor('a')
63
+ expect(hasCellEditor('a')).toBe(false)
64
+ expect(registeredCellEditorTypes()).toEqual(['b'])
65
+ })
66
+
67
+ it('getCellEditor returns undefined for unknown types', () => {
68
+ expect(getCellEditor('nope')).toBeUndefined()
69
+ })
70
+
71
+ it('defaultEditorProps maps value + the three callbacks', () => {
72
+ const onChange = vi.fn(), onCommit = vi.fn(), onCancel = vi.fn()
73
+ const props = defaultEditorProps(ctx({ onChange, onCommit, onCancel }))
74
+ expect(props).toEqual({ value: 42, onChange, onCommit, onCancel })
75
+ })
76
+
77
+ it('resolveEditorProps uses the custom mapping when present, else default', () => {
78
+ registerCellEditor('custom', { component: Fake, props: (c) => ({ only: c.value }) })
79
+ expect(resolveEditorProps(getCellEditor('custom')!, ctx())).toEqual({ only: 42 })
80
+
81
+ registerCellEditor('plain', Fake)
82
+ const props = resolveEditorProps(getCellEditor('plain')!, ctx())
83
+ expect(props.value).toBe(42)
84
+ expect(typeof props.onCommit).toBe('function')
85
+ })
86
+ })
@@ -0,0 +1,106 @@
1
+ /**
2
+ * editor-registry - the extension point that opens up the grid's cell editors.
3
+ *
4
+ * Historically SvGrid rendered cell editors through a fixed `{#if editorType ===
5
+ * ...}` chain over a closed `CellEditorType` union. This registry lets any
6
+ * component - a built-in `Sv*` editor or one you author - be mounted for a
7
+ * column by name, wired to one uniform in-cell contract (change / commit /
8
+ * cancel). Register once at module load:
9
+ *
10
+ * ```ts
11
+ * import { registerCellEditor } from '@svgrid/grid'
12
+ * import StarRating from './StarRating.svelte'
13
+ * registerCellEditor('stars', StarRating)
14
+ * // then: columns = [{ field: 'score', editorType: 'stars' }]
15
+ * ```
16
+ *
17
+ * The registered component receives the {@link CellEditorContext} as props
18
+ * (value + onChange/onCommit/onCancel), or a custom mapping via `props`.
19
+ * Framework-light: this file only holds the Map + helpers; the grid does the
20
+ * mounting.
21
+ */
22
+ import type { Component } from 'svelte'
23
+
24
+ /** Context the grid hands a cell editor when it mounts one for an edit. */
25
+ export type CellEditorContext<T = unknown> = {
26
+ /** The value currently being edited. */
27
+ value: T
28
+ /** Id of the row being edited. */
29
+ rowId: string
30
+ /** Id of the column being edited. */
31
+ columnId: string
32
+ /** Update the in-progress value WITHOUT ending the edit. */
33
+ onChange: (value: T) => void
34
+ /** Commit the value and stop editing (optionally pass a final value). */
35
+ onCommit: (value?: T) => void
36
+ /** Discard changes and stop editing (returns focus to the grid). */
37
+ onCancel: () => void
38
+ }
39
+
40
+ /** How a registered component is mounted and wired for a cell edit. */
41
+ export type CellEditorRegistration = {
42
+ /** The Svelte component to mount in the editing cell. */
43
+ component: Component<any>
44
+ /**
45
+ * Map the editing context to the component's props. Defaults to
46
+ * {@link defaultEditorProps} (value + onChange/onCommit/onCancel).
47
+ */
48
+ props?: (ctx: CellEditorContext) => Record<string, unknown>
49
+ /** Hint that the editor should open its popover immediately on mount. */
50
+ autoOpen?: boolean
51
+ }
52
+
53
+ const registry = new Map<string, CellEditorRegistration>()
54
+
55
+ /**
56
+ * Register a cell editor under `type`. Pass a component (uses the default prop
57
+ * mapping) or a full {@link CellEditorRegistration} for custom prop mapping.
58
+ * Re-registering the same type replaces it.
59
+ */
60
+ export function registerCellEditor(
61
+ type: string,
62
+ registration: CellEditorRegistration | Component<any>,
63
+ ): void {
64
+ registry.set(
65
+ type,
66
+ typeof registration === 'function' ? { component: registration } : registration,
67
+ )
68
+ }
69
+
70
+ /** Look up a registered cell editor by type. */
71
+ export function getCellEditor(type: string): CellEditorRegistration | undefined {
72
+ return registry.get(type)
73
+ }
74
+
75
+ /** Whether a custom editor is registered for `type`. */
76
+ export function hasCellEditor(type: string): boolean {
77
+ return registry.has(type)
78
+ }
79
+
80
+ /** Remove a registered cell editor. */
81
+ export function unregisterCellEditor(type: string): void {
82
+ registry.delete(type)
83
+ }
84
+
85
+ /** All registered custom editor type names. */
86
+ export function registeredCellEditorTypes(): string[] {
87
+ return [...registry.keys()]
88
+ }
89
+
90
+ /** The default context → props mapping used when a registration has no `props`. */
91
+ export function defaultEditorProps(ctx: CellEditorContext): Record<string, unknown> {
92
+ return {
93
+ value: ctx.value,
94
+ onChange: ctx.onChange,
95
+ onCommit: ctx.onCommit,
96
+ onCancel: ctx.onCancel,
97
+ }
98
+ }
99
+
100
+ /** Resolve the props to spread onto a registered editor for a given context. */
101
+ export function resolveEditorProps(
102
+ registration: CellEditorRegistration,
103
+ ctx: CellEditorContext,
104
+ ): Record<string, unknown> {
105
+ return registration.props ? registration.props(ctx) : defaultEditorProps(ctx)
106
+ }
@@ -1,4 +1,6 @@
1
- export type CellEditorType =
1
+ import { hasCellEditor } from '../editor-registry'
2
+
3
+ export type CellEditorType =
2
4
  | 'text'
3
5
  | 'number'
4
6
  | 'date'
@@ -18,6 +20,10 @@
18
20
  | 'textarea'
19
21
  | 'color'
20
22
  | 'rating'
23
+ // Any other string names a CUSTOM editor registered via `registerCellEditor`.
24
+ // `(string & {})` keeps the literals above autocompleting while allowing
25
+ // arbitrary custom type names.
26
+ | (string & {})
21
27
 
22
28
  /** Normalized option used by list/chips editors. The optional `color`
23
29
  * paints the chip - both in the in-cell readonly chips render and in
@@ -118,6 +124,9 @@ export function parseEditorValue(
118
124
  if (type === 'textarea') {
119
125
  return String(value ?? '')
120
126
  }
127
+ // Custom registered editors own their value shape (like list/chips), so pass
128
+ // whatever the editor committed through unchanged instead of stringifying it.
129
+ if (hasCellEditor(type)) return value
121
130
  if (type === 'list' || type === 'chips') {
122
131
  // The editor manages the value shape: a scalar for single-select, an
123
132
  // array for multi-select. Trust what the editor handed us, just coerce
@@ -0,0 +1,40 @@
1
+ import { describe, expect, it } from 'vitest'
2
+ import { fuzzyScore, fuzzyMatch } from './fuzzy'
3
+
4
+ describe('fuzzyScore', () => {
5
+ it('matches a subsequence, rejects a non-subsequence', () => {
6
+ expect(fuzzyScore('New invoice', 'ni')).not.toBeNull()
7
+ expect(fuzzyScore('New invoice', 'niv')).not.toBeNull()
8
+ expect(fuzzyScore('New invoice', 'xyz')).toBeNull()
9
+ expect(fuzzyScore('New invoice', 'ivn')).toBeNull() // out of order
10
+ })
11
+
12
+ it('is case-insensitive and returns 0 for an empty query', () => {
13
+ expect(fuzzyMatch('Export CSV', 'EXPORT')).toBe(true)
14
+ expect(fuzzyScore('anything', '')).toBe(0)
15
+ })
16
+
17
+ it('scores a contiguous / prefix match higher than a scattered one', () => {
18
+ const prefix = fuzzyScore('Settings', 'set')!
19
+ const scattered = fuzzyScore('Silent tester', 'set')!
20
+ expect(prefix).toBeGreaterThan(scattered)
21
+ })
22
+
23
+ it('rewards word-boundary starts', () => {
24
+ const boundary = fuzzyScore('Open file', 'of')! // O..f (word starts)
25
+ const inner = fuzzyScore('Profile', 'of')! // ...of.. inside a word
26
+ expect(boundary).toBeGreaterThan(inner)
27
+ })
28
+
29
+ it('filters a list to matches only', () => {
30
+ const cmds = ['New invoice', 'New order', 'Export invoices', 'Delete invoice']
31
+ const ranked = cmds
32
+ .map((c) => ({ c, s: fuzzyScore(c, 'inv') }))
33
+ .filter((x) => x.s !== null)
34
+ .sort((a, b) => b.s! - a.s!)
35
+ .map((x) => x.c)
36
+ expect(ranked).toHaveLength(3) // "New order" has no 'inv' subsequence
37
+ expect(ranked).not.toContain('New order')
38
+ expect(ranked).toContain('New invoice')
39
+ })
40
+ })
package/src/fuzzy.ts ADDED
@@ -0,0 +1,39 @@
1
+ /**
2
+ * fuzzy - a tiny subsequence fuzzy matcher for command palettes / quick filters.
3
+ * `fuzzyScore(text, query)` returns a score (higher = better) when every query
4
+ * character appears in order in `text`, or `null` when it doesn't. Contiguous
5
+ * runs, word-boundary starts and early matches score higher. Case-insensitive,
6
+ * pure, dependency-free.
7
+ */
8
+ const BOUNDARY = /[\s\-_/.]/
9
+
10
+ export function fuzzyScore(text: string, query: string): number | null {
11
+ const q = query.trim().toLowerCase()
12
+ if (!q) return 0
13
+ const t = text.toLowerCase()
14
+ let from = 0
15
+ let score = 0
16
+ let run = 0
17
+ let prev = -2
18
+ for (const ch of q) {
19
+ const idx = t.indexOf(ch, from)
20
+ if (idx < 0) return null
21
+ if (idx === prev + 1) {
22
+ run += 1
23
+ score += 5 + run * 2 // contiguous run
24
+ } else {
25
+ run = 0
26
+ score += 1
27
+ }
28
+ if (idx === 0 || BOUNDARY.test(t[idx - 1] ?? '')) score += 8 // word start
29
+ score += Math.max(0, 3 - Math.floor(idx / 4)) // earliness
30
+ prev = idx
31
+ from = idx + 1
32
+ }
33
+ return score
34
+ }
35
+
36
+ /** Whether `query` fuzzy-matches `text`. */
37
+ export function fuzzyMatch(text: string, query: string): boolean {
38
+ return fuzzyScore(text, query) !== null
39
+ }
@@ -0,0 +1,28 @@
1
+ import { describe, expect, it } from 'vitest'
2
+ import { filterGridRows } from './grid-select'
3
+
4
+ const rows = [
5
+ { id: 1, name: 'Ada Lovelace', email: 'ada@x.io' },
6
+ { id: 2, name: 'Alan Turing', email: 'alan@x.io' },
7
+ { id: 3, name: 'Grace Hopper', email: 'grace@x.io' },
8
+ ]
9
+
10
+ describe('filterGridRows', () => {
11
+ it('returns every row for a blank query', () => {
12
+ expect(filterGridRows(rows, ['name', 'email'], '')).toHaveLength(3)
13
+ expect(filterGridRows(rows, ['name'], ' ')).toHaveLength(3)
14
+ })
15
+
16
+ it('matches a substring in any of the given fields (case-insensitive)', () => {
17
+ expect(filterGridRows(rows, ['name', 'email'], 'grace').map((r) => r.id)).toEqual([3])
18
+ expect(filterGridRows(rows, ['name', 'email'], 'ALAN').map((r) => r.id)).toEqual([2])
19
+ // "a" appears in every name -> all 3
20
+ expect(filterGridRows(rows, ['name'], 'a')).toHaveLength(3)
21
+ })
22
+
23
+ it('only searches the fields it is given', () => {
24
+ // 'x.io' is in email but we only search name -> no matches
25
+ expect(filterGridRows(rows, ['name'], 'x.io')).toHaveLength(0)
26
+ expect(filterGridRows(rows, ['email'], 'x.io')).toHaveLength(3)
27
+ })
28
+ })
@@ -0,0 +1,20 @@
1
+ /**
2
+ * grid-select - pure helper behind <SvGridSelect>: filter option rows by a
3
+ * free-text query across a set of fields (case-insensitive substring match).
4
+ * Framework-free + pure so it is unit-tested directly.
5
+ */
6
+ export type Row = Record<string, unknown>
7
+
8
+ /** Rows where ANY of `fields` contains `query` (case-insensitive). Blank query
9
+ * returns every row. */
10
+ export function filterGridRows<T extends Row>(
11
+ rows: ReadonlyArray<T>,
12
+ fields: ReadonlyArray<string>,
13
+ query: string,
14
+ ): T[] {
15
+ const q = query.trim().toLowerCase()
16
+ if (!q) return [...rows]
17
+ return rows.filter((row) =>
18
+ fields.some((f) => String(row[f] ?? '').toLowerCase().includes(q)),
19
+ )
20
+ }