@startsimpli/ui 0.4.6 → 0.4.7

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 (69) hide show
  1. package/package.json +2 -1
  2. package/src/components/ActivityTimeline.tsx +173 -0
  3. package/src/components/LogActivityDialog.tsx +303 -0
  4. package/src/components/QuickLogButtons.tsx +32 -0
  5. package/src/components/badge/StageBadge.tsx +31 -0
  6. package/src/components/badge/index.ts +3 -0
  7. package/src/components/command-palette/CommandPalette.tsx +344 -0
  8. package/src/components/command-palette/command-palette-context.tsx +51 -0
  9. package/src/components/command-palette/index.ts +3 -0
  10. package/src/components/compose/compose-header.tsx +72 -0
  11. package/src/components/compose/compose-loading.tsx +13 -0
  12. package/src/components/compose/index.ts +6 -0
  13. package/src/components/compose/save-status-indicator.tsx +57 -0
  14. package/src/components/compose/send-confirmation-dialog.tsx +87 -0
  15. package/src/components/compose/subject-input.tsx +25 -0
  16. package/src/components/compose/useAutoSave.ts +93 -0
  17. package/src/components/dashboard/DashboardGrid.tsx +32 -0
  18. package/src/components/dashboard/DashboardSection.tsx +32 -0
  19. package/src/components/dashboard/MetricCard.tsx +129 -0
  20. package/src/components/dashboard/PeriodSelector.tsx +55 -0
  21. package/src/components/dashboard/SparklineTrend.tsx +102 -0
  22. package/src/components/dashboard/index.ts +14 -0
  23. package/src/components/email-dialogs/index.ts +14 -0
  24. package/src/components/email-dialogs/merge-fields.tsx +196 -0
  25. package/src/components/email-dialogs/preview-dialog.tsx +194 -0
  26. package/src/components/email-dialogs/schedule-dialog.tsx +297 -0
  27. package/src/components/email-dialogs/template-picker.tsx +225 -0
  28. package/src/components/email-dialogs/test-send-dialog.tsx +188 -0
  29. package/src/components/email-editor/add-block-menu.tsx +151 -0
  30. package/src/components/email-editor/block-toolbar.tsx +73 -0
  31. package/src/components/email-editor/blocks/button-block.tsx +44 -0
  32. package/src/components/email-editor/blocks/divider-block.tsx +43 -0
  33. package/src/components/email-editor/blocks/footer-block.tsx +39 -0
  34. package/src/components/email-editor/blocks/header-block.tsx +39 -0
  35. package/src/components/email-editor/blocks/image-block.tsx +61 -0
  36. package/src/components/email-editor/blocks/index.ts +9 -0
  37. package/src/components/email-editor/blocks/metrics-block.tsx +198 -0
  38. package/src/components/email-editor/blocks/social-block.tsx +75 -0
  39. package/src/components/email-editor/blocks/spacer-block.tsx +26 -0
  40. package/src/components/email-editor/blocks/text-block.tsx +75 -0
  41. package/src/components/email-editor/editor-sidebar.tsx +791 -0
  42. package/src/components/email-editor/email-editor.tsx +886 -0
  43. package/src/components/email-editor/index.ts +50 -0
  44. package/src/components/email-editor/renderer/block-renderers.ts +209 -0
  45. package/src/components/email-editor/renderer/email-html-renderer.ts +128 -0
  46. package/src/components/email-editor/types.ts +413 -0
  47. package/src/components/email-editor/utils/defaults.ts +116 -0
  48. package/src/components/email-editor/utils/undo-redo.ts +59 -0
  49. package/src/components/enrichment/EnrichButton.tsx +33 -0
  50. package/src/components/enrichment/EnrichmentProgress.tsx +66 -0
  51. package/src/components/enrichment/QualityBadge.tsx +43 -0
  52. package/src/components/enrichment/index.ts +8 -0
  53. package/src/components/gantt/GanttChart.tsx +25 -25
  54. package/src/components/gantt/types.ts +5 -5
  55. package/src/components/index.ts +46 -0
  56. package/src/components/integrations/ConnectionStatus.tsx +77 -0
  57. package/src/components/integrations/IntegrationCard.tsx +92 -0
  58. package/src/components/integrations/index.ts +5 -0
  59. package/src/components/kanban/KanbanBoard.tsx +103 -0
  60. package/src/components/kanban/index.ts +2 -0
  61. package/src/components/lists/CreateListDialog.tsx +158 -0
  62. package/src/components/lists/ListCard.tsx +77 -0
  63. package/src/components/lists/index.ts +5 -0
  64. package/src/components/pipeline/StageTransitionModal.tsx +146 -0
  65. package/src/components/pipeline/index.ts +2 -0
  66. package/src/components/settings/SettingsCard.tsx +33 -0
  67. package/src/components/settings/SettingsLayout.tsx +28 -0
  68. package/src/components/settings/SettingsNav.tsx +42 -0
  69. package/src/components/settings/index.ts +6 -0
