Add prototypical landing page

This commit is contained in:
Maurice Heumann
2025-04-22 14:32:43 +02:00
parent 3eed628a67
commit 82eae543d6
7 changed files with 541 additions and 123 deletions

149
page/package-lock.json generated
View File

@@ -15,6 +15,7 @@
"@radix-ui/react-scroll-area": "^1.2.5",
"@radix-ui/react-separator": "^1.1.4",
"@radix-ui/react-slot": "^1.2.0",
"@radix-ui/react-tabs": "^1.1.8",
"@radix-ui/react-tooltip": "^1.2.3",
"@tailwindcss/vite": "^4.1.4",
"@types/react-window": "^1.8.8",
@@ -25,6 +26,7 @@
"react": "^19.0.0",
"react-bootstrap-icons": "^1.11.5",
"react-dom": "^19.0.0",
"react-router-dom": "^7.5.1",
"react-window": "^1.8.11",
"tailwind-merge": "^3.2.0",
"tailwindcss": "^4.1.4",
@@ -1275,6 +1277,32 @@
}
}
},
"node_modules/@radix-ui/react-collection": {
"version": "1.1.4",
"resolved": "https://registry.npmjs.org/@radix-ui/react-collection/-/react-collection-1.1.4.tgz",
"integrity": "sha512-cv4vSf7HttqXilDnAnvINd53OTl1/bjUYVZrkFnA7nwmY9Ob2POUy0WY0sfqBAe1s5FyKsyceQlqiEGPYNTadg==",
"license": "MIT",
"dependencies": {
"@radix-ui/react-compose-refs": "1.1.2",
"@radix-ui/react-context": "1.1.2",
"@radix-ui/react-primitive": "2.1.0",
"@radix-ui/react-slot": "1.2.0"
},
"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
}
}
},
"node_modules/@radix-ui/react-compose-refs": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/@radix-ui/react-compose-refs/-/react-compose-refs-1.1.2.tgz",
@@ -1604,6 +1632,37 @@
}
}
},
"node_modules/@radix-ui/react-roving-focus": {
"version": "1.1.7",
"resolved": "https://registry.npmjs.org/@radix-ui/react-roving-focus/-/react-roving-focus-1.1.7.tgz",
"integrity": "sha512-C6oAg451/fQT3EGbWHbCQjYTtbyjNO1uzQgMzwyivcHT3GKNEmu1q3UuREhN+HzHAVtv3ivMVK08QlC+PkYw9Q==",
"license": "MIT",
"dependencies": {
"@radix-ui/primitive": "1.1.2",
"@radix-ui/react-collection": "1.1.4",
"@radix-ui/react-compose-refs": "1.1.2",
"@radix-ui/react-context": "1.1.2",
"@radix-ui/react-direction": "1.1.1",
"@radix-ui/react-id": "1.1.1",
"@radix-ui/react-primitive": "2.1.0",
"@radix-ui/react-use-callback-ref": "1.1.1",
"@radix-ui/react-use-controllable-state": "1.2.2"
},
"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
}
}
},
"node_modules/@radix-ui/react-scroll-area": {
"version": "1.2.5",
"resolved": "https://registry.npmjs.org/@radix-ui/react-scroll-area/-/react-scroll-area-1.2.5.tgz",
@@ -1676,6 +1735,36 @@
}
}
},
"node_modules/@radix-ui/react-tabs": {
"version": "1.1.8",
"resolved": "https://registry.npmjs.org/@radix-ui/react-tabs/-/react-tabs-1.1.8.tgz",
"integrity": "sha512-4iUaN9SYtG+/E+hJ7jRks/Nv90f+uAsRHbLYA6BcA9EsR6GNWgsvtS4iwU2SP0tOZfDGAyqIT0yz7ckgohEIFA==",
"license": "MIT",
"dependencies": {
"@radix-ui/primitive": "1.1.2",
"@radix-ui/react-context": "1.1.2",
"@radix-ui/react-direction": "1.1.1",
"@radix-ui/react-id": "1.1.1",
"@radix-ui/react-presence": "1.1.3",
"@radix-ui/react-primitive": "2.1.0",
"@radix-ui/react-roving-focus": "1.1.7",
"@radix-ui/react-use-controllable-state": "1.2.2"
},
"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
}
}
},
"node_modules/@radix-ui/react-tooltip": {
"version": "1.2.3",
"resolved": "https://registry.npmjs.org/@radix-ui/react-tooltip/-/react-tooltip-1.2.3.tgz",
@@ -3020,6 +3109,15 @@
"dev": true,
"license": "MIT"
},
"node_modules/cookie": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/cookie/-/cookie-1.0.2.tgz",
"integrity": "sha512-9Kr/j4O16ISv8zBBhJoi4bXOYNTkFLOqSL3UDB0njXxCXNezjeyVrJyGOWtgfs/q2km1gwBcfH8q1yEGoMYunA==",
"license": "MIT",
"engines": {
"node": ">=18"
}
},
"node_modules/core-util-is": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz",
@@ -4458,6 +4556,45 @@
}
}
},
"node_modules/react-router": {
"version": "7.5.1",
"resolved": "https://registry.npmjs.org/react-router/-/react-router-7.5.1.tgz",
"integrity": "sha512-/jjU3fcYNd2bwz9Q0xt5TwyiyoO8XjSEFXJY4O/lMAlkGTHWuHRAbR9Etik+lSDqMC7A7mz3UlXzgYT6Vl58sA==",
"license": "MIT",
"dependencies": {
"cookie": "^1.0.1",
"set-cookie-parser": "^2.6.0",
"turbo-stream": "2.4.0"
},
"engines": {
"node": ">=20.0.0"
},
"peerDependencies": {
"react": ">=18",
"react-dom": ">=18"
},
"peerDependenciesMeta": {
"react-dom": {
"optional": true
}
}
},
"node_modules/react-router-dom": {
"version": "7.5.1",
"resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-7.5.1.tgz",
"integrity": "sha512-5DPSPc7ENrt2tlKPq0FtpG80ZbqA9aIKEyqX6hSNJDlol/tr6iqCK4crqdsusmOSSotq6zDsn0y3urX9TuTNmA==",
"license": "MIT",
"dependencies": {
"react-router": "7.5.1"
},
"engines": {
"node": ">=20.0.0"
},
"peerDependencies": {
"react": ">=18",
"react-dom": ">=18"
}
},
"node_modules/react-style-singleton": {
"version": "2.2.3",
"resolved": "https://registry.npmjs.org/react-style-singleton/-/react-style-singleton-2.2.3.tgz",
@@ -4624,6 +4761,12 @@
"semver": "bin/semver.js"
}
},
"node_modules/set-cookie-parser": {
"version": "2.7.1",
"resolved": "https://registry.npmjs.org/set-cookie-parser/-/set-cookie-parser-2.7.1.tgz",
"integrity": "sha512-IOc8uWeOZgnb3ptbCURJWNjWUPcO3ZnTTdzsurqERrP6nPyv+paC55vJM0LpOlT2ne+Ix+9+CRG1MNLlyZ4GjQ==",
"license": "MIT"
},
"node_modules/setimmediate": {
"version": "1.0.5",
"resolved": "https://registry.npmjs.org/setimmediate/-/setimmediate-1.0.5.tgz",
@@ -4796,6 +4939,12 @@
"integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==",
"license": "0BSD"
},
"node_modules/turbo-stream": {
"version": "2.4.0",
"resolved": "https://registry.npmjs.org/turbo-stream/-/turbo-stream-2.4.0.tgz",
"integrity": "sha512-FHncC10WpBd2eOmGwpmQsWLDoK4cqsA/UT/GqNoaKOQnT8uzhtCbg3EoUDMvqpOSAI0S26mr0rkjzbOO6S3v1g==",
"license": "ISC"
},
"node_modules/tw-animate-css": {
"version": "1.2.8",
"resolved": "https://registry.npmjs.org/tw-animate-css/-/tw-animate-css-1.2.8.tgz",

View File

@@ -17,6 +17,7 @@
"@radix-ui/react-scroll-area": "^1.2.5",
"@radix-ui/react-separator": "^1.1.4",
"@radix-ui/react-slot": "^1.2.0",
"@radix-ui/react-tabs": "^1.1.8",
"@radix-ui/react-tooltip": "^1.2.3",
"@tailwindcss/vite": "^4.1.4",
"@types/react-window": "^1.8.8",
@@ -27,6 +28,7 @@
"react": "^19.0.0",
"react-bootstrap-icons": "^1.11.5",
"react-dom": "^19.0.0",
"react-router-dom": "^7.5.1",
"react-window": "^1.8.11",
"tailwind-merge": "^3.2.0",
"tailwindcss": "^4.1.4",

View File

@@ -21,10 +21,6 @@
}
}
html {
overflow: hidden;
}
.terminal-output {
line-height: 1.5;
font-weight: 600;

View File

@@ -1,126 +1,21 @@
import { useState, useRef } from 'react'
import { Output } from '@/components/output'
import { ThemeProvider } from "@/components/theme-provider";
import { BrowserRouter, Route, Routes } from "react-router-dom";
import { Playground } from "./Playground";
import { LandingPage } from "./LandingPage";
import { AppSidebar } from "@/components/app-sidebar"
import { ThemeProvider } from "@/components/theme-provider"
import { Separator } from "@/components/ui/separator"
import {
SidebarInset,
SidebarProvider,
SidebarTrigger,
} from "@/components/ui/sidebar"
import { Button } from './components/ui/button'
import { Emulator, UserFile } from './emulator';
import { getFilesystem } from './filesystem';
import './App.css'
import { Popover, PopoverContent, PopoverTrigger } from './components/ui/popover'
import { createDefaultSettings } from './settings';
import { SettingsMenu } from './components/settings-menu';
import { PlayFill, StopFill, GearFill } from 'react-bootstrap-icons';
import { StatusIndicator } from './components/status-indicator'
function selectAndReadFile(): Promise<UserFile> {
return new Promise((resolve, reject) => {
const fileInput = document.createElement('input');
fileInput.type = 'file';
fileInput.accept = '.exe';
fileInput.addEventListener('change', function (event) {
const file = (event as any).target.files[0];
if (file) {
const reader = new FileReader();
reader.onload = function (e: ProgressEvent<FileReader>) {
const arrayBuffer = e.target?.result;
resolve({
name: file.name,
data: arrayBuffer as ArrayBuffer
});
};
reader.onerror = function (e: ProgressEvent<FileReader>) {
reject(new Error('Error reading file: ' + e.target?.error));
};
reader.readAsArrayBuffer(file);
} else {
reject(new Error('No file selected'));
}
});
fileInput.click();
});
}
import "./App.css";
function App() {
const output = useRef<Output>(null);
const [settings, setSettings] = useState(createDefaultSettings());
const [emulator, setEmulator] = useState<Emulator | null>(null);
function logLine(line: string) {
output.current?.logLine(line);
}
function logLines(lines: string[]) {
output.current?.logLines(lines);
}
async function createEmulator(userFile: UserFile | null = null) {
emulator?.stop();
output.current?.clear();
logLine("Starting emulation...");
const fs = await getFilesystem((current, total, file) => {
logLine(`Processing filesystem (${current}/${total}): ${file}`);
});
const new_emulator = new Emulator(fs, logLines);
new_emulator.onTerminate().then(() => setEmulator(null));
setEmulator(new_emulator);
new_emulator.start(settings, userFile);
}
async function loadAndRunUserFile() {
const fileBuffer = await selectAndReadFile();
await createEmulator(fileBuffer);
}
return (
<ThemeProvider defaultTheme="dark" storageKey="vite-ui-theme">
<SidebarProvider defaultOpen={false}>
<AppSidebar />
<SidebarInset className='h-[100dvh]'>
<header className="flex h-16 shrink-0 items-center gap-2 border-b px-4 overflow-y-auto">
<SidebarTrigger className="-ml-1" />
<Separator orientation="vertical" className="mr-2 h-4" />
<Button onClick={() => createEmulator()}><PlayFill /> Run Sample</Button>
<Button onClick={() => loadAndRunUserFile()}><PlayFill /> Run your .exe</Button>
<Button variant="secondary" onClick={() => emulator?.stop()}><StopFill /> Stop Emulation</Button>
<Popover>
<PopoverTrigger asChild>
<Button variant="secondary"><GearFill /> Settings</Button>
</PopoverTrigger>
<PopoverContent>
<SettingsMenu settings={settings} onChange={setSettings} />
</PopoverContent>
</Popover>
<div className='text-right flex-1'>
<StatusIndicator running={!!emulator} />
</div>
</header>
<div className="flex flex-1 flex-col gap-4 p-4 overflow-auto">
<Output ref={output} />
</div>
</SidebarInset>
</SidebarProvider>
</ThemeProvider>)
<BrowserRouter>
<Routes>
<Route path="/" element={<LandingPage />} />
<Route path="/playground" element={<Playground />} />
</Routes>
</BrowserRouter>
</ThemeProvider>
);
}
export default App
export default App;

