@evolve.labs/devflow 0.8.0

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 (106) hide show
  1. package/.claude/commands/agents/architect.md +1162 -0
  2. package/.claude/commands/agents/architect.meta.yaml +124 -0
  3. package/.claude/commands/agents/builder.md +1432 -0
  4. package/.claude/commands/agents/builder.meta.yaml +117 -0
  5. package/.claude/commands/agents/chronicler.md +633 -0
  6. package/.claude/commands/agents/chronicler.meta.yaml +217 -0
  7. package/.claude/commands/agents/guardian.md +456 -0
  8. package/.claude/commands/agents/guardian.meta.yaml +127 -0
  9. package/.claude/commands/agents/strategist.md +483 -0
  10. package/.claude/commands/agents/strategist.meta.yaml +158 -0
  11. package/.claude/commands/agents/system-designer.md +1137 -0
  12. package/.claude/commands/agents/system-designer.meta.yaml +156 -0
  13. package/.claude/commands/devflow-help.md +93 -0
  14. package/.claude/commands/devflow-status.md +60 -0
  15. package/.claude/commands/quick/create-adr.md +82 -0
  16. package/.claude/commands/quick/new-feature.md +57 -0
  17. package/.claude/commands/quick/security-check.md +54 -0
  18. package/.claude/commands/quick/system-design.md +58 -0
  19. package/.claude_project +52 -0
  20. package/.devflow/agents/architect.meta.yaml +122 -0
  21. package/.devflow/agents/builder.meta.yaml +116 -0
  22. package/.devflow/agents/chronicler.meta.yaml +222 -0
  23. package/.devflow/agents/guardian.meta.yaml +127 -0
  24. package/.devflow/agents/strategist.meta.yaml +158 -0
  25. package/.devflow/agents/system-designer.meta.yaml +265 -0
  26. package/.devflow/project.yaml +242 -0
  27. package/.gitignore-template +84 -0
  28. package/LICENSE +21 -0
  29. package/README.md +249 -0
  30. package/bin/devflow.js +54 -0
  31. package/lib/autopilot.js +235 -0
  32. package/lib/autopilotConstants.js +213 -0
  33. package/lib/constants.js +95 -0
  34. package/lib/init.js +200 -0
  35. package/lib/update.js +181 -0
  36. package/lib/utils.js +157 -0
  37. package/lib/web.js +119 -0
  38. package/package.json +57 -0
  39. package/web/CHANGELOG.md +192 -0
  40. package/web/README.md +156 -0
  41. package/web/app/api/autopilot/execute/route.ts +102 -0
  42. package/web/app/api/autopilot/terminal-execute/route.ts +124 -0
  43. package/web/app/api/files/route.ts +280 -0
  44. package/web/app/api/files/tree/route.ts +160 -0
  45. package/web/app/api/git/route.ts +201 -0
  46. package/web/app/api/health/route.ts +94 -0
  47. package/web/app/api/project/open/route.ts +134 -0
  48. package/web/app/api/search/route.ts +247 -0
  49. package/web/app/api/specs/route.ts +405 -0
  50. package/web/app/api/terminal/route.ts +222 -0
  51. package/web/app/globals.css +160 -0
  52. package/web/app/ide/layout.tsx +43 -0
  53. package/web/app/ide/page.tsx +216 -0
  54. package/web/app/layout.tsx +34 -0
  55. package/web/app/page.tsx +303 -0
  56. package/web/components/agents/AgentIcons.tsx +281 -0
  57. package/web/components/autopilot/AutopilotConfigModal.tsx +245 -0
  58. package/web/components/autopilot/AutopilotPanel.tsx +299 -0
  59. package/web/components/dashboard/DashboardPanel.tsx +393 -0
  60. package/web/components/editor/Breadcrumbs.tsx +134 -0
  61. package/web/components/editor/EditorPanel.tsx +120 -0
  62. package/web/components/editor/EditorTabs.tsx +229 -0
  63. package/web/components/editor/MarkdownPreview.tsx +154 -0
  64. package/web/components/editor/MermaidDiagram.tsx +113 -0
  65. package/web/components/editor/MonacoEditor.tsx +177 -0
  66. package/web/components/editor/TabContextMenu.tsx +207 -0
  67. package/web/components/git/GitPanel.tsx +534 -0
  68. package/web/components/layout/Shell.tsx +15 -0
  69. package/web/components/layout/StatusBar.tsx +100 -0
  70. package/web/components/modals/CommandPalette.tsx +393 -0
  71. package/web/components/modals/GlobalSearch.tsx +348 -0
  72. package/web/components/modals/QuickOpen.tsx +241 -0
  73. package/web/components/modals/RecentFiles.tsx +208 -0
  74. package/web/components/projects/ProjectSelector.tsx +147 -0
  75. package/web/components/settings/SettingItem.tsx +150 -0
  76. package/web/components/settings/SettingsPanel.tsx +323 -0
  77. package/web/components/specs/SpecsPanel.tsx +1091 -0
  78. package/web/components/terminal/TerminalPanel.tsx +683 -0
  79. package/web/components/ui/ContextMenu.tsx +182 -0
  80. package/web/components/ui/LoadingSpinner.tsx +66 -0
  81. package/web/components/ui/ResizeHandle.tsx +110 -0
  82. package/web/components/ui/Skeleton.tsx +108 -0
  83. package/web/components/ui/SkipLinks.tsx +37 -0
  84. package/web/components/ui/Toaster.tsx +57 -0
  85. package/web/hooks/useFocusTrap.ts +141 -0
  86. package/web/hooks/useKeyboardShortcuts.ts +169 -0
  87. package/web/hooks/useListNavigation.ts +237 -0
  88. package/web/lib/autopilotConstants.ts +213 -0
  89. package/web/lib/constants/agents.ts +67 -0
  90. package/web/lib/git.ts +339 -0
  91. package/web/lib/ptyManager.ts +191 -0
  92. package/web/lib/specsParser.ts +299 -0
  93. package/web/lib/stores/autopilotStore.ts +288 -0
  94. package/web/lib/stores/fileStore.ts +550 -0
  95. package/web/lib/stores/gitStore.ts +386 -0
  96. package/web/lib/stores/projectStore.ts +196 -0
  97. package/web/lib/stores/settingsStore.ts +126 -0
  98. package/web/lib/stores/specsStore.ts +297 -0
  99. package/web/lib/stores/uiStore.ts +175 -0
  100. package/web/lib/types/index.ts +177 -0
  101. package/web/lib/utils.ts +98 -0
  102. package/web/next.config.js +50 -0
  103. package/web/package.json +54 -0
  104. package/web/postcss.config.js +6 -0
  105. package/web/tailwind.config.ts +68 -0
  106. package/web/tsconfig.json +41 -0
