@object-ui/plugin-kanban 3.3.0 → 3.3.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.
Files changed (39) hide show
  1. package/CHANGELOG.md +19 -0
  2. package/README.md +24 -0
  3. package/dist/{KanbanEnhanced-TdUe0kQH.js → KanbanEnhanced-Do9ZB1Mh.js} +35 -32
  4. package/dist/{KanbanImpl-BtlPa7GE.js → KanbanImpl-BdocXM5T.js} +1 -1
  5. package/dist/{chevron-down-B6UH8BbF.js → chevron-down-C0JUlGjk.js} +1 -1
  6. package/dist/index.js +3 -3
  7. package/dist/index.umd.cjs +2 -2
  8. package/dist/{plus-BTqoaaEC.js → plus-CHsXVJSY.js} +1 -1
  9. package/package.json +34 -11
  10. package/.turbo/turbo-build.log +0 -32
  11. package/src/CardTemplates.tsx +0 -123
  12. package/src/InlineQuickAdd.tsx +0 -189
  13. package/src/KanbanEnhanced.tsx +0 -525
  14. package/src/KanbanImpl.tsx +0 -597
  15. package/src/ObjectKanban.EdgeCases.stories.tsx +0 -168
  16. package/src/ObjectKanban.msw.test.tsx +0 -95
  17. package/src/ObjectKanban.stories.tsx +0 -152
  18. package/src/ObjectKanban.tsx +0 -276
  19. package/src/__tests__/KanbanEnhanced.test.tsx +0 -260
  20. package/src/__tests__/KanbanGrouping.test.tsx +0 -164
  21. package/src/__tests__/KanbanSwimlanes.test.tsx +0 -194
  22. package/src/__tests__/ObjectKanbanTitle.test.tsx +0 -93
  23. package/src/__tests__/SwimlanePersistence.test.tsx +0 -159
  24. package/src/__tests__/accessibility.test.tsx +0 -296
  25. package/src/__tests__/dnd-undo-integration.test.tsx +0 -525
  26. package/src/__tests__/performance-benchmark.test.tsx +0 -306
  27. package/src/__tests__/phase13-features.test.tsx +0 -387
  28. package/src/__tests__/view-states.test.tsx +0 -403
  29. package/src/index.test.ts +0 -112
  30. package/src/index.tsx +0 -327
  31. package/src/registration.test.tsx +0 -26
  32. package/src/types.ts +0 -185
  33. package/src/useColumnWidths.ts +0 -125
  34. package/src/useCrossSwimlaneMove.ts +0 -116
  35. package/src/useQuickAddReorder.ts +0 -107
  36. package/tsconfig.json +0 -19
  37. package/vite.config.ts +0 -62
  38. package/vitest.config.ts +0 -12
  39. package/vitest.setup.ts +0 -1
