website refactor
This commit is contained in:
@@ -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,
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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';
|
||||
|
||||
|
||||
@@ -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 = {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -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";
|
||||
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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';
|
||||
|
||||
|
||||
@@ -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';
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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 (
|
||||
<Surface
|
||||
variant="muted"
|
||||
padding={3}
|
||||
rounded="lg"
|
||||
style={{ display: 'flex', alignItems: 'start', gap: '0.75rem' }}
|
||||
display="flex"
|
||||
alignItems="start"
|
||||
gap={3}
|
||||
p={4}
|
||||
>
|
||||
{typeColor && (
|
||||
<Box
|
||||
style={{
|
||||
width: '0.5rem',
|
||||
height: '0.5rem',
|
||||
borderRadius: '9999px',
|
||||
marginTop: '0.5rem',
|
||||
backgroundColor: typeColor,
|
||||
flexShrink: 0,
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
<Box style={{ flex: 1, minWidth: 0 }}>
|
||||
<Box
|
||||
w="2"
|
||||
h="2"
|
||||
mt={1.5}
|
||||
rounded="full"
|
||||
bg={color}
|
||||
flexShrink={0}
|
||||
/>
|
||||
<Box flex={1} minWidth={0}>
|
||||
<Text color="text-white" weight="medium" block>
|
||||
{headline}
|
||||
{title || headline}
|
||||
</Text>
|
||||
{body && (
|
||||
<Text size="sm" color="text-gray-400" block mt={1}>
|
||||
{body}
|
||||
</Text>
|
||||
<Text size="sm" color="text-gray-400" block mt={0.5}>
|
||||
{description || body}
|
||||
</Text>
|
||||
<Text size="xs" color="text-gray-500" block mt={2}>
|
||||
{timeAgo || formattedTime}
|
||||
</Text>
|
||||
{ctaHref && ctaLabel && (
|
||||
<Box mt={3}>
|
||||
<Link href={ctaHref} size="xs" variant="primary">
|
||||
{ctaLabel}
|
||||
</Link>
|
||||
</Box>
|
||||
)}
|
||||
<Text size="xs" color="text-gray-500" block mt={1}>
|
||||
{formattedTime}
|
||||
</Text>
|
||||
</Box>
|
||||
{ctaHref && ctaLabel && (
|
||||
<Box>
|
||||
<Link href={ctaHref} variant="primary">
|
||||
<Text size="xs">{ctaLabel}</Text>
|
||||
</Link>
|
||||
</Box>
|
||||
)}
|
||||
</Surface>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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 (
|
||||
<Box style={{ minHeight: '100vh', display: 'flex', alignItems: 'center', justifyContent: 'center', backgroundColor: '#0f1115' }}>
|
||||
<Box
|
||||
fullWidth
|
||||
minHeight="100vh"
|
||||
display="flex"
|
||||
center
|
||||
bg="bg-[#0f1115]"
|
||||
>
|
||||
<Stack align="center" gap={4}>
|
||||
<LoadingSpinner size={12} />
|
||||
<Text color="text-gray-400">{message}</Text>
|
||||
<LoadingSpinner size={10} />
|
||||
<Text color="text-gray-400" weight="medium">
|
||||
{message}
|
||||
</Text>
|
||||
</Stack>
|
||||
</Box>
|
||||
);
|
||||
|
||||
@@ -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 (
|
||||
<Box
|
||||
display="flex"
|
||||
alignItems="center"
|
||||
justifyContent="center"
|
||||
<Surface
|
||||
variant="muted"
|
||||
rounded="full"
|
||||
overflow="hidden"
|
||||
bg="bg-charcoal-outline/20"
|
||||
border={border}
|
||||
border
|
||||
borderColor="border-charcoal-outline/50"
|
||||
className={className}
|
||||
style={{ width: size, height: size, flexShrink: 0 }}
|
||||
w={`${size}px`}
|
||||
h={`${size}px`}
|
||||
flexShrink={0}
|
||||
overflow="hidden"
|
||||
>
|
||||
{avatarSrc ? (
|
||||
{src ? (
|
||||
<Image
|
||||
src={avatarSrc}
|
||||
src={src}
|
||||
alt={alt}
|
||||
className="w-full h-full object-cover"
|
||||
fullWidth
|
||||
fullHeight
|
||||
className="object-cover"
|
||||
fallbackSrc="/default-avatar.png"
|
||||
/>
|
||||
) : (
|
||||
<Icon icon={User} size={size > 32 ? 5 : 4} color="text-gray-500" />
|
||||
<Box fullWidth fullHeight bg="bg-charcoal-outline" display="flex" center>
|
||||
<span className="text-gray-400 font-bold" style={{ fontSize: size * 0.4 }}>
|
||||
{alt.charAt(0).toUpperCase()}
|
||||
</span>
|
||||
</Box>
|
||||
)}
|
||||
</Box>
|
||||
</Surface>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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<BoxProps<'div'>, '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 (
|
||||
<Box className={classes} style={style}>
|
||||
<Box className={classes} {...props}>
|
||||
{icon && <Icon icon={icon} size={3} />}
|
||||
{children}
|
||||
</Box>
|
||||
|
||||
@@ -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 (
|
||||
<Box borderBottom borderColor="border-charcoal-outline">
|
||||
<Box display="flex" gap={4}>
|
||||
<Box borderBottom borderColor="border-border-gray/50" className={className}>
|
||||
<Stack direction="row" gap={8}>
|
||||
{tabs.map((tab) => {
|
||||
const isActive = activeTab === tab.id;
|
||||
return (
|
||||
<Box
|
||||
<Surface
|
||||
key={tab.id}
|
||||
as="button"
|
||||
type="button"
|
||||
onClick={() => 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
|
||||
>
|
||||
<Box display="flex" alignItems="center" gap={2}>
|
||||
<Stack direction="row" align="center" gap={2}>
|
||||
{tab.icon && (
|
||||
<Box color={isActive ? 'text-primary-blue' : 'text-gray-400'} groupHoverTextColor={!isActive ? 'white' : undefined}>
|
||||
{tab.icon}
|
||||
</Box>
|
||||
)}
|
||||
<Text
|
||||
size="sm"
|
||||
weight="medium"
|
||||
color={isActive ? 'text-primary-blue' : 'text-gray-400'}
|
||||
className={!isActive ? 'hover:text-white' : ''}
|
||||
color={isActive ? 'text-primary-blue' : 'text-gray-400'}
|
||||
groupHoverTextColor={!isActive ? 'white' : undefined}
|
||||
>
|
||||
{tab.label}
|
||||
</Text>
|
||||
{tab.count !== undefined && tab.count > 0 && (
|
||||
<Badge variant={tab.countVariant || 'warning'}>
|
||||
{tab.count}
|
||||
</Badge>
|
||||
)}
|
||||
</Box>
|
||||
</Box>
|
||||
</Stack>
|
||||
</Surface>
|
||||
);
|
||||
})}
|
||||
</Box>
|
||||
</Stack>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -4,7 +4,7 @@ import { Box, BoxProps } from './primitives/Box';
|
||||
import { Loader2 } from 'lucide-react';
|
||||
import { Icon } from './Icon';
|
||||
|
||||
interface ButtonProps extends Omit<ButtonHTMLAttributes<HTMLButtonElement>, 'as' | 'onMouseEnter' | 'onMouseLeave' | 'onSubmit'>, Omit<BoxProps<'button'>, 'as' | 'onClick' | 'onSubmit'> {
|
||||
interface ButtonProps extends Omit<ButtonHTMLAttributes<HTMLButtonElement>, 'as' | 'onMouseEnter' | 'onMouseLeave' | 'onSubmit' | 'role' | 'translate' | 'onScroll' | 'draggable' | 'onChange' | 'onMouseDown' | 'onMouseUp' | 'onMouseMove' | 'value' | 'onBlur' | 'onKeyDown'>, Omit<BoxProps<'button'>, 'as' | 'onClick' | 'onSubmit'> {
|
||||
children: ReactNode;
|
||||
onClick?: MouseEventHandler<HTMLButtonElement>;
|
||||
className?: string;
|
||||
@@ -19,6 +19,8 @@ interface ButtonProps extends Omit<ButtonHTMLAttributes<HTMLButtonElement>, 'as'
|
||||
href?: string;
|
||||
target?: string;
|
||||
rel?: string;
|
||||
fontSize?: string;
|
||||
backgroundColor?: string;
|
||||
}
|
||||
|
||||
export const Button = forwardRef<HTMLButtonElement, ButtonProps>(({
|
||||
@@ -36,6 +38,8 @@ export const Button = forwardRef<HTMLButtonElement, ButtonProps>(({
|
||||
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<HTMLButtonElement, ButtonProps>(({
|
||||
target={target}
|
||||
rel={rel}
|
||||
className={classes}
|
||||
fontSize={fontSize}
|
||||
backgroundColor={backgroundColor}
|
||||
{...props}
|
||||
>
|
||||
{content}
|
||||
@@ -98,6 +104,8 @@ export const Button = forwardRef<HTMLButtonElement, ButtonProps>(({
|
||||
className={classes}
|
||||
onClick={onClick}
|
||||
disabled={disabled || isLoading}
|
||||
fontSize={fontSize}
|
||||
backgroundColor={backgroundColor}
|
||||
{...props}
|
||||
>
|
||||
{content}
|
||||
|
||||
@@ -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<BoxProps<'div'>, 'children' | 'className' | 'onClick'> {
|
||||
export interface CardProps extends Omit<BoxProps<'div'>, 'children' | 'onClick'> {
|
||||
children: ReactNode;
|
||||
className?: string;
|
||||
onClick?: MouseEventHandler<HTMLDivElement>;
|
||||
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 = [
|
||||
|
||||
@@ -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 (
|
||||
<Box p={4} rounded="xl" className={`${bgColor} border ${borderColor}`}>
|
||||
<Box p={4} rounded="xl" bg={bgColor} border borderColor={borderColor}>
|
||||
<Box display="flex" alignItems="center" justifyContent="between" mb={3}>
|
||||
<Text size="2xl" weight="bold" className={color}>{count}</Text>
|
||||
<Text size="2xl" weight="bold" color={color}>{count}</Text>
|
||||
<Box p={2} rounded="lg" bg="bg-white/5">
|
||||
<Icon icon={icon} size={5} color={color} />
|
||||
</Box>
|
||||
</Box>
|
||||
<Text color="text-white" weight="medium" block mb={1}>{label}</Text>
|
||||
<ProgressBar value={percentage} max={100} color={progressColor} bg="bg-deep-graphite/50" />
|
||||
<Text size="xs" color="text-gray-500" block mt={1}>{percentage}% of drivers</Text>
|
||||
<Text size="sm" weight="medium" color="text-white" block mb={1}>
|
||||
{label}
|
||||
</Text>
|
||||
<Box w="full" h="1.5" bg="bg-white/5" rounded="full" overflow="hidden">
|
||||
<Box
|
||||
h="full"
|
||||
bg={color.replace('text-', 'bg-')}
|
||||
style={{ width: `${percentage}%` }}
|
||||
/>
|
||||
</Box>
|
||||
<Text size="xs" color="text-gray-500" mt={2}>
|
||||
{percentage.toFixed(1)}% of total
|
||||
</Text>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
/>
|
||||
<Text size="sm" color={disabled ? 'text-gray-500' : 'text-white'}>{label}</Text>
|
||||
</Box>
|
||||
|
||||
@@ -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<BoxProps<'div'>, 'size' | 'padding'> {
|
||||
children: ReactNode;
|
||||
size?: 'sm' | 'md' | 'lg' | 'xl' | 'full';
|
||||
padding?: boolean;
|
||||
|
||||
@@ -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 (
|
||||
<Box
|
||||
as="section"
|
||||
position="relative"
|
||||
className={`bg-[#0C0D0F] border-b border-[#23272B] overflow-hidden ${className}`}
|
||||
<Box
|
||||
position="relative"
|
||||
bg="bg-[#0C0D0F]"
|
||||
borderBottom
|
||||
borderColor="border-[#23272B]"
|
||||
overflow="hidden"
|
||||
className={className}
|
||||
>
|
||||
{/* Subtle Accent Glow */}
|
||||
<Glow
|
||||
position="top-right"
|
||||
color="primary"
|
||||
opacity={0.1}
|
||||
size="xl"
|
||||
{/* Background Glow */}
|
||||
<Box
|
||||
position="absolute"
|
||||
top={-100}
|
||||
right={-100}
|
||||
w="500px"
|
||||
h="500px"
|
||||
bg="bg-primary-blue/10"
|
||||
rounded="full"
|
||||
blur="3xl"
|
||||
/>
|
||||
|
||||
<Box
|
||||
maxWidth="80rem"
|
||||
mx="auto"
|
||||
px={6}
|
||||
py={8}
|
||||
position="relative"
|
||||
zIndex={1}
|
||||
>
|
||||
<Box display="flex" flexDirection="col" gap={8}>
|
||||
<Box display="flex" align="center" justify="between" wrap gap={6}>
|
||||
{/* Driver Identity */}
|
||||
<Box display="flex" align="center" gap={6}>
|
||||
<Box position="relative">
|
||||
<Box
|
||||
w="24"
|
||||
h="24"
|
||||
className="border border-[#23272B] p-1 bg-[#141619]"
|
||||
>
|
||||
<Image
|
||||
src={avatarUrl}
|
||||
alt={driverName}
|
||||
width={96}
|
||||
height={96}
|
||||
className="w-full h-full object-cover grayscale hover:grayscale-0 transition-all duration-300"
|
||||
/>
|
||||
</Box>
|
||||
<Box
|
||||
position="absolute"
|
||||
bottom="-1"
|
||||
right="-1"
|
||||
w="4"
|
||||
h="4"
|
||||
className="bg-[#4ED4E0] border-2 border-[#0C0D0F]"
|
||||
/>
|
||||
</Box>
|
||||
|
||||
<Box>
|
||||
<Box display="flex" align="center" gap={3} mb={1}>
|
||||
<Text size="xs" color="text-gray-500" uppercase weight="bold" letterSpacing="widest">
|
||||
Driver Profile
|
||||
</Text>
|
||||
<Text size="xs" color="text-gray-600">
|
||||
/
|
||||
</Text>
|
||||
<Text size="xs" color="text-gray-400">
|
||||
{country}
|
||||
</Text>
|
||||
</Box>
|
||||
<Heading level={1} className="text-3xl md:text-4xl font-black uppercase tracking-tighter mb-2">
|
||||
{driverName}
|
||||
</Heading>
|
||||
<Box display="flex" align="center" gap={4}>
|
||||
<Box display="flex" align="center" gap={2}>
|
||||
<Text size="xs" color="text-gray-500" uppercase>Rating</Text>
|
||||
<Text size="sm" weight="bold" className="text-[#4ED4E0] font-mono">{rating}</Text>
|
||||
</Box>
|
||||
<Box display="flex" align="center" gap={2}>
|
||||
<Text size="xs" color="text-gray-500" uppercase>Rank</Text>
|
||||
<Text size="sm" weight="bold" className="text-[#FFBE4D] font-mono">#{rank}</Text>
|
||||
</Box>
|
||||
<Box display="flex" align="center" gap={2}>
|
||||
<Text size="xs" color="text-gray-500" uppercase>Starts</Text>
|
||||
<Text size="sm" weight="bold" className="text-gray-300 font-mono">{totalRaces}</Text>
|
||||
</Box>
|
||||
</Box>
|
||||
</Box>
|
||||
<Box p={{ base: 6, md: 10 }} position="relative" zIndex={10}>
|
||||
<Stack direction={{ base: 'col', md: 'row' }} align="center" gap={8}>
|
||||
{/* Avatar Section */}
|
||||
<Box position="relative">
|
||||
<Box
|
||||
p={1}
|
||||
rounded="2xl"
|
||||
bg="bg-[#141619]"
|
||||
border
|
||||
borderColor="border-[#23272B]"
|
||||
>
|
||||
<Avatar
|
||||
src={avatarUrl}
|
||||
alt={driverName}
|
||||
size={120}
|
||||
className="rounded-xl"
|
||||
/>
|
||||
</Box>
|
||||
<Box
|
||||
position="absolute"
|
||||
bottom={-2}
|
||||
right={-2}
|
||||
w="10"
|
||||
h="10"
|
||||
rounded="xl"
|
||||
bg="bg-[#4ED4E0]"
|
||||
borderWidth="2px"
|
||||
borderStyle="solid"
|
||||
borderColor="border-[#0C0D0F]"
|
||||
display="flex"
|
||||
center
|
||||
>
|
||||
<Icon icon={Star} size={5} color="#0C0D0F" />
|
||||
</Box>
|
||||
|
||||
{/* Actions */}
|
||||
{actions && (
|
||||
<Box display="flex" gap={3}>
|
||||
{actions}
|
||||
</Box>
|
||||
)}
|
||||
</Box>
|
||||
|
||||
{/* Stats Grid */}
|
||||
{stats && (
|
||||
<Box
|
||||
display="grid"
|
||||
gridCols={2}
|
||||
responsiveGridCols={{ md: 4 }}
|
||||
gap={4}
|
||||
className="border-t border-[#23272B]/50 pt-6"
|
||||
>
|
||||
{stats}
|
||||
{/* Info Section */}
|
||||
<Stack flex={1} align={{ base: 'center', md: 'start' }} gap={4}>
|
||||
<Box>
|
||||
<Heading level={1} uppercase letterSpacing="tighter" mb={2}>
|
||||
{driverName}
|
||||
</Heading>
|
||||
<Stack direction="row" gap={4}>
|
||||
<Stack gap={0.5}>
|
||||
<Text size="xs" color="text-gray-500" uppercase>Rating</Text>
|
||||
<Text size="sm" weight="bold" color="text-[#4ED4E0]" font="mono">{rating}</Text>
|
||||
</Stack>
|
||||
<Stack gap={0.5}>
|
||||
<Text size="xs" color="text-gray-500" uppercase>Rank</Text>
|
||||
<Text size="sm" weight="bold" color="text-[#FFBE4D]" font="mono">#{rank}</Text>
|
||||
</Stack>
|
||||
<Stack gap={0.5}>
|
||||
<Text size="xs" color="text-gray-500" uppercase>Starts</Text>
|
||||
<Text size="sm" weight="bold" color="text-gray-300" font="mono">{totalRaces}</Text>
|
||||
</Stack>
|
||||
</Stack>
|
||||
</Box>
|
||||
)}
|
||||
</Box>
|
||||
|
||||
<Stack direction="row" gap={3} wrap>
|
||||
<Badge variant="primary" rounded="lg" icon={Trophy}>
|
||||
{winRate}% Win Rate
|
||||
</Badge>
|
||||
<Badge variant="info" rounded="lg" icon={Flag}>
|
||||
Pro License
|
||||
</Badge>
|
||||
<Badge variant="default" rounded="lg" icon={Users}>
|
||||
Team Redline
|
||||
</Badge>
|
||||
</Stack>
|
||||
</Stack>
|
||||
|
||||
{/* Quick Stats */}
|
||||
<Stack
|
||||
direction="row"
|
||||
gap={4}
|
||||
p={6}
|
||||
bg="bg-white/5"
|
||||
rounded="2xl"
|
||||
border
|
||||
borderColor="border-white/10"
|
||||
className="backdrop-blur-md"
|
||||
>
|
||||
<Stack align="center" px={4}>
|
||||
<Text size="2xl" weight="bold" color="text-white">12</Text>
|
||||
<Text size="xs" color="text-gray-500" uppercase>Podiums</Text>
|
||||
</Stack>
|
||||
<Box w="1px" h="10" bg="bg-white/10" />
|
||||
<Stack align="center" px={4}>
|
||||
<Text size="2xl" weight="bold" color="text-white">4</Text>
|
||||
<Text size="xs" color="text-gray-500" uppercase>Wins</Text>
|
||||
</Stack>
|
||||
</Stack>
|
||||
</Stack>
|
||||
</Box>
|
||||
</Box>
|
||||
);
|
||||
|
||||
@@ -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 (
|
||||
<Surface
|
||||
variant="muted"
|
||||
rounded="lg"
|
||||
rounded="xl"
|
||||
border
|
||||
padding={4}
|
||||
style={{ backgroundColor: colors.bg, borderColor: colors.border }}
|
||||
p={4}
|
||||
backgroundColor={colors.bg}
|
||||
borderColor={colors.border}
|
||||
>
|
||||
<Box style={{ flex: 1 }}>
|
||||
{title && <Text weight="medium" style={{ color: colors.text }} block mb={1}>{title}</Text>}
|
||||
<Text size="sm" style={{ color: colors.text, opacity: 0.9 }} block>{message}</Text>
|
||||
</Box>
|
||||
<Stack direction="row" align="start" gap={3}>
|
||||
<Icon icon={colors.icon} size={5} color={colors.text} />
|
||||
<Box flex={1}>
|
||||
{title && <Text weight="medium" color={colors.text} block mb={1}>{title}</Text>}
|
||||
<Text size="sm" color={colors.text} opacity={0.9} block>{message}</Text>
|
||||
</Box>
|
||||
</Stack>
|
||||
</Surface>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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}
|
||||
</Text>
|
||||
|
||||
@@ -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<BoxProps<'h1'>, '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<string, string> = {
|
||||
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 (
|
||||
<Box as={Tag} {...props} className={classes}>
|
||||
<Box
|
||||
as={Tag}
|
||||
{...props}
|
||||
className={classes}
|
||||
style={{
|
||||
...(weight && !weightClasses[weight as keyof typeof weightClasses] ? { fontWeight: weight } : {}),
|
||||
...(props.style || {})
|
||||
}}
|
||||
>
|
||||
{content}
|
||||
</Box>
|
||||
);
|
||||
|
||||
@@ -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 (
|
||||
<Card>
|
||||
<Stack direction="row" align="center" gap={3}>
|
||||
<Surface
|
||||
variant="muted"
|
||||
rounded="full"
|
||||
padding={3}
|
||||
style={{ backgroundColor: iconBgColor }}
|
||||
<Surface variant="muted" rounded="xl" border p={4}>
|
||||
<Stack direction="row" align="center" gap={4}>
|
||||
<Surface
|
||||
variant="muted"
|
||||
rounded="lg"
|
||||
p={3}
|
||||
backgroundColor={iconBgColor}
|
||||
>
|
||||
{icon}
|
||||
<Icon icon={icon} size={5} color={iconColor} />
|
||||
</Surface>
|
||||
<Box>
|
||||
<Text size="xs" color="text-gray-400" block mb={1}>
|
||||
<Text size="xs" color="text-gray-500" uppercase letterSpacing="wider" block>
|
||||
{label}
|
||||
</Text>
|
||||
<Text size="2xl" weight="bold" color="text-white" block>
|
||||
<Text size="xl" weight="bold" color="text-white" block>
|
||||
{value}
|
||||
</Text>
|
||||
{subValue && (
|
||||
<Text size="sm" color="text-gray-400">
|
||||
{subValue}
|
||||
</Text>
|
||||
)}
|
||||
</Box>
|
||||
</Stack>
|
||||
</Card>
|
||||
</Surface>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -2,14 +2,29 @@ import React from 'react';
|
||||
import { LucideIcon } from 'lucide-react';
|
||||
import { Box, BoxProps } from './primitives/Box';
|
||||
|
||||
interface IconProps extends Omit<BoxProps<'svg'>, 'children' | 'as'> {
|
||||
icon: LucideIcon;
|
||||
export interface IconProps extends Omit<BoxProps<'div'>, '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<string | number, string> = {
|
||||
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 <LucideIconComponent size="100%" strokeWidth={props.strokeWidth} />;
|
||||
}
|
||||
return IconProp;
|
||||
};
|
||||
|
||||
return (
|
||||
<Box
|
||||
as={LucideIcon}
|
||||
className={`${sizeClass} ${className}`}
|
||||
className={classes}
|
||||
style={combinedStyle}
|
||||
color={boxColor}
|
||||
display="inline-flex"
|
||||
alignItems="center"
|
||||
justifyContent="center"
|
||||
{...props}
|
||||
/>
|
||||
>
|
||||
{renderIcon()}
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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}
|
||||
>
|
||||
<Icon icon={icon} size={sizeMap[size].icon} color={color} />
|
||||
|
||||
@@ -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<BannerType, {
|
||||
icon: LucideIcon;
|
||||
bg: string;
|
||||
border: string;
|
||||
titleColor: string;
|
||||
iconColor: string;
|
||||
}> = {
|
||||
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 (
|
||||
<Surface
|
||||
variant="muted"
|
||||
rounded="lg"
|
||||
rounded="xl"
|
||||
border
|
||||
padding={4}
|
||||
style={{ backgroundColor: config.bg, borderColor: config.border }}
|
||||
p={4}
|
||||
backgroundColor={config.bg}
|
||||
borderColor={config.border}
|
||||
>
|
||||
<Stack direction="row" align="start" gap={3}>
|
||||
<Icon icon={BannerIcon} size={5} color={config.iconColor} />
|
||||
<Box style={{ flex: 1 }}>
|
||||
<Box flex={1}>
|
||||
{title && (
|
||||
<Text weight="medium" color={config.titleColor} block mb={1}>{title}</Text>
|
||||
<Text weight="medium" color="text-white" block mb={1}>
|
||||
{title}
|
||||
</Text>
|
||||
)}
|
||||
<Text size="sm" color="text-gray-400" block>{children}</Text>
|
||||
{message && (
|
||||
<Text size="sm" color="text-gray-300" block>
|
||||
{message}
|
||||
</Text>
|
||||
)}
|
||||
{children}
|
||||
</Box>
|
||||
</Stack>
|
||||
</Surface>
|
||||
|
||||
@@ -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 (
|
||||
<Surface
|
||||
variant="muted"
|
||||
rounded="xl"
|
||||
border
|
||||
padding={4}
|
||||
style={{ backgroundColor: colors.bg, borderColor: colors.border }}
|
||||
p={4}
|
||||
backgroundColor={colors.bg}
|
||||
borderColor={colors.border}
|
||||
>
|
||||
<Stack direction="row" align="start" gap={3}>
|
||||
<Surface variant="muted" rounded="lg" padding={2} style={{ backgroundColor: 'rgba(255, 255, 255, 0.05)' }}>
|
||||
<Surface variant="muted" rounded="lg" p={2} bg="bg-white/5">
|
||||
<Icon icon={icon} size={5} color={colors.icon} />
|
||||
</Surface>
|
||||
<Box>
|
||||
<Text weight="medium" style={{ color: colors.text }} block>{title}</Text>
|
||||
<Text weight="medium" color={colors.text} block>{title}</Text>
|
||||
<Text size="sm" color="text-gray-400" block mt={1}>{description}</Text>
|
||||
</Box>
|
||||
</Stack>
|
||||
|
||||
@@ -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<HTMLInputElement> {
|
||||
variant?: 'default' | 'error';
|
||||
interface InputProps extends React.InputHTMLAttributes<HTMLInputElement> {
|
||||
label?: string;
|
||||
icon?: ReactNode;
|
||||
errorMessage?: string;
|
||||
icon?: React.ReactNode;
|
||||
label?: React.ReactNode;
|
||||
variant?: 'default' | 'error';
|
||||
}
|
||||
|
||||
export const Input = forwardRef<HTMLInputElement, InputProps>(
|
||||
({ 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 (
|
||||
<Stack gap={1.5} fullWidth>
|
||||
{label && (
|
||||
<Text as="label" size="xs" weight="bold" color="text-gray-500" className="uppercase tracking-wider">
|
||||
<Text as="label" size="xs" weight="bold" color="text-gray-500" uppercase letterSpacing="wider">
|
||||
{label}
|
||||
</Text>
|
||||
)}
|
||||
@@ -28,20 +32,21 @@ export const Input = forwardRef<HTMLInputElement, InputProps>(
|
||||
{icon && (
|
||||
<Box
|
||||
position="absolute"
|
||||
left="3"
|
||||
left={0}
|
||||
top="50%"
|
||||
style={{ transform: 'translateY(-50%)' }}
|
||||
zIndex={10}
|
||||
display="flex"
|
||||
center
|
||||
className="text-gray-500"
|
||||
translateY="-50%"
|
||||
zIndex={10}
|
||||
w="11"
|
||||
display="flex"
|
||||
center
|
||||
color="text-gray-500"
|
||||
>
|
||||
{icon}
|
||||
</Box>
|
||||
)}
|
||||
<input ref={ref} className={classes} {...props} />
|
||||
{errorMessage && (
|
||||
<Text size="xs" color="text-critical-red" block mt={1}>
|
||||
<Text size="xs" color="text-warning-amber" mt={1}>
|
||||
{errorMessage}
|
||||
</Text>
|
||||
)}
|
||||
|
||||
@@ -1,19 +1,19 @@
|
||||
import React, { ReactNode } from 'react';
|
||||
import { Box, BoxProps } from './primitives/Box';
|
||||
|
||||
interface LinkProps extends Omit<BoxProps<'a'>, 'children' | 'className' | 'onClick'> {
|
||||
export interface LinkProps extends Omit<BoxProps<'a'>, '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<HTMLAnchorElement>;
|
||||
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<string, string> = {
|
||||
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}
|
||||
|
||||
@@ -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 (
|
||||
<div
|
||||
<Box
|
||||
w={`${size * 0.25}rem`}
|
||||
h={`${size * 0.25}rem`}
|
||||
rounded="full"
|
||||
borderWidth="2px"
|
||||
borderStyle="solid"
|
||||
borderColor="transparent"
|
||||
borderTopColor={color}
|
||||
borderLeftColor={color}
|
||||
className={`animate-spin ${className}`}
|
||||
style={style}
|
||||
role="status"
|
||||
aria-label="Loading"
|
||||
/>
|
||||
|
||||
@@ -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 (
|
||||
<Box
|
||||
display="flex"
|
||||
flexDirection="col"
|
||||
bg="bg-charcoal-outline/10"
|
||||
<Surface
|
||||
variant="muted"
|
||||
rounded="xl"
|
||||
border
|
||||
borderColor="border-charcoal-outline/50"
|
||||
rounded="lg"
|
||||
overflow="hidden"
|
||||
transition
|
||||
hoverScale={!!onClick}
|
||||
cursor={onClick ? 'pointer' : 'default'}
|
||||
cursor="pointer"
|
||||
onClick={onClick}
|
||||
className={`group ${className}`}
|
||||
group
|
||||
className={className}
|
||||
>
|
||||
<Box position="relative" width="full" style={{ aspectRatio }}>
|
||||
<Box position="relative" w="full" aspectRatio={aspectRatio}>
|
||||
{isLoading ? (
|
||||
<ImagePlaceholder variant="loading" aspectRatio={aspectRatio} rounded="none" />
|
||||
) : error ? (
|
||||
<ImagePlaceholder variant="error" message={error} aspectRatio={aspectRatio} rounded="none" />
|
||||
) : src ? (
|
||||
<Box fullWidth fullHeight bg="bg-white/5" className="animate-pulse" />
|
||||
) : (
|
||||
<Image
|
||||
src={src}
|
||||
alt={alt}
|
||||
className="w-full h-full object-cover"
|
||||
fullWidth
|
||||
fullHeight
|
||||
className="object-cover"
|
||||
/>
|
||||
) : (
|
||||
<ImagePlaceholder aspectRatio={aspectRatio} rounded="none" />
|
||||
)}
|
||||
|
||||
{actions && (
|
||||
|
||||
{/* Overlay */}
|
||||
<Box
|
||||
position="absolute"
|
||||
inset={0}
|
||||
bg="bg-black/40"
|
||||
display="flex"
|
||||
center
|
||||
opacity={0}
|
||||
groupHoverOpacity={1}
|
||||
transition="all 0.2s"
|
||||
>
|
||||
<Box
|
||||
w="12"
|
||||
h="12"
|
||||
rounded="full"
|
||||
bg="bg-white/20"
|
||||
display="flex"
|
||||
center
|
||||
className="backdrop-blur-md"
|
||||
>
|
||||
<Icon
|
||||
icon={type === 'video' ? Play : ImageIcon}
|
||||
size={6}
|
||||
color="white"
|
||||
/>
|
||||
</Box>
|
||||
</Box>
|
||||
|
||||
{title && (
|
||||
<Box
|
||||
position="absolute"
|
||||
top={2}
|
||||
right={2}
|
||||
display="flex"
|
||||
gap={2}
|
||||
opacity={0}
|
||||
className="group-hover:opacity-100 transition-opacity"
|
||||
bottom={0}
|
||||
left={0}
|
||||
right={0}
|
||||
p={3}
|
||||
bg="bg-gradient-to-t from-black/80 to-transparent"
|
||||
>
|
||||
{actions}
|
||||
<Text size="xs" weight="medium" color="text-white" truncate>
|
||||
{title}
|
||||
</Text>
|
||||
</Box>
|
||||
)}
|
||||
</Box>
|
||||
|
||||
{(title || subtitle) && (
|
||||
<Box p={3} borderTop borderColor="border-charcoal-outline/30">
|
||||
{title && (
|
||||
<Text block size="sm" weight="semibold" truncate>
|
||||
{title}
|
||||
</Text>
|
||||
)}
|
||||
{subtitle && (
|
||||
<Text block size="xs" color="text-gray-500" truncate mt={0.5}>
|
||||
{subtitle}
|
||||
</Text>
|
||||
)}
|
||||
</Box>
|
||||
)}
|
||||
</Box>
|
||||
</Surface>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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<void>;
|
||||
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<HTMLDivElement>) => {
|
||||
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<HTMLDivElement>) => {
|
||||
if (event.target === event.currentTarget && onOpenChange) {
|
||||
onOpenChange(false);
|
||||
}
|
||||
const handleClose = () => {
|
||||
if (onClose) onClose();
|
||||
if (onOpenChange) onOpenChange(false);
|
||||
};
|
||||
|
||||
if (!isOpen) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<Box
|
||||
style={{ position: 'fixed', inset: 0, zIndex: 60, display: 'flex', alignItems: 'center', justifyContent: 'center', backgroundColor: 'rgba(0, 0, 0, 0.6)', padding: '0 1rem', backdropFilter: 'blur(4px)' }}
|
||||
position="fixed"
|
||||
inset={0}
|
||||
zIndex={60}
|
||||
display="flex"
|
||||
alignItems="center"
|
||||
justifyContent="center"
|
||||
bg="bg-black/60"
|
||||
px={4}
|
||||
className="backdrop-blur-sm"
|
||||
role="dialog"
|
||||
aria-modal="true"
|
||||
aria-labelledby="modal-title"
|
||||
aria-describedby={description ? 'modal-description' : undefined}
|
||||
onKeyDown={handleKeyDown}
|
||||
onClick={handleBackdropClick}
|
||||
>
|
||||
{/* Backdrop click to close */}
|
||||
<Box position="absolute" inset={0} onClick={handleClose} />
|
||||
|
||||
<Box
|
||||
style={{ width: '100%', maxWidth: '28rem', borderRadius: '1rem', backgroundColor: '#0f1115', border: '1px solid #262626', boxShadow: '0 25px 50px -12px rgba(0, 0, 0, 0.5)', outline: 'none', overflow: 'hidden' }}
|
||||
position="relative"
|
||||
w="full"
|
||||
maxWidth={sizeMap[size]}
|
||||
rounded="2xl"
|
||||
bg="bg-[#0f1115]"
|
||||
border
|
||||
borderColor="border-[#262626]"
|
||||
shadow="2xl"
|
||||
overflow="hidden"
|
||||
tabIndex={-1}
|
||||
>
|
||||
<Box p={6} style={{ borderBottom: '1px solid rgba(38, 38, 38, 0.8)' }}>
|
||||
<Stack direction="row" align="center" gap={3}>
|
||||
{icon && <Box>{icon}</Box>}
|
||||
<Box>
|
||||
<Heading level={2} id="modal-title">{title}</Heading>
|
||||
{description && (
|
||||
<Text
|
||||
id="modal-description"
|
||||
size="sm"
|
||||
color="text-gray-400"
|
||||
block
|
||||
mt={1}
|
||||
>
|
||||
{description}
|
||||
</Text>
|
||||
)}
|
||||
</Box>
|
||||
{/* Header */}
|
||||
<Box p={6} borderBottom borderColor="border-white/5">
|
||||
<Stack direction="row" align="center" justify="between">
|
||||
<Stack direction="row" align="center" gap={3}>
|
||||
{icon && <Box>{icon}</Box>}
|
||||
<Box>
|
||||
{title && (
|
||||
<Text size="xl" weight="bold" color="text-white" block>
|
||||
{title}
|
||||
</Text>
|
||||
)}
|
||||
{description && (
|
||||
<Text size="sm" color="text-gray-400" block mt={1}>
|
||||
{description}
|
||||
</Text>
|
||||
)}
|
||||
</Box>
|
||||
</Stack>
|
||||
<IconButton
|
||||
icon={X}
|
||||
onClick={handleClose}
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
title="Close modal"
|
||||
/>
|
||||
</Stack>
|
||||
</Box>
|
||||
|
||||
<Box p={6}>
|
||||
{/* Content */}
|
||||
<Box p={6} overflowY="auto" maxHeight="calc(100vh - 200px)">
|
||||
{children}
|
||||
</Box>
|
||||
|
||||
{/* Footer */}
|
||||
{(primaryActionLabel || secondaryActionLabel || footer) && (
|
||||
<Box p={6} style={{ borderTop: '1px solid rgba(38, 38, 38, 0.8)' }}>
|
||||
{(primaryActionLabel || secondaryActionLabel) && (
|
||||
<Box style={{ display: 'flex', justifyContent: 'flex-end', gap: '0.75rem' }}>
|
||||
<Box p={6} borderTop borderColor="border-white/5">
|
||||
{footer || (
|
||||
<Stack direction="row" justify="end" gap={3}>
|
||||
{secondaryActionLabel && (
|
||||
<Button
|
||||
type="button"
|
||||
onClick={() => {
|
||||
onSecondaryAction?.();
|
||||
onOpenChange?.(false);
|
||||
}}
|
||||
variant="secondary"
|
||||
size="sm"
|
||||
fullWidth={!primaryActionLabel}
|
||||
variant="ghost"
|
||||
onClick={onSecondaryAction || onClose}
|
||||
disabled={isLoading}
|
||||
>
|
||||
{secondaryActionLabel}
|
||||
</Button>
|
||||
)}
|
||||
{primaryActionLabel && (
|
||||
<Button
|
||||
type="button"
|
||||
onClick={async () => {
|
||||
if (onPrimaryAction) {
|
||||
await onPrimaryAction();
|
||||
}
|
||||
}}
|
||||
variant="primary"
|
||||
size="sm"
|
||||
fullWidth={!secondaryActionLabel}
|
||||
onClick={onPrimaryAction}
|
||||
isLoading={isLoading}
|
||||
>
|
||||
{primaryActionLabel}
|
||||
</Button>
|
||||
)}
|
||||
</Box>
|
||||
)}
|
||||
{footer && (
|
||||
<Box mt={4}>
|
||||
{footer}
|
||||
</Box>
|
||||
</Stack>
|
||||
)}
|
||||
</Box>
|
||||
)}
|
||||
|
||||
@@ -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 (
|
||||
<div className="mt-6 flex items-start gap-3 p-4 rounded-xl bg-red-500/10 border border-red-500/30">
|
||||
<span className="text-red-400 flex-shrink-0 mt-0.5">⚠</span>
|
||||
<p className="text-sm text-red-400">{message}</p>
|
||||
</div>
|
||||
<Box
|
||||
mt={6}
|
||||
display="flex"
|
||||
alignItems="start"
|
||||
gap={3}
|
||||
p={4}
|
||||
rounded="xl"
|
||||
bg="bg-red-500/10"
|
||||
border
|
||||
borderColor="border-red-500/30"
|
||||
>
|
||||
<Text color="text-red-400" flexShrink={0} mt={0.5}>⚠</Text>
|
||||
<Text size="sm" color="text-red-400">{message}</Text>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<void>;
|
||||
children: ReactNode;
|
||||
onSubmit: (e: FormEvent) => void;
|
||||
}
|
||||
|
||||
export function OnboardingForm({ children, onSubmit }: OnboardingFormProps) {
|
||||
return (
|
||||
<form onSubmit={onSubmit} className="relative">
|
||||
<Box as="form" onSubmit={onSubmit} position="relative">
|
||||
{children}
|
||||
</form>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 (
|
||||
<Surface
|
||||
variant="dark"
|
||||
rounded="xl"
|
||||
variant="muted"
|
||||
rounded="2xl"
|
||||
border
|
||||
padding={6}
|
||||
className={`border-charcoal-outline ${className}`}
|
||||
p={6}
|
||||
borderColor="border-charcoal-outline"
|
||||
className={className}
|
||||
>
|
||||
{children}
|
||||
</Surface>
|
||||
|
||||
@@ -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 (
|
||||
<Grid cols={2} gap={4}>
|
||||
<Grid cols={2} mdCols={4} gap={4}>
|
||||
{stats.map((stat, idx) => (
|
||||
<Box key={idx} p={4} style={{ backgroundColor: '#0f1115', borderRadius: '0.75rem', border: '1px solid #262626', textAlign: 'center' }}>
|
||||
<Box
|
||||
key={idx}
|
||||
p={4}
|
||||
bg="bg-[#0f1115]"
|
||||
rounded="xl"
|
||||
border
|
||||
borderColor="border-[#262626]"
|
||||
textAlign="center"
|
||||
>
|
||||
<Text size="3xl" weight="bold" color={stat.color} block mb={1}>{stat.value}</Text>
|
||||
<Text size="xs" color="text-gray-500" style={{ textTransform: 'uppercase', letterSpacing: '0.05em' }}>{stat.label}</Text>
|
||||
<Text size="xs" color="text-gray-500" uppercase letterSpacing="0.05em">{stat.label}</Text>
|
||||
</Box>
|
||||
))}
|
||||
</Grid>
|
||||
|
||||
@@ -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 (
|
||||
<Box p={5} style={{ borderBottom: '1px solid #262626', background: 'linear-gradient(to right, rgba(38, 38, 38, 0.3), transparent)' }}>
|
||||
<Box
|
||||
p={5}
|
||||
borderBottom
|
||||
borderColor="border-white/5"
|
||||
style={{ background: 'linear-gradient(to right, rgba(38, 38, 38, 0.3), transparent)' }}
|
||||
>
|
||||
<Stack direction="row" align="center" justify="between" wrap gap={4}>
|
||||
<Box>
|
||||
<Stack direction="row" align="center" gap={3}>
|
||||
<Surface variant="muted" rounded="lg" padding={2} style={{ backgroundColor: 'rgba(38, 38, 38, 0.5)' }}>
|
||||
<Stack direction="row" align="center" gap={3}>
|
||||
{icon && (
|
||||
<Surface variant="muted" rounded="lg" p={2} bg="bg-white/5">
|
||||
<Icon icon={icon} size={5} color={color} />
|
||||
</Surface>
|
||||
<Box>
|
||||
<Heading level={2}>{title}</Heading>
|
||||
{description && (
|
||||
<Text size="sm" color="text-gray-500" block mt={1}>{description}</Text>
|
||||
)}
|
||||
</Box>
|
||||
</Stack>
|
||||
</Box>
|
||||
{action && <Box>{action}</Box>}
|
||||
)}
|
||||
<Box>
|
||||
<Text size="lg" weight="bold" color="text-white" block>
|
||||
{title}
|
||||
</Text>
|
||||
{description && (
|
||||
<Text size="sm" color="text-gray-400" block>
|
||||
{description}
|
||||
</Text>
|
||||
)}
|
||||
</Box>
|
||||
</Stack>
|
||||
{actions && (
|
||||
<Box>
|
||||
{actions}
|
||||
</Box>
|
||||
)}
|
||||
</Stack>
|
||||
</Box>
|
||||
);
|
||||
|
||||
@@ -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 (
|
||||
<Box style={{ display: 'inline-flex', width: '100%', flexWrap: 'wrap', gap: '0.5rem', borderRadius: '9999px', backgroundColor: 'rgba(38, 38, 38, 0.6)', padding: '0.25rem' }}>
|
||||
<Stack
|
||||
direction="row"
|
||||
display="inline-flex"
|
||||
w="full"
|
||||
flexWrap="wrap"
|
||||
gap={2}
|
||||
rounded="full"
|
||||
bg="bg-black/60"
|
||||
p={1}
|
||||
>
|
||||
{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"
|
||||
>
|
||||
<Stack gap={0.5}>
|
||||
<Text size="xs" weight="medium" color="inherit">{option.label}</Text>
|
||||
{option.description && (
|
||||
<Text size="xs" color={isSelected ? 'text-white' : 'text-gray-400'} style={{ fontSize: '10px', opacity: isSelected ? 0.8 : 1 }}>
|
||||
<Text
|
||||
size="xs"
|
||||
color={isSelected ? 'text-white' : 'text-gray-400'}
|
||||
fontSize="10px"
|
||||
opacity={isSelected ? 0.8 : 1}
|
||||
>
|
||||
{option.description}
|
||||
</Text>
|
||||
)}
|
||||
@@ -66,6 +77,6 @@ export function SegmentedControl({
|
||||
</Box>
|
||||
);
|
||||
})}
|
||||
</Box>
|
||||
</Stack>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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<HTMLSelectElement> {
|
||||
id?: string;
|
||||
'aria-label'?: string;
|
||||
value?: string;
|
||||
onChange?: (e: ChangeEvent<HTMLSelectElement>) => void;
|
||||
options: SelectOption[];
|
||||
className?: string;
|
||||
style?: React.CSSProperties;
|
||||
interface SelectProps extends React.SelectHTMLAttributes<HTMLSelectElement> {
|
||||
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<number, string> = {
|
||||
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<HTMLSelectElement, SelectProps>(
|
||||
({ 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 (
|
||||
<Stack gap={1.5} fullWidth={fullWidth}>
|
||||
{label && (
|
||||
<Text as="label" size="sm" weight="medium" color="text-gray-400">
|
||||
{label}
|
||||
</Text>
|
||||
)}
|
||||
<select
|
||||
id={id}
|
||||
aria-label={ariaLabel}
|
||||
value={value}
|
||||
onChange={onChange}
|
||||
className={classes}
|
||||
style={style}
|
||||
{...props}
|
||||
>
|
||||
{options.map((option) => (
|
||||
<option key={option.value} value={option.value}>
|
||||
{option.label}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</Stack>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<Stack gap={1.5} fullWidth={fullWidth}>
|
||||
{label && (
|
||||
<Text as="label" size="xs" weight="bold" color="text-gray-500" uppercase letterSpacing="wider">
|
||||
{label}
|
||||
</Text>
|
||||
)}
|
||||
<Box
|
||||
as="select"
|
||||
ref={ref}
|
||||
className={classes}
|
||||
style={style}
|
||||
{...props}
|
||||
>
|
||||
{options ? options.map(opt => (
|
||||
<option key={opt.value} value={opt.value}>{opt.label}</option>
|
||||
)) : children}
|
||||
</Box>
|
||||
{errorMessage && (
|
||||
<Text size="xs" color="text-warning-amber" mt={1}>
|
||||
{errorMessage}
|
||||
</Text>
|
||||
)}
|
||||
</Stack>
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
Select.displayName = 'Select';
|
||||
|
||||
@@ -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"
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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 (
|
||||
<div
|
||||
<Box
|
||||
w={width}
|
||||
h={height}
|
||||
rounded={circle ? 'full' : 'md'}
|
||||
bg="bg-white/5"
|
||||
className={`animate-pulse ${className}`}
|
||||
style={style}
|
||||
role="status"
|
||||
aria-label="Loading..."
|
||||
/>
|
||||
|
||||
@@ -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 = (
|
||||
<Card className={`${variantClasses[variant]} ${className} h-full`} p={5}>
|
||||
<Card variant="default" p={5} className={`${variantClasses[variant]} ${className} h-full`}>
|
||||
<Stack gap={3}>
|
||||
<Stack direction="row" align="start" justify="between">
|
||||
<Stack direction="row" align="center" justify="between">
|
||||
<Text size="xs" weight="bold" color="text-gray-500" uppercase letterSpacing="widest">
|
||||
{label}
|
||||
</Text>
|
||||
{icon && (
|
||||
<Box
|
||||
width="11"
|
||||
height="11"
|
||||
rounded="xl"
|
||||
display="flex"
|
||||
center
|
||||
bg="bg-iron-gray/50"
|
||||
border={true}
|
||||
borderColor="border-charcoal-outline"
|
||||
p={2}
|
||||
rounded="lg"
|
||||
bg={iconBgClasses[variant]}
|
||||
className={iconColorClasses[variant]}
|
||||
>
|
||||
<Icon icon={icon} size={5} className={iconColorClasses[variant]} />
|
||||
<Icon icon={icon} size={5} />
|
||||
</Box>
|
||||
)}
|
||||
</Stack>
|
||||
|
||||
<Stack gap={1}>
|
||||
<Text size="3xl" weight="bold" color="text-white">
|
||||
{prefix}{value}{suffix}
|
||||
</Text>
|
||||
{trend && (
|
||||
<Stack
|
||||
direction="row"
|
||||
align="center"
|
||||
gap={1}
|
||||
color={trend.isPositive ? 'text-performance-green' : 'text-error-red'}
|
||||
>
|
||||
<Icon icon={trend.isPositive ? ArrowUpRight : ArrowDownRight} size={4} />
|
||||
<Text size="sm" weight="medium">{Math.abs(trend.value)}%</Text>
|
||||
<Stack direction="row" align="center" gap={1}>
|
||||
<Text
|
||||
size="xs"
|
||||
weight="bold"
|
||||
color={trend.isPositive ? 'text-success-green' : 'text-critical-red'}
|
||||
>
|
||||
{trend.isPositive ? '+' : ''}{trend.value}%
|
||||
</Text>
|
||||
<Text size="xs" color="text-gray-500">
|
||||
vs last period
|
||||
</Text>
|
||||
</Stack>
|
||||
)}
|
||||
</Stack>
|
||||
<Box>
|
||||
<Text size="2xl" weight="bold" color="text-white" block mb={1}>
|
||||
{prefix}{typeof value === 'number' ? value.toLocaleString() : value}{suffix}
|
||||
</Text>
|
||||
<Text size="sm" color="text-gray-400" block>{label}</Text>
|
||||
{subValue && (
|
||||
<Text size="xs" color="text-gray-500" block mt={1}>
|
||||
{subValue}
|
||||
</Text>
|
||||
)}
|
||||
</Box>
|
||||
</Stack>
|
||||
</Card>
|
||||
);
|
||||
|
||||
if (shouldReduceMotion) {
|
||||
return <Box fullHeight>{cardContent}</Box>;
|
||||
if (onClick) {
|
||||
return (
|
||||
<Box as="button" onClick={onClick} w="full" textAlign="left" className="focus:outline-none">
|
||||
{cardContent}
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Box
|
||||
as={motion.div}
|
||||
fullHeight
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ delay }}
|
||||
>
|
||||
{cardContent}
|
||||
</Box>
|
||||
);
|
||||
return cardContent;
|
||||
}
|
||||
|
||||
@@ -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 (
|
||||
<Box
|
||||
p={4}
|
||||
bg="bg-deep-graphite/60"
|
||||
rounded="xl"
|
||||
border={true}
|
||||
borderColor="border-charcoal-outline"
|
||||
textAlign="center"
|
||||
>
|
||||
<Box p={4} textAlign="center">
|
||||
{icon && (
|
||||
<Stack direction="row" align="center" justify="center" gap={2} mb={1} className={color}>
|
||||
<Stack direction="row" align="center" justify="center" gap={2} mb={1} color={color}>
|
||||
<Icon icon={icon} size={4} />
|
||||
<Text size="xs" weight="medium" uppercase letterSpacing="0.05em">{label}</Text>
|
||||
</Stack>
|
||||
)}
|
||||
{!icon && (
|
||||
<Text size="xs" color="text-gray-500" uppercase letterSpacing="0.05em" block mb={1}>{label}</Text>
|
||||
)}
|
||||
<Text size="3xl" weight="bold" color={color} block>{value}</Text>
|
||||
<Text size="2xl" weight="bold" color="text-white" block>
|
||||
{value}
|
||||
</Text>
|
||||
<Text size="xs" weight="medium" color="text-gray-500" uppercase letterSpacing="wider">
|
||||
{label}
|
||||
</Text>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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 (
|
||||
<Surface
|
||||
variant="muted"
|
||||
padding={3}
|
||||
rounded="lg"
|
||||
p={4}
|
||||
display="flex"
|
||||
alignItems="center"
|
||||
gap={4}
|
||||
cursor={onClick ? 'pointer' : 'default'}
|
||||
onClick={onClick}
|
||||
style={{
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'space-between',
|
||||
cursor: onClick ? 'pointer' : 'default',
|
||||
}}
|
||||
hoverBg={onClick ? 'bg-white/5' : undefined}
|
||||
transition={!!onClick}
|
||||
>
|
||||
<Box>
|
||||
<Text color="text-white" weight="medium" block>
|
||||
{title}
|
||||
</Text>
|
||||
{subtitle && (
|
||||
<Text size="xs" color="text-gray-500">
|
||||
{subtitle}
|
||||
{icon && (
|
||||
<Box p={2} rounded="lg" bg="bg-white/5">
|
||||
<Icon icon={icon} size={5} color="text-gray-400" />
|
||||
</Box>
|
||||
)}
|
||||
<Box flex={1}>
|
||||
{(label || title) && (
|
||||
<Text size="xs" color="text-gray-500" uppercase letterSpacing="wider" block>
|
||||
{label || title}
|
||||
</Text>
|
||||
)}
|
||||
{(value || subtitle) && (
|
||||
<Text size="lg" weight="bold" color="text-white" block>
|
||||
{value || subtitle}
|
||||
</Text>
|
||||
)}
|
||||
</Box>
|
||||
|
||||
@@ -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 (
|
||||
<Box
|
||||
display="flex"
|
||||
alignItems="center"
|
||||
gap={1}
|
||||
p={1.5}
|
||||
<Surface
|
||||
variant="muted"
|
||||
rounded="xl"
|
||||
bg="bg-iron-gray/50"
|
||||
border
|
||||
borderColor="border-charcoal-outline"
|
||||
w="fit"
|
||||
position="relative"
|
||||
p={1}
|
||||
display="inline-flex"
|
||||
zIndex={10}
|
||||
className={className}
|
||||
>
|
||||
{tabs.map((tab) => {
|
||||
const isActive = activeTab === tab.id;
|
||||
return (
|
||||
<Box
|
||||
key={tab.id}
|
||||
as="button"
|
||||
type="button"
|
||||
onClick={() => 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 && <Icon icon={tab.icon} size={4} color={isActive ? 'text-white' : 'text-gray-400'} />}
|
||||
<Text
|
||||
size="sm"
|
||||
weight="medium"
|
||||
color={isActive ? 'text-white' : 'text-gray-400'}
|
||||
className={!isActive ? 'hover:text-white' : ''}
|
||||
<Stack direction="row" gap={1}>
|
||||
{tabs.map((tab) => {
|
||||
const isActive = activeTab === tab.id;
|
||||
return (
|
||||
<Surface
|
||||
key={tab.id}
|
||||
as="button"
|
||||
onClick={() => 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}
|
||||
</Text>
|
||||
</Box>
|
||||
);
|
||||
})}
|
||||
</Box>
|
||||
<Stack direction="row" align="center" gap={2}>
|
||||
{tab.icon && (
|
||||
<Box color={isActive ? 'text-white' : 'text-gray-400'} groupHoverTextColor={!isActive ? 'white' : undefined}>
|
||||
{tab.icon}
|
||||
</Box>
|
||||
)}
|
||||
<Text
|
||||
size="sm"
|
||||
weight="medium"
|
||||
color={isActive ? 'text-white' : 'text-gray-400'}
|
||||
groupHoverTextColor={!isActive ? 'white' : undefined}
|
||||
>
|
||||
{tab.label}
|
||||
</Text>
|
||||
</Stack>
|
||||
</Surface>
|
||||
);
|
||||
})}
|
||||
</Stack>
|
||||
</Surface>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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<HTMLTableElement> {
|
||||
interface TableProps extends BoxProps<'table'> {
|
||||
children: ReactNode;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
export function Table({ children, className = '', ...props }: TableProps) {
|
||||
const { border, translate, ...rest } = props;
|
||||
return (
|
||||
<Box overflow="auto" className="border border-border-gray rounded-sm">
|
||||
<table className={`w-full border-collapse text-left ${className}`} {...props}>
|
||||
<Box overflow="auto" border borderColor="border-border-gray" rounded="sm">
|
||||
<table className={`w-full border-collapse text-left ${className}`} {...(rest as any)}>
|
||||
{children}
|
||||
</table>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
interface TableHeadProps extends HTMLAttributes<HTMLTableSectionElement> {
|
||||
interface TableHeaderProps extends BoxProps<'thead'> {
|
||||
children: ReactNode;
|
||||
}
|
||||
|
||||
export function TableHead({ children, ...props }: TableHeadProps) {
|
||||
export function TableHeader({ children, className = '', ...props }: TableHeaderProps) {
|
||||
return (
|
||||
<thead className="bg-graphite-black border-b border-border-gray" {...props}>
|
||||
<Box as="thead" className={`bg-graphite-black border-b border-border-gray ${className}`} {...props}>
|
||||
{children}
|
||||
</thead>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
interface TableBodyProps extends HTMLAttributes<HTMLTableSectionElement> {
|
||||
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 (
|
||||
<tbody className="divide-y divide-border-gray/50" {...props}>
|
||||
<Box as="tbody" className={`divide-y divide-border-gray/50 ${className}`} {...props}>
|
||||
{children}
|
||||
</tbody>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
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 (
|
||||
<Box as="th" className={classes} {...props}>
|
||||
@@ -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 (
|
||||
<Box as="td" className={classes} {...props}>
|
||||
|
||||
@@ -25,14 +25,14 @@ interface ResponsiveTextAlign {
|
||||
'2xl'?: TextAlign;
|
||||
}
|
||||
|
||||
interface TextProps<T extends ElementType = 'span'> extends Omit<BoxProps<T>, 'children' | 'className'> {
|
||||
interface TextProps<T extends ElementType = 'span'> extends Omit<BoxProps<T>, '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<T extends ElementType = 'span'> extends Omit<BoxProps<T>, '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<T extends ElementType = 'span'>({
|
||||
style,
|
||||
block = false,
|
||||
italic = false,
|
||||
lineClamp,
|
||||
ml, mr, mt, mb,
|
||||
...props
|
||||
}: TextProps<T> & ComponentPropsWithoutRef<T>) {
|
||||
@@ -115,7 +117,7 @@ export function Text<T extends ElementType = 'span'>({
|
||||
bold: 'font-bold'
|
||||
};
|
||||
|
||||
const fontClasses = {
|
||||
const fontClasses: Record<string, string> = {
|
||||
mono: 'font-mono',
|
||||
sans: 'font-sans'
|
||||
};
|
||||
@@ -175,8 +177,8 @@ export function Text<T extends ElementType = 'span'>({
|
||||
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<T extends ElementType = 'span'>({
|
||||
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<T extends ElementType = 'span'>({
|
||||
|
||||
const combinedStyle = {
|
||||
...(fontSize ? { fontSize } : {}),
|
||||
...(weight && !weightClasses[weight] ? { fontWeight: weight } : {}),
|
||||
...(font && !fontClasses[font] ? { fontFamily: font } : {}),
|
||||
...style
|
||||
};
|
||||
|
||||
|
||||
@@ -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<HTMLTextAreaElement> {
|
||||
label?: React.ReactNode;
|
||||
interface TextAreaProps extends React.TextareaHTMLAttributes<HTMLTextAreaElement> {
|
||||
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<HTMLTextAreaElement, TextAreaProps>(
|
||||
({ label, errorMessage, variant = 'default', fullWidth = true, className = '', ...props }, ref) => {
|
||||
const isError = variant === 'error' || !!errorMessage;
|
||||
|
||||
return (
|
||||
<Stack gap={1.5} fullWidth={fullWidth}>
|
||||
{label && (
|
||||
<Text as="label" size="xs" weight="bold" color="text-gray-500" uppercase letterSpacing="wider">
|
||||
{label}
|
||||
</Text>
|
||||
)}
|
||||
<Box position="relative" fullWidth={fullWidth}>
|
||||
<Box
|
||||
as="textarea"
|
||||
ref={ref}
|
||||
fullWidth={fullWidth}
|
||||
p={3}
|
||||
bg="bg-deep-graphite"
|
||||
rounded="lg"
|
||||
color="text-white"
|
||||
border
|
||||
borderColor={isError ? 'var(--warning-amber)' : 'rgba(38, 38, 38, 0.8)'}
|
||||
className={`placeholder:text-gray-500 focus:ring-2 focus:ring-primary-blue transition-all duration-150 sm:text-sm ${className}`}
|
||||
{...props}
|
||||
/>
|
||||
{errorMessage && (
|
||||
<Text size="xs" color="text-warning-amber" mt={1}>
|
||||
{errorMessage}
|
||||
</Text>
|
||||
)}
|
||||
</Box>
|
||||
</Stack>
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
return (
|
||||
<Stack gap={1.5} fullWidth={fullWidth}>
|
||||
{label && (
|
||||
<Text as="label" size="sm" weight="medium" color="text-gray-300">
|
||||
{label}
|
||||
</Text>
|
||||
)}
|
||||
<Box position="relative" fullWidth={fullWidth}>
|
||||
<Box
|
||||
as="textarea"
|
||||
fullWidth={fullWidth}
|
||||
p={3}
|
||||
rounded="md"
|
||||
bg="bg-iron-gray"
|
||||
color="text-white"
|
||||
border
|
||||
style={{
|
||||
borderColor: isError ? 'var(--warning-amber)' : 'rgba(38, 38, 38, 0.8)',
|
||||
resize: 'none',
|
||||
}}
|
||||
className={`placeholder:text-gray-500 focus:ring-2 focus:ring-primary-blue transition-all duration-150 sm:text-sm ${className}`}
|
||||
{...props}
|
||||
/>
|
||||
</Box>
|
||||
{errorMessage && (
|
||||
<Text size="xs" color="text-warning-amber">
|
||||
{errorMessage}
|
||||
</Text>
|
||||
)}
|
||||
</Stack>
|
||||
);
|
||||
}
|
||||
TextArea.displayName = 'TextArea';
|
||||
|
||||
@@ -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 (
|
||||
<label className={`flex items-start justify-between cursor-pointer py-3 border-b border-charcoal-outline/50 last:border-b-0 ${disabled ? 'opacity-50 cursor-not-allowed' : ''}`}>
|
||||
<Box style={{ flex: 1, paddingRight: '1rem' }}>
|
||||
<Box
|
||||
as="label"
|
||||
display="flex"
|
||||
alignItems="start"
|
||||
justifyContent="between"
|
||||
cursor={disabled ? 'not-allowed' : 'pointer'}
|
||||
py={3}
|
||||
borderBottom
|
||||
borderColor="border-charcoal-outline/50"
|
||||
className="last:border-b-0"
|
||||
opacity={disabled ? 0.5 : 1}
|
||||
>
|
||||
<Box flex={1} pr={4}>
|
||||
<Text weight="medium" color="text-gray-200" block>{label}</Text>
|
||||
{description && (
|
||||
<Text size="sm" color="text-gray-500" block mt={1}>{description}</Text>
|
||||
<Text size="xs" color="text-gray-500" block mt={1}>
|
||||
{description}
|
||||
</Text>
|
||||
)}
|
||||
</Box>
|
||||
<button
|
||||
type="button"
|
||||
role="switch"
|
||||
aria-checked={checked}
|
||||
onClick={() => !disabled && onChange(!checked)}
|
||||
disabled={disabled}
|
||||
className={`relative w-12 h-6 rounded-full transition-colors duration-200 flex-shrink-0 focus:outline-none focus:ring-2 focus:ring-primary-blue/50 ${
|
||||
checked
|
||||
? 'bg-primary-blue'
|
||||
: 'bg-iron-gray'
|
||||
} ${disabled ? 'cursor-not-allowed' : ''}`}
|
||||
>
|
||||
{/* Glow effect when active */}
|
||||
{checked && (
|
||||
|
||||
<Box position="relative">
|
||||
<Box
|
||||
as="button"
|
||||
type="button"
|
||||
role="switch"
|
||||
aria-checked={checked}
|
||||
onClick={() => !disabled && onChange(!checked)}
|
||||
disabled={disabled}
|
||||
w="12"
|
||||
h="6"
|
||||
rounded="full"
|
||||
transition="all 0.2s"
|
||||
flexShrink={0}
|
||||
ring="primary-blue/50"
|
||||
bg={checked ? 'bg-primary-blue/20' : 'bg-charcoal-outline'}
|
||||
className="focus:outline-none focus:ring-2"
|
||||
>
|
||||
<motion.div
|
||||
className="absolute inset-0 rounded-full bg-primary-blue"
|
||||
initial={{ boxShadow: '0 0 0px rgba(25, 140, 255, 0)' }}
|
||||
animate={{ boxShadow: '0 0 12px rgba(25, 140, 255, 0.4)' }}
|
||||
transition={{ duration: 0.2 }}
|
||||
animate={{
|
||||
opacity: checked ? 1 : 0,
|
||||
boxShadow: checked ? '0 0 10px rgba(25, 140, 255, 0.4)' : '0 0 0px rgba(25, 140, 255, 0)'
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* Knob */}
|
||||
<motion.span
|
||||
</Box>
|
||||
<motion.span
|
||||
className="absolute top-0.5 w-5 h-5 bg-white rounded-full shadow-md"
|
||||
initial={false}
|
||||
animate={{
|
||||
x: checked ? 24 : 2,
|
||||
scale: 1,
|
||||
}}
|
||||
whileTap={{ scale: disabled ? 1 : 0.9 }}
|
||||
transition={{
|
||||
type: 'spring',
|
||||
stiffness: 500,
|
||||
damping: 30,
|
||||
left: checked ? '26px' : '2px',
|
||||
}}
|
||||
transition={{ type: "spring", stiffness: 500, damping: 30 }}
|
||||
/>
|
||||
</button>
|
||||
</label>
|
||||
</Box>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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<T> = {
|
||||
@@ -49,38 +51,151 @@ export interface BoxProps<T extends ElementType> {
|
||||
px?: Spacing | ResponsiveSpacing;
|
||||
py?: Spacing | ResponsiveSpacing;
|
||||
// Sizing
|
||||
w?: string | ResponsiveValue<string>;
|
||||
h?: string | ResponsiveValue<string>;
|
||||
width?: string;
|
||||
height?: string;
|
||||
w?: string | number | ResponsiveValue<string | number>;
|
||||
h?: string | number | ResponsiveValue<string | number>;
|
||||
width?: string | number;
|
||||
height?: string | number;
|
||||
maxWidth?: string | ResponsiveValue<string>;
|
||||
minWidth?: string | ResponsiveValue<string>;
|
||||
maxHeight?: string | ResponsiveValue<string>;
|
||||
minHeight?: string | ResponsiveValue<string>;
|
||||
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<string | number>;
|
||||
right?: string | number | ResponsiveValue<string | number>;
|
||||
bottom?: string | number | ResponsiveValue<string | number>;
|
||||
left?: string | number | ResponsiveValue<string | number>;
|
||||
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<string>;
|
||||
flexWrap?: 'wrap' | 'nowrap' | 'wrap-reverse' | string;
|
||||
alignItems?: 'start' | 'center' | 'end' | 'stretch' | 'baseline' | string | ResponsiveValue<string>;
|
||||
justifyContent?: 'start' | 'center' | 'end' | 'between' | 'around' | 'evenly' | string | ResponsiveValue<string>;
|
||||
alignSelf?: 'auto' | 'start' | 'end' | 'center' | 'stretch' | 'baseline';
|
||||
gap?: number | string | ResponsiveValue<number | string>;
|
||||
gridCols?: number | ResponsiveValue<number>;
|
||||
responsiveGridCols?: number | ResponsiveValue<number>;
|
||||
colSpan?: number | ResponsiveValue<number>;
|
||||
responsiveColSpan?: number | ResponsiveValue<number>;
|
||||
order?: number | string | ResponsiveValue<number | string>;
|
||||
// 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<T>;
|
||||
onMouseLeave?: React.MouseEventHandler<T>;
|
||||
onClick?: React.MouseEventHandler<T>;
|
||||
onMouseEnter?: React.MouseEventHandler<any>;
|
||||
onMouseLeave?: React.MouseEventHandler<any>;
|
||||
onClick?: React.MouseEventHandler<any>;
|
||||
onMouseDown?: React.MouseEventHandler<any>;
|
||||
onMouseUp?: React.MouseEventHandler<any>;
|
||||
onMouseMove?: React.MouseEventHandler<any>;
|
||||
onKeyDown?: React.KeyboardEventHandler<any>;
|
||||
onBlur?: React.FocusEventHandler<any>;
|
||||
onSubmit?: React.FormEventHandler<any>;
|
||||
onScroll?: React.UIEventHandler<any>;
|
||||
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<string>;
|
||||
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<any>;
|
||||
placeholder?: string;
|
||||
title?: string;
|
||||
padding?: Spacing | ResponsiveSpacing;
|
||||
paddingLeft?: Spacing | ResponsiveSpacing;
|
||||
paddingRight?: Spacing | ResponsiveSpacing;
|
||||
paddingTop?: Spacing | ResponsiveSpacing;
|
||||
paddingBottom?: Spacing | ResponsiveSpacing;
|
||||
size?: string | number | ResponsiveValue<string | number>;
|
||||
accept?: string;
|
||||
autoPlay?: boolean;
|
||||
loop?: boolean;
|
||||
muted?: boolean;
|
||||
playsInline?: boolean;
|
||||
objectFit?: 'fill' | 'contain' | 'cover' | 'none' | 'scale-down';
|
||||
}
|
||||
|
||||
export const Box = forwardRef(<T extends ElementType = 'div'>(
|
||||
@@ -92,26 +207,126 @@ export const Box = forwardRef(<T extends ElementType = 'div'>(
|
||||
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<T> & ComponentPropsWithoutRef<T>,
|
||||
ref: ForwardedRef<HTMLElement>
|
||||
@@ -131,14 +346,16 @@ export const Box = forwardRef(<T extends ElementType = 'div'>(
|
||||
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<string | number> | undefined) => {
|
||||
const getResponsiveClasses = (prefix: string, value: any | ResponsiveValue<any> | undefined) => {
|
||||
if (value === undefined) return '';
|
||||
if (typeof value === 'object') {
|
||||
const classes = [];
|
||||
@@ -161,42 +378,111 @@ export const Box = forwardRef(<T extends ElementType = 'div'>(
|
||||
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(<T extends ElementType = 'div'>(
|
||||
ref={ref as React.ForwardedRef<HTMLElement>}
|
||||
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}
|
||||
|
||||
@@ -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<T extends ElementType = 'div'> extends Omit<BoxProps<T>, '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<string>;
|
||||
h?: string | ResponsiveValue<string>;
|
||||
}
|
||||
|
||||
export function Grid({
|
||||
export function Grid<T extends ElementType = 'div'>({
|
||||
children,
|
||||
cols = 1,
|
||||
mdCols,
|
||||
@@ -46,7 +30,7 @@ export function Grid({
|
||||
gap = 4,
|
||||
className = '',
|
||||
...props
|
||||
}: GridProps) {
|
||||
}: GridProps<T>) {
|
||||
const colClasses: Record<number, string> = {
|
||||
1: 'grid-cols-1',
|
||||
2: 'grid-cols-1 md:grid-cols-2',
|
||||
|
||||
@@ -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<T extends ElementType = 'div'> extends Omit<BoxProps<T>, '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<T extends ElementType = 'div'>({ children, colSpan, mdSpan, lgSpan, className = '', ...props }: GridItemProps<T>) {
|
||||
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 (
|
||||
<Box className={spanClasses}>
|
||||
<Box className={spanClasses} {...props}>
|
||||
{children}
|
||||
</Box>
|
||||
);
|
||||
|
||||
@@ -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<T extends ElementType> {
|
||||
export interface StackProps<T extends ElementType> extends Omit<BoxProps<T>, '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<string>;
|
||||
h?: string | ResponsiveValue<string>;
|
||||
minWidth?: string | ResponsiveValue<string>;
|
||||
maxWidth?: string | ResponsiveValue<string>;
|
||||
minHeight?: string | ResponsiveValue<string>;
|
||||
maxHeight?: string | ResponsiveValue<string>;
|
||||
// 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<T extends ElementType = 'div'>({
|
||||
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<T>) {
|
||||
export const Stack = forwardRef(<T extends ElementType = 'div'>(
|
||||
{
|
||||
children,
|
||||
className = '',
|
||||
direction = 'col',
|
||||
gap = 4,
|
||||
align,
|
||||
justify,
|
||||
wrap = false,
|
||||
as,
|
||||
...props
|
||||
}: StackProps<T>,
|
||||
ref: ForwardedRef<HTMLElement>
|
||||
) => {
|
||||
const gapClasses: Record<number, string> = {
|
||||
0: 'gap-0',
|
||||
1: 'gap-1',
|
||||
@@ -104,50 +60,19 @@ export function Stack<T extends ElementType = 'div'>({
|
||||
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<number, string> = {
|
||||
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<T extends ElementType = 'div'>({
|
||||
].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<T extends ElementType = 'div'>({
|
||||
return (
|
||||
<Box
|
||||
as={as}
|
||||
ref={ref}
|
||||
className={`${classes} ${layoutClasses}`}
|
||||
w={w}
|
||||
h={h}
|
||||
minWidth={minWidth}
|
||||
maxWidth={maxWidth}
|
||||
minHeight={minHeight}
|
||||
maxHeight={maxHeight}
|
||||
flex={flex}
|
||||
flexGrow={flexGrow}
|
||||
flexShrink={flexShrink}
|
||||
alignSelf={alignSelf}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
Stack.displayName = 'Stack';
|
||||
|
||||
@@ -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<T extends ElementType = 'div'> {
|
||||
export interface SurfaceProps<T extends ElementType = 'div'> extends Omit<BoxProps<T>, '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<string>;
|
||||
h?: string | ResponsiveValue<string>;
|
||||
maxWidth?: string | ResponsiveValue<string>;
|
||||
}
|
||||
|
||||
export function Surface<T extends ElementType = 'div'>({
|
||||
as,
|
||||
children,
|
||||
variant = 'default',
|
||||
rounded = 'none',
|
||||
border = false,
|
||||
padding = 0,
|
||||
className = '',
|
||||
shadow = 'none',
|
||||
w, h, maxWidth,
|
||||
...props
|
||||
}: SurfaceProps<T> & ComponentPropsWithoutRef<T>) {
|
||||
export const Surface = forwardRef(<T extends ElementType = 'div'>(
|
||||
{
|
||||
as,
|
||||
children,
|
||||
variant = 'default',
|
||||
rounded = 'none',
|
||||
border = false,
|
||||
padding = 0,
|
||||
className = '',
|
||||
shadow = 'none',
|
||||
...props
|
||||
}: SurfaceProps<T> & ComponentPropsWithoutRef<T>,
|
||||
ref: ForwardedRef<HTMLElement>
|
||||
) => {
|
||||
const variantClasses: Record<string, string> = {
|
||||
default: 'bg-panel-gray',
|
||||
muted: 'bg-panel-gray/40',
|
||||
@@ -85,7 +83,7 @@ export function Surface<T extends ElementType = 'div'>({
|
||||
|
||||
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<T extends ElementType = 'div'>({
|
||||
].filter(Boolean).join(' ');
|
||||
|
||||
return (
|
||||
<Box as={as} className={classes} w={w} h={h} maxWidth={maxWidth} {...props}>
|
||||
<Box as={as} ref={ref} className={classes} {...props}>
|
||||
{children}
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
Surface.displayName = 'Surface';
|
||||
|
||||
Reference in New Issue
Block a user