website refactor

This commit is contained in:
2026-01-19 01:24:07 +01:00
parent e1ce3bffd1
commit edc4cd7f21
64 changed files with 1113 additions and 753 deletions

View File

@@ -53,9 +53,9 @@
"lib/builders/view-data/*.tsx"
],
"rules": {
"gridpilot-rules/view-data-builder-contract": "off",
"gridpilot-rules/filename-matches-export": "off",
"gridpilot-rules/single-export-per-file": "off",
"gridpilot-rules/filename-matches-export": "off"
"gridpilot-rules/view-data-builder-contract": "off"
}
},
{
@@ -72,11 +72,11 @@
"lib/mutations/**/*.ts"
],
"rules": {
"gridpilot-rules/mutation-contract": "off",
"gridpilot-rules/mutation-must-use-builders": "off",
"gridpilot-rules/mutation-must-map-errors": "off",
"gridpilot-rules/clean-error-handling": "off",
"gridpilot-rules/filename-service-match": "off",
"gridpilot-rules/clean-error-handling": "off"
"gridpilot-rules/mutation-contract": "off",
"gridpilot-rules/mutation-must-map-errors": "off",
"gridpilot-rules/mutation-must-use-builders": "off"
}
},
{
@@ -84,16 +84,16 @@
"templates/**/*.tsx"
],
"rules": {
"gridpilot-rules/template-no-direct-mutations": "off",
"gridpilot-rules/template-no-side-effects": "off",
"gridpilot-rules/component-no-data-manipulation": "off",
"gridpilot-rules/no-hardcoded-routes": "off",
"gridpilot-rules/no-raw-html-in-app": "off",
"gridpilot-rules/template-no-async-render": "off",
"gridpilot-rules/template-no-direct-mutations": "off",
"gridpilot-rules/template-no-external-state": "off",
"gridpilot-rules/template-no-global-objects": "off",
"gridpilot-rules/template-no-mutation-props": "off",
"gridpilot-rules/template-no-unsafe-html": "off",
"gridpilot-rules/component-no-data-manipulation": "off",
"gridpilot-rules/no-hardcoded-routes": "off",
"gridpilot-rules/no-raw-html-in-app": "off"
"gridpilot-rules/template-no-side-effects": "off",
"gridpilot-rules/template-no-unsafe-html": "off"
}
},
{
@@ -111,33 +111,33 @@
"app/**/layout.tsx"
],
"rules": {
"@typescript-eslint/no-explicit-any": "off",
"@typescript-eslint/no-unused-vars": "off",
"gridpilot-rules/component-classification": "off",
"gridpilot-rules/no-console": "off",
"gridpilot-rules/no-direct-process-env": "off",
"gridpilot-rules/no-hardcoded-routes": "off",
"gridpilot-rules/no-hardcoded-search-params": "off",
"gridpilot-rules/no-index-files": "off",
"gridpilot-rules/no-next-cookies-in-pages": "off",
"gridpilot-rules/no-raw-html-in-app": "off",
"gridpilot-rules/rsc-no-container-manager": "off",
"gridpilot-rules/rsc-no-page-data-fetcher": "off",
"gridpilot-rules/rsc-no-view-models": "off",
"gridpilot-rules/rsc-no-presenters": "off",
"gridpilot-rules/rsc-no-intl": "off",
"gridpilot-rules/rsc-no-sorting-filtering": "off",
"gridpilot-rules/rsc-no-display-objects": "off",
"gridpilot-rules/rsc-no-unsafe-services": "off",
"gridpilot-rules/rsc-no-container-manager-calls": "off",
"gridpilot-rules/rsc-no-di": "off",
"gridpilot-rules/rsc-no-display-objects": "off",
"gridpilot-rules/rsc-no-intl": "off",
"gridpilot-rules/rsc-no-local-helpers": "off",
"gridpilot-rules/rsc-no-object-construction": "off",
"gridpilot-rules/rsc-no-container-manager-calls": "off",
"gridpilot-rules/no-hardcoded-search-params": "off",
"gridpilot-rules/no-next-cookies-in-pages": "off",
"gridpilot-rules/no-hardcoded-routes": "off",
"gridpilot-rules/component-classification": "off",
"gridpilot-rules/no-raw-html-in-app": "off",
"gridpilot-rules/no-console": "off",
"gridpilot-rules/rsc-no-page-data-fetcher": "off",
"gridpilot-rules/rsc-no-presenters": "off",
"gridpilot-rules/rsc-no-sorting-filtering": "off",
"gridpilot-rules/rsc-no-unsafe-services": "off",
"gridpilot-rules/rsc-no-view-models": "off",
"import/no-default-export": "off",
"no-restricted-syntax": "off",
"react-hooks/exhaustive-deps": "off",
"react-hooks/rules-of-hooks": "off",
"react/no-unescaped-entities": "off",
"gridpilot-rules/no-index-files": "off",
"gridpilot-rules/no-direct-process-env": "off",
"@typescript-eslint/no-explicit-any": "off",
"@typescript-eslint/no-unused-vars": "off"
"react/no-unescaped-entities": "off"
}
},
{
@@ -149,8 +149,8 @@
"lib/mutations/auth/types/*.ts"
],
"rules": {
"gridpilot-rules/no-direct-process-env": "off",
"gridpilot-rules/clean-error-handling": "off"
"gridpilot-rules/clean-error-handling": "off",
"gridpilot-rules/no-direct-process-env": "off"
}
},
{
@@ -159,10 +159,10 @@
"lib/display-objects/**/*.tsx"
],
"rules": {
"gridpilot-rules/display-no-domain-models": "off",
"gridpilot-rules/display-no-business-logic": "off",
"gridpilot-rules/model-no-domain-in-display": "off",
"gridpilot-rules/filename-display-match": "off"
"gridpilot-rules/display-no-domain-models": "off",
"gridpilot-rules/filename-display-match": "off",
"gridpilot-rules/model-no-domain-in-display": "off"
}
},
{
@@ -170,17 +170,17 @@
"lib/page-queries/**/*.ts"
],
"rules": {
"gridpilot-rules/page-query-no-null-returns": "off",
"gridpilot-rules/page-query-filename": "off",
"gridpilot-rules/clean-error-handling": "off",
"gridpilot-rules/filename-matches-export": "off",
"gridpilot-rules/no-hardcoded-routes": "off",
"gridpilot-rules/no-hardcoded-search-params": "off",
"gridpilot-rules/page-query-contract": "off",
"gridpilot-rules/page-query-execute": "off",
"gridpilot-rules/page-query-return-type": "off",
"gridpilot-rules/page-query-filename": "off",
"gridpilot-rules/page-query-must-use-builders": "off",
"gridpilot-rules/single-export-per-file": "off",
"gridpilot-rules/filename-matches-export": "off",
"gridpilot-rules/clean-error-handling": "off",
"gridpilot-rules/no-hardcoded-routes": "off",
"gridpilot-rules/no-hardcoded-search-params": "off"
"gridpilot-rules/page-query-no-null-returns": "off",
"gridpilot-rules/page-query-return-type": "off",
"gridpilot-rules/single-export-per-file": "off"
}
},
{
@@ -197,11 +197,11 @@
"lib/services/**/*.ts"
],
"rules": {
"gridpilot-rules/services-no-external-api": "off",
"gridpilot-rules/services-must-be-pure": "off",
"gridpilot-rules/filename-service-match": "off",
"gridpilot-rules/services-implement-contract": "off",
"gridpilot-rules/services-must-be-pure": "off",
"gridpilot-rules/services-must-return-result": "off",
"gridpilot-rules/services-implement-contract": "off"
"gridpilot-rules/services-no-external-api": "off"
}
},
{
@@ -210,12 +210,12 @@
"app/**/*.ts"
],
"rules": {
"gridpilot-rules/client-only-no-server-code": "off",
"gridpilot-rules/client-only-must-have-directive": "off",
"gridpilot-rules/server-actions-must-use-mutations": "off",
"gridpilot-rules/server-actions-return-result": "off",
"gridpilot-rules/client-only-no-server-code": "off",
"gridpilot-rules/no-use-mutation-in-client": "off",
"gridpilot-rules/server-actions-interface": "off",
"gridpilot-rules/no-use-mutation-in-client": "off"
"gridpilot-rules/server-actions-must-use-mutations": "off",
"gridpilot-rules/server-actions-return-result": "off"
}
},
{
@@ -223,8 +223,8 @@
"lib/write-boundaries/**/*.ts"
],
"rules": {
"gridpilot-rules/write-boundary-no-direct-mutations": "error",
"gridpilot-rules/write-boundary-must-use-repository": "error"
"gridpilot-rules/write-boundary-must-use-repository": "error",
"gridpilot-rules/write-boundary-no-direct-mutations": "error"
}
},
{
@@ -263,10 +263,10 @@
"app/**/*.ts"
],
"rules": {
"gridpilot-rules/no-raw-html-in-app": "off",
"gridpilot-rules/no-nextjs-imports-in-ui": "off",
"gridpilot-rules/component-classification": "off",
"gridpilot-rules/no-hardcoded-routes": "off",
"gridpilot-rules/component-classification": "off"
"gridpilot-rules/no-nextjs-imports-in-ui": "off",
"gridpilot-rules/no-raw-html-in-app": "off"
}
},
{
@@ -276,8 +276,8 @@
],
"rules": {
"gridpilot-rules/no-hardcoded-routes": "error",
"gridpilot-rules/server-actions-return-result": "error",
"gridpilot-rules/server-actions-interface": "error"
"gridpilot-rules/server-actions-interface": "error",
"gridpilot-rules/server-actions-return-result": "error"
}
},
{
@@ -286,9 +286,9 @@
"ui/**/*.ts"
],
"rules": {
"gridpilot-rules/ui-element-purity": "error",
"gridpilot-rules/component-classification": "error",
"gridpilot-rules/no-nextjs-imports-in-ui": "error",
"gridpilot-rules/component-classification": "error"
"gridpilot-rules/ui-element-purity": "error"
}
},
{
@@ -297,11 +297,10 @@
"components/**/*.ts"
],
"rules": {
"gridpilot-rules/no-nextjs-imports-in-ui": "off",
"gridpilot-rules/component-classification": "off",
"gridpilot-rules/no-hardcoded-routes": "off",
"gridpilot-rules/no-nextjs-imports-in-ui": "off",
"gridpilot-rules/no-raw-html-in-app": "error",
"gridpilot-rules/no-generic-ui-primitives-in-components": "error",
"no-restricted-imports": "off"
}
},
@@ -310,8 +309,7 @@
"components/mockups/**/*.tsx"
],
"rules": {
"gridpilot-rules/no-raw-html-in-app": "off",
"gridpilot-rules/no-generic-ui-primitives-in-components": "off"
"gridpilot-rules/no-raw-html-in-app": "off"
}
},
{
@@ -319,11 +317,11 @@
"lib/services/**/*.ts"
],
"rules": {
"gridpilot-rules/no-hardcoded-routes": "off",
"gridpilot-rules/service-function-format": "off",
"gridpilot-rules/services-must-be-pure": "off",
"gridpilot-rules/services-no-external-api": "off",
"gridpilot-rules/services-implement-contract": "off",
"gridpilot-rules/no-hardcoded-routes": "off"
"gridpilot-rules/services-must-be-pure": "off",
"gridpilot-rules/services-no-external-api": "off"
}
},
{
@@ -350,8 +348,8 @@
"off",
{
"argsIgnorePattern": "^_",
"varsIgnorePattern": "^_",
"caughtErrorsIgnorePattern": "^_"
"caughtErrorsIgnorePattern": "^_",
"varsIgnorePattern": "^_"
}
],
"boundaries/element-types": [
@@ -370,6 +368,7 @@
]
}
],
"gridpilot-rules/no-index-files": "off",
"import/no-default-export": "off",
"import/no-named-as-default-member": "off",
"no-restricted-syntax": "off",
@@ -377,8 +376,7 @@
"react-hooks/rules-of-hooks": "off",
"react/no-unescaped-entities": "off",
"unused-imports/no-unused-imports": "off",
"unused-imports/no-unused-vars": "off",
"gridpilot-rules/no-index-files": "off"
"unused-imports/no-unused-vars": "off"
},
"settings": {
"boundaries/elements": [
@@ -393,4 +391,4 @@
"typescript": {}
}
}
}
}

View File

