@object-ui/plugin-kanban 0.3.0 → 0.5.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.
@@ -0,0 +1,36 @@
1
+ /**
2
+ * ObjectUI
3
+ * Copyright (c) 2024-present ObjectStack Inc.
4
+ *
5
+ * This source code is licensed under the MIT license found in the
6
+ * LICENSE file in the root directory of this source tree.
7
+ */
8
+ export interface KanbanCard {
9
+ id: string;
10
+ title: string;
11
+ description?: string;
12
+ badges?: Array<{
13
+ label: string;
14
+ variant?: "default" | "secondary" | "destructive" | "outline";
15
+ }>;
16
+ [key: string]: any;
17
+ }
18
+ export interface KanbanColumn {
19
+ id: string;
20
+ title: string;
21
+ cards: KanbanCard[];
22
+ limit?: number;
23
+ className?: string;
24
+ collapsed?: boolean;
25
+ }
26
+ export interface KanbanEnhancedProps {
27
+ columns: KanbanColumn[];
28
+ onCardMove?: (cardId: string, fromColumnId: string, toColumnId: string, newIndex: number) => void;
29
+ onColumnToggle?: (columnId: string, collapsed: boolean) => void;
30
+ enableVirtualScrolling?: boolean;
31
+ virtualScrollThreshold?: number;
32
+ className?: string;
33
+ }
34
+ export declare function KanbanEnhanced({ columns, onCardMove, onColumnToggle, enableVirtualScrolling, virtualScrollThreshold, className, }: KanbanEnhancedProps): import("react/jsx-runtime").JSX.Element;
35
+ export default KanbanEnhanced;
36
+ //# sourceMappingURL=KanbanEnhanced.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"KanbanEnhanced.d.ts","sourceRoot":"","sources":["../../src/KanbanEnhanced.tsx"],"names":[],"mappings":"AAAA;;;;;;GAMG;AA0BH,MAAM,WAAW,UAAU;IACzB,EAAE,EAAE,MAAM,CAAA;IACV,KAAK,EAAE,MAAM,CAAA;IACb,WAAW,CAAC,EAAE,MAAM,CAAA;IACpB,MAAM,CAAC,EAAE,KAAK,CAAC;QAAE,KAAK,EAAE,MAAM,CAAC;QAAC,OAAO,CAAC,EAAE,SAAS,GAAG,WAAW,GAAG,aAAa,GAAG,SAAS,CAAA;KAAE,CAAC,CAAA;IAChG,CAAC,GAAG,EAAE,MAAM,GAAG,GAAG,CAAA;CACnB;AAED,MAAM,WAAW,YAAY;IAC3B,EAAE,EAAE,MAAM,CAAA;IACV,KAAK,EAAE,MAAM,CAAA;IACb,KAAK,EAAE,UAAU,EAAE,CAAA;IACnB,KAAK,CAAC,EAAE,MAAM,CAAA;IACd,SAAS,CAAC,EAAE,MAAM,CAAA;IAClB,SAAS,CAAC,EAAE,OAAO,CAAA;CACpB;AAED,MAAM,WAAW,mBAAmB;IAClC,OAAO,EAAE,YAAY,EAAE,CAAA;IACvB,UAAU,CAAC,EAAE,CAAC,MAAM,EAAE,MAAM,EAAE,YAAY,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,KAAK,IAAI,CAAA;IACjG,cAAc,CAAC,EAAE,CAAC,QAAQ,EAAE,MAAM,EAAE,SAAS,EAAE,OAAO,KAAK,IAAI,CAAA;IAC/D,sBAAsB,CAAC,EAAE,OAAO,CAAA;IAChC,sBAAsB,CAAC,EAAE,MAAM,CAAA;IAC/B,SAAS,CAAC,EAAE,MAAM,CAAA;CACnB;AAsLD,wBAAgB,cAAc,CAAC,EAC7B,OAAO,EACP,UAAU,EACV,cAAc,EACd,sBAA8B,EAC9B,sBAA2B,EAC3B,SAAS,GACV,EAAE,mBAAmB,2CAkJrB;AAED,eAAe,cAAc,CAAC"}
@@ -1,3 +1,10 @@
1
+ /**
2
+ * ObjectUI
3
+ * Copyright (c) 2024-present ObjectStack Inc.
4
+ *
5
+ * This source code is licensed under the MIT license found in the
6
+ * LICENSE file in the root directory of this source tree.
7
+ */
1
8
  export interface KanbanCard {
2
9
  id: string;
3
10
  title: string;
@@ -0,0 +1 @@
1
+ {"version":3,"file":"KanbanImpl.d.ts","sourceRoot":"","sources":["../../src/KanbanImpl.tsx"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAyBH,MAAM,WAAW,UAAU;IACzB,EAAE,EAAE,MAAM,CAAA;IACV,KAAK,EAAE,MAAM,CAAA;IACb,WAAW,CAAC,EAAE,MAAM,CAAA;IACpB,MAAM,CAAC,EAAE,KAAK,CAAC;QAAE,KAAK,EAAE,MAAM,CAAC;QAAC,OAAO,CAAC,EAAE,SAAS,GAAG,WAAW,GAAG,aAAa,GAAG,SAAS,CAAA;KAAE,CAAC,CAAA;IAChG,CAAC,GAAG,EAAE,MAAM,GAAG,GAAG,CAAA;CACnB;AAED,MAAM,WAAW,YAAY;IAC3B,EAAE,EAAE,MAAM,CAAA;IACV,KAAK,EAAE,MAAM,CAAA;IACb,KAAK,EAAE,UAAU,EAAE,CAAA;IACnB,KAAK,CAAC,EAAE,MAAM,CAAA;IACd,SAAS,CAAC,EAAE,MAAM,CAAA;CACnB;AAED,MAAM,WAAW,gBAAgB;IAC/B,OAAO,EAAE,YAAY,EAAE,CAAA;IACvB,UAAU,CAAC,EAAE,CAAC,MAAM,EAAE,MAAM,EAAE,YAAY,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,KAAK,IAAI,CAAA;IACjG,SAAS,CAAC,EAAE,MAAM,CAAA;CACnB;AAsGD,MAAM,CAAC,OAAO,UAAU,WAAW,CAAC,EAAE,OAAO,EAAE,UAAU,EAAE,SAAS,EAAE,EAAE,gBAAgB,2CA0IvF"}
@@ -0,0 +1,10 @@
1
+ import { default as React } from 'react';
2
+ import { DataSource } from '../../types/src';
3
+ import { KanbanSchema } from './types';
4
+ export interface ObjectKanbanProps {
5
+ schema: KanbanSchema;
6
+ dataSource?: DataSource;
7
+ className?: string;
8
+ }
9
+ export declare const ObjectKanban: React.FC<ObjectKanbanProps>;
10
+ //# sourceMappingURL=ObjectKanban.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ObjectKanban.d.ts","sourceRoot":"","sources":["../../src/ObjectKanban.tsx"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH,OAAO,KAAuC,MAAM,OAAO,CAAC;AAC5D,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,kBAAkB,CAAC;AAGnD,OAAO,EAAE,YAAY,EAAE,MAAM,SAAS,CAAC;AAEvC,MAAM,WAAW,iBAAiB;IAChC,MAAM,EAAE,YAAY,CAAC;IACrB,UAAU,CAAC,EAAE,UAAU,CAAC;IACxB,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB;AAED,eAAO,MAAM,YAAY,EAAE,KAAK,CAAC,EAAE,CAAC,iBAAiB,CAuKpD,CAAA"}
@@ -0,0 +1,31 @@
1
+ import { default as React } from 'react';
2
+ import { ObjectKanban } from './ObjectKanban';
3
+ export type { KanbanSchema, KanbanCard, KanbanColumn } from './types';
4
+ export { ObjectKanban };
5
+ export type { ObjectKanbanProps } from './ObjectKanban';
6
+ export interface KanbanRendererProps {
7
+ schema: {
8
+ type: string;
9
+ id?: string;
10
+ className?: string;
11
+ columns?: Array<any>;
12
+ data?: Array<any>;
13
+ groupBy?: string;
14
+ onCardMove?: (cardId: string, fromColumnId: string, toColumnId: string, newIndex: number) => void;
15
+ };
16
+ }
17
+ /**
18
+ * KanbanRenderer - The public API for the kanban board component
19
+ * This wrapper handles lazy loading internally using React.Suspense
20
+ */
21
+ export declare const KanbanRenderer: React.FC<KanbanRendererProps>;
22
+ export declare const kanbanComponents: {
23
+ kanban: React.FC<KanbanRendererProps>;
24
+ 'kanban-enhanced': React.LazyExoticComponent<typeof import('./KanbanEnhanced').KanbanEnhanced>;
25
+ 'object-kanban': React.FC<import('./ObjectKanban').ObjectKanbanProps>;
26
+ };
27
+ export declare const ObjectKanbanRenderer: React.FC<{
28
+ schema: any;
29
+ [key: string]: any;
30
+ }>;
31
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/index.tsx"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH,OAAO,KAAmB,MAAM,OAAO,CAAC;AAIxC,OAAO,EAAE,YAAY,EAAE,MAAM,gBAAgB,CAAC;AAG9C,YAAY,EAAE,YAAY,EAAE,UAAU,EAAE,YAAY,EAAE,MAAM,SAAS,CAAC;AACtE,OAAO,EAAE,YAAY,EAAE,CAAC;AACxB,YAAY,EAAE,iBAAiB,EAAE,MAAM,gBAAgB,CAAC;AAMxD,MAAM,WAAW,mBAAmB;IAClC,MAAM,EAAE;QACN,IAAI,EAAE,MAAM,CAAC;QACb,EAAE,CAAC,EAAE,MAAM,CAAC;QACZ,SAAS,CAAC,EAAE,MAAM,CAAC;QACnB,OAAO,CAAC,EAAE,KAAK,CAAC,GAAG,CAAC,CAAC;QACrB,IAAI,CAAC,EAAE,KAAK,CAAC,GAAG,CAAC,CAAC;QAClB,OAAO,CAAC,EAAE,MAAM,CAAC;QACjB,UAAU,CAAC,EAAE,CAAC,MAAM,EAAE,MAAM,EAAE,YAAY,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,KAAK,IAAI,CAAC;KACnG,CAAC;CACH;AAED;;;GAGG;AACH,eAAO,MAAM,cAAc,EAAE,KAAK,CAAC,EAAE,CAAC,mBAAmB,CAsCxD,CAAC;AAqGF,eAAO,MAAM,gBAAgB;;;;CAI5B,CAAC;AA2DF,eAAO,MAAM,oBAAoB,EAAE,KAAK,CAAC,EAAE,CAAC;IAAE,MAAM,EAAE,GAAG,CAAC;IAAC,CAAC,GAAG,EAAE,MAAM,GAAG,GAAG,CAAA;CAAE,CAG9E,CAAC"}
@@ -28,6 +28,26 @@ export interface KanbanColumn {
28
28
  */
29
29
  export interface KanbanSchema extends BaseSchema {
30
30
  type: 'kanban';
31
+ /**
32
+ * Object name to fetch data from.
33
+ */
34
+ objectName?: string;
35
+ /**
36
+ * Field to group records by (maps to column IDs).
37
+ */
38
+ groupBy?: string;
39
+ /**
40
+ * Field to use as the card title.
41
+ */
42
+ cardTitle?: string;
43
+ /**
44
+ * Fields to display on the card.
45
+ */
46
+ cardFields?: string[];
47
+ /**
48
+ * Static data or bound data.
49
+ */
50
+ data?: any[];
31
51
  /**
32
52
  * Array of columns to display in the kanban board.
33
53
  * Each column contains an array of cards.
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../src/types.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,kBAAkB,CAAC;AAEnD;;GAEG;AACH,MAAM,WAAW,UAAU;IACzB,EAAE,EAAE,MAAM,CAAC;IACX,KAAK,EAAE,MAAM,CAAC;IACd,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,MAAM,CAAC,EAAE,KAAK,CAAC;QAAE,KAAK,EAAE,MAAM,CAAC;QAAC,OAAO,CAAC,EAAE,SAAS,GAAG,WAAW,GAAG,aAAa,GAAG,SAAS,CAAA;KAAE,CAAC,CAAC;IACjG,CAAC,GAAG,EAAE,MAAM,GAAG,GAAG,CAAC;CACpB;AAED;;GAEG;AACH,MAAM,WAAW,YAAY;IAC3B,EAAE,EAAE,MAAM,CAAC;IACX,KAAK,EAAE,MAAM,CAAC;IACd,KAAK,EAAE,UAAU,EAAE,CAAC;IACpB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB;AAED;;;GAGG;AACH,MAAM,WAAW,YAAa,SAAQ,UAAU;IAC9C,IAAI,EAAE,QAAQ,CAAC;IAEf;;OAEG;IACH,UAAU,CAAC,EAAE,MAAM,CAAC;IAEpB;;OAEG;IACH,OAAO,CAAC,EAAE,MAAM,CAAC;IAEjB;;OAEG;IACH,SAAS,CAAC,EAAE,MAAM,CAAC;IAEnB;;OAEG;IACH,UAAU,CAAC,EAAE,MAAM,EAAE,CAAC;IAEtB;;OAEG;IACH,IAAI,CAAC,EAAE,GAAG,EAAE,CAAC;IAEb;;;OAGG;IACH,OAAO,CAAC,EAAE,YAAY,EAAE,CAAC;IAEzB;;OAEG;IACH,UAAU,CAAC,EAAE,CAAC,MAAM,EAAE,MAAM,EAAE,YAAY,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,KAAK,IAAI,CAAC;IAElG;;OAEG;IACH,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB"}
package/package.json CHANGED
@@ -1,8 +1,18 @@
1
1
  {
2
2
  "name": "@object-ui/plugin-kanban",
3
- "version": "0.3.0",
3
+ "version": "0.5.0",
4
4
  "type": "module",
5
5
  "license": "MIT",
6
+ "description": "Kanban board plugin for Object UI, powered by dnd-kit",
7
+ "homepage": "https://www.objectui.org",
8
+ "repository": {
9
+ "type": "git",
10
+ "url": "https://github.com/objectstack-ai/objectui.git",
11
+ "directory": "packages/plugin-kanban"
12
+ },
13
+ "bugs": {
14
+ "url": "https://github.com/objectstack-ai/objectui/issues"
15
+ },
6
16
  "main": "dist/index.umd.cjs",
7
17
  "module": "dist/index.js",
8
18
  "types": "dist/index.d.ts",
@@ -17,22 +27,25 @@
17
27
  "@dnd-kit/core": "^6.3.1",
18
28
  "@dnd-kit/sortable": "^10.0.0",
19
29
  "@dnd-kit/utilities": "^3.2.2",
20
- "@object-ui/components": "0.3.0",
21
- "@object-ui/core": "0.3.0",
22
- "@object-ui/react": "0.3.0",
23
- "@object-ui/types": "0.3.0"
30
+ "@tanstack/react-virtual": "^3.10.8",
31
+ "lucide-react": "^0.563.0",
32
+ "@object-ui/components": "0.5.0",
33
+ "@object-ui/core": "0.5.0",
34
+ "@object-ui/react": "0.5.0",
35
+ "@object-ui/types": "0.5.0"
24
36
  },
25
37
  "peerDependencies": {
26
38
  "react": "^18.0.0 || ^19.0.0",
27
39
  "react-dom": "^18.0.0 || ^19.0.0"
28
40
  },
29
41
  "devDependencies": {
30
- "@types/react": "^18.3.12",
31
- "@types/react-dom": "^18.3.1",
32
- "@vitejs/plugin-react": "^4.2.1",
42
+ "@types/react": "^19.2.10",
43
+ "@types/react-dom": "^19.2.3",
44
+ "@vitejs/plugin-react": "^5.1.3",
33
45
  "typescript": "^5.9.3",
34
46
  "vite": "^7.3.1",
35
- "vite-plugin-dts": "^4.5.4"
47
+ "vite-plugin-dts": "^4.5.4",
48
+ "@object-ui/data-objectstack": "0.5.0"
36
49
  },
37
50
  "scripts": {
38
51
  "build": "vite build",
@@ -0,0 +1,394 @@
1
+ /**
2
+ * ObjectUI
3
+ * Copyright (c) 2024-present ObjectStack Inc.
4
+ *
5
+ * This source code is licensed under the MIT license found in the
6
+ * LICENSE file in the root directory of this source tree.
7
+ */
8
+
9
+ import * as React from "react"
10
+ import { useVirtualizer } from "@tanstack/react-virtual"
11
+ import {
12
+ DndContext,
13
+ DragEndEvent,
14
+ DragOverlay,
15
+ DragStartEvent,
16
+ PointerSensor,
17
+ useSensor,
18
+ useSensors,
19
+ closestCorners,
20
+ } from "@dnd-kit/core"
21
+ import {
22
+ SortableContext,
23
+ arrayMove,
24
+ useSortable,
25
+ verticalListSortingStrategy,
26
+ } from "@dnd-kit/sortable"
27
+ import { CSS } from "@dnd-kit/utilities"
28
+ import { Badge, Card, CardHeader, CardTitle, CardDescription, CardContent, Button } from "@object-ui/components"
29
+ import { ChevronDown, ChevronRight, AlertTriangle } from "lucide-react"
30
+
31
+ const cn = (...classes: (string | undefined)[]) => classes.filter(Boolean).join(' ')
32
+
33
+ export interface KanbanCard {
34
+ id: string
35
+ title: string
36
+ description?: string
37
+ badges?: Array<{ label: string; variant?: "default" | "secondary" | "destructive" | "outline" }>
38
+ [key: string]: any
39
+ }
40
+
41
+ export interface KanbanColumn {
42
+ id: string
43
+ title: string
44
+ cards: KanbanCard[]
45
+ limit?: number
46
+ className?: string
47
+ collapsed?: boolean
48
+ }
49
+
50
+ export interface KanbanEnhancedProps {
51
+ columns: KanbanColumn[]
52
+ onCardMove?: (cardId: string, fromColumnId: string, toColumnId: string, newIndex: number) => void
53
+ onColumnToggle?: (columnId: string, collapsed: boolean) => void
54
+ enableVirtualScrolling?: boolean
55
+ virtualScrollThreshold?: number
56
+ className?: string
57
+ }
58
+
59
+ function SortableCard({ card }: { card: KanbanCard }) {
60
+ const {
61
+ attributes,
62
+ listeners,
63
+ setNodeRef,
64
+ transform,
65
+ transition,
66
+ isDragging,
67
+ } = useSortable({ id: card.id })
68
+
69
+ const style = {
70
+ transform: CSS.Transform.toString(transform),
71
+ transition,
72
+ opacity: isDragging ? 0.5 : undefined,
73
+ }
74
+
75
+ return (
76
+ <div ref={setNodeRef} style={style} {...attributes} {...listeners}>
77
+ <Card className="mb-2 cursor-grab active:cursor-grabbing border-border bg-card/60 hover:border-primary/40 hover:shadow-lg hover:shadow-primary/10 transition-all duration-300 group">
78
+ <CardHeader className="p-4">
79
+ <CardTitle className="text-sm font-medium font-mono tracking-tight text-foreground group-hover:text-primary transition-colors">{card.title}</CardTitle>
80
+ {card.description && (
81
+ <CardDescription className="text-xs text-muted-foreground font-mono">
82
+ {card.description}
83
+ </CardDescription>
84
+ )}
85
+ </CardHeader>
86
+ {card.badges && card.badges.length > 0 && (
87
+ <CardContent className="p-4 pt-0">
88
+ <div className="flex flex-wrap gap-1">
89
+ {card.badges.map((badge, index) => (
90
+ <Badge key={index} variant={badge.variant || "default"} className="text-xs">
91
+ {badge.label}
92
+ </Badge>
93
+ ))}
94
+ </div>
95
+ </CardContent>
96
+ )}
97
+ </Card>
98
+ </div>
99
+ )
100
+ }
101
+
102
+ function VirtualizedCardList({ cards, parentRef }: { cards: KanbanCard[]; parentRef: React.RefObject<HTMLDivElement | null> }) {
103
+ const rowVirtualizer = useVirtualizer({
104
+ count: cards.length,
105
+ getScrollElement: () => parentRef.current,
106
+ estimateSize: () => 120,
107
+ overscan: 5,
108
+ })
109
+
110
+ return (
111
+ <div
112
+ style={{
113
+ height: `${rowVirtualizer.getTotalSize()}px`,
114
+ width: '100%',
115
+ position: 'relative',
116
+ }}
117
+ >
118
+ {rowVirtualizer.getVirtualItems().map((virtualItem) => {
119
+ const card = cards[virtualItem.index]
120
+ return (
121
+ <div
122
+ key={card.id}
123
+ style={{
124
+ position: 'absolute',
125
+ top: 0,
126
+ left: 0,
127
+ width: '100%',
128
+ transform: `translateY(${virtualItem.start}px)`,
129
+ }}
130
+ >
131
+ <SortableCard card={card} />
132
+ </div>
133
+ )
134
+ })}
135
+ </div>
136
+ )
137
+ }
138
+
139
+ function KanbanColumnEnhanced({
140
+ column,
141
+ cards,
142
+ onToggle,
143
+ enableVirtual,
144
+ }: {
145
+ column: KanbanColumn
146
+ cards: KanbanCard[]
147
+ onToggle: (collapsed: boolean) => void
148
+ enableVirtual: boolean
149
+ }) {
150
+ const safeCards = cards || []
151
+ const scrollRef = React.useRef<HTMLDivElement>(null)
152
+ const { setNodeRef } = useSortable({
153
+ id: column.id,
154
+ data: {
155
+ type: "column",
156
+ },
157
+ })
158
+
159
+ const isLimitExceeded = column.limit && safeCards.length >= column.limit
160
+ const isNearLimit = column.limit && safeCards.length >= column.limit * 0.8
161
+
162
+ return (
163
+ <div
164
+ ref={setNodeRef}
165
+ className={cn(
166
+ "flex flex-col flex-shrink-0 rounded-lg border border-border bg-card/20 backdrop-blur-sm shadow-xl transition-all",
167
+ column.collapsed ? "w-16" : "w-80",
168
+ column.className
169
+ )}
170
+ >
171
+ <div className="p-4 border-b border-border/50 bg-muted/20 flex items-center justify-between">
172
+ <div className="flex items-center gap-2 flex-1 min-w-0">
173
+ <Button
174
+ variant="ghost"
175
+ size="sm"
176
+ className="h-6 w-6 p-0"
177
+ onClick={() => onToggle(!column.collapsed)}
178
+ >
179
+ {column.collapsed ? <ChevronRight className="h-4 w-4" /> : <ChevronDown className="h-4 w-4" />}
180
+ </Button>
181
+ {!column.collapsed && (
182
+ <>
183
+ <h3 className="font-mono text-sm font-semibold tracking-wider text-primary/90 uppercase truncate">
184
+ {column.title}
185
+ </h3>
186
+ <div className="flex items-center gap-2">
187
+ <span className={cn(
188
+ "font-mono text-xs",
189
+ isLimitExceeded ? "text-destructive" : isNearLimit ? "text-yellow-500" : "text-muted-foreground"
190
+ )}>
191
+ {safeCards.length}
192
+ {column.limit && ` / ${column.limit}`}
193
+ </span>
194
+ {isLimitExceeded && (
195
+ <Badge variant="destructive" className="text-xs">
196
+ Full
197
+ </Badge>
198
+ )}
199
+ {isNearLimit && !isLimitExceeded && (
200
+ <AlertTriangle className="h-3 w-3 text-yellow-500" />
201
+ )}
202
+ </div>
203
+ </>
204
+ )}
205
+ </div>
206
+ {column.collapsed && (
207
+ <div className="flex flex-col items-center gap-1">
208
+ <span className="font-mono text-xs font-bold text-primary/90 [writing-mode:vertical-rl] rotate-180">
209
+ {column.title}
210
+ </span>
211
+ <Badge variant="secondary" className="text-xs">
212
+ {safeCards.length}
213
+ </Badge>
214
+ </div>
215
+ )}
216
+ </div>
217
+ {!column.collapsed && (
218
+ <div ref={scrollRef} className="flex-1 p-4 overflow-y-auto" style={{ maxHeight: '600px' }}>
219
+ <SortableContext
220
+ items={safeCards.map((c) => c.id)}
221
+ strategy={verticalListSortingStrategy}
222
+ >
223
+ {enableVirtual ? (
224
+ <VirtualizedCardList cards={safeCards} parentRef={scrollRef} />
225
+ ) : (
226
+ <div className="space-y-2">
227
+ {safeCards.map((card) => (
228
+ <SortableCard key={card.id} card={card} />
229
+ ))}
230
+ </div>
231
+ )}
232
+ </SortableContext>
233
+ </div>
234
+ )}
235
+ </div>
236
+ )
237
+ }
238
+
239
+ export function KanbanEnhanced({
240
+ columns,
241
+ onCardMove,
242
+ onColumnToggle,
243
+ enableVirtualScrolling = false,
244
+ virtualScrollThreshold = 50,
245
+ className,
246
+ }: KanbanEnhancedProps) {
247
+ const [activeCard, setActiveCard] = React.useState<KanbanCard | null>(null)
248
+
249
+ const safeColumns = React.useMemo(() => {
250
+ return (columns || []).map(col => ({
251
+ ...col,
252
+ cards: col.cards || []
253
+ }));
254
+ }, [columns]);
255
+
256
+ const [boardColumns, setBoardColumns] = React.useState<KanbanColumn[]>(safeColumns)
257
+
258
+ React.useEffect(() => {
259
+ setBoardColumns(safeColumns)
260
+ }, [safeColumns])
261
+
262
+ const sensors = useSensors(
263
+ useSensor(PointerSensor, {
264
+ activationConstraint: {
265
+ distance: 8,
266
+ },
267
+ })
268
+ )
269
+
270
+ const handleDragStart = (event: DragStartEvent) => {
271
+ const { active } = event
272
+ const card = findCard(active.id as string)
273
+ setActiveCard(card)
274
+ }
275
+
276
+ const handleDragEnd = (event: DragEndEvent) => {
277
+ const { active, over } = event
278
+ setActiveCard(null)
279
+
280
+ if (!over) return
281
+
282
+ const activeId = active.id as string
283
+ const overId = over.id as string
284
+
285
+ if (activeId === overId) return
286
+
287
+ const activeColumn = findColumnByCardId(activeId)
288
+ const overColumn = findColumnByCardId(overId) || findColumnById(overId)
289
+
290
+ if (!activeColumn || !overColumn) return
291
+
292
+ if (activeColumn.id === overColumn.id) {
293
+ const cards = [...activeColumn.cards]
294
+ const oldIndex = cards.findIndex((c) => c.id === activeId)
295
+ const newIndex = cards.findIndex((c) => c.id === overId)
296
+
297
+ const newCards = arrayMove(cards, oldIndex, newIndex)
298
+ setBoardColumns((prev) =>
299
+ prev.map((col) =>
300
+ col.id === activeColumn.id ? { ...col, cards: newCards } : col
301
+ )
302
+ )
303
+ } else {
304
+ const activeCards = [...activeColumn.cards]
305
+ const overCards = [...overColumn.cards]
306
+ const activeIndex = activeCards.findIndex((c) => c.id === activeId)
307
+
308
+ const isDroppingOnColumn = overId === overColumn.id
309
+ const overIndex = isDroppingOnColumn
310
+ ? overCards.length
311
+ : overCards.findIndex((c) => c.id === overId)
312
+
313
+ const [movedCard] = activeCards.splice(activeIndex, 1)
314
+ overCards.splice(overIndex, 0, movedCard)
315
+
316
+ setBoardColumns((prev) =>
317
+ prev.map((col) => {
318
+ if (col.id === activeColumn.id) {
319
+ return { ...col, cards: activeCards }
320
+ }
321
+ if (col.id === overColumn.id) {
322
+ return { ...col, cards: overCards }
323
+ }
324
+ return col
325
+ })
326
+ )
327
+
328
+ if (onCardMove) {
329
+ onCardMove(activeId, activeColumn.id, overColumn.id, overIndex)
330
+ }
331
+ }
332
+ }
333
+
334
+ const findCard = React.useCallback(
335
+ (cardId: string): KanbanCard | null => {
336
+ for (const column of boardColumns) {
337
+ const card = column.cards.find((c) => c.id === cardId)
338
+ if (card) return card
339
+ }
340
+ return null
341
+ },
342
+ [boardColumns]
343
+ )
344
+
345
+ const findColumnByCardId = React.useCallback(
346
+ (cardId: string): KanbanColumn | null => {
347
+ return boardColumns.find((col) => col.cards.some((c) => c.id === cardId)) || null
348
+ },
349
+ [boardColumns]
350
+ )
351
+
352
+ const findColumnById = React.useCallback(
353
+ (columnId: string): KanbanColumn | null => {
354
+ return boardColumns.find((col) => col.id === columnId) || null
355
+ },
356
+ [boardColumns]
357
+ )
358
+
359
+ const handleColumnToggle = React.useCallback((columnId: string, collapsed: boolean) => {
360
+ setBoardColumns(prev =>
361
+ prev.map(col => col.id === columnId ? { ...col, collapsed } : col)
362
+ )
363
+ onColumnToggle?.(columnId, collapsed)
364
+ }, [onColumnToggle])
365
+
366
+ return (
367
+ <DndContext
368
+ sensors={sensors}
369
+ collisionDetection={closestCorners}
370
+ onDragStart={handleDragStart}
371
+ onDragEnd={handleDragEnd}
372
+ >
373
+ <div className={cn("flex gap-4 overflow-x-auto p-4", className)}>
374
+ {boardColumns.map((column) => {
375
+ const shouldUseVirtual = enableVirtualScrolling && column.cards.length > virtualScrollThreshold
376
+ return (
377
+ <KanbanColumnEnhanced
378
+ key={column.id}
379
+ column={column}
380
+ cards={column.cards}
381
+ onToggle={(collapsed) => handleColumnToggle(column.id, collapsed)}
382
+ enableVirtual={shouldUseVirtual}
383
+ />
384
+ )
385
+ })}
386
+ </div>
387
+ <DragOverlay>
388
+ {activeCard ? <SortableCard card={activeCard} /> : null}
389
+ </DragOverlay>
390
+ </DndContext>
391
+ )
392
+ }
393
+
394
+ export default KanbanEnhanced;
@@ -1,3 +1,11 @@
1
+ /**
2
+ * ObjectUI
3
+ * Copyright (c) 2024-present ObjectStack Inc.
4
+ *
5
+ * This source code is licensed under the MIT license found in the
6
+ * LICENSE file in the root directory of this source tree.
7
+ */
8
+
1
9
  import * as React from "react"
2
10
  import {
3
11
  DndContext,