@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,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
- }