@leanspec/ui 0.2.14 → 0.2.15

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 (150) hide show
  1. package/bin/leanspec-ui.js +126 -0
  2. package/dist/assets/_baseUniq-B6x_7o5y.js +1 -0
  3. package/dist/assets/arc-DZ27bDb2.js +1 -0
  4. package/dist/assets/architectureDiagram-VXUJARFQ-VTQAQir-.js +36 -0
  5. package/dist/assets/blockDiagram-VD42YOAC-BeZAaaB1.js +122 -0
  6. package/dist/assets/c4Diagram-YG6GDRKO-BnT3bg74.js +10 -0
  7. package/dist/assets/channel-BSVY_tOy.js +1 -0
  8. package/dist/assets/chunk-4BX2VUAB-qtS73lje.js +1 -0
  9. package/dist/assets/chunk-55IACEB6-B41Ne73X.js +1 -0
  10. package/dist/assets/chunk-B4BG7PRW-CRL0j0p8.js +165 -0
  11. package/dist/assets/chunk-DI55MBZ5-BRa_G3mf.js +220 -0
  12. package/dist/assets/chunk-FMBD7UC4-D_AT_wL5.js +15 -0
  13. package/dist/assets/chunk-QN33PNHL-Q1Nos5j_.js +1 -0
  14. package/dist/assets/chunk-QZHKN3VN-DflSXVVh.js +1 -0
  15. package/dist/assets/chunk-TZMSLE5B-B0OC-s8d.js +1 -0
  16. package/dist/assets/classDiagram-2ON5EDUG-Dn0xX9IG.js +1 -0
  17. package/dist/assets/classDiagram-v2-WZHVMYZB-Dn0xX9IG.js +1 -0
  18. package/dist/assets/clone-C-KMhWbr.js +1 -0
  19. package/dist/assets/core-DV6XEvTN.js +1 -0
  20. package/dist/assets/cose-bilkent-S5V4N54A-CboCNDKn.js +1 -0
  21. package/dist/assets/cytoscape.esm-5J0xJHOV.js +321 -0
  22. package/dist/assets/dagre-6UL2VRFP-DOonQ6kf.js +4 -0
  23. package/dist/assets/diagram-PSM6KHXK-DPYPbSse.js +24 -0
  24. package/dist/assets/diagram-QEK2KX5R-DfTIvQXt.js +43 -0
  25. package/dist/assets/diagram-S2PKOQOG-Dl0bD_cb.js +24 -0
  26. package/dist/assets/erDiagram-Q2GNP2WA-C36i3Lze.js +60 -0
  27. package/dist/assets/flowDiagram-NV44I4VS-BskiGL1V.js +162 -0
  28. package/dist/assets/ganttDiagram-JELNMOA3-BvEghcko.js +267 -0
  29. package/dist/assets/gitGraphDiagram-NY62KEGX-BEkcYMS3.js +65 -0
  30. package/dist/assets/graph-DfQs0Ukg.js +1 -0
  31. package/dist/assets/index-BQDji5Db.js +389 -0
  32. package/dist/assets/index-BaBk6Eb5.css +1 -0
  33. package/dist/assets/infoDiagram-WHAUD3N6-BNQZZTcd.js +2 -0
  34. package/dist/assets/journeyDiagram-XKPGCS4Q-BmcOKIu0.js +139 -0
  35. package/dist/assets/kanban-definition-3W4ZIXB7-etkUgKbz.js +89 -0
  36. package/dist/assets/katex-XbL3y5x-.js +261 -0
  37. package/dist/assets/layout-CyPK9cFq.js +1 -0
  38. package/dist/assets/min-D1_JVZu9.js +1 -0
  39. package/dist/assets/mindmap-definition-VGOIOE7T-D-3bnFXY.js +68 -0
  40. package/dist/assets/pieDiagram-ADFJNKIX-SSpBbb1Z.js +30 -0
  41. package/dist/assets/quadrantDiagram-AYHSOK5B-kCW_e4Rj.js +7 -0
  42. package/dist/assets/requirementDiagram-UZGBJVZJ-B-hRBRHn.js +64 -0
  43. package/dist/assets/sankeyDiagram-TZEHDZUN-Bq18cS4Z.js +10 -0
  44. package/dist/assets/sequenceDiagram-WL72ISMW-D6dOwWak.js +145 -0
  45. package/dist/assets/stateDiagram-FKZM4ZOC-DRnWZawn.js +1 -0
  46. package/dist/assets/stateDiagram-v2-4FDKWEC3-ortqHAq8.js +1 -0
  47. package/dist/assets/timeline-definition-IT6M3QCI-DLIDeF--.js +61 -0
  48. package/dist/assets/treemap-KMMF4GRG-D5oyLJbR.js +128 -0
  49. package/dist/assets/xychartDiagram-PRI3JC2R-B_qUVnv4.js +7 -0
  50. package/{index.html → dist/index.html} +2 -1
  51. package/package.json +10 -1
  52. package/eslint.config.js +0 -23
  53. package/package.json.backup +0 -83
  54. package/postcss.config.js +0 -6
  55. package/src/App.css +0 -42
  56. package/src/App.tsx +0 -17
  57. package/src/assets/react.svg +0 -1
  58. package/src/components/LanguageSwitcher.tsx +0 -67
  59. package/src/components/Layout.tsx +0 -88
  60. package/src/components/MainSidebar.tsx +0 -163
  61. package/src/components/MermaidDiagram.tsx +0 -85
  62. package/src/components/MinimalLayout.tsx +0 -51
  63. package/src/components/Navigation.tsx +0 -254
  64. package/src/components/PriorityBadge.tsx +0 -59
  65. package/src/components/ProjectSwitcher.tsx +0 -222
  66. package/src/components/QuickSearch.tsx +0 -225
  67. package/src/components/RootRedirect.tsx +0 -40
  68. package/src/components/SpecDetailLayout.context.ts +0 -10
  69. package/src/components/SpecDetailLayout.tsx +0 -14
  70. package/src/components/SpecsNavSidebar.tsx +0 -615
  71. package/src/components/StatusBadge.tsx +0 -59
  72. package/src/components/ThemeToggle.tsx +0 -25
  73. package/src/components/Tooltip.tsx +0 -29
  74. package/src/components/context/ContextClient.tsx +0 -471
  75. package/src/components/context/ContextFileDetail.tsx +0 -163
  76. package/src/components/dashboard/ActivityItem.tsx +0 -36
  77. package/src/components/dashboard/DashboardClient.tsx +0 -218
  78. package/src/components/dashboard/SpecListItem.tsx +0 -58
  79. package/src/components/dashboard/StatCard.tsx +0 -52
  80. package/src/components/dependencies/SpecNode.tsx +0 -128
  81. package/src/components/dependencies/SpecSidebar.tsx +0 -256
  82. package/src/components/dependencies/constants.ts +0 -25
  83. package/src/components/dependencies/types.ts +0 -38
  84. package/src/components/dependencies/utils.ts +0 -261
  85. package/src/components/metadata-editors/PriorityEditor.tsx +0 -89
  86. package/src/components/metadata-editors/StatusEditor.tsx +0 -85
  87. package/src/components/metadata-editors/TagsEditor.tsx +0 -207
  88. package/src/components/projects/CreateProjectDialog.tsx +0 -162
  89. package/src/components/projects/DirectoryPicker.tsx +0 -182
  90. package/src/components/shared/BackToTop.tsx +0 -39
  91. package/src/components/shared/ColorPicker.tsx +0 -68
  92. package/src/components/shared/EmptyState.tsx +0 -35
  93. package/src/components/shared/ErrorBoundary.tsx +0 -79
  94. package/src/components/shared/PageHeader.tsx +0 -23
  95. package/src/components/shared/PageTransition.tsx +0 -40
  96. package/src/components/shared/ProjectAvatar.tsx +0 -107
  97. package/src/components/shared/Skeletons.tsx +0 -184
  98. package/src/components/spec-detail/EditableMetadata.tsx +0 -129
  99. package/src/components/spec-detail/MarkdownRenderer.tsx +0 -47
  100. package/src/components/spec-detail/TableOfContents.tsx +0 -150
  101. package/src/components/specs/BoardView.tsx +0 -204
  102. package/src/components/specs/ListView.tsx +0 -62
  103. package/src/components/specs/SpecsFilters.tsx +0 -190
  104. package/src/contexts/KeyboardShortcutsContext.tsx +0 -95
  105. package/src/contexts/LayoutContext.tsx +0 -45
  106. package/src/contexts/ProjectContext.tsx +0 -163
  107. package/src/contexts/ThemeContext.tsx +0 -90
  108. package/src/contexts/index.ts +0 -7
  109. package/src/hooks/useKeyboardShortcuts.ts +0 -87
  110. package/src/index.css +0 -624
  111. package/src/lib/api.ts +0 -72
  112. package/src/lib/backend-adapter.ts +0 -382
  113. package/src/lib/date-utils.ts +0 -122
  114. package/src/lib/i18n.test.ts +0 -57
  115. package/src/lib/i18n.ts +0 -51
  116. package/src/lib/markdown-utils.ts +0 -38
  117. package/src/lib/sub-spec-utils.ts +0 -166
  118. package/src/lib/utils.ts +0 -6
  119. package/src/locales/en/common.json +0 -660
  120. package/src/locales/en/errors.json +0 -20
  121. package/src/locales/en/help.json +0 -8
  122. package/src/locales/zh-CN/common.json +0 -660
  123. package/src/locales/zh-CN/errors.json +0 -20
  124. package/src/locales/zh-CN/help.json +0 -8
  125. package/src/main.tsx +0 -12
  126. package/src/pages/ContextPage.tsx +0 -111
  127. package/src/pages/DashboardPage.tsx +0 -97
  128. package/src/pages/DependenciesPage.tsx +0 -881
  129. package/src/pages/ProjectsPage.tsx +0 -432
  130. package/src/pages/SpecDetailPage.tsx +0 -592
  131. package/src/pages/SpecsPage.tsx +0 -319
  132. package/src/pages/StatsPage.tsx +0 -307
  133. package/src/router/projectRoutes.tsx +0 -36
  134. package/src/router.tsx +0 -33
  135. package/src/test/setup.ts +0 -39
  136. package/src/types/api.ts +0 -185
  137. package/tailwind.config.ts +0 -57
  138. package/tsconfig.app.json +0 -29
  139. package/tsconfig.json +0 -7
  140. package/tsconfig.node.json +0 -26
  141. package/tsconfig.tsbuildinfo +0 -1
  142. package/vite.config.ts +0 -27
  143. package/vitest.config.ts +0 -18
  144. /package/{public → dist}/favicon.ico +0 -0
  145. /package/{public → dist}/github-mark-white.svg +0 -0
  146. /package/{public → dist}/github-mark.svg +0 -0
  147. /package/{public → dist}/logo-dark-bg.svg +0 -0
  148. /package/{public → dist}/logo-with-bg.svg +0 -0
  149. /package/{public → dist}/logo.svg +0 -0
  150. /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
- };