@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,68 +0,0 @@
|
|
|
1
|
-
import { useState } from 'react';
|
|
2
|
-
import { Button, Popover, PopoverContent, PopoverTrigger } from '@leanspec/ui-components';
|
|
3
|
-
import { cn } from '../../lib/utils';
|
|
4
|
-
import { useTranslation } from 'react-i18next';
|
|
5
|
-
|
|
6
|
-
const PROJECT_COLORS = [
|
|
7
|
-
'#ef4444',
|
|
8
|
-
'#f97316',
|
|
9
|
-
'#eab308',
|
|
10
|
-
'#22c55e',
|
|
11
|
-
'#14b8a6',
|
|
12
|
-
'#3b82f6',
|
|
13
|
-
'#6366f1',
|
|
14
|
-
'#8b5cf6',
|
|
15
|
-
'#d946ef',
|
|
16
|
-
'#ec4899',
|
|
17
|
-
'#6b7280',
|
|
18
|
-
'#78716c',
|
|
19
|
-
];
|
|
20
|
-
|
|
21
|
-
interface ColorPickerProps {
|
|
22
|
-
value?: string | null;
|
|
23
|
-
onChange: (color: string) => void;
|
|
24
|
-
disabled?: boolean;
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
export function ColorPicker({ value, onChange, disabled }: ColorPickerProps) {
|
|
28
|
-
const [open, setOpen] = useState(false);
|
|
29
|
-
const { t } = useTranslation('common');
|
|
30
|
-
|
|
31
|
-
return (
|
|
32
|
-
<Popover open={open} onOpenChange={setOpen}>
|
|
33
|
-
<PopoverTrigger asChild>
|
|
34
|
-
<Button
|
|
35
|
-
variant="outline"
|
|
36
|
-
size="sm"
|
|
37
|
-
className="h-8 w-8 p-0"
|
|
38
|
-
disabled={disabled}
|
|
39
|
-
aria-label={t('colorPicker.pickColor')}
|
|
40
|
-
>
|
|
41
|
-
<div
|
|
42
|
-
className="h-4 w-4 rounded-full border"
|
|
43
|
-
style={{ backgroundColor: value || '#666' }}
|
|
44
|
-
/>
|
|
45
|
-
</Button>
|
|
46
|
-
</PopoverTrigger>
|
|
47
|
-
<PopoverContent className="w-[200px] p-3" align="start">
|
|
48
|
-
<div className="grid grid-cols-6 gap-2">
|
|
49
|
-
{PROJECT_COLORS.map((color) => (
|
|
50
|
-
<button
|
|
51
|
-
key={color}
|
|
52
|
-
className={cn(
|
|
53
|
-
'h-6 w-6 rounded-full border-2 transition-transform hover:scale-110',
|
|
54
|
-
value === color ? 'border-primary' : 'border-transparent'
|
|
55
|
-
)}
|
|
56
|
-
style={{ backgroundColor: color }}
|
|
57
|
-
onClick={() => {
|
|
58
|
-
onChange(color);
|
|
59
|
-
setOpen(false);
|
|
60
|
-
}}
|
|
61
|
-
aria-label={t('colorPicker.selectColor', { color })}
|
|
62
|
-
/>
|
|
63
|
-
))}
|
|
64
|
-
</div>
|
|
65
|
-
</PopoverContent>
|
|
66
|
-
</Popover>
|
|
67
|
-
);
|
|
68
|
-
}
|
|
@@ -1,35 +0,0 @@
|
|
|
1
|
-
import type { LucideIcon } from 'lucide-react';
|
|
2
|
-
import { Card, CardContent, Button } from '@leanspec/ui-components';
|
|
3
|
-
import { cn } from '../../lib/utils';
|
|
4
|
-
import type { ReactNode } from 'react';
|
|
5
|
-
|
|
6
|
-
interface EmptyStateProps {
|
|
7
|
-
icon: LucideIcon;
|
|
8
|
-
title: string;
|
|
9
|
-
description?: string;
|
|
10
|
-
actions?: ReactNode;
|
|
11
|
-
className?: string;
|
|
12
|
-
tone?: 'muted' | 'error';
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
export function EmptyState({ icon: Icon, title, description, actions, className, tone = 'muted' }: EmptyStateProps) {
|
|
16
|
-
return (
|
|
17
|
-
<Card className={cn('border-dashed', className)}>
|
|
18
|
-
<CardContent className="py-10 text-center space-y-3">
|
|
19
|
-
<div className="flex justify-center">
|
|
20
|
-
<Button
|
|
21
|
-
size="icon"
|
|
22
|
-
variant={tone === 'error' ? 'destructive' : 'secondary'}
|
|
23
|
-
className="h-10 w-10 rounded-full"
|
|
24
|
-
aria-label={title}
|
|
25
|
-
>
|
|
26
|
-
<Icon className="h-5 w-5" />
|
|
27
|
-
</Button>
|
|
28
|
-
</div>
|
|
29
|
-
<div className="text-lg font-semibold">{title}</div>
|
|
30
|
-
{description && <p className="text-sm text-muted-foreground max-w-xl mx-auto">{description}</p>}
|
|
31
|
-
{actions && <div className="flex justify-center gap-2 flex-wrap pt-1">{actions}</div>}
|
|
32
|
-
</CardContent>
|
|
33
|
-
</Card>
|
|
34
|
-
);
|
|
35
|
-
}
|
|
@@ -1,79 +0,0 @@
|
|
|
1
|
-
import { type ReactNode } from 'react';
|
|
2
|
-
import { ErrorBoundary as ReactErrorBoundary } from 'react-error-boundary';
|
|
3
|
-
import { AlertTriangle } from 'lucide-react';
|
|
4
|
-
import { Button } from '@leanspec/ui-components';
|
|
5
|
-
import { EmptyState } from './EmptyState';
|
|
6
|
-
import i18n from '../../lib/i18n';
|
|
7
|
-
|
|
8
|
-
interface Props {
|
|
9
|
-
children: ReactNode;
|
|
10
|
-
title?: string;
|
|
11
|
-
message?: string;
|
|
12
|
-
onReset?: () => void;
|
|
13
|
-
/**
|
|
14
|
-
* When this value changes, the boundary resets its error state.
|
|
15
|
-
* This lets us recover on navigation without remounting the whole subtree.
|
|
16
|
-
*/
|
|
17
|
-
resetKey?: unknown;
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
interface FallbackProps {
|
|
21
|
-
error: Error;
|
|
22
|
-
resetErrorBoundary: () => void;
|
|
23
|
-
title?: string;
|
|
24
|
-
message?: string;
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
/**
|
|
28
|
-
* ErrorBoundary wrapper using react-error-boundary library.
|
|
29
|
-
* Provides a hook-friendly API while using a class component internally.
|
|
30
|
-
*/
|
|
31
|
-
function ErrorFallback({ error, resetErrorBoundary, title, message }: FallbackProps) {
|
|
32
|
-
// Use i18n.t directly with fallbacks to avoid hook issues
|
|
33
|
-
const fallbackTitle = i18n.t?.('pageError.title', { ns: 'errors', defaultValue: 'Something went wrong' }) || 'Something went wrong';
|
|
34
|
-
const fallbackMessage = i18n.t?.('pageError.description', { ns: 'errors', defaultValue: 'An unexpected error occurred. Please try again.' }) || 'An unexpected error occurred. Please try again.';
|
|
35
|
-
const retryLabel = i18n.t?.('actions.retry', { ns: 'common', defaultValue: 'Try again' }) || 'Try again';
|
|
36
|
-
const reloadLabel = i18n.t?.('actions.refresh', { ns: 'common', defaultValue: 'Reload' }) || 'Reload';
|
|
37
|
-
|
|
38
|
-
return (
|
|
39
|
-
<EmptyState
|
|
40
|
-
icon={AlertTriangle}
|
|
41
|
-
title={title || fallbackTitle}
|
|
42
|
-
description={message || error?.message || fallbackMessage}
|
|
43
|
-
tone="error"
|
|
44
|
-
actions={(
|
|
45
|
-
<>
|
|
46
|
-
<Button size="sm" onClick={resetErrorBoundary}>
|
|
47
|
-
{retryLabel}
|
|
48
|
-
</Button>
|
|
49
|
-
<Button size="sm" variant="outline" onClick={() => window.location.reload()}>
|
|
50
|
-
{reloadLabel}
|
|
51
|
-
</Button>
|
|
52
|
-
</>
|
|
53
|
-
)}
|
|
54
|
-
/>
|
|
55
|
-
);
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
export function ErrorBoundary({ children, title, message, onReset, resetKey }: Props) {
|
|
59
|
-
const handleReset = () => {
|
|
60
|
-
onReset?.();
|
|
61
|
-
};
|
|
62
|
-
|
|
63
|
-
const handleError = (error: Error, errorInfo: { componentStack?: string | null }) => {
|
|
64
|
-
console.error('UI error captured', error, errorInfo);
|
|
65
|
-
};
|
|
66
|
-
|
|
67
|
-
return (
|
|
68
|
-
<ReactErrorBoundary
|
|
69
|
-
FallbackComponent={(props) => (
|
|
70
|
-
<ErrorFallback {...props} title={title} message={message} />
|
|
71
|
-
)}
|
|
72
|
-
onReset={handleReset}
|
|
73
|
-
onError={handleError}
|
|
74
|
-
resetKeys={[resetKey]}
|
|
75
|
-
>
|
|
76
|
-
{children}
|
|
77
|
-
</ReactErrorBoundary>
|
|
78
|
-
);
|
|
79
|
-
}
|
|
@@ -1,23 +0,0 @@
|
|
|
1
|
-
import type { ReactNode } from 'react';
|
|
2
|
-
|
|
3
|
-
interface PageHeaderProps {
|
|
4
|
-
title: ReactNode;
|
|
5
|
-
description?: ReactNode;
|
|
6
|
-
actions?: ReactNode;
|
|
7
|
-
}
|
|
8
|
-
|
|
9
|
-
export function PageHeader({ title, description, actions }: PageHeaderProps) {
|
|
10
|
-
return (
|
|
11
|
-
<div className="flex flex-col gap-3 sm:flex-row sm:items-start sm:justify-between">
|
|
12
|
-
<div>
|
|
13
|
-
<h1 className="text-3xl sm:text-4xl font-bold tracking-tight">{title}</h1>
|
|
14
|
-
{description ? (
|
|
15
|
-
<p className="text-muted-foreground mt-2">{description}</p>
|
|
16
|
-
) : null}
|
|
17
|
-
</div>
|
|
18
|
-
{actions ? (
|
|
19
|
-
<div className="flex flex-wrap items-center gap-2 sm:pt-1">{actions}</div>
|
|
20
|
-
) : null}
|
|
21
|
-
</div>
|
|
22
|
-
);
|
|
23
|
-
}
|
|
@@ -1,40 +0,0 @@
|
|
|
1
|
-
import { useEffect, useState, type ReactNode } from 'react';
|
|
2
|
-
import { useLocation } from 'react-router-dom';
|
|
3
|
-
import { cn } from '../../lib/utils';
|
|
4
|
-
|
|
5
|
-
interface PageTransitionProps {
|
|
6
|
-
className?: string;
|
|
7
|
-
children: ReactNode;
|
|
8
|
-
}
|
|
9
|
-
|
|
10
|
-
export function PageTransition({ className, children }: PageTransitionProps) {
|
|
11
|
-
const location = useLocation();
|
|
12
|
-
const [visible, setVisible] = useState(false);
|
|
13
|
-
|
|
14
|
-
useEffect(() => {
|
|
15
|
-
let raf1 = 0;
|
|
16
|
-
let raf2 = 0;
|
|
17
|
-
|
|
18
|
-
raf1 = window.requestAnimationFrame(() => {
|
|
19
|
-
setVisible(false);
|
|
20
|
-
raf2 = window.requestAnimationFrame(() => setVisible(true));
|
|
21
|
-
});
|
|
22
|
-
|
|
23
|
-
return () => {
|
|
24
|
-
window.cancelAnimationFrame(raf1);
|
|
25
|
-
window.cancelAnimationFrame(raf2);
|
|
26
|
-
};
|
|
27
|
-
}, [location.pathname]);
|
|
28
|
-
|
|
29
|
-
return (
|
|
30
|
-
<div
|
|
31
|
-
className={cn(
|
|
32
|
-
className,
|
|
33
|
-
'transition-all duration-200 ease-out',
|
|
34
|
-
visible ? 'opacity-100 translate-y-0' : 'opacity-0 translate-y-2'
|
|
35
|
-
)}
|
|
36
|
-
>
|
|
37
|
-
{children}
|
|
38
|
-
</div>
|
|
39
|
-
);
|
|
40
|
-
}
|
|
@@ -1,107 +0,0 @@
|
|
|
1
|
-
import * as React from 'react';
|
|
2
|
-
import { Avatar, AvatarFallback, AvatarImage } from '@leanspec/ui-components';
|
|
3
|
-
import { cn } from '../../lib/utils';
|
|
4
|
-
|
|
5
|
-
interface ProjectAvatarProps {
|
|
6
|
-
name: string;
|
|
7
|
-
color?: string | null;
|
|
8
|
-
icon?: string;
|
|
9
|
-
size?: 'sm' | 'md' | 'lg' | 'xl';
|
|
10
|
-
className?: string;
|
|
11
|
-
}
|
|
12
|
-
|
|
13
|
-
const sizeClasses = {
|
|
14
|
-
sm: 'h-6 w-6 text-[10px]',
|
|
15
|
-
md: 'h-8 w-8 text-xs',
|
|
16
|
-
lg: 'h-10 w-10 text-sm',
|
|
17
|
-
xl: 'h-12 w-12 text-base',
|
|
18
|
-
};
|
|
19
|
-
|
|
20
|
-
function getInitials(name: string): string {
|
|
21
|
-
if (!name) return '??';
|
|
22
|
-
|
|
23
|
-
const words = name.trim().split(/\s+/);
|
|
24
|
-
|
|
25
|
-
if (words.length >= 2) {
|
|
26
|
-
return (words[0][0] + words[1][0]).toUpperCase();
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
const word = words[0];
|
|
30
|
-
return word.length >= 2 ? (word[0] + word[1]).toUpperCase() : word[0].toUpperCase();
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
function getContrastColor(hexColor?: string): string | undefined {
|
|
34
|
-
if (!hexColor) return undefined;
|
|
35
|
-
|
|
36
|
-
const hex = hexColor.replace('#', '');
|
|
37
|
-
const r = parseInt(hex.substring(0, 2), 16);
|
|
38
|
-
const g = parseInt(hex.substring(2, 4), 16);
|
|
39
|
-
const b = parseInt(hex.substring(4, 6), 16);
|
|
40
|
-
const luminance = (0.299 * r + 0.587 * g + 0.114 * b) / 255;
|
|
41
|
-
|
|
42
|
-
return luminance > 0.5 ? '#000000' : '#ffffff';
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
const AVATAR_COLORS = [
|
|
46
|
-
'#ef4444', // red-500
|
|
47
|
-
'#f97316', // orange-500
|
|
48
|
-
'#f59e0b', // amber-500
|
|
49
|
-
'#84cc16', // lime-500
|
|
50
|
-
'#22c55e', // green-500
|
|
51
|
-
'#10b981', // emerald-500
|
|
52
|
-
'#06b6d4', // cyan-500
|
|
53
|
-
'#0ea5e9', // sky-500
|
|
54
|
-
'#3b82f6', // blue-500
|
|
55
|
-
'#6366f1', // indigo-500
|
|
56
|
-
'#8b5cf6', // violet-500
|
|
57
|
-
'#d946ef', // fuchsia-500
|
|
58
|
-
'#ec4899', // pink-500
|
|
59
|
-
'#f43f5e', // rose-500
|
|
60
|
-
'#78716c', // stone-500
|
|
61
|
-
];
|
|
62
|
-
|
|
63
|
-
export function getColorForName(name: string): string {
|
|
64
|
-
if (!name) return 'hsl(var(--primary))';
|
|
65
|
-
|
|
66
|
-
let hash = 0;
|
|
67
|
-
for (let i = 0; i < name.length; i++) {
|
|
68
|
-
hash = name.charCodeAt(i) + ((hash << 5) - hash);
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
const index = Math.abs(hash % AVATAR_COLORS.length);
|
|
72
|
-
return AVATAR_COLORS[index];
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
export function ProjectAvatar({
|
|
76
|
-
name,
|
|
77
|
-
color,
|
|
78
|
-
icon,
|
|
79
|
-
size = 'md',
|
|
80
|
-
className,
|
|
81
|
-
}: ProjectAvatarProps) {
|
|
82
|
-
const initials = React.useMemo(() => getInitials(name), [name]);
|
|
83
|
-
|
|
84
|
-
const displayColor = React.useMemo(() => {
|
|
85
|
-
if (color) return color;
|
|
86
|
-
return getColorForName(name);
|
|
87
|
-
}, [color, name]);
|
|
88
|
-
|
|
89
|
-
const textColor = React.useMemo(() =>
|
|
90
|
-
getContrastColor(displayColor?.startsWith('#') ? displayColor : undefined),
|
|
91
|
-
[displayColor]);
|
|
92
|
-
|
|
93
|
-
return (
|
|
94
|
-
<Avatar className={cn(sizeClasses[size], className)}>
|
|
95
|
-
{icon && <AvatarImage src={icon} alt={name} />}
|
|
96
|
-
<AvatarFallback
|
|
97
|
-
className="font-semibold border"
|
|
98
|
-
style={{
|
|
99
|
-
backgroundColor: displayColor,
|
|
100
|
-
...(textColor && { color: textColor }),
|
|
101
|
-
}}
|
|
102
|
-
>
|
|
103
|
-
{initials}
|
|
104
|
-
</AvatarFallback>
|
|
105
|
-
</Avatar>
|
|
106
|
-
);
|
|
107
|
-
}
|
|
@@ -1,184 +0,0 @@
|
|
|
1
|
-
import { Card, CardContent, CardHeader, Skeleton } from '@leanspec/ui-components';
|
|
2
|
-
|
|
3
|
-
export function DashboardSkeleton() {
|
|
4
|
-
return (
|
|
5
|
-
<div className="space-y-6">
|
|
6
|
-
<div className="space-y-2">
|
|
7
|
-
<Skeleton className="h-5 w-24" />
|
|
8
|
-
<Skeleton className="h-8 w-64" />
|
|
9
|
-
</div>
|
|
10
|
-
|
|
11
|
-
<div className="grid gap-4 sm:grid-cols-2 lg:grid-cols-4">
|
|
12
|
-
{[...Array(4)].map((_, idx) => (
|
|
13
|
-
<Card key={idx} className="overflow-hidden">
|
|
14
|
-
<CardContent className="p-4 space-y-3">
|
|
15
|
-
<Skeleton className="h-4 w-28" />
|
|
16
|
-
<Skeleton className="h-8 w-20" />
|
|
17
|
-
<Skeleton className="h-3 w-16" />
|
|
18
|
-
</CardContent>
|
|
19
|
-
</Card>
|
|
20
|
-
))}
|
|
21
|
-
</div>
|
|
22
|
-
|
|
23
|
-
<div className="grid gap-6 lg:grid-cols-2">
|
|
24
|
-
<Card>
|
|
25
|
-
<CardHeader className="pb-2">
|
|
26
|
-
<Skeleton className="h-5 w-32" />
|
|
27
|
-
</CardHeader>
|
|
28
|
-
<CardContent className="space-y-2">
|
|
29
|
-
{[...Array(5)].map((_, idx) => (
|
|
30
|
-
<div key={idx} className="space-y-2">
|
|
31
|
-
<Skeleton className="h-5 w-3/4" />
|
|
32
|
-
<Skeleton className="h-4 w-1/2" />
|
|
33
|
-
</div>
|
|
34
|
-
))}
|
|
35
|
-
</CardContent>
|
|
36
|
-
</Card>
|
|
37
|
-
<Card>
|
|
38
|
-
<CardHeader className="pb-2">
|
|
39
|
-
<Skeleton className="h-5 w-28" />
|
|
40
|
-
</CardHeader>
|
|
41
|
-
<CardContent className="space-y-2">
|
|
42
|
-
{[...Array(4)].map((_, idx) => (
|
|
43
|
-
<div key={idx} className="space-y-2">
|
|
44
|
-
<Skeleton className="h-5 w-2/3" />
|
|
45
|
-
<Skeleton className="h-4 w-1/3" />
|
|
46
|
-
</div>
|
|
47
|
-
))}
|
|
48
|
-
</CardContent>
|
|
49
|
-
</Card>
|
|
50
|
-
</div>
|
|
51
|
-
</div>
|
|
52
|
-
);
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
export function SpecListSkeleton() {
|
|
56
|
-
return (
|
|
57
|
-
<div className="space-y-4">
|
|
58
|
-
{[...Array(5)].map((_, i) => (
|
|
59
|
-
<Card key={i}>
|
|
60
|
-
<CardHeader className="pb-3">
|
|
61
|
-
<div className="flex items-start justify-between gap-4">
|
|
62
|
-
<div className="space-y-2 flex-1">
|
|
63
|
-
<Skeleton className="h-6 w-3/4" />
|
|
64
|
-
<Skeleton className="h-4 w-1/2" />
|
|
65
|
-
</div>
|
|
66
|
-
<Skeleton className="h-6 w-20" />
|
|
67
|
-
</div>
|
|
68
|
-
</CardHeader>
|
|
69
|
-
<CardContent>
|
|
70
|
-
<div className="flex items-center gap-2">
|
|
71
|
-
<Skeleton className="h-5 w-16" />
|
|
72
|
-
<Skeleton className="h-5 w-16" />
|
|
73
|
-
<Skeleton className="h-5 w-16" />
|
|
74
|
-
</div>
|
|
75
|
-
</CardContent>
|
|
76
|
-
</Card>
|
|
77
|
-
))}
|
|
78
|
-
</div>
|
|
79
|
-
);
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
export function SpecDetailSkeleton() {
|
|
83
|
-
return (
|
|
84
|
-
<div className="container mx-auto py-8 px-4 max-w-6xl">
|
|
85
|
-
<div className="space-y-6">
|
|
86
|
-
<Skeleton className="h-5 w-48" />
|
|
87
|
-
<div className="space-y-3">
|
|
88
|
-
<Skeleton className="h-10 w-2/3" />
|
|
89
|
-
<Skeleton className="h-5 w-1/3" />
|
|
90
|
-
</div>
|
|
91
|
-
<div className="grid grid-cols-2 md:grid-cols-4 gap-4">
|
|
92
|
-
{[...Array(4)].map((_, i) => (
|
|
93
|
-
<Card key={i}>
|
|
94
|
-
<CardContent className="p-4 space-y-2">
|
|
95
|
-
<Skeleton className="h-4 w-20" />
|
|
96
|
-
<Skeleton className="h-6 w-24" />
|
|
97
|
-
</CardContent>
|
|
98
|
-
</Card>
|
|
99
|
-
))}
|
|
100
|
-
</div>
|
|
101
|
-
<Card>
|
|
102
|
-
<CardContent className="p-6 space-y-4">
|
|
103
|
-
<Skeleton className="h-8 w-1/3" />
|
|
104
|
-
<Skeleton className="h-4 w-full" />
|
|
105
|
-
<Skeleton className="h-4 w-full" />
|
|
106
|
-
<Skeleton className="h-4 w-3/4" />
|
|
107
|
-
<div className="py-2" />
|
|
108
|
-
<Skeleton className="h-8 w-1/4" />
|
|
109
|
-
<Skeleton className="h-4 w-full" />
|
|
110
|
-
<Skeleton className="h-4 w-4/5" />
|
|
111
|
-
</CardContent>
|
|
112
|
-
</Card>
|
|
113
|
-
</div>
|
|
114
|
-
</div>
|
|
115
|
-
);
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
export function StatsSkeleton() {
|
|
119
|
-
return (
|
|
120
|
-
<div className="space-y-6">
|
|
121
|
-
<div className="space-y-2">
|
|
122
|
-
<Skeleton className="h-5 w-24" />
|
|
123
|
-
<Skeleton className="h-7 w-48" />
|
|
124
|
-
</div>
|
|
125
|
-
<div className="grid gap-4 md:grid-cols-2 lg:grid-cols-4">
|
|
126
|
-
{[...Array(4)].map((_, idx) => (
|
|
127
|
-
<Card key={idx}>
|
|
128
|
-
<CardHeader className="pb-2">
|
|
129
|
-
<Skeleton className="h-4 w-20" />
|
|
130
|
-
</CardHeader>
|
|
131
|
-
<CardContent className="space-y-2">
|
|
132
|
-
<Skeleton className="h-8 w-16" />
|
|
133
|
-
<Skeleton className="h-3 w-20" />
|
|
134
|
-
</CardContent>
|
|
135
|
-
</Card>
|
|
136
|
-
))}
|
|
137
|
-
</div>
|
|
138
|
-
<div className="grid gap-6 md:grid-cols-2">
|
|
139
|
-
{[...Array(2)].map((_, idx) => (
|
|
140
|
-
<Card key={idx}>
|
|
141
|
-
<CardHeader>
|
|
142
|
-
<Skeleton className="h-5 w-32" />
|
|
143
|
-
</CardHeader>
|
|
144
|
-
<CardContent>
|
|
145
|
-
<Skeleton className="h-[220px] w-full" />
|
|
146
|
-
</CardContent>
|
|
147
|
-
</Card>
|
|
148
|
-
))}
|
|
149
|
-
</div>
|
|
150
|
-
</div>
|
|
151
|
-
);
|
|
152
|
-
}
|
|
153
|
-
|
|
154
|
-
export function ContextPageSkeleton() {
|
|
155
|
-
return (
|
|
156
|
-
<div className="grid gap-6 lg:grid-cols-[320px_minmax(0,1fr)]">
|
|
157
|
-
<Card className="h-full">
|
|
158
|
-
<CardHeader className="space-y-2">
|
|
159
|
-
<Skeleton className="h-5 w-32" />
|
|
160
|
-
<Skeleton className="h-9 w-full" />
|
|
161
|
-
</CardHeader>
|
|
162
|
-
<CardContent className="space-y-3">
|
|
163
|
-
{[...Array(6)].map((_, idx) => (
|
|
164
|
-
<div key={idx} className="space-y-1">
|
|
165
|
-
<Skeleton className="h-4 w-3/4" />
|
|
166
|
-
<Skeleton className="h-3 w-1/2" />
|
|
167
|
-
</div>
|
|
168
|
-
))}
|
|
169
|
-
</CardContent>
|
|
170
|
-
</Card>
|
|
171
|
-
<Card>
|
|
172
|
-
<CardHeader>
|
|
173
|
-
<Skeleton className="h-6 w-40" />
|
|
174
|
-
</CardHeader>
|
|
175
|
-
<CardContent className="space-y-3">
|
|
176
|
-
<Skeleton className="h-4 w-24" />
|
|
177
|
-
<Skeleton className="h-4 w-full" />
|
|
178
|
-
<Skeleton className="h-4 w-full" />
|
|
179
|
-
<Skeleton className="h-4 w-5/6" />
|
|
180
|
-
</CardContent>
|
|
181
|
-
</Card>
|
|
182
|
-
</div>
|
|
183
|
-
);
|
|
184
|
-
}
|
|
@@ -1,129 +0,0 @@
|
|
|
1
|
-
import { Calendar, GitBranch, Tag, User, ExternalLink } from 'lucide-react';
|
|
2
|
-
import { Card, CardContent } from '@leanspec/ui-components';
|
|
3
|
-
import { StatusBadge } from '../StatusBadge';
|
|
4
|
-
import { PriorityBadge } from '../PriorityBadge';
|
|
5
|
-
import { StatusEditor } from '../metadata-editors/StatusEditor';
|
|
6
|
-
import { PriorityEditor } from '../metadata-editors/PriorityEditor';
|
|
7
|
-
import { TagsEditor } from '../metadata-editors/TagsEditor';
|
|
8
|
-
import { formatDate, formatRelativeTime } from '../../lib/date-utils';
|
|
9
|
-
import type { SpecDetail } from '../../types/api';
|
|
10
|
-
import { useTranslation } from 'react-i18next';
|
|
11
|
-
|
|
12
|
-
interface EditableMetadataProps {
|
|
13
|
-
spec: SpecDetail;
|
|
14
|
-
onSpecChange?: (updates: Partial<SpecDetail>) => void;
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
export function EditableMetadata({ spec, onSpecChange }: EditableMetadataProps) {
|
|
18
|
-
const created = (spec.metadata?.created_at as string | undefined) || spec.createdAt;
|
|
19
|
-
const updated = (spec.metadata?.updated_at as string | undefined) || spec.updatedAt;
|
|
20
|
-
const githubUrl = spec.metadata?.github_url as string | undefined;
|
|
21
|
-
const assignee = spec.metadata?.assignee as string | undefined;
|
|
22
|
-
const { t, i18n } = useTranslation('common');
|
|
23
|
-
|
|
24
|
-
return (
|
|
25
|
-
<Card>
|
|
26
|
-
<CardContent className="pt-6 space-y-4">
|
|
27
|
-
<dl className="grid grid-cols-2 gap-4">
|
|
28
|
-
<div>
|
|
29
|
-
<dt className="text-sm font-medium text-muted-foreground mb-1">{t('specsPage.filters.status')}</dt>
|
|
30
|
-
<dd className="flex items-center gap-2">
|
|
31
|
-
{spec.status && <StatusBadge status={spec.status} />}
|
|
32
|
-
<StatusEditor
|
|
33
|
-
specName={spec.specName}
|
|
34
|
-
value={spec.status}
|
|
35
|
-
onChange={(status) => onSpecChange?.({ status })}
|
|
36
|
-
/>
|
|
37
|
-
</dd>
|
|
38
|
-
</div>
|
|
39
|
-
|
|
40
|
-
<div>
|
|
41
|
-
<dt className="text-sm font-medium text-muted-foreground mb-1">{t('specsPage.filters.priority')}</dt>
|
|
42
|
-
<dd className="flex items-center gap-2">
|
|
43
|
-
{spec.priority && <PriorityBadge priority={spec.priority} />}
|
|
44
|
-
<PriorityEditor
|
|
45
|
-
specName={spec.specName}
|
|
46
|
-
value={spec.priority}
|
|
47
|
-
onChange={(priority) => onSpecChange?.({ priority })}
|
|
48
|
-
/>
|
|
49
|
-
</dd>
|
|
50
|
-
</div>
|
|
51
|
-
|
|
52
|
-
<div>
|
|
53
|
-
<dt className="text-sm font-medium text-muted-foreground flex items-center gap-2 mb-1">
|
|
54
|
-
<Calendar className="h-4 w-4" />
|
|
55
|
-
{t('specDetail.metadata.created')}
|
|
56
|
-
</dt>
|
|
57
|
-
<dd className="text-sm">
|
|
58
|
-
{formatDate(created, i18n.language)}
|
|
59
|
-
{created && (
|
|
60
|
-
<span className="text-muted-foreground ml-1">
|
|
61
|
-
({formatRelativeTime(created, i18n.language)})
|
|
62
|
-
</span>
|
|
63
|
-
)}
|
|
64
|
-
</dd>
|
|
65
|
-
</div>
|
|
66
|
-
|
|
67
|
-
<div>
|
|
68
|
-
<dt className="text-sm font-medium text-muted-foreground flex items-center gap-2 mb-1">
|
|
69
|
-
<Calendar className="h-4 w-4" />
|
|
70
|
-
{t('specDetail.metadata.updated')}
|
|
71
|
-
</dt>
|
|
72
|
-
<dd className="text-sm">
|
|
73
|
-
{formatDate(updated, i18n.language)}
|
|
74
|
-
{updated && (
|
|
75
|
-
<span className="text-muted-foreground ml-1">
|
|
76
|
-
({formatRelativeTime(updated, i18n.language)})
|
|
77
|
-
</span>
|
|
78
|
-
)}
|
|
79
|
-
</dd>
|
|
80
|
-
</div>
|
|
81
|
-
|
|
82
|
-
{assignee && (
|
|
83
|
-
<div>
|
|
84
|
-
<dt className="text-sm font-medium text-muted-foreground flex items-center gap-2 mb-1">
|
|
85
|
-
<User className="h-4 w-4" />
|
|
86
|
-
{t('specDetail.metadata.assignee')}
|
|
87
|
-
</dt>
|
|
88
|
-
<dd className="text-sm">{assignee}</dd>
|
|
89
|
-
</div>
|
|
90
|
-
)}
|
|
91
|
-
|
|
92
|
-
<div className={assignee ? '' : 'col-span-2'}>
|
|
93
|
-
<dt className="text-sm font-medium text-muted-foreground flex items-center gap-2 mb-1">
|
|
94
|
-
<Tag className="h-4 w-4" />
|
|
95
|
-
{t('spec.tags')}
|
|
96
|
-
</dt>
|
|
97
|
-
<dd>
|
|
98
|
-
<TagsEditor
|
|
99
|
-
specName={spec.specName}
|
|
100
|
-
value={spec.tags}
|
|
101
|
-
onChange={(tags) => onSpecChange?.({ tags })}
|
|
102
|
-
/>
|
|
103
|
-
</dd>
|
|
104
|
-
</div>
|
|
105
|
-
|
|
106
|
-
{githubUrl && (
|
|
107
|
-
<div className="col-span-2">
|
|
108
|
-
<dt className="text-sm font-medium text-muted-foreground flex items-center gap-2 mb-1">
|
|
109
|
-
<GitBranch className="h-4 w-4" />
|
|
110
|
-
{t('specDetail.metadata.source')}
|
|
111
|
-
</dt>
|
|
112
|
-
<dd>
|
|
113
|
-
<a
|
|
114
|
-
href={githubUrl}
|
|
115
|
-
target="_blank"
|
|
116
|
-
rel="noreferrer"
|
|
117
|
-
className="text-sm text-primary hover:underline inline-flex items-center gap-1"
|
|
118
|
-
>
|
|
119
|
-
{t('specDetail.metadata.viewOnGitHub')}
|
|
120
|
-
<ExternalLink className="h-3.5 w-3.5" />
|
|
121
|
-
</a>
|
|
122
|
-
</dd>
|
|
123
|
-
</div>
|
|
124
|
-
)}
|
|
125
|
-
</dl>
|
|
126
|
-
</CardContent>
|
|
127
|
-
</Card>
|
|
128
|
-
);
|
|
129
|
-
}
|