feat(ui): introduce Control Rail design system and shell
This commit is contained in:
@@ -1,19 +1,34 @@
|
||||
import { useState } from "react";
|
||||
import { NavLink, Outlet, useNavigate } from "react-router-dom";
|
||||
import { useAuth } from "../context/AuthContext";
|
||||
import { BrandMark, Icon, type IconName } from "./Icons";
|
||||
|
||||
const NAV_ITEMS = [
|
||||
{ to: "/dashboard", label: "Dashboard" },
|
||||
{ to: "/vehicles", label: "Vehicles" },
|
||||
{ to: "/bookings", label: "Bookings" },
|
||||
{ to: "/data-quality", label: "Data Quality" },
|
||||
{ to: "/knowledge", label: "Knowledge" },
|
||||
{ to: "/automation", label: "Automation" },
|
||||
{ to: "/audit", label: "Audit" },
|
||||
const NAV_GROUPS: Array<{ label: string; items: Array<{ to: string; label: string; shortLabel: string; icon: IconName }> }> = [
|
||||
{
|
||||
label: "Operate",
|
||||
items: [
|
||||
{ to: "/dashboard", label: "Overview", shortLabel: "Overview", icon: "activity" },
|
||||
{ to: "/vehicles", label: "Fleet", shortLabel: "Fleet", icon: "fleet" },
|
||||
{ to: "/bookings", label: "Bookings", shortLabel: "Bookings", icon: "bookings" },
|
||||
{ to: "/data-quality", label: "Data quality", shortLabel: "Quality", icon: "quality" },
|
||||
],
|
||||
},
|
||||
{
|
||||
label: "Assure",
|
||||
items: [
|
||||
{ to: "/knowledge", label: "Knowledge", shortLabel: "Knowledge", icon: "knowledge" },
|
||||
{ to: "/automation", label: "Integrations", shortLabel: "Systems", icon: "integrations" },
|
||||
{ to: "/audit", label: "Audit trail", shortLabel: "Audit", icon: "audit" },
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
const MOBILE_ITEMS = NAV_GROUPS.flatMap((group) => group.items).slice(0, 5);
|
||||
|
||||
export function Layout() {
|
||||
const { user, logout } = useAuth();
|
||||
const navigate = useNavigate();
|
||||
const [mobileOpen, setMobileOpen] = useState(false);
|
||||
|
||||
function handleLogout() {
|
||||
logout();
|
||||
@@ -22,38 +37,80 @@ export function Layout() {
|
||||
|
||||
return (
|
||||
<div className="app-shell">
|
||||
<p className="demo-banner">
|
||||
Synthetic demo environment — no real customer or vehicle data.
|
||||
</p>
|
||||
<header className="app-header">
|
||||
<span className="brand">MobilityOps</span>
|
||||
<nav aria-label="Primary">
|
||||
<ul>
|
||||
{NAV_ITEMS.map((item) => (
|
||||
<li key={item.to}>
|
||||
<NavLink to={item.to} className={({ isActive }) => (isActive ? "active" : "")}>
|
||||
{item.label}
|
||||
</NavLink>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</nav>
|
||||
<div className="user-badge">
|
||||
{user && (
|
||||
<>
|
||||
<span>
|
||||
{user.display_name} · {user.role === "operations_manager" ? "Operations Manager" : "Rental Employee"}
|
||||
</span>
|
||||
<button type="button" onClick={handleLogout}>
|
||||
Switch role
|
||||
</button>
|
||||
</>
|
||||
)}
|
||||
<a className="skip-link" href="#main-content">Skip to main content</a>
|
||||
|
||||
<aside className={`sidebar ${mobileOpen ? "is-open" : ""}`}>
|
||||
<div className="brand-lockup">
|
||||
<BrandMark className="brand-mark" />
|
||||
<div><strong>MobilityOps</strong><span>Control centre</span></div>
|
||||
</div>
|
||||
</header>
|
||||
<main id="main-content">
|
||||
<Outlet />
|
||||
</main>
|
||||
<nav aria-label="Primary navigation">
|
||||
{NAV_GROUPS.map((group) => (
|
||||
<div className="nav-group" key={group.label}>
|
||||
<p>{group.label}</p>
|
||||
<ul>
|
||||
{group.items.map((item) => (
|
||||
<li key={item.to}>
|
||||
<NavLink to={item.to} onClick={() => setMobileOpen(false)}>
|
||||
<Icon name={item.icon} />
|
||||
<span>{item.label}</span>
|
||||
</NavLink>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
))}
|
||||
</nav>
|
||||
<div className="sidebar-foot">
|
||||
<span className="environment-dot" />
|
||||
<div><strong>Demo environment</strong><span>Synthetic data only</span></div>
|
||||
</div>
|
||||
</aside>
|
||||
|
||||
{mobileOpen && <button className="nav-scrim" aria-label="Close navigation" onClick={() => setMobileOpen(false)} />}
|
||||
|
||||
<div className="app-workspace">
|
||||
<header className="topbar">
|
||||
<button className="icon-button mobile-menu" type="button" onClick={() => setMobileOpen(true)} aria-label="Open navigation">
|
||||
<Icon name="menu" />
|
||||
</button>
|
||||
<label className="global-search">
|
||||
<Icon name="search" />
|
||||
<span className="visually-hidden">Search MobilityOps</span>
|
||||
<input type="search" placeholder="Search fleet, booking or customer…" />
|
||||
<kbd>Ctrl K</kbd>
|
||||
</label>
|
||||
<div className="topbar-meta">
|
||||
<span className="timezone"><Icon name="clock" /> Europe/Brussels</span>
|
||||
{user && (
|
||||
<div className="operator">
|
||||
<span className="avatar">{user.display_name.split(" ").map((name) => name[0]).join("").slice(0, 2)}</span>
|
||||
<span><strong>{user.display_name}</strong><small>{user.role === "operations_manager" ? "Operations manager" : "Rental employee"}</small></span>
|
||||
</div>
|
||||
)}
|
||||
<button className="icon-button" type="button" onClick={handleLogout} aria-label="Switch role" title="Switch demo role">
|
||||
<Icon name="logout" />
|
||||
</button>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<p className="demo-banner"><Icon name="shield" /> Synthetic demo data · no real customer or vehicle information</p>
|
||||
<main id="main-content" tabIndex={-1}><Outlet /></main>
|
||||
<footer className="app-footer"><span>MobilityOps PoC</span><span>Europe/Brussels · Synthetic demo data</span></footer>
|
||||
</div>
|
||||
|
||||
<nav className="mobile-nav" aria-label="Mobile navigation">
|
||||
{MOBILE_ITEMS.map((item) => (
|
||||
<NavLink key={item.to} to={item.to}>
|
||||
<Icon name={item.icon} />
|
||||
<span>{item.shortLabel}</span>
|
||||
</NavLink>
|
||||
))}
|
||||
<button type="button" onClick={() => setMobileOpen(true)}>
|
||||
<Icon name="menu" />
|
||||
<span>More</span>
|
||||
</button>
|
||||
</nav>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user