@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.
- package/bin/leanspec-ui.js +191 -0
- package/dist/assets/_baseUniq-CRqreL7N.js +1 -0
- package/dist/assets/arc-DMhx9AJT.js +1 -0
- package/dist/assets/architectureDiagram-VXUJARFQ-DM0L0YzO.js +36 -0
- package/dist/assets/blockDiagram-VD42YOAC-DHQXDHsD.js +122 -0
- package/dist/assets/c4Diagram-YG6GDRKO-0L7o2gpH.js +10 -0
- package/dist/assets/channel-2tOl0nAZ.js +1 -0
- package/dist/assets/chunk-4BX2VUAB-CwFT-Uaj.js +1 -0
- package/dist/assets/chunk-55IACEB6-CjvuUHHG.js +1 -0
- package/dist/assets/chunk-B4BG7PRW-BRJBysMK.js +165 -0
- package/dist/assets/chunk-DI55MBZ5-BnNEeoaA.js +220 -0
- package/dist/assets/chunk-FMBD7UC4-BK2l30pm.js +15 -0
- package/dist/assets/chunk-QN33PNHL-BN_cZkCU.js +1 -0
- package/dist/assets/chunk-QZHKN3VN-Brc3Yrub.js +1 -0
- package/dist/assets/chunk-TZMSLE5B-D2zzpLfO.js +1 -0
- package/dist/assets/classDiagram-2ON5EDUG-BB9CSNmS.js +1 -0
- package/dist/assets/classDiagram-v2-WZHVMYZB-BB9CSNmS.js +1 -0
- package/dist/assets/clone-BjxVFtyI.js +1 -0
- package/dist/assets/core-DV6XEvTN.js +1 -0
- package/dist/assets/cose-bilkent-S5V4N54A-CLJgM3XR.js +1 -0
- package/dist/assets/cytoscape.esm-5J0xJHOV.js +321 -0
- package/dist/assets/dagre-6UL2VRFP-_IFvBJKJ.js +4 -0
- package/dist/assets/diagram-PSM6KHXK--83HIYSQ.js +24 -0
- package/dist/assets/diagram-QEK2KX5R-6jAWnCnZ.js +43 -0
- package/dist/assets/diagram-S2PKOQOG-D5pwHvjZ.js +24 -0
- package/dist/assets/erDiagram-Q2GNP2WA-B4FV3mTd.js +60 -0
- package/dist/assets/flowDiagram-NV44I4VS-mtD2kF4M.js +162 -0
- package/dist/assets/ganttDiagram-JELNMOA3-BKALgqTK.js +267 -0
- package/dist/assets/gitGraphDiagram-NY62KEGX-Bd7r0pAf.js +65 -0
- package/dist/assets/graph-B2rEI7cK.js +1 -0
- package/dist/assets/index-Bekv_o1t.css +1 -0
- package/dist/assets/index-DSRxU-E5.js +389 -0
- package/dist/assets/infoDiagram-WHAUD3N6--nJOBKqh.js +2 -0
- package/dist/assets/journeyDiagram-XKPGCS4Q-BzGutKN3.js +139 -0
- package/dist/assets/kanban-definition-3W4ZIXB7-DyQO17vq.js +89 -0
- package/dist/assets/katex-XbL3y5x-.js +261 -0
- package/dist/assets/layout-iCSHU015.js +1 -0
- package/dist/assets/min-BK_AIJdo.js +1 -0
- package/dist/assets/mindmap-definition-VGOIOE7T-BZMj_6zo.js +68 -0
- package/dist/assets/pieDiagram-ADFJNKIX-CkAGsq9p.js +30 -0
- package/dist/assets/quadrantDiagram-AYHSOK5B-CWa93px1.js +7 -0
- package/dist/assets/requirementDiagram-UZGBJVZJ-CufFVR8c.js +64 -0
- package/dist/assets/sankeyDiagram-TZEHDZUN-BEPgVgU4.js +10 -0
- package/dist/assets/sequenceDiagram-WL72ISMW-BkdBWhel.js +145 -0
- package/dist/assets/stateDiagram-FKZM4ZOC-D5T73yx0.js +1 -0
- package/dist/assets/stateDiagram-v2-4FDKWEC3-9hJWG2n6.js +1 -0
- package/dist/assets/timeline-definition-IT6M3QCI-CX7kTdU2.js +61 -0
- package/dist/assets/treemap-KMMF4GRG-ftWCQ9lJ.js +128 -0
- package/dist/assets/xychartDiagram-PRI3JC2R-Ngrels4n.js +7 -0
- package/{index.html → dist/index.html} +2 -1
- package/package.json +13 -3
- package/eslint.config.js +0 -23
- package/postcss.config.js +0 -6
- package/src/App.css +0 -42
- package/src/App.tsx +0 -17
- package/src/assets/react.svg +0 -1
- package/src/components/LanguageSwitcher.tsx +0 -67
- package/src/components/Layout.tsx +0 -88
- package/src/components/MainSidebar.tsx +0 -163
- package/src/components/MermaidDiagram.tsx +0 -85
- package/src/components/MinimalLayout.tsx +0 -51
- package/src/components/Navigation.tsx +0 -254
- package/src/components/PriorityBadge.tsx +0 -59
- package/src/components/ProjectSwitcher.tsx +0 -222
- package/src/components/QuickSearch.tsx +0 -225
- package/src/components/RootRedirect.tsx +0 -40
- package/src/components/SpecDetailLayout.context.ts +0 -10
- package/src/components/SpecDetailLayout.tsx +0 -14
- package/src/components/SpecsNavSidebar.tsx +0 -615
- package/src/components/StatusBadge.tsx +0 -59
- package/src/components/ThemeToggle.tsx +0 -25
- package/src/components/Tooltip.tsx +0 -29
- package/src/components/context/ContextClient.tsx +0 -471
- package/src/components/context/ContextFileDetail.tsx +0 -163
- package/src/components/dashboard/ActivityItem.tsx +0 -36
- package/src/components/dashboard/DashboardClient.tsx +0 -218
- package/src/components/dashboard/SpecListItem.tsx +0 -58
- package/src/components/dashboard/StatCard.tsx +0 -52
- package/src/components/dependencies/SpecNode.tsx +0 -128
- package/src/components/dependencies/SpecSidebar.tsx +0 -256
- package/src/components/dependencies/constants.ts +0 -25
- package/src/components/dependencies/types.ts +0 -38
- package/src/components/dependencies/utils.ts +0 -261
- package/src/components/metadata-editors/PriorityEditor.tsx +0 -89
- package/src/components/metadata-editors/StatusEditor.tsx +0 -85
- package/src/components/metadata-editors/TagsEditor.tsx +0 -207
- package/src/components/projects/CreateProjectDialog.tsx +0 -162
- package/src/components/projects/DirectoryPicker.tsx +0 -182
- package/src/components/shared/BackToTop.tsx +0 -39
- package/src/components/shared/ColorPicker.tsx +0 -68
- package/src/components/shared/EmptyState.tsx +0 -35
- package/src/components/shared/ErrorBoundary.tsx +0 -79
- package/src/components/shared/PageHeader.tsx +0 -23
- package/src/components/shared/PageTransition.tsx +0 -40
- package/src/components/shared/ProjectAvatar.tsx +0 -107
- package/src/components/shared/Skeletons.tsx +0 -184
- package/src/components/spec-detail/EditableMetadata.tsx +0 -129
- package/src/components/spec-detail/MarkdownRenderer.tsx +0 -47
- package/src/components/spec-detail/TableOfContents.tsx +0 -150
- package/src/components/specs/BoardView.tsx +0 -204
- package/src/components/specs/ListView.tsx +0 -62
- package/src/components/specs/SpecsFilters.tsx +0 -190
- package/src/contexts/KeyboardShortcutsContext.tsx +0 -95
- package/src/contexts/LayoutContext.tsx +0 -45
- package/src/contexts/ProjectContext.tsx +0 -163
- package/src/contexts/ThemeContext.tsx +0 -90
- package/src/contexts/index.ts +0 -7
- package/src/hooks/useKeyboardShortcuts.ts +0 -87
- package/src/index.css +0 -624
- package/src/lib/api.ts +0 -72
- package/src/lib/backend-adapter.ts +0 -382
- package/src/lib/date-utils.ts +0 -122
- package/src/lib/i18n.test.ts +0 -57
- package/src/lib/i18n.ts +0 -51
- package/src/lib/markdown-utils.ts +0 -38
- package/src/lib/sub-spec-utils.ts +0 -166
- package/src/lib/utils.ts +0 -6
- package/src/locales/en/common.json +0 -660
- package/src/locales/en/errors.json +0 -20
- package/src/locales/en/help.json +0 -8
- package/src/locales/zh-CN/common.json +0 -660
- package/src/locales/zh-CN/errors.json +0 -20
- package/src/locales/zh-CN/help.json +0 -8
- package/src/main.tsx +0 -12
- package/src/pages/ContextPage.tsx +0 -111
- package/src/pages/DashboardPage.tsx +0 -97
- package/src/pages/DependenciesPage.tsx +0 -881
- package/src/pages/ProjectsPage.tsx +0 -432
- package/src/pages/SpecDetailPage.tsx +0 -592
- package/src/pages/SpecsPage.tsx +0 -319
- package/src/pages/StatsPage.tsx +0 -307
- package/src/router/projectRoutes.tsx +0 -36
- package/src/router.tsx +0 -33
- package/src/test/setup.ts +0 -39
- package/src/types/api.ts +0 -185
- package/tailwind.config.ts +0 -57
- package/tsconfig.app.json +0 -29
- package/tsconfig.json +0 -7
- package/tsconfig.node.json +0 -26
- package/tsconfig.tsbuildinfo +0 -1
- package/vite.config.ts +0 -27
- package/vitest.config.ts +0 -18
- /package/{public → dist}/favicon.ico +0 -0
- /package/{public → dist}/github-mark-white.svg +0 -0
- /package/{public → dist}/github-mark.svg +0 -0
- /package/{public → dist}/logo-dark-bg.svg +0 -0
- /package/{public → dist}/logo-with-bg.svg +0 -0
- /package/{public → dist}/logo.svg +0 -0
- /package/{public → dist}/vite.svg +0 -0
|
@@ -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
|
-
}
|