189
page/src/LandingPage.tsx Normal file
View File

@@ -0,0 +1,189 @@
import { Button } from "@/components/ui/button";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import {
Shield,
FileCode,
Layers,
Cpu,
Database,
Terminal,
ExternalLink,
Github,
Play
} from "lucide-react";
export function LandingPage() {
const features = [
{
icon: <Cpu className="h-6 w-6 text-[var(--primary)]" />,
title: "Syscall-Level Emulation",
description:
"Operates at syscall level, leveraging existing system DLLs instead of reimplementing Windows APIs",
},
{
icon: <Database className="h-6 w-6 text-[var(--primary)]" />,
title: "Advanced Memory Management",
description:
"Supports Windows-specific memory types including reserved, committed, built on top of Unicorn's memory management",
},
{
icon: <FileCode className="h-6 w-6 text-[var(--primary)]" />,
title: "Complete PE Loading",
description:
"Handles executable and DLL loading with proper memory mapping, relocations, and TLS",
},
{
icon: <Shield className="h-6 w-6 text-[var(--primary)]" />,
title: "Exception Handling",
description:
"Implements Windows structured exception handling (SEH) with proper exception dispatcher and unwinding support",
},
{
icon: <Layers className="h-6 w-6 text-[var(--primary)]" />,
title: "Threading Support",
description: "Provides a scheduled (round-robin) threading model",
},
{
icon: <Terminal className="h-6 w-6 text-[var(--primary)]" />,
title: "Debugging Interface",
description:
"Implements GDB serial protocol for integration with common debugging tools",
},
];
return (
<div className="flex flex-col min-h-screen">
{/* Hero Section */}
<header className="bg-gradient-to-r from-blue-600 to-cyan-500 py-16 md:py-24">
<div className="container mx-auto px-4 md:px-6">
<div className="flex flex-col md:flex-row items-center justify-between">
<div className="w-full md:w-1/2 space-y-6 text-center md:text-left text-white">
<h1 className="text-4xl md:text-6xl font-bold tracking-tight">
Sogen
</h1>
<p className="text-xl md:text-2xl font-light">
High-performance Windows user space emulator operating at
syscall level
</p>
<div className="flex flex-wrap gap-4 justify-center md:justify-start">
<a href="./playground" target="_blank">
<Button
size="lg"
className="bg-white text-blue-700 hover:bg-blue-50"
>
<Play className="mr-2 h-5 w-5" />
Try Online
</Button>
</a>
<a href="https://github.com/momo5502/sogen" target="_blank">
<Button
size="lg"
variant="outline"
className="border-white text-white hover:bg-white/10"
>
<Github className="mr-2 h-5 w-5" />
GitHub
</Button>
</a>
</div>
</div>
{/*<div className="w-full md:w-1/2 mt-8 md:mt-0 flex justify-center md:justify-end">
<div className="relative w-full max-w-md">
<div className="absolute inset-0 bg-gradient-to-r from-blue-500/20 to-indigo-500/20 rounded-lg blur-xl"></div>
<img
src="/api/placeholder/600/400"
alt="Sogen Emulator"
className="relative rounded-lg shadow-xl w-full"
/>
</div>
</div>*/}
</div>
</div>
</header>
{/* Key Features */}
<section className="py-16 md:py-24">
<div className="container mx-auto px-4 md:px-6">
<div className="text-center mb-12">
<h2 className="text-3xl md:text-4xl font-bold mb-4">
Key Features
</h2>
</div>
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
{features.map((feature, index) => (
<Card key={index} className="hover:shadow-lg transition-shadow">
<CardHeader>
<div className="mb-2">{feature.icon}</div>
<CardTitle>{feature.title}</CardTitle>
</CardHeader>
<CardContent>
<p className="text-gray-600 dark:text-gray-400">
{feature.description}
</p>
</CardContent>
</Card>
))}
</div>
</div>
</section>
{/* Video Section */}
<section className="bg-zinc-900 py-16 md:py-24">
<div className="container mx-auto px-4 md:px-6">
<div className="text-center mb-12">
<h2 className="text-3xl md:text-4xl font-bold mb-4">
See Sogen in Action
</h2>
<p className="text-lg text-gray-600 dark:text-gray-400 max-w-3xl mx-auto">
Watch an overview of the emulator's capabilities and see how it
can help with your research.
</p>
</div>
<div className="max-w-4xl mx-auto">
<div className="relative aspect-video rounded-2xl shadow-2xl">
<iframe
className="rounded-2xl"
width="100%"
height="100%"
src="https://www.youtube.com/embed/wY9Q0DhodOQ?si=ag_zebGFpQPXBsTx"
title="YouTube video player"
allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share"
></iframe>
</div>
<div className="mt-4 text-center"></div>
</div>
</div>
</section>
{/* Footer */}
<footer className="py-12">
<div className="container mx-auto px-4 md:px-6">
<div className="flex flex-col md:flex-row justify-between items-center">
<div className="mb-6 md:mb-0">
<h2 className="text-2xl font-bold">Sogen</h2>
<p className="mt-2 text-gray-400">Windows User Space Emulator</p>
</div>
<div className="flex space-x-6">
<a
href="https://github.com/momo5502/sogen"
target="_blank"
className="hover:text-blue-400"
>
<Github className="h-6 w-6" />
</a>
<a
href="./playground"
target="_blank"
className="hover:text-blue-400"
>
<ExternalLink className="h-6 w-6" />
</a>
</div>
</div>
</div>
</footer>
</div>
);
}

