tembro 4.0.1 → 4.2.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/CHANGELOG.md +15 -0
- package/dist/components/display/chat.cjs +1 -0
- package/dist/components/display/chat.d.ts +94 -0
- package/dist/components/display/chat.js +1 -0
- package/dist/components/display/index.d.ts +1 -0
- package/dist/components/display/kanban.d.ts +33 -1
- package/dist/components/inputs/combobox.cjs +1 -0
- package/dist/components/inputs/combobox.d.ts +46 -0
- package/dist/components/inputs/combobox.js +1 -0
- package/dist/components/inputs/index.d.ts +1 -0
- package/dist/components/inputs/public.d.ts +1 -0
- package/dist/components/layout/sidebar.d.ts +24 -2
- package/dist/components/modern/rich-text-editor.d.ts +13 -1
- package/dist/components/theme-provider.d.ts +19 -6
- package/dist/components/ui/badge/index.d.ts +8 -2
- package/dist/src/components/display/activity-feed.cjs +1 -1
- package/dist/src/components/display/activity-feed.js +4 -1
- package/dist/src/components/display/chat.cjs +1 -0
- package/dist/src/components/display/chat.js +372 -0
- package/dist/src/components/display/index.cjs +1 -1
- package/dist/src/components/display/index.js +17 -16
- package/dist/src/components/display/kanban.cjs +1 -1
- package/dist/src/components/display/kanban.js +321 -176
- package/dist/src/components/display/timeline.cjs +1 -1
- package/dist/src/components/display/timeline.js +5 -5
- package/dist/src/components/inputs/combobox.cjs +1 -0
- package/dist/src/components/inputs/combobox.js +162 -0
- package/dist/src/components/inputs/index.cjs +1 -1
- package/dist/src/components/inputs/index.js +10 -9
- package/dist/src/components/inputs/public.cjs +1 -1
- package/dist/src/components/inputs/public.js +10 -9
- package/dist/src/components/inputs/tag-input.cjs +1 -1
- package/dist/src/components/inputs/tag-input.js +6 -2
- package/dist/src/components/layout/index.cjs +1 -1
- package/dist/src/components/layout/index.js +4 -4
- package/dist/src/components/layout/public.cjs +1 -1
- package/dist/src/components/layout/public.js +4 -4
- package/dist/src/components/layout/sidebar.cjs +1 -1
- package/dist/src/components/layout/sidebar.js +326 -228
- package/dist/src/components/modern/rich-text-editor.cjs +1 -1
- package/dist/src/components/modern/rich-text-editor.js +216 -89
- package/dist/src/components/theme-provider.cjs +1 -1
- package/dist/src/components/theme-provider.js +58 -36
- package/dist/src/components/ui/badge/index.cjs +1 -1
- package/dist/src/components/ui/badge/index.js +59 -47
- package/dist/src/components/ui/input/clearable.cjs +1 -1
- package/dist/src/components/ui/input/clearable.js +5 -2
- package/dist/src/components/ui/input/primitive.cjs +1 -1
- package/dist/src/components/ui/input/primitive.js +1 -1
- package/dist/src/index.cjs +1 -1
- package/dist/src/index.js +88 -86
- package/dist/src/public-component-surface.cjs +1 -1
- package/dist/src/public-component-surface.js +107 -41
- package/package.json +1 -1
- package/packages/cli/dist/index.cjs +37 -37
- package/packages/cli/vendor/src/components/display/activity-feed.tsx +1 -1
- package/packages/cli/vendor/src/components/display/chat.tsx +237 -0
- package/packages/cli/vendor/src/components/display/index.ts +1 -0
- package/packages/cli/vendor/src/components/display/kanban.tsx +219 -72
- package/packages/cli/vendor/src/components/display/timeline.tsx +10 -10
- package/packages/cli/vendor/src/components/inputs/combobox.tsx +303 -0
- package/packages/cli/vendor/src/components/inputs/index.ts +1 -0
- package/packages/cli/vendor/src/components/inputs/public.ts +1 -0
- package/packages/cli/vendor/src/components/inputs/tag-input.tsx +3 -2
- package/packages/cli/vendor/src/components/layout/sidebar.tsx +212 -51
- package/packages/cli/vendor/src/components/modern/rich-text-editor.tsx +115 -32
- package/packages/cli/vendor/src/components/theme-provider.tsx +82 -36
- package/packages/cli/vendor/src/components/ui/badge/index.tsx +29 -8
- package/packages/cli/vendor/src/components/ui/input/clearable.tsx +2 -2
- package/packages/cli/vendor/src/components/ui/input/primitive.tsx +1 -1
- package/packages/cli/vendor/src/public-component-surface.ts +36 -22
- package/packages/cli/vendor/templates/showcase/src/showcase/layout/HeroSection.tsx +2 -2
- package/packages/cli/vendor/templates/showcase/src/showcase/layout/WorkbenchSidebar.tsx +1 -1
- package/packages/cli/vendor/templates/styles/globals.css +91 -10
- package/registry.json +28 -11
|
@@ -11,9 +11,12 @@ import {
|
|
|
11
11
|
type DragStartEvent,
|
|
12
12
|
} from "@dnd-kit/react"
|
|
13
13
|
import { isSortable, useSortable } from "@dnd-kit/react/sortable"
|
|
14
|
-
import { GripVerticalIcon } from "lucide-react"
|
|
15
|
-
|
|
16
|
-
import {
|
|
14
|
+
import { CalendarDaysIcon, CheckIcon, GripVerticalIcon, PlusIcon } from "lucide-react"
|
|
15
|
+
|
|
16
|
+
import { Avatar } from "@/components/display/avatar"
|
|
17
|
+
import { Badge } from "@/components/ui/badge"
|
|
18
|
+
import { Button } from "@/components/ui/button"
|
|
19
|
+
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"
|
|
17
20
|
import { cn } from "@/lib/utils"
|
|
18
21
|
|
|
19
22
|
const KANBAN_CARD_PREFIX = "tembro-kanban-card:"
|
|
@@ -36,31 +39,42 @@ function getColumnKey(id: string | number | symbol | undefined) {
|
|
|
36
39
|
return value.startsWith(KANBAN_COLUMN_PREFIX) ? value.slice(KANBAN_COLUMN_PREFIX.length) : value
|
|
37
40
|
}
|
|
38
41
|
|
|
39
|
-
export type KanbanCard = {
|
|
42
|
+
export type KanbanCard = {
|
|
40
43
|
key: string
|
|
41
44
|
title: React.ReactNode
|
|
42
45
|
description?: React.ReactNode
|
|
43
46
|
meta?: React.ReactNode
|
|
44
47
|
extra?: React.ReactNode
|
|
45
|
-
actions?: React.ReactNode
|
|
46
|
-
|
|
47
|
-
|
|
48
|
+
actions?: React.ReactNode
|
|
49
|
+
labels?: Array<React.ReactNode | { key: string; label: React.ReactNode; tone?: "neutral" | "info" | "success" | "warning" | "danger" | "muted" }>
|
|
50
|
+
priority?: "low" | "medium" | "high" | "urgent"
|
|
51
|
+
assignee?: { name: string; src?: string; fallback?: React.ReactNode }
|
|
52
|
+
dueDate?: React.ReactNode
|
|
53
|
+
selected?: boolean
|
|
54
|
+
disabled?: boolean
|
|
55
|
+
}
|
|
48
56
|
|
|
49
57
|
export type KanbanColumn = {
|
|
50
58
|
key: string
|
|
51
59
|
title: React.ReactNode
|
|
52
60
|
description?: React.ReactNode
|
|
53
61
|
cards: KanbanCard[]
|
|
54
|
-
count?: React.ReactNode
|
|
55
|
-
|
|
56
|
-
|
|
62
|
+
count?: React.ReactNode
|
|
63
|
+
limit?: number
|
|
64
|
+
color?: string
|
|
65
|
+
actions?: React.ReactNode
|
|
66
|
+
footer?: React.ReactNode
|
|
67
|
+
disabled?: boolean
|
|
68
|
+
}
|
|
57
69
|
|
|
58
70
|
export type KanbanCardRenderContext = {
|
|
59
71
|
index: number
|
|
60
72
|
isDragging: boolean
|
|
61
|
-
isDropTarget: boolean
|
|
62
|
-
|
|
63
|
-
|
|
73
|
+
isDropTarget: boolean
|
|
74
|
+
selected: boolean
|
|
75
|
+
selectControl: React.ReactNode
|
|
76
|
+
handle: React.ReactNode
|
|
77
|
+
}
|
|
64
78
|
|
|
65
79
|
export type KanbanCardMove = {
|
|
66
80
|
card: KanbanCard
|
|
@@ -74,20 +88,33 @@ export type KanbanBoardProps = Omit<React.ComponentProps<"div">, "defaultValue">
|
|
|
74
88
|
columns?: KanbanColumn[]
|
|
75
89
|
defaultColumns?: KanbanColumn[]
|
|
76
90
|
onColumnsChange?: (columns: KanbanColumn[], change: KanbanCardMove) => void
|
|
77
|
-
onCardMove?: (change: KanbanCardMove) => void
|
|
91
|
+
onCardMove?: (change: KanbanCardMove) => void
|
|
92
|
+
canMoveCard?: (change: KanbanCardMove) => boolean
|
|
78
93
|
renderCard?: (
|
|
79
94
|
card: KanbanCard,
|
|
80
95
|
column: KanbanColumn,
|
|
81
96
|
context: KanbanCardRenderContext
|
|
82
97
|
) => React.ReactNode
|
|
83
|
-
onCardClick?: (card: KanbanCard, column: KanbanColumn) => void
|
|
98
|
+
onCardClick?: (card: KanbanCard, column: KanbanColumn) => void
|
|
99
|
+
onAddCard?: (column: KanbanColumn) => void
|
|
100
|
+
onAddColumn?: () => void
|
|
101
|
+
renderColumnHeader?: (column: KanbanColumn) => React.ReactNode
|
|
102
|
+
renderColumnFooter?: (column: KanbanColumn) => React.ReactNode
|
|
103
|
+
selectionMode?: "none" | "single" | "multiple"
|
|
104
|
+
selectedCardKeys?: string[]
|
|
105
|
+
defaultSelectedCardKeys?: string[]
|
|
106
|
+
onSelectionChange?: (keys: string[], cards: KanbanCard[]) => void
|
|
107
|
+
allowCrossColumn?: boolean
|
|
108
|
+
allowReorder?: boolean
|
|
109
|
+
density?: "compact" | "comfortable"
|
|
110
|
+
columnWidth?: number | string
|
|
84
111
|
disabled?: boolean
|
|
85
112
|
emptyColumn?: React.ReactNode
|
|
86
113
|
dragHandleLabel?: (card: KanbanCard, column: KanbanColumn) => string
|
|
87
114
|
columnClassName?: string
|
|
88
115
|
cardClassName?: string
|
|
89
|
-
overlayClassName?: string
|
|
90
|
-
}
|
|
116
|
+
overlayClassName?: string
|
|
117
|
+
}
|
|
91
118
|
|
|
92
119
|
type KanbanCardViewProps = {
|
|
93
120
|
card: KanbanCard
|
|
@@ -96,6 +123,9 @@ type KanbanCardViewProps = {
|
|
|
96
123
|
boardDisabled: boolean
|
|
97
124
|
renderCard?: KanbanBoardProps["renderCard"]
|
|
98
125
|
onCardClick?: KanbanBoardProps["onCardClick"]
|
|
126
|
+
selected: boolean
|
|
127
|
+
selectionMode: NonNullable<KanbanBoardProps["selectionMode"]>
|
|
128
|
+
onSelect: (card: KanbanCard) => void
|
|
99
129
|
dragHandleLabel?: KanbanBoardProps["dragHandleLabel"]
|
|
100
130
|
cardClassName?: string
|
|
101
131
|
onNativeDragStart?: (source: NativeDragSource) => void
|
|
@@ -191,6 +221,37 @@ function moveKanbanCardToTarget(
|
|
|
191
221
|
},
|
|
192
222
|
}
|
|
193
223
|
}
|
|
224
|
+
|
|
225
|
+
function getKanbanChange(before: KanbanColumn[], after: KanbanColumn[], cardKey: string): KanbanCardMove | null {
|
|
226
|
+
const fromColumn = before.find((column) => column.cards.some((card) => card.key === cardKey))
|
|
227
|
+
const toColumn = after.find((column) => column.cards.some((card) => card.key === cardKey))
|
|
228
|
+
const card = fromColumn?.cards.find((item) => item.key === cardKey)
|
|
229
|
+
if (!fromColumn || !toColumn || !card) return null
|
|
230
|
+
return {
|
|
231
|
+
card,
|
|
232
|
+
fromColumn,
|
|
233
|
+
toColumn,
|
|
234
|
+
fromIndex: fromColumn.cards.findIndex((item) => item.key === cardKey),
|
|
235
|
+
toIndex: toColumn.cards.findIndex((item) => item.key === cardKey),
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
function isKanbanMoveAllowed(
|
|
240
|
+
before: KanbanColumn[],
|
|
241
|
+
after: KanbanColumn[],
|
|
242
|
+
cardKey: string,
|
|
243
|
+
allowCrossColumn: boolean,
|
|
244
|
+
allowReorder: boolean,
|
|
245
|
+
canMoveCard?: KanbanBoardProps["canMoveCard"]
|
|
246
|
+
) {
|
|
247
|
+
const change = getKanbanChange(before, after, cardKey)
|
|
248
|
+
if (!change) return false
|
|
249
|
+
const crossColumn = change.fromColumn.key !== change.toColumn.key
|
|
250
|
+
if (crossColumn && !allowCrossColumn) return false
|
|
251
|
+
if (!crossColumn && !allowReorder) return false
|
|
252
|
+
if (crossColumn && change.toColumn.limit !== undefined && change.toColumn.cards.length > change.toColumn.limit) return false
|
|
253
|
+
return canMoveCard?.(change) ?? true
|
|
254
|
+
}
|
|
194
255
|
|
|
195
256
|
const KanbanDragHandle = React.forwardRef<HTMLButtonElement, {
|
|
196
257
|
label: string
|
|
@@ -210,23 +271,36 @@ const KanbanDragHandle = React.forwardRef<HTMLButtonElement, {
|
|
|
210
271
|
)
|
|
211
272
|
})
|
|
212
273
|
|
|
213
|
-
function DefaultKanbanCard({ card, handle }: { card: KanbanCard; handle: React.ReactNode }) {
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
<div className="flex items-
|
|
274
|
+
function DefaultKanbanCard({ card, handle, selectControl }: { card: KanbanCard; handle: React.ReactNode; selectControl?: React.ReactNode }) {
|
|
275
|
+
const priorityTone = card.priority === "urgent" ? "danger" : card.priority === "high" ? "warning" : card.priority === "medium" ? "info" : "muted"
|
|
276
|
+
return (
|
|
277
|
+
<Card className="border-border/80 shadow-sm transition-[border-color,box-shadow,transform] hover:-translate-y-0.5 hover:border-foreground/20 hover:shadow-md">
|
|
278
|
+
<CardHeader className="p-3.5 pb-2">
|
|
279
|
+
<div className="flex items-start justify-between gap-2">
|
|
280
|
+
<div className="flex min-w-0 items-start gap-2">
|
|
281
|
+
{selectControl}
|
|
282
|
+
<CardTitle className="min-w-0 text-sm leading-5">{card.title}</CardTitle>
|
|
283
|
+
</div>
|
|
284
|
+
<div className="flex items-center gap-1">
|
|
220
285
|
{card.extra}
|
|
221
286
|
{handle}
|
|
222
287
|
</div>
|
|
223
288
|
</div>
|
|
224
289
|
</CardHeader>
|
|
225
|
-
{(card.description || card.meta) ? (
|
|
226
|
-
<CardContent className="grid gap-
|
|
227
|
-
{card.description}
|
|
228
|
-
{card.
|
|
229
|
-
|
|
290
|
+
{(card.description || card.meta || card.labels?.length || card.priority || card.assignee || card.dueDate || card.actions) ? (
|
|
291
|
+
<CardContent className="grid gap-3 p-3.5 pt-0 text-xs text-muted-foreground">
|
|
292
|
+
{card.description ? <div className="line-clamp-3 leading-5">{card.description}</div> : null}
|
|
293
|
+
{card.labels?.length || card.priority ? <div className="flex flex-wrap gap-1.5">
|
|
294
|
+
{card.priority ? <Badge size="sm" tone={priorityTone} variant="soft" label={card.priority} showDot /> : null}
|
|
295
|
+
{card.labels?.map((label, index) => React.isValidElement(label) ? React.cloneElement(label, { key: label.key ?? index }) : typeof label === "object" && label !== null && "key" in label ? <Badge key={label.key} size="sm" variant="outline" tone={label.tone} label={label.label} /> : <Badge key={index} size="sm" variant="outline" label={label} />)}
|
|
296
|
+
</div> : null}
|
|
297
|
+
{card.meta ? <div>{card.meta}</div> : null}
|
|
298
|
+
{(card.assignee || card.dueDate) ? <div className="flex items-center justify-between gap-3 border-t pt-2.5">
|
|
299
|
+
{card.assignee ? <div className="flex min-w-0 items-center gap-2"><Avatar size="xs" name={card.assignee.name} src={card.assignee.src} fallback={card.assignee.fallback} /><span className="truncate">{card.assignee.name}</span></div> : <span />}
|
|
300
|
+
{card.dueDate ? <span className="inline-flex shrink-0 items-center gap-1"><CalendarDaysIcon className="size-3.5" />{card.dueDate}</span> : null}
|
|
301
|
+
</div> : null}
|
|
302
|
+
{card.actions ? <div className="flex flex-wrap items-center gap-2">{card.actions}</div> : null}
|
|
303
|
+
</CardContent>
|
|
230
304
|
) : null}
|
|
231
305
|
</Card>
|
|
232
306
|
)
|
|
@@ -239,6 +313,9 @@ function KanbanCardView({
|
|
|
239
313
|
boardDisabled,
|
|
240
314
|
renderCard,
|
|
241
315
|
onCardClick,
|
|
316
|
+
selected,
|
|
317
|
+
selectionMode,
|
|
318
|
+
onSelect,
|
|
242
319
|
dragHandleLabel,
|
|
243
320
|
cardClassName,
|
|
244
321
|
onNativeDragStart,
|
|
@@ -257,18 +334,31 @@ function KanbanCardView({
|
|
|
257
334
|
transition: { duration: 180, easing: "cubic-bezier(0.2, 0, 0, 1)", idle: true },
|
|
258
335
|
})
|
|
259
336
|
const label = dragHandleLabel?.(card, column) ?? `Move ${String(card.title)}`
|
|
260
|
-
const handle = (
|
|
337
|
+
const handle = (
|
|
261
338
|
<KanbanDragHandle
|
|
262
339
|
ref={handleRef as React.Ref<HTMLButtonElement>}
|
|
263
340
|
label={label}
|
|
264
341
|
disabled={disabled}
|
|
265
342
|
/>
|
|
266
|
-
)
|
|
267
|
-
const
|
|
343
|
+
)
|
|
344
|
+
const selectControl = selectionMode !== "none" ? (
|
|
345
|
+
<button
|
|
346
|
+
type="button"
|
|
347
|
+
aria-label={`${selected ? "Deselect" : "Select"} ${String(card.title)}`}
|
|
348
|
+
aria-pressed={selected}
|
|
349
|
+
className={cn("mt-0.5 grid size-4 shrink-0 place-items-center rounded border bg-background text-primary outline-none transition-colors focus-visible:ring-2 focus-visible:ring-ring", selected && "border-primary bg-primary text-primary-foreground")}
|
|
350
|
+
onClick={(event) => { event.stopPropagation(); onSelect(card) }}
|
|
351
|
+
>
|
|
352
|
+
{selected ? <CheckIcon className="size-3" strokeWidth={3} /> : null}
|
|
353
|
+
</button>
|
|
354
|
+
) : null
|
|
355
|
+
const context: KanbanCardRenderContext = {
|
|
268
356
|
index,
|
|
269
357
|
isDragging,
|
|
270
|
-
isDropTarget,
|
|
271
|
-
|
|
358
|
+
isDropTarget,
|
|
359
|
+
selected,
|
|
360
|
+
selectControl,
|
|
361
|
+
handle,
|
|
272
362
|
}
|
|
273
363
|
|
|
274
364
|
return (
|
|
@@ -278,15 +368,15 @@ function KanbanCardView({
|
|
|
278
368
|
data-dragging={isDragging || undefined}
|
|
279
369
|
data-drop-target={isDropTarget || undefined}
|
|
280
370
|
data-disabled={disabled || undefined}
|
|
281
|
-
role=
|
|
282
|
-
tabIndex={onCardClick && !disabled ? 0 : undefined}
|
|
371
|
+
role="group"
|
|
283
372
|
aria-label={typeof card.title === "string" ? card.title : undefined}
|
|
284
373
|
draggable={!disabled}
|
|
285
374
|
className={cn(
|
|
286
375
|
"rounded-lg outline-none transition-[opacity,box-shadow]",
|
|
287
376
|
onCardClick && !disabled && "cursor-pointer focus-visible:ring-2 focus-visible:ring-ring",
|
|
288
377
|
isDropTarget && "ring-2 ring-primary/30",
|
|
289
|
-
isDragging && "opacity-30",
|
|
378
|
+
isDragging && "opacity-30",
|
|
379
|
+
selected && "ring-2 ring-primary/35",
|
|
290
380
|
disabled && "opacity-55",
|
|
291
381
|
cardClassName
|
|
292
382
|
)}
|
|
@@ -310,15 +400,8 @@ function KanbanCardView({
|
|
|
310
400
|
event.stopPropagation()
|
|
311
401
|
onNativeDrop?.({ columnKey: column.key, index })
|
|
312
402
|
}}
|
|
313
|
-
onKeyDown={(event) => {
|
|
314
|
-
if (event.target !== event.currentTarget || disabled || !onCardClick) return
|
|
315
|
-
if (event.key === "Enter" || event.key === " ") {
|
|
316
|
-
event.preventDefault()
|
|
317
|
-
onCardClick(card, column)
|
|
318
|
-
}
|
|
319
|
-
}}
|
|
320
403
|
>
|
|
321
|
-
{renderCard?.(card, column, context) ?? <DefaultKanbanCard card={card} handle={handle} />}
|
|
404
|
+
{renderCard?.(card, column, context) ?? <DefaultKanbanCard card={card} handle={handle} selectControl={selectControl} />}
|
|
322
405
|
</article>
|
|
323
406
|
)
|
|
324
407
|
}
|
|
@@ -327,7 +410,13 @@ function KanbanColumnView({
|
|
|
327
410
|
column,
|
|
328
411
|
boardDisabled,
|
|
329
412
|
renderCard,
|
|
330
|
-
onCardClick,
|
|
413
|
+
onCardClick,
|
|
414
|
+
selectedKeys,
|
|
415
|
+
selectionMode,
|
|
416
|
+
onSelect,
|
|
417
|
+
onAddCard,
|
|
418
|
+
renderColumnHeader,
|
|
419
|
+
renderColumnFooter,
|
|
331
420
|
dragHandleLabel,
|
|
332
421
|
emptyColumn,
|
|
333
422
|
columnClassName,
|
|
@@ -335,11 +424,18 @@ function KanbanColumnView({
|
|
|
335
424
|
onNativeDragStart,
|
|
336
425
|
onNativeDragEnd,
|
|
337
426
|
onNativeDrop,
|
|
427
|
+
density,
|
|
338
428
|
}: {
|
|
339
429
|
column: KanbanColumn
|
|
340
430
|
boardDisabled: boolean
|
|
341
431
|
renderCard?: KanbanBoardProps["renderCard"]
|
|
342
|
-
onCardClick?: KanbanBoardProps["onCardClick"]
|
|
432
|
+
onCardClick?: KanbanBoardProps["onCardClick"]
|
|
433
|
+
selectedKeys: Set<string>
|
|
434
|
+
selectionMode: NonNullable<KanbanBoardProps["selectionMode"]>
|
|
435
|
+
onSelect: (card: KanbanCard) => void
|
|
436
|
+
onAddCard?: KanbanBoardProps["onAddCard"]
|
|
437
|
+
renderColumnHeader?: KanbanBoardProps["renderColumnHeader"]
|
|
438
|
+
renderColumnFooter?: KanbanBoardProps["renderColumnFooter"]
|
|
343
439
|
dragHandleLabel?: KanbanBoardProps["dragHandleLabel"]
|
|
344
440
|
emptyColumn: React.ReactNode
|
|
345
441
|
columnClassName?: string
|
|
@@ -347,6 +443,7 @@ function KanbanColumnView({
|
|
|
347
443
|
onNativeDragStart?: (source: NativeDragSource) => void
|
|
348
444
|
onNativeDragEnd?: () => void
|
|
349
445
|
onNativeDrop?: (target: NativeDropTarget) => void
|
|
446
|
+
density: NonNullable<KanbanBoardProps["density"]>
|
|
350
447
|
}) {
|
|
351
448
|
const { ref, isDropTarget } = useDroppable({
|
|
352
449
|
id: getColumnId(column.key),
|
|
@@ -362,10 +459,11 @@ function KanbanColumnView({
|
|
|
362
459
|
ref={ref as React.Ref<HTMLElement>}
|
|
363
460
|
data-slot="kanban-column"
|
|
364
461
|
data-drop-target={isDropTarget || undefined}
|
|
365
|
-
data-disabled={column.disabled || undefined}
|
|
462
|
+
data-disabled={column.disabled || undefined}
|
|
463
|
+
data-density={density}
|
|
366
464
|
aria-label={typeof column.title === "string" ? column.title : undefined}
|
|
367
465
|
className={cn(
|
|
368
|
-
"grid min-h-40 content-start gap-3 rounded-lg border bg-muted/
|
|
466
|
+
"grid min-h-40 content-start gap-3 rounded-lg border bg-muted/25 p-3 transition-[border-color,background-color,box-shadow] data-[density=compact]:gap-2 data-[density=compact]:p-2.5",
|
|
369
467
|
isDropTarget && "border-primary/60 bg-primary/5 ring-2 ring-primary/15",
|
|
370
468
|
column.disabled && "opacity-60",
|
|
371
469
|
columnClassName
|
|
@@ -379,15 +477,18 @@ function KanbanColumnView({
|
|
|
379
477
|
onNativeDrop?.({ columnKey: column.key, index: column.cards.length })
|
|
380
478
|
}}
|
|
381
479
|
>
|
|
382
|
-
<div className="flex items-start justify-between gap-3">
|
|
383
|
-
<
|
|
480
|
+
{renderColumnHeader?.(column) ?? <div className="flex items-start justify-between gap-3">
|
|
481
|
+
{column.color ? <span className="mt-1.5 size-2 shrink-0 rounded-full" style={{ backgroundColor: column.color }} /> : null}
|
|
482
|
+
<div className="grid gap-0.5">
|
|
384
483
|
<h3 className="text-sm font-semibold text-foreground">{column.title}</h3>
|
|
385
484
|
{column.description ? <p className="text-xs text-muted-foreground">{column.description}</p> : null}
|
|
386
485
|
</div>
|
|
387
|
-
<div className="
|
|
388
|
-
{column.
|
|
389
|
-
|
|
390
|
-
|
|
486
|
+
<div className="ml-auto flex items-center gap-1">
|
|
487
|
+
{column.actions}
|
|
488
|
+
{onAddCard && !column.disabled ? <Button size="icon-xs" variant="ghost" iconOnly aria-label={`Add card to ${String(column.title)}`} onClick={() => onAddCard(column)}><PlusIcon /></Button> : null}
|
|
489
|
+
<Badge size="sm" variant={column.limit !== undefined && column.cards.length >= column.limit ? "soft" : "outline"} tone={column.limit !== undefined && column.cards.length >= column.limit ? "warning" : "muted"} label={column.count ?? (column.limit === undefined ? column.cards.length : `${column.cards.length}/${column.limit}`)} />
|
|
490
|
+
</div>
|
|
491
|
+
</div>}
|
|
391
492
|
<div className="grid min-h-16 content-start gap-2">
|
|
392
493
|
{column.cards.length === 0 ? (
|
|
393
494
|
<div className="grid min-h-20 place-items-center rounded-lg border border-dashed p-3 text-center text-xs text-muted-foreground">
|
|
@@ -403,7 +504,10 @@ function KanbanColumnView({
|
|
|
403
504
|
index={index}
|
|
404
505
|
boardDisabled={boardDisabled}
|
|
405
506
|
renderCard={renderCard}
|
|
406
|
-
onCardClick={onCardClick}
|
|
507
|
+
onCardClick={onCardClick}
|
|
508
|
+
selected={selectedKeys.has(card.key) || card.selected === true}
|
|
509
|
+
selectionMode={selectionMode}
|
|
510
|
+
onSelect={onSelect}
|
|
407
511
|
dragHandleLabel={dragHandleLabel}
|
|
408
512
|
cardClassName={cardClassName}
|
|
409
513
|
onNativeDragStart={onNativeDragStart}
|
|
@@ -411,9 +515,10 @@ function KanbanColumnView({
|
|
|
411
515
|
onNativeDrop={onNativeDrop}
|
|
412
516
|
/>
|
|
413
517
|
))
|
|
414
|
-
)}
|
|
415
|
-
</div>
|
|
416
|
-
|
|
518
|
+
)}
|
|
519
|
+
</div>
|
|
520
|
+
{renderColumnFooter?.(column) ?? column.footer ?? (onAddCard && !column.disabled ? <Button size="sm" variant="ghost" className="w-full justify-start" leftIcon={<PlusIcon />} onClick={() => onAddCard(column)}>Add task</Button> : null)}
|
|
521
|
+
</section>
|
|
417
522
|
)
|
|
418
523
|
}
|
|
419
524
|
|
|
@@ -421,9 +526,22 @@ function KanbanBoard({
|
|
|
421
526
|
columns: columnsProp,
|
|
422
527
|
defaultColumns = [],
|
|
423
528
|
onColumnsChange,
|
|
424
|
-
onCardMove,
|
|
529
|
+
onCardMove,
|
|
530
|
+
canMoveCard,
|
|
425
531
|
renderCard,
|
|
426
|
-
onCardClick,
|
|
532
|
+
onCardClick,
|
|
533
|
+
onAddCard,
|
|
534
|
+
onAddColumn,
|
|
535
|
+
renderColumnHeader,
|
|
536
|
+
renderColumnFooter,
|
|
537
|
+
selectionMode = "none",
|
|
538
|
+
selectedCardKeys,
|
|
539
|
+
defaultSelectedCardKeys = [],
|
|
540
|
+
onSelectionChange,
|
|
541
|
+
allowCrossColumn = true,
|
|
542
|
+
allowReorder = true,
|
|
543
|
+
density = "comfortable",
|
|
544
|
+
columnWidth = 320,
|
|
427
545
|
disabled = false,
|
|
428
546
|
emptyColumn = "Drop cards here.",
|
|
429
547
|
dragHandleLabel,
|
|
@@ -439,9 +557,22 @@ function KanbanBoard({
|
|
|
439
557
|
const [activeCardId, setActiveCardId] = React.useState<string | null>(null)
|
|
440
558
|
const draftColumnsRef = React.useRef<KanbanColumn[] | null>(null)
|
|
441
559
|
const nativeDragRef = React.useRef<NativeDragSource | null>(null)
|
|
560
|
+
const [internalSelectedKeys, setInternalSelectedKeys] = React.useState(defaultSelectedCardKeys)
|
|
442
561
|
const columns = columnsProp ?? internalColumns
|
|
443
562
|
const renderedColumns = draftColumns ?? columns
|
|
444
|
-
const isControlled = columnsProp !== undefined
|
|
563
|
+
const isControlled = columnsProp !== undefined
|
|
564
|
+
const resolvedSelectedKeys = selectedCardKeys ?? internalSelectedKeys
|
|
565
|
+
const selectedKeySet = React.useMemo(() => new Set(resolvedSelectedKeys), [resolvedSelectedKeys])
|
|
566
|
+
|
|
567
|
+
function updateSelection(card: KanbanCard) {
|
|
568
|
+
if (selectionMode === "none" || card.disabled) return
|
|
569
|
+
const next = selectionMode === "single"
|
|
570
|
+
? (selectedKeySet.has(card.key) ? [] : [card.key])
|
|
571
|
+
: (selectedKeySet.has(card.key) ? resolvedSelectedKeys.filter((key) => key !== card.key) : [...resolvedSelectedKeys, card.key])
|
|
572
|
+
if (selectedCardKeys === undefined) setInternalSelectedKeys(next)
|
|
573
|
+
const allCards = columns.flatMap((column) => column.cards)
|
|
574
|
+
onSelectionChange?.(next, allCards.filter((item) => next.includes(item.key)))
|
|
575
|
+
}
|
|
445
576
|
|
|
446
577
|
function handleDragStart(event: DragStartEvent) {
|
|
447
578
|
const source = event.operation.source
|
|
@@ -458,7 +589,9 @@ function KanbanBoard({
|
|
|
458
589
|
if (!source || !isSortable(source)) return
|
|
459
590
|
|
|
460
591
|
const currentColumns = draftColumnsRef.current ?? columns
|
|
461
|
-
const nextColumns = moveKanbanCards(currentColumns, event)
|
|
592
|
+
const nextColumns = moveKanbanCards(currentColumns, event)
|
|
593
|
+
const cardKey = getCardKey(String(source.id))
|
|
594
|
+
if (!isKanbanMoveAllowed(columns, nextColumns, cardKey, allowCrossColumn, allowReorder, canMoveCard)) return
|
|
462
595
|
draftColumnsRef.current = nextColumns
|
|
463
596
|
setDraftColumns(nextColumns)
|
|
464
597
|
}
|
|
@@ -473,7 +606,7 @@ function KanbanBoard({
|
|
|
473
606
|
return
|
|
474
607
|
}
|
|
475
608
|
|
|
476
|
-
const finalColumns = draftColumnsRef.current ?? moveKanbanCards(columns, event)
|
|
609
|
+
const finalColumns = draftColumnsRef.current ?? moveKanbanCards(columns, event)
|
|
477
610
|
const cardKey = getCardKey(String(source.id))
|
|
478
611
|
const fromColumnKey = getColumnKey(source.initialGroup)
|
|
479
612
|
const toColumnKey = getColumnKey(source.group)
|
|
@@ -484,15 +617,16 @@ function KanbanBoard({
|
|
|
484
617
|
draftColumnsRef.current = null
|
|
485
618
|
setDraftColumns(null)
|
|
486
619
|
|
|
487
|
-
if (!card || !fromColumn || !toColumn) return
|
|
620
|
+
if (!card || !fromColumn || !toColumn) return
|
|
488
621
|
|
|
489
|
-
const change: KanbanCardMove = {
|
|
622
|
+
const change: KanbanCardMove = {
|
|
490
623
|
card,
|
|
491
624
|
fromColumn,
|
|
492
625
|
toColumn,
|
|
493
626
|
fromIndex: source.initialIndex,
|
|
494
627
|
toIndex: source.index,
|
|
495
|
-
}
|
|
628
|
+
}
|
|
629
|
+
if (!isKanbanMoveAllowed(columns, finalColumns, card.key, allowCrossColumn, allowReorder, canMoveCard)) return
|
|
496
630
|
if (!isControlled) setInternalColumns(finalColumns)
|
|
497
631
|
onColumnsChange?.(finalColumns, change)
|
|
498
632
|
onCardMove?.(change)
|
|
@@ -505,6 +639,7 @@ function KanbanBoard({
|
|
|
505
639
|
|
|
506
640
|
const result = moveKanbanCardToTarget(columns, source, target)
|
|
507
641
|
if (!result) return
|
|
642
|
+
if (!isKanbanMoveAllowed(columns, result.columns, result.change.card.key, allowCrossColumn, allowReorder, canMoveCard)) return
|
|
508
643
|
|
|
509
644
|
if (!isControlled) setInternalColumns(result.columns)
|
|
510
645
|
onColumnsChange?.(result.columns, result.change)
|
|
@@ -527,9 +662,11 @@ function KanbanBoard({
|
|
|
527
662
|
{...props}
|
|
528
663
|
role="group"
|
|
529
664
|
aria-label={ariaLabel}
|
|
530
|
-
data-slot="kanban-board"
|
|
665
|
+
data-slot="kanban-board"
|
|
666
|
+
data-density={density}
|
|
667
|
+
style={{ "--kanban-column-width": typeof columnWidth === "number" ? `${columnWidth}px` : columnWidth, ...props.style } as React.CSSProperties}
|
|
531
668
|
className={cn(
|
|
532
|
-
"grid gap-4 sm:auto-cols-[
|
|
669
|
+
"grid gap-4 sm:auto-cols-[var(--kanban-column-width)] sm:grid-flow-col sm:overflow-x-auto sm:pb-2",
|
|
533
670
|
className
|
|
534
671
|
)}
|
|
535
672
|
>
|
|
@@ -539,7 +676,13 @@ function KanbanBoard({
|
|
|
539
676
|
column={column}
|
|
540
677
|
boardDisabled={disabled}
|
|
541
678
|
renderCard={renderCard}
|
|
542
|
-
onCardClick={onCardClick}
|
|
679
|
+
onCardClick={onCardClick}
|
|
680
|
+
selectedKeys={selectedKeySet}
|
|
681
|
+
selectionMode={selectionMode}
|
|
682
|
+
onSelect={updateSelection}
|
|
683
|
+
onAddCard={onAddCard}
|
|
684
|
+
renderColumnHeader={renderColumnHeader}
|
|
685
|
+
renderColumnFooter={renderColumnFooter}
|
|
543
686
|
dragHandleLabel={dragHandleLabel}
|
|
544
687
|
emptyColumn={emptyColumn}
|
|
545
688
|
columnClassName={columnClassName}
|
|
@@ -547,8 +690,10 @@ function KanbanBoard({
|
|
|
547
690
|
onNativeDragStart={(source) => { nativeDragRef.current = source }}
|
|
548
691
|
onNativeDragEnd={() => { nativeDragRef.current = null }}
|
|
549
692
|
onNativeDrop={handleNativeDrop}
|
|
693
|
+
density={density}
|
|
550
694
|
/>
|
|
551
|
-
))}
|
|
695
|
+
))}
|
|
696
|
+
{onAddColumn ? <button type="button" className="flex min-h-32 items-center justify-center gap-2 rounded-lg border border-dashed bg-muted/15 text-sm font-medium text-muted-foreground outline-none transition-colors hover:border-foreground/25 hover:bg-muted/35 hover:text-foreground focus-visible:ring-2 focus-visible:ring-ring" onClick={onAddColumn}><PlusIcon className="size-4" />Add column</button> : null}
|
|
552
697
|
</div>
|
|
553
698
|
|
|
554
699
|
<DragOverlay>
|
|
@@ -559,7 +704,9 @@ function KanbanBoard({
|
|
|
559
704
|
isDragging: true,
|
|
560
705
|
isDropTarget: false,
|
|
561
706
|
handle: null,
|
|
562
|
-
|
|
707
|
+
selected: selectedKeySet.has(activeEntry.card.key),
|
|
708
|
+
selectControl: null,
|
|
709
|
+
}) ?? <DefaultKanbanCard card={activeEntry.card} handle={null} />}
|
|
563
710
|
</div>
|
|
564
711
|
) : null}
|
|
565
712
|
</DragOverlay>
|
|
@@ -78,28 +78,28 @@ function Timeline({
|
|
|
78
78
|
)
|
|
79
79
|
}
|
|
80
80
|
|
|
81
|
-
function TimelineDot({ item }: { item: TimelineItem }) {
|
|
81
|
+
function TimelineDot({ item }: { item: TimelineItem }) {
|
|
82
82
|
const tone = item.tone ?? "default"
|
|
83
83
|
|
|
84
84
|
return (
|
|
85
85
|
<span
|
|
86
86
|
data-slot="timeline-dot"
|
|
87
87
|
data-tone={tone}
|
|
88
|
-
className={cn("
|
|
88
|
+
className={cn("relative z-10 grid size-7 shrink-0 place-items-center rounded-full border-2 border-background text-[10px] shadow-[0_0_0_1px_var(--border)]", dotClassName[tone])}
|
|
89
89
|
>
|
|
90
90
|
{item.icon ?? <CircleIcon className="size-2 fill-current" />}
|
|
91
91
|
</span>
|
|
92
92
|
)
|
|
93
93
|
}
|
|
94
94
|
|
|
95
|
-
function TimelineVerticalItem({ item, compact, className, isLast }: { item: TimelineItem; compact: boolean; className?: string; isLast?: boolean }) {
|
|
96
|
-
return (
|
|
97
|
-
<div data-slot="timeline-item" className={cn("grid grid-cols-[
|
|
98
|
-
<div className="
|
|
99
|
-
<TimelineDot item={item} />
|
|
100
|
-
{!isLast && <div data-slot="timeline-line" className="w-px
|
|
101
|
-
</div>
|
|
102
|
-
<div className={cn("min-w-0 pb-5", compact && "pb-3")}>
|
|
95
|
+
function TimelineVerticalItem({ item, compact, className, isLast }: { item: TimelineItem; compact: boolean; className?: string; isLast?: boolean }) {
|
|
96
|
+
return (
|
|
97
|
+
<div data-slot="timeline-item" className={cn("relative grid grid-cols-[1.75rem_minmax(0,1fr)] gap-3", className, item.className)}>
|
|
98
|
+
<div className="relative flex justify-center">
|
|
99
|
+
<TimelineDot item={item} />
|
|
100
|
+
{!isLast && <div data-slot="timeline-line" className="absolute left-1/2 top-7 h-[calc(100%_-_1.75rem)] w-px -translate-x-1/2 bg-border" />}
|
|
101
|
+
</div>
|
|
102
|
+
<div className={cn("min-w-0 pb-5 pt-0.5", compact && "pb-3")}>
|
|
103
103
|
<div className="flex min-w-0 flex-col gap-1 sm:flex-row sm:items-start sm:justify-between">
|
|
104
104
|
<div className="min-w-0">
|
|
105
105
|
{item.title && <div className="text-sm font-medium text-foreground">{item.title}</div>}
|