website refactor

This commit is contained in:
2026-01-18 16:18:18 +01:00
parent 0b301feb61
commit 13567d51af
329 changed files with 4701 additions and 4750 deletions

View File

@@ -2,13 +2,12 @@
import { CheckCircle2, Clock, Star } from 'lucide-react';
import { Badge } from '@/ui/Badge';
import { Box } from '@/ui/Box';
import { Stack } from '@/ui/Stack';
import { Button } from '@/ui/Button';
import { Card } from '@/ui/Card';
import { Heading } from '@/ui/Heading';
import { Icon } from '@/ui/Icon';
import { Link } from '@/ui/Link';
import { Stack } from '@/ui/Stack';
import { Text } from '@/ui/Text';
interface AvailableLeague {
@@ -53,33 +52,33 @@ export function AvailableLeagueCard({ league }: AvailableLeagueCardProps) {
<Stack gap={4}>
{/* Header */}
<Stack direction="row" align="start" justify="between">
<Box flexGrow={1}>
<Stack flexGrow={1}>
<Stack direction="row" align="center" gap={2} mb={1} wrap>
<Badge variant="primary">{config.icon} {config.label}</Badge>
<Box px={2} py={0.5} rounded="full" className={status.bgColor}>
<Stack px={2} py={0.5} rounded="full" className={status.bgColor}>
<Text size="xs" weight="medium" className={status.color}>{status.label}</Text>
</Box>
</Stack>
</Stack>
<Heading level={3}>{league.name}</Heading>
<Text size="sm" color="text-gray-500" block mt={1}>{league.game}</Text>
</Box>
<Box px={2} py={1} rounded="lg" bg="bg-iron-gray/50">
</Stack>
<Stack px={2} py={1} rounded="lg" bg="bg-iron-gray/50">
<Stack direction="row" align="center" gap={1}>
<Icon icon={Star} size={3.5} color="text-yellow-400" />
<Text size="sm" weight="medium" color="text-white">{league.rating}</Text>
</Stack>
</Box>
</Stack>
</Stack>
{/* Description */}
<Text size="sm" color="text-gray-400" block truncate>{league.description}</Text>
{/* Stats Grid */}
<Box display="grid" gridCols={3} gap={2}>
<Stack display="grid" gridCols={3} gap={2}>
<StatItem label="Drivers" value={league.drivers} />
<StatItem label="Avg Views" value={league.formattedAvgViews} />
<StatItem label="CPM" value={league.formattedCpm} color="text-performance-green" />
</Box>
</Stack>
{/* Next Race */}
{league.nextRace && (
@@ -106,21 +105,21 @@ export function AvailableLeagueCard({ league }: AvailableLeagueCardProps) {
{/* Actions */}
<Stack direction="row" gap={2}>
<Box flexGrow={1}>
<Stack flexGrow={1}>
<Link href={`/sponsor/leagues/${league.id}`} block>
<Button variant="secondary" fullWidth size="sm">
View Details
</Button>
</Link>
</Box>
</Stack>
{(league.mainSponsorSlot.available || league.secondarySlots.available > 0) && (
<Box flexGrow={1}>
<Stack flexGrow={1}>
<Link href={`/sponsor/leagues/${league.id}?action=sponsor`} block>
<Button variant="primary" fullWidth size="sm">
Sponsor
</Button>
</Link>
</Box>
</Stack>
)}
</Stack>
</Stack>
@@ -130,22 +129,22 @@ export function AvailableLeagueCard({ league }: AvailableLeagueCardProps) {
function StatItem({ label, value, color = 'text-white' }: { label: string, value: string | number, color?: string }) {
return (
<Box p={2} bg="bg-iron-gray/50" rounded="lg" textAlign="center">
<Stack p={2} bg="bg-iron-gray/50" rounded="lg" textAlign="center">
<Text weight="bold" className={color}>{value}</Text>
<Text size="xs" color="text-gray-500" block mt={1}>{label}</Text>
</Box>
</Stack>
);
}
function SlotRow({ label, available, price }: { label: string, available: boolean, price: string }) {
return (
<Box p={2} rounded="lg" bg="bg-iron-gray/30">
<Stack p={2} rounded="lg" bg="bg-iron-gray/30">
<Stack direction="row" align="center" justify="between">
<Stack direction="row" align="center" gap={2}>
<Box width="2.5" height="2.5" rounded="full" bg={available ? 'bg-performance-green' : 'bg-error-red'} />
<Stack width="2.5" height="2.5" rounded="full" bg={available ? 'bg-performance-green' : 'bg-error-red'} />
<Text size="sm" color="text-gray-300">{label}</Text>
</Stack>
<Box>
<Stack>
{available ? (
<Text size="sm" weight="semibold" color="text-white">{price}</Text>
) : (
@@ -154,8 +153,8 @@ function SlotRow({ label, available, price }: { label: string, available: boolea
<Text size="sm" color="text-gray-500">Filled</Text>
</Stack>
)}
</Box>
</Stack>
</Stack>
</Box>
</Stack>
);
}

View File

@@ -6,7 +6,6 @@ import { Text } from '@/ui/Text';
import { Stack } from '@/ui/Stack';
import { Modal } from '@/ui/Modal';
import { InfoBanner } from '@/ui/InfoBanner';
import { Box } from '@/ui/Box';
import { ModalIcon } from '@/ui/ModalIcon';
interface EndRaceModalProps {
@@ -58,14 +57,14 @@ export function EndRaceModal({ raceId, raceName, onConfirm, onCancel, isOpen }:
</Stack>
</InfoBanner>
<Box textAlign="center">
<Stack textAlign="center">
<Text size="sm" color="text-gray-400">
Race: <Text color="text-white" weight="medium">{raceName}</Text>
</Text>
<Text size="xs" color="text-gray-500" block mt={1}>
ID: {raceId}
</Text>
</Box>
</Stack>
</Stack>
</Modal>
);

View File

@@ -1,7 +1,6 @@
import React from 'react';
import { Box } from '@/ui/Box';
import { Text } from '@/ui/Text';
import { Stack } from '@/ui/Stack';
import { Text } from '@/ui/Text';
import { Button } from '@/ui/Button';
interface JoinRequestItemProps {
@@ -22,7 +21,7 @@ export function JoinRequestItem({
isRejecting,
}: JoinRequestItemProps) {
return (
<Box
<Stack
display="flex"
alignItems="center"
justifyContent="between"
@@ -33,7 +32,7 @@ export function JoinRequestItem({
borderColor="border-charcoal-outline"
>
<Stack direction="row" align="center" gap={4} flexGrow={1}>
<Box
<Stack
width="12"
height="12"
rounded="full"
@@ -45,13 +44,13 @@ export function JoinRequestItem({
style={{ fontSize: '1.125rem' }}
>
{driverId.charAt(0)}
</Box>
<Box flexGrow={1}>
</Stack>
<Stack flexGrow={1}>
<Text color="text-white" weight="medium" block>{driverId}</Text>
<Text size="sm" color="text-gray-400" block>
Requested {new Date(requestedAt).toLocaleDateString()}
</Text>
</Box>
</Stack>
</Stack>
<Stack direction="row" gap={2}>
<Button
@@ -71,6 +70,6 @@ export function JoinRequestItem({
{isRejecting ? 'Rejecting...' : 'Reject'}
</Button>
</Stack>
</Box>
</Stack>
);
}

View File

@@ -1,5 +1,4 @@
import React from 'react';
import { Box } from '@/ui/Box';
import { Stack } from '@/ui/Stack';
import { Text } from '@/ui/Text';
import { Heading } from '@/ui/Heading';
@@ -22,36 +21,36 @@ interface JoinRequestsPanelProps {
export function JoinRequestsPanel({ requests, onAccept, onDecline }: JoinRequestsPanelProps) {
if (requests.length === 0) {
return (
<Box p={8} border borderDash borderColor="border-steel-grey" bg="surface-charcoal/20" textAlign="center">
<Stack p={8} border borderDash borderColor="border-steel-grey" bg="surface-charcoal/20" textAlign="center">
<Text color="text-gray-500" size="sm">No pending join requests</Text>
</Box>
</Stack>
);
}
return (
<Box border borderColor="border-steel-grey" bg="surface-charcoal/50" overflow="hidden">
<Box p={4} borderBottom borderColor="border-steel-grey" bg="base-graphite/50">
<Stack border borderColor="border-steel-grey" bg="surface-charcoal/50" overflow="hidden">
<Stack p={4} borderBottom borderColor="border-steel-grey" bg="base-graphite/50">
<Heading level={4} weight="bold" className="uppercase tracking-widest text-gray-400 text-[10px]">
Pending Requests ({requests.length})
</Heading>
</Box>
</Stack>
<Stack gap={0} className="divide-y divide-border-steel-grey/30">
{requests.map((request) => (
<Box key={request.id} p={4} className="hover:bg-white/[0.02] transition-colors">
<Stack key={request.id} p={4} className="hover:bg-white/[0.02] transition-colors">
<Stack direction="row" align="start" justify="between" gap={4}>
<Stack direction="row" align="center" gap={3}>
<Box w="10" h="10" bg="base-graphite" border borderColor="border-steel-grey" display="flex" center>
<Stack w="10" h="10" bg="base-graphite" border borderColor="border-steel-grey" display="flex" center>
<Text size="xs" weight="bold" color="text-primary-blue">
{request.driverName.substring(0, 2).toUpperCase()}
</Text>
</Box>
<Box>
</Stack>
<Stack>
<Text weight="bold" size="sm" color="text-white" block>{request.driverName}</Text>
<Stack direction="row" align="center" gap={1.5} mt={0.5}>
<Icon icon={Clock} size={3} color="text-gray-500" />
<Text size="xs" color="text-gray-500" font="mono">{request.requestedAt}</Text>
</Stack>
</Box>
</Stack>
</Stack>
<Stack direction="row" align="center" gap={2}>
@@ -74,15 +73,15 @@ export function JoinRequestsPanel({ requests, onAccept, onDecline }: JoinRequest
</Stack>
</Stack>
{request.message && (
<Box mt={3} p={3} bg="base-graphite/30" borderLeft borderPrimary borderColor="primary-blue/40">
<Stack mt={3} p={3} bg="base-graphite/30" borderLeft borderPrimary borderColor="primary-blue/40">
<Text size="xs" color="text-gray-400" italic leading="relaxed">
&ldquo;{request.message}&rdquo;
</Text>
</Box>
</Stack>
)}
</Box>
</Stack>
))}
</Stack>
</Box>
</Stack>
);
}

View File

@@ -4,7 +4,6 @@ import React from 'react';
import { FileText, Gamepad2, Check } from 'lucide-react';
import { Input } from '@/ui/Input';
import type { LeagueConfigFormModel } from '@/lib/types/LeagueConfigFormModel';
import { Box } from '@/ui/Box';
import { Stack } from '@/ui/Stack';
import { Text } from '@/ui/Text';
import { Heading } from '@/ui/Heading';
@@ -47,18 +46,18 @@ export function LeagueBasicsSection({
return (
<Stack gap={8}>
{/* Emotional header for the step */}
<Box textAlign="center" pb={2}>
<Box mb={2}>
<Stack textAlign="center" pb={2}>
<Stack mb={2}>
<Heading level={3}>
Every great championship starts with a name
</Heading>
</Box>
<Box maxWidth="lg" mx="auto">
</Stack>
<Stack maxWidth="lg" mx="auto">
<Text size="sm" color="text-gray-400">
This is where legends begin. Give your league an identity that drivers will remember.
</Text>
</Box>
</Box>
</Stack>
</Stack>
{/* League name */}
<Stack gap={3}>
@@ -130,11 +129,11 @@ export function LeagueBasicsSection({
/>
<Surface variant="muted" rounded="lg" border padding={4}>
<Box mb={3}>
<Stack mb={3}>
<Text size="xs" color="text-gray-400">
<Text weight="medium" color="text-gray-300">Great descriptions include:</Text>
</Text>
</Box>
</Stack>
<Grid cols={3} gap={3}>
{[
'Racing style & pace',

View File

@@ -1,11 +1,10 @@
import { ReactNode } from 'react';
import { Box } from '@/ui/Box';
import { Stack } from '@/ui/Stack';
import { Heading } from '@/ui/Heading';
import { Icon } from '@/ui/Icon';
import { Text } from '@/ui/Text';
import { Stack } from '@/ui/Stack';
import { Image } from '@/ui/Image';
import { PlaceholderImage } from '@/ui/PlaceholderImage';
import { Calendar as LucideCalendar, ChevronRight as LucideChevronRight } from 'lucide-react';
@@ -50,7 +49,7 @@ export function LeagueCard({
onClick,
}: LeagueCardProps) {
return (
<Box
<Stack
position="relative"
cursor={onClick ? 'pointer' : 'default'}
h="full"
@@ -58,7 +57,7 @@ export function LeagueCard({
className="group"
>
{/* Card Container */}
<Box
<Stack
position="relative"
h="full"
rounded="none"
@@ -70,7 +69,7 @@ export function LeagueCard({
className="hover:border-primary-accent/30 hover:bg-panel-gray/60 transition-all duration-300"
>
{/* Cover Image */}
<Box position="relative" h="32" overflow="hidden">
<Stack position="relative" h="32" overflow="hidden">
<Image
src={coverUrl}
alt={`${name} cover`}
@@ -80,21 +79,21 @@ export function LeagueCard({
className="transition-transform duration-500 group-hover:scale-105 opacity-60"
/>
{/* Gradient Overlay */}
<Box position="absolute" inset="0" bg="linear-gradient(to top, #0C0D0F, transparent)" />
<Stack position="absolute" inset="0" bg="linear-gradient(to top, #0C0D0F, transparent)" />
{/* Badges - Top Left */}
<Box position="absolute" top="3" left="3" display="flex" alignItems="center" gap={2}>
<Stack position="absolute" top="3" left="3" display="flex" alignItems="center" gap={2}>
{badges}
</Box>
</Stack>
{/* Championship Type Badge - Top Right */}
<Box position="absolute" top="3" right="3">
<Stack position="absolute" top="3" right="3">
{championshipBadge}
</Box>
</Stack>
{/* Logo */}
<Box position="absolute" left="4" bottom="-6" zIndex={10}>
<Box w="12" h="12" rounded="none" overflow="hidden" border borderColor="border-gray/50" bg="graphite-black" shadow="xl">
<Stack position="absolute" left="4" bottom="-6" zIndex={10}>
<Stack w="12" h="12" rounded="none" overflow="hidden" border borderColor="border-gray/50" bg="graphite-black" shadow="xl">
{logoUrl ? (
<Image
src={logoUrl}
@@ -108,15 +107,15 @@ export function LeagueCard({
) : (
<PlaceholderImage size={48} />
)}
</Box>
</Box>
</Box>
</Stack>
</Stack>
</Stack>
{/* Content */}
<Box pt={8} px={4} pb={4} display="flex" flexDirection="col" fullHeight>
<Stack pt={8} px={4} pb={4} display="flex" flexDirection="col" fullHeight>
{/* Title & Description */}
<Stack direction="row" align="center" gap={2} mb={1}>
<Box w="1" h="4" bg="primary-accent" />
<Stack w="1" h="4" bg="primary-accent" />
<Heading level={3} fontSize="lg" weight="bold" className="line-clamp-1 group-hover:text-primary-accent transition-colors tracking-tight">
{name}
</Heading>
@@ -126,17 +125,17 @@ export function LeagueCard({
</Text>
{/* Stats Row */}
<Box display="flex" alignItems="center" gap={3} mb={4}>
<Stack display="flex" alignItems="center" gap={3} mb={4}>
{/* Primary Slots (Drivers/Teams/Nations) */}
<Box flexGrow={1}>
<Box display="flex" alignItems="center" justifyContent="between" mb={1.5}>
<Stack flexGrow={1}>
<Stack display="flex" alignItems="center" justifyContent="between" mb={1.5}>
<Text size="xs" color="text-gray-500" weight="bold" className="uppercase tracking-widest">{slotLabel}</Text>
<Text size="xs" color="text-gray-400" font="mono">
{usedSlots}/{maxSlots || '∞'}
</Text>
</Box>
<Box h="1" rounded="none" bg="border-gray/30" overflow="hidden">
<Box
</Stack>
<Stack h="1" rounded="none" bg="border-gray/30" overflow="hidden">
<Stack
h="full"
rounded="none"
transition
@@ -149,45 +148,45 @@ export function LeagueCard({
}
style={{ width: `${Math.min(fillPercentage, 100)}%` }}
/>
</Box>
</Box>
</Stack>
</Stack>
{/* Open Slots Badge */}
{hasOpenSlots && (
<Box display="flex" alignItems="center" gap={1.5} px={2} py={1} rounded="none" bg="primary-accent/5" border borderColor="primary-accent/20">
<Box w="1.5" h="1.5" rounded="full" bg="primary-accent" className="animate-pulse" />
<Stack display="flex" alignItems="center" gap={1.5} px={2} py={1} rounded="none" bg="primary-accent/5" border borderColor="primary-accent/20">
<Stack w="1.5" h="1.5" rounded="full" bg="primary-accent" className="animate-pulse" />
<Text size="xs" color="text-primary-accent" weight="bold" className="uppercase tracking-tighter">
{openSlotsCount} OPEN
</Text>
</Box>
</Stack>
)}
</Box>
</Stack>
{/* Spacer to push footer to bottom */}
<Box flexGrow={1} />
<Stack flexGrow={1} />
{/* Footer Info */}
<Box display="flex" alignItems="center" justifyContent="between" pt={3} borderTop borderColor="border-gray/30" mt="auto">
<Box display="flex" alignItems="center" gap={3}>
<Stack display="flex" alignItems="center" justifyContent="between" pt={3} borderTop borderColor="border-gray/30" mt="auto">
<Stack display="flex" alignItems="center" gap={3}>
{timingSummary && (
<Box display="flex" alignItems="center" gap={2}>
<Stack display="flex" alignItems="center" gap={2}>
<Icon icon={LucideCalendar} size={3} color="text-gray-500" />
<Text size="xs" color="text-gray-500" font="mono">
{timingSummary.split('•')[1]?.trim() || timingSummary}
</Text>
</Box>
</Stack>
)}
</Box>
</Stack>
{/* View Arrow */}
<Box display="flex" alignItems="center" gap={1} className="group-hover:text-primary-accent transition-colors">
<Stack display="flex" alignItems="center" gap={1} className="group-hover:text-primary-accent transition-colors">
<Text size="xs" color="text-gray-500" weight="bold" className="uppercase tracking-widest">VIEW</Text>
<Icon icon={LucideChevronRight} size={3} color="text-gray-500" className="transition-transform group-hover:translate-x-0.5" />
</Box>
</Box>
</Box>
</Box>
</Box>
</Stack>
</Stack>
</Stack>
</Stack>
</Stack>
);
}

View File

@@ -3,9 +3,8 @@
import { useState, useRef, useCallback } from 'react';
import { Card } from '@/ui/Card';
import { Button } from '@/ui/Button';
import { Box } from '@/ui/Box';
import { Text } from '@/ui/Text';
import { Stack } from '@/ui/Stack';
import { Text } from '@/ui/Text';
import { Heading } from '@/ui/Heading';
import { Icon } from '@/ui/Icon';
import {
@@ -174,12 +173,12 @@ export function LeagueDecalPlacementEditor({
return (
<Stack gap={6}>
{/* Header */}
<Box display="flex" alignItems="center" justifyContent="between">
<Box>
<Stack display="flex" alignItems="center" justifyContent="between">
<Stack>
<Heading level={3} fontSize="lg" weight="semibold" color="text-white">{carName}</Heading>
<Text size="sm" color="text-gray-400">Position sponsor decals on this car&apos;s template</Text>
</Box>
<Box display="flex" alignItems="center" gap={2}>
</Stack>
<Stack display="flex" alignItems="center" gap={2}>
<Button
variant="secondary"
onClick={() => setZoom(z => Math.max(0.5, z - 0.25))}
@@ -201,13 +200,13 @@ export function LeagueDecalPlacementEditor({
>
<Icon icon={ZoomIn} size={4} />
</Button>
</Box>
</Box>
</Stack>
</Stack>
<Box display="grid" responsiveGridCols={{ base: 1, lg: 3 }} gap={6}>
<Stack display="grid" responsiveGridCols={{ base: 1, lg: 3 }} gap={6}>
{/* Canvas */}
<Box responsiveColSpan={{ lg: 2 }}>
<Box
<Stack responsiveColSpan={{ lg: 2 }}>
<Stack
ref={canvasRef}
position="relative"
// eslint-disable-next-line gridpilot-rules/component-classification
@@ -220,7 +219,7 @@ export function LeagueDecalPlacementEditor({
>
{/* Base Image or Placeholder */}
{baseImageUrl ? (
<Box
<Stack
as="img"
src={baseImageUrl}
alt="Livery template"
@@ -231,21 +230,21 @@ export function LeagueDecalPlacementEditor({
draggable={false}
/>
) : (
<Box fullWidth fullHeight display="flex" flexDirection="col" alignItems="center" justifyContent="center">
<Stack fullWidth fullHeight display="flex" flexDirection="col" alignItems="center" justifyContent="center">
<Icon icon={ImageIcon} size={16} color="text-gray-600"
// eslint-disable-next-line gridpilot-rules/component-classification
className="mb-2"
/>
<Text size="sm" color="text-gray-500">No base template uploaded</Text>
<Text size="xs" color="text-gray-600">Upload a template image first</Text>
</Box>
</Stack>
)}
{/* Decal Placeholders */}
{placements.map((placement) => {
const decalColors = getSponsorTypeColor(placement.sponsorType);
return (
<Box
<Stack
key={placement.id}
onMouseDown={(e: React.MouseEvent) => handleMouseDown(e, placement.id)}
onClick={() => handleDecalClick(placement.id)}
@@ -272,8 +271,8 @@ export function LeagueDecalPlacementEditor({
transform: `translate(-50%, -50%) rotate(${placement.rotation}deg)`,
}}
>
<Box textAlign="center" truncate px={1}>
<Box
<Stack textAlign="center" truncate px={1}>
<Stack
// eslint-disable-next-line gridpilot-rules/component-classification
style={{ fontSize: '10px' }}
transform="uppercase"
@@ -281,36 +280,36 @@ export function LeagueDecalPlacementEditor({
className="tracking-wide opacity-70"
>
{placement.sponsorType === 'main' ? 'Main' : 'Secondary'}
</Box>
<Box truncate>{placement.sponsorName}</Box>
</Box>
</Stack>
<Stack truncate>{placement.sponsorName}</Stack>
</Stack>
{/* Drag handle indicator */}
{selectedDecal === placement.id && (
<Box position="absolute" top="-1" left="-1" w="3" h="3" bg="bg-white" rounded="full" border borderWidth="2px" borderColor="border-primary-blue" />
<Stack position="absolute" top="-1" left="-1" w="3" h="3" bg="bg-white" rounded="full" border borderWidth="2px" borderColor="border-primary-blue" />
)}
</Box>
</Stack>
);
})}
{/* Grid overlay when dragging */}
{isDragging && (
<Box position="absolute" inset="0" pointerEvents="none">
<Box fullWidth fullHeight
<Stack position="absolute" inset="0" pointerEvents="none">
<Stack fullWidth fullHeight
// eslint-disable-next-line gridpilot-rules/component-classification
style={{
backgroundImage: 'linear-gradient(to right, rgba(255,255,255,0.05) 1px, transparent 1px), linear-gradient(to bottom, rgba(255,255,255,0.05) 1px, transparent 1px)',
backgroundSize: '10% 10%',
}}
/>
</Box>
</Stack>
)}
</Box>
</Stack>
<Text size="xs" color="text-gray-500" mt={2} block>
Click a decal to select it, then drag to reposition. Use controls on the right to adjust size and rotation.
</Text>
</Box>
</Stack>
{/* Controls Panel */}
<Stack gap={4}>
@@ -321,7 +320,7 @@ export function LeagueDecalPlacementEditor({
{placements.map((placement) => {
const decalColors = getSponsorTypeColor(placement.sponsorType);
return (
<Box
<Stack
key={placement.id}
as="button"
onClick={() => setSelectedDecal(placement.id)}
@@ -335,9 +334,9 @@ export function LeagueDecalPlacementEditor({
bg={selectedDecal === placement.id ? decalColors.bg : 'bg-iron-gray/30'}
hoverBg={selectedDecal !== placement.id ? 'bg-iron-gray/50' : undefined}
>
<Box display="flex" alignItems="center" justifyContent="between">
<Box>
<Box
<Stack display="flex" alignItems="center" justifyContent="between">
<Stack>
<Stack
// eslint-disable-next-line gridpilot-rules/component-classification
style={{ fontSize: '10px' }}
weight="medium"
@@ -345,19 +344,19 @@ export function LeagueDecalPlacementEditor({
color={decalColors.text}
>
{placement.sponsorType === 'main' ? 'Main Sponsor' : `Secondary ${placement.sponsorType.split('-')[1]}`}
</Box>
<Box
</Stack>
<Stack
// eslint-disable-next-line gridpilot-rules/component-classification
style={{ fontSize: '10px' }}
color="text-gray-500"
mt={0.5}
>
{Math.round(placement.x * 100)}%, {Math.round(placement.y * 100)}% {placement.rotation}°
</Box>
</Box>
</Stack>
</Stack>
<Icon icon={Target} size={4} color={selectedDecal === placement.id ? decalColors.text : 'text-gray-500'} />
</Box>
</Box>
</Stack>
</Stack>
);
})}
</Stack>
@@ -369,12 +368,12 @@ export function LeagueDecalPlacementEditor({
<Heading level={4} fontSize="sm" weight="semibold" color="text-white" mb={3}>Adjust Selected</Heading>
{/* Position */}
<Box mb={4}>
<Stack mb={4}>
<Text as="label" size="xs" color="text-gray-400" block mb={2}>Position</Text>
<Box display="grid" gridCols={2} gap={2}>
<Box>
<Stack display="grid" gridCols={2} gap={2}>
<Stack>
<Text as="label" size="xs" color="text-gray-500" block mb={1}>X</Text>
<Box
<Stack
as="input"
type="range"
min="0"
@@ -388,10 +387,10 @@ export function LeagueDecalPlacementEditor({
// eslint-disable-next-line gridpilot-rules/component-classification
className="appearance-none cursor-pointer accent-primary-blue"
/>
</Box>
<Box>
</Stack>
<Stack>
<Text as="label" size="xs" color="text-gray-500" block mb={1}>Y</Text>
<Box
<Stack
as="input"
type="range"
min="0"
@@ -405,14 +404,14 @@ export function LeagueDecalPlacementEditor({
// eslint-disable-next-line gridpilot-rules/component-classification
className="appearance-none cursor-pointer accent-primary-blue"
/>
</Box>
</Box>
</Box>
</Stack>
</Stack>
</Stack>
{/* Size */}
<Box mb={4}>
<Stack mb={4}>
<Text as="label" size="xs" color="text-gray-400" block mb={2}>Size</Text>
<Box display="flex" gap={2}>
<Stack display="flex" gap={2}>
<Button
variant="secondary"
onClick={() => handleResize(selectedPlacement.id, 0.9)}
@@ -435,14 +434,14 @@ export function LeagueDecalPlacementEditor({
/>
Larger
</Button>
</Box>
</Box>
</Stack>
</Stack>
{/* Rotation */}
<Box mb={4}>
<Stack mb={4}>
<Text as="label" size="xs" color="text-gray-400" block mb={2}>Rotation: {selectedPlacement.rotation}°</Text>
<Box display="flex" alignItems="center" gap={2}>
<Box
<Stack display="flex" alignItems="center" gap={2}>
<Stack
as="input"
type="range"
min="0"
@@ -464,8 +463,8 @@ export function LeagueDecalPlacementEditor({
>
<Icon icon={RotateCw} size={4} />
</Button>
</Box>
</Box>
</Stack>
</Stack>
</Card>
)}
@@ -484,14 +483,14 @@ export function LeagueDecalPlacementEditor({
</Button>
{/* Help Text */}
<Box p={3} rounded="lg" bg="bg-iron-gray/30" border borderColor="border-charcoal-outline/50">
<Stack p={3} rounded="lg" bg="bg-iron-gray/30" border borderColor="border-charcoal-outline/50">
<Text size="xs" color="text-gray-500" block>
<Text weight="bold" color="text-gray-400">Tip:</Text> Main sponsor gets the largest, most prominent placement.
Secondary sponsors get smaller positions. These decals will be burned onto all driver liveries.
</Text>
</Box>
</Stack>
</Stack>
</Box>
</Stack>
</Stack>
);
}

View File

@@ -1,10 +1,9 @@
'use client';
import type { LeagueConfigFormModel } from '@/lib/types/LeagueConfigFormModel';
import { Box } from '@/ui/Box';
import { Stack } from '@/ui/Stack';
import { Heading } from '@/ui/Heading';
import { Icon } from '@/ui/Icon';
import { Stack } from '@/ui/Stack';
import { Text } from '@/ui/Text';
import { Check, HelpCircle, TrendingDown, X, Zap } from 'lucide-react';
import React, { useEffect, useRef, useState } from 'react';
@@ -83,7 +82,7 @@ function InfoFlyout({ isOpen, onClose, title, children, anchorRef }: InfoFlyoutP
if (!isOpen) return null;
return createPortal(
<Box
<Stack
ref={flyoutRef}
position="fixed"
zIndex={50}
@@ -96,7 +95,7 @@ function InfoFlyout({ isOpen, onClose, title, children, anchorRef }: InfoFlyoutP
// eslint-disable-next-line gridpilot-rules/component-classification
style={{ top: position.top, left: position.left, maxHeight: '80vh', overflowY: 'auto' }}
>
<Box
<Stack
display="flex"
alignItems="center"
justifyContent="between"
@@ -112,7 +111,7 @@ function InfoFlyout({ isOpen, onClose, title, children, anchorRef }: InfoFlyoutP
<Icon icon={HelpCircle} size={4} color="text-primary-blue" />
<Text size="sm" weight="semibold" color="text-white">{title}</Text>
</Stack>
<Box
<Stack
as="button"
type="button"
onClick={onClose}
@@ -126,19 +125,19 @@ function InfoFlyout({ isOpen, onClose, title, children, anchorRef }: InfoFlyoutP
hoverBg="bg-charcoal-outline"
>
<Icon icon={X} size={4} color="text-gray-400" />
</Box>
</Box>
<Box p={4}>
</Stack>
</Stack>
<Stack p={4}>
{children}
</Box>
</Box>,
</Stack>
</Stack>,
document.body
);
}
function InfoButton({ onClick, buttonRef }: { onClick: () => void; buttonRef: React.Ref<HTMLButtonElement> }) {
return (
<Box
<Stack
as="button"
ref={buttonRef}
type="button"
@@ -155,7 +154,7 @@ function InfoButton({ onClick, buttonRef }: { onClick: () => void; buttonRef: Re
hoverBg="bg-primary-blue/10"
>
<Icon icon={HelpCircle} size={3.5} />
</Box>
</Stack>
);
}
@@ -174,13 +173,13 @@ function DropRulesMockup() {
const wouldBe = results.reduce((sum, r) => sum + r.pts, 0);
return (
<Box bg="bg-deep-graphite" rounded="lg" p={4}>
<Box display="flex" alignItems="center" gap={2} mb={3} pb={2} borderBottom borderColor="border-charcoal-outline/50" opacity={0.5}>
<Stack bg="bg-deep-graphite" rounded="lg" p={4}>
<Stack display="flex" alignItems="center" gap={2} mb={3} pb={2} borderBottom borderColor="border-charcoal-outline/50" opacity={0.5}>
<Text size="xs" weight="semibold" color="text-white">Best 4 of 6 Results</Text>
</Box>
<Box display="flex" gap={1} mb={3}>
</Stack>
<Stack display="flex" gap={1} mb={3}>
{results.map((r, i) => (
<Box
<Stack
key={i}
flexGrow={1}
p={2}
@@ -209,14 +208,14 @@ function DropRulesMockup() {
>
{r.pts}
</Text>
</Box>
</Stack>
))}
</Box>
<Box display="flex" justifyContent="between" alignItems="center">
</Stack>
<Stack display="flex" justifyContent="between" alignItems="center">
<Text size="xs" color="text-gray-500">Total counted:</Text>
<Text font="mono" weight="semibold" color="text-performance-green" size="xs">{total} pts</Text>
</Box>
<Box display="flex" justifyContent="between" alignItems="center" mt={1}>
</Stack>
<Stack display="flex" justifyContent="between" alignItems="center" mt={1}>
<Text
// eslint-disable-next-line gridpilot-rules/component-classification
style={{ fontSize: '10px' }}
@@ -231,8 +230,8 @@ function DropRulesMockup() {
>
{wouldBe} pts
</Text>
</Box>
</Box>
</Stack>
</Stack>
);
}
@@ -363,18 +362,18 @@ export function LeagueDropSection({
return (
<Stack gap={4}>
{/* Section header */}
<Box display="flex" alignItems="center" gap={3}>
<Box display="flex" h="10" w="10" alignItems="center" justifyContent="center" rounded="xl" bg="bg-primary-blue/10">
<Stack display="flex" alignItems="center" gap={3}>
<Stack display="flex" h="10" w="10" alignItems="center" justifyContent="center" rounded="xl" bg="bg-primary-blue/10">
<Icon icon={TrendingDown} size={5} color="text-primary-blue" />
</Box>
<Box flexGrow={1}>
<Box display="flex" alignItems="center" gap={2}>
</Stack>
<Stack flexGrow={1}>
<Stack display="flex" alignItems="center" gap={2}>
<Heading level={3}>Drop Rules</Heading>
<InfoButton buttonRef={dropInfoRef} onClick={() => setShowDropFlyout(true)} />
</Box>
</Stack>
<Text size="xs" color="text-gray-500">Protect from bad races</Text>
</Box>
</Box>
</Stack>
</Stack>
{/* Drop Rules Flyout */}
<InfoFlyout
@@ -389,7 +388,7 @@ export function LeagueDropSection({
This protects against mechanical failures, bad luck, or occasional poor performances.
</Text>
<Box>
<Stack>
<Text size="xs" weight="bold" color="text-gray-500" transform="uppercase"
// eslint-disable-next-line gridpilot-rules/component-classification
className="tracking-wide"
@@ -401,7 +400,7 @@ export function LeagueDropSection({
Visual Example
</Text>
<DropRulesMockup />
</Box>
</Stack>
<Stack gap={2}>
<Text size="xs" weight="bold" color="text-gray-500" transform="uppercase"
@@ -414,9 +413,9 @@ export function LeagueDropSection({
Drop Strategies
</Text>
<Stack gap={2}>
<Box display="flex" alignItems="start" gap={2} p={2} rounded="lg" bg="bg-deep-graphite" border borderColor="border-charcoal-outline/30">
<Stack display="flex" alignItems="start" gap={2} p={2} rounded="lg" bg="bg-deep-graphite" border borderColor="border-charcoal-outline/30">
<Text size="base"></Text>
<Box>
<Stack>
<Text size="xs" weight="medium" color="text-white" block
// eslint-disable-next-line gridpilot-rules/component-classification
style={{ fontSize: '10px' }}
@@ -429,11 +428,11 @@ export function LeagueDropSection({
>
Every race affects standings. Best for short seasons.
</Text>
</Box>
</Box>
<Box display="flex" alignItems="start" gap={2} p={2} rounded="lg" bg="bg-deep-graphite" border borderColor="border-charcoal-outline/30">
</Stack>
</Stack>
<Stack display="flex" alignItems="start" gap={2} p={2} rounded="lg" bg="bg-deep-graphite" border borderColor="border-charcoal-outline/30">
<Text size="base">🏆</Text>
<Box>
<Stack>
<Text size="xs" weight="medium" color="text-white" block
// eslint-disable-next-line gridpilot-rules/component-classification
style={{ fontSize: '10px' }}
@@ -446,11 +445,11 @@ export function LeagueDropSection({
>
Only your top N races count. Extra races are optional.
</Text>
</Box>
</Box>
<Box display="flex" alignItems="start" gap={2} p={2} rounded="lg" bg="bg-deep-graphite" border borderColor="border-charcoal-outline/30">
</Stack>
</Stack>
<Stack display="flex" alignItems="start" gap={2} p={2} rounded="lg" bg="bg-deep-graphite" border borderColor="border-charcoal-outline/30">
<Text size="base">🗑</Text>
<Box>
<Stack>
<Text size="xs" weight="medium" color="text-white" block
// eslint-disable-next-line gridpilot-rules/component-classification
style={{ fontSize: '10px' }}
@@ -463,15 +462,15 @@ export function LeagueDropSection({
>
Exclude your N worst results. Forgives bad days.
</Text>
</Box>
</Box>
</Stack>
</Stack>
</Stack>
</Stack>
<Box rounded="lg" bg="bg-primary-blue/5" border borderColor="border-primary-blue/20" p={3}>
<Box display="flex" alignItems="start" gap={2}>
<Stack rounded="lg" bg="bg-primary-blue/5" border borderColor="border-primary-blue/20" p={3}>
<Stack display="flex" alignItems="start" gap={2}>
<Icon icon={Zap} size={3.5} color="text-primary-blue" flexShrink={0} mt={0.5} />
<Box>
<Stack>
<Text size="xs" color="text-gray-400"
// eslint-disable-next-line gridpilot-rules/component-classification
style={{ fontSize: '11px' }}
@@ -479,20 +478,20 @@ export function LeagueDropSection({
<Text weight="medium" color="text-primary-blue">Pro tip:</Text> For an 8-round season,
&quot;Best 6&quot; or &quot;Drop 2&quot; are popular choices.
</Text>
</Box>
</Box>
</Box>
</Stack>
</Stack>
</Stack>
</Stack>
</InfoFlyout>
{/* Strategy buttons + N stepper inline */}
<Box display="flex" flexWrap="wrap" alignItems="center" gap={2}>
<Stack display="flex" flexWrap="wrap" alignItems="center" gap={2}>
{DROP_OPTIONS.map((option) => {
const isSelected = dropPolicy.strategy === option.value;
const ruleInfo = DROP_RULE_INFO[option.value];
return (
<Box key={option.value} display="flex" alignItems="center" position="relative">
<Box
<Stack key={option.value} display="flex" alignItems="center" position="relative">
<Stack
as="button"
type="button"
disabled={disabled}
@@ -516,7 +515,7 @@ export function LeagueDropSection({
style={{ borderRightWidth: 0, borderTopRightRadius: 0, borderBottomRightRadius: 0 }}
>
{/* Radio indicator */}
<Box
<Stack
display="flex"
h="4"
w="4"
@@ -530,16 +529,16 @@ export function LeagueDropSection({
transition
>
{isSelected && <Icon icon={Check} size={2.5} color="text-white" />}
</Box>
</Stack>
<Text size="sm">{option.emoji}</Text>
<Text size="sm" weight="medium" color={isSelected ? 'text-white' : 'text-gray-400'}>
{option.label}
</Text>
</Box>
</Stack>
{/* Info button - separate from main button */}
<Box
<Stack
as="button"
ref={(el: HTMLButtonElement | null) => { dropRuleRefs.current[option.value] = el; }}
type="button"
@@ -569,7 +568,7 @@ export function LeagueDropSection({
// eslint-disable-next-line gridpilot-rules/component-classification
className="hover:text-primary-blue transition-colors"
/>
</Box>
</Stack>
{/* Drop Rule Info Flyout */}
<InfoFlyout
@@ -594,18 +593,18 @@ export function LeagueDropSection({
</Text>
<Stack gap={1.5}>
{ruleInfo.details.map((detail, idx) => (
<Box key={idx} display="flex" alignItems="start" gap={2}>
<Stack key={idx} display="flex" alignItems="start" gap={2}>
<Icon icon={Check} size={3} color="text-performance-green" flexShrink={0} mt={0.5} />
<Text size="xs" color="text-gray-400">{detail}</Text>
</Box>
</Stack>
))}
</Stack>
</Stack>
<Box rounded="lg" bg="bg-deep-graphite" border borderColor="border-charcoal-outline/30" p={3}>
<Box display="flex" alignItems="center" gap={2}>
<Stack rounded="lg" bg="bg-deep-graphite" border borderColor="border-charcoal-outline/30" p={3}>
<Stack display="flex" alignItems="center" gap={2}>
<Text size="base">{option.emoji}</Text>
<Box>
<Stack>
<Text size="xs" color="text-gray-400" block
// eslint-disable-next-line gridpilot-rules/component-classification
style={{ fontSize: '10px' }}
@@ -613,20 +612,20 @@ export function LeagueDropSection({
Example
</Text>
<Text size="xs" weight="medium" color="text-white" block>{ruleInfo.example}</Text>
</Box>
</Box>
</Box>
</Stack>
</Stack>
</Stack>
</Stack>
</InfoFlyout>
</Box>
</Stack>
);
})}
{/* N Stepper - only show when needed */}
{needsN && (
<Box display="flex" alignItems="center" gap={1} ml={2}>
<Stack display="flex" alignItems="center" gap={1} ml={2}>
<Text size="xs" color="text-gray-500" mr={1}>N =</Text>
<Box
<Stack
as="button"
type="button"
disabled={disabled || (dropPolicy.n ?? 1) <= 1}
@@ -647,11 +646,11 @@ export function LeagueDropSection({
opacity={disabled || (dropPolicy.n ?? 1) <= 1 ? 0.4 : 1}
>
</Box>
<Box display="flex" h="7" w="10" alignItems="center" justifyContent="center" rounded="md" bg="bg-iron-gray/50" border borderColor="border-charcoal-outline/50">
</Stack>
<Stack display="flex" h="7" w="10" alignItems="center" justifyContent="center" rounded="md" bg="bg-iron-gray/50" border borderColor="border-charcoal-outline/50">
<Text size="sm" weight="semibold" color="text-white">{dropPolicy.n ?? 1}</Text>
</Box>
<Box
</Stack>
<Stack
as="button"
type="button"
disabled={disabled}
@@ -672,10 +671,10 @@ export function LeagueDropSection({
opacity={disabled ? 0.4 : 1}
>
+
</Box>
</Box>
</Stack>
</Stack>
)}
</Box>
</Stack>
{/* Explanation text */}
<Text size="xs" color="text-gray-500" block>

View File

@@ -1,9 +1,8 @@
import { ReactNode } from 'react';
import { Box } from '@/ui/Box';
import { Heading } from '@/ui/Heading';
import { Stack } from '@/ui/Stack';
import { Heading } from '@/ui/Heading';
import { Text } from '@/ui/Text';
import { Image } from '@/ui/Image';
@@ -23,10 +22,10 @@ export function LeagueHeader({
statusContent,
}: LeagueHeaderProps) {
return (
<Box mb={8}>
<Box display="flex" alignItems="center" justifyContent="between" mb={6}>
<Stack mb={8}>
<Stack display="flex" alignItems="center" justifyContent="between" mb={6}>
<Stack direction="row" align="center" gap={4}>
<Box h="16" w="16" rounded="xl" overflow="hidden" border style={{ borderColor: 'rgba(38, 38, 38, 0.8)', backgroundColor: '#1a1d23' }} shadow="lg">
<Stack h="16" w="16" rounded="xl" overflow="hidden" border style={{ borderColor: 'rgba(38, 38, 38, 0.8)', backgroundColor: '#1a1d23' }} shadow="lg">
<Image
src={logoUrl}
alt={`${name} logo`}
@@ -36,9 +35,9 @@ export function LeagueHeader({
fullHeight
objectFit="cover"
/>
</Box>
<Box>
<Box display="flex" alignItems="center" gap={3} mb={1}>
</Stack>
<Stack>
<Stack display="flex" alignItems="center" gap={3} mb={1}>
<Heading level={1}>
{name}
{sponsorContent && (
@@ -48,15 +47,15 @@ export function LeagueHeader({
)}
</Heading>
{statusContent}
</Box>
</Stack>
{description && (
<Text color="text-gray-400" size="sm" maxWidth="xl" block>
{description}
</Text>
)}
</Box>
</Stack>
</Stack>
</Box>
</Box>
</Stack>
</Stack>
);
}

View File

@@ -1,14 +1,13 @@
'use client';
import React from 'react';
import { Box } from '@/ui/Box';
import { Stack } from '@/ui/Stack';
import { Text } from '@/ui/Text';
import { Heading } from '@/ui/Heading';
import { Surface } from '@/ui/Surface';
import { Icon } from '@/ui/Icon';
import { Trophy, Users, Timer, Activity, type LucideIcon } from 'lucide-react';
import type { LeagueDetailViewData } from '@/lib/view-data/LeagueDetailViewData';
import { Stack } from '@/ui/Stack';
import { Card } from '@/ui/Card';
interface LeagueHeaderPanelProps {
viewData: LeagueDetailViewData;
@@ -16,24 +15,24 @@ interface LeagueHeaderPanelProps {
export function LeagueHeaderPanel({ viewData }: LeagueHeaderPanelProps) {
return (
<Surface variant="dark" border rounded="lg" padding={6} position="relative" overflow="hidden">
<Card variant="outline" p={6} position="relative" overflow="hidden" className="bg-graphite-black">
{/* Background Accent */}
<Box
<Stack
position="absolute"
top={0}
right={0}
w="300px"
h="100%"
h="full"
bg="bg-gradient-to-l from-primary-blue/5 to-transparent"
pointerEvents="none"
/>
>{null}</Stack>
<Stack direction={{ base: 'col', md: 'row' }} justify="between" align="center" gap={6}>
<Stack gap={2}>
<Stack direction="row" align="center" gap={3}>
<Box p={2} bg="bg-primary-blue/10" rounded="md" border borderColor="border-primary-blue/20">
<Stack p={2} bg="bg-primary-blue/10" rounded="md" border borderColor="border-primary-blue/20">
<Icon icon={Trophy} size={6} color="text-primary-blue" />
</Box>
</Stack>
<Heading level={1} letterSpacing="tight">
{viewData.name}
</Heading>
@@ -64,7 +63,7 @@ export function LeagueHeaderPanel({ viewData }: LeagueHeaderPanelProps) {
/>
</Stack>
</Stack>
</Surface>
</Card>
);
}
@@ -73,7 +72,7 @@ function StatItem({ icon, label, value, color }: { icon: LucideIcon, label: stri
<Stack gap={1}>
<Stack direction="row" align="center" gap={1.5}>
<Icon icon={icon} size={3.5} color="text-gray-500" />
<Text size="xs" color="text-gray-500" weight="medium" letterSpacing="wider" display="block">
<Text size="xs" color="text-gray-500" weight="medium" letterSpacing="wider" block>
{label.toUpperCase()}
</Text>
</Stack>

View File

@@ -1,11 +1,8 @@
import { Box } from '@/ui/Box';
import { Button } from '@/ui/Button';
import { Link } from '@/ui/Link';
import { Stack } from '@/ui/Stack';
import { Surface } from '@/ui/Surface';
import { Text } from '@/ui/Text';
import { Stack } from '@/ui/Stack';
import { Card } from '@/ui/Card';
interface League {
leagueId: string;
@@ -21,40 +18,40 @@ interface LeagueListItemProps {
export function LeagueListItem({ league, isAdmin }: LeagueListItemProps) {
return (
<Surface
variant="dark"
rounded="lg"
border
padding={4}
style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', borderColor: '#262626' }}
<Card
variant="outline"
p={4}
className="bg-graphite-black border-[#262626]"
>
<Box style={{ flex: 1, minWidth: 0 }}>
<Text weight="medium" color="text-white" block>{league.name}</Text>
<Text size="xs" color="text-gray-400" block mt={1} style={{ display: '-webkit-box', WebkitLineClamp: 2, WebkitBoxOrient: 'vertical', overflow: 'hidden' }}>
{league.description}
</Text>
{league.membershipRole && (
<Text size="xs" color="text-gray-500" block mt={1}>
Your role:{' '}
<Text color="text-gray-400" style={{ textTransform: 'capitalize' }}>{league.membershipRole}</Text>
<Stack direction="row" align="center" justify="between" fullWidth>
<Stack style={{ flex: 1, minWidth: 0 }}>
<Text weight="medium" color="text-white" block>{league.name}</Text>
<Text size="xs" color="text-gray-400" block mt={1} style={{ display: '-webkit-box', WebkitLineClamp: 2, WebkitBoxOrient: 'vertical', overflow: 'hidden' }}>
{league.description}
</Text>
)}
</Box>
<Stack direction="row" align="center" gap={2} style={{ marginLeft: '1rem' }}>
<Link
href={`/leagues/${league.leagueId}`}
variant="ghost"
>
<Text size="sm" color="text-gray-300">View</Text>
</Link>
{isAdmin && (
<Link href={`/leagues/${league.leagueId}?tab=admin`} variant="ghost">
<Button variant="primary" size="sm">
Manage
</Button>
{league.membershipRole && (
<Text size="xs" color="text-gray-500" block mt={1}>
Your role:{' '}
<Text color="text-gray-400" style={{ textTransform: 'capitalize' }}>{league.membershipRole}</Text>
</Text>
)}
</Stack>
<Stack direction="row" align="center" gap={2} style={{ marginLeft: '1rem' }}>
<Link
href={`/leagues/${league.leagueId}`}
variant="ghost"
>
<Text size="sm" color="text-gray-300">View</Text>
</Link>
)}
{isAdmin && (
<Link href={`/leagues/${league.leagueId}?tab=admin`} variant="ghost">
<Button variant="primary" size="sm">
Manage
</Button>
</Link>
)}
</Stack>
</Stack>
</Surface>
</Card>
);
}

View File

@@ -3,9 +3,8 @@
import { useState } from 'react';
import { Button } from '@/ui/Button';
import { Input } from '@/ui/Input';
import { Box } from '@/ui/Box';
import { Text } from '@/ui/Text';
import { Stack } from '@/ui/Stack';
import { Text } from '@/ui/Text';
import { Heading } from '@/ui/Heading';
import { Icon } from '@/ui/Icon';
import { DollarSign, Calendar, User, TrendingUp } from 'lucide-react';
@@ -76,13 +75,13 @@ export function LeagueMembershipFeesSection({
return (
<Stack gap={6}>
{/* Header */}
<Box display="flex" alignItems="center" justifyContent="between">
<Box>
<Stack display="flex" alignItems="center" justifyContent="between">
<Stack>
<Heading level={3} fontSize="lg" weight="semibold" color="text-white">Membership Fees</Heading>
<Text size="sm" color="text-gray-400" mt={1} block>
Charge drivers for league participation
</Text>
</Box>
</Stack>
{!feeConfig.enabled && !readOnly && (
<Button
variant="primary"
@@ -91,18 +90,18 @@ export function LeagueMembershipFeesSection({
Enable Fees
</Button>
)}
</Box>
</Stack>
{!feeConfig.enabled ? (
<Box textAlign="center" py={12} rounded="lg" border borderColor="border-charcoal-outline" bg="bg-iron-gray/30">
<Box w="16" h="16" mx="auto" mb={4} rounded="full" bg="bg-iron-gray/50" display="flex" alignItems="center" justifyContent="center">
<Stack textAlign="center" py={12} rounded="lg" border borderColor="border-charcoal-outline" bg="bg-iron-gray/30">
<Stack w="16" h="16" mx="auto" mb={4} rounded="full" bg="bg-iron-gray/50" display="flex" alignItems="center" justifyContent="center">
<Icon icon={DollarSign} size={8} color="text-gray-500" />
</Box>
</Stack>
<Heading level={4} fontSize="lg" weight="medium" color="text-white" mb={2}>No Membership Fees</Heading>
<Text size="sm" color="text-gray-400" maxWidth="md" mx="auto" block>
This league is free to join. Enable membership fees to charge drivers for participation.
</Text>
</Box>
</Stack>
) : (
<>
{/* Fee Type Selection */}
@@ -110,13 +109,13 @@ export function LeagueMembershipFeesSection({
<Text as="label" size="sm" weight="medium" color="text-gray-300" block>
Fee Type
</Text>
<Box display="grid" gridCols={3} gap={3}>
<Stack display="grid" gridCols={3} gap={3}>
{(['season', 'monthly', 'per_race'] as FeeType[]).map((type) => {
const FeeIcon = type === 'season' ? Calendar : type === 'monthly' ? TrendingUp : User;
const isSelected = feeConfig.type === type;
return (
<Box
<Stack
key={type}
as="button"
type="button"
@@ -137,10 +136,10 @@ export function LeagueMembershipFeesSection({
<Text size="xs" color="text-gray-500" block>
{typeDescriptions[type]}
</Text>
</Box>
</Stack>
);
})}
</Box>
</Stack>
</Stack>
{/* Amount Configuration */}
@@ -150,8 +149,8 @@ export function LeagueMembershipFeesSection({
</Text>
{editing ? (
<Box display="flex" alignItems="center" gap={3}>
<Box flexGrow={1}>
<Stack display="flex" alignItems="center" gap={3}>
<Stack flexGrow={1}>
<Input
type="number"
value={tempAmount}
@@ -160,7 +159,7 @@ export function LeagueMembershipFeesSection({
min="0"
step="0.01"
/>
</Box>
</Stack>
<Button
variant="primary"
onClick={handleSave}
@@ -175,17 +174,17 @@ export function LeagueMembershipFeesSection({
>
Cancel
</Button>
</Box>
</Stack>
) : (
<Box display="flex" alignItems="center" justifyContent="between" p={4} rounded="lg" bg="bg-iron-gray/50" border borderColor="border-charcoal-outline">
<Box>
<Stack display="flex" alignItems="center" justifyContent="between" p={4} rounded="lg" bg="bg-iron-gray/50" border borderColor="border-charcoal-outline">
<Stack>
<Text size="2xl" weight="bold" color="text-white" block>
${feeConfig.amount.toFixed(2)}
</Text>
<Text size="xs" color="text-gray-500" mt={1} block>
{typeLabels[feeConfig.type]}
</Text>
</Box>
</Stack>
{!readOnly && (
<Button
variant="secondary"
@@ -195,49 +194,49 @@ export function LeagueMembershipFeesSection({
Edit Amount
</Button>
)}
</Box>
</Stack>
)}
</Stack>
{/* Revenue Breakdown */}
{feeConfig.amount > 0 && (
<Box display="grid" gridCols={2} gap={4}>
<Box rounded="lg" bg="bg-iron-gray/50" border borderColor="border-charcoal-outline" p={4}>
<Stack display="grid" gridCols={2} gap={4}>
<Stack rounded="lg" bg="bg-iron-gray/50" border borderColor="border-charcoal-outline" p={4}>
<Text size="xs" color="text-gray-400" block mb={1}>Platform Fee (10%)</Text>
<Text size="lg" weight="bold" color="text-warning-amber" block>
-${platformFee.toFixed(2)}
</Text>
</Box>
<Box rounded="lg" bg="bg-iron-gray/50" border borderColor="border-charcoal-outline" p={4}>
</Stack>
<Stack rounded="lg" bg="bg-iron-gray/50" border borderColor="border-charcoal-outline" p={4}>
<Text size="xs" color="text-gray-400" block mb={1}>Net per Driver</Text>
<Text size="lg" weight="bold" color="text-performance-green" block>
${netAmount.toFixed(2)}
</Text>
</Box>
</Box>
</Stack>
</Stack>
)}
{/* Disable Fees */}
{!readOnly && (
<Box pt={4} borderTop borderColor="border-charcoal-outline">
<Stack pt={4} borderTop borderColor="border-charcoal-outline">
<Button
variant="danger"
onClick={() => setFeeConfig({ type: 'season', amount: 0, enabled: false })}
>
Disable Membership Fees
</Button>
</Box>
</Stack>
)}
</>
)}
{/* Alpha Notice */}
<Box rounded="lg" bg="bg-warning-amber/10" border borderColor="border-warning-amber/30" p={4}>
<Stack rounded="lg" bg="bg-warning-amber/10" border borderColor="border-warning-amber/30" p={4}>
<Text size="xs" color="text-gray-400" block>
<Text weight="bold" color="text-warning-amber">Alpha Note:</Text> Membership fee collection is demonstration-only.
In production, fees are collected via payment gateway and deposited to league wallet (minus platform fee).
</Text>
</Box>
</Stack>
</Stack>
);
}

View File

@@ -1,7 +1,6 @@
'use client';
import React from 'react';
import { Box } from '@/ui/Box';
import { Stack } from '@/ui/Stack';
import { Link } from '@/ui/Link';
@@ -18,7 +17,7 @@ interface LeagueNavTabsProps {
export function LeagueNavTabs({ tabs, currentPathname }: LeagueNavTabsProps) {
return (
<Box as="nav" borderBottom borderColor="zinc-800" mb={6}>
<Stack as="nav" borderBottom borderColor="zinc-800" mb={6}>
<Stack as="ul" direction="row" gap={8} overflow="auto" hideScrollbar>
{tabs.map((tab) => {
const isActive = tab.exact
@@ -26,7 +25,7 @@ export function LeagueNavTabs({ tabs, currentPathname }: LeagueNavTabsProps) {
: currentPathname.startsWith(tab.href);
return (
<Box as="li" key={tab.href} position="relative">
<Stack as="li" key={tab.href} position="relative">
<Link
href={tab.href}
variant="ghost"
@@ -41,7 +40,7 @@ export function LeagueNavTabs({ tabs, currentPathname }: LeagueNavTabsProps) {
{tab.label}
</Link>
{isActive && (
<Box
<Stack
position="absolute"
bottom="0"
left="0"
@@ -50,10 +49,10 @@ export function LeagueNavTabs({ tabs, currentPathname }: LeagueNavTabsProps) {
bg="bg-blue-500"
/>
)}
</Box>
</Stack>
);
})}
</Stack>
</Box>
</Stack>
);
}

View File

@@ -4,11 +4,10 @@ import { Button } from '@/ui/Button';
import { UserCog } from 'lucide-react';
import { LeagueSettingsViewModel } from '@/lib/view-models/LeagueSettingsViewModel';
import { DriverViewModel } from '@/lib/view-models/DriverViewModel';
import { Box } from '@/ui/Box';
import { Stack } from '@/ui/Stack';
import { Text } from '@/ui/Text';
import { Heading } from '@/ui/Heading';
import { Surface } from '@/ui/Surface';
import { Card } from '@/ui/Card';
import { Select } from '@/ui/Select';
import { Icon } from '@/ui/Icon';
@@ -48,10 +47,10 @@ export function LeagueOwnershipTransfer({
return (
<Stack gap={4}>
{/* League Owner */}
<Surface variant="muted" rounded="xl" border padding={5}>
<Box mb={3}>
<Card variant="outline" p={5} rounded="xl" className="bg-panel-gray/40">
<Stack mb={3}>
<Heading level={3}>League Owner</Heading>
</Box>
</Stack>
{ownerSummary ? (
<DriverSummaryPill
driver={new DriverViewModel({
@@ -69,20 +68,20 @@ export function LeagueOwnershipTransfer({
) : (
<Text size="sm" color="text-gray-500">Loading owner details...</Text>
)}
</Surface>
</Card>
{/* Transfer Ownership - Owner Only */}
{settings.league.ownerId === currentDriverId && settings.members.length > 0 && (
<Surface variant="muted" rounded="xl" border padding={5}>
<Card variant="outline" p={5} rounded="xl" className="bg-panel-gray/40">
<Stack direction="row" align="center" gap={2} mb={3}>
<Icon icon={UserCog} size={4} color="text-gray-400" />
<Heading level={3}>Transfer Ownership</Heading>
</Stack>
<Box mb={4}>
<Stack mb={4}>
<Text size="xs" color="text-gray-500">
Transfer league ownership to another active member. You will become an admin.
</Text>
</Box>
</Stack>
{!showTransferDialog ? (
<Button
@@ -126,7 +125,7 @@ export function LeagueOwnershipTransfer({
</Stack>
</Stack>
)}
</Surface>
</Card>
)}
</Stack>
);

View File

@@ -20,11 +20,12 @@ import {
} from 'lucide-react';
import type { LeagueConfigFormModel } from '@/lib/types/LeagueConfigFormModel';
import type { LeagueScoringPresetViewModel } from '@/lib/view-models/LeagueScoringPresetViewModel';
import { Box } from '@/ui/Box';
import { Text } from '@/ui/Text';
import { Stack } from '@/ui/Stack';
import { Text } from '@/ui/Text';
import { Heading } from '@/ui/Heading';
import { Icon } from '@/ui/Icon';
import { Card } from '@/ui/Card';
import { Grid } from '@/ui/Grid';
interface LeagueReviewSummaryProps {
form: LeagueConfigFormModel;
@@ -46,17 +47,17 @@ function ReviewCard({
children: React.ReactNode;
}) {
return (
<Box rounded="xl" bg="bg-iron-gray/50" border borderColor="border-charcoal-outline/40" p={4}>
<Card rounded="xl" className="bg-iron-gray/50 border-charcoal-outline/40" p={4}>
<Stack gap={3}>
<Box display="flex" alignItems="center" gap={3}>
<Box display="flex" h="9" w="9" alignItems="center" justifyContent="center" rounded="lg" bg={bgColor}>
<Stack direction="row" align="center" gap={3}>
<Stack h="9" w="9" align="center" justify="center" rounded="lg" bg={bgColor}>
<Icon icon={icon} size={4} color={iconColor} />
</Box>
</Stack>
<Heading level={3} fontSize="sm" weight="semibold" color="text-white">{title}</Heading>
</Box>
</Stack>
{children}
</Stack>
</Box>
</Card>
);
}
@@ -73,21 +74,19 @@ function InfoRow({
valueClass?: string;
}) {
return (
<Box display="flex" alignItems="center" justifyContent="between" py={2} borderBottom borderColor="border-charcoal-outline/20"
// eslint-disable-next-line gridpilot-rules/component-classification
<Stack direction="row" align="center" justify="between" py={2} borderBottom borderColor="border-charcoal-outline/20"
className="last:border-0"
>
<Box display="flex" alignItems="center" gap={2}>
<Stack direction="row" align="center" gap={2}>
{icon && <Icon icon={icon} size={3.5} color="text-gray-500" />}
<Text size="xs" color="text-gray-500">{label}</Text>
</Box>
</Stack>
<Text size="sm" weight="medium" color="text-white"
// eslint-disable-next-line gridpilot-rules/component-classification
className={valueClass}
>
{value}
</Text>
</Box>
</Stack>
);
}
@@ -186,34 +185,32 @@ export function LeagueReviewSummary({ form, presets }: LeagueReviewSummaryProps)
{/* League Summary */}
<Stack gap={3}>
<Heading level={3} fontSize="sm" weight="semibold" color="text-gray-300">League summary</Heading>
<Box position="relative" rounded="2xl" bg="bg-gradient-to-br from-primary-blue/20 via-iron-gray to-iron-gray" border borderColor="border-primary-blue/30" p={6} overflow="hidden">
<Stack position="relative" rounded="2xl" bg="bg-gradient-to-br from-primary-blue/20 via-iron-gray to-iron-gray" border borderColor="border-primary-blue/30" p={6} overflow="hidden">
{/* Background decoration */}
<Box position="absolute" top="0" right="0" w="32" h="32" bg="bg-primary-blue/10" rounded="full"
// eslint-disable-next-line gridpilot-rules/component-classification
<Stack position="absolute" top={0} right={0} w="32" h="32" bg="bg-primary-blue/10" rounded="full"
className="blur-3xl"
/>
<Box position="absolute" bottom="0" left="0" w="24" h="24" bg="bg-neon-aqua/5" rounded="full"
// eslint-disable-next-line gridpilot-rules/component-classification
>{null}</Stack>
<Stack position="absolute" bottom="0" left="0" w="24" h="24" bg="bg-neon-aqua/5" rounded="full"
className="blur-2xl"
/>
>{null}</Stack>
<Box position="relative" display="flex" alignItems="start" gap={4}>
<Box display="flex" h="14" w="14" alignItems="center" justifyContent="center" rounded="2xl" bg="bg-primary-blue/20" border borderColor="border-primary-blue/30" flexShrink={0}>
<Stack position="relative" direction="row" align="start" gap={4}>
<Stack h="14" w="14" align="center" justify="center" rounded="2xl" bg="bg-primary-blue/20" border borderColor="border-primary-blue/30" flexShrink={0}>
<Icon icon={Rocket} size={7} color="text-primary-blue" />
</Box>
<Box flexGrow={1} minWidth="0">
</Stack>
<Stack flexGrow={1} style={{ minWidth: 0 }}>
<Heading level={2} fontSize="xl" weight="bold" color="text-white" mb={1} truncate>
{basics.name || 'Your New League'}
</Heading>
<Text size="sm" color="text-gray-400" mb={3} block>
{basics.description || 'Ready to launch your racing series!'}
</Text>
<Box display="flex" flexWrap="wrap" alignItems="center" gap={3}>
<Stack direction="row" wrap align="center" gap={3}>
{/* Ranked/Unranked Badge */}
<Box
<Stack
as="span"
display="inline-flex"
alignItems="center"
direction="row"
align="center"
gap={1.5}
rounded="full"
px={3}
@@ -226,31 +223,30 @@ export function LeagueReviewSummary({ form, presets }: LeagueReviewSummaryProps)
<Icon icon={isRanked ? Trophy : Users} size={3} />
<Text weight="semibold" size="xs">{visibilityLabel}</Text>
<Text
// eslint-disable-next-line gridpilot-rules/component-classification
style={{ fontSize: '10px' }}
opacity={0.7}
>
{visibilityDescription}
</Text>
</Box>
<Box as="span" display="inline-flex" alignItems="center" gap={1.5} rounded="full" bg="bg-charcoal-outline/50" px={3} py={1} color="text-gray-300">
</Stack>
<Stack as="span" direction="row" align="center" gap={1.5} rounded="full" bg="bg-charcoal-outline/50" px={3} py={1} color="text-gray-300">
<Icon icon={Gamepad2} size={3} />
<Text size="xs" weight="medium">iRacing</Text>
</Box>
<Box as="span" display="inline-flex" alignItems="center" gap={1.5} rounded="full" bg="bg-charcoal-outline/50" px={3} py={1} color="text-gray-300">
</Stack>
<Stack as="span" direction="row" align="center" gap={1.5} rounded="full" bg="bg-charcoal-outline/50" px={3} py={1} color="text-gray-300">
<Icon icon={structure.mode === 'solo' ? User : UsersRound} size={3} />
<Text size="xs" weight="medium">{modeLabel}</Text>
</Box>
</Box>
</Box>
</Box>
</Box>
</Stack>
</Stack>
</Stack>
</Stack>
</Stack>
</Stack>
{/* Season Summary */}
<Stack gap={3}>
<Heading level={3} fontSize="sm" weight="semibold" color="text-gray-300">First season summary</Heading>
<Box display="flex" flexWrap="wrap" alignItems="center" gap={2}>
<Stack direction="row" wrap align="center" gap={2}>
<Text size="xs" color="text-gray-400">{seasonName || 'First season of this league'}</Text>
{seasonStartLabel && (
<>
@@ -266,51 +262,51 @@ export function LeagueReviewSummary({ form, presets }: LeagueReviewSummaryProps)
)}
<Text size="xs" color="text-gray-400"></Text>
<Text size="xs" color="text-gray-400">Stewarding: {stewardingLabel}</Text>
</Box>
</Stack>
{/* Stats Grid */}
<Box display="grid" gridCols={{ base: 2, md: 4 }} gap={3}>
<Grid cols={2} mdCols={4} gap={3}>
{/* Capacity */}
<Box rounded="xl" bg="bg-iron-gray/50" border borderColor="border-charcoal-outline/40" p={4} textAlign="center">
<Box display="flex" h="10" w="10" alignItems="center" justifyContent="center" rounded="lg" bg="bg-primary-blue/10" mx="auto" mb={2}>
<Card rounded="xl" className="bg-iron-gray/50 border-charcoal-outline/40 text-center" p={4}>
<Stack h="10" w="10" align="center" justify="center" rounded="lg" bg="bg-primary-blue/10" mx="auto" mb={2}>
<Icon icon={Users} size={5} color="text-primary-blue" />
</Box>
</Stack>
<Text size="2xl" weight="bold" color="text-white" block>{capacityValue}</Text>
<Text size="xs" color="text-gray-500" block>{capacityLabel}</Text>
</Box>
</Card>
{/* Rounds */}
<Box rounded="xl" bg="bg-iron-gray/50" border borderColor="border-charcoal-outline/40" p={4} textAlign="center">
<Box display="flex" h="10" w="10" alignItems="center" justifyContent="center" rounded="lg" bg="bg-performance-green/10" mx="auto" mb={2}>
<Card rounded="xl" className="bg-iron-gray/50 border-charcoal-outline/40 text-center" p={4}>
<Stack h="10" w="10" align="center" justify="center" rounded="lg" bg="bg-performance-green/10" mx="auto" mb={2}>
<Icon icon={Flag} size={5} color="text-performance-green" />
</Box>
</Stack>
<Text size="2xl" weight="bold" color="text-white" block>{timings.roundsPlanned ?? '—'}</Text>
<Text size="xs" color="text-gray-500" block>rounds</Text>
</Box>
</Card>
{/* Weekend Duration */}
<Box rounded="xl" bg="bg-iron-gray/50" border borderColor="border-charcoal-outline/40" p={4} textAlign="center">
<Box display="flex" h="10" w="10" alignItems="center" justifyContent="center" rounded="lg" bg="bg-warning-amber/10" mx="auto" mb={2}>
<Card rounded="xl" className="bg-iron-gray/50 border-charcoal-outline/40 text-center" p={4}>
<Stack h="10" w="10" align="center" justify="center" rounded="lg" bg="bg-warning-amber/10" mx="auto" mb={2}>
<Icon icon={Timer} size={5} color="text-warning-amber" />
</Box>
</Stack>
<Text size="2xl" weight="bold" color="text-white" block>{totalWeekendMinutes > 0 ? `${totalWeekendMinutes}` : '—'}</Text>
<Text size="xs" color="text-gray-500" block>min/weekend</Text>
</Box>
</Card>
{/* Championships */}
<Box rounded="xl" bg="bg-iron-gray/50" border borderColor="border-charcoal-outline/40" p={4} textAlign="center">
<Box display="flex" h="10" w="10" alignItems="center" justifyContent="center" rounded="lg" bg="bg-neon-aqua/10" mx="auto" mb={2}>
<Card rounded="xl" className="bg-iron-gray/50 border-charcoal-outline/40 text-center" p={4}>
<Stack h="10" w="10" align="center" justify="center" rounded="lg" bg="bg-neon-aqua/10" mx="auto" mb={2}>
<Icon icon={Award} size={5} color="text-neon-aqua" />
</Box>
</Stack>
<Text size="2xl" weight="bold" color="text-white" block>
{[championships.enableDriverChampionship, championships.enableTeamChampionship, championships.enableNationsChampionship, championships.enableTrophyChampionship].filter(Boolean).length}
</Text>
<Text size="xs" color="text-gray-500" block>championships</Text>
</Box>
</Box>
</Card>
</Grid>
</Stack>
{/* Detail Cards Grid */}
<Box display="grid" gridCols={{ base: 1, md: 2 }} gap={4}>
<Grid cols={1} mdCols={2} gap={4}>
{/* Schedule Card */}
<ReviewCard icon={Calendar} title="Race Weekend">
<Stack gap={1}>
@@ -329,91 +325,90 @@ export function LeagueReviewSummary({ form, presets }: LeagueReviewSummaryProps)
<ReviewCard icon={Trophy} iconColor="text-warning-amber" bgColor="bg-warning-amber/10" title="Scoring System">
<Stack gap={3}>
{/* Scoring Preset */}
<Box display="flex" alignItems="center" gap={3} p={3} rounded="lg" bg="bg-deep-graphite" border borderColor="border-charcoal-outline/30">
<Stack direction="row" align="center" gap={3} p={3} rounded="lg" bg="bg-deep-graphite" border borderColor="border-charcoal-outline/30">
<Text size="2xl">{getScoringEmoji()}</Text>
<Box flexGrow={1} minWidth="0">
<Stack flexGrow={1} style={{ minWidth: 0 }}>
<Text size="sm" weight="medium" color="text-white" block>{preset?.name ?? 'Custom'}</Text>
<Text size="xs" color="text-gray-500" block>{preset?.sessionSummary ?? 'Custom scoring enabled'}</Text>
</Box>
</Stack>
{scoring.customScoringEnabled && (
<Box as="span" px={2} py={0.5} rounded="sm" bg="bg-primary-blue/20">
<Stack as="span" px={2} py={0.5} rounded="sm" bg="bg-primary-blue/20">
<Text
// eslint-disable-next-line gridpilot-rules/component-classification
style={{ fontSize: '10px' }}
weight="medium"
color="text-primary-blue"
>
Custom
</Text>
</Box>
</Stack>
)}
</Box>
</Stack>
{/* Drop Rule */}
<Box display="flex" alignItems="center" gap={3} p={3} rounded="lg" bg="bg-deep-graphite" border borderColor="border-charcoal-outline/30">
<Box display="flex" h="8" w="8" alignItems="center" justifyContent="center" rounded="lg" bg="bg-charcoal-outline/50">
<Stack direction="row" align="center" gap={3} p={3} rounded="lg" bg="bg-deep-graphite" border borderColor="border-charcoal-outline/30">
<Stack h="8" w="8" align="center" justify="center" rounded="lg" bg="bg-charcoal-outline/50">
<Text size="base">{dropRuleInfo.emoji}</Text>
</Box>
<Box flexGrow={1} minWidth="0">
</Stack>
<Stack flexGrow={1} style={{ minWidth: 0 }}>
<Text size="sm" weight="medium" color="text-white" block>{dropRuleInfo.label}</Text>
<Text size="xs" color="text-gray-500" block>{dropRuleInfo.description}</Text>
</Box>
</Box>
</Stack>
</Stack>
</Stack>
</ReviewCard>
</Box>
</Grid>
{/* Championships Section */}
<ReviewCard icon={Award} iconColor="text-neon-aqua" bgColor="bg-neon-aqua/10" title="Active Championships">
<Box display="flex" flexWrap="wrap" gap={2}>
<Stack direction="row" wrap gap={2}>
{championships.enableDriverChampionship && (
<Box as="span" display="inline-flex" alignItems="center" gap={1.5} rounded="lg" bg="bg-primary-blue/10" border borderColor="border-primary-blue/20" px={3} py={2}>
<Stack as="span" direction="row" align="center" gap={1.5} rounded="lg" bg="bg-primary-blue/10" border borderColor="border-primary-blue/20" px={3} py={2}>
<Icon icon={Trophy} size={3.5} color="text-primary-blue" />
<Text size="xs" weight="medium" color="text-primary-blue">Driver Championship</Text>
<Icon icon={Check} size={3} color="text-performance-green" />
</Box>
</Stack>
)}
{championships.enableTeamChampionship && (
<Box as="span" display="inline-flex" alignItems="center" gap={1.5} rounded="lg" bg="bg-primary-blue/10" border borderColor="border-primary-blue/20" px={3} py={2}>
<Stack as="span" direction="row" align="center" gap={1.5} rounded="lg" bg="bg-primary-blue/10" border borderColor="border-primary-blue/20" px={3} py={2}>
<Icon icon={Award} size={3.5} color="text-primary-blue" />
<Text size="xs" weight="medium" color="text-primary-blue">Team Championship</Text>
<Icon icon={Check} size={3} color="text-performance-green" />
</Box>
</Stack>
)}
{championships.enableNationsChampionship && (
<Box as="span" display="inline-flex" alignItems="center" gap={1.5} rounded="lg" bg="bg-primary-blue/10" border borderColor="border-primary-blue/20" px={3} py={2}>
<Stack as="span" direction="row" align="center" gap={1.5} rounded="lg" bg="bg-primary-blue/10" border borderColor="border-primary-blue/20" px={3} py={2}>
<Icon icon={Globe} size={3.5} color="text-primary-blue" />
<Text size="xs" weight="medium" color="text-primary-blue">Nations Cup</Text>
<Icon icon={Check} size={3} color="text-performance-green" />
</Box>
</Stack>
)}
{championships.enableTrophyChampionship && (
<Box as="span" display="inline-flex" alignItems="center" gap={1.5} rounded="lg" bg="bg-primary-blue/10" border borderColor="border-primary-blue/20" px={3} py={2}>
<Stack as="span" direction="row" align="center" gap={1.5} rounded="lg" bg="bg-primary-blue/10" border borderColor="border-primary-blue/20" px={3} py={2}>
<Icon icon={Medal} size={3.5} color="text-primary-blue" />
<Text size="xs" weight="medium" color="text-primary-blue">Trophy Championship</Text>
<Icon icon={Check} size={3} color="text-performance-green" />
</Box>
</Stack>
)}
{![championships.enableDriverChampionship, championships.enableTeamChampionship, championships.enableNationsChampionship, championships.enableTrophyChampionship].some(Boolean) && (
<Text size="sm" color="text-gray-500">No championships enabled</Text>
)}
</Box>
</Stack>
</ReviewCard>
{/* Ready to launch message */}
<Box rounded="xl" bg="bg-performance-green/5" border borderColor="border-performance-green/20" p={4}>
<Box display="flex" alignItems="center" gap={3}>
<Box display="flex" h="10" w="10" alignItems="center" justifyContent="center" rounded="lg" bg="bg-performance-green/20">
<Card rounded="xl" className="bg-performance-green/5 border-performance-green/20" p={4}>
<Stack direction="row" align="center" gap={3}>
<Stack h="10" w="10" align="center" justify="center" rounded="lg" bg="bg-performance-green/20">
<Icon icon={Check} size={5} color="text-performance-green" />
</Box>
<Box>
</Stack>
<Stack>
<Text size="sm" weight="medium" color="text-white" block>Ready to launch!</Text>
<Text size="xs" color="text-gray-400" block>
Click &quot;Create League&quot; to launch your racing series. You can modify all settings later.
Click "Create League" to launch your racing series. You can modify all settings later.
</Text>
</Box>
</Box>
</Box>
</Stack>
</Stack>
</Card>
</Stack>
);
}
}

View File

@@ -1,7 +1,6 @@
'use client';
import React from 'react';
import { Box } from '@/ui/Box';
import { Stack } from '@/ui/Stack';
import { Text } from '@/ui/Text';
import { Heading } from '@/ui/Heading';
@@ -19,10 +18,10 @@ interface LeagueRulesPanelProps {
export function LeagueRulesPanel({ rules }: LeagueRulesPanelProps) {
return (
<Box as="section">
<Stack as="section">
<Stack gap={8}>
<Box display="flex" alignItems="start" gap={4} p={4} bg="blue-500/5" border borderColor="blue-500/20">
<Box color="text-blue-500" mt={0.5}><Info size={20} /></Box>
<Stack display="flex" alignItems="start" gap={4} p={4} bg="blue-500/5" border borderColor="blue-500/20">
<Stack color="text-blue-500" mt={0.5}><Info size={20} /></Stack>
<Stack gap={1}>
<Text size="sm" weight="bold" color="text-blue-500" uppercase letterSpacing="0.05em">Code of Conduct</Text>
<Text size="sm" color="text-zinc-400" leading="relaxed">
@@ -30,24 +29,24 @@ export function LeagueRulesPanel({ rules }: LeagueRulesPanelProps) {
Intentional wrecking or abusive behavior will result in immediate disqualification.
</Text>
</Stack>
</Box>
</Stack>
<Box display="grid" responsiveGridCols={{ base: 1, md: 2 }} gap={6}>
<Stack display="grid" responsiveGridCols={{ base: 1, md: 2 }} gap={6}>
{rules.map((rule) => (
<Box as="article" key={rule.id} display="flex" flexDirection="col" gap={3} p={6} border borderColor="zinc-800" bg="zinc-900/30">
<Box display="flex" alignItems="center" gap={3}>
<Box p={2} bg="zinc-800" color="text-zinc-400">
<Stack as="article" key={rule.id} display="flex" flexDirection="col" gap={3} p={6} border borderColor="zinc-800" bg="zinc-900/30">
<Stack display="flex" alignItems="center" gap={3}>
<Stack p={2} bg="zinc-800" color="text-zinc-400">
<Shield size={18} />
</Box>
</Stack>
<Heading level={3} fontSize="md" weight="bold" color="text-white">{rule.title}</Heading>
</Box>
</Stack>
<Text size="sm" color="text-zinc-400" leading="relaxed">
{rule.content}
</Text>
</Box>
</Stack>
))}
</Box>
</Stack>
</Stack>
</Box>
</Stack>
);
}

View File

@@ -10,9 +10,8 @@ import type { LeagueScheduleRaceViewModel } from '@/lib/view-models/LeagueSchedu
import { StateContainer } from '@/components/shared/state/StateContainer';
import { useLeagueSchedule } from "@/hooks/league/useLeagueSchedule";
import { Calendar } from 'lucide-react';
import { Box } from '@/ui/Box';
import { Text } from '@/ui/Text';
import { Stack } from '@/ui/Stack';
import { Text } from '@/ui/Text';
import { Heading } from '@/ui/Heading';
import { Button } from '@/ui/Button';
@@ -102,11 +101,11 @@ export function LeagueSchedule({ leagueId, onRaceClick }: LeagueScheduleProps) {
return (
<Stack gap={4}>
{/* Filter Controls */}
<Box display="flex" alignItems="center" justifyContent="between">
<Stack display="flex" alignItems="center" justifyContent="between">
<Text size="sm" color="text-gray-400">
{displayRaces.length} {displayRaces.length === 1 ? 'race' : 'races'}
</Text>
<Box display="flex" gap={2}>
<Stack display="flex" gap={2}>
<Button
variant={filter === 'upcoming' ? 'primary' : 'secondary'}
size="sm"
@@ -128,17 +127,17 @@ export function LeagueSchedule({ leagueId, onRaceClick }: LeagueScheduleProps) {
>
All ({races.length})
</Button>
</Box>
</Box>
</Stack>
</Stack>
{/* Race List */}
{displayRaces.length === 0 ? (
<Box textAlign="center" py={8}>
<Stack textAlign="center" py={8}>
<Text color="text-gray-400" block mb={2}>No {filter} races</Text>
{filter === 'upcoming' && (
<Text size="sm" color="text-gray-500" block>Schedule your first race to get started</Text>
)}
</Box>
</Stack>
) : (
<Stack gap={3}>
{displayRaces.map((race) => {
@@ -152,7 +151,7 @@ export function LeagueSchedule({ leagueId, onRaceClick }: LeagueScheduleProps) {
registerMutation.isPending || withdrawMutation.isPending;
return (
<Box
<Stack
key={race.id}
p={4}
rounded="lg"
@@ -166,34 +165,34 @@ export function LeagueSchedule({ leagueId, onRaceClick }: LeagueScheduleProps) {
opacity={isPast ? 0.75 : 1}
onClick={() => onRaceClick?.(race.id)}
>
<Box display="flex" alignItems="center" justifyContent="between" gap={4}>
<Box flexGrow={1}>
<Box display="flex" alignItems="center" gap={2} mb={1} flexWrap="wrap">
<Stack display="flex" alignItems="center" justifyContent="between" gap={4}>
<Stack flexGrow={1}>
<Stack display="flex" alignItems="center" gap={2} mb={1} flexWrap="wrap">
<Heading level={3} fontSize="base" weight="medium" color="text-white">{trackLabel}</Heading>
{isUpcoming && !isRegistered && (
<Box as="span" px={2} py={0.5} bg="bg-primary-blue/10" border borderColor="border-primary-blue/30" rounded="sm">
<Stack as="span" px={2} py={0.5} bg="bg-primary-blue/10" border borderColor="border-primary-blue/30" rounded="sm">
<Text size="xs" weight="medium" color="text-primary-blue">Upcoming</Text>
</Box>
</Stack>
)}
{isUpcoming && isRegistered && (
<Box as="span" px={2} py={0.5} bg="bg-green-500/10" border borderColor="border-green-500/30" rounded="sm">
<Stack as="span" px={2} py={0.5} bg="bg-green-500/10" border borderColor="border-green-500/30" rounded="sm">
<Text size="xs" weight="medium" color="text-green-400"> Registered</Text>
</Box>
</Stack>
)}
{isPast && (
<Box as="span" px={2} py={0.5} bg="bg-gray-700/50" border borderColor="border-gray-600/50" rounded="sm">
<Stack as="span" px={2} py={0.5} bg="bg-gray-700/50" border borderColor="border-gray-600/50" rounded="sm">
<Text size="xs" weight="medium" color="text-gray-400">Completed</Text>
</Box>
</Stack>
)}
</Box>
</Stack>
<Text size="sm" color="text-gray-400" block>{carLabel}</Text>
<Box mt={2}>
<Stack mt={2}>
<Text size="xs" color="text-gray-500" transform="uppercase">{sessionTypeLabel}</Text>
</Box>
</Box>
</Stack>
</Stack>
<Box display="flex" alignItems="center" gap={3}>
<Box textAlign="right">
<Stack display="flex" alignItems="center" gap={3}>
<Stack textAlign="right">
<Text color="text-white" weight="medium" block>
{race.scheduledAt.toLocaleDateString('en-US', {
month: 'short',
@@ -210,11 +209,11 @@ export function LeagueSchedule({ leagueId, onRaceClick }: LeagueScheduleProps) {
{isPast && race.status === 'completed' && (
<Text size="xs" color="text-primary-blue" mt={1} block>View Results </Text>
)}
</Box>
</Stack>
{/* Registration Actions */}
{isUpcoming && (
<Box onClick={(e: React.MouseEvent) => e.stopPropagation()}>
<Stack onClick={(e: React.MouseEvent) => e.stopPropagation()}>
{!isRegistered ? (
<Button
variant="primary"
@@ -239,11 +238,11 @@ export function LeagueSchedule({ leagueId, onRaceClick }: LeagueScheduleProps) {
{withdrawMutation.isPending ? 'Withdrawing...' : 'Withdraw'}
</Button>
)}
</Box>
</Stack>
)}
</Box>
</Box>
</Box>
</Stack>
</Stack>
</Stack>
);
})}
</Stack>

View File

@@ -1,7 +1,6 @@
'use client';
import React from 'react';
import { Box } from '@/ui/Box';
import { Stack } from '@/ui/Stack';
import { Text } from '@/ui/Text';
import { Heading } from '@/ui/Heading';
@@ -22,10 +21,10 @@ interface LeagueSchedulePanelProps {
export function LeagueSchedulePanel({ events }: LeagueSchedulePanelProps) {
return (
<Box as="section">
<Stack as="section">
<Stack gap={4}>
{events.map((event) => (
<Box
<Stack
as="article"
key={event.id}
display="flex"
@@ -38,56 +37,56 @@ export function LeagueSchedulePanel({ events }: LeagueSchedulePanelProps) {
hoverBorderColor="zinc-700"
transition
>
<Box display="flex" flexDirection="col" alignItems="center" justifyContent="center" w="16" h="16" borderRight borderColor="zinc-800" pr={6}>
<Stack display="flex" flexDirection="col" alignItems="center" justifyContent="center" w="16" h="16" borderRight borderColor="zinc-800" pr={6}>
<Text size="xs" weight="bold" color="text-zinc-500" uppercase>
{new Date(event.date).toLocaleDateString('en-US', { month: 'short' })}
</Text>
<Text size="2xl" weight="bold" color="text-white">
{new Date(event.date).toLocaleDateString('en-US', { day: 'numeric' })}
</Text>
</Box>
</Stack>
<Box flexGrow={1}>
<Stack flexGrow={1}>
<Heading level={3} fontSize="lg" weight="bold" color="text-white">{event.title}</Heading>
<Stack direction="row" gap={4} mt={1}>
<Box display="flex" alignItems="center" gap={1.5}>
<Box color="text-zinc-600"><MapPin size={14} /></Box>
<Stack display="flex" alignItems="center" gap={1.5}>
<Stack color="text-zinc-600"><MapPin size={14} /></Stack>
<Text size="sm" color="text-zinc-400">{event.trackName}</Text>
</Box>
<Box display="flex" alignItems="center" gap={1.5}>
<Box color="text-zinc-600"><Clock size={14} /></Box>
</Stack>
<Stack display="flex" alignItems="center" gap={1.5}>
<Stack color="text-zinc-600"><Clock size={14} /></Stack>
<Text size="sm" color="text-zinc-400">{event.time}</Text>
</Box>
</Stack>
</Stack>
</Box>
</Stack>
<Box display="flex" alignItems="center" gap={3}>
<Stack display="flex" alignItems="center" gap={3}>
{event.status === 'live' && (
<Box display="flex" alignItems="center" gap={1.5} px={2} py={1} bg="red-500/10" border borderColor="red-500/20">
<Box w="1.5" h="1.5" rounded="full" bg="bg-red-500" animate="pulse" />
<Stack display="flex" alignItems="center" gap={1.5} px={2} py={1} bg="red-500/10" border borderColor="red-500/20">
<Stack w="1.5" h="1.5" rounded="full" bg="bg-red-500" animate="pulse" />
<Text size="xs" weight="bold" color="text-red-500" uppercase letterSpacing="0.05em">
Live
</Text>
</Box>
</Stack>
)}
{event.status === 'upcoming' && (
<Box px={2} py={1} bg="blue-500/10" border borderColor="blue-500/20">
<Stack px={2} py={1} bg="blue-500/10" border borderColor="blue-500/20">
<Text size="xs" weight="bold" color="text-blue-500" uppercase letterSpacing="0.05em">
Upcoming
</Text>
</Box>
</Stack>
)}
{event.status === 'completed' && (
<Box px={2} py={1} bg="zinc-800" border borderColor="zinc-700">
<Stack px={2} py={1} bg="zinc-800" border borderColor="zinc-700">
<Text size="xs" weight="bold" color="text-zinc-500" uppercase letterSpacing="0.05em">
Results
</Text>
</Box>
</Stack>
)}
</Box>
</Box>
</Stack>
</Stack>
))}
</Stack>
</Box>
</Stack>
);
}

View File

@@ -6,9 +6,8 @@ import { createPortal } from 'react-dom';
import type { LeagueScoringPresetViewModel } from '@/lib/view-models/LeagueScoringPresetViewModel';
import type { LeagueConfigFormModel } from '@/lib/types/LeagueConfigFormModel';
import type { CustomPointsConfig } from '@/lib/view-models/ScoringConfigurationViewModel';
import { Box } from '@/ui/Box';
import { Text } from '@/ui/Text';
import { Stack } from '@/ui/Stack';
import { Text } from '@/ui/Text';
import { Heading } from '@/ui/Heading';
import { Grid } from '@/ui/Grid';
import { Surface } from '@/ui/Surface';
@@ -98,7 +97,7 @@ function InfoFlyout({ isOpen, onClose, title, children, anchorRef }: InfoFlyoutP
if (!isOpen) return null;
return createPortal(
<Box
<Stack
ref={flyoutRef}
position="fixed"
zIndex={50}
@@ -113,7 +112,7 @@ function InfoFlyout({ isOpen, onClose, title, children, anchorRef }: InfoFlyoutP
style={{ top: position.top, left: position.left }}
>
{/* Header */}
<Box display="flex" alignItems="center" justifyContent="between" p={4} borderBottom borderColor="border-charcoal-outline" position="sticky" top="0" bg="bg-iron-gray" zIndex={10}>
<Stack display="flex" alignItems="center" justifyContent="between" p={4} borderBottom borderColor="border-charcoal-outline" position="sticky" top="0" bg="bg-iron-gray" zIndex={10}>
<Stack direction="row" align="center" gap={2}>
<Icon icon={HelpCircle} size={4} color="text-primary-blue" />
<Text size="sm" weight="semibold" color="text-white">{title}</Text>
@@ -127,12 +126,12 @@ function InfoFlyout({ isOpen, onClose, title, children, anchorRef }: InfoFlyoutP
>
<Icon icon={X} size={4} color="text-gray-400" />
</Button>
</Box>
</Stack>
{/* Content */}
<Box p={4}>
<Stack p={4}>
{children}
</Box>
</Box>,
</Stack>
</Stack>,
document.body
);
}
@@ -168,7 +167,7 @@ function PointsSystemMockup() {
return (
<Surface variant="dark" rounded="lg" p={4}>
<Stack gap={3}>
<Box display="flex" alignItems="center" justifyContent="between" px={1}>
<Stack display="flex" alignItems="center" justifyContent="between" px={1}>
<Text
// eslint-disable-next-line gridpilot-rules/component-classification
style={{ fontSize: '10px' }}
@@ -189,27 +188,27 @@ function PointsSystemMockup() {
>
Points
</Text>
</Box>
</Stack>
{positions.map((p) => (
<Stack key={p.pos} direction="row" align="center" gap={3}>
<Box w="8" h="8" rounded="lg" bg={p.color} display="flex" alignItems="center" justifyContent="center">
<Stack w="8" h="8" rounded="lg" bg={p.color} display="flex" alignItems="center" justifyContent="center">
<Text size="sm" weight="bold" color={p.pos <= 3 ? 'text-deep-graphite' : 'text-gray-400'}>P{p.pos}</Text>
</Box>
<Box flexGrow={1} h="2" bg="bg-charcoal-outline" rounded="full" overflow="hidden" opacity={0.5}>
<Box
</Stack>
<Stack flexGrow={1} h="2" bg="bg-charcoal-outline" rounded="full" overflow="hidden" opacity={0.5}>
<Stack
h="full"
bg="bg-gradient-to-r from-primary-blue to-neon-aqua"
rounded="full"
// eslint-disable-next-line gridpilot-rules/component-classification
style={{ width: `${(p.pts / 25) * 100}%` }}
/>
</Box>
<Box w="8" textAlign="right">
</Stack>
<Stack w="8" textAlign="right">
<Text size="sm" font="mono" weight="semibold" color="text-white">{p.pts}</Text>
</Box>
</Stack>
</Stack>
))}
<Box display="flex" alignItems="center" justifyContent="center" gap={1} pt={2}>
<Stack display="flex" alignItems="center" justifyContent="center" gap={1} pt={2}>
<Text
// eslint-disable-next-line gridpilot-rules/component-classification
style={{ fontSize: '10px' }}
@@ -224,7 +223,7 @@ function PointsSystemMockup() {
>
down to P10 = 1 point
</Text>
</Box>
</Stack>
</Stack>
</Surface>
);
@@ -244,7 +243,7 @@ function BonusPointsMockup() {
<Surface key={i} variant="muted" border rounded="lg" p={2}>
<Stack direction="row" align="center" gap={3}>
<Text size="xl">{b.emoji}</Text>
<Box flexGrow={1}>
<Stack flexGrow={1}>
<Text size="xs" weight="medium" color="text-white" block>{b.label}</Text>
<Text
// eslint-disable-next-line gridpilot-rules/component-classification
@@ -254,7 +253,7 @@ function BonusPointsMockup() {
>
{b.desc}
</Text>
</Box>
</Stack>
<Text size="sm" font="mono" weight="semibold" color="text-performance-green">{b.pts}</Text>
</Stack>
</Surface>
@@ -273,14 +272,14 @@ function ChampionshipMockup() {
return (
<Surface variant="dark" rounded="lg" p={4}>
<Box display="flex" alignItems="center" gap={2} mb={3} pb={2} borderBottom borderColor="border-charcoal-outline" opacity={0.5}>
<Stack display="flex" alignItems="center" gap={2} mb={3} pb={2} borderBottom borderColor="border-charcoal-outline" opacity={0.5}>
<Icon icon={Trophy} size={4} color="text-yellow-500" />
<Text size="xs" weight="semibold" color="text-white">Driver Championship</Text>
</Box>
</Stack>
<Stack gap={2}>
{standings.map((s) => (
<Stack key={s.pos} direction="row" align="center" gap={2}>
<Box w="6" h="6" rounded="full" display="flex" alignItems="center" justifyContent="center" bg={s.pos === 1 ? 'bg-yellow-500' : 'bg-charcoal-outline'} color={s.pos === 1 ? 'text-deep-graphite' : 'text-gray-400'}>
<Stack w="6" h="6" rounded="full" display="flex" alignItems="center" justifyContent="center" bg={s.pos === 1 ? 'bg-yellow-500' : 'bg-charcoal-outline'} color={s.pos === 1 ? 'text-deep-graphite' : 'text-gray-400'}>
<Text
// eslint-disable-next-line gridpilot-rules/component-classification
style={{ fontSize: '10px' }}
@@ -288,10 +287,10 @@ function ChampionshipMockup() {
>
{s.pos}
</Text>
</Box>
<Box flexGrow={1}>
</Stack>
<Stack flexGrow={1}>
<Text size="xs" color="text-white" truncate block>{s.name}</Text>
</Box>
</Stack>
<Text size="xs" font="mono" weight="semibold" color="text-white">{s.pts}</Text>
{s.delta && (
<Text
@@ -306,7 +305,7 @@ function ChampionshipMockup() {
</Stack>
))}
</Stack>
<Box mt={3} pt={2} borderTop borderColor="border-charcoal-outline" opacity={0.5} textAlign="center">
<Stack mt={3} pt={2} borderTop borderColor="border-charcoal-outline" opacity={0.5} textAlign="center">
<Text
// eslint-disable-next-line gridpilot-rules/component-classification
style={{ fontSize: '10px' }}
@@ -314,7 +313,7 @@ function ChampionshipMockup() {
>
Points accumulated across all races
</Text>
</Box>
</Stack>
</Surface>
);
}
@@ -419,11 +418,11 @@ export function LeagueScoringSection({
const championshipsPanel = <ChampionshipsSection {...championshipsProps} />;
if (patternOnly) {
return <Box>{patternPanel}</Box>;
return <Stack>{patternPanel}</Stack>;
}
if (championshipsOnly) {
return <Box>{championshipsPanel}</Box>;
return <Stack>{championshipsPanel}</Stack>;
}
return (
@@ -572,16 +571,16 @@ export function ScoringPatternSection({
<Stack gap={5}>
{/* Section header */}
<Stack direction="row" align="center" gap={3}>
<Box w="10" h="10" display="flex" alignItems="center" justifyContent="center" rounded="xl" bg="bg-primary-blue" opacity={0.1}>
<Stack w="10" h="10" display="flex" alignItems="center" justifyContent="center" rounded="xl" bg="bg-primary-blue" opacity={0.1}>
<Icon icon={Trophy} size={5} color="text-primary-blue" />
</Box>
<Box flexGrow={1}>
</Stack>
<Stack flexGrow={1}>
<Stack direction="row" align="center" gap={2}>
<Heading level={3}>Points System</Heading>
<InfoButton buttonRef={pointsInfoRef} onClick={() => setShowPointsFlyout(true)} />
</Stack>
<Text size="xs" color="text-gray-500">Choose how points are awarded</Text>
</Box>
</Stack>
</Stack>
{/* Points System Flyout */}
@@ -597,8 +596,8 @@ export function ScoringPatternSection({
which accumulate across the season to determine championship standings.
</Text>
<Box>
<Box mb={2}>
<Stack>
<Stack mb={2}>
<Text
// eslint-disable-next-line gridpilot-rules/component-classification
style={{ fontSize: '10px' }}
@@ -609,9 +608,9 @@ export function ScoringPatternSection({
>
Example: F1-Style Points
</Text>
</Box>
</Stack>
<PointsSystemMockup />
</Box>
</Stack>
<Surface variant="muted" border rounded="lg" p={3}>
<Stack direction="row" align="start" gap={2}>
@@ -640,15 +639,15 @@ export function ScoringPatternSection({
{/* Preset options */}
<Stack gap={2}>
{presets.length === 0 ? (
<Box p={4} border borderStyle="dashed" borderColor="border-charcoal-outline" rounded="lg">
<Stack p={4} border borderStyle="dashed" borderColor="border-charcoal-outline" rounded="lg">
<Text size="sm" color="text-gray-400">Loading presets...</Text>
</Box>
</Stack>
) : (
presets.map((preset) => {
const isSelected = !isCustom && scoring.patternId === preset.id;
const presetInfo = getPresetInfoContent(preset.name);
return (
<Box key={preset.id} position="relative">
<Stack key={preset.id} position="relative">
<Button
variant="ghost"
onClick={() => onChangePatternId?.(preset.id)}
@@ -663,7 +662,7 @@ export function ScoringPatternSection({
`}
>
{/* Radio indicator */}
<Box
<Stack
w="5"
h="5"
display="flex"
@@ -677,33 +676,33 @@ export function ScoringPatternSection({
flexShrink={0}
>
{isSelected && <Icon icon={Check} size={3} color="text-white" />}
</Box>
</Stack>
{/* Emoji */}
<Text size="xl">{getPresetEmoji(preset)}</Text>
{/* Text */}
<Box flexGrow={1}
<Stack flexGrow={1}
// eslint-disable-next-line gridpilot-rules/component-classification
className="min-w-0"
>
<Text size="sm" weight="medium" color="text-white" block>{preset.name}</Text>
<Text size="xs" color="text-gray-500" block>{getPresetDescription(preset)}</Text>
</Box>
</Stack>
{/* Bonus badge */}
{preset.bonusSummary && (
<Box
<Stack
// eslint-disable-next-line gridpilot-rules/component-classification
className="hidden sm:inline-flex items-center gap-1 px-2 py-1 rounded-full bg-charcoal-outline/30 text-[10px] text-gray-400"
>
<Icon icon={Zap} size={3} />
<Text>{preset.bonusSummary}</Text>
</Box>
</Stack>
)}
{/* Info button */}
<Box
<Stack
ref={(el: HTMLElement | null) => { presetInfoRefs.current[preset.id] = el; }}
role="button"
tabIndex={0}
@@ -731,7 +730,7 @@ export function ScoringPatternSection({
flexShrink={0}
>
<Icon icon={HelpCircle} size={3.5} />
</Box>
</Stack>
</Button>
{/* Preset Info Flyout */}
@@ -745,7 +744,7 @@ export function ScoringPatternSection({
<Text size="xs" color="text-gray-400">{presetInfo.description}</Text>
<Stack gap={2}>
<Box>
<Stack>
<Text
// eslint-disable-next-line gridpilot-rules/component-classification
style={{ fontSize: '10px' }}
@@ -756,21 +755,21 @@ export function ScoringPatternSection({
>
Key Features
</Text>
</Box>
<Box as="ul"
</Stack>
<Stack as="ul"
// eslint-disable-next-line gridpilot-rules/component-classification
className="space-y-1.5"
>
{presetInfo.details.map((detail, idx) => (
<Box as="li" key={idx} display="flex" alignItems="start" gap={2}>
<Stack as="li" key={idx} display="flex" alignItems="start" gap={2}>
<Icon icon={Check} size={3} color="text-performance-green"
// eslint-disable-next-line gridpilot-rules/component-classification
className="mt-0.5"
/>
<Text size="xs" color="text-gray-400">{detail}</Text>
</Box>
</Stack>
))}
</Box>
</Stack>
</Stack>
{preset.bonusSummary && (
@@ -792,14 +791,14 @@ export function ScoringPatternSection({
)}
</Stack>
</InfoFlyout>
</Box>
</Stack>
);
})
)}
</Stack>
{/* Custom scoring option */}
<Box w="full"
<Stack w="full"
// eslint-disable-next-line gridpilot-rules/component-classification
className="lg:w-48"
>
@@ -816,7 +815,7 @@ export function ScoringPatternSection({
}
`}
>
<Box
<Stack
w="10"
h="10"
display="flex"
@@ -828,8 +827,8 @@ export function ScoringPatternSection({
transition
>
<Icon icon={Settings} size={5} color={isCustom ? 'text-primary-blue' : 'text-gray-500'} />
</Box>
<Box textAlign="center">
</Stack>
<Stack textAlign="center">
<Text size="sm" weight="medium" color={isCustom ? 'text-white' : 'text-gray-400'} block>Custom</Text>
<Text
// eslint-disable-next-line gridpilot-rules/component-classification
@@ -839,9 +838,9 @@ export function ScoringPatternSection({
>
Define your own
</Text>
</Box>
</Stack>
{isCustom && (
<Box display="flex" alignItems="center" gap={1} px={2} py={0.5} rounded="full" bg="bg-primary-blue" opacity={0.2}>
<Stack display="flex" alignItems="center" gap={1} px={2} py={0.5} rounded="full" bg="bg-primary-blue" opacity={0.2}>
<Icon icon={Check} size={2.5} color="text-primary-blue" />
<Text
// eslint-disable-next-line gridpilot-rules/component-classification
@@ -851,10 +850,10 @@ export function ScoringPatternSection({
>
Active
</Text>
</Box>
</Stack>
)}
</Button>
</Box>
</Stack>
</Grid>
{/* Error message */}
@@ -867,7 +866,7 @@ export function ScoringPatternSection({
<Surface variant="muted" border rounded="xl" p={4}>
<Stack gap={4}>
{/* Header with reset button */}
<Box display="flex" alignItems="center" justifyContent="between">
<Stack display="flex" alignItems="center" justifyContent="between">
<Stack direction="row" align="center" gap={2}>
<Icon icon={Settings} size={4} color="text-primary-blue" />
<Text size="sm" weight="medium" color="text-white">Custom Points Table</Text>
@@ -887,8 +886,8 @@ export function ScoringPatternSection({
They add strategic depth and excitement to your championship.
</Text>
<Box>
<Box mb={2}>
<Stack>
<Stack mb={2}>
<Text
// eslint-disable-next-line gridpilot-rules/component-classification
style={{ fontSize: '10px' }}
@@ -899,9 +898,9 @@ export function ScoringPatternSection({
>
Available Bonuses
</Text>
</Box>
</Stack>
<BonusPointsMockup />
</Box>
</Stack>
<Surface variant="muted" border rounded="lg" p={3}>
<Stack direction="row" align="start" gap={2}>
@@ -935,11 +934,11 @@ export function ScoringPatternSection({
<Text>Reset</Text>
</Stack>
</Button>
</Box>
</Stack>
{/* Race position points */}
<Stack gap={2}>
<Box display="flex" alignItems="center" justifyContent="between">
<Stack display="flex" alignItems="center" justifyContent="between">
<Text size="xs" color="text-gray-400">Finish position points</Text>
<Stack direction="row" align="center" gap={1}>
<Button
@@ -963,9 +962,9 @@ export function ScoringPatternSection({
<Icon icon={Plus} size={3} />
</Button>
</Stack>
</Box>
</Stack>
<Box display="flex" flexWrap="wrap" gap={1}>
<Stack display="flex" flexWrap="wrap" gap={1}>
{customPoints.racePoints.map((pts, idx) => (
<Stack key={idx} align="center">
<Text
@@ -976,7 +975,7 @@ export function ScoringPatternSection({
>
P{idx + 1}
</Text>
<Box display="flex" alignItems="center">
<Stack display="flex" alignItems="center">
<Button
variant="secondary"
size="sm"
@@ -987,7 +986,7 @@ export function ScoringPatternSection({
>
</Button>
<Box w="6" h="5" display="flex" alignItems="center" justifyContent="center" bg="bg-deep-graphite" border borderTop borderBottom borderColor="border-charcoal-outline">
<Stack w="6" h="5" display="flex" alignItems="center" justifyContent="center" bg="bg-deep-graphite" border borderTop borderBottom borderColor="border-charcoal-outline">
<Text
// eslint-disable-next-line gridpilot-rules/component-classification
style={{ fontSize: '10px' }}
@@ -996,7 +995,7 @@ export function ScoringPatternSection({
>
{pts}
</Text>
</Box>
</Stack>
<Button
variant="secondary"
size="sm"
@@ -1007,10 +1006,10 @@ export function ScoringPatternSection({
>
+
</Button>
</Box>
</Stack>
</Stack>
))}
</Box>
</Stack>
</Stack>
{/* Bonus points */}
@@ -1028,7 +1027,7 @@ export function ScoringPatternSection({
>
{bonus.emoji} {bonus.label}
</Text>
<Box display="flex" alignItems="center">
<Stack display="flex" alignItems="center">
<Button
variant="secondary"
size="sm"
@@ -1039,9 +1038,9 @@ export function ScoringPatternSection({
>
</Button>
<Box w="7" h="6" display="flex" alignItems="center" justifyContent="center" bg="bg-deep-graphite" border borderTop borderBottom borderColor="border-charcoal-outline">
<Stack w="7" h="6" display="flex" alignItems="center" justifyContent="center" bg="bg-deep-graphite" border borderTop borderBottom borderColor="border-charcoal-outline">
<Text size="xs" weight="medium" color="text-white">{customPoints[bonus.key]}</Text>
</Box>
</Stack>
<Button
variant="secondary"
size="sm"
@@ -1052,7 +1051,7 @@ export function ScoringPatternSection({
>
+
</Button>
</Box>
</Stack>
</Stack>
))}
</Grid>
@@ -1174,16 +1173,16 @@ export function ChampionshipsSection({
<Stack gap={4}>
{/* Section header */}
<Stack direction="row" align="center" gap={3}>
<Box w="10" h="10" display="flex" alignItems="center" justifyContent="center" rounded="xl" bg="bg-primary-blue" opacity={0.1}>
<Stack w="10" h="10" display="flex" alignItems="center" justifyContent="center" rounded="xl" bg="bg-primary-blue" opacity={0.1}>
<Icon icon={Award} size={5} color="text-primary-blue" />
</Box>
<Box flexGrow={1}>
</Stack>
<Stack flexGrow={1}>
<Stack direction="row" align="center" gap={2}>
<Heading level={3}>Championships</Heading>
<InfoButton buttonRef={champInfoRef} onClick={() => setShowChampFlyout(true)} />
</Stack>
<Text size="xs" color="text-gray-500">What standings to track</Text>
</Box>
</Stack>
</Stack>
{/* Championships Flyout */}
@@ -1199,8 +1198,8 @@ export function ChampionshipsSection({
championship types to run different competitions simultaneously.
</Text>
<Box>
<Box mb={2}>
<Stack>
<Stack mb={2}>
<Text
// eslint-disable-next-line gridpilot-rules/component-classification
style={{ fontSize: '10px' }}
@@ -1211,12 +1210,12 @@ export function ChampionshipsSection({
>
Live Standings Example
</Text>
</Box>
</Stack>
<ChampionshipMockup />
</Box>
</Stack>
<Stack gap={2}>
<Box>
<Stack>
<Text
// eslint-disable-next-line gridpilot-rules/component-classification
style={{ fontSize: '10px' }}
@@ -1227,7 +1226,7 @@ export function ChampionshipsSection({
>
Championship Types
</Text>
</Box>
</Stack>
<Grid cols={2} gap={2}>
{[
{ icon: Trophy, label: 'Driver', desc: 'Individual points' },
@@ -1238,7 +1237,7 @@ export function ChampionshipsSection({
<Surface key={i} variant="dark" border rounded="lg" p={2}>
<Stack direction="row" align="center" gap={2}>
<Icon icon={t.icon} size={3.5} color="text-primary-blue" />
<Box>
<Stack>
<Text
// eslint-disable-next-line gridpilot-rules/component-classification
style={{ fontSize: '10px' }}
@@ -1256,7 +1255,7 @@ export function ChampionshipsSection({
>
{t.desc}
</Text>
</Box>
</Stack>
</Stack>
</Surface>
))}
@@ -1272,7 +1271,7 @@ export function ChampionshipsSection({
const champInfo = CHAMPIONSHIP_INFO[champ.key];
return (
<Box key={champ.key} position="relative">
<Stack key={champ.key} position="relative">
<Button
variant="ghost"
disabled={disabled || !champ.available}
@@ -1289,7 +1288,7 @@ export function ChampionshipsSection({
`}
>
{/* Toggle indicator */}
<Box
<Stack
w="5"
h="5"
display="flex"
@@ -1302,7 +1301,7 @@ export function ChampionshipsSection({
flexShrink={0}
>
{isEnabled && <Icon icon={Check} size={3} color="text-white" />}
</Box>
</Stack>
{/* Icon */}
<Icon icon={champ.icon} size={4} color={isEnabled ? 'text-primary-blue' : 'text-gray-500'}
@@ -1311,7 +1310,7 @@ export function ChampionshipsSection({
/>
{/* Text */}
<Box flexGrow={1}
<Stack flexGrow={1}
// eslint-disable-next-line gridpilot-rules/component-classification
className="min-w-0"
>
@@ -1333,10 +1332,10 @@ export function ChampionshipsSection({
{champ.unavailableHint}
</Text>
)}
</Box>
</Stack>
{/* Info button */}
<Box
<Stack
ref={(el: HTMLElement | null) => { champItemRefs.current[champ.key] = el; }}
role="button"
tabIndex={0}
@@ -1364,7 +1363,7 @@ export function ChampionshipsSection({
flexShrink={0}
>
<Icon icon={HelpCircle} size={3} />
</Box>
</Stack>
</Button>
{/* Championship Item Info Flyout */}
@@ -1379,7 +1378,7 @@ export function ChampionshipsSection({
<Text size="xs" color="text-gray-400">{champInfo.description}</Text>
<Stack gap={2}>
<Box>
<Stack>
<Text
// eslint-disable-next-line gridpilot-rules/component-classification
style={{ fontSize: '10px' }}
@@ -1390,21 +1389,21 @@ export function ChampionshipsSection({
>
How It Works
</Text>
</Box>
<Box as="ul"
</Stack>
<Stack as="ul"
// eslint-disable-next-line gridpilot-rules/component-classification
className="space-y-1.5"
>
{champInfo.details.map((detail, idx) => (
<Box as="li" key={idx} display="flex" alignItems="start" gap={2}>
<Stack as="li" key={idx} display="flex" alignItems="start" gap={2}>
<Icon icon={Check} size={3} color="text-performance-green"
// eslint-disable-next-line gridpilot-rules/component-classification
className="mt-0.5"
/>
<Text size="xs" color="text-gray-400">{detail}</Text>
</Box>
</Stack>
))}
</Box>
</Stack>
</Stack>
{!champ.available && (
@@ -1430,7 +1429,7 @@ export function ChampionshipsSection({
</Stack>
</InfoFlyout>
)}
</Box>
</Stack>
);
})}
</Grid>

View File

@@ -1,7 +1,6 @@
'use client';
import React, { useCallback, useRef, useState } from 'react';
import { Box } from '@/ui/Box';
import { Stack } from '@/ui/Stack';
import { Text } from '@/ui/Text';
import { Heading } from '@/ui/Heading';
@@ -55,20 +54,20 @@ export function LeagueSlider({
if (leagues.length === 0) return null;
return (
<Box mb={10}>
<Stack mb={10}>
{/* Section header */}
<Box display="flex" alignItems="center" justifyContent="between" mb={4}>
<Stack display="flex" alignItems="center" justifyContent="between" mb={4}>
<Stack direction="row" align="center" gap={3}>
<Box display="flex" h={10} w={10} alignItems="center" justifyContent="center" rounded="xl" bg="bg-iron-gray" border borderColor="border-charcoal-outline">
<Stack display="flex" h={10} w={10} alignItems="center" justifyContent="center" rounded="xl" bg="bg-iron-gray" border borderColor="border-charcoal-outline">
<Icon icon={IconComp} size={5} color={iconColor} />
</Box>
<Box>
</Stack>
<Stack>
<Heading level={2}>{title}</Heading>
<Text size="xs" color="text-gray-500">{description}</Text>
</Box>
<Box as="span" ml={2} px={2} py={0.5} rounded="full" fontSize="0.75rem" bg="bg-charcoal-outline/50" color="text-gray-400">
</Stack>
<Stack as="span" ml={2} px={2} py={0.5} rounded="full" fontSize="0.75rem" bg="bg-charcoal-outline/50" color="text-gray-400">
{leagues.length}
</Box>
</Stack>
</Stack>
{/* Navigation arrows */}
@@ -94,11 +93,11 @@ export function LeagueSlider({
<Icon icon={ChevronRight} size={4} />
</Button>
</Stack>
</Box>
</Stack>
{/* Scrollable container with fade edges */}
<Box position="relative">
<Box
<Stack position="relative">
<Stack
position="absolute"
top={0}
bottom={4}
@@ -108,7 +107,7 @@ export function LeagueSlider({
zIndex={10}
pointerEvents="none"
/>
<Box
<Stack
position="absolute"
top={0}
bottom={4}
@@ -119,7 +118,7 @@ export function LeagueSlider({
pointerEvents="none"
/>
<Box
<Stack
ref={scrollRef}
onScroll={checkScrollButtons}
display="flex"
@@ -133,15 +132,15 @@ export function LeagueSlider({
const viewModel = LeagueSummaryViewModelBuilder.build(league);
return (
<Box key={league.id} flexShrink={0} w="320px">
<Stack key={league.id} flexShrink={0} w="320px">
<Link href={routes.league.detail(league.id)} block>
<LeagueCard league={viewModel} />
</Link>
</Box>
</Stack>
);
})}
</Box>
</Box>
</Box>
</Stack>
</Stack>
</Stack>
);
}

View File

@@ -5,10 +5,9 @@ import { useState } from 'react';
import { PendingSponsorshipRequests } from '../sponsors/PendingSponsorshipRequests';
import { Button } from '@/ui/Button';
import { Input } from '@/ui/Input';
import { Box } from '@/ui/Box';
import { Stack } from '@/ui/Stack';
import { Text } from '@/ui/Text';
import { Heading } from '@/ui/Heading';
import { Stack } from '@/ui/Stack';
import { Icon } from '@/ui/Icon';
import { Badge } from '@/ui/Badge';
import { StatBox } from '@/ui/StatBox';
@@ -120,8 +119,8 @@ export function LeagueSponsorshipsSection({
return (
<Stack gap={6}>
{/* Header */}
<Box display="flex" alignItems="center" justifyContent="between">
<Box>
<Stack display="flex" alignItems="center" justifyContent="between">
<Stack>
<Heading level={3}>Sponsorships</Heading>
<Text size="sm" color="text-gray-400" mt={1} block>
Define pricing for sponsor slots in this league. Sponsors pay per season.
@@ -129,20 +128,20 @@ export function LeagueSponsorshipsSection({
<Text size="xs" color="text-gray-500" mt={1} block>
These sponsors are attached to seasons in this league, so you can change partners from season to season.
</Text>
</Box>
</Stack>
{!readOnly && (
<Box display="flex" alignItems="center" gap={2} px={3} py={1.5} rounded="full" bg="bg-primary-blue/10" border borderColor="border-primary-blue/30">
<Stack display="flex" alignItems="center" gap={2} px={3} py={1.5} rounded="full" bg="bg-primary-blue/10" border borderColor="border-primary-blue/30">
<Icon icon={DollarSign} size={4} color="var(--primary-blue)" />
<Text size="xs" weight="medium" color="text-primary-blue">
{availableSlots} slot{availableSlots !== 1 ? 's' : ''} available
</Text>
</Box>
</Stack>
)}
</Box>
</Stack>
{/* Revenue Summary */}
{totalRevenue > 0 && (
<Box display="grid" gridCols={3} gap={4}>
<Stack display="grid" gridCols={3} gap={4}>
<StatBox
icon={DollarSign}
label="Total Revenue"
@@ -161,7 +160,7 @@ export function LeagueSponsorshipsSection({
value={`$${netRevenue.toFixed(2)}`}
color="var(--performance-green)"
/>
</Box>
</Stack>
)}
{/* Sponsorship Slots */}
@@ -171,7 +170,7 @@ export function LeagueSponsorshipsSection({
const IconComp = slot.tier === 'main' ? Star : Award;
return (
<Box
<Stack
key={index}
rounded="lg"
border
@@ -179,9 +178,9 @@ export function LeagueSponsorshipsSection({
bg="bg-deep-graphite/70"
p={4}
>
<Box display="flex" alignItems="center" justifyContent="between" gap={4}>
<Box display="flex" alignItems="center" gap={3} flexGrow={1}>
<Box
<Stack display="flex" alignItems="center" justifyContent="between" gap={4}>
<Stack display="flex" alignItems="center" gap={3} flexGrow={1}>
<Stack
display="flex"
h="10"
w="10"
@@ -191,10 +190,10 @@ export function LeagueSponsorshipsSection({
bg={slot.tier === 'main' ? 'bg-primary-blue/10' : 'bg-gray-500/10'}
>
<Icon icon={IconComp} size={5} color={slot.tier === 'main' ? 'var(--primary-blue)' : 'var(--gray-400)'} />
</Box>
</Stack>
<Box flexGrow={1}>
<Box display="flex" alignItems="center" gap={2}>
<Stack flexGrow={1}>
<Stack display="flex" alignItems="center" gap={2}>
<Heading level={4}>
{slot.tier === 'main' ? 'Main Sponsor' : 'Secondary Sponsor'}
</Heading>
@@ -203,18 +202,18 @@ export function LeagueSponsorshipsSection({
Occupied
</Badge>
)}
</Box>
</Stack>
<Text size="xs" color="text-gray-500" mt={0.5} block>
{slot.tier === 'main'
? 'Big livery slot • League page logo • Name in league title'
: 'Small livery slot • League page logo'}
</Text>
</Box>
</Box>
</Stack>
</Stack>
<Box display="flex" alignItems="center" gap={3}>
<Stack display="flex" alignItems="center" gap={3}>
{isEditing ? (
<Box display="flex" alignItems="center" gap={2}>
<Stack display="flex" alignItems="center" gap={2}>
<Input
type="number"
value={tempPrice}
@@ -239,15 +238,15 @@ export function LeagueSponsorshipsSection({
>
<Icon icon={X} size={4} />
</Button>
</Box>
</Stack>
) : (
<>
<Box textAlign="right">
<Stack textAlign="right">
<Text size="lg" weight="bold" color="text-white" block>
${slot.price.toFixed(2)}
</Text>
<Text size="xs" color="text-gray-500" block>per season</Text>
</Box>
</Stack>
{!readOnly && !slot.isOccupied && (
<Button
variant="secondary"
@@ -259,16 +258,16 @@ export function LeagueSponsorshipsSection({
)}
</>
)}
</Box>
</Box>
</Box>
</Stack>
</Stack>
</Stack>
);
})}
</Stack>
{/* Pending Sponsorship Requests */}
{!readOnly && (pendingRequests.length > 0 || requestsLoading) && (
<Box mt={8} pt={6} borderTop borderColor="border-charcoal-outline">
<Stack mt={8} pt={6} borderTop borderColor="border-charcoal-outline">
<PendingSponsorshipRequests
entityType="season"
entityId={seasonId || ''}
@@ -278,16 +277,16 @@ export function LeagueSponsorshipsSection({
onReject={handleRejectRequest}
isLoading={requestsLoading}
/>
</Box>
</Stack>
)}
{/* Alpha Notice */}
<Box rounded="lg" bg="bg-warning-amber/10" border borderColor="border-warning-amber/30" p={4}>
<Stack rounded="lg" bg="bg-warning-amber/10" border borderColor="border-warning-amber/30" p={4}>
<Text size="xs" color="text-gray-400" block>
<Text weight="bold" color="text-warning-amber">Alpha Note:</Text> Sponsorship management is demonstration-only.
In production, sponsors can browse leagues, select slots, and complete payment integration.
</Text>
</Box>
</Stack>
</Stack>
);
}

View File

@@ -1,8 +1,9 @@
'use client';
import React from 'react';
import { Box } from '@/ui/Box';
import { Text } from '@/ui/Text';
import { Table, TableHead, TableBody, TableRow, TableHeader, TableCell } from '@/ui/Table';
import { Stack } from '@/ui/Stack';
interface StandingEntry {
position: number;
@@ -20,61 +21,61 @@ interface LeagueStandingsTableProps {
export function LeagueStandingsTable({ standings }: LeagueStandingsTableProps) {
return (
<Box as="section" overflow="hidden" border borderColor="zinc-800" bg="zinc-900/50">
<Box as="table" w="full" textAlign="left">
<Box as="thead">
<Box as="tr" borderBottom borderColor="zinc-800" bg="zinc-900/80">
<Box as="th" px={4} py={3} w="12">
<Stack as="section" overflow="hidden" border borderColor="zinc-800" bg="zinc-900/50">
<Table className="border-none rounded-none">
<TableHead>
<TableRow className="bg-zinc-900/80 border-zinc-800">
<TableHeader className="w-12 px-4 py-3">
<Text size="xs" weight="bold" color="text-zinc-500" uppercase letterSpacing="widest">Pos</Text>
</Box>
<Box as="th" px={4} py={3}>
</TableHeader>
<TableHeader className="px-4 py-3">
<Text size="xs" weight="bold" color="text-zinc-500" uppercase letterSpacing="widest">Driver</Text>
</Box>
<Box as="th" px={4} py={3} display={{ base: 'none', md: 'table-cell' }}>
</TableHeader>
<TableHeader className="hidden md:table-cell px-4 py-3">
<Text size="xs" weight="bold" color="text-zinc-500" uppercase letterSpacing="widest">Team</Text>
</Box>
<Box as="th" px={4} py={3} textAlign="center">
</TableHeader>
<TableHeader className="text-center px-4 py-3">
<Text size="xs" weight="bold" color="text-zinc-500" uppercase letterSpacing="widest">Wins</Text>
</Box>
<Box as="th" px={4} py={3} textAlign="center">
</TableHeader>
<TableHeader className="text-center px-4 py-3">
<Text size="xs" weight="bold" color="text-zinc-500" uppercase letterSpacing="widest">Podiums</Text>
</Box>
<Box as="th" px={4} py={3} textAlign="right">
</TableHeader>
<TableHeader className="text-right px-4 py-3">
<Text size="xs" weight="bold" color="text-zinc-500" uppercase letterSpacing="widest">Points</Text>
</Box>
<Box as="th" px={4} py={3} textAlign="right">
</TableHeader>
<TableHeader className="text-right px-4 py-3">
<Text size="xs" weight="bold" color="text-zinc-500" uppercase letterSpacing="widest">Gap</Text>
</Box>
</Box>
</Box>
<Box as="tbody">
</TableHeader>
</TableRow>
</TableHead>
<TableBody className="divide-zinc-800">
{standings.map((entry) => (
<Box as="tr" key={entry.driverName} borderBottom borderColor="zinc-800" hoverBg="zinc-800/50" transition>
<Box as="td" px={4} py={3}>
<TableRow key={entry.driverName} className="border-zinc-800 hover:bg-zinc-800/50">
<TableCell className="px-4 py-3">
<Text size="sm" color="text-zinc-400" font="mono">{entry.position}</Text>
</Box>
<Box as="td" px={4} py={3}>
</TableCell>
<TableCell className="px-4 py-3">
<Text size="sm" weight="medium" color="text-zinc-200">{entry.driverName}</Text>
</Box>
<Box as="td" px={4} py={3} display={{ base: 'none', md: 'table-cell' }}>
</TableCell>
<TableCell className="hidden md:table-cell px-4 py-3">
<Text size="sm" color="text-zinc-500">{entry.teamName || '—'}</Text>
</Box>
<Box as="td" px={4} py={3} textAlign="center">
</TableCell>
<TableCell className="text-center px-4 py-3">
<Text size="sm" color="text-zinc-400">{entry.wins}</Text>
</Box>
<Box as="td" px={4} py={3} textAlign="center">
</TableCell>
<TableCell className="text-center px-4 py-3">
<Text size="sm" color="text-zinc-400">{entry.podiums}</Text>
</Box>
<Box as="td" px={4} py={3} textAlign="right">
</TableCell>
<TableCell className="text-right px-4 py-3">
<Text size="sm" weight="bold" color="text-white">{entry.points}</Text>
</Box>
<Box as="td" px={4} py={3} textAlign="right">
</TableCell>
<TableCell className="text-right px-4 py-3">
<Text size="sm" color="text-zinc-500" font="mono">{entry.gap}</Text>
</Box>
</Box>
</TableCell>
</TableRow>
))}
</Box>
</Box>
</Box>
</TableBody>
</Table>
</Stack>
);
}

View File

@@ -3,9 +3,8 @@
import React from 'react';
import { Scale, Clock, Bell, Shield, Vote, AlertTriangle } from 'lucide-react';
import type { LeagueConfigFormModel } from '@/lib/types/LeagueConfigFormModel';
import { Box } from '@/ui/Box';
import { Text } from '@/ui/Text';
import { Stack } from '@/ui/Stack';
import { Text } from '@/ui/Text';
import { Heading } from '@/ui/Heading';
import { Icon } from '@/ui/Icon';
import { Select } from '@/ui/Select';
@@ -75,7 +74,7 @@ export function LeagueStewardingSection({
return (
<Stack gap={8}>
{/* Decision Mode Selection */}
<Box>
<Stack>
<Heading level={3} fontSize="sm" weight="semibold" color="text-white" mb={1}>
<Stack direction="row" align="center" gap={2}>
<Icon icon={Scale} size={4} color="text-primary-blue" />
@@ -86,9 +85,9 @@ export function LeagueStewardingSection({
Choose who has the authority to issue penalties
</Text>
<Box display="grid" gridCols={{ base: 1, sm: 2, lg: 3 }} gap={3}>
<Stack display="grid" gridCols={{ base: 1, sm: 2, lg: 3 }} gap={3}>
{decisionModeOptions.map((option) => (
<Box
<Stack
key={option.value}
as="button"
type="button"
@@ -112,29 +111,29 @@ export function LeagueStewardingSection({
opacity={readOnly ? 0.6 : 1}
cursor={readOnly ? 'not-allowed' : 'pointer'}
>
<Box
<Stack
p={2}
rounded="lg"
bg={stewarding.decisionMode === option.value ? 'bg-primary-blue/20' : 'bg-charcoal-outline/50'}
color={stewarding.decisionMode === option.value ? 'text-primary-blue' : 'text-gray-400'}
>
{option.icon}
</Box>
<Box>
</Stack>
<Stack>
<Text size="sm" weight="medium" color="text-white" block>{option.label}</Text>
<Text size="xs" color="text-gray-400" mt={0.5} block>{option.description}</Text>
</Box>
</Stack>
{stewarding.decisionMode === option.value && (
<Box position="absolute" top="2" right="2" w="2" h="2" rounded="full" bg="bg-primary-blue" />
<Stack position="absolute" top="2" right="2" w="2" h="2" rounded="full" bg="bg-primary-blue" />
)}
</Box>
</Stack>
))}
</Box>
</Box>
</Stack>
</Stack>
{/* Vote Requirements (conditional) */}
{selectedMode?.requiresVotes && (
<Box p={4} rounded="xl" bg="bg-iron-gray/40" border borderColor="border-charcoal-outline">
<Stack p={4} rounded="xl" bg="bg-iron-gray/40" border borderColor="border-charcoal-outline">
<Stack gap={4}>
<Heading level={4} fontSize="sm" weight="medium" color="text-white">
<Stack direction="row" align="center" gap={2}>
@@ -143,8 +142,8 @@ export function LeagueStewardingSection({
</Stack>
</Heading>
<Box display="grid" gridCols={{ base: 1, sm: 2 }} gap={4}>
<Box>
<Stack display="grid" gridCols={{ base: 1, sm: 2 }} gap={4}>
<Stack>
<Text as="label" size="xs" weight="medium" color="text-gray-400" block mb={1.5}>
Required votes to uphold
</Text>
@@ -160,9 +159,9 @@ export function LeagueStewardingSection({
{ value: '5', label: '5 votes' },
]}
/>
</Box>
</Stack>
<Box>
<Stack>
<Text as="label" size="xs" weight="medium" color="text-gray-400" block mb={1.5}>
Voting time limit
</Text>
@@ -178,14 +177,14 @@ export function LeagueStewardingSection({
{ value: '168', label: '168 hours (7 days)' },
]}
/>
</Box>
</Box>
</Stack>
</Stack>
</Stack>
</Box>
</Stack>
)}
{/* Defense Settings */}
<Box>
<Stack>
<Heading level={3} fontSize="sm" weight="semibold" color="text-white" mb={1}>
<Stack direction="row" align="center" gap={2}>
<Icon icon={Shield} size={4} color="text-primary-blue" />
@@ -196,8 +195,8 @@ export function LeagueStewardingSection({
Should accused drivers be required to submit a defense?
</Text>
<Box display="grid" gridCols={{ base: 1, sm: 2 }} gap={3}>
<Box
<Stack display="grid" gridCols={{ base: 1, sm: 2 }} gap={3}>
<Stack
as="button"
type="button"
disabled={readOnly}
@@ -217,16 +216,16 @@ export function LeagueStewardingSection({
opacity={readOnly ? 0.6 : 1}
cursor={readOnly ? 'not-allowed' : 'pointer'}
>
<Box w="4" h="4" rounded="full" border borderWidth="2px" display="flex" alignItems="center" justifyContent="center" borderColor={!stewarding.requireDefense ? 'border-primary-blue' : 'border-gray-500'}>
{!stewarding.requireDefense && <Box w="2" h="2" rounded="full" bg="bg-primary-blue" />}
</Box>
<Box>
<Stack w="4" h="4" rounded="full" border borderWidth="2px" display="flex" alignItems="center" justifyContent="center" borderColor={!stewarding.requireDefense ? 'border-primary-blue' : 'border-gray-500'}>
{!stewarding.requireDefense && <Stack w="2" h="2" rounded="full" bg="bg-primary-blue" />}
</Stack>
<Stack>
<Text size="sm" weight="medium" color="text-white" block>Defense optional</Text>
<Text size="xs" color="text-gray-400" block>Proceed without waiting for defense</Text>
</Box>
</Box>
</Stack>
</Stack>
<Box
<Stack
as="button"
type="button"
disabled={readOnly}
@@ -246,18 +245,18 @@ export function LeagueStewardingSection({
opacity={readOnly ? 0.6 : 1}
cursor={readOnly ? 'not-allowed' : 'pointer'}
>
<Box w="4" h="4" rounded="full" border borderWidth="2px" display="flex" alignItems="center" justifyContent="center" borderColor={stewarding.requireDefense ? 'border-primary-blue' : 'border-gray-500'}>
{stewarding.requireDefense && <Box w="2" h="2" rounded="full" bg="bg-primary-blue" />}
</Box>
<Box>
<Stack w="4" h="4" rounded="full" border borderWidth="2px" display="flex" alignItems="center" justifyContent="center" borderColor={stewarding.requireDefense ? 'border-primary-blue' : 'border-gray-500'}>
{stewarding.requireDefense && <Stack w="2" h="2" rounded="full" bg="bg-primary-blue" />}
</Stack>
<Stack>
<Text size="sm" weight="medium" color="text-white" block>Defense required</Text>
<Text size="xs" color="text-gray-400" block>Wait for defense before deciding</Text>
</Box>
</Box>
</Box>
</Stack>
</Stack>
</Stack>
{stewarding.requireDefense && (
<Box mt={4} p={4} rounded="xl" bg="bg-iron-gray/40" border borderColor="border-charcoal-outline">
<Stack mt={4} p={4} rounded="xl" bg="bg-iron-gray/40" border borderColor="border-charcoal-outline">
<Text as="label" size="xs" weight="medium" color="text-gray-400" block mb={1.5}>
Defense time limit
</Text>
@@ -275,12 +274,12 @@ export function LeagueStewardingSection({
<Text size="xs" color="text-gray-500" mt={2} block>
After this time, the decision can proceed without a defense
</Text>
</Box>
</Stack>
)}
</Box>
</Stack>
{/* Deadlines */}
<Box>
<Stack>
<Heading level={3} fontSize="sm" weight="semibold" color="text-white" mb={1}>
<Stack direction="row" align="center" gap={2}>
<Icon icon={Clock} size={4} color="text-primary-blue" />
@@ -291,8 +290,8 @@ export function LeagueStewardingSection({
Set time limits for filing protests and closing stewarding
</Text>
<Box display="grid" gridCols={{ base: 1, sm: 2 }} gap={4}>
<Box p={4} rounded="xl" bg="bg-iron-gray/40" border borderColor="border-charcoal-outline">
<Stack display="grid" gridCols={{ base: 1, sm: 2 }} gap={4}>
<Stack p={4} rounded="xl" bg="bg-iron-gray/40" border borderColor="border-charcoal-outline">
<Text as="label" size="xs" weight="medium" color="text-gray-400" block mb={1.5}>
Protest filing deadline (after race)
</Text>
@@ -311,9 +310,9 @@ export function LeagueStewardingSection({
<Text size="xs" color="text-gray-500" mt={2} block>
Drivers cannot file protests after this time
</Text>
</Box>
</Stack>
<Box p={4} rounded="xl" bg="bg-iron-gray/40" border borderColor="border-charcoal-outline">
<Stack p={4} rounded="xl" bg="bg-iron-gray/40" border borderColor="border-charcoal-outline">
<Text as="label" size="xs" weight="medium" color="text-gray-400" block mb={1.5}>
Stewarding closes (after race)
</Text>
@@ -331,12 +330,12 @@ export function LeagueStewardingSection({
<Text size="xs" color="text-gray-500" mt={2} block>
All stewarding must be concluded by this time
</Text>
</Box>
</Box>
</Box>
</Stack>
</Stack>
</Stack>
{/* Notifications */}
<Box>
<Stack>
<Heading level={3} fontSize="sm" weight="semibold" color="text-white" mb={1}>
<Stack direction="row" align="center" gap={2}>
<Icon icon={Bell} size={4} color="text-primary-blue" />
@@ -348,7 +347,7 @@ export function LeagueStewardingSection({
</Text>
<Stack gap={3}>
<Box
<Stack
p={4}
rounded="xl"
bg="bg-iron-gray/40"
@@ -364,14 +363,14 @@ export function LeagueStewardingSection({
onChange={(checked) => updateStewarding({ notifyAccusedOnProtest: checked })}
disabled={readOnly}
/>
<Box ml={7} mt={1}>
<Stack ml={7} mt={1}>
<Text size="xs" color="text-gray-400" block>
Send notification when a protest is filed against them
</Text>
</Box>
</Box>
</Stack>
</Stack>
<Box
<Stack
p={4}
rounded="xl"
bg="bg-iron-gray/40"
@@ -387,27 +386,27 @@ export function LeagueStewardingSection({
onChange={(checked) => updateStewarding({ notifyOnVoteRequired: checked })}
disabled={readOnly}
/>
<Box ml={7} mt={1}>
<Stack ml={7} mt={1}>
<Text size="xs" color="text-gray-400" block>
Send notification to stewards/members when their vote is needed
</Text>
</Box>
</Box>
</Stack>
</Stack>
</Stack>
</Box>
</Stack>
{/* Warning about strict settings */}
{stewarding.requireDefense && stewarding.decisionMode !== 'single_steward' && (
<Box display="flex" alignItems="start" gap={3} p={4} rounded="xl" bg="bg-warning-amber/10" border borderColor="border-warning-amber/20">
<Stack display="flex" alignItems="start" gap={3} p={4} rounded="xl" bg="bg-warning-amber/10" border borderColor="border-warning-amber/20">
<Icon icon={AlertTriangle} size={5} color="text-warning-amber" mt={0.5} />
<Box>
<Stack>
<Text size="sm" weight="medium" color="text-warning-amber" block>Strict settings enabled</Text>
<Text size="xs" color="text-warning-amber" opacity={0.8} mt={1} block>
Requiring defense and voting may delay penalty decisions. Make sure your stewards/members
are active enough to meet the deadlines.
</Text>
</Box>
</Box>
</Stack>
</Stack>
)}
</Stack>
);

View File

@@ -5,10 +5,9 @@ import { useState, useRef, useEffect, useMemo } from 'react';
import type * as React from 'react';
import { createPortal } from 'react-dom';
import { Input } from '@/ui/Input';
import { Box } from '@/ui/Box';
import { Stack } from '@/ui/Stack';
import { Text } from '@/ui/Text';
import { Heading } from '@/ui/Heading';
import { Stack } from '@/ui/Stack';
import { Icon } from '@/ui/Icon';
import type { LeagueConfigFormModel } from '@/lib/types/LeagueConfigFormModel';
@@ -85,7 +84,7 @@ function InfoFlyout({ isOpen, onClose, title, children, anchorRef }: InfoFlyoutP
if (!isOpen || !mounted) return null;
return createPortal(
<Box
<Stack
ref={flyoutRef}
position="fixed"
zIndex={50}
@@ -98,7 +97,7 @@ function InfoFlyout({ isOpen, onClose, title, children, anchorRef }: InfoFlyoutP
// eslint-disable-next-line gridpilot-rules/component-classification
style={{ top: position.top, left: position.left, maxHeight: '80vh', overflowY: 'auto' }}
>
<Box
<Stack
display="flex"
alignItems="center"
justifyContent="between"
@@ -114,7 +113,7 @@ function InfoFlyout({ isOpen, onClose, title, children, anchorRef }: InfoFlyoutP
<Icon icon={HelpCircle} size={4} color="text-primary-blue" />
<Text size="sm" weight="semibold" color="text-white">{title}</Text>
</Stack>
<Box
<Stack
as="button"
type="button"
onClick={onClose}
@@ -128,12 +127,12 @@ function InfoFlyout({ isOpen, onClose, title, children, anchorRef }: InfoFlyoutP
hoverBg="bg-charcoal-outline"
>
<Icon icon={X} size={4} color="text-gray-400" />
</Box>
</Box>
<Box p={4}>
</Stack>
</Stack>
<Stack p={4}>
{children}
</Box>
</Box>,
</Stack>
</Stack>,
document.body
);
}
@@ -281,20 +280,20 @@ export function LeagueStructureSection({
return (
<Stack gap={8}>
{/* Emotional header */}
<Box textAlign="center" pb={2}>
<Stack textAlign="center" pb={2}>
<Heading level={3} mb={2}>
How will your drivers compete?
</Heading>
<Text size="sm" color="text-gray-400" maxWidth="lg" mx="auto" block>
Choose your championship format individual glory or team triumph.
</Text>
</Box>
</Stack>
{/* Mode Selection Cards */}
<Box display="grid" responsiveGridCols={{ base: 1, md: 2 }} gap={4}>
<Stack display="grid" responsiveGridCols={{ base: 1, md: 2 }} gap={4}>
{/* Solo Mode Card */}
<Box position="relative">
<Box
<Stack position="relative">
<Stack
as="button"
type="button"
disabled={disabled}
@@ -319,9 +318,9 @@ export function LeagueStructureSection({
group
>
{/* Header */}
<Box display="flex" alignItems="start" justifyContent="between">
<Stack display="flex" alignItems="start" justifyContent="between">
<Stack direction="row" align="center" gap={3}>
<Box
<Stack
display="flex"
h="12"
w="12"
@@ -331,18 +330,18 @@ export function LeagueStructureSection({
bg={isSolo ? 'bg-primary-blue/30' : 'bg-charcoal-outline/50'}
>
<Icon icon={User} size={6} color={isSolo ? 'text-primary-blue' : 'text-gray-400'} />
</Box>
<Box>
</Stack>
<Stack>
<Text weight="bold" size="lg" color={isSolo ? 'text-white' : 'text-gray-300'} block>
Solo Drivers
</Text>
<Text size="xs" color={isSolo ? 'text-primary-blue' : 'text-gray-500'} block>
Individual competition
</Text>
</Box>
</Stack>
</Stack>
{/* Radio indicator */}
<Box
<Stack
display="flex"
h="6"
w="6"
@@ -356,8 +355,8 @@ export function LeagueStructureSection({
transition
>
{isSolo && <Icon icon={Check} size={3.5} color="text-white" />}
</Box>
</Box>
</Stack>
</Stack>
{/* Emotional tagline */}
<Text size="sm" color={isSolo ? 'text-gray-300' : 'text-gray-500'} block>
@@ -379,10 +378,10 @@ export function LeagueStructureSection({
<Text size="xs" color="text-gray-400">Perfect for any grid size</Text>
</Stack>
</Stack>
</Box>
</Stack>
{/* Info button */}
<Box
<Stack
as="button"
ref={soloInfoRef}
type="button"
@@ -402,8 +401,8 @@ export function LeagueStructureSection({
hoverBg="bg-primary-blue/10"
>
<Icon icon={HelpCircle} size={4} />
</Box>
</Box>
</Stack>
</Stack>
{/* Solo Info Flyout */}
<InfoFlyout
@@ -445,8 +444,8 @@ export function LeagueStructureSection({
</InfoFlyout>
{/* Teams Mode Card */}
<Box position="relative">
<Box
<Stack position="relative">
<Stack
as="button"
type="button"
disabled={disabled}
@@ -471,9 +470,9 @@ export function LeagueStructureSection({
group
>
{/* Header */}
<Box display="flex" alignItems="start" justifyContent="between">
<Stack display="flex" alignItems="start" justifyContent="between">
<Stack direction="row" align="center" gap={3}>
<Box
<Stack
display="flex"
h="12"
w="12"
@@ -483,18 +482,18 @@ export function LeagueStructureSection({
bg={!isSolo ? 'bg-neon-aqua/30' : 'bg-charcoal-outline/50'}
>
<Icon icon={Users2} size={6} color={!isSolo ? 'text-neon-aqua' : 'text-gray-400'} />
</Box>
<Box>
</Stack>
<Stack>
<Text weight="bold" size="lg" color={!isSolo ? 'text-white' : 'text-gray-300'} block>
Team Racing
</Text>
<Text size="xs" color={!isSolo ? 'text-neon-aqua' : 'text-gray-500'} block>
Shared destiny
</Text>
</Box>
</Stack>
</Stack>
{/* Radio indicator */}
<Box
<Stack
display="flex"
h="6"
w="6"
@@ -508,8 +507,8 @@ export function LeagueStructureSection({
transition
>
{!isSolo && <Icon icon={Check} size={3.5} color="text-deep-graphite" />}
</Box>
</Box>
</Stack>
</Stack>
{/* Emotional tagline */}
<Text size="sm" color={!isSolo ? 'text-gray-300' : 'text-gray-500'} block>
@@ -531,10 +530,10 @@ export function LeagueStructureSection({
<Text size="xs" color="text-gray-400">Great for endurance & pro-am</Text>
</Stack>
</Stack>
</Box>
</Stack>
{/* Info button */}
<Box
<Stack
as="button"
ref={teamsInfoRef}
type="button"
@@ -554,8 +553,8 @@ export function LeagueStructureSection({
hoverBg="bg-neon-aqua/10"
>
<Icon icon={HelpCircle} size={4} />
</Box>
</Box>
</Stack>
</Stack>
{/* Teams Info Flyout */}
<InfoFlyout
@@ -595,13 +594,13 @@ export function LeagueStructureSection({
</Stack>
</Stack>
</InfoFlyout>
</Box>
</Stack>
{/* Configuration Panel */}
<Box rounded="xl" border borderColor="border-charcoal-outline" bg="bg-iron-gray/30" p={6}>
<Stack rounded="xl" border borderColor="border-charcoal-outline" bg="bg-iron-gray/30" p={6}>
<Stack gap={5}>
<Stack direction="row" align="center" gap={3}>
<Box
<Stack
display="flex"
h="10"
w="10"
@@ -615,8 +614,8 @@ export function LeagueStructureSection({
) : (
<Icon icon={Users2} size={5} color="text-neon-aqua" />
)}
</Box>
<Box>
</Stack>
<Stack>
<Text size="sm" weight="semibold" color="text-white" block>
{isSolo ? 'Grid size' : 'Team configuration'}
</Text>
@@ -626,7 +625,7 @@ export function LeagueStructureSection({
: 'Configure teams and roster sizes'
}
</Text>
</Box>
</Stack>
</Stack>
{/* Solo mode capacity */}
@@ -653,8 +652,8 @@ export function LeagueStructureSection({
<Stack gap={2}>
<Text size="xs" color="text-gray-500" block>Quick select:</Text>
<Box display="flex" flexWrap="wrap" gap={2}>
<Box
<Stack display="flex" flexWrap="wrap" gap={2}>
<Stack
as="button"
type="button"
onClick={() => handleMaxDriversChange('16')}
@@ -671,8 +670,8 @@ export function LeagueStructureSection({
transition
>
Compact (16)
</Box>
<Box
</Stack>
<Stack
as="button"
type="button"
onClick={() => handleMaxDriversChange('24')}
@@ -689,8 +688,8 @@ export function LeagueStructureSection({
transition
>
Standard (24)
</Box>
<Box
</Stack>
<Stack
as="button"
type="button"
onClick={() => handleMaxDriversChange('30')}
@@ -707,8 +706,8 @@ export function LeagueStructureSection({
transition
>
Full Grid (30)
</Box>
<Box
</Stack>
<Stack
as="button"
type="button"
onClick={() => handleMaxDriversChange('40')}
@@ -725,8 +724,8 @@ export function LeagueStructureSection({
transition
>
Large (40)
</Box>
<Box
</Stack>
<Stack
as="button"
type="button"
onClick={() => handleMaxDriversChange(String(gameConstraints.maxDrivers))}
@@ -743,8 +742,8 @@ export function LeagueStructureSection({
transition
>
Max ({gameConstraints.maxDrivers})
</Box>
</Box>
</Stack>
</Stack>
</Stack>
</Stack>
)}
@@ -755,8 +754,8 @@ export function LeagueStructureSection({
{/* Quick presets */}
<Stack gap={3}>
<Text size="xs" color="text-gray-500" block>Popular configurations:</Text>
<Box display="flex" flexWrap="wrap" gap={2}>
<Box
<Stack display="flex" flexWrap="wrap" gap={2}>
<Stack
as="button"
type="button"
onClick={() => {
@@ -776,8 +775,8 @@ export function LeagueStructureSection({
transition
>
10 × 2 (20 grid)
</Box>
<Box
</Stack>
<Stack
as="button"
type="button"
onClick={() => {
@@ -797,8 +796,8 @@ export function LeagueStructureSection({
transition
>
12 × 2 (24 grid)
</Box>
<Box
</Stack>
<Stack
as="button"
type="button"
onClick={() => {
@@ -818,8 +817,8 @@ export function LeagueStructureSection({
transition
>
8 × 3 (24 grid)
</Box>
<Box
</Stack>
<Stack
as="button"
type="button"
onClick={() => {
@@ -839,12 +838,12 @@ export function LeagueStructureSection({
transition
>
15 × 2 (30 grid)
</Box>
</Box>
</Stack>
</Stack>
</Stack>
{/* Manual configuration */}
<Box display="grid" responsiveGridCols={{ base: 1, sm: 3 }} gap={4} pt={2} borderTop borderColor="border-charcoal-outline/50">
<Stack display="grid" responsiveGridCols={{ base: 1, sm: 3 }} gap={4} pt={2} borderTop borderColor="border-charcoal-outline/50">
<Stack gap={2}>
<Text as="label" size="sm" weight="medium" color="text-gray-300" block>
Teams
@@ -877,7 +876,7 @@ export function LeagueStructureSection({
<Text as="label" size="sm" weight="medium" color="text-gray-300" block>
Total grid
</Text>
<Box
<Stack
display="flex"
alignItems="center"
justifyContent="center"
@@ -891,13 +890,13 @@ export function LeagueStructureSection({
{structure.maxDrivers ?? 0}
</Text>
<Text size="xs" color="text-gray-500" ml={1}>drivers</Text>
</Box>
</Stack>
</Stack>
</Box>
</Stack>
</Stack>
)}
</Stack>
</Box>
</Stack>
</Stack>
);
}

View File

@@ -1,7 +1,4 @@
import { ArrowRight } from 'lucide-react';
import { Box } from '@/ui/Box';
import { Button } from '@/ui/Button';
import { Card } from '@/ui/Card';
import { Grid } from '@/ui/Grid';
@@ -9,9 +6,8 @@ import { Heading } from '@/ui/Heading';
import { Icon } from '@/ui/Icon';
import { LeagueLogo } from './LeagueLogo';
import { Link } from '@/ui/Link';
import { Stack } from '@/ui/Stack';
import { Surface } from '@/ui/Surface';
import { Text } from '@/ui/Text';
import { Stack } from '@/ui/Stack';
interface LeagueSummaryCardProps {
id: string;
@@ -32,10 +28,10 @@ export function LeagueSummaryCard({
}: LeagueSummaryCardProps) {
return (
<Card p={0} style={{ overflow: 'hidden' }}>
<Box p={4}>
<Stack p={4}>
<Stack direction="row" align="center" gap={4} mb={4}>
<LeagueLogo leagueId={id} alt={name} size={56} />
<Box style={{ flex: 1, minWidth: 0 }}>
<Stack style={{ flex: 1, minWidth: 0 }}>
<Text
size="xs"
color="text-gray-500"
@@ -48,7 +44,7 @@ export function LeagueSummaryCard({
<Heading level={3} style={{ fontSize: '1rem' }}>
{name}
</Heading>
</Box>
</Stack>
</Stack>
{description && (
@@ -64,17 +60,17 @@ export function LeagueSummaryCard({
</Text>
)}
<Box mb={4}>
<Stack mb={4}>
<Grid cols={2} gap={3}>
<Surface variant="dark" rounded="lg" padding={3}>
<Card variant="outline" rounded="lg" p={3} className="bg-graphite-black">
<Text size="xs" color="text-gray-500" block mb={1}>
Max Drivers
</Text>
<Text weight="medium" color="text-white">
{maxDrivers}
</Text>
</Surface>
<Surface variant="dark" rounded="lg" padding={3}>
</Card>
<Card variant="outline" rounded="lg" p={3} className="bg-graphite-black">
<Text size="xs" color="text-gray-500" block mb={1}>
Format
</Text>
@@ -85,11 +81,11 @@ export function LeagueSummaryCard({
>
{qualifyingFormat}
</Text>
</Surface>
</Card>
</Grid>
</Box>
</Stack>
<Box>
<Stack>
<Link href={href}>
<Button
variant="secondary"
@@ -99,8 +95,8 @@ export function LeagueSummaryCard({
View League
</Button>
</Link>
</Box>
</Box>
</Stack>
</Stack>
</Card>
);
}

View File

@@ -1,7 +1,6 @@
'use client';
import React from 'react';
import { Box } from '@/ui/Box';
import { Stack } from '@/ui/Stack';
import { Link } from '@/ui/Link';
import { Text } from '@/ui/Text';
@@ -18,7 +17,7 @@ interface LeagueTabsProps {
export function LeagueTabs({ tabs }: LeagueTabsProps) {
return (
<Box borderBottom borderColor="border-charcoal-outline">
<Stack borderBottom borderColor="border-charcoal-outline">
<Stack direction="row" gap={6} overflow="auto">
{tabs.map((tab) => (
<Link
@@ -26,17 +25,17 @@ export function LeagueTabs({ tabs }: LeagueTabsProps) {
href={tab.href}
variant="ghost"
>
<Box pb={3} px={1}>
<Stack pb={3} px={1}>
<Text weight="medium"
// eslint-disable-next-line gridpilot-rules/component-classification
className="whitespace-nowrap"
>
{tab.label}
</Text>
</Box>
</Stack>
</Link>
))}
</Stack>
</Box>
</Stack>
);
}

View File

@@ -22,9 +22,8 @@ import type { LeagueConfigFormModel } from '@/lib/types/LeagueConfigFormModel';
import type { Weekday } from '@/lib/types/Weekday';
import { Input } from '@/ui/Input';
import { RangeField } from '@/components/shared/RangeField';
import { Box } from '@/ui/Box';
import { Text } from '@/ui/Text';
import { Stack } from '@/ui/Stack';
import { Text } from '@/ui/Text';
import { Heading } from '@/ui/Heading';
import { Icon } from '@/ui/Icon';
@@ -119,7 +118,7 @@ function RaceDayPreview({
return (
<Stack gap={4}>
<Box display="flex" alignItems="center" gap={2}>
<Stack display="flex" alignItems="center" gap={2}>
<Icon icon={Flag} size={3.5} color="text-primary-blue" />
<Text size="xs" weight="medium" color="text-white">Race Day Schedule</Text>
<Text size="xs" color="text-gray-600"></Text>
@@ -127,7 +126,7 @@ function RaceDayPreview({
<Text size="xs" color="text-gray-400">
Starts {effectiveRaceTime}{!raceTime && <Text as="span" color="text-gray-600" ml={1}>(default)</Text>}
</Text>
</Box>
</Stack>
{/* Timeline visualization - show ALL sessions */}
<Stack gap={2}>
@@ -138,7 +137,7 @@ function RaceDayPreview({
const startTime = isActive ? getStartTime(activeIndex) : null;
return (
<Box
<Stack
key={session.name}
position="relative"
display="flex"
@@ -157,7 +156,7 @@ function RaceDayPreview({
>
{/* Status badge */}
{!isActive && (
<Box position="absolute" top="-1" right="-1" px={1.5} py={0.5} rounded="sm" bg="bg-charcoal-outline">
<Stack position="absolute" top="-1" right="-1" px={1.5} py={0.5} rounded="sm" bg="bg-charcoal-outline">
<Text
// eslint-disable-next-line gridpilot-rules/component-classification
style={{ fontSize: '8px' }}
@@ -166,21 +165,21 @@ function RaceDayPreview({
>
Not included
</Text>
</Box>
</Stack>
)}
{/* Time marker */}
<Box w="12" textAlign="right" flexShrink={0}>
<Stack w="12" textAlign="right" flexShrink={0}>
<Text size="xs" font="mono" color={!isActive ? 'text-gray-600' : isRace ? 'text-primary-blue' : 'text-gray-400'}>
{startTime ?? '—'}
</Text>
</Box>
</Stack>
{/* Session indicator */}
<Box w="1" h="8" rounded="full" transition bg={!isActive ? 'bg-charcoal-outline/30' : isRace ? 'bg-primary-blue' : 'bg-charcoal-outline'} />
<Stack w="1" h="8" rounded="full" transition bg={!isActive ? 'bg-charcoal-outline/30' : isRace ? 'bg-primary-blue' : 'bg-charcoal-outline'} />
{/* Session info */}
<Box flexGrow={1}>
<Stack flexGrow={1}>
<Text size="sm" weight="medium" color={!isActive ? 'text-gray-600' : isRace ? 'text-white' : 'text-gray-300'} block>
{session.name}
</Text>
@@ -192,29 +191,29 @@ function RaceDayPreview({
>
{isActive ? `${session.duration} minutes` : 'Disabled'}
</Text>
</Box>
</Stack>
{/* Duration bar */}
{isActive && (
<Box w="16" h="2" bg="bg-charcoal-outline/50" rounded="full" overflow="hidden">
<Box
<Stack w="16" h="2" bg="bg-charcoal-outline/50" rounded="full" overflow="hidden">
<Stack
h="full"
rounded="full"
bg={isRace ? 'bg-primary-blue' : 'bg-gray-600'}
// eslint-disable-next-line gridpilot-rules/component-classification
style={{ width: `${(session.duration / Math.max(...activeSessions.map(s => s.duration))) * 100}%` }}
/>
</Box>
</Stack>
)}
</Box>
</Stack>
);
})}
</Stack>
{/* Legend */}
<Box display="flex" alignItems="center" justifyContent="center" gap={4} pt={2} borderTop borderColor="border-charcoal-outline/30">
<Box display="flex" alignItems="center" gap={1.5}>
<Box w="2" h="2" rounded="sm" bg="bg-primary-blue" />
<Stack display="flex" alignItems="center" justifyContent="center" gap={4} pt={2} borderTop borderColor="border-charcoal-outline/30">
<Stack display="flex" alignItems="center" gap={1.5}>
<Stack w="2" h="2" rounded="sm" bg="bg-primary-blue" />
<Text
// eslint-disable-next-line gridpilot-rules/component-classification
style={{ fontSize: '10px' }}
@@ -222,9 +221,9 @@ function RaceDayPreview({
>
Active race
</Text>
</Box>
<Box display="flex" alignItems="center" gap={1.5}>
<Box w="2" h="2" rounded="sm" bg="bg-gray-600" />
</Stack>
<Stack display="flex" alignItems="center" gap={1.5}>
<Stack w="2" h="2" rounded="sm" bg="bg-gray-600" />
<Text
// eslint-disable-next-line gridpilot-rules/component-classification
style={{ fontSize: '10px' }}
@@ -232,9 +231,9 @@ function RaceDayPreview({
>
Active session
</Text>
</Box>
<Box display="flex" alignItems="center" gap={1.5}>
<Box w="2" h="2" rounded="sm" border borderStyle="dashed" borderColor="border-charcoal-outline/50" bg="transparent" />
</Stack>
<Stack display="flex" alignItems="center" gap={1.5}>
<Stack w="2" h="2" rounded="sm" border borderStyle="dashed" borderColor="border-charcoal-outline/50" bg="transparent" />
<Text
// eslint-disable-next-line gridpilot-rules/component-classification
style={{ fontSize: '10px' }}
@@ -242,12 +241,12 @@ function RaceDayPreview({
>
Not included
</Text>
</Box>
</Box>
</Stack>
</Stack>
{/* Summary */}
<Box display="flex" alignItems="center" justifyContent="between">
<Box display="flex" alignItems="center" gap={3}>
<Stack display="flex" alignItems="center" justifyContent="between">
<Stack display="flex" alignItems="center" gap={3}>
<Text
// eslint-disable-next-line gridpilot-rules/component-classification
style={{ fontSize: '10px' }}
@@ -269,14 +268,14 @@ function RaceDayPreview({
>
{activeSessions.filter(s => s.type === 'race' || s.type === 'sprint').length} race{activeSessions.filter(s => s.type === 'race' || s.type === 'sprint').length > 1 ? 's' : ''}
</Text>
</Box>
<Box display="flex" alignItems="center" gap={1.5} px={2} py={1} rounded="full" bg="bg-iron-gray/60">
</Stack>
<Stack display="flex" alignItems="center" gap={1.5} px={2} py={1} rounded="full" bg="bg-iron-gray/60">
<Icon icon={Timer} size={3} color="text-primary-blue" />
<Text size="xs" weight="semibold" color="text-white">
{Math.floor(totalDuration / 60)}h {totalDuration % 60}m
</Text>
</Box>
</Box>
</Stack>
</Stack>
</Stack>
);
}
@@ -565,11 +564,11 @@ function YearCalendarPreview({
return (
<Stack gap={4}>
<Box display="flex" alignItems="center" justifyContent="between">
<Box display="flex" alignItems="center" gap={2}>
<Stack display="flex" alignItems="center" justifyContent="between">
<Stack display="flex" alignItems="center" gap={2}>
<Icon icon={CalendarRange} size={3.5} color="text-primary-blue" />
<Text size="xs" weight="medium" color="text-white">Season Calendar</Text>
</Box>
</Stack>
<Text
// eslint-disable-next-line gridpilot-rules/component-classification
style={{ fontSize: '10px' }}
@@ -577,17 +576,17 @@ function YearCalendarPreview({
>
{raceDates.length} race{raceDates.length !== 1 ? 's' : ''} scheduled
</Text>
</Box>
</Stack>
{/* Year grid - 3 columns x 4 rows */}
<Box display="grid" gridCols={3} gap={2}>
<Stack display="grid" gridCols={3} gap={2}>
{yearView.map(({ month, monthIndex, year, days }) => {
const hasRaces = days.some(d => d.isRace);
const raceCount = days.filter(d => d.isRace).length;
const uniqueKey = `${year}-${monthIndex}`;
return (
<Box
<Stack
key={uniqueKey}
rounded="lg"
p={2}
@@ -596,7 +595,7 @@ function YearCalendarPreview({
borderColor={hasRaces ? 'border-primary-blue/30' : 'border-charcoal-outline/30'}
bg={hasRaces ? 'bg-primary-blue/5' : 'bg-iron-gray/20'}
>
<Box display="flex" alignItems="center" justifyContent="between" mb={1}>
<Stack display="flex" alignItems="center" justifyContent="between" mb={1}>
<Text
// eslint-disable-next-line gridpilot-rules/component-classification
style={{ fontSize: '10px' }}
@@ -615,17 +614,17 @@ function YearCalendarPreview({
{raceCount}
</Text>
)}
</Box>
</Stack>
{/* Mini calendar grid */}
<Box display="grid" gridCols={7} gap="1px">
<Stack display="grid" gridCols={7} gap="1px">
{/* Fill empty days at start - getDay() returns 0 for Sunday, we want Monday first */}
{Array.from({ length: (new Date(year, monthIndex, 1).getDay() + 6) % 7 }).map((_, i) => (
<Box key={`empty-${uniqueKey}-${i}`} w="2" h="2" />
<Stack key={`empty-${uniqueKey}-${i}`} w="2" h="2" />
))}
{days.map(({ dayOfMonth, isRace, isStart, isEnd, raceNumber }) => (
<Box
<Stack
key={`${uniqueKey}-${dayOfMonth}`}
w="2"
h="2"
@@ -651,15 +650,15 @@ function YearCalendarPreview({
}
/>
))}
</Box>
</Box>
</Stack>
</Stack>
);
})}
</Box>
</Stack>
{/* Season summary */}
<Box display="grid" gridCols={3} gap={2} pt={2} borderTop borderColor="border-charcoal-outline/30">
<Box textAlign="center">
<Stack display="grid" gridCols={3} gap={2} pt={2} borderTop borderColor="border-charcoal-outline/30">
<Stack textAlign="center">
<Text size="lg" weight="bold" color="text-white" block>{rounds}</Text>
<Text
// eslint-disable-next-line gridpilot-rules/component-classification
@@ -669,8 +668,8 @@ function YearCalendarPreview({
>
Rounds
</Text>
</Box>
<Box textAlign="center">
</Stack>
<Stack textAlign="center">
<Text size="lg" weight="bold" color="text-white" block>
{seasonDurationWeeks || '—'}
</Text>
@@ -682,8 +681,8 @@ function YearCalendarPreview({
>
Weeks
</Text>
</Box>
<Box textAlign="center">
</Stack>
<Stack textAlign="center">
<Text size="lg" weight="bold" color="text-primary-blue" block>
{firstRace && lastRace
? `${getMonthLabel(firstRace.getMonth())}${getMonthLabel(
@@ -699,13 +698,13 @@ function YearCalendarPreview({
>
Duration
</Text>
</Box>
</Box>
</Stack>
</Stack>
{/* Legend */}
<Box display="flex" alignItems="center" justifyContent="center" gap={3} flexWrap="wrap">
<Box display="flex" alignItems="center" gap={1}>
<Box w="2" h="2" rounded="sm" bg="bg-performance-green"
<Stack display="flex" alignItems="center" justifyContent="center" gap={3} flexWrap="wrap">
<Stack display="flex" alignItems="center" gap={1}>
<Stack w="2" h="2" rounded="sm" bg="bg-performance-green"
// eslint-disable-next-line gridpilot-rules/component-classification
className="ring-1 ring-performance-green"
/>
@@ -716,9 +715,9 @@ function YearCalendarPreview({
>
Start
</Text>
</Box>
<Box display="flex" alignItems="center" gap={1}>
<Box w="2" h="2" rounded="sm" bg="bg-primary-blue" />
</Stack>
<Stack display="flex" alignItems="center" gap={1}>
<Stack w="2" h="2" rounded="sm" bg="bg-primary-blue" />
<Text
// eslint-disable-next-line gridpilot-rules/component-classification
style={{ fontSize: '10px' }}
@@ -726,10 +725,10 @@ function YearCalendarPreview({
>
Race
</Text>
</Box>
</Stack>
{seasonEnd && (
<Box display="flex" alignItems="center" gap={1}>
<Box w="2" h="2" rounded="sm" bg="bg-warning-amber"
<Stack display="flex" alignItems="center" gap={1}>
<Stack w="2" h="2" rounded="sm" bg="bg-warning-amber"
// eslint-disable-next-line gridpilot-rules/component-classification
className="ring-1 ring-warning-amber"
/>
@@ -740,10 +739,10 @@ function YearCalendarPreview({
>
End
</Text>
</Box>
</Stack>
)}
<Box display="flex" alignItems="center" gap={1}>
<Box w="2" h="2" rounded="sm" bg="bg-charcoal-outline/30" />
<Stack display="flex" alignItems="center" gap={1}>
<Stack w="2" h="2" rounded="sm" bg="bg-charcoal-outline/30" />
<Text
// eslint-disable-next-line gridpilot-rules/component-classification
style={{ fontSize: '10px' }}
@@ -751,8 +750,8 @@ function YearCalendarPreview({
>
No race
</Text>
</Box>
</Box>
</Stack>
</Stack>
</Stack>
);
}
@@ -790,16 +789,16 @@ function SeasonStatsPreview({
return (
<Stack gap={4}>
<Box display="flex" alignItems="center" gap={2}>
<Stack display="flex" alignItems="center" gap={2}>
<Icon icon={Trophy} size={3.5} color="text-primary-blue" />
<Text size="xs" weight="medium" color="text-white">Season Statistics</Text>
</Box>
</Stack>
{/* Visual rounds */}
<Stack gap={2}>
<Box display="flex" alignItems="center" gap={1} flexWrap="wrap">
<Stack display="flex" alignItems="center" gap={1} flexWrap="wrap">
{Array.from({ length: Math.min(rounds, 20) }).map((_, i) => (
<Box
<Stack
key={i}
w="5"
h="5"
@@ -830,7 +829,7 @@ function SeasonStatsPreview({
>
{i + 1}
</Text>
</Box>
</Stack>
))}
{rounds > 20 && (
<Text
@@ -842,10 +841,10 @@ function SeasonStatsPreview({
+{rounds - 20}
</Text>
)}
</Box>
<Box display="flex" alignItems="center" gap={3}>
<Box display="flex" alignItems="center" gap={1}>
<Box w="2" h="2" rounded="sm" bg="bg-performance-green" />
</Stack>
<Stack display="flex" alignItems="center" gap={3}>
<Stack display="flex" alignItems="center" gap={1}>
<Stack w="2" h="2" rounded="sm" bg="bg-performance-green" />
<Text
// eslint-disable-next-line gridpilot-rules/component-classification
style={{ fontSize: '10px' }}
@@ -853,9 +852,9 @@ function SeasonStatsPreview({
>
Season start
</Text>
</Box>
<Box display="flex" alignItems="center" gap={1}>
<Box w="2" h="2" rounded="sm" bg="bg-primary-blue" />
</Stack>
<Stack display="flex" alignItems="center" gap={1}>
<Stack w="2" h="2" rounded="sm" bg="bg-primary-blue" />
<Text
// eslint-disable-next-line gridpilot-rules/component-classification
style={{ fontSize: '10px' }}
@@ -863,13 +862,13 @@ function SeasonStatsPreview({
>
Finale
</Text>
</Box>
</Box>
</Stack>
</Stack>
</Stack>
{/* Stats grid */}
<Box display="grid" gridCols={2} gap={2}>
<Box rounded="lg" bg="bg-iron-gray/30" border borderColor="border-charcoal-outline/50" p={3}>
<Stack display="grid" gridCols={2} gap={2}>
<Stack rounded="lg" bg="bg-iron-gray/30" border borderColor="border-charcoal-outline/50" p={3}>
<Text size="2xl" weight="bold" color="text-white" block>{totalSessions}</Text>
<Text
// eslint-disable-next-line gridpilot-rules/component-classification
@@ -879,8 +878,8 @@ function SeasonStatsPreview({
>
Total sessions
</Text>
</Box>
<Box rounded="lg" bg="bg-iron-gray/30" border borderColor="border-charcoal-outline/50" p={3}>
</Stack>
<Stack rounded="lg" bg="bg-iron-gray/30" border borderColor="border-charcoal-outline/50" p={3}>
<Text size="2xl" weight="bold" color="text-white" block>{Math.round(totalRaceMinutes / 60)}h</Text>
<Text
// eslint-disable-next-line gridpilot-rules/component-classification
@@ -890,8 +889,8 @@ function SeasonStatsPreview({
>
Racing time
</Text>
</Box>
<Box rounded="lg" bg="bg-iron-gray/30" border borderColor="border-charcoal-outline/50" p={3}>
</Stack>
<Stack rounded="lg" bg="bg-iron-gray/30" border borderColor="border-charcoal-outline/50" p={3}>
<Text size="2xl" weight="bold" color="text-white" block>~{weeksNeeded}</Text>
<Text
// eslint-disable-next-line gridpilot-rules/component-classification
@@ -901,8 +900,8 @@ function SeasonStatsPreview({
>
Weeks duration
</Text>
</Box>
<Box rounded="lg" bg="bg-iron-gray/30" border borderColor="border-charcoal-outline/50" p={3}>
</Stack>
<Stack rounded="lg" bg="bg-iron-gray/30" border borderColor="border-charcoal-outline/50" p={3}>
<Text size="2xl" weight="bold" color="text-white" block>{totalMinutesPerRound}</Text>
<Text
// eslint-disable-next-line gridpilot-rules/component-classification
@@ -912,8 +911,8 @@ function SeasonStatsPreview({
>
min/race day
</Text>
</Box>
</Box>
</Stack>
</Stack>
</Stack>
);
}
@@ -964,12 +963,12 @@ function InlineEditableRounds({
};
return (
<Box display="flex" alignItems="center" gap={4} p={4} rounded="xl"
<Stack display="flex" alignItems="center" gap={4} p={4} rounded="xl"
// eslint-disable-next-line gridpilot-rules/component-classification
className="bg-gradient-to-br from-iron-gray/60 to-iron-gray/30 border border-charcoal-outline"
>
{isEditing ? (
<Box
<Stack
as="input"
ref={inputRef}
type="number"
@@ -991,7 +990,7 @@ function InlineEditableRounds({
className="text-4xl outline-none"
/>
) : (
<Box
<Stack
as="button"
type="button"
onClick={() => {
@@ -1013,9 +1012,9 @@ function InlineEditableRounds({
// eslint-disable-next-line gridpilot-rules/component-classification
className="group-hover:opacity-100 text-primary-blue transition-opacity"
/>
</Box>
</Stack>
)}
<Box flexGrow={1}>
<Stack flexGrow={1}>
<Text size="sm" weight="medium" color="text-gray-300" block>rounds</Text>
<Text
// eslint-disable-next-line gridpilot-rules/component-classification
@@ -1025,9 +1024,9 @@ function InlineEditableRounds({
>
Click to edit
</Text>
</Box>
<Box display="flex" flexDirection="col" gap={1}>
<Box
</Stack>
<Stack display="flex" flexDirection="col" gap={1}>
<Stack
as="button"
type="button"
onClick={() => onChange(Math.min(value + 1, max))}
@@ -1043,8 +1042,8 @@ function InlineEditableRounds({
cursor={value >= max ? 'not-allowed' : 'pointer'}
>
<Icon icon={ChevronUp} size={4} color="text-gray-400" />
</Box>
<Box
</Stack>
<Stack
as="button"
type="button"
onClick={() => onChange(Math.max(value - 1, min))}
@@ -1060,9 +1059,9 @@ function InlineEditableRounds({
cursor={value <= min ? 'not-allowed' : 'pointer'}
>
<Icon icon={ChevronDown} size={4} color="text-gray-400" />
</Box>
</Box>
</Box>
</Stack>
</Stack>
</Stack>
);
}
@@ -1088,8 +1087,8 @@ function CollapsibleSection({
const [isOpen, setIsOpen] = useState(defaultOpen);
return (
<Box rounded="xl" border borderColor="border-charcoal-outline/50" bg="bg-iron-gray/20" overflow="hidden" transition>
<Box
<Stack rounded="xl" border borderColor="border-charcoal-outline/50" bg="bg-iron-gray/20" overflow="hidden" transition>
<Stack
as="button"
type="button"
onClick={() => setIsOpen(!isOpen)}
@@ -1101,36 +1100,36 @@ function CollapsibleSection({
transition
hoverBg="bg-iron-gray/30"
>
<Box display="flex" alignItems="center" gap={3}>
<Box display="flex" h="8" w="8" alignItems="center" justifyContent="center" rounded="lg" bg="bg-primary-blue/10" flexShrink={0}>
<Stack display="flex" alignItems="center" gap={3}>
<Stack display="flex" h="8" w="8" alignItems="center" justifyContent="center" rounded="lg" bg="bg-primary-blue/10" flexShrink={0}>
{icon}
</Box>
<Box textAlign="left">
</Stack>
<Stack textAlign="left">
<Heading level={3} fontSize="sm" weight="semibold" color="text-white">{title}</Heading>
{description && (
<Text size="xs" color="text-gray-500" mt={0.5} block>{description}</Text>
)}
</Box>
</Box>
<Box transform transition
</Stack>
</Stack>
<Stack transform transition
// eslint-disable-next-line gridpilot-rules/component-classification
className={isOpen ? 'rotate-180' : ''}
>
<Icon icon={ChevronDown} size={4} color="text-gray-400" />
</Box>
</Box>
<Box
</Stack>
</Stack>
<Stack
transition
// eslint-disable-next-line gridpilot-rules/component-classification
className={`ease-in-out ${
isOpen ? 'max-h-[2000px] opacity-100' : 'max-h-0 opacity-0 overflow-hidden'
}`}
>
<Box px={4} pb={4} pt={2} borderTop borderColor="border-charcoal-outline/30">
<Stack px={4} pb={4} pt={2} borderTop borderColor="border-charcoal-outline/30">
{children}
</Box>
</Box>
</Box>
</Stack>
</Stack>
</Stack>
);
}
@@ -1193,10 +1192,10 @@ export function LeagueTimingsSection({
<Stack gap={4}>
<Heading level={3} fontSize="lg" weight="semibold" color="text-white">Schedule & timings</Heading>
<Stack gap={3}>
<Box>
<Stack>
<Text weight="medium" color="text-gray-200">Planned rounds:</Text>{' '}
<Text color="text-gray-300">{timings.roundsPlanned ?? '—'}</Text>
</Box>
</Stack>
</Stack>
</Stack>
);
@@ -1213,7 +1212,7 @@ export function LeagueTimingsSection({
];
return (
<Box display="grid" responsiveGridCols={{ base: 1, lg: 2 }} gap={6}>
<Stack display="grid" responsiveGridCols={{ base: 1, lg: 2 }} gap={6}>
{/* LEFT COLUMN: Configuration */}
<Stack gap={4}>
{/* Session Durations - Collapsible */}
@@ -1223,7 +1222,7 @@ export function LeagueTimingsSection({
description="Configure practice, qualifying, and race lengths"
defaultOpen={false}
>
<Box display="grid" gridCols={2} gap={3}>
<Stack display="grid" gridCols={2} gap={3}>
<RangeField
label="Practice"
value={timings.practiceMinutes ?? 20}
@@ -1264,7 +1263,7 @@ export function LeagueTimingsSection({
compact
error={errors?.mainRaceMinutes}
/>
</Box>
</Stack>
</CollapsibleSection>
{/* Season Length - Collapsible */}
@@ -1301,7 +1300,7 @@ export function LeagueTimingsSection({
{/* Frequency */}
<Stack gap={2} mb={4}>
<Text as="label" size="xs" color="text-gray-400" block>How often?</Text>
<Box display="flex" gap={2}>
<Stack display="flex" gap={2}>
{[
{ id: 'weekly', label: 'Weekly' },
{ id: 'everyNWeeks', label: 'Every 2 weeks' },
@@ -1310,7 +1309,7 @@ export function LeagueTimingsSection({
(opt.id === 'weekly' && recurrenceStrategy === 'weekly') ||
(opt.id === 'everyNWeeks' && recurrenceStrategy === 'everyNWeeks');
return (
<Box
<Stack
key={opt.id}
as="button"
type="button"
@@ -1336,15 +1335,15 @@ export function LeagueTimingsSection({
style={{ fontSize: '12px', fontWeight: 500 }}
>
{opt.label}
</Box>
</Stack>
);
})}
</Box>
</Stack>
</Stack>
{/* Day selection */}
<Stack gap={2}>
<Box display="flex" alignItems="center" justifyContent="between">
<Stack display="flex" alignItems="center" justifyContent="between">
<Text as="label" size="xs" color="text-gray-400">Which days?</Text>
{weekdays.length === 0 && (
<Text
@@ -1355,13 +1354,13 @@ export function LeagueTimingsSection({
Select at least one
</Text>
)}
</Box>
</Stack>
<Box display="flex" gap={1}>
<Stack display="flex" gap={1}>
{allWeekdays.map(({ day, short }) => {
const isSelected = weekdays.includes(day);
return (
<Box
<Stack
key={day}
as="button"
type="button"
@@ -1380,10 +1379,10 @@ export function LeagueTimingsSection({
style={{ fontSize: '10px', fontWeight: 500 }}
>
{short}
</Box>
</Stack>
);
})}
</Box>
</Stack>
</Stack>
</CollapsibleSection>
@@ -1395,7 +1394,7 @@ export function LeagueTimingsSection({
defaultOpen={false}
>
<Stack gap={4}>
<Box display="grid" gridCols={2} gap={3}>
<Stack display="grid" gridCols={2} gap={3}>
<Stack gap={1.5}>
<Text as="label"
// eslint-disable-next-line gridpilot-rules/component-classification
@@ -1406,7 +1405,7 @@ export function LeagueTimingsSection({
alignItems="center"
gap={1}
>
<Box w="2" h="2" rounded="sm" bg="bg-performance-green" />
<Stack w="2" h="2" rounded="sm" bg="bg-performance-green" />
Season Start
</Text>
<Input
@@ -1427,7 +1426,7 @@ export function LeagueTimingsSection({
alignItems="center"
gap={1}
>
<Box w="2" h="2" rounded="sm" bg="bg-warning-amber" />
<Stack w="2" h="2" rounded="sm" bg="bg-warning-amber" />
Season End
</Text>
<Input
@@ -1438,10 +1437,10 @@ export function LeagueTimingsSection({
className="bg-iron-gray/30"
/>
</Stack>
</Box>
</Stack>
{timings.seasonStartDate && timings.seasonEndDate && (
<Box display="flex" alignItems="start" gap={2} p={2} rounded="lg" bg="bg-primary-blue/5" border borderColor="border-primary-blue/20">
<Stack display="flex" alignItems="start" gap={2} p={2} rounded="lg" bg="bg-primary-blue/5" border borderColor="border-primary-blue/20">
<Icon icon={Info} size={3.5} color="text-primary-blue" mt={0.5} flexShrink={0} />
<Text
// eslint-disable-next-line gridpilot-rules/component-classification
@@ -1450,7 +1449,7 @@ export function LeagueTimingsSection({
>
Races will be <Text as="span" color="text-white" weight="medium">evenly distributed</Text> between start and end dates on your selected race days.
</Text>
</Box>
</Stack>
)}
<Stack gap={1.5}>
@@ -1481,12 +1480,12 @@ export function LeagueTimingsSection({
>
Time Zone
</Text>
<Box display="grid" gridCols={2} gap={2}>
<Stack display="grid" gridCols={2} gap={2}>
{TIME_ZONES.slice(0, 2).map((tz) => {
const isSelected = (timings.timezoneId ?? 'UTC') === tz.value;
const TzIcon = tz.icon;
return (
<Box
<Stack
key={tz.value}
as="button"
type="button"
@@ -1506,7 +1505,7 @@ export function LeagueTimingsSection({
textAlign="left"
>
<Icon icon={TzIcon} size={4} color={isSelected ? (tz.value === 'track' ? 'text-performance-green' : 'text-primary-blue') : 'text-gray-400'} />
<Box flexGrow={1}>
<Stack flexGrow={1}>
<Text size="xs" weight="medium" color={isSelected ? 'text-white' : 'text-gray-300'} block>
{tz.label}
</Text>
@@ -1520,14 +1519,14 @@ export function LeagueTimingsSection({
Adjusts per track
</Text>
)}
</Box>
</Box>
</Stack>
</Stack>
);
})}
</Box>
</Stack>
{/* More time zones - expandable */}
<Box
<Stack
as="button"
type="button"
onClick={() => setShowAdvanced(!showAdvanced)}
@@ -1545,15 +1544,15 @@ export function LeagueTimingsSection({
>
{showAdvanced ? 'Hide' : 'Show'} more time zones
</Text>
</Box>
</Stack>
{showAdvanced && (
<Box display="grid" gridCols={2} gap={2} mt={2}>
<Stack display="grid" gridCols={2} gap={2} mt={2}>
{TIME_ZONES.slice(2).map((tz) => {
const isSelected = (timings.timezoneId ?? 'UTC') === tz.value;
const TzIcon = tz.icon;
return (
<Box
<Stack
key={tz.value}
as="button"
type="button"
@@ -1573,14 +1572,14 @@ export function LeagueTimingsSection({
>
<Icon icon={TzIcon} size={3.5} color={isSelected ? 'text-primary-blue' : 'text-gray-500'} />
<Text size="xs" color={isSelected ? 'text-white' : 'text-gray-400'}>{tz.label}</Text>
</Box>
</Stack>
);
})}
</Box>
</Stack>
)}
</Stack>
<Box display="flex" alignItems="start" gap={2} p={2} rounded="lg" bg="bg-performance-green/5" border borderColor="border-performance-green/20">
<Stack display="flex" alignItems="start" gap={2} p={2} rounded="lg" bg="bg-performance-green/5" border borderColor="border-performance-green/20">
<Icon icon={Info} size={3.5} color="text-performance-green" mt={0.5} flexShrink={0} />
<Text
// eslint-disable-next-line gridpilot-rules/component-classification
@@ -1592,35 +1591,35 @@ export function LeagueTimingsSection({
: 'All race times will be displayed in the selected time zone for consistency.'
}
</Text>
</Box>
</Stack>
</Stack>
</CollapsibleSection>
</Stack>
{/* RIGHT COLUMN: Live Preview */}
<Stack gap={4}>
<Box
<Stack
// eslint-disable-next-line gridpilot-rules/component-classification
style={{ position: 'sticky', top: '1rem' }}
>
<Box rounded="xl" border borderColor="border-charcoal-outline"
<Stack rounded="xl" border borderColor="border-charcoal-outline"
// eslint-disable-next-line gridpilot-rules/component-classification
className="bg-gradient-to-br from-iron-gray/80 to-deep-graphite"
overflow="hidden"
>
{/* Preview header with tabs */}
<Box display="flex" alignItems="center" justifyContent="between" p={3} borderBottom borderColor="border-charcoal-outline/50" bg="bg-deep-graphite/50">
<Box display="flex" alignItems="center" gap={2}>
<Stack display="flex" alignItems="center" justifyContent="between" p={3} borderBottom borderColor="border-charcoal-outline/50" bg="bg-deep-graphite/50">
<Stack display="flex" alignItems="center" gap={2}>
<Icon icon={Eye} size={4} color="text-primary-blue" />
<Text size="xs" weight="semibold" color="text-white">Preview</Text>
</Box>
<Box display="flex" gap={1} p={0.5} rounded="lg" bg="bg-iron-gray/60">
</Stack>
<Stack display="flex" gap={1} p={0.5} rounded="lg" bg="bg-iron-gray/60">
{[
{ id: 'day', label: 'Race Day', icon: Play },
{ id: 'year', label: 'Calendar', icon: CalendarRange },
{ id: 'stats', label: 'Stats', icon: Trophy },
].map((tab) => (
<Box
<Stack
key={tab.id}
as="button"
type="button"
@@ -1639,19 +1638,19 @@ export function LeagueTimingsSection({
style={{ fontSize: '10px', fontWeight: 500 }}
>
<Icon icon={tab.icon} size={3} />
<Box as="span"
<Stack as="span"
// eslint-disable-next-line gridpilot-rules/component-classification
className="hidden sm:inline"
>
{tab.label}
</Box>
</Box>
</Stack>
</Stack>
))}
</Box>
</Box>
</Stack>
</Stack>
{/* Preview content */}
<Box p={4}
<Stack p={4}
// eslint-disable-next-line gridpilot-rules/component-classification
style={{ minHeight: '300px' }}
>
@@ -1708,11 +1707,11 @@ export function LeagueTimingsSection({
/>
);
})()}
</Box>
</Box>
</Stack>
</Stack>
{/* Helper tip */}
<Box mt={3} display="flex" alignItems="start" gap={2} p={3} rounded="lg" bg="bg-primary-blue/5" border borderColor="border-primary-blue/20">
<Stack mt={3} display="flex" alignItems="start" gap={2} p={3} rounded="lg" bg="bg-primary-blue/5" border borderColor="border-primary-blue/20">
<Icon icon={Info} size={4} color="text-primary-blue" mt={0.5} flexShrink={0} />
<Text
// eslint-disable-next-line gridpilot-rules/component-classification
@@ -1722,9 +1721,9 @@ export function LeagueTimingsSection({
>
Preview updates live as you configure. Check <Text as="span" color="text-white" weight="medium">Race Day</Text> for session timing, <Text as="span" color="text-white" weight="medium">Calendar</Text> for the full year view, and <Text as="span" color="text-white" weight="medium">Stats</Text> for season totals.
</Text>
</Box>
</Box>
</Stack>
</Stack>
</Stack>
</Box>
</Stack>
);
}

View File

@@ -5,10 +5,9 @@ import { useState, useRef, useEffect } from 'react';
import type * as React from 'react';
import { createPortal } from 'react-dom';
import type { LeagueConfigFormModel } from '@/lib/types/LeagueConfigFormModel';
import { Box } from '@/ui/Box';
import { Stack } from '@/ui/Stack';
import { Text } from '@/ui/Text';
import { Heading } from '@/ui/Heading';
import { Stack } from '@/ui/Stack';
import { Icon } from '@/ui/Icon';
// Minimum drivers for ranked leagues
@@ -87,7 +86,7 @@ function InfoFlyout({ isOpen, onClose, title, children, anchorRef }: InfoFlyoutP
if (!isOpen || !mounted) return null;
return createPortal(
<Box
<Stack
ref={flyoutRef}
position="fixed"
zIndex={50}
@@ -100,7 +99,7 @@ function InfoFlyout({ isOpen, onClose, title, children, anchorRef }: InfoFlyoutP
// eslint-disable-next-line gridpilot-rules/component-classification
style={{ top: position.top, left: position.left, maxHeight: '80vh', overflowY: 'auto' }}
>
<Box
<Stack
display="flex"
alignItems="center"
justifyContent="between"
@@ -116,7 +115,7 @@ function InfoFlyout({ isOpen, onClose, title, children, anchorRef }: InfoFlyoutP
<Icon icon={HelpCircle} size={4} color="text-primary-blue" />
<Text size="sm" weight="semibold" color="text-white">{title}</Text>
</Stack>
<Box
<Stack
as="button"
type="button"
onClick={onClose}
@@ -130,12 +129,12 @@ function InfoFlyout({ isOpen, onClose, title, children, anchorRef }: InfoFlyoutP
hoverBg="bg-charcoal-outline"
>
<Icon icon={X} size={4} color="text-gray-400" />
</Box>
</Box>
<Box p={4}>
</Stack>
</Stack>
<Stack p={4}>
{children}
</Box>
</Box>,
</Stack>
</Stack>,
document.body
);
}
@@ -189,20 +188,20 @@ export function LeagueVisibilitySection({
return (
<Stack gap={8}>
{/* Emotional header for the step */}
<Box textAlign="center" pb={2}>
<Stack textAlign="center" pb={2}>
<Heading level={3} mb={2}>
Choose your league&apos;s destiny
</Heading>
<Text size="sm" color="text-gray-400" maxWidth="lg" mx="auto" block>
Will you compete for glory on the global leaderboards, or race with friends in a private series?
</Text>
</Box>
</Stack>
{/* League Type Selection */}
<Box display="grid" responsiveGridCols={{ base: 1, md: 2 }} gap={6}>
<Stack display="grid" responsiveGridCols={{ base: 1, md: 2 }} gap={6}>
{/* Ranked (Public) Option */}
<Box position="relative">
<Box
<Stack position="relative">
<Stack
as="button"
type="button"
disabled={disabled}
@@ -227,9 +226,9 @@ export function LeagueVisibilitySection({
group
>
{/* Header */}
<Box display="flex" alignItems="start" justifyContent="between">
<Stack display="flex" alignItems="start" justifyContent="between">
<Stack direction="row" align="center" gap={3}>
<Box
<Stack
display="flex"
h="14"
w="14"
@@ -239,18 +238,18 @@ export function LeagueVisibilitySection({
bg={isRanked ? 'bg-primary-blue/30' : 'bg-charcoal-outline/50'}
>
<Icon icon={Trophy} size={7} color={isRanked ? 'text-primary-blue' : 'text-gray-400'} />
</Box>
<Box>
</Stack>
<Stack>
<Text weight="bold" size="xl" color={isRanked ? 'text-white' : 'text-gray-300'} block>
Ranked
</Text>
<Text size="sm" color={isRanked ? 'text-primary-blue' : 'text-gray-500'} block>
Compete for glory
</Text>
</Box>
</Stack>
</Stack>
{/* Radio indicator */}
<Box
<Stack
display="flex"
h="7"
w="7"
@@ -264,8 +263,8 @@ export function LeagueVisibilitySection({
transition
>
{isRanked && <Icon icon={Check} size={4} color="text-white" />}
</Box>
</Box>
</Stack>
</Stack>
{/* Emotional tagline */}
<Text size="sm" color={isRanked ? 'text-gray-300' : 'text-gray-500'} block>
@@ -289,16 +288,16 @@ export function LeagueVisibilitySection({
</Stack>
{/* Requirement badge */}
<Box display="flex" alignItems="center" gap={2} mt="auto" px={3} py={2} rounded="lg" bg="bg-warning-amber/10" border borderColor="border-warning-amber/20" w="fit">
<Stack display="flex" alignItems="center" gap={2} mt="auto" px={3} py={2} rounded="lg" bg="bg-warning-amber/10" border borderColor="border-warning-amber/20" w="fit">
<Icon icon={Users} size={4} color="text-warning-amber" />
<Text size="xs" color="text-warning-amber" weight="medium">
Requires {MIN_RANKED_DRIVERS}+ drivers for competitive integrity
</Text>
</Box>
</Box>
</Stack>
</Stack>
{/* Info button */}
<Box
<Stack
as="button"
ref={rankedInfoRef}
type="button"
@@ -318,8 +317,8 @@ export function LeagueVisibilitySection({
hoverBg="bg-primary-blue/10"
>
<Icon icon={HelpCircle} size={4} />
</Box>
</Box>
</Stack>
</Stack>
{/* Ranked Info Flyout */}
<InfoFlyout
@@ -343,16 +342,16 @@ export function LeagueVisibilitySection({
Requirements
</Text>
<Stack gap={1.5}>
<Box display="flex" alignItems="start" gap={2}>
<Stack display="flex" alignItems="start" gap={2}>
<Icon icon={Users} size={3.5} color="text-warning-amber" flexShrink={0} mt={0.5} />
<Text size="xs" color="text-gray-400">
<Text weight="bold" color="text-white">Minimum {MIN_RANKED_DRIVERS} drivers</Text> for competitive integrity
</Text>
</Box>
<Box display="flex" alignItems="start" gap={2}>
</Stack>
<Stack display="flex" alignItems="start" gap={2}>
<Icon icon={Check} size={3.5} color="text-performance-green" flexShrink={0} mt={0.5} />
<Text size="xs" color="text-gray-400">Anyone can discover and join your league</Text>
</Box>
</Stack>
</Stack>
</Stack>
@@ -365,22 +364,22 @@ export function LeagueVisibilitySection({
Benefits
</Text>
<Stack gap={1.5}>
<Box display="flex" alignItems="start" gap={2}>
<Stack display="flex" alignItems="start" gap={2}>
<Icon icon={Trophy} size={3.5} color="text-primary-blue" flexShrink={0} mt={0.5} />
<Text size="xs" color="text-gray-400">Results affect driver ratings and rankings</Text>
</Box>
<Box display="flex" alignItems="start" gap={2}>
</Stack>
<Stack display="flex" alignItems="start" gap={2}>
<Icon icon={Check} size={3.5} color="text-performance-green" flexShrink={0} mt={0.5} />
<Text size="xs" color="text-gray-400">Featured in league discovery</Text>
</Box>
</Stack>
</Stack>
</Stack>
</Stack>
</InfoFlyout>
{/* Unranked (Private) Option */}
<Box position="relative">
<Box
<Stack position="relative">
<Stack
as="button"
type="button"
disabled={disabled}
@@ -405,9 +404,9 @@ export function LeagueVisibilitySection({
group
>
{/* Header */}
<Box display="flex" alignItems="start" justifyContent="between">
<Stack display="flex" alignItems="start" justifyContent="between">
<Stack direction="row" align="center" gap={3}>
<Box
<Stack
display="flex"
h="14"
w="14"
@@ -417,18 +416,18 @@ export function LeagueVisibilitySection({
bg={!isRanked ? 'bg-neon-aqua/30' : 'bg-charcoal-outline/50'}
>
<Icon icon={Users} size={7} color={!isRanked ? 'text-neon-aqua' : 'text-gray-400'} />
</Box>
<Box>
</Stack>
<Stack>
<Text weight="bold" size="xl" color={!isRanked ? 'text-white' : 'text-gray-300'} block>
Unranked
</Text>
<Text size="sm" color={!isRanked ? 'text-neon-aqua' : 'text-gray-500'} block>
Race with friends
</Text>
</Box>
</Stack>
</Stack>
{/* Radio indicator */}
<Box
<Stack
display="flex"
h="7"
w="7"
@@ -442,8 +441,8 @@ export function LeagueVisibilitySection({
transition
>
{!isRanked && <Icon icon={Check} size={4} color="text-deep-graphite" />}
</Box>
</Box>
</Stack>
</Stack>
{/* Emotional tagline */}
<Text size="sm" color={!isRanked ? 'text-gray-300' : 'text-gray-500'} block>
@@ -467,16 +466,16 @@ export function LeagueVisibilitySection({
</Stack>
{/* Flexibility badge */}
<Box display="flex" alignItems="center" gap={2} mt="auto" px={3} py={2} rounded="lg" bg="bg-neon-aqua/10" border borderColor="border-neon-aqua/20" w="fit">
<Stack display="flex" alignItems="center" gap={2} mt="auto" px={3} py={2} rounded="lg" bg="bg-neon-aqua/10" border borderColor="border-neon-aqua/20" w="fit">
<Icon icon={Users} size={4} color={!isRanked ? 'text-neon-aqua' : 'text-gray-400'} />
<Text size="xs" color={!isRanked ? 'text-neon-aqua' : 'text-gray-400'} weight="medium">
Any size even 2 friends
</Text>
</Box>
</Box>
</Stack>
</Stack>
{/* Info button */}
<Box
<Stack
as="button"
ref={unrankedInfoRef}
type="button"
@@ -496,8 +495,8 @@ export function LeagueVisibilitySection({
hoverBg="bg-neon-aqua/10"
>
<Icon icon={HelpCircle} size={4} />
</Box>
</Box>
</Stack>
</Stack>
{/* Unranked Info Flyout */}
<InfoFlyout
@@ -522,18 +521,18 @@ export function LeagueVisibilitySection({
Perfect For
</Text>
<Stack gap={1.5}>
<Box display="flex" alignItems="start" gap={2}>
<Stack display="flex" alignItems="start" gap={2}>
<Icon icon={Check} size={3.5} color="text-neon-aqua" flexShrink={0} mt={0.5} />
<Text size="xs" color="text-gray-400">Private racing with friends</Text>
</Box>
<Box display="flex" alignItems="start" gap={2}>
</Stack>
<Stack display="flex" alignItems="start" gap={2}>
<Icon icon={Check} size={3.5} color="text-neon-aqua" flexShrink={0} mt={0.5} />
<Text size="xs" color="text-gray-400">Practice and training sessions</Text>
</Box>
<Box display="flex" alignItems="start" gap={2}>
</Stack>
<Stack display="flex" alignItems="start" gap={2}>
<Icon icon={Check} size={3.5} color="text-neon-aqua" flexShrink={0} mt={0.5} />
<Text size="xs" color="text-gray-400">Small groups (2+ drivers)</Text>
</Box>
</Stack>
</Stack>
</Stack>
@@ -546,29 +545,29 @@ export function LeagueVisibilitySection({
Features
</Text>
<Stack gap={1.5}>
<Box display="flex" alignItems="start" gap={2}>
<Stack display="flex" alignItems="start" gap={2}>
<Icon icon={Users} size={3.5} color="text-neon-aqua" flexShrink={0} mt={0.5} />
<Text size="xs" color="text-gray-400">Invite-only membership</Text>
</Box>
<Box display="flex" alignItems="start" gap={2}>
</Stack>
<Stack display="flex" alignItems="start" gap={2}>
<Icon icon={Check} size={3.5} color="text-neon-aqua" flexShrink={0} mt={0.5} />
<Text size="xs" color="text-gray-400">Full stats and standings (internal only)</Text>
</Box>
</Stack>
</Stack>
</Stack>
</Stack>
</InfoFlyout>
</Box>
</Stack>
{errors?.visibility && (
<Box display="flex" alignItems="center" gap={2} p={3} rounded="lg" bg="bg-warning-amber/10" border borderColor="border-warning-amber/20">
<Stack display="flex" alignItems="center" gap={2} p={3} rounded="lg" bg="bg-warning-amber/10" border borderColor="border-warning-amber/20">
<Icon icon={HelpCircle} size={4} color="text-warning-amber" flexShrink={0} />
<Text size="xs" color="text-warning-amber">{errors.visibility}</Text>
</Box>
</Stack>
)}
{/* Contextual info based on selection */}
<Box
<Stack
rounded="xl"
p={5}
border
@@ -576,33 +575,33 @@ export function LeagueVisibilitySection({
bg={isRanked ? 'bg-primary-blue/5' : 'bg-neon-aqua/5'}
borderColor={isRanked ? 'border-primary-blue/20' : 'border-neon-aqua/20'}
>
<Box display="flex" alignItems="start" gap={3}>
<Stack display="flex" alignItems="start" gap={3}>
{isRanked ? (
<>
<Icon icon={Trophy} size={5} color="text-primary-blue" flexShrink={0} mt={0.5} />
<Box>
<Stack>
<Text size="sm" weight="medium" color="text-white" block mb={1}>Ready to compete</Text>
<Text size="xs" color="text-gray-400" block>
Your league will be visible to all GridPilot drivers. Results will affect driver ratings
and contribute to the global leaderboards. Make sure you have at least {MIN_RANKED_DRIVERS} drivers
to ensure competitive integrity.
</Text>
</Box>
</Stack>
</>
) : (
<>
<Icon icon={Users} size={5} color="text-neon-aqua" flexShrink={0} mt={0.5} />
<Box>
<Stack>
<Text size="sm" weight="medium" color="text-white" block mb={1}>Private racing awaits</Text>
<Text size="xs" color="text-gray-400" block>
Your league will be invite-only. Perfect for racing with friends, practice sessions,
or any time you want to have fun without affecting your official ratings.
</Text>
</Box>
</Stack>
</>
)}
</Box>
</Box>
</Stack>
</Stack>
</Stack>
);
}

View File

@@ -5,7 +5,6 @@ import { ProtestViewModel } from "@/lib/view-models/ProtestViewModel";
import { RaceViewModel } from "@/lib/view-models/RaceViewModel";
import { DriverViewModel } from "@/lib/view-models/DriverViewModel";
import { Card } from "@/ui/Card";
import { Box } from "@/ui/Box";
import { Stack } from "@/ui/Stack";
import { Text } from "@/ui/Text";
import { Heading } from "@/ui/Heading";
@@ -45,15 +44,15 @@ export function PenaltyHistoryList({
return (
<Stack gap={4}>
{filteredProtests.length === 0 ? (
<Card py={12} textAlign="center">
<Stack alignItems="center" gap={4}>
<Card py={12} className="text-center">
<Stack align="center" gap={4}>
<Icon icon={AlertCircle} size={12} color="text-gray-400" opacity={0.5} />
<Box>
<Stack>
<Text weight="medium" size="lg" color="text-gray-400" block>No Resolved Protests</Text>
<Text size="sm" color="text-gray-500" mt={1} block>
No protests have been resolved in this league
</Text>
</Box>
</Stack>
</Stack>
</Card>
) : (
@@ -68,45 +67,42 @@ export function PenaltyHistoryList({
return (
<Card key={protest.id} p={4}>
<Box display="flex" alignItems="start" gap={4}>
<Box
<Stack direction="row" align="start" gap={4}>
<Stack
w="10"
h="10"
rounded="full"
display="flex"
alignItems="center"
justifyContent="center"
align="center"
justify="center"
flexShrink={0}
bg={statusColors.bg}
color={statusColors.text}
>
<Icon icon={Flag} size={5} />
</Box>
<Box flex={1}>
</Stack>
<Stack flex={1}>
<Stack gap={2}>
<Box display="flex" alignItems="start" justifyContent="between" gap={4}>
<Box>
<Stack direction="row" align="start" justify="between" gap={4}>
<Stack>
<Heading level={3}>
Protest #{protest.id.substring(0, 8)}
</Heading>
<Text size="sm" color="text-gray-400" block>
{resolvedDate ? `Resolved ${new Date(resolvedDate).toLocaleDateString()}` : 'Resolved'}
</Text>
</Box>
<Box
</Stack>
<Stack
px={3}
py={1}
rounded="full"
bg={statusColors.bg}
color={statusColors.text}
fontSize="12px"
weight="medium"
flexShrink={0}
className="text-[12px] font-medium flex-shrink-0"
>
{protest.status.toUpperCase()}
</Box>
</Box>
<Box>
</Stack>
</Stack>
<Stack>
<Text size="sm" color="text-gray-400" block>
<Text weight="medium" color="text-white">{protester?.name || 'Unknown'}</Text> vs <Text weight="medium" color="text-white">{accused?.name || 'Unknown'}</Text>
</Text>
@@ -115,20 +111,20 @@ export function PenaltyHistoryList({
{race.track} ({race.car}) - Lap {incident.lap}
</Text>
)}
</Box>
</Stack>
{incident && (
<Text size="sm" color="text-gray-300" block>{incident.description}</Text>
)}
{protest.decisionNotes && (
<Box mt={2} p={2} rounded="md" bg="bg-iron-gray/30" border borderColor="border-charcoal-outline/50">
<Stack mt={2} p={2} rounded="md" bg="bg-iron-gray/30" border borderColor="border-charcoal-outline/50">
<Text size="xs" color="text-gray-400" block>
<Text weight="medium">Steward Notes:</Text> {protest.decisionNotes}
</Text>
</Box>
</Stack>
)}
</Stack>
</Box>
</Box>
</Stack>
</Stack>
</Card>
);
})}
@@ -136,4 +132,4 @@ export function PenaltyHistoryList({
)}
</Stack>
);
}
}

View File

@@ -1,12 +1,9 @@
import { DriverViewModel } from "@/lib/view-models/DriverViewModel";
import { ProtestViewModel } from "@/lib/view-models/ProtestViewModel";
import { RaceViewModel } from "@/lib/view-models/RaceViewModel";
import { Box } from "@/ui/Box";
import { Stack } from "@/ui/Stack";
import { Card } from "@/ui/Card";
import { ProtestListItem } from "./ProtestListItem";
import { Stack } from "@/ui/Stack";
import { Text } from "@/ui/Text";
import { Flag } from "lucide-react";
@@ -28,17 +25,15 @@ export function PendingProtestsList({
if (protests.length === 0) {
return (
<Card>
<Box p={12} textAlign="center">
<Stack align="center" gap={4}>
<Box w="16" h="16" rounded="full" bg="bg-performance-green/10" display="flex" alignItems="center" justifyContent="center">
<Flag className="h-8 w-8 text-performance-green" />
</Box>
<Box>
<Text weight="semibold" size="lg" color="text-white" block mb={2}>All Clear! 🏁</Text>
<Text size="sm" color="text-gray-400">No pending protests to review</Text>
</Box>
<Stack p={12} align="center" gap={4}>
<Stack w="16" h="16" rounded="full" bg="bg-performance-green/10" align="center" justify="center">
<Flag className="h-8 w-8 text-performance-green" />
</Stack>
</Box>
<Stack align="center">
<Text weight="semibold" size="lg" color="text-white" block mb={2}>All Clear! 🏁</Text>
<Text size="sm" color="text-gray-400">No pending protests to review</Text>
</Stack>
</Stack>
</Card>
);
}

View File

@@ -2,12 +2,11 @@
import { AlertCircle, AlertTriangle, Video } from 'lucide-react';
import { Badge } from '@/ui/Badge';
import { Box } from '@/ui/Box';
import { Stack } from '@/ui/Stack';
import { Button } from '@/ui/Button';
import { Card } from '@/ui/Card';
import { Icon } from '@/ui/Icon';
import { Link } from '@/ui/Link';
import { Stack } from '@/ui/Stack';
import { Text } from '@/ui/Text';
interface ProtestListItemProps {
@@ -61,7 +60,7 @@ export function ProtestListItem({
style={isUrgent ? { borderLeftWidth: '4px' } : undefined}
>
<Stack direction="row" align="start" justify="between" gap={4}>
<Box flexGrow={1} minWidth="0">
<Stack flexGrow={1} minWidth="0">
<Stack direction="row" align="center" gap={2} mb={2} wrap>
<Icon icon={AlertCircle} size={4} color="rgb(156, 163, 175)" />
<Link href={protesterHref}>
@@ -97,12 +96,12 @@ export function ProtestListItem({
<Text size="sm" color="text-gray-300" block>{description}</Text>
{decisionNotes && (
<Box mt={4} p={3} bg="bg-charcoal-outline/30" rounded="lg" border borderColor="border-charcoal-outline/50">
<Stack mt={4} p={3} bg="bg-charcoal-outline/30" rounded="lg" border borderColor="border-charcoal-outline/50">
<Text size="xs" color="text-gray-500" uppercase letterSpacing="0.05em" block mb={1}>Steward Decision</Text>
<Text size="sm" color="text-gray-300">{decisionNotes}</Text>
</Box>
</Stack>
)}
</Box>
</Stack>
{isAdmin && status === 'pending' && onReview && (
<Button
variant="primary"

View File

@@ -4,9 +4,8 @@ import { useState } from 'react';
import { Button } from '@/ui/Button';
import { usePenaltyMutation } from "@/hooks/league/usePenaltyMutation";
import { AlertTriangle, Clock, Flag, Zap } from 'lucide-react';
import { Box } from '@/ui/Box';
import { Text } from '@/ui/Text';
import { Stack } from '@/ui/Stack';
import { Text } from '@/ui/Text';
import { Heading } from '@/ui/Heading';
import { Icon } from '@/ui/Icon';
import { Select } from '@/ui/Select';
@@ -78,18 +77,18 @@ export function QuickPenaltyModal({ raceId, drivers, onClose, onRefresh, preSele
};
return (
<Box position="fixed" inset="0" zIndex={50} display="flex" alignItems="center" justifyContent="center" p={4} bg="bg-black/70"
<Stack position="fixed" inset="0" zIndex={50} display="flex" alignItems="center" justifyContent="center" p={4} bg="bg-black/70"
// eslint-disable-next-line gridpilot-rules/component-classification
className="backdrop-blur-sm"
>
<Box w="full" maxWidth="md" bg="bg-iron-gray" rounded="xl" border borderColor="border-charcoal-outline" shadow="2xl">
<Box p={6}>
<Stack w="full" maxWidth="md" bg="bg-iron-gray" rounded="xl" border borderColor="border-charcoal-outline" shadow="2xl">
<Stack p={6}>
<Heading level={2} fontSize="xl" weight="bold" color="text-white" mb={4}>Quick Penalty</Heading>
<Box as="form" onSubmit={handleSubmit} display="flex" flexDirection="col" gap={4}>
<Stack as="form" onSubmit={handleSubmit} display="flex" flexDirection="col" gap={4}>
{/* Race Selection */}
{races && !raceId && (
<Box>
<Stack>
<Text as="label" size="sm" weight="medium" color="text-gray-300" block mb={2}>
Race
</Text>
@@ -105,18 +104,18 @@ export function QuickPenaltyModal({ raceId, drivers, onClose, onRefresh, preSele
})),
]}
/>
</Box>
</Stack>
)}
{/* Driver Selection */}
<Box>
<Stack>
<Text as="label" size="sm" weight="medium" color="text-gray-300" block mb={2}>
Driver
</Text>
{preSelectedDriver ? (
<Box w="full" px={3} py={2} bg="bg-deep-graphite" border borderColor="border-charcoal-outline" rounded="lg" color="text-white">
<Stack w="full" px={3} py={2} bg="bg-deep-graphite" border borderColor="border-charcoal-outline" rounded="lg" color="text-white">
{preSelectedDriver.name}
</Box>
</Stack>
) : (
<Select
value={selectedDriver}
@@ -131,16 +130,16 @@ export function QuickPenaltyModal({ raceId, drivers, onClose, onRefresh, preSele
]}
/>
)}
</Box>
</Stack>
{/* Infraction Type */}
<Box>
<Stack>
<Text as="label" size="sm" weight="medium" color="text-gray-300" block mb={2}>
Infraction Type
</Text>
<Box display="grid" gridCols={2} gap={2}>
<Stack display="grid" gridCols={2} gap={2}>
{INFRACTION_TYPES.map(({ value, label, icon: InfractionIcon }) => (
<Box
<Stack
key={value}
as="button"
type="button"
@@ -159,19 +158,19 @@ export function QuickPenaltyModal({ raceId, drivers, onClose, onRefresh, preSele
>
<Icon icon={InfractionIcon} size={4} />
<Text size="sm">{label}</Text>
</Box>
</Stack>
))}
</Box>
</Box>
</Stack>
</Stack>
{/* Severity */}
<Box>
<Stack>
<Text as="label" size="sm" weight="medium" color="text-gray-300" block mb={2}>
Severity
</Text>
<Stack gap={2}>
{SEVERITY_LEVELS.map(({ value, label, description }) => (
<Box
<Stack
key={value}
as="button"
type="button"
@@ -189,13 +188,13 @@ export function QuickPenaltyModal({ raceId, drivers, onClose, onRefresh, preSele
>
<Text weight="medium" block>{label}</Text>
<Text size="xs" opacity={0.75} block>{description}</Text>
</Box>
</Stack>
))}
</Stack>
</Box>
</Stack>
{/* Notes */}
<Box>
<Stack>
<Text as="label" size="sm" weight="medium" color="text-gray-300" block mb={2}>
Notes (Optional)
</Text>
@@ -205,16 +204,16 @@ export function QuickPenaltyModal({ raceId, drivers, onClose, onRefresh, preSele
placeholder="Additional details..."
rows={3}
/>
</Box>
</Stack>
{error && (
<Box p={3} bg="bg-red-500/10" border borderColor="border-red-500/20" rounded="lg">
<Stack p={3} bg="bg-red-500/10" border borderColor="border-red-500/20" rounded="lg">
<Text size="sm" color="text-red-400" block>{error}</Text>
</Box>
</Stack>
)}
{/* Actions */}
<Box display="flex" gap={3} pt={4}>
<Stack display="flex" gap={3} pt={4}>
<Button
type="button"
variant="secondary"
@@ -232,10 +231,10 @@ export function QuickPenaltyModal({ raceId, drivers, onClose, onRefresh, preSele
>
{penaltyMutation.isPending ? 'Applying...' : 'Apply Penalty'}
</Button>
</Box>
</Box>
</Box>
</Box>
</Box>
</Stack>
</Stack>
</Stack>
</Stack>
</Stack>
);
}

View File

@@ -7,13 +7,13 @@ import { ProtestViewModel } from "../../lib/view-models/ProtestViewModel";
import { Modal } from "@/ui/Modal";
import { Button } from "@/ui/Button";
import { Card } from "@/ui/Card";
import { Box } from "@/ui/Box";
import { Text } from "@/ui/Text";
import { Stack } from "@/ui/Stack";
import { Text } from "@/ui/Text";
import { Heading } from "@/ui/Heading";
import { Icon } from "@/ui/Icon";
import { TextArea } from "@/ui/TextArea";
import { Input } from "@/ui/Input";
import { Grid } from "@/ui/Grid";
import {
AlertCircle,
Video,
@@ -210,39 +210,39 @@ export function ReviewProtestModal({
return (
<Modal title="Confirm Decision" isOpen={true} onOpenChange={() => setShowConfirmation(false)}>
<Stack gap={6} p={6}>
<Box textAlign="center">
<Stack align="center">
<Stack gap={4}>
{decision === "accept" ? (
<Box display="flex" justifyContent="center">
<Box h="16" w="16" rounded="full" bg="bg-orange-500/20" display="flex" alignItems="center" justifyContent="center">
<Stack direction="row" justify="center">
<Stack h="16" w="16" rounded="full" bg="bg-orange-500/20" align="center" justify="center">
<Icon icon={AlertCircle} size={8} color="text-orange-400" />
</Box>
</Box>
</Stack>
</Stack>
) : (
<Box display="flex" justifyContent="center">
<Box h="16" w="16" rounded="full" bg="bg-gray-500/20" display="flex" alignItems="center" justifyContent="center">
<Stack direction="row" justify="center">
<Stack h="16" w="16" rounded="full" bg="bg-gray-500/20" align="center" justify="center">
<Icon icon={XCircle} size={8} color="text-gray-400" />
</Box>
</Box>
</Stack>
</Stack>
)}
<Box>
<Heading level={3} fontSize="xl" weight="bold" color="text-white">Confirm Decision</Heading>
<Text color="text-gray-400" mt={2} block>
<Stack align="center">
<Heading level={3} weight="bold" color="text-white">Confirm Decision</Heading>
<Text color="text-gray-400" mt={2} block textAlign="center">
{decision === "accept"
? (selectedPenalty?.requiresValue
? `Issue ${penaltyValue} ${selectedPenalty.valueLabel} penalty?`
: `Issue ${selectedPenalty?.name ?? penaltyType} penalty?`)
: "Reject this protest?"}
</Text>
</Box>
</Stack>
</Stack>
</Box>
</Stack>
<Card p={4} bg="bg-gray-800/50">
<Card p={4} className="bg-gray-800/50">
<Text size="sm" color="text-gray-300" block>{stewardNotes}</Text>
</Card>
<Box display="flex" gap={3}>
<Stack direction="row" gap={3}>
<Button
variant="secondary"
fullWidth
@@ -259,7 +259,7 @@ export function ReviewProtestModal({
>
{submitting ? "Submitting..." : "Confirm Decision"}
</Button>
</Box>
</Stack>
</Stack>
</Modal>
);
@@ -268,94 +268,95 @@ export function ReviewProtestModal({
return (
<Modal title="Review Protest" isOpen={true} onOpenChange={onClose}>
<Stack gap={6} p={6}>
<Box display="flex" alignItems="start" gap={4}>
<Box h="12" w="12" rounded="full" bg="bg-orange-500/20" display="flex" alignItems="center" justifyContent="center" flexShrink={0}>
<Stack direction="row" align="start" gap={4}>
<Stack h="12" w="12" rounded="full" bg="bg-orange-500/20" align="center" justify="center" flexShrink={0}>
<Icon icon={AlertCircle} size={6} color="text-orange-400" />
</Box>
<Box flexGrow={1}>
<Heading level={2} fontSize="2xl" weight="bold" color="text-white">Review Protest</Heading>
</Stack>
<Stack flexGrow={1}>
<Heading level={2} weight="bold" color="text-white">Review Protest</Heading>
<Text color="text-gray-400" mt={1} block>
Protest #{protest.id.substring(0, 8)}
</Text>
</Box>
</Box>
</Stack>
</Stack>
<Stack gap={4}>
<Card p={4} bg="bg-gray-800/50">
<Card p={4} className="bg-gray-800/50">
<Stack gap={3}>
<Box display="flex" alignItems="center" justifyContent="between">
<Stack direction="row" align="center" justify="between">
<Text size="sm" color="text-gray-400">Filed Date</Text>
<Text size="sm" color="text-white" weight="medium">
{new Date(protest.filedAt || protest.submittedAt).toLocaleString()}
</Text>
</Box>
<Box display="flex" alignItems="center" justifyContent="between">
</Stack>
<Stack direction="row" align="center" justify="between">
<Text size="sm" color="text-gray-400">Incident Lap</Text>
<Text size="sm" color="text-white" weight="medium">
Lap {protest.incident?.lap || 'N/A'}
</Text>
</Box>
<Box display="flex" alignItems="center" justifyContent="between">
</Stack>
<Stack direction="row" align="center" justify="between">
<Text size="sm" color="text-gray-400">Status</Text>
<Box as="span" px={2} py={1} rounded="sm" bg="bg-orange-500/20">
<Stack as="span" px={2} py={1} rounded="sm" bg="bg-orange-500/20">
<Text size="xs" weight="medium" color="text-orange-400">
{protest.status}
</Text>
</Box>
</Box>
</Stack>
</Stack>
</Stack>
</Card>
<Box>
<Stack>
<Text as="label" size="sm" weight="medium" color="text-gray-400" block mb={2}>
Description
</Text>
<Card p={4} bg="bg-gray-800/50">
<Card p={4} className="bg-gray-800/50">
<Text color="text-gray-300" block>{protest.incident?.description || protest.description}</Text>
</Card>
</Box>
</Stack>
{protest.comment && (
<Box>
<Stack>
<Text as="label" size="sm" weight="medium" color="text-gray-400" block mb={2}>
Additional Comment
</Text>
<Card p={4} bg="bg-gray-800/50">
<Card p={4} className="bg-gray-800/50">
<Text color="text-gray-300" block>{protest.comment}</Text>
</Card>
</Box>
</Stack>
)}
{protest.proofVideoUrl && (
<Box>
<Stack>
<Text as="label" size="sm" weight="medium" color="text-gray-400" block mb={2}>
Evidence
</Text>
<Card p={4} bg="bg-gray-800/50">
<Box
<Card p={4} className="bg-gray-800/50">
<Stack
as="a"
href={protest.proofVideoUrl}
target="_blank"
rel="noopener noreferrer"
display="flex"
alignItems="center"
{...({
href: protest.proofVideoUrl,
target: "_blank",
rel: "noopener noreferrer"
} as any)}
direction="row"
align="center"
gap={2}
color="text-orange-400"
hoverTextColor="text-orange-300"
transition
className="transition-all hover:text-orange-300"
>
<Icon icon={Video} size={4} />
<Text size="sm">View video evidence</Text>
</Box>
</Stack>
</Card>
</Box>
</Stack>
)}
</Stack>
<Box borderTop borderColor="border-gray-800" pt={6}>
<Stack borderTop borderColor="border-gray-800" pt={6}>
<Stack gap={4}>
<Heading level={3} fontSize="lg" weight="semibold" color="text-white">Stewarding Decision</Heading>
<Heading level={3} weight="semibold" color="text-white">Stewarding Decision</Heading>
<Box display="grid" gridCols={2} gap={3}>
<Grid cols={2} gap={3}>
<Button
variant={decision === "accept" ? "primary" : "secondary"}
fullWidth
@@ -376,11 +377,11 @@ export function ReviewProtestModal({
Reject Protest
</Stack>
</Button>
</Box>
</Grid>
{decision === "accept" && (
<Stack gap={4}>
<Box>
<Stack>
<Text as="label" size="sm" weight="medium" color="text-gray-400" block mb={2}>
Penalty Type
</Text>
@@ -388,11 +389,11 @@ export function ReviewProtestModal({
{penaltyTypesLoading ? (
<Text size="sm" color="text-gray-500">Loading penalty types</Text>
) : (
<Box display="grid" gridCols={3} gap={2}>
<Grid cols={3} gap={2}>
{penaltyOptions.map(({ type, name, Icon: PenaltyIcon, colorClass, defaultValue }: { type: string; name: string; Icon: LucideIcon; colorClass: string; defaultValue: number }) => {
const isSelected = penaltyType === type;
return (
<Box
<Stack
key={type}
as="button"
onClick={() => {
@@ -402,25 +403,20 @@ export function ReviewProtestModal({
p={3}
rounded="lg"
border
borderWidth={isSelected ? "2px" : "1px"}
transition
borderColor={isSelected ? undefined : "border-charcoal-outline"}
bg={isSelected ? undefined : "bg-iron-gray/50"}
hoverBorderColor={!isSelected ? "border-gray-600" : undefined}
// eslint-disable-next-line gridpilot-rules/component-classification
className={isSelected ? colorClass : ""}
{...({ borderWidth: isSelected ? "2px" : "1px" } as any)}
className={`transition-all ${isSelected ? colorClass : "bg-iron-gray/50 border-charcoal-outline hover:border-gray-600"}`}
>
<Icon icon={PenaltyIcon} size={5} mx="auto" mb={1} color={isSelected ? undefined : "text-gray-400"} />
<Icon icon={PenaltyIcon} size={5} className="mx-auto mb-1" color={isSelected ? undefined : "text-gray-400"} />
<Text size="xs" weight="medium" color={isSelected ? undefined : "text-gray-400"} block textAlign="center">{name}</Text>
</Box>
</Stack>
);
})}
</Box>
</Grid>
)}
</Box>
</Stack>
{selectedPenalty?.requiresValue && (
<Box>
<Stack>
<Text as="label" size="sm" weight="medium" color="text-gray-400" block mb={2}>
Penalty Value ({selectedPenalty.valueLabel})
</Text>
@@ -430,12 +426,12 @@ export function ReviewProtestModal({
onChange={(e: React.ChangeEvent<HTMLInputElement>) => setPenaltyValue(Number(e.target.value))}
min={1}
/>
</Box>
</Stack>
)}
</Stack>
)}
<Box>
<Stack>
<Text as="label" size="sm" weight="medium" color="text-gray-400" block mb={2}>
Steward Notes *
</Text>
@@ -445,11 +441,11 @@ export function ReviewProtestModal({
placeholder="Explain your decision and reasoning..."
rows={4}
/>
</Box>
</Stack>
</Stack>
</Box>
</Stack>
<Box display="flex" gap={3} pt={4} borderTop borderColor="border-gray-800">
<Stack direction="row" gap={3} pt={4} borderTop borderColor="border-gray-800">
<Button
variant="secondary"
fullWidth
@@ -466,7 +462,7 @@ export function ReviewProtestModal({
>
Submit Decision
</Button>
</Box>
</Stack>
</Stack>
</Modal>
);

View File

@@ -1,5 +1,4 @@
import React, { ReactNode } from 'react';
import { Box } from '@/ui/Box';
import { Stack } from '@/ui/Stack';
import { Text } from '@/ui/Text';
import { Table, TableHead, TableBody, TableRow, TableHeader, TableCell } from '@/ui/Table';
@@ -11,7 +10,7 @@ interface RosterTableProps {
export function RosterTable({ children, columns = ['Driver', 'Role', 'Joined', 'Rating', 'Rank'] }: RosterTableProps) {
return (
<Box border borderColor="border-steel-grey" bg="surface-charcoal/50" overflow="hidden">
<Stack border borderColor="border-steel-grey" bg="surface-charcoal/50" overflow="hidden">
<Table>
<TableHead className="bg-base-graphite/50">
<TableRow>
@@ -31,7 +30,7 @@ export function RosterTable({ children, columns = ['Driver', 'Role', 'Joined', '
{children}
</TableBody>
</Table>
</Box>
</Stack>
);
}

View File

@@ -3,7 +3,6 @@
import React from 'react';
import { Calendar, Clock, MapPin, Car, Trophy } from 'lucide-react';
import { Card } from '@/ui/Card';
import { Box } from '@/ui/Box';
import { Stack } from '@/ui/Stack';
import { Text } from '@/ui/Text';
import { Heading } from '@/ui/Heading';
@@ -31,7 +30,7 @@ export function ScheduleRaceCard({ race }: ScheduleRaceCardProps) {
<Card>
<Stack gap={4}>
<Stack direction="row" align="center" gap={3}>
<Box w="3" h="3" rounded="full" bg={race.isPast ? 'bg-performance-green' : 'bg-primary-blue'} />
<Stack w="3" h="3" rounded="full" bg={race.isPast ? 'bg-performance-green' : 'bg-primary-blue'} />
<Heading level={3} fontSize="lg">{race.name}</Heading>
<Badge variant={race.status === 'completed' ? 'success' : 'primary'}>
{race.status === 'completed' ? 'Completed' : 'Scheduled'}

View File

@@ -1,7 +1,6 @@
'use client';
import React from 'react';
import { Box } from '@/ui/Box';
import { Stack } from '@/ui/Stack';
import { Text } from '@/ui/Text';
import { Icon } from '@/ui/Icon';
@@ -97,7 +96,7 @@ function StatusBadge({ status }: { status: RaceEntry['status'] }) {
};
return (
<Box
<Stack
as="span"
px={2}
py={0.5}
@@ -113,6 +112,6 @@ function StatusBadge({ status }: { status: RaceEntry['status'] }) {
display="inline-block"
>
{status.toUpperCase()}
</Box>
</Stack>
);
}

View File

@@ -2,7 +2,6 @@
import React from 'react';
import { CheckCircle, XCircle, AlertCircle } from 'lucide-react';
import { Box } from '@/ui/Box';
import { Stack } from '@/ui/Stack';
import { Text } from '@/ui/Text';
import { Badge } from '@/ui/Badge';
@@ -54,7 +53,7 @@ export function SponsorshipRequestCard({ request }: SponsorshipRequestCardProps)
>
<Stack direction="row" align="start" justify="between">
{/* eslint-disable-next-line gridpilot-rules/component-classification */}
<Box style={{ flex: 1, minWidth: 0 }}>
<Stack style={{ flex: 1, minWidth: 0 }}>
<Stack direction="row" align="center" gap={3} mb={2}>
<Icon icon={statusIcon} size={5} color={statusColor} />
<Text weight="semibold" color="text-white">{request.sponsorName}</Text>
@@ -70,7 +69,7 @@ export function SponsorshipRequestCard({ request }: SponsorshipRequestCardProps)
<Text size="xs" color="text-gray-400" block>
{new Date(request.requestedAt).toLocaleDateString()}
</Text>
</Box>
</Stack>
</Stack>
</Surface>
);

View File

@@ -2,7 +2,6 @@
import React from 'react';
import { DollarSign } from 'lucide-react';
import { Box } from '@/ui/Box';
import { Stack } from '@/ui/Stack';
import { Text } from '@/ui/Text';
import { Heading } from '@/ui/Heading';
@@ -58,10 +57,10 @@ export function SponsorshipSlotCard({ slot }: SponsorshipSlotCardProps) {
{!slot.isAvailable && slot.sponsoredBy && (
// eslint-disable-next-line gridpilot-rules/component-classification
<Box pt={3} style={{ borderTop: '1px solid #262626' }}>
<Stack pt={3} style={{ borderTop: '1px solid #262626' }}>
<Text size="xs" color="text-gray-400" block mb={1}>Sponsored by</Text>
<Text size="sm" weight="medium" color="text-white">{slot.sponsoredBy.name}</Text>
</Box>
</Stack>
)}
</Stack>
</Surface>

View File

@@ -5,11 +5,10 @@ import { Link } from '@/ui/Link';
import { Image } from '@/ui/Image';
import { CountryFlag } from '@/ui/CountryFlag';
import { PlaceholderImage } from '@/ui/PlaceholderImage';
import { Box } from '@/ui/Box';
import { Stack } from '@/ui/Stack';
import { Text } from '@/ui/Text';
import { Table, TableHead, TableBody, TableRow, TableHeader, TableCell } from '@/ui/Table';
import { Badge } from '@/ui/Badge';
import { Stack } from '@/ui/Stack';
import { routes } from '@/lib/routing/RouteConfig';
import { Icon } from '@/ui/Icon';
import { User, Edit } from 'lucide-react';
@@ -172,7 +171,7 @@ export function StandingsTable({
const hasMembership = !!membership;
return (
<Box
<Stack
ref={menuRef}
position="absolute"
right="0"
@@ -196,7 +195,7 @@ export function StandingsTable({
<>
{/* Role Management for existing members */}
{membership!.role !== 'admin' && membership!.role !== 'owner' && (
<Box
<Stack
as="button"
onClick={(e: React.MouseEvent) => { e.stopPropagation(); handleRoleChange(driverId, 'admin'); }}
display="flex"
@@ -214,10 +213,10 @@ export function StandingsTable({
>
<Text>🛡</Text>
<Text>Promote to Admin</Text>
</Box>
</Stack>
)}
{membership!.role === 'admin' && (
<Box
<Stack
as="button"
onClick={(e: React.MouseEvent) => { e.stopPropagation(); handleRoleChange(driverId, 'member'); }}
display="flex"
@@ -235,10 +234,10 @@ export function StandingsTable({
>
<Text></Text>
<Text>Demote to Member</Text>
</Box>
</Stack>
)}
{membership!.role === 'member' && (
<Box
<Stack
as="button"
onClick={(e: React.MouseEvent) => { e.stopPropagation(); handleRoleChange(driverId, 'steward'); }}
display="flex"
@@ -256,10 +255,10 @@ export function StandingsTable({
>
<Text>🏁</Text>
<Text>Make Steward</Text>
</Box>
</Stack>
)}
{membership!.role === 'steward' && (
<Box
<Stack
as="button"
onClick={(e: React.MouseEvent) => { e.stopPropagation(); handleRoleChange(driverId, 'member'); }}
display="flex"
@@ -277,10 +276,10 @@ export function StandingsTable({
>
<Text>🏁</Text>
<Text>Remove Steward</Text>
</Box>
</Stack>
)}
<Box borderTop borderColor="border-charcoal-outline" my={1} />
<Box
<Stack borderTop borderColor="border-charcoal-outline" my={1} />
<Stack
as="button"
onClick={(e: React.MouseEvent) => { e.stopPropagation(); handleRemove(driverId); }}
display="flex"
@@ -298,15 +297,15 @@ export function StandingsTable({
>
<Text>🚫</Text>
<Text>Remove from League</Text>
</Box>
</Stack>
</>
) : (
<>
{/* Options for drivers without membership (participating but not formal members) */}
<Box bg="bg-yellow-500/10" rounded px={2} py={1} mb={1}>
<Stack bg="bg-yellow-500/10" rounded px={2} py={1} mb={1}>
<Text size="xs" color="text-yellow-400/80">Driver not a formal member</Text>
</Box>
<Box
</Stack>
<Stack
as="button"
onClick={(e: React.MouseEvent) => { e.stopPropagation(); alert('Add as member - feature coming soon'); }}
display="flex"
@@ -324,8 +323,8 @@ export function StandingsTable({
>
<Text></Text>
<Text>Add as Member</Text>
</Box>
<Box
</Stack>
<Stack
as="button"
onClick={(e: React.MouseEvent) => { e.stopPropagation(); handleRemove(driverId); }}
display="flex"
@@ -343,17 +342,17 @@ export function StandingsTable({
>
<Text>🚫</Text>
<Text>Remove from Standings</Text>
</Box>
</Stack>
</>
)}
</Stack>
</Box>
</Stack>
);
};
const PointsActionMenu = () => {
return (
<Box
<Stack
ref={menuRef}
position="absolute"
right="0"
@@ -373,7 +372,7 @@ export function StandingsTable({
Score Actions
</Text>
<Stack gap={1}>
<Box
<Stack
as="button"
onClick={(e: React.MouseEvent) => { e.stopPropagation(); alert('View detailed stats - feature coming soon'); }}
display="flex"
@@ -391,8 +390,8 @@ export function StandingsTable({
>
<Text>📊</Text>
<Text>View Details</Text>
</Box>
<Box
</Stack>
<Stack
as="button"
onClick={(e: React.MouseEvent) => { e.stopPropagation(); alert('Manual adjustment - feature coming soon'); }}
display="flex"
@@ -410,8 +409,8 @@ export function StandingsTable({
>
<Text></Text>
<Text>Adjust Points</Text>
</Box>
<Box
</Stack>
<Stack
as="button"
onClick={(e: React.MouseEvent) => { e.stopPropagation(); alert('Race history - feature coming soon'); }}
display="flex"
@@ -429,22 +428,22 @@ export function StandingsTable({
>
<Text>📝</Text>
<Text>Race History</Text>
</Box>
</Stack>
</Stack>
</Box>
</Stack>
);
};
if (standings.length === 0) {
return (
<Box textAlign="center" py={8}>
<Stack textAlign="center" py={8}>
<Text color="text-gray-400">No standings available</Text>
</Box>
</Stack>
);
}
return (
<Box overflow="auto">
<Stack overflow="auto">
<Table>
<TableHead>
<TableRow>
@@ -489,7 +488,7 @@ export function StandingsTable({
>
{/* Position */}
<TableCell textAlign="center" w="14">
<Box
<Stack
display="inline-flex"
alignItems="center"
justifyContent="center"
@@ -510,15 +509,15 @@ export function StandingsTable({
}
>
{row.position}
</Box>
</Stack>
</TableCell>
{/* Driver with Rating and Nationality */}
<TableCell position="relative">
<Box display="flex" alignItems="center" gap={3}>
<Stack display="flex" alignItems="center" gap={3}>
{/* Avatar */}
<Box position="relative">
<Box
<Stack position="relative">
<Stack
w="10"
h="10"
rounded="full"
@@ -544,18 +543,18 @@ export function StandingsTable({
<PlaceholderImage size={40} />
)
)}
</Box>
</Stack>
{/* Nationality flag */}
{driver && driver.country && (
<Box position="absolute" bottom="-1" right="-1">
<Stack position="absolute" bottom="-1" right="-1">
<CountryFlag countryCode={driver.country} size="sm" />
</Box>
</Stack>
)}
</Box>
</Stack>
{/* Name and Rating */}
<Box flexGrow={1} minWidth="0">
<Box display="flex" alignItems="center" gap={2}>
<Stack flexGrow={1} minWidth="0">
<Stack display="flex" alignItems="center" gap={2}>
<Link
href={routes.driver.detail(row.driverId)}
weight="medium"
@@ -570,7 +569,7 @@ export function StandingsTable({
<Badge variant="primary">You</Badge>
)}
{roleDisplay && roleDisplay.text !== 'Member' && (
<Box
<Stack
as="span"
px={2}
py={0.5}
@@ -583,14 +582,14 @@ export function StandingsTable({
borderColor={roleDisplay.borderColor}
>
{roleDisplay.text}
</Box>
</Stack>
)}
</Box>
</Box>
</Stack>
</Stack>
{/* Hover Actions for Member Management */}
{isAdmin && canModify && (
<Box
<Stack
display="flex"
alignItems="center"
gap={1}
@@ -598,7 +597,7 @@ export function StandingsTable({
transition
visibility={isRowHovered || isMemberMenuOpen ? 'visible' : 'hidden'}
>
<Box
<Stack
as="button"
onClick={(e: React.MouseEvent) => { e.stopPropagation(); setActiveMenu(isMemberMenuOpen ? null : { driverId: row.driverId, type: 'member' }); }}
p={1.5}
@@ -611,10 +610,10 @@ export function StandingsTable({
title="Manage member"
>
<Icon icon={User} size={4} />
</Box>
</Box>
</Stack>
</Stack>
)}
</Box>
</Stack>
{isMemberMenuOpen && <MemberActionMenu driverId={row.driverId} />}
</TableCell>
@@ -625,10 +624,10 @@ export function StandingsTable({
{/* Total Points with Hover Action */}
<TableCell textAlign="right" position="relative">
<Box display="flex" alignItems="center" justifyContent="end" gap={2}>
<Stack display="flex" alignItems="center" justifyContent="end" gap={2}>
<Text color="text-white" weight="bold" size="lg">{row.totalPoints}</Text>
{isAdmin && canModify && (
<Box
<Stack
as="button"
onClick={(e: React.MouseEvent) => { e.stopPropagation(); setActiveMenu(isPointsMenuOpen ? null : { driverId: row.driverId, type: 'points' }); }}
p={1}
@@ -643,9 +642,9 @@ export function StandingsTable({
title="Score actions"
>
<Icon icon={Edit} size={3} />
</Box>
</Stack>
)}
</Box>
</Stack>
{isPointsMenuOpen && <PointsActionMenu />}
</TableCell>
@@ -680,6 +679,6 @@ export function StandingsTable({
})}
</TableBody>
</Table>
</Box>
</Stack>
);
}

View File

@@ -1,7 +1,6 @@
'use client';
import React from 'react';
import { Box } from '@/ui/Box';
import { Stack } from '@/ui/Stack';
import { Text } from '@/ui/Text';
import { Surface } from '@/ui/Surface';
@@ -26,7 +25,7 @@ interface StandingsTableShellProps {
export function StandingsTableShell({ standings, title = 'Championship Standings' }: StandingsTableShellProps) {
return (
<Surface variant="dark" border rounded="lg" overflow="hidden">
<Box px={6} py={4} borderBottom borderColor="border-charcoal-outline" bg="bg-iron-gray/20">
<Stack px={6} py={4} borderBottom borderColor="border-charcoal-outline" bg="bg-iron-gray/20">
<Stack direction="row" align="center" justify="between">
<Stack direction="row" align="center" gap={2}>
<Icon icon={Trophy} size={4} color="text-warning-amber" />
@@ -34,11 +33,11 @@ export function StandingsTableShell({ standings, title = 'Championship Standings
{title.toUpperCase()}
</Text>
</Stack>
<Box px={2} py={0.5} rounded="md" bg="bg-charcoal-outline/50">
<Stack px={2} py={0.5} rounded="md" bg="bg-charcoal-outline/50">
<Text size="xs" color="text-gray-400" weight="medium">{standings.length} Drivers</Text>
</Box>
</Stack>
</Stack>
</Box>
</Stack>
<Table>
<TableHead>
@@ -100,7 +99,7 @@ function PositionBadge({ position }: { position: number }) {
};
return (
<Box
<Stack
center
w={8}
h={8}
@@ -113,6 +112,6 @@ function PositionBadge({ position }: { position: number }) {
<Text size="sm" weight="bold">
{position}
</Text>
</Box>
</Stack>
);
}

View File

@@ -1,13 +1,12 @@
'use client';
import React from 'react';
import { Box } from '@/ui/Box';
import { Stack } from '@/ui/Stack';
import { Text } from '@/ui/Text';
import { Surface } from '@/ui/Surface';
import { Icon } from '@/ui/Icon';
import { Clock, ShieldAlert, MessageSquare } from 'lucide-react';
import { Button } from '@/ui/Button';
import { Stack } from '@/ui/Stack';
import { Card } from '@/ui/Card';
interface Protest {
id: string;
@@ -26,34 +25,34 @@ interface StewardingQueuePanelProps {
export function StewardingQueuePanel({ protests, onReview }: StewardingQueuePanelProps) {
return (
<Surface variant="dark" border rounded="lg" overflow="hidden">
<Box px={6} py={4} borderBottom borderColor="border-charcoal-outline" bg="bg-iron-gray/20">
<Card variant="outline" p={0} rounded="lg" overflow="hidden" className="bg-graphite-black">
<Stack px={6} py={4} borderBottom borderColor="border-charcoal-outline" bg="bg-iron-gray/20">
<Stack direction="row" align="center" justify="between">
<Stack direction="row" align="center" gap={2}>
<Icon icon={ShieldAlert} size={4} color="text-error-red" />
<Text weight="bold" letterSpacing="wider" size="sm" display="block">
<Text weight="bold" letterSpacing="wider" size="sm" block>
STEWARDING QUEUE
</Text>
</Stack>
<Box px={2} py={0.5} rounded="md" bg="bg-error-red/10" border borderColor="border-error-red/20">
<Stack px={2} py={0.5} rounded="md" bg="bg-error-red/10" border borderColor="border-error-red/20">
<Text size="xs" color="text-error-red" weight="bold">
{protests.filter(p => p.status === 'pending').length} Pending
</Text>
</Box>
</Stack>
</Stack>
</Box>
</Stack>
<Stack gap={0}>
{protests.length === 0 ? (
<Box py={12} center>
<Stack py={12} align="center" justify="center">
<Stack align="center" gap={3}>
<Icon icon={ShieldAlert} size={8} color="text-gray-700" />
<Text color="text-gray-500">No active protests in the queue.</Text>
</Stack>
</Box>
</Stack>
) : (
protests.map((protest) => (
<Box key={protest.id} p={6} borderBottom borderColor="border-charcoal-outline" hoverBg="bg-white/5" transition>
<Stack key={protest.id} p={6} borderBottom borderColor="border-charcoal-outline" className="transition-colors hover:bg-white/5">
<Stack direction={{ base: 'col', md: 'row' }} justify="between" align="start" gap={4}>
<Stack gap={3} flexGrow={1}>
<Stack direction="row" align="center" gap={2}>
@@ -61,7 +60,7 @@ export function StewardingQueuePanel({ protests, onReview }: StewardingQueuePane
<Text size="xs" color="text-gray-500" weight="bold" letterSpacing="widest">
{protest.raceName.toUpperCase()}
</Text>
<Box w={1} h={1} rounded="full" bg="bg-gray-700" />
<Stack w="1" h="1" rounded="full" bg="bg-gray-700">{null}</Stack>
<Stack direction="row" align="center" gap={1.5}>
<Icon icon={Clock} size={3} color="text-gray-600" />
<Text size="xs" color="text-gray-500">
@@ -70,7 +69,7 @@ export function StewardingQueuePanel({ protests, onReview }: StewardingQueuePane
</Stack>
</Stack>
<Box>
<Stack>
<Stack direction="row" align="center" gap={2} wrap>
<Text weight="bold" color="text-white">{protest.protestingDriver}</Text>
<Text size="xs" color="text-gray-600" weight="bold">VS</Text>
@@ -79,7 +78,7 @@ export function StewardingQueuePanel({ protests, onReview }: StewardingQueuePane
<Text size="sm" color="text-gray-400" mt={2} lineClamp={2}>
&ldquo;{protest.description}&rdquo;
</Text>
</Box>
</Stack>
</Stack>
<Stack direction="row" align="center" gap={3} w={{ base: 'full', md: 'auto' }}>
@@ -95,11 +94,11 @@ export function StewardingQueuePanel({ protests, onReview }: StewardingQueuePane
</Button>
</Stack>
</Stack>
</Box>
</Stack>
))
)}
</Stack>
</Surface>
</Card>
);
}
@@ -112,6 +111,6 @@ function StatusIndicator({ status }: { status: Protest['status'] }) {
};
return (
<Box w={2} h={2} rounded="full" bg={colors[status]} animate={status === 'under_review' ? 'pulse' : 'none'} />
<Stack w="2" h="2" rounded="full" bg={colors[status]} {...({ animate: status === 'under_review' ? 'pulse' : 'none' } as any)}>{null}</Stack>
);
}

View File

@@ -1,7 +1,7 @@
import React from 'react';
import { CheckCircle, Clock, Gavel } from 'lucide-react';
import { Box } from '@/ui/Box';
import { StatBox } from '@/ui/StatBox';
import { Grid } from '@/ui/Grid';
interface StewardingStatsProps {
totalPending: number;
@@ -11,7 +11,7 @@ interface StewardingStatsProps {
export function StewardingStats({ totalPending, totalResolved, totalPenalties }: StewardingStatsProps) {
return (
<Box display="grid" responsiveGridCols={{ base: 1, sm: 3 }} gap={4} mb={6}>
<Grid cols={1} mdCols={3} gap={4} mb={6}>
<StatBox
icon={Clock}
label="Pending Review"
@@ -30,6 +30,6 @@ export function StewardingStats({ totalPending, totalResolved, totalPenalties }:
value={totalPenalties}
color="var(--racing-red)"
/>
</Box>
</Grid>
);
}

View File

@@ -2,7 +2,6 @@
import React from 'react';
import { ArrowUpRight, ArrowDownRight, DollarSign, TrendingUp, LucideIcon } from 'lucide-react';
import { Box } from '@/ui/Box';
import { Stack } from '@/ui/Stack';
import { Text } from '@/ui/Text';
import { Icon } from '@/ui/Icon';
@@ -46,10 +45,10 @@ export function TransactionRow({ transaction }: TransactionRowProps) {
>
<Stack direction="row" align="center" justify="between">
<Stack direction="row" align="center" gap={3}>
<Box flexShrink={0}>
<Stack flexShrink={0}>
<Icon icon={getTransactionIcon(transaction.type)} size={4} color={transaction.typeColor} />
</Box>
<Box minWidth="0" flexGrow={1}>
</Stack>
<Stack minWidth="0" flexGrow={1}>
<Text size="sm" weight="medium" color="text-white" block truncate>
{transaction.description}
</Text>
@@ -64,14 +63,14 @@ export function TransactionRow({ transaction }: TransactionRowProps) {
{transaction.status}
</Text>
</Stack>
</Box>
</Stack>
</Stack>
<Box textAlign="right">
<Stack textAlign="right">
<Text size="lg" weight="semibold" color={transaction.amountColor}>
{transaction.formattedAmount}
</Text>
</Box>
</Stack>
</Stack>
</Surface>
);

View File

@@ -1,7 +1,6 @@
'use client';
import React from 'react';
import { Box } from '@/ui/Box';
import { Stack } from '@/ui/Stack';
import { Text } from '@/ui/Text';
import { Surface } from '@/ui/Surface';
@@ -30,7 +29,7 @@ export function WalletSummaryPanel({ balance, currency, transactions, onDeposit,
<Stack gap={6}>
<Surface variant="dark" border rounded="lg" padding={8} position="relative" overflow="hidden">
{/* Background Pattern */}
<Box
<Stack
position="absolute"
top="-5rem"
right="-5rem"
@@ -74,26 +73,26 @@ export function WalletSummaryPanel({ balance, currency, transactions, onDeposit,
</Surface>
<Surface variant="dark" border rounded="lg" overflow="hidden">
<Box px={6} py={4} borderBottom borderColor="border-charcoal-outline" bg="bg-iron-gray/20">
<Stack px={6} py={4} borderBottom borderColor="border-charcoal-outline" bg="bg-iron-gray/20">
<Stack direction="row" align="center" gap={2}>
<Icon icon={History} size={4} color="text-primary-blue" />
<Text weight="bold" letterSpacing="wider" size="sm" display="block">
RECENT TRANSACTIONS
</Text>
</Stack>
</Box>
</Stack>
<Stack gap={0}>
{transactions.length === 0 ? (
<Box py={12} center>
<Stack py={12} center>
<Text color="text-gray-500">No recent transactions.</Text>
</Box>
</Stack>
) : (
transactions.map((tx) => (
<Box key={tx.id} p={4} borderBottom borderColor="border-charcoal-outline" hoverBg="bg-white/5" transition>
<Stack key={tx.id} p={4} borderBottom borderColor="border-charcoal-outline" hoverBg="bg-white/5" transition>
<Stack direction="row" justify="between" align="center">
<Stack direction="row" align="center" gap={4}>
<Box
<Stack
center
w={10}
h={10}
@@ -105,7 +104,7 @@ export function WalletSummaryPanel({ balance, currency, transactions, onDeposit,
size={4}
color={tx.type === 'credit' ? 'text-performance-green' : 'text-error-red'}
/>
</Box>
</Stack>
<Stack gap={0.5}>
<Text weight="medium" color="text-white">{tx.description}</Text>
<Text size="xs" color="text-gray-500">{new Date(tx.date).toLocaleDateString()}</Text>
@@ -118,7 +117,7 @@ export function WalletSummaryPanel({ balance, currency, transactions, onDeposit,
{tx.type === 'credit' ? '+' : '-'}{tx.amount.toFixed(2)}
</Text>
</Stack>
</Box>
</Stack>
))
)}
</Stack>