@svgrid/grid 1.2.22 → 1.2.24

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 (263) hide show
  1. package/dist/GridFooter.svelte +34 -20
  2. package/dist/GridFooter.svelte.d.ts +8 -0
  3. package/dist/SvAutoComplete.svelte +89 -0
  4. package/dist/SvAutoComplete.svelte.d.ts +16 -0
  5. package/dist/SvButton.svelte +97 -0
  6. package/dist/SvButton.svelte.d.ts +23 -0
  7. package/dist/SvCalendar.svelte +409 -0
  8. package/dist/SvCalendar.svelte.d.ts +52 -0
  9. package/dist/SvCheckBox.svelte +82 -0
  10. package/dist/SvCheckBox.svelte.d.ts +19 -0
  11. package/dist/SvColorInput.svelte +160 -0
  12. package/dist/SvColorInput.svelte.d.ts +15 -0
  13. package/dist/SvComboBox.svelte +108 -0
  14. package/dist/SvComboBox.svelte.d.ts +15 -0
  15. package/dist/SvCountryInput.svelte +125 -0
  16. package/dist/SvCountryInput.svelte.d.ts +14 -0
  17. package/dist/SvDateTimePicker.svelte +265 -0
  18. package/dist/SvDateTimePicker.svelte.d.ts +38 -0
  19. package/dist/SvDropDownList.svelte +120 -0
  20. package/dist/SvDropDownList.svelte.d.ts +15 -0
  21. package/dist/SvForm.svelte +147 -0
  22. package/dist/SvForm.svelte.d.ts +29 -0
  23. package/dist/SvGauge.svelte +97 -0
  24. package/dist/SvGauge.svelte.d.ts +26 -0
  25. package/dist/SvGrid.controller.svelte.d.ts +16 -2
  26. package/dist/SvGrid.controller.svelte.js +63 -8
  27. package/dist/SvGrid.css +84 -31
  28. package/dist/SvGrid.helpers.d.ts +4 -0
  29. package/dist/SvGrid.helpers.js +40 -12
  30. package/dist/SvGrid.svelte +84 -5
  31. package/dist/SvGrid.types.d.ts +36 -0
  32. package/dist/SvGridDropdown.svelte +16 -30
  33. package/dist/SvGridDropdown.svelte.d.ts +4 -0
  34. package/dist/SvListBox.svelte +89 -0
  35. package/dist/SvListBox.svelte.d.ts +22 -0
  36. package/dist/SvMaskedInput.svelte +66 -0
  37. package/dist/SvMaskedInput.svelte.d.ts +15 -0
  38. package/dist/SvNumberInput.svelte +116 -0
  39. package/dist/SvNumberInput.svelte.d.ts +18 -0
  40. package/dist/SvPasswordInput.svelte +100 -0
  41. package/dist/SvPasswordInput.svelte.d.ts +18 -0
  42. package/dist/SvPhoneInput.svelte +87 -0
  43. package/dist/SvPhoneInput.svelte.d.ts +19 -0
  44. package/dist/SvRadioGroup.svelte +85 -0
  45. package/dist/SvRadioGroup.svelte.d.ts +20 -0
  46. package/dist/SvRating.svelte +91 -0
  47. package/dist/SvRating.svelte.d.ts +18 -0
  48. package/dist/SvRepeatButton.svelte +89 -0
  49. package/dist/SvRepeatButton.svelte.d.ts +21 -0
  50. package/dist/SvSlider.svelte +155 -0
  51. package/dist/SvSlider.svelte.d.ts +20 -0
  52. package/dist/SvSwitchButton.svelte +87 -0
  53. package/dist/SvSwitchButton.svelte.d.ts +14 -0
  54. package/dist/SvTabs.svelte +109 -0
  55. package/dist/SvTabs.svelte.d.ts +26 -0
  56. package/dist/SvTagsInput.svelte +70 -0
  57. package/dist/SvTagsInput.svelte.d.ts +14 -0
  58. package/dist/SvTimePicker.svelte +201 -0
  59. package/dist/SvTimePicker.svelte.d.ts +30 -0
  60. package/dist/SvToggleButton.svelte +57 -0
  61. package/dist/SvToggleButton.svelte.d.ts +16 -0
  62. package/dist/SvTree.svelte +131 -0
  63. package/dist/SvTree.svelte.d.ts +27 -0
  64. package/dist/build-api.js +78 -1
  65. package/dist/cdn/svgrid.js +11674 -5907
  66. package/dist/cdn/svgrid.svelte-external.js +10436 -4671
  67. package/dist/cell-render.d.ts +4 -0
  68. package/dist/cell-render.js +35 -1
  69. package/dist/clipboard.js +19 -1
  70. package/dist/collaboration.d.ts +2 -0
  71. package/dist/collaboration.js +4 -0
  72. package/dist/core.d.ts +22 -1
  73. package/dist/countries.d.ts +15 -0
  74. package/dist/countries.js +61 -0
  75. package/dist/createAutocomplete.svelte.d.ts +77 -0
  76. package/dist/createAutocomplete.svelte.js +100 -0
  77. package/dist/createCalendar.svelte.d.ts +192 -0
  78. package/dist/createCalendar.svelte.js +402 -0
  79. package/dist/createCheckbox.svelte.d.ts +45 -0
  80. package/dist/createCheckbox.svelte.js +26 -0
  81. package/dist/createColorInput.svelte.d.ts +67 -0
  82. package/dist/createColorInput.svelte.js +100 -0
  83. package/dist/createCombobox.svelte.d.ts +102 -0
  84. package/dist/createCombobox.svelte.js +171 -0
  85. package/dist/createCountryInput.svelte.d.ts +86 -0
  86. package/dist/createCountryInput.svelte.js +126 -0
  87. package/dist/createDateTimePicker.svelte.d.ts +83 -0
  88. package/dist/createDateTimePicker.svelte.js +202 -0
  89. package/dist/createDropdownList.svelte.d.ts +78 -0
  90. package/dist/createDropdownList.svelte.js +117 -0
  91. package/dist/createGauge.svelte.d.ts +56 -0
  92. package/dist/createGauge.svelte.js +49 -0
  93. package/dist/createListbox.svelte.d.ts +79 -0
  94. package/dist/createListbox.svelte.js +117 -0
  95. package/dist/createMaskedInput.svelte.d.ts +30 -0
  96. package/dist/createMaskedInput.svelte.js +62 -0
  97. package/dist/createNumberInput.svelte.d.ts +72 -0
  98. package/dist/createNumberInput.svelte.js +167 -0
  99. package/dist/createPasswordInput.svelte.d.ts +60 -0
  100. package/dist/createPasswordInput.svelte.js +72 -0
  101. package/dist/createPhoneInput.svelte.d.ts +46 -0
  102. package/dist/createPhoneInput.svelte.js +105 -0
  103. package/dist/createRadioGroup.svelte.d.ts +64 -0
  104. package/dist/createRadioGroup.svelte.js +73 -0
  105. package/dist/createRating.svelte.d.ts +70 -0
  106. package/dist/createRating.svelte.js +85 -0
  107. package/dist/createSlider.svelte.d.ts +71 -0
  108. package/dist/createSlider.svelte.js +123 -0
  109. package/dist/createSwitch.svelte.d.ts +43 -0
  110. package/dist/createSwitch.svelte.js +41 -0
  111. package/dist/createTabs.svelte.d.ts +78 -0
  112. package/dist/createTabs.svelte.js +92 -0
  113. package/dist/createTagsInput.svelte.d.ts +65 -0
  114. package/dist/createTagsInput.svelte.js +93 -0
  115. package/dist/createTimePicker.svelte.d.ts +106 -0
  116. package/dist/createTimePicker.svelte.js +248 -0
  117. package/dist/createToggle.svelte.d.ts +43 -0
  118. package/dist/createToggle.svelte.js +39 -0
  119. package/dist/createTree.svelte.d.ts +96 -0
  120. package/dist/createTree.svelte.js +194 -0
  121. package/dist/datetime/date-core.d.ts +88 -0
  122. package/dist/datetime/date-core.js +165 -0
  123. package/dist/datetime/date-format.d.ts +39 -0
  124. package/dist/datetime/date-format.js +371 -0
  125. package/dist/datetime/date-restrict.d.ts +23 -0
  126. package/dist/datetime/date-restrict.js +50 -0
  127. package/dist/datetime/date-selection.d.ts +25 -0
  128. package/dist/datetime/date-selection.js +94 -0
  129. package/dist/datetime/mask.d.ts +21 -0
  130. package/dist/datetime/mask.js +90 -0
  131. package/dist/editing.d.ts +1 -1
  132. package/dist/editing.js +7 -0
  133. package/dist/editor-contract.d.ts +48 -0
  134. package/dist/editor-contract.js +26 -0
  135. package/dist/editors/cell-editors.d.ts +2 -2
  136. package/dist/editors/cell-editors.js +3 -1
  137. package/dist/export-format.d.ts +178 -0
  138. package/dist/export-format.js +419 -0
  139. package/dist/hyperformula-adapter.d.ts +4 -0
  140. package/dist/hyperformula-adapter.js +15 -6
  141. package/dist/index.d.ts +60 -0
  142. package/dist/index.js +72 -0
  143. package/dist/list-option.d.ts +10 -0
  144. package/dist/list-option.js +7 -0
  145. package/dist/menus.js +18 -2
  146. package/dist/popover.d.ts +48 -0
  147. package/dist/popover.js +65 -0
  148. package/dist/row-resize.d.ts +4 -4
  149. package/dist/row-resize.js +8 -5
  150. package/dist/selection.d.ts +6 -0
  151. package/dist/selection.js +31 -5
  152. package/dist/server-data-source.d.ts +43 -0
  153. package/dist/server-data-source.js +100 -0
  154. package/dist/sparkline.js +8 -4
  155. package/dist/svgrid-wrapper.types.d.ts +26 -1
  156. package/package.json +6 -1
  157. package/src/GridFooter.svelte +34 -20
  158. package/src/SvAutoComplete.svelte +89 -0
  159. package/src/SvButton.svelte +97 -0
  160. package/src/SvCalendar.svelte +409 -0
  161. package/src/SvCalendar.test.ts +211 -0
  162. package/src/SvCheckBox.svelte +82 -0
  163. package/src/SvColorInput.svelte +160 -0
  164. package/src/SvComboBox.svelte +108 -0
  165. package/src/SvCountryInput.svelte +125 -0
  166. package/src/SvDateTimePicker.svelte +265 -0
  167. package/src/SvDateTimePicker.test.ts +136 -0
  168. package/src/SvDropDownList.svelte +120 -0
  169. package/src/SvForm.svelte +147 -0
  170. package/src/SvGauge.svelte +97 -0
  171. package/src/SvGrid.controller.svelte.ts +74 -8
  172. package/src/SvGrid.css +84 -31
  173. package/src/SvGrid.helpers.ts +41 -12
  174. package/src/SvGrid.svelte +84 -5
  175. package/src/SvGrid.types.ts +33 -0
  176. package/src/SvGridDropdown.svelte +16 -30
  177. package/src/SvListBox.svelte +89 -0
  178. package/src/SvMaskedInput.svelte +66 -0
  179. package/src/SvNumberInput.svelte +116 -0
  180. package/src/SvPasswordInput.svelte +100 -0
  181. package/src/SvPhoneInput.svelte +87 -0
  182. package/src/SvRadioGroup.svelte +85 -0
  183. package/src/SvRating.svelte +91 -0
  184. package/src/SvRepeatButton.svelte +89 -0
  185. package/src/SvSlider.svelte +155 -0
  186. package/src/SvSwitchButton.svelte +87 -0
  187. package/src/SvTabs.svelte +109 -0
  188. package/src/SvTagsInput.svelte +70 -0
  189. package/src/SvTimePicker.svelte +201 -0
  190. package/src/SvTimePicker.test.ts +98 -0
  191. package/src/SvToggleButton.svelte +57 -0
  192. package/src/SvTree.svelte +131 -0
  193. package/src/build-api.ts +115 -0
  194. package/src/cell-render.test.ts +38 -0
  195. package/src/cell-render.ts +43 -0
  196. package/src/clipboard.test.ts +18 -0
  197. package/src/clipboard.ts +24 -1
  198. package/src/collaboration.test.ts +30 -0
  199. package/src/collaboration.ts +6 -0
  200. package/src/core.ts +27 -3
  201. package/src/countries.ts +71 -0
  202. package/src/createAutocomplete.svelte.ts +104 -0
  203. package/src/createCalendar.svelte.ts +488 -0
  204. package/src/createCheckbox.svelte.ts +74 -0
  205. package/src/createColorInput.svelte.ts +115 -0
  206. package/src/createCombobox.svelte.ts +158 -0
  207. package/src/createCountryInput.svelte.ts +126 -0
  208. package/src/createDateTimePicker.svelte.ts +216 -0
  209. package/src/createDropdownList.svelte.ts +125 -0
  210. package/src/createGauge.svelte.ts +86 -0
  211. package/src/createListbox.svelte.ts +186 -0
  212. package/src/createMaskedInput.svelte.ts +81 -0
  213. package/src/createNumberInput.svelte.ts +193 -0
  214. package/src/createPasswordInput.svelte.ts +87 -0
  215. package/src/createPhoneInput.svelte.ts +119 -0
  216. package/src/createRadioGroup.svelte.ts +132 -0
  217. package/src/createRating.svelte.ts +145 -0
  218. package/src/createSlider.svelte.ts +160 -0
  219. package/src/createSwitch.svelte.ts +78 -0
  220. package/src/createTabs.svelte.ts +129 -0
  221. package/src/createTagsInput.svelte.ts +98 -0
  222. package/src/createTimePicker.svelte.ts +289 -0
  223. package/src/createToggle.svelte.ts +69 -0
  224. package/src/createTree.svelte.ts +216 -0
  225. package/src/datetime/date-core.test.ts +217 -0
  226. package/src/datetime/date-core.ts +204 -0
  227. package/src/datetime/date-format.test.ts +121 -0
  228. package/src/datetime/date-format.ts +317 -0
  229. package/src/datetime/date-restrict.ts +60 -0
  230. package/src/datetime/date-selection.test.ts +129 -0
  231. package/src/datetime/date-selection.ts +137 -0
  232. package/src/datetime/mask.test.ts +36 -0
  233. package/src/datetime/mask.ts +84 -0
  234. package/src/editing.test.ts +22 -0
  235. package/src/editing.ts +7 -1
  236. package/src/editor-contract.test.ts +32 -0
  237. package/src/editor-contract.ts +60 -0
  238. package/src/editors/cell-editors.ts +7 -1
  239. package/src/export-data-api.test.ts +126 -0
  240. package/src/export-format.test.ts +107 -0
  241. package/src/export-format.ts +598 -0
  242. package/src/hyperformula-adapter.test.ts +35 -1
  243. package/src/hyperformula-adapter.ts +18 -6
  244. package/src/index.ts +110 -0
  245. package/src/list-option.ts +15 -0
  246. package/src/menus.ts +21 -2
  247. package/src/popover.ts +79 -0
  248. package/src/row-resize.test.ts +25 -0
  249. package/src/row-resize.ts +8 -5
  250. package/src/selection.test.ts +36 -4
  251. package/src/selection.ts +30 -9
  252. package/src/server-data-source.test.ts +201 -0
  253. package/src/server-data-source.ts +148 -0
  254. package/src/sparkline.test.ts +9 -0
  255. package/src/sparkline.ts +9 -4
  256. package/src/svgrid-wrapper.types.ts +28 -1
  257. package/src/svgrid.behavior.test.ts +27 -0
  258. package/src/svgrid.new-features.wrapper.test.ts +27 -1
  259. package/src/ui-buttons.test.ts +105 -0
  260. package/src/ui-composite.test.ts +89 -0
  261. package/src/ui-inputs.test.ts +118 -0
  262. package/src/ui-range.test.ts +61 -0
  263. package/src/ui-selection.test.ts +119 -0
