@svadmin/ui 0.38.5 → 0.38.7
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.
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
|
-
import { untrack } from 'svelte';
|
|
2
|
+
import { onDestroy, untrack } from 'svelte';
|
|
3
3
|
import { cn } from '../utils.js';
|
|
4
4
|
import {
|
|
5
5
|
createTable,
|
|
@@ -10,23 +10,21 @@
|
|
|
10
10
|
type ColumnVisibilityState,
|
|
11
11
|
type ExpandedState,
|
|
12
12
|
} from '@tanstack/svelte-table';
|
|
13
|
-
import type { Column, Header, TableFeatures
|
|
13
|
+
import type { Column, Header, TableFeatures } from '@tanstack/table-core';
|
|
14
|
+
import { createAtom, useSelector } from '@tanstack/svelte-store';
|
|
14
15
|
import {
|
|
15
16
|
column_getCanSort,
|
|
16
17
|
column_getIsSorted,
|
|
17
18
|
column_toggleSorting,
|
|
18
|
-
column_getIsVisible,
|
|
19
19
|
column_toggleVisibility,
|
|
20
20
|
header_getSize,
|
|
21
21
|
table_getRowModel,
|
|
22
|
-
table_getHeaderGroups,
|
|
23
22
|
table_getAllLeafColumns,
|
|
24
23
|
table_getIsAllRowsSelected,
|
|
25
24
|
table_toggleAllRowsSelected,
|
|
26
25
|
row_getIsSelected,
|
|
27
26
|
row_toggleSelected,
|
|
28
27
|
row_getVisibleCells,
|
|
29
|
-
row_getIsExpanded,
|
|
30
28
|
row_toggleExpanded,
|
|
31
29
|
cell_getValue,
|
|
32
30
|
} from '@tanstack/table-core/static-functions';
|
|
@@ -130,6 +128,22 @@
|
|
|
130
128
|
);
|
|
131
129
|
let filters = $state<Filter[]>([]);
|
|
132
130
|
let searchText = $state(urlState.search ?? '');
|
|
131
|
+
let appliedSearchText = $state(untrack(() => searchText));
|
|
132
|
+
let searchDebounceTimer: ReturnType<typeof setTimeout> | undefined;
|
|
133
|
+
|
|
134
|
+
function scheduleSearch(event: Event) {
|
|
135
|
+
searchText = (event.currentTarget as HTMLInputElement).value;
|
|
136
|
+
if (searchDebounceTimer) clearTimeout(searchDebounceTimer);
|
|
137
|
+
searchDebounceTimer = setTimeout(() => {
|
|
138
|
+
appliedSearchText = searchText;
|
|
139
|
+
pagination = { ...pagination, current: 1 };
|
|
140
|
+
searchDebounceTimer = undefined;
|
|
141
|
+
}, 300);
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
onDestroy(() => {
|
|
145
|
+
if (searchDebounceTimer) clearTimeout(searchDebounceTimer);
|
|
146
|
+
});
|
|
133
147
|
|
|
134
148
|
// Sync external controlled state
|
|
135
149
|
$effect(() => {
|
|
@@ -151,7 +165,7 @@
|
|
|
151
165
|
pageSize: pagination.pageSize,
|
|
152
166
|
sortField: sorters[0]?.field,
|
|
153
167
|
sortOrder: sorters[0]?.order,
|
|
154
|
-
search:
|
|
168
|
+
search: appliedSearchText || undefined,
|
|
155
169
|
}, adminContext);
|
|
156
170
|
});
|
|
157
171
|
|
|
@@ -162,16 +176,16 @@
|
|
|
162
176
|
const activeFilterCount = $derived(Object.values(filterValues).filter(v => v.trim()).length);
|
|
163
177
|
const activeFilters = $derived.by(() => {
|
|
164
178
|
const result: Filter[] = [...filters];
|
|
165
|
-
if (
|
|
179
|
+
if (appliedSearchText.trim() && searchableFields.length > 0) {
|
|
166
180
|
if (searchableFields.length === 1) {
|
|
167
|
-
result.push({ field: searchableFields[0].key, operator: 'contains', value:
|
|
181
|
+
result.push({ field: searchableFields[0].key, operator: 'contains', value: appliedSearchText });
|
|
168
182
|
} else {
|
|
169
183
|
const searchFilter: LogicalFilter = {
|
|
170
184
|
operator: 'or',
|
|
171
185
|
value: searchableFields.map(f => ({
|
|
172
186
|
field: f.key,
|
|
173
187
|
operator: 'contains',
|
|
174
|
-
value:
|
|
188
|
+
value: appliedSearchText
|
|
175
189
|
}))
|
|
176
190
|
};
|
|
177
191
|
result.push(searchFilter);
|
|
@@ -251,10 +265,10 @@
|
|
|
251
265
|
const canExport = $derived(canExportPerm.allowed);
|
|
252
266
|
|
|
253
267
|
// ─── TanStack Table state ────────────────────────────────────
|
|
254
|
-
|
|
268
|
+
const initialSorting = untrack<SortingState>(() =>
|
|
255
269
|
sorters.map(s => ({ id: s.field, desc: s.order === 'desc' }))
|
|
256
|
-
)
|
|
257
|
-
|
|
270
|
+
);
|
|
271
|
+
const initialColumnVisibility = (() => {
|
|
258
272
|
// Try to restore from localStorage first
|
|
259
273
|
const storageKey = `svadmin-columns-${resourceName}`;
|
|
260
274
|
if (typeof window !== 'undefined') {
|
|
@@ -269,79 +283,41 @@
|
|
|
269
283
|
if (f.showInList === false) vis[f.key] = false;
|
|
270
284
|
}
|
|
271
285
|
return vis;
|
|
272
|
-
})()
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
286
|
+
})();
|
|
287
|
+
const sortingAtom = createAtom(initialSorting);
|
|
288
|
+
const columnVisibilityAtom = createAtom(initialColumnVisibility);
|
|
289
|
+
const rowSelectionAtom = createAtom({} as RowSelectionState);
|
|
290
|
+
const expandedAtom = createAtom({} as ExpandedState);
|
|
291
|
+
const columnOrderAtom = createAtom([] as string[]);
|
|
292
|
+
|
|
293
|
+
const tableSorting = useSelector(sortingAtom);
|
|
294
|
+
const tableColumnVisibility = useSelector(columnVisibilityAtom);
|
|
295
|
+
const tableRowSelection = useSelector(rowSelectionAtom);
|
|
296
|
+
const tableExpanded = useSelector(expandedAtom);
|
|
297
|
+
const tableColumnOrder = useSelector(columnOrderAtom);
|
|
298
|
+
|
|
299
|
+
function rowIsExpanded(rowId: string): boolean {
|
|
300
|
+
const expanded = tableExpanded.current;
|
|
301
|
+
return expanded === true || expanded[rowId] === true;
|
|
279
302
|
}
|
|
280
303
|
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
const tableRowSelection = $derived<RowSelectionState>({ ...rowSelection });
|
|
287
|
-
const tableExpanded = $derived<ExpandedState>(expanded === true ? true : { ...expanded });
|
|
288
|
-
const tableColumnOrder = $derived<string[]>([...columnOrder]);
|
|
289
|
-
|
|
290
|
-
const sortingAtom = {
|
|
291
|
-
get: () => tableSorting,
|
|
292
|
-
set: (updater: Updater<SortingState>) => {
|
|
293
|
-
const nextSorting = applyTableUpdater(tableSorting, updater);
|
|
294
|
-
sorting = nextSorting;
|
|
295
|
-
|
|
296
|
-
const nextSorters: Sort[] = nextSorting.map((s) => ({
|
|
297
|
-
field: s.id,
|
|
298
|
-
order: s.desc ? 'desc' as const : 'asc' as const,
|
|
299
|
-
}));
|
|
300
|
-
|
|
301
|
-
if (JSON.stringify(nextSorters) !== JSON.stringify(sorters)) {
|
|
302
|
-
sorters = nextSorters;
|
|
303
|
-
}
|
|
304
|
-
|
|
305
|
-
if ((pagination.current ?? 1) !== 1) {
|
|
306
|
-
pagination = { ...pagination, current: 1 };
|
|
307
|
-
}
|
|
308
|
-
},
|
|
309
|
-
};
|
|
310
|
-
|
|
311
|
-
const columnVisibilityAtom = {
|
|
312
|
-
get: () => tableColumnVisibility,
|
|
313
|
-
set: (updater: Updater<ColumnVisibilityState>) => {
|
|
314
|
-
columnVisibility = applyTableUpdater(tableColumnVisibility, updater);
|
|
315
|
-
},
|
|
316
|
-
};
|
|
317
|
-
|
|
318
|
-
const rowSelectionAtom = {
|
|
319
|
-
get: () => tableRowSelection,
|
|
320
|
-
set: (updater: Updater<RowSelectionState>) => {
|
|
321
|
-
rowSelection = applyTableUpdater(tableRowSelection, updater);
|
|
322
|
-
},
|
|
323
|
-
};
|
|
324
|
-
|
|
325
|
-
const expandedAtom = {
|
|
326
|
-
get: () => tableExpanded,
|
|
327
|
-
set: (updater: Updater<ExpandedState>) => {
|
|
328
|
-
expanded = applyTableUpdater(tableExpanded, updater);
|
|
329
|
-
},
|
|
330
|
-
};
|
|
304
|
+
$effect(() => {
|
|
305
|
+
const nextSorters: Sort[] = tableSorting.current.map((sorter) => ({
|
|
306
|
+
field: sorter.id,
|
|
307
|
+
order: sorter.desc ? 'desc' as const : 'asc' as const,
|
|
308
|
+
}));
|
|
331
309
|
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
},
|
|
337
|
-
};
|
|
310
|
+
if (JSON.stringify(nextSorters) !== JSON.stringify(sorters)) {
|
|
311
|
+
sorters = nextSorters;
|
|
312
|
+
}
|
|
313
|
+
});
|
|
338
314
|
|
|
339
315
|
// Persist column visibility to localStorage
|
|
340
316
|
$effect(() => {
|
|
341
317
|
const storageKey = `svadmin-columns-${resourceName}`;
|
|
342
318
|
if (typeof window !== 'undefined') {
|
|
343
319
|
try {
|
|
344
|
-
localStorage.setItem(storageKey, JSON.stringify(
|
|
320
|
+
localStorage.setItem(storageKey, JSON.stringify(tableColumnVisibility.current));
|
|
345
321
|
} catch { /* ignore quota errors */ }
|
|
346
322
|
}
|
|
347
323
|
});
|
|
@@ -353,8 +329,8 @@
|
|
|
353
329
|
sorters = externalSorters;
|
|
354
330
|
}
|
|
355
331
|
const nextSorting = externalSorters.map(s => ({ id: s.field, desc: s.order === 'desc' }));
|
|
356
|
-
if (JSON.stringify(nextSorting) !== JSON.stringify(
|
|
357
|
-
|
|
332
|
+
if (JSON.stringify(nextSorting) !== JSON.stringify(tableSorting.current)) {
|
|
333
|
+
sortingAtom.set(nextSorting);
|
|
358
334
|
}
|
|
359
335
|
});
|
|
360
336
|
|
|
@@ -397,12 +373,25 @@
|
|
|
397
373
|
enableSorting: false,
|
|
398
374
|
},
|
|
399
375
|
]);
|
|
376
|
+
const orderedColumns = $derived.by(() => {
|
|
377
|
+
const columnOrder = tableColumnOrder.current;
|
|
378
|
+
if (!columnOrder.length) return columns;
|
|
379
|
+
|
|
380
|
+
const columnsById = new Map(columns.map((column) => [column.id, column]));
|
|
381
|
+
return [
|
|
382
|
+
...columnOrder.flatMap((id) => {
|
|
383
|
+
const column = columnsById.get(id);
|
|
384
|
+
return column ? [column] : [];
|
|
385
|
+
}),
|
|
386
|
+
...columns.filter((column) => !columnOrder.includes(column.id ?? '')),
|
|
387
|
+
];
|
|
388
|
+
});
|
|
400
389
|
|
|
401
390
|
// ─── Create TanStack Table ────────────────────────────────────
|
|
402
391
|
const tbl = createTable(
|
|
403
392
|
{
|
|
404
393
|
get data() { return query.data?.data ?? []; },
|
|
405
|
-
get columns() { return
|
|
394
|
+
get columns() { return orderedColumns; },
|
|
406
395
|
getCoreRowModel: createCoreRowModel(),
|
|
407
396
|
manualPagination: true,
|
|
408
397
|
manualSorting: true,
|
|
@@ -412,15 +401,38 @@
|
|
|
412
401
|
columnVisibility: columnVisibilityAtom,
|
|
413
402
|
rowSelection: rowSelectionAtom,
|
|
414
403
|
expanded: expandedAtom,
|
|
415
|
-
columnOrder: columnOrderAtom,
|
|
416
404
|
},
|
|
405
|
+
onSortingChange: sortingAtom.set,
|
|
406
|
+
onColumnVisibilityChange: columnVisibilityAtom.set,
|
|
407
|
+
onRowSelectionChange: rowSelectionAtom.set,
|
|
408
|
+
onExpandedChange: expandedAtom.set,
|
|
417
409
|
get enableRowSelection() { return selectable && (canDelete || batchActions); },
|
|
418
410
|
get enableExpanding() { return !!expandedRowRender; },
|
|
411
|
+
getRowCanExpand: () => !!expandedRowRender,
|
|
419
412
|
},
|
|
413
|
+
// AutoTable reads focused atoms directly; selecting the full state triggers Svelte proxy equality warnings.
|
|
420
414
|
() => undefined,
|
|
421
415
|
);
|
|
422
416
|
|
|
423
|
-
|
|
417
|
+
$effect.pre(() => {
|
|
418
|
+
const nextColumns = orderedColumns;
|
|
419
|
+
untrack(() => {
|
|
420
|
+
tbl.setOptions({ ...tbl.options, columns: nextColumns });
|
|
421
|
+
});
|
|
422
|
+
});
|
|
423
|
+
|
|
424
|
+
const selectedCount = $derived(Object.keys(tableRowSelection.current).length);
|
|
425
|
+
const tableView = $derived.by(() => {
|
|
426
|
+
void tableSorting.current;
|
|
427
|
+
void tableColumnVisibility.current;
|
|
428
|
+
void tableRowSelection.current;
|
|
429
|
+
void tableExpanded.current;
|
|
430
|
+
void tableColumnOrder.current;
|
|
431
|
+
return {
|
|
432
|
+
headerGroups: tbl.getHeaderGroups(),
|
|
433
|
+
rows: table_getRowModel(tbl).rows,
|
|
434
|
+
};
|
|
435
|
+
});
|
|
424
436
|
const totalPages = $derived(Math.ceil((query.data?.total ?? 0) / (pagination.pageSize ?? 10)));
|
|
425
437
|
|
|
426
438
|
// ─── Pagination helpers ───────────────────────────────────────
|
|
@@ -456,7 +468,7 @@
|
|
|
456
468
|
}
|
|
457
469
|
|
|
458
470
|
function confirmBatchDelete() {
|
|
459
|
-
const ids = Object.keys(
|
|
471
|
+
const ids = Object.keys(tableRowSelection.current);
|
|
460
472
|
confirmMessage = i18n.t('common.batchDeleteConfirm', { count: ids.length });
|
|
461
473
|
confirmAction = async () => {
|
|
462
474
|
try {
|
|
@@ -465,7 +477,7 @@
|
|
|
465
477
|
} catch {
|
|
466
478
|
notify({ type: 'error', message: i18n.t('common.batchDeletePartialFail', { failed: 1, total: ids.length }) });
|
|
467
479
|
}
|
|
468
|
-
|
|
480
|
+
rowSelectionAtom.set({});
|
|
469
481
|
confirmOpen = false;
|
|
470
482
|
};
|
|
471
483
|
confirmOpen = true;
|
|
@@ -520,7 +532,7 @@
|
|
|
520
532
|
{/if}
|
|
521
533
|
|
|
522
534
|
{#if selectedCount > 0 && batchActions}
|
|
523
|
-
{@render batchActions({ selectedIds: Object.keys(
|
|
535
|
+
{@render batchActions({ selectedIds: Object.keys(tableRowSelection.current) })}
|
|
524
536
|
{/if}
|
|
525
537
|
|
|
526
538
|
<!-- Column Visibility Picker (DropdownMenu) -->
|
|
@@ -528,14 +540,14 @@
|
|
|
528
540
|
<DropdownMenu.Trigger>
|
|
529
541
|
{#snippet child({ props })}
|
|
530
542
|
<Button variant="outline" size="sm" {...props}>
|
|
531
|
-
<SlidersHorizontal class="h-4 w-4" data-icon="inline-start" /> {i18n.t('common.columns')
|
|
543
|
+
<SlidersHorizontal class="h-4 w-4" data-icon="inline-start" /> {i18n.t('common.columns')}
|
|
532
544
|
</Button>
|
|
533
545
|
{/snippet}
|
|
534
546
|
</DropdownMenu.Trigger>
|
|
535
547
|
<DropdownMenu.Content align="end" class="w-48">
|
|
536
548
|
{#each table_getAllLeafColumns(tbl).filter((column: Column<TableFeatures, BaseRecord, unknown>) => !column.id.startsWith('_')) as column, _i (_i)}
|
|
537
549
|
<DropdownMenu.CheckboxItem
|
|
538
|
-
checked={
|
|
550
|
+
checked={tableColumnVisibility.current[column.id] ?? true}
|
|
539
551
|
onCheckedChange={(v) => column_toggleVisibility(column, !!v)}
|
|
540
552
|
>
|
|
541
553
|
{visibleFields.find(f => f.key === column.id)?.label ?? column.id}
|
|
@@ -561,8 +573,8 @@
|
|
|
561
573
|
<Search class="absolute left-3 top-1/2 h-4 w-4 -translate-y-1/2 text-muted-foreground" />
|
|
562
574
|
<Input
|
|
563
575
|
type="text"
|
|
564
|
-
|
|
565
|
-
oninput={
|
|
576
|
+
value={searchText}
|
|
577
|
+
oninput={scheduleSearch}
|
|
566
578
|
placeholder={i18n.t('common.search')}
|
|
567
579
|
class="pl-10 h-9"
|
|
568
580
|
/>
|
|
@@ -656,11 +668,11 @@
|
|
|
656
668
|
<div class="hidden md:block">
|
|
657
669
|
<Table.Root>
|
|
658
670
|
<Table.Header>
|
|
659
|
-
{#each
|
|
671
|
+
{#each tableView.headerGroups as headerGroup, _i (_i)}
|
|
660
672
|
<DraggableHeader
|
|
661
673
|
columns={headerGroup.headers.map((header: Header<TableFeatures, BaseRecord, unknown>) => ({ id: header.column.id, header }))}
|
|
662
674
|
resourceName={resourceName}
|
|
663
|
-
onReorder={(newOrder) =>
|
|
675
|
+
onReorder={(newOrder) => columnOrderAtom.set(newOrder.map((column) => column.id))}
|
|
664
676
|
>
|
|
665
677
|
{#snippet header(col, _index, dragProps)}
|
|
666
678
|
{@const header = col.header as typeof headerGroup.headers[0]}
|
|
@@ -702,7 +714,7 @@
|
|
|
702
714
|
{/each}
|
|
703
715
|
</Table.Header>
|
|
704
716
|
<Table.Body>
|
|
705
|
-
{#each
|
|
717
|
+
{#each tableView.rows as row, _i (_i)}
|
|
706
718
|
{@const record = row.original}
|
|
707
719
|
{@const id = record[primaryKey] as string | number}
|
|
708
720
|
<ContextMenu.Root>
|
|
@@ -717,8 +729,8 @@
|
|
|
717
729
|
onCheckedChange={() => row_toggleSelected(row)}
|
|
718
730
|
/>
|
|
719
731
|
{:else if cell.column.id === '_expand'}
|
|
720
|
-
<TooltipButton tooltip={
|
|
721
|
-
{#if
|
|
732
|
+
<TooltipButton tooltip={rowIsExpanded(row.id) ? i18n.t('common.collapse') : i18n.t('common.expand')} variant="ghost" size="icon" class="h-7 w-7" onclick={() => row_toggleExpanded(row)}>
|
|
733
|
+
{#if rowIsExpanded(row.id)}
|
|
722
734
|
<ChevronUp class="h-4 w-4" />
|
|
723
735
|
{:else}
|
|
724
736
|
<ChevronDown class="h-4 w-4" />
|
|
@@ -799,7 +811,7 @@
|
|
|
799
811
|
{/if}
|
|
800
812
|
</ContextMenu.Content>
|
|
801
813
|
</ContextMenu.Root>
|
|
802
|
-
{#if expandedRowRender &&
|
|
814
|
+
{#if expandedRowRender && rowIsExpanded(row.id)}
|
|
803
815
|
<Table.Row class="bg-muted/10 border-b border-border/10 transition-all">
|
|
804
816
|
<Table.Cell colspan={row_getVisibleCells(row).length}>
|
|
805
817
|
{@render expandedRowRender({ record })}
|
|
@@ -817,7 +829,7 @@
|
|
|
817
829
|
<path stroke-linecap="round" stroke-linejoin="round" d="M20 7l-8-4-8 4m16 0l-8 4m8-4v10l-8 4m0-10L4 7m8 4v10M4 7v10l8 4" />
|
|
818
830
|
</svg>
|
|
819
831
|
<p class="text-sm font-medium text-muted-foreground mb-1">{i18n.t('common.noData')}</p>
|
|
820
|
-
<p class="text-xs text-muted-foreground/60 mb-4">{i18n.t('common.noDataHint')
|
|
832
|
+
<p class="text-xs text-muted-foreground/60 mb-4">{i18n.t('common.noDataHint')}</p>
|
|
821
833
|
{#if canCreate}
|
|
822
834
|
<Button variant="outline" size="sm" class="gap-2" onclick={() => adminContext.navigate(`/${resourceName}/create`)}>
|
|
823
835
|
<Plus class="h-3.5 w-3.5" />
|
|
@@ -835,7 +847,7 @@
|
|
|
835
847
|
|
|
836
848
|
<!-- Mobile Card View (visible only on small screens) -->
|
|
837
849
|
<div class="md:hidden space-y-3 p-2">
|
|
838
|
-
{#each
|
|
850
|
+
{#each tableView.rows as row, _i (_i)}
|
|
839
851
|
{@const record = row.original}
|
|
840
852
|
{@const id = record[primaryKey] as string | number}
|
|
841
853
|
<div
|
|
@@ -35,11 +35,6 @@
|
|
|
35
35
|
const colorTheme = $derived(getColorTheme());
|
|
36
36
|
const locale = $derived(i18n.locale);
|
|
37
37
|
|
|
38
|
-
// Auto-trace state changes in dev mode via Svelte 5 $inspect
|
|
39
|
-
if ((import.meta as any).env?.DEV) {
|
|
40
|
-
// eslint-disable-next-line svelte/no-inspect
|
|
41
|
-
$inspect({ path, theme, colorTheme, locale, resourceCount: resources.length });
|
|
42
|
-
}
|
|
43
38
|
</script>
|
|
44
39
|
|
|
45
40
|
<svelte:window onkeydown={handleKeydown} />
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@svadmin/ui",
|
|
3
|
-
"version": "0.38.
|
|
3
|
+
"version": "0.38.7",
|
|
4
4
|
"description": "Pre-built admin UI components — AdminApp, AutoTable, AutoForm, Sidebar, Layout",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"sideEffects": [
|
|
@@ -76,6 +76,7 @@
|
|
|
76
76
|
"dependencies": {
|
|
77
77
|
"@floating-ui/dom": "^1.8.0",
|
|
78
78
|
"@lucide/svelte": "^1.25.0",
|
|
79
|
+
"@tanstack/svelte-store": "^0.12.0",
|
|
79
80
|
"@tanstack/svelte-table": "9.0.0-alpha.53",
|
|
80
81
|
"@tanstack/table-core": "9.0.0-alpha.53",
|
|
81
82
|
"bits-ui": "^2.18.1",
|