website refactor

This commit is contained in:
2026-01-19 12:35:16 +01:00
parent a8731e6937
commit 15290400b3
122 changed files with 902 additions and 255 deletions

View File

@@ -28,5 +28,5 @@ export default async function AdminPage() {
const output = result.unwrap(); const output = result.unwrap();
// Pass to client wrapper for UI interactions // Pass to client wrapper for UI interactions
return <AdminDashboardWrapper initialViewData={output} />; return <AdminDashboardWrapper viewData={output} />;
} }

View File

@@ -30,5 +30,5 @@ export default async function AdminUsersPage() {
const output = result.unwrap(); const output = result.unwrap();
// Pass to client wrapper for UI interactions // Pass to client wrapper for UI interactions
return <AdminUsersWrapper initialViewData={output} />; return <AdminUsersWrapper viewData={output} />;
} }

View File

@@ -25,7 +25,7 @@ export default async function LeagueStewardingPage({ params }: Props) {
leagueId={leagueId} leagueId={leagueId}
currentDriverId="" currentDriverId=""
onRefetch={() => {}} onRefetch={() => {}}
data={{ viewData={{
leagueId, leagueId,
totalPending: 0, totalPending: 0,
totalResolved: 0, totalResolved: 0,
@@ -39,7 +39,7 @@ export default async function LeagueStewardingPage({ params }: Props) {
const data = result.unwrap(); const data = result.unwrap();
return <StewardingPageClient return <StewardingPageClient
data={data} viewData={data}
leagueId={leagueId} leagueId={leagueId}
currentDriverId="" // Should be fetched or passed currentDriverId="" // Should be fetched or passed
onRefetch={() => {}} // Should be handled onRefetch={() => {}} // Should be handled

View File

@@ -21,5 +21,5 @@ export default async function Page({ params }: Props) {
const viewData = result.isOk() ? result.unwrap() : null; const viewData = result.isOk() ? result.unwrap() : null;
return <ProtestDetailPageClient initialViewData={viewData} />; return <ProtestDetailPageClient viewData={viewData as any} />;
} }

View File

@@ -13,8 +13,12 @@ export default async function AvatarsPage() {
return ( return (
<MediaPageClient <MediaPageClient
initialAssets={assets} viewData={{
categories={categories} assets,
categories,
title: 'Avatars',
description: 'Choose your driver avatar'
}}
/> />
); );
} }

View File

@@ -13,8 +13,12 @@ export default async function LeaguesMediaPage() {
return ( return (
<MediaPageClient <MediaPageClient
initialAssets={assets} viewData={{
categories={categories} assets,
categories,
title: 'League Media',
description: 'Manage league logos and covers'
}}
/> />
); );
} }

View File

@@ -23,8 +23,12 @@ export default async function MediaPage() {
return ( return (
<MediaPageClient <MediaPageClient
initialAssets={assets} viewData={{
categories={categories} assets,
categories,
title: 'Media Library',
description: 'Manage all your racing assets'
}}
/> />
); );
} }

View File

@@ -13,8 +13,12 @@ export default async function SponsorsMediaPage() {
return ( return (
<MediaPageClient <MediaPageClient
initialAssets={assets} viewData={{
categories={categories} assets,
categories,
title: 'Sponsor Media',
description: 'Manage sponsor logos'
}}
/> />
); );
} }

View File

@@ -13,8 +13,12 @@ export default async function TeamsMediaPage() {
return ( return (
<MediaPageClient <MediaPageClient
initialAssets={assets} viewData={{
categories={categories} assets,
categories,
title: 'Team Media',
description: 'Manage team logos'
}}
/> />
); );
} }

View File

@@ -13,8 +13,12 @@ export default async function TracksMediaPage() {
return ( return (
<MediaPageClient <MediaPageClient
initialAssets={assets} viewData={{
categories={categories} assets,
categories,
title: 'Track Media',
description: 'Manage track images'
}}
/> />
); );
} }

View File

