@leanspec/ui 0.2.14 → 0.2.15
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bin/leanspec-ui.js +126 -0
- package/dist/assets/_baseUniq-B6x_7o5y.js +1 -0
- package/dist/assets/arc-DZ27bDb2.js +1 -0
- package/dist/assets/architectureDiagram-VXUJARFQ-VTQAQir-.js +36 -0
- package/dist/assets/blockDiagram-VD42YOAC-BeZAaaB1.js +122 -0
- package/dist/assets/c4Diagram-YG6GDRKO-BnT3bg74.js +10 -0
- package/dist/assets/channel-BSVY_tOy.js +1 -0
- package/dist/assets/chunk-4BX2VUAB-qtS73lje.js +1 -0
- package/dist/assets/chunk-55IACEB6-B41Ne73X.js +1 -0
- package/dist/assets/chunk-B4BG7PRW-CRL0j0p8.js +165 -0
- package/dist/assets/chunk-DI55MBZ5-BRa_G3mf.js +220 -0
- package/dist/assets/chunk-FMBD7UC4-D_AT_wL5.js +15 -0
- package/dist/assets/chunk-QN33PNHL-Q1Nos5j_.js +1 -0
- package/dist/assets/chunk-QZHKN3VN-DflSXVVh.js +1 -0
- package/dist/assets/chunk-TZMSLE5B-B0OC-s8d.js +1 -0
- package/dist/assets/classDiagram-2ON5EDUG-Dn0xX9IG.js +1 -0
- package/dist/assets/classDiagram-v2-WZHVMYZB-Dn0xX9IG.js +1 -0
- package/dist/assets/clone-C-KMhWbr.js +1 -0
- package/dist/assets/core-DV6XEvTN.js +1 -0
- package/dist/assets/cose-bilkent-S5V4N54A-CboCNDKn.js +1 -0
- package/dist/assets/cytoscape.esm-5J0xJHOV.js +321 -0
- package/dist/assets/dagre-6UL2VRFP-DOonQ6kf.js +4 -0
- package/dist/assets/diagram-PSM6KHXK-DPYPbSse.js +24 -0
- package/dist/assets/diagram-QEK2KX5R-DfTIvQXt.js +43 -0
- package/dist/assets/diagram-S2PKOQOG-Dl0bD_cb.js +24 -0
- package/dist/assets/erDiagram-Q2GNP2WA-C36i3Lze.js +60 -0
- package/dist/assets/flowDiagram-NV44I4VS-BskiGL1V.js +162 -0
- package/dist/assets/ganttDiagram-JELNMOA3-BvEghcko.js +267 -0
- package/dist/assets/gitGraphDiagram-NY62KEGX-BEkcYMS3.js +65 -0
- package/dist/assets/graph-DfQs0Ukg.js +1 -0
- package/dist/assets/index-BQDji5Db.js +389 -0
- package/dist/assets/index-BaBk6Eb5.css +1 -0
- package/dist/assets/infoDiagram-WHAUD3N6-BNQZZTcd.js +2 -0
- package/dist/assets/journeyDiagram-XKPGCS4Q-BmcOKIu0.js +139 -0
- package/dist/assets/kanban-definition-3W4ZIXB7-etkUgKbz.js +89 -0
- package/dist/assets/katex-XbL3y5x-.js +261 -0
- package/dist/assets/layout-CyPK9cFq.js +1 -0
- package/dist/assets/min-D1_JVZu9.js +1 -0
- package/dist/assets/mindmap-definition-VGOIOE7T-D-3bnFXY.js +68 -0
- package/dist/assets/pieDiagram-ADFJNKIX-SSpBbb1Z.js +30 -0
- package/dist/assets/quadrantDiagram-AYHSOK5B-kCW_e4Rj.js +7 -0
- package/dist/assets/requirementDiagram-UZGBJVZJ-B-hRBRHn.js +64 -0
- package/dist/assets/sankeyDiagram-TZEHDZUN-Bq18cS4Z.js +10 -0
- package/dist/assets/sequenceDiagram-WL72ISMW-D6dOwWak.js +145 -0
- package/dist/assets/stateDiagram-FKZM4ZOC-DRnWZawn.js +1 -0
- package/dist/assets/stateDiagram-v2-4FDKWEC3-ortqHAq8.js +1 -0
- package/dist/assets/timeline-definition-IT6M3QCI-DLIDeF--.js +61 -0
- package/dist/assets/treemap-KMMF4GRG-D5oyLJbR.js +128 -0
- package/dist/assets/xychartDiagram-PRI3JC2R-B_qUVnv4.js +7 -0
- package/{index.html → dist/index.html} +2 -1
- package/package.json +10 -1
- package/eslint.config.js +0 -23
- package/package.json.backup +0 -83
- 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,47 +0,0 @@
|
|
|
1
|
-
import ReactMarkdown from 'react-markdown';
|
|
2
|
-
import remarkGfm from 'remark-gfm';
|
|
3
|
-
import rehypeSlug from 'rehype-slug';
|
|
4
|
-
import { MermaidDiagram } from '../MermaidDiagram';
|
|
5
|
-
import type { ComponentPropsWithoutRef } from 'react';
|
|
6
|
-
import type { Components } from 'react-markdown';
|
|
7
|
-
|
|
8
|
-
function useMarkdownComponents(): Components {
|
|
9
|
-
return {
|
|
10
|
-
code({ className, children, ...props }: ComponentPropsWithoutRef<'code'>) {
|
|
11
|
-
const inline = !className?.includes('language-');
|
|
12
|
-
const match = /language-(\w+)/.exec(className || '');
|
|
13
|
-
const language = match ? match[1] : null;
|
|
14
|
-
const code = String(children).replace(/\n$/, '');
|
|
15
|
-
|
|
16
|
-
if (!inline && language === 'mermaid') {
|
|
17
|
-
return <MermaidDiagram chart={code} className="my-4" />;
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
return (
|
|
21
|
-
<code className={className} {...props}>
|
|
22
|
-
{children}
|
|
23
|
-
</code>
|
|
24
|
-
);
|
|
25
|
-
},
|
|
26
|
-
};
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
interface MarkdownRendererProps {
|
|
30
|
-
content: string;
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
export function MarkdownRenderer({ content }: MarkdownRendererProps) {
|
|
34
|
-
const markdownComponents = useMarkdownComponents();
|
|
35
|
-
|
|
36
|
-
return (
|
|
37
|
-
<article className="prose prose-sm sm:prose-base dark:prose-invert max-w-none">
|
|
38
|
-
<ReactMarkdown
|
|
39
|
-
remarkPlugins={[remarkGfm]}
|
|
40
|
-
rehypePlugins={[rehypeSlug]}
|
|
41
|
-
components={markdownComponents}
|
|
42
|
-
>
|
|
43
|
-
{content}
|
|
44
|
-
</ReactMarkdown>
|
|
45
|
-
</article>
|
|
46
|
-
);
|
|
47
|
-
}
|
|
@@ -1,150 +0,0 @@
|
|
|
1
|
-
import { useMemo, useState } from 'react';
|
|
2
|
-
import { List } from 'lucide-react';
|
|
3
|
-
import {
|
|
4
|
-
Button,
|
|
5
|
-
Dialog,
|
|
6
|
-
DialogContent,
|
|
7
|
-
DialogHeader,
|
|
8
|
-
DialogTitle,
|
|
9
|
-
cn,
|
|
10
|
-
} from '@leanspec/ui-components';
|
|
11
|
-
import { useTranslation } from 'react-i18next';
|
|
12
|
-
import GithubSlugger from 'github-slugger';
|
|
13
|
-
|
|
14
|
-
interface TOCItem {
|
|
15
|
-
id: string;
|
|
16
|
-
text: string;
|
|
17
|
-
level: number;
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
/**
|
|
21
|
-
* Extract headings from markdown content
|
|
22
|
-
*/
|
|
23
|
-
function extractHeadings(markdown: string): TOCItem[] {
|
|
24
|
-
if (!markdown) return [];
|
|
25
|
-
|
|
26
|
-
const headings: TOCItem[] = [];
|
|
27
|
-
const lines = markdown.split('\n');
|
|
28
|
-
let inCodeBlock = false;
|
|
29
|
-
const slugger = new GithubSlugger();
|
|
30
|
-
|
|
31
|
-
for (const line of lines) {
|
|
32
|
-
// Track code blocks
|
|
33
|
-
if (line.trim().startsWith('```')) {
|
|
34
|
-
inCodeBlock = !inCodeBlock;
|
|
35
|
-
continue;
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
// Skip lines inside code blocks
|
|
39
|
-
if (inCodeBlock) continue;
|
|
40
|
-
|
|
41
|
-
// Match headings (## Heading or ### Heading, skip # H1)
|
|
42
|
-
const match = line.match(/^(#{2,6})\s+(.+)$/);
|
|
43
|
-
if (match) {
|
|
44
|
-
const level = match[1].length;
|
|
45
|
-
const text = match[2].trim();
|
|
46
|
-
const id = slugger.slug(text);
|
|
47
|
-
|
|
48
|
-
headings.push({ id, text, level });
|
|
49
|
-
}
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
return headings;
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
function scrollToHeading(id: string) {
|
|
56
|
-
const element = document.getElementById(id);
|
|
57
|
-
if (element) {
|
|
58
|
-
element.scrollIntoView({ behavior: 'smooth', block: 'start' });
|
|
59
|
-
|
|
60
|
-
if (window.history.replaceState) {
|
|
61
|
-
window.history.replaceState(null, '', `#${id}`);
|
|
62
|
-
} else {
|
|
63
|
-
window.location.hash = id;
|
|
64
|
-
}
|
|
65
|
-
}
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
interface TOCListProps {
|
|
69
|
-
headings: TOCItem[];
|
|
70
|
-
onHeadingClick: (id: string) => void;
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
function TOCList({ headings, onHeadingClick }: TOCListProps) {
|
|
74
|
-
return (
|
|
75
|
-
<nav className="space-y-1">
|
|
76
|
-
{headings.map((heading, index) => (
|
|
77
|
-
<button
|
|
78
|
-
key={`${heading.id}-${index}`}
|
|
79
|
-
onClick={() => onHeadingClick(heading.id)}
|
|
80
|
-
className={cn(
|
|
81
|
-
'w-full text-left px-2 py-1.5 text-sm rounded-md hover:bg-muted transition-colors flex items-start gap-2 group text-muted-foreground hover:text-foreground',
|
|
82
|
-
heading.level === 2 && 'font-medium text-foreground',
|
|
83
|
-
heading.level === 3 && 'pl-6',
|
|
84
|
-
heading.level === 4 && 'pl-10',
|
|
85
|
-
heading.level === 5 && 'pl-14',
|
|
86
|
-
heading.level === 6 && 'pl-18'
|
|
87
|
-
)}
|
|
88
|
-
>
|
|
89
|
-
<span className="flex-1 truncate">{heading.text}</span>
|
|
90
|
-
</button>
|
|
91
|
-
))}
|
|
92
|
-
</nav>
|
|
93
|
-
);
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
interface TableOfContentsProps {
|
|
97
|
-
content: string;
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
export function TableOfContentsSidebar({ content }: TableOfContentsProps) {
|
|
101
|
-
const { t } = useTranslation('common');
|
|
102
|
-
const headings = useMemo(() => extractHeadings(content), [content]);
|
|
103
|
-
if (headings.length === 0) return null;
|
|
104
|
-
|
|
105
|
-
return (
|
|
106
|
-
<div className="py-2">
|
|
107
|
-
<h4 className="mb-4 text-sm font-semibold leading-none tracking-tight px-2">
|
|
108
|
-
{t('tableOfContents.onThisPage')}
|
|
109
|
-
</h4>
|
|
110
|
-
<TOCList headings={headings} onHeadingClick={scrollToHeading} />
|
|
111
|
-
</div>
|
|
112
|
-
);
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
export function TableOfContents({ content }: TableOfContentsProps) {
|
|
116
|
-
const { t } = useTranslation('common');
|
|
117
|
-
const [open, setOpen] = useState(false);
|
|
118
|
-
const headings = useMemo(() => extractHeadings(content), [content]);
|
|
119
|
-
|
|
120
|
-
if (headings.length === 0) return null;
|
|
121
|
-
|
|
122
|
-
const handleHeadingClick = (id: string) => {
|
|
123
|
-
setOpen(false);
|
|
124
|
-
// Small delay to allow dialog to close before scrolling
|
|
125
|
-
setTimeout(() => {
|
|
126
|
-
scrollToHeading(id);
|
|
127
|
-
}, 100);
|
|
128
|
-
};
|
|
129
|
-
|
|
130
|
-
return (
|
|
131
|
-
<Dialog open={open} onOpenChange={setOpen}>
|
|
132
|
-
<Button
|
|
133
|
-
size="icon"
|
|
134
|
-
aria-haspopup="dialog"
|
|
135
|
-
aria-expanded={open}
|
|
136
|
-
onClick={() => setOpen(true)}
|
|
137
|
-
className="fixed bottom-24 right-6 h-12 w-12 rounded-full shadow-lg z-40 hover:scale-110 transition-transform"
|
|
138
|
-
aria-label={t('tableOfContents.open')}
|
|
139
|
-
>
|
|
140
|
-
<List className="h-5 w-5" />
|
|
141
|
-
</Button>
|
|
142
|
-
<DialogContent className="max-w-md max-h-[80vh] overflow-y-auto">
|
|
143
|
-
<DialogHeader>
|
|
144
|
-
<DialogTitle>{t('tableOfContents.title')}</DialogTitle>
|
|
145
|
-
</DialogHeader>
|
|
146
|
-
<TOCList headings={headings} onHeadingClick={handleHeadingClick} />
|
|
147
|
-
</DialogContent>
|
|
148
|
-
</Dialog>
|
|
149
|
-
);
|
|
150
|
-
}
|
|
@@ -1,204 +0,0 @@
|
|
|
1
|
-
import { useState, useMemo, type DragEvent } from 'react';
|
|
2
|
-
import { Link } from 'react-router-dom';
|
|
3
|
-
import { Clock, PlayCircle, CheckCircle2, Archive } from 'lucide-react';
|
|
4
|
-
import type { Spec } from '../../types/api';
|
|
5
|
-
import { PriorityBadge } from '../PriorityBadge';
|
|
6
|
-
import { cn } from '@leanspec/ui-components';
|
|
7
|
-
import { useTranslation } from 'react-i18next';
|
|
8
|
-
|
|
9
|
-
type SpecStatus = 'planned' | 'in-progress' | 'complete' | 'archived';
|
|
10
|
-
|
|
11
|
-
interface BoardViewProps {
|
|
12
|
-
specs: Spec[];
|
|
13
|
-
onStatusChange: (spec: Spec, status: SpecStatus) => void;
|
|
14
|
-
basePath?: string;
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
const STATUS_CONFIG: Record<SpecStatus, {
|
|
18
|
-
icon: typeof Clock;
|
|
19
|
-
titleKey: `status.${string}`;
|
|
20
|
-
colorClass: string;
|
|
21
|
-
bgClass: string;
|
|
22
|
-
borderClass: string;
|
|
23
|
-
}> = {
|
|
24
|
-
'planned': {
|
|
25
|
-
icon: Clock,
|
|
26
|
-
titleKey: 'status.planned',
|
|
27
|
-
colorClass: 'text-blue-600 dark:text-blue-400',
|
|
28
|
-
bgClass: 'bg-blue-50 dark:bg-blue-900/20',
|
|
29
|
-
borderClass: 'border-blue-200 dark:border-blue-800'
|
|
30
|
-
},
|
|
31
|
-
'in-progress': {
|
|
32
|
-
icon: PlayCircle,
|
|
33
|
-
titleKey: 'status.inProgress',
|
|
34
|
-
colorClass: 'text-orange-600 dark:text-orange-400',
|
|
35
|
-
bgClass: 'bg-orange-50 dark:bg-orange-900/20',
|
|
36
|
-
borderClass: 'border-orange-200 dark:border-orange-800'
|
|
37
|
-
},
|
|
38
|
-
'complete': {
|
|
39
|
-
icon: CheckCircle2,
|
|
40
|
-
titleKey: 'status.complete',
|
|
41
|
-
colorClass: 'text-green-600 dark:text-green-400',
|
|
42
|
-
bgClass: 'bg-green-50 dark:bg-green-900/20',
|
|
43
|
-
borderClass: 'border-green-200 dark:border-green-800'
|
|
44
|
-
},
|
|
45
|
-
'archived': {
|
|
46
|
-
icon: Archive,
|
|
47
|
-
titleKey: 'status.archived',
|
|
48
|
-
colorClass: 'text-gray-600 dark:text-gray-400',
|
|
49
|
-
bgClass: 'bg-gray-50 dark:bg-gray-900/20',
|
|
50
|
-
borderClass: 'border-gray-200 dark:border-gray-800'
|
|
51
|
-
}
|
|
52
|
-
};
|
|
53
|
-
|
|
54
|
-
export function BoardView({ specs, onStatusChange, basePath = '/projects/default' }: BoardViewProps) {
|
|
55
|
-
const [draggingId, setDraggingId] = useState<string | null>(null);
|
|
56
|
-
const [activeDropZone, setActiveDropZone] = useState<SpecStatus | null>(null);
|
|
57
|
-
const { t } = useTranslation('common');
|
|
58
|
-
|
|
59
|
-
const columns = useMemo(() => {
|
|
60
|
-
const cols: SpecStatus[] = ['planned', 'in-progress', 'complete'];
|
|
61
|
-
return cols;
|
|
62
|
-
}, []);
|
|
63
|
-
|
|
64
|
-
const specsByStatus = useMemo(() => {
|
|
65
|
-
const grouped: Record<SpecStatus, Spec[]> = {
|
|
66
|
-
'planned': [],
|
|
67
|
-
'in-progress': [],
|
|
68
|
-
'complete': [],
|
|
69
|
-
'archived': []
|
|
70
|
-
};
|
|
71
|
-
|
|
72
|
-
specs.forEach((spec) => {
|
|
73
|
-
const status = spec.status as SpecStatus | null;
|
|
74
|
-
if (!status) return;
|
|
75
|
-
grouped[status].push(spec);
|
|
76
|
-
});
|
|
77
|
-
|
|
78
|
-
return grouped;
|
|
79
|
-
}, [specs]);
|
|
80
|
-
|
|
81
|
-
const handleDragStart = (spec: Spec, e: DragEvent<HTMLDivElement>) => {
|
|
82
|
-
setDraggingId(spec.specName);
|
|
83
|
-
e.dataTransfer.effectAllowed = 'move';
|
|
84
|
-
// Set drag image or data if needed
|
|
85
|
-
};
|
|
86
|
-
|
|
87
|
-
const handleDragOver = (status: SpecStatus, e: DragEvent<HTMLDivElement>) => {
|
|
88
|
-
e.preventDefault();
|
|
89
|
-
e.dataTransfer.dropEffect = 'move';
|
|
90
|
-
if (activeDropZone !== status) {
|
|
91
|
-
setActiveDropZone(status);
|
|
92
|
-
}
|
|
93
|
-
};
|
|
94
|
-
|
|
95
|
-
const handleDrop = (status: SpecStatus, e: DragEvent<HTMLDivElement>) => {
|
|
96
|
-
e.preventDefault();
|
|
97
|
-
setActiveDropZone(null);
|
|
98
|
-
|
|
99
|
-
if (draggingId) {
|
|
100
|
-
const spec = specs.find(s => s.specName === draggingId);
|
|
101
|
-
if (spec && spec.status !== status) {
|
|
102
|
-
onStatusChange(spec, status);
|
|
103
|
-
}
|
|
104
|
-
setDraggingId(null);
|
|
105
|
-
}
|
|
106
|
-
};
|
|
107
|
-
|
|
108
|
-
return (
|
|
109
|
-
<div className="flex flex-col md:flex-row gap-3 sm:gap-4 md:gap-6 h-full pb-2 md:snap-x md:snap-mandatory overflow-y-auto md:overflow-y-hidden md:overflow-x-auto">
|
|
110
|
-
{columns.map(status => {
|
|
111
|
-
const config = STATUS_CONFIG[status];
|
|
112
|
-
const statusSpecs = specsByStatus[status] || [];
|
|
113
|
-
const Icon = config.icon;
|
|
114
|
-
const isDropActive = activeDropZone === status;
|
|
115
|
-
|
|
116
|
-
return (
|
|
117
|
-
<div
|
|
118
|
-
key={status}
|
|
119
|
-
className={cn(
|
|
120
|
-
"flex-shrink-0 w-80 flex flex-col rounded-lg bg-secondary/30 border border-transparent transition-colors",
|
|
121
|
-
isDropActive && "bg-secondary/60 border-primary/50 ring-2 ring-primary/20"
|
|
122
|
-
)}
|
|
123
|
-
onDragOver={(e) => handleDragOver(status, e)}
|
|
124
|
-
onDrop={(e) => handleDrop(status, e)}
|
|
125
|
-
>
|
|
126
|
-
{/* Column Header */}
|
|
127
|
-
<div className={cn(
|
|
128
|
-
"p-3 flex items-center justify-between border-b sticky top-0 z-5",
|
|
129
|
-
config.borderClass,
|
|
130
|
-
config.bgClass,
|
|
131
|
-
"rounded-t-lg"
|
|
132
|
-
)}>
|
|
133
|
-
<div className="flex items-center gap-2">
|
|
134
|
-
<Icon className={cn("w-4 h-4", config.colorClass)} />
|
|
135
|
-
<span className={cn("font-medium text-sm", config.colorClass)}>
|
|
136
|
-
{t(config.titleKey)}
|
|
137
|
-
</span>
|
|
138
|
-
<span className="text-xs px-2 py-0.5 bg-background/50 rounded-full text-muted-foreground">
|
|
139
|
-
{statusSpecs.length}
|
|
140
|
-
</span>
|
|
141
|
-
</div>
|
|
142
|
-
</div>
|
|
143
|
-
|
|
144
|
-
{/* Column Content */}
|
|
145
|
-
<div className="flex-1 p-2 overflow-y-auto">
|
|
146
|
-
<div className="space-y-2">
|
|
147
|
-
{statusSpecs.map(spec => (
|
|
148
|
-
<div
|
|
149
|
-
key={spec.specName}
|
|
150
|
-
draggable
|
|
151
|
-
onDragStart={(e) => handleDragStart(spec, e)}
|
|
152
|
-
className={cn(
|
|
153
|
-
"bg-background p-4 rounded-xl border shadow-sm cursor-move hover:border-primary/50 transition-all group/card",
|
|
154
|
-
draggingId === spec.specName && "opacity-50"
|
|
155
|
-
)}
|
|
156
|
-
>
|
|
157
|
-
<Link to={`${basePath}/specs/${spec.specName}`} className="select-none h-full flex flex-col">
|
|
158
|
-
{/* Top: #ID */}
|
|
159
|
-
<div className="text-xs text-muted-foreground font-mono mb-1">
|
|
160
|
-
#{spec.specNumber || spec.specName.split('-')[0].replace(/^0+/, '')}
|
|
161
|
-
</div>
|
|
162
|
-
|
|
163
|
-
{/* Middle: Title & Filename */}
|
|
164
|
-
<div className="space-y-1.5 mb-4 flex-1">
|
|
165
|
-
<h4 className="font-semibold text-base leading-snug group-hover/card:text-primary transition-colors">
|
|
166
|
-
{spec.title || spec.specName}
|
|
167
|
-
</h4>
|
|
168
|
-
<div className="text-xs text-muted-foreground font-mono truncate">
|
|
169
|
-
{spec.specName}
|
|
170
|
-
</div>
|
|
171
|
-
</div>
|
|
172
|
-
|
|
173
|
-
{/* Bottom: Priority & Tags */}
|
|
174
|
-
<div className="flex items-center justify-between gap-2 mt-auto">
|
|
175
|
-
{spec.priority && (
|
|
176
|
-
<PriorityBadge priority={spec.priority} className="h-6 px-2.5 rounded-md" />
|
|
177
|
-
)}
|
|
178
|
-
|
|
179
|
-
{spec.tags && spec.tags.length > 0 && (
|
|
180
|
-
<div className="flex flex-wrap gap-1.5 justify-end ml-auto">
|
|
181
|
-
{spec.tags.slice(0, 2).map((tag: string) => (
|
|
182
|
-
<span key={tag} className="text-[10px] px-2 py-0.5 bg-secondary/30 border border-border/50 rounded-md text-muted-foreground font-mono">
|
|
183
|
-
{tag}
|
|
184
|
-
</span>
|
|
185
|
-
))}
|
|
186
|
-
{spec.tags.length > 2 && (
|
|
187
|
-
<span className="text-[10px] px-2 py-0.5 bg-secondary/30 border border-border/50 rounded-md text-muted-foreground font-mono">
|
|
188
|
-
+{spec.tags.length - 2}
|
|
189
|
-
</span>
|
|
190
|
-
)}
|
|
191
|
-
</div>
|
|
192
|
-
)}
|
|
193
|
-
</div>
|
|
194
|
-
</Link>
|
|
195
|
-
</div>
|
|
196
|
-
))}
|
|
197
|
-
</div>
|
|
198
|
-
</div>
|
|
199
|
-
</div>
|
|
200
|
-
);
|
|
201
|
-
})}
|
|
202
|
-
</div>
|
|
203
|
-
);
|
|
204
|
-
}
|
|
@@ -1,62 +0,0 @@
|
|
|
1
|
-
import { Link } from 'react-router-dom';
|
|
2
|
-
import type { Spec } from '../../types/api';
|
|
3
|
-
import { StatusBadge } from '../StatusBadge';
|
|
4
|
-
import { PriorityBadge } from '../PriorityBadge';
|
|
5
|
-
import { useTranslation } from 'react-i18next';
|
|
6
|
-
|
|
7
|
-
interface ListViewProps {
|
|
8
|
-
specs: Spec[];
|
|
9
|
-
basePath?: string;
|
|
10
|
-
}
|
|
11
|
-
|
|
12
|
-
export function ListView({ specs, basePath = '/projects/default' }: ListViewProps) {
|
|
13
|
-
const { t } = useTranslation('common');
|
|
14
|
-
|
|
15
|
-
if (specs.length === 0) {
|
|
16
|
-
return (
|
|
17
|
-
<div className="text-center py-12 text-muted-foreground border rounded-lg bg-secondary/10">
|
|
18
|
-
{t('specsPage.list.empty')}
|
|
19
|
-
</div>
|
|
20
|
-
);
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
return (
|
|
24
|
-
<div className="h-full overflow-y-auto space-y-2">
|
|
25
|
-
{specs.map((spec) => (
|
|
26
|
-
<Link
|
|
27
|
-
key={spec.specName}
|
|
28
|
-
to={`${basePath}/specs/${spec.specName}`}
|
|
29
|
-
className="block p-4 border rounded-lg hover:bg-secondary/50 transition-colors bg-background"
|
|
30
|
-
>
|
|
31
|
-
<div className="flex items-start justify-between gap-4">
|
|
32
|
-
<div className="flex-1 min-w-0">
|
|
33
|
-
<div className="flex items-center gap-2 mb-1">
|
|
34
|
-
<span className="text-xs font-mono text-muted-foreground bg-secondary px-1.5 py-0.5 rounded">
|
|
35
|
-
{spec.specName.split('-')[0]}
|
|
36
|
-
</span>
|
|
37
|
-
<h3 className="font-medium truncate">{spec.title}</h3>
|
|
38
|
-
</div>
|
|
39
|
-
<p className="text-sm text-muted-foreground truncate">{spec.specName}</p>
|
|
40
|
-
</div>
|
|
41
|
-
<div className="flex gap-2 items-center flex-shrink-0">
|
|
42
|
-
{spec.status && <StatusBadge status={spec.status} />}
|
|
43
|
-
{spec.priority && <PriorityBadge priority={spec.priority} />}
|
|
44
|
-
</div>
|
|
45
|
-
</div>
|
|
46
|
-
{spec.tags && spec.tags.length > 0 && (
|
|
47
|
-
<div className="flex gap-2 mt-3 flex-wrap">
|
|
48
|
-
{spec.tags.map((tag: string) => (
|
|
49
|
-
<span
|
|
50
|
-
key={tag}
|
|
51
|
-
className="text-xs px-2 py-0.5 bg-secondary rounded text-secondary-foreground"
|
|
52
|
-
>
|
|
53
|
-
{tag}
|
|
54
|
-
</span>
|
|
55
|
-
))}
|
|
56
|
-
</div>
|
|
57
|
-
)}
|
|
58
|
-
</Link>
|
|
59
|
-
))}
|
|
60
|
-
</div>
|
|
61
|
-
);
|
|
62
|
-
}
|
|
@@ -1,190 +0,0 @@
|
|
|
1
|
-
import { Search, Filter, X, Clock, PlayCircle, CheckCircle2, Archive, AlertCircle, ArrowUp, Minus, ArrowDown } from 'lucide-react';
|
|
2
|
-
import {
|
|
3
|
-
Select,
|
|
4
|
-
SelectContent,
|
|
5
|
-
SelectItem,
|
|
6
|
-
SelectTrigger,
|
|
7
|
-
SelectValue,
|
|
8
|
-
Input,
|
|
9
|
-
Button,
|
|
10
|
-
} from '@leanspec/ui-components';
|
|
11
|
-
import { useTranslation } from 'react-i18next';
|
|
12
|
-
|
|
13
|
-
interface SpecsFiltersProps {
|
|
14
|
-
searchQuery: string;
|
|
15
|
-
onSearchChange: (query: string) => void;
|
|
16
|
-
statusFilter: string;
|
|
17
|
-
onStatusFilterChange: (status: string) => void;
|
|
18
|
-
priorityFilter: string;
|
|
19
|
-
onPriorityFilterChange: (priority: string) => void;
|
|
20
|
-
tagFilter: string;
|
|
21
|
-
onTagFilterChange: (tag: string) => void;
|
|
22
|
-
sortBy: string;
|
|
23
|
-
onSortByChange: (sort: string) => void;
|
|
24
|
-
uniqueStatuses: string[];
|
|
25
|
-
uniquePriorities: string[];
|
|
26
|
-
uniqueTags: string[];
|
|
27
|
-
onClearFilters: () => void;
|
|
28
|
-
totalSpecs: number;
|
|
29
|
-
filteredCount: number;
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
export function SpecsFilters({
|
|
33
|
-
searchQuery,
|
|
34
|
-
onSearchChange,
|
|
35
|
-
statusFilter,
|
|
36
|
-
onStatusFilterChange,
|
|
37
|
-
priorityFilter,
|
|
38
|
-
onPriorityFilterChange,
|
|
39
|
-
tagFilter,
|
|
40
|
-
onTagFilterChange,
|
|
41
|
-
sortBy,
|
|
42
|
-
onSortByChange,
|
|
43
|
-
uniqueStatuses,
|
|
44
|
-
uniquePriorities,
|
|
45
|
-
uniqueTags,
|
|
46
|
-
onClearFilters,
|
|
47
|
-
totalSpecs,
|
|
48
|
-
filteredCount,
|
|
49
|
-
}: SpecsFiltersProps) {
|
|
50
|
-
const { t } = useTranslation('common');
|
|
51
|
-
|
|
52
|
-
// Status icons mapping
|
|
53
|
-
const statusIcons: Record<string, React.ComponentType<{ className?: string }>> = {
|
|
54
|
-
planned: Clock,
|
|
55
|
-
'in-progress': PlayCircle,
|
|
56
|
-
complete: CheckCircle2,
|
|
57
|
-
archived: Archive,
|
|
58
|
-
};
|
|
59
|
-
|
|
60
|
-
// Priority icons mapping
|
|
61
|
-
const priorityIcons: Record<string, React.ComponentType<{ className?: string }>> = {
|
|
62
|
-
critical: AlertCircle,
|
|
63
|
-
high: ArrowUp,
|
|
64
|
-
medium: Minus,
|
|
65
|
-
low: ArrowDown,
|
|
66
|
-
};
|
|
67
|
-
|
|
68
|
-
const statusKeyMap: Record<string, `status.${string}`> = {
|
|
69
|
-
planned: 'status.planned',
|
|
70
|
-
'in-progress': 'status.inProgress',
|
|
71
|
-
complete: 'status.complete',
|
|
72
|
-
archived: 'status.archived',
|
|
73
|
-
};
|
|
74
|
-
const priorityKeyMap: Record<string, `priority.${string}`> = {
|
|
75
|
-
critical: 'priority.critical',
|
|
76
|
-
high: 'priority.high',
|
|
77
|
-
medium: 'priority.medium',
|
|
78
|
-
low: 'priority.low',
|
|
79
|
-
};
|
|
80
|
-
|
|
81
|
-
const formatStatus = (status: string) => statusKeyMap[status] ? t(statusKeyMap[status]) : status;
|
|
82
|
-
const formatPriority = (priority: string) => priorityKeyMap[priority] ? t(priorityKeyMap[priority]) : priority;
|
|
83
|
-
const hasActiveFilters = searchQuery || statusFilter !== 'all' || priorityFilter !== 'all' || tagFilter !== 'all';
|
|
84
|
-
|
|
85
|
-
return (
|
|
86
|
-
<div className="space-y-4">
|
|
87
|
-
{/* Search Bar */}
|
|
88
|
-
<div className="relative">
|
|
89
|
-
<Search className="absolute left-3 top-1/2 transform -translate-y-1/2 w-4 h-4 text-muted-foreground" />
|
|
90
|
-
<Input
|
|
91
|
-
type="text"
|
|
92
|
-
placeholder={t('specsPage.searchPlaceholder')}
|
|
93
|
-
value={searchQuery}
|
|
94
|
-
onChange={(e) => onSearchChange(e.target.value)}
|
|
95
|
-
className="w-full pl-10 pr-4 py-2"
|
|
96
|
-
/>
|
|
97
|
-
</div>
|
|
98
|
-
|
|
99
|
-
{/* Filters Row */}
|
|
100
|
-
<div className="flex flex-wrap gap-3 items-center justify-between">
|
|
101
|
-
<div className="flex flex-wrap gap-3 items-center">
|
|
102
|
-
<div className="flex items-center gap-2 text-muted-foreground">
|
|
103
|
-
<Filter className="w-4 h-4" />
|
|
104
|
-
<span className="text-sm font-medium">{t('specsNavSidebar.filtersLabel')}</span>
|
|
105
|
-
</div>
|
|
106
|
-
|
|
107
|
-
<Select value={statusFilter} onValueChange={onStatusFilterChange}>
|
|
108
|
-
<SelectTrigger className="w-[140px]">
|
|
109
|
-
<SelectValue placeholder={t('specsPage.filters.statusAll')} />
|
|
110
|
-
</SelectTrigger>
|
|
111
|
-
<SelectContent>
|
|
112
|
-
<SelectItem value="all">{t('specsPage.filters.statusAll')}</SelectItem>
|
|
113
|
-
{uniqueStatuses.map(status => {
|
|
114
|
-
const StatusIcon = statusIcons[status];
|
|
115
|
-
return (
|
|
116
|
-
<SelectItem key={status} value={status}>
|
|
117
|
-
<div className="flex items-center gap-2">
|
|
118
|
-
{StatusIcon && <StatusIcon className="h-4 w-4" />}
|
|
119
|
-
<span>{formatStatus(status)}</span>
|
|
120
|
-
</div>
|
|
121
|
-
</SelectItem>
|
|
122
|
-
);
|
|
123
|
-
})}
|
|
124
|
-
</SelectContent>
|
|
125
|
-
</Select>
|
|
126
|
-
|
|
127
|
-
<Select value={priorityFilter} onValueChange={onPriorityFilterChange}>
|
|
128
|
-
<SelectTrigger className="w-[140px]">
|
|
129
|
-
<SelectValue placeholder={t('specsPage.filters.priorityAll')} />
|
|
130
|
-
</SelectTrigger>
|
|
131
|
-
<SelectContent>
|
|
132
|
-
<SelectItem value="all">{t('specsPage.filters.priorityAll')}</SelectItem>
|
|
133
|
-
{uniquePriorities.map(priority => {
|
|
134
|
-
const PriorityIcon = priorityIcons[priority];
|
|
135
|
-
return (
|
|
136
|
-
<SelectItem key={priority} value={priority}>
|
|
137
|
-
<div className="flex items-center gap-2">
|
|
138
|
-
{PriorityIcon && <PriorityIcon className="h-4 w-4" />}
|
|
139
|
-
<span>{formatPriority(priority)}</span>
|
|
140
|
-
</div>
|
|
141
|
-
</SelectItem>
|
|
142
|
-
);
|
|
143
|
-
})}
|
|
144
|
-
</SelectContent>
|
|
145
|
-
</Select>
|
|
146
|
-
|
|
147
|
-
<Select value={tagFilter} onValueChange={onTagFilterChange}>
|
|
148
|
-
<SelectTrigger className="w-[140px]">
|
|
149
|
-
<SelectValue placeholder={t('specsNavSidebar.select.tag.all')} />
|
|
150
|
-
</SelectTrigger>
|
|
151
|
-
<SelectContent>
|
|
152
|
-
<SelectItem value="all">{t('specsNavSidebar.select.tag.all')}</SelectItem>
|
|
153
|
-
{uniqueTags.map(tag => (
|
|
154
|
-
<SelectItem key={tag} value={tag}>{tag}</SelectItem>
|
|
155
|
-
))}
|
|
156
|
-
</SelectContent>
|
|
157
|
-
</Select>
|
|
158
|
-
|
|
159
|
-
<Select value={sortBy} onValueChange={onSortByChange}>
|
|
160
|
-
<SelectTrigger className="w-[160px]">
|
|
161
|
-
<SelectValue placeholder={t('specsPage.filters.sort')} />
|
|
162
|
-
</SelectTrigger>
|
|
163
|
-
<SelectContent>
|
|
164
|
-
<SelectItem value="id-desc">{t('specsPage.filters.sortOptions.id-desc')}</SelectItem>
|
|
165
|
-
<SelectItem value="id-asc">{t('specsPage.filters.sortOptions.id-asc')}</SelectItem>
|
|
166
|
-
<SelectItem value="updated-desc">{t('specsPage.filters.sortOptions.updated-desc')}</SelectItem>
|
|
167
|
-
<SelectItem value="title-asc">{t('specsPage.filters.sortOptions.title-asc')}</SelectItem>
|
|
168
|
-
</SelectContent>
|
|
169
|
-
</Select>
|
|
170
|
-
|
|
171
|
-
{hasActiveFilters && (
|
|
172
|
-
<Button
|
|
173
|
-
onClick={onClearFilters}
|
|
174
|
-
variant="ghost"
|
|
175
|
-
size="sm"
|
|
176
|
-
className="h-8 gap-1"
|
|
177
|
-
>
|
|
178
|
-
<X className="w-3 h-3" />
|
|
179
|
-
{t('specsNavSidebar.clearFilters')}
|
|
180
|
-
</Button>
|
|
181
|
-
)}
|
|
182
|
-
</div>
|
|
183
|
-
|
|
184
|
-
<div className="text-sm text-muted-foreground">
|
|
185
|
-
{t('specsPage.filters.filteredCount', { filtered: filteredCount, total: totalSpecs })}
|
|
186
|
-
</div>
|
|
187
|
-
</div>
|
|
188
|
-
</div>
|
|
189
|
-
);
|
|
190
|
-
}
|