website refactor

This commit is contained in:
2026-01-20 01:22:05 +01:00
parent f8e7ec7948
commit 30a31dc44f
21 changed files with 1242 additions and 393 deletions

View File

@@ -0,0 +1,20 @@
import { Section } from '@/ui/Section';
import { Heading } from '@/ui/Heading';
import { Text } from '@/ui/Text';
import { Stack } from '@/ui/Stack';
export default function MigrationPage() {
return (
<main>
<Section variant="default" padding="lg">
<Stack gap={6} align="center" textAlign="center">
<Heading level={1} weight="bold">League Migration</Heading>
<Text variant="med" size="lg" maxWidth="36rem">
We are currently preparing our migration tools.
If you want to move your league to GridPilot today, please contact our support team.
</Text>
</Stack>
</Section>
</main>
);
}

View File

@@ -0,0 +1,50 @@
'use client';
import React from 'react';
import { Section } from '@/ui/Section';
import { Heading } from '@/ui/Heading';
import { Text } from '@/ui/Text';
import { Button } from '@/ui/Button';
import { Group } from '@/ui/Group';
import { Box } from '@/ui/Box';
import { Panel } from '@/ui/Panel';
import { Stack } from '@/ui/Stack';
import { Trophy } from 'lucide-react';
/**
* CtaSection - Final call to action for league admins.
* Redesigned to match the "Modern Precision" theme and "Dieter Rams" style.
*/
export function CtaSection() {
return (
<Section variant="default" py={32}>
<Panel variant="muted" padding="xl">
<Stack gap={12} align="center" textAlign="center">
<Box>
<Box marginBottom={6} display="flex" justifyContent="center">
<Box width="3rem" height="3rem" bg="var(--ui-color-bg-surface)" rounded="full" border={true} display="flex" alignItems="center" justifyContent="center">
<Trophy size={24} className="text-[var(--ui-color-intent-primary)]" />
</Box>
</Box>
<Heading level={2} weight="bold" marginBottom={4}>
Ready to elevate your league?
</Heading>
<Text variant="med" size="lg" maxWidth="36rem" mx="auto">
Join the growing ecosystem of professional sim racing leagues.
Start for free and scale as your community grows.
</Text>
</Box>
<Group gap={6} justify="center">
<Button variant="primary" size="lg">
Create Your League
</Button>
<Button variant="secondary" size="lg">
View Documentation
</Button>
</Group>
</Stack>
</Panel>
</Section>
);
}

View File

@@ -0,0 +1,96 @@
'use client';
import React from 'react';
import { Section } from '@/ui/Section';
import { Heading } from '@/ui/Heading';
import { Text } from '@/ui/Text';
import { Card } from '@/ui/Card';
import { Button } from '@/ui/Button';
import { FeatureGrid } from '@/ui/FeatureGrid';
import { Stack } from '@/ui/Stack';
import { Trophy, Users, Flag } from 'lucide-react';
interface EcosystemGridProps {
leagues: Array<{ id: string; name: string; description: string }>;
teams: Array<{ id: string; name: string; description: string }>;
races: Array<{ id: string; track: string; car: string; formattedDate: string }>;
}
/**
* EcosystemGrid - Discovery section for the live ecosystem.
* Designed with a "grid" layout and precision details.
* Uses ONLY UI components.
*/
export function EcosystemGrid({ leagues, teams, races }: EcosystemGridProps) {
return (
<Section variant="muted" padding="lg">
<Stack direction="col" gap={16}>
<Stack direction={{ base: 'col', md: 'row' }} align="end" justify="between" gap={8}>
<Stack direction="col" gap={4} maxWidth="42rem">
<Text variant="primary" weight="bold" uppercase size="xs">
Live Ecosystem
</Text>
<Heading level={2} weight="bold">
THE GRID
</Heading>
<Text size="lg" variant="med" block>
Explore the leagues, teams, and races that define the GridPilot ecosystem.
</Text>
</Stack>
<Button variant="secondary" size="md">
VIEW ALL DATA
</Button>
</Stack>
<FeatureGrid columns={{ base: 1, lg: 3 }} gap={8}>
{/* Leagues Column */}
<Stack direction="col" gap={6}>
<Stack direction="row" align="center" gap={2} px={2}>
<Trophy className="w-4 h-4 text-[var(--ui-color-text-low)]" />
<Text size="sm" weight="bold" uppercase variant="low">Top Leagues</Text>
</Stack>
{leagues.slice(0, 3).map((league) => (
<Card key={league.id} variant="default" padding="md">
<Heading level={5} weight="bold">{league.name}</Heading>
<Text size="sm" variant="low" leading="snug" block mt={2}>{league.description}</Text>
</Card>
))}
</Stack>
{/* Teams Column */}
<Stack direction="col" gap={6}>
<Stack direction="row" align="center" gap={2} px={2}>
<Users className="w-4 h-4 text-[var(--ui-color-text-low)]" />
<Text size="sm" weight="bold" uppercase variant="low">Active Teams</Text>
</Stack>
{teams.slice(0, 3).map((team) => (
<Card key={team.id} variant="default" padding="md">
<Heading level={5} weight="bold">{team.name}</Heading>
<Text size="sm" variant="low" leading="snug" block mt={2}>{team.description}</Text>
</Card>
))}
</Stack>
{/* Races Column */}
<Stack direction="col" gap={6}>
<Stack direction="row" align="center" gap={2} px={2}>
<Flag className="w-4 h-4 text-[var(--ui-color-text-low)]" />
<Text size="sm" weight="bold" uppercase variant="low">Upcoming Races</Text>
</Stack>
{races.slice(0, 3).map((race) => (
<Card key={race.id} variant="default" padding="md">
<Stack direction="row" justify="between" align="start">
<Stack direction="col" gap={1}>
<Heading level={5} weight="bold">{race.track}</Heading>
<Text size="xs" variant="primary" weight="bold" uppercase>{race.car}</Text>
</Stack>
<Text size="xs" variant="low" mono>{race.formattedDate}</Text>
</Stack>
</Card>
))}
</Stack>
</FeatureGrid>
</Stack>
</Section>
);
}

View File

@@ -0,0 +1,65 @@
'use client';
import { Heading } from '@/ui/Heading';
import { Text } from '@/ui/Text';
import { Button } from '@/ui/Button';
import { Section } from '@/ui/Section';
import { ButtonGroup } from '@/ui/ButtonGroup';
import { Stack } from '@/ui/Stack';
import { Box } from '@/ui/Box';
/**
* Hero - Refined with Dieter Rams principles.
* "Less, but better."
* Focuses on clarity, honesty, and unobtrusive design.
*/
export function Hero() {
return (
<Section variant="default" py={32}>
<Box maxWidth="54rem">
<Box marginBottom={24}>
<Text
variant="primary"
weight="bold"
uppercase
size="xs"
leading="none"
block
letterSpacing="0.2em"
marginBottom={10}
>
Sim Racing Infrastructure
</Text>
<Heading level={1} weight="bold" size="4xl">
Professional League Management.<br />
Engineered for Control.
</Heading>
</Box>
<Box marginBottom={24}>
<Text size="xl" variant="med" leading="relaxed" block maxWidth="42rem">
GridPilot eliminates the administrative overhead of running iRacing leagues.
No spreadsheets. No manual points. No protest chaos.
Just pure competition, structured for growth.
</Text>
</Box>
<ButtonGroup gap={10}>
<Button
variant="primary"
size="lg"
>
Create Your League
</Button>
<Button
variant="secondary"
size="lg"
>
View Demo
</Button>
</ButtonGroup>
</Box>
</Section>
);
}

View File