@@ -1,525 +0,0 @@
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, Input } from "@object-ui/components"
29
- import { ChevronDown, ChevronRight, AlertTriangle, Plus } 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
- coverImage?: string
39
- [key: string]: any
40
- }
41
-
42
- export interface KanbanColumn {
43
- id: string
44
- title: string
45
- cards: KanbanCard[]
46
- limit?: number
47
- className?: string
48
- collapsed?: boolean
49
- }
50
-
51
- export interface ConditionalFormattingRule {
52
- field: string
53
- operator: 'equals' | 'not_equals' | 'contains' | 'in'
54
- value: string | string[]
55
- backgroundColor?: string
56
- borderColor?: string
57
- }
58
-
59
- export interface KanbanEnhancedProps {
60
- columns: KanbanColumn[]
61
- onCardMove?: (cardId: string, fromColumnId: string, toColumnId: string, newIndex: number) => void
62
- onColumnToggle?: (columnId: string, collapsed: boolean) => void
63
- enableVirtualScrolling?: boolean
64
- virtualScrollThreshold?: number
65
- className?: string
66
- quickAdd?: boolean
67
- onQuickAdd?: (columnId: string, title: string) => void
68
- conditionalFormatting?: ConditionalFormattingRule[]
69
- }
70
-
71
- function getCardStyles(card: KanbanCard, rules?: ConditionalFormattingRule[]): React.CSSProperties {
72
- if (!rules || rules.length === 0) return {}
73
-
74
- for (const rule of rules) {
75
- const fieldValue = card[rule.field]
76
- if (fieldValue === undefined || fieldValue === null) continue
77
-
78
- let matches = false
79
- const strValue = String(fieldValue)
80
-
81
- switch (rule.operator) {
82
- case 'equals':
83
- matches = strValue === String(rule.value)
84
- break
85
- case 'not_equals':
86
- matches = strValue !== String(rule.value)
87
- break
88
- case 'contains':
89
- matches = strValue.toLowerCase().includes(String(rule.value).toLowerCase())
90
- break
91
- case 'in':
92
- matches = Array.isArray(rule.value) && rule.value.includes(strValue)
93
- break
94
- }
95
-
96
- if (matches) {
97
- return {
98
- ...(rule.backgroundColor ? { backgroundColor: rule.backgroundColor } : {}),
99
- ...(rule.borderColor ? { borderColor: rule.borderColor } : {}),
100
- }
101
- }
102
- }
103
- return {}
104
- }
105
-
106
- function SortableCard({ card, conditionalFormatting }: { card: KanbanCard; conditionalFormatting?: ConditionalFormattingRule[] }) {
107
- const {
108
- attributes,
109
- listeners,
110
- setNodeRef,
111
- transform,
112
- transition,
113
- isDragging,
114
- } = useSortable({ id: card.id })
115
-
116
- const style = {
117
- transform: CSS.Transform.toString(transform),
118
- transition,
119
- opacity: isDragging ? 0.5 : undefined,
120
- }
121
-
122
- const cardStyles = getCardStyles(card, conditionalFormatting)
123
-
124
- return (
125
- <div ref={setNodeRef} style={style} {...attributes} {...listeners}>
126
- <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" style={cardStyles}>
127
- {card.coverImage && (
128
- <div className="w-full h-32 overflow-hidden rounded-t-lg">
129
- <img
130
- src={card.coverImage}
131
- alt=""
132
- className="w-full h-full object-cover"
133
- loading="lazy"
134
- />
135
- </div>
136
- )}
137
- <CardHeader className="p-4">
138
- <CardTitle className="text-sm font-medium font-mono tracking-tight text-foreground group-hover:text-primary transition-colors">{card.title}</CardTitle>
139
- {card.description && (
140
- <CardDescription className="text-xs text-muted-foreground font-mono">
141
- {card.description}
142
- </CardDescription>
143
- )}
144
- </CardHeader>
145
- {card.badges && card.badges.length > 0 && (
146
- <CardContent className="p-4 pt-0">
147
- <div className="flex flex-wrap gap-1">
148
- {card.badges.map((badge, index) => (
149
- <Badge key={index} variant={badge.variant || "default"} className="text-xs">
150
- {badge.label}
151
- </Badge>
152
- ))}
153
- </div>
154
- </CardContent>
155
- )}
156
- </Card>
157
- </div>
158
- )
159
- }
160
-
161
- function VirtualizedCardList({ cards, parentRef, conditionalFormatting }: { cards: KanbanCard[]; parentRef: React.RefObject<HTMLDivElement | null>; conditionalFormatting?: ConditionalFormattingRule[] }) {
162
- const rowVirtualizer = useVirtualizer({
163
- count: cards.length,
164
- getScrollElement: () => parentRef.current,
165
- estimateSize: () => 120,
166
- overscan: 5,
167
- })
168
-
169
- return (
170
- <div
171
- style={{
172
- height: `${rowVirtualizer.getTotalSize()}px`,
173
- width: '100%',
174
- position: 'relative',
175
- }}
176
- >
177
- {rowVirtualizer.getVirtualItems().map((virtualItem) => {
178
- const card = cards[virtualItem.index]
179
- return (
180
- <div
181
- key={card.id}
182
- style={{
183
- position: 'absolute',
184
- top: 0,
185
- left: 0,
186
- width: '100%',
187
- transform: `translateY(${virtualItem.start}px)`,
188
- }}
189
- >
190
- <SortableCard card={card} conditionalFormatting={conditionalFormatting} />
191
- </div>
192
- )
193
- })}
194
- </div>
195
- )
196
- }
197
-
198
- function QuickAddForm({ columnId, onAdd }: { columnId: string; onAdd: (columnId: string, title: string) => void }) {
199
- const [isAdding, setIsAdding] = React.useState(false)
200
- const [title, setTitle] = React.useState('')
201
- const inputRef = React.useRef<HTMLInputElement>(null)
202
-
203
- const handleSubmit = () => {
204
- const trimmed = title.trim()
205
- if (trimmed) {
206
- onAdd(columnId, trimmed)
207
- setTitle('')
208
- }
209
- setIsAdding(false)
210
- }
211
-
212
- const handleKeyDown = (e: React.KeyboardEvent) => {
213
- if (e.key === 'Enter') {
214
- e.preventDefault()
215
- handleSubmit()
216
- } else if (e.key === 'Escape') {
217
- setTitle('')
218
- setIsAdding(false)
219
- }
220
- }
221
-
222
- if (!isAdding) {
223
- return (
224
- <Button
225
- variant="ghost"
226
- size="sm"
227
- className="w-full mt-2 text-muted-foreground hover:text-foreground"
228
- onClick={() => {
229
- setIsAdding(true)
230
- setTimeout(() => inputRef.current?.focus(), 0)
231
- }}
232
- >
233
- <Plus className="h-4 w-4 mr-1" />
234
- Add Card
235
- </Button>
236
- )
237
- }
238
-
239
- return (
240
- <div className="mt-2 space-y-2">
241
- <Input
242
- ref={inputRef}
243
- value={title}
244
- onChange={(e) => setTitle(e.target.value)}
245
- onKeyDown={handleKeyDown}
246
- onBlur={handleSubmit}
247
- placeholder="Enter card title..."
248
- className="text-sm"
249
- autoFocus
250
- />
251
- </div>
252
- )
253
- }
254
-
255
- function KanbanColumnEnhanced({
256
- column,
257
- cards,
258
- onToggle,
259
- enableVirtual,
260
- quickAdd,
261
- onQuickAdd,
262
- conditionalFormatting,
263
- }: {
264
- column: KanbanColumn
265
- cards: KanbanCard[]
266
- onToggle: (collapsed: boolean) => void
267
- enableVirtual: boolean
268
- quickAdd?: boolean
269
- onQuickAdd?: (columnId: string, title: string) => void
270
- conditionalFormatting?: ConditionalFormattingRule[]
271
- }) {
272
- const safeCards = cards || []
273
- const scrollRef = React.useRef<HTMLDivElement>(null)
274
- const { setNodeRef } = useSortable({
275
- id: column.id,
276
- data: {
277
- type: "column",
278
- },
279
- })
280
-
281
- const isLimitExceeded = column.limit && safeCards.length >= column.limit
282
- const isNearLimit = column.limit && safeCards.length >= column.limit * 0.8
283
-
284
- return (
285
- <div
286
- ref={setNodeRef}
287
- className={cn(
288
- "flex flex-col flex-shrink-0 rounded-lg border border-border bg-card/20 backdrop-blur-sm shadow-xl transition-all",
289
- column.collapsed ? "w-16" : "w-80",
290
- column.className
291
- )}
292
- >
293
- <div className="p-4 border-b border-border/50 bg-muted/20 flex items-center justify-between">
294
- <div className="flex items-center gap-2 flex-1 min-w-0">
295
- <Button
296
- variant="ghost"
297
- size="sm"
298
- className="h-6 w-6 p-0"
299
- onClick={() => onToggle(!column.collapsed)}
300
- >
301
- {column.collapsed ? <ChevronRight className="h-4 w-4" /> : <ChevronDown className="h-4 w-4" />}
302
- </Button>
303
- {!column.collapsed && (
304
- <>
305
- <h3 className="font-mono text-sm font-semibold tracking-wider text-primary/90 uppercase truncate">
306
- {column.title}
307
- </h3>
308
- <div className="flex items-center gap-2">
309
- <span className={cn(
310
- "font-mono text-xs",
311
- isLimitExceeded ? "text-destructive" : isNearLimit ? "text-yellow-500" : "text-muted-foreground"
312
- )}>
313
- {safeCards.length}
314
- {column.limit && ` / ${column.limit}`}
315
- </span>
316
- {isLimitExceeded && (
317
- <Badge variant="destructive" className="text-xs">
318
- Full
319
- </Badge>
320
- )}
321
- {isNearLimit && !isLimitExceeded && (
322
- <AlertTriangle className="h-3 w-3 text-yellow-500" />
323
- )}
324
- </div>
325
- </>
326
- )}
327
- </div>
328
- {column.collapsed && (
329
- <div className="flex flex-col items-center gap-1">
330
- <span className="font-mono text-xs font-bold text-primary/90 [writing-mode:vertical-rl] rotate-180">
331
- {column.title}
332
- </span>
333
- <Badge variant="secondary" className="text-xs">
334
- {safeCards.length}
335
- </Badge>
336
- </div>
337
- )}
338
- </div>
339
- {!column.collapsed && (
340
- <div ref={scrollRef} className="flex-1 p-4 overflow-y-auto" style={{ maxHeight: '600px' }}>
341
- <SortableContext
342
- items={safeCards.map((c) => c.id)}
343
- strategy={verticalListSortingStrategy}
344
- >
345
- {enableVirtual ? (
346
- <VirtualizedCardList cards={safeCards} parentRef={scrollRef} conditionalFormatting={conditionalFormatting} />
347
- ) : (
348
- <div className="space-y-2">
349
- {safeCards.map((card) => (
350
- <SortableCard key={card.id} card={card} conditionalFormatting={conditionalFormatting} />
351
- ))}
352
- </div>
353
- )}
354
- </SortableContext>
355
- {quickAdd && onQuickAdd && (
356
- <QuickAddForm columnId={column.id} onAdd={onQuickAdd} />
357
- )}
358
- </div>
359
- )}
360
- </div>
361
- )
362
- }
363
-
364
- export function KanbanEnhanced({
365
- columns,
366
- onCardMove,
367
- onColumnToggle,
368
- enableVirtualScrolling = false,
369
- virtualScrollThreshold = 50,
370
- className,
371
- quickAdd,
372
- onQuickAdd,
373
- conditionalFormatting,
374
- }: KanbanEnhancedProps) {
375
- const [activeCard, setActiveCard] = React.useState<KanbanCard | null>(null)
376
-
377
- const safeColumns = React.useMemo(() => {
378
- return (columns || []).map(col => ({
379
- ...col,
380
- cards: col.cards || []
381
- }));
382
- }, [columns]);
383
-
384
- const [boardColumns, setBoardColumns] = React.useState<KanbanColumn[]>(safeColumns)
385
-
386
- React.useEffect(() => {
387
- setBoardColumns(safeColumns)
388
- }, [safeColumns])
389
-
390
- const sensors = useSensors(
391
- useSensor(PointerSensor, {
392
- activationConstraint: {
393
- distance: 8,
394
- },
395
- })
396
- )
397
-
398
- const handleDragStart = (event: DragStartEvent) => {
399
- const { active } = event
400
- const card = findCard(active.id as string)
401
- setActiveCard(card)
402
- }
403
-
404
- const handleDragEnd = (event: DragEndEvent) => {
405
- const { active, over } = event
406
- setActiveCard(null)
407
-
408
- if (!over) return
409
-
410
- const activeId = active.id as string
411
- const overId = over.id as string
412
-
413
- if (activeId === overId) return
414
-
415
- const activeColumn = findColumnByCardId(activeId)
416
- const overColumn = findColumnByCardId(overId) || findColumnById(overId)
417
-
418
- if (!activeColumn || !overColumn) return
419
-
420
- if (activeColumn.id === overColumn.id) {
421
- const cards = [...activeColumn.cards]
422
- const oldIndex = cards.findIndex((c) => c.id === activeId)
423
- const newIndex = cards.findIndex((c) => c.id === overId)
424
-
425
- const newCards = arrayMove(cards, oldIndex, newIndex)
426
- setBoardColumns((prev) =>
427
- prev.map((col) =>
428
- col.id === activeColumn.id ? { ...col, cards: newCards } : col
429
- )
430
- )
431
- } else {
432
- const activeCards = [...activeColumn.cards]
433
- const overCards = [...overColumn.cards]
434
- const activeIndex = activeCards.findIndex((c) => c.id === activeId)
435
-
436
- const isDroppingOnColumn = overId === overColumn.id
437
- const overIndex = isDroppingOnColumn
438
- ? overCards.length
439
- : overCards.findIndex((c) => c.id === overId)
440
-
441
- const [movedCard] = activeCards.splice(activeIndex, 1)
442
- overCards.splice(overIndex, 0, movedCard)
443
-
444
- setBoardColumns((prev) =>
445
- prev.map((col) => {
446
- if (col.id === activeColumn.id) {
447
- return { ...col, cards: activeCards }
448
- }
449
- if (col.id === overColumn.id) {
450
- return { ...col, cards: overCards }
451
- }
452
- return col
453
- })
454
- )
455
-
456
- if (onCardMove) {
457
- onCardMove(activeId, activeColumn.id, overColumn.id, overIndex)
458
- }
459
- }
460
- }
461
-
462
- const findCard = React.useCallback(
463
- (cardId: string): KanbanCard | null => {
464
- for (const column of boardColumns) {
465
- const card = column.cards.find((c) => c.id === cardId)
466
- if (card) return card
467
- }
468
- return null
469
- },
470
- [boardColumns]
471
- )
472
-
473
- const findColumnByCardId = React.useCallback(
474
- (cardId: string): KanbanColumn | null => {
475
- return boardColumns.find((col) => col.cards.some((c) => c.id === cardId)) || null
476
- },
477
- [boardColumns]
478
- )
479
-
480
- const findColumnById = React.useCallback(
481
- (columnId: string): KanbanColumn | null => {
482
- return boardColumns.find((col) => col.id === columnId) || null
483
- },
484
- [boardColumns]
485
- )
486
-
487
- const handleColumnToggle = React.useCallback((columnId: string, collapsed: boolean) => {
488
- setBoardColumns(prev =>
489
- prev.map(col => col.id === columnId ? { ...col, collapsed } : col)
490
- )
491
- onColumnToggle?.(columnId, collapsed)
492
- }, [onColumnToggle])
493
-
494
- return (
495
- <DndContext
496
- sensors={sensors}
497
- collisionDetection={closestCorners}
498
- onDragStart={handleDragStart}
499
- onDragEnd={handleDragEnd}
500
- >
501
- <div className={cn("flex gap-4 overflow-x-auto p-4", className)}>
502
- {boardColumns.map((column) => {
503
- const shouldUseVirtual = enableVirtualScrolling && column.cards.length > virtualScrollThreshold
504
- return (
505
- <KanbanColumnEnhanced
506
- key={column.id}
507
- column={column}
508
- cards={column.cards}
509
- onToggle={(collapsed) => handleColumnToggle(column.id, collapsed)}
510
- enableVirtual={shouldUseVirtual}
511
- quickAdd={quickAdd}
512
- onQuickAdd={onQuickAdd}
513
- conditionalFormatting={conditionalFormatting}
514
- />
515
- )
516
- })}
517
- </div>
518
- <DragOverlay>
519
- {activeCard ? <SortableCard card={activeCard} conditionalFormatting={conditionalFormatting} /> : null}
520
- </DragOverlay>
521
- </DndContext>
522
- )
523
- }
524
-
525
- export default KanbanEnhanced;