@@ -0,0 +1,43 @@
1
+ import * as React from 'react'
2
+
3
+ export interface QualityScore {
4
+ score: number
5
+ grade: 'A' | 'B' | 'C' | 'D' | 'F'
6
+ }
7
+
8
+ export interface QualityBadgeProps {
9
+ quality: QualityScore
10
+ showScore?: boolean
11
+ }
12
+
13
+ const GRADE_STYLES: Record<QualityScore['grade'], string> = {
14
+ A: 'bg-green-100 text-green-800 border-green-200',
15
+ B: 'bg-blue-100 text-blue-800 border-blue-200',
16
+ C: 'bg-yellow-100 text-yellow-800 border-yellow-200',
17
+ D: 'bg-orange-100 text-orange-800 border-orange-200',
18
+ F: 'bg-red-100 text-red-800 border-red-200',
19
+ }
20
+
21
+ const GRADE_LABELS: Record<QualityScore['grade'], string> = {
22
+ A: 'Excellent',
23
+ B: 'Good',
24
+ C: 'Fair',
25
+ D: 'Poor',
26
+ F: 'Incomplete',
27
+ }
28
+
29
+ export function QualityBadge({ quality, showScore = false }: QualityBadgeProps) {
30
+ const { grade, score } = quality
31
+ const styleClass = GRADE_STYLES[grade]
32
+ const label = GRADE_LABELS[grade]
33
+
34
+ return (
35
+ <span
36
+ className={`inline-flex items-center gap-1 px-2 py-0.5 rounded-full text-xs font-semibold border ${styleClass}`}
37
+ title={`Data quality: ${label} (${score}/100)`}
38
+ >
39
+ <span>{grade}</span>
40
+ {showScore && <span className="font-normal opacity-75">({score})</span>}
41
+ </span>
42
+ )
43
+ }
@@ -0,0 +1,8 @@
1
+ export { QualityBadge } from './QualityBadge'
2
+ export type { QualityBadgeProps, QualityScore } from './QualityBadge'
3
+
4
+ export { EnrichButton } from './EnrichButton'
5
+ export type { EnrichButtonProps } from './EnrichButton'
6
+
7
+ export { EnrichmentProgress } from './EnrichmentProgress'
8
+ export type { EnrichmentProgressProps, QueueStatus as EnrichmentQueueStatus } from './EnrichmentProgress'
@@ -173,8 +173,8 @@ export function GanttChart({
173
173
  if (statuses.length > 0 && !statuses.includes(item.status)) return false
174
174
  if (categories.length > 0 && (!item.category || !categories.includes(item.category))) return false
175
175
  if (dateRange.start || dateRange.end) {
176
- const itemEnd = item.end_date ? new Date(item.end_date) : null
177
- const itemStart = item.start_date ? new Date(item.start_date) : null
176
+ const itemEnd = item.endDate ? new Date(item.endDate) : null
177
+ const itemStart = item.startDate ? new Date(item.startDate) : null
178
178
  if (dateRange.start && itemEnd && isBefore(itemEnd, dateRange.start)) return false
179
179
  if (dateRange.end && itemStart && isAfter(itemStart, dateRange.end)) return false
180
180
  }
@@ -229,16 +229,16 @@ export function GanttChart({
229
229
 
230
230
  for (const dep of dependencies) {
231
231
  if (dep.type === 'blocks') {
232
- const parentItem = itemMap.get(dep.from_id)
233
- const childItem = itemMap.get(dep.to_id)
232
+ const parentItem = itemMap.get(dep.fromId)
233
+ const childItem = itemMap.get(dep.toId)
234
234
  if (!parentItem || !childItem) continue
235
235
  const parentLevel = getHierarchyLevel(parentItem.title)
236
236
  const childLevel = getHierarchyLevel(childItem.title)
237
- if (parentLevel > childLevel && !parentMap.has(dep.to_id)) {
238
- const children = childrenMap.get(dep.from_id) || []
239
- children.push(dep.to_id)
240
- childrenMap.set(dep.from_id, children)
241
- parentMap.set(dep.to_id, dep.from_id)
237
+ if (parentLevel > childLevel && !parentMap.has(dep.toId)) {
238
+ const children = childrenMap.get(dep.fromId) || []
239
+ children.push(dep.toId)
240
+ childrenMap.set(dep.fromId, children)
241
+ parentMap.set(dep.toId, dep.fromId)
242
242
  }
243
243
  }
244
244
  }
@@ -257,8 +257,8 @@ export function GanttChart({
257
257
  const parsedRange = parseDateRangeFromTitle(item.title, currentYear)
258
258
  if (parsedRange) { dates.set(item.id, parsedRange); continue }
259
259
 
260
- const startStr = item.start_date
261
- const endStr = item.end_date
260
+ const startStr = item.startDate
261
+ const endStr = item.endDate
262
262
 
263
263
  if (startStr && endStr) {
264
264
  const s = new Date(startStr)
@@ -271,8 +271,8 @@ export function GanttChart({
271
271
  if (endStr) {
272
272
  const e = new Date(endStr)
273
273
  if (!isNaN(e.getTime())) {
274
- const s = item.start_date ? new Date(item.start_date) :
275
- item.created_at ? new Date(item.created_at) : addDays(e, -14)
274
+ const s = item.startDate ? new Date(item.startDate) :
275
+ item.createdAt ? new Date(item.createdAt) : addDays(e, -14)
276
276
  dates.set(item.id, { start: startOfDay(s), end: endOfDay(e) }); continue
277
277
  }
278
278
  }
@@ -284,7 +284,7 @@ export function GanttChart({
284
284
  }
285
285
  }
286
286
 
287
- const fallbackStart = item.created_at ? new Date(item.created_at) : now
287
+ const fallbackStart = item.createdAt ? new Date(item.createdAt) : now
288
288
  dates.set(item.id, { start: startOfDay(fallbackStart), end: endOfDay(addDays(fallbackStart, 14)) })
289
289
  }
290
290
 
@@ -546,15 +546,15 @@ export function GanttChart({
546
546
  tasks.forEach((task, index) => taskIndexMap.set(task.item.id, index))
547
547
 
548
548
  for (const dep of dependencies) {
549
- const fromIdx = taskIndexMap.get(dep.from_id)
550
- const toIdx = taskIndexMap.get(dep.to_id)
549
+ const fromIdx = taskIndexMap.get(dep.fromId)
550
+ const toIdx = taskIndexMap.get(dep.toId)
551
551
  if (fromIdx !== undefined && toIdx !== undefined) {
552
552
  const fromPos = getBarPosition(tasks[fromIdx], fromIdx)
553
553
  const toPos = getBarPosition(tasks[toIdx], toIdx)
554
554
  const x1 = fromPos.left + fromPos.width, y1 = fromPos.top + fromPos.height / 2
555
555
  const x2 = toPos.left, y2 = toPos.top + toPos.height / 2
556
556
  const midX = (x1 + x2) / 2
557
- paths.push({ path: `M ${x1} ${y1} C ${midX} ${y1}, ${midX} ${y2}, ${x2} ${y2}`, fromId: dep.from_id, toId: dep.to_id })
557
+ paths.push({ path: `M ${x1} ${y1} C ${midX} ${y1}, ${midX} ${y2}, ${x2} ${y2}`, fromId: dep.fromId, toId: dep.toId })
558
558
  }
559
559
  }
560
560
  return paths
@@ -856,8 +856,8 @@ export function GanttChart({
856
856
  {item.category}
857
857
  </span>
858
858
  )}
859
- {item.end_date && (
860
- <span className="gantt-board-card-date">{format(new Date(item.end_date), 'MMM d')}</span>
859
+ {item.endDate && (
860
+ <span className="gantt-board-card-date">{format(new Date(item.endDate), 'MMM d')}</span>
861
861
  )}
862
862
  </div>
863
863
  ))}
@@ -969,11 +969,11 @@ export function GanttChart({
969
969
  <input
970
970
  type="date"
971
971
  className="gantt-detail-input"
972
- value={detailItem.start_date ? format(new Date(detailItem.start_date), 'yyyy-MM-dd') : ''}
972
+ value={detailItem.startDate ? format(new Date(detailItem.startDate), 'yyyy-MM-dd') : ''}
973
973
  onChange={(e) => {
974
- const updated = { ...detailItem, start_date: e.target.value || null }
974
+ const updated = { ...detailItem, startDate: e.target.value || null }
975
975
  setDetailItem(updated)
976
- onItemEdit(detailItem.id, { start_date: e.target.value || null })
976
+ onItemEdit(detailItem.id, { startDate: e.target.value || null })
977
977
  }}
978
978
  />
979
979
  </div>
@@ -982,11 +982,11 @@ export function GanttChart({
982
982
  <input
983
983
  type="date"
984
984
  className="gantt-detail-input"
985
- value={detailItem.end_date ? format(new Date(detailItem.end_date), 'yyyy-MM-dd') : ''}
985
+ value={detailItem.endDate ? format(new Date(detailItem.endDate), 'yyyy-MM-dd') : ''}
986
986
  onChange={(e) => {
987
- const updated = { ...detailItem, end_date: e.target.value || null }
987
+ const updated = { ...detailItem, endDate: e.target.value || null }
988
988
  setDetailItem(updated)
989
- onItemEdit(detailItem.id, { end_date: e.target.value || null })
989
+ onItemEdit(detailItem.id, { endDate: e.target.value || null })
990
990
  }}
991
991
  />
992
992
  </div>
@@ -9,9 +9,9 @@ export interface TimelineItem {
9
9
  status: string
10
10
  category?: string
11
11
  progress?: number // 0-100
12
- start_date?: string | null
13
- end_date?: string | null
14
- created_at?: string
12
+ startDate?: string | null
13
+ endDate?: string | null
14
+ createdAt?: string
15
15
  children?: TimelineItem[]
16
16
  }
17
17
 
@@ -19,8 +19,8 @@ export interface TimelineItem {
19
19
  * Dependency between two timeline items.
20
20
  */
21
21
  export interface TimelineDependency {
22
- from_id: string
23
- to_id: string
22
+ fromId: string
23
+ toId: string
24
24
  type?: string
25
25
  }
26
26
 
@@ -76,3 +76,49 @@ export * from './wizard'
76
76
 
77
77
  // Account (profile + password forms)
78
78
  export * from './account'
79
+
80
+ // Compose flow (auto-save, header, status, confirmation dialog)
81
+ export * from './compose'
82
+
83
+ // Email Editor (block-based email builder with drag-drop, undo/redo, HTML renderer)
84
+ export * from './email-editor'
85
+
86
+ // Email dialogs (schedule, test send, preview, template picker, merge fields)
87
+ export * from './email-dialogs'
88
+
89
+ // Dashboard components (MetricCard, PeriodSelector, SparklineTrend, DashboardGrid, DashboardSection)
90
+ export * from './dashboard'
91
+
92
+ // Enrichment components (QualityBadge, EnrichButton, EnrichmentProgress)
93
+ export * from './enrichment'
94
+
95
+ // Integrations (IntegrationCard, ConnectionStatus)
96
+ export * from './integrations'
97
+
98
+ // Command Palette
99
+ export * from './command-palette'
100
+
101
+ // Settings components (SettingsLayout, SettingsNav, SettingsCard)
102
+ export * from './settings'
103
+
104
+ // Kanban board layout
105
+ export * from './kanban'
106
+
107
+ // List components (ListCard, CreateListDialog)
108
+ export * from './lists'
109
+
110
+ // Pipeline components (StageTransitionModal)
111
+ export * from './pipeline'
112
+
113
+ // Activity components (timeline, quick log, log dialog)
114
+ export { ActivityTimeline } from './ActivityTimeline'
115
+ export type { ActivityTimelineProps, ActivityTimelineItem } from './ActivityTimeline'
116
+ export { QuickLogButtons } from './QuickLogButtons'
117
+ export type { QuickLogButtonsProps, QuickLogAction } from './QuickLogButtons'
118
+ export { LogActivityDialog } from './LogActivityDialog'
119
+ export type {
120
+ LogActivityDialogProps,
121
+ LogActivityFormData,
122
+ ActivityTypeOption,
123
+ OutcomeOption,
124
+ } from './LogActivityDialog'
@@ -0,0 +1,77 @@
1
+ 'use client'
2
+
3
+ import * as React from 'react'
4
+ import { Loader2 } from 'lucide-react'
5
+ import { cn } from '../../lib/utils'
6
+
7
+ export interface ConnectionStatusProps {
8
+ providerName: string
9
+ providerIcon: React.ElementType
10
+ connected: boolean
11
+ accountLabel?: string
12
+ onConnect?: () => void
13
+ onDisconnect?: () => void
14
+ isLoading?: boolean
15
+ className?: string
16
+ }
17
+
18
+ export function ConnectionStatus({
19
+ providerName,
20
+ providerIcon: Icon,
21
+ connected,
22
+ accountLabel,
23
+ onConnect,
24
+ onDisconnect,
25
+ isLoading = false,
26
+ className,
27
+ }: ConnectionStatusProps) {
28
+ return (
29
+ <div
30
+ className={cn(
31
+ 'flex items-center justify-between rounded-lg border p-4',
32
+ className
33
+ )}
34
+ >
35
+ <div className="flex items-center gap-3">
36
+ <div className="flex-shrink-0 w-9 h-9 rounded-lg bg-gray-100 flex items-center justify-center">
37
+ <Icon className="w-5 h-5 text-gray-600" />
38
+ </div>
39
+ <div>
40
+ <p className="text-sm font-medium">{providerName}</p>
41
+ {connected && accountLabel && (
42
+ <p className="text-xs text-muted-foreground">{accountLabel}</p>
43
+ )}
44
+ {!connected && (
45
+ <p className="text-xs text-muted-foreground">Not connected</p>
46
+ )}
47
+ </div>
48
+ </div>
49
+
50
+ <div>
51
+ {isLoading ? (
52
+ <button
53
+ disabled
54
+ className="inline-flex items-center gap-1.5 rounded-md px-3 py-1.5 text-xs font-medium bg-gray-100 text-gray-400 cursor-not-allowed"
55
+ >
56
+ <Loader2 className="w-3 h-3 animate-spin" />
57
+ Loading...
58
+ </button>
59
+ ) : connected ? (
60
+ <button
61
+ onClick={onDisconnect}
62
+ className="inline-flex items-center rounded-md px-3 py-1.5 text-xs font-medium border border-red-200 text-red-600 hover:bg-red-50 transition-colors"
63
+ >
64
+ Disconnect
65
+ </button>
66
+ ) : (
67
+ <button
68
+ onClick={onConnect}
69
+ className="inline-flex items-center rounded-md px-3 py-1.5 text-xs font-medium bg-primary text-primary-foreground hover:bg-primary/90 transition-colors"
70
+ >
71
+ Connect
72
+ </button>
73
+ )}
74
+ </div>
75
+ </div>
76
+ )
77
+ }
@@ -0,0 +1,92 @@
1
+ 'use client'
2
+
3
+ import * as React from 'react'
4
+ import { ChevronRight } from 'lucide-react'
5
+ import { cn } from '../../lib/utils'
6
+
7
+ export type IntegrationStatus = 'connected' | 'available' | 'coming-soon'
8
+
9
+ export interface IntegrationCardProps {
10
+ name: string
11
+ description: string
12
+ icon: React.ElementType
13
+ status: IntegrationStatus
14
+ onClick?: () => void
15
+ lastSync?: string
16
+ className?: string
17
+ }
18
+
19
+ const STATUS_CONFIG: Record<IntegrationStatus, { label: string; className: string }> = {
20
+ connected: { label: 'Connected', className: 'bg-green-100 text-green-700' },
21
+ available: { label: 'Available', className: 'bg-blue-100 text-blue-700' },
22
+ 'coming-soon': { label: 'Coming Soon', className: 'bg-gray-100 text-gray-500' },
23
+ }
24
+
25
+ export function IntegrationCard({
26
+ name,
27
+ description,
28
+ icon: Icon,
29
+ status,
30
+ onClick,
31
+ lastSync,
32
+ className,
33
+ }: IntegrationCardProps) {
34
+ const isClickable = status !== 'coming-soon'
35
+ const statusEntry = STATUS_CONFIG[status]
36
+
37
+ return (
38
+ <div
39
+ className={cn(
40
+ 'rounded-xl border bg-card text-card-foreground shadow p-5 flex items-start gap-4',
41
+ isClickable && 'cursor-pointer hover:shadow-md hover:border-primary/30 transition-shadow',
42
+ !isClickable && 'opacity-75',
43
+ className
44
+ )}
45
+ onClick={isClickable ? onClick : undefined}
46
+ role={isClickable ? 'button' : undefined}
47
+ tabIndex={isClickable ? 0 : undefined}
48
+ onKeyDown={
49
+ isClickable
50
+ ? (e: React.KeyboardEvent) => {
51
+ if (e.key === 'Enter' || e.key === ' ') {
52
+ e.preventDefault()
53
+ onClick?.()
54
+ }
55
+ }
56
+ : undefined
57
+ }
58
+ >
59
+ <div className="flex-shrink-0 w-10 h-10 rounded-lg bg-gray-100 flex items-center justify-center">
60
+ <Icon className="w-5 h-5 text-gray-600" />
61
+ </div>
62
+
63
+ <div className="flex-1 min-w-0">
64
+ <div className="flex items-center gap-2 mb-1">
65
+ <h3 className="font-medium text-sm truncate">{name}</h3>
66
+ <span
67
+ className={cn(
68
+ 'inline-flex items-center rounded-full px-2 py-0.5 text-xs font-medium',
69
+ statusEntry.className
70
+ )}
71
+ >
72
+ {statusEntry.label}
73
+ </span>
74
+ </div>
75
+
76
+ <p className="text-xs text-muted-foreground line-clamp-2">{description}</p>
77
+
78
+ {lastSync && (
79
+ <p className="text-xs text-muted-foreground mt-1">
80
+ Last synced: {lastSync}
81
+ </p>
82
+ )}
83
+
84
+ {isClickable && (
85
+ <span className="inline-flex items-center text-xs text-primary mt-2 font-medium">
86
+ Configure <ChevronRight className="w-3 h-3 ml-0.5" />
87
+ </span>
88
+ )}
89
+ </div>
90
+ </div>
91
+ )
92
+ }
@@ -0,0 +1,5 @@
1
+ export { IntegrationCard } from './IntegrationCard'
2
+ export type { IntegrationCardProps, IntegrationStatus } from './IntegrationCard'
3
+
4
+ export { ConnectionStatus } from './ConnectionStatus'
5
+ export type { ConnectionStatusProps } from './ConnectionStatus'
@@ -0,0 +1,103 @@
1
+ 'use client'
2
+
3
+ import * as React from 'react'
4
+ import { cn } from '../../lib/utils'
5
+
6
+ export interface KanbanColumnConfig {
7
+ id: string
8
+ label: string
9
+ color?: string
10
+ }
11
+
12
+ export interface KanbanBoardProps<T> {
13
+ columns: KanbanColumnConfig[]
14
+ items: Record<string, T[]>
15
+ renderCard: (item: T) => React.ReactNode
16
+ renderColumnHeader?: (column: KanbanColumnConfig, items: T[]) => React.ReactNode
17
+ renderColumnFooter?: (column: KanbanColumnConfig) => React.ReactNode
18
+ emptyColumnMessage?: string
19
+ columnWidth?: number
20
+ columnRef?: (columnId: string) => React.Ref<HTMLDivElement> | undefined
21
+ isColumnOver?: (columnId: string) => boolean
22
+ className?: string
23
+ }
24
+
25
+ export function KanbanBoard<T>({
26
+ columns,
27
+ items,
28
+ renderCard,
29
+ renderColumnHeader,
30
+ renderColumnFooter,
31
+ emptyColumnMessage = 'No items',
32
+ columnWidth = 320,
33
+ columnRef,
34
+ isColumnOver,
35
+ className,
36
+ }: KanbanBoardProps<T>) {
37
+ return (
38
+ <div className={cn('flex gap-4 overflow-x-auto pb-4', className)}>
39
+ {columns.map((column) => {
40
+ const columnItems = items[column.id] ?? []
41
+ const ref = columnRef?.(column.id)
42
+ const isOver = isColumnOver?.(column.id) ?? false
43
+
44
+ return (
45
+ <div
46
+ key={column.id}
47
+ ref={ref}
48
+ style={{ minWidth: columnWidth, maxWidth: columnWidth }}
49
+ className={cn(
50
+ 'flex flex-col rounded-lg border bg-muted/50 transition-colors',
51
+ isOver && 'border-primary ring-2 ring-primary/20',
52
+ )}
53
+ >
54
+ {/* Column Header */}
55
+ {renderColumnHeader ? (
56
+ renderColumnHeader(column, columnItems)
57
+ ) : (
58
+ <div className="flex items-center justify-between px-3 py-2 border-b">
59
+ <div className="flex items-center gap-2">
60
+ {column.color && (
61
+ <span
62
+ className="w-2.5 h-2.5 rounded-full shrink-0"
63
+ style={{ backgroundColor: column.color }}
64
+ aria-hidden="true"
65
+ />
66
+ )}
67
+ <h3 className="text-sm font-semibold text-foreground truncate">
68
+ {column.label}
69
+ </h3>
70
+ </div>
71
+ <span className="inline-flex items-center justify-center rounded-full bg-muted px-2 py-0.5 text-xs font-medium text-muted-foreground">
72
+ {columnItems.length}
73
+ </span>
74
+ </div>
75
+ )}
76
+
77
+ {/* Column Body */}
78
+ <div className="flex-1 overflow-y-auto p-2 space-y-2">
79
+ {columnItems.length === 0 ? (
80
+ <p className="text-xs text-muted-foreground text-center py-8">
81
+ {emptyColumnMessage}
82
+ </p>
83
+ ) : (
84
+ columnItems.map((item, index) => (
85
+ <React.Fragment key={index}>
86
+ {renderCard(item)}
87
+ </React.Fragment>
88
+ ))
89
+ )}
90
+ </div>
91
+
92
+ {/* Column Footer */}
93
+ {renderColumnFooter && (
94
+ <div className="border-t px-3 py-2">
95
+ {renderColumnFooter(column)}
96
+ </div>
97
+ )}
98
+ </div>
99
+ )
100
+ })}
101
+ </div>
102
+ )
103
+ }
@@ -0,0 +1,2 @@
1
+ export { KanbanBoard } from './KanbanBoard'
2
+ export type { KanbanBoardProps, KanbanColumnConfig } from './KanbanBoard'