website refactor

This commit is contained in:
2026-01-18 21:31:08 +01:00
parent 502d4aa092
commit b43a23a48c
96 changed files with 3461 additions and 4067 deletions

View File

@@ -1,49 +1,40 @@
import React, { ReactNode, useState } from 'react';
import { ChevronDown, ChevronUp } from 'lucide-react';
import { ReactNode } from 'react';
import { Icon } from './Icon';
import { Box } from './primitives/Box'; import { Box } from './primitives/Box';
import { Stack } from './primitives/Stack';
import { Text } from './Text'; 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; title: string;
icon: ReactNode;
children: ReactNode; children: ReactNode;
isOpen: boolean; defaultOpen?: boolean;
onToggle: () => void;
} }
export function Accordion({ title, icon, children, isOpen, onToggle }: AccordionProps) { export const Accordion = ({
title,
children,
defaultOpen = false
}: AccordionProps) => {
const [isOpen, setIsOpen] = useState(defaultOpen);
return ( return (
<Box border borderColor="border-charcoal-outline" rounded="lg" overflow="hidden" bg="bg-iron-gray/30"> <Surface variant="muted" rounded="lg" style={{ border: '1px solid var(--ui-color-border-default)', overflow: 'hidden' }}>
<Box <button
as="button" onClick={() => setIsOpen(!isOpen)}
onClick={onToggle} className="w-full flex items-center justify-between px-4 py-3 hover:bg-white/5 transition-colors"
display="flex"
alignItems="center"
justifyContent="between"
px={3}
py={2}
fullWidth
hoverBg="iron-gray/50"
clickable
> >
<Stack direction="row" align="center" gap={2}> <Text weight="bold" size="sm" variant="high">
{icon} {title}
<Text size="xs" weight="semibold" color="text-gray-400" uppercase letterSpacing="wide"> </Text>
{title} <Icon icon={isOpen ? ChevronUp : ChevronDown} size={4} intent="low" />
</Text> </button>
</Stack>
<Icon icon={isOpen ? ChevronDown : ChevronUp} size={4} color="text-gray-400" />
</Box>
{isOpen && ( {isOpen && (
<Box p={3} borderTop borderColor="border-charcoal-outline"> <Box padding={4} borderTop>
{children} {children}
</Box> </Box>
)} )}
</Box> </Surface>
); );
} };

View File

@@ -1,67 +1,44 @@
import React from 'react'; import React, { ReactNode } from 'react';
import { Box } from './primitives/Box'; import { Box } from './primitives/Box';
import { Text } from './Text'; import { Text } from './Text';
import { Surface } from './primitives/Surface'; import { Surface } from './primitives/Surface';
import { Link } from './Link';
interface ActivityItemProps { export interface ActivityItemProps {
title?: string; title: string;
description?: string; description?: string;
timeAgo?: string; timestamp: string;
color?: string; icon?: ReactNode;
headline?: string; children?: ReactNode;
body?: string;
formattedTime?: string;
ctaHref?: string;
ctaLabel?: string;
} }
export function ActivityItem({ export const ActivityItem = ({
title, title,
description, description,
timeAgo, timestamp,
color = 'bg-primary-blue', icon,
headline, children
body, }: ActivityItemProps) => {
formattedTime,
ctaHref,
ctaLabel
}: ActivityItemProps) {
return ( return (
<Surface <Surface variant="muted" rounded="lg" padding={4}>
variant="muted" <Box display="flex" alignItems="start" gap={4}>
rounded="lg" {icon && (
display="flex" <Box padding={2} rounded="lg" bg="var(--ui-color-bg-surface-muted)">
alignItems="start" {icon}
gap={3}
p={4}
>
<Box
w="2"
h="2"
mt={1.5}
rounded="full"
bg={color}
flexShrink={0}
/>
<Box flex={1} minWidth={0}>
<Text color="text-white" weight="medium" block>
{title || headline}
</Text>
<Text size="sm" color="text-gray-400" block mt={0.5}>
{description || body}
</Text>
<Text size="xs" color="text-gray-500" block mt={2}>
{timeAgo || formattedTime}
</Text>
{ctaHref && ctaLabel && (
<Box mt={3}>
<Link href={ctaHref} size="xs" variant="primary">
{ctaLabel}
</Link>
</Box> </Box>
)} )}
<Box flex={1}>
<Box display="flex" alignItems="center" justifyContent="between" marginBottom={1}>
<Text weight="bold" variant="high">{title}</Text>
<Text size="xs" variant="low">{timestamp}</Text>
</Box>
{description && (
<Text size="sm" variant="low" marginBottom={children ? 4 : 0}>
{description}
</Text>
)}
{children}
</Box>
</Box> </Box>
</Surface> </Surface>
); );
} };

View File

@@ -1,44 +1,62 @@
import React from 'react'; import React from 'react';
import { Box } from './primitives/Box';
import { Image } from './Image';
import { Surface } from './primitives/Surface'; import { Surface } from './primitives/Surface';
import { Box } from './primitives/Box';
import { User } from 'lucide-react';
import { Icon } from './Icon';
interface AvatarProps { export interface AvatarProps {
src?: string | null; src?: string;
alt: string; alt?: string;
size?: number; size?: 'sm' | 'md' | 'lg' | 'xl';
className?: string; 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 ( return (
<Surface <Surface
variant="muted" variant="muted"
rounded="full" rounded="full"
border style={{
borderColor="border-charcoal-outline/50" width: sizeMap[size],
className={className} height: sizeMap[size],
w={`${size}px`} overflow: 'hidden',
h={`${size}px`} border: '2px solid var(--ui-color-border-default)'
flexShrink={0} }}
overflow="hidden"
> >
{src ? ( {src ? (
<Image <img
src={src} src={src}
alt={alt} alt={alt}
fullWidth className="w-full h-full object-cover"
fullHeight
className="object-cover"
fallbackSrc="/default-avatar.png"
/> />
) : ( ) : (
<Box fullWidth fullHeight bg="bg-charcoal-outline" display="flex" center> <Box center fullWidth fullHeight bg="var(--ui-color-bg-base)">
<span className="text-gray-400 font-bold" style={{ fontSize: size * 0.4 }}> {fallback ? (
{alt.charAt(0).toUpperCase()} <span className="text-sm font-bold text-[var(--ui-color-text-med)]">{fallback}</span>
</span> ) : (
<Icon icon={User} size={iconSizeMap[size]} intent="low" />
)}
</Box> </Box>
)} )}
</Surface> </Surface>
); );
} };

View File

