@startsimpli/ui 0.4.7 → 0.4.9

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 (62) hide show
  1. package/package.json +21 -23
  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/safe-html.tsx +9 -8
  61. package/src/components/settings/__tests__/settings.test.tsx +181 -0
  62. package/src/components/wizard/__tests__/wizard.test.tsx +97 -0
@@ -0,0 +1,100 @@
1
+ 'use client'
2
+
3
+ import { format } from 'date-fns'
4
+ import type { GanttFilterState } from './types'
5
+
6
+ export interface GanttFilterBarProps {
7
+ filters: GanttFilterState
8
+ onFilterChange: (filters: GanttFilterState) => void
9
+ uniqueStatuses: string[]
10
+ uniqueCategories: string[]
11
+ }
12
+
13
+ export function GanttFilterBar({ filters, onFilterChange, uniqueStatuses, uniqueCategories }: GanttFilterBarProps) {
14
+ const hasActiveFilters =
15
+ filters.search !== '' ||
16
+ filters.statuses.length > 0 ||
17
+ filters.categories.length > 0 ||
18
+ filters.dateRange.start !== null ||
19
+ filters.dateRange.end !== null
20
+
21
+ return (
22
+ <div className="gantt-filter-bar">
23
+ <input
24
+ type="text"
25
+ className="gantt-filter-search"
26
+ placeholder="Search items..."
27
+ value={filters.search}
28
+ onChange={(e) => onFilterChange({ ...filters, search: e.target.value })}
29
+ />
30
+ {uniqueStatuses.length > 0 && (
31
+ <select
32
+ className="gantt-filter-select"
33
+ value=""
34
+ onChange={(e) => {
35
+ const val = e.target.value
36
+ if (!val) return
37
+ onFilterChange({
38
+ ...filters,
39
+ statuses: filters.statuses.includes(val) ? filters.statuses.filter((s) => s !== val) : [...filters.statuses, val],
40
+ })
41
+ }}
42
+ >
43
+ <option value="">Status{filters.statuses.length > 0 ? ` (${filters.statuses.length})` : ''}</option>
44
+ {uniqueStatuses.map((s) => (
45
+ <option key={s} value={s}>{filters.statuses.includes(s) ? '\u2713 ' : ''}{s.replace(/_/g, ' ')}</option>
46
+ ))}
47
+ </select>
48
+ )}
49
+ {uniqueCategories.length > 0 && (
50
+ <select
51
+ className="gantt-filter-select"
52
+ value=""
53
+ onChange={(e) => {
54
+ const val = e.target.value
55
+ if (!val) return
56
+ onFilterChange({
57
+ ...filters,
58
+ categories: filters.categories.includes(val) ? filters.categories.filter((c) => c !== val) : [...filters.categories, val],
59
+ })
60
+ }}
61
+ >
62
+ <option value="">Category{filters.categories.length > 0 ? ` (${filters.categories.length})` : ''}</option>
63
+ {uniqueCategories.map((c) => (
64
+ <option key={c} value={c}>{filters.categories.includes(c) ? '\u2713 ' : ''}{c}</option>
65
+ ))}
66
+ </select>
67
+ )}
68
+ <input
69
+ type="date"
70
+ className="gantt-filter-date"
71
+ value={filters.dateRange.start ? format(filters.dateRange.start, 'yyyy-MM-dd') : ''}
72
+ onChange={(e) => onFilterChange({ ...filters, dateRange: { ...filters.dateRange, start: e.target.value ? new Date(e.target.value) : null } })}
73
+ title="Filter from date"
74
+ />
75
+ <input
76
+ type="date"
77
+ className="gantt-filter-date"
78
+ value={filters.dateRange.end ? format(filters.dateRange.end, 'yyyy-MM-dd') : ''}
79
+ onChange={(e) => onFilterChange({ ...filters, dateRange: { ...filters.dateRange, end: e.target.value ? new Date(e.target.value) : null } })}
80
+ title="Filter to date"
81
+ />
82
+ {hasActiveFilters && (
83
+ <button className="gantt-filter-clear" onClick={() => onFilterChange({ search: '', statuses: [], categories: [], dateRange: { start: null, end: null } })}>Clear</button>
84
+ )}
85
+ {/* Active filter pills */}
86
+ {filters.statuses.map((s) => (
87
+ <span key={`s-${s}`} className="gantt-filter-pill">
88
+ {s.replace(/_/g, ' ')}
89
+ <button onClick={() => onFilterChange({ ...filters, statuses: filters.statuses.filter((x) => x !== s) })}>x</button>
90
+ </span>
91
+ ))}
92
+ {filters.categories.map((c) => (
93
+ <span key={`c-${c}`} className="gantt-filter-pill">
94
+ {c}
95
+ <button onClick={() => onFilterChange({ ...filters, categories: filters.categories.filter((x) => x !== c) })}>x</button>
96
+ </span>
97
+ ))}
98
+ </div>
99
+ )
100
+ }
@@ -0,0 +1,63 @@
1
+ 'use client'
2
+
3
+ import { format } from 'date-fns'
4
+ import { getHealthColor } from './lib/progress'
5
+ import { INDENT_WIDTH } from './hooks/useGanttState'
6
+ import type { UseGanttStateReturn } from './hooks/useGanttState'
7
+
8
+ export interface GanttListViewProps {
9
+ listItems: UseGanttStateReturn['listItems']
10
+ focusedRowIndex: UseGanttStateReturn['focusedRowIndex']
11
+ categoryColors: UseGanttStateReturn['categoryColors']
12
+ handleItemClick: UseGanttStateReturn['handleItemClick']
13
+ }
14
+
15
+ export function GanttListView({
16
+ listItems,
17
+ focusedRowIndex,
18
+ categoryColors,
19
+ handleItemClick,
20
+ }: GanttListViewProps) {
21
+ return (
22
+ <div className="gantt-list">
23
+ <div className="gantt-list-header">
24
+ <span className="gantt-list-col-title">Title</span>
25
+ <span className="gantt-list-col-status">Status</span>
26
+ <span className="gantt-list-col-category">Category</span>
27
+ <span className="gantt-list-col-dates">Dates</span>
28
+ <span className="gantt-list-col-progress">Progress</span>
29
+ </div>
30
+ {listItems.map((task, idx) => (
31
+ <div
32
+ key={task.item.id}
33
+ className={`gantt-list-row ${focusedRowIndex === idx ? 'gantt-row-focused' : ''}`}
34
+ onClick={() => handleItemClick(task.item)}
35
+ >
36
+ <span className="gantt-list-col-title">
37
+ {task.depth > 0 && <span style={{ width: task.depth * INDENT_WIDTH, display: 'inline-block' }} />}
38
+ {task.item.title}
39
+ </span>
40
+ <span className="gantt-list-col-status">
41
+ <span className={`gantt-status-badge gantt-status-${task.item.status}`}>{task.item.status.replace(/_/g, ' ')}</span>
42
+ </span>
43
+ <span className="gantt-list-col-category">
44
+ {task.item.category && (
45
+ <span className="gantt-category-badge" style={{ backgroundColor: categoryColors[task.item.category] || categoryColors.other || '#6b7280' }}>
46
+ {task.item.category}
47
+ </span>
48
+ )}
49
+ </span>
50
+ <span className="gantt-list-col-dates">
51
+ {format(task.start, 'MMM d')} \u2013 {format(task.end, 'MMM d')}
52
+ </span>
53
+ <span className="gantt-list-col-progress">
54
+ <div className="gantt-list-progress-bar">
55
+ <div className="gantt-list-progress-fill" style={{ width: `${task.progress}%`, backgroundColor: getHealthColor(task.healthStatus) }} />
56
+ </div>
57
+ <span>{task.progress}%</span>
58
+ </span>
59
+ </div>
60
+ ))}
61
+ </div>
62
+ )
63
+ }
@@ -0,0 +1,215 @@
1
+ 'use client'
2
+
3
+ import { differenceInDays, format, isWeekend, isToday } from 'date-fns'
4
+ import type { GanttTask, TimelineItem } from './types'
5
+ import { getHealthColor } from './lib/progress'
6
+ import { ROW_HEIGHT, ZOOM_LEVELS, INDENT_WIDTH } from './hooks/useGanttState'
7
+ import type { UseGanttStateReturn } from './hooks/useGanttState'
8
+
9
+ export interface GanttTimelineViewProps {
10
+ // Layout props
11
+ infoColumnWidth: number
12
+ infoColumnLabel: string
13
+ showCategory: boolean
14
+ showStatus: boolean
15
+ showFullscreen: boolean
16
+
17
+ // State from useGanttState
18
+ zoomIndex: UseGanttStateReturn['zoomIndex']
19
+ setZoomIndex: UseGanttStateReturn['setZoomIndex']
20
+ collapsed: UseGanttStateReturn['collapsed']
21
+ mounted: UseGanttStateReturn['mounted']
22
+ dragState: UseGanttStateReturn['dragState']
23
+ focusedRowIndex: UseGanttStateReturn['focusedRowIndex']
24
+ dayWidth: UseGanttStateReturn['dayWidth']
25
+ tasks: UseGanttStateReturn['tasks']
26
+ startDate: UseGanttStateReturn['startDate']
27
+ days: UseGanttStateReturn['days']
28
+ months: UseGanttStateReturn['months']
29
+ timeHeaderUnits: UseGanttStateReturn['timeHeaderUnits']
30
+ dependencyPaths: UseGanttStateReturn['dependencyPaths']
31
+ timelineWidth: UseGanttStateReturn['timelineWidth']
32
+ bodyHeight: UseGanttStateReturn['bodyHeight']
33
+ isFullscreen: UseGanttStateReturn['isFullscreen']
34
+ categoryColors: UseGanttStateReturn['categoryColors']
35
+ onDateChange: UseGanttStateReturn['onDateChange']
36
+
37
+ // Refs
38
+ bodyScrollRef: UseGanttStateReturn['bodyScrollRef']
39
+ headerTimelineRef: UseGanttStateReturn['headerTimelineRef']
40
+ infoColumnRef: UseGanttStateReturn['infoColumnRef']
41
+
42
+ // Handlers
43
+ toggleFullscreen: UseGanttStateReturn['toggleFullscreen']
44
+ handleItemClick: UseGanttStateReturn['handleItemClick']
45
+ toggleCollapse: UseGanttStateReturn['toggleCollapse']
46
+ handleBodyScroll: UseGanttStateReturn['handleBodyScroll']
47
+ getBarPosition: UseGanttStateReturn['getBarPosition']
48
+ handleDragStart: UseGanttStateReturn['handleDragStart']
49
+ }
50
+
51
+ export function GanttTimelineView({
52
+ infoColumnWidth,
53
+ infoColumnLabel,
54
+ showCategory,
55
+ showStatus,
56
+ showFullscreen,
57
+ zoomIndex,
58
+ setZoomIndex,
59
+ collapsed,
60
+ mounted,
61
+ dragState,
62
+ focusedRowIndex,
63
+ dayWidth,
64
+ tasks,
65
+ startDate,
66
+ days,
67
+ months,
68
+ timeHeaderUnits,
69
+ dependencyPaths,
70
+ timelineWidth,
71
+ bodyHeight,
72
+ isFullscreen,
73
+ categoryColors,
74
+ onDateChange,
75
+ bodyScrollRef,
76
+ headerTimelineRef,
77
+ infoColumnRef,
78
+ toggleFullscreen,
79
+ handleItemClick,
80
+ toggleCollapse,
81
+ handleBodyScroll,
82
+ getBarPosition,
83
+ handleDragStart,
84
+ }: GanttTimelineViewProps) {
85
+ return (
86
+ <>
87
+ {/* Header row */}
88
+ <div className="gantt-header-row">
89
+ <div className="gantt-info-header" style={{ width: infoColumnWidth }}>
90
+ <span style={{ flex: 1 }}>{infoColumnLabel}</span>
91
+ <div className="gantt-zoom-controls">
92
+ <button onClick={() => setZoomIndex((i) => Math.max(i - 1, 0))} disabled={zoomIndex === 0} aria-label="Zoom out">-</button>
93
+ <input type="range" min="0" max={ZOOM_LEVELS.length - 1} value={zoomIndex} onChange={(e) => setZoomIndex(parseInt(e.target.value))} className="gantt-zoom-slider" aria-label={`Zoom level: ${dayWidth}px per day`} />
94
+ <button onClick={() => setZoomIndex((i) => Math.min(i + 1, ZOOM_LEVELS.length - 1))} disabled={zoomIndex === ZOOM_LEVELS.length - 1} aria-label="Zoom in">+</button>
95
+ {showFullscreen && (
96
+ <button onClick={toggleFullscreen} aria-label={isFullscreen ? 'Exit fullscreen' : 'Fullscreen'} title={isFullscreen ? 'Exit fullscreen' : 'Fullscreen'}>
97
+ {isFullscreen ? '\u22A0' : '\u229E'}
98
+ </button>
99
+ )}
100
+ </div>
101
+ </div>
102
+ <div className="gantt-timeline-header" ref={headerTimelineRef}>
103
+ <div style={{ width: timelineWidth }}>
104
+ <div className="gantt-month-header" style={{ width: timelineWidth }}>
105
+ {months.map((m, i) => (<div key={i} className="gantt-month" style={{ width: m.days * dayWidth }}>{m.label}</div>))}
106
+ </div>
107
+ <div className="gantt-day-header" style={{ width: timelineWidth }}>
108
+ {timeHeaderUnits.mode === 'weeks' ? (
109
+ timeHeaderUnits.units.map((unit, i) => (
110
+ <div key={i} className={`gantt-week ${unit.hasToday ? 'has-today' : ''}`} style={{ width: unit.width }}>{unit.width > 40 ? unit.label : ''}</div>
111
+ ))
112
+ ) : (
113
+ timeHeaderUnits.units.map((unit, i) => (
114
+ <div key={i} className={`gantt-day ${unit.isWeekend ? 'weekend' : ''} ${unit.isToday ? 'today' : ''}`} style={{ width: unit.width }}>{unit.label}</div>
115
+ ))
116
+ )}
117
+ </div>
118
+ </div>
119
+ </div>
120
+ </div>
121
+
122
+ {/* Scrollable body */}
123
+ <div className="gantt-body-scroll" ref={bodyScrollRef} onScroll={handleBodyScroll}>
124
+ <div className="gantt-info-column" ref={infoColumnRef} style={{ width: infoColumnWidth }}>
125
+ {tasks.map((task, rowIdx) => (
126
+ <div key={task.item.id} className={`gantt-info-row ${focusedRowIndex === rowIdx ? 'gantt-row-focused' : ''}`}>
127
+ <div className="gantt-row-indent" style={{ width: task.depth * INDENT_WIDTH }} />
128
+ {task.hasChildren ? (
129
+ <button className="gantt-collapse-btn" onClick={() => toggleCollapse(task.item.id)} aria-label={collapsed.has(task.item.id) ? 'Expand' : 'Collapse'}>
130
+ {collapsed.has(task.item.id) ? '\u25B6' : '\u25BC'}
131
+ </button>
132
+ ) : (<span className="gantt-collapse-spacer" />)}
133
+ <span className="gantt-row-title" onClick={() => handleItemClick(task.item)} title={task.item.title} style={{ cursor: 'pointer' }}>
134
+ {task.item.title}
135
+ </span>
136
+ {showCategory && task.item.category && (
137
+ <span className="gantt-category-badge" style={{ backgroundColor: categoryColors[task.item.category] || categoryColors.other || '#6b7280' }}>
138
+ {task.item.category}
139
+ </span>
140
+ )}
141
+ {showStatus && (
142
+ <span className={`gantt-status-badge gantt-status-${task.item.status}`}>{task.item.status.replace(/_/g, ' ')}</span>
143
+ )}
144
+ </div>
145
+ ))}
146
+ </div>
147
+
148
+ <div className="gantt-timeline-body" style={{ width: timelineWidth, height: bodyHeight }}>
149
+ {days.map((day, i) => (
150
+ <div key={i} className={`gantt-grid-line ${isWeekend(day) ? 'weekend' : ''} ${mounted && isToday(day) ? 'today' : ''}`} style={{ left: i * dayWidth, width: dayWidth, height: bodyHeight }} />
151
+ ))}
152
+ {tasks.map((_, i) => (<div key={i} className="gantt-row-line" style={{ top: (i + 1) * ROW_HEIGHT - 1 }} />))}
153
+
154
+ {dependencyPaths.length > 0 && (
155
+ <svg className="gantt-deps-svg" style={{ width: timelineWidth, height: bodyHeight }} aria-hidden="true">
156
+ {dependencyPaths.map((dep, i) => (<path key={`${dep.fromId}-${dep.toId}-${i}`} d={dep.path} className="gantt-dep-line" />))}
157
+ </svg>
158
+ )}
159
+
160
+ {tasks.map((task, rowIndex) => {
161
+ const isDragging = dragState?.taskId === task.item.id
162
+ const pos = getBarPosition(task, rowIndex, true)
163
+ const barColor = getHealthColor(task.healthStatus)
164
+ const dates = isDragging ? { start: dragState!.currentStart, end: dragState!.currentEnd } : { start: task.start, end: task.end }
165
+
166
+ return (
167
+ <div
168
+ key={task.item.id}
169
+ className={`gantt-bar depth-${Math.min(task.depth, 3)} ${isDragging ? 'dragging' : ''}`}
170
+ style={{
171
+ left: pos.left, width: pos.width, top: pos.top, height: pos.height,
172
+ backgroundColor: barColor,
173
+ cursor: onDateChange ? (dragState ? (dragState.type === 'move' ? 'grabbing' : 'ew-resize') : 'grab') : 'pointer',
174
+ opacity: task.item.status === 'completed' || task.item.status === 'closed' ? 0.7 : 0.85,
175
+ }}
176
+ onClick={() => !dragState && handleItemClick(task.item)}
177
+ onMouseDown={(e) => handleDragStart(e, task, 'move')}
178
+ title={`${task.item.title}\n${format(dates.start, 'MMM d, yyyy')} \u2013 ${format(dates.end, 'MMM d, yyyy')}\nProgress: ${task.progress}%`}
179
+ >
180
+ {onDateChange && (
181
+ <>
182
+ <div className="gantt-bar-handle gantt-bar-handle-left" onMouseDown={(e) => { e.stopPropagation(); handleDragStart(e, task, 'resize-start') }} />
183
+ <div className="gantt-bar-handle gantt-bar-handle-right" onMouseDown={(e) => { e.stopPropagation(); handleDragStart(e, task, 'resize-end') }} />
184
+ </>
185
+ )}
186
+ {task.timeProgress > 0 && task.timeProgress < 100 && (
187
+ <div className="gantt-bar-time-marker" style={{ left: `${task.timeProgress}%` }} />
188
+ )}
189
+ {task.progress > 0 && (
190
+ <div className={`gantt-bar-progress ${task.progress < task.timeProgress - 10 ? 'behind' : task.progress > task.timeProgress + 10 ? 'ahead' : ''}`} style={{ width: `${task.progress}%` }} />
191
+ )}
192
+ <span className="gantt-bar-label">{pos.width > 80 ? task.item.title.replace(/^\[[^\]]+\]\s*/, '') : ''}</span>
193
+ {isDragging && (<div className="gantt-drag-tooltip">{format(dates.start, 'MMM d')} \u2013 {format(dates.end, 'MMM d')}</div>)}
194
+ </div>
195
+ )
196
+ })}
197
+
198
+ {mounted && (() => {
199
+ const todayOffset = differenceInDays(new Date(), startDate)
200
+ if (todayOffset >= 0 && todayOffset < days.length) return <div className="gantt-today-line" style={{ left: todayOffset * dayWidth + dayWidth / 2 }} />
201
+ return null
202
+ })()}
203
+ </div>
204
+ </div>
205
+
206
+ <div className="gantt-legend">
207
+ <div className="gantt-legend-item"><div className="gantt-legend-color" style={{ background: '#22c55e' }} /><span>On Track</span></div>
208
+ <div className="gantt-legend-item"><div className="gantt-legend-color" style={{ background: '#eab308' }} /><span>At Risk</span></div>
209
+ <div className="gantt-legend-item"><div className="gantt-legend-color" style={{ background: '#ef4444' }} /><span>Blocked</span></div>
210
+ <div className="gantt-legend-item"><div className="gantt-legend-color" style={{ background: '#9ca3af' }} /><span>Not Started</span></div>
211
+ <div className="gantt-legend-item"><div className="gantt-legend-color" style={{ background: '#3b82f6', width: 2, height: 12 }} /><span>Today</span></div>
212
+ </div>
213
+ </>
214
+ )
215
+ }