@@ -0,0 +1,151 @@
'use client';
import React from 'react';
import { Section } from '@/ui/Section';
import { Panel } from '@/ui/Panel';
import { Text } from '@/ui/Text';
import { Heading } from '@/ui/Heading';
import { Group } from '@/ui/Group';
import { Stack } from '@/ui/Stack';
import { Grid } from '@/ui/Grid';
import { Box } from '@/ui/Box';
import { StatusBadge } from '@/ui/StatusBadge';
import { Trophy, Globe, Settings2, Palette, ShieldCheck, BarChart3 } from 'lucide-react';
/**
* LeagueIdentityPreview - Radically redesigned for "Modern Precision" and "Dieter Rams" style.
* Focuses on the professional identity and deep customization options for admins.
*/
export function LeagueIdentityPreview() {
return (
<Section variant="default" py={32}>
<Box>
<Box marginBottom={24} maxWidth="42rem">
<Heading level={2} weight="bold" marginBottom={8}>Your Brand. Your Rules.</Heading>
<Text variant="med" size="lg" leading="relaxed">
GridPilot is designed to be invisible where it matters, letting your league's identity take center stage.
Professional tools that respect your community's unique culture.
</Text>
</Box>
<Grid cols={{ base: 1, md: 2 }} gap={20}>
{/* Your Brand - Visual Identity */}
<Box>
<Panel variant="muted" padding="lg">
<Stack gap={8}>
<Stack gap={2}>
<Group gap={2}>
<Palette size={16} className="text-[var(--ui-color-intent-primary)]" />
<Text variant="primary" weight="bold" uppercase size="xs" letterSpacing="0.1em">Your Brand</Text>
</Group>
<Heading level={3} weight="bold">Professional Presence</Heading>
<Text variant="low" size="sm">Build prestige with a dedicated home for your competition.</Text>
</Stack>
<Stack gap={4}>
<Panel variant="bordered" padding="md">
<Group gap={4}>
<Globe size={20} className="text-[var(--ui-color-text-low)]" />
<Stack gap={1}>
<Text weight="bold">Custom Subdomains</Text>
<Text size="xs" variant="low">yourleague.gridpilot.racing</Text>
</Stack>
</Group>
</Panel>
<Panel variant="bordered" padding="md">
<Group gap={4}>
<Trophy size={20} className="text-[var(--ui-color-text-low)]" />
<Stack gap={1}>
<Text weight="bold">Live Standings</Text>
<Text size="xs" variant="low">Real-time updates across all car classes.</Text>
</Stack>
</Group>
</Panel>
<Panel variant="bordered" padding="md">
<Group gap={4}>
<BarChart3 size={20} className="text-[var(--ui-color-text-low)]" />
<Stack gap={1}>
<Text weight="bold">Driver Roster</Text>
<Text size="xs" variant="low">Detailed profiles with lifetime league stats.</Text>
</Stack>
</Group>
</Panel>
<Panel variant="elevated" padding="sm">
<Group gap={3}>
<Box width="2.5rem" height="2.5rem" bg="var(--ui-color-bg-surface)" rounded="sm" border={true} display="flex" alignItems="center" justifyContent="center">
<Trophy size={20} className="text-[var(--ui-color-text-low)]" />
</Box>
<Stack gap={0}>
<Text weight="bold" size="sm">Apex Racing League</Text>
<Text size="xs" variant="low">apex.gridpilot.racing</Text>
</Stack>
</Group>
</Panel>
</Stack>
</Stack>
</Panel>
</Box>
{/* Your Rules - Deep Customization */}
<Box>
<Panel variant="muted" padding="lg">
<Stack gap={8}>
<Stack gap={2}>
<Group gap={2}>
<Settings2 size={16} className="text-[var(--ui-color-intent-primary)]" />
<Text variant="primary" weight="bold" uppercase size="xs" letterSpacing="0.1em">Your Rules</Text>
</Group>
<Heading level={3} weight="bold">Absolute Control</Heading>
<Text variant="low" size="sm">The platform adapts to your league, not the other way around.</Text>
</Stack>
<Stack gap={4}>
<Panel variant="bordered" padding="md">
<Group gap={4}>
<ShieldCheck size={20} className="text-[var(--ui-color-text-low)]" />
<Stack gap={1}>
<Text weight="bold">Flexible Points Systems</Text>
<Text size="xs" variant="low">Custom points for positions, fastest laps, and incidents.</Text>
</Stack>
</Group>
</Panel>
<Panel variant="bordered" padding="md">
<Group gap={4}>
<Settings2 size={20} className="text-[var(--ui-color-text-low)]" />
<Stack gap={1}>
<Text weight="bold">Drop Weeks & Playoffs</Text>
<Text size="xs" variant="low">Configure complex season structures with ease.</Text>
</Stack>
</Group>
</Panel>
<Panel variant="bordered" padding="md">
<Group gap={4}>
<Trophy size={20} className="text-[var(--ui-color-text-low)]" />
<Stack gap={1}>
<Text weight="bold">Multi-Class Support</Text>
<Text size="xs" variant="low">Manage GT3, GTP, and more in a single season.</Text>
</Stack>
</Group>
</Panel>
<Panel variant="elevated" padding="sm">
<Stack gap={2}>
<Text size="xs" uppercase weight="bold" variant="low">Points Configuration</Text>
<Group gap={2} wrap>
<StatusBadge variant="info">P1: 25pts</StatusBadge>
<StatusBadge variant="info">P2: 18pts</StatusBadge>
<StatusBadge variant="info">P3: 15pts</StatusBadge>
<StatusBadge variant="warning">INC: -1pt</StatusBadge>
</Group>
</Stack>
</Panel>
</Stack>
</Stack>
</Panel>
</Box>
</Grid>
</Box>
</Section>
);
}

View File

@@ -0,0 +1,47 @@
'use client';
import React from 'react';
import { Section } from '@/ui/Section';
import { Heading } from '@/ui/Heading';
import { Text } from '@/ui/Text';
import { Button } from '@/ui/Button';
import { Stack } from '@/ui/Stack';
import { Group } from '@/ui/Group';
import { Panel } from '@/ui/Panel';
import { ArrowRight, Database } from 'lucide-react';
import { routes } from '@/lib/routing/RouteConfig';
/**
* MigrationSection - Offers help with migrating existing league data.
*/
export function MigrationSection() {
return (
<Section variant="default" py={32}>
<Panel variant="bordered" padding="xl">
<Group justify="between" align="center" gap={12} wrap>
<Stack gap={6} flex={1} minWidth="20rem">
<Group gap={3}>
<Database size={20} className="text-[var(--ui-color-intent-primary)]" />
<Text variant="primary" weight="bold" uppercase size="xs" letterSpacing="0.1em">League Migration</Text>
</Group>
<Heading level={2} weight="bold">Moving from Sheets or Discord?</Heading>
<Text variant="med" size="lg">
We know that moving years of history is daunting. Send us your data, and we will handle the migration for you for free.
We support CSV, Google Sheets, and manual data entry.
</Text>
</Stack>
<Button
as="a"
href={routes.league.migration}
variant="primary"
size="lg"
icon={<ArrowRight size={18} />}
>
Start Migration
</Button>
</Group>
</Panel>
</Section>
);
}

View File

@@ -0,0 +1,101 @@
'use client';
import React from 'react';
import { Section } from '@/ui/Section';
import { Panel } from '@/ui/Panel';
import { Text } from '@/ui/Text';
import { Heading } from '@/ui/Heading';
import { StatusBadge } from '@/ui/StatusBadge';
import { Button } from '@/ui/Button';
import { Group } from '@/ui/Group';
import { Stack } from '@/ui/Stack';
import { Grid } from '@/ui/Grid';
import { Box } from '@/ui/Box';
import { Gavel, Clock, User, MessageSquare } from 'lucide-react';
/**
* StewardingPreview - Refined for "Modern Precision" and "Dieter Rams" style.
* Thorough down to the last detail.
*/
export function StewardingPreview() {
return (
<Section variant="muted" py={32}>
<Box>
<Box marginBottom={16} maxWidth="42rem">
<Heading level={2} weight="bold" marginBottom={6}>Structured Stewarding</Heading>
<Text variant="med" size="lg" leading="relaxed">
Protests are part of racing. Managing them shouldn't be a second job.
GridPilot provides a dedicated workflow for drivers to report incidents and for you to resolve them with precision.
</Text>
</Box>
<Panel variant="elevated" padding="lg">
<Stack gap={8}>
<Group justify="between" align="center">
<Stack gap={1}>
<Group gap={2}>
<Text variant="low" size="xs" uppercase weight="bold" letterSpacing="0.1em">Incident Report</Text>
<Text variant="low" size="xs"></Text>
<Text variant="low" size="xs" uppercase weight="bold" letterSpacing="0.1em">ID: 402-WG</Text>
</Group>
<Heading level={3} weight="bold">Turn 1 Contact: Miller vs Chen</Heading>
</Stack>
<StatusBadge variant="warning">UNDER REVIEW</StatusBadge>
</Group>
<Grid cols={{ base: 1, md: 3 }} gap={6}>
<Panel variant="bordered" padding="md">
<Stack gap={3}>
<Group gap={2}>
<User size={14} className="text-[var(--ui-color-intent-primary)]" />
<Text size="xs" uppercase weight="bold" variant="low">Protestor</Text>
</Group>
<Text weight="bold">Alex Miller</Text>
<Text size="sm" variant="low">#42 - Porsche 911 GT3 R</Text>
</Stack>
</Panel>
<Panel variant="bordered" padding="md">
<Stack gap={3}>
<Group gap={2}>
<User size={14} className="text-[var(--ui-color-intent-critical)]" />
<Text size="xs" uppercase weight="bold" variant="low">Defendant</Text>
</Group>
<Text weight="bold">David Chen</Text>
<Text size="sm" variant="low">#18 - BMW M4 GT3</Text>
</Stack>
</Panel>
<Panel variant="bordered" padding="md">
<Stack gap={3}>
<Group gap={2}>
<Clock size={14} className="text-[var(--ui-color-text-low)]" />
<Text size="xs" uppercase weight="bold" variant="low">Session Info</Text>
</Group>
<Text weight="bold">Lap 1, 00:42.150</Text>
<Text size="sm" variant="low">Watkins Glen - Cup</Text>
</Stack>
</Panel>
</Grid>
<Stack gap={4}>
<Group gap={2}>
<MessageSquare size={14} className="text-[var(--ui-color-text-low)]" />
<Text size="xs" uppercase weight="bold" variant="low">Description</Text>
</Group>
<Panel variant="muted" padding="md">
<Text size="sm" variant="high" leading="relaxed">
"David missed his braking point into T1 and hit my rear right corner.
I was forced into the grass and lost 4 positions. Replay attached shows he was never alongside."
</Text>
</Panel>
</Stack>
<Group gap={3} justify="end">
<Button variant="secondary" size="md">Dismiss</Button>
<Button variant="primary" size="md" icon={<Gavel size={16} />}>Apply Penalty</Button>
</Group>
</Stack>
</Panel>
</Box>
</Section>
);
}

