diff --git a/apps/web/app/bootstrap/page.tsx b/apps/web/app/bootstrap/page.tsx index 5aedee2..c137484 100644 --- a/apps/web/app/bootstrap/page.tsx +++ b/apps/web/app/bootstrap/page.tsx @@ -1,10 +1,12 @@ "use client"; -import { Loader2 } from "lucide-react"; +import { ArrowRight, Check } from "lucide-react"; import { useRouter } from "next/navigation"; import { useEffect, useState } from "react"; import { Button } from "@/components/ui/button"; -import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"; +import { AuthFormCard, FormError } from "@/components/auth/auth-form-card"; +import { BrandMark } from "@/components/brand"; +import { FullScreenLoader } from "@/components/page-layout"; import { Input } from "@/components/ui/input"; import { Label } from "@/components/ui/label"; import { api } from "@/lib/api-client"; @@ -77,68 +79,86 @@ export default function BootstrapPage() { } }; - if (checkingStatus) { - return ( -
- -
- ); - } + if (checkingStatus) return ; return ( -
- - - Welcome to Minikura - - Create your admin account to get started - - - -
-
- - -
-
- - -
-
- - -
-
- - -
- {error &&
{error}
} - -
-
-
-
+
+
+
+ +
+ System bootstrap / 01 +

+ Build your command center. +

+

+ The first account controls users, workloads, routing, and cluster visibility. +

+
+
+

+ Admin authority +

+

+ Secure session +

+

+ Ready in one step +

+
+
+ +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+ + + +
+
+
); } diff --git a/apps/web/app/globals.css b/apps/web/app/globals.css index de45600..47f7a28 100644 --- a/apps/web/app/globals.css +++ b/apps/web/app/globals.css @@ -26,6 +26,10 @@ --color-accent: var(--accent); --color-accent-foreground: var(--accent-foreground); --color-destructive: var(--destructive); + --color-success: var(--success); + --color-success-foreground: var(--success-foreground); + --color-warning: var(--warning); + --color-warning-foreground: var(--warning-foreground); --color-border: var(--border); --color-input: var(--input); --color-ring: var(--ring); @@ -42,41 +46,49 @@ --color-sidebar-accent-foreground: var(--sidebar-accent-foreground); --color-sidebar-border: var(--sidebar-border); --color-sidebar-ring: var(--sidebar-ring); + --font-sans: var(--font-sans); + --font-mono: var(--font-mono); } :root { - --radius: 0.625rem; - --background: oklch(1 0 0); - --foreground: oklch(0.145 0 0); - --card: oklch(1 0 0); - --card-foreground: oklch(0.145 0 0); - --popover: oklch(1 0 0); - --popover-foreground: oklch(0.145 0 0); - --primary: oklch(0.205 0 0); - --primary-foreground: oklch(0.985 0 0); - --secondary: oklch(0.97 0 0); - --secondary-foreground: oklch(0.205 0 0); - --muted: oklch(0.97 0 0); - --muted-foreground: oklch(0.556 0 0); - --accent: oklch(0.97 0 0); - --accent-foreground: oklch(0.205 0 0); - --destructive: oklch(0.577 0.245 27.325); - --border: oklch(0.922 0 0); - --input: oklch(0.922 0 0); - --ring: oklch(0.708 0 0); - --chart-1: oklch(0.646 0.222 41.116); - --chart-2: oklch(0.6 0.118 184.704); - --chart-3: oklch(0.398 0.07 227.392); - --chart-4: oklch(0.828 0.189 84.429); - --chart-5: oklch(0.769 0.188 70.08); - --sidebar: oklch(0.985 0 0); - --sidebar-foreground: oklch(0.145 0 0); - --sidebar-primary: oklch(0.205 0 0); - --sidebar-primary-foreground: oklch(0.985 0 0); - --sidebar-accent: oklch(0.97 0 0); - --sidebar-accent-foreground: oklch(0.205 0 0); - --sidebar-border: oklch(0.922 0 0); - --sidebar-ring: oklch(0.708 0 0); + --radius: 0.25rem; + --font-sans: "Arial Narrow", "Roboto Condensed", "Helvetica Neue", Arial, sans-serif; + --font-mono: "IBM Plex Mono", "SFMono-Regular", Consolas, monospace; + --background: oklch(0.965 0.012 95); + --foreground: oklch(0.18 0.018 75); + --card: oklch(0.99 0.006 95); + --card-foreground: oklch(0.18 0.018 75); + --popover: oklch(0.995 0.004 95); + --popover-foreground: oklch(0.18 0.018 75); + --primary: oklch(0.75 0.205 125); + --primary-foreground: oklch(0.15 0.025 125); + --secondary: oklch(0.91 0.022 95); + --secondary-foreground: oklch(0.24 0.018 75); + --muted: oklch(0.925 0.012 95); + --muted-foreground: oklch(0.48 0.018 75); + --accent: oklch(0.88 0.04 112); + --accent-foreground: oklch(0.2 0.028 110); + --destructive: oklch(0.57 0.205 28); + --success: oklch(0.59 0.155 132); + --success-foreground: oklch(0.28 0.09 132); + --warning: oklch(0.72 0.15 76); + --warning-foreground: oklch(0.34 0.09 66); + --border: oklch(0.79 0.018 90); + --input: oklch(0.76 0.02 90); + --ring: oklch(0.68 0.19 125); + --chart-1: oklch(0.75 0.205 125); + --chart-2: oklch(0.66 0.16 205); + --chart-3: oklch(0.58 0.18 50); + --chart-4: oklch(0.72 0.17 80); + --chart-5: oklch(0.55 0.16 300); + --sidebar: oklch(0.17 0.02 75); + --sidebar-foreground: oklch(0.91 0.015 95); + --sidebar-primary: oklch(0.75 0.205 125); + --sidebar-primary-foreground: oklch(0.15 0.025 125); + --sidebar-accent: oklch(0.25 0.025 90); + --sidebar-accent-foreground: oklch(0.96 0.01 95); + --sidebar-border: oklch(0.31 0.025 80); + --sidebar-ring: oklch(0.75 0.205 125); } .dark { @@ -117,8 +129,77 @@ * { @apply border-border outline-ring/50; } + html { + @apply bg-background; + } body { - @apply bg-background text-foreground; + @apply bg-background text-foreground antialiased; + min-width: 320px; + background-image: + linear-gradient(to right, color-mix(in oklch, var(--border) 22%, transparent) 1px, transparent 1px), + linear-gradient(to bottom, color-mix(in oklch, var(--border) 22%, transparent) 1px, transparent 1px); + background-size: 32px 32px; + } + h1, + h2, + h3 { + letter-spacing: -0.025em; + } + ::selection { + background: var(--primary); + color: var(--primary-foreground); + } +} + +@layer components { + .page-shell { + @apply mx-auto w-full max-w-[1600px] space-y-6; + } + + .page-heading { + @apply flex flex-col gap-4 border-b-2 border-foreground pb-5 sm:flex-row sm:items-end sm:justify-between; + } + + .page-eyebrow { + @apply mb-2 block font-mono text-[10px] font-bold uppercase tracking-[0.22em] text-muted-foreground; + } + + .page-title { + @apply text-3xl font-black leading-none tracking-[-0.04em] sm:text-4xl; + } + + .page-description { + @apply mt-2 max-w-2xl text-sm leading-6 text-muted-foreground; + } + + .auth-grid { + background-image: + linear-gradient(to right, color-mix(in oklch, white 9%, transparent) 1px, transparent 1px), + linear-gradient(to bottom, color-mix(in oklch, white 9%, transparent) 1px, transparent 1px); + background-size: 36px 36px; + } + + .server-form [data-slot="tabs-content"] { + @apply border border-border bg-background/65 p-4 sm:p-6; + } +} + +@media (prefers-reduced-motion: reduce) { + *, + *::before, + *::after { + scroll-behavior: auto !important; + animation-duration: 0.01ms !important; + animation-iteration-count: 1 !important; + transition-duration: 0.01ms !important; + } +} + +@media (max-width: 767px) { + .server-form .grid-cols-2, + .server-form .grid-cols-3, + .server-form .grid-cols-4 { + grid-template-columns: minmax(0, 1fr); } } diff --git a/apps/web/app/layout.tsx b/apps/web/app/layout.tsx index e164233..6052794 100644 --- a/apps/web/app/layout.tsx +++ b/apps/web/app/layout.tsx @@ -2,14 +2,14 @@ import type { Metadata } from "next"; import "./globals.css"; export const metadata: Metadata = { - title: "Minikura - Minecraft Server Manager", - description: "Manage your Minecraft servers with ease", + title: "Minikura | Infrastructure Console", + description: "Minecraft infrastructure control plane", }; export default function RootLayout({ children }: { children: React.ReactNode }) { return ( - {children} + {children} ); } diff --git a/apps/web/app/login/page.tsx b/apps/web/app/login/page.tsx index 036a694..0b4297c 100644 --- a/apps/web/app/login/page.tsx +++ b/apps/web/app/login/page.tsx @@ -1,13 +1,15 @@ "use client"; -import { Loader2 } from "lucide-react"; +import { ArrowRight } from "lucide-react"; import { useRouter } from "next/navigation"; import { useEffect, useState } from "react"; import { Button } from "@/components/ui/button"; -import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"; +import { AuthFormCard, FormError } from "@/components/auth/auth-form-card"; +import { BrandLockup } from "@/components/brand"; +import { FullScreenLoader } from "@/components/page-layout"; import { Input } from "@/components/ui/input"; import { Label } from "@/components/ui/label"; -import { signIn, useSession } from "@/lib/auth-client"; +import { authClient, signIn, useSession } from "@/lib/auth-client"; export default function LoginPage() { const router = useRouter(); @@ -39,7 +41,16 @@ export default function LoginPage() { if (result.error) { setError(result.error.message || "Invalid email or password"); } else { - router.push("/dashboard"); + const refreshedSession = await authClient.getSession({ + query: { disableCookieCache: true }, + }); + + if (!refreshedSession.data?.user) { + setError("Signed in, but the session cookie was not accepted. Check the web and API URLs."); + return; + } + + window.location.assign("/dashboard"); } } catch (_err) { setError("Failed to connect to server"); @@ -48,59 +59,72 @@ export default function LoginPage() { } }; - if (isPending) { - return ( -
- -
- ); - } - - if (session?.user) { - return ( -
- -
- ); - } + if (isPending || session?.user) return ; return ( -
- - - Minikura - Sign in to your account - - -
-
- - -
-
- - -
- {error &&
{error}
} - -
-
-
-
+
+
+
+ + +
+ + Minecraft operations platform + +

+ Orchestrate +
+ Every World. +

+

+ Provision servers, manage proxy routes, and monitor Kubernetes workloads from a single + operational interface. +

+
+ +

+ Server and cluster administration +

+
+ +
+ +
+
+ + +
+
+ + +
+ + + +
+
+
); } diff --git a/apps/web/components/auth/auth-form-card.tsx b/apps/web/components/auth/auth-form-card.tsx new file mode 100644 index 0000000..e4beecb --- /dev/null +++ b/apps/web/components/auth/auth-form-card.tsx @@ -0,0 +1,44 @@ +import type * as React from "react"; +import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"; +import { cn } from "@/lib/cn"; + +type AuthFormCardProps = { + title: string; + description: string; + children: React.ReactNode; + className?: string; + headerClassName?: string; + contentClassName?: string; +}; + +export function AuthFormCard({ + title, + description, + children, + className, + headerClassName, + contentClassName, +}: AuthFormCardProps) { + return ( + + + {title} + {description} + + {children} + + ); +} + +export function FormError({ message }: { message?: string | null }) { + if (!message) return null; + + return ( +
+ {message} +
+ ); +} diff --git a/apps/web/components/brand.tsx b/apps/web/components/brand.tsx new file mode 100644 index 0000000..8c6f515 --- /dev/null +++ b/apps/web/components/brand.tsx @@ -0,0 +1,42 @@ +import { cn } from "@/lib/cn"; + +type BrandMarkProps = { + className?: string; + inverted?: boolean; +}; + +export function BrandMark({ className, inverted = false }: BrandMarkProps) { + return ( +
+ M +
+ ); +} + +type BrandLockupProps = BrandMarkProps & { + subtitle: string; + compact?: boolean; + textClassName?: string; +}; + +export function BrandLockup({ subtitle, compact, className, textClassName }: BrandLockupProps) { + return ( +
+ +
+

+ Minikura +

+

{subtitle}

+
+
+ ); +} diff --git a/apps/web/components/confirm-dialog.tsx b/apps/web/components/confirm-dialog.tsx new file mode 100644 index 0000000..6f2c0f0 --- /dev/null +++ b/apps/web/components/confirm-dialog.tsx @@ -0,0 +1,46 @@ +import { Button } from "@/components/ui/button"; +import { + Dialog, + DialogContent, + DialogDescription, + DialogFooter, + DialogHeader, + DialogTitle, +} from "@/components/ui/dialog"; + +type ConfirmDialogProps = { + open: boolean; + title: string; + description: React.ReactNode; + confirmLabel: string; + onConfirm: () => void | Promise; + onOpenChange: (open: boolean) => void; +}; + +export function ConfirmDialog({ + open, + title, + description, + confirmLabel, + onConfirm, + onOpenChange, +}: ConfirmDialogProps) { + return ( + + + + {title} + {description} + + + + + + + + ); +} diff --git a/apps/web/components/dashboard-layout.tsx b/apps/web/components/dashboard-layout.tsx index 7ba3250..8420b54 100644 --- a/apps/web/components/dashboard-layout.tsx +++ b/apps/web/components/dashboard-layout.tsx @@ -1,16 +1,17 @@ "use client"; -import { GitGraph, Loader2, LogOut, Network, Server, Settings, Users } from "lucide-react"; +import { GitGraph, LogOut, type LucideIcon, Network, Server, Users } from "lucide-react"; import Link from "next/link"; import { usePathname, useRouter } from "next/navigation"; import { useEffect } from "react"; +import { BrandLockup } from "@/components/brand"; +import { FullScreenLoader } from "@/components/page-layout"; import { Avatar, AvatarFallback } from "@/components/ui/avatar"; import { Button } from "@/components/ui/button"; import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, - DropdownMenuSeparator, DropdownMenuTrigger, } from "@/components/ui/dropdown-menu"; import { @@ -29,13 +30,30 @@ import { } from "@/components/ui/sidebar"; import { signOut, useSession } from "@/lib/auth-client"; -const menuItems = [ - { href: "/dashboard/users", icon: Users, label: "Users" }, - { href: "/dashboard/servers", icon: Server, label: "Servers" }, - { href: "/dashboard/topology", icon: GitGraph, label: "Network" }, -]; +type NavigationGroup = { + label: string; + items: Array<{ + href: string; + icon: LucideIcon; + label: string; + context: string; + }>; +}; -const k8sMenuItems = [{ href: "/dashboard/k8s", icon: Network, label: "Resources" }]; +const navigation: NavigationGroup[] = [ + { + label: "Operations", + items: [ + { href: "/dashboard/users", icon: Users, label: "Users", context: "Identity" }, + { href: "/dashboard/servers", icon: Server, label: "Servers", context: "Workloads" }, + { href: "/dashboard/topology", icon: GitGraph, label: "Network", context: "Topology" }, + ], + }, + { + label: "Kubernetes", + items: [{ href: "/dashboard/k8s", icon: Network, label: "Resources", context: "Cluster" }], + }, +]; export function DashboardLayout({ children }: { children: React.ReactNode }) { const pathname = usePathname(); @@ -48,21 +66,7 @@ export function DashboardLayout({ children }: { children: React.ReactNode }) { } }, [session, isPending, router]); - if (isPending) { - return ( -
- -
- ); - } - - if (!session?.user) { - return ( -
- -
- ); - } + if (isPending || !session?.user) return ; const handleSignOut = async () => { await signOut(); @@ -75,69 +79,73 @@ export function DashboardLayout({ children }: { children: React.ReactNode }) { .map((n) => n[0]) .join("") .toUpperCase() || "U"; + const currentPage = navigation + .flatMap((group) => group.items) + .find((item) => pathname === item.href || pathname.startsWith(`${item.href}/`)); return ( - - - -

Minikura

+ + + + - - - - - {menuItems.map((item) => ( - - - - - {item.label} - - - - ))} - - - - - Kubernetes - - - {k8sMenuItems.map((item) => ( - - - - - {item.label} - - - - ))} - - - + + {navigation.map((group) => ( + + + {group.label} + + + + {group.items.map((item) => ( + + + + + {item.label} + + + + ))} + + + + ))} -
+
- - - - - Settings - - - Sign Out @@ -147,11 +155,14 @@ export function DashboardLayout({ children }: { children: React.ReactNode }) {
-
- -
+
+ +
+ + {currentPage ? `${currentPage.context} / ${currentPage.label}` : "Control plane"} +
-
{children}
+
{children}
); diff --git a/apps/web/components/data-table.tsx b/apps/web/components/data-table.tsx new file mode 100644 index 0000000..5777f57 --- /dev/null +++ b/apps/web/components/data-table.tsx @@ -0,0 +1,50 @@ +import type { ReactNode } from "react"; +import { + Table, + TableBody, + TableCell, + TableHead, + TableHeader, + TableRow, +} from "@/components/ui/table"; + +export type DataTableColumn = { + id: string; + header: ReactNode; + cell: (item: T) => ReactNode; + className?: string; + headerClassName?: string; +}; + +type DataTableProps = { + data: readonly T[]; + columns: readonly DataTableColumn[]; + getRowKey: (item: T, index: number) => string | number; +}; + +export function DataTable({ data, columns, getRowKey }: DataTableProps) { + return ( + + + + {columns.map((column) => ( + + {column.header} + + ))} + + + + {data.map((item, index) => ( + + {columns.map((column) => ( + + {column.cell(item)} + + ))} + + ))} + +
+ ); +} diff --git a/apps/web/components/page-layout.tsx b/apps/web/components/page-layout.tsx new file mode 100644 index 0000000..ebb36db --- /dev/null +++ b/apps/web/components/page-layout.tsx @@ -0,0 +1,93 @@ +import { Loader2 } from "lucide-react"; +import type * as React from "react"; +import { cn } from "@/lib/cn"; + +type PageHeaderProps = { + eyebrow: string; + title: React.ReactNode; + description?: React.ReactNode; + leading?: React.ReactNode; + actions?: React.ReactNode; + className?: string; +}; + +export function PageShell({ className, ...props }: React.ComponentProps<"div">) { + return
; +} + +export function PageHeader({ + eyebrow, + title, + description, + leading, + actions, + className, +}: PageHeaderProps) { + return ( +
+
+ {leading} +
+ {eyebrow} +

{title}

+ {description &&

{description}

} +
+
+ {actions} +
+ ); +} + +type StatePanelProps = React.ComponentProps<"div"> & { + title: React.ReactNode; + description?: React.ReactNode; + icon?: React.ReactNode; + action?: React.ReactNode; + tone?: "default" | "error"; + loading?: boolean; +}; + +export function StatePanel({ + title, + description, + icon, + action, + tone = "default", + loading = false, + className, + ...props +}: StatePanelProps) { + return ( +
+
+ {loading ? ( + + ) : ( + icon &&
{icon}
+ )} +

{title}

+ {description &&
{description}
} + {action &&
{action}
} +
+
+ ); +} + +export function FullScreenLoader({ label }: { label?: string }) { + return ( +
+ + {label && ( + + {label} + + )} +
+ ); +} diff --git a/apps/web/components/section-card.tsx b/apps/web/components/section-card.tsx new file mode 100644 index 0000000..e35b00c --- /dev/null +++ b/apps/web/components/section-card.tsx @@ -0,0 +1,77 @@ +import type * as React from "react"; +import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"; +import { cn } from "@/lib/cn"; + +type SectionCardProps = React.ComponentProps & { + title: React.ReactNode; + description?: React.ReactNode; + icon?: React.ReactNode; + headerAction?: React.ReactNode; + contentClassName?: string; +}; + +export function SectionCard({ + title, + description, + icon, + headerAction, + children, + className, + contentClassName, + ...props +}: SectionCardProps) { + return ( + + +
+
+ {icon} + {title} +
+ {headerAction} +
+ {description && {description}} +
+ {children} +
+ ); +} + +type ResourceSectionProps = SectionCardProps & { + count: number; + emptyIcon: React.ReactNode; + emptyTitle: string; + emptyDescription: string; +}; + +export function ResourceSection({ + count, + emptyIcon, + emptyTitle, + emptyDescription, + children, + ...props +}: ResourceSectionProps) { + return ( + {String(count).padStart(2, "0")} + } + > + {count === 0 ? ( +
+
{emptyIcon}
+

{emptyTitle}

+

{emptyDescription}

+
+ ) : ( + children + )} +
+ ); +} + +export function TableActions({ className, ...props }: React.ComponentProps<"div">) { + return
; +} diff --git a/apps/web/components/stat-strip.tsx b/apps/web/components/stat-strip.tsx new file mode 100644 index 0000000..f462f78 --- /dev/null +++ b/apps/web/components/stat-strip.tsx @@ -0,0 +1,47 @@ +import type { LucideIcon } from "lucide-react"; +import { cn } from "@/lib/cn"; + +export type StatItem = { + label: string; + value: number | string; + icon?: LucideIcon; + tone?: "default" | "positive" | "warning"; +}; + +export function StatStrip({ + items, + className, +}: { + items: readonly StatItem[]; + className?: string; +}) { + return ( +
+ {items.map(({ label, value, icon: Icon, tone = "default" }) => ( +
+
+ {Icon && ( + + )} + + {label} + +
+ {value} +
+ ))} +
+ ); +} diff --git a/apps/web/components/status-badge.tsx b/apps/web/components/status-badge.tsx new file mode 100644 index 0000000..5bd03f5 --- /dev/null +++ b/apps/web/components/status-badge.tsx @@ -0,0 +1,40 @@ +import { AlertCircle, CheckCircle2, CircleDashed, XCircle } from "lucide-react"; +import type { ReactNode } from "react"; +import { Badge } from "@/components/ui/badge"; +import { cn } from "@/lib/cn"; + +const statusConfig = { + success: { + icon: CheckCircle2, + className: "border-success/35 bg-success/12 text-success-foreground", + }, + warning: { + icon: AlertCircle, + className: "border-warning/40 bg-warning/14 text-warning-foreground", + }, + error: { icon: XCircle, className: "border-destructive/35 bg-destructive/10 text-destructive" }, + neutral: { icon: CircleDashed, className: "border-border bg-muted/60 text-muted-foreground" }, +} as const; + +export type StatusTone = keyof typeof statusConfig; + +export function StatusBadge({ + tone = "neutral", + children, + pulse = false, + className, +}: { + tone?: StatusTone; + children: ReactNode; + pulse?: boolean; + className?: string; +}) { + const { icon: Icon, className: toneClassName } = statusConfig[tone]; + + return ( + + + {children} + + ); +} diff --git a/apps/web/components/ui/accordion.tsx b/apps/web/components/ui/accordion.tsx index 91c6a78..f92ec4d 100644 --- a/apps/web/components/ui/accordion.tsx +++ b/apps/web/components/ui/accordion.tsx @@ -24,7 +24,7 @@ const AccordionTrigger = React.forwardRef< svg]:rotate-180", + "flex flex-1 items-center justify-between py-4 font-mono text-[10px] font-bold uppercase tracking-[0.1em] transition-colors hover:text-primary [&[data-state=open]>svg]:rotate-180", className )} {...props} diff --git a/apps/web/components/ui/badge.tsx b/apps/web/components/ui/badge.tsx index ce5e877..277e768 100644 --- a/apps/web/components/ui/badge.tsx +++ b/apps/web/components/ui/badge.tsx @@ -5,11 +5,11 @@ import type * as React from "react"; import { cn } from "@/lib/cn"; const badgeVariants = cva( - "inline-flex items-center justify-center rounded-full border px-2 py-0.5 text-xs font-medium w-fit whitespace-nowrap shrink-0 [&>svg]:size-3 gap-1 [&>svg]:pointer-events-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive transition-[color,box-shadow] overflow-hidden", + "inline-flex w-fit shrink-0 items-center justify-center gap-1 overflow-hidden rounded-sm border px-2 py-0.5 font-mono text-[10px] font-bold uppercase tracking-[0.08em] whitespace-nowrap transition-[color,box-shadow] [&>svg]:size-3 [&>svg]:pointer-events-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 aria-invalid:border-destructive", { variants: { variant: { - default: "border-transparent bg-primary text-primary-foreground [a&]:hover:bg-primary/90", + default: "border-foreground/20 bg-primary text-primary-foreground [a&]:hover:bg-primary/90", secondary: "border-transparent bg-secondary text-secondary-foreground [a&]:hover:bg-secondary/90", destructive: diff --git a/apps/web/components/ui/button.tsx b/apps/web/components/ui/button.tsx index a03d6ca..42ff5c9 100644 --- a/apps/web/components/ui/button.tsx +++ b/apps/web/components/ui/button.tsx @@ -5,24 +5,24 @@ import type * as React from "react"; import { cn } from "@/lib/cn"; const buttonVariants = cva( - "inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-all disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 shrink-0 [&_svg]:shrink-0 outline-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive", + "inline-flex shrink-0 items-center justify-center gap-2 whitespace-nowrap rounded-sm border border-transparent text-sm font-bold tracking-[-0.01em] transition-[background-color,color,border-color,transform,box-shadow] disabled:pointer-events-none disabled:opacity-50 active:translate-y-px [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 [&_svg]:shrink-0 outline-none focus-visible:ring-ring/40 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 aria-invalid:border-destructive", { variants: { variant: { - default: "bg-primary text-primary-foreground hover:bg-primary/90", + default: + "border-foreground bg-primary text-primary-foreground shadow-[3px_3px_0_var(--foreground)] hover:-translate-y-0.5 hover:shadow-[4px_4px_0_var(--foreground)]", destructive: - "bg-destructive text-white hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60", - outline: - "border bg-background shadow-xs hover:bg-accent hover:text-accent-foreground dark:bg-input/30 dark:border-input dark:hover:bg-input/50", - secondary: "bg-secondary text-secondary-foreground hover:bg-secondary/80", - ghost: "hover:bg-accent hover:text-accent-foreground dark:hover:bg-accent/50", + "border-destructive bg-destructive text-white hover:bg-destructive/90 focus-visible:ring-destructive/20", + outline: "border-border bg-card text-foreground hover:border-foreground hover:bg-accent", + secondary: "border-border bg-secondary text-secondary-foreground hover:border-foreground", + ghost: "text-current hover:bg-accent hover:text-accent-foreground", link: "text-primary underline-offset-4 hover:underline", }, size: { - default: "h-9 px-4 py-2 has-[>svg]:px-3", - sm: "h-8 rounded-md gap-1.5 px-3 has-[>svg]:px-2.5", - lg: "h-10 rounded-md px-6 has-[>svg]:px-4", - icon: "size-9", + default: "h-10 px-4 py-2 has-[>svg]:px-3", + sm: "h-8 gap-1.5 px-3 has-[>svg]:px-2.5", + lg: "h-12 px-6 has-[>svg]:px-4", + icon: "size-10", "icon-sm": "size-8", "icon-lg": "size-10", }, diff --git a/apps/web/components/ui/card.tsx b/apps/web/components/ui/card.tsx index b3fc811..641c4d4 100644 --- a/apps/web/components/ui/card.tsx +++ b/apps/web/components/ui/card.tsx @@ -7,7 +7,7 @@ function Card({ className, ...props }: React.ComponentProps<"div">) {
) {
) { return (
); @@ -59,7 +59,7 @@ function CardAction({ className, ...props }: React.ComponentProps<"div">) { } function CardContent({ className, ...props }: React.ComponentProps<"div">) { - return
; + return
; } function CardFooter({ className, ...props }: React.ComponentProps<"div">) { @@ -72,4 +72,4 @@ function CardFooter({ className, ...props }: React.ComponentProps<"div">) { ); } -export { Card, CardHeader, CardFooter, CardTitle, CardAction, CardDescription, CardContent }; +export { Card, CardAction, CardContent, CardDescription, CardFooter, CardHeader, CardTitle }; diff --git a/apps/web/components/ui/dialog.tsx b/apps/web/components/ui/dialog.tsx index 34e4f53..0e6a72b 100644 --- a/apps/web/components/ui/dialog.tsx +++ b/apps/web/components/ui/dialog.tsx @@ -52,7 +52,7 @@ function DialogContent({ ); diff --git a/apps/web/components/ui/input.tsx b/apps/web/components/ui/input.tsx index de7e989..db74fcc 100644 --- a/apps/web/components/ui/input.tsx +++ b/apps/web/components/ui/input.tsx @@ -8,8 +8,8 @@ function Input({ className, type, ...props }: React.ComponentProps<"input">) { type={type} data-slot="input" className={cn( - "file:text-foreground placeholder:text-muted-foreground selection:bg-primary selection:text-primary-foreground dark:bg-input/30 border-input h-9 w-full min-w-0 rounded-md border bg-transparent px-3 py-1 text-base shadow-xs transition-[color,box-shadow] outline-none file:inline-flex file:h-7 file:border-0 file:bg-transparent file:text-sm file:font-medium disabled:pointer-events-none disabled:cursor-not-allowed disabled:opacity-50 md:text-sm", - "focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px]", + "file:text-foreground placeholder:text-muted-foreground selection:bg-primary selection:text-primary-foreground border-input h-10 w-full min-w-0 rounded-sm border bg-card px-3 py-1 font-mono text-base transition-[color,box-shadow] outline-none file:inline-flex file:h-7 file:border-0 file:bg-transparent file:text-sm file:font-medium disabled:pointer-events-none disabled:cursor-not-allowed disabled:opacity-50 md:text-sm", + "focus-visible:border-foreground focus-visible:shadow-[inset_3px_0_0_var(--primary)]", "aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive", className )} diff --git a/apps/web/components/ui/label.tsx b/apps/web/components/ui/label.tsx index d2f3637..94a97de 100644 --- a/apps/web/components/ui/label.tsx +++ b/apps/web/components/ui/label.tsx @@ -10,7 +10,7 @@ function Label({ className, ...props }: React.ComponentProps) { ) { [role=checkbox]]:translate-y-[2px]", + "h-10 px-3 text-left align-middle font-mono text-[10px] font-bold uppercase tracking-[0.14em] text-muted-foreground whitespace-nowrap [&:has([role=checkbox])]:pr-0 [&>[role=checkbox]]:translate-y-[2px]", className )} {...props} @@ -71,7 +71,7 @@ function TableCell({ className, ...props }: React.ComponentProps<"td">) { [role=checkbox]]:translate-y-[2px]", + "h-14 px-3 py-2 align-middle whitespace-nowrap [&:has([role=checkbox])]:pr-0 [&>[role=checkbox]]:translate-y-[2px]", className )} {...props} diff --git a/apps/web/components/ui/tabs.tsx b/apps/web/components/ui/tabs.tsx index 5e6ebaa..4917c8e 100644 --- a/apps/web/components/ui/tabs.tsx +++ b/apps/web/components/ui/tabs.tsx @@ -20,7 +20,7 @@ function TabsList({ className, ...props }: React.ComponentProps( return (