@leanspec/ui 0.2.13 → 0.2.15-dev.21022397862

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 (149) hide show
  1. package/bin/leanspec-ui.js +191 -0
  2. package/dist/assets/_baseUniq-CRqreL7N.js +1 -0
  3. package/dist/assets/arc-DMhx9AJT.js +1 -0
  4. package/dist/assets/architectureDiagram-VXUJARFQ-DM0L0YzO.js +36 -0
  5. package/dist/assets/blockDiagram-VD42YOAC-DHQXDHsD.js +122 -0
  6. package/dist/assets/c4Diagram-YG6GDRKO-0L7o2gpH.js +10 -0
  7. package/dist/assets/channel-2tOl0nAZ.js +1 -0
  8. package/dist/assets/chunk-4BX2VUAB-CwFT-Uaj.js +1 -0
  9. package/dist/assets/chunk-55IACEB6-CjvuUHHG.js +1 -0
  10. package/dist/assets/chunk-B4BG7PRW-BRJBysMK.js +165 -0
  11. package/dist/assets/chunk-DI55MBZ5-BnNEeoaA.js +220 -0
  12. package/dist/assets/chunk-FMBD7UC4-BK2l30pm.js +15 -0
  13. package/dist/assets/chunk-QN33PNHL-BN_cZkCU.js +1 -0
  14. package/dist/assets/chunk-QZHKN3VN-Brc3Yrub.js +1 -0
  15. package/dist/assets/chunk-TZMSLE5B-D2zzpLfO.js +1 -0
  16. package/dist/assets/classDiagram-2ON5EDUG-BB9CSNmS.js +1 -0
  17. package/dist/assets/classDiagram-v2-WZHVMYZB-BB9CSNmS.js +1 -0
  18. package/dist/assets/clone-BjxVFtyI.js +1 -0
  19. package/dist/assets/core-DV6XEvTN.js +1 -0
  20. package/dist/assets/cose-bilkent-S5V4N54A-CLJgM3XR.js +1 -0
  21. package/dist/assets/cytoscape.esm-5J0xJHOV.js +321 -0
  22. package/dist/assets/dagre-6UL2VRFP-_IFvBJKJ.js +4 -0
  23. package/dist/assets/diagram-PSM6KHXK--83HIYSQ.js +24 -0
  24. package/dist/assets/diagram-QEK2KX5R-6jAWnCnZ.js +43 -0
  25. package/dist/assets/diagram-S2PKOQOG-D5pwHvjZ.js +24 -0
  26. package/dist/assets/erDiagram-Q2GNP2WA-B4FV3mTd.js +60 -0
  27. package/dist/assets/flowDiagram-NV44I4VS-mtD2kF4M.js +162 -0
  28. package/dist/assets/ganttDiagram-JELNMOA3-BKALgqTK.js +267 -0
  29. package/dist/assets/gitGraphDiagram-NY62KEGX-Bd7r0pAf.js +65 -0
  30. package/dist/assets/graph-B2rEI7cK.js +1 -0
  31. package/dist/assets/index-Bekv_o1t.css +1 -0
  32. package/dist/assets/index-DSRxU-E5.js +389 -0
  33. package/dist/assets/infoDiagram-WHAUD3N6--nJOBKqh.js +2 -0
  34. package/dist/assets/journeyDiagram-XKPGCS4Q-BzGutKN3.js +139 -0
  35. package/dist/assets/kanban-definition-3W4ZIXB7-DyQO17vq.js +89 -0
  36. package/dist/assets/katex-XbL3y5x-.js +261 -0
  37. package/dist/assets/layout-iCSHU015.js +1 -0
  38. package/dist/assets/min-BK_AIJdo.js +1 -0
  39. package/dist/assets/mindmap-definition-VGOIOE7T-BZMj_6zo.js +68 -0
  40. package/dist/assets/pieDiagram-ADFJNKIX-CkAGsq9p.js +30 -0
  41. package/dist/assets/quadrantDiagram-AYHSOK5B-CWa93px1.js +7 -0
  42. package/dist/assets/requirementDiagram-UZGBJVZJ-CufFVR8c.js +64 -0
  43. package/dist/assets/sankeyDiagram-TZEHDZUN-BEPgVgU4.js +10 -0
  44. package/dist/assets/sequenceDiagram-WL72ISMW-BkdBWhel.js +145 -0
  45. package/dist/assets/stateDiagram-FKZM4ZOC-D5T73yx0.js +1 -0
  46. package/dist/assets/stateDiagram-v2-4FDKWEC3-9hJWG2n6.js +1 -0
  47. package/dist/assets/timeline-definition-IT6M3QCI-CX7kTdU2.js +61 -0
  48. package/dist/assets/treemap-KMMF4GRG-ftWCQ9lJ.js +128 -0
  49. package/dist/assets/xychartDiagram-PRI3JC2R-Ngrels4n.js +7 -0
  50. package/{index.html → dist/index.html} +2 -1
  51. package/package.json +13 -3
  52. package/eslint.config.js +0 -23
  53. package/postcss.config.js +0 -6
  54. package/src/App.css +0 -42
  55. package/src/App.tsx +0 -17
  56. package/src/assets/react.svg +0 -1
  57. package/src/components/LanguageSwitcher.tsx +0 -67
  58. package/src/components/Layout.tsx +0 -88
  59. package/src/components/MainSidebar.tsx +0 -163
  60. package/src/components/MermaidDiagram.tsx +0 -85
  61. package/src/components/MinimalLayout.tsx +0 -51
  62. package/src/components/Navigation.tsx +0 -254
  63. package/src/components/PriorityBadge.tsx +0 -59
  64. package/src/components/ProjectSwitcher.tsx +0 -222
  65. package/src/components/QuickSearch.tsx +0 -225
  66. package/src/components/RootRedirect.tsx +0 -40
  67. package/src/components/SpecDetailLayout.context.ts +0 -10
  68. package/src/components/SpecDetailLayout.tsx +0 -14
  69. package/src/components/SpecsNavSidebar.tsx +0 -615
  70. package/src/components/StatusBadge.tsx +0 -59
  71. package/src/components/ThemeToggle.tsx +0 -25
  72. package/src/components/Tooltip.tsx +0 -29
  73. package/src/components/context/ContextClient.tsx +0 -471
  74. package/src/components/context/ContextFileDetail.tsx +0 -163
  75. package/src/components/dashboard/ActivityItem.tsx +0 -36
  76. package/src/components/dashboard/DashboardClient.tsx +0 -218
  77. package/src/components/dashboard/SpecListItem.tsx +0 -58
  78. package/src/components/dashboard/StatCard.tsx +0 -52
  79. package/src/components/dependencies/SpecNode.tsx +0 -128
  80. package/src/components/dependencies/SpecSidebar.tsx +0 -256
  81. package/src/components/dependencies/constants.ts +0 -25
  82. package/src/components/dependencies/types.ts +0 -38
  83. package/src/components/dependencies/utils.ts +0 -261
  84. package/src/components/metadata-editors/PriorityEditor.tsx +0 -89
  85. package/src/components/metadata-editors/StatusEditor.tsx +0 -85
  86. package/src/components/metadata-editors/TagsEditor.tsx +0 -207
  87. package/src/components/projects/CreateProjectDialog.tsx +0 -162
  88. package/src/components/projects/DirectoryPicker.tsx +0 -182
  89. package/src/components/shared/BackToTop.tsx +0 -39
  90. package/src/components/shared/ColorPicker.tsx +0 -68
  91. package/src/components/shared/EmptyState.tsx +0 -35
  92. package/src/components/shared/ErrorBoundary.tsx +0 -79
  93. package/src/components/shared/PageHeader.tsx +0 -23
  94. package/src/components/shared/PageTransition.tsx +0 -40
  95. package/src/components/shared/ProjectAvatar.tsx +0 -107
  96. package/src/components/shared/Skeletons.tsx +0 -184
  97. package/src/components/spec-detail/EditableMetadata.tsx +0 -129
  98. package/src/components/spec-detail/MarkdownRenderer.tsx +0 -47
  99. package/src/components/spec-detail/TableOfContents.tsx +0 -150
  100. package/src/components/specs/BoardView.tsx +0 -204
  101. package/src/components/specs/ListView.tsx +0 -62
  102. package/src/components/specs/SpecsFilters.tsx +0 -190
  103. package/src/contexts/KeyboardShortcutsContext.tsx +0 -95
  104. package/src/contexts/LayoutContext.tsx +0 -45
  105. package/src/contexts/ProjectContext.tsx +0 -163
  106. package/src/contexts/ThemeContext.tsx +0 -90
  107. package/src/contexts/index.ts +0 -7
  108. package/src/hooks/useKeyboardShortcuts.ts +0 -87
  109. package/src/index.css +0 -624
  110. package/src/lib/api.ts +0 -72
  111. package/src/lib/backend-adapter.ts +0 -382
  112. package/src/lib/date-utils.ts +0 -122
  113. package/src/lib/i18n.test.ts +0 -57
  114. package/src/lib/i18n.ts +0 -51
  115. package/src/lib/markdown-utils.ts +0 -38
  116. package/src/lib/sub-spec-utils.ts +0 -166
  117. package/src/lib/utils.ts +0 -6
  118. package/src/locales/en/common.json +0 -660
  119. package/src/locales/en/errors.json +0 -20
  120. package/src/locales/en/help.json +0 -8
  121. package/src/locales/zh-CN/common.json +0 -660
  122. package/src/locales/zh-CN/errors.json +0 -20
  123. package/src/locales/zh-CN/help.json +0 -8
  124. package/src/main.tsx +0 -12
  125. package/src/pages/ContextPage.tsx +0 -111
  126. package/src/pages/DashboardPage.tsx +0 -97
  127. package/src/pages/DependenciesPage.tsx +0 -881
  128. package/src/pages/ProjectsPage.tsx +0 -432
  129. package/src/pages/SpecDetailPage.tsx +0 -592
  130. package/src/pages/SpecsPage.tsx +0 -319
  131. package/src/pages/StatsPage.tsx +0 -307
  132. package/src/router/projectRoutes.tsx +0 -36
  133. package/src/router.tsx +0 -33
  134. package/src/test/setup.ts +0 -39
  135. package/src/types/api.ts +0 -185
  136. package/tailwind.config.ts +0 -57
  137. package/tsconfig.app.json +0 -29
  138. package/tsconfig.json +0 -7
  139. package/tsconfig.node.json +0 -26
  140. package/tsconfig.tsbuildinfo +0 -1
  141. package/vite.config.ts +0 -27
  142. package/vitest.config.ts +0 -18
  143. /package/{public → dist}/favicon.ico +0 -0
  144. /package/{public → dist}/github-mark-white.svg +0 -0
  145. /package/{public → dist}/github-mark.svg +0 -0
  146. /package/{public → dist}/logo-dark-bg.svg +0 -0
  147. /package/{public → dist}/logo-with-bg.svg +0 -0
  148. /package/{public → dist}/logo.svg +0 -0
  149. /package/{public → dist}/vite.svg +0 -0