View File

@@ -0,0 +1,45 @@
'use client';
import React from 'react';
import { StatsStrip } from '@/ui/StatsStrip';
import { Section } from '@/ui/Section';
import { XCircle, CheckCircle2, AlertCircle, ShieldCheck } from 'lucide-react';
/**
* TelemetryStrip - Redesigned as a "Status of Chaos" vs "Status of Order" strip.
* Focuses on the transition from manual work to GridPilot.
*/
export function TelemetryStrip() {
const stats = [
{
label: "NO SPREADSHEETS",
value: "STRUCTURED",
icon: CheckCircle2,
intent: "success" as const
},
{
label: "NO DM PROTESTS",
value: "CENTRALIZED",
icon: ShieldCheck,
intent: "primary" as const
},
{
label: "NO MANUAL POINTS",
value: "AUTOMATED",
icon: CheckCircle2,
intent: "success" as const
},
{
label: "NO ROSTER CHAOS",
value: "MANAGED",
icon: ShieldCheck,
intent: "primary" as const
},
];
return (
<Section variant="default" py={16}>
<StatsStrip stats={stats} />
</Section>
);
}

View File

@@ -0,0 +1,50 @@
'use client';
import React from 'react';
import { Section } from '@/ui/Section';
import { FeatureGrid } from '@/ui/FeatureGrid';
import { FeatureItem } from '@/ui/FeatureItem';
import { Box } from '@/ui/Box';
import { RefreshCw, Gavel, Layout } from 'lucide-react';
/**
* ValuePillars - Redesigned for League Admin features.
* Focuses on solving the core problems of league management.
*/
export function ValuePillars() {
const pillars = [
{
title: "Automatic Results Sync",
description: "Direct iRacing integration. Positions, incidents, and best laps imported instantly. Standings update the moment the race ends.",
icon: RefreshCw,
},
{
title: "Structured Stewarding",
description: "A dedicated review panel for protests. Drivers submit timestamps and clips; you make the call. Points adjust automatically.",
icon: Gavel,
},
{
title: "Professional Presence",
description: "A clean, modern home for your league. Schedules, standings, and rosters that build prestige and attract drivers.",
icon: Layout,
},
];
return (
<Section variant="default" py={32}>
<Box>
<FeatureGrid columns={{ base: 1, md: 3 }} gap={16}>
{pillars.map((pillar) => (
<FeatureItem
key={pillar.title}
title={pillar.title}
description={pillar.description}
icon={pillar.icon}
/>
))}
</FeatureGrid>
</Box>
</Section>
);
}

View File

