@svgrid/grid 1.0.2 → 1.1.0

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 (143) hide show
  1. package/LICENSE +1 -1
  2. package/README.md +137 -39
  3. package/dist/GridFooter.svelte +164 -0
  4. package/dist/GridFooter.svelte.d.ts +29 -0
  5. package/dist/GridMenus.svelte +570 -0
  6. package/dist/GridMenus.svelte.d.ts +31 -0
  7. package/dist/SvGrid.controller.svelte.d.ts +429 -0
  8. package/dist/SvGrid.controller.svelte.js +1732 -0
  9. package/dist/SvGrid.css +1709 -0
  10. package/dist/SvGrid.helpers.d.ts +35 -0
  11. package/dist/SvGrid.helpers.js +160 -0
  12. package/dist/SvGrid.svelte +344 -7043
  13. package/dist/SvGrid.svelte.d.ts +4 -357
  14. package/dist/SvGrid.types.d.ts +436 -0
  15. package/dist/SvGrid.types.js +1 -0
  16. package/dist/SvGridChart.svelte +1060 -23
  17. package/dist/SvGridChart.svelte.d.ts +17 -0
  18. package/dist/build-api.d.ts +5 -0
  19. package/dist/build-api.js +527 -0
  20. package/dist/cell-render.d.ts +15 -0
  21. package/dist/cell-render.js +246 -0
  22. package/dist/cell-values.d.ts +28 -0
  23. package/dist/cell-values.js +89 -0
  24. package/dist/chart.d.ts +370 -3
  25. package/dist/chart.js +1135 -42
  26. package/dist/clipboard.d.ts +15 -0
  27. package/dist/clipboard.js +356 -0
  28. package/dist/columns.d.ts +30 -0
  29. package/dist/columns.js +277 -0
  30. package/dist/core.d.ts +1 -1
  31. package/dist/css.d.ts +3 -0
  32. package/dist/editing.d.ts +24 -0
  33. package/dist/editing.js +343 -0
  34. package/dist/editors/cell-editors.d.ts +1 -1
  35. package/dist/facet-buckets.d.ts +13 -0
  36. package/dist/facet-buckets.js +54 -0
  37. package/dist/features.d.ts +5 -0
  38. package/dist/features.js +30 -0
  39. package/dist/filter-operators.d.ts +16 -0
  40. package/dist/filter-operators.js +69 -0
  41. package/dist/hyperformula-adapter.d.ts +82 -0
  42. package/dist/hyperformula-adapter.js +73 -0
  43. package/dist/index.d.ts +5 -2
  44. package/dist/index.js +5 -2
  45. package/dist/keyboard-handlers.d.ts +7 -0
  46. package/dist/keyboard-handlers.js +197 -0
  47. package/dist/menus.d.ts +40 -0
  48. package/dist/menus.js +389 -0
  49. package/dist/named-views.d.ts +27 -0
  50. package/dist/named-views.js +39 -0
  51. package/dist/row-resize.d.ts +43 -0
  52. package/dist/row-resize.js +158 -0
  53. package/dist/scroll-sync.d.ts +9 -0
  54. package/dist/scroll-sync.js +86 -0
  55. package/dist/selection.d.ts +26 -0
  56. package/dist/selection.js +387 -0
  57. package/dist/spreadsheet.d.ts +80 -0
  58. package/dist/spreadsheet.js +194 -0
  59. package/dist/summaries.d.ts +12 -0
  60. package/dist/summaries.js +65 -0
  61. package/dist/svgrid-wrapper.types.d.ts +12 -1
  62. package/dist/svgrid.comments-autocomplete.test.d.ts +1 -0
  63. package/dist/svgrid.comments-autocomplete.test.js +96 -0
  64. package/dist/svgrid.context-menu.test.d.ts +1 -0
  65. package/dist/svgrid.context-menu.test.js +102 -0
  66. package/dist/svgrid.new-features.wrapper.test.js +30 -4
  67. package/dist/svgrid.wrapper.test.js +27 -1
  68. package/dist/virtualization/column-virtualizer.d.ts +2 -0
  69. package/dist/virtualization/scroll-scaling.d.ts +28 -0
  70. package/dist/virtualization/scroll-scaling.js +64 -0
  71. package/dist/virtualization/scroll-scaling.test.d.ts +1 -0
  72. package/dist/virtualization/scroll-scaling.test.js +86 -0
  73. package/dist/virtualization/svelte-virtualizer.svelte.d.ts +2 -0
  74. package/dist/virtualization/svelte-virtualizer.svelte.js +2 -0
  75. package/dist/virtualization/virtualizer.d.ts +7 -0
  76. package/dist/virtualization/virtualizer.js +30 -0
  77. package/package.json +1 -1
  78. package/src/GridFooter.svelte +164 -0
  79. package/src/GridMenus.svelte +570 -0
  80. package/src/SvGrid.controller.svelte.ts +2195 -0
  81. package/src/SvGrid.css +1747 -0
  82. package/src/SvGrid.helpers.test.ts +415 -0
  83. package/src/SvGrid.helpers.ts +185 -0
  84. package/src/SvGrid.svelte +348 -7043
  85. package/src/SvGrid.types.ts +456 -0
  86. package/src/SvGridChart.svelte +1060 -23
  87. package/src/build-api.coverage.test.ts +532 -0
  88. package/src/build-api.ts +663 -0
  89. package/src/cell-render.test.ts +451 -0
  90. package/src/cell-render.ts +426 -0
  91. package/src/cell-values.ts +114 -0
  92. package/src/chart-export.test.ts +370 -0
  93. package/src/chart.coverage.test.ts +814 -0
  94. package/src/chart.ts +1352 -47
  95. package/src/clipboard.test.ts +731 -0
  96. package/src/clipboard.ts +524 -0
  97. package/src/collaboration.coverage.test.ts +220 -0
  98. package/src/columns.test.ts +702 -0
  99. package/src/columns.ts +419 -0
  100. package/src/core.ts +8 -0
  101. package/src/css.d.ts +3 -0
  102. package/src/editing.test.ts +837 -0
  103. package/src/editing.ts +513 -0
  104. package/src/editors/cell-editors.coverage.test.ts +156 -0
  105. package/src/editors/cell-editors.ts +1 -0
  106. package/src/facet-buckets.test.ts +353 -0
  107. package/src/facet-buckets.ts +67 -0
  108. package/src/features.ts +128 -0
  109. package/src/filter-operators.test.ts +174 -0
  110. package/src/filter-operators.ts +87 -0
  111. package/src/hyperformula-adapter.test.ts +256 -0
  112. package/src/hyperformula-adapter.ts +124 -0
  113. package/src/index.ts +37 -0
  114. package/src/keyboard-handlers.coverage.test.ts +560 -0
  115. package/src/keyboard-handlers.ts +353 -0
  116. package/src/keyboard.ts +97 -97
  117. package/src/menus.test.ts +620 -0
  118. package/src/menus.ts +554 -0
  119. package/src/named-views.coverage.test.ts +210 -0
  120. package/src/named-views.ts +48 -0
  121. package/src/row-resize.test.ts +369 -0
  122. package/src/row-resize.ts +171 -0
  123. package/src/scroll-sync.test.ts +330 -0
  124. package/src/scroll-sync.ts +216 -0
  125. package/src/selection.test.ts +722 -0
  126. package/src/selection.ts +545 -0
  127. package/src/server-data-source.coverage.test.ts +180 -0
  128. package/src/spreadsheet.test.ts +445 -0
  129. package/src/spreadsheet.ts +246 -0
  130. package/src/summaries.ts +204 -0
  131. package/src/sv-grid-scrollbar.ts +13 -1
  132. package/src/svgrid-wrapper.types.ts +12 -1
  133. package/src/svgrid.behavior.test.ts +22 -0
  134. package/src/svgrid.comments-autocomplete.test.ts +112 -0
  135. package/src/svgrid.context-menu.test.ts +126 -0
  136. package/src/svgrid.interaction.test.ts +30 -0
  137. package/src/svgrid.new-features.wrapper.test.ts +65 -4
  138. package/src/svgrid.wrapper.test.ts +27 -1
  139. package/src/test-setup.ts +9 -6
  140. package/src/virtualization/scroll-scaling.test.ts +148 -0
  141. package/src/virtualization/scroll-scaling.ts +121 -0
  142. package/src/virtualization/svelte-virtualizer.svelte.ts +2 -0
  143. package/src/virtualization/virtualizer.ts +26 -0