123
page/src/Playground.tsx Normal file
View File

@@ -0,0 +1,123 @@
import { useState, useRef } from 'react'
import { Output } from '@/components/output'
import { AppSidebar } from "@/components/app-sidebar"
import { Separator } from "@/components/ui/separator"
import {
SidebarInset,
SidebarProvider,
SidebarTrigger,
} from "@/components/ui/sidebar"
import { Button } from './components/ui/button'
import { Emulator, UserFile } from './emulator';
import { getFilesystem } from './filesystem';
import './App.css'
import { Popover, PopoverContent, PopoverTrigger } from './components/ui/popover'
import { createDefaultSettings } from './settings';
import { SettingsMenu } from './components/settings-menu';
import { PlayFill, StopFill, GearFill } from 'react-bootstrap-icons';
import { StatusIndicator } from './components/status-indicator'
function selectAndReadFile(): Promise<UserFile> {
return new Promise((resolve, reject) => {
const fileInput = document.createElement('input');
fileInput.type = 'file';
fileInput.accept = '.exe';
fileInput.addEventListener('change', function (event) {
const file = (event as any).target.files[0];
if (file) {
const reader = new FileReader();
reader.onload = function (e: ProgressEvent<FileReader>) {
const arrayBuffer = e.target?.result;
resolve({
name: file.name,
data: arrayBuffer as ArrayBuffer
});
};
reader.onerror = function (e: ProgressEvent<FileReader>) {
reject(new Error('Error reading file: ' + e.target?.error));
};
reader.readAsArrayBuffer(file);
} else {
reject(new Error('No file selected'));
}
});
fileInput.click();
});
}
export function Playground() {
const output = useRef<Output>(null);
const [settings, setSettings] = useState(createDefaultSettings());
const [emulator, setEmulator] = useState<Emulator | null>(null);
function logLine(line: string) {
output.current?.logLine(line);
}
function logLines(lines: string[]) {
output.current?.logLines(lines);
}
async function createEmulator(userFile: UserFile | null = null) {
emulator?.stop();
output.current?.clear();
logLine("Starting emulation...");
const fs = await getFilesystem((current, total, file) => {
logLine(`Processing filesystem (${current}/${total}): ${file}`);
});
const new_emulator = new Emulator(fs, logLines);
new_emulator.onTerminate().then(() => setEmulator(null));
setEmulator(new_emulator);
new_emulator.start(settings, userFile);
}
async function loadAndRunUserFile() {
const fileBuffer = await selectAndReadFile();
await createEmulator(fileBuffer);
}
return (
<SidebarProvider defaultOpen={false}>
<AppSidebar />
<SidebarInset className='h-[100dvh]'>
<header className="flex h-16 shrink-0 items-center gap-2 border-b px-4 overflow-y-auto">
<SidebarTrigger className="-ml-1" />
<Separator orientation="vertical" className="mr-2 h-4" />
<Button onClick={() => createEmulator()}><PlayFill /> Run Sample</Button>
<Button onClick={() => loadAndRunUserFile()}><PlayFill /> Run your .exe</Button>
<Button variant="secondary" onClick={() => emulator?.stop()}><StopFill /> Stop Emulation</Button>
<Popover>
<PopoverTrigger asChild>
<Button variant="secondary"><GearFill /> Settings</Button>
</PopoverTrigger>
<PopoverContent>
<SettingsMenu settings={settings} onChange={setSettings} />
</PopoverContent>
</Popover>
<div className='text-right flex-1'>
<StatusIndicator running={!!emulator} />
</div>
</header>
<div className="flex flex-1 flex-col gap-4 p-4 overflow-auto">
<Output ref={output} />
</div>
</SidebarInset>
</SidebarProvider>
)
}

