@fastnd/components 1.0.31 → 1.0.32

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 (25) hide show
  1. package/dist/components/QuickAccessCard/QuickAccessCard.d.ts +12 -0
  2. package/dist/components/ScoreBar/ScoreBar.d.ts +8 -0
  3. package/dist/components/index.d.ts +2 -0
  4. package/dist/examples/dashboard/StatusDonutChart/StatusDonutChart.tsx +41 -56
  5. package/dist/examples/data-visualization/CardCarouselPanel/CardCarouselPanel.tsx +171 -0
  6. package/dist/examples/data-visualization/CellRenderers/CellRenderers.tsx +175 -0
  7. package/dist/examples/data-visualization/ColumnConfigPopover/ColumnConfigPopover.tsx +124 -0
  8. package/dist/examples/data-visualization/DataCardView/DataCardView.tsx +223 -0
  9. package/dist/examples/data-visualization/DataExplorerPagination/DataExplorerPagination.tsx +143 -0
  10. package/dist/examples/data-visualization/DataExplorerToolbar/DataExplorerToolbar.tsx +88 -0
  11. package/dist/examples/data-visualization/DataListView/DataListView.tsx +246 -0
  12. package/dist/examples/data-visualization/DataTableView/DataTableView.tsx +449 -0
  13. package/dist/examples/data-visualization/DataVisualizationPage/DataVisualizationPage.tsx +140 -0
  14. package/dist/examples/data-visualization/FilterChipGroup/FilterChipGroup.tsx +125 -0
  15. package/dist/examples/data-visualization/MoreFiltersPopover/MoreFiltersPopover.tsx +132 -0
  16. package/dist/examples/data-visualization/constants.ts +587 -0
  17. package/dist/examples/data-visualization/hooks/use-column-config.ts +76 -0
  18. package/dist/examples/data-visualization/hooks/use-data-explorer-state.ts +313 -0
  19. package/dist/examples/data-visualization/index.ts +1 -0
  20. package/dist/examples/data-visualization/types.ts +99 -0
  21. package/dist/examples/quickaccess/QuickAccess/QuickAccess.tsx +97 -0
  22. package/dist/examples/quickaccess/index.ts +2 -0
  23. package/dist/examples/quickaccess/types.ts +11 -0
  24. package/dist/fastnd-components.js +5708 -5590
  25. package/package.json +1 -1
@@ -0,0 +1,12 @@
1
+ import * as React from 'react';
2
+ interface QuickAccessCardProps extends React.ComponentProps<'a'> {
3
+ label: string;
4
+ count: number;
5
+ countLabel?: string;
6
+ icon: React.ReactNode;
7
+ variant?: 'default' | 'overdue';
8
+ isActive?: boolean;
9
+ }
10
+ declare const QuickAccessCard: React.ForwardRefExoticComponent<Omit<QuickAccessCardProps, "ref"> & React.RefAttributes<HTMLAnchorElement>>;
11
+ export { QuickAccessCard };
12
+ export type { QuickAccessCardProps };
@@ -0,0 +1,8 @@
1
+ import * as React from 'react';
2
+ interface ScoreBarProps extends Omit<React.HTMLAttributes<HTMLDivElement>, 'children'> {
3
+ value: number;
4
+ showLabel?: boolean;
5
+ }
6
+ declare const ScoreBar: React.ForwardRefExoticComponent<ScoreBarProps & React.RefAttributes<HTMLDivElement>>;
7
+ export { ScoreBar };
8
+ export type { ScoreBarProps };
@@ -1,4 +1,6 @@
1
1
  export * from './FavoriteButton/FavoriteButton';
2
+ export * from './QuickAccessCard/QuickAccessCard';
3
+ export * from './ScoreBar/ScoreBar';
2
4
  export * from './ui/accordion';
3
5
  export * from './ui/alert';
4
6
  export * from './ui/alert-dialog';
@@ -1,5 +1,5 @@
1
1
  import * as React from 'react'
2
- import { Pie, PieChart, Label, Sector } from 'recharts'
2
+ import { Pie, PieChart, Sector } from 'recharts'
3
3
  import type { PieSectorShapeProps } from 'recharts/types/polar/Pie'
