@leanspec/ui 0.2.14 → 0.2.15-dev.21025278490

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,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
- }