@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.
Files changed (150) 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 +12 -2
  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,25 +0,0 @@
1
- import { Sun, Moon } from 'lucide-react';
2
- import { useTheme } from '../contexts';
3
- import { Button } from '@leanspec/ui-components';
4
- import { useTranslation } from 'react-i18next';
5
-
6
- export function ThemeToggle() {
7
- const { setTheme, resolvedTheme } = useTheme();
8
- const isDark = resolvedTheme === 'dark';
9
- const { t } = useTranslation('common');
10
-
11
- const toggleTheme = () => setTheme(isDark ? 'light' : 'dark');
12
-
13
- return (
14
- <Button
15
- variant="ghost"
16
- size="icon"
17
- onClick={toggleTheme}
18
- aria-label={t('theme.toggleTheme')}
19
- className="relative h-9 w-9"
20
- >
21
- <Sun className="h-5 w-5 rotate-0 scale-100 transition-all dark:-rotate-90 dark:scale-0" />
22
- <Moon className="absolute h-5 w-5 rotate-90 scale-0 transition-all dark:rotate-0 dark:scale-100" />
23
- </Button>
24
- );
25
- }
@@ -1,29 +0,0 @@
1
- import * as React from 'react';
2
- import * as TooltipPrimitive from '@radix-ui/react-tooltip';
3
- import { cn } from '../lib/utils';
4
-
5
- const TooltipProvider = TooltipPrimitive.Provider;
6
-
7
- const Tooltip = TooltipPrimitive.Root;
8
-
9
- const TooltipTrigger = TooltipPrimitive.Trigger;
10
-
11
- const TooltipContent = React.forwardRef<
12
- React.ElementRef<typeof TooltipPrimitive.Content>,
13
- React.ComponentPropsWithoutRef<typeof TooltipPrimitive.Content>
14
- >(({ className, sideOffset = 4, ...props }, ref) => (
15
- <TooltipPrimitive.Portal>
16
- <TooltipPrimitive.Content
17
- ref={ref}
18
- sideOffset={sideOffset}
19
- className={cn(
20
- 'z-[100] overflow-hidden rounded-md border bg-popover px-3 py-1.5 text-xs text-popover-foreground shadow-md animate-in fade-in-0 zoom-in-95 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2',
21
- className
22
- )}
23
- {...props}
24
- />
25
- </TooltipPrimitive.Portal>
26
- ));
27
- TooltipContent.displayName = TooltipPrimitive.Content.displayName;
28
-
29
- export { Tooltip, TooltipTrigger, TooltipContent, TooltipProvider };
@@ -1,471 +0,0 @@
1
- /**
2
- * Project Context Client Component
3
- * Displays project-level context files (AGENTS.md, config, README, etc.)
4
- * Aligned with Next.js UI implementation - uses structured ProjectContext
5
- * Spec 131 - UI Project Context Visibility
6
- */
7
-
8
- import * as React from 'react';
9
- import { BookOpen, Settings, FileText, Copy, Check, AlertCircle, Coins, Info, Search, X } from 'lucide-react';
10
- import { Card, CardHeader, CardTitle, CardDescription, CardContent, Badge, Button, Input } from '@leanspec/ui-components';
11
- import type { ProjectContext, ContextFile } from '../../types/api';
12
- import { cn } from '../../lib/utils';
13
- import { useTranslation } from 'react-i18next';
14
- import { ContextFileDetail } from './ContextFileDetail';
15
- import { PageHeader } from '../shared/PageHeader';
16
-
17
- interface ContextClientProps {
18
- context: ProjectContext;
19
- }
20
-
21
- /**
22
- * Get token threshold color
23
- */
24
- function getTotalTokenColor(tokens: number): string {
25
- if (tokens < 5000) return 'text-green-600 dark:text-green-400';
26
- if (tokens < 10000) return 'text-blue-600 dark:text-blue-400';
27
- if (tokens < 20000) return 'text-yellow-600 dark:text-yellow-400';
28
- return 'text-red-600 dark:text-red-400';
29
- }
30
-
31
- /**
32
- * Count matches in content
33
- */
34
- function countMatches(content: string, query: string): number {
35
- if (!query || query.length < 2) return 0;
36
- const regex = new RegExp(query.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'), 'gi');
37
- const matches = content.match(regex);
38
- return matches ? matches.length : 0;
39
- }
40
-
41
- /**
42
- * Empty state component
43
- */
44
- function EmptyState({
45
- icon: Icon,
46
- title,
47
- description,
48
- suggestion
49
- }: {
50
- icon: React.ComponentType<{ className?: string }>;
51
- title: string;
52
- description: string;
53
- suggestion?: string;
54
- }) {
55
- return (
56
- <div className="flex flex-col items-center justify-center py-8 text-center">
57
- <Icon className="h-12 w-12 text-muted-foreground/50 mb-4" />
58
- <h3 className="text-sm font-medium text-muted-foreground">{title}</h3>
59
- <p className="text-xs text-muted-foreground/70 mt-1 max-w-xs">{description}</p>
60
- {suggestion && (
61
- <p className="text-xs text-primary mt-2">{suggestion}</p>
62
- )}
63
- </div>
64
- );
65
- }
66
-
67
- /**
68
- * Simple file card for list view
69
- */
70
- function FileCard({
71
- file,
72
- searchQuery,
73
- onClick,
74
- }: {
75
- file: ContextFile;
76
- searchQuery?: string;
77
- onClick: () => void;
78
- }) {
79
- const { t } = useTranslation('common');
80
- const matches = searchQuery ? countMatches(file.content, searchQuery) : 0;
81
- const hasMatch = !searchQuery || matches > 0 || file.name.toLowerCase().includes(searchQuery.toLowerCase());
82
-
83
- if (!hasMatch) return null;
84
-
85
- const matchesLabel = matches === 1
86
- ? t('contextPage.badges.matchesSingular', { count: matches })
87
- : t('contextPage.badges.matchesPlural', { count: matches });
88
-
89
- const tokensLabel = t('contextPage.badges.tokens', { formattedCount: file.tokenCount.toLocaleString() });
90
-
91
- return (
92
- <button
93
- onClick={onClick}
94
- className="w-full text-left p-4 rounded-lg border border-border bg-card hover:bg-accent transition-colors"
95
- >
96
- <div className="flex items-center justify-between gap-3">
97
- <div className="flex items-center gap-2 min-w-0">
98
- <FileText className="h-4 w-4 text-primary shrink-0" />
99
- <div className="min-w-0">
100
- <div className="text-sm font-medium truncate">{file.name}</div>
101
- <div className="text-xs text-muted-foreground truncate">{file.path}</div>
102
- </div>
103
- </div>
104
- <div className="flex items-center gap-2 shrink-0">
105
- {searchQuery && matches > 0 && (
106
- <Badge variant="secondary" className="text-xs bg-yellow-100 dark:bg-yellow-900">
107
- {matchesLabel}
108
- </Badge>
109
- )}
110
- <Badge variant="outline" className="text-xs">
111
- <Coins className="h-3 w-3 mr-1" />
112
- {tokensLabel}
113
- </Badge>
114
- </div>
115
- </div>
116
- </button>
117
- );
118
- }
119
-
120
- /**
121
- * Section with collapsible file cards
122
- */
123
- function ContextSection({
124
- title,
125
- description,
126
- icon: Icon,
127
- files,
128
- emptyMessage,
129
- emptySuggestion,
130
- searchQuery,
131
- onFileSelect,
132
- }: {
133
- title: string;
134
- description: string;
135
- icon: React.ComponentType<{ className?: string }>;
136
- files: ContextFile[];
137
- emptyMessage: string;
138
- emptySuggestion?: string;
139
- searchQuery?: string;
140
- onFileSelect?: (file: ContextFile) => void;
141
- }) {
142
- const { t } = useTranslation('common');
143
- const totalTokens = files.reduce((sum, f) => sum + f.tokenCount, 0);
144
-
145
- // Filter files by search if query exists
146
- const filteredFiles = React.useMemo(() => {
147
- if (!searchQuery || searchQuery.length < 2) return files;
148
- return files.filter(file => {
149
- const matches = countMatches(file.content, searchQuery);
150
- const nameMatch = file.name.toLowerCase().includes(searchQuery.toLowerCase());
151
- return matches > 0 || nameMatch;
152
- });
153
- }, [files, searchQuery]);
154
-
155
- // Calculate total matches in this section
156
- const totalMatches = React.useMemo(() => {
157
- if (!searchQuery || searchQuery.length < 2) return 0;
158
- return filteredFiles.reduce((sum, file) => sum + countMatches(file.content, searchQuery), 0);
159
- }, [filteredFiles, searchQuery]);
160
-
161
- const filteredCount = filteredFiles.length;
162
-
163
- const totalMatchesLabel = React.useMemo(() => (
164
- totalMatches === 1
165
- ? t('contextPage.badges.matchesSingular', { count: totalMatches })
166
- : t('contextPage.badges.matchesPlural', { count: totalMatches })
167
- ), [t, totalMatches]);
168
-
169
- const filesLabel = React.useMemo(() => {
170
- if (filteredCount === files.length) {
171
- return filteredCount === 1
172
- ? t('contextPage.badges.filesSingular', { count: filteredCount })
173
- : t('contextPage.badges.filesPlural', { count: filteredCount });
174
- }
175
- return t('contextPage.badges.filesFiltered', { count: filteredCount, total: files.length });
176
- }, [files.length, filteredCount, t]);
177
-
178
- const tokensLabel = React.useMemo(
179
- () => t('contextPage.badges.tokens', { formattedCount: totalTokens.toLocaleString() }),
180
- [t, totalTokens]
181
- );
182
-
183
- const emptyTitle = searchQuery
184
- ? t('contextPage.search.noMatchesTitle')
185
- : emptyMessage;
186
-
187
- const emptyDescription = searchQuery
188
- ? t('contextPage.search.noMatchesDescription')
189
- : t('contextPage.search.noFilesDescription');
190
-
191
- return (
192
- <Card>
193
- <CardHeader className="pb-3">
194
- <div className="flex items-center justify-between">
195
- <div className="flex items-center gap-3">
196
- <div className="p-2 rounded-lg bg-primary/10">
197
- <Icon className="h-5 w-5 text-primary" />
198
- </div>
199
- <div>
200
- <CardTitle className="text-lg">{title}</CardTitle>
201
- <CardDescription>{description}</CardDescription>
202
- </div>
203
- </div>
204
- {files.length > 0 && (
205
- <div className="flex items-center gap-2">
206
- {searchQuery && totalMatches > 0 && (
207
- <Badge variant="secondary" className="text-xs bg-yellow-100 dark:bg-yellow-900 text-yellow-800 dark:text-yellow-200">
208
- {totalMatchesLabel}
209
- </Badge>
210
- )}
211
- <Badge variant="secondary" className="text-xs">
212
- {filesLabel}
213
- </Badge>
214
- <Badge variant="outline" className={cn('text-xs', getTotalTokenColor(totalTokens))}>
215
- <Coins className="h-3 w-3 mr-1" />
216
- {tokensLabel}
217
- </Badge>
218
- </div>
219
- )}
220
- </div>
221
- </CardHeader>
222
- <CardContent>
223
- {filteredFiles.length === 0 ? (
224
- <EmptyState
225
- icon={AlertCircle}
226
- title={emptyTitle}
227
- description={emptyDescription}
228
- suggestion={searchQuery ? undefined : emptySuggestion}
229
- />
230
- ) : (
231
- <div className="space-y-3">
232
- {filteredFiles.map((file) => (
233
- <FileCard
234
- key={file.path}
235
- file={file}
236
- searchQuery={searchQuery}
237
- onClick={() => onFileSelect?.(file)}
238
- />
239
- ))}
240
- </div>
241
- )}
242
- </CardContent>
243
- </Card>
244
- );
245
- }
246
-
247
- export function ContextClient({ context }: ContextClientProps) {
248
- const [copiedAll, setCopiedAll] = React.useState(false);
249
- const [searchQuery, setSearchQuery] = React.useState('');
250
- const [selectedFile, setSelectedFile] = React.useState<ContextFile | null>(null);
251
- const { t } = useTranslation('common');
252
-
253
- // Collect all content for "Copy All" feature
254
- const handleCopyAll = async () => {
255
- const allContent: string[] = [];
256
-
257
- // Agent instructions
258
- for (const file of context.agentInstructions) {
259
- allContent.push(`# ${file.path}\n\n${file.content}\n`);
260
- }
261
-
262
- // Config
263
- if (context.config.file) {
264
- allContent.push(`# ${context.config.file.path}\n\n${context.config.file.content}\n`);
265
- }
266
-
267
- // Project docs
268
- for (const file of context.projectDocs) {
269
- allContent.push(`# ${file.path}\n\n${file.content}\n`);
270
- }
271
-
272
- try {
273
- await navigator.clipboard.writeText(allContent.join('\n---\n\n'));
274
- setCopiedAll(true);
275
- setTimeout(() => setCopiedAll(false), 2000);
276
- } catch (error) {
277
- console.error('Failed to copy all content:', error);
278
- }
279
- };
280
-
281
- const hasAnyContent =
282
- context.agentInstructions.length > 0 ||
283
- context.config.file !== null ||
284
- context.projectDocs.length > 0;
285
-
286
- // Calculate total matches across all files
287
- const totalMatches = React.useMemo(() => {
288
- if (!searchQuery || searchQuery.length < 2) return 0;
289
- let total = 0;
290
- for (const file of context.agentInstructions) {
291
- total += countMatches(file.content, searchQuery);
292
- }
293
- if (context.config.file) {
294
- total += countMatches(context.config.file.content, searchQuery);
295
- }
296
- for (const file of context.projectDocs) {
297
- total += countMatches(file.content, searchQuery);
298
- }
299
- return total;
300
- }, [context, searchQuery]);
301
-
302
- // Convert ContextFile to the format expected by ContextFileDetail
303
- const selectedFileForDetail = selectedFile ? {
304
- name: selectedFile.name,
305
- path: selectedFile.path,
306
- content: selectedFile.content,
307
- size: new Blob([selectedFile.content]).size,
308
- tokenCount: selectedFile.tokenCount,
309
- lineCount: selectedFile.content.split('\n').length,
310
- modified: selectedFile.lastModified instanceof Date ? selectedFile.lastModified.toISOString() : selectedFile.lastModified,
311
- } : null;
312
-
313
- // If a file is selected, show the detail view
314
- if (selectedFile && selectedFileForDetail) {
315
- return (
316
- <div className="min-h-screen bg-background">
317
- <ContextFileDetail
318
- file={selectedFileForDetail}
319
- projectRoot={context.projectRoot}
320
- onBack={() => setSelectedFile(null)}
321
- />
322
- </div>
323
- );
324
- }
325
-
326
- return (
327
- <div className="space-y-6 p-6">
328
- <PageHeader
329
- title={t('contextPage.title')}
330
- description={t('contextPage.description')}
331
- actions={hasAnyContent ? (
332
- <Button
333
- variant="outline"
334
- size="sm"
335
- onClick={handleCopyAll}
336
- className="shrink-0"
337
- >
338
- {copiedAll ? (
339
- <>
340
- <Check className="h-4 w-4 mr-2 text-green-600" />
341
- {t('contextPage.copyAllSuccess')}
342
- </>
343
- ) : (
344
- <>
345
- <Copy className="h-4 w-4 mr-2" />
346
- {t('contextPage.copyAll')}
347
- </>
348
- )}
349
- </Button>
350
- ) : undefined}
351
- />
352
-
353
- {/* Search and Summary */}
354
- {hasAnyContent && (
355
- <div className="space-y-4">
356
- {/* Search bar */}
357
- <div className="relative">
358
- <Search className="absolute left-3 top-1/2 -translate-y-1/2 h-4 w-4 text-muted-foreground" />
359
- <Input
360
- type="text"
361
- placeholder={t('contextPage.searchPlaceholder')}
362
- value={searchQuery}
363
- onChange={(e) => setSearchQuery(e.target.value)}
364
- className="pl-10 pr-10"
365
- />
366
- {searchQuery && (
367
- <Button
368
- variant="ghost"
369
- size="sm"
370
- className="absolute right-1 top-1/2 -translate-y-1/2 h-7 w-7 p-0"
371
- onClick={() => setSearchQuery('')}
372
- >
373
- <X className="h-4 w-4" />
374
- </Button>
375
- )}
376
- </div>
377
-
378
- {/* Search results info */}
379
- {searchQuery && searchQuery.length >= 2 && (
380
- <div className="flex items-center gap-2 text-sm">
381
- {totalMatches > 0 ? (
382
- <Badge variant="secondary" className="bg-yellow-100 dark:bg-yellow-900 text-yellow-800 dark:text-yellow-200">
383
- {totalMatches === 1
384
- ? t('contextPage.badges.matchesFoundSingular', { count: totalMatches })
385
- : t('contextPage.badges.matchesFoundPlural', { count: totalMatches })}
386
- </Badge>
387
- ) : (
388
- <span className="text-muted-foreground">
389
- {t('contextPage.searchNoMatches', { query: searchQuery })}
390
- </span>
391
- )}
392
- </div>
393
- )}
394
-
395
- {/* Summary card */}
396
- <Card className="bg-muted/30">
397
- <CardContent className="py-4">
398
- <div className="flex flex-wrap items-center gap-4 sm:gap-8">
399
- <div className="flex items-center gap-2">
400
- <Coins className={cn('h-5 w-5', getTotalTokenColor(context.totalTokens))} />
401
- <span className="text-sm">
402
- <strong className={getTotalTokenColor(context.totalTokens)}>
403
- {t('contextPage.summary.totalTokens', { formattedCount: context.totalTokens.toLocaleString() })}
404
- </strong>
405
- </span>
406
- </div>
407
- <div className="flex items-center gap-2 text-sm text-muted-foreground">
408
- <Info className="h-4 w-4" />
409
- {t('contextPage.summary.contextBudget')}
410
- </div>
411
- </div>
412
- </CardContent>
413
- </Card>
414
- </div>
415
- )}
416
-
417
- {/* Content sections */}
418
- <div className="space-y-6">
419
- {/* Agent Instructions */}
420
- <ContextSection
421
- title={t('contextPage.sections.agents.title')}
422
- description={t('contextPage.sections.agents.description')}
423
- icon={BookOpen}
424
- files={context.agentInstructions}
425
- emptyMessage={t('contextPage.sections.agents.empty')}
426
- emptySuggestion={t('contextPage.sections.agents.suggestion')}
427
- searchQuery={searchQuery}
428
- onFileSelect={setSelectedFile}
429
- />
430
-
431
- {/* Configuration */}
432
- <ContextSection
433
- title={t('contextPage.sections.config.title')}
434
- description={t('contextPage.sections.config.description')}
435
- icon={Settings}
436
- files={context.config.file ? [context.config.file] : []}
437
- emptyMessage={t('contextPage.sections.config.empty')}
438
- emptySuggestion={t('contextPage.sections.config.suggestion')}
439
- searchQuery={searchQuery}
440
- onFileSelect={setSelectedFile}
441
- />
442
-
443
- {/* Project Documentation */}
444
- <ContextSection
445
- title={t('contextPage.sections.docs.title')}
446
- description={t('contextPage.sections.docs.description')}
447
- icon={FileText}
448
- files={context.projectDocs}
449
- emptyMessage={t('contextPage.sections.docs.empty')}
450
- emptySuggestion={t('contextPage.sections.docs.suggestion')}
451
- searchQuery={searchQuery}
452
- onFileSelect={setSelectedFile}
453
- />
454
- </div>
455
-
456
- {/* No content state */}
457
- {!hasAnyContent && (
458
- <Card className="mt-8">
459
- <CardContent className="py-12">
460
- <EmptyState
461
- icon={BookOpen}
462
- title={t('contextPage.emptyState.title')}
463
- description={t('contextPage.emptyState.description')}
464
- suggestion={t('contextPage.emptyState.suggestion')}
465
- />
466
- </CardContent>
467
- </Card>
468
- )}
469
- </div>
470
- );
471
- }
@@ -1,163 +0,0 @@
1
- import { useMemo, useState } from 'react';
2
- import ReactMarkdown from 'react-markdown';
3
- import remarkGfm from 'remark-gfm';
4
- import rehypeHighlight from 'rehype-highlight';
5
- import rehypeSlug from 'rehype-slug';
6
- import { ArrowLeft, Clock, Copy, ExternalLink, FileText, Hash, Layers, Type } from 'lucide-react';
7
- import { Badge, Button } from '@leanspec/ui-components';
8
- import { TableOfContents, TableOfContentsSidebar } from '../spec-detail/TableOfContents';
9
- import { MermaidDiagram } from '../MermaidDiagram';
10
- import type { ContextFileContent } from '../../types/api';
11
- import { useTranslation } from 'react-i18next';
12
- import { formatDate } from '../../lib/date-utils';
13
-
14
- interface ContextFileDetailProps {
15
- file: ContextFileContent;
16
- projectRoot?: string;
17
- onBack?: () => void;
18
- }
19
-
20
- function toVSCodeUri(projectRoot: string, filePath: string) {
21
- const fullPath = filePath.startsWith('/') ? filePath : `${projectRoot}/${filePath}`;
22
- return `vscode://file${fullPath}`;
23
- }
24
-
25
- export function ContextFileDetail({ file, projectRoot, onBack }: ContextFileDetailProps) {
26
- const [copied, setCopied] = useState(false);
27
- const isMarkdown = file.name.toLowerCase().endsWith('.md');
28
- const isJson = file.name.toLowerCase().endsWith('.json');
29
- const { t, i18n } = useTranslation('common');
30
-
31
- const headingContent = useMemo(() => file.content, [file.content]);
32
-
33
- const handleCopy = async () => {
34
- try {
35
- await navigator.clipboard.writeText(file.content);
36
- setCopied(true);
37
- setTimeout(() => setCopied(false), 1500);
38
- } catch (err) {
39
- console.error('Failed to copy context content', err);
40
- }
41
- };
42
-
43
- const handleOpenInEditor = () => {
44
- if (!projectRoot) return;
45
- window.open(toVSCodeUri(projectRoot, file.path), '_blank');
46
- };
47
-
48
- return (
49
- <div className="space-y-6 px-6 py-3">
50
- <header className="flex flex-col gap-3 sticky top-14 bg-background py-2 z-10">
51
- <div className="flex items-center justify-between gap-3">
52
- <div className="flex items-center gap-2">
53
- {onBack && (
54
- <Button variant="ghost" size="sm" onClick={onBack} className="h-8 px-2">
55
- <ArrowLeft className="h-4 w-4 mr-1" />
56
- {t('contextPage.detail.back')}
57
- </Button>
58
- )}
59
- <FileText className="h-5 w-5 text-primary" />
60
- <div className="flex flex-col">
61
- <h2 className="text-lg sm:text-xl font-semibold leading-tight break-words">{file.name}</h2>
62
- <span className="text-xs text-muted-foreground break-all">{file.path}</span>
63
- </div>
64
- </div>
65
- <div className="flex items-center gap-2">
66
- {projectRoot && (
67
- <Button variant="ghost" size="sm" onClick={handleOpenInEditor} className="h-8 px-3 text-xs">
68
- <ExternalLink className="h-3.5 w-3.5 mr-1" />
69
- {t('contextPage.detail.openInEditor')}
70
- </Button>
71
- )}
72
- <Button variant="ghost" size="sm" onClick={handleCopy} className="h-8 px-3 text-xs">
73
- {copied ? <Hash className="h-3.5 w-3.5 mr-1 text-green-600" /> : <Copy className="h-3.5 w-3.5 mr-1" />}
74
- {copied ? t('contextPage.detail.copySuccess') : t('contextPage.detail.copy')}
75
- </Button>
76
- </div>
77
- </div>
78
- <div className="flex flex-wrap items-center gap-2 text-xs text-muted-foreground">
79
- <Badge variant="outline" className="text-xs flex items-center gap-1">
80
- <Type className="h-3 w-3" />
81
- {file.fileType || t('contextPage.detail.defaultFileType')}
82
- </Badge>
83
- <Badge variant="outline" className="text-xs flex items-center gap-1">
84
- <Layers className="h-3 w-3" />
85
- {t('contextPage.badges.tokens', { formattedCount: file.tokenCount.toLocaleString() })}
86
- </Badge>
87
- <Badge variant="outline" className="text-xs flex items-center gap-1">
88
- <FileText className="h-3 w-3" />
89
- {t('contextPage.detail.lines', { count: file.lineCount })}
90
- </Badge>
91
- <span className="flex items-center gap-1">
92
- <Clock className="h-3.5 w-3.5" />
93
- {t('contextPage.detail.modified', {
94
- date: formatDate(file.modified ?? file.modifiedAt, i18n.language),
95
- })}
96
- </span>
97
- <span className="text-muted-foreground">•</span>
98
- <span>{t('contextPage.detail.size', { size: (file.size / 1024).toFixed(1) })}</span>
99
- </div>
100
- </header>
101
-
102
- <div className="grid gap-6 xl:grid-cols-[minmax(0,1fr)_320px]">
103
- <div className="overflow-hidden p-4 sm:p-6">
104
- {isJson ? (
105
- <pre className="p-3 text-sm overflow-x-auto bg-muted/40 rounded-md border whitespace-pre-wrap">
106
- {(() => {
107
- try {
108
- return JSON.stringify(JSON.parse(file.content), null, 2);
109
- } catch (err) {
110
- console.error('Failed to parse JSON context file', err);
111
- return file.content;
112
- }
113
- })()}
114
- </pre>
115
- ) : isMarkdown ? (
116
- <article className="prose prose-slate dark:prose-invert max-w-none prose-sm sm:prose-base">
117
- <ReactMarkdown
118
- remarkPlugins={[remarkGfm]}
119
- rehypePlugins={[rehypeHighlight, rehypeSlug]}
120
- components={{
121
- pre: ({ children, ...props }) => {
122
- const childArray = Array.isArray(children) ? children : [children];
123
- const firstChild = childArray[0];
124
- if (
125
- firstChild &&
126
- typeof firstChild === 'object' &&
127
- 'props' in firstChild &&
128
- typeof (firstChild as { props?: { className?: string; children?: string } }).props?.className === 'string' &&
129
- (firstChild as { props?: { className?: string } }).props?.className?.includes('language-mermaid')
130
- ) {
131
- const code = (firstChild as { props?: { children?: string } }).props?.children;
132
- const content = typeof code === 'string' ? code : '';
133
- return <MermaidDiagram chart={content} />;
134
- }
135
- return <pre {...props}>{children}</pre>;
136
- },
137
- }}
138
- >
139
- {file.content}
140
- </ReactMarkdown>
141
- </article>
142
- ) : (
143
- <pre className="p-3 text-sm overflow-x-auto bg-muted/40 rounded-md border whitespace-pre-wrap">
144
- {file.content}
145
- </pre>
146
- )}
147
- </div>
148
-
149
- {isMarkdown && (
150
- <aside className="hidden xl:block sticky top-28 h-[calc(100vh-8rem)] overflow-y-auto">
151
- <TableOfContentsSidebar content={headingContent} />
152
- </aside>
153
- )}
154
- </div>
155
-
156
- {isMarkdown && (
157
- <div className="xl:hidden">
158
- <TableOfContents content={headingContent} />
159
- </div>
160
- )}
161
- </div>
162
- );
163
- }