@startsimpli/ui 0.4.7 → 0.4.8

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 (61) hide show
  1. package/package.json +1 -1
  2. package/src/__mocks__/next/link.js +11 -0
  3. package/src/components/account/__tests__/account.test.tsx +315 -0
  4. package/src/components/command-palette/CommandGroup.tsx +23 -0
  5. package/src/components/command-palette/CommandPalette.tsx +183 -200
  6. package/src/components/command-palette/CommandResultItem.tsx +59 -0
  7. package/src/components/command-palette/__tests__/CommandGroup.test.tsx +81 -0
  8. package/src/components/command-palette/__tests__/CommandResultItem.test.tsx +166 -0
  9. package/src/components/command-palette/__tests__/command-palette-context.test.tsx +166 -0
  10. package/src/components/command-palette/__tests__/useCommandPaletteSearch.test.ts +271 -0
  11. package/src/components/command-palette/index.ts +6 -0
  12. package/src/components/command-palette/useCommandPaletteSearch.ts +114 -0
  13. package/src/components/compose/__tests__/compose.test.tsx +656 -0
  14. package/src/components/dashboard/PipelineFunnel.tsx +126 -0
  15. package/src/components/dashboard/TopCampaigns.tsx +132 -0
  16. package/src/components/dashboard/__tests__/dashboard.test.tsx +785 -0
  17. package/src/components/dashboard/index.ts +6 -0
  18. package/src/components/dialog/ConfirmDialog.tsx +72 -0
  19. package/src/components/dialog/__tests__/ConfirmDialog.test.tsx +126 -0
  20. package/src/components/dialog/index.ts +3 -0
  21. package/src/components/email-dialogs/__tests__/email-dialogs.test.tsx +982 -0
  22. package/src/components/email-editor/BlockRenderer.tsx +120 -0
  23. package/src/components/email-editor/__tests__/BlockRenderer.test.tsx +332 -0
  24. package/src/components/email-editor/__tests__/block-renderers.test.ts +624 -0
  25. package/src/components/email-editor/__tests__/email-html-renderer.test.ts +376 -0
  26. package/src/components/email-editor/blocks/__tests__/blocks.test.tsx +818 -0
  27. package/src/components/email-editor/editor-sidebar.tsx +6 -731
  28. package/src/components/email-editor/email-editor.tsx +78 -467
  29. package/src/components/email-editor/hooks/__tests__/useDragDrop.test.ts +355 -0
  30. package/src/components/email-editor/hooks/__tests__/useEmailEditorState.test.ts +551 -0
  31. package/src/components/email-editor/hooks/useDragDrop.ts +181 -0
  32. package/src/components/email-editor/hooks/useEmailEditorState.ts +426 -0
  33. package/src/components/email-editor/index.ts +1 -0
  34. package/src/components/email-editor/panels/BlockPropertyPanel.tsx +637 -0
  35. package/src/components/email-editor/panels/GlobalStylesPanel.tsx +108 -0
  36. package/src/components/email-editor/panels/SectionSettingsPanel.tsx +80 -0
  37. package/src/components/email-editor/panels/__tests__/BlockPropertyPanel.test.tsx +707 -0
  38. package/src/components/email-editor/panels/__tests__/GlobalStylesPanel.test.tsx +226 -0
  39. package/src/components/email-editor/panels/index.ts +3 -0
  40. package/src/components/enrichment/__tests__/enrichment.test.tsx +184 -0
  41. package/src/components/gantt/GanttBoardView.tsx +71 -0
  42. package/src/components/gantt/GanttChart.tsx +134 -881
  43. package/src/components/gantt/GanttFilterBar.tsx +100 -0
  44. package/src/components/gantt/GanttListView.tsx +63 -0
  45. package/src/components/gantt/GanttTimelineView.tsx +215 -0
  46. package/src/components/gantt/__tests__/GanttBoardView.test.tsx +305 -0
  47. package/src/components/gantt/__tests__/GanttFilterBar.test.tsx +544 -0
  48. package/src/components/gantt/__tests__/GanttListView.test.tsx +337 -0
  49. package/src/components/gantt/__tests__/GanttTimelineView.test.tsx +375 -0
  50. package/src/components/gantt/__tests__/gantt-utils.test.ts +341 -0
  51. package/src/components/gantt/__tests__/useGanttState.test.ts +535 -0
  52. package/src/components/gantt/hooks/useGanttState.ts +644 -0
  53. package/src/components/gantt/index.ts +10 -0
  54. package/src/components/integrations/__tests__/integrations.test.tsx +191 -0
  55. package/src/components/kanban/__tests__/kanban.test.tsx +157 -0
  56. package/src/components/lists/__tests__/lists.test.tsx +263 -0
  57. package/src/components/loading/__tests__/loading.test.tsx +114 -0
  58. package/src/components/navigation/__tests__/navigation.test.tsx +194 -0
  59. package/src/components/pipeline/__tests__/pipeline.test.tsx +169 -0
  60. package/src/components/settings/__tests__/settings.test.tsx +181 -0
  61. package/src/components/wizard/__tests__/wizard.test.tsx +97 -0
