@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.
- package/bin/leanspec-ui.js +126 -0
- package/dist/assets/_baseUniq-B6x_7o5y.js +1 -0
- package/dist/assets/arc-DZ27bDb2.js +1 -0
- package/dist/assets/architectureDiagram-VXUJARFQ-VTQAQir-.js +36 -0
- package/dist/assets/blockDiagram-VD42YOAC-BeZAaaB1.js +122 -0
- package/dist/assets/c4Diagram-YG6GDRKO-BnT3bg74.js +10 -0
- package/dist/assets/channel-BSVY_tOy.js +1 -0
- package/dist/assets/chunk-4BX2VUAB-qtS73lje.js +1 -0
- package/dist/assets/chunk-55IACEB6-B41Ne73X.js +1 -0
- package/dist/assets/chunk-B4BG7PRW-CRL0j0p8.js +165 -0
- package/dist/assets/chunk-DI55MBZ5-BRa_G3mf.js +220 -0
- package/dist/assets/chunk-FMBD7UC4-D_AT_wL5.js +15 -0
- package/dist/assets/chunk-QN33PNHL-Q1Nos5j_.js +1 -0
- package/dist/assets/chunk-QZHKN3VN-DflSXVVh.js +1 -0
- package/dist/assets/chunk-TZMSLE5B-B0OC-s8d.js +1 -0
- package/dist/assets/classDiagram-2ON5EDUG-Dn0xX9IG.js +1 -0
- package/dist/assets/classDiagram-v2-WZHVMYZB-Dn0xX9IG.js +1 -0
- package/dist/assets/clone-C-KMhWbr.js +1 -0
- package/dist/assets/core-DV6XEvTN.js +1 -0
- package/dist/assets/cose-bilkent-S5V4N54A-CboCNDKn.js +1 -0
- package/dist/assets/cytoscape.esm-5J0xJHOV.js +321 -0
- package/dist/assets/dagre-6UL2VRFP-DOonQ6kf.js +4 -0
- package/dist/assets/diagram-PSM6KHXK-DPYPbSse.js +24 -0
- package/dist/assets/diagram-QEK2KX5R-DfTIvQXt.js +43 -0
- package/dist/assets/diagram-S2PKOQOG-Dl0bD_cb.js +24 -0
- package/dist/assets/erDiagram-Q2GNP2WA-C36i3Lze.js +60 -0
- package/dist/assets/flowDiagram-NV44I4VS-BskiGL1V.js +162 -0
- package/dist/assets/ganttDiagram-JELNMOA3-BvEghcko.js +267 -0
- package/dist/assets/gitGraphDiagram-NY62KEGX-BEkcYMS3.js +65 -0
- package/dist/assets/graph-DfQs0Ukg.js +1 -0
- package/dist/assets/index-BQDji5Db.js +389 -0
- package/dist/assets/index-BaBk6Eb5.css +1 -0
- package/dist/assets/infoDiagram-WHAUD3N6-BNQZZTcd.js +2 -0
- package/dist/assets/journeyDiagram-XKPGCS4Q-BmcOKIu0.js +139 -0
- package/dist/assets/kanban-definition-3W4ZIXB7-etkUgKbz.js +89 -0
- package/dist/assets/katex-XbL3y5x-.js +261 -0
- package/dist/assets/layout-CyPK9cFq.js +1 -0
- package/dist/assets/min-D1_JVZu9.js +1 -0
- package/dist/assets/mindmap-definition-VGOIOE7T-D-3bnFXY.js +68 -0
- package/dist/assets/pieDiagram-ADFJNKIX-SSpBbb1Z.js +30 -0
- package/dist/assets/quadrantDiagram-AYHSOK5B-kCW_e4Rj.js +7 -0
- package/dist/assets/requirementDiagram-UZGBJVZJ-B-hRBRHn.js +64 -0
- package/dist/assets/sankeyDiagram-TZEHDZUN-Bq18cS4Z.js +10 -0
- package/dist/assets/sequenceDiagram-WL72ISMW-D6dOwWak.js +145 -0
- package/dist/assets/stateDiagram-FKZM4ZOC-DRnWZawn.js +1 -0
- package/dist/assets/stateDiagram-v2-4FDKWEC3-ortqHAq8.js +1 -0
- package/dist/assets/timeline-definition-IT6M3QCI-DLIDeF--.js +61 -0
- package/dist/assets/treemap-KMMF4GRG-D5oyLJbR.js +128 -0
- package/dist/assets/xychartDiagram-PRI3JC2R-B_qUVnv4.js +7 -0
- package/{index.html → dist/index.html} +2 -1
- package/package.json +10 -1
- package/eslint.config.js +0 -23
- package/package.json.backup +0 -83
- package/postcss.config.js +0 -6
- package/src/App.css +0 -42
- package/src/App.tsx +0 -17
- package/src/assets/react.svg +0 -1
- package/src/components/LanguageSwitcher.tsx +0 -67
- package/src/components/Layout.tsx +0 -88
- package/src/components/MainSidebar.tsx +0 -163
- package/src/components/MermaidDiagram.tsx +0 -85
- package/src/components/MinimalLayout.tsx +0 -51
- package/src/components/Navigation.tsx +0 -254
- package/src/components/PriorityBadge.tsx +0 -59
- package/src/components/ProjectSwitcher.tsx +0 -222
- package/src/components/QuickSearch.tsx +0 -225
- package/src/components/RootRedirect.tsx +0 -40
- package/src/components/SpecDetailLayout.context.ts +0 -10
- package/src/components/SpecDetailLayout.tsx +0 -14
- package/src/components/SpecsNavSidebar.tsx +0 -615
- package/src/components/StatusBadge.tsx +0 -59
- package/src/components/ThemeToggle.tsx +0 -25
- package/src/components/Tooltip.tsx +0 -29
- package/src/components/context/ContextClient.tsx +0 -471
- package/src/components/context/ContextFileDetail.tsx +0 -163
- package/src/components/dashboard/ActivityItem.tsx +0 -36
- package/src/components/dashboard/DashboardClient.tsx +0 -218
- package/src/components/dashboard/SpecListItem.tsx +0 -58
- package/src/components/dashboard/StatCard.tsx +0 -52
- package/src/components/dependencies/SpecNode.tsx +0 -128
- package/src/components/dependencies/SpecSidebar.tsx +0 -256
- package/src/components/dependencies/constants.ts +0 -25
- package/src/components/dependencies/types.ts +0 -38
- package/src/components/dependencies/utils.ts +0 -261
- package/src/components/metadata-editors/PriorityEditor.tsx +0 -89
- package/src/components/metadata-editors/StatusEditor.tsx +0 -85
- package/src/components/metadata-editors/TagsEditor.tsx +0 -207
- package/src/components/projects/CreateProjectDialog.tsx +0 -162
- package/src/components/projects/DirectoryPicker.tsx +0 -182
- package/src/components/shared/BackToTop.tsx +0 -39
- package/src/components/shared/ColorPicker.tsx +0 -68
- package/src/components/shared/EmptyState.tsx +0 -35
- package/src/components/shared/ErrorBoundary.tsx +0 -79
- package/src/components/shared/PageHeader.tsx +0 -23
- package/src/components/shared/PageTransition.tsx +0 -40
- package/src/components/shared/ProjectAvatar.tsx +0 -107
- package/src/components/shared/Skeletons.tsx +0 -184
- package/src/components/spec-detail/EditableMetadata.tsx +0 -129
- package/src/components/spec-detail/MarkdownRenderer.tsx +0 -47
- package/src/components/spec-detail/TableOfContents.tsx +0 -150
- package/src/components/specs/BoardView.tsx +0 -204
- package/src/components/specs/ListView.tsx +0 -62
- package/src/components/specs/SpecsFilters.tsx +0 -190
- package/src/contexts/KeyboardShortcutsContext.tsx +0 -95
- package/src/contexts/LayoutContext.tsx +0 -45
- package/src/contexts/ProjectContext.tsx +0 -163
- package/src/contexts/ThemeContext.tsx +0 -90
- package/src/contexts/index.ts +0 -7
- package/src/hooks/useKeyboardShortcuts.ts +0 -87
- package/src/index.css +0 -624
- package/src/lib/api.ts +0 -72
- package/src/lib/backend-adapter.ts +0 -382
- package/src/lib/date-utils.ts +0 -122
- package/src/lib/i18n.test.ts +0 -57
- package/src/lib/i18n.ts +0 -51
- package/src/lib/markdown-utils.ts +0 -38
- package/src/lib/sub-spec-utils.ts +0 -166
- package/src/lib/utils.ts +0 -6
- package/src/locales/en/common.json +0 -660
- package/src/locales/en/errors.json +0 -20
- package/src/locales/en/help.json +0 -8
- package/src/locales/zh-CN/common.json +0 -660
- package/src/locales/zh-CN/errors.json +0 -20
- package/src/locales/zh-CN/help.json +0 -8
- package/src/main.tsx +0 -12
- package/src/pages/ContextPage.tsx +0 -111
- package/src/pages/DashboardPage.tsx +0 -97
- package/src/pages/DependenciesPage.tsx +0 -881
- package/src/pages/ProjectsPage.tsx +0 -432
- package/src/pages/SpecDetailPage.tsx +0 -592
- package/src/pages/SpecsPage.tsx +0 -319
- package/src/pages/StatsPage.tsx +0 -307
- package/src/router/projectRoutes.tsx +0 -36
- package/src/router.tsx +0 -33
- package/src/test/setup.ts +0 -39
- package/src/types/api.ts +0 -185
- package/tailwind.config.ts +0 -57
- package/tsconfig.app.json +0 -29
- package/tsconfig.json +0 -7
- package/tsconfig.node.json +0 -26
- package/tsconfig.tsbuildinfo +0 -1
- package/vite.config.ts +0 -27
- package/vitest.config.ts +0 -18
- /package/{public → dist}/favicon.ico +0 -0
- /package/{public → dist}/github-mark-white.svg +0 -0
- /package/{public → dist}/github-mark.svg +0 -0
- /package/{public → dist}/logo-dark-bg.svg +0 -0
- /package/{public → dist}/logo-with-bg.svg +0 -0
- /package/{public → dist}/logo.svg +0 -0
- /package/{public → dist}/vite.svg +0 -0
|
@@ -1,256 +0,0 @@
|
|
|
1
|
-
import { Clock, PlayCircle, CheckCircle2, Archive, AlertCircle, ArrowUp, Minus, ArrowDown } from 'lucide-react';
|
|
2
|
-
import { useTranslation } from 'react-i18next';
|
|
3
|
-
import { cn } from '../../lib/utils';
|
|
4
|
-
import type { SpecNode, FocusedNodeDetails } from './types';
|
|
5
|
-
|
|
6
|
-
const statusIcons = {
|
|
7
|
-
'planned': Clock,
|
|
8
|
-
'in-progress': PlayCircle,
|
|
9
|
-
'complete': CheckCircle2,
|
|
10
|
-
'archived': Archive,
|
|
11
|
-
};
|
|
12
|
-
|
|
13
|
-
const priorityIcons = {
|
|
14
|
-
'critical': AlertCircle,
|
|
15
|
-
'high': ArrowUp,
|
|
16
|
-
'medium': Minus,
|
|
17
|
-
'low': ArrowDown,
|
|
18
|
-
};
|
|
19
|
-
|
|
20
|
-
interface SpecListItemProps {
|
|
21
|
-
spec: SpecNode;
|
|
22
|
-
type: 'upstream' | 'downstream';
|
|
23
|
-
depth: number;
|
|
24
|
-
onClick: () => void;
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
function SpecListItem({ spec, type, depth, onClick }: SpecListItemProps) {
|
|
28
|
-
const { t } = useTranslation();
|
|
29
|
-
const typeColors = {
|
|
30
|
-
upstream: 'border-l-amber-500',
|
|
31
|
-
downstream: 'border-l-emerald-500',
|
|
32
|
-
};
|
|
33
|
-
|
|
34
|
-
const depthLabel = depth === 1
|
|
35
|
-
? t('dependenciesPage.sidebar.depth.direct')
|
|
36
|
-
: t('dependenciesPage.sidebar.depth.level', { depth });
|
|
37
|
-
|
|
38
|
-
const StatusIcon = statusIcons[spec.status as keyof typeof statusIcons] || Clock;
|
|
39
|
-
const PriorityIcon = priorityIcons[spec.priority as keyof typeof priorityIcons] || Minus;
|
|
40
|
-
|
|
41
|
-
return (
|
|
42
|
-
<button
|
|
43
|
-
onClick={onClick}
|
|
44
|
-
className={cn(
|
|
45
|
-
'w-full text-left px-2 py-1.5 rounded border-l-2 bg-muted/30 hover:bg-muted/50 transition-colors',
|
|
46
|
-
typeColors[type]
|
|
47
|
-
)}
|
|
48
|
-
>
|
|
49
|
-
<div className="flex items-center gap-1.5">
|
|
50
|
-
<span className="text-[10px] font-bold text-muted-foreground">
|
|
51
|
-
#{spec.number.toString().padStart(3, '0')}
|
|
52
|
-
</span>
|
|
53
|
-
{/* Status icon */}
|
|
54
|
-
<div
|
|
55
|
-
className={cn(
|
|
56
|
-
'rounded p-0.5 flex items-center justify-center',
|
|
57
|
-
spec.status === 'planned' && 'bg-blue-500/20',
|
|
58
|
-
spec.status === 'in-progress' && 'bg-orange-500/20',
|
|
59
|
-
spec.status === 'complete' && 'bg-green-500/20',
|
|
60
|
-
spec.status === 'archived' && 'bg-gray-500/20'
|
|
61
|
-
)}
|
|
62
|
-
title={t(`status.${spec.status}`)}
|
|
63
|
-
>
|
|
64
|
-
<StatusIcon
|
|
65
|
-
className={cn(
|
|
66
|
-
'h-2.5 w-2.5',
|
|
67
|
-
spec.status === 'planned' && 'text-blue-600 dark:text-blue-400',
|
|
68
|
-
spec.status === 'in-progress' && 'text-orange-600 dark:text-orange-400',
|
|
69
|
-
spec.status === 'complete' && 'text-green-600 dark:text-green-400',
|
|
70
|
-
spec.status === 'archived' && 'text-gray-500 dark:text-gray-400'
|
|
71
|
-
)}
|
|
72
|
-
/>
|
|
73
|
-
</div>
|
|
74
|
-
{/* Priority icon */}
|
|
75
|
-
<div
|
|
76
|
-
className={cn(
|
|
77
|
-
'rounded p-0.5 flex items-center justify-center',
|
|
78
|
-
spec.priority === 'critical' && 'bg-red-500/20',
|
|
79
|
-
spec.priority === 'high' && 'bg-orange-500/20',
|
|
80
|
-
spec.priority === 'medium' && 'bg-blue-500/20',
|
|
81
|
-
spec.priority === 'low' && 'bg-gray-500/20'
|
|
82
|
-
)}
|
|
83
|
-
title={spec.priority ? t(`priority.${spec.priority}`) : undefined}
|
|
84
|
-
>
|
|
85
|
-
<PriorityIcon
|
|
86
|
-
className={cn(
|
|
87
|
-
'h-2.5 w-2.5',
|
|
88
|
-
spec.priority === 'critical' && 'text-red-600 dark:text-red-400',
|
|
89
|
-
spec.priority === 'high' && 'text-orange-600 dark:text-orange-400',
|
|
90
|
-
spec.priority === 'medium' && 'text-blue-600 dark:text-blue-400',
|
|
91
|
-
spec.priority === 'low' && 'text-gray-500 dark:text-gray-400'
|
|
92
|
-
)}
|
|
93
|
-
/>
|
|
94
|
-
</div>
|
|
95
|
-
<span className="text-[8px] px-1 py-0.5 rounded bg-muted text-muted-foreground font-medium ml-auto">
|
|
96
|
-
{depthLabel}
|
|
97
|
-
</span>
|
|
98
|
-
</div>
|
|
99
|
-
<p className="text-[11px] text-foreground truncate leading-tight mt-0.5">{spec.name}</p>
|
|
100
|
-
</button>
|
|
101
|
-
);
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
interface SpecSidebarProps {
|
|
105
|
-
focusedDetails: FocusedNodeDetails | null;
|
|
106
|
-
onSelectSpec: (specId: string) => void;
|
|
107
|
-
onOpenSpec: (specNumber: number) => void;
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
export function SpecSidebar({ focusedDetails, onSelectSpec, onOpenSpec }: SpecSidebarProps) {
|
|
111
|
-
const { t } = useTranslation();
|
|
112
|
-
if (!focusedDetails) {
|
|
113
|
-
return (
|
|
114
|
-
<div className="w-64 shrink-0 rounded-lg border border-border bg-background/95 overflow-hidden flex flex-col">
|
|
115
|
-
<div className="flex-1 flex items-center justify-center p-4">
|
|
116
|
-
<div className="text-center text-muted-foreground">
|
|
117
|
-
<div className="w-12 h-12 mx-auto mb-3 rounded-full bg-muted/50 flex items-center justify-center">
|
|
118
|
-
<svg className="w-6 h-6" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
|
119
|
-
<path
|
|
120
|
-
strokeLinecap="round"
|
|
121
|
-
strokeLinejoin="round"
|
|
122
|
-
strokeWidth={1.5}
|
|
123
|
-
d="M15 15l-2 5L9 9l11 4-5 2zm0 0l5 5M7.188 2.239l.777 2.897M5.136 7.965l-2.898-.777M13.95 4.05l-2.122 2.122m-5.657 5.656l-2.12 2.122"
|
|
124
|
-
/>
|
|
125
|
-
</svg>
|
|
126
|
-
</div>
|
|
127
|
-
<p className="text-sm font-medium">{t('dependenciesPage.sidebar.emptyTitle')}</p>
|
|
128
|
-
<p className="text-xs mt-1">{t('dependenciesPage.sidebar.emptyDescription')}</p>
|
|
129
|
-
</div>
|
|
130
|
-
</div>
|
|
131
|
-
</div>
|
|
132
|
-
);
|
|
133
|
-
}
|
|
134
|
-
|
|
135
|
-
const { node, upstream, downstream } = focusedDetails;
|
|
136
|
-
|
|
137
|
-
const StatusIcon = statusIcons[node.status as keyof typeof statusIcons] || Clock;
|
|
138
|
-
const PriorityIcon = priorityIcons[node.priority as keyof typeof priorityIcons] || Minus;
|
|
139
|
-
|
|
140
|
-
return (
|
|
141
|
-
<div className="w-64 shrink-0 rounded-lg border border-border bg-background/95 overflow-hidden flex flex-col">
|
|
142
|
-
{/* Selected spec header */}
|
|
143
|
-
<div className="p-3 border-b border-border bg-muted/30">
|
|
144
|
-
<div className="flex items-center gap-2 mb-1">
|
|
145
|
-
<span className="font-bold text-sm">#{node.number.toString().padStart(3, '0')}</span>
|
|
146
|
-
{/* Status icon */}
|
|
147
|
-
<div
|
|
148
|
-
className={cn(
|
|
149
|
-
'rounded p-1 flex items-center justify-center',
|
|
150
|
-
node.status === 'planned' && 'bg-blue-500/20',
|
|
151
|
-
node.status === 'in-progress' && 'bg-orange-500/20',
|
|
152
|
-
node.status === 'complete' && 'bg-green-500/20',
|
|
153
|
-
node.status === 'archived' && 'bg-gray-500/20'
|
|
154
|
-
)}
|
|
155
|
-
title={node.status}
|
|
156
|
-
>
|
|
157
|
-
<StatusIcon
|
|
158
|
-
className={cn(
|
|
159
|
-
'h-3 w-3',
|
|
160
|
-
node.status === 'planned' && 'text-blue-600 dark:text-blue-300',
|
|
161
|
-
node.status === 'in-progress' && 'text-orange-600 dark:text-orange-300',
|
|
162
|
-
node.status === 'complete' && 'text-green-600 dark:text-green-300',
|
|
163
|
-
node.status === 'archived' && 'text-gray-500 dark:text-gray-300'
|
|
164
|
-
)}
|
|
165
|
-
/>
|
|
166
|
-
</div>
|
|
167
|
-
{/* Priority icon */}
|
|
168
|
-
<div
|
|
169
|
-
className={cn(
|
|
170
|
-
'rounded p-1 flex items-center justify-center',
|
|
171
|
-
node.priority === 'critical' && 'bg-red-500/20',
|
|
172
|
-
node.priority === 'high' && 'bg-orange-500/20',
|
|
173
|
-
node.priority === 'medium' && 'bg-blue-500/20',
|
|
174
|
-
node.priority === 'low' && 'bg-gray-500/20'
|
|
175
|
-
)}
|
|
176
|
-
title={node.priority}
|
|
177
|
-
>
|
|
178
|
-
<PriorityIcon
|
|
179
|
-
className={cn(
|
|
180
|
-
'h-3 w-3',
|
|
181
|
-
node.priority === 'critical' && 'text-red-600 dark:text-red-300',
|
|
182
|
-
node.priority === 'high' && 'text-orange-600 dark:text-orange-300',
|
|
183
|
-
node.priority === 'medium' && 'text-blue-600 dark:text-blue-300',
|
|
184
|
-
node.priority === 'low' && 'text-gray-500 dark:text-gray-300'
|
|
185
|
-
)}
|
|
186
|
-
/>
|
|
187
|
-
</div>
|
|
188
|
-
</div>
|
|
189
|
-
<p className="text-sm font-medium text-foreground leading-snug">{node.name}</p>
|
|
190
|
-
<button
|
|
191
|
-
onClick={() => onOpenSpec(node.number)}
|
|
192
|
-
className="mt-2 w-full rounded bg-primary/20 border border-primary/40 px-2 py-1.5 text-xs text-primary hover:bg-primary/30 font-medium"
|
|
193
|
-
>
|
|
194
|
-
{t('dependenciesPage.sidebar.openSpec')}
|
|
195
|
-
</button>
|
|
196
|
-
</div>
|
|
197
|
-
|
|
198
|
-
{/* Scrollable spec lists */}
|
|
199
|
-
<div className="flex-1 overflow-auto p-3 space-y-4">
|
|
200
|
-
{/* Upstream Dependencies */}
|
|
201
|
-
<div>
|
|
202
|
-
<div className="flex items-center gap-2 mb-2">
|
|
203
|
-
<span className="inline-block w-2 h-2 rounded-full bg-amber-500" />
|
|
204
|
-
<span className="text-xs font-semibold text-muted-foreground uppercase tracking-wide">
|
|
205
|
-
{t('dependenciesPage.sidebar.dependsOnHeading', { count: upstream.reduce((sum, g) => sum + g.specs.length, 0) })}
|
|
206
|
-
</span>
|
|
207
|
-
</div>
|
|
208
|
-
{upstream.length > 0 ? (
|
|
209
|
-
<div className="space-y-1.5">
|
|
210
|
-
{upstream.flatMap((group) =>
|
|
211
|
-
group.specs.map((spec) => (
|
|
212
|
-
<SpecListItem
|
|
213
|
-
key={spec.id}
|
|
214
|
-
spec={spec}
|
|
215
|
-
type="upstream"
|
|
216
|
-
depth={group.depth}
|
|
217
|
-
onClick={() => onSelectSpec(spec.id)}
|
|
218
|
-
/>
|
|
219
|
-
))
|
|
220
|
-
)}
|
|
221
|
-
</div>
|
|
222
|
-
) : (
|
|
223
|
-
<p className="text-xs text-muted-foreground/60 italic">{t('dependenciesPage.sidebar.emptyUpstream')}</p>
|
|
224
|
-
)}
|
|
225
|
-
</div>
|
|
226
|
-
|
|
227
|
-
{/* Downstream Dependents */}
|
|
228
|
-
<div>
|
|
229
|
-
<div className="flex items-center gap-2 mb-2">
|
|
230
|
-
<span className="inline-block w-2 h-2 rounded-full bg-emerald-500" />
|
|
231
|
-
<span className="text-xs font-semibold text-muted-foreground uppercase tracking-wide">
|
|
232
|
-
{t('dependenciesPage.sidebar.requiredByHeading', { count: downstream.reduce((sum, g) => sum + g.specs.length, 0) })}
|
|
233
|
-
</span>
|
|
234
|
-
</div>
|
|
235
|
-
{downstream.length > 0 ? (
|
|
236
|
-
<div className="space-y-1.5">
|
|
237
|
-
{downstream.flatMap((group) =>
|
|
238
|
-
group.specs.map((spec) => (
|
|
239
|
-
<SpecListItem
|
|
240
|
-
key={spec.id}
|
|
241
|
-
spec={spec}
|
|
242
|
-
type="downstream"
|
|
243
|
-
depth={group.depth}
|
|
244
|
-
onClick={() => onSelectSpec(spec.id)}
|
|
245
|
-
/>
|
|
246
|
-
))
|
|
247
|
-
)}
|
|
248
|
-
</div>
|
|
249
|
-
) : (
|
|
250
|
-
<p className="text-xs text-muted-foreground/60 italic">{t('dependenciesPage.sidebar.emptyDownstream')}</p>
|
|
251
|
-
)}
|
|
252
|
-
</div>
|
|
253
|
-
</div>
|
|
254
|
-
</div>
|
|
255
|
-
);
|
|
256
|
-
}
|
|
@@ -1,25 +0,0 @@
|
|
|
1
|
-
// Node dimensions
|
|
2
|
-
export const NODE_WIDTH = 180;
|
|
3
|
-
export const NODE_HEIGHT = 60;
|
|
4
|
-
export const COMPACT_NODE_WIDTH = 120;
|
|
5
|
-
export const COMPACT_NODE_HEIGHT = 40;
|
|
6
|
-
|
|
7
|
-
// Edge colors
|
|
8
|
-
export const DEPENDS_ON_COLOR = '#f59e0b';
|
|
9
|
-
|
|
10
|
-
// Status tone classes for node styling (aligned with StatusBadge component)
|
|
11
|
-
// Using light/dark mode compatible colors
|
|
12
|
-
export const toneClasses: Record<string, string> = {
|
|
13
|
-
planned: 'border-blue-500 bg-blue-100 text-blue-800 dark:bg-blue-950/60 dark:text-blue-200',
|
|
14
|
-
'in-progress': 'border-orange-500 bg-orange-100 text-orange-800 dark:bg-orange-950/60 dark:text-orange-200',
|
|
15
|
-
complete: 'border-green-500 bg-green-100 text-green-800 dark:bg-green-950/60 dark:text-green-200',
|
|
16
|
-
archived: 'border-gray-400 bg-gray-100 text-gray-600 dark:border-gray-500/80 dark:bg-gray-900/60 dark:text-gray-400',
|
|
17
|
-
};
|
|
18
|
-
|
|
19
|
-
// Background colors for minimap (aligned with StatusBadge component)
|
|
20
|
-
export const toneBgColors: Record<string, string> = {
|
|
21
|
-
planned: '#3b82f6', // blue-500 (works in both themes)
|
|
22
|
-
'in-progress': '#f97316', // orange-500
|
|
23
|
-
complete: '#22c55e', // green-500
|
|
24
|
-
archived: '#6b7280', // gray-500
|
|
25
|
-
};
|
|
@@ -1,38 +0,0 @@
|
|
|
1
|
-
import type { DependencyGraph } from '../../types/api';
|
|
2
|
-
|
|
3
|
-
export type GraphTone = 'planned' | 'in-progress' | 'complete' | 'archived';
|
|
4
|
-
|
|
5
|
-
export interface SpecNodeData {
|
|
6
|
-
label: string;
|
|
7
|
-
shortLabel: string;
|
|
8
|
-
badge: string;
|
|
9
|
-
number: number;
|
|
10
|
-
tone: GraphTone;
|
|
11
|
-
priority: string;
|
|
12
|
-
href?: string;
|
|
13
|
-
interactive?: boolean;
|
|
14
|
-
isFocused?: boolean;
|
|
15
|
-
connectionDepth?: number;
|
|
16
|
-
isDimmed?: boolean;
|
|
17
|
-
isCompact?: boolean;
|
|
18
|
-
isSecondary?: boolean; // Shown due to critical path, not primary filter
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
export type SpecNode = DependencyGraph['nodes'][0];
|
|
22
|
-
|
|
23
|
-
// Specs grouped by their depth level from the focused node
|
|
24
|
-
export interface SpecsByDepth {
|
|
25
|
-
depth: number;
|
|
26
|
-
specs: SpecNode[];
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
export interface FocusedNodeDetails {
|
|
30
|
-
node: SpecNode;
|
|
31
|
-
upstream: SpecsByDepth[]; // All transitive deps grouped by depth
|
|
32
|
-
downstream: SpecsByDepth[]; // All transitive dependents grouped by depth
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
export interface ConnectionStats {
|
|
36
|
-
connected: number;
|
|
37
|
-
standalone: number;
|
|
38
|
-
}
|
|
@@ -1,261 +0,0 @@
|
|
|
1
|
-
import dagre from '@dagrejs/dagre';
|
|
2
|
-
import type { Node, Edge } from 'reactflow';
|
|
3
|
-
import type { SpecNodeData } from './types';
|
|
4
|
-
import {
|
|
5
|
-
NODE_WIDTH,
|
|
6
|
-
NODE_HEIGHT,
|
|
7
|
-
COMPACT_NODE_WIDTH,
|
|
8
|
-
COMPACT_NODE_HEIGHT,
|
|
9
|
-
} from './constants';
|
|
10
|
-
|
|
11
|
-
/**
|
|
12
|
-
* Get nodes at various depths from a starting node (directional BFS)
|
|
13
|
-
* Only includes upstream (specs this depends on) and downstream (specs that depend on this)
|
|
14
|
-
* Edge direction: source depends_on target (A→B means A depends on B)
|
|
15
|
-
*/
|
|
16
|
-
export function getConnectionDepths(
|
|
17
|
-
startId: string,
|
|
18
|
-
edges: Array<{ source: string; target: string }>,
|
|
19
|
-
maxDepth: number = 2
|
|
20
|
-
): Map<string, number> {
|
|
21
|
-
const depths = new Map<string, number>();
|
|
22
|
-
depths.set(startId, 0);
|
|
23
|
-
|
|
24
|
-
// Build directional adjacency maps
|
|
25
|
-
// upstreamMap: source → targets (specs that source depends on)
|
|
26
|
-
// downstreamMap: target → sources (specs that depend on target)
|
|
27
|
-
const upstreamMap = new Map<string, Set<string>>();
|
|
28
|
-
const downstreamMap = new Map<string, Set<string>>();
|
|
29
|
-
|
|
30
|
-
edges.forEach((e) => {
|
|
31
|
-
// source depends on target, so target is upstream of source
|
|
32
|
-
if (!upstreamMap.has(e.source)) upstreamMap.set(e.source, new Set());
|
|
33
|
-
upstreamMap.get(e.source)!.add(e.target);
|
|
34
|
-
|
|
35
|
-
// source depends on target, so source is downstream of target
|
|
36
|
-
if (!downstreamMap.has(e.target)) downstreamMap.set(e.target, new Set());
|
|
37
|
-
downstreamMap.get(e.target)!.add(e.source);
|
|
38
|
-
});
|
|
39
|
-
|
|
40
|
-
// BFS upstream (specs this depends on, directly or transitively)
|
|
41
|
-
let currentLevel = new Set([startId]);
|
|
42
|
-
let depth = 1;
|
|
43
|
-
while (currentLevel.size > 0 && depth <= maxDepth) {
|
|
44
|
-
const nextLevel = new Set<string>();
|
|
45
|
-
currentLevel.forEach((nodeId) => {
|
|
46
|
-
const upstreamNodes = upstreamMap.get(nodeId);
|
|
47
|
-
if (upstreamNodes) {
|
|
48
|
-
upstreamNodes.forEach((upstream) => {
|
|
49
|
-
if (!depths.has(upstream)) {
|
|
50
|
-
depths.set(upstream, depth);
|
|
51
|
-
nextLevel.add(upstream);
|
|
52
|
-
}
|
|
53
|
-
});
|
|
54
|
-
}
|
|
55
|
-
});
|
|
56
|
-
currentLevel = nextLevel;
|
|
57
|
-
depth++;
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
// BFS downstream (specs that depend on this, directly or transitively)
|
|
61
|
-
currentLevel = new Set([startId]);
|
|
62
|
-
depth = 1;
|
|
63
|
-
while (currentLevel.size > 0 && depth <= maxDepth) {
|
|
64
|
-
const nextLevel = new Set<string>();
|
|
65
|
-
currentLevel.forEach((nodeId) => {
|
|
66
|
-
const downstreamNodes = downstreamMap.get(nodeId);
|
|
67
|
-
if (downstreamNodes) {
|
|
68
|
-
downstreamNodes.forEach((downstream) => {
|
|
69
|
-
if (!depths.has(downstream)) {
|
|
70
|
-
depths.set(downstream, depth);
|
|
71
|
-
nextLevel.add(downstream);
|
|
72
|
-
}
|
|
73
|
-
});
|
|
74
|
-
}
|
|
75
|
-
});
|
|
76
|
-
currentLevel = nextLevel;
|
|
77
|
-
depth++;
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
return depths;
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
/**
|
|
84
|
-
* Layout the graph using dagre (hierarchical DAG layout)
|
|
85
|
-
*/
|
|
86
|
-
export function layoutGraph(
|
|
87
|
-
nodes: Node<SpecNodeData>[],
|
|
88
|
-
edges: Edge[],
|
|
89
|
-
isCompact: boolean,
|
|
90
|
-
showStandalone: boolean,
|
|
91
|
-
options: {
|
|
92
|
-
mode?: 'graph' | 'focus';
|
|
93
|
-
focusedNodeId?: string | null;
|
|
94
|
-
upstreamIds?: Set<string>;
|
|
95
|
-
downstreamIds?: Set<string>;
|
|
96
|
-
} = {}
|
|
97
|
-
): { nodes: Node<SpecNodeData>[]; edges: Edge[] } {
|
|
98
|
-
if (nodes.length === 0) return { nodes: [], edges: [] };
|
|
99
|
-
|
|
100
|
-
const mode = options.mode ?? 'graph';
|
|
101
|
-
|
|
102
|
-
if (mode === 'focus' && options.focusedNodeId) {
|
|
103
|
-
return layeredLayout(nodes, edges, isCompact);
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
const width = isCompact ? COMPACT_NODE_WIDTH : NODE_WIDTH;
|
|
107
|
-
const height = isCompact ? COMPACT_NODE_HEIGHT : NODE_HEIGHT;
|
|
108
|
-
const gap = isCompact ? 30 : 50;
|
|
109
|
-
|
|
110
|
-
// Separate nodes with dependencies from standalone nodes
|
|
111
|
-
const nodesWithDeps = new Set<string>();
|
|
112
|
-
edges.forEach((e) => {
|
|
113
|
-
nodesWithDeps.add(e.source);
|
|
114
|
-
nodesWithDeps.add(e.target);
|
|
115
|
-
});
|
|
116
|
-
|
|
117
|
-
const connectedNodes = nodes.filter((n) => nodesWithDeps.has(n.id));
|
|
118
|
-
const standaloneNodes = showStandalone ? nodes.filter((n) => !nodesWithDeps.has(n.id)) : [];
|
|
119
|
-
|
|
120
|
-
const allLayoutedNodes: Node<SpecNodeData>[] = [];
|
|
121
|
-
|
|
122
|
-
// DAG view: Layout connected nodes with dagre (left-to-right for dependency flow)
|
|
123
|
-
if (connectedNodes.length > 0) {
|
|
124
|
-
const graph = new dagre.graphlib.Graph();
|
|
125
|
-
graph.setGraph({
|
|
126
|
-
rankdir: 'LR',
|
|
127
|
-
align: 'UL',
|
|
128
|
-
nodesep: isCompact ? 30 : 50,
|
|
129
|
-
ranksep: isCompact ? 80 : 120,
|
|
130
|
-
marginx: 40,
|
|
131
|
-
marginy: 40,
|
|
132
|
-
});
|
|
133
|
-
graph.setDefaultEdgeLabel(() => ({}));
|
|
134
|
-
|
|
135
|
-
connectedNodes.forEach((node) => {
|
|
136
|
-
graph.setNode(node.id, { width, height });
|
|
137
|
-
});
|
|
138
|
-
edges.forEach((edge) => {
|
|
139
|
-
if (nodesWithDeps.has(edge.source) && nodesWithDeps.has(edge.target)) {
|
|
140
|
-
graph.setEdge(edge.source, edge.target);
|
|
141
|
-
}
|
|
142
|
-
});
|
|
143
|
-
|
|
144
|
-
dagre.layout(graph);
|
|
145
|
-
|
|
146
|
-
// Find bounds for centering
|
|
147
|
-
let minX = Infinity, minY = Infinity, maxX = 0, maxY = 0;
|
|
148
|
-
connectedNodes.forEach((node) => {
|
|
149
|
-
const pos = graph.node(node.id);
|
|
150
|
-
minX = Math.min(minX, pos.x - width / 2);
|
|
151
|
-
minY = Math.min(minY, pos.y - height / 2);
|
|
152
|
-
maxX = Math.max(maxX, pos.x + width / 2);
|
|
153
|
-
maxY = Math.max(maxY, pos.y + height / 2);
|
|
154
|
-
});
|
|
155
|
-
|
|
156
|
-
connectedNodes.forEach((node) => {
|
|
157
|
-
const pos = graph.node(node.id);
|
|
158
|
-
allLayoutedNodes.push({
|
|
159
|
-
...node,
|
|
160
|
-
position: {
|
|
161
|
-
x: pos.x - minX,
|
|
162
|
-
y: pos.y - minY,
|
|
163
|
-
},
|
|
164
|
-
});
|
|
165
|
-
});
|
|
166
|
-
|
|
167
|
-
// Layout standalone nodes in a grid below the graph
|
|
168
|
-
if (standaloneNodes.length > 0) {
|
|
169
|
-
const graphHeight = maxY - minY;
|
|
170
|
-
const graphWidth = maxX - minX;
|
|
171
|
-
const gridStartY = graphHeight + gap * 2;
|
|
172
|
-
const cols = Math.ceil(Math.sqrt(standaloneNodes.length * 1.5));
|
|
173
|
-
const gridWidth = cols * (width + gap);
|
|
174
|
-
const gridStartX = graphWidth > gridWidth ? Math.floor((graphWidth - gridWidth) / 2) : 0;
|
|
175
|
-
|
|
176
|
-
standaloneNodes.forEach((node, i) => {
|
|
177
|
-
const col = i % cols;
|
|
178
|
-
const row = Math.floor(i / cols);
|
|
179
|
-
allLayoutedNodes.push({
|
|
180
|
-
...node,
|
|
181
|
-
position: {
|
|
182
|
-
x: gridStartX + col * (width + gap),
|
|
183
|
-
y: gridStartY + row * (height + gap),
|
|
184
|
-
},
|
|
185
|
-
});
|
|
186
|
-
});
|
|
187
|
-
}
|
|
188
|
-
} else {
|
|
189
|
-
// Only standalone nodes - arrange in a grid
|
|
190
|
-
const cols = Math.ceil(Math.sqrt(standaloneNodes.length * 1.5));
|
|
191
|
-
|
|
192
|
-
standaloneNodes.forEach((node, i) => {
|
|
193
|
-
const col = i % cols;
|
|
194
|
-
const row = Math.floor(i / cols);
|
|
195
|
-
allLayoutedNodes.push({
|
|
196
|
-
...node,
|
|
197
|
-
position: {
|
|
198
|
-
x: col * (width + gap),
|
|
199
|
-
y: row * (height + gap),
|
|
200
|
-
},
|
|
201
|
-
});
|
|
202
|
-
});
|
|
203
|
-
}
|
|
204
|
-
|
|
205
|
-
return { nodes: allLayoutedNodes, edges };
|
|
206
|
-
}
|
|
207
|
-
|
|
208
|
-
function layeredLayout(
|
|
209
|
-
nodes: Node<SpecNodeData>[],
|
|
210
|
-
edges: Edge[],
|
|
211
|
-
isCompact: boolean,
|
|
212
|
-
): { nodes: Node<SpecNodeData>[]; edges: Edge[] } {
|
|
213
|
-
// Use dagre for consistent hierarchical layout
|
|
214
|
-
// This preserves the structure of complex dependency chains (A->B->C)
|
|
215
|
-
// instead of flattening them into just "upstream" and "downstream" buckets
|
|
216
|
-
|
|
217
|
-
const width = isCompact ? COMPACT_NODE_WIDTH : NODE_WIDTH;
|
|
218
|
-
const height = isCompact ? COMPACT_NODE_HEIGHT : NODE_HEIGHT;
|
|
219
|
-
|
|
220
|
-
const graph = new dagre.graphlib.Graph();
|
|
221
|
-
graph.setGraph({
|
|
222
|
-
rankdir: 'LR', // Consistent with main graph
|
|
223
|
-
align: 'UL',
|
|
224
|
-
nodesep: isCompact ? 30 : 50,
|
|
225
|
-
ranksep: isCompact ? 80 : 120,
|
|
226
|
-
marginx: 40,
|
|
227
|
-
marginy: 40,
|
|
228
|
-
});
|
|
229
|
-
graph.setDefaultEdgeLabel(() => ({}));
|
|
230
|
-
|
|
231
|
-
nodes.forEach((node) => {
|
|
232
|
-
graph.setNode(node.id, { width, height });
|
|
233
|
-
});
|
|
234
|
-
|
|
235
|
-
edges.forEach((edge) => {
|
|
236
|
-
graph.setEdge(edge.source, edge.target);
|
|
237
|
-
});
|
|
238
|
-
|
|
239
|
-
dagre.layout(graph);
|
|
240
|
-
|
|
241
|
-
// Find bounds to normalize coordinates (start at 0,0)
|
|
242
|
-
let minX = Infinity, minY = Infinity;
|
|
243
|
-
nodes.forEach((node) => {
|
|
244
|
-
const pos = graph.node(node.id);
|
|
245
|
-
minX = Math.min(minX, pos.x - width / 2);
|
|
246
|
-
minY = Math.min(minY, pos.y - height / 2);
|
|
247
|
-
});
|
|
248
|
-
|
|
249
|
-
const layoutedNodes = nodes.map((node) => {
|
|
250
|
-
const pos = graph.node(node.id);
|
|
251
|
-
return {
|
|
252
|
-
...node,
|
|
253
|
-
position: {
|
|
254
|
-
x: pos.x - minX,
|
|
255
|
-
y: pos.y - minY,
|
|
256
|
-
},
|
|
257
|
-
};
|
|
258
|
-
});
|
|
259
|
-
|
|
260
|
-
return { nodes: layoutedNodes, edges };
|
|
261
|
-
}
|
|
@@ -1,89 +0,0 @@
|
|
|
1
|
-
import { useState } from 'react';
|
|
2
|
-
import { AlertCircle, ArrowDown, ArrowUp, Loader2, Minus } from 'lucide-react';
|
|
3
|
-
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@leanspec/ui-components';
|
|
4
|
-
import { cn } from '../../lib/utils';
|
|
5
|
-
import { api } from '../../lib/api';
|
|
6
|
-
import type { Spec } from '../../types/api';
|
|
7
|
-
import { useTranslation } from 'react-i18next';
|
|
8
|
-
|
|
9
|
-
const PRIORITY_OPTIONS: Array<{ value: NonNullable<Spec['priority']>; labelKey: `priority.${string}`; className: string; Icon: React.ComponentType<{ className?: string }> }> = [
|
|
10
|
-
{ value: 'critical', labelKey: 'priority.critical', className: 'bg-red-100 text-red-800 dark:bg-red-900/30 dark:text-red-400', Icon: AlertCircle },
|
|
11
|
-
{ value: 'high', labelKey: 'priority.high', className: 'bg-orange-100 text-orange-800 dark:bg-orange-900/30 dark:text-orange-400', Icon: ArrowUp },
|
|
12
|
-
{ value: 'medium', labelKey: 'priority.medium', className: 'bg-blue-100 text-blue-800 dark:bg-blue-900/30 dark:text-blue-400', Icon: Minus },
|
|
13
|
-
{ value: 'low', labelKey: 'priority.low', className: 'bg-gray-100 text-gray-800 dark:bg-gray-800 dark:text-gray-400', Icon: ArrowDown },
|
|
14
|
-
];
|
|
15
|
-
|
|
16
|
-
interface PriorityEditorProps {
|
|
17
|
-
specName: string;
|
|
18
|
-
value: Spec['priority'];
|
|
19
|
-
onChange?: (priority: NonNullable<Spec['priority']>) => void;
|
|
20
|
-
disabled?: boolean;
|
|
21
|
-
className?: string;
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
export function PriorityEditor({ specName, value, onChange, disabled = false, className }: PriorityEditorProps) {
|
|
25
|
-
const initial = value || 'medium';
|
|
26
|
-
const [priority, setPriority] = useState<NonNullable<Spec['priority']>>(initial as NonNullable<Spec['priority']>);
|
|
27
|
-
const [updating, setUpdating] = useState(false);
|
|
28
|
-
const [error, setError] = useState<string | null>(null);
|
|
29
|
-
const { t } = useTranslation('common');
|
|
30
|
-
|
|
31
|
-
const option = PRIORITY_OPTIONS.find((opt) => opt.value === priority) || PRIORITY_OPTIONS[1];
|
|
32
|
-
|
|
33
|
-
const handleChange = async (next: NonNullable<Spec['priority']>) => {
|
|
34
|
-
if (next === priority) return;
|
|
35
|
-
const previous = priority;
|
|
36
|
-
setPriority(next);
|
|
37
|
-
setUpdating(true);
|
|
38
|
-
setError(null);
|
|
39
|
-
|
|
40
|
-
try {
|
|
41
|
-
await api.updateSpec(specName, { priority: next });
|
|
42
|
-
onChange?.(next);
|
|
43
|
-
} catch (err) {
|
|
44
|
-
setPriority(previous);
|
|
45
|
-
const message = err instanceof Error ? err.message : t('editors.priorityError');
|
|
46
|
-
setError(message);
|
|
47
|
-
} finally {
|
|
48
|
-
setUpdating(false);
|
|
49
|
-
}
|
|
50
|
-
};
|
|
51
|
-
|
|
52
|
-
return (
|
|
53
|
-
<div className="space-y-1">
|
|
54
|
-
<Select
|
|
55
|
-
value={priority}
|
|
56
|
-
onValueChange={(value) => handleChange(value as NonNullable<Spec['priority']>)}
|
|
57
|
-
disabled={disabled || updating}
|
|
58
|
-
>
|
|
59
|
-
<SelectTrigger
|
|
60
|
-
className={cn(
|
|
61
|
-
'h-7 w-fit min-w-[100px] border-0 px-2 text-xs font-medium justify-start',
|
|
62
|
-
option.className,
|
|
63
|
-
className,
|
|
64
|
-
updating && 'opacity-70'
|
|
65
|
-
)}
|
|
66
|
-
aria-label={t('editors.changePriority')}
|
|
67
|
-
>
|
|
68
|
-
<div className="flex items-center gap-1.5">
|
|
69
|
-
{updating ? <Loader2 className="h-3.5 w-3.5 animate-spin" /> : <option.Icon className="h-3.5 w-3.5" />}
|
|
70
|
-
<SelectValue placeholder={t('specsPage.filters.priority')}>
|
|
71
|
-
{t(option.labelKey)}
|
|
72
|
-
</SelectValue>
|
|
73
|
-
</div>
|
|
74
|
-
</SelectTrigger>
|
|
75
|
-
<SelectContent>
|
|
76
|
-
{PRIORITY_OPTIONS.map((opt) => (
|
|
77
|
-
<SelectItem key={opt.value} value={opt.value} className="flex items-center gap-2">
|
|
78
|
-
<div className="flex items-center gap-2">
|
|
79
|
-
<opt.Icon className="h-4 w-4" />
|
|
80
|
-
<span>{t(opt.labelKey)}</span>
|
|
81
|
-
</div>
|
|
82
|
-
</SelectItem>
|
|
83
|
-
))}
|
|
84
|
-
</SelectContent>
|
|
85
|
-
</Select>
|
|
86
|
-
{error && <p className="text-xs text-destructive">{error}</p>}
|
|
87
|
-
</div>
|
|
88
|
-
);
|
|
89
|
-
}
|