@kinetixui/ui 0.11.0 → 0.13.0
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 +364 -1
- package/dist/index.js +2209 -493
- package/package.json +6 -2
- package/src/components/alert.tsx +1 -1
- package/src/components/app-bar.tsx +2 -2
- package/src/components/color-picker.tsx +352 -0
- package/src/components/data-grid.tsx +304 -0
- package/src/components/dialog.tsx +3 -2
- package/src/components/diff-viewer.tsx +155 -0
- package/src/components/direction-provider.tsx +27 -0
- package/src/components/drawer.tsx +1 -1
- package/src/components/dropdown-menu.tsx +9 -9
- package/src/components/input-group.tsx +2 -2
- package/src/components/json-viewer.tsx +168 -0
- package/src/components/kanban-board.tsx +247 -0
- package/src/components/markdown-editor.tsx +288 -0
- package/src/components/message-bubble.tsx +83 -0
- package/src/components/multi-select.tsx +134 -0
- package/src/components/native-select.tsx +2 -2
- package/src/components/notification-center.tsx +119 -0
- package/src/components/select.tsx +2 -2
- package/src/components/sheet.tsx +5 -2
- package/src/components/tour.tsx +156 -0
- package/src/components/tree-view.tsx +273 -0
- package/src/components/virtual-list.tsx +92 -0
- package/src/index.ts +24 -0
- package/src/stories/ColorPicker.stories.tsx +28 -0
- package/src/stories/DataGrid.stories.tsx +54 -0
- package/src/stories/DiffViewer.stories.tsx +26 -0
- package/src/stories/JsonViewer.stories.tsx +31 -0
- package/src/stories/KanbanBoard.stories.tsx +44 -0
- package/src/stories/MarkdownEditor.stories.tsx +25 -0
- package/src/stories/MessageBubble.stories.tsx +32 -0
- package/src/stories/MultiSelect.stories.tsx +35 -0
- package/src/stories/NotificationCenter.stories.tsx +41 -0
- package/src/stories/Tour.stories.tsx +70 -0
- package/src/stories/TreeView.stories.tsx +38 -0
- package/src/stories/VirtualList.stories.tsx +36 -0
- package/tailwind.config.ts +2 -0
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import * as React from "react";
|
|
4
|
+
import { Check, ChevronRight, Copy } from "lucide-react";
|
|
5
|
+
import { cn } from "../lib/utils";
|
|
6
|
+
|
|
7
|
+
export interface JsonViewerProps extends Omit<React.HTMLAttributes<HTMLDivElement>, "children"> {
|
|
8
|
+
data: unknown;
|
|
9
|
+
/** tree depth (root = 0) to auto-expand; deeper nodes start collapsed. Default 1. */
|
|
10
|
+
expandDepth?: number;
|
|
11
|
+
hideCopy?: boolean;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* JsonViewer — a collapsible, syntax-colored tree for arbitrary JSON data
|
|
16
|
+
* (API responses, registry payloads, token diffs — a dev-tool surface the
|
|
17
|
+
* site itself needs for `/docs/kinetixui-json` and friends). Distinct from
|
|
18
|
+
* `TreeView`: this renders a *data structure* (object/array/primitive),
|
|
19
|
+
* not a caller-composed hierarchy of `TreeItem`s, so expand state is
|
|
20
|
+
* per-node uncontrolled rather than a lifted `expanded` prop — there's no
|
|
21
|
+
* stable "value" to key a node by beyond its position in the data, and
|
|
22
|
+
* nothing external needs to drive which nodes are open. Value colors reuse
|
|
23
|
+
* the existing semantic tokens (`--success`/`--info`/`--warning`) rather
|
|
24
|
+
* than introducing a separate syntax-highlighting palette. Gap-fill
|
|
25
|
+
* addition (not in the original Figma source).
|
|
26
|
+
*/
|
|
27
|
+
const JsonViewer = React.forwardRef<HTMLDivElement, JsonViewerProps>(
|
|
28
|
+
({ data, expandDepth = 1, hideCopy, className, ...props }, ref) => {
|
|
29
|
+
const [copied, setCopied] = React.useState(false);
|
|
30
|
+
|
|
31
|
+
const copy = async () => {
|
|
32
|
+
try {
|
|
33
|
+
await navigator.clipboard.writeText(JSON.stringify(data, null, 2));
|
|
34
|
+
setCopied(true);
|
|
35
|
+
window.setTimeout(() => setCopied(false), 1500);
|
|
36
|
+
} catch {
|
|
37
|
+
// clipboard unavailable — no-op
|
|
38
|
+
}
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
return (
|
|
42
|
+
<div
|
|
43
|
+
ref={ref}
|
|
44
|
+
className={cn("relative overflow-auto rounded-md border bg-muted/30 p-3 font-mono text-xs", className)}
|
|
45
|
+
{...props}
|
|
46
|
+
>
|
|
47
|
+
{!hideCopy && (
|
|
48
|
+
<button
|
|
49
|
+
type="button"
|
|
50
|
+
onClick={copy}
|
|
51
|
+
aria-label="Copy JSON"
|
|
52
|
+
className="absolute right-2 top-2 inline-flex size-6 items-center justify-center rounded-sm text-muted-foreground hover:bg-accent hover:text-foreground"
|
|
53
|
+
>
|
|
54
|
+
{copied ? <Check className="size-3.5" /> : <Copy className="size-3.5" />}
|
|
55
|
+
</button>
|
|
56
|
+
)}
|
|
57
|
+
<div role="tree">
|
|
58
|
+
<JsonNode value={data} depth={0} expandDepth={expandDepth} isLast />
|
|
59
|
+
</div>
|
|
60
|
+
</div>
|
|
61
|
+
);
|
|
62
|
+
},
|
|
63
|
+
);
|
|
64
|
+
JsonViewer.displayName = "JsonViewer";
|
|
65
|
+
|
|
66
|
+
function JsonNode({
|
|
67
|
+
name,
|
|
68
|
+
value,
|
|
69
|
+
depth,
|
|
70
|
+
expandDepth,
|
|
71
|
+
isLast,
|
|
72
|
+
}: {
|
|
73
|
+
name?: string;
|
|
74
|
+
value: unknown;
|
|
75
|
+
depth: number;
|
|
76
|
+
expandDepth: number;
|
|
77
|
+
isLast: boolean;
|
|
78
|
+
}) {
|
|
79
|
+
const isContainer = value !== null && typeof value === "object";
|
|
80
|
+
const [expanded, setExpanded] = React.useState(depth < expandDepth);
|
|
81
|
+
|
|
82
|
+
if (!isContainer) {
|
|
83
|
+
return (
|
|
84
|
+
<div role="treeitem" style={{ paddingLeft: depth * 16 }}>
|
|
85
|
+
{name !== undefined && <span className="text-muted-foreground">{JSON.stringify(name)}: </span>}
|
|
86
|
+
<JsonPrimitive value={value} />
|
|
87
|
+
{!isLast && <span className="text-muted-foreground">,</span>}
|
|
88
|
+
</div>
|
|
89
|
+
);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
const isArray = Array.isArray(value);
|
|
93
|
+
const entries: [string, unknown][] = isArray
|
|
94
|
+
? (value as unknown[]).map((v, i) => [String(i), v])
|
|
95
|
+
: Object.entries(value as Record<string, unknown>);
|
|
96
|
+
const openBracket = isArray ? "[" : "{";
|
|
97
|
+
const closeBracket = isArray ? "]" : "}";
|
|
98
|
+
|
|
99
|
+
if (entries.length === 0) {
|
|
100
|
+
return (
|
|
101
|
+
<div role="treeitem" style={{ paddingLeft: depth * 16 + 20 }}>
|
|
102
|
+
{name !== undefined && <span className="text-muted-foreground">{JSON.stringify(name)}: </span>}
|
|
103
|
+
<span>
|
|
104
|
+
{openBracket}
|
|
105
|
+
{closeBracket}
|
|
106
|
+
</span>
|
|
107
|
+
{!isLast && <span className="text-muted-foreground">,</span>}
|
|
108
|
+
</div>
|
|
109
|
+
);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
return (
|
|
113
|
+
<div role="treeitem" aria-expanded={expanded}>
|
|
114
|
+
<div style={{ paddingLeft: depth * 16 }} className="flex items-start gap-1">
|
|
115
|
+
<button
|
|
116
|
+
type="button"
|
|
117
|
+
onClick={() => setExpanded((e) => !e)}
|
|
118
|
+
aria-label={expanded ? "Collapse" : "Expand"}
|
|
119
|
+
className="mt-0.5 inline-flex size-4 shrink-0 items-center justify-center text-muted-foreground hover:text-foreground"
|
|
120
|
+
>
|
|
121
|
+
<ChevronRight className={cn("size-3 transition-transform", expanded && "rotate-90")} />
|
|
122
|
+
</button>
|
|
123
|
+
<span>
|
|
124
|
+
{name !== undefined && <span className="text-muted-foreground">{JSON.stringify(name)}: </span>}
|
|
125
|
+
<span>{openBracket}</span>
|
|
126
|
+
{!expanded && (
|
|
127
|
+
<span className="text-muted-foreground">
|
|
128
|
+
{" "}
|
|
129
|
+
{entries.length} {isArray ? "item" : "key"}
|
|
130
|
+
{entries.length === 1 ? "" : "s"}{" "}
|
|
131
|
+
</span>
|
|
132
|
+
)}
|
|
133
|
+
{!expanded && <span>{closeBracket}</span>}
|
|
134
|
+
{!expanded && !isLast && <span className="text-muted-foreground">,</span>}
|
|
135
|
+
</span>
|
|
136
|
+
</div>
|
|
137
|
+
{expanded && (
|
|
138
|
+
<>
|
|
139
|
+
{entries.map(([key, val], i) => (
|
|
140
|
+
<JsonNode
|
|
141
|
+
key={key}
|
|
142
|
+
name={isArray ? undefined : key}
|
|
143
|
+
value={val}
|
|
144
|
+
depth={depth + 1}
|
|
145
|
+
expandDepth={expandDepth}
|
|
146
|
+
isLast={i === entries.length - 1}
|
|
147
|
+
/>
|
|
148
|
+
))}
|
|
149
|
+
<div style={{ paddingLeft: depth * 16 }}>
|
|
150
|
+
<span>{closeBracket}</span>
|
|
151
|
+
{!isLast && <span className="text-muted-foreground">,</span>}
|
|
152
|
+
</div>
|
|
153
|
+
</>
|
|
154
|
+
)}
|
|
155
|
+
</div>
|
|
156
|
+
);
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
function JsonPrimitive({ value }: { value: unknown }) {
|
|
160
|
+
if (value === null) return <span className="italic text-muted-foreground">null</span>;
|
|
161
|
+
if (value === undefined) return <span className="italic text-muted-foreground">undefined</span>;
|
|
162
|
+
if (typeof value === "string") return <span className="text-success">{JSON.stringify(value)}</span>;
|
|
163
|
+
if (typeof value === "number") return <span className="text-info">{value}</span>;
|
|
164
|
+
if (typeof value === "boolean") return <span className="text-warning">{String(value)}</span>;
|
|
165
|
+
return <span>{String(value)}</span>;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
export { JsonViewer };
|
|
@@ -0,0 +1,247 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import * as React from "react";
|
|
4
|
+
import {
|
|
5
|
+
DndContext,
|
|
6
|
+
DragOverlay,
|
|
7
|
+
KeyboardSensor,
|
|
8
|
+
PointerSensor,
|
|
9
|
+
closestCenter,
|
|
10
|
+
getFirstCollision,
|
|
11
|
+
pointerWithin,
|
|
12
|
+
rectIntersection,
|
|
13
|
+
useDroppable,
|
|
14
|
+
useSensor,
|
|
15
|
+
useSensors,
|
|
16
|
+
type CollisionDetection,
|
|
17
|
+
type DragEndEvent,
|
|
18
|
+
type DragOverEvent,
|
|
19
|
+
type DragStartEvent,
|
|
20
|
+
} from "@dnd-kit/core";
|
|
21
|
+
import {
|
|
22
|
+
SortableContext,
|
|
23
|
+
arrayMove,
|
|
24
|
+
sortableKeyboardCoordinates,
|
|
25
|
+
useSortable,
|
|
26
|
+
verticalListSortingStrategy,
|
|
27
|
+
} from "@dnd-kit/sortable";
|
|
28
|
+
import { CSS } from "@dnd-kit/utilities";
|
|
29
|
+
import { tokens } from "@kinetixui/tokens";
|
|
30
|
+
import { cn } from "../lib/utils";
|
|
31
|
+
|
|
32
|
+
export interface KanbanCard {
|
|
33
|
+
id: string;
|
|
34
|
+
content: React.ReactNode;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export interface KanbanColumn {
|
|
38
|
+
id: string;
|
|
39
|
+
title: string;
|
|
40
|
+
cards: KanbanCard[];
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export interface KanbanBoardProps extends Omit<React.HTMLAttributes<HTMLDivElement>, "children"> {
|
|
44
|
+
columns: KanbanColumn[];
|
|
45
|
+
onColumnsChange: (columns: KanbanColumn[]) => void;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* KanbanBoard — draggable cards across columns, with keyboard DnD. No DnD
|
|
50
|
+
* primitive existed in this package at all, and hand-rolling accessible
|
|
51
|
+
* drag-and-drop (pointer + touch + keyboard, collision detection, live
|
|
52
|
+
* screen-reader announcements) from scratch would both take far longer
|
|
53
|
+
* than this component's own logic and likely be worse than a battle-tested
|
|
54
|
+
* library — unlike `DiffViewer`'s hand-rolled LCS diff, there's no
|
|
55
|
+
* cross-platform-identical-algorithm argument here, since each native
|
|
56
|
+
* platform reaches for its own idiomatic drag primitive anyway (Compose's
|
|
57
|
+
* drag gestures, SwiftUI's `.draggable`/`.dropDestination`, Flutter's
|
|
58
|
+
* `Draggable`/`DragTarget`). `@dnd-kit` is the dependency: no runtime CSS,
|
|
59
|
+
* accessible by default, and this is exactly its documented "multiple
|
|
60
|
+
* containers" sortable pattern — `onDragOver` re-parents a card into the
|
|
61
|
+
* hovered column live (the standard approach; a little re-render churn
|
|
62
|
+
* mid-drag is expected/upstream-documented), `onDragEnd` commits the
|
|
63
|
+
* final within-column reorder. Column order itself isn't draggable — the
|
|
64
|
+
* `COMPONENT-ADDITIONS.md` entry only asked for card dragging.
|
|
65
|
+
*
|
|
66
|
+
* `collisionDetectionStrategy` below is dnd-kit's own documented fix for a
|
|
67
|
+
* specific multi-container gotcha: each column is both a `useDroppable`
|
|
68
|
+
* (so an empty column, or the space below its last card, is still a valid
|
|
69
|
+
* drop target) and wraps a `SortableContext` of its cards. A column's rect
|
|
70
|
+
* is a strict superset of its cards' rects, so plain `closestCorners` (or
|
|
71
|
+
* `closestCenter`/`rectIntersection` alone) almost always reports the
|
|
72
|
+
* column itself as the collision, never a specific card underneath — over
|
|
73
|
+
* gets stuck on the column id and item-level reordering never fires. The
|
|
74
|
+
* fix (lifted from dnd-kit's own multiple-containers example): try
|
|
75
|
+
* `pointerWithin` first, fall back to `rectIntersection`, and if the
|
|
76
|
+
* winning id is a column, re-run `closestCenter` scoped to just that
|
|
77
|
+
* column's cards to find the specific one under the pointer.
|
|
78
|
+
*/
|
|
79
|
+
const KanbanBoard = React.forwardRef<HTMLDivElement, KanbanBoardProps>(
|
|
80
|
+
({ columns, onColumnsChange, className, ...props }, ref) => {
|
|
81
|
+
const sensors = useSensors(
|
|
82
|
+
useSensor(PointerSensor, { activationConstraint: { distance: tokens.interaction.drag.threshold } }),
|
|
83
|
+
useSensor(KeyboardSensor, { coordinateGetter: sortableKeyboardCoordinates }),
|
|
84
|
+
);
|
|
85
|
+
const [activeCard, setActiveCard] = React.useState<KanbanCard | null>(null);
|
|
86
|
+
const lastOverId = React.useRef<string | null>(null);
|
|
87
|
+
|
|
88
|
+
const findColumn = React.useCallback(
|
|
89
|
+
(id: string) => columns.find((col) => col.id === id || col.cards.some((c) => c.id === id)),
|
|
90
|
+
[columns],
|
|
91
|
+
);
|
|
92
|
+
|
|
93
|
+
const collisionDetectionStrategy: CollisionDetection = React.useCallback(
|
|
94
|
+
(args) => {
|
|
95
|
+
const pointerIntersections = pointerWithin(args);
|
|
96
|
+
const intersections = pointerIntersections.length > 0 ? pointerIntersections : rectIntersection(args);
|
|
97
|
+
let overId = getFirstCollision(intersections, "id");
|
|
98
|
+
|
|
99
|
+
if (overId != null) {
|
|
100
|
+
const overColumn = columns.find((col) => col.id === overId);
|
|
101
|
+
if (overColumn && overColumn.cards.length > 0) {
|
|
102
|
+
const cardIds = new Set(overColumn.cards.map((c) => c.id));
|
|
103
|
+
const cardCollision = getFirstCollision(
|
|
104
|
+
closestCenter({
|
|
105
|
+
...args,
|
|
106
|
+
droppableContainers: args.droppableContainers.filter((c) => cardIds.has(String(c.id))),
|
|
107
|
+
}),
|
|
108
|
+
"id",
|
|
109
|
+
);
|
|
110
|
+
if (cardCollision != null) overId = cardCollision;
|
|
111
|
+
}
|
|
112
|
+
lastOverId.current = String(overId);
|
|
113
|
+
return [{ id: overId }];
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
return lastOverId.current ? [{ id: lastOverId.current }] : [];
|
|
117
|
+
},
|
|
118
|
+
[columns],
|
|
119
|
+
);
|
|
120
|
+
|
|
121
|
+
function handleDragStart(event: DragStartEvent) {
|
|
122
|
+
lastOverId.current = String(event.active.id);
|
|
123
|
+
const column = findColumn(String(event.active.id));
|
|
124
|
+
const card = column?.cards.find((c) => c.id === event.active.id);
|
|
125
|
+
setActiveCard(card ?? null);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
function handleDragOver(event: DragOverEvent) {
|
|
129
|
+
const { active, over } = event;
|
|
130
|
+
if (!over) return;
|
|
131
|
+
const activeId = String(active.id);
|
|
132
|
+
const overId = String(over.id);
|
|
133
|
+
if (activeId === overId) return;
|
|
134
|
+
|
|
135
|
+
const activeColumn = findColumn(activeId);
|
|
136
|
+
const overColumn = findColumn(overId);
|
|
137
|
+
if (!activeColumn || !overColumn || activeColumn.id === overColumn.id) return;
|
|
138
|
+
|
|
139
|
+
onColumnsChange(
|
|
140
|
+
columns.map((col) => {
|
|
141
|
+
if (col.id === activeColumn.id) {
|
|
142
|
+
return { ...col, cards: col.cards.filter((c) => c.id !== activeId) };
|
|
143
|
+
}
|
|
144
|
+
if (col.id === overColumn.id) {
|
|
145
|
+
const movedCard = activeColumn.cards.find((c) => c.id === activeId);
|
|
146
|
+
if (!movedCard) return col;
|
|
147
|
+
const overIndex = col.cards.findIndex((c) => c.id === overId);
|
|
148
|
+
const insertAt = overIndex >= 0 ? overIndex : col.cards.length;
|
|
149
|
+
const nextCards = [...col.cards];
|
|
150
|
+
nextCards.splice(insertAt, 0, movedCard);
|
|
151
|
+
return { ...col, cards: nextCards };
|
|
152
|
+
}
|
|
153
|
+
return col;
|
|
154
|
+
}),
|
|
155
|
+
);
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
function handleDragEnd(event: DragEndEvent) {
|
|
159
|
+
setActiveCard(null);
|
|
160
|
+
lastOverId.current = null;
|
|
161
|
+
const { active, over } = event;
|
|
162
|
+
if (!over) return;
|
|
163
|
+
const activeId = String(active.id);
|
|
164
|
+
const overId = String(over.id);
|
|
165
|
+
const activeColumn = findColumn(activeId);
|
|
166
|
+
// overId may be the column itself (dropped in empty space / an empty column)
|
|
167
|
+
const overColumn = columns.find((col) => col.id === overId) ?? findColumn(overId);
|
|
168
|
+
if (!activeColumn || !overColumn || activeColumn.id !== overColumn.id) return;
|
|
169
|
+
|
|
170
|
+
const activeIndex = activeColumn.cards.findIndex((c) => c.id === activeId);
|
|
171
|
+
const rawOverIndex = overColumn.cards.findIndex((c) => c.id === overId);
|
|
172
|
+
const overIndex = rawOverIndex >= 0 ? rawOverIndex : overColumn.cards.length - 1;
|
|
173
|
+
if (activeIndex === -1 || overIndex === -1 || activeIndex === overIndex) return;
|
|
174
|
+
|
|
175
|
+
onColumnsChange(
|
|
176
|
+
columns.map((col) =>
|
|
177
|
+
col.id === activeColumn.id ? { ...col, cards: arrayMove(col.cards, activeIndex, overIndex) } : col,
|
|
178
|
+
),
|
|
179
|
+
);
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
return (
|
|
183
|
+
<DndContext
|
|
184
|
+
sensors={sensors}
|
|
185
|
+
collisionDetection={collisionDetectionStrategy}
|
|
186
|
+
onDragStart={handleDragStart}
|
|
187
|
+
onDragOver={handleDragOver}
|
|
188
|
+
onDragEnd={handleDragEnd}
|
|
189
|
+
onDragCancel={() => {
|
|
190
|
+
setActiveCard(null);
|
|
191
|
+
lastOverId.current = null;
|
|
192
|
+
}}
|
|
193
|
+
>
|
|
194
|
+
<div ref={ref} role="group" className={cn("flex items-start gap-4 overflow-x-auto font-sans", className)} {...props}>
|
|
195
|
+
{columns.map((column) => (
|
|
196
|
+
<KanbanColumnView key={column.id} column={column} />
|
|
197
|
+
))}
|
|
198
|
+
</div>
|
|
199
|
+
<DragOverlay>{activeCard ? <KanbanCardView card={activeCard} /> : null}</DragOverlay>
|
|
200
|
+
</DndContext>
|
|
201
|
+
);
|
|
202
|
+
},
|
|
203
|
+
);
|
|
204
|
+
KanbanBoard.displayName = "KanbanBoard";
|
|
205
|
+
|
|
206
|
+
function KanbanColumnView({ column }: { column: KanbanColumn }) {
|
|
207
|
+
const { setNodeRef } = useDroppable({ id: column.id });
|
|
208
|
+
return (
|
|
209
|
+
<div className="flex w-64 shrink-0 flex-col gap-2 rounded-md border bg-muted/30 p-2">
|
|
210
|
+
<div className="flex items-center justify-between px-1 py-1">
|
|
211
|
+
<span className="text-sm font-medium text-foreground">{column.title}</span>
|
|
212
|
+
<span className="text-xs text-muted-foreground">{column.cards.length}</span>
|
|
213
|
+
</div>
|
|
214
|
+
<SortableContext items={column.cards.map((c) => c.id)} strategy={verticalListSortingStrategy}>
|
|
215
|
+
<div ref={setNodeRef} className="flex min-h-10 flex-col gap-2">
|
|
216
|
+
{column.cards.map((card) => (
|
|
217
|
+
<SortableKanbanCard key={card.id} card={card} />
|
|
218
|
+
))}
|
|
219
|
+
</div>
|
|
220
|
+
</SortableContext>
|
|
221
|
+
</div>
|
|
222
|
+
);
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
function SortableKanbanCard({ card }: { card: KanbanCard }) {
|
|
226
|
+
const { attributes, listeners, setNodeRef, transform, transition, isDragging } = useSortable({ id: card.id });
|
|
227
|
+
const style: React.CSSProperties = {
|
|
228
|
+
transform: CSS.Transform.toString(transform),
|
|
229
|
+
transition,
|
|
230
|
+
opacity: isDragging ? 0.4 : 1,
|
|
231
|
+
};
|
|
232
|
+
return (
|
|
233
|
+
<div ref={setNodeRef} style={style} {...attributes} {...listeners}>
|
|
234
|
+
<KanbanCardView card={card} />
|
|
235
|
+
</div>
|
|
236
|
+
);
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
function KanbanCardView({ card }: { card: KanbanCard }) {
|
|
240
|
+
return (
|
|
241
|
+
<div className="cursor-grab touch-none rounded-md border bg-background p-3 text-sm text-foreground shadow-sm outline-none active:cursor-grabbing focus-visible:ring-1 focus-visible:ring-ring">
|
|
242
|
+
{card.content}
|
|
243
|
+
</div>
|
|
244
|
+
);
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
export { KanbanBoard };
|