@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.
- package/package.json +1 -1
- package/src/__mocks__/next/link.js +11 -0
- package/src/components/account/__tests__/account.test.tsx +315 -0
- package/src/components/command-palette/CommandGroup.tsx +23 -0
- package/src/components/command-palette/CommandPalette.tsx +183 -200
- package/src/components/command-palette/CommandResultItem.tsx +59 -0
- package/src/components/command-palette/__tests__/CommandGroup.test.tsx +81 -0
- package/src/components/command-palette/__tests__/CommandResultItem.test.tsx +166 -0
- package/src/components/command-palette/__tests__/command-palette-context.test.tsx +166 -0
- package/src/components/command-palette/__tests__/useCommandPaletteSearch.test.ts +271 -0
- package/src/components/command-palette/index.ts +6 -0
- package/src/components/command-palette/useCommandPaletteSearch.ts +114 -0
- package/src/components/compose/__tests__/compose.test.tsx +656 -0
- package/src/components/dashboard/PipelineFunnel.tsx +126 -0
- package/src/components/dashboard/TopCampaigns.tsx +132 -0
- package/src/components/dashboard/__tests__/dashboard.test.tsx +785 -0
- package/src/components/dashboard/index.ts +6 -0
- package/src/components/dialog/ConfirmDialog.tsx +72 -0
- package/src/components/dialog/__tests__/ConfirmDialog.test.tsx +126 -0
- package/src/components/dialog/index.ts +3 -0
- package/src/components/email-dialogs/__tests__/email-dialogs.test.tsx +982 -0
- package/src/components/email-editor/BlockRenderer.tsx +120 -0
- package/src/components/email-editor/__tests__/BlockRenderer.test.tsx +332 -0
- package/src/components/email-editor/__tests__/block-renderers.test.ts +624 -0
- package/src/components/email-editor/__tests__/email-html-renderer.test.ts +376 -0
- package/src/components/email-editor/blocks/__tests__/blocks.test.tsx +818 -0
- package/src/components/email-editor/editor-sidebar.tsx +6 -731
- package/src/components/email-editor/email-editor.tsx +78 -467
- package/src/components/email-editor/hooks/__tests__/useDragDrop.test.ts +355 -0
- package/src/components/email-editor/hooks/__tests__/useEmailEditorState.test.ts +551 -0
- package/src/components/email-editor/hooks/useDragDrop.ts +181 -0
- package/src/components/email-editor/hooks/useEmailEditorState.ts +426 -0
- package/src/components/email-editor/index.ts +1 -0
- package/src/components/email-editor/panels/BlockPropertyPanel.tsx +637 -0
- package/src/components/email-editor/panels/GlobalStylesPanel.tsx +108 -0
- package/src/components/email-editor/panels/SectionSettingsPanel.tsx +80 -0
- package/src/components/email-editor/panels/__tests__/BlockPropertyPanel.test.tsx +707 -0
- package/src/components/email-editor/panels/__tests__/GlobalStylesPanel.test.tsx +226 -0
- package/src/components/email-editor/panels/index.ts +3 -0
- package/src/components/enrichment/__tests__/enrichment.test.tsx +184 -0
- package/src/components/gantt/GanttBoardView.tsx +71 -0
- package/src/components/gantt/GanttChart.tsx +134 -881
- package/src/components/gantt/GanttFilterBar.tsx +100 -0
- package/src/components/gantt/GanttListView.tsx +63 -0
- package/src/components/gantt/GanttTimelineView.tsx +215 -0
- package/src/components/gantt/__tests__/GanttBoardView.test.tsx +305 -0
- package/src/components/gantt/__tests__/GanttFilterBar.test.tsx +544 -0
- package/src/components/gantt/__tests__/GanttListView.test.tsx +337 -0
- package/src/components/gantt/__tests__/GanttTimelineView.test.tsx +375 -0
- package/src/components/gantt/__tests__/gantt-utils.test.ts +341 -0
- package/src/components/gantt/__tests__/useGanttState.test.ts +535 -0
- package/src/components/gantt/hooks/useGanttState.ts +644 -0
- package/src/components/gantt/index.ts +10 -0
- package/src/components/integrations/__tests__/integrations.test.tsx +191 -0
- package/src/components/kanban/__tests__/kanban.test.tsx +157 -0
- package/src/components/lists/__tests__/lists.test.tsx +263 -0
- package/src/components/loading/__tests__/loading.test.tsx +114 -0
- package/src/components/navigation/__tests__/navigation.test.tsx +194 -0
- package/src/components/pipeline/__tests__/pipeline.test.tsx +169 -0
- package/src/components/settings/__tests__/settings.test.tsx +181 -0
- 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
|
+
}
|