@warkypublic/svelix 0.1.44 → 0.1.47

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 (109) hide show
  1. package/README.md +93 -33
  2. package/dist/components/BetterMenu/BetterMenu.svelte +21 -37
  3. package/dist/components/BetterMenu/BetterMenuAsyncButton.svelte +21 -31
  4. package/dist/components/BetterMenu/BetterMenuPreview.svelte +27 -29
  5. package/dist/components/BetterMenu/MenuRenderer.svelte +40 -51
  6. package/dist/components/Boxer/Boxer.svelte +488 -602
  7. package/dist/components/Boxer/BoxerTarget.svelte +6 -46
  8. package/dist/components/Boxer/BoxerTarget.svelte.d.ts +1 -0
  9. package/dist/components/Boxer/types.d.ts +0 -1
  10. package/dist/components/Button.svelte +19 -43
  11. package/dist/components/ButtonPreview.svelte +2 -11
  12. package/dist/components/CardGrid/CardGrid.svelte +1141 -0
  13. package/dist/components/CardGrid/CardGrid.svelte.d.ts +61 -0
  14. package/dist/components/CardGrid/CardGridFilterPanel.svelte +320 -0
  15. package/dist/components/CardGrid/CardGridFilterPanel.svelte.d.ts +11 -0
  16. package/dist/components/CardGrid/DefaultCard.svelte +287 -0
  17. package/dist/components/CardGrid/DefaultCard.svelte.d.ts +10 -0
  18. package/dist/components/CardGrid/ImageCardStory.svelte +308 -0
  19. package/dist/components/CardGrid/ImageCardStory.svelte.d.ts +18 -0
  20. package/dist/components/CardGrid/cardGrid.d.ts +33 -0
  21. package/dist/components/CardGrid/cardGrid.js +21 -0
  22. package/dist/components/CardGrid/index.d.ts +4 -0
  23. package/dist/components/CardGrid/index.js +4 -0
  24. package/dist/components/ContentEditor/CollaboraInsertTextDemo.svelte +5 -10
  25. package/dist/components/ContentEditor/ContentEditor.svelte +24 -50
  26. package/dist/components/ContentEditor/subcomponents/AudioPlayer.svelte +17 -17
  27. package/dist/components/ContentEditor/subcomponents/CollaboraEditor.svelte +244 -309
  28. package/dist/components/ContentEditor/subcomponents/EmailViewer.svelte +2 -3
  29. package/dist/components/ContentEditor/subcomponents/ImageViewer.svelte +14 -15
  30. package/dist/components/ContentEditor/subcomponents/MarkdownViewer.svelte +74 -90
  31. package/dist/components/ContentEditor/subcomponents/MonacoEditor.svelte +75 -103
  32. package/dist/components/ContentEditor/subcomponents/Office365Editor.svelte +234 -297
  33. package/dist/components/ContentEditor/subcomponents/PdfViewer.svelte +14 -20
  34. package/dist/components/ContentEditor/subcomponents/TextEditor.svelte +122 -150
  35. package/dist/components/ContentEditor/subcomponents/UnknownFile.svelte +105 -126
  36. package/dist/components/ContentEditor/subcomponents/VideoPlayer.svelte +17 -17
  37. package/dist/components/ContentEditor/subcomponents/ZipViewer.svelte +2 -3
  38. package/dist/components/ErrorBoundary/ErrorBoundary.svelte +41 -58
  39. package/dist/components/ErrorBoundary/ErrorBoundaryPreview.svelte +8 -16
  40. package/dist/components/Former/Former.svelte +306 -424
  41. package/dist/components/Former/FormerButtonArea.svelte +17 -68
  42. package/dist/components/Former/FormerDrawer.svelte +26 -78
  43. package/dist/components/Former/FormerDrawerPreview.svelte +99 -89
  44. package/dist/components/Former/FormerModal.svelte +24 -70
  45. package/dist/components/Former/FormerModalPreview.svelte +99 -89
  46. package/dist/components/Former/FormerPreview.svelte +76 -78
  47. package/dist/components/Former/FormerRestApiPreview.svelte +55 -48
  48. package/dist/components/FormerControllers/ButtonCtrl.svelte +13 -44
  49. package/dist/components/FormerControllers/DateTimeCtrl/DateTimeCtrl.svelte +280 -445
  50. package/dist/components/FormerControllers/DateTimeCtrl/DateTimeCtrlCalendar.svelte +95 -136
  51. package/dist/components/FormerControllers/DateTimeCtrl/DateTimeCtrlPickerPanel.svelte +3 -62
  52. package/dist/components/FormerControllers/DateTimeCtrl/DateTimeCtrlTimeFields.svelte +143 -217
  53. package/dist/components/FormerControllers/IconButtonCtrl.svelte +13 -42
  54. package/dist/components/FormerControllers/InlineWrapper.svelte +12 -41
  55. package/dist/components/FormerControllers/InlineWrapperPreview.svelte +4 -26
  56. package/dist/components/FormerControllers/NativeSelectCtrl.svelte +3 -29
  57. package/dist/components/FormerControllers/NumberInputCtrl.svelte +9 -35
  58. package/dist/components/FormerControllers/PasswordInputCtrl.svelte +4 -23
  59. package/dist/components/FormerControllers/SwitchCtrl.svelte +3 -20
  60. package/dist/components/FormerControllers/TextAreaCtrl.svelte +3 -24
  61. package/dist/components/FormerControllers/TextInputCtrl.svelte +3 -24
  62. package/dist/components/GlobalStateStore/GlobalStateStoreProvider.svelte +73 -120
  63. package/dist/components/Gridler/components/Gridler.svelte +338 -442
  64. package/dist/components/Gridler/components/GridlerCanvas.svelte +720 -1115
  65. package/dist/components/Gridler/components/GridlerEditor.svelte +2 -15
  66. package/dist/components/Gridler/components/GridlerFull.svelte +520 -841
  67. package/dist/components/Gridler/components/GridlerFull.svelte.d.ts +1 -1
  68. package/dist/components/Gridler/components/GridlerFullWithFormerPreview.svelte +194 -147
  69. package/dist/components/Gridler/components/GridlerSearch.svelte +7 -16
  70. package/dist/components/Gridler/components/GridlerSearchToggle.svelte +7 -19
  71. package/dist/components/Gridler/types.d.ts +1 -1
  72. package/dist/components/Icons/IconAdd.svelte +1 -3
  73. package/dist/components/Icons/IconAlertCircle.svelte +1 -3
  74. package/dist/components/Icons/IconAlertTriangle.svelte +1 -3
  75. package/dist/components/Icons/IconCamera.svelte +1 -3
  76. package/dist/components/Icons/IconClose.svelte +1 -3
  77. package/dist/components/Icons/IconEdit.svelte +1 -3
  78. package/dist/components/Icons/IconFilter.svelte +1 -3
  79. package/dist/components/Icons/IconSearch.svelte +1 -3
  80. package/dist/components/Icons/IconSort.svelte +1 -3
  81. package/dist/components/Icons/IconTrash.svelte +1 -3
  82. package/dist/components/Portal/Portal.svelte +14 -38
  83. package/dist/components/Screenshot/Screenshot.svelte +7 -13
  84. package/dist/components/Svark/Svark.svelte +680 -852
  85. package/dist/components/Svark/SvarkContextMenu.svelte +44 -63
  86. package/dist/components/Svark/SvarkInfiniteLoader.svelte +2 -6
  87. package/dist/components/Svark/SvarkPager.svelte +2 -28
  88. package/dist/components/Svark/SvarkSelectionDemo.svelte +4 -8
  89. package/dist/components/Svark/SvarkTopBar.svelte +9 -74
  90. package/dist/components/SvarkGrid/SvarkGrid.svelte +877 -1123
  91. package/dist/components/SvarkGrid/components/SvarkColumnFilterForm.svelte +45 -58
  92. package/dist/components/SvarkGrid/components/SvarkContextMenu.svelte +44 -63
  93. package/dist/components/SvarkGrid/components/SvarkHeaderFilterCell.svelte +41 -75
  94. package/dist/components/SvarkGrid/components/SvarkInfiniteLoader.svelte +2 -6
  95. package/dist/components/SvarkGrid/components/SvarkPager.svelte +2 -28
  96. package/dist/components/SvarkGrid/components/SvarkSearchPopover.svelte +8 -16
  97. package/dist/components/SvarkGrid/components/SvarkTopBar.svelte +6 -62
  98. package/dist/components/SvarkGrid/internal/SvarkGridView.svelte +345 -507
  99. package/dist/components/VTree/VTree.svelte +349 -431
  100. package/dist/components/VTree/VTreeContextMenu.svelte +21 -33
  101. package/dist/components/VTree/VTreeEventsDemo.svelte +48 -58
  102. package/dist/components/VTree/VTreeRow.svelte +13 -65
  103. package/dist/components/VTree/VTreeSearch.svelte +6 -13
  104. package/dist/components/VTree/VTreeVirtualViewport.svelte +37 -106
  105. package/dist/components/index.d.ts +1 -0
  106. package/dist/components/index.js +1 -0
  107. package/llm/COMPONENT_GUIDE.md +240 -0
  108. package/llm/plans/card_grid.plan.md +305 -0
  109. package/package.json +48 -46
