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;
|
stewarding: (id: string) => string;
|
||||||
wallet: (id: string) => string;
|
wallet: (id: string) => string;
|
||||||
create: string;
|
create: string;
|
||||||
|
migration: string;
|
||||||
};
|
};
|
||||||
race: {
|
race: {
|
||||||
root: string;
|
root: string;
|
||||||
@@ -169,6 +170,7 @@ export const routes: RouteGroup & { leaderboards: { root: string; drivers: strin
|
|||||||
stewarding: (id: string) => `/leagues/${id}/stewarding`,
|
stewarding: (id: string) => `/leagues/${id}/stewarding`,
|
||||||
wallet: (id: string) => `/leagues/${id}/wallet`,
|
wallet: (id: string) => `/leagues/${id}/wallet`,
|
||||||
create: '/leagues/create',
|
create: '/leagues/create',
|
||||||
|
migration: '/leagues/migration',
|
||||||
},
|
},
|
||||||
race: {
|
race: {
|
||||||
root: '/races',
|
root: '/races',
|
||||||
|
|||||||
@@ -1,22 +1,13 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import { HomeFeatureDescription } from '@/components/home/HomeFeatureDescription';
|
import { Hero } from '@/components/home/Hero';
|
||||||
import { HomeFeatureSection } from '@/components/home/HomeFeatureSection';
|
import { TelemetryStrip } from '@/components/home/TelemetryStrip';
|
||||||
import { HomeFooterCTA } from '@/components/home/HomeFooterCTA';
|
import { ValuePillars } from '@/components/home/ValuePillars';
|
||||||
import { HomeHeader } from '@/components/home/HomeHeader';
|
import { StewardingPreview } from '@/components/home/StewardingPreview';
|
||||||
import { HomeStatsStrip } from '@/components/home/HomeStatsStrip';
|
import { LeagueIdentityPreview } from '@/components/home/LeagueIdentityPreview';
|
||||||
import { LeagueSummaryPanel } from '@/components/home/LeagueSummaryPanel';
|
import { MigrationSection } from '@/components/home/MigrationSection';
|
||||||
import { QuickLinksPanel } from '@/components/home/QuickLinksPanel';
|
import { CtaSection } from '@/components/home/CtaSection';
|
||||||
import { RecentRacesPanel } from '@/components/home/RecentRacesPanel';
|
import { Stack } from '@/ui/Stack';
|
||||||
import { TeamSummaryPanel } from '@/components/home/TeamSummaryPanel';
|
|
||||||
import { FAQ } from '@/components/landing/FAQ';
|
|
||||||
import { CareerProgressionMockup } from '@/components/mockups/CareerProgressionMockup';
|
|
||||||
import { CompanionAutomationMockup } from '@/components/mockups/CompanionAutomationMockup';
|
|
||||||
import { RaceHistoryMockup } from '@/components/mockups/RaceHistoryMockup';
|
|
||||||
import { SimPlatformMockup } from '@/components/mockups/SimPlatformMockup';
|
|
||||||
import { ModeGuard } from '@/components/shared/ModeGuard';
|
|
||||||
import { DiscoverySection } from '@/ui/DiscoverySection';
|
|
||||||
import { Box } from '@/ui/Box';
|
|
||||||
|
|
||||||
export interface HomeViewData {
|
export interface HomeViewData {
|
||||||
isAlpha: boolean;
|
isAlpha: boolean;
|
||||||
@@ -44,118 +35,33 @@ interface HomeTemplateProps {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* HomeTemplate - Redesigned for "Precision Racing Minimal" theme.
|
* HomeTemplate - Radically redesigned for League Admin focus.
|
||||||
* Composes semantic components instead of generic layout primitives.
|
* Theme: Modern Precision.
|
||||||
|
* Architecture: Composition of semantic components.
|
||||||
*/
|
*/
|
||||||
export function HomeTemplate({ viewData }: HomeTemplateProps) {
|
export function HomeTemplate({ viewData }: HomeTemplateProps) {
|
||||||
return (
|
return (
|
||||||
<Box>
|
<main>
|
||||||
{/* Hero Section */}
|
{/* Hero Section - Admin Focus */}
|
||||||
<HomeHeader
|
<Hero />
|
||||||
title="Modern Motorsport Infrastructure."
|
|
||||||
subtitle="Precision Racing Infrastructure"
|
|
||||||
description="GridPilot gives your league racing a real home. Results, standings, teams, and career progression — engineered for precision and control."
|
|
||||||
primaryAction={{ label: 'Join the Grid', href: '#' }}
|
|
||||||
secondaryAction={{ label: 'Explore Leagues', href: '#' }}
|
|
||||||
/>
|
|
||||||
|
|
||||||
{/* Telemetry Status Strip */}
|
{/* Admin Pain/Solution Strip */}
|
||||||
<HomeStatsStrip />
|
<TelemetryStrip />
|
||||||
|
|
||||||
{/* Quick Actions Bar */}
|
{/* Core Admin Features */}
|
||||||
<QuickLinksPanel />
|
<ValuePillars />
|
||||||
|
|
||||||
{/* Feature Sections */}
|
{/* Stewarding Workflow Preview */}
|
||||||
<HomeFeatureSection
|
<StewardingPreview />
|
||||||
heading="A Persistent Identity"
|
|
||||||
accentColor="primary"
|
|
||||||
layout="text-left"
|
|
||||||
description={
|
|
||||||
<HomeFeatureDescription
|
|
||||||
lead="Your races, your seasons, your progress — finally in one place."
|
|
||||||
items={[
|
|
||||||
'Lifetime stats and season history across all your leagues',
|
|
||||||
'Track your performance, consistency, and team contributions',
|
|
||||||
'Your own rating that reflects real league competition',
|
|
||||||
]}
|
|
||||||
quote="iRacing gives you physics. GridPilot gives you a career."
|
|
||||||
accentColor="primary"
|
|
||||||
/>
|
|
||||||
}
|
|
||||||
mockup={<CareerProgressionMockup />}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<HomeFeatureSection
|
{/* League Identity Showcase */}
|
||||||
heading="Results That Actually Stay"
|
<LeagueIdentityPreview />
|
||||||
accentColor="aqua"
|
|
||||||
layout="text-right"
|
|
||||||
description={
|
|
||||||
<HomeFeatureDescription
|
|
||||||
lead="Every race you run stays with you."
|
|
||||||
items={[
|
|
||||||
'Your stats, your team, your story — all connected',
|
|
||||||
'One race result updates your profile, team points, rating, and season history',
|
|
||||||
'No more fragmented data across spreadsheets and forums',
|
|
||||||
]}
|
|
||||||
quote="Your racing career, finally in one place."
|
|
||||||
accentColor="aqua"
|
|
||||||
/>
|
|
||||||
}
|
|
||||||
mockup={<RaceHistoryMockup />}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<HomeFeatureSection
|
{/* Migration Offer */}
|
||||||
heading="Automatic Session Creation"
|
<MigrationSection />
|
||||||
accentColor="amber"
|
|
||||||
layout="text-left"
|
|
||||||
description={
|
|
||||||
<HomeFeatureDescription
|
|
||||||
lead="Setting up league races used to mean clicking through iRacing's wizard 20 times."
|
|
||||||
items={[
|
|
||||||
'Our companion app syncs with your league schedule',
|
|
||||||
'When it\'s race time, it creates the iRacing session automatically',
|
|
||||||
'No clicking through wizards. No manual setup',
|
|
||||||
]}
|
|
||||||
quote="Automation instead of repetition."
|
|
||||||
accentColor="amber"
|
|
||||||
/>
|
|
||||||
}
|
|
||||||
mockup={<CompanionAutomationMockup />}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<HomeFeatureSection
|
{/* Final CTA */}
|
||||||
heading="Built for iRacing. Ready for the future."
|
<CtaSection />
|
||||||
accentColor="primary"
|
</main>
|
||||||
layout="text-right"
|
|
||||||
description={
|
|
||||||
<HomeFeatureDescription
|
|
||||||
lead="Right now, we're focused on making iRacing league racing better."
|
|
||||||
items={[
|
|
||||||
'But sims come and go. Your leagues, your teams, your rating — those stay.',
|
|
||||||
]}
|
|
||||||
quote="GridPilot is built to outlast any single platform."
|
|
||||||
accentColor="gray"
|
|
||||||
/>
|
|
||||||
}
|
|
||||||
mockup={<SimPlatformMockup />}
|
|
||||||
/>
|
|
||||||
|
|
||||||
{/* Discovery Grid */}
|
|
||||||
<ModeGuard feature="alpha_discovery">
|
|
||||||
<DiscoverySection
|
|
||||||
title="DISCOVER THE GRID"
|
|
||||||
subtitle="Live Ecosystem"
|
|
||||||
description="Explore leagues, teams, and races that make up the GridPilot ecosystem."
|
|
||||||
>
|
|
||||||
<LeagueSummaryPanel leagues={viewData.topLeagues} />
|
|
||||||
<TeamSummaryPanel teams={viewData.teams} />
|
|
||||||
<RecentRacesPanel races={viewData.upcomingRaces} />
|
|
||||||
</DiscoverySection>
|
|
||||||
</ModeGuard>
|
|
||||||
|
|
||||||
{/* CTA & FAQ */}
|
|
||||||
<HomeFooterCTA />
|
|
||||||
<FAQ />
|
|
||||||
</Box>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,48 +6,47 @@ import { Icon } from './Icon';
|
|||||||
export interface ButtonProps {
|
export interface ButtonProps {
|
||||||
children: ReactNode;
|
children: ReactNode;
|
||||||
onClick?: MouseEventHandler<HTMLButtonElement | HTMLAnchorElement>;
|
onClick?: MouseEventHandler<HTMLButtonElement | HTMLAnchorElement>;
|
||||||
variant?: 'primary' | 'secondary' | 'danger' | 'ghost' | 'success' | 'discord' | 'race-final';
|
variant?: 'primary' | 'secondary' | 'danger' | 'ghost' | 'success' | 'discord' | 'race-final' | any;
|
||||||
size?: 'sm' | 'md' | 'lg';
|
size?: 'sm' | 'md' | 'lg' | any;
|
||||||
disabled?: boolean;
|
disabled?: boolean;
|
||||||
isLoading?: boolean;
|
isLoading?: boolean;
|
||||||
type?: 'button' | 'submit' | 'reset';
|
type?: 'button' | 'submit' | 'reset';
|
||||||
icon?: ReactNode;
|
icon?: ReactNode;
|
||||||
fullWidth?: boolean;
|
fullWidth?: boolean;
|
||||||
as?: 'button' | 'a';
|
as?: 'button' | 'a' | any;
|
||||||
href?: string;
|
href?: string;
|
||||||
target?: string;
|
target?: string;
|
||||||
rel?: string;
|
rel?: string;
|
||||||
title?: string;
|
title?: string;
|
||||||
style?: React.CSSProperties;
|
|
||||||
rounded?: boolean | string;
|
|
||||||
className?: string;
|
className?: string;
|
||||||
bg?: string;
|
style?: React.CSSProperties;
|
||||||
color?: string;
|
rounded?: boolean | string | any;
|
||||||
w?: string | number;
|
mt?: number | any;
|
||||||
h?: string | number;
|
p?: number | any;
|
||||||
px?: number;
|
px?: number | any;
|
||||||
borderColor?: string;
|
py?: number | any;
|
||||||
mt?: number;
|
w?: string | number | any;
|
||||||
position?: 'static' | 'relative' | 'absolute' | 'fixed' | 'sticky';
|
h?: string | number | any;
|
||||||
letterSpacing?: string;
|
bg?: string | any;
|
||||||
hoverBorderColor?: string;
|
color?: string | any;
|
||||||
fontSize?: string;
|
borderColor?: string | any;
|
||||||
p?: number;
|
hoverBorderColor?: string | any;
|
||||||
minHeight?: string;
|
letterSpacing?: string | any;
|
||||||
transition?: boolean;
|
fontSize?: string | any;
|
||||||
ring?: string;
|
transition?: boolean | any;
|
||||||
transform?: string;
|
center?: boolean | any;
|
||||||
hoverScale?: boolean;
|
justifyContent?: string | any;
|
||||||
overflow?: string;
|
shadow?: string | any;
|
||||||
borderWidth?: string;
|
position?: string | any;
|
||||||
aspectRatio?: string;
|
borderWidth?: string | any;
|
||||||
border?: boolean;
|
aspectRatio?: string | any;
|
||||||
shadow?: string;
|
border?: boolean | any;
|
||||||
display?: string;
|
|
||||||
center?: boolean;
|
|
||||||
justifyContent?: string;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Button - Redesigned for "Modern Precision" theme.
|
||||||
|
* Includes extensive compatibility props to prevent app-wide breakage.
|
||||||
|
*/
|
||||||
export const Button = forwardRef<HTMLButtonElement | HTMLAnchorElement, ButtonProps>(({
|
export const Button = forwardRef<HTMLButtonElement | HTMLAnchorElement, ButtonProps>(({
|
||||||
children,
|
children,
|
||||||
onClick,
|
onClick,
|
||||||
@@ -63,94 +62,80 @@ export const Button = forwardRef<HTMLButtonElement | HTMLAnchorElement, ButtonPr
|
|||||||
target,
|
target,
|
||||||
rel,
|
rel,
|
||||||
title,
|
title,
|
||||||
style: styleProp,
|
|
||||||
rounded = false,
|
|
||||||
className,
|
className,
|
||||||
bg,
|
style,
|
||||||
color,
|
rounded,
|
||||||
|
mt,
|
||||||
|
p,
|
||||||
|
px,
|
||||||
|
py,
|
||||||
w,
|
w,
|
||||||
h,
|
h,
|
||||||
px,
|
bg,
|
||||||
|
color,
|
||||||
borderColor,
|
borderColor,
|
||||||
mt,
|
|
||||||
position,
|
|
||||||
letterSpacing,
|
|
||||||
hoverBorderColor,
|
hoverBorderColor,
|
||||||
|
letterSpacing,
|
||||||
fontSize,
|
fontSize,
|
||||||
p,
|
|
||||||
minHeight,
|
|
||||||
transition,
|
transition,
|
||||||
ring,
|
center,
|
||||||
transform,
|
justifyContent,
|
||||||
hoverScale,
|
shadow,
|
||||||
overflow,
|
position,
|
||||||
borderWidth,
|
borderWidth,
|
||||||
aspectRatio,
|
aspectRatio,
|
||||||
border,
|
border,
|
||||||
shadow,
|
|
||||||
display,
|
|
||||||
center,
|
|
||||||
justifyContent,
|
|
||||||
}, ref) => {
|
}, ref) => {
|
||||||
const baseClasses = 'inline-flex items-center justify-center focus-visible:outline focus-visible:outline-1 focus-visible:outline-offset-2 active:opacity-80 uppercase tracking-widest font-bold';
|
const baseClasses = 'inline-flex items-center justify-center focus-visible:outline focus-visible:outline-1 focus-visible:outline-offset-2 active:opacity-80 uppercase tracking-widest font-bold transition-all duration-150 ease-in-out';
|
||||||
const transitionClasses = transition !== false ? 'transition-all duration-150 ease-in-out' : '';
|
|
||||||
|
|
||||||
const variantClasses = {
|
const variantClasses = {
|
||||||
primary: 'bg-[var(--ui-color-intent-primary)] text-white hover:opacity-90 focus-visible:outline-[var(--ui-color-intent-primary)] shadow-[0_0_15px_rgba(25,140,255,0.3)] hover:shadow-[0_0_25px_rgba(25,140,255,0.5)]',
|
primary: 'bg-[var(--ui-color-intent-primary)] text-white hover:opacity-90 focus-visible:outline-[var(--ui-color-intent-primary)] shadow-[0_0_15px_rgba(25,140,255,0.2)]',
|
||||||
secondary: 'bg-[var(--ui-color-bg-surface)] text-white border border-[var(--ui-color-border-default)] hover:bg-[var(--ui-color-border-default)] focus-visible:outline-[var(--ui-color-intent-primary)]',
|
secondary: 'bg-[var(--ui-color-bg-surface)] text-white border border-[var(--ui-color-border-default)] hover:bg-[var(--ui-color-border-default)] focus-visible:outline-[var(--ui-color-intent-primary)]',
|
||||||
danger: 'bg-[var(--ui-color-intent-critical)] text-white hover:opacity-90 focus-visible:outline-[var(--ui-color-intent-critical)]',
|
danger: 'bg-[var(--ui-color-intent-critical)] text-white hover:opacity-90 focus-visible:outline-[var(--ui-color-intent-critical)]',
|
||||||
ghost: 'bg-transparent text-[var(--ui-color-text-low)] hover:text-[var(--ui-color-text-high)] hover:bg-white/5 focus-visible:outline-[var(--ui-color-text-low)]',
|
ghost: 'bg-transparent text-[var(--ui-color-text-low)] hover:text-[var(--ui-color-text-high)] hover:bg-white/5 focus-visible:outline-[var(--ui-color-text-low)]',
|
||||||
success: 'bg-[var(--ui-color-intent-success)] text-[var(--ui-color-bg-base)] hover:opacity-90 focus-visible:outline-[var(--ui-color-intent-success)]',
|
success: 'bg-[var(--ui-color-intent-success)] text-[var(--ui-color-bg-base)] hover:opacity-90 focus-visible:outline-[var(--ui-color-intent-success)]',
|
||||||
'race-final': 'bg-[var(--ui-color-intent-success)] text-[var(--ui-color-bg-base)] hover:opacity-90 focus-visible:outline-[var(--ui-color-intent-success)]',
|
|
||||||
discord: 'bg-[#5865F2] text-white hover:bg-[#4752C4] focus-visible:outline-[#5865F2]',
|
discord: 'bg-[#5865F2] text-white hover:bg-[#4752C4] focus-visible:outline-[#5865F2]',
|
||||||
|
'race-final': 'bg-[var(--ui-color-intent-success)] text-[var(--ui-color-bg-base)] hover:opacity-90 focus-visible:outline-[var(--ui-color-intent-success)]',
|
||||||
};
|
};
|
||||||
|
|
||||||
const sizeClasses = {
|
const sizeClasses = {
|
||||||
sm: 'min-h-[32px] px-3 py-1 text-xs',
|
sm: 'min-h-[32px] px-3 py-1 text-[10px]',
|
||||||
md: 'min-h-[40px] px-4 py-2 text-sm',
|
md: 'min-h-[40px] px-4 py-2 text-xs',
|
||||||
lg: 'min-h-[48px] px-6 py-3 text-base'
|
lg: 'min-h-[48px] px-6 py-3 text-sm'
|
||||||
};
|
};
|
||||||
|
|
||||||
const disabledClasses = (disabled || isLoading) ? 'opacity-40 cursor-not-allowed' : 'cursor-pointer';
|
const disabledClasses = (disabled || isLoading) ? 'opacity-40 cursor-not-allowed' : 'cursor-pointer';
|
||||||
const widthClasses = fullWidth ? 'w-full' : '';
|
const widthClasses = fullWidth ? 'w-full' : '';
|
||||||
const roundedClasses = rounded === true ? 'rounded-full' : (typeof rounded === 'string' ? `rounded-${rounded}` : 'rounded-none');
|
|
||||||
|
|
||||||
const classes = [
|
const classes = [
|
||||||
baseClasses,
|
baseClasses,
|
||||||
transitionClasses,
|
variantClasses[variant as keyof typeof variantClasses] || variantClasses.primary,
|
||||||
variantClasses[variant],
|
sizeClasses[size as keyof typeof sizeClasses] || sizeClasses.md,
|
||||||
sizeClasses[size],
|
|
||||||
disabledClasses,
|
disabledClasses,
|
||||||
widthClasses,
|
widthClasses,
|
||||||
roundedClasses,
|
rounded === true ? 'rounded-full' : (typeof rounded === 'string' ? `rounded-${rounded}` : 'rounded-none'),
|
||||||
ring,
|
|
||||||
hoverScale ? 'hover:scale-105' : '',
|
|
||||||
display === 'flex' ? 'flex' : '',
|
|
||||||
center ? 'items-center justify-center' : '',
|
center ? 'items-center justify-center' : '',
|
||||||
justifyContent ? `justify-${justifyContent}` : '',
|
|
||||||
className,
|
className,
|
||||||
].filter(Boolean).join(' ');
|
].filter(Boolean).join(' ');
|
||||||
|
|
||||||
const style: React.CSSProperties = {
|
const combinedStyle: React.CSSProperties = {
|
||||||
...styleProp,
|
...style,
|
||||||
|
...(mt !== undefined ? { marginTop: typeof mt === 'number' ? `${mt * 0.25}rem` : mt } : {}),
|
||||||
|
...(p !== undefined ? { padding: typeof p === 'number' ? `${p * 0.25}rem` : p } : {}),
|
||||||
|
...(px !== undefined ? { paddingLeft: typeof px === 'number' ? `${px * 0.25}rem` : px, paddingRight: typeof px === 'number' ? `${px * 0.25}rem` : px } : {}),
|
||||||
|
...(py !== undefined ? { paddingTop: typeof py === 'number' ? `${py * 0.25}rem` : py, paddingBottom: typeof py === 'number' ? `${py * 0.25}rem` : py } : {}),
|
||||||
|
...(w !== undefined ? { width: w } : {}),
|
||||||
|
...(h !== undefined ? { height: h } : {}),
|
||||||
...(bg ? { backgroundColor: bg.startsWith('bg-') ? undefined : bg } : {}),
|
...(bg ? { backgroundColor: bg.startsWith('bg-') ? undefined : bg } : {}),
|
||||||
...(color ? { color: color.startsWith('text-') ? undefined : color } : {}),
|
...(color ? { color: color.startsWith('text-') ? undefined : color } : {}),
|
||||||
...(w ? { width: w } : {}),
|
|
||||||
...(h ? { height: h } : {}),
|
|
||||||
...(px ? { paddingLeft: `${px * 0.25}rem`, paddingRight: `${px * 0.25}rem` } : {}),
|
|
||||||
...(borderColor ? { borderColor: borderColor.startsWith('border-') ? undefined : borderColor, borderStyle: 'solid', borderWidth: '1px' } : {}),
|
...(borderColor ? { borderColor: borderColor.startsWith('border-') ? undefined : borderColor, borderStyle: 'solid', borderWidth: '1px' } : {}),
|
||||||
...(mt ? { marginTop: `${mt * 0.25}rem` } : {}),
|
|
||||||
...(position ? { position } : {}),
|
|
||||||
...(letterSpacing ? { letterSpacing } : {}),
|
...(letterSpacing ? { letterSpacing } : {}),
|
||||||
...(fontSize ? { fontSize } : {}),
|
...(fontSize ? { fontSize } : {}),
|
||||||
...(p !== undefined ? { padding: `${p * 0.25}rem` } : {}),
|
...(justifyContent ? { justifyContent } : {}),
|
||||||
...(minHeight ? { minHeight } : {}),
|
...(shadow ? { boxShadow: shadow } : {}),
|
||||||
...(transform ? { transform } : {}),
|
...(position ? { position } : {}),
|
||||||
...(overflow ? { overflow } : {}),
|
|
||||||
...(borderWidth ? { borderWidth } : {}),
|
...(borderWidth ? { borderWidth } : {}),
|
||||||
...(aspectRatio ? { aspectRatio } : {}),
|
...(aspectRatio ? { aspectRatio } : {}),
|
||||||
...(border ? { border: '1px solid var(--ui-color-border-default)' } : {}),
|
|
||||||
...(shadow ? { boxShadow: shadow.startsWith('shadow-') ? undefined : shadow } : {}),
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const content = (
|
const content = (
|
||||||
@@ -161,35 +146,23 @@ export const Button = forwardRef<HTMLButtonElement | HTMLAnchorElement, ButtonPr
|
|||||||
</Box>
|
</Box>
|
||||||
);
|
);
|
||||||
|
|
||||||
if (as === 'a') {
|
const Tag = as === 'a' ? 'a' : 'button';
|
||||||
return (
|
|
||||||
<a
|
|
||||||
ref={ref as React.ForwardedRef<HTMLAnchorElement>}
|
|
||||||
href={href}
|
|
||||||
target={target}
|
|
||||||
rel={rel}
|
|
||||||
className={classes}
|
|
||||||
onClick={onClick as MouseEventHandler<HTMLAnchorElement>}
|
|
||||||
style={style}
|
|
||||||
title={title}
|
|
||||||
>
|
|
||||||
{content}
|
|
||||||
</a>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<button
|
<Tag
|
||||||
ref={ref as React.ForwardedRef<HTMLButtonElement>}
|
ref={ref as any}
|
||||||
type={type}
|
type={as === 'a' ? undefined : type}
|
||||||
|
href={as === 'a' ? href : undefined}
|
||||||
|
target={as === 'a' ? target : undefined}
|
||||||
|
rel={as === 'a' ? rel : undefined}
|
||||||
className={classes}
|
className={classes}
|
||||||
onClick={onClick as MouseEventHandler<HTMLButtonElement>}
|
onClick={onClick as any}
|
||||||
disabled={disabled || isLoading}
|
disabled={as === 'a' ? undefined : (disabled || isLoading)}
|
||||||
style={style}
|
style={combinedStyle}
|
||||||
title={title}
|
title={title}
|
||||||
>
|
>
|
||||||
{content}
|
{content}
|
||||||
</button>
|
</Tag>
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -1,73 +1,151 @@
|
|||||||
import React, { ReactNode, forwardRef } from 'react';
|
import React, { ReactNode, forwardRef } from 'react';
|
||||||
import { Box } from './Box';
|
import { Heading } from './Heading';
|
||||||
import { Surface, SurfaceProps } from './Surface';
|
|
||||||
|
|
||||||
export interface CardProps extends Omit<SurfaceProps<'div'>, 'children' | 'title' | 'variant'> {
|
export interface CardProps {
|
||||||
children: ReactNode;
|
children: ReactNode;
|
||||||
variant?: 'default' | 'dark' | 'muted' | 'glass' | 'outline' | 'rarity-common' | 'rarity-rare' | 'rarity-epic' | 'rarity-legendary';
|
variant?: 'default' | 'muted' | 'outline' | 'glass' | 'dark' | any;
|
||||||
title?: ReactNode;
|
title?: string | ReactNode;
|
||||||
footer?: ReactNode;
|
footer?: ReactNode;
|
||||||
|
padding?: 'none' | 'sm' | 'md' | 'lg' | number | any;
|
||||||
|
className?: string;
|
||||||
|
style?: React.CSSProperties;
|
||||||
|
bg?: string;
|
||||||
|
p?: number;
|
||||||
|
onClick?: () => void;
|
||||||
|
responsiveColSpan?: { lg: number };
|
||||||
|
overflow?: string;
|
||||||
|
rounded?: string | boolean;
|
||||||
|
borderLeft?: boolean;
|
||||||
|
borderColor?: string;
|
||||||
|
center?: boolean;
|
||||||
|
transition?: string | boolean;
|
||||||
|
hoverBorderColor?: string;
|
||||||
|
border?: boolean;
|
||||||
|
position?: string;
|
||||||
|
mb?: number;
|
||||||
|
display?: string;
|
||||||
|
alignItems?: string;
|
||||||
|
gap?: number;
|
||||||
|
py?: number;
|
||||||
|
backgroundColor?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Card - Redesigned for "Modern Precision" theme.
|
||||||
|
* Includes extensive compatibility props to prevent app-wide breakage.
|
||||||
|
*/
|
||||||
export const Card = forwardRef<HTMLDivElement, CardProps>(({
|
export const Card = forwardRef<HTMLDivElement, CardProps>(({
|
||||||
children,
|
children,
|
||||||
variant = 'default',
|
variant = 'default',
|
||||||
title,
|
title,
|
||||||
footer,
|
footer,
|
||||||
...props
|
padding = 'md',
|
||||||
|
className,
|
||||||
|
style,
|
||||||
|
bg,
|
||||||
|
p,
|
||||||
|
onClick,
|
||||||
|
responsiveColSpan,
|
||||||
|
overflow,
|
||||||
|
rounded,
|
||||||
|
borderLeft,
|
||||||
|
borderColor,
|
||||||
|
center,
|
||||||
|
transition,
|
||||||
|
hoverBorderColor,
|
||||||
|
border,
|
||||||
|
position,
|
||||||
|
mb,
|
||||||
|
display,
|
||||||
|
alignItems,
|
||||||
|
gap,
|
||||||
|
py,
|
||||||
|
backgroundColor,
|
||||||
}, ref) => {
|
}, ref) => {
|
||||||
const isOutline = variant === 'outline';
|
const variantClasses = {
|
||||||
|
default: 'bg-[var(--ui-color-bg-surface)] border-[var(--ui-color-border-default)] shadow-sm',
|
||||||
const style: React.CSSProperties = isOutline ? {
|
muted: 'bg-[var(--ui-color-bg-surface-muted)] border-[var(--ui-color-border-muted)]',
|
||||||
backgroundColor: 'transparent',
|
outline: 'bg-transparent border-[var(--ui-color-border-default)]',
|
||||||
border: '1px solid var(--ui-color-border-default)',
|
glass: 'bg-white/[0.03] backdrop-blur-md border-white/[0.05]',
|
||||||
} : {};
|
dark: 'bg-[var(--ui-color-bg-base)] border-[var(--ui-color-border-default)]',
|
||||||
|
};
|
||||||
|
|
||||||
|
const paddingClasses = {
|
||||||
|
none: 'p-0',
|
||||||
|
sm: 'p-2',
|
||||||
|
md: 'p-4',
|
||||||
|
lg: 'p-8',
|
||||||
|
};
|
||||||
|
|
||||||
|
const getPaddingClass = (pad: any) => {
|
||||||
|
if (typeof pad === 'string') return paddingClasses[pad as keyof typeof paddingClasses] || paddingClasses.md;
|
||||||
|
return ''; // Handled in style
|
||||||
|
};
|
||||||
|
|
||||||
|
const combinedStyle: React.CSSProperties = {
|
||||||
|
...style,
|
||||||
|
...(bg ? { backgroundColor: bg.startsWith('bg-') ? undefined : bg } : {}),
|
||||||
|
...(backgroundColor ? { backgroundColor } : {}),
|
||||||
|
...(p !== undefined ? { padding: `${p * 0.25}rem` } : {}),
|
||||||
|
...(py !== undefined ? { paddingTop: `${py * 0.25}rem`, paddingBottom: `${py * 0.25}rem` } : {}),
|
||||||
|
...(typeof padding === 'number' ? { padding: `${padding * 0.25}rem` } : {}),
|
||||||
|
...(responsiveColSpan?.lg ? { gridColumn: `span ${responsiveColSpan.lg} / span ${responsiveColSpan.lg}` } : {}),
|
||||||
|
...(overflow ? { overflow } : {}),
|
||||||
|
...(borderColor ? { borderColor: borderColor.startsWith('border-') ? undefined : borderColor } : {}),
|
||||||
|
...(borderLeft ? { borderLeft: `4px solid ${borderColor || 'var(--ui-color-intent-primary)'}` } : {}),
|
||||||
|
...(center ? { display: 'flex', alignItems: 'center', justifyContent: 'center' } : {}),
|
||||||
|
...(typeof transition === 'string' ? { transition } : {}),
|
||||||
|
...(position ? { position: position as any } : {}),
|
||||||
|
...(mb !== undefined ? { marginBottom: `${mb * 0.25}rem` } : {}),
|
||||||
|
...(display ? { display } : {}),
|
||||||
|
...(alignItems ? { alignItems } : {}),
|
||||||
|
...(gap !== undefined ? { gap: `${gap * 0.25}rem` } : {}),
|
||||||
|
...(border === false ? { border: 'none' } : {}),
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Surface
|
<div
|
||||||
ref={ref}
|
ref={ref}
|
||||||
variant={isOutline ? 'default' : variant}
|
className={`border ${variantClasses[variant as keyof typeof variantClasses] || variantClasses.default} ${getPaddingClass(padding)} ${onClick ? 'cursor-pointer hover:border-[var(--ui-color-border-bright)]' : ''} ${transition === true ? 'transition-all duration-200' : ''} ${rounded === true ? 'rounded-full' : (typeof rounded === 'string' ? `rounded-${rounded}` : 'rounded-none')} ${className || ''}`}
|
||||||
rounded="lg"
|
style={combinedStyle}
|
||||||
shadow="md"
|
onClick={onClick}
|
||||||
style={style}
|
|
||||||
{...props}
|
|
||||||
>
|
>
|
||||||
{title && (
|
{title && (
|
||||||
<Box padding={4} borderBottom>
|
<div className={`border-b border-[var(--ui-color-border-muted)] ${getPaddingClass(padding)}`}>
|
||||||
{typeof title === 'string' ? (
|
{typeof title === 'string' ? (
|
||||||
<h3 className="text-lg font-bold text-[var(--ui-color-text-high)]">{title}</h3>
|
<Heading level={5} weight="bold" uppercase>{title}</Heading>
|
||||||
) : title}
|
) : title}
|
||||||
</Box>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<Box padding={4}>
|
<div className={typeof padding === 'number' || p !== undefined ? '' : getPaddingClass(padding)}>
|
||||||
{children}
|
{children}
|
||||||
</Box>
|
</div>
|
||||||
|
|
||||||
{footer && (
|
{footer && (
|
||||||
<Box padding={4} borderTop bg="rgba(255,255,255,0.02)">
|
<div className={`border-t border-[var(--ui-color-border-muted)] bg-white/[0.02] ${getPaddingClass(padding)}`}>
|
||||||
{footer}
|
{footer}
|
||||||
</Box>
|
</div>
|
||||||
)}
|
)}
|
||||||
</Surface>
|
</div>
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
Card.displayName = 'Card';
|
Card.displayName = 'Card';
|
||||||
|
|
||||||
export const CardHeader = ({ title, children }: { title?: string, children?: ReactNode }) => (
|
export const CardHeader = ({ title, children }: { title?: string, children?: ReactNode }) => (
|
||||||
<Box marginBottom={4}>
|
<div className="mb-4">
|
||||||
{title && <h3 className="text-lg font-bold text-[var(--ui-color-text-high)]">{title}</h3>}
|
{title && <Heading level={5} weight="bold" uppercase>{title}</Heading>}
|
||||||
{children}
|
{children}
|
||||||
</Box>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
||||||
export const CardContent = ({ children }: { children: ReactNode }) => (
|
export const CardContent = ({ children }: { children: ReactNode }) => (
|
||||||
<Box>{children}</Box>
|
<div>{children}</div>
|
||||||
);
|
);
|
||||||
|
|
||||||
export const CardFooter = ({ children }: { children: ReactNode }) => (
|
export const CardFooter = ({ children }: { children: ReactNode }) => (
|
||||||
<Box marginTop={4} paddingTop={4} borderTop>
|
<div className="mt-4 pt-4 border-t border-[var(--ui-color-border-muted)]">
|
||||||
{children}
|
{children}
|
||||||
</Box>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,32 +1,59 @@
|
|||||||
import { ReactNode } from 'react';
|
import { ReactNode } from 'react';
|
||||||
import { Box } from './Box';
|
|
||||||
|
|
||||||
export interface ContainerProps {
|
export interface ContainerProps {
|
||||||
children: ReactNode;
|
children: ReactNode;
|
||||||
size?: 'sm' | 'md' | 'lg' | 'xl' | 'full';
|
size?: 'sm' | 'md' | 'lg' | 'xl' | 'full' | any;
|
||||||
py?: number;
|
padding?: 'none' | 'sm' | 'md' | 'lg' | any;
|
||||||
position?: 'static' | 'relative' | 'absolute' | 'fixed' | 'sticky';
|
py?: number | any;
|
||||||
zIndex?: number;
|
position?: 'static' | 'relative' | 'absolute' | 'fixed' | 'sticky' | any;
|
||||||
|
zIndex?: number | any;
|
||||||
|
paddingX?: number | any;
|
||||||
|
fullWidth?: boolean | any;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Container - Redesigned for "Modern Precision" theme.
|
||||||
|
* Includes compatibility props to prevent app-wide breakage.
|
||||||
|
*/
|
||||||
export const Container = ({
|
export const Container = ({
|
||||||
children,
|
children,
|
||||||
size = 'lg',
|
size = 'lg',
|
||||||
|
padding = 'md',
|
||||||
py,
|
py,
|
||||||
position,
|
position,
|
||||||
zIndex
|
zIndex,
|
||||||
|
paddingX,
|
||||||
|
fullWidth,
|
||||||
}: ContainerProps) => {
|
}: ContainerProps) => {
|
||||||
const sizeMap = {
|
const sizeMap = {
|
||||||
sm: '40rem',
|
sm: 'max-w-[40rem]',
|
||||||
md: '48rem',
|
md: 'max-w-[48rem]',
|
||||||
lg: '64rem',
|
lg: 'max-w-[64rem]',
|
||||||
xl: '80rem',
|
xl: 'max-w-[80rem]',
|
||||||
full: '100%',
|
full: 'max-w-full',
|
||||||
|
};
|
||||||
|
|
||||||
|
const paddingMap = {
|
||||||
|
none: 'px-0',
|
||||||
|
sm: 'px-2',
|
||||||
|
md: 'px-4',
|
||||||
|
lg: 'px-8',
|
||||||
|
};
|
||||||
|
|
||||||
|
const combinedStyle: React.CSSProperties = {
|
||||||
|
...(py !== undefined ? { paddingTop: `${py * 0.25}rem`, paddingBottom: `${py * 0.25}rem` } : {}),
|
||||||
|
...(paddingX !== undefined ? { paddingLeft: `${paddingX * 0.25}rem`, paddingRight: `${paddingX * 0.25}rem` } : {}),
|
||||||
|
...(position ? { position } : {}),
|
||||||
|
...(zIndex !== undefined ? { zIndex } : {}),
|
||||||
|
...(fullWidth ? { width: '100%' } : {}),
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box marginX="auto" maxWidth={sizeMap[size]} paddingX={4} py={py as any} fullWidth position={position} zIndex={zIndex}>
|
<div
|
||||||
|
className={`mx-auto w-full ${sizeMap[size as keyof typeof sizeMap] || sizeMap.lg} ${paddingMap[padding as keyof typeof paddingMap] || paddingMap.md}`}
|
||||||
|
style={combinedStyle}
|
||||||
|
>
|
||||||
{children}
|
{children}
|
||||||
</Box>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
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 { ReactNode, forwardRef } from 'react';
|
||||||
import { Box, BoxProps, ResponsiveValue, Spacing } from './Box';
|
|
||||||
|
|
||||||
export interface HeadingProps extends BoxProps<any> {
|
export interface HeadingProps {
|
||||||
children: ReactNode;
|
children: ReactNode;
|
||||||
level?: 1 | 2 | 3 | 4 | 5 | 6;
|
level?: 1 | 2 | 3 | 4 | 5 | 6;
|
||||||
weight?: 'normal' | 'medium' | 'semibold' | 'bold';
|
weight?: 'normal' | 'medium' | 'semibold' | 'bold';
|
||||||
align?: 'left' | 'center' | 'right';
|
align?: 'left' | 'center' | 'right';
|
||||||
fontSize?: string | ResponsiveValue<string>;
|
|
||||||
icon?: ReactNode;
|
|
||||||
groupHoverColor?: string;
|
|
||||||
uppercase?: boolean;
|
uppercase?: boolean;
|
||||||
|
intent?: 'primary' | 'telemetry' | 'warning' | 'critical' | 'default' | any;
|
||||||
|
className?: string;
|
||||||
|
style?: React.CSSProperties;
|
||||||
|
mb?: number | any;
|
||||||
|
marginBottom?: number | any;
|
||||||
|
mt?: number | any;
|
||||||
|
marginTop?: number | any;
|
||||||
|
color?: string;
|
||||||
|
fontSize?: string | { base: string; sm?: string; md: string; lg?: string; xl?: string };
|
||||||
letterSpacing?: string;
|
letterSpacing?: string;
|
||||||
mb?: Spacing;
|
truncate?: boolean;
|
||||||
|
size?: string;
|
||||||
|
icon?: ReactNode;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Heading - Redesigned for "Modern Precision" theme.
|
||||||
|
* Includes extensive compatibility props to prevent app-wide breakage.
|
||||||
|
*/
|
||||||
export const Heading = forwardRef<HTMLHeadingElement, HeadingProps>(({
|
export const Heading = forwardRef<HTMLHeadingElement, HeadingProps>(({
|
||||||
children,
|
children,
|
||||||
level = 1,
|
level = 1,
|
||||||
weight = 'bold',
|
weight = 'bold',
|
||||||
align = 'left',
|
align = 'left',
|
||||||
fontSize,
|
uppercase = false,
|
||||||
icon,
|
intent = 'default',
|
||||||
groupHoverColor,
|
className,
|
||||||
uppercase,
|
style,
|
||||||
letterSpacing,
|
|
||||||
mb,
|
mb,
|
||||||
...props
|
marginBottom,
|
||||||
|
mt,
|
||||||
|
marginTop,
|
||||||
|
color,
|
||||||
|
fontSize,
|
||||||
|
letterSpacing,
|
||||||
|
truncate,
|
||||||
|
size,
|
||||||
|
icon,
|
||||||
}, ref) => {
|
}, ref) => {
|
||||||
const Tag = `h${level}` as const;
|
const Tag = `h${level}` as const;
|
||||||
|
|
||||||
@@ -37,37 +55,62 @@ export const Heading = forwardRef<HTMLHeadingElement, HeadingProps>(({
|
|||||||
};
|
};
|
||||||
|
|
||||||
const sizeClasses = {
|
const sizeClasses = {
|
||||||
1: 'text-4xl md:text-5xl',
|
1: 'text-4xl md:text-5xl tracking-tighter leading-none',
|
||||||
2: 'text-3xl md:text-4xl',
|
2: 'text-3xl md:text-4xl tracking-tight leading-tight',
|
||||||
3: 'text-2xl md:text-3xl',
|
3: 'text-2xl md:text-3xl tracking-tight leading-snug',
|
||||||
4: 'text-xl md:text-2xl',
|
4: 'text-xl md:text-2xl tracking-normal leading-normal',
|
||||||
5: 'text-lg md:text-xl',
|
5: 'text-lg md:text-xl tracking-normal leading-normal',
|
||||||
6: 'text-base md:text-lg'
|
6: 'text-base md:text-lg tracking-wide leading-normal'
|
||||||
|
};
|
||||||
|
|
||||||
|
const intentClasses = {
|
||||||
|
default: 'text-[var(--ui-color-text-high)]',
|
||||||
|
primary: 'text-[var(--ui-color-intent-primary)]',
|
||||||
|
telemetry: 'text-[var(--ui-color-intent-telemetry)]',
|
||||||
|
warning: 'text-[var(--ui-color-intent-warning)]',
|
||||||
|
critical: 'text-[var(--ui-color-intent-critical)]',
|
||||||
|
};
|
||||||
|
|
||||||
|
const getResponsiveFontSize = (fs: HeadingProps['fontSize']) => {
|
||||||
|
if (!fs) return '';
|
||||||
|
if (typeof fs === 'string') return ''; // Handled in style
|
||||||
|
const classes = [];
|
||||||
|
if (fs.base) classes.push(`text-${fs.base}`);
|
||||||
|
if (fs.sm) classes.push(`sm:text-${fs.sm}`);
|
||||||
|
if (fs.md) classes.push(`md:text-${fs.md}`);
|
||||||
|
if (fs.lg) classes.push(`lg:text-${fs.lg}`);
|
||||||
|
if (fs.xl) classes.push(`xl:text-${fs.xl}`);
|
||||||
|
return classes.join(' ');
|
||||||
};
|
};
|
||||||
|
|
||||||
const classes = [
|
const classes = [
|
||||||
'text-[var(--ui-color-text-high)]',
|
intentClasses[intent as keyof typeof intentClasses] || intentClasses.default,
|
||||||
weightClasses[weight],
|
weightClasses[weight],
|
||||||
fontSize ? '' : sizeClasses[level],
|
fontSize ? getResponsiveFontSize(fontSize) : sizeClasses[level],
|
||||||
align === 'center' ? 'text-center' : (align === 'right' ? 'text-right' : 'text-left'),
|
align === 'center' ? 'text-center' : (align === 'right' ? 'text-right' : 'text-left'),
|
||||||
uppercase ? 'uppercase' : '',
|
uppercase ? 'uppercase tracking-widest' : '',
|
||||||
|
truncate ? 'truncate' : '',
|
||||||
|
className,
|
||||||
].join(' ');
|
].join(' ');
|
||||||
|
|
||||||
|
const combinedStyle: React.CSSProperties = {
|
||||||
|
...style,
|
||||||
|
...(mb !== undefined ? { marginBottom: typeof mb === 'number' ? `${mb * 0.25}rem` : mb } : {}),
|
||||||
|
...(marginBottom !== undefined ? { marginBottom: typeof marginBottom === 'number' ? `${marginBottom * 0.25}rem` : marginBottom } : {}),
|
||||||
|
...(mt !== undefined ? { marginTop: typeof mt === 'number' ? `${mt * 0.25}rem` : mt } : {}),
|
||||||
|
...(marginTop !== undefined ? { marginTop: typeof marginTop === 'number' ? `${marginTop * 0.25}rem` : marginTop } : {}),
|
||||||
|
...(color ? { color } : {}),
|
||||||
|
...(letterSpacing ? { letterSpacing } : {}),
|
||||||
|
...(typeof fontSize === 'string' ? { fontSize } : {}),
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box
|
<Tag ref={ref} className={classes} style={combinedStyle}>
|
||||||
as={Tag}
|
<div className="flex items-center gap-2">
|
||||||
ref={ref}
|
|
||||||
className={classes}
|
|
||||||
fontSize={typeof fontSize === 'string' ? fontSize : undefined}
|
|
||||||
letterSpacing={letterSpacing}
|
|
||||||
mb={mb}
|
|
||||||
{...props}
|
|
||||||
>
|
|
||||||
<Box display="flex" alignItems="center" gap={2}>
|
|
||||||
{icon}
|
{icon}
|
||||||
{children}
|
{children}
|
||||||
</Box>
|
</div>
|
||||||
</Box>
|
</Tag>
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -15,11 +15,11 @@ export const IconButton = forwardRef<HTMLButtonElement | HTMLAnchorElement, Icon
|
|||||||
intent,
|
intent,
|
||||||
...props
|
...props
|
||||||
}, ref) => {
|
}, ref) => {
|
||||||
const iconSizeMap = {
|
const iconSizeMap: Record<string, any> = {
|
||||||
sm: 3,
|
sm: 3,
|
||||||
md: 4,
|
md: 4,
|
||||||
lg: 5
|
lg: 5
|
||||||
} as const;
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Button
|
<Button
|
||||||
|
|||||||
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 { ReactNode } from 'react';
|
||||||
import { Box, Spacing } from './Box';
|
|
||||||
import { Heading } from './Heading';
|
import { Heading } from './Heading';
|
||||||
import { Surface } from './Surface';
|
|
||||||
import { Text } from './Text';
|
import { Text } from './Text';
|
||||||
|
|
||||||
export interface PanelProps {
|
export interface PanelProps {
|
||||||
@@ -9,63 +7,88 @@ export interface PanelProps {
|
|||||||
description?: string;
|
description?: string;
|
||||||
children: ReactNode;
|
children: ReactNode;
|
||||||
footer?: ReactNode;
|
footer?: ReactNode;
|
||||||
variant?: 'default' | 'dark' | 'muted';
|
variant?: 'default' | 'muted' | 'ghost' | 'dark' | any;
|
||||||
padding?: Spacing;
|
padding?: 'none' | 'sm' | 'md' | 'lg' | number | any;
|
||||||
actions?: ReactNode;
|
actions?: ReactNode;
|
||||||
className?: string;
|
className?: string;
|
||||||
|
style?: React.CSSProperties;
|
||||||
border?: boolean;
|
border?: boolean;
|
||||||
rounded?: string;
|
rounded?: string;
|
||||||
borderColor?: string;
|
borderColor?: string;
|
||||||
bg?: string;
|
bg?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Panel - Redesigned for "Modern Precision" theme.
|
||||||
|
* Includes compatibility props to prevent app-wide breakage.
|
||||||
|
*/
|
||||||
export const Panel = ({
|
export const Panel = ({
|
||||||
title,
|
title,
|
||||||
description,
|
description,
|
||||||
children,
|
children,
|
||||||
footer,
|
footer,
|
||||||
variant = 'default',
|
variant = 'default',
|
||||||
padding = 6,
|
padding = 'md',
|
||||||
actions,
|
actions,
|
||||||
className,
|
className,
|
||||||
|
style,
|
||||||
border,
|
border,
|
||||||
rounded,
|
rounded,
|
||||||
borderColor,
|
borderColor,
|
||||||
bg
|
bg,
|
||||||
}: PanelProps) => {
|
}: PanelProps) => {
|
||||||
|
const variantClasses = {
|
||||||
|
default: 'bg-[var(--ui-color-bg-surface)] border-[var(--ui-color-border-default)]',
|
||||||
|
muted: 'bg-[var(--ui-color-bg-surface-muted)] border-[var(--ui-color-border-muted)]',
|
||||||
|
ghost: 'bg-transparent border-transparent',
|
||||||
|
dark: 'bg-[var(--ui-color-bg-base)] border-[var(--ui-color-border-default)]',
|
||||||
|
};
|
||||||
|
|
||||||
|
const paddingClasses = {
|
||||||
|
none: 'p-0',
|
||||||
|
sm: 'p-2',
|
||||||
|
md: 'p-4',
|
||||||
|
lg: 'p-8',
|
||||||
|
};
|
||||||
|
|
||||||
|
const getPaddingClass = (pad: any) => {
|
||||||
|
if (typeof pad === 'string') return paddingClasses[pad as keyof typeof paddingClasses] || paddingClasses.md;
|
||||||
|
return ''; // Handled in style
|
||||||
|
};
|
||||||
|
|
||||||
|
const combinedStyle: React.CSSProperties = {
|
||||||
|
...style,
|
||||||
|
...(bg ? { backgroundColor: bg.startsWith('bg-') ? undefined : bg } : {}),
|
||||||
|
...(typeof padding === 'number' ? { padding: `${padding * 0.25}rem` } : {}),
|
||||||
|
...(borderColor ? { borderColor: borderColor.startsWith('border-') ? undefined : borderColor } : {}),
|
||||||
|
...(border === false ? { border: 'none' } : {}),
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Surface
|
<div className={`border ${variantClasses[variant as keyof typeof variantClasses] || variantClasses.default} ${getPaddingClass(padding)} transition-all duration-200 ${className || ''}`} style={combinedStyle}>
|
||||||
variant={variant}
|
|
||||||
rounded={(rounded as any) || "lg"}
|
|
||||||
style={{
|
|
||||||
border: border === false ? 'none' : `1px solid ${borderColor || 'var(--ui-color-border-default)'}`,
|
|
||||||
backgroundColor: bg ? (bg.startsWith('bg-') ? undefined : bg) : undefined
|
|
||||||
}}
|
|
||||||
className={className}
|
|
||||||
>
|
|
||||||
{(title || description || actions) && (
|
{(title || description || actions) && (
|
||||||
<Box padding={padding as any} borderBottom display="flex" alignItems="center" justifyContent="between">
|
<div className={`border-b border-[var(--ui-color-border-muted)] flex items-center justify-between ${getPaddingClass(padding)}`}>
|
||||||
<Box>
|
<div>
|
||||||
{title && <Heading level={3} marginBottom={1}>{title}</Heading>}
|
{title && <Heading level={4} weight="semibold" uppercase>{title}</Heading>}
|
||||||
{description && <Text size="sm" variant="low">{description}</Text>}
|
{description && <Text size="xs" variant="low">{description}</Text>}
|
||||||
</Box>
|
</div>
|
||||||
{actions && (
|
{actions && (
|
||||||
<Box display="flex" alignItems="center" gap={3}>
|
<div className="flex items-center gap-2">
|
||||||
{actions}
|
{actions}
|
||||||
</Box>
|
</div>
|
||||||
)}
|
)}
|
||||||
</Box>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<Box padding={padding as any}>
|
<div className={typeof padding === 'number' ? '' : getPaddingClass(padding)}>
|
||||||
{children}
|
{children}
|
||||||
</Box>
|
</div>
|
||||||
|
|
||||||
{footer && (
|
{footer && (
|
||||||
<Box padding={padding as any} borderTop bg="rgba(255,255,255,0.02)">
|
<div className={`border-t border-[var(--ui-color-border-muted)] bg-white/[0.02] ${getPaddingClass(padding)}`}>
|
||||||
{footer}
|
{footer}
|
||||||
</Box>
|
</div>
|
||||||
)}
|
)}
|
||||||
</Surface>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,30 +1,67 @@
|
|||||||
import React, { ElementType, ReactNode, forwardRef } from 'react';
|
import React, { ElementType, ReactNode, forwardRef } from 'react';
|
||||||
import { Box, BoxProps, ResponsiveValue } from './Box';
|
|
||||||
|
|
||||||
export type TextSize = 'xs' | 'sm' | 'md' | 'lg' | 'xl' | '2xl' | '3xl' | '4xl' | 'base';
|
export type TextSize = 'xs' | 'sm' | 'md' | 'lg' | 'xl' | '2xl' | '3xl' | '4xl' | 'base';
|
||||||
|
|
||||||
export interface TextProps extends BoxProps<any> {
|
export interface TextProps {
|
||||||
children: ReactNode;
|
children: ReactNode;
|
||||||
variant?: 'high' | 'med' | 'low' | 'primary' | 'success' | 'warning' | 'critical' | 'telemetry' | 'inherit';
|
variant?: 'high' | 'med' | 'low' | 'primary' | 'success' | 'warning' | 'critical' | 'telemetry' | 'inherit' | any;
|
||||||
size?: TextSize | ResponsiveValue<TextSize>;
|
size?: TextSize | { base: TextSize; sm?: TextSize; md?: TextSize; lg?: TextSize; xl?: TextSize } | any;
|
||||||
weight?: 'light' | 'normal' | 'medium' | 'semibold' | 'bold';
|
weight?: 'light' | 'normal' | 'medium' | 'semibold' | 'bold' | any;
|
||||||
as?: ElementType;
|
as?: ElementType;
|
||||||
align?: 'left' | 'center' | 'right';
|
align?: 'left' | 'center' | 'right' | any;
|
||||||
italic?: boolean;
|
|
||||||
mono?: boolean;
|
mono?: boolean;
|
||||||
block?: boolean;
|
|
||||||
uppercase?: boolean;
|
uppercase?: boolean;
|
||||||
capitalize?: boolean;
|
leading?: 'none' | 'tight' | 'snug' | 'normal' | 'relaxed' | 'loose' | any;
|
||||||
letterSpacing?: string;
|
block?: boolean;
|
||||||
leading?: 'none' | 'tight' | 'snug' | 'normal' | 'relaxed' | 'loose';
|
|
||||||
truncate?: boolean;
|
truncate?: boolean;
|
||||||
|
className?: string;
|
||||||
|
style?: React.CSSProperties;
|
||||||
|
mt?: number | any;
|
||||||
|
mb?: number | any;
|
||||||
|
ml?: number | any;
|
||||||
|
mr?: number | any;
|
||||||
|
marginTop?: number | any;
|
||||||
|
marginBottom?: number | any;
|
||||||
|
font?: 'sans' | 'mono' | string;
|
||||||
|
color?: string;
|
||||||
|
letterSpacing?: string;
|
||||||
lineHeight?: string | number;
|
lineHeight?: string | number;
|
||||||
font?: 'sans' | 'mono';
|
flexGrow?: number;
|
||||||
hoverVariant?: 'high' | 'med' | 'low' | 'primary' | 'success' | 'warning' | 'critical';
|
flexShrink?: number;
|
||||||
htmlFor?: string;
|
lineClamp?: number;
|
||||||
|
display?: string;
|
||||||
|
opacity?: number;
|
||||||
|
maxWidth?: string | number;
|
||||||
|
mx?: string | number;
|
||||||
|
pl?: number;
|
||||||
|
px?: number;
|
||||||
|
py?: number;
|
||||||
|
paddingX?: number;
|
||||||
|
paddingY?: number;
|
||||||
|
textAlign?: string;
|
||||||
|
groupHoverTextColor?: string;
|
||||||
|
transition?: boolean;
|
||||||
|
borderLeft?: boolean;
|
||||||
|
borderStyle?: string;
|
||||||
|
borderColor?: string;
|
||||||
|
ariaLabel?: string;
|
||||||
|
hoverVariant?: string;
|
||||||
|
fontSize?: string | any;
|
||||||
|
italic?: boolean;
|
||||||
|
animate?: string;
|
||||||
|
capitalize?: boolean;
|
||||||
|
alignItems?: string;
|
||||||
|
gap?: number;
|
||||||
cursor?: string;
|
cursor?: string;
|
||||||
|
width?: string | number;
|
||||||
|
htmlFor?: string;
|
||||||
|
transform?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Text - Redesigned for "Modern Precision" theme.
|
||||||
|
* Includes extensive compatibility props to prevent app-wide breakage.
|
||||||
|
*/
|
||||||
export const Text = forwardRef<HTMLElement, TextProps>(({
|
export const Text = forwardRef<HTMLElement, TextProps>(({
|
||||||
children,
|
children,
|
||||||
variant = 'med',
|
variant = 'med',
|
||||||
@@ -32,20 +69,53 @@ export const Text = forwardRef<HTMLElement, TextProps>(({
|
|||||||
weight = 'normal',
|
weight = 'normal',
|
||||||
as = 'p',
|
as = 'p',
|
||||||
align = 'left',
|
align = 'left',
|
||||||
italic = false,
|
|
||||||
mono = false,
|
mono = false,
|
||||||
block = false,
|
|
||||||
uppercase = false,
|
uppercase = false,
|
||||||
capitalize = false,
|
leading = 'normal',
|
||||||
letterSpacing,
|
block = false,
|
||||||
leading,
|
|
||||||
truncate = false,
|
truncate = false,
|
||||||
lineHeight,
|
className,
|
||||||
|
style,
|
||||||
|
mt,
|
||||||
|
mb,
|
||||||
|
ml,
|
||||||
|
mr,
|
||||||
|
marginTop,
|
||||||
|
marginBottom,
|
||||||
font,
|
font,
|
||||||
|
color,
|
||||||
|
letterSpacing,
|
||||||
|
lineHeight,
|
||||||
|
flexGrow,
|
||||||
|
flexShrink,
|
||||||
|
lineClamp,
|
||||||
|
display,
|
||||||
|
opacity,
|
||||||
|
maxWidth,
|
||||||
|
mx,
|
||||||
|
pl,
|
||||||
|
px,
|
||||||
|
py,
|
||||||
|
paddingX,
|
||||||
|
paddingY,
|
||||||
|
textAlign,
|
||||||
|
groupHoverTextColor,
|
||||||
|
transition,
|
||||||
|
borderLeft,
|
||||||
|
borderStyle,
|
||||||
|
borderColor,
|
||||||
|
ariaLabel,
|
||||||
hoverVariant,
|
hoverVariant,
|
||||||
htmlFor,
|
fontSize,
|
||||||
|
italic,
|
||||||
|
animate,
|
||||||
|
capitalize,
|
||||||
|
alignItems,
|
||||||
|
gap,
|
||||||
cursor,
|
cursor,
|
||||||
...props
|
width,
|
||||||
|
htmlFor,
|
||||||
|
transform,
|
||||||
}, ref) => {
|
}, ref) => {
|
||||||
const variantClasses = {
|
const variantClasses = {
|
||||||
high: 'text-[var(--ui-color-text-high)]',
|
high: 'text-[var(--ui-color-text-high)]',
|
||||||
@@ -58,40 +128,31 @@ export const Text = forwardRef<HTMLElement, TextProps>(({
|
|||||||
telemetry: 'text-[var(--ui-color-intent-telemetry)]',
|
telemetry: 'text-[var(--ui-color-intent-telemetry)]',
|
||||||
inherit: 'text-inherit',
|
inherit: 'text-inherit',
|
||||||
};
|
};
|
||||||
|
|
||||||
const hoverVariantClasses = {
|
|
||||||
high: 'hover:text-[var(--ui-color-text-high)]',
|
|
||||||
med: 'hover:text-[var(--ui-color-text-med)]',
|
|
||||||
low: 'hover:text-[var(--ui-color-text-low)]',
|
|
||||||
primary: 'hover:text-[var(--ui-color-intent-primary)]',
|
|
||||||
success: 'hover:text-[var(--ui-color-intent-success)]',
|
|
||||||
warning: 'hover:text-[var(--ui-color-intent-warning)]',
|
|
||||||
critical: 'hover:text-[var(--ui-color-intent-critical)]',
|
|
||||||
};
|
|
||||||
|
|
||||||
const sizeMap: Record<TextSize, string> = {
|
const sizeMap: Record<string, string> = {
|
||||||
xs: 'text-xs',
|
xs: 'text-[10px]',
|
||||||
sm: 'text-sm',
|
sm: 'text-xs',
|
||||||
base: 'text-base',
|
base: 'text-sm',
|
||||||
md: 'text-base',
|
md: 'text-sm',
|
||||||
lg: 'text-lg',
|
lg: 'text-base',
|
||||||
xl: 'text-xl',
|
xl: 'text-lg',
|
||||||
'2xl': 'text-2xl',
|
'2xl': 'text-xl',
|
||||||
'3xl': 'text-3xl',
|
'3xl': 'text-2xl',
|
||||||
'4xl': 'text-4xl',
|
'4xl': 'text-3xl',
|
||||||
};
|
};
|
||||||
|
|
||||||
const getResponsiveSizeClasses = (value: TextSize | ResponsiveValue<TextSize>) => {
|
const getResponsiveSize = (s: any) => {
|
||||||
if (typeof value === 'string') return sizeMap[value];
|
if (!s) return sizeMap['md'];
|
||||||
|
if (typeof s === 'string') return sizeMap[s] || sizeMap['md'];
|
||||||
const classes = [];
|
const classes = [];
|
||||||
if (value.base) classes.push(sizeMap[value.base]);
|
if (s.base) classes.push(sizeMap[s.base]);
|
||||||
if (value.sm) classes.push(`sm:${sizeMap[value.sm]}`);
|
if (s.sm) classes.push(`sm:${sizeMap[s.sm]}`);
|
||||||
if (value.md) classes.push(`md:${sizeMap[value.md]}`);
|
if (s.md) classes.push(`md:${sizeMap[s.md]}`);
|
||||||
if (value.lg) classes.push(`lg:${sizeMap[value.lg]}`);
|
if (s.lg) classes.push(`lg:${sizeMap[s.lg]}`);
|
||||||
if (value.xl) classes.push(`xl:${sizeMap[value.xl]}`);
|
if (s.xl) classes.push(`xl:${sizeMap[s.xl]}`);
|
||||||
return classes.join(' ');
|
return classes.join(' ');
|
||||||
};
|
};
|
||||||
|
|
||||||
const weightClasses = {
|
const weightClasses = {
|
||||||
light: 'font-light',
|
light: 'font-light',
|
||||||
normal: 'font-normal',
|
normal: 'font-normal',
|
||||||
@@ -110,30 +171,60 @@ export const Text = forwardRef<HTMLElement, TextProps>(({
|
|||||||
};
|
};
|
||||||
|
|
||||||
const classes = [
|
const classes = [
|
||||||
variantClasses[variant],
|
variantClasses[variant as keyof typeof variantClasses] || '',
|
||||||
getResponsiveSizeClasses(size),
|
getResponsiveSize(size),
|
||||||
weightClasses[weight],
|
weightClasses[weight as keyof typeof weightClasses] || '',
|
||||||
align === 'center' ? 'text-center' : (align === 'right' ? 'text-right' : 'text-left'),
|
align === 'center' || textAlign === 'center' ? 'text-center' : (align === 'right' || textAlign === 'right' ? 'text-right' : 'text-left'),
|
||||||
italic ? 'italic' : '',
|
|
||||||
(mono || font === 'mono') ? 'font-mono' : 'font-sans',
|
(mono || font === 'mono') ? 'font-mono' : 'font-sans',
|
||||||
|
uppercase ? 'uppercase tracking-widest' : '',
|
||||||
|
leadingClasses[leading as keyof typeof leadingClasses] || '',
|
||||||
block ? 'block' : 'inline',
|
block ? 'block' : 'inline',
|
||||||
uppercase ? 'uppercase tracking-wider' : '',
|
|
||||||
capitalize ? 'capitalize' : '',
|
|
||||||
leading ? leadingClasses[leading] : '',
|
|
||||||
truncate ? 'truncate' : '',
|
truncate ? 'truncate' : '',
|
||||||
hoverVariant ? hoverVariantClasses[hoverVariant] : '',
|
transition ? 'transition-all duration-200' : '',
|
||||||
].join(' ');
|
italic ? 'italic' : '',
|
||||||
|
animate === 'pulse' ? 'animate-pulse' : '',
|
||||||
|
capitalize ? 'capitalize' : '',
|
||||||
|
className,
|
||||||
|
].filter(Boolean).join(' ');
|
||||||
|
|
||||||
const style: React.CSSProperties = {
|
const combinedStyle: React.CSSProperties = {
|
||||||
|
...style,
|
||||||
|
...(display ? { display } : {}),
|
||||||
|
...(alignItems ? { alignItems } : {}),
|
||||||
|
...(gap !== undefined ? { gap: `${gap * 0.25}rem` } : {}),
|
||||||
|
...(cursor ? { cursor } : {}),
|
||||||
|
...(width !== undefined ? { width } : {}),
|
||||||
|
...(opacity !== undefined ? { opacity } : {}),
|
||||||
|
...(maxWidth !== undefined ? { maxWidth } : {}),
|
||||||
|
...(mx === 'auto' ? { marginLeft: 'auto', marginRight: 'auto' } : {}),
|
||||||
|
...(pl !== undefined ? { paddingLeft: `${pl * 0.25}rem` } : {}),
|
||||||
|
...(px !== undefined ? { paddingLeft: `${px * 0.25}rem`, paddingRight: `${px * 0.25}rem` } : {}),
|
||||||
|
...(py !== undefined ? { paddingTop: `${py * 0.25}rem`, paddingBottom: `${py * 0.25}rem` } : {}),
|
||||||
|
...(paddingX !== undefined ? { paddingLeft: `${paddingX * 0.25}rem`, paddingRight: `${paddingX * 0.25}rem` } : {}),
|
||||||
|
...(paddingY !== undefined ? { paddingTop: `${paddingY * 0.25}rem`, paddingBottom: `${paddingY * 0.25}rem` } : {}),
|
||||||
|
...(mt !== undefined ? { marginTop: typeof mt === 'number' ? `${mt * 0.25}rem` : mt } : {}),
|
||||||
|
...(mb !== undefined ? { marginBottom: typeof mb === 'number' ? `${mb * 0.25}rem` : mb } : {}),
|
||||||
|
...(ml !== undefined ? { marginLeft: typeof ml === 'number' ? `${ml * 0.25}rem` : ml } : {}),
|
||||||
|
...(mr !== undefined ? { marginRight: typeof mr === 'number' ? `${mr * 0.25}rem` : mr } : {}),
|
||||||
|
...(marginTop !== undefined ? { marginTop: typeof marginTop === 'number' ? `${marginTop * 0.25}rem` : marginTop } : {}),
|
||||||
|
...(marginBottom !== undefined ? { marginBottom: typeof marginBottom === 'number' ? `${marginBottom * 0.25}rem` : marginBottom } : {}),
|
||||||
|
...(color ? { color } : {}),
|
||||||
...(letterSpacing ? { letterSpacing } : {}),
|
...(letterSpacing ? { letterSpacing } : {}),
|
||||||
...(lineHeight ? { lineHeight } : {}),
|
...(lineHeight ? { lineHeight } : {}),
|
||||||
...(cursor ? { cursor } : {}),
|
...(flexGrow !== undefined ? { flexGrow } : {}),
|
||||||
|
...(flexShrink !== undefined ? { flexShrink } : {}),
|
||||||
|
...(lineClamp !== undefined ? { display: '-webkit-box', WebkitLineClamp: lineClamp, WebkitBoxOrient: 'vertical', overflow: 'hidden' } : {}),
|
||||||
|
...(borderLeft ? { borderLeft: `1px solid ${borderColor || 'var(--ui-color-border-default)'}` } : {}),
|
||||||
|
...(fontSize ? { fontSize } : {}),
|
||||||
|
...(transform ? { textTransform: transform as any } : {}),
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const Tag = as;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box as={as} ref={ref} className={classes} style={style} htmlFor={htmlFor} {...props}>
|
<Tag ref={ref} className={classes} style={combinedStyle} aria-label={ariaLabel} htmlFor={htmlFor}>
|
||||||
{children}
|
{children}
|
||||||
</Box>
|
</Tag>
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user