@@ -1,43 +1,57 @@
import React, { ReactNode } from 'react'; import React, { ReactNode } from 'react';
import { Box, BoxProps } from './primitives/Box'; import { Box } from './primitives/Box';
import { Icon } from './Icon'; import { Icon } from './Icon';
import { LucideIcon } from 'lucide-react'; import { LucideIcon } from 'lucide-react';
import { Stack } from './primitives/Stack';
interface BadgeProps { export interface BadgeProps {
children: ReactNode; children: ReactNode;
variant?: 'default' | 'primary' | 'success' | 'warning' | 'danger' | 'info'; variant?: 'primary' | 'secondary' | 'success' | 'warning' | 'critical' | 'info' | 'outline' | 'default' | 'danger';
size?: 'xs' | 'sm' | 'md'; size?: 'sm' | 'md';
style?: React.CSSProperties;
icon?: LucideIcon; icon?: LucideIcon;
} }
export function Badge({ children, variant = 'default', size = 'sm', icon }: BadgeProps) { export const Badge = ({
const baseClasses = 'flex items-center gap-1.5 border font-bold uppercase tracking-widest'; children,
variant = 'primary',
const sizeClasses = { size = 'md',
xs: 'px-1.5 py-0.5 text-[9px]', style,
sm: 'px-2 py-0.5 text-[10px]', icon
md: 'px-3 py-1 text-xs' }: 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 = { const sizeClasses = {
default: 'bg-gray-500/10 border-gray-500/30 text-gray-400', sm: 'px-1.5 py-0.5 text-[10px]',
primary: 'bg-primary-accent/10 border-primary-accent/30 text-primary-accent', md: 'px-2 py-0.5 text-xs',
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 classes = [ const classes = [
baseClasses, 'inline-flex items-center justify-center font-bold uppercase tracking-wider rounded-none',
sizeClasses[size],
variantClasses[variant], variantClasses[variant],
].filter(Boolean).join(' '); sizeClasses[size],
].join(' ');
const content = icon ? (
<Stack direction="row" align="center" gap={1}>
<Icon icon={icon} size={3} />
{children}
</Stack>
) : children;
return ( return (
<Box className={classes}> <Box as="span" className={classes} style={style}>
{icon && <Icon icon={icon} size={3} />} {content}
{children}
</Box> </Box>
); );
} };

View File

@@ -1,63 +1,55 @@
import React from 'react'; import React from 'react';
import { Box } from './primitives/Box'; import { Box } from './primitives/Box';
import { Stack } from './primitives/Stack';
import { Surface } from './primitives/Surface';
import { Text } from './Text'; import { Text } from './Text';
interface Tab { export interface TabOption {
id: string; id: string;
label: string; label: string;
icon?: React.ReactNode; icon?: React.ReactNode;
} }
interface BorderTabsProps { export interface BorderTabsProps {
tabs: Tab[]; tabs: TabOption[];
activeTab: string; activeTabId: string;
onTabChange: (tabId: string) => void; onTabChange: (id: string) => void;
className?: string;
} }
export function BorderTabs({ tabs, activeTab, onTabChange, className = '' }: BorderTabsProps) { export const BorderTabs = ({
tabs,
activeTabId,
onTabChange
}: BorderTabsProps) => {
return ( return (
<Box borderBottom borderColor="border-border-gray/50" className={className}> <Box display="flex" borderBottom>
<Stack direction="row" gap={8}> {tabs.map((tab) => {
{tabs.map((tab) => { const isActive = tab.id === activeTabId;
const isActive = activeTab === tab.id; return (
return ( <button
<Surface key={tab.id}
key={tab.id} onClick={() => onTabChange(tab.id)}
as="button" className={`px-6 py-4 text-sm font-bold uppercase tracking-widest transition-all relative ${
onClick={() => onTabChange(tab.id)} isActive
variant="ghost" ? 'text-[var(--ui-color-intent-primary)]'
px={1} : 'text-[var(--ui-color-text-low)] hover:text-[var(--ui-color-text-high)]'
py={4} }`}
position="relative" >
borderColor={isActive ? 'border-primary-blue' : ''} <Box display="flex" alignItems="center" gap={2}>
borderBottom={isActive} {tab.icon}
borderWidth={isActive ? '2px' : '0'} {tab.label}
mb="-1px" </Box>
transition="all 0.2s" {isActive && (
group <Box
> position="absolute"
<Stack direction="row" align="center" gap={2}> bottom={0}
{tab.icon && ( left={0}
<Box color={isActive ? 'text-primary-blue' : 'text-gray-400'} groupHoverTextColor={!isActive ? 'white' : undefined}> right={0}
{tab.icon} height="2px"
</Box> bg="var(--ui-color-intent-primary)"
)} />
<Text )}
size="sm" </button>
weight="medium" );
color={isActive ? 'text-primary-blue' : 'text-gray-400'} })}
groupHoverTextColor={!isActive ? 'white' : undefined}
>
{tab.label}
</Text>
</Stack>
</Surface>
);
})}
</Stack>
</Box> </Box>
); );
} };

View File

@@ -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 { export interface BreadcrumbBarProps {
children: React.ReactNode; items: BreadcrumbItem[];
className?: string; actions?: ReactNode;
} }
/** export const BreadcrumbBar = ({
* BreadcrumbBar is a container for breadcrumbs, typically placed at the top of the ContentShell. items,
*/ actions
export function BreadcrumbBar({ children, className = '' }: BreadcrumbBarProps) { }: BreadcrumbBarProps) => {
return ( return (
<div className={`mb-6 flex items-center space-x-2 text-sm ${className}`}> <Box
{children} display="flex"
</div> alignItems="center"
justifyContent="between"
paddingY={4}
borderBottom
>
<Breadcrumbs items={items} />
{actions && (
<Box display="flex" alignItems="center" gap={4}>
{actions}
</Box>
)}
</Box>
); );
} };

View File

@@ -1,50 +1,39 @@
import { Link } from '@/ui/Link'; import React from 'react';
import { Box } from '@/ui/primitives/Box'; import { Box } from './primitives/Box';
import { Stack } from '@/ui/primitives/Stack'; import { Text } from './Text';
import { Text } from '@/ui/Text'; import { ChevronRight } from 'lucide-react';
import { Icon } from './Icon';
import { Link } from './Link';
export type BreadcrumbItem = { export interface BreadcrumbItem {
label: string; label: string;
href?: string; href?: string;
}; }
interface BreadcrumbsProps { export interface BreadcrumbsProps {
items: BreadcrumbItem[]; items: BreadcrumbItem[];
} }
export function Breadcrumbs({ items }: BreadcrumbsProps) { export const Breadcrumbs = ({ items }: BreadcrumbsProps) => {
if (!items || items.length === 0) {
return null;
}
const lastIndex = items.length - 1;
return ( return (
<Box as="nav" aria-label="Breadcrumb" mb={4}> <Box display="flex" alignItems="center" gap={2}>
<Stack direction="row" align="center" gap={2} wrap> {items.map((item, index) => {
{items.map((item, index) => { const isLast = index === items.length - 1;
const isLast = index === lastIndex; return (
const content = item.href && !isLast ? ( <React.Fragment key={index}>
<Link {index > 0 && <Icon icon={ChevronRight} size={3} intent="low" />}
href={item.href} {isLast || !item.href ? (
variant="ghost" <Text size="sm" variant={isLast ? 'high' : 'low'} weight={isLast ? 'bold' : 'normal'}>
> {item.label}
{item.label} </Text>
</Link> ) : (
) : ( <Link href={item.href} variant="secondary">
<Text color={isLast ? 'text-white' : 'text-gray-400'}>{item.label}</Text> <Text size="sm">{item.label}</Text>
); </Link>
)}
return ( </React.Fragment>
<Stack key={`${item.label}-${index}`} direction="row" align="center" gap={2}> );
{index > 0 && ( })}
<Text color="text-gray-600">/</Text>
)}
{content}
</Stack>
);
})}
</Stack>
</Box> </Box>
); );
} };

View File

@@ -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 { Stack } from './primitives/Stack';
import { Box, BoxProps } from './primitives/Box';
import { Loader2 } from 'lucide-react';
import { Icon } from './Icon'; import { Icon } from './Icon';
import { Loader2 } from 'lucide-react';
import { ResponsiveValue } from './primitives/Box';
interface ButtonProps extends Omit<ButtonHTMLAttributes<HTMLButtonElement>, 'as' | 'onMouseEnter' | 'onMouseLeave' | 'onSubmit' | 'role' | 'translate' | 'onScroll' | 'draggable' | 'onChange' | 'onMouseDown' | 'onMouseUp' | 'onMouseMove' | 'value' | 'onBlur' | 'onKeyDown'>, Omit<BoxProps<'button'>, 'as' | 'onClick' | 'onSubmit'> { export interface ButtonProps {
children: ReactNode; children: ReactNode;
onClick?: MouseEventHandler<HTMLButtonElement>; onClick?: MouseEventHandler<HTMLButtonElement | HTMLAnchorElement>;
className?: string; variant?: 'primary' | 'secondary' | 'danger' | 'ghost' | 'success' | 'discord' | 'race-final';
variant?: 'primary' | 'secondary' | 'danger' | 'ghost' | 'race-final' | 'discord';
size?: 'sm' | 'md' | 'lg'; size?: 'sm' | 'md' | 'lg';
disabled?: boolean; disabled?: boolean;
isLoading?: boolean; isLoading?: boolean;
@@ -19,14 +18,25 @@ interface ButtonProps extends Omit<ButtonHTMLAttributes<HTMLButtonElement>, 'as'
href?: string; href?: string;
target?: string; target?: string;
rel?: string; rel?: string;
className?: string;
style?: React.CSSProperties;
width?: string | number | ResponsiveValue<string | number>;
height?: string | number | ResponsiveValue<string | number>;
minWidth?: string | number;
px?: number;
py?: number;
p?: number;
rounded?: string;
bg?: string;
color?: string;
fontSize?: string; fontSize?: string;
backgroundColor?: string; h?: string;
w?: string;
} }
export const Button = forwardRef<HTMLButtonElement, ButtonProps>(({ export const Button = forwardRef<HTMLButtonElement | HTMLAnchorElement, ButtonProps>(({
children, children,
onClick, onClick,
className = '',
variant = 'primary', variant = 'primary',
size = 'md', size = 'md',
disabled = false, disabled = false,
@@ -38,25 +48,37 @@ export const Button = forwardRef<HTMLButtonElement, ButtonProps>(({
href, href,
target, target,
rel, rel,
className,
style: styleProp,
width,
height,
minWidth,
px,
py,
p,
rounded,
bg,
color,
fontSize, fontSize,
backgroundColor, h,
...props w,
}, ref) => { }, ref) => {
const baseClasses = 'inline-flex items-center justify-center rounded-none transition-all duration-150 ease-smooth focus-visible:outline focus-visible:outline-1 focus-visible:outline-offset-2 active:opacity-80 uppercase tracking-widest font-bold'; const baseClasses = 'inline-flex items-center justify-center rounded-none transition-all duration-150 ease-in-out focus-visible:outline focus-visible:outline-1 focus-visible:outline-offset-2 active:opacity-80 uppercase tracking-widest font-bold';
const variantClasses = { 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)]', 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-panel-gray text-white border border-border-gray hover:bg-border-gray/50 focus-visible:outline-primary-accent', 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-critical-red text-white hover:bg-critical-red/90 focus-visible:outline-critical-red', danger: 'bg-[var(--ui-color-intent-critical)] text-white hover:opacity-90 focus-visible:outline-[var(--ui-color-intent-critical)]',
ghost: 'bg-transparent text-gray-400 hover:text-white hover:bg-white/5 focus-visible:outline-gray-400', 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)]',
'race-final': 'bg-success-green text-graphite-black hover:bg-success-green/90 focus-visible:outline-success-green', 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]', discord: 'bg-[#5865F2] text-white hover:bg-[#4752C4] focus-visible:outline-[#5865F2]',
}; };
const sizeClasses = { const sizeClasses = {
sm: 'min-h-[32px] px-3 py-1 text-xs font-medium', sm: 'min-h-[32px] px-3 py-1 text-xs',
md: 'min-h-[40px] px-4 py-2 text-sm font-medium', md: 'min-h-[40px] px-4 py-2 text-sm',
lg: 'min-h-[48px] px-6 py-3 text-base font-medium' lg: 'min-h-[48px] px-6 py-3 text-base'
}; };
const disabledClasses = (disabled || isLoading) ? 'opacity-40 cursor-not-allowed' : 'cursor-pointer'; const disabledClasses = (disabled || isLoading) ? 'opacity-40 cursor-not-allowed' : 'cursor-pointer';
@@ -71,8 +93,18 @@ export const Button = forwardRef<HTMLButtonElement, ButtonProps>(({
className className
].filter(Boolean).join(' '); ].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 = ( const content = (
<Stack direction="row" align="center" gap={2} center={fullWidth}> <Stack direction="row" align="center" gap={2}>
{isLoading && <Icon icon={Loader2} size={size === 'sm' ? 3 : 4} animate="spin" />} {isLoading && <Icon icon={Loader2} size={size === 'sm' ? 3 : 4} animate="spin" />}
{!isLoading && icon} {!isLoading && icon}
{children} {children}
@@ -81,35 +113,31 @@ export const Button = forwardRef<HTMLButtonElement, ButtonProps>(({
if (as === 'a') { if (as === 'a') {
return ( return (
<Box <a
as="a" ref={ref as React.ForwardedRef<HTMLAnchorElement>}
href={href} href={href}
target={target} target={target}
rel={rel} rel={rel}
className={classes} className={classes}
fontSize={fontSize} onClick={onClick as MouseEventHandler<HTMLAnchorElement>}
backgroundColor={backgroundColor} style={style}
{...props}
> >
{content} {content}
</Box> </a>
); );
} }
return ( return (
<Box <button
as="button" ref={ref as React.ForwardedRef<HTMLButtonElement>}
ref={ref}
type={type} type={type}
className={classes} className={classes}
onClick={onClick} onClick={onClick as MouseEventHandler<HTMLButtonElement>}
disabled={disabled || isLoading} disabled={disabled || isLoading}
fontSize={fontSize} style={style}
backgroundColor={backgroundColor}
{...props}
> >
{content} {content}
</Box> </button>
); );
}); });

View File

@@ -1,49 +1,73 @@
import React, { ReactNode, MouseEventHandler } from 'react'; import React, { ReactNode, forwardRef } from 'react';
import { Box, BoxProps } from './primitives/Box'; import { Surface, SurfaceProps } from './primitives/Surface';
import { Box } from './primitives/Box';
export interface CardProps extends Omit<BoxProps<'div'>, 'children' | 'onClick'> { export interface CardProps extends Omit<SurfaceProps<'div'>, 'children' | 'title' | 'variant'> {
children: ReactNode; children: ReactNode;
onClick?: MouseEventHandler<HTMLDivElement>; variant?: 'default' | 'dark' | 'muted' | 'glass' | 'outline';
variant?: 'default' | 'outline' | 'ghost' | 'muted' | 'dark' | 'glass'; title?: ReactNode;
footer?: ReactNode;
} }
export function Card({ export const Card = forwardRef<HTMLDivElement, CardProps>(({
children, children,
className = '',
onClick,
variant = 'default', variant = 'default',
title,
footer,
...props ...props
}: CardProps) { }, ref) => {
const baseClasses = 'rounded-none transition-all duration-150 ease-smooth'; const isOutline = variant === 'outline';
const variantClasses = { const style: React.CSSProperties = isOutline ? {
default: 'bg-panel-gray border border-border-gray shadow-card', backgroundColor: 'transparent',
outline: 'bg-transparent border border-border-gray', border: '1px solid var(--ui-color-border-default)',
ghost: 'bg-transparent border-none', } : {};
muted: 'bg-panel-gray/40 border border-border-gray',
dark: 'bg-graphite-black border border-border-gray',
glass: 'bg-graphite-black/60 backdrop-blur-md border border-border-gray'
};
const classes = [
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 ( return (
<Box <Surface
className={classes} ref={ref}
onClick={onClick} variant={isOutline ? 'default' : variant}
p={hasPadding ? undefined : 4} rounded="lg"
shadow="md"
style={style}
{...props} {...props}
> >
{children} {title && (
</Box> <Box padding={4} borderBottom>
{typeof title === 'string' ? (
<h3 className="text-lg font-bold text-[var(--ui-color-text-high)]">{title}</h3>
) : title}
</Box>
)}
<Box padding={4}>
{children}
</Box>
{footer && (
<Box padding={4} borderTop bg="rgba(255,255,255,0.02)">
{footer}
</Box>
)}
</Surface>
); );
} });
Card.displayName = 'Card';
export const CardHeader = ({ title, children }: { title?: string, children?: ReactNode }) => (
<Box marginBottom={4}>
{title && <h3 className="text-lg font-bold text-[var(--ui-color-text-high)]">{title}</h3>}
{children}
</Box>
);
export const CardContent = ({ children }: { children: ReactNode }) => (
<Box>{children}</Box>
);
export const CardFooter = ({ children }: { children: ReactNode }) => (
<Box marginTop={4} paddingTop={4} borderTop>
{children}
</Box>
);

View File

@@ -1,77 +1,37 @@
import React from 'react';
import { Box } from './primitives/Box';
import { CategoryDistributionCard } from './CategoryDistributionCard';
import { LucideIcon } from 'lucide-react';
export interface CategoryData {
import { CategoryDistributionCard } from '@/ui/CategoryDistributionCard'; id: string;
import { Heading } from '@/ui/Heading'; label: string;
import { Icon } from '@/ui/Icon'; count: number;
import { Box } from '@/ui/primitives/Box'; icon: LucideIcon;
import { Grid } from '@/ui/primitives/Grid'; intent?: 'primary' | 'success' | 'warning' | 'critical' | 'telemetry';
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 function CategoryDistribution({ drivers }: CategoryDistributionProps) { export interface CategoryDistributionProps {
const distribution = CATEGORIES.map((category) => ({ categories: CategoryData[];
...category, }
count: drivers.filter((d) => d.category === category.id).length,
percentage: drivers.length > 0 export const CategoryDistribution = ({
? Math.round((drivers.filter((d) => d.category === category.id).length / drivers.length) * 100) categories
: 0, }: CategoryDistributionProps) => {
})); const total = categories.reduce((acc, cat) => acc + cat.count, 0);
return ( return (
<Box mb={10}> <Box display="grid" gridCols={{ base: 1, md: 2, lg: 3 }} gap={4}>
<Box display="flex" alignItems="center" gap={3} mb={4}> {categories.map((category) => (
<Box <CategoryDistributionCard
display="flex" key={category.id}
h="10" label={category.label}
w="10" count={category.count}
alignItems="center" total={total}
justifyContent="center" icon={category.icon}
rounded="xl" intent={category.intent}
bg="bg-purple-400/10" />
border ))}
borderColor="border-purple-400/20"
>
<Icon
icon={BarChart3}
size={5}
color="rgb(192, 132, 252)"
/>
</Box>
<Box>
<Heading level={2}>Category Distribution</Heading>
<Text size="xs" color="text-gray-500">Driver population by category</Text>
</Box>
</Box>
<Grid cols={2} lgCols={3} gap={4}>
{distribution.map((category) => (
<CategoryDistributionCard
key={category.id}
label={category.label}
count={category.count}
percentage={category.percentage}
color={category.color}
bgColor={category.bgColor}
borderColor={category.borderColor}
progressColor={category.progressColor}
/>
))}
</Grid>
</Box> </Box>
); );
} };

View File

@@ -3,47 +3,57 @@ import { Box } from './primitives/Box';
import { Text } from './Text'; import { Text } from './Text';
import { Icon } from './Icon'; import { Icon } from './Icon';
import { LucideIcon } from 'lucide-react'; import { LucideIcon } from 'lucide-react';
import { Surface } from './primitives/Surface';
interface CategoryDistributionCardProps { export interface CategoryDistributionCardProps {
label: string; label: string;
count: number; count: number;
percentage: number; total: number;
icon: LucideIcon; icon: LucideIcon;
color: string; intent?: 'primary' | 'success' | 'warning' | 'critical' | 'telemetry';
bgColor: string;
borderColor: string;
} }
export function CategoryDistributionCard({ export const CategoryDistributionCard = ({
label, label,
count, count,
percentage, total,
icon, icon,
color, intent = 'primary'
bgColor, }: CategoryDistributionCardProps) => {
borderColor, const percentage = total > 0 ? (count / total) * 100 : 0;
}: CategoryDistributionCardProps) {
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 ( return (
<Box p={4} rounded="xl" bg={bgColor} border borderColor={borderColor}> <Surface variant="muted" rounded="xl" padding={4} style={{ border: '1px solid var(--ui-color-border-default)' }}>
<Box display="flex" alignItems="center" justifyContent="between" mb={3}> <Box display="flex" alignItems="center" justifyContent="between" marginBottom={3}>
<Text size="2xl" weight="bold" color={color}>{count}</Text> <Text size="2xl" weight="bold" variant="high">{count}</Text>
<Box p={2} rounded="lg" bg="bg-white/5"> <Box padding={2} rounded="lg" bg="var(--ui-color-bg-surface-muted)">
<Icon icon={icon} size={5} color={color} /> <Icon icon={icon} size={5} intent={intent} />
</Box> </Box>
</Box> </Box>
<Text size="sm" weight="medium" color="text-white" block mb={1}>
<Text size="sm" weight="medium" variant="high" block marginBottom={1}>
{label} {label}
</Text> </Text>
<Box w="full" h="1.5" bg="bg-white/5" rounded="full" overflow="hidden">
<Box <Box fullWidth height="0.375rem" bg="var(--ui-color-bg-surface-muted)" style={{ borderRadius: '9999px', overflow: 'hidden' }}>
h="full" <Box
bg={color.replace('text-', 'bg-')} fullHeight
style={{ width: `${percentage}%` }} bg={intentColorMap[intent]}
style={{ width: `${percentage}%` }}
/> />
</Box> </Box>
<Text size="xs" color="text-gray-500" mt={2}>
{percentage.toFixed(1)}% of total <Text size="xs" variant="low" marginTop={2}>
{Math.round(percentage)}% of total
</Text> </Text>
</Box> </Surface>
); );
} };

View File

@@ -1,44 +1,29 @@
import React from 'react'; import React from 'react';
import { Box } from './primitives/Box'; import { Box } from './primitives/Box';
import { Image } from './Image';
import { Tag } from 'lucide-react';
import { Icon } from './Icon'; import { Icon } from './Icon';
import { Tag } from 'lucide-react';
export interface CategoryIconProps { export interface CategoryIconProps {
categoryId?: string; category: string;
src?: string;
alt: string;
size?: number; size?: number;
className?: string;
} }
export function CategoryIcon({ export const CategoryIcon = ({
categoryId, category,
src, size = 24
alt, }: CategoryIconProps) => {
size = 24, // Map categories to icons if needed, for now just use Tag
className = '',
}: CategoryIconProps) {
const iconSrc = src || (categoryId ? `/media/categories/${categoryId}/icon` : undefined);
return ( return (
<Box <Box
display="flex" width={size}
alignItems="center" height={size}
justifyContent="center" display="flex"
className={className} alignItems="center"
style={{ width: size, height: size, flexShrink: 0 }} justifyContent="center"
bg="var(--ui-color-bg-surface-muted)"
style={{ borderRadius: 'var(--ui-radius-sm)' }}
> >
{iconSrc ? ( <Icon icon={Tag} size={size > 20 ? 4 : 3} intent="low" />
<Image
src={iconSrc}
alt={alt}
className="w-full h-full object-contain"
fallbackSrc="/default-category-icon.png"
/>
) : (
<Icon icon={Tag} size={size > 20 ? 4 : 3} color="text-gray-500" />
)}
</Box> </Box>
); );
} };

View File

@@ -1,33 +1,50 @@
import React from 'react'; import React, { forwardRef, ChangeEvent } from 'react';
import { Box } from './primitives/Box'; import { Box } from './primitives/Box';
import { Text } from './Text'; import { Text } from './Text';
interface CheckboxProps { export interface CheckboxProps {
label: string; label: string;
checked: boolean; checked: boolean;
onChange: (checked: boolean) => void; onChange: (checked: boolean) => void;
disabled?: boolean; disabled?: boolean;
error?: string;
} }
export function Checkbox({ label, checked, onChange, disabled }: CheckboxProps) { export const Checkbox = forwardRef<HTMLInputElement, CheckboxProps>(({
label,
checked,
onChange,
disabled = false,
error
}, ref) => {
const handleChange = (e: ChangeEvent<HTMLInputElement>) => {
onChange(e.target.checked);
};
return ( return (
<Box as="label" display="flex" alignItems="center" gap={2} cursor={disabled ? 'not-allowed' : 'pointer'}> <Box>
<Box <Box as="label" display="flex" alignItems="center" gap={2} style={{ cursor: disabled ? 'not-allowed' : 'pointer' }}>
as="input" <input
type="checkbox" ref={ref}
checked={checked} type="checkbox"
onChange={(e: React.ChangeEvent<HTMLInputElement>) => onChange(e.target.checked)} checked={checked}
disabled={disabled} onChange={handleChange}
w="4" disabled={disabled}
h="4" 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)]"
bg="bg-deep-graphite" />
border <Text size="sm" variant={disabled ? 'low' : 'high'}>
borderColor="border-charcoal-outline" {label}
rounded="sm" </Text>
ring="primary-blue" </Box>
color="text-primary-blue" {error && (
/> <Box marginTop={1}>
<Text size="sm" color={disabled ? 'text-gray-500' : 'text-white'}>{label}</Text> <Text size="xs" variant="critical">
{error}
</Text>
</Box>
)}
</Box> </Box>
); );
} });
Checkbox.displayName = 'Checkbox';

View File

@@ -1,53 +1,79 @@
import React from 'react';
import { Box } from './primitives/Box';
import { Text } from './Text';
export interface CircularProgressProps {
interface CircularProgressProps {
value: number; value: number;
max: number; max?: number;
label: string;
color: string;
size?: 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) { export const CircularProgress = ({
const percentage = Math.min((value / max) * 100, 100); value,
const strokeWidth = 6; max = 100,
size = 64,
strokeWidth = 4,
intent = 'primary',
showValue = false,
label,
color: colorProp
}: CircularProgressProps) => {
const radius = (size - strokeWidth) / 2; const radius = (size - strokeWidth) / 2;
const circumference = radius * 2 * Math.PI; 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 ( return (
<div className="flex flex-col items-center"> <Box display="flex" flexDirection="col" alignItems="center" gap={2}>
<div className="relative" style={{ width: size, height: size }}> <Box position="relative" width={size} height={size} display="flex" alignItems="center" justifyContent="center">
<svg className="transform -rotate-90" width={size} height={size}> <svg width={size} height={size} className="transform -rotate-90">
<circle <circle
cx={size / 2} cx={size / 2}
cy={size / 2} cy={size / 2}
r={radius} r={radius}
stroke="currentColor" stroke="var(--ui-color-bg-surface-muted)"
strokeWidth={strokeWidth} strokeWidth={strokeWidth}
fill="transparent" fill="transparent"
className="text-charcoal-outline"
/> />
<circle <circle
cx={size / 2} cx={size / 2}
cy={size / 2} cy={size / 2}
r={radius} r={radius}
stroke="currentColor" stroke={color}
strokeWidth={strokeWidth} strokeWidth={strokeWidth}
fill="transparent" fill="transparent"
strokeDasharray={circumference} strokeDasharray={circumference}
strokeDashoffset={strokeDashoffset} style={{ strokeDashoffset: offset, transition: 'stroke-dashoffset 0.3s ease-in-out' }}
strokeLinecap="round" strokeLinecap="round"
className={color}
style={{ transition: 'stroke-dashoffset 0.5s ease-in-out' }}
/> />
</svg> </svg>
<div className="absolute inset-0 flex items-center justify-center"> {showValue && (
<span className="text-lg font-bold text-white">{percentage.toFixed(0)}%</span> <Box position="absolute" inset={0} display="flex" alignItems="center" justifyContent="center">
</div> <Text size="xs" weight="bold" variant="high">
</div> {Math.round((value / max) * 100)}%
<span className="text-xs text-gray-400 mt-2">{label}</span> </Text>
</div> </Box>
)}
</Box>
{label && (
<Text size="xs" weight="bold" variant="low" uppercase letterSpacing="wider">
{label}
</Text>
)}
</Box>
); );
} };

View File

@@ -1,53 +1,26 @@
import React, { ReactNode } from 'react'; 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; export interface ContainerProps {
interface ContainerProps extends Omit<BoxProps<'div'>, 'size' | 'padding'> {
children: ReactNode; children: ReactNode;
size?: 'sm' | 'md' | 'lg' | 'xl' | 'full'; size?: 'sm' | 'md' | 'lg' | 'xl' | 'full';
padding?: boolean;
className?: string;
py?: Spacing;
pb?: Spacing;
} }
export function Container({ export const Container = ({
children, children,
size = 'lg', size = 'lg'
padding = true, }: ContainerProps) => {
className = '', const sizeMap = {
py, sm: '40rem',
pb, md: '48rem',
...props lg: '64rem',
}: ContainerProps) { xl: '80rem',
const sizeClasses = { full: '100%',
sm: 'max-w-2xl',
md: 'max-w-4xl',
lg: 'max-w-7xl',
xl: 'max-w-[1400px]',
full: 'max-w-full'
}; };
const spacingMap: Record<number, string> = {
0: '0', 0.5: '0.5', 1: '1', 1.5: '1.5', 2: '2', 2.5: '2.5', 3: '3', 3.5: '3.5', 4: '4',
5: '5', 6: '6', 7: '7', 8: '8', 9: '9', 10: '10', 11: '11', 12: '12', 14: '14',
16: '16', 20: '20', 24: '24', 28: '28', 32: '32', 36: '36', 40: '40', 44: '44',
48: '48', 52: '52', 56: '56', 60: '60', 64: '64', 72: '72', 80: '80', 96: '96'
};
const 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 ( return (
<Box className={classes} {...props}> <Box marginX="auto" maxWidth={sizeMap[size]} paddingX={4} fullWidth>
{children} {children}
</Box> </Box>
); );
} };

View File

@@ -1,20 +1,34 @@
import React from 'react'; import React, { ReactNode } from 'react';
import { Box } from './primitives/Box';
interface ContentShellProps { export interface ContentShellProps {
children: React.ReactNode; children: ReactNode;
className?: string; header?: ReactNode;
sidebar?: ReactNode;
} }
/** export const ContentShell = ({
* ContentShell is the main data zone of the application. children,
* It houses the primary content and track maps/data tables. header,
*/ sidebar
export function ContentShell({ children, className = '' }: ContentShellProps) { }: ContentShellProps) => {
return ( return (
<main className={`flex-1 overflow-y-auto bg-[#0C0D0F] ${className}`}> <Box display="flex" flexDirection="col" fullHeight>
<div className="max-w-7xl mx-auto px-4 md:px-6 py-6"> {header && (
{children} <Box borderBottom>
</div> {header}
</main> </Box>
)}
<Box display="flex" flex={1} minHeight="0">
{sidebar && (
<Box width="18rem" borderRight display={{ base: 'none', lg: 'block' }}>
{sidebar}
</Box>
)}
<Box flex={1} overflow="auto">
{children}
</Box>
</Box>
</Box>
); );
} };

View File

@@ -1,22 +1,30 @@
import React from 'react'; import React, { ReactNode } from 'react';
import { Box } from './primitives/Box';
import { Container } from './Container';
interface ContentViewportProps { export interface ContentViewportProps {
children: React.ReactNode; children: ReactNode;
className?: string; padding?: 'none' | 'sm' | 'md' | 'lg';
fullWidth?: boolean;
} }
/** export const ContentViewport = ({
* ContentViewport is the main data zone of the "Telemetry Workspace". children,
* It houses the primary content, track maps, and data tables. padding = 'md'
* Aligned with "Precision Racing Minimal" theme. }: ContentViewportProps) => {
*/ const paddingMap: Record<string, any> = {
export function ContentViewport({ children, className = '', fullWidth = false }: ContentViewportProps) { none: 0,
sm: 4,
md: 8,
lg: 12,
};
return ( return (
<main className={`flex-1 overflow-y-auto bg-[#0C0D0F] ${className}`}> <Box as="main" flex={1} overflow="auto">
<div className={fullWidth ? '' : 'max-w-7xl mx-auto px-4 md:px-6 py-6'}> <Container size="xl">
{children} <Box paddingY={paddingMap[padding]}>
</div> {children}
</main> </Box>
</Container>
</Box>
); );
} };

View File

@@ -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 { export interface ControlBarProps {
children: React.ReactNode; children: ReactNode;
className?: string; actions?: ReactNode;
} }
/** export const ControlBar = ({
* ControlBar is the top-level header of the "Telemetry Workspace". children,
* It provides global controls, navigation, and status information. actions
* Aligned with "Precision Racing Minimal" theme. }: ControlBarProps) => {
*/
export function ControlBar({ children, className = '' }: ControlBarProps) {
return ( return (
<header <Surface
className={`sticky top-0 z-50 h-16 md:h-20 bg-[#0C0D0F]/80 backdrop-blur-md border-b border-[#23272B] flex items-center px-4 md:px-6 ${className}`} variant="muted"
padding={4}
style={{ borderBottom: '1px solid var(--ui-color-border-default)' }}
> >
{children} <Box display="flex" alignItems="center" justifyContent="between">
</header> <Box display="flex" alignItems="center" gap={4}>
{children}
</Box>
{actions && (
<Box display="flex" alignItems="center" gap={4}>
{actions}
</Box>
)}
</Box>
</Surface>
); );
} };

View File

@@ -1,97 +1,36 @@
import React from 'react';
import { Box } from './primitives/Box';
export interface CountryFlagProps {
// ISO 3166-1 alpha-2 country code to full country name mapping
const countryNames: Record<string, string> = {
'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 {
countryCode: string; countryCode: string;
size?: 'sm' | 'md' | 'lg'; size?: 'sm' | 'md' | 'lg';
className?: string;
showTooltip?: boolean;
} }
export function CountryFlag({ export const CountryFlag = ({
countryCode, countryCode,
size = 'md', size = 'md'
className = '', }: CountryFlagProps) => {
showTooltip = true const sizeMap = {
}: CountryFlagProps) { sm: '1rem',
const sizeClasses = { md: '1.5rem',
sm: 'text-xs', lg: '2rem',
md: 'text-sm',
lg: 'text-base'
}; };
const flag = countryCodeToFlag(countryCode);
const countryName = countryNames[countryCode.toUpperCase()] || countryCode;
return ( return (
<span <Box
className={`inline-flex items-center relative ${sizeClasses[size]} ${className}`} width={sizeMap[size]}
title={showTooltip ? countryName : undefined} height={sizeMap[size]}
display="flex"
alignItems="center"
justifyContent="center"
overflow="hidden"
style={{ borderRadius: '2px' }}
> >
<span className="select-none">{flag}</span> <img
</span> src={`https://flagcdn.com/w40/${countryCode.toLowerCase()}.png`}
alt={countryCode}
className="w-full h-full object-cover"
/>
</Box>
); );
} };

View File

@@ -1,26 +1,37 @@
import React, { ReactNode } from 'react'; import React, { ReactNode } from 'react';
import { Box } from './primitives/Box'; import { Box } from './primitives/Box';
import { Text } from './Text';
import { Heading } from './Heading'; import { Heading } from './Heading';
import { Card } from './Card'; import { Text } from './Text';
import { Surface } from './primitives/Surface';
interface DangerZoneProps { export interface DangerZoneProps {
title: string; title: string;
description: string; description: string;
children: ReactNode; children: ReactNode;
} }
export function DangerZone({ title, description, children }: DangerZoneProps) { export const DangerZone = ({
title,
description,
children
}: DangerZoneProps) => {
return ( return (
<Card> <Box marginTop={8}>
<Heading level={3} mb={4}>Danger Zone</Heading> <Heading level={3} marginBottom={4}>Danger Zone</Heading>
<Box p={4} rounded="lg" bg="bg-red-900/10" border={true} borderColor="border-red-900/30"> <Surface
<Text color="text-white" weight="medium" block mb={2}>{title}</Text> variant="muted"
<Text size="sm" color="text-gray-400" block mb={4}> rounded="lg"
{description} padding={4}
</Text> style={{ border: '1px solid var(--ui-color-intent-critical)', backgroundColor: 'rgba(227, 92, 92, 0.05)' }}
>
<Box marginBottom={4}>
<Text variant="high" weight="medium" block marginBottom={2}>{title}</Text>
<Text size="sm" variant="low" block>
{description}
</Text>
</Box>
{children} {children}
</Box> </Surface>
</Card> </Box>
); );
} };

View File

@@ -1,30 +1,31 @@
import React from 'react'; import React from 'react';
import { Calendar } from 'lucide-react';
import { Box } from './primitives/Box'; import { Box } from './primitives/Box';
import { Stack } from './primitives/Stack';
import { Text } from './Text'; import { Text } from './Text';
import { Stack } from './primitives/Stack';
import { Calendar } from 'lucide-react';
import { Icon } from './Icon'; import { Icon } from './Icon';
interface DateHeaderProps { export interface DateHeaderProps {
label: string; date: string;
count?: number; showIcon?: boolean;
countLabel?: string;
} }
export function DateHeader({ label, count, countLabel = 'races' }: DateHeaderProps) { export const DateHeader = ({
date,
showIcon = true
}: DateHeaderProps) => {
return ( return (
<Stack direction="row" align="center" gap={3} px={2}> <Stack direction="row" align="center" gap={3} paddingX={2}>
<Box p={2} bg="bg-primary-blue/10" rounded="lg"> {showIcon && (
<Icon icon={Calendar} size={4} color="rgb(59, 130, 246)" /> <Box padding={2} bg="rgba(25, 140, 255, 0.1)" rounded="lg">
</Box> <Icon icon={Calendar} size={4} intent="primary" />
<Text weight="semibold" size="sm" color="text-white"> </Box>
{label}
</Text>
{count !== undefined && (
<Text size="xs" color="text-gray-500">
{count} {count === 1 ? countLabel.replace(/s$/, '') : countLabel}
</Text>
)} )}
<Box>
<Text weight="semibold" size="sm" variant="high">
{date}
</Text>
</Box>
</Stack> </Stack>
); );
} };

View File

@@ -1,71 +1,73 @@
import React, { ChangeEvent } from 'react';
import { Box } from './primitives/Box';
import { Text } from './Text';
import { Input } from './Input';
export interface DurationFieldProps {
import { Input } from '@/ui/Input';
interface DurationFieldProps {
label: string; label: string;
value: number | ''; value: number; // in minutes
onChange: (value: number | '') => void; onChange: (value: number) => void;
helperText?: string;
required?: boolean;
disabled?: boolean; disabled?: boolean;
unit?: 'minutes' | 'laps';
error?: string; error?: string;
} }
export function DurationField({ export const DurationField = ({
label, label,
value, value,
onChange, onChange,
helperText, disabled = false,
required, error
disabled, }: DurationFieldProps) => {
unit = 'minutes', const hours = Math.floor(value / 60);
error, const minutes = value % 60;
}: DurationFieldProps) {
const handleChange = (raw: string) => {
if (raw.trim() === '') {
onChange('');
return;
}
const parsed = parseInt(raw, 10); const handleHoursChange = (e: ChangeEvent<HTMLInputElement>) => {
if (Number.isNaN(parsed) || parsed <= 0) { const h = parseInt(e.target.value) || 0;
onChange(''); onChange(h * 60 + minutes);
return;
}
onChange(parsed);
}; };
const unitLabel = unit === 'laps' ? 'laps' : 'min'; const handleMinutesChange = (e: ChangeEvent<HTMLInputElement>) => {
const m = parseInt(e.target.value) || 0;
onChange(hours * 60 + m);
};
return ( return (
<div className="space-y-1"> <Box>
<label className="block text-sm font-medium text-gray-300"> <Text as="label" size="xs" weight="bold" variant="low" block marginBottom={1.5}>
{label} {label}
{required && <span className="text-warning-amber ml-1">*</span>} </Text>
</label> <Box display="flex" alignItems="center" gap={4}>
<div className="flex items-center gap-2"> <Box display="flex" alignItems="center" gap={2}>
<div className="flex-1">
<Input <Input
type="number" type="number"
value={value === '' ? '' : String(value)} value={hours}
onChange={(e) => handleChange(e.target.value)} onChange={handleHoursChange}
disabled={disabled} disabled={disabled}
min={1} min={0}
className="pr-16" style={{ width: '4rem' }}
variant={error ? 'error' : 'default'}
/> />
</div> <Text size="sm" variant="low">h</Text>
<span className="text-xs text-gray-400 -ml-14">{unitLabel}</span> </Box>
</div> <Box display="flex" alignItems="center" gap={2}>
{helperText && ( <Input
<p className="text-xs text-gray-500">{helperText}</p> type="number"
)} value={minutes}
onChange={handleMinutesChange}
disabled={disabled}
min={0}
max={59}
style={{ width: '4rem' }}
/>
<Text size="sm" variant="low">m</Text>
</Box>
</Box>
{error && ( {error && (
<p className="text-xs text-warning-amber mt-1">{error}</p> <Box marginTop={1}>
<Text size="xs" variant="critical">
{error}
</Text>
</Box>
)} )}
</div> </Box>
); );
} };

View File

@@ -1,55 +1,29 @@
import React from 'react'; 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; onRetry?: () => void;
onHomeClick: () => void; onGoHome?: () => void;
showRetry?: boolean;
homeLabel?: string;
} }
/** export const ErrorActionButtons = ({
* ErrorActionButtons onRetry,
* onGoHome
* Action buttons for error pages (Try Again, Go Home) }: ErrorActionButtonsProps) => {
* 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 (
<div className="flex items-center justify-center gap-3 pt-2">
<button
type="button"
onClick={onRetry}
className="inline-flex items-center justify-center rounded-md bg-primary-blue px-4 py-2 text-sm font-medium text-white hover:bg-primary-blue/80 transition-colors"
>
Try again
</button>
<button
type="button"
onClick={onHomeClick}
className="inline-flex items-center justify-center rounded-md bg-iron-gray px-4 py-2 text-sm font-medium text-white hover:bg-iron-gray/80 transition-colors"
>
Go home
</button>
</div>
);
}
return ( return (
<div className="pt-2"> <Box display="flex" alignItems="center" gap={4}>
<button {onRetry && (
type="button" <Button variant="primary" onClick={onRetry} icon={<RefreshCw size={16} />}>
onClick={onHomeClick} Retry
className="inline-flex items-center justify-center rounded-md bg-primary-blue px-4 py-2 text-sm font-medium text-white hover:bg-primary-blue/80 transition-colors" </Button>
> )}
{homeLabel} {onGoHome && (
</button> <Button variant="secondary" onClick={onGoHome} icon={<Home size={16} />}>
</div> Go Home
</Button>
)}
</Box>
); );
} };

View File

@@ -1,63 +1,45 @@
import React from 'react'; import React from 'react';
import { Box } from './primitives/Box'; import { Box } from './primitives/Box';
import { Stack } from './primitives/Stack';
import { Text } from './Text'; import { Text } from './Text';
import { Surface } from './primitives/Surface';
import { Icon } from './Icon'; 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; title?: string;
message: string; message: string;
variant?: 'error' | 'warning' | 'info' | 'success'; variant?: 'error' | 'warning' | 'info';
} }
export function ErrorBanner({ title, message, variant = 'error' }: ErrorBannerProps) { export const ErrorBanner = ({
const configs = { title,
error: { message,
bg: 'rgba(239, 68, 68, 0.1)', variant = 'error'
border: 'rgba(239, 68, 68, 0.2)', }: ErrorBannerProps) => {
text: 'rgb(248, 113, 113)', const intent = variant === 'error' ? 'critical' : variant === 'warning' ? 'warning' : 'primary';
icon: XCircle 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)';
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];
return ( return (
<Surface <Surface
variant="muted" variant="muted"
rounded="xl" rounded="lg"
border padding={4}
p={4} style={{ backgroundColor: color, border: `1px solid ${borderColor}` }}
backgroundColor={colors.bg}
borderColor={colors.border}
> >
<Stack direction="row" align="start" gap={3}> <Box display="flex" alignItems="start" gap={4}>
<Icon icon={colors.icon} size={5} color={colors.text} /> <Icon icon={AlertTriangle} size={5} intent={intent} />
<Box flex={1}> <Box>
{title && <Text weight="medium" color={colors.text} block mb={1}>{title}</Text>} {title && (
<Text size="sm" color={colors.text} opacity={0.9} block>{message}</Text> <Text weight="medium" variant="high" block marginBottom={1}>
{title}
</Text>
)}
<Text size="sm" variant="low">
{message}
</Text>
</Box> </Box>
</Stack> </Box>
</Surface> </Surface>
); );
} };

View File

@@ -1,29 +1,24 @@
import React, { ReactNode } from 'react'; import React, { ReactNode } from 'react';
import { Box } from './primitives/Box';
import { Surface } from './primitives/Surface';
interface ErrorPageContainerProps { export interface ErrorPageContainerProps {
children: ReactNode; children: ReactNode;
errorCode: string;
description: string;
} }
/** export const ErrorPageContainer = ({ children }: ErrorPageContainerProps) => {
* 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) {
return ( return (
<main className="min-h-screen flex items-center justify-center bg-deep-graphite text-white px-6"> <Box
<div className="max-w-md text-center space-y-4"> minHeight="100vh"
<h1 className="text-3xl font-semibold">{errorCode}</h1> display="flex"
<p className="text-sm text-gray-400">{description}</p> alignItems="center"
justifyContent="center"
padding={4}
bg="var(--ui-color-bg-base)"
>
<Surface variant="default" rounded="xl" padding={8} style={{ maxWidth: '32rem', width: '100%', border: '1px solid var(--ui-color-border-default)' }}>
{children} {children}
</div> </Surface>
</main> </Box>
); );
} };

View File

@@ -1,34 +1,29 @@
import { Button } from '@/ui/Button'; import React from 'react';
import { Card } from '@/ui/Card'; import { Box } from './primitives/Box';
import { Heading } from '@/ui/Heading'; import { Text } from './Text';
import { Box } from '@/ui/primitives/Box'; import { Icon } from './Icon';
import { Text } from '@/ui/Text'; import { MessageSquare } from 'lucide-react';
export function FeedEmptyState() { export interface FeedEmptyStateProps {
return ( message?: string;
<Card bg="bg-iron-gray/80" border={true} borderColor="border-charcoal-outline" className="border-dashed">
<Box textAlign="center" py={10}>
<Text size="3xl" block mb={3}>🏁</Text>
<Box mb={2}>
<Heading level={3}>
Your feed is warming up
</Heading>
</Box>
<Box maxWidth="md" mx="auto" mb={4}>
<Text size="sm" color="text-gray-400">
As leagues, teams, and friends start racing, this feed will show their latest results,
signups, and highlights.
</Text>
</Box>
<Button
as="a"
href="/leagues"
variant="secondary"
size="sm"
>
Explore leagues
</Button>
</Box>
</Card>
);
} }
export const FeedEmptyState = ({
message = 'No activity yet.'
}: FeedEmptyStateProps) => {
return (
<Box
display="flex"
flexDirection="col"
alignItems="center"
justifyContent="center"
paddingY={12}
textAlign="center"
>
<Box padding={4} rounded="full" bg="var(--ui-color-bg-surface-muted)" marginBottom={4}>
<Icon icon={MessageSquare} size={8} intent="low" />
</Box>
<Text variant="low">{message}</Text>
</Box>
);
};

View File

@@ -1,77 +1,46 @@
import React, { ReactNode } from 'react'; import React, { ReactNode } from 'react';
import { Box } from './primitives/Box'; import { Box } from './primitives/Box';
import { Text } from './Text'; import { Text } from './Text';
import { Image } from './Image'; import { Surface } from './primitives/Surface';
import { Avatar } from './Avatar';
interface FeedItemProps { export interface FeedItemProps {
actorName?: string; user: {
actorAvatarUrl?: string; name: string;
typeLabel: string; avatar?: string;
headline: string; };
body?: string; content: ReactNode;
timeAgo: string; timestamp: string;
cta?: ReactNode; actions?: ReactNode;
} }
export function FeedItem({ export const FeedItem = ({
actorName, user,
actorAvatarUrl, content,
typeLabel, timestamp,
headline, actions
body, }: FeedItemProps) => {
timeAgo,
cta,
}: FeedItemProps) {
return ( return (
<Box display="flex" gap={4}> <Surface variant="default" rounded="lg" padding={4} style={{ border: '1px solid var(--ui-color-border-default)' }}>
<Box flexShrink={0}> <Box display="flex" alignItems="start" gap={4}>
{actorAvatarUrl ? ( <Avatar src={user.avatar} alt={user.name} size="md" />
<Box width="10" height="10" rounded="full" overflow="hidden" bg="bg-charcoal-outline"> <Box flex={1}>
<Image <Box display="flex" alignItems="center" justifyContent="between" marginBottom={2}>
src={actorAvatarUrl} <Text weight="bold" variant="high">{user.name}</Text>
alt={actorName || ''} <Text size="xs" variant="low">{timestamp}</Text>
width={40}
height={40}
fullWidth
fullHeight
objectFit="cover"
/>
</Box> </Box>
) : ( <Box marginBottom={4}>
<Box {typeof content === 'string' ? (
width="10" <Text variant="med">{content}</Text>
height="10" ) : content}
display="flex"
center
rounded="full"
bg="bg-primary-blue/10"
border={true}
borderColor="border-primary-blue/40"
>
<Text size="xs" color="text-primary-blue" weight="semibold">
{typeLabel}
</Text>
</Box> </Box>
)} {actions && (
</Box> <Box display="flex" alignItems="center" gap={4} borderTop paddingTop={4}>
<Box flexGrow={1} minWidth="0"> {actions}
<Box display="flex" alignItems="start" justifyContent="between" gap={2}> </Box>
<Box> )}
<Text size="sm" color="text-white" block>{headline}</Text>
{body && (
<Text size="xs" color="text-gray-400" block mt={1}>{body}</Text>
)}
</Box>
<Text size="xs" color="text-gray-500" className="whitespace-nowrap" style={{ fontSize: '11px' }}>
{timeAgo}
</Text>
</Box> </Box>
{cta && (
<Box mt={3}>
{cta}
</Box>
)}
</Box> </Box>
</Box> </Surface>
); );
} };

View File

@@ -1,46 +1,61 @@
import React from 'react';
import { Box } from './primitives/Box'; import { Box } from './primitives/Box';
import { Button } from './Button'; 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; id: string;
label: string; label: string;
indicatorColor?: string; indicatorColor?: string;
} }
interface FilterGroupProps { export interface FilterGroupProps {
options: FilterOption[]; options: FilterOption[];
activeId: string; 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 ( return (
<Stack direction="row" align="center" gap={1} bg="bg-deep-graphite" p={1} rounded="lg"> <Box display="flex" flexDirection="col" gap={2}>
{options.map((option) => ( {label && (
<Button <Text size="xs" weight="bold" variant="low" uppercase letterSpacing="wider">
key={option.id} {label}
variant={activeId === option.id ? 'primary' : 'ghost'} </Text>
onClick={() => onSelect(option.id)} )}
size="sm" <Surface variant="muted" rounded="lg" padding={1} display="flex" gap={1}>
px={4} {options.map((option) => {
> const isActive = option.id === activeId;
{option.indicatorColor && ( return (
<Box <Button
as="span" key={option.id}
w="2" variant={isActive ? 'primary' : 'ghost'}
h="2" size="sm"
bg={option.indicatorColor} onClick={() => onChange(option.id)}
rounded="full" fullWidth
mr={2} >
animate={option.indicatorColor.includes('green') ? 'pulse' : 'none'} <Box display="flex" alignItems="center" gap={2}>
/> {option.indicatorColor && (
)} <Box
{option.label} width="0.5rem"
</Button> height="0.5rem"
))} rounded="full"
</Stack> style={{ backgroundColor: option.indicatorColor }}
/>
)}
{option.label}
</Box>
</Button>
);
})}
</Surface>
</Box>
); );
} };

View File

@@ -1,81 +1,54 @@
import { Link } from '@/ui/Link'; import React from 'react';
import { Box } from '@/ui/primitives/Box'; import { Box } from './primitives/Box';
import { Text } from '@/ui/Text'; import { Container } from './Container';
import { Text } from './Text';
import { Link } from './Link';
const discordUrl = process.env.NEXT_PUBLIC_DISCORD_URL || 'https://discord.gg/gridpilot'; export const Footer = () => {
const xUrl = process.env.NEXT_PUBLIC_X_URL || '#';
export function Footer() {
return ( return (
<Box as="footer" position="relative" bg="graphite-black" borderTop borderColor="border-gray/50"> <Box as="footer" bg="var(--ui-color-bg-surface)" borderTop paddingY={12}>
<Box position="absolute" top="0" left="0" right="0" h="px" bg="linear-gradient(to right, transparent, #198CFF, transparent)" opacity={0.3} /> <Container size="xl">
<Box display="grid" gridCols={{ base: 1, md: 4 }} gap={12}>
<Box maxWidth="7xl" mx="auto" px={{ base: 6, lg: 8 }} py={{ base: 12, md: 16 }}> <Box>
{/* Racing stripe accent */} <Text weight="bold" variant="high" marginBottom={4}>GridPilot</Text>
<Box <Text size="sm" variant="low">
display="flex" The ultimate companion for sim racers. Track your performance, manage your team, and compete in leagues.
gap={2} </Text>
mb={8} </Box>
justifyContent="center"
> <Box>
<Box w="12" h="1" bg="white" opacity={0.1} /> <Text weight="bold" variant="high" marginBottom={4}>Platform</Text>
<Box w="12" h="1" bg="primary-accent" /> <Box display="flex" flexDirection="col" gap={2}>
<Box w="12" h="1" bg="white" opacity={0.1} /> <Link href="/leagues" variant="secondary">Leagues</Link>
<Link href="/teams" variant="secondary">Teams</Link>
<Link href="/leaderboards" variant="secondary">Leaderboards</Link>
</Box>
</Box>
<Box>
<Text weight="bold" variant="high" marginBottom={4}>Support</Text>
<Box display="flex" flexDirection="col" gap={2}>
<Link href="/docs" variant="secondary">Documentation</Link>
<Link href="/status" variant="secondary">System Status</Link>
<Link href="/contact" variant="secondary">Contact Us</Link>
</Box>
</Box>
<Box>
<Text weight="bold" variant="high" marginBottom={4}>Legal</Text>
<Box display="flex" flexDirection="col" gap={2}>
<Link href="/privacy" variant="secondary">Privacy Policy</Link>
<Link href="/terms" variant="secondary">Terms of Service</Link>
</Box>
</Box>
</Box> </Box>
{/* Personal message */} <Box borderTop marginTop={12} paddingTop={8} textAlign="center">
<Box <Text size="xs" variant="low">
textAlign="center" © {new Date().getFullYear()} GridPilot. All rights reserved.
mb={12}
>
<Text size="sm" color="text-gray-300" block mb={2} weight="bold" className="tracking-wide">
🏁 Built by a sim racer, for sim racers
</Text>
<Text size="xs" color="text-gray-500" weight="normal" maxWidth="2xl" mx="auto" block leading="relaxed">
Just a fellow racer tired of spreadsheets and chaos. GridPilot is my passion project to make league racing actually fun again.
</Text> </Text>
</Box> </Box>
</Container>
{/* Community links */}
<Box
display="flex"
justifyContent="center"
gap={8}
mb={12}
>
<Link
href={discordUrl}
variant="ghost"
size="sm"
className="text-gray-400 hover:text-primary-accent transition-colors font-bold uppercase tracking-widest"
>
💬 Discord
</Link>
<Link
href={xUrl}
variant="ghost"
size="sm"
className="text-gray-400 hover:text-primary-accent transition-colors font-bold uppercase tracking-widest"
>
𝕏 Twitter
</Link>
</Box>
{/* Development status */}
<Box
textAlign="center"
pt={8}
borderTop
borderColor="border-gray/30"
>
<Text size="xs" color="text-gray-600" block mb={1} font="mono" uppercase letterSpacing="widest">
Early development Feedback welcome
</Text>
<Text size="xs" color="text-gray-700" block font="mono">
&copy; {new Date().getFullYear()} GridPilot
</Text>
</Box>
</Box>
</Box> </Box>
); );
} };

View File

@@ -1,45 +1,55 @@
import React, { ReactNode } from 'react';
import { Box } from './primitives/Box';
import React from 'react';
import { Icon } from './Icon';
import { Stack } from './primitives/Stack';
import { Text } from './Text'; import { Text } from './Text';
import { Icon } from './Icon';
import { LucideIcon } from 'lucide-react'; import { LucideIcon } from 'lucide-react';
interface FormFieldProps { export interface FormFieldProps {
label: string; label?: string;
icon?: LucideIcon;
children: React.ReactNode;
required?: boolean;
error?: string; error?: string;
hint?: string; hint?: string;
required?: boolean;
icon?: LucideIcon;
children: ReactNode;
} }
export function FormField({ export const FormField = ({
label, label,
error,
hint,
required,
icon, icon,
children, children
required = false, }: FormFieldProps) => {
error,
hint,
}: FormFieldProps) {
return ( return (
<Stack gap={2}> <Box marginBottom={4}>
<label className="block text-sm font-medium text-gray-300"> {label && (
<Stack direction="row" align="center" gap={2}> <Box display="flex" alignItems="center" gap={2} marginBottom={1.5}>
{icon && <Icon icon={icon} size={4} color="#6b7280" />} {icon && <Icon icon={icon} size={4} intent="low" />}
<Text size="sm" weight="medium" color="text-gray-300">{label}</Text> <Text size="sm" weight="medium" variant="high">
{required && <Text color="text-error-red">*</Text>} {label}
</Stack> </Text>
</label> {required && <Text variant="critical">*</Text>}
</Box>
)}
{children} {children}
{error && ( {error && (
<Text size="xs" color="text-error-red" block mt={1}>{error}</Text> <Box marginTop={1}>
<Text size="xs" variant="critical" block>
{error}
</Text>
</Box>
)} )}
{hint && !error && ( {hint && !error && (
<Text size="xs" color="text-gray-500" block mt={1}>{hint}</Text> <Box marginTop={1}>
<Text size="xs" variant="low" block>
{hint}
</Text>
</Box>
)} )}
</Stack> </Box>
); );
} };

