@vendure/dashboard 3.5.2-master-202512170238 → 3.5.2-master-202512180239

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,4 +1,4 @@
1
- import { AccessorFnColumnDef } from '@tanstack/react-table';
1
+ import { AccessorFnColumnDef, ExpandedState } from '@tanstack/react-table';
2
2
  import { AccessorKeyColumnDef } from '@tanstack/table-core';
3
3
 
4
4
  /**
@@ -49,3 +49,243 @@ export function getStandardizedDefaultColumnOrder<T extends string | number | sy
49
49
  const rest = defaultColumnOrder.filter(c => !standardFirstColumns.has(c as string));
50
50
  return [...standardFirstColumns, ...rest] as T[];
51
51
  }
52
+
53
+ /**
54
+ * Hierarchical item type with parent-child relationships
55
+ */
56
+ export interface HierarchicalItem {
57
+ id: string;
58
+ parentId?: string | null;
59
+ breadcrumbs?: Array<{ id: string }>;
60
+ children?: Array<{ id: string }> | null;
61
+ }
62
+
63
+ /**
64
+ * Gets the parent ID of a hierarchical item
65
+ */
66
+ export function getItemParentId<T extends HierarchicalItem>(
67
+ item: T | null | undefined,
68
+ ): string | null | undefined {
69
+ return item?.parentId || item?.breadcrumbs?.[0]?.id;
70
+ }
71
+
72
+ /**
73
+ * Gets all siblings (items with the same parent) for a given parent ID
74
+ */
75
+ export function getItemSiblings<T extends HierarchicalItem>(
76
+ items: T[],
77
+ parentId: string | null | undefined,
78
+ ): T[] {
79
+ return items.filter(item => getItemParentId(item) === parentId);
80
+ }
81
+
82
+ /**
83
+ * Checks if moving an item to a new parent would create a circular reference
84
+ */
85
+ export function isCircularReference<T extends HierarchicalItem>(
86
+ item: T,
87
+ targetParentId: string,
88
+ items: T[],
89
+ ): boolean {
90
+ const targetParentItem = items.find(i => i.id === targetParentId);
91
+ return (
92
+ item.children?.some(child => {
93
+ if (child.id === targetParentId) return true;
94
+ const targetBreadcrumbIds = targetParentItem?.breadcrumbs?.map(b => b.id) || [];
95
+ return targetBreadcrumbIds.includes(item.id);
96
+ }) ?? false
97
+ );
98
+ }
99
+
100
+ /**
101
+ * Result of calculating the target position for a drag and drop operation
102
+ */
103
+ export interface TargetPosition {
104
+ targetParentId: string;
105
+ adjustedIndex: number;
106
+ }
107
+
108
+ /**
109
+ * Context for drag and drop position calculation
110
+ */
111
+ interface DragContext<T extends HierarchicalItem> {
112
+ item: T;
113
+ targetItem: T | undefined;
114
+ previousItem: T | null;
115
+ isDraggingDown: boolean;
116
+ isTargetExpanded: boolean;
117
+ isPreviousExpanded: boolean;
118
+ sourceParentId: string;
119
+ items: T[];
120
+ }
121
+
122
+ /**
123
+ * Checks if dragging down directly onto an expanded item
124
+ */
125
+ function isDroppingIntoExpandedTarget<T extends HierarchicalItem>(context: DragContext<T>): boolean {
126
+ const { isDraggingDown, targetItem, item, isTargetExpanded } = context;
127
+ return isDraggingDown && targetItem?.id !== item.id && isTargetExpanded;
128
+ }
129
+
130
+ /**
131
+ * Checks if dragging down into an expanded item's children area
132
+ */
133
+ function isDroppingIntoExpandedPreviousChildren<T extends HierarchicalItem>(
134
+ context: DragContext<T>,
135
+ ): boolean {
136
+ const { isDraggingDown, targetItem, previousItem, item, isPreviousExpanded } = context;
137
+ return (
138
+ isDraggingDown &&
139
+ previousItem !== null &&
140
+ targetItem?.id !== item.id &&
141
+ isPreviousExpanded &&
142
+ targetItem?.parentId === previousItem.id
143
+ );
144
+ }
145
+
146
+ /**
147
+ * Checks if dragging up into an expanded item's children area
148
+ */
149
+ function isDroppingIntoExpandedPreviousWhenDraggingUp<T extends HierarchicalItem>(
150
+ context: DragContext<T>,
151
+ ): boolean {
152
+ const { isDraggingDown, previousItem, isPreviousExpanded } = context;
153
+ return !isDraggingDown && previousItem !== null && isPreviousExpanded;
154
+ }
155
+
156
+ /**
157
+ * Creates a position for dropping into an expanded item as first child
158
+ */
159
+ function createFirstChildPosition(parentId: string): TargetPosition {
160
+ return { targetParentId: parentId, adjustedIndex: 0 };
161
+ }
162
+
163
+ /**
164
+ * Calculates position for cross-parent drag operations
165
+ */
166
+ function calculateCrossParentPosition<T extends HierarchicalItem>(
167
+ targetItem: T,
168
+ sourceParentId: string,
169
+ items: T[],
170
+ ): TargetPosition | null {
171
+ const targetItemParentId = getItemParentId(targetItem);
172
+
173
+ if (!targetItemParentId || targetItemParentId === sourceParentId) {
174
+ return null;
175
+ }
176
+
177
+ const targetSiblings = getItemSiblings(items, targetItemParentId);
178
+ const adjustedIndex = targetSiblings.findIndex(i => i.id === targetItem.id);
179
+
180
+ return { targetParentId: targetItemParentId, adjustedIndex };
181
+ }
182
+
183
+ /**
184
+ * Calculates position when dropping at the end of the list
185
+ */
186
+ function calculateDropAtEndPosition<T extends HierarchicalItem>(
187
+ previousItem: T | null,
188
+ sourceParentId: string,
189
+ items: T[],
190
+ ): TargetPosition | null {
191
+ if (!previousItem) {
192
+ return null;
193
+ }
194
+
195
+ const previousItemParentId = getItemParentId(previousItem);
196
+
197
+ if (!previousItemParentId || previousItemParentId === sourceParentId) {
198
+ return null;
199
+ }
200
+
201
+ const targetSiblings = getItemSiblings(items, previousItemParentId);
202
+ return { targetParentId: previousItemParentId, adjustedIndex: targetSiblings.length };
203
+ }
204
+
205
+ /**
206
+ * Determines the target parent and index for a hierarchical drag and drop operation
207
+ */
208
+ export function calculateDragTargetPosition<T extends HierarchicalItem>(params: {
209
+ item: T;
210
+ oldIndex: number;
211
+ newIndex: number;
212
+ items: T[];
213
+ sourceParentId: string;
214
+ expanded: ExpandedState;
215
+ }): TargetPosition {
216
+ const { item, oldIndex, newIndex, items, sourceParentId, expanded } = params;
217
+
218
+ const targetItem = items[newIndex];
219
+ const previousItem = newIndex > 0 ? items[newIndex - 1] : null;
220
+
221
+ const context: DragContext<T> = {
222
+ item,
223
+ targetItem,
224
+ previousItem,
225
+ isDraggingDown: oldIndex < newIndex,
226
+ isTargetExpanded: targetItem ? !!expanded[targetItem.id as keyof ExpandedState] : false,
227
+ isPreviousExpanded: previousItem ? !!expanded[previousItem.id as keyof ExpandedState] : false,
228
+ sourceParentId,
229
+ items,
230
+ };
231
+
232
+ // Handle dropping into expanded items (becomes first child)
233
+ if (isDroppingIntoExpandedTarget(context)) {
234
+ return createFirstChildPosition(targetItem.id);
235
+ }
236
+
237
+ if (previousItem && isDroppingIntoExpandedPreviousChildren(context)) {
238
+ return createFirstChildPosition(previousItem.id);
239
+ }
240
+
241
+ if (previousItem && isDroppingIntoExpandedPreviousWhenDraggingUp(context)) {
242
+ return createFirstChildPosition(previousItem.id);
243
+ }
244
+
245
+ // Handle cross-parent drag operations
246
+ if (targetItem?.id !== item.id) {
247
+ const crossParentPosition = calculateCrossParentPosition(targetItem, sourceParentId, items);
248
+ if (crossParentPosition) {
249
+ return crossParentPosition;
250
+ }
251
+ }
252
+
253
+ // Handle dropping at the end of the list
254
+ if (!targetItem && previousItem) {
255
+ const dropAtEndPosition = calculateDropAtEndPosition(previousItem, sourceParentId, items);
256
+ if (dropAtEndPosition) {
257
+ return dropAtEndPosition;
258
+ }
259
+ }
260
+
261
+ // Default: stay in the same parent at the beginning
262
+ return { targetParentId: sourceParentId, adjustedIndex: 0 };
263
+ }
264
+
265
+ /**
266
+ * Calculates the adjusted sibling index when reordering within the same parent
267
+ */
268
+ export function calculateSiblingIndex<T extends HierarchicalItem>(params: {
269
+ item: T;
270
+ oldIndex: number;
271
+ newIndex: number;
272
+ items: T[];
273
+ parentId: string;
274
+ }): number {
275
+ const { item, oldIndex, newIndex, items, parentId } = params;
276
+
277
+ const siblings = getItemSiblings(items, parentId);
278
+ const oldSiblingIndex = siblings.findIndex(i => i.id === item.id);
279
+ const isDraggingDown = oldIndex < newIndex;
280
+
281
+ let newSiblingIndex = oldSiblingIndex;
282
+ const [start, end] = isDraggingDown ? [oldIndex + 1, newIndex] : [newIndex, oldIndex - 1];
283
+
284
+ for (let i = start; i <= end; i++) {
285
+ if (getItemParentId(items[i]) === parentId) {
286
+ newSiblingIndex += isDraggingDown ? 1 : -1;
287
+ }
288
+ }
289
+
290
+ return newSiblingIndex;
291
+ }
@@ -15,6 +15,17 @@ import { useChannel } from '@/vdb/hooks/use-channel.js';
15
15
  import { usePage } from '@/vdb/hooks/use-page.js';