@@ -71,6 +71,7 @@ export interface RouteGroup {
stewarding: (id: string) => string; stewarding: (id: string) => string;
wallet: (id: string) => string; wallet: (id: string) => string;
create: string; create: string;
migration: string;
}; };
race: { race: {
root: string; root: string;
@@ -169,6 +170,7 @@ export const routes: RouteGroup & { leaderboards: { root: string; drivers: strin
stewarding: (id: string) => `/leagues/${id}/stewarding`, stewarding: (id: string) => `/leagues/${id}/stewarding`,
wallet: (id: string) => `/leagues/${id}/wallet`, wallet: (id: string) => `/leagues/${id}/wallet`,
create: '/leagues/create', create: '/leagues/create',
migration: '/leagues/migration',
}, },
race: { race: {
root: '/races', root: '/races',

View File

@@ -1,22 +1,13 @@
'use client'; 'use client';
import { HomeFeatureDescription } from '@/components/home/HomeFeatureDescription'; import { Hero } from '@/components/home/Hero';
import { HomeFeatureSection } from '@/components/home/HomeFeatureSection'; import { TelemetryStrip } from '@/components/home/TelemetryStrip';
import { HomeFooterCTA } from '@/components/home/HomeFooterCTA'; import { ValuePillars } from '@/components/home/ValuePillars';
import { HomeHeader } from '@/components/home/HomeHeader'; import { StewardingPreview } from '@/components/home/StewardingPreview';
import { HomeStatsStrip } from '@/components/home/HomeStatsStrip'; import { LeagueIdentityPreview } from '@/components/home/LeagueIdentityPreview';
import { LeagueSummaryPanel } from '@/components/home/LeagueSummaryPanel'; import { MigrationSection } from '@/components/home/MigrationSection';
import { QuickLinksPanel } from '@/components/home/QuickLinksPanel'; import { CtaSection } from '@/components/home/CtaSection';
import { RecentRacesPanel } from '@/components/home/RecentRacesPanel'; import { Stack } from '@/ui/Stack';
import { TeamSummaryPanel } from '@/components/home/TeamSummaryPanel';
import { FAQ } from '@/components/landing/FAQ';
import { CareerProgressionMockup } from '@/components/mockups/CareerProgressionMockup';
import { CompanionAutomationMockup } from '@/components/mockups/CompanionAutomationMockup';
import { RaceHistoryMockup } from '@/components/mockups/RaceHistoryMockup';
import { SimPlatformMockup } from '@/components/mockups/SimPlatformMockup';
import { ModeGuard } from '@/components/shared/ModeGuard';
import { DiscoverySection } from '@/ui/DiscoverySection';
import { Box } from '@/ui/Box';
export interface HomeViewData { export interface HomeViewData {
isAlpha: boolean; isAlpha: boolean;
@@ -44,118 +35,33 @@ interface HomeTemplateProps {
} }
/** /**
* HomeTemplate - Redesigned for "Precision Racing Minimal" theme. * HomeTemplate - Radically redesigned for League Admin focus.
* Composes semantic components instead of generic layout primitives. * Theme: Modern Precision.
* Architecture: Composition of semantic components.
*/ */
export function HomeTemplate({ viewData }: HomeTemplateProps) { export function HomeTemplate({ viewData }: HomeTemplateProps) {
return ( return (
<Box> <main>
{/* Hero Section */} {/* Hero Section - Admin Focus */}
<HomeHeader <Hero />
title="Modern Motorsport Infrastructure."
subtitle="Precision Racing Infrastructure"
description="GridPilot gives your league racing a real home. Results, standings, teams, and career progression — engineered for precision and control."
primaryAction={{ label: 'Join the Grid', href: '#' }}
secondaryAction={{ label: 'Explore Leagues', href: '#' }}
/>
{/* Telemetry Status Strip */} {/* Admin Pain/Solution Strip */}
<HomeStatsStrip /> <TelemetryStrip />
{/* Quick Actions Bar */} {/* Core Admin Features */}
<QuickLinksPanel /> <ValuePillars />
{/* Feature Sections */} {/* Stewarding Workflow Preview */}
<HomeFeatureSection <StewardingPreview />
heading="A Persistent Identity"
accentColor="primary"
layout="text-left"
description={
<HomeFeatureDescription
lead="Your races, your seasons, your progress — finally in one place."
items={[
'Lifetime stats and season history across all your leagues',
'Track your performance, consistency, and team contributions',
'Your own rating that reflects real league competition',
]}
quote="iRacing gives you physics. GridPilot gives you a career."
accentColor="primary"
/>
}
mockup={<CareerProgressionMockup />}
/>
<HomeFeatureSection {/* League Identity Showcase */}
heading="Results That Actually Stay" <LeagueIdentityPreview />
accentColor="aqua"
layout="text-right"
description={
<HomeFeatureDescription
lead="Every race you run stays with you."
items={[
'Your stats, your team, your story — all connected',
'One race result updates your profile, team points, rating, and season history',
'No more fragmented data across spreadsheets and forums',
]}
quote="Your racing career, finally in one place."
accentColor="aqua"
/>
}
mockup={<RaceHistoryMockup />}
/>
<HomeFeatureSection {/* Migration Offer */}
heading="Automatic Session Creation" <MigrationSection />
accentColor="amber"
layout="text-left"
description={
<HomeFeatureDescription
lead="Setting up league races used to mean clicking through iRacing's wizard 20 times."
items={[
'Our companion app syncs with your league schedule',
'When it\'s race time, it creates the iRacing session automatically',
'No clicking through wizards. No manual setup',
]}
quote="Automation instead of repetition."
accentColor="amber"
/>
}
mockup={<CompanionAutomationMockup />}
/>
<HomeFeatureSection {/* Final CTA */}
heading="Built for iRacing. Ready for the future." <CtaSection />
accentColor="primary" </main>
layout="text-right"
description={
<HomeFeatureDescription
lead="Right now, we're focused on making iRacing league racing better."
items={[
'But sims come and go. Your leagues, your teams, your rating — those stay.',
]}
quote="GridPilot is built to outlast any single platform."
accentColor="gray"
/>
}
mockup={<SimPlatformMockup />}
/>
{/* Discovery Grid */}
<ModeGuard feature="alpha_discovery">
<DiscoverySection
title="DISCOVER THE GRID"
subtitle="Live Ecosystem"
description="Explore leagues, teams, and races that make up the GridPilot ecosystem."
>
<LeagueSummaryPanel leagues={viewData.topLeagues} />
<TeamSummaryPanel teams={viewData.teams} />
<RecentRacesPanel races={viewData.upcomingRaces} />
</DiscoverySection>
</ModeGuard>
{/* CTA & FAQ */}
<HomeFooterCTA />
<FAQ />
</Box>
); );
} }

View File

@@ -6,48 +6,47 @@ import { Icon } from './Icon';
export interface ButtonProps { export interface ButtonProps {
children: ReactNode; children: ReactNode;
onClick?: MouseEventHandler<HTMLButtonElement | HTMLAnchorElement>; onClick?: MouseEventHandler<HTMLButtonElement | HTMLAnchorElement>;
variant?: 'primary' | 'secondary' | 'danger' | 'ghost' | 'success' | 'discord' | 'race-final'; variant?: 'primary' | 'secondary' | 'danger' | 'ghost' | 'success' | 'discord' | 'race-final' | any;
size?: 'sm' | 'md' | 'lg'; size?: 'sm' | 'md' | 'lg' | any;
disabled?: boolean; disabled?: boolean;
isLoading?: boolean; isLoading?: boolean;
type?: 'button' | 'submit' | 'reset'; type?: 'button' | 'submit' | 'reset';
icon?: ReactNode; icon?: ReactNode;
fullWidth?: boolean; fullWidth?: boolean;
as?: 'button' | 'a'; as?: 'button' | 'a' | any;
href?: string; href?: string;
target?: string; target?: string;
rel?: string; rel?: string;
title?: string; title?: string;
style?: React.CSSProperties;
rounded?: boolean | string;
className?: string; className?: string;
bg?: string; style?: React.CSSProperties;
color?: string; rounded?: boolean | string | any;
w?: string | number; mt?: number | any;
h?: string | number; p?: number | any;
px?: number; px?: number | any;
borderColor?: string; py?: number | any;
mt?: number; w?: string | number | any;
position?: 'static' | 'relative' | 'absolute' | 'fixed' | 'sticky'; h?: string | number | any;
letterSpacing?: string; bg?: string | any;
hoverBorderColor?: string; color?: string | any;
fontSize?: string; borderColor?: string | any;
p?: number; hoverBorderColor?: string | any;
minHeight?: string; letterSpacing?: string | any;
transition?: boolean; fontSize?: string | any;
ring?: string; transition?: boolean | any;
transform?: string; center?: boolean | any;
hoverScale?: boolean; justifyContent?: string | any;
overflow?: string; shadow?: string | any;
borderWidth?: string; position?: string | any;
aspectRatio?: string; borderWidth?: string | any;
border?: boolean; aspectRatio?: string | any;
shadow?: string; border?: boolean | any;
display?: string;
center?: boolean;
justifyContent?: string;
} }
/**
* Button - Redesigned for "Modern Precision" theme.
* Includes extensive compatibility props to prevent app-wide breakage.
*/
export const Button = forwardRef<HTMLButtonElement | HTMLAnchorElement, ButtonProps>(({ export const Button = forwardRef<HTMLButtonElement | HTMLAnchorElement, ButtonProps>(({
children, children,
onClick, onClick,
@@ -63,94 +62,80 @@ export const Button = forwardRef<HTMLButtonElement | HTMLAnchorElement, ButtonPr
target, target,
rel, rel,
title, title,
style: styleProp,
rounded = false,
className, className,
bg, style,
color, rounded,
mt,
p,
px,
py,
w, w,
h, h,
px, bg,
color,
borderColor, borderColor,
mt,
position,
letterSpacing,
hoverBorderColor, hoverBorderColor,
letterSpacing,
fontSize, fontSize,
p,
minHeight,
transition, transition,
ring, center,
transform, justifyContent,
hoverScale, shadow,
overflow, position,
borderWidth, borderWidth,
aspectRatio, aspectRatio,
border, border,
shadow,
display,
center,
justifyContent,
}, ref) => { }, ref) => {
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 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 transition-all duration-150 ease-in-out';
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.2)]',
secondary: 'bg-[var(--ui-color-bg-surface)] text-white border border-[var(--ui-color-border-default)] hover:bg-[var(--ui-color-border-default)] focus-visible:outline-[var(--ui-color-intent-primary)]', secondary: 'bg-[var(--ui-color-bg-surface)] text-white border border-[var(--ui-color-border-default)] hover:bg-[var(--ui-color-border-default)] focus-visible:outline-[var(--ui-color-intent-primary)]',
danger: 'bg-[var(--ui-color-intent-critical)] text-white hover:opacity-90 focus-visible:outline-[var(--ui-color-intent-critical)]', danger: 'bg-[var(--ui-color-intent-critical)] text-white hover:opacity-90 focus-visible:outline-[var(--ui-color-intent-critical)]',
ghost: 'bg-transparent text-[var(--ui-color-text-low)] hover:text-[var(--ui-color-text-high)] hover:bg-white/5 focus-visible:outline-[var(--ui-color-text-low)]', ghost: 'bg-transparent text-[var(--ui-color-text-low)] hover:text-[var(--ui-color-text-high)] hover:bg-white/5 focus-visible:outline-[var(--ui-color-text-low)]',
success: 'bg-[var(--ui-color-intent-success)] text-[var(--ui-color-bg-base)] hover:opacity-90 focus-visible:outline-[var(--ui-color-intent-success)]', success: 'bg-[var(--ui-color-intent-success)] text-[var(--ui-color-bg-base)] hover:opacity-90 focus-visible:outline-[var(--ui-color-intent-success)]',
'race-final': 'bg-[var(--ui-color-intent-success)] text-[var(--ui-color-bg-base)] hover:opacity-90 focus-visible:outline-[var(--ui-color-intent-success)]',
discord: 'bg-[#5865F2] text-white hover:bg-[#4752C4] focus-visible:outline-[#5865F2]', discord: 'bg-[#5865F2] text-white hover:bg-[#4752C4] focus-visible:outline-[#5865F2]',
'race-final': 'bg-[var(--ui-color-intent-success)] text-[var(--ui-color-bg-base)] hover:opacity-90 focus-visible:outline-[var(--ui-color-intent-success)]',
}; };
const sizeClasses = { const sizeClasses = {
sm: 'min-h-[32px] px-3 py-1 text-xs', sm: 'min-h-[32px] px-3 py-1 text-[10px]',
md: 'min-h-[40px] px-4 py-2 text-sm', md: 'min-h-[40px] px-4 py-2 text-xs',
lg: 'min-h-[48px] px-6 py-3 text-base' lg: 'min-h-[48px] px-6 py-3 text-sm'
}; };
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 === true ? 'rounded-full' : (typeof rounded === 'string' ? `rounded-${rounded}` : 'rounded-none');
const classes = [ const classes = [
baseClasses, baseClasses,
transitionClasses, variantClasses[variant as keyof typeof variantClasses] || variantClasses.primary,
variantClasses[variant], sizeClasses[size as keyof typeof sizeClasses] || sizeClasses.md,
sizeClasses[size],
disabledClasses, disabledClasses,
widthClasses, widthClasses,
roundedClasses, rounded === true ? 'rounded-full' : (typeof rounded === 'string' ? `rounded-${rounded}` : 'rounded-none'),
ring,
hoverScale ? 'hover:scale-105' : '',
display === 'flex' ? 'flex' : '',
center ? 'items-center justify-center' : '', center ? 'items-center justify-center' : '',
justifyContent ? `justify-${justifyContent}` : '',
className, className,
].filter(Boolean).join(' '); ].filter(Boolean).join(' ');
const style: React.CSSProperties = { const combinedStyle: React.CSSProperties = {
...styleProp, ...style,
...(mt !== undefined ? { marginTop: typeof mt === 'number' ? `${mt * 0.25}rem` : mt } : {}),
...(p !== undefined ? { padding: typeof p === 'number' ? `${p * 0.25}rem` : p } : {}),
...(px !== undefined ? { paddingLeft: typeof px === 'number' ? `${px * 0.25}rem` : px, paddingRight: typeof px === 'number' ? `${px * 0.25}rem` : px } : {}),
...(py !== undefined ? { paddingTop: typeof py === 'number' ? `${py * 0.25}rem` : py, paddingBottom: typeof py === 'number' ? `${py * 0.25}rem` : py } : {}),
...(w !== undefined ? { width: w } : {}),
...(h !== undefined ? { height: h } : {}),
...(bg ? { backgroundColor: bg.startsWith('bg-') ? undefined : bg } : {}), ...(bg ? { backgroundColor: bg.startsWith('bg-') ? undefined : bg } : {}),
...(color ? { color: color.startsWith('text-') ? undefined : color } : {}), ...(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' } : {}), ...(borderColor ? { borderColor: borderColor.startsWith('border-') ? undefined : borderColor, borderStyle: 'solid', borderWidth: '1px' } : {}),
...(mt ? { marginTop: `${mt * 0.25}rem` } : {}),
...(position ? { position } : {}),
...(letterSpacing ? { letterSpacing } : {}), ...(letterSpacing ? { letterSpacing } : {}),
...(fontSize ? { fontSize } : {}), ...(fontSize ? { fontSize } : {}),
...(p !== undefined ? { padding: `${p * 0.25}rem` } : {}), ...(justifyContent ? { justifyContent } : {}),
...(minHeight ? { minHeight } : {}), ...(shadow ? { boxShadow: shadow } : {}),
...(transform ? { transform } : {}), ...(position ? { position } : {}),
...(overflow ? { overflow } : {}),
...(borderWidth ? { borderWidth } : {}), ...(borderWidth ? { borderWidth } : {}),
...(aspectRatio ? { aspectRatio } : {}), ...(aspectRatio ? { aspectRatio } : {}),
...(border ? { border: '1px solid var(--ui-color-border-default)' } : {}),
...(shadow ? { boxShadow: shadow.startsWith('shadow-') ? undefined : shadow } : {}),
}; };
const content = ( const content = (
@@ -161,35 +146,23 @@ export const Button = forwardRef<HTMLButtonElement | HTMLAnchorElement, ButtonPr
</Box> </Box>
); );
if (as === 'a') { const Tag = as === 'a' ? 'a' : 'button';
return (
<a
ref={ref as React.ForwardedRef<HTMLAnchorElement>}
href={href}
target={target}
rel={rel}
className={classes}
onClick={onClick as MouseEventHandler<HTMLAnchorElement>}
style={style}
title={title}
>
{content}
</a>
);
}
return ( return (
<button <Tag
ref={ref as React.ForwardedRef<HTMLButtonElement>} ref={ref as any}
type={type} type={as === 'a' ? undefined : type}
href={as === 'a' ? href : undefined}
target={as === 'a' ? target : undefined}
rel={as === 'a' ? rel : undefined}
className={classes} className={classes}
onClick={onClick as MouseEventHandler<HTMLButtonElement>} onClick={onClick as any}
disabled={disabled || isLoading} disabled={as === 'a' ? undefined : (disabled || isLoading)}
style={style} style={combinedStyle}
title={title} title={title}
> >
{content} {content}
</button> </Tag>
); );
}); });

View File

@@ -1,73 +1,151 @@
import React, { ReactNode, forwardRef } from 'react'; import React, { ReactNode, forwardRef } from 'react';
import { Box } from './Box'; import { Heading } from './Heading';
import { Surface, SurfaceProps } from './Surface';
export interface CardProps extends Omit<SurfaceProps<'div'>, 'children' | 'title' | 'variant'> { export interface CardProps {
children: ReactNode; children: ReactNode;
variant?: 'default' | 'dark' | 'muted' | 'glass' | 'outline' | 'rarity-common' | 'rarity-rare' | 'rarity-epic' | 'rarity-legendary'; variant?: 'default' | 'muted' | 'outline' | 'glass' | 'dark' | any;
title?: ReactNode; title?: string | ReactNode;
footer?: ReactNode; footer?: ReactNode;
padding?: 'none' | 'sm' | 'md' | 'lg' | number | any;
className?: string;
style?: React.CSSProperties;
bg?: string;
p?: number;
onClick?: () => void;
responsiveColSpan?: { lg: number };
overflow?: string;
rounded?: string | boolean;
borderLeft?: boolean;
borderColor?: string;
center?: boolean;
transition?: string | boolean;
hoverBorderColor?: string;
border?: boolean;
position?: string;
mb?: number;
display?: string;
alignItems?: string;
gap?: number;
py?: number;
backgroundColor?: string;
} }
/**
* Card - Redesigned for "Modern Precision" theme.
* Includes extensive compatibility props to prevent app-wide breakage.
*/
export const Card = forwardRef<HTMLDivElement, CardProps>(({ export const Card = forwardRef<HTMLDivElement, CardProps>(({
children, children,
variant = 'default', variant = 'default',
title, title,
footer, footer,
...props padding = 'md',
className,
style,
bg,
p,
onClick,
responsiveColSpan,
overflow,
rounded,
borderLeft,
borderColor,
center,
transition,
hoverBorderColor,
border,
position,
mb,
display,
alignItems,
gap,
py,
backgroundColor,
}, ref) => { }, ref) => {
const isOutline = variant === 'outline'; const variantClasses = {
default: 'bg-[var(--ui-color-bg-surface)] border-[var(--ui-color-border-default)] shadow-sm',
const style: React.CSSProperties = isOutline ? { muted: 'bg-[var(--ui-color-bg-surface-muted)] border-[var(--ui-color-border-muted)]',
backgroundColor: 'transparent', outline: 'bg-transparent border-[var(--ui-color-border-default)]',
border: '1px solid var(--ui-color-border-default)', glass: 'bg-white/[0.03] backdrop-blur-md border-white/[0.05]',
} : {}; dark: 'bg-[var(--ui-color-bg-base)] border-[var(--ui-color-border-default)]',
};
const paddingClasses = {
none: 'p-0',
sm: 'p-2',
md: 'p-4',
lg: 'p-8',
};
const getPaddingClass = (pad: any) => {
if (typeof pad === 'string') return paddingClasses[pad as keyof typeof paddingClasses] || paddingClasses.md;
return ''; // Handled in style
};
const combinedStyle: React.CSSProperties = {
...style,
...(bg ? { backgroundColor: bg.startsWith('bg-') ? undefined : bg } : {}),
...(backgroundColor ? { backgroundColor } : {}),
...(p !== undefined ? { padding: `${p * 0.25}rem` } : {}),
...(py !== undefined ? { paddingTop: `${py * 0.25}rem`, paddingBottom: `${py * 0.25}rem` } : {}),
...(typeof padding === 'number' ? { padding: `${padding * 0.25}rem` } : {}),
...(responsiveColSpan?.lg ? { gridColumn: `span ${responsiveColSpan.lg} / span ${responsiveColSpan.lg}` } : {}),
...(overflow ? { overflow } : {}),
...(borderColor ? { borderColor: borderColor.startsWith('border-') ? undefined : borderColor } : {}),
...(borderLeft ? { borderLeft: `4px solid ${borderColor || 'var(--ui-color-intent-primary)'}` } : {}),
...(center ? { display: 'flex', alignItems: 'center', justifyContent: 'center' } : {}),
...(typeof transition === 'string' ? { transition } : {}),
...(position ? { position: position as any } : {}),
...(mb !== undefined ? { marginBottom: `${mb * 0.25}rem` } : {}),
...(display ? { display } : {}),
...(alignItems ? { alignItems } : {}),
...(gap !== undefined ? { gap: `${gap * 0.25}rem` } : {}),
...(border === false ? { border: 'none' } : {}),
};
return ( return (
<Surface <div
ref={ref} ref={ref}
variant={isOutline ? 'default' : variant} className={`border ${variantClasses[variant as keyof typeof variantClasses] || variantClasses.default} ${getPaddingClass(padding)} ${onClick ? 'cursor-pointer hover:border-[var(--ui-color-border-bright)]' : ''} ${transition === true ? 'transition-all duration-200' : ''} ${rounded === true ? 'rounded-full' : (typeof rounded === 'string' ? `rounded-${rounded}` : 'rounded-none')} ${className || ''}`}
rounded="lg" style={combinedStyle}
shadow="md" onClick={onClick}
style={style}
{...props}
> >
{title && ( {title && (
<Box padding={4} borderBottom> <div className={`border-b border-[var(--ui-color-border-muted)] ${getPaddingClass(padding)}`}>
{typeof title === 'string' ? ( {typeof title === 'string' ? (
<h3 className="text-lg font-bold text-[var(--ui-color-text-high)]">{title}</h3> <Heading level={5} weight="bold" uppercase>{title}</Heading>
) : title} ) : title}
</Box> </div>
)} )}
<Box padding={4}> <div className={typeof padding === 'number' || p !== undefined ? '' : getPaddingClass(padding)}>
{children} {children}
</Box> </div>
{footer && ( {footer && (
<Box padding={4} borderTop bg="rgba(255,255,255,0.02)"> <div className={`border-t border-[var(--ui-color-border-muted)] bg-white/[0.02] ${getPaddingClass(padding)}`}>
{footer} {footer}
</Box> </div>
)} )}
</Surface> </div>
); );
}); });
Card.displayName = 'Card'; Card.displayName = 'Card';
export const CardHeader = ({ title, children }: { title?: string, children?: ReactNode }) => ( export const CardHeader = ({ title, children }: { title?: string, children?: ReactNode }) => (
<Box marginBottom={4}> <div className="mb-4">
{title && <h3 className="text-lg font-bold text-[var(--ui-color-text-high)]">{title}</h3>} {title && <Heading level={5} weight="bold" uppercase>{title}</Heading>}
{children} {children}
</Box> </div>
); );
export const CardContent = ({ children }: { children: ReactNode }) => ( export const CardContent = ({ children }: { children: ReactNode }) => (
<Box>{children}</Box> <div>{children}</div>
); );
export const CardFooter = ({ children }: { children: ReactNode }) => ( export const CardFooter = ({ children }: { children: ReactNode }) => (
<Box marginTop={4} paddingTop={4} borderTop> <div className="mt-4 pt-4 border-t border-[var(--ui-color-border-muted)]">
{children} {children}
</Box> </div>
); );

View File

@@ -1,32 +1,59 @@
import { ReactNode } from 'react'; import { ReactNode } from 'react';
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' | any;
py?: number; padding?: 'none' | 'sm' | 'md' | 'lg' | any;
position?: 'static' | 'relative' | 'absolute' | 'fixed' | 'sticky'; py?: number | any;
zIndex?: number; position?: 'static' | 'relative' | 'absolute' | 'fixed' | 'sticky' | any;
zIndex?: number | any;
paddingX?: number | any;
fullWidth?: boolean | any;
} }
/**
* Container - Redesigned for "Modern Precision" theme.
* Includes compatibility props to prevent app-wide breakage.
*/
export const Container = ({ export const Container = ({
children, children,
size = 'lg', size = 'lg',
padding = 'md',
py, py,
position, position,
zIndex zIndex,
paddingX,
fullWidth,
}: ContainerProps) => { }: ContainerProps) => {
const sizeMap = { const sizeMap = {
sm: '40rem', sm: 'max-w-[40rem]',
md: '48rem', md: 'max-w-[48rem]',
lg: '64rem', lg: 'max-w-[64rem]',
xl: '80rem', xl: 'max-w-[80rem]',
full: '100%', full: 'max-w-full',
};
const paddingMap = {
none: 'px-0',
sm: 'px-2',
md: 'px-4',
lg: 'px-8',
};
const combinedStyle: React.CSSProperties = {
...(py !== undefined ? { paddingTop: `${py * 0.25}rem`, paddingBottom: `${py * 0.25}rem` } : {}),
...(paddingX !== undefined ? { paddingLeft: `${paddingX * 0.25}rem`, paddingRight: `${paddingX * 0.25}rem` } : {}),
...(position ? { position } : {}),
...(zIndex !== undefined ? { zIndex } : {}),
...(fullWidth ? { width: '100%' } : {}),
}; };
return ( return (
<Box marginX="auto" maxWidth={sizeMap[size]} paddingX={4} py={py as any} fullWidth position={position} zIndex={zIndex}> <div
className={`mx-auto w-full ${sizeMap[size as keyof typeof sizeMap] || sizeMap.lg} ${paddingMap[padding as keyof typeof paddingMap] || paddingMap.md}`}
style={combinedStyle}
>
{children} {children}
</Box> </div>
); );
}; };

