website refactor
This commit is contained in:
20
apps/website/app/leagues/migration/page.tsx
Normal file
20
apps/website/app/leagues/migration/page.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
50
apps/website/components/home/CtaSection.tsx
Normal file
50
apps/website/components/home/CtaSection.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
96
apps/website/components/home/EcosystemGrid.tsx
Normal file
96
apps/website/components/home/EcosystemGrid.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
65
apps/website/components/home/Hero.tsx
Normal file
65
apps/website/components/home/Hero.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
151
apps/website/components/home/LeagueIdentityPreview.tsx
Normal file
151
apps/website/components/home/LeagueIdentityPreview.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
47
apps/website/components/home/MigrationSection.tsx
Normal file
47
apps/website/components/home/MigrationSection.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
101
apps/website/components/home/StewardingPreview.tsx
Normal file
101
apps/website/components/home/StewardingPreview.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
45
apps/website/components/home/TelemetryStrip.tsx
Normal file
45
apps/website/components/home/TelemetryStrip.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
50
apps/website/components/home/ValuePillars.tsx
Normal file
50
apps/website/components/home/ValuePillars.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
});
|
||||
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
|
||||
@@ -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
apps/website/ui/FeatureGrid.tsx
Normal file
20
apps/website/ui/FeatureGrid.tsx
Normal 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
apps/website/ui/FeatureItem.tsx
Normal file
37
apps/website/ui/FeatureItem.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
});
|
||||
|
||||
|
||||
@@ -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
apps/website/ui/IconContainer.tsx
Normal file
24
apps/website/ui/IconContainer.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
});
|
||||
|
||||
|
||||
Reference in New Issue
Block a user