View File

@@ -0,0 +1,64 @@
import * as React from "react"
import * as TabsPrimitive from "@radix-ui/react-tabs"
import { cn } from "@/lib/utils"
function Tabs({
className,
...props
}: React.ComponentProps<typeof TabsPrimitive.Root>) {
return (
<TabsPrimitive.Root
data-slot="tabs"
className={cn("flex flex-col gap-2", className)}
{...props}
/>
)
}
function TabsList({
className,
...props
}: React.ComponentProps<typeof TabsPrimitive.List>) {
return (
<TabsPrimitive.List
data-slot="tabs-list"
className={cn(
"bg-muted text-muted-foreground inline-flex h-9 w-fit items-center justify-center rounded-lg p-[3px]",
className
)}
{...props}
/>
)
}
function TabsTrigger({
className,
...props
}: React.ComponentProps<typeof TabsPrimitive.Trigger>) {
return (
<TabsPrimitive.Trigger
data-slot="tabs-trigger"
className={cn(
"data-[state=active]:bg-background dark:data-[state=active]:text-foreground focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:outline-ring dark:data-[state=active]:border-input dark:data-[state=active]:bg-input/30 text-foreground dark:text-muted-foreground inline-flex h-[calc(100%-1px)] flex-1 items-center justify-center gap-1.5 rounded-md border border-transparent px-2 py-1 text-sm font-medium whitespace-nowrap transition-[color,box-shadow] focus-visible:ring-[3px] focus-visible:outline-1 disabled:pointer-events-none disabled:opacity-50 data-[state=active]:shadow-sm [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
className
)}
{...props}
/>
)
}
function TabsContent({
className,
...props
}: React.ComponentProps<typeof TabsPrimitive.Content>) {
return (
<TabsPrimitive.Content
data-slot="tabs-content"
className={cn("flex-1 outline-none", className)}
{...props}
/>
)
}
export { Tabs, TabsList, TabsTrigger, TabsContent }