@@ -1,36 +0,0 @@
1
- import { Link } from 'react-router-dom';
2
- import type { DashboardSpec } from './SpecListItem';
3
- import { formatRelativeTime } from '../../lib/date-utils';
4
- import { useTranslation } from 'react-i18next';
5
-
6
- interface ActivityItemProps {
7
- spec: DashboardSpec;
8
- action: string;
9
- time: Date | string | null;
10
- basePath?: string;
11
- }
12
-
13
- export function ActivityItem({ spec, action, time, basePath = '/projects/default' }: ActivityItemProps) {
14
- const displayTitle = spec.title || spec.specName;
15
- const specUrl = `${basePath}/specs/${spec.specName}`;
16
- const { i18n } = useTranslation('common');
17
- const relativeTime = formatRelativeTime(time, i18n.language);
18
-
19
- return (
20
- <div className="flex items-start gap-3 py-2">
21
- <div className="w-2 h-2 rounded-full bg-primary mt-2 shrink-0" />
22
- <div className="flex-1 min-w-0">
23
- <p className="text-sm">
24
- <Link to={specUrl} className="font-medium hover:underline" title={spec.specName}>
25
- {spec.specNumber && `#${spec.specNumber.toString().padStart(3, '0')} `}
26
- {displayTitle}
27
- </Link>{' '}
28
- <span className="text-muted-foreground">{action}</span>
29
- </p>
30
- <p className="text-xs text-muted-foreground mt-0.5">
31
- {relativeTime}
32
- </p>
33
- </div>
34
- </div>
35
- );
36
- }
@@ -1,218 +0,0 @@
1
- import { Link } from 'react-router-dom';
2
- import {
3
- Button,
4
- Card,
5
- CardContent,
6
- CardHeader,
7
- CardTitle,
8
- } from '@leanspec/ui-components';
9
- import {
10
- Calendar,
11
- CheckCircle2,
12
- Clock,
13
- FileText,
14
- List as ListIcon,
15
- PlayCircle,
16
- TrendingUp,
17
- } from 'lucide-react';
18
- import type { Stats } from '../../types/api';
19
- import { StatCard } from './StatCard';
20
- import { SpecListItem, type DashboardSpec } from './SpecListItem';
21
- import { ActivityItem } from './ActivityItem';
22
- import { useTranslation } from 'react-i18next';
23
-
24
- interface DashboardClientProps {
25
- specs: DashboardSpec[];
26
- stats: Stats;
27
- projectColor?: string;
28
- projectName?: string;
29
- basePath?: string;
30
- }
31
-
32
- export function DashboardClient({ specs, stats, projectColor, projectName, basePath = '/projects/default' }: DashboardClientProps) {
33
- const { t } = useTranslation('common');
34
- const inProgressSpecs = specs
35
- .filter((spec) => spec.status === 'in-progress')
36
- .sort((a, b) => (b.specNumber || 0) - (a.specNumber || 0))
37
- .slice(0, 5);
38
-
39
- const plannedSpecs = specs
40
- .filter((spec) => spec.status === 'planned')
41
- .sort((a, b) => (b.specNumber || 0) - (a.specNumber || 0))
42
- .slice(0, 5);
43
-
44
- const recentlyAdded = specs
45
- .slice()
46
- .sort((a, b) => {
47
- if (!a.createdAt) return 1;
48
- if (!b.createdAt) return -1;
49
- const aTime = new Date(a.createdAt).getTime();
50
- const bTime = new Date(b.createdAt).getTime();
51
- return bTime - aTime;
52
- })
53
- .slice(0, 5);
54
-
55
- const recentActivity = specs
56
- .filter((spec) => spec.updatedAt)
57
- .sort((a, b) => {
58
- if (!a.updatedAt) return 1;
59
- if (!b.updatedAt) return -1;
60
- const aTime = new Date(a.updatedAt).getTime();
61
- const bTime = new Date(b.updatedAt).getTime();
62
- return bTime - aTime;
63
- })
64
- .slice(0, 10);
65
-
66
- const statusCounts = stats.specsByStatus.reduce<Record<string, number>>((acc: Record<string, number>, entry: { status: string; count: number }) => {
67
- acc[entry.status] = entry.count;
68
- return acc;
69
- }, {});
70
-
71
- const completeCount = statusCounts['complete'] || 0;
72
- const completionRate = stats.completionRate ?? 0;
73
-
74
- return (
75
- <div className="min-h-screen bg-background p-4 sm:p-8">
76
- <div className="max-w-7xl mx-auto space-y-6 sm:space-y-8">
77
- <div>
78
- <div className="flex items-center gap-3">
79
- {projectColor && (
80
- <div
81
- className="h-8 w-2 rounded-full shrink-0"
82
- style={{ backgroundColor: projectColor }}
83
- />
84
- )}
85
- <div>
86
- <h1 className="text-3xl sm:text-4xl font-bold tracking-tight">{t('dashboard.title')}</h1>
87
- <p className="text-muted-foreground mt-2">
88
- {projectName
89
- ? t('dashboard.subtitleWithProject', { projectName })
90
- : t('dashboard.subtitle')}
91
- </p>
92
- </div>
93
- </div>
94
- </div>
95
-
96
- <div className="grid grid-cols-2 lg:grid-cols-4 gap-3 sm:gap-4">
97
- <StatCard
98
- title={t('dashboard.cards.total')}
99
- value={stats.totalSpecs}
100
- icon={FileText}
101
- iconColor="text-blue-600"
102
- gradientFrom="from-blue-500/10"
103
- />
104
- <StatCard
105
- title={t('dashboard.cards.planned')}
106
- value={statusCounts['planned'] || 0}
107
- icon={Clock}
108
- iconColor="text-purple-600"
109
- gradientFrom="from-purple-500/10"
110
- />
111
- <StatCard
112
- title={t('dashboard.cards.inProgress')}
113
- value={statusCounts['in-progress'] || 0}
114
- icon={PlayCircle}
115
- iconColor="text-orange-600"
116
- gradientFrom="from-orange-500/10"
117
- />
118
- <StatCard
119
- title={t('dashboard.cards.completed')}
120
- value={completeCount}
121
- icon={CheckCircle2}
122
- iconColor="text-green-600"
123
- gradientFrom="from-green-500/10"
124
- subtext={
125
- <span className="flex items-center gap-1">
126
- <TrendingUp className="h-3 w-3" />
127
- {t('dashboard.cards.completionRate', { rate: completionRate.toFixed(1) })}
128
- </span>
129
- }
130
- />
131
- </div>
132
-
133
- <div className="grid grid-cols-1 lg:grid-cols-3 gap-4 sm:gap-6">
134
- <Card>
135
- <CardHeader>
136
- <CardTitle className="text-lg flex items-center gap-2">
137
- <Calendar className="h-5 w-5 text-blue-600" />
138
- {t('dashboard.recentlyAdded')}
139
- </CardTitle>
140
- </CardHeader>
141
- <CardContent className="space-y-1">
142
- {recentlyAdded.slice(0, 5).map((spec) => (
143
- <SpecListItem key={spec.id} spec={spec} basePath={basePath} />
144
- ))}
145
- </CardContent>
146
- </Card>
147
-
148
- <Card>
149
- <CardHeader>
150
- <CardTitle className="text-lg flex items-center gap-2">
151
- <Clock className="h-5 w-5 text-purple-600" />
152
- {t('dashboard.plannedSection', { count: plannedSpecs.length })}
153
- </CardTitle>
154
- </CardHeader>
155
- <CardContent className="space-y-1">
156
- {plannedSpecs.length > 0 ? (
157
- plannedSpecs.map((spec) => <SpecListItem key={spec.id} spec={spec} basePath={basePath} />)
158
- ) : (
159
- <p className="text-sm text-muted-foreground py-4 text-center">{t('dashboard.noPlanned')}</p>
160
- )}
161
- </CardContent>
162
- </Card>
163
-
164
- <Card>
165
- <CardHeader>
166
- <CardTitle className="text-lg flex items-center gap-2">
167
- <PlayCircle className="h-5 w-5 text-orange-600" />
168
- {t('dashboard.inProgressSection', { count: inProgressSpecs.length })}
169
- </CardTitle>
170
- </CardHeader>
171
- <CardContent className="space-y-1">
172
- {inProgressSpecs.length > 0 ? (
173
- inProgressSpecs.map((spec) => <SpecListItem key={spec.id} spec={spec} basePath={basePath} />)
174
- ) : (
175
- <p className="text-sm text-muted-foreground py-4 text-center">{t('dashboard.noInProgress')}</p>
176
- )}
177
- </CardContent>
178
- </Card>
179
- </div>
180
-
181
- <Card>
182
- <CardHeader>
183
- <CardTitle className="text-lg">{t('dashboard.recentActivity')}</CardTitle>
184
- </CardHeader>
185
- <CardContent>
186
- <div className="border-l-2 border-muted pl-4 space-y-1">
187
- {recentActivity.map((spec) => (
188
- <ActivityItem key={spec.id} spec={spec} action={t('dashboard.actionUpdated')} time={spec.updatedAt ?? null} basePath={basePath} />
189
- ))}
190
- </div>
191
- </CardContent>
192
- </Card>
193
-
194
- <Card>
195
- <CardHeader>
196
- <CardTitle className="text-lg">{t('dashboard.quickActions')}</CardTitle>
197
- </CardHeader>
198
- <CardContent>
199
- <div className="flex flex-wrap gap-3">
200
- <Button asChild>
201
- <Link to={`${basePath}/specs`}>
202
- <ListIcon className="h-4 w-4 mr-2" />
203
- {t('dashboard.viewAllSpecs')}
204
- </Link>
205
- </Button>
206
- <Button variant="outline" asChild>
207
- <Link to={`${basePath}/stats`}>
208
- <TrendingUp className="h-4 w-4 mr-2" />
209
- {t('dashboard.viewStats')}
210
- </Link>
211
- </Button>
212
- </div>
213
- </CardContent>
214
- </Card>
215
- </div>
216
- </div>
217
- );
218
- }
@@ -1,58 +0,0 @@
1
- import { Link } from 'react-router-dom';
2
- import { Badge } from '@leanspec/ui-components';
3
- import { StatusBadge } from '../StatusBadge';
4
- import { PriorityBadge } from '../PriorityBadge';
5
- import type { Spec } from '../../types/api';
6
-
7
- export type DashboardSpec = Spec;
8
-
9
- interface SpecListItemProps {
10
- spec: DashboardSpec;
11
- basePath?: string;
12
- }
13
-
14
- export function SpecListItem({ spec, basePath = '/projects/default' }: SpecListItemProps) {
15
- const displayTitle = spec.title || spec.specName;
16
- const specUrl = `${basePath}/specs/${spec.specName}`;
17
-
18
- return (
19
- <Link
20
- to={specUrl}
21
- className="block p-3 rounded-lg hover:bg-accent transition-colors"
22
- title={spec.specName}
23
- >
24
- <div className="flex items-start justify-between gap-2">
25
- <div className="flex-1 min-w-0">
26
- <div className="flex items-center gap-2 mb-1">
27
- {spec.specNumber && (
28
- <span className="text-sm font-mono text-muted-foreground shrink-0">
29
- #{spec.specNumber.toString().padStart(3, '0')}
30
- </span>
31
- )}
32
- <h4 className="text-sm font-medium truncate">
33
- {displayTitle}
34
- </h4>
35
- </div>
36
- {displayTitle !== spec.specName && (
37
- <div className="text-xs text-muted-foreground mb-1">
38
- {spec.specName}
39
- </div>
40
- )}
41
- {spec.tags && spec.tags.length > 0 && (
42
- <div className="flex flex-wrap gap-1">
43
- {spec.tags.slice(0, 3).map((tag: string) => (
44
- <Badge key={tag} variant="secondary" className="text-xs">
45
- {tag}
46
- </Badge>
47
- ))}
48
- </div>
49
- )}
50
- </div>
51
- <div className="flex flex-col items-end gap-1 shrink-0">
52
- {spec.status && <StatusBadge status={spec.status} className="text-[11px]" />}
53
- {spec.priority && <PriorityBadge priority={spec.priority} className="text-[11px]" />}
54
- </div>
55
- </div>
56
- </Link>
57
- );
58
- }
@@ -1,52 +0,0 @@
1
- import type { LucideIcon } from 'lucide-react';
2
- import { Card, CardContent, CardHeader, CardTitle } from '@leanspec/ui-components';
3
- import { cn } from '../../lib/utils';
4
-
5
- interface StatCardProps {
6
- title: string;
7
- value: number | string;
8
- icon: LucideIcon;
9
- iconColor?: string;
10
- gradientFrom?: string;
11
- subtext?: React.ReactNode;
12
- className?: string;
13
- }
14
-
15
- export function StatCard({
16
- title,
17
- value,
18
- icon: Icon,
19
- iconColor = 'text-primary',
20
- gradientFrom,
21
- subtext,
22
- className,
23
- }: StatCardProps) {
24
- return (
25
- <Card className={cn('relative overflow-hidden', className)}>
26
- {gradientFrom && (
27
- <div
28
- className={cn(
29
- 'absolute inset-0 bg-gradient-to-br to-transparent',
30
- gradientFrom
31
- )}
32
- />
33
- )}
34
- <CardHeader className="relative pb-3">
35
- <div className="flex items-center justify-between">
36
- <CardTitle className="text-sm font-medium text-muted-foreground">
37
- {title}
38
- </CardTitle>
39
- <Icon className={cn('h-5 w-5', iconColor)} />
40
- </div>
41
- </CardHeader>
42
- <CardContent className="relative">
43
- <div className="text-3xl font-bold">{value}</div>
44
- {subtext && (
45
- <div className="text-xs text-muted-foreground mt-1">
46
- {subtext}
47
- </div>
48
- )}
49
- </CardContent>
50
- </Card>
51
- );
52
- }
@@ -1,128 +0,0 @@
1
- import * as React from 'react';
2
- import { Handle, Position, type NodeProps } from 'reactflow';
3
- import { Clock, PlayCircle, CheckCircle2, Archive, AlertCircle, ArrowUp, Minus, ArrowDown } from 'lucide-react';
4
- import { useTranslation } from 'react-i18next';
5
- import { cn } from '../../lib/utils';
6
- import type { SpecNodeData } from './types';
7
- import {
8
- NODE_WIDTH,
9
- COMPACT_NODE_WIDTH,
10
- toneClasses,
11
- } from './constants';
12
-
13
- const statusIcons = {
14
- 'planned': Clock,
15
- 'in-progress': PlayCircle,
16
- 'complete': CheckCircle2,
17
- 'archived': Archive,
18
- };
19
-
20
- const priorityIcons = {
21
- 'critical': AlertCircle,
22
- 'high': ArrowUp,
23
- 'medium': Minus,
24
- 'low': ArrowDown,
25
- };
26
-
27
- export const SpecNode = React.memo(function SpecNode({ data }: NodeProps<SpecNodeData>) {
28
- const { t } = useTranslation();
29
- const isCompact = data.isCompact;
30
- const isSecondary = data.isSecondary;
31
- const depthOpacity =
32
- data.connectionDepth === 0
33
- ? 1
34
- : data.connectionDepth === 1
35
- ? 0.95
36
- : data.connectionDepth === 2
37
- ? 0.7
38
- : data.isDimmed
39
- ? 0.15
40
- : 1;
41
-
42
- // Secondary nodes (shown due to critical path) are slightly transparent
43
- const baseOpacity = isSecondary ? 0.65 : 1;
44
-
45
- const StatusIcon = statusIcons[data.tone as keyof typeof statusIcons] || Clock;
46
- const PriorityIcon = priorityIcons[data.priority as keyof typeof priorityIcons] || Minus;
47
-
48
- return (
49
- <div
50
- className={cn(
51
- 'flex flex-col rounded-lg shadow-lg transition-all duration-200',
52
- toneClasses[data.tone],
53
- data.interactive && 'cursor-pointer hover:scale-105 hover:shadow-xl hover:border-gray-400 dark:hover:border-white/50',
54
- data.isFocused && 'ring-2 ring-gray-800 dark:ring-white ring-offset-2 ring-offset-background scale-110 z-50',
55
- data.connectionDepth === 1 && 'ring-1 ring-gray-400 dark:ring-white/40',
56
- isCompact ? 'px-2 py-1 gap-0.5' : 'px-2.5 py-1.5 gap-0.5',
57
- isSecondary ? 'border border-dashed' : 'border-2'
58
- )}
59
- style={{
60
- width: isCompact ? COMPACT_NODE_WIDTH : NODE_WIDTH,
61
- opacity: depthOpacity * baseOpacity,
62
- transform: data.isDimmed ? 'scale(0.9)' : undefined,
63
- }}
64
- >
65
- <Handle type="target" position={Position.Left} className="opacity-0" />
66
- <div className="flex items-center justify-between gap-1">
67
- <span className={cn('font-bold', isCompact ? 'text-[9px]' : 'text-[10px]')}>
68
- #{data.number.toString().padStart(3, '0')}
69
- </span>
70
- <div className="flex items-center gap-0.5">
71
- {/* Status icon */}
72
- <div
73
- className={cn(
74
- 'rounded flex items-center justify-center',
75
- isCompact ? 'p-0.5' : 'p-1',
76
- data.tone === 'planned' && 'bg-blue-500/30',
77
- data.tone === 'in-progress' && 'bg-orange-500/30',
78
- data.tone === 'complete' && 'bg-green-500/30',
79
- data.tone === 'archived' && 'bg-gray-500/30'
80
- )}
81
- title={t(`status.${data.tone}`)}
82
- >
83
- <StatusIcon className={cn(isCompact ? 'h-2 w-2' : 'h-2.5 w-2.5')} />
84
- </div>
85
- {/* Priority icon */}
86
- <div
87
- className={cn(
88
- 'rounded flex items-center justify-center',
89
- isCompact ? 'p-0.5' : 'p-1',
90
- data.priority === 'critical' && 'bg-red-500/30',
91
- data.priority === 'high' && 'bg-orange-500/30',
92
- data.priority === 'medium' && 'bg-blue-500/30',
93
- data.priority === 'low' && 'bg-gray-500/30'
94
- )}
95
- title={data.priority ? t(`priority.${data.priority}`) : undefined}
96
- >
97
- <PriorityIcon className={cn(isCompact ? 'h-2 w-2' : 'h-2.5 w-2.5')} />
98
- </div>
99
- {/* Level indicator */}
100
- {data.connectionDepth !== undefined && data.connectionDepth > 0 && (
101
- <span
102
- className={cn(
103
- 'font-medium rounded bg-muted/50 text-muted-foreground',
104
- isCompact ? 'text-[7px] px-0.5 py-0.5' : 'text-[8px] px-1 py-0.5'
105
- )}
106
- title={t('dependenciesPage.graph.levelTitle', { depth: data.connectionDepth })}
107
- >
108
- {t('dependenciesPage.graph.levelBadge', { depth: data.connectionDepth })}
109
- </span>
110
- )}
111
- </div>
112
- </div>
113
- <span
114
- className={cn('font-medium leading-tight truncate', isCompact ? 'text-[8px]' : 'text-[10px]')}
115
- title={data.label}
116
- >
117
- {isCompact ? data.shortLabel : data.label}
118
- </span>
119
- <Handle type="source" position={Position.Right} className="opacity-0" />
120
- </div>
121
- );
122
- });
123
-
124
- SpecNode.displayName = 'SpecNode';
125
-
126
- export const nodeTypes = {
127
- specNode: SpecNode,
128
- };