claude-dashboard 0.1.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 (88) hide show
  1. package/.claude/settings.local.json +10 -0
  2. package/LICENSE +21 -0
  3. package/README.md +99 -0
  4. package/README.zh-TW.md +99 -0
  5. package/bin/cdb.ts +60 -0
  6. package/bun.lock +1612 -0
  7. package/bunfig.toml +4 -0
  8. package/components.json +20 -0
  9. package/next.config.ts +19 -0
  10. package/package.json +62 -0
  11. package/postcss.config.mjs +9 -0
  12. package/prompts/pm-system.md +61 -0
  13. package/prompts/rd-system.md +68 -0
  14. package/prompts/sec-system.md +93 -0
  15. package/prompts/test-system.md +71 -0
  16. package/prompts/ui-system.md +72 -0
  17. package/server.ts +118 -0
  18. package/sql.js.d.ts +33 -0
  19. package/src/__tests__/api/usage/route.test.ts +193 -0
  20. package/src/__tests__/components/layout/TopNav.test.tsx +155 -0
  21. package/src/__tests__/components/layout/UsageIndicator.test.tsx +503 -0
  22. package/src/__tests__/hooks/useUsage.test.tsx +174 -0
  23. package/src/__tests__/lib/usage/get-token.test.ts +117 -0
  24. package/src/__tests__/react-sanity.test.tsx +14 -0
  25. package/src/__tests__/sanity.test.ts +7 -0
  26. package/src/__tests__/setup.ts +1 -0
  27. package/src/app/api/health/route.ts +8 -0
  28. package/src/app/api/usage/route.ts +86 -0
  29. package/src/app/api/workflows/[id]/route.ts +17 -0
  30. package/src/app/api/workflows/route.ts +14 -0
  31. package/src/app/globals.css +74 -0
  32. package/src/app/history/page.tsx +15 -0
  33. package/src/app/layout.tsx +24 -0
  34. package/src/app/page.tsx +112 -0
  35. package/src/components/agent/AgentCard.tsx +117 -0
  36. package/src/components/agent/AgentCardGrid.tsx +14 -0
  37. package/src/components/agent/AgentOutput.tsx +87 -0
  38. package/src/components/agent/AgentStatusBadge.tsx +20 -0
  39. package/src/components/events/EventLog.tsx +65 -0
  40. package/src/components/events/EventLogItem.tsx +39 -0
  41. package/src/components/history/HistoryTable.tsx +105 -0
  42. package/src/components/layout/DashboardShell.tsx +12 -0
  43. package/src/components/layout/TopNav.tsx +86 -0
  44. package/src/components/layout/UsageIndicator.tsx +163 -0
  45. package/src/components/pipeline/PipelineBar.tsx +59 -0
  46. package/src/components/pipeline/PipelineNode.tsx +55 -0
  47. package/src/components/terminal/TerminalPanel.tsx +138 -0
  48. package/src/components/terminal/XTermRenderer.tsx +129 -0
  49. package/src/components/ui/badge.tsx +37 -0
  50. package/src/components/ui/button.tsx +55 -0
  51. package/src/components/ui/card.tsx +80 -0
  52. package/src/components/ui/input.tsx +26 -0
  53. package/src/components/ui/scroll-area.tsx +52 -0
  54. package/src/components/ui/separator.tsx +31 -0
  55. package/src/components/ui/textarea.tsx +25 -0
  56. package/src/components/ui/tooltip.tsx +73 -0
  57. package/src/components/workflow/WorkflowLauncher.tsx +102 -0
  58. package/src/hooks/useAgentStream.ts +27 -0
  59. package/src/hooks/useAutoScroll.ts +24 -0
  60. package/src/hooks/useUsage.ts +66 -0
  61. package/src/hooks/useWebSocket.ts +289 -0
  62. package/src/lib/agents/prompts.ts +341 -0
  63. package/src/lib/db/connection.ts +263 -0
  64. package/src/lib/db/queries.ts +257 -0
  65. package/src/lib/db/schema.ts +39 -0
  66. package/src/lib/output-buffer.ts +41 -0
  67. package/src/lib/terminal/pty-manager.ts +106 -0
  68. package/src/lib/usage/get-token.ts +48 -0
  69. package/src/lib/utils.ts +6 -0
  70. package/src/lib/websocket/connection-manager.ts +71 -0
  71. package/src/lib/websocket/protocol.ts +90 -0
  72. package/src/lib/websocket/server.ts +231 -0
  73. package/src/lib/workflow/agent-runner.ts +254 -0
  74. package/src/lib/workflow/context-builder.ts +62 -0
  75. package/src/lib/workflow/engine.ts +310 -0
  76. package/src/lib/workflow/pipeline.ts +28 -0
  77. package/src/lib/workflow/types.ts +111 -0
  78. package/src/stores/agentStore.ts +152 -0
  79. package/src/stores/eventStore.ts +35 -0
  80. package/src/stores/terminalStore.ts +20 -0
  81. package/src/stores/uiStore.ts +35 -0
  82. package/src/stores/workflowStore.ts +57 -0
  83. package/src/types/css.d.ts +4 -0
  84. package/src/types/index.ts +12 -0
  85. package/tailwind.config.ts +65 -0
  86. package/tsconfig.json +25 -0
  87. package/tsconfig.server.json +21 -0
  88. package/vitest.config.ts +25 -0
