@svgrid/grid 2.6.16 → 2.6.18

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 (212) hide show
  1. package/dist/DockNodeView.svelte.d.ts +1 -1
  2. package/dist/DockPopoutHost.svelte.d.ts +1 -1
  3. package/dist/FlexRender.svelte.d.ts +1 -1
  4. package/dist/GridFooter.svelte +6 -3
  5. package/dist/GridFooter.svelte.d.ts +3 -3
  6. package/dist/GridMenus.svelte.d.ts +3 -3
  7. package/dist/SvAccordion.svelte.d.ts +3 -3
  8. package/dist/SvAutoComplete.svelte.d.ts +2 -2
  9. package/dist/SvBreadcrumb.svelte.d.ts +2 -2
  10. package/dist/SvButtonGroup.svelte.d.ts +3 -3
  11. package/dist/SvCalendar.svelte.d.ts +7 -7
  12. package/dist/SvCarousel.svelte.d.ts +1 -1
  13. package/dist/SvCheckBox.svelte.d.ts +1 -1
  14. package/dist/SvColorInput.svelte.d.ts +1 -1
  15. package/dist/SvComboBox.svelte.d.ts +2 -2
  16. package/dist/SvCommand.svelte.d.ts +2 -2
  17. package/dist/SvCountryInput.svelte.d.ts +1 -1
  18. package/dist/SvDateRangeInput.svelte.d.ts +3 -3
  19. package/dist/SvDateTimePicker.svelte.d.ts +4 -4
  20. package/dist/SvDockLayout.svelte.d.ts +2 -2
  21. package/dist/SvDockManager.svelte.d.ts +2 -2
  22. package/dist/SvDropDownList.svelte.d.ts +2 -2
  23. package/dist/SvDurationInput.svelte.d.ts +1 -1
  24. package/dist/SvField.svelte.d.ts +1 -1
  25. package/dist/SvFileUpload.svelte.d.ts +3 -3
  26. package/dist/SvForm.svelte.d.ts +4 -4
  27. package/dist/SvGauge.svelte.d.ts +1 -1
  28. package/dist/SvGrid.controller.svelte.d.ts +28 -28
  29. package/dist/SvGrid.controller.svelte.js +117 -45
  30. package/dist/SvGrid.css +42 -2
  31. package/dist/SvGrid.helpers.d.ts +2 -2
  32. package/dist/SvGrid.svelte.d.ts +3 -3
  33. package/dist/SvGrid.types.d.ts +28 -7
  34. package/dist/SvGridCellEditor.svelte.d.ts +2 -2
  35. package/dist/SvGridChart.svelte.d.ts +1 -1
  36. package/dist/SvGridChartPanel.svelte.d.ts +2 -2
  37. package/dist/SvGridChartView.svelte.d.ts +1 -1
  38. package/dist/SvGridDropdown.svelte.d.ts +1 -1
  39. package/dist/SvGridSelect.svelte.d.ts +4 -4
  40. package/dist/SvGroupCell.svelte.d.ts +1 -1
  41. package/dist/SvHoverCard.svelte.d.ts +1 -1
  42. package/dist/SvListBox.svelte.d.ts +2 -2
  43. package/dist/SvMaskedInput.svelte.d.ts +1 -1
  44. package/dist/SvMenu.svelte.d.ts +1 -1
  45. package/dist/SvMenuList.svelte.d.ts +3 -3
  46. package/dist/SvMenubar.svelte.d.ts +3 -3
  47. package/dist/SvMultiSelect.svelte.d.ts +3 -3
  48. package/dist/SvNavPane.svelte.d.ts +1 -1
  49. package/dist/SvNumberInput.svelte.d.ts +1 -1
  50. package/dist/SvOtpInput.svelte.d.ts +1 -1
  51. package/dist/SvPasswordInput.svelte.d.ts +1 -1
  52. package/dist/SvPhoneInput.svelte.d.ts +2 -2
  53. package/dist/SvPopconfirm.svelte.d.ts +1 -1
  54. package/dist/SvPopover.svelte.d.ts +1 -1
  55. package/dist/SvRadioGroup.svelte.d.ts +3 -3
  56. package/dist/SvRating.svelte.d.ts +1 -1
  57. package/dist/SvSegmented.svelte.d.ts +3 -3
  58. package/dist/SvSlider.svelte.d.ts +1 -1
  59. package/dist/SvSparkline.svelte.d.ts +1 -1
  60. package/dist/SvSplitter.svelte.d.ts +3 -3
  61. package/dist/SvStepper.svelte.d.ts +2 -2
  62. package/dist/SvSwitchButton.svelte.d.ts +1 -1
  63. package/dist/SvTabs.svelte.d.ts +3 -3
  64. package/dist/SvTagsInput.svelte.d.ts +1 -1
  65. package/dist/SvTextArea.svelte.d.ts +1 -1
  66. package/dist/SvTextInput.svelte.d.ts +1 -1
  67. package/dist/SvTimePicker.svelte.d.ts +3 -3
  68. package/dist/SvTimeline.svelte.d.ts +2 -2
  69. package/dist/SvToggleButton.svelte.d.ts +1 -1
  70. package/dist/SvTooltip.svelte.d.ts +1 -1
  71. package/dist/SvTour.svelte.d.ts +3 -3
  72. package/dist/SvTree.svelte.d.ts +3 -3
  73. package/dist/SvTreeSelect.svelte.d.ts +3 -3
  74. package/dist/advanced-filter.svelte.d.ts +1 -1
  75. package/dist/ai.d.ts +1 -1
  76. package/dist/ai.js +1 -1
  77. package/dist/build-api.d.ts +2 -2
  78. package/dist/build-api.js +8 -8
  79. package/dist/builtin-editors.js +1 -1
  80. package/dist/cdn/{GridMenus-CPdy5dxw.js → GridMenus-B0F9iBrG.js} +1 -1
  81. package/dist/cdn/{GridMenus-BLajq_RP.js → GridMenus-IHK_l7m6.js} +1 -1
  82. package/dist/cdn/{src-CKpl94Cx.js → src-B1TdiyS8.js} +272 -270
  83. package/dist/cdn/{src-ogpR-lHu.js → src-Cd0tearp.js} +272 -270
  84. package/dist/cdn/svgrid.js +2 -2
  85. package/dist/cdn/svgrid.svelte-external.js +2 -2
  86. package/dist/cell-render.d.ts +3 -3
  87. package/dist/cell-render.js +6 -6
  88. package/dist/cell-values.d.ts +1 -1
  89. package/dist/cell-values.js +1 -1
  90. package/dist/chart-export.d.ts +1 -1
  91. package/dist/clipboard.d.ts +2 -2
  92. package/dist/clipboard.js +5 -5
  93. package/dist/column-groups.js +1 -1
  94. package/dist/columns.d.ts +2 -2
  95. package/dist/columns.js +1 -1
  96. package/dist/core.d.ts +1 -1
  97. package/dist/core.js +1 -1
  98. package/dist/createAutocomplete.svelte.d.ts +1 -1
  99. package/dist/createAutocomplete.svelte.js +2 -2
  100. package/dist/createButtonGroup.svelte.js +1 -1
  101. package/dist/createCalendar.svelte.d.ts +4 -4
  102. package/dist/createCalendar.svelte.js +4 -4
  103. package/dist/createCarousel.svelte.d.ts +1 -1
  104. package/dist/createCheckbox.svelte.js +1 -1
  105. package/dist/createColorInput.svelte.js +1 -1
  106. package/dist/createCombobox.svelte.d.ts +1 -1
  107. package/dist/createCombobox.svelte.js +2 -2
  108. package/dist/createCommand.svelte.d.ts +1 -1
  109. package/dist/createCommand.svelte.js +2 -2
  110. package/dist/createCountryInput.svelte.d.ts +1 -1
  111. package/dist/createCountryInput.svelte.js +2 -2
  112. package/dist/createDateTimePicker.svelte.d.ts +1 -1
  113. package/dist/createDateTimePicker.svelte.js +3 -3
  114. package/dist/createDropdownList.svelte.d.ts +1 -1
  115. package/dist/createDropdownList.svelte.js +2 -2
  116. package/dist/createFileUpload.svelte.js +1 -1
  117. package/dist/createForm.svelte.d.ts +1 -1
  118. package/dist/createForm.svelte.js +2 -2
  119. package/dist/createGrid.svelte.d.ts +1 -1
  120. package/dist/createGrid.svelte.js +1 -1
  121. package/dist/createGridState.svelte.d.ts +1 -1
  122. package/dist/createListbox.svelte.d.ts +1 -1
  123. package/dist/createListbox.svelte.js +2 -2
  124. package/dist/createMaskedInput.svelte.js +2 -2
  125. package/dist/createMenu.svelte.d.ts +2 -2
  126. package/dist/createMenu.svelte.js +2 -2
  127. package/dist/createNumberInput.svelte.js +1 -1
  128. package/dist/createOverlay.svelte.js +3 -3
  129. package/dist/createPagination.d.ts +1 -1
  130. package/dist/createPagination.js +1 -1
  131. package/dist/createPasswordInput.svelte.js +1 -1
  132. package/dist/createPhoneInput.svelte.js +3 -3
  133. package/dist/createPopoverSelect.svelte.d.ts +1 -1
  134. package/dist/createPopoverSelect.svelte.js +3 -3
  135. package/dist/createRadioGroup.svelte.js +1 -1
  136. package/dist/createRating.svelte.js +1 -1
  137. package/dist/createSlider.svelte.js +1 -1
  138. package/dist/createSwitch.svelte.js +1 -1
  139. package/dist/createTabs.svelte.d.ts +1 -1
  140. package/dist/createTagsInput.svelte.js +1 -1
  141. package/dist/createToggle.svelte.js +1 -1
  142. package/dist/createTooltip.svelte.js +1 -1
  143. package/dist/createTree.svelte.d.ts +1 -1
  144. package/dist/datetime/date-restrict.d.ts +1 -1
  145. package/dist/datetime/date-restrict.js +1 -1
  146. package/dist/datetime/date-selection.js +1 -1
  147. package/dist/dock-context.d.ts +1 -1
  148. package/dist/dock-manager-model.d.ts +1 -1
  149. package/dist/dock-manager-model.js +1 -1
  150. package/dist/dock-popout.d.ts +1 -1
  151. package/dist/dock-popout.js +1 -1
  152. package/dist/editing.d.ts +2 -2
  153. package/dist/editing.js +5 -5
  154. package/dist/editor-registry.d.ts +1 -1
  155. package/dist/editors/cell-editors.js +1 -1
  156. package/dist/export-format.d.ts +2 -2
  157. package/dist/export-format.js +2 -2
  158. package/dist/export-provider.d.ts +1 -1
  159. package/dist/facet-buckets.d.ts +1 -1
  160. package/dist/facet-buckets.js +1 -1
  161. package/dist/features.d.ts +2 -2
  162. package/dist/features.js +2 -2
  163. package/dist/filter-operators.d.ts +2 -2
  164. package/dist/filter-operators.js +1 -1
  165. package/dist/form-field.d.ts +1 -1
  166. package/dist/grid-messages.js +1 -1
  167. package/dist/headless.d.ts +11 -4
  168. package/dist/headless.js +11 -4
  169. package/dist/index.d.ts +120 -120
  170. package/dist/index.js +111 -111
  171. package/dist/keyboard-handlers.d.ts +2 -2
  172. package/dist/keyboard-handlers.js +2 -2
  173. package/dist/keyboard.d.ts +1 -1
  174. package/dist/menus.d.ts +3 -3
  175. package/dist/menus.js +3 -3
  176. package/dist/named-views.d.ts +1 -1
  177. package/dist/pivot-view.svelte.d.ts +1 -1
  178. package/dist/recurrence.d.ts +1 -1
  179. package/dist/recurrence.js +1 -1
  180. package/dist/row-drag.js +1 -1
  181. package/dist/scheduler-ical.d.ts +1 -1
  182. package/dist/scheduler-model.d.ts +3 -3
  183. package/dist/scheduler-model.js +2 -2
  184. package/dist/scroll-sync.d.ts +2 -2
  185. package/dist/scroll-sync.js +1 -1
  186. package/dist/selection.d.ts +2 -2
  187. package/dist/selection.js +3 -3
  188. package/dist/server-data-source.d.ts +1 -1
  189. package/dist/server-group-model.d.ts +1 -1
  190. package/dist/static-functions.d.ts +1 -1
  191. package/dist/static-functions.js +1 -1
  192. package/dist/subscribe.d.ts +1 -1
  193. package/dist/summaries.d.ts +2 -2
  194. package/dist/summaries.js +3 -3
  195. package/dist/svgrid-wrapper.types.d.ts +5 -5
  196. package/dist/themes/index.d.ts +3 -1
  197. package/dist/themes/index.js +4 -2
  198. package/dist/toast-store.svelte.js +1 -1
  199. package/dist/tree-select.d.ts +1 -1
  200. package/dist/virtualization/column-virtualizer.d.ts +3 -3
  201. package/dist/virtualization/column-virtualizer.js +1 -1
  202. package/dist/virtualization/svelte-virtualizer.svelte.d.ts +3 -3
  203. package/dist/virtualization/svelte-virtualizer.svelte.js +1 -1
  204. package/dist/virtualization/virtualizer.d.ts +1 -1
  205. package/package.json +14 -2
  206. package/src/GridFooter.svelte +6 -3
  207. package/src/SvGrid.controller.svelte.ts +90 -14
  208. package/src/SvGrid.css +42 -2
  209. package/src/SvGrid.types.ts +21 -0
  210. package/src/headless.ts +7 -0
  211. package/src/svgrid.behavior.test.ts +208 -0
  212. package/src/themes/index.ts +4 -2