@@ -0,0 +1,226 @@
1
+ import { render, screen, fireEvent } from '@testing-library/react'
2
+ import { GlobalStylesPanel } from '../GlobalStylesPanel'
3
+ import type { GlobalStyles } from '../../types'
4
+ import { DEFAULT_GLOBAL_STYLES } from '../../types'
5
+
6
+ // ---------------------------------------------------------------------------
7
+ // Fixtures
8
+ // ---------------------------------------------------------------------------
9
+
10
+ const baseStyles: GlobalStyles = {
11
+ backgroundColor: '#f3f4f6',
12
+ contentWidth: 600,
13
+ fontFamily: '-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif',
14
+ theme: 'clean',
15
+ }
16
+
17
+ function renderPanel(styles: GlobalStyles = baseStyles, onChange = jest.fn()) {
18
+ return render(<GlobalStylesPanel globalStyles={styles} onChange={onChange} />)
19
+ }
20
+
21
+ // ---------------------------------------------------------------------------
22
+ // Renders style inputs
23
+ // ---------------------------------------------------------------------------
24
+
25
+ describe('GlobalStylesPanel — renders style inputs', () => {
26
+ it('renders the Background Color label', () => {
27
+ renderPanel()
28
+ expect(screen.getByText('Background Color')).toBeInTheDocument()
29
+ })
30
+
31
+ it('renders the background color text input with current value', () => {
32
+ renderPanel()
33
+ const inputs = screen.getAllByDisplayValue('#f3f4f6')
34
+ // At least one text input shows the background color value
35
+ expect(inputs.length).toBeGreaterThan(0)
36
+ })
37
+
38
+ it('renders the Content Width input', () => {
39
+ renderPanel()
40
+ expect(screen.getByText('Content Width (px)')).toBeInTheDocument()
41
+ })
42
+
43
+ it('renders content width number input with current value', () => {
44
+ renderPanel()
45
+ expect(screen.getByDisplayValue('600')).toBeInTheDocument()
46
+ })
47
+
48
+ it('renders the Font Family label', () => {
49
+ renderPanel()
50
+ expect(screen.getByText('Font Family')).toBeInTheDocument()
51
+ })
52
+
53
+ it('renders the Themes section heading', () => {
54
+ renderPanel()
55
+ expect(screen.getByText('Themes')).toBeInTheDocument()
56
+ })
57
+
58
+ it('renders Clean theme preset button', () => {
59
+ renderPanel()
60
+ expect(screen.getByText('Clean')).toBeInTheDocument()
61
+ })
62
+
63
+ it('renders Minimal theme preset button', () => {
64
+ renderPanel()
65
+ expect(screen.getByText('Minimal')).toBeInTheDocument()
66
+ })
67
+
68
+ it('renders Bold theme preset button', () => {
69
+ renderPanel()
70
+ expect(screen.getByText('Bold')).toBeInTheDocument()
71
+ })
72
+
73
+ it('renders theme description text for each preset', () => {
74
+ renderPanel()
75
+ expect(screen.getByText('White background with subtle gray accents')).toBeInTheDocument()
76
+ expect(screen.getByText('Pure white with thin borders')).toBeInTheDocument()
77
+ expect(screen.getByText('Dark background with vivid accents')).toBeInTheDocument()
78
+ })
79
+
80
+ it('renders color picker input for background color', () => {
81
+ const { container } = renderPanel()
82
+ const colorInput = container.querySelector('input[type="color"]') as HTMLInputElement
83
+ expect(colorInput).toBeInTheDocument()
84
+ expect(colorInput.value).toBe('#f3f4f6')
85
+ })
86
+
87
+ it('renders Email Design section heading', () => {
88
+ renderPanel()
89
+ expect(screen.getByText('Email Design')).toBeInTheDocument()
90
+ })
91
+ })
92
+
93
+ // ---------------------------------------------------------------------------
94
+ // onChange fires with updated styles
95
+ // ---------------------------------------------------------------------------
96
+
97
+ describe('GlobalStylesPanel — onChange fires with updated styles', () => {
98
+ it('calls onChange when background color text input changes', () => {
99
+ const onChange = jest.fn()
100
+ renderPanel(baseStyles, onChange)
101
+ const inputs = screen.getAllByDisplayValue('#f3f4f6')
102
+ // Find the text input (not color picker) — the Input component has type="text" by default
103
+ const textInput = inputs.find(
104
+ (el) => (el as HTMLInputElement).type !== 'color'
105
+ ) as HTMLInputElement
106
+ fireEvent.change(textInput, { target: { value: '#ffffff' } })
107
+ expect(onChange).toHaveBeenCalledTimes(1)
108
+ expect(onChange).toHaveBeenCalledWith(
109
+ expect.objectContaining({ backgroundColor: '#ffffff' })
110
+ )
111
+ })
112
+
113
+ it('calls onChange when color picker changes', () => {
114
+ const onChange = jest.fn()
115
+ const { container } = renderPanel(baseStyles, onChange)
116
+ const colorInput = container.querySelector('input[type="color"]') as HTMLInputElement
117
+ fireEvent.change(colorInput, { target: { value: '#123456' } })
118
+ expect(onChange).toHaveBeenCalledTimes(1)
119
+ expect(onChange).toHaveBeenCalledWith(
120
+ expect.objectContaining({ backgroundColor: '#123456' })
121
+ )
122
+ })
123
+
124
+ it('calls onChange with parsed integer when content width changes', () => {
125
+ const onChange = jest.fn()
126
+ renderPanel(baseStyles, onChange)
127
+ const widthInput = screen.getByDisplayValue('600')
128
+ fireEvent.change(widthInput, { target: { value: '700' } })
129
+ expect(onChange).toHaveBeenCalledTimes(1)
130
+ expect(onChange).toHaveBeenCalledWith(
131
+ expect.objectContaining({ contentWidth: 700 })
132
+ )
133
+ })
134
+
135
+ it('defaults contentWidth to 600 when invalid value entered', () => {
136
+ const onChange = jest.fn()
137
+ renderPanel(baseStyles, onChange)
138
+ const widthInput = screen.getByDisplayValue('600')
139
+ fireEvent.change(widthInput, { target: { value: 'abc' } })
140
+ expect(onChange).toHaveBeenCalledWith(
141
+ expect.objectContaining({ contentWidth: 600 })
142
+ )
143
+ })
144
+
145
+ it('calls onChange with Clean theme preset globalStyles on Clean button click', () => {
146
+ const onChange = jest.fn()
147
+ renderPanel(baseStyles, onChange)
148
+ fireEvent.click(screen.getByText('Clean'))
149
+ expect(onChange).toHaveBeenCalledTimes(1)
150
+ const called = onChange.mock.calls[0][0] as GlobalStyles
151
+ expect(called.theme).toBe('clean')
152
+ })
153
+
154
+ it('calls onChange with Minimal theme preset globalStyles on Minimal button click', () => {
155
+ const onChange = jest.fn()
156
+ renderPanel(baseStyles, onChange)
157
+ fireEvent.click(screen.getByText('Minimal'))
158
+ expect(onChange).toHaveBeenCalledTimes(1)
159
+ const called = onChange.mock.calls[0][0] as GlobalStyles
160
+ expect(called.theme).toBe('minimal')
161
+ expect(called.backgroundColor).toBe('#ffffff')
162
+ })
163
+
164
+ it('calls onChange with Bold theme preset globalStyles on Bold button click', () => {
165
+ const onChange = jest.fn()
166
+ renderPanel(baseStyles, onChange)
167
+ fireEvent.click(screen.getByText('Bold'))
168
+ expect(onChange).toHaveBeenCalledTimes(1)
169
+ const called = onChange.mock.calls[0][0] as GlobalStyles
170
+ expect(called.theme).toBe('bold')
171
+ expect(called.backgroundColor).toBe('#1f2937')
172
+ })
173
+
174
+ it('preserves all other globalStyles fields when only backgroundColor changes', () => {
175
+ const onChange = jest.fn()
176
+ renderPanel(baseStyles, onChange)
177
+ const inputs = screen.getAllByDisplayValue('#f3f4f6')
178
+ const textInput = inputs.find(
179
+ (el) => (el as HTMLInputElement).type !== 'color'
180
+ ) as HTMLInputElement
181
+ fireEvent.change(textInput, { target: { value: '#aaaaaa' } })
182
+ const result = onChange.mock.calls[0][0] as GlobalStyles
183
+ expect(result.contentWidth).toBe(baseStyles.contentWidth)
184
+ expect(result.fontFamily).toBe(baseStyles.fontFamily)
185
+ expect(result.theme).toBe(baseStyles.theme)
186
+ })
187
+ })
188
+
189
+ // ---------------------------------------------------------------------------
190
+ // Active theme highlighting
191
+ // ---------------------------------------------------------------------------
192
+
193
+ describe('GlobalStylesPanel — active theme highlight', () => {
194
+ it('applies active border class to the currently selected theme button', () => {
195
+ const { container } = renderPanel({ ...baseStyles, theme: 'clean' })
196
+ const cleanButton = screen.getByText('Clean').closest('button') as HTMLElement
197
+ expect(cleanButton.className).toContain('border-primary')
198
+ })
199
+
200
+ it('does not apply active border class to non-selected theme buttons', () => {
201
+ const { container } = renderPanel({ ...baseStyles, theme: 'clean' })
202
+ const minimalButton = screen.getByText('Minimal').closest('button') as HTMLElement
203
+ expect(minimalButton.className).not.toContain('border-primary')
204
+ })
205
+
206
+ it('applies active class to Minimal when minimal theme is selected', () => {
207
+ renderPanel({ ...baseStyles, theme: 'minimal' })
208
+ const minimalButton = screen.getByText('Minimal').closest('button') as HTMLElement
209
+ expect(minimalButton.className).toContain('border-primary')
210
+ })
211
+ })
212
+
213
+ // ---------------------------------------------------------------------------
214
+ // DEFAULT_GLOBAL_STYLES as initial values
215
+ // ---------------------------------------------------------------------------
216
+
217
+ describe('GlobalStylesPanel — DEFAULT_GLOBAL_STYLES compatibility', () => {
218
+ it('renders correctly with DEFAULT_GLOBAL_STYLES', () => {
219
+ expect(() => renderPanel(DEFAULT_GLOBAL_STYLES)).not.toThrow()
220
+ })
221
+
222
+ it('shows default content width 600', () => {
223
+ renderPanel(DEFAULT_GLOBAL_STYLES)
224
+ expect(screen.getByDisplayValue('600')).toBeInTheDocument()
225
+ })
226
+ })
@@ -0,0 +1,3 @@
1
+ export { GlobalStylesPanel } from './GlobalStylesPanel'
2
+ export { BlockPropertyPanel } from './BlockPropertyPanel'
3
+ export { SectionSettingsPanel } from './SectionSettingsPanel'
@@ -0,0 +1,184 @@
1
+ import { render, screen, fireEvent } from '@testing-library/react'
2
+ import { QualityBadge } from '../QualityBadge'
3
+ import { EnrichButton } from '../EnrichButton'
4
+ import { EnrichmentProgress } from '../EnrichmentProgress'
5
+
6
+ // ---------------------------------------------------------------------------
7
+ // QualityBadge
8
+ // ---------------------------------------------------------------------------
9
+
10
+ describe('QualityBadge', () => {
11
+ it('renders grade A with correct label in title', () => {
12
+ render(<QualityBadge quality={{ grade: 'A', score: 95 }} />)
13
+ const badge = screen.getByTitle('Data quality: Excellent (95/100)')
14
+ expect(badge).toBeInTheDocument()
15
+ expect(badge).toHaveTextContent('A')
16
+ })
17
+
18
+ it('renders grade B with correct title', () => {
19
+ render(<QualityBadge quality={{ grade: 'B', score: 80 }} />)
20
+ expect(screen.getByTitle('Data quality: Good (80/100)')).toBeInTheDocument()
21
+ })
22
+
23
+ it('renders grade C with correct title', () => {
24
+ render(<QualityBadge quality={{ grade: 'C', score: 65 }} />)
25
+ expect(screen.getByTitle('Data quality: Fair (65/100)')).toBeInTheDocument()
26
+ })
27
+
28
+ it('renders grade D with correct title', () => {
29
+ render(<QualityBadge quality={{ grade: 'D', score: 45 }} />)
30
+ expect(screen.getByTitle('Data quality: Poor (45/100)')).toBeInTheDocument()
31
+ })
32
+
33
+ it('renders grade F with correct title', () => {
34
+ render(<QualityBadge quality={{ grade: 'F', score: 20 }} />)
35
+ expect(screen.getByTitle('Data quality: Incomplete (20/100)')).toBeInTheDocument()
36
+ })
37
+
38
+ it('does not show score by default', () => {
39
+ render(<QualityBadge quality={{ grade: 'A', score: 95 }} />)
40
+ expect(screen.queryByText('(95)')).not.toBeInTheDocument()
41
+ })
42
+
43
+ it('shows score when showScore is true', () => {
44
+ render(<QualityBadge quality={{ grade: 'A', score: 95 }} showScore />)
45
+ expect(screen.getByText('(95)')).toBeInTheDocument()
46
+ })
47
+
48
+ it('applies green styles for grade A', () => {
49
+ render(<QualityBadge quality={{ grade: 'A', score: 95 }} />)
50
+ const badge = screen.getByTitle('Data quality: Excellent (95/100)')
51
+ expect(badge).toHaveClass('bg-green-100')
52
+ expect(badge).toHaveClass('text-green-800')
53
+ })
54
+
55
+ it('applies red styles for grade F', () => {
56
+ render(<QualityBadge quality={{ grade: 'F', score: 20 }} />)
57
+ const badge = screen.getByTitle('Data quality: Incomplete (20/100)')
58
+ expect(badge).toHaveClass('bg-red-100')
59
+ expect(badge).toHaveClass('text-red-800')
60
+ })
61
+
62
+ it('applies yellow styles for grade C', () => {
63
+ render(<QualityBadge quality={{ grade: 'C', score: 65 }} />)
64
+ const badge = screen.getByTitle('Data quality: Fair (65/100)')
65
+ expect(badge).toHaveClass('bg-yellow-100')
66
+ })
67
+ })
68
+
69
+ // ---------------------------------------------------------------------------
70
+ // EnrichButton
71
+ // ---------------------------------------------------------------------------
72
+
73
+ describe('EnrichButton', () => {
74
+ it('renders default label', () => {
75
+ render(<EnrichButton onClick={jest.fn()} />)
76
+ expect(screen.getByRole('button', { name: /enrich/i })).toBeInTheDocument()
77
+ })
78
+
79
+ it('renders custom label', () => {
80
+ render(<EnrichButton onClick={jest.fn()} label="Run Enrichment" />)
81
+ expect(screen.getByRole('button', { name: /run enrichment/i })).toBeInTheDocument()
82
+ })
83
+
84
+ it('shows Enriching... when isLoading is true', () => {
85
+ render(<EnrichButton onClick={jest.fn()} isLoading />)
86
+ expect(screen.getByText('Enriching...')).toBeInTheDocument()
87
+ })
88
+
89
+ it('is disabled when isLoading is true', () => {
90
+ render(<EnrichButton onClick={jest.fn()} isLoading />)
91
+ expect(screen.getByRole('button')).toBeDisabled()
92
+ })
93
+
94
+ it('calls onClick when clicked', () => {
95
+ const onClick = jest.fn()
96
+ render(<EnrichButton onClick={onClick} />)
97
+ fireEvent.click(screen.getByRole('button'))
98
+ expect(onClick).toHaveBeenCalledTimes(1)
99
+ })
100
+
101
+ it('does not call onClick when disabled', () => {
102
+ const onClick = jest.fn()
103
+ render(<EnrichButton onClick={onClick} isLoading />)
104
+ fireEvent.click(screen.getByRole('button'))
105
+ expect(onClick).not.toHaveBeenCalled()
106
+ })
107
+
108
+ it('applies sm size classes by default', () => {
109
+ render(<EnrichButton onClick={jest.fn()} />)
110
+ expect(screen.getByRole('button')).toHaveClass('px-3', 'py-1.5', 'text-xs')
111
+ })
112
+
113
+ it('applies md size classes when size is md', () => {
114
+ render(<EnrichButton onClick={jest.fn()} size="md" />)
115
+ expect(screen.getByRole('button')).toHaveClass('px-4', 'py-2', 'text-sm')
116
+ })
117
+
118
+ it('applies custom className', () => {
119
+ render(<EnrichButton onClick={jest.fn()} className="my-custom-class" />)
120
+ expect(screen.getByRole('button')).toHaveClass('my-custom-class')
121
+ })
122
+ })
123
+
124
+ // ---------------------------------------------------------------------------
125
+ // EnrichmentProgress
126
+ // ---------------------------------------------------------------------------
127
+
128
+ describe('EnrichmentProgress', () => {
129
+ const queueStatus = {
130
+ pending: 3,
131
+ processing: 1,
132
+ completed: 12,
133
+ failed: 0,
134
+ }
135
+
136
+ it('renders unavailable message when queueStatus is null', () => {
137
+ render(<EnrichmentProgress queueStatus={null} />)
138
+ expect(screen.getByText('Queue status unavailable')).toBeInTheDocument()
139
+ })
140
+
141
+ it('renders all four status counts', () => {
142
+ render(<EnrichmentProgress queueStatus={queueStatus} />)
143
+ expect(screen.getByText('3')).toBeInTheDocument() // pending
144
+ expect(screen.getByText('1')).toBeInTheDocument() // processing
145
+ expect(screen.getByText('12')).toBeInTheDocument() // completed
146
+ expect(screen.getByText('0')).toBeInTheDocument() // failed
147
+ })
148
+
149
+ it('renders all four status labels', () => {
150
+ render(<EnrichmentProgress queueStatus={queueStatus} />)
151
+ expect(screen.getByText('Pending')).toBeInTheDocument()
152
+ expect(screen.getByText('Processing')).toBeInTheDocument()
153
+ expect(screen.getByText('Completed')).toBeInTheDocument()
154
+ expect(screen.getByText('Failed')).toBeInTheDocument()
155
+ })
156
+
157
+ it('does not render refresh button when onRefresh is not provided', () => {
158
+ render(<EnrichmentProgress queueStatus={queueStatus} />)
159
+ expect(screen.queryByText('Refresh')).not.toBeInTheDocument()
160
+ })
161
+
162
+ it('renders refresh button when onRefresh is provided', () => {
163
+ render(<EnrichmentProgress queueStatus={queueStatus} onRefresh={jest.fn()} />)
164
+ expect(screen.getByText('Refresh')).toBeInTheDocument()
165
+ })
166
+
167
+ it('calls onRefresh when refresh button is clicked', () => {
168
+ const onRefresh = jest.fn()
169
+ render(<EnrichmentProgress queueStatus={queueStatus} onRefresh={onRefresh} />)
170
+ fireEvent.click(screen.getByText('Refresh'))
171
+ expect(onRefresh).toHaveBeenCalledTimes(1)
172
+ })
173
+
174
+ it('disables refresh button when isLoading is true', () => {
175
+ render(
176
+ <EnrichmentProgress
177
+ queueStatus={queueStatus}
178
+ onRefresh={jest.fn()}
179
+ isLoading
180
+ />
181
+ )
182
+ expect(screen.getByText('Refresh').closest('button')).toBeDisabled()
183
+ })
184
+ })
@@ -0,0 +1,71 @@
1
+ 'use client'
2
+
3
+ import { format } from 'date-fns'
4
+ import type { TimelineItem } from './types'
5
+ import type { UseGanttStateReturn } from './hooks/useGanttState'
6
+
7
+ export interface GanttBoardViewProps {
8
+ boardColumns: UseGanttStateReturn['boardColumns']
9
+ boardDragItem: UseGanttStateReturn['boardDragItem']
10
+ setBoardDragItem: UseGanttStateReturn['setBoardDragItem']
11
+ onStatusChange: UseGanttStateReturn['onStatusChange']
12
+ categoryColors: UseGanttStateReturn['categoryColors']
13
+ handleItemClick: UseGanttStateReturn['handleItemClick']
14
+ }
15
+
16
+ export function GanttBoardView({
17
+ boardColumns,
18
+ boardDragItem,
19
+ setBoardDragItem,
20
+ onStatusChange,
21
+ categoryColors,
22
+ handleItemClick,
23
+ }: GanttBoardViewProps) {
24
+ return (
25
+ <div className="gantt-board">
26
+ {boardColumns.map((col) => (
27
+ <div
28
+ key={col.status}
29
+ className={`gantt-board-column ${boardDragItem ? 'gantt-board-drop-target' : ''}`}
30
+ onDragOver={(e) => { e.preventDefault(); e.currentTarget.classList.add('gantt-board-drag-over') }}
31
+ onDragLeave={(e) => { e.currentTarget.classList.remove('gantt-board-drag-over') }}
32
+ onDrop={(e) => {
33
+ e.preventDefault()
34
+ e.currentTarget.classList.remove('gantt-board-drag-over')
35
+ if (boardDragItem && onStatusChange) {
36
+ onStatusChange(boardDragItem, col.status)
37
+ }
38
+ setBoardDragItem(null)
39
+ }}
40
+ >
41
+ <div className="gantt-board-column-header">
42
+ <span className={`gantt-status-badge gantt-status-${col.status}`}>{col.status.replace(/_/g, ' ')}</span>
43
+ <span className="gantt-board-count">{col.items.length}</span>
44
+ </div>
45
+ <div className="gantt-board-cards">
46
+ {col.items.map((item) => (
47
+ <div
48
+ key={item.id}
49
+ className="gantt-board-card"
50
+ draggable={!!onStatusChange}
51
+ onDragStart={() => setBoardDragItem(item.id)}
52
+ onDragEnd={() => setBoardDragItem(null)}
53
+ onClick={() => handleItemClick(item)}
54
+ >
55
+ <div className="gantt-board-card-title">{item.title}</div>
56
+ {item.category && (
57
+ <span className="gantt-category-badge" style={{ backgroundColor: categoryColors[item.category] || categoryColors.other || '#6b7280' }}>
58
+ {item.category}
59
+ </span>
60
+ )}
61
+ {item.endDate && (
62
+ <span className="gantt-board-card-date">{format(new Date(item.endDate), 'MMM d')}</span>
63
+ )}
64
+ </div>
65
+ ))}
66
+ </div>
67
+ </div>
68
+ ))}
69
+ </div>
70
+ )
71
+ }