4
4
  import {
5
5
  ChartContainer,
@@ -57,62 +57,47 @@ const StatusDonutChart = React.forwardRef<HTMLDivElement, StatusDonutChartProps>
57
57
  className={cn(className)}
58
58
  {...props}
59
59
  >
60
- <ChartContainer config={chartConfig} className="mx-auto aspect-square max-h-[200px]">
61
- <PieChart>
62
- <ChartTooltip cursor={false} content={<ChartTooltipContent hideLabel />} />
63
- <Pie
64
- data={chartData}
65
- dataKey="count"
66
- nameKey="status"
67
- innerRadius={60}
68
- outerRadius={85}
69
- strokeWidth={5}
70
- className="cursor-pointer"
71
- onClick={handlePieClick}
72
- shape={({
73
- index,
74
- outerRadius: or = 0,
75
- ...sectorProps
76
- }: PieSectorShapeProps) => (
77
- <Sector
78
- {...sectorProps}
79
- outerRadius={index === activeIndex ? or + 10 : or}
80
- />
81
- )}
82
- >
83
- <Label
84
- content={({ viewBox }) => {
85
- if (viewBox && 'cx' in viewBox && 'cy' in viewBox) {
86
- return (
87
- <text
88
- x={viewBox.cx}
89
- y={viewBox.cy}
90
- textAnchor="middle"
91
- dominantBaseline="middle"
92
- >
93
- <tspan
94
- x={viewBox.cx}
95
- y={viewBox.cy}
96
- className="fill-foreground text-3xl font-bold"
97
- style={{ fontFamily: 'var(--font-clash)' }}
98
- >
99
- {total.toLocaleString()}
100
- </tspan>
101
- <tspan
102
- x={viewBox.cx}
103
- y={(viewBox.cy || 0) + 24}
104
- className="fill-muted-foreground text-xs uppercase tracking-wider"
105
- >
106
- Gesamt
107
- </tspan>
108
- </text>
109
- )
110
- }
111
- }}
60
+ {/* Relative wrapper so the center label overlay can be positioned absolutely */}
61
+ <div className="relative mx-auto aspect-square max-h-[200px]">
62
+ <ChartContainer config={chartConfig} className="size-full">
63
+ <PieChart>
64
+ <ChartTooltip cursor={false} content={<ChartTooltipContent hideLabel />} />
65
+ <Pie
66
+ data={chartData}
67
+ dataKey="count"
68
+ nameKey="status"
69
+ innerRadius={60}
70
+ outerRadius={85}
71
+ strokeWidth={5}
72
+ className="cursor-pointer"
73
+ onClick={handlePieClick}
74
+ shape={({
75
+ index,
76
+ outerRadius: or = 0,
77
+ ...sectorProps
78
+ }: PieSectorShapeProps) => (
79
+ <Sector
80
+ {...sectorProps}
81
+ outerRadius={index === activeIndex ? or + 10 : or}
82
+ />
83
+ )}
112
84
  />
113
- </Pie>
114
- </PieChart>
115
- </ChartContainer>
85
+ </PieChart>
86
+ </ChartContainer>
87
+
88
+ {/* Center label — rendered as HTML so it is always in the DOM (recharts SVG labels are invisible in jsdom) */}
89
+ <div className="pointer-events-none absolute inset-0 flex flex-col items-center justify-center gap-0.5">
90
+ <span
91
+ className="text-3xl font-bold text-foreground"
92
+ style={{ fontFamily: 'var(--font-clash)' }}
93
+ >
94
+ {total.toLocaleString()}
95
+ </span>
96
+ <span className="text-xs uppercase tracking-wider text-muted-foreground">
97
+ Gesamt
98
+ </span>
99
+ </div>
100
+ </div>
116
101
  </div>
117
102
  )
118
103
  }
