website refactor

This commit is contained in:
2026-01-20 00:10:30 +01:00
parent 92bf97e21a
commit 6df1b50536
14 changed files with 511 additions and 351 deletions

View File

@@ -141,9 +141,9 @@
@layer utilities {
/* Precision Racing Utilities */
.glass-panel {
background: rgba(20, 22, 25, 0.7);
backdrop-filter: blur(12px);
border: 1px solid var(--color-outline);
background: rgba(20, 22, 25, 0.85);
backdrop-filter: blur(16px);
border-bottom: 1px solid var(--color-outline);
}
.subtle-gradient {
@@ -161,7 +161,7 @@
left: 0;
width: 100%;
height: 1px;
background: linear-gradient(90deg, var(--color-primary) 0%, transparent 100%);
background: linear-gradient(90deg, transparent 0%, var(--color-primary) 50%, transparent 100%);
opacity: 0.5;
}

View File

@@ -21,7 +21,6 @@ export function AdminToolbar({
return (
<ControlBar
leftContent={leftContent}
variant="dark"
>
{children}
</ControlBar>

View File

@@ -1,44 +1,72 @@
'use client';
import { Surface } from '@/ui/Surface';
import { Stack } from '@/ui/Stack';
import { Text } from '@/ui/Text';
import { Box } from '@/ui/Box';
import { Text } from '@/ui/Text';
import { Link } from '@/ui/Link';
import { AppShellBar } from './AppShellBar';
import { Activity, Database, Server, Wifi } from 'lucide-react';
export function AppFooter() {
return (
<Surface
as="footer"
variant="precision"
paddingY={6}
paddingX={6}
borderTop
backgroundColor="rgba(10, 10, 11, 0.92)"
className="backdrop-blur-xl"
style={{
boxShadow: '0 -1px 0 0 rgba(255, 255, 255, 0.05)',
}}
>
<Box display="flex" justifyContent="between" alignItems="center" width="full" gap={4}>
<Stack direction="row" align="center" gap={3}>
<Text size="xs" variant="low" font="mono" uppercase letterSpacing="0.12em">
© {new Date().getFullYear()} GridPilot
</Text>
<Box w="px" h="3" bg="var(--ui-color-border-muted)" opacity={0.6} />
<Text size="xs" variant="low" font="mono" uppercase letterSpacing="0.12em">
Session ready
</Text>
</Stack>
const currentYear = new Date().getFullYear();
<Box display={{ base: 'none', sm: 'flex' }}>
<Stack direction="row" align="center" gap={2}>
<Box w="1.5" h="1.5" rounded="full" bg="var(--ui-color-intent-success)" />
<Text size="xs" variant="low" font="mono" uppercase letterSpacing="0.12em">
System Normal
return (
<AppShellBar position="bottom">
{/* Left: System Info */}
<Box display="flex" alignItems="center" gap={6}>
<Box display="flex" alignItems="center" gap={2}>
<Box className="w-2 h-2 bg-success-green rounded-full animate-pulse" />
<Text size="xs" className="text-text-med font-mono text-[10px] uppercase tracking-wider">
GRIDPILOT OS v2.0
</Text>
</Stack>
</Box>
<Text size="xs" className="text-text-low font-mono text-[10px] uppercase tracking-wider hidden md:block">
© {currentYear}
</Text>
</Box>
{/* Center: Telemetry Status */}
<Box display="flex" alignItems="center" gap={6} className="hidden md:flex">
<StatusIndicator icon={Database} label="DB" status="good" />
<StatusIndicator icon={Server} label="API" status="good" />
<StatusIndicator icon={Wifi} label="WS" status="good" />
<Box className="h-3 w-px bg-outline-steel" />
<Box display="flex" alignItems="center" gap={2}>
<Activity size={12} className="text-text-low" />
<Text size="xs" className="text-text-med font-mono text-[10px]">12ms</Text>
</Box>
</Box>
</Surface>
{/* Right: Legal & Tools */}
<Box display="flex" alignItems="center" gap={2}>
<FooterLink href="/terms">Terms</FooterLink>
<FooterLink href="/privacy">Privacy</FooterLink>
<FooterLink href="/status">Status</FooterLink>
</Box>
</AppShellBar>
);
}
function StatusIndicator({ icon: Icon, label, status }: { icon: any, label: string, status: 'good' | 'warn' | 'bad' }) {
const color = status === 'good' ? 'text-success-green' : status === 'warn' ? 'text-warning-amber' : 'text-critical-red';
return (
<Box display="flex" alignItems="center" gap={2} title={`${label}: ${status.toUpperCase()}`}>
<Icon size={10} className={color} />
<Text size="xs" className="text-text-low font-mono text-[10px] uppercase tracking-wider">
{label}
</Text>
</Box>
);
}
function FooterLink({ href, children }: { href: string, children: React.ReactNode }) {
return (
<Link
href={href}
variant="ghost"
className="px-2 py-1 rounded hover:bg-white/5 text-text-low hover:text-text-high font-mono text-[10px] uppercase tracking-wider transition-colors"
>
{children}
</Link>
);
}

View File

@@ -1,72 +1,119 @@
'use client';
import { BrandMark } from '@/ui/BrandMark';
import { HeaderActions } from '@/components/layout/HeaderActions';
import { PublicNav } from '@/components/layout/PublicNav';
import { Box } from '@/ui/Box';
import { Text } from '@/ui/Text';
import { Search, Bell, User, ChevronDown, Command } from 'lucide-react';
import { usePathname } from 'next/navigation';
import { useCurrentSession } from '@/hooks/auth/useCurrentSession';
import { routes } from '@/lib/routing/RouteConfig';
import { Box } from '@/ui/Box';
import { Stack } from '@/ui/Stack';
import { Text } from '@/ui/Text';
import { Surface } from '@/ui/Surface';
import { usePathname } from 'next/navigation';
import { Button } from '@/ui/Button';
import { useState, useEffect } from 'react';
import { AppShellBar } from './AppShellBar';
export function AppHeader() {
const pathname = usePathname();
const { data: session } = useCurrentSession();
const isAuthenticated = !!session;
const homeHref = isAuthenticated ? routes.protected.dashboard : routes.public.home;
// Simple breadcrumb logic
const pathSegments = pathname.split('/').filter(Boolean);
const breadcrumbs = pathSegments.length > 0
? pathSegments.map(s => s.charAt(0).toUpperCase() + s.slice(1)).join(' / ')
: 'Home';
// Clock
const [time, setTime] = useState<string>('');
useEffect(() => {
const updateTime = () => {
const now = new Date();
setTime(now.toLocaleTimeString('en-US', { hour: '2-digit', minute: '2-digit', hour12: false }));
};
updateTime();
const interval = setInterval(updateTime, 60000);
return () => clearInterval(interval);
}, []);
return (
<Surface
as="header"
variant="dark"
height="14"
zIndex={50}
borderBottom
backgroundColor="rgba(13, 13, 14, 0.8)"
className="backdrop-blur-xl"
>
<Box display="flex" alignItems="center" justifyContent="between" width="full" h="full" px={6}>
{/* Left: Brand & Context */}
<Stack direction="row" align="center" gap={4} h="full">
<BrandMark href={homeHref} priority />
<AppShellBar position="top">
{/* Left: Context & Search */}
<Box display="flex" alignItems="center" gap={6} flex={1}>
<Text size="sm" className="text-text-med font-medium tracking-wide whitespace-nowrap min-w-[100px]">
{breadcrumbs}
</Text>
{isAuthenticated && (
{/* Command Search - Refined */}
<Box
display="flex"
alignItems="center"
gap={3}
paddingLeft={4}
borderLeft
h="6"
borderColor="var(--ui-color-border-muted)"
className="hidden md:flex items-center gap-3 px-3 h-9 bg-surface-charcoal/50 border border-outline-steel rounded-md w-96 text-text-low hover:border-text-low/50 focus-within:border-primary-accent focus-within:ring-1 focus-within:ring-primary-accent/20 transition-all cursor-text group"
>
<Text size="xs" weight="medium" variant="low" font="mono" uppercase>
Workspace
</Text>
<Search size={14} className="text-text-low group-hover:text-text-med transition-colors" />
<Box
as="input"
type="text"
placeholder="Search or type a command..."
className="bg-transparent border-none outline-none text-sm w-full text-text-high placeholder:text-text-low/50 h-full"
/>
<Box className="flex items-center gap-1 px-1.5 py-0.5 rounded bg-white/5 border border-white/5 text-[10px] font-mono text-text-low">
<Command size={10} />
<span>K</span>
</Box>
</Box>
)}
</Stack>
{/* Center: Navigation (if public) */}
{!isAuthenticated && (
<Box display={{ base: 'none', md: 'flex' }} data-testid="public-top-nav">
<PublicNav pathname={pathname} direction="row" />
</Box>
)}
{/* Right: Session Controls */}
{/* Right: System Status & User */}
<Box display="flex" alignItems="center" gap={4}>
<Box display={{ base: 'none', sm: 'flex' }} alignItems="center" gap={2}>
<Box w="1.5" h="1.5" rounded="full" bg="var(--ui-color-intent-success)" />
<Text size="xs" variant="low" weight="bold" font="mono" letterSpacing="0.1em">
LIVE
{/* System Time */}
<Box className="hidden lg:flex items-center gap-2 text-text-med border-r border-outline-steel pr-4 h-6">
<Text size="sm" font="mono" className="tracking-widest tabular-nums">
{time} UTC
</Text>
</Box>
<HeaderActions isAuthenticated={isAuthenticated} />
{/* Notifications */}
<Box
as="button"
className="w-8 h-8 flex items-center justify-center text-text-low hover:text-text-high hover:bg-white/5 transition-colors rounded-md relative"
title="Notifications"
>
<Bell size={16} />
<Box className="absolute top-2 right-2 w-1.5 h-1.5 bg-primary-accent rounded-full ring-2 ring-base-black" />
</Box>
{/* User Menu */}
{isAuthenticated ? (
<Box display="flex" alignItems="center" gap={3} className="pl-2 cursor-pointer group">
<Box
className="w-8 h-8 rounded-full bg-surface-charcoal flex items-center justify-center text-text-med border border-outline-steel group-hover:border-primary-accent transition-colors"
>
<User size={14} />
</Box>
</Surface>
<Box className="hidden md:block text-left">
<Text size="sm" weight="medium" className="text-text-high leading-none group-hover:text-primary-accent transition-colors">
{session.user.displayName || 'Driver'}
</Text>
</Box>
<ChevronDown size={12} className="text-text-low group-hover:text-text-high transition-colors" />
</Box>
) : (
<Box display="flex" gap={2}>
<Button
as="a"
href={routes.auth.login}
variant="ghost"
size="sm"
>
Sign In
</Button>
<Button
as="a"
href={routes.auth.signup}
variant="primary"
size="sm"
>
Join
</Button>
</Box>
)}
</Box>
</AppShellBar>
);
}

View File

@@ -0,0 +1,34 @@
'use client';
import { Box } from '@/ui/Box';
import { ReactNode } from 'react';
interface AppShellBarProps {
position: 'top' | 'bottom';
children: ReactNode;
sidebarOffset?: boolean;
}
export function AppShellBar({ position, children, sidebarOffset = true }: AppShellBarProps) {
const isTop = position === 'top';
return (
<Box
as={isTop ? 'header' : 'footer'}
className={`
fixed ${isTop ? 'top-0' : 'bottom-0'} right-0 z-40
h-14
bg-base-black/70 backdrop-blur-xl
border-${isTop ? 'b' : 't'} border-outline-steel
flex items-center justify-between px-6
transition-all duration-200
`}
// Use style for responsive left offset to ensure it works
// We use a CSS variable or calc if possible, but here we rely on Tailwind classes via className if we could
// But since we need responsive logic that matches Layout, we'll use the Box props
left={sidebarOffset ? { base: 0, lg: 64 } : 0}
>
{children}
</Box>
);
}

View File

@@ -1,50 +1,54 @@
'use client';
import { AuthedNav } from '@/components/layout/AuthedNav';
import { PublicNav } from '@/components/layout/PublicNav';
import { useCurrentSession } from '@/hooks/auth/useCurrentSession';
import { BrandMark } from '@/ui/BrandMark';
import { NavLink } from '@/ui/NavLink';
import { routes } from '@/lib/routing/RouteConfig';
import { Box } from '@/ui/Box';
import { DashboardRail } from '@/components/dashboard/DashboardRail';
import { Text } from '@/ui/Text';
import { Surface } from '@/ui/Surface';
import { Stack } from '@/ui/Stack';
import {
LayoutGrid,
Trophy,
Users,
Calendar,
Flag,
Home
} from 'lucide-react';
import { usePathname } from 'next/navigation';
export function AppSidebar() {
const pathname = usePathname();
const { data: session } = useCurrentSession();
const isAuthenticated = !!session;
const navItems = [
{ label: 'Home', href: routes.public.home, icon: Home },
{ label: 'Leagues', href: routes.public.leagues, icon: Trophy },
{ label: 'Drivers', href: routes.public.drivers, icon: Users },
{ label: 'Leaderboards', href: routes.public.leaderboards, icon: LayoutGrid },
{ label: 'Teams', href: routes.public.teams, icon: Flag },
{ label: 'Races', href: routes.public.races, icon: Calendar },
];
return (
<Surface
as="aside"
variant="dark"
width="64"
h="full"
borderRight
backgroundColor="linear-gradient(180deg, #0d0d0e 0%, #0a0a0b 100%)"
style={{
boxShadow: 'inset -1px 0 0 0 rgba(255, 255, 255, 0.01)',
}}
>
<DashboardRail>
<Box py={8} fullWidth>
<Box px={6} mb={10}>
<Box display="flex" alignItems="center" gap={2} mb={2}>
<Box w="0.5" h="3" bg="var(--ui-color-intent-primary)" />
<Text size="xs" variant="low" weight="bold" font="mono" letterSpacing="0.2em">
DASHBOARD
</Text>
<Box display="flex" flexDirection="col" height="full" className="bg-base-black border-r border-outline-steel">
{/* Brand Header */}
<Box p={6} pb={8}>
<BrandMark />
</Box>
{/* Navigation */}
<Box flex={1} px={2} className="overflow-y-auto">
<Stack gap={1}>
{navItems.map((item) => (
<NavLink
key={item.href}
href={item.href}
label={item.label}
icon={item.icon}
isActive={pathname === item.href}
variant="sidebar"
/>
))}
</Stack>
</Box>
</Box>
<Box px={4}>
{isAuthenticated ? (
<AuthedNav pathname={pathname} />
) : (
<PublicNav pathname={pathname} />
)}
</Box>
</Box>
</DashboardRail>
</Surface>
);
}

View File

@@ -1,45 +1,49 @@
import { routes } from '@/lib/routing/RouteConfig';
import { Button } from '@/ui/Button';
import { Stack } from '@/ui/Stack';
import { LogIn, UserPlus } from 'lucide-react';
import { Box } from '@/ui/Box';
interface HeaderActionsProps {
isAuthenticated: boolean;
}
/**
* HeaderActions provides the primary actions in the header (Login, Signup, Profile).
*/
export function HeaderActions({ isAuthenticated }: HeaderActionsProps) {
if (isAuthenticated) {
return (
<Stack direction="row" gap={3}>
<Button as="a" href={routes.protected.profile} variant="secondary" size="sm">
<Button
as="a"
href={routes.protected.profile}
variant="ghost"
size="sm"
className="text-text-med hover:text-text-high transition-colors duration-200"
>
Profile
</Button>
</Stack>
);
}
return (
<Stack direction="row" gap={3}>
<Stack direction="row" gap={4} align="center">
<Button
as="a"
href={routes.auth.login}
variant="ghost"
size="sm"
icon={<LogIn size={16} />}
data-testid="public-nav-login"
className="text-text-med hover:text-text-high transition-colors duration-200"
>
Login
Sign In
</Button>
<Box className="w-px h-4 bg-outline-steel" />
<Button
as="a"
href={routes.auth.signup}
variant="primary"
size="sm"
icon={<UserPlus size={16} />}
data-testid="public-nav-signup"
className="px-6 font-medium tracking-wide"
>
Sign Up
</Button>

View File

@@ -1,6 +1,6 @@
import { routes } from '@/lib/routing/RouteConfig';
import { Stack } from '@/ui/Stack';
import { Calendar, Home, Layout, Trophy, Users } from 'lucide-react';
import { Calendar, Home, LayoutGrid, Trophy, Users, Flag } from 'lucide-react';
import { NavLink } from '@/ui/NavLink';
interface PublicNavProps {
@@ -8,21 +8,18 @@ interface PublicNavProps {
direction?: 'row' | 'col';
}
/**
* PublicNav displays navigation items for unauthenticated users.
*/
export function PublicNav({ pathname, direction = 'col' }: PublicNavProps) {
const items = [
{ label: 'Home', href: routes.public.home, icon: Home },
{ label: 'Leagues', href: routes.public.leagues, icon: Trophy },
{ label: 'Drivers', href: routes.public.drivers, icon: Users },
{ label: 'Leaderboards', href: routes.public.leaderboards, icon: Layout },
{ label: 'Teams', href: routes.public.teams, icon: Users },
{ label: 'Leaderboards', href: routes.public.leaderboards, icon: LayoutGrid },
{ label: 'Teams', href: routes.public.teams, icon: Flag },
{ label: 'Races', href: routes.public.races, icon: Calendar },
];
return (
<Stack direction={direction} gap={direction === 'row' ? 4 : 1}>
<Stack direction={direction} gap={direction === 'row' ? 1 : 1}>
{items.map((item) => (
<NavLink
key={item.href}

View File

@@ -3,11 +3,8 @@
import { AppFooter } from '@/components/layout/AppFooter';
import { AppHeader } from '@/components/layout/AppHeader';
import { AppSidebar } from '@/components/layout/AppSidebar';
import { useCurrentSession } from '@/hooks/auth/useCurrentSession';
import { routes } from '@/lib/routing/RouteConfig';
import { Layout } from '@/ui/Layout';
import { Container } from '@/ui/Container';
import { usePathname } from 'next/navigation';
import { Box } from '@/ui/Box';
import React from 'react';
export interface RootAppShellViewData {
@@ -16,31 +13,26 @@ export interface RootAppShellViewData {
/**
* RootAppShellTemplate orchestrates the top-level semantic shells of the application.
* It uses the canonical ui/Layout component to define the app frame.
* Redesigned for the "Cockpit" layout.
*/
export function RootAppShellTemplate({ children }: RootAppShellViewData) {
const pathname = usePathname();
const { data: session } = useCurrentSession();
const isAuthenticated = !!session;
// Hide sidebar on landing page for unauthenticated users
const isLandingPage = pathname === routes.public.home;
const showSidebar = isAuthenticated && !isLandingPage;
return (
<Layout
header={<AppHeader />}
sidebar={showSidebar ? <AppSidebar /> : undefined}
sidebar={<AppSidebar />}
footer={<AppFooter />}
fixedHeader
fixedSidebar
fixedHeader={true}
fixedSidebar={true}
fixedFooter={false}
>
<Container
size={isLandingPage ? 'full' : 'xl'}
py={isLandingPage ? 0 : 8}
<Box
width="full"
className="max-w-[1920px] mx-auto"
px={8}
py={8}
>
{children}
</Container>
</Box>
</Layout>
);
}

View File

@@ -0,0 +1,2 @@
components/admin/AdminToolbar.tsx(24,7): error TS2322: Type '{ children: ReactNode; leftContent: ReactNode; variant: string; }' is not assignable to type 'IntrinsicAttributes & ControlBarProps'.
Property 'variant' does not exist on type 'IntrinsicAttributes & ControlBarProps'.

View File

@@ -7,29 +7,20 @@ interface BrandMarkProps {
priority?: boolean;
}
/**
* BrandMark provides the consistent logo/wordmark for the application.
* Aligned with "Precision Racing Minimal" theme.
*/
export function BrandMark({ href = '/' }: BrandMarkProps) {
return (
<Link href={href} variant="inherit" underline="none">
<Box position="relative" display="inline-flex" alignItems="center">
<Box height={{ base: '1.5rem', md: '1.75rem' }} style={{ transition: 'opacity 0.2s' }}>
<Box display="flex" alignItems="center" gap={2}>
<Image
src="/images/logos/square-logo-dark.svg"
alt=""
style={{ height: '1.5rem', width: 'auto' }}
/>
<Image
src="/images/logos/wordmark-rectangle-dark.svg"
alt="GridPilot"
style={{ height: '100%', width: 'auto', display: 'block' }}
/>
</Box>
<Box
position="absolute"
bottom="-4px"
left="0"
width="0"
height="2px"
bg="var(--ui-color-intent-primary)"
style={{ transition: 'width 0.2s' }}
style={{ height: '1.125rem', width: 'auto' }}
/>
</Box>
</Link>

View File

@@ -6,79 +6,72 @@ export interface LayoutProps {
header?: ReactNode;
footer?: ReactNode;
sidebar?: ReactNode;
/**
* Whether the sidebar should be fixed to the side.
* If true, the main content will be offset by the sidebar width.
*/
fixedSidebar?: boolean;
/**
* Whether the header should be fixed to the top.
* If true, the main content will be offset by the header height.
*/
fixedHeader?: boolean;
fixedFooter?: boolean;
}
/**
* Layout is the canonical app frame component.
* It orchestrates the high-level structure: Header, Sidebar, Main Content, and Footer.
* Redesigned for "Cockpit" layout: Sidebar is primary (full height), Header and Content sit to the right.
*/
export const Layout = ({
children,
header,
footer,
sidebar,
fixedSidebar = false,
fixedHeader = false
fixedSidebar = true,
fixedHeader = true,
fixedFooter = true // Default to true for AppShellBar
}: LayoutProps) => {
return (
<Box display="flex" flexDirection="col" minHeight="100vh">
{header && (
<Box
as="header"
position={fixedHeader ? "fixed" : "relative"}
top={fixedHeader ? 0 : undefined}
left={fixedHeader ? 0 : undefined}
right={fixedHeader ? 0 : undefined}
zIndex={50}
>
{header}
</Box>
)}
<Box display="flex" flex={1} marginTop={fixedHeader ? 14 : undefined}>
<Box display="flex" minHeight="100vh" className="bg-base-black text-text-high">
{/* Sidebar - Primary Vertical Axis - Solid Background */}
{sidebar && (
<Box
as="aside"
width="64"
width="64" // 16rem / 256px
display={{ base: 'none', lg: 'block' }}
position={fixedSidebar ? "fixed" : "relative"}
top={fixedSidebar ? (fixedHeader ? 14 : 0) : undefined}
bottom={fixedSidebar ? 0 : undefined}
left={fixedSidebar ? 0 : undefined}
zIndex={40}
top={0}
bottom={0}
left={0}
zIndex={50}
className="bg-base-black border-r border-outline-steel"
>
{sidebar}
</Box>
)}
{/* Main Content Area - Right of Sidebar */}
<Box
display="flex"
flexDirection="col"
flex={1}
marginLeft={fixedSidebar && sidebar ? { lg: 64 } : undefined}
minWidth="0" // Prevent flex child overflow
>
{/* Header - Rendered directly as it contains AppShellBar (fixed) */}
{header}
{/* Main Scrollable Content */}
<Box
as="main"
flex={1}
display="flex"
flexDirection="col"
marginLeft={fixedSidebar ? { lg: 64 } : undefined}
position="relative"
marginTop={fixedHeader ? 14 : 0} // Offset for fixed header (h-14)
paddingBottom={fixedFooter ? 14 : 0} // Offset for fixed footer (h-14)
>
<Box flex={1}>
<Box flex={1} p={0}>
{children}
</Box>
</Box>
{footer && (
<Box as="footer">
{/* Footer - Rendered directly as it contains AppShellBar (fixed) */}
{footer}
</Box>
)}
</Box>
</Box>
</Box>
);
};

View File

@@ -12,25 +12,56 @@ interface NavLinkProps {
variant?: 'sidebar' | 'top';
}
/**
* NavLink provides a consistent link component for navigation.
* Supports both sidebar and top navigation variants.
*/
export function NavLink({ href, label, icon, isActive, variant = 'sidebar' }: NavLinkProps) {
const isTop = variant === 'top';
// Dieter Rams style: Unobtrusive, Honest, Thorough.
// No glows. No shadows. Just clear contrast and alignment.
const content = (
<Box display="flex" alignItems="center" gap={variant === 'top' ? 2 : 3} paddingX={3} paddingY={2} rounded={variant === 'sidebar' ? 'md' : undefined} style={{ transition: 'all 0.2s' }}>
{icon && <Icon icon={icon} size={variant === 'top' ? 4 : 5} intent={isActive ? 'primary' : 'low'} />}
<Text size="sm" weight={isActive ? 'bold' : 'medium'} variant={isActive ? 'primary' : 'med'}>
<Box
display="flex"
alignItems="center"
gap={3}
paddingX={isTop ? 4 : 4}
paddingY={isTop ? 2 : 3}
className={`
relative group transition-all duration-200 ease-out
${isActive
? 'text-text-high bg-white/5'
: 'text-text-med hover:text-text-high hover:bg-white/5'
}
${!isTop && 'rounded-md mx-2'}
`}
>
{icon && (
<Icon
icon={icon}
size={4}
className={`transition-colors duration-200 ${isActive ? 'text-primary-accent' : 'text-text-low group-hover:text-text-med'}`}
/>
)}
<Text
size="sm"
weight={isActive ? 'medium' : 'normal'}
variant="inherit"
className="tracking-wide"
>
{label}
</Text>
{variant === 'sidebar' && isActive && (
<Box marginLeft="auto" width="4px" height="1rem" bg="var(--ui-color-intent-primary)" rounded="full" />
{/* Minimal Active Indicator */}
{!isTop && isActive && (
<Box
className="absolute left-0 top-1/2 -translate-y-1/2 w-1 h-4 bg-primary-accent rounded-r-full"
/>
)}
</Box>
);
return (
<Link href={href} variant="inherit" underline="none" block={variant === 'sidebar'}>
<Link href={href} variant="inherit" underline="none" block={!isTop}>
{content}
</Link>
);

View File

@@ -1,134 +1,172 @@
🎨 GridPilot Theme — “Precision Racing Minimal”
GridPilot Theme — “Modern Precision with Obsessive Detail”
A clean, modern racing interface that feels like a cockpit dashboard — calm, sharp, and serious.
A meticulously crafted sim-racing interface built with absolute love to detail.
1. Identity & Mood
Core Essence
GridPilot should feel like:
• a real motorsport instrument, not a game launcher
• calm and focused, like the moment before a qualifying lap
• precise, like a telemetry screen
• modern and minimal, without visual noise
• slightly futuristic, but never “RGB gamer chaos”
GridPilot should feel like a product where every pixel was placed on purpose.
A UI that radiates care, intention, and craft — the way a beautifully machined steering wheel or pedal set feels when you touch it.
It should appeal equally to sim racers, gamers, and casual fans.
Not loud.
Not flashy.
Not corporate.
But modern, obsessive, refined.
The user should think:
“Someone really cared when they designed this.”
2. Visual Style
1. The Kind of Modern We Want
Core Aesthetic
• matte dark surfaces
thin, crisp separators
• soft blue/cyan glows on interaction
no aggressive neon
subtle gradients for depth
everything feels instrument-grade, not decorative
Modern ≠ trendy
GridPilot is modern in the way high-end hardware is modern:
clean materials
• subtle gradients
precision shadows
invisible order
absolute visual consistency
Color System
• Graphite Black — base background
• Charcoal — panel surfaces
• Steel Grey — separators & outlines
• Electric Blue — primary actions
• Telemetry Aqua — interactive highlights
• Motorsport Amber — warnings & signals
It feels engineered, not styled.
Colors should feel like motorsport, not corporate SaaS.
Everything is smooth, but never busy.
Everything is polished, but never glossy.
Everything is minimal, but never empty.
It has the vibe of:
• Apples focus
• Porsches restraint
• Moza/Fanatecs precision hardware aesthetics
• Telemetry dashboards
• Calm esports production graphics
A blend of racing seriousness and premium digital craft.
3. Components & Interaction
2. Love to Detail (the signature of GridPilot)
Panels & Cards
• slightly inset or raised
• reminiscent of cockpit modules
• structured and clean
This is where GridPilot stands out.
Tables
• information-dense
• instantly scannable
• light hover highlights
• telemetry-style status colors
Microspacing
Buttons
• flat by default
• glow only on hover
• snappy, “race-engineer” response speed
Spacing is not “good enough” — its perfectly balanced.
Margins breathe.
Rows align with intent.
Nothing floats randomly.
Modals
• soft frosted blur
• fast open/close
• subtle pit-lane lighting vibes
Typography finesse
Numbers align sharply for standings.
Headings sit exactly on their baseline rhythm.
Secondary metadata uses softer brightness and tighter spacing.
4. Motion & Feedback
Shadow discipline
Motion should feel racing-inspired:
• short, crisp animations
• no bounce or playful movement
• hover = slight lift + color pulse
• loading = a thin progress line (pit limiter style)
• tab switching = sliding underline (chicane motion)
No random shadows — only a small, soft, controlled spread.
Layer depth is subtle but unmistakably refined.
Responsive, not playful.
Color temperature
Dark modes often feel muddy or flat.
GridPilot should feel crisp, layered, and tuned —
as if the entire palette has been color-graded like broadcast graphics.
5. Layout Structure
Interactive texture
Think Telemetry Workspace:
Sidebar → control rail
Header → context + session controls
Main Area → race tables, session details, track maps
• Right Panel → contextual info, drivers, actions
Everything modular, readable, and effortless to navigate.
6. Tone & Copywriting
The product tone is:
• calm
• concise
• direct
• technical but human
• zero hype
Examples:
• “Race added.”
• “Standings updated.”
• “Session ready.”
• “Review protest.”
• “Sponsor payout queued.”
Never corporate. Never salesy. Never loud.
7. Emotional Experience
Users should feel:
• in control
• supported
• efficient
• connected to motorsport culture
• confident in the system
A disciplined, trustworthy tool — not a flashy app.
8. Overall Vibe
“A premium cockpit dashboard — for people who actually race.”
• minimal
Every hover, highlight, or focus state is:
soft
smooth
fast
• precise
• clean
• serious
• attractive without noise
A tool youre happy to use, because it respects your time and your craft.
Nothing is bouncy, glowy, or game-y.
But everything feels alive when touched.
Love to detail = you dont notice it, but you feel it.
3. Emotional Atmosphere
The moment the UI loads, the user should feel:
• This is premium.
• This was crafted, not slapped together.
• This tool respects my time.
• This belongs to sim racing, not generic SaaS.
• This is serious but not sterile.
Its that mixture of calm confidence and quiet intensity that sim racers crave before entering a session.
4. Visual Style Summary (crisp and modern)
Surfaces
• matte
• low contrast
• ultra-clean edges
• subtle gradients only on interaction
Accents
• electric blue or aqua, used sparingly
• amber for warnings (motorsport heritage)
• desaturated greys for hierarchy
Lighting
• no neon
• no RGB vibes
• subtle glow only on very important interactive elements
Depth
• controlled layering
• meaningful shadows
• frosted blur for modals (garage glass vibe)
5. Motion
Animations reflect precision:
• fast acceleration
• quick settle
• no jitter
• no wasted frames
Transitions should feel like:
• opening a garage screen
• switching telemetry pages
• selecting a gear cleanly
A sense of mechanical smoothness.
6. Why This Theme Works for Our Audience
For gamers:
• visually immersive
• polished like a AAA menu
• subtle effects that feel “alive”
For sim racers:
• data clarity
• racing-inspired accents
• tool-like seriousness
For devs:
• clean structure
• intentional simplicity
• maintainable aesthetic rules
This theme hits the sweet spot:
professional, modern, crafted, premium.
One-Line Summary
GridPilot should feel like a high-end sim racing cockpit UI — obsessively detailed, modern, calm, and deeply cared for, where every interaction feels engineered with precision.