@leanspec/ui 0.2.14 → 0.2.15

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