@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.
Files changed (149) 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 +13 -3
  52. package/eslint.config.js +0 -23
  53. package/postcss.config.js +0 -6
  54. package/src/App.css +0 -42
  55. package/src/App.tsx +0 -17
  56. package/src/assets/react.svg +0 -1
  57. package/src/components/LanguageSwitcher.tsx +0 -67
  58. package/src/components/Layout.tsx +0 -88
  59. package/src/components/MainSidebar.tsx +0 -163
  60. package/src/components/MermaidDiagram.tsx +0 -85
  61. package/src/components/MinimalLayout.tsx +0 -51
  62. package/src/components/Navigation.tsx +0 -254
  63. package/src/components/PriorityBadge.tsx +0 -59
  64. package/src/components/ProjectSwitcher.tsx +0 -222
  65. package/src/components/QuickSearch.tsx +0 -225
  66. package/src/components/RootRedirect.tsx +0 -40
  67. package/src/components/SpecDetailLayout.context.ts +0 -10
  68. package/src/components/SpecDetailLayout.tsx +0 -14
  69. package/src/components/SpecsNavSidebar.tsx +0 -615
  70. package/src/components/StatusBadge.tsx +0 -59
  71. package/src/components/ThemeToggle.tsx +0 -25
  72. package/src/components/Tooltip.tsx +0 -29
  73. package/src/components/context/ContextClient.tsx +0 -471
  74. package/src/components/context/ContextFileDetail.tsx +0 -163
  75. package/src/components/dashboard/ActivityItem.tsx +0 -36
  76. package/src/components/dashboard/DashboardClient.tsx +0 -218
  77. package/src/components/dashboard/SpecListItem.tsx +0 -58
  78. package/src/components/dashboard/StatCard.tsx +0 -52
  79. package/src/components/dependencies/SpecNode.tsx +0 -128
  80. package/src/components/dependencies/SpecSidebar.tsx +0 -256
  81. package/src/components/dependencies/constants.ts +0 -25
  82. package/src/components/dependencies/types.ts +0 -38
  83. package/src/components/dependencies/utils.ts +0 -261
  84. package/src/components/metadata-editors/PriorityEditor.tsx +0 -89
  85. package/src/components/metadata-editors/StatusEditor.tsx +0 -85
  86. package/src/components/metadata-editors/TagsEditor.tsx +0 -207
  87. package/src/components/projects/CreateProjectDialog.tsx +0 -162
  88. package/src/components/projects/DirectoryPicker.tsx +0 -182
  89. package/src/components/shared/BackToTop.tsx +0 -39
  90. package/src/components/shared/ColorPicker.tsx +0 -68
  91. package/src/components/shared/EmptyState.tsx +0 -35
  92. package/src/components/shared/ErrorBoundary.tsx +0 -79
  93. package/src/components/shared/PageHeader.tsx +0 -23
  94. package/src/components/shared/PageTransition.tsx +0 -40
  95. package/src/components/shared/ProjectAvatar.tsx +0 -107
  96. package/src/components/shared/Skeletons.tsx +0 -184
  97. package/src/components/spec-detail/EditableMetadata.tsx +0 -129
  98. package/src/components/spec-detail/MarkdownRenderer.tsx +0 -47
  99. package/src/components/spec-detail/TableOfContents.tsx +0 -150
  100. package/src/components/specs/BoardView.tsx +0 -204
  101. package/src/components/specs/ListView.tsx +0 -62
  102. package/src/components/specs/SpecsFilters.tsx +0 -190
  103. package/src/contexts/KeyboardShortcutsContext.tsx +0 -95
  104. package/src/contexts/LayoutContext.tsx +0 -45
  105. package/src/contexts/ProjectContext.tsx +0 -163
  106. package/src/contexts/ThemeContext.tsx +0 -90
  107. package/src/contexts/index.ts +0 -7
  108. package/src/hooks/useKeyboardShortcuts.ts +0 -87
  109. package/src/index.css +0 -624
  110. package/src/lib/api.ts +0 -72
  111. package/src/lib/backend-adapter.ts +0 -382
  112. package/src/lib/date-utils.ts +0 -122
  113. package/src/lib/i18n.test.ts +0 -57
  114. package/src/lib/i18n.ts +0 -51
  115. package/src/lib/markdown-utils.ts +0 -38
  116. package/src/lib/sub-spec-utils.ts +0 -166
  117. package/src/lib/utils.ts +0 -6
  118. package/src/locales/en/common.json +0 -660
  119. package/src/locales/en/errors.json +0 -20
  120. package/src/locales/en/help.json +0 -8
  121. package/src/locales/zh-CN/common.json +0 -660
  122. package/src/locales/zh-CN/errors.json +0 -20
  123. package/src/locales/zh-CN/help.json +0 -8
  124. package/src/main.tsx +0 -12
  125. package/src/pages/ContextPage.tsx +0 -111
  126. package/src/pages/DashboardPage.tsx +0 -97
  127. package/src/pages/DependenciesPage.tsx +0 -881
  128. package/src/pages/ProjectsPage.tsx +0 -432
  129. package/src/pages/SpecDetailPage.tsx +0 -592
  130. package/src/pages/SpecsPage.tsx +0 -319
  131. package/src/pages/StatsPage.tsx +0 -307
  132. package/src/router/projectRoutes.tsx +0 -36
  133. package/src/router.tsx +0 -33
  134. package/src/test/setup.ts +0 -39
  135. package/src/types/api.ts +0 -185
  136. package/tailwind.config.ts +0 -57
  137. package/tsconfig.app.json +0 -29
  138. package/tsconfig.json +0 -7
  139. package/tsconfig.node.json +0 -26
  140. package/tsconfig.tsbuildinfo +0 -1
  141. package/vite.config.ts +0 -27
  142. package/vitest.config.ts +0 -18
  143. /package/{public → dist}/favicon.ico +0 -0
  144. /package/{public → dist}/github-mark-white.svg +0 -0
  145. /package/{public → dist}/github-mark.svg +0 -0
  146. /package/{public → dist}/logo-dark-bg.svg +0 -0
  147. /package/{public → dist}/logo-with-bg.svg +0 -0
  148. /package/{public → dist}/logo.svg +0 -0
  149. /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
- }