@olwiba/ui 0.2.0 → 0.2.2
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/dist/index.d.ts +57 -1
- package/dist/index.js +349 -121
- package/dist/index.js.map +1 -1
- package/package.json +3 -3
- package/src/app/AppShell.tsx +12 -5
- package/src/components/Chart.tsx +245 -0
- package/src/index.ts +2 -0
- package/src/mechanics/Sortable.tsx +128 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@olwiba/ui",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.2",
|
|
4
4
|
"private": false,
|
|
5
5
|
"type": "module",
|
|
6
6
|
"sideEffects": false,
|
|
@@ -63,8 +63,8 @@
|
|
|
63
63
|
},
|
|
64
64
|
"devDependencies": {
|
|
65
65
|
"@content-collections/mdx": "^0.2.2",
|
|
66
|
-
"@olwiba/cn": "0.1.
|
|
67
|
-
"@olwiba/docs": "0.1.
|
|
66
|
+
"@olwiba/cn": "0.1.26",
|
|
67
|
+
"@olwiba/docs": "0.1.35",
|
|
68
68
|
"@tailwindcss/vite": "^4.1.18",
|
|
69
69
|
"@tanstack/react-router": "1.154.8",
|
|
70
70
|
"@tanstack/react-router-devtools": "1.154.8",
|
package/src/app/AppShell.tsx
CHANGED
|
@@ -127,7 +127,7 @@ function NavUser({ user }: { user: AppShellUser }) {
|
|
|
127
127
|
<DropdownMenuTrigger asChild>
|
|
128
128
|
<SidebarMenuButton
|
|
129
129
|
size="lg"
|
|
130
|
-
className="data-[state=open]:bg-sidebar-accent data-[state=open]:text-sidebar-accent-foreground"
|
|
130
|
+
className="data-[state=open]:bg-sidebar-accent data-[state=open]:text-sidebar-accent-foreground group-data-[collapsible=icon]:!size-8 group-data-[collapsible=icon]:!min-w-8 group-data-[collapsible=icon]:!max-w-8 group-data-[collapsible=icon]:!p-0"
|
|
131
131
|
>
|
|
132
132
|
<Avatar mode={avatarMode} size="sm" className="grayscale">
|
|
133
133
|
{user.avatar && <AvatarImage src={user.avatar} alt={user.name} />}
|
|
@@ -218,20 +218,27 @@ function ShellSidebar({
|
|
|
218
218
|
|
|
219
219
|
return (
|
|
220
220
|
<Sidebar side={side} collapsible={collapsible} sidebarPosition={sidebarPosition}>
|
|
221
|
-
<SidebarHeader>
|
|
221
|
+
<SidebarHeader className="py-3">
|
|
222
222
|
<SidebarMenu>
|
|
223
223
|
<SidebarMenuItem>
|
|
224
|
-
<SidebarMenuButton
|
|
224
|
+
<SidebarMenuButton
|
|
225
|
+
asChild
|
|
226
|
+
size="lg"
|
|
227
|
+
tooltip={typeof brand.name === 'string' ? brand.name : undefined}
|
|
228
|
+
className="group-data-[collapsible=icon]:!size-8 group-data-[collapsible=icon]:!min-w-8 group-data-[collapsible=icon]:!max-w-8 group-data-[collapsible=icon]:!p-0"
|
|
229
|
+
>
|
|
225
230
|
{renderLink({
|
|
226
231
|
href: brand.href ?? '#',
|
|
227
232
|
children: (
|
|
228
233
|
<>
|
|
229
234
|
{brand.logo ? (
|
|
230
|
-
|
|
235
|
+
// Neutral slot: the consumer's logo owns its own chrome
|
|
236
|
+
// (background, radius). Sized to fill the collapsed icon rail.
|
|
237
|
+
<span className="flex size-8 shrink-0 items-center justify-center [&>svg]:size-5">
|
|
231
238
|
{brand.logo}
|
|
232
239
|
</span>
|
|
233
240
|
) : (
|
|
234
|
-
<span className="flex size-
|
|
241
|
+
<span className="flex size-8 shrink-0 items-center justify-center rounded-lg bg-primary text-sm font-semibold text-primary-foreground">
|
|
235
242
|
{fallbackLogo}
|
|
236
243
|
</span>
|
|
237
244
|
)}
|
|
@@ -0,0 +1,245 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import * as React from 'react';
|
|
4
|
+
import {
|
|
5
|
+
Area,
|
|
6
|
+
AreaChart,
|
|
7
|
+
Bar,
|
|
8
|
+
BarChart,
|
|
9
|
+
CartesianGrid,
|
|
10
|
+
Cell,
|
|
11
|
+
Line,
|
|
12
|
+
LineChart,
|
|
13
|
+
Pie,
|
|
14
|
+
PieChart,
|
|
15
|
+
ResponsiveContainer,
|
|
16
|
+
Tooltip,
|
|
17
|
+
XAxis,
|
|
18
|
+
YAxis,
|
|
19
|
+
} from 'recharts';
|
|
20
|
+
import { cn } from '@olwiba/cn';
|
|
21
|
+
|
|
22
|
+
export interface ChartSeries {
|
|
23
|
+
/** Data key to plot. */
|
|
24
|
+
key: string;
|
|
25
|
+
/** Legend and tooltip label. @default key */
|
|
26
|
+
label?: string;
|
|
27
|
+
/** CSS color. @default theme tokens --chart-1..5 in fixed order */
|
|
28
|
+
color?: string;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export interface ChartProps {
|
|
32
|
+
/** Chart form. @default 'line' */
|
|
33
|
+
type?: 'line' | 'area' | 'bar' | 'donut';
|
|
34
|
+
data: Array<Record<string, string | number>>;
|
|
35
|
+
/** Key for x-axis categories (line/area/bar) or slice labels (donut). */
|
|
36
|
+
xKey: string;
|
|
37
|
+
/** Series to plot. Donut uses `series[0].key` as the slice value. */
|
|
38
|
+
series: ChartSeries[];
|
|
39
|
+
/** Chart height in px. @default 300 */
|
|
40
|
+
height?: number;
|
|
41
|
+
/** Horizontal grid lines. @default true (ignored for donut) */
|
|
42
|
+
grid?: boolean;
|
|
43
|
+
/** Legend. @default true for multiple series or donut, false for one series */
|
|
44
|
+
legend?: boolean;
|
|
45
|
+
/** Formats values in tooltips and the y-axis, e.g. `(v) => \`$${v}\``. */
|
|
46
|
+
valueFormatter?: (value: number) => string;
|
|
47
|
+
className?: string;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// Fixed categorical order — series N always gets token N, never cycled.
|
|
51
|
+
const TOKEN_COLORS = ['var(--chart-1)', 'var(--chart-2)', 'var(--chart-3)', 'var(--chart-4)', 'var(--chart-5)'];
|
|
52
|
+
|
|
53
|
+
const seriesColor = (s: ChartSeries, i: number) => s.color ?? TOKEN_COLORS[i % TOKEN_COLORS.length];
|
|
54
|
+
|
|
55
|
+
function ChartTooltip({
|
|
56
|
+
active,
|
|
57
|
+
payload,
|
|
58
|
+
label,
|
|
59
|
+
valueFormatter,
|
|
60
|
+
}: {
|
|
61
|
+
active?: boolean;
|
|
62
|
+
payload?: Array<{ name?: string; value?: number | string; color?: string; payload?: { fill?: string } }>;
|
|
63
|
+
label?: string | number;
|
|
64
|
+
valueFormatter: (value: number) => string;
|
|
65
|
+
}) {
|
|
66
|
+
if (!active || !payload?.length) return null;
|
|
67
|
+
return (
|
|
68
|
+
<div className="rounded-lg border bg-popover px-3 py-2 text-xs text-popover-foreground shadow-md">
|
|
69
|
+
{label !== undefined && <p className="mb-1 font-medium">{label}</p>}
|
|
70
|
+
<div className="space-y-1">
|
|
71
|
+
{payload.map((entry, i) => (
|
|
72
|
+
<div key={i} className="flex items-center gap-2">
|
|
73
|
+
<span
|
|
74
|
+
aria-hidden
|
|
75
|
+
className="size-2 shrink-0 rounded-full"
|
|
76
|
+
style={{ background: entry.color ?? entry.payload?.fill }}
|
|
77
|
+
/>
|
|
78
|
+
<span className="text-muted-foreground">{entry.name}</span>
|
|
79
|
+
<span className="ml-auto pl-3 font-medium tabular-nums">
|
|
80
|
+
{typeof entry.value === 'number' ? valueFormatter(entry.value) : entry.value}
|
|
81
|
+
</span>
|
|
82
|
+
</div>
|
|
83
|
+
))}
|
|
84
|
+
</div>
|
|
85
|
+
</div>
|
|
86
|
+
);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function ChartLegend({ entries }: { entries: Array<{ label: string; color: string }> }) {
|
|
90
|
+
return (
|
|
91
|
+
<div className="mt-3 flex flex-wrap items-center justify-center gap-x-4 gap-y-1">
|
|
92
|
+
{entries.map((entry) => (
|
|
93
|
+
<div key={entry.label} className="flex items-center gap-1.5 text-xs text-muted-foreground">
|
|
94
|
+
<span aria-hidden className="size-2 rounded-full" style={{ background: entry.color }} />
|
|
95
|
+
{entry.label}
|
|
96
|
+
</div>
|
|
97
|
+
))}
|
|
98
|
+
</div>
|
|
99
|
+
);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
const axisProps = {
|
|
103
|
+
tickLine: false,
|
|
104
|
+
axisLine: false,
|
|
105
|
+
tick: { fill: 'var(--muted-foreground)', fontSize: 12 },
|
|
106
|
+
tickMargin: 8,
|
|
107
|
+
} as const;
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Theme-aware chart — one component, `type` switches the form. Colors come
|
|
111
|
+
* from the `--chart-1..5` theme tokens in fixed order (override per series
|
|
112
|
+
* via `color`). Line and area for change-over-time, bar for magnitude
|
|
113
|
+
* comparison, donut for part-of-whole identity. More than 5 donut slices:
|
|
114
|
+
* fold the tail into an "Other" slice in your data instead of adding hues.
|
|
115
|
+
*/
|
|
116
|
+
export function Chart({
|
|
117
|
+
type = 'line',
|
|
118
|
+
data,
|
|
119
|
+
xKey,
|
|
120
|
+
series,
|
|
121
|
+
height = 300,
|
|
122
|
+
grid = true,
|
|
123
|
+
legend,
|
|
124
|
+
valueFormatter = (v) => String(v),
|
|
125
|
+
className,
|
|
126
|
+
}: ChartProps) {
|
|
127
|
+
const showLegend = legend ?? (type === 'donut' || series.length > 1);
|
|
128
|
+
const tooltip = (
|
|
129
|
+
<Tooltip
|
|
130
|
+
cursor={type === 'bar' ? { fill: 'var(--muted)', opacity: 0.4 } : { stroke: 'var(--border)' }}
|
|
131
|
+
content={<ChartTooltip valueFormatter={valueFormatter} />}
|
|
132
|
+
/>
|
|
133
|
+
);
|
|
134
|
+
const gridEl = grid ? (
|
|
135
|
+
<CartesianGrid vertical={false} stroke="var(--border)" strokeOpacity={0.6} />
|
|
136
|
+
) : null;
|
|
137
|
+
|
|
138
|
+
let chart: React.ReactElement;
|
|
139
|
+
let legendEntries: Array<{ label: string; color: string }>;
|
|
140
|
+
|
|
141
|
+
if (type === 'donut') {
|
|
142
|
+
const valueKey = series[0]?.key;
|
|
143
|
+
legendEntries = data.map((row, i) => ({
|
|
144
|
+
label: String(row[xKey]),
|
|
145
|
+
color: TOKEN_COLORS[i % TOKEN_COLORS.length],
|
|
146
|
+
}));
|
|
147
|
+
chart = (
|
|
148
|
+
<PieChart>
|
|
149
|
+
<Pie
|
|
150
|
+
data={data}
|
|
151
|
+
dataKey={valueKey}
|
|
152
|
+
nameKey={xKey}
|
|
153
|
+
innerRadius="60%"
|
|
154
|
+
outerRadius="85%"
|
|
155
|
+
paddingAngle={2}
|
|
156
|
+
stroke="var(--card)"
|
|
157
|
+
strokeWidth={2}
|
|
158
|
+
>
|
|
159
|
+
{data.map((row, i) => (
|
|
160
|
+
<Cell key={String(row[xKey])} fill={TOKEN_COLORS[i % TOKEN_COLORS.length]} />
|
|
161
|
+
))}
|
|
162
|
+
</Pie>
|
|
163
|
+
{tooltip}
|
|
164
|
+
</PieChart>
|
|
165
|
+
);
|
|
166
|
+
} else {
|
|
167
|
+
legendEntries = series.map((s, i) => ({ label: s.label ?? s.key, color: seriesColor(s, i) }));
|
|
168
|
+
|
|
169
|
+
if (type === 'bar') {
|
|
170
|
+
chart = (
|
|
171
|
+
<BarChart data={data} barCategoryGap="25%">
|
|
172
|
+
{gridEl}
|
|
173
|
+
<XAxis dataKey={xKey} {...axisProps} />
|
|
174
|
+
<YAxis {...axisProps} width={48} tickFormatter={valueFormatter} />
|
|
175
|
+
{tooltip}
|
|
176
|
+
{series.map((s, i) => (
|
|
177
|
+
<Bar
|
|
178
|
+
key={s.key}
|
|
179
|
+
dataKey={s.key}
|
|
180
|
+
name={s.label ?? s.key}
|
|
181
|
+
fill={seriesColor(s, i)}
|
|
182
|
+
radius={[4, 4, 0, 0]}
|
|
183
|
+
maxBarSize={40}
|
|
184
|
+
/>
|
|
185
|
+
))}
|
|
186
|
+
</BarChart>
|
|
187
|
+
);
|
|
188
|
+
} else if (type === 'area') {
|
|
189
|
+
chart = (
|
|
190
|
+
<AreaChart data={data}>
|
|
191
|
+
{gridEl}
|
|
192
|
+
<XAxis dataKey={xKey} {...axisProps} />
|
|
193
|
+
<YAxis {...axisProps} width={48} tickFormatter={valueFormatter} />
|
|
194
|
+
{tooltip}
|
|
195
|
+
{series.map((s, i) => (
|
|
196
|
+
<Area
|
|
197
|
+
key={s.key}
|
|
198
|
+
type="monotone"
|
|
199
|
+
dataKey={s.key}
|
|
200
|
+
name={s.label ?? s.key}
|
|
201
|
+
stroke={seriesColor(s, i)}
|
|
202
|
+
strokeWidth={2}
|
|
203
|
+
fill={seriesColor(s, i)}
|
|
204
|
+
fillOpacity={0.12}
|
|
205
|
+
dot={false}
|
|
206
|
+
activeDot={{ r: 4 }}
|
|
207
|
+
/>
|
|
208
|
+
))}
|
|
209
|
+
</AreaChart>
|
|
210
|
+
);
|
|
211
|
+
} else {
|
|
212
|
+
chart = (
|
|
213
|
+
<LineChart data={data}>
|
|
214
|
+
{gridEl}
|
|
215
|
+
<XAxis dataKey={xKey} {...axisProps} />
|
|
216
|
+
<YAxis {...axisProps} width={48} tickFormatter={valueFormatter} />
|
|
217
|
+
{tooltip}
|
|
218
|
+
{series.map((s, i) => (
|
|
219
|
+
<Line
|
|
220
|
+
key={s.key}
|
|
221
|
+
type="monotone"
|
|
222
|
+
dataKey={s.key}
|
|
223
|
+
name={s.label ?? s.key}
|
|
224
|
+
stroke={seriesColor(s, i)}
|
|
225
|
+
strokeWidth={2}
|
|
226
|
+
dot={false}
|
|
227
|
+
activeDot={{ r: 4 }}
|
|
228
|
+
/>
|
|
229
|
+
))}
|
|
230
|
+
</LineChart>
|
|
231
|
+
);
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
return (
|
|
236
|
+
<div className={cn('w-full', className)}>
|
|
237
|
+
<div style={{ height }}>
|
|
238
|
+
<ResponsiveContainer width="100%" height="100%">
|
|
239
|
+
{chart}
|
|
240
|
+
</ResponsiveContainer>
|
|
241
|
+
</div>
|
|
242
|
+
{showLegend && <ChartLegend entries={legendEntries} />}
|
|
243
|
+
</div>
|
|
244
|
+
);
|
|
245
|
+
}
|
package/src/index.ts
CHANGED
|
@@ -115,6 +115,7 @@ export { PageTransition, type PageTransitionProps } from './motion/PageTransitio
|
|
|
115
115
|
|
|
116
116
|
// ─── Mechanics — behavior wrappers that play any children ────────────────────
|
|
117
117
|
export { Carousel, type CarouselProps } from './mechanics/Carousel';
|
|
118
|
+
export { Sortable, type SortableProps } from './mechanics/Sortable';
|
|
118
119
|
|
|
119
120
|
// ─── Components — interactive ────────────────────────────────────────────────
|
|
120
121
|
export { Spotlight, type SpotlightProps, type SpotlightGroup, type SpotlightItem } from './components/Spotlight';
|
|
@@ -125,6 +126,7 @@ export { CommandMenu, type CommandMenuProps, type CommandMenuGroup, type Command
|
|
|
125
126
|
|
|
126
127
|
// ─── Components — data ───────────────────────────────────────────────────────
|
|
127
128
|
export { DataTable, type DataTableProps, type DataTableColumn } from './components/DataTable';
|
|
129
|
+
export { Chart, type ChartProps, type ChartSeries } from './components/Chart';
|
|
128
130
|
export { FileUpload, type FileUploadProps, type FileUploadEntry } from './components/FileUpload';
|
|
129
131
|
|
|
130
132
|
// ─── Components — notifications ──────────────────────────────────────────────
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import * as React from 'react';
|
|
4
|
+
import {
|
|
5
|
+
DndContext,
|
|
6
|
+
KeyboardSensor,
|
|
7
|
+
PointerSensor,
|
|
8
|
+
closestCenter,
|
|
9
|
+
useSensor,
|
|
10
|
+
useSensors,
|
|
11
|
+
type DragEndEvent,
|
|
12
|
+
} from '@dnd-kit/core';
|
|
13
|
+
import {
|
|
14
|
+
SortableContext,
|
|
15
|
+
arrayMove,
|
|
16
|
+
horizontalListSortingStrategy,
|
|
17
|
+
rectSortingStrategy,
|
|
18
|
+
sortableKeyboardCoordinates,
|
|
19
|
+
useSortable,
|
|
20
|
+
verticalListSortingStrategy,
|
|
21
|
+
} from '@dnd-kit/sortable';
|
|
22
|
+
import { CSS } from '@dnd-kit/utilities';
|
|
23
|
+
import { cn } from '@olwiba/cn';
|
|
24
|
+
|
|
25
|
+
export interface SortableProps {
|
|
26
|
+
/** Item ids in render order — one per child, same order as `children`. */
|
|
27
|
+
items: string[];
|
|
28
|
+
/** Fired with the new id order after a drag completes. */
|
|
29
|
+
onReorder: (items: string[]) => void;
|
|
30
|
+
/** One element per id — pairing is by position, so keep orders aligned. */
|
|
31
|
+
children: React.ReactNode;
|
|
32
|
+
/** Layout of the sortable collection. @default 'vertical' */
|
|
33
|
+
direction?: 'vertical' | 'horizontal' | 'grid';
|
|
34
|
+
disabled?: boolean;
|
|
35
|
+
className?: string;
|
|
36
|
+
itemClassName?: string;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const strategies = {
|
|
40
|
+
vertical: verticalListSortingStrategy,
|
|
41
|
+
horizontal: horizontalListSortingStrategy,
|
|
42
|
+
grid: rectSortingStrategy,
|
|
43
|
+
} as const;
|
|
44
|
+
|
|
45
|
+
const containerClasses = {
|
|
46
|
+
vertical: 'flex flex-col gap-2',
|
|
47
|
+
horizontal: 'flex gap-2',
|
|
48
|
+
grid: 'grid gap-2',
|
|
49
|
+
} as const;
|
|
50
|
+
|
|
51
|
+
function SortableItem({
|
|
52
|
+
id,
|
|
53
|
+
disabled,
|
|
54
|
+
className,
|
|
55
|
+
children,
|
|
56
|
+
}: {
|
|
57
|
+
id: string;
|
|
58
|
+
disabled?: boolean;
|
|
59
|
+
className?: string;
|
|
60
|
+
children: React.ReactNode;
|
|
61
|
+
}) {
|
|
62
|
+
const { attributes, listeners, setNodeRef, transform, transition, isDragging } = useSortable({ id, disabled });
|
|
63
|
+
|
|
64
|
+
return (
|
|
65
|
+
<div
|
|
66
|
+
ref={setNodeRef}
|
|
67
|
+
style={{ transform: CSS.Transform.toString(transform), transition }}
|
|
68
|
+
className={cn(
|
|
69
|
+
'touch-none',
|
|
70
|
+
!disabled && 'cursor-grab active:cursor-grabbing',
|
|
71
|
+
isDragging && 'relative z-10 opacity-80',
|
|
72
|
+
className,
|
|
73
|
+
)}
|
|
74
|
+
{...attributes}
|
|
75
|
+
{...listeners}
|
|
76
|
+
>
|
|
77
|
+
{children}
|
|
78
|
+
</div>
|
|
79
|
+
);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Drag-to-reorder behavior — wraps any children and makes them sortable by
|
|
84
|
+
* pointer or keyboard. A mechanic, not a list component: feed it cards, rows,
|
|
85
|
+
* or tiles. Controlled: pass `items` (ids in order) and apply the new order
|
|
86
|
+
* in `onReorder`.
|
|
87
|
+
*/
|
|
88
|
+
export function Sortable({
|
|
89
|
+
items,
|
|
90
|
+
onReorder,
|
|
91
|
+
children,
|
|
92
|
+
direction = 'vertical',
|
|
93
|
+
disabled,
|
|
94
|
+
className,
|
|
95
|
+
itemClassName,
|
|
96
|
+
}: SortableProps) {
|
|
97
|
+
const sensors = useSensors(
|
|
98
|
+
useSensor(PointerSensor, { activationConstraint: { distance: 4 } }),
|
|
99
|
+
useSensor(KeyboardSensor, { coordinateGetter: sortableKeyboardCoordinates }),
|
|
100
|
+
);
|
|
101
|
+
|
|
102
|
+
const handleDragEnd = (event: DragEndEvent) => {
|
|
103
|
+
const { active, over } = event;
|
|
104
|
+
if (!over || active.id === over.id) return;
|
|
105
|
+
const oldIndex = items.indexOf(String(active.id));
|
|
106
|
+
const newIndex = items.indexOf(String(over.id));
|
|
107
|
+
if (oldIndex === -1 || newIndex === -1) return;
|
|
108
|
+
onReorder(arrayMove(items, oldIndex, newIndex));
|
|
109
|
+
};
|
|
110
|
+
|
|
111
|
+
const childArray = React.Children.toArray(children);
|
|
112
|
+
|
|
113
|
+
return (
|
|
114
|
+
<DndContext sensors={sensors} collisionDetection={closestCenter} onDragEnd={handleDragEnd}>
|
|
115
|
+
<SortableContext items={items} strategy={strategies[direction]}>
|
|
116
|
+
<div className={cn(containerClasses[direction], className)}>
|
|
117
|
+
{childArray.map((child, i) =>
|
|
118
|
+
items[i] === undefined ? null : (
|
|
119
|
+
<SortableItem key={items[i]} id={items[i]} disabled={disabled} className={itemClassName}>
|
|
120
|
+
{child}
|
|
121
|
+
</SortableItem>
|
|
122
|
+
),
|
|
123
|
+
)}
|
|
124
|
+
</div>
|
|
125
|
+
</SortableContext>
|
|
126
|
+
</DndContext>
|
|
127
|
+
);
|
|
128
|
+
}
|