@@ -0,0 +1,64 @@
1
+ // Pure math for "huge list" vertical scroll scaling.
2
+ //
3
+ // Browsers cap how tall a single element may be (Chrome/Safari ~33.5M px,
4
+ // Firefox ~17.9M, mobile/high-DPR lower). Once a virtualized grid's true
5
+ // content height (count * rowHeight) exceeds that cap the scroll container
6
+ // silently clamps its scrollHeight and the tail rows become unreachable - a
7
+ // 1,000,000-row grid that only scrolls to ~994,000 on a phone.
8
+ //
9
+ // The fix (the technique react-virtualized calls "scaling"): size the DOM
10
+ // scroll spacer to a capped height the browser CAN render, and map between the
11
+ // limited DOM scroll range and the full logical range the virtualizer works
12
+ // in. This module is the pure, side-effect-free core of that mapping so the
13
+ // invariants can be unit-tested independently of the Svelte controller.
14
+ //
15
+ // When the true height fits under the cap, scaling is INERT: every mapping is
16
+ // the identity and callers behave exactly as before.
17
+ function clamp01(value) {
18
+ if (value <= 0)
19
+ return 0;
20
+ if (value >= 1)
21
+ return 1;
22
+ return value;
23
+ }
24
+ /**
25
+ * Build the scaling mapping for one axis.
26
+ *
27
+ * @param trueTotal The real content height in logical px (count * rowH, or
28
+ * the cumulative offset total for variable rows).
29
+ * @param maxDomHeight The browser's max element height (detected at runtime).
30
+ * @param viewport The scroll viewport height in px.
31
+ *
32
+ * Invariants (verified in scroll-scaling.test.ts):
33
+ * - Inert when `trueTotal <= maxDomHeight`: both maps are the identity.
34
+ * - `domTotal <= maxDomHeight` always, so the spacer never exceeds the cap.
35
+ * - Endpoints line up: domTop 0 -> logical 0, and the DOM max
36
+ * (`domTotal - viewport`) -> the logical max (`trueTotal - viewport`), so
37
+ * the very last row is always reachable.
38
+ * - `logicalToDom` is the inverse of `domToLogical` across the range.
39
+ * - Both maps are monotonic non-decreasing and clamp out-of-range inputs.
40
+ */
41
+ export function createRowScrollScaling(trueTotal, maxDomHeight, viewport) {
42
+ const safeMax = Math.max(maxDomHeight, 1);
43
+ const domTotal = Math.min(Math.max(trueTotal, 0), safeMax);
44
+ const active = trueTotal > safeMax;
45
+ // Scrollable ranges (content minus one viewport). Guard against zero so the
46
+ // ratios below never divide by zero on tiny / empty grids.
47
+ const domRange = Math.max(domTotal - viewport, 1);
48
+ const logicalRange = Math.max(trueTotal - viewport, 0);
49
+ return {
50
+ active,
51
+ domTotal,
52
+ domToLogical(domTop) {
53
+ if (!active)
54
+ return domTop;
55
+ return clamp01(domTop / domRange) * logicalRange;
56
+ },
57
+ logicalToDom(logical) {
58
+ if (!active)
59
+ return logical;
60
+ const lr = Math.max(logicalRange, 1);
61
+ return clamp01(logical / lr) * Math.max(domTotal - viewport, 0);
62
+ },
63
+ };
64
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,86 @@
1
+ import { describe, it, expect } from 'vitest';
2
+ import { createRowScrollScaling } from './scroll-scaling';
3
+ const VIEWPORT = 600;
4
+ describe('createRowScrollScaling - inert (content fits under the cap)', () => {
5
+ // 1M rows * 18px = 18M, under a 33.5M (Chrome) cap -> no scaling.
6
+ const s = createRowScrollScaling(18000000, 33554400, VIEWPORT);
7
+ it('is inactive and exposes the true height as the DOM height', () => {
8
+ expect(s.active).toBe(false);
9
+ expect(s.domTotal).toBe(18000000);
10
+ });
11
+ it('both maps are the identity', () => {
12
+ for (const v of [0, 1, 1234, 9000000, 17999400]) {
13
+ expect(s.domToLogical(v)).toBe(v);
14
+ expect(s.logicalToDom(v)).toBe(v);
15
+ }
16
+ });
17
+ });
18
+ describe('createRowScrollScaling - active (content exceeds the cap)', () => {
19
+ // 1M rows * 32px = 32M, over Firefox's ~17.9M cap -> scaling engages.
20
+ const trueTotal = 32000000;
21
+ const maxDom = 17895697;
22
+ const s = createRowScrollScaling(trueTotal, maxDom, VIEWPORT);
23
+ const domMax = s.domTotal - VIEWPORT;
24
+ const logicalMax = trueTotal - VIEWPORT;
25
+ it('caps the DOM height at the browser limit', () => {
26
+ expect(s.active).toBe(true);
27
+ expect(s.domTotal).toBe(maxDom);
28
+ expect(s.domTotal).toBeLessThanOrEqual(maxDom);
29
+ });
30
+ it('maps the endpoints exactly so the first and last rows are reachable', () => {
31
+ expect(s.domToLogical(0)).toBe(0);
32
+ // The bottom of the DOM scroll range reaches the bottom of the logical range.
33
+ expect(s.domToLogical(domMax)).toBeCloseTo(logicalMax, 3);
34
+ expect(s.logicalToDom(0)).toBe(0);
35
+ expect(s.logicalToDom(logicalMax)).toBeCloseTo(domMax, 3);
36
+ });
37
+ it('clamps out-of-range inputs instead of overshooting', () => {
38
+ expect(s.domToLogical(-100)).toBe(0);
39
+ expect(s.domToLogical(domMax + 10000)).toBeCloseTo(logicalMax, 3);
40
+ expect(s.logicalToDom(-100)).toBe(0);
41
+ expect(s.logicalToDom(logicalMax + 10000)).toBeCloseTo(domMax, 3);
42
+ });
43
+ it('logicalToDom is the inverse of domToLogical across the range', () => {
44
+ for (let i = 0; i <= 10; i += 1) {
45
+ const domTop = (domMax * i) / 10;
46
+ const roundTrip = s.logicalToDom(s.domToLogical(domTop));
47
+ expect(roundTrip).toBeCloseTo(domTop, 2);
48
+ }
49
+ });
50
+ it('both maps are monotonic non-decreasing', () => {
51
+ let prevL = -1;
52
+ let prevD = -1;
53
+ for (let i = 0; i <= 20; i += 1) {
54
+ const domTop = (domMax * i) / 20;
55
+ const logical = s.domToLogical(domTop);
56
+ expect(logical).toBeGreaterThanOrEqual(prevL);
57
+ prevL = logical;
58
+ const logIn = (logicalMax * i) / 20;
59
+ const dom = s.logicalToDom(logIn);
60
+ expect(dom).toBeGreaterThanOrEqual(prevD);
61
+ prevD = dom;
62
+ }
63
+ });
64
+ });
65
+ describe('createRowScrollScaling - edge cases', () => {
66
+ it('handles an empty grid without dividing by zero', () => {
67
+ const s = createRowScrollScaling(0, 17895697, VIEWPORT);
68
+ expect(s.active).toBe(false);
69
+ expect(s.domTotal).toBe(0);
70
+ expect(s.domToLogical(0)).toBe(0);
71
+ expect(s.logicalToDom(0)).toBe(0);
72
+ });
73
+ it('handles content shorter than the viewport', () => {
74
+ const s = createRowScrollScaling(200, 17895697, VIEWPORT);
75
+ expect(s.active).toBe(false);
76
+ expect(s.domToLogical(0)).toBe(0);
77
+ });
78
+ it('extreme scale (100M rows) keeps the last row reachable', () => {
79
+ const trueTotal = 100000000 * 32; // 3.2 billion px
80
+ const s = createRowScrollScaling(trueTotal, 33554400, VIEWPORT);
81
+ expect(s.active).toBe(true);
82
+ expect(s.domTotal).toBe(33554400);
83
+ const domMax = s.domTotal - VIEWPORT;
84
+ expect(s.domToLogical(domMax)).toBeCloseTo(trueTotal - VIEWPORT, 0);
85
+ });
86
+ });
@@ -7,5 +7,7 @@ export declare function createSvelteVirtualizer(options: VirtualizerOptions): {
7
7
  scrollToIndex: (index: number) => void;
8
8
  getVirtualItems: () => import("./types").VirtualItem[];
9
9
  getTotalSize: () => number;
10
+ getOffsetForIndex: (index: number) => number;
11
+ getSizeForIndex: (index: number) => number;
10
12
  getState: () => import("./types").VirtualizerState;
11
13
  };
@@ -15,6 +15,8 @@ export function createSvelteVirtualizer(options) {
15
15
  scrollToIndex: virtualizer.scrollToIndex,
16
16
  getVirtualItems: virtualizer.getVirtualItems,
17
17
  getTotalSize: virtualizer.getTotalSize,
18
+ getOffsetForIndex: virtualizer.getOffsetForIndex,
19
+ getSizeForIndex: virtualizer.getSizeForIndex,
18
20
  getState: virtualizer.getState,
19
21
  };
20
22
  }
