diff --git a/apps/web/package.json b/apps/web/package.json
index f3ad613..4e03e42 100644
--- a/apps/web/package.json
+++ b/apps/web/package.json
@@ -10,13 +10,33 @@
"lint": "eslint src/"
},
"dependencies": {
+ "@radix-ui/react-avatar": "^1.1.11",
+ "@radix-ui/react-dialog": "^1.1.15",
+ "@radix-ui/react-dropdown-menu": "^2.1.16",
+ "@radix-ui/react-label": "^2.1.8",
+ "@radix-ui/react-progress": "^1.1.8",
+ "@radix-ui/react-scroll-area": "^1.2.10",
+ "@radix-ui/react-select": "^2.2.6",
+ "@radix-ui/react-separator": "^1.1.8",
+ "@radix-ui/react-slot": "^1.2.4",
+ "@radix-ui/react-switch": "^1.2.6",
+ "@radix-ui/react-tabs": "^1.1.13",
+ "@radix-ui/react-toast": "^1.2.15",
+ "@radix-ui/react-tooltip": "^1.2.8",
"@source/shared": "workspace:*",
"@source/ui": "workspace:*",
"@tanstack/react-query": "^5.62.0",
+ "@xterm/addon-fit": "^0.11.0",
+ "@xterm/addon-web-links": "^0.12.0",
+ "@xterm/xterm": "^6.0.0",
+ "class-variance-authority": "^0.7.0",
+ "lucide-react": "^0.575.0",
"react": "^19.0.0",
"react-dom": "^19.0.0",
"react-router": "^7.1.0",
- "socket.io-client": "^4.8.0"
+ "socket.io-client": "^4.8.0",
+ "sonner": "^2.0.7",
+ "zustand": "^5.0.11"
},
"devDependencies": {
"@types/react": "^19.0.0",
diff --git a/apps/web/src/App.tsx b/apps/web/src/App.tsx
index b544bb6..5e8cd06 100644
--- a/apps/web/src/App.tsx
+++ b/apps/web/src/App.tsx
@@ -1,5 +1,38 @@
+import { useEffect } from 'react';
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
-import { BrowserRouter, Routes, Route } from 'react-router';
+import { BrowserRouter, Routes, Route, Navigate, Outlet } from 'react-router';
+import { Toaster } from 'sonner';
+import { TooltipProvider } from '@/components/ui/tooltip';
+import { useAuthStore } from '@/stores/auth';
+
+// Layouts
+import { AppLayout } from '@/components/layout/app-layout';
+import { ServerLayout } from '@/components/layout/server-layout';
+
+// Auth pages
+import { LoginPage } from '@/pages/auth/login';
+import { RegisterPage } from '@/pages/auth/register';
+
+// App pages
+import { OrganizationsPage } from '@/pages/organizations/index';
+import { DashboardPage } from '@/pages/dashboard/index';
+import { CreateServerPage } from '@/pages/servers/create';
+import { NodesPage } from '@/pages/nodes/index';
+import { MembersPage } from '@/pages/settings/members';
+
+// Server pages
+import { ConsolePage } from '@/pages/server/console';
+import { FilesPage } from '@/pages/server/files';
+import { BackupsPage } from '@/pages/server/backups';
+import { SchedulesPage } from '@/pages/server/schedules';
+import { PluginsPage } from '@/pages/server/plugins';
+import { PlayersPage } from '@/pages/server/players';
+import { ServerSettingsPage } from '@/pages/server/settings';
+
+// Admin pages
+import { AdminUsersPage } from '@/pages/admin/users';
+import { AdminGamesPage } from '@/pages/admin/games';
+import { AdminAuditLogsPage } from '@/pages/admin/audit-logs';
const queryClient = new QueryClient({
defaultOptions: {
@@ -10,24 +43,76 @@ const queryClient = new QueryClient({
},
});
+function AuthGuard() {
+ const { isAuthenticated, isLoading, fetchUser } = useAuthStore();
+
+ useEffect(() => {
+ fetchUser();
+ }, [fetchUser]);
+
+ if (isLoading) {
+ return (
+
+ );
+ }
+
+ if (!isAuthenticated) {
+ return ;
+ }
+
+ return ;
+}
+
export function App() {
return (
-
-
-
-
-
GamePanel
-
Game Server Management Panel
-
-
- }
- />
-
-
+
+
+
+ {/* Public routes */}
+ } />
+ } />
+
+ {/* Protected routes */}
+ }>
+ }>
+ {/* Organizations */}
+ } />
+
+ {/* Org-scoped routes */}
+ } />
+ } />
+ } />
+ } />
+
+ {/* Server detail */}
+ }>
+ } />
+ } />
+ } />
+ } />
+ } />
+ } />
+ } />
+ } />
+
+
+ {/* Admin */}
+ } />
+ } />
+ } />
+ } />
+
+
+
+ {/* Fallback */}
+ } />
+
+
+
+
);
}
diff --git a/apps/web/src/components/layout/app-layout.tsx b/apps/web/src/components/layout/app-layout.tsx
new file mode 100644
index 0000000..2abb765
--- /dev/null
+++ b/apps/web/src/components/layout/app-layout.tsx
@@ -0,0 +1,17 @@
+import { Outlet } from 'react-router';
+import { Sidebar } from './sidebar';
+import { Header } from './header';
+
+export function AppLayout() {
+ return (
+
+ );
+}
diff --git a/apps/web/src/components/layout/header.tsx b/apps/web/src/components/layout/header.tsx
new file mode 100644
index 0000000..2c6f45a
--- /dev/null
+++ b/apps/web/src/components/layout/header.tsx
@@ -0,0 +1,56 @@
+import { useNavigate } from 'react-router';
+import { LogOut, User, Moon, Sun } from 'lucide-react';
+import { Button } from '@/components/ui/button';
+import {
+ DropdownMenu,
+ DropdownMenuContent,
+ DropdownMenuItem,
+ DropdownMenuLabel,
+ DropdownMenuSeparator,
+ DropdownMenuTrigger,
+} from '@/components/ui/dropdown-menu';
+import { useAuthStore } from '@/stores/auth';
+import { useTheme } from '@/hooks/use-theme';
+
+export function Header() {
+ const navigate = useNavigate();
+ const { user, logout } = useAuthStore();
+ const { theme, toggleTheme } = useTheme();
+
+ const handleLogout = async () => {
+ await logout();
+ navigate('/login');
+ };
+
+ return (
+
+ );
+}
diff --git a/apps/web/src/components/layout/server-layout.tsx b/apps/web/src/components/layout/server-layout.tsx
new file mode 100644
index 0000000..39602e3
--- /dev/null
+++ b/apps/web/src/components/layout/server-layout.tsx
@@ -0,0 +1,89 @@
+import { Outlet, useParams, Link, useLocation } from 'react-router';
+import { useQuery } from '@tanstack/react-query';
+import { Terminal, FolderOpen, Settings, Calendar, HardDrive, Users, Puzzle } from 'lucide-react';
+import { cn } from '@source/ui';
+import { api } from '@/lib/api';
+import { Badge } from '@/components/ui/badge';
+import { PowerControls } from '@/components/server/power-controls';
+import { statusBadgeVariant } from '@/lib/utils';
+
+interface ServerDetail {
+ id: string;
+ uuid: string;
+ name: string;
+ status: string;
+ nodeName: string;
+ nodeFqdn: string;
+ gameName: string;
+ gameSlug: string;
+ port: number;
+ memoryLimit: number;
+ diskLimit: number;
+ cpuLimit: number;
+}
+
+const tabs = [
+ { label: 'Console', path: 'console', icon: Terminal },
+ { label: 'Files', path: 'files', icon: FolderOpen },
+ { label: 'Backups', path: 'backups', icon: HardDrive },
+ { label: 'Schedules', path: 'schedules', icon: Calendar },
+ { label: 'Plugins', path: 'plugins', icon: Puzzle },
+ { label: 'Players', path: 'players', icon: Users },
+ { label: 'Settings', path: 'settings', icon: Settings },
+];
+
+export function ServerLayout() {
+ const { orgId, serverId } = useParams();
+ const location = useLocation();
+
+ const { data: server } = useQuery({
+ queryKey: ['server', orgId, serverId],
+ queryFn: () => api.get(`/organizations/${orgId}/servers/${serverId}`),
+ });
+
+ const currentTab = location.pathname.split('/').pop();
+
+ return (
+
+
+
+
+
{server?.name ?? 'Loading...'}
+ {server && (
+ {server.status}
+ )}
+
+ {server && (
+
+ {server.gameName} · {server.nodeFqdn}:{server.port} · {server.uuid}
+
+ )}
+
+ {server &&
}
+
+
+
+
+
+
+ );
+}
diff --git a/apps/web/src/components/layout/sidebar.tsx b/apps/web/src/components/layout/sidebar.tsx
new file mode 100644
index 0000000..953d5f7
--- /dev/null
+++ b/apps/web/src/components/layout/sidebar.tsx
@@ -0,0 +1,115 @@
+import { Link, useLocation, useParams } from 'react-router';
+import {
+ Server,
+ LayoutDashboard,
+ Network,
+ Settings,
+ Users,
+ Shield,
+ Gamepad2,
+ ScrollText,
+ ChevronLeft,
+} from 'lucide-react';
+import { cn } from '@source/ui';
+import { Button } from '@/components/ui/button';
+import { ScrollArea } from '@/components/ui/scroll-area';
+import { Separator } from '@/components/ui/separator';
+import { useAuthStore } from '@/stores/auth';
+
+interface NavItem {
+ label: string;
+ href: string;
+ icon: React.ComponentType<{ className?: string }>;
+}
+
+export function Sidebar() {
+ const location = useLocation();
+ const { orgId } = useParams();
+ const user = useAuthStore((s) => s.user);
+
+ const orgNav: NavItem[] = orgId
+ ? [
+ { label: 'Dashboard', href: `/org/${orgId}/dashboard`, icon: LayoutDashboard },
+ { label: 'Servers', href: `/org/${orgId}/servers`, icon: Server },
+ { label: 'Nodes', href: `/org/${orgId}/nodes`, icon: Network },
+ { label: 'Members', href: `/org/${orgId}/settings/members`, icon: Users },
+ { label: 'Settings', href: `/org/${orgId}/settings`, icon: Settings },
+ ]
+ : [];
+
+ const adminNav: NavItem[] = user?.isSuperAdmin
+ ? [
+ { label: 'Users', href: '/admin/users', icon: Users },
+ { label: 'Games', href: '/admin/games', icon: Gamepad2 },
+ { label: 'Nodes', href: '/admin/nodes', icon: Network },
+ { label: 'Audit Logs', href: '/admin/audit-logs', icon: ScrollText },
+ ]
+ : [];
+
+ return (
+
+
+
+ GamePanel
+
+
+
+ {orgId && (
+
+
+
+ Organizations
+
+
+
+
+ )}
+
+ {!orgId && (
+
+
ORGANIZATIONS
+
+
+
+
+ )}
+
+ {adminNav.length > 0 && (
+ <>
+
+
+ >
+ )}
+
+
+ );
+}
+
+function NavSection({ items, currentPath }: { items: NavItem[]; currentPath: string }) {
+ return (
+
+ );
+}
diff --git a/apps/web/src/components/server/power-controls.tsx b/apps/web/src/components/server/power-controls.tsx
new file mode 100644
index 0000000..e4da16b
--- /dev/null
+++ b/apps/web/src/components/server/power-controls.tsx
@@ -0,0 +1,102 @@
+import { useMutation, useQueryClient } from '@tanstack/react-query';
+import { Play, Square, RotateCcw, Skull } from 'lucide-react';
+import { api } from '@/lib/api';
+import { Button } from '@/components/ui/button';
+import {
+ Dialog,
+ DialogContent,
+ DialogDescription,
+ DialogFooter,
+ DialogHeader,
+ DialogTitle,
+ DialogTrigger,
+ DialogClose,
+} from '@/components/ui/dialog';
+
+interface PowerControlsProps {
+ serverId: string;
+ orgId: string;
+ status: string;
+}
+
+export function PowerControls({ serverId, orgId, status }: PowerControlsProps) {
+ const queryClient = useQueryClient();
+
+ const powerMutation = useMutation({
+ mutationFn: (action: string) =>
+ api.post(`/organizations/${orgId}/servers/${serverId}/power`, { action }),
+ onSuccess: () => {
+ queryClient.invalidateQueries({ queryKey: ['server', orgId, serverId] });
+ queryClient.invalidateQueries({ queryKey: ['servers', orgId] });
+ },
+ });
+
+ const isRunning = status === 'running';
+ const isStopped = status === 'stopped' || status === 'error';
+ const isTransitioning = status === 'starting' || status === 'stopping' || status === 'installing';
+
+ return (
+
+
+
+
+
+
+
+
+
+ );
+}
diff --git a/apps/web/src/components/ui/badge.tsx b/apps/web/src/components/ui/badge.tsx
new file mode 100644
index 0000000..e42ee0f
--- /dev/null
+++ b/apps/web/src/components/ui/badge.tsx
@@ -0,0 +1,28 @@
+import * as React from 'react';
+import { cva, type VariantProps } from 'class-variance-authority';
+import { cn } from '@source/ui';
+
+const badgeVariants = cva(
+ 'inline-flex items-center rounded-md 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',
+ {
+ variants: {
+ variant: {
+ default: 'border-transparent bg-primary text-primary-foreground shadow',
+ secondary: 'border-transparent bg-secondary text-secondary-foreground',
+ destructive: 'border-transparent bg-destructive text-destructive-foreground shadow',
+ outline: 'text-foreground',
+ },
+ },
+ defaultVariants: { variant: 'default' },
+ },
+);
+
+export interface BadgeProps
+ extends React.HTMLAttributes,
+ VariantProps {}
+
+function Badge({ className, variant, ...props }: BadgeProps) {
+ return ;
+}
+
+export { Badge, badgeVariants };
diff --git a/apps/web/src/components/ui/button.tsx b/apps/web/src/components/ui/button.tsx
new file mode 100644
index 0000000..5824756
--- /dev/null
+++ b/apps/web/src/components/ui/button.tsx
@@ -0,0 +1,46 @@
+import * as React from 'react';
+import { Slot } from '@radix-ui/react-slot';
+import { cva, type VariantProps } from 'class-variance-authority';
+import { cn } from '@source/ui';
+
+const buttonVariants = cva(
+ 'inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0',
+ {
+ variants: {
+ variant: {
+ default: 'bg-primary text-primary-foreground shadow hover:bg-primary/90',
+ destructive: 'bg-destructive text-destructive-foreground shadow-sm hover:bg-destructive/90',
+ outline: 'border border-input bg-background shadow-sm hover:bg-accent hover:text-accent-foreground',
+ secondary: 'bg-secondary text-secondary-foreground shadow-sm hover:bg-secondary/80',
+ ghost: 'hover:bg-accent hover:text-accent-foreground',
+ link: 'text-primary underline-offset-4 hover:underline',
+ },
+ size: {
+ default: 'h-9 px-4 py-2',
+ sm: 'h-8 rounded-md px-3 text-xs',
+ lg: 'h-10 rounded-md px-8',
+ icon: 'h-9 w-9',
+ },
+ },
+ defaultVariants: {
+ variant: 'default',
+ size: 'default',
+ },
+ },
+);
+
+export interface ButtonProps
+ extends React.ButtonHTMLAttributes,
+ VariantProps {
+ asChild?: boolean;
+}
+
+const Button = React.forwardRef(
+ ({ className, variant, size, asChild = false, ...props }, ref) => {
+ const Comp = asChild ? Slot : 'button';
+ return ;
+ },
+);
+Button.displayName = 'Button';
+
+export { Button, buttonVariants };
diff --git a/apps/web/src/components/ui/card.tsx b/apps/web/src/components/ui/card.tsx
new file mode 100644
index 0000000..f79b730
--- /dev/null
+++ b/apps/web/src/components/ui/card.tsx
@@ -0,0 +1,44 @@
+import * as React from 'react';
+import { cn } from '@source/ui';
+
+const Card = React.forwardRef>(
+ ({ className, ...props }, ref) => (
+
+ ),
+);
+Card.displayName = 'Card';
+
+const CardHeader = React.forwardRef>(
+ ({ className, ...props }, ref) => (
+
+ ),
+);
+CardHeader.displayName = 'CardHeader';
+
+const CardTitle = React.forwardRef>(
+ ({ className, ...props }, ref) => (
+
+ ),
+);
+CardTitle.displayName = 'CardTitle';
+
+const CardDescription = React.forwardRef>(
+ ({ className, ...props }, ref) => (
+
+ ),
+);
+CardDescription.displayName = 'CardDescription';
+
+const CardContent = React.forwardRef>(
+ ({ className, ...props }, ref) => ,
+);
+CardContent.displayName = 'CardContent';
+
+const CardFooter = React.forwardRef>(
+ ({ className, ...props }, ref) => (
+
+ ),
+);
+CardFooter.displayName = 'CardFooter';
+
+export { Card, CardHeader, CardFooter, CardTitle, CardDescription, CardContent };
diff --git a/apps/web/src/components/ui/dialog.tsx b/apps/web/src/components/ui/dialog.tsx
new file mode 100644
index 0000000..2c86098
--- /dev/null
+++ b/apps/web/src/components/ui/dialog.tsx
@@ -0,0 +1,90 @@
+import * as React from 'react';
+import * as DialogPrimitive from '@radix-ui/react-dialog';
+import { X } from 'lucide-react';
+import { cn } from '@source/ui';
+
+const Dialog = DialogPrimitive.Root;
+const DialogTrigger = DialogPrimitive.Trigger;
+const DialogClose = DialogPrimitive.Close;
+
+const DialogPortal = DialogPrimitive.Portal;
+
+const DialogOverlay = React.forwardRef<
+ React.ComponentRef,
+ React.ComponentPropsWithoutRef
+>(({ className, ...props }, ref) => (
+
+));
+DialogOverlay.displayName = DialogPrimitive.Overlay.displayName;
+
+const DialogContent = React.forwardRef<
+ React.ComponentRef,
+ React.ComponentPropsWithoutRef
+>(({ className, children, ...props }, ref) => (
+
+
+
+ {children}
+
+
+ Close
+
+
+
+));
+DialogContent.displayName = DialogPrimitive.Content.displayName;
+
+const DialogHeader = ({ className, ...props }: React.HTMLAttributes) => (
+
+);
+
+const DialogFooter = ({ className, ...props }: React.HTMLAttributes) => (
+
+);
+
+const DialogTitle = React.forwardRef<
+ React.ComponentRef,
+ React.ComponentPropsWithoutRef
+>(({ className, ...props }, ref) => (
+
+));
+DialogTitle.displayName = DialogPrimitive.Title.displayName;
+
+const DialogDescription = React.forwardRef<
+ React.ComponentRef,
+ React.ComponentPropsWithoutRef
+>(({ className, ...props }, ref) => (
+
+));
+DialogDescription.displayName = DialogPrimitive.Description.displayName;
+
+export {
+ Dialog,
+ DialogPortal,
+ DialogOverlay,
+ DialogTrigger,
+ DialogClose,
+ DialogContent,
+ DialogHeader,
+ DialogFooter,
+ DialogTitle,
+ DialogDescription,
+};
diff --git a/apps/web/src/components/ui/dropdown-menu.tsx b/apps/web/src/components/ui/dropdown-menu.tsx
new file mode 100644
index 0000000..330b6cb
--- /dev/null
+++ b/apps/web/src/components/ui/dropdown-menu.tsx
@@ -0,0 +1,71 @@
+import * as React from 'react';
+import * as DropdownMenuPrimitive from '@radix-ui/react-dropdown-menu';
+import { cn } from '@source/ui';
+
+const DropdownMenu = DropdownMenuPrimitive.Root;
+const DropdownMenuTrigger = DropdownMenuPrimitive.Trigger;
+const DropdownMenuGroup = DropdownMenuPrimitive.Group;
+
+const DropdownMenuContent = React.forwardRef<
+ React.ComponentRef,
+ React.ComponentPropsWithoutRef
+>(({ className, sideOffset = 4, ...props }, ref) => (
+
+
+
+));
+DropdownMenuContent.displayName = DropdownMenuPrimitive.Content.displayName;
+
+const DropdownMenuItem = React.forwardRef<
+ React.ComponentRef,
+ React.ComponentPropsWithoutRef & { inset?: boolean }
+>(({ className, inset, ...props }, ref) => (
+ svg]:size-4 [&>svg]:shrink-0',
+ inset && 'pl-8',
+ className,
+ )}
+ {...props}
+ />
+));
+DropdownMenuItem.displayName = DropdownMenuPrimitive.Item.displayName;
+
+const DropdownMenuSeparator = React.forwardRef<
+ React.ComponentRef,
+ React.ComponentPropsWithoutRef
+>(({ className, ...props }, ref) => (
+
+));
+DropdownMenuSeparator.displayName = DropdownMenuPrimitive.Separator.displayName;
+
+const DropdownMenuLabel = React.forwardRef<
+ React.ComponentRef,
+ React.ComponentPropsWithoutRef & { inset?: boolean }
+>(({ className, inset, ...props }, ref) => (
+
+));
+DropdownMenuLabel.displayName = DropdownMenuPrimitive.Label.displayName;
+
+export {
+ DropdownMenu,
+ DropdownMenuTrigger,
+ DropdownMenuContent,
+ DropdownMenuItem,
+ DropdownMenuSeparator,
+ DropdownMenuLabel,
+ DropdownMenuGroup,
+};
diff --git a/apps/web/src/components/ui/input.tsx b/apps/web/src/components/ui/input.tsx
new file mode 100644
index 0000000..b73eea7
--- /dev/null
+++ b/apps/web/src/components/ui/input.tsx
@@ -0,0 +1,21 @@
+import * as React from 'react';
+import { cn } from '@source/ui';
+
+const Input = React.forwardRef>(
+ ({ className, type, ...props }, ref) => {
+ return (
+
+ );
+ },
+);
+Input.displayName = 'Input';
+
+export { Input };
diff --git a/apps/web/src/components/ui/label.tsx b/apps/web/src/components/ui/label.tsx
new file mode 100644
index 0000000..40e63c7
--- /dev/null
+++ b/apps/web/src/components/ui/label.tsx
@@ -0,0 +1,20 @@
+import * as React from 'react';
+import * as LabelPrimitive from '@radix-ui/react-label';
+import { cn } from '@source/ui';
+
+const Label = React.forwardRef<
+ React.ComponentRef,
+ React.ComponentPropsWithoutRef
+>(({ className, ...props }, ref) => (
+
+));
+Label.displayName = LabelPrimitive.Root.displayName;
+
+export { Label };
diff --git a/apps/web/src/components/ui/progress.tsx b/apps/web/src/components/ui/progress.tsx
new file mode 100644
index 0000000..b017b9f
--- /dev/null
+++ b/apps/web/src/components/ui/progress.tsx
@@ -0,0 +1,22 @@
+import * as React from 'react';
+import * as ProgressPrimitive from '@radix-ui/react-progress';
+import { cn } from '@source/ui';
+
+const Progress = React.forwardRef<
+ React.ComponentRef,
+ React.ComponentPropsWithoutRef
+>(({ className, value, ...props }, ref) => (
+
+
+
+));
+Progress.displayName = ProgressPrimitive.Root.displayName;
+
+export { Progress };
diff --git a/apps/web/src/components/ui/scroll-area.tsx b/apps/web/src/components/ui/scroll-area.tsx
new file mode 100644
index 0000000..9e48f31
--- /dev/null
+++ b/apps/web/src/components/ui/scroll-area.tsx
@@ -0,0 +1,24 @@
+import * as React from 'react';
+import * as ScrollAreaPrimitive from '@radix-ui/react-scroll-area';
+import { cn } from '@source/ui';
+
+const ScrollArea = React.forwardRef<
+ React.ComponentRef,
+ React.ComponentPropsWithoutRef
+>(({ className, children, ...props }, ref) => (
+
+
+ {children}
+
+
+
+
+
+
+));
+ScrollArea.displayName = ScrollAreaPrimitive.Root.displayName;
+
+export { ScrollArea };
diff --git a/apps/web/src/components/ui/select.tsx b/apps/web/src/components/ui/select.tsx
new file mode 100644
index 0000000..352320c
--- /dev/null
+++ b/apps/web/src/components/ui/select.tsx
@@ -0,0 +1,88 @@
+import * as React from 'react';
+import * as SelectPrimitive from '@radix-ui/react-select';
+import { ChevronDown, ChevronUp, Check } from 'lucide-react';
+import { cn } from '@source/ui';
+
+const Select = SelectPrimitive.Root;
+const SelectGroup = SelectPrimitive.Group;
+const SelectValue = SelectPrimitive.Value;
+
+const SelectTrigger = React.forwardRef<
+ React.ComponentRef,
+ React.ComponentPropsWithoutRef
+>(({ className, children, ...props }, ref) => (
+ span]:line-clamp-1',
+ className,
+ )}
+ {...props}
+ >
+ {children}
+
+
+
+
+));
+SelectTrigger.displayName = SelectPrimitive.Trigger.displayName;
+
+const SelectContent = React.forwardRef<
+ React.ComponentRef,
+ React.ComponentPropsWithoutRef
+>(({ className, children, position = 'popper', ...props }, ref) => (
+
+
+
+
+
+
+ {children}
+
+
+
+
+
+
+));
+SelectContent.displayName = SelectPrimitive.Content.displayName;
+
+const SelectItem = React.forwardRef<
+ React.ComponentRef,
+ React.ComponentPropsWithoutRef
+>(({ className, children, ...props }, ref) => (
+
+
+
+
+
+
+ {children}
+
+));
+SelectItem.displayName = SelectPrimitive.Item.displayName;
+
+export { Select, SelectGroup, SelectValue, SelectTrigger, SelectContent, SelectItem };
diff --git a/apps/web/src/components/ui/separator.tsx b/apps/web/src/components/ui/separator.tsx
new file mode 100644
index 0000000..98c6248
--- /dev/null
+++ b/apps/web/src/components/ui/separator.tsx
@@ -0,0 +1,23 @@
+import * as React from 'react';
+import * as SeparatorPrimitive from '@radix-ui/react-separator';
+import { cn } from '@source/ui';
+
+const Separator = React.forwardRef<
+ React.ComponentRef,
+ React.ComponentPropsWithoutRef
+>(({ className, orientation = 'horizontal', decorative = true, ...props }, ref) => (
+
+));
+Separator.displayName = SeparatorPrimitive.Root.displayName;
+
+export { Separator };
diff --git a/apps/web/src/components/ui/tabs.tsx b/apps/web/src/components/ui/tabs.tsx
new file mode 100644
index 0000000..ce62344
--- /dev/null
+++ b/apps/web/src/components/ui/tabs.tsx
@@ -0,0 +1,52 @@
+import * as React from 'react';
+import * as TabsPrimitive from '@radix-ui/react-tabs';
+import { cn } from '@source/ui';
+
+const Tabs = TabsPrimitive.Root;
+
+const TabsList = React.forwardRef<
+ React.ComponentRef,
+ React.ComponentPropsWithoutRef
+>(({ className, ...props }, ref) => (
+
+));
+TabsList.displayName = TabsPrimitive.List.displayName;
+
+const TabsTrigger = React.forwardRef<
+ React.ComponentRef,
+ React.ComponentPropsWithoutRef
+>(({ className, ...props }, ref) => (
+
+));
+TabsTrigger.displayName = TabsPrimitive.Trigger.displayName;
+
+const TabsContent = React.forwardRef<
+ React.ComponentRef,
+ React.ComponentPropsWithoutRef
+>(({ className, ...props }, ref) => (
+
+));
+TabsContent.displayName = TabsPrimitive.Content.displayName;
+
+export { Tabs, TabsList, TabsTrigger, TabsContent };
diff --git a/apps/web/src/components/ui/tooltip.tsx b/apps/web/src/components/ui/tooltip.tsx
new file mode 100644
index 0000000..90d707c
--- /dev/null
+++ b/apps/web/src/components/ui/tooltip.tsx
@@ -0,0 +1,27 @@
+import * as React from 'react';
+import * as TooltipPrimitive from '@radix-ui/react-tooltip';
+import { cn } from '@source/ui';
+
+const TooltipProvider = TooltipPrimitive.Provider;
+const Tooltip = TooltipPrimitive.Root;
+const TooltipTrigger = TooltipPrimitive.Trigger;
+
+const TooltipContent = React.forwardRef<
+ React.ComponentRef,
+ React.ComponentPropsWithoutRef
+>(({ className, sideOffset = 4, ...props }, ref) => (
+
+
+
+));
+TooltipContent.displayName = TooltipPrimitive.Content.displayName;
+
+export { Tooltip, TooltipTrigger, TooltipContent, TooltipProvider };
diff --git a/apps/web/src/hooks/use-theme.ts b/apps/web/src/hooks/use-theme.ts
new file mode 100644
index 0000000..2db98ce
--- /dev/null
+++ b/apps/web/src/hooks/use-theme.ts
@@ -0,0 +1,25 @@
+import { useCallback, useSyncExternalStore } from 'react';
+
+function getTheme(): 'dark' | 'light' {
+ return document.documentElement.classList.contains('dark') ? 'dark' : 'light';
+}
+
+const listeners = new Set<() => void>();
+
+function subscribe(listener: () => void) {
+ listeners.add(listener);
+ return () => listeners.delete(listener);
+}
+
+export function useTheme() {
+ const theme = useSyncExternalStore(subscribe, getTheme);
+
+ const toggleTheme = useCallback(() => {
+ const next = getTheme() === 'dark' ? 'light' : 'dark';
+ document.documentElement.classList.toggle('dark', next === 'dark');
+ localStorage.setItem('theme', next);
+ listeners.forEach((l) => l());
+ }, []);
+
+ return { theme, toggleTheme };
+}
diff --git a/apps/web/src/lib/api.ts b/apps/web/src/lib/api.ts
new file mode 100644
index 0000000..7b31bad
--- /dev/null
+++ b/apps/web/src/lib/api.ts
@@ -0,0 +1,99 @@
+const API_BASE = '/api';
+
+interface RequestOptions extends RequestInit {
+ params?: Record;
+}
+
+class ApiError extends Error {
+ constructor(
+ public status: number,
+ public data: unknown,
+ ) {
+ super(`API Error ${status}`);
+ this.name = 'ApiError';
+ }
+}
+
+async function request(path: string, options: RequestOptions = {}): Promise {
+ const { params, ...fetchOptions } = options;
+
+ let url = `${API_BASE}${path}`;
+ if (params) {
+ const searchParams = new URLSearchParams(params);
+ url += `?${searchParams.toString()}`;
+ }
+
+ const token = localStorage.getItem('access_token');
+ const headers: Record = {
+ ...(fetchOptions.headers as Record),
+ };
+
+ if (token) {
+ headers['Authorization'] = `Bearer ${token}`;
+ }
+
+ if (fetchOptions.body && typeof fetchOptions.body === 'string') {
+ headers['Content-Type'] = 'application/json';
+ }
+
+ const res = await fetch(url, { ...fetchOptions, headers });
+
+ if (res.status === 401) {
+ // Try refresh
+ const refreshed = await refreshToken();
+ if (refreshed) {
+ headers['Authorization'] = `Bearer ${localStorage.getItem('access_token')}`;
+ const retry = await fetch(url, { ...fetchOptions, headers });
+ if (!retry.ok) throw new ApiError(retry.status, await retry.json().catch(() => null));
+ if (retry.status === 204) return undefined as T;
+ return retry.json();
+ }
+ localStorage.removeItem('access_token');
+ window.location.href = '/login';
+ throw new ApiError(401, null);
+ }
+
+ if (!res.ok) {
+ throw new ApiError(res.status, await res.json().catch(() => null));
+ }
+
+ if (res.status === 204) return undefined as T;
+ return res.json();
+}
+
+async function refreshToken(): Promise {
+ try {
+ const res = await fetch(`${API_BASE}/auth/refresh`, {
+ method: 'POST',
+ credentials: 'include',
+ });
+ if (!res.ok) return false;
+ const data = await res.json();
+ localStorage.setItem('access_token', data.accessToken);
+ return true;
+ } catch {
+ return false;
+ }
+}
+
+export const api = {
+ get: (path: string, params?: Record) =>
+ request(path, { params }),
+
+ post: (path: string, body?: unknown) =>
+ request(path, {
+ method: 'POST',
+ body: body ? JSON.stringify(body) : undefined,
+ }),
+
+ patch: (path: string, body?: unknown) =>
+ request(path, {
+ method: 'PATCH',
+ body: body ? JSON.stringify(body) : undefined,
+ }),
+
+ delete: (path: string) =>
+ request(path, { method: 'DELETE' }),
+};
+
+export { ApiError };
diff --git a/apps/web/src/lib/socket.ts b/apps/web/src/lib/socket.ts
new file mode 100644
index 0000000..c2043c8
--- /dev/null
+++ b/apps/web/src/lib/socket.ts
@@ -0,0 +1,30 @@
+import { io, Socket } from 'socket.io-client';
+
+let socket: Socket | null = null;
+
+export function getSocket(): Socket {
+ if (!socket) {
+ socket = io('/', {
+ path: '/socket.io',
+ auth: {
+ token: localStorage.getItem('access_token'),
+ },
+ autoConnect: false,
+ });
+ }
+ return socket;
+}
+
+export function connectSocket() {
+ const s = getSocket();
+ if (!s.connected) {
+ s.auth = { token: localStorage.getItem('access_token') };
+ s.connect();
+ }
+}
+
+export function disconnectSocket() {
+ if (socket?.connected) {
+ socket.disconnect();
+ }
+}
diff --git a/apps/web/src/lib/utils.ts b/apps/web/src/lib/utils.ts
new file mode 100644
index 0000000..66745f7
--- /dev/null
+++ b/apps/web/src/lib/utils.ts
@@ -0,0 +1,51 @@
+export function formatBytes(bytes: number, decimals = 1): string {
+ if (bytes === 0) return '0 B';
+ const k = 1024;
+ const sizes = ['B', 'KB', 'MB', 'GB', 'TB'];
+ const i = Math.floor(Math.log(bytes) / Math.log(k));
+ return `${parseFloat((bytes / Math.pow(k, i)).toFixed(decimals))} ${sizes[i]}`;
+}
+
+export function formatUptime(seconds: number): string {
+ const days = Math.floor(seconds / 86400);
+ const hours = Math.floor((seconds % 86400) / 3600);
+ const mins = Math.floor((seconds % 3600) / 60);
+ if (days > 0) return `${days}d ${hours}h`;
+ if (hours > 0) return `${hours}h ${mins}m`;
+ return `${mins}m`;
+}
+
+export function statusColor(status: string): string {
+ switch (status) {
+ case 'running':
+ return 'text-green-500';
+ case 'stopped':
+ return 'text-red-500';
+ case 'starting':
+ case 'stopping':
+ case 'installing':
+ return 'text-yellow-500';
+ case 'suspended':
+ return 'text-orange-500';
+ case 'error':
+ return 'text-destructive';
+ default:
+ return 'text-muted-foreground';
+ }
+}
+
+export function statusBadgeVariant(
+ status: string,
+): 'default' | 'secondary' | 'destructive' | 'outline' {
+ switch (status) {
+ case 'running':
+ return 'default';
+ case 'stopped':
+ return 'secondary';
+ case 'error':
+ case 'suspended':
+ return 'destructive';
+ default:
+ return 'outline';
+ }
+}
diff --git a/apps/web/src/pages/admin/audit-logs.tsx b/apps/web/src/pages/admin/audit-logs.tsx
new file mode 100644
index 0000000..483791f
--- /dev/null
+++ b/apps/web/src/pages/admin/audit-logs.tsx
@@ -0,0 +1,58 @@
+import { useQuery } from '@tanstack/react-query';
+import { api } from '@/lib/api';
+import { Card, CardContent } from '@/components/ui/card';
+import { Badge } from '@/components/ui/badge';
+
+interface AuditLog {
+ id: string;
+ action: string;
+ username: string;
+ ipAddress: string | null;
+ metadata: Record;
+ createdAt: string;
+}
+
+interface PaginatedResponse {
+ data: T[];
+ meta: { total: number };
+}
+
+export function AdminAuditLogsPage() {
+ const { data } = useQuery({
+ queryKey: ['admin-audit-logs'],
+ queryFn: () => api.get>('/admin/audit-logs'),
+ });
+
+ const logs = data?.data ?? [];
+
+ return (
+
+
Audit Logs
+
+
+
+ {logs.map((log) => (
+
+
+ {log.action}
+
+ {log.username}
+ {log.ipAddress && (
+ from {log.ipAddress}
+ )}
+
+
+
+ {new Date(log.createdAt).toLocaleString()}
+
+
+ ))}
+ {logs.length === 0 && (
+
No audit logs
+ )}
+
+
+
+
+ );
+}
diff --git a/apps/web/src/pages/admin/games.tsx b/apps/web/src/pages/admin/games.tsx
new file mode 100644
index 0000000..da5d88a
--- /dev/null
+++ b/apps/web/src/pages/admin/games.tsx
@@ -0,0 +1,152 @@
+import { useState } from 'react';
+import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
+import { Plus, Gamepad2 } from 'lucide-react';
+import { api } from '@/lib/api';
+import { Button } from '@/components/ui/button';
+import { Input } from '@/components/ui/input';
+import { Label } from '@/components/ui/label';
+import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
+import { Badge } from '@/components/ui/badge';
+import {
+ Dialog,
+ DialogContent,
+ DialogFooter,
+ DialogHeader,
+ DialogTitle,
+ DialogTrigger,
+} from '@/components/ui/dialog';
+
+interface Game {
+ id: string;
+ slug: string;
+ name: string;
+ dockerImage: string;
+ defaultPort: number;
+ startupCommand: string;
+}
+
+interface PaginatedResponse {
+ data: T[];
+ meta: { total: number };
+}
+
+export function AdminGamesPage() {
+ const queryClient = useQueryClient();
+ const [open, setOpen] = useState(false);
+ const [name, setName] = useState('');
+ const [slug, setSlug] = useState('');
+ const [dockerImage, setDockerImage] = useState('');
+ const [defaultPort, setDefaultPort] = useState(25565);
+ const [startupCommand, setStartupCommand] = useState('');
+
+ const { data } = useQuery({
+ queryKey: ['admin-games'],
+ queryFn: () => api.get>('/admin/games'),
+ });
+
+ const createMutation = useMutation({
+ mutationFn: (body: Record) => api.post('/admin/games', body),
+ onSuccess: () => {
+ queryClient.invalidateQueries({ queryKey: ['admin-games'] });
+ setOpen(false);
+ setName('');
+ setSlug('');
+ setDockerImage('');
+ setStartupCommand('');
+ },
+ });
+
+ const games = data?.data ?? [];
+
+ return (
+
+
+
Games
+
+
+
+
+ {games.map((game) => (
+
+
+
+ {game.name}
+
+
+
+
+ {game.slug}
+
+
{game.dockerImage}
+
Port: {game.defaultPort}
+
+
+
+ ))}
+
+
+ );
+}
diff --git a/apps/web/src/pages/admin/users.tsx b/apps/web/src/pages/admin/users.tsx
new file mode 100644
index 0000000..509fc94
--- /dev/null
+++ b/apps/web/src/pages/admin/users.tsx
@@ -0,0 +1,64 @@
+import { useQuery } from '@tanstack/react-query';
+import { api } from '@/lib/api';
+import { Badge } from '@/components/ui/badge';
+import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
+
+interface User {
+ id: string;
+ email: string;
+ username: string;
+ isSuperAdmin: boolean;
+ createdAt: string;
+}
+
+interface PaginatedResponse {
+ data: T[];
+ meta: { total: number };
+}
+
+export function AdminUsersPage() {
+ const { data } = useQuery({
+ queryKey: ['admin-users'],
+ queryFn: () => api.get>('/admin/users'),
+ });
+
+ const users = data?.data ?? [];
+
+ return (
+
+
Users
+
+
+
+
+
+ | Username |
+ Email |
+ Role |
+ Created |
+
+
+
+ {users.map((user) => (
+
+ | {user.username} |
+ {user.email} |
+
+ {user.isSuperAdmin ? (
+ Admin
+ ) : (
+ User
+ )}
+ |
+
+ {new Date(user.createdAt).toLocaleDateString()}
+ |
+
+ ))}
+
+
+
+
+
+ );
+}
diff --git a/apps/web/src/pages/auth/login.tsx b/apps/web/src/pages/auth/login.tsx
new file mode 100644
index 0000000..98518e3
--- /dev/null
+++ b/apps/web/src/pages/auth/login.tsx
@@ -0,0 +1,89 @@
+import { useState } from 'react';
+import { Link, useNavigate } from 'react-router';
+import { Shield } from 'lucide-react';
+import { Button } from '@/components/ui/button';
+import { Input } from '@/components/ui/input';
+import { Label } from '@/components/ui/label';
+import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from '@/components/ui/card';
+import { useAuthStore } from '@/stores/auth';
+import { ApiError } from '@/lib/api';
+
+export function LoginPage() {
+ const navigate = useNavigate();
+ const login = useAuthStore((s) => s.login);
+ const [email, setEmail] = useState('');
+ const [password, setPassword] = useState('');
+ const [error, setError] = useState('');
+ const [loading, setLoading] = useState(false);
+
+ const handleSubmit = async (e: React.FormEvent) => {
+ e.preventDefault();
+ setError('');
+ setLoading(true);
+ try {
+ await login(email, password);
+ navigate('/');
+ } catch (err) {
+ if (err instanceof ApiError) {
+ setError(err.status === 401 ? 'Invalid email or password' : 'An error occurred');
+ } else {
+ setError('An error occurred');
+ }
+ } finally {
+ setLoading(false);
+ }
+ };
+
+ return (
+
+
+
+
+
+
+ Welcome back
+ Sign in to your GamePanel account
+
+
+
+
+ );
+}
diff --git a/apps/web/src/pages/auth/register.tsx b/apps/web/src/pages/auth/register.tsx
new file mode 100644
index 0000000..d3c8546
--- /dev/null
+++ b/apps/web/src/pages/auth/register.tsx
@@ -0,0 +1,102 @@
+import { useState } from 'react';
+import { Link, useNavigate } from 'react-router';
+import { Shield } from 'lucide-react';
+import { Button } from '@/components/ui/button';
+import { Input } from '@/components/ui/input';
+import { Label } from '@/components/ui/label';
+import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from '@/components/ui/card';
+import { useAuthStore } from '@/stores/auth';
+import { ApiError } from '@/lib/api';
+
+export function RegisterPage() {
+ const navigate = useNavigate();
+ const register = useAuthStore((s) => s.register);
+ const [email, setEmail] = useState('');
+ const [username, setUsername] = useState('');
+ const [password, setPassword] = useState('');
+ const [error, setError] = useState('');
+ const [loading, setLoading] = useState(false);
+
+ const handleSubmit = async (e: React.FormEvent) => {
+ e.preventDefault();
+ setError('');
+ setLoading(true);
+ try {
+ await register(email, username, password);
+ navigate('/');
+ } catch (err) {
+ if (err instanceof ApiError) {
+ setError(err.status === 409 ? 'Email or username already taken' : 'An error occurred');
+ } else {
+ setError('An error occurred');
+ }
+ } finally {
+ setLoading(false);
+ }
+ };
+
+ return (
+
+
+
+
+
+
+ Create an account
+ Get started with GamePanel
+
+
+
+
+ );
+}
diff --git a/apps/web/src/pages/dashboard/index.tsx b/apps/web/src/pages/dashboard/index.tsx
new file mode 100644
index 0000000..3a238e1
--- /dev/null
+++ b/apps/web/src/pages/dashboard/index.tsx
@@ -0,0 +1,125 @@
+import { useParams, Link } from 'react-router';
+import { useQuery } from '@tanstack/react-query';
+import { Server, Network, Activity, Plus } from 'lucide-react';
+import { api } from '@/lib/api';
+import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
+import { Button } from '@/components/ui/button';
+import { Badge } from '@/components/ui/badge';
+import { statusBadgeVariant } from '@/lib/utils';
+
+interface ServerSummary {
+ id: string;
+ uuid: string;
+ name: string;
+ status: string;
+ gameName: string;
+ nodeName: string;
+ port: number;
+}
+
+interface PaginatedResponse {
+ data: T[];
+ meta: { total: number; page: number; perPage: number; totalPages: number };
+}
+
+export function DashboardPage() {
+ const { orgId } = useParams();
+
+ const { data: serversData } = useQuery({
+ queryKey: ['servers', orgId],
+ queryFn: () => api.get>(`/organizations/${orgId}/servers`),
+ });
+
+ const { data: nodesData } = useQuery({
+ queryKey: ['nodes', orgId],
+ queryFn: () => api.get>(`/organizations/${orgId}/nodes`),
+ });
+
+ const servers = serversData?.data ?? [];
+ const running = servers.filter((s) => s.status === 'running').length;
+ const totalNodes = nodesData?.meta.total ?? 0;
+
+ return (
+
+
+
Dashboard
+
+
+
+
+
+
+
+
+ Total Servers
+
+
+
+ {servers.length}
+
+
+
+
+ Running
+
+
+
+ {running}
+
+
+
+
+ Nodes
+
+
+
+ {totalNodes}
+
+
+
+
+
+
Servers
+ {servers.length === 0 ? (
+
+
+
+ No servers yet
+
+
+
+
+
+ ) : (
+
+ {servers.map((server) => (
+
+
+
+
+
+
+
+
+
{server.name}
+
+ {server.gameName} · {server.nodeName} · :{server.port}
+
+
+
+ {server.status}
+
+
+
+ ))}
+
+ )}
+
+
+ );
+}
diff --git a/apps/web/src/pages/nodes/index.tsx b/apps/web/src/pages/nodes/index.tsx
new file mode 100644
index 0000000..83af433
--- /dev/null
+++ b/apps/web/src/pages/nodes/index.tsx
@@ -0,0 +1,182 @@
+import { useState } from 'react';
+import { useParams } from 'react-router';
+import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
+import { Plus, Network, Wifi, WifiOff } from 'lucide-react';
+import { api } from '@/lib/api';
+import { formatBytes } from '@/lib/utils';
+import { Button } from '@/components/ui/button';
+import { Input } from '@/components/ui/input';
+import { Label } from '@/components/ui/label';
+import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
+import { Badge } from '@/components/ui/badge';
+import {
+ Dialog,
+ DialogContent,
+ DialogFooter,
+ DialogHeader,
+ DialogTitle,
+ DialogTrigger,
+} from '@/components/ui/dialog';
+
+interface NodeItem {
+ id: string;
+ name: string;
+ fqdn: string;
+ daemonPort: number;
+ grpcPort: number;
+ memoryTotal: number;
+ diskTotal: number;
+ isOnline: boolean;
+}
+
+interface PaginatedResponse {
+ data: T[];
+ meta: { total: number };
+}
+
+export function NodesPage() {
+ const { orgId } = useParams();
+ const queryClient = useQueryClient();
+ const [open, setOpen] = useState(false);
+ const [name, setName] = useState('');
+ const [fqdn, setFqdn] = useState('');
+ const [daemonPort, setDaemonPort] = useState(8443);
+ const [grpcPort, setGrpcPort] = useState(50051);
+ const [memoryTotal, setMemoryTotal] = useState(8192);
+ const [diskTotal, setDiskTotal] = useState(51200);
+
+ const { data } = useQuery({
+ queryKey: ['nodes', orgId],
+ queryFn: () => api.get>(`/organizations/${orgId}/nodes`),
+ });
+
+ const createMutation = useMutation({
+ mutationFn: (body: Record) =>
+ api.post(`/organizations/${orgId}/nodes`, body),
+ onSuccess: () => {
+ queryClient.invalidateQueries({ queryKey: ['nodes', orgId] });
+ setOpen(false);
+ setName('');
+ setFqdn('');
+ },
+ });
+
+ const nodes = data?.data ?? [];
+
+ return (
+
+
+
+
Nodes
+
Manage your daemon nodes
+
+
+
+
+
+ {nodes.map((node) => (
+
+
+
+
+ {node.name}
+
+
+ {node.isOnline ? (
+ <> Online>
+ ) : (
+ <> Offline>
+ )}
+
+
+
+ {node.fqdn}:{node.daemonPort}
+
+ {formatBytes(node.memoryTotal)} RAM
+ {formatBytes(node.diskTotal)} Disk
+
+
+
+ ))}
+
+
+ );
+}
diff --git a/apps/web/src/pages/organizations/index.tsx b/apps/web/src/pages/organizations/index.tsx
new file mode 100644
index 0000000..48b8fd4
--- /dev/null
+++ b/apps/web/src/pages/organizations/index.tsx
@@ -0,0 +1,129 @@
+import { useState } from 'react';
+import { Link } from 'react-router';
+import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
+import { Plus, Building2 } from 'lucide-react';
+import { api } from '@/lib/api';
+import { Button } from '@/components/ui/button';
+import { Input } from '@/components/ui/input';
+import { Label } from '@/components/ui/label';
+import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card';
+import {
+ Dialog,
+ DialogContent,
+ DialogFooter,
+ DialogHeader,
+ DialogTitle,
+ DialogTrigger,
+} from '@/components/ui/dialog';
+
+interface Organization {
+ id: string;
+ name: string;
+ slug: string;
+ maxServers: number;
+ maxNodes: number;
+ createdAt: string;
+}
+
+interface PaginatedResponse {
+ data: T[];
+ meta: { total: number; page: number; perPage: number; totalPages: number };
+}
+
+export function OrganizationsPage() {
+ const queryClient = useQueryClient();
+ const [open, setOpen] = useState(false);
+ const [name, setName] = useState('');
+ const [slug, setSlug] = useState('');
+
+ const { data } = useQuery({
+ queryKey: ['organizations'],
+ queryFn: () => api.get>('/organizations'),
+ });
+
+ const createMutation = useMutation({
+ mutationFn: (body: { name: string; slug: string }) => api.post('/organizations', body),
+ onSuccess: () => {
+ queryClient.invalidateQueries({ queryKey: ['organizations'] });
+ setOpen(false);
+ setName('');
+ setSlug('');
+ },
+ });
+
+ return (
+
+
+
+
Organizations
+
Manage your organizations
+
+
+
+
+
+ {data?.data.map((org) => (
+
+
+
+
+
+
+
+
+ {org.name}
+ {org.slug}
+
+
+
+
+
+ Max {org.maxServers} servers
+ Max {org.maxNodes} nodes
+
+
+
+
+ ))}
+
+
+ );
+}
diff --git a/apps/web/src/pages/server/backups.tsx b/apps/web/src/pages/server/backups.tsx
new file mode 100644
index 0000000..711e0a0
--- /dev/null
+++ b/apps/web/src/pages/server/backups.tsx
@@ -0,0 +1,17 @@
+import { useParams } from 'react-router';
+import { HardDrive } from 'lucide-react';
+import { Card, CardContent } from '@/components/ui/card';
+
+export function BackupsPage() {
+ const { serverId } = useParams();
+
+ return (
+
+
+
+ Backup management coming soon
+ Server: {serverId}
+
+
+ );
+}
diff --git a/apps/web/src/pages/server/console.tsx b/apps/web/src/pages/server/console.tsx
new file mode 100644
index 0000000..a8d5356
--- /dev/null
+++ b/apps/web/src/pages/server/console.tsx
@@ -0,0 +1,127 @@
+import { useEffect, useRef, useState } from 'react';
+import { useParams } from 'react-router';
+import { Terminal } from '@xterm/xterm';
+import { FitAddon } from '@xterm/addon-fit';
+import { WebLinksAddon } from '@xterm/addon-web-links';
+import '@xterm/xterm/css/xterm.css';
+import { getSocket, connectSocket } from '@/lib/socket';
+import { Input } from '@/components/ui/input';
+import { Button } from '@/components/ui/button';
+import { Send } from 'lucide-react';
+import { Card, CardContent } from '@/components/ui/card';
+
+export function ConsolePage() {
+ const { orgId, serverId } = useParams();
+ const termRef = useRef(null);
+ const terminalRef = useRef(null);
+ const fitAddonRef = useRef(null);
+ const [command, setCommand] = useState('');
+ const [history, setHistory] = useState([]);
+ const [historyIndex, setHistoryIndex] = useState(-1);
+
+ useEffect(() => {
+ if (!termRef.current) return;
+
+ const terminal = new Terminal({
+ cursorBlink: false,
+ disableStdin: true,
+ fontSize: 13,
+ fontFamily: '"JetBrains Mono", "Fira Code", monospace',
+ theme: {
+ background: '#0a0a0f',
+ foreground: '#d4d4d8',
+ cursor: '#d4d4d8',
+ selectionBackground: '#27272a',
+ },
+ scrollback: 5000,
+ convertEol: true,
+ });
+
+ const fitAddon = new FitAddon();
+ terminal.loadAddon(fitAddon);
+ terminal.loadAddon(new WebLinksAddon());
+ terminal.open(termRef.current);
+ fitAddon.fit();
+
+ terminalRef.current = terminal;
+ fitAddonRef.current = fitAddon;
+
+ terminal.writeln('\x1b[90m--- Console connected ---\x1b[0m');
+
+ // Socket.IO connection
+ connectSocket();
+ const socket = getSocket();
+
+ socket.emit('server:console:join', { serverId });
+
+ const handleOutput = (data: { line: string }) => {
+ terminal.writeln(data.line);
+ };
+
+ socket.on('server:console:output', handleOutput);
+
+ const handleResize = () => fitAddon.fit();
+ window.addEventListener('resize', handleResize);
+
+ return () => {
+ socket.off('server:console:output', handleOutput);
+ socket.emit('server:console:leave', { serverId });
+ window.removeEventListener('resize', handleResize);
+ terminal.dispose();
+ };
+ }, [serverId]);
+
+ const sendCommand = () => {
+ if (!command.trim()) return;
+ const socket = getSocket();
+ socket.emit('server:console:command', { serverId, orgId, command: command.trim() });
+ setHistory((prev) => [...prev, command.trim()]);
+ setHistoryIndex(-1);
+ setCommand('');
+ };
+
+ const handleKeyDown = (e: React.KeyboardEvent) => {
+ if (e.key === 'Enter') {
+ sendCommand();
+ } else if (e.key === 'ArrowUp') {
+ e.preventDefault();
+ if (history.length === 0) return;
+ const newIndex = historyIndex < history.length - 1 ? historyIndex + 1 : historyIndex;
+ setHistoryIndex(newIndex);
+ setCommand(history[history.length - 1 - newIndex] ?? '');
+ } else if (e.key === 'ArrowDown') {
+ e.preventDefault();
+ if (historyIndex <= 0) {
+ setHistoryIndex(-1);
+ setCommand('');
+ } else {
+ const newIndex = historyIndex - 1;
+ setHistoryIndex(newIndex);
+ setCommand(history[history.length - 1 - newIndex] ?? '');
+ }
+ }
+ };
+
+ return (
+
+
+
+
+
+
+
+
+ setCommand(e.target.value)}
+ onKeyDown={handleKeyDown}
+ className="font-mono text-sm"
+ />
+
+
+
+ );
+}
diff --git a/apps/web/src/pages/server/files.tsx b/apps/web/src/pages/server/files.tsx
new file mode 100644
index 0000000..7d92be3
--- /dev/null
+++ b/apps/web/src/pages/server/files.tsx
@@ -0,0 +1,279 @@
+import { useState } from 'react';
+import { useParams } from 'react-router';
+import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
+import {
+ Folder,
+ FileText,
+ ArrowUp,
+ Trash2,
+ Plus,
+ Download,
+ Upload,
+ Save,
+ X,
+} from 'lucide-react';
+import { api } from '@/lib/api';
+import { formatBytes } from '@/lib/utils';
+import { Button } from '@/components/ui/button';
+import { Input } from '@/components/ui/input';
+import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
+import {
+ Dialog,
+ DialogContent,
+ DialogFooter,
+ DialogHeader,
+ DialogTitle,
+ DialogClose,
+} from '@/components/ui/dialog';
+
+interface FileEntry {
+ name: string;
+ path: string;
+ isDirectory: boolean;
+ size: number;
+ modifiedAt: number;
+}
+
+export function FilesPage() {
+ const { orgId, serverId } = useParams();
+ const queryClient = useQueryClient();
+ const [currentPath, setCurrentPath] = useState('/');
+ const [editingFile, setEditingFile] = useState<{ path: string; content: string } | null>(null);
+ const [newFileName, setNewFileName] = useState('');
+ const [showNewFile, setShowNewFile] = useState(false);
+ const [deleteTarget, setDeleteTarget] = useState(null);
+
+ const filesQuery = useQuery({
+ queryKey: ['files', orgId, serverId, currentPath],
+ queryFn: () =>
+ api.get<{ files: FileEntry[] }>(
+ `/organizations/${orgId}/servers/${serverId}/files`,
+ { path: currentPath },
+ ),
+ enabled: !editingFile,
+ });
+
+ const deleteMutation = useMutation({
+ mutationFn: (paths: string[]) =>
+ api.post(`/organizations/${orgId}/servers/${serverId}/files/delete`, { paths }),
+ onSuccess: () => {
+ queryClient.invalidateQueries({ queryKey: ['files', orgId, serverId, currentPath] });
+ setDeleteTarget(null);
+ },
+ });
+
+ const saveMutation = useMutation({
+ mutationFn: ({ path, data }: { path: string; data: string }) =>
+ api.post(`/organizations/${orgId}/servers/${serverId}/files/write`, { path, data }),
+ onSuccess: () => {
+ setEditingFile(null);
+ queryClient.invalidateQueries({ queryKey: ['files', orgId, serverId, currentPath] });
+ },
+ });
+
+ const createFileMutation = useMutation({
+ mutationFn: ({ path, data }: { path: string; data: string }) =>
+ api.post(`/organizations/${orgId}/servers/${serverId}/files/write`, { path, data }),
+ onSuccess: () => {
+ setShowNewFile(false);
+ setNewFileName('');
+ queryClient.invalidateQueries({ queryKey: ['files', orgId, serverId, currentPath] });
+ },
+ });
+
+ const openFile = async (file: FileEntry) => {
+ if (file.isDirectory) {
+ setCurrentPath(file.path);
+ return;
+ }
+ const res = await api.get<{ data: string }>(
+ `/organizations/${orgId}/servers/${serverId}/files/read`,
+ { path: file.path },
+ );
+ setEditingFile({ path: file.path, content: res.data });
+ };
+
+ const goUp = () => {
+ if (currentPath === '/') return;
+ const parts = currentPath.split('/').filter(Boolean);
+ parts.pop();
+ setCurrentPath('/' + parts.join('/'));
+ };
+
+ const breadcrumbs = currentPath.split('/').filter(Boolean);
+
+ const files = filesQuery.data?.files ?? [];
+
+ return (
+
+ {editingFile ? (
+
+
+ {editingFile.path}
+
+
+
+
+
+
+
+
+ ) : (
+ <>
+
+
+
+
/
+ {breadcrumbs.map((crumb, i) => (
+
+
+ {i < breadcrumbs.length - 1 && (
+ /
+ )}
+
+ ))}
+
+
+
+
+ {showNewFile && (
+
+ setNewFileName(e.target.value)}
+ onKeyDown={(e) => {
+ if (e.key === 'Enter' && newFileName) {
+ const path =
+ currentPath === '/' ? `/${newFileName}` : `${currentPath}/${newFileName}`;
+ createFileMutation.mutate({ path, data: '' });
+ }
+ }}
+ />
+
+
+
+ )}
+
+
+
+
+ {files.length === 0 && (
+
+ This directory is empty
+
+ )}
+ {files.map((file) => (
+
openFile(file)}
+ >
+
+ {file.isDirectory ? (
+
+ ) : (
+
+ )}
+ {file.name}
+
+
+ {!file.isDirectory && (
+
+ {formatBytes(file.size)}
+
+ )}
+
+
+
+ ))}
+
+
+
+
+
+ >
+ )}
+
+ );
+}
diff --git a/apps/web/src/pages/server/players.tsx b/apps/web/src/pages/server/players.tsx
new file mode 100644
index 0000000..8c16739
--- /dev/null
+++ b/apps/web/src/pages/server/players.tsx
@@ -0,0 +1,13 @@
+import { Users } from 'lucide-react';
+import { Card, CardContent } from '@/components/ui/card';
+
+export function PlayersPage() {
+ return (
+
+
+
+ Active player tracking coming soon
+
+
+ );
+}
diff --git a/apps/web/src/pages/server/plugins.tsx b/apps/web/src/pages/server/plugins.tsx
new file mode 100644
index 0000000..ffaa179
--- /dev/null
+++ b/apps/web/src/pages/server/plugins.tsx
@@ -0,0 +1,13 @@
+import { Puzzle } from 'lucide-react';
+import { Card, CardContent } from '@/components/ui/card';
+
+export function PluginsPage() {
+ return (
+
+
+
+ Plugin management coming soon
+
+
+ );
+}
diff --git a/apps/web/src/pages/server/schedules.tsx b/apps/web/src/pages/server/schedules.tsx
new file mode 100644
index 0000000..a841f33
--- /dev/null
+++ b/apps/web/src/pages/server/schedules.tsx
@@ -0,0 +1,13 @@
+import { Calendar } from 'lucide-react';
+import { Card, CardContent } from '@/components/ui/card';
+
+export function SchedulesPage() {
+ return (
+
+
+
+ Scheduled tasks coming soon
+
+
+ );
+}
diff --git a/apps/web/src/pages/server/settings.tsx b/apps/web/src/pages/server/settings.tsx
new file mode 100644
index 0000000..e9d699e
--- /dev/null
+++ b/apps/web/src/pages/server/settings.tsx
@@ -0,0 +1,101 @@
+import { useState } from 'react';
+import { useParams, useOutletContext } from 'react-router';
+import { useMutation, useQueryClient } from '@tanstack/react-query';
+import { api } from '@/lib/api';
+import { Button } from '@/components/ui/button';
+import { Input } from '@/components/ui/input';
+import { Label } from '@/components/ui/label';
+import { Card, CardContent, CardHeader, CardTitle, CardDescription } from '@/components/ui/card';
+import { formatBytes } from '@/lib/utils';
+
+interface ServerDetail {
+ id: string;
+ name: string;
+ description?: string;
+ memoryLimit: number;
+ diskLimit: number;
+ cpuLimit: number;
+ startupOverride?: string;
+ environment?: Record;
+}
+
+export function ServerSettingsPage() {
+ const { orgId, serverId } = useParams();
+ const { server } = useOutletContext<{ server?: ServerDetail }>();
+ const queryClient = useQueryClient();
+
+ const [name, setName] = useState(server?.name ?? '');
+ const [description, setDescription] = useState(server?.description ?? '');
+
+ const updateMutation = useMutation({
+ mutationFn: (body: Record) =>
+ api.patch(`/organizations/${orgId}/servers/${serverId}`, body),
+ onSuccess: () => {
+ queryClient.invalidateQueries({ queryKey: ['server', orgId, serverId] });
+ },
+ });
+
+ return (
+
+
+
+ General
+ Basic server information
+
+
+
+
+ setName(e.target.value)} />
+
+
+
+ setDescription(e.target.value)} />
+
+
+
+
+
+
+
+ Resources
+ Current resource limits
+
+
+
+
+
Memory
+
+ {server ? formatBytes(server.memoryLimit) : '—'}
+
+
+
+
Disk
+
+ {server ? formatBytes(server.diskLimit) : '—'}
+
+
+
+
CPU
+
{server?.cpuLimit ?? '—'}%
+
+
+
+
+
+
+
+ Danger Zone
+ Irreversible actions
+
+
+
+
+
+
+ );
+}
diff --git a/apps/web/src/pages/servers/create.tsx b/apps/web/src/pages/servers/create.tsx
new file mode 100644
index 0000000..691301f
--- /dev/null
+++ b/apps/web/src/pages/servers/create.tsx
@@ -0,0 +1,277 @@
+import { useState } from 'react';
+import { useParams, useNavigate } from 'react-router';
+import { useQuery, useMutation } from '@tanstack/react-query';
+import { api } from '@/lib/api';
+import { formatBytes } from '@/lib/utils';
+import { Button } from '@/components/ui/button';
+import { Input } from '@/components/ui/input';
+import { Label } from '@/components/ui/label';
+import { Card, CardContent, CardHeader, CardTitle, CardDescription } from '@/components/ui/card';
+import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';
+
+interface Game {
+ id: string;
+ name: string;
+ slug: string;
+ dockerImage: string;
+}
+
+interface Node {
+ id: string;
+ name: string;
+ fqdn: string;
+ memoryTotal: number;
+ diskTotal: number;
+}
+
+interface Allocation {
+ id: string;
+ ip: string;
+ port: number;
+ serverId: string | null;
+}
+
+interface PaginatedResponse {
+ data: T[];
+ meta: { total: number };
+}
+
+export function CreateServerPage() {
+ const { orgId } = useParams();
+ const navigate = useNavigate();
+
+ const [step, setStep] = useState(1);
+ const [name, setName] = useState('');
+ const [description, setDescription] = useState('');
+ const [gameId, setGameId] = useState('');
+ const [nodeId, setNodeId] = useState('');
+ const [allocationId, setAllocationId] = useState('');
+ const [memoryLimit, setMemoryLimit] = useState(1024);
+ const [diskLimit, setDiskLimit] = useState(5120);
+ const [cpuLimit, setCpuLimit] = useState(100);
+
+ const { data: gamesData } = useQuery({
+ queryKey: ['admin-games'],
+ queryFn: () => api.get>('/admin/games'),
+ });
+
+ const { data: nodesData } = useQuery({
+ queryKey: ['nodes', orgId],
+ queryFn: () => api.get>(`/organizations/${orgId}/nodes`),
+ });
+
+ const { data: allocationsData } = useQuery({
+ queryKey: ['allocations', orgId, nodeId],
+ queryFn: () =>
+ api.get>(
+ `/organizations/${orgId}/nodes/${nodeId}/allocations`,
+ ),
+ enabled: !!nodeId,
+ });
+
+ const freeAllocations = (allocationsData?.data ?? []).filter((a) => !a.serverId);
+
+ const createMutation = useMutation({
+ mutationFn: (body: Record) =>
+ api.post(`/organizations/${orgId}/servers`, body),
+ onSuccess: () => {
+ navigate(`/org/${orgId}/dashboard`);
+ },
+ });
+
+ const games = gamesData?.data ?? [];
+ const nodes = nodesData?.data ?? [];
+
+ const handleCreate = () => {
+ createMutation.mutate({
+ name,
+ description: description || undefined,
+ gameId,
+ nodeId,
+ allocationId,
+ memoryLimit: memoryLimit * 1024 * 1024,
+ diskLimit: diskLimit * 1024 * 1024,
+ cpuLimit,
+ });
+ };
+
+ return (
+
+
+
Create Server
+
Set up a new game server
+
+
+
+ {[1, 2, 3].map((s) => (
+
+ ))}
+
+
+ {step === 1 && (
+
+
+ Basic Information
+ Choose a name and game for your server
+
+
+
+
+ setName(e.target.value)}
+ placeholder="My Awesome Server"
+ />
+
+
+
+ setDescription(e.target.value)}
+ placeholder="A short description"
+ />
+
+
+
+
+
+
+
+
+ )}
+
+ {step === 2 && (
+
+
+ Node & Allocation
+ Choose where to host your server
+
+
+
+
+
+
+ {nodeId && (
+
+
+
+ {freeAllocations.length === 0 && nodeId && (
+
No free allocations on this node
+ )}
+
+ )}
+
+
+
+
+
+
+ )}
+
+ {step === 3 && (
+
+
+ Resources
+ Set resource limits for this server
+
+
+
+
+
setMemoryLimit(Number(e.target.value))}
+ min={128}
+ />
+
+ {formatBytes(memoryLimit * 1024 * 1024)}
+
+
+
+
+
setDiskLimit(Number(e.target.value))}
+ min={256}
+ />
+
+ {formatBytes(diskLimit * 1024 * 1024)}
+
+
+
+
+
setCpuLimit(Number(e.target.value))}
+ min={10}
+ max={10000}
+ />
+
100% = 1 core
+
+
+
+
+
+ {createMutation.isError && (
+ Failed to create server. Please try again.
+ )}
+
+
+ )}
+
+ );
+}
diff --git a/apps/web/src/pages/settings/members.tsx b/apps/web/src/pages/settings/members.tsx
new file mode 100644
index 0000000..9220d45
--- /dev/null
+++ b/apps/web/src/pages/settings/members.tsx
@@ -0,0 +1,143 @@
+import { useState } from 'react';
+import { useParams } from 'react-router';
+import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
+import { Plus, Trash2 } from 'lucide-react';
+import { api } from '@/lib/api';
+import { Button } from '@/components/ui/button';
+import { Input } from '@/components/ui/input';
+import { Label } from '@/components/ui/label';
+import { Badge } from '@/components/ui/badge';
+import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
+import {
+ Dialog,
+ DialogContent,
+ DialogFooter,
+ DialogHeader,
+ DialogTitle,
+ DialogTrigger,
+} from '@/components/ui/dialog';
+import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';
+
+interface Member {
+ id: string;
+ userId: string;
+ username: string;
+ email: string;
+ role: 'admin' | 'user';
+}
+
+export function MembersPage() {
+ const { orgId } = useParams();
+ const queryClient = useQueryClient();
+ const [open, setOpen] = useState(false);
+ const [email, setEmail] = useState('');
+ const [role, setRole] = useState<'admin' | 'user'>('user');
+
+ const { data: members } = useQuery({
+ queryKey: ['members', orgId],
+ queryFn: () => api.get(`/organizations/${orgId}/members`),
+ });
+
+ const addMutation = useMutation({
+ mutationFn: (body: { email: string; role: string }) =>
+ api.post(`/organizations/${orgId}/members`, body),
+ onSuccess: () => {
+ queryClient.invalidateQueries({ queryKey: ['members', orgId] });
+ setOpen(false);
+ setEmail('');
+ },
+ });
+
+ const removeMutation = useMutation({
+ mutationFn: (memberId: string) =>
+ api.delete(`/organizations/${orgId}/members/${memberId}`),
+ onSuccess: () => {
+ queryClient.invalidateQueries({ queryKey: ['members', orgId] });
+ },
+ });
+
+ return (
+
+
+
+
Members
+
Manage organization members
+
+
+
+
+
+
+
+ {(members ?? []).map((member) => (
+
+
+
{member.username}
+
{member.email}
+
+
+
+ {member.role}
+
+
+
+
+ ))}
+
+
+
+
+ );
+}
diff --git a/apps/web/src/stores/auth.ts b/apps/web/src/stores/auth.ts
new file mode 100644
index 0000000..c8eb87c
--- /dev/null
+++ b/apps/web/src/stores/auth.ts
@@ -0,0 +1,69 @@
+import { create } from 'zustand';
+import { api, ApiError } from '@/lib/api';
+
+interface User {
+ id: string;
+ email: string;
+ username: string;
+ isSuperAdmin: boolean;
+ avatarUrl: string | null;
+}
+
+interface AuthState {
+ user: User | null;
+ isLoading: boolean;
+ isAuthenticated: boolean;
+
+ login: (email: string, password: string) => Promise;
+ register: (email: string, username: string, password: string) => Promise;
+ logout: () => Promise;
+ fetchUser: () => Promise;
+}
+
+export const useAuthStore = create((set) => ({
+ user: null,
+ isLoading: true,
+ isAuthenticated: false,
+
+ login: async (email, password) => {
+ const data = await api.post<{ accessToken: string; user: User }>('/auth/login', {
+ email,
+ password,
+ });
+ localStorage.setItem('access_token', data.accessToken);
+ set({ user: data.user, isAuthenticated: true });
+ },
+
+ register: async (email, username, password) => {
+ const data = await api.post<{ accessToken: string; user: User }>('/auth/register', {
+ email,
+ username,
+ password,
+ });
+ localStorage.setItem('access_token', data.accessToken);
+ set({ user: data.user, isAuthenticated: true });
+ },
+
+ logout: async () => {
+ try {
+ await api.post('/auth/logout');
+ } catch {
+ // ignore
+ }
+ localStorage.removeItem('access_token');
+ set({ user: null, isAuthenticated: false });
+ },
+
+ fetchUser: async () => {
+ try {
+ const user = await api.get('/auth/me');
+ set({ user, isAuthenticated: true, isLoading: false });
+ } catch (err) {
+ if (err instanceof ApiError && err.status === 401) {
+ set({ user: null, isAuthenticated: false, isLoading: false });
+ } else {
+ set({ isLoading: false });
+ }
+ }
+ },
+}));
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index 23bf11d..5bd69ef 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -81,6 +81,45 @@ importers:
apps/web:
dependencies:
+ '@radix-ui/react-avatar':
+ specifier: ^1.1.11
+ version: 1.1.11(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
+ '@radix-ui/react-dialog':
+ specifier: ^1.1.15
+ version: 1.1.15(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
+ '@radix-ui/react-dropdown-menu':
+ specifier: ^2.1.16
+ version: 2.1.16(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
+ '@radix-ui/react-label':
+ specifier: ^2.1.8
+ version: 2.1.8(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
+ '@radix-ui/react-progress':
+ specifier: ^1.1.8
+ version: 1.1.8(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
+ '@radix-ui/react-scroll-area':
+ specifier: ^1.2.10
+ version: 1.2.10(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
+ '@radix-ui/react-select':
+ specifier: ^2.2.6
+ version: 2.2.6(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
+ '@radix-ui/react-separator':
+ specifier: ^1.1.8
+ version: 1.1.8(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
+ '@radix-ui/react-slot':
+ specifier: ^1.2.4
+ version: 1.2.4(@types/react@19.2.14)(react@19.2.4)
+ '@radix-ui/react-switch':
+ specifier: ^1.2.6
+ version: 1.2.6(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
+ '@radix-ui/react-tabs':
+ specifier: ^1.1.13
+ version: 1.1.13(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
+ '@radix-ui/react-toast':
+ specifier: ^1.2.15
+ version: 1.2.15(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
+ '@radix-ui/react-tooltip':
+ specifier: ^1.2.8
+ version: 1.2.8(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
'@source/shared':
specifier: workspace:*
version: link:../../packages/shared
@@ -90,6 +129,21 @@ importers:
'@tanstack/react-query':
specifier: ^5.62.0
version: 5.90.21(react@19.2.4)
+ '@xterm/addon-fit':
+ specifier: ^0.11.0
+ version: 0.11.0
+ '@xterm/addon-web-links':
+ specifier: ^0.12.0
+ version: 0.12.0
+ '@xterm/xterm':
+ specifier: ^6.0.0
+ version: 6.0.0
+ class-variance-authority:
+ specifier: ^0.7.0
+ version: 0.7.1
+ lucide-react:
+ specifier: ^0.575.0
+ version: 0.575.0(react@19.2.4)
react:
specifier: ^19.0.0
version: 19.2.4
@@ -102,6 +156,12 @@ importers:
socket.io-client:
specifier: ^4.8.0
version: 4.8.3
+ sonner:
+ specifier: ^2.0.7
+ version: 2.0.7(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
+ zustand:
+ specifier: ^5.0.11
+ version: 5.0.11(@types/react@19.2.14)(react@19.2.4)(use-sync-external-store@1.6.0(react@19.2.4))
devDependencies:
'@types/react':
specifier: ^19.0.0
@@ -919,6 +979,21 @@ packages:
'@fastify/websocket@11.2.0':
resolution: {integrity: sha512-3HrDPbAG1CzUCqnslgJxppvzaAZffieOVbLp1DAy1huCSynUWPifSvfdEDUR8HlJLp3sp1A36uOM2tJogADS8w==}
+ '@floating-ui/core@1.7.4':
+ resolution: {integrity: sha512-C3HlIdsBxszvm5McXlB8PeOEWfBhcGBTZGkGlWc2U0KFY5IwG5OQEuQ8rq52DZmcHDlPLd+YFBK+cZcytwIFWg==}
+
+ '@floating-ui/dom@1.7.5':
+ resolution: {integrity: sha512-N0bD2kIPInNHUHehXhMke1rBGs1dwqvC9O9KYMyyjK7iXt7GAhnro7UlcuYcGdS/yYOlq0MAVgrow8IbWJwyqg==}
+
+ '@floating-ui/react-dom@2.1.7':
+ resolution: {integrity: sha512-0tLRojf/1Go2JgEVm+3Frg9A3IW8bJgKgdO0BN5RkF//ufuz2joZM63Npau2ff3J6lUVYgDSNzNkR+aH3IVfjg==}
+ peerDependencies:
+ react: '>=16.8.0'
+ react-dom: '>=16.8.0'
+
+ '@floating-ui/utils@0.2.10':
+ resolution: {integrity: sha512-aGTxbpbg8/b5JfU1HXSrbH3wXZuLPJcNEcZQFMxLs3oSzgtVu6nFPkbbGGUvBcUjKV2YyB9Wxxabo+HEH9tcRQ==}
+
'@humanfs/core@0.19.1':
resolution: {integrity: sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA==}
engines: {node: '>=18.18.0'}
@@ -977,6 +1052,480 @@ packages:
'@pinojs/redact@0.4.0':
resolution: {integrity: sha512-k2ENnmBugE/rzQfEcdWHcCY+/FM3VLzH9cYEsbdsoqrvzAKRhUZeRNhAZvB8OitQJ1TBed3yqWtdjzS6wJKBwg==}
+ '@radix-ui/number@1.1.1':
+ resolution: {integrity: sha512-MkKCwxlXTgz6CFoJx3pCwn07GKp36+aZyu/u2Ln2VrA5DcdyCZkASEDBTd8x5whTQQL5CiYf4prXKLcgQdv29g==}
+
+ '@radix-ui/primitive@1.1.3':
+ resolution: {integrity: sha512-JTF99U/6XIjCBo0wqkU5sK10glYe27MRRsfwoiq5zzOEZLHU3A3KCMa5X/azekYRCJ0HlwI0crAXS/5dEHTzDg==}
+
+ '@radix-ui/react-arrow@1.1.7':
+ resolution: {integrity: sha512-F+M1tLhO+mlQaOWspE8Wstg+z6PwxwRd8oQ8IXceWz92kfAmalTRf0EjrouQeo7QssEPfCn05B4Ihs1K9WQ/7w==}
+ 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-avatar@1.1.11':
+ resolution: {integrity: sha512-0Qk603AHGV28BOBO34p7IgD5m+V5Sg/YovfayABkoDDBM5d3NCx0Mp4gGrjzLGes1jV5eNOE1r3itqOR33VC6Q==}
+ 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:
+ '@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-compose-refs@1.1.2':
+ resolution: {integrity: sha512-z4eqJvfiNnFMHIIvXP3CY57y2WJs5g2v3X0zm9mEJkrkNv4rDxu+sg9Jh8EkXyeqBkB7SOcboo9dMVqhyrACIg==}
+ peerDependencies:
+ '@types/react': '*'
+ react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+ peerDependenciesMeta:
+ '@types/react':
+ optional: true
+
+ '@radix-ui/react-context@1.1.2':
+ resolution: {integrity: sha512-jCi/QKUM2r1Ju5a3J64TH2A5SpKAgh0LpknyqdQ4m6DCV0xJ2HG1xARRwNGPQfi1SLdLWZ1OJz6F4OMBBNiGJA==}
+ peerDependencies:
+ '@types/react': '*'
+ react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+ peerDependenciesMeta:
+ '@types/react':
+ optional: true
+
+ '@radix-ui/react-context@1.1.3':
+ resolution: {integrity: sha512-ieIFACdMpYfMEjF0rEf5KLvfVyIkOz6PDGyNnP+u+4xQ6jny3VCgA4OgXOwNx2aUkxn8zx9fiVcM8CfFYv9Lxw==}
+ peerDependencies:
+ '@types/react': '*'
+ react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+ peerDependenciesMeta:
+ '@types/react':
+ optional: true
+
+ '@radix-ui/react-dialog@1.1.15':
+ resolution: {integrity: sha512-TCglVRtzlffRNxRMEyR36DGBLJpeusFcgMVD9PZEzAKnUs1lKCgX5u9BmC2Yg+LL9MgZDugFFs1Vl+Jp4t/PGw==}
+ 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-direction@1.1.1':
+ resolution: {integrity: sha512-1UEWRX6jnOA2y4H5WczZ44gOOjTEmlqv1uNW4GAJEO5+bauCBhv8snY65Iw5/VOS/ghKN9gr2KjnLKxrsvoMVw==}
+ peerDependencies:
+ '@types/react': '*'
+ react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+ peerDependenciesMeta:
+ '@types/react':
+ optional: true
+
+ '@radix-ui/react-dismissable-layer@1.1.11':
+ resolution: {integrity: sha512-Nqcp+t5cTB8BinFkZgXiMJniQH0PsUt2k51FUhbdfeKvc4ACcG2uQniY/8+h1Yv6Kza4Q7lD7PQV0z0oicE0Mg==}
+ 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-dropdown-menu@2.1.16':
+ resolution: {integrity: sha512-1PLGQEynI/3OX/ftV54COn+3Sud/Mn8vALg2rWnBLnRaGtJDduNW/22XjlGgPdpcIbiQxjKtb7BkcjP00nqfJw==}
+ 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-focus-guards@1.1.3':
+ resolution: {integrity: sha512-0rFg/Rj2Q62NCm62jZw0QX7a3sz6QCQU0LpZdNrJX8byRGaGVTqbrW9jAoIAHyMQqsNpeZ81YgSizOt5WXq0Pw==}
+ peerDependencies:
+ '@types/react': '*'
+ react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+ peerDependenciesMeta:
+ '@types/react':
+ optional: true
+
+ '@radix-ui/react-focus-scope@1.1.7':
+ resolution: {integrity: sha512-t2ODlkXBQyn7jkl6TNaw/MtVEVvIGelJDCG41Okq/KwUsJBwQ4XVZsHAVUkK4mBv3ewiAS3PGuUWuY2BoK4ZUw==}
+ 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-id@1.1.1':
+ resolution: {integrity: sha512-kGkGegYIdQsOb4XjsfM97rXsiHaBwco+hFI66oO4s9LU+PLAC5oJ7khdOVFxkhsmlbpUqDAvXw11CluXP+jkHg==}
+ peerDependencies:
+ '@types/react': '*'
+ react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+ peerDependenciesMeta:
+ '@types/react':
+ optional: true
+
+ '@radix-ui/react-label@2.1.8':
+ resolution: {integrity: sha512-FmXs37I6hSBVDlO4y764TNz1rLgKwjJMQ0EGte6F3Cb3f4bIuHB/iLa/8I9VKkmOy+gNHq8rql3j686ACVV21A==}
+ 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-menu@2.1.16':
+ resolution: {integrity: sha512-72F2T+PLlphrqLcAotYPp0uJMr5SjP5SL01wfEspJbru5Zs5vQaSHb4VB3ZMJPimgHHCHG7gMOeOB9H3Hdmtxg==}
+ 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-popper@1.2.8':
+ resolution: {integrity: sha512-0NJQ4LFFUuWkE7Oxf0htBKS6zLkkjBH+hM1uk7Ng705ReR8m/uelduy1DBo0PyBXPKVnBA6YBlU94MBGXrSBCw==}
+ 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-portal@1.1.9':
+ resolution: {integrity: sha512-bpIxvq03if6UNwXZ+HTK71JLh4APvnXntDc6XOX8UVq4XQOVl7lwok0AvIl+b8zgCw3fSaVTZMpAPPagXbKmHQ==}
+ 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-presence@1.1.5':
+ resolution: {integrity: sha512-/jfEwNDdQVBCNvjkGit4h6pMOzq8bHkopq458dPt2lMjx+eBQUohZNG9A7DtO/O5ukSbxuaNGXMjHicgwy6rQQ==}
+ 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-primitive@2.1.3':
+ resolution: {integrity: sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ==}
+ 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-primitive@2.1.4':
+ resolution: {integrity: sha512-9hQc4+GNVtJAIEPEqlYqW5RiYdrr8ea5XQ0ZOnD6fgru+83kqT15mq2OCcbe8KnjRZl5vF3ks69AKz3kh1jrhg==}
+ 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-progress@1.1.8':
+ resolution: {integrity: sha512-+gISHcSPUJ7ktBy9RnTqbdKW78bcGke3t6taawyZ71pio1JewwGSJizycs7rLhGTvMJYCQB1DBK4KQsxs7U8dA==}
+ 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-roving-focus@1.1.11':
+ resolution: {integrity: sha512-7A6S9jSgm/S+7MdtNDSb+IU859vQqJ/QAtcYQcfFC6W8RS4IxIZDldLR0xqCFZ6DCyrQLjLPsxtTNch5jVA4lA==}
+ 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-scroll-area@1.2.10':
+ resolution: {integrity: sha512-tAXIa1g3sM5CGpVT0uIbUx/U3Gs5N8T52IICuCtObaos1S8fzsrPXG5WObkQN3S6NVl6wKgPhAIiBGbWnvc97A==}
+ 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-select@2.2.6':
+ resolution: {integrity: sha512-I30RydO+bnn2PQztvo25tswPH+wFBjehVGtmagkU78yMdwTwVf12wnAOF+AeP8S2N8xD+5UPbGhkUfPyvT+mwQ==}
+ 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-separator@1.1.8':
+ resolution: {integrity: sha512-sDvqVY4itsKwwSMEe0jtKgfTh+72Sy3gPmQpjqcQneqQ4PFmr/1I0YA+2/puilhggCe2gJcx5EBAYFkWkdpa5g==}
+ 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-slot@1.2.3':
+ resolution: {integrity: sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A==}
+ peerDependencies:
+ '@types/react': '*'
+ react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+ peerDependenciesMeta:
+ '@types/react':
+ optional: true
+
+ '@radix-ui/react-slot@1.2.4':
+ resolution: {integrity: sha512-Jl+bCv8HxKnlTLVrcDE8zTMJ09R9/ukw4qBs/oZClOfoQk/cOTbDn+NceXfV7j09YPVQUryJPHurafcSg6EVKA==}
+ peerDependencies:
+ '@types/react': '*'
+ react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+ peerDependenciesMeta:
+ '@types/react':
+ optional: true
+
+ '@radix-ui/react-switch@1.2.6':
+ resolution: {integrity: sha512-bByzr1+ep1zk4VubeEVViV592vu2lHE2BZY5OnzehZqOOgogN80+mNtCqPkhn2gklJqOpxWgPoYTSnhBCqpOXQ==}
+ 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-tabs@1.1.13':
+ resolution: {integrity: sha512-7xdcatg7/U+7+Udyoj2zodtI9H/IIopqo+YOIcZOq1nJwXWBZ9p8xiu5llXlekDbZkca79a/fozEYQXIA4sW6A==}
+ 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-toast@1.2.15':
+ resolution: {integrity: sha512-3OSz3TacUWy4WtOXV38DggwxoqJK4+eDkNMl5Z/MJZaoUPaP4/9lf81xXMe1I2ReTAptverZUpbPY4wWwWyL5g==}
+ 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-tooltip@1.2.8':
+ resolution: {integrity: sha512-tY7sVt1yL9ozIxvmbtN5qtmH2krXcBCfjEiCgKGLqunJHvgvZG2Pcl2oQ3kbcZARb1BGEHdkLzcYGO8ynVlieg==}
+ 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-use-callback-ref@1.1.1':
+ resolution: {integrity: sha512-FkBMwD+qbGQeMu1cOHnuGB6x4yzPjho8ap5WtbEJ26umhgqVXbhekKUQO+hZEL1vU92a3wHwdp0HAcqAUF5iDg==}
+ 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-controllable-state@1.2.2':
+ resolution: {integrity: sha512-BjasUjixPFdS+NKkypcyyN5Pmg83Olst0+c6vGov0diwTEo6mgdqVR6hxcEgFuh4QrAs7Rc+9KuGJ9TVCj0Zzg==}
+ 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-effect-event@0.0.2':
+ resolution: {integrity: sha512-Qp8WbZOBe+blgpuUT+lw2xheLP8q0oatc9UpmiemEICxGvFLYmHm9QowVZGHtJlGbS6A6yJ3iViad/2cVjnOiA==}
+ 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-escape-keydown@1.1.1':
+ resolution: {integrity: sha512-Il0+boE7w/XebUHyBjroE+DbByORGR9KKmITzbR7MyQ4akpORYP/ZmbhAr0DG7RmmBqoOnZdy2QlvajJ2QA59g==}
+ 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-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:
+ '@types/react': '*'
+ react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+ peerDependenciesMeta:
+ '@types/react':
+ optional: true
+
+ '@radix-ui/react-use-previous@1.1.1':
+ resolution: {integrity: sha512-2dHfToCj/pzca2Ck724OZ5L0EVrr3eHRNsG/b3xQJLA2hZpVCS99bLAX+hm1IHXDEnzU6by5z/5MIY794/a8NQ==}
+ 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-rect@1.1.1':
+ resolution: {integrity: sha512-QTYuDesS0VtuHNNvMh+CjlKJ4LJickCMUAqjlE3+j8w+RlRpwyX3apEQKGFzbZGdo7XNG1tXa+bQqIE7HIXT2w==}
+ 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-size@1.1.1':
+ resolution: {integrity: sha512-ewrXRDTAqAXlkl6t/fkXWNAhFX9I+CkKlw6zjEwk86RSPKwZr3xpBRso655aqYafwtnbpHLj6toFzmd6xdVptQ==}
+ peerDependencies:
+ '@types/react': '*'
+ react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+ peerDependenciesMeta:
+ '@types/react':
+ optional: true
+
+ '@radix-ui/react-visually-hidden@1.2.3':
+ resolution: {integrity: sha512-pzJq12tEaaIhqjbzpCuv/OypJY/BPavOofm+dbab+MHLajy277+1lLm6JFcGgF5eskJ6mquGirhXY2GD/8u8Ug==}
+ 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/rect@1.1.1':
+ resolution: {integrity: sha512-HPwpGIzkl28mWyZqG52jiqDJ12waP11Pa1lGoiyUkIEuMLBP0oeK/C89esbXrxsky5we7dfd8U58nm0SgAWpVw==}
+
'@rolldown/pluginutils@1.0.0-beta.27':
resolution: {integrity: sha512-+d0F4MKMCbeVUJwG96uQ4SgAznZNSq93I3V+9NHA4OpvqG8mRCpGdKmK8l/dl02h2CCDHwW2FqilnTyDcAnqjA==}
@@ -1219,6 +1768,15 @@ packages:
peerDependencies:
vite: ^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0
+ '@xterm/addon-fit@0.11.0':
+ resolution: {integrity: sha512-jYcgT6xtVYhnhgxh3QgYDnnNMYTcf8ElbxxFzX0IZo+vabQqSPAjC3c1wJrKB5E19VwQei89QCiZZP86DCPF7g==}
+
+ '@xterm/addon-web-links@0.12.0':
+ resolution: {integrity: sha512-4Smom3RPyVp7ZMYOYDoC/9eGJJJqYhnPLGGqJ6wOBfB8VxPViJNSKdgRYb8NpaM6YSelEKbA2SStD7lGyqaobw==}
+
+ '@xterm/xterm@6.0.0':
+ resolution: {integrity: sha512-TQwDdQGtwwDt+2cgKDLn0IRaSxYu1tSUjgKarSDkUM0ZNiSRXFpjxEsvc/Zgc5kq5omJ+V0a8/kIM2WD3sMOYg==}
+
abstract-logging@2.0.1:
resolution: {integrity: sha512-2BjRTZxTPvheOvGbBslFSYOUkr+SjPtOnrLP33f+VIWLzezQpZcqVg7ja3L4dBXmzzgwT+a029jRx5PCi3JuiA==}
@@ -1271,6 +1829,10 @@ packages:
argparse@2.0.1:
resolution: {integrity: sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==}
+ aria-hidden@1.2.6:
+ resolution: {integrity: sha512-ik3ZgC9dY/lYVVM++OISsaYDeg1tb0VtP5uL3ouh1koGOaUMDPpbFIei4JkFimWUFPn90sbMNMXQAIVOlnYKJA==}
+ engines: {node: '>=10'}
+
asn1.js@5.4.1:
resolution: {integrity: sha512-+I//4cYPccV8LdmBLiX8CYvf9Sp3vQsrqu2QNXRcrbiWvcx/UdlFiqUJJzxRQxgsZmvhXhn4cSKeSmoFjVdupA==}
@@ -1414,6 +1976,9 @@ packages:
resolution: {integrity: sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==}
engines: {node: '>=6'}
+ detect-node-es@1.1.0:
+ resolution: {integrity: sha512-ypdmJU/TbBby2Dxibuv7ZLW3Bs1QEmM7nHjEANfohJLvE0XVujisn1qPJcZxg+qDucsr+bP6fLD1rPS3AhJ7EQ==}
+
didyoumean@1.2.2:
resolution: {integrity: sha512-gxtyfqMg7GKyhQmb056K7M3xszy/myH8w+B4RT+QXBQsvAOdc3XymqDDPHx1BgPgsdAA5SIifona89YtRATDzw==}
@@ -1746,6 +2311,10 @@ packages:
resolution: {integrity: sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==}
engines: {node: '>=6.9.0'}
+ get-nonce@1.0.1:
+ resolution: {integrity: sha512-FJhYRoDaiatfEkUK8HKlicmu/3SGFD51q3itKDGoSTysQJBnfOcxU5GxnhE1E6soB76MbT0MBtnKJuXyAx+96Q==}
+ engines: {node: '>=6'}
+
get-tsconfig@4.13.6:
resolution: {integrity: sha512-shZT/QMiSHc/YBLxxOkMtgSid5HFoauqCE3/exfsEcwg1WkeqjG+V40yBbBrsD+jW2HDXcs28xOfcbm2jI8Ddw==}
@@ -1889,6 +2458,11 @@ packages:
lru-cache@5.1.1:
resolution: {integrity: sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==}
+ lucide-react@0.575.0:
+ resolution: {integrity: sha512-VuXgKZrk0uiDlWjGGXmKV6MSk9Yy4l10qgVvzGn2AWBx1Ylt0iBexKOAoA6I7JO3m+M9oeovJd3yYENfkUbOeg==}
+ peerDependencies:
+ react: ^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0
+
merge2@1.4.1:
resolution: {integrity: sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==}
engines: {node: '>= 8'}
@@ -2123,6 +2697,26 @@ packages:
resolution: {integrity: sha512-z6F7K9bV85EfseRCp2bzrpyQ0Gkw1uLoCel9XBVWPg/TjRj94SkJzUTGfOa4bs7iJvBWtQG0Wq7wnI0syw3EBQ==}
engines: {node: '>=0.10.0'}
+ react-remove-scroll-bar@2.3.8:
+ resolution: {integrity: sha512-9r+yi9+mgU33AKcj6IbT9oRCO78WriSj6t/cF8DWBZJ9aOGPOTEDvdUDz1FwKim7QXWwmHqtdHnRJfhAxEG46Q==}
+ engines: {node: '>=10'}
+ peerDependencies:
+ '@types/react': '*'
+ react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0
+ peerDependenciesMeta:
+ '@types/react':
+ optional: true
+
+ react-remove-scroll@2.7.2:
+ resolution: {integrity: sha512-Iqb9NjCCTt6Hf+vOdNIZGdTiH1QSqr27H/Ek9sv/a97gfueI/5h1s3yRi1nngzMUaOOToin5dI1dXKdXiF+u0Q==}
+ engines: {node: '>=10'}
+ peerDependencies:
+ '@types/react': '*'
+ react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc
+ peerDependenciesMeta:
+ '@types/react':
+ optional: true
+
react-router@7.13.0:
resolution: {integrity: sha512-PZgus8ETambRT17BUm/LL8lX3Of+oiLaPuVTRH3l1eLvSPpKO3AvhAEb5N7ihAFZQrYDqkvvWfFh9p0z9VsjLw==}
engines: {node: '>=20.0.0'}
@@ -2133,6 +2727,16 @@ packages:
react-dom:
optional: true
+ react-style-singleton@2.2.3:
+ resolution: {integrity: sha512-b6jSvxvVnyptAiLjbkWLE/lOnR4lfTtDAl+eUC7RZy+QQWc6wRzIV2CE6xBuMmDxc2qIihtDCZD5NPOFl7fRBQ==}
+ engines: {node: '>=10'}
+ peerDependencies:
+ '@types/react': '*'
+ react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc
+ peerDependenciesMeta:
+ '@types/react':
+ optional: true
+
react@19.2.4:
resolution: {integrity: sha512-9nfp2hYpCwOjAN+8TZFGhtWEwgvWHXqESH8qT89AT/lWklpLON22Lc8pEtnpsZz7VmawabSU0gCjnj8aC0euHQ==}
engines: {node: '>=0.10.0'}
@@ -2248,6 +2852,12 @@ packages:
sonic-boom@4.2.1:
resolution: {integrity: sha512-w6AxtubXa2wTXAUsZMMWERrsIRAdrK0Sc+FUytWvYAhBJLyuI4llrMIC1DtlNSdI99EI86KZum2MMq3EAZlF9Q==}
+ sonner@2.0.7:
+ resolution: {integrity: sha512-W6ZN4p58k8aDKA4XPcx2hpIQXBRAgyiWVkYhT7CvK6D3iAu7xjvVyhQHg2/iaKJZ1XVJ4r7XuwGL+WGEK37i9w==}
+ peerDependencies:
+ react: ^18.0.0 || ^19.0.0 || ^19.0.0-rc
+ react-dom: ^18.0.0 || ^19.0.0 || ^19.0.0-rc
+
source-map-js@1.2.1:
resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==}
engines: {node: '>=0.10.0'}
@@ -2333,6 +2943,9 @@ packages:
ts-interface-checker@0.1.13:
resolution: {integrity: sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA==}
+ tslib@2.8.1:
+ resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==}
+
tsx@4.21.0:
resolution: {integrity: sha512-5C1sg4USs1lfG0GFb2RLXsdpXqBSEhAaA/0kPL01wxzpMqLILNxIxIOKiILz+cdg/pLnOUxFYOR5yhHU666wbw==}
engines: {node: '>=18.0.0'}
@@ -2403,6 +3016,31 @@ packages:
uri-js@4.4.1:
resolution: {integrity: sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==}
+ use-callback-ref@1.3.3:
+ resolution: {integrity: sha512-jQL3lRnocaFtu3V00JToYz/4QkNWswxijDaCVNZRiRTO3HQDLsdu1ZtmIUvV4yPp+rvWm5j0y0TG/S61cuijTg==}
+ engines: {node: '>=10'}
+ peerDependencies:
+ '@types/react': '*'
+ react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc
+ peerDependenciesMeta:
+ '@types/react':
+ optional: true
+
+ use-sidecar@1.1.3:
+ resolution: {integrity: sha512-Fedw0aZvkhynoPYlA5WXrMCAMm+nSWdZt6lzJQ7Ok8S6Q+VsHmHpRWndVRJ8Be0ZbkfPc5LRYH+5XrzXcEeLRQ==}
+ engines: {node: '>=10'}
+ peerDependencies:
+ '@types/react': '*'
+ react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc
+ peerDependenciesMeta:
+ '@types/react':
+ optional: true
+
+ 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
+
util-deprecate@1.0.2:
resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==}
@@ -2506,6 +3144,24 @@ packages:
resolution: {integrity: sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==}
engines: {node: '>=10'}
+ zustand@5.0.11:
+ resolution: {integrity: sha512-fdZY+dk7zn/vbWNCYmzZULHRrss0jx5pPFiOuMZ/5HJN6Yv3u+1Wswy/4MpZEkEGhtNH+pwxZB8OKgUBPzYAGg==}
+ engines: {node: '>=12.20.0'}
+ peerDependencies:
+ '@types/react': '>=18.0.0'
+ immer: '>=9.0.6'
+ react: '>=18.0.0'
+ use-sync-external-store: '>=1.2.0'
+ peerDependenciesMeta:
+ '@types/react':
+ optional: true
+ immer:
+ optional: true
+ react:
+ optional: true
+ use-sync-external-store:
+ optional: true
+
snapshots:
'@alloc/quick-lru@5.2.0': {}
@@ -3021,6 +3677,23 @@ snapshots:
- bufferutil
- utf-8-validate
+ '@floating-ui/core@1.7.4':
+ dependencies:
+ '@floating-ui/utils': 0.2.10
+
+ '@floating-ui/dom@1.7.5':
+ dependencies:
+ '@floating-ui/core': 1.7.4
+ '@floating-ui/utils': 0.2.10
+
+ '@floating-ui/react-dom@2.1.7(react-dom@19.2.4(react@19.2.4))(react@19.2.4)':
+ dependencies:
+ '@floating-ui/dom': 1.7.5
+ react: 19.2.4
+ react-dom: 19.2.4(react@19.2.4)
+
+ '@floating-ui/utils@0.2.10': {}
+
'@humanfs/core@0.19.1': {}
'@humanfs/node@0.16.7':
@@ -3071,6 +3744,472 @@ snapshots:
'@pinojs/redact@0.4.0': {}
+ '@radix-ui/number@1.1.1': {}
+
+ '@radix-ui/primitive@1.1.3': {}
+
+ '@radix-ui/react-arrow@1.1.7(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)':
+ dependencies:
+ '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
+ react: 19.2.4
+ react-dom: 19.2.4(react@19.2.4)
+ optionalDependencies:
+ '@types/react': 19.2.14
+ '@types/react-dom': 19.2.3(@types/react@19.2.14)
+
+ '@radix-ui/react-avatar@1.1.11(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)':
+ dependencies:
+ '@radix-ui/react-context': 1.1.3(@types/react@19.2.14)(react@19.2.4)
+ '@radix-ui/react-primitive': 2.1.4(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
+ '@radix-ui/react-use-callback-ref': 1.1.1(@types/react@19.2.14)(react@19.2.4)
+ '@radix-ui/react-use-is-hydrated': 0.1.0(@types/react@19.2.14)(react@19.2.4)
+ '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.2.14)(react@19.2.4)
+ react: 19.2.4
+ react-dom: 19.2.4(react@19.2.4)
+ optionalDependencies:
+ '@types/react': 19.2.14
+ '@types/react-dom': 19.2.3(@types/react@19.2.14)
+
+ '@radix-ui/react-collection@1.1.7(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)':
+ dependencies:
+ '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.14)(react@19.2.4)
+ '@radix-ui/react-context': 1.1.2(@types/react@19.2.14)(react@19.2.4)
+ '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
+ '@radix-ui/react-slot': 1.2.3(@types/react@19.2.14)(react@19.2.4)
+ react: 19.2.4
+ react-dom: 19.2.4(react@19.2.4)
+ optionalDependencies:
+ '@types/react': 19.2.14
+ '@types/react-dom': 19.2.3(@types/react@19.2.14)
+
+ '@radix-ui/react-compose-refs@1.1.2(@types/react@19.2.14)(react@19.2.4)':
+ dependencies:
+ react: 19.2.4
+ optionalDependencies:
+ '@types/react': 19.2.14
+
+ '@radix-ui/react-context@1.1.2(@types/react@19.2.14)(react@19.2.4)':
+ dependencies:
+ react: 19.2.4
+ optionalDependencies:
+ '@types/react': 19.2.14
+
+ '@radix-ui/react-context@1.1.3(@types/react@19.2.14)(react@19.2.4)':
+ dependencies:
+ react: 19.2.4
+ optionalDependencies:
+ '@types/react': 19.2.14
+
+ '@radix-ui/react-dialog@1.1.15(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)':
+ dependencies:
+ '@radix-ui/primitive': 1.1.3
+ '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.14)(react@19.2.4)
+ '@radix-ui/react-context': 1.1.2(@types/react@19.2.14)(react@19.2.4)
+ '@radix-ui/react-dismissable-layer': 1.1.11(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
+ '@radix-ui/react-focus-guards': 1.1.3(@types/react@19.2.14)(react@19.2.4)
+ '@radix-ui/react-focus-scope': 1.1.7(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
+ '@radix-ui/react-id': 1.1.1(@types/react@19.2.14)(react@19.2.4)
+ '@radix-ui/react-portal': 1.1.9(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
+ '@radix-ui/react-presence': 1.1.5(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
+ '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
+ '@radix-ui/react-slot': 1.2.3(@types/react@19.2.14)(react@19.2.4)
+ '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.2.14)(react@19.2.4)
+ aria-hidden: 1.2.6
+ react: 19.2.4
+ react-dom: 19.2.4(react@19.2.4)
+ react-remove-scroll: 2.7.2(@types/react@19.2.14)(react@19.2.4)
+ optionalDependencies:
+ '@types/react': 19.2.14
+ '@types/react-dom': 19.2.3(@types/react@19.2.14)
+
+ '@radix-ui/react-direction@1.1.1(@types/react@19.2.14)(react@19.2.4)':
+ dependencies:
+ react: 19.2.4
+ optionalDependencies:
+ '@types/react': 19.2.14
+
+ '@radix-ui/react-dismissable-layer@1.1.11(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)':
+ dependencies:
+ '@radix-ui/primitive': 1.1.3
+ '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.14)(react@19.2.4)
+ '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
+ '@radix-ui/react-use-callback-ref': 1.1.1(@types/react@19.2.14)(react@19.2.4)
+ '@radix-ui/react-use-escape-keydown': 1.1.1(@types/react@19.2.14)(react@19.2.4)
+ react: 19.2.4
+ react-dom: 19.2.4(react@19.2.4)
+ optionalDependencies:
+ '@types/react': 19.2.14
+ '@types/react-dom': 19.2.3(@types/react@19.2.14)
+
+ '@radix-ui/react-dropdown-menu@2.1.16(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)':
+ dependencies:
+ '@radix-ui/primitive': 1.1.3
+ '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.14)(react@19.2.4)
+ '@radix-ui/react-context': 1.1.2(@types/react@19.2.14)(react@19.2.4)
+ '@radix-ui/react-id': 1.1.1(@types/react@19.2.14)(react@19.2.4)
+ '@radix-ui/react-menu': 2.1.16(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
+ '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
+ '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.2.14)(react@19.2.4)
+ react: 19.2.4
+ react-dom: 19.2.4(react@19.2.4)
+ optionalDependencies:
+ '@types/react': 19.2.14
+ '@types/react-dom': 19.2.3(@types/react@19.2.14)
+
+ '@radix-ui/react-focus-guards@1.1.3(@types/react@19.2.14)(react@19.2.4)':
+ dependencies:
+ react: 19.2.4
+ optionalDependencies:
+ '@types/react': 19.2.14
+
+ '@radix-ui/react-focus-scope@1.1.7(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)':
+ dependencies:
+ '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.14)(react@19.2.4)
+ '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
+ '@radix-ui/react-use-callback-ref': 1.1.1(@types/react@19.2.14)(react@19.2.4)
+ react: 19.2.4
+ react-dom: 19.2.4(react@19.2.4)
+ optionalDependencies:
+ '@types/react': 19.2.14
+ '@types/react-dom': 19.2.3(@types/react@19.2.14)
+
+ '@radix-ui/react-id@1.1.1(@types/react@19.2.14)(react@19.2.4)':
+ dependencies:
+ '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.2.14)(react@19.2.4)
+ react: 19.2.4
+ optionalDependencies:
+ '@types/react': 19.2.14
+
+ '@radix-ui/react-label@2.1.8(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)':
+ dependencies:
+ '@radix-ui/react-primitive': 2.1.4(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
+ react: 19.2.4
+ react-dom: 19.2.4(react@19.2.4)
+ optionalDependencies:
+ '@types/react': 19.2.14
+ '@types/react-dom': 19.2.3(@types/react@19.2.14)
+
+ '@radix-ui/react-menu@2.1.16(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)':
+ dependencies:
+ '@radix-ui/primitive': 1.1.3
+ '@radix-ui/react-collection': 1.1.7(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
+ '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.14)(react@19.2.4)
+ '@radix-ui/react-context': 1.1.2(@types/react@19.2.14)(react@19.2.4)
+ '@radix-ui/react-direction': 1.1.1(@types/react@19.2.14)(react@19.2.4)
+ '@radix-ui/react-dismissable-layer': 1.1.11(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
+ '@radix-ui/react-focus-guards': 1.1.3(@types/react@19.2.14)(react@19.2.4)
+ '@radix-ui/react-focus-scope': 1.1.7(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
+ '@radix-ui/react-id': 1.1.1(@types/react@19.2.14)(react@19.2.4)
+ '@radix-ui/react-popper': 1.2.8(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
+ '@radix-ui/react-portal': 1.1.9(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
+ '@radix-ui/react-presence': 1.1.5(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
+ '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
+ '@radix-ui/react-roving-focus': 1.1.11(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
+ '@radix-ui/react-slot': 1.2.3(@types/react@19.2.14)(react@19.2.4)
+ '@radix-ui/react-use-callback-ref': 1.1.1(@types/react@19.2.14)(react@19.2.4)
+ aria-hidden: 1.2.6
+ react: 19.2.4
+ react-dom: 19.2.4(react@19.2.4)
+ react-remove-scroll: 2.7.2(@types/react@19.2.14)(react@19.2.4)
+ optionalDependencies:
+ '@types/react': 19.2.14
+ '@types/react-dom': 19.2.3(@types/react@19.2.14)
+
+ '@radix-ui/react-popper@1.2.8(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)':
+ dependencies:
+ '@floating-ui/react-dom': 2.1.7(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
+ '@radix-ui/react-arrow': 1.1.7(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
+ '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.14)(react@19.2.4)
+ '@radix-ui/react-context': 1.1.2(@types/react@19.2.14)(react@19.2.4)
+ '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
+ '@radix-ui/react-use-callback-ref': 1.1.1(@types/react@19.2.14)(react@19.2.4)
+ '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.2.14)(react@19.2.4)
+ '@radix-ui/react-use-rect': 1.1.1(@types/react@19.2.14)(react@19.2.4)
+ '@radix-ui/react-use-size': 1.1.1(@types/react@19.2.14)(react@19.2.4)
+ '@radix-ui/rect': 1.1.1
+ react: 19.2.4
+ react-dom: 19.2.4(react@19.2.4)
+ optionalDependencies:
+ '@types/react': 19.2.14
+ '@types/react-dom': 19.2.3(@types/react@19.2.14)
+
+ '@radix-ui/react-portal@1.1.9(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)':
+ dependencies:
+ '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
+ '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.2.14)(react@19.2.4)
+ react: 19.2.4
+ react-dom: 19.2.4(react@19.2.4)
+ optionalDependencies:
+ '@types/react': 19.2.14
+ '@types/react-dom': 19.2.3(@types/react@19.2.14)
+
+ '@radix-ui/react-presence@1.1.5(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)':
+ dependencies:
+ '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.14)(react@19.2.4)
+ '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.2.14)(react@19.2.4)
+ react: 19.2.4
+ react-dom: 19.2.4(react@19.2.4)
+ optionalDependencies:
+ '@types/react': 19.2.14
+ '@types/react-dom': 19.2.3(@types/react@19.2.14)
+
+ '@radix-ui/react-primitive@2.1.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)':
+ dependencies:
+ '@radix-ui/react-slot': 1.2.3(@types/react@19.2.14)(react@19.2.4)
+ react: 19.2.4
+ react-dom: 19.2.4(react@19.2.4)
+ optionalDependencies:
+ '@types/react': 19.2.14
+ '@types/react-dom': 19.2.3(@types/react@19.2.14)
+
+ '@radix-ui/react-primitive@2.1.4(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)':
+ dependencies:
+ '@radix-ui/react-slot': 1.2.4(@types/react@19.2.14)(react@19.2.4)
+ react: 19.2.4
+ react-dom: 19.2.4(react@19.2.4)
+ optionalDependencies:
+ '@types/react': 19.2.14
+ '@types/react-dom': 19.2.3(@types/react@19.2.14)
+
+ '@radix-ui/react-progress@1.1.8(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)':
+ dependencies:
+ '@radix-ui/react-context': 1.1.3(@types/react@19.2.14)(react@19.2.4)
+ '@radix-ui/react-primitive': 2.1.4(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
+ react: 19.2.4
+ react-dom: 19.2.4(react@19.2.4)
+ optionalDependencies:
+ '@types/react': 19.2.14
+ '@types/react-dom': 19.2.3(@types/react@19.2.14)
+
+ '@radix-ui/react-roving-focus@1.1.11(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)':
+ dependencies:
+ '@radix-ui/primitive': 1.1.3
+ '@radix-ui/react-collection': 1.1.7(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
+ '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.14)(react@19.2.4)
+ '@radix-ui/react-context': 1.1.2(@types/react@19.2.14)(react@19.2.4)
+ '@radix-ui/react-direction': 1.1.1(@types/react@19.2.14)(react@19.2.4)
+ '@radix-ui/react-id': 1.1.1(@types/react@19.2.14)(react@19.2.4)
+ '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
+ '@radix-ui/react-use-callback-ref': 1.1.1(@types/react@19.2.14)(react@19.2.4)
+ '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.2.14)(react@19.2.4)
+ react: 19.2.4
+ react-dom: 19.2.4(react@19.2.4)
+ optionalDependencies:
+ '@types/react': 19.2.14
+ '@types/react-dom': 19.2.3(@types/react@19.2.14)
+
+ '@radix-ui/react-scroll-area@1.2.10(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)':
+ dependencies:
+ '@radix-ui/number': 1.1.1
+ '@radix-ui/primitive': 1.1.3
+ '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.14)(react@19.2.4)
+ '@radix-ui/react-context': 1.1.2(@types/react@19.2.14)(react@19.2.4)
+ '@radix-ui/react-direction': 1.1.1(@types/react@19.2.14)(react@19.2.4)
+ '@radix-ui/react-presence': 1.1.5(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
+ '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
+ '@radix-ui/react-use-callback-ref': 1.1.1(@types/react@19.2.14)(react@19.2.4)
+ '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.2.14)(react@19.2.4)
+ react: 19.2.4
+ react-dom: 19.2.4(react@19.2.4)
+ optionalDependencies:
+ '@types/react': 19.2.14
+ '@types/react-dom': 19.2.3(@types/react@19.2.14)
+
+ '@radix-ui/react-select@2.2.6(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)':
+ dependencies:
+ '@radix-ui/number': 1.1.1
+ '@radix-ui/primitive': 1.1.3
+ '@radix-ui/react-collection': 1.1.7(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
+ '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.14)(react@19.2.4)
+ '@radix-ui/react-context': 1.1.2(@types/react@19.2.14)(react@19.2.4)
+ '@radix-ui/react-direction': 1.1.1(@types/react@19.2.14)(react@19.2.4)
+ '@radix-ui/react-dismissable-layer': 1.1.11(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
+ '@radix-ui/react-focus-guards': 1.1.3(@types/react@19.2.14)(react@19.2.4)
+ '@radix-ui/react-focus-scope': 1.1.7(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
+ '@radix-ui/react-id': 1.1.1(@types/react@19.2.14)(react@19.2.4)
+ '@radix-ui/react-popper': 1.2.8(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
+ '@radix-ui/react-portal': 1.1.9(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
+ '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
+ '@radix-ui/react-slot': 1.2.3(@types/react@19.2.14)(react@19.2.4)
+ '@radix-ui/react-use-callback-ref': 1.1.1(@types/react@19.2.14)(react@19.2.4)
+ '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.2.14)(react@19.2.4)
+ '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.2.14)(react@19.2.4)
+ '@radix-ui/react-use-previous': 1.1.1(@types/react@19.2.14)(react@19.2.4)
+ '@radix-ui/react-visually-hidden': 1.2.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
+ aria-hidden: 1.2.6
+ react: 19.2.4
+ react-dom: 19.2.4(react@19.2.4)
+ react-remove-scroll: 2.7.2(@types/react@19.2.14)(react@19.2.4)
+ optionalDependencies:
+ '@types/react': 19.2.14
+ '@types/react-dom': 19.2.3(@types/react@19.2.14)
+
+ '@radix-ui/react-separator@1.1.8(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)':
+ dependencies:
+ '@radix-ui/react-primitive': 2.1.4(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
+ react: 19.2.4
+ react-dom: 19.2.4(react@19.2.4)
+ optionalDependencies:
+ '@types/react': 19.2.14
+ '@types/react-dom': 19.2.3(@types/react@19.2.14)
+
+ '@radix-ui/react-slot@1.2.3(@types/react@19.2.14)(react@19.2.4)':
+ dependencies:
+ '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.14)(react@19.2.4)
+ react: 19.2.4
+ optionalDependencies:
+ '@types/react': 19.2.14
+
+ '@radix-ui/react-slot@1.2.4(@types/react@19.2.14)(react@19.2.4)':
+ dependencies:
+ '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.14)(react@19.2.4)
+ react: 19.2.4
+ optionalDependencies:
+ '@types/react': 19.2.14
+
+ '@radix-ui/react-switch@1.2.6(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)':
+ dependencies:
+ '@radix-ui/primitive': 1.1.3
+ '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.14)(react@19.2.4)
+ '@radix-ui/react-context': 1.1.2(@types/react@19.2.14)(react@19.2.4)
+ '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
+ '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.2.14)(react@19.2.4)
+ '@radix-ui/react-use-previous': 1.1.1(@types/react@19.2.14)(react@19.2.4)
+ '@radix-ui/react-use-size': 1.1.1(@types/react@19.2.14)(react@19.2.4)
+ react: 19.2.4
+ react-dom: 19.2.4(react@19.2.4)
+ optionalDependencies:
+ '@types/react': 19.2.14
+ '@types/react-dom': 19.2.3(@types/react@19.2.14)
+
+ '@radix-ui/react-tabs@1.1.13(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)':
+ dependencies:
+ '@radix-ui/primitive': 1.1.3
+ '@radix-ui/react-context': 1.1.2(@types/react@19.2.14)(react@19.2.4)
+ '@radix-ui/react-direction': 1.1.1(@types/react@19.2.14)(react@19.2.4)
+ '@radix-ui/react-id': 1.1.1(@types/react@19.2.14)(react@19.2.4)
+ '@radix-ui/react-presence': 1.1.5(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
+ '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
+ '@radix-ui/react-roving-focus': 1.1.11(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
+ '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.2.14)(react@19.2.4)
+ react: 19.2.4
+ react-dom: 19.2.4(react@19.2.4)
+ optionalDependencies:
+ '@types/react': 19.2.14
+ '@types/react-dom': 19.2.3(@types/react@19.2.14)
+
+ '@radix-ui/react-toast@1.2.15(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)':
+ dependencies:
+ '@radix-ui/primitive': 1.1.3
+ '@radix-ui/react-collection': 1.1.7(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
+ '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.14)(react@19.2.4)
+ '@radix-ui/react-context': 1.1.2(@types/react@19.2.14)(react@19.2.4)
+ '@radix-ui/react-dismissable-layer': 1.1.11(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
+ '@radix-ui/react-portal': 1.1.9(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
+ '@radix-ui/react-presence': 1.1.5(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
+ '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
+ '@radix-ui/react-use-callback-ref': 1.1.1(@types/react@19.2.14)(react@19.2.4)
+ '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.2.14)(react@19.2.4)
+ '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.2.14)(react@19.2.4)
+ '@radix-ui/react-visually-hidden': 1.2.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
+ react: 19.2.4
+ react-dom: 19.2.4(react@19.2.4)
+ optionalDependencies:
+ '@types/react': 19.2.14
+ '@types/react-dom': 19.2.3(@types/react@19.2.14)
+
+ '@radix-ui/react-tooltip@1.2.8(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)':
+ dependencies:
+ '@radix-ui/primitive': 1.1.3
+ '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.14)(react@19.2.4)
+ '@radix-ui/react-context': 1.1.2(@types/react@19.2.14)(react@19.2.4)
+ '@radix-ui/react-dismissable-layer': 1.1.11(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
+ '@radix-ui/react-id': 1.1.1(@types/react@19.2.14)(react@19.2.4)
+ '@radix-ui/react-popper': 1.2.8(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
+ '@radix-ui/react-portal': 1.1.9(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
+ '@radix-ui/react-presence': 1.1.5(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
+ '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
+ '@radix-ui/react-slot': 1.2.3(@types/react@19.2.14)(react@19.2.4)
+ '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.2.14)(react@19.2.4)
+ '@radix-ui/react-visually-hidden': 1.2.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
+ react: 19.2.4
+ react-dom: 19.2.4(react@19.2.4)
+ optionalDependencies:
+ '@types/react': 19.2.14
+ '@types/react-dom': 19.2.3(@types/react@19.2.14)
+
+ '@radix-ui/react-use-callback-ref@1.1.1(@types/react@19.2.14)(react@19.2.4)':
+ dependencies:
+ react: 19.2.4
+ optionalDependencies:
+ '@types/react': 19.2.14
+
+ '@radix-ui/react-use-controllable-state@1.2.2(@types/react@19.2.14)(react@19.2.4)':
+ dependencies:
+ '@radix-ui/react-use-effect-event': 0.0.2(@types/react@19.2.14)(react@19.2.4)
+ '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.2.14)(react@19.2.4)
+ react: 19.2.4
+ optionalDependencies:
+ '@types/react': 19.2.14
+
+ '@radix-ui/react-use-effect-event@0.0.2(@types/react@19.2.14)(react@19.2.4)':
+ dependencies:
+ '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.2.14)(react@19.2.4)
+ react: 19.2.4
+ optionalDependencies:
+ '@types/react': 19.2.14
+
+ '@radix-ui/react-use-escape-keydown@1.1.1(@types/react@19.2.14)(react@19.2.4)':
+ dependencies:
+ '@radix-ui/react-use-callback-ref': 1.1.1(@types/react@19.2.14)(react@19.2.4)
+ react: 19.2.4
+ optionalDependencies:
+ '@types/react': 19.2.14
+
+ '@radix-ui/react-use-is-hydrated@0.1.0(@types/react@19.2.14)(react@19.2.4)':
+ dependencies:
+ react: 19.2.4
+ use-sync-external-store: 1.6.0(react@19.2.4)
+ optionalDependencies:
+ '@types/react': 19.2.14
+
+ '@radix-ui/react-use-layout-effect@1.1.1(@types/react@19.2.14)(react@19.2.4)':
+ dependencies:
+ react: 19.2.4
+ optionalDependencies:
+ '@types/react': 19.2.14
+
+ '@radix-ui/react-use-previous@1.1.1(@types/react@19.2.14)(react@19.2.4)':
+ dependencies:
+ react: 19.2.4
+ optionalDependencies:
+ '@types/react': 19.2.14
+
+ '@radix-ui/react-use-rect@1.1.1(@types/react@19.2.14)(react@19.2.4)':
+ dependencies:
+ '@radix-ui/rect': 1.1.1
+ react: 19.2.4
+ optionalDependencies:
+ '@types/react': 19.2.14
+
+ '@radix-ui/react-use-size@1.1.1(@types/react@19.2.14)(react@19.2.4)':
+ dependencies:
+ '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.2.14)(react@19.2.4)
+ react: 19.2.4
+ optionalDependencies:
+ '@types/react': 19.2.14
+
+ '@radix-ui/react-visually-hidden@1.2.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)':
+ dependencies:
+ '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
+ react: 19.2.4
+ react-dom: 19.2.4(react@19.2.4)
+ optionalDependencies:
+ '@types/react': 19.2.14
+ '@types/react-dom': 19.2.3(@types/react@19.2.14)
+
+ '@radix-ui/rect@1.1.1': {}
+
'@rolldown/pluginutils@1.0.0-beta.27': {}
'@rollup/rollup-android-arm-eabi@4.58.0':
@@ -3308,6 +4447,12 @@ snapshots:
transitivePeerDependencies:
- supports-color
+ '@xterm/addon-fit@0.11.0': {}
+
+ '@xterm/addon-web-links@0.12.0': {}
+
+ '@xterm/xterm@6.0.0': {}
+
abstract-logging@2.0.1: {}
accepts@1.3.8:
@@ -3360,6 +4505,10 @@ snapshots:
argparse@2.0.1: {}
+ aria-hidden@1.2.6:
+ dependencies:
+ tslib: 2.8.1
+
asn1.js@5.4.1:
dependencies:
bn.js: 4.12.3
@@ -3488,6 +4637,8 @@ snapshots:
dequal@2.0.3: {}
+ detect-node-es@1.1.0: {}
+
didyoumean@1.2.2: {}
dlv@1.1.3: {}
@@ -3892,6 +5043,8 @@ snapshots:
gensync@1.0.0-beta.2: {}
+ get-nonce@1.0.1: {}
+
get-tsconfig@4.13.6:
dependencies:
resolve-pkg-maps: 1.0.0
@@ -4004,6 +5157,10 @@ snapshots:
dependencies:
yallist: 3.1.1
+ lucide-react@0.575.0(react@19.2.4):
+ dependencies:
+ react: 19.2.4
+
merge2@1.4.1: {}
micromatch@4.0.8:
@@ -4210,6 +5367,25 @@ snapshots:
react-refresh@0.17.0: {}
+ react-remove-scroll-bar@2.3.8(@types/react@19.2.14)(react@19.2.4):
+ dependencies:
+ react: 19.2.4
+ react-style-singleton: 2.2.3(@types/react@19.2.14)(react@19.2.4)
+ tslib: 2.8.1
+ optionalDependencies:
+ '@types/react': 19.2.14
+
+ react-remove-scroll@2.7.2(@types/react@19.2.14)(react@19.2.4):
+ dependencies:
+ react: 19.2.4
+ react-remove-scroll-bar: 2.3.8(@types/react@19.2.14)(react@19.2.4)
+ react-style-singleton: 2.2.3(@types/react@19.2.14)(react@19.2.4)
+ tslib: 2.8.1
+ use-callback-ref: 1.3.3(@types/react@19.2.14)(react@19.2.4)
+ use-sidecar: 1.1.3(@types/react@19.2.14)(react@19.2.4)
+ optionalDependencies:
+ '@types/react': 19.2.14
+
react-router@7.13.0(react-dom@19.2.4(react@19.2.4))(react@19.2.4):
dependencies:
cookie: 1.1.1
@@ -4218,6 +5394,14 @@ snapshots:
optionalDependencies:
react-dom: 19.2.4(react@19.2.4)
+ react-style-singleton@2.2.3(@types/react@19.2.14)(react@19.2.4):
+ dependencies:
+ get-nonce: 1.0.1
+ react: 19.2.4
+ tslib: 2.8.1
+ optionalDependencies:
+ '@types/react': 19.2.14
+
react@19.2.4: {}
read-cache@1.0.0:
@@ -4362,6 +5546,11 @@ snapshots:
dependencies:
atomic-sleep: 1.0.0
+ sonner@2.0.7(react-dom@19.2.4(react@19.2.4))(react@19.2.4):
+ dependencies:
+ react: 19.2.4
+ react-dom: 19.2.4(react@19.2.4)
+
source-map-js@1.2.1: {}
source-map-support@0.5.21:
@@ -4466,6 +5655,8 @@ snapshots:
ts-interface-checker@0.1.13: {}
+ tslib@2.8.1: {}
+
tsx@4.21.0:
dependencies:
esbuild: 0.27.3
@@ -4532,6 +5723,25 @@ snapshots:
dependencies:
punycode: 2.3.1
+ use-callback-ref@1.3.3(@types/react@19.2.14)(react@19.2.4):
+ dependencies:
+ react: 19.2.4
+ tslib: 2.8.1
+ optionalDependencies:
+ '@types/react': 19.2.14
+
+ use-sidecar@1.1.3(@types/react@19.2.14)(react@19.2.4):
+ dependencies:
+ detect-node-es: 1.1.0
+ react: 19.2.4
+ tslib: 2.8.1
+ optionalDependencies:
+ '@types/react': 19.2.14
+
+ use-sync-external-store@1.6.0(react@19.2.4):
+ dependencies:
+ react: 19.2.4
+
util-deprecate@1.0.2: {}
vary@1.1.2: {}
@@ -4573,3 +5783,9 @@ snapshots:
yallist@3.1.1: {}
yocto-queue@0.1.0: {}
+
+ zustand@5.0.11(@types/react@19.2.14)(react@19.2.4)(use-sync-external-store@1.6.0(react@19.2.4)):
+ optionalDependencies:
+ '@types/react': 19.2.14
+ react: 19.2.4
+ use-sync-external-store: 1.6.0(react@19.2.4)