From 30a31dc44ffcc2df304fef2c3964edd882e65d11 Mon Sep 17 00:00:00 2001 From: Marc Mintel Date: Tue, 20 Jan 2026 01:22:05 +0100 Subject: [PATCH] website refactor --- apps/website/app/leagues/migration/page.tsx | 20 ++ apps/website/components/home/CtaSection.tsx | 50 ++++ .../website/components/home/EcosystemGrid.tsx | 96 ++++++++ apps/website/components/home/Hero.tsx | 65 ++++++ .../components/home/LeagueIdentityPreview.tsx | 151 ++++++++++++ .../components/home/MigrationSection.tsx | 47 ++++ .../components/home/StewardingPreview.tsx | 101 ++++++++ .../components/home/TelemetryStrip.tsx | 45 ++++ apps/website/components/home/ValuePillars.tsx | 50 ++++ apps/website/lib/routing/RouteConfig.ts | 2 + apps/website/templates/HomeTemplate.tsx | 148 +++--------- apps/website/ui/Button.tsx | 173 ++++++-------- apps/website/ui/Card.tsx | 142 +++++++++--- apps/website/ui/Container.tsx | 53 +++-- apps/website/ui/FeatureGrid.tsx | 20 ++ apps/website/ui/FeatureItem.tsx | 37 +++ apps/website/ui/Heading.tsx | 109 ++++++--- apps/website/ui/IconButton.tsx | 4 +- apps/website/ui/IconContainer.tsx | 24 ++ apps/website/ui/Panel.tsx | 79 ++++--- apps/website/ui/Text.tsx | 219 +++++++++++++----- 21 files changed, 1242 insertions(+), 393 deletions(-) create mode 100644 apps/website/app/leagues/migration/page.tsx create mode 100644 apps/website/components/home/CtaSection.tsx create mode 100644 apps/website/components/home/EcosystemGrid.tsx create mode 100644 apps/website/components/home/Hero.tsx create mode 100644 apps/website/components/home/LeagueIdentityPreview.tsx create mode 100644 apps/website/components/home/MigrationSection.tsx create mode 100644 apps/website/components/home/StewardingPreview.tsx create mode 100644 apps/website/components/home/TelemetryStrip.tsx create mode 100644 apps/website/components/home/ValuePillars.tsx create mode 100644 apps/website/ui/FeatureGrid.tsx create mode 100644 apps/website/ui/FeatureItem.tsx create mode 100644 apps/website/ui/IconContainer.tsx diff --git a/apps/website/app/leagues/migration/page.tsx b/apps/website/app/leagues/migration/page.tsx new file mode 100644 index 000000000..00d191009 --- /dev/null +++ b/apps/website/app/leagues/migration/page.tsx @@ -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 ( +
+
+ + League Migration + + We are currently preparing our migration tools. + If you want to move your league to GridPilot today, please contact our support team. + + +
+
+ ); +} diff --git a/apps/website/components/home/CtaSection.tsx b/apps/website/components/home/CtaSection.tsx new file mode 100644 index 000000000..1c13ef31f --- /dev/null +++ b/apps/website/components/home/CtaSection.tsx @@ -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 ( +
+ + + + + + + + + + Ready to elevate your league? + + + Join the growing ecosystem of professional sim racing leagues. + Start for free and scale as your community grows. + + + + + + + + + +
+ ); +} diff --git a/apps/website/components/home/EcosystemGrid.tsx b/apps/website/components/home/EcosystemGrid.tsx new file mode 100644 index 000000000..298017e6d --- /dev/null +++ b/apps/website/components/home/EcosystemGrid.tsx @@ -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 ( +
+ + + + + Live Ecosystem + + + THE GRID + + + Explore the leagues, teams, and races that define the GridPilot ecosystem. + + + + + + + {/* Leagues Column */} + + + + Top Leagues + + {leagues.slice(0, 3).map((league) => ( + + {league.name} + {league.description} + + ))} + + + {/* Teams Column */} + + + + Active Teams + + {teams.slice(0, 3).map((team) => ( + + {team.name} + {team.description} + + ))} + + + {/* Races Column */} + + + + Upcoming Races + + {races.slice(0, 3).map((race) => ( + + + + {race.track} + {race.car} + + {race.formattedDate} + + + ))} + + + +
+ ); +} diff --git a/apps/website/components/home/Hero.tsx b/apps/website/components/home/Hero.tsx new file mode 100644 index 000000000..677e84951 --- /dev/null +++ b/apps/website/components/home/Hero.tsx @@ -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 ( +
+ + + + Sim Racing Infrastructure + + + + Professional League Management.
+ Engineered for Control. +
+
+ + + + GridPilot eliminates the administrative overhead of running iRacing leagues. + No spreadsheets. No manual points. No protest chaos. + Just pure competition, structured for growth. + + + + + + + +
+
+ ); +} diff --git a/apps/website/components/home/LeagueIdentityPreview.tsx b/apps/website/components/home/LeagueIdentityPreview.tsx new file mode 100644 index 000000000..806a67062 --- /dev/null +++ b/apps/website/components/home/LeagueIdentityPreview.tsx @@ -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 ( +
+ + + Your Brand. Your Rules. + + 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. + + + + + {/* Your Brand - Visual Identity */} + + + + + + + Your Brand + + Professional Presence + Build prestige with a dedicated home for your competition. + + + + + + + + Custom Subdomains + yourleague.gridpilot.racing + + + + + + + + Live Standings + Real-time updates across all car classes. + + + + + + + + Driver Roster + Detailed profiles with lifetime league stats. + + + + + + + + + + + Apex Racing League + apex.gridpilot.racing + + + + + + + + + {/* Your Rules - Deep Customization */} + + + + + + + Your Rules + + Absolute Control + The platform adapts to your league, not the other way around. + + + + + + + + Flexible Points Systems + Custom points for positions, fastest laps, and incidents. + + + + + + + + Drop Weeks & Playoffs + Configure complex season structures with ease. + + + + + + + + Multi-Class Support + Manage GT3, GTP, and more in a single season. + + + + + + + Points Configuration + + P1: 25pts + P2: 18pts + P3: 15pts + INC: -1pt + + + + + + + + + +
+ ); +} diff --git a/apps/website/components/home/MigrationSection.tsx b/apps/website/components/home/MigrationSection.tsx new file mode 100644 index 000000000..10a430b49 --- /dev/null +++ b/apps/website/components/home/MigrationSection.tsx @@ -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 ( +
+ + + + + + League Migration + + Moving from Sheets or Discord? + + 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. + + + + + + +
+ ); +} diff --git a/apps/website/components/home/StewardingPreview.tsx b/apps/website/components/home/StewardingPreview.tsx new file mode 100644 index 000000000..cbf129086 --- /dev/null +++ b/apps/website/components/home/StewardingPreview.tsx @@ -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 ( +
+ + + Structured Stewarding + + 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. + + + + + + + + + Incident Report + + ID: 402-WG + + Turn 1 Contact: Miller vs Chen + + UNDER REVIEW + + + + + + + + Protestor + + Alex Miller + #42 - Porsche 911 GT3 R + + + + + + + Defendant + + David Chen + #18 - BMW M4 GT3 + + + + + + + Session Info + + Lap 1, 00:42.150 + Watkins Glen - Cup + + + + + + + + Description + + + + "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." + + + + + + + + + + + +
+ ); +} diff --git a/apps/website/components/home/TelemetryStrip.tsx b/apps/website/components/home/TelemetryStrip.tsx new file mode 100644 index 000000000..8865f06ae --- /dev/null +++ b/apps/website/components/home/TelemetryStrip.tsx @@ -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 ( +
+ +
+ ); +} diff --git a/apps/website/components/home/ValuePillars.tsx b/apps/website/components/home/ValuePillars.tsx new file mode 100644 index 000000000..d8d396d8c --- /dev/null +++ b/apps/website/components/home/ValuePillars.tsx @@ -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 ( +
+ + + {pillars.map((pillar) => ( + + ))} + + +
+ ); +} + diff --git a/apps/website/lib/routing/RouteConfig.ts b/apps/website/lib/routing/RouteConfig.ts index e0f9e5074..63e47db5f 100644 --- a/apps/website/lib/routing/RouteConfig.ts +++ b/apps/website/lib/routing/RouteConfig.ts @@ -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', diff --git a/apps/website/templates/HomeTemplate.tsx b/apps/website/templates/HomeTemplate.tsx index 6b44d1776..570b28b62 100644 --- a/apps/website/templates/HomeTemplate.tsx +++ b/apps/website/templates/HomeTemplate.tsx @@ -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 ( - - {/* Hero Section */} - +
+ {/* Hero Section - Admin Focus */} + - {/* Telemetry Status Strip */} - + {/* Admin Pain/Solution Strip */} + - {/* Quick Actions Bar */} - + {/* Core Admin Features */} + - {/* Feature Sections */} - - } - mockup={} - /> + {/* Stewarding Workflow Preview */} + - - } - mockup={} - /> + {/* League Identity Showcase */} + - - } - mockup={} - /> + {/* Migration Offer */} + - - } - mockup={} - /> - - {/* Discovery Grid */} - - - - - - - - - {/* CTA & FAQ */} - - - + {/* Final CTA */} + +
); } diff --git a/apps/website/ui/Button.tsx b/apps/website/ui/Button.tsx index 00ccb189d..fb33d195c 100644 --- a/apps/website/ui/Button.tsx +++ b/apps/website/ui/Button.tsx @@ -6,48 +6,47 @@ import { Icon } from './Icon'; export interface ButtonProps { children: ReactNode; onClick?: MouseEventHandler; - 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(({ children, onClick, @@ -63,94 +62,80 @@ export const Button = forwardRef { - 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 ); - if (as === 'a') { - return ( - } - href={href} - target={target} - rel={rel} - className={classes} - onClick={onClick as MouseEventHandler} - style={style} - title={title} - > - {content} - - ); - } + const Tag = as === 'a' ? 'a' : 'button'; return ( - + ); }); diff --git a/apps/website/ui/Card.tsx b/apps/website/ui/Card.tsx index 4bf16b20b..167f2822a 100644 --- a/apps/website/ui/Card.tsx +++ b/apps/website/ui/Card.tsx @@ -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, '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(({ 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 ( - {title && ( - +
{typeof title === 'string' ? ( -

{title}

+ {title} ) : title} - +
)} - +
{children} - +
{footer && ( - +
{footer} - +
)} -
+ ); }); Card.displayName = 'Card'; export const CardHeader = ({ title, children }: { title?: string, children?: ReactNode }) => ( - - {title &&

{title}

} +
+ {title && {title}} {children} - +
); export const CardContent = ({ children }: { children: ReactNode }) => ( - {children} +
{children}
); export const CardFooter = ({ children }: { children: ReactNode }) => ( - +
{children} - +
); diff --git a/apps/website/ui/Container.tsx b/apps/website/ui/Container.tsx index 1a7cc350b..bb9768a3e 100644 --- a/apps/website/ui/Container.tsx +++ b/apps/website/ui/Container.tsx @@ -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 ( - +
{children} - +
); }; diff --git a/apps/website/ui/FeatureGrid.tsx b/apps/website/ui/FeatureGrid.tsx new file mode 100644 index 000000000..20b7a0d85 --- /dev/null +++ b/apps/website/ui/FeatureGrid.tsx @@ -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 ( + + {children} + + ); +} diff --git a/apps/website/ui/FeatureItem.tsx b/apps/website/ui/FeatureItem.tsx new file mode 100644 index 000000000..f2c59b9d2 --- /dev/null +++ b/apps/website/ui/FeatureItem.tsx @@ -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 ( + + + + + + + + {title} + + + {description} + + + + + ); +} diff --git a/apps/website/ui/Heading.tsx b/apps/website/ui/Heading.tsx index b6e08743f..6e629a731 100644 --- a/apps/website/ui/Heading.tsx +++ b/apps/website/ui/Heading.tsx @@ -1,31 +1,49 @@ import { ReactNode, forwardRef } from 'react'; -import { Box, BoxProps, ResponsiveValue, Spacing } from './Box'; -export interface HeadingProps extends BoxProps { +export interface HeadingProps { children: ReactNode; level?: 1 | 2 | 3 | 4 | 5 | 6; weight?: 'normal' | 'medium' | 'semibold' | 'bold'; align?: 'left' | 'center' | 'right'; - fontSize?: string | ResponsiveValue; - 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(({ 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(({ }; 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 ( - - + +
{icon} {children} - - +
+
); }); diff --git a/apps/website/ui/IconButton.tsx b/apps/website/ui/IconButton.tsx index 9f1b2ea00..d19a66095 100644 --- a/apps/website/ui/IconButton.tsx +++ b/apps/website/ui/IconButton.tsx @@ -15,11 +15,11 @@ export const IconButton = forwardRef { - const iconSizeMap = { + const iconSizeMap: Record = { sm: 3, md: 4, lg: 5 - } as const; + }; return (