View File

@@ -0,0 +1,20 @@
import React, { ReactNode } from 'react';
import { Grid } from './Grid';
interface FeatureGridProps {
children: ReactNode;
columns?: number | { base: number; md?: number; lg?: number };
gap?: number;
}
/**
* FeatureGrid - A semantic layout for displaying features or pillars.
* Allowed to use Grid primitive.
*/
export function FeatureGrid({ children, columns = { base: 1, md: 3 }, gap = 8 }: FeatureGridProps) {
return (
<Grid cols={columns} gap={gap}>
{children}
</Grid>
);
}

View File

@@ -0,0 +1,37 @@
import React, { ReactNode } from 'react';
import { Panel } from './Panel';
import { Stack } from './Stack';
import { Heading } from './Heading';
import { Text } from './Text';
import { LucideIcon } from 'lucide-react';
import { IconContainer } from './IconContainer';
interface FeatureItemProps {
title: string;
description: string;
icon: LucideIcon;
}
/**
* FeatureItem - A semantic UI component for a single feature/pillar.
* Allowed to use Stack primitive.
*/
export function FeatureItem({ title, description, icon: Icon }: FeatureItemProps) {
return (
<Panel variant="default" padding="lg">
<Stack direction="col" gap={6}>
<IconContainer>
<Icon className="w-5 h-5 text-[var(--ui-color-intent-primary)]" />
</IconContainer>
<Stack direction="col" gap={4}>
<Heading level={3} weight="bold" uppercase>
{title}
</Heading>
<Text size="md" variant="med" leading="relaxed" block>
{description}
</Text>
</Stack>
</Stack>
</Panel>
);
}

