@svgrid/grid 1.2.22 → 1.2.23

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 (190) hide show
  1. package/dist/GridFooter.svelte +34 -20
  2. package/dist/GridFooter.svelte.d.ts +8 -0
  3. package/dist/SvAutoComplete.svelte +100 -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 +669 -0
  8. package/dist/SvCalendar.svelte.d.ts +70 -0
  9. package/dist/SvCheckBox.svelte +81 -0
  10. package/dist/SvCheckBox.svelte.d.ts +19 -0
  11. package/dist/SvColorInput.svelte +187 -0
  12. package/dist/SvColorInput.svelte.d.ts +15 -0
  13. package/dist/SvComboBox.svelte +130 -0
  14. package/dist/SvComboBox.svelte.d.ts +15 -0
  15. package/dist/SvCountryInput.svelte +123 -0
  16. package/dist/SvCountryInput.svelte.d.ts +14 -0
  17. package/dist/SvDateTimePicker.svelte +322 -0
  18. package/dist/SvDateTimePicker.svelte.d.ts +37 -0
  19. package/dist/SvDropDownList.svelte +143 -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 +99 -0
  24. package/dist/SvGauge.svelte.d.ts +31 -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 +122 -0
  35. package/dist/SvListBox.svelte.d.ts +19 -0
  36. package/dist/SvMaskedInput.svelte +0 -0
  37. package/dist/SvMaskedInput.svelte.d.ts +15 -0
  38. package/dist/SvNumberInput.svelte +0 -0
  39. package/dist/SvNumberInput.svelte.d.ts +22 -0
  40. package/dist/SvPasswordInput.svelte +110 -0
  41. package/dist/SvPasswordInput.svelte.d.ts +22 -0
  42. package/dist/SvPhoneInput.svelte +0 -0
  43. package/dist/SvPhoneInput.svelte.d.ts +19 -0
  44. package/dist/SvRadioGroup.svelte +108 -0
  45. package/dist/SvRadioGroup.svelte.d.ts +23 -0
  46. package/dist/SvRating.svelte +111 -0
  47. package/dist/SvRating.svelte.d.ts +20 -0
  48. package/dist/SvRepeatButton.svelte +89 -0
  49. package/dist/SvRepeatButton.svelte.d.ts +21 -0
  50. package/dist/SvSlider.svelte +182 -0
  51. package/dist/SvSlider.svelte.d.ts +25 -0
  52. package/dist/SvSwitchButton.svelte +94 -0
  53. package/dist/SvSwitchButton.svelte.d.ts +19 -0
  54. package/dist/SvTabs.svelte +122 -0
  55. package/dist/SvTabs.svelte.d.ts +26 -0
  56. package/dist/SvTagsInput.svelte +81 -0
  57. package/dist/SvTagsInput.svelte.d.ts +19 -0
  58. package/dist/SvTimePicker.svelte +359 -0
  59. package/dist/SvTimePicker.svelte.d.ts +27 -0
  60. package/dist/SvToggleButton.svelte +57 -0
  61. package/dist/SvToggleButton.svelte.d.ts +16 -0
  62. package/dist/SvTree.svelte +210 -0
  63. package/dist/SvTree.svelte.d.ts +29 -0
  64. package/dist/build-api.js +78 -1
  65. package/dist/cdn/svgrid.js +10022 -5904
  66. package/dist/cdn/svgrid.svelte-external.js +8531 -4429
  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/datetime/date-core.d.ts +88 -0
  76. package/dist/datetime/date-core.js +165 -0
  77. package/dist/datetime/date-format.d.ts +39 -0
  78. package/dist/datetime/date-format.js +371 -0
  79. package/dist/datetime/date-restrict.d.ts +23 -0
  80. package/dist/datetime/date-restrict.js +50 -0
  81. package/dist/datetime/date-selection.d.ts +25 -0
  82. package/dist/datetime/date-selection.js +94 -0
  83. package/dist/datetime/mask.d.ts +21 -0
  84. package/dist/datetime/mask.js +90 -0
  85. package/dist/editing.d.ts +1 -1
  86. package/dist/editing.js +7 -0
  87. package/dist/editors/cell-editors.d.ts +2 -2
  88. package/dist/editors/cell-editors.js +3 -1
  89. package/dist/export-format.d.ts +178 -0
  90. package/dist/export-format.js +419 -0
  91. package/dist/hyperformula-adapter.d.ts +4 -0
  92. package/dist/hyperformula-adapter.js +15 -6
  93. package/dist/index.d.ts +36 -0
  94. package/dist/index.js +41 -0
  95. package/dist/list-option.d.ts +10 -0
  96. package/dist/list-option.js +7 -0
  97. package/dist/menus.js +18 -2
  98. package/dist/popover.d.ts +48 -0
  99. package/dist/popover.js +65 -0
  100. package/dist/row-resize.d.ts +4 -4
  101. package/dist/row-resize.js +8 -5
  102. package/dist/selection.d.ts +6 -0
  103. package/dist/selection.js +31 -5
  104. package/dist/server-data-source.d.ts +43 -0
  105. package/dist/server-data-source.js +100 -0
  106. package/dist/sparkline.js +8 -4
  107. package/dist/svgrid-wrapper.types.d.ts +26 -1
  108. package/package.json +6 -1
  109. package/src/GridFooter.svelte +34 -20
  110. package/src/SvAutoComplete.svelte +100 -0
  111. package/src/SvButton.svelte +97 -0
  112. package/src/SvCalendar.svelte +669 -0
  113. package/src/SvCalendar.test.ts +211 -0
  114. package/src/SvCheckBox.svelte +81 -0
  115. package/src/SvColorInput.svelte +187 -0
  116. package/src/SvComboBox.svelte +130 -0
  117. package/src/SvCountryInput.svelte +123 -0
  118. package/src/SvDateTimePicker.svelte +322 -0
  119. package/src/SvDateTimePicker.test.ts +136 -0
  120. package/src/SvDropDownList.svelte +143 -0
  121. package/src/SvForm.svelte +147 -0
  122. package/src/SvGauge.svelte +99 -0
  123. package/src/SvGrid.controller.svelte.ts +74 -8
  124. package/src/SvGrid.css +84 -31
  125. package/src/SvGrid.helpers.ts +41 -12
  126. package/src/SvGrid.svelte +84 -5
  127. package/src/SvGrid.types.ts +33 -0
  128. package/src/SvGridDropdown.svelte +16 -30
  129. package/src/SvListBox.svelte +122 -0
  130. package/src/SvMaskedInput.svelte +0 -0
  131. package/src/SvNumberInput.svelte +0 -0
  132. package/src/SvPasswordInput.svelte +110 -0
  133. package/src/SvPhoneInput.svelte +0 -0
  134. package/src/SvRadioGroup.svelte +108 -0
  135. package/src/SvRating.svelte +111 -0
  136. package/src/SvRepeatButton.svelte +89 -0
  137. package/src/SvSlider.svelte +182 -0
  138. package/src/SvSwitchButton.svelte +94 -0
  139. package/src/SvTabs.svelte +122 -0
  140. package/src/SvTagsInput.svelte +81 -0
  141. package/src/SvTimePicker.svelte +359 -0
  142. package/src/SvTimePicker.test.ts +98 -0
  143. package/src/SvToggleButton.svelte +57 -0
  144. package/src/SvTree.svelte +210 -0
  145. package/src/build-api.ts +115 -0
  146. package/src/cell-render.test.ts +38 -0
  147. package/src/cell-render.ts +43 -0
  148. package/src/clipboard.test.ts +18 -0
  149. package/src/clipboard.ts +24 -1
  150. package/src/collaboration.test.ts +30 -0
  151. package/src/collaboration.ts +6 -0
  152. package/src/core.ts +27 -3
  153. package/src/countries.ts +71 -0
  154. package/src/datetime/date-core.test.ts +217 -0
  155. package/src/datetime/date-core.ts +204 -0
  156. package/src/datetime/date-format.test.ts +121 -0
  157. package/src/datetime/date-format.ts +317 -0
  158. package/src/datetime/date-restrict.ts +60 -0
  159. package/src/datetime/date-selection.test.ts +129 -0
  160. package/src/datetime/date-selection.ts +137 -0
  161. package/src/datetime/mask.test.ts +36 -0
  162. package/src/datetime/mask.ts +84 -0
  163. package/src/editing.test.ts +22 -0
  164. package/src/editing.ts +7 -1
  165. package/src/editors/cell-editors.ts +7 -1
  166. package/src/export-data-api.test.ts +126 -0
  167. package/src/export-format.test.ts +107 -0
  168. package/src/export-format.ts +598 -0
  169. package/src/hyperformula-adapter.test.ts +35 -1
  170. package/src/hyperformula-adapter.ts +18 -6
  171. package/src/index.ts +71 -0
  172. package/src/list-option.ts +15 -0
  173. package/src/menus.ts +21 -2
  174. package/src/popover.ts +79 -0
  175. package/src/row-resize.test.ts +25 -0
  176. package/src/row-resize.ts +8 -5
  177. package/src/selection.test.ts +36 -4
  178. package/src/selection.ts +30 -9
  179. package/src/server-data-source.test.ts +201 -0
  180. package/src/server-data-source.ts +148 -0
  181. package/src/sparkline.test.ts +9 -0
  182. package/src/sparkline.ts +9 -4
  183. package/src/svgrid-wrapper.types.ts +28 -1
  184. package/src/svgrid.behavior.test.ts +27 -0
  185. package/src/svgrid.new-features.wrapper.test.ts +27 -1
  186. package/src/ui-buttons.test.ts +105 -0
  187. package/src/ui-composite.test.ts +89 -0
  188. package/src/ui-inputs.test.ts +92 -0
  189. package/src/ui-range.test.ts +61 -0
  190. package/src/ui-selection.test.ts +106 -0
