website refactor
This commit is contained in:
@@ -8,7 +8,7 @@ import { Button } from '@/ui/Button';
|
|||||||
import { Heading } from '@/ui/Heading';
|
import { Heading } from '@/ui/Heading';
|
||||||
import { Input } from '@/ui/Input';
|
import { Input } from '@/ui/Input';
|
||||||
import { Box } from '@/ui/primitives/Box';
|
import { Box } from '@/ui/primitives/Box';
|
||||||
import { Stack } from '@/ui/Stack';
|
import { Stack } from '@/ui/primitives/Stack';
|
||||||
import { Text } from '@/ui/Text';
|
import { Text } from '@/ui/Text';
|
||||||
import {
|
import {
|
||||||
Award,
|
Award,
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ import { LeagueAdminScheduleTemplate } from '@/templates/LeagueAdminScheduleTemp
|
|||||||
import { Card } from '@/ui/Card';
|
import { Card } from '@/ui/Card';
|
||||||
import { Heading } from '@/ui/Heading';
|
import { Heading } from '@/ui/Heading';
|
||||||
import { Box } from '@/ui/primitives/Box';
|
import { Box } from '@/ui/primitives/Box';
|
||||||
import { Stack } from '@/ui/Stack';
|
import { Stack } from '@/ui/primitives/Stack';
|
||||||
import { Text } from '@/ui/Text';
|
import { Text } from '@/ui/Text';
|
||||||
import { useParams, useRouter } from 'next/navigation';
|
import { useParams, useRouter } from 'next/navigation';
|
||||||
import { useState } from 'react';
|
import { useState } from 'react';
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ import { RaceViewModel } from '@/lib/view-models/RaceViewModel';
|
|||||||
import { Button } from '@/ui/Button';
|
import { Button } from '@/ui/Button';
|
||||||
import { Card } from '@/ui/Card';
|
import { Card } from '@/ui/Card';
|
||||||
import { Box } from '@/ui/primitives/Box';
|
import { Box } from '@/ui/primitives/Box';
|
||||||
import { Stack } from '@/ui/Stack';
|
import { Stack } from '@/ui/primitives/Stack';
|
||||||
import { Text } from '@/ui/Text';
|
import { Text } from '@/ui/Text';
|
||||||
import { useMemo, useState } from 'react';
|
import { useMemo, useState } from 'react';
|
||||||
|
|
||||||
|
|||||||
@@ -38,13 +38,13 @@ import { StateContainer } from '@/components/shared/state/StateContainer';
|
|||||||
import { useLeagueAdminStatus } from "@/hooks/league/useLeagueAdminStatus";
|
import { useLeagueAdminStatus } from "@/hooks/league/useLeagueAdminStatus";
|
||||||
import { useProtestDetail } from "@/hooks/league/useProtestDetail";
|
import { useProtestDetail } from "@/hooks/league/useProtestDetail";
|
||||||
import { routes } from '@/lib/routing/RouteConfig';
|
import { routes } from '@/lib/routing/RouteConfig';
|
||||||
import { GridItem } from '@/ui/GridItem';
|
import { GridItem } from '@/ui/primitives/GridItem';
|
||||||
import { Heading } from '@/ui/Heading';
|
import { Heading } from '@/ui/Heading';
|
||||||
import { Icon as UIIcon } from '@/ui/Icon';
|
import { Icon as UIIcon } from '@/ui/Icon';
|
||||||
import { Link as UILink } from '@/ui/Link';
|
import { Link as UILink } from '@/ui/Link';
|
||||||
import { Box } from '@/ui/primitives/Box';
|
import { Box } from '@/ui/primitives/Box';
|
||||||
import { Grid } from '@/ui/primitives/Grid';
|
import { Grid } from '@/ui/primitives/Grid';
|
||||||
import { Stack } from '@/ui/Stack';
|
import { Stack } from '@/ui/primitives/Stack';
|
||||||
import { Text } from '@/ui/Text';
|
import { Text } from '@/ui/Text';
|
||||||
|
|
||||||
type PenaltyUiConfig = {
|
type PenaltyUiConfig = {
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ import { Container } from '@/ui/Container';
|
|||||||
import { Heading } from '@/ui/Heading';
|
import { Heading } from '@/ui/Heading';
|
||||||
import { Icon as UIIcon } from '@/ui/Icon';
|
import { Icon as UIIcon } from '@/ui/Icon';
|
||||||
import { Box } from '@/ui/primitives/Box';
|
import { Box } from '@/ui/primitives/Box';
|
||||||
import { Stack } from '@/ui/Stack';
|
import { Stack } from '@/ui/primitives/Stack';
|
||||||
import { Text } from '@/ui/Text';
|
import { Text } from '@/ui/Text';
|
||||||
import {
|
import {
|
||||||
Download
|
Download
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ import { Heading } from '@/ui/Heading';
|
|||||||
import { Icon } from '@/ui/Icon';
|
import { Icon } from '@/ui/Icon';
|
||||||
import { Input } from '@/ui/Input';
|
import { Input } from '@/ui/Input';
|
||||||
import { Box } from '@/ui/primitives/Box';
|
import { Box } from '@/ui/primitives/Box';
|
||||||
import { Stack } from '@/ui/Stack';
|
import { Stack } from '@/ui/primitives/Stack';
|
||||||
import { Text } from '@/ui/Text';
|
import { Text } from '@/ui/Text';
|
||||||
import {
|
import {
|
||||||
AlertCircle,
|
AlertCircle,
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
import { useSponsorBilling } from "@/hooks/sponsor/useSponsorBilling";
|
import { useSponsorBilling } from "@/hooks/sponsor/useSponsorBilling";
|
||||||
import { SponsorBillingTemplate } from "@/templates/SponsorBillingTemplate";
|
import { SponsorBillingTemplate } from "@/templates/SponsorBillingTemplate";
|
||||||
import { Box } from "@/ui/Box";
|
import { Box } from "@/ui/primitives/Box";
|
||||||
import { Text } from "@/ui/Text";
|
import { Text } from "@/ui/Text";
|
||||||
import { Button } from "@/ui/Button";
|
import { Button } from "@/ui/Button";
|
||||||
import { DollarSign, AlertTriangle, Calendar, TrendingUp } from "lucide-react";
|
import { DollarSign, AlertTriangle, Calendar, TrendingUp } from "lucide-react";
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
import { useState } from 'react';
|
import { useState } from 'react';
|
||||||
import { useSponsorSponsorships } from "@/hooks/sponsor/useSponsorSponsorships";
|
import { useSponsorSponsorships } from "@/hooks/sponsor/useSponsorSponsorships";
|
||||||
import { SponsorCampaignsTemplate, SponsorshipType } from "@/templates/SponsorCampaignsTemplate";
|
import { SponsorCampaignsTemplate, SponsorshipType } from "@/templates/SponsorCampaignsTemplate";
|
||||||
import { Box } from "@/ui/Box";
|
import { Box } from "@/ui/primitives/Box";
|
||||||
import { Text } from "@/ui/Text";
|
import { Text } from "@/ui/Text";
|
||||||
import { Button } from "@/ui/Button";
|
import { Button } from "@/ui/Button";
|
||||||
|
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ import { Card } from '@/ui/Card';
|
|||||||
import { Heading } from '@/ui/Heading';
|
import { Heading } from '@/ui/Heading';
|
||||||
import { Input } from '@/ui/Input';
|
import { Input } from '@/ui/Input';
|
||||||
import { Box } from '@/ui/primitives/Box';
|
import { Box } from '@/ui/primitives/Box';
|
||||||
import { Stack } from '@/ui/Stack';
|
import { Stack } from '@/ui/primitives/Stack';
|
||||||
import { Text } from '@/ui/Text';
|
import { Text } from '@/ui/Text';
|
||||||
import { motion, useReducedMotion } from 'framer-motion';
|
import { motion, useReducedMotion } from 'framer-motion';
|
||||||
import {
|
import {
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
import { AchievementDisplay } from '@/lib/display-objects/AchievementDisplay';
|
import { AchievementDisplay } from '@/lib/display-objects/AchievementDisplay';
|
||||||
import { Card } from '@/ui/Card';
|
import { Card } from '@/ui/Card';
|
||||||
import { Grid } from '@/ui/Grid';
|
import { Grid } from '@/ui/primitives/Grid';
|
||||||
import { Heading } from '@/ui/Heading';
|
import { Heading } from '@/ui/Heading';
|
||||||
import { Icon } from '@/ui/Icon';
|
import { Icon } from '@/ui/Icon';
|
||||||
import { Stack } from '@/ui/Stack';
|
import { Stack } from '@/ui/primitives/Stack';
|
||||||
import { Text } from '@/ui/Text';
|
import { Text } from '@/ui/Text';
|
||||||
import { Award, Crown, Medal, Star, Target, Trophy, Zap } from 'lucide-react';
|
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 { Stack } from '@/ui/primitives/Stack';
|
||||||
import { Text } from '@/ui/Text';
|
import { Text } from '@/ui/Text';
|
||||||
|
|
||||||
|
|||||||
@@ -3,8 +3,8 @@ import { Card } from '@/ui/Card';
|
|||||||
import { GoalCard } from '@/ui/GoalCard';
|
import { GoalCard } from '@/ui/GoalCard';
|
||||||
import { Heading } from '@/ui/Heading';
|
import { Heading } from '@/ui/Heading';
|
||||||
import { MilestoneItem } from '@/components/achievements/MilestoneItem';
|
import { MilestoneItem } from '@/components/achievements/MilestoneItem';
|
||||||
import { Stack } from '@/ui/Stack';
|
import { Stack } from '@/ui/primitives/Stack';
|
||||||
import { Grid } from '@/ui/Grid';
|
import { Grid } from '@/ui/primitives/Grid';
|
||||||
|
|
||||||
interface Achievement {
|
interface Achievement {
|
||||||
id: string;
|
id: string;
|
||||||
|
|||||||
@@ -3,8 +3,8 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { Panel } from '@/ui/Panel';
|
import { Panel } from '@/ui/Panel';
|
||||||
import { Glow } from '@/ui/Glow';
|
import { Glow } from '@/ui/Glow';
|
||||||
import { Stack } from '@/ui/Stack';
|
import { Stack } from '@/ui/primitives/Stack';
|
||||||
import { Grid } from '@/ui/Grid';
|
import { Grid } from '@/ui/primitives/Grid';
|
||||||
import { Container } from '@/ui/Container';
|
import { Container } from '@/ui/Container';
|
||||||
import { Heading } from '@/ui/Heading';
|
import { Heading } from '@/ui/Heading';
|
||||||
import { Section } from '@/ui/Section';
|
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 { Code, Lightbulb, LucideIcon, MessageSquare, Users } from 'lucide-react';
|
||||||
import { Heading } from '@/ui/Heading';
|
import { Heading } from '@/ui/Heading';
|
||||||
import { Text } from '@/ui/Text';
|
import { Text } from '@/ui/Text';
|
||||||
import { Stack } from '@/ui/Stack';
|
import { Stack } from '@/ui/primitives/Stack';
|
||||||
import { Grid } from '@/ui/Grid';
|
import { Grid } from '@/ui/primitives/Grid';
|
||||||
import { Card } from '@/ui/Card';
|
import { Card } from '@/ui/Card';
|
||||||
import { Section } from '@/ui/Section';
|
import { Section } from '@/ui/Section';
|
||||||
import { Container } from '@/ui/Container';
|
import { Container } from '@/ui/Container';
|
||||||
|
|||||||
@@ -4,8 +4,8 @@ import React from 'react';
|
|||||||
import { MetricCard } from '@/ui/MetricCard';
|
import { MetricCard } from '@/ui/MetricCard';
|
||||||
import { Activity, Users, Trophy, Calendar } from 'lucide-react';
|
import { Activity, Users, Trophy, Calendar } from 'lucide-react';
|
||||||
import { Container } from '@/ui/Container';
|
import { Container } from '@/ui/Container';
|
||||||
import { Grid } from '@/ui/Grid';
|
import { Grid } from '@/ui/primitives/Grid';
|
||||||
import { Stack } from '@/ui/Stack';
|
import { Stack } from '@/ui/primitives/Stack';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* HomeStatsStrip - A thin strip showing some status or quick info.
|
* HomeStatsStrip - A thin strip showing some status or quick info.
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import { routes } from '@/lib/routing/RouteConfig';
|
import { routes } from '@/lib/routing/RouteConfig';
|
||||||
import { Stack } from '@/ui/Stack';
|
import { Stack } from '@/ui/primitives/Stack';
|
||||||
import { Button } from '@/ui/Button';
|
import { Button } from '@/ui/Button';
|
||||||
import { Container } from '@/ui/Container';
|
import { Container } from '@/ui/Container';
|
||||||
import { Grid } from '@/ui/Grid';
|
import { Grid } from '@/ui/primitives/Grid';
|
||||||
import { Heading } from '@/ui/Heading';
|
import { Heading } from '@/ui/Heading';
|
||||||
import { Link } from '@/ui/Link';
|
import { Link } from '@/ui/Link';
|
||||||
import { Text } from '@/ui/Text';
|
import { Text } from '@/ui/Text';
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
import type { LeagueConfigFormModel } from '@/lib/types/LeagueConfigFormModel';
|
import type { LeagueConfigFormModel } from '@/lib/types/LeagueConfigFormModel';
|
||||||
import { Button } from '@/ui/Button';
|
import { Button } from '@/ui/Button';
|
||||||
import { Grid } from '@/ui/Grid';
|
import { Grid } from '@/ui/primitives/Grid';
|
||||||
import { Heading } from '@/ui/Heading';
|
import { Heading } from '@/ui/Heading';
|
||||||
import { Icon } from '@/ui/Icon';
|
import { Icon } from '@/ui/Icon';
|
||||||
import { Input } from '@/ui/Input';
|
import { Input } from '@/ui/Input';
|
||||||
|
|||||||
@@ -20,12 +20,12 @@ import {
|
|||||||
} from 'lucide-react';
|
} from 'lucide-react';
|
||||||
import type { LeagueConfigFormModel } from '@/lib/types/LeagueConfigFormModel';
|
import type { LeagueConfigFormModel } from '@/lib/types/LeagueConfigFormModel';
|
||||||
import type { LeagueScoringPresetViewModel } from '@/lib/view-models/LeagueScoringPresetViewModel';
|
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 { Text } from '@/ui/Text';
|
||||||
import { Heading } from '@/ui/Heading';
|
import { Heading } from '@/ui/Heading';
|
||||||
import { Icon } from '@/ui/Icon';
|
import { Icon } from '@/ui/Icon';
|
||||||
import { Card } from '@/ui/Card';
|
import { Card } from '@/ui/Card';
|
||||||
import { Grid } from '@/ui/Grid';
|
import { Grid } from '@/ui/primitives/Grid';
|
||||||
|
|
||||||
interface LeagueReviewSummaryProps {
|
interface LeagueReviewSummaryProps {
|
||||||
form: LeagueConfigFormModel;
|
form: LeagueConfigFormModel;
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import type { LeagueConfigFormModel } from '@/lib/types/LeagueConfigFormModel';
|
|||||||
import type { LeagueScoringPresetViewModel } from '@/lib/view-models/LeagueScoringPresetViewModel';
|
import type { LeagueScoringPresetViewModel } from '@/lib/view-models/LeagueScoringPresetViewModel';
|
||||||
import type { CustomPointsConfig } from '@/lib/view-models/ScoringConfigurationViewModel';
|
import type { CustomPointsConfig } from '@/lib/view-models/ScoringConfigurationViewModel';
|
||||||
import { Button } from '@/ui/Button';
|
import { Button } from '@/ui/Button';
|
||||||
import { Grid } from '@/ui/Grid';
|
import { Grid } from '@/ui/primitives/Grid';
|
||||||
import { Heading } from '@/ui/Heading';
|
import { Heading } from '@/ui/Heading';
|
||||||
import { Icon } from '@/ui/Icon';
|
import { Icon } from '@/ui/Icon';
|
||||||
import { Stack } from '@/ui/primitives/Stack';
|
import { Stack } from '@/ui/primitives/Stack';
|
||||||
|
|||||||
@@ -1,13 +1,13 @@
|
|||||||
import { ArrowRight } from 'lucide-react';
|
import { ArrowRight } from 'lucide-react';
|
||||||
import { Button } from '@/ui/Button';
|
import { Button } from '@/ui/Button';
|
||||||
import { Card } from '@/ui/Card';
|
import { Card } from '@/ui/Card';
|
||||||
import { Grid } from '@/ui/Grid';
|
import { Grid } from '@/ui/primitives/Grid';
|
||||||
import { Heading } from '@/ui/Heading';
|
import { Heading } from '@/ui/Heading';
|
||||||
import { Icon } from '@/ui/Icon';
|
import { Icon } from '@/ui/Icon';
|
||||||
import { LeagueLogo } from './LeagueLogo';
|
import { LeagueLogo } from './LeagueLogo';
|
||||||
import { Link } from '@/ui/Link';
|
import { Link } from '@/ui/Link';
|
||||||
import { Text } from '@/ui/Text';
|
import { Text } from '@/ui/Text';
|
||||||
import { Stack } from '@/ui/Stack';
|
import { Stack } from '@/ui/primitives/Stack';
|
||||||
|
|
||||||
interface LeagueSummaryCardProps {
|
interface LeagueSummaryCardProps {
|
||||||
id: string;
|
id: string;
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import { ProtestViewModel } from "@/lib/view-models/ProtestViewModel";
|
|||||||
import { RaceViewModel } from "@/lib/view-models/RaceViewModel";
|
import { RaceViewModel } from "@/lib/view-models/RaceViewModel";
|
||||||
import { DriverViewModel } from "@/lib/view-models/DriverViewModel";
|
import { DriverViewModel } from "@/lib/view-models/DriverViewModel";
|
||||||
import { Card } from "@/ui/Card";
|
import { Card } from "@/ui/Card";
|
||||||
import { Stack } from "@/ui/Stack";
|
import { Stack } from "@/ui/primitives/Stack";
|
||||||
import { Text } from "@/ui/Text";
|
import { Text } from "@/ui/Text";
|
||||||
import { Heading } from "@/ui/Heading";
|
import { Heading } from "@/ui/Heading";
|
||||||
import { Icon } from "@/ui/Icon";
|
import { Icon } from "@/ui/Icon";
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { DriverViewModel } from "@/lib/view-models/DriverViewModel";
|
import { DriverViewModel } from "@/lib/view-models/DriverViewModel";
|
||||||
import { ProtestViewModel } from "@/lib/view-models/ProtestViewModel";
|
import { ProtestViewModel } from "@/lib/view-models/ProtestViewModel";
|
||||||
import { RaceViewModel } from "@/lib/view-models/RaceViewModel";
|
import { RaceViewModel } from "@/lib/view-models/RaceViewModel";
|
||||||
import { Stack } from "@/ui/Stack";
|
import { Stack } from "@/ui/primitives/Stack";
|
||||||
import { Card } from "@/ui/Card";
|
import { Card } from "@/ui/Card";
|
||||||
import { ProtestListItem } from "./ProtestListItem";
|
import { ProtestListItem } from "./ProtestListItem";
|
||||||
import { Text } from "@/ui/Text";
|
import { Text } from "@/ui/Text";
|
||||||
|
|||||||
@@ -7,13 +7,13 @@ import { ProtestViewModel } from "../../lib/view-models/ProtestViewModel";
|
|||||||
import { Modal } from "@/ui/Modal";
|
import { Modal } from "@/ui/Modal";
|
||||||
import { Button } from "@/ui/Button";
|
import { Button } from "@/ui/Button";
|
||||||
import { Card } from "@/ui/Card";
|
import { Card } from "@/ui/Card";
|
||||||
import { Stack } from "@/ui/Stack";
|
import { Stack } from "@/ui/primitives/Stack";
|
||||||
import { Text } from "@/ui/Text";
|
import { Text } from "@/ui/Text";
|
||||||
import { Heading } from "@/ui/Heading";
|
import { Heading } from "@/ui/Heading";
|
||||||
import { Icon } from "@/ui/Icon";
|
import { Icon } from "@/ui/Icon";
|
||||||
import { TextArea } from "@/ui/TextArea";
|
import { TextArea } from "@/ui/TextArea";
|
||||||
import { Input } from "@/ui/Input";
|
import { Input } from "@/ui/Input";
|
||||||
import { Grid } from "@/ui/Grid";
|
import { Grid } from "@/ui/primitives/Grid";
|
||||||
import {
|
import {
|
||||||
AlertCircle,
|
AlertCircle,
|
||||||
Video,
|
Video,
|
||||||
|
|||||||
@@ -3,11 +3,11 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { Calendar, Clock, MapPin, Car, Trophy } from 'lucide-react';
|
import { Calendar, Clock, MapPin, Car, Trophy } from 'lucide-react';
|
||||||
import { Card } from '@/ui/Card';
|
import { Card } from '@/ui/Card';
|
||||||
import { Stack } from '@/ui/Stack';
|
import { Stack } from '@/ui/primitives/Stack';
|
||||||
import { Text } from '@/ui/Text';
|
import { Text } from '@/ui/Text';
|
||||||
import { Heading } from '@/ui/Heading';
|
import { Heading } from '@/ui/Heading';
|
||||||
import { Badge } from '@/ui/Badge';
|
import { Badge } from '@/ui/Badge';
|
||||||
import { Grid } from '@/ui/Grid';
|
import { Grid } from '@/ui/primitives/Grid';
|
||||||
import { Icon } from '@/ui/Icon';
|
import { Icon } from '@/ui/Icon';
|
||||||
|
|
||||||
interface Race {
|
interface Race {
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { Grid } from '@/ui/Grid';
|
import { Grid } from '@/ui/primitives/Grid';
|
||||||
import { Icon } from '@/ui/Icon';
|
import { Icon } from '@/ui/Icon';
|
||||||
import { Stack } from '@/ui/primitives/Stack';
|
import { Stack } from '@/ui/primitives/Stack';
|
||||||
import { Text } from '@/ui/Text';
|
import { Text } from '@/ui/Text';
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { Button } from '@/ui/Button';
|
import { Button } from '@/ui/Button';
|
||||||
import { Grid } from '@/ui/Grid';
|
import { Grid } from '@/ui/primitives/Grid';
|
||||||
import { Icon } from '@/ui/Icon';
|
import { Icon } from '@/ui/Icon';
|
||||||
import { Stack } from '@/ui/primitives/Stack';
|
import { Stack } from '@/ui/primitives/Stack';
|
||||||
import { Text } from '@/ui/Text';
|
import { Text } from '@/ui/Text';
|
||||||
|
|||||||
@@ -9,11 +9,11 @@ import { AdminDashboardViewData } from '@/lib/view-data/AdminDashboardViewData';
|
|||||||
import { Button } from '@/ui/Button';
|
import { Button } from '@/ui/Button';
|
||||||
import { Card } from '@/ui/Card';
|
import { Card } from '@/ui/Card';
|
||||||
import { Container } from '@/ui/Container';
|
import { Container } from '@/ui/Container';
|
||||||
import { Grid } from '@/ui/Grid';
|
import { Grid } from '@/ui/primitives/Grid';
|
||||||
import { Icon } from '@/ui/Icon';
|
import { Icon } from '@/ui/Icon';
|
||||||
import { Box } from '@/ui/primitives/Box';
|
import { Box } from '@/ui/primitives/Box';
|
||||||
import { QuickActionLink } from '@/ui/QuickActionLink';
|
import { QuickActionLink } from '@/ui/QuickActionLink';
|
||||||
import { Stack } from '@/ui/Stack';
|
import { Stack } from '@/ui/primitives/Stack';
|
||||||
import { StatusBadge } from '@/ui/StatusBadge';
|
import { StatusBadge } from '@/ui/StatusBadge';
|
||||||
import { Text } from '@/ui/Text';
|
import { Text } from '@/ui/Text';
|
||||||
import {
|
import {
|
||||||
|
|||||||
@@ -10,10 +10,10 @@ import { routes } from '@/lib/routing/RouteConfig';
|
|||||||
import type { DashboardViewData } from '@/lib/view-data/DashboardViewData';
|
import type { DashboardViewData } from '@/lib/view-data/DashboardViewData';
|
||||||
import { Avatar } from '@/ui/Avatar';
|
import { Avatar } from '@/ui/Avatar';
|
||||||
import { Button } from '@/ui/Button';
|
import { Button } from '@/ui/Button';
|
||||||
import { Grid } from '@/ui/Grid';
|
import { Grid } from '@/ui/primitives/Grid';
|
||||||
import { IconButton } from '@/ui/IconButton';
|
import { IconButton } from '@/ui/IconButton';
|
||||||
import { Box } from '@/ui/primitives/Box';
|
import { Box } from '@/ui/primitives/Box';
|
||||||
import { Stack } from '@/ui/Stack';
|
import { Stack } from '@/ui/primitives/Stack';
|
||||||
import { Text } from '@/ui/Text';
|
import { Text } from '@/ui/Text';
|
||||||
import { Bell, Calendar, LayoutDashboard, Search, Settings, Trophy, Users } from 'lucide-react';
|
import { Bell, Calendar, LayoutDashboard, Search, Settings, Trophy, Users } from 'lucide-react';
|
||||||
import { useRouter } from 'next/navigation';
|
import { useRouter } from 'next/navigation';
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import { DriverLeaderboardPreview } from '@/components/leaderboards/DriverLeader
|
|||||||
import { TeamLeaderboardPreview } from '@/components/teams/TeamLeaderboardPreviewWrapper';
|
import { TeamLeaderboardPreview } from '@/components/teams/TeamLeaderboardPreviewWrapper';
|
||||||
import type { LeaderboardsViewData } from '@/lib/view-data/LeaderboardsViewData';
|
import type { LeaderboardsViewData } from '@/lib/view-data/LeaderboardsViewData';
|
||||||
import { Container } from '@/ui/Container';
|
import { Container } from '@/ui/Container';
|
||||||
import { GridItem } from '@/ui/GridItem';
|
import { GridItem } from '@/ui/primitives/GridItem';
|
||||||
import { PageHero } from '@/ui/PageHero';
|
import { PageHero } from '@/ui/PageHero';
|
||||||
import { Grid } from '@/ui/primitives/Grid';
|
import { Grid } from '@/ui/primitives/Grid';
|
||||||
import { Trophy, Users } from 'lucide-react';
|
import { Trophy, Users } from 'lucide-react';
|
||||||
|
|||||||
@@ -1,66 +1,67 @@
|
|||||||
|
import React from 'react';
|
||||||
|
|
||||||
import { Box } from './primitives/Box';
|
import { Box } from './primitives/Box';
|
||||||
import { Link } from './Link';
|
|
||||||
import { Text } from './Text';
|
import { Text } from './Text';
|
||||||
import { Surface } from './primitives/Surface';
|
import { Surface } from './primitives/Surface';
|
||||||
|
import { Link } from './Link';
|
||||||
|
|
||||||
interface ActivityItemProps {
|
interface ActivityItemProps {
|
||||||
headline: string;
|
title?: string;
|
||||||
|
description?: string;
|
||||||
|
timeAgo?: string;
|
||||||
|
color?: string;
|
||||||
|
headline?: string;
|
||||||
body?: string;
|
body?: string;
|
||||||
formattedTime: string;
|
formattedTime?: string;
|
||||||
ctaHref?: string;
|
ctaHref?: string;
|
||||||
ctaLabel?: string;
|
ctaLabel?: string;
|
||||||
typeColor?: string;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function ActivityItem({
|
export function ActivityItem({
|
||||||
|
title,
|
||||||
|
description,
|
||||||
|
timeAgo,
|
||||||
|
color = 'bg-primary-blue',
|
||||||
headline,
|
headline,
|
||||||
body,
|
body,
|
||||||
formattedTime,
|
formattedTime,
|
||||||
ctaHref,
|
ctaHref,
|
||||||
ctaLabel,
|
ctaLabel
|
||||||
typeColor,
|
|
||||||
}: ActivityItemProps) {
|
}: ActivityItemProps) {
|
||||||
return (
|
return (
|
||||||
<Surface
|
<Surface
|
||||||
variant="muted"
|
variant="muted"
|
||||||
padding={3}
|
|
||||||
rounded="lg"
|
rounded="lg"
|
||||||
style={{ display: 'flex', alignItems: 'start', gap: '0.75rem' }}
|
display="flex"
|
||||||
|
alignItems="start"
|
||||||
|
gap={3}
|
||||||
|
p={4}
|
||||||
>
|
>
|
||||||
{typeColor && (
|
<Box
|
||||||
<Box
|
w="2"
|
||||||
style={{
|
h="2"
|
||||||
width: '0.5rem',
|
mt={1.5}
|
||||||
height: '0.5rem',
|
rounded="full"
|
||||||
borderRadius: '9999px',
|
bg={color}
|
||||||
marginTop: '0.5rem',
|
flexShrink={0}
|
||||||
backgroundColor: typeColor,
|
/>
|
||||||
flexShrink: 0,
|
<Box flex={1} minWidth={0}>
|
||||||
}}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
<Box style={{ flex: 1, minWidth: 0 }}>
|
|
||||||
<Text color="text-white" weight="medium" block>
|
<Text color="text-white" weight="medium" block>
|
||||||
{headline}
|
{title || headline}
|
||||||
</Text>
|
</Text>
|
||||||
{body && (
|
<Text size="sm" color="text-gray-400" block mt={0.5}>
|
||||||
<Text size="sm" color="text-gray-400" block mt={1}>
|
{description || body}
|
||||||
{body}
|
</Text>
|
||||||
</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>
|
</Box>
|
||||||
{ctaHref && ctaLabel && (
|
|
||||||
<Box>
|
|
||||||
<Link href={ctaHref} variant="primary">
|
|
||||||
<Text size="xs">{ctaLabel}</Text>
|
|
||||||
</Link>
|
|
||||||
</Box>
|
|
||||||
)}
|
|
||||||
</Surface>
|
</Surface>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,8 +1,7 @@
|
|||||||
|
import React from 'react';
|
||||||
|
|
||||||
import { Box } from './primitives/Box';
|
import { Box } from './primitives/Box';
|
||||||
import { LoadingSpinner } from './LoadingSpinner';
|
|
||||||
import { Stack } from './primitives/Stack';
|
import { Stack } from './primitives/Stack';
|
||||||
|
import { LoadingSpinner } from './LoadingSpinner';
|
||||||
import { Text } from './Text';
|
import { Text } from './Text';
|
||||||
|
|
||||||
interface AuthLoadingProps {
|
interface AuthLoadingProps {
|
||||||
@@ -11,10 +10,18 @@ interface AuthLoadingProps {
|
|||||||
|
|
||||||
export function AuthLoading({ message = 'Authenticating...' }: AuthLoadingProps) {
|
export function AuthLoading({ message = 'Authenticating...' }: AuthLoadingProps) {
|
||||||
return (
|
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}>
|
<Stack align="center" gap={4}>
|
||||||
<LoadingSpinner size={12} />
|
<LoadingSpinner size={10} />
|
||||||
<Text color="text-gray-400">{message}</Text>
|
<Text color="text-gray-400" weight="medium">
|
||||||
|
{message}
|
||||||
|
</Text>
|
||||||
</Stack>
|
</Stack>
|
||||||
</Box>
|
</Box>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,51 +1,44 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { Box } from './primitives/Box';
|
import { Box } from './primitives/Box';
|
||||||
import { Image } from './Image';
|
import { Image } from './Image';
|
||||||
import { User } from 'lucide-react';
|
import { Surface } from './primitives/Surface';
|
||||||
import { Icon } from './Icon';
|
|
||||||
|
|
||||||
export interface AvatarProps {
|
interface AvatarProps {
|
||||||
driverId?: string;
|
src?: string | null;
|
||||||
src?: string;
|
|
||||||
alt: string;
|
alt: string;
|
||||||
size?: number;
|
size?: number;
|
||||||
className?: string;
|
className?: string;
|
||||||
border?: boolean;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function Avatar({
|
export function Avatar({ src, alt, size = 40, className = '' }: AvatarProps) {
|
||||||
driverId,
|
|
||||||
src,
|
|
||||||
alt,
|
|
||||||
size = 40,
|
|
||||||
className = '',
|
|
||||||
border = true,
|
|
||||||
}: AvatarProps) {
|
|
||||||
const avatarSrc = src || (driverId ? `/media/avatar/${driverId}` : undefined);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box
|
<Surface
|
||||||
display="flex"
|
variant="muted"
|
||||||
alignItems="center"
|
|
||||||
justifyContent="center"
|
|
||||||
rounded="full"
|
rounded="full"
|
||||||
overflow="hidden"
|
border
|
||||||
bg="bg-charcoal-outline/20"
|
|
||||||
border={border}
|
|
||||||
borderColor="border-charcoal-outline/50"
|
borderColor="border-charcoal-outline/50"
|
||||||
className={className}
|
className={className}
|
||||||
style={{ width: size, height: size, flexShrink: 0 }}
|
w={`${size}px`}
|
||||||
|
h={`${size}px`}
|
||||||
|
flexShrink={0}
|
||||||
|
overflow="hidden"
|
||||||
>
|
>
|
||||||
{avatarSrc ? (
|
{src ? (
|
||||||
<Image
|
<Image
|
||||||
src={avatarSrc}
|
src={src}
|
||||||
alt={alt}
|
alt={alt}
|
||||||
className="w-full h-full object-cover"
|
fullWidth
|
||||||
|
fullHeight
|
||||||
|
className="object-cover"
|
||||||
fallbackSrc="/default-avatar.png"
|
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 React, { ReactNode } from 'react';
|
||||||
import { Box } from './primitives/Box';
|
import { Box, BoxProps } from './primitives/Box';
|
||||||
import { Icon } from './Icon';
|
import { Icon } from './Icon';
|
||||||
import { LucideIcon } from 'lucide-react';
|
import { LucideIcon } from 'lucide-react';
|
||||||
|
|
||||||
interface BadgeProps {
|
interface BadgeProps extends Omit<BoxProps<'div'>, 'children'> {
|
||||||
children: ReactNode;
|
children: ReactNode;
|
||||||
className?: string;
|
|
||||||
variant?: 'default' | 'primary' | 'success' | 'warning' | 'danger' | 'info';
|
variant?: 'default' | 'primary' | 'success' | 'warning' | 'danger' | 'info';
|
||||||
size?: 'xs' | 'sm' | 'md';
|
size?: 'xs' | 'sm' | 'md';
|
||||||
icon?: LucideIcon;
|
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 baseClasses = 'flex items-center gap-1.5 border font-bold uppercase tracking-widest';
|
||||||
|
|
||||||
const sizeClasses = {
|
const sizeClasses = {
|
||||||
@@ -47,16 +41,13 @@ export function Badge({ children, className = '', variant = 'default', size = 's
|
|||||||
const classes = [
|
const classes = [
|
||||||
baseClasses,
|
baseClasses,
|
||||||
sizeClasses[size],
|
sizeClasses[size],
|
||||||
roundedClasses[rounded],
|
typeof rounded === 'string' && roundedClasses[rounded as keyof typeof roundedClasses] ? roundedClasses[rounded as keyof typeof roundedClasses] : '',
|
||||||
!bg && !color && !borderColor ? variantClasses[variant] : '',
|
!props.bg && !props.color && !props.borderColor ? variantClasses[variant] : '',
|
||||||
bg,
|
|
||||||
color,
|
|
||||||
borderColor,
|
|
||||||
className
|
className
|
||||||
].filter(Boolean).join(' ');
|
].filter(Boolean).join(' ');
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box className={classes} style={style}>
|
<Box className={classes} {...props}>
|
||||||
{icon && <Icon icon={icon} size={3} />}
|
{icon && <Icon icon={icon} size={3} />}
|
||||||
{children}
|
{children}
|
||||||
</Box>
|
</Box>
|
||||||
|
|||||||
@@ -1,64 +1,63 @@
|
|||||||
|
import React from 'react';
|
||||||
|
|
||||||
import { Badge } from './Badge';
|
|
||||||
import { Box } from './primitives/Box';
|
import { Box } from './primitives/Box';
|
||||||
|
import { Stack } from './primitives/Stack';
|
||||||
|
import { Surface } from './primitives/Surface';
|
||||||
import { Text } from './Text';
|
import { Text } from './Text';
|
||||||
|
|
||||||
interface Tab {
|
interface Tab {
|
||||||
id: string;
|
id: string;
|
||||||
label: string;
|
label: string;
|
||||||
count?: number;
|
icon?: React.ReactNode;
|
||||||
countVariant?: 'default' | 'primary' | 'success' | 'warning' | 'danger' | 'info';
|
|
||||||
}
|
}
|
||||||
|
|
||||||
interface BorderTabsProps {
|
interface BorderTabsProps {
|
||||||
tabs: Tab[];
|
tabs: Tab[];
|
||||||
activeTab: string;
|
activeTab: string;
|
||||||
onTabChange: (tabId: string) => void;
|
onTabChange: (tabId: string) => void;
|
||||||
|
className?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function BorderTabs({ tabs, activeTab, onTabChange }: BorderTabsProps) {
|
export function BorderTabs({ tabs, activeTab, onTabChange, className = '' }: BorderTabsProps) {
|
||||||
return (
|
return (
|
||||||
<Box borderBottom borderColor="border-charcoal-outline">
|
<Box borderBottom borderColor="border-border-gray/50" className={className}>
|
||||||
<Box display="flex" gap={4}>
|
<Stack direction="row" gap={8}>
|
||||||
{tabs.map((tab) => {
|
{tabs.map((tab) => {
|
||||||
const isActive = activeTab === tab.id;
|
const isActive = activeTab === tab.id;
|
||||||
return (
|
return (
|
||||||
<Box
|
<Surface
|
||||||
key={tab.id}
|
key={tab.id}
|
||||||
as="button"
|
as="button"
|
||||||
type="button"
|
|
||||||
onClick={() => onTabChange(tab.id)}
|
onClick={() => onTabChange(tab.id)}
|
||||||
pb={3}
|
variant="ghost"
|
||||||
px={1}
|
px={1}
|
||||||
cursor="pointer"
|
py={4}
|
||||||
transition
|
position="relative"
|
||||||
borderBottom={isActive}
|
|
||||||
borderColor={isActive ? 'border-primary-blue' : ''}
|
borderColor={isActive ? 'border-primary-blue' : ''}
|
||||||
style={{
|
borderBottom={isActive}
|
||||||
borderBottomWidth: isActive ? '2px' : '0',
|
borderWidth={isActive ? '2px' : '0'}
|
||||||
marginBottom: '-1px'
|
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
|
<Text
|
||||||
size="sm"
|
size="sm"
|
||||||
weight="medium"
|
weight="medium"
|
||||||
color={isActive ? 'text-primary-blue' : 'text-gray-400'}
|
color={isActive ? 'text-primary-blue' : 'text-gray-400'}
|
||||||
className={!isActive ? 'hover:text-white' : ''}
|
groupHoverTextColor={!isActive ? 'white' : undefined}
|
||||||
>
|
>
|
||||||
{tab.label}
|
{tab.label}
|
||||||
</Text>
|
</Text>
|
||||||
{tab.count !== undefined && tab.count > 0 && (
|
</Stack>
|
||||||
<Badge variant={tab.countVariant || 'warning'}>
|
</Surface>
|
||||||
{tab.count}
|
|
||||||
</Badge>
|
|
||||||
)}
|
|
||||||
</Box>
|
|
||||||
</Box>
|
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
</Box>
|
</Stack>
|
||||||
</Box>
|
</Box>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import { Box, BoxProps } from './primitives/Box';
|
|||||||
import { Loader2 } from 'lucide-react';
|
import { Loader2 } from 'lucide-react';
|
||||||
import { Icon } from './Icon';
|
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;
|
children: ReactNode;
|
||||||
onClick?: MouseEventHandler<HTMLButtonElement>;
|
onClick?: MouseEventHandler<HTMLButtonElement>;
|
||||||
className?: string;
|
className?: string;
|
||||||
@@ -19,6 +19,8 @@ interface ButtonProps extends Omit<ButtonHTMLAttributes<HTMLButtonElement>, 'as'
|
|||||||
href?: string;
|
href?: string;
|
||||||
target?: string;
|
target?: string;
|
||||||
rel?: string;
|
rel?: string;
|
||||||
|
fontSize?: string;
|
||||||
|
backgroundColor?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const Button = forwardRef<HTMLButtonElement, ButtonProps>(({
|
export const Button = forwardRef<HTMLButtonElement, ButtonProps>(({
|
||||||
@@ -36,6 +38,8 @@ export const Button = forwardRef<HTMLButtonElement, ButtonProps>(({
|
|||||||
href,
|
href,
|
||||||
target,
|
target,
|
||||||
rel,
|
rel,
|
||||||
|
fontSize,
|
||||||
|
backgroundColor,
|
||||||
...props
|
...props
|
||||||
}, ref) => {
|
}, 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';
|
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}
|
target={target}
|
||||||
rel={rel}
|
rel={rel}
|
||||||
className={classes}
|
className={classes}
|
||||||
|
fontSize={fontSize}
|
||||||
|
backgroundColor={backgroundColor}
|
||||||
{...props}
|
{...props}
|
||||||
>
|
>
|
||||||
{content}
|
{content}
|
||||||
@@ -98,6 +104,8 @@ export const Button = forwardRef<HTMLButtonElement, ButtonProps>(({
|
|||||||
className={classes}
|
className={classes}
|
||||||
onClick={onClick}
|
onClick={onClick}
|
||||||
disabled={disabled || isLoading}
|
disabled={disabled || isLoading}
|
||||||
|
fontSize={fontSize}
|
||||||
|
backgroundColor={backgroundColor}
|
||||||
{...props}
|
{...props}
|
||||||
>
|
>
|
||||||
{content}
|
{content}
|
||||||
|
|||||||
@@ -1,26 +1,10 @@
|
|||||||
import React, { ReactNode, MouseEventHandler } from 'react';
|
import React, { ReactNode, MouseEventHandler } from 'react';
|
||||||
import { Box, BoxProps } from './primitives/Box';
|
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;
|
export interface CardProps extends Omit<BoxProps<'div'>, 'children' | 'onClick'> {
|
||||||
|
|
||||||
interface ResponsiveSpacing {
|
|
||||||
base?: Spacing;
|
|
||||||
md?: Spacing;
|
|
||||||
lg?: Spacing;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface CardProps extends Omit<BoxProps<'div'>, 'children' | 'className' | 'onClick'> {
|
|
||||||
children: ReactNode;
|
children: ReactNode;
|
||||||
className?: string;
|
|
||||||
onClick?: MouseEventHandler<HTMLDivElement>;
|
onClick?: MouseEventHandler<HTMLDivElement>;
|
||||||
variant?: 'default' | 'outline' | 'ghost';
|
variant?: 'default' | 'outline' | 'ghost' | 'muted' | 'dark' | 'glass';
|
||||||
p?: Spacing | ResponsiveSpacing;
|
|
||||||
px?: Spacing | ResponsiveSpacing;
|
|
||||||
py?: Spacing | ResponsiveSpacing;
|
|
||||||
pt?: Spacing | ResponsiveSpacing;
|
|
||||||
pb?: Spacing | ResponsiveSpacing;
|
|
||||||
pl?: Spacing | ResponsiveSpacing;
|
|
||||||
pr?: Spacing | ResponsiveSpacing;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function Card({
|
export function Card({
|
||||||
@@ -35,7 +19,10 @@ export function Card({
|
|||||||
const variantClasses = {
|
const variantClasses = {
|
||||||
default: 'bg-panel-gray border border-border-gray shadow-card',
|
default: 'bg-panel-gray border border-border-gray shadow-card',
|
||||||
outline: 'bg-transparent border border-border-gray',
|
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 = [
|
const classes = [
|
||||||
|
|||||||
@@ -1,35 +1,49 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { Box } from './primitives/Box';
|
import { Box } from './primitives/Box';
|
||||||
import { Text } from './Text';
|
import { Text } from './Text';
|
||||||
import { ProgressBar } from './ProgressBar';
|
import { Icon } from './Icon';
|
||||||
|
import { LucideIcon } from 'lucide-react';
|
||||||
|
|
||||||
interface CategoryDistributionCardProps {
|
interface CategoryDistributionCardProps {
|
||||||
label: string;
|
label: string;
|
||||||
count: number;
|
count: number;
|
||||||
percentage: number;
|
percentage: number;
|
||||||
|
icon: LucideIcon;
|
||||||
color: string;
|
color: string;
|
||||||
bgColor: string;
|
bgColor: string;
|
||||||
borderColor: string;
|
borderColor: string;
|
||||||
progressColor: string;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function CategoryDistributionCard({
|
export function CategoryDistributionCard({
|
||||||
label,
|
label,
|
||||||
count,
|
count,
|
||||||
percentage,
|
percentage,
|
||||||
|
icon,
|
||||||
color,
|
color,
|
||||||
bgColor,
|
bgColor,
|
||||||
borderColor,
|
borderColor,
|
||||||
progressColor,
|
|
||||||
}: CategoryDistributionCardProps) {
|
}: CategoryDistributionCardProps) {
|
||||||
return (
|
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}>
|
<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>
|
</Box>
|
||||||
<Text color="text-white" weight="medium" block mb={1}>{label}</Text>
|
<Text size="sm" weight="medium" color="text-white" block mb={1}>
|
||||||
<ProgressBar value={percentage} max={100} color={progressColor} bg="bg-deep-graphite/50" />
|
{label}
|
||||||
<Text size="xs" color="text-gray-500" block mt={1}>{percentage}% of drivers</Text>
|
</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>
|
</Box>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,3 @@
|
|||||||
|
|
||||||
|
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { Box } from './primitives/Box';
|
import { Box } from './primitives/Box';
|
||||||
import { Text } from './Text';
|
import { Text } from './Text';
|
||||||
@@ -26,7 +24,8 @@ export function Checkbox({ label, checked, onChange, disabled }: CheckboxProps)
|
|||||||
border
|
border
|
||||||
borderColor="border-charcoal-outline"
|
borderColor="border-charcoal-outline"
|
||||||
rounded="sm"
|
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>
|
<Text size="sm" color={disabled ? 'text-gray-500' : 'text-white'}>{label}</Text>
|
||||||
</Box>
|
</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;
|
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;
|
children: ReactNode;
|
||||||
size?: 'sm' | 'md' | 'lg' | 'xl' | 'full';
|
size?: 'sm' | 'md' | 'lg' | 'xl' | 'full';
|
||||||
padding?: boolean;
|
padding?: boolean;
|
||||||
|
|||||||
@@ -1,142 +1,146 @@
|
|||||||
import React, { ReactNode } from 'react';
|
import React from 'react';
|
||||||
import { Box } from './primitives/Box';
|
import { Box } from './primitives/Box';
|
||||||
import { Heading } from './Heading';
|
import { Stack } from './primitives/Stack';
|
||||||
import { Image } from './Image';
|
|
||||||
import { Text } from './Text';
|
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 {
|
interface DashboardHeroProps {
|
||||||
driverName: string;
|
driverName: string;
|
||||||
avatarUrl: string;
|
avatarUrl?: string | null;
|
||||||
country: string;
|
rating: number;
|
||||||
rating: string | number;
|
rank: number;
|
||||||
rank: string | number;
|
totalRaces: number;
|
||||||
totalRaces: string | number;
|
winRate: number;
|
||||||
actions?: ReactNode;
|
|
||||||
stats?: ReactNode;
|
|
||||||
className?: string;
|
className?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* DashboardHero
|
|
||||||
*
|
|
||||||
* Redesigned for "Precision Racing Minimal" theme.
|
|
||||||
* Uses subtle accent glows and crisp separators.
|
|
||||||
*/
|
|
||||||
export function DashboardHero({
|
export function DashboardHero({
|
||||||
driverName,
|
driverName,
|
||||||
avatarUrl,
|
avatarUrl,
|
||||||
country,
|
|
||||||
rating,
|
rating,
|
||||||
rank,
|
rank,
|
||||||
totalRaces,
|
totalRaces,
|
||||||
actions,
|
winRate,
|
||||||
stats,
|
|
||||||
className = '',
|
className = '',
|
||||||
}: DashboardHeroProps) {
|
}: DashboardHeroProps) {
|
||||||
return (
|
return (
|
||||||
<Box
|
<Box
|
||||||
as="section"
|
position="relative"
|
||||||
position="relative"
|
bg="bg-[#0C0D0F]"
|
||||||
className={`bg-[#0C0D0F] border-b border-[#23272B] overflow-hidden ${className}`}
|
borderBottom
|
||||||
|
borderColor="border-[#23272B]"
|
||||||
|
overflow="hidden"
|
||||||
|
className={className}
|
||||||
>
|
>
|
||||||
{/* Subtle Accent Glow */}
|
{/* Background Glow */}
|
||||||
<Glow
|
<Box
|
||||||
position="top-right"
|
position="absolute"
|
||||||
color="primary"
|
top={-100}
|
||||||
opacity={0.1}
|
right={-100}
|
||||||
size="xl"
|
w="500px"
|
||||||
|
h="500px"
|
||||||
|
bg="bg-primary-blue/10"
|
||||||
|
rounded="full"
|
||||||
|
blur="3xl"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<Box
|
<Box p={{ base: 6, md: 10 }} position="relative" zIndex={10}>
|
||||||
maxWidth="80rem"
|
<Stack direction={{ base: 'col', md: 'row' }} align="center" gap={8}>
|
||||||
mx="auto"
|
{/* Avatar Section */}
|
||||||
px={6}
|
<Box position="relative">
|
||||||
py={8}
|
<Box
|
||||||
position="relative"
|
p={1}
|
||||||
zIndex={1}
|
rounded="2xl"
|
||||||
>
|
bg="bg-[#141619]"
|
||||||
<Box display="flex" flexDirection="col" gap={8}>
|
border
|
||||||
<Box display="flex" align="center" justify="between" wrap gap={6}>
|
borderColor="border-[#23272B]"
|
||||||
{/* Driver Identity */}
|
>
|
||||||
<Box display="flex" align="center" gap={6}>
|
<Avatar
|
||||||
<Box position="relative">
|
src={avatarUrl}
|
||||||
<Box
|
alt={driverName}
|
||||||
w="24"
|
size={120}
|
||||||
h="24"
|
className="rounded-xl"
|
||||||
className="border border-[#23272B] p-1 bg-[#141619]"
|
/>
|
||||||
>
|
</Box>
|
||||||
<Image
|
<Box
|
||||||
src={avatarUrl}
|
position="absolute"
|
||||||
alt={driverName}
|
bottom={-2}
|
||||||
width={96}
|
right={-2}
|
||||||
height={96}
|
w="10"
|
||||||
className="w-full h-full object-cover grayscale hover:grayscale-0 transition-all duration-300"
|
h="10"
|
||||||
/>
|
rounded="xl"
|
||||||
</Box>
|
bg="bg-[#4ED4E0]"
|
||||||
<Box
|
borderWidth="2px"
|
||||||
position="absolute"
|
borderStyle="solid"
|
||||||
bottom="-1"
|
borderColor="border-[#0C0D0F]"
|
||||||
right="-1"
|
display="flex"
|
||||||
w="4"
|
center
|
||||||
h="4"
|
>
|
||||||
className="bg-[#4ED4E0] border-2 border-[#0C0D0F]"
|
<Icon icon={Star} size={5} color="#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>
|
</Box>
|
||||||
|
|
||||||
{/* Actions */}
|
|
||||||
{actions && (
|
|
||||||
<Box display="flex" gap={3}>
|
|
||||||
{actions}
|
|
||||||
</Box>
|
|
||||||
)}
|
|
||||||
</Box>
|
</Box>
|
||||||
|
|
||||||
{/* Stats Grid */}
|
{/* Info Section */}
|
||||||
{stats && (
|
<Stack flex={1} align={{ base: 'center', md: 'start' }} gap={4}>
|
||||||
<Box
|
<Box>
|
||||||
display="grid"
|
<Heading level={1} uppercase letterSpacing="tighter" mb={2}>
|
||||||
gridCols={2}
|
{driverName}
|
||||||
responsiveGridCols={{ md: 4 }}
|
</Heading>
|
||||||
gap={4}
|
<Stack direction="row" gap={4}>
|
||||||
className="border-t border-[#23272B]/50 pt-6"
|
<Stack gap={0.5}>
|
||||||
>
|
<Text size="xs" color="text-gray-500" uppercase>Rating</Text>
|
||||||
{stats}
|
<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>
|
||||||
)}
|
|
||||||
</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>
|
||||||
</Box>
|
</Box>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,36 +1,63 @@
|
|||||||
|
import React from 'react';
|
||||||
|
|
||||||
import { Box } from './primitives/Box';
|
import { Box } from './primitives/Box';
|
||||||
import { Surface } from './primitives/Surface';
|
import { Stack } from './primitives/Stack';
|
||||||
import { Text } from './Text';
|
import { Text } from './Text';
|
||||||
|
import { Surface } from './primitives/Surface';
|
||||||
|
import { Icon } from './Icon';
|
||||||
|
import { AlertCircle, XCircle, Info, AlertTriangle } from 'lucide-react';
|
||||||
|
|
||||||
export interface ErrorBannerProps {
|
interface ErrorBannerProps {
|
||||||
message: string;
|
|
||||||
title?: string;
|
title?: string;
|
||||||
variant?: 'error' | 'warning' | 'info';
|
message: string;
|
||||||
|
variant?: 'error' | 'warning' | 'info' | 'success';
|
||||||
}
|
}
|
||||||
|
|
||||||
export function ErrorBanner({ message, title, variant = 'error' }: ErrorBannerProps) {
|
export function ErrorBanner({ title, message, variant = 'error' }: ErrorBannerProps) {
|
||||||
const variantColors = {
|
const configs = {
|
||||||
error: { bg: 'rgba(239, 68, 68, 0.1)', border: '#ef4444', text: '#ef4444' },
|
error: {
|
||||||
warning: { bg: 'rgba(245, 158, 11, 0.1)', border: '#f59e0b', text: '#fcd34d' },
|
bg: 'rgba(239, 68, 68, 0.1)',
|
||||||
info: { bg: 'rgba(59, 130, 246, 0.1)', border: '#3b82f6', text: '#3b82f6' },
|
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 (
|
return (
|
||||||
<Surface
|
<Surface
|
||||||
variant="muted"
|
variant="muted"
|
||||||
rounded="lg"
|
rounded="xl"
|
||||||
border
|
border
|
||||||
padding={4}
|
p={4}
|
||||||
style={{ backgroundColor: colors.bg, borderColor: colors.border }}
|
backgroundColor={colors.bg}
|
||||||
|
borderColor={colors.border}
|
||||||
>
|
>
|
||||||
<Box style={{ flex: 1 }}>
|
<Stack direction="row" align="start" gap={3}>
|
||||||
{title && <Text weight="medium" style={{ color: colors.text }} block mb={1}>{title}</Text>}
|
<Icon icon={colors.icon} size={5} color={colors.text} />
|
||||||
<Text size="sm" style={{ color: colors.text, opacity: 0.9 }} block>{message}</Text>
|
<Box flex={1}>
|
||||||
</Box>
|
{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>
|
</Surface>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -20,7 +20,11 @@ export function FormSection({ children, title }: FormSectionProps) {
|
|||||||
size="xs"
|
size="xs"
|
||||||
weight="bold"
|
weight="bold"
|
||||||
color="text-gray-500"
|
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}
|
{title}
|
||||||
</Text>
|
</Text>
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import React, { ReactNode, ElementType } from 'react';
|
import React, { ReactNode, ElementType } from 'react';
|
||||||
import { Stack } from './primitives/Stack';
|
import { Stack } from './primitives/Stack';
|
||||||
import { Box, BoxProps } from './primitives/Box';
|
import { Box, BoxProps, ResponsiveValue } from './primitives/Box';
|
||||||
|
|
||||||
interface ResponsiveFontSize {
|
interface ResponsiveFontSize {
|
||||||
base?: string;
|
base?: string;
|
||||||
@@ -18,11 +18,13 @@ interface HeadingProps extends Omit<BoxProps<'h1'>, 'children' | 'as' | 'fontSiz
|
|||||||
id?: string;
|
id?: string;
|
||||||
groupHoverColor?: string;
|
groupHoverColor?: string;
|
||||||
truncate?: boolean;
|
truncate?: boolean;
|
||||||
|
uppercase?: boolean;
|
||||||
fontSize?: string | ResponsiveFontSize;
|
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 Tag = `h${level}` as ElementType;
|
||||||
|
|
||||||
const levelClasses = {
|
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',
|
6: 'text-xs font-bold text-white tracking-tight uppercase tracking-widest',
|
||||||
};
|
};
|
||||||
|
|
||||||
const weightClasses = {
|
const weightClasses: Record<string, string> = {
|
||||||
light: 'font-light',
|
light: 'font-light',
|
||||||
normal: 'font-normal',
|
normal: 'font-normal',
|
||||||
medium: 'font-medium',
|
medium: 'font-medium',
|
||||||
@@ -67,14 +69,24 @@ export function Heading({ level, children, icon, groupHoverColor, truncate, font
|
|||||||
const classes = [
|
const classes = [
|
||||||
levelClasses[level],
|
levelClasses[level],
|
||||||
getFontSizeClasses(fontSize),
|
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}` : '',
|
groupHoverColor ? `group-hover:text-${groupHoverColor}` : '',
|
||||||
truncate ? 'truncate' : '',
|
truncate ? 'truncate' : '',
|
||||||
props.className
|
props.className
|
||||||
].filter(Boolean).join(' ');
|
].filter(Boolean).join(' ');
|
||||||
|
|
||||||
return (
|
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}
|
{content}
|
||||||
</Box>
|
</Box>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,52 +1,46 @@
|
|||||||
|
import React from 'react';
|
||||||
|
|
||||||
import { ReactNode } from 'react';
|
|
||||||
import { Box } from './primitives/Box';
|
import { Box } from './primitives/Box';
|
||||||
import { Card } from './Card';
|
|
||||||
import { Stack } from './primitives/Stack';
|
import { Stack } from './primitives/Stack';
|
||||||
import { Surface } from './primitives/Surface';
|
|
||||||
import { Text } from './Text';
|
import { Text } from './Text';
|
||||||
|
import { Surface } from './primitives/Surface';
|
||||||
|
import { Icon } from './Icon';
|
||||||
|
import { LucideIcon } from 'lucide-react';
|
||||||
|
|
||||||
interface HorizontalStatCardProps {
|
interface HorizontalStatCardProps {
|
||||||
label: string;
|
label: string;
|
||||||
value: string | number;
|
value: string | number;
|
||||||
subValue?: string;
|
icon: LucideIcon;
|
||||||
icon: ReactNode;
|
iconColor?: string;
|
||||||
iconBgColor?: string;
|
iconBgColor?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function HorizontalStatCard({
|
export function HorizontalStatCard({
|
||||||
label,
|
label,
|
||||||
value,
|
value,
|
||||||
subValue,
|
|
||||||
icon,
|
icon,
|
||||||
iconBgColor,
|
iconColor = 'text-primary-blue',
|
||||||
|
iconBgColor = 'rgba(59, 130, 246, 0.1)',
|
||||||
}: HorizontalStatCardProps) {
|
}: HorizontalStatCardProps) {
|
||||||
return (
|
return (
|
||||||
<Card>
|
<Surface variant="muted" rounded="xl" border p={4}>
|
||||||
<Stack direction="row" align="center" gap={3}>
|
<Stack direction="row" align="center" gap={4}>
|
||||||
<Surface
|
<Surface
|
||||||
variant="muted"
|
variant="muted"
|
||||||
rounded="full"
|
rounded="lg"
|
||||||
padding={3}
|
p={3}
|
||||||
style={{ backgroundColor: iconBgColor }}
|
backgroundColor={iconBgColor}
|
||||||
>
|
>
|
||||||
{icon}
|
<Icon icon={icon} size={5} color={iconColor} />
|
||||||
</Surface>
|
</Surface>
|
||||||
<Box>
|
<Box>
|
||||||
<Text size="xs" color="text-gray-400" block mb={1}>
|
<Text size="xs" color="text-gray-500" uppercase letterSpacing="wider" block>
|
||||||
{label}
|
{label}
|
||||||
</Text>
|
</Text>
|
||||||
<Text size="2xl" weight="bold" color="text-white" block>
|
<Text size="xl" weight="bold" color="text-white" block>
|
||||||
{value}
|
{value}
|
||||||
</Text>
|
</Text>
|
||||||
{subValue && (
|
|
||||||
<Text size="sm" color="text-gray-400">
|
|
||||||
{subValue}
|
|
||||||
</Text>
|
|
||||||
)}
|
|
||||||
</Box>
|
</Box>
|
||||||
</Stack>
|
</Stack>
|
||||||
</Card>
|
</Surface>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,14 +2,29 @@ import React from 'react';
|
|||||||
import { LucideIcon } from 'lucide-react';
|
import { LucideIcon } from 'lucide-react';
|
||||||
import { Box, BoxProps } from './primitives/Box';
|
import { Box, BoxProps } from './primitives/Box';
|
||||||
|
|
||||||
interface IconProps extends Omit<BoxProps<'svg'>, 'children' | 'as'> {
|
export interface IconProps extends Omit<BoxProps<'div'>, 'children'> {
|
||||||
icon: LucideIcon;
|
icon: LucideIcon | React.ReactNode;
|
||||||
size?: number | string;
|
size?: number | string;
|
||||||
color?: string;
|
color?: string;
|
||||||
strokeWidth?: number;
|
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> = {
|
const sizeMap: Record<string | number, string> = {
|
||||||
3: 'w-3 h-3',
|
3: 'w-3 h-3',
|
||||||
3.5: 'w-3.5 h-3.5',
|
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 combinedStyle = color && !isTailwindColor ? { color, ...style } : style;
|
||||||
const boxColor = isTailwindColor ? color : undefined;
|
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 (
|
return (
|
||||||
<Box
|
<Box
|
||||||
as={LucideIcon}
|
className={classes}
|
||||||
className={`${sizeClass} ${className}`}
|
|
||||||
style={combinedStyle}
|
style={combinedStyle}
|
||||||
color={boxColor}
|
color={boxColor}
|
||||||
|
display="inline-flex"
|
||||||
|
alignItems="center"
|
||||||
|
justifyContent="center"
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
>
|
||||||
|
{renderIcon()}
|
||||||
|
</Box>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,3 @@
|
|||||||
|
|
||||||
|
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { LucideIcon } from 'lucide-react';
|
import { LucideIcon } from 'lucide-react';
|
||||||
import { Button } from './Button';
|
import { Button } from './Button';
|
||||||
@@ -29,9 +27,9 @@ export function IconButton({
|
|||||||
backgroundColor,
|
backgroundColor,
|
||||||
}: IconButtonProps) {
|
}: IconButtonProps) {
|
||||||
const sizeMap = {
|
const sizeMap = {
|
||||||
sm: { btn: 'w-8 h-8 p-0', icon: 4 },
|
sm: { w: '8', h: '8', icon: 4 },
|
||||||
md: { btn: 'w-10 h-10 p-0', icon: 5 },
|
md: { w: '10', h: '10', icon: 5 },
|
||||||
lg: { btn: 'w-12 h-12 p-0', icon: 6 },
|
lg: { w: '12', h: '12', icon: 6 },
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -40,7 +38,14 @@ export function IconButton({
|
|||||||
onClick={onClick}
|
onClick={onClick}
|
||||||
title={title}
|
title={title}
|
||||||
disabled={disabled}
|
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}
|
backgroundColor={backgroundColor}
|
||||||
>
|
>
|
||||||
<Icon icon={icon} size={sizeMap[size].icon} color={color} />
|
<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 React from 'react';
|
||||||
import { Box } from './primitives/Box';
|
import { Box } from './primitives/Box';
|
||||||
import { Icon } from './Icon';
|
|
||||||
import { Stack } from './primitives/Stack';
|
import { Stack } from './primitives/Stack';
|
||||||
import { Surface } from './primitives/Surface';
|
|
||||||
import { Text } from './Text';
|
import { Text } from './Text';
|
||||||
|
import { Surface } from './primitives/Surface';
|
||||||
type BannerType = 'info' | 'warning' | 'success' | 'error';
|
import { Icon } from './Icon';
|
||||||
|
import { Info, AlertTriangle, AlertCircle, CheckCircle, LucideIcon } from 'lucide-react';
|
||||||
|
|
||||||
interface InfoBannerProps {
|
interface InfoBannerProps {
|
||||||
type?: BannerType;
|
|
||||||
title?: string;
|
title?: string;
|
||||||
children: React.ReactNode;
|
message?: string;
|
||||||
|
children?: React.ReactNode;
|
||||||
|
variant?: 'info' | 'warning' | 'error' | 'success';
|
||||||
|
type?: 'info' | 'warning' | 'error' | 'success';
|
||||||
icon?: LucideIcon;
|
icon?: LucideIcon;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function InfoBanner({
|
export function InfoBanner({ title, message, children, variant = 'info', type, icon }: InfoBannerProps) {
|
||||||
type = 'info',
|
const configs = {
|
||||||
title,
|
|
||||||
children,
|
|
||||||
icon: CustomIcon,
|
|
||||||
}: InfoBannerProps) {
|
|
||||||
const bannerConfig: Record<BannerType, {
|
|
||||||
icon: LucideIcon;
|
|
||||||
bg: string;
|
|
||||||
border: string;
|
|
||||||
titleColor: string;
|
|
||||||
iconColor: string;
|
|
||||||
}> = {
|
|
||||||
info: {
|
info: {
|
||||||
icon: Info,
|
bg: 'rgba(59, 130, 246, 0.1)',
|
||||||
bg: 'rgba(38, 38, 38, 0.3)',
|
border: 'rgba(59, 130, 246, 0.2)',
|
||||||
border: 'rgba(38, 38, 38, 0.5)',
|
iconColor: 'rgb(96, 165, 250)',
|
||||||
titleColor: 'text-gray-300',
|
icon: Info
|
||||||
iconColor: '#9ca3af',
|
|
||||||
},
|
},
|
||||||
warning: {
|
warning: {
|
||||||
icon: AlertTriangle,
|
|
||||||
bg: 'rgba(245, 158, 11, 0.1)',
|
bg: 'rgba(245, 158, 11, 0.1)',
|
||||||
border: 'rgba(245, 158, 11, 0.3)',
|
border: 'rgba(245, 158, 11, 0.2)',
|
||||||
titleColor: 'text-warning-amber',
|
iconColor: 'rgb(251, 191, 36)',
|
||||||
iconColor: '#f59e0b',
|
icon: AlertTriangle
|
||||||
},
|
|
||||||
success: {
|
|
||||||
icon: CheckCircle,
|
|
||||||
bg: 'rgba(16, 185, 129, 0.1)',
|
|
||||||
border: 'rgba(16, 185, 129, 0.3)',
|
|
||||||
titleColor: 'text-performance-green',
|
|
||||||
iconColor: '#10b981',
|
|
||||||
},
|
},
|
||||||
error: {
|
error: {
|
||||||
icon: XCircle,
|
|
||||||
bg: 'rgba(239, 68, 68, 0.1)',
|
bg: 'rgba(239, 68, 68, 0.1)',
|
||||||
border: 'rgba(239, 68, 68, 0.3)',
|
border: 'rgba(239, 68, 68, 0.2)',
|
||||||
titleColor: 'text-error-red',
|
iconColor: 'rgb(248, 113, 113)',
|
||||||
iconColor: '#ef4444',
|
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 activeVariant = type || variant;
|
||||||
const BannerIcon = CustomIcon || config.icon;
|
const config = configs[activeVariant as keyof typeof configs] || configs.info;
|
||||||
|
const BannerIcon = icon || config.icon;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Surface
|
<Surface
|
||||||
variant="muted"
|
variant="muted"
|
||||||
rounded="lg"
|
rounded="xl"
|
||||||
border
|
border
|
||||||
padding={4}
|
p={4}
|
||||||
style={{ backgroundColor: config.bg, borderColor: config.border }}
|
backgroundColor={config.bg}
|
||||||
|
borderColor={config.border}
|
||||||
>
|
>
|
||||||
<Stack direction="row" align="start" gap={3}>
|
<Stack direction="row" align="start" gap={3}>
|
||||||
<Icon icon={BannerIcon} size={5} color={config.iconColor} />
|
<Icon icon={BannerIcon} size={5} color={config.iconColor} />
|
||||||
<Box style={{ flex: 1 }}>
|
<Box flex={1}>
|
||||||
{title && (
|
{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>
|
</Box>
|
||||||
</Stack>
|
</Stack>
|
||||||
</Surface>
|
</Surface>
|
||||||
|
|||||||
@@ -1,62 +1,63 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { Surface } from './primitives/Surface';
|
|
||||||
import { Stack } from './primitives/Stack';
|
|
||||||
import { Box } from './primitives/Box';
|
import { Box } from './primitives/Box';
|
||||||
import { Icon } from './Icon';
|
import { Stack } from './primitives/Stack';
|
||||||
import { Text } from './Text';
|
import { Text } from './Text';
|
||||||
|
import { Surface } from './primitives/Surface';
|
||||||
|
import { Icon } from './Icon';
|
||||||
import { LucideIcon } from 'lucide-react';
|
import { LucideIcon } from 'lucide-react';
|
||||||
|
|
||||||
interface InfoBoxProps {
|
interface InfoBoxProps {
|
||||||
icon: LucideIcon;
|
|
||||||
title: string;
|
title: string;
|
||||||
description: string;
|
description: string;
|
||||||
variant?: 'primary' | 'success' | 'warning' | 'default';
|
icon: LucideIcon;
|
||||||
|
variant?: 'info' | 'warning' | 'error' | 'success';
|
||||||
}
|
}
|
||||||
|
|
||||||
export function InfoBox({ icon, title, description, variant = 'default' }: InfoBoxProps) {
|
export function InfoBox({ title, description, icon, variant = 'info' }: InfoBoxProps) {
|
||||||
const variantColors = {
|
const configs = {
|
||||||
primary: {
|
info: {
|
||||||
bg: 'rgba(59, 130, 246, 0.1)',
|
bg: 'rgba(59, 130, 246, 0.1)',
|
||||||
border: '#3b82f6',
|
border: 'rgba(59, 130, 246, 0.2)',
|
||||||
text: '#3b82f6',
|
icon: 'rgb(96, 165, 250)',
|
||||||
icon: '#3b82f6'
|
text: 'text-white'
|
||||||
},
|
|
||||||
success: {
|
|
||||||
bg: 'rgba(16, 185, 129, 0.1)',
|
|
||||||
border: '#10b981',
|
|
||||||
text: '#10b981',
|
|
||||||
icon: '#10b981'
|
|
||||||
},
|
},
|
||||||
warning: {
|
warning: {
|
||||||
bg: 'rgba(245, 158, 11, 0.1)',
|
bg: 'rgba(245, 158, 11, 0.1)',
|
||||||
border: '#f59e0b',
|
border: 'rgba(245, 158, 11, 0.2)',
|
||||||
text: '#f59e0b',
|
icon: 'rgb(251, 191, 36)',
|
||||||
icon: '#f59e0b'
|
text: 'text-white'
|
||||||
},
|
},
|
||||||
default: {
|
error: {
|
||||||
bg: 'rgba(38, 38, 38, 0.3)',
|
bg: 'rgba(239, 68, 68, 0.1)',
|
||||||
border: '#262626',
|
border: 'rgba(239, 68, 68, 0.2)',
|
||||||
text: 'white',
|
icon: 'rgb(248, 113, 113)',
|
||||||
icon: '#9ca3af'
|
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 (
|
return (
|
||||||
<Surface
|
<Surface
|
||||||
variant="muted"
|
variant="muted"
|
||||||
rounded="xl"
|
rounded="xl"
|
||||||
border
|
border
|
||||||
padding={4}
|
p={4}
|
||||||
style={{ backgroundColor: colors.bg, borderColor: colors.border }}
|
backgroundColor={colors.bg}
|
||||||
|
borderColor={colors.border}
|
||||||
>
|
>
|
||||||
<Stack direction="row" align="start" gap={3}>
|
<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} />
|
<Icon icon={icon} size={5} color={colors.icon} />
|
||||||
</Surface>
|
</Surface>
|
||||||
<Box>
|
<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>
|
<Text size="sm" color="text-gray-400" block mt={1}>{description}</Text>
|
||||||
</Box>
|
</Box>
|
||||||
</Stack>
|
</Stack>
|
||||||
|
|||||||
@@ -1,26 +1,30 @@
|
|||||||
import React, { forwardRef, InputHTMLAttributes } from 'react';
|
import React, { forwardRef, ReactNode } from 'react';
|
||||||
import { Text } from './Text';
|
|
||||||
import { Box } from './primitives/Box';
|
import { Box } from './primitives/Box';
|
||||||
import { Stack } from './primitives/Stack';
|
import { Stack } from './primitives/Stack';
|
||||||
|
import { Text } from './Text';
|
||||||
|
|
||||||
interface InputProps extends InputHTMLAttributes<HTMLInputElement> {
|
interface InputProps extends React.InputHTMLAttributes<HTMLInputElement> {
|
||||||
variant?: 'default' | 'error';
|
label?: string;
|
||||||
|
icon?: ReactNode;
|
||||||
errorMessage?: string;
|
errorMessage?: string;
|
||||||
icon?: React.ReactNode;
|
variant?: 'default' | 'error';
|
||||||
label?: React.ReactNode;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export const Input = forwardRef<HTMLInputElement, InputProps>(
|
export const Input = forwardRef<HTMLInputElement, InputProps>(
|
||||||
({ className = '', variant = 'default', errorMessage, icon, label, ...props }, ref) => {
|
({ label, icon, errorMessage, variant = 'default', className = '', ...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 isError = variant === 'error' || !!errorMessage;
|
||||||
const variantClasses = (variant === 'error' || errorMessage) ? 'border-critical-red' : 'border-border-gray';
|
|
||||||
const iconClasses = icon ? 'pl-10' : '';
|
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 classes = `${baseClasses} ${variantClasses} ${iconClasses} ${className}`;
|
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 (
|
return (
|
||||||
<Stack gap={1.5} fullWidth>
|
<Stack gap={1.5} fullWidth>
|
||||||
{label && (
|
{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}
|
{label}
|
||||||
</Text>
|
</Text>
|
||||||
)}
|
)}
|
||||||
@@ -28,20 +32,21 @@ export const Input = forwardRef<HTMLInputElement, InputProps>(
|
|||||||
{icon && (
|
{icon && (
|
||||||
<Box
|
<Box
|
||||||
position="absolute"
|
position="absolute"
|
||||||
left="3"
|
left={0}
|
||||||
top="50%"
|
top="50%"
|
||||||
style={{ transform: 'translateY(-50%)' }}
|
translateY="-50%"
|
||||||
zIndex={10}
|
zIndex={10}
|
||||||
display="flex"
|
w="11"
|
||||||
center
|
display="flex"
|
||||||
className="text-gray-500"
|
center
|
||||||
|
color="text-gray-500"
|
||||||
>
|
>
|
||||||
{icon}
|
{icon}
|
||||||
</Box>
|
</Box>
|
||||||
)}
|
)}
|
||||||
<input ref={ref} className={classes} {...props} />
|
<input ref={ref} className={classes} {...props} />
|
||||||
{errorMessage && (
|
{errorMessage && (
|
||||||
<Text size="xs" color="text-critical-red" block mt={1}>
|
<Text size="xs" color="text-warning-amber" mt={1}>
|
||||||
{errorMessage}
|
{errorMessage}
|
||||||
</Text>
|
</Text>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -1,19 +1,19 @@
|
|||||||
import React, { ReactNode } from 'react';
|
import React, { ReactNode } from 'react';
|
||||||
import { Box, BoxProps } from './primitives/Box';
|
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;
|
href: string;
|
||||||
children: ReactNode;
|
children: ReactNode;
|
||||||
className?: string;
|
|
||||||
variant?: 'primary' | 'secondary' | 'ghost';
|
variant?: 'primary' | 'secondary' | 'ghost';
|
||||||
size?: 'xs' | 'sm' | 'md' | 'lg';
|
size?: 'xs' | 'sm' | 'md' | 'lg';
|
||||||
target?: '_blank' | '_self' | '_parent' | '_top';
|
target?: '_blank' | '_self' | '_parent' | '_top';
|
||||||
rel?: string;
|
rel?: string;
|
||||||
onClick?: React.MouseEventHandler<HTMLAnchorElement>;
|
onClick?: React.MouseEventHandler<HTMLAnchorElement>;
|
||||||
style?: React.CSSProperties;
|
|
||||||
block?: boolean;
|
block?: boolean;
|
||||||
weight?: 'light' | 'normal' | 'medium' | 'semibold' | 'bold';
|
weight?: 'light' | 'normal' | 'medium' | 'semibold' | 'bold' | string;
|
||||||
truncate?: boolean;
|
truncate?: boolean;
|
||||||
|
hoverColor?: string;
|
||||||
|
transition?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function Link({
|
export function Link({
|
||||||
@@ -25,10 +25,11 @@ export function Link({
|
|||||||
target = '_self',
|
target = '_self',
|
||||||
rel = '',
|
rel = '',
|
||||||
onClick,
|
onClick,
|
||||||
style,
|
|
||||||
block = false,
|
block = false,
|
||||||
weight,
|
weight,
|
||||||
truncate,
|
truncate,
|
||||||
|
hoverColor,
|
||||||
|
transition,
|
||||||
...props
|
...props
|
||||||
}: LinkProps) {
|
}: LinkProps) {
|
||||||
const baseClasses = 'inline-flex items-center transition-colors';
|
const baseClasses = 'inline-flex items-center transition-colors';
|
||||||
@@ -46,7 +47,7 @@ export function Link({
|
|||||||
lg: 'text-lg'
|
lg: 'text-lg'
|
||||||
};
|
};
|
||||||
|
|
||||||
const weightClasses = {
|
const weightClasses: Record<string, string> = {
|
||||||
light: 'font-light',
|
light: 'font-light',
|
||||||
normal: 'font-normal',
|
normal: 'font-normal',
|
||||||
medium: 'font-medium',
|
medium: 'font-medium',
|
||||||
@@ -58,8 +59,10 @@ export function Link({
|
|||||||
block ? 'flex' : baseClasses,
|
block ? 'flex' : baseClasses,
|
||||||
variantClasses[variant],
|
variantClasses[variant],
|
||||||
sizeClasses[size],
|
sizeClasses[size],
|
||||||
weight ? weightClasses[weight] : '',
|
weight && weightClasses[weight] ? weightClasses[weight] : '',
|
||||||
truncate ? 'truncate' : '',
|
truncate ? 'truncate' : '',
|
||||||
|
hoverColor ? `hover:${hoverColor}` : '',
|
||||||
|
transition ? 'transition-all duration-150' : '',
|
||||||
className
|
className
|
||||||
].filter(Boolean).join(' ');
|
].filter(Boolean).join(' ');
|
||||||
|
|
||||||
@@ -71,7 +74,10 @@ export function Link({
|
|||||||
target={target}
|
target={target}
|
||||||
rel={rel}
|
rel={rel}
|
||||||
onClick={onClick}
|
onClick={onClick}
|
||||||
style={style}
|
style={{
|
||||||
|
...(weight && !weightClasses[weight] ? { fontWeight: weight } : {}),
|
||||||
|
...(props.style || {})
|
||||||
|
}}
|
||||||
{...props}
|
{...props}
|
||||||
>
|
>
|
||||||
{children}
|
{children}
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
import { Box } from './primitives/Box';
|
||||||
|
|
||||||
interface LoadingSpinnerProps {
|
interface LoadingSpinnerProps {
|
||||||
size?: number;
|
size?: number;
|
||||||
@@ -7,19 +8,17 @@ interface LoadingSpinnerProps {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function LoadingSpinner({ size = 8, color = '#3b82f6', className = '' }: 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 (
|
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}`}
|
className={`animate-spin ${className}`}
|
||||||
style={style}
|
|
||||||
role="status"
|
role="status"
|
||||||
aria-label="Loading"
|
aria-label="Loading"
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -1,93 +1,100 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { Box } from './primitives/Box';
|
import { Box } from './primitives/Box';
|
||||||
import { Text } from './Text';
|
|
||||||
import { ImagePlaceholder } from './ImagePlaceholder';
|
|
||||||
import { Image } from './Image';
|
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 {
|
interface MediaPreviewCardProps {
|
||||||
src?: string;
|
type: 'image' | 'video';
|
||||||
alt?: string;
|
src: string;
|
||||||
|
alt: string;
|
||||||
title?: string;
|
title?: string;
|
||||||
subtitle?: string;
|
subtitle?: string;
|
||||||
|
onClick?: () => void;
|
||||||
aspectRatio?: string;
|
aspectRatio?: string;
|
||||||
isLoading?: boolean;
|
isLoading?: boolean;
|
||||||
error?: string;
|
|
||||||
onClick?: () => void;
|
|
||||||
className?: string;
|
className?: string;
|
||||||
actions?: React.ReactNode;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function MediaPreviewCard({
|
export function MediaPreviewCard({
|
||||||
|
type,
|
||||||
src,
|
src,
|
||||||
alt = 'Media preview',
|
alt,
|
||||||
title,
|
title,
|
||||||
subtitle,
|
|
||||||
aspectRatio = '16/9',
|
|
||||||
isLoading,
|
|
||||||
error,
|
|
||||||
onClick,
|
onClick,
|
||||||
|
aspectRatio = '16/9',
|
||||||
|
isLoading = false,
|
||||||
className = '',
|
className = '',
|
||||||
actions,
|
|
||||||
}: MediaPreviewCardProps) {
|
}: MediaPreviewCardProps) {
|
||||||
return (
|
return (
|
||||||
<Box
|
<Surface
|
||||||
display="flex"
|
variant="muted"
|
||||||
flexDirection="col"
|
rounded="xl"
|
||||||
bg="bg-charcoal-outline/10"
|
|
||||||
border
|
border
|
||||||
borderColor="border-charcoal-outline/50"
|
|
||||||
rounded="lg"
|
|
||||||
overflow="hidden"
|
overflow="hidden"
|
||||||
transition
|
cursor="pointer"
|
||||||
hoverScale={!!onClick}
|
|
||||||
cursor={onClick ? 'pointer' : 'default'}
|
|
||||||
onClick={onClick}
|
onClick={onClick}
|
||||||
className={`group ${className}`}
|
group
|
||||||
|
className={className}
|
||||||
>
|
>
|
||||||
<Box position="relative" width="full" style={{ aspectRatio }}>
|
<Box position="relative" w="full" aspectRatio={aspectRatio}>
|
||||||
{isLoading ? (
|
{isLoading ? (
|
||||||
<ImagePlaceholder variant="loading" aspectRatio={aspectRatio} rounded="none" />
|
<Box fullWidth fullHeight bg="bg-white/5" className="animate-pulse" />
|
||||||
) : error ? (
|
) : (
|
||||||
<ImagePlaceholder variant="error" message={error} aspectRatio={aspectRatio} rounded="none" />
|
|
||||||
) : src ? (
|
|
||||||
<Image
|
<Image
|
||||||
src={src}
|
src={src}
|
||||||
alt={alt}
|
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
|
<Box
|
||||||
position="absolute"
|
position="absolute"
|
||||||
top={2}
|
bottom={0}
|
||||||
right={2}
|
left={0}
|
||||||
display="flex"
|
right={0}
|
||||||
gap={2}
|
p={3}
|
||||||
opacity={0}
|
bg="bg-gradient-to-t from-black/80 to-transparent"
|
||||||
className="group-hover:opacity-100 transition-opacity"
|
|
||||||
>
|
>
|
||||||
{actions}
|
<Text size="xs" weight="medium" color="text-white" truncate>
|
||||||
|
{title}
|
||||||
|
</Text>
|
||||||
</Box>
|
</Box>
|
||||||
)}
|
)}
|
||||||
</Box>
|
</Box>
|
||||||
|
</Surface>
|
||||||
{(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>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,138 +1,144 @@
|
|||||||
|
import React, { ReactNode } from 'react';
|
||||||
|
|
||||||
import React, {
|
|
||||||
type KeyboardEvent as ReactKeyboardEvent,
|
|
||||||
type ReactNode,
|
|
||||||
} from 'react';
|
|
||||||
import { Box } from './primitives/Box';
|
import { Box } from './primitives/Box';
|
||||||
import { Button } from './Button';
|
|
||||||
import { Heading } from './Heading';
|
|
||||||
import { Stack } from './primitives/Stack';
|
import { Stack } from './primitives/Stack';
|
||||||
|
import { Button } from './Button';
|
||||||
import { Text } from './Text';
|
import { Text } from './Text';
|
||||||
|
import { X } from 'lucide-react';
|
||||||
|
import { IconButton } from './IconButton';
|
||||||
|
|
||||||
interface ModalProps {
|
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;
|
isOpen: boolean;
|
||||||
|
onClose?: () => void;
|
||||||
|
onOpenChange?: (open: boolean) => void;
|
||||||
|
title?: string;
|
||||||
|
description?: string;
|
||||||
|
icon?: React.ReactNode;
|
||||||
|
children: ReactNode;
|
||||||
footer?: ReactNode;
|
footer?: ReactNode;
|
||||||
|
primaryActionLabel?: string;
|
||||||
|
onPrimaryAction?: () => void;
|
||||||
|
secondaryActionLabel?: string;
|
||||||
|
onSecondaryAction?: () => void;
|
||||||
|
isLoading?: boolean;
|
||||||
|
size?: 'sm' | 'md' | 'lg' | 'xl';
|
||||||
}
|
}
|
||||||
|
|
||||||
export function Modal({
|
export function Modal({
|
||||||
|
isOpen,
|
||||||
|
onClose,
|
||||||
|
onOpenChange,
|
||||||
title,
|
title,
|
||||||
description,
|
description,
|
||||||
icon,
|
icon,
|
||||||
children,
|
children,
|
||||||
primaryActionLabel,
|
|
||||||
secondaryActionLabel,
|
|
||||||
onPrimaryAction,
|
|
||||||
onSecondaryAction,
|
|
||||||
onOpenChange,
|
|
||||||
isOpen,
|
|
||||||
footer,
|
footer,
|
||||||
|
primaryActionLabel,
|
||||||
|
onPrimaryAction,
|
||||||
|
secondaryActionLabel,
|
||||||
|
onSecondaryAction,
|
||||||
|
isLoading = false,
|
||||||
|
size = 'md',
|
||||||
}: ModalProps) {
|
}: ModalProps) {
|
||||||
const handleKeyDown = (event: ReactKeyboardEvent<HTMLDivElement>) => {
|
if (!isOpen) return null;
|
||||||
if (event.key === 'Escape') {
|
|
||||||
if (onOpenChange) {
|
const sizeMap = {
|
||||||
onOpenChange(false);
|
sm: 'max-w-md',
|
||||||
}
|
md: 'max-w-lg',
|
||||||
return;
|
lg: 'max-w-2xl',
|
||||||
}
|
xl: 'max-w-4xl',
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleBackdropClick = (event: React.MouseEvent<HTMLDivElement>) => {
|
const handleClose = () => {
|
||||||
if (event.target === event.currentTarget && onOpenChange) {
|
if (onClose) onClose();
|
||||||
onOpenChange(false);
|
if (onOpenChange) onOpenChange(false);
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
if (!isOpen) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box
|
<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"
|
role="dialog"
|
||||||
aria-modal="true"
|
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
|
<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}
|
tabIndex={-1}
|
||||||
>
|
>
|
||||||
<Box p={6} style={{ borderBottom: '1px solid rgba(38, 38, 38, 0.8)' }}>
|
{/* Header */}
|
||||||
<Stack direction="row" align="center" gap={3}>
|
<Box p={6} borderBottom borderColor="border-white/5">
|
||||||
{icon && <Box>{icon}</Box>}
|
<Stack direction="row" align="center" justify="between">
|
||||||
<Box>
|
<Stack direction="row" align="center" gap={3}>
|
||||||
<Heading level={2} id="modal-title">{title}</Heading>
|
{icon && <Box>{icon}</Box>}
|
||||||
{description && (
|
<Box>
|
||||||
<Text
|
{title && (
|
||||||
id="modal-description"
|
<Text size="xl" weight="bold" color="text-white" block>
|
||||||
size="sm"
|
{title}
|
||||||
color="text-gray-400"
|
</Text>
|
||||||
block
|
)}
|
||||||
mt={1}
|
{description && (
|
||||||
>
|
<Text size="sm" color="text-gray-400" block mt={1}>
|
||||||
{description}
|
{description}
|
||||||
</Text>
|
</Text>
|
||||||
)}
|
)}
|
||||||
</Box>
|
</Box>
|
||||||
|
</Stack>
|
||||||
|
<IconButton
|
||||||
|
icon={X}
|
||||||
|
onClick={handleClose}
|
||||||
|
variant="ghost"
|
||||||
|
size="sm"
|
||||||
|
title="Close modal"
|
||||||
|
/>
|
||||||
</Stack>
|
</Stack>
|
||||||
</Box>
|
</Box>
|
||||||
|
|
||||||
<Box p={6}>
|
{/* Content */}
|
||||||
|
<Box p={6} overflowY="auto" maxHeight="calc(100vh - 200px)">
|
||||||
{children}
|
{children}
|
||||||
</Box>
|
</Box>
|
||||||
|
|
||||||
|
{/* Footer */}
|
||||||
{(primaryActionLabel || secondaryActionLabel || footer) && (
|
{(primaryActionLabel || secondaryActionLabel || footer) && (
|
||||||
<Box p={6} style={{ borderTop: '1px solid rgba(38, 38, 38, 0.8)' }}>
|
<Box p={6} borderTop borderColor="border-white/5">
|
||||||
{(primaryActionLabel || secondaryActionLabel) && (
|
{footer || (
|
||||||
<Box style={{ display: 'flex', justifyContent: 'flex-end', gap: '0.75rem' }}>
|
<Stack direction="row" justify="end" gap={3}>
|
||||||
{secondaryActionLabel && (
|
{secondaryActionLabel && (
|
||||||
<Button
|
<Button
|
||||||
type="button"
|
variant="ghost"
|
||||||
onClick={() => {
|
onClick={onSecondaryAction || onClose}
|
||||||
onSecondaryAction?.();
|
disabled={isLoading}
|
||||||
onOpenChange?.(false);
|
|
||||||
}}
|
|
||||||
variant="secondary"
|
|
||||||
size="sm"
|
|
||||||
fullWidth={!primaryActionLabel}
|
|
||||||
>
|
>
|
||||||
{secondaryActionLabel}
|
{secondaryActionLabel}
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
{primaryActionLabel && (
|
{primaryActionLabel && (
|
||||||
<Button
|
<Button
|
||||||
type="button"
|
|
||||||
onClick={async () => {
|
|
||||||
if (onPrimaryAction) {
|
|
||||||
await onPrimaryAction();
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
variant="primary"
|
variant="primary"
|
||||||
size="sm"
|
onClick={onPrimaryAction}
|
||||||
fullWidth={!secondaryActionLabel}
|
isLoading={isLoading}
|
||||||
>
|
>
|
||||||
{primaryActionLabel}
|
{primaryActionLabel}
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
</Box>
|
</Stack>
|
||||||
)}
|
|
||||||
{footer && (
|
|
||||||
<Box mt={4}>
|
|
||||||
{footer}
|
|
||||||
</Box>
|
|
||||||
)}
|
)}
|
||||||
</Box>
|
</Box>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -1,12 +1,26 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { Box } from './primitives/Box';
|
||||||
|
import { Text } from './Text';
|
||||||
|
|
||||||
interface OnboardingErrorProps {
|
interface OnboardingErrorProps {
|
||||||
message: string;
|
message: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function OnboardingError({ message }: OnboardingErrorProps) {
|
export function OnboardingError({ message }: OnboardingErrorProps) {
|
||||||
return (
|
return (
|
||||||
<div className="mt-6 flex items-start gap-3 p-4 rounded-xl bg-red-500/10 border border-red-500/30">
|
<Box
|
||||||
<span className="text-red-400 flex-shrink-0 mt-0.5">⚠</span>
|
mt={6}
|
||||||
<p className="text-sm text-red-400">{message}</p>
|
display="flex"
|
||||||
</div>
|
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 {
|
interface OnboardingFormProps {
|
||||||
children: React.ReactNode;
|
children: ReactNode;
|
||||||
onSubmit: (e: React.FormEvent) => void | Promise<void>;
|
onSubmit: (e: FormEvent) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function OnboardingForm({ children, onSubmit }: OnboardingFormProps) {
|
export function OnboardingForm({ children, onSubmit }: OnboardingFormProps) {
|
||||||
return (
|
return (
|
||||||
<form onSubmit={onSubmit} className="relative">
|
<Box as="form" onSubmit={onSubmit} position="relative">
|
||||||
{children}
|
{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 {
|
interface OnboardingStepPanelProps {
|
||||||
children: React.ReactNode;
|
children: ReactNode;
|
||||||
className?: string;
|
className?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function OnboardingStepPanel({ children, className = '' }: OnboardingStepPanelProps) {
|
export function OnboardingStepPanel({ children, className = '' }: OnboardingStepPanelProps) {
|
||||||
return (
|
return (
|
||||||
<Surface
|
<Surface
|
||||||
variant="dark"
|
variant="muted"
|
||||||
rounded="xl"
|
rounded="2xl"
|
||||||
border
|
border
|
||||||
padding={6}
|
p={6}
|
||||||
className={`border-charcoal-outline ${className}`}
|
borderColor="border-charcoal-outline"
|
||||||
|
className={className}
|
||||||
>
|
>
|
||||||
{children}
|
{children}
|
||||||
</Surface>
|
</Surface>
|
||||||
|
|||||||
@@ -1,8 +1,7 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { Box } from './primitives/Box';
|
||||||
import { Box } from '@/ui/primitives/Box';
|
import { Text } from './Text';
|
||||||
import { Grid } from '@/ui/primitives/Grid';
|
import { Grid } from './primitives/Grid';
|
||||||
import { Text } from '@/ui/Text';
|
|
||||||
|
|
||||||
interface Stat {
|
interface Stat {
|
||||||
label: string;
|
label: string;
|
||||||
@@ -16,11 +15,19 @@ interface ProfileStatGridProps {
|
|||||||
|
|
||||||
export function ProfileStatGrid({ stats }: ProfileStatGridProps) {
|
export function ProfileStatGrid({ stats }: ProfileStatGridProps) {
|
||||||
return (
|
return (
|
||||||
<Grid cols={2} gap={4}>
|
<Grid cols={2} mdCols={4} gap={4}>
|
||||||
{stats.map((stat, idx) => (
|
{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="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>
|
</Box>
|
||||||
))}
|
))}
|
||||||
</Grid>
|
</Grid>
|
||||||
|
|||||||
@@ -1,46 +1,50 @@
|
|||||||
|
import React, { ReactNode } from 'react';
|
||||||
|
|
||||||
import { LucideIcon } from 'lucide-react';
|
|
||||||
import React from 'react';
|
|
||||||
import { Box } from './primitives/Box';
|
import { Box } from './primitives/Box';
|
||||||
import { Heading } from './Heading';
|
|
||||||
import { Icon } from './Icon';
|
|
||||||
import { Stack } from './primitives/Stack';
|
import { Stack } from './primitives/Stack';
|
||||||
import { Surface } from './primitives/Surface';
|
|
||||||
import { Text } from './Text';
|
import { Text } from './Text';
|
||||||
|
import { Surface } from './primitives/Surface';
|
||||||
|
import { Icon } from './Icon';
|
||||||
|
import { LucideIcon } from 'lucide-react';
|
||||||
|
|
||||||
interface SectionHeaderProps {
|
interface SectionHeaderProps {
|
||||||
icon: LucideIcon;
|
|
||||||
title: string;
|
title: string;
|
||||||
description?: string;
|
description?: string;
|
||||||
action?: React.ReactNode;
|
icon?: LucideIcon;
|
||||||
color?: string;
|
color?: string;
|
||||||
|
actions?: ReactNode;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function SectionHeader({
|
export function SectionHeader({ title, description, icon, color = 'text-primary-blue', actions }: SectionHeaderProps) {
|
||||||
icon,
|
|
||||||
title,
|
|
||||||
description,
|
|
||||||
action,
|
|
||||||
color = '#3b82f6'
|
|
||||||
}: SectionHeaderProps) {
|
|
||||||
return (
|
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}>
|
<Stack direction="row" align="center" justify="between" wrap gap={4}>
|
||||||
<Box>
|
<Stack direction="row" align="center" gap={3}>
|
||||||
<Stack direction="row" align="center" gap={3}>
|
{icon && (
|
||||||
<Surface variant="muted" rounded="lg" padding={2} style={{ backgroundColor: 'rgba(38, 38, 38, 0.5)' }}>
|
<Surface variant="muted" rounded="lg" p={2} bg="bg-white/5">
|
||||||
<Icon icon={icon} size={5} color={color} />
|
<Icon icon={icon} size={5} color={color} />
|
||||||
</Surface>
|
</Surface>
|
||||||
<Box>
|
)}
|
||||||
<Heading level={2}>{title}</Heading>
|
<Box>
|
||||||
{description && (
|
<Text size="lg" weight="bold" color="text-white" block>
|
||||||
<Text size="sm" color="text-gray-500" block mt={1}>{description}</Text>
|
{title}
|
||||||
)}
|
</Text>
|
||||||
</Box>
|
{description && (
|
||||||
</Stack>
|
<Text size="sm" color="text-gray-400" block>
|
||||||
</Box>
|
{description}
|
||||||
{action && <Box>{action}</Box>}
|
</Text>
|
||||||
|
)}
|
||||||
|
</Box>
|
||||||
|
</Stack>
|
||||||
|
{actions && (
|
||||||
|
<Box>
|
||||||
|
{actions}
|
||||||
|
</Box>
|
||||||
|
)}
|
||||||
</Stack>
|
</Stack>
|
||||||
</Box>
|
</Box>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,5 +1,3 @@
|
|||||||
|
|
||||||
|
|
||||||
import { Box } from './primitives/Box';
|
import { Box } from './primitives/Box';
|
||||||
import { Stack } from './primitives/Stack';
|
import { Stack } from './primitives/Stack';
|
||||||
import { Text } from './Text';
|
import { Text } from './Text';
|
||||||
@@ -29,7 +27,16 @@ export function SegmentedControl({
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
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) => {
|
{options.map((option) => {
|
||||||
const isSelected = option.value === value;
|
const isSelected = option.value === value;
|
||||||
|
|
||||||
@@ -41,24 +48,28 @@ export function SegmentedControl({
|
|||||||
onClick={() => handleSelect(option.value, option.disabled)}
|
onClick={() => handleSelect(option.value, option.disabled)}
|
||||||
aria-pressed={isSelected}
|
aria-pressed={isSelected}
|
||||||
disabled={option.disabled}
|
disabled={option.disabled}
|
||||||
style={{
|
flex={1}
|
||||||
flex: 1,
|
minWidth="140px"
|
||||||
minWidth: '140px',
|
px={3}
|
||||||
padding: '0.375rem 0.75rem',
|
py={1.5}
|
||||||
borderRadius: '9999px',
|
rounded="full"
|
||||||
transition: 'all 0.2s',
|
transition="all 0.2s"
|
||||||
textAlign: 'left',
|
textAlign="left"
|
||||||
backgroundColor: isSelected ? '#3b82f6' : 'transparent',
|
bg={isSelected ? 'bg-primary-blue' : 'transparent'}
|
||||||
color: isSelected ? 'white' : '#d1d5db',
|
color={isSelected ? 'text-white' : 'text-gray-400'}
|
||||||
opacity: option.disabled ? 0.5 : 1,
|
opacity={option.disabled ? 0.5 : 1}
|
||||||
cursor: option.disabled ? 'not-allowed' : 'pointer',
|
cursor={option.disabled ? 'not-allowed' : 'pointer'}
|
||||||
border: 'none'
|
border="none"
|
||||||
}}
|
|
||||||
>
|
>
|
||||||
<Stack gap={0.5}>
|
<Stack gap={0.5}>
|
||||||
<Text size="xs" weight="medium" color="inherit">{option.label}</Text>
|
<Text size="xs" weight="medium" color="inherit">{option.label}</Text>
|
||||||
{option.description && (
|
{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}
|
{option.description}
|
||||||
</Text>
|
</Text>
|
||||||
)}
|
)}
|
||||||
@@ -66,6 +77,6 @@ export function SegmentedControl({
|
|||||||
</Box>
|
</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 { Stack } from './primitives/Stack';
|
||||||
import { Text } from './Text';
|
import { Text } from './Text';
|
||||||
|
|
||||||
@@ -7,64 +8,57 @@ interface SelectOption {
|
|||||||
label: string;
|
label: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface SelectProps extends SelectHTMLAttributes<HTMLSelectElement> {
|
interface SelectProps extends React.SelectHTMLAttributes<HTMLSelectElement> {
|
||||||
id?: string;
|
|
||||||
'aria-label'?: string;
|
|
||||||
value?: string;
|
|
||||||
onChange?: (e: ChangeEvent<HTMLSelectElement>) => void;
|
|
||||||
options: SelectOption[];
|
|
||||||
className?: string;
|
|
||||||
style?: React.CSSProperties;
|
|
||||||
label?: string;
|
label?: string;
|
||||||
fullWidth?: boolean;
|
fullWidth?: boolean;
|
||||||
pl?: number;
|
pl?: number;
|
||||||
|
errorMessage?: string;
|
||||||
|
variant?: 'default' | 'error';
|
||||||
|
options?: SelectOption[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export function Select({
|
export const Select = forwardRef<HTMLSelectElement, SelectProps>(
|
||||||
id,
|
({ label, fullWidth = true, pl, errorMessage, variant = 'default', options, children, className = '', style, ...props }, ref) => {
|
||||||
'aria-label': ariaLabel,
|
const isError = variant === 'error' || !!errorMessage;
|
||||||
value,
|
|
||||||
onChange,
|
const variantClasses = isError
|
||||||
options,
|
? 'border-warning-amber focus:border-warning-amber'
|
||||||
className = '',
|
: 'border-charcoal-outline focus:border-primary-blue';
|
||||||
style,
|
|
||||||
label,
|
const defaultClasses = `${fullWidth ? 'w-full' : 'w-auto'} px-3 py-2 bg-deep-graphite border rounded-lg text-white focus:outline-none transition-colors`;
|
||||||
fullWidth = true,
|
const classes = [
|
||||||
pl,
|
defaultClasses,
|
||||||
...props
|
variantClasses,
|
||||||
}: SelectProps) {
|
pl ? `pl-${pl}` : '',
|
||||||
const spacingMap: Record<number, string> = {
|
className
|
||||||
10: 'pl-10'
|
].filter(Boolean).join(' ');
|
||||||
};
|
|
||||||
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(' ');
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Stack gap={1.5} fullWidth={fullWidth}>
|
<Stack gap={1.5} fullWidth={fullWidth}>
|
||||||
{label && (
|
{label && (
|
||||||
<Text as="label" size="sm" weight="medium" color="text-gray-400">
|
<Text as="label" size="xs" weight="bold" color="text-gray-500" uppercase letterSpacing="wider">
|
||||||
{label}
|
{label}
|
||||||
</Text>
|
</Text>
|
||||||
)}
|
)}
|
||||||
<select
|
<Box
|
||||||
id={id}
|
as="select"
|
||||||
aria-label={ariaLabel}
|
ref={ref}
|
||||||
value={value}
|
className={classes}
|
||||||
onChange={onChange}
|
style={style}
|
||||||
className={classes}
|
{...props}
|
||||||
style={style}
|
>
|
||||||
{...props}
|
{options ? options.map(opt => (
|
||||||
>
|
<option key={opt.value} value={opt.value}>{opt.label}</option>
|
||||||
{options.map((option) => (
|
)) : children}
|
||||||
<option key={option.value} value={option.value}>
|
</Box>
|
||||||
{option.label}
|
{errorMessage && (
|
||||||
</option>
|
<Text size="xs" color="text-warning-amber" mt={1}>
|
||||||
))}
|
{errorMessage}
|
||||||
</select>
|
</Text>
|
||||||
</Stack>
|
)}
|
||||||
);
|
</Stack>
|
||||||
}
|
);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
Select.displayName = 'Select';
|
||||||
|
|||||||
@@ -28,7 +28,8 @@ export function SimpleCheckbox({ checked, onChange, disabled, 'aria-label': aria
|
|||||||
borderColor="border-charcoal-outline"
|
borderColor="border-charcoal-outline"
|
||||||
rounded="sm"
|
rounded="sm"
|
||||||
aria-label={ariaLabel}
|
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 React from 'react';
|
||||||
|
import { Box } from './primitives/Box';
|
||||||
|
|
||||||
interface SkeletonProps {
|
interface SkeletonProps {
|
||||||
width?: string | number;
|
width?: string | number;
|
||||||
@@ -8,17 +9,13 @@ interface SkeletonProps {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function Skeleton({ width, height, circle, className = '' }: 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 (
|
return (
|
||||||
<div
|
<Box
|
||||||
|
w={width}
|
||||||
|
h={height}
|
||||||
|
rounded={circle ? 'full' : 'md'}
|
||||||
|
bg="bg-white/5"
|
||||||
className={`animate-pulse ${className}`}
|
className={`animate-pulse ${className}`}
|
||||||
style={style}
|
|
||||||
role="status"
|
role="status"
|
||||||
aria-label="Loading..."
|
aria-label="Loading..."
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -1,115 +1,115 @@
|
|||||||
|
import React, { ReactNode } from 'react';
|
||||||
|
|
||||||
import { motion, useReducedMotion } from 'framer-motion';
|
|
||||||
import { ArrowDownRight, ArrowUpRight, LucideIcon } from 'lucide-react';
|
|
||||||
import { Box } from './primitives/Box';
|
import { Box } from './primitives/Box';
|
||||||
import { Card } from './Card';
|
|
||||||
import { Icon } from './Icon';
|
|
||||||
import { Stack } from './primitives/Stack';
|
import { Stack } from './primitives/Stack';
|
||||||
import { Text } from './Text';
|
import { Text } from './Text';
|
||||||
|
import { Card } from './Card';
|
||||||
|
import { Icon } from './Icon';
|
||||||
|
import { LucideIcon } from 'lucide-react';
|
||||||
|
|
||||||
interface StatCardProps {
|
interface StatCardProps {
|
||||||
label: string;
|
label: string;
|
||||||
value: string | number;
|
value: string | number;
|
||||||
subValue?: string;
|
|
||||||
icon?: LucideIcon;
|
icon?: LucideIcon;
|
||||||
variant?: 'blue' | 'purple' | 'green' | 'orange';
|
|
||||||
className?: string;
|
|
||||||
trend?: {
|
trend?: {
|
||||||
value: number;
|
value: number;
|
||||||
isPositive: boolean;
|
isPositive: boolean;
|
||||||
};
|
};
|
||||||
|
variant?: 'default' | 'primary' | 'success' | 'warning' | 'danger' | 'info';
|
||||||
|
className?: string;
|
||||||
|
onClick?: () => void;
|
||||||
prefix?: string;
|
prefix?: string;
|
||||||
suffix?: string;
|
suffix?: string;
|
||||||
delay?: number;
|
delay?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function StatCard({
|
export function StatCard({
|
||||||
label,
|
label,
|
||||||
value,
|
value,
|
||||||
subValue,
|
|
||||||
icon,
|
icon,
|
||||||
variant = 'blue',
|
|
||||||
className = '',
|
|
||||||
trend,
|
trend,
|
||||||
prefix = '',
|
variant = 'default',
|
||||||
suffix = '',
|
className = '',
|
||||||
delay = 0,
|
onClick,
|
||||||
|
prefix,
|
||||||
|
suffix,
|
||||||
|
delay,
|
||||||
}: StatCardProps) {
|
}: StatCardProps) {
|
||||||
const shouldReduceMotion = useReducedMotion();
|
|
||||||
|
|
||||||
const variantClasses = {
|
const variantClasses = {
|
||||||
blue: 'bg-gradient-to-br from-blue-900/20 to-blue-700/10 border-blue-500/30',
|
default: 'bg-panel-gray border-border-gray',
|
||||||
purple: 'bg-gradient-to-br from-purple-900/20 to-purple-700/10 border-purple-500/30',
|
primary: 'bg-primary-accent/5 border-primary-accent/20',
|
||||||
green: 'bg-gradient-to-br from-green-900/20 to-green-700/10 border-green-500/30',
|
success: 'bg-success-green/5 border-success-green/20',
|
||||||
orange: 'bg-gradient-to-br from-orange-900/20 to-orange-700/10 border-orange-500/30'
|
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 = {
|
const iconColorClasses = {
|
||||||
blue: 'text-primary-blue',
|
default: 'text-gray-400',
|
||||||
purple: 'text-purple-400',
|
primary: 'text-primary-accent',
|
||||||
green: 'text-performance-green',
|
success: 'text-success-green',
|
||||||
orange: 'text-warning-amber'
|
warning: 'text-warning-amber',
|
||||||
|
danger: 'text-critical-red',
|
||||||
|
info: 'text-telemetry-aqua',
|
||||||
};
|
};
|
||||||
|
|
||||||
const cardContent = (
|
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 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 && (
|
{icon && (
|
||||||
<Box
|
<Box
|
||||||
width="11"
|
p={2}
|
||||||
height="11"
|
rounded="lg"
|
||||||
rounded="xl"
|
bg={iconBgClasses[variant]}
|
||||||
display="flex"
|
className={iconColorClasses[variant]}
|
||||||
center
|
|
||||||
bg="bg-iron-gray/50"
|
|
||||||
border={true}
|
|
||||||
borderColor="border-charcoal-outline"
|
|
||||||
>
|
>
|
||||||
<Icon icon={icon} size={5} className={iconColorClasses[variant]} />
|
<Icon icon={icon} size={5} />
|
||||||
</Box>
|
</Box>
|
||||||
)}
|
)}
|
||||||
|
</Stack>
|
||||||
|
|
||||||
|
<Stack gap={1}>
|
||||||
|
<Text size="3xl" weight="bold" color="text-white">
|
||||||
|
{prefix}{value}{suffix}
|
||||||
|
</Text>
|
||||||
{trend && (
|
{trend && (
|
||||||
<Stack
|
<Stack direction="row" align="center" gap={1}>
|
||||||
direction="row"
|
<Text
|
||||||
align="center"
|
size="xs"
|
||||||
gap={1}
|
weight="bold"
|
||||||
color={trend.isPositive ? 'text-performance-green' : 'text-error-red'}
|
color={trend.isPositive ? 'text-success-green' : 'text-critical-red'}
|
||||||
>
|
>
|
||||||
<Icon icon={trend.isPositive ? ArrowUpRight : ArrowDownRight} size={4} />
|
{trend.isPositive ? '+' : ''}{trend.value}%
|
||||||
<Text size="sm" weight="medium">{Math.abs(trend.value)}%</Text>
|
</Text>
|
||||||
|
<Text size="xs" color="text-gray-500">
|
||||||
|
vs last period
|
||||||
|
</Text>
|
||||||
</Stack>
|
</Stack>
|
||||||
)}
|
)}
|
||||||
</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>
|
</Stack>
|
||||||
</Card>
|
</Card>
|
||||||
);
|
);
|
||||||
|
|
||||||
if (shouldReduceMotion) {
|
if (onClick) {
|
||||||
return <Box fullHeight>{cardContent}</Box>;
|
return (
|
||||||
|
<Box as="button" onClick={onClick} w="full" textAlign="left" className="focus:outline-none">
|
||||||
|
{cardContent}
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return cardContent;
|
||||||
<Box
|
|
||||||
as={motion.div}
|
|
||||||
fullHeight
|
|
||||||
initial={{ opacity: 0, y: 20 }}
|
|
||||||
animate={{ opacity: 1, y: 0 }}
|
|
||||||
transition={{ delay }}
|
|
||||||
>
|
|
||||||
{cardContent}
|
|
||||||
</Box>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,37 +1,36 @@
|
|||||||
import React from 'react';
|
import React, { ReactNode } from 'react';
|
||||||
import { LucideIcon } from 'lucide-react';
|
|
||||||
import { Box } from './primitives/Box';
|
import { Box } from './primitives/Box';
|
||||||
|
import { Stack } from './primitives/Stack';
|
||||||
import { Text } from './Text';
|
import { Text } from './Text';
|
||||||
import { Icon } from './Icon';
|
import { Icon } from './Icon';
|
||||||
import { Stack } from './primitives/Stack';
|
import { LucideIcon } from 'lucide-react';
|
||||||
|
|
||||||
interface StatGridItemProps {
|
interface StatGridItemProps {
|
||||||
label: string;
|
label: string;
|
||||||
value: string | number;
|
value: string | number;
|
||||||
color?: string;
|
|
||||||
icon?: LucideIcon;
|
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 (
|
return (
|
||||||
<Box
|
<Box p={4} textAlign="center">
|
||||||
p={4}
|
|
||||||
bg="bg-deep-graphite/60"
|
|
||||||
rounded="xl"
|
|
||||||
border={true}
|
|
||||||
borderColor="border-charcoal-outline"
|
|
||||||
textAlign="center"
|
|
||||||
>
|
|
||||||
{icon && (
|
{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} />
|
<Icon icon={icon} size={4} />
|
||||||
<Text size="xs" weight="medium" uppercase letterSpacing="0.05em">{label}</Text>
|
|
||||||
</Stack>
|
</Stack>
|
||||||
)}
|
)}
|
||||||
{!icon && (
|
<Text size="2xl" weight="bold" color="text-white" block>
|
||||||
<Text size="xs" color="text-gray-500" uppercase letterSpacing="0.05em" block mb={1}>{label}</Text>
|
{value}
|
||||||
)}
|
</Text>
|
||||||
<Text size="3xl" weight="bold" color={color} block>{value}</Text>
|
<Text size="xs" weight="medium" color="text-gray-500" uppercase letterSpacing="wider">
|
||||||
|
{label}
|
||||||
|
</Text>
|
||||||
</Box>
|
</Box>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,43 +1,48 @@
|
|||||||
|
import React from 'react';
|
||||||
|
|
||||||
import { ReactNode } from 'react';
|
|
||||||
import { Box } from './primitives/Box';
|
import { Box } from './primitives/Box';
|
||||||
import { Surface } from './primitives/Surface';
|
|
||||||
import { Text } from './Text';
|
import { Text } from './Text';
|
||||||
|
import { Surface } from './primitives/Surface';
|
||||||
|
import { Icon } from './Icon';
|
||||||
|
import { LucideIcon } from 'lucide-react';
|
||||||
|
|
||||||
interface SummaryItemProps {
|
interface SummaryItemProps {
|
||||||
title: string;
|
label?: string;
|
||||||
subtitle?: string;
|
value?: string | number;
|
||||||
rightContent?: ReactNode;
|
icon?: LucideIcon;
|
||||||
onClick?: () => void;
|
onClick?: () => void;
|
||||||
|
title?: string;
|
||||||
|
subtitle?: string;
|
||||||
|
rightContent?: React.ReactNode;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function SummaryItem({
|
export function SummaryItem({ label, value, icon, onClick, title, subtitle, rightContent }: SummaryItemProps) {
|
||||||
title,
|
|
||||||
subtitle,
|
|
||||||
rightContent,
|
|
||||||
onClick,
|
|
||||||
}: SummaryItemProps) {
|
|
||||||
return (
|
return (
|
||||||
<Surface
|
<Surface
|
||||||
variant="muted"
|
variant="muted"
|
||||||
padding={3}
|
|
||||||
rounded="lg"
|
rounded="lg"
|
||||||
|
p={4}
|
||||||
|
display="flex"
|
||||||
|
alignItems="center"
|
||||||
|
gap={4}
|
||||||
|
cursor={onClick ? 'pointer' : 'default'}
|
||||||
onClick={onClick}
|
onClick={onClick}
|
||||||
style={{
|
hoverBg={onClick ? 'bg-white/5' : undefined}
|
||||||
display: 'flex',
|
transition={!!onClick}
|
||||||
alignItems: 'center',
|
|
||||||
justifyContent: 'space-between',
|
|
||||||
cursor: onClick ? 'pointer' : 'default',
|
|
||||||
}}
|
|
||||||
>
|
>
|
||||||
<Box>
|
{icon && (
|
||||||
<Text color="text-white" weight="medium" block>
|
<Box p={2} rounded="lg" bg="bg-white/5">
|
||||||
{title}
|
<Icon icon={icon} size={5} color="text-gray-400" />
|
||||||
</Text>
|
</Box>
|
||||||
{subtitle && (
|
)}
|
||||||
<Text size="xs" color="text-gray-500">
|
<Box flex={1}>
|
||||||
{subtitle}
|
{(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>
|
</Text>
|
||||||
)}
|
)}
|
||||||
</Box>
|
</Box>
|
||||||
|
|||||||
@@ -1,13 +1,13 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { Box } from './primitives/Box';
|
import { Box } from './primitives/Box';
|
||||||
|
import { Stack } from './primitives/Stack';
|
||||||
|
import { Surface } from './primitives/Surface';
|
||||||
import { Text } from './Text';
|
import { Text } from './Text';
|
||||||
import { Icon } from './Icon';
|
|
||||||
import { LucideIcon } from 'lucide-react';
|
|
||||||
|
|
||||||
interface Tab {
|
interface Tab {
|
||||||
id: string;
|
id: string;
|
||||||
label: string;
|
label: string;
|
||||||
icon?: LucideIcon;
|
icon?: React.ReactNode;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface TabNavigationProps {
|
interface TabNavigationProps {
|
||||||
@@ -19,51 +19,50 @@ interface TabNavigationProps {
|
|||||||
|
|
||||||
export function TabNavigation({ tabs, activeTab, onTabChange, className = '' }: TabNavigationProps) {
|
export function TabNavigation({ tabs, activeTab, onTabChange, className = '' }: TabNavigationProps) {
|
||||||
return (
|
return (
|
||||||
<Box
|
<Surface
|
||||||
display="flex"
|
variant="muted"
|
||||||
alignItems="center"
|
|
||||||
gap={1}
|
|
||||||
p={1.5}
|
|
||||||
rounded="xl"
|
rounded="xl"
|
||||||
bg="bg-iron-gray/50"
|
p={1}
|
||||||
border
|
display="inline-flex"
|
||||||
borderColor="border-charcoal-outline"
|
|
||||||
w="fit"
|
|
||||||
position="relative"
|
|
||||||
zIndex={10}
|
zIndex={10}
|
||||||
className={className}
|
className={className}
|
||||||
>
|
>
|
||||||
{tabs.map((tab) => {
|
<Stack direction="row" gap={1}>
|
||||||
const isActive = activeTab === tab.id;
|
{tabs.map((tab) => {
|
||||||
return (
|
const isActive = activeTab === tab.id;
|
||||||
<Box
|
return (
|
||||||
key={tab.id}
|
<Surface
|
||||||
as="button"
|
key={tab.id}
|
||||||
type="button"
|
as="button"
|
||||||
onClick={() => onTabChange(tab.id)}
|
onClick={() => onTabChange(tab.id)}
|
||||||
display="flex"
|
variant={isActive ? 'default' : 'ghost'}
|
||||||
alignItems="center"
|
bg={isActive ? 'bg-primary-blue' : ''}
|
||||||
gap={2}
|
rounded="lg"
|
||||||
px={5}
|
px={4}
|
||||||
py={2.5}
|
py={2}
|
||||||
rounded="lg"
|
transition="all 0.2s"
|
||||||
cursor="pointer"
|
group
|
||||||
transition
|
className={`select-none ${isActive ? 'shadow-lg shadow-primary-blue/25' : 'hover:bg-iron-gray/80'}`}
|
||||||
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' : ''}
|
|
||||||
>
|
>
|
||||||
{tab.label}
|
<Stack direction="row" align="center" gap={2}>
|
||||||
</Text>
|
{tab.icon && (
|
||||||
</Box>
|
<Box color={isActive ? 'text-white' : 'text-gray-400'} groupHoverTextColor={!isActive ? 'white' : undefined}>
|
||||||
);
|
{tab.icon}
|
||||||
})}
|
</Box>
|
||||||
</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';
|
import { Box, BoxProps } from './primitives/Box';
|
||||||
|
|
||||||
interface TableProps extends HTMLAttributes<HTMLTableElement> {
|
interface TableProps extends BoxProps<'table'> {
|
||||||
children: ReactNode;
|
children: ReactNode;
|
||||||
className?: string;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function Table({ children, className = '', ...props }: TableProps) {
|
export function Table({ children, className = '', ...props }: TableProps) {
|
||||||
|
const { border, translate, ...rest } = props;
|
||||||
return (
|
return (
|
||||||
<Box overflow="auto" className="border border-border-gray rounded-sm">
|
<Box overflow="auto" border borderColor="border-border-gray" rounded="sm">
|
||||||
<table className={`w-full border-collapse text-left ${className}`} {...props}>
|
<table className={`w-full border-collapse text-left ${className}`} {...(rest as any)}>
|
||||||
{children}
|
{children}
|
||||||
</table>
|
</table>
|
||||||
</Box>
|
</Box>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
interface TableHeadProps extends HTMLAttributes<HTMLTableSectionElement> {
|
interface TableHeaderProps extends BoxProps<'thead'> {
|
||||||
children: ReactNode;
|
children: ReactNode;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function TableHead({ children, ...props }: TableHeadProps) {
|
export function TableHeader({ children, className = '', ...props }: TableHeaderProps) {
|
||||||
return (
|
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}
|
{children}
|
||||||
</thead>
|
</Box>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
interface TableBodyProps extends HTMLAttributes<HTMLTableSectionElement> {
|
export const TableHead = TableHeader;
|
||||||
|
|
||||||
|
interface TableBodyProps extends BoxProps<'tbody'> {
|
||||||
children: ReactNode;
|
children: ReactNode;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function TableBody({ children, ...props }: TableBodyProps) {
|
export function TableBody({ children, className = '', ...props }: TableBodyProps) {
|
||||||
return (
|
return (
|
||||||
<tbody className="divide-y divide-border-gray/50" {...props}>
|
<Box as="tbody" className={`divide-y divide-border-gray/50 ${className}`} {...props}>
|
||||||
{children}
|
{children}
|
||||||
</tbody>
|
</Box>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
interface TableRowProps extends BoxProps<'tr'> {
|
interface TableRowProps extends BoxProps<'tr'> {
|
||||||
children: ReactNode;
|
children: ReactNode;
|
||||||
|
hoverBg?: string;
|
||||||
clickable?: boolean;
|
clickable?: boolean;
|
||||||
variant?: 'default' | 'highlight';
|
variant?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function TableRow({ children, className = '', clickable = false, variant = 'default', ...props }: TableRowProps) {
|
export function TableRow({ children, className = '', hoverBg, clickable, variant, ...props }: TableRowProps) {
|
||||||
const baseClasses = 'transition-colors duration-150 ease-smooth';
|
|
||||||
const variantClasses = variant === 'highlight' ? 'bg-primary-accent/5' : 'hover:bg-white/[0.02]';
|
|
||||||
const classes = [
|
const classes = [
|
||||||
baseClasses,
|
'transition-colors',
|
||||||
variantClasses,
|
clickable || props.onClick ? 'cursor-pointer' : '',
|
||||||
clickable ? 'cursor-pointer' : '',
|
hoverBg ? `hover:${hoverBg}` : (clickable || props.onClick ? 'hover:bg-white/5' : ''),
|
||||||
className
|
className
|
||||||
].filter(Boolean).join(' ');
|
].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;
|
children: ReactNode;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function TableHeader({ children, className = '', ...props }: TableHeaderProps) {
|
export function TableHeaderCell({ children, className = '', ...props }: TableCellProps) {
|
||||||
const baseClasses = 'py-2.5 px-4 text-[11px] font-bold text-gray-500 uppercase tracking-wider';
|
const classes = [
|
||||||
const classes = [baseClasses, className].filter(Boolean).join(' ');
|
'px-4 py-3 text-xs font-bold text-gray-400 uppercase tracking-wider',
|
||||||
|
className
|
||||||
|
].filter(Boolean).join(' ');
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box as="th" className={classes} {...props}>
|
<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) {
|
export function TableCell({ children, className = '', ...props }: TableCellProps) {
|
||||||
const baseClasses = 'py-3 px-4 text-sm text-gray-300';
|
const classes = [
|
||||||
const classes = [baseClasses, className].filter(Boolean).join(' ');
|
'px-4 py-4 text-sm text-gray-300',
|
||||||
|
className
|
||||||
|
].filter(Boolean).join(' ');
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box as="td" className={classes} {...props}>
|
<Box as="td" className={classes} {...props}>
|
||||||
|
|||||||
@@ -25,14 +25,14 @@ interface ResponsiveTextAlign {
|
|||||||
'2xl'?: TextAlign;
|
'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;
|
as?: T;
|
||||||
children: ReactNode;
|
children: ReactNode;
|
||||||
className?: string;
|
className?: string;
|
||||||
size?: TextSize | ResponsiveTextSize;
|
size?: TextSize | ResponsiveTextSize;
|
||||||
weight?: 'light' | 'normal' | 'medium' | 'semibold' | 'bold';
|
weight?: 'light' | 'normal' | 'medium' | 'semibold' | 'bold' | string;
|
||||||
color?: string;
|
color?: string;
|
||||||
font?: 'mono' | 'sans';
|
font?: 'mono' | 'sans' | string;
|
||||||
align?: TextAlign | ResponsiveTextAlign;
|
align?: TextAlign | ResponsiveTextAlign;
|
||||||
truncate?: boolean;
|
truncate?: boolean;
|
||||||
uppercase?: boolean;
|
uppercase?: boolean;
|
||||||
@@ -43,6 +43,7 @@ interface TextProps<T extends ElementType = 'span'> extends Omit<BoxProps<T>, 'c
|
|||||||
style?: React.CSSProperties;
|
style?: React.CSSProperties;
|
||||||
block?: boolean;
|
block?: boolean;
|
||||||
italic?: boolean;
|
italic?: boolean;
|
||||||
|
lineClamp?: number;
|
||||||
ml?: Spacing | ResponsiveSpacing;
|
ml?: Spacing | ResponsiveSpacing;
|
||||||
mr?: Spacing | ResponsiveSpacing;
|
mr?: Spacing | ResponsiveSpacing;
|
||||||
mt?: Spacing | ResponsiveSpacing;
|
mt?: Spacing | ResponsiveSpacing;
|
||||||
@@ -76,6 +77,7 @@ export function Text<T extends ElementType = 'span'>({
|
|||||||
style,
|
style,
|
||||||
block = false,
|
block = false,
|
||||||
italic = false,
|
italic = false,
|
||||||
|
lineClamp,
|
||||||
ml, mr, mt, mb,
|
ml, mr, mt, mb,
|
||||||
...props
|
...props
|
||||||
}: TextProps<T> & ComponentPropsWithoutRef<T>) {
|
}: TextProps<T> & ComponentPropsWithoutRef<T>) {
|
||||||
@@ -115,7 +117,7 @@ export function Text<T extends ElementType = 'span'>({
|
|||||||
bold: 'font-bold'
|
bold: 'font-bold'
|
||||||
};
|
};
|
||||||
|
|
||||||
const fontClasses = {
|
const fontClasses: Record<string, string> = {
|
||||||
mono: 'font-mono',
|
mono: 'font-mono',
|
||||||
sans: 'font-sans'
|
sans: 'font-sans'
|
||||||
};
|
};
|
||||||
@@ -175,8 +177,8 @@ export function Text<T extends ElementType = 'span'>({
|
|||||||
const classes = [
|
const classes = [
|
||||||
block ? 'block' : 'inline',
|
block ? 'block' : 'inline',
|
||||||
getSizeClasses(size),
|
getSizeClasses(size),
|
||||||
weightClasses[weight],
|
weightClasses[weight] || '',
|
||||||
fontClasses[font],
|
fontClasses[font] || '',
|
||||||
getAlignClasses(align),
|
getAlignClasses(align),
|
||||||
leading ? leadingClasses[leading] : '',
|
leading ? leadingClasses[leading] : '',
|
||||||
color,
|
color,
|
||||||
@@ -184,6 +186,7 @@ export function Text<T extends ElementType = 'span'>({
|
|||||||
uppercase ? 'uppercase' : '',
|
uppercase ? 'uppercase' : '',
|
||||||
capitalize ? 'capitalize' : '',
|
capitalize ? 'capitalize' : '',
|
||||||
italic ? 'italic' : '',
|
italic ? 'italic' : '',
|
||||||
|
lineClamp ? `line-clamp-${lineClamp}` : '',
|
||||||
letterSpacing === '0.05em' ? 'tracking-wider' : letterSpacing ? `tracking-${letterSpacing}` : '',
|
letterSpacing === '0.05em' ? 'tracking-wider' : letterSpacing ? `tracking-${letterSpacing}` : '',
|
||||||
getSpacingClass('ml', ml),
|
getSpacingClass('ml', ml),
|
||||||
getSpacingClass('mr', mr),
|
getSpacingClass('mr', mr),
|
||||||
@@ -194,6 +197,8 @@ export function Text<T extends ElementType = 'span'>({
|
|||||||
|
|
||||||
const combinedStyle = {
|
const combinedStyle = {
|
||||||
...(fontSize ? { fontSize } : {}),
|
...(fontSize ? { fontSize } : {}),
|
||||||
|
...(weight && !weightClasses[weight] ? { fontWeight: weight } : {}),
|
||||||
|
...(font && !fontClasses[font] ? { fontFamily: font } : {}),
|
||||||
...style
|
...style
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -1,56 +1,49 @@
|
|||||||
|
import React, { forwardRef } from 'react';
|
||||||
|
|
||||||
import React, { TextareaHTMLAttributes } from 'react';
|
|
||||||
import { Box } from './primitives/Box';
|
import { Box } from './primitives/Box';
|
||||||
import { Stack } from './primitives/Stack';
|
import { Stack } from './primitives/Stack';
|
||||||
import { Text } from './Text';
|
import { Text } from './Text';
|
||||||
|
|
||||||
interface TextAreaProps extends TextareaHTMLAttributes<HTMLTextAreaElement> {
|
interface TextAreaProps extends React.TextareaHTMLAttributes<HTMLTextAreaElement> {
|
||||||
label?: React.ReactNode;
|
label?: string;
|
||||||
errorMessage?: string;
|
errorMessage?: string;
|
||||||
variant?: 'default' | 'error';
|
variant?: 'default' | 'error';
|
||||||
fullWidth?: boolean;
|
fullWidth?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function TextArea({
|
export const TextArea = forwardRef<HTMLTextAreaElement, TextAreaProps>(
|
||||||
label,
|
({ label, errorMessage, variant = 'default', fullWidth = true, className = '', ...props }, ref) => {
|
||||||
errorMessage,
|
const isError = variant === 'error' || !!errorMessage;
|
||||||
variant = 'default',
|
|
||||||
fullWidth = true,
|
return (
|
||||||
className = '',
|
<Stack gap={1.5} fullWidth={fullWidth}>
|
||||||
...props
|
{label && (
|
||||||
}: TextAreaProps) {
|
<Text as="label" size="xs" weight="bold" color="text-gray-500" uppercase letterSpacing="wider">
|
||||||
const isError = variant === 'error' || !!errorMessage;
|
{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 (
|
TextArea.displayName = 'TextArea';
|
||||||
<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>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -1,68 +1,74 @@
|
|||||||
import { motion } from 'framer-motion';
|
import React from 'react';
|
||||||
import { Box } from './primitives/Box';
|
import { Box } from './primitives/Box';
|
||||||
import { Text } from './Text';
|
import { Text } from './Text';
|
||||||
|
import { motion } from 'framer-motion';
|
||||||
|
|
||||||
interface ToggleProps {
|
interface ToggleProps {
|
||||||
checked: boolean;
|
|
||||||
onChange: (checked: boolean) => void;
|
|
||||||
label: string;
|
label: string;
|
||||||
description?: string;
|
description?: string;
|
||||||
|
checked: boolean;
|
||||||
|
onChange: (checked: boolean) => void;
|
||||||
disabled?: boolean;
|
disabled?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function Toggle({
|
export function Toggle({ label, description, checked, onChange, disabled }: ToggleProps) {
|
||||||
checked,
|
|
||||||
onChange,
|
|
||||||
label,
|
|
||||||
description,
|
|
||||||
disabled = false,
|
|
||||||
}: ToggleProps) {
|
|
||||||
return (
|
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
|
||||||
<Box style={{ flex: 1, paddingRight: '1rem' }}>
|
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>
|
<Text weight="medium" color="text-gray-200" block>{label}</Text>
|
||||||
{description && (
|
{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>
|
</Box>
|
||||||
<button
|
|
||||||
type="button"
|
<Box position="relative">
|
||||||
role="switch"
|
<Box
|
||||||
aria-checked={checked}
|
as="button"
|
||||||
onClick={() => !disabled && onChange(!checked)}
|
type="button"
|
||||||
disabled={disabled}
|
role="switch"
|
||||||
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 ${
|
aria-checked={checked}
|
||||||
checked
|
onClick={() => !disabled && onChange(!checked)}
|
||||||
? 'bg-primary-blue'
|
disabled={disabled}
|
||||||
: 'bg-iron-gray'
|
w="12"
|
||||||
} ${disabled ? 'cursor-not-allowed' : ''}`}
|
h="6"
|
||||||
>
|
rounded="full"
|
||||||
{/* Glow effect when active */}
|
transition="all 0.2s"
|
||||||
{checked && (
|
flexShrink={0}
|
||||||
|
ring="primary-blue/50"
|
||||||
|
bg={checked ? 'bg-primary-blue/20' : 'bg-charcoal-outline'}
|
||||||
|
className="focus:outline-none focus:ring-2"
|
||||||
|
>
|
||||||
<motion.div
|
<motion.div
|
||||||
className="absolute inset-0 rounded-full bg-primary-blue"
|
className="absolute inset-0 rounded-full bg-primary-blue"
|
||||||
initial={{ boxShadow: '0 0 0px rgba(25, 140, 255, 0)' }}
|
initial={{ boxShadow: '0 0 0px rgba(25, 140, 255, 0)' }}
|
||||||
animate={{ boxShadow: '0 0 12px rgba(25, 140, 255, 0.4)' }}
|
animate={{
|
||||||
transition={{ duration: 0.2 }}
|
opacity: checked ? 1 : 0,
|
||||||
|
boxShadow: checked ? '0 0 10px rgba(25, 140, 255, 0.4)' : '0 0 0px rgba(25, 140, 255, 0)'
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
)}
|
</Box>
|
||||||
|
<motion.span
|
||||||
{/* Knob */}
|
|
||||||
<motion.span
|
|
||||||
className="absolute top-0.5 w-5 h-5 bg-white rounded-full shadow-md"
|
className="absolute top-0.5 w-5 h-5 bg-white rounded-full shadow-md"
|
||||||
initial={false}
|
initial={false}
|
||||||
animate={{
|
animate={{
|
||||||
x: checked ? 24 : 2,
|
left: checked ? '26px' : '2px',
|
||||||
scale: 1,
|
|
||||||
}}
|
|
||||||
whileTap={{ scale: disabled ? 1 : 0.9 }}
|
|
||||||
transition={{
|
|
||||||
type: 'spring',
|
|
||||||
stiffness: 500,
|
|
||||||
damping: 30,
|
|
||||||
}}
|
}}
|
||||||
|
transition={{ type: "spring", stiffness: 500, damping: 30 }}
|
||||||
/>
|
/>
|
||||||
</button>
|
</Box>
|
||||||
</label>
|
</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 {
|
interface ResponsiveSpacing {
|
||||||
base?: Spacing;
|
base?: Spacing;
|
||||||
|
sm?: Spacing;
|
||||||
md?: Spacing;
|
md?: Spacing;
|
||||||
lg?: Spacing;
|
lg?: Spacing;
|
||||||
|
xl?: Spacing;
|
||||||
}
|
}
|
||||||
|
|
||||||
export type ResponsiveValue<T> = {
|
export type ResponsiveValue<T> = {
|
||||||
@@ -49,38 +51,151 @@ export interface BoxProps<T extends ElementType> {
|
|||||||
px?: Spacing | ResponsiveSpacing;
|
px?: Spacing | ResponsiveSpacing;
|
||||||
py?: Spacing | ResponsiveSpacing;
|
py?: Spacing | ResponsiveSpacing;
|
||||||
// Sizing
|
// Sizing
|
||||||
w?: string | ResponsiveValue<string>;
|
w?: string | number | ResponsiveValue<string | number>;
|
||||||
h?: string | ResponsiveValue<string>;
|
h?: string | number | ResponsiveValue<string | number>;
|
||||||
width?: string;
|
width?: string | number;
|
||||||
height?: string;
|
height?: string | number;
|
||||||
maxWidth?: string | ResponsiveValue<string>;
|
maxWidth?: string | ResponsiveValue<string>;
|
||||||
minWidth?: string | ResponsiveValue<string>;
|
minWidth?: string | ResponsiveValue<string>;
|
||||||
maxHeight?: string | ResponsiveValue<string>;
|
maxHeight?: string | ResponsiveValue<string>;
|
||||||
minHeight?: string | ResponsiveValue<string>;
|
minHeight?: string | ResponsiveValue<string>;
|
||||||
|
fullWidth?: boolean;
|
||||||
|
fullHeight?: boolean;
|
||||||
|
aspectRatio?: string;
|
||||||
// Display
|
// 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
|
// Basic Styling
|
||||||
rounded?: 'none' | 'sm' | 'md' | 'lg' | 'xl' | '2xl' | 'full';
|
rounded?: 'none' | 'sm' | 'md' | 'lg' | 'xl' | '2xl' | 'full' | string | boolean;
|
||||||
border?: 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;
|
borderColor?: string;
|
||||||
|
borderOpacity?: number;
|
||||||
bg?: string;
|
bg?: string;
|
||||||
|
backgroundColor?: string;
|
||||||
|
backgroundImage?: string;
|
||||||
|
backgroundSize?: string;
|
||||||
|
backgroundPosition?: string;
|
||||||
|
bgOpacity?: number;
|
||||||
color?: string;
|
color?: string;
|
||||||
shadow?: string;
|
shadow?: string;
|
||||||
opacity?: number;
|
opacity?: number;
|
||||||
|
blur?: 'none' | 'sm' | 'md' | 'lg' | 'xl' | '2xl' | '3xl' | string;
|
||||||
|
pointerEvents?: 'auto' | 'none' | string;
|
||||||
// Flex/Grid Item props
|
// Flex/Grid Item props
|
||||||
flex?: number | string;
|
flex?: number | string;
|
||||||
flexShrink?: number;
|
flexShrink?: number;
|
||||||
flexGrow?: 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';
|
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>;
|
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
|
// Events
|
||||||
onMouseEnter?: React.MouseEventHandler<T>;
|
onMouseEnter?: React.MouseEventHandler<any>;
|
||||||
onMouseLeave?: React.MouseEventHandler<T>;
|
onMouseLeave?: React.MouseEventHandler<any>;
|
||||||
onClick?: React.MouseEventHandler<T>;
|
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;
|
style?: React.CSSProperties;
|
||||||
id?: string;
|
id?: string;
|
||||||
role?: string;
|
role?: React.AriaRole;
|
||||||
tabIndex?: number;
|
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'>(
|
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,
|
p, pt, pb, pl, pr, px, py,
|
||||||
w, h, width, height,
|
w, h, width, height,
|
||||||
maxWidth, minWidth, maxHeight, minHeight,
|
maxWidth, minWidth, maxHeight, minHeight,
|
||||||
|
fullWidth, fullHeight,
|
||||||
|
aspectRatio,
|
||||||
display,
|
display,
|
||||||
|
center,
|
||||||
|
overflow, overflowX, overflowY,
|
||||||
|
textAlign,
|
||||||
|
visibility,
|
||||||
|
position,
|
||||||
|
top, right, bottom, left,
|
||||||
|
inset, insetY, insetX,
|
||||||
|
zIndex,
|
||||||
rounded,
|
rounded,
|
||||||
border,
|
border,
|
||||||
|
borderTop,
|
||||||
|
borderBottom,
|
||||||
|
borderLeft,
|
||||||
|
borderRight,
|
||||||
|
borderWidth,
|
||||||
|
borderStyle,
|
||||||
borderColor,
|
borderColor,
|
||||||
|
borderOpacity,
|
||||||
bg,
|
bg,
|
||||||
|
backgroundColor,
|
||||||
|
backgroundImage,
|
||||||
|
backgroundSize,
|
||||||
|
backgroundPosition,
|
||||||
|
bgOpacity,
|
||||||
color,
|
color,
|
||||||
shadow,
|
shadow,
|
||||||
opacity,
|
opacity,
|
||||||
|
blur,
|
||||||
|
pointerEvents,
|
||||||
flex,
|
flex,
|
||||||
flexShrink,
|
flexShrink,
|
||||||
flexGrow,
|
flexGrow,
|
||||||
|
flexDirection,
|
||||||
|
flexWrap,
|
||||||
|
alignItems,
|
||||||
|
justifyContent,
|
||||||
alignSelf,
|
alignSelf,
|
||||||
|
gap,
|
||||||
|
gridCols,
|
||||||
|
responsiveGridCols,
|
||||||
|
colSpan,
|
||||||
|
responsiveColSpan,
|
||||||
order,
|
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,
|
onMouseEnter,
|
||||||
onMouseLeave,
|
onMouseLeave,
|
||||||
onClick,
|
onClick,
|
||||||
|
onMouseDown,
|
||||||
|
onMouseUp,
|
||||||
|
onMouseMove,
|
||||||
|
onKeyDown,
|
||||||
|
onBlur,
|
||||||
|
onSubmit,
|
||||||
|
onScroll,
|
||||||
style: styleProp,
|
style: styleProp,
|
||||||
id,
|
id,
|
||||||
role,
|
role,
|
||||||
tabIndex,
|
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
|
...props
|
||||||
}: BoxProps<T> & ComponentPropsWithoutRef<T>,
|
}: BoxProps<T> & ComponentPropsWithoutRef<T>,
|
||||||
ref: ForwardedRef<HTMLElement>
|
ref: ForwardedRef<HTMLElement>
|
||||||
@@ -131,14 +346,16 @@ export const Box = forwardRef(<T extends ElementType = 'div'>(
|
|||||||
if (typeof value === 'object') {
|
if (typeof value === 'object') {
|
||||||
const classes = [];
|
const classes = [];
|
||||||
if (value.base !== undefined) classes.push(`${prefix}-${spacingMap[value.base]}`);
|
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.md !== undefined) classes.push(`md:${prefix}-${spacingMap[value.md]}`);
|
||||||
if (value.lg !== undefined) classes.push(`lg:${prefix}-${spacingMap[value.lg]}`);
|
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 classes.join(' ');
|
||||||
}
|
}
|
||||||
return `${prefix}-${spacingMap[value]}`;
|
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 (value === undefined) return '';
|
||||||
if (typeof value === 'object') {
|
if (typeof value === 'object') {
|
||||||
const classes = [];
|
const classes = [];
|
||||||
@@ -161,42 +378,111 @@ export const Box = forwardRef(<T extends ElementType = 'div'>(
|
|||||||
getSpacingClass('mr', mr),
|
getSpacingClass('mr', mr),
|
||||||
getSpacingClass('mx', mx),
|
getSpacingClass('mx', mx),
|
||||||
getSpacingClass('my', my),
|
getSpacingClass('my', my),
|
||||||
getSpacingClass('p', p),
|
getSpacingClass('p', p || padding),
|
||||||
getSpacingClass('pt', pt),
|
getSpacingClass('pt', pt || paddingTop),
|
||||||
getSpacingClass('pb', pb),
|
getSpacingClass('pb', pb || paddingBottom),
|
||||||
getSpacingClass('pl', pl),
|
getSpacingClass('pl', pl || paddingLeft),
|
||||||
getSpacingClass('pr', pr),
|
getSpacingClass('pr', pr || paddingRight),
|
||||||
getSpacingClass('px', px),
|
getSpacingClass('px', px),
|
||||||
getSpacingClass('py', py),
|
getSpacingClass('py', py),
|
||||||
getResponsiveClasses('w', w),
|
fullWidth ? 'w-full' : getResponsiveClasses('w', w),
|
||||||
getResponsiveClasses('h', h),
|
fullHeight ? 'h-full' : getResponsiveClasses('h', h),
|
||||||
getResponsiveClasses('max-w', maxWidth),
|
getResponsiveClasses('max-w', maxWidth),
|
||||||
getResponsiveClasses('min-w', minWidth),
|
getResponsiveClasses('min-w', minWidth),
|
||||||
getResponsiveClasses('max-h', maxHeight),
|
getResponsiveClasses('max-h', maxHeight),
|
||||||
getResponsiveClasses('min-h', minHeight),
|
getResponsiveClasses('min-h', minHeight),
|
||||||
getResponsiveClasses('', display),
|
getResponsiveClasses('', display),
|
||||||
rounded ? `rounded-${rounded}` : '',
|
center ? 'flex items-center justify-center' : '',
|
||||||
border ? 'border' : '',
|
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 : '',
|
borderColor ? borderColor : '',
|
||||||
|
borderOpacity !== undefined ? `border-opacity-${borderOpacity * 100}` : '',
|
||||||
bg ? bg : '',
|
bg ? bg : '',
|
||||||
|
backgroundColor ? backgroundColor : '',
|
||||||
|
bgOpacity !== undefined ? `bg-opacity-${bgOpacity * 100}` : '',
|
||||||
color ? color : '',
|
color ? color : '',
|
||||||
shadow ? shadow : '',
|
shadow ? shadow : '',
|
||||||
|
opacity !== undefined ? `opacity-${opacity * 100}` : '',
|
||||||
|
blur ? (blur === 'none' ? 'blur-none' : `blur-${blur}`) : '',
|
||||||
|
pointerEvents ? `pointer-events-${pointerEvents}` : '',
|
||||||
flex !== undefined ? `flex-${flex}` : '',
|
flex !== undefined ? `flex-${flex}` : '',
|
||||||
flexShrink !== undefined ? `flex-shrink-${flexShrink}` : '',
|
flexShrink !== undefined ? `flex-shrink-${flexShrink}` : '',
|
||||||
flexGrow !== undefined ? `flex-grow-${flexGrow}` : '',
|
flexGrow !== undefined ? `flex-grow-${flexGrow}` : '',
|
||||||
|
getResponsiveClasses('flex', flexDirection),
|
||||||
|
flexWrap ? `flex-${flexWrap}` : '',
|
||||||
|
getResponsiveClasses('items', alignItems),
|
||||||
|
getResponsiveClasses('justify', justifyContent),
|
||||||
alignSelf !== undefined ? `self-${alignSelf}` : '',
|
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('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
|
className
|
||||||
].filter(Boolean).join(' ');
|
].filter(Boolean).join(' ');
|
||||||
|
|
||||||
const style: React.CSSProperties = {
|
const style: React.CSSProperties = {
|
||||||
...(width ? { width } : {}),
|
...(typeof width === 'string' || typeof width === 'number' ? { width } : {}),
|
||||||
...(height ? { height } : {}),
|
...(typeof height === 'string' || typeof height === 'number' ? { height } : {}),
|
||||||
...(typeof maxWidth === 'string' ? { maxWidth } : {}),
|
...(typeof maxWidth === 'string' ? { maxWidth } : {}),
|
||||||
...(typeof minWidth === 'string' ? { minWidth } : {}),
|
...(typeof minWidth === 'string' ? { minWidth } : {}),
|
||||||
...(typeof maxHeight === 'string' ? { maxHeight } : {}),
|
...(typeof maxHeight === 'string' ? { maxHeight } : {}),
|
||||||
...(typeof minHeight === 'string' ? { minHeight } : {}),
|
...(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 || {})
|
...(styleProp || {})
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -205,12 +491,35 @@ export const Box = forwardRef(<T extends ElementType = 'div'>(
|
|||||||
ref={ref as React.ForwardedRef<HTMLElement>}
|
ref={ref as React.ForwardedRef<HTMLElement>}
|
||||||
className={classes}
|
className={classes}
|
||||||
onClick={onClick}
|
onClick={onClick}
|
||||||
|
onMouseDown={onMouseDown}
|
||||||
|
onMouseUp={onMouseUp}
|
||||||
|
onMouseMove={onMouseMove}
|
||||||
|
onKeyDown={onKeyDown}
|
||||||
|
onBlur={onBlur}
|
||||||
|
onSubmit={onSubmit}
|
||||||
onMouseEnter={onMouseEnter}
|
onMouseEnter={onMouseEnter}
|
||||||
onMouseLeave={onMouseLeave}
|
onMouseLeave={onMouseLeave}
|
||||||
|
onScroll={onScroll}
|
||||||
style={style}
|
style={style}
|
||||||
id={id}
|
id={id}
|
||||||
role={role}
|
role={role}
|
||||||
tabIndex={tabIndex}
|
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}
|
{...props}
|
||||||
>
|
>
|
||||||
{children}
|
{children}
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import React, { ReactNode } from 'react';
|
import React, { ReactNode, ElementType } from 'react';
|
||||||
import { Box, BoxProps, ResponsiveValue } from './Box';
|
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.
|
* If you need a more specific layout, create a new component in apps/website/components.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
export interface GridProps {
|
export interface GridProps<T extends ElementType = 'div'> extends Omit<BoxProps<T>, 'children'> {
|
||||||
children: ReactNode;
|
children?: ReactNode;
|
||||||
cols?: 1 | 2 | 3 | 4 | 5 | 6 | 12;
|
cols?: 1 | 2 | 3 | 4 | 5 | 6 | 12;
|
||||||
mdCols?: 1 | 2 | 3 | 4 | 5 | 6 | 12;
|
mdCols?: 1 | 2 | 3 | 4 | 5 | 6 | 12;
|
||||||
lgCols?: 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;
|
gap?: 0 | 1 | 2 | 3 | 4 | 6 | 8 | 12 | 16;
|
||||||
className?: string;
|
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,
|
children,
|
||||||
cols = 1,
|
cols = 1,
|
||||||
mdCols,
|
mdCols,
|
||||||
@@ -46,7 +30,7 @@ export function Grid({
|
|||||||
gap = 4,
|
gap = 4,
|
||||||
className = '',
|
className = '',
|
||||||
...props
|
...props
|
||||||
}: GridProps) {
|
}: GridProps<T>) {
|
||||||
const colClasses: Record<number, string> = {
|
const colClasses: Record<number, string> = {
|
||||||
1: 'grid-cols-1',
|
1: 'grid-cols-1',
|
||||||
2: 'grid-cols-1 md:grid-cols-2',
|
2: 'grid-cols-1 md:grid-cols-2',
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import React from 'react';
|
import React, { ElementType } from 'react';
|
||||||
import { Box } from './Box';
|
import { Box, BoxProps } from './Box';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* WARNING: DO NOT VIOLATE THE PURPOSE OF THIS PRIMITIVE.
|
* 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.
|
* If you need a more specific layout, create a new component in apps/website/components.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
export interface GridItemProps {
|
export interface GridItemProps<T extends ElementType = 'div'> extends Omit<BoxProps<T>, 'children'> {
|
||||||
children: React.ReactNode;
|
children?: React.ReactNode;
|
||||||
colSpan?: 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12;
|
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;
|
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;
|
lgSpan?: 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12;
|
||||||
className?: string;
|
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 = [
|
const spanClasses = [
|
||||||
colSpan ? `col-span-${colSpan}` : '',
|
colSpan ? `col-span-${colSpan}` : '',
|
||||||
mdSpan ? `md:col-span-${mdSpan}` : '',
|
mdSpan ? `md:col-span-${mdSpan}` : '',
|
||||||
@@ -29,7 +29,7 @@ export function GridItem({ children, colSpan, mdSpan, lgSpan, className = '' }:
|
|||||||
].filter(Boolean).join(' ');
|
].filter(Boolean).join(' ');
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box className={spanClasses}>
|
<Box className={spanClasses} {...props}>
|
||||||
{children}
|
{children}
|
||||||
</Box>
|
</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';
|
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.
|
* 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 {
|
interface ResponsiveGap {
|
||||||
base?: number;
|
base?: number;
|
||||||
sm?: number;
|
sm?: number;
|
||||||
@@ -23,73 +21,31 @@ interface ResponsiveGap {
|
|||||||
xl?: number;
|
xl?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface ResponsiveSpacing {
|
export interface StackProps<T extends ElementType> extends Omit<BoxProps<T>, 'children'> {
|
||||||
base?: Spacing;
|
|
||||||
sm?: Spacing;
|
|
||||||
md?: Spacing;
|
|
||||||
lg?: Spacing;
|
|
||||||
xl?: Spacing;
|
|
||||||
'2xl'?: Spacing;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface StackProps<T extends ElementType> {
|
|
||||||
as?: T;
|
as?: T;
|
||||||
children: ReactNode;
|
children?: ReactNode;
|
||||||
className?: string;
|
className?: string;
|
||||||
direction?: 'row' | 'col' | { base?: 'row' | 'col'; md?: 'row' | 'col'; lg?: 'row' | 'col' };
|
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'>;
|
align?: 'start' | 'center' | 'end' | 'stretch' | 'baseline' | ResponsiveValue<'start' | 'center' | 'end' | 'stretch' | 'baseline'>;
|
||||||
justify?: 'start' | 'center' | 'end' | 'between' | 'around' | ResponsiveValue<'start' | 'center' | 'end' | 'between' | 'around'>;
|
justify?: 'start' | 'center' | 'end' | 'between' | 'around' | ResponsiveValue<'start' | 'center' | 'end' | 'between' | 'around'>;
|
||||||
wrap?: boolean;
|
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'>({
|
export const Stack = forwardRef(<T extends ElementType = 'div'>(
|
||||||
children,
|
{
|
||||||
className = '',
|
children,
|
||||||
direction = 'col',
|
className = '',
|
||||||
gap = 4,
|
direction = 'col',
|
||||||
align,
|
gap = 4,
|
||||||
justify,
|
align,
|
||||||
wrap = false,
|
justify,
|
||||||
m, mt, mb, ml, mr,
|
wrap = false,
|
||||||
p, pt, pb, pl, pr, px, py,
|
as,
|
||||||
w, h, minWidth, maxWidth, minHeight, maxHeight,
|
...props
|
||||||
rounded,
|
}: StackProps<T>,
|
||||||
flex,
|
ref: ForwardedRef<HTMLElement>
|
||||||
flexGrow,
|
) => {
|
||||||
flexShrink,
|
|
||||||
alignSelf,
|
|
||||||
as,
|
|
||||||
...props
|
|
||||||
}: StackProps<T>) {
|
|
||||||
const gapClasses: Record<number, string> = {
|
const gapClasses: Record<number, string> = {
|
||||||
0: 'gap-0',
|
0: 'gap-0',
|
||||||
1: 'gap-1',
|
1: 'gap-1',
|
||||||
@@ -104,50 +60,19 @@ export function Stack<T extends ElementType = 'div'>({
|
|||||||
16: 'gap-16'
|
16: 'gap-16'
|
||||||
};
|
};
|
||||||
|
|
||||||
const getGapClasses = (value: number | ResponsiveGap | undefined) => {
|
const getGapClasses = (value: number | string | ResponsiveGap | undefined) => {
|
||||||
if (value === undefined) return '';
|
if (value === undefined) return '';
|
||||||
if (typeof value === 'object') {
|
if (typeof value === 'object') {
|
||||||
const classes = [];
|
const classes = [];
|
||||||
if (value.base !== undefined) classes.push(gapClasses[value.base]);
|
if (value.base !== undefined) classes.push(typeof value.base === 'number' ? gapClasses[value.base] : `gap-${value.base}`);
|
||||||
if (value.sm !== undefined) classes.push(`sm:${gapClasses[value.sm]}`);
|
if (value.sm !== undefined) classes.push(typeof value.sm === 'number' ? `sm:${gapClasses[value.sm]}` : `sm:gap-${value.sm}`);
|
||||||
if (value.md !== undefined) classes.push(`md:${gapClasses[value.md]}`);
|
if (value.md !== undefined) classes.push(typeof value.md === 'number' ? `md:${gapClasses[value.md]}` : `md:gap-${value.md}`);
|
||||||
if (value.lg !== undefined) classes.push(`lg:${gapClasses[value.lg]}`);
|
if (value.lg !== undefined) classes.push(typeof value.lg === 'number' ? `lg:${gapClasses[value.lg]}` : `lg:gap-${value.lg}`);
|
||||||
if (value.xl !== undefined) classes.push(`xl:${gapClasses[value.xl]}`);
|
if (value.xl !== undefined) classes.push(typeof value.xl === 'number' ? `xl:${gapClasses[value.xl]}` : `xl:gap-${value.xl}`);
|
||||||
return classes.join(' ');
|
return classes.join(' ');
|
||||||
}
|
}
|
||||||
return gapClasses[value];
|
if (typeof value === 'number') return gapClasses[value];
|
||||||
};
|
return `gap-${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]}`;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const classes = [
|
const classes = [
|
||||||
@@ -161,19 +86,6 @@ export function Stack<T extends ElementType = 'div'>({
|
|||||||
].filter(Boolean).join(' '),
|
].filter(Boolean).join(' '),
|
||||||
getGapClasses(gap) || 'gap-4',
|
getGapClasses(gap) || 'gap-4',
|
||||||
wrap ? 'flex-wrap' : '',
|
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
|
className
|
||||||
].filter(Boolean).join(' ');
|
].filter(Boolean).join(' ');
|
||||||
|
|
||||||
@@ -217,20 +129,13 @@ export function Stack<T extends ElementType = 'div'>({
|
|||||||
return (
|
return (
|
||||||
<Box
|
<Box
|
||||||
as={as}
|
as={as}
|
||||||
|
ref={ref}
|
||||||
className={`${classes} ${layoutClasses}`}
|
className={`${classes} ${layoutClasses}`}
|
||||||
w={w}
|
|
||||||
h={h}
|
|
||||||
minWidth={minWidth}
|
|
||||||
maxWidth={maxWidth}
|
|
||||||
minHeight={minHeight}
|
|
||||||
maxHeight={maxHeight}
|
|
||||||
flex={flex}
|
|
||||||
flexGrow={flexGrow}
|
|
||||||
flexShrink={flexShrink}
|
|
||||||
alignSelf={alignSelf}
|
|
||||||
{...props}
|
{...props}
|
||||||
>
|
>
|
||||||
{children}
|
{children}
|
||||||
</Box>
|
</Box>
|
||||||
);
|
);
|
||||||
}
|
});
|
||||||
|
|
||||||
|
Stack.displayName = 'Stack';
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import React, { ReactNode, ElementType, ComponentPropsWithoutRef } from 'react';
|
import React, { ReactNode, ElementType, ComponentPropsWithoutRef, forwardRef, ForwardedRef } from 'react';
|
||||||
import { Box, BoxProps, ResponsiveValue } from './Box';
|
import { Box, BoxProps } from './Box';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* WARNING: DO NOT VIOLATE THE PURPOSE OF THIS PRIMITIVE.
|
* 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.
|
* 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;
|
as?: T;
|
||||||
children: ReactNode;
|
children?: ReactNode;
|
||||||
variant?: 'default' | 'muted' | 'dark' | 'glass' | 'gradient-blue' | 'gradient-gold' | 'gradient-purple' | 'gradient-green' | 'discord' | 'discord-inner';
|
variant?: 'default' | 'muted' | 'dark' | 'glass' | 'gradient-blue' | 'gradient-gold' | 'gradient-purple' | 'gradient-green' | 'discord' | 'discord-inner';
|
||||||
rounded?: 'none' | 'sm' | 'md' | 'lg' | 'xl' | '2xl' | 'full';
|
rounded?: 'none' | 'sm' | 'md' | 'lg' | 'xl' | '2xl' | 'full' | string | boolean;
|
||||||
border?: boolean;
|
border?: boolean | string;
|
||||||
padding?: number;
|
padding?: number;
|
||||||
className?: string;
|
className?: string;
|
||||||
shadow?: 'none' | 'sm' | 'md' | 'lg' | 'xl' | 'discord' | 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'>({
|
export const Surface = forwardRef(<T extends ElementType = 'div'>(
|
||||||
as,
|
{
|
||||||
children,
|
as,
|
||||||
variant = 'default',
|
children,
|
||||||
rounded = 'none',
|
variant = 'default',
|
||||||
border = false,
|
rounded = 'none',
|
||||||
padding = 0,
|
border = false,
|
||||||
className = '',
|
padding = 0,
|
||||||
shadow = 'none',
|
className = '',
|
||||||
w, h, maxWidth,
|
shadow = 'none',
|
||||||
...props
|
...props
|
||||||
}: SurfaceProps<T> & ComponentPropsWithoutRef<T>) {
|
}: SurfaceProps<T> & ComponentPropsWithoutRef<T>,
|
||||||
|
ref: ForwardedRef<HTMLElement>
|
||||||
|
) => {
|
||||||
const variantClasses: Record<string, string> = {
|
const variantClasses: Record<string, string> = {
|
||||||
default: 'bg-panel-gray',
|
default: 'bg-panel-gray',
|
||||||
muted: 'bg-panel-gray/40',
|
muted: 'bg-panel-gray/40',
|
||||||
@@ -85,7 +83,7 @@ export function Surface<T extends ElementType = 'div'>({
|
|||||||
|
|
||||||
const classes = [
|
const classes = [
|
||||||
variantClasses[variant],
|
variantClasses[variant],
|
||||||
roundedClasses[rounded],
|
typeof rounded === 'string' && roundedClasses[rounded] ? roundedClasses[rounded] : '',
|
||||||
border ? 'border border-border-gray' : '',
|
border ? 'border border-border-gray' : '',
|
||||||
paddingClasses[padding] || 'p-0',
|
paddingClasses[padding] || 'p-0',
|
||||||
shadowClasses[shadow],
|
shadowClasses[shadow],
|
||||||
@@ -93,8 +91,10 @@ export function Surface<T extends ElementType = 'div'>({
|
|||||||
].filter(Boolean).join(' ');
|
].filter(Boolean).join(' ');
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box as={as} className={classes} w={w} h={h} maxWidth={maxWidth} {...props}>
|
<Box as={as} ref={ref} className={classes} {...props}>
|
||||||
{children}
|
{children}
|
||||||
</Box>
|
</Box>
|
||||||
);
|
);
|
||||||
}
|
});
|
||||||
|
|
||||||
|
Surface.displayName = 'Surface';
|
||||||
|
|||||||
Reference in New Issue
Block a user