@@ -24,6 +24,8 @@
24
24
  } from "./render-component";
25
25
  import { buildSparkline, toSparklineValues } from "./sparkline";
26
26
  import SvGridDropdown from "./SvGridDropdown.svelte";
27
+ import SvDateTimePicker from "./SvDateTimePicker.svelte";
28
+ import { timeStringToDate, dateToTimeString } from "./SvGrid.helpers";
27
29
  import type {
28
30
  Props,
29
31
  SelectionPoint,
@@ -246,6 +248,7 @@
246
248
  const computeRowClass = $derived(ctrl.computeRowClass);
247
249
  const computeCellClass = $derived(ctrl.computeCellClass);
248
250
  const computeCellTooltip = $derived(ctrl.computeCellTooltip);
251
+ const computeCellValidity = $derived(ctrl.computeCellValidity);
249
252
  const computeCellNote = $derived(ctrl.computeCellNote);
250
253
  const getCellDisplayValue = $derived(ctrl.getCellDisplayValue);
251
254
  const getColumnAlign = $derived(ctrl.getColumnAlign);
@@ -266,6 +269,7 @@
266
269
  const getCellRangeEdges = $derived(ctrl.getCellRangeEdges);
267
270
  const fillHandleCell = $derived(ctrl.fillHandleCell);
268
271
  const isInFillPreview = $derived(ctrl.isInFillPreview);
272
+ const fillMarqueeEdges = $derived(ctrl.fillMarqueeEdges);
269
273
  const startFillDrag = $derived(ctrl.startFillDrag);
270
274
  const onCellPointerDown = $derived(ctrl.onCellPointerDown);
271
275
  const onCellPointerEnter = $derived(ctrl.onCellPointerEnter);
@@ -306,7 +310,12 @@
306
310
  onpointermove={onWindowPointerMove}
307
311
  />
308
312
 
309
- {#if props.loading && !props.loadingOverlay}
313
+ {#if props.loading && !props.loadingOverlay && !hasMeasured}
314
+ <!-- Full-screen loading state only on the *initial* load, before the grid
315
+ has ever rendered. Once measured, a `loading` flip (from a server-mode
316
+ sort / filter / page refetch) keeps the table mounted so header inputs
317
+ and focus survive - the empty-row message + optional `loadingOverlay`
318
+ cover the in-place refresh. -->
310
319
  <div class="sv-grid-state sv-grid-state-loading" role="status">
311
320
  Loading grid data...
312
321
  </div>
@@ -798,6 +807,49 @@
798
807
  </div>
799
808
  {/if}
800
809
  </div>
810
+ {:else if ctrl.editingCell?.editorType === "date"}
811
+ <!-- Rich date editor: SvCalendar popover over a formatted input. Opts
812
+ out to the native <input type="date"> via editorType 'date-native'. -->
813
+ <SvDateTimePicker
814
+ value={ctrl.editingCell?.value as string | number | Date | null}
815
+ formatString="yyyy-MM-dd"
816
+ dropDownDisplayMode="calendar"
817
+ autoOpen
818
+ onChange={(d) => updateEditingCellValue(d)}
819
+ onCommit={() => saveEditingCell()}
820
+ onCancel={() => {
821
+ ctrl.editingCell = null;
822
+ ctrl.gridRootEl?.focus({ preventScroll: true });
823
+ }}
824
+ />
825
+ {:else if ctrl.editingCell?.editorType === "datetime"}
826
+ <SvDateTimePicker
827
+ value={ctrl.editingCell?.value as string | number | Date | null}
828
+ formatString="yyyy-MM-dd HH:mm"
829
+ dropDownDisplayMode="both"
830
+ autoOpen
831
+ onChange={(d) => updateEditingCellValue(d)}
832
+ onCommit={() => saveEditingCell()}
833
+ onCancel={() => {
834
+ ctrl.editingCell = null;
835
+ ctrl.gridRootEl?.focus({ preventScroll: true });
836
+ }}
837
+ />
838
+ {:else if ctrl.editingCell?.editorType === "time"}
839
+ <!-- Rich time editor: SvTimePicker dial. Stores the 'HH:MM' string the
840
+ `time` parser expects; opts out via editorType 'time-native'. -->
841
+ <SvDateTimePicker
842
+ value={timeStringToDate(ctrl.editingCell?.value)}
843
+ formatString="HH:mm"
844
+ dropDownDisplayMode="time"
845
+ autoOpen
846
+ onChange={(d) => updateEditingCellValue(dateToTimeString(d))}
847
+ onCommit={() => saveEditingCell()}
848
+ onCancel={() => {
849
+ ctrl.editingCell = null;
850
+ ctrl.gridRootEl?.focus({ preventScroll: true });
851
+ }}
852
+ />
801
853
  {:else if ctrl.editingCell?.editorType === "color"}
802
854
  <!-- Native <input type="color"> opens its picker in a separate OS
803
855
  overlay; once the picker closes, focus stays on the input so
@@ -1179,6 +1231,10 @@
1179
1231
  </div>
1180
1232
  {/if}
1181
1233
 
1234
+ {#if hasMeasured && (props.paginationPosition === "top" || props.paginationPosition === "both")}
1235
+ <GridFooter {ctrl} showStatus={false} top pager pageSizeOptions={props.pageSizeOptions} />
1236
+ {/if}
1237
+
1182
1238
  <div
1183
1239
  class="sv-grid-shell"
1184
1240
  style={`height: ${
@@ -1197,6 +1253,7 @@
1197
1253
  <table
1198
1254
  bind:this={ctrl.gridRootEl}
1199
1255
  class="sv-grid-table"
1256
+ class:sv-grid-no-row-hover={props.enableRowHover !== true}
1200
1257
  {...getGridRootA11yProps({
1201
1258
  activeDescendantId,
1202
1259
  rowCount: allRows.length,
@@ -1795,6 +1852,7 @@
1795
1852
  rowIndex,
1796
1853
  colIndex,
1797
1854
  )}
1855
+ {@const fillEdges = fillMarqueeEdges(rowIndex, colIndex)}
1798
1856
  {@const hasFillHandle =
1799
1857
  fillHandleCell &&
1800
1858
  fillHandleCell.rowIndex === rowIndex &&
@@ -1807,6 +1865,10 @@
1807
1865
  row,
1808
1866
  rendered.column,
1809
1867
  )}
1868
+ {@const cellValidity = computeCellValidity(
1869
+ row,
1870
+ rendered.column,
1871
+ )}
1810
1872
  {@const cellNote = computeCellNote(
1811
1873
  row,
1812
1874
  rendered.column,
@@ -1818,7 +1880,9 @@
1818
1880
  rowIndex && activeCell.colIndex === colIndex}
1819
1881
  class:sv-grid-cell-has-fill-handle={hasFillHandle}
1820
1882
  class:sv-grid-cell-cf={hasConditionalFormats}
1883
+ class:sv-grid-cell-invalid={cellValidity.invalid}
1821
1884
  class:sv-grid-cell-has-note={cellNote != null}
1885
+ title={cellValidity.message ?? undefined}
1822
1886
  data-svgrid-row={rowIndex}
1823
1887
  data-svgrid-col={colIndex}
1824
1888
  data-col-id={rendered.column.id}
@@ -1839,6 +1903,10 @@
1839
1903
  data-fill-preview={isInFillPreview(rowIndex, colIndex)
1840
1904
  ? "true"
1841
1905
  : undefined}
1906
+ data-fill-top={fillEdges?.top ? "true" : undefined}
1907
+ data-fill-bottom={fillEdges?.bottom ? "true" : undefined}
1908
+ data-fill-left={fillEdges?.left ? "true" : undefined}
1909
+ data-fill-right={fillEdges?.right ? "true" : undefined}
1842
1910
  style={`width: ${rendered.item.size}px; min-width: ${rendered.item.size}px; max-width: ${rendered.item.size}px; ${cellPinStyle(rendered.column.id)}`}
1843
1911
  onpointerdown={(event) =>
1844
1912
  onCellPointerDown(rowIndex, colIndex, event)}
@@ -2040,6 +2108,10 @@
2040
2108
  row,
2041
2109
  rendered.column,
2042
2110
  )}
2111
+ {@const cellValidity = computeCellValidity(
2112
+ row,
2113
+ rendered.column,
2114
+ )}
2043
2115
  {@const cellNote = computeCellNote(row, rendered.column)}
2044
2116
  <td
2045
2117
  class={`sv-grid-cell ${userCellClass}`}
@@ -2047,6 +2119,7 @@
2047
2119
  class:sv-grid-cell-active={activeCell.rowIndex ===
2048
2120
  rowIndex && activeCell.colIndex === colIndex}
2049
2121
  class:sv-grid-cell-cf={hasConditionalFormats}
2122
+ class:sv-grid-cell-invalid={cellValidity.invalid}
2050
2123
  class:sv-grid-cell-has-note={cellNote != null}
2051
2124
  data-svgrid-row={rowIndex}
2052
2125
  data-svgrid-col={colIndex}
@@ -2070,11 +2143,17 @@
2070
2143
  // Column tooltip fires on whole-cell hover.
2071
2144
  // Per-cell notes are gated on the corner hot-
2072
2145
  // zone below (Excel-style: hover the small
2073
- // triangle to read the note).
2074
- if (cellTooltip)
2146
+ // triangle to read the note). A validation
2147
+ // message (when the cell is invalid) wins over
2148
+ // the plain column tooltip.
2149
+ const tip =
2150
+ cellValidity.invalid && cellValidity.message
2151
+ ? cellValidity.message
2152
+ : cellTooltip;
2153
+ if (tip)
2075
2154
  showTooltipFor(
2076
2155
  event.currentTarget as HTMLElement,
2077
- cellTooltip,
2156
+ tip,
2078
2157
  );
2079
2158
  }}
2080
2159
  onpointerleave={hideTooltip}
@@ -2284,7 +2363,7 @@
2284
2363
  {/if}
2285
2364
  </div>
2286
2365
 
2287
- <GridFooter {ctrl} />
2366
+ <GridFooter {ctrl} pager={(props.paginationPosition ?? "bottom") !== "top"} pageSizeOptions={props.pageSizeOptions} />
2288
2367
 
2289
2368
  {#if ctrl.findOpen}
2290
2369
  <!-- Find-in-grid overlay. Anchored to the TOP of the grid root so
@@ -92,6 +92,35 @@ export type Props<TFeatures extends TableFeatures = TableFeatures, TData extends
92
92
  showPagination?: boolean;
93
93
  /** Initial page size when pagination is enabled. Defaults to 10. */
94
94
  pageSize?: number;
95
+ /** Page-size choices shown in the footer's selector. Defaults to `[10, 25, 50, 100]`. */
96
+ pageSizeOptions?: number[];
97
+ /**
98
+ * Where the pagination footer sits: `'bottom'` (default), `'top'`, or `'both'`.
99
+ * The status bar (when enabled) always stays at the bottom.
100
+ */
101
+ paginationPosition?: 'top' | 'bottom' | 'both';
102
+ /**
103
+ * Server-side pagination. When `true`, the grid renders its native
104
+ * pagination footer from the `rowCount` / `pageIndex` you provide (rather
105
+ * than counting the local rows), does NOT slice `data` (the rows you pass are
106
+ * treated as the current page), and emits `onPaginationChange` when the user
107
+ * pages or changes page size. Pair with `externalSort` / `externalFilter`
108
+ * and a server data source. Requires `showPagination` / `pageable` to show
109
+ * the footer. Controlled: you own `pageIndex` + `pageSize` + `rowCount`.
110
+ */
111
+ externalPagination?: boolean;
112
+ /** Total rows on the server, for the footer's range + page count. Used with `externalPagination`. */
113
+ rowCount?: number;
114
+ /** Current 0-based page index. Used with `externalPagination` (controlled). */
115
+ pageIndex?: number;
116
+ /**
117
+ * Fires when the user changes page or page size while `externalPagination`
118
+ * is on. Fetch that page and update `data` / `rowCount` / `pageIndex`.
119
+ */
120
+ onPaginationChange?: (pagination: {
121
+ pageIndex: number;
122
+ pageSize: number;
123
+ }) => void;
95
124
  virtualization?: boolean;
96
125
  /** Row height in pixels. Pass a function `(rowIndex) => px` for
97
126
  * per-row variable heights (e.g. an interactive row-resize feature).
@@ -137,6 +166,13 @@ export type Props<TFeatures extends TableFeatures = TableFeatures, TData extends
137
166
  showFilterMenu?: boolean;
138
167
  showFilterRow?: boolean;
139
168
  enableCellSelection?: boolean;
169
+ /**
170
+ * Highlight the row under the pointer. Default **false** - the hover tint can
171
+ * compete with the cell selection / fill marquee. Set `true` to opt in; when
172
+ * on, the hover no longer paints over selected cells so the selection stays
173
+ * visible.
174
+ */
175
+ enableRowHover?: boolean;
140
176
  /**
141
177
  * Prepend a header row (the column labels) to copied cell ranges, so pasting
142
178
  * into Excel / Sheets includes the headers. Applies per copied range.
@@ -19,6 +19,7 @@
19
19
  * Tab - commit and let focus escape.
20
20
  */
21
21
  import type { CellEditorOption } from './editors/cell-editors'
22
+ import { anchoredRect, portalToBody, type AnchoredRect } from './popover'
22
23
 
23
24
  type Props = {
24
25
  options: ReadonlyArray<CellEditorOption>
@@ -30,6 +31,10 @@
30
31
  /** Add a typeahead input at the top of the popover that filters the
31
32
  * option list as the user types. Used by the rich-select editor. */
32
33
  searchable?: boolean
34
+ /** Focus the trigger and open the panel on mount. True for in-cell editing
35
+ * (you clicked into the cell to edit); set false for form fields, which
36
+ * should stay closed and unfocused until the user acts. Default true. */
37
+ autoOpen?: boolean
33
38
  /** Called with the new full value (scalar for single, array for multi). */
34
39
  onChange?: (next: unknown) => void
35
40
  /** Called when the user finalizes the selection (single pick, Enter, blur with selection). */
@@ -45,6 +50,7 @@
45
50
  placeholder = 'Select…',
46
51
  renderChipsInTrigger = false,
47
52
  searchable = false,
53
+ autoOpen = true,
48
54
  onChange,
49
55
  onCommit,
50
56
  onCancel,
@@ -69,7 +75,7 @@
69
75
  /** Viewport-anchored panel position. We `position: fixed` the panel so it
70
76
  * escapes the grid's overflow:hidden scroll container - otherwise the
71
77
  * bottom of the list gets clipped by the pager or the grid's footer. */
72
- let panelRect = $state<{ top: number; left: number; width: number; openUpward: boolean }>({
78
+ let panelRect = $state<AnchoredRect>({
73
79
  top: 0,
74
80
  left: 0,
75
81
  width: 0,
@@ -134,20 +140,15 @@
134
140
  * flips upward when there isn't enough room below the trigger. */
135
141
  function updatePanelPosition() {
136
142
  if (!triggerEl) return
137
- const r = triggerEl.getBoundingClientRect()
138
143
  // Use the smaller of (option count, 10) for the upward-flip math so
139
144
  // a 3-option list doesn't think it needs 320px of headroom.
140
145
  const visibleCount = Math.min(options.length, 10)
141
146
  const estimatedHeight = visibleCount * 32 + (multiple ? 40 : 0) + 8
142
- const spaceBelow = window.innerHeight - r.bottom
143
- const spaceAbove = r.top
144
- const openUpward = spaceBelow < estimatedHeight && spaceAbove > spaceBelow
145
- panelRect = {
146
- top: openUpward ? r.top - estimatedHeight - 2 : r.bottom + 2,
147
- left: r.left,
148
- width: r.width,
149
- openUpward,
150
- }
147
+ panelRect = anchoredRect(triggerEl.getBoundingClientRect(), {
148
+ estimatedHeight,
149
+ // Preserve the original behavior: anchor to the trigger, no clamping.
150
+ clampHorizontal: false,
151
+ })
151
152
  }
152
153
 
153
154
  // Keep the panel anchored as the page scrolls or resizes - without this,
@@ -249,28 +250,13 @@
249
250
  }
250
251
 
251
252
  function focusTrigger(node: HTMLButtonElement) {
253
+ // Form fields (autoOpen=false) stay closed + unfocused until the user acts;
254
+ // in-cell editing auto-focuses + opens to match clicking into a cell.
255
+ if (!autoOpen) return
252
256
  node.focus()
253
- // Auto-open so the user can immediately pick - matches the
254
- // expectation set by clicking into an editing cell.
255
257
  openPanel()
256
258
  }
257
259
 
258
- /** Svelte action: detach the element on mount and append it to
259
- * document.body, then put it back / remove on destroy. This is how
260
- * we "portal" the panel out of the grid's overflow:hidden scroll
261
- * container so it can never be clipped, no matter how deeply nested
262
- * the editing cell is. */
263
- function portalToBody(node: HTMLElement) {
264
- document.body.appendChild(node)
265
- return {
266
- destroy() {
267
- if (node.parentNode === document.body) {
268
- document.body.removeChild(node)
269
- }
270
- },
271
- }
272
- }
273
-
274
260
  /** Close the panel when the user clicks outside both trigger AND
275
261
  * panel - `onfocusout` on the dropdown root no longer catches this
276
262
  * because the portal'd panel is a sibling of body, not a descendant
@@ -388,7 +374,7 @@
388
374
  style:left={`${panelRect.left}px`}
389
375
  style:width={`${panelRect.width}px`}
390
376
  style:max-height={panelMax !== null ? `${panelMax}px` : null}
391
- style:z-index="9999"
377
+ style:z-index="2147483647"
392
378
  onpointerdown={(event) => event.stopPropagation()}
393
379
  onmousedown={(event) => event.stopPropagation()}
394
380
  onclick={(event) => event.stopPropagation()}
@@ -28,6 +28,10 @@ type Props = {
28
28
  /** Add a typeahead input at the top of the popover that filters the
29
29
  * option list as the user types. Used by the rich-select editor. */
30
30
  searchable?: boolean;
31
+ /** Focus the trigger and open the panel on mount. True for in-cell editing
32
+ * (you clicked into the cell to edit); set false for form fields, which
33
+ * should stay closed and unfocused until the user acts. Default true. */
34
+ autoOpen?: boolean;
31
35
  /** Called with the new full value (scalar for single, array for multi). */
32
36
  onChange?: (next: unknown) => void;
33
37
  /** Called when the user finalizes the selection (single pick, Enter, blur with selection). */
@@ -0,0 +1,89 @@
1
+ <script lang="ts">
2
+ /**
3
+ * SvListBox - an inline single/multi-select list (WAI-ARIA listbox) with
4
+ * roving highlight + full keyboard. Parity: Smart `smart-list-box`. Controlled
5
+ * via `value` (scalar or array) + `onChange`.
6
+ */
7
+ import type { ListOption } from './list-option'
8
+ import { createListbox } from './createListbox.svelte'
9
+
10
+ type Props = {
11
+ options: ReadonlyArray<ListOption>
12
+ value?: string | number | Array<string | number> | null
13
+ onChange?: (value: any) => void
14
+ multiple?: boolean
15
+ disabled?: boolean
16
+ /** Visible height in rows before scrolling. */
17
+ rows?: number
18
+ ariaLabel?: string
19
+ /** Form field name; emits a hidden input per selected value so the listbox
20
+ * posts in a native <form> (multiple selection -> repeated name). */
21
+ name?: string
22
+ }
23
+
24
+ let { options, value = null, onChange, multiple = false, disabled = false, rows = 7, ariaLabel, name }: Props = $props()
25
+
26
+ // The styled listbox is just a renderer over the headless core.
27
+ const lb = createListbox({
28
+ options: () => options,
29
+ value: () => value,
30
+ onChange: (v) => onChange?.(v),
31
+ multiple: () => multiple,
32
+ disabled: () => disabled,
33
+ ariaLabel: () => ariaLabel,
34
+ })
35
+ const isSel = (o: ListOption) => lb.isSelected(o.value)
36
+
37
+ let listEl: HTMLUListElement | null = null
38
+ // Scroll-into-view is a render concern, so it lives here (not in the core).
39
+ $effect(() => {
40
+ const i = lb.activeIndex
41
+ queueMicrotask(() => listEl?.querySelector<HTMLElement>(`[data-idx="${i}"]`)?.scrollIntoView({ block: 'nearest' }))
42
+ })
43
+ </script>
44
+
45
+ <div style="display: contents">
46
+ <ul
47
+ bind:this={listEl}
48
+ class="sv-listbox"
49
+ style:--sv-rows={rows}
50
+ {...lb.rootProps()}
51
+ >
52
+ {#each options as opt, i (opt.value)}
53
+ <!-- svelte-ignore a11y_click_events_have_key_events a11y_interactive_supports_focus -->
54
+ <li
55
+ class="sv-listbox__opt"
56
+ class:is-selected={isSel(opt)}
57
+ class:is-active={lb.isActive(i)}
58
+ class:is-disabled={opt.disabled}
59
+ {...lb.optionProps(i)}
60
+ >
61
+ {#if multiple}<span class="sv-listbox__check" aria-hidden="true">{isSel(opt) ? '✓' : ''}</span>{/if}
62
+ <span class="sv-listbox__label">{opt.label}</span>
63
+ </li>
64
+ {/each}
65
+ </ul>
66
+ {#if name}{#each lb.selectedValues as v (v)}<input type="hidden" {name} value={v} />{/each}{/if}
67
+ </div>
68
+
69
+ <style>
70
+ .sv-listbox {
71
+ --_accent: var(--sg-accent, #2563eb);
72
+ margin: 0; padding: 4px; list-style: none;
73
+ max-height: calc(var(--sv-rows, 7) * 32px + 8px); overflow-y: auto;
74
+ background: var(--sg-input-bg, #fff); color: var(--sg-fg, #0f172a);
75
+ border: 1px solid var(--sg-input-border, var(--sg-border, #cbd5e1)); border-radius: var(--sg-radius, 8px);
76
+ width: 220px; outline: none;
77
+ }
78
+ .sv-listbox:focus-visible { border-color: var(--_accent); box-shadow: 0 0 0 2px color-mix(in srgb, var(--_accent) 22%, transparent); }
79
+ .sv-listbox__opt {
80
+ display: flex; align-items: center; gap: 8px; height: 32px; padding: 0 10px;
81
+ border-radius: 6px; cursor: pointer; font-size: 13px;
82
+ }
83
+ .sv-listbox__opt.is-active { background: var(--sg-row-hover-bg, #f1f5f9); }
84
+ .sv-listbox__opt.is-selected { background: color-mix(in srgb, var(--_accent) 14%, transparent); color: var(--_accent); font-weight: 600; }
85
+ .sv-listbox__opt.is-selected.is-active { background: color-mix(in srgb, var(--_accent) 22%, transparent); }
86
+ .sv-listbox__opt.is-disabled { opacity: 0.4; cursor: not-allowed; }
87
+ .sv-listbox__check { width: 14px; text-align: center; color: var(--_accent); }
88
+ .sv-listbox__label { flex: 1; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
89
+ </style>
@@ -0,0 +1,22 @@
1
+ /**
2
+ * SvListBox - an inline single/multi-select list (WAI-ARIA listbox) with
3
+ * roving highlight + full keyboard. Parity: Smart `smart-list-box`. Controlled
4
+ * via `value` (scalar or array) + `onChange`.
5
+ */
6
+ import type { ListOption } from './list-option';
7
+ type Props = {
8
+ options: ReadonlyArray<ListOption>;
9
+ value?: string | number | Array<string | number> | null;
10
+ onChange?: (value: any) => void;
11
+ multiple?: boolean;
12
+ disabled?: boolean;
13
+ /** Visible height in rows before scrolling. */
14
+ rows?: number;
15
+ ariaLabel?: string;
16
+ /** Form field name; emits a hidden input per selected value so the listbox
17
+ * posts in a native <form> (multiple selection -> repeated name). */
18
+ name?: string;
19
+ };
20
+ declare const SvListBox: import("svelte").Component<Props, {}, "">;
21
+ type SvListBox = ReturnType<typeof SvListBox>;
22
+ export default SvListBox;
@@ -0,0 +1,66 @@
1
+ <script lang="ts">
2
+ /**
3
+ * SvMaskedInput - a pattern-masked text input (#=digit, A=letter, *=alnum;
4
+ * other chars are literals). Parity: Smart `smart-masked-text-box`. Emits both
5
+ * the masked display value and the raw (unmasked) value.
6
+ *
7
+ * The styled renderer over the headless `createMaskedInput` core; the mask
8
+ * formatting + state live in the core, spread here via prop-getters.
9
+ */
10
+ import { unmask } from './datetime/mask'
11
+ import { createMaskedInput } from './createMaskedInput.svelte'
12
+
13
+ type Props = {
14
+ value?: string
15
+ /** Called with (masked, raw, complete). */
16
+ onChange?: (masked: string, raw: string, complete: boolean) => void
17
+ mask?: string
18
+ placeholder?: string
19
+ disabled?: boolean
20
+ readonly?: boolean
21
+ name?: string
22
+ size?: 'sm' | 'md' | 'lg'
23
+ ariaLabel?: string
24
+ }
25
+
26
+ let {
27
+ value = '',
28
+ onChange,
29
+ mask = '',
30
+ placeholder,
31
+ disabled = false,
32
+ readonly = false,
33
+ name,
34
+ size = 'md',
35
+ ariaLabel,
36
+ }: Props = $props()
37
+
38
+ const mi = createMaskedInput({
39
+ value: () => value,
40
+ onChange: (m, r, c) => onChange?.(m, r, c),
41
+ mask: () => mask,
42
+ placeholder: () => placeholder,
43
+ disabled: () => disabled,
44
+ readonly: () => readonly,
45
+ ariaLabel: () => ariaLabel,
46
+ })
47
+ </script>
48
+
49
+ <input class="sv-masked sv-masked--{size}" {...mi.inputProps()} />
50
+ {#if name}<input type="hidden" {name} value={mask ? unmask(mi.masked, mask) : mi.masked} />{/if}
51
+
52
+ <style>
53
+ .sv-masked {
54
+ --_accent: var(--sg-accent, #2563eb);
55
+ box-sizing: border-box; width: 200px;
56
+ background: var(--sg-input-bg, #fff); color: var(--sg-fg, #0f172a);
57
+ border: 1px solid var(--sg-input-border, var(--sg-border, #cbd5e1)); border-radius: var(--sg-radius, 8px);
58
+ font: inherit; padding: 0 10px; outline: none;
59
+ font-variant-numeric: tabular-nums; letter-spacing: 0.02em;
60
+ }
61
+ .sv-masked--sm { height: 28px; font-size: 12px; }
62
+ .sv-masked--md { height: 34px; font-size: 13px; }
63
+ .sv-masked--lg { height: 40px; font-size: 15px; }
64
+ .sv-masked:focus { border-color: var(--_accent); box-shadow: 0 0 0 2px color-mix(in srgb, var(--_accent) 22%, transparent); }
65
+ .sv-masked:disabled { opacity: 0.6; }
66
+ </style>
@@ -0,0 +1,15 @@
1
+ type Props = {
2
+ value?: string;
3
+ /** Called with (masked, raw, complete). */
4
+ onChange?: (masked: string, raw: string, complete: boolean) => void;
5
+ mask?: string;
6
+ placeholder?: string;
7
+ disabled?: boolean;
8
+ readonly?: boolean;
9
+ name?: string;
10
+ size?: 'sm' | 'md' | 'lg';
11
+ ariaLabel?: string;
12
+ };
13
+ declare const SvMaskedInput: import("svelte").Component<Props, {}, "">;
14
+ type SvMaskedInput = ReturnType<typeof SvMaskedInput>;
15
+ export default SvMaskedInput;
@@ -0,0 +1,116 @@
1
+ <script lang="ts">
2
+ /**
3
+ * SvNumberInput - a numeric input with min/max/step, spinner buttons, optional
4
+ * thousands grouping + precision, prefix/suffix. Parity: Smart `smart-number-input`.
5
+ * Emits number | null. The SvGrid number cell editor, standalone too.
6
+ *
7
+ * This is the styled renderer over the headless `createNumberInput` core - the
8
+ * same split as the grid (`createSvGrid` / `<SvGrid>`): parse/format/clamp,
9
+ * spinner + keyboard and ARIA all come from the core via prop-getters.
10
+ */
11
+ import SvRepeatButton from './SvRepeatButton.svelte'
12
+ import { editorErrorId, type SvEditorProps } from './editor-contract'
13
+ import { createNumberInput } from './createNumberInput.svelte'
14
+
15
+ type Props = SvEditorProps & {
16
+ value?: number | null
17
+ onChange?: (value: number | null) => void
18
+ min?: number
19
+ max?: number
20
+ step?: number
21
+ precision?: number
22
+ /** Group thousands (1,234) on display. */
23
+ grouping?: boolean
24
+ prefix?: string
25
+ suffix?: string
26
+ placeholder?: string
27
+ spinButtons?: boolean
28
+ }
29
+
30
+ let {
31
+ value = null,
32
+ onChange,
33
+ min = -Infinity,
34
+ max = Infinity,
35
+ step = 1,
36
+ precision,
37
+ grouping = false,
38
+ prefix = '',
39
+ suffix = '',
40
+ placeholder,
41
+ disabled = false,
42
+ readonly = false,
43
+ spinButtons = true,
44
+ name,
45
+ size = 'md',
46
+ ariaLabel,
47
+ invalid = false,
48
+ required = false,
49
+ error,
50
+ id,
51
+ }: Props = $props()
52
+
53
+ const num = createNumberInput({
54
+ value: () => value,
55
+ onChange: (v) => onChange?.(v),
56
+ min: () => min,
57
+ max: () => max,
58
+ step: () => step,
59
+ precision: () => precision,
60
+ grouping: () => grouping,
61
+ prefix: () => prefix,
62
+ suffix: () => suffix,
63
+ placeholder: () => placeholder,
64
+ disabled: () => disabled,
65
+ readonly: () => readonly,
66
+ id: () => id,
67
+ invalid: () => invalid,
68
+ required: () => required,
69
+ error: () => error,
70
+ ariaLabel: () => ariaLabel,
71
+ })
72
+ </script>
73
+
74
+ <div class="sv-field">
75
+ <div class="sv-num sv-num--{size}" class:is-disabled={disabled} class:is-invalid={invalid}>
76
+ <input class="sv-num__input" {...num.inputProps()} />
77
+ {#if spinButtons}
78
+ <div class="sv-num__spin">
79
+ <SvRepeatButton size="sm" variant="ghost" ariaLabel="Increment" onclick={num.increment}>
80
+ <svg viewBox="0 0 24 24" width="11" height="11" fill="none" stroke="currentColor" stroke-width="3" stroke-linecap="round" stroke-linejoin="round"><path d="M6 15l6-6 6 6" /></svg>
81
+ </SvRepeatButton>
82
+ <SvRepeatButton size="sm" variant="ghost" ariaLabel="Decrement" onclick={num.decrement}>
83
+ <svg viewBox="0 0 24 24" width="11" height="11" fill="none" stroke="currentColor" stroke-width="3" stroke-linecap="round" stroke-linejoin="round"><path d="M6 9l6 6 6-6" /></svg>
84
+ </SvRepeatButton>
85
+ </div>
86
+ {/if}
87
+ {#if name}<input type="hidden" {name} value={value ?? ''} />{/if}
88
+ </div>
89
+ {#if error}<span class="sv-field__error" id={editorErrorId(id)} role="alert">{error}</span>{/if}
90
+ </div>
91
+
92
+ <style>
93
+ /* Shared field wrapper: stacks the control box above an optional error line. */
94
+ .sv-field { display: inline-flex; flex-direction: column; gap: 3px; }
95
+ .sv-field__error { font-size: 11.5px; font-weight: 500; color: var(--sg-danger, #dc2626); line-height: 1.35; }
96
+ .sv-num {
97
+ --_accent: var(--sg-accent, #2563eb);
98
+ display: inline-flex; align-items: stretch; width: 150px;
99
+ background: var(--sg-input-bg, #fff); color: var(--sg-fg, #0f172a);
100
+ border: 1px solid var(--sg-input-border, var(--sg-border, #cbd5e1)); border-radius: var(--sg-radius, 8px);
101
+ overflow: hidden;
102
+ }
103
+ .sv-num:focus-within { border-color: var(--_accent); box-shadow: 0 0 0 2px color-mix(in srgb, var(--_accent) 22%, transparent); }
104
+ .sv-num.is-invalid { border-color: var(--sg-danger, #dc2626); }
105
+ .sv-num.is-invalid:focus-within { box-shadow: 0 0 0 2px color-mix(in srgb, var(--sg-danger, #dc2626) 22%, transparent); }
106
+ .sv-num.is-disabled { opacity: 0.6; }
107
+ .sv-num__input {
108
+ flex: 1; min-width: 0; border: 0; background: none; outline: none; color: inherit; font: inherit;
109
+ text-align: right; padding: 0 8px;
110
+ }
111
+ .sv-num--sm { height: 28px; font-size: 12px; }
112
+ .sv-num--md { height: 34px; font-size: 13px; }
113
+ .sv-num--lg { height: 40px; font-size: 15px; }
114
+ .sv-num__spin { display: flex; flex-direction: column; border-left: 1px solid var(--sg-border, #e2e8f0); }
115
+ .sv-num__spin :global(.sv-repeat) { flex: 1; padding: 0 6px; border-radius: 0; }
116
+ </style>