From b43a23a48c26976508478d37559c8a5c091dd8e7 Mon Sep 17 00:00:00 2001 From: Marc Mintel Date: Sun, 18 Jan 2026 21:31:08 +0100 Subject: [PATCH] website refactor --- apps/website/ui/Accordion.tsx | 59 ++- apps/website/ui/ActivityItem.tsx | 81 ++-- apps/website/ui/Avatar.tsx | 78 ++-- apps/website/ui/Badge.tsx | 64 +-- apps/website/ui/BorderTabs.tsx | 90 ++-- apps/website/ui/BreadcrumbBar.tsx | 37 +- apps/website/ui/Breadcrumbs.tsx | 69 ++-- apps/website/ui/Button.tsx | 100 +++-- apps/website/ui/Card.tsx | 96 +++-- apps/website/ui/CategoryDistribution.tsx | 100 ++--- apps/website/ui/CategoryDistributionCard.tsx | 68 +-- apps/website/ui/CategoryIcon.tsx | 49 +-- apps/website/ui/Checkbox.tsx | 59 ++- apps/website/ui/CircularProgress.tsx | 78 ++-- apps/website/ui/Container.tsx | 53 +-- apps/website/ui/ContentShell.tsx | 44 +- apps/website/ui/ContentViewport.tsx | 42 +- apps/website/ui/ControlBar.tsx | 41 +- apps/website/ui/CountryFlag.tsx | 113 ++--- apps/website/ui/DangerZone.tsx | 39 +- apps/website/ui/DateHeader.tsx | 39 +- apps/website/ui/DurationField.tsx | 106 ++--- apps/website/ui/ErrorActionButtons.tsx | 70 +--- apps/website/ui/ErrorBanner.tsx | 74 ++-- apps/website/ui/ErrorPageContainer.tsx | 37 +- apps/website/ui/FeedEmptyState.tsx | 59 ++- apps/website/ui/FeedItem.tsx | 99 ++--- apps/website/ui/FilterGroup.tsx | 79 ++-- apps/website/ui/Footer.tsx | 121 +++--- apps/website/ui/FormField.tsx | 68 +-- apps/website/ui/FormSection.tsx | 48 +-- apps/website/ui/GoalCard.tsx | 65 +-- apps/website/ui/Header.tsx | 42 +- apps/website/ui/Heading.tsx | 109 ++--- apps/website/ui/HorizontalBarChart.tsx | 68 +-- apps/website/ui/HorizontalStatCard.tsx | 46 +-- apps/website/ui/HorizontalStatItem.tsx | 18 +- apps/website/ui/Icon.tsx | 82 ++-- apps/website/ui/IconButton.tsx | 63 +-- apps/website/ui/Image.tsx | 63 ++- apps/website/ui/ImagePlaceholder.tsx | 97 ++--- apps/website/ui/InfoBanner.tsx | 99 +++-- apps/website/ui/InfoBox.tsx | 86 ++-- apps/website/ui/InfoItem.tsx | 37 +- apps/website/ui/Input.tsx | 105 ++--- apps/website/ui/LandingItems.tsx | 69 ++-- apps/website/ui/Layout.tsx | 93 ++--- apps/website/ui/LeaderboardList.tsx | 27 +- apps/website/ui/LeaderboardPreviewShell.tsx | 104 +++-- apps/website/ui/LeaderboardTableShell.tsx | 54 +-- apps/website/ui/Link.tsx | 102 ++--- apps/website/ui/LoadingSpinner.tsx | 45 +- apps/website/ui/MainContent.tsx | 17 +- apps/website/ui/MetricCard.tsx | 82 ++-- apps/website/ui/MiniStat.tsx | 18 +- apps/website/ui/Modal.tsx | 218 +++++----- apps/website/ui/PageHero.tsx | 155 ++----- apps/website/ui/Pagination.tsx | 117 +++--- apps/website/ui/Panel.tsx | 71 ++-- apps/website/ui/PasswordField.tsx | 57 ++- apps/website/ui/PlaceholderImage.tsx | 33 +- apps/website/ui/Podium.tsx | 132 +++--- apps/website/ui/PresetCard.tsx | 162 +++----- apps/website/ui/ProgressBar.tsx | 64 ++- apps/website/ui/QuickActionItem.tsx | 78 ++-- apps/website/ui/QuickActionLink.tsx | 55 +-- apps/website/ui/QuickActionsPanel.tsx | 35 +- apps/website/ui/Section.tsx | 84 ++-- apps/website/ui/SectionHeader.tsx | 40 +- apps/website/ui/SegmentedControl.tsx | 96 ++--- apps/website/ui/Select.tsx | 118 +++--- apps/website/ui/SidebarActionLink.tsx | 73 +++- apps/website/ui/SimpleCheckbox.tsx | 40 +- apps/website/ui/Skeleton.tsx | 41 +- apps/website/ui/StatBox.tsx | 42 +- apps/website/ui/StatCard.tsx | 143 +++---- apps/website/ui/StatGrid.tsx | 59 +-- apps/website/ui/StatGridItem.tsx | 38 +- apps/website/ui/StatItem.tsx | 19 +- apps/website/ui/StatusBadge.tsx | 26 +- apps/website/ui/StatusDot.tsx | 43 +- apps/website/ui/StatusIndicator.tsx | 164 ++++---- apps/website/ui/StepIndicator.tsx | 90 ++-- apps/website/ui/SummaryItem.tsx | 68 ++- apps/website/ui/TabNavigation.tsx | 86 ++-- apps/website/ui/Table.tsx | 124 +++--- apps/website/ui/Text.tsx | 249 ++++------- apps/website/ui/TextArea.tsx | 86 ++-- apps/website/ui/Toggle.tsx | 78 ++-- apps/website/ui/primitives/Box.tsx | 414 ++++++++++--------- apps/website/ui/primitives/Grid.tsx | 52 +-- apps/website/ui/primitives/GridItem.tsx | 46 +-- apps/website/ui/primitives/Stack.tsx | 102 +---- apps/website/ui/primitives/Surface.tsx | 105 ++--- apps/website/ui/theme/Theme.ts | 38 ++ apps/website/ui/theme/themes/default.ts | 36 ++ 96 files changed, 3461 insertions(+), 4067 deletions(-) diff --git a/apps/website/ui/Accordion.tsx b/apps/website/ui/Accordion.tsx index 9f9408acd..9ec550db7 100644 --- a/apps/website/ui/Accordion.tsx +++ b/apps/website/ui/Accordion.tsx @@ -1,49 +1,40 @@ - - -import { ChevronDown, ChevronUp } from 'lucide-react'; -import { ReactNode } from 'react'; -import { Icon } from './Icon'; +import React, { ReactNode, useState } from 'react'; import { Box } from './primitives/Box'; -import { Stack } from './primitives/Stack'; import { Text } from './Text'; +import { ChevronDown, ChevronUp } from 'lucide-react'; +import { Icon } from './Icon'; +import { Surface } from './primitives/Surface'; -interface AccordionProps { +export interface AccordionProps { title: string; - icon: ReactNode; children: ReactNode; - isOpen: boolean; - onToggle: () => void; + defaultOpen?: boolean; } -export function Accordion({ title, icon, children, isOpen, onToggle }: AccordionProps) { +export const Accordion = ({ + title, + children, + defaultOpen = false +}: AccordionProps) => { + const [isOpen, setIsOpen] = useState(defaultOpen); + return ( - - + {isOpen && ( - + {children} )} - + ); -} +}; diff --git a/apps/website/ui/ActivityItem.tsx b/apps/website/ui/ActivityItem.tsx index 789b3c384..c89a3552b 100644 --- a/apps/website/ui/ActivityItem.tsx +++ b/apps/website/ui/ActivityItem.tsx @@ -1,67 +1,44 @@ -import React from 'react'; +import React, { ReactNode } from 'react'; import { Box } from './primitives/Box'; import { Text } from './Text'; import { Surface } from './primitives/Surface'; -import { Link } from './Link'; -interface ActivityItemProps { - title?: string; +export interface ActivityItemProps { + title: string; description?: string; - timeAgo?: string; - color?: string; - headline?: string; - body?: string; - formattedTime?: string; - ctaHref?: string; - ctaLabel?: string; + timestamp: string; + icon?: ReactNode; + children?: ReactNode; } -export function ActivityItem({ +export const ActivityItem = ({ title, description, - timeAgo, - color = 'bg-primary-blue', - headline, - body, - formattedTime, - ctaHref, - ctaLabel -}: ActivityItemProps) { + timestamp, + icon, + children +}: ActivityItemProps) => { return ( - - - - - {title || headline} - - - {description || body} - - - {timeAgo || formattedTime} - - {ctaHref && ctaLabel && ( - - - {ctaLabel} - + + + {icon && ( + + {icon} )} + + + {title} + {timestamp} + + {description && ( + + {description} + + )} + {children} + ); -} +}; diff --git a/apps/website/ui/Avatar.tsx b/apps/website/ui/Avatar.tsx index 99a6ecd37..31cd5833e 100644 --- a/apps/website/ui/Avatar.tsx +++ b/apps/website/ui/Avatar.tsx @@ -1,44 +1,62 @@ import React from 'react'; -import { Box } from './primitives/Box'; -import { Image } from './Image'; import { Surface } from './primitives/Surface'; +import { Box } from './primitives/Box'; +import { User } from 'lucide-react'; +import { Icon } from './Icon'; -interface AvatarProps { - src?: string | null; - alt: string; - size?: number; - className?: string; +export interface AvatarProps { + src?: string; + alt?: string; + size?: 'sm' | 'md' | 'lg' | 'xl'; + fallback?: string; } -export function Avatar({ src, alt, size = 40, className = '' }: AvatarProps) { +export const Avatar = ({ + src, + alt, + size = 'md', + fallback +}: AvatarProps) => { + const sizeMap = { + sm: '2rem', + md: '3rem', + lg: '4rem', + xl: '6rem' + }; + + const iconSizeMap = { + sm: 3, + md: 5, + lg: 8, + xl: 12 + } as const; + return ( - {src ? ( - {alt} ) : ( - - - {alt.charAt(0).toUpperCase()} - + + {fallback ? ( + {fallback} + ) : ( + + )} )} ); -} +}; diff --git a/apps/website/ui/Badge.tsx b/apps/website/ui/Badge.tsx index 89683ff55..1860bc33c 100644 --- a/apps/website/ui/Badge.tsx +++ b/apps/website/ui/Badge.tsx @@ -1,43 +1,57 @@ import React, { ReactNode } from 'react'; -import { Box, BoxProps } from './primitives/Box'; +import { Box } from './primitives/Box'; import { Icon } from './Icon'; import { LucideIcon } from 'lucide-react'; +import { Stack } from './primitives/Stack'; -interface BadgeProps { +export interface BadgeProps { children: ReactNode; - variant?: 'default' | 'primary' | 'success' | 'warning' | 'danger' | 'info'; - size?: 'xs' | 'sm' | 'md'; + variant?: 'primary' | 'secondary' | 'success' | 'warning' | 'critical' | 'info' | 'outline' | 'default' | 'danger'; + size?: 'sm' | 'md'; + style?: React.CSSProperties; icon?: LucideIcon; } -export function Badge({ children, variant = 'default', size = 'sm', icon }: BadgeProps) { - const baseClasses = 'flex items-center gap-1.5 border font-bold uppercase tracking-widest'; - - const sizeClasses = { - xs: 'px-1.5 py-0.5 text-[9px]', - sm: 'px-2 py-0.5 text-[10px]', - md: 'px-3 py-1 text-xs' +export const Badge = ({ + children, + variant = 'primary', + size = 'md', + style, + icon +}: BadgeProps) => { + const variantClasses = { + primary: 'bg-[var(--ui-color-intent-primary)] text-white', + secondary: 'bg-[var(--ui-color-bg-surface)] text-[var(--ui-color-text-med)] border border-[var(--ui-color-border-default)]', + success: 'bg-[var(--ui-color-intent-success)] text-[var(--ui-color-bg-base)]', + warning: 'bg-[var(--ui-color-intent-warning)] text-[var(--ui-color-bg-base)]', + critical: 'bg-[var(--ui-color-intent-critical)] text-white', + danger: 'bg-[var(--ui-color-intent-critical)] text-white', + info: 'bg-[var(--ui-color-intent-telemetry)] text-[var(--ui-color-bg-base)]', + outline: 'bg-transparent text-[var(--ui-color-text-med)] border border-[var(--ui-color-border-default)]', + default: 'bg-[var(--ui-color-bg-surface-muted)] text-[var(--ui-color-text-med)]', }; - const variantClasses = { - default: 'bg-gray-500/10 border-gray-500/30 text-gray-400', - primary: 'bg-primary-accent/10 border-primary-accent/30 text-primary-accent', - success: 'bg-success-green/10 border-success-green/30 text-success-green', - warning: 'bg-warning-amber/10 border-warning-amber/30 text-warning-amber', - danger: 'bg-critical-red/10 border-critical-red/30 text-critical-red', - info: 'bg-telemetry-aqua/10 border-telemetry-aqua/30 text-telemetry-aqua' + const sizeClasses = { + sm: 'px-1.5 py-0.5 text-[10px]', + md: 'px-2 py-0.5 text-xs', }; const classes = [ - baseClasses, - sizeClasses[size], + 'inline-flex items-center justify-center font-bold uppercase tracking-wider rounded-none', variantClasses[variant], - ].filter(Boolean).join(' '); + sizeClasses[size], + ].join(' '); + + const content = icon ? ( + + + {children} + + ) : children; return ( - - {icon && } - {children} + + {content} ); -} +}; diff --git a/apps/website/ui/BorderTabs.tsx b/apps/website/ui/BorderTabs.tsx index d775667a6..26a137812 100644 --- a/apps/website/ui/BorderTabs.tsx +++ b/apps/website/ui/BorderTabs.tsx @@ -1,63 +1,55 @@ import React from 'react'; import { Box } from './primitives/Box'; -import { Stack } from './primitives/Stack'; -import { Surface } from './primitives/Surface'; import { Text } from './Text'; -interface Tab { +export interface TabOption { id: string; label: string; icon?: React.ReactNode; } -interface BorderTabsProps { - tabs: Tab[]; - activeTab: string; - onTabChange: (tabId: string) => void; - className?: string; +export interface BorderTabsProps { + tabs: TabOption[]; + activeTabId: string; + onTabChange: (id: string) => void; } -export function BorderTabs({ tabs, activeTab, onTabChange, className = '' }: BorderTabsProps) { +export const BorderTabs = ({ + tabs, + activeTabId, + onTabChange +}: BorderTabsProps) => { return ( - - - {tabs.map((tab) => { - const isActive = activeTab === tab.id; - return ( - onTabChange(tab.id)} - variant="ghost" - px={1} - py={4} - position="relative" - borderColor={isActive ? 'border-primary-blue' : ''} - borderBottom={isActive} - borderWidth={isActive ? '2px' : '0'} - mb="-1px" - transition="all 0.2s" - group - > - - {tab.icon && ( - - {tab.icon} - - )} - - {tab.label} - - - - ); - })} - + + {tabs.map((tab) => { + const isActive = tab.id === activeTabId; + return ( + + ); + })} ); -} +}; diff --git a/apps/website/ui/BreadcrumbBar.tsx b/apps/website/ui/BreadcrumbBar.tsx index 725a6dc32..f20343df6 100644 --- a/apps/website/ui/BreadcrumbBar.tsx +++ b/apps/website/ui/BreadcrumbBar.tsx @@ -1,17 +1,30 @@ -import React from 'react'; +import React, { ReactNode } from 'react'; +import { Box } from './primitives/Box'; +import { Breadcrumbs, BreadcrumbItem } from './Breadcrumbs'; -interface BreadcrumbBarProps { - children: React.ReactNode; - className?: string; +export interface BreadcrumbBarProps { + items: BreadcrumbItem[]; + actions?: ReactNode; } -/** - * BreadcrumbBar is a container for breadcrumbs, typically placed at the top of the ContentShell. - */ -export function BreadcrumbBar({ children, className = '' }: BreadcrumbBarProps) { +export const BreadcrumbBar = ({ + items, + actions +}: BreadcrumbBarProps) => { return ( -
- {children} -
+ + + {actions && ( + + {actions} + + )} + ); -} +}; diff --git a/apps/website/ui/Breadcrumbs.tsx b/apps/website/ui/Breadcrumbs.tsx index 4037c9676..69abf229b 100644 --- a/apps/website/ui/Breadcrumbs.tsx +++ b/apps/website/ui/Breadcrumbs.tsx @@ -1,50 +1,39 @@ -import { Link } from '@/ui/Link'; -import { Box } from '@/ui/primitives/Box'; -import { Stack } from '@/ui/primitives/Stack'; -import { Text } from '@/ui/Text'; +import React from 'react'; +import { Box } from './primitives/Box'; +import { Text } from './Text'; +import { ChevronRight } from 'lucide-react'; +import { Icon } from './Icon'; +import { Link } from './Link'; -export type BreadcrumbItem = { +export interface BreadcrumbItem { label: string; href?: string; -}; +} -interface BreadcrumbsProps { +export interface BreadcrumbsProps { items: BreadcrumbItem[]; } -export function Breadcrumbs({ items }: BreadcrumbsProps) { - if (!items || items.length === 0) { - return null; - } - - const lastIndex = items.length - 1; - +export const Breadcrumbs = ({ items }: BreadcrumbsProps) => { return ( - - - {items.map((item, index) => { - const isLast = index === lastIndex; - const content = item.href && !isLast ? ( - - {item.label} - - ) : ( - {item.label} - ); - - return ( - - {index > 0 && ( - / - )} - {content} - - ); - })} - + + {items.map((item, index) => { + const isLast = index === items.length - 1; + return ( + + {index > 0 && } + {isLast || !item.href ? ( + + {item.label} + + ) : ( + + {item.label} + + )} + + ); + })} ); -} \ No newline at end of file +}; diff --git a/apps/website/ui/Button.tsx b/apps/website/ui/Button.tsx index 950ee3d87..cadae58db 100644 --- a/apps/website/ui/Button.tsx +++ b/apps/website/ui/Button.tsx @@ -1,14 +1,13 @@ -import React, { ReactNode, MouseEventHandler, ButtonHTMLAttributes, forwardRef } from 'react'; +import React, { ReactNode, MouseEventHandler, forwardRef } from 'react'; import { Stack } from './primitives/Stack'; -import { Box, BoxProps } from './primitives/Box'; -import { Loader2 } from 'lucide-react'; import { Icon } from './Icon'; +import { Loader2 } from 'lucide-react'; +import { ResponsiveValue } from './primitives/Box'; -interface ButtonProps extends Omit, 'as' | 'onMouseEnter' | 'onMouseLeave' | 'onSubmit' | 'role' | 'translate' | 'onScroll' | 'draggable' | 'onChange' | 'onMouseDown' | 'onMouseUp' | 'onMouseMove' | 'value' | 'onBlur' | 'onKeyDown'>, Omit, 'as' | 'onClick' | 'onSubmit'> { +export interface ButtonProps { children: ReactNode; - onClick?: MouseEventHandler; - className?: string; - variant?: 'primary' | 'secondary' | 'danger' | 'ghost' | 'race-final' | 'discord'; + onClick?: MouseEventHandler; + variant?: 'primary' | 'secondary' | 'danger' | 'ghost' | 'success' | 'discord' | 'race-final'; size?: 'sm' | 'md' | 'lg'; disabled?: boolean; isLoading?: boolean; @@ -19,14 +18,25 @@ interface ButtonProps extends Omit, 'as' href?: string; target?: string; rel?: string; + className?: string; + style?: React.CSSProperties; + width?: string | number | ResponsiveValue; + height?: string | number | ResponsiveValue; + minWidth?: string | number; + px?: number; + py?: number; + p?: number; + rounded?: string; + bg?: string; + color?: string; fontSize?: string; - backgroundColor?: string; + h?: string; + w?: string; } -export const Button = forwardRef(({ +export const Button = forwardRef(({ children, onClick, - className = '', variant = 'primary', size = 'md', disabled = false, @@ -38,25 +48,37 @@ export const Button = forwardRef(({ href, target, rel, + className, + style: styleProp, + width, + height, + minWidth, + px, + py, + p, + rounded, + bg, + color, fontSize, - backgroundColor, - ...props + h, + w, }, 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-in-out focus-visible:outline focus-visible:outline-1 focus-visible:outline-offset-2 active:opacity-80 uppercase tracking-widest font-bold'; const variantClasses = { - primary: 'bg-primary-accent text-white hover:bg-primary-accent/90 focus-visible:outline-primary-accent shadow-[0_0_15px_rgba(25,140,255,0.3)] hover:shadow-[0_0_25px_rgba(25,140,255,0.5)]', - secondary: 'bg-panel-gray text-white border border-border-gray hover:bg-border-gray/50 focus-visible:outline-primary-accent', - danger: 'bg-critical-red text-white hover:bg-critical-red/90 focus-visible:outline-critical-red', - ghost: 'bg-transparent text-gray-400 hover:text-white hover:bg-white/5 focus-visible:outline-gray-400', - 'race-final': 'bg-success-green text-graphite-black hover:bg-success-green/90 focus-visible:outline-success-green', + primary: 'bg-[var(--ui-color-intent-primary)] text-white hover:opacity-90 focus-visible:outline-[var(--ui-color-intent-primary)] shadow-[0_0_15px_rgba(25,140,255,0.3)] hover:shadow-[0_0_25px_rgba(25,140,255,0.5)]', + secondary: 'bg-[var(--ui-color-bg-surface)] text-white border border-[var(--ui-color-border-default)] hover:bg-[var(--ui-color-border-default)] focus-visible:outline-[var(--ui-color-intent-primary)]', + danger: 'bg-[var(--ui-color-intent-critical)] text-white hover:opacity-90 focus-visible:outline-[var(--ui-color-intent-critical)]', + ghost: 'bg-transparent text-[var(--ui-color-text-low)] hover:text-[var(--ui-color-text-high)] hover:bg-white/5 focus-visible:outline-[var(--ui-color-text-low)]', + success: 'bg-[var(--ui-color-intent-success)] text-[var(--ui-color-bg-base)] hover:opacity-90 focus-visible:outline-[var(--ui-color-intent-success)]', + 'race-final': 'bg-[var(--ui-color-intent-success)] text-[var(--ui-color-bg-base)] hover:opacity-90 focus-visible:outline-[var(--ui-color-intent-success)]', discord: 'bg-[#5865F2] text-white hover:bg-[#4752C4] focus-visible:outline-[#5865F2]', }; const sizeClasses = { - sm: 'min-h-[32px] px-3 py-1 text-xs font-medium', - md: 'min-h-[40px] px-4 py-2 text-sm font-medium', - lg: 'min-h-[48px] px-6 py-3 text-base font-medium' + sm: 'min-h-[32px] px-3 py-1 text-xs', + md: 'min-h-[40px] px-4 py-2 text-sm', + lg: 'min-h-[48px] px-6 py-3 text-base' }; const disabledClasses = (disabled || isLoading) ? 'opacity-40 cursor-not-allowed' : 'cursor-pointer'; @@ -71,8 +93,18 @@ export const Button = forwardRef(({ className ].filter(Boolean).join(' '); + const style: React.CSSProperties = { + ...(width ? { width: typeof width === 'object' ? undefined : width } : {}), + ...(height ? { height: typeof height === 'object' ? undefined : height } : {}), + ...(minWidth ? { minWidth } : {}), + ...(fontSize ? { fontSize } : {}), + ...(h ? { height: h } : {}), + ...(w ? { width: w } : {}), + ...(styleProp || {}) + }; + const content = ( - + {isLoading && } {!isLoading && icon} {children} @@ -81,35 +113,31 @@ export const Button = forwardRef(({ if (as === 'a') { return ( - } href={href} target={target} rel={rel} className={classes} - fontSize={fontSize} - backgroundColor={backgroundColor} - {...props} + onClick={onClick as MouseEventHandler} + style={style} > {content} - + ); } return ( - } type={type} className={classes} - onClick={onClick} + onClick={onClick as MouseEventHandler} disabled={disabled || isLoading} - fontSize={fontSize} - backgroundColor={backgroundColor} - {...props} + style={style} > {content} - + ); }); diff --git a/apps/website/ui/Card.tsx b/apps/website/ui/Card.tsx index 9440b1b31..980096b2a 100644 --- a/apps/website/ui/Card.tsx +++ b/apps/website/ui/Card.tsx @@ -1,49 +1,73 @@ -import React, { ReactNode, MouseEventHandler } from 'react'; -import { Box, BoxProps } from './primitives/Box'; +import React, { ReactNode, forwardRef } from 'react'; +import { Surface, SurfaceProps } from './primitives/Surface'; +import { Box } from './primitives/Box'; -export interface CardProps extends Omit, 'children' | 'onClick'> { +export interface CardProps extends Omit, 'children' | 'title' | 'variant'> { children: ReactNode; - onClick?: MouseEventHandler; - variant?: 'default' | 'outline' | 'ghost' | 'muted' | 'dark' | 'glass'; + variant?: 'default' | 'dark' | 'muted' | 'glass' | 'outline'; + title?: ReactNode; + footer?: ReactNode; } -export function Card({ +export const Card = forwardRef(({ children, - className = '', - onClick, variant = 'default', + title, + footer, ...props -}: CardProps) { - const baseClasses = 'rounded-none transition-all duration-150 ease-smooth'; +}, ref) => { + const isOutline = variant === 'outline'; - const variantClasses = { - default: 'bg-panel-gray border border-border-gray shadow-card', - outline: 'bg-transparent border border-border-gray', - 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 style: React.CSSProperties = isOutline ? { + backgroundColor: 'transparent', + border: '1px solid var(--ui-color-border-default)', + } : {}; - const classes = [ - baseClasses, - variantClasses[variant], - onClick ? 'cursor-pointer hover:bg-border-gray/30' : '', - className - ].filter(Boolean).join(' '); - - // Default padding if none provided - const hasPadding = props.p !== undefined || props.px !== undefined || props.py !== undefined || - props.pt !== undefined || props.pb !== undefined || props.pl !== undefined || props.pr !== undefined; - return ( - - {children} - + {title && ( + + {typeof title === 'string' ? ( +

{title}

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

{title}

} + {children} +
+); + +export const CardContent = ({ children }: { children: ReactNode }) => ( + {children} +); + +export const CardFooter = ({ children }: { children: ReactNode }) => ( + + {children} + +); diff --git a/apps/website/ui/CategoryDistribution.tsx b/apps/website/ui/CategoryDistribution.tsx index 8aa07e8fd..dd6968dd5 100644 --- a/apps/website/ui/CategoryDistribution.tsx +++ b/apps/website/ui/CategoryDistribution.tsx @@ -1,77 +1,37 @@ +import React from 'react'; +import { Box } from './primitives/Box'; +import { CategoryDistributionCard } from './CategoryDistributionCard'; +import { LucideIcon } from 'lucide-react'; - -import { CategoryDistributionCard } from '@/ui/CategoryDistributionCard'; -import { Heading } from '@/ui/Heading'; -import { Icon } from '@/ui/Icon'; -import { Box } from '@/ui/primitives/Box'; -import { Grid } from '@/ui/primitives/Grid'; -import { Text } from '@/ui/Text'; -import { BarChart3 } from 'lucide-react'; - -const CATEGORIES = [ - { id: 'beginner', label: 'Beginner', color: 'text-green-400', bgColor: 'bg-green-400/10', borderColor: 'border-green-400/30', progressColor: 'bg-green-400' }, - { id: 'intermediate', label: 'Intermediate', color: 'text-primary-blue', bgColor: 'bg-primary-blue/10', borderColor: 'border-primary-blue/30', progressColor: 'bg-primary-blue' }, - { id: 'advanced', label: 'Advanced', color: 'text-purple-400', bgColor: 'bg-purple-400/10', borderColor: 'border-purple-400/30', progressColor: 'bg-purple-400' }, - { id: 'pro', label: 'Pro', color: 'text-yellow-400', bgColor: 'bg-yellow-400/10', borderColor: 'border-yellow-400/30', progressColor: 'bg-yellow-400' }, - { id: 'endurance', label: 'Endurance', color: 'text-orange-400', bgColor: 'bg-orange-400/10', borderColor: 'border-orange-400/30', progressColor: 'bg-orange-400' }, - { id: 'sprint', label: 'Sprint', color: 'text-red-400', bgColor: 'bg-red-400/10', borderColor: 'border-red-400/30', progressColor: 'bg-red-400' }, -]; - -interface CategoryDistributionProps { - drivers: { - category?: string; - }[]; +export interface CategoryData { + id: string; + label: string; + count: number; + icon: LucideIcon; + intent?: 'primary' | 'success' | 'warning' | 'critical' | 'telemetry'; } -export function CategoryDistribution({ drivers }: CategoryDistributionProps) { - const distribution = CATEGORIES.map((category) => ({ - ...category, - count: drivers.filter((d) => d.category === category.id).length, - percentage: drivers.length > 0 - ? Math.round((drivers.filter((d) => d.category === category.id).length / drivers.length) * 100) - : 0, - })); +export interface CategoryDistributionProps { + categories: CategoryData[]; +} + +export const CategoryDistribution = ({ + categories +}: CategoryDistributionProps) => { + const total = categories.reduce((acc, cat) => acc + cat.count, 0); return ( - - - - - - - Category Distribution - Driver population by category - - - - - {distribution.map((category) => ( - - ))} - + + {categories.map((category) => ( + + ))} ); -} +}; diff --git a/apps/website/ui/CategoryDistributionCard.tsx b/apps/website/ui/CategoryDistributionCard.tsx index a33b4c6bc..fda083760 100644 --- a/apps/website/ui/CategoryDistributionCard.tsx +++ b/apps/website/ui/CategoryDistributionCard.tsx @@ -3,47 +3,57 @@ import { Box } from './primitives/Box'; import { Text } from './Text'; import { Icon } from './Icon'; import { LucideIcon } from 'lucide-react'; +import { Surface } from './primitives/Surface'; -interface CategoryDistributionCardProps { +export interface CategoryDistributionCardProps { label: string; count: number; - percentage: number; + total: number; icon: LucideIcon; - color: string; - bgColor: string; - borderColor: string; + intent?: 'primary' | 'success' | 'warning' | 'critical' | 'telemetry'; } -export function CategoryDistributionCard({ - label, - count, - percentage, - icon, - color, - bgColor, - borderColor, -}: CategoryDistributionCardProps) { +export const CategoryDistributionCard = ({ + label, + count, + total, + icon, + intent = 'primary' +}: CategoryDistributionCardProps) => { + const percentage = total > 0 ? (count / total) * 100 : 0; + + const intentColorMap = { + primary: 'var(--ui-color-intent-primary)', + success: 'var(--ui-color-intent-success)', + warning: 'var(--ui-color-intent-warning)', + critical: 'var(--ui-color-intent-critical)', + telemetry: 'var(--ui-color-intent-telemetry)', + }; + return ( - - - {count} - - + + + {count} + + - + + {label} - - + - - {percentage.toFixed(1)}% of total + + + {Math.round(percentage)}% of total - + ); -} +}; diff --git a/apps/website/ui/CategoryIcon.tsx b/apps/website/ui/CategoryIcon.tsx index 1bddac9cf..6327a6c87 100644 --- a/apps/website/ui/CategoryIcon.tsx +++ b/apps/website/ui/CategoryIcon.tsx @@ -1,44 +1,29 @@ import React from 'react'; import { Box } from './primitives/Box'; -import { Image } from './Image'; -import { Tag } from 'lucide-react'; import { Icon } from './Icon'; +import { Tag } from 'lucide-react'; export interface CategoryIconProps { - categoryId?: string; - src?: string; - alt: string; + category: string; size?: number; - className?: string; } -export function CategoryIcon({ - categoryId, - src, - alt, - size = 24, - className = '', -}: CategoryIconProps) { - const iconSrc = src || (categoryId ? `/media/categories/${categoryId}/icon` : undefined); - +export const CategoryIcon = ({ + category, + size = 24 +}: CategoryIconProps) => { + // Map categories to icons if needed, for now just use Tag return ( - - {iconSrc ? ( - {alt} - ) : ( - 20 ? 4 : 3} color="text-gray-500" /> - )} + 20 ? 4 : 3} intent="low" /> ); -} +}; diff --git a/apps/website/ui/Checkbox.tsx b/apps/website/ui/Checkbox.tsx index 5b9acd3ba..227d32d90 100644 --- a/apps/website/ui/Checkbox.tsx +++ b/apps/website/ui/Checkbox.tsx @@ -1,33 +1,50 @@ -import React from 'react'; +import React, { forwardRef, ChangeEvent } from 'react'; import { Box } from './primitives/Box'; import { Text } from './Text'; -interface CheckboxProps { +export interface CheckboxProps { label: string; checked: boolean; onChange: (checked: boolean) => void; disabled?: boolean; + error?: string; } -export function Checkbox({ label, checked, onChange, disabled }: CheckboxProps) { +export const Checkbox = forwardRef(({ + label, + checked, + onChange, + disabled = false, + error +}, ref) => { + const handleChange = (e: ChangeEvent) => { + onChange(e.target.checked); + }; + return ( - - ) => onChange(e.target.checked)} - disabled={disabled} - w="4" - h="4" - bg="bg-deep-graphite" - border - borderColor="border-charcoal-outline" - rounded="sm" - ring="primary-blue" - color="text-primary-blue" - /> - {label} + + + + + {label} + + + {error && ( + + + {error} + + + )} ); -} +}); + +Checkbox.displayName = 'Checkbox'; diff --git a/apps/website/ui/CircularProgress.tsx b/apps/website/ui/CircularProgress.tsx index 1216b7008..5c303d7e6 100644 --- a/apps/website/ui/CircularProgress.tsx +++ b/apps/website/ui/CircularProgress.tsx @@ -1,53 +1,79 @@ +import React from 'react'; +import { Box } from './primitives/Box'; +import { Text } from './Text'; - - -interface CircularProgressProps { +export interface CircularProgressProps { value: number; - max: number; - label: string; - color: string; + max?: number; size?: number; + strokeWidth?: number; + intent?: 'primary' | 'success' | 'warning' | 'critical' | 'telemetry'; + showValue?: boolean; + label?: string; + color?: string; // Alias for intent } -export function CircularProgress({ value, max, label, color, size = 80 }: CircularProgressProps) { - const percentage = Math.min((value / max) * 100, 100); - const strokeWidth = 6; +export const CircularProgress = ({ + value, + max = 100, + size = 64, + strokeWidth = 4, + intent = 'primary', + showValue = false, + label, + color: colorProp +}: CircularProgressProps) => { const radius = (size - strokeWidth) / 2; const circumference = radius * 2 * Math.PI; - const strokeDashoffset = circumference - (percentage / 100) * circumference; + const offset = circumference - (value / max) * circumference; + + const intentColorMap = { + primary: 'var(--ui-color-intent-primary)', + success: 'var(--ui-color-intent-success)', + warning: 'var(--ui-color-intent-warning)', + critical: 'var(--ui-color-intent-critical)', + telemetry: 'var(--ui-color-intent-telemetry)', + }; + + const color = colorProp || intentColorMap[intent]; return ( -
-
- + + + -
- {percentage.toFixed(0)}% -
-
- {label} -
+ {showValue && ( + + + {Math.round((value / max) * 100)}% + + + )} +
+ {label && ( + + {label} + + )} +
); -} \ No newline at end of file +}; diff --git a/apps/website/ui/Container.tsx b/apps/website/ui/Container.tsx index 9ba3bfa9d..c99baca6e 100644 --- a/apps/website/ui/Container.tsx +++ b/apps/website/ui/Container.tsx @@ -1,53 +1,26 @@ import React, { ReactNode } from 'react'; -import { Box, BoxProps } from './primitives/Box'; +import { Box } from './primitives/Box'; -type Spacing = 0 | 0.5 | 1 | 1.5 | 2 | 2.5 | 3 | 3.5 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 14 | 16 | 20 | 24 | 28 | 32 | 36 | 40 | 44 | 48 | 52 | 56 | 60 | 64 | 72 | 80 | 96; - -interface ContainerProps extends Omit, 'size' | 'padding'> { +export interface ContainerProps { children: ReactNode; size?: 'sm' | 'md' | 'lg' | 'xl' | 'full'; - padding?: boolean; - className?: string; - py?: Spacing; - pb?: Spacing; } -export function Container({ +export const Container = ({ children, - size = 'lg', - padding = true, - className = '', - py, - pb, - ...props -}: ContainerProps) { - const sizeClasses = { - sm: 'max-w-2xl', - md: 'max-w-4xl', - lg: 'max-w-7xl', - xl: 'max-w-[1400px]', - full: 'max-w-full' + size = 'lg' +}: ContainerProps) => { + const sizeMap = { + sm: '40rem', + md: '48rem', + lg: '64rem', + xl: '80rem', + full: '100%', }; - const spacingMap: Record = { - 0: '0', 0.5: '0.5', 1: '1', 1.5: '1.5', 2: '2', 2.5: '2.5', 3: '3', 3.5: '3.5', 4: '4', - 5: '5', 6: '6', 7: '7', 8: '8', 9: '9', 10: '10', 11: '11', 12: '12', 14: '14', - 16: '16', 20: '20', 24: '24', 28: '28', 32: '32', 36: '36', 40: '40', 44: '44', - 48: '48', 52: '52', 56: '56', 60: '60', 64: '64', 72: '72', 80: '80', 96: '96' - }; - - const classes = [ - 'mx-auto', - sizeClasses[size], - padding ? 'px-4 sm:px-6 lg:px-8' : '', - py !== undefined ? `py-${spacingMap[py]}` : '', - pb !== undefined ? `pb-${spacingMap[pb]}` : '', - className - ].filter(Boolean).join(' '); - return ( - + {children} ); -} +}; diff --git a/apps/website/ui/ContentShell.tsx b/apps/website/ui/ContentShell.tsx index 858e6e59f..28c949e48 100644 --- a/apps/website/ui/ContentShell.tsx +++ b/apps/website/ui/ContentShell.tsx @@ -1,20 +1,34 @@ -import React from 'react'; +import React, { ReactNode } from 'react'; +import { Box } from './primitives/Box'; -interface ContentShellProps { - children: React.ReactNode; - className?: string; +export interface ContentShellProps { + children: ReactNode; + header?: ReactNode; + sidebar?: ReactNode; } -/** - * ContentShell is the main data zone of the application. - * It houses the primary content and track maps/data tables. - */ -export function ContentShell({ children, className = '' }: ContentShellProps) { +export const ContentShell = ({ + children, + header, + sidebar +}: ContentShellProps) => { return ( -
-
- {children} -
-
+ + {header && ( + + {header} + + )} + + {sidebar && ( + + {sidebar} + + )} + + {children} + + + ); -} +}; diff --git a/apps/website/ui/ContentViewport.tsx b/apps/website/ui/ContentViewport.tsx index 00107b5ef..47df522a2 100644 --- a/apps/website/ui/ContentViewport.tsx +++ b/apps/website/ui/ContentViewport.tsx @@ -1,22 +1,30 @@ -import React from 'react'; +import React, { ReactNode } from 'react'; +import { Box } from './primitives/Box'; +import { Container } from './Container'; -interface ContentViewportProps { - children: React.ReactNode; - className?: string; - fullWidth?: boolean; +export interface ContentViewportProps { + children: ReactNode; + padding?: 'none' | 'sm' | 'md' | 'lg'; } -/** - * ContentViewport is the main data zone of the "Telemetry Workspace". - * It houses the primary content, track maps, and data tables. - * Aligned with "Precision Racing Minimal" theme. - */ -export function ContentViewport({ children, className = '', fullWidth = false }: ContentViewportProps) { +export const ContentViewport = ({ + children, + padding = 'md' +}: ContentViewportProps) => { + const paddingMap: Record = { + none: 0, + sm: 4, + md: 8, + lg: 12, + }; + return ( -
-
- {children} -
-
+ + + + {children} + + + ); -} +}; diff --git a/apps/website/ui/ControlBar.tsx b/apps/website/ui/ControlBar.tsx index 5185d95bc..28ca5e18a 100644 --- a/apps/website/ui/ControlBar.tsx +++ b/apps/website/ui/ControlBar.tsx @@ -1,21 +1,32 @@ -import React from 'react'; +import React, { ReactNode } from 'react'; +import { Box } from './primitives/Box'; +import { Surface } from './primitives/Surface'; -interface ControlBarProps { - children: React.ReactNode; - className?: string; +export interface ControlBarProps { + children: ReactNode; + actions?: ReactNode; } -/** - * ControlBar is the top-level header of the "Telemetry Workspace". - * It provides global controls, navigation, and status information. - * Aligned with "Precision Racing Minimal" theme. - */ -export function ControlBar({ children, className = '' }: ControlBarProps) { +export const ControlBar = ({ + children, + actions +}: ControlBarProps) => { return ( -
- {children} -
+ + + {children} + + {actions && ( + + {actions} + + )} + + ); -} +}; diff --git a/apps/website/ui/CountryFlag.tsx b/apps/website/ui/CountryFlag.tsx index 8f1979a51..b8382b58a 100644 --- a/apps/website/ui/CountryFlag.tsx +++ b/apps/website/ui/CountryFlag.tsx @@ -1,97 +1,36 @@ +import React from 'react'; +import { Box } from './primitives/Box'; - - -// ISO 3166-1 alpha-2 country code to full country name mapping -const countryNames: Record = { - 'US': 'United States', - 'GB': 'United Kingdom', - 'CA': 'Canada', - 'AU': 'Australia', - 'NZ': 'New Zealand', - 'DE': 'Germany', - 'FR': 'France', - 'IT': 'Italy', - 'ES': 'Spain', - 'NL': 'Netherlands', - 'BE': 'Belgium', - 'SE': 'Sweden', - 'NO': 'Norway', - 'DK': 'Denmark', - 'FI': 'Finland', - 'PL': 'Poland', - 'CZ': 'Czech Republic', - 'AT': 'Austria', - 'CH': 'Switzerland', - 'PT': 'Portugal', - 'IE': 'Ireland', - 'BR': 'Brazil', - 'MX': 'Mexico', - 'AR': 'Argentina', - 'JP': 'Japan', - 'CN': 'China', - 'KR': 'South Korea', - 'IN': 'India', - 'SG': 'Singapore', - 'TH': 'Thailand', - 'MY': 'Malaysia', - 'ID': 'Indonesia', - 'PH': 'Philippines', - 'ZA': 'South Africa', - 'RU': 'Russia', - 'MC': 'Monaco', - 'TR': 'Turkey', - 'GR': 'Greece', - 'HU': 'Hungary', - 'RO': 'Romania', - 'BG': 'Bulgaria', - 'HR': 'Croatia', - 'SI': 'Slovenia', - 'SK': 'Slovakia', - 'LT': 'Lithuania', - 'LV': 'Latvia', - 'EE': 'Estonia', -}; - -// ISO 3166-1 alpha-2 country code to flag emoji conversion -const countryCodeToFlag = (countryCode: string): string => { - if (!countryCode || countryCode.length !== 2) return '🏁'; - - // Convert ISO 3166-1 alpha-2 to regional indicator symbols - const codePoints = countryCode - .toUpperCase() - .split('') - .map(char => 127397 + char.charCodeAt(0)); - return String.fromCodePoint(...codePoints); -}; - -interface CountryFlagProps { +export interface CountryFlagProps { countryCode: string; size?: 'sm' | 'md' | 'lg'; - className?: string; - showTooltip?: boolean; } -export function CountryFlag({ +export const CountryFlag = ({ countryCode, - size = 'md', - className = '', - showTooltip = true -}: CountryFlagProps) { - const sizeClasses = { - sm: 'text-xs', - md: 'text-sm', - lg: 'text-base' + size = 'md' +}: CountryFlagProps) => { + const sizeMap = { + sm: '1rem', + md: '1.5rem', + lg: '2rem', }; - - const flag = countryCodeToFlag(countryCode); - const countryName = countryNames[countryCode.toUpperCase()] || countryCode; - + return ( - - {flag} - + {countryCode} +
); -} +}; diff --git a/apps/website/ui/DangerZone.tsx b/apps/website/ui/DangerZone.tsx index 4be6b844e..ba017e47f 100644 --- a/apps/website/ui/DangerZone.tsx +++ b/apps/website/ui/DangerZone.tsx @@ -1,26 +1,37 @@ import React, { ReactNode } from 'react'; import { Box } from './primitives/Box'; -import { Text } from './Text'; import { Heading } from './Heading'; -import { Card } from './Card'; +import { Text } from './Text'; +import { Surface } from './primitives/Surface'; -interface DangerZoneProps { +export interface DangerZoneProps { title: string; description: string; children: ReactNode; } -export function DangerZone({ title, description, children }: DangerZoneProps) { +export const DangerZone = ({ + title, + description, + children +}: DangerZoneProps) => { return ( - - Danger Zone - - {title} - - {description} - + + Danger Zone + + + {title} + + {description} + + {children} - - + +
); -} +}; diff --git a/apps/website/ui/DateHeader.tsx b/apps/website/ui/DateHeader.tsx index afc97ddd7..b267b9929 100644 --- a/apps/website/ui/DateHeader.tsx +++ b/apps/website/ui/DateHeader.tsx @@ -1,30 +1,31 @@ import React from 'react'; -import { Calendar } from 'lucide-react'; import { Box } from './primitives/Box'; -import { Stack } from './primitives/Stack'; import { Text } from './Text'; +import { Stack } from './primitives/Stack'; +import { Calendar } from 'lucide-react'; import { Icon } from './Icon'; -interface DateHeaderProps { - label: string; - count?: number; - countLabel?: string; +export interface DateHeaderProps { + date: string; + showIcon?: boolean; } -export function DateHeader({ label, count, countLabel = 'races' }: DateHeaderProps) { +export const DateHeader = ({ + date, + showIcon = true +}: DateHeaderProps) => { return ( - - - - - - {label} - - {count !== undefined && ( - - {count} {count === 1 ? countLabel.replace(/s$/, '') : countLabel} - + + {showIcon && ( + + + )} + + + {date} + + ); -} +}; diff --git a/apps/website/ui/DurationField.tsx b/apps/website/ui/DurationField.tsx index bb705464c..31027f4c4 100644 --- a/apps/website/ui/DurationField.tsx +++ b/apps/website/ui/DurationField.tsx @@ -1,71 +1,73 @@ +import React, { ChangeEvent } from 'react'; +import { Box } from './primitives/Box'; +import { Text } from './Text'; +import { Input } from './Input'; - -import { Input } from '@/ui/Input'; - -interface DurationFieldProps { +export interface DurationFieldProps { label: string; - value: number | ''; - onChange: (value: number | '') => void; - helperText?: string; - required?: boolean; + value: number; // in minutes + onChange: (value: number) => void; disabled?: boolean; - unit?: 'minutes' | 'laps'; error?: string; } -export function DurationField({ - label, - value, - onChange, - helperText, - required, - disabled, - unit = 'minutes', - error, -}: DurationFieldProps) { - const handleChange = (raw: string) => { - if (raw.trim() === '') { - onChange(''); - return; - } +export const DurationField = ({ + label, + value, + onChange, + disabled = false, + error +}: DurationFieldProps) => { + const hours = Math.floor(value / 60); + const minutes = value % 60; - const parsed = parseInt(raw, 10); - if (Number.isNaN(parsed) || parsed <= 0) { - onChange(''); - return; - } - - onChange(parsed); + const handleHoursChange = (e: ChangeEvent) => { + const h = parseInt(e.target.value) || 0; + onChange(h * 60 + minutes); }; - const unitLabel = unit === 'laps' ? 'laps' : 'min'; + const handleMinutesChange = (e: ChangeEvent) => { + const m = parseInt(e.target.value) || 0; + onChange(hours * 60 + m); + }; return ( -
- -
-
+ + + handleChange(e.target.value)} + value={hours} + onChange={handleHoursChange} disabled={disabled} - min={1} - className="pr-16" - variant={error ? 'error' : 'default'} + min={0} + style={{ width: '4rem' }} /> -
- {unitLabel} -
- {helperText && ( -

{helperText}

- )} + h + + + + m + + {error && ( -

{error}

+ + + {error} + + )} -
+
); -} \ No newline at end of file +}; diff --git a/apps/website/ui/ErrorActionButtons.tsx b/apps/website/ui/ErrorActionButtons.tsx index 6c6211345..f1a2f75dc 100644 --- a/apps/website/ui/ErrorActionButtons.tsx +++ b/apps/website/ui/ErrorActionButtons.tsx @@ -1,55 +1,29 @@ import React from 'react'; +import { Box } from './primitives/Box'; +import { Button } from './Button'; +import { RefreshCw, Home } from 'lucide-react'; -interface ErrorActionButtonsProps { +export interface ErrorActionButtonsProps { onRetry?: () => void; - onHomeClick: () => void; - showRetry?: boolean; - homeLabel?: string; + onGoHome?: () => void; } -/** - * ErrorActionButtons - * - * Action buttons for error pages (Try Again, Go Home) - * Provides consistent styling and behavior. - * All navigation callbacks must be provided by the caller. - */ -export function ErrorActionButtons({ - onRetry, - onHomeClick, - showRetry = false, - homeLabel = 'Drive home', -}: ErrorActionButtonsProps) { - if (showRetry && onRetry) { - return ( -
- - -
- ); - } - +export const ErrorActionButtons = ({ + onRetry, + onGoHome +}: ErrorActionButtonsProps) => { return ( -
- -
+ + {onRetry && ( + + )} + {onGoHome && ( + + )} + ); -} \ No newline at end of file +}; diff --git a/apps/website/ui/ErrorBanner.tsx b/apps/website/ui/ErrorBanner.tsx index 453791dda..7c8c369cd 100644 --- a/apps/website/ui/ErrorBanner.tsx +++ b/apps/website/ui/ErrorBanner.tsx @@ -1,63 +1,45 @@ import React from 'react'; import { Box } from './primitives/Box'; -import { Stack } from './primitives/Stack'; import { Text } from './Text'; -import { Surface } from './primitives/Surface'; import { Icon } from './Icon'; -import { AlertCircle, XCircle, Info, AlertTriangle } from 'lucide-react'; +import { AlertTriangle } from 'lucide-react'; +import { Surface } from './primitives/Surface'; -interface ErrorBannerProps { +export interface ErrorBannerProps { title?: string; message: string; - variant?: 'error' | 'warning' | 'info' | 'success'; + variant?: 'error' | 'warning' | 'info'; } -export function ErrorBanner({ title, message, variant = 'error' }: ErrorBannerProps) { - const configs = { - error: { - bg: 'rgba(239, 68, 68, 0.1)', - border: 'rgba(239, 68, 68, 0.2)', - text: 'rgb(248, 113, 113)', - icon: XCircle - }, - warning: { - bg: 'rgba(245, 158, 11, 0.1)', - border: 'rgba(245, 158, 11, 0.2)', - text: 'rgb(251, 191, 36)', - icon: AlertTriangle - }, - info: { - bg: 'rgba(59, 130, 246, 0.1)', - border: 'rgba(59, 130, 246, 0.2)', - text: 'rgb(96, 165, 250)', - icon: Info - }, - success: { - bg: 'rgba(16, 185, 129, 0.1)', - border: 'rgba(16, 185, 129, 0.2)', - text: 'rgb(52, 211, 153)', - icon: AlertCircle - } - }; - - const colors = configs[variant]; +export const ErrorBanner = ({ + title, + message, + variant = 'error' +}: ErrorBannerProps) => { + const intent = variant === 'error' ? 'critical' : variant === 'warning' ? 'warning' : 'primary'; + const color = variant === 'error' ? 'rgba(227, 92, 92, 0.05)' : variant === 'warning' ? 'rgba(255, 190, 77, 0.05)' : 'rgba(25, 140, 255, 0.05)'; + const borderColor = variant === 'error' ? 'rgba(227, 92, 92, 0.2)' : variant === 'warning' ? 'rgba(255, 190, 77, 0.2)' : 'rgba(25, 140, 255, 0.2)'; return ( - - - - {title && {title}} - {message} + + + + {title && ( + + {title} + + )} + + {message} + - +
); -} +}; diff --git a/apps/website/ui/ErrorPageContainer.tsx b/apps/website/ui/ErrorPageContainer.tsx index 724bacdeb..3fea78602 100644 --- a/apps/website/ui/ErrorPageContainer.tsx +++ b/apps/website/ui/ErrorPageContainer.tsx @@ -1,29 +1,24 @@ import React, { ReactNode } from 'react'; +import { Box } from './primitives/Box'; +import { Surface } from './primitives/Surface'; -interface ErrorPageContainerProps { +export interface ErrorPageContainerProps { children: ReactNode; - errorCode: string; - description: string; } -/** - * ErrorPageContainer - * - * A reusable container for error pages (404, 500, etc.) - * Provides consistent styling and layout for error states. - */ -export function ErrorPageContainer({ - children, - errorCode, - description, -}: ErrorPageContainerProps) { +export const ErrorPageContainer = ({ children }: ErrorPageContainerProps) => { return ( -
-
-

{errorCode}

-

{description}

+ + {children} -
-
+ +
); -} \ No newline at end of file +}; diff --git a/apps/website/ui/FeedEmptyState.tsx b/apps/website/ui/FeedEmptyState.tsx index 4b244dbbf..d1856b24e 100644 --- a/apps/website/ui/FeedEmptyState.tsx +++ b/apps/website/ui/FeedEmptyState.tsx @@ -1,34 +1,29 @@ -import { Button } from '@/ui/Button'; -import { Card } from '@/ui/Card'; -import { Heading } from '@/ui/Heading'; -import { Box } from '@/ui/primitives/Box'; -import { Text } from '@/ui/Text'; +import React from 'react'; +import { Box } from './primitives/Box'; +import { Text } from './Text'; +import { Icon } from './Icon'; +import { MessageSquare } from 'lucide-react'; -export function FeedEmptyState() { - return ( - - - 🏁 - - - Your feed is warming up - - - - - As leagues, teams, and friends start racing, this feed will show their latest results, - signups, and highlights. - - - - - - ); +export interface FeedEmptyStateProps { + message?: string; } + +export const FeedEmptyState = ({ + message = 'No activity yet.' +}: FeedEmptyStateProps) => { + return ( + + + + + {message} + + ); +}; diff --git a/apps/website/ui/FeedItem.tsx b/apps/website/ui/FeedItem.tsx index 19accd558..3252f9089 100644 --- a/apps/website/ui/FeedItem.tsx +++ b/apps/website/ui/FeedItem.tsx @@ -1,77 +1,46 @@ import React, { ReactNode } from 'react'; import { Box } from './primitives/Box'; import { Text } from './Text'; -import { Image } from './Image'; +import { Surface } from './primitives/Surface'; +import { Avatar } from './Avatar'; -interface FeedItemProps { - actorName?: string; - actorAvatarUrl?: string; - typeLabel: string; - headline: string; - body?: string; - timeAgo: string; - cta?: ReactNode; +export interface FeedItemProps { + user: { + name: string; + avatar?: string; + }; + content: ReactNode; + timestamp: string; + actions?: ReactNode; } -export function FeedItem({ - actorName, - actorAvatarUrl, - typeLabel, - headline, - body, - timeAgo, - cta, -}: FeedItemProps) { +export const FeedItem = ({ + user, + content, + timestamp, + actions +}: FeedItemProps) => { return ( - - - {actorAvatarUrl ? ( - - {actorName + + + + + + {user.name} + {timestamp} - ) : ( - - - {typeLabel} - + + {typeof content === 'string' ? ( + {content} + ) : content} - )} - - - - - {headline} - {body && ( - {body} - )} - - - {timeAgo} - + {actions && ( + + {actions} + + )} - {cta && ( - - {cta} - - )} - + ); -} +}; diff --git a/apps/website/ui/FilterGroup.tsx b/apps/website/ui/FilterGroup.tsx index a838ca70f..97a3ca55b 100644 --- a/apps/website/ui/FilterGroup.tsx +++ b/apps/website/ui/FilterGroup.tsx @@ -1,46 +1,61 @@ - - +import React from 'react'; import { Box } from './primitives/Box'; import { Button } from './Button'; -import { Stack } from './primitives/Stack'; +import { Text } from './Text'; +import { Surface } from './primitives/Surface'; -interface FilterOption { +export interface FilterOption { id: string; label: string; indicatorColor?: string; } -interface FilterGroupProps { +export interface FilterGroupProps { options: FilterOption[]; activeId: string; - onSelect: (id: string) => void; + onChange: (id: string) => void; + label?: string; } -export function FilterGroup({ options, activeId, onSelect }: FilterGroupProps) { +export const FilterGroup = ({ + options, + activeId, + onChange, + label +}: FilterGroupProps) => { return ( - - {options.map((option) => ( - - ))} - + + {label && ( + + {label} + + )} + + {options.map((option) => { + const isActive = option.id === activeId; + return ( + + ); + })} + + ); -} +}; diff --git a/apps/website/ui/Footer.tsx b/apps/website/ui/Footer.tsx index b855abd1d..656316583 100644 --- a/apps/website/ui/Footer.tsx +++ b/apps/website/ui/Footer.tsx @@ -1,81 +1,54 @@ -import { Link } from '@/ui/Link'; -import { Box } from '@/ui/primitives/Box'; -import { Text } from '@/ui/Text'; +import React from 'react'; +import { Box } from './primitives/Box'; +import { Container } from './Container'; +import { Text } from './Text'; +import { Link } from './Link'; -const discordUrl = process.env.NEXT_PUBLIC_DISCORD_URL || 'https://discord.gg/gridpilot'; -const xUrl = process.env.NEXT_PUBLIC_X_URL || '#'; - -export function Footer() { +export const Footer = () => { return ( - - - - - {/* Racing stripe accent */} - - - - + + + + + GridPilot + + The ultimate companion for sim racers. Track your performance, manage your team, and compete in leagues. + + + + + Platform + + Leagues + Teams + Leaderboards + + + + + Support + + Documentation + System Status + Contact Us + + + + + Legal + + Privacy Policy + Terms of Service + + - - {/* Personal message */} - - - 🏁 Built by a sim racer, for sim racers - - - Just a fellow racer tired of spreadsheets and chaos. GridPilot is my passion project to make league racing actually fun again. + + + + © {new Date().getFullYear()} GridPilot. All rights reserved. - - {/* Community links */} - - - 💬 Discord - - - 𝕏 Twitter - - - - {/* Development status */} - - - ⚡ Early development • Feedback welcome - - - © {new Date().getFullYear()} GridPilot - - - + ); -} \ No newline at end of file +}; diff --git a/apps/website/ui/FormField.tsx b/apps/website/ui/FormField.tsx index 764aeb595..881571111 100644 --- a/apps/website/ui/FormField.tsx +++ b/apps/website/ui/FormField.tsx @@ -1,45 +1,55 @@ - - -import React from 'react'; -import { Icon } from './Icon'; -import { Stack } from './primitives/Stack'; +import React, { ReactNode } from 'react'; +import { Box } from './primitives/Box'; import { Text } from './Text'; - +import { Icon } from './Icon'; import { LucideIcon } from 'lucide-react'; -interface FormFieldProps { - label: string; - icon?: LucideIcon; - children: React.ReactNode; - required?: boolean; +export interface FormFieldProps { + label?: string; error?: string; hint?: string; + required?: boolean; + icon?: LucideIcon; + children: ReactNode; } -export function FormField({ +export const FormField = ({ label, + error, + hint, + required, icon, - children, - required = false, - error, - hint, -}: FormFieldProps) { + children +}: FormFieldProps) => { return ( - - + + {label && ( + + {icon && } + + {label} + + {required && *} + + )} + {children} + {error && ( - {error} + + + {error} + + )} + {hint && !error && ( - {hint} + + + {hint} + + )} - + ); -} +}; diff --git a/apps/website/ui/FormSection.tsx b/apps/website/ui/FormSection.tsx index 3f685fe08..1565770f9 100644 --- a/apps/website/ui/FormSection.tsx +++ b/apps/website/ui/FormSection.tsx @@ -1,37 +1,33 @@ import React, { ReactNode } from 'react'; -import { Stack } from './primitives/Stack'; +import { Box } from './primitives/Box'; import { Text } from './Text'; -interface FormSectionProps { +export interface FormSectionProps { + title: string; + description?: string; children: ReactNode; - title?: string; } -/** - * FormSection - * - * Groups related form fields with an optional title. - */ -export function FormSection({ children, title }: FormSectionProps) { +export const FormSection = ({ + title, + description, + children +}: FormSectionProps) => { return ( - - {title && ( - + + + {title} - )} - + {description && ( + + {description} + + )} + + {children} - - + + ); -} +}; diff --git a/apps/website/ui/GoalCard.tsx b/apps/website/ui/GoalCard.tsx index 5eaf716a1..e3f42cbb9 100644 --- a/apps/website/ui/GoalCard.tsx +++ b/apps/website/ui/GoalCard.tsx @@ -1,41 +1,48 @@ import React from 'react'; -import { Box } from './primitives/Box'; -import { Stack } from './primitives/Stack'; -import { Text } from './Text'; -import { Heading } from './Heading'; import { Card } from './Card'; -import { ProgressBar } from './ProgressBar'; +import { Box } from './primitives/Box'; +import { Text } from './Text'; -interface GoalCardProps { +export interface GoalCardProps { title: string; + current: number; + target: number; + unit: string; icon: string; - goalLabel: string; - currentValue: number; - maxValue: number; - color?: string; } -export function GoalCard({ - title, - icon, - goalLabel, - currentValue, - maxValue, - color = 'text-primary-blue', -}: GoalCardProps) { +export const GoalCard = ({ + title, + current, + target, + unit, + icon +}: GoalCardProps) => { + const percentage = Math.min(Math.max((current / target) * 100, 0), 100); + return ( - - + + {icon} - {title} - - - - {goalLabel} - {currentValue}/{maxValue} + + {title} + {unit} - - + + + + + Progress + {Math.round(percentage)}% + + + + + + + + {current} / {target} {unit} + ); -} +}; diff --git a/apps/website/ui/Header.tsx b/apps/website/ui/Header.tsx index 9fa4062f9..5d4c3e680 100644 --- a/apps/website/ui/Header.tsx +++ b/apps/website/ui/Header.tsx @@ -1,16 +1,38 @@ -import React from 'react'; -import { Container } from '@/ui/Container'; +import React, { ReactNode } from 'react'; +import { Box } from './primitives/Box'; +import { Container } from './Container'; -interface HeaderProps { - children: React.ReactNode; +export interface HeaderProps { + children: ReactNode; + actions?: ReactNode; } -export function Header({ children }: HeaderProps) { +export const Header = ({ + children, + actions +}: HeaderProps) => { return ( -
- - {children} + + + + + {children} + + {actions && ( + + {actions} + + )} + -
+
); -} \ No newline at end of file +}; diff --git a/apps/website/ui/Heading.tsx b/apps/website/ui/Heading.tsx index 433296ef4..0457251a9 100644 --- a/apps/website/ui/Heading.tsx +++ b/apps/website/ui/Heading.tsx @@ -1,93 +1,52 @@ -import React, { ReactNode, ElementType } from 'react'; -import { Stack } from './primitives/Stack'; +import React, { ReactNode, forwardRef } from 'react'; import { Box, BoxProps, ResponsiveValue } from './primitives/Box'; -interface ResponsiveFontSize { - base?: string; - sm?: string; - md?: string; - lg?: string; - xl?: string; - '2xl'?: string; -} - -interface HeadingProps extends Omit, 'children' | 'as' | 'fontSize'> { - level: 1 | 2 | 3 | 4 | 5 | 6; +export interface HeadingProps extends BoxProps { children: ReactNode; - icon?: ReactNode; - id?: string; - groupHoverColor?: string; - truncate?: boolean; - uppercase?: boolean; - fontSize?: string | ResponsiveFontSize; - weight?: 'light' | 'normal' | 'medium' | 'semibold' | 'bold' | string; - letterSpacing?: string; + level?: 1 | 2 | 3 | 4 | 5 | 6; + weight?: 'normal' | 'medium' | 'semibold' | 'bold'; + align?: 'left' | 'center' | 'right'; + fontSize?: string | ResponsiveValue; } -export function Heading({ level, children, icon, groupHoverColor, truncate, uppercase, fontSize, weight, letterSpacing, ...props }: HeadingProps) { - const Tag = `h${level}` as ElementType; +export const Heading = forwardRef(({ + children, + level = 1, + weight = 'bold', + align = 'left', + fontSize, + ...props +}, ref) => { + const Tag = `h${level}` as const; - const levelClasses = { - 1: 'text-3xl md:text-4xl font-bold text-white tracking-tight', - 2: 'text-xl md:text-2xl font-bold text-white tracking-tight', - 3: 'text-lg font-bold text-white tracking-tight', - 4: 'text-base font-bold text-white tracking-tight', - 5: 'text-sm font-bold text-white tracking-tight uppercase tracking-wider', - 6: 'text-xs font-bold text-white tracking-tight uppercase tracking-widest', - }; - - const weightClasses: Record = { - light: 'font-light', + const weightClasses = { normal: 'font-normal', medium: 'font-medium', semibold: 'font-semibold', bold: 'font-bold' }; - - const getFontSizeClasses = (value: string | ResponsiveFontSize | undefined) => { - if (value === undefined) return ''; - if (typeof value === 'object') { - const classes = []; - if (value.base) classes.push(`text-${value.base}`); - if (value.sm) classes.push(`sm:text-${value.sm}`); - if (value.md) classes.push(`md:text-${value.md}`); - if (value.lg) classes.push(`lg:text-${value.lg}`); - if (value.xl) classes.push(`xl:text-${value.xl}`); - if (value['2xl']) classes.push(`2xl:text-${value['2xl']}`); - return classes.join(' '); - } - return `text-${value}`; + + const sizeClasses = { + 1: 'text-4xl md:text-5xl', + 2: 'text-3xl md:text-4xl', + 3: 'text-2xl md:text-3xl', + 4: 'text-xl md:text-2xl', + 5: 'text-lg md:text-xl', + 6: 'text-base md:text-lg' }; - const content = icon ? ( - - {icon} - {children} - - ) : children; - const classes = [ - levelClasses[level], - getFontSizeClasses(fontSize), - weight && weightClasses[weight as keyof typeof weightClasses] ? weightClasses[weight as keyof typeof weightClasses] : '', - letterSpacing ? `tracking-${letterSpacing}` : '', - uppercase ? 'uppercase' : '', - groupHoverColor ? `group-hover:text-${groupHoverColor}` : '', - truncate ? 'truncate' : '', - props.className - ].filter(Boolean).join(' '); + 'text-[var(--ui-color-text-high)]', + weightClasses[weight], + fontSize ? '' : sizeClasses[level], + align === 'center' ? 'text-center' : (align === 'right' ? 'text-right' : 'text-left'), + ].join(' '); return ( - - {content} + + {children} ); -} +}); + +Heading.displayName = 'Heading'; diff --git a/apps/website/ui/HorizontalBarChart.tsx b/apps/website/ui/HorizontalBarChart.tsx index 86257be15..8f64e800a 100644 --- a/apps/website/ui/HorizontalBarChart.tsx +++ b/apps/website/ui/HorizontalBarChart.tsx @@ -1,28 +1,50 @@ +import React from 'react'; +import { Box } from './primitives/Box'; +import { Text } from './Text'; +import { Stack } from './primitives/Stack'; - - -interface BarChartProps { - data: { label: string; value: number; color: string }[]; - maxValue: number; +export interface HorizontalBarChartItem { + label: string; + value: number; + color?: string; } -export function HorizontalBarChart({ data, maxValue }: BarChartProps) { +export interface HorizontalBarChartProps { + items?: HorizontalBarChartItem[]; + total?: number; + data?: HorizontalBarChartItem[]; // Alias for items + maxValue?: number; // Alias for total +} + +export const HorizontalBarChart = ({ + items, + total, + data, + maxValue +}: HorizontalBarChartProps) => { + const actualItems = items || data || []; + const actualTotal = total || maxValue || actualItems.reduce((acc, item) => acc + item.value, 0); + return ( -
- {data.map((item) => ( -
-
- {item.label} - {item.value} -
-
-
-
-
- ))} -
+ + {actualItems.map((item, index) => { + const percentage = actualTotal > 0 ? (item.value / actualTotal) * 100 : 0; + return ( + + + {item.label} + {item.value} + + + + + + ); + })} + ); -} \ No newline at end of file +}; diff --git a/apps/website/ui/HorizontalStatCard.tsx b/apps/website/ui/HorizontalStatCard.tsx index acba3fec6..b32fd68ca 100644 --- a/apps/website/ui/HorizontalStatCard.tsx +++ b/apps/website/ui/HorizontalStatCard.tsx @@ -1,46 +1,38 @@ import React from 'react'; +import { Card } from './Card'; import { Box } from './primitives/Box'; -import { Stack } from './primitives/Stack'; import { Text } from './Text'; -import { Surface } from './primitives/Surface'; import { Icon } from './Icon'; import { LucideIcon } from 'lucide-react'; -interface HorizontalStatCardProps { +export interface HorizontalStatCardProps { label: string; value: string | number; icon: LucideIcon; - iconColor?: string; - iconBgColor?: string; + intent?: 'primary' | 'success' | 'warning' | 'critical' | 'telemetry'; } -export function HorizontalStatCard({ - label, - value, - icon, - iconColor = 'text-primary-blue', - iconBgColor = 'rgba(59, 130, 246, 0.1)', -}: HorizontalStatCardProps) { +export const HorizontalStatCard = ({ + label, + value, + icon, + intent = 'primary' +}: HorizontalStatCardProps) => { return ( - - - - - + + + + + - + {label} - + {value} - - + + ); -} +}; diff --git a/apps/website/ui/HorizontalStatItem.tsx b/apps/website/ui/HorizontalStatItem.tsx index 4da2cacad..72b3d0fba 100644 --- a/apps/website/ui/HorizontalStatItem.tsx +++ b/apps/website/ui/HorizontalStatItem.tsx @@ -2,17 +2,21 @@ import React from 'react'; import { Box } from './primitives/Box'; import { Text } from './Text'; -interface HorizontalStatItemProps { +export interface HorizontalStatItemProps { label: string; value: string | number; - color?: string; + intent?: 'primary' | 'success' | 'warning' | 'critical' | 'high' | 'med' | 'low'; } -export function HorizontalStatItem({ label, value, color = 'text-white' }: HorizontalStatItemProps) { +export const HorizontalStatItem = ({ + label, + value, + intent = 'high' +}: HorizontalStatItemProps) => { return ( - - {label} - {value} + + {label} + {value} ); -} +}; diff --git a/apps/website/ui/Icon.tsx b/apps/website/ui/Icon.tsx index 75a2d7d28..5b7b1f121 100644 --- a/apps/website/ui/Icon.tsx +++ b/apps/website/ui/Icon.tsx @@ -4,77 +4,71 @@ import { Box, BoxProps } from './primitives/Box'; export interface IconProps extends Omit, 'children'> { icon: LucideIcon | React.ReactNode; - size?: number | string; - color?: string; - strokeWidth?: number; - animate?: string; - transition?: boolean; - groupHoverTextColor?: string; - groupHoverScale?: boolean; + size?: 3 | 3.5 | 4 | 5 | 6 | 7 | 8 | 10 | 12 | 16 | 'full' | number; + intent?: 'primary' | 'telemetry' | 'warning' | 'success' | 'critical' | 'high' | 'med' | 'low'; + animate?: 'spin' | 'none'; } export function Icon({ icon: IconProp, size = 4, - color, - className = '', - style, - animate, - transition, - groupHoverTextColor, - groupHoverScale, + intent, + animate = 'none', ...props }: IconProps) { const sizeMap: Record = { - 3: 'w-3 h-3', - 3.5: 'w-3.5 h-3.5', - 4: 'w-4 h-4', - 5: 'w-5 h-5', - 6: 'w-6 h-6', - 7: 'w-7 h-7', - 8: 'w-8 h-8', - 10: 'w-10 h-10', - 12: 'w-12 h-12', - 16: 'w-16 h-16', - 'full': 'w-full h-full' + 3: '0.75rem', + 3.5: '0.875rem', + 4: '1rem', + 5: '1.25rem', + 6: '1.5rem', + 7: '1.75rem', + 8: '2rem', + 10: '2.5rem', + 12: '3rem', + 16: '4rem', + 'full': '100%' }; - const sizeClass = sizeMap[size] || 'w-4 h-4'; - - // If color starts with 'text-', it's a tailwind class, so pass it as color prop to Box - const isTailwindColor = typeof color === 'string' && color.startsWith('text-'); - const combinedStyle = color && !isTailwindColor ? { color, ...style } : style; - const boxColor = isTailwindColor ? color : undefined; + const dimension = typeof size === 'string' ? sizeMap[size] : (sizeMap[size] || `${size * 0.25}rem`); - 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 intentColorMap: Record = { + primary: 'var(--ui-color-intent-primary)', + telemetry: 'var(--ui-color-intent-telemetry)', + warning: 'var(--ui-color-intent-warning)', + success: 'var(--ui-color-intent-success)', + critical: 'var(--ui-color-intent-critical)', + high: 'var(--ui-color-text-high)', + med: 'var(--ui-color-text-med)', + low: 'var(--ui-color-text-low)', + }; + + const style: React.CSSProperties = { + width: dimension, + height: dimension, + color: intent ? intentColorMap[intent] : undefined, + }; const renderIcon = () => { if (!IconProp) return null; if (typeof IconProp === 'function' || (typeof IconProp === 'object' && 'render' in IconProp)) { const LucideIconComponent = IconProp as LucideIcon; - return ; + return ; } return IconProp; }; return ( - {renderIcon()} +
+ {renderIcon()} +
); } diff --git a/apps/website/ui/IconButton.tsx b/apps/website/ui/IconButton.tsx index 40bdc82e9..bb197bb13 100644 --- a/apps/website/ui/IconButton.tsx +++ b/apps/website/ui/IconButton.tsx @@ -1,54 +1,31 @@ -import React from 'react'; -import { LucideIcon } from 'lucide-react'; -import { Button } from './Button'; +import React, { MouseEventHandler } from 'react'; +import { Button, ButtonProps } from './Button'; import { Icon } from './Icon'; -interface IconButtonProps { - icon: LucideIcon; - onClick?: React.MouseEventHandler; - variant?: 'primary' | 'secondary' | 'danger' | 'ghost'; - size?: 'sm' | 'md' | 'lg'; +export interface IconButtonProps extends Omit { + icon: any; title?: string; - disabled?: boolean; - color?: string; - className?: string; - backgroundColor?: string; } -export function IconButton({ - icon, - onClick, - variant = 'secondary', - size = 'md', +export const IconButton = ({ + icon, title, - disabled, - color, - className = '', - backgroundColor, -}: IconButtonProps) { - const sizeMap = { - sm: { w: '8', h: '8', icon: 4 }, - md: { w: '10', h: '10', icon: 5 }, - lg: { w: '12', h: '12', icon: 6 }, - }; + size = 'md', + ...props +}: IconButtonProps) => { + const iconSizeMap = { + sm: 3, + md: 4, + lg: 5 + } as const; return ( - ); -} +}; diff --git a/apps/website/ui/Image.tsx b/apps/website/ui/Image.tsx index a1b0af02b..880c4447f 100644 --- a/apps/website/ui/Image.tsx +++ b/apps/website/ui/Image.tsx @@ -1,53 +1,36 @@ -import React, { ImgHTMLAttributes } from 'react'; +import React, { useState } from 'react'; +import { Box, BoxProps } from './primitives/Box'; +import { ImagePlaceholder } from './ImagePlaceholder'; -interface ImageProps extends ImgHTMLAttributes { +export interface ImageProps extends Omit, 'children'> { src: string; alt: string; - width?: number; - height?: number; - className?: string; fallbackSrc?: string; - objectFit?: 'cover' | 'contain' | 'fill' | 'none' | 'scale-down'; - fill?: boolean; - fullWidth?: boolean; - fullHeight?: boolean; + fallbackComponent?: React.ReactNode; } -export function Image({ +export const Image = ({ src, alt, - width, - height, - className = '', - fallbackSrc, - objectFit, - fill, - fullWidth, - fullHeight, + fallbackSrc, + fallbackComponent, ...props -}: ImageProps) { - const classes = [ - objectFit ? `object-${objectFit}` : '', - fill ? 'absolute inset-0 w-full h-full' : '', - fullWidth ? 'w-full' : '', - fullHeight ? 'h-full' : '', - className - ].filter(Boolean).join(' '); +}: ImageProps) => { + const [error, setError] = useState(false); + + if (error) { + if (fallbackComponent) return <>{fallbackComponent}; + if (fallbackSrc) return ; + return ; + } return ( - // eslint-disable-next-line @next/next/no-img-element - {alt} { - if (fallbackSrc) { - (e.target as HTMLImageElement).src = fallbackSrc; - } - }} - {...props} + setError(true)} + {...props} /> ); -} +}; diff --git a/apps/website/ui/ImagePlaceholder.tsx b/apps/website/ui/ImagePlaceholder.tsx index ac20605b6..ef447a24e 100644 --- a/apps/website/ui/ImagePlaceholder.tsx +++ b/apps/website/ui/ImagePlaceholder.tsx @@ -1,86 +1,39 @@ import React from 'react'; -import { Image as ImageIcon, AlertCircle, Loader2 } from 'lucide-react'; import { Box } from './primitives/Box'; import { Icon } from './Icon'; -import { Text } from './Text'; +import { Image as ImageIcon } from 'lucide-react'; export interface ImagePlaceholderProps { - size?: number | string; + width?: string | number; + height?: string | number; + animate?: 'pulse' | 'none' | 'spin'; aspectRatio?: string; - variant?: 'default' | 'error' | 'loading'; - message?: string; - className?: string; - rounded?: 'none' | 'sm' | 'md' | 'lg' | 'xl' | '2xl' | 'full'; } -export function ImagePlaceholder({ - size = 'full', - aspectRatio = '1/1', - variant = 'default', - message, - className = '', - rounded = 'md', -}: ImagePlaceholderProps) { - const config = { - default: { - icon: ImageIcon, - color: 'text-gray-500', - bg: 'bg-charcoal-outline/20', - borderColor: 'border-charcoal-outline/50', - animate: undefined as 'spin' | 'pulse' | 'bounce' | 'fade-in' | 'none' | undefined, - }, - error: { - icon: AlertCircle, - color: 'text-amber-500', - bg: 'bg-amber-500/5', - borderColor: 'border-amber-500/20', - animate: undefined as 'spin' | 'pulse' | 'bounce' | 'fade-in' | 'none' | undefined, - }, - loading: { - icon: Loader2, - color: 'text-blue-500', - bg: 'bg-blue-500/5', - borderColor: 'border-blue-500/20', - animate: 'spin' as const, - }, - }; - - const { icon, color, bg, borderColor, animate } = config[variant]; - +export const ImagePlaceholder = ({ + width = '100%', + height = '100%', + animate = 'pulse', + aspectRatio +}: ImagePlaceholderProps) => { return ( - - {message && ( - - {message} - - )} ); -} +}; diff --git a/apps/website/ui/InfoBanner.tsx b/apps/website/ui/InfoBanner.tsx index 4a636c1af..7b08bf959 100644 --- a/apps/website/ui/InfoBanner.tsx +++ b/apps/website/ui/InfoBanner.tsx @@ -1,77 +1,72 @@ -import React from 'react'; +import React, { ReactNode } from 'react'; import { Box } from './primitives/Box'; -import { Stack } from './primitives/Stack'; import { Text } from './Text'; -import { Surface } from './primitives/Surface'; import { Icon } from './Icon'; -import { Info, AlertTriangle, AlertCircle, CheckCircle, LucideIcon } from 'lucide-react'; +import { Info, AlertTriangle, CheckCircle, XCircle } from 'lucide-react'; +import { Surface } from './primitives/Surface'; -interface InfoBannerProps { +export interface InfoBannerProps { + children?: ReactNode; + variant?: 'info' | 'warning' | 'success' | 'critical'; + type?: 'info' | 'warning' | 'success' | 'critical'; // Alias for variant title?: string; - message?: string; - children?: React.ReactNode; - variant?: 'info' | 'warning' | 'error' | 'success'; - type?: 'info' | 'warning' | 'error' | 'success'; - icon?: LucideIcon; } -export function InfoBanner({ title, message, children, variant = 'info', type, icon }: InfoBannerProps) { - const configs = { +export const InfoBanner = ({ + children, + variant, + type, + title +}: InfoBannerProps) => { + const activeVariant = variant || type || 'info'; + + const config = { info: { - bg: 'rgba(59, 130, 246, 0.1)', - border: 'rgba(59, 130, 246, 0.2)', - iconColor: 'rgb(96, 165, 250)', - icon: Info + icon: Info, + intent: 'primary' as const, + bg: 'rgba(25, 140, 255, 0.05)', + border: 'rgba(25, 140, 255, 0.2)', }, warning: { - bg: 'rgba(245, 158, 11, 0.1)', - border: 'rgba(245, 158, 11, 0.2)', - iconColor: 'rgb(251, 191, 36)', - icon: AlertTriangle - }, - error: { - bg: 'rgba(239, 68, 68, 0.1)', - border: 'rgba(239, 68, 68, 0.2)', - iconColor: 'rgb(248, 113, 113)', - icon: AlertCircle + icon: AlertTriangle, + intent: 'warning' as const, + bg: 'rgba(255, 190, 77, 0.05)', + border: 'rgba(255, 190, 77, 0.2)', }, success: { - bg: 'rgba(16, 185, 129, 0.1)', - border: 'rgba(16, 185, 129, 0.2)', - iconColor: 'rgb(52, 211, 153)', - icon: CheckCircle - } - }; - - const activeVariant = type || variant; - const config = configs[activeVariant as keyof typeof configs] || configs.info; - const BannerIcon = icon || config.icon; + icon: CheckCircle, + intent: 'success' as const, + bg: 'rgba(111, 227, 122, 0.05)', + border: 'rgba(111, 227, 122, 0.2)', + }, + critical: { + icon: XCircle, + intent: 'critical' as const, + bg: 'rgba(227, 92, 92, 0.05)', + border: 'rgba(227, 92, 92, 0.2)', + }, + }[activeVariant]; return ( - - + + {title && ( - + {title} )} - {message && ( - - {message} - - )} - {children} + + {children} + - + ); -} +}; diff --git a/apps/website/ui/InfoBox.tsx b/apps/website/ui/InfoBox.tsx index 6bedfd120..1dfc7b82f 100644 --- a/apps/website/ui/InfoBox.tsx +++ b/apps/website/ui/InfoBox.tsx @@ -1,66 +1,72 @@ import React from 'react'; import { Box } from './primitives/Box'; -import { Stack } from './primitives/Stack'; import { Text } from './Text'; -import { Surface } from './primitives/Surface'; import { Icon } from './Icon'; import { LucideIcon } from 'lucide-react'; +import { Surface } from './primitives/Surface'; -interface InfoBoxProps { +export interface InfoBoxProps { title: string; description: string; icon: LucideIcon; - variant?: 'info' | 'warning' | 'error' | 'success'; + intent?: 'primary' | 'success' | 'warning' | 'critical' | 'telemetry'; + variant?: string; // Alias for intent } -export function InfoBox({ title, description, icon, variant = 'info' }: InfoBoxProps) { - const configs = { - info: { - bg: 'rgba(59, 130, 246, 0.1)', - border: 'rgba(59, 130, 246, 0.2)', - icon: 'rgb(96, 165, 250)', - text: 'text-white' - }, - warning: { - bg: 'rgba(245, 158, 11, 0.1)', - border: 'rgba(245, 158, 11, 0.2)', - icon: 'rgb(251, 191, 36)', - text: 'text-white' - }, - error: { - bg: 'rgba(239, 68, 68, 0.1)', - border: 'rgba(239, 68, 68, 0.2)', - icon: 'rgb(248, 113, 113)', - text: 'text-white' +export const InfoBox = ({ + title, + description, + icon, + intent = 'primary', + variant +}: InfoBoxProps) => { + const activeIntent = (variant || intent) as any; + + const configMap: any = { + primary: { + bg: 'rgba(25, 140, 255, 0.05)', + border: 'rgba(25, 140, 255, 0.2)', }, success: { - bg: 'rgba(16, 185, 129, 0.1)', - border: 'rgba(16, 185, 129, 0.2)', - icon: 'rgb(52, 211, 153)', - text: 'text-white' - } + bg: 'rgba(111, 227, 122, 0.05)', + border: 'rgba(111, 227, 122, 0.2)', + }, + warning: { + bg: 'rgba(255, 190, 77, 0.05)', + border: 'rgba(255, 190, 77, 0.2)', + }, + critical: { + bg: 'rgba(227, 92, 92, 0.05)', + border: 'rgba(227, 92, 92, 0.2)', + }, + telemetry: { + bg: 'rgba(78, 212, 224, 0.05)', + border: 'rgba(78, 212, 224, 0.2)', + }, }; - const colors = configs[variant]; + const config = configMap[activeIntent] || configMap.primary; return ( - - - + + + - {title} - {description} + + {title} + + + {description} + - + ); -} +}; diff --git a/apps/website/ui/InfoItem.tsx b/apps/website/ui/InfoItem.tsx index 7fbe2052e..a7c3a565d 100644 --- a/apps/website/ui/InfoItem.tsx +++ b/apps/website/ui/InfoItem.tsx @@ -4,33 +4,32 @@ import { Text } from './Text'; import { Icon } from './Icon'; import { LucideIcon } from 'lucide-react'; -interface InfoItemProps { - icon: LucideIcon; +export interface InfoItemProps { label: string; value: string | number; + icon: LucideIcon; + intent?: 'primary' | 'success' | 'warning' | 'critical' | 'telemetry' | 'high' | 'med' | 'low'; } -export function InfoItem({ icon, label, value }: InfoItemProps) { +export const InfoItem = ({ + label, + value, + icon, + intent = 'med' +}: InfoItemProps) => { return ( - - - + + + - - {label} - + + + {label} + + {value} ); -} +}; diff --git a/apps/website/ui/Input.tsx b/apps/website/ui/Input.tsx index 30c088ca9..7b569b2ed 100644 --- a/apps/website/ui/Input.tsx +++ b/apps/website/ui/Input.tsx @@ -1,59 +1,70 @@ -import React, { forwardRef, ReactNode } from 'react'; +import React, { forwardRef, InputHTMLAttributes } from 'react'; import { Box } from './primitives/Box'; -import { Stack } from './primitives/Stack'; import { Text } from './Text'; -interface InputProps extends React.InputHTMLAttributes { +export interface InputProps extends Omit, 'size'> { label?: string; - icon?: ReactNode; - errorMessage?: string; - variant?: 'default' | 'error'; + error?: string; + hint?: string; + fullWidth?: boolean; + size?: 'sm' | 'md' | 'lg'; } -export const Input = forwardRef( - ({ label, icon, errorMessage, variant = 'default', className = '', ...props }, ref) => { - const isError = variant === 'error' || !!errorMessage; - - const baseClasses = 'w-full px-4 py-2 bg-deep-graphite border rounded-lg text-white placeholder:text-gray-500 focus:outline-none transition-all duration-150 sm:text-sm'; - const variantClasses = isError - ? 'border-warning-amber focus:border-warning-amber focus:ring-1 focus:ring-warning-amber' - : 'border-charcoal-outline focus:border-primary-blue focus:ring-1 focus:ring-primary-blue'; - - const classes = `${baseClasses} ${variantClasses} ${icon ? 'pl-11' : ''} ${className}`; +export const Input = forwardRef(({ + label, + error, + hint, + fullWidth = false, + size = 'md', + ...props +}, ref) => { + const sizeClasses = { + sm: 'px-3 py-1.5 text-xs', + md: 'px-4 py-2 text-sm', + lg: 'px-4 py-3 text-base' + }; - return ( - - {label && ( - + const baseClasses = 'bg-[var(--ui-color-bg-surface)] border border-[var(--ui-color-border-default)] text-[var(--ui-color-text-high)] placeholder-[var(--ui-color-text-low)] focus:outline-none focus:border-[var(--ui-color-intent-primary)] transition-colors'; + const errorClasses = error ? 'border-[var(--ui-color-intent-critical)]' : ''; + const widthClasses = fullWidth ? 'w-full' : ''; + + const classes = [ + baseClasses, + sizeClasses[size], + errorClasses, + widthClasses, + ].filter(Boolean).join(' '); + + return ( + + {label && ( + + {label} - )} - - {icon && ( - - {icon} - - )} - - {errorMessage && ( - - {errorMessage} - - )} - - ); - } -); + )} + + {error && ( + + + {error} + + + )} + {hint && !error && ( + + + {hint} + + + )} + + ); +}); Input.displayName = 'Input'; diff --git a/apps/website/ui/LandingItems.tsx b/apps/website/ui/LandingItems.tsx index 9d39eeb75..300632874 100644 --- a/apps/website/ui/LandingItems.tsx +++ b/apps/website/ui/LandingItems.tsx @@ -1,45 +1,38 @@ -import { Box } from '@/ui/primitives/Box'; -import { Stack } from '@/ui/primitives/Stack'; -import { Surface } from '@/ui/primitives/Surface'; -import { Text } from '@/ui/Text'; +import React from 'react'; +import { Box } from './primitives/Box'; +import { Text } from './Text'; +import { Surface } from './primitives/Surface'; +import { Icon } from './Icon'; +import { LucideIcon } from 'lucide-react'; -export function FeatureItem({ text }: { text: string }) { - return ( - - - - - {text} - - - - ); +export interface LandingItemProps { + title: string; + description: string; + icon: LucideIcon; + intent?: 'primary' | 'success' | 'warning' | 'critical' | 'telemetry'; } -export function ResultItem({ text, color }: { text: string, color: string }) { +export const LandingItem = ({ + title, + description, + icon, + intent = 'primary' +}: LandingItemProps) => { return ( - - - - - {text} - - - - ); -} - -export function StepItem({ step, text }: { step: number, text: string }) { - return ( - - - - {step.toString().padStart(2, '0')} + + + + - - {text} - - + + + {title} + + + {description} + + + ); -} +}; diff --git a/apps/website/ui/Layout.tsx b/apps/website/ui/Layout.tsx index e81671e70..2a593f341 100644 --- a/apps/website/ui/Layout.tsx +++ b/apps/website/ui/Layout.tsx @@ -1,67 +1,44 @@ -import { ReactNode } from 'react'; +import React, { ReactNode } from 'react'; import { Box } from './primitives/Box'; -import { Grid } from './primitives/Grid'; -import { Stack } from './primitives/Stack'; -/** - * WARNING: DO NOT VIOLATE THE PURPOSE OF THIS COMPONENT. - * - * Layout is a high-level component for page or section layouts. - * It should use Grid or Stack primitives internally. - * - * If you need a specific layout pattern, create a new component. - */ - -interface LayoutProps { +export interface LayoutProps { children: ReactNode; - padding?: string; - gap?: string; - grid?: boolean; - gridCols?: 1 | 2 | 3 | 4; - flex?: boolean; - flexCol?: boolean; - items?: 'start' | 'center' | 'end' | 'stretch'; - justify?: 'start' | 'center' | 'end' | 'between' | 'around'; + header?: ReactNode; + footer?: ReactNode; + sidebar?: ReactNode; } -export function Layout({ +export const Layout = ({ children, - padding = 'p-6', - gap = 'gap-4', - grid = false, - gridCols = 1, - flex = false, - flexCol = false, - items = 'start', - justify = 'start' -}: LayoutProps) { - if (grid) { - return ( - - {children} - - ); - } - - if (flex) { - return ( - - {children} - - ); - } - + header, + footer, + sidebar +}: LayoutProps) => { return ( - - {children} + + {header && ( + + {header} + + )} + + + {sidebar && ( + + {sidebar} + + )} + + + {children} + + + + {footer && ( + + {footer} + + )} ); -} +}; diff --git a/apps/website/ui/LeaderboardList.tsx b/apps/website/ui/LeaderboardList.tsx index c8522259e..778aba920 100644 --- a/apps/website/ui/LeaderboardList.tsx +++ b/apps/website/ui/LeaderboardList.tsx @@ -1,16 +1,31 @@ import React, { ReactNode } from 'react'; import { Box } from './primitives/Box'; +import { Surface } from './primitives/Surface'; -interface LeaderboardListProps { +export interface LeaderboardListProps { children: ReactNode; } -export function LeaderboardList({ children }: LeaderboardListProps) { +export const LeaderboardList = ({ children }: LeaderboardListProps) => { return ( - -
+ + {children} -
+
+ + ); +}; + +export const LeaderboardListItem = ({ children, onClick }: { children: ReactNode, onClick?: () => void }) => { + return ( + + {children} ); -} +}; diff --git a/apps/website/ui/LeaderboardPreviewShell.tsx b/apps/website/ui/LeaderboardPreviewShell.tsx index d624801c6..1b8836ca7 100644 --- a/apps/website/ui/LeaderboardPreviewShell.tsx +++ b/apps/website/ui/LeaderboardPreviewShell.tsx @@ -1,70 +1,58 @@ -import { Award, ChevronRight, LucideIcon } from 'lucide-react'; -import { ReactNode } from 'react'; +import React, { ReactNode } from 'react'; import { Box } from './primitives/Box'; -import { Button } from './Button'; -import { Heading } from './Heading'; -import { Icon } from './Icon'; -import { Stack } from './primitives/Stack'; import { Text } from './Text'; +import { Surface } from './primitives/Surface'; +import { Icon } from './Icon'; +import { LucideIcon } from 'lucide-react'; -interface LeaderboardPreviewShellProps { +export interface LeaderboardPreviewShellProps { title: string; - subtitle: string; - onViewFull: () => void; + subtitle?: string; + icon: LucideIcon; + intent?: 'primary' | 'success' | 'warning' | 'critical' | 'telemetry'; children: ReactNode; - icon?: LucideIcon; - iconColor?: string; - iconBgGradient?: string; - viewFullLabel?: string; + footer?: ReactNode; } -export function LeaderboardPreviewShell({ - title, - subtitle, - onViewFull, +export const LeaderboardPreviewShell = ({ + title, + subtitle, + icon, + intent = 'primary', children, - icon = Award, - iconColor = "#facc15", - iconBgGradient = 'linear-gradient(to bottom right, rgba(250, 204, 21, 0.2), rgba(217, 119, 6, 0.1))', - viewFullLabel = "View Full Leaderboard", -}: LeaderboardPreviewShellProps) { + footer +}: LeaderboardPreviewShellProps) => { return ( - - {/* Header */} - - - - + + + + + + + + + + {title} + + {subtitle && ( + + {subtitle} + + )} + - - {title} - {subtitle} - - - - - - - {/* Compact Leaderboard */} - - - {children} - + -
+ + + {children} + + + {footer && ( + + {footer} + + )} + ); -} +}; diff --git a/apps/website/ui/LeaderboardTableShell.tsx b/apps/website/ui/LeaderboardTableShell.tsx index c88202ce2..069be7d82 100644 --- a/apps/website/ui/LeaderboardTableShell.tsx +++ b/apps/website/ui/LeaderboardTableShell.tsx @@ -1,49 +1,17 @@ -import React from 'react'; +import React, { ReactNode } from 'react'; import { Box } from './primitives/Box'; +import { Surface } from './primitives/Surface'; -interface LeaderboardTableShellProps { - columns: { - key: string; - label: string; - align?: 'left' | 'center' | 'right'; - width?: string; - }[]; - children: React.ReactNode; - className?: string; +export interface LeaderboardTableShellProps { + children: ReactNode; } -export function LeaderboardTableShell({ columns, children, className = '' }: LeaderboardTableShellProps) { +export const LeaderboardTableShell = ({ children }: LeaderboardTableShellProps) => { return ( - -
- - - - {columns.map((col) => ( - - ))} - - - - {children} - -
- {col.label} -
-
-
+ + + {children} + + ); -} +}; diff --git a/apps/website/ui/Link.tsx b/apps/website/ui/Link.tsx index b3c5814e9..0b06ed11d 100644 --- a/apps/website/ui/Link.tsx +++ b/apps/website/ui/Link.tsx @@ -1,86 +1,66 @@ -import React, { ReactNode } from 'react'; -import { Box, BoxProps } from './primitives/Box'; +import React, { ReactNode, forwardRef, AnchorHTMLAttributes } from 'react'; -export interface LinkProps extends Omit, 'children' | 'onClick'> { - href: string; +export interface LinkProps extends AnchorHTMLAttributes { children: ReactNode; - variant?: 'primary' | 'secondary' | 'ghost'; - size?: 'xs' | 'sm' | 'md' | 'lg'; - target?: '_blank' | '_self' | '_parent' | '_top'; - rel?: string; - onClick?: React.MouseEventHandler; + variant?: 'primary' | 'secondary' | 'ghost' | 'inherit'; + underline?: 'always' | 'hover' | 'none'; + size?: string; + weight?: string; + letterSpacing?: string; block?: boolean; - weight?: 'light' | 'normal' | 'medium' | 'semibold' | 'bold' | string; - truncate?: boolean; hoverColor?: string; transition?: boolean; } -export function Link({ - href, +export const Link = forwardRef(({ children, - className = '', variant = 'primary', - size = 'md', - target = '_self', - rel = '', - onClick, - block = false, + underline = 'hover', + size, weight, - truncate, + letterSpacing, + block = false, hoverColor, - transition, + transition = true, ...props -}: LinkProps) { - const baseClasses = 'inline-flex items-center transition-colors'; - +}, ref) => { const variantClasses = { - primary: 'text-primary-accent hover:text-primary-accent/80', - secondary: 'text-telemetry-aqua hover:text-telemetry-aqua/80', - ghost: 'text-gray-400 hover:text-gray-300' + primary: 'text-[var(--ui-color-intent-primary)] hover:opacity-80', + secondary: 'text-[var(--ui-color-text-med)] hover:text-[var(--ui-color-text-high)]', + ghost: 'text-[var(--ui-color-text-low)] hover:text-[var(--ui-color-text-high)]', + inherit: 'text-inherit', }; - const sizeClasses = { - xs: 'text-xs', - sm: 'text-sm', - md: 'text-base', - lg: 'text-lg' + const underlineClasses = { + always: 'underline', + hover: 'hover:underline', + none: 'no-underline', }; - const weightClasses: Record = { - light: 'font-light', - normal: 'font-normal', - medium: 'font-medium', - semibold: 'font-semibold', - bold: 'font-bold' - }; - const classes = [ - block ? 'flex' : baseClasses, + transition ? 'transition-all duration-150 ease-in-out' : '', + 'cursor-pointer', + block ? 'block' : 'inline', variantClasses[variant], - sizeClasses[size], - weight && weightClasses[weight] ? weightClasses[weight] : '', - truncate ? 'truncate' : '', - hoverColor ? `hover:${hoverColor}` : '', - transition ? 'transition-all duration-150' : '', - className - ].filter(Boolean).join(' '); - + underlineClasses[underline], + ].join(' '); + + const style: React.CSSProperties = { + ...(size ? { fontSize: size } : {}), + ...(weight ? { fontWeight: weight } : {}), + ...(letterSpacing ? { letterSpacing } : {}), + }; + return ( - {children} - + ); -} +}); + +Link.displayName = 'Link'; diff --git a/apps/website/ui/LoadingSpinner.tsx b/apps/website/ui/LoadingSpinner.tsx index f90541c96..fd62d4349 100644 --- a/apps/website/ui/LoadingSpinner.tsx +++ b/apps/website/ui/LoadingSpinner.tsx @@ -1,26 +1,41 @@ import React from 'react'; import { Box } from './primitives/Box'; -interface LoadingSpinnerProps { - size?: number; - color?: string; - className?: string; +export interface LoadingSpinnerProps { + size?: 'sm' | 'md' | 'lg' | number; + intent?: 'primary' | 'high' | 'low'; } -export function LoadingSpinner({ size = 8, color = '#3b82f6', className = '' }: LoadingSpinnerProps) { +export const LoadingSpinner = ({ + size = 'md', + intent = 'primary' +}: LoadingSpinnerProps) => { + const sizeMap = { + sm: '1rem', + md: '2rem', + lg: '3rem', + }; + + const dimension = typeof size === 'string' ? sizeMap[size] : `${size * 0.25}rem`; + + const intentColorMap = { + primary: 'var(--ui-color-intent-primary)', + high: 'var(--ui-color-text-high)', + low: 'var(--ui-color-text-low)', + }; + return ( ); -} +}; diff --git a/apps/website/ui/MainContent.tsx b/apps/website/ui/MainContent.tsx index 2e9961784..60a152fb7 100644 --- a/apps/website/ui/MainContent.tsx +++ b/apps/website/ui/MainContent.tsx @@ -1,9 +1,14 @@ -import React from 'react'; +import React, { ReactNode } from 'react'; +import { Box } from './primitives/Box'; -interface MainContentProps { - children: React.ReactNode; +export interface MainContentProps { + children: ReactNode; } -export function MainContent({ children }: MainContentProps) { - return
{children}
; -} \ No newline at end of file +export const MainContent = ({ children }: MainContentProps) => { + return ( + + {children} + + ); +}; diff --git a/apps/website/ui/MetricCard.tsx b/apps/website/ui/MetricCard.tsx index 2a0951b5d..253cd4302 100644 --- a/apps/website/ui/MetricCard.tsx +++ b/apps/website/ui/MetricCard.tsx @@ -1,64 +1,60 @@ import React from 'react'; -import { LucideIcon } from 'lucide-react'; +import { Card } from './Card'; import { Box } from './primitives/Box'; import { Text } from './Text'; import { Icon } from './Icon'; +import { LucideIcon } from 'lucide-react'; -interface MetricCardProps { +export interface MetricCardProps { label: string; value: string | number; icon?: LucideIcon; - color?: string; + intent?: 'primary' | 'success' | 'warning' | 'critical' | 'telemetry'; trend?: { value: number; isPositive: boolean; }; - border?: boolean; - bg?: string; } -/** - * A semantic component for displaying metrics. - * Instrument-grade typography and dense-but-readable hierarchy. - */ -export function MetricCard({ - label, - value, - icon, - color = 'text-primary-accent', - trend, - border = true, - bg = 'panel-gray/40', -}: MetricCardProps) { +export const MetricCard = ({ + label, + value, + icon, + intent = 'primary', + trend +}: MetricCardProps) => { return ( - - - - {icon && } - + + + + {label} - - {trend && ( - - {trend.isPositive ? '▲' : '▼'} {trend.value}% + + {value} + + {icon && ( + + + )} - - {typeof value === 'number' ? value.toLocaleString() : value} - - + + {trend && ( + + + {trend.isPositive ? '+' : '-'}{Math.abs(trend.value)}% + + + vs last period + + + )} + ); -} +}; diff --git a/apps/website/ui/MiniStat.tsx b/apps/website/ui/MiniStat.tsx index 336c8f582..6483a04e8 100644 --- a/apps/website/ui/MiniStat.tsx +++ b/apps/website/ui/MiniStat.tsx @@ -2,17 +2,21 @@ import React from 'react'; import { Box } from './primitives/Box'; import { Text } from './Text'; -interface MiniStatProps { +export interface MiniStatProps { label: string; value: string | number; - color?: string; + intent?: 'primary' | 'success' | 'warning' | 'critical' | 'high' | 'med' | 'low'; } -export function MiniStat({ label, value, color = 'text-white' }: MiniStatProps) { +export const MiniStat = ({ + label, + value, + intent = 'high' +}: MiniStatProps) => { return ( - - {value} - {label} + + {value} + {label} ); -} +}; diff --git a/apps/website/ui/Modal.tsx b/apps/website/ui/Modal.tsx index c2fae7030..5118deaaa 100644 --- a/apps/website/ui/Modal.tsx +++ b/apps/website/ui/Modal.tsx @@ -1,51 +1,63 @@ -import React, { ReactNode } from 'react'; +import React, { ReactNode, useEffect } from 'react'; +import { createPortal } from 'react-dom'; import { Box } from './primitives/Box'; -import { Stack } from './primitives/Stack'; -import { Button } from './Button'; -import { Text } from './Text'; -import { X } from 'lucide-react'; +import { Surface } from './primitives/Surface'; import { IconButton } from './IconButton'; +import { Button } from './Button'; +import { X } from 'lucide-react'; +import { Heading } from './Heading'; +import { Text } from './Text'; -interface ModalProps { +export interface ModalProps { + children: ReactNode; isOpen: boolean; onClose?: () => void; - onOpenChange?: (open: boolean) => void; + onOpenChange?: (isOpen: boolean) => void; title?: string; - description?: string; - icon?: React.ReactNode; - children: ReactNode; - footer?: ReactNode; + size?: 'sm' | 'md' | 'lg' | 'xl' | 'full'; primaryActionLabel?: string; onPrimaryAction?: () => void; secondaryActionLabel?: string; onSecondaryAction?: () => void; - isLoading?: boolean; - size?: 'sm' | 'md' | 'lg' | 'xl'; + footer?: ReactNode; + description?: string; + icon?: ReactNode; } -export function Modal({ - isOpen, - onClose, +export const Modal = ({ + children, + isOpen, + onClose, onOpenChange, title, - description, - icon, - children, - footer, + size = 'md', primaryActionLabel, onPrimaryAction, secondaryActionLabel, onSecondaryAction, - isLoading = false, - size = 'md', -}: ModalProps) { + footer, + description, + icon +}: ModalProps) => { + useEffect(() => { + if (isOpen) { + document.body.style.overflow = 'hidden'; + } else { + document.body.style.overflow = 'unset'; + } + return () => { + document.body.style.overflow = 'unset'; + }; + }, [isOpen]); + if (!isOpen) return null; const sizeMap = { - sm: 'max-w-md', - md: 'max-w-lg', - lg: 'max-w-2xl', - xl: 'max-w-4xl', + sm: '24rem', + md: '32rem', + lg: '48rem', + xl: '64rem', + full: '100%', }; const handleClose = () => { @@ -53,96 +65,82 @@ export function Modal({ if (onOpenChange) onOpenChange(false); }; - return ( - - {/* Backdrop click to close */} - - - + + - {/* Header */} - - - - {icon && {icon}} - - {title && ( - - {title} - - )} - {description && ( - - {description} - - )} - - - - + + + {icon} + + {title && {title}} + {description && {description}} + + + - - {/* Content */} - + + {children} - {/* Footer */} - {(primaryActionLabel || secondaryActionLabel || footer) && ( - - {footer || ( - - {secondaryActionLabel && ( - - )} - {primaryActionLabel && ( - - )} - + {(footer || primaryActionLabel || secondaryActionLabel) && ( + + {footer} + {secondaryActionLabel && ( + + )} + {primaryActionLabel && ( + )} )} - - + + , + document.body ); -} +}; diff --git a/apps/website/ui/PageHero.tsx b/apps/website/ui/PageHero.tsx index 32787144b..ae641de0c 100644 --- a/apps/website/ui/PageHero.tsx +++ b/apps/website/ui/PageHero.tsx @@ -1,129 +1,56 @@ -import React from 'react'; -import { LucideIcon } from 'lucide-react'; -import { Heading } from './Heading'; -import { Button } from './Button'; +import React, { ReactNode } from 'react'; import { Box } from './primitives/Box'; -import { Stack } from './primitives/Stack'; +import { Heading } from './Heading'; import { Text } from './Text'; -import { Icon } from './Icon'; -import { ModalIcon } from './ModalIcon'; +import { Surface } from './primitives/Surface'; -interface PageHeroProps { +export interface PageHeroProps { title: string; description?: string; - icon?: LucideIcon; - backgroundPattern?: React.ReactNode; - stats?: Array<{ - icon?: LucideIcon; - value: string | number; - label: string; - color?: string; - animate?: boolean; - }>; - actions?: Array<{ - label: string; - onClick: () => void; - variant?: 'primary' | 'secondary'; - icon?: LucideIcon; - description?: string; - }>; - children?: React.ReactNode; - className?: string; + children?: ReactNode; + image?: ReactNode; } -export const PageHero = ({ - title, - description, - icon, - backgroundPattern, - stats, - actions, +export const PageHero = ({ + title, + description, children, - className = '' -}: PageHeroProps) => ( - - {/* Background Pattern */} - {backgroundPattern || ( - <> - - - - )} - - - - {/* Main Content */} - - {icon && ( - - - - {title} - - - )} - {!icon && ( - - {title} - - )} + image +}: PageHeroProps) => { + return ( + + + + {title} {description && ( - + {description} )} - - {/* Stats */} - {stats && stats.length > 0 && ( - - {stats.map((stat, index) => ( - - {stat.icon ? ( - - ) : ( - - )} - - {stat.value} {stat.label} - - - ))} - - )} + {children} - - {/* Actions or Custom Content */} - {actions && actions.length > 0 && ( - - {actions.map((action, index) => ( - - - {action.description && ( - {action.description} - )} - - ))} - + {image && ( + + {image} + )} - - {children} - - -); + + {/* Decorative elements */} + + + ); +}; diff --git a/apps/website/ui/Pagination.tsx b/apps/website/ui/Pagination.tsx index 93dc51ea1..a6356b3c1 100644 --- a/apps/website/ui/Pagination.tsx +++ b/apps/website/ui/Pagination.tsx @@ -1,88 +1,77 @@ - - -import { ChevronLeft, ChevronRight } from 'lucide-react'; +import React from 'react'; import { Box } from './primitives/Box'; import { Button } from './Button'; -import { Icon } from './Icon'; -import { Stack } from './primitives/Stack'; import { Text } from './Text'; +import { ChevronLeft, ChevronRight } from 'lucide-react'; -interface PaginationProps { +export interface PaginationProps { currentPage: number; totalPages: number; - totalItems: number; - itemsPerPage: number; onPageChange: (page: number) => void; } -export function Pagination({ - currentPage, - totalPages, - totalItems, - itemsPerPage, - onPageChange, -}: PaginationProps) { - if (totalPages <= 1) return null; - - const startItem = ((currentPage - 1) * itemsPerPage) + 1; - const endItem = Math.min(currentPage * itemsPerPage, totalItems); - - const getPageNumbers = () => { - if (totalPages <= 5) { - return Array.from({ length: totalPages }, (_, i) => i + 1); - } - - if (currentPage <= 3) { - return [1, 2, 3, 4, 5]; - } - - if (currentPage >= totalPages - 2) { - return [totalPages - 4, totalPages - 3, totalPages - 2, totalPages - 1, totalPages]; - } - - return [currentPage - 2, currentPage - 1, currentPage, currentPage + 1, currentPage + 2]; - }; +export const Pagination = ({ + currentPage, + totalPages, + onPageChange +}: PaginationProps) => { + const pages = Array.from({ length: totalPages }, (_, i) => i + 1); + + const visiblePages = pages.filter(page => { + if (totalPages <= 7) return true; + if (page === 1 || page === totalPages) return true; + if (page >= currentPage - 1 && page <= currentPage + 1) return true; + return false; + }); return ( - - - Showing {startItem}–{endItem} of {totalItems} + + + Page {currentPage} of {totalPages} - + - - {getPageNumbers().map(pageNum => ( - - ))} - - + + {visiblePages.map((page, index) => { + const prevPage = visiblePages[index - 1]; + const showEllipsis = prevPage && page - prevPage > 1; + + return ( + + {showEllipsis && ...} + + + ); + })} + + - + ); -} +}; diff --git a/apps/website/ui/Panel.tsx b/apps/website/ui/Panel.tsx index a5a11ce07..9b2af5cdc 100644 --- a/apps/website/ui/Panel.tsx +++ b/apps/website/ui/Panel.tsx @@ -1,57 +1,42 @@ import React, { ReactNode } from 'react'; import { Surface } from './primitives/Surface'; -import { Box, BoxProps } from './primitives/Box'; +import { Box } from './primitives/Box'; +import { Heading } from './Heading'; import { Text } from './Text'; -interface PanelProps extends Omit, 'variant' | 'padding'> { - children: ReactNode; +export interface PanelProps { title?: string; description?: string; - variant?: 'default' | 'muted' | 'dark' | 'glass'; - padding?: 0 | 1 | 2 | 3 | 4 | 6 | 8 | 10 | 12; - border?: boolean; - className?: string; + children: ReactNode; + footer?: ReactNode; + variant?: 'default' | 'dark' | 'muted'; } -/** - * A semantic wrapper for content panels. - * Follows the "Precision Racing Minimal" theme. - */ -export function Panel({ - children, - title, - description, - variant = 'default', - padding = 6, - border = true, - ...props -}: PanelProps) { +export const Panel = ({ + title, + description, + children, + footer, + variant = 'default' +}: PanelProps) => { return ( - + {(title || description) && ( - - {title && ( - - {title} - - )} - {description && ( - - {description} - - )} + + {title && {title}} + {description && {description}} + + )} + + + {children} + + + {footer && ( + + {footer} )} - {children} ); -} +}; diff --git a/apps/website/ui/PasswordField.tsx b/apps/website/ui/PasswordField.tsx index 92b9a87ad..77b822411 100644 --- a/apps/website/ui/PasswordField.tsx +++ b/apps/website/ui/PasswordField.tsx @@ -1,42 +1,35 @@ -import React, { ComponentProps } from 'react'; -import { Eye, EyeOff, Lock } from 'lucide-react'; -import { Input } from './Input'; +import React, { useState } from 'react'; +import { Input, InputProps } from './Input'; +import { Eye, EyeOff } from 'lucide-react'; +import { IconButton } from './IconButton'; import { Box } from './primitives/Box'; -interface PasswordFieldProps extends ComponentProps { - showPassword?: boolean; - onTogglePassword?: () => void; -} +export interface PasswordFieldProps extends InputProps {} + +export const PasswordField = (props: PasswordFieldProps) => { + const [showPassword, setShowPassword] = useState(false); -/** - * PasswordField - * - * A specialized input for passwords with visibility toggling. - * Stateless UI component. - */ -export function PasswordField({ showPassword, onTogglePassword, ...props }: PasswordFieldProps) { return ( - + } /> - {onTogglePassword && ( - - {showPassword ? : } - - )} + + setShowPassword(!showPassword)} + variant="ghost" + size="sm" + title={showPassword ? 'Hide password' : 'Show password'} + /> + ); -} +}; diff --git a/apps/website/ui/PlaceholderImage.tsx b/apps/website/ui/PlaceholderImage.tsx index 8661e07b6..9a5bd4ddd 100644 --- a/apps/website/ui/PlaceholderImage.tsx +++ b/apps/website/ui/PlaceholderImage.tsx @@ -1,21 +1,34 @@ - - -import { User } from 'lucide-react'; +import React from 'react'; import { Box } from './primitives/Box'; import { Icon } from './Icon'; +import { User } from 'lucide-react'; export interface PlaceholderImageProps { - size?: number; + width?: string | number; + height?: string | number; + size?: string | number; className?: string; } -export function PlaceholderImage({ size = 48, className = '' }: PlaceholderImageProps) { +export const PlaceholderImage = ({ + width, + height, + size, + className +}: PlaceholderImageProps) => { + const dimension = size || '100%'; return ( - - + ); -} +}; diff --git a/apps/website/ui/Podium.tsx b/apps/website/ui/Podium.tsx index 43db57786..5221286ef 100644 --- a/apps/website/ui/Podium.tsx +++ b/apps/website/ui/Podium.tsx @@ -1,70 +1,80 @@ - - -import { Trophy } from 'lucide-react'; -import { ReactNode } from 'react'; +import React, { ReactNode } from 'react'; import { Box } from './primitives/Box'; -import { Heading } from './Heading'; -import { Icon } from './Icon'; -import { Stack } from './primitives/Stack'; import { Text } from './Text'; +import { Avatar } from './Avatar'; +import { Trophy } from 'lucide-react'; +import { Icon } from './Icon'; +import { Heading } from './Heading'; -interface PodiumProps { - title: string; - children: ReactNode; +export interface PodiumEntry { + name: string; + avatar?: string; + value: string | number; + position: 1 | 2 | 3; } -export function Podium({ title, children }: PodiumProps) { - return ( - - - - - {title} - - +export interface PodiumProps { + entries?: PodiumEntry[]; + title?: string; + children?: ReactNode; +} - - {children} - +export const Podium = ({ entries = [], title, children }: PodiumProps) => { + const sortedEntries = [...entries].sort((a, b) => { + const order = { 2: 0, 1: 1, 3: 2 }; + return order[a.position] - order[b.position]; + }); + + const getPositionColor = (pos: number) => { + if (pos === 1) return 'var(--ui-color-intent-warning)'; + if (pos === 2) return '#A1A1AA'; + if (pos === 3) return '#CD7F32'; + return 'var(--ui-color-text-low)'; + }; + + return ( + + {title && {title}} + + + {sortedEntries.map((entry) => { + const height = entry.position === 1 ? '12rem' : entry.position === 2 ? '10rem' : '8rem'; + const color = getPositionColor(entry.position); + + return ( + + + {entry.position === 1 && } + + {entry.name} + {entry.value} + + + + + {entry.position} + + + + ); + })} + + {children} ); -} +}; -interface PodiumItemProps { - position: number; - height: string; - cardContent: ReactNode; - bgColor: string; - positionColor: string; -} - -export function PodiumItem({ - position, - height, - cardContent, - bgColor, - positionColor, -}: PodiumItemProps) { - return ( - - {cardContent} - - {/* Podium stand */} - - - - {position} - - - - - ); -} +export const PodiumItem = ({ children }: { children: ReactNode }) => <>{children}; diff --git a/apps/website/ui/PresetCard.tsx b/apps/website/ui/PresetCard.tsx index 8c77300c4..92d9841ef 100644 --- a/apps/website/ui/PresetCard.tsx +++ b/apps/website/ui/PresetCard.tsx @@ -1,122 +1,56 @@ - - -import type { MouseEventHandler, ReactNode } from 'react'; -import { Card } from './Card'; - -interface PresetCardStat { - label: string; - value: string; -} +import React from 'react'; +import { Box } from './primitives/Box'; +import { Text } from './Text'; +import { Surface } from './primitives/Surface'; +import { Icon } from './Icon'; +import { LucideIcon } from 'lucide-react'; export interface PresetCardProps { title: string; - subtitle?: string; - primaryTag?: string; - description?: string; - stats?: PresetCardStat[]; - selected?: boolean; - disabled?: boolean; - onSelect?: () => void; - className?: string; - children?: ReactNode; + description: string; + icon: LucideIcon; + onClick: () => void; + isSelected?: boolean; } -export function PresetCard({ - title, - subtitle, - primaryTag, - description, - stats, - selected, - disabled, - onSelect, - className = '', - children, -}: PresetCardProps) { - const isInteractive = typeof onSelect === 'function' && !disabled; - - const handleClick: MouseEventHandler = (event) => { - if (!isInteractive) { - return; - } - event.preventDefault(); - onSelect?.(); - }; - - const baseBorder = selected ? 'border-primary-blue' : 'border-charcoal-outline'; - const baseBg = selected ? 'bg-primary-blue/10' : 'bg-iron-gray'; - const baseRing = selected ? 'ring-2 ring-primary-blue/40' : ''; - const disabledClasses = disabled ? 'opacity-60 cursor-not-allowed' : ''; - const hoverClasses = isInteractive && !disabled ? 'hover:bg-iron-gray/80 hover:scale-[1.01]' : ''; - - const content = ( -
-
-
-
{title}
- {subtitle && ( -
{subtitle}
- )} -
-
- {primaryTag && ( - - {primaryTag} - - )} - {selected && ( - - - Selected - - )} -
-
- - {description && ( -

{description}

- )} - - {children} - - {stats && stats.length > 0 && ( -
-
- {stats.map((stat) => ( -
-
{stat.label}
-
{stat.value}
-
- ))} -
-
- )} -
- ); - - const commonClasses = `${baseBorder} ${baseBg} ${baseRing} ${hoverClasses} ${disabledClasses} ${className}`; - - if (isInteractive) { - return ( - - ); - } - +export const PresetCard = ({ + title, + description, + icon, + onClick, + isSelected = false +}: PresetCardProps) => { return ( - } + - {content} - + + + + + + + {title} + + + {description} + + + +
); -} \ No newline at end of file +}; diff --git a/apps/website/ui/ProgressBar.tsx b/apps/website/ui/ProgressBar.tsx index 8b2dd1d66..f201a207e 100644 --- a/apps/website/ui/ProgressBar.tsx +++ b/apps/website/ui/ProgressBar.tsx @@ -1,33 +1,55 @@ import React from 'react'; -import { Box, BoxProps } from './primitives/Box'; +import { Box } from './primitives/Box'; -interface ProgressBarProps extends Omit, 'children'> { +export interface ProgressBarProps { value: number; - max: number; - color?: string; - bg?: string; - height?: string; + max?: number; + intent?: 'primary' | 'success' | 'warning' | 'critical' | 'telemetry'; + color?: string; // Alias for intent + size?: 'sm' | 'md' | 'lg'; + marginBottom?: any; + mb?: any; // Alias for marginBottom } -export function ProgressBar({ - value, - max, - color = 'bg-primary-blue', - bg = 'bg-deep-graphite', - height = '2', - ...props -}: ProgressBarProps) { - const percentage = Math.min((value / max) * 100, 100); +export const ProgressBar = ({ + value, + max = 100, + intent = 'primary', + color: colorProp, + size = 'md', + marginBottom, + mb +}: ProgressBarProps) => { + const percentage = Math.min(Math.max((value / max) * 100, 0), 100); + + const intentColorMap = { + primary: 'var(--ui-color-intent-primary)', + success: 'var(--ui-color-intent-success)', + warning: 'var(--ui-color-intent-warning)', + critical: 'var(--ui-color-intent-critical)', + telemetry: 'var(--ui-color-intent-telemetry)', + }; + + const color = colorProp || intentColorMap[intent]; + + const sizeMap = { + sm: '0.25rem', + md: '0.5rem', + lg: '1rem', + }; return ( - + ); -} +}; diff --git a/apps/website/ui/QuickActionItem.tsx b/apps/website/ui/QuickActionItem.tsx index 66d4a391d..b581f6eea 100644 --- a/apps/website/ui/QuickActionItem.tsx +++ b/apps/website/ui/QuickActionItem.tsx @@ -1,51 +1,59 @@ - - -import { ChevronRight, LucideIcon } from 'lucide-react'; +import React from 'react'; import { Box } from './primitives/Box'; -import { Icon } from './Icon'; -import { Link } from './Link'; import { Text } from './Text'; +import { Icon } from './Icon'; +import { LucideIcon, ChevronRight } from 'lucide-react'; +import { Link } from './Link'; -interface QuickActionItemProps { - href: string; +export interface QuickActionItemProps { label: string; icon: LucideIcon; - iconVariant?: 'blue' | 'amber' | 'purple' | 'green'; + href: string; + variant?: 'primary' | 'secondary' | 'success' | 'warning' | 'critical'; } -export function QuickActionItem({ href, label, icon, iconVariant = 'blue' }: QuickActionItemProps) { - const variantColors = { - blue: 'rgb(59, 130, 246)', - amber: 'rgb(245, 158, 11)', - purple: 'rgb(168, 85, 247)', - green: 'rgb(16, 185, 129)', +export const QuickActionItem = ({ + label, + icon, + href, + variant = 'primary' +}: QuickActionItemProps) => { + const variantBgs = { + primary: 'rgba(25, 140, 255, 0.1)', + secondary: 'var(--ui-color-bg-surface-muted)', + success: 'rgba(111, 227, 122, 0.1)', + warning: 'rgba(255, 190, 77, 0.1)', + critical: 'rgba(227, 92, 92, 0.1)', }; - const variantBgs = { - blue: 'bg-primary-blue/10', - amber: 'bg-warning-amber/10', - purple: 'bg-purple-500/10', - green: 'bg-performance-green/10', + const variantIntents = { + primary: 'primary' as const, + secondary: 'med' as const, + success: 'success' as const, + warning: 'warning' as const, + critical: 'critical' as const, }; return ( - - - - + + + + - {label} - + + {label} + + ); -} +}; diff --git a/apps/website/ui/QuickActionLink.tsx b/apps/website/ui/QuickActionLink.tsx index c6bd338ec..e112af46d 100644 --- a/apps/website/ui/QuickActionLink.tsx +++ b/apps/website/ui/QuickActionLink.tsx @@ -1,33 +1,36 @@ import React from 'react'; +import { Box } from './primitives/Box'; +import { Text } from './Text'; +import { Icon } from './Icon'; +import { LucideIcon, ArrowRight } from 'lucide-react'; +import { Link } from './Link'; -interface QuickActionLinkProps { +export interface QuickActionLinkProps { + label: string; + icon: LucideIcon; href: string; - children: React.ReactNode; - variant?: 'blue' | 'purple' | 'orange'; - className?: string; } -export function QuickActionLink({ - href, - children, - variant = 'blue', - className = '' -}: QuickActionLinkProps) { - const variantClasses = { - blue: 'bg-primary-blue/20 border-primary-blue/30 text-primary-blue hover:bg-primary-blue/30', - purple: 'bg-purple-500/20 border-purple-500/30 text-purple-300 hover:bg-purple-500/30', - orange: 'bg-orange-500/20 border-orange-500/30 text-orange-300 hover:bg-orange-500/30' - }; - - const classes = [ - 'px-4 py-3 border rounded-lg transition-colors text-sm font-medium text-center inline-block w-full', - variantClasses[variant], - className - ].filter(Boolean).join(' '); - +export const QuickActionLink = ({ + label, + icon, + href +}: QuickActionLinkProps) => { return ( - - {children} - + + + + + {label} + + + + ); -} \ No newline at end of file +}; diff --git a/apps/website/ui/QuickActionsPanel.tsx b/apps/website/ui/QuickActionsPanel.tsx index 615c80278..8a0ebff42 100644 --- a/apps/website/ui/QuickActionsPanel.tsx +++ b/apps/website/ui/QuickActionsPanel.tsx @@ -5,42 +5,37 @@ import { Button } from './Button'; import { Icon } from './Icon'; import { LucideIcon } from 'lucide-react'; -interface QuickAction { +export interface QuickAction { label: string; icon: LucideIcon; onClick: () => void; - variant?: 'primary' | 'secondary' | 'ghost'; + intent?: 'primary' | 'secondary' | 'danger'; } -interface QuickActionsPanelProps { +export interface QuickActionsPanelProps { actions: QuickAction[]; - className?: string; } -/** - * QuickActionsPanel - * - * Provides fast access to common dashboard tasks. - */ -export function QuickActionsPanel({ actions, className = '' }: QuickActionsPanelProps) { +export const QuickActionsPanel = ({ + actions +}: QuickActionsPanelProps) => { return ( - - + + {actions.map((action, index) => ( - ))} ); -} +}; diff --git a/apps/website/ui/Section.tsx b/apps/website/ui/Section.tsx index 968c16f27..5a52356b5 100644 --- a/apps/website/ui/Section.tsx +++ b/apps/website/ui/Section.tsx @@ -1,74 +1,42 @@ - - -import { ReactNode } from 'react'; +import React, { ReactNode } from 'react'; import { Box } from './primitives/Box'; -import { Heading } from './Heading'; -import { Text } from './Text'; -interface SectionProps { +export interface SectionProps { children: ReactNode; - className?: string; - title?: string; - description?: string; - variant?: 'default' | 'card' | 'highlight' | 'dark' | 'light'; + variant?: 'default' | 'dark' | 'muted'; + padding?: 'none' | 'sm' | 'md' | 'lg'; id?: string; - py?: number; - minHeight?: string; - borderBottom?: boolean; - borderColor?: string; - overflow?: 'hidden' | 'visible' | 'auto' | 'scroll'; - position?: 'relative' | 'absolute' | 'fixed' | 'sticky'; } -export function Section({ +export const Section = ({ children, - className = '', - title, - description, variant = 'default', - id, - py = 16, - minHeight, - borderBottom, - borderColor, - overflow, - position -}: SectionProps) { + padding = 'md', + id +}: SectionProps) => { const variantClasses = { - default: '', - card: 'bg-panel-gray rounded-none p-6 border border-border-gray', - highlight: 'bg-gradient-to-r from-primary-accent/10 to-transparent rounded-none p-6 border border-primary-accent/30', - dark: 'bg-graphite-black', - light: 'bg-panel-gray' + default: 'bg-[var(--ui-color-bg-base)]', + dark: 'bg-black', + muted: 'bg-[var(--ui-color-bg-surface)]', }; - + + const paddingClasses = { + none: 'py-0', + sm: 'py-8', + md: 'py-16', + lg: 'py-24', + }; + const classes = [ variantClasses[variant], - className - ].filter(Boolean).join(' '); - + paddingClasses[padding], + ].join(' '); + return ( - - - {(title || description) && ( - - {title && {title}} - {description && {description}} - - )} +
+ {children} - +
); -} +}; diff --git a/apps/website/ui/SectionHeader.tsx b/apps/website/ui/SectionHeader.tsx index 8c8ca2552..6be417b32 100644 --- a/apps/website/ui/SectionHeader.tsx +++ b/apps/website/ui/SectionHeader.tsx @@ -1,51 +1,55 @@ import React, { ReactNode } from 'react'; import { Box } from './primitives/Box'; -import { Stack } from './primitives/Stack'; import { Text } from './Text'; -import { Surface } from './primitives/Surface'; import { Icon } from './Icon'; import { LucideIcon } from 'lucide-react'; +import { Surface } from './primitives/Surface'; -interface SectionHeaderProps { +export interface SectionHeaderProps { title: string; description?: string; icon?: LucideIcon; - color?: string; + intent?: 'primary' | 'success' | 'warning' | 'critical' | 'telemetry'; actions?: ReactNode; } -export function SectionHeader({ title, description, icon, color = 'text-primary-blue', actions }: SectionHeaderProps) { +export const SectionHeader = ({ + title, + description, + icon, + intent = 'primary', + actions +}: SectionHeaderProps) => { return ( - - + + {icon && ( - - + + )} - + {title} {description && ( - + {description} )} - + {actions && ( - + {actions} )} - +
); -} +}; diff --git a/apps/website/ui/SegmentedControl.tsx b/apps/website/ui/SegmentedControl.tsx index 5c4794671..e7068de6b 100644 --- a/apps/website/ui/SegmentedControl.tsx +++ b/apps/website/ui/SegmentedControl.tsx @@ -1,82 +1,52 @@ +import React from 'react'; import { Box } from './primitives/Box'; -import { Stack } from './primitives/Stack'; import { Text } from './Text'; +import { Surface } from './primitives/Surface'; -interface SegmentedControlOption { - value: string; +export interface SegmentedControlOption { + id: string; label: string; - description?: string; - disabled?: boolean; + icon?: React.ReactNode; } -interface SegmentedControlProps { +export interface SegmentedControlProps { options: SegmentedControlOption[]; - value: string; - onChange?: (value: string) => void; + activeId: string; + onChange: (id: string) => void; + fullWidth?: boolean; } -export function SegmentedControl({ - options, - value, +export const SegmentedControl = ({ + options, + activeId, onChange, -}: SegmentedControlProps) { - const handleSelect = (optionValue: string, optionDisabled?: boolean) => { - if (!onChange || optionDisabled) return; - if (optionValue === value) return; - onChange(optionValue); - }; - + fullWidth = false +}: SegmentedControlProps) => { return ( - {options.map((option) => { - const isSelected = option.value === value; - + const isSelected = option.id === activeId; return ( - handleSelect(option.value, option.disabled)} - aria-pressed={isSelected} - disabled={option.disabled} - flex={1} - minWidth="140px" - px={3} - py={1.5} - rounded="full" - transition="all 0.2s" - textAlign="left" - bg={isSelected ? 'bg-primary-blue' : 'transparent'} - color={isSelected ? 'text-white' : 'text-gray-400'} - opacity={option.disabled ? 0.5 : 1} - cursor={option.disabled ? 'not-allowed' : 'pointer'} - border="none" + ); })} - + ); -} +}; diff --git a/apps/website/ui/Select.tsx b/apps/website/ui/Select.tsx index 3026f54c5..1e9a7cda2 100644 --- a/apps/website/ui/Select.tsx +++ b/apps/website/ui/Select.tsx @@ -1,64 +1,90 @@ -import React, { forwardRef, ReactNode } from 'react'; +import React, { forwardRef, SelectHTMLAttributes } from 'react'; import { Box } from './primitives/Box'; -import { Stack } from './primitives/Stack'; import { Text } from './Text'; -interface SelectOption { - value: string; +export interface SelectOption { + value: string | number; label: string; } -interface SelectProps extends React.SelectHTMLAttributes { +export interface SelectProps extends Omit, 'size'> { label?: string; + options: SelectOption[]; + error?: string; + hint?: string; fullWidth?: boolean; - pl?: number; - errorMessage?: string; - variant?: 'default' | 'error'; - options?: SelectOption[]; + size?: 'sm' | 'md' | 'lg'; } -export const Select = forwardRef( - ({ label, fullWidth = true, pl, errorMessage, variant = 'default', options, children, className = '', style, ...props }, ref) => { - const isError = variant === 'error' || !!errorMessage; - - const variantClasses = isError - ? 'border-warning-amber focus:border-warning-amber' - : 'border-charcoal-outline focus:border-primary-blue'; - - const defaultClasses = `${fullWidth ? 'w-full' : 'w-auto'} px-3 py-2 bg-deep-graphite border rounded-lg text-white focus:outline-none transition-colors`; - const classes = [ - defaultClasses, - variantClasses, - pl ? `pl-${pl}` : '', - className - ].filter(Boolean).join(' '); +export const Select = forwardRef(({ + label, + options, + error, + hint, + fullWidth = false, + size = 'md', + ...props +}, ref) => { + const sizeClasses = { + sm: 'px-3 py-1.5 text-xs', + md: 'px-4 py-2 text-sm', + lg: 'px-4 py-3 text-base' + }; - return ( - - {label && ( - + const baseClasses = 'bg-[var(--ui-color-bg-surface)] border border-[var(--ui-color-border-default)] text-[var(--ui-color-text-high)] focus:outline-none focus:border-[var(--ui-color-intent-primary)] transition-colors appearance-none'; + const errorClasses = error ? 'border-[var(--ui-color-intent-critical)]' : ''; + const widthClasses = fullWidth ? 'w-full' : ''; + + const classes = [ + baseClasses, + sizeClasses[size], + errorClasses, + widthClasses, + ].filter(Boolean).join(' '); + + return ( + + {label && ( + + {label} - )} - + )} +
+ +
+ + + +
+
+ {error && ( + + + {error} - )} -
- ); - } -); +
+ )} + {hint && !error && ( + + + {hint} + + + )} +
+ ); +}); Select.displayName = 'Select'; diff --git a/apps/website/ui/SidebarActionLink.tsx b/apps/website/ui/SidebarActionLink.tsx index 14eeb21f0..f9f4f033d 100644 --- a/apps/website/ui/SidebarActionLink.tsx +++ b/apps/website/ui/SidebarActionLink.tsx @@ -1,37 +1,66 @@ import React from 'react'; -import { ChevronRight, LucideIcon } from 'lucide-react'; import { Box } from './primitives/Box'; import { Text } from './Text'; import { Icon } from './Icon'; +import { LucideIcon, ChevronRight } from 'lucide-react'; import { Link } from './Link'; -interface SidebarActionLinkProps { - href: string; - icon: LucideIcon; +export interface SidebarActionLinkProps { label: string; - iconColor?: string; - iconBgColor?: string; + icon: LucideIcon; + href: string; + isActive?: boolean; } -export function SidebarActionLink({ +export const SidebarActionLink = ({ + label, + icon, href, - icon, - label, - iconColor = 'text-primary-blue', - iconBgColor = 'bg-primary-blue/10', -}: SidebarActionLinkProps) { + isActive = false +}: SidebarActionLinkProps) => { return ( - - - + + + + + + {label} + + - {label} - ); -} +}; diff --git a/apps/website/ui/SimpleCheckbox.tsx b/apps/website/ui/SimpleCheckbox.tsx index 75eda2cd2..d70ecee81 100644 --- a/apps/website/ui/SimpleCheckbox.tsx +++ b/apps/website/ui/SimpleCheckbox.tsx @@ -1,35 +1,31 @@ -import React from 'react'; +import React, { forwardRef, ChangeEvent } from 'react'; import { Box } from './primitives/Box'; -interface CheckboxProps { +export interface SimpleCheckboxProps { checked: boolean; onChange: (checked: boolean) => void; disabled?: boolean; - 'aria-label'?: string; } -/** - * SimpleCheckbox - * - * A checkbox without a label for use in tables. - */ -export function SimpleCheckbox({ checked, onChange, disabled, 'aria-label': ariaLabel }: CheckboxProps) { +export const SimpleCheckbox = forwardRef(({ + checked, + onChange, + disabled = false +}, ref) => { + const handleChange = (e: ChangeEvent) => { + onChange(e.target.checked); + }; + return ( - ) => onChange(e.target.checked)} + onChange={handleChange} disabled={disabled} - w="4" - h="4" - bg="bg-deep-graphite" - border - borderColor="border-charcoal-outline" - rounded="sm" - aria-label={ariaLabel} - ring="primary-blue" - color="text-primary-blue" + className="w-4 h-4 rounded-none border-[var(--ui-color-border-default)] bg-[var(--ui-color-bg-surface)] text-[var(--ui-color-intent-primary)] focus:ring-[var(--ui-color-intent-primary)] cursor-pointer disabled:cursor-not-allowed" /> ); -} +}); + +SimpleCheckbox.displayName = 'SimpleCheckbox'; diff --git a/apps/website/ui/Skeleton.tsx b/apps/website/ui/Skeleton.tsx index c4a449a96..50b303ecf 100644 --- a/apps/website/ui/Skeleton.tsx +++ b/apps/website/ui/Skeleton.tsx @@ -1,23 +1,44 @@ import React from 'react'; import { Box } from './primitives/Box'; -interface SkeletonProps { +export interface SkeletonProps { width?: string | number; height?: string | number; - circle?: boolean; - className?: string; + variant?: 'text' | 'circular' | 'rectangular'; + animation?: 'pulse' | 'wave' | 'none'; } -export function Skeleton({ width, height, circle, className = '' }: SkeletonProps) { +export const Skeleton = ({ + width, + height, + variant = 'rectangular', + animation = 'pulse' +}: SkeletonProps) => { + const variantClasses = { + text: 'rounded-sm', + circular: 'rounded-full', + rectangular: 'rounded-none', + }; + + const animationClasses = { + pulse: 'animate-pulse', + wave: 'animate-shimmer', // Assuming shimmer is defined + none: '', + }; + + const classes = [ + 'bg-[var(--ui-color-bg-surface-muted)]', + variantClasses[variant], + animationClasses[animation], + ].join(' '); + return ( ); -} +}; diff --git a/apps/website/ui/StatBox.tsx b/apps/website/ui/StatBox.tsx index be737d137..729eb5ffa 100644 --- a/apps/website/ui/StatBox.tsx +++ b/apps/website/ui/StatBox.tsx @@ -1,30 +1,38 @@ import React from 'react'; -import { LucideIcon } from 'lucide-react'; -import { Stack } from './primitives/Stack'; +import { Surface } from './primitives/Surface'; import { Box } from './primitives/Box'; import { Text } from './Text'; import { Icon } from './Icon'; -import { Surface } from './primitives/Surface'; +import { LucideIcon } from 'lucide-react'; -interface StatBoxProps { - icon: LucideIcon; +export interface StatBoxProps { label: string; value: string | number; - color?: string; + icon: LucideIcon; + intent?: 'primary' | 'success' | 'warning' | 'critical' | 'telemetry'; } -export function StatBox({ icon, label, value, color = 'text-primary-blue' }: StatBoxProps) { +export const StatBox = ({ + label, + value, + icon, + intent = 'primary' +}: StatBoxProps) => { return ( - - - - - - - {value} - {label} + + + + - + + + {label} + + + {value} + + + ); -} +}; diff --git a/apps/website/ui/StatCard.tsx b/apps/website/ui/StatCard.tsx index 266f55473..6bd48714c 100644 --- a/apps/website/ui/StatCard.tsx +++ b/apps/website/ui/StatCard.tsx @@ -1,115 +1,68 @@ import React, { ReactNode } from 'react'; -import { Box } from './primitives/Box'; -import { Stack } from './primitives/Stack'; -import { Text } from './Text'; import { Card } from './Card'; +import { Box } from './primitives/Box'; +import { Text } from './Text'; import { Icon } from './Icon'; import { LucideIcon } from 'lucide-react'; -interface StatCardProps { +export interface StatCardProps { label: string; value: string | number; icon?: LucideIcon; + intent?: 'primary' | 'success' | 'warning' | 'critical' | 'telemetry'; trend?: { value: number; isPositive: boolean; }; - variant?: 'default' | 'primary' | 'success' | 'warning' | 'danger' | 'info'; - className?: string; - onClick?: () => void; - prefix?: string; - suffix?: string; - delay?: number; + footer?: ReactNode; } -export function StatCard({ - label, - value, - icon, +export const StatCard = ({ + label, + value, + icon, + intent = 'primary', trend, - variant = 'default', - className = '', - onClick, - prefix, - suffix, - delay, -}: StatCardProps) { - const variantClasses = { - default: 'bg-panel-gray border-border-gray', - primary: 'bg-primary-accent/5 border-primary-accent/20', - success: 'bg-success-green/5 border-success-green/20', - warning: 'bg-warning-amber/5 border-warning-amber/20', - danger: 'bg-critical-red/5 border-critical-red/20', - info: 'bg-telemetry-aqua/5 border-telemetry-aqua/20', - }; - - const iconBgClasses = { - default: 'bg-white/5', - primary: 'bg-primary-accent/10', - success: 'bg-success-green/10', - warning: 'bg-warning-amber/10', - danger: 'bg-critical-red/10', - info: 'bg-telemetry-aqua/10', - }; - - const iconColorClasses = { - default: 'text-gray-400', - primary: 'text-primary-accent', - success: 'text-success-green', - warning: 'text-warning-amber', - danger: 'text-critical-red', - info: 'text-telemetry-aqua', - }; - - const cardContent = ( - - - - + footer +}: StatCardProps) => { + return ( + + + + {label} - {icon && ( - - - - )} - - - - - {prefix}{value}{suffix} + + {value} - {trend && ( - - - {trend.isPositive ? '+' : ''}{trend.value}% - - - vs last period - - - )} - - + + {icon && ( + + + + )} + + + {trend && ( + + + {trend.isPositive ? '+' : '-'}{Math.abs(trend.value)}% + + + vs last period + + + )} + + {footer && ( + + {footer} + + )} ); - - if (onClick) { - return ( - - {cardContent} - - ); - } - - return cardContent; -} +}; diff --git a/apps/website/ui/StatGrid.tsx b/apps/website/ui/StatGrid.tsx index 9201a273a..de11facdc 100644 --- a/apps/website/ui/StatGrid.tsx +++ b/apps/website/ui/StatGrid.tsx @@ -1,58 +1,21 @@ import React from 'react'; import { Grid } from './primitives/Grid'; -import { GridItem } from './primitives/GridItem'; -import { Surface } from './primitives/Surface'; -import { Text } from './Text'; -import { Stack } from './primitives/Stack'; +import { StatBox, StatBoxProps } from './StatBox'; -type GridCols = 1 | 2 | 3 | 4 | 5 | 6 | 12; - -interface StatItem { - label: string; - value: string | number; - subValue?: string; - color?: string; - icon?: React.ElementType; +export interface StatGridProps { + stats: StatBoxProps[]; + columns?: number; } -interface StatGridProps { - stats: StatItem[]; - cols?: GridCols; - mdCols?: GridCols; - lgCols?: GridCols; - className?: string; -} - -export function StatGrid({ stats, cols = 2, mdCols = 3, lgCols = 4, className = '' }: StatGridProps) { +export const StatGrid = ({ + stats, + columns = 3 +}: StatGridProps) => { return ( - + {stats.map((stat, index) => ( - - - - - {stat.label} - - - - {stat.value} - - {stat.subValue && ( - - {stat.subValue} - - )} - - - - + ))} ); -} +}; diff --git a/apps/website/ui/StatGridItem.tsx b/apps/website/ui/StatGridItem.tsx index 9a86a91b9..d2de63773 100644 --- a/apps/website/ui/StatGridItem.tsx +++ b/apps/website/ui/StatGridItem.tsx @@ -1,36 +1,36 @@ -import React, { ReactNode } from 'react'; +import React from 'react'; import { Box } from './primitives/Box'; -import { Stack } from './primitives/Stack'; import { Text } from './Text'; -import { Icon } from './Icon'; -import { LucideIcon } from 'lucide-react'; +import { Stack } from './primitives/Stack'; -interface StatGridItemProps { +export interface StatGridItemProps { label: string; value: string | number; - icon?: LucideIcon; - color?: string; + icon?: React.ReactNode; + intent?: 'primary' | 'success' | 'warning' | 'critical' | 'high' | 'med' | 'low'; + color?: string; // Alias for intent } -/** - * StatGridItem - * - * A simple stat display for use in a grid. - */ -export function StatGridItem({ label, value, icon, color = 'text-primary-blue' }: StatGridItemProps) { +export const StatGridItem = ({ + label, + value, + icon, + intent = 'high', + color +}: StatGridItemProps) => { return ( - + {icon && ( - - + + {icon} )} - + {value} - + {label} ); -} +}; diff --git a/apps/website/ui/StatItem.tsx b/apps/website/ui/StatItem.tsx index 2733bf198..104c7f4e2 100644 --- a/apps/website/ui/StatItem.tsx +++ b/apps/website/ui/StatItem.tsx @@ -2,18 +2,23 @@ import React from 'react'; import { Box } from './primitives/Box'; import { Text } from './Text'; -interface StatItemProps { +export interface StatItemProps { label: string; value: string | number; - color?: string; + intent?: 'primary' | 'success' | 'warning' | 'critical' | 'high' | 'med' | 'low'; align?: 'left' | 'center' | 'right'; } -export function StatItem({ label, value, color = 'text-white', align = 'left' }: StatItemProps) { +export const StatItem = ({ + label, + value, + intent = 'high', + align = 'left' +}: StatItemProps) => { return ( - - {label} - {value} + + {label} + {value} ); -} +}; diff --git a/apps/website/ui/StatusBadge.tsx b/apps/website/ui/StatusBadge.tsx index 96a1446d3..afac8e206 100644 --- a/apps/website/ui/StatusBadge.tsx +++ b/apps/website/ui/StatusBadge.tsx @@ -3,36 +3,36 @@ import { Icon } from './Icon'; import { LucideIcon } from 'lucide-react'; import { Stack } from './primitives/Stack'; -interface StatusBadgeProps { +export interface StatusBadgeProps { children: React.ReactNode; variant?: 'success' | 'warning' | 'error' | 'info' | 'neutral' | 'pending'; - className?: string; icon?: LucideIcon; + className?: string; } export function StatusBadge({ children, variant = 'success', - className = '', icon, + className }: StatusBadgeProps) { const variantClasses = { - success: 'bg-performance-green/20 text-performance-green border-performance-green/30', - warning: 'bg-warning-amber/20 text-warning-amber border-warning-amber/30', - error: 'bg-red-600/20 text-red-400 border-red-600/30', - info: 'bg-blue-500/20 text-blue-400 border-blue-500/30', - neutral: 'bg-iron-gray text-gray-400 border-charcoal-outline', - pending: 'bg-warning-amber/20 text-warning-amber border-warning-amber/30', + success: 'bg-[var(--ui-color-intent-success)]/20 text-[var(--ui-color-intent-success)] border-[var(--ui-color-intent-success)]/30', + warning: 'bg-[var(--ui-color-intent-warning)]/20 text-[var(--ui-color-intent-warning)] border-[var(--ui-color-intent-warning)]/30', + error: 'bg-[var(--ui-color-intent-critical)]/20 text-[var(--ui-color-intent-critical)] border-[var(--ui-color-intent-critical)]/30', + info: 'bg-[var(--ui-color-intent-primary)]/20 text-[var(--ui-color-intent-primary)] border-[var(--ui-color-intent-primary)]/30', + neutral: 'bg-[var(--ui-color-bg-surface-muted)] text-[var(--ui-color-text-med)] border-[var(--ui-color-border-default)]', + pending: 'bg-[var(--ui-color-intent-warning)]/20 text-[var(--ui-color-intent-warning)] border-[var(--ui-color-intent-warning)]/30', }; const classes = [ - 'px-2 py-0.5 text-xs rounded-full border font-medium inline-flex items-center', + 'px-2 py-0.5 text-[10px] uppercase tracking-wider rounded-full border font-bold inline-flex items-center', variantClasses[variant], className - ].filter(Boolean).join(' '); + ].join(' '); const content = icon ? ( - + {children} @@ -43,4 +43,4 @@ export function StatusBadge({ {content} ); -} \ No newline at end of file +} diff --git a/apps/website/ui/StatusDot.tsx b/apps/website/ui/StatusDot.tsx index d04345ae7..cfae964b7 100644 --- a/apps/website/ui/StatusDot.tsx +++ b/apps/website/ui/StatusDot.tsx @@ -1,11 +1,11 @@ import React from 'react'; import { Box } from './primitives/Box'; -interface StatusDotProps { - color?: string; +export interface StatusDotProps { + intent?: 'primary' | 'success' | 'warning' | 'critical' | 'telemetry'; + color?: string; // Alias for intent or custom color pulse?: boolean; - size?: number; - className?: string; + size?: 'sm' | 'md' | 'lg' | number; } /** @@ -14,28 +14,41 @@ interface StatusDotProps { * A simple status indicator dot with optional pulse effect. */ export function StatusDot({ - color = '#4ED4E0', + intent = 'telemetry', + color: colorProp, pulse = false, - size = 2, - className = '' + size = 'md', }: StatusDotProps) { - const sizeClass = `w-${size} h-${size}`; + const intentColorMap = { + primary: 'var(--ui-color-intent-primary)', + success: 'var(--ui-color-intent-success)', + warning: 'var(--ui-color-intent-warning)', + critical: 'var(--ui-color-intent-critical)', + telemetry: 'var(--ui-color-intent-telemetry)', + }; + + const sizeMap = { + sm: '0.375rem', + md: '0.5rem', + lg: '0.75rem', + }; + + const color = colorProp || intentColorMap[intent]; + const dimension = typeof size === 'string' ? sizeMap[size] : `${size * 0.25}rem`; return ( - + {pulse && ( )} diff --git a/apps/website/ui/StatusIndicator.tsx b/apps/website/ui/StatusIndicator.tsx index 3b21f2878..c212c0a57 100644 --- a/apps/website/ui/StatusIndicator.tsx +++ b/apps/website/ui/StatusIndicator.tsx @@ -1,110 +1,94 @@ - - -import { LucideIcon } from 'lucide-react'; import React from 'react'; import { Box } from './primitives/Box'; import { Text } from './Text'; -import { Icon } from './Icon'; +import { StatusDot } from './StatusDot'; +import { Badge } from './Badge'; -interface StatusIndicatorProps { - icon: LucideIcon; - label: string; +export { Badge }; + +export interface StatusIndicatorProps { + status?: 'live' | 'upcoming' | 'completed' | 'cancelled' | 'pending'; + variant?: string; // Alias for status + label?: string; subLabel?: string; - variant: 'success' | 'warning' | 'danger' | 'info'; + size?: 'sm' | 'md' | 'lg'; + icon?: any; // Alias for status dot } -export function StatusIndicator({ icon, label, subLabel, variant }: StatusIndicatorProps) { - const colors = { - success: { - text: 'text-performance-green', - bg: 'bg-green-500/10', - border: 'border-green-500/30', - icon: 'rgb(16, 185, 129)' +export const StatusIndicator = ({ + status, + variant, + label, + subLabel, + size = 'md', + icon +}: StatusIndicatorProps) => { + const activeStatus = (status || variant || 'pending') as any; + + const configMap: any = { + live: { + intent: 'success' as const, + pulse: true, + text: 'Live', }, - warning: { - text: 'text-warning-amber', - bg: 'bg-yellow-500/10', - border: 'border-yellow-500/30', - icon: 'rgb(245, 158, 11)' + upcoming: { + intent: 'primary' as const, + pulse: false, + text: 'Upcoming', + }, + completed: { + intent: 'telemetry' as const, + pulse: false, + text: 'Completed', + }, + cancelled: { + intent: 'critical' as const, + pulse: false, + text: 'Cancelled', + }, + pending: { + intent: 'warning' as const, + pulse: false, + text: 'Pending', + }, + success: { + intent: 'success' as const, + pulse: false, + text: 'Success', }, danger: { - text: 'text-red-400', - bg: 'bg-red-500/10', - border: 'border-red-500/30', - icon: 'rgb(239, 68, 68)' + intent: 'critical' as const, + pulse: false, + text: 'Danger', + }, + warning: { + intent: 'warning' as const, + pulse: false, + text: 'Warning', }, - info: { - text: 'text-primary-blue', - bg: 'bg-blue-500/10', - border: 'border-blue-500/30', - icon: 'rgb(59, 130, 246)' - } }; - const config = colors[variant]; + const config = configMap[activeStatus] || configMap.pending; return ( - - - - {label} - - {subLabel && ( - - {subLabel} + + + + + {label || config.text} - )} + {subLabel && {subLabel}} + ); -} +}; -interface StatRowProps { - label: string; - value: string | number; - valueColor?: string; - valueFont?: 'sans' | 'mono'; -} - -export function StatRow({ label, value, valueColor = 'text-white', valueFont = 'sans' }: StatRowProps) { - return ( - - {label} - - {value} - +export const StatRow = ({ label, value, subLabel, variant, valueColor, valueFont }: { label: string, value: string, subLabel?: string, variant?: string, valueColor?: string, valueFont?: string }) => ( + + + {label} + {subLabel && {subLabel}} - ); -} - -interface BadgeProps { - children: React.ReactNode; - variant: 'success' | 'warning' | 'danger' | 'info' | 'gray'; - size?: 'xs' | 'sm'; -} - -export function Badge({ children, variant, size = 'xs' }: BadgeProps) { - const variants = { - success: 'bg-green-500/20 text-performance-green', - warning: 'bg-yellow-500/20 text-warning-amber', - danger: 'bg-red-500/20 text-red-400', - info: 'bg-blue-500/20 text-primary-blue', - gray: 'bg-gray-500/20 text-gray-400' - }; - - return ( - - - {children} - - - ); -} + {value} + +); diff --git a/apps/website/ui/StepIndicator.tsx b/apps/website/ui/StepIndicator.tsx index 469292342..69e24f6fb 100644 --- a/apps/website/ui/StepIndicator.tsx +++ b/apps/website/ui/StepIndicator.tsx @@ -1,65 +1,63 @@ -import { User, Camera, Check } from 'lucide-react'; +import React from 'react'; +import { Box } from './primitives/Box'; +import { Text } from './Text'; +import { Check } from 'lucide-react'; +import { Icon } from './Icon'; -interface Step { - id: number; +export interface Step { + id: string; label: string; - icon: React.ElementType; } -interface StepIndicatorProps { - currentStep: number; - steps?: Step[]; +export interface StepIndicatorProps { + steps: Step[]; + currentStepId: string; + completedStepIds: string[]; } -const DEFAULT_STEPS: Step[] = [ - { id: 1, label: 'Personal', icon: User }, - { id: 2, label: 'Avatar', icon: Camera }, -]; - -export function StepIndicator({ currentStep, steps = DEFAULT_STEPS }: StepIndicatorProps) { +export const StepIndicator = ({ + steps, + currentStepId, + completedStepIds +}: StepIndicatorProps) => { return ( -
+ {steps.map((step, index) => { - const Icon = step.icon; - const isCompleted = step.id < currentStep; - const isCurrent = step.id === currentStep; + const isCurrent = step.id === currentStepId; + const isCompleted = completedStepIds.includes(step.id); + const isLast = index === steps.length - 1; return ( -
-
-
+ + {isCompleted ? ( - + ) : ( - + + {index + 1} + )} -
- + + {step.label} - -
- {index < steps.length - 1 && ( -
+ + + {!isLast && ( + )} -
+ ); })} -
+
); -} \ No newline at end of file +}; diff --git a/apps/website/ui/SummaryItem.tsx b/apps/website/ui/SummaryItem.tsx index 605f06dc7..010117e0b 100644 --- a/apps/website/ui/SummaryItem.tsx +++ b/apps/website/ui/SummaryItem.tsx @@ -1,56 +1,46 @@ -import React from 'react'; +import React, { ReactNode } from 'react'; import { Box } from './primitives/Box'; import { Text } from './Text'; import { Surface } from './primitives/Surface'; import { Icon } from './Icon'; import { LucideIcon } from 'lucide-react'; -interface SummaryItemProps { - label?: string; - value?: string | number; - icon?: LucideIcon; +export interface SummaryItemProps { + label: string; + value: string | number; + icon: LucideIcon; + intent?: 'primary' | 'success' | 'warning' | 'critical' | 'telemetry'; onClick?: () => void; - title?: string; - subtitle?: string; - rightContent?: React.ReactNode; } -export function SummaryItem({ label, value, icon, onClick, title, subtitle, rightContent }: SummaryItemProps) { +export const SummaryItem = ({ + label, + value, + icon, + intent = 'primary', + onClick +}: SummaryItemProps) => { return ( - - {icon && ( - - + + + - )} - - {(label || title) && ( - - {label || title} - - )} - {(value || subtitle) && ( - - {value || subtitle} - - )} - - {rightContent && ( - {rightContent} + + {label} + + + {value} + - )} + ); -} +}; diff --git a/apps/website/ui/TabNavigation.tsx b/apps/website/ui/TabNavigation.tsx index f0a7ffb8f..312fdb244 100644 --- a/apps/website/ui/TabNavigation.tsx +++ b/apps/website/ui/TabNavigation.tsx @@ -1,68 +1,46 @@ import React from 'react'; import { Box } from './primitives/Box'; -import { Stack } from './primitives/Stack'; -import { Surface } from './primitives/Surface'; import { Text } from './Text'; +import { Surface } from './primitives/Surface'; -interface Tab { +export interface TabNavigationOption { id: string; label: string; icon?: React.ReactNode; } -interface TabNavigationProps { - tabs: Tab[]; - activeTab: string; - onTabChange: (tabId: string) => void; - className?: string; +export interface TabNavigationProps { + options: TabNavigationOption[]; + activeId: string; + onChange: (id: string) => void; } -export function TabNavigation({ tabs, activeTab, onTabChange, className = '' }: TabNavigationProps) { +export const TabNavigation = ({ + options, + activeId, + onChange +}: TabNavigationProps) => { return ( - - - {tabs.map((tab) => { - const isActive = activeTab === tab.id; - return ( - onTabChange(tab.id)} - variant={isActive ? 'default' : 'ghost'} - bg={isActive ? 'bg-primary-blue' : ''} - rounded="lg" - px={4} - py={2} - transition="all 0.2s" - group - className={`select-none ${isActive ? 'shadow-lg shadow-primary-blue/25' : 'hover:bg-iron-gray/80'}`} - > - - {tab.icon && ( - - {tab.icon} - - )} - - {tab.label} - - - - ); - })} - + + {options.map((option) => { + const isActive = option.id === activeId; + return ( + + ); + })} ); -} +}; diff --git a/apps/website/ui/Table.tsx b/apps/website/ui/Table.tsx index c465fd51c..4bba9720c 100644 --- a/apps/website/ui/Table.tsx +++ b/apps/website/ui/Table.tsx @@ -1,95 +1,87 @@ -import React, { ReactNode, ElementType } from 'react'; -import { Box, BoxProps } from './primitives/Box'; +import React, { ReactNode } from 'react'; +import { Box } from './primitives/Box'; +import { Surface } from './primitives/Surface'; -interface TableProps extends BoxProps<'table'> { +export interface TableProps { children: ReactNode; + className?: string; } -export function Table({ children, className = '', ...props }: TableProps) { - const { border, translate, ...rest } = props; +export const Table = ({ children, className }: TableProps) => { return ( - - + +
{children}
-
+
); -} +}; -interface TableHeaderProps extends BoxProps<'thead'> { - children: ReactNode; -} - -export function TableHeader({ children, className = '', ...props }: TableHeaderProps) { +export const TableHeader = ({ children, className, textAlign, w }: { children: ReactNode, className?: string, textAlign?: 'left' | 'center' | 'right', w?: string }) => { return ( - - {children} - + + + {React.Children.map(children, child => { + if (React.isValidElement(child)) { + return React.cloneElement(child as any, { textAlign: textAlign || (child.props as any).textAlign, w: w || (child.props as any).w }); + } + return child; + })} + + ); -} +}; export const TableHead = TableHeader; -interface TableBodyProps extends BoxProps<'tbody'> { - children: ReactNode; -} - -export function TableBody({ children, className = '', ...props }: TableBodyProps) { +export const TableBody = ({ children }: { children: ReactNode }) => { return ( - + {children} - + ); -} - -interface TableRowProps extends BoxProps<'tr'> { - children: ReactNode; - hoverBg?: string; - clickable?: boolean; - variant?: string; -} - -export function TableRow({ children, className = '', hoverBg, clickable, variant, ...props }: TableRowProps) { - const classes = [ - 'transition-colors', - clickable || props.onClick ? 'cursor-pointer' : '', - hoverBg ? `hover:${hoverBg}` : (clickable || props.onClick ? 'hover:bg-white/5' : ''), - className - ].filter(Boolean).join(' '); +}; +export const TableRow = ({ children, onClick, className, variant, clickable, bg, ...props }: { children: ReactNode, onClick?: () => void, className?: string, variant?: string, clickable?: boolean, bg?: string, [key: string]: any }) => { + const isClickable = clickable || !!onClick; return ( - + {children} - + ); -} - -interface TableCellProps extends BoxProps<'td'> { - children: ReactNode; -} - -export function TableHeaderCell({ children, className = '', ...props }: TableCellProps) { - const classes = [ - 'px-4 py-3 text-xs font-bold text-gray-400 uppercase tracking-wider', - className - ].filter(Boolean).join(' '); +}; +export const TableHeaderCell = ({ children, textAlign, w, className }: { children: ReactNode, textAlign?: 'left' | 'center' | 'right', w?: string, className?: string }) => { + const alignClass = textAlign === 'center' ? 'text-center' : (textAlign === 'right' ? 'text-right' : 'text-left'); return ( - + {children} - + ); -} - -export function TableCell({ children, className = '', ...props }: TableCellProps) { - const classes = [ - 'px-4 py-4 text-sm text-gray-300', - className - ].filter(Boolean).join(' '); +}; +export const TableCell = ({ children, textAlign, className, py, colSpan, w, position, ...props }: { children: ReactNode, textAlign?: 'left' | 'center' | 'right', className?: string, py?: number, colSpan?: number, w?: string, position?: string, [key: string]: any }) => { + const alignClass = textAlign === 'center' ? 'text-center' : (textAlign === 'right' ? 'text-right' : 'text-left'); return ( - + {children} - + ); -} +}; diff --git a/apps/website/ui/Text.tsx b/apps/website/ui/Text.tsx index 407486d15..9a96d8203 100644 --- a/apps/website/ui/Text.tsx +++ b/apps/website/ui/Text.tsx @@ -1,206 +1,121 @@ -import React, { ReactNode, ElementType, ComponentPropsWithoutRef } from 'react'; -import { Box, BoxProps } from './primitives/Box'; +import React, { ReactNode, forwardRef, ElementType } from 'react'; +import { Box, BoxProps, ResponsiveValue } 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 type TextSize = 'xs' | 'sm' | 'md' | 'lg' | 'xl' | '2xl' | '3xl' | '4xl' | 'base'; -type TextSize = 'xs' | 'sm' | 'base' | 'lg' | 'xl' | '2xl' | '3xl' | '4xl'; - -interface ResponsiveTextSize { - base?: TextSize; - sm?: TextSize; - md?: TextSize; - lg?: TextSize; - xl?: TextSize; - '2xl'?: TextSize; -} - -type TextAlign = 'left' | 'center' | 'right'; - -interface ResponsiveTextAlign { - base?: TextAlign; - sm?: TextAlign; - md?: TextAlign; - lg?: TextAlign; - xl?: TextAlign; - '2xl'?: TextAlign; -} - -interface TextProps extends Omit, 'children' | 'className' | 'size'> { - as?: T; +export interface TextProps extends BoxProps { children: ReactNode; - className?: string; - size?: TextSize | ResponsiveTextSize; - weight?: 'light' | 'normal' | 'medium' | 'semibold' | 'bold' | string; - color?: string; - font?: 'mono' | 'sans' | string; - align?: TextAlign | ResponsiveTextAlign; - truncate?: boolean; - uppercase?: boolean; - capitalize?: boolean; - letterSpacing?: 'tighter' | 'tight' | 'normal' | 'wide' | 'wider' | 'widest' | '0.05em' | string; - leading?: 'none' | 'tight' | 'snug' | 'normal' | 'relaxed' | 'loose'; - fontSize?: string; - style?: React.CSSProperties; - block?: boolean; + variant?: 'high' | 'med' | 'low' | 'primary' | 'success' | 'warning' | 'critical' | 'inherit'; + size?: TextSize | ResponsiveValue; + weight?: 'light' | 'normal' | 'medium' | 'semibold' | 'bold'; + as?: ElementType; + align?: 'left' | 'center' | 'right'; italic?: boolean; - lineClamp?: number; - ml?: Spacing | ResponsiveSpacing; - mr?: Spacing | ResponsiveSpacing; - mt?: Spacing | ResponsiveSpacing; - mb?: Spacing | ResponsiveSpacing; + mono?: boolean; + block?: boolean; + uppercase?: boolean; + letterSpacing?: string; + leading?: 'none' | 'tight' | 'snug' | 'normal' | 'relaxed' | 'loose'; + truncate?: boolean; + lineHeight?: string | number; + font?: 'sans' | 'mono'; + hoverTextColor?: string; } -interface ResponsiveSpacing { - base?: Spacing; - sm?: Spacing; - md?: Spacing; - lg?: Spacing; - xl?: Spacing; - '2xl'?: Spacing; -} - -export function Text({ - as, +export const Text = forwardRef(({ children, - className = '', - size = 'base', + variant = 'med', + size = 'md', weight = 'normal', - color = '', - font = 'sans', + as = 'p', align = 'left', - truncate = false, + italic = false, + mono = false, + block = false, uppercase = false, - capitalize = false, letterSpacing, leading, - fontSize, - style, - block = false, - italic = false, - lineClamp, - ml, mr, mt, mb, + truncate = false, + lineHeight, + font, + hoverTextColor, ...props -}: TextProps & ComponentPropsWithoutRef) { - const Tag = (as as ElementType) || 'span'; - - const sizeClasses: Record = { +}, ref) => { + const variantClasses = { + high: 'text-[var(--ui-color-text-high)]', + med: 'text-[var(--ui-color-text-med)]', + low: 'text-[var(--ui-color-text-low)]', + primary: 'text-[var(--ui-color-intent-primary)]', + success: 'text-[var(--ui-color-intent-success)]', + warning: 'text-[var(--ui-color-intent-warning)]', + critical: 'text-[var(--ui-color-intent-critical)]', + inherit: 'text-inherit', + }; + + const sizeMap: Record = { xs: 'text-xs', sm: 'text-sm', base: 'text-base', + md: 'text-base', lg: 'text-lg', xl: 'text-xl', '2xl': 'text-2xl', '3xl': 'text-3xl', - '4xl': 'text-4xl' + '4xl': 'text-4xl', }; - const getSizeClasses = (value: TextSize | ResponsiveTextSize | undefined) => { - if (value === undefined) return ''; - if (typeof value === 'object') { - const classes = []; - if (value.base) classes.push(sizeClasses[value.base]); - if (value.sm) classes.push(`sm:${sizeClasses[value.sm]}`); - if (value.md) classes.push(`md:${sizeClasses[value.md]}`); - if (value.lg) classes.push(`lg:${sizeClasses[value.lg]}`); - if (value.xl) classes.push(`xl:${sizeClasses[value.xl]}`); - if (value['2xl']) classes.push(`2xl:${sizeClasses[value['2xl']]}`); - return classes.join(' '); - } - return sizeClasses[value]; + const getResponsiveSizeClasses = (value: TextSize | ResponsiveValue) => { + if (typeof value === 'string') return sizeMap[value]; + const classes = []; + if (value.base) classes.push(sizeMap[value.base]); + if (value.sm) classes.push(`sm:${sizeMap[value.sm]}`); + if (value.md) classes.push(`md:${sizeMap[value.md]}`); + if (value.lg) classes.push(`lg:${sizeMap[value.lg]}`); + if (value.xl) classes.push(`xl:${sizeMap[value.xl]}`); + return classes.join(' '); }; - const weightClasses: Record = { + const weightClasses = { light: 'font-light', normal: 'font-normal', medium: 'font-medium', semibold: 'font-semibold', - bold: 'font-bold' - }; - - const fontClasses: Record = { - mono: 'font-mono', - sans: 'font-sans' - }; - - const alignClasses: Record = { - left: 'text-left', - center: 'text-center', - right: 'text-right' + bold: 'font-bold', }; - const getAlignClasses = (value: TextAlign | ResponsiveTextAlign | undefined) => { - if (value === undefined) return ''; - if (typeof value === 'object') { - const classes = []; - if (value.base) classes.push(alignClasses[value.base]); - if (value.sm) classes.push(`sm:${alignClasses[value.sm]}`); - if (value.md) classes.push(`md:${alignClasses[value.md]}`); - if (value.lg) classes.push(`lg:${alignClasses[value.lg]}`); - if (value.xl) classes.push(`xl:${alignClasses[value.xl]}`); - if (value['2xl']) classes.push(`2xl:${alignClasses[value['2xl']]}`); - return classes.join(' '); - } - return alignClasses[value]; - }; - - const spacingMap: Record = { - 0: '0', 0.5: '0.5', 1: '1', 1.5: '1.5', 2: '2', 2.5: '2.5', 3: '3', 3.5: '3.5', 4: '4', - 5: '5', 6: '6', 7: '7', 8: '8', 9: '9', 10: '10', 11: '11', 12: '12', 14: '14', - 16: '16', 20: '20', 24: '24', 28: '28', 32: '32', 36: '36', 40: '40', 44: '44', - 48: '48', 52: '52', 56: '56', 60: '60', 64: '64', 72: '72', 80: '80', 96: '96' - }; - - const leadingClasses: Record = { + const leadingClasses = { none: 'leading-none', tight: 'leading-tight', snug: 'leading-snug', normal: 'leading-normal', relaxed: 'leading-relaxed', - loose: 'leading-loose' + loose: 'leading-loose', }; - - 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 as number]}`); - if (value.sm !== undefined) classes.push(`sm:${prefix}-${spacingMap[value.sm as number]}`); - if (value.md !== undefined) classes.push(`md:${prefix}-${spacingMap[value.md as number]}`); - if (value.lg !== undefined) classes.push(`lg:${prefix}-${spacingMap[value.lg as number]}`); - if (value.xl !== undefined) classes.push(`xl:${prefix}-${spacingMap[value.xl as number]}`); - if (value['2xl'] !== undefined) classes.push(`2xl:${prefix}-${spacingMap[value['2xl'] as number]}`); - return classes.join(' '); - } - return `${prefix}-${spacingMap[value as number]}`; - }; - + const classes = [ - block ? 'block' : 'inline', - getSizeClasses(size), - weightClasses[weight] || '', - fontClasses[font] || '', - getAlignClasses(align), - leading ? leadingClasses[leading] : '', - color, - truncate ? 'truncate' : '', - uppercase ? 'uppercase' : '', - capitalize ? 'capitalize' : '', + variantClasses[variant], + getResponsiveSizeClasses(size), + weightClasses[weight], + align === 'center' ? 'text-center' : (align === 'right' ? 'text-right' : 'text-left'), italic ? 'italic' : '', - lineClamp ? `line-clamp-${lineClamp}` : '', - letterSpacing === '0.05em' ? 'tracking-wider' : letterSpacing ? `tracking-${letterSpacing}` : '', - getSpacingClass('ml', ml), - getSpacingClass('mr', mr), - getSpacingClass('mt', mt), - getSpacingClass('mb', mb), - className - ].filter(Boolean).join(' '); - - const combinedStyle = { - ...(fontSize ? { fontSize } : {}), - ...(weight && !weightClasses[weight] ? { fontWeight: weight } : {}), - ...(font && !fontClasses[font] ? { fontFamily: font } : {}), - ...style + (mono || font === 'mono') ? 'font-mono' : 'font-sans', + block ? 'block' : 'inline', + uppercase ? 'uppercase tracking-wider' : '', + leading ? leadingClasses[leading] : '', + truncate ? 'truncate' : '', + hoverTextColor ? `hover:text-${hoverTextColor}` : '', + ].join(' '); + + const style: React.CSSProperties = { + ...(letterSpacing ? { letterSpacing } : {}), + ...(lineHeight ? { lineHeight } : {}), }; - - return {children}; -} + + return ( + + {children} + + ); +}); + +Text.displayName = 'Text'; diff --git a/apps/website/ui/TextArea.tsx b/apps/website/ui/TextArea.tsx index 6135e16f3..d6ff70534 100644 --- a/apps/website/ui/TextArea.tsx +++ b/apps/website/ui/TextArea.tsx @@ -1,49 +1,61 @@ -import React, { forwardRef } from 'react'; +import React, { forwardRef, TextareaHTMLAttributes } from 'react'; import { Box } from './primitives/Box'; -import { Stack } from './primitives/Stack'; import { Text } from './Text'; -interface TextAreaProps extends React.TextareaHTMLAttributes { +export interface TextAreaProps extends TextareaHTMLAttributes { label?: string; - errorMessage?: string; - variant?: 'default' | 'error'; + error?: string; + hint?: string; fullWidth?: boolean; } -export const TextArea = forwardRef( - ({ label, errorMessage, variant = 'default', fullWidth = true, className = '', ...props }, ref) => { - const isError = variant === 'error' || !!errorMessage; - - return ( - - {label && ( - +export const TextArea = forwardRef(({ + label, + error, + hint, + fullWidth = false, + ...props +}, ref) => { + const baseClasses = 'bg-[var(--ui-color-bg-surface)] border border-[var(--ui-color-border-default)] text-[var(--ui-color-text-high)] placeholder-[var(--ui-color-text-low)] focus:outline-none focus:border-[var(--ui-color-intent-primary)] transition-colors p-3 text-sm min-h-[100px]'; + const errorClasses = error ? 'border-[var(--ui-color-intent-critical)]' : ''; + const widthClasses = fullWidth ? 'w-full' : ''; + + const classes = [ + baseClasses, + errorClasses, + widthClasses, + ].filter(Boolean).join(' '); + + return ( + + {label && ( + + {label} - )} - - - {errorMessage && ( - - {errorMessage} - - )} - - ); - } -); + )} +