View File

@@ -1,37 +1,33 @@
import React, { ReactNode } from 'react'; import React, { ReactNode } from 'react';
import { Stack } from './primitives/Stack'; import { Box } from './primitives/Box';
import { Text } from './Text'; import { Text } from './Text';
interface FormSectionProps { export interface FormSectionProps {
title: string;
description?: string;
children: ReactNode; children: ReactNode;
title?: string;
} }
/** export const FormSection = ({
* FormSection title,
* description,
* Groups related form fields with an optional title. children
*/ }: FormSectionProps) => {
export function FormSection({ children, title }: FormSectionProps) {
return ( return (
<Stack gap={4} fullWidth> <Box display="flex" flexDirection="col" gap={6}>
{title && ( <Box borderBottom paddingBottom={4}>
<Text <Text weight="bold" variant="high" size="lg" marginBottom={1} block>
size="xs"
weight="bold"
color="text-gray-500"
uppercase
letterSpacing="widest"
borderBottom
borderColor="border-border-gray"
pb={1}
>
{title} {title}
</Text> </Text>
)} {description && (
<Stack gap={4} fullWidth> <Text size="sm" variant="low">
{description}
</Text>
)}
</Box>
<Box display="flex" flexDirection="col" gap={4}>
{children} {children}
</Stack> </Box>
</Stack> </Box>
); );
} };

View File

@@ -1,41 +1,48 @@
import React from 'react'; 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 { Card } from './Card';
import { ProgressBar } from './ProgressBar'; import { Box } from './primitives/Box';
import { Text } from './Text';
interface GoalCardProps { export interface GoalCardProps {
title: string; title: string;
current: number;
target: number;
unit: string;
icon: string; icon: string;
goalLabel: string;
currentValue: number;
maxValue: number;
color?: string;
} }
export function GoalCard({ export const GoalCard = ({
title, title,
icon, current,
goalLabel, target,
currentValue, unit,
maxValue, icon
color = 'text-primary-blue', }: GoalCardProps) => {
}: GoalCardProps) { const percentage = Math.min(Math.max((current / target) * 100, 0), 100);
return ( return (
<Card bg="bg-charcoal-200/50" borderColor="border-primary-blue/30"> <Card variant="default">
<Box display="flex" alignItems="center" gap={3} mb={3}> <Box display="flex" alignItems="center" gap={3} marginBottom={3}>
<Text size="2xl">{icon}</Text> <Text size="2xl">{icon}</Text>
<Heading level={3}>{title}</Heading> <Box>
</Box> <Text weight="bold" variant="high">{title}</Text>
<Stack gap={2}> <Text size="sm" variant="low">{unit}</Text>
<Box display="flex" alignItems="center" justifyContent="between">
<Text size="sm" color="text-gray-400">{goalLabel}</Text>
<Text size="sm" className={color}>{currentValue}/{maxValue}</Text>
</Box> </Box>
<ProgressBar value={currentValue} max={maxValue} /> </Box>
</Stack>
<Box marginBottom={2}>
<Box display="flex" alignItems="center" justifyContent="between" marginBottom={1}>
<Text size="xs" variant="low">Progress</Text>
<Text size="xs" weight="bold" variant="high">{Math.round(percentage)}%</Text>
</Box>
<Box fullWidth height="0.5rem" bg="var(--ui-color-bg-surface-muted)" style={{ borderRadius: '9999px', overflow: 'hidden' }}>
<Box fullHeight bg="var(--ui-color-intent-primary)" style={{ width: `${percentage}%` }} />
</Box>
</Box>
<Text size="xs" variant="low">
{current} / {target} {unit}
</Text>
</Card> </Card>
); );
} };

View File

@@ -1,16 +1,38 @@
import React from 'react'; import React, { ReactNode } from 'react';
import { Container } from '@/ui/Container'; import { Box } from './primitives/Box';
import { Container } from './Container';
interface HeaderProps { export interface HeaderProps {
children: React.ReactNode; children: ReactNode;
actions?: ReactNode;
} }
export function Header({ children }: HeaderProps) { export const Header = ({
children,
actions
}: HeaderProps) => {
return ( return (
<header className="fixed top-0 left-0 right-0 z-50 bg-graphite-black/80 backdrop-blur-md border-b border-border-gray/50"> <Box
<Container> as="header"
{children} bg="var(--ui-color-bg-surface)"
borderBottom
paddingY={4}
position="sticky"
top={0}
zIndex={50}
>
<Container size="xl">
<Box display="flex" alignItems="center" justifyContent="between">
<Box display="flex" alignItems="center" gap={8}>
{children}
</Box>
{actions && (
<Box display="flex" alignItems="center" gap={4}>
{actions}
</Box>
)}
</Box>
</Container> </Container>
</header> </Box>
); );
} };

View File

@@ -1,93 +1,52 @@
import React, { ReactNode, ElementType } from 'react'; import React, { ReactNode, forwardRef } from 'react';
import { Stack } from './primitives/Stack';
import { Box, BoxProps, ResponsiveValue } from './primitives/Box'; import { Box, BoxProps, ResponsiveValue } from './primitives/Box';
interface ResponsiveFontSize { export interface HeadingProps extends BoxProps<any> {
base?: string;
sm?: string;
md?: string;
lg?: string;
xl?: string;
'2xl'?: string;
}
interface HeadingProps extends Omit<BoxProps<'h1'>, 'children' | 'as' | 'fontSize'> {
level: 1 | 2 | 3 | 4 | 5 | 6;
children: ReactNode; children: ReactNode;
icon?: ReactNode; level?: 1 | 2 | 3 | 4 | 5 | 6;
id?: string; weight?: 'normal' | 'medium' | 'semibold' | 'bold';
groupHoverColor?: string; align?: 'left' | 'center' | 'right';
truncate?: boolean; fontSize?: string | ResponsiveValue<string>;
uppercase?: boolean;
fontSize?: string | ResponsiveFontSize;
weight?: 'light' | 'normal' | 'medium' | 'semibold' | 'bold' | string;
letterSpacing?: string;
} }
export function Heading({ level, children, icon, groupHoverColor, truncate, uppercase, fontSize, weight, letterSpacing, ...props }: HeadingProps) { export const Heading = forwardRef<HTMLHeadingElement, HeadingProps>(({
const Tag = `h${level}` as ElementType; children,
level = 1,
weight = 'bold',
align = 'left',
fontSize,
...props
}, ref) => {
const Tag = `h${level}` as const;
const levelClasses = { const weightClasses = {
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<string, string> = {
light: 'font-light',
normal: 'font-normal', normal: 'font-normal',
medium: 'font-medium', medium: 'font-medium',
semibold: 'font-semibold', semibold: 'font-semibold',
bold: 'font-bold' bold: 'font-bold'
}; };
const getFontSizeClasses = (value: string | ResponsiveFontSize | undefined) => { const sizeClasses = {
if (value === undefined) return ''; 1: 'text-4xl md:text-5xl',
if (typeof value === 'object') { 2: 'text-3xl md:text-4xl',
const classes = []; 3: 'text-2xl md:text-3xl',
if (value.base) classes.push(`text-${value.base}`); 4: 'text-xl md:text-2xl',
if (value.sm) classes.push(`sm:text-${value.sm}`); 5: 'text-lg md:text-xl',
if (value.md) classes.push(`md:text-${value.md}`); 6: 'text-base md:text-lg'
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 content = icon ? (
<Stack direction="row" align="center" gap={2}>
{icon}
{children}
</Stack>
) : children;
const classes = [ const classes = [
levelClasses[level], 'text-[var(--ui-color-text-high)]',
getFontSizeClasses(fontSize), weightClasses[weight],
weight && weightClasses[weight as keyof typeof weightClasses] ? weightClasses[weight as keyof typeof weightClasses] : '', fontSize ? '' : sizeClasses[level],
letterSpacing ? `tracking-${letterSpacing}` : '', align === 'center' ? 'text-center' : (align === 'right' ? 'text-right' : 'text-left'),
uppercase ? 'uppercase' : '', ].join(' ');
groupHoverColor ? `group-hover:text-${groupHoverColor}` : '',
truncate ? 'truncate' : '',
props.className
].filter(Boolean).join(' ');
return ( return (
<Box <Box as={Tag} ref={ref} className={classes} fontSize={typeof fontSize === 'string' ? fontSize : undefined} {...props}>
as={Tag} {children}
{...props}
className={classes}
style={{
...(weight && !weightClasses[weight as keyof typeof weightClasses] ? { fontWeight: weight } : {}),
...(props.style || {})
}}
>
{content}
</Box> </Box>
); );
} });
Heading.displayName = 'Heading';

View File

@@ -1,28 +1,50 @@
import React from 'react';
import { Box } from './primitives/Box';
import { Text } from './Text';
import { Stack } from './primitives/Stack';
export interface HorizontalBarChartItem {
label: string;
interface BarChartProps { value: number;
data: { label: string; value: number; color: string }[]; color?: string;
maxValue: number;
} }
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 ( return (
<div className="space-y-3"> <Box display="flex" flexDirection="col" gap={4}>
{data.map((item) => ( {actualItems.map((item, index) => {
<div key={item.label}> const percentage = actualTotal > 0 ? (item.value / actualTotal) * 100 : 0;
<div className="flex justify-between text-sm mb-1"> return (
<span className="text-gray-400">{item.label}</span> <Box key={index}>
<span className="text-white font-medium">{item.value}</span> <Stack direction="row" justify="between" marginBottom={1}>
</div> <Text size="xs" variant="low" uppercase weight="bold">{item.label}</Text>
<div className="h-2 bg-charcoal-outline rounded-full overflow-hidden"> <Text size="xs" variant="high" weight="bold">{item.value}</Text>
<div </Stack>
className={`h-full rounded-full ${item.color} transition-all duration-500 ease-out`} <Box fullWidth height="0.5rem" bg="var(--ui-color-bg-surface-muted)" style={{ borderRadius: '9999px', overflow: 'hidden' }}>
style={{ width: `${Math.min((item.value / maxValue) * 100, 100)}%` }} <Box
/> fullHeight
</div> bg={item.color || 'var(--ui-color-intent-primary)'}
</div> style={{ width: `${percentage}%`, transition: 'width 0.3s ease-in-out' }}
))} />
</div> </Box>
</Box>
);
})}
</Box>
); );
} };

View File

@@ -1,46 +1,38 @@
import React from 'react'; import React from 'react';
import { Card } from './Card';
import { Box } from './primitives/Box'; import { Box } from './primitives/Box';
import { Stack } from './primitives/Stack';
import { Text } from './Text'; import { Text } from './Text';
import { Surface } from './primitives/Surface';
import { Icon } from './Icon'; import { Icon } from './Icon';
import { LucideIcon } from 'lucide-react'; import { LucideIcon } from 'lucide-react';
interface HorizontalStatCardProps { export interface HorizontalStatCardProps {
label: string; label: string;
value: string | number; value: string | number;
icon: LucideIcon; icon: LucideIcon;
iconColor?: string; intent?: 'primary' | 'success' | 'warning' | 'critical' | 'telemetry';
iconBgColor?: string;
} }
export function HorizontalStatCard({ export const HorizontalStatCard = ({
label, label,
value, value,
icon, icon,
iconColor = 'text-primary-blue', intent = 'primary'
iconBgColor = 'rgba(59, 130, 246, 0.1)', }: HorizontalStatCardProps) => {
}: HorizontalStatCardProps) {
return ( return (
<Surface variant="muted" rounded="xl" border p={4}> <Card variant="default">
<Stack direction="row" align="center" gap={4}> <Box display="flex" alignItems="center" gap={4}>
<Surface <Box padding={3} rounded="lg" bg="var(--ui-color-bg-surface-muted)">
variant="muted" <Icon icon={icon} size={5} intent={intent} />
rounded="lg" </Box>
p={3}
backgroundColor={iconBgColor}
>
<Icon icon={icon} size={5} color={iconColor} />
</Surface>
<Box> <Box>
<Text size="xs" color="text-gray-500" uppercase letterSpacing="wider" block> <Text size="xs" weight="bold" variant="low" uppercase>
{label} {label}
</Text> </Text>
<Text size="xl" weight="bold" color="text-white" block> <Text size="xl" weight="bold" variant="high" block marginTop={0.5}>
{value} {value}
</Text> </Text>
</Box> </Box>
</Stack> </Box>
</Surface> </Card>
); );
} };

View File

@@ -2,17 +2,21 @@ import React from 'react';
import { Box } from './primitives/Box'; import { Box } from './primitives/Box';
import { Text } from './Text'; import { Text } from './Text';
interface HorizontalStatItemProps { export interface HorizontalStatItemProps {
label: string; label: string;
value: string | number; 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 ( return (
<Box display="flex" alignItems="center" justifyContent="between" fullWidth> <Box display="flex" alignItems="center" justifyContent="between" paddingY={2}>
<Text size="sm" color="text-gray-400">{label}</Text> <Text size="sm" variant="low">{label}</Text>
<Text weight="semibold" color={color}>{value}</Text> <Text weight="semibold" variant={intent}>{value}</Text>
</Box> </Box>
); );
} };

View File

@@ -4,77 +4,71 @@ import { Box, BoxProps } from './primitives/Box';
export interface IconProps extends Omit<BoxProps<'div'>, 'children'> { export interface IconProps extends Omit<BoxProps<'div'>, 'children'> {
icon: LucideIcon | React.ReactNode; icon: LucideIcon | React.ReactNode;
size?: number | string; size?: 3 | 3.5 | 4 | 5 | 6 | 7 | 8 | 10 | 12 | 16 | 'full' | number;
color?: string; intent?: 'primary' | 'telemetry' | 'warning' | 'success' | 'critical' | 'high' | 'med' | 'low';
strokeWidth?: number; animate?: 'spin' | 'none';
animate?: string;
transition?: boolean;
groupHoverTextColor?: string;
groupHoverScale?: boolean;
} }
export function Icon({ export function Icon({
icon: IconProp, icon: IconProp,
size = 4, size = 4,
color, intent,
className = '', animate = 'none',
style,
animate,
transition,
groupHoverTextColor,
groupHoverScale,
...props ...props
}: IconProps) { }: IconProps) {
const sizeMap: Record<string | number, string> = { const sizeMap: Record<string | number, string> = {
3: 'w-3 h-3', 3: '0.75rem',
3.5: 'w-3.5 h-3.5', 3.5: '0.875rem',
4: 'w-4 h-4', 4: '1rem',
5: 'w-5 h-5', 5: '1.25rem',
6: 'w-6 h-6', 6: '1.5rem',
7: 'w-7 h-7', 7: '1.75rem',
8: 'w-8 h-8', 8: '2rem',
10: 'w-10 h-10', 10: '2.5rem',
12: 'w-12 h-12', 12: '3rem',
16: 'w-16 h-16', 16: '4rem',
'full': 'w-full h-full' 'full': '100%'
}; };
const sizeClass = sizeMap[size] || 'w-4 h-4'; const dimension = typeof size === 'string' ? sizeMap[size] : (sizeMap[size] || `${size * 0.25}rem`);
// 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 classes = [ const intentColorMap: Record<string, string> = {
sizeClass, primary: 'var(--ui-color-intent-primary)',
animate === 'spin' ? 'animate-spin' : '', telemetry: 'var(--ui-color-intent-telemetry)',
transition ? 'transition-all duration-150' : '', warning: 'var(--ui-color-intent-warning)',
groupHoverTextColor ? `group-hover:text-${groupHoverTextColor}` : '', success: 'var(--ui-color-intent-success)',
groupHoverScale ? 'group-hover:scale-110 transition-transform' : '', critical: 'var(--ui-color-intent-critical)',
className high: 'var(--ui-color-text-high)',
].filter(Boolean).join(' '); 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 = () => { const renderIcon = () => {
if (!IconProp) return null; if (!IconProp) return null;
if (typeof IconProp === 'function' || (typeof IconProp === 'object' && 'render' in IconProp)) { if (typeof IconProp === 'function' || (typeof IconProp === 'object' && 'render' in IconProp)) {
const LucideIconComponent = IconProp as LucideIcon; const LucideIconComponent = IconProp as LucideIcon;
return <LucideIconComponent size="100%" strokeWidth={props.strokeWidth} />; return <LucideIconComponent size="100%" />;
} }
return IconProp; return IconProp;
}; };
return ( return (
<Box <Box
className={classes}
style={combinedStyle}
color={boxColor}
display="inline-flex" display="inline-flex"
alignItems="center" alignItems="center"
justifyContent="center" justifyContent="center"
style={style}
{...props} {...props}
> >
{renderIcon()} <div className={animate === 'spin' ? 'animate-spin w-full h-full flex items-center justify-center' : 'w-full h-full flex items-center justify-center'}>
{renderIcon()}
</div>
</Box> </Box>
); );
} }

View File

@@ -1,54 +1,31 @@
import React from 'react'; import React, { MouseEventHandler } from 'react';
import { LucideIcon } from 'lucide-react'; import { Button, ButtonProps } from './Button';
import { Button } from './Button';
import { Icon } from './Icon'; import { Icon } from './Icon';
interface IconButtonProps { export interface IconButtonProps extends Omit<ButtonProps, 'children' | 'icon'> {
icon: LucideIcon; icon: any;
onClick?: React.MouseEventHandler<HTMLButtonElement>;
variant?: 'primary' | 'secondary' | 'danger' | 'ghost';
size?: 'sm' | 'md' | 'lg';
title?: string; title?: string;
disabled?: boolean;
color?: string;
className?: string;
backgroundColor?: string;
} }
export function IconButton({ export const IconButton = ({
icon, icon,
onClick,
variant = 'secondary',
size = 'md',
title, title,
disabled, size = 'md',
color, ...props
className = '', }: IconButtonProps) => {
backgroundColor, const iconSizeMap = {
}: IconButtonProps) { sm: 3,
const sizeMap = { md: 4,
sm: { w: '8', h: '8', icon: 4 }, lg: 5
md: { w: '10', h: '10', icon: 5 }, } as const;
lg: { w: '12', h: '12', icon: 6 },
};
return ( return (
<Button <Button
variant={variant} size={size}
onClick={onClick} {...props}
title={title}
disabled={disabled}
w={sizeMap[size].w}
h={sizeMap[size].h}
p={0}
rounded="full"
display="flex"
center
minHeight="0"
className={className}
backgroundColor={backgroundColor}
> >
<Icon icon={icon} size={sizeMap[size].icon} color={color} /> <Icon icon={icon} size={iconSizeMap[size]} />
{title && <span className="sr-only">{title}</span>}
</Button> </Button>
); );
} };

View File

@@ -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<HTMLImageElement> { export interface ImageProps extends Omit<BoxProps<'img'>, 'children'> {
src: string; src: string;
alt: string; alt: string;
width?: number;
height?: number;
className?: string;
fallbackSrc?: string; fallbackSrc?: string;
objectFit?: 'cover' | 'contain' | 'fill' | 'none' | 'scale-down'; fallbackComponent?: React.ReactNode;
fill?: boolean;
fullWidth?: boolean;
fullHeight?: boolean;
} }
export function Image({ export const Image = ({
src, src,
alt, alt,
width, fallbackSrc,
height, fallbackComponent,
className = '',
fallbackSrc,
objectFit,
fill,
fullWidth,
fullHeight,
...props ...props
}: ImageProps) { }: ImageProps) => {
const classes = [ const [error, setError] = useState(false);
objectFit ? `object-${objectFit}` : '',
fill ? 'absolute inset-0 w-full h-full' : '', if (error) {
fullWidth ? 'w-full' : '', if (fallbackComponent) return <>{fallbackComponent}</>;
fullHeight ? 'h-full' : '', if (fallbackSrc) return <Box as="img" src={fallbackSrc} alt={alt} {...props} />;
className return <ImagePlaceholder />;
].filter(Boolean).join(' '); }
return ( return (
// eslint-disable-next-line @next/next/no-img-element <Box
<img as="img"
src={src} src={src}
alt={alt} alt={alt}
width={width} onError={() => setError(true)}
height={height} {...props}
className={classes}
onError={(e) => {
if (fallbackSrc) {
(e.target as HTMLImageElement).src = fallbackSrc;
}
}}
{...props}
/> />
); );
} };

View File

@@ -1,86 +1,39 @@
import React from 'react'; import React from 'react';
import { Image as ImageIcon, AlertCircle, Loader2 } from 'lucide-react';
import { Box } from './primitives/Box'; import { Box } from './primitives/Box';
import { Icon } from './Icon'; import { Icon } from './Icon';
import { Text } from './Text'; import { Image as ImageIcon } from 'lucide-react';
export interface ImagePlaceholderProps { export interface ImagePlaceholderProps {
size?: number | string; width?: string | number;
height?: string | number;
animate?: 'pulse' | 'none' | 'spin';
aspectRatio?: string; aspectRatio?: string;
variant?: 'default' | 'error' | 'loading';
message?: string;
className?: string;
rounded?: 'none' | 'sm' | 'md' | 'lg' | 'xl' | '2xl' | 'full';
} }
export function ImagePlaceholder({ export const ImagePlaceholder = ({
size = 'full', width = '100%',
aspectRatio = '1/1', height = '100%',
variant = 'default', animate = 'pulse',
message, aspectRatio
className = '', }: ImagePlaceholderProps) => {
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];
return ( return (
<Box <Box
display="flex" width={width}
flexDirection="col" height={height}
alignItems="center" aspectRatio={aspectRatio}
justifyContent="center" display="flex"
w={typeof size === 'string' ? size : undefined} alignItems="center"
h={typeof size === 'string' ? size : undefined} justifyContent="center"
style={typeof size === 'number' ? { width: size, height: size } : { aspectRatio }} bg="var(--ui-color-bg-surface-muted)"
bg={bg} style={{ borderRadius: 'var(--ui-radius-md)' }}
border
borderColor={borderColor}
rounded={rounded}
className={`overflow-hidden ${className}`}
gap={2}
p={4}
> >
<Icon <Icon
icon={icon} icon={ImageIcon}
size={6} size={8}
color={color} intent="low"
animate={animate} animate={animate === 'spin' ? 'spin' : 'none'}
className={animate === 'pulse' ? 'animate-pulse' : ''}
/> />
{message && (
<Text
size="xs"
color={color}
weight="medium"
align="center"
className="max-w-[80%]"
>
{message}
</Text>
)}
</Box> </Box>
); );
} };

View File

@@ -1,77 +1,72 @@
import React from 'react'; import React, { ReactNode } from 'react';
import { Box } from './primitives/Box'; import { Box } from './primitives/Box';
import { Stack } from './primitives/Stack';
import { Text } from './Text'; import { Text } from './Text';
import { Surface } from './primitives/Surface';
import { Icon } from './Icon'; 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; 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) { export const InfoBanner = ({
const configs = { children,
variant,
type,
title
}: InfoBannerProps) => {
const activeVariant = variant || type || 'info';
const config = {
info: { info: {
bg: 'rgba(59, 130, 246, 0.1)', icon: Info,
border: 'rgba(59, 130, 246, 0.2)', intent: 'primary' as const,
iconColor: 'rgb(96, 165, 250)', bg: 'rgba(25, 140, 255, 0.05)',
icon: Info border: 'rgba(25, 140, 255, 0.2)',
}, },
warning: { warning: {
bg: 'rgba(245, 158, 11, 0.1)', icon: AlertTriangle,
border: 'rgba(245, 158, 11, 0.2)', intent: 'warning' as const,
iconColor: 'rgb(251, 191, 36)', bg: 'rgba(255, 190, 77, 0.05)',
icon: AlertTriangle border: 'rgba(255, 190, 77, 0.2)',
},
error: {
bg: 'rgba(239, 68, 68, 0.1)',
border: 'rgba(239, 68, 68, 0.2)',
iconColor: 'rgb(248, 113, 113)',
icon: AlertCircle
}, },
success: { success: {
bg: 'rgba(16, 185, 129, 0.1)', icon: CheckCircle,
border: 'rgba(16, 185, 129, 0.2)', intent: 'success' as const,
iconColor: 'rgb(52, 211, 153)', bg: 'rgba(111, 227, 122, 0.05)',
icon: CheckCircle border: 'rgba(111, 227, 122, 0.2)',
} },
}; critical: {
icon: XCircle,
const activeVariant = type || variant; intent: 'critical' as const,
const config = configs[activeVariant as keyof typeof configs] || configs.info; bg: 'rgba(227, 92, 92, 0.05)',
const BannerIcon = icon || config.icon; border: 'rgba(227, 92, 92, 0.2)',
},
}[activeVariant];
return ( return (
<Surface <Surface
variant="muted" variant="muted"
rounded="xl" rounded="lg"
border padding={4}
p={4} style={{ backgroundColor: config.bg, border: `1px solid ${config.border}` }}
backgroundColor={config.bg}
borderColor={config.border}
> >
<Stack direction="row" align="start" gap={3}> <Box display="flex" alignItems="start" gap={3}>
<Icon icon={BannerIcon} size={5} color={config.iconColor} /> <Icon icon={config.icon} size={5} intent={config.intent} />
<Box flex={1}> <Box flex={1}>
{title && ( {title && (
<Text weight="medium" color="text-white" block mb={1}> <Text size="sm" weight="bold" variant="high" marginBottom={1} block>
{title} {title}
</Text> </Text>
)} )}
{message && ( <Text size="sm" variant="high">
<Text size="sm" color="text-gray-300" block> {children}
{message} </Text>
</Text>
)}
{children}
</Box> </Box>
</Stack> </Box>
</Surface> </Surface>
); );
} };

View File

