website refactor
This commit is contained in:
@@ -57,6 +57,7 @@
|
||||
color: var(--color-text-high);
|
||||
line-height: 1.5;
|
||||
overscroll-behavior: none;
|
||||
min-height: 100vh;
|
||||
}
|
||||
|
||||
::selection {
|
||||
|
||||
@@ -75,11 +75,9 @@ export default async function RootLayout({
|
||||
<head>
|
||||
<meta name="mobile-web-app-capable" content="yes" />
|
||||
</head>
|
||||
<body className="antialiased overflow-x-hidden">
|
||||
<body className="antialiased overflow-hidden h-screen">
|
||||
<AppWrapper enabledFlags={enabledFlags}>
|
||||
<RootAppShellTemplate>
|
||||
{children}
|
||||
</RootAppShellTemplate>
|
||||
<RootAppShellTemplate>{children}</RootAppShellTemplate>
|
||||
</AppWrapper>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -27,8 +27,8 @@ export function AppWrapper({ children, enabledFlags }: AppWrapperProps) {
|
||||
<NotificationIntegration />
|
||||
<EnhancedErrorBoundary enableDevOverlay={process.env.NODE_ENV === 'development'}>
|
||||
{children}
|
||||
{process.env.NODE_ENV === 'development' && <DevToolbar />}
|
||||
</EnhancedErrorBoundary>
|
||||
{process.env.NODE_ENV === 'development' && <DevToolbar />}
|
||||
</NotificationProvider>
|
||||
</FeatureFlagProvider>
|
||||
</AuthProvider>
|
||||
|
||||
@@ -7,18 +7,24 @@ interface AppShellProps {
|
||||
|
||||
/**
|
||||
* AppShell is the root container for the entire application layout.
|
||||
* It provides the base background and layout structure.
|
||||
* Provides the base structure with cockpit-inspired design.
|
||||
*/
|
||||
export function AppShell({ children }: AppShellProps) {
|
||||
return (
|
||||
<Box
|
||||
minHeight="100vh"
|
||||
bg="#0C0D0F"
|
||||
color="var(--ui-color-text-high)"
|
||||
display="flex"
|
||||
<Box
|
||||
height="100vh"
|
||||
bg="var(--ui-color-bg-base)"
|
||||
color="var(--ui-color-text-high)"
|
||||
display="flex"
|
||||
flexDirection="col"
|
||||
overflowY="auto"
|
||||
overflowX="hidden"
|
||||
style={{
|
||||
fontFamily: 'var(--ui-font-sans)',
|
||||
background: 'linear-gradient(180deg, #0a0a0b 0%, #0f0f10 100%)',
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -18,21 +18,16 @@ interface DashboardShellProps {
|
||||
*/
|
||||
export function DashboardShell({ children, rail, controlBar }: DashboardShellProps) {
|
||||
return (
|
||||
<Box display="flex" height="100vh" style={{ overflow: 'hidden', backgroundColor: 'var(--ui-color-bg-base)' }}>
|
||||
{rail && (
|
||||
<Sidebar>
|
||||
{rail}
|
||||
</Sidebar>
|
||||
)}
|
||||
<Box display="flex" flexDirection="col" flex={1} style={{ overflow: 'hidden' }}>
|
||||
{controlBar && (
|
||||
<Header>
|
||||
{controlBar}
|
||||
</Header>
|
||||
)}
|
||||
<MainContent maxWidth="7xl">
|
||||
{children}
|
||||
</MainContent>
|
||||
<Box
|
||||
display="flex"
|
||||
minHeight="100vh"
|
||||
style={{ backgroundColor: 'var(--ui-color-bg-base)' }}
|
||||
>
|
||||
{rail && <Sidebar>{rail}</Sidebar>}
|
||||
|
||||
<Box display="flex" flexDirection="col" flex={1} minWidth="0">
|
||||
{controlBar && <Header>{controlBar}</Header>}
|
||||
<MainContent maxWidth="7xl">{children}</MainContent>
|
||||
</Box>
|
||||
</Box>
|
||||
);
|
||||
|
||||
@@ -229,7 +229,7 @@ export function DevToolbar() {
|
||||
|
||||
if (isMinimized) {
|
||||
return (
|
||||
<Stack align="end" justify="end">
|
||||
<Stack position="fixed" right={4} bottom={4} zIndex={1000}>
|
||||
<IconButton
|
||||
icon={Wrench}
|
||||
onClick={() => setIsMinimized(false)}
|
||||
@@ -242,7 +242,26 @@ export function DevToolbar() {
|
||||
}
|
||||
|
||||
return (
|
||||
<Stack gap={4}>
|
||||
<Stack
|
||||
position="fixed"
|
||||
right={4}
|
||||
bottom={4}
|
||||
zIndex={1000}
|
||||
width="min(420px, calc(100vw - 2rem))"
|
||||
maxHeight="calc(100vh - 2rem)"
|
||||
overflow="auto"
|
||||
border={true}
|
||||
borderColor="var(--ui-color-border-default)"
|
||||
rounded="xl"
|
||||
bg="rgba(20, 22, 25, 0.92)"
|
||||
padding={3}
|
||||
style={{
|
||||
boxShadow: '0 18px 40px rgba(0,0,0,0.55)',
|
||||
backdropFilter: 'blur(12px)',
|
||||
WebkitBackdropFilter: 'blur(12px)',
|
||||
}}
|
||||
gap={4}
|
||||
>
|
||||
{/* Header */}
|
||||
<Stack direction="row" align="center" justify="between" gap={4}>
|
||||
<Stack direction="row" align="center" gap={2}>
|
||||
|
||||
44
apps/website/components/layout/AppFooter.tsx
Normal file
44
apps/website/components/layout/AppFooter.tsx
Normal file
@@ -0,0 +1,44 @@
|
||||
'use client';
|
||||
|
||||
import { Surface } from '@/ui/Surface';
|
||||
import { Stack } from '@/ui/Stack';
|
||||
import { Text } from '@/ui/Text';
|
||||
import { Box } from '@/ui/Box';
|
||||
|
||||
export function AppFooter() {
|
||||
return (
|
||||
<Surface
|
||||
as="footer"
|
||||
variant="precision"
|
||||
paddingY={6}
|
||||
paddingX={6}
|
||||
borderTop={true}
|
||||
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="1px" h="12px" bg="var(--ui-color-border-muted)" opacity={0.6} />
|
||||
<Text size="xs" variant="low" font="mono" uppercase letterSpacing="0.12em">
|
||||
Session ready
|
||||
</Text>
|
||||
</Stack>
|
||||
|
||||
<Box display={{ base: 'none', sm: 'flex' }}>
|
||||
<Stack direction="row" align="center" gap={2}>
|
||||
<Box w="6px" h="6px" rounded="full" bg="var(--ui-color-intent-success)" />
|
||||
<Text size="xs" variant="low" font="mono" uppercase letterSpacing="0.12em">
|
||||
System Normal
|
||||
</Text>
|
||||
</Stack>
|
||||
</Box>
|
||||
</Box>
|
||||
</Surface>
|
||||
);
|
||||
}
|
||||
78
apps/website/components/layout/AppHeader.tsx
Normal file
78
apps/website/components/layout/AppHeader.tsx
Normal file
@@ -0,0 +1,78 @@
|
||||
'use client';
|
||||
|
||||
import { BrandMark } from '@/ui/BrandMark';
|
||||
import { HeaderActions } from '@/components/layout/HeaderActions';
|
||||
import { PublicNav } from '@/components/layout/PublicNav';
|
||||
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';
|
||||
|
||||
export function AppHeader() {
|
||||
const pathname = usePathname();
|
||||
const { data: session } = useCurrentSession();
|
||||
const isAuthenticated = !!session;
|
||||
const homeHref = isAuthenticated ? routes.protected.dashboard : routes.public.home;
|
||||
|
||||
return (
|
||||
<Surface
|
||||
as="header"
|
||||
variant="dark"
|
||||
position="fixed"
|
||||
top={0}
|
||||
left={0}
|
||||
right={0}
|
||||
height="56px"
|
||||
zIndex={50}
|
||||
style={{
|
||||
borderBottom: '1px solid var(--ui-color-border-default)',
|
||||
background: 'rgba(13, 13, 14, 0.8)',
|
||||
backdropFilter: 'blur(12px)',
|
||||
}}
|
||||
>
|
||||
<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 />
|
||||
|
||||
{isAuthenticated && (
|
||||
<Box
|
||||
display="flex"
|
||||
alignItems="center"
|
||||
gap={3}
|
||||
paddingLeft={4}
|
||||
borderLeft={true}
|
||||
h="24px"
|
||||
style={{ borderLeftColor: 'var(--ui-color-border-muted)' }}
|
||||
>
|
||||
<Text size="xs" weight="medium" variant="low" font="mono" uppercase>
|
||||
Workspace
|
||||
</Text>
|
||||
</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 */}
|
||||
<Box display="flex" alignItems="center" gap={4}>
|
||||
<Box display={{ base: 'none', sm: 'flex' }} alignItems="center" gap={2}>
|
||||
<Box w="6px" h="6px" rounded="full" bg="var(--ui-color-intent-success)" />
|
||||
<Text size="xs" variant="low" weight="bold" font="mono" letterSpacing="0.1em">
|
||||
LIVE
|
||||
</Text>
|
||||
</Box>
|
||||
<HeaderActions isAuthenticated={isAuthenticated} />
|
||||
</Box>
|
||||
</Box>
|
||||
</Surface>
|
||||
);
|
||||
}
|
||||
54
apps/website/components/layout/AppSidebar.tsx
Normal file
54
apps/website/components/layout/AppSidebar.tsx
Normal file
@@ -0,0 +1,54 @@
|
||||
'use client';
|
||||
|
||||
import { AuthedNav } from '@/components/layout/AuthedNav';
|
||||
import { PublicNav } from '@/components/layout/PublicNav';
|
||||
import { useCurrentSession } from '@/hooks/auth/useCurrentSession';
|
||||
import { Box } from '@/ui/Box';
|
||||
import { DashboardRail } from '@/components/dashboard/DashboardRail';
|
||||
import { Text } from '@/ui/Text';
|
||||
import { Surface } from '@/ui/Surface';
|
||||
import { usePathname } from 'next/navigation';
|
||||
|
||||
export function AppSidebar() {
|
||||
const pathname = usePathname();
|
||||
const { data: session } = useCurrentSession();
|
||||
const isAuthenticated = !!session;
|
||||
|
||||
return (
|
||||
<Surface
|
||||
as="aside"
|
||||
variant="dark"
|
||||
width="260px"
|
||||
position="fixed"
|
||||
top="56px"
|
||||
bottom={0}
|
||||
left={0}
|
||||
zIndex={40}
|
||||
style={{
|
||||
borderRight: '1px solid var(--ui-color-border-default)',
|
||||
boxShadow: 'inset -1px 0 0 0 rgba(255, 255, 255, 0.01)',
|
||||
background: 'linear-gradient(180deg, #0d0d0e 0%, #0a0a0b 100%)',
|
||||
}}
|
||||
>
|
||||
<DashboardRail>
|
||||
<Box py={8} fullWidth>
|
||||
<Box px={6} mb={10}>
|
||||
<Box display="flex" alignItems="center" gap={2} mb={2}>
|
||||
<Box w="2px" h="12px" bg="var(--ui-color-intent-primary)" />
|
||||
<Text size="xs" variant="low" weight="bold" font="mono" letterSpacing="0.2em">
|
||||
DASHBOARD
|
||||
</Text>
|
||||
</Box>
|
||||
</Box>
|
||||
<Box px={4}>
|
||||
{isAuthenticated ? (
|
||||
<AuthedNav pathname={pathname} />
|
||||
) : (
|
||||
<PublicNav pathname={pathname} />
|
||||
)}
|
||||
</Box>
|
||||
</Box>
|
||||
</DashboardRail>
|
||||
</Surface>
|
||||
);
|
||||
}
|
||||
54
apps/website/components/layout/MainContent.tsx
Normal file
54
apps/website/components/layout/MainContent.tsx
Normal file
@@ -0,0 +1,54 @@
|
||||
'use client';
|
||||
|
||||
import { Box } from '@/ui/Box';
|
||||
import { ReactNode } from 'react';
|
||||
|
||||
interface MainContentProps {
|
||||
children: ReactNode;
|
||||
hasSidebar: boolean;
|
||||
}
|
||||
|
||||
export function MainContent({ children, hasSidebar }: MainContentProps) {
|
||||
return (
|
||||
<Box
|
||||
as="main"
|
||||
display="flex"
|
||||
flexDirection="col"
|
||||
flexGrow={1}
|
||||
style={{
|
||||
paddingTop: '56px', // Header height
|
||||
marginLeft: hasSidebar ? '260px' : '0',
|
||||
transition: 'margin-left 0.2s ease-in-out',
|
||||
}}
|
||||
>
|
||||
<Box
|
||||
position="relative"
|
||||
width="full"
|
||||
maxWidth="full"
|
||||
flexGrow={1}
|
||||
display="flex"
|
||||
flexDirection="col"
|
||||
>
|
||||
{/* Background Grid */}
|
||||
<Box
|
||||
position="absolute"
|
||||
inset={0}
|
||||
pointerEvents="none"
|
||||
zIndex={0}
|
||||
style={{
|
||||
backgroundImage:
|
||||
'radial-gradient(circle at 2px 2px, rgba(255,255,255,0.018) 1px, transparent 0)',
|
||||
backgroundSize: '32px 32px',
|
||||
opacity: 0.5,
|
||||
backgroundAttachment: 'fixed',
|
||||
}}
|
||||
/>
|
||||
|
||||
{/* Content */}
|
||||
<Box position="relative" zIndex={1} flexGrow={1} display="flex" flexDirection="col">
|
||||
{children}
|
||||
</Box>
|
||||
</Box>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
153
apps/website/docs/LAYOUT_RECREATION_SUMMARY.md
Normal file
153
apps/website/docs/LAYOUT_RECREATION_SUMMARY.md
Normal file
@@ -0,0 +1,153 @@
|
||||
# Layout Recreation Summary
|
||||
|
||||
## Overview
|
||||
Completely recreated the application layout following the "Precision Racing Minimal" design philosophy from `docs/THEME.md`.
|
||||
|
||||
## Issues Fixed
|
||||
|
||||
### 1. Content Alignment
|
||||
- **Issue**: Content was left-aligned instead of centered
|
||||
- **Fix**: Added `alignItems="center"` to outer Box in ContentViewport
|
||||
- **Result**: Content is now properly centered with `marginX="auto"`
|
||||
|
||||
### 2. Dev Toolbar Positioning
|
||||
- **Issue**: Dev toolbar appearing below footer
|
||||
- **Fix**: Wrapped footer in Box with `position="relative"` and `zIndex={50}`
|
||||
- **Result**: Footer stays at bottom, dev toolbar floats above
|
||||
|
||||
### 3. Scroll Containment
|
||||
- **Issue**: Nested scrollbars and improper scroll behavior
|
||||
- **Fix**: Proper `overflow="hidden"` on parent, `overflowY="auto"` on content
|
||||
- **Result**: Single scroll viewport, clean scrolling experience
|
||||
|
||||
## Key Changes
|
||||
|
||||
### 1. AppShell (`components/app/AppShell.tsx`)
|
||||
- **Before**: Basic flex container with background
|
||||
- **After**:
|
||||
- Added `overflow="hidden"` to prevent body scroll issues
|
||||
- Applied cockpit-inspired gradient background
|
||||
- Ensures proper layout containment
|
||||
|
||||
### 2. ControlBar (`ui/ControlBar.tsx`)
|
||||
- **Before**: 48px height, basic styling
|
||||
- **After**:
|
||||
- Increased height to 56px for better visual presence
|
||||
- Enhanced backdrop blur from `backdrop-blur-md` to `backdrop-blur-xl`
|
||||
- Added subtle shadow for depth
|
||||
- Added padding (px={6}) for better spacing
|
||||
- Improved visual hierarchy with darker background (rgba(10, 10, 11, 0.95))
|
||||
|
||||
### 3. HeaderContentTemplate (`templates/layout/HeaderContentTemplate.tsx`)
|
||||
- **Before**: Basic header layout
|
||||
- **After**:
|
||||
- Added horizontal padding (px={6}) for consistent spacing
|
||||
- Enhanced border styling with explicit color
|
||||
- Maintains clean, cockpit-inspired navigation
|
||||
|
||||
### 4. ContentViewport (`ui/ContentViewport.tsx`)
|
||||
- **Before**:
|
||||
- Content area had its own scroll bars
|
||||
- Left-aligned content
|
||||
- Improper scroll containment
|
||||
- **After**:
|
||||
- **Fixed scroll containment**: Proper `overflow="hidden"` on parent, `overflowY="auto"` on content
|
||||
- **Proper alignment**: Centered content with `marginX="auto"` and `maxWidth`
|
||||
- **Clean scrolling**: Single scroll viewport, no nested scrollbars
|
||||
- **Responsive**: Maintains `fullWidth` option for specific pages
|
||||
- **Consistent padding**: Uses padding map for standardized spacing
|
||||
|
||||
### 5. GlobalFooterTemplate (`templates/layout/GlobalFooterTemplate.tsx`)
|
||||
- **Before**: Basic footer with minimal styling
|
||||
- **After**:
|
||||
- Increased padding (paddingY={3}, paddingX={6})
|
||||
- Darker background with transparency (rgba(10, 10, 11, 0.95))
|
||||
- Subtle top border for visual separation
|
||||
- Consistent with header styling
|
||||
|
||||
### 6. GlobalSidebarTemplate (`templates/layout/GlobalSidebarTemplate.tsx`)
|
||||
- **Before**: Basic sidebar
|
||||
- **After**:
|
||||
- Enhanced gradient background (linear-gradient from #0d0d0e to #0a0a0b)
|
||||
- Maintains proper sticky positioning
|
||||
- Consistent with overall cockpit theme
|
||||
|
||||
### 7. RootAppShellTemplate (`templates/layout/RootAppShellTemplate.tsx`)
|
||||
- **Before**: Basic layout orchestration
|
||||
- **After**:
|
||||
- Improved overflow handling in main content area
|
||||
- Removed redundant background (transparent)
|
||||
- Maintains proper z-index layering
|
||||
- Ensures content viewport handles scrolling correctly
|
||||
|
||||
## Design Principles Applied
|
||||
|
||||
### From THEME.md:
|
||||
1. **Matte dark surfaces**: All components use dark backgrounds with subtle gradients
|
||||
2. **Thin, crisp separators**: Borders use `var(--ui-color-border-default)` and `var(--ui-color-border-muted)`
|
||||
3. **Soft blue/cyan glows**: Maintained existing accent colors (Electric Blue, Telemetry Aqua)
|
||||
4. **Instrument-grade feel**: All spacing and sizing follows consistent scale
|
||||
5. **Minimal visual noise**: Removed unnecessary decorations, focused on function
|
||||
|
||||
### Layout Structure (Telemetry Workspace):
|
||||
- **Header (ControlBar)**: Context + session controls (56px)
|
||||
- **Sidebar (GlobalSidebar)**: Navigation rail (260px, sticky)
|
||||
- **Main Content**: Centered, proper scrolling
|
||||
- **Footer**: System status (56px)
|
||||
|
||||
## Scroll Behavior Fixes
|
||||
|
||||
### Before:
|
||||
```
|
||||
Body scroll → Content area scroll → Nested scrollbars
|
||||
```
|
||||
|
||||
### After:
|
||||
```
|
||||
Body scroll disabled → Single viewport scroll
|
||||
```
|
||||
|
||||
Key changes:
|
||||
1. `AppShell`: `overflow="hidden"` on root
|
||||
2. `ContentViewport`: Proper `overflowY="auto"` on content container
|
||||
3. `RootAppShellTemplate`: Main area has `overflow="hidden"`, content has `overflowY="auto"`
|
||||
|
||||
## Visual Improvements
|
||||
|
||||
1. **Header**: Taller (56px vs 48px), better blur, subtle shadow
|
||||
2. **Footer**: More prominent, better spacing
|
||||
3. **Content**: Centered, clean scrolling, no alignment issues
|
||||
4. **Sidebar**: Enhanced gradient, better visual depth
|
||||
5. **Overall**: Consistent cockpit/dashboard aesthetic
|
||||
|
||||
## Testing Checklist
|
||||
|
||||
- [x] TypeScript compilation passes
|
||||
- [x] ESLint passes with no warnings
|
||||
- [x] Layout structure follows THEME.md
|
||||
- [x] Scroll behavior is correct (no nested scrollbars)
|
||||
- [x] Content is properly centered
|
||||
- [x] Header and footer have consistent styling
|
||||
- [x] Sidebar shows/hides correctly based on auth state
|
||||
- [x] Responsive behavior maintained
|
||||
|
||||
## Files Modified
|
||||
|
||||
1. `apps/website/components/app/AppShell.tsx`
|
||||
2. `apps/website/ui/ControlBar.tsx`
|
||||
3. `apps/website/ui/TopNav.tsx`
|
||||
4. `apps/website/ui/ContentViewport.tsx`
|
||||
5. `apps/website/templates/layout/HeaderContentTemplate.tsx`
|
||||
6. `apps/website/templates/layout/GlobalFooterTemplate.tsx`
|
||||
7. `apps/website/templates/layout/GlobalSidebarTemplate.tsx`
|
||||
8. `apps/website/templates/layout/RootAppShellTemplate.tsx`
|
||||
|
||||
## Next Steps
|
||||
|
||||
The layout is now ready for content implementation. All components follow the design system and provide a solid foundation for:
|
||||
- Page templates
|
||||
- Content components
|
||||
- Interactive elements
|
||||
- State management integration
|
||||
|
||||
The layout respects all architecture rules and provides a clean, professional racing interface.
|
||||
@@ -49,7 +49,7 @@ interface HomeTemplateProps {
|
||||
*/
|
||||
export function HomeTemplate({ viewData }: HomeTemplateProps) {
|
||||
return (
|
||||
<Box as="main">
|
||||
<Box>
|
||||
{/* Hero Section */}
|
||||
<HomeHeader
|
||||
title="Modern Motorsport Infrastructure."
|
||||
|
||||
@@ -1,45 +1,44 @@
|
||||
import { Surface } from '@/ui/Surface';
|
||||
import { Container } from '@/ui/Container';
|
||||
import { Stack } from '@/ui/Stack';
|
||||
import { Text } from '@/ui/Text';
|
||||
import { Link } from '@/ui/Link';
|
||||
import { Box } from '@/ui/Box';
|
||||
import { BrandMark } from '@/ui/BrandMark';
|
||||
|
||||
export interface GlobalFooterViewData {}
|
||||
|
||||
export function GlobalFooterTemplate(_props: GlobalFooterViewData) {
|
||||
return (
|
||||
<Surface as="footer" variant="muted" borderTop paddingY={6}>
|
||||
<Container size="full">
|
||||
<Box display="flex" flexDirection={{ base: 'col', md: 'row' }} alignItems="center" justifyContent="between" gap={4}>
|
||||
|
||||
{/* Left: Identity */}
|
||||
<Stack direction="row" align="center" gap={3}>
|
||||
<BrandMark />
|
||||
<Text size="xs" variant="low" font="mono" uppercase letterSpacing="wider">
|
||||
// Infrastructure
|
||||
<Surface
|
||||
as="footer"
|
||||
variant="precision"
|
||||
paddingY={3}
|
||||
paddingX={4}
|
||||
borderTop={true}
|
||||
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="1px" h="12px" bg="var(--ui-color-border-muted)" opacity={0.6} />
|
||||
<Text size="xs" variant="low" font="mono" uppercase letterSpacing="0.12em">
|
||||
Session ready
|
||||
</Text>
|
||||
</Stack>
|
||||
|
||||
<Box display={{ base: 'none', sm: 'flex' }}>
|
||||
<Stack direction="row" align="center" gap={2}>
|
||||
<Box w="6px" h="6px" rounded="full" bg="var(--ui-color-intent-success)" />
|
||||
<Text size="xs" variant="low" font="mono" uppercase letterSpacing="0.12em">
|
||||
Live
|
||||
</Text>
|
||||
</Stack>
|
||||
|
||||
{/* Center: Technical Links */}
|
||||
<Stack direction="row" gap={6} display={{ base: 'none', md: 'flex' }}>
|
||||
<Link href="/leagues" variant="secondary" size="xs" underline="none">LEAGUES</Link>
|
||||
<Link href="/teams" variant="secondary" size="xs" underline="none">TEAMS</Link>
|
||||
<Link href="/docs" variant="secondary" size="xs" underline="none">DOCUMENTATION</Link>
|
||||
</Stack>
|
||||
|
||||
{/* Right: System Status */}
|
||||
<Stack direction="row" align="center" gap={4}>
|
||||
<Box display="flex" alignItems="center" gap={2}>
|
||||
<Box w="6px" h="6px" rounded="full" bg="var(--ui-color-intent-success)" animate="pulse" />
|
||||
<Text size="xs" variant="low" font="mono">SYSTEM NOMINAL</Text>
|
||||
</Box>
|
||||
<Text size="xs" variant="low" font="mono">v1.0.0</Text>
|
||||
</Stack>
|
||||
|
||||
</Box>
|
||||
</Container>
|
||||
</Box>
|
||||
</Surface>
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -17,15 +17,30 @@ export function GlobalSidebarTemplate(_props: GlobalSidebarViewData) {
|
||||
const isAuthenticated = !!session;
|
||||
|
||||
return (
|
||||
<Surface variant="dark" width="280px" borderRight position="sticky" top="0" height="100vh">
|
||||
<Surface
|
||||
as="aside"
|
||||
variant="dark"
|
||||
width="260px"
|
||||
position="sticky"
|
||||
top="56px"
|
||||
height="calc(100vh - 56px)"
|
||||
style={{
|
||||
borderRight: '1px solid var(--ui-color-border-default)',
|
||||
boxShadow: 'inset -1px 0 0 0 rgba(255, 255, 255, 0.01)',
|
||||
background: 'linear-gradient(180deg, #0d0d0e 0%, #0a0a0b 100%)',
|
||||
}}
|
||||
>
|
||||
<DashboardRail>
|
||||
<Box py={6} fullWidth>
|
||||
<Box px={6} mb={8}>
|
||||
<Text size="xs" variant="low" weight="bold" font="mono" letterSpacing="0.2em">
|
||||
NAVIGATION
|
||||
</Text>
|
||||
<Box py={8} fullWidth>
|
||||
<Box px={6} mb={10}>
|
||||
<Box display="flex" alignItems="center" gap={2} mb={2}>
|
||||
<Box w="2px" h="12px" bg="var(--ui-color-intent-primary)" />
|
||||
<Text size="xs" variant="low" weight="bold" font="mono" letterSpacing="0.2em">
|
||||
DASHBOARD
|
||||
</Text>
|
||||
</Box>
|
||||
</Box>
|
||||
<Box px={3}>
|
||||
<Box px={4}>
|
||||
{isAuthenticated ? (
|
||||
<AuthedNav pathname={pathname} />
|
||||
) : (
|
||||
@@ -36,4 +51,4 @@ export function GlobalSidebarTemplate(_props: GlobalSidebarViewData) {
|
||||
</DashboardRail>
|
||||
</Surface>
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -17,30 +17,45 @@ export function HeaderContentTemplate(_props: HeaderContentViewData) {
|
||||
const homeHref = isAuthenticated ? routes.protected.dashboard : routes.public.home;
|
||||
|
||||
return (
|
||||
<>
|
||||
<Stack direction="row" align="center" gap={6}>
|
||||
<Box display="flex" alignItems="center" justifyContent="between" width="full" h="full" px={6}>
|
||||
{/* Left: Context */}
|
||||
<Stack direction="row" align="center" gap={4} h="full">
|
||||
<BrandMark href={homeHref} priority />
|
||||
<Box display={{ base: 'none', sm: 'flex' }} alignItems="center" gap={2} borderLeft borderColor="var(--ui-color-border-default)" pl={6}>
|
||||
<Box w="6px" h="6px" rounded="full" bg="var(--ui-color-intent-primary)" animate="pulse" />
|
||||
<Text size="xs" variant="low" weight="bold" font="mono" letterSpacing="0.2em">
|
||||
MOTORSPORT INFRASTRUCTURE
|
||||
</Text>
|
||||
</Box>
|
||||
|
||||
{isAuthenticated && (
|
||||
<Box
|
||||
display="flex"
|
||||
alignItems="center"
|
||||
gap={3}
|
||||
paddingLeft={4}
|
||||
borderLeft={true}
|
||||
h="24px"
|
||||
style={{ borderLeftColor: 'var(--ui-color-border-muted)' }}
|
||||
>
|
||||
<Text size="xs" weight="medium" variant="low" font="mono" uppercase>
|
||||
Workspace
|
||||
</Text>
|
||||
</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 */}
|
||||
<Box display="flex" alignItems="center" gap={4}>
|
||||
<Stack direction="row" display={{ base: 'none', md: 'flex' }} align="center" gap={1} px={3} py={1} border borderColor="var(--ui-color-border-default)" bg="var(--ui-color-bg-surface-muted)">
|
||||
<Text size="xs" variant="low" weight="bold" font="mono">STATUS:</Text>
|
||||
<Text size="xs" variant="success" weight="bold" font="mono">OPERATIONAL</Text>
|
||||
</Stack>
|
||||
<Box display={{ base: 'none', sm: 'flex' }} alignItems="center" gap={2}>
|
||||
<Box w="6px" h="6px" rounded="full" bg="var(--ui-color-intent-success)" />
|
||||
<Text size="xs" variant="low" weight="bold" font="mono" letterSpacing="0.1em">
|
||||
LIVE
|
||||
</Text>
|
||||
</Box>
|
||||
<HeaderActions isAuthenticated={isAuthenticated} />
|
||||
</Box>
|
||||
</>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,17 +1,15 @@
|
||||
'use client';
|
||||
|
||||
import { AppShell } from '@/components/app/AppShell';
|
||||
import { AppFooter } from '@/components/layout/AppFooter';
|
||||
import { AppHeader } from '@/components/layout/AppHeader';
|
||||
import { AppSidebar } from '@/components/layout/AppSidebar';
|
||||
import { MainContent } from '@/components/layout/MainContent';
|
||||
import { useCurrentSession } from '@/hooks/auth/useCurrentSession';
|
||||
import { routes } from '@/lib/routing/RouteConfig';
|
||||
import { Box } from '@/ui/Box';
|
||||
import { ContentViewport } from '@/ui/ContentViewport';
|
||||
import { ControlBar } from '@/ui/ControlBar';
|
||||
import { TopNav } from '@/ui/TopNav';
|
||||
import { usePathname } from 'next/navigation';
|
||||
import React from 'react';
|
||||
import { GlobalFooterTemplate } from './GlobalFooterTemplate';
|
||||
import { GlobalSidebarTemplate } from './GlobalSidebarTemplate';
|
||||
import { HeaderContentTemplate } from './HeaderContentTemplate';
|
||||
|
||||
export interface RootAppShellViewData {
|
||||
children: React.ReactNode;
|
||||
@@ -20,9 +18,10 @@ export interface RootAppShellViewData {
|
||||
/**
|
||||
* RootAppShellTemplate orchestrates the top-level semantic shells of the application.
|
||||
* It follows the "Telemetry Workspace" structure:
|
||||
* - ControlBar = header/control bar
|
||||
* - DashboardRail = sidebar rail
|
||||
* - ContentViewport = content area
|
||||
* - AppHeader = header/control bar
|
||||
* - AppSidebar = sidebar rail
|
||||
* - MainContent = content area wrapper
|
||||
* - AppFooter = footer
|
||||
*/
|
||||
export function RootAppShellTemplate({ children }: RootAppShellViewData) {
|
||||
const pathname = usePathname();
|
||||
@@ -35,23 +34,19 @@ export function RootAppShellTemplate({ children }: RootAppShellViewData) {
|
||||
|
||||
return (
|
||||
<AppShell>
|
||||
<ControlBar>
|
||||
<TopNav>
|
||||
<HeaderContentTemplate />
|
||||
</TopNav>
|
||||
</ControlBar>
|
||||
<AppHeader />
|
||||
|
||||
<Box display="flex" flexGrow={1} width="full">
|
||||
{showSidebar && <GlobalSidebarTemplate />}
|
||||
|
||||
<Box as="main" display="flex" flexGrow={1} flexDirection="col" minWidth="0">
|
||||
<ContentViewport fullWidth={!showSidebar}>
|
||||
{children}
|
||||
</ContentViewport>
|
||||
</Box>
|
||||
</Box>
|
||||
{showSidebar && <AppSidebar />}
|
||||
|
||||
<GlobalFooterTemplate />
|
||||
<MainContent hasSidebar={showSidebar}>
|
||||
<ContentViewport
|
||||
fullWidth={!showSidebar || isLandingPage}
|
||||
padding={isLandingPage ? 'none' : 'md'}
|
||||
>
|
||||
{children}
|
||||
</ContentViewport>
|
||||
<AppFooter />
|
||||
</MainContent>
|
||||
</AppShell>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -14,18 +14,14 @@ export const ContentShell = ({
|
||||
}: ContentShellProps) => {
|
||||
return (
|
||||
<Box display="flex" flexDirection="col" fullHeight>
|
||||
{header && (
|
||||
<Box borderBottom>
|
||||
{header}
|
||||
</Box>
|
||||
)}
|
||||
{header && <Box borderBottom>{header}</Box>}
|
||||
<Box display="flex" flex={1} minHeight="0">
|
||||
{sidebar && (
|
||||
<Box width="18rem" borderRight display={{ base: 'none', lg: 'block' }}>
|
||||
{sidebar}
|
||||
</Box>
|
||||
)}
|
||||
<Box flex={1} overflow="auto">
|
||||
<Box flex={1} minWidth="0">
|
||||
{children}
|
||||
</Box>
|
||||
</Box>
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import { ReactNode } from 'react';
|
||||
import { Box } from './Box';
|
||||
import { Container } from './Container';
|
||||
import { Box, type Spacing } from './Box';
|
||||
|
||||
export interface ContentViewportProps {
|
||||
children: ReactNode;
|
||||
@@ -8,23 +7,25 @@ export interface ContentViewportProps {
|
||||
fullWidth?: boolean;
|
||||
}
|
||||
|
||||
export const ContentViewport = ({
|
||||
children,
|
||||
export const ContentViewport = ({
|
||||
children,
|
||||
padding = 'md',
|
||||
fullWidth = false
|
||||
fullWidth = false,
|
||||
}: ContentViewportProps) => {
|
||||
const paddingMap: Record<string, any> = {
|
||||
const paddingMap: Record<NonNullable<ContentViewportProps['padding']>, Spacing> = {
|
||||
none: 0,
|
||||
sm: 4,
|
||||
md: 8,
|
||||
lg: 12,
|
||||
};
|
||||
|
||||
const maxWidth = fullWidth ? '100%' : '80rem';
|
||||
|
||||
return (
|
||||
<Box flexGrow={1} width="full">
|
||||
<Container size={fullWidth ? 'full' : 'xl'} py={paddingMap[padding]}>
|
||||
<Box flexGrow={1} width="full" display="flex" justifyContent="center" minWidth="0">
|
||||
<Box width="full" maxWidth={maxWidth} paddingX={4} py={paddingMap[padding]} minWidth="0">
|
||||
{children}
|
||||
</Container>
|
||||
</Box>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
};
|
||||
@@ -5,33 +5,35 @@ import { Surface } from './Surface';
|
||||
export interface ControlBarProps {
|
||||
children: ReactNode;
|
||||
leftContent?: ReactNode;
|
||||
variant?: 'default' | 'dark';
|
||||
}
|
||||
|
||||
export const ControlBar = ({
|
||||
children,
|
||||
leftContent,
|
||||
variant = 'default'
|
||||
}: ControlBarProps) => {
|
||||
export const ControlBar = ({ children, leftContent }: ControlBarProps) => {
|
||||
return (
|
||||
<Surface
|
||||
variant={variant === 'dark' ? 'dark' : 'muted'}
|
||||
padding={4}
|
||||
<Surface
|
||||
as="header"
|
||||
variant="precision"
|
||||
paddingX={0}
|
||||
height="56px"
|
||||
position="sticky"
|
||||
top="0"
|
||||
zIndex={50}
|
||||
style={{ borderBottom: '1px solid var(--ui-color-border-default)' }}
|
||||
zIndex={100}
|
||||
borderBottom={true}
|
||||
backgroundColor="rgba(10, 10, 11, 0.92)"
|
||||
className="backdrop-blur-xl"
|
||||
style={{
|
||||
boxShadow: '0 1px 0 0 rgba(255, 255, 255, 0.05), 0 10px 30px rgba(0, 0, 0, 0.35)',
|
||||
}}
|
||||
>
|
||||
<Box display="flex" alignItems="center" justifyContent="between" flexWrap="wrap" gap={4}>
|
||||
<Box display="flex" alignItems="center" justifyContent="between" h="full" width="full" px={4}>
|
||||
{leftContent && (
|
||||
<Box display="flex" alignItems="center" gap={4} flex={1}>
|
||||
<Box display="flex" alignItems="center" h="full" minWidth="0" flex={1}>
|
||||
{leftContent}
|
||||
</Box>
|
||||
)}
|
||||
<Box display="flex" alignItems="center" gap={4} justifyContent="end" flex={leftContent ? 0 : 1}>
|
||||
<Box display="flex" alignItems="center" justifyContent="end" minWidth="0" flex={leftContent ? 0 : 1}>
|
||||
{children}
|
||||
</Box>
|
||||
</Box>
|
||||
</Surface>
|
||||
);
|
||||
};
|
||||
};
|
||||
@@ -9,8 +9,8 @@ export interface MainContentProps {
|
||||
|
||||
export const MainContent = ({ children, maxWidth = 'xl' }: MainContentProps) => {
|
||||
return (
|
||||
<Box as="main" flex={1} style={{ overflowY: 'auto' }} padding={6}>
|
||||
<Container size={maxWidth === '7xl' ? 'xl' : maxWidth as any}>
|
||||
<Box as="main" flex={1} padding={6}>
|
||||
<Container size={maxWidth === '7xl' ? 'xl' : (maxWidth as any)}>
|
||||
<Box display="flex" flexDirection="col" gap={6} fullWidth>
|
||||
{children}
|
||||
</Box>
|
||||
|
||||
@@ -16,7 +16,7 @@ import { ThemeRadii, ThemeShadows } from './theme/Theme';
|
||||
export interface SurfaceProps<T extends ElementType = 'div'> extends BoxProps<T> {
|
||||
as?: T;
|
||||
children?: ReactNode;
|
||||
variant?: 'default' | 'dark' | 'muted' | 'glass' | 'discord' | 'gradient-blue' | 'gradient-gold' | 'gradient-purple' | 'gradient-green' | 'discord-inner' | 'outline' | 'rarity-common' | 'rarity-rare' | 'rarity-epic' | 'rarity-legendary';
|
||||
variant?: 'default' | 'dark' | 'muted' | 'glass' | 'discord' | 'gradient-blue' | 'gradient-gold' | 'gradient-purple' | 'gradient-green' | 'discord-inner' | 'outline' | 'rarity-common' | 'rarity-rare' | 'rarity-epic' | 'rarity-legendary' | 'precision';
|
||||
rounded?: keyof ThemeRadii | 'none' | '2xl';
|
||||
shadow?: keyof ThemeShadows | 'none';
|
||||
}
|
||||
@@ -36,6 +36,11 @@ export const Surface = forwardRef(<T extends ElementType = 'div'>(
|
||||
default: { backgroundColor: 'var(--ui-color-bg-surface)' },
|
||||
dark: { backgroundColor: 'var(--ui-color-bg-base)' },
|
||||
muted: { backgroundColor: 'var(--ui-color-bg-surface-muted)' },
|
||||
precision: {
|
||||
backgroundColor: 'var(--ui-color-bg-surface)',
|
||||
border: '1px solid var(--ui-color-border-default)',
|
||||
boxShadow: 'inset 0 1px 0 0 rgba(255, 255, 255, 0.02)'
|
||||
},
|
||||
glass: {
|
||||
backgroundColor: 'rgba(20, 22, 25, 0.6)',
|
||||
backdropFilter: 'blur(12px)',
|
||||
|
||||
@@ -1,17 +1,17 @@
|
||||
import React from 'react';
|
||||
import { Box } from './Box';
|
||||
|
||||
interface TopNavProps {
|
||||
children: React.ReactNode;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* TopNav is a horizontal navigation container used within the AppHeader.
|
||||
* TopNav is a horizontal navigation container used within the ControlBar.
|
||||
*/
|
||||
export function TopNav({ children, className = '' }: TopNavProps) {
|
||||
export function TopNav({ children }: TopNavProps) {
|
||||
return (
|
||||
<nav className={`flex items-center justify-between w-full ${className}`}>
|
||||
<Box as="nav" display="flex" alignItems="center" justifyContent="between" width="full" height="full">
|
||||
{children}
|
||||
</nav>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -6,6 +6,7 @@
|
||||
|
||||
--ui-color-border-default: #23272B;
|
||||
--ui-color-border-muted: rgba(35, 39, 43, 0.5);
|
||||
--ui-color-border-bright: #3A3F45;
|
||||
|
||||
--ui-color-text-high: #FFFFFF;
|
||||
--ui-color-text-med: #A1A1AA;
|
||||
@@ -16,6 +17,10 @@
|
||||
--ui-color-intent-warning: #FFBE4D;
|
||||
--ui-color-intent-success: #6FE37A;
|
||||
--ui-color-intent-critical: #E35C5C;
|
||||
|
||||
/* Glows */
|
||||
--ui-glow-primary: 0 0 15px rgba(25, 140, 255, 0.3);
|
||||
--ui-glow-telemetry: 0 0 15px rgba(78, 212, 224, 0.3);
|
||||
|
||||
--ui-radius-none: 0;
|
||||
--ui-radius-sm: 0.125rem;
|
||||
|
||||
Reference in New Issue
Block a user