@leanspec/ui 0.2.13 → 0.2.15-dev.21022397862

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (149) hide show
  1. package/bin/leanspec-ui.js +191 -0
  2. package/dist/assets/_baseUniq-CRqreL7N.js +1 -0
  3. package/dist/assets/arc-DMhx9AJT.js +1 -0
  4. package/dist/assets/architectureDiagram-VXUJARFQ-DM0L0YzO.js +36 -0
  5. package/dist/assets/blockDiagram-VD42YOAC-DHQXDHsD.js +122 -0
  6. package/dist/assets/c4Diagram-YG6GDRKO-0L7o2gpH.js +10 -0
  7. package/dist/assets/channel-2tOl0nAZ.js +1 -0
  8. package/dist/assets/chunk-4BX2VUAB-CwFT-Uaj.js +1 -0
  9. package/dist/assets/chunk-55IACEB6-CjvuUHHG.js +1 -0
  10. package/dist/assets/chunk-B4BG7PRW-BRJBysMK.js +165 -0
  11. package/dist/assets/chunk-DI55MBZ5-BnNEeoaA.js +220 -0
  12. package/dist/assets/chunk-FMBD7UC4-BK2l30pm.js +15 -0
  13. package/dist/assets/chunk-QN33PNHL-BN_cZkCU.js +1 -0
  14. package/dist/assets/chunk-QZHKN3VN-Brc3Yrub.js +1 -0
  15. package/dist/assets/chunk-TZMSLE5B-D2zzpLfO.js +1 -0
  16. package/dist/assets/classDiagram-2ON5EDUG-BB9CSNmS.js +1 -0
  17. package/dist/assets/classDiagram-v2-WZHVMYZB-BB9CSNmS.js +1 -0
  18. package/dist/assets/clone-BjxVFtyI.js +1 -0
  19. package/dist/assets/core-DV6XEvTN.js +1 -0
  20. package/dist/assets/cose-bilkent-S5V4N54A-CLJgM3XR.js +1 -0
  21. package/dist/assets/cytoscape.esm-5J0xJHOV.js +321 -0
  22. package/dist/assets/dagre-6UL2VRFP-_IFvBJKJ.js +4 -0
  23. package/dist/assets/diagram-PSM6KHXK--83HIYSQ.js +24 -0
  24. package/dist/assets/diagram-QEK2KX5R-6jAWnCnZ.js +43 -0
  25. package/dist/assets/diagram-S2PKOQOG-D5pwHvjZ.js +24 -0
  26. package/dist/assets/erDiagram-Q2GNP2WA-B4FV3mTd.js +60 -0
  27. package/dist/assets/flowDiagram-NV44I4VS-mtD2kF4M.js +162 -0
  28. package/dist/assets/ganttDiagram-JELNMOA3-BKALgqTK.js +267 -0
  29. package/dist/assets/gitGraphDiagram-NY62KEGX-Bd7r0pAf.js +65 -0
  30. package/dist/assets/graph-B2rEI7cK.js +1 -0
  31. package/dist/assets/index-Bekv_o1t.css +1 -0
  32. package/dist/assets/index-DSRxU-E5.js +389 -0
  33. package/dist/assets/infoDiagram-WHAUD3N6--nJOBKqh.js +2 -0
  34. package/dist/assets/journeyDiagram-XKPGCS4Q-BzGutKN3.js +139 -0
  35. package/dist/assets/kanban-definition-3W4ZIXB7-DyQO17vq.js +89 -0
  36. package/dist/assets/katex-XbL3y5x-.js +261 -0
  37. package/dist/assets/layout-iCSHU015.js +1 -0
  38. package/dist/assets/min-BK_AIJdo.js +1 -0
  39. package/dist/assets/mindmap-definition-VGOIOE7T-BZMj_6zo.js +68 -0
  40. package/dist/assets/pieDiagram-ADFJNKIX-CkAGsq9p.js +30 -0
  41. package/dist/assets/quadrantDiagram-AYHSOK5B-CWa93px1.js +7 -0
  42. package/dist/assets/requirementDiagram-UZGBJVZJ-CufFVR8c.js +64 -0
  43. package/dist/assets/sankeyDiagram-TZEHDZUN-BEPgVgU4.js +10 -0
  44. package/dist/assets/sequenceDiagram-WL72ISMW-BkdBWhel.js +145 -0
  45. package/dist/assets/stateDiagram-FKZM4ZOC-D5T73yx0.js +1 -0
  46. package/dist/assets/stateDiagram-v2-4FDKWEC3-9hJWG2n6.js +1 -0
  47. package/dist/assets/timeline-definition-IT6M3QCI-CX7kTdU2.js +61 -0
  48. package/dist/assets/treemap-KMMF4GRG-ftWCQ9lJ.js +128 -0
  49. package/dist/assets/xychartDiagram-PRI3JC2R-Ngrels4n.js +7 -0
  50. package/{index.html → dist/index.html} +2 -1
  51. package/package.json +13 -3
  52. package/eslint.config.js +0 -23
  53. package/postcss.config.js +0 -6
  54. package/src/App.css +0 -42
  55. package/src/App.tsx +0 -17
  56. package/src/assets/react.svg +0 -1
  57. package/src/components/LanguageSwitcher.tsx +0 -67
  58. package/src/components/Layout.tsx +0 -88
  59. package/src/components/MainSidebar.tsx +0 -163
  60. package/src/components/MermaidDiagram.tsx +0 -85
  61. package/src/components/MinimalLayout.tsx +0 -51
  62. package/src/components/Navigation.tsx +0 -254
  63. package/src/components/PriorityBadge.tsx +0 -59
  64. package/src/components/ProjectSwitcher.tsx +0 -222
  65. package/src/components/QuickSearch.tsx +0 -225
  66. package/src/components/RootRedirect.tsx +0 -40
  67. package/src/components/SpecDetailLayout.context.ts +0 -10
  68. package/src/components/SpecDetailLayout.tsx +0 -14
  69. package/src/components/SpecsNavSidebar.tsx +0 -615
  70. package/src/components/StatusBadge.tsx +0 -59
  71. package/src/components/ThemeToggle.tsx +0 -25
  72. package/src/components/Tooltip.tsx +0 -29
  73. package/src/components/context/ContextClient.tsx +0 -471
  74. package/src/components/context/ContextFileDetail.tsx +0 -163
  75. package/src/components/dashboard/ActivityItem.tsx +0 -36
  76. package/src/components/dashboard/DashboardClient.tsx +0 -218
  77. package/src/components/dashboard/SpecListItem.tsx +0 -58
  78. package/src/components/dashboard/StatCard.tsx +0 -52
  79. package/src/components/dependencies/SpecNode.tsx +0 -128
  80. package/src/components/dependencies/SpecSidebar.tsx +0 -256
  81. package/src/components/dependencies/constants.ts +0 -25
  82. package/src/components/dependencies/types.ts +0 -38
  83. package/src/components/dependencies/utils.ts +0 -261
  84. package/src/components/metadata-editors/PriorityEditor.tsx +0 -89
  85. package/src/components/metadata-editors/StatusEditor.tsx +0 -85
  86. package/src/components/metadata-editors/TagsEditor.tsx +0 -207
  87. package/src/components/projects/CreateProjectDialog.tsx +0 -162
  88. package/src/components/projects/DirectoryPicker.tsx +0 -182
  89. package/src/components/shared/BackToTop.tsx +0 -39
  90. package/src/components/shared/ColorPicker.tsx +0 -68
  91. package/src/components/shared/EmptyState.tsx +0 -35
  92. package/src/components/shared/ErrorBoundary.tsx +0 -79
  93. package/src/components/shared/PageHeader.tsx +0 -23
  94. package/src/components/shared/PageTransition.tsx +0 -40
  95. package/src/components/shared/ProjectAvatar.tsx +0 -107
  96. package/src/components/shared/Skeletons.tsx +0 -184
  97. package/src/components/spec-detail/EditableMetadata.tsx +0 -129
  98. package/src/components/spec-detail/MarkdownRenderer.tsx +0 -47
  99. package/src/components/spec-detail/TableOfContents.tsx +0 -150
  100. package/src/components/specs/BoardView.tsx +0 -204
  101. package/src/components/specs/ListView.tsx +0 -62
  102. package/src/components/specs/SpecsFilters.tsx +0 -190
  103. package/src/contexts/KeyboardShortcutsContext.tsx +0 -95
  104. package/src/contexts/LayoutContext.tsx +0 -45
  105. package/src/contexts/ProjectContext.tsx +0 -163
  106. package/src/contexts/ThemeContext.tsx +0 -90
  107. package/src/contexts/index.ts +0 -7
  108. package/src/hooks/useKeyboardShortcuts.ts +0 -87
  109. package/src/index.css +0 -624
  110. package/src/lib/api.ts +0 -72
  111. package/src/lib/backend-adapter.ts +0 -382
  112. package/src/lib/date-utils.ts +0 -122
  113. package/src/lib/i18n.test.ts +0 -57
  114. package/src/lib/i18n.ts +0 -51
  115. package/src/lib/markdown-utils.ts +0 -38
  116. package/src/lib/sub-spec-utils.ts +0 -166
  117. package/src/lib/utils.ts +0 -6
  118. package/src/locales/en/common.json +0 -660
  119. package/src/locales/en/errors.json +0 -20
  120. package/src/locales/en/help.json +0 -8
  121. package/src/locales/zh-CN/common.json +0 -660
  122. package/src/locales/zh-CN/errors.json +0 -20
  123. package/src/locales/zh-CN/help.json +0 -8
  124. package/src/main.tsx +0 -12
  125. package/src/pages/ContextPage.tsx +0 -111
  126. package/src/pages/DashboardPage.tsx +0 -97
  127. package/src/pages/DependenciesPage.tsx +0 -881
  128. package/src/pages/ProjectsPage.tsx +0 -432
  129. package/src/pages/SpecDetailPage.tsx +0 -592
  130. package/src/pages/SpecsPage.tsx +0 -319
  131. package/src/pages/StatsPage.tsx +0 -307
  132. package/src/router/projectRoutes.tsx +0 -36
  133. package/src/router.tsx +0 -33
  134. package/src/test/setup.ts +0 -39
  135. package/src/types/api.ts +0 -185
  136. package/tailwind.config.ts +0 -57
  137. package/tsconfig.app.json +0 -29
  138. package/tsconfig.json +0 -7
  139. package/tsconfig.node.json +0 -26
  140. package/tsconfig.tsbuildinfo +0 -1
  141. package/vite.config.ts +0 -27
  142. package/vitest.config.ts +0 -18
  143. /package/{public → dist}/favicon.ico +0 -0
  144. /package/{public → dist}/github-mark-white.svg +0 -0
  145. /package/{public → dist}/github-mark.svg +0 -0
  146. /package/{public → dist}/logo-dark-bg.svg +0 -0
  147. /package/{public → dist}/logo-with-bg.svg +0 -0
  148. /package/{public → dist}/logo.svg +0 -0
  149. /package/{public → dist}/vite.svg +0 -0
@@ -1,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
- }