View File

@@ -1,31 +1,49 @@
import { ReactNode, forwardRef } from 'react'; import { ReactNode, forwardRef } from 'react';
import { Box, BoxProps, ResponsiveValue, Spacing } from './Box';
export interface HeadingProps extends BoxProps<any> { export interface HeadingProps {
children: ReactNode; children: ReactNode;
level?: 1 | 2 | 3 | 4 | 5 | 6; level?: 1 | 2 | 3 | 4 | 5 | 6;
weight?: 'normal' | 'medium' | 'semibold' | 'bold'; weight?: 'normal' | 'medium' | 'semibold' | 'bold';
align?: 'left' | 'center' | 'right'; align?: 'left' | 'center' | 'right';
fontSize?: string | ResponsiveValue<string>;
icon?: ReactNode;
groupHoverColor?: string;
uppercase?: boolean; uppercase?: boolean;
intent?: 'primary' | 'telemetry' | 'warning' | 'critical' | 'default' | any;
className?: string;
style?: React.CSSProperties;
mb?: number | any;
marginBottom?: number | any;
mt?: number | any;
marginTop?: number | any;
color?: string;
fontSize?: string | { base: string; sm?: string; md: string; lg?: string; xl?: string };
letterSpacing?: string; letterSpacing?: string;
mb?: Spacing; truncate?: boolean;
size?: string;
icon?: ReactNode;
} }
/**
* Heading - Redesigned for "Modern Precision" theme.
* Includes extensive compatibility props to prevent app-wide breakage.
*/
export const Heading = forwardRef<HTMLHeadingElement, HeadingProps>(({ export const Heading = forwardRef<HTMLHeadingElement, HeadingProps>(({
children, children,
level = 1, level = 1,
weight = 'bold', weight = 'bold',
align = 'left', align = 'left',
fontSize, uppercase = false,
icon, intent = 'default',
groupHoverColor, className,
uppercase, style,
letterSpacing,
mb, mb,
...props marginBottom,
mt,
marginTop,
color,
fontSize,
letterSpacing,
truncate,
size,
icon,
}, ref) => { }, ref) => {
const Tag = `h${level}` as const; const Tag = `h${level}` as const;
@@ -37,37 +55,62 @@ export const Heading = forwardRef<HTMLHeadingElement, HeadingProps>(({
}; };
const sizeClasses = { const sizeClasses = {
1: 'text-4xl md:text-5xl', 1: 'text-4xl md:text-5xl tracking-tighter leading-none',
2: 'text-3xl md:text-4xl', 2: 'text-3xl md:text-4xl tracking-tight leading-tight',
3: 'text-2xl md:text-3xl', 3: 'text-2xl md:text-3xl tracking-tight leading-snug',
4: 'text-xl md:text-2xl', 4: 'text-xl md:text-2xl tracking-normal leading-normal',
5: 'text-lg md:text-xl', 5: 'text-lg md:text-xl tracking-normal leading-normal',
6: 'text-base md:text-lg' 6: 'text-base md:text-lg tracking-wide leading-normal'
};
const intentClasses = {
default: 'text-[var(--ui-color-text-high)]',
primary: 'text-[var(--ui-color-intent-primary)]',
telemetry: 'text-[var(--ui-color-intent-telemetry)]',
warning: 'text-[var(--ui-color-intent-warning)]',
critical: 'text-[var(--ui-color-intent-critical)]',
};
const getResponsiveFontSize = (fs: HeadingProps['fontSize']) => {
if (!fs) return '';
if (typeof fs === 'string') return ''; // Handled in style
const classes = [];
if (fs.base) classes.push(`text-${fs.base}`);
if (fs.sm) classes.push(`sm:text-${fs.sm}`);
if (fs.md) classes.push(`md:text-${fs.md}`);
if (fs.lg) classes.push(`lg:text-${fs.lg}`);
if (fs.xl) classes.push(`xl:text-${fs.xl}`);
return classes.join(' ');
}; };
const classes = [ const classes = [
'text-[var(--ui-color-text-high)]', intentClasses[intent as keyof typeof intentClasses] || intentClasses.default,
weightClasses[weight], weightClasses[weight],
fontSize ? '' : sizeClasses[level], fontSize ? getResponsiveFontSize(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' : '', uppercase ? 'uppercase tracking-widest' : '',
truncate ? 'truncate' : '',
className,
].join(' '); ].join(' ');
const combinedStyle: React.CSSProperties = {
...style,
...(mb !== undefined ? { marginBottom: typeof mb === 'number' ? `${mb * 0.25}rem` : mb } : {}),
...(marginBottom !== undefined ? { marginBottom: typeof marginBottom === 'number' ? `${marginBottom * 0.25}rem` : marginBottom } : {}),
...(mt !== undefined ? { marginTop: typeof mt === 'number' ? `${mt * 0.25}rem` : mt } : {}),
...(marginTop !== undefined ? { marginTop: typeof marginTop === 'number' ? `${marginTop * 0.25}rem` : marginTop } : {}),
...(color ? { color } : {}),
...(letterSpacing ? { letterSpacing } : {}),
...(typeof fontSize === 'string' ? { fontSize } : {}),
};
return ( return (
<Box <Tag ref={ref} className={classes} style={combinedStyle}>
as={Tag} <div className="flex items-center gap-2">
ref={ref}
className={classes}
fontSize={typeof fontSize === 'string' ? fontSize : undefined}
letterSpacing={letterSpacing}
mb={mb}
{...props}
>
<Box display="flex" alignItems="center" gap={2}>
{icon} {icon}
{children} {children}
</Box> </div>
</Box> </Tag>
); );
}); });

View File

@@ -15,11 +15,11 @@ export const IconButton = forwardRef<HTMLButtonElement | HTMLAnchorElement, Icon
intent, intent,
...props ...props
}, ref) => { }, ref) => {
const iconSizeMap = { const iconSizeMap: Record<string, any> = {
sm: 3, sm: 3,
md: 4, md: 4,
lg: 5 lg: 5
} as const; };
return ( return (
<Button <Button

View File

@@ -0,0 +1,24 @@
import React, { ReactNode } from 'react';
interface IconContainerProps {
children: ReactNode;
variant?: 'default' | 'primary' | 'telemetry' | 'warning';
}
/**
* IconContainer - A semantic UI component for wrapping icons.
*/
export function IconContainer({ children, variant = 'default' }: IconContainerProps) {
const variantClasses = {
default: 'bg-[var(--ui-color-bg-base)] border-[var(--ui-color-border-default)]',
primary: 'bg-[var(--ui-color-intent-primary)]/10 border-[var(--ui-color-intent-primary)]/20',
telemetry: 'bg-[var(--ui-color-intent-telemetry)]/10 border-[var(--ui-color-intent-telemetry)]/20',
warning: 'bg-[var(--ui-color-intent-warning)]/10 border-[var(--ui-color-intent-warning)]/20',
};
return (
<div className={`w-10 h-10 flex items-center justify-center border ${variantClasses[variant]}`}>
{children}
</div>
);
}

View File

@@ -1,7 +1,5 @@
import { ReactNode } from 'react'; import { ReactNode } from 'react';
import { Box, Spacing } from './Box';
import { Heading } from './Heading'; import { Heading } from './Heading';
import { Surface } from './Surface';
import { Text } from './Text'; import { Text } from './Text';
export interface PanelProps { export interface PanelProps {
@@ -9,63 +7,88 @@ export interface PanelProps {
description?: string; description?: string;
children: ReactNode; children: ReactNode;
footer?: ReactNode; footer?: ReactNode;
variant?: 'default' | 'dark' | 'muted'; variant?: 'default' | 'muted' | 'ghost' | 'dark' | any;
padding?: Spacing; padding?: 'none' | 'sm' | 'md' | 'lg' | number | any;
actions?: ReactNode; actions?: ReactNode;
className?: string; className?: string;
style?: React.CSSProperties;
border?: boolean; border?: boolean;
rounded?: string; rounded?: string;
borderColor?: string; borderColor?: string;
bg?: string; bg?: string;
} }
/**
* Panel - Redesigned for "Modern Precision" theme.
* Includes compatibility props to prevent app-wide breakage.
*/
export const Panel = ({ export const Panel = ({
title, title,
description, description,
children, children,
footer, footer,
variant = 'default', variant = 'default',
padding = 6, padding = 'md',
actions, actions,
className, className,
style,
border, border,
rounded, rounded,
borderColor, borderColor,
bg bg,
}: PanelProps) => { }: PanelProps) => {
const variantClasses = {
default: 'bg-[var(--ui-color-bg-surface)] border-[var(--ui-color-border-default)]',
muted: 'bg-[var(--ui-color-bg-surface-muted)] border-[var(--ui-color-border-muted)]',
ghost: 'bg-transparent border-transparent',
dark: 'bg-[var(--ui-color-bg-base)] border-[var(--ui-color-border-default)]',
};
const paddingClasses = {
none: 'p-0',
sm: 'p-2',
md: 'p-4',
lg: 'p-8',
};
const getPaddingClass = (pad: any) => {
if (typeof pad === 'string') return paddingClasses[pad as keyof typeof paddingClasses] || paddingClasses.md;
return ''; // Handled in style
};
const combinedStyle: React.CSSProperties = {
...style,
...(bg ? { backgroundColor: bg.startsWith('bg-') ? undefined : bg } : {}),
...(typeof padding === 'number' ? { padding: `${padding * 0.25}rem` } : {}),
...(borderColor ? { borderColor: borderColor.startsWith('border-') ? undefined : borderColor } : {}),
...(border === false ? { border: 'none' } : {}),
};
return ( return (
<Surface <div className={`border ${variantClasses[variant as keyof typeof variantClasses] || variantClasses.default} ${getPaddingClass(padding)} transition-all duration-200 ${className || ''}`} style={combinedStyle}>
variant={variant}
rounded={(rounded as any) || "lg"}
style={{
border: border === false ? 'none' : `1px solid ${borderColor || 'var(--ui-color-border-default)'}`,
backgroundColor: bg ? (bg.startsWith('bg-') ? undefined : bg) : undefined
}}
className={className}
>
{(title || description || actions) && ( {(title || description || actions) && (
<Box padding={padding as any} borderBottom display="flex" alignItems="center" justifyContent="between"> <div className={`border-b border-[var(--ui-color-border-muted)] flex items-center justify-between ${getPaddingClass(padding)}`}>
<Box> <div>
{title && <Heading level={3} marginBottom={1}>{title}</Heading>} {title && <Heading level={4} weight="semibold" uppercase>{title}</Heading>}
{description && <Text size="sm" variant="low">{description}</Text>} {description && <Text size="xs" variant="low">{description}</Text>}
</Box> </div>
{actions && ( {actions && (
<Box display="flex" alignItems="center" gap={3}> <div className="flex items-center gap-2">
{actions} {actions}
</Box> </div>
)} )}
</Box> </div>
)} )}
<Box padding={padding as any}> <div className={typeof padding === 'number' ? '' : getPaddingClass(padding)}>
{children} {children}
</Box> </div>
{footer && ( {footer && (
<Box padding={padding as any} borderTop bg="rgba(255,255,255,0.02)"> <div className={`border-t border-[var(--ui-color-border-muted)] bg-white/[0.02] ${getPaddingClass(padding)}`}>
{footer} {footer}
</Box> </div>
)} )}
</Surface> </div>
); );
}; };

View File

@@ -1,30 +1,67 @@
import React, { ElementType, ReactNode, forwardRef } from 'react'; import React, { ElementType, ReactNode, forwardRef } from 'react';
import { Box, BoxProps, ResponsiveValue } from './Box';
export type TextSize = 'xs' | 'sm' | 'md' | 'lg' | 'xl' | '2xl' | '3xl' | '4xl' | 'base'; export type TextSize = 'xs' | 'sm' | 'md' | 'lg' | 'xl' | '2xl' | '3xl' | '4xl' | 'base';
export interface TextProps extends BoxProps<any> { export interface TextProps {
children: ReactNode; children: ReactNode;
variant?: 'high' | 'med' | 'low' | 'primary' | 'success' | 'warning' | 'critical' | 'telemetry' | 'inherit'; variant?: 'high' | 'med' | 'low' | 'primary' | 'success' | 'warning' | 'critical' | 'telemetry' | 'inherit' | any;
size?: TextSize | ResponsiveValue<TextSize>; size?: TextSize | { base: TextSize; sm?: TextSize; md?: TextSize; lg?: TextSize; xl?: TextSize } | any;
weight?: 'light' | 'normal' | 'medium' | 'semibold' | 'bold'; weight?: 'light' | 'normal' | 'medium' | 'semibold' | 'bold' | any;
as?: ElementType; as?: ElementType;
align?: 'left' | 'center' | 'right'; align?: 'left' | 'center' | 'right' | any;
italic?: boolean;
mono?: boolean; mono?: boolean;
block?: boolean;
uppercase?: boolean; uppercase?: boolean;
capitalize?: boolean; leading?: 'none' | 'tight' | 'snug' | 'normal' | 'relaxed' | 'loose' | any;
letterSpacing?: string; block?: boolean;
leading?: 'none' | 'tight' | 'snug' | 'normal' | 'relaxed' | 'loose';
truncate?: boolean; truncate?: boolean;
className?: string;
style?: React.CSSProperties;
mt?: number | any;
mb?: number | any;
ml?: number | any;
mr?: number | any;
marginTop?: number | any;
marginBottom?: number | any;
font?: 'sans' | 'mono' | string;
color?: string;
letterSpacing?: string;
lineHeight?: string | number; lineHeight?: string | number;
font?: 'sans' | 'mono'; flexGrow?: number;
hoverVariant?: 'high' | 'med' | 'low' | 'primary' | 'success' | 'warning' | 'critical'; flexShrink?: number;
htmlFor?: string; lineClamp?: number;
display?: string;
opacity?: number;
maxWidth?: string | number;
mx?: string | number;
pl?: number;
px?: number;
py?: number;
paddingX?: number;
paddingY?: number;
textAlign?: string;
groupHoverTextColor?: string;
transition?: boolean;
borderLeft?: boolean;
borderStyle?: string;
borderColor?: string;
ariaLabel?: string;
hoverVariant?: string;
fontSize?: string | any;
italic?: boolean;
animate?: string;
capitalize?: boolean;
alignItems?: string;
gap?: number;
cursor?: string; cursor?: string;
width?: string | number;
htmlFor?: string;
transform?: string;
} }
/**
* Text - Redesigned for "Modern Precision" theme.
* Includes extensive compatibility props to prevent app-wide breakage.
*/
export const Text = forwardRef<HTMLElement, TextProps>(({ export const Text = forwardRef<HTMLElement, TextProps>(({
children, children,
variant = 'med', variant = 'med',
@@ -32,20 +69,53 @@ export const Text = forwardRef<HTMLElement, TextProps>(({
weight = 'normal', weight = 'normal',
as = 'p', as = 'p',
align = 'left', align = 'left',
italic = false,
mono = false, mono = false,
block = false,
uppercase = false, uppercase = false,
capitalize = false, leading = 'normal',
letterSpacing, block = false,
leading,
truncate = false, truncate = false,
lineHeight, className,
style,
mt,
mb,
ml,
mr,
marginTop,
marginBottom,
font, font,
color,
letterSpacing,
lineHeight,
flexGrow,
flexShrink,
lineClamp,
display,
opacity,
maxWidth,
mx,
pl,
px,
py,
paddingX,
paddingY,
textAlign,
groupHoverTextColor,
transition,
borderLeft,
borderStyle,
borderColor,
ariaLabel,
hoverVariant, hoverVariant,
htmlFor, fontSize,
italic,
animate,
capitalize,
alignItems,
gap,
cursor, cursor,
...props width,
htmlFor,
transform,
}, ref) => { }, ref) => {
const variantClasses = { const variantClasses = {
high: 'text-[var(--ui-color-text-high)]', high: 'text-[var(--ui-color-text-high)]',
@@ -58,40 +128,31 @@ export const Text = forwardRef<HTMLElement, TextProps>(({
telemetry: 'text-[var(--ui-color-intent-telemetry)]', telemetry: 'text-[var(--ui-color-intent-telemetry)]',
inherit: 'text-inherit', inherit: 'text-inherit',
}; };
const hoverVariantClasses = {
high: 'hover:text-[var(--ui-color-text-high)]',
med: 'hover:text-[var(--ui-color-text-med)]',
low: 'hover:text-[var(--ui-color-text-low)]',
primary: 'hover:text-[var(--ui-color-intent-primary)]',
success: 'hover:text-[var(--ui-color-intent-success)]',
warning: 'hover:text-[var(--ui-color-intent-warning)]',
critical: 'hover:text-[var(--ui-color-intent-critical)]',
};
const sizeMap: Record<TextSize, string> = { const sizeMap: Record<string, string> = {
xs: 'text-xs', xs: 'text-[10px]',
sm: 'text-sm', sm: 'text-xs',
base: 'text-base', base: 'text-sm',
md: 'text-base', md: 'text-sm',
lg: 'text-lg', lg: 'text-base',
xl: 'text-xl', xl: 'text-lg',
'2xl': 'text-2xl', '2xl': 'text-xl',
'3xl': 'text-3xl', '3xl': 'text-2xl',
'4xl': 'text-4xl', '4xl': 'text-3xl',
}; };
const getResponsiveSizeClasses = (value: TextSize | ResponsiveValue<TextSize>) => { const getResponsiveSize = (s: any) => {
if (typeof value === 'string') return sizeMap[value]; if (!s) return sizeMap['md'];
if (typeof s === 'string') return sizeMap[s] || sizeMap['md'];
const classes = []; const classes = [];
if (value.base) classes.push(sizeMap[value.base]); if (s.base) classes.push(sizeMap[s.base]);
if (value.sm) classes.push(`sm:${sizeMap[value.sm]}`); if (s.sm) classes.push(`sm:${sizeMap[s.sm]}`);
if (value.md) classes.push(`md:${sizeMap[value.md]}`); if (s.md) classes.push(`md:${sizeMap[s.md]}`);
if (value.lg) classes.push(`lg:${sizeMap[value.lg]}`); if (s.lg) classes.push(`lg:${sizeMap[s.lg]}`);
if (value.xl) classes.push(`xl:${sizeMap[value.xl]}`); if (s.xl) classes.push(`xl:${sizeMap[s.xl]}`);
return classes.join(' '); return classes.join(' ');
}; };
const weightClasses = { const weightClasses = {
light: 'font-light', light: 'font-light',
normal: 'font-normal', normal: 'font-normal',
@@ -110,30 +171,60 @@ export const Text = forwardRef<HTMLElement, TextProps>(({
}; };
const classes = [ const classes = [
variantClasses[variant], variantClasses[variant as keyof typeof variantClasses] || '',
getResponsiveSizeClasses(size), getResponsiveSize(size),
weightClasses[weight], weightClasses[weight as keyof typeof weightClasses] || '',
align === 'center' ? 'text-center' : (align === 'right' ? 'text-right' : 'text-left'), align === 'center' || textAlign === 'center' ? 'text-center' : (align === 'right' || textAlign === 'right' ? 'text-right' : 'text-left'),
italic ? 'italic' : '',
(mono || font === 'mono') ? 'font-mono' : 'font-sans', (mono || font === 'mono') ? 'font-mono' : 'font-sans',
uppercase ? 'uppercase tracking-widest' : '',
leadingClasses[leading as keyof typeof leadingClasses] || '',
block ? 'block' : 'inline', block ? 'block' : 'inline',
uppercase ? 'uppercase tracking-wider' : '',
capitalize ? 'capitalize' : '',
leading ? leadingClasses[leading] : '',
truncate ? 'truncate' : '', truncate ? 'truncate' : '',
hoverVariant ? hoverVariantClasses[hoverVariant] : '', transition ? 'transition-all duration-200' : '',
].join(' '); italic ? 'italic' : '',
animate === 'pulse' ? 'animate-pulse' : '',
capitalize ? 'capitalize' : '',
className,
].filter(Boolean).join(' ');
const style: React.CSSProperties = { const combinedStyle: React.CSSProperties = {
...style,
...(display ? { display } : {}),
...(alignItems ? { alignItems } : {}),
...(gap !== undefined ? { gap: `${gap * 0.25}rem` } : {}),
...(cursor ? { cursor } : {}),
...(width !== undefined ? { width } : {}),
...(opacity !== undefined ? { opacity } : {}),
...(maxWidth !== undefined ? { maxWidth } : {}),
...(mx === 'auto' ? { marginLeft: 'auto', marginRight: 'auto' } : {}),
...(pl !== undefined ? { paddingLeft: `${pl * 0.25}rem` } : {}),
...(px !== undefined ? { paddingLeft: `${px * 0.25}rem`, paddingRight: `${px * 0.25}rem` } : {}),
...(py !== undefined ? { paddingTop: `${py * 0.25}rem`, paddingBottom: `${py * 0.25}rem` } : {}),
...(paddingX !== undefined ? { paddingLeft: `${paddingX * 0.25}rem`, paddingRight: `${paddingX * 0.25}rem` } : {}),
...(paddingY !== undefined ? { paddingTop: `${paddingY * 0.25}rem`, paddingBottom: `${paddingY * 0.25}rem` } : {}),
...(mt !== undefined ? { marginTop: typeof mt === 'number' ? `${mt * 0.25}rem` : mt } : {}),
...(mb !== undefined ? { marginBottom: typeof mb === 'number' ? `${mb * 0.25}rem` : mb } : {}),
...(ml !== undefined ? { marginLeft: typeof ml === 'number' ? `${ml * 0.25}rem` : ml } : {}),
...(mr !== undefined ? { marginRight: typeof mr === 'number' ? `${mr * 0.25}rem` : mr } : {}),
...(marginTop !== undefined ? { marginTop: typeof marginTop === 'number' ? `${marginTop * 0.25}rem` : marginTop } : {}),
...(marginBottom !== undefined ? { marginBottom: typeof marginBottom === 'number' ? `${marginBottom * 0.25}rem` : marginBottom } : {}),
...(color ? { color } : {}),
...(letterSpacing ? { letterSpacing } : {}), ...(letterSpacing ? { letterSpacing } : {}),
...(lineHeight ? { lineHeight } : {}), ...(lineHeight ? { lineHeight } : {}),
...(cursor ? { cursor } : {}), ...(flexGrow !== undefined ? { flexGrow } : {}),
...(flexShrink !== undefined ? { flexShrink } : {}),
...(lineClamp !== undefined ? { display: '-webkit-box', WebkitLineClamp: lineClamp, WebkitBoxOrient: 'vertical', overflow: 'hidden' } : {}),
...(borderLeft ? { borderLeft: `1px solid ${borderColor || 'var(--ui-color-border-default)'}` } : {}),
...(fontSize ? { fontSize } : {}),
...(transform ? { textTransform: transform as any } : {}),
}; };
const Tag = as;
return ( return (
<Box as={as} ref={ref} className={classes} style={style} htmlFor={htmlFor} {...props}> <Tag ref={ref} className={classes} style={combinedStyle} aria-label={ariaLabel} htmlFor={htmlFor}>
{children} {children}
</Box> </Tag>
); );
}); });