@leanspec/ui 0.2.14 → 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.
Files changed (150) hide show
  1. package/bin/leanspec-ui.js +191 -0
  2. package/dist/assets/_baseUniq-CRqreL7N.js +1 -0
  3. package/dist/assets/arc-DMhx9AJT.js +1 -0
  4. package/dist/assets/architectureDiagram-VXUJARFQ-DM0L0YzO.js +36 -0
  5. package/dist/assets/blockDiagram-VD42YOAC-DHQXDHsD.js +122 -0
  6. package/dist/assets/c4Diagram-YG6GDRKO-0L7o2gpH.js +10 -0
  7. package/dist/assets/channel-2tOl0nAZ.js +1 -0
  8. package/dist/assets/chunk-4BX2VUAB-CwFT-Uaj.js +1 -0
  9. package/dist/assets/chunk-55IACEB6-CjvuUHHG.js +1 -0
  10. package/dist/assets/chunk-B4BG7PRW-BRJBysMK.js +165 -0
  11. package/dist/assets/chunk-DI55MBZ5-BnNEeoaA.js +220 -0
  12. package/dist/assets/chunk-FMBD7UC4-BK2l30pm.js +15 -0
  13. package/dist/assets/chunk-QN33PNHL-BN_cZkCU.js +1 -0
  14. package/dist/assets/chunk-QZHKN3VN-Brc3Yrub.js +1 -0
  15. package/dist/assets/chunk-TZMSLE5B-D2zzpLfO.js +1 -0
  16. package/dist/assets/classDiagram-2ON5EDUG-BB9CSNmS.js +1 -0
  17. package/dist/assets/classDiagram-v2-WZHVMYZB-BB9CSNmS.js +1 -0
  18. package/dist/assets/clone-BjxVFtyI.js +1 -0
  19. package/dist/assets/core-DV6XEvTN.js +1 -0
  20. package/dist/assets/cose-bilkent-S5V4N54A-CLJgM3XR.js +1 -0
  21. package/dist/assets/cytoscape.esm-5J0xJHOV.js +321 -0
  22. package/dist/assets/dagre-6UL2VRFP-_IFvBJKJ.js +4 -0
  23. package/dist/assets/diagram-PSM6KHXK--83HIYSQ.js +24 -0
  24. package/dist/assets/diagram-QEK2KX5R-6jAWnCnZ.js +43 -0
  25. package/dist/assets/diagram-S2PKOQOG-D5pwHvjZ.js +24 -0
  26. package/dist/assets/erDiagram-Q2GNP2WA-B4FV3mTd.js +60 -0
  27. package/dist/assets/flowDiagram-NV44I4VS-mtD2kF4M.js +162 -0
  28. package/dist/assets/ganttDiagram-JELNMOA3-BKALgqTK.js +267 -0
  29. package/dist/assets/gitGraphDiagram-NY62KEGX-Bd7r0pAf.js +65 -0
  30. package/dist/assets/graph-B2rEI7cK.js +1 -0
  31. package/dist/assets/index-Bekv_o1t.css +1 -0
  32. package/dist/assets/index-DSRxU-E5.js +389 -0
  33. package/dist/assets/infoDiagram-WHAUD3N6--nJOBKqh.js +2 -0
  34. package/dist/assets/journeyDiagram-XKPGCS4Q-BzGutKN3.js +139 -0
  35. package/dist/assets/kanban-definition-3W4ZIXB7-DyQO17vq.js +89 -0
  36. package/dist/assets/katex-XbL3y5x-.js +261 -0
  37. package/dist/assets/layout-iCSHU015.js +1 -0
  38. package/dist/assets/min-BK_AIJdo.js +1 -0
  39. package/dist/assets/mindmap-definition-VGOIOE7T-BZMj_6zo.js +68 -0
  40. package/dist/assets/pieDiagram-ADFJNKIX-CkAGsq9p.js +30 -0
  41. package/dist/assets/quadrantDiagram-AYHSOK5B-CWa93px1.js +7 -0
  42. package/dist/assets/requirementDiagram-UZGBJVZJ-CufFVR8c.js +64 -0
  43. package/dist/assets/sankeyDiagram-TZEHDZUN-BEPgVgU4.js +10 -0
  44. package/dist/assets/sequenceDiagram-WL72ISMW-BkdBWhel.js +145 -0
  45. package/dist/assets/stateDiagram-FKZM4ZOC-D5T73yx0.js +1 -0
  46. package/dist/assets/stateDiagram-v2-4FDKWEC3-9hJWG2n6.js +1 -0
  47. package/dist/assets/timeline-definition-IT6M3QCI-CX7kTdU2.js +61 -0
  48. package/dist/assets/treemap-KMMF4GRG-ftWCQ9lJ.js +128 -0
  49. package/dist/assets/xychartDiagram-PRI3JC2R-Ngrels4n.js +7 -0
  50. package/{index.html → dist/index.html} +2 -1
  51. package/package.json +12 -2
  52. package/eslint.config.js +0 -23
  53. package/package.json.backup +0 -83
  54. package/postcss.config.js +0 -6
  55. package/src/App.css +0 -42
  56. package/src/App.tsx +0 -17
  57. package/src/assets/react.svg +0 -1
  58. package/src/components/LanguageSwitcher.tsx +0 -67
  59. package/src/components/Layout.tsx +0 -88
  60. package/src/components/MainSidebar.tsx +0 -163
  61. package/src/components/MermaidDiagram.tsx +0 -85
  62. package/src/components/MinimalLayout.tsx +0 -51
  63. package/src/components/Navigation.tsx +0 -254
  64. package/src/components/PriorityBadge.tsx +0 -59
  65. package/src/components/ProjectSwitcher.tsx +0 -222
  66. package/src/components/QuickSearch.tsx +0 -225
  67. package/src/components/RootRedirect.tsx +0 -40
  68. package/src/components/SpecDetailLayout.context.ts +0 -10
  69. package/src/components/SpecDetailLayout.tsx +0 -14
  70. package/src/components/SpecsNavSidebar.tsx +0 -615
  71. package/src/components/StatusBadge.tsx +0 -59
  72. package/src/components/ThemeToggle.tsx +0 -25
  73. package/src/components/Tooltip.tsx +0 -29
  74. package/src/components/context/ContextClient.tsx +0 -471
  75. package/src/components/context/ContextFileDetail.tsx +0 -163
  76. package/src/components/dashboard/ActivityItem.tsx +0 -36
  77. package/src/components/dashboard/DashboardClient.tsx +0 -218
  78. package/src/components/dashboard/SpecListItem.tsx +0 -58
  79. package/src/components/dashboard/StatCard.tsx +0 -52
  80. package/src/components/dependencies/SpecNode.tsx +0 -128
  81. package/src/components/dependencies/SpecSidebar.tsx +0 -256
  82. package/src/components/dependencies/constants.ts +0 -25
  83. package/src/components/dependencies/types.ts +0 -38
  84. package/src/components/dependencies/utils.ts +0 -261
  85. package/src/components/metadata-editors/PriorityEditor.tsx +0 -89
  86. package/src/components/metadata-editors/StatusEditor.tsx +0 -85
  87. package/src/components/metadata-editors/TagsEditor.tsx +0 -207
  88. package/src/components/projects/CreateProjectDialog.tsx +0 -162
  89. package/src/components/projects/DirectoryPicker.tsx +0 -182
  90. package/src/components/shared/BackToTop.tsx +0 -39
  91. package/src/components/shared/ColorPicker.tsx +0 -68
  92. package/src/components/shared/EmptyState.tsx +0 -35
  93. package/src/components/shared/ErrorBoundary.tsx +0 -79
  94. package/src/components/shared/PageHeader.tsx +0 -23
  95. package/src/components/shared/PageTransition.tsx +0 -40
  96. package/src/components/shared/ProjectAvatar.tsx +0 -107
  97. package/src/components/shared/Skeletons.tsx +0 -184
  98. package/src/components/spec-detail/EditableMetadata.tsx +0 -129
  99. package/src/components/spec-detail/MarkdownRenderer.tsx +0 -47
  100. package/src/components/spec-detail/TableOfContents.tsx +0 -150
  101. package/src/components/specs/BoardView.tsx +0 -204
  102. package/src/components/specs/ListView.tsx +0 -62
  103. package/src/components/specs/SpecsFilters.tsx +0 -190
  104. package/src/contexts/KeyboardShortcutsContext.tsx +0 -95
  105. package/src/contexts/LayoutContext.tsx +0 -45
  106. package/src/contexts/ProjectContext.tsx +0 -163
  107. package/src/contexts/ThemeContext.tsx +0 -90
  108. package/src/contexts/index.ts +0 -7
  109. package/src/hooks/useKeyboardShortcuts.ts +0 -87
  110. package/src/index.css +0 -624
  111. package/src/lib/api.ts +0 -72
  112. package/src/lib/backend-adapter.ts +0 -382
  113. package/src/lib/date-utils.ts +0 -122
  114. package/src/lib/i18n.test.ts +0 -57
  115. package/src/lib/i18n.ts +0 -51
  116. package/src/lib/markdown-utils.ts +0 -38
  117. package/src/lib/sub-spec-utils.ts +0 -166
  118. package/src/lib/utils.ts +0 -6
  119. package/src/locales/en/common.json +0 -660
  120. package/src/locales/en/errors.json +0 -20
  121. package/src/locales/en/help.json +0 -8
  122. package/src/locales/zh-CN/common.json +0 -660
  123. package/src/locales/zh-CN/errors.json +0 -20
  124. package/src/locales/zh-CN/help.json +0 -8
  125. package/src/main.tsx +0 -12
  126. package/src/pages/ContextPage.tsx +0 -111
  127. package/src/pages/DashboardPage.tsx +0 -97
  128. package/src/pages/DependenciesPage.tsx +0 -881
  129. package/src/pages/ProjectsPage.tsx +0 -432
  130. package/src/pages/SpecDetailPage.tsx +0 -592
  131. package/src/pages/SpecsPage.tsx +0 -319
  132. package/src/pages/StatsPage.tsx +0 -307
  133. package/src/router/projectRoutes.tsx +0 -36
  134. package/src/router.tsx +0 -33
  135. package/src/test/setup.ts +0 -39
  136. package/src/types/api.ts +0 -185
  137. package/tailwind.config.ts +0 -57
  138. package/tsconfig.app.json +0 -29
  139. package/tsconfig.json +0 -7
  140. package/tsconfig.node.json +0 -26
  141. package/tsconfig.tsbuildinfo +0 -1
  142. package/vite.config.ts +0 -27
  143. package/vitest.config.ts +0 -18
  144. /package/{public → dist}/favicon.ico +0 -0
  145. /package/{public → dist}/github-mark-white.svg +0 -0
  146. /package/{public → dist}/github-mark.svg +0 -0
  147. /package/{public → dist}/logo-dark-bg.svg +0 -0
  148. /package/{public → dist}/logo-with-bg.svg +0 -0
  149. /package/{public → dist}/logo.svg +0 -0
  150. /package/{public → dist}/vite.svg +0 -0
@@ -1,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
- }