@@ -1,1177 +1,931 @@
1
- <script lang="ts">
2
- /* eslint-disable @typescript-eslint/no-explicit-any */
3
- import type { IApi } from '@svar-ui/svelte-grid';
4
- import SvarkGridView from './internal/SvarkGridView.svelte';
5
- import { SvarkResolveSpecAdapter } from './adapters/SvarkResolveSpecAdapter.js';
6
- import { SvarkRestHeaderSpecAdapter } from './adapters/SvarkRestHeaderSpecAdapter.js';
7
- import type {
8
- GridColumn,
9
- GridColumnFilters,
10
- GridColumnSortOrder,
11
- GridCommonProps,
12
- GridEventType
13
- } from '../Types/generic_grid';
14
- import type {
15
- SvarkFilterState,
16
- SvarkSortState,
17
- SvarkAdapterType,
18
- SvarkInitialQuery,
19
- SvarkContextMenuItem,
20
- SvarkContextMenuProps,
21
- SvarkProps,
22
- IColumnConfig,
23
- IConfig
24
- } from './types.js';
25
- import {
26
- adapterKindFromDataSource,
27
- gridColumnFiltersToSvarkFilterState,
28
- idsToSelectedItems,
29
- mapGenericColumnsToSvarColumns,
30
- selectedItemsToIds,
31
- sortOrderToSvarkSortState,
32
- svarkSortStateToSortMarks,
33
- svarkSortStateToSortOrder,
34
- toCssSize
35
- } from './utils/svarGridMapping.js';
36
- import SvarkHeaderFilterCell from './components/SvarkHeaderFilterCell.svelte';
37
- import { SVARKGRID_DEFAULT_ITEMS_MARKER_ID } from './utils/contextMenu.js';
38
-
39
- export type SvarProps = Partial<
40
- Omit<
41
- SvarkProps,
42
- // Generic-first equivalents / resolved from GridCommonProps:
43
- | 'columns'
44
- | 'height'
45
- // Server config is generic-first but allowed as fallback:
46
- | 'url'
47
- | 'baseUrl'
48
- | 'token'
49
- | 'schema'
50
- | 'entity'
51
- >
52
- > & {
53
- columns?: IColumnConfig[];
54
- url?: string;
55
- baseUrl?: string;
56
- token?: string;
57
- schema?: string;
58
- entity?: string;
59
- height?: string | number;
60
- config?: Omit<IConfig, 'data'>;
61
- };
62
-
63
- export interface Props extends GridCommonProps<Record<string, unknown>, unknown> {
64
- columns: Array<GridColumn<Record<string, unknown>, unknown>>;
65
- /** Svark/SVAR-grid escape hatch props. Generic props take precedence when overlapping. */
66
- SvarProps?: SvarProps;
67
- /** Used only when `searchValue` is undefined (uncontrolled). */
68
- defaultSearchValue?: string;
69
- /** Rows per fetch/page. If omitted, falls back to `SvarProps.pageSize` then 100. */
70
- pageSize?: number;
71
- /** Enables SVAR virtual scrolling. If omitted, falls back to `SvarProps.virtualScroll` then false. */
72
- virtualScroll?: boolean;
73
- /** Enables SVAR infinite scrolling. If omitted, falls back to `SvarProps.infiniteScroll` then false. */
74
- infiniteScroll?: boolean;
75
- /** Enables SVAR row selection. If omitted, falls back to `SvarProps.select` then true. */
76
- select?: boolean;
77
- /** Enables SVAR multi-row selection. If omitted, falls back to `SvarProps.multiselect` then false. */
78
- multiselect?: boolean;
79
- }
80
-
81
- let {
82
- columns,
83
- data: dataProp,
84
- width,
85
- height,
86
- rowHeight,
87
- headerHeight,
88
- onColumnMoved: _onColumnMoved,
89
- sortOrder,
90
- onSortOrderChange,
91
- filters,
92
- onFilterChange,
93
- searchValue,
94
- onSearchValueChange,
95
- selectedItems,
96
- onSelectedItemsChange,
97
- dataSource,
98
- dataSourceOptions,
99
- onDataChange,
100
- settings,
101
- onCellEvent,
102
- onGridEvent,
103
- onMenuClick,
104
- SvarProps: svarProps = {},
105
- defaultSearchValue = '',
106
- pageSize: pageSizeProp,
107
- virtualScroll: virtualScrollProp,
108
- infiniteScroll: infiniteScrollProp,
109
- select: selectProp,
110
- multiselect: multiselectProp
111
- }: Props = $props();
112
-
113
- function normalizeAdapterType(t: SvarkAdapterType): 'ResolveSpec' | 'RestHeaderSpec' {
114
- return t === 'header' || t === 'RestHeaderSpec' ? 'RestHeaderSpec' : 'ResolveSpec';
115
- }
116
-
117
- const resolvedUrl = $derived(dataSourceOptions?.url ?? svarProps.url ?? svarProps.baseUrl ?? '');
118
- const resolvedToken = $derived(dataSourceOptions?.authToken ?? svarProps.token);
119
- const resolvedSchema = $derived(dataSourceOptions?.schema ?? svarProps.schema ?? '');
120
- const resolvedEntity = $derived(dataSourceOptions?.entity ?? svarProps.entity ?? '');
121
- const resolvedUniqueID = $derived(dataSourceOptions?.uniqueID ?? 'id');
122
-
123
- const resolvedAdapterType = $derived(
124
- normalizeAdapterType(
125
- svarProps.adapterType ??
126
- svarProps.clientType ??
127
- adapterKindFromDataSource(dataSource)
128
- )
129
- );
130
-
131
- const resolvedServerSide = $derived(
132
- svarProps.serverSide ??
133
- (!!dataSource && dataSource !== 'local' && dataSource !== 'custom')
134
- );
135
-
136
- const resolvedResizableColumns = $derived(settings?.resizableColumns ?? true);
137
- const resolvedHideHeader = $derived(settings?.hideHeader ?? false);
138
- const resolvedReadonly = $derived(settings?.readonly ?? false);
139
- const enableColumnFilters = $derived(resolvedServerSide || dataSource === 'local');
140
-
141
- const resolvedHeight = $derived(height ?? svarProps.height ?? '400px');
142
- const resolvedWidth = $derived(width ?? '100%');
143
-
144
- const baseColumns = $derived.by((): IColumnConfig[] => {
145
- if (svarProps.columns?.length) return svarProps.columns;
146
- return mapGenericColumnsToSvarColumns(columns, {
147
- serverSide: resolvedServerSide,
148
- resizableColumns: resolvedResizableColumns,
149
- hideHeader: resolvedHideHeader
150
- });
1
+ <script lang="ts">import SvarkGridView from "./internal/SvarkGridView.svelte";
2
+ import { SvarkResolveSpecAdapter } from "./adapters/SvarkResolveSpecAdapter.js";
3
+ import { SvarkRestHeaderSpecAdapter } from "./adapters/SvarkRestHeaderSpecAdapter.js";
4
+ import { adapterKindFromDataSource, gridColumnFiltersToSvarkFilterState, idsToSelectedItems, mapGenericColumnsToSvarColumns, selectedItemsToIds, sortOrderToSvarkSortState, svarkSortStateToSortMarks, svarkSortStateToSortOrder, toCssSize } from "./utils/svarGridMapping.js";
5
+ import SvarkHeaderFilterCell from "./components/SvarkHeaderFilterCell.svelte";
6
+ import { SVARKGRID_DEFAULT_ITEMS_MARKER_ID } from "./utils/contextMenu.js";
7
+ let { columns, data: dataProp, width, height, rowHeight, headerHeight, onColumnMoved: _onColumnMoved, sortOrder, onSortOrderChange, filters, onFilterChange, searchValue, onSearchValueChange, selectedItems, onSelectedItemsChange, dataSource, dataSourceOptions, onDataChange, settings, onCellEvent, onGridEvent, onMenuClick, SvarProps: svarProps = {}, defaultSearchValue = "", pageSize: pageSizeProp, virtualScroll: virtualScrollProp, infiniteScroll: infiniteScrollProp, select: selectProp, multiselect: multiselectProp } = $props();
8
+ function normalizeAdapterType(t) {
9
+ return t === "header" || t === "RestHeaderSpec" ? "RestHeaderSpec" : "ResolveSpec";
10
+ }
11
+ const resolvedUrl = $derived(dataSourceOptions?.url ?? svarProps.url ?? svarProps.baseUrl ?? "");
12
+ const resolvedToken = $derived(dataSourceOptions?.authToken ?? svarProps.token);
13
+ const resolvedSchema = $derived(dataSourceOptions?.schema ?? svarProps.schema ?? "");
14
+ const resolvedEntity = $derived(dataSourceOptions?.entity ?? svarProps.entity ?? "");
15
+ const resolvedUniqueID = $derived(dataSourceOptions?.uniqueID ?? "id");
16
+ const resolvedAdapterType = $derived(normalizeAdapterType(svarProps.adapterType ?? svarProps.clientType ?? adapterKindFromDataSource(dataSource)));
17
+ const resolvedServerSide = $derived(svarProps.serverSide ?? (!!dataSource && dataSource !== "local" && dataSource !== "custom"));
18
+ const resolvedResizableColumns = $derived(settings?.resizableColumns ?? true);
19
+ const resolvedHideHeader = $derived(settings?.hideHeader ?? false);
20
+ const resolvedReadonly = $derived(settings?.readonly ?? false);
21
+ const enableColumnFilters = $derived(resolvedServerSide || dataSource === "local");
22
+ const resolvedHeight = $derived(height ?? svarProps.height ?? "400px");
23
+ const resolvedWidth = $derived(width ?? "100%");
24
+ const baseColumns = $derived.by(() => {
25
+ if (svarProps.columns?.length) return svarProps.columns;
26
+ return mapGenericColumnsToSvarColumns(columns, {
27
+ serverSide: resolvedServerSide,
28
+ resizableColumns: resolvedResizableColumns,
29
+ hideHeader: resolvedHideHeader
151
30
  });
152
-
153
- const heightStyle = $derived(toCssSize(resolvedHeight, '400px'));
154
- const widthStyle = $derived(toCssSize(resolvedWidth, '100%'));
155
-
156
- const pageSize = $derived(pageSizeProp ?? svarProps.pageSize ?? 100);
157
- const virtualScroll = $derived(virtualScrollProp ?? svarProps.virtualScroll ?? false);
158
- const infiniteScroll = $derived(infiniteScrollProp ?? svarProps.infiniteScroll ?? false);
159
- const select = $derived(selectProp ?? svarProps.select ?? true);
160
- const multiselect = $derived(multiselectProp ?? svarProps.multiselect ?? false);
161
- const query = $derived(svarProps.query ?? ({} as SvarkInitialQuery));
162
- const config = $derived(svarProps.config ?? ({} as Omit<IConfig, 'data'>));
163
-
164
- const topBarLeft = $derived(svarProps.topBarLeft);
165
- const topBarRight = $derived(svarProps.topBarRight);
166
- const onchange = $derived(svarProps.onchange);
167
- const onerror = $derived(svarProps.onerror);
168
- const onselect = $derived(svarProps.onselect);
169
- const onrowclick = $derived(svarProps.onrowclick);
170
- const onrowdblclick = $derived(svarProps.onrowdblclick);
171
- const onrowcontextmenu = $derived(svarProps.onrowcontextmenu);
172
- const userContextMenuProps = $derived(svarProps.contextMenuProps);
173
-
174
- let data = $state<Record<string, unknown>[]>([]);
175
- let localRawData = $state<Record<string, unknown>[]>([]);
176
- let loading = $state(false);
177
- let infiniteLoading = $state(false);
178
- let error = $state<string | null>(null);
179
- let total = $state(0);
180
- let sortState = $state<SvarkSortState[]>([]);
181
- let gridFilterState = $state<SvarkFilterState>({});
182
- let searchFilterState = $state<SvarkFilterState>({});
183
- let page = $state(0);
184
- let reload = $state(0);
185
- let lastServerRequestKey = $state<string>('');
186
- let activeServerRequestId = 0;
187
- let activeServerRequestKey = '';
188
-
189
- let gridApi = $state<IApi | null>(null);
190
- let searchOpen = $state(false);
191
- let searchValueState = $state(defaultSearchValue);
192
-
193
- const effectiveFilterState = $derived({ ...gridFilterState, ...searchFilterState });
194
-
195
- // Paging is active when serverSide with no scroll variant
196
- const showPager = $derived(resolvedServerSide && !infiniteScroll && !virtualScroll);
197
- const totalPages = $derived(pageSize > 0 ? Math.ceil(total / pageSize) : 1);
198
- const pageStart = $derived(total === 0 ? 0 : page * pageSize + 1);
199
- const pageEnd = $derived(Math.min((page + 1) * pageSize, total));
200
-
201
- // ── Generic controlled/uncontrolled bridges ───────────────────────────────
202
-
203
- let internalSortOrder = $state<GridColumnSortOrder>({});
204
- $effect(() => {
205
- if (sortOrder === undefined) return;
206
- internalSortOrder = sortOrder;
207
- });
208
- const resolvedSortOrder = $derived(sortOrder ?? internalSortOrder);
209
-
210
- let internalFilters = $state<GridColumnFilters>({});
211
- $effect(() => {
212
- if (filters === undefined) return;
213
- internalFilters = filters;
214
- });
215
- const resolvedFilters = $derived(filters ?? internalFilters);
216
-
217
- const instanceId = $state(
218
- `svarkgrid-${globalThis.crypto?.randomUUID?.() ?? Math.random().toString(36).slice(2)}`
219
- );
220
-
221
- let internalSelectedItems = $state<Record<string, unknown>[]>([]);
222
- $effect(() => {
223
- if (selectedItems === undefined) return;
224
- internalSelectedItems = selectedItems;
225
- });
226
- const resolvedSelectedItems = $derived(selectedItems ?? internalSelectedItems);
227
- const resolvedSelectedIds = $derived(selectedItemsToIds(resolvedSelectedItems, resolvedUniqueID));
228
-
229
- function emitSortOrder(next: GridColumnSortOrder) {
230
- if (sortOrder === undefined) internalSortOrder = next;
231
- onSortOrderChange?.(next);
232
- }
233
-
234
- function emitFilters(next: GridColumnFilters) {
235
- if (filters === undefined) internalFilters = next;
236
- onFilterChange?.(next);
237
- }
238
-
239
- function emitGridEvent(
240
- type: GridEventType,
241
- item?: Record<string, unknown>,
242
- column?: GridColumn<Record<string, unknown>, unknown>,
243
- coords?: { x: number; y: number; code?: string },
244
- detail?: Record<string, unknown>
245
- ) {
246
- onGridEvent?.(type, item, column, coords, detail);
247
- }
248
-
249
- const genericColumnsById = $derived.by(() => {
250
- const map: Record<string, GridColumn<Record<string, unknown>, unknown>> = {};
251
- for (const col of columns ?? []) map[String(col.id)] = col;
252
- return map;
31
+ });
32
+ const heightStyle = $derived(toCssSize(resolvedHeight, "400px"));
33
+ const widthStyle = $derived(toCssSize(resolvedWidth, "100%"));
34
+ const pageSize = $derived(pageSizeProp ?? svarProps.pageSize ?? 100);
35
+ const virtualScroll = $derived(virtualScrollProp ?? svarProps.virtualScroll ?? false);
36
+ const infiniteScroll = $derived(infiniteScrollProp ?? svarProps.infiniteScroll ?? false);
37
+ const select = $derived(selectProp ?? svarProps.select ?? true);
38
+ const multiselect = $derived(multiselectProp ?? svarProps.multiselect ?? false);
39
+ const query = $derived(svarProps.query ?? {});
40
+ const config = $derived(svarProps.config ?? {});
41
+ const topBarLeft = $derived(svarProps.topBarLeft);
42
+ const topBarRight = $derived(svarProps.topBarRight);
43
+ const onchange = $derived(svarProps.onchange);
44
+ const onerror = $derived(svarProps.onerror);
45
+ const onselect = $derived(svarProps.onselect);
46
+ const onrowclick = $derived(svarProps.onrowclick);
47
+ const onrowdblclick = $derived(svarProps.onrowdblclick);
48
+ const onrowcontextmenu = $derived(svarProps.onrowcontextmenu);
49
+ const userContextMenuProps = $derived(svarProps.contextMenuProps);
50
+ let data = $state([]);
51
+ let localRawData = $state([]);
52
+ let loading = $state(false);
53
+ let infiniteLoading = $state(false);
54
+ let error = $state(null);
55
+ let total = $state(0);
56
+ let sortState = $state([]);
57
+ let gridFilterState = $state({});
58
+ let searchFilterState = $state({});
59
+ let page = $state(0);
60
+ let reload = $state(0);
61
+ let lastServerRequestKey = $state("");
62
+ let activeServerRequestId = 0;
63
+ let activeServerRequestKey = "";
64
+ let gridApi = $state(null);
65
+ let searchOpen = $state(false);
66
+ // eslint-disable-next-line svelte/no-unused-svelte-ignore
67
+ // svelte-ignore state_referenced_locally -- intentional initial capture; controlled changes handled by $effect
68
+ let searchValueState = $state(defaultSearchValue);
69
+ const effectiveFilterState = $derived({
70
+ ...gridFilterState,
71
+ ...searchFilterState
72
+ });
73
+ // Paging is active when serverSide with no scroll variant
74
+ const showPager = $derived(resolvedServerSide && !infiniteScroll && !virtualScroll);
75
+ const totalPages = $derived(pageSize > 0 ? Math.ceil(total / pageSize) : 1);
76
+ const pageStart = $derived(total === 0 ? 0 : page * pageSize + 1);
77
+ const pageEnd = $derived(Math.min((page + 1) * pageSize, total));
78
+ // ── Generic controlled/uncontrolled bridges ───────────────────────────────
79
+ let internalSortOrder = $state({});
80
+ $effect(() => {
81
+ if (sortOrder === undefined) return;
82
+ internalSortOrder = sortOrder;
83
+ });
84
+ const resolvedSortOrder = $derived(sortOrder ?? internalSortOrder);
85
+ let internalFilters = $state({});
86
+ $effect(() => {
87
+ if (filters === undefined) return;
88
+ internalFilters = filters;
89
+ });
90
+ const resolvedFilters = $derived(filters ?? internalFilters);
91
+ const instanceId = $state(`svarkgrid-${globalThis.crypto?.randomUUID?.() ?? Math.random().toString(36).slice(2)}`);
92
+ let internalSelectedItems = $state([]);
93
+ $effect(() => {
94
+ if (selectedItems === undefined) return;
95
+ internalSelectedItems = selectedItems;
96
+ });
97
+ const resolvedSelectedItems = $derived(selectedItems ?? internalSelectedItems);
98
+ const resolvedSelectedIds = $derived(selectedItemsToIds(resolvedSelectedItems, resolvedUniqueID));
99
+ function emitSortOrder(next) {
100
+ if (sortOrder === undefined) internalSortOrder = next;
101
+ onSortOrderChange?.(next);
102
+ }
103
+ function emitFilters(next) {
104
+ if (filters === undefined) internalFilters = next;
105
+ onFilterChange?.(next);
106
+ }
107
+ function emitGridEvent(type, item, column, coords, detail) {
108
+ onGridEvent?.(type, item, column, coords, detail);
109
+ }
110
+ const genericColumnsById = $derived.by(() => {
111
+ const map = {};
112
+ for (const col of columns ?? []) map[String(col.id)] = col;
113
+ return map;
114
+ });
115
+ function setColumnFilter(columnId, next) {
116
+ const col = genericColumnsById[columnId];
117
+ const dataKey = col?.dataKey;
118
+ const merged = {
119
+ ...resolvedFilters[columnId] ?? {},
120
+ ...next,
121
+ ...dataKey ? { dataKey } : {}
122
+ };
123
+ emitFilters({
124
+ ...resolvedFilters,
125
+ [columnId]: merged
253
126
  });
254
-
255
- function setColumnFilter(columnId: string, next: GridColumnFilters[string]) {
256
- const col = genericColumnsById[columnId];
257
- const dataKey = col?.dataKey;
258
- const merged: GridColumnFilters[string] = {
259
- ...(resolvedFilters[columnId] ?? {}),
260
- ...next,
261
- ...(dataKey ? { dataKey } : {})
262
- };
263
- emitFilters({ ...resolvedFilters, [columnId]: merged });
264
- }
265
-
266
- function clearColumnFilter(columnId: string) {
267
- const { [columnId]: _ignored, ...rest } = resolvedFilters;
268
- emitFilters(rest);
269
- }
270
-
271
- function setColumnSort(columnId: string, next: 'none' | 'asc' | 'desc') {
272
- // Prefer using the grid API so local mode actually sorts rows and
273
- // server-side mode triggers the existing intercept logic.
274
- if (gridApi) {
275
- if (next === 'none') {
276
- gridApi.exec('sort-rows', {} as any);
277
- } else {
278
- gridApi.exec('sort-rows', { key: columnId, add: false, order: next });
279
- }
280
- return;
127
+ }
128
+ function clearColumnFilter(columnId) {
129
+ const { [columnId]: _ignored, ...rest } = resolvedFilters;
130
+ emitFilters(rest);
131
+ }
132
+ function setColumnSort(columnId, next) {
133
+ // Prefer using the grid API so local mode actually sorts rows and
134
+ // server-side mode triggers the existing intercept logic.
135
+ if (gridApi) {
136
+ if (next === "none") {
137
+ gridApi.exec("sort-rows", {});
138
+ } else {
139
+ gridApi.exec("sort-rows", {
140
+ key: columnId,
141
+ add: false,
142
+ order: next
143
+ });
281
144
  }
282
-
283
- const merged: GridColumnSortOrder = { ...(resolvedSortOrder ?? {}) };
284
- merged[columnId] = next;
285
- emitSortOrder(merged);
286
- }
287
-
288
- function isGenericColumnFilterDisabled(columnId: string): boolean {
289
- const col = genericColumnsById[columnId];
290
- return col?.disableFilter === true;
145
+ return;
146
+ }
147
+ const merged = { ...resolvedSortOrder ?? {} };
148
+ merged[columnId] = next;
149
+ emitSortOrder(merged);
150
+ }
151
+ function isGenericColumnFilterDisabled(columnId) {
152
+ const col = genericColumnsById[columnId];
153
+ return col?.disableFilter === true;
154
+ }
155
+ function enhanceColumnHeader(col) {
156
+ const id = col?.id;
157
+ const columnId = id == null ? "" : String(id);
158
+ const queryText = searchValueState.trim();
159
+ // Enable column sorting by default (SVAR Grid expects `sort` opt-out).
160
+ const sort = col.sort ?? true;
161
+ if (resolvedHideHeader) return {
162
+ ...col,
163
+ sort,
164
+ header: undefined
165
+ };
166
+ const rawHeader = col.header;
167
+ // Avoid messing with complex multi-row headers.
168
+ if (Array.isArray(rawHeader)) return {
169
+ ...col,
170
+ sort
171
+ };
172
+ const headerCfg = typeof rawHeader === "string" ? { text: rawHeader } : rawHeader && typeof rawHeader === "object" ? { ...rawHeader } : { text: columnId };
173
+ const isSearchTarget = !!queryText && !!columnId && !col.hidden && (resolvedServerSide ? col.searchable === true : col.searchable !== false);
174
+ if (!headerCfg.cell) {
175
+ const filterable = enableColumnFilters && !!columnId && !col.hidden && !isGenericColumnFilterDisabled(columnId);
176
+ headerCfg.cell = SvarkHeaderFilterCell;
177
+ headerCfg.__svarkMenuId = `${instanceId}:filter:${columnId}`;
178
+ headerCfg.__svarkFilterable = filterable;
179
+ headerCfg.__svarkColumnId = columnId;
180
+ headerCfg.__svarkColumnLabel = String(headerCfg.text ?? columnId);
181
+ headerCfg.__svarkSearchHighlight = isSearchTarget;
182
+ headerCfg.__svarkCurrentSort = resolvedSortOrder?.[columnId] ?? "none";
183
+ headerCfg.__svarkSetSort = (next) => setColumnSort(columnId, next);
184
+ headerCfg.__svarkCurrentFilter = resolvedFilters[columnId];
185
+ headerCfg.__svarkSetFilter = (next) => setColumnFilter(columnId, next);
186
+ headerCfg.__svarkClearFilter = () => clearColumnFilter(columnId);
187
+ }
188
+ return {
189
+ ...col,
190
+ sort,
191
+ header: headerCfg
192
+ };
193
+ }
194
+ const gridColumns = $derived.by(() => (baseColumns ?? []).map((col) => enhanceColumnHeader(col)));
195
+ function emitSearchValue(next) {
196
+ if (searchValue === undefined) searchValueState = next;
197
+ onSearchValueChange?.(next);
198
+ }
199
+ function emitSelectedItems(next) {
200
+ if (selectedItems === undefined) internalSelectedItems = next;
201
+ onSelectedItemsChange?.(next);
202
+ }
203
+ $effect(() => {
204
+ sortState = sortOrderToSvarkSortState(resolvedSortOrder);
205
+ });
206
+ $effect(() => {
207
+ gridFilterState = gridColumnFiltersToSvarkFilterState(resolvedFilters, { prefix: "" });
208
+ });
209
+ $effect(() => {
210
+ if (searchValue === undefined) return;
211
+ searchValueState = searchValue;
212
+ });
213
+ // ── Local data mode (dataSource: 'local' | 'custom') ──────────────────────
214
+ $effect(() => {
215
+ if (resolvedServerSide) return;
216
+ void reload;
217
+ error = null;
218
+ loading = false;
219
+ if (!dataProp) {
220
+ localRawData = [];
221
+ data = [];
222
+ total = 0;
223
+ return;
224
+ }
225
+ let cancelled = false;
226
+ if (typeof dataProp === "function") {
227
+ loading = true;
228
+ dataProp().then((result) => {
229
+ if (cancelled) return;
230
+ localRawData = result ?? [];
231
+ }).catch((e) => {
232
+ if (cancelled) return;
233
+ const msg = e instanceof Error ? e.message : "Failed to load data";
234
+ error = msg;
235
+ onerror?.(msg);
236
+ }).finally(() => {
237
+ if (!cancelled) loading = false;
238
+ });
239
+ } else {
240
+ localRawData = dataProp ?? [];
291
241
  }
292
-
293
- function enhanceColumnHeader(col: IColumnConfig): IColumnConfig {
294
- const id = (col as any)?.id;
295
- const columnId = id == null ? '' : String(id);
296
- const queryText = searchValueState.trim();
297
-
298
- // Enable column sorting by default (SVAR Grid expects `sort` opt-out).
299
- const sort = (col as any).sort ?? true;
300
-
301
- if (resolvedHideHeader) return { ...col, sort, header: undefined };
302
-
303
- const rawHeader = (col as any).header;
304
- // Avoid messing with complex multi-row headers.
305
- if (Array.isArray(rawHeader)) return { ...col, sort };
306
-
307
- const headerCfg: any =
308
- typeof rawHeader === 'string'
309
- ? { text: rawHeader }
310
- : rawHeader && typeof rawHeader === 'object'
311
- ? { ...rawHeader }
312
- : { text: columnId };
313
-
314
- const isSearchTarget =
315
- !!queryText &&
316
- !!columnId &&
317
- !(col as any).hidden &&
318
- (resolvedServerSide ? (col as any).searchable === true : (col as any).searchable !== false);
319
-
320
- if (!headerCfg.cell) {
321
- const filterable =
322
- enableColumnFilters &&
323
- !!columnId &&
324
- !(col as any).hidden &&
325
- !isGenericColumnFilterDisabled(columnId);
326
-
327
- headerCfg.cell = SvarkHeaderFilterCell;
328
- headerCfg.__svarkMenuId = `${instanceId}:filter:${columnId}`;
329
- headerCfg.__svarkFilterable = filterable;
330
- headerCfg.__svarkColumnId = columnId;
331
- headerCfg.__svarkColumnLabel = String(headerCfg.text ?? columnId);
332
- headerCfg.__svarkSearchHighlight = isSearchTarget;
333
- headerCfg.__svarkCurrentSort = (resolvedSortOrder?.[columnId] ?? 'none') as any;
334
- headerCfg.__svarkSetSort = (next: 'none' | 'asc' | 'desc') => setColumnSort(columnId, next);
335
- headerCfg.__svarkCurrentFilter = resolvedFilters[columnId];
336
- headerCfg.__svarkSetFilter = (next: GridColumnFilters[string]) => setColumnFilter(columnId, next);
337
- headerCfg.__svarkClearFilter = () => clearColumnFilter(columnId);
242
+ return () => {
243
+ cancelled = true;
244
+ };
245
+ });
246
+ function valueMatchesLocal(cellValue, filter) {
247
+ const operator = String(filter.operator ?? "eq").toLowerCase();
248
+ const rawValue = filter.value;
249
+ const actual = cellValue ?? "";
250
+ if (operator === "eq") return String(actual) === String(rawValue ?? "");
251
+ if (operator === "neq") return String(actual) !== String(rawValue ?? "");
252
+ if (operator === "gt" || operator === "gte" || operator === "lt" || operator === "lte") {
253
+ const left = Number(actual);
254
+ const right = Number(rawValue);
255
+ if (Number.isNaN(left) || Number.isNaN(right)) return false;
256
+ if (operator === "gt") return left > right;
257
+ if (operator === "gte") return left >= right;
258
+ if (operator === "lt") return left < right;
259
+ return left <= right;
260
+ }
261
+ const haystack = String(actual).toLowerCase();
262
+ let needle = String(rawValue ?? "").toLowerCase();
263
+ if (operator === "like" || operator === "ilike" || operator === "contains") {
264
+ const startsWith = needle.startsWith("%");
265
+ const endsWith = needle.endsWith("%");
266
+ const core = needle.replace(/^%|%$/g, "");
267
+ // If the UI provided a bare string, treat it like "%value%".
268
+ if (!startsWith && !endsWith) return haystack.includes(needle);
269
+ if (startsWith && endsWith) return haystack.includes(core);
270
+ if (startsWith) return haystack.endsWith(core);
271
+ if (endsWith) return haystack.startsWith(core);
272
+ return haystack.includes(core);
273
+ }
274
+ if (operator === "startswith") return haystack.startsWith(needle);
275
+ if (operator === "endswith") return haystack.endsWith(needle);
276
+ return haystack.includes(needle);
277
+ }
278
+ function applyLocalFiltersAndSearch(rows) {
279
+ let result = rows.slice();
280
+ const entries = Object.values(effectiveFilterState);
281
+ const andFilters = entries.filter((f) => String(f.logic_operator ?? "AND").toUpperCase() !== "OR");
282
+ const orFilters = entries.filter((f) => String(f.logic_operator ?? "AND").toUpperCase() === "OR");
283
+ for (const f of andFilters) {
284
+ const col = String(f.column ?? "");
285
+ if (!col) continue;
286
+ result = result.filter((row) => valueMatchesLocal(row[col], f));
287
+ }
288
+ if (orFilters.length > 0) {
289
+ result = result.filter((row) => orFilters.some((f) => {
290
+ const col = String(f.column ?? "");
291
+ if (!col) return false;
292
+ return valueMatchesLocal(row[col], f);
293
+ }));
294
+ }
295
+ const q = searchValueState.trim().toLowerCase();
296
+ if (q) {
297
+ const searchableCols = gridColumns.filter((c) => c?.id != null && !c.hidden && c.searchable !== false).map((c) => String(c.id));
298
+ if (searchableCols.length > 0) {
299
+ result = result.filter((row) => searchableCols.some((id) => String(row[id] ?? "").toLowerCase().includes(q)));
338
300
  }
339
-
340
- return { ...col, sort, header: headerCfg };
341
301
  }
342
-
343
- const gridColumns = $derived.by(() => (baseColumns ?? []).map((col) => enhanceColumnHeader(col)));
344
-
345
- function emitSearchValue(next: string) {
346
- if (searchValue === undefined) searchValueState = next;
347
- onSearchValueChange?.(next);
348
- }
349
-
350
- function emitSelectedItems(next: Record<string, unknown>[]) {
351
- if (selectedItems === undefined) internalSelectedItems = next;
352
- onSelectedItemsChange?.(next);
302
+ return result;
303
+ }
304
+ $effect(() => {
305
+ if (resolvedServerSide) return;
306
+ const next = applyLocalFiltersAndSearch(localRawData);
307
+ data = next;
308
+ total = next.length;
309
+ });
310
+ const sortMarks = $derived.by(() => svarkSortStateToSortMarks(sortState));
311
+ const gridConfig = $derived.by(() => {
312
+ const base = config ?? {};
313
+ const sizes = {
314
+ ...base.sizes,
315
+ ...rowHeight != null ? { rowHeight } : {},
316
+ ...headerHeight != null ? { headerHeight } : {}
317
+ };
318
+ const next = {
319
+ ...base,
320
+ sizes,
321
+ // Keep SVAR's sort indicators in sync with generic sortOrder.
322
+ ...resolvedServerSide ? { sortMarks } : {},
323
+ ...selectedItems !== undefined ? { selectedRows: resolvedSelectedIds } : {}
324
+ };
325
+ return next;
326
+ });
327
+ // Debounced mirrors of sortState/filterState used by the main fetch effect.
328
+ // When serverSide is true, changes are held for 300 ms before committing so
329
+ // rapid keystrokes / click-sort chains don't fire a request per event.
330
+ // Page resets to 0 atomically with the debounced update so only one fetch fires.
331
+ let debouncedSort = $state([]);
332
+ let debouncedFilter = $state({});
333
+ let _debounceTimer = null;
334
+ function isSameSortState(a, b) {
335
+ if (a.length !== b.length) return false;
336
+ return a.every((item, index) => item.key === b[index]?.key && item.order === b[index]?.order);
337
+ }
338
+ function isSameFilterState(a, b) {
339
+ const aKeys = Object.keys(a);
340
+ const bKeys = Object.keys(b);
341
+ if (aKeys.length !== bKeys.length) return false;
342
+ for (const key of aKeys) {
343
+ const left = a[key];
344
+ const right = b[key];
345
+ if (!right || left?.column !== right.column || left?.operator !== right.operator || left?.logic_operator !== right.logic_operator || left?.value !== right.value) {
346
+ return false;
347
+ }
353
348
  }
354
-
355
- $effect(() => {
356
- sortState = sortOrderToSvarkSortState(resolvedSortOrder);
357
- });
358
-
359
- $effect(() => {
360
- gridFilterState = gridColumnFiltersToSvarkFilterState(resolvedFilters, { prefix: '' });
361
- });
362
-
363
- $effect(() => {
364
- if (searchValue === undefined) return;
365
- searchValueState = searchValue;
366
- });
367
-
368
- // ── Local data mode (dataSource: 'local' | 'custom') ──────────────────────
369
-
370
- $effect(() => {
371
- if (resolvedServerSide) return;
372
-
373
- void reload; // allow Refresh to re-run local loaders
374
-
375
- error = null;
376
- loading = false;
377
-
378
- if (!dataProp) {
379
- localRawData = [];
380
- data = [];
381
- total = 0;
349
+ return true;
350
+ }
351
+ $effect(() => {
352
+ const s = sortState;
353
+ const f = effectiveFilterState;
354
+ if (!resolvedServerSide) {
355
+ debouncedSort = s;
356
+ debouncedFilter = f;
357
+ return;
358
+ }
359
+ if (_debounceTimer !== null) clearTimeout(_debounceTimer);
360
+ _debounceTimer = setTimeout(() => {
361
+ _debounceTimer = null;
362
+ if (page === 0 && isSameSortState(debouncedSort, s) && isSameFilterState(debouncedFilter, f)) {
382
363
  return;
383
364
  }
384
-
385
- let cancelled = false;
386
-
387
- if (typeof dataProp === 'function') {
388
- loading = true;
389
- dataProp()
390
- .then((result) => {
391
- if (cancelled) return;
392
- localRawData = (result ?? []) as any;
393
- })
394
- .catch((e) => {
395
- if (cancelled) return;
396
- const msg = e instanceof Error ? e.message : 'Failed to load data';
397
- error = msg;
398
- onerror?.(msg);
399
- })
400
- .finally(() => {
401
- if (!cancelled) loading = false;
402
- });
403
- } else {
404
- localRawData = (dataProp ?? []) as any;
365
+ page = 0;
366
+ debouncedSort = s;
367
+ debouncedFilter = f;
368
+ }, 300);
369
+ return () => {
370
+ if (_debounceTimer !== null) {
371
+ clearTimeout(_debounceTimer);
372
+ _debounceTimer = null;
405
373
  }
406
-
407
- return () => {
408
- cancelled = true;
374
+ };
375
+ });
376
+ function getLocalSearchColumns() {
377
+ const columnsMap = {};
378
+ for (const col of gridColumns) {
379
+ const id = col.id;
380
+ if (id == null) continue;
381
+ if (col.hidden) continue;
382
+ if (col.searchable === false) continue;
383
+ columnsMap[String(id)] = true;
384
+ }
385
+ return Object.keys(columnsMap).length > 0 ? columnsMap : undefined;
386
+ }
387
+ function applyServerSearchFilters(value) {
388
+ if (!resolvedServerSide) return;
389
+ if (!value) {
390
+ // Avoid triggering a refetch loop by reassigning `{}` repeatedly.
391
+ if (Object.keys(searchFilterState).length === 0) return;
392
+ searchFilterState = {};
393
+ return;
394
+ }
395
+ const searchableIds = [];
396
+ for (const col of gridColumns) {
397
+ const id = col.id;
398
+ if (id == null) continue;
399
+ if (col.hidden) continue;
400
+ if (col.searchable === true) searchableIds.push(String(id));
401
+ }
402
+ if (searchableIds.length === 0) {
403
+ if (Object.keys(searchFilterState).length === 0) return;
404
+ searchFilterState = {};
405
+ return;
406
+ }
407
+ const next = {};
408
+ for (const id of searchableIds) {
409
+ // Avoid clobbering an existing column filter the user set elsewhere.
410
+ const hasExistingFilter = Object.entries({ ...gridFilterState }).some(([key, filter]) => {
411
+ const target = filter.column ?? key;
412
+ return target === id && filter.value != null && filter.value !== "";
413
+ });
414
+ if (hasExistingFilter) continue;
415
+ next[`search:${id}`] = {
416
+ column: id,
417
+ value,
418
+ operator: "ilike",
419
+ logic_operator: "OR"
409
420
  };
410
- });
411
-
412
- function valueMatchesLocal(cellValue: unknown, filter: { operator?: string; value?: unknown }) {
413
- const operator = String(filter.operator ?? 'eq').toLowerCase();
414
- const rawValue = filter.value;
415
- const actual = cellValue ?? '';
416
-
417
- if (operator === 'eq') return String(actual) === String(rawValue ?? '');
418
- if (operator === 'neq') return String(actual) !== String(rawValue ?? '');
419
-
420
- if (operator === 'gt' || operator === 'gte' || operator === 'lt' || operator === 'lte') {
421
- const left = Number(actual);
422
- const right = Number(rawValue);
423
- if (Number.isNaN(left) || Number.isNaN(right)) return false;
424
- if (operator === 'gt') return left > right;
425
- if (operator === 'gte') return left >= right;
426
- if (operator === 'lt') return left < right;
427
- return left <= right;
428
- }
429
-
430
- const haystack = String(actual).toLowerCase();
431
- let needle = String(rawValue ?? '').toLowerCase();
432
-
433
- if (operator === 'like' || operator === 'ilike' || operator === 'contains') {
434
- const startsWith = needle.startsWith('%');
435
- const endsWith = needle.endsWith('%');
436
- const core = needle.replace(/^%|%$/g, '');
437
- // If the UI provided a bare string, treat it like "%value%".
438
- if (!startsWith && !endsWith) return haystack.includes(needle);
439
- if (startsWith && endsWith) return haystack.includes(core);
440
- if (startsWith) return haystack.endsWith(core);
441
- if (endsWith) return haystack.startsWith(core);
442
- return haystack.includes(core);
443
- }
444
-
445
- if (operator === 'startswith') return haystack.startsWith(needle);
446
- if (operator === 'endswith') return haystack.endsWith(needle);
447
-
448
- return haystack.includes(needle);
449
421
  }
450
-
451
- function applyLocalFiltersAndSearch(rows: Record<string, unknown>[]) {
452
- let result = rows.slice();
453
-
454
- const entries = Object.values(effectiveFilterState);
455
- const andFilters = entries.filter((f) => String(f.logic_operator ?? 'AND').toUpperCase() !== 'OR');
456
- const orFilters = entries.filter((f) => String(f.logic_operator ?? 'AND').toUpperCase() === 'OR');
457
-
458
- for (const f of andFilters) {
459
- const col = String(f.column ?? '');
460
- if (!col) continue;
461
- result = result.filter((row) => valueMatchesLocal((row as any)[col], f));
462
- }
463
-
464
- if (orFilters.length > 0) {
465
- result = result.filter((row) =>
466
- orFilters.some((f) => {
467
- const col = String(f.column ?? '');
468
- if (!col) return false;
469
- return valueMatchesLocal((row as any)[col], f);
470
- })
471
- );
472
- }
473
-
474
- const q = searchValueState.trim().toLowerCase();
475
- if (q) {
476
- const searchableCols = gridColumns
477
- .filter((c: any) => c?.id != null && !c.hidden && c.searchable !== false)
478
- .map((c: any) => String(c.id));
479
-
480
- if (searchableCols.length > 0) {
481
- result = result.filter((row) =>
482
- searchableCols.some((id) => String((row as any)[id] ?? '').toLowerCase().includes(q))
483
- );
422
+ // Idempotency guard: avoid reassigning equivalent filter state.
423
+ const prev = searchFilterState;
424
+ const prevKeys = Object.keys(prev);
425
+ const nextKeys = Object.keys(next);
426
+ if (prevKeys.length === nextKeys.length) {
427
+ let same = true;
428
+ for (const k of nextKeys) {
429
+ const a = prev[k];
430
+ const b = next[k];
431
+ if (!a || !b) {
432
+ same = false;
433
+ break;
434
+ }
435
+ if (String(a.column ?? "") !== String(b.column ?? "") || String(a.operator ?? "") !== String(b.operator ?? "") || String(a.logic_operator ?? "") !== String(b.logic_operator ?? "") || String(a.value ?? "") !== String(b.value ?? "")) {
436
+ same = false;
437
+ break;
484
438
  }
485
439
  }
486
-
487
- return result;
488
- }
489
-
490
- $effect(() => {
491
- if (resolvedServerSide) return;
492
- const next = applyLocalFiltersAndSearch(localRawData);
493
- data = next;
494
- total = next.length;
440
+ if (same) return;
441
+ }
442
+ searchFilterState = next;
443
+ }
444
+ function setSearch(next) {
445
+ emitSearchValue(next);
446
+ gridApi?.exec("search-rows", {
447
+ search: next,
448
+ columns: getLocalSearchColumns()
495
449
  });
496
-
497
- const sortMarks = $derived.by(() => svarkSortStateToSortMarks(sortState));
498
-
499
- const gridConfig = $derived.by(() => {
500
- const base = config ?? {};
501
- const sizes = {
502
- ...(base as any).sizes,
503
- ...(rowHeight != null ? { rowHeight } : {}),
504
- ...(headerHeight != null ? { headerHeight } : {})
505
- };
506
-
507
- const next: Omit<IConfig, 'data'> & {
508
- selectedRows?: Array<string | number>;
509
- } = {
510
- ...base,
511
- sizes,
512
- // Keep SVAR's sort indicators in sync with generic sortOrder.
513
- ...(resolvedServerSide ? { sortMarks } : {}),
514
- ...(selectedItems !== undefined ? { selectedRows: resolvedSelectedIds } : {})
450
+ applyServerSearchFilters(next.trim());
451
+ }
452
+ function refreshGrid() {
453
+ reload++;
454
+ }
455
+ function clearAllSort() {
456
+ // Clear in the UI/store first (local mode), then clear generic sort state.
457
+ gridApi?.exec("sort-rows", {});
458
+ emitSortOrder({});
459
+ }
460
+ function clearAllFilters() {
461
+ emitFilters({});
462
+ }
463
+ async function applyLocalDataChange(next) {
464
+ localRawData = next;
465
+ await onDataChange?.(next);
466
+ }
467
+ function resolveGenericColumn(columnId) {
468
+ if (!columnId) return undefined;
469
+ return genericColumnsById[columnId];
470
+ }
471
+ function extractRowsFromResponse(response) {
472
+ if (Array.isArray(response)) {
473
+ return response;
474
+ }
475
+ if (response && typeof response === "object" && Array.isArray(response.data)) {
476
+ return response.data;
477
+ }
478
+ return [];
479
+ }
480
+ function normalizeGridRows(rows, uniqueID) {
481
+ return rows.map((row) => {
482
+ const rowId = row?.id;
483
+ const uniqueValue = row?.[uniqueID];
484
+ if (rowId != null || uniqueValue == null) return row;
485
+ return {
486
+ ...row,
487
+ id: uniqueValue
515
488
  };
516
-
517
- return next;
518
489
  });
519
-
520
- // Debounced mirrors of sortState/filterState used by the main fetch effect.
521
- // When serverSide is true, changes are held for 300 ms before committing so
522
- // rapid keystrokes / click-sort chains don't fire a request per event.
523
- // Page resets to 0 atomically with the debounced update so only one fetch fires.
524
- let debouncedSort = $state<SvarkSortState[]>([]);
525
- let debouncedFilter = $state<SvarkFilterState>({});
526
- let _debounceTimer: ReturnType<typeof setTimeout> | null = null;
527
-
528
- function isSameSortState(a: SvarkSortState[], b: SvarkSortState[]): boolean {
529
- if (a.length !== b.length) return false;
530
- return a.every((item, index) => item.key === b[index]?.key && item.order === b[index]?.order);
531
- }
532
-
533
- function isSameFilterState(a: SvarkFilterState, b: SvarkFilterState): boolean {
534
- const aKeys = Object.keys(a);
535
- const bKeys = Object.keys(b);
536
- if (aKeys.length !== bKeys.length) return false;
537
-
538
- for (const key of aKeys) {
539
- const left = a[key];
540
- const right = b[key];
541
- if (
542
- !right ||
543
- left?.column !== right.column ||
544
- left?.operator !== right.operator ||
545
- left?.logic_operator !== right.logic_operator ||
546
- left?.value !== right.value
547
- ) {
548
- return false;
490
+ }
491
+ function extractTotalFromResponse(response, rows) {
492
+ if (response && typeof response === "object" && typeof response.metadata?.total === "number") {
493
+ return response.metadata.total;
494
+ }
495
+ return rows.length;
496
+ }
497
+ $effect(() => {
498
+ // Keep SVAR's local highlight + server-side search filters in sync with controlled searchValue.
499
+ const next = searchValueState;
500
+ gridApi?.exec("search-rows", {
501
+ search: next,
502
+ columns: getLocalSearchColumns()
503
+ });
504
+ applyServerSearchFilters(next.trim());
505
+ });
506
+ function openSearch() {
507
+ searchOpen = true;
508
+ }
509
+ function closeSearch() {
510
+ searchOpen = false;
511
+ }
512
+ let selectedIdsInternal = $state([]);
513
+ $effect(() => {
514
+ if (selectedItems === undefined) return;
515
+ selectedIdsInternal = resolvedSelectedIds;
516
+ });
517
+ function handleSelectedIdsChange(ids) {
518
+ selectedIdsInternal = ids;
519
+ emitSelectedItems(idsToSelectedItems(ids, data, resolvedUniqueID));
520
+ }
521
+ const contextMenuProps = $derived.by(() => {
522
+ const hasSort = Object.values(resolvedSortOrder ?? {}).some((dir) => dir === "asc" || dir === "desc");
523
+ const hasFilters = Object.keys(resolvedFilters ?? {}).length > 0;
524
+ const hasSearch = searchValueState.trim().length > 0;
525
+ return {
526
+ items: (ctx) => {
527
+ const defaults = [
528
+ {
529
+ id: "refresh",
530
+ label: "Refresh",
531
+ onselect: () => refreshGrid()
532
+ },
533
+ {
534
+ id: "clear-sort",
535
+ label: "Clear Sort",
536
+ disabled: !hasSort,
537
+ onselect: () => clearAllSort()
538
+ },
539
+ {
540
+ id: "clear-filters",
541
+ label: "Clear Filters",
542
+ disabled: !hasFilters,
543
+ onselect: () => clearAllFilters()
544
+ },
545
+ {
546
+ id: "clear-search",
547
+ label: "Clear Search",
548
+ disabled: !hasSearch,
549
+ onselect: () => setSearch("")
550
+ }
551
+ ];
552
+ const userItemsRaw = typeof userContextMenuProps?.items === "function" ? userContextMenuProps.items(ctx) : userContextMenuProps?.items;
553
+ const userItems = Array.isArray(userItemsRaw) ? userItemsRaw : [];
554
+ if (userItems.length === 0) return defaults;
555
+ const hasMarker = userItems.some((it) => it?.id === SVARKGRID_DEFAULT_ITEMS_MARKER_ID);
556
+ if (hasMarker) {
557
+ const merged = [];
558
+ for (const it of userItems) {
559
+ if (it?.id === SVARKGRID_DEFAULT_ITEMS_MARKER_ID) merged.push(...defaults);
560
+ else merged.push(it);
561
+ }
562
+ return merged;
549
563
  }
564
+ return [
565
+ ...defaults,
566
+ {
567
+ id: "__sep__",
568
+ kind: "separator",
569
+ label: ""
570
+ },
571
+ ...userItems
572
+ ];
573
+ },
574
+ onselectitem: userContextMenuProps?.onselectitem
575
+ };
576
+ });
577
+ const isServerConfigured = $derived(!!(resolvedUrl && resolvedSchema && resolvedEntity));
578
+ const adapter = $derived.by(() => {
579
+ if (!isServerConfigured) return null;
580
+ return resolvedAdapterType === "RestHeaderSpec" ? new SvarkRestHeaderSpecAdapter({
581
+ url: resolvedUrl,
582
+ token: resolvedToken,
583
+ schema: resolvedSchema,
584
+ entity: resolvedEntity
585
+ }) : new SvarkResolveSpecAdapter({
586
+ url: resolvedUrl,
587
+ token: resolvedToken,
588
+ schema: resolvedSchema,
589
+ entity: resolvedEntity
590
+ });
591
+ });
592
+ // For virtual scroll: set dynamic rowCount once total is known
593
+ const gridDynamic = $derived(virtualScroll && total > 0 ? { rowCount: total } : undefined);
594
+ // ── Main data effect (initial load + sort/filter/page/reload triggers) ──────
595
+ $effect(() => {
596
+ const _adapter = adapter;
597
+ const _limit = virtualScroll ? 1 : pageSize;
598
+ const _sort = resolvedServerSide ? debouncedSort : [];
599
+ const _filters = resolvedServerSide ? debouncedFilter : {};
600
+ // Paging offset: only applied when server-side paging is active
601
+ const _offset = showPager ? page * pageSize : 0;
602
+ void reload;
603
+ if (!_adapter) {
604
+ activeServerRequestId += 1;
605
+ activeServerRequestKey = "";
606
+ loading = false;
607
+ error = resolvedServerSide ? "Missing server configuration (dataSourceOptions.url/schema/entity)" : null;
608
+ if (error) onerror?.(error);
609
+ return;
610
+ }
611
+ // Guard: if something upstream keeps re-triggering the effect without changing the
612
+ // actual server query parameters, avoid a tight refetch loop.
613
+ let requestId = 0;
614
+ let reqKey = "";
615
+ if (resolvedServerSide) {
616
+ const sortKey = JSON.stringify(_sort ?? []);
617
+ const filterKey = JSON.stringify(Object.keys(_filters ?? {}).sort().map((k) => [
618
+ k,
619
+ _filters[k]?.column,
620
+ _filters[k]?.operator,
621
+ _filters[k]?.value
622
+ ]));
623
+ const queryKey = JSON.stringify(query ?? {});
624
+ reqKey = JSON.stringify({
625
+ url: resolvedUrl,
626
+ schema: resolvedSchema,
627
+ entity: resolvedEntity,
628
+ adapter: resolvedAdapterType,
629
+ limit: _limit,
630
+ offset: _offset,
631
+ sortKey,
632
+ filterKey,
633
+ queryKey,
634
+ reload
635
+ });
636
+ if (reqKey === lastServerRequestKey) {
637
+ return;
550
638
  }
551
-
552
- return true;
553
- }
554
-
555
- $effect(() => {
556
- const s = sortState;
557
- const f = effectiveFilterState;
558
-
559
- if (!resolvedServerSide) {
560
- debouncedSort = s;
561
- debouncedFilter = f;
639
+ lastServerRequestKey = reqKey;
640
+ activeServerRequestKey = reqKey;
641
+ activeServerRequestId += 1;
642
+ requestId = activeServerRequestId;
643
+ }
644
+ loading = true;
645
+ error = null;
646
+ _adapter.read(_sort, _filters, _limit, _offset, query).then((response) => {
647
+ if (resolvedServerSide && (requestId !== activeServerRequestId || reqKey !== activeServerRequestKey)) {
562
648
  return;
563
649
  }
564
-
565
- if (_debounceTimer !== null) clearTimeout(_debounceTimer);
566
- _debounceTimer = setTimeout(() => {
567
- _debounceTimer = null;
568
- if (page === 0 && isSameSortState(debouncedSort, s) && isSameFilterState(debouncedFilter, f)) {
569
- return;
650
+ if (response?.success !== false) {
651
+ const incoming = normalizeGridRows(extractRowsFromResponse(response), resolvedUniqueID);
652
+ total = extractTotalFromResponse(response, incoming);
653
+ if (virtualScroll) {
654
+ data = [];
655
+ } else {
656
+ data = incoming;
570
657
  }
571
- page = 0; // reset page atomically — prevents a double-fetch
572
- debouncedSort = s;
573
- debouncedFilter = f;
574
- }, 300);
575
-
576
- return () => {
577
- if (_debounceTimer !== null) {
578
- clearTimeout(_debounceTimer);
579
- _debounceTimer = null;
658
+ emitGridEvent("load", incoming[0], undefined, {
659
+ x: 0,
660
+ y: 0
661
+ }, { total });
662
+ // If selection is currently id-based, refresh selectedItems mapping.
663
+ if (selectedIdsInternal.length > 0 && onSelectedItemsChange) {
664
+ const items = idsToSelectedItems(selectedIdsInternal, incoming, resolvedUniqueID);
665
+ emitSelectedItems(items);
580
666
  }
581
- };
582
- });
583
-
584
- function getLocalSearchColumns() {
585
- const columnsMap: Record<string, boolean> = {};
586
- for (const col of gridColumns) {
587
- const id = (col as any).id;
588
- if (id == null) continue;
589
- if ((col as any).hidden) continue;
590
- if ((col as any).searchable === false) continue;
591
- columnsMap[String(id)] = true;
592
- }
593
- return Object.keys(columnsMap).length > 0 ? columnsMap : undefined;
594
- }
595
-
596
- function applyServerSearchFilters(value: string) {
597
- if (!resolvedServerSide) return;
598
-
599
- if (!value) {
600
- // Avoid triggering a refetch loop by reassigning `{}` repeatedly.
601
- if (Object.keys(searchFilterState).length === 0) return;
602
- searchFilterState = {};
603
- return;
604
- }
605
-
606
- const searchableIds: string[] = [];
607
- for (const col of gridColumns) {
608
- const id = (col as any).id;
609
- if (id == null) continue;
610
- if ((col as any).hidden) continue;
611
- if ((col as any).searchable === true) searchableIds.push(String(id));
667
+ } else {
668
+ const msg = response.error?.message ?? "Failed to load data";
669
+ error = msg;
670
+ onerror?.(msg);
612
671
  }
613
-
614
- if (searchableIds.length === 0) {
615
- if (Object.keys(searchFilterState).length === 0) return;
616
- searchFilterState = {};
672
+ }).catch((e) => {
673
+ if (resolvedServerSide && (requestId !== activeServerRequestId || reqKey !== activeServerRequestKey)) {
617
674
  return;
618
675
  }
619
-
620
- const next: SvarkFilterState = {};
621
- for (const id of searchableIds) {
622
- // Avoid clobbering an existing column filter the user set elsewhere.
623
- const hasExistingFilter = Object.entries({ ...gridFilterState }).some(([key, filter]) => {
624
- const target = filter.column ?? key;
625
- return target === id && filter.value != null && filter.value !== '';
626
- });
627
- if (hasExistingFilter) continue;
628
- next[`search:${id}`] = { column: id, value, operator: 'ilike', logic_operator: 'OR' };
676
+ const msg = e instanceof Error ? e.message : "Unknown error";
677
+ error = msg;
678
+ onerror?.(msg);
679
+ }).finally(() => {
680
+ if (!resolvedServerSide || requestId === activeServerRequestId && reqKey === activeServerRequestKey) {
681
+ loading = false;
629
682
  }
630
-
631
- // Idempotency guard: avoid reassigning equivalent filter state.
632
- const prev = searchFilterState;
633
- const prevKeys = Object.keys(prev);
634
- const nextKeys = Object.keys(next);
635
- if (prevKeys.length === nextKeys.length) {
636
- let same = true;
637
- for (const k of nextKeys) {
638
- const a = (prev as any)[k];
639
- const b = (next as any)[k];
640
- if (!a || !b) {
641
- same = false;
642
- break;
643
- }
644
- if (
645
- String(a.column ?? '') !== String(b.column ?? '') ||
646
- String(a.operator ?? '') !== String(b.operator ?? '') ||
647
- String(a.logic_operator ?? '') !== String(b.logic_operator ?? '') ||
648
- String(a.value ?? '') !== String(b.value ?? '')
649
- ) {
650
- same = false;
651
- break;
652
- }
683
+ });
684
+ });
685
+ async function loadNextPage() {
686
+ const _adapter = adapter;
687
+ if (!_adapter) return;
688
+ // Block if main fetch is running (sort/filter change in progress)
689
+ if (loading || infiniteLoading || data.length >= total) return;
690
+ // Snapshot debounced sort+filter so we can discard the response if they
691
+ // change while the request is in-flight (race condition guard).
692
+ const snapshotSort = resolvedServerSide ? JSON.stringify(debouncedSort) : "";
693
+ const snapshotFilter = resolvedServerSide ? JSON.stringify(debouncedFilter) : "";
694
+ const snapshotOffset = data.length;
695
+ infiniteLoading = true;
696
+ try {
697
+ const response = await _adapter.read(resolvedServerSide ? debouncedSort : [], resolvedServerSide ? debouncedFilter : {}, pageSize, snapshotOffset, query);
698
+ // Discard if sort/filter changed while we were fetching
699
+ if (resolvedServerSide) {
700
+ if (JSON.stringify(debouncedSort) !== snapshotSort || JSON.stringify(debouncedFilter) !== snapshotFilter) {
701
+ return;
653
702
  }
654
- if (same) return;
655
- }
656
-
657
- searchFilterState = next;
658
- }
659
-
660
- function setSearch(next: string) {
661
- emitSearchValue(next);
662
- gridApi?.exec('search-rows', { search: next, columns: getLocalSearchColumns() });
663
- applyServerSearchFilters(next.trim());
664
- }
665
-
666
- function refreshGrid() {
667
- reload++;
668
- }
669
-
670
- function clearAllSort() {
671
- // Clear in the UI/store first (local mode), then clear generic sort state.
672
- gridApi?.exec('sort-rows', {} as any);
673
- emitSortOrder({});
674
- }
675
-
676
- function clearAllFilters() {
677
- emitFilters({});
678
- }
679
-
680
- async function applyLocalDataChange(next: Record<string, unknown>[]) {
681
- localRawData = next;
682
- await onDataChange?.(next);
683
- }
684
-
685
- function resolveGenericColumn(columnId: string | undefined) {
686
- if (!columnId) return undefined;
687
- return genericColumnsById[columnId];
688
- }
689
-
690
- function extractRowsFromResponse(response: unknown): Record<string, unknown>[] {
691
- if (Array.isArray(response)) {
692
- return response as Record<string, unknown>[];
693
703
  }
694
-
695
- if (
696
- response &&
697
- typeof response === 'object' &&
698
- Array.isArray((response as { data?: unknown }).data)
699
- ) {
700
- return (response as { data: Record<string, unknown>[] }).data;
701
- }
702
-
703
- return [];
704
- }
705
-
706
- function normalizeGridRows(rows: Record<string, unknown>[], uniqueID: string): Record<string, unknown>[] {
707
- return rows.map((row) => {
708
- const rowId = row?.id;
709
- const uniqueValue = row?.[uniqueID];
710
- if (rowId != null || uniqueValue == null) return row;
711
- return { ...row, id: uniqueValue };
712
- });
713
- }
714
-
715
- function extractTotalFromResponse(response: unknown, rows: Record<string, unknown>[]): number {
716
- if (
717
- response &&
718
- typeof response === 'object' &&
719
- typeof (response as { metadata?: { total?: unknown } }).metadata?.total === 'number'
720
- ) {
721
- return (response as { metadata: { total: number } }).metadata.total;
704
+ if (response?.success !== false) {
705
+ const incoming = normalizeGridRows(extractRowsFromResponse(response), resolvedUniqueID);
706
+ data = [...data, ...incoming];
722
707
  }
723
-
724
- return rows.length;
725
- }
726
-
727
- $effect(() => {
728
- // Keep SVAR's local highlight + server-side search filters in sync with controlled searchValue.
729
- const next = searchValueState;
730
- gridApi?.exec('search-rows', { search: next, columns: getLocalSearchColumns() });
731
- applyServerSearchFilters(next.trim());
732
- });
733
-
734
- function openSearch() {
735
- searchOpen = true;
736
- }
737
-
738
- function closeSearch() {
739
- searchOpen = false;
740
- }
741
-
742
- let selectedIdsInternal = $state<(string | number)[]>([]);
743
- $effect(() => {
744
- if (selectedItems === undefined) return;
745
- selectedIdsInternal = resolvedSelectedIds;
746
- });
747
- function handleSelectedIdsChange(ids: Array<string | number>) {
708
+ } catch {} finally {
709
+ infiniteLoading = false;
710
+ }
711
+ }
712
+ // ── SVAR Grid init ─────────────────────────────────────────────────────────
713
+ function handleInit(api) {
714
+ gridApi = api;
715
+ // Row selection callback
716
+ selectedIdsInternal = api.getState().selectedRows ?? [];
717
+ api.on("select-row", () => {
718
+ const ids = api.getState().selectedRows ?? [];
748
719
  selectedIdsInternal = ids;
720
+ onselect?.(ids);
749
721
  emitSelectedItems(idsToSelectedItems(ids, data, resolvedUniqueID));
722
+ });
723
+ // Virtual scroll: SVAR fires request-data when the visible row window changes.
724
+ // We fetch only that window from the API and replace data.
725
+ if (virtualScroll) {
726
+ api.on("request-data", async (ev) => {
727
+ const _adapter = adapter;
728
+ if (!_adapter) return;
729
+ const { start, end } = ev.row;
730
+ const limit = end - start;
731
+ if (limit <= 0) return;
732
+ loading = true;
733
+ try {
734
+ const response = await _adapter.read(resolvedServerSide ? debouncedSort : [], resolvedServerSide ? debouncedFilter : {}, limit, start, query);
735
+ if (response?.success !== false) {
736
+ data = normalizeGridRows(extractRowsFromResponse(response), resolvedUniqueID);
737
+ }
738
+ } catch (e) {
739
+ const msg = e instanceof Error ? e.message : "Failed to load data";
740
+ error = msg;
741
+ onerror?.(msg);
742
+ } finally {
743
+ loading = false;
744
+ }
745
+ });
750
746
  }
751
-
752
- const contextMenuProps = $derived.by((): SvarkContextMenuProps => {
753
- const hasSort = Object.values(resolvedSortOrder ?? {}).some((dir) => dir === 'asc' || dir === 'desc');
754
- const hasFilters = Object.keys(resolvedFilters ?? {}).length > 0;
755
- const hasSearch = searchValueState.trim().length > 0;
756
-
757
- return {
758
- items: (ctx) => {
759
- const defaults: SvarkContextMenuItem[] = [
760
- { id: 'refresh', label: 'Refresh', onselect: () => refreshGrid() },
761
- { id: 'clear-sort', label: 'Clear Sort', disabled: !hasSort, onselect: () => clearAllSort() },
762
- { id: 'clear-filters', label: 'Clear Filters', disabled: !hasFilters, onselect: () => clearAllFilters() },
763
- { id: 'clear-search', label: 'Clear Search', disabled: !hasSearch, onselect: () => setSearch('') }
764
- ];
765
-
766
- const userItemsRaw =
767
- typeof userContextMenuProps?.items === 'function'
768
- ? (userContextMenuProps.items as any)(ctx)
769
- : (userContextMenuProps?.items as any);
770
-
771
- const userItems = Array.isArray(userItemsRaw) ? (userItemsRaw as SvarkContextMenuItem[]) : [];
772
- if (userItems.length === 0) return defaults;
773
-
774
- const hasMarker = userItems.some((it) => it?.id === SVARKGRID_DEFAULT_ITEMS_MARKER_ID);
775
- if (hasMarker) {
776
- const merged: SvarkContextMenuItem[] = [];
777
- for (const it of userItems) {
778
- if (it?.id === SVARKGRID_DEFAULT_ITEMS_MARKER_ID) merged.push(...defaults);
779
- else merged.push(it);
780
- }
781
- return merged;
747
+ // Keep generic sortOrder in sync with SVAR, in both local and server-side modes.
748
+ // In server-side mode we block SVAR's default sorting (the server returns sorted rows).
749
+ api.intercept("sort-rows", (ev) => {
750
+ if (ev.key) {
751
+ if (ev.add) {
752
+ const idx = sortState.findIndex((s) => s.key === ev.key);
753
+ if (idx >= 0) {
754
+ sortState[idx] = {
755
+ key: ev.key,
756
+ order: ev.order ?? "asc"
757
+ };
758
+ } else {
759
+ sortState = [...sortState, {
760
+ key: ev.key,
761
+ order: ev.order ?? "asc"
762
+ }];
782
763
  }
783
-
784
- return [...defaults, { id: '__sep__', kind: 'separator', label: '' } as any, ...userItems];
785
- },
786
- onselectitem: userContextMenuProps?.onselectitem
787
- };
764
+ } else {
765
+ sortState = [{
766
+ key: ev.key,
767
+ order: ev.order ?? "asc"
768
+ }];
769
+ }
770
+ } else {
771
+ sortState = [];
772
+ }
773
+ emitSortOrder(svarkSortStateToSortOrder(sortState));
774
+ return resolvedServerSide ? false : true;
788
775
  });
789
-
790
- const isServerConfigured = $derived(!!(resolvedUrl && resolvedSchema && resolvedEntity));
791
-
792
- type Adapter = SvarkResolveSpecAdapter | SvarkRestHeaderSpecAdapter;
793
-
794
- const adapter = $derived.by((): Adapter | null => {
795
- if (!isServerConfigured) return null;
796
- return resolvedAdapterType === 'RestHeaderSpec'
797
- ? new SvarkRestHeaderSpecAdapter({
798
- url: resolvedUrl,
799
- token: resolvedToken,
800
- schema: resolvedSchema,
801
- entity: resolvedEntity
802
- })
803
- : new SvarkResolveSpecAdapter({
804
- url: resolvedUrl,
805
- token: resolvedToken,
806
- schema: resolvedSchema,
807
- entity: resolvedEntity
808
- });
776
+ api.intercept("resize-column", (ev) => {
777
+ emitGridEvent("column_resized", undefined, resolveGenericColumn(ev?.id != null ? String(ev.id) : undefined), {
778
+ x: 0,
779
+ y: 0
780
+ }, {
781
+ width: ev?.width,
782
+ inProgress: ev?.inProgress
783
+ });
784
+ return true;
809
785
  });
810
-
811
- // For virtual scroll: set dynamic rowCount once total is known
812
- const gridDynamic = $derived(
813
- virtualScroll && total > 0 ? { rowCount: total } : undefined
814
- );
815
-
816
- // ── Main data effect (initial load + sort/filter/page/reload triggers) ──────
817
-
818
- $effect(() => {
786
+ api.on("add-row", async (ev) => {
787
+ if (resolvedReadonly) return;
819
788
  const _adapter = adapter;
820
- const _limit = virtualScroll ? 1 : pageSize;
821
- const _sort = resolvedServerSide ? debouncedSort : [];
822
- const _filters = resolvedServerSide ? debouncedFilter : {};
823
- // Paging offset: only applied when server-side paging is active
824
- const _offset = showPager ? page * pageSize : 0;
825
- void reload;
826
-
827
- if (!_adapter) {
828
- activeServerRequestId += 1;
829
- activeServerRequestKey = '';
830
- loading = false;
831
- error = resolvedServerSide
832
- ? 'Missing server configuration (dataSourceOptions.url/schema/entity)'
833
- : null;
834
- if (error) onerror?.(error);
835
- return;
836
- }
837
-
838
- // Guard: if something upstream keeps re-triggering the effect without changing the
839
- // actual server query parameters, avoid a tight refetch loop.
840
- let requestId = 0;
841
- let reqKey = '';
842
- if (resolvedServerSide) {
843
- const sortKey = JSON.stringify(_sort ?? []);
844
- const filterKey = JSON.stringify(
845
- Object.keys(_filters ?? {})
846
- .sort()
847
- .map((k) => [k, (_filters as any)[k]?.column, (_filters as any)[k]?.operator, (_filters as any)[k]?.value])
848
- );
849
- const queryKey = JSON.stringify(query ?? {});
850
- reqKey = JSON.stringify({
851
- url: resolvedUrl,
852
- schema: resolvedSchema,
853
- entity: resolvedEntity,
854
- adapter: resolvedAdapterType,
855
- limit: _limit,
856
- offset: _offset,
857
- sortKey,
858
- filterKey,
859
- queryKey,
860
- reload
861
- });
862
-
863
- if (reqKey === lastServerRequestKey) {
864
- return;
789
+ try {
790
+ if (_adapter) {
791
+ await _adapter.create(ev.row);
792
+ } else {
793
+ await applyLocalDataChange([...localRawData, ev.row]);
865
794
  }
866
-
867
- lastServerRequestKey = reqKey;
868
- activeServerRequestKey = reqKey;
869
- activeServerRequestId += 1;
870
- requestId = activeServerRequestId;
871
- }
872
-
873
- loading = true;
874
- error = null;
875
-
876
- _adapter
877
- .read(_sort, _filters, _limit, _offset, query)
878
- .then((response) => {
879
- if (
880
- resolvedServerSide &&
881
- (requestId !== activeServerRequestId || reqKey !== activeServerRequestKey)
882
- ) {
883
- return;
884
- }
885
- if ((response as any)?.success !== false) {
886
- const incoming = normalizeGridRows(
887
- extractRowsFromResponse(response),
888
- resolvedUniqueID
889
- );
890
- total = extractTotalFromResponse(response, incoming);
891
-
892
- if (virtualScroll) {
893
- data = [];
894
- } else {
895
- data = incoming;
896
- }
897
- emitGridEvent('load', incoming[0], undefined, { x: 0, y: 0 }, { total });
898
-
899
- // If selection is currently id-based, refresh selectedItems mapping.
900
- if (selectedIdsInternal.length > 0 && onSelectedItemsChange) {
901
- const items = idsToSelectedItems(selectedIdsInternal, incoming as any, resolvedUniqueID);
902
- emitSelectedItems(items);
903
- }
904
- } else {
905
- const msg = (response as any).error?.message ?? 'Failed to load data';
906
- error = msg;
907
- onerror?.(msg);
908
- }
909
- })
910
- .catch((e) => {
911
- if (
912
- resolvedServerSide &&
913
- (requestId !== activeServerRequestId || reqKey !== activeServerRequestKey)
914
- ) {
915
- return;
916
- }
917
- const msg = e instanceof Error ? e.message : 'Unknown error';
918
- error = msg;
919
- onerror?.(msg);
920
- })
921
- .finally(() => {
922
- if (
923
- !resolvedServerSide ||
924
- (requestId === activeServerRequestId && reqKey === activeServerRequestKey)
925
- ) {
926
- loading = false;
927
- }
795
+ onchange?.({
796
+ action: "add",
797
+ row: ev.row
928
798
  });
799
+ reload++;
800
+ } catch (e) {
801
+ onerror?.(e instanceof Error ? e.message : "Create failed");
802
+ }
929
803
  });
930
-
931
- async function loadNextPage() {
804
+ api.on("delete-row", async (ev) => {
805
+ if (resolvedReadonly) return;
932
806
  const _adapter = adapter;
933
- if (!_adapter) return;
934
-
935
- // Block if main fetch is running (sort/filter change in progress)
936
- if (loading || infiniteLoading || data.length >= total) return;
937
-
938
- // Snapshot debounced sort+filter so we can discard the response if they
939
- // change while the request is in-flight (race condition guard).
940
- const snapshotSort = resolvedServerSide ? JSON.stringify(debouncedSort) : '';
941
- const snapshotFilter = resolvedServerSide ? JSON.stringify(debouncedFilter) : '';
942
- const snapshotOffset = data.length;
943
-
944
- infiniteLoading = true;
945
807
  try {
946
- const response = await _adapter.read(
947
- resolvedServerSide ? debouncedSort : [],
948
- resolvedServerSide ? debouncedFilter : {},
949
- pageSize,
950
- snapshotOffset,
951
- query
952
- );
953
- // Discard if sort/filter changed while we were fetching
954
- if (resolvedServerSide) {
955
- if (
956
- JSON.stringify(debouncedSort) !== snapshotSort ||
957
- JSON.stringify(debouncedFilter) !== snapshotFilter
958
- ) {
959
- return;
960
- }
961
- }
962
- if ((response as any)?.success !== false) {
963
- const incoming = normalizeGridRows(
964
- extractRowsFromResponse(response),
965
- resolvedUniqueID
966
- );
967
- data = [...data, ...incoming];
808
+ if (_adapter) {
809
+ await _adapter.delete(ev.id);
810
+ } else {
811
+ await applyLocalDataChange(localRawData.filter((row) => String(row?.[resolvedUniqueID]) !== String(ev.id)));
968
812
  }
969
- } catch {
970
- // Silent — don't replace existing rows with an error on pagination
971
- } finally {
972
- infiniteLoading = false;
973
- }
974
- }
975
-
976
- // ── SVAR Grid init ─────────────────────────────────────────────────────────
977
-
978
- function handleInit(api: IApi) {
979
- gridApi = api;
980
-
981
- // Row selection callback
982
- selectedIdsInternal = ((api.getState() as any).selectedRows ?? []) as (string | number)[];
983
- api.on('select-row', () => {
984
- const ids = ((api.getState() as any).selectedRows ?? []) as (string | number)[];
985
- selectedIdsInternal = ids;
986
- onselect?.(ids);
987
- emitSelectedItems(idsToSelectedItems(ids, data, resolvedUniqueID));
988
- });
989
-
990
- // Virtual scroll: SVAR fires request-data when the visible row window changes.
991
- // We fetch only that window from the API and replace data.
992
- if (virtualScroll) {
993
- api.on('request-data', async (ev: any) => {
994
- const _adapter = adapter;
995
- if (!_adapter) return;
996
-
997
- const { start, end } = ev.row as { start: number; end: number };
998
- const limit = end - start;
999
- if (limit <= 0) return;
1000
-
1001
- loading = true;
1002
- try {
1003
- const response = await _adapter.read(
1004
- resolvedServerSide ? debouncedSort : [],
1005
- resolvedServerSide ? debouncedFilter : {},
1006
- limit,
1007
- start,
1008
- query
1009
- );
1010
- if ((response as any)?.success !== false) {
1011
- data = normalizeGridRows(extractRowsFromResponse(response), resolvedUniqueID);
1012
- }
1013
- } catch (e) {
1014
- const msg = e instanceof Error ? e.message : 'Failed to load data';
1015
- error = msg;
1016
- onerror?.(msg);
1017
- } finally {
1018
- loading = false;
1019
- }
813
+ onchange?.({
814
+ action: "delete",
815
+ id: ev.id
1020
816
  });
817
+ reload++;
818
+ } catch (e) {
819
+ onerror?.(e instanceof Error ? e.message : "Delete failed");
1021
820
  }
1022
-
1023
- // Keep generic sortOrder in sync with SVAR, in both local and server-side modes.
1024
- // In server-side mode we block SVAR's default sorting (the server returns sorted rows).
1025
- api.intercept('sort-rows', (ev: any) => {
1026
- if (ev.key) {
1027
- if (ev.add) {
1028
- const idx = sortState.findIndex((s) => s.key === ev.key);
1029
- if (idx >= 0) {
1030
- sortState[idx] = { key: ev.key, order: ev.order ?? 'asc' };
1031
- } else {
1032
- sortState = [...sortState, { key: ev.key, order: ev.order ?? 'asc' }];
1033
- }
1034
- } else {
1035
- sortState = [{ key: ev.key, order: ev.order ?? 'asc' }];
1036
- }
821
+ });
822
+ api.on("update-cell", async (ev) => {
823
+ if (resolvedReadonly) return;
824
+ const _adapter = adapter;
825
+ try {
826
+ if (_adapter) {
827
+ await _adapter.update(ev.id, { [String(ev.column)]: ev.value });
1037
828
  } else {
1038
- sortState = [];
1039
- }
1040
- emitSortOrder(svarkSortStateToSortOrder(sortState));
1041
- return resolvedServerSide ? false : true;
1042
- });
1043
-
1044
- api.intercept('resize-column', (ev: any) => {
1045
- emitGridEvent(
1046
- 'column_resized',
1047
- undefined,
1048
- resolveGenericColumn(ev?.id != null ? String(ev.id) : undefined),
1049
- { x: 0, y: 0 },
1050
- { width: ev?.width, inProgress: ev?.inProgress }
1051
- );
1052
- return true;
1053
- });
1054
-
1055
- api.on('add-row', async (ev: any) => {
1056
- if (resolvedReadonly) return;
1057
- const _adapter = adapter;
1058
- try {
1059
- if (_adapter) {
1060
- await _adapter.create(ev.row);
1061
- } else {
1062
- await applyLocalDataChange([...localRawData, ev.row]);
1063
- }
1064
- onchange?.({ action: 'add', row: ev.row });
1065
- reload++;
1066
- } catch (e) {
1067
- onerror?.(e instanceof Error ? e.message : 'Create failed');
1068
- }
1069
- });
1070
-
1071
- api.on('delete-row', async (ev: any) => {
1072
- if (resolvedReadonly) return;
1073
- const _adapter = adapter;
1074
- try {
1075
- if (_adapter) {
1076
- await _adapter.delete(ev.id);
1077
- } else {
1078
- await applyLocalDataChange(
1079
- localRawData.filter((row) => String(row?.[resolvedUniqueID]) !== String(ev.id))
1080
- );
1081
- }
1082
- onchange?.({ action: 'delete', id: ev.id });
1083
- reload++;
1084
- } catch (e) {
1085
- onerror?.(e instanceof Error ? e.message : 'Delete failed');
1086
- }
1087
- });
1088
-
1089
- api.on('update-cell', async (ev: any) => {
1090
- if (resolvedReadonly) return;
1091
- const _adapter = adapter;
1092
- try {
1093
- if (_adapter) {
1094
- await _adapter.update(ev.id, { [String(ev.column)]: ev.value });
1095
- } else {
1096
- await applyLocalDataChange(
1097
- localRawData.map((row) =>
1098
- String(row?.[resolvedUniqueID]) === String(ev.id)
1099
- ? { ...row, [String(ev.column)]: ev.value }
1100
- : row
1101
- )
1102
- );
1103
- }
1104
- onchange?.({ action: 'update', id: ev.id, column: ev.column, value: ev.value });
1105
- } catch (e) {
1106
- onerror?.(e instanceof Error ? e.message : 'Update failed');
829
+ await applyLocalDataChange(localRawData.map((row) => String(row?.[resolvedUniqueID]) === String(ev.id) ? {
830
+ ...row,
831
+ [String(ev.column)]: ev.value
832
+ } : row));
1107
833
  }
1108
- });
1109
- }
1110
-
1111
- let lastSortSignature = $state('');
1112
- $effect(() => {
1113
- const signature = JSON.stringify(resolvedSortOrder ?? {});
1114
- if (!signature || signature === lastSortSignature) return;
1115
- lastSortSignature = signature;
1116
- emitGridEvent('sort_changed', undefined, undefined, { x: 0, y: 0 }, { sortOrder: resolvedSortOrder });
1117
- });
1118
-
1119
- let lastFilterSignature = $state('');
1120
- $effect(() => {
1121
- const signature = JSON.stringify(resolvedFilters ?? {});
1122
- if (signature === lastFilterSignature) return;
1123
- lastFilterSignature = signature;
1124
- emitGridEvent('filter_changed', undefined, undefined, { x: 0, y: 0 }, { filters: resolvedFilters });
1125
- });
1126
-
1127
- let lastSearchValue = $state(searchValueState);
1128
- $effect(() => {
1129
- const next = searchValueState;
1130
- if (next === lastSearchValue) return;
1131
- lastSearchValue = next;
1132
- emitGridEvent('search_changed', undefined, undefined, { x: 0, y: 0 }, { value: next });
1133
- });
1134
-
1135
- let lastSelectionSignature = $state('');
1136
- $effect(() => {
1137
- const signature = JSON.stringify(selectedIdsInternal);
1138
- if (signature === lastSelectionSignature) return;
1139
- lastSelectionSignature = signature;
1140
- emitGridEvent('selection_changed', resolvedSelectedItems[0], undefined, { x: 0, y: 0 }, {
1141
- selectedIds: selectedIdsInternal,
1142
- selectedItems: resolvedSelectedItems
1143
- });
834
+ onchange?.({
835
+ action: "update",
836
+ id: ev.id,
837
+ column: ev.column,
838
+ value: ev.value
839
+ });
840
+ } catch (e) {
841
+ onerror?.(e instanceof Error ? e.message : "Update failed");
842
+ }
1144
843
  });
1145
-
1146
- let lastSettingsSignature = $state('');
1147
- $effect(() => {
1148
- const signature = JSON.stringify(settings ?? {});
1149
- if (signature === lastSettingsSignature) return;
1150
- lastSettingsSignature = signature;
1151
- emitGridEvent('settings_changed', undefined, undefined, { x: 0, y: 0 }, { settings });
844
+ }
845
+ let lastSortSignature = $state("");
846
+ $effect(() => {
847
+ const signature = JSON.stringify(resolvedSortOrder ?? {});
848
+ if (!signature || signature === lastSortSignature) return;
849
+ lastSortSignature = signature;
850
+ emitGridEvent("sort_changed", undefined, undefined, {
851
+ x: 0,
852
+ y: 0
853
+ }, { sortOrder: resolvedSortOrder });
854
+ });
855
+ let lastFilterSignature = $state("");
856
+ $effect(() => {
857
+ const signature = JSON.stringify(resolvedFilters ?? {});
858
+ if (signature === lastFilterSignature) return;
859
+ lastFilterSignature = signature;
860
+ emitGridEvent("filter_changed", undefined, undefined, {
861
+ x: 0,
862
+ y: 0
863
+ }, { filters: resolvedFilters });
864
+ });
865
+ // eslint-disable-next-line svelte/no-unused-svelte-ignore
866
+ // svelte-ignore state_referenced_locally -- intentional initial capture for change-detection
867
+ let lastSearchValue = $state(searchValueState);
868
+ $effect(() => {
869
+ const next = searchValueState;
870
+ if (next === lastSearchValue) return;
871
+ lastSearchValue = next;
872
+ emitGridEvent("search_changed", undefined, undefined, {
873
+ x: 0,
874
+ y: 0
875
+ }, { value: next });
876
+ });
877
+ let lastSelectionSignature = $state("");
878
+ $effect(() => {
879
+ const signature = JSON.stringify(selectedIdsInternal);
880
+ if (signature === lastSelectionSignature) return;
881
+ lastSelectionSignature = signature;
882
+ emitGridEvent("selection_changed", resolvedSelectedItems[0], undefined, {
883
+ x: 0,
884
+ y: 0
885
+ }, {
886
+ selectedIds: selectedIdsInternal,
887
+ selectedItems: resolvedSelectedItems
1152
888
  });
1153
-
1154
- let lastPageSignature = $state('');
1155
- $effect(() => {
1156
- if (!showPager) return;
1157
- const signature = `${page}:${totalPages}:${pageStart}:${pageEnd}`;
1158
- if (signature === lastPageSignature) return;
1159
- lastPageSignature = signature;
1160
- emitGridEvent('page_loaded', data[0], undefined, { x: 0, y: 0 }, {
1161
- page,
1162
- totalPages,
1163
- pageStart,
1164
- pageEnd
1165
- });
889
+ });
890
+ let lastSettingsSignature = $state("");
891
+ $effect(() => {
892
+ const signature = JSON.stringify(settings ?? {});
893
+ if (signature === lastSettingsSignature) return;
894
+ lastSettingsSignature = signature;
895
+ emitGridEvent("settings_changed", undefined, undefined, {
896
+ x: 0,
897
+ y: 0
898
+ }, { settings });
899
+ });
900
+ let lastPageSignature = $state("");
901
+ $effect(() => {
902
+ if (!showPager) return;
903
+ const signature = `${page}:${totalPages}:${pageStart}:${pageEnd}`;
904
+ if (signature === lastPageSignature) return;
905
+ lastPageSignature = signature;
906
+ emitGridEvent("page_loaded", data[0], undefined, {
907
+ x: 0,
908
+ y: 0
909
+ }, {
910
+ page,
911
+ totalPages,
912
+ pageStart,
913
+ pageEnd
1166
914
  });
1167
-
1168
- let lastDataSignature = $state('');
1169
- $effect(() => {
1170
- const signature = `${data.length}:${total}:${loading ? 1 : 0}`;
1171
- if (signature === lastDataSignature || loading) return;
1172
- lastDataSignature = signature;
1173
- emitGridEvent('data_changed', data[0], undefined, { x: 0, y: 0 }, { data, total });
915
+ });
916
+ let lastDataSignature = $state("");
917
+ $effect(() => {
918
+ const signature = `${data.length}:${total}:${loading ? 1 : 0}`;
919
+ if (signature === lastDataSignature || loading) return;
920
+ lastDataSignature = signature;
921
+ emitGridEvent("data_changed", data[0], undefined, {
922
+ x: 0,
923
+ y: 0
924
+ }, {
925
+ data,
926
+ total
1174
927
  });
928
+ });
1175
929
  </script>
1176
930
 
1177
931
  <SvarkGridView