@@ -69,7 +69,9 @@ export type ThemePreset = {
69
69
  };
70
70
  /** The built-in theme presets, in gallery order (SvGrid's own theme first). */
71
71
  export declare const themePresets: ThemePreset[];
72
- /** The default preset (used when nothing is chosen). */
72
+ /** The default preset (used when nothing is chosen): Ember, the house theme the
73
+ * demo gallery and svgrid.com open on, so a scaffolded or Studio-generated app
74
+ * looks like the demos unless a preset is picked. */
73
75
  export declare const defaultThemePreset: ThemePreset;
74
76
  /** Look up a preset by id. */
75
77
  export declare const getThemePreset: (id?: string) => ThemePreset | undefined;
@@ -101,8 +101,10 @@ export const themePresets = [
101
101
  light: { bg: '#eff1f5', fg: '#4c4f69', muted: '#5c5f77', border: '#ccd0da', headerBg: '#e6e9ef', headerFg: '#4c4f69', accent: '#8839ef', rowAlt: '#e6e9ef', rowHover: '#dce0e8', selectionBg: '#dce0e8' },
102
102
  dark: { bg: '#1e1e2e', fg: '#cdd6f4', muted: '#a6adc8', border: '#313244', headerBg: '#181825', headerFg: '#cdd6f4', accent: '#cba6f7', rowAlt: '#1a1a28', rowHover: '#313244', selectionBg: '#45475a' } },