@@ -1,66 +1,72 @@
import React from 'react'; import React from 'react';
import { Box } from './primitives/Box'; import { Box } from './primitives/Box';
import { Stack } from './primitives/Stack';
import { Text } from './Text'; import { Text } from './Text';
import { Surface } from './primitives/Surface';
import { Icon } from './Icon'; import { Icon } from './Icon';
import { LucideIcon } from 'lucide-react'; import { LucideIcon } from 'lucide-react';
import { Surface } from './primitives/Surface';
interface InfoBoxProps { export interface InfoBoxProps {
title: string; title: string;
description: string; description: string;
icon: LucideIcon; 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) { export const InfoBox = ({
const configs = { title,
info: { description,
bg: 'rgba(59, 130, 246, 0.1)', icon,
border: 'rgba(59, 130, 246, 0.2)', intent = 'primary',
icon: 'rgb(96, 165, 250)', variant
text: 'text-white' }: InfoBoxProps) => {
}, const activeIntent = (variant || intent) as any;
warning: {
bg: 'rgba(245, 158, 11, 0.1)', const configMap: any = {
border: 'rgba(245, 158, 11, 0.2)', primary: {
icon: 'rgb(251, 191, 36)', bg: 'rgba(25, 140, 255, 0.05)',
text: 'text-white' border: 'rgba(25, 140, 255, 0.2)',
},
error: {
bg: 'rgba(239, 68, 68, 0.1)',
border: 'rgba(239, 68, 68, 0.2)',
icon: 'rgb(248, 113, 113)',
text: 'text-white'
}, },
success: { success: {
bg: 'rgba(16, 185, 129, 0.1)', bg: 'rgba(111, 227, 122, 0.05)',
border: 'rgba(16, 185, 129, 0.2)', border: 'rgba(111, 227, 122, 0.2)',
icon: 'rgb(52, 211, 153)', },
text: 'text-white' 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 ( return (
<Surface <Surface
variant="muted" variant="muted"
rounded="xl" rounded="xl"
border padding={4}
p={4} style={{ backgroundColor: config.bg, border: `1px solid ${config.border}` }}
backgroundColor={colors.bg}
borderColor={colors.border}
> >
<Stack direction="row" align="start" gap={3}> <Box display="flex" alignItems="start" gap={4}>
<Surface variant="muted" rounded="lg" p={2} bg="bg-white/5"> <Surface variant="muted" rounded="lg" padding={2} style={{ backgroundColor: 'rgba(255,255,255,0.05)' }}>
<Icon icon={icon} size={5} color={colors.icon} /> <Icon icon={icon} size={5} intent={activeIntent} />
</Surface> </Surface>
<Box> <Box>
<Text weight="medium" color={colors.text} block>{title}</Text> <Text weight="medium" variant="high" block>
<Text size="sm" color="text-gray-400" block mt={1}>{description}</Text> {title}
</Text>
<Text size="sm" variant="low" block marginTop={1}>
{description}
</Text>
</Box> </Box>
</Stack> </Box>
</Surface> </Surface>
); );
} };

View File

@@ -4,33 +4,32 @@ import { Text } from './Text';
import { Icon } from './Icon'; import { Icon } from './Icon';
import { LucideIcon } from 'lucide-react'; import { LucideIcon } from 'lucide-react';
interface InfoItemProps { export interface InfoItemProps {
icon: LucideIcon;
label: string; label: string;
value: string | number; 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 ( return (
<Box display="flex" alignItems="start" gap={2.5}> <Box display="flex" alignItems="center" gap={3}>
<Box <Box padding={2} rounded="lg" bg="var(--ui-color-bg-surface-muted)">
display="flex" <Icon icon={icon} size={4} intent={intent as any} />
h="7"
w="7"
alignItems="center"
justifyContent="center"
rounded="lg"
bg="bg-iron-gray/60"
flexShrink={0}
>
<Icon icon={icon} size={3.5} color="text-gray-500" />
</Box> </Box>
<Box flexGrow={1} minWidth="0"> <Box>
<Text size="xs" color="text-gray-500" block mb={0.5} style={{ fontSize: '10px' }}>{label}</Text> <Text size="xs" variant="low" block marginBottom={0.5} style={{ fontSize: '10px' }}>
<Text size="xs" weight="medium" color="text-gray-300" block className="truncate"> {label}
</Text>
<Text size="xs" weight="medium" variant="high" block className="truncate">
{value} {value}
</Text> </Text>
</Box> </Box>
</Box> </Box>
); );
} };

View File

@@ -1,59 +1,70 @@
import React, { forwardRef, ReactNode } from 'react'; import React, { forwardRef, InputHTMLAttributes } from 'react';
import { Box } from './primitives/Box'; import { Box } from './primitives/Box';
import { Stack } from './primitives/Stack';
import { Text } from './Text'; import { Text } from './Text';
interface InputProps extends React.InputHTMLAttributes<HTMLInputElement> { export interface InputProps extends Omit<InputHTMLAttributes<HTMLInputElement>, 'size'> {
label?: string; label?: string;
icon?: ReactNode; error?: string;
errorMessage?: string; hint?: string;
variant?: 'default' | 'error'; fullWidth?: boolean;
size?: 'sm' | 'md' | 'lg';
} }
export const Input = forwardRef<HTMLInputElement, InputProps>( export const Input = forwardRef<HTMLInputElement, InputProps>(({
({ label, icon, errorMessage, variant = 'default', className = '', ...props }, ref) => { label,
const isError = variant === 'error' || !!errorMessage; error,
hint,
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'; fullWidth = false,
const variantClasses = isError size = 'md',
? 'border-warning-amber focus:border-warning-amber focus:ring-1 focus:ring-warning-amber' ...props
: 'border-charcoal-outline focus:border-primary-blue focus:ring-1 focus:ring-primary-blue'; }, ref) => {
const sizeClasses = {
const classes = `${baseClasses} ${variantClasses} ${icon ? 'pl-11' : ''} ${className}`; sm: 'px-3 py-1.5 text-xs',
md: 'px-4 py-2 text-sm',
lg: 'px-4 py-3 text-base'
};
return ( 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';
<Stack gap={1.5} fullWidth> const errorClasses = error ? 'border-[var(--ui-color-intent-critical)]' : '';
{label && ( const widthClasses = fullWidth ? 'w-full' : '';
<Text as="label" size="xs" weight="bold" color="text-gray-500" uppercase letterSpacing="wider">
const classes = [
baseClasses,
sizeClasses[size],
errorClasses,
widthClasses,
].filter(Boolean).join(' ');
return (
<Box width={fullWidth ? '100%' : undefined}>
{label && (
<Box marginBottom={1.5}>
<Text as="label" size="xs" weight="bold" variant="low">
{label} {label}
</Text> </Text>
)}
<Box fullWidth position="relative">
{icon && (
<Box
position="absolute"
left={0}
top="50%"
translateY="-50%"
zIndex={10}
w="11"
display="flex"
center
color="text-gray-500"
>
{icon}
</Box>
)}
<input ref={ref} className={classes} {...props} />
{errorMessage && (
<Text size="xs" color="text-warning-amber" mt={1}>
{errorMessage}
</Text>
)}
</Box> </Box>
</Stack> )}
); <input
} ref={ref}
); className={classes}
{...props}
/>
{error && (
<Box marginTop={1}>
<Text size="xs" variant="critical">
{error}
</Text>
</Box>
)}
{hint && !error && (
<Box marginTop={1}>
<Text size="xs" variant="low">
{hint}
</Text>
</Box>
)}
</Box>
);
});
Input.displayName = 'Input'; Input.displayName = 'Input';

View File

@@ -1,45 +1,38 @@
import { Box } from '@/ui/primitives/Box'; import React from 'react';
import { Stack } from '@/ui/primitives/Stack'; import { Box } from './primitives/Box';
import { Surface } from '@/ui/primitives/Surface'; import { Text } from './Text';
import { Text } from '@/ui/Text'; import { Surface } from './primitives/Surface';
import { Icon } from './Icon';
import { LucideIcon } from 'lucide-react';
export function FeatureItem({ text }: { text: string }) { export interface LandingItemProps {
return ( title: string;
<Surface variant="muted" rounded="none" border padding={4} bg="panel-gray/10" borderColor="border-gray/10" className="group hover:border-primary-accent/20 transition-colors"> description: string;
<Stack direction="row" align="center" gap={4}> icon: LucideIcon;
<Box w="0.5" h="3" bg="primary-accent" className="group-hover:h-5 transition-all" /> intent?: 'primary' | 'success' | 'warning' | 'critical' | 'telemetry';
<Text color="text-gray-500" leading="relaxed" weight="normal" size="sm" className="tracking-wide group-hover:text-gray-300 transition-colors">
{text}
</Text>
</Stack>
</Surface>
);
} }
export function ResultItem({ text, color }: { text: string, color: string }) { export const LandingItem = ({
title,
description,
icon,
intent = 'primary'
}: LandingItemProps) => {
return ( return (
<Surface variant="muted" rounded="none" border padding={4} bg="panel-gray/10" borderColor="border-gray/10" className="group hover:border-primary-accent/20 transition-colors"> <Surface variant="muted" rounded="xl" padding={6} style={{ border: '1px solid var(--ui-color-border-default)' }}>
<Stack direction="row" align="center" gap={4}> <Box display="flex" flexDirection="col" gap={4}>
<Box w="0.5" h="3" style={{ backgroundColor: color }} className="group-hover:h-5 transition-all" /> <Box padding={3} rounded="lg" bg="var(--ui-color-bg-surface-muted)" width="fit-content">
<Text color="text-gray-500" leading="relaxed" weight="normal" size="sm" className="tracking-wide group-hover:text-gray-300 transition-colors"> <Icon icon={icon} size={6} intent={intent} />
{text}
</Text>
</Stack>
</Surface>
);
}
export function StepItem({ step, text }: { step: number, text: string }) {
return (
<Surface variant="muted" rounded="none" border padding={4} bg="panel-gray/10" borderColor="border-gray/10" className="group hover:border-primary-accent/20 transition-colors">
<Stack direction="row" align="center" gap={4}>
<Box w="6" h="6" display="flex" center border borderColor="border-gray/20" className="group-hover:border-primary-accent/30 transition-colors">
<Text weight="bold" size="xs" color="text-primary-accent" font="mono" opacity={0.7}>{step.toString().padStart(2, '0')}</Text>
</Box> </Box>
<Text color="text-gray-500" leading="relaxed" weight="normal" size="sm" className="tracking-wide group-hover:text-gray-300 transition-colors"> <Box>
{text} <Text weight="bold" variant="high" size="lg" marginBottom={2} block>
</Text> {title}
</Stack> </Text>
<Text variant="low" leading="relaxed">
{description}
</Text>
</Box>
</Box>
</Surface> </Surface>
); );
} };

View File

@@ -1,67 +1,44 @@
import { ReactNode } from 'react'; import React, { ReactNode } from 'react';
import { Box } from './primitives/Box'; import { Box } from './primitives/Box';
import { Grid } from './primitives/Grid';
import { Stack } from './primitives/Stack';
/** export interface LayoutProps {
* 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 {
children: ReactNode; children: ReactNode;
padding?: string; header?: ReactNode;
gap?: string; footer?: ReactNode;
grid?: boolean; sidebar?: ReactNode;
gridCols?: 1 | 2 | 3 | 4;
flex?: boolean;
flexCol?: boolean;
items?: 'start' | 'center' | 'end' | 'stretch';
justify?: 'start' | 'center' | 'end' | 'between' | 'around';
} }
export function Layout({ export const Layout = ({
children, children,
padding = 'p-6', header,
gap = 'gap-4', footer,
grid = false, sidebar
gridCols = 1, }: LayoutProps) => {
flex = false,
flexCol = false,
items = 'start',
justify = 'start'
}: LayoutProps) {
if (grid) {
return (
<Grid
cols={gridCols as any}
className={`${padding} ${gap}`}
>
{children}
</Grid>
);
}
if (flex) {
return (
<Stack
direction={flexCol ? 'col' : 'row'}
align={items}
justify={justify}
className={`${padding} ${gap}`}
>
{children}
</Stack>
);
}
return ( return (
<Box className={`${padding} ${gap}`}> <Box display="flex" flexDirection="col" minHeight="100vh" bg="var(--ui-color-bg-base)">
{children} {header && (
<Box as="header" position="sticky" top="0" zIndex={50}>
{header}
</Box>
)}
<Box display="flex" flex={1}>
{sidebar && (
<Box as="aside" width="16rem" display={{ base: 'none', lg: 'block' }}>
{sidebar}
</Box>
)}
<Box as="main" flex={1}>
{children}
</Box>
</Box>
{footer && (
<Box as="footer">
{footer}
</Box>
)}
</Box> </Box>
); );
} };

View File

@@ -1,16 +1,31 @@
import React, { ReactNode } from 'react'; import React, { ReactNode } from 'react';
import { Box } from './primitives/Box'; import { Box } from './primitives/Box';
import { Surface } from './primitives/Surface';
interface LeaderboardListProps { export interface LeaderboardListProps {
children: ReactNode; children: ReactNode;
} }
export function LeaderboardList({ children }: LeaderboardListProps) { export const LeaderboardList = ({ children }: LeaderboardListProps) => {
return ( return (
<Box rounded="xl" bg="bg-iron-gray/30" border={true} borderColor="border-charcoal-outline" overflow="hidden"> <Surface variant="muted" rounded="xl" style={{ border: '1px solid var(--ui-color-border-default)', overflow: 'hidden' }}>
<div className="divide-y divide-charcoal-outline/50"> <Box display="flex" flexDirection="col">
{children} {children}
</div> </Box>
</Surface>
);
};
export const LeaderboardListItem = ({ children, onClick }: { children: ReactNode, onClick?: () => void }) => {
return (
<Box
padding={4}
borderBottom
onClick={onClick}
style={{ cursor: onClick ? 'pointer' : 'default' }}
className={onClick ? 'hover:bg-white/5 transition-colors' : ''}
>
{children}
</Box> </Box>
); );
} };

View File

@@ -1,70 +1,58 @@
import { Award, ChevronRight, LucideIcon } from 'lucide-react'; import React, { ReactNode } from 'react';
import { ReactNode } from 'react';
import { Box } from './primitives/Box'; 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 { Text } from './Text';
import { Surface } from './primitives/Surface';
import { Icon } from './Icon';
import { LucideIcon } from 'lucide-react';
interface LeaderboardPreviewShellProps { export interface LeaderboardPreviewShellProps {
title: string; title: string;
subtitle: string; subtitle?: string;
onViewFull: () => void; icon: LucideIcon;
intent?: 'primary' | 'success' | 'warning' | 'critical' | 'telemetry';
children: ReactNode; children: ReactNode;
icon?: LucideIcon; footer?: ReactNode;
iconColor?: string;
iconBgGradient?: string;
viewFullLabel?: string;
} }
export function LeaderboardPreviewShell({ export const LeaderboardPreviewShell = ({
title, title,
subtitle, subtitle,
onViewFull, icon,
intent = 'primary',
children, children,
icon = Award, footer
iconColor = "#facc15", }: LeaderboardPreviewShellProps) => {
iconBgGradient = 'linear-gradient(to bottom right, rgba(250, 204, 21, 0.2), rgba(217, 119, 6, 0.1))',
viewFullLabel = "View Full Leaderboard",
}: LeaderboardPreviewShellProps) {
return ( return (
<Box mb={12}> <Surface variant="default" rounded="xl" style={{ border: '1px solid var(--ui-color-border-default)', overflow: 'hidden' }}>
{/* Header */} <Box padding={6} borderBottom>
<Stack direction="row" align="center" justify="between" mb={4}> <Box display="flex" alignItems="center" justifyContent="between" marginBottom={4}>
<Stack direction="row" align="center" gap={3}> <Box display="flex" alignItems="center" gap={4}>
<Box <Box padding={2} rounded="lg" bg="var(--ui-color-bg-surface-muted)">
display="flex" <Icon icon={icon} size={5} intent={intent} />
h="11" </Box>
w="11" <Box>
alignItems="center" <Text size="lg" weight="bold" variant="high" block>
justifyContent="center" {title}
rounded="xl" </Text>
style={{ background: iconBgGradient, border: `1px solid ${iconColor}4D` }} {subtitle && (
> <Text size="sm" variant="low">
<Icon icon={icon} size={5} color={iconColor} /> {subtitle}
</Text>
)}
</Box>
</Box> </Box>
<Box> </Box>
<Heading level={2}>{title}</Heading>
<Text size="sm" color="text-gray-500">{subtitle}</Text>
</Box>
</Stack>
<Button
variant="secondary"
onClick={onViewFull}
icon={<Icon icon={ChevronRight} size={4} />}
>
{viewFullLabel}
</Button>
</Stack>
{/* Compact Leaderboard */}
<Box rounded="xl" bg="bg-iron-gray/30" border borderColor="border-charcoal-outline/80" overflow="hidden">
<Stack gap={0}>
{children}
</Stack>
</Box> </Box>
</Box>
<Box>
{children}
</Box>
{footer && (
<Box padding={4} borderTop bg="rgba(255,255,255,0.02)">
{footer}
</Box>
)}
</Surface>
); );
} };

View File

@@ -1,49 +1,17 @@
import React from 'react'; import React, { ReactNode } from 'react';
import { Box } from './primitives/Box'; import { Box } from './primitives/Box';
import { Surface } from './primitives/Surface';
interface LeaderboardTableShellProps { export interface LeaderboardTableShellProps {
columns: { children: ReactNode;
key: string;
label: string;
align?: 'left' | 'center' | 'right';
width?: string;
}[];
children: React.ReactNode;
className?: string;
} }
export function LeaderboardTableShell({ columns, children, className = '' }: LeaderboardTableShellProps) { export const LeaderboardTableShell = ({ children }: LeaderboardTableShellProps) => {
return ( return (
<Box <Surface variant="default" rounded="xl" style={{ border: '1px solid var(--ui-color-border-default)', overflow: 'hidden' }}>
rounded="xl" <Box>
bg="bg-iron-gray/30" {children}
border </Box>
borderColor="border-charcoal-outline" </Surface>
overflow="hidden"
className={className}
>
<div className="overflow-x-auto">
<table className="w-full text-left border-collapse">
<thead>
<tr className="border-b border-charcoal-outline/50 bg-graphite-black/50">
{columns.map((col) => (
<th
key={col.key}
className={`px-4 py-3 text-[10px] uppercase tracking-widest font-bold text-gray-500 ${
col.align === 'center' ? 'text-center' : col.align === 'right' ? 'text-right' : 'text-left'
}`}
style={col.width ? { width: col.width } : {}}
>
{col.label}
</th>
))}
</tr>
</thead>
<tbody className="divide-y divide-charcoal-outline/30">
{children}
</tbody>
</table>
</div>
</Box>
); );
} };

View File

@@ -1,86 +1,66 @@
import React, { ReactNode } from 'react'; import React, { ReactNode, forwardRef, AnchorHTMLAttributes } from 'react';
import { Box, BoxProps } from './primitives/Box';
export interface LinkProps extends Omit<BoxProps<'a'>, 'children' | 'onClick'> { export interface LinkProps extends AnchorHTMLAttributes<HTMLAnchorElement> {
href: string;
children: ReactNode; children: ReactNode;
variant?: 'primary' | 'secondary' | 'ghost'; variant?: 'primary' | 'secondary' | 'ghost' | 'inherit';
size?: 'xs' | 'sm' | 'md' | 'lg'; underline?: 'always' | 'hover' | 'none';
target?: '_blank' | '_self' | '_parent' | '_top'; size?: string;
rel?: string; weight?: string;
onClick?: React.MouseEventHandler<HTMLAnchorElement>; letterSpacing?: string;
block?: boolean; block?: boolean;
weight?: 'light' | 'normal' | 'medium' | 'semibold' | 'bold' | string;
truncate?: boolean;
hoverColor?: string; hoverColor?: string;
transition?: boolean; transition?: boolean;
} }
export function Link({ export const Link = forwardRef<HTMLAnchorElement, LinkProps>(({
href,
children, children,
className = '',
variant = 'primary', variant = 'primary',
size = 'md', underline = 'hover',
target = '_self', size,
rel = '',
onClick,
block = false,
weight, weight,
truncate, letterSpacing,
block = false,
hoverColor, hoverColor,
transition, transition = true,
...props ...props
}: LinkProps) { }, ref) => {
const baseClasses = 'inline-flex items-center transition-colors';
const variantClasses = { const variantClasses = {
primary: 'text-primary-accent hover:text-primary-accent/80', primary: 'text-[var(--ui-color-intent-primary)] hover:opacity-80',
secondary: 'text-telemetry-aqua hover:text-telemetry-aqua/80', secondary: 'text-[var(--ui-color-text-med)] hover:text-[var(--ui-color-text-high)]',
ghost: 'text-gray-400 hover:text-gray-300' ghost: 'text-[var(--ui-color-text-low)] hover:text-[var(--ui-color-text-high)]',
inherit: 'text-inherit',
}; };
const sizeClasses = { const underlineClasses = {
xs: 'text-xs', always: 'underline',
sm: 'text-sm', hover: 'hover:underline',
md: 'text-base', none: 'no-underline',
lg: 'text-lg'
}; };
const weightClasses: Record<string, string> = {
light: 'font-light',
normal: 'font-normal',
medium: 'font-medium',
semibold: 'font-semibold',
bold: 'font-bold'
};
const classes = [ const classes = [
block ? 'flex' : baseClasses, transition ? 'transition-all duration-150 ease-in-out' : '',
'cursor-pointer',
block ? 'block' : 'inline',
variantClasses[variant], variantClasses[variant],
sizeClasses[size], underlineClasses[underline],
weight && weightClasses[weight] ? weightClasses[weight] : '', ].join(' ');
truncate ? 'truncate' : '',
hoverColor ? `hover:${hoverColor}` : '', const style: React.CSSProperties = {
transition ? 'transition-all duration-150' : '', ...(size ? { fontSize: size } : {}),
className ...(weight ? { fontWeight: weight } : {}),
].filter(Boolean).join(' '); ...(letterSpacing ? { letterSpacing } : {}),
};
return ( return (
<Box <a
as="a" ref={ref}
href={href}
className={classes} className={classes}
target={target} style={style}
rel={rel}
onClick={onClick}
style={{
...(weight && !weightClasses[weight] ? { fontWeight: weight } : {}),
...(props.style || {})
}}
{...props} {...props}
> >
{children} {children}
</Box> </a>
); );
} });
Link.displayName = 'Link';

View File

@@ -1,26 +1,41 @@
import React from 'react'; import React from 'react';
import { Box } from './primitives/Box'; import { Box } from './primitives/Box';
interface LoadingSpinnerProps { export interface LoadingSpinnerProps {
size?: number; size?: 'sm' | 'md' | 'lg' | number;
color?: string; intent?: 'primary' | 'high' | 'low';
className?: string;
} }
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 ( return (
<Box <Box
w={`${size * 0.25}rem`} width={dimension}
h={`${size * 0.25}rem`} height={dimension}
rounded="full" style={{
borderWidth="2px" border: '2px solid rgba(255, 255, 255, 0.1)',
borderStyle="solid" borderTop: `2px solid ${intentColorMap[intent]}`,
borderColor="transparent" borderRadius: '50%',
borderTopColor={color} }}
borderLeftColor={color} className="animate-spin"
className={`animate-spin ${className}`}
role="status" role="status"
aria-label="Loading" aria-label="Loading"
/> />
); );
} };

View File

@@ -1,9 +1,14 @@
import React from 'react'; import React, { ReactNode } from 'react';
import { Box } from './primitives/Box';
interface MainContentProps { export interface MainContentProps {
children: React.ReactNode; children: ReactNode;
} }
export function MainContent({ children }: MainContentProps) { export const MainContent = ({ children }: MainContentProps) => {
return <div className="pt-16 md:pt-20">{children}</div>; return (
} <Box as="main" flex={1} display="flex" flexDirection="col" minHeight="0">
{children}
</Box>
);
};

View File

@@ -1,64 +1,60 @@
import React from 'react'; import React from 'react';
import { LucideIcon } from 'lucide-react'; import { Card } from './Card';
import { Box } from './primitives/Box'; import { Box } from './primitives/Box';
import { Text } from './Text'; import { Text } from './Text';
import { Icon } from './Icon'; import { Icon } from './Icon';
import { LucideIcon } from 'lucide-react';
interface MetricCardProps { export interface MetricCardProps {
label: string; label: string;
value: string | number; value: string | number;
icon?: LucideIcon; icon?: LucideIcon;
color?: string; intent?: 'primary' | 'success' | 'warning' | 'critical' | 'telemetry';
trend?: { trend?: {
value: number; value: number;
isPositive: boolean; isPositive: boolean;
}; };
border?: boolean;
bg?: string;
} }
/** export const MetricCard = ({
* A semantic component for displaying metrics. label,
* Instrument-grade typography and dense-but-readable hierarchy. value,
*/ icon,
export function MetricCard({ intent = 'primary',
label, trend
value, }: MetricCardProps) => {
icon,
color = 'text-primary-accent',
trend,
border = true,
bg = 'panel-gray/40',
}: MetricCardProps) {
return ( return (
<Box <Card variant="default">
bg={bg} <Box display="flex" alignItems="start" justifyContent="between" marginBottom={4}>
rounded="none" <Box>
p={4} <Text size="xs" weight="bold" variant="low" uppercase>
border={border}
borderColor="border-gray/30"
display="flex"
flexDirection="col"
gap={2}
hoverBg="panel-gray/60"
transition
>
<Box display="flex" alignItems="center" justifyContent="between">
<Box display="flex" alignItems="center" gap={2}>
{icon && <Icon icon={icon} size={4} className={color} />}
<Text size="xs" weight="bold" color="text-gray-500" uppercase letterSpacing="widest">
{label} {label}
</Text> </Text>
</Box> <Text size="2xl" weight="bold" variant="high" block marginTop={1}>
{trend && ( {value}
<Text size="xs" color={trend.isPositive ? 'text-success-green' : 'text-red-400'} font="mono">
{trend.isPositive ? '▲' : '▼'} {trend.value}%
</Text> </Text>
</Box>
{icon && (
<Box padding={2} rounded="lg" bg="var(--ui-color-bg-surface-muted)">
<Icon icon={icon} size={5} intent={intent} />
</Box>
)} )}
</Box> </Box>
<Text size="2xl" weight="bold" color="text-white" font="mono">
{typeof value === 'number' ? value.toLocaleString() : value} {trend && (
</Text> <Box display="flex" alignItems="center" gap={1}>
</Box> <Text
size="xs"
weight="bold"
variant={trend.isPositive ? 'success' : 'critical'}
>
{trend.isPositive ? '+' : '-'}{Math.abs(trend.value)}%
</Text>
<Text size="xs" variant="low">
vs last period
</Text>
</Box>
)}
</Card>
); );
} };

View File

@@ -2,17 +2,21 @@ import React from 'react';
import { Box } from './primitives/Box'; import { Box } from './primitives/Box';
import { Text } from './Text'; import { Text } from './Text';
interface MiniStatProps { export interface MiniStatProps {
label: string; label: string;
value: string | number; 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 ( return (
<Box textAlign="center" p={2} rounded="lg" bg="bg-charcoal-outline/30"> <Box textAlign="center" padding={2} rounded="lg" bg="var(--ui-color-bg-surface-muted)">
<Text size="lg" weight="bold" color={color} block>{value}</Text> <Text size="lg" weight="bold" variant={intent} block>{value}</Text>
<Text size="xs" color="text-gray-500" block style={{ fontSize: '10px' }}>{label}</Text> <Text size="xs" variant="low" block style={{ fontSize: '10px' }}>{label}</Text>
</Box> </Box>
); );
} };

View File

@@ -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 { Box } from './primitives/Box';
import { Stack } from './primitives/Stack'; import { Surface } from './primitives/Surface';
import { Button } from './Button';
import { Text } from './Text';
import { X } from 'lucide-react';
import { IconButton } from './IconButton'; 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; isOpen: boolean;
onClose?: () => void; onClose?: () => void;
onOpenChange?: (open: boolean) => void; onOpenChange?: (isOpen: boolean) => void;
title?: string; title?: string;
description?: string; size?: 'sm' | 'md' | 'lg' | 'xl' | 'full';
icon?: React.ReactNode;
children: ReactNode;
footer?: ReactNode;
primaryActionLabel?: string; primaryActionLabel?: string;
onPrimaryAction?: () => void; onPrimaryAction?: () => void;
secondaryActionLabel?: string; secondaryActionLabel?: string;
onSecondaryAction?: () => void; onSecondaryAction?: () => void;
isLoading?: boolean; footer?: ReactNode;
size?: 'sm' | 'md' | 'lg' | 'xl'; description?: string;
icon?: ReactNode;
} }
export function Modal({ export const Modal = ({
isOpen, children,
onClose, isOpen,
onClose,
onOpenChange, onOpenChange,
title, title,
description, size = 'md',
icon,
children,
footer,
primaryActionLabel, primaryActionLabel,
onPrimaryAction, onPrimaryAction,
secondaryActionLabel, secondaryActionLabel,
onSecondaryAction, onSecondaryAction,
isLoading = false, footer,
size = 'md', description,
}: ModalProps) { 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; if (!isOpen) return null;
const sizeMap = { const sizeMap = {
sm: 'max-w-md', sm: '24rem',
md: 'max-w-lg', md: '32rem',
lg: 'max-w-2xl', lg: '48rem',
xl: 'max-w-4xl', xl: '64rem',
full: '100%',
}; };
const handleClose = () => { const handleClose = () => {
@@ -53,96 +65,82 @@ export function Modal({
if (onOpenChange) onOpenChange(false); if (onOpenChange) onOpenChange(false);
}; };
return ( return createPortal(
<Box <Box
position="fixed" position="fixed"
inset={0} inset={0}
zIndex={60} zIndex={100}
display="flex" display="flex"
alignItems="center" alignItems="center"
justifyContent="center" justifyContent="center"
bg="bg-black/60" padding={4}
px={4} bg="rgba(0, 0, 0, 0.8)"
className="backdrop-blur-sm"
role="dialog"
aria-modal="true"
> >
{/* Backdrop click to close */} <Box
<Box position="absolute" inset={0} onClick={handleClose} /> position="absolute"
inset={0}
<Box onClick={handleClose}
position="relative" />
w="full"
maxWidth={sizeMap[size]} <Surface
rounded="2xl" variant="default"
bg="bg-[#0f1115]" rounded="lg"
border shadow="xl"
borderColor="border-[#262626]" style={{
shadow="2xl" width: '100%',
overflow="hidden" maxWidth: sizeMap[size],
tabIndex={-1} maxHeight: '90vh',
overflow: 'hidden',
display: 'flex',
flexDirection: 'column',
position: 'relative',
border: '1px solid var(--ui-color-border-default)'
}}
> >
{/* Header */} <Box
<Box p={6} borderBottom borderColor="border-white/5"> display="flex"
<Stack direction="row" align="center" justify="between"> alignItems="center"
<Stack direction="row" align="center" gap={3}> justifyContent="between"
{icon && <Box>{icon}</Box>} padding={4}
<Box> borderBottom
{title && ( >
<Text size="xl" weight="bold" color="text-white" block> <Box display="flex" alignItems="center" gap={3}>
{title} {icon}
</Text> <Box>
)} {title && <Heading level={3}>{title}</Heading>}
{description && ( {description && <Box marginTop={1}><Text size="sm" variant="low">{description}</Text></Box>}
<Text size="sm" color="text-gray-400" block mt={1}> </Box>
{description} </Box>
</Text> <IconButton icon={X} onClick={handleClose} variant="ghost" title="Close modal" />
)}
</Box>
</Stack>
<IconButton
icon={X}
onClick={handleClose}
variant="ghost"
size="sm"
title="Close modal"
/>
</Stack>
</Box> </Box>
{/* Content */} <Box flex={1} overflow="auto" padding={6}>
<Box p={6} overflowY="auto" maxHeight="calc(100vh - 200px)">
{children} {children}
</Box> </Box>
{/* Footer */} {(footer || primaryActionLabel || secondaryActionLabel) && (
{(primaryActionLabel || secondaryActionLabel || footer) && ( <Box padding={4} borderTop bg="rgba(255,255,255,0.02)" display="flex" justifyContent="end" gap={3}>
<Box p={6} borderTop borderColor="border-white/5"> {footer}
{footer || ( {secondaryActionLabel && (
<Stack direction="row" justify="end" gap={3}> <Button
{secondaryActionLabel && ( onClick={onSecondaryAction || handleClose}
<Button variant="ghost"
variant="ghost" >
onClick={onSecondaryAction || onClose} {secondaryActionLabel}
disabled={isLoading} </Button>
> )}
{secondaryActionLabel} {primaryActionLabel && (
</Button> <Button
)} onClick={onPrimaryAction}
{primaryActionLabel && ( variant="primary"
<Button >
variant="primary" {primaryActionLabel}
onClick={onPrimaryAction} </Button>
isLoading={isLoading}
>
{primaryActionLabel}
</Button>
)}
</Stack>
)} )}
</Box> </Box>
)} )}
</Box> </Surface>
</Box> </Box>,
document.body
); );
} };

View File

@@ -1,129 +1,56 @@
import React from 'react'; import React, { ReactNode } from 'react';
import { LucideIcon } from 'lucide-react';
import { Heading } from './Heading';
import { Button } from './Button';
import { Box } from './primitives/Box'; import { Box } from './primitives/Box';
import { Stack } from './primitives/Stack'; import { Heading } from './Heading';
import { Text } from './Text'; import { Text } from './Text';
import { Icon } from './Icon'; import { Surface } from './primitives/Surface';
import { ModalIcon } from './ModalIcon';
interface PageHeroProps { export interface PageHeroProps {
title: string; title: string;
description?: string; description?: string;
icon?: LucideIcon; children?: ReactNode;
backgroundPattern?: React.ReactNode; image?: 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;
} }
export const PageHero = ({ export const PageHero = ({
title, title,
description, description,
icon,
backgroundPattern,
stats,
actions,
children, children,
className = '' image
}: PageHeroProps) => ( }: PageHeroProps) => {
<Box return (
as="section" <Surface
position="relative" variant="dark"
overflow="hidden" rounded="xl"
rounded="2xl" padding={8}
bg="bg-gradient-to-br from-iron-gray/80 via-deep-graphite to-iron-gray/60" style={{ position: 'relative', overflow: 'hidden', border: '1px solid var(--ui-color-border-default)' }}
border={true} >
borderColor="border-charcoal-outline/50" <Box display="flex" flexDirection={{ base: 'col', lg: 'row' }} alignItems="center" gap={8}>
className={className} <Box flex={1}>
> <Heading level={1} marginBottom={4}>{title}</Heading>
{/* Background Pattern */}
{backgroundPattern || (
<>
<Box position="absolute" top="0" right="0" width="96" height="96" bg="bg-primary-blue/5" rounded="full" blur="3xl" />
<Box position="absolute" bottom="0" left="0" width="64" height="64" bg="bg-neon-aqua/5" rounded="full" blur="3xl" />
</>
)}
<Box position="relative" maxWidth="7xl" mx="auto" px={8} py={10}>
<Box display="flex" flexDirection={{ base: 'col', lg: 'row' }} alignItems={{ lg: 'center' }} justifyContent="between" gap={8}>
{/* Main Content */}
<Box maxWidth="2xl">
{icon && (
<Stack direction="row" align="center" gap={3} mb={4}>
<ModalIcon icon={icon} />
<Heading level={1}>
{title}
</Heading>
</Stack>
)}
{!icon && (
<Heading level={1} mb={4}>
{title}
</Heading>
)}
{description && ( {description && (
<Text size="lg" color="text-gray-400" block style={{ lineHeight: 1.625 }}> <Text size="lg" variant="low" marginBottom={6} block>
{description} {description}
</Text> </Text>
)} )}
{children}
{/* Stats */}
{stats && stats.length > 0 && (
<Box display="flex" flexWrap="wrap" gap={6} mt={6}>
{stats.map((stat, index) => (
<Stack key={index} direction="row" align="center" gap={2}>
{stat.icon ? (
<Icon icon={stat.icon} size={4} className={stat.color || 'text-primary-blue'} />
) : (
<Box width="2" height="2" rounded="full" className={`${stat.color || 'bg-primary-blue'} ${stat.animate ? 'animate-pulse' : ''}`} />
)}
<Text size="sm" color="text-gray-400">
<Text color="text-white" weight="semibold">{stat.value}</Text> {stat.label}
</Text>
</Stack>
))}
</Box>
)}
</Box> </Box>
{image && (
{/* Actions or Custom Content */} <Box flex={1} display="flex" justifyContent="center">
{actions && actions.length > 0 && ( {image}
<Stack gap={4}> </Box>
{actions.map((action, index) => (
<Stack key={index} gap={2}>
<Button
variant={action.variant || 'primary'}
onClick={action.onClick}
icon={action.icon && <Icon icon={action.icon} size={5} />}
className="px-6 py-3"
>
{action.label}
</Button>
{action.description && (
<Text size="xs" color="text-gray-500" align="center" block>{action.description}</Text>
)}
</Stack>
))}
</Stack>
)} )}
{children}
</Box> </Box>
</Box>
</Box> {/* Decorative elements */}
); <Box
position="absolute"
top="-4rem"
right="-4rem"
width="16rem"
height="16rem"
bg="var(--ui-color-intent-primary)"
style={{ opacity: 0.05, filter: 'blur(64px)', borderRadius: '9999px' }}
/>
</Surface>
);
};

View File

@@ -1,88 +1,77 @@
import React from 'react';
import { ChevronLeft, ChevronRight } from 'lucide-react';
import { Box } from './primitives/Box'; import { Box } from './primitives/Box';
import { Button } from './Button'; import { Button } from './Button';
import { Icon } from './Icon';
import { Stack } from './primitives/Stack';
import { Text } from './Text'; import { Text } from './Text';
import { ChevronLeft, ChevronRight } from 'lucide-react';
interface PaginationProps { export interface PaginationProps {
currentPage: number; currentPage: number;
totalPages: number; totalPages: number;
totalItems: number;
itemsPerPage: number;
onPageChange: (page: number) => void; onPageChange: (page: number) => void;
} }
export function Pagination({ export const Pagination = ({
currentPage, currentPage,
totalPages, totalPages,
totalItems, onPageChange
itemsPerPage, }: PaginationProps) => {
onPageChange, const pages = Array.from({ length: totalPages }, (_, i) => i + 1);
}: PaginationProps) {
if (totalPages <= 1) return null; const visiblePages = pages.filter(page => {
if (totalPages <= 7) return true;
const startItem = ((currentPage - 1) * itemsPerPage) + 1; if (page === 1 || page === totalPages) return true;
const endItem = Math.min(currentPage * itemsPerPage, totalItems); if (page >= currentPage - 1 && page <= currentPage + 1) return true;
return false;
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];
};
return ( return (
<Box display="flex" alignItems="center" justifyContent="between" pt={4}> <Box display="flex" alignItems="center" justifyContent="between" paddingTop={4}>
<Text size="sm" color="text-gray-500"> <Text size="sm" variant="low">
Showing {startItem}{endItem} of {totalItems} Page {currentPage} of {totalPages}
</Text> </Text>
<Stack direction="row" align="center" gap={2}> <Box display="flex" alignItems="center" gap={2}>
<Button <Button
variant="secondary" variant="ghost"
onClick={() => onPageChange(Math.max(1, currentPage - 1))}
disabled={currentPage === 1}
size="sm" size="sm"
icon={<Icon icon={ChevronLeft} size={5} />} disabled={currentPage === 1}
onClick={() => onPageChange(currentPage - 1)}
icon={<ChevronLeft size={16} />}
> >
<Box as="span" className="sr-only">Previous</Box> Previous
</Button> </Button>
<Stack direction="row" align="center" gap={1}> <Box display="flex" alignItems="center" gap={1}>
{getPageNumbers().map(pageNum => ( {visiblePages.map((page, index) => {
<Button const prevPage = visiblePages[index - 1];
key={pageNum} const showEllipsis = prevPage && page - prevPage > 1;
variant={currentPage === pageNum ? 'primary' : 'ghost'}
onClick={() => onPageChange(pageNum)} return (
className="w-10 h-10 p-0" <React.Fragment key={page}>
> {showEllipsis && <Text variant="low">...</Text>}
{pageNum} <Button
</Button> variant={page === currentPage ? 'primary' : 'ghost'}
))} size="sm"
</Stack> onClick={() => onPageChange(page)}
style={{ minWidth: '2.5rem', padding: 0 }}
>
{page}
</Button>
</React.Fragment>
);
})}
</Box>
<Button <Button
variant="secondary" variant="ghost"
onClick={() => onPageChange(Math.min(totalPages, currentPage + 1))}
disabled={currentPage === totalPages}
size="sm" size="sm"
icon={<Icon icon={ChevronRight} size={5} />} disabled={currentPage === totalPages}
onClick={() => onPageChange(currentPage + 1)}
icon={<ChevronRight size={16} />}
> >
<Box as="span" className="sr-only">Next</Box> Next
</Button> </Button>
</Stack> </Box>
</Box> </Box>
); );
} };

View File

@@ -1,57 +1,42 @@
import React, { ReactNode } from 'react'; import React, { ReactNode } from 'react';
import { Surface } from './primitives/Surface'; import { Surface } from './primitives/Surface';
import { Box, BoxProps } from './primitives/Box'; import { Box } from './primitives/Box';
import { Heading } from './Heading';
import { Text } from './Text'; import { Text } from './Text';
interface PanelProps extends Omit<BoxProps<'div'>, 'variant' | 'padding'> { export interface PanelProps {
children: ReactNode;
title?: string; title?: string;
description?: string; description?: string;
variant?: 'default' | 'muted' | 'dark' | 'glass'; children: ReactNode;
padding?: 0 | 1 | 2 | 3 | 4 | 6 | 8 | 10 | 12; footer?: ReactNode;
border?: boolean; variant?: 'default' | 'dark' | 'muted';
className?: string;
} }
/** export const Panel = ({
* A semantic wrapper for content panels. title,
* Follows the "Precision Racing Minimal" theme. description,
*/ children,
export function Panel({ footer,
children, variant = 'default'
title, }: PanelProps) => {
description,
variant = 'default',
padding = 6,
border = true,
...props
}: PanelProps) {
return ( return (
<Surface <Surface variant={variant} rounded="lg" style={{ border: '1px solid var(--ui-color-border-default)' }}>
variant={variant}
padding={padding}
border={border}
display="flex"
flexDirection="col"
gap={4}
// eslint-disable-next-line @typescript-eslint/no-explicit-any
{...(props as any)}
>
{(title || description) && ( {(title || description) && (
<Box display="flex" flexDirection="col" gap={1} borderBottom borderStyle="solid" borderColor="border-gray/30" pb={4} mb={2}> <Box padding={6} borderBottom>
{title && ( {title && <Heading level={3} marginBottom={1}>{title}</Heading>}
<Text as="h3" size="xs" weight="bold" color="text-gray-500" uppercase letterSpacing="widest"> {description && <Text size="sm" variant="low">{description}</Text>}
{title} </Box>
</Text> )}
)}
{description && ( <Box padding={6}>
<Text size="sm" color="text-gray-400"> {children}
{description} </Box>
</Text>
)} {footer && (
<Box padding={4} borderTop bg="rgba(255,255,255,0.02)">
{footer}
</Box> </Box>
)} )}
{children}
</Surface> </Surface>
); );
} };

View File

@@ -1,42 +1,35 @@
import React, { ComponentProps } from 'react'; import React, { useState } from 'react';
import { Eye, EyeOff, Lock } from 'lucide-react'; import { Input, InputProps } from './Input';
import { Input } from './Input'; import { Eye, EyeOff } from 'lucide-react';
import { IconButton } from './IconButton';
import { Box } from './primitives/Box'; import { Box } from './primitives/Box';
interface PasswordFieldProps extends ComponentProps<typeof Input> { export interface PasswordFieldProps extends InputProps {}
showPassword?: boolean;
onTogglePassword?: () => void; 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 ( return (
<Box position="relative" fullWidth> <Box position="relative">
<Input <Input
{...props} {...props}
type={showPassword ? 'text' : 'password'} type={showPassword ? 'text' : 'password'}
icon={<Lock size={16} />}
/> />
{onTogglePassword && ( <Box
<Box position="absolute"
as="button" right="0.5rem"
type="button" top="50%"
onClick={onTogglePassword} style={{ transform: 'translateY(-50%)' }}
position="absolute" zIndex={10}
right="3" >
top={props.label ? "34px" : "50%"} <IconButton
style={props.label ? {} : { transform: 'translateY(-50%)' }} icon={showPassword ? EyeOff : Eye}
zIndex={10} onClick={() => setShowPassword(!showPassword)}
className="text-gray-500 hover:text-gray-300 transition-colors" variant="ghost"
> size="sm"
{showPassword ? <EyeOff size={16} /> : <Eye size={16} />} title={showPassword ? 'Hide password' : 'Show password'}
</Box> />
)} </Box>
</Box> </Box>
); );
} };

