diff --git a/apps/website/app/leagues/LeaguesPageClient.tsx b/apps/website/app/leagues/LeaguesPageClient.tsx
index 546214b59..6e129379c 100644
--- a/apps/website/app/leagues/LeaguesPageClient.tsx
+++ b/apps/website/app/leagues/LeaguesPageClient.tsx
@@ -8,7 +8,7 @@ import { Button } from '@/ui/Button';
import { Heading } from '@/ui/Heading';
import { Input } from '@/ui/Input';
import { Box } from '@/ui/primitives/Box';
-import { Stack } from '@/ui/Stack';
+import { Stack } from '@/ui/primitives/Stack';
import { Text } from '@/ui/Text';
import {
Award,
diff --git a/apps/website/app/leagues/[id]/schedule/admin/LeagueAdminSchedulePageClient.tsx b/apps/website/app/leagues/[id]/schedule/admin/LeagueAdminSchedulePageClient.tsx
index 78930c367..944714277 100644
--- a/apps/website/app/leagues/[id]/schedule/admin/LeagueAdminSchedulePageClient.tsx
+++ b/apps/website/app/leagues/[id]/schedule/admin/LeagueAdminSchedulePageClient.tsx
@@ -20,7 +20,7 @@ import { LeagueAdminScheduleTemplate } from '@/templates/LeagueAdminScheduleTemp
import { Card } from '@/ui/Card';
import { Heading } from '@/ui/Heading';
import { Box } from '@/ui/primitives/Box';
-import { Stack } from '@/ui/Stack';
+import { Stack } from '@/ui/primitives/Stack';
import { Text } from '@/ui/Text';
import { useParams, useRouter } from 'next/navigation';
import { useState } from 'react';
diff --git a/apps/website/app/leagues/[id]/stewarding/StewardingPageClient.tsx b/apps/website/app/leagues/[id]/stewarding/StewardingPageClient.tsx
index 8b0c26b4b..8448caa09 100644
--- a/apps/website/app/leagues/[id]/stewarding/StewardingPageClient.tsx
+++ b/apps/website/app/leagues/[id]/stewarding/StewardingPageClient.tsx
@@ -14,7 +14,7 @@ import { RaceViewModel } from '@/lib/view-models/RaceViewModel';
import { Button } from '@/ui/Button';
import { Card } from '@/ui/Card';
import { Box } from '@/ui/primitives/Box';
-import { Stack } from '@/ui/Stack';
+import { Stack } from '@/ui/primitives/Stack';
import { Text } from '@/ui/Text';
import { useMemo, useState } from 'react';
diff --git a/apps/website/app/leagues/[id]/stewarding/protests/[protestId]/ProtestDetailPageClient.tsx b/apps/website/app/leagues/[id]/stewarding/protests/[protestId]/ProtestDetailPageClient.tsx
index 470e8693b..043f23b39 100644
--- a/apps/website/app/leagues/[id]/stewarding/protests/[protestId]/ProtestDetailPageClient.tsx
+++ b/apps/website/app/leagues/[id]/stewarding/protests/[protestId]/ProtestDetailPageClient.tsx
@@ -38,13 +38,13 @@ import { StateContainer } from '@/components/shared/state/StateContainer';
import { useLeagueAdminStatus } from "@/hooks/league/useLeagueAdminStatus";
import { useProtestDetail } from "@/hooks/league/useProtestDetail";
import { routes } from '@/lib/routing/RouteConfig';
-import { GridItem } from '@/ui/GridItem';
+import { GridItem } from '@/ui/primitives/GridItem';
import { Heading } from '@/ui/Heading';
import { Icon as UIIcon } from '@/ui/Icon';
import { Link as UILink } from '@/ui/Link';
import { Box } from '@/ui/primitives/Box';
import { Grid } from '@/ui/primitives/Grid';
-import { Stack } from '@/ui/Stack';
+import { Stack } from '@/ui/primitives/Stack';
import { Text } from '@/ui/Text';
type PenaltyUiConfig = {
diff --git a/apps/website/app/leagues/[id]/wallet/LeagueWalletPageClient.tsx b/apps/website/app/leagues/[id]/wallet/LeagueWalletPageClient.tsx
index 47a53fb22..b440b6083 100644
--- a/apps/website/app/leagues/[id]/wallet/LeagueWalletPageClient.tsx
+++ b/apps/website/app/leagues/[id]/wallet/LeagueWalletPageClient.tsx
@@ -7,7 +7,7 @@ import { Container } from '@/ui/Container';
import { Heading } from '@/ui/Heading';
import { Icon as UIIcon } from '@/ui/Icon';
import { Box } from '@/ui/primitives/Box';
-import { Stack } from '@/ui/Stack';
+import { Stack } from '@/ui/primitives/Stack';
import { Text } from '@/ui/Text';
import {
Download
diff --git a/apps/website/app/leagues/create/CreateLeagueWizard.tsx b/apps/website/app/leagues/create/CreateLeagueWizard.tsx
index abc1c0f94..6d7aeb2bc 100644
--- a/apps/website/app/leagues/create/CreateLeagueWizard.tsx
+++ b/apps/website/app/leagues/create/CreateLeagueWizard.tsx
@@ -8,7 +8,7 @@ import { Heading } from '@/ui/Heading';
import { Icon } from '@/ui/Icon';
import { Input } from '@/ui/Input';
import { Box } from '@/ui/primitives/Box';
-import { Stack } from '@/ui/Stack';
+import { Stack } from '@/ui/primitives/Stack';
import { Text } from '@/ui/Text';
import {
AlertCircle,
diff --git a/apps/website/app/sponsor/billing/page.tsx b/apps/website/app/sponsor/billing/page.tsx
index 071272192..5ceb57ee0 100644
--- a/apps/website/app/sponsor/billing/page.tsx
+++ b/apps/website/app/sponsor/billing/page.tsx
@@ -2,7 +2,7 @@
import { useSponsorBilling } from "@/hooks/sponsor/useSponsorBilling";
import { SponsorBillingTemplate } from "@/templates/SponsorBillingTemplate";
-import { Box } from "@/ui/Box";
+import { Box } from "@/ui/primitives/Box";
import { Text } from "@/ui/Text";
import { Button } from "@/ui/Button";
import { DollarSign, AlertTriangle, Calendar, TrendingUp } from "lucide-react";
diff --git a/apps/website/app/sponsor/campaigns/page.tsx b/apps/website/app/sponsor/campaigns/page.tsx
index b24030593..9b6e9b17b 100644
--- a/apps/website/app/sponsor/campaigns/page.tsx
+++ b/apps/website/app/sponsor/campaigns/page.tsx
@@ -3,7 +3,7 @@
import { useState } from 'react';
import { useSponsorSponsorships } from "@/hooks/sponsor/useSponsorSponsorships";
import { SponsorCampaignsTemplate, SponsorshipType } from "@/templates/SponsorCampaignsTemplate";
-import { Box } from "@/ui/Box";
+import { Box } from "@/ui/primitives/Box";
import { Text } from "@/ui/Text";
import { Button } from "@/ui/Button";
diff --git a/apps/website/app/sponsor/signup/page.tsx b/apps/website/app/sponsor/signup/page.tsx
index 445569922..fda0cef9b 100644
--- a/apps/website/app/sponsor/signup/page.tsx
+++ b/apps/website/app/sponsor/signup/page.tsx
@@ -10,7 +10,7 @@ import { Card } from '@/ui/Card';
import { Heading } from '@/ui/Heading';
import { Input } from '@/ui/Input';
import { Box } from '@/ui/primitives/Box';
-import { Stack } from '@/ui/Stack';
+import { Stack } from '@/ui/primitives/Stack';
import { Text } from '@/ui/Text';
import { motion, useReducedMotion } from 'framer-motion';
import {
diff --git a/apps/website/components/achievements/AchievementGrid.tsx b/apps/website/components/achievements/AchievementGrid.tsx
index 7b1cd50de..c47c526fa 100644
--- a/apps/website/components/achievements/AchievementGrid.tsx
+++ b/apps/website/components/achievements/AchievementGrid.tsx
@@ -1,9 +1,9 @@
import { AchievementDisplay } from '@/lib/display-objects/AchievementDisplay';
import { Card } from '@/ui/Card';
-import { Grid } from '@/ui/Grid';
+import { Grid } from '@/ui/primitives/Grid';
import { Heading } from '@/ui/Heading';
import { Icon } from '@/ui/Icon';
-import { Stack } from '@/ui/Stack';
+import { Stack } from '@/ui/primitives/Stack';
import { Text } from '@/ui/Text';
import { Award, Crown, Medal, Star, Target, Trophy, Zap } from 'lucide-react';
diff --git a/apps/website/components/dashboard/DashboardKpiRow.tsx b/apps/website/components/dashboard/DashboardKpiRow.tsx
index 620974e32..f371fa4b9 100644
--- a/apps/website/components/dashboard/DashboardKpiRow.tsx
+++ b/apps/website/components/dashboard/DashboardKpiRow.tsx
@@ -1,4 +1,4 @@
-import { Grid } from '@/ui/Grid';
+import { Grid } from '@/ui/primitives/Grid';
import { Stack } from '@/ui/primitives/Stack';
import { Text } from '@/ui/Text';
diff --git a/apps/website/components/drivers/CareerHighlights.tsx b/apps/website/components/drivers/CareerHighlights.tsx
index c7b981934..8b08e2db2 100644
--- a/apps/website/components/drivers/CareerHighlights.tsx
+++ b/apps/website/components/drivers/CareerHighlights.tsx
@@ -3,8 +3,8 @@ import { Card } from '@/ui/Card';
import { GoalCard } from '@/ui/GoalCard';
import { Heading } from '@/ui/Heading';
import { MilestoneItem } from '@/components/achievements/MilestoneItem';
-import { Stack } from '@/ui/Stack';
-import { Grid } from '@/ui/Grid';
+import { Stack } from '@/ui/primitives/Stack';
+import { Grid } from '@/ui/primitives/Grid';
interface Achievement {
id: string;
diff --git a/apps/website/components/home/HomeFeatureSection.tsx b/apps/website/components/home/HomeFeatureSection.tsx
index 83f40bb54..8e98b454d 100644
--- a/apps/website/components/home/HomeFeatureSection.tsx
+++ b/apps/website/components/home/HomeFeatureSection.tsx
@@ -3,8 +3,8 @@
import React from 'react';
import { Panel } from '@/ui/Panel';
import { Glow } from '@/ui/Glow';
-import { Stack } from '@/ui/Stack';
-import { Grid } from '@/ui/Grid';
+import { Stack } from '@/ui/primitives/Stack';
+import { Grid } from '@/ui/primitives/Grid';
import { Container } from '@/ui/Container';
import { Heading } from '@/ui/Heading';
import { Section } from '@/ui/Section';
diff --git a/apps/website/components/home/HomeFooterCTA.tsx b/apps/website/components/home/HomeFooterCTA.tsx
index a895bc68b..384fb6a35 100644
--- a/apps/website/components/home/HomeFooterCTA.tsx
+++ b/apps/website/components/home/HomeFooterCTA.tsx
@@ -8,8 +8,8 @@ import { DiscordIcon } from '@/ui/icons/DiscordIcon';
import { Code, Lightbulb, LucideIcon, MessageSquare, Users } from 'lucide-react';
import { Heading } from '@/ui/Heading';
import { Text } from '@/ui/Text';
-import { Stack } from '@/ui/Stack';
-import { Grid } from '@/ui/Grid';
+import { Stack } from '@/ui/primitives/Stack';
+import { Grid } from '@/ui/primitives/Grid';
import { Card } from '@/ui/Card';
import { Section } from '@/ui/Section';
import { Container } from '@/ui/Container';
diff --git a/apps/website/components/home/HomeStatsStrip.tsx b/apps/website/components/home/HomeStatsStrip.tsx
index 819b5f36c..c0450d207 100644
--- a/apps/website/components/home/HomeStatsStrip.tsx
+++ b/apps/website/components/home/HomeStatsStrip.tsx
@@ -4,8 +4,8 @@ import React from 'react';
import { MetricCard } from '@/ui/MetricCard';
import { Activity, Users, Trophy, Calendar } from 'lucide-react';
import { Container } from '@/ui/Container';
-import { Grid } from '@/ui/Grid';
-import { Stack } from '@/ui/Stack';
+import { Grid } from '@/ui/primitives/Grid';
+import { Stack } from '@/ui/primitives/Stack';
/**
* HomeStatsStrip - A thin strip showing some status or quick info.
diff --git a/apps/website/components/landing/DiscoverySection.tsx b/apps/website/components/landing/DiscoverySection.tsx
index 42a376dde..f633dcc8f 100644
--- a/apps/website/components/landing/DiscoverySection.tsx
+++ b/apps/website/components/landing/DiscoverySection.tsx
@@ -1,10 +1,10 @@
'use client';
import { routes } from '@/lib/routing/RouteConfig';
-import { Stack } from '@/ui/Stack';
+import { Stack } from '@/ui/primitives/Stack';
import { Button } from '@/ui/Button';
import { Container } from '@/ui/Container';
-import { Grid } from '@/ui/Grid';
+import { Grid } from '@/ui/primitives/Grid';
import { Heading } from '@/ui/Heading';
import { Link } from '@/ui/Link';
import { Text } from '@/ui/Text';
diff --git a/apps/website/components/leagues/LeagueBasicsSection.tsx b/apps/website/components/leagues/LeagueBasicsSection.tsx
index 21103114f..f64addd52 100644
--- a/apps/website/components/leagues/LeagueBasicsSection.tsx
+++ b/apps/website/components/leagues/LeagueBasicsSection.tsx
@@ -2,7 +2,7 @@
import type { LeagueConfigFormModel } from '@/lib/types/LeagueConfigFormModel';
import { Button } from '@/ui/Button';
-import { Grid } from '@/ui/Grid';
+import { Grid } from '@/ui/primitives/Grid';
import { Heading } from '@/ui/Heading';
import { Icon } from '@/ui/Icon';
import { Input } from '@/ui/Input';
diff --git a/apps/website/components/leagues/LeagueReviewSummary.tsx b/apps/website/components/leagues/LeagueReviewSummary.tsx
index 4de605070..bef37554a 100644
--- a/apps/website/components/leagues/LeagueReviewSummary.tsx
+++ b/apps/website/components/leagues/LeagueReviewSummary.tsx
@@ -20,12 +20,12 @@ import {
} from 'lucide-react';
import type { LeagueConfigFormModel } from '@/lib/types/LeagueConfigFormModel';
import type { LeagueScoringPresetViewModel } from '@/lib/view-models/LeagueScoringPresetViewModel';
-import { Stack } from '@/ui/Stack';
+import { Stack } from '@/ui/primitives/Stack';
import { Text } from '@/ui/Text';
import { Heading } from '@/ui/Heading';
import { Icon } from '@/ui/Icon';
import { Card } from '@/ui/Card';
-import { Grid } from '@/ui/Grid';
+import { Grid } from '@/ui/primitives/Grid';
interface LeagueReviewSummaryProps {
form: LeagueConfigFormModel;
diff --git a/apps/website/components/leagues/LeagueScoringSection.tsx b/apps/website/components/leagues/LeagueScoringSection.tsx
index 41e40d03a..04b86937f 100644
--- a/apps/website/components/leagues/LeagueScoringSection.tsx
+++ b/apps/website/components/leagues/LeagueScoringSection.tsx
@@ -4,7 +4,7 @@ import type { LeagueConfigFormModel } from '@/lib/types/LeagueConfigFormModel';
import type { LeagueScoringPresetViewModel } from '@/lib/view-models/LeagueScoringPresetViewModel';
import type { CustomPointsConfig } from '@/lib/view-models/ScoringConfigurationViewModel';
import { Button } from '@/ui/Button';
-import { Grid } from '@/ui/Grid';
+import { Grid } from '@/ui/primitives/Grid';
import { Heading } from '@/ui/Heading';
import { Icon } from '@/ui/Icon';
import { Stack } from '@/ui/primitives/Stack';
diff --git a/apps/website/components/leagues/LeagueSummaryCard.tsx b/apps/website/components/leagues/LeagueSummaryCard.tsx
index 44f97ab69..4feeb93af 100644
--- a/apps/website/components/leagues/LeagueSummaryCard.tsx
+++ b/apps/website/components/leagues/LeagueSummaryCard.tsx
@@ -1,13 +1,13 @@
import { ArrowRight } from 'lucide-react';
import { Button } from '@/ui/Button';
import { Card } from '@/ui/Card';
-import { Grid } from '@/ui/Grid';
+import { Grid } from '@/ui/primitives/Grid';
import { Heading } from '@/ui/Heading';
import { Icon } from '@/ui/Icon';
import { LeagueLogo } from './LeagueLogo';
import { Link } from '@/ui/Link';
import { Text } from '@/ui/Text';
-import { Stack } from '@/ui/Stack';
+import { Stack } from '@/ui/primitives/Stack';
interface LeagueSummaryCardProps {
id: string;
diff --git a/apps/website/components/leagues/PenaltyHistoryList.tsx b/apps/website/components/leagues/PenaltyHistoryList.tsx
index 84d2c1085..304d49d37 100644
--- a/apps/website/components/leagues/PenaltyHistoryList.tsx
+++ b/apps/website/components/leagues/PenaltyHistoryList.tsx
@@ -5,7 +5,7 @@ import { ProtestViewModel } from "@/lib/view-models/ProtestViewModel";
import { RaceViewModel } from "@/lib/view-models/RaceViewModel";
import { DriverViewModel } from "@/lib/view-models/DriverViewModel";
import { Card } from "@/ui/Card";
-import { Stack } from "@/ui/Stack";
+import { Stack } from "@/ui/primitives/Stack";
import { Text } from "@/ui/Text";
import { Heading } from "@/ui/Heading";
import { Icon } from "@/ui/Icon";
diff --git a/apps/website/components/leagues/PendingProtestsList.tsx b/apps/website/components/leagues/PendingProtestsList.tsx
index 64402c75e..4ed8ca062 100644
--- a/apps/website/components/leagues/PendingProtestsList.tsx
+++ b/apps/website/components/leagues/PendingProtestsList.tsx
@@ -1,7 +1,7 @@
import { DriverViewModel } from "@/lib/view-models/DriverViewModel";
import { ProtestViewModel } from "@/lib/view-models/ProtestViewModel";
import { RaceViewModel } from "@/lib/view-models/RaceViewModel";
-import { Stack } from "@/ui/Stack";
+import { Stack } from "@/ui/primitives/Stack";
import { Card } from "@/ui/Card";
import { ProtestListItem } from "./ProtestListItem";
import { Text } from "@/ui/Text";
diff --git a/apps/website/components/leagues/ReviewProtestModal.tsx b/apps/website/components/leagues/ReviewProtestModal.tsx
index d9aa8059f..5424f128f 100644
--- a/apps/website/components/leagues/ReviewProtestModal.tsx
+++ b/apps/website/components/leagues/ReviewProtestModal.tsx
@@ -7,13 +7,13 @@ import { ProtestViewModel } from "../../lib/view-models/ProtestViewModel";
import { Modal } from "@/ui/Modal";
import { Button } from "@/ui/Button";
import { Card } from "@/ui/Card";
-import { Stack } from "@/ui/Stack";
+import { Stack } from "@/ui/primitives/Stack";
import { Text } from "@/ui/Text";
import { Heading } from "@/ui/Heading";
import { Icon } from "@/ui/Icon";
import { TextArea } from "@/ui/TextArea";
import { Input } from "@/ui/Input";
-import { Grid } from "@/ui/Grid";
+import { Grid } from "@/ui/primitives/Grid";
import {
AlertCircle,
Video,
diff --git a/apps/website/components/leagues/ScheduleRaceCard.tsx b/apps/website/components/leagues/ScheduleRaceCard.tsx
index 84898bad6..f2ef3a37c 100644
--- a/apps/website/components/leagues/ScheduleRaceCard.tsx
+++ b/apps/website/components/leagues/ScheduleRaceCard.tsx
@@ -3,11 +3,11 @@
import React from 'react';
import { Calendar, Clock, MapPin, Car, Trophy } from 'lucide-react';
import { Card } from '@/ui/Card';
-import { Stack } from '@/ui/Stack';
+import { Stack } from '@/ui/primitives/Stack';
import { Text } from '@/ui/Text';
import { Heading } from '@/ui/Heading';
import { Badge } from '@/ui/Badge';
-import { Grid } from '@/ui/Grid';
+import { Grid } from '@/ui/primitives/Grid';
import { Icon } from '@/ui/Icon';
interface Race {
diff --git a/apps/website/components/sponsors/BillingSummaryPanel.tsx b/apps/website/components/sponsors/BillingSummaryPanel.tsx
index 78e73562f..db6ab169c 100644
--- a/apps/website/components/sponsors/BillingSummaryPanel.tsx
+++ b/apps/website/components/sponsors/BillingSummaryPanel.tsx
@@ -1,4 +1,4 @@
-import { Grid } from '@/ui/Grid';
+import { Grid } from '@/ui/primitives/Grid';
import { Icon } from '@/ui/Icon';
import { Stack } from '@/ui/primitives/Stack';
import { Text } from '@/ui/Text';
diff --git a/apps/website/components/sponsors/TransactionTable.tsx b/apps/website/components/sponsors/TransactionTable.tsx
index e0bce0c82..abd71be41 100644
--- a/apps/website/components/sponsors/TransactionTable.tsx
+++ b/apps/website/components/sponsors/TransactionTable.tsx
@@ -1,5 +1,5 @@
import { Button } from '@/ui/Button';
-import { Grid } from '@/ui/Grid';
+import { Grid } from '@/ui/primitives/Grid';
import { Icon } from '@/ui/Icon';
import { Stack } from '@/ui/primitives/Stack';
import { Text } from '@/ui/Text';
diff --git a/apps/website/templates/AdminDashboardTemplate.tsx b/apps/website/templates/AdminDashboardTemplate.tsx
index ed0e75299..9bb5727f0 100644
--- a/apps/website/templates/AdminDashboardTemplate.tsx
+++ b/apps/website/templates/AdminDashboardTemplate.tsx
@@ -9,11 +9,11 @@ import { AdminDashboardViewData } from '@/lib/view-data/AdminDashboardViewData';
import { Button } from '@/ui/Button';
import { Card } from '@/ui/Card';
import { Container } from '@/ui/Container';
-import { Grid } from '@/ui/Grid';
+import { Grid } from '@/ui/primitives/Grid';
import { Icon } from '@/ui/Icon';
import { Box } from '@/ui/primitives/Box';
import { QuickActionLink } from '@/ui/QuickActionLink';
-import { Stack } from '@/ui/Stack';
+import { Stack } from '@/ui/primitives/Stack';
import { StatusBadge } from '@/ui/StatusBadge';
import { Text } from '@/ui/Text';
import {
diff --git a/apps/website/templates/DashboardTemplate.tsx b/apps/website/templates/DashboardTemplate.tsx
index 67f1c2108..a0a565f3f 100644
--- a/apps/website/templates/DashboardTemplate.tsx
+++ b/apps/website/templates/DashboardTemplate.tsx
@@ -10,10 +10,10 @@ import { routes } from '@/lib/routing/RouteConfig';
import type { DashboardViewData } from '@/lib/view-data/DashboardViewData';
import { Avatar } from '@/ui/Avatar';
import { Button } from '@/ui/Button';
-import { Grid } from '@/ui/Grid';
+import { Grid } from '@/ui/primitives/Grid';
import { IconButton } from '@/ui/IconButton';
import { Box } from '@/ui/primitives/Box';
-import { Stack } from '@/ui/Stack';
+import { Stack } from '@/ui/primitives/Stack';
import { Text } from '@/ui/Text';
import { Bell, Calendar, LayoutDashboard, Search, Settings, Trophy, Users } from 'lucide-react';
import { useRouter } from 'next/navigation';
diff --git a/apps/website/templates/LeaderboardsTemplate.tsx b/apps/website/templates/LeaderboardsTemplate.tsx
index 81227654c..fc69624ca 100644
--- a/apps/website/templates/LeaderboardsTemplate.tsx
+++ b/apps/website/templates/LeaderboardsTemplate.tsx
@@ -4,7 +4,7 @@ import { DriverLeaderboardPreview } from '@/components/leaderboards/DriverLeader
import { TeamLeaderboardPreview } from '@/components/teams/TeamLeaderboardPreviewWrapper';
import type { LeaderboardsViewData } from '@/lib/view-data/LeaderboardsViewData';
import { Container } from '@/ui/Container';
-import { GridItem } from '@/ui/GridItem';
+import { GridItem } from '@/ui/primitives/GridItem';
import { PageHero } from '@/ui/PageHero';
import { Grid } from '@/ui/primitives/Grid';
import { Trophy, Users } from 'lucide-react';
diff --git a/apps/website/ui/ActivityItem.tsx b/apps/website/ui/ActivityItem.tsx
index 795fce903..789b3c384 100644
--- a/apps/website/ui/ActivityItem.tsx
+++ b/apps/website/ui/ActivityItem.tsx
@@ -1,66 +1,67 @@
-
-
+import React from 'react';
import { Box } from './primitives/Box';
-import { Link } from './Link';
import { Text } from './Text';
import { Surface } from './primitives/Surface';
+import { Link } from './Link';
interface ActivityItemProps {
- headline: string;
+ title?: string;
+ description?: string;
+ timeAgo?: string;
+ color?: string;
+ headline?: string;
body?: string;
- formattedTime: string;
+ formattedTime?: string;
ctaHref?: string;
ctaLabel?: string;
- typeColor?: string;
}
-export function ActivityItem({
+export function ActivityItem({
+ title,
+ description,
+ timeAgo,
+ color = 'bg-primary-blue',
headline,
body,
formattedTime,
ctaHref,
- ctaLabel,
- typeColor,
+ ctaLabel
}: ActivityItemProps) {
return (
- {typeColor && (
-
- )}
-
+
+
- {headline}
+ {title || headline}
- {body && (
-
- {body}
-
+
+ {description || body}
+
+
+ {timeAgo || formattedTime}
+
+ {ctaHref && ctaLabel && (
+
+
+ {ctaLabel}
+
+
)}
-
- {formattedTime}
-
- {ctaHref && ctaLabel && (
-
-
- {ctaLabel}
-
-
- )}
);
}
diff --git a/apps/website/ui/AuthLoading.tsx b/apps/website/ui/AuthLoading.tsx
index 77580ad8e..b32c4763c 100644
--- a/apps/website/ui/AuthLoading.tsx
+++ b/apps/website/ui/AuthLoading.tsx
@@ -1,8 +1,7 @@
-
-
+import React from 'react';
import { Box } from './primitives/Box';
-import { LoadingSpinner } from './LoadingSpinner';
import { Stack } from './primitives/Stack';
+import { LoadingSpinner } from './LoadingSpinner';
import { Text } from './Text';
interface AuthLoadingProps {
@@ -11,10 +10,18 @@ interface AuthLoadingProps {
export function AuthLoading({ message = 'Authenticating...' }: AuthLoadingProps) {
return (
-
+
-
- {message}
+
+
+ {message}
+
);
diff --git a/apps/website/ui/Avatar.tsx b/apps/website/ui/Avatar.tsx
index 3664cbb9d..99a6ecd37 100644
--- a/apps/website/ui/Avatar.tsx
+++ b/apps/website/ui/Avatar.tsx
@@ -1,51 +1,44 @@
import React from 'react';
import { Box } from './primitives/Box';
import { Image } from './Image';
-import { User } from 'lucide-react';
-import { Icon } from './Icon';
+import { Surface } from './primitives/Surface';
-export interface AvatarProps {
- driverId?: string;
- src?: string;
+interface AvatarProps {
+ src?: string | null;
alt: string;
size?: number;
className?: string;
- border?: boolean;
}
-export function Avatar({
- driverId,
- src,
- alt,
- size = 40,
- className = '',
- border = true,
-}: AvatarProps) {
- const avatarSrc = src || (driverId ? `/media/avatar/${driverId}` : undefined);
-
+export function Avatar({ src, alt, size = 40, className = '' }: AvatarProps) {
return (
-
- {avatarSrc ? (
+ {src ? (
) : (
- 32 ? 5 : 4} color="text-gray-500" />
+
+
+ {alt.charAt(0).toUpperCase()}
+
+
)}
-
+
);
}
diff --git a/apps/website/ui/Badge.tsx b/apps/website/ui/Badge.tsx
index 9464f5df0..8c1aec76c 100644
--- a/apps/website/ui/Badge.tsx
+++ b/apps/website/ui/Badge.tsx
@@ -1,22 +1,16 @@
import React, { ReactNode } from 'react';
-import { Box } from './primitives/Box';
+import { Box, BoxProps } from './primitives/Box';
import { Icon } from './Icon';
import { LucideIcon } from 'lucide-react';
-interface BadgeProps {
+interface BadgeProps extends Omit, 'children'> {
children: ReactNode;
- className?: string;
variant?: 'default' | 'primary' | 'success' | 'warning' | 'danger' | 'info';
size?: 'xs' | 'sm' | 'md';
icon?: LucideIcon;
- style?: React.CSSProperties;
- bg?: string;
- color?: string;
- borderColor?: string;
- rounded?: 'none' | 'sm' | 'md' | 'lg' | 'xl' | '2xl' | 'full';
}
-export function Badge({ children, className = '', variant = 'default', size = 'sm', icon, style, bg, color, borderColor, rounded = 'none' }: BadgeProps) {
+export function Badge({ children, className = '', variant = 'default', size = 'sm', icon, rounded = 'none', ...props }: BadgeProps) {
const baseClasses = 'flex items-center gap-1.5 border font-bold uppercase tracking-widest';
const sizeClasses = {
@@ -47,16 +41,13 @@ export function Badge({ children, className = '', variant = 'default', size = 's
const classes = [
baseClasses,
sizeClasses[size],
- roundedClasses[rounded],
- !bg && !color && !borderColor ? variantClasses[variant] : '',
- bg,
- color,
- borderColor,
+ typeof rounded === 'string' && roundedClasses[rounded as keyof typeof roundedClasses] ? roundedClasses[rounded as keyof typeof roundedClasses] : '',
+ !props.bg && !props.color && !props.borderColor ? variantClasses[variant] : '',
className
].filter(Boolean).join(' ');
return (
-
+
{icon && }
{children}
diff --git a/apps/website/ui/BorderTabs.tsx b/apps/website/ui/BorderTabs.tsx
index 3f59c4f7c..d775667a6 100644
--- a/apps/website/ui/BorderTabs.tsx
+++ b/apps/website/ui/BorderTabs.tsx
@@ -1,64 +1,63 @@
-
-
-import { Badge } from './Badge';
+import React from 'react';
import { Box } from './primitives/Box';
+import { Stack } from './primitives/Stack';
+import { Surface } from './primitives/Surface';
import { Text } from './Text';
interface Tab {
id: string;
label: string;
- count?: number;
- countVariant?: 'default' | 'primary' | 'success' | 'warning' | 'danger' | 'info';
+ icon?: React.ReactNode;
}
interface BorderTabsProps {
tabs: Tab[];
activeTab: string;
onTabChange: (tabId: string) => void;
+ className?: string;
}
-export function BorderTabs({ tabs, activeTab, onTabChange }: BorderTabsProps) {
+export function BorderTabs({ tabs, activeTab, onTabChange, className = '' }: BorderTabsProps) {
return (
-
-
+
+
{tabs.map((tab) => {
const isActive = activeTab === tab.id;
return (
- onTabChange(tab.id)}
- pb={3}
+ variant="ghost"
px={1}
- cursor="pointer"
- transition
- borderBottom={isActive}
+ py={4}
+ position="relative"
borderColor={isActive ? 'border-primary-blue' : ''}
- style={{
- borderBottomWidth: isActive ? '2px' : '0',
- marginBottom: '-1px'
- }}
+ borderBottom={isActive}
+ borderWidth={isActive ? '2px' : '0'}
+ mb="-1px"
+ transition="all 0.2s"
+ group
>
-
+
+ {tab.icon && (
+
+ {tab.icon}
+
+ )}
{tab.label}
- {tab.count !== undefined && tab.count > 0 && (
-
- {tab.count}
-
- )}
-
-
+
+
);
})}
-
+
);
}
diff --git a/apps/website/ui/Button.tsx b/apps/website/ui/Button.tsx
index 0efab6a31..950ee3d87 100644
--- a/apps/website/ui/Button.tsx
+++ b/apps/website/ui/Button.tsx
@@ -4,7 +4,7 @@ import { Box, BoxProps } from './primitives/Box';
import { Loader2 } from 'lucide-react';
import { Icon } from './Icon';
-interface ButtonProps extends Omit, 'as' | 'onMouseEnter' | 'onMouseLeave' | 'onSubmit'>, Omit, 'as' | 'onClick' | 'onSubmit'> {
+interface ButtonProps extends Omit, 'as' | 'onMouseEnter' | 'onMouseLeave' | 'onSubmit' | 'role' | 'translate' | 'onScroll' | 'draggable' | 'onChange' | 'onMouseDown' | 'onMouseUp' | 'onMouseMove' | 'value' | 'onBlur' | 'onKeyDown'>, Omit, 'as' | 'onClick' | 'onSubmit'> {
children: ReactNode;
onClick?: MouseEventHandler;
className?: string;
@@ -19,6 +19,8 @@ interface ButtonProps extends Omit, 'as'
href?: string;
target?: string;
rel?: string;
+ fontSize?: string;
+ backgroundColor?: string;
}
export const Button = forwardRef(({
@@ -36,6 +38,8 @@ export const Button = forwardRef(({
href,
target,
rel,
+ fontSize,
+ backgroundColor,
...props
}, ref) => {
const baseClasses = 'inline-flex items-center justify-center rounded-none transition-all duration-150 ease-smooth focus-visible:outline focus-visible:outline-1 focus-visible:outline-offset-2 active:opacity-80 uppercase tracking-widest font-bold';
@@ -83,6 +87,8 @@ export const Button = forwardRef(({
target={target}
rel={rel}
className={classes}
+ fontSize={fontSize}
+ backgroundColor={backgroundColor}
{...props}
>
{content}
@@ -98,6 +104,8 @@ export const Button = forwardRef(({
className={classes}
onClick={onClick}
disabled={disabled || isLoading}
+ fontSize={fontSize}
+ backgroundColor={backgroundColor}
{...props}
>
{content}
diff --git a/apps/website/ui/Card.tsx b/apps/website/ui/Card.tsx
index 7a01ad32a..9440b1b31 100644
--- a/apps/website/ui/Card.tsx
+++ b/apps/website/ui/Card.tsx
@@ -1,26 +1,10 @@
import React, { ReactNode, MouseEventHandler } from 'react';
import { Box, BoxProps } from './primitives/Box';
-type Spacing = 0 | 0.5 | 1 | 1.5 | 2 | 2.5 | 3 | 3.5 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 14 | 16 | 20 | 24 | 28 | 32 | 36 | 40 | 44 | 48 | 52 | 56 | 60 | 64 | 72 | 80 | 96;
-
-interface ResponsiveSpacing {
- base?: Spacing;
- md?: Spacing;
- lg?: Spacing;
-}
-
-interface CardProps extends Omit, 'children' | 'className' | 'onClick'> {
+export interface CardProps extends Omit, 'children' | 'onClick'> {
children: ReactNode;
- className?: string;
onClick?: MouseEventHandler;
- variant?: 'default' | 'outline' | 'ghost';
- p?: Spacing | ResponsiveSpacing;
- px?: Spacing | ResponsiveSpacing;
- py?: Spacing | ResponsiveSpacing;
- pt?: Spacing | ResponsiveSpacing;
- pb?: Spacing | ResponsiveSpacing;
- pl?: Spacing | ResponsiveSpacing;
- pr?: Spacing | ResponsiveSpacing;
+ variant?: 'default' | 'outline' | 'ghost' | 'muted' | 'dark' | 'glass';
}
export function Card({
@@ -35,7 +19,10 @@ export function Card({
const variantClasses = {
default: 'bg-panel-gray border border-border-gray shadow-card',
outline: 'bg-transparent border border-border-gray',
- ghost: 'bg-transparent border-none'
+ ghost: 'bg-transparent border-none',
+ muted: 'bg-panel-gray/40 border border-border-gray',
+ dark: 'bg-graphite-black border border-border-gray',
+ glass: 'bg-graphite-black/60 backdrop-blur-md border border-border-gray'
};
const classes = [
diff --git a/apps/website/ui/CategoryDistributionCard.tsx b/apps/website/ui/CategoryDistributionCard.tsx
index 8e105cdb5..a33b4c6bc 100644
--- a/apps/website/ui/CategoryDistributionCard.tsx
+++ b/apps/website/ui/CategoryDistributionCard.tsx
@@ -1,35 +1,49 @@
import React from 'react';
import { Box } from './primitives/Box';
import { Text } from './Text';
-import { ProgressBar } from './ProgressBar';
+import { Icon } from './Icon';
+import { LucideIcon } from 'lucide-react';
interface CategoryDistributionCardProps {
label: string;
count: number;
percentage: number;
+ icon: LucideIcon;
color: string;
bgColor: string;
borderColor: string;
- progressColor: string;
}
export function CategoryDistributionCard({
label,
count,
percentage,
+ icon,
color,
bgColor,
borderColor,
- progressColor,
}: CategoryDistributionCardProps) {
return (
-
+
- {count}
+ {count}
+
+
+
- {label}
-
- {percentage}% of drivers
+
+ {label}
+
+
+
+
+
+ {percentage.toFixed(1)}% of total
+
);
}
diff --git a/apps/website/ui/Checkbox.tsx b/apps/website/ui/Checkbox.tsx
index d14c7a3af..5b9acd3ba 100644
--- a/apps/website/ui/Checkbox.tsx
+++ b/apps/website/ui/Checkbox.tsx
@@ -1,5 +1,3 @@
-
-
import React from 'react';
import { Box } from './primitives/Box';
import { Text } from './Text';
@@ -26,7 +24,8 @@ export function Checkbox({ label, checked, onChange, disabled }: CheckboxProps)
border
borderColor="border-charcoal-outline"
rounded="sm"
- className="text-primary-blue focus:ring-primary-blue"
+ ring="primary-blue"
+ color="text-primary-blue"
/>
{label}
diff --git a/apps/website/ui/Container.tsx b/apps/website/ui/Container.tsx
index b94a6fbbd..9ba3bfa9d 100644
--- a/apps/website/ui/Container.tsx
+++ b/apps/website/ui/Container.tsx
@@ -3,7 +3,7 @@ import { Box, BoxProps } from './primitives/Box';
type Spacing = 0 | 0.5 | 1 | 1.5 | 2 | 2.5 | 3 | 3.5 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 14 | 16 | 20 | 24 | 28 | 32 | 36 | 40 | 44 | 48 | 52 | 56 | 60 | 64 | 72 | 80 | 96;
-interface ContainerProps extends BoxProps<'div'> {
+interface ContainerProps extends Omit, 'size' | 'padding'> {
children: ReactNode;
size?: 'sm' | 'md' | 'lg' | 'xl' | 'full';
padding?: boolean;
diff --git a/apps/website/ui/DashboardHero.tsx b/apps/website/ui/DashboardHero.tsx
index 5a169b92f..aa75355a9 100644
--- a/apps/website/ui/DashboardHero.tsx
+++ b/apps/website/ui/DashboardHero.tsx
@@ -1,142 +1,146 @@
-import React, { ReactNode } from 'react';
+import React from 'react';
import { Box } from './primitives/Box';
-import { Heading } from './Heading';
-import { Image } from './Image';
+import { Stack } from './primitives/Stack';
import { Text } from './Text';
-import { Glow } from './Glow';
+import { Heading } from './Heading';
+import { Avatar } from './Avatar';
+import { Badge } from './Badge';
+import { Trophy, Flag, Users, Star } from 'lucide-react';
+import { Icon } from './Icon';
interface DashboardHeroProps {
driverName: string;
- avatarUrl: string;
- country: string;
- rating: string | number;
- rank: string | number;
- totalRaces: string | number;
- actions?: ReactNode;
- stats?: ReactNode;
+ avatarUrl?: string | null;
+ rating: number;
+ rank: number;
+ totalRaces: number;
+ winRate: number;
className?: string;
}
-/**
- * DashboardHero
- *
- * Redesigned for "Precision Racing Minimal" theme.
- * Uses subtle accent glows and crisp separators.
- */
export function DashboardHero({
driverName,
avatarUrl,
- country,
rating,
rank,
totalRaces,
- actions,
- stats,
+ winRate,
className = '',
}: DashboardHeroProps) {
return (
-
- {/* Subtle Accent Glow */}
-
-
-
-
- {/* Driver Identity */}
-
-
-
-
-
-
-
-
-
-
-
- Driver Profile
-
-
- /
-
-
- {country}
-
-
-
- {driverName}
-
-
-
- Rating
- {rating}
-
-
- Rank
- #{rank}
-
-
- Starts
- {totalRaces}
-
-
-
+
+
+ {/* Avatar Section */}
+
+
+
+
+
+
-
- {/* Actions */}
- {actions && (
-
- {actions}
-
- )}
- {/* Stats Grid */}
- {stats && (
-
- {stats}
+ {/* Info Section */}
+
+
+
+ {driverName}
+
+
+
+ Rating
+ {rating}
+
+
+ Rank
+ #{rank}
+
+
+ Starts
+ {totalRaces}
+
+
- )}
-
+
+
+
+ {winRate}% Win Rate
+
+
+ Pro License
+
+
+ Team Redline
+
+
+
+
+ {/* Quick Stats */}
+
+
+ 12
+ Podiums
+
+
+
+ 4
+ Wins
+
+
+
);
diff --git a/apps/website/ui/ErrorBanner.tsx b/apps/website/ui/ErrorBanner.tsx
index 81b529748..453791dda 100644
--- a/apps/website/ui/ErrorBanner.tsx
+++ b/apps/website/ui/ErrorBanner.tsx
@@ -1,36 +1,63 @@
-
-
+import React from 'react';
import { Box } from './primitives/Box';
-import { Surface } from './primitives/Surface';
+import { Stack } from './primitives/Stack';
import { Text } from './Text';
+import { Surface } from './primitives/Surface';
+import { Icon } from './Icon';
+import { AlertCircle, XCircle, Info, AlertTriangle } from 'lucide-react';
-export interface ErrorBannerProps {
- message: string;
+interface ErrorBannerProps {
title?: string;
- variant?: 'error' | 'warning' | 'info';
+ message: string;
+ variant?: 'error' | 'warning' | 'info' | 'success';
}
-export function ErrorBanner({ message, title, variant = 'error' }: ErrorBannerProps) {
- const variantColors = {
- error: { bg: 'rgba(239, 68, 68, 0.1)', border: '#ef4444', text: '#ef4444' },
- warning: { bg: 'rgba(245, 158, 11, 0.1)', border: '#f59e0b', text: '#fcd34d' },
- info: { bg: 'rgba(59, 130, 246, 0.1)', border: '#3b82f6', text: '#3b82f6' },
+export function ErrorBanner({ title, message, variant = 'error' }: ErrorBannerProps) {
+ const configs = {
+ error: {
+ bg: 'rgba(239, 68, 68, 0.1)',
+ border: 'rgba(239, 68, 68, 0.2)',
+ text: 'rgb(248, 113, 113)',
+ icon: XCircle
+ },
+ warning: {
+ bg: 'rgba(245, 158, 11, 0.1)',
+ border: 'rgba(245, 158, 11, 0.2)',
+ text: 'rgb(251, 191, 36)',
+ icon: AlertTriangle
+ },
+ info: {
+ bg: 'rgba(59, 130, 246, 0.1)',
+ border: 'rgba(59, 130, 246, 0.2)',
+ text: 'rgb(96, 165, 250)',
+ icon: Info
+ },
+ success: {
+ bg: 'rgba(16, 185, 129, 0.1)',
+ border: 'rgba(16, 185, 129, 0.2)',
+ text: 'rgb(52, 211, 153)',
+ icon: AlertCircle
+ }
};
- const colors = variantColors[variant];
+ const colors = configs[variant];
return (
-
- {title && {title}}
- {message}
-
+
+
+
+ {title && {title}}
+ {message}
+
+
);
}
diff --git a/apps/website/ui/FormSection.tsx b/apps/website/ui/FormSection.tsx
index 0169927a9..3f685fe08 100644
--- a/apps/website/ui/FormSection.tsx
+++ b/apps/website/ui/FormSection.tsx
@@ -20,7 +20,11 @@ export function FormSection({ children, title }: FormSectionProps) {
size="xs"
weight="bold"
color="text-gray-500"
- className="uppercase tracking-widest border-b border-border-gray pb-1"
+ uppercase
+ letterSpacing="widest"
+ borderBottom
+ borderColor="border-border-gray"
+ pb={1}
>
{title}
diff --git a/apps/website/ui/Heading.tsx b/apps/website/ui/Heading.tsx
index f7881e22c..433296ef4 100644
--- a/apps/website/ui/Heading.tsx
+++ b/apps/website/ui/Heading.tsx
@@ -1,6 +1,6 @@
import React, { ReactNode, ElementType } from 'react';
import { Stack } from './primitives/Stack';
-import { Box, BoxProps } from './primitives/Box';
+import { Box, BoxProps, ResponsiveValue } from './primitives/Box';
interface ResponsiveFontSize {
base?: string;
@@ -18,11 +18,13 @@ interface HeadingProps extends Omit, 'children' | 'as' | 'fontSiz
id?: string;
groupHoverColor?: string;
truncate?: boolean;
+ uppercase?: boolean;
fontSize?: string | ResponsiveFontSize;
- weight?: 'light' | 'normal' | 'medium' | 'semibold' | 'bold';
+ weight?: 'light' | 'normal' | 'medium' | 'semibold' | 'bold' | string;
+ letterSpacing?: string;
}
-export function Heading({ level, children, icon, groupHoverColor, truncate, fontSize, weight, ...props }: HeadingProps) {
+export function Heading({ level, children, icon, groupHoverColor, truncate, uppercase, fontSize, weight, letterSpacing, ...props }: HeadingProps) {
const Tag = `h${level}` as ElementType;
const levelClasses = {
@@ -34,7 +36,7 @@ export function Heading({ level, children, icon, groupHoverColor, truncate, font
6: 'text-xs font-bold text-white tracking-tight uppercase tracking-widest',
};
- const weightClasses = {
+ const weightClasses: Record = {
light: 'font-light',
normal: 'font-normal',
medium: 'font-medium',
@@ -67,14 +69,24 @@ export function Heading({ level, children, icon, groupHoverColor, truncate, font
const classes = [
levelClasses[level],
getFontSizeClasses(fontSize),
- weight ? weightClasses[weight] : '',
+ weight && weightClasses[weight as keyof typeof weightClasses] ? weightClasses[weight as keyof typeof weightClasses] : '',
+ letterSpacing ? `tracking-${letterSpacing}` : '',
+ uppercase ? 'uppercase' : '',
groupHoverColor ? `group-hover:text-${groupHoverColor}` : '',
truncate ? 'truncate' : '',
props.className
].filter(Boolean).join(' ');
return (
-
+
{content}
);
diff --git a/apps/website/ui/HorizontalStatCard.tsx b/apps/website/ui/HorizontalStatCard.tsx
index f224a9130..acba3fec6 100644
--- a/apps/website/ui/HorizontalStatCard.tsx
+++ b/apps/website/ui/HorizontalStatCard.tsx
@@ -1,52 +1,46 @@
-
-
-import { ReactNode } from 'react';
+import React from 'react';
import { Box } from './primitives/Box';
-import { Card } from './Card';
import { Stack } from './primitives/Stack';
-import { Surface } from './primitives/Surface';
import { Text } from './Text';
+import { Surface } from './primitives/Surface';
+import { Icon } from './Icon';
+import { LucideIcon } from 'lucide-react';
interface HorizontalStatCardProps {
label: string;
value: string | number;
- subValue?: string;
- icon: ReactNode;
+ icon: LucideIcon;
+ iconColor?: string;
iconBgColor?: string;
}
export function HorizontalStatCard({
label,
value,
- subValue,
icon,
- iconBgColor,
+ iconColor = 'text-primary-blue',
+ iconBgColor = 'rgba(59, 130, 246, 0.1)',
}: HorizontalStatCardProps) {
return (
-
-
-
+
+
- {icon}
+
-
+
{label}
-
+
{value}
- {subValue && (
-
- {subValue}
-
- )}
-
+
);
}
diff --git a/apps/website/ui/Icon.tsx b/apps/website/ui/Icon.tsx
index e56d6c8f9..75a2d7d28 100644
--- a/apps/website/ui/Icon.tsx
+++ b/apps/website/ui/Icon.tsx
@@ -2,14 +2,29 @@ import React from 'react';
import { LucideIcon } from 'lucide-react';
import { Box, BoxProps } from './primitives/Box';
-interface IconProps extends Omit, 'children' | 'as'> {
- icon: LucideIcon;
+export interface IconProps extends Omit, 'children'> {
+ icon: LucideIcon | React.ReactNode;
size?: number | string;
color?: string;
strokeWidth?: number;
+ animate?: string;
+ transition?: boolean;
+ groupHoverTextColor?: string;
+ groupHoverScale?: boolean;
}
-export function Icon({ icon: LucideIcon, size = 4, color, className = '', style, ...props }: IconProps) {
+export function Icon({
+ icon: IconProp,
+ size = 4,
+ color,
+ className = '',
+ style,
+ animate,
+ transition,
+ groupHoverTextColor,
+ groupHoverScale,
+ ...props
+}: IconProps) {
const sizeMap: Record = {
3: 'w-3 h-3',
3.5: 'w-3.5 h-3.5',
@@ -31,13 +46,35 @@ export function Icon({ icon: LucideIcon, size = 4, color, className = '', style,
const combinedStyle = color && !isTailwindColor ? { color, ...style } : style;
const boxColor = isTailwindColor ? color : undefined;
+ const classes = [
+ sizeClass,
+ animate === 'spin' ? 'animate-spin' : '',
+ transition ? 'transition-all duration-150' : '',
+ groupHoverTextColor ? `group-hover:text-${groupHoverTextColor}` : '',
+ groupHoverScale ? 'group-hover:scale-110 transition-transform' : '',
+ className
+ ].filter(Boolean).join(' ');
+
+ const renderIcon = () => {
+ if (!IconProp) return null;
+ if (typeof IconProp === 'function' || (typeof IconProp === 'object' && 'render' in IconProp)) {
+ const LucideIconComponent = IconProp as LucideIcon;
+ return ;
+ }
+ return IconProp;
+ };
+
return (
+ >
+ {renderIcon()}
+
);
}
diff --git a/apps/website/ui/IconButton.tsx b/apps/website/ui/IconButton.tsx
index b898b4e8f..40bdc82e9 100644
--- a/apps/website/ui/IconButton.tsx
+++ b/apps/website/ui/IconButton.tsx
@@ -1,5 +1,3 @@
-
-
import React from 'react';
import { LucideIcon } from 'lucide-react';
import { Button } from './Button';
@@ -29,9 +27,9 @@ export function IconButton({
backgroundColor,
}: IconButtonProps) {
const sizeMap = {
- sm: { btn: 'w-8 h-8 p-0', icon: 4 },
- md: { btn: 'w-10 h-10 p-0', icon: 5 },
- lg: { btn: 'w-12 h-12 p-0', icon: 6 },
+ sm: { w: '8', h: '8', icon: 4 },
+ md: { w: '10', h: '10', icon: 5 },
+ lg: { w: '12', h: '12', icon: 6 },
};
return (
@@ -40,7 +38,14 @@ export function IconButton({
onClick={onClick}
title={title}
disabled={disabled}
- className={`${sizeMap[size].btn} rounded-full flex items-center justify-center min-h-0 ${className}`}
+ w={sizeMap[size].w}
+ h={sizeMap[size].h}
+ p={0}
+ rounded="full"
+ display="flex"
+ center
+ minHeight="0"
+ className={className}
backgroundColor={backgroundColor}
>
diff --git a/apps/website/ui/InfoBanner.tsx b/apps/website/ui/InfoBanner.tsx
index 312cc61e6..4a636c1af 100644
--- a/apps/website/ui/InfoBanner.tsx
+++ b/apps/website/ui/InfoBanner.tsx
@@ -1,83 +1,75 @@
-
-
-import { AlertTriangle, CheckCircle, Info, LucideIcon, XCircle } from 'lucide-react';
import React from 'react';
import { Box } from './primitives/Box';
-import { Icon } from './Icon';
import { Stack } from './primitives/Stack';
-import { Surface } from './primitives/Surface';
import { Text } from './Text';
-
-type BannerType = 'info' | 'warning' | 'success' | 'error';
+import { Surface } from './primitives/Surface';
+import { Icon } from './Icon';
+import { Info, AlertTriangle, AlertCircle, CheckCircle, LucideIcon } from 'lucide-react';
interface InfoBannerProps {
- type?: BannerType;
title?: string;
- children: React.ReactNode;
+ message?: string;
+ children?: React.ReactNode;
+ variant?: 'info' | 'warning' | 'error' | 'success';
+ type?: 'info' | 'warning' | 'error' | 'success';
icon?: LucideIcon;
}
-export function InfoBanner({
- type = 'info',
- title,
- children,
- icon: CustomIcon,
-}: InfoBannerProps) {
- const bannerConfig: Record = {
+export function InfoBanner({ title, message, children, variant = 'info', type, icon }: InfoBannerProps) {
+ const configs = {
info: {
- icon: Info,
- bg: 'rgba(38, 38, 38, 0.3)',
- border: 'rgba(38, 38, 38, 0.5)',
- titleColor: 'text-gray-300',
- iconColor: '#9ca3af',
+ bg: 'rgba(59, 130, 246, 0.1)',
+ border: 'rgba(59, 130, 246, 0.2)',
+ iconColor: 'rgb(96, 165, 250)',
+ icon: Info
},
warning: {
- icon: AlertTriangle,
bg: 'rgba(245, 158, 11, 0.1)',
- border: 'rgba(245, 158, 11, 0.3)',
- titleColor: 'text-warning-amber',
- iconColor: '#f59e0b',
- },
- success: {
- icon: CheckCircle,
- bg: 'rgba(16, 185, 129, 0.1)',
- border: 'rgba(16, 185, 129, 0.3)',
- titleColor: 'text-performance-green',
- iconColor: '#10b981',
+ border: 'rgba(245, 158, 11, 0.2)',
+ iconColor: 'rgb(251, 191, 36)',
+ icon: AlertTriangle
},
error: {
- icon: XCircle,
bg: 'rgba(239, 68, 68, 0.1)',
- border: 'rgba(239, 68, 68, 0.3)',
- titleColor: 'text-error-red',
- iconColor: '#ef4444',
+ border: 'rgba(239, 68, 68, 0.2)',
+ iconColor: 'rgb(248, 113, 113)',
+ icon: AlertCircle
},
+ success: {
+ bg: 'rgba(16, 185, 129, 0.1)',
+ border: 'rgba(16, 185, 129, 0.2)',
+ iconColor: 'rgb(52, 211, 153)',
+ icon: CheckCircle
+ }
};
- const config = bannerConfig[type];
- const BannerIcon = CustomIcon || config.icon;
-
+ const activeVariant = type || variant;
+ const config = configs[activeVariant as keyof typeof configs] || configs.info;
+ const BannerIcon = icon || config.icon;
+
return (
-
+
{title && (
- {title}
+
+ {title}
+
)}
- {children}
+ {message && (
+
+ {message}
+
+ )}
+ {children}
diff --git a/apps/website/ui/InfoBox.tsx b/apps/website/ui/InfoBox.tsx
index 3055527b6..6bedfd120 100644
--- a/apps/website/ui/InfoBox.tsx
+++ b/apps/website/ui/InfoBox.tsx
@@ -1,62 +1,63 @@
import React from 'react';
-import { Surface } from './primitives/Surface';
-import { Stack } from './primitives/Stack';
import { Box } from './primitives/Box';
-import { Icon } from './Icon';
+import { Stack } from './primitives/Stack';
import { Text } from './Text';
+import { Surface } from './primitives/Surface';
+import { Icon } from './Icon';
import { LucideIcon } from 'lucide-react';
interface InfoBoxProps {
- icon: LucideIcon;
title: string;
description: string;
- variant?: 'primary' | 'success' | 'warning' | 'default';
+ icon: LucideIcon;
+ variant?: 'info' | 'warning' | 'error' | 'success';
}
-export function InfoBox({ icon, title, description, variant = 'default' }: InfoBoxProps) {
- const variantColors = {
- primary: {
+export function InfoBox({ title, description, icon, variant = 'info' }: InfoBoxProps) {
+ const configs = {
+ info: {
bg: 'rgba(59, 130, 246, 0.1)',
- border: '#3b82f6',
- text: '#3b82f6',
- icon: '#3b82f6'
- },
- success: {
- bg: 'rgba(16, 185, 129, 0.1)',
- border: '#10b981',
- text: '#10b981',
- icon: '#10b981'
+ border: 'rgba(59, 130, 246, 0.2)',
+ icon: 'rgb(96, 165, 250)',
+ text: 'text-white'
},
warning: {
bg: 'rgba(245, 158, 11, 0.1)',
- border: '#f59e0b',
- text: '#f59e0b',
- icon: '#f59e0b'
+ border: 'rgba(245, 158, 11, 0.2)',
+ icon: 'rgb(251, 191, 36)',
+ text: 'text-white'
},
- default: {
- bg: 'rgba(38, 38, 38, 0.3)',
- border: '#262626',
- text: 'white',
- icon: '#9ca3af'
+ error: {
+ bg: 'rgba(239, 68, 68, 0.1)',
+ border: 'rgba(239, 68, 68, 0.2)',
+ icon: 'rgb(248, 113, 113)',
+ text: 'text-white'
+ },
+ success: {
+ bg: 'rgba(16, 185, 129, 0.1)',
+ border: 'rgba(16, 185, 129, 0.2)',
+ icon: 'rgb(52, 211, 153)',
+ text: 'text-white'
}
};
- const colors = variantColors[variant];
+ const colors = configs[variant];
return (
-
+
- {title}
+ {title}
{description}
diff --git a/apps/website/ui/Input.tsx b/apps/website/ui/Input.tsx
index 74d6e549e..30c088ca9 100644
--- a/apps/website/ui/Input.tsx
+++ b/apps/website/ui/Input.tsx
@@ -1,26 +1,30 @@
-import React, { forwardRef, InputHTMLAttributes } from 'react';
-import { Text } from './Text';
+import React, { forwardRef, ReactNode } from 'react';
import { Box } from './primitives/Box';
import { Stack } from './primitives/Stack';
+import { Text } from './Text';
-interface InputProps extends InputHTMLAttributes {
- variant?: 'default' | 'error';
+interface InputProps extends React.InputHTMLAttributes {
+ label?: string;
+ icon?: ReactNode;
errorMessage?: string;
- icon?: React.ReactNode;
- label?: React.ReactNode;
+ variant?: 'default' | 'error';
}
export const Input = forwardRef(
- ({ className = '', variant = 'default', errorMessage, icon, label, ...props }, ref) => {
- const baseClasses = 'px-3 py-2 border rounded-sm text-white bg-graphite-black focus:outline-none focus:border-primary-accent transition-all duration-150 ease-smooth w-full text-sm placeholder:text-gray-600';
- const variantClasses = (variant === 'error' || errorMessage) ? 'border-critical-red' : 'border-border-gray';
- const iconClasses = icon ? 'pl-10' : '';
- const classes = `${baseClasses} ${variantClasses} ${iconClasses} ${className}`;
+ ({ label, icon, errorMessage, variant = 'default', className = '', ...props }, ref) => {
+ const isError = variant === 'error' || !!errorMessage;
+
+ const baseClasses = 'w-full px-4 py-2 bg-deep-graphite border rounded-lg text-white placeholder:text-gray-500 focus:outline-none transition-all duration-150 sm:text-sm';
+ const variantClasses = isError
+ ? 'border-warning-amber focus:border-warning-amber focus:ring-1 focus:ring-warning-amber'
+ : 'border-charcoal-outline focus:border-primary-blue focus:ring-1 focus:ring-primary-blue';
+
+ const classes = `${baseClasses} ${variantClasses} ${icon ? 'pl-11' : ''} ${className}`;
return (
{label && (
-
+
{label}
)}
@@ -28,20 +32,21 @@ export const Input = forwardRef(
{icon && (
{icon}
)}
{errorMessage && (
-
+
{errorMessage}
)}
diff --git a/apps/website/ui/Link.tsx b/apps/website/ui/Link.tsx
index eb546b496..b3c5814e9 100644
--- a/apps/website/ui/Link.tsx
+++ b/apps/website/ui/Link.tsx
@@ -1,19 +1,19 @@
import React, { ReactNode } from 'react';
import { Box, BoxProps } from './primitives/Box';
-interface LinkProps extends Omit, 'children' | 'className' | 'onClick'> {
+export interface LinkProps extends Omit, 'children' | 'onClick'> {
href: string;
children: ReactNode;
- className?: string;
variant?: 'primary' | 'secondary' | 'ghost';
size?: 'xs' | 'sm' | 'md' | 'lg';
target?: '_blank' | '_self' | '_parent' | '_top';
rel?: string;
onClick?: React.MouseEventHandler;
- style?: React.CSSProperties;
block?: boolean;
- weight?: 'light' | 'normal' | 'medium' | 'semibold' | 'bold';
+ weight?: 'light' | 'normal' | 'medium' | 'semibold' | 'bold' | string;
truncate?: boolean;
+ hoverColor?: string;
+ transition?: boolean;
}
export function Link({
@@ -25,10 +25,11 @@ export function Link({
target = '_self',
rel = '',
onClick,
- style,
block = false,
weight,
truncate,
+ hoverColor,
+ transition,
...props
}: LinkProps) {
const baseClasses = 'inline-flex items-center transition-colors';
@@ -46,7 +47,7 @@ export function Link({
lg: 'text-lg'
};
- const weightClasses = {
+ const weightClasses: Record = {
light: 'font-light',
normal: 'font-normal',
medium: 'font-medium',
@@ -58,8 +59,10 @@ export function Link({
block ? 'flex' : baseClasses,
variantClasses[variant],
sizeClasses[size],
- weight ? weightClasses[weight] : '',
+ weight && weightClasses[weight] ? weightClasses[weight] : '',
truncate ? 'truncate' : '',
+ hoverColor ? `hover:${hoverColor}` : '',
+ transition ? 'transition-all duration-150' : '',
className
].filter(Boolean).join(' ');
@@ -71,7 +74,10 @@ export function Link({
target={target}
rel={rel}
onClick={onClick}
- style={style}
+ style={{
+ ...(weight && !weightClasses[weight] ? { fontWeight: weight } : {}),
+ ...(props.style || {})
+ }}
{...props}
>
{children}
diff --git a/apps/website/ui/LoadingSpinner.tsx b/apps/website/ui/LoadingSpinner.tsx
index 4542ea786..f90541c96 100644
--- a/apps/website/ui/LoadingSpinner.tsx
+++ b/apps/website/ui/LoadingSpinner.tsx
@@ -1,4 +1,5 @@
import React from 'react';
+import { Box } from './primitives/Box';
interface LoadingSpinnerProps {
size?: number;
@@ -7,19 +8,17 @@ interface LoadingSpinnerProps {
}
export function LoadingSpinner({ size = 8, color = '#3b82f6', className = '' }: LoadingSpinnerProps) {
- const style: React.CSSProperties = {
- width: `${size * 0.25}rem`,
- height: `${size * 0.25}rem`,
- border: '2px solid transparent',
- borderTopColor: color,
- borderLeftColor: color,
- borderRadius: '9999px',
- };
-
return (
-
diff --git a/apps/website/ui/MediaPreviewCard.tsx b/apps/website/ui/MediaPreviewCard.tsx
index d1a6fb0a2..488aa618c 100644
--- a/apps/website/ui/MediaPreviewCard.tsx
+++ b/apps/website/ui/MediaPreviewCard.tsx
@@ -1,93 +1,100 @@
import React from 'react';
import { Box } from './primitives/Box';
-import { Text } from './Text';
-import { ImagePlaceholder } from './ImagePlaceholder';
import { Image } from './Image';
+import { Surface } from './primitives/Surface';
+import { Text } from './Text';
+import { Play, Image as ImageIcon } from 'lucide-react';
+import { Icon } from './Icon';
-export interface MediaPreviewCardProps {
- src?: string;
- alt?: string;
+interface MediaPreviewCardProps {
+ type: 'image' | 'video';
+ src: string;
+ alt: string;
title?: string;
subtitle?: string;
+ onClick?: () => void;
aspectRatio?: string;
isLoading?: boolean;
- error?: string;
- onClick?: () => void;
className?: string;
- actions?: React.ReactNode;
}
export function MediaPreviewCard({
+ type,
src,
- alt = 'Media preview',
+ alt,
title,
- subtitle,
- aspectRatio = '16/9',
- isLoading,
- error,
onClick,
+ aspectRatio = '16/9',
+ isLoading = false,
className = '',
- actions,
}: MediaPreviewCardProps) {
return (
-
-
+
{isLoading ? (
-
- ) : error ? (
-
- ) : src ? (
+
+ ) : (
- ) : (
-
)}
-
- {actions && (
+
+ {/* Overlay */}
+
+
+
+
+
+
+ {title && (
- {actions}
+
+ {title}
+
)}
-
- {(title || subtitle) && (
-
- {title && (
-
- {title}
-
- )}
- {subtitle && (
-
- {subtitle}
-
- )}
-
- )}
-
+
);
}
diff --git a/apps/website/ui/Modal.tsx b/apps/website/ui/Modal.tsx
index 12cb2c91a..c2fae7030 100644
--- a/apps/website/ui/Modal.tsx
+++ b/apps/website/ui/Modal.tsx
@@ -1,138 +1,144 @@
-
-
-import React, {
- type KeyboardEvent as ReactKeyboardEvent,
- type ReactNode,
-} from 'react';
+import React, { ReactNode } from 'react';
import { Box } from './primitives/Box';
-import { Button } from './Button';
-import { Heading } from './Heading';
import { Stack } from './primitives/Stack';
+import { Button } from './Button';
import { Text } from './Text';
+import { X } from 'lucide-react';
+import { IconButton } from './IconButton';
interface ModalProps {
- title: string;
- description?: string;
- icon?: ReactNode;
- children?: ReactNode;
- primaryActionLabel?: string;
- secondaryActionLabel?: string;
- onPrimaryAction?: () => void | Promise;
- onSecondaryAction?: () => void;
- onOpenChange?: (open: boolean) => void;
isOpen: boolean;
+ onClose?: () => void;
+ onOpenChange?: (open: boolean) => void;
+ title?: string;
+ description?: string;
+ icon?: React.ReactNode;
+ children: ReactNode;
footer?: ReactNode;
+ primaryActionLabel?: string;
+ onPrimaryAction?: () => void;
+ secondaryActionLabel?: string;
+ onSecondaryAction?: () => void;
+ isLoading?: boolean;
+ size?: 'sm' | 'md' | 'lg' | 'xl';
}
export function Modal({
+ isOpen,
+ onClose,
+ onOpenChange,
title,
description,
icon,
children,
- primaryActionLabel,
- secondaryActionLabel,
- onPrimaryAction,
- onSecondaryAction,
- onOpenChange,
- isOpen,
footer,
+ primaryActionLabel,
+ onPrimaryAction,
+ secondaryActionLabel,
+ onSecondaryAction,
+ isLoading = false,
+ size = 'md',
}: ModalProps) {
- const handleKeyDown = (event: ReactKeyboardEvent) => {
- if (event.key === 'Escape') {
- if (onOpenChange) {
- onOpenChange(false);
- }
- return;
- }
+ if (!isOpen) return null;
+
+ const sizeMap = {
+ sm: 'max-w-md',
+ md: 'max-w-lg',
+ lg: 'max-w-2xl',
+ xl: 'max-w-4xl',
};
- const handleBackdropClick = (event: React.MouseEvent) => {
- if (event.target === event.currentTarget && onOpenChange) {
- onOpenChange(false);
- }
+ const handleClose = () => {
+ if (onClose) onClose();
+ if (onOpenChange) onOpenChange(false);
};
- if (!isOpen) {
- return null;
- }
-
return (
+ {/* Backdrop click to close */}
+
+
-
-
- {icon && {icon}}
-
- {title}
- {description && (
-
- {description}
-
- )}
-
+ {/* Header */}
+
+
+
+ {icon && {icon}}
+
+ {title && (
+
+ {title}
+
+ )}
+ {description && (
+
+ {description}
+
+ )}
+
+
+
-
+ {/* Content */}
+
{children}
+ {/* Footer */}
{(primaryActionLabel || secondaryActionLabel || footer) && (
-
- {(primaryActionLabel || secondaryActionLabel) && (
-
+
+ {footer || (
+
{secondaryActionLabel && (
)}
{primaryActionLabel && (
)}
-
- )}
- {footer && (
-
- {footer}
-
+
)}
)}
diff --git a/apps/website/ui/OnboardingError.tsx b/apps/website/ui/OnboardingError.tsx
index 613d56da8..470ce6a5c 100644
--- a/apps/website/ui/OnboardingError.tsx
+++ b/apps/website/ui/OnboardingError.tsx
@@ -1,12 +1,26 @@
+import React from 'react';
+import { Box } from './primitives/Box';
+import { Text } from './Text';
+
interface OnboardingErrorProps {
message: string;
}
export function OnboardingError({ message }: OnboardingErrorProps) {
return (
-
+
+ âš
+ {message}
+
);
-}
\ No newline at end of file
+}
diff --git a/apps/website/ui/OnboardingForm.tsx b/apps/website/ui/OnboardingForm.tsx
index 20bc2890e..2f0429aff 100644
--- a/apps/website/ui/OnboardingForm.tsx
+++ b/apps/website/ui/OnboardingForm.tsx
@@ -1,12 +1,15 @@
+import React, { ReactNode, FormEvent } from 'react';
+import { Box } from './primitives/Box';
+
interface OnboardingFormProps {
- children: React.ReactNode;
- onSubmit: (e: React.FormEvent) => void | Promise;
+ children: ReactNode;
+ onSubmit: (e: FormEvent) => void;
}
export function OnboardingForm({ children, onSubmit }: OnboardingFormProps) {
return (
-
+
);
-}
\ No newline at end of file
+}
diff --git a/apps/website/ui/OnboardingStepPanel.tsx b/apps/website/ui/OnboardingStepPanel.tsx
index b3e299ebd..13972f861 100644
--- a/apps/website/ui/OnboardingStepPanel.tsx
+++ b/apps/website/ui/OnboardingStepPanel.tsx
@@ -1,18 +1,20 @@
-import { Surface } from '@/ui/primitives/Surface';
+import React, { ReactNode } from 'react';
+import { Surface } from './primitives/Surface';
interface OnboardingStepPanelProps {
- children: React.ReactNode;
+ children: ReactNode;
className?: string;
}
export function OnboardingStepPanel({ children, className = '' }: OnboardingStepPanelProps) {
return (
{children}
diff --git a/apps/website/ui/ProfileStatGrid.tsx b/apps/website/ui/ProfileStatGrid.tsx
index 58d882745..d47ef1ebb 100644
--- a/apps/website/ui/ProfileStatGrid.tsx
+++ b/apps/website/ui/ProfileStatGrid.tsx
@@ -1,8 +1,7 @@
-
-
-import { Box } from '@/ui/primitives/Box';
-import { Grid } from '@/ui/primitives/Grid';
-import { Text } from '@/ui/Text';
+import React from 'react';
+import { Box } from './primitives/Box';
+import { Text } from './Text';
+import { Grid } from './primitives/Grid';
interface Stat {
label: string;
@@ -16,11 +15,19 @@ interface ProfileStatGridProps {
export function ProfileStatGrid({ stats }: ProfileStatGridProps) {
return (
-
+
{stats.map((stat, idx) => (
-
+
{stat.value}
- {stat.label}
+ {stat.label}
))}
diff --git a/apps/website/ui/SectionHeader.tsx b/apps/website/ui/SectionHeader.tsx
index 124b333ce..8c8ca2552 100644
--- a/apps/website/ui/SectionHeader.tsx
+++ b/apps/website/ui/SectionHeader.tsx
@@ -1,46 +1,50 @@
-
-
-import { LucideIcon } from 'lucide-react';
-import React from 'react';
+import React, { ReactNode } from 'react';
import { Box } from './primitives/Box';
-import { Heading } from './Heading';
-import { Icon } from './Icon';
import { Stack } from './primitives/Stack';
-import { Surface } from './primitives/Surface';
import { Text } from './Text';
+import { Surface } from './primitives/Surface';
+import { Icon } from './Icon';
+import { LucideIcon } from 'lucide-react';
interface SectionHeaderProps {
- icon: LucideIcon;
title: string;
description?: string;
- action?: React.ReactNode;
+ icon?: LucideIcon;
color?: string;
+ actions?: ReactNode;
}
-export function SectionHeader({
- icon,
- title,
- description,
- action,
- color = '#3b82f6'
-}: SectionHeaderProps) {
+export function SectionHeader({ title, description, icon, color = 'text-primary-blue', actions }: SectionHeaderProps) {
return (
-
+
-
-
-
+
+ {icon && (
+
-
- {title}
- {description && (
- {description}
- )}
-
-
-
- {action && {action}}
+ )}
+
+
+ {title}
+
+ {description && (
+
+ {description}
+
+ )}
+
+
+ {actions && (
+
+ {actions}
+
+ )}
);
diff --git a/apps/website/ui/SegmentedControl.tsx b/apps/website/ui/SegmentedControl.tsx
index 51c92a299..5c4794671 100644
--- a/apps/website/ui/SegmentedControl.tsx
+++ b/apps/website/ui/SegmentedControl.tsx
@@ -1,5 +1,3 @@
-
-
import { Box } from './primitives/Box';
import { Stack } from './primitives/Stack';
import { Text } from './Text';
@@ -29,7 +27,16 @@ export function SegmentedControl({
};
return (
-
+
{options.map((option) => {
const isSelected = option.value === value;
@@ -41,24 +48,28 @@ export function SegmentedControl({
onClick={() => handleSelect(option.value, option.disabled)}
aria-pressed={isSelected}
disabled={option.disabled}
- style={{
- flex: 1,
- minWidth: '140px',
- padding: '0.375rem 0.75rem',
- borderRadius: '9999px',
- transition: 'all 0.2s',
- textAlign: 'left',
- backgroundColor: isSelected ? '#3b82f6' : 'transparent',
- color: isSelected ? 'white' : '#d1d5db',
- opacity: option.disabled ? 0.5 : 1,
- cursor: option.disabled ? 'not-allowed' : 'pointer',
- border: 'none'
- }}
+ flex={1}
+ minWidth="140px"
+ px={3}
+ py={1.5}
+ rounded="full"
+ transition="all 0.2s"
+ textAlign="left"
+ bg={isSelected ? 'bg-primary-blue' : 'transparent'}
+ color={isSelected ? 'text-white' : 'text-gray-400'}
+ opacity={option.disabled ? 0.5 : 1}
+ cursor={option.disabled ? 'not-allowed' : 'pointer'}
+ border="none"
>
{option.label}
{option.description && (
-
+
{option.description}
)}
@@ -66,6 +77,6 @@ export function SegmentedControl({
);
})}
-
+
);
}
diff --git a/apps/website/ui/Select.tsx b/apps/website/ui/Select.tsx
index 57d222e8f..3026f54c5 100644
--- a/apps/website/ui/Select.tsx
+++ b/apps/website/ui/Select.tsx
@@ -1,4 +1,5 @@
-import React, { ChangeEvent, SelectHTMLAttributes } from 'react';
+import React, { forwardRef, ReactNode } from 'react';
+import { Box } from './primitives/Box';
import { Stack } from './primitives/Stack';
import { Text } from './Text';
@@ -7,64 +8,57 @@ interface SelectOption {
label: string;
}
-interface SelectProps extends SelectHTMLAttributes {
- id?: string;
- 'aria-label'?: string;
- value?: string;
- onChange?: (e: ChangeEvent) => void;
- options: SelectOption[];
- className?: string;
- style?: React.CSSProperties;
+interface SelectProps extends React.SelectHTMLAttributes {
label?: string;
fullWidth?: boolean;
pl?: number;
+ errorMessage?: string;
+ variant?: 'default' | 'error';
+ options?: SelectOption[];
}
-export function Select({
- id,
- 'aria-label': ariaLabel,
- value,
- onChange,
- options,
- className = '',
- style,
- label,
- fullWidth = true,
- pl,
- ...props
-}: SelectProps) {
- const spacingMap: Record = {
- 10: 'pl-10'
- };
- const defaultClasses = `${fullWidth ? 'w-full' : 'w-auto'} px-3 py-2 bg-deep-graphite border border-charcoal-outline rounded-lg text-white focus:outline-none focus:border-primary-blue transition-colors`;
- const classes = [
- defaultClasses,
- pl !== undefined ? spacingMap[pl] : '',
- className
- ].filter(Boolean).join(' ');
+export const Select = forwardRef(
+ ({ label, fullWidth = true, pl, errorMessage, variant = 'default', options, children, className = '', style, ...props }, ref) => {
+ const isError = variant === 'error' || !!errorMessage;
+
+ const variantClasses = isError
+ ? 'border-warning-amber focus:border-warning-amber'
+ : 'border-charcoal-outline focus:border-primary-blue';
+
+ const defaultClasses = `${fullWidth ? 'w-full' : 'w-auto'} px-3 py-2 bg-deep-graphite border rounded-lg text-white focus:outline-none transition-colors`;
+ const classes = [
+ defaultClasses,
+ variantClasses,
+ pl ? `pl-${pl}` : '',
+ className
+ ].filter(Boolean).join(' ');
- return (
-
- {label && (
-
- {label}
-
- )}
-
-
- );
-}
+ return (
+
+ {label && (
+
+ {label}
+
+ )}
+
+ {options ? options.map(opt => (
+
+ )) : children}
+
+ {errorMessage && (
+
+ {errorMessage}
+
+ )}
+
+ );
+ }
+);
+
+Select.displayName = 'Select';
diff --git a/apps/website/ui/SimpleCheckbox.tsx b/apps/website/ui/SimpleCheckbox.tsx
index 7e04690c9..75eda2cd2 100644
--- a/apps/website/ui/SimpleCheckbox.tsx
+++ b/apps/website/ui/SimpleCheckbox.tsx
@@ -28,7 +28,8 @@ export function SimpleCheckbox({ checked, onChange, disabled, 'aria-label': aria
borderColor="border-charcoal-outline"
rounded="sm"
aria-label={ariaLabel}
- className="text-primary-blue focus:ring-primary-blue"
+ ring="primary-blue"
+ color="text-primary-blue"
/>
);
}
diff --git a/apps/website/ui/Skeleton.tsx b/apps/website/ui/Skeleton.tsx
index 97c145f17..c4a449a96 100644
--- a/apps/website/ui/Skeleton.tsx
+++ b/apps/website/ui/Skeleton.tsx
@@ -1,4 +1,5 @@
import React from 'react';
+import { Box } from './primitives/Box';
interface SkeletonProps {
width?: string | number;
@@ -8,17 +9,13 @@ interface SkeletonProps {
}
export function Skeleton({ width, height, circle, className = '' }: SkeletonProps) {
- const style: React.CSSProperties = {
- width: width,
- height: height,
- borderRadius: circle ? '9999px' : '0.375rem',
- backgroundColor: 'rgba(38, 38, 38, 0.4)',
- };
-
return (
-
diff --git a/apps/website/ui/StatCard.tsx b/apps/website/ui/StatCard.tsx
index 48b2d2de0..266f55473 100644
--- a/apps/website/ui/StatCard.tsx
+++ b/apps/website/ui/StatCard.tsx
@@ -1,115 +1,115 @@
-
-
-import { motion, useReducedMotion } from 'framer-motion';
-import { ArrowDownRight, ArrowUpRight, LucideIcon } from 'lucide-react';
+import React, { ReactNode } from 'react';
import { Box } from './primitives/Box';
-import { Card } from './Card';
-import { Icon } from './Icon';
import { Stack } from './primitives/Stack';
import { Text } from './Text';
+import { Card } from './Card';
+import { Icon } from './Icon';
+import { LucideIcon } from 'lucide-react';
interface StatCardProps {
label: string;
value: string | number;
- subValue?: string;
icon?: LucideIcon;
- variant?: 'blue' | 'purple' | 'green' | 'orange';
- className?: string;
trend?: {
value: number;
isPositive: boolean;
};
+ variant?: 'default' | 'primary' | 'success' | 'warning' | 'danger' | 'info';
+ className?: string;
+ onClick?: () => void;
prefix?: string;
suffix?: string;
delay?: number;
}
-export function StatCard({
- label,
- value,
- subValue,
+export function StatCard({
+ label,
+ value,
icon,
- variant = 'blue',
- className = '',
trend,
- prefix = '',
- suffix = '',
- delay = 0,
+ variant = 'default',
+ className = '',
+ onClick,
+ prefix,
+ suffix,
+ delay,
}: StatCardProps) {
- const shouldReduceMotion = useReducedMotion();
-
const variantClasses = {
- blue: 'bg-gradient-to-br from-blue-900/20 to-blue-700/10 border-blue-500/30',
- purple: 'bg-gradient-to-br from-purple-900/20 to-purple-700/10 border-purple-500/30',
- green: 'bg-gradient-to-br from-green-900/20 to-green-700/10 border-green-500/30',
- orange: 'bg-gradient-to-br from-orange-900/20 to-orange-700/10 border-orange-500/30'
+ default: 'bg-panel-gray border-border-gray',
+ primary: 'bg-primary-accent/5 border-primary-accent/20',
+ success: 'bg-success-green/5 border-success-green/20',
+ warning: 'bg-warning-amber/5 border-warning-amber/20',
+ danger: 'bg-critical-red/5 border-critical-red/20',
+ info: 'bg-telemetry-aqua/5 border-telemetry-aqua/20',
};
-
+
+ const iconBgClasses = {
+ default: 'bg-white/5',
+ primary: 'bg-primary-accent/10',
+ success: 'bg-success-green/10',
+ warning: 'bg-warning-amber/10',
+ danger: 'bg-critical-red/10',
+ info: 'bg-telemetry-aqua/10',
+ };
+
const iconColorClasses = {
- blue: 'text-primary-blue',
- purple: 'text-purple-400',
- green: 'text-performance-green',
- orange: 'text-warning-amber'
+ default: 'text-gray-400',
+ primary: 'text-primary-accent',
+ success: 'text-success-green',
+ warning: 'text-warning-amber',
+ danger: 'text-critical-red',
+ info: 'text-telemetry-aqua',
};
-
+
const cardContent = (
-
+
-
+
+
+ {label}
+
{icon && (
-
+
)}
+
+
+
+
+ {prefix}{value}{suffix}
+
{trend && (
-
-
- {Math.abs(trend.value)}%
+
+
+ {trend.isPositive ? '+' : ''}{trend.value}%
+
+
+ vs last period
+
)}
-
-
- {prefix}{typeof value === 'number' ? value.toLocaleString() : value}{suffix}
-
- {label}
- {subValue && (
-
- {subValue}
-
- )}
-
);
- if (shouldReduceMotion) {
- return {cardContent};
+ if (onClick) {
+ return (
+
+ {cardContent}
+
+ );
}
- return (
-
- {cardContent}
-
- );
+ return cardContent;
}
diff --git a/apps/website/ui/StatGridItem.tsx b/apps/website/ui/StatGridItem.tsx
index e6583aa03..9a86a91b9 100644
--- a/apps/website/ui/StatGridItem.tsx
+++ b/apps/website/ui/StatGridItem.tsx
@@ -1,37 +1,36 @@
-import React from 'react';
-import { LucideIcon } from 'lucide-react';
+import React, { ReactNode } from 'react';
import { Box } from './primitives/Box';
+import { Stack } from './primitives/Stack';
import { Text } from './Text';
import { Icon } from './Icon';
-import { Stack } from './primitives/Stack';
+import { LucideIcon } from 'lucide-react';
interface StatGridItemProps {
label: string;
value: string | number;
- color?: string;
icon?: LucideIcon;
+ color?: string;
}
-export function StatGridItem({ label, value, color = 'text-white', icon }: StatGridItemProps) {
+/**
+ * StatGridItem
+ *
+ * A simple stat display for use in a grid.
+ */
+export function StatGridItem({ label, value, icon, color = 'text-primary-blue' }: StatGridItemProps) {
return (
-
+
{icon && (
-
+
- {label}
)}
- {!icon && (
- {label}
- )}
- {value}
+
+ {value}
+
+
+ {label}
+
);
}
diff --git a/apps/website/ui/SummaryItem.tsx b/apps/website/ui/SummaryItem.tsx
index 21a2dd23d..605f06dc7 100644
--- a/apps/website/ui/SummaryItem.tsx
+++ b/apps/website/ui/SummaryItem.tsx
@@ -1,43 +1,48 @@
-
-
-import { ReactNode } from 'react';
+import React from 'react';
import { Box } from './primitives/Box';
-import { Surface } from './primitives/Surface';
import { Text } from './Text';
+import { Surface } from './primitives/Surface';
+import { Icon } from './Icon';
+import { LucideIcon } from 'lucide-react';
interface SummaryItemProps {
- title: string;
- subtitle?: string;
- rightContent?: ReactNode;
+ label?: string;
+ value?: string | number;
+ icon?: LucideIcon;
onClick?: () => void;
+ title?: string;
+ subtitle?: string;
+ rightContent?: React.ReactNode;
}
-export function SummaryItem({
- title,
- subtitle,
- rightContent,
- onClick,
-}: SummaryItemProps) {
+export function SummaryItem({ label, value, icon, onClick, title, subtitle, rightContent }: SummaryItemProps) {
return (
-
-
- {title}
-
- {subtitle && (
-
- {subtitle}
+ {icon && (
+
+
+
+ )}
+
+ {(label || title) && (
+
+ {label || title}
+
+ )}
+ {(value || subtitle) && (
+
+ {value || subtitle}
)}
diff --git a/apps/website/ui/TabNavigation.tsx b/apps/website/ui/TabNavigation.tsx
index c1b1cbe84..f0a7ffb8f 100644
--- a/apps/website/ui/TabNavigation.tsx
+++ b/apps/website/ui/TabNavigation.tsx
@@ -1,13 +1,13 @@
import React from 'react';
import { Box } from './primitives/Box';
+import { Stack } from './primitives/Stack';
+import { Surface } from './primitives/Surface';
import { Text } from './Text';
-import { Icon } from './Icon';
-import { LucideIcon } from 'lucide-react';
interface Tab {
id: string;
label: string;
- icon?: LucideIcon;
+ icon?: React.ReactNode;
}
interface TabNavigationProps {
@@ -19,51 +19,50 @@ interface TabNavigationProps {
export function TabNavigation({ tabs, activeTab, onTabChange, className = '' }: TabNavigationProps) {
return (
-
- {tabs.map((tab) => {
- const isActive = activeTab === tab.id;
- return (
- onTabChange(tab.id)}
- display="flex"
- alignItems="center"
- gap={2}
- px={5}
- py={2.5}
- rounded="lg"
- cursor="pointer"
- transition
- bg={isActive ? 'bg-primary-blue' : ''}
- className={`select-none ${isActive ? 'shadow-lg shadow-primary-blue/25' : 'hover:bg-iron-gray/80'}`}
- >
- {tab.icon && }
-
+ {tabs.map((tab) => {
+ const isActive = activeTab === tab.id;
+ return (
+ onTabChange(tab.id)}
+ variant={isActive ? 'default' : 'ghost'}
+ bg={isActive ? 'bg-primary-blue' : ''}
+ rounded="lg"
+ px={4}
+ py={2}
+ transition="all 0.2s"
+ group
+ className={`select-none ${isActive ? 'shadow-lg shadow-primary-blue/25' : 'hover:bg-iron-gray/80'}`}
>
- {tab.label}
-
-
- );
- })}
-
+
+ {tab.icon && (
+
+ {tab.icon}
+
+ )}
+
+ {tab.label}
+
+
+
+ );
+ })}
+
+
);
}
diff --git a/apps/website/ui/Table.tsx b/apps/website/ui/Table.tsx
index 6a8a8b9c4..c465fd51c 100644
--- a/apps/website/ui/Table.tsx
+++ b/apps/website/ui/Table.tsx
@@ -1,58 +1,59 @@
-import React, { ReactNode, HTMLAttributes } from 'react';
+import React, { ReactNode, ElementType } from 'react';
import { Box, BoxProps } from './primitives/Box';
-interface TableProps extends HTMLAttributes {
+interface TableProps extends BoxProps<'table'> {
children: ReactNode;
- className?: string;
}
export function Table({ children, className = '', ...props }: TableProps) {
+ const { border, translate, ...rest } = props;
return (
-
-
+
+
);
}
-interface TableHeadProps extends HTMLAttributes {
+interface TableHeaderProps extends BoxProps<'thead'> {
children: ReactNode;
}
-export function TableHead({ children, ...props }: TableHeadProps) {
+export function TableHeader({ children, className = '', ...props }: TableHeaderProps) {
return (
-
+
{children}
-
+
);
}
-interface TableBodyProps extends HTMLAttributes {
+export const TableHead = TableHeader;
+
+interface TableBodyProps extends BoxProps<'tbody'> {
children: ReactNode;
}
-export function TableBody({ children, ...props }: TableBodyProps) {
+export function TableBody({ children, className = '', ...props }: TableBodyProps) {
return (
-
+
{children}
-
+
);
}
interface TableRowProps extends BoxProps<'tr'> {
children: ReactNode;
+ hoverBg?: string;
clickable?: boolean;
- variant?: 'default' | 'highlight';
+ variant?: string;
}
-export function TableRow({ children, className = '', clickable = false, variant = 'default', ...props }: TableRowProps) {
- const baseClasses = 'transition-colors duration-150 ease-smooth';
- const variantClasses = variant === 'highlight' ? 'bg-primary-accent/5' : 'hover:bg-white/[0.02]';
+export function TableRow({ children, className = '', hoverBg, clickable, variant, ...props }: TableRowProps) {
const classes = [
- baseClasses,
- variantClasses,
- clickable ? 'cursor-pointer' : '',
+ 'transition-colors',
+ clickable || props.onClick ? 'cursor-pointer' : '',
+ hoverBg ? `hover:${hoverBg}` : (clickable || props.onClick ? 'hover:bg-white/5' : ''),
className
].filter(Boolean).join(' ');
@@ -63,13 +64,15 @@ export function TableRow({ children, className = '', clickable = false, variant
);
}
-interface TableHeaderProps extends BoxProps<'th'> {
+interface TableCellProps extends BoxProps<'td'> {
children: ReactNode;
}
-export function TableHeader({ children, className = '', ...props }: TableHeaderProps) {
- const baseClasses = 'py-2.5 px-4 text-[11px] font-bold text-gray-500 uppercase tracking-wider';
- const classes = [baseClasses, className].filter(Boolean).join(' ');
+export function TableHeaderCell({ children, className = '', ...props }: TableCellProps) {
+ const classes = [
+ 'px-4 py-3 text-xs font-bold text-gray-400 uppercase tracking-wider',
+ className
+ ].filter(Boolean).join(' ');
return (
@@ -78,13 +81,11 @@ export function TableHeader({ children, className = '', ...props }: TableHeaderP
);
}
-interface TableCellProps extends BoxProps<'td'> {
- children: ReactNode;
-}
-
export function TableCell({ children, className = '', ...props }: TableCellProps) {
- const baseClasses = 'py-3 px-4 text-sm text-gray-300';
- const classes = [baseClasses, className].filter(Boolean).join(' ');
+ const classes = [
+ 'px-4 py-4 text-sm text-gray-300',
+ className
+ ].filter(Boolean).join(' ');
return (
diff --git a/apps/website/ui/Text.tsx b/apps/website/ui/Text.tsx
index 2d600da97..407486d15 100644
--- a/apps/website/ui/Text.tsx
+++ b/apps/website/ui/Text.tsx
@@ -25,14 +25,14 @@ interface ResponsiveTextAlign {
'2xl'?: TextAlign;
}
-interface TextProps extends Omit, 'children' | 'className'> {
+interface TextProps extends Omit, 'children' | 'className' | 'size'> {
as?: T;
children: ReactNode;
className?: string;
size?: TextSize | ResponsiveTextSize;
- weight?: 'light' | 'normal' | 'medium' | 'semibold' | 'bold';
+ weight?: 'light' | 'normal' | 'medium' | 'semibold' | 'bold' | string;
color?: string;
- font?: 'mono' | 'sans';
+ font?: 'mono' | 'sans' | string;
align?: TextAlign | ResponsiveTextAlign;
truncate?: boolean;
uppercase?: boolean;
@@ -43,6 +43,7 @@ interface TextProps extends Omit, 'c
style?: React.CSSProperties;
block?: boolean;
italic?: boolean;
+ lineClamp?: number;
ml?: Spacing | ResponsiveSpacing;
mr?: Spacing | ResponsiveSpacing;
mt?: Spacing | ResponsiveSpacing;
@@ -76,6 +77,7 @@ export function Text({
style,
block = false,
italic = false,
+ lineClamp,
ml, mr, mt, mb,
...props
}: TextProps & ComponentPropsWithoutRef) {
@@ -115,7 +117,7 @@ export function Text({
bold: 'font-bold'
};
- const fontClasses = {
+ const fontClasses: Record = {
mono: 'font-mono',
sans: 'font-sans'
};
@@ -175,8 +177,8 @@ export function Text({
const classes = [
block ? 'block' : 'inline',
getSizeClasses(size),
- weightClasses[weight],
- fontClasses[font],
+ weightClasses[weight] || '',
+ fontClasses[font] || '',
getAlignClasses(align),
leading ? leadingClasses[leading] : '',
color,
@@ -184,6 +186,7 @@ export function Text({
uppercase ? 'uppercase' : '',
capitalize ? 'capitalize' : '',
italic ? 'italic' : '',
+ lineClamp ? `line-clamp-${lineClamp}` : '',
letterSpacing === '0.05em' ? 'tracking-wider' : letterSpacing ? `tracking-${letterSpacing}` : '',
getSpacingClass('ml', ml),
getSpacingClass('mr', mr),
@@ -194,6 +197,8 @@ export function Text({
const combinedStyle = {
...(fontSize ? { fontSize } : {}),
+ ...(weight && !weightClasses[weight] ? { fontWeight: weight } : {}),
+ ...(font && !fontClasses[font] ? { fontFamily: font } : {}),
...style
};
diff --git a/apps/website/ui/TextArea.tsx b/apps/website/ui/TextArea.tsx
index f5425bbba..6135e16f3 100644
--- a/apps/website/ui/TextArea.tsx
+++ b/apps/website/ui/TextArea.tsx
@@ -1,56 +1,49 @@
-
-
-import React, { TextareaHTMLAttributes } from 'react';
+import React, { forwardRef } from 'react';
import { Box } from './primitives/Box';
import { Stack } from './primitives/Stack';
import { Text } from './Text';
-interface TextAreaProps extends TextareaHTMLAttributes {
- label?: React.ReactNode;
+interface TextAreaProps extends React.TextareaHTMLAttributes {
+ label?: string;
errorMessage?: string;
variant?: 'default' | 'error';
fullWidth?: boolean;
}
-export function TextArea({
- label,
- errorMessage,
- variant = 'default',
- fullWidth = true,
- className = '',
- ...props
-}: TextAreaProps) {
- const isError = variant === 'error' || !!errorMessage;
+export const TextArea = forwardRef(
+ ({ label, errorMessage, variant = 'default', fullWidth = true, className = '', ...props }, ref) => {
+ const isError = variant === 'error' || !!errorMessage;
+
+ return (
+
+ {label && (
+
+ {label}
+
+ )}
+
+
+ {errorMessage && (
+
+ {errorMessage}
+
+ )}
+
+
+ );
+ }
+);
- return (
-
- {label && (
-
- {label}
-
- )}
-
-
-
- {errorMessage && (
-
- {errorMessage}
-
- )}
-
- );
-}
+TextArea.displayName = 'TextArea';
diff --git a/apps/website/ui/Toggle.tsx b/apps/website/ui/Toggle.tsx
index 962bd8ee1..7c1a133df 100644
--- a/apps/website/ui/Toggle.tsx
+++ b/apps/website/ui/Toggle.tsx
@@ -1,68 +1,74 @@
-import { motion } from 'framer-motion';
+import React from 'react';
import { Box } from './primitives/Box';
import { Text } from './Text';
+import { motion } from 'framer-motion';
interface ToggleProps {
- checked: boolean;
- onChange: (checked: boolean) => void;
label: string;
description?: string;
+ checked: boolean;
+ onChange: (checked: boolean) => void;
disabled?: boolean;
}
-export function Toggle({
- checked,
- onChange,
- label,
- description,
- disabled = false,
-}: ToggleProps) {
+export function Toggle({ label, description, checked, onChange, disabled }: ToggleProps) {
return (
-
+
+
);
}
diff --git a/apps/website/ui/primitives/Box.tsx b/apps/website/ui/primitives/Box.tsx
index 5a1701e47..f7aa51aba 100644
--- a/apps/website/ui/primitives/Box.tsx
+++ b/apps/website/ui/primitives/Box.tsx
@@ -16,8 +16,10 @@ type Spacing = 0 | 0.5 | 1 | 1.5 | 2 | 2.5 | 3 | 3.5 | 4 | 5 | 6 | 7 | 8 | 9 | 1
interface ResponsiveSpacing {
base?: Spacing;
+ sm?: Spacing;
md?: Spacing;
lg?: Spacing;
+ xl?: Spacing;
}
export type ResponsiveValue = {
@@ -49,38 +51,151 @@ export interface BoxProps {
px?: Spacing | ResponsiveSpacing;
py?: Spacing | ResponsiveSpacing;
// Sizing
- w?: string | ResponsiveValue;
- h?: string | ResponsiveValue;
- width?: string;
- height?: string;
+ w?: string | number | ResponsiveValue;
+ h?: string | number | ResponsiveValue;
+ width?: string | number;
+ height?: string | number;
maxWidth?: string | ResponsiveValue;
minWidth?: string | ResponsiveValue;
maxHeight?: string | ResponsiveValue;
minHeight?: string | ResponsiveValue;
+ fullWidth?: boolean;
+ fullHeight?: boolean;
+ aspectRatio?: string;
// Display
- display?: 'block' | 'inline-block' | 'flex' | 'inline-flex' | 'grid' | 'none' | ResponsiveValue<'block' | 'inline-block' | 'flex' | 'inline-flex' | 'grid' | 'none'>;
+ display?: 'block' | 'inline-block' | 'flex' | 'inline-flex' | 'grid' | 'none' | string | ResponsiveValue<'block' | 'inline-block' | 'flex' | 'inline-flex' | 'grid' | 'none' | string>;
+ center?: boolean;
+ overflow?: 'auto' | 'hidden' | 'visible' | 'scroll' | string;
+ overflowX?: 'auto' | 'hidden' | 'visible' | 'scroll';
+ overflowY?: 'auto' | 'hidden' | 'visible' | 'scroll';
+ textAlign?: 'left' | 'center' | 'right' | 'justify' | string;
+ visibility?: 'visible' | 'hidden' | 'collapse';
+ // Positioning
+ position?: 'static' | 'relative' | 'absolute' | 'fixed' | 'sticky';
+ top?: string | number | ResponsiveValue;
+ right?: string | number | ResponsiveValue;
+ bottom?: string | number | ResponsiveValue;
+ left?: string | number | ResponsiveValue;
+ inset?: string | number;
+ insetY?: string | number;
+ insetX?: string | number;
+ zIndex?: number;
// Basic Styling
- rounded?: 'none' | 'sm' | 'md' | 'lg' | 'xl' | '2xl' | 'full';
- border?: boolean;
+ rounded?: 'none' | 'sm' | 'md' | 'lg' | 'xl' | '2xl' | 'full' | string | boolean;
+ border?: boolean | string;
+ borderTop?: boolean | string;
+ borderBottom?: boolean | string;
+ borderLeft?: boolean | string;
+ borderRight?: boolean | string;
+ borderWidth?: string | number;
+ borderStyle?: 'solid' | 'dashed' | 'dotted' | 'none' | string;
borderColor?: string;
+ borderOpacity?: number;
bg?: string;
+ backgroundColor?: string;
+ backgroundImage?: string;
+ backgroundSize?: string;
+ backgroundPosition?: string;
+ bgOpacity?: number;
color?: string;
shadow?: string;
opacity?: number;
+ blur?: 'none' | 'sm' | 'md' | 'lg' | 'xl' | '2xl' | '3xl' | string;
+ pointerEvents?: 'auto' | 'none' | string;
// Flex/Grid Item props
flex?: number | string;
flexShrink?: number;
flexGrow?: number;
+ flexDirection?: 'row' | 'row-reverse' | 'col' | 'col-reverse' | string | ResponsiveValue;
+ flexWrap?: 'wrap' | 'nowrap' | 'wrap-reverse' | string;
+ alignItems?: 'start' | 'center' | 'end' | 'stretch' | 'baseline' | string | ResponsiveValue;
+ justifyContent?: 'start' | 'center' | 'end' | 'between' | 'around' | 'evenly' | string | ResponsiveValue;
alignSelf?: 'auto' | 'start' | 'end' | 'center' | 'stretch' | 'baseline';
+ gap?: number | string | ResponsiveValue;
+ gridCols?: number | ResponsiveValue;
+ responsiveGridCols?: number | ResponsiveValue;
+ colSpan?: number | ResponsiveValue;
+ responsiveColSpan?: number | ResponsiveValue;
order?: number | string | ResponsiveValue;
+ // Transform
+ transform?: string | boolean;
+ translate?: string;
+ translateX?: string;
+ translateY?: string;
+ // Animation (Framer Motion support)
+ initial?: any;
+ animate?: any;
+ exit?: any;
+ transition?: any;
+ variants?: any;
+ whileHover?: any;
+ whileTap?: any;
+ onHoverStart?: any;
+ onHoverEnd?: any;
+ whileInView?: any;
+ viewport?: any;
+ custom?: any;
+ // Interaction
+ group?: boolean;
+ groupHoverTextColor?: string;
+ groupHoverScale?: boolean;
+ groupHoverOpacity?: number;
+ groupHoverBorderColor?: string;
+ hoverBorderColor?: string;
+ hoverBg?: string;
+ hoverTextColor?: string;
+ hoverScale?: boolean | number;
+ clickable?: boolean;
// Events
- onMouseEnter?: React.MouseEventHandler;
- onMouseLeave?: React.MouseEventHandler;
- onClick?: React.MouseEventHandler;
+ onMouseEnter?: React.MouseEventHandler;
+ onMouseLeave?: React.MouseEventHandler;
+ onClick?: React.MouseEventHandler;
+ onMouseDown?: React.MouseEventHandler;
+ onMouseUp?: React.MouseEventHandler;
+ onMouseMove?: React.MouseEventHandler;
+ onKeyDown?: React.KeyboardEventHandler;
+ onBlur?: React.FocusEventHandler;
+ onSubmit?: React.FormEventHandler;
+ onScroll?: React.UIEventHandler;
style?: React.CSSProperties;
id?: string;
- role?: string;
+ role?: React.AriaRole;
tabIndex?: number;
+ // Other
+ type?: 'button' | 'submit' | 'reset' | string;
+ disabled?: boolean;
+ cursor?: string;
+ fontSize?: string | ResponsiveValue;
+ weight?: string;
+ fontWeight?: string | number;
+ letterSpacing?: string;
+ lineHeight?: string | number;
+ font?: string;
+ ring?: string;
+ hideScrollbar?: boolean;
+ truncate?: boolean;
+ src?: string;
+ alt?: string;
+ draggable?: boolean;
+ min?: string | number;
+ max?: string | number;
+ step?: string | number;
+ value?: string | number;
+ onChange?: React.ChangeEventHandler;
+ placeholder?: string;
+ title?: string;
+ padding?: Spacing | ResponsiveSpacing;
+ paddingLeft?: Spacing | ResponsiveSpacing;
+ paddingRight?: Spacing | ResponsiveSpacing;
+ paddingTop?: Spacing | ResponsiveSpacing;
+ paddingBottom?: Spacing | ResponsiveSpacing;
+ size?: string | number | ResponsiveValue;
+ accept?: string;
+ autoPlay?: boolean;
+ loop?: boolean;
+ muted?: boolean;
+ playsInline?: boolean;
+ objectFit?: 'fill' | 'contain' | 'cover' | 'none' | 'scale-down';
}
export const Box = forwardRef((
@@ -92,26 +207,126 @@ export const Box = forwardRef((
p, pt, pb, pl, pr, px, py,
w, h, width, height,
maxWidth, minWidth, maxHeight, minHeight,
+ fullWidth, fullHeight,
+ aspectRatio,
display,
+ center,
+ overflow, overflowX, overflowY,
+ textAlign,
+ visibility,
+ position,
+ top, right, bottom, left,
+ inset, insetY, insetX,
+ zIndex,
rounded,
border,
+ borderTop,
+ borderBottom,
+ borderLeft,
+ borderRight,
+ borderWidth,
+ borderStyle,
borderColor,
+ borderOpacity,
bg,
+ backgroundColor,
+ backgroundImage,
+ backgroundSize,
+ backgroundPosition,
+ bgOpacity,
color,
shadow,
opacity,
+ blur,
+ pointerEvents,
flex,
flexShrink,
flexGrow,
+ flexDirection,
+ flexWrap,
+ alignItems,
+ justifyContent,
alignSelf,
+ gap,
+ gridCols,
+ responsiveGridCols,
+ colSpan,
+ responsiveColSpan,
order,
+ transform,
+ translate,
+ translateX,
+ translateY,
+ initial,
+ animate,
+ exit,
+ transition,
+ variants,
+ whileHover,
+ whileTap,
+ onHoverStart,
+ onHoverEnd,
+ whileInView,
+ viewport,
+ custom,
+ group,
+ groupHoverTextColor,
+ groupHoverScale,
+ groupHoverOpacity,
+ groupHoverBorderColor,
+ hoverBorderColor,
+ hoverBg,
+ hoverTextColor,
+ hoverScale,
+ clickable,
onMouseEnter,
onMouseLeave,
onClick,
+ onMouseDown,
+ onMouseUp,
+ onMouseMove,
+ onKeyDown,
+ onBlur,
+ onSubmit,
+ onScroll,
style: styleProp,
id,
role,
tabIndex,
+ type,
+ disabled,
+ cursor,
+ fontSize,
+ weight,
+ fontWeight,
+ letterSpacing,
+ lineHeight,
+ font,
+ ring,
+ hideScrollbar,
+ truncate,
+ src,
+ alt,
+ draggable,
+ min,
+ max,
+ step,
+ value,
+ onChange,
+ placeholder,
+ title,
+ padding,
+ paddingLeft,
+ paddingRight,
+ paddingTop,
+ paddingBottom,
+ size,
+ accept,
+ autoPlay,
+ loop,
+ muted,
+ playsInline,
+ objectFit,
...props
}: BoxProps & ComponentPropsWithoutRef,
ref: ForwardedRef
@@ -131,14 +346,16 @@ export const Box = forwardRef((
if (typeof value === 'object') {
const classes = [];
if (value.base !== undefined) classes.push(`${prefix}-${spacingMap[value.base]}`);
+ if (value.sm !== undefined) classes.push(`sm:${prefix}-${spacingMap[value.sm]}`);
if (value.md !== undefined) classes.push(`md:${prefix}-${spacingMap[value.md]}`);
if (value.lg !== undefined) classes.push(`lg:${prefix}-${spacingMap[value.lg]}`);
+ if (value.xl !== undefined) classes.push(`xl:${prefix}-${spacingMap[value.xl]}`);
return classes.join(' ');
}
return `${prefix}-${spacingMap[value]}`;
};
- const getResponsiveClasses = (prefix: string, value: string | number | ResponsiveValue | undefined) => {
+ const getResponsiveClasses = (prefix: string, value: any | ResponsiveValue | undefined) => {
if (value === undefined) return '';
if (typeof value === 'object') {
const classes = [];
@@ -161,42 +378,111 @@ export const Box = forwardRef((
getSpacingClass('mr', mr),
getSpacingClass('mx', mx),
getSpacingClass('my', my),
- getSpacingClass('p', p),
- getSpacingClass('pt', pt),
- getSpacingClass('pb', pb),
- getSpacingClass('pl', pl),
- getSpacingClass('pr', pr),
+ getSpacingClass('p', p || padding),
+ getSpacingClass('pt', pt || paddingTop),
+ getSpacingClass('pb', pb || paddingBottom),
+ getSpacingClass('pl', pl || paddingLeft),
+ getSpacingClass('pr', pr || paddingRight),
getSpacingClass('px', px),
getSpacingClass('py', py),
- getResponsiveClasses('w', w),
- getResponsiveClasses('h', h),
+ fullWidth ? 'w-full' : getResponsiveClasses('w', w),
+ fullHeight ? 'h-full' : getResponsiveClasses('h', h),
getResponsiveClasses('max-w', maxWidth),
getResponsiveClasses('min-w', minWidth),
getResponsiveClasses('max-h', maxHeight),
getResponsiveClasses('min-h', minHeight),
getResponsiveClasses('', display),
- rounded ? `rounded-${rounded}` : '',
- border ? 'border' : '',
+ center ? 'flex items-center justify-center' : '',
+ overflow ? (overflow.includes(':') ? overflow : `overflow-${overflow}`) : '',
+ overflowX ? `overflow-x-${overflowX}` : '',
+ overflowY ? `overflow-y-${overflowY}` : '',
+ textAlign ? `text-${textAlign}` : '',
+ visibility ? visibility : '',
+ position ? position : '',
+ getResponsiveClasses('top', top),
+ getResponsiveClasses('right', right),
+ getResponsiveClasses('bottom', bottom),
+ getResponsiveClasses('left', left),
+ inset !== undefined ? `inset-${inset}` : '',
+ insetY !== undefined ? `inset-y-${insetY}` : '',
+ insetX !== undefined ? `inset-x-${insetX}` : '',
+ zIndex !== undefined ? `z-${zIndex}` : '',
+ rounded === true ? 'rounded' : (rounded === false ? 'rounded-none' : (typeof rounded === 'string' ? (rounded.includes('-') ? rounded : `rounded-${rounded}`) : '')),
+ border === true ? 'border' : (typeof border === 'string' ? (border === 'none' ? 'border-none' : border) : ''),
+ borderTop === true ? 'border-t' : (typeof borderTop === 'string' ? borderTop : ''),
+ borderBottom === true ? 'border-b' : (typeof borderBottom === 'string' ? borderBottom : ''),
+ borderLeft === true ? 'border-l' : (typeof borderLeft === 'string' ? borderLeft : ''),
+ borderRight === true ? 'border-r' : (typeof borderRight === 'string' ? borderRight : ''),
+ borderStyle ? `border-${borderStyle}` : '',
borderColor ? borderColor : '',
+ borderOpacity !== undefined ? `border-opacity-${borderOpacity * 100}` : '',
bg ? bg : '',
+ backgroundColor ? backgroundColor : '',
+ bgOpacity !== undefined ? `bg-opacity-${bgOpacity * 100}` : '',
color ? color : '',
shadow ? shadow : '',
+ opacity !== undefined ? `opacity-${opacity * 100}` : '',
+ blur ? (blur === 'none' ? 'blur-none' : `blur-${blur}`) : '',
+ pointerEvents ? `pointer-events-${pointerEvents}` : '',
flex !== undefined ? `flex-${flex}` : '',
flexShrink !== undefined ? `flex-shrink-${flexShrink}` : '',
flexGrow !== undefined ? `flex-grow-${flexGrow}` : '',
+ getResponsiveClasses('flex', flexDirection),
+ flexWrap ? `flex-${flexWrap}` : '',
+ getResponsiveClasses('items', alignItems),
+ getResponsiveClasses('justify', justifyContent),
alignSelf !== undefined ? `self-${alignSelf}` : '',
- opacity !== undefined ? `opacity-${opacity * 100}` : '',
+ getResponsiveClasses('gap', gap),
+ getResponsiveClasses('grid-cols', gridCols || responsiveGridCols),
+ getResponsiveClasses('col-span', colSpan || responsiveColSpan),
getResponsiveClasses('order', order),
+ getResponsiveClasses('text', fontSize),
+ group ? 'group' : '',
+ groupHoverTextColor ? `group-hover:text-${groupHoverTextColor}` : '',
+ groupHoverScale ? 'group-hover:scale-105 transition-transform' : '',
+ groupHoverOpacity !== undefined ? `group-hover:opacity-${groupHoverOpacity * 100}` : '',
+ groupHoverBorderColor ? `group-hover:border-${groupHoverBorderColor}` : '',
+ hoverBorderColor ? `hover:border-${hoverBorderColor}` : '',
+ hoverBg ? `hover:bg-${hoverBg}` : '',
+ hoverTextColor ? `hover:text-${hoverTextColor}` : '',
+ hoverScale === true ? 'hover:scale-105 transition-transform' : (typeof hoverScale === 'number' ? `hover:scale-${hoverScale} transition-transform` : ''),
+ clickable ? 'cursor-pointer active:opacity-80 transition-all' : '',
+ ring ? `ring-${ring}` : '',
+ hideScrollbar ? 'scrollbar-hide' : '',
+ truncate ? 'truncate' : '',
+ transform === true ? 'transform' : (transform === false ? 'transform-none' : ''),
className
].filter(Boolean).join(' ');
const style: React.CSSProperties = {
- ...(width ? { width } : {}),
- ...(height ? { height } : {}),
+ ...(typeof width === 'string' || typeof width === 'number' ? { width } : {}),
+ ...(typeof height === 'string' || typeof height === 'number' ? { height } : {}),
...(typeof maxWidth === 'string' ? { maxWidth } : {}),
...(typeof minWidth === 'string' ? { minWidth } : {}),
...(typeof maxHeight === 'string' ? { maxHeight } : {}),
...(typeof minHeight === 'string' ? { minHeight } : {}),
+ ...(aspectRatio ? { aspectRatio } : {}),
+ ...(typeof top === 'string' || typeof top === 'number' ? { top } : {}),
+ ...(typeof right === 'string' || typeof right === 'number' ? { right } : {}),
+ ...(typeof bottom === 'string' || typeof bottom === 'number' ? { bottom } : {}),
+ ...(typeof left === 'string' || typeof left === 'number' ? { left } : {}),
+ ...(borderWidth !== undefined ? { borderWidth } : {}),
+ ...(typeof transform === 'string' ? { transform } : {}),
+ ...(translate ? { translate } : {}),
+ ...(translateX ? { transform: `translateX(${translateX})` } : {}),
+ ...(translateY ? { transform: `translateY(${translateY})` } : {}),
+ ...(cursor ? { cursor } : {}),
+ ...(fontSize && typeof fontSize === 'string' && !fontSize.includes(':') ? { fontSize } : {}),
+ ...(weight ? { fontWeight: weight } : {}),
+ ...(fontWeight ? { fontWeight } : {}),
+ ...(letterSpacing ? { letterSpacing } : {}),
+ ...(lineHeight ? { lineHeight } : {}),
+ ...(font ? { fontFamily: font } : {}),
+ ...(typeof size === 'string' || typeof size === 'number' ? { width: size, height: size } : {}),
+ ...(backgroundImage ? { backgroundImage } : {}),
+ ...(backgroundSize ? { backgroundSize } : {}),
+ ...(backgroundPosition ? { backgroundPosition } : {}),
+ ...(objectFit ? { objectFit } : {}),
...(styleProp || {})
};
@@ -205,12 +491,35 @@ export const Box = forwardRef((
ref={ref as React.ForwardedRef}
className={classes}
onClick={onClick}
+ onMouseDown={onMouseDown}
+ onMouseUp={onMouseUp}
+ onMouseMove={onMouseMove}
+ onKeyDown={onKeyDown}
+ onBlur={onBlur}
+ onSubmit={onSubmit}
onMouseEnter={onMouseEnter}
onMouseLeave={onMouseLeave}
+ onScroll={onScroll}
style={style}
id={id}
role={role}
tabIndex={tabIndex}
+ type={type}
+ disabled={disabled}
+ src={src}
+ alt={alt}
+ draggable={draggable}
+ min={min}
+ max={max}
+ step={step}
+ value={value}
+ onChange={onChange}
+ placeholder={placeholder}
+ title={title}
+ autoPlay={autoPlay}
+ loop={loop}
+ muted={muted}
+ playsInline={playsInline}
{...props}
>
{children}
diff --git a/apps/website/ui/primitives/Grid.tsx b/apps/website/ui/primitives/Grid.tsx
index 02ea4780e..3e4f0dbe6 100644
--- a/apps/website/ui/primitives/Grid.tsx
+++ b/apps/website/ui/primitives/Grid.tsx
@@ -1,4 +1,4 @@
-import React, { ReactNode } from 'react';
+import React, { ReactNode, ElementType } from 'react';
import { Box, BoxProps, ResponsiveValue } from './Box';
/**
@@ -13,32 +13,16 @@ import { Box, BoxProps, ResponsiveValue } from './Box';
* If you need a more specific layout, create a new component in apps/website/components.
*/
-export interface GridProps {
- children: ReactNode;
+export interface GridProps extends Omit, 'children'> {
+ children?: ReactNode;
cols?: 1 | 2 | 3 | 4 | 5 | 6 | 12;
mdCols?: 1 | 2 | 3 | 4 | 5 | 6 | 12;
lgCols?: 1 | 2 | 3 | 4 | 5 | 6 | 12;
gap?: 0 | 1 | 2 | 3 | 4 | 6 | 8 | 12 | 16;
className?: string;
- // Spacing
- m?: number;
- mt?: number;
- mb?: number;
- ml?: number;
- mr?: number;
- p?: number;
- pt?: number;
- pb?: number;
- pl?: number;
- pr?: number;
- px?: number;
- py?: number;
- // Sizing
- w?: string | ResponsiveValue;
- h?: string | ResponsiveValue;
}
-export function Grid({
+export function Grid({
children,
cols = 1,
mdCols,
@@ -46,7 +30,7 @@ export function Grid({
gap = 4,
className = '',
...props
-}: GridProps) {
+}: GridProps) {
const colClasses: Record = {
1: 'grid-cols-1',
2: 'grid-cols-1 md:grid-cols-2',
diff --git a/apps/website/ui/primitives/GridItem.tsx b/apps/website/ui/primitives/GridItem.tsx
index cb0809c69..89c6d64f5 100644
--- a/apps/website/ui/primitives/GridItem.tsx
+++ b/apps/website/ui/primitives/GridItem.tsx
@@ -1,5 +1,5 @@
-import React from 'react';
-import { Box } from './Box';
+import React, { ElementType } from 'react';
+import { Box, BoxProps } from './Box';
/**
* WARNING: DO NOT VIOLATE THE PURPOSE OF THIS PRIMITIVE.
@@ -12,15 +12,15 @@ import { Box } from './Box';
* If you need a more specific layout, create a new component in apps/website/components.
*/
-export interface GridItemProps {
- children: React.ReactNode;
+export interface GridItemProps extends Omit, 'children'> {
+ children?: React.ReactNode;
colSpan?: 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12;
mdSpan?: 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12;
lgSpan?: 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12;
className?: string;
}
-export function GridItem({ children, colSpan, mdSpan, lgSpan, className = '' }: GridItemProps) {
+export function GridItem({ children, colSpan, mdSpan, lgSpan, className = '', ...props }: GridItemProps) {
const spanClasses = [
colSpan ? `col-span-${colSpan}` : '',
mdSpan ? `md:col-span-${mdSpan}` : '',
@@ -29,7 +29,7 @@ export function GridItem({ children, colSpan, mdSpan, lgSpan, className = '' }:
].filter(Boolean).join(' ');
return (
-
+
{children}
);
diff --git a/apps/website/ui/primitives/Stack.tsx b/apps/website/ui/primitives/Stack.tsx
index ff0db3bf2..eb51d072e 100644
--- a/apps/website/ui/primitives/Stack.tsx
+++ b/apps/website/ui/primitives/Stack.tsx
@@ -1,4 +1,4 @@
-import React, { ReactNode, ElementType } from 'react';
+import React, { ReactNode, ElementType, forwardRef, ForwardedRef } from 'react';
import { Box, BoxProps, ResponsiveValue } from './Box';
/**
@@ -13,8 +13,6 @@ import { Box, BoxProps, ResponsiveValue } from './Box';
* If you need a more specific layout, create a new component in apps/website/components.
*/
-type Spacing = 0 | 0.5 | 1 | 1.5 | 2 | 2.5 | 3 | 3.5 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 14 | 16 | 20 | 24 | 28 | 32 | 36 | 40 | 44 | 48 | 52 | 56 | 60 | 64 | 72 | 80 | 96;
-
interface ResponsiveGap {
base?: number;
sm?: number;
@@ -23,73 +21,31 @@ interface ResponsiveGap {
xl?: number;
}
-interface ResponsiveSpacing {
- base?: Spacing;
- sm?: Spacing;
- md?: Spacing;
- lg?: Spacing;
- xl?: Spacing;
- '2xl'?: Spacing;
-}
-
-export interface StackProps {
+export interface StackProps extends Omit, 'children'> {
as?: T;
- children: ReactNode;
+ children?: ReactNode;
className?: string;
direction?: 'row' | 'col' | { base?: 'row' | 'col'; md?: 'row' | 'col'; lg?: 'row' | 'col' };
- gap?: number | ResponsiveGap;
+ gap?: number | string | ResponsiveGap;
align?: 'start' | 'center' | 'end' | 'stretch' | 'baseline' | ResponsiveValue<'start' | 'center' | 'end' | 'stretch' | 'baseline'>;
justify?: 'start' | 'center' | 'end' | 'between' | 'around' | ResponsiveValue<'start' | 'center' | 'end' | 'between' | 'around'>;
wrap?: boolean;
- // Spacing (allowed for layout)
- m?: Spacing | ResponsiveSpacing;
- mt?: Spacing | ResponsiveSpacing;
- mb?: Spacing | ResponsiveSpacing;
- ml?: Spacing | ResponsiveSpacing;
- mr?: Spacing | ResponsiveSpacing;
- p?: Spacing | ResponsiveSpacing;
- pt?: Spacing | ResponsiveSpacing;
- pb?: Spacing | ResponsiveSpacing;
- pl?: Spacing | ResponsiveSpacing;
- pr?: Spacing | ResponsiveSpacing;
- px?: Spacing | ResponsiveSpacing;
- py?: Spacing | ResponsiveSpacing;
- // Sizing (allowed for layout)
- w?: string | ResponsiveValue;
- h?: string | ResponsiveValue;
- minWidth?: string | ResponsiveValue;
- maxWidth?: string | ResponsiveValue;
- minHeight?: string | ResponsiveValue;
- maxHeight?: string | ResponsiveValue;
- // Basic styling (sometimes needed for containers)
- rounded?: 'none' | 'sm' | 'md' | 'lg' | 'xl' | '2xl' | 'full';
- // Flex item props
- flex?: number | string;
- flexGrow?: number;
- flexShrink?: number;
- alignSelf?: 'auto' | 'start' | 'end' | 'center' | 'stretch' | 'baseline';
- style?: React.CSSProperties;
}
-export function Stack({
- children,
- className = '',
- direction = 'col',
- gap = 4,
- align,
- justify,
- wrap = false,
- m, mt, mb, ml, mr,
- p, pt, pb, pl, pr, px, py,
- w, h, minWidth, maxWidth, minHeight, maxHeight,
- rounded,
- flex,
- flexGrow,
- flexShrink,
- alignSelf,
- as,
- ...props
-}: StackProps) {
+export const Stack = forwardRef((
+ {
+ children,
+ className = '',
+ direction = 'col',
+ gap = 4,
+ align,
+ justify,
+ wrap = false,
+ as,
+ ...props
+ }: StackProps,
+ ref: ForwardedRef
+) => {
const gapClasses: Record = {
0: 'gap-0',
1: 'gap-1',
@@ -104,50 +60,19 @@ export function Stack({
16: 'gap-16'
};
- const getGapClasses = (value: number | ResponsiveGap | undefined) => {
+ const getGapClasses = (value: number | string | ResponsiveGap | undefined) => {
if (value === undefined) return '';
if (typeof value === 'object') {
const classes = [];
- if (value.base !== undefined) classes.push(gapClasses[value.base]);
- if (value.sm !== undefined) classes.push(`sm:${gapClasses[value.sm]}`);
- if (value.md !== undefined) classes.push(`md:${gapClasses[value.md]}`);
- if (value.lg !== undefined) classes.push(`lg:${gapClasses[value.lg]}`);
- if (value.xl !== undefined) classes.push(`xl:${gapClasses[value.xl]}`);
+ if (value.base !== undefined) classes.push(typeof value.base === 'number' ? gapClasses[value.base] : `gap-${value.base}`);
+ if (value.sm !== undefined) classes.push(typeof value.sm === 'number' ? `sm:${gapClasses[value.sm]}` : `sm:gap-${value.sm}`);
+ if (value.md !== undefined) classes.push(typeof value.md === 'number' ? `md:${gapClasses[value.md]}` : `md:gap-${value.md}`);
+ if (value.lg !== undefined) classes.push(typeof value.lg === 'number' ? `lg:${gapClasses[value.lg]}` : `lg:gap-${value.lg}`);
+ if (value.xl !== undefined) classes.push(typeof value.xl === 'number' ? `xl:${gapClasses[value.xl]}` : `xl:gap-${value.xl}`);
return classes.join(' ');
}
- return gapClasses[value];
- };
-
- const spacingMap: Record = {
- 0: '0', 0.5: '0.5', 1: '1', 1.5: '1.5', 2: '2', 2.5: '2.5', 3: '3', 3.5: '3.5', 4: '4',
- 5: '5', 6: '6', 7: '7', 8: '8', 9: '9', 10: '10', 11: '11', 12: '12', 14: '14',
- 16: '16', 20: '20', 24: '24', 28: '28', 32: '32', 36: '36', 40: '40', 44: '44',
- 48: '48', 52: '52', 56: '56', 60: '60', 64: '64', 72: '72', 80: '80', 96: '96'
- };
-
- const roundedClasses = {
- none: 'rounded-none',
- sm: 'rounded-sm',
- md: 'rounded-md',
- lg: 'rounded-lg',
- xl: 'rounded-xl',
- '2xl': 'rounded-2xl',
- full: 'rounded-full'
- };
-
- const getSpacingClass = (prefix: string, value: Spacing | ResponsiveSpacing | undefined) => {
- if (value === undefined) return '';
- if (typeof value === 'object') {
- const classes = [];
- if (value.base !== undefined) classes.push(`${prefix}-${spacingMap[value.base]}`);
- if (value.sm !== undefined) classes.push(`sm:${prefix}-${spacingMap[value.sm]}`);
- if (value.md !== undefined) classes.push(`md:${prefix}-${spacingMap[value.md]}`);
- if (value.lg !== undefined) classes.push(`lg:${prefix}-${spacingMap[value.lg]}`);
- if (value.xl !== undefined) classes.push(`xl:${prefix}-${spacingMap[value.xl]}`);
- if (value['2xl'] !== undefined) classes.push(`2xl:${prefix}-${spacingMap[value['2xl']]}`);
- return classes.join(' ');
- }
- return `${prefix}-${spacingMap[value]}`;
+ if (typeof value === 'number') return gapClasses[value];
+ return `gap-${value}`;
};
const classes = [
@@ -161,19 +86,6 @@ export function Stack({
].filter(Boolean).join(' '),
getGapClasses(gap) || 'gap-4',
wrap ? 'flex-wrap' : '',
- getSpacingClass('m', m),
- getSpacingClass('mt', mt),
- getSpacingClass('mb', mb),
- getSpacingClass('ml', ml),
- getSpacingClass('mr', mr),
- getSpacingClass('p', p),
- getSpacingClass('pt', pt),
- getSpacingClass('pb', pb),
- getSpacingClass('pl', pl),
- getSpacingClass('pr', pr),
- getSpacingClass('px', px),
- getSpacingClass('py', py),
- rounded ? roundedClasses[rounded] : '',
className
].filter(Boolean).join(' ');
@@ -217,20 +129,13 @@ export function Stack({
return (
{children}
);
-}
+});
+
+Stack.displayName = 'Stack';
diff --git a/apps/website/ui/primitives/Surface.tsx b/apps/website/ui/primitives/Surface.tsx
index 37c2e4e44..868aa4ae4 100644
--- a/apps/website/ui/primitives/Surface.tsx
+++ b/apps/website/ui/primitives/Surface.tsx
@@ -1,5 +1,5 @@
-import React, { ReactNode, ElementType, ComponentPropsWithoutRef } from 'react';
-import { Box, BoxProps, ResponsiveValue } from './Box';
+import React, { ReactNode, ElementType, ComponentPropsWithoutRef, forwardRef, ForwardedRef } from 'react';
+import { Box, BoxProps } from './Box';
/**
* WARNING: DO NOT VIOLATE THE PURPOSE OF THIS PRIMITIVE.
@@ -12,33 +12,31 @@ import { Box, BoxProps, ResponsiveValue } from './Box';
* If you need a more specific layout, create a new component in apps/website/components.
*/
-export interface SurfaceProps {
+export interface SurfaceProps extends Omit, 'children' | 'padding'> {
as?: T;
- children: ReactNode;
+ children?: ReactNode;
variant?: 'default' | 'muted' | 'dark' | 'glass' | 'gradient-blue' | 'gradient-gold' | 'gradient-purple' | 'gradient-green' | 'discord' | 'discord-inner';
- rounded?: 'none' | 'sm' | 'md' | 'lg' | 'xl' | '2xl' | 'full';
- border?: boolean;
+ rounded?: 'none' | 'sm' | 'md' | 'lg' | 'xl' | '2xl' | 'full' | string | boolean;
+ border?: boolean | string;
padding?: number;
className?: string;
shadow?: 'none' | 'sm' | 'md' | 'lg' | 'xl' | 'discord' | string;
- // Sizing
- w?: string | ResponsiveValue;
- h?: string | ResponsiveValue;
- maxWidth?: string | ResponsiveValue;
}
-export function Surface({
- as,
- children,
- variant = 'default',
- rounded = 'none',
- border = false,
- padding = 0,
- className = '',
- shadow = 'none',
- w, h, maxWidth,
- ...props
-}: SurfaceProps & ComponentPropsWithoutRef) {
+export const Surface = forwardRef((
+ {
+ as,
+ children,
+ variant = 'default',
+ rounded = 'none',
+ border = false,
+ padding = 0,
+ className = '',
+ shadow = 'none',
+ ...props
+ }: SurfaceProps & ComponentPropsWithoutRef,
+ ref: ForwardedRef
+) => {
const variantClasses: Record = {
default: 'bg-panel-gray',
muted: 'bg-panel-gray/40',
@@ -85,7 +83,7 @@ export function Surface({
const classes = [
variantClasses[variant],
- roundedClasses[rounded],
+ typeof rounded === 'string' && roundedClasses[rounded] ? roundedClasses[rounded] : '',
border ? 'border border-border-gray' : '',
paddingClasses[padding] || 'p-0',
shadowClasses[shadow],
@@ -93,8 +91,10 @@ export function Surface({
].filter(Boolean).join(' ');
return (
-
+
{children}
);
-}
+});
+
+Surface.displayName = 'Surface';