Implement shadcn/ui + Tailwind CSS UI layer
- Design token system via CSS custom properties (light/dark mode) - Theme context hook + ThemeToggle component - AppShell + collapsible Sidebar replace inline Nav - LoginPage redesigned: two-column grid with hero panel - shadcn/ui Button and Input components - Tailwind config wired to CSS variable tokens - All pages de-Nav'd; PrivateRoute/AdminRoute wrap with AppShell - TypeScript passes clean (npm run typecheck) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -2,6 +2,7 @@ import { Routes, Route, Navigate } from "react-router-dom";
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import { useAuth } from "./hooks/useAuth";
|
||||
import { getMe } from "./api/client";
|
||||
import AppShell from "./components/AppShell";
|
||||
import LoginPage from "./pages/LoginPage";
|
||||
import DashboardPage from "./pages/DashboardPage";
|
||||
import ProfilePage from "./pages/ProfilePage";
|
||||
@@ -13,7 +14,11 @@ import AIAdminSettingsPage from "./pages/AIAdminSettingsPage";
|
||||
|
||||
function PrivateRoute({ children }: { children: React.ReactNode }) {
|
||||
const { token } = useAuth();
|
||||
return token ? <>{children}</> : <Navigate to="/login" replace />;
|
||||
return token ? (
|
||||
<AppShell>{children}</AppShell>
|
||||
) : (
|
||||
<Navigate to="/login" replace />
|
||||
);
|
||||
}
|
||||
|
||||
function AdminRoute({ children }: { children: React.ReactNode }) {
|
||||
@@ -25,7 +30,7 @@ function AdminRoute({ children }: { children: React.ReactNode }) {
|
||||
if (isLoading) return null;
|
||||
// Redirect to /login (not /) so the route appears not to exist
|
||||
if (!user?.is_admin) return <Navigate to="/login" replace />;
|
||||
return <>{children}</>;
|
||||
return <AppShell>{children}</AppShell>;
|
||||
}
|
||||
|
||||
export default function App() {
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
import Sidebar from "@/components/Sidebar";
|
||||
|
||||
interface AppShellProps {
|
||||
children: React.ReactNode;
|
||||
}
|
||||
|
||||
export default function AppShell({ children }: AppShellProps) {
|
||||
return (
|
||||
<div className="flex h-screen bg-background">
|
||||
<Sidebar />
|
||||
<main className="flex-1 overflow-y-auto p-6">{children}</main>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,133 @@
|
||||
import { useState } from "react";
|
||||
import { NavLink } from "react-router-dom";
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import {
|
||||
Home,
|
||||
Grid2X2,
|
||||
Settings,
|
||||
ShieldCheck,
|
||||
ChevronRight,
|
||||
ChevronLeft,
|
||||
LogOut,
|
||||
UserCircle,
|
||||
} from "lucide-react";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import ThemeToggle from "@/components/ThemeToggle";
|
||||
import { useAuth } from "@/hooks/useAuth";
|
||||
import { getMe } from "@/api/client";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
const NAV_ITEMS = [
|
||||
{ to: "/", label: "Home", icon: Home, exact: true },
|
||||
{ to: "/apps", label: "Apps", icon: Grid2X2, exact: false },
|
||||
{ to: "/settings", label: "Settings", icon: Settings, exact: false },
|
||||
];
|
||||
|
||||
export default function Sidebar() {
|
||||
const [expanded, setExpanded] = useState(true);
|
||||
const { logout } = useAuth();
|
||||
const { data: user } = useQuery({ queryKey: ["me"], queryFn: getMe });
|
||||
|
||||
return (
|
||||
<aside
|
||||
className={cn(
|
||||
"flex flex-col h-screen bg-surface border-r border-border transition-all duration-200 shrink-0",
|
||||
expanded ? "w-56" : "w-16"
|
||||
)}
|
||||
>
|
||||
{/* Nav items */}
|
||||
<nav className="flex-1 py-4 px-2 space-y-1 overflow-hidden">
|
||||
{NAV_ITEMS.map(({ to, label, icon: Icon, exact }) => (
|
||||
<NavLink
|
||||
key={to}
|
||||
to={to}
|
||||
end={exact}
|
||||
className={({ isActive }) =>
|
||||
cn(
|
||||
"flex items-center rounded-lg transition-colors",
|
||||
expanded ? "px-3 py-2 gap-3" : "justify-center py-3",
|
||||
isActive
|
||||
? "bg-primary/10 text-primary"
|
||||
: "text-muted hover:bg-muted/20 hover:text-foreground"
|
||||
)
|
||||
}
|
||||
>
|
||||
<Icon className="h-5 w-5 shrink-0" />
|
||||
{expanded && (
|
||||
<span className="text-sm font-medium whitespace-nowrap">{label}</span>
|
||||
)}
|
||||
</NavLink>
|
||||
))}
|
||||
|
||||
{user?.is_admin && (
|
||||
<NavLink
|
||||
to="/admin"
|
||||
className={({ isActive }) =>
|
||||
cn(
|
||||
"flex items-center rounded-lg transition-colors",
|
||||
expanded ? "px-3 py-2 gap-3" : "justify-center py-3",
|
||||
isActive
|
||||
? "bg-primary/10 text-primary"
|
||||
: "text-muted hover:bg-muted/20 hover:text-foreground"
|
||||
)
|
||||
}
|
||||
>
|
||||
<ShieldCheck className="h-5 w-5 shrink-0" />
|
||||
{expanded && (
|
||||
<span className="text-sm font-medium whitespace-nowrap">Admin</span>
|
||||
)}
|
||||
</NavLink>
|
||||
)}
|
||||
</nav>
|
||||
|
||||
{/* Bottom section */}
|
||||
<div className="py-4 px-2 space-y-1 border-t border-border">
|
||||
{/* User avatar row */}
|
||||
<div
|
||||
className={cn(
|
||||
"flex items-center rounded-lg py-2 text-muted",
|
||||
expanded ? "px-3 gap-3" : "justify-center"
|
||||
)}
|
||||
>
|
||||
<UserCircle className="h-5 w-5 shrink-0" />
|
||||
{expanded && user && (
|
||||
<span className="text-sm font-medium truncate">{user.email}</span>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Theme toggle */}
|
||||
<div className={cn("flex", expanded ? "px-1" : "justify-center")}>
|
||||
<ThemeToggle />
|
||||
</div>
|
||||
|
||||
{/* Logout */}
|
||||
<Button
|
||||
variant="ghost"
|
||||
className={cn(
|
||||
"w-full text-muted hover:text-foreground",
|
||||
expanded ? "justify-start px-3 gap-3" : "justify-center px-0"
|
||||
)}
|
||||
onClick={logout}
|
||||
>
|
||||
<LogOut className="h-5 w-5 shrink-0" />
|
||||
{expanded && <span className="text-sm font-medium">Logout</span>}
|
||||
</Button>
|
||||
|
||||
{/* Collapse toggle */}
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className={cn("w-full text-muted hover:text-foreground", !expanded && "px-0")}
|
||||
onClick={() => setExpanded((e) => !e)}
|
||||
aria-label={expanded ? "Collapse sidebar" : "Expand sidebar"}
|
||||
>
|
||||
{expanded ? (
|
||||
<ChevronLeft className="h-5 w-5" />
|
||||
) : (
|
||||
<ChevronRight className="h-5 w-5" />
|
||||
)}
|
||||
</Button>
|
||||
</div>
|
||||
</aside>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
import { Sun, Moon } from "lucide-react";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { useTheme } from "@/hooks/useTheme";
|
||||
|
||||
export default function ThemeToggle() {
|
||||
const { theme, toggleTheme } = useTheme();
|
||||
|
||||
return (
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
onClick={toggleTheme}
|
||||
aria-label={theme === "dark" ? "Switch to light mode" : "Switch to dark mode"}
|
||||
>
|
||||
{theme === "dark" ? (
|
||||
<Sun className="h-5 w-5" />
|
||||
) : (
|
||||
<Moon className="h-5 w-5" />
|
||||
)}
|
||||
</Button>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
import * as React from "react";
|
||||
import { Slot } from "@radix-ui/react-slot";
|
||||
import { cva, type VariantProps } from "class-variance-authority";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
const buttonVariants = cva(
|
||||
"inline-flex items-center justify-center whitespace-nowrap rounded-lg text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50",
|
||||
{
|
||||
variants: {
|
||||
variant: {
|
||||
default:
|
||||
"bg-primary text-white hover:bg-primary-hover",
|
||||
ghost:
|
||||
"hover:bg-muted/20 hover:text-foreground",
|
||||
outline:
|
||||
"border border-border bg-transparent hover:bg-muted/20 text-foreground",
|
||||
destructive:
|
||||
"bg-red-600 text-white hover:bg-red-700",
|
||||
},
|
||||
size: {
|
||||
default: "h-10 px-4 py-2",
|
||||
sm: "h-9 px-3",
|
||||
lg: "h-11 px-8",
|
||||
icon: "h-10 w-10",
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
variant: "default",
|
||||
size: "default",
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
export interface ButtonProps
|
||||
extends React.ButtonHTMLAttributes<HTMLButtonElement>,
|
||||
VariantProps<typeof buttonVariants> {
|
||||
asChild?: boolean;
|
||||
}
|
||||
|
||||
const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
|
||||
({ className, variant, size, asChild = false, ...props }, ref) => {
|
||||
const Comp = asChild ? Slot : "button";
|
||||
return (
|
||||
<Comp
|
||||
className={cn(buttonVariants({ variant, size, className }))}
|
||||
ref={ref}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
);
|
||||
Button.displayName = "Button";
|
||||
|
||||
export { Button, buttonVariants };
|
||||
@@ -0,0 +1,22 @@
|
||||
import * as React from "react";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
const Input = React.forwardRef<
|
||||
HTMLInputElement,
|
||||
React.InputHTMLAttributes<HTMLInputElement>
|
||||
>(({ className, type, ...props }, ref) => {
|
||||
return (
|
||||
<input
|
||||
type={type}
|
||||
className={cn(
|
||||
"flex h-10 w-full rounded-lg border border-border bg-surface px-3 py-2 text-sm text-foreground placeholder:text-muted focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50",
|
||||
className
|
||||
)}
|
||||
ref={ref}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
});
|
||||
Input.displayName = "Input";
|
||||
|
||||
export { Input };
|
||||
@@ -0,0 +1,32 @@
|
||||
import { useState, useEffect, useCallback } from "react";
|
||||
|
||||
type Theme = "light" | "dark";
|
||||
|
||||
function applyTheme(theme: Theme) {
|
||||
if (theme === "dark") {
|
||||
document.documentElement.classList.add("dark");
|
||||
} else {
|
||||
document.documentElement.classList.remove("dark");
|
||||
}
|
||||
}
|
||||
|
||||
export function useTheme() {
|
||||
const [theme, setTheme] = useState<Theme>(() => {
|
||||
const stored = localStorage.getItem("theme") as Theme | null;
|
||||
if (stored === "light" || stored === "dark") return stored;
|
||||
return window.matchMedia("(prefers-color-scheme: dark)").matches
|
||||
? "dark"
|
||||
: "light";
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
applyTheme(theme);
|
||||
localStorage.setItem("theme", theme);
|
||||
}, [theme]);
|
||||
|
||||
const toggleTheme = useCallback(() => {
|
||||
setTheme((t) => (t === "light" ? "dark" : "light"));
|
||||
}, []);
|
||||
|
||||
return { theme, toggleTheme };
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
import { clsx, type ClassValue } from "clsx";
|
||||
import { twMerge } from "tailwind-merge";
|
||||
|
||||
export function cn(...inputs: ClassValue[]) {
|
||||
return twMerge(clsx(inputs));
|
||||
}
|
||||
@@ -3,6 +3,7 @@ import ReactDOM from "react-dom/client";
|
||||
import { BrowserRouter } from "react-router-dom";
|
||||
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
|
||||
|
||||
import "./styles/theme.css";
|
||||
import App from "./App";
|
||||
|
||||
const queryClient = new QueryClient();
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import { useEffect, useState } from "react";
|
||||
import { useQuery, useMutation } from "@tanstack/react-query";
|
||||
import Nav from "../components/Nav";
|
||||
import { getAISettings, updateAISettings, testAIConnection } from "../api/client";
|
||||
|
||||
type Provider = "anthropic" | "ollama" | "lmstudio";
|
||||
@@ -96,17 +95,11 @@ export default function AIAdminSettingsPage() {
|
||||
};
|
||||
|
||||
if (isLoading) {
|
||||
return (
|
||||
<>
|
||||
<Nav />
|
||||
<div style={{ padding: 32 }}>Loading…</div>
|
||||
</>
|
||||
);
|
||||
return <div style={{ padding: 32 }}>Loading…</div>;
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<Nav />
|
||||
<div style={{ padding: 32, maxWidth: 600, margin: "0 auto" }}>
|
||||
<h1 style={{ fontSize: 24, marginBottom: 32 }}>AI Service — Settings</h1>
|
||||
|
||||
|
||||
@@ -9,7 +9,6 @@ import {
|
||||
type AdminUserCreate,
|
||||
type UserData,
|
||||
} from "../api/client";
|
||||
import Nav from "../components/Nav";
|
||||
|
||||
export default function AdminPage() {
|
||||
const queryClient = useQueryClient();
|
||||
@@ -68,7 +67,6 @@ export default function AdminPage() {
|
||||
|
||||
return (
|
||||
<>
|
||||
<Nav />
|
||||
<div style={{ padding: 32, maxWidth: 800 }}>
|
||||
<h1>User Management</h1>
|
||||
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import { Link } from "react-router-dom";
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import Nav from "../components/Nav";
|
||||
import { getMe } from "../api/client";
|
||||
|
||||
interface AppCard {
|
||||
@@ -36,7 +35,6 @@ export default function AppsPage() {
|
||||
|
||||
return (
|
||||
<>
|
||||
<Nav />
|
||||
<div style={{ padding: 32, maxWidth: 900, margin: "0 auto" }}>
|
||||
<h1>Apps</h1>
|
||||
<div style={{ display: "flex", gap: 24, flexWrap: "wrap", marginTop: 24 }}>
|
||||
@@ -102,3 +100,4 @@ export default function AppsPage() {
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,17 +1,13 @@
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import { getMe } from "../api/client";
|
||||
import Nav from "../components/Nav";
|
||||
|
||||
export default function DashboardPage() {
|
||||
const { data: user } = useQuery({ queryKey: ["me"], queryFn: getMe });
|
||||
|
||||
return (
|
||||
<>
|
||||
<Nav />
|
||||
<div style={{ padding: 32 }}>
|
||||
<h1>Dashboard</h1>
|
||||
{user && <p>Welcome, {user.full_name ?? user.email}</p>}
|
||||
</div>
|
||||
</>
|
||||
<div>
|
||||
<h1 className="text-2xl font-semibold text-foreground mb-2">Dashboard</h1>
|
||||
{user && <p className="text-muted">Welcome, {user.full_name ?? user.email}</p>}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import { useEffect, useState } from "react";
|
||||
import { useQuery, useMutation } from "@tanstack/react-query";
|
||||
import Nav from "../components/Nav";
|
||||
import { getDocumentLimits, updateDocumentLimits } from "../api/client";
|
||||
|
||||
const inputStyle: React.CSSProperties = {
|
||||
@@ -34,17 +33,11 @@ export default function DocumentAdminSettingsPage() {
|
||||
});
|
||||
|
||||
if (isLoading) {
|
||||
return (
|
||||
<>
|
||||
<Nav />
|
||||
<div style={{ padding: 32 }}>Loading…</div>
|
||||
</>
|
||||
);
|
||||
return <div style={{ padding: 32 }}>Loading…</div>;
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<Nav />
|
||||
<div style={{ padding: 32, maxWidth: 600, margin: "0 auto" }}>
|
||||
<h1 style={{ fontSize: 24, marginBottom: 32 }}>Documents — Settings</h1>
|
||||
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import { useRef, useState, useEffect, useCallback } from "react";
|
||||
import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query";
|
||||
import Nav from "../components/Nav";
|
||||
import {
|
||||
listDocuments,
|
||||
uploadDocument,
|
||||
@@ -707,7 +706,6 @@ export default function DocumentsPage() {
|
||||
|
||||
return (
|
||||
<>
|
||||
<Nav />
|
||||
<div style={{ padding: 32, maxWidth: 960, margin: "0 auto" }}>
|
||||
<h1>Documents</h1>
|
||||
|
||||
|
||||
@@ -1,11 +1,13 @@
|
||||
import axios from "axios";
|
||||
import { useState } from "react";
|
||||
import { Briefcase, LayoutDashboard } from "lucide-react";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import ThemeToggle from "@/components/ThemeToggle";
|
||||
import { useAuth } from "../hooks/useAuth";
|
||||
|
||||
// ── Customise these two constants for each deployment ────────────────────────
|
||||
// ── Customise for each deployment ────────────────────────────────────────────
|
||||
const BUSINESS_NAME = "Your Business Name";
|
||||
// Replace the src with the actual logo URL or import, or swap the placeholder
|
||||
// div below for an <img> tag once a logo is available.
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
export default function LoginPage() {
|
||||
@@ -30,62 +32,59 @@ export default function LoginPage() {
|
||||
};
|
||||
|
||||
return (
|
||||
<div style={{
|
||||
minHeight: "100vh",
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
}}>
|
||||
<div style={{ width: 360, padding: 32 }}>
|
||||
|
||||
{/* Logo placeholder — swap for <img src="..." alt="Logo" /> */}
|
||||
<div style={{
|
||||
width: 96,
|
||||
height: 96,
|
||||
margin: "0 auto 24px",
|
||||
border: "2px dashed #aaa",
|
||||
borderRadius: 8,
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
color: "#aaa",
|
||||
fontSize: 12,
|
||||
}}>
|
||||
Logo
|
||||
<div className="grid md:grid-cols-2 min-h-screen">
|
||||
{/* ── Left column: login form ── */}
|
||||
<div className="relative flex flex-col items-center justify-center p-8 bg-surface">
|
||||
{/* Theme toggle — top-right of this panel */}
|
||||
<div className="absolute top-4 right-4">
|
||||
<ThemeToggle />
|
||||
</div>
|
||||
|
||||
<h1 style={{ textAlign: "center", marginBottom: 24 }}>{BUSINESS_NAME}</h1>
|
||||
<div className="w-full max-w-sm space-y-6">
|
||||
{/* Logo placeholder */}
|
||||
<div className="flex flex-col items-center gap-3">
|
||||
<div className="flex h-16 w-16 items-center justify-center rounded-lg bg-primary/10">
|
||||
<Briefcase className="h-8 w-8 text-primary" />
|
||||
</div>
|
||||
<h1 className="text-xl font-semibold text-primary">{BUSINESS_NAME}</h1>
|
||||
<p className="text-sm text-muted">Sign in to your account</p>
|
||||
</div>
|
||||
|
||||
<form onSubmit={handleSubmit}>
|
||||
<div style={{ marginBottom: 12 }}>
|
||||
<label style={{ display: "block", marginBottom: 4 }}>Email</label>
|
||||
<input
|
||||
{/* Form */}
|
||||
<form onSubmit={handleSubmit} className="space-y-4">
|
||||
<Input
|
||||
type="email"
|
||||
placeholder="you@company.com"
|
||||
value={email}
|
||||
onChange={(e) => setEmail(e.target.value)}
|
||||
required
|
||||
style={{ width: "100%", padding: "6px 8px", boxSizing: "border-box" }}
|
||||
/>
|
||||
</div>
|
||||
<div style={{ marginBottom: 12 }}>
|
||||
<label style={{ display: "block", marginBottom: 4 }}>Password</label>
|
||||
<input
|
||||
<Input
|
||||
type="password"
|
||||
placeholder="••••••••"
|
||||
value={password}
|
||||
onChange={(e) => setPassword(e.target.value)}
|
||||
required
|
||||
style={{ width: "100%", padding: "6px 8px", boxSizing: "border-box" }}
|
||||
/>
|
||||
</div>
|
||||
{error && <p style={{ color: "red", margin: "8px 0" }}>{error}</p>}
|
||||
<button
|
||||
type="submit"
|
||||
style={{ width: "100%", padding: "8px 0", marginTop: 8, cursor: "pointer" }}
|
||||
>
|
||||
Sign in
|
||||
</button>
|
||||
</form>
|
||||
{error && (
|
||||
<p className="text-sm text-red-500">{error}</p>
|
||||
)}
|
||||
<Button type="submit" className="w-full">
|
||||
Sign In
|
||||
</Button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* ── Right column: hero panel ── */}
|
||||
<div className="hidden md:flex flex-col items-center justify-center bg-primary p-8 gap-4">
|
||||
<LayoutDashboard className="h-20 w-20 text-white/80" />
|
||||
<h2 className="text-2xl font-bold text-white text-center">
|
||||
Manage your team, your way
|
||||
</h2>
|
||||
<p className="text-sm text-white/70 text-center">
|
||||
The all-in-one employer platform.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import { useState } from "react";
|
||||
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
||||
import { getMe, getProfile, updateProfile, type ProfileUpdate } from "../api/client";
|
||||
import Nav from "../components/Nav";
|
||||
|
||||
export default function ProfilePage() {
|
||||
const queryClient = useQueryClient();
|
||||
@@ -54,11 +53,10 @@ export default function ProfilePage() {
|
||||
mutation.mutate(payload);
|
||||
};
|
||||
|
||||
if (isLoading) return <><Nav /><div style={{ padding: 32 }}>Loading…</div></>;
|
||||
if (isLoading) return <div style={{ padding: 32 }}>Loading…</div>;
|
||||
|
||||
return (
|
||||
<>
|
||||
<Nav />
|
||||
<div style={{ padding: 32, maxWidth: 480 }}>
|
||||
<h1>Profile</h1>
|
||||
|
||||
|
||||
@@ -0,0 +1,60 @@
|
||||
@tailwind base;
|
||||
@tailwind components;
|
||||
@tailwind utilities;
|
||||
|
||||
/* ── Design tokens ──────────────────────────────────────────────────────────
|
||||
Colors are stored as RGB triplets so Tailwind's opacity modifier syntax
|
||||
(e.g. bg-primary/10) works correctly.
|
||||
────────────────────────────────────────────────────────────────────────── */
|
||||
|
||||
:root {
|
||||
/* Brand / interactive */
|
||||
--color-primary: 37 99 235; /* #2563EB blue-600 */
|
||||
--color-primary-hover: 29 78 216; /* #1D4ED8 blue-700 */
|
||||
--color-accent: 234 179 8; /* #EAB308 yellow-500 */
|
||||
--color-accent-hover: 202 138 4; /* #CA8A04 yellow-600 */
|
||||
|
||||
/* Layout */
|
||||
--color-background: 248 250 252; /* #F8FAFC slate-50 */
|
||||
--color-surface: 255 255 255; /* #FFFFFF */
|
||||
--color-border: 226 232 240; /* #E2E8F0 slate-200 */
|
||||
|
||||
/* Text */
|
||||
--color-text-primary: 15 23 42; /* #0F172A slate-900 */
|
||||
--color-text-muted: 100 116 139; /* #64748B slate-500 */
|
||||
|
||||
/* Shape */
|
||||
--radius: 0.5rem;
|
||||
}
|
||||
|
||||
.dark {
|
||||
/* Brand / interactive */
|
||||
--color-primary: 59 130 246; /* #3B82F6 blue-500 */
|
||||
--color-primary-hover: 37 99 235; /* #2563EB blue-600 */
|
||||
--color-accent: 250 204 21; /* #FACC15 yellow-400 */
|
||||
--color-accent-hover: 234 179 8; /* #EAB308 yellow-500 */
|
||||
|
||||
/* Layout */
|
||||
--color-background: 15 23 42; /* #0F172A slate-900 */
|
||||
--color-surface: 30 41 59; /* #1E293B slate-800 */
|
||||
--color-border: 51 65 85; /* #334155 slate-700 */
|
||||
|
||||
/* Text */
|
||||
--color-text-primary: 248 250 252; /* #F8FAFC slate-50 */
|
||||
--color-text-muted: 148 163 184; /* #94A3B8 slate-400 */
|
||||
}
|
||||
|
||||
/* ── Base resets ─────────────────────────────────────────────────────────── */
|
||||
|
||||
*,
|
||||
*::before,
|
||||
*::after {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
body {
|
||||
@apply bg-background text-foreground;
|
||||
font-family: system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto,
|
||||
sans-serif;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
}
|
||||
Reference in New Issue
Block a user