feat: redesign terminal UI with theme support and retro aesthetic
- Add terminal-chat-interface component with dual-panel layout - Implement light/dark mode with next-themes - Reorganize shadcn components to shadcn-io subdirectory - Add custom retro icons (security, terminal, bot, etc.) - Update color scheme with oklch values for both themes - Add theme toggle and Gitea repository link - Include corner bracket accents and grid/scan line effects - Fix hydration mismatch for session time display
This commit is contained in:
parent
8cbc9538ca
commit
074c79f302
6
.gitignore
vendored
6
.gitignore
vendored
@ -166,6 +166,12 @@ Thumbs.db
|
||||
!.vscode/settings.json
|
||||
!.vscode/tasks.json
|
||||
!.vscode/launch.json
|
||||
.cursorrules
|
||||
.cursorrules/*
|
||||
!.cursorrules/.gitignore
|
||||
.cursor/*
|
||||
.cursor/*/*
|
||||
.cursor
|
||||
|
||||
# Turborepo cache (if you introduce turbo later)
|
||||
.turbo/
|
||||
|
||||
@ -15,6 +15,8 @@
|
||||
"@icons-pack/react-simple-icons": "^13.8.0",
|
||||
"@opennextjs/cloudflare": "^1.3.0",
|
||||
"@radix-ui/react-alert-dialog": "^1.1.15",
|
||||
"@radix-ui/react-avatar": "^1.1.10",
|
||||
"@radix-ui/react-collapsible": "^1.1.12",
|
||||
"@radix-ui/react-dialog": "^1.1.15",
|
||||
"@radix-ui/react-label": "^2.1.7",
|
||||
"@radix-ui/react-scroll-area": "^1.2.10",
|
||||
|
||||
86
bandit-runner-app/pnpm-lock.yaml
generated
86
bandit-runner-app/pnpm-lock.yaml
generated
@ -17,6 +17,12 @@ importers:
|
||||
'@radix-ui/react-alert-dialog':
|
||||
specifier: ^1.1.15
|
||||
version: 1.1.15(@types/react-dom@19.2.1(@types/react@19.2.2))(@types/react@19.2.2)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
|
||||
'@radix-ui/react-avatar':
|
||||
specifier: ^1.1.10
|
||||
version: 1.1.10(@types/react-dom@19.2.1(@types/react@19.2.2))(@types/react@19.2.2)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
|
||||
'@radix-ui/react-collapsible':
|
||||
specifier: ^1.1.12
|
||||
version: 1.1.12(@types/react-dom@19.2.1(@types/react@19.2.2))(@types/react@19.2.2)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
|
||||
'@radix-ui/react-dialog':
|
||||
specifier: ^1.1.15
|
||||
version: 1.1.15(@types/react-dom@19.2.1(@types/react@19.2.2))(@types/react@19.2.2)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
|
||||
@ -1219,6 +1225,32 @@ packages:
|
||||
'@types/react-dom':
|
||||
optional: true
|
||||
|
||||
'@radix-ui/react-avatar@1.1.10':
|
||||
resolution: {integrity: sha512-V8piFfWapM5OmNCXTzVQY+E1rDa53zY+MQ4Y7356v4fFz6vqCyUtIz2rUD44ZEdwg78/jKmMJHj07+C/Z/rcog==}
|
||||
peerDependencies:
|
||||
'@types/react': '*'
|
||||
'@types/react-dom': '*'
|
||||
react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
|
||||
react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
|
||||
peerDependenciesMeta:
|
||||
'@types/react':
|
||||
optional: true
|
||||
'@types/react-dom':
|
||||
optional: true
|
||||
|
||||
'@radix-ui/react-collapsible@1.1.12':
|
||||
resolution: {integrity: sha512-Uu+mSh4agx2ib1uIGPP4/CKNULyajb3p92LsVXmH2EHVMTfZWpll88XJ0j4W0z3f8NK1eYl1+Mf/szHPmcHzyA==}
|
||||
peerDependencies:
|
||||
'@types/react': '*'
|
||||
'@types/react-dom': '*'
|
||||
react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
|
||||
react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
|
||||
peerDependenciesMeta:
|
||||
'@types/react':
|
||||
optional: true
|
||||
'@types/react-dom':
|
||||
optional: true
|
||||
|
||||
'@radix-ui/react-collection@1.1.7':
|
||||
resolution: {integrity: sha512-Fh9rGN0MoI4ZFUNyfFVNU4y9LUz93u9/0K+yLgA2bwRojxM8JU1DyvvMBabnZPBgMWREAJvU2jjVzq+LrFUglw==}
|
||||
peerDependencies:
|
||||
@ -1504,6 +1536,15 @@ packages:
|
||||
'@types/react':
|
||||
optional: true
|
||||
|
||||
'@radix-ui/react-use-is-hydrated@0.1.0':
|
||||
resolution: {integrity: sha512-U+UORVEq+cTnRIaostJv9AGdV3G6Y+zbVd+12e18jQ5A3c0xL03IhnHuiU4UV69wolOQp5GfR58NW/EgdQhwOA==}
|
||||
peerDependencies:
|
||||
'@types/react': '*'
|
||||
react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
|
||||
peerDependenciesMeta:
|
||||
'@types/react':
|
||||
optional: true
|
||||
|
||||
'@radix-ui/react-use-layout-effect@1.1.1':
|
||||
resolution: {integrity: sha512-RbJRS4UWQFkzHTTwVymMTUv8EqYhOp8dOOviLj2ugtTiXRaRQS7GLGxZTLL1jWhMeoSCf5zmcZkqTl9IiYfXcQ==}
|
||||
peerDependencies:
|
||||
@ -4548,6 +4589,11 @@ packages:
|
||||
peerDependencies:
|
||||
react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0
|
||||
|
||||
use-sync-external-store@1.6.0:
|
||||
resolution: {integrity: sha512-Pp6GSwGP/NrPIrxVFAIkOQeyw8lFenOHijQWkUTrDvrF4ALqylP2C/KCkeS9dpUM3KvYRQhna5vt7IL95+ZQ9w==}
|
||||
peerDependencies:
|
||||
react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0
|
||||
|
||||
utils-merge@1.0.1:
|
||||
resolution: {integrity: sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==}
|
||||
engines: {node: '>= 0.4.0'}
|
||||
@ -6308,6 +6354,35 @@ snapshots:
|
||||
'@types/react': 19.2.2
|
||||
'@types/react-dom': 19.2.1(@types/react@19.2.2)
|
||||
|
||||
'@radix-ui/react-avatar@1.1.10(@types/react-dom@19.2.1(@types/react@19.2.2))(@types/react@19.2.2)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)':
|
||||
dependencies:
|
||||
'@radix-ui/react-context': 1.1.2(@types/react@19.2.2)(react@19.1.0)
|
||||
'@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.1(@types/react@19.2.2))(@types/react@19.2.2)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
|
||||
'@radix-ui/react-use-callback-ref': 1.1.1(@types/react@19.2.2)(react@19.1.0)
|
||||
'@radix-ui/react-use-is-hydrated': 0.1.0(@types/react@19.2.2)(react@19.1.0)
|
||||
'@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.2.2)(react@19.1.0)
|
||||
react: 19.1.0
|
||||
react-dom: 19.1.0(react@19.1.0)
|
||||
optionalDependencies:
|
||||
'@types/react': 19.2.2
|
||||
'@types/react-dom': 19.2.1(@types/react@19.2.2)
|
||||
|
||||
'@radix-ui/react-collapsible@1.1.12(@types/react-dom@19.2.1(@types/react@19.2.2))(@types/react@19.2.2)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)':
|
||||
dependencies:
|
||||
'@radix-ui/primitive': 1.1.3
|
||||
'@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.2)(react@19.1.0)
|
||||
'@radix-ui/react-context': 1.1.2(@types/react@19.2.2)(react@19.1.0)
|
||||
'@radix-ui/react-id': 1.1.1(@types/react@19.2.2)(react@19.1.0)
|
||||
'@radix-ui/react-presence': 1.1.5(@types/react-dom@19.2.1(@types/react@19.2.2))(@types/react@19.2.2)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
|
||||
'@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.1(@types/react@19.2.2))(@types/react@19.2.2)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
|
||||
'@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.2.2)(react@19.1.0)
|
||||
'@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.2.2)(react@19.1.0)
|
||||
react: 19.1.0
|
||||
react-dom: 19.1.0(react@19.1.0)
|
||||
optionalDependencies:
|
||||
'@types/react': 19.2.2
|
||||
'@types/react-dom': 19.2.1(@types/react@19.2.2)
|
||||
|
||||
'@radix-ui/react-collection@1.1.7(@types/react-dom@19.2.1(@types/react@19.2.2))(@types/react@19.2.2)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)':
|
||||
dependencies:
|
||||
'@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.2)(react@19.1.0)
|
||||
@ -6591,6 +6666,13 @@ snapshots:
|
||||
optionalDependencies:
|
||||
'@types/react': 19.2.2
|
||||
|
||||
'@radix-ui/react-use-is-hydrated@0.1.0(@types/react@19.2.2)(react@19.1.0)':
|
||||
dependencies:
|
||||
react: 19.1.0
|
||||
use-sync-external-store: 1.6.0(react@19.1.0)
|
||||
optionalDependencies:
|
||||
'@types/react': 19.2.2
|
||||
|
||||
'@radix-ui/react-use-layout-effect@1.1.1(@types/react@19.2.2)(react@19.1.0)':
|
||||
dependencies:
|
||||
react: 19.1.0
|
||||
@ -10466,6 +10548,10 @@ snapshots:
|
||||
dependencies:
|
||||
react: 19.1.0
|
||||
|
||||
use-sync-external-store@1.6.0(react@19.1.0):
|
||||
dependencies:
|
||||
react: 19.1.0
|
||||
|
||||
utils-merge@1.0.1: {}
|
||||
|
||||
uuid@9.0.1: {}
|
||||
|
||||
@ -3,180 +3,165 @@
|
||||
|
||||
@custom-variant dark (&:is(.dark *));
|
||||
|
||||
:root {
|
||||
--background: oklch(0.98 0 0);
|
||||
--foreground: oklch(0.15 0 0);
|
||||
--card: oklch(0.99 0 0);
|
||||
--card-foreground: oklch(0.15 0 0);
|
||||
--popover: oklch(0.99 0 0);
|
||||
--popover-foreground: oklch(0.15 0 0);
|
||||
--primary: oklch(0.35 0.15 250);
|
||||
--primary-foreground: oklch(0.98 0 0);
|
||||
--secondary: oklch(0.92 0 0);
|
||||
--secondary-foreground: oklch(0.15 0 0);
|
||||
--muted: oklch(0.94 0 0);
|
||||
--muted-foreground: oklch(0.50 0 0);
|
||||
--accent: oklch(0.88 0.08 250);
|
||||
--accent-foreground: oklch(0.25 0.15 250);
|
||||
--destructive: oklch(0.55 0.22 25);
|
||||
--destructive-foreground: oklch(0.98 0 0);
|
||||
--border: oklch(0.88 0 0);
|
||||
--input: oklch(0.92 0 0);
|
||||
--ring: oklch(0.35 0.15 250);
|
||||
--chart-1: oklch(0.81 0.17 75.35);
|
||||
--chart-2: oklch(0.55 0.22 264.53);
|
||||
--chart-3: oklch(0.72 0 0);
|
||||
--chart-4: oklch(0.92 0 0);
|
||||
--chart-5: oklch(0.56 0 0);
|
||||
--sidebar: oklch(0.99 0 0);
|
||||
--sidebar-foreground: oklch(0.15 0 0);
|
||||
--sidebar-primary: oklch(0.35 0.15 250);
|
||||
--sidebar-primary-foreground: oklch(0.98 0 0);
|
||||
--sidebar-accent: oklch(0.92 0 0);
|
||||
--sidebar-accent-foreground: oklch(0.15 0 0);
|
||||
--sidebar-border: oklch(0.92 0 0);
|
||||
--sidebar-ring: oklch(0.35 0.15 250);
|
||||
--font-sans: Geist, sans-serif;
|
||||
--font-serif: Georgia, serif;
|
||||
--font-mono: Geist Mono, monospace;
|
||||
--radius: 0.5rem;
|
||||
--shadow-x: 0px;
|
||||
--shadow-y: 1px;
|
||||
--shadow-blur: 2px;
|
||||
--shadow-spread: 0px;
|
||||
--shadow-opacity: 0.18;
|
||||
--shadow-color: hsl(0 0% 0%);
|
||||
--shadow-2xs: 0px 1px 2px 0px hsl(0 0% 0% / 0.09);
|
||||
--shadow-xs: 0px 1px 2px 0px hsl(0 0% 0% / 0.09);
|
||||
--shadow-sm: 0px 1px 2px 0px hsl(0 0% 0% / 0.18), 0px 1px 2px -1px hsl(0 0% 0% / 0.18);
|
||||
--shadow: 0px 1px 2px 0px hsl(0 0% 0% / 0.18), 0px 1px 2px -1px hsl(0 0% 0% / 0.18);
|
||||
--shadow-md: 0px 1px 2px 0px hsl(0 0% 0% / 0.18), 0px 2px 4px -1px hsl(0 0% 0% / 0.18);
|
||||
--shadow-lg: 0px 1px 2px 0px hsl(0 0% 0% / 0.18), 0px 4px 6px -1px hsl(0 0% 0% / 0.18);
|
||||
--shadow-xl: 0px 1px 2px 0px hsl(0 0% 0% / 0.18), 0px 8px 10px -1px hsl(0 0% 0% / 0.18);
|
||||
--shadow-2xl: 0px 1px 2px 0px hsl(0 0% 0% / 0.45);
|
||||
--tracking-normal: 0em;
|
||||
--spacing: 0.25rem;
|
||||
}
|
||||
|
||||
.dark {
|
||||
--background: oklch(0.08 0.01 250);
|
||||
--foreground: oklch(0.90 0.05 200);
|
||||
--card: oklch(0.12 0.01 250);
|
||||
--card-foreground: oklch(0.90 0.05 200);
|
||||
--popover: oklch(0.14 0.01 250);
|
||||
--popover-foreground: oklch(0.90 0.05 200);
|
||||
--primary: oklch(0.70 0.15 195);
|
||||
--primary-foreground: oklch(0.08 0.01 250);
|
||||
--secondary: oklch(0.22 0.01 250);
|
||||
--secondary-foreground: oklch(0.90 0.05 200);
|
||||
--muted: oklch(0.20 0.01 250);
|
||||
--muted-foreground: oklch(0.55 0.03 200);
|
||||
--accent: oklch(0.28 0.05 250);
|
||||
--accent-foreground: oklch(0.75 0.15 180);
|
||||
--destructive: oklch(0.60 0.20 25);
|
||||
--destructive-foreground: oklch(0.95 0 0);
|
||||
--border: oklch(0.24 0.02 250);
|
||||
--input: oklch(0.28 0.02 250);
|
||||
--ring: oklch(0.70 0.15 195);
|
||||
--chart-1: oklch(0.81 0.17 75.35);
|
||||
--chart-2: oklch(0.58 0.21 260.84);
|
||||
--chart-3: oklch(0.56 0 0);
|
||||
--chart-4: oklch(0.44 0 0);
|
||||
--chart-5: oklch(0.92 0 0);
|
||||
--sidebar: oklch(0.12 0.01 250);
|
||||
--sidebar-foreground: oklch(0.90 0.05 200);
|
||||
--sidebar-primary: oklch(0.70 0.15 195);
|
||||
--sidebar-primary-foreground: oklch(0.08 0.01 250);
|
||||
--sidebar-accent: oklch(0.28 0.05 250);
|
||||
--sidebar-accent-foreground: oklch(0.75 0.15 180);
|
||||
--sidebar-border: oklch(0.24 0.02 250);
|
||||
--sidebar-ring: oklch(0.70 0.15 195);
|
||||
--font-sans: Geist, sans-serif;
|
||||
--font-serif: Georgia, serif;
|
||||
--font-mono: Geist Mono, monospace;
|
||||
--radius: 0.5rem;
|
||||
--shadow-x: 0px;
|
||||
--shadow-y: 1px;
|
||||
--shadow-blur: 2px;
|
||||
--shadow-spread: 0px;
|
||||
--shadow-opacity: 0.18;
|
||||
--shadow-color: hsl(0 0% 0%);
|
||||
--shadow-2xs: 0px 1px 2px 0px hsl(0 0% 0% / 0.09);
|
||||
--shadow-xs: 0px 1px 2px 0px hsl(0 0% 0% / 0.09);
|
||||
--shadow-sm: 0px 1px 2px 0px hsl(0 0% 0% / 0.18), 0px 1px 2px -1px hsl(0 0% 0% / 0.18);
|
||||
--shadow: 0px 1px 2px 0px hsl(0 0% 0% / 0.18), 0px 1px 2px -1px hsl(0 0% 0% / 0.18);
|
||||
--shadow-md: 0px 1px 2px 0px hsl(0 0% 0% / 0.18), 0px 2px 4px -1px hsl(0 0% 0% / 0.18);
|
||||
--shadow-lg: 0px 1px 2px 0px hsl(0 0% 0% / 0.18), 0px 4px 6px -1px hsl(0 0% 0% / 0.18);
|
||||
--shadow-xl: 0px 1px 2px 0px hsl(0 0% 0% / 0.18), 0px 8px 10px -1px hsl(0 0% 0% / 0.18);
|
||||
--shadow-2xl: 0px 1px 2px 0px hsl(0 0% 0% / 0.45);
|
||||
}
|
||||
|
||||
@theme inline {
|
||||
--color-background: var(--background);
|
||||
--color-foreground: var(--foreground);
|
||||
--font-sans: 'JetBrains Mono', monospace;
|
||||
--font-mono: 'JetBrains Mono', monospace;
|
||||
--font-serif: 'JetBrains Mono', monospace;
|
||||
--radius: 0rem;
|
||||
--tracking-tighter: calc(var(--tracking-normal) - 0.05em);
|
||||
--tracking-tight: calc(var(--tracking-normal) - 0.025em);
|
||||
--tracking-wide: calc(var(--tracking-normal) + 0.025em);
|
||||
--tracking-wider: calc(var(--tracking-normal) + 0.05em);
|
||||
--tracking-widest: calc(var(--tracking-normal) + 0.1em);
|
||||
--tracking-normal: var(--tracking-normal);
|
||||
--shadow-2xl: var(--shadow-2xl);
|
||||
--shadow-xl: var(--shadow-xl);
|
||||
--shadow-lg: var(--shadow-lg);
|
||||
--shadow-md: var(--shadow-md);
|
||||
--shadow: var(--shadow);
|
||||
--shadow-sm: var(--shadow-sm);
|
||||
--shadow-xs: var(--shadow-xs);
|
||||
--shadow-2xs: var(--shadow-2xs);
|
||||
--spacing: var(--spacing);
|
||||
--letter-spacing: var(--letter-spacing);
|
||||
--shadow-offset-y: var(--shadow-offset-y);
|
||||
--shadow-offset-x: var(--shadow-offset-x);
|
||||
--shadow-spread: var(--shadow-spread);
|
||||
--shadow-blur: var(--shadow-blur);
|
||||
--shadow-opacity: var(--shadow-opacity);
|
||||
--color-shadow-color: var(--shadow-color);
|
||||
--color-destructive-foreground: var(--destructive-foreground);
|
||||
--color-sidebar-ring: var(--sidebar-ring);
|
||||
--color-sidebar-border: var(--sidebar-border);
|
||||
--color-sidebar-accent-foreground: var(--sidebar-accent-foreground);
|
||||
--color-sidebar-accent: var(--sidebar-accent);
|
||||
--color-sidebar-primary-foreground: var(--sidebar-primary-foreground);
|
||||
--color-sidebar-primary: var(--sidebar-primary);
|
||||
--color-sidebar-foreground: var(--sidebar-foreground);
|
||||
--color-sidebar: var(--sidebar);
|
||||
--color-chart-5: var(--chart-5);
|
||||
--color-chart-4: var(--chart-4);
|
||||
--color-chart-3: var(--chart-3);
|
||||
--color-chart-2: var(--chart-2);
|
||||
--color-chart-1: var(--chart-1);
|
||||
--color-ring: var(--ring);
|
||||
--color-input: var(--input);
|
||||
--color-border: var(--border);
|
||||
--color-destructive: var(--destructive);
|
||||
--color-accent-foreground: var(--accent-foreground);
|
||||
--color-accent: var(--accent);
|
||||
--color-muted-foreground: var(--muted-foreground);
|
||||
--color-muted: var(--muted);
|
||||
--color-secondary-foreground: var(--secondary-foreground);
|
||||
--color-secondary: var(--secondary);
|
||||
--color-primary-foreground: var(--primary-foreground);
|
||||
--color-primary: var(--primary);
|
||||
--color-popover-foreground: var(--popover-foreground);
|
||||
--color-popover: var(--popover);
|
||||
--color-card-foreground: var(--card-foreground);
|
||||
--color-card: var(--card);
|
||||
--color-card-foreground: var(--card-foreground);
|
||||
--color-popover: var(--popover);
|
||||
--color-popover-foreground: var(--popover-foreground);
|
||||
--color-primary: var(--primary);
|
||||
--color-primary-foreground: var(--primary-foreground);
|
||||
--color-secondary: var(--secondary);
|
||||
--color-secondary-foreground: var(--secondary-foreground);
|
||||
--color-muted: var(--muted);
|
||||
--color-muted-foreground: var(--muted-foreground);
|
||||
--color-accent: var(--accent);
|
||||
--color-accent-foreground: var(--accent-foreground);
|
||||
--color-destructive: var(--destructive);
|
||||
--color-destructive-foreground: var(--destructive-foreground);
|
||||
--color-border: var(--border);
|
||||
--color-input: var(--input);
|
||||
--color-ring: var(--ring);
|
||||
--color-chart-1: var(--chart-1);
|
||||
--color-chart-2: var(--chart-2);
|
||||
--color-chart-3: var(--chart-3);
|
||||
--color-chart-4: var(--chart-4);
|
||||
--color-chart-5: var(--chart-5);
|
||||
--color-sidebar: var(--sidebar);
|
||||
--color-sidebar-foreground: var(--sidebar-foreground);
|
||||
--color-sidebar-primary: var(--sidebar-primary);
|
||||
--color-sidebar-primary-foreground: var(--sidebar-primary-foreground);
|
||||
--color-sidebar-accent: var(--sidebar-accent);
|
||||
--color-sidebar-accent-foreground: var(--sidebar-accent-foreground);
|
||||
--color-sidebar-border: var(--sidebar-border);
|
||||
--color-sidebar-ring: var(--sidebar-ring);
|
||||
|
||||
--font-sans: var(--font-sans);
|
||||
--font-mono: var(--font-mono);
|
||||
--font-serif: var(--font-serif);
|
||||
|
||||
--radius-sm: calc(var(--radius) - 4px);
|
||||
--radius-md: calc(var(--radius) - 2px);
|
||||
--radius-lg: var(--radius);
|
||||
--radius-xl: calc(var(--radius) + 4px);
|
||||
}
|
||||
|
||||
:root {
|
||||
--radius: 0rem;
|
||||
--background: oklch(0 0 0);
|
||||
--foreground: oklch(0.8664 0.2948 142.4953);
|
||||
--card: oklch(0.2178 0 0);
|
||||
--card-foreground: oklch(0.8664 0.2948 142.4953);
|
||||
--popover: oklch(0.1448 0 0);
|
||||
--popover-foreground: oklch(0.8664 0.2948 142.4953);
|
||||
--primary: oklch(0.7323 0.2492 142.4953);
|
||||
--primary-foreground: oklch(0 0 0);
|
||||
--secondary: oklch(0.5430 0.1848 142.4953);
|
||||
--secondary-foreground: oklch(1.0000 0 0);
|
||||
--muted: oklch(0.2782 0.0947 142.4953);
|
||||
--muted-foreground: oklch(0.6394 0.2176 142.4953);
|
||||
--accent: oklch(0.3895 0.1325 142.4953);
|
||||
--accent-foreground: oklch(0.8664 0.2948 142.4953);
|
||||
--destructive: oklch(0.6280 0.2577 29.2339);
|
||||
--border: oklch(0.3350 0.1140 142.4953);
|
||||
--input: oklch(0.1448 0 0);
|
||||
--ring: oklch(0.8664 0.2948 142.4953);
|
||||
--chart-1: oklch(0.8664 0.2948 142.4953);
|
||||
--chart-2: oklch(0.6863 0.2335 142.4953);
|
||||
--chart-3: oklch(0.4932 0.1678 142.4953);
|
||||
--chart-4: oklch(0.3895 0.1325 142.4953);
|
||||
--chart-5: oklch(0.2782 0.0947 142.4953);
|
||||
--sidebar: oklch(0.1149 0 0);
|
||||
--sidebar-foreground: oklch(0.8664 0.2948 142.4953);
|
||||
--sidebar-primary: oklch(0.7323 0.2492 142.4953);
|
||||
--sidebar-primary-foreground: oklch(1.0000 0 0);
|
||||
--sidebar-accent: oklch(0.5430 0.1848 142.4953);
|
||||
--sidebar-accent-foreground: oklch(0.8664 0.2948 142.4953);
|
||||
--sidebar-border: oklch(0.3350 0.1140 142.4953);
|
||||
--sidebar-ring: oklch(0.8664 0.2948 142.4953);
|
||||
--destructive-foreground: oklch(1.0000 0 0);
|
||||
--font-sans: 'JetBrains Mono', monospace;
|
||||
--font-serif: 'JetBrains Mono', monospace;
|
||||
--font-mono: 'JetBrains Mono', monospace;
|
||||
--shadow-color: #00ff00;
|
||||
--shadow-opacity: 0.2;
|
||||
--shadow-blur: 0.2rem;
|
||||
--shadow-spread: 0rem;
|
||||
--shadow-offset-x: 0rem;
|
||||
--shadow-offset-y: 0.1rem;
|
||||
--letter-spacing: 0.025em;
|
||||
--spacing: 0.25rem;
|
||||
--shadow-2xs: 0rem 0.1rem 0.2rem 0rem hsl(120 100% 50% / 0.10);
|
||||
--shadow-xs: 0rem 0.1rem 0.2rem 0rem hsl(120 100% 50% / 0.10);
|
||||
--shadow-sm: 0rem 0.1rem 0.2rem 0rem hsl(120 100% 50% / 0.20), 0rem 1px 2px -1px hsl(120 100% 50% / 0.20);
|
||||
--shadow: 0rem 0.1rem 0.2rem 0rem hsl(120 100% 50% / 0.20), 0rem 1px 2px -1px hsl(120 100% 50% / 0.20);
|
||||
--shadow-md: 0rem 0.1rem 0.2rem 0rem hsl(120 100% 50% / 0.20), 0rem 2px 4px -1px hsl(120 100% 50% / 0.20);
|
||||
--shadow-lg: 0rem 0.1rem 0.2rem 0rem hsl(120 100% 50% / 0.20), 0rem 4px 6px -1px hsl(120 100% 50% / 0.20);
|
||||
--shadow-xl: 0rem 0.1rem 0.2rem 0rem hsl(120 100% 50% / 0.20), 0rem 8px 10px -1px hsl(120 100% 50% / 0.20);
|
||||
--shadow-2xl: 0rem 0.1rem 0.2rem 0rem hsl(120 100% 50% / 0.50);
|
||||
--tracking-normal: 0.025em;
|
||||
}
|
||||
|
||||
.dark {
|
||||
--background: oklch(0 0 0);
|
||||
--foreground: oklch(1.0000 0 0);
|
||||
--card: oklch(0.1822 0 0);
|
||||
--card-foreground: oklch(0.9706 0.0017 67.8024);
|
||||
--popover: oklch(0.1448 0 0);
|
||||
--popover-foreground: oklch(0.9706 0.0017 67.8024);
|
||||
--primary: oklch(1.0000 0 0);
|
||||
--primary-foreground: oklch(0 0 0);
|
||||
--secondary: oklch(0.9706 0.0017 67.8024);
|
||||
--secondary-foreground: oklch(0 0 0);
|
||||
--muted: oklch(0.3979 0 0);
|
||||
--muted-foreground: oklch(0.8975 0.0042 91.4501);
|
||||
--accent: oklch(0.6829 0.0045 91.4665);
|
||||
--accent-foreground: oklch(1.0000 0 0);
|
||||
--destructive: oklch(0.6280 0.2577 29.2339);
|
||||
--border: oklch(0.4794 0.0129 297.3461);
|
||||
--input: oklch(0.1448 0 0);
|
||||
--ring: oklch(1.0000 0 0);
|
||||
--chart-1: oklch(0.8664 0.2948 142.4953);
|
||||
--chart-2: oklch(0.6863 0.2335 142.4953);
|
||||
--chart-3: oklch(0.4932 0.1678 142.4953);
|
||||
--chart-4: oklch(0.3895 0.1325 142.4953);
|
||||
--chart-5: oklch(0.2782 0.0947 142.4953);
|
||||
--sidebar: oklch(0.1149 0 0);
|
||||
--sidebar-foreground: oklch(0.8664 0.2948 142.4953);
|
||||
--sidebar-primary: oklch(0.7323 0.2492 142.4953);
|
||||
--sidebar-primary-foreground: oklch(1.0000 0 0);
|
||||
--sidebar-accent: oklch(0.5430 0.1848 142.4953);
|
||||
--sidebar-accent-foreground: oklch(0.8664 0.2948 142.4953);
|
||||
--sidebar-border: oklch(0.3350 0.1140 142.4953);
|
||||
--sidebar-ring: oklch(0.8664 0.2948 142.4953);
|
||||
--destructive-foreground: oklch(1.0000 0 0);
|
||||
--radius: 0rem;
|
||||
--font-sans: 'JetBrains Mono', monospace;
|
||||
--font-serif: 'JetBrains Mono', monospace;
|
||||
--font-mono: 'JetBrains Mono', monospace;
|
||||
--shadow-color: #00ff00;
|
||||
--shadow-opacity: 0.2;
|
||||
--shadow-blur: 0.2rem;
|
||||
--shadow-spread: 0rem;
|
||||
--shadow-offset-x: 0rem;
|
||||
--shadow-offset-y: 0.1rem;
|
||||
--letter-spacing: 0.025em;
|
||||
--spacing: 0.25rem;
|
||||
--shadow-2xs: 0rem 0.1rem 0.2rem 0rem hsl(120 100% 50% / 0.10);
|
||||
--shadow-xs: 0rem 0.1rem 0.2rem 0rem hsl(120 100% 50% / 0.10);
|
||||
--shadow-sm: 0rem 0.1rem 0.2rem 0rem hsl(120 100% 50% / 0.20), 0rem 1px 2px -1px hsl(120 100% 50% / 0.20);
|
||||
--shadow: 0rem 0.1rem 0.2rem 0rem hsl(120 100% 50% / 0.20), 0rem 1px 2px -1px hsl(120 100% 50% / 0.20);
|
||||
--shadow-md: 0rem 0.1rem 0.2rem 0rem hsl(120 100% 50% / 0.20), 0rem 2px 4px -1px hsl(120 100% 50% / 0.20);
|
||||
--shadow-lg: 0rem 0.1rem 0.2rem 0rem hsl(120 100% 50% / 0.20), 0rem 4px 6px -1px hsl(120 100% 50% / 0.20);
|
||||
--shadow-xl: 0rem 0.1rem 0.2rem 0rem hsl(120 100% 50% / 0.20), 0rem 8px 10px -1px hsl(120 100% 50% / 0.20);
|
||||
--shadow-2xl: 0rem 0.1rem 0.2rem 0rem hsl(120 100% 50% / 0.50);
|
||||
--shadow-2xs: var(--shadow-2xs);
|
||||
--shadow-xs: var(--shadow-xs);
|
||||
--shadow-sm: var(--shadow-sm);
|
||||
--shadow: var(--shadow);
|
||||
--shadow-md: var(--shadow-md);
|
||||
--shadow-lg: var(--shadow-lg);
|
||||
--shadow-xl: var(--shadow-xl);
|
||||
--shadow-2xl: var(--shadow-2xl);
|
||||
}
|
||||
|
||||
@layer base {
|
||||
@ -187,4 +172,30 @@
|
||||
@apply bg-background text-foreground;
|
||||
letter-spacing: var(--tracking-normal);
|
||||
}
|
||||
|
||||
@keyframes scan-lines {
|
||||
0% {
|
||||
transform: translateY(0);
|
||||
}
|
||||
100% {
|
||||
transform: translateY(4px);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes cursor-blink {
|
||||
0%, 49% {
|
||||
opacity: 1;
|
||||
}
|
||||
50%, 100% {
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.animate-scan-lines {
|
||||
animation: scan-lines 0.1s linear infinite;
|
||||
}
|
||||
|
||||
.animate-cursor-blink {
|
||||
animation: cursor-blink 1s step-end infinite;
|
||||
}
|
||||
}
|
||||
@ -1,6 +1,7 @@
|
||||
import type { Metadata } from "next";
|
||||
import { Geist, Geist_Mono } from "next/font/google";
|
||||
import "./globals.css";
|
||||
import { ThemeProvider } from "@/components/theme-provider";
|
||||
|
||||
const geistSans = Geist({
|
||||
variable: "--font-geist-sans",
|
||||
@ -13,8 +14,8 @@ const geistMono = Geist_Mono({
|
||||
});
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: "Create Next App",
|
||||
description: "Generated by create next app",
|
||||
title: "Bandit Runner",
|
||||
description: "Security Automation Console",
|
||||
};
|
||||
|
||||
export default function RootLayout({
|
||||
@ -23,11 +24,18 @@ export default function RootLayout({
|
||||
children: React.ReactNode;
|
||||
}>) {
|
||||
return (
|
||||
<html lang="en">
|
||||
<html lang="en" suppressHydrationWarning>
|
||||
<body
|
||||
className={`${geistSans.variable} ${geistMono.variable} antialiased`}
|
||||
>
|
||||
<ThemeProvider
|
||||
attribute="class"
|
||||
defaultTheme="dark"
|
||||
enableSystem
|
||||
disableTransitionOnChange
|
||||
>
|
||||
{children}
|
||||
</ThemeProvider>
|
||||
</body>
|
||||
</html>
|
||||
);
|
||||
|
||||
@ -1,103 +1,5 @@
|
||||
import Image from "next/image";
|
||||
import { TerminalChatInterface } from "@/components/terminal-chat-interface"
|
||||
|
||||
export default function Home() {
|
||||
return (
|
||||
<div className="font-sans grid grid-rows-[20px_1fr_20px] items-center justify-items-center min-h-screen p-8 pb-20 gap-16 sm:p-20">
|
||||
<main className="flex flex-col gap-[32px] row-start-2 items-center sm:items-start">
|
||||
<Image
|
||||
className="dark:invert"
|
||||
src="/next.svg"
|
||||
alt="Next.js logo"
|
||||
width={180}
|
||||
height={38}
|
||||
priority
|
||||
/>
|
||||
<ol className="font-mono list-inside list-decimal text-sm/6 text-center sm:text-left">
|
||||
<li className="mb-2 tracking-[-.01em]">
|
||||
Get started by editing{" "}
|
||||
<code className="bg-black/[.05] dark:bg-white/[.06] font-mono font-semibold px-1 py-0.5 rounded">
|
||||
src/app/page.tsx
|
||||
</code>
|
||||
.
|
||||
</li>
|
||||
<li className="tracking-[-.01em]">
|
||||
Save and see your changes instantly.
|
||||
</li>
|
||||
</ol>
|
||||
|
||||
<div className="flex gap-4 items-center flex-col sm:flex-row">
|
||||
<a
|
||||
className="rounded-full border border-solid border-transparent transition-colors flex items-center justify-center bg-foreground text-background gap-2 hover:bg-[#383838] dark:hover:bg-[#ccc] font-medium text-sm sm:text-base h-10 sm:h-12 px-4 sm:px-5 sm:w-auto"
|
||||
href="https://vercel.com/new?utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
<Image
|
||||
className="dark:invert"
|
||||
src="/vercel.svg"
|
||||
alt="Vercel logomark"
|
||||
width={20}
|
||||
height={20}
|
||||
/>
|
||||
Deploy now
|
||||
</a>
|
||||
<a
|
||||
className="rounded-full border border-solid border-black/[.08] dark:border-white/[.145] transition-colors flex items-center justify-center hover:bg-[#f2f2f2] dark:hover:bg-[#1a1a1a] hover:border-transparent font-medium text-sm sm:text-base h-10 sm:h-12 px-4 sm:px-5 w-full sm:w-auto md:w-[158px]"
|
||||
href="https://nextjs.org/docs?utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
Read our docs
|
||||
</a>
|
||||
</div>
|
||||
</main>
|
||||
<footer className="row-start-3 flex gap-[24px] flex-wrap items-center justify-center">
|
||||
<a
|
||||
className="flex items-center gap-2 hover:underline hover:underline-offset-4"
|
||||
href="https://nextjs.org/learn?utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
<Image
|
||||
aria-hidden
|
||||
src="/file.svg"
|
||||
alt="File icon"
|
||||
width={16}
|
||||
height={16}
|
||||
/>
|
||||
Learn
|
||||
</a>
|
||||
<a
|
||||
className="flex items-center gap-2 hover:underline hover:underline-offset-4"
|
||||
href="https://vercel.com/templates?framework=next.js&utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
<Image
|
||||
aria-hidden
|
||||
src="/window.svg"
|
||||
alt="Window icon"
|
||||
width={16}
|
||||
height={16}
|
||||
/>
|
||||
Examples
|
||||
</a>
|
||||
<a
|
||||
className="flex items-center gap-2 hover:underline hover:underline-offset-4"
|
||||
href="https://nextjs.org?utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
<Image
|
||||
aria-hidden
|
||||
src="/globe.svg"
|
||||
alt="Globe icon"
|
||||
width={16}
|
||||
height={16}
|
||||
/>
|
||||
Go to nextjs.org →
|
||||
</a>
|
||||
</footer>
|
||||
</div>
|
||||
);
|
||||
return <TerminalChatInterface />
|
||||
}
|
||||
|
||||
117
bandit-runner-app/src/components/retro-icons.tsx
Normal file
117
bandit-runner-app/src/components/retro-icons.tsx
Normal file
@ -0,0 +1,117 @@
|
||||
export function TerminalIcon({ className }: { className?: string }) {
|
||||
return (
|
||||
<svg
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
strokeWidth="2"
|
||||
strokeLinecap="square"
|
||||
strokeLinejoin="miter"
|
||||
className={className}
|
||||
>
|
||||
<rect x="2" y="3" width="20" height="18" />
|
||||
<line x1="2" y1="7" x2="22" y2="7" />
|
||||
<polyline points="6,11 9,14 6,17" />
|
||||
<line x1="12" y1="17" x2="18" y2="17" />
|
||||
</svg>
|
||||
)
|
||||
}
|
||||
|
||||
export function BotIcon({ className }: { className?: string }) {
|
||||
return (
|
||||
<svg
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
strokeWidth="2"
|
||||
strokeLinecap="square"
|
||||
strokeLinejoin="miter"
|
||||
className={className}
|
||||
>
|
||||
<rect x="6" y="8" width="12" height="10" />
|
||||
<rect x="8" y="11" width="2" height="2" />
|
||||
<rect x="14" y="11" width="2" height="2" />
|
||||
<line x1="12" y1="5" x2="12" y2="8" />
|
||||
<circle cx="12" cy="4" r="1" />
|
||||
<line x1="6" y1="18" x2="4" y2="21" />
|
||||
<line x1="18" y1="18" x2="20" y2="21" />
|
||||
<line x1="9" y1="15" x2="15" y2="15" />
|
||||
</svg>
|
||||
)
|
||||
}
|
||||
|
||||
export function DatabaseIcon({ className }: { className?: string }) {
|
||||
return (
|
||||
<svg
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
strokeWidth="2"
|
||||
strokeLinecap="square"
|
||||
strokeLinejoin="miter"
|
||||
className={className}
|
||||
>
|
||||
<ellipse cx="12" cy="5" rx="9" ry="3" />
|
||||
<path d="M3 5v14c0 1.66 4 3 9 3s9-1.34 9-3V5" />
|
||||
<line x1="3" y1="12" x2="21" y2="12" />
|
||||
</svg>
|
||||
)
|
||||
}
|
||||
|
||||
export function SecurityIcon({ className }: { className?: string }) {
|
||||
return (
|
||||
<svg
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
strokeWidth="2"
|
||||
strokeLinecap="square"
|
||||
strokeLinejoin="miter"
|
||||
className={className}
|
||||
>
|
||||
<path d="M12 2L4 6v6c0 5.55 3.84 10.74 9 12 5.16-1.26 9-6.45 9-12V6l-8-4z" />
|
||||
<path d="M9 12l2 2 4-4" />
|
||||
</svg>
|
||||
)
|
||||
}
|
||||
|
||||
export function GitBranchIcon({ className }: { className?: string }) {
|
||||
return (
|
||||
<svg
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
strokeWidth="2"
|
||||
strokeLinecap="square"
|
||||
strokeLinejoin="miter"
|
||||
className={className}
|
||||
>
|
||||
<circle cx="6" cy="6" r="3" />
|
||||
<circle cx="18" cy="18" r="3" />
|
||||
<line x1="6" y1="9" x2="6" y2="15" />
|
||||
<path d="M18 15c0-3-3-4.5-6-4.5" />
|
||||
</svg>
|
||||
)
|
||||
}
|
||||
|
||||
export function ServerIcon({ className }: { className?: string }) {
|
||||
return (
|
||||
<svg
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
strokeWidth="2"
|
||||
strokeLinecap="square"
|
||||
strokeLinejoin="miter"
|
||||
className={className}
|
||||
>
|
||||
<rect x="2" y="2" width="20" height="6" />
|
||||
<rect x="2" y="10" width="20" height="6" />
|
||||
<rect x="2" y="18" width="20" height="4" />
|
||||
<circle cx="6" cy="5" r="1" />
|
||||
<circle cx="6" cy="13" r="1" />
|
||||
<circle cx="6" cy="20" r="1" />
|
||||
</svg>
|
||||
)
|
||||
}
|
||||
|
||||
416
bandit-runner-app/src/components/terminal-chat-interface.tsx
Normal file
416
bandit-runner-app/src/components/terminal-chat-interface.tsx
Normal file
@ -0,0 +1,416 @@
|
||||
"use client"
|
||||
|
||||
import type React from "react"
|
||||
import { useState, useRef, useEffect } from "react"
|
||||
import { Github } from "lucide-react"
|
||||
import { Input } from "@/components/ui/shadcn-io/input"
|
||||
import { ScrollArea } from "@/components/ui/shadcn-io/scroll-area"
|
||||
import { ThemeToggle } from "@/components/theme-toggle"
|
||||
import { SecurityIcon } from "@/components/retro-icons"
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
interface TerminalLine {
|
||||
type: "input" | "output" | "error"
|
||||
content: string
|
||||
timestamp: Date
|
||||
}
|
||||
|
||||
interface ChatMessage {
|
||||
type: "user" | "agent" | "typing"
|
||||
content: string
|
||||
timestamp: Date
|
||||
}
|
||||
|
||||
const SCAN_LINES_OVERLAY =
|
||||
"absolute inset-0 pointer-events-none bg-[linear-gradient(transparent_50%,hsl(var(--primary)/0.03)_50%)] bg-[length:100%_4px] animate-scan-lines"
|
||||
|
||||
const GRID_PATTERN =
|
||||
"absolute inset-0 pointer-events-none opacity-[0.015] bg-[linear-gradient(hsl(var(--primary))_1px,transparent_1px),linear-gradient(90deg,hsl(var(--primary))_1px,transparent_1px)] bg-[size:20px_20px]"
|
||||
|
||||
export function TerminalChatInterface() {
|
||||
const [terminalLines, setTerminalLines] = useState<TerminalLine[]>([
|
||||
{ type: "output", content: "Bandit Runner Console v1.0", timestamp: new Date() },
|
||||
{ type: "output", content: "System initialized. Ready for commands.", timestamp: new Date() },
|
||||
{ type: "output", content: "", timestamp: new Date() },
|
||||
])
|
||||
const [chatMessages, setChatMessages] = useState<ChatMessage[]>([
|
||||
{ type: "agent", content: "Agent ready. Awaiting commands...", timestamp: new Date() },
|
||||
])
|
||||
const [currentCommand, setCurrentCommand] = useState("")
|
||||
const [chatInput, setChatInput] = useState("")
|
||||
const [commandHistory, setCommandHistory] = useState<string[]>([])
|
||||
const [historyIndex, setHistoryIndex] = useState(-1)
|
||||
const [isTyping, setIsTyping] = useState(false)
|
||||
const [sessionTime, setSessionTime] = useState("")
|
||||
const [focusedPanel, setFocusedPanel] = useState<"terminal" | "chat">("terminal")
|
||||
const [mounted, setMounted] = useState(false)
|
||||
|
||||
const terminalEndRef = useRef<HTMLDivElement>(null)
|
||||
const chatEndRef = useRef<HTMLDivElement>(null)
|
||||
const terminalInputRef = useRef<HTMLInputElement>(null)
|
||||
const chatInputRef = useRef<HTMLInputElement>(null)
|
||||
|
||||
useEffect(() => {
|
||||
setMounted(true)
|
||||
setSessionTime(new Date().toLocaleTimeString())
|
||||
terminalInputRef.current?.focus()
|
||||
}, [])
|
||||
|
||||
useEffect(() => {
|
||||
terminalEndRef.current?.scrollIntoView({ behavior: "smooth" })
|
||||
}, [terminalLines])
|
||||
|
||||
useEffect(() => {
|
||||
chatEndRef.current?.scrollIntoView({ behavior: "smooth" })
|
||||
}, [chatMessages])
|
||||
|
||||
const formatTimestamp = (date: Date) => {
|
||||
return date.toLocaleTimeString("en-US", { hour12: false, hour: "2-digit", minute: "2-digit", second: "2-digit" })
|
||||
}
|
||||
|
||||
const handleCommandSubmit = (e: React.FormEvent) => {
|
||||
e.preventDefault()
|
||||
if (!currentCommand.trim()) return
|
||||
|
||||
const command = currentCommand.trim()
|
||||
setCommandHistory((prev) => [...prev, command])
|
||||
setHistoryIndex(-1)
|
||||
|
||||
setTerminalLines((prev) => [
|
||||
...prev,
|
||||
{ type: "input", content: `$ ${command}`, timestamp: new Date() },
|
||||
])
|
||||
|
||||
setTimeout(() => {
|
||||
setTerminalLines((prev) => [
|
||||
...prev,
|
||||
{
|
||||
type: "output",
|
||||
content: `Executing: ${command}...`,
|
||||
timestamp: new Date(),
|
||||
},
|
||||
{
|
||||
type: "output",
|
||||
content: `Command completed successfully.`,
|
||||
timestamp: new Date(),
|
||||
},
|
||||
{ type: "output", content: "", timestamp: new Date() },
|
||||
])
|
||||
}, 100)
|
||||
|
||||
setCurrentCommand("")
|
||||
}
|
||||
|
||||
const handleChatSubmit = (e: React.FormEvent) => {
|
||||
e.preventDefault()
|
||||
if (!chatInput.trim()) return
|
||||
|
||||
const message = chatInput.trim()
|
||||
setChatMessages((prev) => [
|
||||
...prev,
|
||||
{
|
||||
type: "user",
|
||||
content: message,
|
||||
timestamp: new Date(),
|
||||
},
|
||||
])
|
||||
|
||||
setChatInput("")
|
||||
setIsTyping(true)
|
||||
|
||||
setTimeout(() => {
|
||||
setIsTyping(false)
|
||||
setChatMessages((prev) => [
|
||||
...prev,
|
||||
{
|
||||
type: "agent",
|
||||
content: `Processing: "${message}"`,
|
||||
timestamp: new Date(),
|
||||
},
|
||||
])
|
||||
}, 1500)
|
||||
}
|
||||
|
||||
const handleCommandKeyDown = (e: React.KeyboardEvent<HTMLInputElement>) => {
|
||||
if (e.key === "ArrowUp") {
|
||||
e.preventDefault()
|
||||
if (commandHistory.length === 0) return
|
||||
const newIndex = historyIndex === -1 ? commandHistory.length - 1 : Math.max(0, historyIndex - 1)
|
||||
setHistoryIndex(newIndex)
|
||||
setCurrentCommand(commandHistory[newIndex])
|
||||
} else if (e.key === "ArrowDown") {
|
||||
e.preventDefault()
|
||||
if (historyIndex === -1) return
|
||||
const newIndex = historyIndex + 1
|
||||
if (newIndex >= commandHistory.length) {
|
||||
setHistoryIndex(-1)
|
||||
setCurrentCommand("")
|
||||
} else {
|
||||
setHistoryIndex(newIndex)
|
||||
setCurrentCommand(commandHistory[newIndex])
|
||||
}
|
||||
} else if (e.key === "Escape") {
|
||||
setFocusedPanel("chat")
|
||||
chatInputRef.current?.focus()
|
||||
}
|
||||
}
|
||||
|
||||
const handleChatKeyDown = (e: React.KeyboardEvent<HTMLInputElement>) => {
|
||||
if (e.key === "Escape") {
|
||||
setFocusedPanel("terminal")
|
||||
terminalInputRef.current?.focus()
|
||||
}
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
const handleGlobalKeyDown = (e: KeyboardEvent) => {
|
||||
if (e.key === "j" && e.ctrlKey) {
|
||||
e.preventDefault()
|
||||
setFocusedPanel("chat")
|
||||
chatInputRef.current?.focus()
|
||||
} else if (e.key === "k" && e.ctrlKey) {
|
||||
e.preventDefault()
|
||||
setFocusedPanel("terminal")
|
||||
terminalInputRef.current?.focus()
|
||||
}
|
||||
}
|
||||
|
||||
window.addEventListener("keydown", handleGlobalKeyDown)
|
||||
return () => window.removeEventListener("keydown", handleGlobalKeyDown)
|
||||
}, [])
|
||||
|
||||
return (
|
||||
<div className="h-screen w-full bg-background flex flex-col font-mono overflow-hidden p-3 sm:p-6 md:p-8">
|
||||
<div className="w-full max-w-[1900px] mx-auto flex flex-col h-full gap-4 sm:gap-6">
|
||||
{/* Header with corner accents */}
|
||||
<div className="relative flex-shrink-0">
|
||||
{/* Corner brackets */}
|
||||
<div className="absolute -left-1 -top-1 w-6 h-6 border-l-2 border-t-2 border-primary" />
|
||||
<div className="absolute -right-1 -top-1 w-6 h-6 border-r-2 border-t-2 border-primary" />
|
||||
<div className="absolute -left-1 -bottom-1 w-6 h-6 border-l-2 border-b-2 border-primary" />
|
||||
<div className="absolute -right-1 -bottom-1 w-6 h-6 border-r-2 border-b-2 border-primary" />
|
||||
|
||||
<div className="border-l border-r border-border bg-card px-6 sm:px-8 py-3 sm:py-4">
|
||||
<div className="flex items-center justify-between gap-4">
|
||||
<div className="flex items-center gap-3 sm:gap-4">
|
||||
<div className="w-1 h-8 bg-primary" />
|
||||
<SecurityIcon className="w-5 h-5 sm:w-6 sm:h-6 text-primary hidden xs:block" />
|
||||
<div>
|
||||
<div className="text-foreground text-sm sm:text-base font-bold tracking-wider">
|
||||
BANDIT RUNNER
|
||||
</div>
|
||||
<div className="text-muted-foreground text-[10px] sm:text-xs">
|
||||
Security Automation Console
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center gap-2 sm:gap-4 text-[10px] sm:text-xs">
|
||||
{mounted && (
|
||||
<div className="hidden md:flex items-center gap-2">
|
||||
<span className="text-muted-foreground">SESSION</span>
|
||||
<span className="text-primary font-mono">{sessionTime}</span>
|
||||
</div>
|
||||
)}
|
||||
<div className="flex items-center gap-1.5">
|
||||
<div className="w-2 h-2 bg-accent-foreground" />
|
||||
<span className="text-accent-foreground font-bold">ONLINE</span>
|
||||
</div>
|
||||
<a
|
||||
href="https://git.biohazardvfx.com/Nicholai/bandit-runner"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="w-8 h-8 flex items-center justify-center border border-border bg-card text-muted-foreground hover:text-primary hover:border-primary transition-colors"
|
||||
title="View Repository"
|
||||
>
|
||||
<Github className="w-4 h-4" />
|
||||
</a>
|
||||
<ThemeToggle />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Main content area */}
|
||||
<div className="flex-1 grid grid-cols-1 lg:grid-cols-[1.2fr_0.8fr] gap-4 sm:gap-6 min-h-0">
|
||||
{/* Terminal Panel */}
|
||||
<div className="relative flex flex-col h-[55vh] lg:h-auto">
|
||||
{/* Corner accents */}
|
||||
<div className="absolute -left-1 -top-1 w-4 h-4 border-l-2 border-t-2 border-primary z-20" />
|
||||
<div className="absolute -right-1 -top-1 w-4 h-4 border-r-2 border-t-2 border-primary z-20" />
|
||||
|
||||
<div className="flex-1 flex flex-col border border-border bg-card relative overflow-hidden">
|
||||
<div className={GRID_PATTERN} />
|
||||
<div className={SCAN_LINES_OVERLAY} />
|
||||
|
||||
{/* Header */}
|
||||
<div className="relative z-10 flex items-center justify-between px-4 py-2 border-b border-border bg-muted/30">
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="w-1 h-4 bg-primary" />
|
||||
<span className="text-foreground text-xs font-bold tracking-widest">TERMINAL</span>
|
||||
</div>
|
||||
<div className="flex gap-1.5">
|
||||
<div className="w-3 h-3 border border-primary/40" />
|
||||
<div className="w-3 h-3 border border-primary/40" />
|
||||
<div className="w-3 h-3 border border-primary" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Terminal content */}
|
||||
<ScrollArea className="flex-1 p-4 relative z-10">
|
||||
<div className="space-y-1">
|
||||
{terminalLines.map((line, idx) => (
|
||||
<div
|
||||
key={idx}
|
||||
className={cn(
|
||||
"text-xs md:text-sm leading-relaxed flex gap-3 font-mono",
|
||||
line.type === "input" && "text-accent-foreground font-bold",
|
||||
line.type === "output" && "text-foreground/80",
|
||||
line.type === "error" && "text-destructive",
|
||||
)}
|
||||
>
|
||||
{line.content && (
|
||||
<>
|
||||
<span className="text-muted-foreground text-[10px] sm:text-xs flex-shrink-0 w-20">
|
||||
{formatTimestamp(line.timestamp)}
|
||||
</span>
|
||||
<span className="flex-1">{line.content}</span>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
<div ref={terminalEndRef} />
|
||||
</div>
|
||||
</ScrollArea>
|
||||
|
||||
{/* Input area */}
|
||||
<div className="border-t border-border p-3 bg-muted/20 relative z-10">
|
||||
<form onSubmit={handleCommandSubmit} className="flex items-center gap-2">
|
||||
<div className="flex items-center gap-2 text-primary">
|
||||
<div className="w-1 h-4 bg-primary" />
|
||||
<span className="text-sm">$</span>
|
||||
</div>
|
||||
<Input
|
||||
ref={terminalInputRef}
|
||||
value={currentCommand}
|
||||
onChange={(e) => setCurrentCommand(e.target.value)}
|
||||
onKeyDown={handleCommandKeyDown}
|
||||
placeholder="enter command..."
|
||||
className="flex-1 bg-transparent border-0 text-foreground placeholder:text-muted-foreground focus-visible:ring-0 focus-visible:ring-offset-0 font-mono text-sm h-6 px-0 caret-primary"
|
||||
/>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
{/* Footer */}
|
||||
<div className="border-t border-border px-3 py-1.5 bg-muted/30 relative z-10">
|
||||
<div className="flex items-center justify-between text-[10px] text-muted-foreground">
|
||||
<span className="hidden sm:inline">user@bandit-runner</span>
|
||||
<span>↑↓ history • ESC switch panels</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Agent Panel */}
|
||||
<div className="relative flex flex-col h-[40vh] lg:h-auto">
|
||||
{/* Corner accents */}
|
||||
<div className="absolute -left-1 -top-1 w-4 h-4 border-l-2 border-t-2 border-primary z-20" />
|
||||
<div className="absolute -right-1 -top-1 w-4 h-4 border-r-2 border-t-2 border-primary z-20" />
|
||||
|
||||
<div className="flex-1 flex flex-col border border-border bg-card relative overflow-hidden">
|
||||
<div className={GRID_PATTERN} />
|
||||
<div className={SCAN_LINES_OVERLAY} />
|
||||
|
||||
{/* Header */}
|
||||
<div className="relative z-10 flex items-center justify-between px-4 py-2 border-b border-border bg-muted/30">
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="w-1 h-4 bg-accent-foreground" />
|
||||
<span className="text-foreground text-xs font-bold tracking-widest">AGENT</span>
|
||||
</div>
|
||||
<div className="flex gap-1.5">
|
||||
<div className="w-3 h-3 border border-accent-foreground/40" />
|
||||
<div className="w-3 h-3 border border-accent-foreground" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Messages */}
|
||||
<ScrollArea className="flex-1 p-4 relative z-10">
|
||||
<div className="space-y-4">
|
||||
{chatMessages.map((msg, idx) => (
|
||||
<div key={idx} className="space-y-1">
|
||||
<div className="flex items-center gap-2 text-[10px]">
|
||||
<span className="text-muted-foreground font-mono">
|
||||
{formatTimestamp(msg.timestamp)}
|
||||
</span>
|
||||
<div className="h-px flex-1 bg-border" />
|
||||
<span className={cn(
|
||||
"font-bold px-2 py-0.5 border",
|
||||
msg.type === "user"
|
||||
? "text-accent-foreground border-accent-foreground/30"
|
||||
: "text-primary border-primary/30"
|
||||
)}>
|
||||
{msg.type === "user" ? "USER" : "AGENT"}
|
||||
</span>
|
||||
</div>
|
||||
<div className={cn(
|
||||
"text-xs md:text-sm leading-relaxed pl-4 border-l-2 font-mono",
|
||||
msg.type === "user"
|
||||
? "text-accent-foreground border-accent-foreground/30"
|
||||
: "text-foreground/80 border-primary/30"
|
||||
)}>
|
||||
{msg.content}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
{isTyping && (
|
||||
<div className="space-y-1">
|
||||
<div className="flex items-center gap-2 text-[10px]">
|
||||
<span className="text-muted-foreground font-mono">
|
||||
{formatTimestamp(new Date())}
|
||||
</span>
|
||||
<div className="h-px flex-1 bg-border" />
|
||||
<span className="text-primary border border-primary/30 font-bold px-2 py-0.5">
|
||||
AGENT
|
||||
</span>
|
||||
</div>
|
||||
<div className="text-xs md:text-sm text-foreground/60 pl-4 border-l-2 border-primary/30 flex items-center gap-2">
|
||||
<span>Processing</span>
|
||||
<span className="animate-cursor-blink text-primary">▊</span>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
<div ref={chatEndRef} />
|
||||
</div>
|
||||
</ScrollArea>
|
||||
|
||||
{/* Input area */}
|
||||
<div className="border-t border-border p-3 bg-muted/20 relative z-10">
|
||||
<form onSubmit={handleChatSubmit} className="flex items-center gap-2 bg-background/40 px-3 py-2 border border-border/50">
|
||||
<div className="flex items-center gap-2 text-primary">
|
||||
<div className="w-1 h-4 bg-accent-foreground" />
|
||||
<span className="text-sm">›</span>
|
||||
</div>
|
||||
<Input
|
||||
ref={chatInputRef}
|
||||
value={chatInput}
|
||||
onChange={(e) => setChatInput(e.target.value)}
|
||||
onKeyDown={handleChatKeyDown}
|
||||
placeholder="message agent..."
|
||||
className="flex-1 bg-transparent border-0 text-foreground placeholder:text-muted-foreground focus-visible:ring-0 focus-visible:ring-offset-0 font-mono text-sm h-6 px-0 caret-accent-foreground"
|
||||
/>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
{/* Footer */}
|
||||
<div className="border-t border-border px-3 py-1.5 bg-muted/30 relative z-10">
|
||||
<div className="flex items-center justify-between text-[10px] text-muted-foreground">
|
||||
<span>Ctrl+K/J nav</span>
|
||||
<span className="hidden sm:inline">DeepSeek-V3</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
12
bandit-runner-app/src/components/theme-provider.tsx
Normal file
12
bandit-runner-app/src/components/theme-provider.tsx
Normal file
@ -0,0 +1,12 @@
|
||||
"use client"
|
||||
|
||||
import * as React from "react"
|
||||
import { ThemeProvider as NextThemesProvider } from "next-themes"
|
||||
|
||||
export function ThemeProvider({
|
||||
children,
|
||||
...props
|
||||
}: React.ComponentProps<typeof NextThemesProvider>) {
|
||||
return <NextThemesProvider {...props}>{children}</NextThemesProvider>
|
||||
}
|
||||
|
||||
40
bandit-runner-app/src/components/theme-toggle.tsx
Normal file
40
bandit-runner-app/src/components/theme-toggle.tsx
Normal file
@ -0,0 +1,40 @@
|
||||
"use client"
|
||||
|
||||
import * as React from "react"
|
||||
import { Moon, Sun } from "lucide-react"
|
||||
import { useTheme } from "next-themes"
|
||||
|
||||
export function ThemeToggle() {
|
||||
const { theme, setTheme } = useTheme()
|
||||
const [mounted, setMounted] = React.useState(false)
|
||||
|
||||
React.useEffect(() => {
|
||||
setMounted(true)
|
||||
}, [])
|
||||
|
||||
if (!mounted) {
|
||||
return (
|
||||
<button
|
||||
className="w-8 h-8 flex items-center justify-center border border-border bg-card text-muted-foreground"
|
||||
disabled
|
||||
>
|
||||
<div className="w-4 h-4" />
|
||||
</button>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<button
|
||||
onClick={() => setTheme(theme === "dark" ? "light" : "dark")}
|
||||
className="w-8 h-8 flex items-center justify-center border border-primary/40 bg-card text-primary hover:border-primary transition-colors"
|
||||
title={`Switch to ${theme === "dark" ? "light" : "dark"} mode`}
|
||||
>
|
||||
{theme === "dark" ? (
|
||||
<Sun className="w-4 h-4" />
|
||||
) : (
|
||||
<Moon className="w-4 h-4" />
|
||||
)}
|
||||
</button>
|
||||
)
|
||||
}
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
'use client';
|
||||
|
||||
import { Button } from '@repo/shadcn-ui/components/ui/button';
|
||||
import { cn } from '@repo/shadcn-ui/lib/utils';
|
||||
import { Button } from '@/components/ui/shadcn-io/button';
|
||||
import { cn } from '@/lib/utils';
|
||||
import { CheckIcon, CopyIcon } from 'lucide-react';
|
||||
import type { ComponentProps, HTMLAttributes, ReactNode } from 'react';
|
||||
import { createContext, useContext, useState } from 'react';
|
||||
|
||||
@ -2,8 +2,8 @@ import {
|
||||
Avatar,
|
||||
AvatarFallback,
|
||||
AvatarImage,
|
||||
} from '@repo/shadcn-ui/components/ui/avatar';
|
||||
import { cn } from '@repo/shadcn-ui/lib/utils';
|
||||
} from '@/components/ui/shadcn-io/avatar';
|
||||
import { cn } from '@/lib/utils';
|
||||
import type { UIMessage } from 'ai';
|
||||
import type { ComponentProps, HTMLAttributes } from 'react';
|
||||
|
||||
|
||||
@ -5,8 +5,8 @@ import {
|
||||
Collapsible,
|
||||
CollapsibleContent,
|
||||
CollapsibleTrigger,
|
||||
} from '@repo/shadcn-ui/components/ui/collapsible';
|
||||
import { cn } from '@repo/shadcn-ui/lib/utils';
|
||||
} from '@/components/ui/shadcn-io/collapsible';
|
||||
import { cn } from '@/lib/utils';
|
||||
import { BrainIcon, ChevronDownIcon } from 'lucide-react';
|
||||
import type { ComponentProps } from 'react';
|
||||
import { createContext, memo, useContext, useEffect, useState } from 'react';
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
'use client';
|
||||
|
||||
import { cn } from '@repo/shadcn-ui/lib/utils';
|
||||
import { cn } from '@/lib/utils';
|
||||
import type { ComponentProps, HTMLAttributes } from 'react';
|
||||
import { isValidElement, memo } from 'react';
|
||||
import ReactMarkdown, { type Options } from 'react-markdown';
|
||||
|
||||
@ -1,12 +1,12 @@
|
||||
'use client';
|
||||
|
||||
import { Badge } from '@repo/shadcn-ui/components/ui/badge';
|
||||
import { Badge } from '@/components/ui/shadcn-io/badge';
|
||||
import {
|
||||
Collapsible,
|
||||
CollapsibleContent,
|
||||
CollapsibleTrigger,
|
||||
} from '@repo/shadcn-ui/components/ui/collapsible';
|
||||
import { cn } from '@repo/shadcn-ui/lib/utils';
|
||||
} from '@/components/ui/shadcn-io/collapsible';
|
||||
import { cn } from '@/lib/utils';
|
||||
import type { ToolUIPart } from 'ai';
|
||||
import {
|
||||
CheckCircleIcon,
|
||||
|
||||
@ -4,7 +4,7 @@ import * as React from "react"
|
||||
import * as AlertDialogPrimitive from "@radix-ui/react-alert-dialog"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
import { buttonVariants } from "@/components/ui/button"
|
||||
import { buttonVariants } from "@/components/ui/shadcn-io/button"
|
||||
|
||||
function AlertDialog({
|
||||
...props
|
||||
53
bandit-runner-app/src/components/ui/shadcn-io/avatar.tsx
Normal file
53
bandit-runner-app/src/components/ui/shadcn-io/avatar.tsx
Normal file
@ -0,0 +1,53 @@
|
||||
"use client"
|
||||
|
||||
import * as React from "react"
|
||||
import * as AvatarPrimitive from "@radix-ui/react-avatar"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
function Avatar({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof AvatarPrimitive.Root>) {
|
||||
return (
|
||||
<AvatarPrimitive.Root
|
||||
data-slot="avatar"
|
||||
className={cn(
|
||||
"relative flex size-8 shrink-0 overflow-hidden rounded-full",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function AvatarImage({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof AvatarPrimitive.Image>) {
|
||||
return (
|
||||
<AvatarPrimitive.Image
|
||||
data-slot="avatar-image"
|
||||
className={cn("aspect-square size-full", className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function AvatarFallback({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof AvatarPrimitive.Fallback>) {
|
||||
return (
|
||||
<AvatarPrimitive.Fallback
|
||||
data-slot="avatar-fallback"
|
||||
className={cn(
|
||||
"bg-muted flex size-full items-center justify-center rounded-full",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
export { Avatar, AvatarImage, AvatarFallback }
|
||||
@ -86,14 +86,14 @@ import {
|
||||
useEffect,
|
||||
useState,
|
||||
} from 'react';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Button } from '@/components/ui/shadcn-io/button';
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from '@/components/ui/select';
|
||||
} from '@/components/ui/shadcn-io/select';
|
||||
import { cn } from '@/lib/utils';
|
||||
|
||||
export type BundledLanguage = string;
|
||||
|
||||
@ -0,0 +1,33 @@
|
||||
"use client"
|
||||
|
||||
import * as CollapsiblePrimitive from "@radix-ui/react-collapsible"
|
||||
|
||||
function Collapsible({
|
||||
...props
|
||||
}: React.ComponentProps<typeof CollapsiblePrimitive.Root>) {
|
||||
return <CollapsiblePrimitive.Root data-slot="collapsible" {...props} />
|
||||
}
|
||||
|
||||
function CollapsibleTrigger({
|
||||
...props
|
||||
}: React.ComponentProps<typeof CollapsiblePrimitive.CollapsibleTrigger>) {
|
||||
return (
|
||||
<CollapsiblePrimitive.CollapsibleTrigger
|
||||
data-slot="collapsible-trigger"
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function CollapsibleContent({
|
||||
...props
|
||||
}: React.ComponentProps<typeof CollapsiblePrimitive.CollapsibleContent>) {
|
||||
return (
|
||||
<CollapsiblePrimitive.CollapsibleContent
|
||||
data-slot="collapsible-content"
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
export { Collapsible, CollapsibleTrigger, CollapsibleContent }
|
||||
Loading…
x
Reference in New Issue
Block a user