@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,382 +0,0 @@
1
- // Backend adapter pattern for web (HTTP) vs desktop (Tauri IPC)
2
- // This allows the same UI code to work in both browser and Tauri contexts
3
-
4
- import type {
5
- ContextFileContent,
6
- ContextFileListItem,
7
- DependencyGraph,
8
- DirectoryListResponse,
9
- ListParams,
10
- Spec,
11
- SpecDetail,
12
- Stats,
13
- Project,
14
- ProjectStatsResponse,
15
- ProjectValidationResponse,
16
- ProjectsResponse,
17
- ListSpecsResponse,
18
- ProjectContext,
19
- } from '../types/api';
20
-
21
- export class APIError extends Error {
22
- status: number;
23
-
24
- constructor(status: number, message: string) {
25
- super(message);
26
- this.status = status;
27
- this.name = 'APIError';
28
- }
29
- }
30
-
31
- /**
32
- * Backend adapter interface - abstracts the communication layer
33
- * Web uses HTTP fetch, Desktop uses Tauri invoke
34
- */
35
- export interface BackendAdapter {
36
- // Project operations
37
- getProjects(): Promise<ProjectsResponse>;
38
- createProject(
39
- path: string,
40
- options?: { favorite?: boolean; color?: string; name?: string; description?: string | null }
41
- ): Promise<Project>;
42
- updateProject(
43
- projectId: string,
44
- updates: Partial<Pick<Project, 'name' | 'color' | 'favorite' | 'description'>>
45
- ): Promise<Project | undefined>;
46
- deleteProject(projectId: string): Promise<void>;
47
- validateProject(projectId: string): Promise<ProjectValidationResponse>;
48
-
49
- // Spec operations
50
- getSpecs(projectId: string, params?: ListParams): Promise<Spec[]>;
51
- getSpec(projectId: string, specName: string): Promise<SpecDetail>;
52
- updateSpec(projectId: string, specName: string, updates: Partial<Pick<Spec, 'status' | 'priority' | 'tags'>>): Promise<void>;
53
-
54
- // Stats and dependencies
55
- getStats(projectId: string): Promise<Stats>;
56
- getProjectStats(projectId: string): Promise<Stats>;
57
- getDependencies(projectId: string, specName?: string): Promise<DependencyGraph>;
58
-
59
- // Context files & local filesystem
60
- getContextFiles(): Promise<ContextFileListItem[]>;
61
- getContextFile(path: string): Promise<ContextFileContent>;
62
- getProjectContext(projectId: string): Promise<ProjectContext>;
63
- listDirectory(path?: string): Promise<DirectoryListResponse>;
64
- }
65
-
66
- /**
67
- * HTTP adapter for web browser - connects to Rust HTTP server
68
- */
69
- export class HttpBackendAdapter implements BackendAdapter {
70
- private baseUrl: string;
71
-
72
- constructor(baseUrl?: string) {
73
- this.baseUrl = baseUrl || import.meta.env.VITE_API_URL || 'http://localhost:3333';
74
- }
75
-
76
- private async fetchAPI<T>(endpoint: string, options?: RequestInit): Promise<T> {
77
- const response = await fetch(`${this.baseUrl}${endpoint}`, {
78
- ...options,
79
- headers: {
80
- 'Content-Type': 'application/json',
81
- ...options?.headers,
82
- },
83
- });
84
-
85
- if (!response.ok) {
86
- const raw = await response.text();
87
- let message = raw || response.statusText;
88
-
89
- try {
90
- const parsed = JSON.parse(raw);
91
- if (typeof parsed.message === 'string') {
92
- message = parsed.message;
93
- } else if (typeof parsed.error === 'string') {
94
- message = parsed.error;
95
- } else if (typeof parsed.detail === 'string') {
96
- message = parsed.detail;
97
- }
98
- } catch {
99
- // Fall back to raw message
100
- }
101
-
102
- throw new APIError(response.status, message || response.statusText);
103
- }
104
-
105
- if (response.status === 204) {
106
- return undefined as T;
107
- }
108
-
109
- const text = await response.text();
110
- if (!text) {
111
- return undefined as T;
112
- }
113
-
114
- try {
115
- return JSON.parse(text) as T;
116
- } catch (err) {
117
- throw new APIError(response.status, err instanceof Error ? err.message : 'Failed to parse response');
118
- }
119
- }
120
-
121
- async getProjects(): Promise<ProjectsResponse> {
122
- const data = await this.fetchAPI<ProjectsResponse>('/api/projects');
123
- return data;
124
- }
125
-
126
- async createProject(
127
- path: string,
128
- options?: { favorite?: boolean; color?: string; name?: string; description?: string | null }
129
- ): Promise<Project> {
130
- // Rust backend returns Project directly (not wrapped in { project: ... })
131
- const project = await this.fetchAPI<Project>('/api/projects', {
132
- method: 'POST',
133
- body: JSON.stringify({ path, ...options }),
134
- });
135
- return project;
136
- }
137
-
138
- async updateProject(
139
- projectId: string,
140
- updates: Partial<Pick<Project, 'name' | 'color' | 'favorite' | 'description'>>
141
- ): Promise<Project | undefined> {
142
- // Rust backend returns Project directly (not wrapped in { project: ... })
143
- const project = await this.fetchAPI<Project>(`/api/projects/${encodeURIComponent(projectId)}`, {
144
- method: 'PATCH',
145
- body: JSON.stringify(updates),
146
- });
147
- return project;
148
- }
149
-
150
- async deleteProject(projectId: string): Promise<void> {
151
- await this.fetchAPI(`/api/projects/${encodeURIComponent(projectId)}`, {
152
- method: 'DELETE',
153
- });
154
- }
155
-
156
- async validateProject(projectId: string): Promise<ProjectValidationResponse> {
157
- return this.fetchAPI<ProjectValidationResponse>(`/api/projects/${encodeURIComponent(projectId)}/validate`, {
158
- method: 'POST',
159
- });
160
- }
161
-
162
- async getSpecs(projectId: string, params?: ListParams): Promise<Spec[]> {
163
- const query = params
164
- ? new URLSearchParams(
165
- Object.entries(params).reduce<string[][]>((acc, [key, value]) => {
166
- if (typeof value === 'string' && value.length > 0) acc.push([key, value]);
167
- return acc;
168
- }, [])
169
- ).toString()
170
- : '';
171
- const endpoint = query
172
- ? `/api/projects/${encodeURIComponent(projectId)}/specs?${query}`
173
- : `/api/projects/${encodeURIComponent(projectId)}/specs`;
174
- const data = await this.fetchAPI<ListSpecsResponse>(endpoint);
175
- return data.specs || [];
176
- }
177
-
178
- async getSpec(projectId: string, specName: string): Promise<SpecDetail> {
179
- const data = await this.fetchAPI<SpecDetail | { spec: SpecDetail }>(
180
- `/api/projects/${encodeURIComponent(projectId)}/specs/${encodeURIComponent(specName)}`
181
- );
182
- return 'spec' in data ? data.spec : data;
183
- }
184
-
185
- async updateSpec(
186
- projectId: string,
187
- specName: string,
188
- updates: Partial<Pick<Spec, 'status' | 'priority' | 'tags'>>
189
- ): Promise<void> {
190
- await this.fetchAPI(`/api/projects/${encodeURIComponent(projectId)}/specs/${encodeURIComponent(specName)}/metadata`, {
191
- method: 'PATCH',
192
- body: JSON.stringify(updates),
193
- });
194
- }
195
-
196
- async getStats(projectId: string): Promise<Stats> {
197
- const data = await this.fetchAPI<Stats | { stats: Stats }>(
198
- `/api/projects/${encodeURIComponent(projectId)}/stats`
199
- );
200
- return 'stats' in data ? data.stats : data;
201
- }
202
-
203
- async getProjectStats(projectId: string): Promise<Stats> {
204
- const data = await this.fetchAPI<ProjectStatsResponse | Stats>(
205
- `/api/projects/${encodeURIComponent(projectId)}/stats`
206
- );
207
- const statsPayload = 'stats' in data ? data.stats : data;
208
- return statsPayload;
209
- }
210
-
211
- async getDependencies(projectId: string, _specName?: string): Promise<DependencyGraph> {
212
- // Note: specName parameter is ignored for HTTP adapter as the project endpoint
213
- // returns the full dependency graph. Individual spec dependencies can be computed client-side.
214
- const data = await this.fetchAPI<DependencyGraph>(
215
- `/api/projects/${encodeURIComponent(projectId)}/dependencies`
216
- );
217
- return data;
218
- }
219
-
220
- async getContextFiles(): Promise<ContextFileListItem[]> {
221
- const data = await this.fetchAPI<{ files?: ContextFileListItem[] }>('/api/context');
222
- return data.files || [];
223
- }
224
-
225
- async getContextFile(path: string): Promise<ContextFileContent> {
226
- const safePath = encodeURIComponent(path);
227
- const data = await this.fetchAPI<ContextFileContent>(
228
- `/api/context/${safePath}`
229
- );
230
- return data;
231
- }
232
-
233
- async getProjectContext(projectId: string): Promise<ProjectContext> {
234
- const data = await this.fetchAPI<ProjectContext>(
235
- `/api/projects/${encodeURIComponent(projectId)}/context`
236
- );
237
- return data;
238
- }
239
-
240
- async listDirectory(path = ''): Promise<DirectoryListResponse> {
241
- return this.fetchAPI<DirectoryListResponse>('/api/local-projects/list-directory', {
242
- method: 'POST',
243
- body: JSON.stringify({ path }),
244
- });
245
- }
246
- }
247
-
248
- /**
249
- * Tauri adapter for desktop app - uses IPC commands
250
- */
251
- export class TauriBackendAdapter implements BackendAdapter {
252
- private async invoke<T>(command: string, args?: Record<string, unknown>): Promise<T> {
253
- // Dynamic import to avoid bundling Tauri in web builds
254
- const { invoke } = await import('@tauri-apps/api/core');
255
- return invoke<T>(command, args);
256
- }
257
-
258
- async getProjects(): Promise<ProjectsResponse> {
259
- const data = await this.invoke<{
260
- projects: Project[];
261
- recentProjects?: string[];
262
- favoriteProjects?: string[];
263
- }>('desktop_bootstrap');
264
-
265
- return {
266
- projects: data.projects,
267
- recentProjects: data.recentProjects,
268
- favoriteProjects: data.favoriteProjects,
269
- };
270
- }
271
-
272
- async createProject(
273
- _path: string,
274
- _options?: { favorite?: boolean; color?: string; name?: string; description?: string | null }
275
- ): Promise<Project> {
276
- throw new Error('createProject is not implemented for the Tauri backend yet');
277
- }
278
-
279
- async updateProject(
280
- _projectId: string,
281
- _updates: Partial<Pick<Project, 'name' | 'color' | 'favorite' | 'description'>>
282
- ): Promise<Project | undefined> {
283
- throw new Error('updateProject is not implemented for the Tauri backend yet');
284
- }
285
-
286
- async deleteProject(_projectId: string): Promise<void> {
287
- throw new Error('deleteProject is not implemented for the Tauri backend yet');
288
- }
289
-
290
- async validateProject(_projectId: string): Promise<ProjectValidationResponse> {
291
- throw new Error('validateProject is not implemented for the Tauri backend yet');
292
- }
293
-
294
- async getSpecs(projectId: string, _params?: ListParams): Promise<Spec[]> {
295
- // Tauri commands return LightweightSpec[], need to map to Spec[]
296
- const specs = await this.invoke<Spec[]>('get_specs', {
297
- projectId
298
- });
299
- return specs;
300
- }
301
-
302
- async getSpec(projectId: string, specName: string): Promise<SpecDetail> {
303
- const spec = await this.invoke<SpecDetail>('get_spec_detail', {
304
- projectId,
305
- specId: specName,
306
- });
307
- return spec;
308
- }
309
-
310
- async updateSpec(
311
- projectId: string,
312
- specName: string,
313
- updates: Partial<Pick<Spec, 'status' | 'priority' | 'tags'>>
314
- ): Promise<void> {
315
- // For now, only status update is supported
316
- if (updates.status) {
317
- await this.invoke('update_spec_status', {
318
- projectId,
319
- specId: specName,
320
- newStatus: updates.status,
321
- });
322
- }
323
- }
324
-
325
- async getStats(projectId: string): Promise<Stats> {
326
- const stats = await this.invoke<Stats>('get_project_stats', {
327
- projectId,
328
- });
329
- return stats;
330
- }
331
-
332
- async getProjectStats(projectId: string): Promise<Stats> {
333
- const stats = await this.invoke<Stats>('get_project_stats', {
334
- projectId,
335
- });
336
- return stats;
337
- }
338
-
339
- async getDependencies(projectId: string, _specName?: string): Promise<DependencyGraph> {
340
- return this.invoke<DependencyGraph>('get_dependency_graph', {
341
- projectId,
342
- });
343
- }
344
-
345
- async getContextFiles(): Promise<ContextFileListItem[]> {
346
- throw new Error('getContextFiles is not implemented for the Tauri backend yet');
347
- }
348
-
349
- async getContextFile(_path: string): Promise<ContextFileContent> {
350
- throw new Error('getContextFile is not implemented for the Tauri backend yet');
351
- }
352
-
353
- async getProjectContext(_projectId: string): Promise<ProjectContext> {
354
- throw new Error('getProjectContext is not implemented for the Tauri backend yet');
355
- }
356
-
357
- async listDirectory(_path = ''): Promise<DirectoryListResponse> {
358
- throw new Error('listDirectory is not implemented for the Tauri backend yet');
359
- }
360
- }
361
-
362
- /**
363
- * Detect environment and create appropriate backend adapter
364
- */
365
- export function createBackendAdapter(): BackendAdapter {
366
- // Check if running in Tauri
367
- // @ts-expect-error __TAURI__ is injected by Tauri at runtime
368
- if (typeof window !== 'undefined' && window.__TAURI__) {
369
- return new TauriBackendAdapter();
370
- }
371
- return new HttpBackendAdapter();
372
- }
373
-
374
- // Export singleton instance
375
- let backendInstance: BackendAdapter | null = null;
376
-
377
- export function getBackend(): BackendAdapter {
378
- if (!backendInstance) {
379
- backendInstance = createBackendAdapter();
380
- }
381
- return backendInstance;
382
- }
@@ -1,122 +0,0 @@
1
- /**
2
- * Utility functions for date formatting
3
- */
4
-
5
- import dayjs from 'dayjs';
6
- import relativeTime from 'dayjs/plugin/relativeTime';
7
- import 'dayjs/locale/zh-cn';
8
-
9
- dayjs.extend(relativeTime);
10
-
11
- function isChineseLocale(locale?: string) {
12
- if (!locale) return false;
13
- return locale.toLowerCase().startsWith('zh');
14
- }
15
-
16
- function resolveDayjsLocale(locale?: string) {
17
- return isChineseLocale(locale) ? 'zh-cn' : 'en';
18
- }
19
-
20
- /**
21
- * Format a date as relative time (e.g., "2 days ago")
22
- */
23
- export function formatRelativeTime(
24
- date: Date | string | number | null | undefined,
25
- locale?: string
26
- ): string {
27
- if (!date) return isChineseLocale(locale) ? '未知' : 'Unknown';
28
- return dayjs(date).locale(resolveDayjsLocale(locale)).fromNow();
29
- }
30
-
31
- /**
32
- * Format a date in a readable format (e.g., "Nov 12, 2025")
33
- */
34
- export function formatDate(
35
- date: Date | string | number | null | undefined,
36
- locale?: string
37
- ): string {
38
- if (!date) return isChineseLocale(locale) ? '未知' : 'Unknown';
39
- return dayjs(date).locale(resolveDayjsLocale(locale)).format('MMM D, YYYY');
40
- }
41
-
42
- /**
43
- * Format a date with time (e.g., "Nov 12, 2025 10:30 AM")
44
- */
45
- export function formatDateTime(
46
- date: Date | string | number | null | undefined,
47
- locale?: string
48
- ): string {
49
- if (!date) return isChineseLocale(locale) ? '未知' : 'Unknown';
50
- return dayjs(date).locale(resolveDayjsLocale(locale)).format('MMM D, YYYY h:mm A');
51
- }
52
-
53
- /**
54
- * Format duration between two dates in a human-readable format
55
- */
56
- export function formatDuration(
57
- start: Date | string | number | null | undefined,
58
- end: Date | string | number | null | undefined,
59
- locale?: string
60
- ): string {
61
- if (!start || !end) return '';
62
-
63
- const startDate = dayjs(start);
64
- const endDate = dayjs(end);
65
- const diffMs = endDate.diff(startDate);
66
-
67
- if (diffMs < 0) return '';
68
-
69
- const days = Math.floor(diffMs / (1000 * 60 * 60 * 24));
70
- const hours = Math.floor((diffMs % (1000 * 60 * 60 * 24)) / (1000 * 60 * 60));
71
- const minutes = Math.floor((diffMs % (1000 * 60 * 60)) / (1000 * 60));
72
- const chinese = isChineseLocale(locale);
73
-
74
- const unitFormatters = chinese
75
- ? {
76
- minute: (value: number) => `${value} 分钟`,
77
- hour: (value: number) => `${value} 小时`,
78
- day: (value: number) => `${value} 天`,
79
- month: (value: number) => `${value} 个月`,
80
- year: (value: number) => `${value} 年`,
81
- }
82
- : {
83
- minute: (value: number) => `${value}m`,
84
- hour: (value: number) => `${value}h`,
85
- day: (value: number) => `${value}d`,
86
- month: (value: number) => `${value}mo`,
87
- year: (value: number) => `${value}y`,
88
- };
89
-
90
- const joinValues = (...parts: string[]) => parts.filter(Boolean).join(chinese ? ' ' : ' ');
91
- const lessThanMinute = chinese ? '小于 1 分钟' : '< 1m';
92
-
93
- if (days === 0 && hours === 0) {
94
- if (minutes === 0) return lessThanMinute;
95
- return unitFormatters.minute(minutes);
96
- }
97
-
98
- if (days === 0) {
99
- return unitFormatters.hour(hours);
100
- }
101
-
102
- if (days < 30) {
103
- return hours > 0
104
- ? joinValues(unitFormatters.day(days), unitFormatters.hour(hours))
105
- : unitFormatters.day(days);
106
- }
107
-
108
- const months = Math.floor(days / 30);
109
- const remainingDays = days % 30;
110
-
111
- if (months < 12) {
112
- return remainingDays > 0
113
- ? joinValues(unitFormatters.month(months), unitFormatters.day(remainingDays))
114
- : unitFormatters.month(months);
115
- }
116
-
117
- const years = Math.floor(months / 12);
118
- const remainingMonths = months % 12;
119
- return remainingMonths > 0
120
- ? joinValues(unitFormatters.year(years), unitFormatters.month(remainingMonths))
121
- : unitFormatters.year(years);
122
- }
@@ -1,57 +0,0 @@
1
- import { beforeEach, describe, expect, it } from 'vitest';
2
- import i18n from './i18n';
3
-
4
- describe('i18n configuration', () => {
5
- beforeEach(() => {
6
- localStorage.clear();
7
- void i18n.changeLanguage('en');
8
- });
9
-
10
- it('should have English and Chinese languages available', () => {
11
- const languages = Object.keys(i18n.options.resources || {});
12
- expect(languages).toContain('en');
13
- expect(languages).toContain('zh-CN');
14
- });
15
-
16
- it('should have namespaces: common, errors, help', () => {
17
- expect(i18n.options.ns).toContain('common');
18
- expect(i18n.options.ns).toContain('errors');
19
- expect(i18n.options.ns).toContain('help');
20
- });
21
-
22
- it('should translate navigation.home to Chinese', async () => {
23
- await i18n.changeLanguage('zh-CN');
24
- expect(i18n.t('navigation.home', { ns: 'common' })).toBe('首页');
25
- });
26
-
27
- it('should translate Spec label to Chinese', async () => {
28
- await i18n.changeLanguage('zh-CN');
29
- expect(i18n.t('spec.spec', { ns: 'common' })).toBe('规范');
30
- });
31
-
32
- it('should translate status terms', async () => {
33
- await i18n.changeLanguage('zh-CN');
34
- expect(i18n.t('status.planned', { ns: 'common' })).toBe('已计划');
35
- expect(i18n.t('status.inProgress', { ns: 'common' })).toBe('进行中');
36
- expect(i18n.t('status.complete', { ns: 'common' })).toBe('已完成');
37
- });
38
-
39
- it('should fallback to English for missing keys', async () => {
40
- await i18n.changeLanguage('zh-CN');
41
- const result = i18n.t('nonexistent.key', {
42
- ns: 'common',
43
- defaultValue: 'fallback',
44
- });
45
- expect(result).toBe('fallback');
46
- });
47
-
48
- it('should detect browser language on init', () => {
49
- expect(i18n.options.detection).toBeDefined();
50
- });
51
-
52
- it('should persist language choice to localStorage', async () => {
53
- await i18n.changeLanguage('zh-CN');
54
- const stored = localStorage.getItem('leanspec-language');
55
- expect(stored).toBe('zh-CN');
56
- });
57
- });
package/src/lib/i18n.ts DELETED
@@ -1,51 +0,0 @@
1
- import i18n from 'i18next';
2
- import { initReactI18next } from 'react-i18next';
3
- import LanguageDetector from 'i18next-browser-languagedetector';
4
-
5
- import commonEn from '../locales/en/common.json';
6
- import errorsEn from '../locales/en/errors.json';
7
- import helpEn from '../locales/en/help.json';
8
-
9
- import commonZh from '../locales/zh-CN/common.json';
10
- import errorsZh from '../locales/zh-CN/errors.json';
11
- import helpZh from '../locales/zh-CN/help.json';
12
-
13
- const resources = {
14
- en: {
15
- common: commonEn,
16
- errors: errorsEn,
17
- help: helpEn,
18
- },
19
- 'zh-CN': {
20
- common: commonZh,
21
- errors: errorsZh,
22
- help: helpZh,
23
- },
24
- };
25
-
26
- i18n
27
- .use(LanguageDetector)
28
- .use(initReactI18next)
29
- .init({
30
- resources,
31
- fallbackLng: 'en',
32
- defaultNS: 'common',
33
- ns: ['common', 'errors', 'help'],
34
- detection: {
35
- order: ['localStorage', 'navigator'],
36
- caches: ['localStorage'],
37
- lookupLocalStorage: 'leanspec-language',
38
- },
39
- interpolation: {
40
- escapeValue: false,
41
- },
42
- });
43
-
44
- // Persist language preference for environments without detector cache writes
45
- i18n.on('languageChanged', (lng) => {
46
- if (typeof localStorage !== 'undefined') {
47
- localStorage.setItem('leanspec-language', lng);
48
- }
49
- });
50
-
51
- export default i18n;
@@ -1,38 +0,0 @@
1
- import GithubSlugger from 'github-slugger';
2
-
3
- export interface HeadingItem {
4
- id: string;
5
- text: string;
6
- level: number;
7
- }
8
-
9
- /**
10
- * Extract markdown headings while skipping fenced code blocks.
11
- */
12
- export function extractHeadings(markdown: string): HeadingItem[] {
13
- if (!markdown) return [];
14
-
15
- const headings: HeadingItem[] = [];
16
- const lines = markdown.split('\n');
17
- let inCodeBlock = false;
18
- const slugger = new GithubSlugger();
19
-
20
- for (const line of lines) {
21
- if (line.trim().startsWith('```')) {
22
- inCodeBlock = !inCodeBlock;
23
- continue;
24
- }
25
-
26
- if (inCodeBlock) continue;
27
-
28
- const match = line.match(/^(#{2,6})\s+(.+)$/);
29
- if (match) {
30
- const level = match[1].length;
31
- const text = match[2].trim();
32
- const id = slugger.slug(text);
33
- headings.push({ id, text, level });
34
- }
35
- }
36
-
37
- return headings;
38
- }