View File

@@ -1,21 +1,34 @@
import React from 'react';
import { User } from 'lucide-react';
import { Box } from './primitives/Box'; import { Box } from './primitives/Box';
import { Icon } from './Icon'; import { Icon } from './Icon';
import { User } from 'lucide-react';
export interface PlaceholderImageProps { export interface PlaceholderImageProps {
size?: number; width?: string | number;
height?: string | number;
size?: string | number;
className?: string; className?: string;
} }
export function PlaceholderImage({ size = 48, className = '' }: PlaceholderImageProps) { export const PlaceholderImage = ({
width,
height,
size,
className
}: PlaceholderImageProps) => {
const dimension = size || '100%';
return ( return (
<Box <Box
className={`rounded-full bg-charcoal-outline flex items-center justify-center ${className}`} width={width || dimension}
style={{ width: size, height: size }} height={height || dimension}
display="flex"
alignItems="center"
justifyContent="center"
bg="var(--ui-color-bg-surface-muted)"
style={{ borderRadius: 'var(--ui-radius-md)' }}
className={className}
> >
<Icon icon={User} size={6} color="#9ca3af" /> <Icon icon={User} size={6} intent="low" />
</Box> </Box>
); );
} };

View File

@@ -1,70 +1,80 @@
import React, { ReactNode } from 'react';
import { Trophy } from 'lucide-react';
import { ReactNode } from 'react';
import { Box } from './primitives/Box'; import { Box } from './primitives/Box';
import { Heading } from './Heading';
import { Icon } from './Icon';
import { Stack } from './primitives/Stack';
import { Text } from './Text'; import { Text } from './Text';
import { Avatar } from './Avatar';
import { Trophy } from 'lucide-react';
import { Icon } from './Icon';
import { Heading } from './Heading';
interface PodiumProps { export interface PodiumEntry {
title: string; name: string;
children: ReactNode; avatar?: string;
value: string | number;
position: 1 | 2 | 3;
} }
export function Podium({ title, children }: PodiumProps) { export interface PodiumProps {
return ( entries?: PodiumEntry[];
<Box bg="bg-iron-gray/50" rounded="2xl" border style={{ borderColor: 'rgba(38, 38, 38, 0.8)' }} p={8} mb={10}> title?: string;
<Box display="flex" justifyContent="center" mb={8}> children?: ReactNode;
<Stack direction="row" align="center" gap={2}> }
<Icon icon={Trophy} size={6} color="var(--warning-amber)" />
<Heading level={2}>{title}</Heading>
</Stack>
</Box>
<Stack direction="row" align="end" justify="center" gap={8}> export const Podium = ({ entries = [], title, children }: PodiumProps) => {
{children} const sortedEntries = [...entries].sort((a, b) => {
</Stack> 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 (
<Box paddingY={8}>
{title && <Heading level={2} align="center" marginBottom={8}>{title}</Heading>}
<Box display="flex" alignItems="end" justifyContent="center" gap={4}>
{sortedEntries.map((entry) => {
const height = entry.position === 1 ? '12rem' : entry.position === 2 ? '10rem' : '8rem';
const color = getPositionColor(entry.position);
return (
<Box key={entry.position} display="flex" flexDirection="col" alignItems="center" gap={4}>
<Box display="flex" flexDirection="col" alignItems="center" gap={2}>
{entry.position === 1 && <Icon icon={Trophy} size={6} intent="warning" />}
<Avatar src={entry.avatar} alt={entry.name} size={entry.position === 1 ? 'lg' : 'md'} />
<Text weight="bold" variant="high" size={entry.position === 1 ? 'md' : 'sm'}>{entry.name}</Text>
<Text size="xs" variant="low">{entry.value}</Text>
</Box>
<Box
width="6rem"
height={height}
bg="var(--ui-color-bg-surface-muted)"
display="flex"
alignItems="center"
justifyContent="center"
style={{
borderTopLeftRadius: 'var(--ui-radius-lg)',
borderTopRightRadius: 'var(--ui-radius-lg)',
border: `1px solid ${color}`,
borderBottom: 'none'
}}
>
<Text size="3xl" weight="bold" style={{ color }}>
{entry.position}
</Text>
</Box>
</Box>
);
})}
</Box>
{children}
</Box> </Box>
); );
} };
interface PodiumItemProps { export const PodiumItem = ({ children }: { children: ReactNode }) => <>{children}</>;
position: number;
height: string;
cardContent: ReactNode;
bgColor: string;
positionColor: string;
}
export function PodiumItem({
position,
height,
cardContent,
bgColor,
positionColor,
}: PodiumItemProps) {
return (
<Stack align="center">
{cardContent}
{/* Podium stand */}
<Box
bg={bgColor}
h={height}
border
style={{ borderColor: 'rgba(38, 38, 38, 0.8)', borderTopLeftRadius: '0.5rem', borderTopRightRadius: '0.5rem' }}
w="28"
display="flex"
p={3}
>
<Box display="flex" center fullWidth>
<Text size="3xl" weight="bold" color={positionColor}>
{position}
</Text>
</Box>
</Box>
</Stack>
);
}

View File

@@ -1,122 +1,56 @@
import React from 'react';
import { Box } from './primitives/Box';
import type { MouseEventHandler, ReactNode } from 'react'; import { Text } from './Text';
import { Card } from './Card'; import { Surface } from './primitives/Surface';
import { Icon } from './Icon';
interface PresetCardStat { import { LucideIcon } from 'lucide-react';
label: string;
value: string;
}
export interface PresetCardProps { export interface PresetCardProps {
title: string; title: string;
subtitle?: string; description: string;
primaryTag?: string; icon: LucideIcon;
description?: string; onClick: () => void;
stats?: PresetCardStat[]; isSelected?: boolean;
selected?: boolean;
disabled?: boolean;
onSelect?: () => void;
className?: string;
children?: ReactNode;
} }
export function PresetCard({ export const PresetCard = ({
title, title,
subtitle, description,
primaryTag, icon,
description, onClick,
stats, isSelected = false
selected, }: PresetCardProps) => {
disabled,
onSelect,
className = '',
children,
}: PresetCardProps) {
const isInteractive = typeof onSelect === 'function' && !disabled;
const handleClick: MouseEventHandler<HTMLButtonElement | HTMLDivElement> = (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 = (
<div className="flex h-full flex-col gap-3">
<div className="flex items-start justify-between gap-3">
<div>
<div className="text-sm font-semibold text-white">{title}</div>
{subtitle && (
<div className="mt-0.5 text-xs text-gray-400">{subtitle}</div>
)}
</div>
<div className="flex flex-col items-end gap-1">
{primaryTag && (
<span className="inline-flex rounded-full bg-primary-blue/15 px-2 py-0.5 text-[10px] font-medium uppercase tracking-wide text-primary-blue">
{primaryTag}
</span>
)}
{selected && (
<span className="inline-flex items-center gap-1 rounded-full bg-primary-blue/10 px-2 py-0.5 text-[10px] font-medium text-primary-blue">
<span className="h-1.5 w-1.5 rounded-full bg-primary-blue" />
Selected
</span>
)}
</div>
</div>
{description && (
<p className="text-xs text-gray-300">{description}</p>
)}
{children}
{stats && stats.length > 0 && (
<div className="mt-1 border-t border-charcoal-outline/70 pt-2">
<dl className="grid grid-cols-1 gap-2 text-[11px] text-gray-400 sm:grid-cols-3">
{stats.map((stat) => (
<div key={stat.label} className="space-y-0.5">
<dt className="font-medium text-gray-500">{stat.label}</dt>
<dd className="text-xs text-gray-200">{stat.value}</dd>
</div>
))}
</dl>
</div>
)}
</div>
);
const commonClasses = `${baseBorder} ${baseBg} ${baseRing} ${hoverClasses} ${disabledClasses} ${className}`;
if (isInteractive) {
return (
<button
type="button"
onClick={handleClick as MouseEventHandler<HTMLButtonElement>}
disabled={disabled}
className={`group block w-full rounded-lg text-left text-sm shadow-card outline-none transition-all duration-150 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-primary-blue ${commonClasses}`}
>
<div className="p-4">
{content}
</div>
</button>
);
}
return ( return (
<Card <Surface
className={commonClasses} variant={isSelected ? 'default' : 'muted'}
onClick={handleClick as MouseEventHandler<HTMLDivElement>} rounded="lg"
padding={4}
onClick={onClick}
style={{
cursor: 'pointer',
border: isSelected ? '2px solid var(--ui-color-intent-primary)' : '1px solid var(--ui-color-border-default)',
transition: 'all 0.2s ease-in-out'
}}
className="group hover:bg-white/5"
> >
{content} <Box display="flex" alignItems="start" gap={4}>
</Card> <Box
padding={3}
rounded="lg"
bg={isSelected ? 'var(--ui-color-intent-primary)' : 'var(--ui-color-bg-surface-muted)'}
className="transition-colors"
>
<Icon icon={icon} size={6} intent={isSelected ? 'high' : 'low'} />
</Box>
<Box>
<Text weight="bold" variant="high" block marginBottom={1}>
{title}
</Text>
<Text size="sm" variant="low">
{description}
</Text>
</Box>
</Box>
</Surface>
); );
} };

View File

@@ -1,33 +1,55 @@
import React from 'react'; import React from 'react';
import { Box, BoxProps } from './primitives/Box'; import { Box } from './primitives/Box';
interface ProgressBarProps extends Omit<BoxProps<'div'>, 'children'> { export interface ProgressBarProps {
value: number; value: number;
max: number; max?: number;
color?: string; intent?: 'primary' | 'success' | 'warning' | 'critical' | 'telemetry';
bg?: string; color?: string; // Alias for intent
height?: string; size?: 'sm' | 'md' | 'lg';
marginBottom?: any;
mb?: any; // Alias for marginBottom
} }
export function ProgressBar({ export const ProgressBar = ({
value, value,
max, max = 100,
color = 'bg-primary-blue', intent = 'primary',
bg = 'bg-deep-graphite', color: colorProp,
height = '2', size = 'md',
...props marginBottom,
}: ProgressBarProps) { mb
const percentage = Math.min((value / max) * 100, 100); }: 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 ( return (
<Box fullWidth bg={bg} rounded="full" height={height} overflow="hidden" {...props}> <Box
fullWidth
bg="var(--ui-color-bg-surface-muted)"
style={{ height: sizeMap[size], borderRadius: '9999px', overflow: 'hidden' }}
marginBottom={marginBottom || mb}
>
<Box <Box
bg={color}
rounded="full"
fullHeight fullHeight
style={{ width: `${percentage}%` }} bg={color}
className="transition-all duration-500 ease-out" style={{ width: `${percentage}%`, transition: 'width 0.3s ease-in-out' }}
/> />
</Box> </Box>
); );
} };

View File

@@ -1,51 +1,59 @@
import React from 'react';
import { ChevronRight, LucideIcon } from 'lucide-react';
import { Box } from './primitives/Box'; import { Box } from './primitives/Box';
import { Icon } from './Icon';
import { Link } from './Link';
import { Text } from './Text'; import { Text } from './Text';
import { Icon } from './Icon';
import { LucideIcon, ChevronRight } from 'lucide-react';
import { Link } from './Link';
interface QuickActionItemProps { export interface QuickActionItemProps {
href: string;
label: string; label: string;
icon: LucideIcon; icon: LucideIcon;
iconVariant?: 'blue' | 'amber' | 'purple' | 'green'; href: string;
variant?: 'primary' | 'secondary' | 'success' | 'warning' | 'critical';
} }
export function QuickActionItem({ href, label, icon, iconVariant = 'blue' }: QuickActionItemProps) { export const QuickActionItem = ({
const variantColors = { label,
blue: 'rgb(59, 130, 246)', icon,
amber: 'rgb(245, 158, 11)', href,
purple: 'rgb(168, 85, 247)', variant = 'primary'
green: 'rgb(16, 185, 129)', }: 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 = { const variantIntents = {
blue: 'bg-primary-blue/10', primary: 'primary' as const,
amber: 'bg-warning-amber/10', secondary: 'med' as const,
purple: 'bg-purple-500/10', success: 'success' as const,
green: 'bg-performance-green/10', warning: 'warning' as const,
critical: 'critical' as const,
}; };
return ( return (
<Link <Link href={href} underline="none">
href={href} <Box
block display="flex"
p={3} alignItems="center"
rounded="lg" gap={4}
bg="bg-deep-graphite" padding={4}
hoverBorderColor="charcoal-outline/50" rounded="lg"
transition bg="var(--ui-color-bg-surface)"
className="hover:bg-charcoal-outline/50" style={{ border: '1px solid var(--ui-color-border-default)' }}
> className="group hover:bg-white/5 transition-colors"
<Box display="flex" alignItems="center" gap={3} fullWidth> >
<Box p={2} bg={variantBgs[iconVariant]} rounded="lg"> <Box padding={2} bg={variantBgs[variant]} rounded="lg">
<Icon icon={icon} size={4} color={variantColors[iconVariant]} /> <Icon icon={icon} size={5} intent={variantIntents[variant] as any} />
</Box> </Box>
<Text size="sm" color="text-white" weight="medium">{label}</Text> <Text weight="medium" variant="high" flexGrow={1}>
<Icon icon={ChevronRight} size={4} color="rgb(107, 114, 128)" ml="auto" /> {label}
</Text>
<Icon icon={ChevronRight} size={4} intent="low" />
</Box> </Box>
</Link> </Link>
); );
} };

View File

@@ -1,33 +1,36 @@
import React from 'react'; 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; href: string;
children: React.ReactNode;
variant?: 'blue' | 'purple' | 'orange';
className?: string;
} }
export function QuickActionLink({ export const QuickActionLink = ({
href, label,
children, icon,
variant = 'blue', href
className = '' }: QuickActionLinkProps) => {
}: 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(' ');
return ( return (
<a href={href} className={classes}> <Link href={href} underline="none">
{children} <Box
</a> display="flex"
alignItems="center"
gap={3}
paddingY={2}
className="group"
>
<Icon icon={icon} size={4} intent="low" className="group-hover:text-[var(--ui-color-intent-primary)] transition-colors" />
<Text size="sm" variant="med" className="group-hover:text-[var(--ui-color-text-high)] transition-colors">
{label}
</Text>
<Icon icon={ArrowRight} size={3} intent="low" className="opacity-0 group-hover:opacity-100 group-hover:translate-x-1 transition-all" />
</Box>
</Link>
); );
} };

View File

@@ -5,42 +5,37 @@ import { Button } from './Button';
import { Icon } from './Icon'; import { Icon } from './Icon';
import { LucideIcon } from 'lucide-react'; import { LucideIcon } from 'lucide-react';
interface QuickAction { export interface QuickAction {
label: string; label: string;
icon: LucideIcon; icon: LucideIcon;
onClick: () => void; onClick: () => void;
variant?: 'primary' | 'secondary' | 'ghost'; intent?: 'primary' | 'secondary' | 'danger';
} }
interface QuickActionsPanelProps { export interface QuickActionsPanelProps {
actions: QuickAction[]; actions: QuickAction[];
className?: string;
} }
/** export const QuickActionsPanel = ({
* QuickActionsPanel actions
* }: QuickActionsPanelProps) => {
* Provides fast access to common dashboard tasks.
*/
export function QuickActionsPanel({ actions, className = '' }: QuickActionsPanelProps) {
return ( return (
<Panel title="Quick Actions" className={className}> <Panel title="Quick Actions">
<Box display="grid" gridCols={1} gap={2}> <Box display="flex" flexDirection="col" gap={2}>
{actions.map((action, index) => ( {actions.map((action, index) => (
<Button <Button
key={index} key={index}
variant={action.variant || 'secondary'} variant={action.intent === 'danger' ? 'danger' : 'secondary'}
onClick={action.onClick} onClick={action.onClick}
fullWidth fullWidth
style={{ justifyContent: 'flex-start', height: 'auto', padding: '12px' }}
> >
<Box display="flex" align="center" gap={3}> <Box display="flex" alignItems="center" gap={3} fullWidth>
<Icon icon={action.icon} size={5} /> <Icon icon={action.icon} size={4} />
<span>{action.label}</span> {action.label}
</Box> </Box>
</Button> </Button>
))} ))}
</Box> </Box>
</Panel> </Panel>
); );
} };

View File

@@ -1,74 +1,42 @@
import React, { ReactNode } from 'react';
import { ReactNode } from 'react';
import { Box } from './primitives/Box'; import { Box } from './primitives/Box';
import { Heading } from './Heading';
import { Text } from './Text';
interface SectionProps { export interface SectionProps {
children: ReactNode; children: ReactNode;
className?: string; variant?: 'default' | 'dark' | 'muted';
title?: string; padding?: 'none' | 'sm' | 'md' | 'lg';
description?: string;
variant?: 'default' | 'card' | 'highlight' | 'dark' | 'light';
id?: string; 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, children,
className = '',
title,
description,
variant = 'default', variant = 'default',
id, padding = 'md',
py = 16, id
minHeight, }: SectionProps) => {
borderBottom,
borderColor,
overflow,
position
}: SectionProps) {
const variantClasses = { const variantClasses = {
default: '', default: 'bg-[var(--ui-color-bg-base)]',
card: 'bg-panel-gray rounded-none p-6 border border-border-gray', dark: 'bg-black',
highlight: 'bg-gradient-to-r from-primary-accent/10 to-transparent rounded-none p-6 border border-primary-accent/30', muted: 'bg-[var(--ui-color-bg-surface)]',
dark: 'bg-graphite-black',
light: 'bg-panel-gray'
}; };
const paddingClasses = {
none: 'py-0',
sm: 'py-8',
md: 'py-16',
lg: 'py-24',
};
const classes = [ const classes = [
variantClasses[variant], variantClasses[variant],
className paddingClasses[padding],
].filter(Boolean).join(' '); ].join(' ');
return ( return (
<Box <section id={id} className={classes}>
as="section" <Box marginX="auto" maxWidth="80rem" paddingX={4}>
id={id}
className={classes}
py={py as 0}
px={4}
minHeight={minHeight}
borderBottom={borderBottom}
borderColor={borderColor}
overflow={overflow}
position={position}
>
<Box className="mx-auto max-w-7xl">
{(title || description) && (
<Box mb={8}>
{title && <Heading level={2}>{title}</Heading>}
{description && <Text color="text-gray-400" block mt={2}>{description}</Text>}
</Box>
)}
{children} {children}
</Box> </Box>
</Box> </section>
); );
} };

View File

@@ -1,51 +1,55 @@
import React, { ReactNode } from 'react'; import React, { ReactNode } from 'react';
import { Box } from './primitives/Box'; import { Box } from './primitives/Box';
import { Stack } from './primitives/Stack';
import { Text } from './Text'; import { Text } from './Text';
import { Surface } from './primitives/Surface';
import { Icon } from './Icon'; import { Icon } from './Icon';
import { LucideIcon } from 'lucide-react'; import { LucideIcon } from 'lucide-react';
import { Surface } from './primitives/Surface';
interface SectionHeaderProps { export interface SectionHeaderProps {
title: string; title: string;
description?: string; description?: string;
icon?: LucideIcon; icon?: LucideIcon;
color?: string; intent?: 'primary' | 'success' | 'warning' | 'critical' | 'telemetry';
actions?: ReactNode; 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 ( return (
<Box <Box
p={5} padding={5}
borderBottom borderBottom
borderColor="border-white/5" style={{ background: 'linear-gradient(to right, var(--ui-color-bg-surface), transparent)' }}
style={{ background: 'linear-gradient(to right, rgba(38, 38, 38, 0.3), transparent)' }}
> >
<Stack direction="row" align="center" justify="between" wrap gap={4}> <Box display="flex" alignItems="center" justifyContent="between">
<Stack direction="row" align="center" gap={3}> <Box display="flex" alignItems="center" gap={4}>
{icon && ( {icon && (
<Surface variant="muted" rounded="lg" p={2} bg="bg-white/5"> <Surface variant="muted" rounded="lg" padding={2} style={{ backgroundColor: 'rgba(255,255,255,0.05)' }}>
<Icon icon={icon} size={5} color={color} /> <Icon icon={icon} size={5} intent={intent} />
</Surface> </Surface>
)} )}
<Box> <Box>
<Text size="lg" weight="bold" color="text-white" block> <Text size="lg" weight="bold" variant="high" block>
{title} {title}
</Text> </Text>
{description && ( {description && (
<Text size="sm" color="text-gray-400" block> <Text size="sm" variant="low" block>
{description} {description}
</Text> </Text>
)} )}
</Box> </Box>
</Stack> </Box>
{actions && ( {actions && (
<Box> <Box display="flex" alignItems="center" gap={3}>
{actions} {actions}
</Box> </Box>
)} )}
</Stack> </Box>
</Box> </Box>
); );
} };

View File

@@ -1,82 +1,52 @@
import React from 'react';
import { Box } from './primitives/Box'; import { Box } from './primitives/Box';
import { Stack } from './primitives/Stack';
import { Text } from './Text'; import { Text } from './Text';
import { Surface } from './primitives/Surface';
interface SegmentedControlOption { export interface SegmentedControlOption {
value: string; id: string;
label: string; label: string;
description?: string; icon?: React.ReactNode;
disabled?: boolean;
} }
interface SegmentedControlProps { export interface SegmentedControlProps {
options: SegmentedControlOption[]; options: SegmentedControlOption[];
value: string; activeId: string;
onChange?: (value: string) => void; onChange: (id: string) => void;
fullWidth?: boolean;
} }
export function SegmentedControl({ export const SegmentedControl = ({
options, options,
value, activeId,
onChange, onChange,
}: SegmentedControlProps) { fullWidth = false
const handleSelect = (optionValue: string, optionDisabled?: boolean) => { }: SegmentedControlProps) => {
if (!onChange || optionDisabled) return;
if (optionValue === value) return;
onChange(optionValue);
};
return ( return (
<Stack <Surface
direction="row" variant="muted"
rounded="lg"
padding={1}
display="inline-flex" display="inline-flex"
w="full" width={fullWidth ? '100%' : undefined}
flexWrap="wrap"
gap={2}
rounded="full"
bg="bg-black/60"
p={1}
> >
{options.map((option) => { {options.map((option) => {
const isSelected = option.value === value; const isSelected = option.id === activeId;
return ( return (
<Box <button
key={option.value} key={option.id}
as="button" onClick={() => onChange(option.id)}
type="button" className={`flex-1 flex items-center justify-center gap-2 px-4 py-1.5 text-xs font-bold uppercase tracking-widest transition-all rounded-md ${
onClick={() => handleSelect(option.value, option.disabled)} isSelected
aria-pressed={isSelected} ? 'bg-[var(--ui-color-bg-surface)] text-[var(--ui-color-intent-primary)] shadow-sm'
disabled={option.disabled} : 'text-[var(--ui-color-text-low)] hover:text-[var(--ui-color-text-high)]'
flex={1} }`}
minWidth="140px"
px={3}
py={1.5}
rounded="full"
transition="all 0.2s"
textAlign="left"
bg={isSelected ? 'bg-primary-blue' : 'transparent'}
color={isSelected ? 'text-white' : 'text-gray-400'}
opacity={option.disabled ? 0.5 : 1}
cursor={option.disabled ? 'not-allowed' : 'pointer'}
border="none"
> >
<Stack gap={0.5}> {option.icon}
<Text size="xs" weight="medium" color="inherit">{option.label}</Text> {option.label}
{option.description && ( </button>
<Text
size="xs"
color={isSelected ? 'text-white' : 'text-gray-400'}
fontSize="10px"
opacity={isSelected ? 0.8 : 1}
>
{option.description}
</Text>
)}
</Stack>
</Box>
); );
})} })}
</Stack> </Surface>
); );
} };

View File

