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

View File

@@ -1,67 +1,44 @@
import React from 'react';
import React, { ReactNode } from 'react';
import { Box } from './primitives/Box';
import { Text } from './Text';
import { Surface } from './primitives/Surface';
import { Link } from './Link';
interface ActivityItemProps {
title?: string;
export interface ActivityItemProps {
title: string;
description?: string;
timeAgo?: string;
color?: string;
headline?: string;
body?: string;
formattedTime?: string;
ctaHref?: string;
ctaLabel?: string;
timestamp: string;
icon?: ReactNode;
children?: ReactNode;
}
export function ActivityItem({
export const ActivityItem = ({
title,
description,
timeAgo,
color = 'bg-primary-blue',
headline,
body,
formattedTime,
ctaHref,
ctaLabel
}: ActivityItemProps) {
timestamp,
icon,
children
}: ActivityItemProps) => {
return (
<Surface
variant="muted"
rounded="lg"
display="flex"
alignItems="start"
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>
<Surface variant="muted" rounded="lg" padding={4}>
<Box display="flex" alignItems="start" gap={4}>
{icon && (
<Box padding={2} rounded="lg" bg="var(--ui-color-bg-surface-muted)">
{icon}
</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>
</Surface>
);
}
};

View File

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

View File

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

View File

@@ -1,63 +1,55 @@
import React from 'react';
import { Box } from './primitives/Box';
import { Stack } from './primitives/Stack';
import { Surface } from './primitives/Surface';
import { Text } from './Text';
interface Tab {
export interface TabOption {
id: string;
label: string;
icon?: React.ReactNode;
}
interface BorderTabsProps {
tabs: Tab[];
activeTab: string;
onTabChange: (tabId: string) => void;
className?: string;
export interface BorderTabsProps {
tabs: TabOption[];
activeTabId: string;
onTabChange: (id: string) => void;
}
export function BorderTabs({ tabs, activeTab, onTabChange, className = '' }: BorderTabsProps) {
export const BorderTabs = ({
tabs,
activeTabId,
onTabChange
}: BorderTabsProps) => {
return (
<Box borderBottom borderColor="border-border-gray/50" className={className}>
<Stack direction="row" gap={8}>
{tabs.map((tab) => {
const isActive = activeTab === tab.id;
return (
<Surface
key={tab.id}
as="button"
onClick={() => onTabChange(tab.id)}
variant="ghost"
px={1}
py={4}
position="relative"
borderColor={isActive ? 'border-primary-blue' : ''}
borderBottom={isActive}
borderWidth={isActive ? '2px' : '0'}
mb="-1px"
transition="all 0.2s"
group
>
<Stack direction="row" align="center" gap={2}>
{tab.icon && (
<Box color={isActive ? 'text-primary-blue' : 'text-gray-400'} groupHoverTextColor={!isActive ? 'white' : undefined}>
{tab.icon}
</Box>
)}
<Text
size="sm"
weight="medium"
color={isActive ? 'text-primary-blue' : 'text-gray-400'}
groupHoverTextColor={!isActive ? 'white' : undefined}
>
{tab.label}
</Text>
</Stack>
</Surface>
);
})}
</Stack>
<Box display="flex" borderBottom>
{tabs.map((tab) => {
const isActive = tab.id === activeTabId;
return (
<button
key={tab.id}
onClick={() => onTabChange(tab.id)}
className={`px-6 py-4 text-sm font-bold uppercase tracking-widest transition-all relative ${
isActive
? 'text-[var(--ui-color-intent-primary)]'
: 'text-[var(--ui-color-text-low)] hover:text-[var(--ui-color-text-high)]'
}`}
>
<Box display="flex" alignItems="center" gap={2}>
{tab.icon}
{tab.label}
</Box>
{isActive && (
<Box
position="absolute"
bottom={0}
left={0}
right={0}
height="2px"
bg="var(--ui-color-intent-primary)"
/>
)}
</button>
);
})}
</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 {
children: React.ReactNode;
className?: string;
export interface BreadcrumbBarProps {
items: BreadcrumbItem[];
actions?: ReactNode;
}
/**
* BreadcrumbBar is a container for breadcrumbs, typically placed at the top of the ContentShell.
*/
export function BreadcrumbBar({ children, className = '' }: BreadcrumbBarProps) {
export const BreadcrumbBar = ({
items,
actions
}: BreadcrumbBarProps) => {
return (
<div className={`mb-6 flex items-center space-x-2 text-sm ${className}`}>
{children}
</div>
<Box
display="flex"
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 { Box } from '@/ui/primitives/Box';
import { Stack } from '@/ui/primitives/Stack';
import { Text } from '@/ui/Text';
import React from 'react';
import { Box } from './primitives/Box';
import { Text } from './Text';
import { ChevronRight } from 'lucide-react';
import { Icon } from './Icon';
import { Link } from './Link';
export type BreadcrumbItem = {
export interface BreadcrumbItem {
label: string;
href?: string;
};
}
interface BreadcrumbsProps {
export interface BreadcrumbsProps {
items: BreadcrumbItem[];
}
export function Breadcrumbs({ items }: BreadcrumbsProps) {
if (!items || items.length === 0) {
return null;
}
const lastIndex = items.length - 1;
export const Breadcrumbs = ({ items }: BreadcrumbsProps) => {
return (
<Box as="nav" aria-label="Breadcrumb" mb={4}>
<Stack direction="row" align="center" gap={2} wrap>
{items.map((item, index) => {
const isLast = index === lastIndex;
const content = item.href && !isLast ? (
<Link
href={item.href}
variant="ghost"
>
{item.label}
</Link>
) : (
<Text color={isLast ? 'text-white' : 'text-gray-400'}>{item.label}</Text>
);
return (
<Stack key={`${item.label}-${index}`} direction="row" align="center" gap={2}>
{index > 0 && (
<Text color="text-gray-600">/</Text>
)}
{content}
</Stack>
);
})}
</Stack>
<Box display="flex" alignItems="center" gap={2}>
{items.map((item, index) => {
const isLast = index === items.length - 1;
return (
<React.Fragment key={index}>
{index > 0 && <Icon icon={ChevronRight} size={3} intent="low" />}
{isLast || !item.href ? (
<Text size="sm" variant={isLast ? 'high' : 'low'} weight={isLast ? 'bold' : 'normal'}>
{item.label}
</Text>
) : (
<Link href={item.href} variant="secondary">
<Text size="sm">{item.label}</Text>
</Link>
)}
</React.Fragment>
);
})}
</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 { Box, BoxProps } from './primitives/Box';
import { Loader2 } from 'lucide-react';
import { Icon } from './Icon';
import { Loader2 } from 'lucide-react';
import { ResponsiveValue } from './primitives/Box';
interface ButtonProps extends Omit<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;
onClick?: MouseEventHandler<HTMLButtonElement>;
className?: string;
variant?: 'primary' | 'secondary' | 'danger' | 'ghost' | 'race-final' | 'discord';
onClick?: MouseEventHandler<HTMLButtonElement | HTMLAnchorElement>;
variant?: 'primary' | 'secondary' | 'danger' | 'ghost' | 'success' | 'discord' | 'race-final';
size?: 'sm' | 'md' | 'lg';
disabled?: boolean;
isLoading?: boolean;
@@ -19,14 +18,25 @@ interface ButtonProps extends Omit<ButtonHTMLAttributes<HTMLButtonElement>, 'as'
href?: string;
target?: 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;
backgroundColor?: string;
h?: string;
w?: string;
}
export const Button = forwardRef<HTMLButtonElement, ButtonProps>(({
export const Button = forwardRef<HTMLButtonElement | HTMLAnchorElement, ButtonProps>(({
children,
onClick,
className = '',
variant = 'primary',
size = 'md',
disabled = false,
@@ -38,25 +48,37 @@ export const Button = forwardRef<HTMLButtonElement, ButtonProps>(({
href,
target,
rel,
className,
style: styleProp,
width,
height,
minWidth,
px,
py,
p,
rounded,
bg,
color,
fontSize,
backgroundColor,
...props
h,
w,
}, ref) => {
const baseClasses = 'inline-flex items-center justify-center rounded-none transition-all duration-150 ease-smooth focus-visible:outline focus-visible:outline-1 focus-visible:outline-offset-2 active:opacity-80 uppercase tracking-widest font-bold';
const baseClasses = 'inline-flex items-center justify-center rounded-none transition-all duration-150 ease-in-out focus-visible:outline focus-visible:outline-1 focus-visible:outline-offset-2 active:opacity-80 uppercase tracking-widest font-bold';
const variantClasses = {
primary: 'bg-primary-accent text-white hover:bg-primary-accent/90 focus-visible:outline-primary-accent shadow-[0_0_15px_rgba(25,140,255,0.3)] hover:shadow-[0_0_25px_rgba(25,140,255,0.5)]',
secondary: 'bg-panel-gray text-white border border-border-gray hover:bg-border-gray/50 focus-visible:outline-primary-accent',
danger: 'bg-critical-red text-white hover:bg-critical-red/90 focus-visible:outline-critical-red',
ghost: 'bg-transparent text-gray-400 hover:text-white hover:bg-white/5 focus-visible:outline-gray-400',
'race-final': 'bg-success-green text-graphite-black hover:bg-success-green/90 focus-visible:outline-success-green',
primary: 'bg-[var(--ui-color-intent-primary)] text-white hover:opacity-90 focus-visible:outline-[var(--ui-color-intent-primary)] shadow-[0_0_15px_rgba(25,140,255,0.3)] hover:shadow-[0_0_25px_rgba(25,140,255,0.5)]',
secondary: 'bg-[var(--ui-color-bg-surface)] text-white border border-[var(--ui-color-border-default)] hover:bg-[var(--ui-color-border-default)] focus-visible:outline-[var(--ui-color-intent-primary)]',
danger: 'bg-[var(--ui-color-intent-critical)] text-white hover:opacity-90 focus-visible:outline-[var(--ui-color-intent-critical)]',
ghost: 'bg-transparent text-[var(--ui-color-text-low)] hover:text-[var(--ui-color-text-high)] hover:bg-white/5 focus-visible:outline-[var(--ui-color-text-low)]',
success: 'bg-[var(--ui-color-intent-success)] text-[var(--ui-color-bg-base)] hover:opacity-90 focus-visible:outline-[var(--ui-color-intent-success)]',
'race-final': 'bg-[var(--ui-color-intent-success)] text-[var(--ui-color-bg-base)] hover:opacity-90 focus-visible:outline-[var(--ui-color-intent-success)]',
discord: 'bg-[#5865F2] text-white hover:bg-[#4752C4] focus-visible:outline-[#5865F2]',
};
const sizeClasses = {
sm: 'min-h-[32px] px-3 py-1 text-xs font-medium',
md: 'min-h-[40px] px-4 py-2 text-sm font-medium',
lg: 'min-h-[48px] px-6 py-3 text-base font-medium'
sm: 'min-h-[32px] px-3 py-1 text-xs',
md: 'min-h-[40px] px-4 py-2 text-sm',
lg: 'min-h-[48px] px-6 py-3 text-base'
};
const disabledClasses = (disabled || isLoading) ? 'opacity-40 cursor-not-allowed' : 'cursor-pointer';
@@ -71,8 +93,18 @@ export const Button = forwardRef<HTMLButtonElement, ButtonProps>(({
className
].filter(Boolean).join(' ');
const style: React.CSSProperties = {
...(width ? { width: typeof width === 'object' ? undefined : width } : {}),
...(height ? { height: typeof height === 'object' ? undefined : height } : {}),
...(minWidth ? { minWidth } : {}),
...(fontSize ? { fontSize } : {}),
...(h ? { height: h } : {}),
...(w ? { width: w } : {}),
...(styleProp || {})
};
const content = (
<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}
{children}
@@ -81,35 +113,31 @@ export const Button = forwardRef<HTMLButtonElement, ButtonProps>(({
if (as === 'a') {
return (
<Box
as="a"
<a
ref={ref as React.ForwardedRef<HTMLAnchorElement>}
href={href}
target={target}
rel={rel}
className={classes}
fontSize={fontSize}
backgroundColor={backgroundColor}
{...props}
onClick={onClick as MouseEventHandler<HTMLAnchorElement>}
style={style}
>
{content}
</Box>
</a>
);
}
return (
<Box
as="button"
ref={ref}
<button
ref={ref as React.ForwardedRef<HTMLButtonElement>}
type={type}
className={classes}
onClick={onClick}
onClick={onClick as MouseEventHandler<HTMLButtonElement>}
disabled={disabled || isLoading}
fontSize={fontSize}
backgroundColor={backgroundColor}
{...props}
style={style}
>
{content}
</Box>
</button>
);
});

View File

@@ -1,49 +1,73 @@
import React, { ReactNode, MouseEventHandler } from 'react';
import { Box, BoxProps } from './primitives/Box';
import React, { ReactNode, forwardRef } from 'react';
import { Surface, SurfaceProps } from './primitives/Surface';
import { Box } from './primitives/Box';
export interface CardProps extends Omit<BoxProps<'div'>, 'children' | 'onClick'> {
export interface CardProps extends Omit<SurfaceProps<'div'>, 'children' | 'title' | 'variant'> {
children: ReactNode;
onClick?: MouseEventHandler<HTMLDivElement>;
variant?: 'default' | 'outline' | 'ghost' | 'muted' | 'dark' | 'glass';
variant?: 'default' | 'dark' | 'muted' | 'glass' | 'outline';
title?: ReactNode;
footer?: ReactNode;
}
export function Card({
export const Card = forwardRef<HTMLDivElement, CardProps>(({
children,
className = '',
onClick,
variant = 'default',
title,
footer,
...props
}: CardProps) {
const baseClasses = 'rounded-none transition-all duration-150 ease-smooth';
}, ref) => {
const isOutline = variant === 'outline';
const variantClasses = {
default: 'bg-panel-gray border border-border-gray shadow-card',
outline: 'bg-transparent border border-border-gray',
ghost: 'bg-transparent border-none',
muted: 'bg-panel-gray/40 border border-border-gray',
dark: 'bg-graphite-black border border-border-gray',
glass: 'bg-graphite-black/60 backdrop-blur-md border border-border-gray'
};
const style: React.CSSProperties = isOutline ? {
backgroundColor: 'transparent',
border: '1px solid var(--ui-color-border-default)',
} : {};
const classes = [
baseClasses,
variantClasses[variant],
onClick ? 'cursor-pointer hover:bg-border-gray/30' : '',
className
].filter(Boolean).join(' ');
// Default padding if none provided
const hasPadding = props.p !== undefined || props.px !== undefined || props.py !== undefined ||
props.pt !== undefined || props.pb !== undefined || props.pl !== undefined || props.pr !== undefined;
return (
<Box
className={classes}
onClick={onClick}
p={hasPadding ? undefined : 4}
<Surface
ref={ref}
variant={isOutline ? 'default' : variant}
rounded="lg"
shadow="md"
style={style}
{...props}
>
{children}
</Box>
{title && (
<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';
import { CategoryDistributionCard } from '@/ui/CategoryDistributionCard';
import { Heading } from '@/ui/Heading';
import { Icon } from '@/ui/Icon';
import { Box } from '@/ui/primitives/Box';
import { Grid } from '@/ui/primitives/Grid';
import { Text } from '@/ui/Text';
import { BarChart3 } from 'lucide-react';
const CATEGORIES = [
{ id: 'beginner', label: 'Beginner', color: 'text-green-400', bgColor: 'bg-green-400/10', borderColor: 'border-green-400/30', progressColor: 'bg-green-400' },
{ id: 'intermediate', label: 'Intermediate', color: 'text-primary-blue', bgColor: 'bg-primary-blue/10', borderColor: 'border-primary-blue/30', progressColor: 'bg-primary-blue' },
{ id: 'advanced', label: 'Advanced', color: 'text-purple-400', bgColor: 'bg-purple-400/10', borderColor: 'border-purple-400/30', progressColor: 'bg-purple-400' },
{ id: 'pro', label: 'Pro', color: 'text-yellow-400', bgColor: 'bg-yellow-400/10', borderColor: 'border-yellow-400/30', progressColor: 'bg-yellow-400' },
{ id: 'endurance', label: 'Endurance', color: 'text-orange-400', bgColor: 'bg-orange-400/10', borderColor: 'border-orange-400/30', progressColor: 'bg-orange-400' },
{ id: 'sprint', label: 'Sprint', color: 'text-red-400', bgColor: 'bg-red-400/10', borderColor: 'border-red-400/30', progressColor: 'bg-red-400' },
];
interface CategoryDistributionProps {
drivers: {
category?: string;
}[];
export interface CategoryData {
id: string;
label: string;
count: number;
icon: LucideIcon;
intent?: 'primary' | 'success' | 'warning' | 'critical' | 'telemetry';
}
export function CategoryDistribution({ drivers }: CategoryDistributionProps) {
const distribution = CATEGORIES.map((category) => ({
...category,
count: drivers.filter((d) => d.category === category.id).length,
percentage: drivers.length > 0
? Math.round((drivers.filter((d) => d.category === category.id).length / drivers.length) * 100)
: 0,
}));
export interface CategoryDistributionProps {
categories: CategoryData[];
}
export const CategoryDistribution = ({
categories
}: CategoryDistributionProps) => {
const total = categories.reduce((acc, cat) => acc + cat.count, 0);
return (
<Box mb={10}>
<Box display="flex" alignItems="center" gap={3} mb={4}>
<Box
display="flex"
h="10"
w="10"
alignItems="center"
justifyContent="center"
rounded="xl"
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 display="grid" gridCols={{ base: 1, md: 2, lg: 3 }} gap={4}>
{categories.map((category) => (
<CategoryDistributionCard
key={category.id}
label={category.label}
count={category.count}
total={total}
icon={category.icon}
intent={category.intent}
/>
))}
</Box>
);
}
};

View File

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

View File

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

View File

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

View File

@@ -1,53 +1,79 @@
import React from 'react';
import { Box } from './primitives/Box';
import { Text } from './Text';
interface CircularProgressProps {
export interface CircularProgressProps {
value: number;
max: number;
label: string;
color: string;
max?: number;
size?: number;
strokeWidth?: number;
intent?: 'primary' | 'success' | 'warning' | 'critical' | 'telemetry';
showValue?: boolean;
label?: string;
color?: string; // Alias for intent
}
export function CircularProgress({ value, max, label, color, size = 80 }: CircularProgressProps) {
const percentage = Math.min((value / max) * 100, 100);
const strokeWidth = 6;
export const CircularProgress = ({
value,
max = 100,
size = 64,
strokeWidth = 4,
intent = 'primary',
showValue = false,
label,
color: colorProp
}: CircularProgressProps) => {
const radius = (size - strokeWidth) / 2;
const circumference = radius * 2 * Math.PI;
const strokeDashoffset = circumference - (percentage / 100) * circumference;
const offset = circumference - (value / max) * circumference;
const intentColorMap = {
primary: 'var(--ui-color-intent-primary)',
success: 'var(--ui-color-intent-success)',
warning: 'var(--ui-color-intent-warning)',
critical: 'var(--ui-color-intent-critical)',
telemetry: 'var(--ui-color-intent-telemetry)',
};
const color = colorProp || intentColorMap[intent];
return (
<div className="flex flex-col items-center">
<div className="relative" style={{ width: size, height: size }}>
<svg className="transform -rotate-90" width={size} height={size}>
<Box display="flex" flexDirection="col" alignItems="center" gap={2}>
<Box position="relative" width={size} height={size} display="flex" alignItems="center" justifyContent="center">
<svg width={size} height={size} className="transform -rotate-90">
<circle
cx={size / 2}
cy={size / 2}
r={radius}
stroke="currentColor"
stroke="var(--ui-color-bg-surface-muted)"
strokeWidth={strokeWidth}
fill="transparent"
className="text-charcoal-outline"
/>
<circle
cx={size / 2}
cy={size / 2}
r={radius}
stroke="currentColor"
stroke={color}
strokeWidth={strokeWidth}
fill="transparent"
strokeDasharray={circumference}
strokeDashoffset={strokeDashoffset}
style={{ strokeDashoffset: offset, transition: 'stroke-dashoffset 0.3s ease-in-out' }}
strokeLinecap="round"
className={color}
style={{ transition: 'stroke-dashoffset 0.5s ease-in-out' }}
/>
</svg>
<div className="absolute inset-0 flex items-center justify-center">
<span className="text-lg font-bold text-white">{percentage.toFixed(0)}%</span>
</div>
</div>
<span className="text-xs text-gray-400 mt-2">{label}</span>
</div>
{showValue && (
<Box position="absolute" inset={0} display="flex" alignItems="center" justifyContent="center">
<Text size="xs" weight="bold" variant="high">
{Math.round((value / max) * 100)}%
</Text>
</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 { Box, BoxProps } from './primitives/Box';
import { Box } from './primitives/Box';
type Spacing = 0 | 0.5 | 1 | 1.5 | 2 | 2.5 | 3 | 3.5 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 14 | 16 | 20 | 24 | 28 | 32 | 36 | 40 | 44 | 48 | 52 | 56 | 60 | 64 | 72 | 80 | 96;
interface ContainerProps extends Omit<BoxProps<'div'>, 'size' | 'padding'> {
export interface ContainerProps {
children: ReactNode;
size?: 'sm' | 'md' | 'lg' | 'xl' | 'full';
padding?: boolean;
className?: string;
py?: Spacing;
pb?: Spacing;
}
export function Container({
export const Container = ({
children,
size = 'lg',
padding = true,
className = '',
py,
pb,
...props
}: ContainerProps) {
const sizeClasses = {
sm: 'max-w-2xl',
md: 'max-w-4xl',
lg: 'max-w-7xl',
xl: 'max-w-[1400px]',
full: 'max-w-full'
size = 'lg'
}: ContainerProps) => {
const sizeMap = {
sm: '40rem',
md: '48rem',
lg: '64rem',
xl: '80rem',
full: '100%',
};
const spacingMap: Record<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 (
<Box className={classes} {...props}>
<Box marginX="auto" maxWidth={sizeMap[size]} paddingX={4} fullWidth>
{children}
</Box>
);
}
};

View File

@@ -1,20 +1,34 @@
import React from 'react';
import React, { ReactNode } from 'react';
import { Box } from './primitives/Box';
interface ContentShellProps {
children: React.ReactNode;
className?: string;
export interface ContentShellProps {
children: ReactNode;
header?: ReactNode;
sidebar?: ReactNode;
}
/**
* ContentShell is the main data zone of the application.
* It houses the primary content and track maps/data tables.
*/
export function ContentShell({ children, className = '' }: ContentShellProps) {
export const ContentShell = ({
children,
header,
sidebar
}: ContentShellProps) => {
return (
<main className={`flex-1 overflow-y-auto bg-[#0C0D0F] ${className}`}>
<div className="max-w-7xl mx-auto px-4 md:px-6 py-6">
{children}
</div>
</main>
<Box display="flex" flexDirection="col" fullHeight>
{header && (
<Box borderBottom>
{header}
</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 {
children: React.ReactNode;
className?: string;
fullWidth?: boolean;
export interface ContentViewportProps {
children: ReactNode;
padding?: 'none' | 'sm' | 'md' | 'lg';
}
/**
* ContentViewport is the main data zone of the "Telemetry Workspace".
* It houses the primary content, track maps, and data tables.
* Aligned with "Precision Racing Minimal" theme.
*/
export function ContentViewport({ children, className = '', fullWidth = false }: ContentViewportProps) {
export const ContentViewport = ({
children,
padding = 'md'
}: ContentViewportProps) => {
const paddingMap: Record<string, any> = {
none: 0,
sm: 4,
md: 8,
lg: 12,
};
return (
<main className={`flex-1 overflow-y-auto bg-[#0C0D0F] ${className}`}>
<div className={fullWidth ? '' : 'max-w-7xl mx-auto px-4 md:px-6 py-6'}>
{children}
</div>
</main>
<Box as="main" flex={1} overflow="auto">
<Container size="xl">
<Box paddingY={paddingMap[padding]}>
{children}
</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 {
children: React.ReactNode;
className?: string;
export interface ControlBarProps {
children: ReactNode;
actions?: ReactNode;
}
/**
* ControlBar is the top-level header of the "Telemetry Workspace".
* It provides global controls, navigation, and status information.
* Aligned with "Precision Racing Minimal" theme.
*/
export function ControlBar({ children, className = '' }: ControlBarProps) {
export const ControlBar = ({
children,
actions
}: ControlBarProps) => {
return (
<header
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}`}
<Surface
variant="muted"
padding={4}
style={{ borderBottom: '1px solid var(--ui-color-border-default)' }}
>
{children}
</header>
<Box display="flex" alignItems="center" justifyContent="between">
<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';
// 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 {
export interface CountryFlagProps {
countryCode: string;
size?: 'sm' | 'md' | 'lg';
className?: string;
showTooltip?: boolean;
}
export function CountryFlag({
export const CountryFlag = ({
countryCode,
size = 'md',
className = '',
showTooltip = true
}: CountryFlagProps) {
const sizeClasses = {
sm: 'text-xs',
md: 'text-sm',
lg: 'text-base'
size = 'md'
}: CountryFlagProps) => {
const sizeMap = {
sm: '1rem',
md: '1.5rem',
lg: '2rem',
};
const flag = countryCodeToFlag(countryCode);
const countryName = countryNames[countryCode.toUpperCase()] || countryCode;
return (
<span
className={`inline-flex items-center relative ${sizeClasses[size]} ${className}`}
title={showTooltip ? countryName : undefined}
<Box
width={sizeMap[size]}
height={sizeMap[size]}
display="flex"
alignItems="center"
justifyContent="center"
overflow="hidden"
style={{ borderRadius: '2px' }}
>
<span className="select-none">{flag}</span>
</span>
<img
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 { Box } from './primitives/Box';
import { Text } from './Text';
import { Heading } from './Heading';
import { Card } from './Card';
import { Text } from './Text';
import { Surface } from './primitives/Surface';
interface DangerZoneProps {
export interface DangerZoneProps {
title: string;
description: string;
children: ReactNode;
}
export function DangerZone({ title, description, children }: DangerZoneProps) {
export const DangerZone = ({
title,
description,
children
}: DangerZoneProps) => {
return (
<Card>
<Heading level={3} mb={4}>Danger Zone</Heading>
<Box p={4} rounded="lg" bg="bg-red-900/10" border={true} borderColor="border-red-900/30">
<Text color="text-white" weight="medium" block mb={2}>{title}</Text>
<Text size="sm" color="text-gray-400" block mb={4}>
{description}
</Text>
<Box marginTop={8}>
<Heading level={3} marginBottom={4}>Danger Zone</Heading>
<Surface
variant="muted"
rounded="lg"
padding={4}
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}
</Box>
</Card>
</Surface>
</Box>
);
}
};

View File

@@ -1,30 +1,31 @@
import React from 'react';
import { Calendar } from 'lucide-react';
import { Box } from './primitives/Box';
import { Stack } from './primitives/Stack';
import { Text } from './Text';
import { Stack } from './primitives/Stack';
import { Calendar } from 'lucide-react';
import { Icon } from './Icon';
interface DateHeaderProps {
label: string;
count?: number;
countLabel?: string;
export interface DateHeaderProps {
date: string;
showIcon?: boolean;
}
export function DateHeader({ label, count, countLabel = 'races' }: DateHeaderProps) {
export const DateHeader = ({
date,
showIcon = true
}: DateHeaderProps) => {
return (
<Stack direction="row" align="center" gap={3} px={2}>
<Box p={2} bg="bg-primary-blue/10" rounded="lg">
<Icon icon={Calendar} size={4} color="rgb(59, 130, 246)" />
</Box>
<Text weight="semibold" size="sm" color="text-white">
{label}
</Text>
{count !== undefined && (
<Text size="xs" color="text-gray-500">
{count} {count === 1 ? countLabel.replace(/s$/, '') : countLabel}
</Text>
<Stack direction="row" align="center" gap={3} paddingX={2}>
{showIcon && (
<Box padding={2} bg="rgba(25, 140, 255, 0.1)" rounded="lg">
<Icon icon={Calendar} size={4} intent="primary" />
</Box>
)}
<Box>
<Text weight="semibold" size="sm" variant="high">
{date}
</Text>
</Box>
</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';
import { Input } from '@/ui/Input';
interface DurationFieldProps {
export interface DurationFieldProps {
label: string;
value: number | '';
onChange: (value: number | '') => void;
helperText?: string;
required?: boolean;
value: number; // in minutes
onChange: (value: number) => void;
disabled?: boolean;
unit?: 'minutes' | 'laps';
error?: string;
}
export function DurationField({
label,
value,
onChange,
helperText,
required,
disabled,
unit = 'minutes',
error,
}: DurationFieldProps) {
const handleChange = (raw: string) => {
if (raw.trim() === '') {
onChange('');
return;
}
export const DurationField = ({
label,
value,
onChange,
disabled = false,
error
}: DurationFieldProps) => {
const hours = Math.floor(value / 60);
const minutes = value % 60;
const parsed = parseInt(raw, 10);
if (Number.isNaN(parsed) || parsed <= 0) {
onChange('');
return;
}
onChange(parsed);
const handleHoursChange = (e: ChangeEvent<HTMLInputElement>) => {
const h = parseInt(e.target.value) || 0;
onChange(h * 60 + minutes);
};
const unitLabel = unit === 'laps' ? 'laps' : 'min';
const handleMinutesChange = (e: ChangeEvent<HTMLInputElement>) => {
const m = parseInt(e.target.value) || 0;
onChange(hours * 60 + m);
};
return (
<div className="space-y-1">
<label className="block text-sm font-medium text-gray-300">
<Box>
<Text as="label" size="xs" weight="bold" variant="low" block marginBottom={1.5}>
{label}
{required && <span className="text-warning-amber ml-1">*</span>}
</label>
<div className="flex items-center gap-2">
<div className="flex-1">
</Text>
<Box display="flex" alignItems="center" gap={4}>
<Box display="flex" alignItems="center" gap={2}>
<Input
type="number"
value={value === '' ? '' : String(value)}
onChange={(e) => handleChange(e.target.value)}
value={hours}
onChange={handleHoursChange}
disabled={disabled}
min={1}
className="pr-16"
variant={error ? 'error' : 'default'}
min={0}
style={{ width: '4rem' }}
/>
</div>
<span className="text-xs text-gray-400 -ml-14">{unitLabel}</span>
</div>
{helperText && (
<p className="text-xs text-gray-500">{helperText}</p>
)}
<Text size="sm" variant="low">h</Text>
</Box>
<Box display="flex" alignItems="center" gap={2}>
<Input
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 && (
<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 { Box } from './primitives/Box';
import { Button } from './Button';
import { RefreshCw, Home } from 'lucide-react';
interface ErrorActionButtonsProps {
export interface ErrorActionButtonsProps {
onRetry?: () => void;
onHomeClick: () => void;
showRetry?: boolean;
homeLabel?: string;
onGoHome?: () => void;
}
/**
* ErrorActionButtons
*
* Action buttons for error pages (Try Again, Go Home)
* Provides consistent styling and behavior.
* All navigation callbacks must be provided by the caller.
*/
export function ErrorActionButtons({
onRetry,
onHomeClick,
showRetry = false,
homeLabel = 'Drive home',
}: ErrorActionButtonsProps) {
if (showRetry && onRetry) {
return (
<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>
);
}
export const ErrorActionButtons = ({
onRetry,
onGoHome
}: ErrorActionButtonsProps) => {
return (
<div className="pt-2">
<button
type="button"
onClick={onHomeClick}
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"
>
{homeLabel}
</button>
</div>
<Box display="flex" alignItems="center" gap={4}>
{onRetry && (
<Button variant="primary" onClick={onRetry} icon={<RefreshCw size={16} />}>
Retry
</Button>
)}
{onGoHome && (
<Button variant="secondary" onClick={onGoHome} icon={<Home size={16} />}>
Go Home
</Button>
)}
</Box>
);
}
};

View File

@@ -1,63 +1,45 @@
import React from 'react';
import { Box } from './primitives/Box';
import { Stack } from './primitives/Stack';
import { Text } from './Text';
import { Surface } from './primitives/Surface';
import { Icon } from './Icon';
import { AlertCircle, XCircle, Info, AlertTriangle } from 'lucide-react';
import { AlertTriangle } from 'lucide-react';
import { Surface } from './primitives/Surface';
interface ErrorBannerProps {
export interface ErrorBannerProps {
title?: string;
message: string;
variant?: 'error' | 'warning' | 'info' | 'success';
variant?: 'error' | 'warning' | 'info';
}
export function ErrorBanner({ title, message, variant = 'error' }: ErrorBannerProps) {
const configs = {
error: {
bg: 'rgba(239, 68, 68, 0.1)',
border: 'rgba(239, 68, 68, 0.2)',
text: 'rgb(248, 113, 113)',
icon: XCircle
},
warning: {
bg: 'rgba(245, 158, 11, 0.1)',
border: 'rgba(245, 158, 11, 0.2)',
text: 'rgb(251, 191, 36)',
icon: AlertTriangle
},
info: {
bg: 'rgba(59, 130, 246, 0.1)',
border: 'rgba(59, 130, 246, 0.2)',
text: 'rgb(96, 165, 250)',
icon: Info
},
success: {
bg: 'rgba(16, 185, 129, 0.1)',
border: 'rgba(16, 185, 129, 0.2)',
text: 'rgb(52, 211, 153)',
icon: AlertCircle
}
};
const colors = configs[variant];
export const ErrorBanner = ({
title,
message,
variant = 'error'
}: ErrorBannerProps) => {
const intent = variant === 'error' ? 'critical' : variant === 'warning' ? 'warning' : 'primary';
const color = variant === 'error' ? 'rgba(227, 92, 92, 0.05)' : variant === 'warning' ? 'rgba(255, 190, 77, 0.05)' : 'rgba(25, 140, 255, 0.05)';
const borderColor = variant === 'error' ? 'rgba(227, 92, 92, 0.2)' : variant === 'warning' ? 'rgba(255, 190, 77, 0.2)' : 'rgba(25, 140, 255, 0.2)';
return (
<Surface
variant="muted"
rounded="xl"
border
p={4}
backgroundColor={colors.bg}
borderColor={colors.border}
rounded="lg"
padding={4}
style={{ backgroundColor: color, border: `1px solid ${borderColor}` }}
>
<Stack direction="row" align="start" gap={3}>
<Icon icon={colors.icon} size={5} color={colors.text} />
<Box flex={1}>
{title && <Text weight="medium" color={colors.text} block mb={1}>{title}</Text>}
<Text size="sm" color={colors.text} opacity={0.9} block>{message}</Text>
<Box display="flex" alignItems="start" gap={4}>
<Icon icon={AlertTriangle} size={5} intent={intent} />
<Box>
{title && (
<Text weight="medium" variant="high" block marginBottom={1}>
{title}
</Text>
)}
<Text size="sm" variant="low">
{message}
</Text>
</Box>
</Stack>
</Box>
</Surface>
);
}
};

View File

@@ -1,29 +1,24 @@
import React, { ReactNode } from 'react';
import { Box } from './primitives/Box';
import { Surface } from './primitives/Surface';
interface ErrorPageContainerProps {
export interface ErrorPageContainerProps {
children: ReactNode;
errorCode: string;
description: string;
}
/**
* ErrorPageContainer
*
* A reusable container for error pages (404, 500, etc.)
* Provides consistent styling and layout for error states.
*/
export function ErrorPageContainer({
children,
errorCode,
description,
}: ErrorPageContainerProps) {
export const ErrorPageContainer = ({ children }: ErrorPageContainerProps) => {
return (
<main className="min-h-screen flex items-center justify-center bg-deep-graphite text-white px-6">
<div className="max-w-md text-center space-y-4">
<h1 className="text-3xl font-semibold">{errorCode}</h1>
<p className="text-sm text-gray-400">{description}</p>
<Box
minHeight="100vh"
display="flex"
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}
</div>
</main>
</Surface>
</Box>
);
}
};

View File

@@ -1,34 +1,29 @@
import { Button } from '@/ui/Button';
import { Card } from '@/ui/Card';
import { Heading } from '@/ui/Heading';
import { Box } from '@/ui/primitives/Box';
import { Text } from '@/ui/Text';
import React from 'react';
import { Box } from './primitives/Box';
import { Text } from './Text';
import { Icon } from './Icon';
import { MessageSquare } from 'lucide-react';
export function FeedEmptyState() {
return (
<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 interface FeedEmptyStateProps {
message?: string;
}
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 { Box } from './primitives/Box';
import { Text } from './Text';
import { Image } from './Image';
import { Surface } from './primitives/Surface';
import { Avatar } from './Avatar';
interface FeedItemProps {
actorName?: string;
actorAvatarUrl?: string;
typeLabel: string;
headline: string;
body?: string;
timeAgo: string;
cta?: ReactNode;
export interface FeedItemProps {
user: {
name: string;
avatar?: string;
};
content: ReactNode;
timestamp: string;
actions?: ReactNode;
}
export function FeedItem({
actorName,
actorAvatarUrl,
typeLabel,
headline,
body,
timeAgo,
cta,
}: FeedItemProps) {
export const FeedItem = ({
user,
content,
timestamp,
actions
}: FeedItemProps) => {
return (
<Box display="flex" gap={4}>
<Box flexShrink={0}>
{actorAvatarUrl ? (
<Box width="10" height="10" rounded="full" overflow="hidden" bg="bg-charcoal-outline">
<Image
src={actorAvatarUrl}
alt={actorName || ''}
width={40}
height={40}
fullWidth
fullHeight
objectFit="cover"
/>
<Surface variant="default" rounded="lg" padding={4} style={{ border: '1px solid var(--ui-color-border-default)' }}>
<Box display="flex" alignItems="start" gap={4}>
<Avatar src={user.avatar} alt={user.name} size="md" />
<Box flex={1}>
<Box display="flex" alignItems="center" justifyContent="between" marginBottom={2}>
<Text weight="bold" variant="high">{user.name}</Text>
<Text size="xs" variant="low">{timestamp}</Text>
</Box>
) : (
<Box
width="10"
height="10"
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 marginBottom={4}>
{typeof content === 'string' ? (
<Text variant="med">{content}</Text>
) : content}
</Box>
)}
</Box>
<Box flexGrow={1} minWidth="0">
<Box display="flex" alignItems="start" justifyContent="between" gap={2}>
<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>
{actions && (
<Box display="flex" alignItems="center" gap={4} borderTop paddingTop={4}>
{actions}
</Box>
)}
</Box>
{cta && (
<Box mt={3}>
{cta}
</Box>
)}
</Box>
</Box>
</Surface>
);
}
};

View File

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

View File

@@ -1,81 +1,54 @@
import { Link } from '@/ui/Link';
import { Box } from '@/ui/primitives/Box';
import { Text } from '@/ui/Text';
import React from 'react';
import { Box } from './primitives/Box';
import { Container } from './Container';
import { Text } from './Text';
import { Link } from './Link';
const discordUrl = process.env.NEXT_PUBLIC_DISCORD_URL || 'https://discord.gg/gridpilot';
const xUrl = process.env.NEXT_PUBLIC_X_URL || '#';
export function Footer() {
export const Footer = () => {
return (
<Box as="footer" position="relative" bg="graphite-black" borderTop borderColor="border-gray/50">
<Box position="absolute" top="0" left="0" right="0" h="px" bg="linear-gradient(to right, transparent, #198CFF, transparent)" opacity={0.3} />
<Box maxWidth="7xl" mx="auto" px={{ base: 6, lg: 8 }} py={{ base: 12, md: 16 }}>
{/* Racing stripe accent */}
<Box
display="flex"
gap={2}
mb={8}
justifyContent="center"
>
<Box w="12" h="1" bg="white" opacity={0.1} />
<Box w="12" h="1" bg="primary-accent" />
<Box w="12" h="1" bg="white" opacity={0.1} />
<Box as="footer" bg="var(--ui-color-bg-surface)" borderTop paddingY={12}>
<Container size="xl">
<Box display="grid" gridCols={{ base: 1, md: 4 }} gap={12}>
<Box>
<Text weight="bold" variant="high" marginBottom={4}>GridPilot</Text>
<Text size="sm" variant="low">
The ultimate companion for sim racers. Track your performance, manage your team, and compete in leagues.
</Text>
</Box>
<Box>
<Text weight="bold" variant="high" marginBottom={4}>Platform</Text>
<Box display="flex" flexDirection="col" gap={2}>
<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>
{/* Personal message */}
<Box
textAlign="center"
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.
<Box borderTop marginTop={12} paddingTop={8} textAlign="center">
<Text size="xs" variant="low">
© {new Date().getFullYear()} GridPilot. All rights reserved.
</Text>
</Box>
{/* 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>
</Container>
</Box>
);
}
};

View File

@@ -1,45 +1,55 @@
import React from 'react';
import { Icon } from './Icon';
import { Stack } from './primitives/Stack';
import React, { ReactNode } from 'react';
import { Box } from './primitives/Box';
import { Text } from './Text';
import { Icon } from './Icon';
import { LucideIcon } from 'lucide-react';
interface FormFieldProps {
label: string;
icon?: LucideIcon;
children: React.ReactNode;
required?: boolean;
export interface FormFieldProps {
label?: string;
error?: string;
hint?: string;
required?: boolean;
icon?: LucideIcon;
children: ReactNode;
}
export function FormField({
export const FormField = ({
label,
error,
hint,
required,
icon,
children,
required = false,
error,
hint,
}: FormFieldProps) {
children
}: FormFieldProps) => {
return (
<Stack gap={2}>
<label className="block text-sm font-medium text-gray-300">
<Stack direction="row" align="center" gap={2}>
{icon && <Icon icon={icon} size={4} color="#6b7280" />}
<Text size="sm" weight="medium" color="text-gray-300">{label}</Text>
{required && <Text color="text-error-red">*</Text>}
</Stack>
</label>
<Box marginBottom={4}>
{label && (
<Box display="flex" alignItems="center" gap={2} marginBottom={1.5}>
{icon && <Icon icon={icon} size={4} intent="low" />}
<Text size="sm" weight="medium" variant="high">
{label}
</Text>
{required && <Text variant="critical">*</Text>}
</Box>
)}
{children}
{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 && (
<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 { Stack } from './primitives/Stack';
import { Box } from './primitives/Box';
import { Text } from './Text';
interface FormSectionProps {
export interface FormSectionProps {
title: string;
description?: string;
children: ReactNode;
title?: string;
}
/**
* FormSection
*
* Groups related form fields with an optional title.
*/
export function FormSection({ children, title }: FormSectionProps) {
export const FormSection = ({
title,
description,
children
}: FormSectionProps) => {
return (
<Stack gap={4} fullWidth>
{title && (
<Text
size="xs"
weight="bold"
color="text-gray-500"
uppercase
letterSpacing="widest"
borderBottom
borderColor="border-border-gray"
pb={1}
>
<Box display="flex" flexDirection="col" gap={6}>
<Box borderBottom paddingBottom={4}>
<Text weight="bold" variant="high" size="lg" marginBottom={1} block>
{title}
</Text>
)}
<Stack gap={4} fullWidth>
{description && (
<Text size="sm" variant="low">
{description}
</Text>
)}
</Box>
<Box display="flex" flexDirection="col" gap={4}>
{children}
</Stack>
</Stack>
</Box>
</Box>
);
}
};

View File

@@ -1,41 +1,48 @@
import React from 'react';
import { Box } from './primitives/Box';
import { Stack } from './primitives/Stack';
import { Text } from './Text';
import { Heading } from './Heading';
import { Card } from './Card';
import { ProgressBar } from './ProgressBar';
import { Box } from './primitives/Box';
import { Text } from './Text';
interface GoalCardProps {
export interface GoalCardProps {
title: string;
current: number;
target: number;
unit: string;
icon: string;
goalLabel: string;
currentValue: number;
maxValue: number;
color?: string;
}
export function GoalCard({
title,
icon,
goalLabel,
currentValue,
maxValue,
color = 'text-primary-blue',
}: GoalCardProps) {
export const GoalCard = ({
title,
current,
target,
unit,
icon
}: GoalCardProps) => {
const percentage = Math.min(Math.max((current / target) * 100, 0), 100);
return (
<Card bg="bg-charcoal-200/50" borderColor="border-primary-blue/30">
<Box display="flex" alignItems="center" gap={3} mb={3}>
<Card variant="default">
<Box display="flex" alignItems="center" gap={3} marginBottom={3}>
<Text size="2xl">{icon}</Text>
<Heading level={3}>{title}</Heading>
</Box>
<Stack gap={2}>
<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>
<Text weight="bold" variant="high">{title}</Text>
<Text size="sm" variant="low">{unit}</Text>
</Box>
<ProgressBar value={currentValue} max={maxValue} />
</Stack>
</Box>
<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>
);
}
};

View File

@@ -1,16 +1,38 @@
import React from 'react';
import { Container } from '@/ui/Container';
import React, { ReactNode } from 'react';
import { Box } from './primitives/Box';
import { Container } from './Container';
interface HeaderProps {
children: React.ReactNode;
export interface HeaderProps {
children: ReactNode;
actions?: ReactNode;
}
export function Header({ children }: HeaderProps) {
export const Header = ({
children,
actions
}: HeaderProps) => {
return (
<header className="fixed top-0 left-0 right-0 z-50 bg-graphite-black/80 backdrop-blur-md border-b border-border-gray/50">
<Container>
{children}
<Box
as="header"
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>
</header>
</Box>
);
}
};

View File

@@ -1,93 +1,52 @@
import React, { ReactNode, ElementType } from 'react';
import { Stack } from './primitives/Stack';
import React, { ReactNode, forwardRef } from 'react';
import { Box, BoxProps, ResponsiveValue } from './primitives/Box';
interface ResponsiveFontSize {
base?: string;
sm?: string;
md?: string;
lg?: string;
xl?: string;
'2xl'?: string;
}
interface HeadingProps extends Omit<BoxProps<'h1'>, 'children' | 'as' | 'fontSize'> {
level: 1 | 2 | 3 | 4 | 5 | 6;
export interface HeadingProps extends BoxProps<any> {
children: ReactNode;
icon?: ReactNode;
id?: string;
groupHoverColor?: string;
truncate?: boolean;
uppercase?: boolean;
fontSize?: string | ResponsiveFontSize;
weight?: 'light' | 'normal' | 'medium' | 'semibold' | 'bold' | string;
letterSpacing?: string;
level?: 1 | 2 | 3 | 4 | 5 | 6;
weight?: 'normal' | 'medium' | 'semibold' | 'bold';
align?: 'left' | 'center' | 'right';
fontSize?: string | ResponsiveValue<string>;
}
export function Heading({ level, children, icon, groupHoverColor, truncate, uppercase, fontSize, weight, letterSpacing, ...props }: HeadingProps) {
const Tag = `h${level}` as ElementType;
export const Heading = forwardRef<HTMLHeadingElement, HeadingProps>(({
children,
level = 1,
weight = 'bold',
align = 'left',
fontSize,
...props
}, ref) => {
const Tag = `h${level}` as const;
const levelClasses = {
1: 'text-3xl md:text-4xl font-bold text-white tracking-tight',
2: 'text-xl md:text-2xl font-bold text-white tracking-tight',
3: 'text-lg font-bold text-white tracking-tight',
4: 'text-base font-bold text-white tracking-tight',
5: 'text-sm font-bold text-white tracking-tight uppercase tracking-wider',
6: 'text-xs font-bold text-white tracking-tight uppercase tracking-widest',
};
const weightClasses: Record<string, string> = {
light: 'font-light',
const weightClasses = {
normal: 'font-normal',
medium: 'font-medium',
semibold: 'font-semibold',
bold: 'font-bold'
};
const getFontSizeClasses = (value: string | ResponsiveFontSize | undefined) => {
if (value === undefined) return '';
if (typeof value === 'object') {
const classes = [];
if (value.base) classes.push(`text-${value.base}`);
if (value.sm) classes.push(`sm:text-${value.sm}`);
if (value.md) classes.push(`md:text-${value.md}`);
if (value.lg) classes.push(`lg:text-${value.lg}`);
if (value.xl) classes.push(`xl:text-${value.xl}`);
if (value['2xl']) classes.push(`2xl:text-${value['2xl']}`);
return classes.join(' ');
}
return `text-${value}`;
const sizeClasses = {
1: 'text-4xl md:text-5xl',
2: 'text-3xl md:text-4xl',
3: 'text-2xl md:text-3xl',
4: 'text-xl md:text-2xl',
5: 'text-lg md:text-xl',
6: 'text-base md:text-lg'
};
const content = icon ? (
<Stack direction="row" align="center" gap={2}>
{icon}
{children}
</Stack>
) : children;
const classes = [
levelClasses[level],
getFontSizeClasses(fontSize),
weight && weightClasses[weight as keyof typeof weightClasses] ? weightClasses[weight as keyof typeof weightClasses] : '',
letterSpacing ? `tracking-${letterSpacing}` : '',
uppercase ? 'uppercase' : '',
groupHoverColor ? `group-hover:text-${groupHoverColor}` : '',
truncate ? 'truncate' : '',
props.className
].filter(Boolean).join(' ');
'text-[var(--ui-color-text-high)]',
weightClasses[weight],
fontSize ? '' : sizeClasses[level],
align === 'center' ? 'text-center' : (align === 'right' ? 'text-right' : 'text-left'),
].join(' ');
return (
<Box
as={Tag}
{...props}
className={classes}
style={{
...(weight && !weightClasses[weight as keyof typeof weightClasses] ? { fontWeight: weight } : {}),
...(props.style || {})
}}
>
{content}
<Box as={Tag} ref={ref} className={classes} fontSize={typeof fontSize === 'string' ? fontSize : undefined} {...props}>
{children}
</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';
interface BarChartProps {
data: { label: string; value: number; color: string }[];
maxValue: number;
export interface HorizontalBarChartItem {
label: string;
value: number;
color?: string;
}
export function HorizontalBarChart({ data, maxValue }: BarChartProps) {
export interface HorizontalBarChartProps {
items?: HorizontalBarChartItem[];
total?: number;
data?: HorizontalBarChartItem[]; // Alias for items
maxValue?: number; // Alias for total
}
export const HorizontalBarChart = ({
items,
total,
data,
maxValue
}: HorizontalBarChartProps) => {
const actualItems = items || data || [];
const actualTotal = total || maxValue || actualItems.reduce((acc, item) => acc + item.value, 0);
return (
<div className="space-y-3">
{data.map((item) => (
<div key={item.label}>
<div className="flex justify-between text-sm mb-1">
<span className="text-gray-400">{item.label}</span>
<span className="text-white font-medium">{item.value}</span>
</div>
<div className="h-2 bg-charcoal-outline rounded-full overflow-hidden">
<div
className={`h-full rounded-full ${item.color} transition-all duration-500 ease-out`}
style={{ width: `${Math.min((item.value / maxValue) * 100, 100)}%` }}
/>
</div>
</div>
))}
</div>
<Box display="flex" flexDirection="col" gap={4}>
{actualItems.map((item, index) => {
const percentage = actualTotal > 0 ? (item.value / actualTotal) * 100 : 0;
return (
<Box key={index}>
<Stack direction="row" justify="between" marginBottom={1}>
<Text size="xs" variant="low" uppercase weight="bold">{item.label}</Text>
<Text size="xs" variant="high" weight="bold">{item.value}</Text>
</Stack>
<Box fullWidth height="0.5rem" bg="var(--ui-color-bg-surface-muted)" style={{ borderRadius: '9999px', overflow: 'hidden' }}>
<Box
fullHeight
bg={item.color || 'var(--ui-color-intent-primary)'}
style={{ width: `${percentage}%`, transition: 'width 0.3s ease-in-out' }}
/>
</Box>
</Box>
);
})}
</Box>
);
}
};

View File

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

View File

@@ -2,17 +2,21 @@ import React from 'react';
import { Box } from './primitives/Box';
import { Text } from './Text';
interface HorizontalStatItemProps {
export interface HorizontalStatItemProps {
label: string;
value: string | number;
color?: string;
intent?: 'primary' | 'success' | 'warning' | 'critical' | 'high' | 'med' | 'low';
}
export function HorizontalStatItem({ label, value, color = 'text-white' }: HorizontalStatItemProps) {
export const HorizontalStatItem = ({
label,
value,
intent = 'high'
}: HorizontalStatItemProps) => {
return (
<Box display="flex" alignItems="center" justifyContent="between" fullWidth>
<Text size="sm" color="text-gray-400">{label}</Text>
<Text weight="semibold" color={color}>{value}</Text>
<Box display="flex" alignItems="center" justifyContent="between" paddingY={2}>
<Text size="sm" variant="low">{label}</Text>
<Text weight="semibold" variant={intent}>{value}</Text>
</Box>
);
}
};

View File

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

View File

@@ -1,54 +1,31 @@
import React from 'react';
import { LucideIcon } from 'lucide-react';
import { Button } from './Button';
import React, { MouseEventHandler } from 'react';
import { Button, ButtonProps } from './Button';
import { Icon } from './Icon';
interface IconButtonProps {
icon: LucideIcon;
onClick?: React.MouseEventHandler<HTMLButtonElement>;
variant?: 'primary' | 'secondary' | 'danger' | 'ghost';
size?: 'sm' | 'md' | 'lg';
export interface IconButtonProps extends Omit<ButtonProps, 'children' | 'icon'> {
icon: any;
title?: string;
disabled?: boolean;
color?: string;
className?: string;
backgroundColor?: string;
}
export function IconButton({
icon,
onClick,
variant = 'secondary',
size = 'md',
export const IconButton = ({
icon,
title,
disabled,
color,
className = '',
backgroundColor,
}: IconButtonProps) {
const sizeMap = {
sm: { w: '8', h: '8', icon: 4 },
md: { w: '10', h: '10', icon: 5 },
lg: { w: '12', h: '12', icon: 6 },
};
size = 'md',
...props
}: IconButtonProps) => {
const iconSizeMap = {
sm: 3,
md: 4,
lg: 5
} as const;
return (
<Button
variant={variant}
onClick={onClick}
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}
<Button
size={size}
{...props}
>
<Icon icon={icon} size={sizeMap[size].icon} color={color} />
<Icon icon={icon} size={iconSizeMap[size]} />
{title && <span className="sr-only">{title}</span>}
</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;
alt: string;
width?: number;
height?: number;
className?: string;
fallbackSrc?: string;
objectFit?: 'cover' | 'contain' | 'fill' | 'none' | 'scale-down';
fill?: boolean;
fullWidth?: boolean;
fullHeight?: boolean;
fallbackComponent?: React.ReactNode;
}
export function Image({
export const Image = ({
src,
alt,
width,
height,
className = '',
fallbackSrc,
objectFit,
fill,
fullWidth,
fullHeight,
fallbackSrc,
fallbackComponent,
...props
}: ImageProps) {
const classes = [
objectFit ? `object-${objectFit}` : '',
fill ? 'absolute inset-0 w-full h-full' : '',
fullWidth ? 'w-full' : '',
fullHeight ? 'h-full' : '',
className
].filter(Boolean).join(' ');
}: ImageProps) => {
const [error, setError] = useState(false);
if (error) {
if (fallbackComponent) return <>{fallbackComponent}</>;
if (fallbackSrc) return <Box as="img" src={fallbackSrc} alt={alt} {...props} />;
return <ImagePlaceholder />;
}
return (
// eslint-disable-next-line @next/next/no-img-element
<img
src={src}
alt={alt}
width={width}
height={height}
className={classes}
onError={(e) => {
if (fallbackSrc) {
(e.target as HTMLImageElement).src = fallbackSrc;
}
}}
{...props}
<Box
as="img"
src={src}
alt={alt}
onError={() => setError(true)}
{...props}
/>
);
}
};

View File

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

View File

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

View File

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

View File

@@ -4,33 +4,32 @@ import { Text } from './Text';
import { Icon } from './Icon';
import { LucideIcon } from 'lucide-react';
interface InfoItemProps {
icon: LucideIcon;
export interface InfoItemProps {
label: string;
value: string | number;
icon: LucideIcon;
intent?: 'primary' | 'success' | 'warning' | 'critical' | 'telemetry' | 'high' | 'med' | 'low';
}
export function InfoItem({ icon, label, value }: InfoItemProps) {
export const InfoItem = ({
label,
value,
icon,
intent = 'med'
}: InfoItemProps) => {
return (
<Box display="flex" alignItems="start" gap={2.5}>
<Box
display="flex"
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 display="flex" alignItems="center" gap={3}>
<Box padding={2} rounded="lg" bg="var(--ui-color-bg-surface-muted)">
<Icon icon={icon} size={4} intent={intent as any} />
</Box>
<Box flexGrow={1} minWidth="0">
<Text size="xs" color="text-gray-500" block mb={0.5} style={{ fontSize: '10px' }}>{label}</Text>
<Text size="xs" weight="medium" color="text-gray-300" block className="truncate">
<Box>
<Text size="xs" variant="low" block marginBottom={0.5} style={{ fontSize: '10px' }}>
{label}
</Text>
<Text size="xs" weight="medium" variant="high" block className="truncate">
{value}
</Text>
</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 { Stack } from './primitives/Stack';
import { Text } from './Text';
interface InputProps extends React.InputHTMLAttributes<HTMLInputElement> {
export interface InputProps extends Omit<InputHTMLAttributes<HTMLInputElement>, 'size'> {
label?: string;
icon?: ReactNode;
errorMessage?: string;
variant?: 'default' | 'error';
error?: string;
hint?: string;
fullWidth?: boolean;
size?: 'sm' | 'md' | 'lg';
}
export const Input = forwardRef<HTMLInputElement, InputProps>(
({ label, icon, errorMessage, variant = 'default', className = '', ...props }, ref) => {
const isError = variant === 'error' || !!errorMessage;
const baseClasses = 'w-full px-4 py-2 bg-deep-graphite border rounded-lg text-white placeholder:text-gray-500 focus:outline-none transition-all duration-150 sm:text-sm';
const variantClasses = isError
? 'border-warning-amber focus:border-warning-amber focus:ring-1 focus:ring-warning-amber'
: 'border-charcoal-outline focus:border-primary-blue focus:ring-1 focus:ring-primary-blue';
const classes = `${baseClasses} ${variantClasses} ${icon ? 'pl-11' : ''} ${className}`;
export const Input = forwardRef<HTMLInputElement, InputProps>(({
label,
error,
hint,
fullWidth = false,
size = 'md',
...props
}, ref) => {
const sizeClasses = {
sm: 'px-3 py-1.5 text-xs',
md: 'px-4 py-2 text-sm',
lg: 'px-4 py-3 text-base'
};
return (
<Stack gap={1.5} fullWidth>
{label && (
<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';
const errorClasses = error ? 'border-[var(--ui-color-intent-critical)]' : '';
const widthClasses = fullWidth ? 'w-full' : '';
const classes = [
baseClasses,
sizeClasses[size],
errorClasses,
widthClasses,
].filter(Boolean).join(' ');
return (
<Box width={fullWidth ? '100%' : undefined}>
{label && (
<Box marginBottom={1.5}>
<Text as="label" size="xs" weight="bold" variant="low">
{label}
</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>
</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';

View File

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

View File

@@ -1,67 +1,44 @@
import { ReactNode } from 'react';
import React, { ReactNode } from 'react';
import { Box } from './primitives/Box';
import { Grid } from './primitives/Grid';
import { Stack } from './primitives/Stack';
/**
* WARNING: DO NOT VIOLATE THE PURPOSE OF THIS COMPONENT.
*
* Layout is a high-level component for page or section layouts.
* It should use Grid or Stack primitives internally.
*
* If you need a specific layout pattern, create a new component.
*/
interface LayoutProps {
export interface LayoutProps {
children: ReactNode;
padding?: string;
gap?: string;
grid?: boolean;
gridCols?: 1 | 2 | 3 | 4;
flex?: boolean;
flexCol?: boolean;
items?: 'start' | 'center' | 'end' | 'stretch';
justify?: 'start' | 'center' | 'end' | 'between' | 'around';
header?: ReactNode;
footer?: ReactNode;
sidebar?: ReactNode;
}
export function Layout({
export const Layout = ({
children,
padding = 'p-6',
gap = 'gap-4',
grid = false,
gridCols = 1,
flex = false,
flexCol = false,
items = 'start',
justify = 'start'
}: LayoutProps) {
if (grid) {
return (
<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>
);
}
header,
footer,
sidebar
}: LayoutProps) => {
return (
<Box className={`${padding} ${gap}`}>
{children}
<Box display="flex" flexDirection="col" minHeight="100vh" bg="var(--ui-color-bg-base)">
{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>
);
}
};

View File

@@ -1,16 +1,31 @@
import React, { ReactNode } from 'react';
import { Box } from './primitives/Box';
import { Surface } from './primitives/Surface';
interface LeaderboardListProps {
export interface LeaderboardListProps {
children: ReactNode;
}
export function LeaderboardList({ children }: LeaderboardListProps) {
export const LeaderboardList = ({ children }: LeaderboardListProps) => {
return (
<Box rounded="xl" bg="bg-iron-gray/30" border={true} borderColor="border-charcoal-outline" overflow="hidden">
<div className="divide-y divide-charcoal-outline/50">
<Surface variant="muted" rounded="xl" style={{ border: '1px solid var(--ui-color-border-default)', overflow: 'hidden' }}>
<Box display="flex" flexDirection="col">
{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>
);
}
};

View File

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

View File

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

View File

@@ -1,26 +1,41 @@
import React from 'react';
import { Box } from './primitives/Box';
interface LoadingSpinnerProps {
size?: number;
color?: string;
className?: string;
export interface LoadingSpinnerProps {
size?: 'sm' | 'md' | 'lg' | number;
intent?: 'primary' | 'high' | 'low';
}
export function LoadingSpinner({ size = 8, color = '#3b82f6', className = '' }: LoadingSpinnerProps) {
export const LoadingSpinner = ({
size = 'md',
intent = 'primary'
}: LoadingSpinnerProps) => {
const sizeMap = {
sm: '1rem',
md: '2rem',
lg: '3rem',
};
const dimension = typeof size === 'string' ? sizeMap[size] : `${size * 0.25}rem`;
const intentColorMap = {
primary: 'var(--ui-color-intent-primary)',
high: 'var(--ui-color-text-high)',
low: 'var(--ui-color-text-low)',
};
return (
<Box
w={`${size * 0.25}rem`}
h={`${size * 0.25}rem`}
rounded="full"
borderWidth="2px"
borderStyle="solid"
borderColor="transparent"
borderTopColor={color}
borderLeftColor={color}
className={`animate-spin ${className}`}
width={dimension}
height={dimension}
style={{
border: '2px solid rgba(255, 255, 255, 0.1)',
borderTop: `2px solid ${intentColorMap[intent]}`,
borderRadius: '50%',
}}
className="animate-spin"
role="status"
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 {
children: React.ReactNode;
export interface MainContentProps {
children: ReactNode;
}
export function MainContent({ children }: MainContentProps) {
return <div className="pt-16 md:pt-20">{children}</div>;
}
export const MainContent = ({ children }: MainContentProps) => {
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 { LucideIcon } from 'lucide-react';
import { Card } from './Card';
import { Box } from './primitives/Box';
import { Text } from './Text';
import { Icon } from './Icon';
import { LucideIcon } from 'lucide-react';
interface MetricCardProps {
export interface MetricCardProps {
label: string;
value: string | number;
icon?: LucideIcon;
color?: string;
intent?: 'primary' | 'success' | 'warning' | 'critical' | 'telemetry';
trend?: {
value: number;
isPositive: boolean;
};
border?: boolean;
bg?: string;
}
/**
* A semantic component for displaying metrics.
* Instrument-grade typography and dense-but-readable hierarchy.
*/
export function MetricCard({
label,
value,
icon,
color = 'text-primary-accent',
trend,
border = true,
bg = 'panel-gray/40',
}: MetricCardProps) {
export const MetricCard = ({
label,
value,
icon,
intent = 'primary',
trend
}: MetricCardProps) => {
return (
<Box
bg={bg}
rounded="none"
p={4}
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">
<Card variant="default">
<Box display="flex" alignItems="start" justifyContent="between" marginBottom={4}>
<Box>
<Text size="xs" weight="bold" variant="low" uppercase>
{label}
</Text>
</Box>
{trend && (
<Text size="xs" color={trend.isPositive ? 'text-success-green' : 'text-red-400'} font="mono">
{trend.isPositive ? '▲' : '▼'} {trend.value}%
<Text size="2xl" weight="bold" variant="high" block marginTop={1}>
{value}
</Text>
</Box>
{icon && (
<Box padding={2} rounded="lg" bg="var(--ui-color-bg-surface-muted)">
<Icon icon={icon} size={5} intent={intent} />
</Box>
)}
</Box>
<Text size="2xl" weight="bold" color="text-white" font="mono">
{typeof value === 'number' ? value.toLocaleString() : value}
</Text>
</Box>
{trend && (
<Box display="flex" alignItems="center" gap={1}>
<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 { Text } from './Text';
interface MiniStatProps {
export interface MiniStatProps {
label: string;
value: string | number;
color?: string;
intent?: 'primary' | 'success' | 'warning' | 'critical' | 'high' | 'med' | 'low';
}
export function MiniStat({ label, value, color = 'text-white' }: MiniStatProps) {
export const MiniStat = ({
label,
value,
intent = 'high'
}: MiniStatProps) => {
return (
<Box textAlign="center" p={2} rounded="lg" bg="bg-charcoal-outline/30">
<Text size="lg" weight="bold" color={color} block>{value}</Text>
<Text size="xs" color="text-gray-500" block style={{ fontSize: '10px' }}>{label}</Text>
<Box textAlign="center" padding={2} rounded="lg" bg="var(--ui-color-bg-surface-muted)">
<Text size="lg" weight="bold" variant={intent} block>{value}</Text>
<Text size="xs" variant="low" block style={{ fontSize: '10px' }}>{label}</Text>
</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 { Stack } from './primitives/Stack';
import { Button } from './Button';
import { Text } from './Text';
import { X } from 'lucide-react';
import { Surface } from './primitives/Surface';
import { IconButton } from './IconButton';
import { Button } from './Button';
import { X } from 'lucide-react';
import { Heading } from './Heading';
import { Text } from './Text';
interface ModalProps {
export interface ModalProps {
children: ReactNode;
isOpen: boolean;
onClose?: () => void;
onOpenChange?: (open: boolean) => void;
onOpenChange?: (isOpen: boolean) => void;
title?: string;
description?: string;
icon?: React.ReactNode;
children: ReactNode;
footer?: ReactNode;
size?: 'sm' | 'md' | 'lg' | 'xl' | 'full';
primaryActionLabel?: string;
onPrimaryAction?: () => void;
secondaryActionLabel?: string;
onSecondaryAction?: () => void;
isLoading?: boolean;
size?: 'sm' | 'md' | 'lg' | 'xl';
footer?: ReactNode;
description?: string;
icon?: ReactNode;
}
export function Modal({
isOpen,
onClose,
export const Modal = ({
children,
isOpen,
onClose,
onOpenChange,
title,
description,
icon,
children,
footer,
size = 'md',
primaryActionLabel,
onPrimaryAction,
secondaryActionLabel,
onSecondaryAction,
isLoading = false,
size = 'md',
}: ModalProps) {
footer,
description,
icon
}: ModalProps) => {
useEffect(() => {
if (isOpen) {
document.body.style.overflow = 'hidden';
} else {
document.body.style.overflow = 'unset';
}
return () => {
document.body.style.overflow = 'unset';
};
}, [isOpen]);
if (!isOpen) return null;
const sizeMap = {
sm: 'max-w-md',
md: 'max-w-lg',
lg: 'max-w-2xl',
xl: 'max-w-4xl',
sm: '24rem',
md: '32rem',
lg: '48rem',
xl: '64rem',
full: '100%',
};
const handleClose = () => {
@@ -53,96 +65,82 @@ export function Modal({
if (onOpenChange) onOpenChange(false);
};
return (
<Box
position="fixed"
inset={0}
zIndex={60}
display="flex"
alignItems="center"
justifyContent="center"
bg="bg-black/60"
px={4}
className="backdrop-blur-sm"
role="dialog"
aria-modal="true"
return createPortal(
<Box
position="fixed"
inset={0}
zIndex={100}
display="flex"
alignItems="center"
justifyContent="center"
padding={4}
bg="rgba(0, 0, 0, 0.8)"
>
{/* Backdrop click to close */}
<Box position="absolute" inset={0} onClick={handleClose} />
<Box
position="relative"
w="full"
maxWidth={sizeMap[size]}
rounded="2xl"
bg="bg-[#0f1115]"
border
borderColor="border-[#262626]"
shadow="2xl"
overflow="hidden"
tabIndex={-1}
<Box
position="absolute"
inset={0}
onClick={handleClose}
/>
<Surface
variant="default"
rounded="lg"
shadow="xl"
style={{
width: '100%',
maxWidth: sizeMap[size],
maxHeight: '90vh',
overflow: 'hidden',
display: 'flex',
flexDirection: 'column',
position: 'relative',
border: '1px solid var(--ui-color-border-default)'
}}
>
{/* Header */}
<Box p={6} borderBottom borderColor="border-white/5">
<Stack direction="row" align="center" justify="between">
<Stack direction="row" align="center" gap={3}>
{icon && <Box>{icon}</Box>}
<Box>
{title && (
<Text size="xl" weight="bold" color="text-white" block>
{title}
</Text>
)}
{description && (
<Text size="sm" color="text-gray-400" block mt={1}>
{description}
</Text>
)}
</Box>
</Stack>
<IconButton
icon={X}
onClick={handleClose}
variant="ghost"
size="sm"
title="Close modal"
/>
</Stack>
<Box
display="flex"
alignItems="center"
justifyContent="between"
padding={4}
borderBottom
>
<Box display="flex" alignItems="center" gap={3}>
{icon}
<Box>
{title && <Heading level={3}>{title}</Heading>}
{description && <Box marginTop={1}><Text size="sm" variant="low">{description}</Text></Box>}
</Box>
</Box>
<IconButton icon={X} onClick={handleClose} variant="ghost" title="Close modal" />
</Box>
{/* Content */}
<Box p={6} overflowY="auto" maxHeight="calc(100vh - 200px)">
<Box flex={1} overflow="auto" padding={6}>
{children}
</Box>
{/* Footer */}
{(primaryActionLabel || secondaryActionLabel || footer) && (
<Box p={6} borderTop borderColor="border-white/5">
{footer || (
<Stack direction="row" justify="end" gap={3}>
{secondaryActionLabel && (
<Button
variant="ghost"
onClick={onSecondaryAction || onClose}
disabled={isLoading}
>
{secondaryActionLabel}
</Button>
)}
{primaryActionLabel && (
<Button
variant="primary"
onClick={onPrimaryAction}
isLoading={isLoading}
>
{primaryActionLabel}
</Button>
)}
</Stack>
{(footer || primaryActionLabel || secondaryActionLabel) && (
<Box padding={4} borderTop bg="rgba(255,255,255,0.02)" display="flex" justifyContent="end" gap={3}>
{footer}
{secondaryActionLabel && (
<Button
onClick={onSecondaryAction || handleClose}
variant="ghost"
>
{secondaryActionLabel}
</Button>
)}
{primaryActionLabel && (
<Button
onClick={onPrimaryAction}
variant="primary"
>
{primaryActionLabel}
</Button>
)}
</Box>
)}
</Box>
</Box>
</Surface>
</Box>,
document.body
);
}
};

View File

@@ -1,129 +1,56 @@
import React from 'react';
import { LucideIcon } from 'lucide-react';
import { Heading } from './Heading';
import { Button } from './Button';
import React, { ReactNode } from 'react';
import { Box } from './primitives/Box';
import { Stack } from './primitives/Stack';
import { Heading } from './Heading';
import { Text } from './Text';
import { Icon } from './Icon';
import { ModalIcon } from './ModalIcon';
import { Surface } from './primitives/Surface';
interface PageHeroProps {
export interface PageHeroProps {
title: string;
description?: string;
icon?: LucideIcon;
backgroundPattern?: React.ReactNode;
stats?: Array<{
icon?: LucideIcon;
value: string | number;
label: string;
color?: string;
animate?: boolean;
}>;
actions?: Array<{
label: string;
onClick: () => void;
variant?: 'primary' | 'secondary';
icon?: LucideIcon;
description?: string;
}>;
children?: React.ReactNode;
className?: string;
children?: ReactNode;
image?: ReactNode;
}
export const PageHero = ({
title,
description,
icon,
backgroundPattern,
stats,
actions,
export const PageHero = ({
title,
description,
children,
className = ''
}: PageHeroProps) => (
<Box
as="section"
position="relative"
overflow="hidden"
rounded="2xl"
bg="bg-gradient-to-br from-iron-gray/80 via-deep-graphite to-iron-gray/60"
border={true}
borderColor="border-charcoal-outline/50"
className={className}
>
{/* 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>
)}
image
}: PageHeroProps) => {
return (
<Surface
variant="dark"
rounded="xl"
padding={8}
style={{ position: 'relative', overflow: 'hidden', border: '1px solid var(--ui-color-border-default)' }}
>
<Box display="flex" flexDirection={{ base: 'col', lg: 'row' }} alignItems="center" gap={8}>
<Box flex={1}>
<Heading level={1} marginBottom={4}>{title}</Heading>
{description && (
<Text size="lg" color="text-gray-400" block style={{ lineHeight: 1.625 }}>
<Text size="lg" variant="low" marginBottom={6} block>
{description}
</Text>
)}
{/* 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>
)}
{children}
</Box>
{/* Actions or Custom Content */}
{actions && actions.length > 0 && (
<Stack gap={4}>
{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>
{image && (
<Box flex={1} display="flex" justifyContent="center">
{image}
</Box>
)}
{children}
</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 { ChevronLeft, ChevronRight } from 'lucide-react';
import React from 'react';
import { Box } from './primitives/Box';
import { Button } from './Button';
import { Icon } from './Icon';
import { Stack } from './primitives/Stack';
import { Text } from './Text';
import { ChevronLeft, ChevronRight } from 'lucide-react';
interface PaginationProps {
export interface PaginationProps {
currentPage: number;
totalPages: number;
totalItems: number;
itemsPerPage: number;
onPageChange: (page: number) => void;
}
export function Pagination({
currentPage,
totalPages,
totalItems,
itemsPerPage,
onPageChange,
}: PaginationProps) {
if (totalPages <= 1) return null;
const startItem = ((currentPage - 1) * itemsPerPage) + 1;
const endItem = Math.min(currentPage * itemsPerPage, totalItems);
const getPageNumbers = () => {
if (totalPages <= 5) {
return Array.from({ length: totalPages }, (_, i) => i + 1);
}
if (currentPage <= 3) {
return [1, 2, 3, 4, 5];
}
if (currentPage >= totalPages - 2) {
return [totalPages - 4, totalPages - 3, totalPages - 2, totalPages - 1, totalPages];
}
return [currentPage - 2, currentPage - 1, currentPage, currentPage + 1, currentPage + 2];
};
export const Pagination = ({
currentPage,
totalPages,
onPageChange
}: PaginationProps) => {
const pages = Array.from({ length: totalPages }, (_, i) => i + 1);
const visiblePages = pages.filter(page => {
if (totalPages <= 7) return true;
if (page === 1 || page === totalPages) return true;
if (page >= currentPage - 1 && page <= currentPage + 1) return true;
return false;
});
return (
<Box display="flex" alignItems="center" justifyContent="between" pt={4}>
<Text size="sm" color="text-gray-500">
Showing {startItem}{endItem} of {totalItems}
<Box display="flex" alignItems="center" justifyContent="between" paddingTop={4}>
<Text size="sm" variant="low">
Page {currentPage} of {totalPages}
</Text>
<Stack direction="row" align="center" gap={2}>
<Box display="flex" alignItems="center" gap={2}>
<Button
variant="secondary"
onClick={() => onPageChange(Math.max(1, currentPage - 1))}
disabled={currentPage === 1}
variant="ghost"
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>
<Stack direction="row" align="center" gap={1}>
{getPageNumbers().map(pageNum => (
<Button
key={pageNum}
variant={currentPage === pageNum ? 'primary' : 'ghost'}
onClick={() => onPageChange(pageNum)}
className="w-10 h-10 p-0"
>
{pageNum}
</Button>
))}
</Stack>
<Box display="flex" alignItems="center" gap={1}>
{visiblePages.map((page, index) => {
const prevPage = visiblePages[index - 1];
const showEllipsis = prevPage && page - prevPage > 1;
return (
<React.Fragment key={page}>
{showEllipsis && <Text variant="low">...</Text>}
<Button
variant={page === currentPage ? 'primary' : 'ghost'}
size="sm"
onClick={() => onPageChange(page)}
style={{ minWidth: '2.5rem', padding: 0 }}
>
{page}
</Button>
</React.Fragment>
);
})}
</Box>
<Button
variant="secondary"
onClick={() => onPageChange(Math.min(totalPages, currentPage + 1))}
disabled={currentPage === totalPages}
variant="ghost"
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>
</Stack>
</Box>
</Box>
);
}
};

View File

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

View File

@@ -1,42 +1,35 @@
import React, { ComponentProps } from 'react';
import { Eye, EyeOff, Lock } from 'lucide-react';
import { Input } from './Input';
import React, { useState } from 'react';
import { Input, InputProps } from './Input';
import { Eye, EyeOff } from 'lucide-react';
import { IconButton } from './IconButton';
import { Box } from './primitives/Box';
interface PasswordFieldProps extends ComponentProps<typeof Input> {
showPassword?: boolean;
onTogglePassword?: () => void;
}
export interface PasswordFieldProps extends InputProps {}
export const PasswordField = (props: PasswordFieldProps) => {
const [showPassword, setShowPassword] = useState(false);
/**
* PasswordField
*
* A specialized input for passwords with visibility toggling.
* Stateless UI component.
*/
export function PasswordField({ showPassword, onTogglePassword, ...props }: PasswordFieldProps) {
return (
<Box position="relative" fullWidth>
<Box position="relative">
<Input
{...props}
type={showPassword ? 'text' : 'password'}
icon={<Lock size={16} />}
/>
{onTogglePassword && (
<Box
as="button"
type="button"
onClick={onTogglePassword}
position="absolute"
right="3"
top={props.label ? "34px" : "50%"}
style={props.label ? {} : { transform: 'translateY(-50%)' }}
zIndex={10}
className="text-gray-500 hover:text-gray-300 transition-colors"
>
{showPassword ? <EyeOff size={16} /> : <Eye size={16} />}
</Box>
)}
<Box
position="absolute"
right="0.5rem"
top="50%"
style={{ transform: 'translateY(-50%)' }}
zIndex={10}
>
<IconButton
icon={showPassword ? EyeOff : Eye}
onClick={() => setShowPassword(!showPassword)}
variant="ghost"
size="sm"
title={showPassword ? 'Hide password' : 'Show password'}
/>
</Box>
</Box>
);
}
};

View File

@@ -1,21 +1,34 @@
import { User } from 'lucide-react';
import React from 'react';
import { Box } from './primitives/Box';
import { Icon } from './Icon';
import { User } from 'lucide-react';
export interface PlaceholderImageProps {
size?: number;
width?: string | number;
height?: string | number;
size?: string | number;
className?: string;
}
export function PlaceholderImage({ size = 48, className = '' }: PlaceholderImageProps) {
export const PlaceholderImage = ({
width,
height,
size,
className
}: PlaceholderImageProps) => {
const dimension = size || '100%';
return (
<Box
className={`rounded-full bg-charcoal-outline flex items-center justify-center ${className}`}
style={{ width: size, height: size }}
<Box
width={width || dimension}
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>
);
}
};

View File

@@ -1,70 +1,80 @@
import { Trophy } from 'lucide-react';
import { ReactNode } from 'react';
import React, { ReactNode } from 'react';
import { Box } from './primitives/Box';
import { Heading } from './Heading';
import { Icon } from './Icon';
import { Stack } from './primitives/Stack';
import { Text } from './Text';
import { Avatar } from './Avatar';
import { Trophy } from 'lucide-react';
import { Icon } from './Icon';
import { Heading } from './Heading';
interface PodiumProps {
title: string;
children: ReactNode;
export interface PodiumEntry {
name: string;
avatar?: string;
value: string | number;
position: 1 | 2 | 3;
}
export function Podium({ title, children }: PodiumProps) {
return (
<Box bg="bg-iron-gray/50" rounded="2xl" border style={{ borderColor: 'rgba(38, 38, 38, 0.8)' }} p={8} mb={10}>
<Box display="flex" justifyContent="center" mb={8}>
<Stack direction="row" align="center" gap={2}>
<Icon icon={Trophy} size={6} color="var(--warning-amber)" />
<Heading level={2}>{title}</Heading>
</Stack>
</Box>
export interface PodiumProps {
entries?: PodiumEntry[];
title?: string;
children?: ReactNode;
}
<Stack direction="row" align="end" justify="center" gap={8}>
{children}
</Stack>
export const Podium = ({ entries = [], title, children }: PodiumProps) => {
const sortedEntries = [...entries].sort((a, b) => {
const order = { 2: 0, 1: 1, 3: 2 };
return order[a.position] - order[b.position];
});
const getPositionColor = (pos: number) => {
if (pos === 1) return 'var(--ui-color-intent-warning)';
if (pos === 2) return '#A1A1AA';
if (pos === 3) return '#CD7F32';
return 'var(--ui-color-text-low)';
};
return (
<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>
);
}
};
interface PodiumItemProps {
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>
);
}
export const PodiumItem = ({ children }: { children: ReactNode }) => <>{children}</>;

View File

@@ -1,122 +1,56 @@
import type { MouseEventHandler, ReactNode } from 'react';
import { Card } from './Card';
interface PresetCardStat {
label: string;
value: string;
}
import React from 'react';
import { Box } from './primitives/Box';
import { Text } from './Text';
import { Surface } from './primitives/Surface';
import { Icon } from './Icon';
import { LucideIcon } from 'lucide-react';
export interface PresetCardProps {
title: string;
subtitle?: string;
primaryTag?: string;
description?: string;
stats?: PresetCardStat[];
selected?: boolean;
disabled?: boolean;
onSelect?: () => void;
className?: string;
children?: ReactNode;
description: string;
icon: LucideIcon;
onClick: () => void;
isSelected?: boolean;
}
export function PresetCard({
title,
subtitle,
primaryTag,
description,
stats,
selected,
disabled,
onSelect,
className = '',
children,
}: PresetCardProps) {
const isInteractive = typeof onSelect === 'function' && !disabled;
const handleClick: MouseEventHandler<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>
);
}
export const PresetCard = ({
title,
description,
icon,
onClick,
isSelected = false
}: PresetCardProps) => {
return (
<Card
className={commonClasses}
onClick={handleClick as MouseEventHandler<HTMLDivElement>}
<Surface
variant={isSelected ? 'default' : 'muted'}
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}
</Card>
<Box display="flex" alignItems="start" gap={4}>
<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 { Box, BoxProps } from './primitives/Box';
import { Box } from './primitives/Box';
interface ProgressBarProps extends Omit<BoxProps<'div'>, 'children'> {
export interface ProgressBarProps {
value: number;
max: number;
color?: string;
bg?: string;
height?: string;
max?: number;
intent?: 'primary' | 'success' | 'warning' | 'critical' | 'telemetry';
color?: string; // Alias for intent
size?: 'sm' | 'md' | 'lg';
marginBottom?: any;
mb?: any; // Alias for marginBottom
}
export function ProgressBar({
value,
max,
color = 'bg-primary-blue',
bg = 'bg-deep-graphite',
height = '2',
...props
}: ProgressBarProps) {
const percentage = Math.min((value / max) * 100, 100);
export const ProgressBar = ({
value,
max = 100,
intent = 'primary',
color: colorProp,
size = 'md',
marginBottom,
mb
}: ProgressBarProps) => {
const percentage = Math.min(Math.max((value / max) * 100, 0), 100);
const intentColorMap = {
primary: 'var(--ui-color-intent-primary)',
success: 'var(--ui-color-intent-success)',
warning: 'var(--ui-color-intent-warning)',
critical: 'var(--ui-color-intent-critical)',
telemetry: 'var(--ui-color-intent-telemetry)',
};
const color = colorProp || intentColorMap[intent];
const sizeMap = {
sm: '0.25rem',
md: '0.5rem',
lg: '1rem',
};
return (
<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
bg={color}
rounded="full"
fullHeight
style={{ width: `${percentage}%` }}
className="transition-all duration-500 ease-out"
bg={color}
style={{ width: `${percentage}%`, transition: 'width 0.3s ease-in-out' }}
/>
</Box>
);
}
};

View File

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

View File

@@ -1,33 +1,36 @@
import React from 'react';
import { Box } from './primitives/Box';
import { Text } from './Text';
import { Icon } from './Icon';
import { LucideIcon, ArrowRight } from 'lucide-react';
import { Link } from './Link';
interface QuickActionLinkProps {
export interface QuickActionLinkProps {
label: string;
icon: LucideIcon;
href: string;
children: React.ReactNode;
variant?: 'blue' | 'purple' | 'orange';
className?: string;
}
export function QuickActionLink({
href,
children,
variant = 'blue',
className = ''
}: QuickActionLinkProps) {
const variantClasses = {
blue: 'bg-primary-blue/20 border-primary-blue/30 text-primary-blue hover:bg-primary-blue/30',
purple: 'bg-purple-500/20 border-purple-500/30 text-purple-300 hover:bg-purple-500/30',
orange: 'bg-orange-500/20 border-orange-500/30 text-orange-300 hover:bg-orange-500/30'
};
const classes = [
'px-4 py-3 border rounded-lg transition-colors text-sm font-medium text-center inline-block w-full',
variantClasses[variant],
className
].filter(Boolean).join(' ');
export const QuickActionLink = ({
label,
icon,
href
}: QuickActionLinkProps) => {
return (
<a href={href} className={classes}>
{children}
</a>
<Link href={href} underline="none">
<Box
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 { LucideIcon } from 'lucide-react';
interface QuickAction {
export interface QuickAction {
label: string;
icon: LucideIcon;
onClick: () => void;
variant?: 'primary' | 'secondary' | 'ghost';
intent?: 'primary' | 'secondary' | 'danger';
}
interface QuickActionsPanelProps {
export interface QuickActionsPanelProps {
actions: QuickAction[];
className?: string;
}
/**
* QuickActionsPanel
*
* Provides fast access to common dashboard tasks.
*/
export function QuickActionsPanel({ actions, className = '' }: QuickActionsPanelProps) {
export const QuickActionsPanel = ({
actions
}: QuickActionsPanelProps) => {
return (
<Panel title="Quick Actions" className={className}>
<Box display="grid" gridCols={1} gap={2}>
<Panel title="Quick Actions">
<Box display="flex" flexDirection="col" gap={2}>
{actions.map((action, index) => (
<Button
key={index}
variant={action.variant || 'secondary'}
<Button
key={index}
variant={action.intent === 'danger' ? 'danger' : 'secondary'}
onClick={action.onClick}
fullWidth
style={{ justifyContent: 'flex-start', height: 'auto', padding: '12px' }}
>
<Box display="flex" align="center" gap={3}>
<Icon icon={action.icon} size={5} />
<span>{action.label}</span>
<Box display="flex" alignItems="center" gap={3} fullWidth>
<Icon icon={action.icon} size={4} />
{action.label}
</Box>
</Button>
))}
</Box>
</Panel>
);
}
};

View File

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

View File

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

View File

@@ -1,82 +1,52 @@
import React from 'react';
import { Box } from './primitives/Box';
import { Stack } from './primitives/Stack';
import { Text } from './Text';
import { Surface } from './primitives/Surface';
interface SegmentedControlOption {
value: string;
export interface SegmentedControlOption {
id: string;
label: string;
description?: string;
disabled?: boolean;
icon?: React.ReactNode;
}
interface SegmentedControlProps {
export interface SegmentedControlProps {
options: SegmentedControlOption[];
value: string;
onChange?: (value: string) => void;
activeId: string;
onChange: (id: string) => void;
fullWidth?: boolean;
}
export function SegmentedControl({
options,
value,
export const SegmentedControl = ({
options,
activeId,
onChange,
}: SegmentedControlProps) {
const handleSelect = (optionValue: string, optionDisabled?: boolean) => {
if (!onChange || optionDisabled) return;
if (optionValue === value) return;
onChange(optionValue);
};
fullWidth = false
}: SegmentedControlProps) => {
return (
<Stack
direction="row"
<Surface
variant="muted"
rounded="lg"
padding={1}
display="inline-flex"
w="full"
flexWrap="wrap"
gap={2}
rounded="full"
bg="bg-black/60"
p={1}
width={fullWidth ? '100%' : undefined}
>
{options.map((option) => {
const isSelected = option.value === value;
const isSelected = option.id === activeId;
return (
<Box
key={option.value}
as="button"
type="button"
onClick={() => handleSelect(option.value, option.disabled)}
aria-pressed={isSelected}
disabled={option.disabled}
flex={1}
minWidth="140px"
px={3}
py={1.5}
rounded="full"
transition="all 0.2s"
textAlign="left"
bg={isSelected ? 'bg-primary-blue' : 'transparent'}
color={isSelected ? 'text-white' : 'text-gray-400'}
opacity={option.disabled ? 0.5 : 1}
cursor={option.disabled ? 'not-allowed' : 'pointer'}
border="none"
<button
key={option.id}
onClick={() => onChange(option.id)}
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 ${
isSelected
? 'bg-[var(--ui-color-bg-surface)] text-[var(--ui-color-intent-primary)] shadow-sm'
: 'text-[var(--ui-color-text-low)] hover:text-[var(--ui-color-text-high)]'
}`}
>
<Stack gap={0.5}>
<Text size="xs" weight="medium" color="inherit">{option.label}</Text>
{option.description && (
<Text
size="xs"
color={isSelected ? 'text-white' : 'text-gray-400'}
fontSize="10px"
opacity={isSelected ? 0.8 : 1}
>
{option.description}
</Text>
)}
</Stack>
</Box>
{option.icon}
{option.label}
</button>
);
})}
</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 { Stack } from './primitives/Stack';
import { Text } from './Text';
interface SelectOption {
value: string;
export interface SelectOption {
value: string | number;
label: string;
}
interface SelectProps extends React.SelectHTMLAttributes<HTMLSelectElement> {
export interface SelectProps extends Omit<SelectHTMLAttributes<HTMLSelectElement>, 'size'> {
label?: string;
options: SelectOption[];
error?: string;
hint?: string;
fullWidth?: boolean;
pl?: number;
errorMessage?: string;
variant?: 'default' | 'error';
options?: SelectOption[];
size?: 'sm' | 'md' | 'lg';
}
export const Select = forwardRef<HTMLSelectElement, SelectProps>(
({ label, fullWidth = true, pl, errorMessage, variant = 'default', options, children, className = '', style, ...props }, ref) => {
const isError = variant === 'error' || !!errorMessage;
const variantClasses = isError
? 'border-warning-amber focus:border-warning-amber'
: 'border-charcoal-outline focus:border-primary-blue';
const defaultClasses = `${fullWidth ? 'w-full' : 'w-auto'} px-3 py-2 bg-deep-graphite border rounded-lg text-white focus:outline-none transition-colors`;
const classes = [
defaultClasses,
variantClasses,
pl ? `pl-${pl}` : '',
className
].filter(Boolean).join(' ');
export const Select = forwardRef<HTMLSelectElement, SelectProps>(({
label,
options,
error,
hint,
fullWidth = false,
size = 'md',
...props
}, ref) => {
const sizeClasses = {
sm: 'px-3 py-1.5 text-xs',
md: 'px-4 py-2 text-sm',
lg: 'px-4 py-3 text-base'
};
return (
<Stack gap={1.5} fullWidth={fullWidth}>
{label && (
<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)] focus:outline-none focus:border-[var(--ui-color-intent-primary)] transition-colors appearance-none';
const errorClasses = error ? 'border-[var(--ui-color-intent-critical)]' : '';
const widthClasses = fullWidth ? 'w-full' : '';
const classes = [
baseClasses,
sizeClasses[size],
errorClasses,
widthClasses,
].filter(Boolean).join(' ');
return (
<Box width={fullWidth ? '100%' : undefined}>
{label && (
<Box marginBottom={1.5}>
<Text as="label" size="xs" weight="bold" variant="low">
{label}
</Text>
)}
<Box
as="select"
ref={ref}
className={classes}
style={style}
</Box>
)}
<div className="relative">
<select
ref={ref}
className={classes}
{...props}
>
{options ? options.map(opt => (
<option key={opt.value} value={opt.value}>{opt.label}</option>
)) : children}
</Box>
{errorMessage && (
<Text size="xs" color="text-warning-amber" mt={1}>
{errorMessage}
{options.map((option) => (
<option key={option.value} value={option.value}>
{option.label}
</option>
))}
</select>
<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>
)}
</Stack>
);
}
);
</Box>
)}
{hint && !error && (
<Box marginTop={1}>
<Text size="xs" variant="low">
{hint}
</Text>
</Box>
)}
</Box>
);
});
Select.displayName = 'Select';

View File

@@ -1,37 +1,66 @@
import React from 'react';
import { ChevronRight, LucideIcon } from 'lucide-react';
import { Box } from './primitives/Box';
import { Text } from './Text';
import { Icon } from './Icon';
import { LucideIcon, ChevronRight } from 'lucide-react';
import { Link } from './Link';
interface SidebarActionLinkProps {
href: string;
icon: LucideIcon;
export interface SidebarActionLinkProps {
label: string;
iconColor?: string;
iconBgColor?: string;
icon: LucideIcon;
href: string;
isActive?: boolean;
}
export function SidebarActionLink({
export const SidebarActionLink = ({
label,
icon,
href,
icon,
label,
iconColor = 'text-primary-blue',
iconBgColor = 'bg-primary-blue/10',
}: SidebarActionLinkProps) {
isActive = false
}: SidebarActionLinkProps) => {
return (
<Link
href={href}
variant="ghost"
block
className="flex items-center gap-3 p-3 rounded-lg bg-deep-graphite hover:bg-iron-gray/50 transition-all"
<Link
href={href}
variant="ghost"
underline="none"
>
<Box p={2} className={iconBgColor} rounded="lg" display="flex" center>
<Icon icon={icon} size={4} className={iconColor} />
<Box
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>
<Text size="sm" color="text-white" flexGrow={1}>{label}</Text>
<Icon icon={ChevronRight} size={4} color="text-gray-500" />
</Link>
);
}
};

View File

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

View File

@@ -1,23 +1,44 @@
import React from 'react';
import { Box } from './primitives/Box';
interface SkeletonProps {
export interface SkeletonProps {
width?: string | number;
height?: string | number;
circle?: boolean;
className?: string;
variant?: 'text' | 'circular' | 'rectangular';
animation?: 'pulse' | 'wave' | 'none';
}
export function Skeleton({ width, height, circle, className = '' }: SkeletonProps) {
export const Skeleton = ({
width,
height,
variant = 'rectangular',
animation = 'pulse'
}: SkeletonProps) => {
const variantClasses = {
text: 'rounded-sm',
circular: 'rounded-full',
rectangular: 'rounded-none',
};
const animationClasses = {
pulse: 'animate-pulse',
wave: 'animate-shimmer', // Assuming shimmer is defined
none: '',
};
const classes = [
'bg-[var(--ui-color-bg-surface-muted)]',
variantClasses[variant],
animationClasses[animation],
].join(' ');
return (
<Box
w={width}
h={height}
rounded={circle ? 'full' : 'md'}
bg="bg-white/5"
className={`animate-pulse ${className}`}
width={width}
height={height}
className={classes}
role="status"
aria-label="Loading..."
/>
);
}
};

View File

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

View File

@@ -1,115 +1,68 @@
import React, { ReactNode } from 'react';
import { Box } from './primitives/Box';
import { Stack } from './primitives/Stack';
import { Text } from './Text';
import { Card } from './Card';
import { Box } from './primitives/Box';
import { Text } from './Text';
import { Icon } from './Icon';
import { LucideIcon } from 'lucide-react';
interface StatCardProps {
export interface StatCardProps {
label: string;
value: string | number;
icon?: LucideIcon;
intent?: 'primary' | 'success' | 'warning' | 'critical' | 'telemetry';
trend?: {
value: number;
isPositive: boolean;
};
variant?: 'default' | 'primary' | 'success' | 'warning' | 'danger' | 'info';
className?: string;
onClick?: () => void;
prefix?: string;
suffix?: string;
delay?: number;
footer?: ReactNode;
}
export function StatCard({
label,
value,
icon,
export const StatCard = ({
label,
value,
icon,
intent = 'primary',
trend,
variant = 'default',
className = '',
onClick,
prefix,
suffix,
delay,
}: StatCardProps) {
const variantClasses = {
default: 'bg-panel-gray border-border-gray',
primary: 'bg-primary-accent/5 border-primary-accent/20',
success: 'bg-success-green/5 border-success-green/20',
warning: 'bg-warning-amber/5 border-warning-amber/20',
danger: 'bg-critical-red/5 border-critical-red/20',
info: 'bg-telemetry-aqua/5 border-telemetry-aqua/20',
};
const iconBgClasses = {
default: 'bg-white/5',
primary: 'bg-primary-accent/10',
success: 'bg-success-green/10',
warning: 'bg-warning-amber/10',
danger: 'bg-critical-red/10',
info: 'bg-telemetry-aqua/10',
};
const iconColorClasses = {
default: 'text-gray-400',
primary: 'text-primary-accent',
success: 'text-success-green',
warning: 'text-warning-amber',
danger: 'text-critical-red',
info: 'text-telemetry-aqua',
};
const cardContent = (
<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">
footer
}: StatCardProps) => {
return (
<Card variant="default">
<Box display="flex" alignItems="start" justifyContent="between" marginBottom={4}>
<Box>
<Text size="xs" weight="bold" variant="low" uppercase>
{label}
</Text>
{icon && (
<Box
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 size="2xl" weight="bold" variant="high" block marginTop={1}>
{value}
</Text>
{trend && (
<Stack direction="row" align="center" gap={1}>
<Text
size="xs"
weight="bold"
color={trend.isPositive ? 'text-success-green' : 'text-critical-red'}
>
{trend.isPositive ? '+' : ''}{trend.value}%
</Text>
<Text size="xs" color="text-gray-500">
vs last period
</Text>
</Stack>
)}
</Stack>
</Stack>
</Box>
{icon && (
<Box padding={2} rounded="lg" bg="var(--ui-color-bg-surface-muted)">
<Icon icon={icon} size={5} intent={intent} />
</Box>
)}
</Box>
{trend && (
<Box display="flex" alignItems="center" gap={1} marginBottom={footer ? 4 : 0}>
<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>
)}
{footer && (
<Box borderTop paddingTop={4}>
{footer}
</Box>
)}
</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 { Grid } from './primitives/Grid';
import { GridItem } from './primitives/GridItem';
import { Surface } from './primitives/Surface';
import { Text } from './Text';
import { Stack } from './primitives/Stack';
import { StatBox, StatBoxProps } from './StatBox';
type GridCols = 1 | 2 | 3 | 4 | 5 | 6 | 12;
interface StatItem {
label: string;
value: string | number;
subValue?: string;
color?: string;
icon?: React.ElementType;
export interface StatGridProps {
stats: StatBoxProps[];
columns?: number;
}
interface StatGridProps {
stats: StatItem[];
cols?: GridCols;
mdCols?: GridCols;
lgCols?: GridCols;
className?: string;
}
export function StatGrid({ stats, cols = 2, mdCols = 3, lgCols = 4, className = '' }: StatGridProps) {
export const StatGrid = ({
stats,
columns = 3
}: StatGridProps) => {
return (
<Grid
cols={cols}
mdCols={mdCols}
lgCols={lgCols}
gap={4}
className={className}
>
<Grid columns={columns} gap={4}>
{stats.map((stat, index) => (
<GridItem key={index}>
<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>
<StatBox key={index} {...stat} />
))}
</Grid>
);
}
};

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,110 +1,94 @@
import { LucideIcon } from 'lucide-react';
import React from 'react';
import { Box } from './primitives/Box';
import { Text } from './Text';
import { Icon } from './Icon';
import { StatusDot } from './StatusDot';
import { Badge } from './Badge';
interface StatusIndicatorProps {
icon: LucideIcon;
label: string;
export { Badge };
export interface StatusIndicatorProps {
status?: 'live' | 'upcoming' | 'completed' | 'cancelled' | 'pending';
variant?: string; // Alias for status
label?: string;
subLabel?: string;
variant: 'success' | 'warning' | 'danger' | 'info';
size?: 'sm' | 'md' | 'lg';
icon?: any; // Alias for status dot
}
export function StatusIndicator({ icon, label, subLabel, variant }: StatusIndicatorProps) {
const colors = {
success: {
text: 'text-performance-green',
bg: 'bg-green-500/10',
border: 'border-green-500/30',
icon: 'rgb(16, 185, 129)'
export const StatusIndicator = ({
status,
variant,
label,
subLabel,
size = 'md',
icon
}: StatusIndicatorProps) => {
const activeStatus = (status || variant || 'pending') as any;
const configMap: any = {
live: {
intent: 'success' as const,
pulse: true,
text: 'Live',
},
warning: {
text: 'text-warning-amber',
bg: 'bg-yellow-500/10',
border: 'border-yellow-500/30',
icon: 'rgb(245, 158, 11)'
upcoming: {
intent: 'primary' as const,
pulse: false,
text: 'Upcoming',
},
completed: {
intent: 'telemetry' as const,
pulse: false,
text: 'Completed',
},
cancelled: {
intent: 'critical' as const,
pulse: false,
text: 'Cancelled',
},
pending: {
intent: 'warning' as const,
pulse: false,
text: 'Pending',
},
success: {
intent: 'success' as const,
pulse: false,
text: 'Success',
},
danger: {
text: 'text-red-400',
bg: 'bg-red-500/10',
border: 'border-red-500/30',
icon: 'rgb(239, 68, 68)'
intent: 'critical' as const,
pulse: false,
text: 'Danger',
},
warning: {
intent: 'warning' as const,
pulse: false,
text: 'Warning',
},
info: {
text: 'text-primary-blue',
bg: 'bg-blue-500/10',
border: 'border-blue-500/30',
icon: 'rgb(59, 130, 246)'
}
};
const config = colors[variant];
const config = configMap[activeStatus] || configMap.pending;
return (
<Box
display="flex"
alignItems="center"
justifyContent="between"
p={2}
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}
<Box display="flex" alignItems="center" gap={2}>
<StatusDot intent={config.intent} pulse={config.pulse} size={size === 'lg' ? 'lg' : size === 'sm' ? 'sm' : 'md'} />
<Box>
<Text size={size === 'lg' ? 'md' : 'sm'} weight="bold" variant="high" uppercase>
{label || config.text}
</Text>
)}
{subLabel && <Text size="xs" variant="low">{subLabel}</Text>}
</Box>
</Box>
);
}
};
interface StatRowProps {
label: string;
value: string | number;
valueColor?: string;
valueFont?: 'sans' | 'mono';
}
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>
export const StatRow = ({ label, value, subLabel, variant, valueColor, valueFont }: { label: string, value: string, subLabel?: string, variant?: string, valueColor?: string, valueFont?: string }) => (
<Box display="flex" alignItems="center" justifyContent="between" paddingY={2} borderBottom>
<Box>
<Text size="sm" variant="high">{label}</Text>
{subLabel && <Text size="xs" variant="low">{subLabel}</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>
);
}
<Text size="sm" weight="bold" variant={variant as any || 'high'} color={valueColor} font={valueFont as any}>{value}</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 {
id: number;
export interface Step {
id: string;
label: string;
icon: React.ElementType;
}
interface StepIndicatorProps {
currentStep: number;
steps?: Step[];
export interface StepIndicatorProps {
steps: Step[];
currentStepId: string;
completedStepIds: string[];
}
const DEFAULT_STEPS: Step[] = [
{ id: 1, label: 'Personal', icon: User },
{ id: 2, label: 'Avatar', icon: Camera },
];
export function StepIndicator({ currentStep, steps = DEFAULT_STEPS }: StepIndicatorProps) {
export const StepIndicator = ({
steps,
currentStepId,
completedStepIds
}: StepIndicatorProps) => {
return (
<div className="flex items-center justify-center gap-2 mb-8">
<Box display="flex" alignItems="center" gap={4}>
{steps.map((step, index) => {
const Icon = step.icon;
const isCompleted = step.id < currentStep;
const isCurrent = step.id === currentStep;
const isCurrent = step.id === currentStepId;
const isCompleted = completedStepIds.includes(step.id);
const isLast = index === steps.length - 1;
return (
<div key={step.id} className="flex items-center">
<div className="flex flex-col items-center">
<div
className={`flex h-12 w-12 items-center justify-center rounded-full transition-all duration-300 ${
isCurrent
? 'bg-primary-blue text-white shadow-lg shadow-primary-blue/30'
: isCompleted
? 'bg-performance-green text-white'
: 'bg-iron-gray border border-charcoal-outline text-gray-500'
}`}
<React.Fragment key={step.id}>
<Box display="flex" alignItems="center" gap={2}>
<Box
width="2rem"
height="2rem"
display="flex"
alignItems="center"
justifyContent="center"
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 ? (
<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>
<span
className={`mt-2 text-xs font-medium ${
isCurrent ? 'text-white' : isCompleted ? 'text-performance-green' : 'text-gray-500'
}`}
>
</Box>
<Text size="sm" weight={isCurrent ? 'bold' : 'medium'} variant={isCurrent ? 'high' : 'low'}>
{step.label}
</span>
</div>
{index < steps.length - 1 && (
<div
className={`w-16 h-0.5 mx-4 mt-[-20px] ${
isCompleted ? 'bg-performance-green' : 'bg-charcoal-outline'
}`}
/>
</Text>
</Box>
{!isLast && (
<Box flex={1} height="2px" bg="var(--ui-color-border-muted)" minWidth="2rem" />
)}
</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 { Text } from './Text';
import { Surface } from './primitives/Surface';
import { Icon } from './Icon';
import { LucideIcon } from 'lucide-react';
interface SummaryItemProps {
label?: string;
value?: string | number;
icon?: LucideIcon;
export interface SummaryItemProps {
label: string;
value: string | number;
icon: LucideIcon;
intent?: 'primary' | 'success' | 'warning' | 'critical' | 'telemetry';
onClick?: () => void;
title?: string;
subtitle?: string;
rightContent?: React.ReactNode;
}
export function SummaryItem({ label, value, icon, onClick, title, subtitle, rightContent }: SummaryItemProps) {
export const SummaryItem = ({
label,
value,
icon,
intent = 'primary',
onClick
}: SummaryItemProps) => {
return (
<Surface
variant="muted"
rounded="lg"
p={4}
display="flex"
alignItems="center"
gap={4}
cursor={onClick ? 'pointer' : 'default'}
<Surface
variant="muted"
rounded="lg"
padding={4}
onClick={onClick}
hoverBg={onClick ? 'bg-white/5' : undefined}
transition={!!onClick}
style={{ cursor: onClick ? 'pointer' : 'default' }}
>
{icon && (
<Box p={2} rounded="lg" bg="bg-white/5">
<Icon icon={icon} size={5} color="text-gray-400" />
<Box display="flex" alignItems="center" gap={4}>
<Box padding={2} rounded="lg" bg="var(--ui-color-bg-surface-muted)">
<Icon icon={icon} size={5} intent={intent} />
</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>
{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>
</Surface>
);
}
};

View File

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

View File

@@ -1,95 +1,87 @@
import React, { ReactNode, ElementType } from 'react';
import { Box, BoxProps } from './primitives/Box';
import React, { ReactNode } from 'react';
import { Box } from './primitives/Box';
import { Surface } from './primitives/Surface';
interface TableProps extends BoxProps<'table'> {
export interface TableProps {
children: ReactNode;
className?: string;
}
export function Table({ children, className = '', ...props }: TableProps) {
const { border, translate, ...rest } = props;
export const Table = ({ children, className }: TableProps) => {
return (
<Box overflow="auto" border borderColor="border-border-gray" rounded="sm">
<table className={`w-full border-collapse text-left ${className}`} {...(rest as any)}>
<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">
{children}
</table>
</Box>
</Surface>
);
}
};
interface TableHeaderProps extends BoxProps<'thead'> {
children: ReactNode;
}
export function TableHeader({ children, className = '', ...props }: TableHeaderProps) {
export const TableHeader = ({ children, className, textAlign, w }: { children: ReactNode, className?: string, textAlign?: 'left' | 'center' | 'right', w?: string }) => {
return (
<Box as="thead" className={`bg-graphite-black border-b border-border-gray ${className}`} {...props}>
{children}
</Box>
<thead className={`bg-[var(--ui-color-bg-base)] border-b border-[var(--ui-color-border-default)] ${className || ''}`}>
<tr>
{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;
interface TableBodyProps extends BoxProps<'tbody'> {
children: ReactNode;
}
export function TableBody({ children, className = '', ...props }: TableBodyProps) {
export const TableBody = ({ children }: { children: ReactNode }) => {
return (
<Box as="tbody" className={`divide-y divide-border-gray/50 ${className}`} {...props}>
<tbody className="divide-y divide-[var(--ui-color-border-muted)]">
{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 (
<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}
</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 (
<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}
</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 (
<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}
</Box>
</td>
);
}
};

View File

@@ -1,206 +1,121 @@
import React, { ReactNode, ElementType, ComponentPropsWithoutRef } from 'react';
import { Box, BoxProps } from './primitives/Box';
import React, { ReactNode, forwardRef, ElementType } from 'react';
import { Box, BoxProps, ResponsiveValue } from './primitives/Box';
type Spacing = 0 | 0.5 | 1 | 1.5 | 2 | 2.5 | 3 | 3.5 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 14 | 16 | 20 | 24 | 28 | 32 | 36 | 40 | 44 | 48 | 52 | 56 | 60 | 64 | 72 | 80 | 96;
export type TextSize = 'xs' | 'sm' | 'md' | 'lg' | 'xl' | '2xl' | '3xl' | '4xl' | 'base';
type TextSize = 'xs' | 'sm' | 'base' | 'lg' | 'xl' | '2xl' | '3xl' | '4xl';
interface ResponsiveTextSize {
base?: TextSize;
sm?: TextSize;
md?: TextSize;
lg?: TextSize;
xl?: TextSize;
'2xl'?: TextSize;
}
type TextAlign = 'left' | 'center' | 'right';
interface ResponsiveTextAlign {
base?: TextAlign;
sm?: TextAlign;
md?: TextAlign;
lg?: TextAlign;
xl?: TextAlign;
'2xl'?: TextAlign;
}
interface TextProps<T extends ElementType = 'span'> extends Omit<BoxProps<T>, 'children' | 'className' | 'size'> {
as?: T;
export interface TextProps extends BoxProps<any> {
children: ReactNode;
className?: string;
size?: TextSize | ResponsiveTextSize;
weight?: 'light' | 'normal' | 'medium' | 'semibold' | 'bold' | string;
color?: string;
font?: 'mono' | 'sans' | string;
align?: TextAlign | ResponsiveTextAlign;
truncate?: boolean;
uppercase?: boolean;
capitalize?: boolean;
letterSpacing?: 'tighter' | 'tight' | 'normal' | 'wide' | 'wider' | 'widest' | '0.05em' | string;
leading?: 'none' | 'tight' | 'snug' | 'normal' | 'relaxed' | 'loose';
fontSize?: string;
style?: React.CSSProperties;
block?: boolean;
variant?: 'high' | 'med' | 'low' | 'primary' | 'success' | 'warning' | 'critical' | 'inherit';
size?: TextSize | ResponsiveValue<TextSize>;
weight?: 'light' | 'normal' | 'medium' | 'semibold' | 'bold';
as?: ElementType;
align?: 'left' | 'center' | 'right';
italic?: boolean;
lineClamp?: number;
ml?: Spacing | ResponsiveSpacing;
mr?: Spacing | ResponsiveSpacing;
mt?: Spacing | ResponsiveSpacing;
mb?: Spacing | ResponsiveSpacing;
mono?: boolean;
block?: boolean;
uppercase?: boolean;
letterSpacing?: string;
leading?: 'none' | 'tight' | 'snug' | 'normal' | 'relaxed' | 'loose';
truncate?: boolean;
lineHeight?: string | number;
font?: 'sans' | 'mono';
hoverTextColor?: string;
}
interface ResponsiveSpacing {
base?: Spacing;
sm?: Spacing;
md?: Spacing;
lg?: Spacing;
xl?: Spacing;
'2xl'?: Spacing;
}
export function Text<T extends ElementType = 'span'>({
as,
export const Text = forwardRef<HTMLElement, TextProps>(({
children,
className = '',
size = 'base',
variant = 'med',
size = 'md',
weight = 'normal',
color = '',
font = 'sans',
as = 'p',
align = 'left',
truncate = false,
italic = false,
mono = false,
block = false,
uppercase = false,
capitalize = false,
letterSpacing,
leading,
fontSize,
style,
block = false,
italic = false,
lineClamp,
ml, mr, mt, mb,
truncate = false,
lineHeight,
font,
hoverTextColor,
...props
}: TextProps<T> & ComponentPropsWithoutRef<T>) {
const Tag = (as as ElementType) || 'span';
const sizeClasses: Record<string, string> = {
}, ref) => {
const variantClasses = {
high: 'text-[var(--ui-color-text-high)]',
med: 'text-[var(--ui-color-text-med)]',
low: 'text-[var(--ui-color-text-low)]',
primary: 'text-[var(--ui-color-intent-primary)]',
success: 'text-[var(--ui-color-intent-success)]',
warning: 'text-[var(--ui-color-intent-warning)]',
critical: 'text-[var(--ui-color-intent-critical)]',
inherit: 'text-inherit',
};
const sizeMap: Record<TextSize, string> = {
xs: 'text-xs',
sm: 'text-sm',
base: 'text-base',
md: 'text-base',
lg: 'text-lg',
xl: 'text-xl',
'2xl': 'text-2xl',
'3xl': 'text-3xl',
'4xl': 'text-4xl'
'4xl': 'text-4xl',
};
const getSizeClasses = (value: TextSize | ResponsiveTextSize | undefined) => {
if (value === undefined) return '';
if (typeof value === 'object') {
const classes = [];
if (value.base) classes.push(sizeClasses[value.base]);
if (value.sm) classes.push(`sm:${sizeClasses[value.sm]}`);
if (value.md) classes.push(`md:${sizeClasses[value.md]}`);
if (value.lg) classes.push(`lg:${sizeClasses[value.lg]}`);
if (value.xl) classes.push(`xl:${sizeClasses[value.xl]}`);
if (value['2xl']) classes.push(`2xl:${sizeClasses[value['2xl']]}`);
return classes.join(' ');
}
return sizeClasses[value];
const getResponsiveSizeClasses = (value: TextSize | ResponsiveValue<TextSize>) => {
if (typeof value === 'string') return sizeMap[value];
const classes = [];
if (value.base) classes.push(sizeMap[value.base]);
if (value.sm) classes.push(`sm:${sizeMap[value.sm]}`);
if (value.md) classes.push(`md:${sizeMap[value.md]}`);
if (value.lg) classes.push(`lg:${sizeMap[value.lg]}`);
if (value.xl) classes.push(`xl:${sizeMap[value.xl]}`);
return classes.join(' ');
};
const weightClasses: Record<string, string> = {
const weightClasses = {
light: 'font-light',
normal: 'font-normal',
medium: 'font-medium',
semibold: 'font-semibold',
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'
bold: 'font-bold',
};
const getAlignClasses = (value: TextAlign | ResponsiveTextAlign | undefined) => {
if (value === undefined) return '';
if (typeof value === 'object') {
const classes = [];
if (value.base) classes.push(alignClasses[value.base]);
if (value.sm) classes.push(`sm:${alignClasses[value.sm]}`);
if (value.md) classes.push(`md:${alignClasses[value.md]}`);
if (value.lg) classes.push(`lg:${alignClasses[value.lg]}`);
if (value.xl) classes.push(`xl:${alignClasses[value.xl]}`);
if (value['2xl']) classes.push(`2xl:${alignClasses[value['2xl']]}`);
return classes.join(' ');
}
return alignClasses[value];
};
const spacingMap: Record<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> = {
const leadingClasses = {
none: 'leading-none',
tight: 'leading-tight',
snug: 'leading-snug',
normal: 'leading-normal',
relaxed: 'leading-relaxed',
loose: 'leading-loose'
loose: 'leading-loose',
};
const getSpacingClass = (prefix: string, value: Spacing | ResponsiveSpacing | undefined) => {
if (value === undefined) return '';
if (typeof value === 'object') {
const classes = [];
if (value.base !== undefined) classes.push(`${prefix}-${spacingMap[value.base as number]}`);
if (value.sm !== undefined) classes.push(`sm:${prefix}-${spacingMap[value.sm as number]}`);
if (value.md !== undefined) classes.push(`md:${prefix}-${spacingMap[value.md as number]}`);
if (value.lg !== undefined) classes.push(`lg:${prefix}-${spacingMap[value.lg as number]}`);
if (value.xl !== undefined) classes.push(`xl:${prefix}-${spacingMap[value.xl as number]}`);
if (value['2xl'] !== undefined) classes.push(`2xl:${prefix}-${spacingMap[value['2xl'] as number]}`);
return classes.join(' ');
}
return `${prefix}-${spacingMap[value as number]}`;
};
const classes = [
block ? 'block' : 'inline',
getSizeClasses(size),
weightClasses[weight] || '',
fontClasses[font] || '',
getAlignClasses(align),
leading ? leadingClasses[leading] : '',
color,
truncate ? 'truncate' : '',
uppercase ? 'uppercase' : '',
capitalize ? 'capitalize' : '',
variantClasses[variant],
getResponsiveSizeClasses(size),
weightClasses[weight],
align === 'center' ? 'text-center' : (align === 'right' ? 'text-right' : 'text-left'),
italic ? 'italic' : '',
lineClamp ? `line-clamp-${lineClamp}` : '',
letterSpacing === '0.05em' ? 'tracking-wider' : letterSpacing ? `tracking-${letterSpacing}` : '',
getSpacingClass('ml', ml),
getSpacingClass('mr', mr),
getSpacingClass('mt', mt),
getSpacingClass('mb', mb),
className
].filter(Boolean).join(' ');
const combinedStyle = {
...(fontSize ? { fontSize } : {}),
...(weight && !weightClasses[weight] ? { fontWeight: weight } : {}),
...(font && !fontClasses[font] ? { fontFamily: font } : {}),
...style
(mono || font === 'mono') ? 'font-mono' : 'font-sans',
block ? 'block' : 'inline',
uppercase ? 'uppercase tracking-wider' : '',
leading ? leadingClasses[leading] : '',
truncate ? 'truncate' : '',
hoverTextColor ? `hover:text-${hoverTextColor}` : '',
].join(' ');
const style: React.CSSProperties = {
...(letterSpacing ? { letterSpacing } : {}),
...(lineHeight ? { lineHeight } : {}),
};
return <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 { Stack } from './primitives/Stack';
import { Text } from './Text';
interface TextAreaProps extends React.TextareaHTMLAttributes<HTMLTextAreaElement> {
export interface TextAreaProps extends TextareaHTMLAttributes<HTMLTextAreaElement> {
label?: string;
errorMessage?: string;
variant?: 'default' | 'error';
error?: string;
hint?: string;
fullWidth?: boolean;
}
export const TextArea = forwardRef<HTMLTextAreaElement, TextAreaProps>(
({ label, errorMessage, variant = 'default', fullWidth = true, className = '', ...props }, ref) => {
const isError = variant === 'error' || !!errorMessage;
return (
<Stack gap={1.5} fullWidth={fullWidth}>
{label && (
<Text as="label" size="xs" weight="bold" color="text-gray-500" uppercase letterSpacing="wider">
export const TextArea = forwardRef<HTMLTextAreaElement, TextAreaProps>(({
label,
error,
hint,
fullWidth = false,
...props
}, ref) => {
const baseClasses = 'bg-[var(--ui-color-bg-surface)] border border-[var(--ui-color-border-default)] text-[var(--ui-color-text-high)] placeholder-[var(--ui-color-text-low)] focus:outline-none focus:border-[var(--ui-color-intent-primary)] transition-colors p-3 text-sm min-h-[100px]';
const errorClasses = error ? 'border-[var(--ui-color-intent-critical)]' : '';
const widthClasses = fullWidth ? 'w-full' : '';
const classes = [
baseClasses,
errorClasses,
widthClasses,
].filter(Boolean).join(' ');
return (
<Box width={fullWidth ? '100%' : undefined}>
{label && (
<Box marginBottom={1.5}>
<Text as="label" size="xs" weight="bold" variant="low">
{label}
</Text>
)}
<Box position="relative" fullWidth={fullWidth}>
<Box
as="textarea"
ref={ref}
fullWidth={fullWidth}
p={3}
bg="bg-deep-graphite"
rounded="lg"
color="text-white"
border
borderColor={isError ? 'var(--warning-amber)' : 'rgba(38, 38, 38, 0.8)'}
className={`placeholder:text-gray-500 focus:ring-2 focus:ring-primary-blue transition-all duration-150 sm:text-sm ${className}`}
{...props}
/>
{errorMessage && (
<Text size="xs" color="text-warning-amber" mt={1}>
{errorMessage}
</Text>
)}
</Box>
</Stack>
);
}
);
)}
<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';

View File

@@ -1,9 +1,8 @@
import React from 'react';
import { Box } from './primitives/Box';
import { Text } from './Text';
import { motion } from 'framer-motion';
interface ToggleProps {
export interface ToggleProps {
label: string;
description?: string;
checked: boolean;
@@ -11,64 +10,49 @@ interface ToggleProps {
disabled?: boolean;
}
export function Toggle({ label, description, checked, onChange, disabled }: ToggleProps) {
export const Toggle = ({
label,
description,
checked,
onChange,
disabled = false
}: ToggleProps) => {
return (
<Box
as="label"
display="flex"
alignItems="start"
alignItems="center"
justifyContent="between"
cursor={disabled ? 'not-allowed' : 'pointer'}
py={3}
paddingY={3}
borderBottom
borderColor="border-charcoal-outline/50"
className="last:border-b-0"
opacity={disabled ? 0.5 : 1}
style={{ cursor: disabled ? 'not-allowed' : 'pointer', opacity: disabled ? 0.5 : 1 }}
>
<Box flex={1} pr={4}>
<Text weight="medium" color="text-gray-200" block>{label}</Text>
<Box flex={1} paddingRight={4}>
<Text weight="medium" variant="high" block>{label}</Text>
{description && (
<Text size="xs" color="text-gray-500" block mt={1}>
<Text size="xs" variant="low" block marginTop={1}>
{description}
</Text>
)}
</Box>
<Box position="relative">
<Box
as="button"
type="button"
role="switch"
aria-checked={checked}
onClick={() => !disabled && onChange(!checked)}
disabled={disabled}
w="12"
h="6"
rounded="full"
transition="all 0.2s"
flexShrink={0}
ring="primary-blue/50"
bg={checked ? 'bg-primary-blue/20' : 'bg-charcoal-outline'}
className="focus:outline-none focus:ring-2"
>
<motion.div
className="absolute inset-0 rounded-full bg-primary-blue"
initial={{ boxShadow: '0 0 0px rgba(25, 140, 255, 0)' }}
animate={{
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 }}
<button
type="button"
role="switch"
aria-checked={checked}
onClick={() => !disabled && onChange(!checked)}
disabled={disabled}
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 ${
checked ? 'bg-[var(--ui-color-intent-primary)]' : 'bg-[var(--ui-color-bg-surface-muted)]'
}`}
>
<span
aria-hidden="true"
className={`pointer-events-none inline-block h-5 w-5 transform rounded-full bg-white shadow ring-0 transition duration-200 ease-in-out ${
checked ? 'translate-x-5' : 'translate-x-0'
}`}
/>
</Box>
</button>
</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.
*
* 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 animation props - create a specific component.
*
@@ -34,8 +34,23 @@ export type ResponsiveValue<T> = {
export interface BoxProps<T extends ElementType> {
as?: T;
children?: React.ReactNode;
className?: string;
// 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;
mt?: Spacing | ResponsiveSpacing;
mb?: Spacing | ResponsiveSpacing;
@@ -50,11 +65,10 @@ export interface BoxProps<T extends ElementType> {
pr?: Spacing | ResponsiveSpacing;
px?: Spacing | ResponsiveSpacing;
py?: Spacing | ResponsiveSpacing;
// Sizing
w?: string | number | ResponsiveValue<string | number>;
h?: string | number | ResponsiveValue<string | number>;
width?: string | number;
height?: string | number;
width?: string | number | ResponsiveValue<string | number>;
height?: string | number | ResponsiveValue<string | number>;
maxWidth?: string | ResponsiveValue<string>;
minWidth?: string | ResponsiveValue<string>;
maxHeight?: string | ResponsiveValue<string>;
@@ -62,6 +76,11 @@ export interface BoxProps<T extends ElementType> {
fullWidth?: boolean;
fullHeight?: boolean;
aspectRatio?: string;
// Aliases
w?: string | number | ResponsiveValue<string | number>;
h?: string | number | ResponsiveValue<string | number>;
// Display
display?: 'block' | 'inline-block' | 'flex' | 'inline-flex' | 'grid' | 'none' | string | ResponsiveValue<'block' | 'inline-block' | 'flex' | 'inline-flex' | 'grid' | 'none' | string>;
center?: boolean;
@@ -80,28 +99,6 @@ export interface BoxProps<T extends ElementType> {
insetY?: string | number;
insetX?: string | 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?: number | string;
flexShrink?: number;
@@ -113,20 +110,41 @@ export interface BoxProps<T extends ElementType> {
alignSelf?: 'auto' | 'start' | 'end' | 'center' | 'stretch' | 'baseline';
gap?: number | string | ResponsiveValue<number | string>;
gridCols?: number | ResponsiveValue<number>;
responsiveGridCols?: number | ResponsiveValue<number>;
colSpan?: number | ResponsiveValue<number>;
responsiveColSpan?: number | ResponsiveValue<number>;
order?: number | string | ResponsiveValue<number | string>;
// Transform
transform?: string | boolean;
translate?: string;
translateX?: string;
translateY?: string;
// Animation (Framer Motion support)
initial?: any;
animate?: any;
exit?: any;
// Interaction
onClick?: React.MouseEventHandler<any>;
onMouseEnter?: React.MouseEventHandler<any>;
onMouseLeave?: React.MouseEventHandler<any>;
id?: string;
role?: React.AriaRole;
tabIndex?: number;
// Internal use only
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;
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;
whileHover?: any;
whileTap?: any;
@@ -135,39 +153,14 @@ export interface BoxProps<T extends ElementType> {
whileInView?: any;
viewport?: any;
custom?: any;
// Interaction
group?: boolean;
groupHoverTextColor?: string;
groupHoverScale?: boolean;
groupHoverOpacity?: number;
groupHoverBorderColor?: string;
hoverBorderColor?: string;
hoverBg?: string;
hoverTextColor?: string;
hoverScale?: boolean | number;
clickable?: boolean;
// Events
onMouseEnter?: React.MouseEventHandler<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;
exit?: any;
translateX?: string;
translateY?: string;
translate?: string;
cursor?: string;
fontSize?: string | ResponsiveValue<string>;
weight?: string;
fontWeight?: string | number;
weight?: string | number;
letterSpacing?: string;
lineHeight?: string | number;
font?: string;
@@ -176,19 +169,15 @@ export interface BoxProps<T extends ElementType> {
truncate?: boolean;
src?: string;
alt?: string;
draggable?: boolean;
draggable?: boolean | string;
min?: string | number;
max?: string | number;
step?: string | number;
value?: string | number;
onChange?: React.ChangeEventHandler<any>;
onError?: React.ReactEventHandler<any>;
placeholder?: string;
title?: string;
padding?: Spacing | ResponsiveSpacing;
paddingLeft?: Spacing | ResponsiveSpacing;
paddingRight?: Spacing | ResponsiveSpacing;
paddingTop?: Spacing | ResponsiveSpacing;
paddingBottom?: Spacing | ResponsiveSpacing;
size?: string | number | ResponsiveValue<string | number>;
accept?: string;
autoPlay?: boolean;
@@ -196,16 +185,46 @@ export interface BoxProps<T extends ElementType> {
muted?: boolean;
playsInline?: boolean;
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'>(
{
as,
children,
className = '',
margin, marginTop, marginBottom, marginLeft, marginRight, marginX, marginY,
padding, paddingTop, paddingBottom, paddingLeft, paddingRight, paddingX, paddingY,
m, mt, mb, ml, mr, mx, my,
p, pt, pb, pl, pr, px, py,
w, h, width, height,
width, height,
w, h,
maxWidth, minWidth, maxHeight, minHeight,
fullWidth, fullHeight,
aspectRatio,
@@ -218,27 +237,6 @@ export const Box = forwardRef(<T extends ElementType = 'div'>(
top, right, bottom, left,
inset, insetY, insetX,
zIndex,
rounded,
border,
borderTop,
borderBottom,
borderLeft,
borderRight,
borderWidth,
borderStyle,
borderColor,
borderOpacity,
bg,
backgroundColor,
backgroundImage,
backgroundSize,
backgroundPosition,
bgOpacity,
color,
shadow,
opacity,
blur,
pointerEvents,
flex,
flexShrink,
flexGrow,
@@ -249,18 +247,39 @@ export const Box = forwardRef(<T extends ElementType = 'div'>(
alignSelf,
gap,
gridCols,
responsiveGridCols,
colSpan,
responsiveColSpan,
order,
transform,
translate,
translateX,
translateY,
initial,
animate,
exit,
onClick,
onMouseEnter,
onMouseLeave,
id,
role,
tabIndex,
style: styleProp,
className,
borderTop,
borderBottom,
borderLeft,
borderRight,
bg,
rounded,
borderColor,
border,
color,
opacity,
transition,
hoverBg,
group,
groupHoverOpacity,
groupHoverBorderColor,
groupHoverWidth,
animate,
blur,
pointerEvents,
bgOpacity,
borderWidth,
borderStyle,
initial,
variants,
whileHover,
whileTap,
@@ -269,36 +288,14 @@ export const Box = forwardRef(<T extends ElementType = 'div'>(
whileInView,
viewport,
custom,
group,
groupHoverTextColor,
groupHoverScale,
groupHoverOpacity,
groupHoverBorderColor,
hoverBorderColor,
hoverBg,
hoverTextColor,
hoverScale,
clickable,
onMouseEnter,
onMouseLeave,
onClick,
onMouseDown,
onMouseUp,
onMouseMove,
onKeyDown,
onBlur,
onSubmit,
onScroll,
style: styleProp,
id,
role,
tabIndex,
type,
disabled,
exit,
translateX,
translateY,
translate,
cursor,
fontSize,
weight,
fontWeight,
weight,
letterSpacing,
lineHeight,
font,
@@ -313,13 +310,9 @@ export const Box = forwardRef(<T extends ElementType = 'div'>(
step,
value,
onChange,
onError,
placeholder,
title,
padding,
paddingLeft,
paddingRight,
paddingTop,
paddingBottom,
size,
accept,
autoPlay,
@@ -327,8 +320,36 @@ export const Box = forwardRef(<T extends ElementType = 'div'>(
muted,
playsInline,
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
}: BoxProps<T> & ComponentPropsWithoutRef<T>,
}: BoxProps<T>,
ref: ForwardedRef<HTMLElement>
) => {
const Tag = (as as ElementType) || 'div';
@@ -371,22 +392,22 @@ export const Box = forwardRef(<T extends ElementType = 'div'>(
};
const classes = [
getSpacingClass('m', m),
getSpacingClass('mt', mt),
getSpacingClass('mb', mb),
getSpacingClass('ml', ml),
getSpacingClass('mr', mr),
getSpacingClass('mx', mx),
getSpacingClass('my', my),
getSpacingClass('p', p || padding),
getSpacingClass('pt', pt || paddingTop),
getSpacingClass('pb', pb || paddingBottom),
getSpacingClass('pl', pl || paddingLeft),
getSpacingClass('pr', pr || paddingRight),
getSpacingClass('px', px),
getSpacingClass('py', py),
fullWidth ? 'w-full' : getResponsiveClasses('w', w),
fullHeight ? 'h-full' : getResponsiveClasses('h', h),
getSpacingClass('m', margin || m),
getSpacingClass('mt', marginTop || mt),
getSpacingClass('mb', marginBottom || mb),
getSpacingClass('ml', marginLeft || ml),
getSpacingClass('mr', marginRight || mr),
getSpacingClass('mx', marginX || mx),
getSpacingClass('my', marginY || my),
getSpacingClass('p', padding || p),
getSpacingClass('pt', paddingTop || pt),
getSpacingClass('pb', paddingBottom || pb),
getSpacingClass('pl', paddingLeft || pl),
getSpacingClass('pr', paddingRight || pr),
getSpacingClass('px', paddingX || px),
getSpacingClass('py', paddingY || py),
fullWidth ? 'w-full' : getResponsiveClasses('w', width || w),
fullHeight ? 'h-full' : getResponsiveClasses('h', height || h),
getResponsiveClasses('max-w', maxWidth),
getResponsiveClasses('min-w', minWidth),
getResponsiveClasses('max-h', maxHeight),
@@ -407,23 +428,6 @@ export const Box = forwardRef(<T extends ElementType = 'div'>(
insetY !== undefined ? `inset-y-${insetY}` : '',
insetX !== undefined ? `inset-x-${insetX}` : '',
zIndex !== undefined ? `z-${zIndex}` : '',
rounded === true ? 'rounded' : (rounded === false ? 'rounded-none' : (typeof rounded === 'string' ? (rounded.includes('-') ? rounded : `rounded-${rounded}`) : '')),
border === true ? 'border' : (typeof border === 'string' ? (border === 'none' ? 'border-none' : border) : ''),
borderTop === true ? 'border-t' : (typeof borderTop === 'string' ? borderTop : ''),
borderBottom === true ? 'border-b' : (typeof borderBottom === 'string' ? borderBottom : ''),
borderLeft === true ? 'border-l' : (typeof borderLeft === 'string' ? borderLeft : ''),
borderRight === true ? 'border-r' : (typeof borderRight === 'string' ? borderRight : ''),
borderStyle ? `border-${borderStyle}` : '',
borderColor ? borderColor : '',
borderOpacity !== undefined ? `border-opacity-${borderOpacity * 100}` : '',
bg ? bg : '',
backgroundColor ? backgroundColor : '',
bgOpacity !== undefined ? `bg-opacity-${bgOpacity * 100}` : '',
color ? color : '',
shadow ? shadow : '',
opacity !== undefined ? `opacity-${opacity * 100}` : '',
blur ? (blur === 'none' ? 'blur-none' : `blur-${blur}`) : '',
pointerEvents ? `pointer-events-${pointerEvents}` : '',
flex !== undefined ? `flex-${flex}` : '',
flexShrink !== undefined ? `flex-shrink-${flexShrink}` : '',
flexGrow !== undefined ? `flex-grow-${flexGrow}` : '',
@@ -436,21 +440,14 @@ export const Box = forwardRef(<T extends ElementType = 'div'>(
getResponsiveClasses('grid-cols', gridCols || responsiveGridCols),
getResponsiveClasses('col-span', colSpan || responsiveColSpan),
getResponsiveClasses('order', order),
getResponsiveClasses('text', fontSize),
group ? 'group' : '',
groupHoverTextColor ? `group-hover:text-${groupHoverTextColor}` : '',
groupHoverScale ? 'group-hover:scale-105 transition-transform' : '',
groupHoverOpacity !== undefined ? `group-hover:opacity-${groupHoverOpacity * 100}` : '',
groupHoverBorderColor ? `group-hover:border-${groupHoverBorderColor}` : '',
hoverBorderColor ? `hover:border-${hoverBorderColor}` : '',
hoverBg ? `hover:bg-${hoverBg}` : '',
hoverTextColor ? `hover:text-${hoverTextColor}` : '',
hoverScale === true ? 'hover:scale-105 transition-transform' : (typeof hoverScale === 'number' ? `hover:scale-${hoverScale} transition-transform` : ''),
clickable ? 'cursor-pointer active:opacity-80 transition-all' : '',
ring ? `ring-${ring}` : '',
animate === 'spin' ? 'animate-spin' : (animate === 'pulse' ? 'animate-pulse' : ''),
blur ? `blur-${blur}` : '',
pointerEvents ? `pointer-events-${pointerEvents}` : '',
hideScrollbar ? 'scrollbar-hide' : '',
truncate ? 'truncate' : '',
transform === true ? 'transform' : (transform === false ? 'transform-none' : ''),
clickable ? 'cursor-pointer' : '',
lineClamp ? `line-clamp-${lineClamp}` : '',
className
].filter(Boolean).join(' ');
@@ -466,23 +463,27 @@ export const Box = forwardRef(<T extends ElementType = 'div'>(
...(typeof right === 'string' || typeof right === 'number' ? { right } : {}),
...(typeof bottom === 'string' || typeof bottom === 'number' ? { bottom } : {}),
...(typeof left === 'string' || typeof left === 'number' ? { left } : {}),
...(borderWidth !== undefined ? { borderWidth } : {}),
...(typeof transform === 'string' ? { transform } : {}),
...(translate ? { translate } : {}),
...(translateX ? { transform: `translateX(${translateX})` } : {}),
...(translateY ? { transform: `translateY(${translateY})` } : {}),
...(cursor ? { cursor } : {}),
...(fontSize && typeof fontSize === 'string' && !fontSize.includes(':') ? { fontSize } : {}),
...(weight ? { fontWeight: weight } : {}),
...(typeof borderTop === 'string' ? { borderTop } : (borderTop === true ? { borderTop: '1px solid var(--ui-color-border-default)' } : {})),
...(typeof borderBottom === 'string' ? { borderBottom } : (borderBottom === true ? { borderBottom: '1px solid var(--ui-color-border-default)' } : {})),
...(typeof borderLeft === 'string' ? { borderLeft } : (borderLeft === true ? { borderLeft: '1px solid var(--ui-color-border-default)' } : {})),
...(typeof borderRight === 'string' ? { borderRight } : (borderRight === true ? { borderRight: '1px solid var(--ui-color-border-default)' } : {})),
...(bg ? { background: bg.startsWith('bg-') ? undefined : bg } : {}),
...(rounded === true ? { borderRadius: 'var(--ui-radius-md)' } : (typeof rounded === 'string' ? { borderRadius: rounded.includes('rem') || rounded.includes('px') ? rounded : `var(--ui-radius-${rounded})` } : {})),
...(borderColor ? { borderColor: borderColor.startsWith('border-') ? undefined : borderColor } : {}),
...(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 } : {}),
...(letterSpacing ? { letterSpacing } : {}),
...(lineHeight ? { lineHeight } : {}),
...(font ? { fontFamily: font } : {}),
...(typeof size === 'string' || typeof size === 'number' ? { width: size, height: size } : {}),
...(backgroundImage ? { backgroundImage } : {}),
...(weight ? { fontWeight: weight } : {}),
...(shadow ? { boxShadow: shadow.startsWith('shadow-') ? undefined : shadow } : {}),
...(transform === true ? { transform: 'auto' } : (typeof transform === 'string' ? { transform } : {})),
...(typeof fill === 'string' ? { fill } : (fill === true ? { fill: 'currentColor' } : {})),
...(stroke ? { stroke } : {}),
...(strokeWidth ? { strokeWidth } : {}),
...(backgroundSize ? { backgroundSize } : {}),
...(backgroundPosition ? { backgroundPosition } : {}),
...(objectFit ? { objectFit } : {}),
...(backgroundImage ? { backgroundImage } : {}),
...(styleProp || {})
};
@@ -491,24 +492,24 @@ export const Box = forwardRef(<T extends ElementType = 'div'>(
ref={ref as React.ForwardedRef<HTMLElement>}
className={classes}
onClick={onClick}
onMouseEnter={onMouseEnter}
onMouseLeave={onMouseLeave}
onMouseDown={onMouseDown}
onMouseUp={onMouseUp}
onMouseMove={onMouseMove}
onKeyDown={onKeyDown}
onBlur={onBlur}
onSubmit={onSubmit}
onMouseEnter={onMouseEnter}
onMouseLeave={onMouseLeave}
onScroll={onScroll}
style={style}
onError={onError}
style={Object.keys(style).length > 0 ? style : undefined}
id={id}
role={role}
tabIndex={tabIndex}
type={type}
disabled={disabled}
src={src}
alt={alt}
draggable={draggable}
draggable={draggable as any}
min={min}
max={max}
step={step}
@@ -520,6 +521,7 @@ export const Box = forwardRef(<T extends ElementType = 'div'>(
loop={loop}
muted={muted}
playsInline={playsInline}
viewBox={viewBox}
{...props}
>
{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.
*/
export interface GridProps<T extends ElementType = 'div'> extends Omit<BoxProps<T>, 'children'> {
export interface GridProps<T extends ElementType = 'div'> extends BoxProps<T> {
children?: ReactNode;
cols?: 1 | 2 | 3 | 4 | 5 | 6 | 12;
mdCols?: 1 | 2 | 3 | 4 | 5 | 6 | 12;
lgCols?: 1 | 2 | 3 | 4 | 5 | 6 | 12;
gap?: 0 | 1 | 2 | 3 | 4 | 6 | 8 | 12 | 16;
className?: string;
columns?: number | ResponsiveValue<number>;
gap?: number | string | ResponsiveValue<number | string>;
}
export function Grid<T extends ElementType = 'div'>({
children,
cols = 1,
mdCols,
lgCols,
columns = 1,
gap = 4,
className = '',
...props
}: 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 (
<Box className={classes} {...props}>
<Box
display="grid"
gridCols={columns}
gap={gap}
{...props}
>
{children}
</Box>
);

View File

@@ -1,35 +1,27 @@
import React, { ElementType } from 'react';
import { Box, BoxProps } from './Box';
import React, { ReactNode, ElementType } from 'react';
import { Box, BoxProps, ResponsiveValue } from './Box';
/**
* WARNING: DO NOT VIOLATE THE PURPOSE OF THIS PRIMITIVE.
*
* GridItem is for items inside a Grid container.
*
* - 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 interface GridItemProps<T extends ElementType = 'div'> extends BoxProps<T> {
children?: ReactNode;
colSpan?: number | ResponsiveValue<number>;
rowSpan?: number | ResponsiveValue<number>;
lgSpan?: number; // Alias for colSpan.lg
}
export function GridItem<T extends ElementType = 'div'>({ children, colSpan, mdSpan, lgSpan, className = '', ...props }: GridItemProps<T>) {
const spanClasses = [
colSpan ? `col-span-${colSpan}` : '',
mdSpan ? `md:col-span-${mdSpan}` : '',
lgSpan ? `lg:col-span-${lgSpan}` : '',
className
].filter(Boolean).join(' ');
export function GridItem<T extends ElementType = 'div'>({
children,
colSpan,
rowSpan,
lgSpan,
...props
}: GridItemProps<T>) {
const actualColSpan = lgSpan ? { base: colSpan as any, lg: lgSpan } : colSpan;
return (
<Box className={spanClasses} {...props}>
<Box
colSpan={actualColSpan as any}
{...props}
>
{children}
</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.
*/
interface ResponsiveGap {
base?: number;
sm?: number;
md?: number;
lg?: number;
xl?: number;
}
export interface StackProps<T extends ElementType> extends Omit<BoxProps<T>, 'children'> {
export interface StackProps<T extends ElementType> extends BoxProps<T> {
as?: T;
children?: ReactNode;
className?: string;
direction?: 'row' | 'col' | { base?: 'row' | 'col'; md?: 'row' | 'col'; lg?: 'row' | 'col' };
gap?: number | string | ResponsiveGap;
direction?: 'row' | 'col' | ResponsiveValue<'row' | 'col'>;
align?: 'start' | 'center' | 'end' | 'stretch' | 'baseline' | ResponsiveValue<'start' | 'center' | 'end' | 'stretch' | 'baseline'>;
justify?: 'start' | 'center' | 'end' | 'between' | 'around' | ResponsiveValue<'start' | 'center' | 'end' | 'between' | 'around'>;
wrap?: boolean;
@@ -35,7 +25,6 @@ export interface StackProps<T extends ElementType> extends Omit<BoxProps<T>, 'ch
export const Stack = forwardRef(<T extends ElementType = 'div'>(
{
children,
className = '',
direction = 'col',
gap = 4,
align,
@@ -46,91 +35,16 @@ export const Stack = forwardRef(<T extends ElementType = 'div'>(
}: StackProps<T>,
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 (
<Box
as={as}
ref={ref}
className={`${classes} ${layoutClasses}`}
display="flex"
flexDirection={direction}
gap={gap}
alignItems={align}
justifyContent={justify}
flexWrap={wrap ? 'wrap' : 'nowrap'}
{...props}
>
{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 { ThemeRadii, ThemeShadows } from '../theme/Theme';
/**
* 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.
*/
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;
children?: ReactNode;
variant?: 'default' | 'muted' | 'dark' | 'glass' | 'gradient-blue' | 'gradient-gold' | 'gradient-purple' | 'gradient-green' | 'discord' | 'discord-inner';
rounded?: 'none' | 'sm' | 'md' | 'lg' | 'xl' | '2xl' | 'full' | string | boolean;
border?: boolean | string;
padding?: number;
className?: string;
shadow?: 'none' | 'sm' | 'md' | 'lg' | 'xl' | 'discord' | string;
variant?: 'default' | 'dark' | 'muted' | 'glass' | 'discord' | 'gradient-blue' | 'gradient-gold' | 'gradient-purple' | 'gradient-green' | 'discord-inner' | 'outline';
rounded?: keyof ThemeRadii | 'none';
shadow?: keyof ThemeShadows | 'none';
}
export const Surface = forwardRef(<T extends ElementType = 'div'>(
@@ -29,69 +27,52 @@ export const Surface = forwardRef(<T extends ElementType = 'div'>(
children,
variant = 'default',
rounded = 'none',
border = false,
padding = 0,
className = '',
shadow = 'none',
...props
}: SurfaceProps<T> & ComponentPropsWithoutRef<T>,
}: SurfaceProps<T>,
ref: ForwardedRef<HTMLElement>
) => {
const variantClasses: Record<string, string> = {
default: 'bg-panel-gray',
muted: 'bg-panel-gray/40',
dark: 'bg-graphite-black',
glass: 'bg-graphite-black/60 backdrop-blur-md',
'gradient-blue': 'bg-gradient-to-br from-primary-accent/10 via-panel-gray/80 to-graphite-black',
'gradient-gold': 'bg-gradient-to-br from-warning-amber/10 via-panel-gray/80 to-graphite-black',
'gradient-purple': 'bg-gradient-to-br from-purple-600/10 via-panel-gray/80 to-graphite-black',
'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-inner': 'bg-gradient-to-br from-panel-gray via-graphite-black to-panel-gray'
const variantStyles: Record<string, React.CSSProperties> = {
default: { backgroundColor: 'var(--ui-color-bg-surface)' },
dark: { backgroundColor: 'var(--ui-color-bg-base)' },
muted: { backgroundColor: 'var(--ui-color-bg-surface-muted)' },
glass: {
backgroundColor: 'rgba(20, 22, 25, 0.6)',
backdropFilter: 'blur(12px)',
WebkitBackdropFilter: 'blur(12px)'
},
discord: {
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> = {
none: '',
sm: 'shadow-sm',
md: 'shadow-md',
lg: 'shadow-lg',
xl: 'shadow-xl',
discord: 'shadow-[0_0_80px_rgba(88,101,242,0.15)]'
const style: React.CSSProperties = {
...variantStyles[variant],
borderRadius: rounded !== 'none' ? `var(--ui-radius-${String(rounded)})` : undefined,
boxShadow: shadow !== 'none' ? `var(--ui-shadow-${String(shadow)})` : undefined,
};
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 (
<Box as={as} ref={ref} className={classes} {...props}>
<Box as={as} ref={ref} {...(props as any)} style={style}>
{children}
</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 {
id: string;
name: string;
@@ -54,4 +91,5 @@ export interface Theme {
radii: ThemeRadii;
shadows: ThemeShadows;
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",
},
},
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',
},
};