16
16
  import { useSavedViews } from '@/vdb/hooks/use-saved-views.js';
17
17
  import { Trans, useLingui } from '@lingui/react/macro';
18
+ import {
19
+ closestCenter,
20
+ DndContext,
21
+ } from '@dnd-kit/core';
22
+ import { restrictToVerticalAxis } from '@dnd-kit/modifiers';
23
+ import {
24
+ SortableContext,
25
+ useSortable,
26
+ verticalListSortingStrategy,
27
+ } from '@dnd-kit/sortable';
28
+ import { CSS } from '@dnd-kit/utilities';
18
29
  import {
19
30
  ColumnDef,
20
31
  ColumnFilter,
@@ -23,18 +34,66 @@ import {
23
34
  getCoreRowModel,
24
35
  getPaginationRowModel,
25
36
  PaginationState,
37
+ Row,
26
38
  SortingState,
27
39
  Table as TableType,
28
40
  useReactTable,
29
41
  VisibilityState,
30
42
  } from '@tanstack/react-table';
31
43
  import { RowSelectionState, TableOptions } from '@tanstack/table-core';
32
- import React, { Suspense, useEffect, useRef } from 'react';
44
+ import { GripVertical } from 'lucide-react';
45
+ import React, { Suspense, useEffect, useId, useMemo, useRef } from 'react';
33
46
  import { AddFilterMenu } from './add-filter-menu.js';
34
47
  import { DataTableBulkActions } from './data-table-bulk-actions.js';
35
48
  import { DataTableProvider } from './data-table-context.js';
36
49
  import { DataTableFacetedFilter, DataTableFacetedFilterOption } from './data-table-faceted-filter.js';
37
50
  import { DataTableFilterBadgeEditable } from './data-table-filter-badge-editable.js';
51
+ import { useDragAndDrop } from '@/vdb/hooks/use-drag-and-drop.js';
52
+ import { toast } from 'sonner';
53
+
54
+ interface DraggableRowProps<TData> {
55
+ row: Row<TData>;
56
+ isDragDisabled: boolean;
57
+ }
58
+
59
+ function DraggableRow<TData>({ row, isDragDisabled }: Readonly<DraggableRowProps<TData>>) {
60
+ const { attributes, listeners, setNodeRef, transform, transition, isDragging } = useSortable({
61
+ id: row.id,
62
+ disabled: isDragDisabled,
63
+ });
64
+
65
+ const style = {
66
+ transform: CSS.Transform.toString(transform),
67
+ transition,
68
+ opacity: isDragging ? 0.5 : 1,
69
+ };
70
+
71
+ return (
72
+ <TableRow
73
+ ref={setNodeRef}
74
+ style={style}
75
+ data-state={row.getIsSelected() && 'selected'}
76
+ className="animate-in fade-in duration-100"
77
+ >
78
+ {!isDragDisabled && (
79
+ <TableCell className="w-[40px] h-12">
80
+ <div
81
+ {...attributes}
82
+ {...listeners}
83
+ className="cursor-move text-muted-foreground hover:text-foreground transition-colors"
84
+ >
85
+ <GripVertical className="h-4 w-4" />
86
+ </div>
87
+ </TableCell>
88
+ )}
89
+ {row.getVisibleCells().filter(cell => cell.column.id !== '__drag_handle__').map(cell => (
90
+ <TableCell key={cell.id} className="h-12">
91
+ {flexRender(cell.column.columnDef.cell, cell.getContext())}
92
+ </TableCell>
93
+ ))}
94
+ </TableRow>
95
+ );
96
+ }
38
97
 
39
98
  export interface FacetedFilter {
40
99
  title: string;
@@ -77,6 +136,18 @@ interface DataTableProps<TData> {
77
136
  */
78
137
  setTableOptions?: (table: TableOptions<TData>) => TableOptions<TData>;
79
138
  onRefresh?: () => void;
139
+ /**
140
+ * @description
141
+ * Callback when items are reordered via drag and drop.
142
+ * When provided, enables drag-and-drop functionality.
143
+ * The fourth parameter provides all items for context-aware reordering.
144
+ */
145
+ onReorder?: (oldIndex: number, newIndex: number, item: TData, allItems?: TData[]) => void | Promise<void>;
146
+ /**
147
+ * @description
148
+ * When true, drag and drop will be disabled. This will only have an effect if the onReorder prop is also set
149
+ */
150
+ disableDragAndDrop?: boolean;
80
151
  }
81
152
 
82
153
  /**
@@ -111,6 +182,8 @@ export function DataTable<TData>({
111
182
  bulkActions,
112
183
  setTableOptions,
113
184
  onRefresh,
185
+ onReorder,
186
+ disableDragAndDrop = false,
114
187
  }: Readonly<DataTableProps<TData>>) {
115
188
  const [sorting, setSorting] = React.useState<SortingState>(sortingInitialState || []);
116
189
  const [columnFilters, setColumnFilters] = React.useState<ColumnFiltersState>(filtersInitialState || []);
@@ -131,6 +204,16 @@ export function DataTable<TData>({
131
204
  const prevSearchTermRef = useRef(searchTerm);
132
205
  const prevColumnFiltersRef = useRef(columnFilters);
133
206
 
207
+ const componentId = useId();
208
+ const { sensors, localData, handleDragEnd, itemIds } = useDragAndDrop({
209
+ data,
210
+ onReorder,
211
+ disabled: disableDragAndDrop,
212
+ onError: error => {
213
+ toast.error(t`Failed to reorder items`);
214
+ },
215
+ });
216
+
134
217
  useEffect(() => {
135
218
  // If the defaultColumnVisibility changes externally (e.g. the user reset the table settings),
136
219
  // we want to reset the column visibility to the default.
@@ -143,9 +226,25 @@ export function DataTable<TData>({
143
226
  // We intentionally do not include `columnVisibility` in the dependency array
144
227
  }, [defaultColumnVisibility]);
145
228
 
229
+ // Add drag handle column if drag and drop is enabled
230
+ const columnsWithOptionalDragHandle = useMemo(() => {
231
+ if (!disableDragAndDrop && onReorder) {
232
+ const dragHandleColumn: ColumnDef<TData, any> = {
233
+ id: '__drag_handle__',
234
+ header: '',
235
+ cell: () => null, // Rendered by DraggableRow
236
+ size: 40,
237
+ enableSorting: false,
238
+ enableHiding: false,
239
+ };
240
+ return [dragHandleColumn, ...columns];
241
+ }
242
+ return columns;
243
+ }, [columns, disableDragAndDrop, onReorder]);
244
+
146
245
  let tableOptions: TableOptions<TData> = {
147
- data,
148
- columns,
246
+ data: localData,
247
+ columns: columnsWithOptionalDragHandle,
149
248
  getRowId: row => (row as { id: string }).id,
150
249
  getCoreRowModel: getCoreRowModel(),
151
250
  getPaginationRowModel: getPaginationRowModel(),
@@ -220,6 +319,8 @@ export function DataTable<TData>({
220
319
 
221
320
  const visibleColumnCount = Object.values(columnVisibility).filter(Boolean).length;
222
321
 
322
+ const isDragDisabled = disableDragAndDrop || !onReorder;
323
+
223
324
  return (
224
325
  <DataTableProvider
225
326
  columnFilters={columnFilters}
@@ -310,66 +411,94 @@ export function DataTable<TData>({
310
411
  ) : null}
311
412
 
312
413
  <div className="rounded-md border my-2 relative shadow-sm">
313
- <Table>
314
- <TableHeader className="bg-muted/50">
315
- {table.getHeaderGroups().map(headerGroup => (
316
- <TableRow key={headerGroup.id}>
317
- {headerGroup.headers.map(header => {
318
- return (
319
- <TableHead key={header.id}>
320
- {header.isPlaceholder
321
- ? null
322
- : flexRender(
323
- header.column.columnDef.header,
324
- header.getContext(),
325
- )}
326
- </TableHead>
327
- );
328
- })}
329
- </TableRow>
330
- ))}
331
- </TableHeader>
332
- <TableBody>
333
- {isLoading && !data?.length ? (
334
- Array.from({ length: Math.min(pagination.pageSize, 100) }).map((_, index) => (
335
- <TableRow
336
- key={`skeleton-${index}`}
337
- className="animate-in fade-in duration-100"
338
- >
339
- {Array.from({ length: visibleColumnCount }).map((_, cellIndex) => (
414
+ <DndContext
415
+ sensors={sensors}
416
+ collisionDetection={closestCenter}
417
+ onDragEnd={handleDragEnd}
418
+ modifiers={[restrictToVerticalAxis]}
419
+ >
420
+ <Table>
421
+ <TableHeader className="bg-muted/50">
422
+ {table.getHeaderGroups().map(headerGroup => (
423
+ <TableRow key={headerGroup.id}>
424
+ {headerGroup.headers.map(header => {
425
+ return (
426
+ <TableHead key={header.id}>
427
+ {header.isPlaceholder
428
+ ? null
429
+ : flexRender(
430
+ header.column.columnDef.header,
431
+ header.getContext(),
432
+ )}
433
+ </TableHead>
434
+ );
435
+ })}
436
+ </TableRow>
437
+ ))}
438
+ </TableHeader>
439
+ <SortableContext items={itemIds} strategy={verticalListSortingStrategy}>
440
+ <TableBody>
441
+ {isLoading && !localData?.length ? (
442
+ Array.from({ length: Math.min(pagination.pageSize, 100) }).map((_, index) => (
443
+ <TableRow
444
+ key={`skeleton-${index}`}
445
+ className="animate-in fade-in duration-100"
446
+ >
447
+ {!isDragDisabled && (
448
+ <TableCell className="w-[40px] h-12">
449
+ <Skeleton className="h-4 w-4" />
450
+ </TableCell>
451
+ )}
452
+ {Array.from({ length: visibleColumnCount }).map((_, cellIndex) => (
453
+ <TableCell
454
+ key={`skeleton-cell-${index}-${cellIndex}`}
455
+ className="h-12"
456
+ >
457
+ <Skeleton className="h-4 my-2 w-full" />
458
+ </TableCell>
459
+ ))}
460
+ </TableRow>
461
+ ))
462
+ ) : table.getRowModel().rows?.length ? (
463
+ (() => {
464
+ const isDraggableEnabled = onReorder && !isDragDisabled;
465
+ const rows = table.getRowModel().rows;
466
+
467
+ if (isDraggableEnabled) {
468
+ return rows.map(row => (
469
+ <DraggableRow key={`${row.id}-${componentId}`} row={row} isDragDisabled={isDragDisabled} />
470
+ ));
471
+ }
472
+
473
+ return rows.map(row => (
474
+ <TableRow
475
+ key={row.id}
476
+ data-state={row.getIsSelected() && 'selected'}
477
+ className="animate-in fade-in duration-100"
478
+ >
479
+ {row.getVisibleCells().map(cell => (
480
+ <TableCell key={cell.id} className="h-12">
481
+ {flexRender(cell.column.columnDef.cell, cell.getContext())}
482
+ </TableCell>
483
+ ))}
484
+ </TableRow>
485
+ ));
486
+ })()
487
+ ) : (
488
+ <TableRow className="animate-in fade-in duration-100">
340
489
  <TableCell
341
- key={`skeleton-cell-${index}-${cellIndex}`}
342
- className="h-12"
490
+ colSpan={columnsWithOptionalDragHandle.length + (isDragDisabled ? 0 : 1)}
491
+ className="h-24 text-center"
343
492
  >
344
- <Skeleton className="h-4 my-2 w-full" />
493
+ <Trans>No results</Trans>
345
494
  </TableCell>
346
- ))}
347
- </TableRow>
348
- ))
349
- ) : table.getRowModel().rows?.length ? (
350
- table.getRowModel().rows.map(row => (
351
- <TableRow
352
- key={row.id}
353
- data-state={row.getIsSelected() && 'selected'}
354
- className="animate-in fade-in duration-100"
355
- >
356
- {row.getVisibleCells().map(cell => (
357
- <TableCell key={cell.id} className="h-12">
358
- {flexRender(cell.column.columnDef.cell, cell.getContext())}
359
- </TableCell>
360
- ))}
361
- </TableRow>
362
- ))
363
- ) : (
364
- <TableRow className="animate-in fade-in duration-100">
365
- <TableCell colSpan={columns.length} className="h-24 text-center">
366
- <Trans>No results</Trans>
367
- </TableCell>
368
- </TableRow>
369
- )}
370
- {children}
371
- </TableBody>
372
- </Table>
495
+ </TableRow>
496
+ )}
497
+ {children}
498
+ </TableBody>
499
+ </SortableContext>
500
+ </Table>
501
+ </DndContext>
373
502
  <DataTableBulkActions bulkActions={bulkActions ?? []} table={table} />
374
503
  </div>
375
504
  {onPageChange && totalItems != null && <DataTablePagination table={table} />}
@@ -234,6 +234,21 @@ export interface PaginatedListDataTableProps<
234
234
  * the list needs to be refreshed.
235
235
  */
236
236
  registerRefresher?: PaginatedListRefresherRegisterFn;
237
+ /**
238
+ * @description
239
+ * Callback when items are reordered via drag and drop.
240
+ * When provided, enables drag-and-drop functionality.
241
+ */
242
+ onReorder?: (
243
+ oldIndex: number,
244
+ newIndex: number,
245
+ item: PaginatedListItemFields<T>,
246
+ ) => void | Promise<void>;
247
+ /**
248
+ * @description
249
+ * When true, drag and drop will be disabled. This will only have an effect if the onReorder prop is also set
250
+ */
251
+ disableDragAndDrop?: boolean;
237
252
  }
238
253
 
239
254
  export const PaginatedListDataTableKey = 'PaginatedListDataTable';
@@ -378,6 +393,8 @@ export function PaginatedListDataTable<
378
393
  setTableOptions,
379
394
  transformData,
380
395
  registerRefresher,
396
+ onReorder,
397
+ disableDragAndDrop = false,
381
398
  }: Readonly<PaginatedListDataTableProps<T, U, V, AC>>) {
382
399
  const [searchTerm, setSearchTerm] = React.useState<string>('');
383
400
  const debouncedSearchTerm = useDebounce(searchTerm, 500);
@@ -498,6 +515,8 @@ export function PaginatedListDataTable<
498
515
  bulkActions={bulkActions}
499
516
  setTableOptions={setTableOptions}
500
517
  onRefresh={refetchPaginatedList}
518
+ onReorder={onReorder}
519
+ disableDragAndDrop={disableDragAndDrop}
501
520
  />
502
521
  </PaginatedListContext.Provider>
503
522
  );
@@ -8,7 +8,7 @@ const alertVariants = cva(
8
8
  {
9
9
  variants: {
10
10
  variant: {
11
- default: 'bg-background text-foreground',
11
+ default: 'bg-background text-primary/80',
12
12
  destructive:
13
13
  'border-destructive/50 text-destructive dark:text-destructive-foreground/80 dark:border-destructive [&>svg]:text-current dark:bg-destructive/50',
14
14
  },