@@ -1,64 +1,90 @@
import React, { forwardRef, ReactNode } from 'react'; import React, { forwardRef, SelectHTMLAttributes } from 'react';
import { Box } from './primitives/Box'; import { Box } from './primitives/Box';
import { Stack } from './primitives/Stack';
import { Text } from './Text'; import { Text } from './Text';
interface SelectOption { export interface SelectOption {
value: string; value: string | number;
label: string; label: string;
} }
interface SelectProps extends React.SelectHTMLAttributes<HTMLSelectElement> { export interface SelectProps extends Omit<SelectHTMLAttributes<HTMLSelectElement>, 'size'> {
label?: string; label?: string;
options: SelectOption[];
error?: string;
hint?: string;
fullWidth?: boolean; fullWidth?: boolean;
pl?: number; size?: 'sm' | 'md' | 'lg';
errorMessage?: string;
variant?: 'default' | 'error';
options?: SelectOption[];
} }
export const Select = forwardRef<HTMLSelectElement, SelectProps>( export const Select = forwardRef<HTMLSelectElement, SelectProps>(({
({ label, fullWidth = true, pl, errorMessage, variant = 'default', options, children, className = '', style, ...props }, ref) => { label,
const isError = variant === 'error' || !!errorMessage; options,
error,
const variantClasses = isError hint,
? 'border-warning-amber focus:border-warning-amber' fullWidth = false,
: 'border-charcoal-outline focus:border-primary-blue'; size = 'md',
...props
const defaultClasses = `${fullWidth ? 'w-full' : 'w-auto'} px-3 py-2 bg-deep-graphite border rounded-lg text-white focus:outline-none transition-colors`; }, ref) => {
const classes = [ const sizeClasses = {
defaultClasses, sm: 'px-3 py-1.5 text-xs',
variantClasses, md: 'px-4 py-2 text-sm',
pl ? `pl-${pl}` : '', lg: 'px-4 py-3 text-base'
className };
].filter(Boolean).join(' ');
return ( 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';
<Stack gap={1.5} fullWidth={fullWidth}> const errorClasses = error ? 'border-[var(--ui-color-intent-critical)]' : '';
{label && ( const widthClasses = fullWidth ? 'w-full' : '';
<Text as="label" size="xs" weight="bold" color="text-gray-500" uppercase letterSpacing="wider">
const classes = [
baseClasses,
sizeClasses[size],
errorClasses,
widthClasses,
].filter(Boolean).join(' ');
return (
<Box width={fullWidth ? '100%' : undefined}>
{label && (
<Box marginBottom={1.5}>
<Text as="label" size="xs" weight="bold" variant="low">
{label} {label}
</Text> </Text>
)} </Box>
<Box )}
as="select" <div className="relative">
ref={ref} <select
className={classes} ref={ref}
style={style} className={classes}
{...props} {...props}
> >
{options ? options.map(opt => ( {options.map((option) => (
<option key={opt.value} value={opt.value}>{opt.label}</option> <option key={option.value} value={option.value}>
)) : children} {option.label}
</Box> </option>
{errorMessage && ( ))}
<Text size="xs" color="text-warning-amber" mt={1}> </select>
{errorMessage} <div className="absolute inset-y-0 right-0 flex items-center px-2 pointer-events-none">
<svg className="w-4 h-4 text-[var(--ui-color-text-low)]" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M19 9l-7 7-7-7" />
</svg>
</div>
</div>
{error && (
<Box marginTop={1}>
<Text size="xs" variant="critical">
{error}
</Text> </Text>
)} </Box>
</Stack> )}
); {hint && !error && (
} <Box marginTop={1}>
); <Text size="xs" variant="low">
{hint}
</Text>
</Box>
)}
</Box>
);
});
Select.displayName = 'Select'; Select.displayName = 'Select';

View File

@@ -1,37 +1,66 @@
import React from 'react'; import React from 'react';
import { ChevronRight, LucideIcon } from 'lucide-react';
import { Box } from './primitives/Box'; import { Box } from './primitives/Box';
import { Text } from './Text'; import { Text } from './Text';
import { Icon } from './Icon'; import { Icon } from './Icon';
import { LucideIcon, ChevronRight } from 'lucide-react';
import { Link } from './Link'; import { Link } from './Link';
interface SidebarActionLinkProps { export interface SidebarActionLinkProps {
href: string;
icon: LucideIcon;
label: string; label: string;
iconColor?: string; icon: LucideIcon;
iconBgColor?: string; href: string;
isActive?: boolean;
} }
export function SidebarActionLink({ export const SidebarActionLink = ({
label,
icon,
href, href,
icon, isActive = false
label, }: SidebarActionLinkProps) => {
iconColor = 'text-primary-blue',
iconBgColor = 'bg-primary-blue/10',
}: SidebarActionLinkProps) {
return ( return (
<Link <Link
href={href} href={href}
variant="ghost" variant="ghost"
block underline="none"
className="flex items-center gap-3 p-3 rounded-lg bg-deep-graphite hover:bg-iron-gray/50 transition-all"
> >
<Box p={2} className={iconBgColor} rounded="lg" display="flex" center> <Box
<Icon icon={icon} size={4} className={iconColor} /> display="flex"
alignItems="center"
gap={3}
padding={3}
rounded="lg"
bg={isActive ? 'var(--ui-color-bg-surface-muted)' : 'transparent'}
className="group hover:bg-white/5 transition-colors"
>
<Box
padding={2}
rounded="md"
bg={isActive ? 'var(--ui-color-intent-primary)' : 'var(--ui-color-bg-surface-muted)'}
display="flex"
center
>
<Icon
icon={icon}
size={4}
intent={isActive ? 'high' : 'low'}
/>
</Box>
<Text
size="sm"
variant={isActive ? 'high' : 'med'}
weight={isActive ? 'bold' : 'medium'}
flexGrow={1}
>
{label}
</Text>
<Icon
icon={ChevronRight}
size={4}
intent="low"
className="opacity-0 group-hover:opacity-100 transition-opacity"
/>
</Box> </Box>
<Text size="sm" color="text-white" flexGrow={1}>{label}</Text>
<Icon icon={ChevronRight} size={4} color="text-gray-500" />
</Link> </Link>
); );
} };

View File

@@ -1,35 +1,31 @@
import React from 'react'; import React, { forwardRef, ChangeEvent } from 'react';
import { Box } from './primitives/Box'; import { Box } from './primitives/Box';
interface CheckboxProps { export interface SimpleCheckboxProps {
checked: boolean; checked: boolean;
onChange: (checked: boolean) => void; onChange: (checked: boolean) => void;
disabled?: boolean; disabled?: boolean;
'aria-label'?: string;
} }
/** export const SimpleCheckbox = forwardRef<HTMLInputElement, SimpleCheckboxProps>(({
* SimpleCheckbox checked,
* onChange,
* A checkbox without a label for use in tables. disabled = false
*/ }, ref) => {
export function SimpleCheckbox({ checked, onChange, disabled, 'aria-label': ariaLabel }: CheckboxProps) { const handleChange = (e: ChangeEvent<HTMLInputElement>) => {
onChange(e.target.checked);
};
return ( return (
<Box <input
as="input" ref={ref}
type="checkbox" type="checkbox"
checked={checked} checked={checked}
onChange={(e: React.ChangeEvent<HTMLInputElement>) => onChange(e.target.checked)} onChange={handleChange}
disabled={disabled} disabled={disabled}
w="4" 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"
h="4"
bg="bg-deep-graphite"
border
borderColor="border-charcoal-outline"
rounded="sm"
aria-label={ariaLabel}
ring="primary-blue"
color="text-primary-blue"
/> />
); );
} });
SimpleCheckbox.displayName = 'SimpleCheckbox';

View File

@@ -1,23 +1,44 @@
import React from 'react'; import React from 'react';
import { Box } from './primitives/Box'; import { Box } from './primitives/Box';
interface SkeletonProps { export interface SkeletonProps {
width?: string | number; width?: string | number;
height?: string | number; height?: string | number;
circle?: boolean; variant?: 'text' | 'circular' | 'rectangular';
className?: string; 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 ( return (
<Box <Box
w={width} width={width}
h={height} height={height}
rounded={circle ? 'full' : 'md'} className={classes}
bg="bg-white/5"
className={`animate-pulse ${className}`}
role="status" role="status"
aria-label="Loading..." aria-label="Loading..."
/> />
); );
} };

View File

@@ -1,30 +1,38 @@
import React from 'react'; import React from 'react';
import { LucideIcon } from 'lucide-react'; import { Surface } from './primitives/Surface';
import { Stack } from './primitives/Stack';
import { Box } from './primitives/Box'; import { Box } from './primitives/Box';
import { Text } from './Text'; import { Text } from './Text';
import { Icon } from './Icon'; import { Icon } from './Icon';
import { Surface } from './primitives/Surface'; import { LucideIcon } from 'lucide-react';
interface StatBoxProps { export interface StatBoxProps {
icon: LucideIcon;
label: string; label: string;
value: string | number; 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 ( return (
<Surface variant="muted" rounded="xl" border padding={4}> <Surface variant="muted" rounded="xl" padding={4} style={{ border: '1px solid var(--ui-color-border-default)' }}>
<Stack direction="row" align="center" gap={3}> <Box display="flex" alignItems="center" gap={4}>
<Surface variant="muted" rounded="lg" padding={2}> <Box padding={2} rounded="lg" bg="var(--ui-color-bg-surface-muted)">
<Icon icon={icon} size={5} color={color} /> <Icon icon={icon} size={5} intent={intent} />
</Surface>
<Box>
<Text size="2xl" weight="bold" color="text-white" block>{value}</Text>
<Text size="xs" color="text-gray-500" block>{label}</Text>
</Box> </Box>
</Stack> <Box>
<Text size="xs" weight="bold" variant="low" uppercase>
{label}
</Text>
<Text size="xl" weight="bold" variant="high" block marginTop={0.5}>
{value}
</Text>
</Box>
</Box>
</Surface> </Surface>
); );
} };

View File

@@ -1,115 +1,68 @@
import React, { ReactNode } from 'react'; import React, { ReactNode } from 'react';
import { Box } from './primitives/Box';
import { Stack } from './primitives/Stack';
import { Text } from './Text';
import { Card } from './Card'; import { Card } from './Card';
import { Box } from './primitives/Box';
import { Text } from './Text';
import { Icon } from './Icon'; import { Icon } from './Icon';
import { LucideIcon } from 'lucide-react'; import { LucideIcon } from 'lucide-react';
interface StatCardProps { export interface StatCardProps {
label: string; label: string;
value: string | number; value: string | number;
icon?: LucideIcon; icon?: LucideIcon;
intent?: 'primary' | 'success' | 'warning' | 'critical' | 'telemetry';
trend?: { trend?: {
value: number; value: number;
isPositive: boolean; isPositive: boolean;
}; };
variant?: 'default' | 'primary' | 'success' | 'warning' | 'danger' | 'info'; footer?: ReactNode;
className?: string;
onClick?: () => void;
prefix?: string;
suffix?: string;
delay?: number;
} }
export function StatCard({ export const StatCard = ({
label, label,
value, value,
icon, icon,
intent = 'primary',
trend, trend,
variant = 'default', footer
className = '', }: StatCardProps) => {
onClick, return (
prefix, <Card variant="default">
suffix, <Box display="flex" alignItems="start" justifyContent="between" marginBottom={4}>
delay, <Box>
}: StatCardProps) { <Text size="xs" weight="bold" variant="low" uppercase>
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 = (
<Card variant="default" p={5} className={`${variantClasses[variant]} ${className} h-full`}>
<Stack gap={3}>
<Stack direction="row" align="center" justify="between">
<Text size="xs" weight="bold" color="text-gray-500" uppercase letterSpacing="widest">
{label} {label}
</Text> </Text>
{icon && ( <Text size="2xl" weight="bold" variant="high" block marginTop={1}>
<Box {value}
p={2}
rounded="lg"
bg={iconBgClasses[variant]}
className={iconColorClasses[variant]}
>
<Icon icon={icon} size={5} />
</Box>
)}
</Stack>
<Stack gap={1}>
<Text size="3xl" weight="bold" color="text-white">
{prefix}{value}{suffix}
</Text> </Text>
{trend && ( </Box>
<Stack direction="row" align="center" gap={1}> {icon && (
<Text <Box padding={2} rounded="lg" bg="var(--ui-color-bg-surface-muted)">
size="xs" <Icon icon={icon} size={5} intent={intent} />
weight="bold" </Box>
color={trend.isPositive ? 'text-success-green' : 'text-critical-red'} )}
> </Box>
{trend.isPositive ? '+' : ''}{trend.value}%
</Text> {trend && (
<Text size="xs" color="text-gray-500"> <Box display="flex" alignItems="center" gap={1} marginBottom={footer ? 4 : 0}>
vs last period <Text
</Text> size="xs"
</Stack> weight="bold"
)} variant={trend.isPositive ? 'success' : 'critical'}
</Stack> >
</Stack> {trend.isPositive ? '+' : '-'}{Math.abs(trend.value)}%
</Text>
<Text size="xs" variant="low">
vs last period
</Text>
</Box>
)}
{footer && (
<Box borderTop paddingTop={4}>
{footer}
</Box>
)}
</Card> </Card>
); );
};
if (onClick) {
return (
<Box as="button" onClick={onClick} w="full" textAlign="left" className="focus:outline-none">
{cardContent}
</Box>
);
}
return cardContent;
}

View File

@@ -1,58 +1,21 @@
import React from 'react'; import React from 'react';
import { Grid } from './primitives/Grid'; import { Grid } from './primitives/Grid';
import { GridItem } from './primitives/GridItem'; import { StatBox, StatBoxProps } from './StatBox';
import { Surface } from './primitives/Surface';
import { Text } from './Text';
import { Stack } from './primitives/Stack';
type GridCols = 1 | 2 | 3 | 4 | 5 | 6 | 12; export interface StatGridProps {
stats: StatBoxProps[];
interface StatItem { columns?: number;
label: string;
value: string | number;
subValue?: string;
color?: string;
icon?: React.ElementType;
} }
interface StatGridProps { export const StatGrid = ({
stats: StatItem[]; stats,
cols?: GridCols; columns = 3
mdCols?: GridCols; }: StatGridProps) => {
lgCols?: GridCols;
className?: string;
}
export function StatGrid({ stats, cols = 2, mdCols = 3, lgCols = 4, className = '' }: StatGridProps) {
return ( return (
<Grid <Grid columns={columns} gap={4}>
cols={cols}
mdCols={mdCols}
lgCols={lgCols}
gap={4}
className={className}
>
{stats.map((stat, index) => ( {stats.map((stat, index) => (
<GridItem key={index}> <StatBox key={index} {...stat} />
<Surface variant="muted" padding={4} rounded="lg" border className="h-full">
<Stack gap={1}>
<Text size="xs" color="text-gray-500" uppercase weight="semibold" letterSpacing="wider">
{stat.label}
</Text>
<Stack direction="row" align="baseline" gap={2}>
<Text size="2xl" weight="bold" font="mono" color={stat.color || 'text-white'}>
{stat.value}
</Text>
{stat.subValue && (
<Text size="xs" color="text-gray-500" font="mono">
{stat.subValue}
</Text>
)}
</Stack>
</Stack>
</Surface>
</GridItem>
))} ))}
</Grid> </Grid>
); );
} };

View File

@@ -1,36 +1,36 @@
import React, { ReactNode } from 'react'; import React from 'react';
import { Box } from './primitives/Box'; import { Box } from './primitives/Box';
import { Stack } from './primitives/Stack';
import { Text } from './Text'; import { Text } from './Text';
import { Icon } from './Icon'; import { Stack } from './primitives/Stack';
import { LucideIcon } from 'lucide-react';
interface StatGridItemProps { export interface StatGridItemProps {
label: string; label: string;
value: string | number; value: string | number;
icon?: LucideIcon; icon?: React.ReactNode;
color?: string; intent?: 'primary' | 'success' | 'warning' | 'critical' | 'high' | 'med' | 'low';
color?: string; // Alias for intent
} }
/** export const StatGridItem = ({
* StatGridItem label,
* value,
* A simple stat display for use in a grid. icon,
*/ intent = 'high',
export function StatGridItem({ label, value, icon, color = 'text-primary-blue' }: StatGridItemProps) { color
}: StatGridItemProps) => {
return ( return (
<Box p={4} textAlign="center"> <Box padding={4} textAlign="center">
{icon && ( {icon && (
<Stack direction="row" align="center" justify="center" gap={2} mb={1} color={color}> <Stack direction="row" align="center" justify="center" gap={2} marginBottom={1}>
<Icon icon={icon} size={4} /> {icon}
</Stack> </Stack>
)} )}
<Text size="2xl" weight="bold" color="text-white" block> <Text size="2xl" weight="bold" variant={intent} color={color} block>
{value} {value}
</Text> </Text>
<Text size="xs" weight="medium" color="text-gray-500" uppercase letterSpacing="wider"> <Text size="xs" weight="medium" variant="low" uppercase letterSpacing="wider">
{label} {label}
</Text> </Text>
</Box> </Box>
); );
} };

View File

@@ -2,18 +2,23 @@ import React from 'react';
import { Box } from './primitives/Box'; import { Box } from './primitives/Box';
import { Text } from './Text'; import { Text } from './Text';
interface StatItemProps { export interface StatItemProps {
label: string; label: string;
value: string | number; value: string | number;
color?: string; intent?: 'primary' | 'success' | 'warning' | 'critical' | 'high' | 'med' | 'low';
align?: 'left' | 'center' | 'right'; 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 ( return (
<Box display="flex" flexDirection="column" alignItems={align === 'center' ? 'center' : align === 'right' ? 'flex-end' : 'flex-start'}> <Box textAlign={align}>
<Text size="xs" color="text-gray-500" block mb={0.5}>{label}</Text> <Text size="xs" variant="low" block marginBottom={0.5}>{label}</Text>
<Text size="sm" weight="semibold" color={color}>{value}</Text> <Text size="sm" weight="semibold" variant={intent}>{value}</Text>
</Box> </Box>
); );
} };

View File

@@ -3,36 +3,36 @@ import { Icon } from './Icon';
import { LucideIcon } from 'lucide-react'; import { LucideIcon } from 'lucide-react';
import { Stack } from './primitives/Stack'; import { Stack } from './primitives/Stack';
interface StatusBadgeProps { export interface StatusBadgeProps {
children: React.ReactNode; children: React.ReactNode;
variant?: 'success' | 'warning' | 'error' | 'info' | 'neutral' | 'pending'; variant?: 'success' | 'warning' | 'error' | 'info' | 'neutral' | 'pending';
className?: string;
icon?: LucideIcon; icon?: LucideIcon;
className?: string;
} }
export function StatusBadge({ export function StatusBadge({
children, children,
variant = 'success', variant = 'success',
className = '',
icon, icon,
className
}: StatusBadgeProps) { }: StatusBadgeProps) {
const variantClasses = { const variantClasses = {
success: 'bg-performance-green/20 text-performance-green border-performance-green/30', success: 'bg-[var(--ui-color-intent-success)]/20 text-[var(--ui-color-intent-success)] border-[var(--ui-color-intent-success)]/30',
warning: 'bg-warning-amber/20 text-warning-amber border-warning-amber/30', warning: 'bg-[var(--ui-color-intent-warning)]/20 text-[var(--ui-color-intent-warning)] border-[var(--ui-color-intent-warning)]/30',
error: 'bg-red-600/20 text-red-400 border-red-600/30', error: 'bg-[var(--ui-color-intent-critical)]/20 text-[var(--ui-color-intent-critical)] border-[var(--ui-color-intent-critical)]/30',
info: 'bg-blue-500/20 text-blue-400 border-blue-500/30', info: 'bg-[var(--ui-color-intent-primary)]/20 text-[var(--ui-color-intent-primary)] border-[var(--ui-color-intent-primary)]/30',
neutral: 'bg-iron-gray text-gray-400 border-charcoal-outline', neutral: 'bg-[var(--ui-color-bg-surface-muted)] text-[var(--ui-color-text-med)] border-[var(--ui-color-border-default)]',
pending: 'bg-warning-amber/20 text-warning-amber border-warning-amber/30', pending: 'bg-[var(--ui-color-intent-warning)]/20 text-[var(--ui-color-intent-warning)] border-[var(--ui-color-intent-warning)]/30',
}; };
const classes = [ 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], variantClasses[variant],
className className
].filter(Boolean).join(' '); ].join(' ');
const content = icon ? ( const content = icon ? (
<Stack direction="row" align="center" gap={1.5}> <Stack direction="row" align="center" gap={1}>
<Icon icon={icon} size={3} /> <Icon icon={icon} size={3} />
{children} {children}
</Stack> </Stack>
@@ -43,4 +43,4 @@ export function StatusBadge({
{content} {content}
</span> </span>
); );
} }

View File

@@ -1,11 +1,11 @@
import React from 'react'; import React from 'react';
import { Box } from './primitives/Box'; import { Box } from './primitives/Box';
interface StatusDotProps { export interface StatusDotProps {
color?: string; intent?: 'primary' | 'success' | 'warning' | 'critical' | 'telemetry';
color?: string; // Alias for intent or custom color
pulse?: boolean; pulse?: boolean;
size?: number; size?: 'sm' | 'md' | 'lg' | number;
className?: string;
} }
/** /**
@@ -14,28 +14,41 @@ interface StatusDotProps {
* A simple status indicator dot with optional pulse effect. * A simple status indicator dot with optional pulse effect.
*/ */
export function StatusDot({ export function StatusDot({
color = '#4ED4E0', intent = 'telemetry',
color: colorProp,
pulse = false, pulse = false,
size = 2, size = 'md',
className = ''
}: StatusDotProps) { }: 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 ( return (
<Box position="relative" className={`${sizeClass} ${className}`}> <Box position="relative" width={dimension} height={dimension}>
<Box <Box
w="full" fullWidth
h="full" fullHeight
rounded="full" style={{ backgroundColor: color, borderRadius: '9999px' }}
style={{ backgroundColor: color }}
/> />
{pulse && ( {pulse && (
<Box <Box
position="absolute" position="absolute"
inset={0} inset={0}
rounded="full"
className="animate-ping" className="animate-ping"
style={{ backgroundColor: color, opacity: 0.75 }} style={{ backgroundColor: color, opacity: 0.75, borderRadius: '9999px' }}
/> />
)} )}
</Box> </Box>

View File

@@ -1,110 +1,94 @@
import { LucideIcon } from 'lucide-react';
import React from 'react'; import React from 'react';
import { Box } from './primitives/Box'; import { Box } from './primitives/Box';
import { Text } from './Text'; import { Text } from './Text';
import { Icon } from './Icon'; import { StatusDot } from './StatusDot';
import { Badge } from './Badge';
interface StatusIndicatorProps { export { Badge };
icon: LucideIcon;
label: string; export interface StatusIndicatorProps {
status?: 'live' | 'upcoming' | 'completed' | 'cancelled' | 'pending';
variant?: string; // Alias for status
label?: string;
subLabel?: 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) { export const StatusIndicator = ({
const colors = { status,
success: { variant,
text: 'text-performance-green', label,
bg: 'bg-green-500/10', subLabel,
border: 'border-green-500/30', size = 'md',
icon: 'rgb(16, 185, 129)' icon
}: StatusIndicatorProps) => {
const activeStatus = (status || variant || 'pending') as any;
const configMap: any = {
live: {
intent: 'success' as const,
pulse: true,
text: 'Live',
}, },
warning: { upcoming: {
text: 'text-warning-amber', intent: 'primary' as const,
bg: 'bg-yellow-500/10', pulse: false,
border: 'border-yellow-500/30', text: 'Upcoming',
icon: 'rgb(245, 158, 11)' },
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: { danger: {
text: 'text-red-400', intent: 'critical' as const,
bg: 'bg-red-500/10', pulse: false,
border: 'border-red-500/30', text: 'Danger',
icon: 'rgb(239, 68, 68)' },
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 ( return (
<Box <Box display="flex" alignItems="center" gap={2}>
display="flex" <StatusDot intent={config.intent} pulse={config.pulse} size={size === 'lg' ? 'lg' : size === 'sm' ? 'sm' : 'md'} />
alignItems="center" <Box>
justifyContent="between" <Text size={size === 'lg' ? 'md' : 'sm'} weight="bold" variant="high" uppercase>
p={2} {label || config.text}
rounded="lg"
bg={config.bg}
border
borderColor={config.border}
>
<Box display="flex" alignItems="center" gap={2}>
<Icon icon={icon} size={4} color={config.icon} />
<Text size="sm" weight="semibold" color="text-white">{label}</Text>
</Box>
{subLabel && (
<Text size="xs" color="text-gray-400">
{subLabel}
</Text> </Text>
)} {subLabel && <Text size="xs" variant="low">{subLabel}</Text>}
</Box>
</Box> </Box>
); );
} };
interface StatRowProps { export const StatRow = ({ label, value, subLabel, variant, valueColor, valueFont }: { label: string, value: string, subLabel?: string, variant?: string, valueColor?: string, valueFont?: string }) => (
label: string; <Box display="flex" alignItems="center" justifyContent="between" paddingY={2} borderBottom>
value: string | number; <Box>
valueColor?: string; <Text size="sm" variant="high">{label}</Text>
valueFont?: 'sans' | 'mono'; {subLabel && <Text size="xs" variant="low">{subLabel}</Text>}
}
export function StatRow({ label, value, valueColor = 'text-white', valueFont = 'sans' }: StatRowProps) {
return (
<Box display="flex" alignItems="center" justifyContent="between">
<Text size="xs" color="text-gray-500">{label}</Text>
<Text size="xs" weight="bold" color={valueColor} font={valueFont}>
{value}
</Text>
</Box> </Box>
); <Text size="sm" weight="bold" variant={variant as any || 'high'} color={valueColor} font={valueFont as any}>{value}</Text>
} </Box>
);
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 (
<Box px={1} rounded="sm" bg={variants[variant]}>
<Text size={size} color="inherit">
{children}
</Text>
</Box>
);
}

View File

@@ -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 { export interface Step {
id: number; id: string;
label: string; label: string;
icon: React.ElementType;
} }
interface StepIndicatorProps { export interface StepIndicatorProps {
currentStep: number; steps: Step[];
steps?: Step[]; currentStepId: string;
completedStepIds: string[];
} }
const DEFAULT_STEPS: Step[] = [ export const StepIndicator = ({
{ id: 1, label: 'Personal', icon: User }, steps,
{ id: 2, label: 'Avatar', icon: Camera }, currentStepId,
]; completedStepIds
}: StepIndicatorProps) => {
export function StepIndicator({ currentStep, steps = DEFAULT_STEPS }: StepIndicatorProps) {
return ( return (
<div className="flex items-center justify-center gap-2 mb-8"> <Box display="flex" alignItems="center" gap={4}>
{steps.map((step, index) => { {steps.map((step, index) => {
const Icon = step.icon; const isCurrent = step.id === currentStepId;
const isCompleted = step.id < currentStep; const isCompleted = completedStepIds.includes(step.id);
const isCurrent = step.id === currentStep; const isLast = index === steps.length - 1;
return ( return (
<div key={step.id} className="flex items-center"> <React.Fragment key={step.id}>
<div className="flex flex-col items-center"> <Box display="flex" alignItems="center" gap={2}>
<div <Box
className={`flex h-12 w-12 items-center justify-center rounded-full transition-all duration-300 ${ width="2rem"
isCurrent height="2rem"
? 'bg-primary-blue text-white shadow-lg shadow-primary-blue/30' display="flex"
: isCompleted alignItems="center"
? 'bg-performance-green text-white' justifyContent="center"
: 'bg-iron-gray border border-charcoal-outline text-gray-500' rounded="full"
}`} bg={isCompleted ? 'var(--ui-color-intent-success)' : isCurrent ? 'var(--ui-color-intent-primary)' : 'var(--ui-color-bg-surface-muted)'}
style={{ border: isCurrent ? '2px solid var(--ui-color-intent-primary)' : 'none' }}
> >
{isCompleted ? ( {isCompleted ? (
<Check className="w-5 h-5" /> <Icon icon={Check} size={4} intent="high" />
) : ( ) : (
<Icon className="w-5 h-5" /> <Text size="xs" weight="bold" variant={isCurrent ? 'high' : 'low'}>
{index + 1}
</Text>
)} )}
</div> </Box>
<span <Text size="sm" weight={isCurrent ? 'bold' : 'medium'} variant={isCurrent ? 'high' : 'low'}>
className={`mt-2 text-xs font-medium ${
isCurrent ? 'text-white' : isCompleted ? 'text-performance-green' : 'text-gray-500'
}`}
>
{step.label} {step.label}
</span> </Text>
</div> </Box>
{index < steps.length - 1 && ( {!isLast && (
<div <Box flex={1} height="2px" bg="var(--ui-color-border-muted)" minWidth="2rem" />
className={`w-16 h-0.5 mx-4 mt-[-20px] ${
isCompleted ? 'bg-performance-green' : 'bg-charcoal-outline'
}`}
/>
)} )}
</div> </React.Fragment>
); );
})} })}
</div> </Box>
); );
} };

View File

@@ -1,56 +1,46 @@
import React from 'react'; import React, { ReactNode } from 'react';
import { Box } from './primitives/Box'; import { Box } from './primitives/Box';
import { Text } from './Text'; import { Text } from './Text';
import { Surface } from './primitives/Surface'; import { Surface } from './primitives/Surface';
import { Icon } from './Icon'; import { Icon } from './Icon';
import { LucideIcon } from 'lucide-react'; import { LucideIcon } from 'lucide-react';
interface SummaryItemProps { export interface SummaryItemProps {
label?: string; label: string;
value?: string | number; value: string | number;
icon?: LucideIcon; icon: LucideIcon;
intent?: 'primary' | 'success' | 'warning' | 'critical' | 'telemetry';
onClick?: () => void; 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 ( return (
<Surface <Surface
variant="muted" variant="muted"
rounded="lg" rounded="lg"
p={4} padding={4}
display="flex"
alignItems="center"
gap={4}
cursor={onClick ? 'pointer' : 'default'}
onClick={onClick} onClick={onClick}
hoverBg={onClick ? 'bg-white/5' : undefined} style={{ cursor: onClick ? 'pointer' : 'default' }}
transition={!!onClick}
> >
{icon && ( <Box display="flex" alignItems="center" gap={4}>
<Box p={2} rounded="lg" bg="bg-white/5"> <Box padding={2} rounded="lg" bg="var(--ui-color-bg-surface-muted)">
<Icon icon={icon} size={5} color="text-gray-400" /> <Icon icon={icon} size={5} intent={intent} />
</Box> </Box>
)}
<Box flex={1}>
{(label || title) && (
<Text size="xs" color="text-gray-500" uppercase letterSpacing="wider" block>
{label || title}
</Text>
)}
{(value || subtitle) && (
<Text size="lg" weight="bold" color="text-white" block>
{value || subtitle}
</Text>
)}
</Box>
{rightContent && (
<Box> <Box>
{rightContent} <Text size="xs" weight="bold" variant="low" uppercase>
{label}
</Text>
<Text size="lg" weight="bold" variant="high" block marginTop={0.5}>
{value}
</Text>
</Box> </Box>
)} </Box>
</Surface> </Surface>
); );
} };

View File

@@ -1,68 +1,46 @@
import React from 'react'; import React from 'react';
import { Box } from './primitives/Box'; import { Box } from './primitives/Box';
import { Stack } from './primitives/Stack';
import { Surface } from './primitives/Surface';
import { Text } from './Text'; import { Text } from './Text';
import { Surface } from './primitives/Surface';
interface Tab { export interface TabNavigationOption {
id: string; id: string;
label: string; label: string;
icon?: React.ReactNode; icon?: React.ReactNode;
} }
interface TabNavigationProps { export interface TabNavigationProps {
tabs: Tab[]; options: TabNavigationOption[];
activeTab: string; activeId: string;
onTabChange: (tabId: string) => void; onChange: (id: string) => void;
className?: string;
} }
export function TabNavigation({ tabs, activeTab, onTabChange, className = '' }: TabNavigationProps) { export const TabNavigation = ({
options,
activeId,
onChange
}: TabNavigationProps) => {
return ( return (
<Surface <Surface variant="muted" rounded="xl" padding={1} display="inline-flex">
variant="muted" {options.map((option) => {
rounded="xl" const isActive = option.id === activeId;
p={1} return (
display="inline-flex" <button
zIndex={10} key={option.id}
className={className} onClick={() => onChange(option.id)}
> className={`px-4 py-2 text-xs font-bold uppercase tracking-widest transition-all rounded-lg ${
<Stack direction="row" gap={1}> isActive
{tabs.map((tab) => { ? 'bg-[var(--ui-color-bg-surface)] text-[var(--ui-color-intent-primary)] shadow-sm'
const isActive = activeTab === tab.id; : 'text-[var(--ui-color-text-low)] hover:text-[var(--ui-color-text-high)]'
return ( }`}
<Surface >
key={tab.id} <Box display="flex" alignItems="center" gap={2}>
as="button" {option.icon}
onClick={() => onTabChange(tab.id)} {option.label}
variant={isActive ? 'default' : 'ghost'} </Box>
bg={isActive ? 'bg-primary-blue' : ''} </button>
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'}`}
>
<Stack direction="row" align="center" gap={2}>
{tab.icon && (
<Box color={isActive ? 'text-white' : 'text-gray-400'} groupHoverTextColor={!isActive ? 'white' : undefined}>
{tab.icon}
</Box>
)}
<Text
size="sm"
weight="medium"
color={isActive ? 'text-white' : 'text-gray-400'}
groupHoverTextColor={!isActive ? 'white' : undefined}
>
{tab.label}
</Text>
</Stack>
</Surface>
);
})}
</Stack>
</Surface> </Surface>
); );
} };

View File

@@ -1,95 +1,87 @@
import React, { ReactNode, ElementType } from 'react'; import React, { ReactNode } from 'react';
import { Box, BoxProps } from './primitives/Box'; import { Box } from './primitives/Box';
import { Surface } from './primitives/Surface';
interface TableProps extends BoxProps<'table'> { export interface TableProps {
children: ReactNode; children: ReactNode;
className?: string;
} }
export function Table({ children, className = '', ...props }: TableProps) { export const Table = ({ children, className }: TableProps) => {
const { border, translate, ...rest } = props;
return ( return (
<Box overflow="auto" border borderColor="border-border-gray" rounded="sm"> <Surface rounded="lg" shadow="sm" style={{ overflow: 'auto', border: '1px solid var(--ui-color-border-default)' }} className={className}>
<table className={`w-full border-collapse text-left ${className}`} {...(rest as any)}> <table className="w-full border-collapse text-left">
{children} {children}
</table> </table>
</Box> </Surface>
); );
} };
interface TableHeaderProps extends BoxProps<'thead'> { export const TableHeader = ({ children, className, textAlign, w }: { children: ReactNode, className?: string, textAlign?: 'left' | 'center' | 'right', w?: string }) => {
children: ReactNode;
}
export function TableHeader({ children, className = '', ...props }: TableHeaderProps) {
return ( return (
<Box as="thead" className={`bg-graphite-black border-b border-border-gray ${className}`} {...props}> <thead className={`bg-[var(--ui-color-bg-base)] border-b border-[var(--ui-color-border-default)] ${className || ''}`}>
{children} <tr>
</Box> {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;
})}
</tr>
</thead>
); );
} };
export const TableHead = TableHeader; export const TableHead = TableHeader;
interface TableBodyProps extends BoxProps<'tbody'> { export const TableBody = ({ children }: { children: ReactNode }) => {
children: ReactNode;
}
export function TableBody({ children, className = '', ...props }: TableBodyProps) {
return ( return (
<Box as="tbody" className={`divide-y divide-border-gray/50 ${className}`} {...props}> <tbody className="divide-y divide-[var(--ui-color-border-muted)]">
{children} {children}
</Box> </tbody>
); );
} };
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 ( return (
<Box as="tr" className={classes} {...props}> <tr
className={`${isClickable ? 'cursor-pointer hover:bg-white/5 transition-colors' : ''} ${variant === 'highlight' ? 'bg-white/5' : ''} ${className || ''}`}
onClick={onClick}
style={bg ? { backgroundColor: bg.startsWith('bg-') ? undefined : bg } : undefined}
{...props}
>
{children} {children}
</Box> </tr>
); );
} };
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 ( return (
<Box as="th" className={classes} {...props}> <th
className={`px-4 py-3 text-xs font-bold uppercase tracking-wider text-[var(--ui-color-text-low)] ${alignClass} ${className || ''}`}
style={w ? { width: w } : undefined}
>
{children} {children}
</Box> </th>
); );
} };
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 ( return (
<Box as="td" className={classes} {...props}> <td
className={`px-4 py-3 text-sm text-[var(--ui-color-text-high)] ${alignClass} ${className || ''}`}
colSpan={colSpan}
style={{
...(py !== undefined ? { paddingTop: `${py * 0.25}rem`, paddingBottom: `${py * 0.25}rem` } : {}),
...(w ? { width: w } : {}),
...(position ? { position: position as any } : {}),
}}
{...props}
>
{children} {children}
</Box> </td>
); );
} };

