Build new shell layout with left sidebar (el-3po)
This commit is contained in:
@@ -0,0 +1,25 @@
|
||||
{
|
||||
"$schema": "https://ui.shadcn.com/schema.json",
|
||||
"style": "radix-nova",
|
||||
"rsc": false,
|
||||
"tsx": true,
|
||||
"tailwind": {
|
||||
"config": "tailwind.config.js",
|
||||
"css": "src/index.css",
|
||||
"baseColor": "neutral",
|
||||
"cssVariables": true,
|
||||
"prefix": ""
|
||||
},
|
||||
"iconLibrary": "lucide",
|
||||
"rtl": false,
|
||||
"aliases": {
|
||||
"components": "@/components",
|
||||
"utils": "@/lib/utils",
|
||||
"ui": "@/components/ui",
|
||||
"lib": "@/lib",
|
||||
"hooks": "@/hooks"
|
||||
},
|
||||
"menuColor": "default",
|
||||
"menuAccent": "subtle",
|
||||
"registries": {}
|
||||
}
|
||||
Generated
+5694
-50
File diff suppressed because it is too large
Load Diff
+14
-1
@@ -12,29 +12,42 @@
|
||||
"dependencies": {
|
||||
"@emotion/react": "^11.14.0",
|
||||
"@emotion/styled": "^11.14.1",
|
||||
"@fontsource-variable/geist": "^5.2.8",
|
||||
"@mui/icons-material": "^9.0.0",
|
||||
"@mui/material": "^9.0.0",
|
||||
"@mui/x-data-grid": "^9.0.4",
|
||||
"@tanstack/react-query": "^5.100.6",
|
||||
"class-variance-authority": "^0.7.1",
|
||||
"clsx": "^2.1.1",
|
||||
"d3": "^7.9.0",
|
||||
"lucide-react": "^1.14.0",
|
||||
"oidc-client-ts": "^3.5.0",
|
||||
"radix-ui": "^1.4.3",
|
||||
"react": "^19.2.5",
|
||||
"react-dom": "^19.2.5",
|
||||
"react-oidc-context": "^3.3.1",
|
||||
"react-router-dom": "^7.14.2",
|
||||
"recharts": "^3.8.1"
|
||||
"recharts": "^3.8.1",
|
||||
"shadcn": "^4.7.0",
|
||||
"tailwind-merge": "^3.6.0",
|
||||
"tw-animate-css": "^1.4.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@eslint/js": "^10.0.1",
|
||||
"@tailwindcss/postcss": "^4.3.0",
|
||||
"@tailwindcss/vite": "^4.3.0",
|
||||
"@types/d3": "^7.4.3",
|
||||
"@types/node": "^24.12.2",
|
||||
"@types/react": "^19.2.14",
|
||||
"@types/react-dom": "^19.2.3",
|
||||
"@vitejs/plugin-react": "^6.0.1",
|
||||
"autoprefixer": "^10.5.0",
|
||||
"eslint": "^10.2.1",
|
||||
"eslint-plugin-react-hooks": "^7.1.1",
|
||||
"eslint-plugin-react-refresh": "^0.5.2",
|
||||
"globals": "^17.5.0",
|
||||
"postcss": "^8.5.14",
|
||||
"tailwindcss": "^4.3.0",
|
||||
"typescript": "~6.0.2",
|
||||
"typescript-eslint": "^8.58.2",
|
||||
"vite": "^8.0.10"
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
module.exports = { plugins: { "@tailwindcss/postcss": {}, autoprefixer: {} } }
|
||||
+351
-242
@@ -4,30 +4,14 @@ import {
|
||||
Route,
|
||||
NavLink,
|
||||
useLocation,
|
||||
Outlet,
|
||||
} from "react-router-dom";
|
||||
import {
|
||||
QueryClient,
|
||||
QueryClientProvider,
|
||||
useQuery,
|
||||
} from "@tanstack/react-query";
|
||||
import { ThemeProvider } from "@mui/material/styles";
|
||||
import {
|
||||
AppBar,
|
||||
Typography,
|
||||
Box,
|
||||
Tabs,
|
||||
Tab,
|
||||
Container,
|
||||
CssBaseline,
|
||||
Chip,
|
||||
useMediaQuery,
|
||||
Button,
|
||||
Stack,
|
||||
Card,
|
||||
CardContent,
|
||||
CircularProgress,
|
||||
} from "@mui/material";
|
||||
import { useEffect, useMemo } from "react";
|
||||
import { useEffect, useMemo, useState } from "react";
|
||||
import { AuthProvider, useAuth } from "react-oidc-context";
|
||||
import { Dashboard } from "./pages/Dashboard";
|
||||
import { Monitoring } from "./pages/Monitoring";
|
||||
@@ -37,27 +21,209 @@ import { UsersPage } from "./pages/Users";
|
||||
import { FileBrowser } from "./pages/FileBrowser";
|
||||
import { Actions } from "./pages/Actions";
|
||||
import BackupsPage from "./components/BackupsPage";
|
||||
import { getAppTheme } from "./theme";
|
||||
import { getOidcConfig, isOidcConfigured, setAccessToken } from "./auth";
|
||||
import { fetchAppVersion } from "./api/client";
|
||||
import { FRONTEND_VERSION_LABEL } from "./version";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import {
|
||||
Tooltip,
|
||||
TooltipContent,
|
||||
TooltipProvider,
|
||||
TooltipTrigger,
|
||||
} from "@/components/ui/tooltip";
|
||||
import {
|
||||
Sheet,
|
||||
SheetContent,
|
||||
SheetHeader,
|
||||
SheetTitle,
|
||||
SheetTrigger,
|
||||
} from "@/components/ui/sheet";
|
||||
import {
|
||||
LayoutDashboard,
|
||||
Activity,
|
||||
Monitor,
|
||||
Users,
|
||||
Zap,
|
||||
FolderOpen,
|
||||
Settings as SettingsIcon,
|
||||
Menu,
|
||||
Sun,
|
||||
Moon,
|
||||
LogOut,
|
||||
ChevronLeft,
|
||||
ChevronRight,
|
||||
} from "lucide-react";
|
||||
|
||||
const queryClient = new QueryClient({
|
||||
defaultOptions: { queries: { retry: 1, refetchOnWindowFocus: false } },
|
||||
});
|
||||
|
||||
function Shell({
|
||||
// Navigation items for sidebar
|
||||
const navItems = [
|
||||
{ path: "/", label: "Dashboard", icon: LayoutDashboard },
|
||||
{ path: "/monitoring", label: "Monitoring", icon: Activity },
|
||||
{ path: "/applications", label: "Media", icon: Monitor },
|
||||
{ path: "/files", label: "Files", icon: FolderOpen },
|
||||
{ path: "/users", label: "Users", icon: Users },
|
||||
{ path: "/actions", label: "Actions", icon: Zap },
|
||||
{ path: "/settings", label: "Settings", icon: SettingsIcon },
|
||||
];
|
||||
|
||||
function Sidebar({
|
||||
collapsed,
|
||||
onToggle,
|
||||
isMobile,
|
||||
}: {
|
||||
collapsed: boolean;
|
||||
onToggle: () => void;
|
||||
isMobile: boolean;
|
||||
}) {
|
||||
const location = useLocation();
|
||||
|
||||
if (isMobile) return null;
|
||||
|
||||
return (
|
||||
<aside
|
||||
className={`fixed left-0 top-0 z-40 h-screen border-r border-border bg-card transition-all duration-300 ${
|
||||
collapsed ? "w-16" : "w-60"
|
||||
}`}
|
||||
>
|
||||
<div className="flex h-full flex-col">
|
||||
{/* Logo area */}
|
||||
<div className="flex h-14 items-center justify-between border-b border-border px-3">
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="flex h-8 w-8 shrink-0 items-center justify-center rounded-lg bg-primary font-bold text-primary-foreground">
|
||||
M
|
||||
</div>
|
||||
{!collapsed && (
|
||||
<span className="font-semibold">Manage</span>
|
||||
)}
|
||||
</div>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="h-8 w-8"
|
||||
onClick={onToggle}
|
||||
>
|
||||
{collapsed ? (
|
||||
<ChevronRight className="h-4 w-4" />
|
||||
) : (
|
||||
<ChevronLeft className="h-4 w-4" />
|
||||
)}
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{/* Navigation */}
|
||||
<nav className="flex-1 space-y-1 p-2">
|
||||
<TooltipProvider delayDuration={0}>
|
||||
{navItems.map((item) => {
|
||||
const isItemActive = location.pathname === item.path;
|
||||
const Icon = item.icon;
|
||||
|
||||
return collapsed ? (
|
||||
<Tooltip key={item.path}>
|
||||
<TooltipTrigger asChild>
|
||||
<NavLink
|
||||
to={item.path}
|
||||
className={() =>
|
||||
`flex h-10 w-full items-center justify-center rounded-md transition-colors ${
|
||||
isItemActive
|
||||
? "bg-primary text-primary-foreground"
|
||||
: "text-muted-foreground hover:bg-accent hover:text-foreground"
|
||||
}`
|
||||
}
|
||||
>
|
||||
<Icon className="h-5 w-5" />
|
||||
</NavLink>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent side="right">
|
||||
{item.label}
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
) : (
|
||||
<NavLink
|
||||
key={item.path}
|
||||
to={item.path}
|
||||
className={() =>
|
||||
`flex h-10 w-full items-center gap-3 rounded-md px-3 transition-colors ${
|
||||
isItemActive
|
||||
? "bg-primary text-primary-foreground"
|
||||
: "text-muted-foreground hover:bg-accent hover:text-foreground"
|
||||
}`
|
||||
}
|
||||
>
|
||||
<Icon className="h-5 w-5 shrink-0" />
|
||||
<span className="text-sm font-medium">{item.label}</span>
|
||||
</NavLink>
|
||||
);
|
||||
})}
|
||||
</TooltipProvider>
|
||||
</nav>
|
||||
</div>
|
||||
</aside>
|
||||
);
|
||||
}
|
||||
|
||||
function MobileDrawer() {
|
||||
const [open, setOpen] = useState(false);
|
||||
const location = useLocation();
|
||||
|
||||
return (
|
||||
<Sheet open={open} onOpenChange={setOpen}>
|
||||
<SheetTrigger asChild>
|
||||
<Button variant="ghost" size="icon" className="md:hidden">
|
||||
<Menu className="h-5 w-5" />
|
||||
</Button>
|
||||
</SheetTrigger>
|
||||
<SheetContent side="left" className="w-60 p-0">
|
||||
<SheetHeader className="border-b border-border p-4">
|
||||
<SheetTitle className="flex items-center gap-2">
|
||||
<div className="flex h-8 w-8 items-center justify-center rounded-lg bg-primary font-bold text-primary-foreground">
|
||||
M
|
||||
</div>
|
||||
<span>Manage</span>
|
||||
</SheetTitle>
|
||||
</SheetHeader>
|
||||
<nav className="space-y-1 p-2">
|
||||
{navItems.map((item) => {
|
||||
const isItemActive = location.pathname === item.path;
|
||||
const Icon = item.icon;
|
||||
return (
|
||||
<NavLink
|
||||
key={item.path}
|
||||
to={item.path}
|
||||
onClick={() => setOpen(false)}
|
||||
className={() =>
|
||||
`flex h-10 w-full items-center gap-3 rounded-md px-3 transition-colors ${
|
||||
isItemActive
|
||||
? "bg-primary text-primary-foreground"
|
||||
: "text-muted-foreground hover:bg-accent hover:text-foreground"
|
||||
}`
|
||||
}
|
||||
>
|
||||
<Icon className="h-5 w-5 shrink-0" />
|
||||
<span className="text-sm font-medium">{item.label}</span>
|
||||
</NavLink>
|
||||
);
|
||||
})}
|
||||
</nav>
|
||||
</SheetContent>
|
||||
</Sheet>
|
||||
);
|
||||
}
|
||||
|
||||
function TopBar({
|
||||
darkMode,
|
||||
authLabel,
|
||||
onSignOut,
|
||||
onToggleDarkMode,
|
||||
}: {
|
||||
darkMode: boolean;
|
||||
authLabel?: string;
|
||||
onSignOut?: () => void;
|
||||
onToggleDarkMode: () => void;
|
||||
}) {
|
||||
const location = useLocation();
|
||||
const current = location.pathname;
|
||||
const isMobile = useMediaQuery("(max-width: 900px)");
|
||||
const { data: appVersion } = useQuery({
|
||||
queryKey: ["app-version"],
|
||||
queryFn: fetchAppVersion,
|
||||
@@ -65,238 +231,148 @@ function Shell({
|
||||
});
|
||||
const backendLabel = appVersion?.backend_label || "…";
|
||||
|
||||
return (
|
||||
<>
|
||||
<CssBaseline />
|
||||
<AppBar
|
||||
position="sticky"
|
||||
color="inherit"
|
||||
elevation={0}
|
||||
sx={{
|
||||
borderBottom: 1,
|
||||
borderColor: "divider",
|
||||
backgroundImage: "none",
|
||||
}}
|
||||
>
|
||||
<Box
|
||||
sx={{
|
||||
display: "flex",
|
||||
flexDirection: { xs: "column", md: "row" },
|
||||
alignItems: { xs: "stretch", md: "center" },
|
||||
justifyContent: "space-between",
|
||||
gap: 1.5,
|
||||
px: { xs: 1.5, sm: 2.5, md: 3 },
|
||||
py: { xs: 1, md: 1.25 },
|
||||
}}
|
||||
>
|
||||
<Box
|
||||
sx={{
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
gap: 1.5,
|
||||
minWidth: 0,
|
||||
}}
|
||||
>
|
||||
<Box
|
||||
sx={{
|
||||
width: 42,
|
||||
height: 42,
|
||||
borderRadius: 2,
|
||||
display: "grid",
|
||||
placeItems: "center",
|
||||
fontWeight: 800,
|
||||
fontSize: 20,
|
||||
color: "primary.contrastText",
|
||||
bgcolor: "primary.main",
|
||||
backgroundImage:
|
||||
"linear-gradient(135deg, rgba(255,255,255,0.22), rgba(255,255,255,0))",
|
||||
boxShadow: 1,
|
||||
flexShrink: 0,
|
||||
}}
|
||||
>
|
||||
M
|
||||
</Box>
|
||||
<Box sx={{ minWidth: 0 }}>
|
||||
<Typography
|
||||
variant="h6"
|
||||
sx={{ fontWeight: 800, lineHeight: 1.05 }}
|
||||
>
|
||||
Manage
|
||||
</Typography>
|
||||
<Typography variant="caption" color="text.secondary" noWrap>
|
||||
Media operations dashboard
|
||||
</Typography>
|
||||
</Box>
|
||||
</Box>
|
||||
const pageTitle =
|
||||
navItems.find((item) => item.path === location.pathname)?.label ||
|
||||
"Dashboard";
|
||||
|
||||
<Stack
|
||||
direction="row"
|
||||
spacing={1}
|
||||
sx={{
|
||||
alignItems: "center",
|
||||
flexWrap: "wrap",
|
||||
justifyContent: { xs: "flex-start", md: "flex-end" },
|
||||
width: { xs: "100%", md: "auto" },
|
||||
}}
|
||||
>
|
||||
<Chip
|
||||
label={`FE ${FRONTEND_VERSION_LABEL}`}
|
||||
variant="outlined"
|
||||
sx={{ maxWidth: { xs: "100%", sm: 220 } }}
|
||||
/>
|
||||
<Chip
|
||||
label={`BE ${backendLabel}`}
|
||||
variant="outlined"
|
||||
sx={{ maxWidth: { xs: "100%", sm: 220 } }}
|
||||
/>
|
||||
return (
|
||||
<header className="sticky top-0 z-30 flex h-14 items-center justify-between border-b border-border bg-background/80 backdrop-blur-md px-4">
|
||||
<div className="flex items-center gap-3">
|
||||
<MobileDrawer />
|
||||
<h1 className="text-lg font-semibold">{pageTitle}</h1>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="hidden sm:inline-flex items-center rounded-full border px-2.5 py-0.5 text-xs font-medium">
|
||||
FE {FRONTEND_VERSION_LABEL}
|
||||
</span>
|
||||
<span className="hidden sm:inline-flex items-center rounded-full border px-2.5 py-0.5 text-xs font-medium">
|
||||
BE {backendLabel}
|
||||
</span>
|
||||
{authLabel && (
|
||||
<Chip
|
||||
label={authLabel}
|
||||
variant="outlined"
|
||||
sx={{ maxWidth: { xs: "100%", sm: 220 } }}
|
||||
/>
|
||||
<span className="hidden md:inline-flex items-center rounded-full border px-2.5 py-0.5 text-xs font-medium max-w-[180px] truncate">
|
||||
{authLabel}
|
||||
</span>
|
||||
)}
|
||||
<Chip label={darkMode ? "Dark" : "Light"} variant="outlined" />
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
onClick={onToggleDarkMode}
|
||||
className="h-8 w-8"
|
||||
>
|
||||
{darkMode ? (
|
||||
<Sun className="h-4 w-4" />
|
||||
) : (
|
||||
<Moon className="h-4 w-4" />
|
||||
)}
|
||||
</Button>
|
||||
{onSignOut && (
|
||||
<Button
|
||||
variant="outlined"
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={onSignOut}
|
||||
sx={{ whiteSpace: "nowrap" }}
|
||||
className="gap-2"
|
||||
>
|
||||
Logout
|
||||
<LogOut className="h-4 w-4" />
|
||||
<span className="hidden sm:inline">Logout</span>
|
||||
</Button>
|
||||
)}
|
||||
</Stack>
|
||||
</Box>
|
||||
</div>
|
||||
</header>
|
||||
);
|
||||
}
|
||||
|
||||
<Box sx={{ borderTop: 1, borderColor: "divider" }}>
|
||||
<Tabs
|
||||
value={current}
|
||||
textColor="primary"
|
||||
indicatorColor="primary"
|
||||
variant={isMobile ? "scrollable" : "standard"}
|
||||
scrollButtons="auto"
|
||||
allowScrollButtonsMobile
|
||||
sx={{
|
||||
px: { xs: 0.75, sm: 1.5, md: 3 },
|
||||
minHeight: 44,
|
||||
"& .MuiTab-root": {
|
||||
minHeight: 44,
|
||||
py: 1,
|
||||
px: 1.5,
|
||||
minWidth: { xs: 88, md: 108 },
|
||||
textTransform: "none",
|
||||
fontWeight: 600,
|
||||
},
|
||||
}}
|
||||
function ShellLayout({
|
||||
darkMode,
|
||||
authLabel,
|
||||
onSignOut,
|
||||
onToggleDarkMode,
|
||||
}: {
|
||||
darkMode: boolean;
|
||||
authLabel?: string;
|
||||
onSignOut?: () => void;
|
||||
onToggleDarkMode: () => void;
|
||||
}) {
|
||||
const [sidebarCollapsed, setSidebarCollapsed] = useState(false);
|
||||
const [isMobile, setIsMobile] = useState(() =>
|
||||
window.matchMedia("(max-width: 768px)").matches,
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
const mql = window.matchMedia("(max-width: 768px)");
|
||||
const handler = (e: MediaQueryListEvent) => setIsMobile(e.matches);
|
||||
mql.addEventListener("change", handler);
|
||||
return () => mql.removeEventListener("change", handler);
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-background">
|
||||
<Sidebar
|
||||
collapsed={sidebarCollapsed}
|
||||
onToggle={() => setSidebarCollapsed(!sidebarCollapsed)}
|
||||
isMobile={isMobile}
|
||||
/>
|
||||
<div
|
||||
className={`transition-all duration-300 ${
|
||||
isMobile ? "" : sidebarCollapsed ? "ml-16" : "ml-60"
|
||||
}`}
|
||||
>
|
||||
<Tab value="/" label="Dashboard" component={NavLink} to="/" />
|
||||
<Tab
|
||||
value="/monitoring"
|
||||
label="Monitoring"
|
||||
component={NavLink}
|
||||
to="/monitoring"
|
||||
<TopBar
|
||||
darkMode={darkMode}
|
||||
authLabel={authLabel}
|
||||
onSignOut={onSignOut}
|
||||
onToggleDarkMode={onToggleDarkMode}
|
||||
/>
|
||||
<Tab
|
||||
value="/applications"
|
||||
label="Applications"
|
||||
component={NavLink}
|
||||
to="/applications"
|
||||
/>
|
||||
<Tab value="/users" label="Users" component={NavLink} to="/users" />
|
||||
<Tab
|
||||
value="/actions"
|
||||
label="Actions"
|
||||
component={NavLink}
|
||||
to="/actions"
|
||||
/>
|
||||
<Tab value="/files" label="Files" component={NavLink} to="/files" />
|
||||
<Tab
|
||||
value="/backups"
|
||||
label="Backups"
|
||||
component={NavLink}
|
||||
to="/backups"
|
||||
/>
|
||||
<Tab
|
||||
value="/settings"
|
||||
label="Settings"
|
||||
component={NavLink}
|
||||
to="/settings"
|
||||
/>
|
||||
</Tabs>
|
||||
</Box>
|
||||
</AppBar>
|
||||
<Container
|
||||
maxWidth={false}
|
||||
sx={{ py: { xs: 1.5, md: 2 }, px: { xs: 1.25, sm: 2, md: 2.5 } }}
|
||||
>
|
||||
<Routes>
|
||||
<Route path="/" element={<Dashboard />} />
|
||||
<Route path="/monitoring" element={<Monitoring />} />
|
||||
<Route path="/applications" element={<Applications />} />
|
||||
<Route path="/media" element={<Applications />} />
|
||||
<Route path="/users" element={<UsersPage />} />
|
||||
<Route path="/actions" element={<Actions />} />
|
||||
<Route path="/files" element={<FileBrowser />} />
|
||||
<Route path="/backups" element={<BackupsPage />} />
|
||||
<Route path="/settings" element={<Settings />} />
|
||||
</Routes>
|
||||
</Container>
|
||||
</>
|
||||
<main className="p-4 md:p-6">
|
||||
<Outlet />
|
||||
</main>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function LoadingScreen({ label }: { label: string }) {
|
||||
return (
|
||||
<Box
|
||||
sx={{
|
||||
minHeight: "100vh",
|
||||
display: "grid",
|
||||
placeItems: "center",
|
||||
p: 2,
|
||||
}}
|
||||
>
|
||||
<Card variant="outlined" sx={{ maxWidth: 420, width: "100%" }}>
|
||||
<CardContent>
|
||||
<Stack spacing={2} sx={{ alignItems: "center", textAlign: "center" }}>
|
||||
<CircularProgress />
|
||||
<Typography variant="h6">{label}</Typography>
|
||||
</Stack>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</Box>
|
||||
<div className="min-h-screen grid place-items-center p-4">
|
||||
<div className="w-full max-w-md rounded-xl border bg-card p-6 shadow-sm">
|
||||
<div className="flex flex-col items-center gap-3 text-center">
|
||||
<div className="h-8 w-8 animate-spin rounded-full border-4 border-primary border-t-transparent" />
|
||||
<h2 className="text-lg font-semibold">{label}</h2>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function SignInScreen({ onSignIn }: { onSignIn: () => void }) {
|
||||
return (
|
||||
<Box
|
||||
sx={{
|
||||
minHeight: "100vh",
|
||||
display: "grid",
|
||||
placeItems: "center",
|
||||
p: 2,
|
||||
}}
|
||||
>
|
||||
<Card variant="outlined" sx={{ maxWidth: 460, width: "100%" }}>
|
||||
<CardContent>
|
||||
<Stack spacing={2} sx={{ alignItems: "center", textAlign: "center" }}>
|
||||
<Typography variant="h5">Sign in required</Typography>
|
||||
<Typography variant="body2" color="text.secondary">
|
||||
<div className="min-h-screen grid place-items-center p-4">
|
||||
<div className="w-full max-w-md rounded-xl border bg-card p-6 shadow-sm">
|
||||
<div className="flex flex-col items-center gap-3 text-center">
|
||||
<h2 className="text-xl font-semibold">Sign in required</h2>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
Use your Authentik account to access Manage.
|
||||
</Typography>
|
||||
<Button variant="contained" onClick={onSignIn}>
|
||||
Sign in with OIDC
|
||||
</Button>
|
||||
</Stack>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</Box>
|
||||
</p>
|
||||
<Button onClick={onSignIn}>Sign in with OIDC</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function AuthenticatedApp({ darkMode }: { darkMode: boolean }) {
|
||||
function AuthenticatedApp() {
|
||||
const auth = useAuth();
|
||||
const [darkMode, setDarkMode] = useState(() =>
|
||||
window.matchMedia("(prefers-color-scheme: dark)").matches,
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
const mql = window.matchMedia("(prefers-color-scheme: dark)");
|
||||
const handler = (e: MediaQueryListEvent) => setDarkMode(e.matches);
|
||||
mql.addEventListener("change", handler);
|
||||
return () => mql.removeEventListener("change", handler);
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
setAccessToken(auth.user?.access_token ?? null);
|
||||
}, [auth.user?.access_token]);
|
||||
@@ -329,41 +405,74 @@ function AuthenticatedApp({ darkMode }: { darkMode: boolean }) {
|
||||
}
|
||||
|
||||
return (
|
||||
<Box sx={{ minHeight: "100vh", bgcolor: "background.default" }}>
|
||||
<BrowserRouter>
|
||||
<Shell
|
||||
<ShellLayout
|
||||
darkMode={darkMode}
|
||||
authLabel={authLabel}
|
||||
onSignOut={() => void auth.signoutRedirect()}
|
||||
onToggleDarkMode={() => setDarkMode(!darkMode)}
|
||||
/>
|
||||
</BrowserRouter>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
function AppInner() {
|
||||
const prefersDarkMode = useMediaQuery("(prefers-color-scheme: dark)");
|
||||
const theme = useMemo(
|
||||
() => getAppTheme(prefersDarkMode ? "dark" : "light"),
|
||||
[prefersDarkMode],
|
||||
const [prefersDarkMode, setPrefersDarkMode] = useState(() =>
|
||||
window.matchMedia("(prefers-color-scheme: dark)").matches,
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
const mql = window.matchMedia("(prefers-color-scheme: dark)");
|
||||
const handler = (e: MediaQueryListEvent) => setPrefersDarkMode(e.matches);
|
||||
mql.addEventListener("change", handler);
|
||||
return () => mql.removeEventListener("change", handler);
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<ThemeProvider theme={theme}>
|
||||
<QueryClientProvider client={queryClient}>
|
||||
{isOidcConfigured() ? (
|
||||
<AuthProvider {...getOidcConfig()}>
|
||||
<AuthenticatedApp darkMode={prefersDarkMode} />
|
||||
<BrowserRouter>
|
||||
<Routes>
|
||||
<Route element={<AuthenticatedApp />}>
|
||||
<Route path="/" element={<Dashboard />} />
|
||||
<Route path="/monitoring" element={<Monitoring />} />
|
||||
<Route path="/applications" element={<Applications />} />
|
||||
<Route path="/media" element={<Applications />} />
|
||||
<Route path="/users" element={<UsersPage />} />
|
||||
<Route path="/actions" element={<Actions />} />
|
||||
<Route path="/files" element={<FileBrowser />} />
|
||||
<Route path="/backups" element={<BackupsPage />} />
|
||||
<Route path="/settings" element={<Settings />} />
|
||||
</Route>
|
||||
</Routes>
|
||||
</BrowserRouter>
|
||||
</AuthProvider>
|
||||
) : (
|
||||
<Box sx={{ minHeight: "100vh", bgcolor: "background.default" }}>
|
||||
<BrowserRouter>
|
||||
<Shell darkMode={prefersDarkMode} />
|
||||
<Routes>
|
||||
<Route
|
||||
element={
|
||||
<ShellLayout
|
||||
darkMode={prefersDarkMode}
|
||||
onToggleDarkMode={() =>
|
||||
setPrefersDarkMode(!prefersDarkMode)
|
||||
}
|
||||
/>
|
||||
}
|
||||
>
|
||||
<Route path="/" element={<Dashboard />} />
|
||||
<Route path="/monitoring" element={<Monitoring />} />
|
||||
<Route path="/applications" element={<Applications />} />
|
||||
<Route path="/media" element={<Applications />} />
|
||||
<Route path="/users" element={<UsersPage />} />
|
||||
<Route path="/actions" element={<Actions />} />
|
||||
<Route path="/files" element={<FileBrowser />} />
|
||||
<Route path="/backups" element={<BackupsPage />} />
|
||||
<Route path="/settings" element={<Settings />} />
|
||||
</Route>
|
||||
</Routes>
|
||||
</BrowserRouter>
|
||||
</Box>
|
||||
)}
|
||||
</QueryClientProvider>
|
||||
</ThemeProvider>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,67 @@
|
||||
import * as React from "react"
|
||||
import { cva, type VariantProps } from "class-variance-authority"
|
||||
import { Slot } from "radix-ui"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
const buttonVariants = cva(
|
||||
"group/button inline-flex shrink-0 items-center justify-center rounded-lg border border-transparent bg-clip-padding text-sm font-medium whitespace-nowrap transition-all outline-none select-none focus-visible:border-ring focus-visible:ring-3 focus-visible:ring-ring/50 active:not-aria-[haspopup]:translate-y-px disabled:pointer-events-none disabled:opacity-50 aria-invalid:border-destructive aria-invalid:ring-3 aria-invalid:ring-destructive/20 dark:aria-invalid:border-destructive/50 dark:aria-invalid:ring-destructive/40 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
|
||||
{
|
||||
variants: {
|
||||
variant: {
|
||||
default: "bg-primary text-primary-foreground [a]:hover:bg-primary/80",
|
||||
outline:
|
||||
"border-border bg-background hover:bg-muted hover:text-foreground aria-expanded:bg-muted aria-expanded:text-foreground dark:border-input dark:bg-input/30 dark:hover:bg-input/50",
|
||||
secondary:
|
||||
"bg-secondary text-secondary-foreground hover:bg-secondary/80 aria-expanded:bg-secondary aria-expanded:text-secondary-foreground",
|
||||
ghost:
|
||||
"hover:bg-muted hover:text-foreground aria-expanded:bg-muted aria-expanded:text-foreground dark:hover:bg-muted/50",
|
||||
destructive:
|
||||
"bg-destructive/10 text-destructive hover:bg-destructive/20 focus-visible:border-destructive/40 focus-visible:ring-destructive/20 dark:bg-destructive/20 dark:hover:bg-destructive/30 dark:focus-visible:ring-destructive/40",
|
||||
link: "text-primary underline-offset-4 hover:underline",
|
||||
},
|
||||
size: {
|
||||
default:
|
||||
"h-8 gap-1.5 px-2.5 has-data-[icon=inline-end]:pr-2 has-data-[icon=inline-start]:pl-2",
|
||||
xs: "h-6 gap-1 rounded-[min(var(--radius-md),10px)] px-2 text-xs in-data-[slot=button-group]:rounded-lg has-data-[icon=inline-end]:pr-1.5 has-data-[icon=inline-start]:pl-1.5 [&_svg:not([class*='size-'])]:size-3",
|
||||
sm: "h-7 gap-1 rounded-[min(var(--radius-md),12px)] px-2.5 text-[0.8rem] in-data-[slot=button-group]:rounded-lg has-data-[icon=inline-end]:pr-1.5 has-data-[icon=inline-start]:pl-1.5 [&_svg:not([class*='size-'])]:size-3.5",
|
||||
lg: "h-9 gap-1.5 px-2.5 has-data-[icon=inline-end]:pr-2 has-data-[icon=inline-start]:pl-2",
|
||||
icon: "size-8",
|
||||
"icon-xs":
|
||||
"size-6 rounded-[min(var(--radius-md),10px)] in-data-[slot=button-group]:rounded-lg [&_svg:not([class*='size-'])]:size-3",
|
||||
"icon-sm":
|
||||
"size-7 rounded-[min(var(--radius-md),12px)] in-data-[slot=button-group]:rounded-lg",
|
||||
"icon-lg": "size-9",
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
variant: "default",
|
||||
size: "default",
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
function Button({
|
||||
className,
|
||||
variant = "default",
|
||||
size = "default",
|
||||
asChild = false,
|
||||
...props
|
||||
}: React.ComponentProps<"button"> &
|
||||
VariantProps<typeof buttonVariants> & {
|
||||
asChild?: boolean
|
||||
}) {
|
||||
const Comp = asChild ? Slot.Root : "button"
|
||||
|
||||
return (
|
||||
<Comp
|
||||
data-slot="button"
|
||||
data-variant={variant}
|
||||
data-size={size}
|
||||
className={cn(buttonVariants({ variant, size, className }))}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
export { Button }
|
||||
@@ -0,0 +1,33 @@
|
||||
"use client"
|
||||
|
||||
import { Collapsible as CollapsiblePrimitive } from "radix-ui"
|
||||
|
||||
function Collapsible({
|
||||
...props
|
||||
}: React.ComponentProps<typeof CollapsiblePrimitive.Root>) {
|
||||
return <CollapsiblePrimitive.Root data-slot="collapsible" {...props} />
|
||||
}
|
||||
|
||||
function CollapsibleTrigger({
|
||||
...props
|
||||
}: React.ComponentProps<typeof CollapsiblePrimitive.CollapsibleTrigger>) {
|
||||
return (
|
||||
<CollapsiblePrimitive.CollapsibleTrigger
|
||||
data-slot="collapsible-trigger"
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function CollapsibleContent({
|
||||
...props
|
||||
}: React.ComponentProps<typeof CollapsiblePrimitive.CollapsibleContent>) {
|
||||
return (
|
||||
<CollapsiblePrimitive.CollapsibleContent
|
||||
data-slot="collapsible-content"
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
export { Collapsible, CollapsibleTrigger, CollapsibleContent }
|
||||
@@ -0,0 +1,145 @@
|
||||
import * as React from "react"
|
||||
import { Dialog as SheetPrimitive } from "radix-ui"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
import { Button } from "@/components/ui/button"
|
||||
import { XIcon } from "lucide-react"
|
||||
|
||||
function Sheet({ ...props }: React.ComponentProps<typeof SheetPrimitive.Root>) {
|
||||
return <SheetPrimitive.Root data-slot="sheet" {...props} />
|
||||
}
|
||||
|
||||
function SheetTrigger({
|
||||
...props
|
||||
}: React.ComponentProps<typeof SheetPrimitive.Trigger>) {
|
||||
return <SheetPrimitive.Trigger data-slot="sheet-trigger" {...props} />
|
||||
}
|
||||
|
||||
function SheetClose({
|
||||
...props
|
||||
}: React.ComponentProps<typeof SheetPrimitive.Close>) {
|
||||
return <SheetPrimitive.Close data-slot="sheet-close" {...props} />
|
||||
}
|
||||
|
||||
function SheetPortal({
|
||||
...props
|
||||
}: React.ComponentProps<typeof SheetPrimitive.Portal>) {
|
||||
return <SheetPrimitive.Portal data-slot="sheet-portal" {...props} />
|
||||
}
|
||||
|
||||
function SheetOverlay({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof SheetPrimitive.Overlay>) {
|
||||
return (
|
||||
<SheetPrimitive.Overlay
|
||||
data-slot="sheet-overlay"
|
||||
className={cn(
|
||||
"fixed inset-0 z-50 bg-black/10 duration-100 supports-backdrop-filter:backdrop-blur-xs data-open:animate-in data-open:fade-in-0 data-closed:animate-out data-closed:fade-out-0",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function SheetContent({
|
||||
className,
|
||||
children,
|
||||
side = "right",
|
||||
showCloseButton = true,
|
||||
...props
|
||||
}: React.ComponentProps<typeof SheetPrimitive.Content> & {
|
||||
side?: "top" | "right" | "bottom" | "left"
|
||||
showCloseButton?: boolean
|
||||
}) {
|
||||
return (
|
||||
<SheetPortal>
|
||||
<SheetOverlay />
|
||||
<SheetPrimitive.Content
|
||||
data-slot="sheet-content"
|
||||
data-side={side}
|
||||
className={cn(
|
||||
"fixed z-50 flex flex-col gap-4 bg-popover bg-clip-padding text-sm text-popover-foreground shadow-lg transition duration-200 ease-in-out data-[side=bottom]:inset-x-0 data-[side=bottom]:bottom-0 data-[side=bottom]:h-auto data-[side=bottom]:border-t data-[side=left]:inset-y-0 data-[side=left]:left-0 data-[side=left]:h-full data-[side=left]:w-3/4 data-[side=left]:border-r data-[side=right]:inset-y-0 data-[side=right]:right-0 data-[side=right]:h-full data-[side=right]:w-3/4 data-[side=right]:border-l data-[side=top]:inset-x-0 data-[side=top]:top-0 data-[side=top]:h-auto data-[side=top]:border-b data-[side=left]:sm:max-w-sm data-[side=right]:sm:max-w-sm data-open:animate-in data-open:fade-in-0 data-[side=bottom]:data-open:slide-in-from-bottom-10 data-[side=left]:data-open:slide-in-from-left-10 data-[side=right]:data-open:slide-in-from-right-10 data-[side=top]:data-open:slide-in-from-top-10 data-closed:animate-out data-closed:fade-out-0 data-[side=bottom]:data-closed:slide-out-to-bottom-10 data-[side=left]:data-closed:slide-out-to-left-10 data-[side=right]:data-closed:slide-out-to-right-10 data-[side=top]:data-closed:slide-out-to-top-10",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
{showCloseButton && (
|
||||
<SheetPrimitive.Close data-slot="sheet-close" asChild>
|
||||
<Button
|
||||
variant="ghost"
|
||||
className="absolute top-3 right-3"
|
||||
size="icon-sm"
|
||||
>
|
||||
<XIcon
|
||||
/>
|
||||
<span className="sr-only">Close</span>
|
||||
</Button>
|
||||
</SheetPrimitive.Close>
|
||||
)}
|
||||
</SheetPrimitive.Content>
|
||||
</SheetPortal>
|
||||
)
|
||||
}
|
||||
|
||||
function SheetHeader({ className, ...props }: React.ComponentProps<"div">) {
|
||||
return (
|
||||
<div
|
||||
data-slot="sheet-header"
|
||||
className={cn("flex flex-col gap-0.5 p-4", className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function SheetFooter({ className, ...props }: React.ComponentProps<"div">) {
|
||||
return (
|
||||
<div
|
||||
data-slot="sheet-footer"
|
||||
className={cn("mt-auto flex flex-col gap-2 p-4", className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function SheetTitle({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof SheetPrimitive.Title>) {
|
||||
return (
|
||||
<SheetPrimitive.Title
|
||||
data-slot="sheet-title"
|
||||
className={cn(
|
||||
"font-heading text-base font-medium text-foreground",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function SheetDescription({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof SheetPrimitive.Description>) {
|
||||
return (
|
||||
<SheetPrimitive.Description
|
||||
data-slot="sheet-description"
|
||||
className={cn("text-sm text-muted-foreground", className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
export {
|
||||
Sheet,
|
||||
SheetTrigger,
|
||||
SheetClose,
|
||||
SheetContent,
|
||||
SheetHeader,
|
||||
SheetFooter,
|
||||
SheetTitle,
|
||||
SheetDescription,
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
import * as React from "react"
|
||||
import { Tooltip as TooltipPrimitive } from "radix-ui"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
function TooltipProvider({
|
||||
delayDuration = 0,
|
||||
...props
|
||||
}: React.ComponentProps<typeof TooltipPrimitive.Provider>) {
|
||||
return (
|
||||
<TooltipPrimitive.Provider
|
||||
data-slot="tooltip-provider"
|
||||
delayDuration={delayDuration}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function Tooltip({
|
||||
...props
|
||||
}: React.ComponentProps<typeof TooltipPrimitive.Root>) {
|
||||
return <TooltipPrimitive.Root data-slot="tooltip" {...props} />
|
||||
}
|
||||
|
||||
function TooltipTrigger({
|
||||
...props
|
||||
}: React.ComponentProps<typeof TooltipPrimitive.Trigger>) {
|
||||
return <TooltipPrimitive.Trigger data-slot="tooltip-trigger" {...props} />
|
||||
}
|
||||
|
||||
function TooltipContent({
|
||||
className,
|
||||
sideOffset = 0,
|
||||
children,
|
||||
...props
|
||||
}: React.ComponentProps<typeof TooltipPrimitive.Content>) {
|
||||
return (
|
||||
<TooltipPrimitive.Portal>
|
||||
<TooltipPrimitive.Content
|
||||
data-slot="tooltip-content"
|
||||
sideOffset={sideOffset}
|
||||
className={cn(
|
||||
"z-50 inline-flex w-fit max-w-xs origin-(--radix-tooltip-content-transform-origin) items-center gap-1.5 rounded-md bg-foreground px-3 py-1.5 text-xs text-background has-data-[slot=kbd]:pr-1.5 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 **:data-[slot=kbd]:relative **:data-[slot=kbd]:isolate **:data-[slot=kbd]:z-50 **:data-[slot=kbd]:rounded-sm data-[state=delayed-open]:animate-in data-[state=delayed-open]:fade-in-0 data-[state=delayed-open]:zoom-in-95 data-open:animate-in data-open:fade-in-0 data-open:zoom-in-95 data-closed:animate-out data-closed:fade-out-0 data-closed:zoom-out-95",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
<TooltipPrimitive.Arrow className="z-50 size-2.5 translate-y-[calc(-50%_-_2px)] rotate-45 rounded-[2px] bg-foreground fill-foreground" />
|
||||
</TooltipPrimitive.Content>
|
||||
</TooltipPrimitive.Portal>
|
||||
)
|
||||
}
|
||||
|
||||
export { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger }
|
||||
@@ -1,3 +1,52 @@
|
||||
@import "tailwindcss";
|
||||
@import url('https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&display=swap');
|
||||
|
||||
@theme {
|
||||
--font-sans: 'Inter', ui-sans-serif, system-ui, sans-serif;
|
||||
|
||||
--color-background: #fafafa;
|
||||
--color-foreground: #0f172a;
|
||||
--color-card: #ffffff;
|
||||
--color-card-foreground: #0f172a;
|
||||
--color-popover: #ffffff;
|
||||
--color-popover-foreground: #0f172a;
|
||||
--color-primary: #4f8cff;
|
||||
--color-primary-foreground: #ffffff;
|
||||
--color-secondary: #f1f5f9;
|
||||
--color-secondary-foreground: #0f172a;
|
||||
--color-muted: #f1f5f9;
|
||||
--color-muted-foreground: #64748b;
|
||||
--color-accent: #f1f5f9;
|
||||
--color-accent-foreground: #0f172a;
|
||||
--color-destructive: #ef4444;
|
||||
--color-destructive-foreground: #ffffff;
|
||||
--color-border: #e2e8f0;
|
||||
--color-input: #e2e8f0;
|
||||
--color-ring: #4f8cff;
|
||||
--color-chart-1: #4f8cff;
|
||||
--color-chart-2: #22c55e;
|
||||
--color-chart-3: #f59e0b;
|
||||
--color-chart-4: #ef4444;
|
||||
--color-chart-5: #8b5cf6;
|
||||
--radius: 0.625rem;
|
||||
--color-sidebar-background: #fafafa;
|
||||
--color-sidebar-foreground: #0f172a;
|
||||
--color-sidebar-primary: #4f8cff;
|
||||
--color-sidebar-primary-foreground: #ffffff;
|
||||
--color-sidebar-accent: #f1f5f9;
|
||||
--color-sidebar-accent-foreground: #0f172a;
|
||||
--color-sidebar-border: #e2e8f0;
|
||||
--color-sidebar-ring: #4f8cff;
|
||||
}
|
||||
|
||||
@layer base {
|
||||
body {
|
||||
background-color: var(--color-background);
|
||||
color: var(--color-foreground);
|
||||
font-family: var(--font-sans);
|
||||
}
|
||||
}
|
||||
|
||||
html,
|
||||
body,
|
||||
#root {
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
import { type ClassValue, clsx } from "clsx";
|
||||
import { twMerge } from "tailwind-merge";
|
||||
|
||||
export function cn(...inputs: ClassValue[]) {
|
||||
return twMerge(clsx(inputs));
|
||||
}
|
||||
+6
-42
@@ -1,43 +1,7 @@
|
||||
import { createTheme, type PaletteMode } from "@mui/material/styles";
|
||||
|
||||
export function getAppTheme(mode: PaletteMode) {
|
||||
const isDark = mode === "dark";
|
||||
return createTheme({
|
||||
palette: {
|
||||
mode,
|
||||
primary: { main: "#4f8cff" },
|
||||
background: {
|
||||
default: isDark ? "#0f172a" : "#f4f6fb",
|
||||
paper: isDark ? "#111827" : "#ffffff",
|
||||
},
|
||||
},
|
||||
shape: { borderRadius: 12 },
|
||||
typography: {
|
||||
fontFamily:
|
||||
'Inter, system-ui, -apple-system, "Segoe UI", Roboto, sans-serif',
|
||||
h5: { fontWeight: 700 },
|
||||
},
|
||||
components: {
|
||||
MuiAppBar: {
|
||||
styleOverrides: {
|
||||
root: {
|
||||
backdropFilter: "blur(8px)",
|
||||
backgroundImage: "none",
|
||||
},
|
||||
},
|
||||
},
|
||||
MuiCard: {
|
||||
styleOverrides: {
|
||||
root: {
|
||||
borderColor: isDark
|
||||
? "rgba(148,163,184,0.25)"
|
||||
: "rgba(15,23,42,0.08)",
|
||||
boxShadow: isDark
|
||||
? "0 2px 12px rgba(2,6,23,0.35)"
|
||||
: "0 2px 8px rgba(15,23,42,0.04)",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
export function getAppTheme(mode: "light" | "dark") {
|
||||
void mode;
|
||||
// Theme is now handled by Tailwind CSS + CSS variables in index.css
|
||||
// This function is kept as a no-op shim for backward compatibility
|
||||
// during the MUI → Tailwind migration.
|
||||
return {} as unknown;
|
||||
}
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
module.exports = { content: ["./src/**/*.{html,js,ts,jsx,tsx}"], theme: { extend: {} }, plugins: [] }
|
||||
@@ -19,7 +19,13 @@
|
||||
"noUnusedLocals": true,
|
||||
"noUnusedParameters": true,
|
||||
"erasableSyntaxOnly": true,
|
||||
"noFallthroughCasesInSwitch": true
|
||||
"noFallthroughCasesInSwitch": true,
|
||||
|
||||
"baseUrl": ".",
|
||||
"paths": {
|
||||
"@/*": ["./src/*"]
|
||||
},
|
||||
"ignoreDeprecations": "6.0"
|
||||
},
|
||||
"include": ["src"]
|
||||
}
|
||||
|
||||
@@ -18,7 +18,9 @@
|
||||
"noUnusedLocals": true,
|
||||
"noUnusedParameters": true,
|
||||
"erasableSyntaxOnly": true,
|
||||
"noFallthroughCasesInSwitch": true
|
||||
"noFallthroughCasesInSwitch": true,
|
||||
"ignoreDeprecations": "6.0"
|
||||
},
|
||||
"include": ["vite.config.ts"]
|
||||
"include": ["vite.config.ts"],
|
||||
"exclude": ["node_modules"]
|
||||
}
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
import { defineConfig, loadEnv } from "vite";
|
||||
import path from "path";
|
||||
import { defineConfig, loadEnv, type Plugin } from "vite";
|
||||
import react from "@vitejs/plugin-react";
|
||||
import tailwindcss from "@tailwindcss/vite";
|
||||
|
||||
export default defineConfig(({ mode }) => {
|
||||
const env = loadEnv(mode, process.cwd(), "");
|
||||
@@ -20,7 +22,7 @@ export default defineConfig(({ mode }) => {
|
||||
__APP_VERSION__: JSON.stringify(appVersion),
|
||||
__APP_BUILD_INFO__: JSON.stringify(appBuildInfo),
|
||||
},
|
||||
plugins: [react()],
|
||||
plugins: [react(), ...(tailwindcss() as Plugin[])],
|
||||
server: {
|
||||
proxy: {
|
||||
"/api": {
|
||||
@@ -30,5 +32,10 @@ export default defineConfig(({ mode }) => {
|
||||
},
|
||||
},
|
||||
},
|
||||
resolve: {
|
||||
alias: {
|
||||
"@": path.resolve(__dirname, "./src"),
|
||||
},
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user