diff --git a/apps/website/components/dev/DevToolbar.tsx b/apps/website/components/dev/DevToolbar.tsx index 41d5f89f4..4ff258439 100644 --- a/apps/website/components/dev/DevToolbar.tsx +++ b/apps/website/components/dev/DevToolbar.tsx @@ -17,6 +17,7 @@ import { Icon } from '@/ui/Icon'; import { IconButton } from '@/ui/IconButton'; import { Text } from '@/ui/Text'; import { Stack } from '@/ui/Stack'; +import { Toolbar } from '@/ui/Toolbar'; import { APIStatusSection } from './sections/APIStatusSection'; import { NotificationSendSection } from './sections/NotificationSendSection'; import { NotificationTypeSection } from './sections/NotificationTypeSection'; @@ -229,9 +230,7 @@ export function DevToolbar() { if (isMinimized) { return ( - + setIsMinimized(false)} @@ -239,31 +238,12 @@ export function DevToolbar() { title="Open Dev Toolbar" size="lg" /> - + ); } return ( - + {/* Header */} @@ -386,6 +366,6 @@ export function DevToolbar() { Click ↑ to expand dev tools )} - + ); } diff --git a/apps/website/components/layout/AppFooter.tsx b/apps/website/components/layout/AppFooter.tsx index 66ff9d77f..86bd758ba 100644 --- a/apps/website/components/layout/AppFooter.tsx +++ b/apps/website/components/layout/AppFooter.tsx @@ -1,13 +1,14 @@ 'use client'; +import { useSidebar } from '@/components/layout/SidebarContext'; import { Box } from '@/ui/Box'; import { Link } from '@/ui/Link'; +import { ShellFooter } from '@/ui/shell/Shell'; import { Text } from '@/ui/Text'; import { useEffect, useState } from 'react'; -import { AppShellBar } from './AppShellBar'; export function AppFooter() { - const currentYear = new Date().getFullYear(); + const { isCollapsed } = useSidebar(); // Clock const [time, setTime] = useState(''); @@ -22,12 +23,12 @@ export function AppFooter() { }, []); return ( - + {/* Left: System Info */} - + GRIDPILOT @@ -35,7 +36,7 @@ export function AppFooter() { {/* Center: Time */} - + {time} UTC @@ -46,7 +47,7 @@ export function AppFooter() { Privacy Status - + ); } diff --git a/apps/website/components/layout/AppHeader.tsx b/apps/website/components/layout/AppHeader.tsx index 136e6203a..67169a27e 100644 --- a/apps/website/components/layout/AppHeader.tsx +++ b/apps/website/components/layout/AppHeader.tsx @@ -1,20 +1,24 @@ 'use client'; -import { Box } from '@/ui/Box'; import { Text } from '@/ui/Text'; -import { Search, Bell, Command } from 'lucide-react'; +import { Bell, Command } from 'lucide-react'; import { usePathname } from 'next/navigation'; import { useCurrentSession } from '@/hooks/auth/useCurrentSession'; import { useState, useEffect } from 'react'; -import { AppShellBar } from './AppShellBar'; +import { ShellHeader } from '@/ui/shell/Shell'; import { CommandModal } from './CommandModal'; import { UserPill } from '@/components/profile/UserPill'; +import { Input } from '@/ui/Input'; +import { Box } from '@/ui/Box'; +import { IconButton } from '@/ui/IconButton'; +import { useSidebar } from '@/components/layout/SidebarContext'; export function AppHeader() { const pathname = usePathname(); const { data: session } = useCurrentSession(); const isAuthenticated = !!session; const [isCommandOpen, setIsCommandOpen] = useState(false); + const { isCollapsed } = useSidebar(); // Simple breadcrumb logic const pathSegments = pathname.split('/').filter(Boolean); @@ -36,48 +40,50 @@ export function AppHeader() { return ( <> - + {/* Left: Context & Search */} - + {breadcrumbs} {/* Command Search Trigger */} - + + setIsCommandOpen(true)} + placeholder="Search or type a command..." + variant="search" + width="24rem" + rightElement={ + + + K + + } + className="cursor-pointer" + /> + {/* Right: User & Notifications */} {/* Notifications - Only when authed */} {isAuthenticated && ( - - - + + + )} {/* User Pill (Handles Auth & Menu) */} - + setIsCommandOpen(false)} /> diff --git a/apps/website/components/layout/AppSidebar.tsx b/apps/website/components/layout/AppSidebar.tsx index 2e080e03b..141735c2a 100644 --- a/apps/website/components/layout/AppSidebar.tsx +++ b/apps/website/components/layout/AppSidebar.tsx @@ -3,7 +3,6 @@ import { BrandMark } from '@/ui/BrandMark'; import { NavLink } from '@/ui/NavLink'; import { routes } from '@/lib/routing/RouteConfig'; -import { Box } from '@/ui/Box'; import { Stack } from '@/ui/Stack'; import { Text } from '@/ui/Text'; import { @@ -18,6 +17,8 @@ import { } from 'lucide-react'; import { usePathname } from 'next/navigation'; import { useSidebar } from '@/components/layout/SidebarContext'; +import { ShellSidebar } from '@/ui/shell/Shell'; +import { Button } from '@/ui/Button'; export function AppSidebar() { const pathname = usePathname(); @@ -36,76 +37,64 @@ export function AppSidebar() { ]; return ( - - {/* Brand Header */} - - - - - {/* Navigation */} - - - - {!isCollapsed && ( - - Platform - - )} - - {mainItems.map((item) => ( - - ))} - - - - - {!isCollapsed && ( - - Competition - - )} - - {competitionItems.map((item) => ( - - ))} - - - - - - {/* Bottom Actions */} - - + } + > + + + {!isCollapsed && ( + + Platform + )} - - - + + {mainItems.map((item) => ( + + ))} + + + + + {!isCollapsed && ( + + Competition + + )} + + {competitionItems.map((item) => ( + + ))} + + + + ); } diff --git a/apps/website/ui/Button.tsx b/apps/website/ui/Button.tsx index 4d7c49c08..00ccb189d 100644 --- a/apps/website/ui/Button.tsx +++ b/apps/website/ui/Button.tsx @@ -45,6 +45,7 @@ export interface ButtonProps { shadow?: string; display?: string; center?: boolean; + justifyContent?: string; } export const Button = forwardRef(({ @@ -89,6 +90,7 @@ export const Button = forwardRef { const baseClasses = 'inline-flex items-center justify-center focus-visible:outline focus-visible:outline-1 focus-visible:outline-offset-2 active:opacity-80 uppercase tracking-widest font-bold'; const transitionClasses = transition !== false ? 'transition-all duration-150 ease-in-out' : ''; @@ -125,6 +127,7 @@ export const Button = forwardRef, 'size'> { + icon?: LucideIcon | React.ReactNode; + rightElement?: React.ReactNode; + variant?: 'default' | 'ghost' | 'search' | 'error'; + fullWidth?: boolean; label?: string; error?: string; - errorMessage?: string; + errorMessage?: string; // Alias for error hint?: string; - fullWidth?: boolean; - size?: 'sm' | 'md' | 'lg'; - icon?: React.ReactNode; - variant?: 'default' | 'error'; + size?: string | number; // Allow size prop } export const Input = forwardRef(({ - label, - error, - errorMessage, - hint, + icon, + rightElement, + variant = 'default', fullWidth = false, - size = 'md', - icon, - variant = 'default', + className, + label, + error, + errorMessage, + hint, + id, + size, ...props }, ref) => { - const finalError = error || errorMessage; - const sizeClasses = { - sm: 'px-3 py-1.5 text-xs', - md: 'px-4 py-2 text-sm', - lg: 'px-4 py-3 text-base' + const variantClasses = { + default: 'bg-surface-charcoal border border-outline-steel focus:border-primary-accent', + ghost: 'bg-transparent border-none', + search: 'bg-surface-charcoal/50 border border-outline-steel focus:border-primary-accent hover:border-text-low/50', + error: 'bg-surface-charcoal border border-critical-red focus:border-critical-red', }; - const baseClasses = 'bg-[var(--ui-color-bg-surface)] border border-[var(--ui-color-border-default)] text-[var(--ui-color-text-high)] placeholder-[var(--ui-color-text-low)] focus:outline-none focus:border-[var(--ui-color-intent-primary)] transition-colors'; - const errorClasses = (finalError || variant === 'error') ? 'border-[var(--ui-color-intent-critical)]' : ''; - const widthClasses = fullWidth ? 'w-full' : ''; - - const classes = [ - baseClasses, - sizeClasses[size], - errorClasses, - widthClasses, - icon ? 'pl-10' : '', - ].filter(Boolean).join(' '); + const inputId = id || (label ? `input-${label.toLowerCase().replace(/\s+/g, '-')}` : undefined); + const displayError = error || errorMessage; return ( - + {label && ( - + {label} )} - + + {icon && ( - - {icon} - + React.isValidElement(icon) ? icon : ( + + ) )} + + + {rightElement} - {finalError && ( + + {displayError && ( - {finalError} + {displayError} )} - {hint && !finalError && ( + + {hint && !displayError && ( {hint} diff --git a/apps/website/ui/Toolbar.tsx b/apps/website/ui/Toolbar.tsx new file mode 100644 index 000000000..e55159a67 --- /dev/null +++ b/apps/website/ui/Toolbar.tsx @@ -0,0 +1,48 @@ +import { ReactNode } from 'react'; +import { Stack } from './Stack'; + +interface ToolbarProps { + children: ReactNode; + minimized?: boolean; + bottom?: number | string; +} + +export function Toolbar({ children, minimized = false, bottom = '2rem' }: ToolbarProps) { + if (minimized) { + return ( + + {children} + + ); + } + + return ( + + {children} + + ); +} diff --git a/apps/website/ui/shell/Shell.tsx b/apps/website/ui/shell/Shell.tsx new file mode 100644 index 000000000..868e32d1c --- /dev/null +++ b/apps/website/ui/shell/Shell.tsx @@ -0,0 +1,101 @@ +import { ReactNode } from 'react'; +import { Box } from '../Box'; + +// --- Shell Sidebar --- + +interface ShellSidebarProps { + children: ReactNode; + header?: ReactNode; + footer?: ReactNode; + collapsed?: boolean; + width?: string | number; + collapsedWidth?: string | number; +} + +export function ShellSidebar({ + children, + header, + footer, + collapsed = false, + width = '16rem', + collapsedWidth = '5rem' +}: ShellSidebarProps) { + return ( + + {header && ( + + {header} + + )} + + + {children} + + + {footer && ( + + {footer} + + )} + + ); +} + +// --- Shell Header --- + +interface ShellHeaderProps { + children: ReactNode; + collapsed?: boolean; +} + +export function ShellHeader({ children, collapsed = false }: ShellHeaderProps) { + return ( + + {children} + + ); +} + +// --- Shell Footer --- + +interface ShellFooterProps { + children: ReactNode; + collapsed?: boolean; +} + +export function ShellFooter({ children, collapsed = false }: ShellFooterProps) { + return ( + + {children} + + ); +}