@startsimpli/ui 0.1.7 → 0.3.0
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 +4 -1
- package/src/components/gantt/GanttChart.tsx +1026 -0
- package/src/components/gantt/gantt.css +877 -0
- package/src/components/gantt/index.ts +19 -0
- package/src/components/gantt/lib/dates.ts +61 -0
- package/src/components/gantt/lib/progress.ts +39 -0
- package/src/components/gantt/types.ts +93 -0
- package/src/theme/tailwind.config.d.ts +3 -3
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
export { GanttChart } from './GanttChart'
|
|
2
|
+
export type {
|
|
3
|
+
TimelineItem,
|
|
4
|
+
TimelineDependency,
|
|
5
|
+
GanttTask,
|
|
6
|
+
GanttChartProps,
|
|
7
|
+
GanttFilterState,
|
|
8
|
+
GanttViewMode,
|
|
9
|
+
HealthStatus,
|
|
10
|
+
} from './types'
|
|
11
|
+
export {
|
|
12
|
+
calculateExpectedProgress,
|
|
13
|
+
calculateHealthStatus,
|
|
14
|
+
getHealthColor,
|
|
15
|
+
} from './lib/progress'
|
|
16
|
+
export {
|
|
17
|
+
parseDateRangeFromTitle,
|
|
18
|
+
getHierarchyLevel,
|
|
19
|
+
} from './lib/dates'
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import { startOfYear, endOfYear, startOfMonth, endOfMonth, addYears } from 'date-fns'
|
|
2
|
+
|
|
3
|
+
const MONTHS: Record<string, number> = {
|
|
4
|
+
JAN: 0, FEB: 1, MAR: 2, APR: 3, MAY: 4, JUN: 5,
|
|
5
|
+
JUL: 6, AUG: 7, SEP: 8, OCT: 9, NOV: 10, DEC: 11,
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export function parseDateRangeFromTitle(
|
|
9
|
+
title: string,
|
|
10
|
+
referenceYear: number = new Date().getFullYear()
|
|
11
|
+
): { start: Date; end: Date } | null {
|
|
12
|
+
const match = title.match(/^\[([^\]]+)\]/)
|
|
13
|
+
if (!match) return null
|
|
14
|
+
|
|
15
|
+
const tag = match[1].toUpperCase()
|
|
16
|
+
const baseDate = new Date(referenceYear, 0, 1)
|
|
17
|
+
|
|
18
|
+
const yearMatch = tag.match(/^(\d+)YR$/)
|
|
19
|
+
if (yearMatch) {
|
|
20
|
+
const years = parseInt(yearMatch[1])
|
|
21
|
+
return { start: startOfYear(baseDate), end: endOfYear(addYears(baseDate, years - 1)) }
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
if (tag === 'H1') return { start: new Date(referenceYear, 0, 1), end: new Date(referenceYear, 5, 30) }
|
|
25
|
+
if (tag === 'H2') return { start: new Date(referenceYear, 6, 1), end: new Date(referenceYear, 11, 31) }
|
|
26
|
+
|
|
27
|
+
const quarterMatch = tag.match(/^Q([1-4])$/)
|
|
28
|
+
if (quarterMatch) {
|
|
29
|
+
const q = parseInt(quarterMatch[1])
|
|
30
|
+
const startMonth = (q - 1) * 3
|
|
31
|
+
return { start: new Date(referenceYear, startMonth, 1), end: endOfMonth(new Date(referenceYear, startMonth + 2, 1)) }
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
if (tag in MONTHS) {
|
|
35
|
+
const month = MONTHS[tag]
|
|
36
|
+
return { start: startOfMonth(new Date(referenceYear, month, 1)), end: endOfMonth(new Date(referenceYear, month, 1)) }
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const weekMatch = tag.match(/^W(\d+)$/)
|
|
40
|
+
if (weekMatch) {
|
|
41
|
+
const week = parseInt(weekMatch[1])
|
|
42
|
+
const start = new Date(referenceYear, 0, 1 + (week - 1) * 7)
|
|
43
|
+
const end = new Date(start)
|
|
44
|
+
end.setDate(end.getDate() + 6)
|
|
45
|
+
return { start, end }
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
return null
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export function getHierarchyLevel(title: string): number {
|
|
52
|
+
const match = title.match(/^\[([^\]]+)\]/)
|
|
53
|
+
if (!match) return 0
|
|
54
|
+
const tag = match[1].toUpperCase()
|
|
55
|
+
if (/^\d+YR$/.test(tag)) return 100 + parseInt(tag)
|
|
56
|
+
if (/^H[12]$/.test(tag)) return 50
|
|
57
|
+
if (/^Q[1-4]$/.test(tag)) return 40
|
|
58
|
+
if (tag in MONTHS) return 30
|
|
59
|
+
if (/^W\d+$/.test(tag)) return 20
|
|
60
|
+
return 10
|
|
61
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import type { HealthStatus } from '../types'
|
|
2
|
+
|
|
3
|
+
export interface HealthStatusResult {
|
|
4
|
+
status: HealthStatus
|
|
5
|
+
reason?: string
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export function calculateExpectedProgress(
|
|
9
|
+
startDate: Date,
|
|
10
|
+
endDate: Date,
|
|
11
|
+
currentDate: Date = new Date()
|
|
12
|
+
): number {
|
|
13
|
+
if (currentDate <= startDate) return 0
|
|
14
|
+
if (currentDate >= endDate) return 100
|
|
15
|
+
const totalDuration = endDate.getTime() - startDate.getTime()
|
|
16
|
+
const elapsed = currentDate.getTime() - startDate.getTime()
|
|
17
|
+
return Math.round((elapsed / totalDuration) * 100)
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function calculateHealthStatus(
|
|
21
|
+
progress: number,
|
|
22
|
+
expectedProgress: number,
|
|
23
|
+
hasBlockers: boolean = false
|
|
24
|
+
): HealthStatusResult {
|
|
25
|
+
if (hasBlockers) return { status: 'blocked', reason: 'Has active blockers' }
|
|
26
|
+
if (progress === 0 && expectedProgress > 0) return { status: 'not_started', reason: 'Work has not started' }
|
|
27
|
+
if (progress - expectedProgress < -10) return { status: 'at_risk', reason: 'Behind schedule' }
|
|
28
|
+
return { status: 'on_track' }
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export function getHealthColor(status: HealthStatus): string {
|
|
32
|
+
const colors: Record<HealthStatus, string> = {
|
|
33
|
+
on_track: '#22c55e',
|
|
34
|
+
at_risk: '#eab308',
|
|
35
|
+
blocked: '#ef4444',
|
|
36
|
+
not_started: '#9ca3af',
|
|
37
|
+
}
|
|
38
|
+
return colors[status]
|
|
39
|
+
}
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Generic timeline item interface.
|
|
3
|
+
* Consumers map their domain models (Goals, Beads, etc.) to this shape.
|
|
4
|
+
*/
|
|
5
|
+
export interface TimelineItem {
|
|
6
|
+
id: string
|
|
7
|
+
title: string
|
|
8
|
+
description?: string
|
|
9
|
+
status: string
|
|
10
|
+
category?: string
|
|
11
|
+
progress?: number // 0-100
|
|
12
|
+
start_date?: string | null
|
|
13
|
+
end_date?: string | null
|
|
14
|
+
created_at?: string
|
|
15
|
+
children?: TimelineItem[]
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Dependency between two timeline items.
|
|
20
|
+
*/
|
|
21
|
+
export interface TimelineDependency {
|
|
22
|
+
from_id: string
|
|
23
|
+
to_id: string
|
|
24
|
+
type?: string
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export type HealthStatus = 'on_track' | 'at_risk' | 'blocked' | 'not_started'
|
|
28
|
+
|
|
29
|
+
export interface GanttTask {
|
|
30
|
+
item: TimelineItem
|
|
31
|
+
start: Date
|
|
32
|
+
end: Date
|
|
33
|
+
progress: number
|
|
34
|
+
timeProgress: number
|
|
35
|
+
depth: number
|
|
36
|
+
hasChildren: boolean
|
|
37
|
+
parentId: string | null
|
|
38
|
+
healthStatus: HealthStatus
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export interface GanttFilterState {
|
|
42
|
+
search: string
|
|
43
|
+
statuses: string[]
|
|
44
|
+
categories: string[]
|
|
45
|
+
dateRange: { start: Date | null; end: Date | null }
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export type GanttViewMode = 'timeline' | 'board' | 'list'
|
|
49
|
+
|
|
50
|
+
export interface GanttChartProps {
|
|
51
|
+
/** Items to display on the timeline */
|
|
52
|
+
items: TimelineItem[]
|
|
53
|
+
/** Optional dependencies between items */
|
|
54
|
+
dependencies?: TimelineDependency[]
|
|
55
|
+
/** Callback when an item is clicked */
|
|
56
|
+
onItemClick?: (item: TimelineItem) => void
|
|
57
|
+
/** Callback when dates are changed via drag */
|
|
58
|
+
onDateChange?: (id: string, startDate: Date, endDate: Date) => void
|
|
59
|
+
/** Callback when an item is edited via the detail modal. Return updated item or void. */
|
|
60
|
+
onItemEdit?: (id: string, changes: Partial<TimelineItem>) => void
|
|
61
|
+
/** Callback when item status changes (e.g. board drag-and-drop) */
|
|
62
|
+
onStatusChange?: (id: string, newStatus: string) => void
|
|
63
|
+
/** Map a status string to a progress value (0-100) */
|
|
64
|
+
statusToProgress?: (status: string) => number
|
|
65
|
+
/** Map a category string to a bar color */
|
|
66
|
+
categoryColors?: Record<string, string>
|
|
67
|
+
/** Whether items have hierarchy via children[] arrays. Default: true */
|
|
68
|
+
hierarchical?: boolean
|
|
69
|
+
/** Initial zoom index (0-5). Default: 3 */
|
|
70
|
+
initialZoom?: number
|
|
71
|
+
/** Custom CSS class for the wrapper */
|
|
72
|
+
className?: string
|
|
73
|
+
/** Info column header label. Default: 'Item' */
|
|
74
|
+
infoColumnLabel?: string
|
|
75
|
+
/** Info column width in px. Default: 320 */
|
|
76
|
+
infoColumnWidth?: number
|
|
77
|
+
/** Show category badge in info column. Default: true */
|
|
78
|
+
showCategory?: boolean
|
|
79
|
+
/** Show status badge in info column. Default: true */
|
|
80
|
+
showStatus?: boolean
|
|
81
|
+
/** Show filter bar above the gantt. Default: false */
|
|
82
|
+
showFilterBar?: boolean
|
|
83
|
+
/** Show fullscreen toggle button. Default: false */
|
|
84
|
+
showFullscreen?: boolean
|
|
85
|
+
/** Show view mode switcher (timeline/board/list). Default: false */
|
|
86
|
+
showViewSwitcher?: boolean
|
|
87
|
+
/** Initial view mode. Default: 'timeline' */
|
|
88
|
+
initialViewMode?: GanttViewMode
|
|
89
|
+
/** Available statuses for the board view columns. If not provided, extracted from items. */
|
|
90
|
+
boardStatuses?: string[]
|
|
91
|
+
/** localStorage key for persisting collapse state. If set, collapse state is saved/restored. */
|
|
92
|
+
persistCollapseKey?: string
|
|
93
|
+
}
|
|
@@ -1,3 +1,3 @@
|
|
|
1
|
-
import type { Config } from 'tailwindcss'
|
|
2
|
-
declare const config: Config
|
|
3
|
-
export default config
|
|
1
|
+
import type { Config } from 'tailwindcss';
|
|
2
|
+
declare const config: Config;
|
|
3
|
+
export default config;
|