@warkypublic/svelix 0.1.46 → 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.
- package/README.md +93 -33
- package/dist/components/BetterMenu/BetterMenu.svelte +21 -37
- package/dist/components/BetterMenu/BetterMenuAsyncButton.svelte +21 -31
- package/dist/components/BetterMenu/BetterMenuPreview.svelte +27 -29
- package/dist/components/BetterMenu/MenuRenderer.svelte +40 -51
- package/dist/components/Boxer/Boxer.svelte +488 -602
- package/dist/components/Boxer/BoxerTarget.svelte +6 -46
- package/dist/components/Boxer/BoxerTarget.svelte.d.ts +1 -0
- package/dist/components/Boxer/types.d.ts +0 -1
- package/dist/components/Button.svelte +19 -43
- package/dist/components/ButtonPreview.svelte +2 -11
- package/dist/components/CardGrid/CardGrid.svelte +443 -614
- package/dist/components/CardGrid/CardGridFilterPanel.svelte +111 -90
- package/dist/components/CardGrid/DefaultCard.svelte +80 -96
- package/dist/components/CardGrid/ImageCardStory.svelte +87 -36
- package/dist/components/ContentEditor/CollaboraInsertTextDemo.svelte +5 -10
- package/dist/components/ContentEditor/ContentEditor.svelte +24 -50
- package/dist/components/ContentEditor/subcomponents/AudioPlayer.svelte +17 -17
- package/dist/components/ContentEditor/subcomponents/CollaboraEditor.svelte +244 -309
- package/dist/components/ContentEditor/subcomponents/EmailViewer.svelte +2 -3
- package/dist/components/ContentEditor/subcomponents/ImageViewer.svelte +14 -15
- package/dist/components/ContentEditor/subcomponents/MarkdownViewer.svelte +74 -90
- package/dist/components/ContentEditor/subcomponents/MonacoEditor.svelte +75 -103
- package/dist/components/ContentEditor/subcomponents/Office365Editor.svelte +234 -297
- package/dist/components/ContentEditor/subcomponents/PdfViewer.svelte +14 -20
- package/dist/components/ContentEditor/subcomponents/TextEditor.svelte +122 -150
- package/dist/components/ContentEditor/subcomponents/UnknownFile.svelte +105 -126
- package/dist/components/ContentEditor/subcomponents/VideoPlayer.svelte +17 -17
- package/dist/components/ContentEditor/subcomponents/ZipViewer.svelte +2 -3
- package/dist/components/ErrorBoundary/ErrorBoundary.svelte +41 -58
- package/dist/components/ErrorBoundary/ErrorBoundaryPreview.svelte +8 -16
- package/dist/components/Former/Former.svelte +306 -424
- package/dist/components/Former/FormerButtonArea.svelte +17 -68
- package/dist/components/Former/FormerDrawer.svelte +26 -78
- package/dist/components/Former/FormerDrawerPreview.svelte +99 -89
- package/dist/components/Former/FormerModal.svelte +24 -70
- package/dist/components/Former/FormerModalPreview.svelte +99 -89
- package/dist/components/Former/FormerPreview.svelte +76 -78
- package/dist/components/Former/FormerRestApiPreview.svelte +55 -48
- package/dist/components/FormerControllers/ButtonCtrl.svelte +13 -44
- package/dist/components/FormerControllers/DateTimeCtrl/DateTimeCtrl.svelte +280 -445
- package/dist/components/FormerControllers/DateTimeCtrl/DateTimeCtrlCalendar.svelte +95 -136
- package/dist/components/FormerControllers/DateTimeCtrl/DateTimeCtrlPickerPanel.svelte +3 -62
- package/dist/components/FormerControllers/DateTimeCtrl/DateTimeCtrlTimeFields.svelte +143 -217
- package/dist/components/FormerControllers/IconButtonCtrl.svelte +13 -42
- package/dist/components/FormerControllers/InlineWrapper.svelte +12 -41
- package/dist/components/FormerControllers/InlineWrapperPreview.svelte +4 -26
- package/dist/components/FormerControllers/NativeSelectCtrl.svelte +3 -29
- package/dist/components/FormerControllers/NumberInputCtrl.svelte +9 -35
- package/dist/components/FormerControllers/PasswordInputCtrl.svelte +4 -23
- package/dist/components/FormerControllers/SwitchCtrl.svelte +3 -20
- package/dist/components/FormerControllers/TextAreaCtrl.svelte +3 -24
- package/dist/components/FormerControllers/TextInputCtrl.svelte +3 -24
- package/dist/components/GlobalStateStore/GlobalStateStoreProvider.svelte +73 -120
- package/dist/components/Gridler/components/Gridler.svelte +338 -442
- package/dist/components/Gridler/components/GridlerCanvas.svelte +720 -1115
- package/dist/components/Gridler/components/GridlerEditor.svelte +2 -15
- package/dist/components/Gridler/components/GridlerFull.svelte +520 -841
- package/dist/components/Gridler/components/GridlerFull.svelte.d.ts +1 -1
- package/dist/components/Gridler/components/GridlerFullWithFormerPreview.svelte +194 -147
- package/dist/components/Gridler/components/GridlerSearch.svelte +7 -16
- package/dist/components/Gridler/components/GridlerSearchToggle.svelte +7 -19
- package/dist/components/Gridler/types.d.ts +1 -1
- package/dist/components/Icons/IconAdd.svelte +1 -3
- package/dist/components/Icons/IconAlertCircle.svelte +1 -3
- package/dist/components/Icons/IconAlertTriangle.svelte +1 -3
- package/dist/components/Icons/IconCamera.svelte +1 -3
- package/dist/components/Icons/IconClose.svelte +1 -3
- package/dist/components/Icons/IconEdit.svelte +1 -3
- package/dist/components/Icons/IconFilter.svelte +1 -3
- package/dist/components/Icons/IconSearch.svelte +1 -3
- package/dist/components/Icons/IconSort.svelte +1 -3
- package/dist/components/Icons/IconTrash.svelte +1 -3
- package/dist/components/Portal/Portal.svelte +14 -38
- package/dist/components/Screenshot/Screenshot.svelte +7 -13
- package/dist/components/Svark/Svark.svelte +680 -852
- package/dist/components/Svark/SvarkContextMenu.svelte +44 -63
- package/dist/components/Svark/SvarkInfiniteLoader.svelte +2 -6
- package/dist/components/Svark/SvarkPager.svelte +2 -28
- package/dist/components/Svark/SvarkSelectionDemo.svelte +4 -8
- package/dist/components/Svark/SvarkTopBar.svelte +9 -74
- package/dist/components/SvarkGrid/SvarkGrid.svelte +877 -1123
- package/dist/components/SvarkGrid/components/SvarkColumnFilterForm.svelte +45 -58
- package/dist/components/SvarkGrid/components/SvarkContextMenu.svelte +44 -63
- package/dist/components/SvarkGrid/components/SvarkHeaderFilterCell.svelte +41 -75
- package/dist/components/SvarkGrid/components/SvarkInfiniteLoader.svelte +2 -6
- package/dist/components/SvarkGrid/components/SvarkPager.svelte +2 -28
- package/dist/components/SvarkGrid/components/SvarkSearchPopover.svelte +8 -16
- package/dist/components/SvarkGrid/components/SvarkTopBar.svelte +6 -62
- package/dist/components/SvarkGrid/internal/SvarkGridView.svelte +345 -507
- package/dist/components/VTree/VTree.svelte +349 -431
- package/dist/components/VTree/VTreeContextMenu.svelte +21 -33
- package/dist/components/VTree/VTreeEventsDemo.svelte +48 -58
- package/dist/components/VTree/VTreeRow.svelte +13 -65
- package/dist/components/VTree/VTreeSearch.svelte +6 -13
- package/dist/components/VTree/VTreeVirtualViewport.svelte +37 -106
- package/package.json +30 -29
|
@@ -1,644 +1,474 @@
|
|
|
1
|
-
<script lang="ts">
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
searchValue?: string;
|
|
70
|
-
onSearchValueChange?: (v: string) => void;
|
|
71
|
-
serverSideSearch?: boolean;
|
|
72
|
-
searchColumns?: string[];
|
|
73
|
-
searchPlaceholder?: string;
|
|
74
|
-
|
|
75
|
-
// Sort
|
|
76
|
-
sortOrder?: GridColumnSortOrder;
|
|
77
|
-
onSortOrderChange?: (order: GridColumnSortOrder) => void;
|
|
78
|
-
sortOptions?: CardSortOption[];
|
|
79
|
-
|
|
80
|
-
// Filters
|
|
81
|
-
filters?: GridColumnFilters;
|
|
82
|
-
onFilterChange?: (filters: GridColumnFilters) => void;
|
|
83
|
-
|
|
84
|
-
// Selection
|
|
85
|
-
selectedItems?: Record<string, unknown>[];
|
|
86
|
-
onSelectedItemsChange?: (items: Record<string, unknown>[]) => void;
|
|
87
|
-
multiSelect?: boolean;
|
|
88
|
-
|
|
89
|
-
// Card events
|
|
90
|
-
onCardClick?: (record: Record<string, unknown>, index: number) => void;
|
|
91
|
-
onCardDblClick?: (record: Record<string, unknown>, index: number) => void;
|
|
92
|
-
onCardContextMenu?: (
|
|
93
|
-
record: Record<string, unknown>,
|
|
94
|
-
index: number,
|
|
95
|
-
x: number,
|
|
96
|
-
y: number,
|
|
97
|
-
) => void;
|
|
98
|
-
|
|
99
|
-
// Context menu
|
|
100
|
-
menuItems?: CardGridContextMenuItem[];
|
|
101
|
-
|
|
102
|
-
// Status (bindable)
|
|
103
|
-
total?: number;
|
|
104
|
-
loading?: boolean;
|
|
105
|
-
onLoadError?: (error: string) => void;
|
|
106
|
-
|
|
107
|
-
// Layout
|
|
108
|
-
class?: string;
|
|
109
|
-
toolbarEnd?: Snippet;
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
let {
|
|
113
|
-
columns,
|
|
114
|
-
displayColumns,
|
|
115
|
-
cardMinWidth = 280,
|
|
116
|
-
cardGap = 16,
|
|
117
|
-
cardMaxFrontColumns,
|
|
118
|
-
card,
|
|
119
|
-
cardBack,
|
|
120
|
-
empty,
|
|
121
|
-
skeleton,
|
|
122
|
-
dataSource = 'local',
|
|
123
|
-
dataSourceOptions,
|
|
124
|
-
data,
|
|
125
|
-
pageSize = 20,
|
|
126
|
-
uniqueID: uniqueIDProp,
|
|
127
|
-
searchValue,
|
|
128
|
-
onSearchValueChange,
|
|
129
|
-
serverSideSearch = true,
|
|
130
|
-
searchColumns,
|
|
131
|
-
searchPlaceholder = 'Search…',
|
|
132
|
-
sortOrder,
|
|
133
|
-
onSortOrderChange,
|
|
134
|
-
sortOptions,
|
|
135
|
-
filters,
|
|
136
|
-
onFilterChange,
|
|
137
|
-
selectedItems: selectedItemsProp,
|
|
138
|
-
onSelectedItemsChange,
|
|
139
|
-
multiSelect = true,
|
|
140
|
-
onCardClick,
|
|
141
|
-
onCardDblClick,
|
|
142
|
-
onCardContextMenu,
|
|
143
|
-
menuItems,
|
|
144
|
-
total = $bindable(0),
|
|
145
|
-
loading = $bindable(false),
|
|
146
|
-
onLoadError,
|
|
147
|
-
class: cls,
|
|
148
|
-
toolbarEnd,
|
|
149
|
-
}: Props = $props();
|
|
150
|
-
|
|
151
|
-
// ── Derived config ─────────────────────────────────────────────────────────
|
|
152
|
-
|
|
153
|
-
const resolvedUniqueID = $derived(uniqueIDProp ?? dataSourceOptions?.uniqueID ?? 'id');
|
|
154
|
-
|
|
155
|
-
const resolvedColumns = $derived(
|
|
156
|
-
displayColumns?.length ? columns.filter((c) => displayColumns!.includes(c.id)) : columns,
|
|
157
|
-
);
|
|
158
|
-
|
|
159
|
-
// Flip mode split
|
|
160
|
-
const frontColumns = $derived(
|
|
161
|
-
cardMaxFrontColumns != null ? resolvedColumns.slice(0, cardMaxFrontColumns) : resolvedColumns,
|
|
162
|
-
);
|
|
163
|
-
const backColumns = $derived(
|
|
164
|
-
cardMaxFrontColumns != null ? resolvedColumns.slice(cardMaxFrontColumns) : [],
|
|
165
|
-
);
|
|
166
|
-
const flipModeEnabled = $derived(backColumns.length > 0);
|
|
167
|
-
|
|
168
|
-
// How many detail rows the skeleton should mimic
|
|
169
|
-
const skeletonDetailRows = $derived(Math.max(0, frontColumns.length - 2));
|
|
170
|
-
|
|
171
|
-
// ── Controlled/uncontrolled search ─────────────────────────────────────────
|
|
172
|
-
|
|
173
|
-
let internalSearchValue = $state('');
|
|
174
|
-
const resolvedSearchValue = $derived(searchValue ?? internalSearchValue);
|
|
175
|
-
|
|
176
|
-
function emitSearchValue(v: string) {
|
|
177
|
-
if (searchValue === undefined) internalSearchValue = v;
|
|
178
|
-
onSearchValueChange?.(v);
|
|
1
|
+
<script lang="ts">import { untrack } from "svelte";
|
|
2
|
+
import { SvelteSet } from "svelte/reactivity";
|
|
3
|
+
import { createVirtualizer } from "@tanstack/svelte-virtual";
|
|
4
|
+
import BetterMenu from "../BetterMenu/BetterMenu.svelte";
|
|
5
|
+
import { GridlerResolveSpecAdapter } from "../Gridler/adapters/GridlerResolveSpecAdapter.js";
|
|
6
|
+
import { GridlerRestHeaderSpecAdapter } from "../Gridler/adapters/GridlerRestHeaderSpecAdapter.js";
|
|
7
|
+
import { gridColumnFiltersToFilterOptions, buildSearchFilters, applyLocalFilter } from "../Gridler/utils/filters.js";
|
|
8
|
+
import { applyLocalSort, sortOrderToSortOptions } from "../Gridler/utils/sort.js";
|
|
9
|
+
import CardGridFilterPanel from "./CardGridFilterPanel.svelte";
|
|
10
|
+
import DefaultCard from "./DefaultCard.svelte";
|
|
11
|
+
import { toggleItemInSelection, rangeSelect, isItemSelected, computeColumnsInRow } from "./cardGrid.js";
|
|
12
|
+
let { columns, displayColumns, cardMinWidth = 280, cardGap = 16, cardMaxFrontColumns, card, cardBack, empty, skeleton, dataSource = "local", dataSourceOptions, data, pageSize = 20, uniqueID: uniqueIDProp, searchValue, onSearchValueChange, serverSideSearch = true, searchColumns, searchPlaceholder = "Search…", sortOrder, onSortOrderChange, sortOptions, filters, onFilterChange, selectedItems: selectedItemsProp, onSelectedItemsChange, multiSelect = true, onCardClick, onCardDblClick, onCardContextMenu, menuItems, total = $bindable(0), loading = $bindable(false), onLoadError, class: cls, toolbarEnd } = $props();
|
|
13
|
+
// ── Derived config ─────────────────────────────────────────────────────────
|
|
14
|
+
const resolvedUniqueID = $derived(uniqueIDProp ?? dataSourceOptions?.uniqueID ?? "id");
|
|
15
|
+
const resolvedColumns = $derived(displayColumns?.length ? columns.filter((c) => displayColumns?.includes(c.id)) : columns);
|
|
16
|
+
// Flip mode split
|
|
17
|
+
const frontColumns = $derived(cardMaxFrontColumns != null ? resolvedColumns.slice(0, cardMaxFrontColumns) : resolvedColumns);
|
|
18
|
+
const backColumns = $derived(cardMaxFrontColumns != null ? resolvedColumns.slice(cardMaxFrontColumns) : []);
|
|
19
|
+
const flipModeEnabled = $derived(backColumns.length > 0);
|
|
20
|
+
// How many detail rows the skeleton should mimic
|
|
21
|
+
const skeletonDetailRows = $derived(Math.max(0, frontColumns.length - 2));
|
|
22
|
+
// ── Controlled/uncontrolled search ─────────────────────────────────────────
|
|
23
|
+
let internalSearchValue = $state("");
|
|
24
|
+
const resolvedSearchValue = $derived(searchValue ?? internalSearchValue);
|
|
25
|
+
function emitSearchValue(v) {
|
|
26
|
+
if (searchValue === undefined) internalSearchValue = v;
|
|
27
|
+
onSearchValueChange?.(v);
|
|
28
|
+
}
|
|
29
|
+
// ── Controlled/uncontrolled filters ───────────────────────────────────────
|
|
30
|
+
let internalFilters = $state({});
|
|
31
|
+
const resolvedFilters = $derived(filters ?? internalFilters);
|
|
32
|
+
function handleFilterChange(next) {
|
|
33
|
+
if (filters === undefined) internalFilters = next;
|
|
34
|
+
onFilterChange?.(next);
|
|
35
|
+
}
|
|
36
|
+
// ── Controlled/uncontrolled sort ──────────────────────────────────────────
|
|
37
|
+
let internalSortOrder = $state({});
|
|
38
|
+
const resolvedSortOrder = $derived(sortOrder ?? internalSortOrder);
|
|
39
|
+
function handleSortOrderChange(order) {
|
|
40
|
+
if (sortOrder === undefined) internalSortOrder = order;
|
|
41
|
+
onSortOrderChange?.(order);
|
|
42
|
+
}
|
|
43
|
+
const resolvedSort = $derived(Object.values(resolvedSortOrder).some((d) => d !== "none") ? sortOrderToSortOptions(resolvedSortOrder) : []);
|
|
44
|
+
// ── Controlled/uncontrolled selection ─────────────────────────────────────
|
|
45
|
+
let internalSelectedItems = $state([]);
|
|
46
|
+
const resolvedSelectedItems = $derived(selectedItemsProp ?? internalSelectedItems);
|
|
47
|
+
function setSelectedItems(items) {
|
|
48
|
+
if (selectedItemsProp === undefined) internalSelectedItems = items;
|
|
49
|
+
onSelectedItemsChange?.(items);
|
|
50
|
+
}
|
|
51
|
+
// ── Local data path ────────────────────────────────────────────────────────
|
|
52
|
+
let localData = $state([]);
|
|
53
|
+
let localLoading = $state(false);
|
|
54
|
+
$effect(() => {
|
|
55
|
+
if (dataSource !== "local") return;
|
|
56
|
+
if (typeof data === "function") {
|
|
57
|
+
localLoading = true;
|
|
58
|
+
data().then((d) => {
|
|
59
|
+
localData = d;
|
|
60
|
+
}).catch((e) => {
|
|
61
|
+
onLoadError?.(e instanceof Error ? e.message : "Failed to load data");
|
|
62
|
+
}).finally(() => {
|
|
63
|
+
localLoading = false;
|
|
64
|
+
});
|
|
65
|
+
} else if (Array.isArray(data)) {
|
|
66
|
+
localData = data;
|
|
67
|
+
} else {
|
|
68
|
+
localData = [];
|
|
179
69
|
}
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
70
|
+
});
|
|
71
|
+
const asCols = (cols) => cols;
|
|
72
|
+
const localSorted = $derived(applyLocalSort(localData, resolvedSort, asCols(columns)));
|
|
73
|
+
const localFiltered = $derived(applyLocalFilter(localSorted, resolvedFilters, asCols(columns)));
|
|
74
|
+
const localSearched = $derived.by(() => {
|
|
75
|
+
if (!resolvedSearchValue.trim() || serverSideSearch && isServerMode) return localFiltered;
|
|
76
|
+
const q = resolvedSearchValue.trim().toLowerCase();
|
|
77
|
+
const searchCols = searchColumns?.length ? searchColumns : columns.filter((c) => !c.disableSearch).map((c) => c.id);
|
|
78
|
+
return localFiltered.filter((row) => searchCols.some((colId) => {
|
|
79
|
+
const col = columns.find((c) => c.id === colId);
|
|
80
|
+
const key = col?.dataKey ?? colId;
|
|
81
|
+
const val = getNestedVal(key, row);
|
|
82
|
+
return val != null && String(val).toLowerCase().includes(q);
|
|
83
|
+
}));
|
|
84
|
+
});
|
|
85
|
+
function getNestedVal(key, row) {
|
|
86
|
+
const parts = key.split(".");
|
|
87
|
+
let val = row;
|
|
88
|
+
for (const part of parts) {
|
|
89
|
+
if (val == null || typeof val !== "object") return undefined;
|
|
90
|
+
val = val[part];
|
|
189
91
|
}
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
92
|
+
return val;
|
|
93
|
+
}
|
|
94
|
+
// ── Server state ───────────────────────────────────────────────────────────
|
|
95
|
+
let serverData = $state([]);
|
|
96
|
+
let serverTotal = $state(0);
|
|
97
|
+
let serverCursor = $state(undefined);
|
|
98
|
+
let serverFetchVersion = 0;
|
|
99
|
+
let serverLoading = $state(false);
|
|
100
|
+
let serverAllLoaded = $state(false);
|
|
101
|
+
let refreshTrigger = $state(0);
|
|
102
|
+
$effect(() => {
|
|
103
|
+
total = serverTotal;
|
|
104
|
+
});
|
|
105
|
+
$effect(() => {
|
|
106
|
+
loading = serverLoading || localLoading;
|
|
107
|
+
});
|
|
108
|
+
const isServerMode = $derived(dataSource !== "local");
|
|
109
|
+
const serverAdapter = $derived.by(() => {
|
|
110
|
+
if (!isServerMode) return null;
|
|
111
|
+
const url = dataSourceOptions?.url;
|
|
112
|
+
const schema = dataSourceOptions?.schema;
|
|
113
|
+
const entity = dataSourceOptions?.entity;
|
|
114
|
+
if (!url || !schema || !entity) return null;
|
|
115
|
+
const config = {
|
|
116
|
+
url,
|
|
117
|
+
token: dataSourceOptions?.authToken,
|
|
118
|
+
schema,
|
|
119
|
+
entity,
|
|
120
|
+
uniqueID: resolvedUniqueID,
|
|
121
|
+
extraOptions: dataSourceOptions?.extraOptions
|
|
122
|
+
};
|
|
123
|
+
return dataSource === "headerspec" ? new GridlerRestHeaderSpecAdapter(config) : new GridlerResolveSpecAdapter(config);
|
|
124
|
+
});
|
|
125
|
+
const resolvedFields = $derived.by(() => {
|
|
126
|
+
const colFields = columns.map((c) => c.dataKey ?? c.id);
|
|
127
|
+
const hotfields = dataSourceOptions?.hotfields ?? [];
|
|
128
|
+
const seen = new Set(colFields);
|
|
129
|
+
return [...colFields, ...hotfields.filter((f) => !seen.has(f))];
|
|
130
|
+
});
|
|
131
|
+
function buildAllFilters(search) {
|
|
132
|
+
const base = Object.keys(resolvedFilters).length ? gridColumnFiltersToFilterOptions(resolvedFilters) : [];
|
|
133
|
+
if (!search?.trim()) return base;
|
|
134
|
+
const searchCols = searchColumns?.length ? searchColumns : columns.filter((c) => !c.disableSearch).map((c) => c.id);
|
|
135
|
+
return [...base, ...buildSearchFilters(search, asCols([]), searchCols)];
|
|
136
|
+
}
|
|
137
|
+
$effect(() => {
|
|
138
|
+
const _adapter = serverAdapter;
|
|
139
|
+
const _sort = resolvedSort;
|
|
140
|
+
const _search = serverSideSearch ? resolvedSearchValue : null;
|
|
141
|
+
const _pageSize = pageSize;
|
|
142
|
+
const _filters = resolvedFilters;
|
|
143
|
+
const _refresh = refreshTrigger;
|
|
144
|
+
void _refresh;
|
|
145
|
+
if (!_adapter) return;
|
|
146
|
+
let cancelled = false;
|
|
147
|
+
serverFetchVersion++;
|
|
148
|
+
serverLoading = true;
|
|
149
|
+
serverData = [];
|
|
150
|
+
serverTotal = 0;
|
|
151
|
+
serverCursor = undefined;
|
|
152
|
+
serverAllLoaded = false;
|
|
153
|
+
_adapter.readPage(_pageSize, undefined, _sort, buildAllFilters(_search), untrack(() => resolvedFields)).then((result) => {
|
|
154
|
+
if (cancelled) return;
|
|
155
|
+
serverData = result.data;
|
|
156
|
+
serverTotal = result.total;
|
|
157
|
+
serverCursor = result.nextCursor;
|
|
158
|
+
serverAllLoaded = result.data.length < _pageSize || !result.nextCursor;
|
|
159
|
+
}).catch((e) => {
|
|
160
|
+
if (!cancelled) onLoadError?.(e instanceof Error ? e.message : "Failed to load data");
|
|
161
|
+
}).finally(() => {
|
|
162
|
+
if (!cancelled) serverLoading = false;
|
|
163
|
+
});
|
|
164
|
+
return () => {
|
|
165
|
+
cancelled = true;
|
|
166
|
+
};
|
|
167
|
+
});
|
|
168
|
+
async function loadNextPage() {
|
|
169
|
+
const _adapter = serverAdapter;
|
|
170
|
+
if (!_adapter || serverLoading || serverAllLoaded || !serverCursor) return;
|
|
171
|
+
const cursorSnapshot = serverCursor;
|
|
172
|
+
const versionSnapshot = serverFetchVersion;
|
|
173
|
+
serverLoading = true;
|
|
174
|
+
try {
|
|
175
|
+
const result = await _adapter.readPage(pageSize, cursorSnapshot, resolvedSort, buildAllFilters(serverSideSearch ? resolvedSearchValue : null), resolvedFields);
|
|
176
|
+
if (serverFetchVersion !== versionSnapshot) return;
|
|
177
|
+
serverData = [...serverData, ...result.data];
|
|
178
|
+
serverCursor = result.nextCursor;
|
|
179
|
+
serverAllLoaded = result.data.length < pageSize || !result.nextCursor || result.nextCursor === cursorSnapshot;
|
|
180
|
+
} catch (e) {
|
|
181
|
+
onLoadError?.(e instanceof Error ? e.message : "Failed to load page");
|
|
182
|
+
} finally {
|
|
183
|
+
serverLoading = false;
|
|
199
184
|
}
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
185
|
+
}
|
|
186
|
+
// ── Display data ───────────────────────────────────────────────────────────
|
|
187
|
+
const displayData = $derived(isServerMode ? serverData : localSearched);
|
|
188
|
+
// ── UI state ───────────────────────────────────────────────────────────────
|
|
189
|
+
let focusedIndex = $state(-1);
|
|
190
|
+
let anchorIndex = $state(-1);
|
|
191
|
+
let columnsInRow = $state(1);
|
|
192
|
+
let containerEl = $state(null);
|
|
193
|
+
let sentinelEl = $state(null);
|
|
194
|
+
let searchInputEl = $state(null);
|
|
195
|
+
let filterBtnEl = $state(null);
|
|
196
|
+
let menu = $state(null);
|
|
197
|
+
// Per-card flip state
|
|
198
|
+
let flippedCards = new SvelteSet();
|
|
199
|
+
function toggleFlip(e, item) {
|
|
200
|
+
e.stopPropagation();
|
|
201
|
+
const id = item[resolvedUniqueID];
|
|
202
|
+
if (flippedCards.has(id)) {
|
|
203
|
+
flippedCards.delete(id);
|
|
204
|
+
} else {
|
|
205
|
+
flippedCards.add(id);
|
|
215
206
|
}
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
$effect(() => {
|
|
223
|
-
if (dataSource !== 'local') return;
|
|
224
|
-
if (typeof data === 'function') {
|
|
225
|
-
localLoading = true;
|
|
226
|
-
(data as () => Promise<Record<string, unknown>[]>)()
|
|
227
|
-
.then((d) => { localData = d; })
|
|
228
|
-
.catch((e) => { onLoadError?.(e instanceof Error ? e.message : 'Failed to load data'); })
|
|
229
|
-
.finally(() => { localLoading = false; });
|
|
230
|
-
} else if (Array.isArray(data)) {
|
|
231
|
-
localData = data as Record<string, unknown>[];
|
|
232
|
-
} else {
|
|
233
|
-
localData = [];
|
|
234
|
-
}
|
|
207
|
+
}
|
|
208
|
+
$effect(() => {
|
|
209
|
+
if (!containerEl) return;
|
|
210
|
+
const obs = new ResizeObserver((entries) => {
|
|
211
|
+
columnsInRow = computeColumnsInRow(entries[0]?.contentRect.width ?? 0, cardMinWidth, cardGap);
|
|
235
212
|
});
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
const
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
213
|
+
obs.observe(containerEl);
|
|
214
|
+
return () => obs.disconnect();
|
|
215
|
+
});
|
|
216
|
+
$effect(() => {
|
|
217
|
+
if (!sentinelEl || !isServerMode) return;
|
|
218
|
+
const obs = new IntersectionObserver((entries) => {
|
|
219
|
+
if (entries[0]?.isIntersecting) loadNextPage();
|
|
220
|
+
}, { rootMargin: "200px" });
|
|
221
|
+
obs.observe(sentinelEl);
|
|
222
|
+
return () => obs.disconnect();
|
|
223
|
+
});
|
|
224
|
+
// ── Selection ──────────────────────────────────────────────────────────────
|
|
225
|
+
function handleCardClick(e, item, index) {
|
|
226
|
+
focusedIndex = index;
|
|
227
|
+
if (!multiSelect) {
|
|
228
|
+
setSelectedItems([item]);
|
|
229
|
+
anchorIndex = index;
|
|
230
|
+
onCardClick?.(item, index);
|
|
231
|
+
return;
|
|
232
|
+
}
|
|
233
|
+
if (e.shiftKey && anchorIndex >= 0) {
|
|
234
|
+
setSelectedItems(rangeSelect(displayData, anchorIndex, index));
|
|
235
|
+
} else if (e.ctrlKey || e.metaKey) {
|
|
236
|
+
setSelectedItems(toggleItemInSelection(resolvedSelectedItems, item, resolvedUniqueID));
|
|
237
|
+
anchorIndex = index;
|
|
238
|
+
} else {
|
|
239
|
+
setSelectedItems([item]);
|
|
240
|
+
anchorIndex = index;
|
|
241
|
+
}
|
|
242
|
+
onCardClick?.(item, index);
|
|
243
|
+
}
|
|
244
|
+
function handleCardContextMenu(e, item, index) {
|
|
245
|
+
e.preventDefault();
|
|
246
|
+
onCardContextMenu?.(item, index, e.clientX, e.clientY);
|
|
247
|
+
menu?.show("cg-context-menu", {
|
|
248
|
+
x: e.clientX,
|
|
249
|
+
y: e.clientY,
|
|
250
|
+
items: buildContextMenuItems(item)
|
|
257
251
|
});
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
252
|
+
}
|
|
253
|
+
function handleContainerClick(e) {
|
|
254
|
+
if (e.target === e.currentTarget) {
|
|
255
|
+
setSelectedItems([]);
|
|
256
|
+
focusedIndex = -1;
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
// ── Context menu ───────────────────────────────────────────────────────────
|
|
260
|
+
function buildContextMenuItems(item) {
|
|
261
|
+
const items = [];
|
|
262
|
+
if (menuItems?.length) {
|
|
263
|
+
for (const mi of menuItems) {
|
|
264
|
+
if (mi.kind === "separator") {
|
|
265
|
+
items.push({ isDivider: true });
|
|
266
|
+
} else {
|
|
267
|
+
items.push({
|
|
268
|
+
label: mi.label,
|
|
269
|
+
onClick: mi.disabled ? undefined : () => mi.onselect?.(item)
|
|
270
|
+
});
|
|
271
|
+
}
|
|
265
272
|
}
|
|
266
|
-
|
|
273
|
+
items.push({ isDivider: true });
|
|
267
274
|
}
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
let serverData = $state<Record<string, unknown>[]>([]);
|
|
272
|
-
let serverTotal = $state(0);
|
|
273
|
-
let serverCursor = $state<string | undefined>(undefined);
|
|
274
|
-
let serverFetchVersion = 0;
|
|
275
|
-
let serverLoading = $state(false);
|
|
276
|
-
let serverAllLoaded = $state(false);
|
|
277
|
-
let refreshTrigger = $state(0);
|
|
278
|
-
|
|
279
|
-
$effect(() => { total = serverTotal; });
|
|
280
|
-
$effect(() => { loading = serverLoading || localLoading; });
|
|
281
|
-
|
|
282
|
-
const isServerMode = $derived(dataSource !== 'local');
|
|
283
|
-
|
|
284
|
-
const serverAdapter = $derived.by(() => {
|
|
285
|
-
if (!isServerMode) return null;
|
|
286
|
-
const url = dataSourceOptions?.url;
|
|
287
|
-
const schema = dataSourceOptions?.schema;
|
|
288
|
-
const entity = dataSourceOptions?.entity;
|
|
289
|
-
if (!url || !schema || !entity) return null;
|
|
290
|
-
const config: GridlerAdapterConfig = {
|
|
291
|
-
url, token: dataSourceOptions?.authToken, schema, entity,
|
|
292
|
-
uniqueID: resolvedUniqueID,
|
|
293
|
-
extraOptions: dataSourceOptions?.extraOptions as GridlerAdapterConfig['extraOptions'],
|
|
294
|
-
};
|
|
295
|
-
return dataSource === 'headerspec'
|
|
296
|
-
? new GridlerRestHeaderSpecAdapter(config)
|
|
297
|
-
: new GridlerResolveSpecAdapter(config);
|
|
275
|
+
items.push({
|
|
276
|
+
label: "Refresh",
|
|
277
|
+
onClick: () => refresh()
|
|
298
278
|
});
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
const hotfields = dataSourceOptions?.hotfields ?? [];
|
|
303
|
-
const seen = new Set(colFields);
|
|
304
|
-
return [...colFields, ...hotfields.filter((f) => !seen.has(f))];
|
|
279
|
+
if (resolvedSearchValue) items.push({
|
|
280
|
+
label: "Clear search",
|
|
281
|
+
onClick: () => emitSearchValue("")
|
|
305
282
|
});
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
? gridColumnFiltersToFilterOptions(resolvedFilters) : [];
|
|
310
|
-
if (!search?.trim()) return base;
|
|
311
|
-
const searchCols = searchColumns?.length
|
|
312
|
-
? searchColumns
|
|
313
|
-
: columns.filter((c) => !c.disableSearch).map((c) => c.id);
|
|
314
|
-
return [...base, ...buildSearchFilters(search, asCols([]), searchCols)];
|
|
315
|
-
}
|
|
316
|
-
|
|
317
|
-
$effect(() => {
|
|
318
|
-
const _adapter = serverAdapter;
|
|
319
|
-
const _sort = resolvedSort;
|
|
320
|
-
const _search = serverSideSearch ? resolvedSearchValue : null;
|
|
321
|
-
const _pageSize = pageSize;
|
|
322
|
-
const _filters = resolvedFilters;
|
|
323
|
-
const _refresh = refreshTrigger;
|
|
324
|
-
void _refresh;
|
|
325
|
-
|
|
326
|
-
if (!_adapter) return;
|
|
327
|
-
|
|
328
|
-
let cancelled = false;
|
|
329
|
-
serverFetchVersion++;
|
|
330
|
-
serverLoading = true;
|
|
331
|
-
serverData = [];
|
|
332
|
-
serverTotal = 0;
|
|
333
|
-
serverCursor = undefined;
|
|
334
|
-
serverAllLoaded = false;
|
|
335
|
-
|
|
336
|
-
_adapter
|
|
337
|
-
.readPage(_pageSize, undefined, _sort, buildAllFilters(_search), untrack(() => resolvedFields))
|
|
338
|
-
.then((result) => {
|
|
339
|
-
if (cancelled) return;
|
|
340
|
-
serverData = result.data;
|
|
341
|
-
serverTotal = result.total;
|
|
342
|
-
serverCursor = result.nextCursor;
|
|
343
|
-
serverAllLoaded = result.data.length < _pageSize || !result.nextCursor;
|
|
344
|
-
})
|
|
345
|
-
.catch((e) => { if (!cancelled) onLoadError?.(e instanceof Error ? e.message : 'Failed to load data'); })
|
|
346
|
-
.finally(() => { if (!cancelled) serverLoading = false; });
|
|
347
|
-
|
|
348
|
-
return () => { cancelled = true; };
|
|
283
|
+
if (Object.keys(resolvedFilters).length > 0) items.push({
|
|
284
|
+
label: "Clear filters",
|
|
285
|
+
onClick: () => handleFilterChange({})
|
|
349
286
|
});
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
if (!_adapter || serverLoading || serverAllLoaded || !serverCursor) return;
|
|
354
|
-
const cursorSnapshot = serverCursor;
|
|
355
|
-
const versionSnapshot = serverFetchVersion;
|
|
356
|
-
serverLoading = true;
|
|
357
|
-
try {
|
|
358
|
-
const result = await _adapter.readPage(
|
|
359
|
-
pageSize, cursorSnapshot, resolvedSort,
|
|
360
|
-
buildAllFilters(serverSideSearch ? resolvedSearchValue : null), resolvedFields,
|
|
361
|
-
);
|
|
362
|
-
if (serverFetchVersion !== versionSnapshot) return;
|
|
363
|
-
serverData = [...serverData, ...result.data];
|
|
364
|
-
serverCursor = result.nextCursor;
|
|
365
|
-
serverAllLoaded = result.data.length < pageSize || !result.nextCursor || result.nextCursor === cursorSnapshot;
|
|
366
|
-
} catch (e) {
|
|
367
|
-
onLoadError?.(e instanceof Error ? e.message : 'Failed to load page');
|
|
368
|
-
} finally {
|
|
369
|
-
serverLoading = false;
|
|
370
|
-
}
|
|
371
|
-
}
|
|
372
|
-
|
|
373
|
-
// ── Display data ───────────────────────────────────────────────────────────
|
|
374
|
-
|
|
375
|
-
const displayData = $derived(isServerMode ? serverData : localSearched);
|
|
376
|
-
|
|
377
|
-
// ── UI state ───────────────────────────────────────────────────────────────
|
|
378
|
-
|
|
379
|
-
let focusedIndex = $state(-1);
|
|
380
|
-
let anchorIndex = $state(-1);
|
|
381
|
-
let columnsInRow = $state(1);
|
|
382
|
-
let containerEl = $state<HTMLElement | null>(null);
|
|
383
|
-
let sentinelEl = $state<HTMLElement | null>(null);
|
|
384
|
-
let searchInputEl = $state<HTMLInputElement | null>(null);
|
|
385
|
-
let filterBtnEl = $state<HTMLButtonElement | null>(null);
|
|
386
|
-
let menu = $state<BetterMenu | null>(null);
|
|
387
|
-
|
|
388
|
-
// Per-card flip state
|
|
389
|
-
let flippedCards = $state(new Set<unknown>());
|
|
390
|
-
|
|
391
|
-
function toggleFlip(e: MouseEvent, item: Record<string, unknown>) {
|
|
392
|
-
e.stopPropagation();
|
|
393
|
-
const id = item[resolvedUniqueID];
|
|
394
|
-
const next = new Set(flippedCards);
|
|
395
|
-
next.has(id) ? next.delete(id) : next.add(id);
|
|
396
|
-
flippedCards = next;
|
|
397
|
-
}
|
|
398
|
-
|
|
399
|
-
$effect(() => {
|
|
400
|
-
if (!containerEl) return;
|
|
401
|
-
const obs = new ResizeObserver((entries) => {
|
|
402
|
-
columnsInRow = computeColumnsInRow(entries[0]?.contentRect.width ?? 0, cardMinWidth, cardGap);
|
|
403
|
-
});
|
|
404
|
-
obs.observe(containerEl);
|
|
405
|
-
return () => obs.disconnect();
|
|
287
|
+
if (Object.values(resolvedSortOrder).some((d) => d !== "none")) items.push({
|
|
288
|
+
label: "Clear sort",
|
|
289
|
+
onClick: () => handleSortOrderChange({})
|
|
406
290
|
});
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
291
|
+
return items;
|
|
292
|
+
}
|
|
293
|
+
// ── Filter panel ───────────────────────────────────────────────────────────
|
|
294
|
+
const activeFilterCount = $derived(Object.keys(resolvedFilters).length);
|
|
295
|
+
function openFilterPanel() {
|
|
296
|
+
if (!filterBtnEl) return;
|
|
297
|
+
const rect = filterBtnEl.getBoundingClientRect();
|
|
298
|
+
menu?.show("cg-filter-panel", {
|
|
299
|
+
x: rect.left,
|
|
300
|
+
y: rect.bottom + 4,
|
|
301
|
+
renderer: filterPanelSnippet
|
|
416
302
|
});
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
setSelectedItems([item]);
|
|
435
|
-
anchorIndex = index;
|
|
436
|
-
}
|
|
437
|
-
onCardClick?.(item, index);
|
|
438
|
-
}
|
|
439
|
-
|
|
440
|
-
function handleCardContextMenu(e: MouseEvent, item: Record<string, unknown>, index: number) {
|
|
441
|
-
e.preventDefault();
|
|
442
|
-
onCardContextMenu?.(item, index, e.clientX, e.clientY);
|
|
443
|
-
menu?.show('cg-context-menu', {
|
|
444
|
-
x: e.clientX, y: e.clientY,
|
|
445
|
-
items: buildContextMenuItems(item),
|
|
446
|
-
});
|
|
447
|
-
}
|
|
448
|
-
|
|
449
|
-
function handleContainerClick(e: MouseEvent) {
|
|
450
|
-
if (e.target === e.currentTarget) {
|
|
451
|
-
setSelectedItems([]);
|
|
452
|
-
focusedIndex = -1;
|
|
453
|
-
}
|
|
303
|
+
}
|
|
304
|
+
// ── Sort pills ─────────────────────────────────────────────────────────────
|
|
305
|
+
function toggleSortOption(columnId) {
|
|
306
|
+
const current = resolvedSortOrder[columnId] ?? "none";
|
|
307
|
+
const next = current === "none" ? "asc" : current === "asc" ? "desc" : "none";
|
|
308
|
+
const newOrder = { ...resolvedSortOrder };
|
|
309
|
+
if (next === "none") delete newOrder[columnId];
|
|
310
|
+
else newOrder[columnId] = next;
|
|
311
|
+
handleSortOrderChange(newOrder);
|
|
312
|
+
}
|
|
313
|
+
// ── Keyboard navigation ────────────────────────────────────────────────────
|
|
314
|
+
function moveFocus(next) {
|
|
315
|
+
focusedIndex = next;
|
|
316
|
+
scrollToCard(next);
|
|
317
|
+
if (!multiSelect) {
|
|
318
|
+
const item = displayData[next];
|
|
319
|
+
if (item) setSelectedItems([item]);
|
|
454
320
|
}
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
321
|
+
}
|
|
322
|
+
function handleKeyDown(e) {
|
|
323
|
+
const count = displayData.length;
|
|
324
|
+
if (count === 0) return;
|
|
325
|
+
switch (e.key) {
|
|
326
|
+
case "ArrowLeft":
|
|
327
|
+
e.preventDefault();
|
|
328
|
+
moveFocus(Math.max(0, focusedIndex - 1));
|
|
329
|
+
break;
|
|
330
|
+
case "ArrowRight":
|
|
331
|
+
e.preventDefault();
|
|
332
|
+
moveFocus(Math.min(count - 1, focusedIndex + 1));
|
|
333
|
+
break;
|
|
334
|
+
case "ArrowUp":
|
|
335
|
+
e.preventDefault();
|
|
336
|
+
moveFocus(Math.max(0, focusedIndex - columnsInRow));
|
|
337
|
+
break;
|
|
338
|
+
case "ArrowDown":
|
|
339
|
+
e.preventDefault();
|
|
340
|
+
moveFocus(Math.min(count - 1, focusedIndex + columnsInRow));
|
|
341
|
+
break;
|
|
342
|
+
case "Tab":
|
|
343
|
+
e.preventDefault();
|
|
344
|
+
moveFocus(e.shiftKey ? Math.max(0, focusedIndex - 1) : Math.min(count - 1, focusedIndex + 1));
|
|
345
|
+
break;
|
|
346
|
+
case "Enter": {
|
|
347
|
+
if (focusedIndex >= 0) {
|
|
348
|
+
const item = displayData[focusedIndex];
|
|
349
|
+
if (item) onCardClick?.(item, focusedIndex);
|
|
467
350
|
}
|
|
468
|
-
|
|
351
|
+
break;
|
|
469
352
|
}
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
return items;
|
|
478
|
-
}
|
|
479
|
-
|
|
480
|
-
// ── Filter panel ───────────────────────────────────────────────────────────
|
|
481
|
-
|
|
482
|
-
const activeFilterCount = $derived(Object.keys(resolvedFilters).length);
|
|
483
|
-
|
|
484
|
-
function openFilterPanel() {
|
|
485
|
-
if (!filterBtnEl) return;
|
|
486
|
-
const rect = filterBtnEl.getBoundingClientRect();
|
|
487
|
-
menu?.show('cg-filter-panel', {
|
|
488
|
-
x: rect.left, y: rect.bottom + 4,
|
|
489
|
-
renderer: filterPanelSnippet as unknown as (props: Record<string, unknown>) => unknown,
|
|
490
|
-
});
|
|
491
|
-
}
|
|
492
|
-
|
|
493
|
-
// ── Sort pills ─────────────────────────────────────────────────────────────
|
|
494
|
-
|
|
495
|
-
function toggleSortOption(columnId: string) {
|
|
496
|
-
const current = resolvedSortOrder[columnId] ?? 'none';
|
|
497
|
-
const next = current === 'none' ? 'asc' : current === 'asc' ? 'desc' : ('none' as const);
|
|
498
|
-
const newOrder = { ...resolvedSortOrder };
|
|
499
|
-
if (next === 'none') delete newOrder[columnId]; else newOrder[columnId] = next;
|
|
500
|
-
handleSortOrderChange(newOrder);
|
|
501
|
-
}
|
|
502
|
-
|
|
503
|
-
// ── Keyboard navigation ────────────────────────────────────────────────────
|
|
504
|
-
|
|
505
|
-
function moveFocus(next: number) {
|
|
506
|
-
focusedIndex = next;
|
|
507
|
-
scrollToCard(next);
|
|
508
|
-
if (!multiSelect) {
|
|
509
|
-
const item = displayData[next];
|
|
510
|
-
if (item) setSelectedItems([item]);
|
|
353
|
+
case " ": {
|
|
354
|
+
e.preventDefault();
|
|
355
|
+
if (focusedIndex >= 0) {
|
|
356
|
+
const item = displayData[focusedIndex];
|
|
357
|
+
if (item) setSelectedItems(multiSelect ? toggleItemInSelection(resolvedSelectedItems, item, resolvedUniqueID) : [item]);
|
|
358
|
+
}
|
|
359
|
+
break;
|
|
511
360
|
}
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
case 'ArrowRight': e.preventDefault(); moveFocus(Math.min(count - 1, focusedIndex + 1)); break;
|
|
520
|
-
case 'ArrowUp': e.preventDefault(); moveFocus(Math.max(0, focusedIndex - columnsInRow)); break;
|
|
521
|
-
case 'ArrowDown': e.preventDefault(); moveFocus(Math.min(count - 1, focusedIndex + columnsInRow)); break;
|
|
522
|
-
case 'Tab':
|
|
361
|
+
case "Escape":
|
|
362
|
+
setSelectedItems([]);
|
|
363
|
+
focusedIndex = -1;
|
|
364
|
+
break;
|
|
365
|
+
case "a":
|
|
366
|
+
case "A":
|
|
367
|
+
if ((e.ctrlKey || e.metaKey) && multiSelect) {
|
|
523
368
|
e.preventDefault();
|
|
524
|
-
|
|
525
|
-
break;
|
|
526
|
-
case 'Enter': {
|
|
527
|
-
if (focusedIndex >= 0) { const item = displayData[focusedIndex]; if (item) onCardClick?.(item, focusedIndex); }
|
|
528
|
-
break;
|
|
369
|
+
setSelectedItems([...displayData]);
|
|
529
370
|
}
|
|
530
|
-
|
|
371
|
+
break;
|
|
372
|
+
case "f":
|
|
373
|
+
case "F":
|
|
374
|
+
if (e.ctrlKey || e.metaKey) {
|
|
531
375
|
e.preventDefault();
|
|
532
|
-
|
|
533
|
-
const item = displayData[focusedIndex];
|
|
534
|
-
if (item) setSelectedItems(multiSelect
|
|
535
|
-
? toggleItemInSelection(resolvedSelectedItems, item, resolvedUniqueID)
|
|
536
|
-
: [item]);
|
|
537
|
-
}
|
|
538
|
-
break;
|
|
376
|
+
searchInputEl?.focus();
|
|
539
377
|
}
|
|
540
|
-
|
|
541
|
-
case 'a': case 'A':
|
|
542
|
-
if ((e.ctrlKey || e.metaKey) && multiSelect) { e.preventDefault(); setSelectedItems([...displayData]); }
|
|
543
|
-
break;
|
|
544
|
-
case 'f': case 'F':
|
|
545
|
-
if (e.ctrlKey || e.metaKey) { e.preventDefault(); searchInputEl?.focus(); }
|
|
546
|
-
break;
|
|
547
|
-
}
|
|
378
|
+
break;
|
|
548
379
|
}
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
380
|
+
}
|
|
381
|
+
function scrollToCard(index) {
|
|
382
|
+
if (virtualizerInstance) {
|
|
383
|
+
const rowIndex = Math.floor(index / Math.max(1, columnsInRow));
|
|
384
|
+
virtualizerInstance.scrollToIndex(rowIndex, {
|
|
385
|
+
align: "auto",
|
|
386
|
+
behavior: "smooth"
|
|
387
|
+
});
|
|
388
|
+
} else {
|
|
389
|
+
containerEl?.querySelector(`[data-card-index="${index}"]`)?.scrollIntoView({
|
|
390
|
+
block: "nearest",
|
|
391
|
+
behavior: "smooth"
|
|
392
|
+
});
|
|
558
393
|
}
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
394
|
+
}
|
|
395
|
+
// ── Public API ─────────────────────────────────────────────────────────────
|
|
396
|
+
export function refresh() {
|
|
397
|
+
if (isServerMode) {
|
|
398
|
+
refreshTrigger++;
|
|
399
|
+
} else if (typeof data === "function") {
|
|
400
|
+
localLoading = true;
|
|
401
|
+
data().then((d) => {
|
|
402
|
+
localData = d;
|
|
403
|
+
}).catch((e) => {
|
|
404
|
+
onLoadError?.(e instanceof Error ? e.message : "Failed to load data");
|
|
405
|
+
}).finally(() => {
|
|
406
|
+
localLoading = false;
|
|
407
|
+
});
|
|
572
408
|
}
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
409
|
+
}
|
|
410
|
+
// ── Loading states ─────────────────────────────────────────────────────────
|
|
411
|
+
const isFirstLoad = $derived(isServerMode && serverLoading && serverData.length === 0 || !isServerMode && localLoading && localData.length === 0);
|
|
412
|
+
const isLoadingMore = $derived(isServerMode && serverLoading && serverData.length > 0);
|
|
413
|
+
const isEmpty = $derived(!isFirstLoad && displayData.length === 0);
|
|
414
|
+
// Cap skeleton count so we don't render 200 ghost cards
|
|
415
|
+
const skeletonCount = $derived(Math.min(pageSize, 8));
|
|
416
|
+
// ── Virtualizer ────────────────────────────────────────────────────────────
|
|
417
|
+
// Group flat displayData into rows of columnsInRow items each
|
|
418
|
+
const virtualRows = $derived.by(() => {
|
|
419
|
+
const cols = Math.max(1, columnsInRow);
|
|
420
|
+
const rows = [];
|
|
421
|
+
for (let i = 0; i < displayData.length; i += cols) {
|
|
422
|
+
rows.push(displayData.slice(i, i + cols));
|
|
423
|
+
}
|
|
424
|
+
return rows;
|
|
425
|
+
});
|
|
426
|
+
// createVirtualizer returns a Svelte 4 store that calls subscribe with the
|
|
427
|
+
// same object reference on every scroll update. Svelte 5 $state uses Object.is
|
|
428
|
+
// equality, so assigning the same reference never triggers reactivity.
|
|
429
|
+
// Fix: extract the values we need inside the subscribe callback directly into
|
|
430
|
+
// $state variables — getVirtualItems() returns a new array each call, and
|
|
431
|
+
// getTotalSize() is a primitive, so both reliably trigger Svelte updates.
|
|
432
|
+
let virtualizerInstance = $state(null);
|
|
433
|
+
let virtualItems = $state([]);
|
|
434
|
+
let totalSize = $state(0);
|
|
435
|
+
$effect(() => {
|
|
436
|
+
const el = containerEl;
|
|
437
|
+
if (!el) {
|
|
438
|
+
virtualizerInstance = null;
|
|
439
|
+
virtualItems = [];
|
|
440
|
+
totalSize = 0;
|
|
441
|
+
return;
|
|
442
|
+
}
|
|
443
|
+
const rowCount = untrack(() => virtualRows.length);
|
|
444
|
+
const gap = untrack(() => cardGap);
|
|
445
|
+
const virt = createVirtualizer({
|
|
446
|
+
count: rowCount,
|
|
447
|
+
estimateSize: () => 220 + gap,
|
|
448
|
+
overscan: 2,
|
|
449
|
+
getScrollElement: () => el
|
|
592
450
|
});
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
// Fix: extract the values we need inside the subscribe callback directly into
|
|
598
|
-
// $state variables — getVirtualItems() returns a new array each call, and
|
|
599
|
-
// getTotalSize() is a primitive, so both reliably trigger Svelte updates.
|
|
600
|
-
let virtualizerInstance = $state<SvelteVirtualizer<HTMLElement, HTMLElement> | null>(null);
|
|
601
|
-
let virtualItems = $state<ReturnType<SvelteVirtualizer<HTMLElement, HTMLElement>['getVirtualItems']>>([]);
|
|
602
|
-
let totalSize = $state(0);
|
|
603
|
-
|
|
604
|
-
$effect(() => {
|
|
605
|
-
const el = containerEl;
|
|
606
|
-
if (!el) {
|
|
607
|
-
virtualizerInstance = null;
|
|
608
|
-
virtualItems = [];
|
|
609
|
-
totalSize = 0;
|
|
610
|
-
return;
|
|
611
|
-
}
|
|
612
|
-
const rowCount = untrack(() => virtualRows.length);
|
|
613
|
-
const gap = untrack(() => cardGap);
|
|
614
|
-
const virt = createVirtualizer<HTMLElement, HTMLElement>({
|
|
615
|
-
count: rowCount,
|
|
616
|
-
estimateSize: () => 220 + gap,
|
|
617
|
-
overscan: 2,
|
|
618
|
-
getScrollElement: () => el,
|
|
619
|
-
});
|
|
620
|
-
return virt.subscribe((v) => {
|
|
621
|
-
virtualizerInstance = v;
|
|
622
|
-
virtualItems = v.getVirtualItems();
|
|
623
|
-
totalSize = v.getTotalSize();
|
|
624
|
-
});
|
|
451
|
+
return virt.subscribe((v) => {
|
|
452
|
+
virtualizerInstance = v;
|
|
453
|
+
virtualItems = v.getVirtualItems();
|
|
454
|
+
totalSize = v.getTotalSize();
|
|
625
455
|
});
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
});
|
|
456
|
+
});
|
|
457
|
+
// Update virtualizer when row count or gap changes.
|
|
458
|
+
// Untracked read of virtualizerInstance so scroll events don't re-trigger this.
|
|
459
|
+
$effect(() => {
|
|
460
|
+
const count = virtualRows.length;
|
|
461
|
+
const gap = cardGap;
|
|
462
|
+
untrack(() => {
|
|
463
|
+
if (!virtualizerInstance || !containerEl) return;
|
|
464
|
+
virtualizerInstance.setOptions({
|
|
465
|
+
...virtualizerInstance.options,
|
|
466
|
+
count,
|
|
467
|
+
estimateSize: () => 220 + gap,
|
|
468
|
+
getScrollElement: () => containerEl
|
|
640
469
|
});
|
|
641
470
|
});
|
|
471
|
+
});
|
|
642
472
|
</script>
|
|
643
473
|
|
|
644
474
|
{#snippet filterPanelSnippet()}
|
|
@@ -734,7 +564,6 @@
|
|
|
734
564
|
</div>
|
|
735
565
|
|
|
736
566
|
<!-- ── Card grid ────────────────────────────────────────────────────── -->
|
|
737
|
-
<!-- svelte-ignore a11y_interactive_supports_focus -->
|
|
738
567
|
<div
|
|
739
568
|
bind:this={containerEl}
|
|
740
569
|
class="cg-grid"
|