package/src/SvGrid.css CHANGED
@@ -348,9 +348,13 @@
348
348
  .sv-grid-table tr > :last-child.sv-grid-cell {
349
349
  border-right: 0;
350
350
  }
351
- /* Row hover tint. */
352
- .sv-grid-table tbody tr:hover > .sv-grid-cell {
353
- background: var(--sg-row-hover-bg, #eef2ff);
351
+ /* Row hover tint - opt-in via `enableRowHover` (off by default; the class is
352
+ present unless the grid opts in). Uses `background-color` (not the shorthand)
353
+ so it never resets the fill-marquee `background-image`, and skips selected
354
+ cells so the selection stays visible under a hovered row. */
355
+ .sv-grid-table:not(.sv-grid-no-row-hover)
356
+ tbody tr:hover > .sv-grid-cell:not([data-selected-range="true"]) {
357
+ background-color: var(--sg-row-hover-bg, #eef2ff);
354
358
  }
355
359
  /* Only the active cell + the cell hosting the fill handle become a
356
360
  positioning context (so the absolutely-positioned handle anchors
@@ -441,8 +445,10 @@
441
445
  ),
442
446
  var(--sg-pinned-bg, color-mix(in oklab, var(--sg-header-bg, #f1f5f9) 92%, var(--sg-accent, #2563eb) 8%));
443
447
  }
444
- /* Hovered row keeps the pinned tint distinguishable from the hover. */
445
- .sv-grid-row:hover > .sv-grid-cell[data-pinned] {
448
+ /* Hovered row keeps the pinned tint distinguishable from the hover. Selected
449
+ cells are excluded so the selection stays visible under hover. */
450
+ .sv-grid-table:not(.sv-grid-no-row-hover)
451
+ .sv-grid-row:hover > .sv-grid-cell[data-pinned]:not([data-selected-range="true"]) {
446
452
  background:
447
453
  linear-gradient(
448
454
  color-mix(in srgb, var(--sg-row-hover-bg, #eef2ff) 55%, transparent),
@@ -1062,18 +1068,52 @@ select.sv-grid-fr-editor {
1062
1068
  transform-origin: bottom right;
1063
1069
  }
1064
1070
 
1065
- /* Fill-preview overlay paints an accent border on the cells the user is
1066
- about to fill. Source cells (still in the selection range) keep their
1067
- normal range styling. The border + tint derive from the theme accent
1068
- (`--sg-accent`) so they match each theme instead of a fixed blue;
1069
- `--sg-fill-preview-border` / `--sg-fill-preview-bg` let a theme override
1070
- them directly. */
1071
- .sv-grid-cell[data-fill-preview="true"] {
1072
- box-shadow: inset 0 0 0 2px
1073
- var(--sg-fill-preview-border,
1074
- color-mix(in srgb, var(--sg-accent, #0b63f3) 50%, transparent));
1075
- background: var(--sg-fill-preview-bg,
1076
- color-mix(in srgb, var(--sg-accent, #0b63f3) 8%, transparent));
1071
+ /* Excel-style fill-handle drag marquee: ONE thin dashed rectangle around the
1072
+ whole target range (source + fill-into area), not a thick per-cell border or
1073
+ fill tint. Drawn as four background-image layers on the boundary cells - no
1074
+ `border`, so it paints on the compositor with zero reflow while dragging.
1075
+ `--sg-fill-marquee` overrides the colour (defaults to a theme-aware grey). */
1076
+ /* Step 1 (LOW specificity): default every edge layer to `none`, then light up
1077
+ the edges this cell sits on. These are custom properties, which the
1078
+ `background:` shorthands on selected / zebra cells do NOT reset - so they can
1079
+ safely live at low specificity and just compete with each other by source
1080
+ order (gradient rules come after the defaults, so they win). */
1081
+ .sv-grid-cell[data-fill-top="true"],
1082
+ .sv-grid-cell[data-fill-bottom="true"],
1083
+ .sv-grid-cell[data-fill-left="true"],
1084
+ .sv-grid-cell[data-fill-right="true"] {
1085
+ --sv-fill-top: none;
1086
+ --sv-fill-bottom: none;
1087
+ --sv-fill-left: none;
1088
+ --sv-fill-right: none;
1089
+ /* Match the selection colour (the grid accent) by default. */
1090
+ --sv-fill-color: var(--sg-fill-marquee, var(--sg-accent, #2563eb));
1091
+ }
1092
+ .sv-grid-cell[data-fill-top="true"] {
1093
+ --sv-fill-top: repeating-linear-gradient(to right, var(--sv-fill-color) 0 3px, transparent 3px 6px);
1094
+ }
1095
+ .sv-grid-cell[data-fill-bottom="true"] {
1096
+ --sv-fill-bottom: repeating-linear-gradient(to right, var(--sv-fill-color) 0 3px, transparent 3px 6px);
1097
+ }
1098
+ .sv-grid-cell[data-fill-left="true"] {
1099
+ --sv-fill-left: repeating-linear-gradient(to bottom, var(--sv-fill-color) 0 3px, transparent 3px 6px);
1100
+ }
1101
+ .sv-grid-cell[data-fill-right="true"] {
1102
+ --sv-fill-right: repeating-linear-gradient(to bottom, var(--sv-fill-color) 0 3px, transparent 3px 6px);
1103
+ }
1104
+ /* Step 2 (HIGH specificity, `.sv-grid-table` prefix): compose the resolved edge
1105
+ layers into `background-image` so it beats the `background:` shorthands on
1106
+ selected / zebra / hover cells (which would otherwise reset background-image).
1107
+ The cell keeps its background-COLOR; this only adds the dashed lines on top. */
1108
+ .sv-grid-table .sv-grid-cell[data-fill-top="true"],
1109
+ .sv-grid-table .sv-grid-cell[data-fill-bottom="true"],
1110
+ .sv-grid-table .sv-grid-cell[data-fill-left="true"],
1111
+ .sv-grid-table .sv-grid-cell[data-fill-right="true"] {
1112
+ background-image: var(--sv-fill-top), var(--sv-fill-bottom),
1113
+ var(--sv-fill-left), var(--sv-fill-right);
1114
+ background-repeat: no-repeat;
1115
+ background-size: 100% 2px, 100% 2px, 2px 100%, 2px 100%;
1116
+ background-position: top left, bottom left, top left, top right;
1077
1117
  }
1078
1118
 
1079
1119
  .sv-grid-cell-editing {
@@ -1338,6 +1378,17 @@ select.sv-grid-fr-editor {
1338
1378
  .sv-grid-cell-cf {
1339
1379
  position: relative;
1340
1380
  }
1381
+ /* Declarative cell validation (column `validate` hook). Invalid cells get a
1382
+ * soft red wash and a red inset ring - Handsontable's `htInvalid` look. The
1383
+ * value keeps rendering as-is; the message (if any) shows as the tooltip. */
1384
+ .sv-grid-cell-invalid {
1385
+ background-color: var(--sg-invalid-bg, rgba(239, 68, 68, 0.14)) !important;
1386
+ box-shadow: inset 0 0 0 1px var(--sg-invalid-border, rgba(239, 68, 68, 0.45));
1387
+ color: var(--sg-invalid-fg, #b91c1c);
1388
+ }
1389
+ .sv-grid-cell-invalid.sv-grid-cell-active {
1390
+ box-shadow: inset 0 0 0 2px var(--sg-invalid-border, rgba(239, 68, 68, 0.7));
1391
+ }
1341
1392
  .sv-grid-status-bar {
1342
1393
  display: flex;
1343
1394
  flex-wrap: wrap;
@@ -2103,32 +2154,34 @@ select.sv-grid-fr-editor {
2103
2154
  color: var(--sg-fg, #0f172a);
2104
2155
  font-size: 13px;
2105
2156
  }
2157
+ /* Top-positioned pager: rounded + bordered on the top edge instead. */
2158
+ .sv-grid-pagination-top {
2159
+ border-top: 1px solid var(--sg-border, #e2e8f0);
2160
+ border-bottom: 0;
2161
+ border-radius: var(--sg-radius, 6px) var(--sg-radius, 6px) 0 0;
2162
+ }
2106
2163
  .sv-grid-pagination-pagesize {
2107
2164
  display: inline-flex;
2108
2165
  align-items: center;
2109
2166
  gap: 8px;
2110
2167
  color: var(--sg-muted, #64748b);
2111
2168
  }
2112
- .sv-grid-pagination-pagesize select {
2169
+ .sv-grid-pagination-pagesize-dd {
2170
+ position: relative;
2113
2171
  height: 28px;
2114
- padding: 0 22px 0 8px;
2172
+ min-width: 62px;
2115
2173
  border: 1px solid var(--sg-input-border, #cbd5e1);
2116
2174
  border-radius: 5px;
2117
2175
  background: var(--sg-input-bg, #fff);
2118
- color: var(--sg-fg, #0f172a);
2119
- font: inherit;
2176
+ }
2177
+ .sv-grid-pagination-pagesize-dd .sv-grid-dropdown,
2178
+ .sv-grid-pagination-pagesize-dd .sv-grid-dropdown-trigger {
2179
+ background: transparent;
2180
+ border-radius: 5px;
2120
2181
  font-size: 13px;
2121
- cursor: pointer;
2122
- appearance: none;
2123
- -webkit-appearance: none;
2124
- background-image: linear-gradient(45deg, transparent 50%, currentColor 50%),
2125
- linear-gradient(135deg, currentColor 50%, transparent 50%);
2126
- background-position: calc(100% - 12px) 12px, calc(100% - 8px) 12px;
2127
- background-size: 4px 4px, 4px 4px;
2128
- background-repeat: no-repeat;
2182
+ color: var(--sg-fg, #0f172a);
2129
2183
  }
2130
- .sv-grid-pagination-pagesize select:focus {
2131
- outline: none;
2184
+ .sv-grid-pagination-pagesize-dd:focus-within {
2132
2185
  border-color: var(--sg-accent, #2563eb);
2133
2186
  box-shadow: 0 0 0 2px color-mix(in srgb, var(--sg-accent, #2563eb) 30%, transparent);
2134
2187
  }
@@ -69,19 +69,47 @@ export function toDateTimeLocalInputValue(value: unknown) {
69
69
  return parsed.toISOString().slice(0, 16);
70
70
  }
71
71
 
72
+ /** Strip the `-native` opt-out suffix so the native-input helpers treat
73
+ * `date-native` exactly like `date` (the rich pickers own the bare types). */
74
+ function baseEditor(editorType: CellEditorType): string {
75
+ return String(editorType).replace(/-native$/, "");
76
+ }
77
+
78
+ /** 'HH:MM[:SS]' string (or Date) to a Date carrying that time (today's date). */
79
+ export function timeStringToDate(value: unknown): Date | null {
80
+ if (value instanceof Date) return value;
81
+ if (typeof value === "string") {
82
+ const m = value.match(/^(\d{1,2}):(\d{2})(?::(\d{2}))?/);
83
+ if (m) {
84
+ const d = new Date();
85
+ d.setHours(Math.min(23, +m[1]!), Math.min(59, +m[2]!), m[3] ? +m[3] : 0, 0);
86
+ return d;
87
+ }
88
+ }
89
+ return null;
90
+ }
91
+
92
+ /** A Date to an 'HH:MM' string (the `time` editor's stored form). */
93
+ export function dateToTimeString(d: Date | null): string | null {
94
+ if (!d) return null;
95
+ return `${String(d.getHours()).padStart(2, "0")}:${String(d.getMinutes()).padStart(2, "0")}`;
96
+ }
97
+
72
98
  export function getEditableInputValue(editorType: CellEditorType, value: unknown) {
73
- if (editorType === "date") return toDateInputValue(value);
74
- if (editorType === "datetime") return toDateTimeLocalInputValue(value);
99
+ const base = baseEditor(editorType);
100
+ if (base === "date") return toDateInputValue(value);
101
+ if (base === "datetime") return toDateTimeLocalInputValue(value);
75
102
  return String(value ?? "");
76
103
  }
77
104
 
78
105
  export function getEditorInputType(editorType: CellEditorType) {
79
- if (editorType === "number") return "number";
80
- if (editorType === "date") return "date";
81
- if (editorType === "datetime") return "datetime-local";
82
- if (editorType === "time") return "time";
83
- if (editorType === "password") return "password";
84
- if (editorType === "color") return "color";
106
+ const base = baseEditor(editorType);
107
+ if (base === "number") return "number";
108
+ if (base === "date") return "date";
109
+ if (base === "datetime") return "datetime-local";
110
+ if (base === "time") return "time";
111
+ if (base === "password") return "password";
112
+ if (base === "color") return "color";
85
113
  return "text";
86
114
  }
87
115
 
@@ -130,13 +158,14 @@ export function colorfulChipStyle(color: string | undefined): string {
130
158
  }
131
159
 
132
160
  export function getEditorClass(editorType: CellEditorType) {
133
- if (editorType === "number")
161
+ const base = baseEditor(editorType);
162
+ if (base === "number")
134
163
  return "sv-grid-cell-editor sv-grid-cell-editor-number";
135
- if (editorType === "date")
164
+ if (base === "date")
136
165
  return "sv-grid-cell-editor sv-grid-cell-editor-date";
137
- if (editorType === "datetime")
166
+ if (base === "datetime")
138
167
  return "sv-grid-cell-editor sv-grid-cell-editor-datetime";
139
- if (editorType === "color")
168
+ if (base === "color")
140
169
  return "sv-grid-cell-editor sv-grid-cell-editor-color";
141
170
  return "sv-grid-cell-editor";
142
171
  }
package/src/SvGrid.svelte CHANGED
@@ -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
@@ -105,6 +105,32 @@ export type Props<TFeatures extends TableFeatures = TableFeatures, TData extends
105
105
  showPagination?: boolean;
106
106
  /** Initial page size when pagination is enabled. Defaults to 10. */
107
107
  pageSize?: number;
108
+ /** Page-size choices shown in the footer's selector. Defaults to `[10, 25, 50, 100]`. */
109
+ pageSizeOptions?: number[];
110
+ /**
111
+ * Where the pagination footer sits: `'bottom'` (default), `'top'`, or `'both'`.
112
+ * The status bar (when enabled) always stays at the bottom.
113
+ */
114
+ paginationPosition?: 'top' | 'bottom' | 'both';
115
+ /**
116
+ * Server-side pagination. When `true`, the grid renders its native
117
+ * pagination footer from the `rowCount` / `pageIndex` you provide (rather
118
+ * than counting the local rows), does NOT slice `data` (the rows you pass are
119
+ * treated as the current page), and emits `onPaginationChange` when the user
120
+ * pages or changes page size. Pair with `externalSort` / `externalFilter`
121
+ * and a server data source. Requires `showPagination` / `pageable` to show
122
+ * the footer. Controlled: you own `pageIndex` + `pageSize` + `rowCount`.
123
+ */
124
+ externalPagination?: boolean;
125
+ /** Total rows on the server, for the footer's range + page count. Used with `externalPagination`. */
126
+ rowCount?: number;
127
+ /** Current 0-based page index. Used with `externalPagination` (controlled). */
128
+ pageIndex?: number;
129
+ /**
130
+ * Fires when the user changes page or page size while `externalPagination`
131
+ * is on. Fetch that page and update `data` / `rowCount` / `pageIndex`.
132
+ */
133
+ onPaginationChange?: (pagination: { pageIndex: number; pageSize: number }) => void;
108
134
  virtualization?: boolean;
109
135
  /** Row height in pixels. Pass a function `(rowIndex) => px` for
110
136
  * per-row variable heights (e.g. an interactive row-resize feature).
@@ -150,6 +176,13 @@ export type Props<TFeatures extends TableFeatures = TableFeatures, TData extends
150
176
  showFilterMenu?: boolean;
151
177
  showFilterRow?: boolean;
152
178
  enableCellSelection?: boolean;
179
+ /**
180
+ * Highlight the row under the pointer. Default **false** - the hover tint can
181
+ * compete with the cell selection / fill marquee. Set `true` to opt in; when
182
+ * on, the hover no longer paints over selected cells so the selection stays
183
+ * visible.
184
+ */
185
+ enableRowHover?: boolean;
153
186
  /**
154
187
  * Prepend a header row (the column labels) to copied cell ranges, so pasting
155
188
  * 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()}