@@ -0,0 +1,182 @@
1
+ 'use client';
2
+
3
+ import * as React from 'react';
4
+ import * as ContextMenuPrimitive from '@radix-ui/react-context-menu';
5
+ import { cn } from '@/lib/utils';
6
+
7
+ const ContextMenu = ContextMenuPrimitive.Root;
8
+ const ContextMenuTrigger = ContextMenuPrimitive.Trigger;
9
+ const ContextMenuGroup = ContextMenuPrimitive.Group;
10
+ const ContextMenuPortal = ContextMenuPrimitive.Portal;
11
+ const ContextMenuSub = ContextMenuPrimitive.Sub;
12
+ const ContextMenuRadioGroup = ContextMenuPrimitive.RadioGroup;
13
+
14
+ const ContextMenuSubTrigger = React.forwardRef<
15
+ React.ElementRef<typeof ContextMenuPrimitive.SubTrigger>,
16
+ React.ComponentPropsWithoutRef<typeof ContextMenuPrimitive.SubTrigger> & {
17
+ inset?: boolean;
18
+ }
19
+ >(({ className, inset, children, ...props }, ref) => (
20
+ <ContextMenuPrimitive.SubTrigger
21
+ ref={ref}
22
+ className={cn(
23
+ 'flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none',
24
+ 'focus:bg-white/10 focus:text-white data-[state=open]:bg-white/10',
25
+ inset && 'pl-8',
26
+ className
27
+ )}
28
+ {...props}
29
+ >
30
+ {children}
31
+ </ContextMenuPrimitive.SubTrigger>
32
+ ));
33
+ ContextMenuSubTrigger.displayName = ContextMenuPrimitive.SubTrigger.displayName;
34
+
35
+ const ContextMenuSubContent = React.forwardRef<
36
+ React.ElementRef<typeof ContextMenuPrimitive.SubContent>,
37
+ React.ComponentPropsWithoutRef<typeof ContextMenuPrimitive.SubContent>
38
+ >(({ className, ...props }, ref) => (
39
+ <ContextMenuPrimitive.SubContent
40
+ ref={ref}
41
+ className={cn(
42
+ 'z-50 min-w-[8rem] overflow-hidden rounded-lg border border-white/10 bg-[#1a1a24] p-1 shadow-xl',
43
+ 'data-[state=open]:animate-in data-[state=closed]:animate-out',
44
+ 'data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0',
45
+ 'data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95',
46
+ className
47
+ )}
48
+ {...props}
49
+ />
50
+ ));
51
+ ContextMenuSubContent.displayName = ContextMenuPrimitive.SubContent.displayName;
52
+
53
+ const ContextMenuContent = React.forwardRef<
54
+ React.ElementRef<typeof ContextMenuPrimitive.Content>,
55
+ React.ComponentPropsWithoutRef<typeof ContextMenuPrimitive.Content>
56
+ >(({ className, ...props }, ref) => (
57
+ <ContextMenuPrimitive.Portal>
58
+ <ContextMenuPrimitive.Content
59
+ ref={ref}
60
+ className={cn(
61
+ 'z-50 min-w-[160px] overflow-hidden rounded-lg border border-white/10 bg-[#1a1a24] p-1 shadow-xl',
62
+ 'data-[state=open]:animate-in data-[state=closed]:animate-out',
63
+ 'data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0',
64
+ 'data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95',
65
+ className
66
+ )}
67
+ {...props}
68
+ />
69
+ </ContextMenuPrimitive.Portal>
70
+ ));
71
+ ContextMenuContent.displayName = ContextMenuPrimitive.Content.displayName;
72
+
73
+ const ContextMenuItem = React.forwardRef<
74
+ React.ElementRef<typeof ContextMenuPrimitive.Item>,
75
+ React.ComponentPropsWithoutRef<typeof ContextMenuPrimitive.Item> & {
76
+ inset?: boolean;
77
+ }
78
+ >(({ className, inset, ...props }, ref) => (
79
+ <ContextMenuPrimitive.Item
80
+ ref={ref}
81
+ className={cn(
82
+ 'relative flex cursor-pointer select-none items-center gap-2 rounded-md px-3 py-1.5 text-sm text-gray-300 outline-none transition-colors',
83
+ 'focus:bg-white/10 focus:text-white',
84
+ 'data-[disabled]:pointer-events-none data-[disabled]:opacity-50',
85
+ inset && 'pl-8',
86
+ className
87
+ )}
88
+ {...props}
89
+ />
90
+ ));
91
+ ContextMenuItem.displayName = ContextMenuPrimitive.Item.displayName;
92
+
93
+ const ContextMenuCheckboxItem = React.forwardRef<
94
+ React.ElementRef<typeof ContextMenuPrimitive.CheckboxItem>,
95
+ React.ComponentPropsWithoutRef<typeof ContextMenuPrimitive.CheckboxItem>
96
+ >(({ className, children, checked, ...props }, ref) => (
97
+ <ContextMenuPrimitive.CheckboxItem
98
+ ref={ref}
99
+ className={cn(
100
+ 'relative flex cursor-pointer select-none items-center rounded-md py-1.5 pl-8 pr-2 text-sm outline-none transition-colors',
101
+ 'focus:bg-white/10 focus:text-white',
102
+ 'data-[disabled]:pointer-events-none data-[disabled]:opacity-50',
103
+ className
104
+ )}
105
+ checked={checked}
106
+ {...props}
107
+ >
108
+ <span className="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
109
+ <ContextMenuPrimitive.ItemIndicator>
110
+ <svg className="h-4 w-4" fill="currentColor" viewBox="0 0 20 20">
111
+ <path
112
+ fillRule="evenodd"
113
+ d="M16.707 5.293a1 1 0 010 1.414l-8 8a1 1 0 01-1.414 0l-4-4a1 1 0 011.414-1.414L8 12.586l7.293-7.293a1 1 0 011.414 0z"
114
+ clipRule="evenodd"
115
+ />
116
+ </svg>
117
+ </ContextMenuPrimitive.ItemIndicator>
118
+ </span>
119
+ {children}
120
+ </ContextMenuPrimitive.CheckboxItem>
121
+ ));
122
+ ContextMenuCheckboxItem.displayName = ContextMenuPrimitive.CheckboxItem.displayName;
123
+
124
+ const ContextMenuLabel = React.forwardRef<
125
+ React.ElementRef<typeof ContextMenuPrimitive.Label>,
126
+ React.ComponentPropsWithoutRef<typeof ContextMenuPrimitive.Label> & {
127
+ inset?: boolean;
128
+ }
129
+ >(({ className, inset, ...props }, ref) => (
130
+ <ContextMenuPrimitive.Label
131
+ ref={ref}
132
+ className={cn(
133
+ 'px-2 py-1.5 text-xs font-semibold text-gray-500 uppercase tracking-wider',
134
+ inset && 'pl-8',
135
+ className
136
+ )}
137
+ {...props}
138
+ />
139
+ ));
140
+ ContextMenuLabel.displayName = ContextMenuPrimitive.Label.displayName;
141
+
142
+ const ContextMenuSeparator = React.forwardRef<
143
+ React.ElementRef<typeof ContextMenuPrimitive.Separator>,
144
+ React.ComponentPropsWithoutRef<typeof ContextMenuPrimitive.Separator>
145
+ >(({ className, ...props }, ref) => (
146
+ <ContextMenuPrimitive.Separator
147
+ ref={ref}
148
+ className={cn('-mx-1 my-1 h-px bg-white/10', className)}
149
+ {...props}
150
+ />
151
+ ));
152
+ ContextMenuSeparator.displayName = ContextMenuPrimitive.Separator.displayName;
153
+
154
+ const ContextMenuShortcut = ({
155
+ className,
156
+ ...props
157
+ }: React.HTMLAttributes<HTMLSpanElement>) => {
158
+ return (
159
+ <span
160
+ className={cn('ml-auto text-xs tracking-widest text-gray-500', className)}
161
+ {...props}
162
+ />
163
+ );
164
+ };
165
+ ContextMenuShortcut.displayName = 'ContextMenuShortcut';
166
+
167
+ export {
168
+ ContextMenu,
169
+ ContextMenuTrigger,
170
+ ContextMenuContent,
171
+ ContextMenuItem,
172
+ ContextMenuCheckboxItem,
173
+ ContextMenuLabel,
174
+ ContextMenuSeparator,
175
+ ContextMenuShortcut,
176
+ ContextMenuGroup,
177
+ ContextMenuPortal,
178
+ ContextMenuSub,
179
+ ContextMenuSubContent,
180
+ ContextMenuSubTrigger,
181
+ ContextMenuRadioGroup,
182
+ };
@@ -0,0 +1,66 @@
1
+ 'use client';
2
+
3
+ import { cn } from '@/lib/utils';
4
+ import { Loader2 } from 'lucide-react';
5
+
6
+ interface LoadingSpinnerProps {
7
+ size?: 'sm' | 'md' | 'lg';
8
+ className?: string;
9
+ }
10
+
11
+ const sizeClasses = {
12
+ sm: 'w-4 h-4',
13
+ md: 'w-6 h-6',
14
+ lg: 'w-8 h-8',
15
+ };
16
+
17
+ export function LoadingSpinner({ size = 'md', className }: LoadingSpinnerProps) {
18
+ return (
19
+ <Loader2
20
+ className={cn(
21
+ 'animate-spin text-purple-400',
22
+ sizeClasses[size],
23
+ className
24
+ )}
25
+ />
26
+ );
27
+ }
28
+
29
+ interface LoadingOverlayProps {
30
+ message?: string;
31
+ className?: string;
32
+ }
33
+
34
+ export function LoadingOverlay({ message, className }: LoadingOverlayProps) {
35
+ return (
36
+ <div className={cn(
37
+ 'absolute inset-0 bg-black/50 backdrop-blur-sm flex items-center justify-center z-50',
38
+ className
39
+ )}>
40
+ <div className="flex flex-col items-center gap-3">
41
+ <LoadingSpinner size="lg" />
42
+ {message && (
43
+ <span className="text-sm text-gray-300">{message}</span>
44
+ )}
45
+ </div>
46
+ </div>
47
+ );
48
+ }
49
+
50
+ interface LoadingDotsProps {
51
+ className?: string;
52
+ }
53
+
54
+ export function LoadingDots({ className }: LoadingDotsProps) {
55
+ return (
56
+ <span className={cn('inline-flex gap-1', className)}>
57
+ {[0, 1, 2].map((i) => (
58
+ <span
59
+ key={i}
60
+ className="w-1.5 h-1.5 rounded-full bg-purple-400 animate-bounce"
61
+ style={{ animationDelay: `${i * 150}ms` }}
62
+ />
63
+ ))}
64
+ </span>
65
+ );
66
+ }
@@ -0,0 +1,110 @@
1
+ 'use client';
2
+
3
+ import { useCallback, useEffect, useRef, useState } from 'react';
4
+ import { cn } from '@/lib/utils';
5
+
6
+ type ResizeDirection = 'horizontal' | 'vertical';
7
+ type ResizeSide = 'left' | 'right' | 'top' | 'bottom';
8
+
9
+ interface ResizeHandleProps {
10
+ direction: ResizeDirection;
11
+ side: ResizeSide;
12
+ onResize: (delta: number) => void;
13
+ onResizeEnd?: () => void;
14
+ minSize?: number;
15
+ maxSize?: number;
16
+ className?: string;
17
+ }
18
+
19
+ export function ResizeHandle({
20
+ direction,
21
+ side,
22
+ onResize,
23
+ onResizeEnd,
24
+ className,
25
+ }: ResizeHandleProps) {
26
+ const [isDragging, setIsDragging] = useState(false);
27
+ const startPosRef = useRef(0);
28
+ const handleRef = useRef<HTMLDivElement>(null);
29
+
30
+ const handleMouseDown = useCallback((e: React.MouseEvent) => {
31
+ e.preventDefault();
32
+ setIsDragging(true);
33
+ startPosRef.current = direction === 'horizontal' ? e.clientX : e.clientY;
34
+ }, [direction]);
35
+
36
+ const handleMouseMove = useCallback((e: MouseEvent) => {
37
+ if (!isDragging) return;
38
+
39
+ const currentPos = direction === 'horizontal' ? e.clientX : e.clientY;
40
+ let delta = currentPos - startPosRef.current;
41
+
42
+ // Invert delta for left/top sides
43
+ if (side === 'left' || side === 'top') {
44
+ delta = -delta;
45
+ }
46
+
47
+ onResize(delta);
48
+ startPosRef.current = currentPos;
49
+ }, [isDragging, direction, side, onResize]);
50
+
51
+ const handleMouseUp = useCallback(() => {
52
+ if (isDragging) {
53
+ setIsDragging(false);
54
+ onResizeEnd?.();
55
+ }
56
+ }, [isDragging, onResizeEnd]);
57
+
58
+ useEffect(() => {
59
+ if (isDragging) {
60
+ document.addEventListener('mousemove', handleMouseMove);
61
+ document.addEventListener('mouseup', handleMouseUp);
62
+ document.body.style.cursor = direction === 'horizontal' ? 'col-resize' : 'row-resize';
63
+ document.body.style.userSelect = 'none';
64
+ }
65
+
66
+ return () => {
67
+ document.removeEventListener('mousemove', handleMouseMove);
68
+ document.removeEventListener('mouseup', handleMouseUp);
69
+ document.body.style.cursor = '';
70
+ document.body.style.userSelect = '';
71
+ };
72
+ }, [isDragging, handleMouseMove, handleMouseUp, direction]);
73
+
74
+ const isHorizontal = direction === 'horizontal';
75
+
76
+ return (
77
+ <div
78
+ ref={handleRef}
79
+ onMouseDown={handleMouseDown}
80
+ className={cn(
81
+ 'group flex-shrink-0 transition-colors',
82
+ isHorizontal
83
+ ? 'w-1 cursor-col-resize hover:w-1'
84
+ : 'h-1 cursor-row-resize hover:h-1',
85
+ 'relative',
86
+ className
87
+ )}
88
+ >
89
+ {/* Visual indicator */}
90
+ <div
91
+ className={cn(
92
+ 'absolute transition-all duration-150',
93
+ isHorizontal
94
+ ? 'inset-y-0 left-0 w-1 group-hover:w-1 group-hover:bg-purple-500/50'
95
+ : 'inset-x-0 top-0 h-1 group-hover:h-1 group-hover:bg-purple-500/50',
96
+ isDragging && 'bg-purple-500'
97
+ )}
98
+ />
99
+ {/* Extended hit area */}
100
+ <div
101
+ className={cn(
102
+ 'absolute',
103
+ isHorizontal
104
+ ? 'inset-y-0 -left-1 -right-1 w-3'
105
+ : 'inset-x-0 -top-1 -bottom-1 h-3'
106
+ )}
107
+ />
108
+ </div>
109
+ );
110
+ }
@@ -0,0 +1,108 @@
1
+ 'use client';
2
+
3
+ import { CSSProperties } from 'react';
4
+ import { cn } from '@/lib/utils';
5
+
6
+ interface SkeletonProps {
7
+ className?: string;
8
+ style?: CSSProperties;
9
+ }
10
+
11
+ export function Skeleton({ className, style }: SkeletonProps) {
12
+ return (
13
+ <div
14
+ className={cn(
15
+ 'animate-pulse rounded-md bg-white/5',
16
+ className
17
+ )}
18
+ style={style}
19
+ />
20
+ );
21
+ }
22
+
23
+ interface SkeletonTextProps {
24
+ lines?: number;
25
+ className?: string;
26
+ }
27
+
28
+ export function SkeletonText({ lines = 3, className }: SkeletonTextProps) {
29
+ return (
30
+ <div className={cn('space-y-2', className)}>
31
+ {Array.from({ length: lines }).map((_, i) => (
32
+ <Skeleton
33
+ key={i}
34
+ className={cn(
35
+ 'h-4',
36
+ i === lines - 1 ? 'w-3/4' : 'w-full'
37
+ )}
38
+ />
39
+ ))}
40
+ </div>
41
+ );
42
+ }
43
+
44
+ interface SkeletonCardProps {
45
+ className?: string;
46
+ }
47
+
48
+ export function SkeletonCard({ className }: SkeletonCardProps) {
49
+ return (
50
+ <div className={cn('p-4 rounded-lg border border-white/10 bg-white/5', className)}>
51
+ <div className="flex items-center gap-3 mb-3">
52
+ <Skeleton className="w-8 h-8 rounded-full" />
53
+ <div className="flex-1">
54
+ <Skeleton className="h-4 w-1/3 mb-1" />
55
+ <Skeleton className="h-3 w-1/2" />
56
+ </div>
57
+ </div>
58
+ <SkeletonText lines={2} />
59
+ </div>
60
+ );
61
+ }
62
+
63
+ interface SkeletonListProps {
64
+ items?: number;
65
+ className?: string;
66
+ }
67
+
68
+ export function SkeletonList({ items = 5, className }: SkeletonListProps) {
69
+ return (
70
+ <div className={cn('space-y-2', className)}>
71
+ {Array.from({ length: items }).map((_, i) => (
72
+ <div key={i} className="flex items-center gap-2 p-2">
73
+ <Skeleton className="w-4 h-4" />
74
+ <Skeleton className="h-4 flex-1" />
75
+ </div>
76
+ ))}
77
+ </div>
78
+ );
79
+ }
80
+
81
+ interface SkeletonTreeProps {
82
+ depth?: number;
83
+ items?: number;
84
+ className?: string;
85
+ }
86
+
87
+ export function SkeletonTree({ depth = 2, items = 5, className }: SkeletonTreeProps) {
88
+ const renderItems = (level: number, count: number) => {
89
+ return Array.from({ length: count }).map((_, i) => (
90
+ <div key={`${level}-${i}`}>
91
+ <div
92
+ className="flex items-center gap-2 py-1"
93
+ style={{ paddingLeft: level * 16 }}
94
+ >
95
+ <Skeleton className="w-4 h-4" />
96
+ <Skeleton className={cn('h-4', i % 3 === 0 ? 'w-24' : i % 2 === 0 ? 'w-32' : 'w-20')} />
97
+ </div>
98
+ {level < depth && i % 2 === 0 && renderItems(level + 1, 2)}
99
+ </div>
100
+ ));
101
+ };
102
+
103
+ return (
104
+ <div className={cn('space-y-1', className)}>
105
+ {renderItems(0, items)}
106
+ </div>
107
+ );
108
+ }
@@ -0,0 +1,37 @@
1
+ 'use client';
2
+
3
+ /**
4
+ * Skip Links component for keyboard accessibility.
5
+ * Provides quick navigation to main content areas for screen reader users.
6
+ * Links are visually hidden but appear on focus.
7
+ */
8
+
9
+ interface SkipLink {
10
+ href: string;
11
+ label: string;
12
+ }
13
+
14
+ const skipLinks: SkipLink[] = [
15
+ { href: '#main-editor', label: 'Skip to editor' },
16
+ { href: '#main-sidebar', label: 'Skip to sidebar' },
17
+ { href: '#main-chat', label: 'Skip to chat' },
18
+ ];
19
+
20
+ export function SkipLinks() {
21
+ return (
22
+ <nav aria-label="Skip navigation" className="sr-only focus-within:not-sr-only">
23
+ <ul className="fixed top-0 left-0 z-[100] flex gap-2 p-2 bg-[#12121a] border-b border-white/10">
24
+ {skipLinks.map((link) => (
25
+ <li key={link.href}>
26
+ <a
27
+ href={link.href}
28
+ className="sr-only focus:not-sr-only focus:inline-block px-4 py-2 text-sm font-medium text-white bg-purple-600 rounded-md hover:bg-purple-500 focus:outline-none focus:ring-2 focus:ring-purple-500 focus:ring-offset-2 focus:ring-offset-[#0a0a0f]"
29
+ >
30
+ {link.label}
31
+ </a>
32
+ </li>
33
+ ))}
34
+ </ul>
35
+ </nav>
36
+ );
37
+ }
@@ -0,0 +1,57 @@
1
+ 'use client';
2
+
3
+ import { Toaster as SonnerToaster } from 'sonner';
4
+
5
+ export function Toaster() {
6
+ return (
7
+ <SonnerToaster
8
+ position="bottom-right"
9
+ toastOptions={{
10
+ style: {
11
+ background: '#12121a',
12
+ border: '1px solid rgba(255, 255, 255, 0.1)',
13
+ color: '#fff',
14
+ },
15
+ classNames: {
16
+ toast: 'group',
17
+ title: 'text-sm font-medium',
18
+ description: 'text-xs text-gray-400',
19
+ success: 'border-green-500/30 bg-green-500/10',
20
+ error: 'border-red-500/30 bg-red-500/10',
21
+ warning: 'border-yellow-500/30 bg-yellow-500/10',
22
+ info: 'border-purple-500/30 bg-purple-500/10',
23
+ },
24
+ }}
25
+ icons={{
26
+ success: (
27
+ <div className="w-5 h-5 rounded-full bg-green-500/20 flex items-center justify-center">
28
+ <svg className="w-3 h-3 text-green-400" fill="none" viewBox="0 0 24 24" stroke="currentColor">
29
+ <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M5 13l4 4L19 7" />
30
+ </svg>
31
+ </div>
32
+ ),
33
+ error: (
34
+ <div className="w-5 h-5 rounded-full bg-red-500/20 flex items-center justify-center">
35
+ <svg className="w-3 h-3 text-red-400" fill="none" viewBox="0 0 24 24" stroke="currentColor">
36
+ <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12" />
37
+ </svg>
38
+ </div>
39
+ ),
40
+ warning: (
41
+ <div className="w-5 h-5 rounded-full bg-yellow-500/20 flex items-center justify-center">
42
+ <svg className="w-3 h-3 text-yellow-400" fill="none" viewBox="0 0 24 24" stroke="currentColor">
43
+ <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z" />
44
+ </svg>
45
+ </div>
46
+ ),
47
+ info: (
48
+ <div className="w-5 h-5 rounded-full bg-purple-500/20 flex items-center justify-center">
49
+ <svg className="w-3 h-3 text-purple-400" fill="none" viewBox="0 0 24 24" stroke="currentColor">
50
+ <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
51
+ </svg>
52
+ </div>
53
+ ),
54
+ }}
55
+ />
56
+ );
57
+ }
@@ -0,0 +1,141 @@
1
+ 'use client';
2
+
3
+ import { useEffect, useRef, RefObject } from 'react';
4
+
5
+ /**
6
+ * Custom hook for trapping focus within a container element.
7
+ * Implements accessible modal focus management per WAI-ARIA guidelines.
8
+ *
9
+ * Features:
10
+ * - Traps Tab/Shift+Tab navigation within container
11
+ * - Auto-focuses first focusable element on activation
12
+ * - Restores focus to previous element on deactivation
13
+ * - Handles ESC key for closing (optional)
14
+ *
15
+ * @param containerRef - Reference to the container element
16
+ * @param isActive - Whether the focus trap is currently active
17
+ * @param options - Configuration options
18
+ */
19
+ interface UseFocusTrapOptions {
20
+ /** Called when Escape is pressed */
21
+ onEscape?: () => void;
22
+ /** Auto-focus first element when activated (default: true) */
23
+ autoFocus?: boolean;
24
+ /** Restore focus when deactivated (default: true) */
25
+ restoreFocus?: boolean;
26
+ }
27
+
28
+ const FOCUSABLE_SELECTORS = [
29
+ 'button:not([disabled])',
30
+ 'a[href]',
31
+ 'input:not([disabled])',
32
+ 'select:not([disabled])',
33
+ 'textarea:not([disabled])',
34
+ '[tabindex]:not([tabindex="-1"])',
35
+ '[contenteditable="true"]',
36
+ ].join(', ');
37
+
38
+ export function useFocusTrap(
39
+ containerRef: RefObject<HTMLElement | null>,
40
+ isActive: boolean,
41
+ options: UseFocusTrapOptions = {}
42
+ ) {
43
+ const { onEscape, autoFocus = true, restoreFocus = true } = options;
44
+ const previousActiveElement = useRef<HTMLElement | null>(null);
45
+
46
+ useEffect(() => {
47
+ if (!isActive || !containerRef.current) return;
48
+
49
+ const container = containerRef.current;
50
+
51
+ // Store currently focused element
52
+ if (restoreFocus) {
53
+ previousActiveElement.current = document.activeElement as HTMLElement;
54
+ }
55
+
56
+ // Get all focusable elements
57
+ const getFocusableElements = (): HTMLElement[] => {
58
+ return Array.from(container.querySelectorAll<HTMLElement>(FOCUSABLE_SELECTORS))
59
+ .filter((el) => {
60
+ // Filter out hidden elements
61
+ const style = window.getComputedStyle(el);
62
+ return style.display !== 'none' && style.visibility !== 'hidden';
63
+ });
64
+ };
65
+
66
+ // Auto-focus first element
67
+ if (autoFocus) {
68
+ const focusableElements = getFocusableElements();
69
+ if (focusableElements.length > 0) {
70
+ // Small delay to ensure DOM is ready
71
+ requestAnimationFrame(() => {
72
+ focusableElements[0]?.focus();
73
+ });
74
+ }
75
+ }
76
+
77
+ // Handle keydown for tab trapping and escape
78
+ const handleKeyDown = (event: KeyboardEvent) => {
79
+ if (event.key === 'Escape' && onEscape) {
80
+ event.preventDefault();
81
+ onEscape();
82
+ return;
83
+ }
84
+
85
+ if (event.key !== 'Tab') return;
86
+
87
+ const focusableElements = getFocusableElements();
88
+ if (focusableElements.length === 0) return;
89
+
90
+ const firstElement = focusableElements[0];
91
+ const lastElement = focusableElements[focusableElements.length - 1];
92
+ const activeElement = document.activeElement;
93
+
94
+ // Shift+Tab on first element → focus last
95
+ if (event.shiftKey && activeElement === firstElement) {
96
+ event.preventDefault();
97
+ lastElement?.focus();
98
+ return;
99
+ }
100
+
101
+ // Tab on last element → focus first
102
+ if (!event.shiftKey && activeElement === lastElement) {
103
+ event.preventDefault();
104
+ firstElement?.focus();
105
+ return;
106
+ }
107
+
108
+ // If focus is outside container, bring it back
109
+ if (!container.contains(activeElement)) {
110
+ event.preventDefault();
111
+ firstElement?.focus();
112
+ }
113
+ };
114
+
115
+ // Prevent focus from leaving container via mouse click
116
+ const handleFocusIn = (event: FocusEvent) => {
117
+ if (!container.contains(event.target as Node)) {
118
+ const focusableElements = getFocusableElements();
119
+ if (focusableElements.length > 0) {
120
+ focusableElements[0]?.focus();
121
+ }
122
+ }
123
+ };
124
+
125
+ document.addEventListener('keydown', handleKeyDown);
126
+ document.addEventListener('focusin', handleFocusIn);
127
+
128
+ return () => {
129
+ document.removeEventListener('keydown', handleKeyDown);
130
+ document.removeEventListener('focusin', handleFocusIn);
131
+
132
+ // Restore focus to previous element
133
+ if (restoreFocus && previousActiveElement.current) {
134
+ // Small delay to prevent focus issues
135
+ requestAnimationFrame(() => {
136
+ previousActiveElement.current?.focus();
137
+ });
138
+ }
139
+ };
140
+ }, [isActive, containerRef, onEscape, autoFocus, restoreFocus]);
141
+ }