@@ -0,0 +1,55 @@
1
+ "use client";
2
+
3
+ import { cn } from "@/lib/utils";
4
+ import type { AgentRole, StepStatus } from "@/lib/workflow/types";
5
+ import { AGENT_CONFIG } from "@/lib/workflow/types";
6
+
7
+ interface PipelineNodeProps {
8
+ role: AgentRole;
9
+ status: StepStatus;
10
+ isCurrent: boolean;
11
+ }
12
+
13
+ export function PipelineNode({ role, status, isCurrent }: PipelineNodeProps) {
14
+ const config = AGENT_CONFIG[role];
15
+
16
+ const statusStyles: Record<StepStatus, string> = {
17
+ pending: "border-gray-600 bg-gray-800/50 text-gray-500",
18
+ running:
19
+ "border-emerald-500 bg-emerald-500/10 text-emerald-400 shadow-lg shadow-emerald-500/20",
20
+ completed: "border-blue-500 bg-blue-500/10 text-blue-400",
21
+ failed: "border-red-500 bg-red-500/10 text-red-400",
22
+ skipped: "border-gray-700 bg-gray-800/30 text-gray-600",
23
+ };
24
+
25
+ return (
26
+ <div className="flex items-center gap-1">
27
+ <div
28
+ className={cn(
29
+ "relative px-3 py-1.5 rounded-md border text-xs font-medium transition-all duration-300",
30
+ statusStyles[status]
31
+ )}
32
+ style={{
33
+ borderColor:
34
+ status === "running" || status === "completed"
35
+ ? config.color
36
+ : undefined,
37
+ }}
38
+ >
39
+ {status === "running" && (
40
+ <span className="absolute -top-0.5 -right-0.5 flex h-2 w-2">
41
+ <span
42
+ className="animate-ping absolute inline-flex h-full w-full rounded-full opacity-75"
43
+ style={{ backgroundColor: config.color }}
44
+ />
45
+ <span
46
+ className="relative inline-flex rounded-full h-2 w-2"
47
+ style={{ backgroundColor: config.color }}
48
+ />
49
+ </span>
50
+ )}
51
+ {config.label}
52
+ </div>
53
+ </div>
54
+ );
55
+ }
@@ -0,0 +1,138 @@
1
+ "use client";
2
+
3
+ import { useCallback, useRef, useState, useEffect } from "react";
4
+ import dynamic from "next/dynamic";
5
+ import { useTerminalStore } from "@/stores/terminalStore";
6
+ import type { XTermHandle } from "./XTermRenderer";
7
+
8
+ const XTermRenderer = dynamic(
9
+ () =>
10
+ import("./XTermRenderer").then((mod) => ({
11
+ default: mod.XTermRenderer,
12
+ })),
13
+ { ssr: false }
14
+ );
15
+
16
+ interface TerminalPanelProps {
17
+ send: (msg: object) => void;
18
+ }
19
+
20
+ export function TerminalPanel({ send }: TerminalPanelProps) {
21
+ const { terminalId, connected } = useTerminalStore();
22
+ const termRef = useRef<XTermHandle>(null);
23
+ const [error, setError] = useState<string | null>(null);
24
+
25
+ // Track terminalId via ref to avoid listener re-registration gaps
26
+ const terminalIdRef = useRef<string | null>(terminalId);
27
+ useEffect(() => { terminalIdRef.current = terminalId; }, [terminalId]);
28
+
29
+ // Create terminal on mount
30
+ useEffect(() => {
31
+ if (connected && !terminalId) {
32
+ send({ type: "terminal:create", payload: { projectPath: "" } });
33
+ }
34
+ }, [connected, terminalId, send]);
35
+
36
+ // Listen for terminal output via CustomEvent — registered once, uses ref for terminalId.
37
+ // Accept output when terminalIdRef is still null (terminal initializing) to avoid
38
+ // dropping early output that arrives before the ref is synced from the store.
39
+ useEffect(() => {
40
+ const handler = (e: CustomEvent) => {
41
+ if (!terminalIdRef.current || e.detail.terminalId === terminalIdRef.current) {
42
+ termRef.current?.write(e.detail.data);
43
+ }
44
+ };
45
+ window.addEventListener("terminal:output" as any, handler);
46
+ return () => window.removeEventListener("terminal:output" as any, handler);
47
+ }, []);
48
+
49
+ // Listen for terminal errors via CustomEvent
50
+ useEffect(() => {
51
+ const handler = (e: CustomEvent) => {
52
+ setError(e.detail.error);
53
+ };
54
+ window.addEventListener("terminal:error" as any, handler);
55
+ return () => window.removeEventListener("terminal:error" as any, handler);
56
+ }, []);
57
+
58
+ const handleData = useCallback(
59
+ (data: string) => {
60
+ if (terminalId) {
61
+ send({
62
+ type: "terminal:input",
63
+ payload: { terminalId, data },
64
+ });
65
+ }
66
+ },
67
+ [terminalId, send]
68
+ );
69
+
70
+ const pendingSizeRef = useRef<{ cols: number; rows: number } | null>(null);
71
+
72
+ const handleResize = useCallback(
73
+ (cols: number, rows: number) => {
74
+ if (terminalId) {
75
+ send({
76
+ type: "terminal:resize",
77
+ payload: { terminalId, cols, rows },
78
+ });
79
+ pendingSizeRef.current = null;
80
+ } else {
81
+ // terminalId not yet available — stash for later
82
+ pendingSizeRef.current = { cols, rows };
83
+ }
84
+ },
85
+ [terminalId, send]
86
+ );
87
+
88
+ // Flush pending resize once terminalId becomes available
89
+ useEffect(() => {
90
+ if (terminalId && pendingSizeRef.current) {
91
+ send({
92
+ type: "terminal:resize",
93
+ payload: { terminalId, ...pendingSizeRef.current },
94
+ });
95
+ pendingSizeRef.current = null;
96
+ }
97
+ }, [terminalId, send]);
98
+
99
+ if (!connected) {
100
+ return (
101
+ <div className="flex items-center justify-center h-full text-xs text-muted-foreground">
102
+ Connecting to server...
103
+ </div>
104
+ );
105
+ }
106
+
107
+ if (error) {
108
+ return (
109
+ <div className="h-full flex flex-col">
110
+ <div className="flex items-center justify-between px-3 py-1.5 border-b border-border">
111
+ <span className="text-xs font-medium">Terminal</span>
112
+ <span className="text-[10px] text-red-400">Error</span>
113
+ </div>
114
+ <div className="flex-1 flex items-center justify-center px-4">
115
+ <p className="text-xs text-red-400 text-center">{error}</p>
116
+ </div>
117
+ </div>
118
+ );
119
+ }
120
+
121
+ return (
122
+ <div className="h-full flex flex-col">
123
+ <div className="flex items-center justify-between px-3 py-1.5 border-b border-border">
124
+ <span className="text-xs font-medium">Terminal</span>
125
+ <span className="text-[10px] text-muted-foreground">
126
+ {terminalId ? "Connected" : "Initializing..."}
127
+ </span>
128
+ </div>
129
+ <div className="flex-1 min-h-0">
130
+ <XTermRenderer
131
+ ref={termRef}
132
+ onData={handleData}
133
+ onResize={handleResize}
134
+ />
135
+ </div>
136
+ </div>
137
+ );
138
+ }
@@ -0,0 +1,129 @@
1
+ "use client";
2
+
3
+ import { useEffect, useRef, useImperativeHandle, forwardRef } from "react";
4
+
5
+ export interface XTermHandle {
6
+ write: (data: string) => void;
7
+ clear: () => void;
8
+ }
9
+
10
+ interface XTermRendererProps {
11
+ onData: (data: string) => void;
12
+ onResize: (cols: number, rows: number) => void;
13
+ }
14
+
15
+ let Terminal: any;
16
+ let FitAddon: any;
17
+
18
+ export const XTermRenderer = forwardRef<XTermHandle, XTermRendererProps>(
19
+ function XTermRenderer({ onData, onResize }, ref) {
20
+ const containerRef = useRef<HTMLDivElement>(null);
21
+ const termRef = useRef<any>(null);
22
+ const fitAddonRef = useRef<any>(null);
23
+ const initialized = useRef(false);
24
+
25
+ // Refs to hold latest callbacks — avoids stale closures in xterm event handlers
26
+ const onDataRef = useRef(onData);
27
+ const onResizeRef = useRef(onResize);
28
+ useEffect(() => { onDataRef.current = onData; }, [onData]);
29
+ useEffect(() => { onResizeRef.current = onResize; }, [onResize]);
30
+
31
+ const pendingWrites = useRef<string[]>([]);
32
+
33
+ useImperativeHandle(ref, () => ({
34
+ write(data: string) {
35
+ if (termRef.current) {
36
+ termRef.current.write(data);
37
+ } else {
38
+ pendingWrites.current.push(data);
39
+ }
40
+ },
41
+ clear() {
42
+ termRef.current?.clear();
43
+ },
44
+ }));
45
+
46
+ const cleanupRef = useRef<(() => void) | null>(null);
47
+
48
+ useEffect(() => {
49
+ if (initialized.current || !containerRef.current) return;
50
+ initialized.current = true;
51
+ let disposed = false;
52
+
53
+ (async () => {
54
+ // Dynamic imports (including xterm.css for proper rendering)
55
+ const [xtermModule, fitModule] = await Promise.all([
56
+ import("xterm"),
57
+ import("xterm-addon-fit"),
58
+ import("xterm/css/xterm.css"),
59
+ ]);
60
+
61
+ if (disposed) return;
62
+
63
+ Terminal = xtermModule.Terminal;
64
+ FitAddon = fitModule.FitAddon;
65
+
66
+ const term = new Terminal({
67
+ theme: {
68
+ background: "#080808",
69
+ foreground: "#e0e0e0",
70
+ cursor: "#e0e0e0",
71
+ selectionBackground: "#ffffff30",
72
+ },
73
+ fontSize: 13,
74
+ fontFamily: '"SF Mono", Menlo, Monaco, "Courier New", monospace',
75
+ cursorBlink: true,
76
+ allowProposedApi: true,
77
+ });
78
+
79
+ const fitAddon = new FitAddon();
80
+ term.loadAddon(fitAddon);
81
+
82
+ term.open(containerRef.current!);
83
+ fitAddon.fit();
84
+
85
+ term.onData((data: string) => onDataRef.current(data));
86
+ term.onResize(({ cols, rows }: { cols: number; rows: number }) => {
87
+ onResizeRef.current(cols, rows);
88
+ });
89
+
90
+ termRef.current = term;
91
+ fitAddonRef.current = fitAddon;
92
+
93
+ // Flush any writes that arrived before xterm was ready
94
+ for (const data of pendingWrites.current) {
95
+ term.write(data);
96
+ }
97
+ pendingWrites.current = [];
98
+
99
+ // Initial resize report
100
+ onResizeRef.current(term.cols, term.rows);
101
+
102
+ // ResizeObserver for container
103
+ const observer = new ResizeObserver(() => {
104
+ fitAddon.fit();
105
+ });
106
+ observer.observe(containerRef.current!);
107
+
108
+ cleanupRef.current = () => {
109
+ observer.disconnect();
110
+ term.dispose();
111
+ };
112
+ })();
113
+
114
+ return () => {
115
+ disposed = true;
116
+ initialized.current = false; // Allow re-init after HMR
117
+ termRef.current = null; // Ensure writes go to pendingWrites until re-init
118
+ cleanupRef.current?.();
119
+ };
120
+ }, []);
121
+
122
+ return (
123
+ <div
124
+ ref={containerRef}
125
+ className="terminal-container w-full h-full bg-[#080808]"
126
+ />
127
+ );
128
+ }
129
+ );
@@ -0,0 +1,37 @@
1
+ "use client";
2
+
3
+ import * as React from "react";
4
+ import { cva, type VariantProps } from "class-variance-authority";
5
+ import { cn } from "@/lib/utils";
6
+
7
+ const badgeVariants = cva(
8
+ "inline-flex items-center rounded-full border px-2.5 py-0.5 text-xs font-semibold transition-colors focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2",
9
+ {
10
+ variants: {
11
+ variant: {
12
+ default:
13
+ "border-transparent bg-primary text-primary-foreground hover:bg-primary/80",
14
+ secondary:
15
+ "border-transparent bg-secondary text-secondary-foreground hover:bg-secondary/80",
16
+ destructive:
17
+ "border-transparent bg-destructive text-destructive-foreground hover:bg-destructive/80",
18
+ outline: "text-foreground",
19
+ },
20
+ },
21
+ defaultVariants: {
22
+ variant: "default",
23
+ },
24
+ }
25
+ );
26
+
27
+ export interface BadgeProps
28
+ extends React.HTMLAttributes<HTMLDivElement>,
29
+ VariantProps<typeof badgeVariants> {}
30
+
31
+ function Badge({ className, variant, ...props }: BadgeProps) {
32
+ return (
33
+ <div className={cn(badgeVariants({ variant }), className)} {...props} />
34
+ );
35
+ }
36
+
37
+ export { Badge, badgeVariants };
@@ -0,0 +1,55 @@
1
+ "use client";
2
+
3
+ import * as React from "react";
4
+ import { cva, type VariantProps } from "class-variance-authority";
5
+ import { cn } from "@/lib/utils";
6
+
7
+ const buttonVariants = cva(
8
+ "inline-flex items-center justify-center whitespace-nowrap rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50",
9
+ {
10
+ variants: {
11
+ variant: {
12
+ default: "bg-primary text-primary-foreground hover:bg-primary/90",
13
+ destructive:
14
+ "bg-destructive text-destructive-foreground hover:bg-destructive/90",
15
+ outline:
16
+ "border border-input bg-background hover:bg-accent hover:text-accent-foreground",
17
+ secondary:
18
+ "bg-secondary text-secondary-foreground hover:bg-secondary/80",
19
+ ghost: "hover:bg-accent hover:text-accent-foreground",
20
+ link: "text-primary underline-offset-4 hover:underline",
21
+ },
22
+ size: {
23
+ default: "h-10 px-4 py-2",
24
+ sm: "h-9 rounded-md px-3",
25
+ lg: "h-11 rounded-md px-8",
26
+ icon: "h-10 w-10",
27
+ },
28
+ },
29
+ defaultVariants: {
30
+ variant: "default",
31
+ size: "default",
32
+ },
33
+ }
34
+ );
35
+
36
+ export interface ButtonProps
37
+ extends React.ButtonHTMLAttributes<HTMLButtonElement>,
38
+ VariantProps<typeof buttonVariants> {
39
+ asChild?: boolean;
40
+ }
41
+
42
+ const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
43
+ ({ className, variant, size, ...props }, ref) => {
44
+ return (
45
+ <button
46
+ className={cn(buttonVariants({ variant, size, className }))}
47
+ ref={ref}
48
+ {...props}
49
+ />
50
+ );
51
+ }
52
+ );
53
+ Button.displayName = "Button";
54
+
55
+ export { Button, buttonVariants };
@@ -0,0 +1,80 @@
1
+ "use client";
2
+
3
+ import * as React from "react";
4
+ import { cn } from "@/lib/utils";
5
+
6
+ const Card = React.forwardRef<
7
+ HTMLDivElement,
8
+ React.HTMLAttributes<HTMLDivElement>
9
+ >(({ className, ...props }, ref) => (
10
+ <div
11
+ ref={ref}
12
+ className={cn(
13
+ "rounded-lg border bg-card text-card-foreground shadow-sm",
14
+ className
15
+ )}
16
+ {...props}
17
+ />
18
+ ));
19
+ Card.displayName = "Card";
20
+
21
+ const CardHeader = React.forwardRef<
22
+ HTMLDivElement,
23
+ React.HTMLAttributes<HTMLDivElement>
24
+ >(({ className, ...props }, ref) => (
25
+ <div
26
+ ref={ref}
27
+ className={cn("flex flex-col space-y-1.5 p-6", className)}
28
+ {...props}
29
+ />
30
+ ));
31
+ CardHeader.displayName = "CardHeader";
32
+
33
+ const CardTitle = React.forwardRef<
34
+ HTMLParagraphElement,
35
+ React.HTMLAttributes<HTMLHeadingElement>
36
+ >(({ className, ...props }, ref) => (
37
+ <h3
38
+ ref={ref}
39
+ className={cn(
40
+ "text-2xl font-semibold leading-none tracking-tight",
41
+ className
42
+ )}
43
+ {...props}
44
+ />
45
+ ));
46
+ CardTitle.displayName = "CardTitle";
47
+
48
+ const CardDescription = React.forwardRef<
49
+ HTMLParagraphElement,
50
+ React.HTMLAttributes<HTMLParagraphElement>
51
+ >(({ className, ...props }, ref) => (
52
+ <p
53
+ ref={ref}
54
+ className={cn("text-sm text-muted-foreground", className)}
55
+ {...props}
56
+ />
57
+ ));
58
+ CardDescription.displayName = "CardDescription";
59
+
60
+ const CardContent = React.forwardRef<
61
+ HTMLDivElement,
62
+ React.HTMLAttributes<HTMLDivElement>
63
+ >(({ className, ...props }, ref) => (
64
+ <div ref={ref} className={cn("p-6 pt-0", className)} {...props} />
65
+ ));
66
+ CardContent.displayName = "CardContent";
67
+
68
+ const CardFooter = React.forwardRef<
69
+ HTMLDivElement,
70
+ React.HTMLAttributes<HTMLDivElement>
71
+ >(({ className, ...props }, ref) => (
72
+ <div
73
+ ref={ref}
74
+ className={cn("flex items-center p-6 pt-0", className)}
75
+ {...props}
76
+ />
77
+ ));
78
+ CardFooter.displayName = "CardFooter";
79
+
80
+ export { Card, CardHeader, CardFooter, CardTitle, CardDescription, CardContent };
@@ -0,0 +1,26 @@
1
+ "use client";
2
+
3
+ import * as React from "react";
4
+ import { cn } from "@/lib/utils";
5
+
6
+ export interface InputProps
7
+ extends React.InputHTMLAttributes<HTMLInputElement> {}
8
+
9
+ const Input = React.forwardRef<HTMLInputElement, InputProps>(
10
+ ({ className, type, ...props }, ref) => {
11
+ return (
12
+ <input
13
+ type={type}
14
+ className={cn(
15
+ "flex h-10 w-full rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background file:border-0 file:bg-transparent file:text-sm file:font-medium placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50",
16
+ className
17
+ )}
18
+ ref={ref}
19
+ {...props}
20
+ />
21
+ );
22
+ }
23
+ );
24
+ Input.displayName = "Input";
25
+
26
+ export { Input };
@@ -0,0 +1,52 @@
1
+ "use client";
2
+
3
+ import * as React from "react";
4
+ import { cn } from "@/lib/utils";
5
+
6
+ export interface ScrollAreaProps extends React.HTMLAttributes<HTMLDivElement> {
7
+ orientation?: "vertical" | "horizontal" | "both";
8
+ }
9
+
10
+ const ScrollArea = React.forwardRef<HTMLDivElement, ScrollAreaProps>(
11
+ ({ className, orientation = "vertical", children, ...props }, ref) => {
12
+ return (
13
+ <div
14
+ ref={ref}
15
+ className={cn(
16
+ "relative",
17
+ orientation === "vertical" && "overflow-y-auto overflow-x-hidden",
18
+ orientation === "horizontal" && "overflow-x-auto overflow-y-hidden",
19
+ orientation === "both" && "overflow-auto",
20
+ className
21
+ )}
22
+ {...props}
23
+ >
24
+ {children}
25
+ </div>
26
+ );
27
+ }
28
+ );
29
+ ScrollArea.displayName = "ScrollArea";
30
+
31
+ const ScrollBar = React.forwardRef<
32
+ HTMLDivElement,
33
+ React.HTMLAttributes<HTMLDivElement> & {
34
+ orientation?: "vertical" | "horizontal";
35
+ }
36
+ >(({ className, orientation = "vertical", ...props }, ref) => (
37
+ <div
38
+ ref={ref}
39
+ className={cn(
40
+ "flex touch-none select-none transition-colors",
41
+ orientation === "vertical" &&
42
+ "h-full w-2.5 border-l border-l-transparent p-[1px]",
43
+ orientation === "horizontal" &&
44
+ "h-2.5 flex-col border-t border-t-transparent p-[1px]",
45
+ className
46
+ )}
47
+ {...props}
48
+ />
49
+ ));
50
+ ScrollBar.displayName = "ScrollBar";
51
+
52
+ export { ScrollArea, ScrollBar };
@@ -0,0 +1,31 @@
1
+ "use client";
2
+
3
+ import * as React from "react";
4
+ import { cn } from "@/lib/utils";
5
+
6
+ export interface SeparatorProps extends React.HTMLAttributes<HTMLDivElement> {
7
+ orientation?: "horizontal" | "vertical";
8
+ decorative?: boolean;
9
+ }
10
+
11
+ const Separator = React.forwardRef<HTMLDivElement, SeparatorProps>(
12
+ (
13
+ { className, orientation = "horizontal", decorative = true, ...props },
14
+ ref
15
+ ) => (
16
+ <div
17
+ ref={ref}
18
+ role={decorative ? "none" : "separator"}
19
+ aria-orientation={decorative ? undefined : orientation}
20
+ className={cn(
21
+ "shrink-0 bg-border",
22
+ orientation === "horizontal" ? "h-[1px] w-full" : "h-full w-[1px]",
23
+ className
24
+ )}
25
+ {...props}
26
+ />
27
+ )
28
+ );
29
+ Separator.displayName = "Separator";
30
+
31
+ export { Separator };
@@ -0,0 +1,25 @@
1
+ "use client";
2
+
3
+ import * as React from "react";
4
+ import { cn } from "@/lib/utils";
5
+
6
+ export interface TextareaProps
7
+ extends React.TextareaHTMLAttributes<HTMLTextAreaElement> {}
8
+
9
+ const Textarea = React.forwardRef<HTMLTextAreaElement, TextareaProps>(
10
+ ({ className, ...props }, ref) => {
11
+ return (
12
+ <textarea
13
+ className={cn(
14
+ "flex min-h-[80px] w-full rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50",
15
+ className
16
+ )}
17
+ ref={ref}
18
+ {...props}
19
+ />
20
+ );
21
+ }
22
+ );
23
+ Textarea.displayName = "Textarea";
24
+
25
+ export { Textarea };