@@ -7,6 +7,13 @@ export declare function createVirtualizer(initial: VirtualizerOptions): {
7
7
  scrollToIndex(index: number): void;
8
8
  getVirtualItems(): VirtualItem[];
9
9
  getTotalSize(): number;
10
+ /** Cumulative offset of row `index` from the top in px, regardless
11
+ * of whether `estimateSize` is uniform or per-index. Uses the
12
+ * cached offsets array under function-form sizing so the lookup
13
+ * is O(1) instead of O(index). */
14
+ getOffsetForIndex(index: number): number;
15
+ /** Height of row `index` in px (whichever estimateSize provides). */
16
+ getSizeForIndex(index: number): number;
10
17
  getState(): VirtualizerState;
11
18
  subscribe(listener: VirtualizerListener): () => boolean;
12
19
  };
@@ -221,6 +221,36 @@ export function createVirtualizer(initial) {
221
221
  getTotalSize() {
222
222
  return state.totalSize;
223
223
  },
224
+ /** Cumulative offset of row `index` from the top in px, regardless
225
+ * of whether `estimateSize` is uniform or per-index. Uses the
226
+ * cached offsets array under function-form sizing so the lookup
227
+ * is O(1) instead of O(index). */
228
+ getOffsetForIndex(index) {
229
+ if (index <= 0)
230
+ return 0;
231
+ const count = Math.max(options.count, 0);
232
+ const bounded = Math.min(index, count);
233
+ if (typeof options.estimateSize === 'function') {
234
+ const offsets = getOffsets();
235
+ if (offsets)
236
+ return offsets[bounded] ?? 0;
237
+ let acc = 0;
238
+ const fn = options.estimateSize;
239
+ for (let i = 0; i < bounded; i += 1)
240
+ acc += Math.max(fn(i), 1);
241
+ return acc;
242
+ }
243
+ return bounded * Math.max(options.estimateSize, 1);
244
+ },
245
+ /** Height of row `index` in px (whichever estimateSize provides). */
246
+ getSizeForIndex(index) {
247
+ if (index < 0 || index >= options.count)
248
+ return 0;
249
+ if (typeof options.estimateSize === 'function') {
250
+ return Math.max(options.estimateSize(index), 1);
251
+ }
252
+ return Math.max(options.estimateSize, 1);
253
+ },
224
254
  getState() {
225
255
  return state;
226
256
  },
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@svgrid/grid",
3
- "version": "1.0.2",
3
+ "version": "1.1.0",
4
4
  "description": "SvGrid - a headless-first, Svelte 5-native data grid engine and render component.",
5
5
  "author": "jQWidgets <boikom@jqwidgets.com>",
6
6
  "license": "MIT",
@@ -0,0 +1,164 @@
1
+ <script lang="ts" generics="TFeatures extends TableFeatures = TableFeatures, TData extends RowData = RowData">
2
+ import {
3
+ getGridCellA11yProps,
4
+ getGridCellDomId,
5
+ getGridHeaderA11yProps,
6
+ getGridRootA11yProps,
7
+ getGridRowA11yProps,
8
+ type EditorContext,
9
+ type CellEditorOption,
10
+ type Column,
11
+ type Row,
12
+ type RowData,
13
+ type TableFeatures,
14
+ } from "./index";
15
+ import "./sv-grid-scrollbar";
16
+ import type { Snippet } from "svelte";
17
+ import {
18
+ RenderSnippetConfig,
19
+ RenderComponentConfig,
20
+ } from "./render-component";
21
+ import {
22
+ buildSparkline,
23
+ toSparklineValues,
24
+ } from "./sparkline";
25
+ import SvGridDropdown from "./SvGridDropdown.svelte";
26
+ import type {
27
+ Props,
28
+ SelectionPoint,
29
+ SelectionRange,
30
+ CellEditState,
31
+ FilterOperator,
32
+ FilterOption,
33
+ MenuPosition,
34
+ } from "./SvGrid.types";
35
+ import {
36
+ cfTextStyle,
37
+ fmtStat,
38
+ getEditableInputValue,
39
+ getEditorInputType,
40
+ toValueArray,
41
+ getOptionLabel,
42
+ getOptionColor,
43
+ colorfulChipStyle,
44
+ getEditorClass,
45
+ } from "./SvGrid.helpers";
46
+ import {
47
+ } from "./SvGrid.controller.svelte";
48
+ import type { SvGridController } from "./SvGrid.controller.svelte";
49
+
50
+ let { ctrl }: { ctrl: SvGridController<TFeatures, TData> } = $props();
51
+
52
+ // View facade: re-bind the controller's reactive members so the markup
53
+ // (moved verbatim from SvGrid.svelte) stays identical.
54
+ const paginationEnabled = $derived(ctrl.paginationEnabled);
55
+ const grid = $derived(ctrl.grid);
56
+ const paginationState = $derived(ctrl.paginationState);
57
+ const allRowsBeforePagination = $derived(ctrl.allRowsBeforePagination);
58
+ const statusBarEnabled = $derived(ctrl.statusBarEnabled);
59
+ const statusBarAggregates = $derived(ctrl.statusBarAggregates);
60
+ const statusBarStats = $derived(ctrl.statusBarStats);
61
+ const changePage = $derived(ctrl.changePage);
62
+ const goToPage = $derived(ctrl.goToPage);
63
+ const setPageSize = $derived(ctrl.setPageSize);
64
+ </script>
65
+
66
+ {#if statusBarEnabled && statusBarStats}
67
+ {@const s = statusBarStats}
68
+ <div class="sv-grid-status-bar" role="status" aria-live="polite">
69
+ {#each statusBarAggregates as agg (agg)}
70
+ {#if agg === "count"}
71
+ <span class="sv-grid-status-item"
72
+ ><span class="sv-grid-status-label">Count</span>{fmtStat(s.count)}</span
73
+ >
74
+ {:else if agg === "numericCount"}
75
+ <span class="sv-grid-status-item"
76
+ ><span class="sv-grid-status-label">Numeric</span>{fmtStat(s.numericCount)}</span
77
+ >
78
+ {:else if s.numericCount > 0 && agg === "sum"}
79
+ <span class="sv-grid-status-item"
80
+ ><span class="sv-grid-status-label">Sum</span>{fmtStat(s.sum)}</span
81
+ >
82
+ {:else if s.numericCount > 0 && agg === "avg"}
83
+ <span class="sv-grid-status-item"
84
+ ><span class="sv-grid-status-label">Avg</span>{fmtStat(s.avg)}</span
85
+ >
86
+ {:else if s.numericCount > 0 && agg === "min"}
87
+ <span class="sv-grid-status-item"
88
+ ><span class="sv-grid-status-label">Min</span>{fmtStat(s.min)}</span
89
+ >
90
+ {:else if s.numericCount > 0 && agg === "max"}
91
+ <span class="sv-grid-status-item"
92
+ ><span class="sv-grid-status-label">Max</span>{fmtStat(s.max)}</span
93
+ >
94
+ {/if}
95
+ {/each}
96
+ </div>
97
+ {/if}
98
+
99
+ {#if paginationEnabled}
100
+ {@const totalRows = allRowsBeforePagination.length}
101
+ {@const pageSize = paginationState.pageSize}
102
+ {@const pageCount = Math.max(1, Math.ceil(totalRows / pageSize))}
103
+ {@const currentPage = Math.min(paginationState.pageIndex + 1, pageCount)}
104
+ {@const rangeStart =
105
+ totalRows === 0 ? 0 : (currentPage - 1) * pageSize + 1}
106
+ {@const rangeEnd = Math.min(totalRows, currentPage * pageSize)}
107
+ {@const onFirst = currentPage <= 1}
108
+ {@const onLast = currentPage >= pageCount}
109
+ <div class="sv-grid-pagination" role="navigation" aria-label="Pagination">
110
+ <label class="sv-grid-pagination-pagesize">
111
+ <span>Page Size:</span>
112
+ <select
113
+ onchange={(event) =>
114
+ setPageSize(
115
+ parseInt((event.currentTarget as HTMLSelectElement).value, 10),
116
+ )}
117
+ >
118
+ <option value="10" selected={pageSize === 10}>10</option>
119
+ <option value="25" selected={pageSize === 25}>25</option>
120
+ <option value="50" selected={pageSize === 50}>50</option>
121
+ <option value="100" selected={pageSize === 100}>100</option>
122
+ </select>
123
+ </label>
124
+ <span class="sv-grid-pagination-range">
125
+ <strong>{rangeStart.toLocaleString()}</strong> to
126
+ <strong>{rangeEnd.toLocaleString()}</strong> of
127
+ <strong>{totalRows.toLocaleString()}</strong>
128
+ </span>
129
+ <div class="sv-grid-pagination-nav">
130
+ <button
131
+ type="button"
132
+ class="sv-grid-pagination-btn"
133
+ disabled={onFirst}
134
+ onclick={() => goToPage(0)}
135
+ aria-label="First page">⇤</button
136
+ >
137
+ <button
138
+ type="button"
139
+ class="sv-grid-pagination-btn"
140
+ disabled={onFirst}
141
+ onclick={() => changePage(-1)}
142
+ aria-label="Previous page">‹</button
143
+ >
144
+ <span class="sv-grid-pagination-label">
145
+ Page <strong>{currentPage.toLocaleString()}</strong> of
146
+ <strong>{pageCount.toLocaleString()}</strong>
147
+ </span>
148
+ <button
149
+ type="button"
150
+ class="sv-grid-pagination-btn"
151
+ disabled={onLast}
152
+ onclick={() => changePage(1)}
153
+ aria-label="Next page">›</button
154
+ >
155
+ <button
156
+ type="button"
157
+ class="sv-grid-pagination-btn"
158
+ disabled={onLast}
159
+ onclick={() => goToPage(pageCount - 1)}
160
+ aria-label="Last page">⇥</button
161
+ >
162
+ </div>
163
+ </div>
164
+ {/if}