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
@@ -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>
);
}
@@ -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>
);
}
@@ -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>
);
}
+65
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>
);
}
@@ -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>
);
}
@@ -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>
);
}
@@ -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>
);
}
@@ -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>
);
}
@@ -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>
);
}
+2
View File
@@ -71,6 +71,7 @@ export interface RouteGroup {
stewarding: (id: string) => string;
wallet: (id: string) => string;
create: string;
migration: string;
};
race: {
root: string;
@@ -169,6 +170,7 @@ export const routes: RouteGroup & { leaderboards: { root: string; drivers: strin
stewarding: (id: string) => `/leagues/${id}/stewarding`,
wallet: (id: string) => `/leagues/${id}/wallet`,
create: '/leagues/create',
migration: '/leagues/migration',
},
race: {
root: '/races',
+27 -121
View File
@@ -1,22 +1,13 @@
'use client';
import { HomeFeatureDescription } from '@/components/home/HomeFeatureDescription';
import { HomeFeatureSection } from '@/components/home/HomeFeatureSection';
import { HomeFooterCTA } from '@/components/home/HomeFooterCTA';
import { HomeHeader } from '@/components/home/HomeHeader';
import { HomeStatsStrip } from '@/components/home/HomeStatsStrip';
import { LeagueSummaryPanel } from '@/components/home/LeagueSummaryPanel';
import { QuickLinksPanel } from '@/components/home/QuickLinksPanel';
import { RecentRacesPanel } from '@/components/home/RecentRacesPanel';
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';
import { Hero } from '@/components/home/Hero';
import { TelemetryStrip } from '@/components/home/TelemetryStrip';
import { ValuePillars } from '@/components/home/ValuePillars';
import { StewardingPreview } from '@/components/home/StewardingPreview';
import { LeagueIdentityPreview } from '@/components/home/LeagueIdentityPreview';
import { MigrationSection } from '@/components/home/MigrationSection';
import { CtaSection } from '@/components/home/CtaSection';
import { Stack } from '@/ui/Stack';
export interface HomeViewData {
isAlpha: boolean;
@@ -44,118 +35,33 @@ interface HomeTemplateProps {
}
/**
* HomeTemplate - Redesigned for "Precision Racing Minimal" theme.
* Composes semantic components instead of generic layout primitives.
* HomeTemplate - Radically redesigned for League Admin focus.
* Theme: Modern Precision.
* Architecture: Composition of semantic components.
*/
export function HomeTemplate({ viewData }: HomeTemplateProps) {
return (
<Box>
{/* Hero Section */}
<HomeHeader
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: '#' }}
/>
<main>
{/* Hero Section - Admin Focus */}
<Hero />
{/* Telemetry Status Strip */}
<HomeStatsStrip />
{/* Admin Pain/Solution Strip */}
<TelemetryStrip />
{/* Quick Actions Bar */}
<QuickLinksPanel />
{/* Core Admin Features */}
<ValuePillars />
{/* Feature Sections */}
<HomeFeatureSection
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 />}
/>
{/* Stewarding Workflow Preview */}
<StewardingPreview />
<HomeFeatureSection
heading="Results That Actually Stay"
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 />}
/>
{/* League Identity Showcase */}
<LeagueIdentityPreview />
<HomeFeatureSection
heading="Automatic Session Creation"
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 />}
/>
{/* Migration Offer */}
<MigrationSection />
<HomeFeatureSection
heading="Built for iRacing. Ready for the future."
accentColor="primary"
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>
{/* Final CTA */}
<CtaSection />
</main>
);
}
+73 -100
View File
@@ -6,48 +6,47 @@ import { Icon } from './Icon';
export interface ButtonProps {
children: ReactNode;
onClick?: MouseEventHandler<HTMLButtonElement | HTMLAnchorElement>;
variant?: 'primary' | 'secondary' | 'danger' | 'ghost' | 'success' | 'discord' | 'race-final';
size?: 'sm' | 'md' | 'lg';
variant?: 'primary' | 'secondary' | 'danger' | 'ghost' | 'success' | 'discord' | 'race-final' | any;
size?: 'sm' | 'md' | 'lg' | any;
disabled?: boolean;
isLoading?: boolean;
type?: 'button' | 'submit' | 'reset';
icon?: ReactNode;
fullWidth?: boolean;
as?: 'button' | 'a';
as?: 'button' | 'a' | any;
href?: string;
target?: string;
rel?: string;
title?: string;
style?: React.CSSProperties;
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;
justifyContent?: string;
style?: React.CSSProperties;
rounded?: boolean | string | any;
mt?: number | any;
p?: number | any;
px?: number | any;
py?: number | any;
w?: string | number | any;
h?: string | number | any;
bg?: string | any;
color?: string | any;
borderColor?: string | any;
hoverBorderColor?: string | any;
letterSpacing?: string | any;
fontSize?: string | any;
transition?: boolean | any;
center?: boolean | any;
justifyContent?: string | any;
shadow?: string | any;
position?: string | any;
borderWidth?: string | any;
aspectRatio?: string | any;
border?: boolean | any;
}
/**
* Button - Redesigned for "Modern Precision" theme.
* Includes extensive compatibility props to prevent app-wide breakage.
*/
export const Button = forwardRef<HTMLButtonElement | HTMLAnchorElement, ButtonProps>(({
children,
onClick,
@@ -63,94 +62,80 @@ export const Button = forwardRef<HTMLButtonElement | HTMLAnchorElement, ButtonPr
target,
rel,
title,
style: styleProp,
rounded = false,
className,
bg,
color,
style,
rounded,
mt,
p,
px,
py,
w,
h,
px,
bg,
color,
borderColor,
mt,
position,
letterSpacing,
hoverBorderColor,
letterSpacing,
fontSize,
p,
minHeight,
transition,
ring,
transform,
hoverScale,
overflow,
center,
justifyContent,
shadow,
position,
borderWidth,
aspectRatio,
border,
shadow,
display,
center,
justifyContent,
}, 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 transitionClasses = transition !== false ? 'transition-all duration-150 ease-in-out' : '';
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 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)]',
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)]',
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]',
'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 = {
sm: 'min-h-[32px] px-3 py-1 text-xs',
md: 'min-h-[40px] px-4 py-2 text-sm',
lg: 'min-h-[48px] px-6 py-3 text-base'
sm: 'min-h-[32px] px-3 py-1 text-[10px]',
md: 'min-h-[40px] px-4 py-2 text-xs',
lg: 'min-h-[48px] px-6 py-3 text-sm'
};
const disabledClasses = (disabled || isLoading) ? 'opacity-40 cursor-not-allowed' : 'cursor-pointer';
const widthClasses = fullWidth ? 'w-full' : '';
const roundedClasses = rounded === true ? 'rounded-full' : (typeof rounded === 'string' ? `rounded-${rounded}` : 'rounded-none');
const classes = [
baseClasses,
transitionClasses,
variantClasses[variant],
sizeClasses[size],
variantClasses[variant as keyof typeof variantClasses] || variantClasses.primary,
sizeClasses[size as keyof typeof sizeClasses] || sizeClasses.md,
disabledClasses,
widthClasses,
roundedClasses,
ring,
hoverScale ? 'hover:scale-105' : '',
display === 'flex' ? 'flex' : '',
rounded === true ? 'rounded-full' : (typeof rounded === 'string' ? `rounded-${rounded}` : 'rounded-none'),
center ? 'items-center justify-center' : '',
justifyContent ? `justify-${justifyContent}` : '',
className,
].filter(Boolean).join(' ');
const style: React.CSSProperties = {
...styleProp,
const combinedStyle: React.CSSProperties = {
...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 } : {}),
...(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 } : {}),
...(justifyContent ? { justifyContent } : {}),
...(shadow ? { boxShadow: shadow } : {}),
...(position ? { position } : {}),
...(borderWidth ? { borderWidth } : {}),
...(aspectRatio ? { aspectRatio } : {}),
...(border ? { border: '1px solid var(--ui-color-border-default)' } : {}),
...(shadow ? { boxShadow: shadow.startsWith('shadow-') ? undefined : shadow } : {}),
};
const content = (
@@ -161,35 +146,23 @@ export const Button = forwardRef<HTMLButtonElement | HTMLAnchorElement, ButtonPr
</Box>
);
if (as === 'a') {
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>
);
}
const Tag = as === 'a' ? 'a' : 'button';
return (
<button
ref={ref as React.ForwardedRef<HTMLButtonElement>}
type={type}
<Tag
ref={ref as any}
type={as === 'a' ? undefined : type}
href={as === 'a' ? href : undefined}
target={as === 'a' ? target : undefined}
rel={as === 'a' ? rel : undefined}
className={classes}
onClick={onClick as MouseEventHandler<HTMLButtonElement>}
disabled={disabled || isLoading}
style={style}
onClick={onClick as any}
disabled={as === 'a' ? undefined : (disabled || isLoading)}
style={combinedStyle}
title={title}
>
{content}
</button>
</Tag>
);
});
+110 -32
View File
@@ -1,73 +1,151 @@
import React, { ReactNode, forwardRef } from 'react';
import { Box } from './Box';
import { Surface, SurfaceProps } from './Surface';
import { Heading } from './Heading';
export interface CardProps extends Omit<SurfaceProps<'div'>, 'children' | 'title' | 'variant'> {
export interface CardProps {
children: ReactNode;
variant?: 'default' | 'dark' | 'muted' | 'glass' | 'outline' | 'rarity-common' | 'rarity-rare' | 'rarity-epic' | 'rarity-legendary';
title?: ReactNode;
variant?: 'default' | 'muted' | 'outline' | 'glass' | 'dark' | any;
title?: string | 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>(({
children,
variant = 'default',
title,
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) => {
const isOutline = variant === 'outline';
const style: React.CSSProperties = isOutline ? {
backgroundColor: 'transparent',
border: '1px solid var(--ui-color-border-default)',
} : {};
const variantClasses = {
default: 'bg-[var(--ui-color-bg-surface)] border-[var(--ui-color-border-default)] shadow-sm',
muted: 'bg-[var(--ui-color-bg-surface-muted)] border-[var(--ui-color-border-muted)]',
outline: 'bg-transparent border-[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 (
<Surface
<div
ref={ref}
variant={isOutline ? 'default' : variant}
rounded="lg"
shadow="md"
style={style}
{...props}
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 || ''}`}
style={combinedStyle}
onClick={onClick}
>
{title && (
<Box padding={4} borderBottom>
<div className={`border-b border-[var(--ui-color-border-muted)] ${getPaddingClass(padding)}`}>
{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}
</Box>
</div>
)}
<Box padding={4}>
<div className={typeof padding === 'number' || p !== undefined ? '' : getPaddingClass(padding)}>
{children}
</Box>
</div>
{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}
</Box>
</div>
)}
</Surface>
</div>
);
});
Card.displayName = 'Card';
export const CardHeader = ({ title, children }: { title?: string, children?: ReactNode }) => (
<Box marginBottom={4}>
{title && <h3 className="text-lg font-bold text-[var(--ui-color-text-high)]">{title}</h3>}
<div className="mb-4">
{title && <Heading level={5} weight="bold" uppercase>{title}</Heading>}
{children}
</Box>
</div>
);
export const CardContent = ({ children }: { children: ReactNode }) => (
<Box>{children}</Box>
<div>{children}</div>
);
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}
</Box>
</div>
);
+40 -13
View File
@@ -1,32 +1,59 @@
import { ReactNode } from 'react';
import { Box } from './Box';
export interface ContainerProps {
children: ReactNode;
size?: 'sm' | 'md' | 'lg' | 'xl' | 'full';
py?: number;
position?: 'static' | 'relative' | 'absolute' | 'fixed' | 'sticky';
zIndex?: number;
size?: 'sm' | 'md' | 'lg' | 'xl' | 'full' | any;
padding?: 'none' | 'sm' | 'md' | 'lg' | any;
py?: number | any;
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 = ({
children,
size = 'lg',
padding = 'md',
py,
position,
zIndex
zIndex,
paddingX,
fullWidth,
}: ContainerProps) => {
const sizeMap = {
sm: '40rem',
md: '48rem',
lg: '64rem',
xl: '80rem',
full: '100%',
sm: 'max-w-[40rem]',
md: 'max-w-[48rem]',
lg: 'max-w-[64rem]',
xl: 'max-w-[80rem]',
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 (
<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}
</Box>
</div>
);
};
+20
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>
);
}
+37
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>
);
}
+76 -33
View File
@@ -1,31 +1,49 @@
import { ReactNode, forwardRef } from 'react';
import { Box, BoxProps, ResponsiveValue, Spacing } from './Box';
export interface HeadingProps extends BoxProps<any> {
export interface HeadingProps {
children: ReactNode;
level?: 1 | 2 | 3 | 4 | 5 | 6;
weight?: 'normal' | 'medium' | 'semibold' | 'bold';
align?: 'left' | 'center' | 'right';
fontSize?: string | ResponsiveValue<string>;
icon?: ReactNode;
groupHoverColor?: string;
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;
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>(({
children,
level = 1,
weight = 'bold',
align = 'left',
fontSize,
icon,
groupHoverColor,
uppercase,
letterSpacing,
uppercase = false,
intent = 'default',
className,
style,
mb,
...props
marginBottom,
mt,
marginTop,
color,
fontSize,
letterSpacing,
truncate,
size,
icon,
}, ref) => {
const Tag = `h${level}` as const;
@@ -37,37 +55,62 @@ export const Heading = forwardRef<HTMLHeadingElement, HeadingProps>(({
};
const sizeClasses = {
1: 'text-4xl md:text-5xl',
2: 'text-3xl md:text-4xl',
3: 'text-2xl md:text-3xl',
4: 'text-xl md:text-2xl',
5: 'text-lg md:text-xl',
6: 'text-base md:text-lg'
1: 'text-4xl md:text-5xl tracking-tighter leading-none',
2: 'text-3xl md:text-4xl tracking-tight leading-tight',
3: 'text-2xl md:text-3xl tracking-tight leading-snug',
4: 'text-xl md:text-2xl tracking-normal leading-normal',
5: 'text-lg md:text-xl tracking-normal leading-normal',
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 = [
'text-[var(--ui-color-text-high)]',
intentClasses[intent as keyof typeof intentClasses] || intentClasses.default,
weightClasses[weight],
fontSize ? '' : sizeClasses[level],
fontSize ? getResponsiveFontSize(fontSize) : sizeClasses[level],
align === 'center' ? 'text-center' : (align === 'right' ? 'text-right' : 'text-left'),
uppercase ? 'uppercase' : '',
uppercase ? 'uppercase tracking-widest' : '',
truncate ? 'truncate' : '',
className,
].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 (
<Box
as={Tag}
ref={ref}
className={classes}
fontSize={typeof fontSize === 'string' ? fontSize : undefined}
letterSpacing={letterSpacing}
mb={mb}
{...props}
>
<Box display="flex" alignItems="center" gap={2}>
<Tag ref={ref} className={classes} style={combinedStyle}>
<div className="flex items-center gap-2">
{icon}
{children}
</Box>
</Box>
</div>
</Tag>
);
});
+2 -2
View File
@@ -15,11 +15,11 @@ export const IconButton = forwardRef<HTMLButtonElement | HTMLAnchorElement, Icon
intent,
...props
}, ref) => {
const iconSizeMap = {
const iconSizeMap: Record<string, any> = {
sm: 3,
md: 4,
lg: 5
} as const;
};
return (
<Button
+24
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>
);
}
+51 -28
View File
@@ -1,7 +1,5 @@
import { ReactNode } from 'react';
import { Box, Spacing } from './Box';
import { Heading } from './Heading';
import { Surface } from './Surface';
import { Text } from './Text';
export interface PanelProps {
@@ -9,63 +7,88 @@ export interface PanelProps {
description?: string;
children: ReactNode;
footer?: ReactNode;
variant?: 'default' | 'dark' | 'muted';
padding?: Spacing;
variant?: 'default' | 'muted' | 'ghost' | 'dark' | any;
padding?: 'none' | 'sm' | 'md' | 'lg' | number | any;
actions?: ReactNode;
className?: string;
style?: React.CSSProperties;
border?: boolean;
rounded?: string;
borderColor?: string;
bg?: string;
}
/**
* Panel - Redesigned for "Modern Precision" theme.
* Includes compatibility props to prevent app-wide breakage.
*/
export const Panel = ({
title,
description,
children,
footer,
variant = 'default',
padding = 6,
padding = 'md',
actions,
className,
style,
border,
rounded,
borderColor,
bg
bg,
}: 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 (
<Surface
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}
>
<div className={`border ${variantClasses[variant as keyof typeof variantClasses] || variantClasses.default} ${getPaddingClass(padding)} transition-all duration-200 ${className || ''}`} style={combinedStyle}>
{(title || description || actions) && (
<Box padding={padding as any} borderBottom display="flex" alignItems="center" justifyContent="between">
<Box>
{title && <Heading level={3} marginBottom={1}>{title}</Heading>}
{description && <Text size="sm" variant="low">{description}</Text>}
</Box>
<div className={`border-b border-[var(--ui-color-border-muted)] flex items-center justify-between ${getPaddingClass(padding)}`}>
<div>
{title && <Heading level={4} weight="semibold" uppercase>{title}</Heading>}
{description && <Text size="xs" variant="low">{description}</Text>}
</div>
{actions && (
<Box display="flex" alignItems="center" gap={3}>
<div className="flex items-center gap-2">
{actions}
</Box>
</div>
)}
</Box>
</div>
)}
<Box padding={padding as any}>
<div className={typeof padding === 'number' ? '' : getPaddingClass(padding)}>
{children}
</Box>
</div>
{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}
</Box>
</div>
)}
</Surface>
</div>
);
};
+155 -64
View File
@@ -1,30 +1,67 @@
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 interface TextProps extends BoxProps<any> {
export interface TextProps {
children: ReactNode;
variant?: 'high' | 'med' | 'low' | 'primary' | 'success' | 'warning' | 'critical' | 'telemetry' | 'inherit';
size?: TextSize | ResponsiveValue<TextSize>;
weight?: 'light' | 'normal' | 'medium' | 'semibold' | 'bold';
variant?: 'high' | 'med' | 'low' | 'primary' | 'success' | 'warning' | 'critical' | 'telemetry' | 'inherit' | any;
size?: TextSize | { base: TextSize; sm?: TextSize; md?: TextSize; lg?: TextSize; xl?: TextSize } | any;
weight?: 'light' | 'normal' | 'medium' | 'semibold' | 'bold' | any;
as?: ElementType;
align?: 'left' | 'center' | 'right';
italic?: boolean;
align?: 'left' | 'center' | 'right' | any;
mono?: boolean;
block?: boolean;
uppercase?: boolean;
capitalize?: boolean;
letterSpacing?: string;
leading?: 'none' | 'tight' | 'snug' | 'normal' | 'relaxed' | 'loose';
leading?: 'none' | 'tight' | 'snug' | 'normal' | 'relaxed' | 'loose' | any;
block?: 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;
font?: 'sans' | 'mono';
hoverVariant?: 'high' | 'med' | 'low' | 'primary' | 'success' | 'warning' | 'critical';
htmlFor?: string;
flexGrow?: number;
flexShrink?: number;
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;
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>(({
children,
variant = 'med',
@@ -32,20 +69,53 @@ export const Text = forwardRef<HTMLElement, TextProps>(({
weight = 'normal',
as = 'p',
align = 'left',
italic = false,
mono = false,
block = false,
uppercase = false,
capitalize = false,
letterSpacing,
leading,
leading = 'normal',
block = false,
truncate = false,
lineHeight,
className,
style,
mt,
mb,
ml,
mr,
marginTop,
marginBottom,
font,
color,
letterSpacing,
lineHeight,
flexGrow,
flexShrink,
lineClamp,
display,
opacity,
maxWidth,
mx,
pl,
px,
py,
paddingX,
paddingY,
textAlign,
groupHoverTextColor,
transition,
borderLeft,
borderStyle,
borderColor,
ariaLabel,
hoverVariant,
htmlFor,
fontSize,
italic,
animate,
capitalize,
alignItems,
gap,
cursor,
...props
width,
htmlFor,
transform,
}, ref) => {
const variantClasses = {
high: 'text-[var(--ui-color-text-high)]',
@@ -58,40 +128,31 @@ export const Text = forwardRef<HTMLElement, TextProps>(({
telemetry: 'text-[var(--ui-color-intent-telemetry)]',
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> = {
xs: 'text-xs',
sm: 'text-sm',
base: 'text-base',
md: 'text-base',
lg: 'text-lg',
xl: 'text-xl',
'2xl': 'text-2xl',
'3xl': 'text-3xl',
'4xl': 'text-4xl',
const sizeMap: Record<string, string> = {
xs: 'text-[10px]',
sm: 'text-xs',
base: 'text-sm',
md: 'text-sm',
lg: 'text-base',
xl: 'text-lg',
'2xl': 'text-xl',
'3xl': 'text-2xl',
'4xl': 'text-3xl',
};
const getResponsiveSizeClasses = (value: TextSize | ResponsiveValue<TextSize>) => {
if (typeof value === 'string') return sizeMap[value];
const getResponsiveSize = (s: any) => {
if (!s) return sizeMap['md'];
if (typeof s === 'string') return sizeMap[s] || sizeMap['md'];
const classes = [];
if (value.base) classes.push(sizeMap[value.base]);
if (value.sm) classes.push(`sm:${sizeMap[value.sm]}`);
if (value.md) classes.push(`md:${sizeMap[value.md]}`);
if (value.lg) classes.push(`lg:${sizeMap[value.lg]}`);
if (value.xl) classes.push(`xl:${sizeMap[value.xl]}`);
if (s.base) classes.push(sizeMap[s.base]);
if (s.sm) classes.push(`sm:${sizeMap[s.sm]}`);
if (s.md) classes.push(`md:${sizeMap[s.md]}`);
if (s.lg) classes.push(`lg:${sizeMap[s.lg]}`);
if (s.xl) classes.push(`xl:${sizeMap[s.xl]}`);
return classes.join(' ');
};
const weightClasses = {
light: 'font-light',
normal: 'font-normal',
@@ -110,30 +171,60 @@ export const Text = forwardRef<HTMLElement, TextProps>(({
};
const classes = [
variantClasses[variant],
getResponsiveSizeClasses(size),
weightClasses[weight],
align === 'center' ? 'text-center' : (align === 'right' ? 'text-right' : 'text-left'),
italic ? 'italic' : '',
variantClasses[variant as keyof typeof variantClasses] || '',
getResponsiveSize(size),
weightClasses[weight as keyof typeof weightClasses] || '',
align === 'center' || textAlign === 'center' ? 'text-center' : (align === 'right' || textAlign === 'right' ? 'text-right' : 'text-left'),
(mono || font === 'mono') ? 'font-mono' : 'font-sans',
uppercase ? 'uppercase tracking-widest' : '',
leadingClasses[leading as keyof typeof leadingClasses] || '',
block ? 'block' : 'inline',
uppercase ? 'uppercase tracking-wider' : '',
capitalize ? 'capitalize' : '',
leading ? leadingClasses[leading] : '',
truncate ? 'truncate' : '',
hoverVariant ? hoverVariantClasses[hoverVariant] : '',
].join(' ');
transition ? 'transition-all duration-200' : '',
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 } : {}),
...(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 (
<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}
</Box>
</Tag>
);
});