View File

@@ -1,206 +1,121 @@
import React, { ReactNode, ElementType, ComponentPropsWithoutRef } from 'react'; import React, { ReactNode, forwardRef, ElementType } from 'react';
import { Box, BoxProps } from './primitives/Box'; 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'; export interface TextProps extends BoxProps<any> {
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<T extends ElementType = 'span'> extends Omit<BoxProps<T>, 'children' | 'className' | 'size'> {
as?: T;
children: ReactNode; children: ReactNode;
className?: string; variant?: 'high' | 'med' | 'low' | 'primary' | 'success' | 'warning' | 'critical' | 'inherit';
size?: TextSize | ResponsiveTextSize; size?: TextSize | ResponsiveValue<TextSize>;
weight?: 'light' | 'normal' | 'medium' | 'semibold' | 'bold' | string; weight?: 'light' | 'normal' | 'medium' | 'semibold' | 'bold';
color?: string; as?: ElementType;
font?: 'mono' | 'sans' | string; align?: 'left' | 'center' | 'right';
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;
italic?: boolean; italic?: boolean;
lineClamp?: number; mono?: boolean;
ml?: Spacing | ResponsiveSpacing; block?: boolean;
mr?: Spacing | ResponsiveSpacing; uppercase?: boolean;
mt?: Spacing | ResponsiveSpacing; letterSpacing?: string;
mb?: Spacing | ResponsiveSpacing; leading?: 'none' | 'tight' | 'snug' | 'normal' | 'relaxed' | 'loose';
truncate?: boolean;
lineHeight?: string | number;
font?: 'sans' | 'mono';
hoverTextColor?: string;
} }
interface ResponsiveSpacing { export const Text = forwardRef<HTMLElement, TextProps>(({
base?: Spacing;
sm?: Spacing;
md?: Spacing;
lg?: Spacing;
xl?: Spacing;
'2xl'?: Spacing;
}
export function Text<T extends ElementType = 'span'>({
as,
children, children,
className = '', variant = 'med',
size = 'base', size = 'md',
weight = 'normal', weight = 'normal',
color = '', as = 'p',
font = 'sans',
align = 'left', align = 'left',
truncate = false, italic = false,
mono = false,
block = false,
uppercase = false, uppercase = false,
capitalize = false,
letterSpacing, letterSpacing,
leading, leading,
fontSize, truncate = false,
style, lineHeight,
block = false, font,
italic = false, hoverTextColor,
lineClamp,
ml, mr, mt, mb,
...props ...props
}: TextProps<T> & ComponentPropsWithoutRef<T>) { }, ref) => {
const Tag = (as as ElementType) || 'span'; const variantClasses = {
high: 'text-[var(--ui-color-text-high)]',
const sizeClasses: Record<string, string> = { 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<TextSize, string> = {
xs: 'text-xs', xs: 'text-xs',
sm: 'text-sm', sm: 'text-sm',
base: 'text-base', base: 'text-base',
md: 'text-base',
lg: 'text-lg', lg: 'text-lg',
xl: 'text-xl', xl: 'text-xl',
'2xl': 'text-2xl', '2xl': 'text-2xl',
'3xl': 'text-3xl', '3xl': 'text-3xl',
'4xl': 'text-4xl' '4xl': 'text-4xl',
}; };
const getSizeClasses = (value: TextSize | ResponsiveTextSize | undefined) => { const getResponsiveSizeClasses = (value: TextSize | ResponsiveValue<TextSize>) => {
if (value === undefined) return ''; if (typeof value === 'string') return sizeMap[value];
if (typeof value === 'object') { const classes = [];
const classes = []; if (value.base) classes.push(sizeMap[value.base]);
if (value.base) classes.push(sizeClasses[value.base]); if (value.sm) classes.push(`sm:${sizeMap[value.sm]}`);
if (value.sm) classes.push(`sm:${sizeClasses[value.sm]}`); if (value.md) classes.push(`md:${sizeMap[value.md]}`);
if (value.md) classes.push(`md:${sizeClasses[value.md]}`); if (value.lg) classes.push(`lg:${sizeMap[value.lg]}`);
if (value.lg) classes.push(`lg:${sizeClasses[value.lg]}`); if (value.xl) classes.push(`xl:${sizeMap[value.xl]}`);
if (value.xl) classes.push(`xl:${sizeClasses[value.xl]}`); return classes.join(' ');
if (value['2xl']) classes.push(`2xl:${sizeClasses[value['2xl']]}`);
return classes.join(' ');
}
return sizeClasses[value];
}; };
const weightClasses: Record<string, string> = { const weightClasses = {
light: 'font-light', light: 'font-light',
normal: 'font-normal', normal: 'font-normal',
medium: 'font-medium', medium: 'font-medium',
semibold: 'font-semibold', semibold: 'font-semibold',
bold: 'font-bold' bold: 'font-bold',
};
const fontClasses: Record<string, string> = {
mono: 'font-mono',
sans: 'font-sans'
};
const alignClasses: Record<string, string> = {
left: 'text-left',
center: 'text-center',
right: 'text-right'
}; };
const getAlignClasses = (value: TextAlign | ResponsiveTextAlign | undefined) => { const leadingClasses = {
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<number, string> = {
0: '0', 0.5: '0.5', 1: '1', 1.5: '1.5', 2: '2', 2.5: '2.5', 3: '3', 3.5: '3.5', 4: '4',
5: '5', 6: '6', 7: '7', 8: '8', 9: '9', 10: '10', 11: '11', 12: '12', 14: '14',
16: '16', 20: '20', 24: '24', 28: '28', 32: '32', 36: '36', 40: '40', 44: '44',
48: '48', 52: '52', 56: '56', 60: '60', 64: '64', 72: '72', 80: '80', 96: '96'
};
const leadingClasses: Record<string, string> = {
none: 'leading-none', none: 'leading-none',
tight: 'leading-tight', tight: 'leading-tight',
snug: 'leading-snug', snug: 'leading-snug',
normal: 'leading-normal', normal: 'leading-normal',
relaxed: 'leading-relaxed', 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 = [ const classes = [
block ? 'block' : 'inline', variantClasses[variant],
getSizeClasses(size), getResponsiveSizeClasses(size),
weightClasses[weight] || '', weightClasses[weight],
fontClasses[font] || '', align === 'center' ? 'text-center' : (align === 'right' ? 'text-right' : 'text-left'),
getAlignClasses(align),
leading ? leadingClasses[leading] : '',
color,
truncate ? 'truncate' : '',
uppercase ? 'uppercase' : '',
capitalize ? 'capitalize' : '',
italic ? 'italic' : '', italic ? 'italic' : '',
lineClamp ? `line-clamp-${lineClamp}` : '', (mono || font === 'mono') ? 'font-mono' : 'font-sans',
letterSpacing === '0.05em' ? 'tracking-wider' : letterSpacing ? `tracking-${letterSpacing}` : '', block ? 'block' : 'inline',
getSpacingClass('ml', ml), uppercase ? 'uppercase tracking-wider' : '',
getSpacingClass('mr', mr), leading ? leadingClasses[leading] : '',
getSpacingClass('mt', mt), truncate ? 'truncate' : '',
getSpacingClass('mb', mb), hoverTextColor ? `hover:text-${hoverTextColor}` : '',
className ].join(' ');
].filter(Boolean).join(' ');
const style: React.CSSProperties = {
const combinedStyle = { ...(letterSpacing ? { letterSpacing } : {}),
...(fontSize ? { fontSize } : {}), ...(lineHeight ? { lineHeight } : {}),
...(weight && !weightClasses[weight] ? { fontWeight: weight } : {}),
...(font && !fontClasses[font] ? { fontFamily: font } : {}),
...style
}; };
return <Box as={Tag} className={classes} style={combinedStyle} {...props}>{children}</Box>; return (
} <Box as={as} ref={ref} className={classes} style={style} {...props}>
{children}
</Box>
);
});
Text.displayName = 'Text';

View File

@@ -1,49 +1,61 @@
import React, { forwardRef } from 'react'; import React, { forwardRef, TextareaHTMLAttributes } from 'react';
import { Box } from './primitives/Box'; import { Box } from './primitives/Box';
import { Stack } from './primitives/Stack';
import { Text } from './Text'; import { Text } from './Text';
interface TextAreaProps extends React.TextareaHTMLAttributes<HTMLTextAreaElement> { export interface TextAreaProps extends TextareaHTMLAttributes<HTMLTextAreaElement> {
label?: string; label?: string;
errorMessage?: string; error?: string;
variant?: 'default' | 'error'; hint?: string;
fullWidth?: boolean; fullWidth?: boolean;
} }
export const TextArea = forwardRef<HTMLTextAreaElement, TextAreaProps>( export const TextArea = forwardRef<HTMLTextAreaElement, TextAreaProps>(({
({ label, errorMessage, variant = 'default', fullWidth = true, className = '', ...props }, ref) => { label,
const isError = variant === 'error' || !!errorMessage; error,
hint,
return ( fullWidth = false,
<Stack gap={1.5} fullWidth={fullWidth}> ...props
{label && ( }, ref) => {
<Text as="label" size="xs" weight="bold" color="text-gray-500" uppercase letterSpacing="wider"> 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 (
<Box width={fullWidth ? '100%' : undefined}>
{label && (
<Box marginBottom={1.5}>
<Text as="label" size="xs" weight="bold" variant="low">
{label} {label}
</Text> </Text>
)}
<Box position="relative" fullWidth={fullWidth}>
<Box
as="textarea"
ref={ref}
fullWidth={fullWidth}
p={3}
bg="bg-deep-graphite"
rounded="lg"
color="text-white"
border
borderColor={isError ? 'var(--warning-amber)' : 'rgba(38, 38, 38, 0.8)'}
className={`placeholder:text-gray-500 focus:ring-2 focus:ring-primary-blue transition-all duration-150 sm:text-sm ${className}`}
{...props}
/>
{errorMessage && (
<Text size="xs" color="text-warning-amber" mt={1}>
{errorMessage}
</Text>
)}
</Box> </Box>
</Stack> )}
); <textarea
} ref={ref}
); className={classes}
{...props}
/>
{error && (
<Box marginTop={1}>
<Text size="xs" variant="critical">
{error}
</Text>
</Box>
)}
{hint && !error && (
<Box marginTop={1}>
<Text size="xs" variant="low">
{hint}
</Text>
</Box>
)}
</Box>
);
});
TextArea.displayName = 'TextArea'; TextArea.displayName = 'TextArea';

View File

@@ -1,9 +1,8 @@
import React from 'react'; import React from 'react';
import { Box } from './primitives/Box'; import { Box } from './primitives/Box';
import { Text } from './Text'; import { Text } from './Text';
import { motion } from 'framer-motion';
interface ToggleProps { export interface ToggleProps {
label: string; label: string;
description?: string; description?: string;
checked: boolean; checked: boolean;
@@ -11,64 +10,49 @@ interface ToggleProps {
disabled?: boolean; disabled?: boolean;
} }
export function Toggle({ label, description, checked, onChange, disabled }: ToggleProps) { export const Toggle = ({
label,
description,
checked,
onChange,
disabled = false
}: ToggleProps) => {
return ( return (
<Box <Box
as="label" as="label"
display="flex" display="flex"
alignItems="start" alignItems="center"
justifyContent="between" justifyContent="between"
cursor={disabled ? 'not-allowed' : 'pointer'} paddingY={3}
py={3}
borderBottom borderBottom
borderColor="border-charcoal-outline/50" style={{ cursor: disabled ? 'not-allowed' : 'pointer', opacity: disabled ? 0.5 : 1 }}
className="last:border-b-0"
opacity={disabled ? 0.5 : 1}
> >
<Box flex={1} pr={4}> <Box flex={1} paddingRight={4}>
<Text weight="medium" color="text-gray-200" block>{label}</Text> <Text weight="medium" variant="high" block>{label}</Text>
{description && ( {description && (
<Text size="xs" color="text-gray-500" block mt={1}> <Text size="xs" variant="low" block marginTop={1}>
{description} {description}
</Text> </Text>
)} )}
</Box> </Box>
<Box position="relative"> <button
<Box type="button"
as="button" role="switch"
type="button" aria-checked={checked}
role="switch" onClick={() => !disabled && onChange(!checked)}
aria-checked={checked} disabled={disabled}
onClick={() => !disabled && onChange(!checked)} className={`relative inline-flex h-6 w-11 flex-shrink-0 cursor-pointer rounded-full border-2 border-transparent transition-colors duration-200 ease-in-out focus:outline-none ${
disabled={disabled} checked ? 'bg-[var(--ui-color-intent-primary)]' : 'bg-[var(--ui-color-bg-surface-muted)]'
w="12" }`}
h="6" >
rounded="full" <span
transition="all 0.2s" aria-hidden="true"
flexShrink={0} className={`pointer-events-none inline-block h-5 w-5 transform rounded-full bg-white shadow ring-0 transition duration-200 ease-in-out ${
ring="primary-blue/50" checked ? 'translate-x-5' : 'translate-x-0'
bg={checked ? 'bg-primary-blue/20' : 'bg-charcoal-outline'} }`}
className="focus:outline-none focus:ring-2"
>
<motion.div
className="absolute inset-0 rounded-full bg-primary-blue"
initial={{ boxShadow: '0 0 0px rgba(25, 140, 255, 0)' }}
animate={{
opacity: checked ? 1 : 0,
boxShadow: checked ? '0 0 10px rgba(25, 140, 255, 0.4)' : '0 0 0px rgba(25, 140, 255, 0)'
}}
/>
</Box>
<motion.span
className="absolute top-0.5 w-5 h-5 bg-white rounded-full shadow-md"
initial={false}
animate={{
left: checked ? '26px' : '2px',
}}
transition={{ type: "spring", stiffness: 500, damping: 30 }}
/> />
</Box> </button>
</Box> </Box>
); );
} };

View File

@@ -1,11 +1,11 @@
import React, { forwardRef, ForwardedRef, ElementType, ComponentPropsWithoutRef } from 'react'; import React, { forwardRef, ForwardedRef, ElementType } from 'react';
/** /**
* WARNING: DO NOT VIOLATE THE PURPOSE OF THIS PRIMITIVE. * WARNING: DO NOT VIOLATE THE PURPOSE OF THIS PRIMITIVE.
* *
* Box is a basic container primitive for spacing, sizing and basic styling. * Box is a basic container primitive for spacing, sizing and basic layout.
* *
* - DO NOT add layout props (flex, grid, gap) - use Stack or Grid instead. * - DO NOT add decoration props (bg, border, shadow) - use Surface instead.
* - DO NOT add positioning props (absolute, top, zIndex) - create a specific component. * - DO NOT add positioning props (absolute, top, zIndex) - create a specific component.
* - DO NOT add animation props - create a specific component. * - DO NOT add animation props - create a specific component.
* *
@@ -34,8 +34,23 @@ export type ResponsiveValue<T> = {
export interface BoxProps<T extends ElementType> { export interface BoxProps<T extends ElementType> {
as?: T; as?: T;
children?: React.ReactNode; children?: React.ReactNode;
className?: string;
// Spacing // Spacing
margin?: Spacing | ResponsiveSpacing;
marginTop?: Spacing | ResponsiveSpacing;
marginBottom?: Spacing | ResponsiveSpacing;
marginLeft?: Spacing | 'auto' | ResponsiveSpacing;
marginRight?: Spacing | 'auto' | ResponsiveSpacing;
marginX?: Spacing | 'auto' | ResponsiveSpacing;
marginY?: Spacing | ResponsiveSpacing;
padding?: Spacing | ResponsiveSpacing;
paddingTop?: Spacing | ResponsiveSpacing;
paddingBottom?: Spacing | ResponsiveSpacing;
paddingLeft?: Spacing | ResponsiveSpacing;
paddingRight?: Spacing | ResponsiveSpacing;
paddingX?: Spacing | ResponsiveSpacing;
paddingY?: Spacing | ResponsiveSpacing;
// Aliases (Deprecated - use full names)
m?: Spacing | ResponsiveSpacing; m?: Spacing | ResponsiveSpacing;
mt?: Spacing | ResponsiveSpacing; mt?: Spacing | ResponsiveSpacing;
mb?: Spacing | ResponsiveSpacing; mb?: Spacing | ResponsiveSpacing;
@@ -50,11 +65,10 @@ export interface BoxProps<T extends ElementType> {
pr?: Spacing | ResponsiveSpacing; pr?: Spacing | ResponsiveSpacing;
px?: Spacing | ResponsiveSpacing; px?: Spacing | ResponsiveSpacing;
py?: Spacing | ResponsiveSpacing; py?: Spacing | ResponsiveSpacing;
// Sizing // Sizing
w?: string | number | ResponsiveValue<string | number>; width?: string | number | ResponsiveValue<string | number>;
h?: string | number | ResponsiveValue<string | number>; height?: string | number | ResponsiveValue<string | number>;
width?: string | number;
height?: string | number;
maxWidth?: string | ResponsiveValue<string>; maxWidth?: string | ResponsiveValue<string>;
minWidth?: string | ResponsiveValue<string>; minWidth?: string | ResponsiveValue<string>;
maxHeight?: string | ResponsiveValue<string>; maxHeight?: string | ResponsiveValue<string>;
@@ -62,6 +76,11 @@ export interface BoxProps<T extends ElementType> {
fullWidth?: boolean; fullWidth?: boolean;
fullHeight?: boolean; fullHeight?: boolean;
aspectRatio?: string; aspectRatio?: string;
// Aliases
w?: string | number | ResponsiveValue<string | number>;
h?: string | number | ResponsiveValue<string | number>;
// Display // Display
display?: 'block' | 'inline-block' | 'flex' | 'inline-flex' | 'grid' | 'none' | string | ResponsiveValue<'block' | 'inline-block' | 'flex' | 'inline-flex' | 'grid' | 'none' | string>; display?: 'block' | 'inline-block' | 'flex' | 'inline-flex' | 'grid' | 'none' | string | ResponsiveValue<'block' | 'inline-block' | 'flex' | 'inline-flex' | 'grid' | 'none' | string>;
center?: boolean; center?: boolean;
@@ -80,28 +99,6 @@ export interface BoxProps<T extends ElementType> {
insetY?: string | number; insetY?: string | number;
insetX?: string | number; insetX?: string | number;
zIndex?: number; zIndex?: number;
// Basic Styling
rounded?: 'none' | 'sm' | 'md' | 'lg' | 'xl' | '2xl' | 'full' | string | boolean;
border?: boolean | string;
borderTop?: boolean | string;
borderBottom?: boolean | string;
borderLeft?: boolean | string;
borderRight?: boolean | string;
borderWidth?: string | number;
borderStyle?: 'solid' | 'dashed' | 'dotted' | 'none' | string;
borderColor?: string;
borderOpacity?: number;
bg?: string;
backgroundColor?: string;
backgroundImage?: string;
backgroundSize?: string;
backgroundPosition?: string;
bgOpacity?: number;
color?: string;
shadow?: string;
opacity?: number;
blur?: 'none' | 'sm' | 'md' | 'lg' | 'xl' | '2xl' | '3xl' | string;
pointerEvents?: 'auto' | 'none' | string;
// Flex/Grid Item props // Flex/Grid Item props
flex?: number | string; flex?: number | string;
flexShrink?: number; flexShrink?: number;
@@ -113,20 +110,41 @@ export interface BoxProps<T extends ElementType> {
alignSelf?: 'auto' | 'start' | 'end' | 'center' | 'stretch' | 'baseline'; alignSelf?: 'auto' | 'start' | 'end' | 'center' | 'stretch' | 'baseline';
gap?: number | string | ResponsiveValue<number | string>; gap?: number | string | ResponsiveValue<number | string>;
gridCols?: number | ResponsiveValue<number>; gridCols?: number | ResponsiveValue<number>;
responsiveGridCols?: number | ResponsiveValue<number>;
colSpan?: number | ResponsiveValue<number>; colSpan?: number | ResponsiveValue<number>;
responsiveColSpan?: number | ResponsiveValue<number>;
order?: number | string | ResponsiveValue<number | string>; order?: number | string | ResponsiveValue<number | string>;
// Transform // Interaction
transform?: string | boolean; onClick?: React.MouseEventHandler<any>;
translate?: string; onMouseEnter?: React.MouseEventHandler<any>;
translateX?: string; onMouseLeave?: React.MouseEventHandler<any>;
translateY?: string; id?: string;
// Animation (Framer Motion support) role?: React.AriaRole;
initial?: any; tabIndex?: number;
animate?: any; // Internal use only
exit?: any; style?: React.CSSProperties;
className?: string;
borderTop?: string | boolean;
borderBottom?: string | boolean;
borderLeft?: string | boolean;
borderRight?: string | boolean;
bg?: string;
rounded?: string | boolean;
borderColor?: string;
border?: string | boolean;
color?: string;
opacity?: number;
transition?: any; transition?: any;
hoverBg?: string;
group?: boolean;
groupHoverOpacity?: number;
groupHoverBorderColor?: string;
groupHoverWidth?: string;
animate?: any;
blur?: string;
pointerEvents?: string;
bgOpacity?: number;
borderWidth?: string | number;
borderStyle?: string;
initial?: any;
variants?: any; variants?: any;
whileHover?: any; whileHover?: any;
whileTap?: any; whileTap?: any;
@@ -135,39 +153,14 @@ export interface BoxProps<T extends ElementType> {
whileInView?: any; whileInView?: any;
viewport?: any; viewport?: any;
custom?: any; custom?: any;
// Interaction exit?: any;
group?: boolean; translateX?: string;
groupHoverTextColor?: string; translateY?: string;
groupHoverScale?: boolean; translate?: string;
groupHoverOpacity?: number;
groupHoverBorderColor?: string;
hoverBorderColor?: string;
hoverBg?: string;
hoverTextColor?: string;
hoverScale?: boolean | number;
clickable?: boolean;
// Events
onMouseEnter?: React.MouseEventHandler<any>;
onMouseLeave?: React.MouseEventHandler<any>;
onClick?: React.MouseEventHandler<any>;
onMouseDown?: React.MouseEventHandler<any>;
onMouseUp?: React.MouseEventHandler<any>;
onMouseMove?: React.MouseEventHandler<any>;
onKeyDown?: React.KeyboardEventHandler<any>;
onBlur?: React.FocusEventHandler<any>;
onSubmit?: React.FormEventHandler<any>;
onScroll?: React.UIEventHandler<any>;
style?: React.CSSProperties;
id?: string;
role?: React.AriaRole;
tabIndex?: number;
// Other
type?: 'button' | 'submit' | 'reset' | string;
disabled?: boolean;
cursor?: string; cursor?: string;
fontSize?: string | ResponsiveValue<string>; fontSize?: string | ResponsiveValue<string>;
weight?: string;
fontWeight?: string | number; fontWeight?: string | number;
weight?: string | number;
letterSpacing?: string; letterSpacing?: string;
lineHeight?: string | number; lineHeight?: string | number;
font?: string; font?: string;
@@ -176,19 +169,15 @@ export interface BoxProps<T extends ElementType> {
truncate?: boolean; truncate?: boolean;
src?: string; src?: string;
alt?: string; alt?: string;
draggable?: boolean; draggable?: boolean | string;
min?: string | number; min?: string | number;
max?: string | number; max?: string | number;
step?: string | number; step?: string | number;
value?: string | number; value?: string | number;
onChange?: React.ChangeEventHandler<any>; onChange?: React.ChangeEventHandler<any>;
onError?: React.ReactEventHandler<any>;
placeholder?: string; placeholder?: string;
title?: string; title?: string;
padding?: Spacing | ResponsiveSpacing;
paddingLeft?: Spacing | ResponsiveSpacing;
paddingRight?: Spacing | ResponsiveSpacing;
paddingTop?: Spacing | ResponsiveSpacing;
paddingBottom?: Spacing | ResponsiveSpacing;
size?: string | number | ResponsiveValue<string | number>; size?: string | number | ResponsiveValue<string | number>;
accept?: string; accept?: string;
autoPlay?: boolean; autoPlay?: boolean;
@@ -196,16 +185,46 @@ export interface BoxProps<T extends ElementType> {
muted?: boolean; muted?: boolean;
playsInline?: boolean; playsInline?: boolean;
objectFit?: 'fill' | 'contain' | 'cover' | 'none' | 'scale-down'; objectFit?: 'fill' | 'contain' | 'cover' | 'none' | 'scale-down';
type?: string;
checked?: boolean;
disabled?: boolean;
onSubmit?: React.FormEventHandler<any>;
onBlur?: React.FocusEventHandler<any>;
onKeyDown?: React.KeyboardEventHandler<any>;
onMouseDown?: React.MouseEventHandler<any>;
onMouseUp?: React.MouseEventHandler<any>;
onMouseMove?: React.MouseEventHandler<any>;
onScroll?: React.UIEventHandler<any>;
responsiveColSpan?: number | ResponsiveValue<number>;
responsiveGridCols?: number | ResponsiveValue<number>;
clickable?: boolean;
hoverScale?: boolean | number;
hoverBorderColor?: string;
hoverTextColor?: string;
groupHoverScale?: boolean;
groupHoverTextColor?: string;
shadow?: string;
transform?: boolean | string;
lineClamp?: number;
fill?: string | boolean;
viewBox?: string;
stroke?: string;
strokeWidth?: string | number;
backgroundSize?: string;
backgroundPosition?: string;
backgroundImage?: string;
} }
export const Box = forwardRef(<T extends ElementType = 'div'>( export const Box = forwardRef(<T extends ElementType = 'div'>(
{ {
as, as,
children, children,
className = '', margin, marginTop, marginBottom, marginLeft, marginRight, marginX, marginY,
padding, paddingTop, paddingBottom, paddingLeft, paddingRight, paddingX, paddingY,
m, mt, mb, ml, mr, mx, my, m, mt, mb, ml, mr, mx, my,
p, pt, pb, pl, pr, px, py, p, pt, pb, pl, pr, px, py,
w, h, width, height, width, height,
w, h,
maxWidth, minWidth, maxHeight, minHeight, maxWidth, minWidth, maxHeight, minHeight,
fullWidth, fullHeight, fullWidth, fullHeight,
aspectRatio, aspectRatio,
@@ -218,27 +237,6 @@ export const Box = forwardRef(<T extends ElementType = 'div'>(
top, right, bottom, left, top, right, bottom, left,
inset, insetY, insetX, inset, insetY, insetX,
zIndex, zIndex,
rounded,
border,
borderTop,
borderBottom,
borderLeft,
borderRight,
borderWidth,
borderStyle,
borderColor,
borderOpacity,
bg,
backgroundColor,
backgroundImage,
backgroundSize,
backgroundPosition,
bgOpacity,
color,
shadow,
opacity,
blur,
pointerEvents,
flex, flex,
flexShrink, flexShrink,
flexGrow, flexGrow,
@@ -249,18 +247,39 @@ export const Box = forwardRef(<T extends ElementType = 'div'>(
alignSelf, alignSelf,
gap, gap,
gridCols, gridCols,
responsiveGridCols,
colSpan, colSpan,
responsiveColSpan,
order, order,
transform, onClick,
translate, onMouseEnter,
translateX, onMouseLeave,
translateY, id,
initial, role,
animate, tabIndex,
exit, style: styleProp,
className,
borderTop,
borderBottom,
borderLeft,
borderRight,
bg,
rounded,
borderColor,
border,
color,
opacity,
transition, transition,
hoverBg,
group,
groupHoverOpacity,
groupHoverBorderColor,
groupHoverWidth,
animate,
blur,
pointerEvents,
bgOpacity,
borderWidth,
borderStyle,
initial,
variants, variants,
whileHover, whileHover,
whileTap, whileTap,
@@ -269,36 +288,14 @@ export const Box = forwardRef(<T extends ElementType = 'div'>(
whileInView, whileInView,
viewport, viewport,
custom, custom,
group, exit,
groupHoverTextColor, translateX,
groupHoverScale, translateY,
groupHoverOpacity, translate,
groupHoverBorderColor,
hoverBorderColor,
hoverBg,
hoverTextColor,
hoverScale,
clickable,
onMouseEnter,
onMouseLeave,
onClick,
onMouseDown,
onMouseUp,
onMouseMove,
onKeyDown,
onBlur,
onSubmit,
onScroll,
style: styleProp,
id,
role,
tabIndex,
type,
disabled,
cursor, cursor,
fontSize, fontSize,
weight,
fontWeight, fontWeight,
weight,
letterSpacing, letterSpacing,
lineHeight, lineHeight,
font, font,
@@ -313,13 +310,9 @@ export const Box = forwardRef(<T extends ElementType = 'div'>(
step, step,
value, value,
onChange, onChange,
onError,
placeholder, placeholder,
title, title,
padding,
paddingLeft,
paddingRight,
paddingTop,
paddingBottom,
size, size,
accept, accept,
autoPlay, autoPlay,
@@ -327,8 +320,36 @@ export const Box = forwardRef(<T extends ElementType = 'div'>(
muted, muted,
playsInline, playsInline,
objectFit, objectFit,
type,
checked,
disabled,
onSubmit,
onBlur,
onKeyDown,
onMouseDown,
onMouseUp,
onMouseMove,
onScroll,
responsiveColSpan,
responsiveGridCols,
clickable,
hoverScale,
hoverBorderColor,
hoverTextColor,
groupHoverScale,
groupHoverTextColor,
shadow,
transform,
lineClamp,
fill,
viewBox,
stroke,
strokeWidth,
backgroundSize,
backgroundPosition,
backgroundImage,
...props ...props
}: BoxProps<T> & ComponentPropsWithoutRef<T>, }: BoxProps<T>,
ref: ForwardedRef<HTMLElement> ref: ForwardedRef<HTMLElement>
) => { ) => {
const Tag = (as as ElementType) || 'div'; const Tag = (as as ElementType) || 'div';
@@ -371,22 +392,22 @@ export const Box = forwardRef(<T extends ElementType = 'div'>(
}; };
const classes = [ const classes = [
getSpacingClass('m', m), getSpacingClass('m', margin || m),
getSpacingClass('mt', mt), getSpacingClass('mt', marginTop || mt),
getSpacingClass('mb', mb), getSpacingClass('mb', marginBottom || mb),
getSpacingClass('ml', ml), getSpacingClass('ml', marginLeft || ml),
getSpacingClass('mr', mr), getSpacingClass('mr', marginRight || mr),
getSpacingClass('mx', mx), getSpacingClass('mx', marginX || mx),
getSpacingClass('my', my), getSpacingClass('my', marginY || my),
getSpacingClass('p', p || padding), getSpacingClass('p', padding || p),
getSpacingClass('pt', pt || paddingTop), getSpacingClass('pt', paddingTop || pt),
getSpacingClass('pb', pb || paddingBottom), getSpacingClass('pb', paddingBottom || pb),
getSpacingClass('pl', pl || paddingLeft), getSpacingClass('pl', paddingLeft || pl),
getSpacingClass('pr', pr || paddingRight), getSpacingClass('pr', paddingRight || pr),
getSpacingClass('px', px), getSpacingClass('px', paddingX || px),
getSpacingClass('py', py), getSpacingClass('py', paddingY || py),
fullWidth ? 'w-full' : getResponsiveClasses('w', w), fullWidth ? 'w-full' : getResponsiveClasses('w', width || w),
fullHeight ? 'h-full' : getResponsiveClasses('h', h), fullHeight ? 'h-full' : getResponsiveClasses('h', height || h),
getResponsiveClasses('max-w', maxWidth), getResponsiveClasses('max-w', maxWidth),
getResponsiveClasses('min-w', minWidth), getResponsiveClasses('min-w', minWidth),
getResponsiveClasses('max-h', maxHeight), getResponsiveClasses('max-h', maxHeight),
@@ -407,23 +428,6 @@ export const Box = forwardRef(<T extends ElementType = 'div'>(
insetY !== undefined ? `inset-y-${insetY}` : '', insetY !== undefined ? `inset-y-${insetY}` : '',
insetX !== undefined ? `inset-x-${insetX}` : '', insetX !== undefined ? `inset-x-${insetX}` : '',
zIndex !== undefined ? `z-${zIndex}` : '', zIndex !== undefined ? `z-${zIndex}` : '',
rounded === true ? 'rounded' : (rounded === false ? 'rounded-none' : (typeof rounded === 'string' ? (rounded.includes('-') ? rounded : `rounded-${rounded}`) : '')),
border === true ? 'border' : (typeof border === 'string' ? (border === 'none' ? 'border-none' : border) : ''),
borderTop === true ? 'border-t' : (typeof borderTop === 'string' ? borderTop : ''),
borderBottom === true ? 'border-b' : (typeof borderBottom === 'string' ? borderBottom : ''),
borderLeft === true ? 'border-l' : (typeof borderLeft === 'string' ? borderLeft : ''),
borderRight === true ? 'border-r' : (typeof borderRight === 'string' ? borderRight : ''),
borderStyle ? `border-${borderStyle}` : '',
borderColor ? borderColor : '',
borderOpacity !== undefined ? `border-opacity-${borderOpacity * 100}` : '',
bg ? bg : '',
backgroundColor ? backgroundColor : '',
bgOpacity !== undefined ? `bg-opacity-${bgOpacity * 100}` : '',
color ? color : '',
shadow ? shadow : '',
opacity !== undefined ? `opacity-${opacity * 100}` : '',
blur ? (blur === 'none' ? 'blur-none' : `blur-${blur}`) : '',
pointerEvents ? `pointer-events-${pointerEvents}` : '',
flex !== undefined ? `flex-${flex}` : '', flex !== undefined ? `flex-${flex}` : '',
flexShrink !== undefined ? `flex-shrink-${flexShrink}` : '', flexShrink !== undefined ? `flex-shrink-${flexShrink}` : '',
flexGrow !== undefined ? `flex-grow-${flexGrow}` : '', flexGrow !== undefined ? `flex-grow-${flexGrow}` : '',
@@ -436,21 +440,14 @@ export const Box = forwardRef(<T extends ElementType = 'div'>(
getResponsiveClasses('grid-cols', gridCols || responsiveGridCols), getResponsiveClasses('grid-cols', gridCols || responsiveGridCols),
getResponsiveClasses('col-span', colSpan || responsiveColSpan), getResponsiveClasses('col-span', colSpan || responsiveColSpan),
getResponsiveClasses('order', order), getResponsiveClasses('order', order),
getResponsiveClasses('text', fontSize),
group ? 'group' : '', group ? 'group' : '',
groupHoverTextColor ? `group-hover:text-${groupHoverTextColor}` : '', animate === 'spin' ? 'animate-spin' : (animate === 'pulse' ? 'animate-pulse' : ''),
groupHoverScale ? 'group-hover:scale-105 transition-transform' : '', blur ? `blur-${blur}` : '',
groupHoverOpacity !== undefined ? `group-hover:opacity-${groupHoverOpacity * 100}` : '', pointerEvents ? `pointer-events-${pointerEvents}` : '',
groupHoverBorderColor ? `group-hover:border-${groupHoverBorderColor}` : '',
hoverBorderColor ? `hover:border-${hoverBorderColor}` : '',
hoverBg ? `hover:bg-${hoverBg}` : '',
hoverTextColor ? `hover:text-${hoverTextColor}` : '',
hoverScale === true ? 'hover:scale-105 transition-transform' : (typeof hoverScale === 'number' ? `hover:scale-${hoverScale} transition-transform` : ''),
clickable ? 'cursor-pointer active:opacity-80 transition-all' : '',
ring ? `ring-${ring}` : '',
hideScrollbar ? 'scrollbar-hide' : '', hideScrollbar ? 'scrollbar-hide' : '',
truncate ? 'truncate' : '', truncate ? 'truncate' : '',
transform === true ? 'transform' : (transform === false ? 'transform-none' : ''), clickable ? 'cursor-pointer' : '',
lineClamp ? `line-clamp-${lineClamp}` : '',
className className
].filter(Boolean).join(' '); ].filter(Boolean).join(' ');
@@ -466,23 +463,27 @@ export const Box = forwardRef(<T extends ElementType = 'div'>(
...(typeof right === 'string' || typeof right === 'number' ? { right } : {}), ...(typeof right === 'string' || typeof right === 'number' ? { right } : {}),
...(typeof bottom === 'string' || typeof bottom === 'number' ? { bottom } : {}), ...(typeof bottom === 'string' || typeof bottom === 'number' ? { bottom } : {}),
...(typeof left === 'string' || typeof left === 'number' ? { left } : {}), ...(typeof left === 'string' || typeof left === 'number' ? { left } : {}),
...(borderWidth !== undefined ? { borderWidth } : {}), ...(typeof borderTop === 'string' ? { borderTop } : (borderTop === true ? { borderTop: '1px solid var(--ui-color-border-default)' } : {})),
...(typeof transform === 'string' ? { transform } : {}), ...(typeof borderBottom === 'string' ? { borderBottom } : (borderBottom === true ? { borderBottom: '1px solid var(--ui-color-border-default)' } : {})),
...(translate ? { translate } : {}), ...(typeof borderLeft === 'string' ? { borderLeft } : (borderLeft === true ? { borderLeft: '1px solid var(--ui-color-border-default)' } : {})),
...(translateX ? { transform: `translateX(${translateX})` } : {}), ...(typeof borderRight === 'string' ? { borderRight } : (borderRight === true ? { borderRight: '1px solid var(--ui-color-border-default)' } : {})),
...(translateY ? { transform: `translateY(${translateY})` } : {}), ...(bg ? { background: bg.startsWith('bg-') ? undefined : bg } : {}),
...(cursor ? { cursor } : {}), ...(rounded === true ? { borderRadius: 'var(--ui-radius-md)' } : (typeof rounded === 'string' ? { borderRadius: rounded.includes('rem') || rounded.includes('px') ? rounded : `var(--ui-radius-${rounded})` } : {})),
...(fontSize && typeof fontSize === 'string' && !fontSize.includes(':') ? { fontSize } : {}), ...(borderColor ? { borderColor: borderColor.startsWith('border-') ? undefined : borderColor } : {}),
...(weight ? { fontWeight: weight } : {}), ...(border === true ? { border: '1px solid var(--ui-color-border-default)' } : (typeof border === 'string' ? { border } : {})),
...(color ? { color: color.startsWith('text-') ? undefined : color } : {}),
...(opacity !== undefined ? { opacity } : {}),
...(fontSize ? (typeof fontSize === 'string' ? { fontSize } : {}) : {}),
...(fontWeight ? { fontWeight } : {}), ...(fontWeight ? { fontWeight } : {}),
...(letterSpacing ? { letterSpacing } : {}), ...(weight ? { fontWeight: weight } : {}),
...(lineHeight ? { lineHeight } : {}), ...(shadow ? { boxShadow: shadow.startsWith('shadow-') ? undefined : shadow } : {}),
...(font ? { fontFamily: font } : {}), ...(transform === true ? { transform: 'auto' } : (typeof transform === 'string' ? { transform } : {})),
...(typeof size === 'string' || typeof size === 'number' ? { width: size, height: size } : {}), ...(typeof fill === 'string' ? { fill } : (fill === true ? { fill: 'currentColor' } : {})),
...(backgroundImage ? { backgroundImage } : {}), ...(stroke ? { stroke } : {}),
...(strokeWidth ? { strokeWidth } : {}),
...(backgroundSize ? { backgroundSize } : {}), ...(backgroundSize ? { backgroundSize } : {}),
...(backgroundPosition ? { backgroundPosition } : {}), ...(backgroundPosition ? { backgroundPosition } : {}),
...(objectFit ? { objectFit } : {}), ...(backgroundImage ? { backgroundImage } : {}),
...(styleProp || {}) ...(styleProp || {})
}; };
@@ -491,24 +492,24 @@ export const Box = forwardRef(<T extends ElementType = 'div'>(
ref={ref as React.ForwardedRef<HTMLElement>} ref={ref as React.ForwardedRef<HTMLElement>}
className={classes} className={classes}
onClick={onClick} onClick={onClick}
onMouseEnter={onMouseEnter}
onMouseLeave={onMouseLeave}
onMouseDown={onMouseDown} onMouseDown={onMouseDown}
onMouseUp={onMouseUp} onMouseUp={onMouseUp}
onMouseMove={onMouseMove} onMouseMove={onMouseMove}
onKeyDown={onKeyDown} onKeyDown={onKeyDown}
onBlur={onBlur} onBlur={onBlur}
onSubmit={onSubmit} onSubmit={onSubmit}
onMouseEnter={onMouseEnter}
onMouseLeave={onMouseLeave}
onScroll={onScroll} onScroll={onScroll}
style={style} onError={onError}
style={Object.keys(style).length > 0 ? style : undefined}
id={id} id={id}
role={role} role={role}
tabIndex={tabIndex} tabIndex={tabIndex}
type={type} type={type}
disabled={disabled}
src={src} src={src}
alt={alt} alt={alt}
draggable={draggable} draggable={draggable as any}
min={min} min={min}
max={max} max={max}
step={step} step={step}
@@ -520,6 +521,7 @@ export const Box = forwardRef(<T extends ElementType = 'div'>(
loop={loop} loop={loop}
muted={muted} muted={muted}
playsInline={playsInline} playsInline={playsInline}
viewBox={viewBox}
{...props} {...props}
> >
{children} {children}

View File

@@ -13,57 +13,25 @@ import { Box, BoxProps, ResponsiveValue } from './Box';
* If you need a more specific layout, create a new component in apps/website/components. * If you need a more specific layout, create a new component in apps/website/components.
*/ */
export interface GridProps<T extends ElementType = 'div'> extends Omit<BoxProps<T>, 'children'> { export interface GridProps<T extends ElementType = 'div'> extends BoxProps<T> {
children?: ReactNode; children?: ReactNode;
cols?: 1 | 2 | 3 | 4 | 5 | 6 | 12; columns?: number | ResponsiveValue<number>;
mdCols?: 1 | 2 | 3 | 4 | 5 | 6 | 12; gap?: number | string | ResponsiveValue<number | string>;
lgCols?: 1 | 2 | 3 | 4 | 5 | 6 | 12;
gap?: 0 | 1 | 2 | 3 | 4 | 6 | 8 | 12 | 16;
className?: string;
} }
export function Grid<T extends ElementType = 'div'>({ export function Grid<T extends ElementType = 'div'>({
children, children,
cols = 1, columns = 1,
mdCols,
lgCols,
gap = 4, gap = 4,
className = '',
...props ...props
}: GridProps<T>) { }: GridProps<T>) {
const colClasses: Record<number, string> = {
1: 'grid-cols-1',
2: 'grid-cols-1 md:grid-cols-2',
3: 'grid-cols-1 md:grid-cols-3',
4: 'grid-cols-2 md:grid-cols-4',
5: 'grid-cols-2 md:grid-cols-3 lg:grid-cols-5',
6: 'grid-cols-2 md:grid-cols-3 lg:grid-cols-6',
12: 'grid-cols-12'
};
const gapClasses: Record<number, string> = {
0: 'gap-0',
1: 'gap-1',
2: 'gap-2',
3: 'gap-3',
4: 'gap-4',
6: 'gap-6',
8: 'gap-8',
12: 'gap-12',
16: 'gap-16'
};
const classes = [
'grid',
colClasses[cols] || 'grid-cols-1',
mdCols ? `md:grid-cols-${mdCols}` : '',
lgCols ? `lg:grid-cols-${lgCols}` : '',
gapClasses[gap] || 'gap-4',
className
].filter(Boolean).join(' ');
return ( return (
<Box className={classes} {...props}> <Box
display="grid"
gridCols={columns}
gap={gap}
{...props}
>
{children} {children}
</Box> </Box>
); );

View File

@@ -1,35 +1,27 @@
import React, { ElementType } from 'react'; import React, { ReactNode, ElementType } from 'react';
import { Box, BoxProps } from './Box'; import { Box, BoxProps, ResponsiveValue } from './Box';
/** export interface GridItemProps<T extends ElementType = 'div'> extends BoxProps<T> {
* WARNING: DO NOT VIOLATE THE PURPOSE OF THIS PRIMITIVE. children?: ReactNode;
* colSpan?: number | ResponsiveValue<number>;
* GridItem is for items inside a Grid container. rowSpan?: number | ResponsiveValue<number>;
* lgSpan?: number; // Alias for colSpan.lg
* - DO NOT add positioning props (absolute, top, zIndex).
* - DO NOT add background/border props.
*
* If you need a more specific layout, create a new component in apps/website/components.
*/
export interface GridItemProps<T extends ElementType = 'div'> extends Omit<BoxProps<T>, 'children'> {
children?: React.ReactNode;
colSpan?: 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12;
mdSpan?: 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12;
lgSpan?: 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12;
className?: string;
} }
export function GridItem<T extends ElementType = 'div'>({ children, colSpan, mdSpan, lgSpan, className = '', ...props }: GridItemProps<T>) { export function GridItem<T extends ElementType = 'div'>({
const spanClasses = [ children,
colSpan ? `col-span-${colSpan}` : '', colSpan,
mdSpan ? `md:col-span-${mdSpan}` : '', rowSpan,
lgSpan ? `lg:col-span-${lgSpan}` : '', lgSpan,
className ...props
].filter(Boolean).join(' '); }: GridItemProps<T>) {
const actualColSpan = lgSpan ? { base: colSpan as any, lg: lgSpan } : colSpan;
return ( return (
<Box className={spanClasses} {...props}> <Box
colSpan={actualColSpan as any}
{...props}
>
{children} {children}
</Box> </Box>
); );

View File

@@ -13,20 +13,10 @@ import { Box, BoxProps, ResponsiveValue } from './Box';
* If you need a more specific layout, create a new component in apps/website/components. * If you need a more specific layout, create a new component in apps/website/components.
*/ */
interface ResponsiveGap { export interface StackProps<T extends ElementType> extends BoxProps<T> {
base?: number;
sm?: number;
md?: number;
lg?: number;
xl?: number;
}
export interface StackProps<T extends ElementType> extends Omit<BoxProps<T>, 'children'> {
as?: T; as?: T;
children?: ReactNode; children?: ReactNode;
className?: string; direction?: 'row' | 'col' | ResponsiveValue<'row' | 'col'>;
direction?: 'row' | 'col' | { base?: 'row' | 'col'; md?: 'row' | 'col'; lg?: 'row' | 'col' };
gap?: number | string | ResponsiveGap;
align?: 'start' | 'center' | 'end' | 'stretch' | 'baseline' | ResponsiveValue<'start' | 'center' | 'end' | 'stretch' | 'baseline'>; align?: 'start' | 'center' | 'end' | 'stretch' | 'baseline' | ResponsiveValue<'start' | 'center' | 'end' | 'stretch' | 'baseline'>;
justify?: 'start' | 'center' | 'end' | 'between' | 'around' | ResponsiveValue<'start' | 'center' | 'end' | 'between' | 'around'>; justify?: 'start' | 'center' | 'end' | 'between' | 'around' | ResponsiveValue<'start' | 'center' | 'end' | 'between' | 'around'>;
wrap?: boolean; wrap?: boolean;
@@ -35,7 +25,6 @@ export interface StackProps<T extends ElementType> extends Omit<BoxProps<T>, 'ch
export const Stack = forwardRef(<T extends ElementType = 'div'>( export const Stack = forwardRef(<T extends ElementType = 'div'>(
{ {
children, children,
className = '',
direction = 'col', direction = 'col',
gap = 4, gap = 4,
align, align,
@@ -46,91 +35,16 @@ export const Stack = forwardRef(<T extends ElementType = 'div'>(
}: StackProps<T>, }: StackProps<T>,
ref: ForwardedRef<HTMLElement> ref: ForwardedRef<HTMLElement>
) => { ) => {
const gapClasses: Record<number, string> = {
0: 'gap-0',
1: 'gap-1',
2: 'gap-2',
3: 'gap-3',
4: 'gap-4',
5: 'gap-5',
6: 'gap-6',
8: 'gap-8',
10: 'gap-10',
12: 'gap-12',
16: 'gap-16'
};
const getGapClasses = (value: number | string | ResponsiveGap | undefined) => {
if (value === undefined) return '';
if (typeof value === 'object') {
const classes = [];
if (value.base !== undefined) classes.push(typeof value.base === 'number' ? gapClasses[value.base] : `gap-${value.base}`);
if (value.sm !== undefined) classes.push(typeof value.sm === 'number' ? `sm:${gapClasses[value.sm]}` : `sm:gap-${value.sm}`);
if (value.md !== undefined) classes.push(typeof value.md === 'number' ? `md:${gapClasses[value.md]}` : `md:gap-${value.md}`);
if (value.lg !== undefined) classes.push(typeof value.lg === 'number' ? `lg:${gapClasses[value.lg]}` : `lg:gap-${value.lg}`);
if (value.xl !== undefined) classes.push(typeof value.xl === 'number' ? `xl:${gapClasses[value.xl]}` : `xl:gap-${value.xl}`);
return classes.join(' ');
}
if (typeof value === 'number') return gapClasses[value];
return `gap-${value}`;
};
const classes = [
'flex',
typeof direction === 'string'
? (direction === 'col' ? 'flex-col' : 'flex-row')
: [
direction.base === 'col' ? 'flex-col' : (direction.base === 'row' ? 'flex-row' : ''),
direction.md === 'col' ? 'md:flex-col' : (direction.md === 'row' ? 'md:flex-row' : ''),
direction.lg === 'col' ? 'lg:flex-col' : (direction.lg === 'row' ? 'lg:flex-row' : ''),
].filter(Boolean).join(' '),
getGapClasses(gap) || 'gap-4',
wrap ? 'flex-wrap' : '',
className
].filter(Boolean).join(' ');
const getAlignItemsClass = (value: StackProps<ElementType>['align']) => {
if (!value) return '';
const map: Record<string, string> = { start: 'items-start', center: 'items-center', end: 'items-end', stretch: 'items-stretch', baseline: 'items-baseline' };
if (typeof value === 'object') {
const classes = [];
if (value.base) classes.push(map[value.base]);
if (value.sm) classes.push(`sm:${map[value.sm]}`);
if (value.md) classes.push(`md:${map[value.md]}`);
if (value.lg) classes.push(`lg:${map[value.lg]}`);
if (value.xl) classes.push(`xl:${map[value.xl]}`);
if (value['2xl']) classes.push(`2xl:${map[value['2xl']]}`);
return classes.join(' ');
}
return map[value];
};
const getJustifyContentClass = (value: StackProps<ElementType>['justify']) => {
if (!value) return '';
const map: Record<string, string> = { start: 'justify-start', center: 'justify-center', end: 'justify-end', between: 'justify-between', around: 'justify-around' };
if (typeof value === 'object') {
const classes = [];
if (value.base) classes.push(map[value.base]);
if (value.sm) classes.push(`sm:${map[value.sm]}`);
if (value.md) classes.push(`md:${map[value.md]}`);
if (value.lg) classes.push(`lg:${map[value.lg]}`);
if (value.xl) classes.push(`xl:${map[value.xl]}`);
if (value['2xl']) classes.push(`2xl:${map[value['2xl']]}`);
return classes.join(' ');
}
return map[value];
};
const layoutClasses = [
getAlignItemsClass(align),
getJustifyContentClass(justify)
].filter(Boolean).join(' ');
return ( return (
<Box <Box
as={as} as={as}
ref={ref} ref={ref}
className={`${classes} ${layoutClasses}`} display="flex"
flexDirection={direction}
gap={gap}
alignItems={align}
justifyContent={justify}
flexWrap={wrap ? 'wrap' : 'nowrap'}
{...props} {...props}
> >
{children} {children}

View File

@@ -1,5 +1,6 @@
import React, { ReactNode, ElementType, ComponentPropsWithoutRef, forwardRef, ForwardedRef } from 'react'; import React, { ReactNode, ElementType, forwardRef, ForwardedRef } from 'react';
import { Box, BoxProps } from './Box'; import { Box, BoxProps } from './Box';
import { ThemeRadii, ThemeShadows } from '../theme/Theme';
/** /**
* WARNING: DO NOT VIOLATE THE PURPOSE OF THIS PRIMITIVE. * WARNING: DO NOT VIOLATE THE PURPOSE OF THIS PRIMITIVE.
@@ -12,15 +13,12 @@ import { Box, BoxProps } from './Box';
* If you need a more specific layout, create a new component in apps/website/components. * If you need a more specific layout, create a new component in apps/website/components.
*/ */
export interface SurfaceProps<T extends ElementType = 'div'> extends Omit<BoxProps<T>, 'children' | 'padding'> { export interface SurfaceProps<T extends ElementType = 'div'> extends BoxProps<T> {
as?: T; as?: T;
children?: ReactNode; children?: ReactNode;
variant?: 'default' | 'muted' | 'dark' | 'glass' | 'gradient-blue' | 'gradient-gold' | 'gradient-purple' | 'gradient-green' | 'discord' | 'discord-inner'; variant?: 'default' | 'dark' | 'muted' | 'glass' | 'discord' | 'gradient-blue' | 'gradient-gold' | 'gradient-purple' | 'gradient-green' | 'discord-inner' | 'outline';
rounded?: 'none' | 'sm' | 'md' | 'lg' | 'xl' | '2xl' | 'full' | string | boolean; rounded?: keyof ThemeRadii | 'none';
border?: boolean | string; shadow?: keyof ThemeShadows | 'none';
padding?: number;
className?: string;
shadow?: 'none' | 'sm' | 'md' | 'lg' | 'xl' | 'discord' | string;
} }
export const Surface = forwardRef(<T extends ElementType = 'div'>( export const Surface = forwardRef(<T extends ElementType = 'div'>(
@@ -29,69 +27,52 @@ export const Surface = forwardRef(<T extends ElementType = 'div'>(
children, children,
variant = 'default', variant = 'default',
rounded = 'none', rounded = 'none',
border = false,
padding = 0,
className = '',
shadow = 'none', shadow = 'none',
...props ...props
}: SurfaceProps<T> & ComponentPropsWithoutRef<T>, }: SurfaceProps<T>,
ref: ForwardedRef<HTMLElement> ref: ForwardedRef<HTMLElement>
) => { ) => {
const variantClasses: Record<string, string> = { const variantStyles: Record<string, React.CSSProperties> = {
default: 'bg-panel-gray', default: { backgroundColor: 'var(--ui-color-bg-surface)' },
muted: 'bg-panel-gray/40', dark: { backgroundColor: 'var(--ui-color-bg-base)' },
dark: 'bg-graphite-black', muted: { backgroundColor: 'var(--ui-color-bg-surface-muted)' },
glass: 'bg-graphite-black/60 backdrop-blur-md', glass: {
'gradient-blue': 'bg-gradient-to-br from-primary-accent/10 via-panel-gray/80 to-graphite-black', backgroundColor: 'rgba(20, 22, 25, 0.6)',
'gradient-gold': 'bg-gradient-to-br from-warning-amber/10 via-panel-gray/80 to-graphite-black', backdropFilter: 'blur(12px)',
'gradient-purple': 'bg-gradient-to-br from-purple-600/10 via-panel-gray/80 to-graphite-black', WebkitBackdropFilter: 'blur(12px)'
'gradient-green': 'bg-gradient-to-br from-success-green/10 via-panel-gray/80 to-graphite-black', },
'discord': 'bg-gradient-to-b from-graphite-black to-panel-gray', discord: {
'discord-inner': 'bg-gradient-to-br from-panel-gray via-graphite-black to-panel-gray' background: 'linear-gradient(to bottom, var(--ui-color-bg-base), var(--ui-color-bg-surface))'
},
'discord-inner': {
background: 'linear-gradient(to br, var(--ui-color-bg-surface), var(--ui-color-bg-base), var(--ui-color-bg-surface))'
},
'gradient-blue': {
background: 'linear-gradient(to br, rgba(25, 140, 255, 0.1), var(--ui-color-bg-surface), var(--ui-color-bg-base))'
},
'gradient-gold': {
background: 'linear-gradient(to br, rgba(255, 190, 77, 0.1), var(--ui-color-bg-surface), var(--ui-color-bg-base))'
},
'gradient-purple': {
background: 'linear-gradient(to br, rgba(147, 51, 234, 0.1), var(--ui-color-bg-surface), var(--ui-color-bg-base))'
},
'gradient-green': {
background: 'linear-gradient(to br, rgba(111, 227, 122, 0.1), var(--ui-color-bg-surface), var(--ui-color-bg-base))'
},
outline: {
backgroundColor: 'transparent',
border: '1px solid var(--ui-color-border-default)'
}
}; };
const shadowClasses: Record<string, string> = { const style: React.CSSProperties = {
none: '', ...variantStyles[variant],
sm: 'shadow-sm', borderRadius: rounded !== 'none' ? `var(--ui-radius-${String(rounded)})` : undefined,
md: 'shadow-md', boxShadow: shadow !== 'none' ? `var(--ui-shadow-${String(shadow)})` : undefined,
lg: 'shadow-lg',
xl: 'shadow-xl',
discord: 'shadow-[0_0_80px_rgba(88,101,242,0.15)]'
}; };
const roundedClasses: Record<string, string> = {
none: 'rounded-none',
sm: 'rounded-sm',
md: 'rounded-md',
lg: 'rounded-lg',
xl: 'rounded-xl',
'2xl': 'rounded-2xl',
full: 'rounded-full'
};
const paddingClasses: Record<number, string> = {
0: 'p-0',
1: 'p-1',
2: 'p-2',
3: 'p-3',
4: 'p-4',
6: 'p-6',
8: 'p-8',
10: 'p-10',
12: 'p-12'
};
const classes = [
variantClasses[variant],
typeof rounded === 'string' && roundedClasses[rounded] ? roundedClasses[rounded] : '',
border ? 'border border-border-gray' : '',
paddingClasses[padding] || 'p-0',
shadowClasses[shadow],
className
].filter(Boolean).join(' ');
return ( return (
<Box as={as} ref={ref} className={classes} {...props}> <Box as={as} ref={ref} {...(props as any)} style={style}>
{children} {children}
</Box> </Box>
); );

View File

@@ -47,6 +47,43 @@ export interface ThemeTypography {
}; };
} }
export interface ThemeSpacing {
0: string;
0.5: string;
1: string;
1.5: string;
2: string;
2.5: string;
3: string;
3.5: string;
4: string;
5: string;
6: string;
7: string;
8: string;
9: string;
10: string;
11: string;
12: string;
14: string;
16: string;
20: string;
24: string;
28: string;
32: string;
36: string;
40: string;
44: string;
48: string;
52: string;
56: string;
60: string;
64: string;
72: string;
80: string;
96: string;
}
export interface Theme { export interface Theme {
id: string; id: string;
name: string; name: string;
@@ -54,4 +91,5 @@ export interface Theme {
radii: ThemeRadii; radii: ThemeRadii;
shadows: ThemeShadows; shadows: ThemeShadows;
typography: ThemeTypography; typography: ThemeTypography;
spacing: ThemeSpacing;
} }

View File

@@ -48,4 +48,40 @@ export const defaultTheme: Theme = {
mono: "ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, 'Liberation Mono', 'Courier New', monospace", mono: "ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, 'Liberation Mono', 'Courier New', monospace",
}, },
}, },
spacing: {
0: '0',
0.5: '0.125rem',
1: '0.25rem',
1.5: '0.375rem',
2: '0.5rem',
2.5: '0.625rem',
3: '0.75rem',
3.5: '0.875rem',
4: '1rem',
5: '1.25rem',
6: '1.5rem',
7: '1.75rem',
8: '2rem',
9: '2.25rem',
10: '2.5rem',
11: '2.75rem',
12: '3rem',
14: '3.5rem',
16: '4rem',
20: '5rem',
24: '6rem',
28: '7rem',
32: '8rem',
36: '9rem',
40: '10rem',
44: '11rem',
48: '12rem',
52: '13rem',
56: '14rem',
60: '15rem',
64: '16rem',
72: '18rem',
80: '20rem',
96: '24rem',
},
}; };