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.
- package/.claude/settings.local.json +10 -0
- package/LICENSE +21 -0
- package/README.md +99 -0
- package/README.zh-TW.md +99 -0
- package/bin/cdb.ts +60 -0
- package/bun.lock +1612 -0
- package/bunfig.toml +4 -0
- package/components.json +20 -0
- package/next.config.ts +19 -0
- package/package.json +62 -0
- package/postcss.config.mjs +9 -0
- package/prompts/pm-system.md +61 -0
- package/prompts/rd-system.md +68 -0
- package/prompts/sec-system.md +93 -0
- package/prompts/test-system.md +71 -0
- package/prompts/ui-system.md +72 -0
- package/server.ts +118 -0
- package/sql.js.d.ts +33 -0
- package/src/__tests__/api/usage/route.test.ts +193 -0
- package/src/__tests__/components/layout/TopNav.test.tsx +155 -0
- package/src/__tests__/components/layout/UsageIndicator.test.tsx +503 -0
- package/src/__tests__/hooks/useUsage.test.tsx +174 -0
- package/src/__tests__/lib/usage/get-token.test.ts +117 -0
- package/src/__tests__/react-sanity.test.tsx +14 -0
- package/src/__tests__/sanity.test.ts +7 -0
- package/src/__tests__/setup.ts +1 -0
- package/src/app/api/health/route.ts +8 -0
- package/src/app/api/usage/route.ts +86 -0
- package/src/app/api/workflows/[id]/route.ts +17 -0
- package/src/app/api/workflows/route.ts +14 -0
- package/src/app/globals.css +74 -0
- package/src/app/history/page.tsx +15 -0
- package/src/app/layout.tsx +24 -0
- package/src/app/page.tsx +112 -0
- package/src/components/agent/AgentCard.tsx +117 -0
- package/src/components/agent/AgentCardGrid.tsx +14 -0
- package/src/components/agent/AgentOutput.tsx +87 -0
- package/src/components/agent/AgentStatusBadge.tsx +20 -0
- package/src/components/events/EventLog.tsx +65 -0
- package/src/components/events/EventLogItem.tsx +39 -0
- package/src/components/history/HistoryTable.tsx +105 -0
- package/src/components/layout/DashboardShell.tsx +12 -0
- package/src/components/layout/TopNav.tsx +86 -0
- package/src/components/layout/UsageIndicator.tsx +163 -0
- package/src/components/pipeline/PipelineBar.tsx +59 -0
- package/src/components/pipeline/PipelineNode.tsx +55 -0
- package/src/components/terminal/TerminalPanel.tsx +138 -0
- package/src/components/terminal/XTermRenderer.tsx +129 -0
- package/src/components/ui/badge.tsx +37 -0
- package/src/components/ui/button.tsx +55 -0
- package/src/components/ui/card.tsx +80 -0
- package/src/components/ui/input.tsx +26 -0
- package/src/components/ui/scroll-area.tsx +52 -0
- package/src/components/ui/separator.tsx +31 -0
- package/src/components/ui/textarea.tsx +25 -0
- package/src/components/ui/tooltip.tsx +73 -0
- package/src/components/workflow/WorkflowLauncher.tsx +102 -0
- package/src/hooks/useAgentStream.ts +27 -0
- package/src/hooks/useAutoScroll.ts +24 -0
- package/src/hooks/useUsage.ts +66 -0
- package/src/hooks/useWebSocket.ts +289 -0
- package/src/lib/agents/prompts.ts +341 -0
- package/src/lib/db/connection.ts +263 -0
- package/src/lib/db/queries.ts +257 -0
- package/src/lib/db/schema.ts +39 -0
- package/src/lib/output-buffer.ts +41 -0
- package/src/lib/terminal/pty-manager.ts +106 -0
- package/src/lib/usage/get-token.ts +48 -0
- package/src/lib/utils.ts +6 -0
- package/src/lib/websocket/connection-manager.ts +71 -0
- package/src/lib/websocket/protocol.ts +90 -0
- package/src/lib/websocket/server.ts +231 -0
- package/src/lib/workflow/agent-runner.ts +254 -0
- package/src/lib/workflow/context-builder.ts +62 -0
- package/src/lib/workflow/engine.ts +310 -0
- package/src/lib/workflow/pipeline.ts +28 -0
- package/src/lib/workflow/types.ts +111 -0
- package/src/stores/agentStore.ts +152 -0
- package/src/stores/eventStore.ts +35 -0
- package/src/stores/terminalStore.ts +20 -0
- package/src/stores/uiStore.ts +35 -0
- package/src/stores/workflowStore.ts +57 -0
- package/src/types/css.d.ts +4 -0
- package/src/types/index.ts +12 -0
- package/tailwind.config.ts +65 -0
- package/tsconfig.json +25 -0
- package/tsconfig.server.json +21 -0
- 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 };
|