@@ -0,0 +1,171 @@
1
+ import React from 'react'
2
+ import { Plus } from 'lucide-react'
3
+ import { Badge } from '@/components/ui/badge'
4
+ import { Button } from '@/components/ui/button'
5
+ import {
6
+ Carousel,
7
+ CarouselContent,
8
+ CarouselItem,
9
+ CarouselPrevious,
10
+ CarouselNext,
11
+ } from '@/components/ui/carousel'
12
+ import { ScoreBar } from '@/components/ScoreBar/ScoreBar'
13
+ import { renderCell } from '../CellRenderers/CellRenderers'
14
+ import { cn } from '@/lib/utils'
15
+ import type { CardLayout, ColumnDef, ExpandColumnDef } from '../types'
16
+
17
+ export interface CardCarouselPanelProps {
18
+ title: string
19
+ items: Record<string, unknown>[]
20
+ expandColumns: ExpandColumnDef[]
21
+ columns: Record<string, ColumnDef>
22
+ layout: CardLayout
23
+ className?: string
24
+ }
25
+
26
+ function buildVirtualRow(
27
+ item: Record<string, unknown>,
28
+ expandColumns: ExpandColumnDef[],
29
+ columns: Record<string, ColumnDef>,
30
+ ): Record<string, unknown> {
31
+ const virtualRow: Record<string, unknown> = {}
32
+ for (const ec of expandColumns) {
33
+ if (ec.mapTo) {
34
+ virtualRow[ec.mapTo] = item[ec.key]
35
+ if (ec.secondaryKey) {
36
+ const mainCol = columns[ec.mapTo]
37
+ if (mainCol?.secondary) {
38
+ virtualRow[mainCol.secondary] = item[ec.secondaryKey]
39
+ }
40
+ }
41
+ }
42
+ }
43
+ return virtualRow
44
+ }
45
+
46
+ function CardCarouselPanel({
47
+ title,
48
+ items,
49
+ expandColumns,
50
+ columns,
51
+ layout,
52
+ className,
53
+ }: CardCarouselPanelProps) {
54
+ const scoreEc = expandColumns.find((ec) => ec.type === 'score-bar')
55
+
56
+ return (
57
+ <div
58
+ data-slot="card-carousel-panel"
59
+ className={cn('border border-border rounded-lg bg-card', className)}
60
+ >
61
+ <Carousel opts={{ align: 'start' }}>
62
+ <div className="flex items-center justify-between p-3 border-b border-border">
63
+ <div className="flex items-center gap-2">
64
+ <span className="text-sm font-semibold">{title}</span>
65
+ <Badge variant="secondary" className="text-xs">
66
+ {items.length}
67
+ </Badge>
68
+ </div>
69
+ <div className="flex items-center gap-1">
70
+ <CarouselPrevious className="static translate-y-0 size-7 rounded-md" />
71
+ <CarouselNext className="static translate-y-0 size-7 rounded-md" />
72
+ </div>
73
+ </div>
74
+
75
+ <CarouselContent className="p-3 -ml-3">
76
+ {items.map((item, index) => {
77
+ const virtualRow = buildVirtualRow(item, expandColumns, columns)
78
+ const titleValue = virtualRow[layout.titleField]
79
+ const subtitleValue = layout.subtitleField
80
+ ? virtualRow[layout.subtitleField]
81
+ : undefined
82
+
83
+ return (
84
+ <CarouselItem
85
+ key={index}
86
+ className="basis-1/3 pl-3 max-lg:basis-1/2 max-sm:basis-[85%]"
87
+ >
88
+ <div className="border border-border rounded-lg p-4 flex flex-col gap-3 h-full">
89
+ {/* Header — single-line truncate for carousel mini-card per spec */}
90
+ <div>
91
+ <div className="text-[13px] font-semibold truncate">
92
+ {titleValue != null ? String(titleValue) : ''}
93
+ </div>
94
+ {subtitleValue != null && (
95
+ <div className="text-xs text-muted-foreground mt-0.5 flex items-center gap-1">
96
+ {String(subtitleValue)}
97
+ </div>
98
+ )}
99
+ </div>
100
+
101
+ {/* Badges */}
102
+ {layout.badgeFields.length > 0 && (
103
+ <div className="flex flex-wrap gap-1">
104
+ {layout.badgeFields.map((bf) => {
105
+ const col = columns[bf]
106
+ if (!col || virtualRow[bf] == null) return null
107
+ return (
108
+ <React.Fragment key={bf}>
109
+ {renderCell(bf, col, virtualRow)}
110
+ </React.Fragment>
111
+ )
112
+ })}
113
+ </div>
114
+ )}
115
+
116
+ {/* Rows */}
117
+ {layout.rows.length > 0 && (
118
+ <div className="flex flex-col gap-1.5 flex-1">
119
+ {layout.rows.map((r) => {
120
+ if (virtualRow[r.field] == null) return null
121
+ const col = columns[r.field]
122
+ const valueNode = col
123
+ ? renderCell(
124
+ r.field,
125
+ col,
126
+ virtualRow,
127
+ r.rendererOverride === 'inventory-label'
128
+ ? { mode: 'inventory-label' }
129
+ : {},
130
+ )
131
+ : String(virtualRow[r.field] ?? '')
132
+ return (
133
+ <div
134
+ key={r.field}
135
+ className="flex justify-between items-center text-[13px]"
136
+ >
137
+ <span className="text-muted-foreground">{r.label}</span>
138
+ <span className="font-medium">{valueNode}</span>
139
+ </div>
140
+ )
141
+ })}
142
+ </div>
143
+ )}
144
+
145
+ {/* Score bar */}
146
+ {scoreEc && (
147
+ <ScoreBar
148
+ value={
149
+ typeof item[scoreEc.key] === 'number'
150
+ ? (item[scoreEc.key] as number)
151
+ : 0
152
+ }
153
+ />
154
+ )}
155
+
156
+ {/* Footer */}
157
+ <Button variant="outline" size="sm" className="w-full gap-1.5">
158
+ <Plus className="size-3.5" aria-hidden="true" />
159
+ Hinzufügen
160
+ </Button>
161
+ </div>
162
+ </CarouselItem>
163
+ )
164
+ })}
165
+ </CarouselContent>
166
+ </Carousel>
167
+ </div>
168
+ )
169
+ }
170
+
171
+ export { CardCarouselPanel }
@@ -0,0 +1,175 @@
1
+ import React from 'react'
2
+ import { ChevronDown, ArrowLeftRight, Sparkles } from 'lucide-react'
3
+ import { Badge } from '@/components/ui/badge'
4
+ import { Button } from '@/components/ui/button'
5
+ import { ScoreBar } from '@/components/ScoreBar/ScoreBar'
6
+ import { FavoriteButton } from '@/components/FavoriteButton/FavoriteButton'
7
+ import { cn } from '@/lib/utils'
8
+ import type { ColumnDef } from '../types'
9
+
10
+ export interface RenderCellOptions {
11
+ mode?: 'default' | 'compact' | 'inventory-label'
12
+ isExpanded?: boolean
13
+ isFavorite?: boolean
14
+ onToggleExpand?: () => void
15
+ onToggleFavorite?: () => void
16
+ }
17
+
18
+ const STATUS_COLORS: Record<string, string> = {
19
+ active: 'bg-[var(--lifecycle-active-bg)] text-[var(--lifecycle-active)] border-transparent',
20
+ nrnd: 'bg-[var(--lifecycle-nrnd-bg)] text-[var(--lifecycle-nrnd)] border-transparent',
21
+ eol: 'bg-[var(--lifecycle-eol-bg)] text-[var(--lifecycle-eol)] border-transparent',
22
+ production: 'bg-[var(--lifecycle-production-bg)] text-[var(--lifecycle-production)] border-transparent',
23
+ }
24
+
25
+ const INVENTORY_COLORS: Record<string, string> = {
26
+ high: 'bg-[var(--inventory-high-bg)] text-[var(--inventory-high)] border-transparent',
27
+ medium: 'bg-[var(--inventory-medium-bg)] text-[var(--inventory-medium)] border-transparent',
28
+ low: 'bg-[var(--inventory-low-bg)] text-[var(--inventory-low)] border-transparent',
29
+ }
30
+
31
+ const EXPAND_ICONS: Record<string, React.ElementType> = {
32
+ arrowLeftRight: ArrowLeftRight,
33
+ sparkles: Sparkles,
34
+ }
35
+
36
+ export function renderCell(
37
+ colKey: string,
38
+ col: ColumnDef,
39
+ row: Record<string, unknown>,
40
+ options: RenderCellOptions = {},
41
+ ): React.ReactNode {
42
+ const { mode = 'default', isExpanded = false, isFavorite = false, onToggleExpand, onToggleFavorite } = options
43
+ const val = row[colKey]
44
+
45
+ switch (col.type) {
46
+ case 'text': {
47
+ // col.rowLines drives the clamp; default 2 so columns can shrink without growing rows
48
+ const clamp = col.rowLines === 3 ? 'line-clamp-3' : 'line-clamp-2'
49
+ return (
50
+ <span className={cn(clamp, 'text-[13px]')}>
51
+ {val != null ? String(val) : ''}
52
+ </span>
53
+ )
54
+ }
55
+
56
+ case 'double-text': {
57
+ const secondary = col.secondary != null ? row[col.secondary] : undefined
58
+ return (
59
+ <>
60
+ <span className="font-medium text-[13px] line-clamp-2">
61
+ {val != null ? String(val) : ''}
62
+ </span>
63
+ <span className="text-muted-foreground text-xs line-clamp-1">
64
+ {secondary != null ? String(secondary) : ''}
65
+ </span>
66
+ </>
67
+ )
68
+ }
69
+
70
+ case 'link': {
71
+ return (
72
+ <a
73
+ href="#"
74
+ className="text-primary hover:underline font-medium text-[13px] line-clamp-3"
75
+ onClick={(e) => e.preventDefault()}
76
+ >
77
+ {val != null ? String(val) : ''}
78
+ </a>
79
+ )
80
+ }
81
+
82
+ case 'status-badge': {
83
+ if (val == null || !col.statusMap) return <span className="text-[13px]">{val != null ? String(val) : ''}</span>
84
+ const status = col.statusMap[String(val)] ?? 'active'
85
+ return (
86
+ <Badge className={STATUS_COLORS[status]}>
87
+ {String(val)}
88
+ </Badge>
89
+ )
90
+ }
91
+
92
+ case 'inventory': {
93
+ if (val == null || !col.levelFn) return null
94
+ const level = col.levelFn(Number(val))
95
+ if (mode === 'inventory-label') {
96
+ const label = col.labelMap?.[level] ?? level
97
+ return (
98
+ <Badge className={cn(INVENTORY_COLORS[level])}>
99
+ {label}
100
+ </Badge>
101
+ )
102
+ }
103
+ const formatted = col.formatFn ? col.formatFn(Number(val)) : String(val)
104
+ return (
105
+ <Badge className={cn(INVENTORY_COLORS[level])}>
106
+ {formatted}
107
+ </Badge>
108
+ )
109
+ }
110
+
111
+ case 'currency': {
112
+ if (val == null) return null
113
+ const currency = col.currencyField != null ? String(row[col.currencyField] ?? '') : ''
114
+ const formatted = Number(val).toLocaleString('de-DE', {
115
+ minimumFractionDigits: 2,
116
+ maximumFractionDigits: 2,
117
+ })
118
+ return (
119
+ <span className="tabular-nums font-medium text-[13px] whitespace-nowrap">
120
+ {formatted} {currency}
121
+ </span>
122
+ )
123
+ }
124
+
125
+ case 'score-bar': {
126
+ return <ScoreBar value={val != null ? Number(val) : 0} />
127
+ }
128
+
129
+ case 'favorite': {
130
+ const itemName = String(row['name'] ?? row['title'] ?? '')
131
+ return (
132
+ <FavoriteButton
133
+ pressed={isFavorite}
134
+ itemName={itemName}
135
+ onPressedChange={() => onToggleFavorite?.()}
136
+ />
137
+ )
138
+ }
139
+
140
+ case 'expand': {
141
+ const items = val as unknown[]
142
+ const count = Array.isArray(items) ? items.length : 0
143
+ if (count === 0) {
144
+ return <span className="text-[13px] text-muted-foreground">—</span>
145
+ }
146
+
147
+ const ExpandIcon = col.expandIcon ? EXPAND_ICONS[col.expandIcon] : null
148
+
149
+ return (
150
+ <Button
151
+ variant="outline"
152
+ size="sm"
153
+ aria-expanded={isExpanded}
154
+ aria-label={col.expandLabel ?? colKey}
155
+ onClick={onToggleExpand}
156
+ className={cn(
157
+ 'gap-1.5',
158
+ isExpanded && 'border-primary text-primary bg-primary/5',
159
+ )}
160
+ >
161
+ {ExpandIcon && <ExpandIcon className="size-3.5" aria-hidden="true" />}
162
+ {count}
163
+ <ChevronDown
164
+ className={cn('size-3.5 transition-transform duration-200', isExpanded && 'rotate-180')}
165
+ aria-hidden="true"
166
+ />
167
+ </Button>
168
+ )
169
+ }
170
+
171
+ default: {
172
+ return <span className="text-[13px]">{val != null ? String(val) : ''}</span>
173
+ }
174
+ }
175
+ }
@@ -0,0 +1,124 @@
1
+ import React from 'react'
2
+ import { Settings, GripVertical } from 'lucide-react'
3
+ import {
4
+ DndContext,
5
+ closestCenter,
6
+ DragEndEvent,
7
+ } from '@dnd-kit/core'
8
+ import {
9
+ SortableContext,
10
+ verticalListSortingStrategy,
11
+ useSortable,
12
+ } from '@dnd-kit/sortable'
13
+ import { CSS } from '@dnd-kit/utilities'
14
+ import { restrictToVerticalAxis } from '@dnd-kit/modifiers'
15
+ import { Button } from '@/components/ui/button'
16
+ import { Popover, PopoverContent, PopoverTrigger } from '@/components/ui/popover'
17
+ import { Switch } from '@/components/ui/switch'
18
+ import { cn } from '@/lib/utils'
19
+ import type { ColumnDef } from '../types'
20
+
21
+ export interface ColumnConfigPopoverProps {
22
+ columns: Record<string, ColumnDef>
23
+ columnOrder: string[]
24
+ columnVisibility: Record<string, boolean>
25
+ onReorderColumn: (activeId: string, overId: string) => void
26
+ onToggleVisibility: (key: string) => void
27
+ className?: string
28
+ }
29
+
30
+ interface SortableColumnItemProps {
31
+ colKey: string
32
+ label: string
33
+ visible: boolean
34
+ onToggle: (key: string) => void
35
+ }
36
+
37
+ function SortableColumnItem({ colKey, label, visible, onToggle }: SortableColumnItemProps) {
38
+ const { attributes, listeners, setNodeRef, transform, transition, isDragging } = useSortable({
39
+ id: colKey,
40
+ })
41
+
42
+ const style = {
43
+ transform: CSS.Transform.toString(transform),
44
+ transition,
45
+ }
46
+
47
+ return (
48
+ <div
49
+ ref={setNodeRef}
50
+ style={style}
51
+ className={cn(
52
+ 'flex items-center gap-2 px-3 py-2 rounded-md',
53
+ isDragging && 'bg-accent shadow-md z-10 relative',
54
+ )}
55
+ >
56
+ <button
57
+ {...attributes}
58
+ {...listeners}
59
+ className="cursor-grab active:cursor-grabbing text-muted-foreground shrink-0"
60
+ aria-label={`${label} verschieben`}
61
+ tabIndex={0}
62
+ >
63
+ <GripVertical className="size-4" />
64
+ </button>
65
+ <span className="flex-1 text-sm">{label}</span>
66
+ <Switch
67
+ checked={visible}
68
+ onCheckedChange={() => onToggle(colKey)}
69
+ aria-label={visible ? 'Spalte ausblenden' : 'Spalte einblenden'}
70
+ />
71
+ </div>
72
+ )
73
+ }
74
+
75
+ export function ColumnConfigPopover({
76
+ columns,
77
+ columnOrder,
78
+ columnVisibility,
79
+ onReorderColumn,
80
+ onToggleVisibility,
81
+ className,
82
+ }: ColumnConfigPopoverProps) {
83
+ function handleDragEnd(event: DragEndEvent) {
84
+ const { active, over } = event
85
+ if (over && active.id !== over.id) {
86
+ onReorderColumn(active.id as string, over.id as string)
87
+ }
88
+ }
89
+
90
+ return (
91
+ <Popover>
92
+ <PopoverTrigger asChild>
93
+ <Button variant="outline" size="sm" className={className}>
94
+ <Settings className="size-4" />
95
+ <span className="max-sm:hidden">Konfigurieren</span>
96
+ </Button>
97
+ </PopoverTrigger>
98
+ <PopoverContent className="w-64 p-0" align="end">
99
+ <div className="p-3 border-b border-border">
100
+ <span className="text-sm font-semibold">Spalten konfigurieren</span>
101
+ </div>
102
+ <DndContext
103
+ collisionDetection={closestCenter}
104
+ onDragEnd={handleDragEnd}
105
+ modifiers={[restrictToVerticalAxis]}
106
+ >
107
+ <SortableContext items={columnOrder} strategy={verticalListSortingStrategy}>
108
+ <div className="p-1">
109
+ {columnOrder.map((key) => (
110
+ <SortableColumnItem
111
+ key={key}
112
+ colKey={key}
113
+ label={columns[key]?.label ?? key}
114
+ visible={columnVisibility[key] ?? true}
115
+ onToggle={onToggleVisibility}
116
+ />
117
+ ))}
118
+ </div>
119
+ </SortableContext>
120
+ </DndContext>
121
+ </PopoverContent>
122
+ </Popover>
123
+ )
124
+ }