@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.
- 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 +13 -3
- package/eslint.config.js +0 -23
- 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,432 +0,0 @@
|
|
|
1
|
-
import { useCallback, useEffect, useMemo, useState } from 'react';
|
|
2
|
-
import {
|
|
3
|
-
AlertTriangle,
|
|
4
|
-
Check,
|
|
5
|
-
CheckCircle2,
|
|
6
|
-
FolderOpen,
|
|
7
|
-
MoreVertical,
|
|
8
|
-
Pencil,
|
|
9
|
-
Plus,
|
|
10
|
-
RefreshCw,
|
|
11
|
-
Search,
|
|
12
|
-
Star,
|
|
13
|
-
Trash2,
|
|
14
|
-
X
|
|
15
|
-
} from 'lucide-react';
|
|
16
|
-
import { Button, Card, CardContent, CardHeader, Input, Popover, PopoverContent, PopoverTrigger } from '@leanspec/ui-components';
|
|
17
|
-
import { useTranslation } from 'react-i18next';
|
|
18
|
-
import { useNavigate } from 'react-router-dom';
|
|
19
|
-
import dayjs from 'dayjs';
|
|
20
|
-
import relativeTime from 'dayjs/plugin/relativeTime';
|
|
21
|
-
|
|
22
|
-
import { CreateProjectDialog } from '../components/projects/CreateProjectDialog';
|
|
23
|
-
import { ProjectAvatar, getColorForName } from '../components/shared/ProjectAvatar';
|
|
24
|
-
import { ColorPicker } from '../components/shared/ColorPicker';
|
|
25
|
-
import { PageHeader } from '../components/shared/PageHeader';
|
|
26
|
-
import { useProject } from '../contexts';
|
|
27
|
-
import { api } from '../lib/api';
|
|
28
|
-
|
|
29
|
-
dayjs.extend(relativeTime);
|
|
30
|
-
|
|
31
|
-
interface ProjectValidationState {
|
|
32
|
-
status: 'unknown' | 'validating' | 'valid' | 'invalid';
|
|
33
|
-
error?: string;
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
// Project stats cache
|
|
37
|
-
interface ProjectStats {
|
|
38
|
-
totalSpecs: number;
|
|
39
|
-
specsByStatus: { status: string; count: number }[];
|
|
40
|
-
completionRate: number;
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
export function ProjectsPage() {
|
|
44
|
-
const { t } = useTranslation('common');
|
|
45
|
-
const navigate = useNavigate();
|
|
46
|
-
const {
|
|
47
|
-
projects,
|
|
48
|
-
switchProject,
|
|
49
|
-
toggleFavorite,
|
|
50
|
-
removeProject,
|
|
51
|
-
updateProject,
|
|
52
|
-
loading,
|
|
53
|
-
} = useProject();
|
|
54
|
-
|
|
55
|
-
const [searchQuery, setSearchQuery] = useState('');
|
|
56
|
-
const [isCreateDialogOpen, setIsCreateDialogOpen] = useState(false);
|
|
57
|
-
const [editingProjectId, setEditingProjectId] = useState<string | null>(null);
|
|
58
|
-
const [editingName, setEditingName] = useState('');
|
|
59
|
-
const [validationStates, setValidationStates] = useState<Record<string, ProjectValidationState>>({});
|
|
60
|
-
const [statsCache, setStatsCache] = useState<Record<string, ProjectStats>>({});
|
|
61
|
-
|
|
62
|
-
const filteredProjects = useMemo(() => {
|
|
63
|
-
return projects.filter((p) =>
|
|
64
|
-
(p.name || p.id).toLowerCase().includes(searchQuery.toLowerCase())
|
|
65
|
-
);
|
|
66
|
-
}, [projects, searchQuery]);
|
|
67
|
-
|
|
68
|
-
// Auto-validate projects on load
|
|
69
|
-
useEffect(() => {
|
|
70
|
-
const validateAll = async () => {
|
|
71
|
-
for (const project of projects) {
|
|
72
|
-
if (validationStates[project.id]?.status === 'valid' || validationStates[project.id]?.status === 'invalid') {
|
|
73
|
-
continue;
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
try {
|
|
77
|
-
setValidationStates(prev => ({
|
|
78
|
-
...prev,
|
|
79
|
-
[project.id]: { status: 'validating' }
|
|
80
|
-
}));
|
|
81
|
-
|
|
82
|
-
const response = await api.validateProject(project.id);
|
|
83
|
-
|
|
84
|
-
setValidationStates(prev => ({
|
|
85
|
-
...prev,
|
|
86
|
-
[project.id]: {
|
|
87
|
-
status: response.validation.isValid ? 'valid' : 'invalid',
|
|
88
|
-
error: response.validation.error || undefined
|
|
89
|
-
}
|
|
90
|
-
}));
|
|
91
|
-
} catch {
|
|
92
|
-
setValidationStates(prev => ({
|
|
93
|
-
...prev,
|
|
94
|
-
[project.id]: { status: 'invalid', error: 'Validation failed' }
|
|
95
|
-
}));
|
|
96
|
-
}
|
|
97
|
-
}
|
|
98
|
-
};
|
|
99
|
-
|
|
100
|
-
if (projects.length > 0) {
|
|
101
|
-
validateAll();
|
|
102
|
-
}
|
|
103
|
-
}, [projects]); // eslint-disable-line react-hooks/exhaustive-deps
|
|
104
|
-
|
|
105
|
-
// Fetch stats for projects
|
|
106
|
-
useEffect(() => {
|
|
107
|
-
const fetchStats = async () => {
|
|
108
|
-
for (const project of projects) {
|
|
109
|
-
if (statsCache[project.id]) continue;
|
|
110
|
-
|
|
111
|
-
try {
|
|
112
|
-
// Use api adapter directly
|
|
113
|
-
const stats = await api.getProjectStats(project.id);
|
|
114
|
-
setStatsCache(prev => ({
|
|
115
|
-
...prev,
|
|
116
|
-
[project.id]: stats as unknown as ProjectStats
|
|
117
|
-
// Note: Stats type in api.ts might differ slightly from UI needs, but assuming overlap
|
|
118
|
-
}));
|
|
119
|
-
} catch {
|
|
120
|
-
// Ignore stats fetch errors
|
|
121
|
-
}
|
|
122
|
-
}
|
|
123
|
-
};
|
|
124
|
-
|
|
125
|
-
if (projects.length > 0) {
|
|
126
|
-
fetchStats();
|
|
127
|
-
}
|
|
128
|
-
}, [projects]); // eslint-disable-line react-hooks/exhaustive-deps
|
|
129
|
-
|
|
130
|
-
const handleProjectClick = async (projectId: string) => {
|
|
131
|
-
try {
|
|
132
|
-
await switchProject(projectId);
|
|
133
|
-
navigate(`/projects/${projectId}/specs`);
|
|
134
|
-
} catch (e) {
|
|
135
|
-
console.error('Failed to switch project', e);
|
|
136
|
-
}
|
|
137
|
-
};
|
|
138
|
-
|
|
139
|
-
const startEditing = (projectId: string, currentName: string) => {
|
|
140
|
-
setEditingProjectId(projectId);
|
|
141
|
-
setEditingName(currentName);
|
|
142
|
-
};
|
|
143
|
-
|
|
144
|
-
const cancelEditing = () => {
|
|
145
|
-
setEditingProjectId(null);
|
|
146
|
-
setEditingName('');
|
|
147
|
-
};
|
|
148
|
-
|
|
149
|
-
const saveProjectName = async (projectId: string) => {
|
|
150
|
-
if (!editingName.trim()) {
|
|
151
|
-
// toast.error('Project name cannot be empty');
|
|
152
|
-
return;
|
|
153
|
-
}
|
|
154
|
-
try {
|
|
155
|
-
await updateProject(projectId, { name: editingName.trim() });
|
|
156
|
-
// toast.success('Project name updated');
|
|
157
|
-
setEditingProjectId(null);
|
|
158
|
-
setEditingName('');
|
|
159
|
-
} catch {
|
|
160
|
-
// toast.error('Failed to update project name');
|
|
161
|
-
}
|
|
162
|
-
};
|
|
163
|
-
|
|
164
|
-
const handleColorChange = async (projectId: string, color: string) => {
|
|
165
|
-
try {
|
|
166
|
-
await updateProject(projectId, { color });
|
|
167
|
-
// toast.success('Project color updated');
|
|
168
|
-
} catch {
|
|
169
|
-
// toast.error('Failed to update project color');
|
|
170
|
-
}
|
|
171
|
-
};
|
|
172
|
-
|
|
173
|
-
const handleValidate = useCallback(async (projectId: string) => {
|
|
174
|
-
setValidationStates(prev => ({
|
|
175
|
-
...prev,
|
|
176
|
-
[projectId]: { status: 'validating' }
|
|
177
|
-
}));
|
|
178
|
-
|
|
179
|
-
try {
|
|
180
|
-
const response = await api.validateProject(projectId);
|
|
181
|
-
|
|
182
|
-
setValidationStates(prev => ({
|
|
183
|
-
...prev,
|
|
184
|
-
[projectId]: {
|
|
185
|
-
status: response.validation.isValid ? 'valid' : 'invalid',
|
|
186
|
-
error: response.validation.error || undefined
|
|
187
|
-
}
|
|
188
|
-
}));
|
|
189
|
-
|
|
190
|
-
// if (response.validation.isValid) {
|
|
191
|
-
// toast.success('Project path is valid');
|
|
192
|
-
// } else {
|
|
193
|
-
// toast.error(`Project path invalid: ${response.validation.error || 'Unknown error'}`);
|
|
194
|
-
// }
|
|
195
|
-
} catch {
|
|
196
|
-
setValidationStates(prev => ({
|
|
197
|
-
...prev,
|
|
198
|
-
[projectId]: { status: 'invalid', error: t('projects.validationFailed') }
|
|
199
|
-
}));
|
|
200
|
-
// toast.error('Failed to validate project');
|
|
201
|
-
}
|
|
202
|
-
}, [t]);
|
|
203
|
-
|
|
204
|
-
const getValidationIcon = (projectId: string) => {
|
|
205
|
-
const state = validationStates[projectId];
|
|
206
|
-
if (!state || state.status === 'unknown') return null;
|
|
207
|
-
|
|
208
|
-
if (state.status === 'validating') {
|
|
209
|
-
return <RefreshCw className="h-4 w-4 animate-spin text-muted-foreground" />;
|
|
210
|
-
}
|
|
211
|
-
if (state.status === 'valid') {
|
|
212
|
-
return <CheckCircle2 className="h-4 w-4 text-green-500" />;
|
|
213
|
-
}
|
|
214
|
-
if (state.status === 'invalid') {
|
|
215
|
-
return (
|
|
216
|
-
<span title={state.error || t('projects.validationFailed')}>
|
|
217
|
-
<AlertTriangle className="h-4 w-4 text-destructive" />
|
|
218
|
-
</span>
|
|
219
|
-
);
|
|
220
|
-
}
|
|
221
|
-
return null;
|
|
222
|
-
};
|
|
223
|
-
|
|
224
|
-
return (
|
|
225
|
-
<div className="min-h-screen bg-background">
|
|
226
|
-
{/* Header Section */}
|
|
227
|
-
<div className="border-b bg-card/50 backdrop-blur-sm sticky top-0 z-10">
|
|
228
|
-
<div className="container max-w-7xl mx-auto py-6 space-y-6 px-4">
|
|
229
|
-
<PageHeader
|
|
230
|
-
title={t('projectsPage.title')}
|
|
231
|
-
description={t('projectsPage.description') || t('projects.description')}
|
|
232
|
-
actions={(
|
|
233
|
-
<Button onClick={() => setIsCreateDialogOpen(true)} size="lg" className="shadow-sm">
|
|
234
|
-
<Plus className="mr-2 h-4 w-4" />
|
|
235
|
-
{t('projects.newProject')}
|
|
236
|
-
</Button>
|
|
237
|
-
)}
|
|
238
|
-
/>
|
|
239
|
-
|
|
240
|
-
<div className="flex items-center space-x-2 max-w-md">
|
|
241
|
-
<div className="relative flex-1">
|
|
242
|
-
<Search className="absolute left-3 top-1/2 -translate-y-1/2 h-4 w-4 text-muted-foreground" />
|
|
243
|
-
<Input
|
|
244
|
-
placeholder={t('projects.searchPlaceholder')}
|
|
245
|
-
value={searchQuery}
|
|
246
|
-
onChange={(e) => setSearchQuery(e.target.value)}
|
|
247
|
-
className="pl-9 bg-background/50"
|
|
248
|
-
/>
|
|
249
|
-
</div>
|
|
250
|
-
</div>
|
|
251
|
-
</div>
|
|
252
|
-
</div>
|
|
253
|
-
|
|
254
|
-
<div className="container max-w-7xl mx-auto py-8 px-4">
|
|
255
|
-
<div className="grid gap-6 grid-cols-1 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4">
|
|
256
|
-
{filteredProjects.map((project) => {
|
|
257
|
-
const stats = statsCache[project.id];
|
|
258
|
-
return (
|
|
259
|
-
<Card
|
|
260
|
-
key={project.id}
|
|
261
|
-
className="group relative flex flex-col transition-all duration-200 hover:shadow-md hover:border-primary/20 overflow-hidden cursor-pointer bg-card"
|
|
262
|
-
onClick={() => handleProjectClick(project.id)}
|
|
263
|
-
>
|
|
264
|
-
<CardHeader className="px-4 pt-4 pb-2 space-y-1">
|
|
265
|
-
<div className="flex items-start justify-between gap-3">
|
|
266
|
-
<div className="flex items-start gap-3 flex-1 min-w-0">
|
|
267
|
-
<ProjectAvatar
|
|
268
|
-
name={project.name || project.id}
|
|
269
|
-
color={project.color}
|
|
270
|
-
size="lg"
|
|
271
|
-
className="shrink-0"
|
|
272
|
-
/>
|
|
273
|
-
<div className="flex-1 min-w-0">
|
|
274
|
-
{editingProjectId === project.id ? (
|
|
275
|
-
<div className="flex items-center gap-1" onClick={(e) => e.stopPropagation()}>
|
|
276
|
-
<Input
|
|
277
|
-
value={editingName}
|
|
278
|
-
onChange={(e) => setEditingName(e.target.value)}
|
|
279
|
-
className="h-7 text-sm"
|
|
280
|
-
autoFocus
|
|
281
|
-
onKeyDown={(e) => {
|
|
282
|
-
if (e.key === 'Enter') saveProjectName(project.id);
|
|
283
|
-
if (e.key === 'Escape') cancelEditing();
|
|
284
|
-
}}
|
|
285
|
-
/>
|
|
286
|
-
<Button
|
|
287
|
-
variant="ghost"
|
|
288
|
-
size="icon"
|
|
289
|
-
className="h-7 w-7"
|
|
290
|
-
onClick={() => saveProjectName(project.id)}
|
|
291
|
-
>
|
|
292
|
-
<Check className="h-3 w-3" />
|
|
293
|
-
</Button>
|
|
294
|
-
<Button
|
|
295
|
-
variant="ghost"
|
|
296
|
-
size="icon"
|
|
297
|
-
className="h-7 w-7"
|
|
298
|
-
onClick={() => cancelEditing()}
|
|
299
|
-
>
|
|
300
|
-
<X className="h-3 w-3" />
|
|
301
|
-
</Button>
|
|
302
|
-
</div>
|
|
303
|
-
) : (
|
|
304
|
-
<div className="flex items-center gap-2 mb-1">
|
|
305
|
-
<h3 className="font-semibold text-base leading-none truncate" title={project.name || project.id}>
|
|
306
|
-
{project.name || project.id}
|
|
307
|
-
</h3>
|
|
308
|
-
{getValidationIcon(project.id)}
|
|
309
|
-
</div>
|
|
310
|
-
)}
|
|
311
|
-
<p className="text-[10px] font-mono text-muted-foreground truncate" title={project.path || project.specsDir}>
|
|
312
|
-
{project.path || project.specsDir}
|
|
313
|
-
</p>
|
|
314
|
-
</div>
|
|
315
|
-
</div>
|
|
316
|
-
|
|
317
|
-
{/* Reuse Popover as simple Dropdown */}
|
|
318
|
-
<div onClick={(e) => e.stopPropagation()}>
|
|
319
|
-
<Popover>
|
|
320
|
-
<PopoverTrigger asChild>
|
|
321
|
-
<Button variant="ghost" size="icon" className="h-8 w-8 -mr-2 text-muted-foreground hover:text-foreground opacity-0 group-hover:opacity-100 transition-opacity">
|
|
322
|
-
<MoreVertical className="h-4 w-4" />
|
|
323
|
-
<span className="sr-only">Open menu</span>
|
|
324
|
-
</Button>
|
|
325
|
-
</PopoverTrigger>
|
|
326
|
-
<PopoverContent align="end" className="w-56 p-1">
|
|
327
|
-
<Button
|
|
328
|
-
variant="ghost"
|
|
329
|
-
className="w-full justify-start h-8 px-2 text-sm"
|
|
330
|
-
onClick={() => startEditing(project.id, project.name || project.id)}
|
|
331
|
-
>
|
|
332
|
-
<Pencil className="mr-2 h-4 w-4" />
|
|
333
|
-
{t('projects.rename')}
|
|
334
|
-
</Button>
|
|
335
|
-
<div className="p-2">
|
|
336
|
-
<p className="text-xs text-muted-foreground mb-2 px-1">{t('projects.projectColor')}</p>
|
|
337
|
-
<div className="flex flex-wrap gap-1">
|
|
338
|
-
<ColorPicker
|
|
339
|
-
value={project.color}
|
|
340
|
-
onChange={(color) => handleColorChange(project.id, color)}
|
|
341
|
-
/>
|
|
342
|
-
</div>
|
|
343
|
-
</div>
|
|
344
|
-
<div className="h-px bg-border my-1" />
|
|
345
|
-
<Button
|
|
346
|
-
variant="ghost"
|
|
347
|
-
className="w-full justify-start h-8 px-2 text-sm"
|
|
348
|
-
onClick={() => toggleFavorite(project.id)}
|
|
349
|
-
>
|
|
350
|
-
<Star className="mr-2 h-4 w-4" />
|
|
351
|
-
{project.favorite ? t('projects.unfavorite') : t('projects.favorite')}
|
|
352
|
-
</Button>
|
|
353
|
-
<Button
|
|
354
|
-
variant="ghost"
|
|
355
|
-
className="w-full justify-start h-8 px-2 text-sm"
|
|
356
|
-
onClick={() => handleValidate(project.id)}
|
|
357
|
-
>
|
|
358
|
-
<RefreshCw className="mr-2 h-4 w-4" />
|
|
359
|
-
{t('projects.validatePath')}
|
|
360
|
-
</Button>
|
|
361
|
-
<div className="h-px bg-border my-1" />
|
|
362
|
-
<Button
|
|
363
|
-
variant="ghost"
|
|
364
|
-
className="w-full justify-start h-8 px-2 text-sm text-destructive hover:text-destructive"
|
|
365
|
-
onClick={() => removeProject(project.id)}
|
|
366
|
-
>
|
|
367
|
-
<Trash2 className="mr-2 h-4 w-4" />
|
|
368
|
-
{t('actions.remove')}
|
|
369
|
-
</Button>
|
|
370
|
-
</PopoverContent>
|
|
371
|
-
</Popover>
|
|
372
|
-
</div>
|
|
373
|
-
</div>
|
|
374
|
-
</CardHeader>
|
|
375
|
-
|
|
376
|
-
<CardContent className="p-2 px-4 pb-4 flex-1">
|
|
377
|
-
<div className="flex items-center gap-4 py-1">
|
|
378
|
-
<div className="flex flex-col">
|
|
379
|
-
<span className="text-[10px] text-muted-foreground font-medium uppercase tracking-wider">{t('projects.specs')}</span>
|
|
380
|
-
<span className="text-lg font-bold tracking-tight">{stats?.totalSpecs || 0}</span>
|
|
381
|
-
</div>
|
|
382
|
-
<div className="w-px h-8 bg-border" />
|
|
383
|
-
<div className="flex flex-col">
|
|
384
|
-
<span className="text-[10px] text-muted-foreground font-medium uppercase tracking-wider">{t('projects.completion')}</span>
|
|
385
|
-
<span className="text-lg font-bold tracking-tight">{(stats?.completionRate || 0).toFixed(1)}%</span>
|
|
386
|
-
</div>
|
|
387
|
-
</div>
|
|
388
|
-
</CardContent>
|
|
389
|
-
|
|
390
|
-
<div className="px-4 py-2 bg-muted/20 border-t flex items-center justify-between text-[10px] text-muted-foreground mt-auto">
|
|
391
|
-
<div className="flex items-center gap-1.5">
|
|
392
|
-
<div className="w-1.5 h-1.5 rounded-full" style={{ backgroundColor: project.color || getColorForName(project.name || project.id) }} />
|
|
393
|
-
<span>{t('projects.local')}</span>
|
|
394
|
-
</div>
|
|
395
|
-
{project.lastAccessed && (
|
|
396
|
-
<span>{dayjs(project.lastAccessed).fromNow()}</span>
|
|
397
|
-
)}
|
|
398
|
-
</div>
|
|
399
|
-
|
|
400
|
-
{project.favorite && (
|
|
401
|
-
<div className="absolute top-0 right-0 p-2 pointer-events-none">
|
|
402
|
-
<div className="bg-background/80 backdrop-blur-sm p-1.5 rounded-bl-lg border-b border-l shadow-sm">
|
|
403
|
-
<Star className="h-3.5 w-3.5 fill-yellow-500 text-yellow-500" />
|
|
404
|
-
</div>
|
|
405
|
-
</div>
|
|
406
|
-
)}
|
|
407
|
-
</Card>
|
|
408
|
-
)
|
|
409
|
-
})}
|
|
410
|
-
|
|
411
|
-
{filteredProjects.length === 0 && !loading && (
|
|
412
|
-
<div className="col-span-full flex flex-col items-center justify-center py-16 text-center border-2 border-dashed rounded-xl bg-muted/10">
|
|
413
|
-
<div className="bg-muted/30 p-4 rounded-full mb-4">
|
|
414
|
-
<FolderOpen className="h-8 w-8 text-muted-foreground" />
|
|
415
|
-
</div>
|
|
416
|
-
<h3 className="text-xl font-semibold">{t('projects.noProjectsFound')}</h3>
|
|
417
|
-
<p className="text-muted-foreground mt-2 mb-6 max-w-sm">
|
|
418
|
-
{searchQuery ? t('quickSearch.noResults') : t('projects.getStarted')}
|
|
419
|
-
</p>
|
|
420
|
-
<Button onClick={() => setIsCreateDialogOpen(true)} size="lg">
|
|
421
|
-
<Plus className="mr-2 h-4 w-4" />
|
|
422
|
-
{t('projects.createProject')}
|
|
423
|
-
</Button>
|
|
424
|
-
</div>
|
|
425
|
-
)}
|
|
426
|
-
</div>
|
|
427
|
-
|
|
428
|
-
<CreateProjectDialog open={isCreateDialogOpen} onOpenChange={setIsCreateDialogOpen} />
|
|
429
|
-
</div>
|
|
430
|
-
</div>
|
|
431
|
-
);
|
|
432
|
-
}
|