@@ -25,8 +25,8 @@ import {
2525 verticalListSortingStrategy ,
2626} from "@dnd-kit/sortable"
2727import { 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"
28+ import { Badge , Card , CardHeader , CardTitle , CardDescription , CardContent , Button , Input } from "@object-ui/components"
29+ import { ChevronDown , ChevronRight , AlertTriangle , Plus } from "lucide-react"
3030
3131const cn = ( ...classes : ( string | undefined ) [ ] ) => classes . filter ( Boolean ) . join ( ' ' )
3232
@@ -35,6 +35,7 @@ export interface KanbanCard {
3535 title : string
3636 description ?: string
3737 badges ?: Array < { label : string ; variant ?: "default" | "secondary" | "destructive" | "outline" } >
38+ coverImage ?: string
3839 [ key : string ] : any
3940}
4041
@@ -47,16 +48,62 @@ export interface KanbanColumn {
4748 collapsed ?: boolean
4849}
4950
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+
5059export interface KanbanEnhancedProps {
5160 columns : KanbanColumn [ ]
5261 onCardMove ?: ( cardId : string , fromColumnId : string , toColumnId : string , newIndex : number ) => void
5362 onColumnToggle ?: ( columnId : string , collapsed : boolean ) => void
5463 enableVirtualScrolling ?: boolean
5564 virtualScrollThreshold ?: number
5665 className ?: string
66+ quickAdd ?: boolean
67+ onQuickAdd ?: ( columnId : string , title : string ) => void
68+ conditionalFormatting ?: ConditionalFormattingRule [ ]
5769}
5870
59- function SortableCard ( { card } : { card : KanbanCard } ) {
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 [ ] } ) {
60107 const {
61108 attributes,
62109 listeners,
@@ -72,9 +119,21 @@ function SortableCard({ card }: { card: KanbanCard }) {
72119 opacity : isDragging ? 0.5 : undefined ,
73120 }
74121
122+ const cardStyles = getCardStyles ( card , conditionalFormatting )
123+
75124 return (
76125 < 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" >
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+ ) }
78137 < CardHeader className = "p-4" >
79138 < CardTitle className = "text-sm font-medium font-mono tracking-tight text-foreground group-hover:text-primary transition-colors" > { card . title } </ CardTitle >
80139 { card . description && (
@@ -99,7 +158,7 @@ function SortableCard({ card }: { card: KanbanCard }) {
99158 )
100159}
101160
102- function VirtualizedCardList ( { cards, parentRef } : { cards : KanbanCard [ ] ; parentRef : React . RefObject < HTMLDivElement | null > } ) {
161+ function VirtualizedCardList ( { cards, parentRef, conditionalFormatting } : { cards : KanbanCard [ ] ; parentRef : React . RefObject < HTMLDivElement | null > ; conditionalFormatting ?: ConditionalFormattingRule [ ] } ) {
103162 const rowVirtualizer = useVirtualizer ( {
104163 count : cards . length ,
105164 getScrollElement : ( ) => parentRef . current ,
@@ -128,24 +187,87 @@ function VirtualizedCardList({ cards, parentRef }: { cards: KanbanCard[]; parent
128187 transform : `translateY(${ virtualItem . start } px)` ,
129188 } }
130189 >
131- < SortableCard card = { card } />
190+ < SortableCard card = { card } conditionalFormatting = { conditionalFormatting } />
132191 </ div >
133192 )
134193 } ) }
135194 </ div >
136195 )
137196}
138197
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+
139255function KanbanColumnEnhanced ( {
140256 column,
141257 cards,
142258 onToggle,
143259 enableVirtual,
260+ quickAdd,
261+ onQuickAdd,
262+ conditionalFormatting,
144263} : {
145264 column : KanbanColumn
146265 cards : KanbanCard [ ]
147266 onToggle : ( collapsed : boolean ) => void
148267 enableVirtual : boolean
268+ quickAdd ?: boolean
269+ onQuickAdd ?: ( columnId : string , title : string ) => void
270+ conditionalFormatting ?: ConditionalFormattingRule [ ]
149271} ) {
150272 const safeCards = cards || [ ]
151273 const scrollRef = React . useRef < HTMLDivElement > ( null )
@@ -221,15 +343,18 @@ function KanbanColumnEnhanced({
221343 strategy = { verticalListSortingStrategy }
222344 >
223345 { enableVirtual ? (
224- < VirtualizedCardList cards = { safeCards } parentRef = { scrollRef } />
346+ < VirtualizedCardList cards = { safeCards } parentRef = { scrollRef } conditionalFormatting = { conditionalFormatting } />
225347 ) : (
226348 < div className = "space-y-2" >
227349 { safeCards . map ( ( card ) => (
228- < SortableCard key = { card . id } card = { card } />
350+ < SortableCard key = { card . id } card = { card } conditionalFormatting = { conditionalFormatting } />
229351 ) ) }
230352 </ div >
231353 ) }
232354 </ SortableContext >
355+ { quickAdd && onQuickAdd && (
356+ < QuickAddForm columnId = { column . id } onAdd = { onQuickAdd } />
357+ ) }
233358 </ div >
234359 ) }
235360 </ div >
@@ -243,6 +368,9 @@ export function KanbanEnhanced({
243368 enableVirtualScrolling = false ,
244369 virtualScrollThreshold = 50 ,
245370 className,
371+ quickAdd,
372+ onQuickAdd,
373+ conditionalFormatting,
246374} : KanbanEnhancedProps ) {
247375 const [ activeCard , setActiveCard ] = React . useState < KanbanCard | null > ( null )
248376
@@ -380,12 +508,15 @@ export function KanbanEnhanced({
380508 cards = { column . cards }
381509 onToggle = { ( collapsed ) => handleColumnToggle ( column . id , collapsed ) }
382510 enableVirtual = { shouldUseVirtual }
511+ quickAdd = { quickAdd }
512+ onQuickAdd = { onQuickAdd }
513+ conditionalFormatting = { conditionalFormatting }
383514 />
384515 )
385516 } ) }
386517 </ div >
387518 < DragOverlay >
388- { activeCard ? < SortableCard card = { activeCard } /> : null }
519+ { activeCard ? < SortableCard card = { activeCard } conditionalFormatting = { conditionalFormatting } /> : null }
389520 </ DragOverlay >
390521 </ DndContext >
391522 )
0 commit comments