@@ -68,9 +68,9 @@ export default function RaceStewardingPage({ params }: RaceStewardingPageProps)
isLoading={isLoading} isLoading={isLoading}
error={error} error={error}
retry={fetchData} retry={fetchData}
Template={({ data }) => ( Template={({ viewData }) => (
<RaceStewardingTemplate <RaceStewardingTemplate
viewData={data as RaceStewardingViewData} viewData={viewData}
isLoading={false} isLoading={false}
error={null} error={null}
onBack={handleBack} onBack={handleBack}

View File

@@ -16,5 +16,5 @@ export default async function Page() {
const viewData = result.isOk() ? result.unwrap() : null; const viewData = result.isOk() ? result.unwrap() : null;
return <RacesAllPageClient initialViewData={viewData} />; return <RacesAllPageClient viewData={viewData as any} />;
} }

View File

@@ -259,7 +259,7 @@ export default function SponsorSignupPage() {
</SponsorHero> </SponsorHero>
{/* Platform Stats */} {/* Platform Stats */}
<Box maxWidth="6xl" mx="auto" px={4} mt={-8} position="relative" zIndex={10}> <Box maxWidth="6xl" mx="auto" px={4} style={{ marginTop: '-2rem' }} position="relative" zIndex={10}>
<Box display="grid" gridCols={{ base: 2, md: 4 }} gap={4}> <Box display="grid" gridCols={{ base: 2, md: 4 }} gap={4}>
{PLATFORM_STATS.map((stat, index) => ( {PLATFORM_STATS.map((stat, index) => (
<motion.div <motion.div

View File

@@ -11,5 +11,5 @@ export default async function TeamLeaderboardPage() {
} }
const data = result.unwrap(); const data = result.unwrap();
return <TeamLeaderboardPageWrapper data={data.teams} />; return <TeamLeaderboardPageWrapper viewData={data} />;
} }

View File

@@ -0,0 +1,30 @@
> @gridpilot/website@0.1.0 build
> next build
▲ Next.js 15.5.9
- Environments: .env.local
Creating an optimized production build ...
✓ Compiled successfully in 3.7s
Skipping linting
Checking validity of types ...
Failed to compile.
./hooks/league/useCreateLeague.ts:7:17
Type error: Return type of exported function has or is using name 'CreateLeagueInput' from external module "/Users/marcmintel/Projects/gridpilot/apps/website/hooks/league/useCreateLeagueWithBlockers" but cannot be named.
  5 |  * This wrapper maintains backward compatibility while using the new blocker-aware hook
 6 |  */
> 7 | export function useCreateLeague() {
 | ^
 8 | return useCreateLeagueWithBlockers();
 9 | }
Next.js build worker exited with code: 1 and signal: null
npm error Lifecycle script `build` failed with error:
npm error code 1
npm error path /Users/marcmintel/Projects/gridpilot/apps/website
npm error workspace @gridpilot/website@0.1.0
npm error location /Users/marcmintel/Projects/gridpilot/apps/website
npm error command failed
npm error command sh -c next build

View File

@@ -175,7 +175,7 @@ export function LeagueAdminSchedulePageClient() {
<SharedStack gap={6}> <SharedStack gap={6}>
<SharedCard> <SharedCard>
<SharedBox p={6} textAlign="center"> <SharedBox p={6} textAlign="center">
<Heading level={3}>Admin Access Required</Heading> <SharedHeading level={3}>Admin Access Required</SharedHeading>
<SharedBox mt={2}> <SharedBox mt={2}>
<SharedText size="sm" color="text-gray-400">Only league admins can manage the schedule.</SharedText> <SharedText size="sm" color="text-gray-400">Only league admins can manage the schedule.</SharedText>
</SharedBox> </SharedBox>
@@ -190,10 +190,10 @@ export function LeagueAdminSchedulePageClient() {
data={templateData} data={templateData}
isLoading={isLoading} isLoading={isLoading}
error={null} error={null}
Template={({ data }) => ( Template={({ viewData }) => (
<> <>
<LeagueAdminScheduleTemplate <LeagueAdminScheduleTemplate
viewData={data} viewData={viewData}
onSeasonChange={handleSeasonChange} onSeasonChange={handleSeasonChange}
onPublishToggle={handlePublishToggle} onPublishToggle={handlePublishToggle}
onAddOrSave={handleAddOrSave} onAddOrSave={handleAddOrSave}

View File

@@ -1,11 +1,10 @@
'use client'; 'use client';
import React, { useState } from 'react'; import React, { useState } from 'react';
import { SponsorLeagueDetailTemplate } from '@/templates/SponsorLeagueDetailTemplate'; import { SponsorLeagueDetailTemplate, type SponsorLeagueDetailViewData } from '@/templates/SponsorLeagueDetailTemplate';
import { ClientWrapperProps } from '@/lib/contracts/components/ComponentContracts'; import { ClientWrapperProps } from '@/lib/contracts/components/ComponentContracts';
import { ViewData } from '@/lib/contracts/view-data/ViewData';
export function SponsorLeagueDetailPageClient({ viewData }: ClientWrapperProps<ViewData>) { export function SponsorLeagueDetailPageClient({ viewData }: ClientWrapperProps<SponsorLeagueDetailViewData>) {
const [activeTab, setActiveTab] = useState<'overview' | 'drivers' | 'races' | 'sponsor'>('overview'); const [activeTab, setActiveTab] = useState<'overview' | 'drivers' | 'races' | 'sponsor'>('overview');
const [selectedTier, setSelectedTier] = useState<'main' | 'secondary'>('main'); const [selectedTier, setSelectedTier] = useState<'main' | 'secondary'>('main');

View File

@@ -1,11 +1,13 @@
import { Flag, Star, Trophy, Users } from 'lucide-react'; import { Flag, Star, Trophy, Users } from 'lucide-react';
import { Avatar } from '../../ui/Avatar'; import { Avatar } from '../../ui/Avatar';
import { Badge } from '../../ui/Badge'; import { Badge } from '../../ui/Badge';
import { Box } from '../../ui/Box';
import { Heading } from '../../ui/Heading'; import { Heading } from '../../ui/Heading';
import { Icon } from '../../ui/Icon'; import { Icon } from '../../ui/Icon';
import { ProfileHero, ProfileAvatar, ProfileStatsGroup, ProfileStat } from '../../ui/ProfileHero'; import { ProfileHero, ProfileAvatar, ProfileStatsGroup, ProfileStat } from '../../ui/ProfileHero';
import { BadgeGroup } from '../../ui/BadgeGroup'; import { BadgeGroup } from '../../ui/BadgeGroup';
import { QuickStatCard, QuickStatItem } from '../../ui/QuickStatCard'; import { QuickStatCard, QuickStatItem } from '../../ui/QuickStatCard';
import { Stack } from '../../ui/Stack';
import React from 'react'; import React from 'react';
interface DashboardHeroProps { interface DashboardHeroProps {
@@ -27,7 +29,7 @@ export function DashboardHero({
}: DashboardHeroProps) { }: DashboardHeroProps) {
return ( return (
<ProfileHero glowColor="aqua"> <ProfileHero glowColor="aqua">
<div style={{ display: 'flex', alignItems: 'center', gap: '2rem', flexWrap: 'wrap' }}> <Stack direction="row" align="center" gap={8} wrap>
{/* Avatar Section */} {/* Avatar Section */}
<ProfileAvatar <ProfileAvatar
badge={<Icon icon={Star} size={5} intent="high" />} badge={<Icon icon={Star} size={5} intent="high" />}
@@ -40,7 +42,7 @@ export function DashboardHero({
</ProfileAvatar> </ProfileAvatar>
{/* Info Section */} {/* Info Section */}
<div style={{ flex: 1, minWidth: '200px' }}> <Box flex={1} minWidth="200px">
<Heading level={1}>{driverName}</Heading> <Heading level={1}>{driverName}</Heading>
<ProfileStatsGroup> <ProfileStatsGroup>
@@ -60,14 +62,14 @@ export function DashboardHero({
Team Redline Team Redline
</Badge> </Badge>
</BadgeGroup> </BadgeGroup>
</div> </Box>
{/* Quick Stats */} {/* Quick Stats */}
<QuickStatCard> <QuickStatCard>
<QuickStatItem label="Podiums" value="12" /> <QuickStatItem label="Podiums" value="12" />
<QuickStatItem label="Wins" value="4" /> <QuickStatItem label="Wins" value="4" />
</QuickStatCard> </QuickStatCard>
</div> </Stack>
</ProfileHero> </ProfileHero>
); );
} }

View File

@@ -5,6 +5,7 @@ import { routes } from '@/lib/routing/RouteConfig';
import { Button } from '@/ui/Button'; import { Button } from '@/ui/Button';
import { Icon } from '@/ui/Icon'; import { Icon } from '@/ui/Icon';
import { Link } from '@/ui/Link'; import { Link } from '@/ui/Link';
import { Stack } from '@/ui/Stack';
import { StatGrid } from '@/ui/StatGrid'; import { StatGrid } from '@/ui/StatGrid';
import { Flag, Medal, Target, Trophy, User, Users } from 'lucide-react'; import { Flag, Medal, Target, Trophy, User, Users } from 'lucide-react';
import React from 'react'; import React from 'react';
@@ -26,7 +27,7 @@ interface DashboardHeroProps {
export function DashboardHero({ currentDriver, activeLeaguesCount }: DashboardHeroProps) { export function DashboardHero({ currentDriver, activeLeaguesCount }: DashboardHeroProps) {
return ( return (
<div style={{ display: 'flex', flexDirection: 'column', gap: '2rem' }}> <Stack gap={8}>
<UiDashboardHero <UiDashboardHero
driverName={currentDriver.name} driverName={currentDriver.name}
avatarUrl={currentDriver.avatarUrl} avatarUrl={currentDriver.avatarUrl}
@@ -36,7 +37,7 @@ export function DashboardHero({ currentDriver, activeLeaguesCount }: DashboardHe
winRate={Math.round((currentDriver.wins / currentDriver.totalRaces) * 100) || 0} winRate={Math.round((currentDriver.wins / currentDriver.totalRaces) * 100) || 0}
/> />
<div style={{ display: 'flex', gap: '1rem', flexWrap: 'wrap' }}> <Stack direction="row" gap={4} wrap>
<Link href={routes.public.leagues}> <Link href={routes.public.leagues}>
<Button variant="secondary" icon={<Icon icon={Flag} size={4} />}> <Button variant="secondary" icon={<Icon icon={Flag} size={4} />}>
Browse Leagues Browse Leagues
@@ -47,7 +48,7 @@ export function DashboardHero({ currentDriver, activeLeaguesCount }: DashboardHe
View Profile View Profile
</Button> </Button>
</Link> </Link>
</div> </Stack>
<StatGrid <StatGrid
variant="box" variant="box"
@@ -59,6 +60,6 @@ export function DashboardHero({ currentDriver, activeLeaguesCount }: DashboardHe
{ icon: Users, label: 'Active Leagues', value: activeLeaguesCount, intent: 'telemetry' }, { icon: Users, label: 'Active Leagues', value: activeLeaguesCount, intent: 'telemetry' },
]} ]}
/> />
</div> </Stack>
); );
} }

View File

@@ -4,7 +4,7 @@ import { useNotifications } from '@/components/notifications/NotificationProvide
import type { NotificationVariant } from '@/components/notifications/notificationTypes'; import type { NotificationVariant } from '@/components/notifications/notificationTypes';
import { useEffectiveDriverId } from "@/hooks/useEffectiveDriverId"; import { useEffectiveDriverId } from "@/hooks/useEffectiveDriverId";
import { ApiConnectionMonitor } from '@/lib/api/base/ApiConnectionMonitor'; import { ApiConnectionMonitor } from '@/lib/api/base/ApiConnectionMonitor';
import { CircuitBreakerRegistry } from '@/lib/api/base/CircuitBreakerRegistry'; import { CircuitBreakerRegistry } from '@/lib/api/base/RetryHandler';
import { getGlobalErrorHandler } from '@/lib/infrastructure/GlobalErrorHandler'; import { getGlobalErrorHandler } from '@/lib/infrastructure/GlobalErrorHandler';
import { Activity, AlertTriangle, ChevronDown, ChevronUp, MessageSquare, Wrench, X } from 'lucide-react'; import { Activity, AlertTriangle, ChevronDown, ChevronUp, MessageSquare, Wrench, X } from 'lucide-react';
import { useEffect, useState } from 'react'; import { useEffect, useState } from 'react';

View File

@@ -2,11 +2,12 @@
import { getGlobalReplaySystem } from '@/lib/infrastructure/ErrorReplay'; import { getGlobalReplaySystem } from '@/lib/infrastructure/ErrorReplay';
import { Button } from '@/ui/Button'; import { Button } from '@/ui/Button';
import { Box } from '@/ui/Box';
import { Icon } from '@/ui/Icon'; import { Icon } from '@/ui/Icon';
import { IconButton } from '@/ui/IconButton'; import { IconButton } from '@/ui/IconButton';
import { Stack } from '@/ui/Stack'; import { Stack } from '@/ui/Stack';
import { Text } from '@/ui/Text'; import { Text } from '@/ui/Text';
import { Box, Clock, Copy, Download, Play, Trash2 } from 'lucide-react'; import { Clock, Copy, Download, Play, Trash2 } from 'lucide-react';
import { useEffect, useState } from 'react'; import { useEffect, useState } from 'react';
interface ReplayEntry { interface ReplayEntry {

View File

@@ -11,8 +11,7 @@ import { Group } from '@/ui/Group';
import { Stack } from '@/ui/Stack'; import { Stack } from '@/ui/Stack';
import { ControlBar } from '@/ui/ControlBar'; import { ControlBar } from '@/ui/ControlBar';
import { Trophy } from 'lucide-react'; import { Trophy } from 'lucide-react';
import { useEffect, useState } from 'react'; import React, { useEffect, useState } from 'react';
import React from 'react';
interface RaceHistoryProps { interface RaceHistoryProps {
driverId: string; driverId: string;

View File

@@ -3,10 +3,12 @@ import { Badge } from '@/ui/Badge';
interface RatingBadgeProps { interface RatingBadgeProps {
rating: number; rating: number;
size?: 'sm' | 'md'; size?: 'sm' | 'md' | 'lg';
} }
export function RatingBadge({ rating, size = 'md' }: RatingBadgeProps) { export function RatingBadge({ rating, size = 'md' }: RatingBadgeProps) {
const badgeSize = size === 'lg' ? 'md' : size;
const getVariant = (val: number): 'warning' | 'primary' | 'success' | 'default' => { const getVariant = (val: number): 'warning' | 'primary' | 'success' | 'default' => {
if (val >= 2500) return 'warning'; if (val >= 2500) return 'warning';
if (val >= 2000) return 'primary'; // Simplified if (val >= 2000) return 'primary'; // Simplified
@@ -18,7 +20,7 @@ export function RatingBadge({ rating, size = 'md' }: RatingBadgeProps) {
return ( return (
<Badge <Badge
variant={getVariant(rating)} variant={getVariant(rating)}
size={size} size={badgeSize}
> >
{rating.toLocaleString()} {rating.toLocaleString()}
</Badge> </Badge>

View File

@@ -56,7 +56,7 @@ export function SafetyRatingBadge({ rating, size = 'md' }: SafetyRatingBadgeProp
border border
bg={getBgColor(rating)} bg={getBgColor(rating)}
borderColor={getBorderColor(rating)} borderColor={getBorderColor(rating)}
{...sizeProps[size]} {...(sizeProps[size] as any)}
> >
<Shield size={iconSizes[size]} color={iconColors[colorClass as keyof typeof iconColors]} /> <Shield size={iconSizes[size]} color={iconColors[colorClass as keyof typeof iconColors]} />
<Text <Text

View File

@@ -3,7 +3,7 @@
import React, { Component, ReactNode, ErrorInfo, useState, version } from 'react'; import React, { Component, ReactNode, ErrorInfo, useState, version } from 'react';
import { ApiError } from '@/lib/api/base/ApiError'; import { ApiError } from '@/lib/api/base/ApiError';
import { getGlobalErrorHandler } from '@/lib/infrastructure/GlobalErrorHandler'; import { getGlobalErrorHandler } from '@/lib/infrastructure/GlobalErrorHandler';
import { DevErrorPanel } from './DevErrorPanel'; import { DevErrorPanel } from '@/ui/DevErrorPanel';
import { ErrorDisplay } from '@/ui/ErrorDisplay'; import { ErrorDisplay } from '@/ui/ErrorDisplay';
interface Props { interface Props {

View File

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

View File

@@ -1,5 +1,6 @@
import { FeedItemCard } from '@/components/feed/FeedItemCard'; import { FeedItemCard } from '@/components/feed/FeedItemCard';
import { FeedEmptyState } from '@/ui/FeedEmptyState'; import { FeedEmptyState } from '@/ui/FeedEmptyState';
import { Stack } from '@/ui/Stack';
import React from 'react'; import React from 'react';
interface FeedItemData { interface FeedItemData {
@@ -23,10 +24,10 @@ export function FeedList({ items }: FeedListProps) {
} }
return ( return (
<div style={{ display: 'flex', flexDirection: 'column', gap: '1rem' }}> <Stack gap={4}>
{items.map(item => ( {items.map(item => (
<FeedItemCard key={item.id} item={item} /> <FeedItemCard key={item.id} item={item} />
))} ))}
</div> </Stack>
); );
} }

View File

@@ -1,7 +1,7 @@
import { routes } from '@/lib/routing/RouteConfig'; import { routes } from '@/lib/routing/RouteConfig';
import { Stack } from '@/ui/Stack'; import { Stack } from '@/ui/Stack';
import { Calendar, Home, Layout, Settings, Trophy, Users } from 'lucide-react'; import { Calendar, Home, Layout, Settings, Trophy, Users } from 'lucide-react';
import { NavLink } from './NavLink'; import { NavLink } from '@/ui/NavLink';
interface AuthedNavProps { interface AuthedNavProps {
pathname: string; pathname: string;

View File

@@ -1,7 +1,7 @@
import { routes } from '@/lib/routing/RouteConfig'; import { routes } from '@/lib/routing/RouteConfig';
import { Stack } from '@/ui/Stack'; import { Stack } from '@/ui/Stack';
import { Calendar, Home, Layout, Trophy, Users } from 'lucide-react'; import { Calendar, Home, Layout, Trophy, Users } from 'lucide-react';
import { NavLink } from './NavLink'; import { NavLink } from '@/ui/NavLink';
interface PublicNavProps { interface PublicNavProps {
pathname: string; pathname: string;

View File

@@ -5,10 +5,12 @@ import React from 'react';
interface RankBadgeProps { interface RankBadgeProps {
rank: number; rank: number;
size?: 'sm' | 'md'; size?: 'sm' | 'md' | 'lg';
} }
export function RankBadge({ rank, size = 'md' }: RankBadgeProps) { export function RankBadge({ rank, size = 'md' }: RankBadgeProps) {
const badgeSize = size === 'lg' ? 'md' : size;
const getVariant = (rank: number): 'warning' | 'primary' | 'info' | 'default' => { const getVariant = (rank: number): 'warning' | 'primary' | 'info' | 'default' => {
if (rank <= 3) return 'warning'; if (rank <= 3) return 'warning';
if (rank <= 10) return 'primary'; if (rank <= 10) return 'primary';
@@ -28,7 +30,7 @@ export function RankBadge({ rank, size = 'md' }: RankBadgeProps) {
const medal = getMedalEmoji(rank); const medal = getMedalEmoji(rank);
return ( return (
<Badge variant={getVariant(rank)} size={size}> <Badge variant={getVariant(rank)} size={badgeSize}>
<Group gap={1}> <Group gap={1}>
{medal && <Text size="xs">{medal}</Text>} {medal && <Text size="xs">{medal}</Text>}
<Text size="xs" weight="bold">#{rank}</Text> <Text size="xs" weight="bold">#{rank}</Text>

View File

@@ -1,6 +1,7 @@
import { DriverIdentity } from '@/ui/DriverIdentity'; import { DriverIdentity } from '@/ui/DriverIdentity';
import { DriverViewModel } from '@/lib/view-models/DriverViewModel'; import { DriverViewModel } from '@/lib/view-models/DriverViewModel';
import { Badge } from '@/ui/Badge'; import { Badge } from '@/ui/Badge';
import { Box } from '@/ui/Box';
import { TableCell, TableRow } from '@/ui/Table'; import { TableCell, TableRow } from '@/ui/Table';
import { Text } from '@/ui/Text'; import { Text } from '@/ui/Text';
import { DateDisplay } from '@/lib/display-objects/DateDisplay'; import { DateDisplay } from '@/lib/display-objects/DateDisplay';
@@ -41,7 +42,7 @@ export function LeagueMemberRow({
return ( return (
<TableRow variant={isTopPerformer ? 'highlight' : 'default'}> <TableRow variant={isTopPerformer ? 'highlight' : 'default'}>
<TableCell> <TableCell>
<div style={{ display: 'flex', alignItems: 'center', gap: '0.5rem' }}> <Box display="flex" alignItems="center" gap={2}>
{driver ? ( {driver ? (
<DriverIdentity <DriverIdentity
driver={driver} driver={driver}
@@ -59,7 +60,7 @@ export function LeagueMemberRow({
{isTopPerformer && ( {isTopPerformer && (
<Text size="xs"></Text> <Text size="xs"></Text>
)} )}
</div> </Box>
</TableCell> </TableCell>
<TableCell> <TableCell>
<Text variant="primary" weight="medium"> <Text variant="primary" weight="medium">

View File

@@ -17,8 +17,7 @@ import { Text } from '@/ui/Text';
import { Box } from '@/ui/Box'; import { Box } from '@/ui/Box';
import { Group } from '@/ui/Group'; import { Group } from '@/ui/Group';
import { ControlBar } from '@/ui/ControlBar'; import { ControlBar } from '@/ui/ControlBar';
import { useCallback, useEffect, useState } from 'react'; import React, { useCallback, useEffect, useState } from 'react';
import React from 'react';
interface LeagueMembersProps { interface LeagueMembersProps {
leagueId: string; leagueId: string;

View File

@@ -7,7 +7,7 @@ import type { LeagueScheduleRaceViewModel } from '@/lib/view-models/LeagueSchedu
import { useState } from 'react'; import { useState } from 'react';
// Shared state components // Shared state components
import { StateContainer } from '@/ui/StateContainer'; import { StateContainer } from '@/components/shared/state/StateContainer';
import { useLeagueSchedule } from "@/hooks/league/useLeagueSchedule"; import { useLeagueSchedule } from "@/hooks/league/useLeagueSchedule";
import { Button } from '@/ui/Button'; import { Button } from '@/ui/Button';
import { Heading } from '@/ui/Heading'; import { Heading } from '@/ui/Heading';

View File

@@ -1,7 +1,6 @@
'use client'; 'use client';
import { Text } from '@/ui/Text'; import { Text } from '@/ui/Text';
import { useState } from 'react';
import { MediaCard } from '@/ui/MediaCard'; import { MediaCard } from '@/ui/MediaCard';
import { MediaFiltersBar } from './MediaFiltersBar'; import { MediaFiltersBar } from './MediaFiltersBar';
import { Grid } from '@/ui/Grid'; import { Grid } from '@/ui/Grid';
@@ -10,7 +9,7 @@ import { MediaViewerModal } from './MediaViewerModal';
import { SectionHeader } from '@/ui/SectionHeader'; import { SectionHeader } from '@/ui/SectionHeader';
import { EmptyState } from '@/ui/EmptyState'; import { EmptyState } from '@/ui/EmptyState';
import { Search } from 'lucide-react'; import { Search } from 'lucide-react';
import React from 'react'; import React, { useState } from 'react';
export interface MediaAsset { export interface MediaAsset {
id: string; id: string;

View File

@@ -235,7 +235,7 @@ function AnimatedRating({ shouldReduceMotion, value }: { shouldReduceMotion: boo
return ( return (
<Stack as={motion.span} weight="bold" color="text-primary-accent" font="mono" size={{ base: 'sm', sm: 'base' }}> <Stack as={motion.span} weight="bold" color="text-primary-accent" font="mono" size={{ base: 'sm', sm: 'base' }}>
{shouldReduceMotion ? value : <Stack as={motion.span}>{rounded}</Stack>} {shouldReduceMotion ? value : <motion.span>{rounded}</motion.span>}
</Stack> </Stack>
); );
} }
@@ -265,7 +265,7 @@ function AnimatedCounter({
return ( return (
<Stack weight="bold" color="text-white" font="mono" size={{ base: 'sm', sm: 'base', md: 'lg' }}> <Stack weight="bold" color="text-white" font="mono" size={{ base: 'sm', sm: 'base', md: 'lg' }}>
{shouldReduceMotion ? value : <Stack as={motion.span}>{rounded}</Stack>}{suffix} {shouldReduceMotion ? value : <motion.span>{rounded}</motion.span>}{suffix}
</Stack> </Stack>
); );
} }

View File

@@ -270,7 +270,7 @@ function AnimatedPoints({
weight="bold" weight="bold"
color="text-white" color="text-white"
> >
{shouldReduceMotion ? points : <Stack as={motion.span}>{spring}</Stack>} {shouldReduceMotion ? points : <motion.span>{spring}</motion.span>}
</Stack> </Stack>
</Stack> </Stack>
</Stack> </Stack>

View File

@@ -6,6 +6,7 @@ interface Stat {
label: string; label: string;
value: string | number; value: string | number;
intent?: 'primary' | 'telemetry' | 'success' | 'critical'; intent?: 'primary' | 'telemetry' | 'success' | 'critical';
color?: string;
} }
interface ProfileStatGridProps { interface ProfileStatGridProps {
@@ -17,6 +18,7 @@ export function ProfileStatGrid({ stats }: ProfileStatGridProps) {
label: stat.label, label: stat.label,
value: stat.value, value: stat.value,
intent: stat.intent || 'primary', intent: stat.intent || 'primary',
color: stat.color,
icon: Bug // Default icon if none provided, but StatBox requires one icon: Bug // Default icon if none provided, but StatBox requires one
})); }));

View File

@@ -190,7 +190,8 @@ export function UserPill() {
return ( return (
<Box position="relative" display="inline-flex" alignItems="center" data-user-pill> <Box position="relative" display="inline-flex" alignItems="center" data-user-pill>
<button <Box
as="button"
type="button" type="button"
onClick={() => setIsMenuOpen((open) => !open)} onClick={() => setIsMenuOpen((open) => !open)}
style={{ style={{
@@ -237,7 +238,7 @@ export function UserPill() {
{/* Chevron */} {/* Chevron */}
<Icon icon={ChevronDown} size={3.5} intent="low" /> <Icon icon={ChevronDown} size={3.5} intent="low" />
</button> </Box>
<UserDropdown isOpen={isMenuOpen}> <UserDropdown isOpen={isMenuOpen}>
<UserDropdownHeader variant={isDemo ? 'demo' : 'default'}> <UserDropdownHeader variant={isDemo ? 'demo' : 'default'}>

View File

@@ -35,7 +35,7 @@ export function LatestResultsSidebar({ results }: LatestResultsSidebarProps) {
<RaceSummaryItem <RaceSummaryItem
track={result.track} track={result.track}
meta={`${result.winnerName}${result.car}`} meta={`${result.winnerName}${result.car}`}
date={scheduledAt} dateLabel={scheduledAt.toLocaleDateString()}
/> />
</Box> </Box>
); );

View File

@@ -24,22 +24,22 @@ interface RaceListItemProps {
export function RaceListItem({ race, onClick }: RaceListItemProps) { export function RaceListItem({ race, onClick }: RaceListItemProps) {
const statusConfig = { const statusConfig = {
scheduled: { scheduled: {
icon: Clock, iconName: 'Clock',
variant: 'primary' as const, variant: 'primary' as const,
label: 'Scheduled', label: 'Scheduled',
}, },
running: { running: {
icon: PlayCircle, iconName: 'PlayCircle',
variant: 'success' as const, variant: 'success' as const,
label: 'LIVE', label: 'LIVE',
}, },
completed: { completed: {
icon: CheckCircle2, iconName: 'CheckCircle2',
variant: 'default' as const, variant: 'default' as const,
label: 'Completed', label: 'Completed',
}, },
cancelled: { cancelled: {
icon: XCircle, iconName: 'XCircle',
variant: 'warning' as const, variant: 'warning' as const,
label: 'Cancelled', label: 'Cancelled',
}, },
@@ -64,11 +64,13 @@ export function RaceListItem({ race, onClick }: RaceListItemProps) {
dayLabel={date.getDate().toString()} dayLabel={date.getDate().toString()}
timeLabel={formatTime(race.scheduledAt)} timeLabel={formatTime(race.scheduledAt)}
status={race.status} status={race.status}
statusLabel={config.label}
statusVariant={config.variant}
statusIconName={config.iconName}
leagueName={race.leagueName} leagueName={race.leagueName}
leagueHref={race.leagueId ? routes.league.detail(race.leagueId) : undefined} leagueHref={race.leagueId ? routes.league.detail(race.leagueId) : undefined}
strengthOfField={race.strengthOfField} strengthOfField={race.strengthOfField}
onClick={() => onClick(race.id)} onClick={() => onClick(race.id)}
statusConfig={config}
/> />
); );
} }

View File

@@ -1,5 +1,6 @@
import { Box } from '@/ui/Box'; import { Box } from '@/ui/Box';
import { Panel } from '@/ui/Panel'; import { Panel } from '@/ui/Panel';
import { Stack } from '@/ui/Stack';
import { StatusDot } from '@/ui/StatusDot'; import { StatusDot } from '@/ui/StatusDot';
import { Text } from '@/ui/Text'; import { Text } from '@/ui/Text';
@@ -30,16 +31,16 @@ export function SessionSummaryPanel({
return ( return (
<Panel title="Session Summary" className={className}> <Panel title="Session Summary" className={className}>
<Box display="flex" flexDirection="col" gap={3}> <Stack gap={3}>
<Box display="flex" align="center" justify="between"> <Stack direction="row" align="center" justify="between">
<Text weight="bold" size="lg">{title}</Text> <Text weight="bold" size="lg">{title}</Text>
<Box display="flex" align="center" gap={2}> <Stack direction="row" align="center" gap={2}>
<StatusDot color={statusColor} pulse={status === 'live'} size={2} /> <StatusDot color={statusColor} pulse={status === 'live'} size={2} />
<Text size="xs" uppercase weight="bold" style={{ color: statusColor }}> <Text size="xs" uppercase weight="bold" style={{ color: statusColor }}>
{status} {status}
</Text> </Text>
</Box> </Stack>
</Box> </Stack>
<Box display="grid" gridCols={2} gap={4} borderTop borderStyle="solid" borderColor="border-gray/10" pt={3}> <Box display="grid" gridCols={2} gap={4} borderTop borderStyle="solid" borderColor="border-gray/10" pt={3}>
{startTime && ( {startTime && (
@@ -61,7 +62,7 @@ export function SessionSummaryPanel({
</Box> </Box>
)} )}
</Box> </Box>
</Box> </Stack>
</Panel> </Panel>
); );
} }

View File

@@ -1,4 +1,5 @@
import { Box } from '@/ui/Box'; import { Box } from '@/ui/Box';
import { Stack } from '@/ui/Stack';
import { Text } from '@/ui/Text'; import { Text } from '@/ui/Text';
interface TelemetryItem { interface TelemetryItem {
@@ -25,7 +26,7 @@ export function TelemetryStrip({ items, className = '' }: TelemetryStripProps) {
className={`bg-graphite-black/80 border-y border-border-gray/30 py-2 px-4 flex items-center gap-8 overflow-x-auto no-scrollbar ${className}`} className={`bg-graphite-black/80 border-y border-border-gray/30 py-2 px-4 flex items-center gap-8 overflow-x-auto no-scrollbar ${className}`}
> >
{items.map((item, index) => ( {items.map((item, index) => (
<Box key={index} display="flex" align="center" gap={2} className="whitespace-nowrap"> <Stack key={index} direction="row" align="center" gap={2} className="whitespace-nowrap">
<Text size="xs" color="text-gray-500" uppercase weight="bold" letterSpacing="widest"> <Text size="xs" color="text-gray-500" uppercase weight="bold" letterSpacing="widest">
{item.label} {item.label}
</Text> </Text>
@@ -42,7 +43,7 @@ export function TelemetryStrip({ items, className = '' }: TelemetryStripProps) {
{item.trend === 'up' ? '↑' : item.trend === 'down' ? '↓' : '•'} {item.trend === 'up' ? '↑' : item.trend === 'down' ? '↓' : '•'}
</Text> </Text>
)} )}
</Box> </Stack>
))} ))}
</Box> </Box>
); );

View File

@@ -3,6 +3,7 @@
import { Icon } from '@/ui/Icon'; import { Icon } from '@/ui/Icon';
import { Text } from '@/ui/Text'; import { Text } from '@/ui/Text';
import { Stack } from '@/ui/Stack'; import { Stack } from '@/ui/Stack';
import { Grid } from '@/ui/Grid';
import { Droplets, Sun, Thermometer, Wind, type LucideIcon } from 'lucide-react'; import { Droplets, Sun, Thermometer, Wind, type LucideIcon } from 'lucide-react';
interface TrackConditionsPanelProps { interface TrackConditionsPanelProps {
@@ -26,7 +27,7 @@ export function TrackConditionsPanel({
Track Conditions Track Conditions
</Text> </Text>
<Stack display="grid" gridCols={2} mdCols={4} gap={4}> <Grid cols={2} mdCols={4} gap={4}>
<ConditionItem <ConditionItem
icon={Thermometer} icon={Thermometer}
label="Air Temp" label="Air Temp"
@@ -51,7 +52,7 @@ export function TrackConditionsPanel({
value={windSpeed} value={windSpeed}
color="text-gray-400" color="text-gray-400"
/> />
</Stack> </Grid>
<Stack mt={4} pt={4} borderTop borderColor="border-outline-steel" bgOpacity={0.5} display="flex" alignItems="center" gap={3}> <Stack mt={4} pt={4} borderTop borderColor="border-outline-steel" bgOpacity={0.5} display="flex" alignItems="center" gap={3}>
<Icon icon={Sun} size={4} color="#FFBE4D" /> <Icon icon={Sun} size={4} color="#FFBE4D" />

View File

@@ -42,7 +42,7 @@ export function UpcomingRacesSidebar({ races }: UpcomingRacesSidebarProps) {
key={race.id} key={race.id}
track={race.track} track={race.track}
meta={race.car} meta={race.car}
date={scheduledAt} dateLabel={scheduledAt.toLocaleDateString()}
/> />
); );
})} })}

View File

@@ -130,7 +130,8 @@ export function RangeField({
<Group justify="between" gap={3}> <Group justify="between" gap={3}>
<Text as="label" size="xs" weight="medium" variant="low">{label}</Text> <Text as="label" size="xs" weight="medium" variant="low">{label}</Text>
<Box display="flex" alignItems="center" gap={2} flex={1} maxWidth="200px"> <Box display="flex" alignItems="center" gap={2} flex={1} maxWidth="200px">
<div <Box
as="div"
ref={sliderRef} ref={sliderRef}
style={{ style={{
position: 'relative', position: 'relative',
@@ -173,7 +174,7 @@ export function RangeField({
boxShadow: isDragging ? '0 0 12px rgba(25, 140, 255, 0.5)' : undefined boxShadow: isDragging ? '0 0 12px rgba(25, 140, 255, 0.5)' : undefined
}} }}
/> />
</div> </Box>
<Box flexShrink={0}> <Box flexShrink={0}>
<Group gap={1}> <Group gap={1}>
<Text size="sm" weight="semibold" variant="high" textAlign="right" width="2rem">{clampedValue}</Text> <Text size="sm" weight="semibold" variant="high" textAlign="right" width="2rem">{clampedValue}</Text>
@@ -202,7 +203,8 @@ export function RangeField({
)} )}
{/* Custom slider */} {/* Custom slider */}
<div <Box
as="div"
ref={sliderRef} ref={sliderRef}
style={{ style={{
position: 'relative', position: 'relative',
@@ -265,12 +267,13 @@ export function RangeField({
boxShadow: isDragging ? '0 0 16px rgba(25, 140, 255, 0.6)' : '0 2px 8px rgba(0,0,0,0.3)' boxShadow: isDragging ? '0 0 16px rgba(25, 140, 255, 0.6)' : '0 2px 8px rgba(0,0,0,0.3)'
}} }}
/> />
</div> </Box>
{/* Value input and quick presets */} {/* Value input and quick presets */}
<Group justify="between" align="center" gap={3}> <Group justify="between" align="center" gap={3}>
<Group gap={2}> <Group gap={2}>
<input <Box
as="input"
ref={inputRef} ref={inputRef}
type="number" type="number"
min={min} min={min}
@@ -301,7 +304,8 @@ export function RangeField({
{quickPresets.length > 0 && ( {quickPresets.length > 0 && (
<Group gap={1}> <Group gap={1}>
{quickPresets.slice(0, 3).map((preset) => ( {quickPresets.slice(0, 3).map((preset) => (
<button <Box
as="button"
key={preset} key={preset}
type="button" type="button"
onClick={() => { onClick={() => {
@@ -321,7 +325,7 @@ export function RangeField({
}} }}
> >
{preset} {preset}
</button> </Box>
))} ))}
</Group> </Group>
)} )}

View File

@@ -18,6 +18,7 @@ import { LoadingSpinner } from '@/ui/LoadingSpinner';
import { Badge } from '@/ui/Badge'; import { Badge } from '@/ui/Badge';
import { ProgressLine } from '@/ui/ProgressLine'; import { ProgressLine } from '@/ui/ProgressLine';
import { SharedEmptyState } from './SharedEmptyState';
export { export {
Pagination as SharedPagination, Pagination as SharedPagination,
@@ -38,5 +39,6 @@ export {
Skeleton as SharedSkeleton, Skeleton as SharedSkeleton,
LoadingSpinner as SharedLoadingSpinner, LoadingSpinner as SharedLoadingSpinner,
Badge as SharedBadge, Badge as SharedBadge,
ProgressLine as SharedProgressLine ProgressLine as SharedProgressLine,
SharedEmptyState
}; };

View File

@@ -84,8 +84,7 @@ export function TransactionTable({ transactions, onDownload }: TransactionTableP
return ( return (
<Grid <Grid
key={tx.id} key={tx.id}
cols={1} cols={{ base: 1, md: 12 }}
mdCols={12}
gap={4} gap={4}
p={4} p={4}
alignItems="center" alignItems="center"

View File

@@ -1,6 +1,7 @@
'use client'; 'use client';
import { useCreateTeam } from "@/hooks/team/useCreateTeam"; import { useCreateTeam } from "@/hooks/team/useCreateTeam";
import { Box } from '@/ui/Box';
import { Button } from '@/ui/Button'; import { Button } from '@/ui/Button';
import { InfoBanner } from '@/ui/InfoBanner'; import { InfoBanner } from '@/ui/InfoBanner';
import { Input } from '@/ui/Input'; import { Input } from '@/ui/Input';
@@ -79,7 +80,7 @@ export function CreateTeamForm({ onCancel, onSuccess, onNavigate }: CreateTeamFo
}; };
return ( return (
<form onSubmit={handleSubmit}> <Box as="form" onSubmit={handleSubmit}>
<Group direction="col" align="stretch" gap={6}> <Group direction="col" align="stretch" gap={6}>
<Input <Input
label="Team Name *" label="Team Name *"
@@ -142,6 +143,6 @@ export function CreateTeamForm({ onCancel, onSuccess, onNavigate }: CreateTeamFo
)} )}
</Group> </Group>
</Group> </Group>
</form> </Box>
); );
} }

View File

@@ -19,8 +19,7 @@ import { SectionHeader } from '@/ui/SectionHeader';
import { Box } from '@/ui/Box'; import { Box } from '@/ui/Box';
import { Group } from '@/ui/Group'; import { Group } from '@/ui/Group';
import { Stack } from '@/ui/Stack'; import { Stack } from '@/ui/Stack';
import { useState } from 'react'; import React, { useState } from 'react';
import React from 'react';
interface TeamAdminProps { interface TeamAdminProps {
team: { team: {

View File

@@ -10,7 +10,13 @@ export function useForgotPassword(
const authService = useInject(AUTH_SERVICE_TOKEN); const authService = useInject(AUTH_SERVICE_TOKEN);
return useMutation<{ message: string; magicLink?: string }, ApiError, ForgotPasswordDTO>({ return useMutation<{ message: string; magicLink?: string }, ApiError, ForgotPasswordDTO>({
mutationFn: (params) => authService.forgotPassword(params), mutationFn: async (params) => {
const result = await authService.forgotPassword(params);
if (result.isErr()) {
throw result.getError();
}
return result.unwrap();
},
...options, ...options,
}); });
} }

View File

@@ -11,7 +11,13 @@ export function useLogin(
const authService = useInject(AUTH_SERVICE_TOKEN); const authService = useInject(AUTH_SERVICE_TOKEN);
return useMutation<SessionViewModel, ApiError, LoginParamsDTO>({ return useMutation<SessionViewModel, ApiError, LoginParamsDTO>({
mutationFn: (params) => authService.login(params), mutationFn: async (params) => {
const result = await authService.login(params);
if (result.isErr()) {
throw result.getError();
}
return result.unwrap();
},
...options, ...options,
}); });
} }

View File

@@ -10,7 +10,13 @@ export function useResetPassword(
const authService = useInject(AUTH_SERVICE_TOKEN); const authService = useInject(AUTH_SERVICE_TOKEN);
return useMutation<{ message: string }, ApiError, ResetPasswordDTO>({ return useMutation<{ message: string }, ApiError, ResetPasswordDTO>({
mutationFn: (params) => authService.resetPassword(params), mutationFn: async (params) => {
const result = await authService.resetPassword(params);
if (result.isErr()) {
throw result.getError();
}
return result.unwrap();
},
...options, ...options,
}); });
} }

View File

@@ -11,7 +11,13 @@ export function useSignup(
const authService = useInject(AUTH_SERVICE_TOKEN); const authService = useInject(AUTH_SERVICE_TOKEN);
return useMutation<SessionViewModel, ApiError, SignupParamsDTO>({ return useMutation<SessionViewModel, ApiError, SignupParamsDTO>({
mutationFn: (params) => authService.signup(params), mutationFn: async (params) => {
const result = await authService.signup(params);
if (result.isErr()) {
throw result.getError();
}
return result.unwrap();
},
...options, ...options,
}); });
} }

View File

@@ -17,7 +17,10 @@ export function useDriverProfilePageData(driverId: string) {
queryFn: async () => { queryFn: async () => {
if (!driverId) return []; if (!driverId) return [];
const allTeams = await teamService.getAllTeams(); const allTeamsResult = await teamService.getAllTeams();
if (allTeamsResult.isErr()) return [];
const allTeams = allTeamsResult.unwrap();
let teamMemberships: Array<{ let teamMemberships: Array<{
team: { id: string; name: string }; team: { id: string; name: string };
role: string; role: string;
@@ -25,7 +28,10 @@ export function useDriverProfilePageData(driverId: string) {
}> = []; }> = [];
for (const team of allTeams) { for (const team of allTeams) {
const teamMembers = await teamService.getTeamMembers(team.id, driverId, ''); const teamMembersResult = await teamService.getTeamMembers(team.id, driverId, '');
if (teamMembersResult.isErr()) continue;
const teamMembers = teamMembersResult.unwrap();
const membership = teamMembers?.find(member => member.driverId === driverId); const membership = teamMembers?.find(member => member.driverId === driverId);
if (membership) { if (membership) {
teamMemberships.push({ teamMemberships.push({

View File

@@ -5,14 +5,14 @@ import { ApiError } from '@/lib/api/base/ApiError';
import { CreateLeagueInputDTO } from '@/lib/types/generated/CreateLeagueInputDTO'; import { CreateLeagueInputDTO } from '@/lib/types/generated/CreateLeagueInputDTO';
import { CreateLeagueOutputDTO } from '@/lib/types/generated/CreateLeagueOutputDTO'; import { CreateLeagueOutputDTO } from '@/lib/types/generated/CreateLeagueOutputDTO';
interface CreateLeagueInput { export interface CreateLeagueInput {
name: string; name: string;
description: string; description: string;
maxDrivers: number; maxDrivers: number;
scoringPresetId: string; scoringPresetId: string;
} }
interface CreateLeagueResult { export interface CreateLeagueResult {
success: boolean; success: boolean;
leagueId: string; leagueId: string;
error?: string; error?: string;

View File

@@ -22,9 +22,13 @@ export function useLeagueDetail({ leagueId, queryOptions }: UseLeagueDetailOptio
return useQuery<LeagueWithCapacityAndScoringDTO, ApiError>({ return useQuery<LeagueWithCapacityAndScoringDTO, ApiError>({
queryKey: ['league-detail', leagueId], queryKey: ['league-detail', leagueId],
queryFn: async () => { queryFn: async () => {
const result = await leagueService.getAllLeagues() as AllLeaguesWithCapacityAndScoringDTO; const result = await leagueService.getAllLeagues();
if (result.isErr()) {
throw result.getError();
}
const data = result.unwrap();
// Filter for the specific league // Filter for the specific league
const leagues = Array.isArray(result?.leagues) ? result.leagues : []; const leagues = Array.isArray(data?.leagues) ? data.leagues : [];
const league = leagues.find(l => l.id === leagueId); const league = leagues.find(l => l.id === leagueId);
if (!league) { if (!league) {
throw new ApiError('League not found', 'NOT_FOUND', { throw new ApiError('League not found', 'NOT_FOUND', {
@@ -46,8 +50,11 @@ export function useLeagueMemberships({ leagueId, queryOptions }: UseLeagueMember
queryKey: ['league-memberships', leagueId], queryKey: ['league-memberships', leagueId],
queryFn: async () => { queryFn: async () => {
const result = await leagueService.getLeagueMemberships(leagueId); const result = await leagueService.getLeagueMemberships(leagueId);
if (result.isErr()) {
throw result.getError();
}
// The DTO already has the correct structure with members property // The DTO already has the correct structure with members property
return result; return result.unwrap();
}, },
...queryOptions, ...queryOptions,
}); });

View File

@@ -9,7 +9,7 @@ export function useLeagueSponsorshipsPageData(leagueId: string, currentDriverId:
return usePageDataMultiple({ return usePageDataMultiple({
league: { league: {
queryKey: ['leagueDetail', leagueId, currentDriverId], queryKey: ['leagueDetail', leagueId, currentDriverId],
queryFn: () => leagueService.getLeagueDetail(leagueId), queryFn: () => leagueService.getLeagueDetailData(leagueId),
}, },
membership: { membership: {
queryKey: ['leagueMembership', leagueId, currentDriverId], queryKey: ['leagueMembership', leagueId, currentDriverId],

View File

@@ -17,25 +17,24 @@ export function useLeagueWalletPageData(leagueId: string) {
// Transform DTO to ViewModel at client boundary // Transform DTO to ViewModel at client boundary
const transactions = dto.transactions.map(t => new WalletTransactionViewModel({ const transactions = dto.transactions.map(t => new WalletTransactionViewModel({
id: t.id, id: t.id,
type: t.type, type: t.type as any,
description: t.description, description: t.description,
amount: t.amount, amount: t.amount,
fee: t.fee, fee: 0,
netAmount: t.netAmount, netAmount: t.amount,
date: new globalThis.Date(t.date), date: new globalThis.Date(t.createdAt),
status: t.status, status: t.status,
reference: t.reference,
})); }));
return new LeagueWalletViewModel({ return new LeagueWalletViewModel({
balance: dto.balance, balance: dto.balance,
currency: dto.currency, currency: dto.currency,
totalRevenue: dto.totalRevenue, totalRevenue: dto.balance, // Fallback
totalFees: dto.totalFees, totalFees: 0,
totalWithdrawals: dto.totalWithdrawals, totalWithdrawals: 0,
pendingPayouts: dto.pendingPayouts, pendingPayouts: 0,
transactions, transactions,
canWithdraw: dto.canWithdraw, canWithdraw: true,
withdrawalBlockReason: dto.withdrawalBlockReason, withdrawalBlockReason: undefined,
}); });
}, },
enabled: !!leagueId, enabled: !!leagueId,

View File

@@ -11,4 +11,22 @@ export class MedalDisplay {
static getMedalIcon(position: number): string | null { static getMedalIcon(position: number): string | null {
return position <= 3 ? '🏆' : null; return position <= 3 ? '🏆' : null;
} }
static getBg(position: number): string {
switch (position) {
case 1: return 'bg-warning-amber';
case 2: return 'bg-gray-300';
case 3: return 'bg-orange-700';
default: return 'bg-gray-800';
}
}
static getColor(position: number): string {
switch (position) {
case 1: return 'text-warning-amber';
case 2: return 'text-gray-300';
case 3: return 'text-orange-700';
default: return 'text-gray-400';
}
}
} }

View File

@@ -52,4 +52,22 @@ export class SponsorService implements Service {
async getSponsorshipPricing(): Promise<any> { async getSponsorshipPricing(): Promise<any> {
return this.apiClient.getPricing(); return this.apiClient.getPricing();
} }
async getAvailableLeagues(): Promise<Result<any[], DomainError>> {
try {
const data = await this.apiClient.getAvailableLeagues();
return Result.ok(data);
} catch (error) {
return Result.err({ type: 'serverError', message: error instanceof Error ? error.message : 'Unknown error' });
}
}
async getLeagueDetail(leagueId: string): Promise<Result<any, DomainError>> {
try {
const data = await this.apiClient.getLeagueDetail(leagueId);
return Result.ok(data);
} catch (error) {
return Result.err({ type: 'serverError', message: error instanceof Error ? error.message : 'Unknown error' });
}
}
} }

View File

@@ -1,7 +1,7 @@
// Export the DTO type that WalletTransactionViewModel expects // Export the DTO type that WalletTransactionViewModel expects
export type FullTransactionDto = { export type FullTransactionDto = {
id: string; id: string;
type: 'sponsorship' | 'membership' | 'withdrawal' | 'prize'; type: 'sponsorship' | 'membership' | 'withdrawal' | 'prize' | 'deposit';
description: string; description: string;
amount: number; amount: number;
fee: number; fee: number;
@@ -13,7 +13,7 @@ export type FullTransactionDto = {
export class WalletTransactionViewModel { export class WalletTransactionViewModel {
id: string; id: string;
type: 'sponsorship' | 'membership' | 'withdrawal' | 'prize'; type: 'sponsorship' | 'membership' | 'withdrawal' | 'prize' | 'deposit';
description: string; description: string;
amount: number; amount: number;
fee: number; fee: number;

View File

@@ -7,6 +7,7 @@ const __dirname = path.dirname(fileURLToPath(import.meta.url));
const nextConfig = { const nextConfig = {
reactStrictMode: true, reactStrictMode: true,
output: 'standalone',
// Fix for monorepos: point tracing to repo root (portable across machines/containers) // Fix for monorepos: point tracing to repo root (portable across machines/containers)
outputFileTracingRoot: path.join(__dirname, '../..'), outputFileTracingRoot: path.join(__dirname, '../..'),
images: { images: {

View File

@@ -8,12 +8,13 @@ import {
SharedLink, SharedLink,
SharedText, SharedText,
SharedStack, SharedStack,
SharedContainer SharedContainer,
SharedIcon
} from '@/components/shared/UIComponents'; } from '@/components/shared/UIComponents';
import { ChevronRight } from 'lucide-react'; import { ChevronRight } from 'lucide-react';
import { TemplateProps } from '@/lib/contracts/components/ComponentContracts'; import { TemplateProps } from '@/lib/contracts/components/ComponentContracts';
export function LeagueDetailTemplate({ viewData }: TemplateProps<LeagueDetailViewData>) { export function LeagueDetailTemplate({ viewData, children, tabs }: TemplateProps<LeagueDetailViewData> & { children?: React.ReactNode, tabs?: any[] }) {
return ( return (
<SharedContainer size="lg"> <SharedContainer size="lg">
<SharedBox paddingY={8}> <SharedBox paddingY={8}>
@@ -27,11 +28,10 @@ export function LeagueDetailTemplate({ viewData }: TemplateProps<LeagueDetailVie
<SharedText size="sm" color="text-white">{viewData.name}</SharedText> <SharedText size="sm" color="text-white">{viewData.name}</SharedText>
</SharedStack> </SharedStack>
</SharedBox> </SharedBox>
{children}
{/* ... rest of the template ... */} {/* ... rest of the template ... */}
</SharedStack> </SharedStack>
</SharedBox> </SharedBox>
</SharedContainer> </SharedContainer>
); );
} }
import { SharedIcon } from '@/components/shared/UIComponents';

View File

@@ -50,7 +50,7 @@ export function TeamDetailTemplate({
return ( return (
<Box bg="base-black" minH="screen" display="flex" center> <Box bg="base-black" minH="screen" display="flex" center>
<Stack align="center" gap={4}> <Stack align="center" gap={4}>
<Box w="12" h="12" border={4} borderColor="primary-accent" borderOpacity={0.2} borderTop borderTopColor="primary-accent" rounded="full" animate="spin" /> <Box w="12" h="12" border borderColor="primary-accent" borderOpacity={0.2} borderTop borderTopColor="primary-accent" rounded="full" animate="spin" />
<Text color="text-gray-500" font="mono" size="xs" uppercase letterSpacing="widest">Synchronizing Telemetry...</Text> <Text color="text-gray-500" font="mono" size="xs" uppercase letterSpacing="widest">Synchronizing Telemetry...</Text>
</Stack> </Stack>
</Box> </Box>

View File

@@ -5,7 +5,7 @@ import { TeamGrid } from '@/components/teams/TeamGrid';
import { TeamLeaderboardPreview } from '@/components/teams/TeamLeaderboardPreviewWrapper'; import { TeamLeaderboardPreview } from '@/components/teams/TeamLeaderboardPreviewWrapper';
import { TeamsDirectoryHeader } from '@/components/teams/TeamsDirectoryHeader'; import { TeamsDirectoryHeader } from '@/components/teams/TeamsDirectoryHeader';
import { TeamsDirectory, TeamsDirectorySection } from '@/components/teams/TeamsDirectory'; import { TeamsDirectory, TeamsDirectorySection } from '@/components/teams/TeamsDirectory';
import { SharedEmptyState } from '@/components/shared/SharedEmptyState'; import { SharedEmptyState } from '@/components/shared/UIComponents';
import type { TeamsViewData } from '@/lib/view-data/TeamsViewData'; import type { TeamsViewData } from '@/lib/view-data/TeamsViewData';
import { Users } from 'lucide-react'; import { Users } from 'lucide-react';
import { TemplateProps } from '@/lib/contracts/components/ComponentContracts'; import { TemplateProps } from '@/lib/contracts/components/ComponentContracts';

View File

@@ -4,7 +4,7 @@ import { AuthedNav } from '@/components/layout/AuthedNav';
import { PublicNav } from '@/components/layout/PublicNav'; import { PublicNav } from '@/components/layout/PublicNav';
import { useCurrentSession } from '@/hooks/auth/useCurrentSession'; import { useCurrentSession } from '@/hooks/auth/useCurrentSession';
import { Box } from '@/ui/Box'; import { Box } from '@/ui/Box';
import { DashboardRail } from '@/ui/DashboardRail'; import { DashboardRail } from '@/components/dashboard/DashboardRail';
import { Text } from '@/ui/Text'; import { Text } from '@/ui/Text';
import { usePathname } from 'next/navigation'; import { usePathname } from 'next/navigation';

View File

@@ -1,4 +1,4 @@
import { BrandMark } from '@/components/layout/BrandMark'; import { BrandMark } from '@/ui/BrandMark';
import { HeaderActions } from '@/components/layout/HeaderActions'; import { HeaderActions } from '@/components/layout/HeaderActions';
import { PublicNav } from '@/components/layout/PublicNav'; import { PublicNav } from '@/components/layout/PublicNav';
import { useCurrentSession } from '@/hooks/auth/useCurrentSession'; import { useCurrentSession } from '@/hooks/auth/useCurrentSession';

View File

@@ -70,13 +70,17 @@
"include": [ "include": [
"app/", "app/",
"components/", "components/",
"ui/",
"templates/",
"hooks/",
"client-wrapper/",
"lib/", "lib/",
"next-env.d.ts", "next-env.d.ts",
"env.d.ts", "env.d.ts",
"types/", "types/",
"utilities/", "utilities/",
".next/types/**/*.ts" ".next/types/**/*.ts"
, "hooks/sponsor/useSponsorMode.ts" ], ],
"exclude": [ "exclude": [
"**/*.test.ts", "**/*.test.ts",
"**/*.test.tsx", "**/*.test.tsx",

View File

@@ -9,19 +9,35 @@ export interface AccordionProps {
title: string; title: string;
children: ReactNode; children: ReactNode;
defaultOpen?: boolean; defaultOpen?: boolean;
isOpen?: boolean;
onToggle?: () => void;
} }
export const Accordion = ({ export const Accordion = ({
title, title,
children, children,
defaultOpen = false defaultOpen = false,
isOpen: controlledIsOpen,
onToggle
}: AccordionProps) => { }: AccordionProps) => {
const [isOpen, setIsOpen] = useState(defaultOpen); const [internalIsOpen, setInternalIsOpen] = useState(defaultOpen);
const isControlled = controlledIsOpen !== undefined;
const isOpen = isControlled ? controlledIsOpen : internalIsOpen;
const handleToggle = () => {
if (onToggle) {
onToggle();
}
if (!isControlled) {
setInternalIsOpen(!internalIsOpen);
}
};
return ( return (
<Surface variant="muted" rounded="lg" style={{ border: '1px solid var(--ui-color-border-default)', overflow: 'hidden' }}> <Surface variant="muted" rounded="lg" style={{ border: '1px solid var(--ui-color-border-default)', overflow: 'hidden' }}>
<button <button
onClick={() => setIsOpen(!isOpen)} onClick={handleToggle}
className="w-full flex items-center justify-between px-4 py-3 hover:bg-white/5 transition-colors" className="w-full flex items-center justify-between px-4 py-3 hover:bg-white/5 transition-colors"
> >
<Text weight="bold" size="sm" variant="high"> <Text weight="bold" size="sm" variant="high">

View File

@@ -6,37 +6,43 @@ import { Surface } from './Surface';
export interface AvatarProps { export interface AvatarProps {
src?: string; src?: string;
alt?: string; alt?: string;
size?: 'sm' | 'md' | 'lg' | 'xl'; size?: 'sm' | 'md' | 'lg' | 'xl' | number;
fallback?: string; fallback?: string;
className?: string;
} }
export const Avatar = ({ export const Avatar = ({
src, src,
alt, alt,
size = 'md', size = 'md',
fallback fallback,
className
}: AvatarProps) => { }: AvatarProps) => {
const sizeMap = { const sizeMap: Record<string, string> = {
sm: '2rem', sm: '2rem',
md: '3rem', md: '3rem',
lg: '4rem', lg: '4rem',
xl: '6rem' xl: '6rem'
}; };
const iconSizeMap = { const iconSizeMap: Record<string, number> = {
sm: 3, sm: 3,
md: 5, md: 5,
lg: 8, lg: 8,
xl: 12 xl: 12
} as const; };
const finalSize = typeof size === 'number' ? `${size / 4}rem` : sizeMap[size];
const finalIconSize = typeof size === 'number' ? Math.round(size / 8) : iconSizeMap[size];
return ( return (
<Surface <Surface
variant="muted" variant="muted"
rounded="full" rounded="full"
className={className}
style={{ style={{
width: sizeMap[size], width: finalSize,
height: sizeMap[size], height: finalSize,
overflow: 'hidden', overflow: 'hidden',
border: '2px solid var(--ui-color-border-default)' border: '2px solid var(--ui-color-border-default)'
}} }}
@@ -52,7 +58,7 @@ export const Avatar = ({
{fallback ? ( {fallback ? (
<span className="text-sm font-bold text-[var(--ui-color-text-med)]">{fallback}</span> <span className="text-sm font-bold text-[var(--ui-color-text-med)]">{fallback}</span>
) : ( ) : (
<Icon icon={User} size={iconSizeMap[size]} intent="low" /> <Icon icon={User} size={finalIconSize as any} intent="low" />
)} )}
</Box> </Box>
)} )}

View File

@@ -7,17 +7,25 @@ import { Stack } from './Stack';
export interface BadgeProps { export interface BadgeProps {
children: ReactNode; children: ReactNode;
variant?: 'primary' | 'secondary' | 'success' | 'warning' | 'critical' | 'info' | 'outline' | 'default' | 'danger'; variant?: 'primary' | 'secondary' | 'success' | 'warning' | 'critical' | 'info' | 'outline' | 'default' | 'danger';
size?: 'sm' | 'md'; size?: 'xs' | 'sm' | 'md';
style?: React.CSSProperties; style?: React.CSSProperties;
icon?: LucideIcon; icon?: LucideIcon;
rounded?: string;
bg?: string;
color?: string;
borderColor?: string;
} }
export const Badge = ({ export const Badge = ({
children, children,
variant = 'primary', variant = 'primary',
size = 'md', size = 'md',
style, style: styleProp,
icon icon,
rounded,
bg,
color,
borderColor
}: BadgeProps) => { }: BadgeProps) => {
const variantClasses = { const variantClasses = {
primary: 'bg-[var(--ui-color-intent-primary)] text-white', primary: 'bg-[var(--ui-color-intent-primary)] text-white',
@@ -32,16 +40,25 @@ export const Badge = ({
}; };
const sizeClasses = { const sizeClasses = {
xs: 'px-1 py-0 text-[9px]',
sm: 'px-1.5 py-0.5 text-[10px]', sm: 'px-1.5 py-0.5 text-[10px]',
md: 'px-2 py-0.5 text-xs', md: 'px-2 py-0.5 text-xs',
}; };
const classes = [ const classes = [
'inline-flex items-center justify-center font-bold uppercase tracking-wider rounded-none', 'inline-flex items-center justify-center font-bold uppercase tracking-wider',
variantClasses[variant], variantClasses[variant],
sizeClasses[size], sizeClasses[size],
rounded ? (rounded === 'full' ? 'rounded-full' : `rounded-${rounded}`) : 'rounded-none',
].join(' '); ].join(' ');
const style: React.CSSProperties = {
...styleProp,
...(bg ? { backgroundColor: bg.startsWith('bg-') ? undefined : bg } : {}),
...(color ? { color: color.startsWith('text-') ? undefined : color } : {}),
...(borderColor ? { borderColor: borderColor.startsWith('border-') ? undefined : borderColor, borderStyle: 'solid', borderWidth: '1px' } : {}),
};
const content = icon ? ( const content = icon ? (
<Stack direction="row" align="center" gap={1}> <Stack direction="row" align="center" gap={1}>
<Icon icon={icon} size={3} /> <Icon icon={icon} size={3} />

View File

@@ -1,21 +1,32 @@
import { Box } from './Box'; import { Box } from './Box';
export interface TabOption {
id: string;
label: string;
icon?: React.ReactNode;
count?: number;
}
export interface BorderTabsProps { export interface BorderTabsProps {
tabs: TabOption[]; tabs: TabOption[];
activeTabId: string; activeTabId?: string;
onTabChange: (id: string) => void; onTabChange: (id: string) => void;
activeTab?: string;
} }
export const BorderTabs = ({ export const BorderTabs = ({
tabs, tabs,
activeTabId, activeTabId,
onTabChange onTabChange,
activeTab
}: BorderTabsProps) => { }: BorderTabsProps) => {
const finalActiveTabId = activeTabId || activeTab || '';
return ( return (
<Box display="flex" borderBottom> <Box display="flex" borderBottom>
{tabs.map((tab) => { {tabs.map((tab) => {
const isActive = tab.id === activeTabId; const isActive = tab.id === finalActiveTabId;
return ( return (
<button <button
key={tab.id} key={tab.id}
@@ -29,6 +40,11 @@ export const BorderTabs = ({
<Box display="flex" alignItems="center" gap={2}> <Box display="flex" alignItems="center" gap={2}>
{tab.icon} {tab.icon}
{tab.label} {tab.label}
{tab.count !== undefined && (
<Box as="span" px={1.5} py={0.5} rounded="full" bg="white/10" fontSize="10px">
{tab.count}
</Box>
)}
</Box> </Box>
{isActive && ( {isActive && (
<Box <Box

View File

@@ -52,12 +52,12 @@ export interface BoxProps<T extends ElementType> {
// Aliases (Deprecated - use full names) // Aliases (Deprecated - use full names)
m?: Spacing | ResponsiveSpacing; m?: Spacing | ResponsiveSpacing;
mt?: Spacing | ResponsiveSpacing; mt?: Spacing | 'auto' | ResponsiveSpacing;
mb?: Spacing | ResponsiveSpacing; mb?: Spacing | 'auto' | ResponsiveSpacing;
ml?: Spacing | 'auto' | ResponsiveSpacing; ml?: Spacing | 'auto' | ResponsiveSpacing;
mr?: Spacing | 'auto' | ResponsiveSpacing; mr?: Spacing | 'auto' | ResponsiveSpacing;
mx?: Spacing | 'auto' | ResponsiveSpacing; mx?: Spacing | 'auto' | ResponsiveSpacing;
my?: Spacing | ResponsiveSpacing; my?: Spacing | 'auto' | ResponsiveSpacing;
p?: Spacing | ResponsiveSpacing; p?: Spacing | ResponsiveSpacing;
pt?: Spacing | ResponsiveSpacing; pt?: Spacing | ResponsiveSpacing;
pb?: Spacing | ResponsiveSpacing; pb?: Spacing | ResponsiveSpacing;
@@ -216,6 +216,19 @@ export interface BoxProps<T extends ElementType> {
backgroundSize?: string; backgroundSize?: string;
backgroundPosition?: string; backgroundPosition?: string;
backgroundImage?: string; backgroundImage?: string;
htmlFor?: string;
minH?: string | number | ResponsiveValue<string | number>;
borderOpacity?: number;
rows?: number;
backgroundColor?: string;
outline?: string;
focusBorderColor?: string;
href?: string;
name?: string;
borderTopColor?: string;
onPointerDown?: React.PointerEventHandler<any>;
onPointerMove?: React.PointerEventHandler<any>;
onPointerUp?: React.PointerEventHandler<any>;
} }
export const Box = forwardRef(<T extends ElementType = 'div'>( export const Box = forwardRef(<T extends ElementType = 'div'>(
@@ -354,6 +367,19 @@ export const Box = forwardRef(<T extends ElementType = 'div'>(
backgroundSize, backgroundSize,
backgroundPosition, backgroundPosition,
backgroundImage, backgroundImage,
htmlFor,
minH,
borderOpacity,
rows,
backgroundColor,
outline,
focusBorderColor,
href,
name,
borderTopColor,
onPointerDown,
onPointerMove,
onPointerUp,
...props ...props
}: BoxProps<T>, }: BoxProps<T>,
ref: ForwardedRef<HTMLElement> ref: ForwardedRef<HTMLElement>
@@ -417,7 +443,7 @@ export const Box = forwardRef(<T extends ElementType = 'div'>(
getResponsiveClasses('max-w', maxWidth), getResponsiveClasses('max-w', maxWidth),
getResponsiveClasses('min-w', minWidth), getResponsiveClasses('min-w', minWidth),
getResponsiveClasses('max-h', maxHeight), getResponsiveClasses('max-h', maxHeight),
getResponsiveClasses('min-h', minHeight), getResponsiveClasses('min-h', minHeight || minH),
getResponsiveClasses('', display), getResponsiveClasses('', display),
center ? 'flex items-center justify-center' : '', center ? 'flex items-center justify-center' : '',
overflow ? (overflow.includes(':') ? overflow : `overflow-${overflow}`) : '', overflow ? (overflow.includes(':') ? overflow : `overflow-${overflow}`) : '',
@@ -464,6 +490,7 @@ export const Box = forwardRef(<T extends ElementType = 'div'>(
...(typeof minWidth === 'string' ? { minWidth } : {}), ...(typeof minWidth === 'string' ? { minWidth } : {}),
...(typeof maxHeight === 'string' ? { maxHeight } : {}), ...(typeof maxHeight === 'string' ? { maxHeight } : {}),
...(typeof minHeight === 'string' ? { minHeight } : {}), ...(typeof minHeight === 'string' ? { minHeight } : {}),
...(typeof minH === 'string' || typeof minH === 'number' ? { minHeight: minH } : {}),
...(aspectRatio ? { aspectRatio } : {}), ...(aspectRatio ? { aspectRatio } : {}),
...(typeof top === 'string' || typeof top === 'number' ? { top } : {}), ...(typeof top === 'string' || typeof top === 'number' ? { top } : {}),
...(typeof right === 'string' || typeof right === 'number' ? { right } : {}), ...(typeof right === 'string' || typeof right === 'number' ? { right } : {}),
@@ -473,7 +500,9 @@ export const Box = forwardRef(<T extends ElementType = 'div'>(
...(typeof borderBottom === 'string' ? { borderBottom } : (borderBottom === true ? { borderBottom: '1px solid var(--ui-color-border-default)' } : {})), ...(typeof borderBottom === 'string' ? { borderBottom } : (borderBottom === true ? { borderBottom: '1px solid var(--ui-color-border-default)' } : {})),
...(typeof borderLeft === 'string' ? { borderLeft } : (borderLeft === true ? { borderLeft: '1px solid var(--ui-color-border-default)' } : {})), ...(typeof borderLeft === 'string' ? { borderLeft } : (borderLeft === true ? { borderLeft: '1px solid var(--ui-color-border-default)' } : {})),
...(typeof borderRight === 'string' ? { borderRight } : (borderRight === true ? { borderRight: '1px solid var(--ui-color-border-default)' } : {})), ...(typeof borderRight === 'string' ? { borderRight } : (borderRight === true ? { borderRight: '1px solid var(--ui-color-border-default)' } : {})),
...(bg ? { background: bg.startsWith('bg-') ? undefined : bg } : {}), ...(borderTopColor ? { borderTopColor: borderTopColor.startsWith('border-') ? undefined : borderTopColor } : {}),
...(borderOpacity !== undefined ? { borderOpacity } : {}),
...(bg || backgroundColor ? { background: (bg || backgroundColor)!.startsWith('bg-') ? undefined : (bg || backgroundColor) } : {}),
...(rounded === true ? { borderRadius: 'var(--ui-radius-md)' } : (typeof rounded === 'string' ? { borderRadius: rounded.includes('rem') || rounded.includes('px') ? rounded : `var(--ui-radius-${rounded})` } : {})), ...(rounded === true ? { borderRadius: 'var(--ui-radius-md)' } : (typeof rounded === 'string' ? { borderRadius: rounded.includes('rem') || rounded.includes('px') ? rounded : `var(--ui-radius-${rounded})` } : {})),
...(borderColor ? { borderColor: borderColor.startsWith('border-') ? undefined : borderColor } : {}), ...(borderColor ? { borderColor: borderColor.startsWith('border-') ? undefined : borderColor } : {}),
...(border === true ? { border: '1px solid var(--ui-color-border-default)' } : (typeof border === 'string' ? { border } : {})), ...(border === true ? { border: '1px solid var(--ui-color-border-default)' } : (typeof border === 'string' ? { border } : {})),
@@ -531,6 +560,13 @@ export const Box = forwardRef(<T extends ElementType = 'div'>(
muted={muted} muted={muted}
playsInline={playsInline} playsInline={playsInline}
viewBox={viewBox} viewBox={viewBox}
htmlFor={htmlFor}
rows={rows}
href={href}
name={name}
onPointerDown={onPointerDown}
onPointerMove={onPointerMove}
onPointerUp={onPointerUp}
{...props} {...props}
> >
{children} {children}

View File

@@ -19,7 +19,32 @@ export interface ButtonProps {
rel?: string; rel?: string;
title?: string; title?: string;
style?: React.CSSProperties; style?: React.CSSProperties;
rounded?: boolean; rounded?: boolean | string;
className?: string;
bg?: string;
color?: string;
w?: string | number;
h?: string | number;
px?: number;
borderColor?: string;
mt?: number;
position?: 'static' | 'relative' | 'absolute' | 'fixed' | 'sticky';
letterSpacing?: string;
hoverBorderColor?: string;
fontSize?: string;
p?: number;
minHeight?: string;
transition?: boolean;
ring?: string;
transform?: string;
hoverScale?: boolean;
overflow?: string;
borderWidth?: string;
aspectRatio?: string;
border?: boolean;
shadow?: string;
display?: string;
center?: boolean;
} }
export const Button = forwardRef<HTMLButtonElement | HTMLAnchorElement, ButtonProps>(({ export const Button = forwardRef<HTMLButtonElement | HTMLAnchorElement, ButtonProps>(({
@@ -39,8 +64,34 @@ export const Button = forwardRef<HTMLButtonElement | HTMLAnchorElement, ButtonPr
title, title,
style: styleProp, style: styleProp,
rounded = false, rounded = false,
className,
bg,
color,
w,
h,
px,
borderColor,
mt,
position,
letterSpacing,
hoverBorderColor,
fontSize,
p,
minHeight,
transition,
ring,
transform,
hoverScale,
overflow,
borderWidth,
aspectRatio,
border,
shadow,
display,
center,
}, ref) => { }, ref) => {
const baseClasses = 'inline-flex items-center justify-center transition-all duration-150 ease-in-out focus-visible:outline focus-visible:outline-1 focus-visible:outline-offset-2 active:opacity-80 uppercase tracking-widest font-bold'; const baseClasses = 'inline-flex items-center justify-center focus-visible:outline focus-visible:outline-1 focus-visible:outline-offset-2 active:opacity-80 uppercase tracking-widest font-bold';
const transitionClasses = transition !== false ? 'transition-all duration-150 ease-in-out' : '';
const variantClasses = { const variantClasses = {
primary: 'bg-[var(--ui-color-intent-primary)] text-white hover:opacity-90 focus-visible:outline-[var(--ui-color-intent-primary)] shadow-[0_0_15px_rgba(25,140,255,0.3)] hover:shadow-[0_0_25px_rgba(25,140,255,0.5)]', primary: 'bg-[var(--ui-color-intent-primary)] text-white hover:opacity-90 focus-visible:outline-[var(--ui-color-intent-primary)] shadow-[0_0_15px_rgba(25,140,255,0.3)] hover:shadow-[0_0_25px_rgba(25,140,255,0.5)]',
@@ -60,17 +111,45 @@ export const Button = forwardRef<HTMLButtonElement | HTMLAnchorElement, ButtonPr
const disabledClasses = (disabled || isLoading) ? 'opacity-40 cursor-not-allowed' : 'cursor-pointer'; const disabledClasses = (disabled || isLoading) ? 'opacity-40 cursor-not-allowed' : 'cursor-pointer';
const widthClasses = fullWidth ? 'w-full' : ''; const widthClasses = fullWidth ? 'w-full' : '';
const roundedClasses = rounded ? 'rounded-full' : 'rounded-none'; const roundedClasses = rounded === true ? 'rounded-full' : (typeof rounded === 'string' ? `rounded-${rounded}` : 'rounded-none');
const classes = [ const classes = [
baseClasses, baseClasses,
transitionClasses,
variantClasses[variant], variantClasses[variant],
sizeClasses[size], sizeClasses[size],
disabledClasses, disabledClasses,
widthClasses, widthClasses,
roundedClasses, roundedClasses,
ring,
hoverScale ? 'hover:scale-105' : '',
display === 'flex' ? 'flex' : '',
center ? 'items-center justify-center' : '',
className,
].filter(Boolean).join(' '); ].filter(Boolean).join(' ');
const style: React.CSSProperties = {
...styleProp,
...(bg ? { backgroundColor: bg.startsWith('bg-') ? undefined : bg } : {}),
...(color ? { color: color.startsWith('text-') ? undefined : color } : {}),
...(w ? { width: w } : {}),
...(h ? { height: h } : {}),
...(px ? { paddingLeft: `${px * 0.25}rem`, paddingRight: `${px * 0.25}rem` } : {}),
...(borderColor ? { borderColor: borderColor.startsWith('border-') ? undefined : borderColor, borderStyle: 'solid', borderWidth: '1px' } : {}),
...(mt ? { marginTop: `${mt * 0.25}rem` } : {}),
...(position ? { position } : {}),
...(letterSpacing ? { letterSpacing } : {}),
...(fontSize ? { fontSize } : {}),
...(p !== undefined ? { padding: `${p * 0.25}rem` } : {}),
...(minHeight ? { minHeight } : {}),
...(transform ? { transform } : {}),
...(overflow ? { overflow } : {}),
...(borderWidth ? { borderWidth } : {}),
...(aspectRatio ? { aspectRatio } : {}),
...(border ? { border: '1px solid var(--ui-color-border-default)' } : {}),
...(shadow ? { boxShadow: shadow.startsWith('shadow-') ? undefined : shadow } : {}),
};
const content = ( const content = (
<Box display="flex" alignItems="center" gap={2}> <Box display="flex" alignItems="center" gap={2}>
{isLoading && <Icon icon={Loader2} size={size === 'sm' ? 3 : 4} animate="spin" />} {isLoading && <Icon icon={Loader2} size={size === 'sm' ? 3 : 4} animate="spin" />}
@@ -88,7 +167,7 @@ export const Button = forwardRef<HTMLButtonElement | HTMLAnchorElement, ButtonPr
rel={rel} rel={rel}
className={classes} className={classes}
onClick={onClick as MouseEventHandler<HTMLAnchorElement>} onClick={onClick as MouseEventHandler<HTMLAnchorElement>}
style={styleProp} style={style}
title={title} title={title}
> >
{content} {content}
@@ -103,7 +182,7 @@ export const Button = forwardRef<HTMLButtonElement | HTMLAnchorElement, ButtonPr
className={classes} className={classes}
onClick={onClick as MouseEventHandler<HTMLButtonElement>} onClick={onClick as MouseEventHandler<HTMLButtonElement>}
disabled={disabled || isLoading} disabled={disabled || isLoading}
style={styleProp} style={style}
title={title} title={title}
> >
{content} {content}

View File

@@ -8,7 +8,9 @@ export interface CategoryIconProps {
} }
export const CategoryIcon = ({ export const CategoryIcon = ({
category}: CategoryIconProps) => { category,
size = 24
}: CategoryIconProps) => {
// Map categories to icons if needed, for now just use Tag // Map categories to icons if needed, for now just use Tag
return ( return (
<Box <Box

View File

@@ -4,11 +4,17 @@ import { Box } from './Box';
export interface ContainerProps { export interface ContainerProps {
children: ReactNode; children: ReactNode;
size?: 'sm' | 'md' | 'lg' | 'xl' | 'full'; size?: 'sm' | 'md' | 'lg' | 'xl' | 'full';
py?: number;
position?: 'static' | 'relative' | 'absolute' | 'fixed' | 'sticky';
zIndex?: number;
} }
export const Container = ({ export const Container = ({
children, children,
size = 'lg' size = 'lg',
py,
position,
zIndex
}: ContainerProps) => { }: ContainerProps) => {
const sizeMap = { const sizeMap = {
sm: '40rem', sm: '40rem',
@@ -19,7 +25,7 @@ export const Container = ({
}; };
return ( return (
<Box marginX="auto" maxWidth={sizeMap[size]} paddingX={4} fullWidth> <Box marginX="auto" maxWidth={sizeMap[size]} paddingX={4} py={py as any} fullWidth position={position} zIndex={zIndex}>
{children} {children}
</Box> </Box>
); );

View File

@@ -5,11 +5,13 @@ import { Container } from './Container';
export interface ContentViewportProps { export interface ContentViewportProps {
children: ReactNode; children: ReactNode;
padding?: 'none' | 'sm' | 'md' | 'lg'; padding?: 'none' | 'sm' | 'md' | 'lg';
fullWidth?: boolean;
} }
export const ContentViewport = ({ export const ContentViewport = ({
children, children,
padding = 'md' padding = 'md',
fullWidth = false
}: ContentViewportProps) => { }: ContentViewportProps) => {
const paddingMap: Record<string, any> = { const paddingMap: Record<string, any> = {
none: 0, none: 0,
@@ -20,7 +22,7 @@ export const ContentViewport = ({
return ( return (
<Box as="main" flex={1} overflow="auto"> <Box as="main" flex={1} overflow="auto">
<Container size="xl"> <Container size={fullWidth ? 'full' : 'xl'}>
<Box paddingY={paddingMap[padding]}> <Box paddingY={paddingMap[padding]}>
{children} {children}
</Box> </Box>

View File

@@ -59,7 +59,7 @@ export function DashboardHero({
borderColor="border-[#23272B]" borderColor="border-[#23272B]"
> >
<Avatar <Avatar
src={avatarUrl} src={avatarUrl || undefined}
alt={driverName} alt={driverName}
size={120} size={120}
className="rounded-xl" className="rounded-xl"

View File

@@ -8,6 +8,7 @@ import { Card } from '@/ui/Card';
import { Heading } from '@/ui/Heading'; import { Heading } from '@/ui/Heading';
import { Icon } from '@/ui/Icon'; import { Icon } from '@/ui/Icon';
import { Text } from '@/ui/Text'; import { Text } from '@/ui/Text';
import { Copy, RefreshCw, Terminal, X } from 'lucide-react';
import { useEffect, useState } from 'react'; import { useEffect, useState } from 'react';
interface DevErrorPanelProps { interface DevErrorPanelProps {

View File

@@ -1,4 +1,5 @@
import { Avatar } from '@/ui/Avatar'; import { Avatar } from '@/ui/Avatar';
import { Badge } from '@/ui/Badge';
import { Box } from '@/ui/Box'; import { Box } from '@/ui/Box';
import { Link } from '@/ui/Link'; import { Link } from '@/ui/Link';
import { Text } from '@/ui/Text'; import { Text } from '@/ui/Text';

View File

@@ -148,3 +148,7 @@ export function EmptyState({
</Box> </Box>
); );
} }
export function MinimalEmptyState(props: Omit<EmptyStateProps, 'variant'>) {
return <EmptyState {...props} variant="minimal" />;
}

View File

@@ -12,7 +12,8 @@ export interface FilterOption {
export interface FilterGroupProps { export interface FilterGroupProps {
options: FilterOption[]; options: FilterOption[];
activeId: string; activeId: string;
onChange: (id: string) => void; onChange?: (id: string) => void;
onSelect?: (id: string) => void;
label?: string; label?: string;
} }
@@ -20,8 +21,11 @@ export const FilterGroup = ({
options, options,
activeId, activeId,
onChange, onChange,
onSelect,
label label
}: FilterGroupProps) => { }: FilterGroupProps) => {
const finalOnChange = onChange || onSelect || (() => {});
return ( return (
<Box display="flex" flexDirection="col" gap={2}> <Box display="flex" flexDirection="col" gap={2}>
{label && ( {label && (
@@ -37,7 +41,7 @@ export const FilterGroup = ({
key={option.id} key={option.id}
variant={isActive ? 'primary' : 'ghost'} variant={isActive ? 'primary' : 'ghost'}
size="sm" size="sm"
onClick={() => onChange(option.id)} onClick={() => finalOnChange(option.id)}
fullWidth fullWidth
> >
<Box display="flex" alignItems="center" gap={2}> <Box display="flex" alignItems="center" gap={2}>

View File

@@ -1,6 +1,7 @@
import { Box } from './Box';
import { Button, ButtonProps } from './Button'; import { Button, ButtonProps } from './Button';
export interface FloatingActionProps extends ButtonProps { export interface FloatingActionProps extends Omit<ButtonProps, 'position'> {
position?: 'bottom-left' | 'bottom-right' | 'top-left' | 'top-right'; position?: 'bottom-left' | 'bottom-right' | 'top-left' | 'top-right';
title?: string; title?: string;
} }

View File

@@ -4,10 +4,13 @@ import { Text } from './Text';
export interface GoalCardProps { export interface GoalCardProps {
title: string; title: string;
current: number; current?: number;
target: number; target?: number;
unit: string; unit?: string;
icon: string; icon: string;
goalLabel?: string;
currentValue?: number;
maxValue?: number;
} }
export const GoalCard = ({ export const GoalCard = ({
@@ -15,9 +18,15 @@ export const GoalCard = ({
current, current,
target, target,
unit, unit,
icon icon,
goalLabel,
currentValue,
maxValue
}: GoalCardProps) => { }: GoalCardProps) => {
const percentage = Math.min(Math.max((current / target) * 100, 0), 100); const finalCurrent = currentValue ?? current ?? 0;
const finalTarget = maxValue ?? target ?? 100;
const finalUnit = goalLabel ?? unit ?? '';
const percentage = Math.min(Math.max((finalCurrent / finalTarget) * 100, 0), 100);
return ( return (
<Card variant="default"> <Card variant="default">
@@ -25,7 +34,7 @@ export const GoalCard = ({
<Text size="2xl">{icon}</Text> <Text size="2xl">{icon}</Text>
<Box> <Box>
<Text weight="bold" variant="high">{title}</Text> <Text weight="bold" variant="high">{title}</Text>
<Text size="sm" variant="low">{unit}</Text> <Text size="sm" variant="low">{finalUnit}</Text>
</Box> </Box>
</Box> </Box>
@@ -40,7 +49,7 @@ export const GoalCard = ({
</Box> </Box>
<Text size="xs" variant="low"> <Text size="xs" variant="low">
{current} / {target} {unit} {finalCurrent} / {finalTarget} {finalUnit}
</Text> </Text>
</Card> </Card>
); );

View File

@@ -17,6 +17,8 @@ export interface GridProps<T extends ElementType> extends BoxProps<T> {
children?: ReactNode; children?: ReactNode;
cols?: number | ResponsiveValue<number>; cols?: number | ResponsiveValue<number>;
gap?: number | string | ResponsiveValue<number | string>; gap?: number | string | ResponsiveValue<number | string>;
mdCols?: number;
lgCols?: number;
} }
export const Grid = forwardRef(<T extends ElementType = 'div'>( export const Grid = forwardRef(<T extends ElementType = 'div'>(
@@ -25,16 +27,22 @@ export const Grid = forwardRef(<T extends ElementType = 'div'>(
cols = 1, cols = 1,
gap = 4, gap = 4,
as, as,
mdCols,
lgCols,
...props ...props
}: GridProps<T>, }: GridProps<T>,
ref: ForwardedRef<HTMLElement> ref: ForwardedRef<HTMLElement>
) => { ) => {
const finalCols = typeof cols === 'object' ? { ...cols } : { base: cols };
if (mdCols) finalCols.md = mdCols;
if (lgCols) finalCols.lg = lgCols;
return ( return (
<Box <Box
as={as} as={as}
ref={ref} ref={ref}
display="grid" display="grid"
gridCols={cols} gridCols={finalCols}
gap={gap} gap={gap}
{...props} {...props}
> >

View File

@@ -6,6 +6,8 @@ export interface GridItemProps<T extends ElementType> extends BoxProps<T> {
children?: ReactNode; children?: ReactNode;
colSpan?: number | ResponsiveValue<number>; colSpan?: number | ResponsiveValue<number>;
rowSpan?: number | ResponsiveValue<number>; rowSpan?: number | ResponsiveValue<number>;
mdSpan?: number;
lgSpan?: number;
} }
export const GridItem = forwardRef(<T extends ElementType = 'div'>( export const GridItem = forwardRef(<T extends ElementType = 'div'>(
@@ -14,15 +16,21 @@ export const GridItem = forwardRef(<T extends ElementType = 'div'>(
colSpan, colSpan,
rowSpan, rowSpan,
as, as,
mdSpan,
lgSpan,
...props ...props
}: GridItemProps<T>, }: GridItemProps<T>,
ref: ForwardedRef<HTMLElement> ref: ForwardedRef<HTMLElement>
) => { ) => {
const finalColSpan = typeof colSpan === 'object' ? { ...colSpan } : { base: colSpan };
if (mdSpan) finalColSpan.md = mdSpan;
if (lgSpan) finalColSpan.lg = lgSpan;
return ( return (
<Box <Box
as={as} as={as}
ref={ref} ref={ref}
colSpan={colSpan} colSpan={finalColSpan as any}
// rowSpan is not directly supported by Box yet, but we can add it if needed // rowSpan is not directly supported by Box yet, but we can add it if needed
// or use style // or use style
{...props} {...props}

View File

@@ -3,7 +3,7 @@ import { Box } from './Box';
export interface GroupProps { export interface GroupProps {
children: ReactNode; children: ReactNode;
direction?: 'row' | 'col'; direction?: 'row' | 'col' | 'column';
align?: 'start' | 'center' | 'end' | 'stretch' | 'baseline'; align?: 'start' | 'center' | 'end' | 'stretch' | 'baseline';
justify?: 'start' | 'center' | 'end' | 'between' | 'around' | 'evenly'; justify?: 'start' | 'center' | 'end' | 'between' | 'around' | 'evenly';
gap?: number; gap?: number;
@@ -20,10 +20,12 @@ export const Group = ({
wrap = false, wrap = false,
fullWidth = false, fullWidth = false,
}: GroupProps) => { }: GroupProps) => {
const finalDirection = direction === 'column' ? 'col' : direction;
return ( return (
<Box <Box
display="flex" display="flex"
flexDirection={direction} flexDirection={finalDirection}
alignItems={align} alignItems={align}
justifyContent={justify} justifyContent={justify}
gap={gap} gap={gap}

View File

@@ -1,5 +1,5 @@
import { ReactNode, forwardRef } from 'react'; import { ReactNode, forwardRef } from 'react';
import { Box, BoxProps, ResponsiveValue } from './Box'; import { Box, BoxProps, ResponsiveValue, Spacing } from './Box';
export interface HeadingProps extends BoxProps<any> { export interface HeadingProps extends BoxProps<any> {
children: ReactNode; children: ReactNode;
@@ -7,6 +7,11 @@ export interface HeadingProps extends BoxProps<any> {
weight?: 'normal' | 'medium' | 'semibold' | 'bold'; weight?: 'normal' | 'medium' | 'semibold' | 'bold';
align?: 'left' | 'center' | 'right'; align?: 'left' | 'center' | 'right';
fontSize?: string | ResponsiveValue<string>; fontSize?: string | ResponsiveValue<string>;
icon?: ReactNode;
groupHoverColor?: string;
uppercase?: boolean;
letterSpacing?: string;
mb?: Spacing;
} }
export const Heading = forwardRef<HTMLHeadingElement, HeadingProps>(({ export const Heading = forwardRef<HTMLHeadingElement, HeadingProps>(({
@@ -15,6 +20,11 @@ export const Heading = forwardRef<HTMLHeadingElement, HeadingProps>(({
weight = 'bold', weight = 'bold',
align = 'left', align = 'left',
fontSize, fontSize,
icon,
groupHoverColor,
uppercase,
letterSpacing,
mb,
...props ...props
}, ref) => { }, ref) => {
const Tag = `h${level}` as const; const Tag = `h${level}` as const;
@@ -40,11 +50,23 @@ export const Heading = forwardRef<HTMLHeadingElement, HeadingProps>(({
weightClasses[weight], weightClasses[weight],
fontSize ? '' : sizeClasses[level], fontSize ? '' : sizeClasses[level],
align === 'center' ? 'text-center' : (align === 'right' ? 'text-right' : 'text-left'), align === 'center' ? 'text-center' : (align === 'right' ? 'text-right' : 'text-left'),
uppercase ? 'uppercase' : '',
].join(' '); ].join(' ');
return ( return (
<Box as={Tag} ref={ref} className={classes} fontSize={typeof fontSize === 'string' ? fontSize : undefined} {...props}> <Box
{children} as={Tag}
ref={ref}
className={classes}
fontSize={typeof fontSize === 'string' ? fontSize : undefined}
letterSpacing={letterSpacing}
mb={mb}
{...props}
>
<Box display="flex" alignItems="center" gap={2}>
{icon}
{children}
</Box>
</Box> </Box>
); );
}); });

View File

@@ -7,21 +7,31 @@ import { Text } from './Text';
export interface HorizontalStatCardProps { export interface HorizontalStatCardProps {
label: string; label: string;
value: string | number; value: string | number;
icon: LucideIcon; icon: LucideIcon | React.ReactNode;
intent?: 'primary' | 'success' | 'warning' | 'critical' | 'telemetry'; intent?: 'primary' | 'success' | 'warning' | 'critical' | 'telemetry';
subValue?: string;
iconBgColor?: string;
} }
export const HorizontalStatCard = ({ export const HorizontalStatCard = ({
label, label,
value, value,
icon, icon,
intent = 'primary' intent = 'primary',
subValue,
iconBgColor
}: HorizontalStatCardProps) => { }: HorizontalStatCardProps) => {
const isLucideIcon = typeof icon === 'function' || (typeof icon === 'object' && icon !== null && 'render' in icon);
return ( return (
<Card variant="default"> <Card variant="default">
<Box display="flex" alignItems="center" gap={4}> <Box display="flex" alignItems="center" gap={4}>
<Box padding={3} rounded="lg" bg="var(--ui-color-bg-surface-muted)"> <Box padding={3} rounded="lg" bg={iconBgColor || "var(--ui-color-bg-surface-muted)"}>
<Icon icon={icon} size={5} intent={intent} /> {isLucideIcon ? (
<Icon icon={icon as LucideIcon} size={5} intent={intent} />
) : (
icon
)}
</Box> </Box>
<Box> <Box>
<Text size="xs" weight="bold" variant="low" uppercase> <Text size="xs" weight="bold" variant="low" uppercase>
@@ -30,6 +40,11 @@ export const HorizontalStatCard = ({
<Text size="xl" weight="bold" variant="high" block marginTop={0.5}> <Text size="xl" weight="bold" variant="high" block marginTop={0.5}>
{value} {value}
</Text> </Text>
{subValue && (
<Text size="xs" variant="low" block marginTop={0.5}>
{subValue}
</Text>
)}
</Box> </Box>
</Box> </Box>
</Card> </Card>

View File

@@ -5,17 +5,19 @@ export interface HorizontalStatItemProps {
label: string; label: string;
value: string | number; value: string | number;
intent?: 'primary' | 'success' | 'warning' | 'critical' | 'high' | 'med' | 'low'; intent?: 'primary' | 'success' | 'warning' | 'critical' | 'high' | 'med' | 'low';
color?: string;
} }
export const HorizontalStatItem = ({ export const HorizontalStatItem = ({
label, label,
value, value,
intent = 'high' intent = 'high',
color
}: HorizontalStatItemProps) => { }: HorizontalStatItemProps) => {
return ( return (
<Box display="flex" alignItems="center" justifyContent="between" paddingY={2}> <Box display="flex" alignItems="center" justifyContent="between" paddingY={2}>
<Text size="sm" variant="low">{label}</Text> <Text size="sm" variant="low">{label}</Text>
<Text weight="semibold" variant={intent}>{value}</Text> <Text weight="semibold" variant={color ? undefined : intent} style={color ? { color } : undefined}>{value}</Text>
</Box> </Box>
); );
}; };

View File

@@ -10,6 +10,7 @@ export interface ImagePlaceholderProps {
aspectRatio?: string; aspectRatio?: string;
variant?: 'default' | 'loading' | 'error'; variant?: 'default' | 'loading' | 'error';
message?: string; message?: string;
rounded?: string;
} }
export const ImagePlaceholder = ({ export const ImagePlaceholder = ({
@@ -18,7 +19,8 @@ export const ImagePlaceholder = ({
animate = 'pulse', animate = 'pulse',
aspectRatio, aspectRatio,
variant = 'default', variant = 'default',
message message,
rounded
}: ImagePlaceholderProps) => { }: ImagePlaceholderProps) => {
const icons = { const icons = {
default: ImageIcon, default: ImageIcon,
@@ -36,7 +38,7 @@ export const ImagePlaceholder = ({
alignItems="center" alignItems="center"
justifyContent="center" justifyContent="center"
bg="var(--ui-color-bg-surface-muted)" bg="var(--ui-color-bg-surface-muted)"
style={{ borderRadius: 'var(--ui-radius-md)' }} style={{ borderRadius: rounded === 'none' ? '0' : (rounded ? `var(--ui-radius-${rounded})` : 'var(--ui-radius-md)') }}
gap={2} gap={2}
> >
<Icon <Icon

View File

@@ -2,6 +2,7 @@ import { HelpCircle, X } from 'lucide-react';
import React, { ReactNode, useEffect, useRef, useState } from 'react'; import React, { ReactNode, useEffect, useRef, useState } from 'react';
import { createPortal } from 'react-dom'; import { createPortal } from 'react-dom';
import { Box } from './Box'; import { Box } from './Box';
import { Heading } from './Heading';
import { Icon } from './Icon'; import { Icon } from './Icon';
import { IconButton } from './IconButton'; import { IconButton } from './IconButton';
import { Surface } from './Surface'; import { Surface } from './Surface';

View File

@@ -5,21 +5,26 @@ import { Text } from './Text';
export interface InputProps extends Omit<InputHTMLAttributes<HTMLInputElement>, 'size'> { export interface InputProps extends Omit<InputHTMLAttributes<HTMLInputElement>, 'size'> {
label?: string; label?: string;
error?: string; error?: string;
errorMessage?: string;
hint?: string; hint?: string;
fullWidth?: boolean; fullWidth?: boolean;
size?: 'sm' | 'md' | 'lg'; size?: 'sm' | 'md' | 'lg';
icon?: React.ReactNode; icon?: React.ReactNode;
variant?: 'default' | 'error';
} }
export const Input = forwardRef<HTMLInputElement, InputProps>(({ export const Input = forwardRef<HTMLInputElement, InputProps>(({
label, label,
error, error,
errorMessage,
hint, hint,
fullWidth = false, fullWidth = false,
size = 'md', size = 'md',
icon, icon,
variant = 'default',
...props ...props
}, ref) => { }, ref) => {
const finalError = error || errorMessage;
const sizeClasses = { const sizeClasses = {
sm: 'px-3 py-1.5 text-xs', sm: 'px-3 py-1.5 text-xs',
md: 'px-4 py-2 text-sm', md: 'px-4 py-2 text-sm',
@@ -27,7 +32,7 @@ export const Input = forwardRef<HTMLInputElement, InputProps>(({
}; };
const baseClasses = 'bg-[var(--ui-color-bg-surface)] border border-[var(--ui-color-border-default)] text-[var(--ui-color-text-high)] placeholder-[var(--ui-color-text-low)] focus:outline-none focus:border-[var(--ui-color-intent-primary)] transition-colors'; const baseClasses = 'bg-[var(--ui-color-bg-surface)] border border-[var(--ui-color-border-default)] text-[var(--ui-color-text-high)] placeholder-[var(--ui-color-text-low)] focus:outline-none focus:border-[var(--ui-color-intent-primary)] transition-colors';
const errorClasses = error ? 'border-[var(--ui-color-intent-critical)]' : ''; const errorClasses = (finalError || variant === 'error') ? 'border-[var(--ui-color-intent-critical)]' : '';
const widthClasses = fullWidth ? 'w-full' : ''; const widthClasses = fullWidth ? 'w-full' : '';
const classes = [ const classes = [
@@ -59,14 +64,14 @@ export const Input = forwardRef<HTMLInputElement, InputProps>(({
{...props} {...props}
/> />
</Box> </Box>
{error && ( {finalError && (
<Box marginTop={1}> <Box marginTop={1}>
<Text size="xs" variant="critical"> <Text size="xs" variant="critical">
{error} {finalError}
</Text> </Text>
</Box> </Box>
)} )}
{hint && !error && ( {hint && !finalError && (
<Box marginTop={1}> <Box marginTop={1}>
<Text size="xs" variant="low"> <Text size="xs" variant="low">
{hint} {hint}

View File

@@ -1,6 +1,7 @@
import { LucideIcon } from 'lucide-react'; import { LucideIcon } from 'lucide-react';
import { ReactNode } from 'react'; import { ReactNode } from 'react';
import { Box } from './Box'; import { Box } from './Box';
import { Button } from './Button';
import { Icon } from './Icon'; import { Icon } from './Icon';
import { Surface } from './Surface'; import { Surface } from './Surface';
import { Text } from './Text'; import { Text } from './Text';
@@ -12,6 +13,10 @@ export interface LeaderboardPreviewShellProps {
intent?: 'primary' | 'success' | 'warning' | 'critical' | 'telemetry'; intent?: 'primary' | 'success' | 'warning' | 'critical' | 'telemetry';
children: ReactNode; children: ReactNode;
footer?: ReactNode; footer?: ReactNode;
onViewFull?: () => void;
iconColor?: string;
iconBgGradient?: string;
viewFullLabel?: string;
} }
export const LeaderboardPreviewShell = ({ export const LeaderboardPreviewShell = ({
@@ -20,15 +25,19 @@ export const LeaderboardPreviewShell = ({
icon, icon,
intent = 'primary', intent = 'primary',
children, children,
footer footer,
onViewFull,
iconColor,
iconBgGradient,
viewFullLabel
}: LeaderboardPreviewShellProps) => { }: LeaderboardPreviewShellProps) => {
return ( return (
<Surface variant="default" rounded="xl" style={{ border: '1px solid var(--ui-color-border-default)', overflow: 'hidden' }}> <Surface variant="default" rounded="xl" style={{ border: '1px solid var(--ui-color-border-default)', overflow: 'hidden' }}>
<Box padding={6} borderBottom> <Box padding={6} borderBottom>
<Box display="flex" alignItems="center" justifyContent="between" marginBottom={4}> <Box display="flex" alignItems="center" justifyContent="between" marginBottom={4}>
<Box display="flex" alignItems="center" gap={4}> <Box display="flex" alignItems="center" gap={4}>
<Box padding={2} rounded="lg" bg="var(--ui-color-bg-surface-muted)"> <Box padding={2} rounded="lg" bg={iconBgGradient || "var(--ui-color-bg-surface-muted)"} style={iconColor ? { color: iconColor } : undefined}>
<Icon icon={icon} size={5} intent={intent} /> <Icon icon={icon} size={5} intent={iconColor ? undefined : intent} />
</Box> </Box>
<Box> <Box>
<Text size="lg" weight="bold" variant="high" block> <Text size="lg" weight="bold" variant="high" block>
@@ -41,6 +50,11 @@ export const LeaderboardPreviewShell = ({
)} )}
</Box> </Box>
</Box> </Box>
{onViewFull && (
<Button variant="ghost" size="sm" onClick={onViewFull}>
{viewFullLabel || 'View Full'}
</Button>
)}
</Box> </Box>
</Box> </Box>

View File

@@ -4,9 +4,10 @@ import { Surface } from './Surface';
export interface LeaderboardTableShellProps { export interface LeaderboardTableShellProps {
children: ReactNode; children: ReactNode;
columns?: any[];
} }
export const LeaderboardTableShell = ({ children }: LeaderboardTableShellProps) => { export const LeaderboardTableShell = ({ children, columns }: LeaderboardTableShellProps) => {
return ( return (
<Surface variant="default" rounded="xl" style={{ border: '1px solid var(--ui-color-border-default)', overflow: 'hidden' }}> <Surface variant="default" rounded="xl" style={{ border: '1px solid var(--ui-color-border-default)', overflow: 'hidden' }}>
<Box> <Box>

View File

@@ -2,6 +2,7 @@ import { ChevronRight } from 'lucide-react';
import { ReactNode } from 'react'; import { ReactNode } from 'react';
import { Box } from './Box'; import { Box } from './Box';
import { Card } from './Card'; import { Card } from './Card';
import { Icon } from './Icon';
import { Image } from './Image'; import { Image } from './Image';
import { Text } from './Text'; import { Text } from './Text';

View File

@@ -10,6 +10,20 @@ export interface LinkProps extends AnchorHTMLAttributes<HTMLAnchorElement> {
block?: boolean; block?: boolean;
hoverColor?: string; hoverColor?: string;
transition?: boolean; transition?: boolean;
pb?: number;
truncate?: boolean;
hoverTextColor?: string;
display?: string;
alignItems?: string;
gap?: number;
rounded?: string;
bg?: string;
px?: number;
py?: number;
border?: boolean;
borderColor?: string;
shadow?: string;
hoverBorderColor?: string;
} }
export const Link = forwardRef<HTMLAnchorElement, LinkProps>(({ export const Link = forwardRef<HTMLAnchorElement, LinkProps>(({
@@ -22,6 +36,20 @@ export const Link = forwardRef<HTMLAnchorElement, LinkProps>(({
block = false, block = false,
hoverColor, hoverColor,
transition = true, transition = true,
pb,
truncate,
hoverTextColor,
display,
alignItems,
gap,
rounded,
bg,
px,
py,
border,
borderColor,
shadow,
hoverBorderColor,
...props ...props
}, ref) => { }, ref) => {
const variantClasses = { const variantClasses = {
@@ -43,12 +71,24 @@ export const Link = forwardRef<HTMLAnchorElement, LinkProps>(({
block ? 'block' : 'inline', block ? 'block' : 'inline',
variantClasses[variant], variantClasses[variant],
underlineClasses[underline], underlineClasses[underline],
truncate ? 'truncate' : '',
].join(' '); ].join(' ');
const style: React.CSSProperties = { const style: React.CSSProperties = {
...(size ? { fontSize: size } : {}), ...(size ? { fontSize: size } : {}),
...(weight ? { fontWeight: weight } : {}), ...(weight ? { fontWeight: weight } : {}),
...(letterSpacing ? { letterSpacing } : {}), ...(letterSpacing ? { letterSpacing } : {}),
...(pb ? { paddingBottom: `${pb * 0.25}rem` } : {}),
...(display ? { display } : {}),
...(alignItems ? { alignItems } : {}),
...(gap ? { gap: `${gap * 0.25}rem` } : {}),
...(rounded ? { borderRadius: rounded === 'full' ? '9999px' : `var(--ui-radius-${rounded})` } : {}),
...(bg ? { backgroundColor: bg.startsWith('bg-') ? undefined : bg } : {}),
...(px ? { paddingLeft: `${px * 0.25}rem`, paddingRight: `${px * 0.25}rem` } : {}),
...(py ? { paddingTop: `${py * 0.25}rem`, paddingBottom: `${py * 0.25}rem` } : {}),
...(border ? { border: '1px solid var(--ui-color-border-default)' } : {}),
...(borderColor ? { borderColor: borderColor.startsWith('border-') ? undefined : borderColor } : {}),
...(shadow ? { boxShadow: shadow.startsWith('shadow-') ? undefined : shadow } : {}),
}; };
return ( return (

View File

@@ -5,16 +5,18 @@ export interface MiniStatProps {
label: string; label: string;
value: string | number; value: string | number;
intent?: 'primary' | 'success' | 'warning' | 'critical' | 'high' | 'med' | 'low'; intent?: 'primary' | 'success' | 'warning' | 'critical' | 'high' | 'med' | 'low';
color?: string;
} }
export const MiniStat = ({ export const MiniStat = ({
label, label,
value, value,
intent = 'high' intent = 'high',
color
}: MiniStatProps) => { }: MiniStatProps) => {
return ( return (
<Box textAlign="center" padding={2} rounded="lg" bg="var(--ui-color-bg-surface-muted)"> <Box textAlign="center" padding={2} rounded="lg" bg="var(--ui-color-bg-surface-muted)">
<Text size="lg" weight="bold" variant={intent} block>{value}</Text> <Text size="lg" weight="bold" variant={color ? undefined : intent} style={color ? { color } : undefined} block>{value}</Text>
<Text size="xs" variant="low" block style={{ fontSize: '10px' }}>{label}</Text> <Text size="xs" variant="low" block style={{ fontSize: '10px' }}>{label}</Text>
</Box> </Box>
); );

View File

@@ -1,3 +1,4 @@
import { LucideIcon } from 'lucide-react';
import { Box } from './Box'; import { Box } from './Box';
import { Icon } from './Icon'; import { Icon } from './Icon';
import { Text } from './Text'; import { Text } from './Text';

View File

@@ -1,6 +1,9 @@
import { LucideIcon } from 'lucide-react';
import { ReactNode } from 'react'; import { ReactNode } from 'react';
import { Box } from './Box'; import { Box } from './Box';
import { Button } from './Button';
import { Heading } from './Heading'; import { Heading } from './Heading';
import { Icon } from './Icon';
import { Surface } from './Surface'; import { Surface } from './Surface';
import { Text } from './Text'; import { Text } from './Text';
@@ -9,13 +12,22 @@ export interface PageHeroProps {
description?: string; description?: string;
children?: ReactNode; children?: ReactNode;
image?: ReactNode; image?: ReactNode;
icon?: LucideIcon;
actions?: Array<{
label: string;
onClick: () => void;
variant?: 'primary' | 'secondary';
icon?: LucideIcon;
}>;
} }
export const PageHero = ({ export const PageHero = ({
title, title,
description, description,
children, children,
image image,
icon,
actions
}: PageHeroProps) => { }: PageHeroProps) => {
return ( return (
<Surface <Surface
@@ -26,12 +38,33 @@ export const PageHero = ({
> >
<Box display="flex" flexDirection={{ base: 'col', lg: 'row' }} alignItems="center" gap={8}> <Box display="flex" flexDirection={{ base: 'col', lg: 'row' }} alignItems="center" gap={8}>
<Box flex={1}> <Box flex={1}>
<Heading level={1} marginBottom={4}>{title}</Heading> <Box display="flex" alignItems="center" gap={4} marginBottom={4}>
{icon && (
<Box padding={3} rounded="lg" bg="var(--ui-color-bg-surface-muted)">
<Icon icon={icon} size={8} intent="primary" />
</Box>
)}
<Heading level={1}>{title}</Heading>
</Box>
{description && ( {description && (
<Text size="lg" variant="low" marginBottom={6} block> <Text size="lg" variant="low" marginBottom={6} block>
{description} {description}
</Text> </Text>
)} )}
{actions && (
<Box display="flex" gap={4} marginBottom={children ? 6 : 0}>
{actions.map((action) => (
<Button
key={action.label}
variant={action.variant || 'primary'}
onClick={action.onClick}
icon={action.icon ? <Icon icon={action.icon} size={4} /> : undefined}
>
{action.label}
</Button>
))}
</Box>
)}
{children} {children}
</Box> </Box>
{image && ( {image && (

Some files were not shown because too many files have changed in this diff Show More