@startsimpli/ui 0.4.6 → 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 (122) hide show
  1. package/package.json +2 -1
  2. package/src/__mocks__/next/link.js +11 -0
  3. package/src/components/ActivityTimeline.tsx +173 -0
  4. package/src/components/LogActivityDialog.tsx +303 -0
  5. package/src/components/QuickLogButtons.tsx +32 -0
  6. package/src/components/account/__tests__/account.test.tsx +315 -0
  7. package/src/components/badge/StageBadge.tsx +31 -0
  8. package/src/components/badge/index.ts +3 -0
  9. package/src/components/command-palette/CommandGroup.tsx +23 -0
  10. package/src/components/command-palette/CommandPalette.tsx +327 -0
  11. package/src/components/command-palette/CommandResultItem.tsx +59 -0
  12. package/src/components/command-palette/__tests__/CommandGroup.test.tsx +81 -0
  13. package/src/components/command-palette/__tests__/CommandResultItem.test.tsx +166 -0
  14. package/src/components/command-palette/__tests__/command-palette-context.test.tsx +166 -0
  15. package/src/components/command-palette/__tests__/useCommandPaletteSearch.test.ts +271 -0
  16. package/src/components/command-palette/command-palette-context.tsx +51 -0
  17. package/src/components/command-palette/index.ts +9 -0
  18. package/src/components/command-palette/useCommandPaletteSearch.ts +114 -0
  19. package/src/components/compose/__tests__/compose.test.tsx +656 -0
  20. package/src/components/compose/compose-header.tsx +72 -0
  21. package/src/components/compose/compose-loading.tsx +13 -0
  22. package/src/components/compose/index.ts +6 -0
  23. package/src/components/compose/save-status-indicator.tsx +57 -0
  24. package/src/components/compose/send-confirmation-dialog.tsx +87 -0
  25. package/src/components/compose/subject-input.tsx +25 -0
  26. package/src/components/compose/useAutoSave.ts +93 -0
  27. package/src/components/dashboard/DashboardGrid.tsx +32 -0
  28. package/src/components/dashboard/DashboardSection.tsx +32 -0
  29. package/src/components/dashboard/MetricCard.tsx +129 -0
  30. package/src/components/dashboard/PeriodSelector.tsx +55 -0
  31. package/src/components/dashboard/PipelineFunnel.tsx +126 -0
  32. package/src/components/dashboard/SparklineTrend.tsx +102 -0
  33. package/src/components/dashboard/TopCampaigns.tsx +132 -0
  34. package/src/components/dashboard/__tests__/dashboard.test.tsx +785 -0
  35. package/src/components/dashboard/index.ts +20 -0
  36. package/src/components/dialog/ConfirmDialog.tsx +72 -0
  37. package/src/components/dialog/__tests__/ConfirmDialog.test.tsx +126 -0
  38. package/src/components/dialog/index.ts +3 -0
  39. package/src/components/email-dialogs/__tests__/email-dialogs.test.tsx +982 -0
  40. package/src/components/email-dialogs/index.ts +14 -0
  41. package/src/components/email-dialogs/merge-fields.tsx +196 -0
  42. package/src/components/email-dialogs/preview-dialog.tsx +194 -0
  43. package/src/components/email-dialogs/schedule-dialog.tsx +297 -0
  44. package/src/components/email-dialogs/template-picker.tsx +225 -0
  45. package/src/components/email-dialogs/test-send-dialog.tsx +188 -0
  46. package/src/components/email-editor/BlockRenderer.tsx +120 -0
  47. package/src/components/email-editor/__tests__/BlockRenderer.test.tsx +332 -0
  48. package/src/components/email-editor/__tests__/block-renderers.test.ts +624 -0
  49. package/src/components/email-editor/__tests__/email-html-renderer.test.ts +376 -0
  50. package/src/components/email-editor/add-block-menu.tsx +151 -0
  51. package/src/components/email-editor/block-toolbar.tsx +73 -0
  52. package/src/components/email-editor/blocks/__tests__/blocks.test.tsx +818 -0
  53. package/src/components/email-editor/blocks/button-block.tsx +44 -0
  54. package/src/components/email-editor/blocks/divider-block.tsx +43 -0
  55. package/src/components/email-editor/blocks/footer-block.tsx +39 -0
  56. package/src/components/email-editor/blocks/header-block.tsx +39 -0
  57. package/src/components/email-editor/blocks/image-block.tsx +61 -0
  58. package/src/components/email-editor/blocks/index.ts +9 -0
  59. package/src/components/email-editor/blocks/metrics-block.tsx +198 -0
  60. package/src/components/email-editor/blocks/social-block.tsx +75 -0
  61. package/src/components/email-editor/blocks/spacer-block.tsx +26 -0
  62. package/src/components/email-editor/blocks/text-block.tsx +75 -0
  63. package/src/components/email-editor/editor-sidebar.tsx +66 -0
  64. package/src/components/email-editor/email-editor.tsx +497 -0
  65. package/src/components/email-editor/hooks/__tests__/useDragDrop.test.ts +355 -0
  66. package/src/components/email-editor/hooks/__tests__/useEmailEditorState.test.ts +551 -0
  67. package/src/components/email-editor/hooks/useDragDrop.ts +181 -0
  68. package/src/components/email-editor/hooks/useEmailEditorState.ts +426 -0
  69. package/src/components/email-editor/index.ts +51 -0
  70. package/src/components/email-editor/panels/BlockPropertyPanel.tsx +637 -0
  71. package/src/components/email-editor/panels/GlobalStylesPanel.tsx +108 -0
  72. package/src/components/email-editor/panels/SectionSettingsPanel.tsx +80 -0
  73. package/src/components/email-editor/panels/__tests__/BlockPropertyPanel.test.tsx +707 -0
  74. package/src/components/email-editor/panels/__tests__/GlobalStylesPanel.test.tsx +226 -0
  75. package/src/components/email-editor/panels/index.ts +3 -0
  76. package/src/components/email-editor/renderer/block-renderers.ts +209 -0
  77. package/src/components/email-editor/renderer/email-html-renderer.ts +128 -0
  78. package/src/components/email-editor/types.ts +413 -0
  79. package/src/components/email-editor/utils/defaults.ts +116 -0
  80. package/src/components/email-editor/utils/undo-redo.ts +59 -0
  81. package/src/components/enrichment/EnrichButton.tsx +33 -0
  82. package/src/components/enrichment/EnrichmentProgress.tsx +66 -0
  83. package/src/components/enrichment/QualityBadge.tsx +43 -0
  84. package/src/components/enrichment/__tests__/enrichment.test.tsx +184 -0
  85. package/src/components/enrichment/index.ts +8 -0
  86. package/src/components/gantt/GanttBoardView.tsx +71 -0
  87. package/src/components/gantt/GanttChart.tsx +140 -887
  88. package/src/components/gantt/GanttFilterBar.tsx +100 -0
  89. package/src/components/gantt/GanttListView.tsx +63 -0
  90. package/src/components/gantt/GanttTimelineView.tsx +215 -0
  91. package/src/components/gantt/__tests__/GanttBoardView.test.tsx +305 -0
  92. package/src/components/gantt/__tests__/GanttFilterBar.test.tsx +544 -0
  93. package/src/components/gantt/__tests__/GanttListView.test.tsx +337 -0
  94. package/src/components/gantt/__tests__/GanttTimelineView.test.tsx +375 -0
  95. package/src/components/gantt/__tests__/gantt-utils.test.ts +341 -0
  96. package/src/components/gantt/__tests__/useGanttState.test.ts +535 -0
  97. package/src/components/gantt/hooks/useGanttState.ts +644 -0
  98. package/src/components/gantt/index.ts +10 -0
  99. package/src/components/gantt/types.ts +5 -5
  100. package/src/components/index.ts +46 -0
  101. package/src/components/integrations/ConnectionStatus.tsx +77 -0
  102. package/src/components/integrations/IntegrationCard.tsx +92 -0
  103. package/src/components/integrations/__tests__/integrations.test.tsx +191 -0
  104. package/src/components/integrations/index.ts +5 -0
  105. package/src/components/kanban/KanbanBoard.tsx +103 -0
  106. package/src/components/kanban/__tests__/kanban.test.tsx +157 -0
  107. package/src/components/kanban/index.ts +2 -0
  108. package/src/components/lists/CreateListDialog.tsx +158 -0
  109. package/src/components/lists/ListCard.tsx +77 -0
  110. package/src/components/lists/__tests__/lists.test.tsx +263 -0
  111. package/src/components/lists/index.ts +5 -0
  112. package/src/components/loading/__tests__/loading.test.tsx +114 -0
  113. package/src/components/navigation/__tests__/navigation.test.tsx +194 -0
  114. package/src/components/pipeline/StageTransitionModal.tsx +146 -0
  115. package/src/components/pipeline/__tests__/pipeline.test.tsx +169 -0
  116. package/src/components/pipeline/index.ts +2 -0
  117. package/src/components/settings/SettingsCard.tsx +33 -0
  118. package/src/components/settings/SettingsLayout.tsx +28 -0
  119. package/src/components/settings/SettingsNav.tsx +42 -0
  120. package/src/components/settings/__tests__/settings.test.tsx +181 -0
  121. package/src/components/settings/index.ts +6 -0
  122. 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
+ }