@@ -1,6 +1,7 @@
import { Card } from '@/ui/Card';
import { Text } from '@/ui/Text';
import { Group } from '@/ui/Group';
import { Stack } from '@/ui/Stack';
import { DateDisplay } from '@/lib/display-objects/DateDisplay';
interface AchievementCardProps {
@@ -31,13 +32,13 @@ export function AchievementCard({
>
<Group direction="row" align="start" gap={3}>
<Text size="3xl">{icon}</Text>
<Group direction="column" gap={1}>
<Stack gap={1}>
<Text weight="medium" variant="high">{title}</Text>
<Text size="xs" variant="med">{description}</Text>
<Text size="xs" variant="low">
{DateDisplay.formatShort(unlockedAt)}
</Text>
</Group>
</Stack>
</Group>
</Card>
);

View File

@@ -4,6 +4,9 @@ import { Grid } from '@/ui/Grid';
import { Heading } from '@/ui/Heading';
import { Icon } from '@/ui/Icon';
import { Group } from '@/ui/Group';
import { Stack } from '@/ui/Stack';
import { Box } from '@/ui/Box';
import { Surface } from '@/ui/Surface';
import { Text } from '@/ui/Text';
import { Award, Crown, Medal, Star, Target, Trophy, Zap } from 'lucide-react';
import React from 'react';
@@ -36,9 +39,9 @@ function getAchievementIcon(icon: string) {
export function AchievementGrid({ achievements }: AchievementGridProps) {
return (
<Card>
<Group direction="column" gap={4} fullWidth>
<Group direction="row" align="center" justify="between" fullWidth>
<Group direction="row" align="center" gap={2}>
<Stack gap={4} fullWidth>
<Group align="center" justify="between" fullWidth>
<Group align="center" gap={2}>
<Icon icon={Award} size={5} intent="warning" />
<Heading level={2}>
Achievements
@@ -46,38 +49,39 @@ export function AchievementGrid({ achievements }: AchievementGridProps) {
</Group>
<Text size="sm" variant="low" weight="normal">{achievements.length} earned</Text>
</Group>
</Group>
<Grid cols={1} gap={4}>
{achievements.map((achievement) => {
const AchievementIcon = getAchievementIcon(achievement.icon);
const rarity = AchievementDisplay.getRarityVariant(achievement.rarity);
return (
<Card
key={achievement.id}
variant={rarity.surface}
>
<Group direction="row" align="start" gap={3}>
<Card variant="default" style={{ padding: '0.75rem' }}>
<Icon icon={AchievementIcon} size={5} intent={rarity.iconIntent} />
</Card>
<Group direction="column" fullWidth>
<Text weight="semibold" size="sm" variant="high" block>{achievement.title}</Text>
<Text size="xs" variant="med" block>{achievement.description}</Text>
<Group direction="row" align="center" gap={2}>
<Text size="xs" variant={rarity.text} weight="medium" uppercase>
{achievement.rarity}
</Text>
<Text size="xs" variant="low"></Text>
<Text size="xs" variant="low">
{AchievementDisplay.formatDate(achievement.earnedAt)}
</Text>
</Group>
<Grid cols={1} gap={4}>
{achievements.map((achievement) => {
const AchievementIcon = getAchievementIcon(achievement.icon);
const rarity = AchievementDisplay.getRarityVariant(achievement.rarity);
return (
<Card
key={achievement.id}
variant={rarity.surface}
>
<Group align="start" gap={3}>
<Surface variant="default" padding={3} rounded="md">
<Icon icon={AchievementIcon} size={5} intent={rarity.iconIntent} />
</Surface>
<Stack flex={1} gap={1}>
<Text weight="semibold" size="sm" variant="high" block>{achievement.title}</Text>
<Text size="xs" variant="med" block>{achievement.description}</Text>
<Group align="center" gap={2}>
<Text size="xs" variant={rarity.text} weight="medium" uppercase>
{achievement.rarity}
</Text>
<Text size="xs" variant="low"></Text>
<Text size="xs" variant="low">
{AchievementDisplay.formatDate(achievement.earnedAt)}
</Text>
</Group>
</Stack>
</Group>
</Group>
</Card>
);
})}
</Grid>
</Card>
);
})}
</Grid>
</Stack>
</Card>
);
}

View File

@@ -1,6 +1,7 @@
'use client';
import { Card } from '@/ui/Card';
import { Box } from '@/ui/Box';
import React from 'react';
interface AdminDataTableProps {
@@ -20,14 +21,12 @@ export function AdminDataTable({
}: AdminDataTableProps) {
return (
<Card padding={0}>
<div
style={{
overflow: 'auto',
maxHeight: typeof maxHeight === 'number' ? `${maxHeight}px` : maxHeight
}}
<Box
overflow="auto"
maxHeight={typeof maxHeight === 'number' ? `${maxHeight}px` : maxHeight}
>
{children}
</div>
</Box>
</Card>
);
}

View File

@@ -6,6 +6,8 @@ import { Button } from '@/ui/Button';
import { IconButton } from '@/ui/IconButton';
import { SimpleCheckbox } from '@/ui/SimpleCheckbox';
import { Badge } from '@/ui/Badge';
import { Box } from '@/ui/Box';
import { Group } from '@/ui/Group';
import { DriverIdentity } from '@/ui/DriverIdentity';
import {
Table,
@@ -87,13 +89,13 @@ export function AdminUsersTable({
/>
</TableCell>
<TableCell>
<div style={{ display: 'flex', gap: '0.375rem', flexWrap: 'wrap' }}>
<Group gap={1.5} wrap>
{user.roles.map((role) => (
<Badge key={role} variant="default" size="sm">
{role}
</Badge>
))}
</div>
</Group>
</TableCell>
<TableCell>
<UserStatusTag status={user.status} />
@@ -104,7 +106,7 @@ export function AdminUsersTable({
</Text>
</TableCell>
<TableCell>
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'flex-end', gap: '0.5rem' }}>
<Group gap={2} justify="end">
{user.status === 'active' ? (
<Button
size="sm"
@@ -138,7 +140,7 @@ export function AdminUsersTable({
icon={MoreVertical}
title="More"
/>
</div>
</Group>
</TableCell>
</TableRow>
))}

View File

@@ -5,6 +5,8 @@ import { Icon } from '@/ui/Icon';
import { Input } from '@/ui/Input';
import { Select } from '@/ui/Select';
import { Text } from '@/ui/Text';
import { Box } from '@/ui/Box';
import { Group } from '@/ui/Group';
import { Filter, Search } from 'lucide-react';
import { AdminToolbar } from './AdminToolbar';
import React from 'react';
@@ -31,7 +33,7 @@ export function UserFilters({
return (
<AdminToolbar
leftContent={
<React.Fragment>
<Group gap={3}>
<Icon icon={Filter} size={4} intent="low" />
<Text weight="medium" variant="high">Filters</Text>
{(search || roleFilter || statusFilter) && (
@@ -43,16 +45,18 @@ export function UserFilters({
Clear all
</Button>
)}
</React.Fragment>
</Group>
}
>
<Input
type="text"
placeholder="Search by email or name..."
value={search}
onChange={(e) => onSearch(e.target.value)}
style={{ width: '300px' }}
/>
<Box width="18.75rem">
<Input
type="text"
placeholder="Search by email or name..."
value={search}
onChange={(e) => onSearch(e.target.value)}
fullWidth
/>
</Box>
<Select
value={roleFilter}

View File

@@ -1,18 +1,24 @@
import React from 'react';
import { Box } from '@/ui/Box';
interface AppShellProps {
children: React.ReactNode;
className?: string;
}
/**
* AppShell is the root container for the entire application layout.
* It provides the base background and layout structure.
*/
export function AppShell({ children, className = '' }: AppShellProps) {
export function AppShell({ children }: AppShellProps) {
return (
<div className={`min-h-screen bg-[#0C0D0F] text-gray-100 flex flex-col ${className}`}>
<Box
minHeight="100vh"
bg="#0C0D0F"
color="var(--ui-color-text-high)"
display="flex"
flexDirection="col"
>
{children}
</div>
</Box>
);
}

View File

@@ -1,7 +1,7 @@
'use client';
import { Card } from '@/ui/Card';
import { Text } from '@/ui/Text';
import { Box } from '@/ui/Box';
import { SectionHeader } from '@/ui/SectionHeader';
import React from 'react';
@@ -24,9 +24,9 @@ export function AuthCard({ children, title, description }: AuthCardProps) {
description={description}
variant="minimal"
/>
<div>
<Box>
{children}
</div>
</Box>
</Card>
);
}

View File

@@ -1,6 +1,7 @@
'use client';
import { Grid } from '@/ui/Grid';
import { Box } from '@/ui/Box';
import React from 'react';
interface AuthProviderButtonsProps {
@@ -14,10 +15,10 @@ interface AuthProviderButtonsProps {
*/
export function AuthProviderButtons({ children }: AuthProviderButtonsProps) {
return (
<div style={{ marginBottom: '1.5rem' }}>
<Box marginBottom={6}>
<Grid cols={1} gap={3}>
{children}
</Grid>
</div>
</Box>
);
}

View File

@@ -1,6 +1,10 @@
import { Heading } from '@/ui/Heading';
import { Icon } from '@/ui/Icon';
import { Text } from '@/ui/Text';
import { Box } from '@/ui/Box';
import { Group } from '@/ui/Group';
import { Stack } from '@/ui/Stack';
import { Surface } from '@/ui/Surface';
import { ListItem, ListItemInfo } from '@/ui/ListItem';
import { motion } from 'framer-motion';
import { Car, Trophy, Users } from 'lucide-react';
@@ -34,26 +38,34 @@ interface UserRolesPreviewProps {
export function UserRolesPreview({ variant = 'full' }: UserRolesPreviewProps) {
if (variant === 'compact') {
return (
<div style={{ marginTop: '2rem' }}>
<Box marginTop={8}>
<Text align="center" size="xs" variant="low" block marginBottom={4}>
One account for all roles
</Text>
<div style={{ display: 'flex', justifyContent: 'center', gap: '1.5rem' }}>
<Group justify="center" gap={6}>
{USER_ROLES.map((role) => (
<div key={role.title} style={{ display: 'flex', flexDirection: 'column', alignItems: 'center', gap: '0.25rem' }}>
<div style={{ width: '2rem', height: '2rem', borderRadius: '0.5rem', backgroundColor: 'var(--ui-color-bg-surface-muted)', display: 'flex', alignItems: 'center', justifyContent: 'center' }}>
<Stack key={role.title} align="center" gap={1}>
<Surface
width="2rem"
height="2rem"
rounded="md"
variant="muted"
display="flex"
alignItems="center"
justifyContent="center"
>
<Icon icon={role.icon} size={4} intent={role.intent} />
</div>
</Surface>
<Text size="xs" variant="low">{role.title}</Text>
</div>
</Stack>
))}
</div>
</div>
</Group>
</Box>
);
}
return (
<div style={{ display: 'flex', flexDirection: 'column', gap: '0.75rem', marginBottom: '2rem' }}>
<Stack gap={3} marginBottom={8}>
{USER_ROLES.map((role, index) => (
<motion.div
key={role.title}
@@ -62,18 +74,26 @@ export function UserRolesPreview({ variant = 'full' }: UserRolesPreviewProps) {
transition={{ delay: index * 0.1 }}
>
<ListItem>
<div style={{ display: 'flex', alignItems: 'center', gap: '1rem' }}>
<div style={{ width: '2.5rem', height: '2.5rem', borderRadius: '0.5rem', backgroundColor: 'var(--ui-color-bg-surface-muted)', display: 'flex', alignItems: 'center', justifyContent: 'center' }}>
<Group gap={4}>
<Surface
width="2.5rem"
height="2.5rem"
rounded="md"
variant="muted"
display="flex"
alignItems="center"
justifyContent="center"
>
<Icon icon={role.icon} size={5} intent={role.intent} />
</div>
</Surface>
<ListItemInfo
title={role.title}
description={role.description}
/>
</div>
</Group>
</ListItem>
</motion.div>
))}
</div>
</Stack>
);
}

View File

@@ -6,6 +6,9 @@ import { Button } from '@/ui/Button';
import { Card } from '@/ui/Card';
import { Pagination } from '@/ui/Pagination';
import { Text } from '@/ui/Text';
import { Box } from '@/ui/Box';
import { Group } from '@/ui/Group';
import { Stack } from '@/ui/Stack';
import { ControlBar } from '@/ui/ControlBar';
import { Trophy } from 'lucide-react';
import { useEffect, useState } from 'react';
@@ -55,9 +58,9 @@ export function ProfileRaceHistory({ driverId }: RaceHistoryProps) {
}
return (
<div style={{ display: 'flex', flexDirection: 'column', gap: '1rem' }}>
<Stack gap={4}>
<ControlBar>
<div style={{ display: 'flex', alignItems: 'center', gap: '0.5rem' }}>
<Group gap={2}>
<Button
variant={filter === 'all' ? 'primary' : 'secondary'}
onClick={() => { setFilter('all'); setPage(1); }}
@@ -79,13 +82,13 @@ export function ProfileRaceHistory({ driverId }: RaceHistoryProps) {
>
Podiums
</Button>
</div>
</Group>
</ControlBar>
<Card>
<div style={{ minHeight: '10rem', display: 'flex', alignItems: 'center', justifyContent: 'center' }}>
<Box minHeight="10rem" display="flex" alignItems="center" justifyContent="center">
<Text variant="low">No results found for the selected filter.</Text>
</div>
</Box>
<Pagination
currentPage={page}
@@ -93,6 +96,6 @@ export function ProfileRaceHistory({ driverId }: RaceHistoryProps) {
onPageChange={setPage}
/>
</Card>
</div>
</Stack>
);
}

View File

@@ -2,6 +2,7 @@ import { Button } from '@/ui/Button';
import { Icon } from '@/ui/Icon';
import { Stack } from '@/ui/Stack';
import { Text } from '@/ui/Text';
import { Group } from '@/ui/Group';
import { LucideIcon } from 'lucide-react';
interface SkillLevelButtonProps {
@@ -28,13 +29,20 @@ export function SkillLevelButton({
variant="ghost"
onClick={onClick}
fullWidth
className={`${bgColor} border ${borderColor} flex items-center justify-between p-3 rounded-lg h-auto`}
style={{
padding: '0.75rem',
height: 'auto',
display: 'flex',
alignItems: 'center',
justifyContent: 'space-between',
borderRadius: 'var(--ui-radius-lg)'
}}
>
<Stack direction="row" align="center" gap={2}>
<Group gap={2}>
<Icon icon={icon} size={4} className={color} />
<Text weight="medium" color="text-white">{label}</Text>
</Stack>
<Text size="sm" color="text-gray-400">{count} teams</Text>
<Text weight="medium" variant="high">{label}</Text>
</Group>
<Text size="sm" variant="low">{count} teams</Text>
</Button>
);
}

View File

@@ -3,6 +3,8 @@
import { Heading } from '@/ui/Heading';
import { Icon } from '@/ui/Icon';
import { Text } from '@/ui/Text';
import { Stack } from '@/ui/Stack';
import { Surface } from '@/ui/Surface';
import { AlertTriangle } from 'lucide-react';
import React from 'react';
@@ -20,30 +22,31 @@ interface AppErrorBoundaryViewProps {
*/
export function AppErrorBoundaryView({ title, description, children }: AppErrorBoundaryViewProps) {
return (
<div style={{ display: 'flex', flexDirection: 'column', alignItems: 'center', gap: '1.5rem', width: '100%' }}>
<Stack alignItems="center" gap={6} fullWidth>
{/* Header Icon */}
<div
style={{
padding: '1rem',
borderRadius: '9999px',
backgroundColor: 'rgba(255, 190, 77, 0.1)',
border: '1px solid rgba(255, 190, 77, 0.3)'
}}
<Surface
padding={4}
rounded="full"
bg="rgba(255, 190, 77, 0.1)"
border="1px solid rgba(255, 190, 77, 0.3)"
display="flex"
alignItems="center"
justifyContent="center"
>
<Icon icon={AlertTriangle} size={8} intent="warning" />
</div>
</Surface>
{/* Typography */}
<div style={{ display: 'flex', flexDirection: 'column', alignItems: 'center', gap: '0.5rem' }}>
<Stack alignItems="center" gap={2}>
<Heading level={1} weight="bold">
{title}
</Heading>
<Text variant="low" align="center" style={{ maxWidth: '32rem' }} leading="relaxed">
{description}
</Text>
</div>
</Stack>
{children}
</div>
</Stack>
);
}

View File

@@ -194,10 +194,10 @@ export function ErrorAnalyticsDashboard({
{/* Header */}
<Stack display="flex" alignItems="center" justifyContent="between" px={4} py={3} bg="bg-iron-gray/50" borderBottom borderColor="border-charcoal-outline">
<Stack display="flex" alignItems="center" gap={2}>
<Icon icon={Activity} size={4} color="rgb(239, 68, 68)" />
<Text size="sm" weight="semibold" color="text-white">Error Analytics</Text>
<Icon icon={Activity} size={4} intent="critical" />
<Text size="sm" weight="semibold" variant="high">Error Analytics</Text>
{isDev && (
<Badge variant="danger" size="xs">
<Badge variant="critical" size="sm">
DEV
</Badge>
)}

View File

@@ -4,6 +4,8 @@ import { Button } from '@/ui/Button';
import { Card } from '@/ui/Card';
import { Icon } from '@/ui/Icon';
import { Text } from '@/ui/Text';
import { Box } from '@/ui/Box';
import { Stack } from '@/ui/Stack';
import { Accordion } from '@/ui/Accordion';
import { Copy } from 'lucide-react';
import React, { useState } from 'react';
@@ -40,9 +42,9 @@ export function ErrorDetails({ error }: ErrorDetailsProps) {
};
return (
<div style={{ width: '100%', marginTop: '1.5rem', paddingTop: '1.5rem', borderTop: '1px solid var(--ui-color-border-muted)' }}>
<Box width="100%" marginTop={6} paddingTop={6} borderTop="1px solid var(--ui-color-border-muted)">
<Accordion title="Technical Logs">
<div style={{ display: 'flex', flexDirection: 'column', gap: '1rem' }}>
<Stack gap={4}>
<Card variant="outline">
<Text font="mono" size="xs" variant="low" block leading="relaxed" style={{ maxHeight: '12rem', overflow: 'auto' }}>
{error.stack || 'No stack trace available'}
@@ -50,7 +52,7 @@ export function ErrorDetails({ error }: ErrorDetailsProps) {
</Text>
</Card>
<div style={{ display: 'flex', justifyContent: 'flex-end' }}>
<Box display="flex" justifyContent="end">
<Button
variant="secondary"
size="sm"
@@ -59,9 +61,9 @@ export function ErrorDetails({ error }: ErrorDetailsProps) {
>
{copied ? 'Copied!' : 'Copy Details'}
</Button>
</div>
</div>
</Box>
</Stack>
</Accordion>
</div>
</Box>
);
}

View File

@@ -4,6 +4,8 @@ import { Button } from '@/ui/Button';
import { Card } from '@/ui/Card';
import { Icon } from '@/ui/Icon';
import { Text } from '@/ui/Text';
import { Box } from '@/ui/Box';
import { Stack } from '@/ui/Stack';
import { Accordion } from '@/ui/Accordion';
import { Copy } from 'lucide-react';
import React, { useState } from 'react';
@@ -40,9 +42,9 @@ export function ErrorDetailsBlock({ error }: ErrorDetailsBlockProps) {
};
return (
<div style={{ width: '100%', marginTop: '1.5rem', paddingTop: '1.5rem', borderTop: '1px solid var(--ui-color-border-muted)' }}>
<Box width="100%" marginTop={6} paddingTop={6} borderTop="1px solid var(--ui-color-border-muted)">
<Accordion title="Technical Logs">
<div style={{ display: 'flex', flexDirection: 'column', gap: '1rem' }}>
<Stack gap={4}>
<Card variant="outline">
<Text font="mono" size="xs" variant="low" block leading="relaxed" style={{ maxHeight: '12rem', overflow: 'auto' }}>
{error.stack || 'No stack trace available'}
@@ -50,7 +52,7 @@ export function ErrorDetailsBlock({ error }: ErrorDetailsBlockProps) {
</Text>
</Card>
<div style={{ display: 'flex', justifyContent: 'flex-end' }}>
<Box display="flex" justifyContent="end">
<Button
variant="secondary"
size="sm"
@@ -59,9 +61,9 @@ export function ErrorDetailsBlock({ error }: ErrorDetailsBlockProps) {
>
{copied ? 'Copied!' : 'Copy Details'}
</Button>
</div>
</div>
</Box>
</Stack>
</Accordion>
</div>
</Box>
);
}

View File

@@ -6,6 +6,9 @@ import { Glow } from '@/ui/Glow';
import { Heading } from '@/ui/Heading';
import { Icon } from '@/ui/Icon';
import { Text } from '@/ui/Text';
import { Box } from '@/ui/Box';
import { Group } from '@/ui/Group';
import { Stack } from '@/ui/Stack';
import { ErrorPageContainer } from '@/ui/ErrorPageContainer';
import { AlertTriangle, Home, RefreshCw, Terminal } from 'lucide-react';
import React from 'react';
@@ -28,39 +31,39 @@ export function GlobalErrorScreen({ error, reset, onHome }: GlobalErrorScreenPro
{/* Background Accents - Subtle telemetry vibe */}
<Glow color="primary" size="xl" position="center" opacity={0.03} />
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', marginBottom: '1.5rem', paddingBottom: '1rem', borderBottom: '1px solid var(--ui-color-border-default)' }}>
<div style={{ display: 'flex', alignItems: 'center', gap: '0.75rem' }}>
<Box display="flex" alignItems="center" justifyContent="between" marginBottom={6} paddingBottom={4} borderBottom="1px solid var(--ui-color-border-default)">
<Group gap={3}>
<Icon icon={AlertTriangle} size={5} intent="warning" />
<Heading level={2} weight="bold">
System Fault Detected
</Heading>
</div>
</Group>
<Text font="mono" size="xs" variant="low" uppercase>
Status: Critical
</Text>
</div>
</Box>
<div style={{ display: 'flex', flexDirection: 'column', gap: '2rem' }}>
<Stack gap={8}>
{/* Fault Description */}
<div style={{ display: 'flex', flexDirection: 'column', gap: '1rem' }}>
<Stack gap={4}>
<Text variant="med" size="base" leading="relaxed">
The application kernel encountered an unrecoverable execution error.
Telemetry has been captured for diagnostic review.
</Text>
<SystemStatusPanel error={error} />
</div>
</Stack>
{/* Recovery Actions */}
<RecoveryActions onRetry={reset} onHome={onHome} />
</div>
</Stack>
{/* Footer / Metadata */}
<div style={{ marginTop: '2rem', paddingTop: '1rem', borderTop: '1px solid var(--ui-color-border-default)', textAlign: 'right' }}>
<Box marginTop={8} paddingTop={4} borderTop="1px solid var(--ui-color-border-default)" textAlign="right">
<Text font="mono" size="xs" variant="low">
GP-CORE-ERR-{error.digest?.substring(0, 8).toUpperCase() || 'UNKNOWN'}
</Text>
</div>
</Box>
</ErrorPageContainer>
);
}
@@ -73,13 +76,13 @@ export function GlobalErrorScreen({ error, reset, onHome }: GlobalErrorScreenPro
function SystemStatusPanel({ error }: { error: Error & { digest?: string } }) {
return (
<Card variant="outline">
<div style={{ display: 'flex', flexDirection: 'column', gap: '0.75rem' }}>
<div style={{ display: 'flex', alignItems: 'center', gap: '0.5rem' }}>
<Stack gap={3}>
<Group gap={2}>
<Icon icon={Terminal} size={3} intent="low" />
<Text font="mono" size="xs" variant="low" uppercase>
Fault Log
</Text>
</div>
</Group>
<Text font="mono" size="sm" variant="warning" block>
{error.message || 'Unknown execution fault'}
</Text>
@@ -88,7 +91,7 @@ function SystemStatusPanel({ error }: { error: Error & { digest?: string } }) {
Digest: {error.digest}
</Text>
)}
</div>
</Stack>
</Card>
);
}
@@ -100,7 +103,7 @@ function SystemStatusPanel({ error }: { error: Error & { digest?: string } }) {
*/
function RecoveryActions({ onRetry, onHome }: { onRetry: () => void; onHome: () => void }) {
return (
<div style={{ display: 'flex', alignItems: 'center', gap: '1rem', flexWrap: 'wrap' }}>
<Group gap={4} wrap>
<Button
variant="primary"
onClick={onRetry}
@@ -115,6 +118,6 @@ function RecoveryActions({ onRetry, onHome }: { onRetry: () => void; onHome: ()
>
Return to Pits
</Button>
</div>
</Group>
);
}

View File

@@ -2,6 +2,7 @@
import { Button } from '@/ui/Button';
import { Text } from '@/ui/Text';
import { Stack } from '@/ui/Stack';
import React from 'react';
interface NotFoundCallToActionProps {
@@ -17,7 +18,7 @@ interface NotFoundCallToActionProps {
*/
export function NotFoundCallToAction({ label, onClick }: NotFoundCallToActionProps) {
return (
<div style={{ display: 'flex', flexDirection: 'column', alignItems: 'center', gap: '1rem' }}>
<Stack alignItems="center" gap={4}>
<Button
variant="primary"
size="lg"
@@ -28,6 +29,6 @@ export function NotFoundCallToAction({ label, onClick }: NotFoundCallToActionPro
<Text size="xs" variant="low" uppercase>
Telemetry connection lost
</Text>
</div>
</Stack>
);
}

View File

@@ -2,6 +2,7 @@
import { Button } from '@/ui/Button';
import { Icon } from '@/ui/Icon';
import { Group } from '@/ui/Group';
import { Home, LifeBuoy, RefreshCw } from 'lucide-react';
import React from 'react';
@@ -18,7 +19,7 @@ interface RecoveryActionsProps {
*/
export function RecoveryActions({ onRetry, onHome }: RecoveryActionsProps) {
return (
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'center', gap: '0.75rem', flexWrap: 'wrap', width: '100%' }}>
<Group gap={3} justify="center" wrap fullWidth>
<Button
variant="primary"
onClick={onRetry}
@@ -43,6 +44,6 @@ export function RecoveryActions({ onRetry, onHome }: RecoveryActionsProps) {
>
Contact Support
</Button>
</div>
</Group>
);
}

View File

@@ -4,6 +4,9 @@ import { Card } from '@/ui/Card';
import { Heading } from '@/ui/Heading';
import { Icon } from '@/ui/Icon';
import { Text } from '@/ui/Text';
import { Box } from '@/ui/Box';
import { Stack } from '@/ui/Stack';
import { Surface } from '@/ui/Surface';
import { AlertTriangle } from 'lucide-react';
import React from 'react';
@@ -20,24 +23,22 @@ interface ServerErrorPanelProps {
*/
export function ServerErrorPanel({ message, incidentId }: ServerErrorPanelProps) {
return (
<div style={{ display: 'flex', flexDirection: 'column', alignItems: 'center', gap: '1.5rem', width: '100%' }}>
<Stack alignItems="center" gap={6} fullWidth>
{/* Status Indicator */}
<div
style={{
padding: '1rem',
borderRadius: '9999px',
backgroundColor: 'rgba(255, 190, 77, 0.1)',
border: '1px solid rgba(255, 190, 77, 0.3)',
display: 'flex',
alignItems: 'center',
justifyContent: 'center'
}}
<Surface
padding={4}
rounded="full"
bg="rgba(255, 190, 77, 0.1)"
border="1px solid rgba(255, 190, 77, 0.3)"
display="flex"
alignItems="center"
justifyContent="center"
>
<Icon icon={AlertTriangle} size={8} intent="warning" />
</div>
</Surface>
{/* Primary Message */}
<div style={{ display: 'flex', flexDirection: 'column', alignItems: 'center', gap: '0.5rem' }}>
<Stack alignItems="center" gap={2}>
<Heading level={1} weight="bold">
CRITICAL_SYSTEM_FAILURE
</Heading>
@@ -45,12 +46,12 @@ export function ServerErrorPanel({ message, incidentId }: ServerErrorPanelProps)
The application engine encountered an unrecoverable state.
Telemetry has been dispatched to engineering.
</Text>
</div>
</Stack>
{/* Technical Summary */}
<div style={{ width: '100%' }}>
<Box fullWidth>
<Card variant="outline">
<div style={{ display: 'flex', flexDirection: 'column', gap: '0.5rem' }}>
<Stack gap={2}>
<Text font="mono" size="sm" variant="warning" block>
STATUS: 500_INTERNAL_SERVER_ERROR
</Text>
@@ -64,9 +65,9 @@ export function ServerErrorPanel({ message, incidentId }: ServerErrorPanelProps)
INCIDENT_ID: {incidentId}
</Text>
)}
</div>
</Stack>
</Card>
</div>
</div>
</Box>
</Stack>
);
}

View File

@@ -3,6 +3,8 @@
import React, { useEffect, useState } from 'react';
import { Button } from '@/ui/Button';
import { FeedItem } from '@/ui/FeedItem';
import { Text } from '@/ui/Text';
import { Stack } from '@/ui/Stack';
import { TimeDisplay } from '@/lib/display-objects/TimeDisplay';
interface FeedItemData {
@@ -50,10 +52,10 @@ export function FeedItemCard({ item }: FeedItemCardProps) {
}}
timestamp={TimeDisplay.timeAgo(item.timestamp)}
content={
<div style={{ display: 'flex', flexDirection: 'column', gap: '0.5rem' }}>
<div style={{ fontWeight: 'bold' }}>{item.headline}</div>
{item.body && <div>{item.body}</div>}
</div>
<Stack gap={2}>
<Text weight="bold" variant="high">{item.headline}</Text>
{item.body && <Text variant="med">{item.body}</Text>}
</Stack>
}
actions={item.ctaHref && item.ctaLabel ? (
<Button

View File

@@ -6,6 +6,8 @@ import { ActiveDriverCard } from '@/ui/ActiveDriverCard';
import { mediaConfig } from '@/lib/config/mediaConfig';
import { SectionHeader } from '@/ui/SectionHeader';
import { Icon } from '@/ui/Icon';
import { Box } from '@/ui/Box';
import { Grid } from '@/ui/Grid';
import { Activity } from 'lucide-react';
import React from 'react';
@@ -25,7 +27,7 @@ export function RecentActivity({ drivers, onDriverClick }: RecentActivityProps)
const activeDrivers = drivers.filter((d) => d.isActive).slice(0, 6);
return (
<div style={{ marginBottom: '2.5rem' }}>
<Box marginBottom={10}>
<SectionHeader
title="Active Drivers"
description="Currently competing in leagues"
@@ -33,7 +35,7 @@ export function RecentActivity({ drivers, onDriverClick }: RecentActivityProps)
actions={<Icon icon={Activity} size={5} intent="success" />}
/>
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fill, minmax(10rem, 1fr))', gap: '0.75rem' }}>
<Grid cols={{ base: 2, sm: 3, md: 4, lg: 6 }} gap={3}>
{activeDrivers.map((driver) => {
return (
<ActiveDriverCard
@@ -46,7 +48,7 @@ export function RecentActivity({ drivers, onDriverClick }: RecentActivityProps)
/>
);
})}
</div>
</div>
</Grid>
</Box>
);
}

View File

@@ -1,6 +1,7 @@
import { Icon } from '@/ui/Icon';
import { Text } from '@/ui/Text';
import { Badge } from '@/ui/Badge';
import { Group } from '@/ui/Group';
import { ChevronDown, ChevronUp, Minus } from 'lucide-react';
import React from 'react';
@@ -12,10 +13,10 @@ interface DeltaChipProps {
export function DeltaChip({ value, type = 'rank' }: DeltaChipProps) {
if (value === 0) {
return (
<div style={{ display: 'flex', alignItems: 'center', gap: '0.25rem' }}>
<Group gap={1}>
<Icon icon={Minus} size={3} intent="low" />
<Text size="xs" font="mono" variant="low">0</Text>
</div>
</Group>
);
}
@@ -26,12 +27,12 @@ export function DeltaChip({ value, type = 'rank' }: DeltaChipProps) {
return (
<Badge variant={variant} size="sm">
<div style={{ display: 'flex', alignItems: 'center', gap: '0.125rem' }}>
<Group gap={0.5}>
<Icon icon={IconComponent} size={3} />
<Text size="xs" font="mono" weight="bold">
{absoluteValue}
</Text>
</div>
</Group>
</Badge>
);
}

View File

@@ -2,6 +2,8 @@ import { Icon } from '@/ui/Icon';
import { Input } from '@/ui/Input';
import { ControlBar } from '@/ui/ControlBar';
import { Button } from '@/ui/Button';
import { Box } from '@/ui/Box';
import { Group } from '@/ui/Group';
import { Filter, Search } from 'lucide-react';
import React from 'react';
@@ -19,10 +21,10 @@ export function LeaderboardFiltersBar({
children,
}: LeaderboardFiltersBarProps) {
return (
<div style={{ marginBottom: '1.5rem' }}>
<Box marginBottom={6}>
<ControlBar
leftContent={
<div style={{ maxWidth: '32rem', width: '100%' }}>
<Box maxWidth="32rem" fullWidth>
<Input
type="text"
value={searchQuery}
@@ -31,10 +33,10 @@ export function LeaderboardFiltersBar({
icon={<Icon icon={Search} size={4} intent="low" />}
fullWidth
/>
</div>
</Box>
}
>
<div style={{ display: 'flex', alignItems: 'center', gap: '1rem' }}>
<Group gap={4}>
{children}
<Button
variant="secondary"
@@ -43,8 +45,8 @@ export function LeaderboardFiltersBar({
>
Filters
</Button>
</div>
</Group>
</ControlBar>
</div>
</Box>
);
}

View File

@@ -18,42 +18,42 @@ export function LeaderboardsHero({ onNavigateToDrivers, onNavigateToTeams }: Lea
return (
<Surface
variant="muted"
rounded="2xl"
rounded="xl"
border
padding={8}
position="relative"
overflow="hidden"
bg="bg-gradient-to-br from-primary-blue/10 via-deep-charcoal to-graphite-black"
borderColor="border-primary-blue/20"
bg="linear-gradient(to bottom right, rgba(25, 140, 255, 0.1), var(--ui-color-bg-surface), var(--ui-color-bg-base))"
borderColor="var(--ui-color-border-default)"
>
<DecorativeBlur color="blue" size="lg" position="top-right" opacity={10} />
<DecorativeBlur color="purple" size="md" position="bottom-left" opacity={5} />
<Stack position="relative" zIndex={10}>
<Stack direction="row" align="center" gap={4} mb={4}>
<Stack
p={3}
<Stack direction="row" align="center" gap={4} marginBottom={4}>
<Surface
padding={3}
bg="linear-gradient(to bottom right, rgba(25, 140, 255, 0.2), rgba(25, 140, 255, 0.05))"
border
borderColor="border-primary-blue/30"
borderColor="rgba(25, 140, 255, 0.3)"
rounded="xl"
display="flex"
alignItems="center"
justifyContent="center"
>
<Icon icon={Award} size={7} color="text-primary-blue" />
</Stack>
<Icon icon={Award} size={7} intent="primary" />
</Surface>
<Stack>
<Heading level={1} weight="bold" letterSpacing="tight">Leaderboards</Heading>
<Text color="text-gray-400" block mt={1} size="sm" uppercase letterSpacing="widest" weight="bold">Precision Performance Tracking</Text>
<Heading level={1} weight="bold">Leaderboards</Heading>
<Text variant="low" block marginTop={1} size="sm" uppercase weight="bold">Precision Performance Tracking</Text>
</Stack>
</Stack>
<Text
size="lg"
color="text-gray-400"
variant="low"
block
mb={8}
marginBottom={8}
leading="relaxed"
maxWidth="42rem"
>
@@ -65,7 +65,6 @@ export function LeaderboardsHero({ onNavigateToDrivers, onNavigateToTeams }: Lea
variant="primary"
onClick={onNavigateToDrivers}
icon={<Icon icon={Trophy} size={4} />}
shadow="shadow-lg shadow-primary-blue/20"
>
Driver Rankings
</Button>
@@ -73,7 +72,6 @@ export function LeaderboardsHero({ onNavigateToDrivers, onNavigateToTeams }: Lea
variant="secondary"
onClick={onNavigateToTeams}
icon={<Icon icon={Users} size={4} />}
hoverBg="bg-white/5"
>
Team Rankings
</Button>

View File

@@ -1,5 +1,6 @@
import { Badge } from '@/ui/Badge';
import { Text } from '@/ui/Text';
import { Group } from '@/ui/Group';
import React from 'react';
interface RankBadgeProps {
@@ -28,10 +29,10 @@ export function RankBadge({ rank, size = 'md' }: RankBadgeProps) {
return (
<Badge variant={getVariant(rank)} size={size}>
<div style={{ display: 'flex', alignItems: 'center', gap: '0.25rem' }}>
{medal && <span>{medal}</span>}
<Group gap={1}>
{medal && <Text size="xs">{medal}</Text>}
<Text size="xs" weight="bold">#{rank}</Text>
</div>
</Group>
</Badge>
);
}

View File

@@ -1,6 +1,7 @@
import { MedalDisplay } from '@/lib/display-objects/MedalDisplay';
import { Icon } from '@/ui/Icon';
import { Text } from '@/ui/Text';
import { Surface } from '@/ui/Surface';
import { Crown, Medal } from 'lucide-react';
import React from 'react';
@@ -33,23 +34,21 @@ export function RankMedal({ rank, size = 'md', showIcon = true }: RankMedalProps
};
return (
<div
style={{
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
borderRadius: '9999px',
border: '1px solid var(--ui-color-border-default)',
height: sizePx[size],
width: sizePx[size],
backgroundColor: 'var(--ui-color-bg-surface-muted)'
}}
<Surface
variant="muted"
rounded="full"
border
height={sizePx[size]}
width={sizePx[size]}
display="flex"
alignItems="center"
justifyContent="center"
>
{isTop3 && showIcon ? (
<Icon icon={rank === 1 ? Crown : Medal} size={iconSize[size]} intent={variant as any} />
) : (
<Text weight="bold" size={textSizeMap[size]} variant={variant as any}>{rank}</Text>
)}
</div>
</Surface>
);
}

View File

@@ -2,6 +2,10 @@ import { getMediaUrl } from '@/lib/utilities/media';
import { Image } from '@/ui/Image';
import { TableCell, TableRow } from '@/ui/Table';
import { Text } from '@/ui/Text';
import { Box } from '@/ui/Box';
import { Group } from '@/ui/Group';
import { Stack } from '@/ui/Stack';
import { Surface } from '@/ui/Surface';
import { RankMedal } from './RankMedal';
import React from 'react';
@@ -34,22 +38,22 @@ export function TeamRankingRow({
onClick={onClick}
>
<TableCell>
<div style={{ width: '2rem', display: 'flex', justifyContent: 'center' }}>
<Box width="2rem" display="flex" justifyContent="center">
<RankMedal rank={rank} size="md" />
</div>
</Box>
</TableCell>
<TableCell>
<div style={{ display: 'flex', alignItems: 'center', gap: '0.75rem' }}>
<div style={{
position: 'relative',
width: '2.5rem',
height: '2.5rem',
borderRadius: '0.5rem',
overflow: 'hidden',
border: '1px solid var(--ui-color-border-default)',
backgroundColor: 'var(--ui-color-bg-surface-muted)'
}}>
<Group gap={3}>
<Surface
position="relative"
width="2.5rem"
height="2.5rem"
rounded="md"
overflow="hidden"
border
variant="muted"
>
<Image
src={logoUrl || getMediaUrl('team-logo', id)}
alt={name}
@@ -57,8 +61,8 @@ export function TeamRankingRow({
height={40}
objectFit="cover"
/>
</div>
<div style={{ minWidth: 0 }}>
</Surface>
<Stack gap={0} flex={1} minWidth="0">
<Text
weight="semibold"
variant="high"
@@ -70,8 +74,8 @@ export function TeamRankingRow({
<Text size="xs" variant="low" block>
{memberCount} Members
</Text>
</div>
</div>
</Stack>
</Group>
</TableCell>
<TableCell textAlign="center">

View File

@@ -4,6 +4,9 @@ import { IconButton } from '@/ui/IconButton';
import { Panel } from '@/ui/Panel';
import { Icon } from '@/ui/Icon';
import { Text } from '@/ui/Text';
import { Box } from '@/ui/Box';
import { Group } from '@/ui/Group';
import { Stack } from '@/ui/Stack';
import { ListItem, ListItemInfo, ListItemActions } from '@/ui/ListItem';
import { EmptyState } from '@/ui/EmptyState';
import { Check, Clock, X, UserPlus } from 'lucide-react';
@@ -34,17 +37,17 @@ export function JoinRequestsPanel({ requests, onAccept, onDecline }: JoinRequest
return (
<Panel title={`Pending Requests (${requests.length})`}>
<div style={{ display: 'flex', flexDirection: 'column', gap: '0.75rem' }}>
<Stack gap={3}>
{requests.map((request) => (
<ListItem key={request.id}>
<ListItemInfo
title={request.driverName}
description={request.message}
meta={
<div style={{ display: 'flex', alignItems: 'center', gap: '0.375rem' }}>
<Group gap={1.5}>
<Icon icon={Clock} size={3} intent="low" />
<Text size="xs" variant="low" font="mono">{request.requestedAt}</Text>
</div>
</Group>
}
/>
<ListItemActions>
@@ -66,7 +69,7 @@ export function JoinRequestsPanel({ requests, onAccept, onDecline }: JoinRequest
</ListItemActions>
</ListItem>
))}
</div>
</Stack>
</Panel>
);
}

View File

@@ -7,6 +7,8 @@ import { Icon } from '@/ui/Icon';
import { Input } from '@/ui/Input';
import { Grid } from '@/ui/Grid';
import { Stack } from '@/ui/Stack';
import { Box } from '@/ui/Box';
import { Group } from '@/ui/Group';
import { Surface } from '@/ui/Surface';
import { Text } from '@/ui/Text';
import { TextArea } from '@/ui/TextArea';
@@ -46,42 +48,38 @@ export function LeagueBasicsSection({
return (
<Stack gap={8}>
{/* Emotional header for the step */}
<Stack textAlign="center" pb={2}>
<Stack mb={2}>
<Stack textAlign="center" paddingBottom={2}>
<Box marginBottom={2}>
<Heading level={3}>
Every great championship starts with a name
</Heading>
</Stack>
<Stack maxWidth="lg" mx="auto">
<Text size="sm" color="text-gray-400">
</Box>
<Box maxWidth="lg" marginX="auto">
<Text size="sm" variant="low">
This is where legends begin. Give your league an identity that drivers will remember.
</Text>
</Stack>
</Box>
</Stack>
{/* League name */}
<Stack gap={3}>
<Input
label={
<Stack direction="row" align="center" gap={2}>
<Icon icon={FileText} size={4} color="var(--primary-blue)" />
<Text size="sm" weight="medium" color="text-gray-300">League name *</Text>
</Stack>
}
label="League name *"
icon={<Icon icon={FileText} size={4} intent="primary" />}
value={basics.name}
onChange={(e: React.ChangeEvent<HTMLInputElement>) => updateBasics({ name: e.target.value })}
placeholder="e.g., GridPilot Sprint Series"
variant={errors?.name ? 'error' : 'default'}
errorMessage={errors?.name}
error={errors?.name}
disabled={disabled}
autoFocus
fullWidth
/>
<Stack gap={2}>
<Text size="xs" color="text-gray-500">
<Text size="xs" variant="low">
Make it memorable this is what drivers will see first
</Text>
<Stack direction="row" wrap gap={2}>
<Text size="xs" color="text-gray-500">Try:</Text>
<Group wrap gap={2}>
<Text size="xs" variant="low">Try:</Text>
{[
'Sunday Showdown Series',
'Midnight Endurance League',
@@ -94,27 +92,19 @@ export function LeagueBasicsSection({
variant="secondary"
size="sm"
disabled={disabled}
rounded="full"
fontSize="0.75rem"
px={2}
py={0.5}
h="auto"
rounded
style={{ fontSize: '0.75rem', height: 'auto', padding: '0.125rem 0.5rem' }}
>
{name}
</Button>
))}
</Stack>
</Group>
</Stack>
</Stack>
{/* Description - Now Required */}
<TextArea
label={
<Stack direction="row" align="center" gap={2}>
<Icon icon={FileText} size={4} color="var(--primary-blue)" />
<Text size="sm" weight="medium" color="text-gray-300">Tell your story *</Text>
</Stack>
}
label="Tell your story *"
value={basics.description ?? ''}
onChange={(e: React.ChangeEvent<HTMLTextAreaElement>) =>
updateBasics({
@@ -123,27 +113,27 @@ export function LeagueBasicsSection({
}
rows={4}
disabled={disabled}
variant={errors?.description ? 'error' : 'default'}
errorMessage={errors?.description}
error={errors?.description}
placeholder="What makes your league special? Tell drivers what to expect..."
fullWidth
/>
<Surface variant="muted" rounded="lg" border padding={4}>
<Stack mb={3}>
<Text size="xs" color="text-gray-400">
<Text weight="medium" color="text-gray-300">Great descriptions include:</Text>
<Box marginBottom={3}>
<Text size="xs" variant="low">
<Text weight="medium" variant="med">Great descriptions include:</Text>
</Text>
</Stack>
</Box>
<Grid cols={3} gap={3}>
{[
'Racing style & pace',
'Schedule & timezone',
'Community vibe'
].map(item => (
<Stack key={item} direction="row" align="start" gap={2}>
<Icon icon={Check} size={3.5} color="var(--performance-green)" mt={0.5} />
<Text size="xs" color="text-gray-400">{item}</Text>
</Stack>
<Group key={item} align="start" gap={2}>
<Icon icon={Check} size={3.5} intent="success" style={{ marginTop: '0.125rem' }} />
<Text size="xs" variant="low">{item}</Text>
</Group>
))}
</Grid>
</Surface>
@@ -151,16 +141,13 @@ export function LeagueBasicsSection({
{/* Game Platform */}
<Stack gap={2}>
<Input
label={
<Stack direction="row" align="center" gap={2}>
<Icon icon={Gamepad2} size={4} color="var(--text-gray-400)" />
<Text size="sm" weight="medium" color="text-gray-300">Game platform</Text>
</Stack>
}
label="Game platform"
icon={<Icon icon={Gamepad2} size={4} intent="low" />}
value="iRacing"
disabled
fullWidth
/>
<Text size="xs" color="text-gray-500">
<Text size="xs" variant="low">
More platforms soon
</Text>
</Stack>

View File

@@ -5,6 +5,9 @@ import { Icon } from '@/ui/Icon';
import { Image } from '@/ui/Image';
import { PlaceholderImage } from '@/ui/PlaceholderImage';
import { Text } from '@/ui/Text';
import { Box } from '@/ui/Box';
import { Group } from '@/ui/Group';
import { Surface } from '@/ui/Surface';
import { LeagueCard as UILeagueCard, LeagueCardStats, LeagueCardFooter } from '@/ui/LeagueCard';
import { Calendar as LucideCalendar } from 'lucide-react';
import React, { ReactNode } from 'react';
@@ -50,7 +53,14 @@ export function LeagueCard({
onClick={onClick}
coverUrl={coverUrl}
logo={
<div style={{ width: '3rem', height: '3rem', borderRadius: '0.5rem', overflow: 'hidden', border: '1px solid var(--ui-color-border-default)', backgroundColor: 'var(--ui-color-bg-base)' }}>
<Surface
width="3rem"
height="3rem"
rounded="md"
overflow="hidden"
border
variant="dark"
>
{logoUrl ? (
<Image
src={logoUrl}
@@ -62,7 +72,7 @@ export function LeagueCard({
) : (
<PlaceholderImage size={48} />
)}
</div>
</Surface>
}
badges={
<React.Fragment>
@@ -71,12 +81,14 @@ export function LeagueCard({
</React.Fragment>
}
>
<div style={{ display: 'flex', alignItems: 'center', gap: '0.5rem', marginBottom: '0.25rem' }}>
<div style={{ width: '0.25rem', height: '1rem', backgroundColor: 'var(--ui-color-intent-primary)' }} />
<Heading level={3} weight="bold" truncate>{name}</Heading>
</div>
<Box marginBottom={1}>
<Group gap={2}>
<Box width="0.25rem" height="1rem" bg="var(--ui-color-intent-primary)" />
<Heading level={3} weight="bold" truncate>{name}</Heading>
</Group>
</Box>
<Text size="xs" variant="low" lineClamp={2} style={{ height: '2.5rem', marginBottom: '1rem' }} block leading="relaxed">
<Text size="xs" variant="low" lineClamp={2} style={{ height: '2.5rem' }} block leading="relaxed" marginBottom={4}>
{description || 'No description available'}
</Text>
@@ -88,20 +100,32 @@ export function LeagueCard({
/>
{hasOpenSlots && (
<div style={{ display: 'flex', alignItems: 'center', gap: '0.375rem', padding: '0.25rem 0.5rem', backgroundColor: 'rgba(25, 140, 255, 0.05)', border: '1px solid rgba(25, 140, 255, 0.2)', borderRadius: 'var(--ui-radius-sm)', width: 'fit-content', marginBottom: '1rem' }}>
<div style={{ width: '0.375rem', height: '0.375rem', borderRadius: '9999px', backgroundColor: 'var(--ui-color-intent-primary)' }} />
<Text size="xs" variant="primary" weight="bold" uppercase>{openSlotsCount} OPEN</Text>
</div>
<Box marginBottom={4}>
<Surface
display="flex"
alignItems="center"
gap={1.5}
paddingX={2}
paddingY={1}
bg="rgba(25, 140, 255, 0.05)"
border="1px solid rgba(25, 140, 255, 0.2)"
rounded="sm"
width="fit-content"
>
<Box width="0.375rem" height="0.375rem" rounded="full" bg="var(--ui-color-intent-primary)" />
<Text size="xs" variant="primary" weight="bold" uppercase>{openSlotsCount} OPEN</Text>
</Surface>
</Box>
)}
<LeagueCardFooter>
{timingSummary && (
<div style={{ display: 'flex', alignItems: 'center', gap: '0.5rem' }}>
<Group gap={2}>
<Icon icon={LucideCalendar} size={3} intent="low" />
<Text size="xs" variant="low" font="mono">
{timingSummary.split('•')[1]?.trim() || timingSummary}
</Text>
</div>
</Group>
)}
</LeagueCardFooter>
</UILeagueCard>

View File

@@ -4,6 +4,10 @@ import type { LeagueConfigFormModel } from '@/lib/types/LeagueConfigFormModel';
import { Heading } from '@/ui/Heading';
import { Icon } from '@/ui/Icon';
import { Text } from '@/ui/Text';
import { Box } from '@/ui/Box';
import { Group } from '@/ui/Group';
import { Stack } from '@/ui/Stack';
import { Surface } from '@/ui/Surface';
import { InfoFlyout } from '@/ui/InfoFlyout';
import { Stepper } from '@/ui/Stepper';
import { Button } from '@/ui/Button';
@@ -91,14 +95,22 @@ export function LeagueDropSection({
const needsN = dropPolicy.strategy !== 'none';
return (
<div style={{ display: 'flex', flexDirection: 'column', gap: '1rem' }}>
<Stack gap={4}>
{/* Section header */}
<div style={{ display: 'flex', alignItems: 'center', gap: '0.75rem' }}>
<div style={{ display: 'flex', width: '2.5rem', height: '2.5rem', alignItems: 'center', justifyContent: 'center', borderRadius: '0.75rem', backgroundColor: 'rgba(25, 140, 255, 0.1)' }}>
<Group gap={3}>
<Surface
display="flex"
width="2.5rem"
height="2.5rem"
alignItems="center"
justifyContent="center"
rounded="md"
bg="rgba(25, 140, 255, 0.1)"
>
<Icon icon={TrendingDown} size={5} intent="primary" />
</div>
<div style={{ flex: 1 }}>
<div style={{ display: 'flex', alignItems: 'center', gap: '0.5rem' }}>
</Surface>
<Stack flex={1} gap={0}>
<Group gap={2}>
<Heading level={3}>Drop Rules</Heading>
<IconButton
ref={dropInfoRef}
@@ -108,10 +120,10 @@ export function LeagueDropSection({
onClick={() => setShowDropFlyout(true)}
title="Help"
/>
</div>
</Group>
<Text size="xs" variant="low">Protect from bad races</Text>
</div>
</div>
</Stack>
</Group>
<InfoFlyout
isOpen={showDropFlyout}
@@ -126,7 +138,7 @@ export function LeagueDropSection({
</InfoFlyout>
{/* Strategy buttons + N stepper inline */}
<div style={{ display: 'flex', flexWrap: 'wrap', alignItems: 'center', gap: '0.5rem' }}>
<Group gap={2} wrap>
{DROP_OPTIONS.map((option) => {
const isSelected = dropPolicy.strategy === option.value;
return (
@@ -137,26 +149,26 @@ export function LeagueDropSection({
onClick={() => handleStrategyChange(option.value)}
disabled={disabled}
>
<div style={{ display: 'flex', alignItems: 'center', gap: '0.5rem' }}>
<Group gap={2}>
{isSelected && <Icon icon={Check} size={3} />}
<span>{option.emoji}</span>
<span>{option.label}</span>
</div>
<Text as="span">{option.emoji}</Text>
<Text as="span">{option.label}</Text>
</Group>
</Button>
);
})}
{needsN && (
<div style={{ marginLeft: '0.5rem' }}>
<Box marginLeft={2}>
<Stepper
value={dropPolicy.n ?? 1}
onChange={handleNChange}
label="N ="
disabled={disabled}
/>
</div>
</Box>
)}
</div>
</Group>
{/* Explanation text */}
<Text size="xs" variant="low" block>
@@ -164,6 +176,6 @@ export function LeagueDropSection({
{dropPolicy.strategy === 'bestNResults' && `Only your best ${dropPolicy.n ?? 1} results will count.`}
{dropPolicy.strategy === 'dropWorstN' && `Your worst ${dropPolicy.n ?? 1} results will be excluded.`}
</Text>
</div>
</Stack>
);
}

View File

@@ -14,6 +14,8 @@ import { DriverViewModel } from '@/lib/view-models/DriverViewModel';
import { Button } from '@/ui/Button';
import { Select } from '@/ui/Select';
import { Text } from '@/ui/Text';
import { Box } from '@/ui/Box';
import { Group } from '@/ui/Group';
import { ControlBar } from '@/ui/ControlBar';
import { useCallback, useEffect, useState } from 'react';
import React from 'react';
@@ -135,7 +137,7 @@ export function LeagueMembers({
}
return (
<div>
<Box>
<ControlBar
leftContent={
<Text size="sm" variant="low">
@@ -143,7 +145,7 @@ export function LeagueMembers({
</Text>
}
>
<div style={{ display: 'flex', alignItems: 'center', gap: '0.5rem' }}>
<Group gap={2}>
<Text size="sm" variant="low">Sort by:</Text>
<Select
value={sortBy}
@@ -158,10 +160,10 @@ export function LeagueMembers({
]}
fullWidth={false}
/>
</div>
</Group>
</ControlBar>
<div style={{ overflowX: 'auto', marginTop: '1rem' }}>
<Box overflowX="auto" marginTop={4}>
<LeagueMemberTable showActions={showActions}>
{sortedMembers.map((member, index) => {
const isCurrentUser = member.driverId === currentDriverId;
@@ -190,7 +192,7 @@ export function LeagueMembers({
href={routes.driver.detail(member.driverId)}
meta={ratingAndWinsMeta}
actions={showActions && !cannotModify && !isCurrentUser ? (
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'flex-end', gap: '0.5rem' }}>
<Group gap={2} justify="end">
{onUpdateRole && (
<Select
value={member.role}
@@ -212,13 +214,13 @@ export function LeagueMembers({
<Text variant="critical">Remove</Text>
</Button>
)}
</div>
</Group>
) : (showActions && cannotModify ? <Text size="xs" variant="low"></Text> : undefined)}
/>
);
})}
</LeagueMemberTable>
</div>
</div>
</Box>
</Box>
);
}

View File

@@ -4,6 +4,8 @@ import { Icon } from '@/ui/Icon';
import { Panel } from '@/ui/Panel';
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from '@/ui/Table';
import { Text } from '@/ui/Text';
import { Box } from '@/ui/Box';
import { Group } from '@/ui/Group';
import { PositionBadge } from '@/ui/ResultRow';
import { TrendingUp, Trophy } from 'lucide-react';
import React from 'react';
@@ -32,7 +34,7 @@ export function StandingsTableShell({ standings, title = 'Championship Standings
<Text size="xs" variant="low">{standings.length} Drivers</Text>
}
>
<div style={{ overflowX: 'auto' }}>
<Box overflowX="auto">
<Table>
<TableHead>
<TableRow>
@@ -50,10 +52,10 @@ export function StandingsTableShell({ standings, title = 'Championship Standings
<PositionBadge position={entry.position} />
</TableCell>
<TableCell>
<div style={{ display: 'flex', alignItems: 'center', gap: '0.75rem' }}>
<Group gap={3}>
<Text weight="bold" variant="high">{entry.driverName}</Text>
{entry.change !== undefined && entry.change !== 0 && (
<div style={{ display: 'flex', alignItems: 'center', gap: '0.125rem' }}>
<Group gap={0.5}>
<Icon
icon={TrendingUp}
size={3}
@@ -63,9 +65,9 @@ export function StandingsTableShell({ standings, title = 'Championship Standings
<Text size="xs" variant={entry.change > 0 ? 'success' : 'critical'}>
{Math.abs(entry.change)}
</Text>
</div>
</Group>
)}
</div>
</Group>
</TableCell>
<TableCell textAlign="center">
<Text size="sm" variant={entry.wins > 0 ? 'high' : 'low'}>{entry.wins}</Text>
@@ -80,7 +82,7 @@ export function StandingsTableShell({ standings, title = 'Championship Standings
))}
</TableBody>
</Table>
</div>
</Box>
</Panel>
);
}

View File

@@ -6,6 +6,8 @@ import { Select } from '@/ui/Select';
import { ControlBar } from '@/ui/ControlBar';
import { SegmentedControl } from '@/ui/SegmentedControl';
import { Icon } from '@/ui/Icon';
import { Box } from '@/ui/Box';
import { Group } from '@/ui/Group';
import { Grid, List, Search } from 'lucide-react';
import React from 'react';
@@ -29,10 +31,10 @@ export function MediaFiltersBar({
onViewModeChange,
}: MediaFiltersBarProps) {
return (
<div style={{ marginBottom: '1.5rem' }}>
<Box marginBottom={6}>
<ControlBar
leftContent={
<div style={{ maxWidth: '32rem', width: '100%' }}>
<Box maxWidth="32rem" fullWidth>
<Input
placeholder="Search media assets..."
value={searchQuery}
@@ -40,17 +42,17 @@ export function MediaFiltersBar({
icon={<Icon icon={Search} size={4} intent="low" />}
fullWidth
/>
</div>
</Box>
}
>
<div style={{ display: 'flex', alignItems: 'center', gap: '1rem' }}>
<div style={{ width: '10rem' }}>
<Group gap={4}>
<Box width="10rem">
<Select
value={category}
onChange={(e) => onCategoryChange(e.target.value)}
options={categories}
/>
</div>
</Box>
{onViewModeChange && (
<SegmentedControl
@@ -62,8 +64,8 @@ export function MediaFiltersBar({
onChange={(id) => onViewModeChange(id as 'grid' | 'list')}
/>
)}
</div>
</Group>
</ControlBar>
</div>
</Box>
);
}

View File

@@ -5,6 +5,7 @@ import { useState } from 'react';
import { MediaCard } from '@/ui/MediaCard';
import { MediaFiltersBar } from './MediaFiltersBar';
import { Grid } from '@/ui/Grid';
import { Stack } from '@/ui/Stack';
import { MediaViewerModal } from './MediaViewerModal';
import { SectionHeader } from '@/ui/SectionHeader';
import { EmptyState } from '@/ui/EmptyState';
@@ -63,7 +64,7 @@ export function MediaGallery({
};
return (
<div style={{ display: 'flex', flexDirection: 'column', gap: '1.5rem' }}>
<Stack gap={6}>
<SectionHeader
title={title}
description={description}
@@ -109,6 +110,6 @@ export function MediaGallery({
onNext={handleNext}
onPrev={handlePrev}
/>
</div>
</Stack>
);
}

View File

@@ -4,6 +4,7 @@ import { IconButton } from '@/ui/IconButton';
import { Image } from '@/ui/Image';
import { Modal } from '@/ui/Modal';
import { Text } from '@/ui/Text';
import { Box } from '@/ui/Box';
import { ChevronLeft, ChevronRight, Download } from 'lucide-react';
import React from 'react';
@@ -33,11 +34,11 @@ export function MediaViewerModal({
title={title}
size="xl"
footer={
<div style={{ width: '100%', textAlign: 'center' }}>
<Box width="100%" textAlign="center">
<Text size="xs" variant="low" uppercase>
Precision Racing Media Viewer
</Text>
</div>
</Box>
}
actions={
<IconButton
@@ -49,7 +50,7 @@ export function MediaViewerModal({
/>
}
>
<div style={{ position: 'relative', display: 'flex', alignItems: 'center', justifyContent: 'center', minHeight: '20rem' }}>
<Box position="relative" display="flex" alignItems="center" justifyContent="center" minHeight="20rem">
{src ? (
<Image
src={src}
@@ -62,26 +63,26 @@ export function MediaViewerModal({
{/* Navigation Controls */}
{onPrev && (
<div style={{ position: 'absolute', left: '1rem', top: '50%', transform: 'translateY(-50%)' }}>
<Box position="absolute" left={4} top="50%" style={{ transform: 'translateY(-50%)' }}>
<IconButton
icon={ChevronLeft}
variant="secondary"
onClick={onPrev}
title="Previous"
/>
</div>
</Box>
)}
{onNext && (
<div style={{ position: 'absolute', right: '1rem', top: '50%', transform: 'translateY(-50%)' }}>
<Box position="absolute" right={4} top="50%" style={{ transform: 'translateY(-50%)' }}>
<IconButton
icon={ChevronRight}
variant="secondary"
onClick={onNext}
title="Next"
/>
</div>
</Box>
)}
</div>
</Box>
</Modal>
);
}

View File

@@ -4,6 +4,9 @@ import { Button } from '@/ui/Button';
import { Icon } from '@/ui/Icon';
import { Modal } from '@/ui/Modal';
import { Text } from '@/ui/Text';
import { Box } from '@/ui/Box';
import { Grid } from '@/ui/Grid';
import { Surface } from '@/ui/Surface';
import { NotificationStat, NotificationDeadline } from '@/ui/NotificationContent';
import {
AlertCircle,
@@ -144,17 +147,19 @@ export function ModalNotification({
</Text>
{isRaceNotification && (
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: '1rem', marginTop: '1.5rem' }}>
<NotificationStat
label="POSITION"
value={notification.data?.position === 'DNF' ? 'DNF' : `P${notification.data?.position || '?'}`}
/>
<NotificationStat
label="RATING"
value={`${ratingChange >= 0 ? '+' : ''}${ratingChange}`}
intent={ratingChange >= 0 ? 'success' : 'critical'}
/>
</div>
<Box marginTop={6}>
<Grid cols={2} gap={4}>
<NotificationStat
label="POSITION"
value={notification.data?.position === 'DNF' ? 'DNF' : `P${notification.data?.position || '?'}`}
/>
<NotificationStat
label="RATING"
value={`${ratingChange >= 0 ? '+' : ''}${ratingChange}`}
intent={ratingChange >= 0 ? 'success' : 'critical'}
/>
</Grid>
</Box>
)}
{hasDeadline && !isRaceNotification && (
@@ -166,18 +171,24 @@ export function ModalNotification({
)}
{protestId && (
<div style={{ marginTop: '1.5rem', padding: '0.75rem', backgroundColor: 'var(--ui-color-bg-base)', border: '1px solid var(--ui-color-border-default)', borderRadius: 'var(--ui-radius-sm)' }}>
<Surface
marginTop={6}
padding={3}
variant="dark"
border
rounded="sm"
>
<Text size="xs" variant="low" weight="bold" uppercase block marginBottom={1}>PROTEST ID</Text>
<Text size="xs" variant="med" font="mono" block>{protestId}</Text>
</div>
</Surface>
)}
{notification.requiresResponse && !isRaceNotification && (
<div style={{ marginTop: '1rem', textAlign: 'center' }}>
<Box marginTop={4} textAlign="center">
<Text size="xs" variant="critical" weight="medium">
This action is required to continue
</Text>
</div>
</Box>
)}
</Modal>
);

View File

@@ -3,6 +3,8 @@
import { Icon } from '@/ui/Icon';
import { Toast } from '@/ui/Toast';
import { Text } from '@/ui/Text';
import { Box } from '@/ui/Box';
import { Group } from '@/ui/Group';
import {
AlertTriangle,
Bell,
@@ -95,15 +97,16 @@ export function ToastNotification({
{notification.message}
</Text>
{notification.actionUrl && (
<div
onClick={handleClick}
style={{ marginTop: '0.5rem', display: 'flex', alignItems: 'center', gap: '0.25rem', cursor: 'pointer' }}
>
<Text size="xs" weight="medium" variant="primary">
View details
</Text>
<Icon icon={ExternalLink} size={3} intent="primary" />
</div>
<Box marginTop={2}>
<Box onClick={handleClick} style={{ cursor: 'pointer' }}>
<Group gap={1}>
<Text size="xs" weight="medium" variant="primary">
View details
</Text>
<Icon icon={ExternalLink} size={3} intent="primary" />
</Group>
</Box>
</Box>
)}
</Toast>
);

View File

@@ -1,5 +1,17 @@
import { Box } from '@/ui/Box';
export function OnboardingCardAccent() {
return (
<div className="absolute top-0 right-0 w-40 h-40 bg-gradient-to-bl from-primary-blue/10 to-transparent rounded-bl-full" />
<Box
position="absolute"
top={0}
right={0}
width="10rem"
height="10rem"
style={{
background: 'linear-gradient(to bottom left, rgba(25, 140, 255, 0.1), transparent)',
borderBottomLeftRadius: '9999px'
}}
/>
);
}

View File

@@ -1,11 +1,16 @@
import { Container } from '@/ui/Container';
import { Box } from '@/ui/Box';
interface OnboardingContainerProps {
children: React.ReactNode;
}
export function OnboardingContainer({ children }: OnboardingContainerProps) {
return (
<div className="max-w-3xl mx-auto px-4 py-10">
{children}
</div>
<Container size="md">
<Box paddingY={10}>
{children}
</Box>
</Container>
);
}

View File

@@ -1,3 +1,8 @@
import { Box } from '@/ui/Box';
import { Heading } from '@/ui/Heading';
import { Text } from '@/ui/Text';
import { Surface } from '@/ui/Surface';
interface OnboardingHeaderProps {
title: string;
subtitle: string;
@@ -6,12 +11,23 @@ interface OnboardingHeaderProps {
export function OnboardingHeader({ title, subtitle, emoji }: OnboardingHeaderProps) {
return (
<div className="text-center mb-8">
<div className="flex h-16 w-16 items-center justify-center rounded-2xl bg-gradient-to-br from-primary-blue/20 to-purple-600/10 border border-primary-blue/30 mx-auto mb-4">
<span className="text-2xl">{emoji}</span>
</div>
<h1 className="text-4xl font-bold mb-2">{title}</h1>
<p className="text-gray-400">{subtitle}</p>
</div>
<Box textAlign="center" marginBottom={8}>
<Surface
variant="gradient-blue"
rounded="xl"
width="4rem"
height="4rem"
display="flex"
alignItems="center"
justifyContent="center"
marginX="auto"
marginBottom={4}
border="1px solid var(--ui-color-intent-primary)"
>
<Text size="2xl">{emoji}</Text>
</Surface>
<Heading level={1} size="4xl" weight="bold" marginBottom={2}>{title}</Heading>
<Text variant="low">{subtitle}</Text>
</Box>
);
}

View File

@@ -1,7 +1,12 @@
import { Text } from '@/ui/Text';
import { Box } from '@/ui/Box';
export function OnboardingHelpText() {
return (
<p className="text-center text-xs text-gray-500 mt-6">
Your avatar will be AI-generated based on your photo and chosen suit color
</p>
<Box marginTop={6} textAlign="center">
<Text size="xs" variant="low">
Your avatar will be AI-generated based on your photo and chosen suit color
</Text>
</Box>
);
}

View File

@@ -3,6 +3,7 @@
import { LeagueListItem } from '@/components/leagues/LeagueListItem';
import { Card } from '@/ui/Card';
import { Text } from '@/ui/Text';
import { Stack } from '@/ui/Stack';
import { ProfileSection } from './ProfileSection';
import React from 'react';
@@ -22,7 +23,7 @@ interface MembershipPanelProps {
export function MembershipPanel({ ownedLeagues, memberLeagues }: MembershipPanelProps) {
return (
<div style={{ display: 'flex', flexDirection: 'column', gap: '2rem' }}>
<Stack gap={8}>
<ProfileSection
title="Leagues You Own"
description="Manage the leagues you have created and lead."
@@ -34,11 +35,11 @@ export function MembershipPanel({ ownedLeagues, memberLeagues }: MembershipPanel
</Text>
</Card>
) : (
<div style={{ display: 'flex', flexDirection: 'column', gap: '0.75rem' }}>
<Stack gap={3}>
{ownedLeagues.map((league) => (
<LeagueListItem key={league.leagueId} league={league} isAdmin />
))}
</div>
</Stack>
)}
</ProfileSection>
@@ -53,13 +54,13 @@ export function MembershipPanel({ ownedLeagues, memberLeagues }: MembershipPanel
</Text>
</Card>
) : (
<div style={{ display: 'flex', flexDirection: 'column', gap: '0.75rem' }}>
<Stack gap={3}>
{memberLeagues.map((league) => (
<LeagueListItem key={league.leagueId} league={league} />
))}
</div>
</Stack>
)}
</ProfileSection>
</div>
</Stack>
);
}

View File

@@ -3,6 +3,8 @@
import { Panel } from '@/ui/Panel';
import { Select } from '@/ui/Select';
import { Toggle } from '@/ui/Toggle';
import { Box } from '@/ui/Box';
import { Stack } from '@/ui/Stack';
import { ProfileStatsGroup, ProfileStat } from '@/ui/ProfileHero';
import React from 'react';
@@ -22,7 +24,7 @@ export function PreferencesPanel({ preferences, isEditing, onUpdate }: Preferenc
if (isEditing) {
return (
<Panel title="Racing Preferences">
<div style={{ display: 'flex', flexDirection: 'column', gap: '1.5rem' }}>
<Stack gap={6}>
<Select
label="Favorite Car Class"
value={preferences.favoriteCarClass}
@@ -45,19 +47,21 @@ export function PreferencesPanel({ preferences, isEditing, onUpdate }: Preferenc
]}
/>
<div style={{ display: 'flex', flexDirection: 'column', gap: '0.75rem', paddingTop: '0.5rem' }}>
<Toggle
label="Public Profile"
checked={preferences.showProfile}
onChange={(checked) => onUpdate?.({ showProfile: checked })}
/>
<Toggle
label="Show Race History"
checked={preferences.showHistory}
onChange={(checked) => onUpdate?.({ showHistory: checked })}
/>
</div>
</div>
<Box paddingTop={2}>
<Stack gap={3}>
<Toggle
label="Public Profile"
checked={preferences.showProfile}
onChange={(checked) => onUpdate?.({ showProfile: checked })}
/>
<Toggle
label="Show Race History"
checked={preferences.showHistory}
onChange={(checked) => onUpdate?.({ showHistory: checked })}
/>
</Stack>
</Box>
</Stack>
</Panel>
);
}

View File

@@ -5,6 +5,9 @@ import { Panel } from '@/ui/Panel';
import { Input } from '@/ui/Input';
import { Text } from '@/ui/Text';
import { TextArea } from '@/ui/TextArea';
import { Box } from '@/ui/Box';
import { Group } from '@/ui/Group';
import { Stack } from '@/ui/Stack';
import { ProfileStat } from '@/ui/ProfileHero';
import React from 'react';
@@ -22,7 +25,7 @@ export function ProfileDetailsPanel({ driver, isEditing, onUpdate }: ProfileDeta
if (isEditing) {
return (
<Panel title="Profile Details">
<div style={{ display: 'flex', flexDirection: 'column', gap: '1.5rem' }}>
<Stack gap={6}>
<Input
label="Nationality (ISO Code)"
value={driver.country}
@@ -35,31 +38,31 @@ export function ProfileDetailsPanel({ driver, isEditing, onUpdate }: ProfileDeta
onChange={(e) => onUpdate?.({ bio: e.target.value })}
placeholder="Tell the community about your racing career..."
/>
</div>
</Stack>
</Panel>
);
}
return (
<Panel title="Profile Details">
<div style={{ display: 'flex', flexDirection: 'column', gap: '1.5rem' }}>
<div>
<Text size="xs" variant="low" weight="bold" uppercase block marginBottom={1}>Nationality</Text>
<div style={{ display: 'flex', alignItems: 'center', gap: '0.5rem' }}>
<Stack gap={6}>
<Stack gap={1}>
<Text size="xs" variant="low" weight="bold" uppercase block>Nationality</Text>
<Group gap={2}>
<Text size="xl">
{CountryFlagDisplay.fromCountryCode(driver.country).toString()}
</Text>
<Text variant="med">{driver.country}</Text>
</div>
</div>
</Group>
</Stack>
<div>
<Text size="xs" variant="low" weight="bold" uppercase block marginBottom={1}>Bio</Text>
<Stack gap={1}>
<Text size="xs" variant="low" weight="bold" uppercase block>Bio</Text>
<Text variant="med" leading="relaxed">
{driver.bio || 'No bio provided.'}
</Text>
</div>
</div>
</Stack>
</Stack>
</Panel>
);
}

View File

@@ -7,6 +7,9 @@ import { Heading } from '@/ui/Heading';
import { Image } from '@/ui/Image';
import { ProfileHero, ProfileAvatar, ProfileStatsGroup, ProfileStat } from '@/ui/ProfileHero';
import { Text } from '@/ui/Text';
import { Box } from '@/ui/Box';
import { Group } from '@/ui/Group';
import { Stack } from '@/ui/Stack';
import { Calendar, Globe, Star, Trophy, UserPlus } from 'lucide-react';
import React from 'react';
@@ -37,7 +40,7 @@ export function ProfileHeader({
}: ProfileHeaderProps) {
return (
<ProfileHero variant="muted">
<div style={{ display: 'flex', alignItems: 'center', gap: '2rem', flexWrap: 'wrap' }}>
<Box display="flex" alignItems="center" gap={8} flexWrap="wrap">
<ProfileAvatar>
<Image
src={driver.avatarUrl || mediaConfig.avatars.defaultFallback}
@@ -48,27 +51,29 @@ export function ProfileHeader({
/>
</ProfileAvatar>
<div style={{ flex: 1, minWidth: '200px' }}>
<div style={{ display: 'flex', alignItems: 'center', gap: '0.75rem', marginBottom: '0.25rem' }}>
<Heading level={1}>{driver.name}</Heading>
<Text size="2xl" aria-label={`Country: ${driver.country}`}>
{CountryFlagDisplay.fromCountryCode(driver.country).toString()}
</Text>
</div>
<Stack flex={1} minWidth="200px" gap={1}>
<Box marginBottom={1}>
<Group gap={3}>
<Heading level={1}>{driver.name}</Heading>
<Text size="2xl" aria-label={`Country: ${driver.country}`}>
{CountryFlagDisplay.fromCountryCode(driver.country).toString()}
</Text>
</Group>
</Box>
<div style={{ display: 'flex', alignItems: 'center', gap: '1rem' }}>
<div style={{ display: 'flex', alignItems: 'center', gap: '0.375rem' }}>
<Group gap={4}>
<Group gap={1.5}>
<Globe size={14} color="var(--ui-color-text-low)" />
<Text size="xs" font="mono" variant="low">ID: {driver.iracingId}</Text>
</div>
<div style={{ display: 'flex', alignItems: 'center', gap: '0.375rem' }}>
</Group>
<Group gap={1.5}>
<Calendar size={14} color="var(--ui-color-text-low)" />
<Text size="xs" variant="low">
Joined {new Date(driver.joinedAt).toLocaleDateString('en-US', { month: 'short', year: 'numeric' })}
</Text>
</div>
</div>
</div>
</Group>
</Group>
</Stack>
<ProfileStatsGroup>
{stats && (
@@ -80,7 +85,7 @@ export function ProfileHeader({
</ProfileStatsGroup>
{!isOwnProfile && onAddFriend && (
<div style={{ marginLeft: 'auto' }}>
<Box marginLeft="auto">
<Button
variant={friendRequestSent ? 'secondary' : 'primary'}
onClick={onAddFriend}
@@ -90,9 +95,9 @@ export function ProfileHeader({
>
{friendRequestSent ? 'Request Sent' : 'Add Friend'}
</Button>
</div>
</Box>
)}
</div>
</Box>
</ProfileHero>
);
}

View File

@@ -1,6 +1,7 @@
'use client';
import { SectionHeader } from '@/ui/SectionHeader';
import { Box } from '@/ui/Box';
import React from 'react';
interface ProfileSectionProps {
@@ -12,14 +13,14 @@ interface ProfileSectionProps {
export function ProfileSection({ title, description, action, children }: ProfileSectionProps) {
return (
<section style={{ marginBottom: '2rem' }}>
<Box as="section" marginBottom={8}>
<SectionHeader
title={title}
description={description}
actions={action}
variant="minimal"
/>
<div>{children}</div>
</section>
<Box>{children}</Box>
</Box>
);
}

View File

@@ -9,6 +9,10 @@ import { Icon } from '@/ui/Icon';
import { Image } from '@/ui/Image';
import { Link } from '@/ui/Link';
import { Text } from '@/ui/Text';
import { Box } from '@/ui/Box';
import { Group } from '@/ui/Group';
import { Stack } from '@/ui/Stack';
import { Surface } from '@/ui/Surface';
import { UserDropdown, UserDropdownHeader, UserDropdownItem, UserDropdownFooter } from '@/ui/UserDropdown';
import { AnimatePresence, motion } from 'framer-motion';
import {
@@ -143,7 +147,7 @@ export function UserPill() {
// Handle unauthenticated users
if (!session) {
return (
<div style={{ display: 'flex', alignItems: 'center', gap: '0.5rem' }}>
<Group gap={2}>
<Link
href={routes.auth.login}
variant="secondary"
@@ -156,7 +160,7 @@ export function UserPill() {
>
Get Started
</Link>
</div>
</Group>
);
}
@@ -185,7 +189,7 @@ export function UserPill() {
} as Record<string, 'primary' | 'success' | 'warning' | 'critical'>)[demoRole || 'driver'] : 'low';
return (
<div style={{ position: 'relative', display: 'inline-flex', alignItems: 'center' }} data-user-pill>
<Box position="relative" display="inline-flex" alignItems="center" data-user-pill>
<button
type="button"
onClick={() => setIsMenuOpen((open) => !open)}
@@ -201,26 +205,26 @@ export function UserPill() {
}}
>
{/* Avatar */}
<div style={{ position: 'relative' }}>
<Box position="relative">
{avatarUrl ? (
<div style={{ width: '2rem', height: '2rem', borderRadius: '9999px', overflow: 'hidden', border: '1px solid var(--ui-color-border-default)' }}>
<Surface width="2rem" height="2rem" rounded="full" overflow="hidden" border>
<Image
src={avatarUrl}
alt={displayName}
objectFit="cover"
/>
</div>
</Surface>
) : (
<div style={{ width: '2rem', height: '2rem', borderRadius: '9999px', backgroundColor: 'var(--ui-color-intent-primary)', display: 'flex', alignItems: 'center', justifyContent: 'center' }}>
<Surface width="2rem" height="2rem" rounded="full" variant="gradient-blue" display="flex" alignItems="center" justifyContent="center">
<Text size="xs" weight="bold" variant="high">
{displayName[0]?.toUpperCase() || 'U'}
</Text>
</div>
</Surface>
)}
</div>
</Box>
{/* Info */}
<div style={{ display: 'flex', flexDirection: 'column', alignItems: 'start' }}>
<Stack align="start" gap={0}>
<Text size="xs" weight="semibold" variant="high" truncate style={{ maxWidth: '100px' }}>
{displayName}
</Text>
@@ -229,7 +233,7 @@ export function UserPill() {
{roleLabel}
</Text>
)}
</div>
</Stack>
{/* Chevron */}
<Icon icon={ChevronDown} size={3.5} intent="low" />
@@ -237,32 +241,32 @@ export function UserPill() {
<UserDropdown isOpen={isMenuOpen}>
<UserDropdownHeader variant={isDemo ? 'demo' : 'default'}>
<div style={{ display: 'flex', alignItems: 'center', gap: '0.75rem' }}>
<Group gap={3}>
{avatarUrl ? (
<div style={{ width: '2.5rem', height: '2.5rem', borderRadius: '0.5rem', overflow: 'hidden', border: '1px solid var(--ui-color-border-default)' }}>
<Surface width="2.5rem" height="2.5rem" rounded="md" overflow="hidden" border>
<Image
src={avatarUrl}
alt={displayName}
objectFit="cover"
/>
</div>
</Surface>
) : (
<div style={{ width: '2.5rem', height: '2.5rem', borderRadius: '0.5rem', backgroundColor: 'var(--ui-color-intent-primary)', display: 'flex', alignItems: 'center', justifyContent: 'center' }}>
<Surface width="2.5rem" height="2.5rem" rounded="md" variant="gradient-blue" display="flex" alignItems="center" justifyContent="center">
<Text size="xs" weight="bold" variant="high">
{displayName[0]?.toUpperCase() || 'U'}
</Text>
</div>
</Surface>
)}
<div>
<Stack gap={0}>
<Text size="sm" weight="semibold" variant="high" block>{displayName}</Text>
{roleLabel && (
<Text size="xs" variant="low" block>{roleLabel}</Text>
)}
</div>
</div>
</Stack>
</Group>
</UserDropdownHeader>
<div style={{ padding: '0.25rem 0' }}>
<Box paddingY={1}>
{hasAdminAccess && (
<UserDropdownItem href="/admin" icon={Shield} label="Admin Area" intent="primary" onClick={() => setIsMenuOpen(false)} />
)}
@@ -280,7 +284,7 @@ export function UserPill() {
<UserDropdownItem href={routes.protected.profileLiveries} icon={Paintbrush} label="Liveries" onClick={() => setIsMenuOpen(false)} />
<UserDropdownItem href={routes.protected.profileSponsorshipRequests} icon={Handshake} label="Sponsorship Requests" intent="success" onClick={() => setIsMenuOpen(false)} />
<UserDropdownItem href={routes.protected.profileSettings} icon={Settings} label="Settings" onClick={() => setIsMenuOpen(false)} />
</div>
</Box>
<UserDropdownFooter>
<UserDropdownItem
@@ -291,6 +295,6 @@ export function UserPill() {
/>
</UserDropdownFooter>
</UserDropdown>
</div>
</Box>
);
}

View File

@@ -4,6 +4,8 @@ import { Badge } from '@/ui/Badge';
import { Icon } from '@/ui/Icon';
import { Link } from '@/ui/Link';
import { Text } from '@/ui/Text';
import { Box } from '@/ui/Box';
import { Group } from '@/ui/Group';
import { RaceCard, RaceTimeColumn, RaceInfo } from '@/ui/RaceCard';
import { Car, Trophy, Zap, ArrowRight, Clock, PlayCircle, CheckCircle2, XCircle, HelpCircle } from 'lucide-react';
import React from 'react';
@@ -61,7 +63,7 @@ export function RaceListItem({
isLive={isLive}
/>
<div style={{ width: '1px', height: '2.5rem', backgroundColor: 'var(--ui-color-border-muted)', opacity: 0.2 }} />
<Box width="1px" height="2.5rem" bg="var(--ui-color-border-muted)" opacity={0.2} />
<RaceInfo
title={track}
@@ -73,12 +75,12 @@ export function RaceListItem({
</Badge>
}
meta={
<div style={{ display: 'flex', alignItems: 'center', gap: '1rem' }}>
<Group gap={4}>
{strengthOfField && (
<div style={{ display: 'flex', alignItems: 'center', gap: '0.25rem' }}>
<Group gap={1}>
<Icon icon={Zap} size={3.5} intent="warning" />
<Text size="sm" variant="low">SOF {strengthOfField}</Text>
</div>
</Group>
)}
{leagueName && leagueHref && (
<Link
@@ -86,14 +88,14 @@ export function RaceListItem({
onClick={(e) => e.stopPropagation()}
variant="primary"
>
<div style={{ display: 'flex', alignItems: 'center', gap: '0.25rem' }}>
<Group gap={1}>
<Icon icon={Trophy} size={3.5} intent="primary" />
<Text size="sm" variant="primary">{leagueName}</Text>
<Icon icon={ArrowRight} size={3} intent="primary" />
</div>
</Group>
</Link>
)}
</div>
</Group>
}
/>
</RaceCard>

View File

@@ -5,6 +5,10 @@ import { Image } from '@/ui/Image';
import { ResultRow, PositionBadge, ResultPoints } from '@/ui/ResultRow';
import { Text } from '@/ui/Text';
import { Badge } from '@/ui/Badge';
import { Box } from '@/ui/Box';
import { Group } from '@/ui/Group';
import { Stack } from '@/ui/Stack';
import { Surface } from '@/ui/Surface';
import React from 'react';
interface ResultEntry {
@@ -35,37 +39,56 @@ export function RaceResultRow({ result, points }: RaceResultRowProps) {
<PositionBadge position={position} />
{/* Avatar */}
<div style={{ position: 'relative', flexShrink: 0 }}>
<div style={{ width: '2.5rem', height: '2.5rem', borderRadius: '9999px', overflow: 'hidden', border: isCurrentUser ? '2px solid var(--ui-color-intent-primary)' : '1px solid var(--ui-color-border-default)' }}>
<Box position="relative" flexShrink={0}>
<Surface
width="2.5rem"
height="2.5rem"
rounded="full"
overflow="hidden"
border={isCurrentUser ? '2px solid var(--ui-color-intent-primary)' : true}
>
<Image src={driverAvatar} alt={driverName} width={40} height={40} objectFit="cover" />
</div>
<div style={{ position: 'absolute', bottom: '-0.125rem', right: '-0.125rem', width: '1.25rem', height: '1.25rem', borderRadius: '9999px', backgroundColor: 'var(--ui-color-bg-base)', display: 'flex', alignItems: 'center', justifyContent: 'center', fontSize: '0.625rem' }}>
{CountryFlagDisplay.fromCountryCode(country).toString()}
</div>
</div>
</Surface>
<Surface
position="absolute"
bottom="-0.125rem"
right="-0.125rem"
width="1.25rem"
height="1.25rem"
rounded="full"
variant="dark"
display="flex"
alignItems="center"
justifyContent="center"
>
<Text size="xs" style={{ fontSize: '0.625rem' }}>
{CountryFlagDisplay.fromCountryCode(country).toString()}
</Text>
</Surface>
</Box>
{/* Driver Info */}
<div style={{ flex: 1, minWidth: 0 }}>
<div style={{ display: 'flex', alignItems: 'center', gap: '0.5rem' }}>
<Stack flex={1} minWidth="0" gap={1}>
<Group gap={2}>
<Text weight="semibold" size="sm" variant={isCurrentUser ? 'primary' : 'high'} truncate>{driverName}</Text>
{isCurrentUser && (
<Badge variant="primary" size="sm">YOU</Badge>
)}
</div>
<div style={{ display: 'flex', alignItems: 'center', gap: '0.5rem', marginTop: '0.25rem' }}>
</Group>
<Group gap={2}>
<Text size="xs" variant="low">{car}</Text>
<Text size="xs" variant="low"></Text>
<Text size="xs" variant="low">Laps: {laps}</Text>
<Text size="xs" variant="low"></Text>
<Text size="xs" variant="low">Incidents: {incidents}</Text>
</div>
</div>
</Group>
</Stack>
{/* Times */}
<div style={{ textAlign: 'right', minWidth: '100px' }}>
<Stack textAlign="right" minWidth="100px" gap={1}>
<Text size="sm" font="mono" variant="high" block>{time}</Text>
<Text size="xs" variant="success" block style={{ marginTop: '0.25rem' }}>FL: {fastestLap}</Text>
</div>
<Text size="xs" variant="success" block>FL: {fastestLap}</Text>
</Stack>
{/* Points */}
<ResultPoints points={points} />

View File

@@ -5,6 +5,10 @@ import { Icon } from '@/ui/Icon';
import { Link } from '@/ui/Link';
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from '@/ui/Table';
import { Text } from '@/ui/Text';
import { Box } from '@/ui/Box';
import { Group } from '@/ui/Group';
import { Stack } from '@/ui/Stack';
import { Surface } from '@/ui/Surface';
import { PositionBadge } from '@/ui/ResultRow';
import { AlertTriangle, ExternalLink } from 'lucide-react';
import React, { ReactNode } from 'react';
@@ -111,14 +115,14 @@ export function RaceResultsTable({
if (results.length === 0) {
return (
<div style={{ textAlign: 'center', padding: '2rem 0' }}>
<Box textAlign="center" paddingY={8}>
<Text variant="low">No results available</Text>
</div>
</Box>
);
}
return (
<div style={{ overflowX: 'auto' }}>
<Box overflowX="auto">
<Table>
<TableHead>
<TableRow>
@@ -150,20 +154,20 @@ export function RaceResultsTable({
<PositionBadge position={result.position} />
</TableCell>
<TableCell>
<div style={{ display: 'flex', alignItems: 'center', gap: '0.75rem' }}>
<Group gap={3}>
{driver ? (
<React.Fragment>
<div style={{
width: '2rem',
height: '2rem',
borderRadius: '9999px',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
flexShrink: 0,
backgroundColor: isCurrentUser ? 'rgba(25, 140, 255, 0.2)' : 'var(--ui-color-bg-surface-muted)',
border: isCurrentUser ? '2px solid var(--ui-color-intent-primary)' : '1px solid var(--ui-color-border-default)'
}}>
<Surface
variant={isCurrentUser ? 'gradient-blue' : 'muted'}
rounded="full"
width="2rem"
height="2rem"
display="flex"
alignItems="center"
justifyContent="center"
flexShrink={0}
border={isCurrentUser ? '2px solid var(--ui-color-intent-primary)' : true}
>
<Text
size="sm"
weight="bold"
@@ -171,24 +175,24 @@ export function RaceResultsTable({
>
{driver.name.charAt(0)}
</Text>
</div>
</Surface>
<Link
href={`/drivers/${driver.id}`}
variant={isCurrentUser ? 'primary' : 'inherit'}
>
<div style={{ display: 'flex', alignItems: 'center', gap: '0.375rem' }}>
<Group gap={1.5}>
<Text weight={isCurrentUser ? 'semibold' : 'normal'}>{driver.name}</Text>
{isCurrentUser && (
<Badge variant="primary" size="sm">You</Badge>
)}
<Icon icon={ExternalLink} size={3} intent="low" />
</div>
</Group>
</Link>
</React.Fragment>
) : (
<Text variant="high">{getDriverName(result.driverId)}</Text>
)}
</div>
</Group>
</TableCell>
<TableCell>
<Text variant={isFastestLap ? 'success' : 'high'} weight={isFastestLap ? 'medium' : 'normal'}>
@@ -212,20 +216,20 @@ export function RaceResultsTable({
</TableCell>
<TableCell>
{driverPenalties.length > 0 ? (
<div style={{ display: 'flex', flexDirection: 'column', gap: '0.25rem' }}>
<Stack gap={1}>
{driverPenalties.map((penalty, idx) => (
<div key={idx} style={{ display: 'flex', alignItems: 'center', gap: '0.375rem' }}>
<Group key={idx} gap={1.5}>
<Icon icon={AlertTriangle} size={3} intent="critical" />
<Text size="xs" variant="critical">{getPenaltyDescription(penalty)}</Text>
</div>
</Group>
))}
</div>
</Stack>
) : (
<Text variant="low"></Text>
)}
</TableCell>
{isAdmin && (
<TableCell style={{ textAlign: 'right' }}>
<TableCell textAlign="right">
{driver && penaltyButtonRenderer && penaltyButtonRenderer(driver)}
</TableCell>
)}
@@ -234,6 +238,6 @@ export function RaceResultsTable({
})}
</TableBody>
</Table>
</div>
</Box>
);
}

View File

@@ -7,6 +7,8 @@ import { Panel } from '@/ui/Panel';
import { Icon } from '@/ui/Icon';
import { SidebarActionLink } from '@/ui/SidebarActionLink';
import { Text } from '@/ui/Text';
import { Stack } from '@/ui/Stack';
import { Box } from '@/ui/Box';
import { Clock, Trophy, Users } from 'lucide-react';
import React from 'react';
@@ -18,18 +20,18 @@ interface RaceSidebarProps {
export function RaceSidebar({ upcomingRaces, recentResults, onRaceClick }: RaceSidebarProps) {
return (
<div style={{ display: 'flex', flexDirection: 'column', gap: '1.5rem' }}>
<Stack gap={6}>
{/* Upcoming This Week */}
<Panel
title="Next Up"
description="This week"
>
{upcomingRaces.length === 0 ? (
<div style={{ padding: '1rem 0', textAlign: 'center' }}>
<Box paddingY={4} textAlign="center">
<Text size="sm" variant="low">No races scheduled this week</Text>
</div>
</Box>
) : (
<div style={{ display: 'flex', flexDirection: 'column', gap: '0.75rem' }}>
<Stack gap={3}>
{upcomingRaces.map((race) => (
<SidebarRaceItem
key={race.id}
@@ -41,7 +43,7 @@ export function RaceSidebar({ upcomingRaces, recentResults, onRaceClick }: RaceS
onClick={() => onRaceClick(race.id)}
/>
))}
</div>
</Stack>
)}
</Panel>
@@ -50,11 +52,11 @@ export function RaceSidebar({ upcomingRaces, recentResults, onRaceClick }: RaceS
title="Recent Results"
>
{recentResults.length === 0 ? (
<div style={{ padding: '1rem 0', textAlign: 'center' }}>
<Box paddingY={4} textAlign="center">
<Text size="sm" variant="low">No completed races yet</Text>
</div>
</Box>
) : (
<div style={{ display: 'flex', flexDirection: 'column', gap: '0.75rem' }}>
<Stack gap={3}>
{recentResults.map((race) => (
<SidebarRaceItem
key={race.id}
@@ -66,13 +68,13 @@ export function RaceSidebar({ upcomingRaces, recentResults, onRaceClick }: RaceS
onClick={() => onRaceClick(race.id)}
/>
))}
</div>
</Stack>
)}
</Panel>
{/* Quick Actions */}
<Panel title="Quick Actions">
<div style={{ display: 'flex', flexDirection: 'column', gap: '0.5rem' }}>
<Stack gap={2}>
<SidebarActionLink
href={routes.public.leagues}
icon={Users}
@@ -83,8 +85,8 @@ export function RaceSidebar({ upcomingRaces, recentResults, onRaceClick }: RaceS
icon={Trophy}
label="View Leaderboards"
/>
</div>
</Stack>
</Panel>
</div>
</Stack>
);
}

View File

@@ -1,4 +1,5 @@
import { StatGrid } from '@/ui/StatGrid';
import { Box } from '@/ui/Box';
import { CalendarDays, Clock, Trophy, Zap } from 'lucide-react';
import React from 'react';
@@ -20,12 +21,12 @@ export function RaceStats({ stats }: RaceStatsProps) {
];
return (
<div style={{ marginTop: '1.5rem' }}>
<Box marginTop={6}>
<StatGrid
stats={mappedStats}
columns={{ base: 2, md: 4 }}
variant="box"
/>
</div>
</Box>
);
}

View File

@@ -4,6 +4,12 @@
import { Check, ChevronDown, Globe, Search } from 'lucide-react';
import { useEffect, useRef, useState } from 'react';
import { CountryFlag } from '@/ui/CountryFlag';
import { Box } from '@/ui/Box';
import { Group } from '@/ui/Group';
import { Stack } from '@/ui/Stack';
import { Text } from '@/ui/Text';
import { Surface } from '@/ui/Surface';
import { Icon } from '@/ui/Icon';
export interface Country {
code: string;
@@ -107,38 +113,62 @@ export function CountrySelect({
};
return (
<div ref={containerRef} className="relative">
<Box ref={containerRef} position="relative">
{/* Trigger Button */}
<button
type="button"
onClick={() => !disabled && setIsOpen(!isOpen)}
disabled={disabled}
className={`flex items-center justify-between w-full rounded-md border-0 px-4 py-3 bg-iron-gray text-white shadow-sm ring-1 ring-inset transition-all duration-150 sm:text-sm ${
error
? 'ring-warning-amber focus:ring-warning-amber'
: 'ring-charcoal-outline focus:ring-primary-blue'
} ${disabled ? 'opacity-50 cursor-not-allowed' : 'cursor-pointer hover:ring-gray-500'}`}
style={{
display: 'flex',
alignItems: 'center',
justifyContent: 'space-between',
width: '100%',
borderRadius: 'var(--ui-radius-md)',
border: 'none',
padding: '0.75rem 1rem',
backgroundColor: 'var(--ui-color-bg-surface-muted)',
color: 'white',
boxShadow: 'var(--ui-shadow-sm)',
transition: 'all 150ms',
cursor: disabled ? 'not-allowed' : 'pointer',
opacity: disabled ? 0.5 : 1,
outline: 'none',
ring: '1px inset',
borderColor: error ? 'var(--ui-color-intent-critical)' : 'var(--ui-color-border-default)'
} as any}
className={error ? 'ring-warning-amber' : 'ring-charcoal-outline focus:ring-primary-blue'}
>
<div className="flex items-center gap-3">
<Group gap={3}>
<Globe className="w-4 h-4 text-gray-500" />
{selectedCountry ? (
<span className="flex items-center gap-2">
<CountryFlag countryCode={selectedCountry.code} size="md" showTooltip={false} />
<span>{selectedCountry.name}</span>
</span>
<Group gap={2}>
<CountryFlag countryCode={selectedCountry.code} size="md" />
<Text as="span">{selectedCountry.name}</Text>
</Group>
) : (
<span className="text-gray-500">{placeholder}</span>
<Text variant="low">{placeholder}</Text>
)}
</div>
</Group>
<ChevronDown className={`w-4 h-4 text-gray-500 transition-transform ${isOpen ? 'rotate-180' : ''}`} />
</button>
{/* Dropdown */}
{isOpen && (
<div className="absolute z-50 mt-2 w-full rounded-lg bg-iron-gray border border-charcoal-outline shadow-xl max-h-80 overflow-hidden">
<Surface
position="absolute"
zIndex={50}
marginTop={2}
width="100%"
rounded="lg"
variant="dark"
border
shadow="xl"
overflow="hidden"
>
{/* Search Input */}
<div className="p-2 border-b border-charcoal-outline">
<div className="relative">
<Box padding={2} borderBottom="1px solid var(--ui-color-border-muted)">
<Box position="relative">
<Search className="absolute left-3 top-1/2 -translate-y-1/2 w-4 h-4 text-gray-500" />
<input
ref={inputRef}
@@ -146,47 +176,67 @@ export function CountrySelect({
value={search}
onChange={(e) => setSearch(e.target.value)}
placeholder="Search countries..."
className="w-full rounded-md border-0 px-4 py-2 pl-9 bg-deep-graphite text-white text-sm placeholder:text-gray-500 focus:outline-none focus:ring-1 focus:ring-primary-blue"
style={{
width: '100%',
borderRadius: 'var(--ui-radius-md)',
border: 'none',
padding: '0.5rem 1rem 0.5rem 2.25rem',
backgroundColor: 'var(--ui-color-bg-base)',
color: 'white',
fontSize: '0.875rem',
outline: 'none'
}}
/>
</div>
</div>
</Box>
</Box>
{/* Country List */}
<div className="overflow-y-auto max-h-60">
<Box overflowY="auto" maxHeight="15rem">
{filteredCountries.length > 0 ? (
filteredCountries.map((country) => (
<button
key={country.code}
type="button"
onClick={() => handleSelect(country.code)}
className={`flex items-center justify-between w-full px-4 py-2.5 text-left text-sm transition-colors ${
value === country.code
? 'bg-primary-blue/20 text-white'
: 'text-gray-300 hover:bg-deep-graphite'
}`}
style={{
display: 'flex',
alignItems: 'center',
justifyContent: 'space-between',
width: '100%',
padding: '0.625rem 1rem',
textAlign: 'left',
fontSize: '0.875rem',
transition: 'colors 150ms',
border: 'none',
backgroundColor: value === country.code ? 'rgba(25, 140, 255, 0.2)' : 'transparent',
color: value === country.code ? 'white' : 'var(--ui-color-text-med)',
cursor: 'pointer'
}}
>
<span className="flex items-center gap-3">
<CountryFlag countryCode={country.code} size="md" showTooltip={false} />
<span>{country.name}</span>
</span>
<Group gap={3}>
<CountryFlag countryCode={country.code} size="md" />
<Text as="span">{country.name}</Text>
</Group>
{value === country.code && (
<Check className="w-4 h-4 text-primary-blue" />
)}
</button>
))
) : (
<div className="px-4 py-6 text-center text-gray-500 text-sm">
No countries found
</div>
<Box paddingX={4} paddingY={6} textAlign="center">
<Text size="sm" variant="low">No countries found</Text>
</Box>
)}
</div>
</div>
</Box>
</Surface>
)}
{/* Error Message */}
{error && errorMessage && (
<p className="mt-2 text-sm text-warning-amber">{errorMessage}</p>
<Box marginTop={2}>
<Text size="sm" variant="critical">{errorMessage}</Text>
</Box>
)}
</div>
</Box>
);
}

View File

@@ -1,8 +1,10 @@
/* eslint-disable gridpilot-rules/no-raw-html-in-app */
import React, { useCallback, useEffect, useRef, useState } from 'react';
import { Box } from '@/ui/Box';
import { Group } from '@/ui/Group';
import { Stack } from '@/ui/Stack';
import { Text } from '@/ui/Text';
import { Surface } from '@/ui/Surface';
interface RangeFieldProps {
label: string;
value: number;
@@ -20,7 +22,7 @@ interface RangeFieldProps {
/** Compact mode - single line */
compact?: boolean;
}
export function RangeField({
label,
value,
@@ -40,23 +42,23 @@ export function RangeField({
const [isDragging, setIsDragging] = useState(false);
const sliderRef = useRef<HTMLDivElement>(null);
const inputRef = useRef<HTMLInputElement>(null);
// Sync local value with prop when not dragging
useEffect(() => {
if (!isDragging) {
setLocalValue(value);
}
}, [value, isDragging]);
const clampedValue = Number.isFinite(localValue)
? Math.min(Math.max(localValue, min), max)
: min;
const rangePercent = ((clampedValue - min) / Math.max(max - min, 1)) * 100;
const effectiveRangeHint =
rangeHint ?? (min === 0 ? `Up to ${max} ${unitLabel}` : `${min}${max} ${unitLabel}`);
const calculateValueFromPosition = useCallback(
(clientX: number) => {
if (!sliderRef.current) return clampedValue;
@@ -68,7 +70,7 @@ export function RangeField({
},
[min, max, step, clampedValue]
);
const handlePointerDown = useCallback(
(e: React.PointerEvent) => {
if (disabled) return;
@@ -81,7 +83,7 @@ export function RangeField({
},
[disabled, calculateValueFromPosition, onChange]
);
const handlePointerMove = useCallback(
(e: React.PointerEvent) => {
if (!isDragging || disabled) return;
@@ -91,11 +93,11 @@ export function RangeField({
},
[isDragging, disabled, calculateValueFromPosition, onChange]
);
const handlePointerUp = useCallback(() => {
setIsDragging(false);
}, []);
const handleInputChange = (e: React.ChangeEvent<HTMLInputElement>) => {
const raw = e.target.value;
if (raw === '') {
@@ -109,120 +111,165 @@ export function RangeField({
onChange(clamped);
}
};
const handleInputBlur = () => {
// Ensure value is synced on blur
onChange(clampedValue);
};
// Quick preset buttons for common values
const quickPresets = [
Math.round(min + (max - min) * 0.25),
Math.round(min + (max - min) * 0.5),
Math.round(min + (max - min) * 0.75),
].filter((v, i, arr) => arr.indexOf(v) === i && v !== clampedValue);
if (compact) {
return (
<div className="space-y-1.5">
<div className="flex items-center justify-between gap-3">
<label className="text-xs font-medium text-gray-400 shrink-0">{label}</label>
<div className="flex items-center gap-2 flex-1 max-w-[200px]">
<Stack gap={1.5}>
<Group justify="between" gap={3}>
<Text as="label" size="xs" weight="medium" variant="low">{label}</Text>
<Box display="flex" alignItems="center" gap={2} flex={1} maxWidth="200px">
<div
ref={sliderRef}
className={`relative flex-1 h-6 cursor-pointer touch-none ${disabled ? 'opacity-50 cursor-not-allowed' : ''}`}
style={{
position: 'relative',
flex: 1,
height: '1.5rem',
cursor: disabled ? 'not-allowed' : 'pointer',
touchAction: 'none',
opacity: disabled ? 0.5 : 1
}}
onPointerDown={handlePointerDown}
onPointerMove={handlePointerMove}
onPointerUp={handlePointerUp}
>
{/* Track background */}
<div className="absolute top-1/2 -translate-y-1/2 left-0 right-0 h-1.5 rounded-full bg-charcoal-outline" />
<Box position="absolute" top="50%" left={0} right={0} height="0.375rem" rounded="full" bg="var(--ui-color-border-muted)" style={{ transform: 'translateY(-50%)' }} />
{/* Track fill */}
<div
className="absolute top-1/2 -translate-y-1/2 left-0 h-1.5 rounded-full bg-primary-blue transition-all duration-75"
style={{ width: `${rangePercent}%` }}
<Box
position="absolute"
top="50%"
left={0}
height="0.375rem"
rounded="full"
bg="var(--ui-color-intent-primary)"
style={{ width: `${rangePercent}%`, transform: 'translateY(-50%)', transition: 'width 75ms' }}
/>
{/* Thumb */}
<div
className={`
absolute top-1/2 -translate-y-1/2 -translate-x-1/2 w-4 h-4 rounded-full
bg-white border-2 border-primary-blue shadow-md
transition-transform duration-75
${isDragging ? 'scale-125 shadow-[0_0_12px_rgba(25,140,255,0.5)]' : ''}
`}
style={{ left: `${rangePercent}%` }}
<Box
position="absolute"
top="50%"
width="1rem"
height="1rem"
rounded="full"
bg="white"
border="2px solid var(--ui-color-intent-primary)"
shadow="md"
style={{
left: `${rangePercent}%`,
transform: `translate(-50%, -50%) ${isDragging ? 'scale(1.25)' : ''}`,
transition: 'transform 75ms, left 75ms',
boxShadow: isDragging ? '0 0 12px rgba(25, 140, 255, 0.5)' : undefined
}}
/>
</div>
<div className="flex items-center gap-1 shrink-0">
<span className="text-sm font-semibold text-white w-8 text-right">{clampedValue}</span>
<span className="text-[10px] text-gray-500">{unitLabel}</span>
</div>
</div>
</div>
{error && <p className="text-[10px] text-warning-amber">{error}</p>}
</div>
<Box flexShrink={0}>
<Group gap={1}>
<Text size="sm" weight="semibold" variant="high" textAlign="right" width="2rem">{clampedValue}</Text>
<Text size="xs" variant="low" style={{ fontSize: '10px' }}>{unitLabel}</Text>
</Group>
</Box>
</Box>
</Group>
{error && <Text size="xs" variant="critical" style={{ fontSize: '10px' }}>{error}</Text>}
</Stack>
);
}
return (
<div className="space-y-3">
<div className="flex items-baseline justify-between gap-2">
<label className="block text-sm font-medium text-gray-300">{label}</label>
<span className="text-[10px] text-gray-500">{effectiveRangeHint}</span>
</div>
<Stack gap={3}>
<Group justify="between" align="baseline" gap={2}>
<Text as="label" size="sm" weight="medium" variant="med">{label}</Text>
<Text size="xs" variant="low" style={{ fontSize: '10px' }}>{effectiveRangeHint}</Text>
</Group>
{showLargeValue && (
<div className="flex items-baseline gap-1">
<span className="text-3xl font-bold text-white tabular-nums">{clampedValue}</span>
<span className="text-sm text-gray-400">{unitLabel}</span>
</div>
<Group align="baseline" gap={1}>
<Text size="3xl" weight="bold" variant="high" font="mono">{clampedValue}</Text>
<Text size="sm" variant="low">{unitLabel}</Text>
</Group>
)}
{/* Custom slider */}
<div
ref={sliderRef}
className={`relative h-8 cursor-pointer touch-none select-none ${disabled ? 'opacity-50 cursor-not-allowed' : ''}`}
style={{
position: 'relative',
height: '2rem',
cursor: disabled ? 'not-allowed' : 'pointer',
touchAction: 'none',
userSelect: 'none',
opacity: disabled ? 0.5 : 1
}}
onPointerDown={handlePointerDown}
onPointerMove={handlePointerMove}
onPointerUp={handlePointerUp}
>
{/* Track background */}
<div className="absolute top-1/2 -translate-y-1/2 left-0 right-0 h-2 rounded-full bg-charcoal-outline/80" />
<Box position="absolute" top="50%" left={0} right={0} height="0.5rem" rounded="full" bg="var(--ui-color-border-muted)" style={{ transform: 'translateY(-50%)', opacity: 0.8 }} />
{/* Track fill with gradient */}
<div
className="absolute top-1/2 -translate-y-1/2 left-0 h-2 rounded-full bg-gradient-to-r from-primary-blue to-neon-aqua transition-all duration-75"
style={{ width: `${rangePercent}%` }}
<Box
position="absolute"
top="50%"
left={0}
height="0.5rem"
rounded="full"
style={{
width: `${rangePercent}%`,
transform: 'translateY(-50%)',
transition: 'width 75ms',
background: 'linear-gradient(to right, var(--ui-color-intent-primary), #00f2ff)'
}}
/>
{/* Tick marks */}
<div className="absolute top-1/2 -translate-y-1/2 left-0 right-0 flex justify-between px-1">
<Box position="absolute" top="50%" left={0} right={0} display="flex" justifyContent="between" paddingX={1} style={{ transform: 'translateY(-50%)' }}>
{[0, 25, 50, 75, 100].map((tick) => (
<div
<Box
key={tick}
className={`w-0.5 h-1 rounded-full transition-colors ${
rangePercent >= tick ? 'bg-white/40' : 'bg-charcoal-outline'
}`}
width="0.125rem"
height="0.25rem"
rounded="full"
bg={rangePercent >= tick ? 'rgba(255, 255, 255, 0.4)' : 'var(--ui-color-border-muted)'}
style={{ transition: 'background-color 75ms' }}
/>
))}
</div>
</Box>
{/* Thumb */}
<div
className={`
absolute top-1/2 -translate-y-1/2 -translate-x-1/2
w-5 h-5 rounded-full bg-white border-2 border-primary-blue
shadow-[0_2px_8px_rgba(0,0,0,0.3)]
transition-all duration-75
${isDragging ? 'scale-125 shadow-[0_0_16px_rgba(25,140,255,0.6)]' : 'hover:scale-110'}
`}
style={{ left: `${rangePercent}%` }}
<Box
position="absolute"
top="50%"
width="1.25rem"
height="1.25rem"
rounded="full"
bg="white"
border="2px solid var(--ui-color-intent-primary)"
shadow="lg"
style={{
left: `${rangePercent}%`,
transform: `translate(-50%, -50%) ${isDragging ? 'scale(1.25)' : ''}`,
transition: 'transform 75ms, left 75ms',
boxShadow: isDragging ? '0 0 16px rgba(25, 140, 255, 0.6)' : '0 2px 8px rgba(0,0,0,0.3)'
}}
/>
</div>
{/* Value input and quick presets */}
<div className="flex items-center justify-between gap-3">
<div className="flex items-center gap-2">
<Group justify="between" align="center" gap={3}>
<Group gap={2}>
<input
ref={inputRef}
type="number"
@@ -233,20 +280,26 @@ export function RangeField({
onChange={handleInputChange}
onBlur={handleInputBlur}
disabled={disabled}
className={`
w-16 px-2 py-1.5 text-sm font-medium text-center rounded-lg
bg-iron-gray border border-charcoal-outline text-white
focus:border-primary-blue focus:ring-1 focus:ring-primary-blue focus:outline-none
transition-colors
${error ? 'border-warning-amber' : ''}
${disabled ? 'opacity-50 cursor-not-allowed' : ''}
`}
style={{
width: '4rem',
padding: '0.375rem 0.5rem',
fontSize: '0.875rem',
fontWeight: 500,
textAlign: 'center',
borderRadius: 'var(--ui-radius-lg)',
backgroundColor: 'var(--ui-color-bg-surface-muted)',
border: `1px solid ${error ? 'var(--ui-color-intent-critical)' : 'var(--ui-color-border-default)'}`,
color: 'white',
outline: 'none',
opacity: disabled ? 0.5 : 1,
cursor: disabled ? 'not-allowed' : 'text'
}}
/>
<span className="text-xs text-gray-400">{unitLabel}</span>
</div>
<Text size="xs" variant="low">{unitLabel}</Text>
</Group>
{quickPresets.length > 0 && (
<div className="flex gap-1">
<Group gap={1}>
{quickPresets.slice(0, 3).map((preset) => (
<button
key={preset}
@@ -256,17 +309,26 @@ export function RangeField({
onChange(preset);
}}
disabled={disabled}
className="px-2 py-1 text-[10px] rounded bg-charcoal-outline/50 text-gray-400 hover:bg-charcoal-outline hover:text-white transition-colors"
style={{
padding: '0.25rem 0.5rem',
fontSize: '10px',
borderRadius: 'var(--ui-radius-sm)',
backgroundColor: 'rgba(255, 255, 255, 0.05)',
color: 'var(--ui-color-text-low)',
border: 'none',
cursor: disabled ? 'not-allowed' : 'pointer',
transition: 'all 150ms'
}}
>
{preset}
</button>
))}
</div>
</Group>
)}
</div>
{helperText && <p className="text-xs text-gray-500">{helperText}</p>}
{error && <p className="text-xs text-warning-amber">{error}</p>}
</div>
</Group>
{helperText && <Text size="xs" variant="low">{helperText}</Text>}
{error && <Text size="xs" variant="critical">{error}</Text>}
</Stack>
);
}

View File

@@ -3,6 +3,8 @@
import { Toast as UIToast } from '@/ui/Toast';
import { Text } from '@/ui/Text';
import { Icon } from '@/ui/Icon';
import { Box } from '@/ui/Box';
import { Stack } from '@/ui/Stack';
import { AlertCircle, CheckCircle, Info } from 'lucide-react';
import React, { createContext, useContext, useState } from 'react';
@@ -32,16 +34,25 @@ export function ToastProvider({ children }: { children: React.ReactNode }) {
return (
<ToastContext.Provider value={{ showToast }}>
{children}
<div style={{ position: 'fixed', bottom: '1.5rem', right: '1.5rem', zIndex: 100, display: 'flex', flexDirection: 'column', gap: '0.75rem', pointerEvents: 'none' }}>
<Box
position="fixed"
bottom={6}
right={6}
zIndex={100}
display="flex"
flexDirection="col"
gap={3}
style={{ pointerEvents: 'none' }}
>
{toasts.map((toast) => (
<div key={toast.id} style={{ pointerEvents: 'auto' }}>
<Box key={toast.id} style={{ pointerEvents: 'auto' }}>
<ToastItem
toast={toast}
onClose={() => setToasts((prev) => prev.filter((t) => t.id !== toast.id))}
/>
</div>
</Box>
))}
</div>
</Box>
</ToastContext.Provider>
);
}

View File

@@ -3,6 +3,7 @@
import { Image } from '@/ui/Image';
import { SidebarItem } from '@/ui/SidebarItem';
import { Text } from '@/ui/Text';
import { Surface } from '@/ui/Surface';
import React from 'react';
interface FriendItemProps {
@@ -15,7 +16,7 @@ export function FriendItem({ name, avatarUrl, country }: FriendItemProps) {
return (
<SidebarItem
icon={
<div style={{ width: '100%', height: '100%', borderRadius: '9999px', overflow: 'hidden' }}>
<Surface width="100%" height="100%" rounded="full" overflow="hidden">
<Image
src={avatarUrl}
alt={name}
@@ -23,7 +24,7 @@ export function FriendItem({ name, avatarUrl, country }: FriendItemProps) {
height={36}
objectFit="cover"
/>
</div>
</Surface>
}
>
<Text size="sm" variant="high" weight="medium" truncate block>

View File

@@ -1,4 +1,5 @@
import React, { ReactNode } from 'react';
import { Stack } from '@/ui/Stack';
interface FriendsListProps {
children: ReactNode;
@@ -6,8 +7,8 @@ interface FriendsListProps {
export function FriendsList({ children }: FriendsListProps) {
return (
<div style={{ display: 'flex', flexDirection: 'column', gap: '0.5rem' }}>
<Stack gap={2}>
{children}
</div>
</Stack>
);
}

View File

@@ -9,6 +9,7 @@ import { SectionHeader } from '@/ui/SectionHeader';
import { Icon } from '@/ui/Icon';
import { Link } from '@/ui/Link';
import { Text } from '@/ui/Text';
import { Box } from '@/ui/Box';
import { UserPlus, Users } from 'lucide-react';
import React from 'react';
@@ -44,14 +45,14 @@ export function FriendsSidebar({ friends, hasFriends }: FriendsSidebarProps) {
/>
))}
{friends.length > 6 && (
<div style={{ padding: '0.5rem 0' }}>
<Box paddingY={2}>
<Link
href={routes.protected.profile}
variant="primary"
>
<Text size="sm" block align="center">+{friends.length - 6} more</Text>
</Link>
</div>
</Box>
)}
</FriendsList>
) : (

View File

@@ -16,6 +16,9 @@ import { Input } from '@/ui/Input';
import { Text } from '@/ui/Text';
import { TextArea } from '@/ui/TextArea';
import { SectionHeader } from '@/ui/SectionHeader';
import { Box } from '@/ui/Box';
import { Group } from '@/ui/Group';
import { Stack } from '@/ui/Stack';
import { useState } from 'react';
import React from 'react';
@@ -94,7 +97,7 @@ export function TeamAdmin({ team, onUpdate }: TeamAdminProps) {
};
return (
<div style={{ display: 'flex', flexDirection: 'column', gap: '1.5rem' }}>
<Stack gap={6}>
<Panel
title="Team Settings"
actions={!editMode && (
@@ -104,7 +107,7 @@ export function TeamAdmin({ team, onUpdate }: TeamAdminProps) {
)}
>
{editMode ? (
<div style={{ display: 'flex', flexDirection: 'column', gap: '1rem' }}>
<Stack gap={4}>
<Input
label="Team Name"
value={editedTeam.name}
@@ -126,7 +129,7 @@ export function TeamAdmin({ team, onUpdate }: TeamAdminProps) {
onChange={(e) => setEditedTeam({ ...editedTeam, description: e.target.value })}
/>
<div style={{ display: 'flex', gap: '0.5rem' }}>
<Group gap={2}>
<Button variant="primary" onClick={handleSaveChanges} disabled={updateTeamMutation.isPending}>
{updateTeamMutation.isPending ? 'Saving...' : 'Save Changes'}
</Button>
@@ -143,23 +146,23 @@ export function TeamAdmin({ team, onUpdate }: TeamAdminProps) {
>
Cancel
</Button>
</div>
</div>
</Group>
</Stack>
) : (
<div style={{ display: 'flex', flexDirection: 'column', gap: '1rem' }}>
<div>
<Text size="sm" variant="low" block marginBottom={1}>Team Name</Text>
<Stack gap={4}>
<Stack gap={1}>
<Text size="sm" variant="low" block>Team Name</Text>
<Text variant="high" weight="medium" block>{team.name}</Text>
</div>
<div>
<Text size="sm" variant="low" block marginBottom={1}>Team Tag</Text>
</Stack>
<Stack gap={1}>
<Text size="sm" variant="low" block>Team Tag</Text>
<Text variant="high" weight="medium" block>{team.tag}</Text>
</div>
<div>
<Text size="sm" variant="low" block marginBottom={1}>Description</Text>
</Stack>
<Stack gap={1}>
<Text size="sm" variant="low" block>Description</Text>
<Text variant="high" block>{team.description}</Text>
</div>
</div>
</Stack>
</Stack>
)}
</Panel>
@@ -197,6 +200,6 @@ export function TeamAdmin({ team, onUpdate }: TeamAdminProps) {
Disband Team (Coming Soon)
</Button>
</DangerZone>
</div>
</Stack>
);
}

View File

@@ -6,6 +6,9 @@ import { TeamHero } from '@/ui/TeamHero';
import { Text } from '@/ui/Text';
import { Badge } from '@/ui/Badge';
import { StatGrid } from '@/ui/StatGrid';
import { Group } from '@/ui/Group';
import { Surface } from '@/ui/Surface';
import { Box } from '@/ui/Box';
interface TeamDetailsHeaderProps {
teamId: string;
@@ -36,23 +39,30 @@ export function TeamDetailsHeader({
return (
<TeamHero
title={
<div className="flex items-center gap-3">
<Group gap={3}>
{name}
{tag && <Badge variant="outline">[{tag}]</Badge>}
</div>
</Group>
}
description={description || 'No mission statement provided.'}
memberCount={memberCount}
memberCountLabel={memberCountLabel}
foundedDateLabel={foundedDateLabel}
sideContent={
<div className="w-32 h-32 bg-[var(--ui-color-bg-surface-muted)] border border-[var(--ui-color-border-default)] flex items-center justify-center overflow-hidden rounded-lg">
<Surface
variant="muted"
rounded="lg"
width="8rem"
height="8rem"
display="flex"
alignItems="center"
justifyContent="center"
overflow="hidden"
border
>
{logoUrl ? (
<Image src={logoUrl} alt={name} width={128} height={128} />
) : (
<Text size="2xl" weight="bold" variant="low">{name.substring(0, 2).toUpperCase()}</Text>
)}
</div>
</Surface>
}
stats={
<StatGrid
@@ -71,7 +81,7 @@ export function TeamDetailsHeader({
/>
}
actions={
<>
<Group gap={3}>
{isAdmin && (
<Button variant="secondary" onClick={onAdminClick}>
Configure
@@ -80,7 +90,7 @@ export function TeamDetailsHeader({
<Button variant="primary">
Join Request
</Button>
</>
</Group>
}
/>
);

View File

@@ -9,6 +9,7 @@ import { Button } from '@/ui/Button';
import { Icon } from '@/ui/Icon';
import { TeamHero } from '@/ui/TeamHero';
import { Text } from '@/ui/Text';
import { Stack } from '@/ui/Stack';
import {
Crown,
Plus,
@@ -27,24 +28,36 @@ const SKILL_LEVELS = [
label: 'Pro',
icon: Crown,
intent: 'warning' as const,
color: 'text-warning-amber',
bgColor: 'bg-warning-amber/10',
borderColor: 'border-warning-amber/20',
},
{
id: 'advanced',
label: 'Advanced',
icon: Star,
intent: 'primary' as const,
color: 'text-primary-blue',
bgColor: 'bg-primary-blue/10',
borderColor: 'border-primary-blue/20',
},
{
id: 'intermediate',
label: 'Intermediate',
icon: TrendingUp,
intent: 'telemetry' as const,
color: 'text-telemetry-aqua',
bgColor: 'bg-telemetry-aqua/10',
borderColor: 'border-telemetry-aqua/20',
},
{
id: 'beginner',
label: 'Beginner',
icon: Shield,
intent: 'success' as const,
color: 'text-performance-green',
bgColor: 'bg-performance-green/10',
borderColor: 'border-performance-green/20',
},
] as const;
@@ -96,11 +109,11 @@ export function TeamHeroSection({
</React.Fragment>
}
sideContent={
<div style={{ display: 'flex', flexDirection: 'column', gap: '1rem' }}>
<Stack gap={4}>
<Text size="xs" variant="low" weight="bold" uppercase>
Find Your Level
</Text>
<div style={{ display: 'flex', flexDirection: 'column', gap: '0.5rem' }}>
<Stack gap={2}>
{SKILL_LEVELS.map((level) => {
const count = teamsByLevel[level.id]?.length || 0;
@@ -109,13 +122,16 @@ export function TeamHeroSection({
key={level.id}
label={level.label}
icon={level.icon}
color={level.color}
bgColor={level.bgColor}
borderColor={level.borderColor}
count={count}
onClick={() => onSkillLevelClick(level.id as SkillLevel)}
/>
);
})}
</div>
</div>
</Stack>
</Stack>
}
/>
);

View File

@@ -4,6 +4,7 @@ import { Box } from './Box';
import { Card } from './Card';
import { Heading } from './Heading';
import { Icon } from './Icon';
import { Text } from './Text';
export interface RaceCardProps {
children: ReactNode;