103
103
  ];
104
- /** The default preset (used when nothing is chosen). */
105
- export const defaultThemePreset = themePresets.find((t) => t.id === 'tailwind') ?? themePresets[0];
104
+ /** The default preset (used when nothing is chosen): Ember, the house theme the
105
+ * demo gallery and svgrid.com open on, so a scaffolded or Studio-generated app
106
+ * looks like the demos unless a preset is picked. */
107
+ export const defaultThemePreset = themePresets.find((t) => t.id === 'ember') ?? themePresets[0];
106
108
  /** Look up a preset by id. */
107
109
  export const getThemePreset = (id) => id ? themePresets.find((t) => t.id === id) : undefined;
108
110
  /** WCAG 2.1 relative luminance. */
@@ -1,4 +1,4 @@
1
- import { announce } from './a11y/live-region';
1
+ import { announce } from './a11y/live-region.js';
2
2
  function createToastStore() {
3
3
  const toasts = $state([]);
4
4
  const timers = new Map();
@@ -4,7 +4,7 @@
4
4
  * resolve the path from root to a value (for the trigger label). Framework-free
5
5
  * + pure so they are unit-tested directly.
6
6
  */
7
- import type { TreeSelectNode } from './ui-app.types';
7
+ import type { TreeSelectNode } from './ui-app.types.js';
8
8
  export type FlatRow = {
9
9
  node: TreeSelectNode;
10
10
  depth: number;
@@ -9,14 +9,14 @@ export declare function createColumnVirtualizer(input: {
9
9
  }): {
10
10
  setViewportWidth(viewportWidth: number): void;
11
11
  setHorizontalOffset(scrollOffset: number): void;
12
- setOptions(next: Partial<import("./types").VirtualizerOptions>): void;
12
+ setOptions(next: Partial<import("./types.js").VirtualizerOptions>): void;
13
13
  setScrollOffset(scrollOffset: number): void;
14
14
  setViewportHeight(viewportHeight: number): void;
15
15
  scrollToIndex(index: number): void;
16
- getVirtualItems(): import("./types").VirtualItem[];
16
+ getVirtualItems(): import("./types.js").VirtualItem[];
17
17
  getTotalSize(): number;
18
18
  getOffsetForIndex(index: number): number;
19
19
  getSizeForIndex(index: number): number;
20
- getState(): import("./types").VirtualizerState;
20
+ getState(): import("./types.js").VirtualizerState;
21
21
  subscribe(listener: () => void): () => boolean;
22
22
  };
@@ -1,4 +1,4 @@
1
- import { createVirtualizer } from './virtualizer';
1
+ import { createVirtualizer } from './virtualizer.js';
2
2
  export function createColumnVirtualizer(input) {
3
3
  const virtualizer = createVirtualizer({
4
4
  count: input.count,
@@ -1,13 +1,13 @@
1
- import type { VirtualizerOptions } from './types';
1
+ import type { VirtualizerOptions } from './types.js';
2
2
  export declare function createSvelteVirtualizer(options: VirtualizerOptions): {
3
3
  readonly version: number;
4
4
  setOptions: (next: Partial<VirtualizerOptions>) => void;
5
5
  setScrollOffset: (scrollOffset: number) => void;
6
6
  setViewportHeight: (viewportHeight: number) => void;
7
7
  scrollToIndex: (index: number) => void;
8
- getVirtualItems: () => import("./types").VirtualItem[];
8
+ getVirtualItems: () => import("./types.js").VirtualItem[];
9
9
  getTotalSize: () => number;
10
10
  getOffsetForIndex: (index: number) => number;
11
11
  getSizeForIndex: (index: number) => number;
12
- getState: () => import("./types").VirtualizerState;
12
+ getState: () => import("./types.js").VirtualizerState;
13
13
  };
@@ -1,4 +1,4 @@
1
- import { createVirtualizer } from './virtualizer';
1
+ import { createVirtualizer } from './virtualizer.js';
2
2
  export function createSvelteVirtualizer(options) {
3
3
  const virtualizer = createVirtualizer(options);
4
4
  let version = $state(0);
@@ -1,4 +1,4 @@
1
- import type { VirtualItem, VirtualizerOptions, VirtualizerState } from './types';
1
+ import type { VirtualItem, VirtualizerOptions, VirtualizerState } from './types.js';
2
2
  type VirtualizerListener = () => void;
3
3
  /**
4
4
  * The window to render when no DOM measurement exists yet.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@svgrid/grid",
3
- "version": "2.6.16",
3
+ "version": "2.6.18",
4
4
  "description": "Svelte 5-native data grid and data table. Headless-first engine with virtual scrolling for 100k+ rows, Excel-style filtering, inline editing, grouping, tree data, pivot, and server-side data. Drop-in <SvGrid> component. TypeScript, SSR-ready.",
5
5
  "author": "jQWidgets <boikom@jqwidgets.com>",
6
6
  "license": "MIT",
@@ -34,7 +34,19 @@
34
34
  "headless",
35
35
  "headless-table",
36
36
  "virtual-scroll",
37
+ "virtual-scrolling",
38
+ "virtualization",
39
+ "sortable",
40
+ "filterable",
41
+ "pagination",
42
+ "tree-grid",
43
+ "pivot-table",
37
44
  "spreadsheet",
45
+ "ag-grid-alternative",
46
+ "tanstack-table",
47
+ "tanstack-table-alternative",
48
+ "svelte-headless-table",
49
+ "svelte-headless-table-alternative",
38
50
  "typescript",
39
51
  "svgrid"
40
52
  ],
@@ -110,7 +122,7 @@
110
122
  "test:types": "svelte-check --tsconfig ./tsconfig.json",
111
123
  "test:lib": "vitest run --coverage",
112
124
  "test:build": "publint --strict",
113
- "build": "svelte-package --input ./src --output ./dist && node ../../tools/strip-dist-tests.mjs ./dist && node ./scripts/build-cdn.mjs && node ./scripts/build-themes.mjs",
125
+ "build": "svelte-package --input ./src --output ./dist && node ../../tools/strip-dist-tests.mjs ./dist && node ../../tools/add-dist-extensions.mjs ./dist && node ./scripts/build-cdn.mjs && node ./scripts/build-themes.mjs",
114
126
  "lint": "eslint ./src"
115
127
  },
116
128
  "devDependencies": {
@@ -117,9 +117,12 @@
117
117
  onChange={(v) => setPageSize(Number(v))}
118
118
  />
119
119
  {:else}
120
- <!-- Mirrors SvGridDropdown's closed trigger exactly (same classes,
121
- same ARIA) so there is no visual change and no layout shift
122
- when the real component swaps in. -->
120
+ <!-- Mirrors SvGridDropdown's closed trigger (same classes, same
121
+ ARIA). Its box styling comes from the global
122
+ .sv-grid-pagination-pagesize-dd rules in SvGrid.css, NOT from
123
+ SvGridDropdown's scoped styles (those only exist once the chunk
124
+ loads) - so keep the two in step or the placeholder renders as
125
+ a stock <button> until the first click. -->
123
126
  <div class="sv-grid-dropdown">
124
127
  <button
125
128
  type="button"
@@ -595,8 +595,13 @@ export function createSvGridController<
595
595
  ((props.selectionMode ?? "both") === "row" ||
596
596
  (props.selectionMode ?? "both") === "both"),
597
597
  );
598
+ // `selectable` is the shortcut alias, so it wins over the fine-grained prop,
599
+ // which in turn wins over `selectionMode` - the same precedence the other
600
+ // shortcuts use. Unlike them it starts ON (selectionMode defaults to 'both'),
601
+ // so its real work is `selectable={false}`.
598
602
  const enableCellSelectionEffective = $derived(
599
- props.enableCellSelection ??
603
+ props.selectable ??
604
+ props.enableCellSelection ??
600
605
  ((props.selectionMode ?? "both") === "cell" ||
601
606
  (props.selectionMode ?? "both") === "both"),
602
607
  );
@@ -2543,7 +2548,53 @@ export function createSvGridController<
2543
2548
  // footer is correct on the first frame (no flicker).
2544
2549
  const SUMMARY_DEFER_CELL_LIMIT = 50_000;
2545
2550
 
2546
- let summaryByColumn = $state<Record<string, string>>({});
2551
+ /** Deferred summaries for a grid too big to total inline. A grid under the
2552
+ * cell limit is handled by `eagerSummaries` below - synchronously, so the
2553
+ * totals are in the SERVER-rendered footer too. */
2554
+ let deferredSummaries = $state<Record<string, string>>({});
2555
+
2556
+ /**
2557
+ * The aggregator, handed over once `createSummaries(ctx)` has run - it needs
2558
+ * `ctx`, which cannot exist this early. Held in `$state` so `eagerSummaries`
2559
+ * recomputes when it lands.
2560
+ *
2561
+ * The whole dance is to keep the declaration ABOVE `ctx`: a `$derived`
2562
+ * referenced before its declaration is mis-compiled (the binding is dropped
2563
+ * and the `ctx` getter comes out with an empty body, so the footer read
2564
+ * `undefined` and threw on the first column id).
2565
+ */
2566
+ let summarize = $state<
2567
+ | null
2568
+ | ((rows: ReadonlyArray<Row<TData>>, columns: ReadonlyArray<Column<TData>>) => Record<string, string>)
2569
+ >(null);
2570
+
2571
+ /**
2572
+ * Footer totals for a grid small enough to aggregate inline, derived rather
2573
+ * than assigned from an `$effect`.
2574
+ *
2575
+ * Effects never run during SSR, so when the totals lived only in the effect
2576
+ * the server emitted a summary row of EMPTY cells: the numbers appeared only
2577
+ * after hydration (a visible pop-in), and never at all for a reader with
2578
+ * JavaScript off - while the row still took up space and drew its border.
2579
+ * A `$derived` runs on the server too, so the totals ship with the HTML.
2580
+ *
2581
+ * `null` above the cell limit - that case stays on the rAF-deferred effect,
2582
+ * which keeps a huge grid painting before it totals.
2583
+ */
2584
+ const eagerSummaries = $derived.by(() => {
2585
+ if (!summarize) return null;
2586
+ if (!(props.enableRowSummaries ?? true)) return null;
2587
+ const rows = allRows;
2588
+ const columns = allColumns;
2589
+ if (rows.length * columns.length > SUMMARY_DEFER_CELL_LIMIT) return null;
2590
+ // `computeSummaries` reads `editedCellValues` through `ctx`, so an inline
2591
+ // edit re-runs this too.
2592
+ return summarize(rows, columns);
2593
+ });
2594
+
2595
+ /** What the footer renders: the inline totals when the grid is small enough,
2596
+ * otherwise the rAF-deferred ones. */
2597
+ const summaryByColumn = $derived(eagerSummaries ?? deferredSummaries);
2547
2598
  $effect(() => {
2548
2599
  // Re-aggregate whenever the data / columns / edits change. We depend on
2549
2600
  // `allRows` / `allColumns` / `editedCellValues` DIRECTLY - not the
@@ -2557,20 +2608,21 @@ export function createSvGridController<
2557
2608
  const rows = allRows;
2558
2609
  const columns = allColumns;
2559
2610
  if (!(props.enableRowSummaries ?? true)) {
2560
- summaryByColumn = {};
2611
+ deferredSummaries = {};
2561
2612
  return;
2562
2613
  }
2563
2614
  if (
2564
2615
  rows.length * columns.length <= SUMMARY_DEFER_CELL_LIMIT ||
2565
2616
  typeof requestAnimationFrame === "undefined"
2566
2617
  ) {
2567
- summaryByColumn = computeSummaries(rows, columns);
2618
+ // Small enough to total inline - `eagerSummaries` already did, and it
2619
+ // wins in the getter, so there is nothing to recompute here.
2568
2620
  return;
2569
2621
  }
2570
2622
  // Large grid: paint first, total a frame later.
2571
2623
  let cancelled = false;
2572
2624
  const handle = requestAnimationFrame(() => {
2573
- if (!cancelled) summaryByColumn = computeSummaries(rows, columns);
2625
+ if (!cancelled) deferredSummaries = computeSummaries(rows, columns);
2574
2626
  });
2575
2627
  return () => {
2576
2628
  cancelled = true;
@@ -2778,18 +2830,39 @@ export function createSvGridController<
2778
2830
  // width/height stays in sync. Always attached (window resize / parent
2779
2831
  // layout shift / sidebar collapse can change the size whether the
2780
2832
  // consumer passed a numeric or "100%" containerHeight).
2781
- /** True after the first ResizeObserver tick - i.e. once the grid has
2782
- * measured its real container size and `fitColumns` has had a chance
2783
- * to scale the columns to that width. Used to gate the scrollbar
2784
- * visibility: rendering it before this flips paints a horizontal
2785
- * scrollbar for ONE frame (based on the base column widths summing
2786
- * larger than the viewport), then immediately hides it once fit
2787
- * scaling kicks in - visible as a "flashing horizontal scrollbar"
2788
- * every time a demo first loads. */
2833
+ /** True once the grid has measured its real container size and `fitColumns`
2834
+ * has had a chance to scale the columns to that width. Gates the scrollbar
2835
+ * visibility, the scrollbar gutter on a trailing right-aligned column and
2836
+ * the top pager: rendering those before this flips paints a horizontal
2837
+ * scrollbar for ONE frame (based on the base column widths summing larger
2838
+ * than the viewport), then immediately hides it once fit scaling kicks in -
2839
+ * visible as a "flashing horizontal scrollbar" every time a demo first
2840
+ * loads. */
2789
2841
  let hasMeasured = $state(false);
2790
2842
 
2791
2843
  $effect(() => {
2792
2844
  if (!scrollContainer) return;
2845
+ // Measure SYNCHRONOUSLY here, not just from the observer below. Effects run
2846
+ // after the DOM is in place but before the browser paints, so reading the
2847
+ // container now lets the very first painted frame already carry the
2848
+ // scrollbars, the fit-scaled column widths, the 16px gutter on a trailing
2849
+ // right-aligned column and the top pager (which has a `border-top`).
2850
+ // `observeSizeRaf` has to defer its callback by a frame (or the
2851
+ // ResizeObserver re-notifies within one delivery cycle), so relying on it
2852
+ // alone guarantees one painted frame in the unmeasured state - a visible
2853
+ // flash on EVERY mount, and therefore on every navigation in an app that
2854
+ // recreates the grid (e.g. a `{#key}` around it, or a route that remounts).
2855
+ // `untrack` keeps the writes below from making this effect depend on
2856
+ // `viewportVersion` (it would then re-run and re-attach the observer).
2857
+ untrack(() => {
2858
+ // A zero-height container means the grid is not laid out yet (hidden
2859
+ // ancestor, `display: none`); leave it unmeasured and let the observer
2860
+ // flip it when the box becomes real.
2861
+ if (scrollContainer && scrollContainer.clientHeight > 0) {
2862
+ viewportVersion += 1;
2863
+ hasMeasured = true;
2864
+ }
2865
+ });
2793
2866
  return observeSizeRaf(scrollContainer, () => {
2794
2867
  viewportVersion += 1;
2795
2868
  if (!hasMeasured) hasMeasured = true;
@@ -3705,7 +3778,6 @@ export function createSvGridController<
3705
3778
  get computeSummaries() { return computeSummaries; },
3706
3779
  get SUMMARY_DEFER_CELL_LIMIT() { return SUMMARY_DEFER_CELL_LIMIT; },
3707
3780
  get summaryByColumn() { return summaryByColumn; },
3708
- set summaryByColumn(v) { summaryByColumn = v as never; },
3709
3781
  get hasMeasured() { return hasMeasured; },
3710
3782
  set hasMeasured(v) { hasMeasured = v as never; },
3711
3783
  get scrollBottomArmed() { return scrollBottomArmed; },
@@ -3859,6 +3931,10 @@ export function createSvGridController<
3859
3931
  const { showTooltipFor, hideTooltip, flushScheduledScrollSync, scheduleScrollSync, onBodyScroll } = createScrollSync<TFeatures, TData>(ctx);
3860
3932
  const { onGridKeyDown, onWindowKeydown, onHeaderSortClick } = createKeyboard<TFeatures, TData>(ctx);
3861
3933
  const { computeSummaries, hasRenderedColumn } = createSummaries<TFeatures, TData>(ctx);
3934
+ // Hand the aggregator to `eagerSummaries` (declared above `ctx`, which this
3935
+ // needs). Plain assignment during init - no effect, so SSR gets it too.
3936
+ summarize = computeSummaries;
3937
+
3862
3938
  const { updateFilterRow, updateFilterOperator, updateFilterMenuValue, updateFilterMenuValueTo, addFilterToken, removeFilterToken, toggleFilterToken, toggleCheckboxWithKeyboard, isColumnFiltered, closeMenus, openInSuggest, closeInSuggest, openChooseColumns, openColumnMenu, openFilterMenu, openOperatorMenu, sortColumnFromMenu, clearColumnSort, groupByColumnFromMenu, clearGroupingFromMenu, isFacetChecked, toggleFacetValue, setFacetSelection, setFilterTokens, isAllFacetsChecked, toggleAllFacets, clearColumnFilter, changePage, goToPage, setPageSize, openContextMenu, closeContextMenu, contextMenuItems, saveComment, removeComment, closeCommentEditor } = createMenus<TFeatures, TData>(ctx);
3863
3939
  const { cellConditionalFormat, computeRowClass, computeCellClass, computeCellTooltip, computeCellValidity, computeCellNote, getColumnEditorOptions, areEditorOptionsLoading, formatListCellValue, formatCellValue, formatPinnedValue, computePinnedCellClass } = createCellRender<TFeatures, TData>(ctx);
3864
3940
  const { isCellEditable, isCellEditableAt, getRowColumnValue, getCellDisplayValue, startEditingWithChar, startEditing, stopEditing, startFullRowEdit, setFullRowDraft, commitFullRowEdit, cancelFullRowEdit, saveEditingCell, applyHistoryStep, updateEditingCellValue, onEditorKeyDown, commitAndMoveByTab, focusOnMount, onCellDoubleClick, pasteFromClipboard, onGridPaste } = createEditing<TFeatures, TData>(ctx);
package/src/SvGrid.css CHANGED
@@ -2495,13 +2495,53 @@ select.sv-grid-fr-editor {
2495
2495
  border-radius: 5px;
2496
2496
  background: var(--sg-input-bg, #fff);
2497
2497
  }
2498
- .sv-grid-pagination-pagesize-dd .sv-grid-dropdown,
2499
- .sv-grid-pagination-pagesize-dd .sv-grid-dropdown-trigger {
2498
+ /* The closed page-size trigger is plain markup until the real SvGridDropdown
2499
+ chunk loads on the first click (GridFooter.svelte keeps that component lazy).
2500
+ SvGridDropdown's own trigger rules are scoped to the component, so they never
2501
+ reach the placeholder - it needs the same box here, or the pager shows a
2502
+ stock <button> (UA border + padding, hugging the left) until the first click.
2503
+ Keep these in step with the .sv-grid-dropdown* rules in SvGridDropdown.svelte. */
2504
+ .sv-grid-pagination-pagesize-dd .sv-grid-dropdown {
2505
+ position: absolute;
2506
+ inset: 0;
2507
+ height: 100%;
2500
2508
  background: transparent;
2501
2509
  border-radius: 5px;
2502
2510
  font-size: 13px;
2503
2511
  color: var(--sg-fg, #0f172a);
2504
2512
  }
2513
+ .sv-grid-pagination-pagesize-dd .sv-grid-dropdown-trigger {
2514
+ display: flex;
2515
+ align-items: center;
2516
+ gap: 6px;
2517
+ width: 100%;
2518
+ height: 100%;
2519
+ min-height: 28px;
2520
+ padding: 0 8px;
2521
+ border: 0;
2522
+ outline: none;
2523
+ border-radius: 5px;
2524
+ background: transparent;
2525
+ color: var(--sg-fg, #0f172a);
2526
+ font: inherit;
2527
+ font-size: 13px;
2528
+ text-align: start;
2529
+ cursor: pointer;
2530
+ box-sizing: border-box;
2531
+ }
2532
+ .sv-grid-pagination-pagesize-dd .sv-grid-dropdown-label {
2533
+ flex: 1 1 auto;
2534
+ min-width: 0;
2535
+ overflow: hidden;
2536
+ text-overflow: ellipsis;
2537
+ white-space: nowrap;
2538
+ }
2539
+ .sv-grid-pagination-pagesize-dd .sv-grid-dropdown-caret {
2540
+ flex: 0 0 auto;
2541
+ font-size: 10px;
2542
+ opacity: 0.7;
2543
+ line-height: 1;
2544
+ }
2505
2545
  .sv-grid-pagination-pagesize-dd:focus-within {
2506
2546
  border-color: var(--sg-accent, #2563eb);
2507
2547
  box-shadow: 0 0 0 2px color-mix(in srgb, var(--sg-accent, #2563eb) 30%, transparent);
@@ -1020,6 +1020,7 @@ export type Props<TFeatures extends TableFeatures = TableFeatures, TData extends
1020
1020
  * `sortable` - column sorting (injects `rowSortingFeature`)
1021
1021
  * `filterable` - column filtering (injects `columnFilteringFeature`)
1022
1022
  * `editable` - inline cell editing (alias of `enableInlineEditing`)
1023
+ * `selectable` - cell selection (alias of `enableCellSelection`)
1023
1024
  * `groupable` - row grouping controls(alias of `showGroupingControls`,
1024
1025
  * also injects `columnGroupingFeature`)
1025
1026
  * `pageable` - pagination footer (alias of `showPagination`)
@@ -1030,6 +1031,26 @@ export type Props<TFeatures extends TableFeatures = TableFeatures, TData extends
1030
1031
  sortable?: boolean;
1031
1032
  filterable?: boolean;
1032
1033
  editable?: boolean;
1034
+ /**
1035
+ * Cell selection - click a cell to select it, drag or shift-click to extend
1036
+ * the selection to a range. Alias of `enableCellSelection`, and the highest
1037
+ * priority of the three inputs that decide it:
1038
+ * `selectable` -> `enableCellSelection` -> `selectionMode`.
1039
+ *
1040
+ * NOTE the asymmetry with its siblings, which are all off until you opt in:
1041
+ * this one is **ON by default**, because `selectionMode` defaults to
1042
+ * `'both'`. It is named here so the capability is discoverable next to the
1043
+ * other shortcuts, and so `selectable={false}` is a one-word way to turn
1044
+ * cell selection off (the longhand being `selectionMode="none"`, which also
1045
+ * removes the row-selection surface).
1046
+ *
1047
+ * This is the CELL surface only. The selection **checkbox column** is the
1048
+ * other half of `selectionMode`, controlled by `showRowSelection` - also on
1049
+ * by default, and it needs no feature import (verified: the column renders
1050
+ * and toggles rows with an empty `features` set). Use `selectionMode` when
1051
+ * you want to move both surfaces at once.
1052
+ */
1053
+ selectable?: boolean;
1033
1054
  /**
1034
1055
  * Client-side tree data: nest rows into a hierarchy by parent id.
1035
1056
  *
package/src/headless.ts CHANGED
@@ -7,6 +7,13 @@
7
7
  * your own renderer, running the engine under SSR, or unit-testing row models
8
8
  * without a browser.
9
9
  *
10
+ * Two entry functions, and the difference matters outside a bundler:
11
+ * `createSvGrid` is the Svelte 5 wrapper - it holds its state in runes, so it
12
+ * needs the Svelte compiler in the pipeline (Vite, SvelteKit, vitest). Bare
13
+ * `node script.mjs` cannot run it. `createSvGridCore` is the same engine with
14
+ * no runes and no reactivity, so it runs anywhere Node runs: a service, a
15
+ * worker, a CLI, a plain unit test.
16
+ *
10
17
  * Every runtime symbol here is also reachable from the main `@svgrid/grid`
11
18
  * barrel, so the two paths never disagree. This entry additionally exposes the
12
19
  * low-level types a custom renderer needs and the barrel does not re-export
@@ -703,3 +703,211 @@ describe('SvGrid - fitColumns', () => {
703
703
  }
704
704
  })
705
705
  })
706
+
707
+ describe('SvGrid - selectable shortcut (cell selection)', () => {
708
+ // The gated path a user actually takes: pointerdown on a cell runs through
709
+ // createSelection.setSelection, which bails on !enableCellSelectionEffective.
710
+ // Asserting through it (rather than api.selectCells, which writes the range
711
+ // directly and bypasses the gate) is what makes these tests meaningful.
712
+ // Deliberately skips `.sv-grid-selection-cell`: the checkbox column is a
713
+ // different surface and a pointerdown there starts no range, so clicking it
714
+ // would make every assertion below pass vacuously.
715
+ async function clickCell(target: HTMLElement, index: number) {
716
+ const cells = target.querySelectorAll(
717
+ '.sv-grid-cell:not(.sv-grid-selection-cell)',
718
+ )
719
+ const cell = cells[index]
720
+ if (!cell) throw new Error(`no data cell at index ${index} (found ${cells.length})`)
721
+ cell.dispatchEvent(new MouseEvent('pointerdown', { bubbles: true, button: 0 }))
722
+ await tick()
723
+ }
724
+
725
+ it('is ON by default - selectionMode defaults to "both"', async () => {
726
+ const { api, target, destroy } = await mountGrid()
727
+ try {
728
+ await tick()
729
+ await clickCell(target, 0)
730
+ expect(api.getSelected().length).toBe(1)
731
+ } finally {
732
+ destroy()
733
+ }
734
+ })
735
+
736
+ it('selectable={false} turns cell selection off', async () => {
737
+ const { api, target, destroy } = await mountGrid({ selectable: false })
738
+ try {
739
+ await tick()
740
+ await clickCell(target, 0)
741
+ expect(api.getSelected()).toEqual([])
742
+ } finally {
743
+ destroy()
744
+ }
745
+ })
746
+
747
+ it('selectable={true} wins over selectionMode="none"', async () => {
748
+ const { api, target, destroy } = await mountGrid({
749
+ selectable: true,
750
+ selectionMode: 'none',
751
+ })
752
+ try {
753
+ await tick()
754
+ await clickCell(target, 0)
755
+ expect(api.getSelected().length).toBe(1)
756
+ } finally {
757
+ destroy()
758
+ }
759
+ })
760
+
761
+ it('selectable={false} wins over enableCellSelection={true}', async () => {
762
+ const { api, target, destroy } = await mountGrid({
763
+ selectable: false,
764
+ enableCellSelection: true,
765
+ })
766
+ try {
767
+ await tick()
768
+ await clickCell(target, 0)
769
+ expect(api.getSelected()).toEqual([])
770
+ } finally {
771
+ destroy()
772
+ }
773
+ })
774
+
775
+ it('falls back to enableCellSelection when selectable is omitted', async () => {
776
+ const { api, target, destroy } = await mountGrid({ enableCellSelection: false })
777
+ try {
778
+ await tick()
779
+ await clickCell(target, 0)
780
+ expect(api.getSelected()).toEqual([])
781
+ } finally {
782
+ destroy()
783
+ }
784
+ })
785
+
786
+ it('api.setOption("selectable", false) turns it off at runtime', async () => {
787
+ const { api, target, destroy } = await mountGrid()
788
+ try {
789
+ await tick()
790
+ await clickCell(target, 0)
791
+ expect(api.getSelected().length).toBe(1)
792
+
793
+ api.setOption('selectable', false)
794
+ await tick()
795
+ api.selectCells([])
796
+ await tick()
797
+ await clickCell(target, 0)
798
+ expect(api.getSelected()).toEqual([])
799
+ } finally {
800
+ destroy()
801
+ }
802
+ })
803
+ })
804
+
805
+ /**
806
+ * Guards the promise the homepage "A real grid in ~15 lines" section makes:
807
+ * the snippet shown there is shortcuts-only - no `features` import, no
808
+ * `editorType` - so every capability it advertises must work from exactly
809
+ * those props. The section drifted from the component once already (it showed
810
+ * a bare grid beside a preview captioned "sort, filter, select, edit ... are
811
+ * all live"), which is what this test exists to catch.
812
+ */
813
+ describe('SvGrid - homepage snippet (shortcuts only, no features import)', () => {
814
+ const snippetRows = [
815
+ { firstName: 'Ada', age: 36, status: 'active' },
816
+ { firstName: 'Linus', age: 54, status: 'active' },
817
+ { firstName: 'Grace', age: 85, status: 'inactive' },
818
+ ]
819
+ const snippetColumns = [
820
+ { field: 'firstName', header: 'First name' },
821
+ { field: 'age', header: 'Age' },
822
+ { field: 'status', header: 'Status' },
823
+ ] as unknown as typeof personColumns
824
+
825
+ function mountSnippet(): Promise<MountResult> {
826
+ return new Promise((resolveApi, rejectApi) => {
827
+ const target = document.createElement('div')
828
+ document.body.appendChild(target)
829
+ const app = mount(SvGrid, {
830
+ target,
831
+ props: {
832
+ data: snippetRows,
833
+ columns: snippetColumns,
834
+ // The four shortcuts the snippet passes - and nothing else.
835
+ sortable: true,
836
+ filterable: true,
837
+ editable: true,
838
+ selectable: true,
839
+ getRowId: (r: { firstName: string }) => r.firstName,
840
+ rowHeight: 36,
841
+ containerHeight: 300,
842
+ virtualization: false,
843
+ onApiReady(api: SvGridApi<any, any>) {
844
+ resolveApi({
845
+ api,
846
+ target,
847
+ destroy: () => {
848
+ unmount(app)
849
+ target.remove()
850
+ },
851
+ } as unknown as MountResult)
852
+ },
853
+ } as never,
854
+ })
855
+ if (!app) rejectApi(new Error('mount failed'))
856
+ })
857
+ }
858
+
859
+ it('sortable works without importing rowSortingFeature', async () => {
860
+ const { api, destroy } = await mountSnippet()
861
+ try {
862
+ await tick()
863
+ api.setSort('age', 'asc')
864
+ await tick()
865
+ expect(api.getState().sorting).toEqual([{ id: 'age', desc: false }])
866
+ } finally {
867
+ destroy()
868
+ }
869
+ })
870
+
871
+ it('editable opens an editor on a column with no editorType', async () => {
872
+ const { api, target, destroy } = await mountSnippet()
873
+ try {
874
+ await tick()
875
+ api.startEditing(0, 'firstName')
876
+ await tick()
877
+ expect(target.querySelectorAll('.sv-grid-cell-editing').length).toBe(1)
878
+ } finally {
879
+ destroy()
880
+ }
881
+ })
882
+
883
+ it('selectable gives cell selection', async () => {
884
+ const { api, target, destroy } = await mountSnippet()
885
+ try {
886
+ await tick()
887
+ const cell = target.querySelector('.sv-grid-cell:not(.sv-grid-selection-cell)')
888
+ cell?.dispatchEvent(new MouseEvent('pointerdown', { bubbles: true, button: 0 }))
889
+ await tick()
890
+ expect(api.getSelected().length).toBe(1)
891
+ } finally {
892
+ destroy()
893
+ }
894
+ })
895
+
896
+ it('the checkbox column renders and toggles a row - no feature import needed', async () => {
897
+ const { api, target, destroy } = await mountSnippet()
898
+ try {
899
+ await tick()
900
+ // [0] is the header select-all; [1] is the first row.
901
+ const boxes = target.querySelectorAll<HTMLElement>('.sv-grid-checkbox')
902
+ expect(boxes.length).toBeGreaterThan(1)
903
+ boxes[1]?.click()
904
+ await tick()
905
+ const names = (api.getSelectedRows() as unknown as { firstName: string }[]).map(
906
+ (r) => r.firstName,
907
+ )
908
+ expect(names).toEqual(['Ada'])
909
+ } finally {
910
+ destroy()
911
+ }
912
+ })
913
+ })
@@ -162,8 +162,10 @@ export const themePresets: ThemePreset[] = [
162
162
  dark: { bg: '#1e1e2e', fg: '#cdd6f4', muted: '#a6adc8', border: '#313244', headerBg: '#181825', headerFg: '#cdd6f4', accent: '#cba6f7', rowAlt: '#1a1a28', rowHover: '#313244', selectionBg: '#45475a' } },
163
163
  ]
164
164
 
165
- /** The default preset (used when nothing is chosen). */
166
- export const defaultThemePreset: ThemePreset = themePresets.find((t) => t.id === 'tailwind') ?? themePresets[0]!
165
+ /** The default preset (used when nothing is chosen): Ember, the house theme the
166
+ * demo gallery and svgrid.com open on, so a scaffolded or Studio-generated app
167
+ * looks like the demos unless a preset is picked. */
168
+ export const defaultThemePreset: ThemePreset = themePresets.find((t) => t.id === 'ember') ?? themePresets[0]!
167
169
 
168
170
  /** Look up a preset by id. */
169
171
  export const getThemePreset = (id?: string): ThemePreset | undefined =>