@leanspec/ui 0.2.14 → 0.2.15-dev.21025278490
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 +191 -0
- package/dist/assets/_baseUniq-CRqreL7N.js +1 -0
- package/dist/assets/arc-DMhx9AJT.js +1 -0
- package/dist/assets/architectureDiagram-VXUJARFQ-DM0L0YzO.js +36 -0
- package/dist/assets/blockDiagram-VD42YOAC-DHQXDHsD.js +122 -0
- package/dist/assets/c4Diagram-YG6GDRKO-0L7o2gpH.js +10 -0
- package/dist/assets/channel-2tOl0nAZ.js +1 -0
- package/dist/assets/chunk-4BX2VUAB-CwFT-Uaj.js +1 -0
- package/dist/assets/chunk-55IACEB6-CjvuUHHG.js +1 -0
- package/dist/assets/chunk-B4BG7PRW-BRJBysMK.js +165 -0
- package/dist/assets/chunk-DI55MBZ5-BnNEeoaA.js +220 -0
- package/dist/assets/chunk-FMBD7UC4-BK2l30pm.js +15 -0
- package/dist/assets/chunk-QN33PNHL-BN_cZkCU.js +1 -0
- package/dist/assets/chunk-QZHKN3VN-Brc3Yrub.js +1 -0
- package/dist/assets/chunk-TZMSLE5B-D2zzpLfO.js +1 -0
- package/dist/assets/classDiagram-2ON5EDUG-BB9CSNmS.js +1 -0
- package/dist/assets/classDiagram-v2-WZHVMYZB-BB9CSNmS.js +1 -0
- package/dist/assets/clone-BjxVFtyI.js +1 -0
- package/dist/assets/core-DV6XEvTN.js +1 -0
- package/dist/assets/cose-bilkent-S5V4N54A-CLJgM3XR.js +1 -0
- package/dist/assets/cytoscape.esm-5J0xJHOV.js +321 -0
- package/dist/assets/dagre-6UL2VRFP-_IFvBJKJ.js +4 -0
- package/dist/assets/diagram-PSM6KHXK--83HIYSQ.js +24 -0
- package/dist/assets/diagram-QEK2KX5R-6jAWnCnZ.js +43 -0
- package/dist/assets/diagram-S2PKOQOG-D5pwHvjZ.js +24 -0
- package/dist/assets/erDiagram-Q2GNP2WA-B4FV3mTd.js +60 -0
- package/dist/assets/flowDiagram-NV44I4VS-mtD2kF4M.js +162 -0
- package/dist/assets/ganttDiagram-JELNMOA3-BKALgqTK.js +267 -0
- package/dist/assets/gitGraphDiagram-NY62KEGX-Bd7r0pAf.js +65 -0
- package/dist/assets/graph-B2rEI7cK.js +1 -0
- package/dist/assets/index-Bekv_o1t.css +1 -0
- package/dist/assets/index-DSRxU-E5.js +389 -0
- package/dist/assets/infoDiagram-WHAUD3N6--nJOBKqh.js +2 -0
- package/dist/assets/journeyDiagram-XKPGCS4Q-BzGutKN3.js +139 -0
- package/dist/assets/kanban-definition-3W4ZIXB7-DyQO17vq.js +89 -0
- package/dist/assets/katex-XbL3y5x-.js +261 -0
- package/dist/assets/layout-iCSHU015.js +1 -0
- package/dist/assets/min-BK_AIJdo.js +1 -0
- package/dist/assets/mindmap-definition-VGOIOE7T-BZMj_6zo.js +68 -0
- package/dist/assets/pieDiagram-ADFJNKIX-CkAGsq9p.js +30 -0
- package/dist/assets/quadrantDiagram-AYHSOK5B-CWa93px1.js +7 -0
- package/dist/assets/requirementDiagram-UZGBJVZJ-CufFVR8c.js +64 -0
- package/dist/assets/sankeyDiagram-TZEHDZUN-BEPgVgU4.js +10 -0
- package/dist/assets/sequenceDiagram-WL72ISMW-BkdBWhel.js +145 -0
- package/dist/assets/stateDiagram-FKZM4ZOC-D5T73yx0.js +1 -0
- package/dist/assets/stateDiagram-v2-4FDKWEC3-9hJWG2n6.js +1 -0
- package/dist/assets/timeline-definition-IT6M3QCI-CX7kTdU2.js +61 -0
- package/dist/assets/treemap-KMMF4GRG-ftWCQ9lJ.js +128 -0
- package/dist/assets/xychartDiagram-PRI3JC2R-Ngrels4n.js +7 -0
- package/{index.html → dist/index.html} +2 -1
- package/package.json +12 -2
- 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,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
|
-
};
|