@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,85 +0,0 @@
1
- import { useEffect, useRef, useState } from 'react';
2
- import mermaid from 'mermaid';
3
- import { useTranslation } from 'react-i18next';
4
-
5
- interface MermaidDiagramProps {
6
- chart: string;
7
- className?: string;
8
- }
9
-
10
- // Initialize mermaid with configuration
11
- mermaid.initialize({
12
- startOnLoad: false,
13
- theme: 'default',
14
- securityLevel: 'loose',
15
- fontFamily: 'system-ui, -apple-system, sans-serif',
16
- });
17
-
18
- // Generate unique IDs for Mermaid diagrams using crypto.randomUUID or fallback
19
- const generateId = () => {
20
- if (typeof crypto !== 'undefined' && crypto.randomUUID) {
21
- return `mermaid-${crypto.randomUUID()}`;
22
- }
23
- // Fallback for older browsers
24
- return `mermaid-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
25
- };
26
-
27
- export function MermaidDiagram({ chart, className = '' }: MermaidDiagramProps) {
28
- const ref = useRef<HTMLDivElement>(null);
29
- const [svg, setSvg] = useState<string>('');
30
- const [error, setError] = useState<string | null>(null);
31
- const [id] = useState(generateId);
32
- const { t } = useTranslation(['common', 'errors']);
33
-
34
- useEffect(() => {
35
- if (!chart || !ref.current) return;
36
-
37
- const renderDiagram = async () => {
38
- try {
39
- setError(null);
40
- // Update theme based on document theme
41
- const isDark = document.documentElement.classList.contains('dark');
42
- mermaid.initialize({
43
- theme: isDark ? 'dark' : 'default',
44
- securityLevel: 'loose',
45
- });
46
-
47
- const { svg } = await mermaid.render(id, chart);
48
- setSvg(svg);
49
- } catch (err) {
50
- console.error('Mermaid rendering error:', err);
51
- const fallback = t('mermaid.renderError', { ns: 'errors' });
52
- setError(err instanceof Error ? err.message : fallback);
53
- }
54
- };
55
-
56
- renderDiagram();
57
- }, [chart, id]);
58
-
59
- if (error) {
60
- return (
61
- <div className={`border border-destructive rounded-lg p-4 ${className}`}>
62
- <div className="text-sm text-destructive">
63
- <strong>{t('mermaid.title', { ns: 'errors' })}</strong>
64
- <pre className="mt-2 text-xs overflow-auto">{error}</pre>
65
- </div>
66
- </div>
67
- );
68
- }
69
-
70
- if (!svg) {
71
- return (
72
- <div className={`border rounded-lg p-4 ${className}`}>
73
- <div className="text-sm text-muted-foreground">{t('mermaid.loading', { ns: 'errors' })}</div>
74
- </div>
75
- );
76
- }
77
-
78
- return (
79
- <div
80
- ref={ref}
81
- className={`mermaid-diagram border rounded-lg p-4 overflow-auto ${className}`}
82
- dangerouslySetInnerHTML={{ __html: svg }}
83
- />
84
- );
85
- }
@@ -1,51 +0,0 @@
1
- import { Outlet } from 'react-router-dom';
2
- import { Navigation } from './Navigation';
3
- import { ErrorBoundary } from './shared/ErrorBoundary';
4
- import { PageTransition } from './shared/PageTransition';
5
- import { BackToTop } from './shared/BackToTop';
6
- import { useGlobalShortcuts } from '../hooks/useKeyboardShortcuts';
7
- import { LayoutProvider, useLayout, useKeyboardShortcuts } from '../contexts';
8
- import type { ReactNode } from 'react';
9
-
10
- /**
11
- * MinimalLayout provides only Navigation (app shell) without MainSidebar.
12
- * Used for pages like ProjectsPage where sidebar navigation doesn't make sense.
13
- */
14
- function MinimalLayoutContent({ navigationRightSlot }: { navigationRightSlot?: ReactNode }) {
15
- const { toggleMobileSidebar } = useLayout();
16
- const { toggleHelp } = useKeyboardShortcuts();
17
-
18
- // Register global keyboard shortcuts
19
- useGlobalShortcuts();
20
-
21
- return (
22
- <div className="min-h-screen flex flex-col bg-background">
23
- <Navigation
24
- onToggleSidebar={toggleMobileSidebar}
25
- onShowShortcuts={toggleHelp}
26
- rightSlot={navigationRightSlot}
27
- />
28
- <main className="flex-1 w-full min-h-[calc(100vh-3.5rem)]">
29
- <ErrorBoundary onReset={() => window.location.reload()}>
30
- <PageTransition>
31
- <Outlet />
32
- </PageTransition>
33
- </ErrorBoundary>
34
- </main>
35
- <BackToTop />
36
- </div>
37
- );
38
- }
39
-
40
- /**
41
- * MinimalLayout wrapper that provides LayoutProvider.
42
- * Note: mobileSidebarOpen state exists but has no effect since there's no sidebar.
43
- * This maintains API consistency with Layout component.
44
- */
45
- export function MinimalLayout({ navigationRightSlot }: { navigationRightSlot?: ReactNode } = {}) {
46
- return (
47
- <LayoutProvider>
48
- <MinimalLayoutContent navigationRightSlot={navigationRightSlot} />
49
- </LayoutProvider>
50
- );
51
- }
@@ -1,254 +0,0 @@
1
- import type { ReactNode } from 'react';
2
- import { Link, useLocation, useParams } from 'react-router-dom';
3
- import { BookOpen, ChevronRight, Menu } from 'lucide-react';
4
- import { useTranslation } from 'react-i18next';
5
- import { Button } from '@leanspec/ui-components';
6
- import { QuickSearch } from './QuickSearch';
7
- import { ThemeToggle } from './ThemeToggle';
8
- import { LanguageSwitcher } from './LanguageSwitcher';
9
- import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from './Tooltip';
10
-
11
- interface BreadcrumbItem {
12
- label: string;
13
- to?: string;
14
- }
15
-
16
- interface NavigationProps {
17
- onToggleSidebar?: () => void;
18
- onShowShortcuts?: () => void;
19
- rightSlot?: ReactNode;
20
- onHeaderDoubleClick?: () => void;
21
- }
22
-
23
- function stripProjectPrefix(pathname: string): string {
24
- const match = pathname.match(/^\/projects\/[^/]+(\/.*)?$/);
25
- return match ? match[1] || '/' : pathname;
26
- }
27
-
28
- function parsePathname(pathname: string): { page: string; specId?: string; query?: string } {
29
- const path = stripProjectPrefix(pathname);
30
-
31
- if (path === '/') return { page: 'home' };
32
- if (path === '/stats') return { page: 'stats' };
33
- if (path === '/dependencies') return { page: 'dependencies' };
34
- if (path === '/context') return { page: 'context' };
35
- if (path === '/settings') return { page: 'settings' };
36
- if (path === '/specs' || path.startsWith('/specs?')) {
37
- return { page: 'specs', query: path.split('?')[1] };
38
- }
39
- if (path.startsWith('/specs/')) {
40
- return { page: 'spec-detail', specId: path.split('/')[2] };
41
- }
42
-
43
- return { page: 'unknown' };
44
- }
45
-
46
- function Breadcrumb({ basePath }: { basePath: string }) {
47
- const location = useLocation();
48
- const { t } = useTranslation('common');
49
-
50
- const homeLabel = t('navigation.home');
51
- const specsLabel = t('navigation.specs');
52
- const statsLabel = t('navigation.stats');
53
- const depsLabel = t('navigation.dependencies');
54
- const contextLabel = t('navigation.context');
55
- const settingsLabel = t('navigation.settings');
56
-
57
- const parsed = parsePathname(location.pathname);
58
-
59
- let items: BreadcrumbItem[] = [];
60
-
61
- switch (parsed.page) {
62
- case 'home':
63
- items = [{ label: homeLabel }];
64
- break;
65
-
66
- case 'stats':
67
- items = [{ label: homeLabel, to: basePath }, { label: statsLabel }];
68
- break;
69
-
70
- case 'dependencies':
71
- items = [{ label: homeLabel, to: basePath }, { label: depsLabel }];
72
- break;
73
-
74
- case 'context':
75
- items = [{ label: homeLabel, to: basePath }, { label: contextLabel }];
76
- break;
77
-
78
- case 'settings':
79
- items = [{ label: homeLabel, to: basePath }, { label: settingsLabel }];
80
- break;
81
-
82
- case 'specs': {
83
- items = [{ label: homeLabel, to: basePath }, { label: specsLabel }];
84
- break;
85
- }
86
-
87
- case 'spec-detail':
88
- items = [
89
- { label: homeLabel, to: basePath },
90
- { label: specsLabel, to: `${basePath}/specs` },
91
- { label: parsed.specId || '' },
92
- ];
93
- break;
94
-
95
- default:
96
- items = [{ label: homeLabel, to: basePath }];
97
- }
98
-
99
- return (
100
- <nav className="hidden md:flex items-center gap-1 text-sm text-muted-foreground">
101
- {items.map((item, index) => (
102
- <div key={index} className="flex items-center gap-1">
103
- {index > 0 && <ChevronRight className="h-4 w-4" />}
104
- {item.to ? (
105
- <Link to={item.to} className="hover:text-foreground transition-colors">
106
- {item.label}
107
- </Link>
108
- ) : (
109
- <span className="text-foreground">{item.label}</span>
110
- )}
111
- </div>
112
- ))}
113
- </nav>
114
- );
115
- }
116
-
117
- export function Navigation({ onToggleSidebar, rightSlot, onHeaderDoubleClick }: NavigationProps) {
118
- const { t } = useTranslation('common');
119
- const { projectId } = useParams<{ projectId: string }>();
120
- const basePath = projectId ? `/projects/${projectId}` : '/projects/default';
121
-
122
- const toggleSidebar = () => {
123
- onToggleSidebar?.();
124
- };
125
-
126
- return (
127
- <header
128
- className="sticky top-0 z-50 w-full h-14 border-b border-border bg-background"
129
- data-tauri-drag-region="true"
130
- onDoubleClick={onHeaderDoubleClick}
131
- >
132
- <div className="flex items-center justify-between h-full lg:px-1 px-4">
133
- {/* Left: Mobile Menu + Logo + Breadcrumb */}
134
- <div className="flex items-center gap-2 sm:gap-4 min-w-0 flex-1">
135
- {/* Mobile hamburger menu */}
136
- <Button
137
- variant="ghost"
138
- size="icon"
139
- onClick={toggleSidebar}
140
- className="lg:hidden h-9 w-9 shrink-0"
141
- data-tauri-drag-region="false"
142
- >
143
- <Menu className="h-5 w-5" />
144
- <span className="sr-only">{t('navigation.toggleMenu')}</span>
145
- </Button>
146
-
147
- <Link
148
- to={basePath}
149
- className="flex items-center space-x-2 shrink-0"
150
- data-tauri-drag-region="false"
151
- >
152
- <img
153
- src="/logo-with-bg.svg"
154
- alt="LeanSpec"
155
- className="h-7 w-7 sm:h-8 sm:w-8 dark:hidden"
156
- />
157
- <img
158
- src="/logo-dark-bg.svg"
159
- alt="LeanSpec"
160
- className="h-7 w-7 sm:h-8 sm:w-8 hidden dark:block"
161
- />
162
- <span className="font-bold text-lg sm:text-xl hidden sm:inline">LeanSpec</span>
163
- </Link>
164
- <div className="hidden md:block min-w-0" data-tauri-drag-region="false">
165
- <Breadcrumb basePath={basePath} />
166
- </div>
167
- </div>
168
-
169
- {/* Right: Search + Language + Theme + Docs + GitHub */}
170
- <div className="flex items-center gap-1 sm:gap-2 shrink-0" data-tauri-drag-region="false">
171
- <div data-tauri-drag-region="false">
172
- <QuickSearch />
173
- </div>
174
- <TooltipProvider>
175
- <LanguageSwitcher />
176
- <Tooltip>
177
- <TooltipTrigger asChild>
178
- <div>
179
- <ThemeToggle />
180
- </div>
181
- </TooltipTrigger>
182
- <TooltipContent>
183
- <p>{t('theme.toggleTheme')}</p>
184
- </TooltipContent>
185
- </Tooltip>
186
-
187
- <Tooltip>
188
- <TooltipTrigger asChild>
189
- <Button
190
- variant="ghost"
191
- size="icon"
192
- asChild
193
- className="h-9 w-9 sm:h-10 sm:w-10"
194
- data-tauri-drag-region="false"
195
- >
196
- <a
197
- href="https://www.lean-spec.dev"
198
- target="_blank"
199
- rel="noopener noreferrer"
200
- aria-label={t('navigation.docsTooltip')}
201
- >
202
- <BookOpen className="h-5 w-5" />
203
- </a>
204
- </Button>
205
- </TooltipTrigger>
206
- <TooltipContent>
207
- <p>{t('navigation.docsTooltip')}</p>
208
- </TooltipContent>
209
- </Tooltip>
210
-
211
- <Tooltip>
212
- <TooltipTrigger asChild>
213
- <Button
214
- variant="ghost"
215
- size="icon"
216
- asChild
217
- className="h-9 w-9 sm:h-10 sm:w-10"
218
- data-tauri-drag-region="false"
219
- >
220
- <a
221
- href="https://github.com/codervisor/lean-spec"
222
- target="_blank"
223
- rel="noopener noreferrer"
224
- aria-label={t('navigation.githubTooltip')}
225
- >
226
- <img
227
- src="/github-mark-white.svg"
228
- alt="GitHub"
229
- className="hidden dark:block w-5 h-5"
230
- />
231
- <img
232
- src="/github-mark.svg"
233
- alt="GitHub"
234
- className="dark:hidden w-5 h-5"
235
- />
236
- </a>
237
- </Button>
238
- </TooltipTrigger>
239
- <TooltipContent>
240
- <p>{t('navigation.githubTooltip')}</p>
241
- </TooltipContent>
242
- </Tooltip>
243
-
244
- {rightSlot && (
245
- <div className="ml-2 flex items-center" data-tauri-drag-region="false">
246
- {rightSlot}
247
- </div>
248
- )}
249
- </TooltipProvider>
250
- </div>
251
- </div>
252
- </header>
253
- );
254
- }
@@ -1,59 +0,0 @@
1
- import { AlertCircle, ArrowUp, Minus, ArrowDown } from 'lucide-react';
2
- import { Badge } from '@leanspec/ui-components';
3
- import { cn } from '../lib/utils';
4
- import { useTranslation } from 'react-i18next';
5
-
6
- interface PriorityBadgeProps {
7
- priority: string;
8
- className?: string;
9
- iconOnly?: boolean;
10
- }
11
-
12
- const priorityConfig: Record<string, { icon: typeof AlertCircle; labelKey: `priority.${string}`; className: string }> = {
13
- 'critical': {
14
- icon: AlertCircle,
15
- labelKey: 'priority.critical',
16
- className: 'bg-red-100 text-red-800 dark:bg-red-900/30 dark:text-red-400'
17
- },
18
- 'high': {
19
- icon: ArrowUp,
20
- labelKey: 'priority.high',
21
- className: 'bg-orange-100 text-orange-800 dark:bg-orange-900/30 dark:text-orange-400'
22
- },
23
- 'medium': {
24
- icon: Minus,
25
- labelKey: 'priority.medium',
26
- className: 'bg-blue-100 text-blue-800 dark:bg-blue-900/30 dark:text-blue-400'
27
- },
28
- 'low': {
29
- icon: ArrowDown,
30
- labelKey: 'priority.low',
31
- className: 'bg-gray-100 text-gray-800 dark:bg-gray-800 dark:text-gray-400'
32
- }
33
- };
34
-
35
- export function getPriorityLabel(priority: string, t: (key: string) => string) {
36
- const config = priorityConfig[priority] || priorityConfig['medium'];
37
- return t(config.labelKey);
38
- }
39
-
40
- export function PriorityBadge({ priority, className, iconOnly = false }: PriorityBadgeProps) {
41
- const config = priorityConfig[priority] || priorityConfig['medium'];
42
- const Icon = config.icon;
43
- const { t } = useTranslation('common');
44
-
45
- return (
46
- <Badge
47
- variant="outline"
48
- className={cn(
49
- 'flex items-center w-fit h-5 px-2 py-0.5 text-xs font-medium border-transparent',
50
- !iconOnly && 'gap-1.5',
51
- config.className,
52
- className,
53
- )}
54
- >
55
- <Icon className="h-3.5 w-3.5" />
56
- {!iconOnly && t(config.labelKey)}
57
- </Badge>
58
- );
59
- }
@@ -1,222 +0,0 @@
1
- /**
2
- * Project Switcher Component
3
- * Dropdown/expandable project selector for the sidebar
4
- */
5
-
6
- import { useState } from 'react';
7
- import { ChevronsUpDown, Plus, Star, Settings, Loader2, Check } from 'lucide-react';
8
- import { cn } from '@leanspec/ui-components';
9
- import { Button } from '@leanspec/ui-components';
10
- import {
11
- Command,
12
- CommandEmpty,
13
- CommandGroup,
14
- CommandInput,
15
- CommandItem,
16
- CommandList,
17
- CommandSeparator,
18
- } from '@leanspec/ui-components';
19
- import {
20
- Popover,
21
- PopoverContent,
22
- PopoverTrigger,
23
- } from '@leanspec/ui-components';
24
- import { Skeleton } from '@leanspec/ui-components';
25
- import { useProject } from '../contexts';
26
- import { CreateProjectDialog } from './projects/CreateProjectDialog';
27
- import { ProjectAvatar } from './shared/ProjectAvatar';
28
- import { useTranslation } from 'react-i18next';
29
- import { useLocation, useNavigate } from 'react-router-dom';
30
-
31
- interface ProjectSwitcherProps {
32
- collapsed?: boolean;
33
- onAddProject?: () => void; // Kept for compatibility, but we'll use internal dialog
34
- }
35
-
36
- export function ProjectSwitcher({ collapsed }: ProjectSwitcherProps) {
37
- const {
38
- currentProject,
39
- projects,
40
- loading: isLoading,
41
- switchProject,
42
- } = useProject();
43
- const { t } = useTranslation('common');
44
- const location = useLocation();
45
- const navigate = useNavigate();
46
-
47
- const [open, setOpen] = useState(false);
48
- const [showNewProjectDialog, setShowNewProjectDialog] = useState(false);
49
- const [isSwitching, setIsSwitching] = useState(false);
50
-
51
- // Show skeleton during initial load
52
- if (isLoading) {
53
- return (
54
- <Skeleton className={cn(
55
- "w-full",
56
- collapsed ? "h-9 w-9" : "h-10"
57
- )} />
58
- );
59
- }
60
-
61
- const handleProjectSelect = async (projectId: string) => {
62
- if (projectId === currentProject?.id) {
63
- setOpen(false);
64
- return;
65
- }
66
-
67
- setIsSwitching(true);
68
- setOpen(false);
69
-
70
- const pathname = location.pathname;
71
- const projectPathMatch = pathname.match(/^\/projects\/[^/]+(\/.*)?$/);
72
- let subPath = projectPathMatch?.[1] || '';
73
-
74
- if (!subPath || subPath === '/') {
75
- subPath = '/specs';
76
- }
77
-
78
- if (subPath.match(/^\/specs\/[^/]+$/)) {
79
- subPath = '/specs';
80
- }
81
-
82
- try {
83
- await switchProject(projectId);
84
- navigate(`/projects/${projectId}${subPath}${location.search}`);
85
- } catch (err) {
86
- console.error('Failed to switch project', err);
87
- } finally {
88
- setIsSwitching(false);
89
- }
90
- };
91
-
92
- const sortedProjects = [...(projects || [])].sort((a, b) => {
93
- if (a.favorite === b.favorite) return 0;
94
- return a.favorite ? -1 : 1;
95
- });
96
-
97
- return (
98
- <>
99
- <CreateProjectDialog
100
- open={showNewProjectDialog}
101
- onOpenChange={setShowNewProjectDialog}
102
- />
103
- <Popover open={open} onOpenChange={setOpen}>
104
- <PopoverTrigger asChild>
105
- <Button
106
- variant="outline"
107
- role="combobox"
108
- aria-expanded={open}
109
- disabled={isSwitching}
110
- className={cn(
111
- "w-full justify-between transition-opacity",
112
- collapsed ? "h-9 w-9 p-0 justify-center" : "px-3",
113
- isSwitching && "opacity-70"
114
- )}
115
- >
116
- {collapsed ? (
117
- isSwitching ? (
118
- <Loader2 className="h-4 w-4 animate-spin" />
119
- ) : (
120
- <ProjectAvatar
121
- name={currentProject?.name || ''}
122
- color={currentProject?.color}
123
- size="sm"
124
- />
125
- )
126
- ) : (
127
- <>
128
- <div className="flex items-center gap-2 truncate">
129
- {isSwitching ? (
130
- <Loader2 className="h-4 w-4 shrink-0 animate-spin" />
131
- ) : (
132
- <ProjectAvatar
133
- name={currentProject?.name || ''}
134
- color={currentProject?.color}
135
- size="sm"
136
- className="shrink-0"
137
- />
138
- )}
139
- <span className="truncate">
140
- {isSwitching ? t('projectSwitcher.switching') : (currentProject?.name || t('projectSwitcher.placeholder'))}
141
- </span>
142
- </div>
143
- <ChevronsUpDown className="ml-2 h-4 w-4 shrink-0 opacity-50" />
144
- </>
145
- )}
146
- </Button>
147
- </PopoverTrigger>
148
- <PopoverContent className="w-[240px] p-0" align="start">
149
- <Command>
150
- <CommandInput placeholder={t('projectSwitcher.searchPlaceholder')} />
151
- <CommandList>
152
- <CommandEmpty>{t('projectSwitcher.noProject')}</CommandEmpty>
153
- <CommandGroup heading={t('projects.projects')}>
154
- {sortedProjects.map((project) => {
155
- const isActive = currentProject?.id === project.id;
156
- return (
157
- <CommandItem
158
- key={project.id}
159
- onSelect={() => handleProjectSelect(project.id)}
160
- className={cn(
161
- "text-sm",
162
- isActive && "bg-accent"
163
- )}
164
- >
165
- <div className="flex items-center gap-2 w-full">
166
- <ProjectAvatar
167
- name={project.name || ''}
168
- color={project.color}
169
- size="sm"
170
- className="shrink-0"
171
- />
172
- <span className="truncate flex-1">{project.name}</span>
173
- {project.favorite && (
174
- <Star className="h-3 w-3 shrink-0 fill-yellow-600 text-yellow-600 dark:fill-yellow-500 dark:text-yellow-500" />
175
- )}
176
- <div
177
- className={cn(
178
- 'mr-2 flex h-4 w-4 items-center justify-center',
179
- currentProject?.id === project.id ? 'opacity-100' : 'opacity-0'
180
- )}
181
- >
182
- <Check className="h-4 w-4" />
183
- </div>
184
- </div>
185
- </CommandItem>
186
- );
187
- })}
188
- </CommandGroup>
189
- <CommandSeparator />
190
- <CommandGroup>
191
- <CommandItem
192
- className="cursor-pointer"
193
- onSelect={() => {
194
- setOpen(false);
195
- setShowNewProjectDialog(true);
196
- }}
197
- >
198
- <div className="flex items-center gap-2">
199
- <Plus className="h-4 w-4" />
200
- <span>{t('projects.createProject')}</span>
201
- </div>
202
- </CommandItem>
203
- <CommandItem
204
- className="cursor-pointer"
205
- onSelect={() => {
206
- setOpen(false);
207
- navigate('/projects');
208
- }}
209
- >
210
- <div className="flex items-center gap-2">
211
- <Settings className="h-4 w-4" />
212
- <span>{t('projects.manageProjects')}</span>
213
- </div>
214
- </CommandItem>
215
- </CommandGroup>
216
- </CommandList>
217
- </Command>
218
- </PopoverContent>
219
- </Popover>
220
- </>
221
- );
222
- }