@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,95 +0,0 @@
|
|
|
1
|
-
import { createContext, useContext, useState, useMemo } from 'react';
|
|
2
|
-
import type { ReactNode } from 'react';
|
|
3
|
-
import { useTranslation } from 'react-i18next';
|
|
4
|
-
import { Button } from '@leanspec/ui-components';
|
|
5
|
-
|
|
6
|
-
/**
|
|
7
|
-
* Context value for keyboard shortcuts help dialog state.
|
|
8
|
-
*/
|
|
9
|
-
interface KeyboardShortcutsContextValue {
|
|
10
|
-
/** Whether the keyboard shortcuts help dialog is visible */
|
|
11
|
-
showHelp: boolean;
|
|
12
|
-
/** Toggle the keyboard shortcuts help dialog */
|
|
13
|
-
toggleHelp: () => void;
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
const KeyboardShortcutsContext = createContext<KeyboardShortcutsContextValue | undefined>(
|
|
17
|
-
undefined
|
|
18
|
-
);
|
|
19
|
-
|
|
20
|
-
/**
|
|
21
|
-
* Keyboard shortcuts help dialog component.
|
|
22
|
-
* Shows available keyboard shortcuts in a modal dialog.
|
|
23
|
-
*/
|
|
24
|
-
function KeyboardShortcutsHelp({ onClose }: { onClose: () => void }) {
|
|
25
|
-
const { t } = useTranslation('common');
|
|
26
|
-
const shortcuts = [
|
|
27
|
-
{ key: 'h', description: t('keyboardShortcuts.items.dashboard') },
|
|
28
|
-
{ key: 'g', description: t('keyboardShortcuts.items.specs') },
|
|
29
|
-
{ key: 's', description: t('keyboardShortcuts.items.stats') },
|
|
30
|
-
{ key: 'd', description: t('keyboardShortcuts.items.dependencies') },
|
|
31
|
-
{ key: ',', description: t('keyboardShortcuts.items.settings') },
|
|
32
|
-
{ key: '/', description: t('keyboardShortcuts.items.search') },
|
|
33
|
-
{ key: '⌘ + K', description: t('keyboardShortcuts.items.quickSearch') },
|
|
34
|
-
];
|
|
35
|
-
|
|
36
|
-
return (
|
|
37
|
-
<div
|
|
38
|
-
className="fixed inset-0 bg-black/50 flex items-center justify-center z-50"
|
|
39
|
-
onClick={onClose}
|
|
40
|
-
>
|
|
41
|
-
<div
|
|
42
|
-
className="bg-background border rounded-lg shadow-lg p-6 max-w-md w-full mx-4"
|
|
43
|
-
onClick={(e) => e.stopPropagation()}
|
|
44
|
-
>
|
|
45
|
-
<h3 className="text-lg font-medium mb-4">{t('keyboardShortcuts.title')}</h3>
|
|
46
|
-
<div className="space-y-2">
|
|
47
|
-
{shortcuts.map((s) => (
|
|
48
|
-
<div key={s.key} className="flex items-center justify-between">
|
|
49
|
-
<span className="text-sm text-muted-foreground">{s.description}</span>
|
|
50
|
-
<kbd className="px-2 py-1 text-xs bg-secondary rounded border">{s.key}</kbd>
|
|
51
|
-
</div>
|
|
52
|
-
))}
|
|
53
|
-
</div>
|
|
54
|
-
<Button onClick={onClose} variant="secondary" size="sm" className="mt-4 w-full">
|
|
55
|
-
{t('actions.close')}
|
|
56
|
-
</Button>
|
|
57
|
-
</div>
|
|
58
|
-
</div>
|
|
59
|
-
);
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
/**
|
|
63
|
-
* Provider for keyboard shortcuts help dialog state.
|
|
64
|
-
* Wraps the app to provide global keyboard shortcuts help functionality.
|
|
65
|
-
*/
|
|
66
|
-
export function KeyboardShortcutsProvider({ children }: { children: ReactNode }) {
|
|
67
|
-
const [showHelp, setShowHelp] = useState(false);
|
|
68
|
-
|
|
69
|
-
const value = useMemo(
|
|
70
|
-
() => ({
|
|
71
|
-
showHelp,
|
|
72
|
-
toggleHelp: () => setShowHelp((prev) => !prev),
|
|
73
|
-
}),
|
|
74
|
-
[showHelp]
|
|
75
|
-
);
|
|
76
|
-
|
|
77
|
-
return (
|
|
78
|
-
<KeyboardShortcutsContext.Provider value={value}>
|
|
79
|
-
{children}
|
|
80
|
-
{showHelp && <KeyboardShortcutsHelp onClose={() => setShowHelp(false)} />}
|
|
81
|
-
</KeyboardShortcutsContext.Provider>
|
|
82
|
-
);
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
/**
|
|
86
|
-
* Hook to access keyboard shortcuts context.
|
|
87
|
-
* Must be used within a KeyboardShortcutsProvider.
|
|
88
|
-
*/
|
|
89
|
-
export function useKeyboardShortcuts() {
|
|
90
|
-
const context = useContext(KeyboardShortcutsContext);
|
|
91
|
-
if (context === undefined) {
|
|
92
|
-
throw new Error('useKeyboardShortcuts must be used within a KeyboardShortcutsProvider');
|
|
93
|
-
}
|
|
94
|
-
return context;
|
|
95
|
-
}
|
|
@@ -1,45 +0,0 @@
|
|
|
1
|
-
import { createContext, useContext, useState, useMemo } from 'react';
|
|
2
|
-
import type { ReactNode } from 'react';
|
|
3
|
-
|
|
4
|
-
/**
|
|
5
|
-
* Layout context value for managing layout-specific UI state.
|
|
6
|
-
* Currently manages mobile sidebar visibility.
|
|
7
|
-
*/
|
|
8
|
-
interface LayoutContextValue {
|
|
9
|
-
/** Whether the mobile main sidebar is open */
|
|
10
|
-
mobileSidebarOpen: boolean;
|
|
11
|
-
/** Toggle the mobile main sidebar */
|
|
12
|
-
toggleMobileSidebar: () => void;
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
const LayoutContext = createContext<LayoutContextValue | undefined>(undefined);
|
|
16
|
-
|
|
17
|
-
/**
|
|
18
|
-
* Provider for layout-specific UI state.
|
|
19
|
-
* Wraps the Layout component to provide mobile sidebar state management.
|
|
20
|
-
*/
|
|
21
|
-
export function LayoutProvider({ children }: { children: ReactNode }) {
|
|
22
|
-
const [mobileSidebarOpen, setMobileSidebarOpen] = useState(false);
|
|
23
|
-
|
|
24
|
-
const value = useMemo(
|
|
25
|
-
() => ({
|
|
26
|
-
mobileSidebarOpen,
|
|
27
|
-
toggleMobileSidebar: () => setMobileSidebarOpen((prev) => !prev),
|
|
28
|
-
}),
|
|
29
|
-
[mobileSidebarOpen]
|
|
30
|
-
);
|
|
31
|
-
|
|
32
|
-
return <LayoutContext.Provider value={value}>{children}</LayoutContext.Provider>;
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
/**
|
|
36
|
-
* Hook to access layout context.
|
|
37
|
-
* Must be used within a LayoutProvider.
|
|
38
|
-
*/
|
|
39
|
-
export function useLayout() {
|
|
40
|
-
const context = useContext(LayoutContext);
|
|
41
|
-
if (context === undefined) {
|
|
42
|
-
throw new Error('useLayout must be used within a LayoutProvider');
|
|
43
|
-
}
|
|
44
|
-
return context;
|
|
45
|
-
}
|
|
@@ -1,163 +0,0 @@
|
|
|
1
|
-
import { createContext, useContext, useState, useEffect, useCallback, type ReactNode } from 'react';
|
|
2
|
-
import { api } from '../lib/api';
|
|
3
|
-
import i18n from '../lib/i18n';
|
|
4
|
-
import type { Project, ProjectValidationResponse, ProjectsResponse } from '../types/api';
|
|
5
|
-
|
|
6
|
-
interface ProjectContextValue {
|
|
7
|
-
currentProject: Project | null;
|
|
8
|
-
projects: Project[];
|
|
9
|
-
availableProjects: Project[];
|
|
10
|
-
favoriteProjects: Project[];
|
|
11
|
-
loading: boolean;
|
|
12
|
-
error: string | null;
|
|
13
|
-
switchProject: (projectId: string) => Promise<void>;
|
|
14
|
-
addProject: (
|
|
15
|
-
path: string,
|
|
16
|
-
options?: { favorite?: boolean; color?: string; name?: string; description?: string | null }
|
|
17
|
-
) => Promise<Project>;
|
|
18
|
-
updateProject: (
|
|
19
|
-
projectId: string,
|
|
20
|
-
updates: Partial<Pick<Project, 'name' | 'color' | 'favorite' | 'description'>>
|
|
21
|
-
) => Promise<Project | undefined>;
|
|
22
|
-
removeProject: (projectId: string) => Promise<void>;
|
|
23
|
-
toggleFavorite: (projectId: string) => Promise<void>;
|
|
24
|
-
refreshProjects: () => Promise<void>;
|
|
25
|
-
validateProject: (projectId: string) => Promise<ProjectValidationResponse>;
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
const ProjectContext = createContext<ProjectContextValue | null>(null);
|
|
29
|
-
|
|
30
|
-
const STORAGE_KEY = 'leanspec-current-project';
|
|
31
|
-
|
|
32
|
-
export function ProjectProvider({ children }: { children: ReactNode }) {
|
|
33
|
-
const [currentProject, setCurrentProject] = useState<Project | null>(null);
|
|
34
|
-
const [projects, setProjects] = useState<Project[]>([]);
|
|
35
|
-
const [favoriteProjects, setFavoriteProjects] = useState<Project[]>([]);
|
|
36
|
-
const [loading, setLoading] = useState(true);
|
|
37
|
-
const [error, setError] = useState<string | null>(null);
|
|
38
|
-
|
|
39
|
-
const applyProjects = useCallback((data: ProjectsResponse) => {
|
|
40
|
-
const normalized = data.projects || [];
|
|
41
|
-
setProjects(normalized);
|
|
42
|
-
setFavoriteProjects(normalized.filter((project: Project) => project.favorite));
|
|
43
|
-
|
|
44
|
-
const storedId = localStorage.getItem(STORAGE_KEY);
|
|
45
|
-
const nextCurrent = (storedId ? normalized.find((p: Project) => p.id === storedId) || null : null)
|
|
46
|
-
|| normalized[0]
|
|
47
|
-
|| null;
|
|
48
|
-
|
|
49
|
-
setCurrentProject(nextCurrent);
|
|
50
|
-
if (nextCurrent) {
|
|
51
|
-
localStorage.setItem(STORAGE_KEY, nextCurrent.id);
|
|
52
|
-
api.setCurrentProjectId(nextCurrent.id);
|
|
53
|
-
} else {
|
|
54
|
-
api.setCurrentProjectId(null);
|
|
55
|
-
}
|
|
56
|
-
}, []);
|
|
57
|
-
|
|
58
|
-
const refreshProjects = useCallback(async () => {
|
|
59
|
-
setLoading(true);
|
|
60
|
-
setError(null);
|
|
61
|
-
try {
|
|
62
|
-
const data: ProjectsResponse = await api.getProjects();
|
|
63
|
-
applyProjects(data);
|
|
64
|
-
} catch (err: unknown) {
|
|
65
|
-
console.error('Failed to load projects', err);
|
|
66
|
-
setError(i18n.t('projects.errors.load', { ns: 'common' }));
|
|
67
|
-
} finally {
|
|
68
|
-
setLoading(false);
|
|
69
|
-
}
|
|
70
|
-
}, [applyProjects]);
|
|
71
|
-
|
|
72
|
-
const switchProject = useCallback(async (projectId: string) => {
|
|
73
|
-
if (projectId === currentProject?.id) return;
|
|
74
|
-
|
|
75
|
-
setLoading(true);
|
|
76
|
-
setError(null);
|
|
77
|
-
try {
|
|
78
|
-
localStorage.setItem(STORAGE_KEY, projectId);
|
|
79
|
-
const data: ProjectsResponse = await api.getProjects();
|
|
80
|
-
applyProjects(data);
|
|
81
|
-
} catch (err: unknown) {
|
|
82
|
-
console.error('Failed to switch project', err);
|
|
83
|
-
setError(i18n.t('projects.errors.switch', { ns: 'common' }));
|
|
84
|
-
throw err;
|
|
85
|
-
} finally {
|
|
86
|
-
setLoading(false);
|
|
87
|
-
}
|
|
88
|
-
}, [applyProjects, currentProject?.id]);
|
|
89
|
-
|
|
90
|
-
const addProject = useCallback(async (
|
|
91
|
-
path: string,
|
|
92
|
-
options?: { favorite?: boolean; color?: string; name?: string; description?: string | null }
|
|
93
|
-
): Promise<Project> => {
|
|
94
|
-
setError(null);
|
|
95
|
-
const project = await api.createProject(path, options);
|
|
96
|
-
await refreshProjects();
|
|
97
|
-
await switchProject(project.id);
|
|
98
|
-
return project;
|
|
99
|
-
}, [refreshProjects, switchProject]);
|
|
100
|
-
|
|
101
|
-
const updateProject = useCallback(async (
|
|
102
|
-
projectId: string,
|
|
103
|
-
updates: Partial<Pick<Project, 'name' | 'color' | 'favorite' | 'description'>>
|
|
104
|
-
) => {
|
|
105
|
-
setError(null);
|
|
106
|
-
const updated = await api.updateProject(projectId, updates);
|
|
107
|
-
await refreshProjects();
|
|
108
|
-
return updated;
|
|
109
|
-
}, [refreshProjects]);
|
|
110
|
-
|
|
111
|
-
const removeProject = useCallback(async (projectId: string) => {
|
|
112
|
-
setError(null);
|
|
113
|
-
await api.deleteProject(projectId);
|
|
114
|
-
if (currentProject?.id === projectId) {
|
|
115
|
-
localStorage.removeItem(STORAGE_KEY);
|
|
116
|
-
}
|
|
117
|
-
await refreshProjects();
|
|
118
|
-
}, [currentProject?.id, refreshProjects]);
|
|
119
|
-
|
|
120
|
-
const toggleFavorite = useCallback(async (projectId: string) => {
|
|
121
|
-
const project = projects.find((p) => p.id === projectId);
|
|
122
|
-
const nextFavorite = !(project?.favorite ?? false);
|
|
123
|
-
await updateProject(projectId, { favorite: nextFavorite });
|
|
124
|
-
}, [projects, updateProject]);
|
|
125
|
-
|
|
126
|
-
const validateProject = useCallback(async (projectId: string) => {
|
|
127
|
-
return api.validateProject(projectId);
|
|
128
|
-
}, []);
|
|
129
|
-
|
|
130
|
-
useEffect(() => {
|
|
131
|
-
refreshProjects();
|
|
132
|
-
}, [refreshProjects]);
|
|
133
|
-
|
|
134
|
-
return (
|
|
135
|
-
<ProjectContext.Provider
|
|
136
|
-
value={{
|
|
137
|
-
currentProject,
|
|
138
|
-
projects,
|
|
139
|
-
availableProjects: projects,
|
|
140
|
-
favoriteProjects,
|
|
141
|
-
loading,
|
|
142
|
-
error,
|
|
143
|
-
switchProject,
|
|
144
|
-
addProject,
|
|
145
|
-
updateProject,
|
|
146
|
-
removeProject,
|
|
147
|
-
toggleFavorite,
|
|
148
|
-
refreshProjects,
|
|
149
|
-
validateProject,
|
|
150
|
-
}}
|
|
151
|
-
>
|
|
152
|
-
{children}
|
|
153
|
-
</ProjectContext.Provider>
|
|
154
|
-
);
|
|
155
|
-
}
|
|
156
|
-
|
|
157
|
-
export function useProject() {
|
|
158
|
-
const context = useContext(ProjectContext);
|
|
159
|
-
if (!context) {
|
|
160
|
-
throw new Error('useProject must be used within a ProjectProvider');
|
|
161
|
-
}
|
|
162
|
-
return context;
|
|
163
|
-
}
|
|
@@ -1,90 +0,0 @@
|
|
|
1
|
-
import { createContext, useContext, useState, useEffect, type ReactNode } from 'react';
|
|
2
|
-
|
|
3
|
-
type Theme = 'light' | 'dark' | 'system';
|
|
4
|
-
|
|
5
|
-
interface ThemeContextValue {
|
|
6
|
-
theme: Theme;
|
|
7
|
-
setTheme: (theme: Theme) => void;
|
|
8
|
-
resolvedTheme: 'light' | 'dark';
|
|
9
|
-
}
|
|
10
|
-
|
|
11
|
-
const ThemeContext = createContext<ThemeContextValue | null>(null);
|
|
12
|
-
|
|
13
|
-
const STORAGE_KEY = 'leanspec-theme';
|
|
14
|
-
|
|
15
|
-
function getSystemTheme(): 'light' | 'dark' {
|
|
16
|
-
if (typeof window === 'undefined') return 'light';
|
|
17
|
-
return window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light';
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
export function ThemeProvider({ children }: { children: ReactNode }) {
|
|
21
|
-
const [theme, setThemeState] = useState<Theme>(() => {
|
|
22
|
-
if (typeof window === 'undefined') return 'system';
|
|
23
|
-
return (localStorage.getItem(STORAGE_KEY) as Theme) || 'system';
|
|
24
|
-
});
|
|
25
|
-
|
|
26
|
-
const resolvedTheme = theme === 'system' ? getSystemTheme() : theme;
|
|
27
|
-
|
|
28
|
-
// Apply theme to document
|
|
29
|
-
useEffect(() => {
|
|
30
|
-
const root = document.documentElement;
|
|
31
|
-
|
|
32
|
-
// Add changing-theme class to disable transitions
|
|
33
|
-
root.classList.add('changing-theme');
|
|
34
|
-
|
|
35
|
-
// Update theme classes
|
|
36
|
-
root.classList.remove('light', 'dark');
|
|
37
|
-
root.classList.add(resolvedTheme);
|
|
38
|
-
|
|
39
|
-
// Remove changing-theme class after a brief delay to allow DOM to update
|
|
40
|
-
const timeoutId = setTimeout(() => {
|
|
41
|
-
root.classList.remove('changing-theme');
|
|
42
|
-
}, 50);
|
|
43
|
-
|
|
44
|
-
return () => clearTimeout(timeoutId);
|
|
45
|
-
}, [resolvedTheme]);
|
|
46
|
-
|
|
47
|
-
// Listen for system theme changes
|
|
48
|
-
useEffect(() => {
|
|
49
|
-
if (theme !== 'system') return;
|
|
50
|
-
|
|
51
|
-
const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)');
|
|
52
|
-
const handleChange = () => {
|
|
53
|
-
const root = document.documentElement;
|
|
54
|
-
|
|
55
|
-
// Add changing-theme class to disable transitions
|
|
56
|
-
root.classList.add('changing-theme');
|
|
57
|
-
|
|
58
|
-
// Update theme classes
|
|
59
|
-
root.classList.remove('light', 'dark');
|
|
60
|
-
root.classList.add(getSystemTheme());
|
|
61
|
-
|
|
62
|
-
// Remove changing-theme class after a brief delay
|
|
63
|
-
setTimeout(() => {
|
|
64
|
-
root.classList.remove('changing-theme');
|
|
65
|
-
}, 50);
|
|
66
|
-
};
|
|
67
|
-
|
|
68
|
-
mediaQuery.addEventListener('change', handleChange);
|
|
69
|
-
return () => mediaQuery.removeEventListener('change', handleChange);
|
|
70
|
-
}, [theme]);
|
|
71
|
-
|
|
72
|
-
const setTheme = (newTheme: Theme) => {
|
|
73
|
-
setThemeState(newTheme);
|
|
74
|
-
localStorage.setItem(STORAGE_KEY, newTheme);
|
|
75
|
-
};
|
|
76
|
-
|
|
77
|
-
return (
|
|
78
|
-
<ThemeContext.Provider value={{ theme, setTheme, resolvedTheme }}>
|
|
79
|
-
{children}
|
|
80
|
-
</ThemeContext.Provider>
|
|
81
|
-
);
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
export function useTheme() {
|
|
85
|
-
const context = useContext(ThemeContext);
|
|
86
|
-
if (!context) {
|
|
87
|
-
throw new Error('useTheme must be used within a ThemeProvider');
|
|
88
|
-
}
|
|
89
|
-
return context;
|
|
90
|
-
}
|
package/src/contexts/index.ts
DELETED
|
@@ -1,7 +0,0 @@
|
|
|
1
|
-
export { ProjectProvider, useProject } from './ProjectContext';
|
|
2
|
-
export { ThemeProvider, useTheme } from './ThemeContext';
|
|
3
|
-
export { LayoutProvider, useLayout } from './LayoutContext';
|
|
4
|
-
export {
|
|
5
|
-
KeyboardShortcutsProvider,
|
|
6
|
-
useKeyboardShortcuts,
|
|
7
|
-
} from './KeyboardShortcutsContext';
|
|
@@ -1,87 +0,0 @@
|
|
|
1
|
-
import { useEffect, useCallback } from 'react';
|
|
2
|
-
import { useNavigate, useParams } from 'react-router-dom';
|
|
3
|
-
|
|
4
|
-
export interface KeyboardShortcut {
|
|
5
|
-
key: string;
|
|
6
|
-
ctrl?: boolean;
|
|
7
|
-
meta?: boolean;
|
|
8
|
-
shift?: boolean;
|
|
9
|
-
description: string;
|
|
10
|
-
action: () => void;
|
|
11
|
-
}
|
|
12
|
-
|
|
13
|
-
export function useKeyboardShortcuts(shortcuts: KeyboardShortcut[]) {
|
|
14
|
-
useEffect(() => {
|
|
15
|
-
function handleKeyDown(event: KeyboardEvent) {
|
|
16
|
-
// Don't trigger shortcuts when typing in inputs
|
|
17
|
-
if (
|
|
18
|
-
event.target instanceof HTMLInputElement ||
|
|
19
|
-
event.target instanceof HTMLTextAreaElement ||
|
|
20
|
-
event.target instanceof HTMLSelectElement
|
|
21
|
-
) {
|
|
22
|
-
return;
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
for (const shortcut of shortcuts) {
|
|
26
|
-
const keyMatch = event.key.toLowerCase() === shortcut.key.toLowerCase();
|
|
27
|
-
const ctrlMatch = shortcut.ctrl ? (event.ctrlKey || event.metaKey) : true;
|
|
28
|
-
const shiftMatch = shortcut.shift ? event.shiftKey : !event.shiftKey;
|
|
29
|
-
|
|
30
|
-
if (keyMatch && ctrlMatch && shiftMatch) {
|
|
31
|
-
event.preventDefault();
|
|
32
|
-
shortcut.action();
|
|
33
|
-
return;
|
|
34
|
-
}
|
|
35
|
-
}
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
document.addEventListener('keydown', handleKeyDown);
|
|
39
|
-
return () => document.removeEventListener('keydown', handleKeyDown);
|
|
40
|
-
}, [shortcuts]);
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
export function useGlobalShortcuts() {
|
|
44
|
-
const navigate = useNavigate();
|
|
45
|
-
const { projectId } = useParams<{ projectId: string }>();
|
|
46
|
-
const basePath = projectId ? `/projects/${projectId}` : '/projects/default';
|
|
47
|
-
|
|
48
|
-
const shortcuts: KeyboardShortcut[] = [
|
|
49
|
-
{
|
|
50
|
-
key: 'h',
|
|
51
|
-
description: 'Go to dashboard (home)',
|
|
52
|
-
action: useCallback(() => navigate(basePath), [basePath, navigate]),
|
|
53
|
-
},
|
|
54
|
-
{
|
|
55
|
-
key: 'g',
|
|
56
|
-
description: 'Go to specs list',
|
|
57
|
-
action: useCallback(() => navigate(`${basePath}/specs`), [basePath, navigate]),
|
|
58
|
-
},
|
|
59
|
-
{
|
|
60
|
-
key: 's',
|
|
61
|
-
description: 'Go to stats',
|
|
62
|
-
action: useCallback(() => navigate(`${basePath}/stats`), [basePath, navigate]),
|
|
63
|
-
},
|
|
64
|
-
{
|
|
65
|
-
key: 'd',
|
|
66
|
-
description: 'Go to dependencies',
|
|
67
|
-
action: useCallback(() => navigate(`${basePath}/dependencies`), [basePath, navigate]),
|
|
68
|
-
},
|
|
69
|
-
{
|
|
70
|
-
key: ',',
|
|
71
|
-
description: 'Go to settings',
|
|
72
|
-
action: useCallback(() => navigate(`${basePath}/settings`), [basePath, navigate]),
|
|
73
|
-
},
|
|
74
|
-
{
|
|
75
|
-
key: '/',
|
|
76
|
-
description: 'Focus search (on specs page)',
|
|
77
|
-
action: useCallback(() => {
|
|
78
|
-
const searchInput = document.querySelector<HTMLInputElement>('input[type="text"]');
|
|
79
|
-
searchInput?.focus();
|
|
80
|
-
}, []),
|
|
81
|
-
},
|
|
82
|
-
];
|
|
83
|
-
|
|
84
|
-
useKeyboardShortcuts(shortcuts);
|
|
85
|
-
|
|
86
|
-
return shortcuts;
|
|
87
|
-
}
|