website refactor
This commit is contained in:
@@ -1,49 +1,40 @@
|
|||||||
|
import React, { ReactNode, useState } from 'react';
|
||||||
|
|
||||||
import { ChevronDown, ChevronUp } from 'lucide-react';
|
|
||||||
import { ReactNode } from 'react';
|
|
||||||
import { Icon } from './Icon';
|
|
||||||
import { Box } from './primitives/Box';
|
import { Box } from './primitives/Box';
|
||||||
import { Stack } from './primitives/Stack';
|
|
||||||
import { Text } from './Text';
|
import { Text } from './Text';
|
||||||
|
import { ChevronDown, ChevronUp } from 'lucide-react';
|
||||||
|
import { Icon } from './Icon';
|
||||||
|
import { Surface } from './primitives/Surface';
|
||||||
|
|
||||||
interface AccordionProps {
|
export interface AccordionProps {
|
||||||
title: string;
|
title: string;
|
||||||
icon: ReactNode;
|
|
||||||
children: ReactNode;
|
children: ReactNode;
|
||||||
isOpen: boolean;
|
defaultOpen?: boolean;
|
||||||
onToggle: () => void;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function Accordion({ title, icon, children, isOpen, onToggle }: AccordionProps) {
|
export const Accordion = ({
|
||||||
|
title,
|
||||||
|
children,
|
||||||
|
defaultOpen = false
|
||||||
|
}: AccordionProps) => {
|
||||||
|
const [isOpen, setIsOpen] = useState(defaultOpen);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box border borderColor="border-charcoal-outline" rounded="lg" overflow="hidden" bg="bg-iron-gray/30">
|
<Surface variant="muted" rounded="lg" style={{ border: '1px solid var(--ui-color-border-default)', overflow: 'hidden' }}>
|
||||||
<Box
|
<button
|
||||||
as="button"
|
onClick={() => setIsOpen(!isOpen)}
|
||||||
onClick={onToggle}
|
className="w-full flex items-center justify-between px-4 py-3 hover:bg-white/5 transition-colors"
|
||||||
display="flex"
|
|
||||||
alignItems="center"
|
|
||||||
justifyContent="between"
|
|
||||||
px={3}
|
|
||||||
py={2}
|
|
||||||
fullWidth
|
|
||||||
hoverBg="iron-gray/50"
|
|
||||||
clickable
|
|
||||||
>
|
>
|
||||||
<Stack direction="row" align="center" gap={2}>
|
<Text weight="bold" size="sm" variant="high">
|
||||||
{icon}
|
{title}
|
||||||
<Text size="xs" weight="semibold" color="text-gray-400" uppercase letterSpacing="wide">
|
</Text>
|
||||||
{title}
|
<Icon icon={isOpen ? ChevronUp : ChevronDown} size={4} intent="low" />
|
||||||
</Text>
|
</button>
|
||||||
</Stack>
|
|
||||||
<Icon icon={isOpen ? ChevronDown : ChevronUp} size={4} color="text-gray-400" />
|
|
||||||
</Box>
|
|
||||||
|
|
||||||
{isOpen && (
|
{isOpen && (
|
||||||
<Box p={3} borderTop borderColor="border-charcoal-outline">
|
<Box padding={4} borderTop>
|
||||||
{children}
|
{children}
|
||||||
</Box>
|
</Box>
|
||||||
)}
|
)}
|
||||||
</Box>
|
</Surface>
|
||||||
);
|
);
|
||||||
}
|
};
|
||||||
|
|||||||
@@ -1,67 +1,44 @@
|
|||||||
import React from 'react';
|
import React, { ReactNode } from 'react';
|
||||||
import { Box } from './primitives/Box';
|
import { Box } from './primitives/Box';
|
||||||
import { Text } from './Text';
|
import { Text } from './Text';
|
||||||
import { Surface } from './primitives/Surface';
|
import { Surface } from './primitives/Surface';
|
||||||
import { Link } from './Link';
|
|
||||||
|
|
||||||
interface ActivityItemProps {
|
export interface ActivityItemProps {
|
||||||
title?: string;
|
title: string;
|
||||||
description?: string;
|
description?: string;
|
||||||
timeAgo?: string;
|
timestamp: string;
|
||||||
color?: string;
|
icon?: ReactNode;
|
||||||
headline?: string;
|
children?: ReactNode;
|
||||||
body?: string;
|
|
||||||
formattedTime?: string;
|
|
||||||
ctaHref?: string;
|
|
||||||
ctaLabel?: string;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function ActivityItem({
|
export const ActivityItem = ({
|
||||||
title,
|
title,
|
||||||
description,
|
description,
|
||||||
timeAgo,
|
timestamp,
|
||||||
color = 'bg-primary-blue',
|
icon,
|
||||||
headline,
|
children
|
||||||
body,
|
}: ActivityItemProps) => {
|
||||||
formattedTime,
|
|
||||||
ctaHref,
|
|
||||||
ctaLabel
|
|
||||||
}: ActivityItemProps) {
|
|
||||||
return (
|
return (
|
||||||
<Surface
|
<Surface variant="muted" rounded="lg" padding={4}>
|
||||||
variant="muted"
|
<Box display="flex" alignItems="start" gap={4}>
|
||||||
rounded="lg"
|
{icon && (
|
||||||
display="flex"
|
<Box padding={2} rounded="lg" bg="var(--ui-color-bg-surface-muted)">
|
||||||
alignItems="start"
|
{icon}
|
||||||
gap={3}
|
|
||||||
p={4}
|
|
||||||
>
|
|
||||||
<Box
|
|
||||||
w="2"
|
|
||||||
h="2"
|
|
||||||
mt={1.5}
|
|
||||||
rounded="full"
|
|
||||||
bg={color}
|
|
||||||
flexShrink={0}
|
|
||||||
/>
|
|
||||||
<Box flex={1} minWidth={0}>
|
|
||||||
<Text color="text-white" weight="medium" block>
|
|
||||||
{title || headline}
|
|
||||||
</Text>
|
|
||||||
<Text size="sm" color="text-gray-400" block mt={0.5}>
|
|
||||||
{description || body}
|
|
||||||
</Text>
|
|
||||||
<Text size="xs" color="text-gray-500" block mt={2}>
|
|
||||||
{timeAgo || formattedTime}
|
|
||||||
</Text>
|
|
||||||
{ctaHref && ctaLabel && (
|
|
||||||
<Box mt={3}>
|
|
||||||
<Link href={ctaHref} size="xs" variant="primary">
|
|
||||||
{ctaLabel}
|
|
||||||
</Link>
|
|
||||||
</Box>
|
</Box>
|
||||||
)}
|
)}
|
||||||
|
<Box flex={1}>
|
||||||
|
<Box display="flex" alignItems="center" justifyContent="between" marginBottom={1}>
|
||||||
|
<Text weight="bold" variant="high">{title}</Text>
|
||||||
|
<Text size="xs" variant="low">{timestamp}</Text>
|
||||||
|
</Box>
|
||||||
|
{description && (
|
||||||
|
<Text size="sm" variant="low" marginBottom={children ? 4 : 0}>
|
||||||
|
{description}
|
||||||
|
</Text>
|
||||||
|
)}
|
||||||
|
{children}
|
||||||
|
</Box>
|
||||||
</Box>
|
</Box>
|
||||||
</Surface>
|
</Surface>
|
||||||
);
|
);
|
||||||
}
|
};
|
||||||
|
|||||||
@@ -1,44 +1,62 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { Box } from './primitives/Box';
|
|
||||||
import { Image } from './Image';
|
|
||||||
import { Surface } from './primitives/Surface';
|
import { Surface } from './primitives/Surface';
|
||||||
|
import { Box } from './primitives/Box';
|
||||||
|
import { User } from 'lucide-react';
|
||||||
|
import { Icon } from './Icon';
|
||||||
|
|
||||||
interface AvatarProps {
|
export interface AvatarProps {
|
||||||
src?: string | null;
|
src?: string;
|
||||||
alt: string;
|
alt?: string;
|
||||||
size?: number;
|
size?: 'sm' | 'md' | 'lg' | 'xl';
|
||||||
className?: string;
|
fallback?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function Avatar({ src, alt, size = 40, className = '' }: AvatarProps) {
|
export const Avatar = ({
|
||||||
|
src,
|
||||||
|
alt,
|
||||||
|
size = 'md',
|
||||||
|
fallback
|
||||||
|
}: AvatarProps) => {
|
||||||
|
const sizeMap = {
|
||||||
|
sm: '2rem',
|
||||||
|
md: '3rem',
|
||||||
|
lg: '4rem',
|
||||||
|
xl: '6rem'
|
||||||
|
};
|
||||||
|
|
||||||
|
const iconSizeMap = {
|
||||||
|
sm: 3,
|
||||||
|
md: 5,
|
||||||
|
lg: 8,
|
||||||
|
xl: 12
|
||||||
|
} as const;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Surface
|
<Surface
|
||||||
variant="muted"
|
variant="muted"
|
||||||
rounded="full"
|
rounded="full"
|
||||||
border
|
style={{
|
||||||
borderColor="border-charcoal-outline/50"
|
width: sizeMap[size],
|
||||||
className={className}
|
height: sizeMap[size],
|
||||||
w={`${size}px`}
|
overflow: 'hidden',
|
||||||
h={`${size}px`}
|
border: '2px solid var(--ui-color-border-default)'
|
||||||
flexShrink={0}
|
}}
|
||||||
overflow="hidden"
|
|
||||||
>
|
>
|
||||||
{src ? (
|
{src ? (
|
||||||
<Image
|
<img
|
||||||
src={src}
|
src={src}
|
||||||
alt={alt}
|
alt={alt}
|
||||||
fullWidth
|
className="w-full h-full object-cover"
|
||||||
fullHeight
|
|
||||||
className="object-cover"
|
|
||||||
fallbackSrc="/default-avatar.png"
|
|
||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
<Box fullWidth fullHeight bg="bg-charcoal-outline" display="flex" center>
|
<Box center fullWidth fullHeight bg="var(--ui-color-bg-base)">
|
||||||
<span className="text-gray-400 font-bold" style={{ fontSize: size * 0.4 }}>
|
{fallback ? (
|
||||||
{alt.charAt(0).toUpperCase()}
|
<span className="text-sm font-bold text-[var(--ui-color-text-med)]">{fallback}</span>
|
||||||
</span>
|
) : (
|
||||||
|
<Icon icon={User} size={iconSizeMap[size]} intent="low" />
|
||||||
|
)}
|
||||||
</Box>
|
</Box>
|
||||||
)}
|
)}
|
||||||
</Surface>
|
</Surface>
|
||||||
);
|
);
|
||||||
}
|
};
|
||||||
|
|||||||
@@ -1,43 +1,57 @@
|
|||||||
import React, { ReactNode } from 'react';
|
import React, { ReactNode } from 'react';
|
||||||
import { Box, BoxProps } from './primitives/Box';
|
import { Box } from './primitives/Box';
|
||||||
import { Icon } from './Icon';
|
import { Icon } from './Icon';
|
||||||
import { LucideIcon } from 'lucide-react';
|
import { LucideIcon } from 'lucide-react';
|
||||||
|
import { Stack } from './primitives/Stack';
|
||||||
|
|
||||||
interface BadgeProps {
|
export interface BadgeProps {
|
||||||
children: ReactNode;
|
children: ReactNode;
|
||||||
variant?: 'default' | 'primary' | 'success' | 'warning' | 'danger' | 'info';
|
variant?: 'primary' | 'secondary' | 'success' | 'warning' | 'critical' | 'info' | 'outline' | 'default' | 'danger';
|
||||||
size?: 'xs' | 'sm' | 'md';
|
size?: 'sm' | 'md';
|
||||||
|
style?: React.CSSProperties;
|
||||||
icon?: LucideIcon;
|
icon?: LucideIcon;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function Badge({ children, variant = 'default', size = 'sm', icon }: BadgeProps) {
|
export const Badge = ({
|
||||||
const baseClasses = 'flex items-center gap-1.5 border font-bold uppercase tracking-widest';
|
children,
|
||||||
|
variant = 'primary',
|
||||||
const sizeClasses = {
|
size = 'md',
|
||||||
xs: 'px-1.5 py-0.5 text-[9px]',
|
style,
|
||||||
sm: 'px-2 py-0.5 text-[10px]',
|
icon
|
||||||
md: 'px-3 py-1 text-xs'
|
}: BadgeProps) => {
|
||||||
|
const variantClasses = {
|
||||||
|
primary: 'bg-[var(--ui-color-intent-primary)] text-white',
|
||||||
|
secondary: 'bg-[var(--ui-color-bg-surface)] text-[var(--ui-color-text-med)] border border-[var(--ui-color-border-default)]',
|
||||||
|
success: 'bg-[var(--ui-color-intent-success)] text-[var(--ui-color-bg-base)]',
|
||||||
|
warning: 'bg-[var(--ui-color-intent-warning)] text-[var(--ui-color-bg-base)]',
|
||||||
|
critical: 'bg-[var(--ui-color-intent-critical)] text-white',
|
||||||
|
danger: 'bg-[var(--ui-color-intent-critical)] text-white',
|
||||||
|
info: 'bg-[var(--ui-color-intent-telemetry)] text-[var(--ui-color-bg-base)]',
|
||||||
|
outline: 'bg-transparent text-[var(--ui-color-text-med)] border border-[var(--ui-color-border-default)]',
|
||||||
|
default: 'bg-[var(--ui-color-bg-surface-muted)] text-[var(--ui-color-text-med)]',
|
||||||
};
|
};
|
||||||
|
|
||||||
const variantClasses = {
|
const sizeClasses = {
|
||||||
default: 'bg-gray-500/10 border-gray-500/30 text-gray-400',
|
sm: 'px-1.5 py-0.5 text-[10px]',
|
||||||
primary: 'bg-primary-accent/10 border-primary-accent/30 text-primary-accent',
|
md: 'px-2 py-0.5 text-xs',
|
||||||
success: 'bg-success-green/10 border-success-green/30 text-success-green',
|
|
||||||
warning: 'bg-warning-amber/10 border-warning-amber/30 text-warning-amber',
|
|
||||||
danger: 'bg-critical-red/10 border-critical-red/30 text-critical-red',
|
|
||||||
info: 'bg-telemetry-aqua/10 border-telemetry-aqua/30 text-telemetry-aqua'
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const classes = [
|
const classes = [
|
||||||
baseClasses,
|
'inline-flex items-center justify-center font-bold uppercase tracking-wider rounded-none',
|
||||||
sizeClasses[size],
|
|
||||||
variantClasses[variant],
|
variantClasses[variant],
|
||||||
].filter(Boolean).join(' ');
|
sizeClasses[size],
|
||||||
|
].join(' ');
|
||||||
|
|
||||||
|
const content = icon ? (
|
||||||
|
<Stack direction="row" align="center" gap={1}>
|
||||||
|
<Icon icon={icon} size={3} />
|
||||||
|
{children}
|
||||||
|
</Stack>
|
||||||
|
) : children;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box className={classes}>
|
<Box as="span" className={classes} style={style}>
|
||||||
{icon && <Icon icon={icon} size={3} />}
|
{content}
|
||||||
{children}
|
|
||||||
</Box>
|
</Box>
|
||||||
);
|
);
|
||||||
}
|
};
|
||||||
|
|||||||
@@ -1,63 +1,55 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { Box } from './primitives/Box';
|
import { Box } from './primitives/Box';
|
||||||
import { Stack } from './primitives/Stack';
|
|
||||||
import { Surface } from './primitives/Surface';
|
|
||||||
import { Text } from './Text';
|
import { Text } from './Text';
|
||||||
|
|
||||||
interface Tab {
|
export interface TabOption {
|
||||||
id: string;
|
id: string;
|
||||||
label: string;
|
label: string;
|
||||||
icon?: React.ReactNode;
|
icon?: React.ReactNode;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface BorderTabsProps {
|
export interface BorderTabsProps {
|
||||||
tabs: Tab[];
|
tabs: TabOption[];
|
||||||
activeTab: string;
|
activeTabId: string;
|
||||||
onTabChange: (tabId: string) => void;
|
onTabChange: (id: string) => void;
|
||||||
className?: string;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function BorderTabs({ tabs, activeTab, onTabChange, className = '' }: BorderTabsProps) {
|
export const BorderTabs = ({
|
||||||
|
tabs,
|
||||||
|
activeTabId,
|
||||||
|
onTabChange
|
||||||
|
}: BorderTabsProps) => {
|
||||||
return (
|
return (
|
||||||
<Box borderBottom borderColor="border-border-gray/50" className={className}>
|
<Box display="flex" borderBottom>
|
||||||
<Stack direction="row" gap={8}>
|
{tabs.map((tab) => {
|
||||||
{tabs.map((tab) => {
|
const isActive = tab.id === activeTabId;
|
||||||
const isActive = activeTab === tab.id;
|
return (
|
||||||
return (
|
<button
|
||||||
<Surface
|
key={tab.id}
|
||||||
key={tab.id}
|
onClick={() => onTabChange(tab.id)}
|
||||||
as="button"
|
className={`px-6 py-4 text-sm font-bold uppercase tracking-widest transition-all relative ${
|
||||||
onClick={() => onTabChange(tab.id)}
|
isActive
|
||||||
variant="ghost"
|
? 'text-[var(--ui-color-intent-primary)]'
|
||||||
px={1}
|
: 'text-[var(--ui-color-text-low)] hover:text-[var(--ui-color-text-high)]'
|
||||||
py={4}
|
}`}
|
||||||
position="relative"
|
>
|
||||||
borderColor={isActive ? 'border-primary-blue' : ''}
|
<Box display="flex" alignItems="center" gap={2}>
|
||||||
borderBottom={isActive}
|
{tab.icon}
|
||||||
borderWidth={isActive ? '2px' : '0'}
|
{tab.label}
|
||||||
mb="-1px"
|
</Box>
|
||||||
transition="all 0.2s"
|
{isActive && (
|
||||||
group
|
<Box
|
||||||
>
|
position="absolute"
|
||||||
<Stack direction="row" align="center" gap={2}>
|
bottom={0}
|
||||||
{tab.icon && (
|
left={0}
|
||||||
<Box color={isActive ? 'text-primary-blue' : 'text-gray-400'} groupHoverTextColor={!isActive ? 'white' : undefined}>
|
right={0}
|
||||||
{tab.icon}
|
height="2px"
|
||||||
</Box>
|
bg="var(--ui-color-intent-primary)"
|
||||||
)}
|
/>
|
||||||
<Text
|
)}
|
||||||
size="sm"
|
</button>
|
||||||
weight="medium"
|
);
|
||||||
color={isActive ? 'text-primary-blue' : 'text-gray-400'}
|
})}
|
||||||
groupHoverTextColor={!isActive ? 'white' : undefined}
|
|
||||||
>
|
|
||||||
{tab.label}
|
|
||||||
</Text>
|
|
||||||
</Stack>
|
|
||||||
</Surface>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</Stack>
|
|
||||||
</Box>
|
</Box>
|
||||||
);
|
);
|
||||||
}
|
};
|
||||||
|
|||||||
@@ -1,17 +1,30 @@
|
|||||||
import React from 'react';
|
import React, { ReactNode } from 'react';
|
||||||
|
import { Box } from './primitives/Box';
|
||||||
|
import { Breadcrumbs, BreadcrumbItem } from './Breadcrumbs';
|
||||||
|
|
||||||
interface BreadcrumbBarProps {
|
export interface BreadcrumbBarProps {
|
||||||
children: React.ReactNode;
|
items: BreadcrumbItem[];
|
||||||
className?: string;
|
actions?: ReactNode;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
export const BreadcrumbBar = ({
|
||||||
* BreadcrumbBar is a container for breadcrumbs, typically placed at the top of the ContentShell.
|
items,
|
||||||
*/
|
actions
|
||||||
export function BreadcrumbBar({ children, className = '' }: BreadcrumbBarProps) {
|
}: BreadcrumbBarProps) => {
|
||||||
return (
|
return (
|
||||||
<div className={`mb-6 flex items-center space-x-2 text-sm ${className}`}>
|
<Box
|
||||||
{children}
|
display="flex"
|
||||||
</div>
|
alignItems="center"
|
||||||
|
justifyContent="between"
|
||||||
|
paddingY={4}
|
||||||
|
borderBottom
|
||||||
|
>
|
||||||
|
<Breadcrumbs items={items} />
|
||||||
|
{actions && (
|
||||||
|
<Box display="flex" alignItems="center" gap={4}>
|
||||||
|
{actions}
|
||||||
|
</Box>
|
||||||
|
)}
|
||||||
|
</Box>
|
||||||
);
|
);
|
||||||
}
|
};
|
||||||
|
|||||||
@@ -1,50 +1,39 @@
|
|||||||
import { Link } from '@/ui/Link';
|
import React from 'react';
|
||||||
import { Box } from '@/ui/primitives/Box';
|
import { Box } from './primitives/Box';
|
||||||
import { Stack } from '@/ui/primitives/Stack';
|
import { Text } from './Text';
|
||||||
import { Text } from '@/ui/Text';
|
import { ChevronRight } from 'lucide-react';
|
||||||
|
import { Icon } from './Icon';
|
||||||
|
import { Link } from './Link';
|
||||||
|
|
||||||
export type BreadcrumbItem = {
|
export interface BreadcrumbItem {
|
||||||
label: string;
|
label: string;
|
||||||
href?: string;
|
href?: string;
|
||||||
};
|
}
|
||||||
|
|
||||||
interface BreadcrumbsProps {
|
export interface BreadcrumbsProps {
|
||||||
items: BreadcrumbItem[];
|
items: BreadcrumbItem[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export function Breadcrumbs({ items }: BreadcrumbsProps) {
|
export const Breadcrumbs = ({ items }: BreadcrumbsProps) => {
|
||||||
if (!items || items.length === 0) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
const lastIndex = items.length - 1;
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box as="nav" aria-label="Breadcrumb" mb={4}>
|
<Box display="flex" alignItems="center" gap={2}>
|
||||||
<Stack direction="row" align="center" gap={2} wrap>
|
{items.map((item, index) => {
|
||||||
{items.map((item, index) => {
|
const isLast = index === items.length - 1;
|
||||||
const isLast = index === lastIndex;
|
return (
|
||||||
const content = item.href && !isLast ? (
|
<React.Fragment key={index}>
|
||||||
<Link
|
{index > 0 && <Icon icon={ChevronRight} size={3} intent="low" />}
|
||||||
href={item.href}
|
{isLast || !item.href ? (
|
||||||
variant="ghost"
|
<Text size="sm" variant={isLast ? 'high' : 'low'} weight={isLast ? 'bold' : 'normal'}>
|
||||||
>
|
{item.label}
|
||||||
{item.label}
|
</Text>
|
||||||
</Link>
|
) : (
|
||||||
) : (
|
<Link href={item.href} variant="secondary">
|
||||||
<Text color={isLast ? 'text-white' : 'text-gray-400'}>{item.label}</Text>
|
<Text size="sm">{item.label}</Text>
|
||||||
);
|
</Link>
|
||||||
|
)}
|
||||||
return (
|
</React.Fragment>
|
||||||
<Stack key={`${item.label}-${index}`} direction="row" align="center" gap={2}>
|
);
|
||||||
{index > 0 && (
|
})}
|
||||||
<Text color="text-gray-600">/</Text>
|
|
||||||
)}
|
|
||||||
{content}
|
|
||||||
</Stack>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</Stack>
|
|
||||||
</Box>
|
</Box>
|
||||||
);
|
);
|
||||||
}
|
};
|
||||||
|
|||||||
@@ -1,14 +1,13 @@
|
|||||||
import React, { ReactNode, MouseEventHandler, ButtonHTMLAttributes, forwardRef } from 'react';
|
import React, { ReactNode, MouseEventHandler, forwardRef } from 'react';
|
||||||
import { Stack } from './primitives/Stack';
|
import { Stack } from './primitives/Stack';
|
||||||
import { Box, BoxProps } from './primitives/Box';
|
|
||||||
import { Loader2 } from 'lucide-react';
|
|
||||||
import { Icon } from './Icon';
|
import { Icon } from './Icon';
|
||||||
|
import { Loader2 } from 'lucide-react';
|
||||||
|
import { ResponsiveValue } from './primitives/Box';
|
||||||
|
|
||||||
interface ButtonProps extends Omit<ButtonHTMLAttributes<HTMLButtonElement>, 'as' | 'onMouseEnter' | 'onMouseLeave' | 'onSubmit' | 'role' | 'translate' | 'onScroll' | 'draggable' | 'onChange' | 'onMouseDown' | 'onMouseUp' | 'onMouseMove' | 'value' | 'onBlur' | 'onKeyDown'>, Omit<BoxProps<'button'>, 'as' | 'onClick' | 'onSubmit'> {
|
export interface ButtonProps {
|
||||||
children: ReactNode;
|
children: ReactNode;
|
||||||
onClick?: MouseEventHandler<HTMLButtonElement>;
|
onClick?: MouseEventHandler<HTMLButtonElement | HTMLAnchorElement>;
|
||||||
className?: string;
|
variant?: 'primary' | 'secondary' | 'danger' | 'ghost' | 'success' | 'discord' | 'race-final';
|
||||||
variant?: 'primary' | 'secondary' | 'danger' | 'ghost' | 'race-final' | 'discord';
|
|
||||||
size?: 'sm' | 'md' | 'lg';
|
size?: 'sm' | 'md' | 'lg';
|
||||||
disabled?: boolean;
|
disabled?: boolean;
|
||||||
isLoading?: boolean;
|
isLoading?: boolean;
|
||||||
@@ -19,14 +18,25 @@ interface ButtonProps extends Omit<ButtonHTMLAttributes<HTMLButtonElement>, 'as'
|
|||||||
href?: string;
|
href?: string;
|
||||||
target?: string;
|
target?: string;
|
||||||
rel?: string;
|
rel?: string;
|
||||||
|
className?: string;
|
||||||
|
style?: React.CSSProperties;
|
||||||
|
width?: string | number | ResponsiveValue<string | number>;
|
||||||
|
height?: string | number | ResponsiveValue<string | number>;
|
||||||
|
minWidth?: string | number;
|
||||||
|
px?: number;
|
||||||
|
py?: number;
|
||||||
|
p?: number;
|
||||||
|
rounded?: string;
|
||||||
|
bg?: string;
|
||||||
|
color?: string;
|
||||||
fontSize?: string;
|
fontSize?: string;
|
||||||
backgroundColor?: string;
|
h?: string;
|
||||||
|
w?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const Button = forwardRef<HTMLButtonElement, ButtonProps>(({
|
export const Button = forwardRef<HTMLButtonElement | HTMLAnchorElement, ButtonProps>(({
|
||||||
children,
|
children,
|
||||||
onClick,
|
onClick,
|
||||||
className = '',
|
|
||||||
variant = 'primary',
|
variant = 'primary',
|
||||||
size = 'md',
|
size = 'md',
|
||||||
disabled = false,
|
disabled = false,
|
||||||
@@ -38,25 +48,37 @@ export const Button = forwardRef<HTMLButtonElement, ButtonProps>(({
|
|||||||
href,
|
href,
|
||||||
target,
|
target,
|
||||||
rel,
|
rel,
|
||||||
|
className,
|
||||||
|
style: styleProp,
|
||||||
|
width,
|
||||||
|
height,
|
||||||
|
minWidth,
|
||||||
|
px,
|
||||||
|
py,
|
||||||
|
p,
|
||||||
|
rounded,
|
||||||
|
bg,
|
||||||
|
color,
|
||||||
fontSize,
|
fontSize,
|
||||||
backgroundColor,
|
h,
|
||||||
...props
|
w,
|
||||||
}, ref) => {
|
}, ref) => {
|
||||||
const baseClasses = 'inline-flex items-center justify-center rounded-none transition-all duration-150 ease-smooth focus-visible:outline focus-visible:outline-1 focus-visible:outline-offset-2 active:opacity-80 uppercase tracking-widest font-bold';
|
const baseClasses = 'inline-flex items-center justify-center rounded-none transition-all duration-150 ease-in-out focus-visible:outline focus-visible:outline-1 focus-visible:outline-offset-2 active:opacity-80 uppercase tracking-widest font-bold';
|
||||||
|
|
||||||
const variantClasses = {
|
const variantClasses = {
|
||||||
primary: 'bg-primary-accent text-white hover:bg-primary-accent/90 focus-visible:outline-primary-accent shadow-[0_0_15px_rgba(25,140,255,0.3)] hover:shadow-[0_0_25px_rgba(25,140,255,0.5)]',
|
primary: 'bg-[var(--ui-color-intent-primary)] text-white hover:opacity-90 focus-visible:outline-[var(--ui-color-intent-primary)] shadow-[0_0_15px_rgba(25,140,255,0.3)] hover:shadow-[0_0_25px_rgba(25,140,255,0.5)]',
|
||||||
secondary: 'bg-panel-gray text-white border border-border-gray hover:bg-border-gray/50 focus-visible:outline-primary-accent',
|
secondary: 'bg-[var(--ui-color-bg-surface)] text-white border border-[var(--ui-color-border-default)] hover:bg-[var(--ui-color-border-default)] focus-visible:outline-[var(--ui-color-intent-primary)]',
|
||||||
danger: 'bg-critical-red text-white hover:bg-critical-red/90 focus-visible:outline-critical-red',
|
danger: 'bg-[var(--ui-color-intent-critical)] text-white hover:opacity-90 focus-visible:outline-[var(--ui-color-intent-critical)]',
|
||||||
ghost: 'bg-transparent text-gray-400 hover:text-white hover:bg-white/5 focus-visible:outline-gray-400',
|
ghost: 'bg-transparent text-[var(--ui-color-text-low)] hover:text-[var(--ui-color-text-high)] hover:bg-white/5 focus-visible:outline-[var(--ui-color-text-low)]',
|
||||||
'race-final': 'bg-success-green text-graphite-black hover:bg-success-green/90 focus-visible:outline-success-green',
|
success: 'bg-[var(--ui-color-intent-success)] text-[var(--ui-color-bg-base)] hover:opacity-90 focus-visible:outline-[var(--ui-color-intent-success)]',
|
||||||
|
'race-final': 'bg-[var(--ui-color-intent-success)] text-[var(--ui-color-bg-base)] hover:opacity-90 focus-visible:outline-[var(--ui-color-intent-success)]',
|
||||||
discord: 'bg-[#5865F2] text-white hover:bg-[#4752C4] focus-visible:outline-[#5865F2]',
|
discord: 'bg-[#5865F2] text-white hover:bg-[#4752C4] focus-visible:outline-[#5865F2]',
|
||||||
};
|
};
|
||||||
|
|
||||||
const sizeClasses = {
|
const sizeClasses = {
|
||||||
sm: 'min-h-[32px] px-3 py-1 text-xs font-medium',
|
sm: 'min-h-[32px] px-3 py-1 text-xs',
|
||||||
md: 'min-h-[40px] px-4 py-2 text-sm font-medium',
|
md: 'min-h-[40px] px-4 py-2 text-sm',
|
||||||
lg: 'min-h-[48px] px-6 py-3 text-base font-medium'
|
lg: 'min-h-[48px] px-6 py-3 text-base'
|
||||||
};
|
};
|
||||||
|
|
||||||
const disabledClasses = (disabled || isLoading) ? 'opacity-40 cursor-not-allowed' : 'cursor-pointer';
|
const disabledClasses = (disabled || isLoading) ? 'opacity-40 cursor-not-allowed' : 'cursor-pointer';
|
||||||
@@ -71,8 +93,18 @@ export const Button = forwardRef<HTMLButtonElement, ButtonProps>(({
|
|||||||
className
|
className
|
||||||
].filter(Boolean).join(' ');
|
].filter(Boolean).join(' ');
|
||||||
|
|
||||||
|
const style: React.CSSProperties = {
|
||||||
|
...(width ? { width: typeof width === 'object' ? undefined : width } : {}),
|
||||||
|
...(height ? { height: typeof height === 'object' ? undefined : height } : {}),
|
||||||
|
...(minWidth ? { minWidth } : {}),
|
||||||
|
...(fontSize ? { fontSize } : {}),
|
||||||
|
...(h ? { height: h } : {}),
|
||||||
|
...(w ? { width: w } : {}),
|
||||||
|
...(styleProp || {})
|
||||||
|
};
|
||||||
|
|
||||||
const content = (
|
const content = (
|
||||||
<Stack direction="row" align="center" gap={2} center={fullWidth}>
|
<Stack direction="row" align="center" gap={2}>
|
||||||
{isLoading && <Icon icon={Loader2} size={size === 'sm' ? 3 : 4} animate="spin" />}
|
{isLoading && <Icon icon={Loader2} size={size === 'sm' ? 3 : 4} animate="spin" />}
|
||||||
{!isLoading && icon}
|
{!isLoading && icon}
|
||||||
{children}
|
{children}
|
||||||
@@ -81,35 +113,31 @@ export const Button = forwardRef<HTMLButtonElement, ButtonProps>(({
|
|||||||
|
|
||||||
if (as === 'a') {
|
if (as === 'a') {
|
||||||
return (
|
return (
|
||||||
<Box
|
<a
|
||||||
as="a"
|
ref={ref as React.ForwardedRef<HTMLAnchorElement>}
|
||||||
href={href}
|
href={href}
|
||||||
target={target}
|
target={target}
|
||||||
rel={rel}
|
rel={rel}
|
||||||
className={classes}
|
className={classes}
|
||||||
fontSize={fontSize}
|
onClick={onClick as MouseEventHandler<HTMLAnchorElement>}
|
||||||
backgroundColor={backgroundColor}
|
style={style}
|
||||||
{...props}
|
|
||||||
>
|
>
|
||||||
{content}
|
{content}
|
||||||
</Box>
|
</a>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box
|
<button
|
||||||
as="button"
|
ref={ref as React.ForwardedRef<HTMLButtonElement>}
|
||||||
ref={ref}
|
|
||||||
type={type}
|
type={type}
|
||||||
className={classes}
|
className={classes}
|
||||||
onClick={onClick}
|
onClick={onClick as MouseEventHandler<HTMLButtonElement>}
|
||||||
disabled={disabled || isLoading}
|
disabled={disabled || isLoading}
|
||||||
fontSize={fontSize}
|
style={style}
|
||||||
backgroundColor={backgroundColor}
|
|
||||||
{...props}
|
|
||||||
>
|
>
|
||||||
{content}
|
{content}
|
||||||
</Box>
|
</button>
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -1,49 +1,73 @@
|
|||||||
import React, { ReactNode, MouseEventHandler } from 'react';
|
import React, { ReactNode, forwardRef } from 'react';
|
||||||
import { Box, BoxProps } from './primitives/Box';
|
import { Surface, SurfaceProps } from './primitives/Surface';
|
||||||
|
import { Box } from './primitives/Box';
|
||||||
|
|
||||||
export interface CardProps extends Omit<BoxProps<'div'>, 'children' | 'onClick'> {
|
export interface CardProps extends Omit<SurfaceProps<'div'>, 'children' | 'title' | 'variant'> {
|
||||||
children: ReactNode;
|
children: ReactNode;
|
||||||
onClick?: MouseEventHandler<HTMLDivElement>;
|
variant?: 'default' | 'dark' | 'muted' | 'glass' | 'outline';
|
||||||
variant?: 'default' | 'outline' | 'ghost' | 'muted' | 'dark' | 'glass';
|
title?: ReactNode;
|
||||||
|
footer?: ReactNode;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function Card({
|
export const Card = forwardRef<HTMLDivElement, CardProps>(({
|
||||||
children,
|
children,
|
||||||
className = '',
|
|
||||||
onClick,
|
|
||||||
variant = 'default',
|
variant = 'default',
|
||||||
|
title,
|
||||||
|
footer,
|
||||||
...props
|
...props
|
||||||
}: CardProps) {
|
}, ref) => {
|
||||||
const baseClasses = 'rounded-none transition-all duration-150 ease-smooth';
|
const isOutline = variant === 'outline';
|
||||||
|
|
||||||
const variantClasses = {
|
const style: React.CSSProperties = isOutline ? {
|
||||||
default: 'bg-panel-gray border border-border-gray shadow-card',
|
backgroundColor: 'transparent',
|
||||||
outline: 'bg-transparent border border-border-gray',
|
border: '1px solid var(--ui-color-border-default)',
|
||||||
ghost: 'bg-transparent border-none',
|
} : {};
|
||||||
muted: 'bg-panel-gray/40 border border-border-gray',
|
|
||||||
dark: 'bg-graphite-black border border-border-gray',
|
|
||||||
glass: 'bg-graphite-black/60 backdrop-blur-md border border-border-gray'
|
|
||||||
};
|
|
||||||
|
|
||||||
const classes = [
|
|
||||||
baseClasses,
|
|
||||||
variantClasses[variant],
|
|
||||||
onClick ? 'cursor-pointer hover:bg-border-gray/30' : '',
|
|
||||||
className
|
|
||||||
].filter(Boolean).join(' ');
|
|
||||||
|
|
||||||
// Default padding if none provided
|
|
||||||
const hasPadding = props.p !== undefined || props.px !== undefined || props.py !== undefined ||
|
|
||||||
props.pt !== undefined || props.pb !== undefined || props.pl !== undefined || props.pr !== undefined;
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box
|
<Surface
|
||||||
className={classes}
|
ref={ref}
|
||||||
onClick={onClick}
|
variant={isOutline ? 'default' : variant}
|
||||||
p={hasPadding ? undefined : 4}
|
rounded="lg"
|
||||||
|
shadow="md"
|
||||||
|
style={style}
|
||||||
{...props}
|
{...props}
|
||||||
>
|
>
|
||||||
{children}
|
{title && (
|
||||||
</Box>
|
<Box padding={4} borderBottom>
|
||||||
|
{typeof title === 'string' ? (
|
||||||
|
<h3 className="text-lg font-bold text-[var(--ui-color-text-high)]">{title}</h3>
|
||||||
|
) : title}
|
||||||
|
</Box>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<Box padding={4}>
|
||||||
|
{children}
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
{footer && (
|
||||||
|
<Box padding={4} borderTop bg="rgba(255,255,255,0.02)">
|
||||||
|
{footer}
|
||||||
|
</Box>
|
||||||
|
)}
|
||||||
|
</Surface>
|
||||||
);
|
);
|
||||||
}
|
});
|
||||||
|
|
||||||
|
Card.displayName = 'Card';
|
||||||
|
|
||||||
|
export const CardHeader = ({ title, children }: { title?: string, children?: ReactNode }) => (
|
||||||
|
<Box marginBottom={4}>
|
||||||
|
{title && <h3 className="text-lg font-bold text-[var(--ui-color-text-high)]">{title}</h3>}
|
||||||
|
{children}
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
|
||||||
|
export const CardContent = ({ children }: { children: ReactNode }) => (
|
||||||
|
<Box>{children}</Box>
|
||||||
|
);
|
||||||
|
|
||||||
|
export const CardFooter = ({ children }: { children: ReactNode }) => (
|
||||||
|
<Box marginTop={4} paddingTop={4} borderTop>
|
||||||
|
{children}
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
|||||||
@@ -1,77 +1,37 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { Box } from './primitives/Box';
|
||||||
|
import { CategoryDistributionCard } from './CategoryDistributionCard';
|
||||||
|
import { LucideIcon } from 'lucide-react';
|
||||||
|
|
||||||
|
export interface CategoryData {
|
||||||
import { CategoryDistributionCard } from '@/ui/CategoryDistributionCard';
|
id: string;
|
||||||
import { Heading } from '@/ui/Heading';
|
label: string;
|
||||||
import { Icon } from '@/ui/Icon';
|
count: number;
|
||||||
import { Box } from '@/ui/primitives/Box';
|
icon: LucideIcon;
|
||||||
import { Grid } from '@/ui/primitives/Grid';
|
intent?: 'primary' | 'success' | 'warning' | 'critical' | 'telemetry';
|
||||||
import { Text } from '@/ui/Text';
|
|
||||||
import { BarChart3 } from 'lucide-react';
|
|
||||||
|
|
||||||
const CATEGORIES = [
|
|
||||||
{ id: 'beginner', label: 'Beginner', color: 'text-green-400', bgColor: 'bg-green-400/10', borderColor: 'border-green-400/30', progressColor: 'bg-green-400' },
|
|
||||||
{ id: 'intermediate', label: 'Intermediate', color: 'text-primary-blue', bgColor: 'bg-primary-blue/10', borderColor: 'border-primary-blue/30', progressColor: 'bg-primary-blue' },
|
|
||||||
{ id: 'advanced', label: 'Advanced', color: 'text-purple-400', bgColor: 'bg-purple-400/10', borderColor: 'border-purple-400/30', progressColor: 'bg-purple-400' },
|
|
||||||
{ id: 'pro', label: 'Pro', color: 'text-yellow-400', bgColor: 'bg-yellow-400/10', borderColor: 'border-yellow-400/30', progressColor: 'bg-yellow-400' },
|
|
||||||
{ id: 'endurance', label: 'Endurance', color: 'text-orange-400', bgColor: 'bg-orange-400/10', borderColor: 'border-orange-400/30', progressColor: 'bg-orange-400' },
|
|
||||||
{ id: 'sprint', label: 'Sprint', color: 'text-red-400', bgColor: 'bg-red-400/10', borderColor: 'border-red-400/30', progressColor: 'bg-red-400' },
|
|
||||||
];
|
|
||||||
|
|
||||||
interface CategoryDistributionProps {
|
|
||||||
drivers: {
|
|
||||||
category?: string;
|
|
||||||
}[];
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function CategoryDistribution({ drivers }: CategoryDistributionProps) {
|
export interface CategoryDistributionProps {
|
||||||
const distribution = CATEGORIES.map((category) => ({
|
categories: CategoryData[];
|
||||||
...category,
|
}
|
||||||
count: drivers.filter((d) => d.category === category.id).length,
|
|
||||||
percentage: drivers.length > 0
|
export const CategoryDistribution = ({
|
||||||
? Math.round((drivers.filter((d) => d.category === category.id).length / drivers.length) * 100)
|
categories
|
||||||
: 0,
|
}: CategoryDistributionProps) => {
|
||||||
}));
|
const total = categories.reduce((acc, cat) => acc + cat.count, 0);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box mb={10}>
|
<Box display="grid" gridCols={{ base: 1, md: 2, lg: 3 }} gap={4}>
|
||||||
<Box display="flex" alignItems="center" gap={3} mb={4}>
|
{categories.map((category) => (
|
||||||
<Box
|
<CategoryDistributionCard
|
||||||
display="flex"
|
key={category.id}
|
||||||
h="10"
|
label={category.label}
|
||||||
w="10"
|
count={category.count}
|
||||||
alignItems="center"
|
total={total}
|
||||||
justifyContent="center"
|
icon={category.icon}
|
||||||
rounded="xl"
|
intent={category.intent}
|
||||||
bg="bg-purple-400/10"
|
/>
|
||||||
border
|
))}
|
||||||
borderColor="border-purple-400/20"
|
|
||||||
>
|
|
||||||
<Icon
|
|
||||||
icon={BarChart3}
|
|
||||||
size={5}
|
|
||||||
color="rgb(192, 132, 252)"
|
|
||||||
/>
|
|
||||||
</Box>
|
|
||||||
<Box>
|
|
||||||
<Heading level={2}>Category Distribution</Heading>
|
|
||||||
<Text size="xs" color="text-gray-500">Driver population by category</Text>
|
|
||||||
</Box>
|
|
||||||
</Box>
|
|
||||||
|
|
||||||
<Grid cols={2} lgCols={3} gap={4}>
|
|
||||||
{distribution.map((category) => (
|
|
||||||
<CategoryDistributionCard
|
|
||||||
key={category.id}
|
|
||||||
label={category.label}
|
|
||||||
count={category.count}
|
|
||||||
percentage={category.percentage}
|
|
||||||
color={category.color}
|
|
||||||
bgColor={category.bgColor}
|
|
||||||
borderColor={category.borderColor}
|
|
||||||
progressColor={category.progressColor}
|
|
||||||
/>
|
|
||||||
))}
|
|
||||||
</Grid>
|
|
||||||
</Box>
|
</Box>
|
||||||
);
|
);
|
||||||
}
|
};
|
||||||
|
|||||||
@@ -3,47 +3,57 @@ import { Box } from './primitives/Box';
|
|||||||
import { Text } from './Text';
|
import { Text } from './Text';
|
||||||
import { Icon } from './Icon';
|
import { Icon } from './Icon';
|
||||||
import { LucideIcon } from 'lucide-react';
|
import { LucideIcon } from 'lucide-react';
|
||||||
|
import { Surface } from './primitives/Surface';
|
||||||
|
|
||||||
interface CategoryDistributionCardProps {
|
export interface CategoryDistributionCardProps {
|
||||||
label: string;
|
label: string;
|
||||||
count: number;
|
count: number;
|
||||||
percentage: number;
|
total: number;
|
||||||
icon: LucideIcon;
|
icon: LucideIcon;
|
||||||
color: string;
|
intent?: 'primary' | 'success' | 'warning' | 'critical' | 'telemetry';
|
||||||
bgColor: string;
|
|
||||||
borderColor: string;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function CategoryDistributionCard({
|
export const CategoryDistributionCard = ({
|
||||||
label,
|
label,
|
||||||
count,
|
count,
|
||||||
percentage,
|
total,
|
||||||
icon,
|
icon,
|
||||||
color,
|
intent = 'primary'
|
||||||
bgColor,
|
}: CategoryDistributionCardProps) => {
|
||||||
borderColor,
|
const percentage = total > 0 ? (count / total) * 100 : 0;
|
||||||
}: CategoryDistributionCardProps) {
|
|
||||||
|
const intentColorMap = {
|
||||||
|
primary: 'var(--ui-color-intent-primary)',
|
||||||
|
success: 'var(--ui-color-intent-success)',
|
||||||
|
warning: 'var(--ui-color-intent-warning)',
|
||||||
|
critical: 'var(--ui-color-intent-critical)',
|
||||||
|
telemetry: 'var(--ui-color-intent-telemetry)',
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box p={4} rounded="xl" bg={bgColor} border borderColor={borderColor}>
|
<Surface variant="muted" rounded="xl" padding={4} style={{ border: '1px solid var(--ui-color-border-default)' }}>
|
||||||
<Box display="flex" alignItems="center" justifyContent="between" mb={3}>
|
<Box display="flex" alignItems="center" justifyContent="between" marginBottom={3}>
|
||||||
<Text size="2xl" weight="bold" color={color}>{count}</Text>
|
<Text size="2xl" weight="bold" variant="high">{count}</Text>
|
||||||
<Box p={2} rounded="lg" bg="bg-white/5">
|
<Box padding={2} rounded="lg" bg="var(--ui-color-bg-surface-muted)">
|
||||||
<Icon icon={icon} size={5} color={color} />
|
<Icon icon={icon} size={5} intent={intent} />
|
||||||
</Box>
|
</Box>
|
||||||
</Box>
|
</Box>
|
||||||
<Text size="sm" weight="medium" color="text-white" block mb={1}>
|
|
||||||
|
<Text size="sm" weight="medium" variant="high" block marginBottom={1}>
|
||||||
{label}
|
{label}
|
||||||
</Text>
|
</Text>
|
||||||
<Box w="full" h="1.5" bg="bg-white/5" rounded="full" overflow="hidden">
|
|
||||||
<Box
|
<Box fullWidth height="0.375rem" bg="var(--ui-color-bg-surface-muted)" style={{ borderRadius: '9999px', overflow: 'hidden' }}>
|
||||||
h="full"
|
<Box
|
||||||
bg={color.replace('text-', 'bg-')}
|
fullHeight
|
||||||
style={{ width: `${percentage}%` }}
|
bg={intentColorMap[intent]}
|
||||||
|
style={{ width: `${percentage}%` }}
|
||||||
/>
|
/>
|
||||||
</Box>
|
</Box>
|
||||||
<Text size="xs" color="text-gray-500" mt={2}>
|
|
||||||
{percentage.toFixed(1)}% of total
|
<Text size="xs" variant="low" marginTop={2}>
|
||||||
|
{Math.round(percentage)}% of total
|
||||||
</Text>
|
</Text>
|
||||||
</Box>
|
</Surface>
|
||||||
);
|
);
|
||||||
}
|
};
|
||||||
|
|||||||
@@ -1,44 +1,29 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { Box } from './primitives/Box';
|
import { Box } from './primitives/Box';
|
||||||
import { Image } from './Image';
|
|
||||||
import { Tag } from 'lucide-react';
|
|
||||||
import { Icon } from './Icon';
|
import { Icon } from './Icon';
|
||||||
|
import { Tag } from 'lucide-react';
|
||||||
|
|
||||||
export interface CategoryIconProps {
|
export interface CategoryIconProps {
|
||||||
categoryId?: string;
|
category: string;
|
||||||
src?: string;
|
|
||||||
alt: string;
|
|
||||||
size?: number;
|
size?: number;
|
||||||
className?: string;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function CategoryIcon({
|
export const CategoryIcon = ({
|
||||||
categoryId,
|
category,
|
||||||
src,
|
size = 24
|
||||||
alt,
|
}: CategoryIconProps) => {
|
||||||
size = 24,
|
// Map categories to icons if needed, for now just use Tag
|
||||||
className = '',
|
|
||||||
}: CategoryIconProps) {
|
|
||||||
const iconSrc = src || (categoryId ? `/media/categories/${categoryId}/icon` : undefined);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box
|
<Box
|
||||||
display="flex"
|
width={size}
|
||||||
alignItems="center"
|
height={size}
|
||||||
justifyContent="center"
|
display="flex"
|
||||||
className={className}
|
alignItems="center"
|
||||||
style={{ width: size, height: size, flexShrink: 0 }}
|
justifyContent="center"
|
||||||
|
bg="var(--ui-color-bg-surface-muted)"
|
||||||
|
style={{ borderRadius: 'var(--ui-radius-sm)' }}
|
||||||
>
|
>
|
||||||
{iconSrc ? (
|
<Icon icon={Tag} size={size > 20 ? 4 : 3} intent="low" />
|
||||||
<Image
|
|
||||||
src={iconSrc}
|
|
||||||
alt={alt}
|
|
||||||
className="w-full h-full object-contain"
|
|
||||||
fallbackSrc="/default-category-icon.png"
|
|
||||||
/>
|
|
||||||
) : (
|
|
||||||
<Icon icon={Tag} size={size > 20 ? 4 : 3} color="text-gray-500" />
|
|
||||||
)}
|
|
||||||
</Box>
|
</Box>
|
||||||
);
|
);
|
||||||
}
|
};
|
||||||
|
|||||||
@@ -1,33 +1,50 @@
|
|||||||
import React from 'react';
|
import React, { forwardRef, ChangeEvent } from 'react';
|
||||||
import { Box } from './primitives/Box';
|
import { Box } from './primitives/Box';
|
||||||
import { Text } from './Text';
|
import { Text } from './Text';
|
||||||
|
|
||||||
interface CheckboxProps {
|
export interface CheckboxProps {
|
||||||
label: string;
|
label: string;
|
||||||
checked: boolean;
|
checked: boolean;
|
||||||
onChange: (checked: boolean) => void;
|
onChange: (checked: boolean) => void;
|
||||||
disabled?: boolean;
|
disabled?: boolean;
|
||||||
|
error?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function Checkbox({ label, checked, onChange, disabled }: CheckboxProps) {
|
export const Checkbox = forwardRef<HTMLInputElement, CheckboxProps>(({
|
||||||
|
label,
|
||||||
|
checked,
|
||||||
|
onChange,
|
||||||
|
disabled = false,
|
||||||
|
error
|
||||||
|
}, ref) => {
|
||||||
|
const handleChange = (e: ChangeEvent<HTMLInputElement>) => {
|
||||||
|
onChange(e.target.checked);
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box as="label" display="flex" alignItems="center" gap={2} cursor={disabled ? 'not-allowed' : 'pointer'}>
|
<Box>
|
||||||
<Box
|
<Box as="label" display="flex" alignItems="center" gap={2} style={{ cursor: disabled ? 'not-allowed' : 'pointer' }}>
|
||||||
as="input"
|
<input
|
||||||
type="checkbox"
|
ref={ref}
|
||||||
checked={checked}
|
type="checkbox"
|
||||||
onChange={(e: React.ChangeEvent<HTMLInputElement>) => onChange(e.target.checked)}
|
checked={checked}
|
||||||
disabled={disabled}
|
onChange={handleChange}
|
||||||
w="4"
|
disabled={disabled}
|
||||||
h="4"
|
className="w-4 h-4 rounded-none border-[var(--ui-color-border-default)] bg-[var(--ui-color-bg-surface)] text-[var(--ui-color-intent-primary)] focus:ring-[var(--ui-color-intent-primary)]"
|
||||||
bg="bg-deep-graphite"
|
/>
|
||||||
border
|
<Text size="sm" variant={disabled ? 'low' : 'high'}>
|
||||||
borderColor="border-charcoal-outline"
|
{label}
|
||||||
rounded="sm"
|
</Text>
|
||||||
ring="primary-blue"
|
</Box>
|
||||||
color="text-primary-blue"
|
{error && (
|
||||||
/>
|
<Box marginTop={1}>
|
||||||
<Text size="sm" color={disabled ? 'text-gray-500' : 'text-white'}>{label}</Text>
|
<Text size="xs" variant="critical">
|
||||||
|
{error}
|
||||||
|
</Text>
|
||||||
|
</Box>
|
||||||
|
)}
|
||||||
</Box>
|
</Box>
|
||||||
);
|
);
|
||||||
}
|
});
|
||||||
|
|
||||||
|
Checkbox.displayName = 'Checkbox';
|
||||||
|
|||||||
@@ -1,53 +1,79 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { Box } from './primitives/Box';
|
||||||
|
import { Text } from './Text';
|
||||||
|
|
||||||
|
export interface CircularProgressProps {
|
||||||
|
|
||||||
interface CircularProgressProps {
|
|
||||||
value: number;
|
value: number;
|
||||||
max: number;
|
max?: number;
|
||||||
label: string;
|
|
||||||
color: string;
|
|
||||||
size?: number;
|
size?: number;
|
||||||
|
strokeWidth?: number;
|
||||||
|
intent?: 'primary' | 'success' | 'warning' | 'critical' | 'telemetry';
|
||||||
|
showValue?: boolean;
|
||||||
|
label?: string;
|
||||||
|
color?: string; // Alias for intent
|
||||||
}
|
}
|
||||||
|
|
||||||
export function CircularProgress({ value, max, label, color, size = 80 }: CircularProgressProps) {
|
export const CircularProgress = ({
|
||||||
const percentage = Math.min((value / max) * 100, 100);
|
value,
|
||||||
const strokeWidth = 6;
|
max = 100,
|
||||||
|
size = 64,
|
||||||
|
strokeWidth = 4,
|
||||||
|
intent = 'primary',
|
||||||
|
showValue = false,
|
||||||
|
label,
|
||||||
|
color: colorProp
|
||||||
|
}: CircularProgressProps) => {
|
||||||
const radius = (size - strokeWidth) / 2;
|
const radius = (size - strokeWidth) / 2;
|
||||||
const circumference = radius * 2 * Math.PI;
|
const circumference = radius * 2 * Math.PI;
|
||||||
const strokeDashoffset = circumference - (percentage / 100) * circumference;
|
const offset = circumference - (value / max) * circumference;
|
||||||
|
|
||||||
|
const intentColorMap = {
|
||||||
|
primary: 'var(--ui-color-intent-primary)',
|
||||||
|
success: 'var(--ui-color-intent-success)',
|
||||||
|
warning: 'var(--ui-color-intent-warning)',
|
||||||
|
critical: 'var(--ui-color-intent-critical)',
|
||||||
|
telemetry: 'var(--ui-color-intent-telemetry)',
|
||||||
|
};
|
||||||
|
|
||||||
|
const color = colorProp || intentColorMap[intent];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-col items-center">
|
<Box display="flex" flexDirection="col" alignItems="center" gap={2}>
|
||||||
<div className="relative" style={{ width: size, height: size }}>
|
<Box position="relative" width={size} height={size} display="flex" alignItems="center" justifyContent="center">
|
||||||
<svg className="transform -rotate-90" width={size} height={size}>
|
<svg width={size} height={size} className="transform -rotate-90">
|
||||||
<circle
|
<circle
|
||||||
cx={size / 2}
|
cx={size / 2}
|
||||||
cy={size / 2}
|
cy={size / 2}
|
||||||
r={radius}
|
r={radius}
|
||||||
stroke="currentColor"
|
stroke="var(--ui-color-bg-surface-muted)"
|
||||||
strokeWidth={strokeWidth}
|
strokeWidth={strokeWidth}
|
||||||
fill="transparent"
|
fill="transparent"
|
||||||
className="text-charcoal-outline"
|
|
||||||
/>
|
/>
|
||||||
<circle
|
<circle
|
||||||
cx={size / 2}
|
cx={size / 2}
|
||||||
cy={size / 2}
|
cy={size / 2}
|
||||||
r={radius}
|
r={radius}
|
||||||
stroke="currentColor"
|
stroke={color}
|
||||||
strokeWidth={strokeWidth}
|
strokeWidth={strokeWidth}
|
||||||
fill="transparent"
|
fill="transparent"
|
||||||
strokeDasharray={circumference}
|
strokeDasharray={circumference}
|
||||||
strokeDashoffset={strokeDashoffset}
|
style={{ strokeDashoffset: offset, transition: 'stroke-dashoffset 0.3s ease-in-out' }}
|
||||||
strokeLinecap="round"
|
strokeLinecap="round"
|
||||||
className={color}
|
|
||||||
style={{ transition: 'stroke-dashoffset 0.5s ease-in-out' }}
|
|
||||||
/>
|
/>
|
||||||
</svg>
|
</svg>
|
||||||
<div className="absolute inset-0 flex items-center justify-center">
|
{showValue && (
|
||||||
<span className="text-lg font-bold text-white">{percentage.toFixed(0)}%</span>
|
<Box position="absolute" inset={0} display="flex" alignItems="center" justifyContent="center">
|
||||||
</div>
|
<Text size="xs" weight="bold" variant="high">
|
||||||
</div>
|
{Math.round((value / max) * 100)}%
|
||||||
<span className="text-xs text-gray-400 mt-2">{label}</span>
|
</Text>
|
||||||
</div>
|
</Box>
|
||||||
|
)}
|
||||||
|
</Box>
|
||||||
|
{label && (
|
||||||
|
<Text size="xs" weight="bold" variant="low" uppercase letterSpacing="wider">
|
||||||
|
{label}
|
||||||
|
</Text>
|
||||||
|
)}
|
||||||
|
</Box>
|
||||||
);
|
);
|
||||||
}
|
};
|
||||||
|
|||||||
@@ -1,53 +1,26 @@
|
|||||||
import React, { ReactNode } from 'react';
|
import React, { ReactNode } from 'react';
|
||||||
import { Box, BoxProps } from './primitives/Box';
|
import { Box } from './primitives/Box';
|
||||||
|
|
||||||
type Spacing = 0 | 0.5 | 1 | 1.5 | 2 | 2.5 | 3 | 3.5 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 14 | 16 | 20 | 24 | 28 | 32 | 36 | 40 | 44 | 48 | 52 | 56 | 60 | 64 | 72 | 80 | 96;
|
export interface ContainerProps {
|
||||||
|
|
||||||
interface ContainerProps extends Omit<BoxProps<'div'>, 'size' | 'padding'> {
|
|
||||||
children: ReactNode;
|
children: ReactNode;
|
||||||
size?: 'sm' | 'md' | 'lg' | 'xl' | 'full';
|
size?: 'sm' | 'md' | 'lg' | 'xl' | 'full';
|
||||||
padding?: boolean;
|
|
||||||
className?: string;
|
|
||||||
py?: Spacing;
|
|
||||||
pb?: Spacing;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function Container({
|
export const Container = ({
|
||||||
children,
|
children,
|
||||||
size = 'lg',
|
size = 'lg'
|
||||||
padding = true,
|
}: ContainerProps) => {
|
||||||
className = '',
|
const sizeMap = {
|
||||||
py,
|
sm: '40rem',
|
||||||
pb,
|
md: '48rem',
|
||||||
...props
|
lg: '64rem',
|
||||||
}: ContainerProps) {
|
xl: '80rem',
|
||||||
const sizeClasses = {
|
full: '100%',
|
||||||
sm: 'max-w-2xl',
|
|
||||||
md: 'max-w-4xl',
|
|
||||||
lg: 'max-w-7xl',
|
|
||||||
xl: 'max-w-[1400px]',
|
|
||||||
full: 'max-w-full'
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const spacingMap: Record<number, string> = {
|
|
||||||
0: '0', 0.5: '0.5', 1: '1', 1.5: '1.5', 2: '2', 2.5: '2.5', 3: '3', 3.5: '3.5', 4: '4',
|
|
||||||
5: '5', 6: '6', 7: '7', 8: '8', 9: '9', 10: '10', 11: '11', 12: '12', 14: '14',
|
|
||||||
16: '16', 20: '20', 24: '24', 28: '28', 32: '32', 36: '36', 40: '40', 44: '44',
|
|
||||||
48: '48', 52: '52', 56: '56', 60: '60', 64: '64', 72: '72', 80: '80', 96: '96'
|
|
||||||
};
|
|
||||||
|
|
||||||
const classes = [
|
|
||||||
'mx-auto',
|
|
||||||
sizeClasses[size],
|
|
||||||
padding ? 'px-4 sm:px-6 lg:px-8' : '',
|
|
||||||
py !== undefined ? `py-${spacingMap[py]}` : '',
|
|
||||||
pb !== undefined ? `pb-${spacingMap[pb]}` : '',
|
|
||||||
className
|
|
||||||
].filter(Boolean).join(' ');
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box className={classes} {...props}>
|
<Box marginX="auto" maxWidth={sizeMap[size]} paddingX={4} fullWidth>
|
||||||
{children}
|
{children}
|
||||||
</Box>
|
</Box>
|
||||||
);
|
);
|
||||||
}
|
};
|
||||||
|
|||||||
@@ -1,20 +1,34 @@
|
|||||||
import React from 'react';
|
import React, { ReactNode } from 'react';
|
||||||
|
import { Box } from './primitives/Box';
|
||||||
|
|
||||||
interface ContentShellProps {
|
export interface ContentShellProps {
|
||||||
children: React.ReactNode;
|
children: ReactNode;
|
||||||
className?: string;
|
header?: ReactNode;
|
||||||
|
sidebar?: ReactNode;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
export const ContentShell = ({
|
||||||
* ContentShell is the main data zone of the application.
|
children,
|
||||||
* It houses the primary content and track maps/data tables.
|
header,
|
||||||
*/
|
sidebar
|
||||||
export function ContentShell({ children, className = '' }: ContentShellProps) {
|
}: ContentShellProps) => {
|
||||||
return (
|
return (
|
||||||
<main className={`flex-1 overflow-y-auto bg-[#0C0D0F] ${className}`}>
|
<Box display="flex" flexDirection="col" fullHeight>
|
||||||
<div className="max-w-7xl mx-auto px-4 md:px-6 py-6">
|
{header && (
|
||||||
{children}
|
<Box borderBottom>
|
||||||
</div>
|
{header}
|
||||||
</main>
|
</Box>
|
||||||
|
)}
|
||||||
|
<Box display="flex" flex={1} minHeight="0">
|
||||||
|
{sidebar && (
|
||||||
|
<Box width="18rem" borderRight display={{ base: 'none', lg: 'block' }}>
|
||||||
|
{sidebar}
|
||||||
|
</Box>
|
||||||
|
)}
|
||||||
|
<Box flex={1} overflow="auto">
|
||||||
|
{children}
|
||||||
|
</Box>
|
||||||
|
</Box>
|
||||||
|
</Box>
|
||||||
);
|
);
|
||||||
}
|
};
|
||||||
|
|||||||
@@ -1,22 +1,30 @@
|
|||||||
import React from 'react';
|
import React, { ReactNode } from 'react';
|
||||||
|
import { Box } from './primitives/Box';
|
||||||
|
import { Container } from './Container';
|
||||||
|
|
||||||
interface ContentViewportProps {
|
export interface ContentViewportProps {
|
||||||
children: React.ReactNode;
|
children: ReactNode;
|
||||||
className?: string;
|
padding?: 'none' | 'sm' | 'md' | 'lg';
|
||||||
fullWidth?: boolean;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
export const ContentViewport = ({
|
||||||
* ContentViewport is the main data zone of the "Telemetry Workspace".
|
children,
|
||||||
* It houses the primary content, track maps, and data tables.
|
padding = 'md'
|
||||||
* Aligned with "Precision Racing Minimal" theme.
|
}: ContentViewportProps) => {
|
||||||
*/
|
const paddingMap: Record<string, any> = {
|
||||||
export function ContentViewport({ children, className = '', fullWidth = false }: ContentViewportProps) {
|
none: 0,
|
||||||
|
sm: 4,
|
||||||
|
md: 8,
|
||||||
|
lg: 12,
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<main className={`flex-1 overflow-y-auto bg-[#0C0D0F] ${className}`}>
|
<Box as="main" flex={1} overflow="auto">
|
||||||
<div className={fullWidth ? '' : 'max-w-7xl mx-auto px-4 md:px-6 py-6'}>
|
<Container size="xl">
|
||||||
{children}
|
<Box paddingY={paddingMap[padding]}>
|
||||||
</div>
|
{children}
|
||||||
</main>
|
</Box>
|
||||||
|
</Container>
|
||||||
|
</Box>
|
||||||
);
|
);
|
||||||
}
|
};
|
||||||
|
|||||||
@@ -1,21 +1,32 @@
|
|||||||
import React from 'react';
|
import React, { ReactNode } from 'react';
|
||||||
|
import { Box } from './primitives/Box';
|
||||||
|
import { Surface } from './primitives/Surface';
|
||||||
|
|
||||||
interface ControlBarProps {
|
export interface ControlBarProps {
|
||||||
children: React.ReactNode;
|
children: ReactNode;
|
||||||
className?: string;
|
actions?: ReactNode;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
export const ControlBar = ({
|
||||||
* ControlBar is the top-level header of the "Telemetry Workspace".
|
children,
|
||||||
* It provides global controls, navigation, and status information.
|
actions
|
||||||
* Aligned with "Precision Racing Minimal" theme.
|
}: ControlBarProps) => {
|
||||||
*/
|
|
||||||
export function ControlBar({ children, className = '' }: ControlBarProps) {
|
|
||||||
return (
|
return (
|
||||||
<header
|
<Surface
|
||||||
className={`sticky top-0 z-50 h-16 md:h-20 bg-[#0C0D0F]/80 backdrop-blur-md border-b border-[#23272B] flex items-center px-4 md:px-6 ${className}`}
|
variant="muted"
|
||||||
|
padding={4}
|
||||||
|
style={{ borderBottom: '1px solid var(--ui-color-border-default)' }}
|
||||||
>
|
>
|
||||||
{children}
|
<Box display="flex" alignItems="center" justifyContent="between">
|
||||||
</header>
|
<Box display="flex" alignItems="center" gap={4}>
|
||||||
|
{children}
|
||||||
|
</Box>
|
||||||
|
{actions && (
|
||||||
|
<Box display="flex" alignItems="center" gap={4}>
|
||||||
|
{actions}
|
||||||
|
</Box>
|
||||||
|
)}
|
||||||
|
</Box>
|
||||||
|
</Surface>
|
||||||
);
|
);
|
||||||
}
|
};
|
||||||
|
|||||||
@@ -1,97 +1,36 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { Box } from './primitives/Box';
|
||||||
|
|
||||||
|
export interface CountryFlagProps {
|
||||||
|
|
||||||
// ISO 3166-1 alpha-2 country code to full country name mapping
|
|
||||||
const countryNames: Record<string, string> = {
|
|
||||||
'US': 'United States',
|
|
||||||
'GB': 'United Kingdom',
|
|
||||||
'CA': 'Canada',
|
|
||||||
'AU': 'Australia',
|
|
||||||
'NZ': 'New Zealand',
|
|
||||||
'DE': 'Germany',
|
|
||||||
'FR': 'France',
|
|
||||||
'IT': 'Italy',
|
|
||||||
'ES': 'Spain',
|
|
||||||
'NL': 'Netherlands',
|
|
||||||
'BE': 'Belgium',
|
|
||||||
'SE': 'Sweden',
|
|
||||||
'NO': 'Norway',
|
|
||||||
'DK': 'Denmark',
|
|
||||||
'FI': 'Finland',
|
|
||||||
'PL': 'Poland',
|
|
||||||
'CZ': 'Czech Republic',
|
|
||||||
'AT': 'Austria',
|
|
||||||
'CH': 'Switzerland',
|
|
||||||
'PT': 'Portugal',
|
|
||||||
'IE': 'Ireland',
|
|
||||||
'BR': 'Brazil',
|
|
||||||
'MX': 'Mexico',
|
|
||||||
'AR': 'Argentina',
|
|
||||||
'JP': 'Japan',
|
|
||||||
'CN': 'China',
|
|
||||||
'KR': 'South Korea',
|
|
||||||
'IN': 'India',
|
|
||||||
'SG': 'Singapore',
|
|
||||||
'TH': 'Thailand',
|
|
||||||
'MY': 'Malaysia',
|
|
||||||
'ID': 'Indonesia',
|
|
||||||
'PH': 'Philippines',
|
|
||||||
'ZA': 'South Africa',
|
|
||||||
'RU': 'Russia',
|
|
||||||
'MC': 'Monaco',
|
|
||||||
'TR': 'Turkey',
|
|
||||||
'GR': 'Greece',
|
|
||||||
'HU': 'Hungary',
|
|
||||||
'RO': 'Romania',
|
|
||||||
'BG': 'Bulgaria',
|
|
||||||
'HR': 'Croatia',
|
|
||||||
'SI': 'Slovenia',
|
|
||||||
'SK': 'Slovakia',
|
|
||||||
'LT': 'Lithuania',
|
|
||||||
'LV': 'Latvia',
|
|
||||||
'EE': 'Estonia',
|
|
||||||
};
|
|
||||||
|
|
||||||
// ISO 3166-1 alpha-2 country code to flag emoji conversion
|
|
||||||
const countryCodeToFlag = (countryCode: string): string => {
|
|
||||||
if (!countryCode || countryCode.length !== 2) return '🏁';
|
|
||||||
|
|
||||||
// Convert ISO 3166-1 alpha-2 to regional indicator symbols
|
|
||||||
const codePoints = countryCode
|
|
||||||
.toUpperCase()
|
|
||||||
.split('')
|
|
||||||
.map(char => 127397 + char.charCodeAt(0));
|
|
||||||
return String.fromCodePoint(...codePoints);
|
|
||||||
};
|
|
||||||
|
|
||||||
interface CountryFlagProps {
|
|
||||||
countryCode: string;
|
countryCode: string;
|
||||||
size?: 'sm' | 'md' | 'lg';
|
size?: 'sm' | 'md' | 'lg';
|
||||||
className?: string;
|
|
||||||
showTooltip?: boolean;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function CountryFlag({
|
export const CountryFlag = ({
|
||||||
countryCode,
|
countryCode,
|
||||||
size = 'md',
|
size = 'md'
|
||||||
className = '',
|
}: CountryFlagProps) => {
|
||||||
showTooltip = true
|
const sizeMap = {
|
||||||
}: CountryFlagProps) {
|
sm: '1rem',
|
||||||
const sizeClasses = {
|
md: '1.5rem',
|
||||||
sm: 'text-xs',
|
lg: '2rem',
|
||||||
md: 'text-sm',
|
|
||||||
lg: 'text-base'
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const flag = countryCodeToFlag(countryCode);
|
|
||||||
const countryName = countryNames[countryCode.toUpperCase()] || countryCode;
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<span
|
<Box
|
||||||
className={`inline-flex items-center relative ${sizeClasses[size]} ${className}`}
|
width={sizeMap[size]}
|
||||||
title={showTooltip ? countryName : undefined}
|
height={sizeMap[size]}
|
||||||
|
display="flex"
|
||||||
|
alignItems="center"
|
||||||
|
justifyContent="center"
|
||||||
|
overflow="hidden"
|
||||||
|
style={{ borderRadius: '2px' }}
|
||||||
>
|
>
|
||||||
<span className="select-none">{flag}</span>
|
<img
|
||||||
</span>
|
src={`https://flagcdn.com/w40/${countryCode.toLowerCase()}.png`}
|
||||||
|
alt={countryCode}
|
||||||
|
className="w-full h-full object-cover"
|
||||||
|
/>
|
||||||
|
</Box>
|
||||||
);
|
);
|
||||||
}
|
};
|
||||||
|
|||||||
@@ -1,26 +1,37 @@
|
|||||||
import React, { ReactNode } from 'react';
|
import React, { ReactNode } from 'react';
|
||||||
import { Box } from './primitives/Box';
|
import { Box } from './primitives/Box';
|
||||||
import { Text } from './Text';
|
|
||||||
import { Heading } from './Heading';
|
import { Heading } from './Heading';
|
||||||
import { Card } from './Card';
|
import { Text } from './Text';
|
||||||
|
import { Surface } from './primitives/Surface';
|
||||||
|
|
||||||
interface DangerZoneProps {
|
export interface DangerZoneProps {
|
||||||
title: string;
|
title: string;
|
||||||
description: string;
|
description: string;
|
||||||
children: ReactNode;
|
children: ReactNode;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function DangerZone({ title, description, children }: DangerZoneProps) {
|
export const DangerZone = ({
|
||||||
|
title,
|
||||||
|
description,
|
||||||
|
children
|
||||||
|
}: DangerZoneProps) => {
|
||||||
return (
|
return (
|
||||||
<Card>
|
<Box marginTop={8}>
|
||||||
<Heading level={3} mb={4}>Danger Zone</Heading>
|
<Heading level={3} marginBottom={4}>Danger Zone</Heading>
|
||||||
<Box p={4} rounded="lg" bg="bg-red-900/10" border={true} borderColor="border-red-900/30">
|
<Surface
|
||||||
<Text color="text-white" weight="medium" block mb={2}>{title}</Text>
|
variant="muted"
|
||||||
<Text size="sm" color="text-gray-400" block mb={4}>
|
rounded="lg"
|
||||||
{description}
|
padding={4}
|
||||||
</Text>
|
style={{ border: '1px solid var(--ui-color-intent-critical)', backgroundColor: 'rgba(227, 92, 92, 0.05)' }}
|
||||||
|
>
|
||||||
|
<Box marginBottom={4}>
|
||||||
|
<Text variant="high" weight="medium" block marginBottom={2}>{title}</Text>
|
||||||
|
<Text size="sm" variant="low" block>
|
||||||
|
{description}
|
||||||
|
</Text>
|
||||||
|
</Box>
|
||||||
{children}
|
{children}
|
||||||
</Box>
|
</Surface>
|
||||||
</Card>
|
</Box>
|
||||||
);
|
);
|
||||||
}
|
};
|
||||||
|
|||||||
@@ -1,30 +1,31 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { Calendar } from 'lucide-react';
|
|
||||||
import { Box } from './primitives/Box';
|
import { Box } from './primitives/Box';
|
||||||
import { Stack } from './primitives/Stack';
|
|
||||||
import { Text } from './Text';
|
import { Text } from './Text';
|
||||||
|
import { Stack } from './primitives/Stack';
|
||||||
|
import { Calendar } from 'lucide-react';
|
||||||
import { Icon } from './Icon';
|
import { Icon } from './Icon';
|
||||||
|
|
||||||
interface DateHeaderProps {
|
export interface DateHeaderProps {
|
||||||
label: string;
|
date: string;
|
||||||
count?: number;
|
showIcon?: boolean;
|
||||||
countLabel?: string;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function DateHeader({ label, count, countLabel = 'races' }: DateHeaderProps) {
|
export const DateHeader = ({
|
||||||
|
date,
|
||||||
|
showIcon = true
|
||||||
|
}: DateHeaderProps) => {
|
||||||
return (
|
return (
|
||||||
<Stack direction="row" align="center" gap={3} px={2}>
|
<Stack direction="row" align="center" gap={3} paddingX={2}>
|
||||||
<Box p={2} bg="bg-primary-blue/10" rounded="lg">
|
{showIcon && (
|
||||||
<Icon icon={Calendar} size={4} color="rgb(59, 130, 246)" />
|
<Box padding={2} bg="rgba(25, 140, 255, 0.1)" rounded="lg">
|
||||||
</Box>
|
<Icon icon={Calendar} size={4} intent="primary" />
|
||||||
<Text weight="semibold" size="sm" color="text-white">
|
</Box>
|
||||||
{label}
|
|
||||||
</Text>
|
|
||||||
{count !== undefined && (
|
|
||||||
<Text size="xs" color="text-gray-500">
|
|
||||||
{count} {count === 1 ? countLabel.replace(/s$/, '') : countLabel}
|
|
||||||
</Text>
|
|
||||||
)}
|
)}
|
||||||
|
<Box>
|
||||||
|
<Text weight="semibold" size="sm" variant="high">
|
||||||
|
{date}
|
||||||
|
</Text>
|
||||||
|
</Box>
|
||||||
</Stack>
|
</Stack>
|
||||||
);
|
);
|
||||||
}
|
};
|
||||||
|
|||||||
@@ -1,71 +1,73 @@
|
|||||||
|
import React, { ChangeEvent } from 'react';
|
||||||
|
import { Box } from './primitives/Box';
|
||||||
|
import { Text } from './Text';
|
||||||
|
import { Input } from './Input';
|
||||||
|
|
||||||
|
export interface DurationFieldProps {
|
||||||
import { Input } from '@/ui/Input';
|
|
||||||
|
|
||||||
interface DurationFieldProps {
|
|
||||||
label: string;
|
label: string;
|
||||||
value: number | '';
|
value: number; // in minutes
|
||||||
onChange: (value: number | '') => void;
|
onChange: (value: number) => void;
|
||||||
helperText?: string;
|
|
||||||
required?: boolean;
|
|
||||||
disabled?: boolean;
|
disabled?: boolean;
|
||||||
unit?: 'minutes' | 'laps';
|
|
||||||
error?: string;
|
error?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function DurationField({
|
export const DurationField = ({
|
||||||
label,
|
label,
|
||||||
value,
|
value,
|
||||||
onChange,
|
onChange,
|
||||||
helperText,
|
disabled = false,
|
||||||
required,
|
error
|
||||||
disabled,
|
}: DurationFieldProps) => {
|
||||||
unit = 'minutes',
|
const hours = Math.floor(value / 60);
|
||||||
error,
|
const minutes = value % 60;
|
||||||
}: DurationFieldProps) {
|
|
||||||
const handleChange = (raw: string) => {
|
|
||||||
if (raw.trim() === '') {
|
|
||||||
onChange('');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const parsed = parseInt(raw, 10);
|
const handleHoursChange = (e: ChangeEvent<HTMLInputElement>) => {
|
||||||
if (Number.isNaN(parsed) || parsed <= 0) {
|
const h = parseInt(e.target.value) || 0;
|
||||||
onChange('');
|
onChange(h * 60 + minutes);
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
onChange(parsed);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const unitLabel = unit === 'laps' ? 'laps' : 'min';
|
const handleMinutesChange = (e: ChangeEvent<HTMLInputElement>) => {
|
||||||
|
const m = parseInt(e.target.value) || 0;
|
||||||
|
onChange(hours * 60 + m);
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="space-y-1">
|
<Box>
|
||||||
<label className="block text-sm font-medium text-gray-300">
|
<Text as="label" size="xs" weight="bold" variant="low" block marginBottom={1.5}>
|
||||||
{label}
|
{label}
|
||||||
{required && <span className="text-warning-amber ml-1">*</span>}
|
</Text>
|
||||||
</label>
|
<Box display="flex" alignItems="center" gap={4}>
|
||||||
<div className="flex items-center gap-2">
|
<Box display="flex" alignItems="center" gap={2}>
|
||||||
<div className="flex-1">
|
|
||||||
<Input
|
<Input
|
||||||
type="number"
|
type="number"
|
||||||
value={value === '' ? '' : String(value)}
|
value={hours}
|
||||||
onChange={(e) => handleChange(e.target.value)}
|
onChange={handleHoursChange}
|
||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
min={1}
|
min={0}
|
||||||
className="pr-16"
|
style={{ width: '4rem' }}
|
||||||
variant={error ? 'error' : 'default'}
|
|
||||||
/>
|
/>
|
||||||
</div>
|
<Text size="sm" variant="low">h</Text>
|
||||||
<span className="text-xs text-gray-400 -ml-14">{unitLabel}</span>
|
</Box>
|
||||||
</div>
|
<Box display="flex" alignItems="center" gap={2}>
|
||||||
{helperText && (
|
<Input
|
||||||
<p className="text-xs text-gray-500">{helperText}</p>
|
type="number"
|
||||||
)}
|
value={minutes}
|
||||||
|
onChange={handleMinutesChange}
|
||||||
|
disabled={disabled}
|
||||||
|
min={0}
|
||||||
|
max={59}
|
||||||
|
style={{ width: '4rem' }}
|
||||||
|
/>
|
||||||
|
<Text size="sm" variant="low">m</Text>
|
||||||
|
</Box>
|
||||||
|
</Box>
|
||||||
{error && (
|
{error && (
|
||||||
<p className="text-xs text-warning-amber mt-1">{error}</p>
|
<Box marginTop={1}>
|
||||||
|
<Text size="xs" variant="critical">
|
||||||
|
{error}
|
||||||
|
</Text>
|
||||||
|
</Box>
|
||||||
)}
|
)}
|
||||||
</div>
|
</Box>
|
||||||
);
|
);
|
||||||
}
|
};
|
||||||
|
|||||||
@@ -1,55 +1,29 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
import { Box } from './primitives/Box';
|
||||||
|
import { Button } from './Button';
|
||||||
|
import { RefreshCw, Home } from 'lucide-react';
|
||||||
|
|
||||||
interface ErrorActionButtonsProps {
|
export interface ErrorActionButtonsProps {
|
||||||
onRetry?: () => void;
|
onRetry?: () => void;
|
||||||
onHomeClick: () => void;
|
onGoHome?: () => void;
|
||||||
showRetry?: boolean;
|
|
||||||
homeLabel?: string;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
export const ErrorActionButtons = ({
|
||||||
* ErrorActionButtons
|
onRetry,
|
||||||
*
|
onGoHome
|
||||||
* Action buttons for error pages (Try Again, Go Home)
|
}: ErrorActionButtonsProps) => {
|
||||||
* Provides consistent styling and behavior.
|
|
||||||
* All navigation callbacks must be provided by the caller.
|
|
||||||
*/
|
|
||||||
export function ErrorActionButtons({
|
|
||||||
onRetry,
|
|
||||||
onHomeClick,
|
|
||||||
showRetry = false,
|
|
||||||
homeLabel = 'Drive home',
|
|
||||||
}: ErrorActionButtonsProps) {
|
|
||||||
if (showRetry && onRetry) {
|
|
||||||
return (
|
|
||||||
<div className="flex items-center justify-center gap-3 pt-2">
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
onClick={onRetry}
|
|
||||||
className="inline-flex items-center justify-center rounded-md bg-primary-blue px-4 py-2 text-sm font-medium text-white hover:bg-primary-blue/80 transition-colors"
|
|
||||||
>
|
|
||||||
Try again
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
onClick={onHomeClick}
|
|
||||||
className="inline-flex items-center justify-center rounded-md bg-iron-gray px-4 py-2 text-sm font-medium text-white hover:bg-iron-gray/80 transition-colors"
|
|
||||||
>
|
|
||||||
Go home
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="pt-2">
|
<Box display="flex" alignItems="center" gap={4}>
|
||||||
<button
|
{onRetry && (
|
||||||
type="button"
|
<Button variant="primary" onClick={onRetry} icon={<RefreshCw size={16} />}>
|
||||||
onClick={onHomeClick}
|
Retry
|
||||||
className="inline-flex items-center justify-center rounded-md bg-primary-blue px-4 py-2 text-sm font-medium text-white hover:bg-primary-blue/80 transition-colors"
|
</Button>
|
||||||
>
|
)}
|
||||||
{homeLabel}
|
{onGoHome && (
|
||||||
</button>
|
<Button variant="secondary" onClick={onGoHome} icon={<Home size={16} />}>
|
||||||
</div>
|
Go Home
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
</Box>
|
||||||
);
|
);
|
||||||
}
|
};
|
||||||
|
|||||||
@@ -1,63 +1,45 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { Box } from './primitives/Box';
|
import { Box } from './primitives/Box';
|
||||||
import { Stack } from './primitives/Stack';
|
|
||||||
import { Text } from './Text';
|
import { Text } from './Text';
|
||||||
import { Surface } from './primitives/Surface';
|
|
||||||
import { Icon } from './Icon';
|
import { Icon } from './Icon';
|
||||||
import { AlertCircle, XCircle, Info, AlertTriangle } from 'lucide-react';
|
import { AlertTriangle } from 'lucide-react';
|
||||||
|
import { Surface } from './primitives/Surface';
|
||||||
|
|
||||||
interface ErrorBannerProps {
|
export interface ErrorBannerProps {
|
||||||
title?: string;
|
title?: string;
|
||||||
message: string;
|
message: string;
|
||||||
variant?: 'error' | 'warning' | 'info' | 'success';
|
variant?: 'error' | 'warning' | 'info';
|
||||||
}
|
}
|
||||||
|
|
||||||
export function ErrorBanner({ title, message, variant = 'error' }: ErrorBannerProps) {
|
export const ErrorBanner = ({
|
||||||
const configs = {
|
title,
|
||||||
error: {
|
message,
|
||||||
bg: 'rgba(239, 68, 68, 0.1)',
|
variant = 'error'
|
||||||
border: 'rgba(239, 68, 68, 0.2)',
|
}: ErrorBannerProps) => {
|
||||||
text: 'rgb(248, 113, 113)',
|
const intent = variant === 'error' ? 'critical' : variant === 'warning' ? 'warning' : 'primary';
|
||||||
icon: XCircle
|
const color = variant === 'error' ? 'rgba(227, 92, 92, 0.05)' : variant === 'warning' ? 'rgba(255, 190, 77, 0.05)' : 'rgba(25, 140, 255, 0.05)';
|
||||||
},
|
const borderColor = variant === 'error' ? 'rgba(227, 92, 92, 0.2)' : variant === 'warning' ? 'rgba(255, 190, 77, 0.2)' : 'rgba(25, 140, 255, 0.2)';
|
||||||
warning: {
|
|
||||||
bg: 'rgba(245, 158, 11, 0.1)',
|
|
||||||
border: 'rgba(245, 158, 11, 0.2)',
|
|
||||||
text: 'rgb(251, 191, 36)',
|
|
||||||
icon: AlertTriangle
|
|
||||||
},
|
|
||||||
info: {
|
|
||||||
bg: 'rgba(59, 130, 246, 0.1)',
|
|
||||||
border: 'rgba(59, 130, 246, 0.2)',
|
|
||||||
text: 'rgb(96, 165, 250)',
|
|
||||||
icon: Info
|
|
||||||
},
|
|
||||||
success: {
|
|
||||||
bg: 'rgba(16, 185, 129, 0.1)',
|
|
||||||
border: 'rgba(16, 185, 129, 0.2)',
|
|
||||||
text: 'rgb(52, 211, 153)',
|
|
||||||
icon: AlertCircle
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const colors = configs[variant];
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Surface
|
<Surface
|
||||||
variant="muted"
|
variant="muted"
|
||||||
rounded="xl"
|
rounded="lg"
|
||||||
border
|
padding={4}
|
||||||
p={4}
|
style={{ backgroundColor: color, border: `1px solid ${borderColor}` }}
|
||||||
backgroundColor={colors.bg}
|
|
||||||
borderColor={colors.border}
|
|
||||||
>
|
>
|
||||||
<Stack direction="row" align="start" gap={3}>
|
<Box display="flex" alignItems="start" gap={4}>
|
||||||
<Icon icon={colors.icon} size={5} color={colors.text} />
|
<Icon icon={AlertTriangle} size={5} intent={intent} />
|
||||||
<Box flex={1}>
|
<Box>
|
||||||
{title && <Text weight="medium" color={colors.text} block mb={1}>{title}</Text>}
|
{title && (
|
||||||
<Text size="sm" color={colors.text} opacity={0.9} block>{message}</Text>
|
<Text weight="medium" variant="high" block marginBottom={1}>
|
||||||
|
{title}
|
||||||
|
</Text>
|
||||||
|
)}
|
||||||
|
<Text size="sm" variant="low">
|
||||||
|
{message}
|
||||||
|
</Text>
|
||||||
</Box>
|
</Box>
|
||||||
</Stack>
|
</Box>
|
||||||
</Surface>
|
</Surface>
|
||||||
);
|
);
|
||||||
}
|
};
|
||||||
|
|||||||
@@ -1,29 +1,24 @@
|
|||||||
import React, { ReactNode } from 'react';
|
import React, { ReactNode } from 'react';
|
||||||
|
import { Box } from './primitives/Box';
|
||||||
|
import { Surface } from './primitives/Surface';
|
||||||
|
|
||||||
interface ErrorPageContainerProps {
|
export interface ErrorPageContainerProps {
|
||||||
children: ReactNode;
|
children: ReactNode;
|
||||||
errorCode: string;
|
|
||||||
description: string;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
export const ErrorPageContainer = ({ children }: ErrorPageContainerProps) => {
|
||||||
* ErrorPageContainer
|
|
||||||
*
|
|
||||||
* A reusable container for error pages (404, 500, etc.)
|
|
||||||
* Provides consistent styling and layout for error states.
|
|
||||||
*/
|
|
||||||
export function ErrorPageContainer({
|
|
||||||
children,
|
|
||||||
errorCode,
|
|
||||||
description,
|
|
||||||
}: ErrorPageContainerProps) {
|
|
||||||
return (
|
return (
|
||||||
<main className="min-h-screen flex items-center justify-center bg-deep-graphite text-white px-6">
|
<Box
|
||||||
<div className="max-w-md text-center space-y-4">
|
minHeight="100vh"
|
||||||
<h1 className="text-3xl font-semibold">{errorCode}</h1>
|
display="flex"
|
||||||
<p className="text-sm text-gray-400">{description}</p>
|
alignItems="center"
|
||||||
|
justifyContent="center"
|
||||||
|
padding={4}
|
||||||
|
bg="var(--ui-color-bg-base)"
|
||||||
|
>
|
||||||
|
<Surface variant="default" rounded="xl" padding={8} style={{ maxWidth: '32rem', width: '100%', border: '1px solid var(--ui-color-border-default)' }}>
|
||||||
{children}
|
{children}
|
||||||
</div>
|
</Surface>
|
||||||
</main>
|
</Box>
|
||||||
);
|
);
|
||||||
}
|
};
|
||||||
|
|||||||
@@ -1,34 +1,29 @@
|
|||||||
import { Button } from '@/ui/Button';
|
import React from 'react';
|
||||||
import { Card } from '@/ui/Card';
|
import { Box } from './primitives/Box';
|
||||||
import { Heading } from '@/ui/Heading';
|
import { Text } from './Text';
|
||||||
import { Box } from '@/ui/primitives/Box';
|
import { Icon } from './Icon';
|
||||||
import { Text } from '@/ui/Text';
|
import { MessageSquare } from 'lucide-react';
|
||||||
|
|
||||||
export function FeedEmptyState() {
|
export interface FeedEmptyStateProps {
|
||||||
return (
|
message?: string;
|
||||||
<Card bg="bg-iron-gray/80" border={true} borderColor="border-charcoal-outline" className="border-dashed">
|
|
||||||
<Box textAlign="center" py={10}>
|
|
||||||
<Text size="3xl" block mb={3}>🏁</Text>
|
|
||||||
<Box mb={2}>
|
|
||||||
<Heading level={3}>
|
|
||||||
Your feed is warming up
|
|
||||||
</Heading>
|
|
||||||
</Box>
|
|
||||||
<Box maxWidth="md" mx="auto" mb={4}>
|
|
||||||
<Text size="sm" color="text-gray-400">
|
|
||||||
As leagues, teams, and friends start racing, this feed will show their latest results,
|
|
||||||
signups, and highlights.
|
|
||||||
</Text>
|
|
||||||
</Box>
|
|
||||||
<Button
|
|
||||||
as="a"
|
|
||||||
href="/leagues"
|
|
||||||
variant="secondary"
|
|
||||||
size="sm"
|
|
||||||
>
|
|
||||||
Explore leagues
|
|
||||||
</Button>
|
|
||||||
</Box>
|
|
||||||
</Card>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const FeedEmptyState = ({
|
||||||
|
message = 'No activity yet.'
|
||||||
|
}: FeedEmptyStateProps) => {
|
||||||
|
return (
|
||||||
|
<Box
|
||||||
|
display="flex"
|
||||||
|
flexDirection="col"
|
||||||
|
alignItems="center"
|
||||||
|
justifyContent="center"
|
||||||
|
paddingY={12}
|
||||||
|
textAlign="center"
|
||||||
|
>
|
||||||
|
<Box padding={4} rounded="full" bg="var(--ui-color-bg-surface-muted)" marginBottom={4}>
|
||||||
|
<Icon icon={MessageSquare} size={8} intent="low" />
|
||||||
|
</Box>
|
||||||
|
<Text variant="low">{message}</Text>
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|||||||
@@ -1,77 +1,46 @@
|
|||||||
import React, { ReactNode } from 'react';
|
import React, { ReactNode } from 'react';
|
||||||
import { Box } from './primitives/Box';
|
import { Box } from './primitives/Box';
|
||||||
import { Text } from './Text';
|
import { Text } from './Text';
|
||||||
import { Image } from './Image';
|
import { Surface } from './primitives/Surface';
|
||||||
|
import { Avatar } from './Avatar';
|
||||||
|
|
||||||
interface FeedItemProps {
|
export interface FeedItemProps {
|
||||||
actorName?: string;
|
user: {
|
||||||
actorAvatarUrl?: string;
|
name: string;
|
||||||
typeLabel: string;
|
avatar?: string;
|
||||||
headline: string;
|
};
|
||||||
body?: string;
|
content: ReactNode;
|
||||||
timeAgo: string;
|
timestamp: string;
|
||||||
cta?: ReactNode;
|
actions?: ReactNode;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function FeedItem({
|
export const FeedItem = ({
|
||||||
actorName,
|
user,
|
||||||
actorAvatarUrl,
|
content,
|
||||||
typeLabel,
|
timestamp,
|
||||||
headline,
|
actions
|
||||||
body,
|
}: FeedItemProps) => {
|
||||||
timeAgo,
|
|
||||||
cta,
|
|
||||||
}: FeedItemProps) {
|
|
||||||
return (
|
return (
|
||||||
<Box display="flex" gap={4}>
|
<Surface variant="default" rounded="lg" padding={4} style={{ border: '1px solid var(--ui-color-border-default)' }}>
|
||||||
<Box flexShrink={0}>
|
<Box display="flex" alignItems="start" gap={4}>
|
||||||
{actorAvatarUrl ? (
|
<Avatar src={user.avatar} alt={user.name} size="md" />
|
||||||
<Box width="10" height="10" rounded="full" overflow="hidden" bg="bg-charcoal-outline">
|
<Box flex={1}>
|
||||||
<Image
|
<Box display="flex" alignItems="center" justifyContent="between" marginBottom={2}>
|
||||||
src={actorAvatarUrl}
|
<Text weight="bold" variant="high">{user.name}</Text>
|
||||||
alt={actorName || ''}
|
<Text size="xs" variant="low">{timestamp}</Text>
|
||||||
width={40}
|
|
||||||
height={40}
|
|
||||||
fullWidth
|
|
||||||
fullHeight
|
|
||||||
objectFit="cover"
|
|
||||||
/>
|
|
||||||
</Box>
|
</Box>
|
||||||
) : (
|
<Box marginBottom={4}>
|
||||||
<Box
|
{typeof content === 'string' ? (
|
||||||
width="10"
|
<Text variant="med">{content}</Text>
|
||||||
height="10"
|
) : content}
|
||||||
display="flex"
|
|
||||||
center
|
|
||||||
rounded="full"
|
|
||||||
bg="bg-primary-blue/10"
|
|
||||||
border={true}
|
|
||||||
borderColor="border-primary-blue/40"
|
|
||||||
>
|
|
||||||
<Text size="xs" color="text-primary-blue" weight="semibold">
|
|
||||||
{typeLabel}
|
|
||||||
</Text>
|
|
||||||
</Box>
|
</Box>
|
||||||
)}
|
{actions && (
|
||||||
</Box>
|
<Box display="flex" alignItems="center" gap={4} borderTop paddingTop={4}>
|
||||||
<Box flexGrow={1} minWidth="0">
|
{actions}
|
||||||
<Box display="flex" alignItems="start" justifyContent="between" gap={2}>
|
</Box>
|
||||||
<Box>
|
)}
|
||||||
<Text size="sm" color="text-white" block>{headline}</Text>
|
|
||||||
{body && (
|
|
||||||
<Text size="xs" color="text-gray-400" block mt={1}>{body}</Text>
|
|
||||||
)}
|
|
||||||
</Box>
|
|
||||||
<Text size="xs" color="text-gray-500" className="whitespace-nowrap" style={{ fontSize: '11px' }}>
|
|
||||||
{timeAgo}
|
|
||||||
</Text>
|
|
||||||
</Box>
|
</Box>
|
||||||
{cta && (
|
|
||||||
<Box mt={3}>
|
|
||||||
{cta}
|
|
||||||
</Box>
|
|
||||||
)}
|
|
||||||
</Box>
|
</Box>
|
||||||
</Box>
|
</Surface>
|
||||||
);
|
);
|
||||||
}
|
};
|
||||||
|
|||||||
@@ -1,46 +1,61 @@
|
|||||||
|
import React from 'react';
|
||||||
|
|
||||||
import { Box } from './primitives/Box';
|
import { Box } from './primitives/Box';
|
||||||
import { Button } from './Button';
|
import { Button } from './Button';
|
||||||
import { Stack } from './primitives/Stack';
|
import { Text } from './Text';
|
||||||
|
import { Surface } from './primitives/Surface';
|
||||||
|
|
||||||
interface FilterOption {
|
export interface FilterOption {
|
||||||
id: string;
|
id: string;
|
||||||
label: string;
|
label: string;
|
||||||
indicatorColor?: string;
|
indicatorColor?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface FilterGroupProps {
|
export interface FilterGroupProps {
|
||||||
options: FilterOption[];
|
options: FilterOption[];
|
||||||
activeId: string;
|
activeId: string;
|
||||||
onSelect: (id: string) => void;
|
onChange: (id: string) => void;
|
||||||
|
label?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function FilterGroup({ options, activeId, onSelect }: FilterGroupProps) {
|
export const FilterGroup = ({
|
||||||
|
options,
|
||||||
|
activeId,
|
||||||
|
onChange,
|
||||||
|
label
|
||||||
|
}: FilterGroupProps) => {
|
||||||
return (
|
return (
|
||||||
<Stack direction="row" align="center" gap={1} bg="bg-deep-graphite" p={1} rounded="lg">
|
<Box display="flex" flexDirection="col" gap={2}>
|
||||||
{options.map((option) => (
|
{label && (
|
||||||
<Button
|
<Text size="xs" weight="bold" variant="low" uppercase letterSpacing="wider">
|
||||||
key={option.id}
|
{label}
|
||||||
variant={activeId === option.id ? 'primary' : 'ghost'}
|
</Text>
|
||||||
onClick={() => onSelect(option.id)}
|
)}
|
||||||
size="sm"
|
<Surface variant="muted" rounded="lg" padding={1} display="flex" gap={1}>
|
||||||
px={4}
|
{options.map((option) => {
|
||||||
>
|
const isActive = option.id === activeId;
|
||||||
{option.indicatorColor && (
|
return (
|
||||||
<Box
|
<Button
|
||||||
as="span"
|
key={option.id}
|
||||||
w="2"
|
variant={isActive ? 'primary' : 'ghost'}
|
||||||
h="2"
|
size="sm"
|
||||||
bg={option.indicatorColor}
|
onClick={() => onChange(option.id)}
|
||||||
rounded="full"
|
fullWidth
|
||||||
mr={2}
|
>
|
||||||
animate={option.indicatorColor.includes('green') ? 'pulse' : 'none'}
|
<Box display="flex" alignItems="center" gap={2}>
|
||||||
/>
|
{option.indicatorColor && (
|
||||||
)}
|
<Box
|
||||||
{option.label}
|
width="0.5rem"
|
||||||
</Button>
|
height="0.5rem"
|
||||||
))}
|
rounded="full"
|
||||||
</Stack>
|
style={{ backgroundColor: option.indicatorColor }}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
{option.label}
|
||||||
|
</Box>
|
||||||
|
</Button>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</Surface>
|
||||||
|
</Box>
|
||||||
);
|
);
|
||||||
}
|
};
|
||||||
|
|||||||
@@ -1,81 +1,54 @@
|
|||||||
import { Link } from '@/ui/Link';
|
import React from 'react';
|
||||||
import { Box } from '@/ui/primitives/Box';
|
import { Box } from './primitives/Box';
|
||||||
import { Text } from '@/ui/Text';
|
import { Container } from './Container';
|
||||||
|
import { Text } from './Text';
|
||||||
|
import { Link } from './Link';
|
||||||
|
|
||||||
const discordUrl = process.env.NEXT_PUBLIC_DISCORD_URL || 'https://discord.gg/gridpilot';
|
export const Footer = () => {
|
||||||
const xUrl = process.env.NEXT_PUBLIC_X_URL || '#';
|
|
||||||
|
|
||||||
export function Footer() {
|
|
||||||
return (
|
return (
|
||||||
<Box as="footer" position="relative" bg="graphite-black" borderTop borderColor="border-gray/50">
|
<Box as="footer" bg="var(--ui-color-bg-surface)" borderTop paddingY={12}>
|
||||||
<Box position="absolute" top="0" left="0" right="0" h="px" bg="linear-gradient(to right, transparent, #198CFF, transparent)" opacity={0.3} />
|
<Container size="xl">
|
||||||
|
<Box display="grid" gridCols={{ base: 1, md: 4 }} gap={12}>
|
||||||
<Box maxWidth="7xl" mx="auto" px={{ base: 6, lg: 8 }} py={{ base: 12, md: 16 }}>
|
<Box>
|
||||||
{/* Racing stripe accent */}
|
<Text weight="bold" variant="high" marginBottom={4}>GridPilot</Text>
|
||||||
<Box
|
<Text size="sm" variant="low">
|
||||||
display="flex"
|
The ultimate companion for sim racers. Track your performance, manage your team, and compete in leagues.
|
||||||
gap={2}
|
</Text>
|
||||||
mb={8}
|
</Box>
|
||||||
justifyContent="center"
|
|
||||||
>
|
<Box>
|
||||||
<Box w="12" h="1" bg="white" opacity={0.1} />
|
<Text weight="bold" variant="high" marginBottom={4}>Platform</Text>
|
||||||
<Box w="12" h="1" bg="primary-accent" />
|
<Box display="flex" flexDirection="col" gap={2}>
|
||||||
<Box w="12" h="1" bg="white" opacity={0.1} />
|
<Link href="/leagues" variant="secondary">Leagues</Link>
|
||||||
|
<Link href="/teams" variant="secondary">Teams</Link>
|
||||||
|
<Link href="/leaderboards" variant="secondary">Leaderboards</Link>
|
||||||
|
</Box>
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
<Box>
|
||||||
|
<Text weight="bold" variant="high" marginBottom={4}>Support</Text>
|
||||||
|
<Box display="flex" flexDirection="col" gap={2}>
|
||||||
|
<Link href="/docs" variant="secondary">Documentation</Link>
|
||||||
|
<Link href="/status" variant="secondary">System Status</Link>
|
||||||
|
<Link href="/contact" variant="secondary">Contact Us</Link>
|
||||||
|
</Box>
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
<Box>
|
||||||
|
<Text weight="bold" variant="high" marginBottom={4}>Legal</Text>
|
||||||
|
<Box display="flex" flexDirection="col" gap={2}>
|
||||||
|
<Link href="/privacy" variant="secondary">Privacy Policy</Link>
|
||||||
|
<Link href="/terms" variant="secondary">Terms of Service</Link>
|
||||||
|
</Box>
|
||||||
|
</Box>
|
||||||
</Box>
|
</Box>
|
||||||
|
|
||||||
{/* Personal message */}
|
<Box borderTop marginTop={12} paddingTop={8} textAlign="center">
|
||||||
<Box
|
<Text size="xs" variant="low">
|
||||||
textAlign="center"
|
© {new Date().getFullYear()} GridPilot. All rights reserved.
|
||||||
mb={12}
|
|
||||||
>
|
|
||||||
<Text size="sm" color="text-gray-300" block mb={2} weight="bold" className="tracking-wide">
|
|
||||||
🏁 Built by a sim racer, for sim racers
|
|
||||||
</Text>
|
|
||||||
<Text size="xs" color="text-gray-500" weight="normal" maxWidth="2xl" mx="auto" block leading="relaxed">
|
|
||||||
Just a fellow racer tired of spreadsheets and chaos. GridPilot is my passion project to make league racing actually fun again.
|
|
||||||
</Text>
|
</Text>
|
||||||
</Box>
|
</Box>
|
||||||
|
</Container>
|
||||||
{/* Community links */}
|
|
||||||
<Box
|
|
||||||
display="flex"
|
|
||||||
justifyContent="center"
|
|
||||||
gap={8}
|
|
||||||
mb={12}
|
|
||||||
>
|
|
||||||
<Link
|
|
||||||
href={discordUrl}
|
|
||||||
variant="ghost"
|
|
||||||
size="sm"
|
|
||||||
className="text-gray-400 hover:text-primary-accent transition-colors font-bold uppercase tracking-widest"
|
|
||||||
>
|
|
||||||
💬 Discord
|
|
||||||
</Link>
|
|
||||||
<Link
|
|
||||||
href={xUrl}
|
|
||||||
variant="ghost"
|
|
||||||
size="sm"
|
|
||||||
className="text-gray-400 hover:text-primary-accent transition-colors font-bold uppercase tracking-widest"
|
|
||||||
>
|
|
||||||
𝕏 Twitter
|
|
||||||
</Link>
|
|
||||||
</Box>
|
|
||||||
|
|
||||||
{/* Development status */}
|
|
||||||
<Box
|
|
||||||
textAlign="center"
|
|
||||||
pt={8}
|
|
||||||
borderTop
|
|
||||||
borderColor="border-gray/30"
|
|
||||||
>
|
|
||||||
<Text size="xs" color="text-gray-600" block mb={1} font="mono" uppercase letterSpacing="widest">
|
|
||||||
⚡ Early development • Feedback welcome
|
|
||||||
</Text>
|
|
||||||
<Text size="xs" color="text-gray-700" block font="mono">
|
|
||||||
© {new Date().getFullYear()} GridPilot
|
|
||||||
</Text>
|
|
||||||
</Box>
|
|
||||||
</Box>
|
|
||||||
</Box>
|
</Box>
|
||||||
);
|
);
|
||||||
}
|
};
|
||||||
|
|||||||
@@ -1,45 +1,55 @@
|
|||||||
|
import React, { ReactNode } from 'react';
|
||||||
|
import { Box } from './primitives/Box';
|
||||||
import React from 'react';
|
|
||||||
import { Icon } from './Icon';
|
|
||||||
import { Stack } from './primitives/Stack';
|
|
||||||
import { Text } from './Text';
|
import { Text } from './Text';
|
||||||
|
import { Icon } from './Icon';
|
||||||
import { LucideIcon } from 'lucide-react';
|
import { LucideIcon } from 'lucide-react';
|
||||||
|
|
||||||
interface FormFieldProps {
|
export interface FormFieldProps {
|
||||||
label: string;
|
label?: string;
|
||||||
icon?: LucideIcon;
|
|
||||||
children: React.ReactNode;
|
|
||||||
required?: boolean;
|
|
||||||
error?: string;
|
error?: string;
|
||||||
hint?: string;
|
hint?: string;
|
||||||
|
required?: boolean;
|
||||||
|
icon?: LucideIcon;
|
||||||
|
children: ReactNode;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function FormField({
|
export const FormField = ({
|
||||||
label,
|
label,
|
||||||
|
error,
|
||||||
|
hint,
|
||||||
|
required,
|
||||||
icon,
|
icon,
|
||||||
children,
|
children
|
||||||
required = false,
|
}: FormFieldProps) => {
|
||||||
error,
|
|
||||||
hint,
|
|
||||||
}: FormFieldProps) {
|
|
||||||
return (
|
return (
|
||||||
<Stack gap={2}>
|
<Box marginBottom={4}>
|
||||||
<label className="block text-sm font-medium text-gray-300">
|
{label && (
|
||||||
<Stack direction="row" align="center" gap={2}>
|
<Box display="flex" alignItems="center" gap={2} marginBottom={1.5}>
|
||||||
{icon && <Icon icon={icon} size={4} color="#6b7280" />}
|
{icon && <Icon icon={icon} size={4} intent="low" />}
|
||||||
<Text size="sm" weight="medium" color="text-gray-300">{label}</Text>
|
<Text size="sm" weight="medium" variant="high">
|
||||||
{required && <Text color="text-error-red">*</Text>}
|
{label}
|
||||||
</Stack>
|
</Text>
|
||||||
</label>
|
{required && <Text variant="critical">*</Text>}
|
||||||
|
</Box>
|
||||||
|
)}
|
||||||
|
|
||||||
{children}
|
{children}
|
||||||
|
|
||||||
{error && (
|
{error && (
|
||||||
<Text size="xs" color="text-error-red" block mt={1}>{error}</Text>
|
<Box marginTop={1}>
|
||||||
|
<Text size="xs" variant="critical" block>
|
||||||
|
{error}
|
||||||
|
</Text>
|
||||||
|
</Box>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{hint && !error && (
|
{hint && !error && (
|
||||||
<Text size="xs" color="text-gray-500" block mt={1}>{hint}</Text>
|
<Box marginTop={1}>
|
||||||
|
<Text size="xs" variant="low" block>
|
||||||
|
{hint}
|
||||||
|
</Text>
|
||||||
|
</Box>
|
||||||
)}
|
)}
|
||||||
</Stack>
|
</Box>
|
||||||
);
|
);
|
||||||
}
|
};
|
||||||
|
|||||||
@@ -1,37 +1,33 @@
|
|||||||
import React, { ReactNode } from 'react';
|
import React, { ReactNode } from 'react';
|
||||||
import { Stack } from './primitives/Stack';
|
import { Box } from './primitives/Box';
|
||||||
import { Text } from './Text';
|
import { Text } from './Text';
|
||||||
|
|
||||||
interface FormSectionProps {
|
export interface FormSectionProps {
|
||||||
|
title: string;
|
||||||
|
description?: string;
|
||||||
children: ReactNode;
|
children: ReactNode;
|
||||||
title?: string;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
export const FormSection = ({
|
||||||
* FormSection
|
title,
|
||||||
*
|
description,
|
||||||
* Groups related form fields with an optional title.
|
children
|
||||||
*/
|
}: FormSectionProps) => {
|
||||||
export function FormSection({ children, title }: FormSectionProps) {
|
|
||||||
return (
|
return (
|
||||||
<Stack gap={4} fullWidth>
|
<Box display="flex" flexDirection="col" gap={6}>
|
||||||
{title && (
|
<Box borderBottom paddingBottom={4}>
|
||||||
<Text
|
<Text weight="bold" variant="high" size="lg" marginBottom={1} block>
|
||||||
size="xs"
|
|
||||||
weight="bold"
|
|
||||||
color="text-gray-500"
|
|
||||||
uppercase
|
|
||||||
letterSpacing="widest"
|
|
||||||
borderBottom
|
|
||||||
borderColor="border-border-gray"
|
|
||||||
pb={1}
|
|
||||||
>
|
|
||||||
{title}
|
{title}
|
||||||
</Text>
|
</Text>
|
||||||
)}
|
{description && (
|
||||||
<Stack gap={4} fullWidth>
|
<Text size="sm" variant="low">
|
||||||
|
{description}
|
||||||
|
</Text>
|
||||||
|
)}
|
||||||
|
</Box>
|
||||||
|
<Box display="flex" flexDirection="col" gap={4}>
|
||||||
{children}
|
{children}
|
||||||
</Stack>
|
</Box>
|
||||||
</Stack>
|
</Box>
|
||||||
);
|
);
|
||||||
}
|
};
|
||||||
|
|||||||
@@ -1,41 +1,48 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { Box } from './primitives/Box';
|
|
||||||
import { Stack } from './primitives/Stack';
|
|
||||||
import { Text } from './Text';
|
|
||||||
import { Heading } from './Heading';
|
|
||||||
import { Card } from './Card';
|
import { Card } from './Card';
|
||||||
import { ProgressBar } from './ProgressBar';
|
import { Box } from './primitives/Box';
|
||||||
|
import { Text } from './Text';
|
||||||
|
|
||||||
interface GoalCardProps {
|
export interface GoalCardProps {
|
||||||
title: string;
|
title: string;
|
||||||
|
current: number;
|
||||||
|
target: number;
|
||||||
|
unit: string;
|
||||||
icon: string;
|
icon: string;
|
||||||
goalLabel: string;
|
|
||||||
currentValue: number;
|
|
||||||
maxValue: number;
|
|
||||||
color?: string;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function GoalCard({
|
export const GoalCard = ({
|
||||||
title,
|
title,
|
||||||
icon,
|
current,
|
||||||
goalLabel,
|
target,
|
||||||
currentValue,
|
unit,
|
||||||
maxValue,
|
icon
|
||||||
color = 'text-primary-blue',
|
}: GoalCardProps) => {
|
||||||
}: GoalCardProps) {
|
const percentage = Math.min(Math.max((current / target) * 100, 0), 100);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Card bg="bg-charcoal-200/50" borderColor="border-primary-blue/30">
|
<Card variant="default">
|
||||||
<Box display="flex" alignItems="center" gap={3} mb={3}>
|
<Box display="flex" alignItems="center" gap={3} marginBottom={3}>
|
||||||
<Text size="2xl">{icon}</Text>
|
<Text size="2xl">{icon}</Text>
|
||||||
<Heading level={3}>{title}</Heading>
|
<Box>
|
||||||
</Box>
|
<Text weight="bold" variant="high">{title}</Text>
|
||||||
<Stack gap={2}>
|
<Text size="sm" variant="low">{unit}</Text>
|
||||||
<Box display="flex" alignItems="center" justifyContent="between">
|
|
||||||
<Text size="sm" color="text-gray-400">{goalLabel}</Text>
|
|
||||||
<Text size="sm" className={color}>{currentValue}/{maxValue}</Text>
|
|
||||||
</Box>
|
</Box>
|
||||||
<ProgressBar value={currentValue} max={maxValue} />
|
</Box>
|
||||||
</Stack>
|
|
||||||
|
<Box marginBottom={2}>
|
||||||
|
<Box display="flex" alignItems="center" justifyContent="between" marginBottom={1}>
|
||||||
|
<Text size="xs" variant="low">Progress</Text>
|
||||||
|
<Text size="xs" weight="bold" variant="high">{Math.round(percentage)}%</Text>
|
||||||
|
</Box>
|
||||||
|
<Box fullWidth height="0.5rem" bg="var(--ui-color-bg-surface-muted)" style={{ borderRadius: '9999px', overflow: 'hidden' }}>
|
||||||
|
<Box fullHeight bg="var(--ui-color-intent-primary)" style={{ width: `${percentage}%` }} />
|
||||||
|
</Box>
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
<Text size="xs" variant="low">
|
||||||
|
{current} / {target} {unit}
|
||||||
|
</Text>
|
||||||
</Card>
|
</Card>
|
||||||
);
|
);
|
||||||
}
|
};
|
||||||
|
|||||||
@@ -1,16 +1,38 @@
|
|||||||
import React from 'react';
|
import React, { ReactNode } from 'react';
|
||||||
import { Container } from '@/ui/Container';
|
import { Box } from './primitives/Box';
|
||||||
|
import { Container } from './Container';
|
||||||
|
|
||||||
interface HeaderProps {
|
export interface HeaderProps {
|
||||||
children: React.ReactNode;
|
children: ReactNode;
|
||||||
|
actions?: ReactNode;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function Header({ children }: HeaderProps) {
|
export const Header = ({
|
||||||
|
children,
|
||||||
|
actions
|
||||||
|
}: HeaderProps) => {
|
||||||
return (
|
return (
|
||||||
<header className="fixed top-0 left-0 right-0 z-50 bg-graphite-black/80 backdrop-blur-md border-b border-border-gray/50">
|
<Box
|
||||||
<Container>
|
as="header"
|
||||||
{children}
|
bg="var(--ui-color-bg-surface)"
|
||||||
|
borderBottom
|
||||||
|
paddingY={4}
|
||||||
|
position="sticky"
|
||||||
|
top={0}
|
||||||
|
zIndex={50}
|
||||||
|
>
|
||||||
|
<Container size="xl">
|
||||||
|
<Box display="flex" alignItems="center" justifyContent="between">
|
||||||
|
<Box display="flex" alignItems="center" gap={8}>
|
||||||
|
{children}
|
||||||
|
</Box>
|
||||||
|
{actions && (
|
||||||
|
<Box display="flex" alignItems="center" gap={4}>
|
||||||
|
{actions}
|
||||||
|
</Box>
|
||||||
|
)}
|
||||||
|
</Box>
|
||||||
</Container>
|
</Container>
|
||||||
</header>
|
</Box>
|
||||||
);
|
);
|
||||||
}
|
};
|
||||||
|
|||||||
@@ -1,93 +1,52 @@
|
|||||||
import React, { ReactNode, ElementType } from 'react';
|
import React, { ReactNode, forwardRef } from 'react';
|
||||||
import { Stack } from './primitives/Stack';
|
|
||||||
import { Box, BoxProps, ResponsiveValue } from './primitives/Box';
|
import { Box, BoxProps, ResponsiveValue } from './primitives/Box';
|
||||||
|
|
||||||
interface ResponsiveFontSize {
|
export interface HeadingProps extends BoxProps<any> {
|
||||||
base?: string;
|
|
||||||
sm?: string;
|
|
||||||
md?: string;
|
|
||||||
lg?: string;
|
|
||||||
xl?: string;
|
|
||||||
'2xl'?: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface HeadingProps extends Omit<BoxProps<'h1'>, 'children' | 'as' | 'fontSize'> {
|
|
||||||
level: 1 | 2 | 3 | 4 | 5 | 6;
|
|
||||||
children: ReactNode;
|
children: ReactNode;
|
||||||
icon?: ReactNode;
|
level?: 1 | 2 | 3 | 4 | 5 | 6;
|
||||||
id?: string;
|
weight?: 'normal' | 'medium' | 'semibold' | 'bold';
|
||||||
groupHoverColor?: string;
|
align?: 'left' | 'center' | 'right';
|
||||||
truncate?: boolean;
|
fontSize?: string | ResponsiveValue<string>;
|
||||||
uppercase?: boolean;
|
|
||||||
fontSize?: string | ResponsiveFontSize;
|
|
||||||
weight?: 'light' | 'normal' | 'medium' | 'semibold' | 'bold' | string;
|
|
||||||
letterSpacing?: string;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function Heading({ level, children, icon, groupHoverColor, truncate, uppercase, fontSize, weight, letterSpacing, ...props }: HeadingProps) {
|
export const Heading = forwardRef<HTMLHeadingElement, HeadingProps>(({
|
||||||
const Tag = `h${level}` as ElementType;
|
children,
|
||||||
|
level = 1,
|
||||||
|
weight = 'bold',
|
||||||
|
align = 'left',
|
||||||
|
fontSize,
|
||||||
|
...props
|
||||||
|
}, ref) => {
|
||||||
|
const Tag = `h${level}` as const;
|
||||||
|
|
||||||
const levelClasses = {
|
const weightClasses = {
|
||||||
1: 'text-3xl md:text-4xl font-bold text-white tracking-tight',
|
|
||||||
2: 'text-xl md:text-2xl font-bold text-white tracking-tight',
|
|
||||||
3: 'text-lg font-bold text-white tracking-tight',
|
|
||||||
4: 'text-base font-bold text-white tracking-tight',
|
|
||||||
5: 'text-sm font-bold text-white tracking-tight uppercase tracking-wider',
|
|
||||||
6: 'text-xs font-bold text-white tracking-tight uppercase tracking-widest',
|
|
||||||
};
|
|
||||||
|
|
||||||
const weightClasses: Record<string, string> = {
|
|
||||||
light: 'font-light',
|
|
||||||
normal: 'font-normal',
|
normal: 'font-normal',
|
||||||
medium: 'font-medium',
|
medium: 'font-medium',
|
||||||
semibold: 'font-semibold',
|
semibold: 'font-semibold',
|
||||||
bold: 'font-bold'
|
bold: 'font-bold'
|
||||||
};
|
};
|
||||||
|
|
||||||
const getFontSizeClasses = (value: string | ResponsiveFontSize | undefined) => {
|
const sizeClasses = {
|
||||||
if (value === undefined) return '';
|
1: 'text-4xl md:text-5xl',
|
||||||
if (typeof value === 'object') {
|
2: 'text-3xl md:text-4xl',
|
||||||
const classes = [];
|
3: 'text-2xl md:text-3xl',
|
||||||
if (value.base) classes.push(`text-${value.base}`);
|
4: 'text-xl md:text-2xl',
|
||||||
if (value.sm) classes.push(`sm:text-${value.sm}`);
|
5: 'text-lg md:text-xl',
|
||||||
if (value.md) classes.push(`md:text-${value.md}`);
|
6: 'text-base md:text-lg'
|
||||||
if (value.lg) classes.push(`lg:text-${value.lg}`);
|
|
||||||
if (value.xl) classes.push(`xl:text-${value.xl}`);
|
|
||||||
if (value['2xl']) classes.push(`2xl:text-${value['2xl']}`);
|
|
||||||
return classes.join(' ');
|
|
||||||
}
|
|
||||||
return `text-${value}`;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const content = icon ? (
|
|
||||||
<Stack direction="row" align="center" gap={2}>
|
|
||||||
{icon}
|
|
||||||
{children}
|
|
||||||
</Stack>
|
|
||||||
) : children;
|
|
||||||
|
|
||||||
const classes = [
|
const classes = [
|
||||||
levelClasses[level],
|
'text-[var(--ui-color-text-high)]',
|
||||||
getFontSizeClasses(fontSize),
|
weightClasses[weight],
|
||||||
weight && weightClasses[weight as keyof typeof weightClasses] ? weightClasses[weight as keyof typeof weightClasses] : '',
|
fontSize ? '' : sizeClasses[level],
|
||||||
letterSpacing ? `tracking-${letterSpacing}` : '',
|
align === 'center' ? 'text-center' : (align === 'right' ? 'text-right' : 'text-left'),
|
||||||
uppercase ? 'uppercase' : '',
|
].join(' ');
|
||||||
groupHoverColor ? `group-hover:text-${groupHoverColor}` : '',
|
|
||||||
truncate ? 'truncate' : '',
|
|
||||||
props.className
|
|
||||||
].filter(Boolean).join(' ');
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box
|
<Box as={Tag} ref={ref} className={classes} fontSize={typeof fontSize === 'string' ? fontSize : undefined} {...props}>
|
||||||
as={Tag}
|
{children}
|
||||||
{...props}
|
|
||||||
className={classes}
|
|
||||||
style={{
|
|
||||||
...(weight && !weightClasses[weight as keyof typeof weightClasses] ? { fontWeight: weight } : {}),
|
|
||||||
...(props.style || {})
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{content}
|
|
||||||
</Box>
|
</Box>
|
||||||
);
|
);
|
||||||
}
|
});
|
||||||
|
|
||||||
|
Heading.displayName = 'Heading';
|
||||||
|
|||||||
@@ -1,28 +1,50 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { Box } from './primitives/Box';
|
||||||
|
import { Text } from './Text';
|
||||||
|
import { Stack } from './primitives/Stack';
|
||||||
|
|
||||||
|
export interface HorizontalBarChartItem {
|
||||||
|
label: string;
|
||||||
interface BarChartProps {
|
value: number;
|
||||||
data: { label: string; value: number; color: string }[];
|
color?: string;
|
||||||
maxValue: number;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function HorizontalBarChart({ data, maxValue }: BarChartProps) {
|
export interface HorizontalBarChartProps {
|
||||||
|
items?: HorizontalBarChartItem[];
|
||||||
|
total?: number;
|
||||||
|
data?: HorizontalBarChartItem[]; // Alias for items
|
||||||
|
maxValue?: number; // Alias for total
|
||||||
|
}
|
||||||
|
|
||||||
|
export const HorizontalBarChart = ({
|
||||||
|
items,
|
||||||
|
total,
|
||||||
|
data,
|
||||||
|
maxValue
|
||||||
|
}: HorizontalBarChartProps) => {
|
||||||
|
const actualItems = items || data || [];
|
||||||
|
const actualTotal = total || maxValue || actualItems.reduce((acc, item) => acc + item.value, 0);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="space-y-3">
|
<Box display="flex" flexDirection="col" gap={4}>
|
||||||
{data.map((item) => (
|
{actualItems.map((item, index) => {
|
||||||
<div key={item.label}>
|
const percentage = actualTotal > 0 ? (item.value / actualTotal) * 100 : 0;
|
||||||
<div className="flex justify-between text-sm mb-1">
|
return (
|
||||||
<span className="text-gray-400">{item.label}</span>
|
<Box key={index}>
|
||||||
<span className="text-white font-medium">{item.value}</span>
|
<Stack direction="row" justify="between" marginBottom={1}>
|
||||||
</div>
|
<Text size="xs" variant="low" uppercase weight="bold">{item.label}</Text>
|
||||||
<div className="h-2 bg-charcoal-outline rounded-full overflow-hidden">
|
<Text size="xs" variant="high" weight="bold">{item.value}</Text>
|
||||||
<div
|
</Stack>
|
||||||
className={`h-full rounded-full ${item.color} transition-all duration-500 ease-out`}
|
<Box fullWidth height="0.5rem" bg="var(--ui-color-bg-surface-muted)" style={{ borderRadius: '9999px', overflow: 'hidden' }}>
|
||||||
style={{ width: `${Math.min((item.value / maxValue) * 100, 100)}%` }}
|
<Box
|
||||||
/>
|
fullHeight
|
||||||
</div>
|
bg={item.color || 'var(--ui-color-intent-primary)'}
|
||||||
</div>
|
style={{ width: `${percentage}%`, transition: 'width 0.3s ease-in-out' }}
|
||||||
))}
|
/>
|
||||||
</div>
|
</Box>
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</Box>
|
||||||
);
|
);
|
||||||
}
|
};
|
||||||
|
|||||||
@@ -1,46 +1,38 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
import { Card } from './Card';
|
||||||
import { Box } from './primitives/Box';
|
import { Box } from './primitives/Box';
|
||||||
import { Stack } from './primitives/Stack';
|
|
||||||
import { Text } from './Text';
|
import { Text } from './Text';
|
||||||
import { Surface } from './primitives/Surface';
|
|
||||||
import { Icon } from './Icon';
|
import { Icon } from './Icon';
|
||||||
import { LucideIcon } from 'lucide-react';
|
import { LucideIcon } from 'lucide-react';
|
||||||
|
|
||||||
interface HorizontalStatCardProps {
|
export interface HorizontalStatCardProps {
|
||||||
label: string;
|
label: string;
|
||||||
value: string | number;
|
value: string | number;
|
||||||
icon: LucideIcon;
|
icon: LucideIcon;
|
||||||
iconColor?: string;
|
intent?: 'primary' | 'success' | 'warning' | 'critical' | 'telemetry';
|
||||||
iconBgColor?: string;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function HorizontalStatCard({
|
export const HorizontalStatCard = ({
|
||||||
label,
|
label,
|
||||||
value,
|
value,
|
||||||
icon,
|
icon,
|
||||||
iconColor = 'text-primary-blue',
|
intent = 'primary'
|
||||||
iconBgColor = 'rgba(59, 130, 246, 0.1)',
|
}: HorizontalStatCardProps) => {
|
||||||
}: HorizontalStatCardProps) {
|
|
||||||
return (
|
return (
|
||||||
<Surface variant="muted" rounded="xl" border p={4}>
|
<Card variant="default">
|
||||||
<Stack direction="row" align="center" gap={4}>
|
<Box display="flex" alignItems="center" gap={4}>
|
||||||
<Surface
|
<Box padding={3} rounded="lg" bg="var(--ui-color-bg-surface-muted)">
|
||||||
variant="muted"
|
<Icon icon={icon} size={5} intent={intent} />
|
||||||
rounded="lg"
|
</Box>
|
||||||
p={3}
|
|
||||||
backgroundColor={iconBgColor}
|
|
||||||
>
|
|
||||||
<Icon icon={icon} size={5} color={iconColor} />
|
|
||||||
</Surface>
|
|
||||||
<Box>
|
<Box>
|
||||||
<Text size="xs" color="text-gray-500" uppercase letterSpacing="wider" block>
|
<Text size="xs" weight="bold" variant="low" uppercase>
|
||||||
{label}
|
{label}
|
||||||
</Text>
|
</Text>
|
||||||
<Text size="xl" weight="bold" color="text-white" block>
|
<Text size="xl" weight="bold" variant="high" block marginTop={0.5}>
|
||||||
{value}
|
{value}
|
||||||
</Text>
|
</Text>
|
||||||
</Box>
|
</Box>
|
||||||
</Stack>
|
</Box>
|
||||||
</Surface>
|
</Card>
|
||||||
);
|
);
|
||||||
}
|
};
|
||||||
|
|||||||
@@ -2,17 +2,21 @@ import React from 'react';
|
|||||||
import { Box } from './primitives/Box';
|
import { Box } from './primitives/Box';
|
||||||
import { Text } from './Text';
|
import { Text } from './Text';
|
||||||
|
|
||||||
interface HorizontalStatItemProps {
|
export interface HorizontalStatItemProps {
|
||||||
label: string;
|
label: string;
|
||||||
value: string | number;
|
value: string | number;
|
||||||
color?: string;
|
intent?: 'primary' | 'success' | 'warning' | 'critical' | 'high' | 'med' | 'low';
|
||||||
}
|
}
|
||||||
|
|
||||||
export function HorizontalStatItem({ label, value, color = 'text-white' }: HorizontalStatItemProps) {
|
export const HorizontalStatItem = ({
|
||||||
|
label,
|
||||||
|
value,
|
||||||
|
intent = 'high'
|
||||||
|
}: HorizontalStatItemProps) => {
|
||||||
return (
|
return (
|
||||||
<Box display="flex" alignItems="center" justifyContent="between" fullWidth>
|
<Box display="flex" alignItems="center" justifyContent="between" paddingY={2}>
|
||||||
<Text size="sm" color="text-gray-400">{label}</Text>
|
<Text size="sm" variant="low">{label}</Text>
|
||||||
<Text weight="semibold" color={color}>{value}</Text>
|
<Text weight="semibold" variant={intent}>{value}</Text>
|
||||||
</Box>
|
</Box>
|
||||||
);
|
);
|
||||||
}
|
};
|
||||||
|
|||||||
@@ -4,77 +4,71 @@ import { Box, BoxProps } from './primitives/Box';
|
|||||||
|
|
||||||
export interface IconProps extends Omit<BoxProps<'div'>, 'children'> {
|
export interface IconProps extends Omit<BoxProps<'div'>, 'children'> {
|
||||||
icon: LucideIcon | React.ReactNode;
|
icon: LucideIcon | React.ReactNode;
|
||||||
size?: number | string;
|
size?: 3 | 3.5 | 4 | 5 | 6 | 7 | 8 | 10 | 12 | 16 | 'full' | number;
|
||||||
color?: string;
|
intent?: 'primary' | 'telemetry' | 'warning' | 'success' | 'critical' | 'high' | 'med' | 'low';
|
||||||
strokeWidth?: number;
|
animate?: 'spin' | 'none';
|
||||||
animate?: string;
|
|
||||||
transition?: boolean;
|
|
||||||
groupHoverTextColor?: string;
|
|
||||||
groupHoverScale?: boolean;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function Icon({
|
export function Icon({
|
||||||
icon: IconProp,
|
icon: IconProp,
|
||||||
size = 4,
|
size = 4,
|
||||||
color,
|
intent,
|
||||||
className = '',
|
animate = 'none',
|
||||||
style,
|
|
||||||
animate,
|
|
||||||
transition,
|
|
||||||
groupHoverTextColor,
|
|
||||||
groupHoverScale,
|
|
||||||
...props
|
...props
|
||||||
}: IconProps) {
|
}: IconProps) {
|
||||||
const sizeMap: Record<string | number, string> = {
|
const sizeMap: Record<string | number, string> = {
|
||||||
3: 'w-3 h-3',
|
3: '0.75rem',
|
||||||
3.5: 'w-3.5 h-3.5',
|
3.5: '0.875rem',
|
||||||
4: 'w-4 h-4',
|
4: '1rem',
|
||||||
5: 'w-5 h-5',
|
5: '1.25rem',
|
||||||
6: 'w-6 h-6',
|
6: '1.5rem',
|
||||||
7: 'w-7 h-7',
|
7: '1.75rem',
|
||||||
8: 'w-8 h-8',
|
8: '2rem',
|
||||||
10: 'w-10 h-10',
|
10: '2.5rem',
|
||||||
12: 'w-12 h-12',
|
12: '3rem',
|
||||||
16: 'w-16 h-16',
|
16: '4rem',
|
||||||
'full': 'w-full h-full'
|
'full': '100%'
|
||||||
};
|
};
|
||||||
|
|
||||||
const sizeClass = sizeMap[size] || 'w-4 h-4';
|
const dimension = typeof size === 'string' ? sizeMap[size] : (sizeMap[size] || `${size * 0.25}rem`);
|
||||||
|
|
||||||
// If color starts with 'text-', it's a tailwind class, so pass it as color prop to Box
|
|
||||||
const isTailwindColor = typeof color === 'string' && color.startsWith('text-');
|
|
||||||
const combinedStyle = color && !isTailwindColor ? { color, ...style } : style;
|
|
||||||
const boxColor = isTailwindColor ? color : undefined;
|
|
||||||
|
|
||||||
const classes = [
|
const intentColorMap: Record<string, string> = {
|
||||||
sizeClass,
|
primary: 'var(--ui-color-intent-primary)',
|
||||||
animate === 'spin' ? 'animate-spin' : '',
|
telemetry: 'var(--ui-color-intent-telemetry)',
|
||||||
transition ? 'transition-all duration-150' : '',
|
warning: 'var(--ui-color-intent-warning)',
|
||||||
groupHoverTextColor ? `group-hover:text-${groupHoverTextColor}` : '',
|
success: 'var(--ui-color-intent-success)',
|
||||||
groupHoverScale ? 'group-hover:scale-110 transition-transform' : '',
|
critical: 'var(--ui-color-intent-critical)',
|
||||||
className
|
high: 'var(--ui-color-text-high)',
|
||||||
].filter(Boolean).join(' ');
|
med: 'var(--ui-color-text-med)',
|
||||||
|
low: 'var(--ui-color-text-low)',
|
||||||
|
};
|
||||||
|
|
||||||
|
const style: React.CSSProperties = {
|
||||||
|
width: dimension,
|
||||||
|
height: dimension,
|
||||||
|
color: intent ? intentColorMap[intent] : undefined,
|
||||||
|
};
|
||||||
|
|
||||||
const renderIcon = () => {
|
const renderIcon = () => {
|
||||||
if (!IconProp) return null;
|
if (!IconProp) return null;
|
||||||
if (typeof IconProp === 'function' || (typeof IconProp === 'object' && 'render' in IconProp)) {
|
if (typeof IconProp === 'function' || (typeof IconProp === 'object' && 'render' in IconProp)) {
|
||||||
const LucideIconComponent = IconProp as LucideIcon;
|
const LucideIconComponent = IconProp as LucideIcon;
|
||||||
return <LucideIconComponent size="100%" strokeWidth={props.strokeWidth} />;
|
return <LucideIconComponent size="100%" />;
|
||||||
}
|
}
|
||||||
return IconProp;
|
return IconProp;
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box
|
<Box
|
||||||
className={classes}
|
|
||||||
style={combinedStyle}
|
|
||||||
color={boxColor}
|
|
||||||
display="inline-flex"
|
display="inline-flex"
|
||||||
alignItems="center"
|
alignItems="center"
|
||||||
justifyContent="center"
|
justifyContent="center"
|
||||||
|
style={style}
|
||||||
{...props}
|
{...props}
|
||||||
>
|
>
|
||||||
{renderIcon()}
|
<div className={animate === 'spin' ? 'animate-spin w-full h-full flex items-center justify-center' : 'w-full h-full flex items-center justify-center'}>
|
||||||
|
{renderIcon()}
|
||||||
|
</div>
|
||||||
</Box>
|
</Box>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,54 +1,31 @@
|
|||||||
import React from 'react';
|
import React, { MouseEventHandler } from 'react';
|
||||||
import { LucideIcon } from 'lucide-react';
|
import { Button, ButtonProps } from './Button';
|
||||||
import { Button } from './Button';
|
|
||||||
import { Icon } from './Icon';
|
import { Icon } from './Icon';
|
||||||
|
|
||||||
interface IconButtonProps {
|
export interface IconButtonProps extends Omit<ButtonProps, 'children' | 'icon'> {
|
||||||
icon: LucideIcon;
|
icon: any;
|
||||||
onClick?: React.MouseEventHandler<HTMLButtonElement>;
|
|
||||||
variant?: 'primary' | 'secondary' | 'danger' | 'ghost';
|
|
||||||
size?: 'sm' | 'md' | 'lg';
|
|
||||||
title?: string;
|
title?: string;
|
||||||
disabled?: boolean;
|
|
||||||
color?: string;
|
|
||||||
className?: string;
|
|
||||||
backgroundColor?: string;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function IconButton({
|
export const IconButton = ({
|
||||||
icon,
|
icon,
|
||||||
onClick,
|
|
||||||
variant = 'secondary',
|
|
||||||
size = 'md',
|
|
||||||
title,
|
title,
|
||||||
disabled,
|
size = 'md',
|
||||||
color,
|
...props
|
||||||
className = '',
|
}: IconButtonProps) => {
|
||||||
backgroundColor,
|
const iconSizeMap = {
|
||||||
}: IconButtonProps) {
|
sm: 3,
|
||||||
const sizeMap = {
|
md: 4,
|
||||||
sm: { w: '8', h: '8', icon: 4 },
|
lg: 5
|
||||||
md: { w: '10', h: '10', icon: 5 },
|
} as const;
|
||||||
lg: { w: '12', h: '12', icon: 6 },
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Button
|
<Button
|
||||||
variant={variant}
|
size={size}
|
||||||
onClick={onClick}
|
{...props}
|
||||||
title={title}
|
|
||||||
disabled={disabled}
|
|
||||||
w={sizeMap[size].w}
|
|
||||||
h={sizeMap[size].h}
|
|
||||||
p={0}
|
|
||||||
rounded="full"
|
|
||||||
display="flex"
|
|
||||||
center
|
|
||||||
minHeight="0"
|
|
||||||
className={className}
|
|
||||||
backgroundColor={backgroundColor}
|
|
||||||
>
|
>
|
||||||
<Icon icon={icon} size={sizeMap[size].icon} color={color} />
|
<Icon icon={icon} size={iconSizeMap[size]} />
|
||||||
|
{title && <span className="sr-only">{title}</span>}
|
||||||
</Button>
|
</Button>
|
||||||
);
|
);
|
||||||
}
|
};
|
||||||
|
|||||||
@@ -1,53 +1,36 @@
|
|||||||
import React, { ImgHTMLAttributes } from 'react';
|
import React, { useState } from 'react';
|
||||||
|
import { Box, BoxProps } from './primitives/Box';
|
||||||
|
import { ImagePlaceholder } from './ImagePlaceholder';
|
||||||
|
|
||||||
interface ImageProps extends ImgHTMLAttributes<HTMLImageElement> {
|
export interface ImageProps extends Omit<BoxProps<'img'>, 'children'> {
|
||||||
src: string;
|
src: string;
|
||||||
alt: string;
|
alt: string;
|
||||||
width?: number;
|
|
||||||
height?: number;
|
|
||||||
className?: string;
|
|
||||||
fallbackSrc?: string;
|
fallbackSrc?: string;
|
||||||
objectFit?: 'cover' | 'contain' | 'fill' | 'none' | 'scale-down';
|
fallbackComponent?: React.ReactNode;
|
||||||
fill?: boolean;
|
|
||||||
fullWidth?: boolean;
|
|
||||||
fullHeight?: boolean;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function Image({
|
export const Image = ({
|
||||||
src,
|
src,
|
||||||
alt,
|
alt,
|
||||||
width,
|
fallbackSrc,
|
||||||
height,
|
fallbackComponent,
|
||||||
className = '',
|
|
||||||
fallbackSrc,
|
|
||||||
objectFit,
|
|
||||||
fill,
|
|
||||||
fullWidth,
|
|
||||||
fullHeight,
|
|
||||||
...props
|
...props
|
||||||
}: ImageProps) {
|
}: ImageProps) => {
|
||||||
const classes = [
|
const [error, setError] = useState(false);
|
||||||
objectFit ? `object-${objectFit}` : '',
|
|
||||||
fill ? 'absolute inset-0 w-full h-full' : '',
|
if (error) {
|
||||||
fullWidth ? 'w-full' : '',
|
if (fallbackComponent) return <>{fallbackComponent}</>;
|
||||||
fullHeight ? 'h-full' : '',
|
if (fallbackSrc) return <Box as="img" src={fallbackSrc} alt={alt} {...props} />;
|
||||||
className
|
return <ImagePlaceholder />;
|
||||||
].filter(Boolean).join(' ');
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
// eslint-disable-next-line @next/next/no-img-element
|
<Box
|
||||||
<img
|
as="img"
|
||||||
src={src}
|
src={src}
|
||||||
alt={alt}
|
alt={alt}
|
||||||
width={width}
|
onError={() => setError(true)}
|
||||||
height={height}
|
{...props}
|
||||||
className={classes}
|
|
||||||
onError={(e) => {
|
|
||||||
if (fallbackSrc) {
|
|
||||||
(e.target as HTMLImageElement).src = fallbackSrc;
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
{...props}
|
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
};
|
||||||
|
|||||||
@@ -1,86 +1,39 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { Image as ImageIcon, AlertCircle, Loader2 } from 'lucide-react';
|
|
||||||
import { Box } from './primitives/Box';
|
import { Box } from './primitives/Box';
|
||||||
import { Icon } from './Icon';
|
import { Icon } from './Icon';
|
||||||
import { Text } from './Text';
|
import { Image as ImageIcon } from 'lucide-react';
|
||||||
|
|
||||||
export interface ImagePlaceholderProps {
|
export interface ImagePlaceholderProps {
|
||||||
size?: number | string;
|
width?: string | number;
|
||||||
|
height?: string | number;
|
||||||
|
animate?: 'pulse' | 'none' | 'spin';
|
||||||
aspectRatio?: string;
|
aspectRatio?: string;
|
||||||
variant?: 'default' | 'error' | 'loading';
|
|
||||||
message?: string;
|
|
||||||
className?: string;
|
|
||||||
rounded?: 'none' | 'sm' | 'md' | 'lg' | 'xl' | '2xl' | 'full';
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function ImagePlaceholder({
|
export const ImagePlaceholder = ({
|
||||||
size = 'full',
|
width = '100%',
|
||||||
aspectRatio = '1/1',
|
height = '100%',
|
||||||
variant = 'default',
|
animate = 'pulse',
|
||||||
message,
|
aspectRatio
|
||||||
className = '',
|
}: ImagePlaceholderProps) => {
|
||||||
rounded = 'md',
|
|
||||||
}: ImagePlaceholderProps) {
|
|
||||||
const config = {
|
|
||||||
default: {
|
|
||||||
icon: ImageIcon,
|
|
||||||
color: 'text-gray-500',
|
|
||||||
bg: 'bg-charcoal-outline/20',
|
|
||||||
borderColor: 'border-charcoal-outline/50',
|
|
||||||
animate: undefined as 'spin' | 'pulse' | 'bounce' | 'fade-in' | 'none' | undefined,
|
|
||||||
},
|
|
||||||
error: {
|
|
||||||
icon: AlertCircle,
|
|
||||||
color: 'text-amber-500',
|
|
||||||
bg: 'bg-amber-500/5',
|
|
||||||
borderColor: 'border-amber-500/20',
|
|
||||||
animate: undefined as 'spin' | 'pulse' | 'bounce' | 'fade-in' | 'none' | undefined,
|
|
||||||
},
|
|
||||||
loading: {
|
|
||||||
icon: Loader2,
|
|
||||||
color: 'text-blue-500',
|
|
||||||
bg: 'bg-blue-500/5',
|
|
||||||
borderColor: 'border-blue-500/20',
|
|
||||||
animate: 'spin' as const,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
const { icon, color, bg, borderColor, animate } = config[variant];
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box
|
<Box
|
||||||
display="flex"
|
width={width}
|
||||||
flexDirection="col"
|
height={height}
|
||||||
alignItems="center"
|
aspectRatio={aspectRatio}
|
||||||
justifyContent="center"
|
display="flex"
|
||||||
w={typeof size === 'string' ? size : undefined}
|
alignItems="center"
|
||||||
h={typeof size === 'string' ? size : undefined}
|
justifyContent="center"
|
||||||
style={typeof size === 'number' ? { width: size, height: size } : { aspectRatio }}
|
bg="var(--ui-color-bg-surface-muted)"
|
||||||
bg={bg}
|
style={{ borderRadius: 'var(--ui-radius-md)' }}
|
||||||
border
|
|
||||||
borderColor={borderColor}
|
|
||||||
rounded={rounded}
|
|
||||||
className={`overflow-hidden ${className}`}
|
|
||||||
gap={2}
|
|
||||||
p={4}
|
|
||||||
>
|
>
|
||||||
<Icon
|
<Icon
|
||||||
icon={icon}
|
icon={ImageIcon}
|
||||||
size={6}
|
size={8}
|
||||||
color={color}
|
intent="low"
|
||||||
animate={animate}
|
animate={animate === 'spin' ? 'spin' : 'none'}
|
||||||
|
className={animate === 'pulse' ? 'animate-pulse' : ''}
|
||||||
/>
|
/>
|
||||||
{message && (
|
|
||||||
<Text
|
|
||||||
size="xs"
|
|
||||||
color={color}
|
|
||||||
weight="medium"
|
|
||||||
align="center"
|
|
||||||
className="max-w-[80%]"
|
|
||||||
>
|
|
||||||
{message}
|
|
||||||
</Text>
|
|
||||||
)}
|
|
||||||
</Box>
|
</Box>
|
||||||
);
|
);
|
||||||
}
|
};
|
||||||
|
|||||||
@@ -1,77 +1,72 @@
|
|||||||
import React from 'react';
|
import React, { ReactNode } from 'react';
|
||||||
import { Box } from './primitives/Box';
|
import { Box } from './primitives/Box';
|
||||||
import { Stack } from './primitives/Stack';
|
|
||||||
import { Text } from './Text';
|
import { Text } from './Text';
|
||||||
import { Surface } from './primitives/Surface';
|
|
||||||
import { Icon } from './Icon';
|
import { Icon } from './Icon';
|
||||||
import { Info, AlertTriangle, AlertCircle, CheckCircle, LucideIcon } from 'lucide-react';
|
import { Info, AlertTriangle, CheckCircle, XCircle } from 'lucide-react';
|
||||||
|
import { Surface } from './primitives/Surface';
|
||||||
|
|
||||||
interface InfoBannerProps {
|
export interface InfoBannerProps {
|
||||||
|
children?: ReactNode;
|
||||||
|
variant?: 'info' | 'warning' | 'success' | 'critical';
|
||||||
|
type?: 'info' | 'warning' | 'success' | 'critical'; // Alias for variant
|
||||||
title?: string;
|
title?: string;
|
||||||
message?: string;
|
|
||||||
children?: React.ReactNode;
|
|
||||||
variant?: 'info' | 'warning' | 'error' | 'success';
|
|
||||||
type?: 'info' | 'warning' | 'error' | 'success';
|
|
||||||
icon?: LucideIcon;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function InfoBanner({ title, message, children, variant = 'info', type, icon }: InfoBannerProps) {
|
export const InfoBanner = ({
|
||||||
const configs = {
|
children,
|
||||||
|
variant,
|
||||||
|
type,
|
||||||
|
title
|
||||||
|
}: InfoBannerProps) => {
|
||||||
|
const activeVariant = variant || type || 'info';
|
||||||
|
|
||||||
|
const config = {
|
||||||
info: {
|
info: {
|
||||||
bg: 'rgba(59, 130, 246, 0.1)',
|
icon: Info,
|
||||||
border: 'rgba(59, 130, 246, 0.2)',
|
intent: 'primary' as const,
|
||||||
iconColor: 'rgb(96, 165, 250)',
|
bg: 'rgba(25, 140, 255, 0.05)',
|
||||||
icon: Info
|
border: 'rgba(25, 140, 255, 0.2)',
|
||||||
},
|
},
|
||||||
warning: {
|
warning: {
|
||||||
bg: 'rgba(245, 158, 11, 0.1)',
|
icon: AlertTriangle,
|
||||||
border: 'rgba(245, 158, 11, 0.2)',
|
intent: 'warning' as const,
|
||||||
iconColor: 'rgb(251, 191, 36)',
|
bg: 'rgba(255, 190, 77, 0.05)',
|
||||||
icon: AlertTriangle
|
border: 'rgba(255, 190, 77, 0.2)',
|
||||||
},
|
|
||||||
error: {
|
|
||||||
bg: 'rgba(239, 68, 68, 0.1)',
|
|
||||||
border: 'rgba(239, 68, 68, 0.2)',
|
|
||||||
iconColor: 'rgb(248, 113, 113)',
|
|
||||||
icon: AlertCircle
|
|
||||||
},
|
},
|
||||||
success: {
|
success: {
|
||||||
bg: 'rgba(16, 185, 129, 0.1)',
|
icon: CheckCircle,
|
||||||
border: 'rgba(16, 185, 129, 0.2)',
|
intent: 'success' as const,
|
||||||
iconColor: 'rgb(52, 211, 153)',
|
bg: 'rgba(111, 227, 122, 0.05)',
|
||||||
icon: CheckCircle
|
border: 'rgba(111, 227, 122, 0.2)',
|
||||||
}
|
},
|
||||||
};
|
critical: {
|
||||||
|
icon: XCircle,
|
||||||
const activeVariant = type || variant;
|
intent: 'critical' as const,
|
||||||
const config = configs[activeVariant as keyof typeof configs] || configs.info;
|
bg: 'rgba(227, 92, 92, 0.05)',
|
||||||
const BannerIcon = icon || config.icon;
|
border: 'rgba(227, 92, 92, 0.2)',
|
||||||
|
},
|
||||||
|
}[activeVariant];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Surface
|
<Surface
|
||||||
variant="muted"
|
variant="muted"
|
||||||
rounded="xl"
|
rounded="lg"
|
||||||
border
|
padding={4}
|
||||||
p={4}
|
style={{ backgroundColor: config.bg, border: `1px solid ${config.border}` }}
|
||||||
backgroundColor={config.bg}
|
|
||||||
borderColor={config.border}
|
|
||||||
>
|
>
|
||||||
<Stack direction="row" align="start" gap={3}>
|
<Box display="flex" alignItems="start" gap={3}>
|
||||||
<Icon icon={BannerIcon} size={5} color={config.iconColor} />
|
<Icon icon={config.icon} size={5} intent={config.intent} />
|
||||||
<Box flex={1}>
|
<Box flex={1}>
|
||||||
{title && (
|
{title && (
|
||||||
<Text weight="medium" color="text-white" block mb={1}>
|
<Text size="sm" weight="bold" variant="high" marginBottom={1} block>
|
||||||
{title}
|
{title}
|
||||||
</Text>
|
</Text>
|
||||||
)}
|
)}
|
||||||
{message && (
|
<Text size="sm" variant="high">
|
||||||
<Text size="sm" color="text-gray-300" block>
|
{children}
|
||||||
{message}
|
</Text>
|
||||||
</Text>
|
|
||||||
)}
|
|
||||||
{children}
|
|
||||||
</Box>
|
</Box>
|
||||||
</Stack>
|
</Box>
|
||||||
</Surface>
|
</Surface>
|
||||||
);
|
);
|
||||||
}
|
};
|
||||||
|
|||||||
@@ -1,66 +1,72 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { Box } from './primitives/Box';
|
import { Box } from './primitives/Box';
|
||||||
import { Stack } from './primitives/Stack';
|
|
||||||
import { Text } from './Text';
|
import { Text } from './Text';
|
||||||
import { Surface } from './primitives/Surface';
|
|
||||||
import { Icon } from './Icon';
|
import { Icon } from './Icon';
|
||||||
import { LucideIcon } from 'lucide-react';
|
import { LucideIcon } from 'lucide-react';
|
||||||
|
import { Surface } from './primitives/Surface';
|
||||||
|
|
||||||
interface InfoBoxProps {
|
export interface InfoBoxProps {
|
||||||
title: string;
|
title: string;
|
||||||
description: string;
|
description: string;
|
||||||
icon: LucideIcon;
|
icon: LucideIcon;
|
||||||
variant?: 'info' | 'warning' | 'error' | 'success';
|
intent?: 'primary' | 'success' | 'warning' | 'critical' | 'telemetry';
|
||||||
|
variant?: string; // Alias for intent
|
||||||
}
|
}
|
||||||
|
|
||||||
export function InfoBox({ title, description, icon, variant = 'info' }: InfoBoxProps) {
|
export const InfoBox = ({
|
||||||
const configs = {
|
title,
|
||||||
info: {
|
description,
|
||||||
bg: 'rgba(59, 130, 246, 0.1)',
|
icon,
|
||||||
border: 'rgba(59, 130, 246, 0.2)',
|
intent = 'primary',
|
||||||
icon: 'rgb(96, 165, 250)',
|
variant
|
||||||
text: 'text-white'
|
}: InfoBoxProps) => {
|
||||||
},
|
const activeIntent = (variant || intent) as any;
|
||||||
warning: {
|
|
||||||
bg: 'rgba(245, 158, 11, 0.1)',
|
const configMap: any = {
|
||||||
border: 'rgba(245, 158, 11, 0.2)',
|
primary: {
|
||||||
icon: 'rgb(251, 191, 36)',
|
bg: 'rgba(25, 140, 255, 0.05)',
|
||||||
text: 'text-white'
|
border: 'rgba(25, 140, 255, 0.2)',
|
||||||
},
|
|
||||||
error: {
|
|
||||||
bg: 'rgba(239, 68, 68, 0.1)',
|
|
||||||
border: 'rgba(239, 68, 68, 0.2)',
|
|
||||||
icon: 'rgb(248, 113, 113)',
|
|
||||||
text: 'text-white'
|
|
||||||
},
|
},
|
||||||
success: {
|
success: {
|
||||||
bg: 'rgba(16, 185, 129, 0.1)',
|
bg: 'rgba(111, 227, 122, 0.05)',
|
||||||
border: 'rgba(16, 185, 129, 0.2)',
|
border: 'rgba(111, 227, 122, 0.2)',
|
||||||
icon: 'rgb(52, 211, 153)',
|
},
|
||||||
text: 'text-white'
|
warning: {
|
||||||
}
|
bg: 'rgba(255, 190, 77, 0.05)',
|
||||||
|
border: 'rgba(255, 190, 77, 0.2)',
|
||||||
|
},
|
||||||
|
critical: {
|
||||||
|
bg: 'rgba(227, 92, 92, 0.05)',
|
||||||
|
border: 'rgba(227, 92, 92, 0.2)',
|
||||||
|
},
|
||||||
|
telemetry: {
|
||||||
|
bg: 'rgba(78, 212, 224, 0.05)',
|
||||||
|
border: 'rgba(78, 212, 224, 0.2)',
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
const colors = configs[variant];
|
const config = configMap[activeIntent] || configMap.primary;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Surface
|
<Surface
|
||||||
variant="muted"
|
variant="muted"
|
||||||
rounded="xl"
|
rounded="xl"
|
||||||
border
|
padding={4}
|
||||||
p={4}
|
style={{ backgroundColor: config.bg, border: `1px solid ${config.border}` }}
|
||||||
backgroundColor={colors.bg}
|
|
||||||
borderColor={colors.border}
|
|
||||||
>
|
>
|
||||||
<Stack direction="row" align="start" gap={3}>
|
<Box display="flex" alignItems="start" gap={4}>
|
||||||
<Surface variant="muted" rounded="lg" p={2} bg="bg-white/5">
|
<Surface variant="muted" rounded="lg" padding={2} style={{ backgroundColor: 'rgba(255,255,255,0.05)' }}>
|
||||||
<Icon icon={icon} size={5} color={colors.icon} />
|
<Icon icon={icon} size={5} intent={activeIntent} />
|
||||||
</Surface>
|
</Surface>
|
||||||
<Box>
|
<Box>
|
||||||
<Text weight="medium" color={colors.text} block>{title}</Text>
|
<Text weight="medium" variant="high" block>
|
||||||
<Text size="sm" color="text-gray-400" block mt={1}>{description}</Text>
|
{title}
|
||||||
|
</Text>
|
||||||
|
<Text size="sm" variant="low" block marginTop={1}>
|
||||||
|
{description}
|
||||||
|
</Text>
|
||||||
</Box>
|
</Box>
|
||||||
</Stack>
|
</Box>
|
||||||
</Surface>
|
</Surface>
|
||||||
);
|
);
|
||||||
}
|
};
|
||||||
|
|||||||
@@ -4,33 +4,32 @@ import { Text } from './Text';
|
|||||||
import { Icon } from './Icon';
|
import { Icon } from './Icon';
|
||||||
import { LucideIcon } from 'lucide-react';
|
import { LucideIcon } from 'lucide-react';
|
||||||
|
|
||||||
interface InfoItemProps {
|
export interface InfoItemProps {
|
||||||
icon: LucideIcon;
|
|
||||||
label: string;
|
label: string;
|
||||||
value: string | number;
|
value: string | number;
|
||||||
|
icon: LucideIcon;
|
||||||
|
intent?: 'primary' | 'success' | 'warning' | 'critical' | 'telemetry' | 'high' | 'med' | 'low';
|
||||||
}
|
}
|
||||||
|
|
||||||
export function InfoItem({ icon, label, value }: InfoItemProps) {
|
export const InfoItem = ({
|
||||||
|
label,
|
||||||
|
value,
|
||||||
|
icon,
|
||||||
|
intent = 'med'
|
||||||
|
}: InfoItemProps) => {
|
||||||
return (
|
return (
|
||||||
<Box display="flex" alignItems="start" gap={2.5}>
|
<Box display="flex" alignItems="center" gap={3}>
|
||||||
<Box
|
<Box padding={2} rounded="lg" bg="var(--ui-color-bg-surface-muted)">
|
||||||
display="flex"
|
<Icon icon={icon} size={4} intent={intent as any} />
|
||||||
h="7"
|
|
||||||
w="7"
|
|
||||||
alignItems="center"
|
|
||||||
justifyContent="center"
|
|
||||||
rounded="lg"
|
|
||||||
bg="bg-iron-gray/60"
|
|
||||||
flexShrink={0}
|
|
||||||
>
|
|
||||||
<Icon icon={icon} size={3.5} color="text-gray-500" />
|
|
||||||
</Box>
|
</Box>
|
||||||
<Box flexGrow={1} minWidth="0">
|
<Box>
|
||||||
<Text size="xs" color="text-gray-500" block mb={0.5} style={{ fontSize: '10px' }}>{label}</Text>
|
<Text size="xs" variant="low" block marginBottom={0.5} style={{ fontSize: '10px' }}>
|
||||||
<Text size="xs" weight="medium" color="text-gray-300" block className="truncate">
|
{label}
|
||||||
|
</Text>
|
||||||
|
<Text size="xs" weight="medium" variant="high" block className="truncate">
|
||||||
{value}
|
{value}
|
||||||
</Text>
|
</Text>
|
||||||
</Box>
|
</Box>
|
||||||
</Box>
|
</Box>
|
||||||
);
|
);
|
||||||
}
|
};
|
||||||
|
|||||||
@@ -1,59 +1,70 @@
|
|||||||
import React, { forwardRef, ReactNode } from 'react';
|
import React, { forwardRef, InputHTMLAttributes } from 'react';
|
||||||
import { Box } from './primitives/Box';
|
import { Box } from './primitives/Box';
|
||||||
import { Stack } from './primitives/Stack';
|
|
||||||
import { Text } from './Text';
|
import { Text } from './Text';
|
||||||
|
|
||||||
interface InputProps extends React.InputHTMLAttributes<HTMLInputElement> {
|
export interface InputProps extends Omit<InputHTMLAttributes<HTMLInputElement>, 'size'> {
|
||||||
label?: string;
|
label?: string;
|
||||||
icon?: ReactNode;
|
error?: string;
|
||||||
errorMessage?: string;
|
hint?: string;
|
||||||
variant?: 'default' | 'error';
|
fullWidth?: boolean;
|
||||||
|
size?: 'sm' | 'md' | 'lg';
|
||||||
}
|
}
|
||||||
|
|
||||||
export const Input = forwardRef<HTMLInputElement, InputProps>(
|
export const Input = forwardRef<HTMLInputElement, InputProps>(({
|
||||||
({ label, icon, errorMessage, variant = 'default', className = '', ...props }, ref) => {
|
label,
|
||||||
const isError = variant === 'error' || !!errorMessage;
|
error,
|
||||||
|
hint,
|
||||||
const baseClasses = 'w-full px-4 py-2 bg-deep-graphite border rounded-lg text-white placeholder:text-gray-500 focus:outline-none transition-all duration-150 sm:text-sm';
|
fullWidth = false,
|
||||||
const variantClasses = isError
|
size = 'md',
|
||||||
? 'border-warning-amber focus:border-warning-amber focus:ring-1 focus:ring-warning-amber'
|
...props
|
||||||
: 'border-charcoal-outline focus:border-primary-blue focus:ring-1 focus:ring-primary-blue';
|
}, ref) => {
|
||||||
|
const sizeClasses = {
|
||||||
const classes = `${baseClasses} ${variantClasses} ${icon ? 'pl-11' : ''} ${className}`;
|
sm: 'px-3 py-1.5 text-xs',
|
||||||
|
md: 'px-4 py-2 text-sm',
|
||||||
|
lg: 'px-4 py-3 text-base'
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
const baseClasses = 'bg-[var(--ui-color-bg-surface)] border border-[var(--ui-color-border-default)] text-[var(--ui-color-text-high)] placeholder-[var(--ui-color-text-low)] focus:outline-none focus:border-[var(--ui-color-intent-primary)] transition-colors';
|
||||||
<Stack gap={1.5} fullWidth>
|
const errorClasses = error ? 'border-[var(--ui-color-intent-critical)]' : '';
|
||||||
{label && (
|
const widthClasses = fullWidth ? 'w-full' : '';
|
||||||
<Text as="label" size="xs" weight="bold" color="text-gray-500" uppercase letterSpacing="wider">
|
|
||||||
|
const classes = [
|
||||||
|
baseClasses,
|
||||||
|
sizeClasses[size],
|
||||||
|
errorClasses,
|
||||||
|
widthClasses,
|
||||||
|
].filter(Boolean).join(' ');
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Box width={fullWidth ? '100%' : undefined}>
|
||||||
|
{label && (
|
||||||
|
<Box marginBottom={1.5}>
|
||||||
|
<Text as="label" size="xs" weight="bold" variant="low">
|
||||||
{label}
|
{label}
|
||||||
</Text>
|
</Text>
|
||||||
)}
|
|
||||||
<Box fullWidth position="relative">
|
|
||||||
{icon && (
|
|
||||||
<Box
|
|
||||||
position="absolute"
|
|
||||||
left={0}
|
|
||||||
top="50%"
|
|
||||||
translateY="-50%"
|
|
||||||
zIndex={10}
|
|
||||||
w="11"
|
|
||||||
display="flex"
|
|
||||||
center
|
|
||||||
color="text-gray-500"
|
|
||||||
>
|
|
||||||
{icon}
|
|
||||||
</Box>
|
|
||||||
)}
|
|
||||||
<input ref={ref} className={classes} {...props} />
|
|
||||||
{errorMessage && (
|
|
||||||
<Text size="xs" color="text-warning-amber" mt={1}>
|
|
||||||
{errorMessage}
|
|
||||||
</Text>
|
|
||||||
)}
|
|
||||||
</Box>
|
</Box>
|
||||||
</Stack>
|
)}
|
||||||
);
|
<input
|
||||||
}
|
ref={ref}
|
||||||
);
|
className={classes}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
{error && (
|
||||||
|
<Box marginTop={1}>
|
||||||
|
<Text size="xs" variant="critical">
|
||||||
|
{error}
|
||||||
|
</Text>
|
||||||
|
</Box>
|
||||||
|
)}
|
||||||
|
{hint && !error && (
|
||||||
|
<Box marginTop={1}>
|
||||||
|
<Text size="xs" variant="low">
|
||||||
|
{hint}
|
||||||
|
</Text>
|
||||||
|
</Box>
|
||||||
|
)}
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
Input.displayName = 'Input';
|
Input.displayName = 'Input';
|
||||||
|
|||||||
@@ -1,45 +1,38 @@
|
|||||||
import { Box } from '@/ui/primitives/Box';
|
import React from 'react';
|
||||||
import { Stack } from '@/ui/primitives/Stack';
|
import { Box } from './primitives/Box';
|
||||||
import { Surface } from '@/ui/primitives/Surface';
|
import { Text } from './Text';
|
||||||
import { Text } from '@/ui/Text';
|
import { Surface } from './primitives/Surface';
|
||||||
|
import { Icon } from './Icon';
|
||||||
|
import { LucideIcon } from 'lucide-react';
|
||||||
|
|
||||||
export function FeatureItem({ text }: { text: string }) {
|
export interface LandingItemProps {
|
||||||
return (
|
title: string;
|
||||||
<Surface variant="muted" rounded="none" border padding={4} bg="panel-gray/10" borderColor="border-gray/10" className="group hover:border-primary-accent/20 transition-colors">
|
description: string;
|
||||||
<Stack direction="row" align="center" gap={4}>
|
icon: LucideIcon;
|
||||||
<Box w="0.5" h="3" bg="primary-accent" className="group-hover:h-5 transition-all" />
|
intent?: 'primary' | 'success' | 'warning' | 'critical' | 'telemetry';
|
||||||
<Text color="text-gray-500" leading="relaxed" weight="normal" size="sm" className="tracking-wide group-hover:text-gray-300 transition-colors">
|
|
||||||
{text}
|
|
||||||
</Text>
|
|
||||||
</Stack>
|
|
||||||
</Surface>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function ResultItem({ text, color }: { text: string, color: string }) {
|
export const LandingItem = ({
|
||||||
|
title,
|
||||||
|
description,
|
||||||
|
icon,
|
||||||
|
intent = 'primary'
|
||||||
|
}: LandingItemProps) => {
|
||||||
return (
|
return (
|
||||||
<Surface variant="muted" rounded="none" border padding={4} bg="panel-gray/10" borderColor="border-gray/10" className="group hover:border-primary-accent/20 transition-colors">
|
<Surface variant="muted" rounded="xl" padding={6} style={{ border: '1px solid var(--ui-color-border-default)' }}>
|
||||||
<Stack direction="row" align="center" gap={4}>
|
<Box display="flex" flexDirection="col" gap={4}>
|
||||||
<Box w="0.5" h="3" style={{ backgroundColor: color }} className="group-hover:h-5 transition-all" />
|
<Box padding={3} rounded="lg" bg="var(--ui-color-bg-surface-muted)" width="fit-content">
|
||||||
<Text color="text-gray-500" leading="relaxed" weight="normal" size="sm" className="tracking-wide group-hover:text-gray-300 transition-colors">
|
<Icon icon={icon} size={6} intent={intent} />
|
||||||
{text}
|
|
||||||
</Text>
|
|
||||||
</Stack>
|
|
||||||
</Surface>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export function StepItem({ step, text }: { step: number, text: string }) {
|
|
||||||
return (
|
|
||||||
<Surface variant="muted" rounded="none" border padding={4} bg="panel-gray/10" borderColor="border-gray/10" className="group hover:border-primary-accent/20 transition-colors">
|
|
||||||
<Stack direction="row" align="center" gap={4}>
|
|
||||||
<Box w="6" h="6" display="flex" center border borderColor="border-gray/20" className="group-hover:border-primary-accent/30 transition-colors">
|
|
||||||
<Text weight="bold" size="xs" color="text-primary-accent" font="mono" opacity={0.7}>{step.toString().padStart(2, '0')}</Text>
|
|
||||||
</Box>
|
</Box>
|
||||||
<Text color="text-gray-500" leading="relaxed" weight="normal" size="sm" className="tracking-wide group-hover:text-gray-300 transition-colors">
|
<Box>
|
||||||
{text}
|
<Text weight="bold" variant="high" size="lg" marginBottom={2} block>
|
||||||
</Text>
|
{title}
|
||||||
</Stack>
|
</Text>
|
||||||
|
<Text variant="low" leading="relaxed">
|
||||||
|
{description}
|
||||||
|
</Text>
|
||||||
|
</Box>
|
||||||
|
</Box>
|
||||||
</Surface>
|
</Surface>
|
||||||
);
|
);
|
||||||
}
|
};
|
||||||
|
|||||||
@@ -1,67 +1,44 @@
|
|||||||
import { ReactNode } from 'react';
|
import React, { ReactNode } from 'react';
|
||||||
import { Box } from './primitives/Box';
|
import { Box } from './primitives/Box';
|
||||||
import { Grid } from './primitives/Grid';
|
|
||||||
import { Stack } from './primitives/Stack';
|
|
||||||
|
|
||||||
/**
|
export interface LayoutProps {
|
||||||
* WARNING: DO NOT VIOLATE THE PURPOSE OF THIS COMPONENT.
|
|
||||||
*
|
|
||||||
* Layout is a high-level component for page or section layouts.
|
|
||||||
* It should use Grid or Stack primitives internally.
|
|
||||||
*
|
|
||||||
* If you need a specific layout pattern, create a new component.
|
|
||||||
*/
|
|
||||||
|
|
||||||
interface LayoutProps {
|
|
||||||
children: ReactNode;
|
children: ReactNode;
|
||||||
padding?: string;
|
header?: ReactNode;
|
||||||
gap?: string;
|
footer?: ReactNode;
|
||||||
grid?: boolean;
|
sidebar?: ReactNode;
|
||||||
gridCols?: 1 | 2 | 3 | 4;
|
|
||||||
flex?: boolean;
|
|
||||||
flexCol?: boolean;
|
|
||||||
items?: 'start' | 'center' | 'end' | 'stretch';
|
|
||||||
justify?: 'start' | 'center' | 'end' | 'between' | 'around';
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function Layout({
|
export const Layout = ({
|
||||||
children,
|
children,
|
||||||
padding = 'p-6',
|
header,
|
||||||
gap = 'gap-4',
|
footer,
|
||||||
grid = false,
|
sidebar
|
||||||
gridCols = 1,
|
}: LayoutProps) => {
|
||||||
flex = false,
|
|
||||||
flexCol = false,
|
|
||||||
items = 'start',
|
|
||||||
justify = 'start'
|
|
||||||
}: LayoutProps) {
|
|
||||||
if (grid) {
|
|
||||||
return (
|
|
||||||
<Grid
|
|
||||||
cols={gridCols as any}
|
|
||||||
className={`${padding} ${gap}`}
|
|
||||||
>
|
|
||||||
{children}
|
|
||||||
</Grid>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (flex) {
|
|
||||||
return (
|
|
||||||
<Stack
|
|
||||||
direction={flexCol ? 'col' : 'row'}
|
|
||||||
align={items}
|
|
||||||
justify={justify}
|
|
||||||
className={`${padding} ${gap}`}
|
|
||||||
>
|
|
||||||
{children}
|
|
||||||
</Stack>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box className={`${padding} ${gap}`}>
|
<Box display="flex" flexDirection="col" minHeight="100vh" bg="var(--ui-color-bg-base)">
|
||||||
{children}
|
{header && (
|
||||||
|
<Box as="header" position="sticky" top="0" zIndex={50}>
|
||||||
|
{header}
|
||||||
|
</Box>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<Box display="flex" flex={1}>
|
||||||
|
{sidebar && (
|
||||||
|
<Box as="aside" width="16rem" display={{ base: 'none', lg: 'block' }}>
|
||||||
|
{sidebar}
|
||||||
|
</Box>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<Box as="main" flex={1}>
|
||||||
|
{children}
|
||||||
|
</Box>
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
{footer && (
|
||||||
|
<Box as="footer">
|
||||||
|
{footer}
|
||||||
|
</Box>
|
||||||
|
)}
|
||||||
</Box>
|
</Box>
|
||||||
);
|
);
|
||||||
}
|
};
|
||||||
|
|||||||
@@ -1,16 +1,31 @@
|
|||||||
import React, { ReactNode } from 'react';
|
import React, { ReactNode } from 'react';
|
||||||
import { Box } from './primitives/Box';
|
import { Box } from './primitives/Box';
|
||||||
|
import { Surface } from './primitives/Surface';
|
||||||
|
|
||||||
interface LeaderboardListProps {
|
export interface LeaderboardListProps {
|
||||||
children: ReactNode;
|
children: ReactNode;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function LeaderboardList({ children }: LeaderboardListProps) {
|
export const LeaderboardList = ({ children }: LeaderboardListProps) => {
|
||||||
return (
|
return (
|
||||||
<Box rounded="xl" bg="bg-iron-gray/30" border={true} borderColor="border-charcoal-outline" overflow="hidden">
|
<Surface variant="muted" rounded="xl" style={{ border: '1px solid var(--ui-color-border-default)', overflow: 'hidden' }}>
|
||||||
<div className="divide-y divide-charcoal-outline/50">
|
<Box display="flex" flexDirection="col">
|
||||||
{children}
|
{children}
|
||||||
</div>
|
</Box>
|
||||||
|
</Surface>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const LeaderboardListItem = ({ children, onClick }: { children: ReactNode, onClick?: () => void }) => {
|
||||||
|
return (
|
||||||
|
<Box
|
||||||
|
padding={4}
|
||||||
|
borderBottom
|
||||||
|
onClick={onClick}
|
||||||
|
style={{ cursor: onClick ? 'pointer' : 'default' }}
|
||||||
|
className={onClick ? 'hover:bg-white/5 transition-colors' : ''}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
</Box>
|
</Box>
|
||||||
);
|
);
|
||||||
}
|
};
|
||||||
|
|||||||
@@ -1,70 +1,58 @@
|
|||||||
import { Award, ChevronRight, LucideIcon } from 'lucide-react';
|
import React, { ReactNode } from 'react';
|
||||||
import { ReactNode } from 'react';
|
|
||||||
import { Box } from './primitives/Box';
|
import { Box } from './primitives/Box';
|
||||||
import { Button } from './Button';
|
|
||||||
import { Heading } from './Heading';
|
|
||||||
import { Icon } from './Icon';
|
|
||||||
import { Stack } from './primitives/Stack';
|
|
||||||
import { Text } from './Text';
|
import { Text } from './Text';
|
||||||
|
import { Surface } from './primitives/Surface';
|
||||||
|
import { Icon } from './Icon';
|
||||||
|
import { LucideIcon } from 'lucide-react';
|
||||||
|
|
||||||
interface LeaderboardPreviewShellProps {
|
export interface LeaderboardPreviewShellProps {
|
||||||
title: string;
|
title: string;
|
||||||
subtitle: string;
|
subtitle?: string;
|
||||||
onViewFull: () => void;
|
icon: LucideIcon;
|
||||||
|
intent?: 'primary' | 'success' | 'warning' | 'critical' | 'telemetry';
|
||||||
children: ReactNode;
|
children: ReactNode;
|
||||||
icon?: LucideIcon;
|
footer?: ReactNode;
|
||||||
iconColor?: string;
|
|
||||||
iconBgGradient?: string;
|
|
||||||
viewFullLabel?: string;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function LeaderboardPreviewShell({
|
export const LeaderboardPreviewShell = ({
|
||||||
title,
|
title,
|
||||||
subtitle,
|
subtitle,
|
||||||
onViewFull,
|
icon,
|
||||||
|
intent = 'primary',
|
||||||
children,
|
children,
|
||||||
icon = Award,
|
footer
|
||||||
iconColor = "#facc15",
|
}: LeaderboardPreviewShellProps) => {
|
||||||
iconBgGradient = 'linear-gradient(to bottom right, rgba(250, 204, 21, 0.2), rgba(217, 119, 6, 0.1))',
|
|
||||||
viewFullLabel = "View Full Leaderboard",
|
|
||||||
}: LeaderboardPreviewShellProps) {
|
|
||||||
return (
|
return (
|
||||||
<Box mb={12}>
|
<Surface variant="default" rounded="xl" style={{ border: '1px solid var(--ui-color-border-default)', overflow: 'hidden' }}>
|
||||||
{/* Header */}
|
<Box padding={6} borderBottom>
|
||||||
<Stack direction="row" align="center" justify="between" mb={4}>
|
<Box display="flex" alignItems="center" justifyContent="between" marginBottom={4}>
|
||||||
<Stack direction="row" align="center" gap={3}>
|
<Box display="flex" alignItems="center" gap={4}>
|
||||||
<Box
|
<Box padding={2} rounded="lg" bg="var(--ui-color-bg-surface-muted)">
|
||||||
display="flex"
|
<Icon icon={icon} size={5} intent={intent} />
|
||||||
h="11"
|
</Box>
|
||||||
w="11"
|
<Box>
|
||||||
alignItems="center"
|
<Text size="lg" weight="bold" variant="high" block>
|
||||||
justifyContent="center"
|
{title}
|
||||||
rounded="xl"
|
</Text>
|
||||||
style={{ background: iconBgGradient, border: `1px solid ${iconColor}4D` }}
|
{subtitle && (
|
||||||
>
|
<Text size="sm" variant="low">
|
||||||
<Icon icon={icon} size={5} color={iconColor} />
|
{subtitle}
|
||||||
|
</Text>
|
||||||
|
)}
|
||||||
|
</Box>
|
||||||
</Box>
|
</Box>
|
||||||
<Box>
|
</Box>
|
||||||
<Heading level={2}>{title}</Heading>
|
|
||||||
<Text size="sm" color="text-gray-500">{subtitle}</Text>
|
|
||||||
</Box>
|
|
||||||
</Stack>
|
|
||||||
|
|
||||||
<Button
|
|
||||||
variant="secondary"
|
|
||||||
onClick={onViewFull}
|
|
||||||
icon={<Icon icon={ChevronRight} size={4} />}
|
|
||||||
>
|
|
||||||
{viewFullLabel}
|
|
||||||
</Button>
|
|
||||||
</Stack>
|
|
||||||
|
|
||||||
{/* Compact Leaderboard */}
|
|
||||||
<Box rounded="xl" bg="bg-iron-gray/30" border borderColor="border-charcoal-outline/80" overflow="hidden">
|
|
||||||
<Stack gap={0}>
|
|
||||||
{children}
|
|
||||||
</Stack>
|
|
||||||
</Box>
|
</Box>
|
||||||
</Box>
|
|
||||||
|
<Box>
|
||||||
|
{children}
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
{footer && (
|
||||||
|
<Box padding={4} borderTop bg="rgba(255,255,255,0.02)">
|
||||||
|
{footer}
|
||||||
|
</Box>
|
||||||
|
)}
|
||||||
|
</Surface>
|
||||||
);
|
);
|
||||||
}
|
};
|
||||||
|
|||||||
@@ -1,49 +1,17 @@
|
|||||||
import React from 'react';
|
import React, { ReactNode } from 'react';
|
||||||
import { Box } from './primitives/Box';
|
import { Box } from './primitives/Box';
|
||||||
|
import { Surface } from './primitives/Surface';
|
||||||
|
|
||||||
interface LeaderboardTableShellProps {
|
export interface LeaderboardTableShellProps {
|
||||||
columns: {
|
children: ReactNode;
|
||||||
key: string;
|
|
||||||
label: string;
|
|
||||||
align?: 'left' | 'center' | 'right';
|
|
||||||
width?: string;
|
|
||||||
}[];
|
|
||||||
children: React.ReactNode;
|
|
||||||
className?: string;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function LeaderboardTableShell({ columns, children, className = '' }: LeaderboardTableShellProps) {
|
export const LeaderboardTableShell = ({ children }: LeaderboardTableShellProps) => {
|
||||||
return (
|
return (
|
||||||
<Box
|
<Surface variant="default" rounded="xl" style={{ border: '1px solid var(--ui-color-border-default)', overflow: 'hidden' }}>
|
||||||
rounded="xl"
|
<Box>
|
||||||
bg="bg-iron-gray/30"
|
{children}
|
||||||
border
|
</Box>
|
||||||
borderColor="border-charcoal-outline"
|
</Surface>
|
||||||
overflow="hidden"
|
|
||||||
className={className}
|
|
||||||
>
|
|
||||||
<div className="overflow-x-auto">
|
|
||||||
<table className="w-full text-left border-collapse">
|
|
||||||
<thead>
|
|
||||||
<tr className="border-b border-charcoal-outline/50 bg-graphite-black/50">
|
|
||||||
{columns.map((col) => (
|
|
||||||
<th
|
|
||||||
key={col.key}
|
|
||||||
className={`px-4 py-3 text-[10px] uppercase tracking-widest font-bold text-gray-500 ${
|
|
||||||
col.align === 'center' ? 'text-center' : col.align === 'right' ? 'text-right' : 'text-left'
|
|
||||||
}`}
|
|
||||||
style={col.width ? { width: col.width } : {}}
|
|
||||||
>
|
|
||||||
{col.label}
|
|
||||||
</th>
|
|
||||||
))}
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody className="divide-y divide-charcoal-outline/30">
|
|
||||||
{children}
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
</div>
|
|
||||||
</Box>
|
|
||||||
);
|
);
|
||||||
}
|
};
|
||||||
|
|||||||
@@ -1,86 +1,66 @@
|
|||||||
import React, { ReactNode } from 'react';
|
import React, { ReactNode, forwardRef, AnchorHTMLAttributes } from 'react';
|
||||||
import { Box, BoxProps } from './primitives/Box';
|
|
||||||
|
|
||||||
export interface LinkProps extends Omit<BoxProps<'a'>, 'children' | 'onClick'> {
|
export interface LinkProps extends AnchorHTMLAttributes<HTMLAnchorElement> {
|
||||||
href: string;
|
|
||||||
children: ReactNode;
|
children: ReactNode;
|
||||||
variant?: 'primary' | 'secondary' | 'ghost';
|
variant?: 'primary' | 'secondary' | 'ghost' | 'inherit';
|
||||||
size?: 'xs' | 'sm' | 'md' | 'lg';
|
underline?: 'always' | 'hover' | 'none';
|
||||||
target?: '_blank' | '_self' | '_parent' | '_top';
|
size?: string;
|
||||||
rel?: string;
|
weight?: string;
|
||||||
onClick?: React.MouseEventHandler<HTMLAnchorElement>;
|
letterSpacing?: string;
|
||||||
block?: boolean;
|
block?: boolean;
|
||||||
weight?: 'light' | 'normal' | 'medium' | 'semibold' | 'bold' | string;
|
|
||||||
truncate?: boolean;
|
|
||||||
hoverColor?: string;
|
hoverColor?: string;
|
||||||
transition?: boolean;
|
transition?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function Link({
|
export const Link = forwardRef<HTMLAnchorElement, LinkProps>(({
|
||||||
href,
|
|
||||||
children,
|
children,
|
||||||
className = '',
|
|
||||||
variant = 'primary',
|
variant = 'primary',
|
||||||
size = 'md',
|
underline = 'hover',
|
||||||
target = '_self',
|
size,
|
||||||
rel = '',
|
|
||||||
onClick,
|
|
||||||
block = false,
|
|
||||||
weight,
|
weight,
|
||||||
truncate,
|
letterSpacing,
|
||||||
|
block = false,
|
||||||
hoverColor,
|
hoverColor,
|
||||||
transition,
|
transition = true,
|
||||||
...props
|
...props
|
||||||
}: LinkProps) {
|
}, ref) => {
|
||||||
const baseClasses = 'inline-flex items-center transition-colors';
|
|
||||||
|
|
||||||
const variantClasses = {
|
const variantClasses = {
|
||||||
primary: 'text-primary-accent hover:text-primary-accent/80',
|
primary: 'text-[var(--ui-color-intent-primary)] hover:opacity-80',
|
||||||
secondary: 'text-telemetry-aqua hover:text-telemetry-aqua/80',
|
secondary: 'text-[var(--ui-color-text-med)] hover:text-[var(--ui-color-text-high)]',
|
||||||
ghost: 'text-gray-400 hover:text-gray-300'
|
ghost: 'text-[var(--ui-color-text-low)] hover:text-[var(--ui-color-text-high)]',
|
||||||
|
inherit: 'text-inherit',
|
||||||
};
|
};
|
||||||
|
|
||||||
const sizeClasses = {
|
const underlineClasses = {
|
||||||
xs: 'text-xs',
|
always: 'underline',
|
||||||
sm: 'text-sm',
|
hover: 'hover:underline',
|
||||||
md: 'text-base',
|
none: 'no-underline',
|
||||||
lg: 'text-lg'
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const weightClasses: Record<string, string> = {
|
|
||||||
light: 'font-light',
|
|
||||||
normal: 'font-normal',
|
|
||||||
medium: 'font-medium',
|
|
||||||
semibold: 'font-semibold',
|
|
||||||
bold: 'font-bold'
|
|
||||||
};
|
|
||||||
|
|
||||||
const classes = [
|
const classes = [
|
||||||
block ? 'flex' : baseClasses,
|
transition ? 'transition-all duration-150 ease-in-out' : '',
|
||||||
|
'cursor-pointer',
|
||||||
|
block ? 'block' : 'inline',
|
||||||
variantClasses[variant],
|
variantClasses[variant],
|
||||||
sizeClasses[size],
|
underlineClasses[underline],
|
||||||
weight && weightClasses[weight] ? weightClasses[weight] : '',
|
].join(' ');
|
||||||
truncate ? 'truncate' : '',
|
|
||||||
hoverColor ? `hover:${hoverColor}` : '',
|
const style: React.CSSProperties = {
|
||||||
transition ? 'transition-all duration-150' : '',
|
...(size ? { fontSize: size } : {}),
|
||||||
className
|
...(weight ? { fontWeight: weight } : {}),
|
||||||
].filter(Boolean).join(' ');
|
...(letterSpacing ? { letterSpacing } : {}),
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box
|
<a
|
||||||
as="a"
|
ref={ref}
|
||||||
href={href}
|
|
||||||
className={classes}
|
className={classes}
|
||||||
target={target}
|
style={style}
|
||||||
rel={rel}
|
|
||||||
onClick={onClick}
|
|
||||||
style={{
|
|
||||||
...(weight && !weightClasses[weight] ? { fontWeight: weight } : {}),
|
|
||||||
...(props.style || {})
|
|
||||||
}}
|
|
||||||
{...props}
|
{...props}
|
||||||
>
|
>
|
||||||
{children}
|
{children}
|
||||||
</Box>
|
</a>
|
||||||
);
|
);
|
||||||
}
|
});
|
||||||
|
|
||||||
|
Link.displayName = 'Link';
|
||||||
|
|||||||
@@ -1,26 +1,41 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { Box } from './primitives/Box';
|
import { Box } from './primitives/Box';
|
||||||
|
|
||||||
interface LoadingSpinnerProps {
|
export interface LoadingSpinnerProps {
|
||||||
size?: number;
|
size?: 'sm' | 'md' | 'lg' | number;
|
||||||
color?: string;
|
intent?: 'primary' | 'high' | 'low';
|
||||||
className?: string;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function LoadingSpinner({ size = 8, color = '#3b82f6', className = '' }: LoadingSpinnerProps) {
|
export const LoadingSpinner = ({
|
||||||
|
size = 'md',
|
||||||
|
intent = 'primary'
|
||||||
|
}: LoadingSpinnerProps) => {
|
||||||
|
const sizeMap = {
|
||||||
|
sm: '1rem',
|
||||||
|
md: '2rem',
|
||||||
|
lg: '3rem',
|
||||||
|
};
|
||||||
|
|
||||||
|
const dimension = typeof size === 'string' ? sizeMap[size] : `${size * 0.25}rem`;
|
||||||
|
|
||||||
|
const intentColorMap = {
|
||||||
|
primary: 'var(--ui-color-intent-primary)',
|
||||||
|
high: 'var(--ui-color-text-high)',
|
||||||
|
low: 'var(--ui-color-text-low)',
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box
|
<Box
|
||||||
w={`${size * 0.25}rem`}
|
width={dimension}
|
||||||
h={`${size * 0.25}rem`}
|
height={dimension}
|
||||||
rounded="full"
|
style={{
|
||||||
borderWidth="2px"
|
border: '2px solid rgba(255, 255, 255, 0.1)',
|
||||||
borderStyle="solid"
|
borderTop: `2px solid ${intentColorMap[intent]}`,
|
||||||
borderColor="transparent"
|
borderRadius: '50%',
|
||||||
borderTopColor={color}
|
}}
|
||||||
borderLeftColor={color}
|
className="animate-spin"
|
||||||
className={`animate-spin ${className}`}
|
|
||||||
role="status"
|
role="status"
|
||||||
aria-label="Loading"
|
aria-label="Loading"
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
};
|
||||||
|
|||||||
@@ -1,9 +1,14 @@
|
|||||||
import React from 'react';
|
import React, { ReactNode } from 'react';
|
||||||
|
import { Box } from './primitives/Box';
|
||||||
|
|
||||||
interface MainContentProps {
|
export interface MainContentProps {
|
||||||
children: React.ReactNode;
|
children: ReactNode;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function MainContent({ children }: MainContentProps) {
|
export const MainContent = ({ children }: MainContentProps) => {
|
||||||
return <div className="pt-16 md:pt-20">{children}</div>;
|
return (
|
||||||
}
|
<Box as="main" flex={1} display="flex" flexDirection="col" minHeight="0">
|
||||||
|
{children}
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|||||||
@@ -1,64 +1,60 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { LucideIcon } from 'lucide-react';
|
import { Card } from './Card';
|
||||||
import { Box } from './primitives/Box';
|
import { Box } from './primitives/Box';
|
||||||
import { Text } from './Text';
|
import { Text } from './Text';
|
||||||
import { Icon } from './Icon';
|
import { Icon } from './Icon';
|
||||||
|
import { LucideIcon } from 'lucide-react';
|
||||||
|
|
||||||
interface MetricCardProps {
|
export interface MetricCardProps {
|
||||||
label: string;
|
label: string;
|
||||||
value: string | number;
|
value: string | number;
|
||||||
icon?: LucideIcon;
|
icon?: LucideIcon;
|
||||||
color?: string;
|
intent?: 'primary' | 'success' | 'warning' | 'critical' | 'telemetry';
|
||||||
trend?: {
|
trend?: {
|
||||||
value: number;
|
value: number;
|
||||||
isPositive: boolean;
|
isPositive: boolean;
|
||||||
};
|
};
|
||||||
border?: boolean;
|
|
||||||
bg?: string;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
export const MetricCard = ({
|
||||||
* A semantic component for displaying metrics.
|
label,
|
||||||
* Instrument-grade typography and dense-but-readable hierarchy.
|
value,
|
||||||
*/
|
icon,
|
||||||
export function MetricCard({
|
intent = 'primary',
|
||||||
label,
|
trend
|
||||||
value,
|
}: MetricCardProps) => {
|
||||||
icon,
|
|
||||||
color = 'text-primary-accent',
|
|
||||||
trend,
|
|
||||||
border = true,
|
|
||||||
bg = 'panel-gray/40',
|
|
||||||
}: MetricCardProps) {
|
|
||||||
return (
|
return (
|
||||||
<Box
|
<Card variant="default">
|
||||||
bg={bg}
|
<Box display="flex" alignItems="start" justifyContent="between" marginBottom={4}>
|
||||||
rounded="none"
|
<Box>
|
||||||
p={4}
|
<Text size="xs" weight="bold" variant="low" uppercase>
|
||||||
border={border}
|
|
||||||
borderColor="border-gray/30"
|
|
||||||
display="flex"
|
|
||||||
flexDirection="col"
|
|
||||||
gap={2}
|
|
||||||
hoverBg="panel-gray/60"
|
|
||||||
transition
|
|
||||||
>
|
|
||||||
<Box display="flex" alignItems="center" justifyContent="between">
|
|
||||||
<Box display="flex" alignItems="center" gap={2}>
|
|
||||||
{icon && <Icon icon={icon} size={4} className={color} />}
|
|
||||||
<Text size="xs" weight="bold" color="text-gray-500" uppercase letterSpacing="widest">
|
|
||||||
{label}
|
{label}
|
||||||
</Text>
|
</Text>
|
||||||
</Box>
|
<Text size="2xl" weight="bold" variant="high" block marginTop={1}>
|
||||||
{trend && (
|
{value}
|
||||||
<Text size="xs" color={trend.isPositive ? 'text-success-green' : 'text-red-400'} font="mono">
|
|
||||||
{trend.isPositive ? '▲' : '▼'} {trend.value}%
|
|
||||||
</Text>
|
</Text>
|
||||||
|
</Box>
|
||||||
|
{icon && (
|
||||||
|
<Box padding={2} rounded="lg" bg="var(--ui-color-bg-surface-muted)">
|
||||||
|
<Icon icon={icon} size={5} intent={intent} />
|
||||||
|
</Box>
|
||||||
)}
|
)}
|
||||||
</Box>
|
</Box>
|
||||||
<Text size="2xl" weight="bold" color="text-white" font="mono">
|
|
||||||
{typeof value === 'number' ? value.toLocaleString() : value}
|
{trend && (
|
||||||
</Text>
|
<Box display="flex" alignItems="center" gap={1}>
|
||||||
</Box>
|
<Text
|
||||||
|
size="xs"
|
||||||
|
weight="bold"
|
||||||
|
variant={trend.isPositive ? 'success' : 'critical'}
|
||||||
|
>
|
||||||
|
{trend.isPositive ? '+' : '-'}{Math.abs(trend.value)}%
|
||||||
|
</Text>
|
||||||
|
<Text size="xs" variant="low">
|
||||||
|
vs last period
|
||||||
|
</Text>
|
||||||
|
</Box>
|
||||||
|
)}
|
||||||
|
</Card>
|
||||||
);
|
);
|
||||||
}
|
};
|
||||||
|
|||||||
@@ -2,17 +2,21 @@ import React from 'react';
|
|||||||
import { Box } from './primitives/Box';
|
import { Box } from './primitives/Box';
|
||||||
import { Text } from './Text';
|
import { Text } from './Text';
|
||||||
|
|
||||||
interface MiniStatProps {
|
export interface MiniStatProps {
|
||||||
label: string;
|
label: string;
|
||||||
value: string | number;
|
value: string | number;
|
||||||
color?: string;
|
intent?: 'primary' | 'success' | 'warning' | 'critical' | 'high' | 'med' | 'low';
|
||||||
}
|
}
|
||||||
|
|
||||||
export function MiniStat({ label, value, color = 'text-white' }: MiniStatProps) {
|
export const MiniStat = ({
|
||||||
|
label,
|
||||||
|
value,
|
||||||
|
intent = 'high'
|
||||||
|
}: MiniStatProps) => {
|
||||||
return (
|
return (
|
||||||
<Box textAlign="center" p={2} rounded="lg" bg="bg-charcoal-outline/30">
|
<Box textAlign="center" padding={2} rounded="lg" bg="var(--ui-color-bg-surface-muted)">
|
||||||
<Text size="lg" weight="bold" color={color} block>{value}</Text>
|
<Text size="lg" weight="bold" variant={intent} block>{value}</Text>
|
||||||
<Text size="xs" color="text-gray-500" block style={{ fontSize: '10px' }}>{label}</Text>
|
<Text size="xs" variant="low" block style={{ fontSize: '10px' }}>{label}</Text>
|
||||||
</Box>
|
</Box>
|
||||||
);
|
);
|
||||||
}
|
};
|
||||||
|
|||||||
@@ -1,51 +1,63 @@
|
|||||||
import React, { ReactNode } from 'react';
|
import React, { ReactNode, useEffect } from 'react';
|
||||||
|
import { createPortal } from 'react-dom';
|
||||||
import { Box } from './primitives/Box';
|
import { Box } from './primitives/Box';
|
||||||
import { Stack } from './primitives/Stack';
|
import { Surface } from './primitives/Surface';
|
||||||
import { Button } from './Button';
|
|
||||||
import { Text } from './Text';
|
|
||||||
import { X } from 'lucide-react';
|
|
||||||
import { IconButton } from './IconButton';
|
import { IconButton } from './IconButton';
|
||||||
|
import { Button } from './Button';
|
||||||
|
import { X } from 'lucide-react';
|
||||||
|
import { Heading } from './Heading';
|
||||||
|
import { Text } from './Text';
|
||||||
|
|
||||||
interface ModalProps {
|
export interface ModalProps {
|
||||||
|
children: ReactNode;
|
||||||
isOpen: boolean;
|
isOpen: boolean;
|
||||||
onClose?: () => void;
|
onClose?: () => void;
|
||||||
onOpenChange?: (open: boolean) => void;
|
onOpenChange?: (isOpen: boolean) => void;
|
||||||
title?: string;
|
title?: string;
|
||||||
description?: string;
|
size?: 'sm' | 'md' | 'lg' | 'xl' | 'full';
|
||||||
icon?: React.ReactNode;
|
|
||||||
children: ReactNode;
|
|
||||||
footer?: ReactNode;
|
|
||||||
primaryActionLabel?: string;
|
primaryActionLabel?: string;
|
||||||
onPrimaryAction?: () => void;
|
onPrimaryAction?: () => void;
|
||||||
secondaryActionLabel?: string;
|
secondaryActionLabel?: string;
|
||||||
onSecondaryAction?: () => void;
|
onSecondaryAction?: () => void;
|
||||||
isLoading?: boolean;
|
footer?: ReactNode;
|
||||||
size?: 'sm' | 'md' | 'lg' | 'xl';
|
description?: string;
|
||||||
|
icon?: ReactNode;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function Modal({
|
export const Modal = ({
|
||||||
isOpen,
|
children,
|
||||||
onClose,
|
isOpen,
|
||||||
|
onClose,
|
||||||
onOpenChange,
|
onOpenChange,
|
||||||
title,
|
title,
|
||||||
description,
|
size = 'md',
|
||||||
icon,
|
|
||||||
children,
|
|
||||||
footer,
|
|
||||||
primaryActionLabel,
|
primaryActionLabel,
|
||||||
onPrimaryAction,
|
onPrimaryAction,
|
||||||
secondaryActionLabel,
|
secondaryActionLabel,
|
||||||
onSecondaryAction,
|
onSecondaryAction,
|
||||||
isLoading = false,
|
footer,
|
||||||
size = 'md',
|
description,
|
||||||
}: ModalProps) {
|
icon
|
||||||
|
}: ModalProps) => {
|
||||||
|
useEffect(() => {
|
||||||
|
if (isOpen) {
|
||||||
|
document.body.style.overflow = 'hidden';
|
||||||
|
} else {
|
||||||
|
document.body.style.overflow = 'unset';
|
||||||
|
}
|
||||||
|
return () => {
|
||||||
|
document.body.style.overflow = 'unset';
|
||||||
|
};
|
||||||
|
}, [isOpen]);
|
||||||
|
|
||||||
if (!isOpen) return null;
|
if (!isOpen) return null;
|
||||||
|
|
||||||
const sizeMap = {
|
const sizeMap = {
|
||||||
sm: 'max-w-md',
|
sm: '24rem',
|
||||||
md: 'max-w-lg',
|
md: '32rem',
|
||||||
lg: 'max-w-2xl',
|
lg: '48rem',
|
||||||
xl: 'max-w-4xl',
|
xl: '64rem',
|
||||||
|
full: '100%',
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleClose = () => {
|
const handleClose = () => {
|
||||||
@@ -53,96 +65,82 @@ export function Modal({
|
|||||||
if (onOpenChange) onOpenChange(false);
|
if (onOpenChange) onOpenChange(false);
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return createPortal(
|
||||||
<Box
|
<Box
|
||||||
position="fixed"
|
position="fixed"
|
||||||
inset={0}
|
inset={0}
|
||||||
zIndex={60}
|
zIndex={100}
|
||||||
display="flex"
|
display="flex"
|
||||||
alignItems="center"
|
alignItems="center"
|
||||||
justifyContent="center"
|
justifyContent="center"
|
||||||
bg="bg-black/60"
|
padding={4}
|
||||||
px={4}
|
bg="rgba(0, 0, 0, 0.8)"
|
||||||
className="backdrop-blur-sm"
|
|
||||||
role="dialog"
|
|
||||||
aria-modal="true"
|
|
||||||
>
|
>
|
||||||
{/* Backdrop click to close */}
|
<Box
|
||||||
<Box position="absolute" inset={0} onClick={handleClose} />
|
position="absolute"
|
||||||
|
inset={0}
|
||||||
<Box
|
onClick={handleClose}
|
||||||
position="relative"
|
/>
|
||||||
w="full"
|
|
||||||
maxWidth={sizeMap[size]}
|
<Surface
|
||||||
rounded="2xl"
|
variant="default"
|
||||||
bg="bg-[#0f1115]"
|
rounded="lg"
|
||||||
border
|
shadow="xl"
|
||||||
borderColor="border-[#262626]"
|
style={{
|
||||||
shadow="2xl"
|
width: '100%',
|
||||||
overflow="hidden"
|
maxWidth: sizeMap[size],
|
||||||
tabIndex={-1}
|
maxHeight: '90vh',
|
||||||
|
overflow: 'hidden',
|
||||||
|
display: 'flex',
|
||||||
|
flexDirection: 'column',
|
||||||
|
position: 'relative',
|
||||||
|
border: '1px solid var(--ui-color-border-default)'
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
{/* Header */}
|
<Box
|
||||||
<Box p={6} borderBottom borderColor="border-white/5">
|
display="flex"
|
||||||
<Stack direction="row" align="center" justify="between">
|
alignItems="center"
|
||||||
<Stack direction="row" align="center" gap={3}>
|
justifyContent="between"
|
||||||
{icon && <Box>{icon}</Box>}
|
padding={4}
|
||||||
<Box>
|
borderBottom
|
||||||
{title && (
|
>
|
||||||
<Text size="xl" weight="bold" color="text-white" block>
|
<Box display="flex" alignItems="center" gap={3}>
|
||||||
{title}
|
{icon}
|
||||||
</Text>
|
<Box>
|
||||||
)}
|
{title && <Heading level={3}>{title}</Heading>}
|
||||||
{description && (
|
{description && <Box marginTop={1}><Text size="sm" variant="low">{description}</Text></Box>}
|
||||||
<Text size="sm" color="text-gray-400" block mt={1}>
|
</Box>
|
||||||
{description}
|
</Box>
|
||||||
</Text>
|
<IconButton icon={X} onClick={handleClose} variant="ghost" title="Close modal" />
|
||||||
)}
|
|
||||||
</Box>
|
|
||||||
</Stack>
|
|
||||||
<IconButton
|
|
||||||
icon={X}
|
|
||||||
onClick={handleClose}
|
|
||||||
variant="ghost"
|
|
||||||
size="sm"
|
|
||||||
title="Close modal"
|
|
||||||
/>
|
|
||||||
</Stack>
|
|
||||||
</Box>
|
</Box>
|
||||||
|
|
||||||
{/* Content */}
|
<Box flex={1} overflow="auto" padding={6}>
|
||||||
<Box p={6} overflowY="auto" maxHeight="calc(100vh - 200px)">
|
|
||||||
{children}
|
{children}
|
||||||
</Box>
|
</Box>
|
||||||
|
|
||||||
{/* Footer */}
|
{(footer || primaryActionLabel || secondaryActionLabel) && (
|
||||||
{(primaryActionLabel || secondaryActionLabel || footer) && (
|
<Box padding={4} borderTop bg="rgba(255,255,255,0.02)" display="flex" justifyContent="end" gap={3}>
|
||||||
<Box p={6} borderTop borderColor="border-white/5">
|
{footer}
|
||||||
{footer || (
|
{secondaryActionLabel && (
|
||||||
<Stack direction="row" justify="end" gap={3}>
|
<Button
|
||||||
{secondaryActionLabel && (
|
onClick={onSecondaryAction || handleClose}
|
||||||
<Button
|
variant="ghost"
|
||||||
variant="ghost"
|
>
|
||||||
onClick={onSecondaryAction || onClose}
|
{secondaryActionLabel}
|
||||||
disabled={isLoading}
|
</Button>
|
||||||
>
|
)}
|
||||||
{secondaryActionLabel}
|
{primaryActionLabel && (
|
||||||
</Button>
|
<Button
|
||||||
)}
|
onClick={onPrimaryAction}
|
||||||
{primaryActionLabel && (
|
variant="primary"
|
||||||
<Button
|
>
|
||||||
variant="primary"
|
{primaryActionLabel}
|
||||||
onClick={onPrimaryAction}
|
</Button>
|
||||||
isLoading={isLoading}
|
|
||||||
>
|
|
||||||
{primaryActionLabel}
|
|
||||||
</Button>
|
|
||||||
)}
|
|
||||||
</Stack>
|
|
||||||
)}
|
)}
|
||||||
</Box>
|
</Box>
|
||||||
)}
|
)}
|
||||||
</Box>
|
</Surface>
|
||||||
</Box>
|
</Box>,
|
||||||
|
document.body
|
||||||
);
|
);
|
||||||
}
|
};
|
||||||
|
|||||||
@@ -1,129 +1,56 @@
|
|||||||
import React from 'react';
|
import React, { ReactNode } from 'react';
|
||||||
import { LucideIcon } from 'lucide-react';
|
|
||||||
import { Heading } from './Heading';
|
|
||||||
import { Button } from './Button';
|
|
||||||
import { Box } from './primitives/Box';
|
import { Box } from './primitives/Box';
|
||||||
import { Stack } from './primitives/Stack';
|
import { Heading } from './Heading';
|
||||||
import { Text } from './Text';
|
import { Text } from './Text';
|
||||||
import { Icon } from './Icon';
|
import { Surface } from './primitives/Surface';
|
||||||
import { ModalIcon } from './ModalIcon';
|
|
||||||
|
|
||||||
interface PageHeroProps {
|
export interface PageHeroProps {
|
||||||
title: string;
|
title: string;
|
||||||
description?: string;
|
description?: string;
|
||||||
icon?: LucideIcon;
|
children?: ReactNode;
|
||||||
backgroundPattern?: React.ReactNode;
|
image?: ReactNode;
|
||||||
stats?: Array<{
|
|
||||||
icon?: LucideIcon;
|
|
||||||
value: string | number;
|
|
||||||
label: string;
|
|
||||||
color?: string;
|
|
||||||
animate?: boolean;
|
|
||||||
}>;
|
|
||||||
actions?: Array<{
|
|
||||||
label: string;
|
|
||||||
onClick: () => void;
|
|
||||||
variant?: 'primary' | 'secondary';
|
|
||||||
icon?: LucideIcon;
|
|
||||||
description?: string;
|
|
||||||
}>;
|
|
||||||
children?: React.ReactNode;
|
|
||||||
className?: string;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export const PageHero = ({
|
export const PageHero = ({
|
||||||
title,
|
title,
|
||||||
description,
|
description,
|
||||||
icon,
|
|
||||||
backgroundPattern,
|
|
||||||
stats,
|
|
||||||
actions,
|
|
||||||
children,
|
children,
|
||||||
className = ''
|
image
|
||||||
}: PageHeroProps) => (
|
}: PageHeroProps) => {
|
||||||
<Box
|
return (
|
||||||
as="section"
|
<Surface
|
||||||
position="relative"
|
variant="dark"
|
||||||
overflow="hidden"
|
rounded="xl"
|
||||||
rounded="2xl"
|
padding={8}
|
||||||
bg="bg-gradient-to-br from-iron-gray/80 via-deep-graphite to-iron-gray/60"
|
style={{ position: 'relative', overflow: 'hidden', border: '1px solid var(--ui-color-border-default)' }}
|
||||||
border={true}
|
>
|
||||||
borderColor="border-charcoal-outline/50"
|
<Box display="flex" flexDirection={{ base: 'col', lg: 'row' }} alignItems="center" gap={8}>
|
||||||
className={className}
|
<Box flex={1}>
|
||||||
>
|
<Heading level={1} marginBottom={4}>{title}</Heading>
|
||||||
{/* Background Pattern */}
|
|
||||||
{backgroundPattern || (
|
|
||||||
<>
|
|
||||||
<Box position="absolute" top="0" right="0" width="96" height="96" bg="bg-primary-blue/5" rounded="full" blur="3xl" />
|
|
||||||
<Box position="absolute" bottom="0" left="0" width="64" height="64" bg="bg-neon-aqua/5" rounded="full" blur="3xl" />
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<Box position="relative" maxWidth="7xl" mx="auto" px={8} py={10}>
|
|
||||||
<Box display="flex" flexDirection={{ base: 'col', lg: 'row' }} alignItems={{ lg: 'center' }} justifyContent="between" gap={8}>
|
|
||||||
{/* Main Content */}
|
|
||||||
<Box maxWidth="2xl">
|
|
||||||
{icon && (
|
|
||||||
<Stack direction="row" align="center" gap={3} mb={4}>
|
|
||||||
<ModalIcon icon={icon} />
|
|
||||||
<Heading level={1}>
|
|
||||||
{title}
|
|
||||||
</Heading>
|
|
||||||
</Stack>
|
|
||||||
)}
|
|
||||||
{!icon && (
|
|
||||||
<Heading level={1} mb={4}>
|
|
||||||
{title}
|
|
||||||
</Heading>
|
|
||||||
)}
|
|
||||||
{description && (
|
{description && (
|
||||||
<Text size="lg" color="text-gray-400" block style={{ lineHeight: 1.625 }}>
|
<Text size="lg" variant="low" marginBottom={6} block>
|
||||||
{description}
|
{description}
|
||||||
</Text>
|
</Text>
|
||||||
)}
|
)}
|
||||||
|
{children}
|
||||||
{/* Stats */}
|
|
||||||
{stats && stats.length > 0 && (
|
|
||||||
<Box display="flex" flexWrap="wrap" gap={6} mt={6}>
|
|
||||||
{stats.map((stat, index) => (
|
|
||||||
<Stack key={index} direction="row" align="center" gap={2}>
|
|
||||||
{stat.icon ? (
|
|
||||||
<Icon icon={stat.icon} size={4} className={stat.color || 'text-primary-blue'} />
|
|
||||||
) : (
|
|
||||||
<Box width="2" height="2" rounded="full" className={`${stat.color || 'bg-primary-blue'} ${stat.animate ? 'animate-pulse' : ''}`} />
|
|
||||||
)}
|
|
||||||
<Text size="sm" color="text-gray-400">
|
|
||||||
<Text color="text-white" weight="semibold">{stat.value}</Text> {stat.label}
|
|
||||||
</Text>
|
|
||||||
</Stack>
|
|
||||||
))}
|
|
||||||
</Box>
|
|
||||||
)}
|
|
||||||
</Box>
|
</Box>
|
||||||
|
{image && (
|
||||||
{/* Actions or Custom Content */}
|
<Box flex={1} display="flex" justifyContent="center">
|
||||||
{actions && actions.length > 0 && (
|
{image}
|
||||||
<Stack gap={4}>
|
</Box>
|
||||||
{actions.map((action, index) => (
|
|
||||||
<Stack key={index} gap={2}>
|
|
||||||
<Button
|
|
||||||
variant={action.variant || 'primary'}
|
|
||||||
onClick={action.onClick}
|
|
||||||
icon={action.icon && <Icon icon={action.icon} size={5} />}
|
|
||||||
className="px-6 py-3"
|
|
||||||
>
|
|
||||||
{action.label}
|
|
||||||
</Button>
|
|
||||||
{action.description && (
|
|
||||||
<Text size="xs" color="text-gray-500" align="center" block>{action.description}</Text>
|
|
||||||
)}
|
|
||||||
</Stack>
|
|
||||||
))}
|
|
||||||
</Stack>
|
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{children}
|
|
||||||
</Box>
|
</Box>
|
||||||
</Box>
|
|
||||||
</Box>
|
{/* Decorative elements */}
|
||||||
);
|
<Box
|
||||||
|
position="absolute"
|
||||||
|
top="-4rem"
|
||||||
|
right="-4rem"
|
||||||
|
width="16rem"
|
||||||
|
height="16rem"
|
||||||
|
bg="var(--ui-color-intent-primary)"
|
||||||
|
style={{ opacity: 0.05, filter: 'blur(64px)', borderRadius: '9999px' }}
|
||||||
|
/>
|
||||||
|
</Surface>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|||||||
@@ -1,88 +1,77 @@
|
|||||||
|
import React from 'react';
|
||||||
|
|
||||||
import { ChevronLeft, ChevronRight } from 'lucide-react';
|
|
||||||
import { Box } from './primitives/Box';
|
import { Box } from './primitives/Box';
|
||||||
import { Button } from './Button';
|
import { Button } from './Button';
|
||||||
import { Icon } from './Icon';
|
|
||||||
import { Stack } from './primitives/Stack';
|
|
||||||
import { Text } from './Text';
|
import { Text } from './Text';
|
||||||
|
import { ChevronLeft, ChevronRight } from 'lucide-react';
|
||||||
|
|
||||||
interface PaginationProps {
|
export interface PaginationProps {
|
||||||
currentPage: number;
|
currentPage: number;
|
||||||
totalPages: number;
|
totalPages: number;
|
||||||
totalItems: number;
|
|
||||||
itemsPerPage: number;
|
|
||||||
onPageChange: (page: number) => void;
|
onPageChange: (page: number) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function Pagination({
|
export const Pagination = ({
|
||||||
currentPage,
|
currentPage,
|
||||||
totalPages,
|
totalPages,
|
||||||
totalItems,
|
onPageChange
|
||||||
itemsPerPage,
|
}: PaginationProps) => {
|
||||||
onPageChange,
|
const pages = Array.from({ length: totalPages }, (_, i) => i + 1);
|
||||||
}: PaginationProps) {
|
|
||||||
if (totalPages <= 1) return null;
|
const visiblePages = pages.filter(page => {
|
||||||
|
if (totalPages <= 7) return true;
|
||||||
const startItem = ((currentPage - 1) * itemsPerPage) + 1;
|
if (page === 1 || page === totalPages) return true;
|
||||||
const endItem = Math.min(currentPage * itemsPerPage, totalItems);
|
if (page >= currentPage - 1 && page <= currentPage + 1) return true;
|
||||||
|
return false;
|
||||||
const getPageNumbers = () => {
|
});
|
||||||
if (totalPages <= 5) {
|
|
||||||
return Array.from({ length: totalPages }, (_, i) => i + 1);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (currentPage <= 3) {
|
|
||||||
return [1, 2, 3, 4, 5];
|
|
||||||
}
|
|
||||||
|
|
||||||
if (currentPage >= totalPages - 2) {
|
|
||||||
return [totalPages - 4, totalPages - 3, totalPages - 2, totalPages - 1, totalPages];
|
|
||||||
}
|
|
||||||
|
|
||||||
return [currentPage - 2, currentPage - 1, currentPage, currentPage + 1, currentPage + 2];
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box display="flex" alignItems="center" justifyContent="between" pt={4}>
|
<Box display="flex" alignItems="center" justifyContent="between" paddingTop={4}>
|
||||||
<Text size="sm" color="text-gray-500">
|
<Text size="sm" variant="low">
|
||||||
Showing {startItem}–{endItem} of {totalItems}
|
Page {currentPage} of {totalPages}
|
||||||
</Text>
|
</Text>
|
||||||
|
|
||||||
<Stack direction="row" align="center" gap={2}>
|
<Box display="flex" alignItems="center" gap={2}>
|
||||||
<Button
|
<Button
|
||||||
variant="secondary"
|
variant="ghost"
|
||||||
onClick={() => onPageChange(Math.max(1, currentPage - 1))}
|
|
||||||
disabled={currentPage === 1}
|
|
||||||
size="sm"
|
size="sm"
|
||||||
icon={<Icon icon={ChevronLeft} size={5} />}
|
disabled={currentPage === 1}
|
||||||
|
onClick={() => onPageChange(currentPage - 1)}
|
||||||
|
icon={<ChevronLeft size={16} />}
|
||||||
>
|
>
|
||||||
<Box as="span" className="sr-only">Previous</Box>
|
Previous
|
||||||
</Button>
|
</Button>
|
||||||
|
|
||||||
<Stack direction="row" align="center" gap={1}>
|
<Box display="flex" alignItems="center" gap={1}>
|
||||||
{getPageNumbers().map(pageNum => (
|
{visiblePages.map((page, index) => {
|
||||||
<Button
|
const prevPage = visiblePages[index - 1];
|
||||||
key={pageNum}
|
const showEllipsis = prevPage && page - prevPage > 1;
|
||||||
variant={currentPage === pageNum ? 'primary' : 'ghost'}
|
|
||||||
onClick={() => onPageChange(pageNum)}
|
return (
|
||||||
className="w-10 h-10 p-0"
|
<React.Fragment key={page}>
|
||||||
>
|
{showEllipsis && <Text variant="low">...</Text>}
|
||||||
{pageNum}
|
<Button
|
||||||
</Button>
|
variant={page === currentPage ? 'primary' : 'ghost'}
|
||||||
))}
|
size="sm"
|
||||||
</Stack>
|
onClick={() => onPageChange(page)}
|
||||||
|
style={{ minWidth: '2.5rem', padding: 0 }}
|
||||||
|
>
|
||||||
|
{page}
|
||||||
|
</Button>
|
||||||
|
</React.Fragment>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</Box>
|
||||||
|
|
||||||
<Button
|
<Button
|
||||||
variant="secondary"
|
variant="ghost"
|
||||||
onClick={() => onPageChange(Math.min(totalPages, currentPage + 1))}
|
|
||||||
disabled={currentPage === totalPages}
|
|
||||||
size="sm"
|
size="sm"
|
||||||
icon={<Icon icon={ChevronRight} size={5} />}
|
disabled={currentPage === totalPages}
|
||||||
|
onClick={() => onPageChange(currentPage + 1)}
|
||||||
|
icon={<ChevronRight size={16} />}
|
||||||
>
|
>
|
||||||
<Box as="span" className="sr-only">Next</Box>
|
Next
|
||||||
</Button>
|
</Button>
|
||||||
</Stack>
|
</Box>
|
||||||
</Box>
|
</Box>
|
||||||
);
|
);
|
||||||
}
|
};
|
||||||
|
|||||||
@@ -1,57 +1,42 @@
|
|||||||
import React, { ReactNode } from 'react';
|
import React, { ReactNode } from 'react';
|
||||||
import { Surface } from './primitives/Surface';
|
import { Surface } from './primitives/Surface';
|
||||||
import { Box, BoxProps } from './primitives/Box';
|
import { Box } from './primitives/Box';
|
||||||
|
import { Heading } from './Heading';
|
||||||
import { Text } from './Text';
|
import { Text } from './Text';
|
||||||
|
|
||||||
interface PanelProps extends Omit<BoxProps<'div'>, 'variant' | 'padding'> {
|
export interface PanelProps {
|
||||||
children: ReactNode;
|
|
||||||
title?: string;
|
title?: string;
|
||||||
description?: string;
|
description?: string;
|
||||||
variant?: 'default' | 'muted' | 'dark' | 'glass';
|
children: ReactNode;
|
||||||
padding?: 0 | 1 | 2 | 3 | 4 | 6 | 8 | 10 | 12;
|
footer?: ReactNode;
|
||||||
border?: boolean;
|
variant?: 'default' | 'dark' | 'muted';
|
||||||
className?: string;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
export const Panel = ({
|
||||||
* A semantic wrapper for content panels.
|
title,
|
||||||
* Follows the "Precision Racing Minimal" theme.
|
description,
|
||||||
*/
|
children,
|
||||||
export function Panel({
|
footer,
|
||||||
children,
|
variant = 'default'
|
||||||
title,
|
}: PanelProps) => {
|
||||||
description,
|
|
||||||
variant = 'default',
|
|
||||||
padding = 6,
|
|
||||||
border = true,
|
|
||||||
...props
|
|
||||||
}: PanelProps) {
|
|
||||||
return (
|
return (
|
||||||
<Surface
|
<Surface variant={variant} rounded="lg" style={{ border: '1px solid var(--ui-color-border-default)' }}>
|
||||||
variant={variant}
|
|
||||||
padding={padding}
|
|
||||||
border={border}
|
|
||||||
display="flex"
|
|
||||||
flexDirection="col"
|
|
||||||
gap={4}
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
||||||
{...(props as any)}
|
|
||||||
>
|
|
||||||
{(title || description) && (
|
{(title || description) && (
|
||||||
<Box display="flex" flexDirection="col" gap={1} borderBottom borderStyle="solid" borderColor="border-gray/30" pb={4} mb={2}>
|
<Box padding={6} borderBottom>
|
||||||
{title && (
|
{title && <Heading level={3} marginBottom={1}>{title}</Heading>}
|
||||||
<Text as="h3" size="xs" weight="bold" color="text-gray-500" uppercase letterSpacing="widest">
|
{description && <Text size="sm" variant="low">{description}</Text>}
|
||||||
{title}
|
</Box>
|
||||||
</Text>
|
)}
|
||||||
)}
|
|
||||||
{description && (
|
<Box padding={6}>
|
||||||
<Text size="sm" color="text-gray-400">
|
{children}
|
||||||
{description}
|
</Box>
|
||||||
</Text>
|
|
||||||
)}
|
{footer && (
|
||||||
|
<Box padding={4} borderTop bg="rgba(255,255,255,0.02)">
|
||||||
|
{footer}
|
||||||
</Box>
|
</Box>
|
||||||
)}
|
)}
|
||||||
{children}
|
|
||||||
</Surface>
|
</Surface>
|
||||||
);
|
);
|
||||||
}
|
};
|
||||||
|
|||||||
@@ -1,42 +1,35 @@
|
|||||||
import React, { ComponentProps } from 'react';
|
import React, { useState } from 'react';
|
||||||
import { Eye, EyeOff, Lock } from 'lucide-react';
|
import { Input, InputProps } from './Input';
|
||||||
import { Input } from './Input';
|
import { Eye, EyeOff } from 'lucide-react';
|
||||||
|
import { IconButton } from './IconButton';
|
||||||
import { Box } from './primitives/Box';
|
import { Box } from './primitives/Box';
|
||||||
|
|
||||||
interface PasswordFieldProps extends ComponentProps<typeof Input> {
|
export interface PasswordFieldProps extends InputProps {}
|
||||||
showPassword?: boolean;
|
|
||||||
onTogglePassword?: () => void;
|
export const PasswordField = (props: PasswordFieldProps) => {
|
||||||
}
|
const [showPassword, setShowPassword] = useState(false);
|
||||||
|
|
||||||
/**
|
|
||||||
* PasswordField
|
|
||||||
*
|
|
||||||
* A specialized input for passwords with visibility toggling.
|
|
||||||
* Stateless UI component.
|
|
||||||
*/
|
|
||||||
export function PasswordField({ showPassword, onTogglePassword, ...props }: PasswordFieldProps) {
|
|
||||||
return (
|
return (
|
||||||
<Box position="relative" fullWidth>
|
<Box position="relative">
|
||||||
<Input
|
<Input
|
||||||
{...props}
|
{...props}
|
||||||
type={showPassword ? 'text' : 'password'}
|
type={showPassword ? 'text' : 'password'}
|
||||||
icon={<Lock size={16} />}
|
|
||||||
/>
|
/>
|
||||||
{onTogglePassword && (
|
<Box
|
||||||
<Box
|
position="absolute"
|
||||||
as="button"
|
right="0.5rem"
|
||||||
type="button"
|
top="50%"
|
||||||
onClick={onTogglePassword}
|
style={{ transform: 'translateY(-50%)' }}
|
||||||
position="absolute"
|
zIndex={10}
|
||||||
right="3"
|
>
|
||||||
top={props.label ? "34px" : "50%"}
|
<IconButton
|
||||||
style={props.label ? {} : { transform: 'translateY(-50%)' }}
|
icon={showPassword ? EyeOff : Eye}
|
||||||
zIndex={10}
|
onClick={() => setShowPassword(!showPassword)}
|
||||||
className="text-gray-500 hover:text-gray-300 transition-colors"
|
variant="ghost"
|
||||||
>
|
size="sm"
|
||||||
{showPassword ? <EyeOff size={16} /> : <Eye size={16} />}
|
title={showPassword ? 'Hide password' : 'Show password'}
|
||||||
</Box>
|
/>
|
||||||
)}
|
</Box>
|
||||||
</Box>
|
</Box>
|
||||||
);
|
);
|
||||||
}
|
};
|
||||||
|
|||||||
@@ -1,21 +1,34 @@
|
|||||||
|
import React from 'react';
|
||||||
|
|
||||||
import { User } from 'lucide-react';
|
|
||||||
import { Box } from './primitives/Box';
|
import { Box } from './primitives/Box';
|
||||||
import { Icon } from './Icon';
|
import { Icon } from './Icon';
|
||||||
|
import { User } from 'lucide-react';
|
||||||
|
|
||||||
export interface PlaceholderImageProps {
|
export interface PlaceholderImageProps {
|
||||||
size?: number;
|
width?: string | number;
|
||||||
|
height?: string | number;
|
||||||
|
size?: string | number;
|
||||||
className?: string;
|
className?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function PlaceholderImage({ size = 48, className = '' }: PlaceholderImageProps) {
|
export const PlaceholderImage = ({
|
||||||
|
width,
|
||||||
|
height,
|
||||||
|
size,
|
||||||
|
className
|
||||||
|
}: PlaceholderImageProps) => {
|
||||||
|
const dimension = size || '100%';
|
||||||
return (
|
return (
|
||||||
<Box
|
<Box
|
||||||
className={`rounded-full bg-charcoal-outline flex items-center justify-center ${className}`}
|
width={width || dimension}
|
||||||
style={{ width: size, height: size }}
|
height={height || dimension}
|
||||||
|
display="flex"
|
||||||
|
alignItems="center"
|
||||||
|
justifyContent="center"
|
||||||
|
bg="var(--ui-color-bg-surface-muted)"
|
||||||
|
style={{ borderRadius: 'var(--ui-radius-md)' }}
|
||||||
|
className={className}
|
||||||
>
|
>
|
||||||
<Icon icon={User} size={6} color="#9ca3af" />
|
<Icon icon={User} size={6} intent="low" />
|
||||||
</Box>
|
</Box>
|
||||||
);
|
);
|
||||||
}
|
};
|
||||||
|
|||||||
@@ -1,70 +1,80 @@
|
|||||||
|
import React, { ReactNode } from 'react';
|
||||||
|
|
||||||
import { Trophy } from 'lucide-react';
|
|
||||||
import { ReactNode } from 'react';
|
|
||||||
import { Box } from './primitives/Box';
|
import { Box } from './primitives/Box';
|
||||||
import { Heading } from './Heading';
|
|
||||||
import { Icon } from './Icon';
|
|
||||||
import { Stack } from './primitives/Stack';
|
|
||||||
import { Text } from './Text';
|
import { Text } from './Text';
|
||||||
|
import { Avatar } from './Avatar';
|
||||||
|
import { Trophy } from 'lucide-react';
|
||||||
|
import { Icon } from './Icon';
|
||||||
|
import { Heading } from './Heading';
|
||||||
|
|
||||||
interface PodiumProps {
|
export interface PodiumEntry {
|
||||||
title: string;
|
name: string;
|
||||||
children: ReactNode;
|
avatar?: string;
|
||||||
|
value: string | number;
|
||||||
|
position: 1 | 2 | 3;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function Podium({ title, children }: PodiumProps) {
|
export interface PodiumProps {
|
||||||
return (
|
entries?: PodiumEntry[];
|
||||||
<Box bg="bg-iron-gray/50" rounded="2xl" border style={{ borderColor: 'rgba(38, 38, 38, 0.8)' }} p={8} mb={10}>
|
title?: string;
|
||||||
<Box display="flex" justifyContent="center" mb={8}>
|
children?: ReactNode;
|
||||||
<Stack direction="row" align="center" gap={2}>
|
}
|
||||||
<Icon icon={Trophy} size={6} color="var(--warning-amber)" />
|
|
||||||
<Heading level={2}>{title}</Heading>
|
|
||||||
</Stack>
|
|
||||||
</Box>
|
|
||||||
|
|
||||||
<Stack direction="row" align="end" justify="center" gap={8}>
|
export const Podium = ({ entries = [], title, children }: PodiumProps) => {
|
||||||
{children}
|
const sortedEntries = [...entries].sort((a, b) => {
|
||||||
</Stack>
|
const order = { 2: 0, 1: 1, 3: 2 };
|
||||||
|
return order[a.position] - order[b.position];
|
||||||
|
});
|
||||||
|
|
||||||
|
const getPositionColor = (pos: number) => {
|
||||||
|
if (pos === 1) return 'var(--ui-color-intent-warning)';
|
||||||
|
if (pos === 2) return '#A1A1AA';
|
||||||
|
if (pos === 3) return '#CD7F32';
|
||||||
|
return 'var(--ui-color-text-low)';
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Box paddingY={8}>
|
||||||
|
{title && <Heading level={2} align="center" marginBottom={8}>{title}</Heading>}
|
||||||
|
|
||||||
|
<Box display="flex" alignItems="end" justifyContent="center" gap={4}>
|
||||||
|
{sortedEntries.map((entry) => {
|
||||||
|
const height = entry.position === 1 ? '12rem' : entry.position === 2 ? '10rem' : '8rem';
|
||||||
|
const color = getPositionColor(entry.position);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Box key={entry.position} display="flex" flexDirection="col" alignItems="center" gap={4}>
|
||||||
|
<Box display="flex" flexDirection="col" alignItems="center" gap={2}>
|
||||||
|
{entry.position === 1 && <Icon icon={Trophy} size={6} intent="warning" />}
|
||||||
|
<Avatar src={entry.avatar} alt={entry.name} size={entry.position === 1 ? 'lg' : 'md'} />
|
||||||
|
<Text weight="bold" variant="high" size={entry.position === 1 ? 'md' : 'sm'}>{entry.name}</Text>
|
||||||
|
<Text size="xs" variant="low">{entry.value}</Text>
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
<Box
|
||||||
|
width="6rem"
|
||||||
|
height={height}
|
||||||
|
bg="var(--ui-color-bg-surface-muted)"
|
||||||
|
display="flex"
|
||||||
|
alignItems="center"
|
||||||
|
justifyContent="center"
|
||||||
|
style={{
|
||||||
|
borderTopLeftRadius: 'var(--ui-radius-lg)',
|
||||||
|
borderTopRightRadius: 'var(--ui-radius-lg)',
|
||||||
|
border: `1px solid ${color}`,
|
||||||
|
borderBottom: 'none'
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Text size="3xl" weight="bold" style={{ color }}>
|
||||||
|
{entry.position}
|
||||||
|
</Text>
|
||||||
|
</Box>
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</Box>
|
||||||
|
{children}
|
||||||
</Box>
|
</Box>
|
||||||
);
|
);
|
||||||
}
|
};
|
||||||
|
|
||||||
interface PodiumItemProps {
|
export const PodiumItem = ({ children }: { children: ReactNode }) => <>{children}</>;
|
||||||
position: number;
|
|
||||||
height: string;
|
|
||||||
cardContent: ReactNode;
|
|
||||||
bgColor: string;
|
|
||||||
positionColor: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function PodiumItem({
|
|
||||||
position,
|
|
||||||
height,
|
|
||||||
cardContent,
|
|
||||||
bgColor,
|
|
||||||
positionColor,
|
|
||||||
}: PodiumItemProps) {
|
|
||||||
return (
|
|
||||||
<Stack align="center">
|
|
||||||
{cardContent}
|
|
||||||
|
|
||||||
{/* Podium stand */}
|
|
||||||
<Box
|
|
||||||
bg={bgColor}
|
|
||||||
h={height}
|
|
||||||
border
|
|
||||||
style={{ borderColor: 'rgba(38, 38, 38, 0.8)', borderTopLeftRadius: '0.5rem', borderTopRightRadius: '0.5rem' }}
|
|
||||||
w="28"
|
|
||||||
display="flex"
|
|
||||||
p={3}
|
|
||||||
>
|
|
||||||
<Box display="flex" center fullWidth>
|
|
||||||
<Text size="3xl" weight="bold" color={positionColor}>
|
|
||||||
{position}
|
|
||||||
</Text>
|
|
||||||
</Box>
|
|
||||||
</Box>
|
|
||||||
</Stack>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -1,122 +1,56 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { Box } from './primitives/Box';
|
||||||
import type { MouseEventHandler, ReactNode } from 'react';
|
import { Text } from './Text';
|
||||||
import { Card } from './Card';
|
import { Surface } from './primitives/Surface';
|
||||||
|
import { Icon } from './Icon';
|
||||||
interface PresetCardStat {
|
import { LucideIcon } from 'lucide-react';
|
||||||
label: string;
|
|
||||||
value: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface PresetCardProps {
|
export interface PresetCardProps {
|
||||||
title: string;
|
title: string;
|
||||||
subtitle?: string;
|
description: string;
|
||||||
primaryTag?: string;
|
icon: LucideIcon;
|
||||||
description?: string;
|
onClick: () => void;
|
||||||
stats?: PresetCardStat[];
|
isSelected?: boolean;
|
||||||
selected?: boolean;
|
|
||||||
disabled?: boolean;
|
|
||||||
onSelect?: () => void;
|
|
||||||
className?: string;
|
|
||||||
children?: ReactNode;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function PresetCard({
|
export const PresetCard = ({
|
||||||
title,
|
title,
|
||||||
subtitle,
|
description,
|
||||||
primaryTag,
|
icon,
|
||||||
description,
|
onClick,
|
||||||
stats,
|
isSelected = false
|
||||||
selected,
|
}: PresetCardProps) => {
|
||||||
disabled,
|
|
||||||
onSelect,
|
|
||||||
className = '',
|
|
||||||
children,
|
|
||||||
}: PresetCardProps) {
|
|
||||||
const isInteractive = typeof onSelect === 'function' && !disabled;
|
|
||||||
|
|
||||||
const handleClick: MouseEventHandler<HTMLButtonElement | HTMLDivElement> = (event) => {
|
|
||||||
if (!isInteractive) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
event.preventDefault();
|
|
||||||
onSelect?.();
|
|
||||||
};
|
|
||||||
|
|
||||||
const baseBorder = selected ? 'border-primary-blue' : 'border-charcoal-outline';
|
|
||||||
const baseBg = selected ? 'bg-primary-blue/10' : 'bg-iron-gray';
|
|
||||||
const baseRing = selected ? 'ring-2 ring-primary-blue/40' : '';
|
|
||||||
const disabledClasses = disabled ? 'opacity-60 cursor-not-allowed' : '';
|
|
||||||
const hoverClasses = isInteractive && !disabled ? 'hover:bg-iron-gray/80 hover:scale-[1.01]' : '';
|
|
||||||
|
|
||||||
const content = (
|
|
||||||
<div className="flex h-full flex-col gap-3">
|
|
||||||
<div className="flex items-start justify-between gap-3">
|
|
||||||
<div>
|
|
||||||
<div className="text-sm font-semibold text-white">{title}</div>
|
|
||||||
{subtitle && (
|
|
||||||
<div className="mt-0.5 text-xs text-gray-400">{subtitle}</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
<div className="flex flex-col items-end gap-1">
|
|
||||||
{primaryTag && (
|
|
||||||
<span className="inline-flex rounded-full bg-primary-blue/15 px-2 py-0.5 text-[10px] font-medium uppercase tracking-wide text-primary-blue">
|
|
||||||
{primaryTag}
|
|
||||||
</span>
|
|
||||||
)}
|
|
||||||
{selected && (
|
|
||||||
<span className="inline-flex items-center gap-1 rounded-full bg-primary-blue/10 px-2 py-0.5 text-[10px] font-medium text-primary-blue">
|
|
||||||
<span className="h-1.5 w-1.5 rounded-full bg-primary-blue" />
|
|
||||||
Selected
|
|
||||||
</span>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{description && (
|
|
||||||
<p className="text-xs text-gray-300">{description}</p>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{children}
|
|
||||||
|
|
||||||
{stats && stats.length > 0 && (
|
|
||||||
<div className="mt-1 border-t border-charcoal-outline/70 pt-2">
|
|
||||||
<dl className="grid grid-cols-1 gap-2 text-[11px] text-gray-400 sm:grid-cols-3">
|
|
||||||
{stats.map((stat) => (
|
|
||||||
<div key={stat.label} className="space-y-0.5">
|
|
||||||
<dt className="font-medium text-gray-500">{stat.label}</dt>
|
|
||||||
<dd className="text-xs text-gray-200">{stat.value}</dd>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</dl>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
|
|
||||||
const commonClasses = `${baseBorder} ${baseBg} ${baseRing} ${hoverClasses} ${disabledClasses} ${className}`;
|
|
||||||
|
|
||||||
if (isInteractive) {
|
|
||||||
return (
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
onClick={handleClick as MouseEventHandler<HTMLButtonElement>}
|
|
||||||
disabled={disabled}
|
|
||||||
className={`group block w-full rounded-lg text-left text-sm shadow-card outline-none transition-all duration-150 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-primary-blue ${commonClasses}`}
|
|
||||||
>
|
|
||||||
<div className="p-4">
|
|
||||||
{content}
|
|
||||||
</div>
|
|
||||||
</button>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Card
|
<Surface
|
||||||
className={commonClasses}
|
variant={isSelected ? 'default' : 'muted'}
|
||||||
onClick={handleClick as MouseEventHandler<HTMLDivElement>}
|
rounded="lg"
|
||||||
|
padding={4}
|
||||||
|
onClick={onClick}
|
||||||
|
style={{
|
||||||
|
cursor: 'pointer',
|
||||||
|
border: isSelected ? '2px solid var(--ui-color-intent-primary)' : '1px solid var(--ui-color-border-default)',
|
||||||
|
transition: 'all 0.2s ease-in-out'
|
||||||
|
}}
|
||||||
|
className="group hover:bg-white/5"
|
||||||
>
|
>
|
||||||
{content}
|
<Box display="flex" alignItems="start" gap={4}>
|
||||||
</Card>
|
<Box
|
||||||
|
padding={3}
|
||||||
|
rounded="lg"
|
||||||
|
bg={isSelected ? 'var(--ui-color-intent-primary)' : 'var(--ui-color-bg-surface-muted)'}
|
||||||
|
className="transition-colors"
|
||||||
|
>
|
||||||
|
<Icon icon={icon} size={6} intent={isSelected ? 'high' : 'low'} />
|
||||||
|
</Box>
|
||||||
|
<Box>
|
||||||
|
<Text weight="bold" variant="high" block marginBottom={1}>
|
||||||
|
{title}
|
||||||
|
</Text>
|
||||||
|
<Text size="sm" variant="low">
|
||||||
|
{description}
|
||||||
|
</Text>
|
||||||
|
</Box>
|
||||||
|
</Box>
|
||||||
|
</Surface>
|
||||||
);
|
);
|
||||||
}
|
};
|
||||||
|
|||||||
@@ -1,33 +1,55 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { Box, BoxProps } from './primitives/Box';
|
import { Box } from './primitives/Box';
|
||||||
|
|
||||||
interface ProgressBarProps extends Omit<BoxProps<'div'>, 'children'> {
|
export interface ProgressBarProps {
|
||||||
value: number;
|
value: number;
|
||||||
max: number;
|
max?: number;
|
||||||
color?: string;
|
intent?: 'primary' | 'success' | 'warning' | 'critical' | 'telemetry';
|
||||||
bg?: string;
|
color?: string; // Alias for intent
|
||||||
height?: string;
|
size?: 'sm' | 'md' | 'lg';
|
||||||
|
marginBottom?: any;
|
||||||
|
mb?: any; // Alias for marginBottom
|
||||||
}
|
}
|
||||||
|
|
||||||
export function ProgressBar({
|
export const ProgressBar = ({
|
||||||
value,
|
value,
|
||||||
max,
|
max = 100,
|
||||||
color = 'bg-primary-blue',
|
intent = 'primary',
|
||||||
bg = 'bg-deep-graphite',
|
color: colorProp,
|
||||||
height = '2',
|
size = 'md',
|
||||||
...props
|
marginBottom,
|
||||||
}: ProgressBarProps) {
|
mb
|
||||||
const percentage = Math.min((value / max) * 100, 100);
|
}: ProgressBarProps) => {
|
||||||
|
const percentage = Math.min(Math.max((value / max) * 100, 0), 100);
|
||||||
|
|
||||||
|
const intentColorMap = {
|
||||||
|
primary: 'var(--ui-color-intent-primary)',
|
||||||
|
success: 'var(--ui-color-intent-success)',
|
||||||
|
warning: 'var(--ui-color-intent-warning)',
|
||||||
|
critical: 'var(--ui-color-intent-critical)',
|
||||||
|
telemetry: 'var(--ui-color-intent-telemetry)',
|
||||||
|
};
|
||||||
|
|
||||||
|
const color = colorProp || intentColorMap[intent];
|
||||||
|
|
||||||
|
const sizeMap = {
|
||||||
|
sm: '0.25rem',
|
||||||
|
md: '0.5rem',
|
||||||
|
lg: '1rem',
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box fullWidth bg={bg} rounded="full" height={height} overflow="hidden" {...props}>
|
<Box
|
||||||
|
fullWidth
|
||||||
|
bg="var(--ui-color-bg-surface-muted)"
|
||||||
|
style={{ height: sizeMap[size], borderRadius: '9999px', overflow: 'hidden' }}
|
||||||
|
marginBottom={marginBottom || mb}
|
||||||
|
>
|
||||||
<Box
|
<Box
|
||||||
bg={color}
|
|
||||||
rounded="full"
|
|
||||||
fullHeight
|
fullHeight
|
||||||
style={{ width: `${percentage}%` }}
|
bg={color}
|
||||||
className="transition-all duration-500 ease-out"
|
style={{ width: `${percentage}%`, transition: 'width 0.3s ease-in-out' }}
|
||||||
/>
|
/>
|
||||||
</Box>
|
</Box>
|
||||||
);
|
);
|
||||||
}
|
};
|
||||||
|
|||||||
@@ -1,51 +1,59 @@
|
|||||||
|
import React from 'react';
|
||||||
|
|
||||||
import { ChevronRight, LucideIcon } from 'lucide-react';
|
|
||||||
import { Box } from './primitives/Box';
|
import { Box } from './primitives/Box';
|
||||||
import { Icon } from './Icon';
|
|
||||||
import { Link } from './Link';
|
|
||||||
import { Text } from './Text';
|
import { Text } from './Text';
|
||||||
|
import { Icon } from './Icon';
|
||||||
|
import { LucideIcon, ChevronRight } from 'lucide-react';
|
||||||
|
import { Link } from './Link';
|
||||||
|
|
||||||
interface QuickActionItemProps {
|
export interface QuickActionItemProps {
|
||||||
href: string;
|
|
||||||
label: string;
|
label: string;
|
||||||
icon: LucideIcon;
|
icon: LucideIcon;
|
||||||
iconVariant?: 'blue' | 'amber' | 'purple' | 'green';
|
href: string;
|
||||||
|
variant?: 'primary' | 'secondary' | 'success' | 'warning' | 'critical';
|
||||||
}
|
}
|
||||||
|
|
||||||
export function QuickActionItem({ href, label, icon, iconVariant = 'blue' }: QuickActionItemProps) {
|
export const QuickActionItem = ({
|
||||||
const variantColors = {
|
label,
|
||||||
blue: 'rgb(59, 130, 246)',
|
icon,
|
||||||
amber: 'rgb(245, 158, 11)',
|
href,
|
||||||
purple: 'rgb(168, 85, 247)',
|
variant = 'primary'
|
||||||
green: 'rgb(16, 185, 129)',
|
}: QuickActionItemProps) => {
|
||||||
|
const variantBgs = {
|
||||||
|
primary: 'rgba(25, 140, 255, 0.1)',
|
||||||
|
secondary: 'var(--ui-color-bg-surface-muted)',
|
||||||
|
success: 'rgba(111, 227, 122, 0.1)',
|
||||||
|
warning: 'rgba(255, 190, 77, 0.1)',
|
||||||
|
critical: 'rgba(227, 92, 92, 0.1)',
|
||||||
};
|
};
|
||||||
|
|
||||||
const variantBgs = {
|
const variantIntents = {
|
||||||
blue: 'bg-primary-blue/10',
|
primary: 'primary' as const,
|
||||||
amber: 'bg-warning-amber/10',
|
secondary: 'med' as const,
|
||||||
purple: 'bg-purple-500/10',
|
success: 'success' as const,
|
||||||
green: 'bg-performance-green/10',
|
warning: 'warning' as const,
|
||||||
|
critical: 'critical' as const,
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Link
|
<Link href={href} underline="none">
|
||||||
href={href}
|
<Box
|
||||||
block
|
display="flex"
|
||||||
p={3}
|
alignItems="center"
|
||||||
rounded="lg"
|
gap={4}
|
||||||
bg="bg-deep-graphite"
|
padding={4}
|
||||||
hoverBorderColor="charcoal-outline/50"
|
rounded="lg"
|
||||||
transition
|
bg="var(--ui-color-bg-surface)"
|
||||||
className="hover:bg-charcoal-outline/50"
|
style={{ border: '1px solid var(--ui-color-border-default)' }}
|
||||||
>
|
className="group hover:bg-white/5 transition-colors"
|
||||||
<Box display="flex" alignItems="center" gap={3} fullWidth>
|
>
|
||||||
<Box p={2} bg={variantBgs[iconVariant]} rounded="lg">
|
<Box padding={2} bg={variantBgs[variant]} rounded="lg">
|
||||||
<Icon icon={icon} size={4} color={variantColors[iconVariant]} />
|
<Icon icon={icon} size={5} intent={variantIntents[variant] as any} />
|
||||||
</Box>
|
</Box>
|
||||||
<Text size="sm" color="text-white" weight="medium">{label}</Text>
|
<Text weight="medium" variant="high" flexGrow={1}>
|
||||||
<Icon icon={ChevronRight} size={4} color="rgb(107, 114, 128)" ml="auto" />
|
{label}
|
||||||
|
</Text>
|
||||||
|
<Icon icon={ChevronRight} size={4} intent="low" />
|
||||||
</Box>
|
</Box>
|
||||||
</Link>
|
</Link>
|
||||||
);
|
);
|
||||||
}
|
};
|
||||||
|
|||||||
@@ -1,33 +1,36 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
import { Box } from './primitives/Box';
|
||||||
|
import { Text } from './Text';
|
||||||
|
import { Icon } from './Icon';
|
||||||
|
import { LucideIcon, ArrowRight } from 'lucide-react';
|
||||||
|
import { Link } from './Link';
|
||||||
|
|
||||||
interface QuickActionLinkProps {
|
export interface QuickActionLinkProps {
|
||||||
|
label: string;
|
||||||
|
icon: LucideIcon;
|
||||||
href: string;
|
href: string;
|
||||||
children: React.ReactNode;
|
|
||||||
variant?: 'blue' | 'purple' | 'orange';
|
|
||||||
className?: string;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function QuickActionLink({
|
export const QuickActionLink = ({
|
||||||
href,
|
label,
|
||||||
children,
|
icon,
|
||||||
variant = 'blue',
|
href
|
||||||
className = ''
|
}: QuickActionLinkProps) => {
|
||||||
}: QuickActionLinkProps) {
|
|
||||||
const variantClasses = {
|
|
||||||
blue: 'bg-primary-blue/20 border-primary-blue/30 text-primary-blue hover:bg-primary-blue/30',
|
|
||||||
purple: 'bg-purple-500/20 border-purple-500/30 text-purple-300 hover:bg-purple-500/30',
|
|
||||||
orange: 'bg-orange-500/20 border-orange-500/30 text-orange-300 hover:bg-orange-500/30'
|
|
||||||
};
|
|
||||||
|
|
||||||
const classes = [
|
|
||||||
'px-4 py-3 border rounded-lg transition-colors text-sm font-medium text-center inline-block w-full',
|
|
||||||
variantClasses[variant],
|
|
||||||
className
|
|
||||||
].filter(Boolean).join(' ');
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<a href={href} className={classes}>
|
<Link href={href} underline="none">
|
||||||
{children}
|
<Box
|
||||||
</a>
|
display="flex"
|
||||||
|
alignItems="center"
|
||||||
|
gap={3}
|
||||||
|
paddingY={2}
|
||||||
|
className="group"
|
||||||
|
>
|
||||||
|
<Icon icon={icon} size={4} intent="low" className="group-hover:text-[var(--ui-color-intent-primary)] transition-colors" />
|
||||||
|
<Text size="sm" variant="med" className="group-hover:text-[var(--ui-color-text-high)] transition-colors">
|
||||||
|
{label}
|
||||||
|
</Text>
|
||||||
|
<Icon icon={ArrowRight} size={3} intent="low" className="opacity-0 group-hover:opacity-100 group-hover:translate-x-1 transition-all" />
|
||||||
|
</Box>
|
||||||
|
</Link>
|
||||||
);
|
);
|
||||||
}
|
};
|
||||||
|
|||||||
@@ -5,42 +5,37 @@ import { Button } from './Button';
|
|||||||
import { Icon } from './Icon';
|
import { Icon } from './Icon';
|
||||||
import { LucideIcon } from 'lucide-react';
|
import { LucideIcon } from 'lucide-react';
|
||||||
|
|
||||||
interface QuickAction {
|
export interface QuickAction {
|
||||||
label: string;
|
label: string;
|
||||||
icon: LucideIcon;
|
icon: LucideIcon;
|
||||||
onClick: () => void;
|
onClick: () => void;
|
||||||
variant?: 'primary' | 'secondary' | 'ghost';
|
intent?: 'primary' | 'secondary' | 'danger';
|
||||||
}
|
}
|
||||||
|
|
||||||
interface QuickActionsPanelProps {
|
export interface QuickActionsPanelProps {
|
||||||
actions: QuickAction[];
|
actions: QuickAction[];
|
||||||
className?: string;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
export const QuickActionsPanel = ({
|
||||||
* QuickActionsPanel
|
actions
|
||||||
*
|
}: QuickActionsPanelProps) => {
|
||||||
* Provides fast access to common dashboard tasks.
|
|
||||||
*/
|
|
||||||
export function QuickActionsPanel({ actions, className = '' }: QuickActionsPanelProps) {
|
|
||||||
return (
|
return (
|
||||||
<Panel title="Quick Actions" className={className}>
|
<Panel title="Quick Actions">
|
||||||
<Box display="grid" gridCols={1} gap={2}>
|
<Box display="flex" flexDirection="col" gap={2}>
|
||||||
{actions.map((action, index) => (
|
{actions.map((action, index) => (
|
||||||
<Button
|
<Button
|
||||||
key={index}
|
key={index}
|
||||||
variant={action.variant || 'secondary'}
|
variant={action.intent === 'danger' ? 'danger' : 'secondary'}
|
||||||
onClick={action.onClick}
|
onClick={action.onClick}
|
||||||
fullWidth
|
fullWidth
|
||||||
style={{ justifyContent: 'flex-start', height: 'auto', padding: '12px' }}
|
|
||||||
>
|
>
|
||||||
<Box display="flex" align="center" gap={3}>
|
<Box display="flex" alignItems="center" gap={3} fullWidth>
|
||||||
<Icon icon={action.icon} size={5} />
|
<Icon icon={action.icon} size={4} />
|
||||||
<span>{action.label}</span>
|
{action.label}
|
||||||
</Box>
|
</Box>
|
||||||
</Button>
|
</Button>
|
||||||
))}
|
))}
|
||||||
</Box>
|
</Box>
|
||||||
</Panel>
|
</Panel>
|
||||||
);
|
);
|
||||||
}
|
};
|
||||||
|
|||||||
@@ -1,74 +1,42 @@
|
|||||||
|
import React, { ReactNode } from 'react';
|
||||||
|
|
||||||
import { ReactNode } from 'react';
|
|
||||||
import { Box } from './primitives/Box';
|
import { Box } from './primitives/Box';
|
||||||
import { Heading } from './Heading';
|
|
||||||
import { Text } from './Text';
|
|
||||||
|
|
||||||
interface SectionProps {
|
export interface SectionProps {
|
||||||
children: ReactNode;
|
children: ReactNode;
|
||||||
className?: string;
|
variant?: 'default' | 'dark' | 'muted';
|
||||||
title?: string;
|
padding?: 'none' | 'sm' | 'md' | 'lg';
|
||||||
description?: string;
|
|
||||||
variant?: 'default' | 'card' | 'highlight' | 'dark' | 'light';
|
|
||||||
id?: string;
|
id?: string;
|
||||||
py?: number;
|
|
||||||
minHeight?: string;
|
|
||||||
borderBottom?: boolean;
|
|
||||||
borderColor?: string;
|
|
||||||
overflow?: 'hidden' | 'visible' | 'auto' | 'scroll';
|
|
||||||
position?: 'relative' | 'absolute' | 'fixed' | 'sticky';
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function Section({
|
export const Section = ({
|
||||||
children,
|
children,
|
||||||
className = '',
|
|
||||||
title,
|
|
||||||
description,
|
|
||||||
variant = 'default',
|
variant = 'default',
|
||||||
id,
|
padding = 'md',
|
||||||
py = 16,
|
id
|
||||||
minHeight,
|
}: SectionProps) => {
|
||||||
borderBottom,
|
|
||||||
borderColor,
|
|
||||||
overflow,
|
|
||||||
position
|
|
||||||
}: SectionProps) {
|
|
||||||
const variantClasses = {
|
const variantClasses = {
|
||||||
default: '',
|
default: 'bg-[var(--ui-color-bg-base)]',
|
||||||
card: 'bg-panel-gray rounded-none p-6 border border-border-gray',
|
dark: 'bg-black',
|
||||||
highlight: 'bg-gradient-to-r from-primary-accent/10 to-transparent rounded-none p-6 border border-primary-accent/30',
|
muted: 'bg-[var(--ui-color-bg-surface)]',
|
||||||
dark: 'bg-graphite-black',
|
|
||||||
light: 'bg-panel-gray'
|
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const paddingClasses = {
|
||||||
|
none: 'py-0',
|
||||||
|
sm: 'py-8',
|
||||||
|
md: 'py-16',
|
||||||
|
lg: 'py-24',
|
||||||
|
};
|
||||||
|
|
||||||
const classes = [
|
const classes = [
|
||||||
variantClasses[variant],
|
variantClasses[variant],
|
||||||
className
|
paddingClasses[padding],
|
||||||
].filter(Boolean).join(' ');
|
].join(' ');
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box
|
<section id={id} className={classes}>
|
||||||
as="section"
|
<Box marginX="auto" maxWidth="80rem" paddingX={4}>
|
||||||
id={id}
|
|
||||||
className={classes}
|
|
||||||
py={py as 0}
|
|
||||||
px={4}
|
|
||||||
minHeight={minHeight}
|
|
||||||
borderBottom={borderBottom}
|
|
||||||
borderColor={borderColor}
|
|
||||||
overflow={overflow}
|
|
||||||
position={position}
|
|
||||||
>
|
|
||||||
<Box className="mx-auto max-w-7xl">
|
|
||||||
{(title || description) && (
|
|
||||||
<Box mb={8}>
|
|
||||||
{title && <Heading level={2}>{title}</Heading>}
|
|
||||||
{description && <Text color="text-gray-400" block mt={2}>{description}</Text>}
|
|
||||||
</Box>
|
|
||||||
)}
|
|
||||||
{children}
|
{children}
|
||||||
</Box>
|
</Box>
|
||||||
</Box>
|
</section>
|
||||||
);
|
);
|
||||||
}
|
};
|
||||||
|
|||||||
@@ -1,51 +1,55 @@
|
|||||||
import React, { ReactNode } from 'react';
|
import React, { ReactNode } from 'react';
|
||||||
import { Box } from './primitives/Box';
|
import { Box } from './primitives/Box';
|
||||||
import { Stack } from './primitives/Stack';
|
|
||||||
import { Text } from './Text';
|
import { Text } from './Text';
|
||||||
import { Surface } from './primitives/Surface';
|
|
||||||
import { Icon } from './Icon';
|
import { Icon } from './Icon';
|
||||||
import { LucideIcon } from 'lucide-react';
|
import { LucideIcon } from 'lucide-react';
|
||||||
|
import { Surface } from './primitives/Surface';
|
||||||
|
|
||||||
interface SectionHeaderProps {
|
export interface SectionHeaderProps {
|
||||||
title: string;
|
title: string;
|
||||||
description?: string;
|
description?: string;
|
||||||
icon?: LucideIcon;
|
icon?: LucideIcon;
|
||||||
color?: string;
|
intent?: 'primary' | 'success' | 'warning' | 'critical' | 'telemetry';
|
||||||
actions?: ReactNode;
|
actions?: ReactNode;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function SectionHeader({ title, description, icon, color = 'text-primary-blue', actions }: SectionHeaderProps) {
|
export const SectionHeader = ({
|
||||||
|
title,
|
||||||
|
description,
|
||||||
|
icon,
|
||||||
|
intent = 'primary',
|
||||||
|
actions
|
||||||
|
}: SectionHeaderProps) => {
|
||||||
return (
|
return (
|
||||||
<Box
|
<Box
|
||||||
p={5}
|
padding={5}
|
||||||
borderBottom
|
borderBottom
|
||||||
borderColor="border-white/5"
|
style={{ background: 'linear-gradient(to right, var(--ui-color-bg-surface), transparent)' }}
|
||||||
style={{ background: 'linear-gradient(to right, rgba(38, 38, 38, 0.3), transparent)' }}
|
|
||||||
>
|
>
|
||||||
<Stack direction="row" align="center" justify="between" wrap gap={4}>
|
<Box display="flex" alignItems="center" justifyContent="between">
|
||||||
<Stack direction="row" align="center" gap={3}>
|
<Box display="flex" alignItems="center" gap={4}>
|
||||||
{icon && (
|
{icon && (
|
||||||
<Surface variant="muted" rounded="lg" p={2} bg="bg-white/5">
|
<Surface variant="muted" rounded="lg" padding={2} style={{ backgroundColor: 'rgba(255,255,255,0.05)' }}>
|
||||||
<Icon icon={icon} size={5} color={color} />
|
<Icon icon={icon} size={5} intent={intent} />
|
||||||
</Surface>
|
</Surface>
|
||||||
)}
|
)}
|
||||||
<Box>
|
<Box>
|
||||||
<Text size="lg" weight="bold" color="text-white" block>
|
<Text size="lg" weight="bold" variant="high" block>
|
||||||
{title}
|
{title}
|
||||||
</Text>
|
</Text>
|
||||||
{description && (
|
{description && (
|
||||||
<Text size="sm" color="text-gray-400" block>
|
<Text size="sm" variant="low" block>
|
||||||
{description}
|
{description}
|
||||||
</Text>
|
</Text>
|
||||||
)}
|
)}
|
||||||
</Box>
|
</Box>
|
||||||
</Stack>
|
</Box>
|
||||||
{actions && (
|
{actions && (
|
||||||
<Box>
|
<Box display="flex" alignItems="center" gap={3}>
|
||||||
{actions}
|
{actions}
|
||||||
</Box>
|
</Box>
|
||||||
)}
|
)}
|
||||||
</Stack>
|
</Box>
|
||||||
</Box>
|
</Box>
|
||||||
);
|
);
|
||||||
}
|
};
|
||||||
|
|||||||
@@ -1,82 +1,52 @@
|
|||||||
|
import React from 'react';
|
||||||
import { Box } from './primitives/Box';
|
import { Box } from './primitives/Box';
|
||||||
import { Stack } from './primitives/Stack';
|
|
||||||
import { Text } from './Text';
|
import { Text } from './Text';
|
||||||
|
import { Surface } from './primitives/Surface';
|
||||||
|
|
||||||
interface SegmentedControlOption {
|
export interface SegmentedControlOption {
|
||||||
value: string;
|
id: string;
|
||||||
label: string;
|
label: string;
|
||||||
description?: string;
|
icon?: React.ReactNode;
|
||||||
disabled?: boolean;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
interface SegmentedControlProps {
|
export interface SegmentedControlProps {
|
||||||
options: SegmentedControlOption[];
|
options: SegmentedControlOption[];
|
||||||
value: string;
|
activeId: string;
|
||||||
onChange?: (value: string) => void;
|
onChange: (id: string) => void;
|
||||||
|
fullWidth?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function SegmentedControl({
|
export const SegmentedControl = ({
|
||||||
options,
|
options,
|
||||||
value,
|
activeId,
|
||||||
onChange,
|
onChange,
|
||||||
}: SegmentedControlProps) {
|
fullWidth = false
|
||||||
const handleSelect = (optionValue: string, optionDisabled?: boolean) => {
|
}: SegmentedControlProps) => {
|
||||||
if (!onChange || optionDisabled) return;
|
|
||||||
if (optionValue === value) return;
|
|
||||||
onChange(optionValue);
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Stack
|
<Surface
|
||||||
direction="row"
|
variant="muted"
|
||||||
|
rounded="lg"
|
||||||
|
padding={1}
|
||||||
display="inline-flex"
|
display="inline-flex"
|
||||||
w="full"
|
width={fullWidth ? '100%' : undefined}
|
||||||
flexWrap="wrap"
|
|
||||||
gap={2}
|
|
||||||
rounded="full"
|
|
||||||
bg="bg-black/60"
|
|
||||||
p={1}
|
|
||||||
>
|
>
|
||||||
{options.map((option) => {
|
{options.map((option) => {
|
||||||
const isSelected = option.value === value;
|
const isSelected = option.id === activeId;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box
|
<button
|
||||||
key={option.value}
|
key={option.id}
|
||||||
as="button"
|
onClick={() => onChange(option.id)}
|
||||||
type="button"
|
className={`flex-1 flex items-center justify-center gap-2 px-4 py-1.5 text-xs font-bold uppercase tracking-widest transition-all rounded-md ${
|
||||||
onClick={() => handleSelect(option.value, option.disabled)}
|
isSelected
|
||||||
aria-pressed={isSelected}
|
? 'bg-[var(--ui-color-bg-surface)] text-[var(--ui-color-intent-primary)] shadow-sm'
|
||||||
disabled={option.disabled}
|
: 'text-[var(--ui-color-text-low)] hover:text-[var(--ui-color-text-high)]'
|
||||||
flex={1}
|
}`}
|
||||||
minWidth="140px"
|
|
||||||
px={3}
|
|
||||||
py={1.5}
|
|
||||||
rounded="full"
|
|
||||||
transition="all 0.2s"
|
|
||||||
textAlign="left"
|
|
||||||
bg={isSelected ? 'bg-primary-blue' : 'transparent'}
|
|
||||||
color={isSelected ? 'text-white' : 'text-gray-400'}
|
|
||||||
opacity={option.disabled ? 0.5 : 1}
|
|
||||||
cursor={option.disabled ? 'not-allowed' : 'pointer'}
|
|
||||||
border="none"
|
|
||||||
>
|
>
|
||||||
<Stack gap={0.5}>
|
{option.icon}
|
||||||
<Text size="xs" weight="medium" color="inherit">{option.label}</Text>
|
{option.label}
|
||||||
{option.description && (
|
</button>
|
||||||
<Text
|
|
||||||
size="xs"
|
|
||||||
color={isSelected ? 'text-white' : 'text-gray-400'}
|
|
||||||
fontSize="10px"
|
|
||||||
opacity={isSelected ? 0.8 : 1}
|
|
||||||
>
|
|
||||||
{option.description}
|
|
||||||
</Text>
|
|
||||||
)}
|
|
||||||
</Stack>
|
|
||||||
</Box>
|
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
</Stack>
|
</Surface>
|
||||||
);
|
);
|
||||||
}
|
};
|
||||||
|
|||||||
@@ -1,64 +1,90 @@
|
|||||||
import React, { forwardRef, ReactNode } from 'react';
|
import React, { forwardRef, SelectHTMLAttributes } from 'react';
|
||||||
import { Box } from './primitives/Box';
|
import { Box } from './primitives/Box';
|
||||||
import { Stack } from './primitives/Stack';
|
|
||||||
import { Text } from './Text';
|
import { Text } from './Text';
|
||||||
|
|
||||||
interface SelectOption {
|
export interface SelectOption {
|
||||||
value: string;
|
value: string | number;
|
||||||
label: string;
|
label: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface SelectProps extends React.SelectHTMLAttributes<HTMLSelectElement> {
|
export interface SelectProps extends Omit<SelectHTMLAttributes<HTMLSelectElement>, 'size'> {
|
||||||
label?: string;
|
label?: string;
|
||||||
|
options: SelectOption[];
|
||||||
|
error?: string;
|
||||||
|
hint?: string;
|
||||||
fullWidth?: boolean;
|
fullWidth?: boolean;
|
||||||
pl?: number;
|
size?: 'sm' | 'md' | 'lg';
|
||||||
errorMessage?: string;
|
|
||||||
variant?: 'default' | 'error';
|
|
||||||
options?: SelectOption[];
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export const Select = forwardRef<HTMLSelectElement, SelectProps>(
|
export const Select = forwardRef<HTMLSelectElement, SelectProps>(({
|
||||||
({ label, fullWidth = true, pl, errorMessage, variant = 'default', options, children, className = '', style, ...props }, ref) => {
|
label,
|
||||||
const isError = variant === 'error' || !!errorMessage;
|
options,
|
||||||
|
error,
|
||||||
const variantClasses = isError
|
hint,
|
||||||
? 'border-warning-amber focus:border-warning-amber'
|
fullWidth = false,
|
||||||
: 'border-charcoal-outline focus:border-primary-blue';
|
size = 'md',
|
||||||
|
...props
|
||||||
const defaultClasses = `${fullWidth ? 'w-full' : 'w-auto'} px-3 py-2 bg-deep-graphite border rounded-lg text-white focus:outline-none transition-colors`;
|
}, ref) => {
|
||||||
const classes = [
|
const sizeClasses = {
|
||||||
defaultClasses,
|
sm: 'px-3 py-1.5 text-xs',
|
||||||
variantClasses,
|
md: 'px-4 py-2 text-sm',
|
||||||
pl ? `pl-${pl}` : '',
|
lg: 'px-4 py-3 text-base'
|
||||||
className
|
};
|
||||||
].filter(Boolean).join(' ');
|
|
||||||
|
|
||||||
return (
|
const baseClasses = 'bg-[var(--ui-color-bg-surface)] border border-[var(--ui-color-border-default)] text-[var(--ui-color-text-high)] focus:outline-none focus:border-[var(--ui-color-intent-primary)] transition-colors appearance-none';
|
||||||
<Stack gap={1.5} fullWidth={fullWidth}>
|
const errorClasses = error ? 'border-[var(--ui-color-intent-critical)]' : '';
|
||||||
{label && (
|
const widthClasses = fullWidth ? 'w-full' : '';
|
||||||
<Text as="label" size="xs" weight="bold" color="text-gray-500" uppercase letterSpacing="wider">
|
|
||||||
|
const classes = [
|
||||||
|
baseClasses,
|
||||||
|
sizeClasses[size],
|
||||||
|
errorClasses,
|
||||||
|
widthClasses,
|
||||||
|
].filter(Boolean).join(' ');
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Box width={fullWidth ? '100%' : undefined}>
|
||||||
|
{label && (
|
||||||
|
<Box marginBottom={1.5}>
|
||||||
|
<Text as="label" size="xs" weight="bold" variant="low">
|
||||||
{label}
|
{label}
|
||||||
</Text>
|
</Text>
|
||||||
)}
|
</Box>
|
||||||
<Box
|
)}
|
||||||
as="select"
|
<div className="relative">
|
||||||
ref={ref}
|
<select
|
||||||
className={classes}
|
ref={ref}
|
||||||
style={style}
|
className={classes}
|
||||||
{...props}
|
{...props}
|
||||||
>
|
>
|
||||||
{options ? options.map(opt => (
|
{options.map((option) => (
|
||||||
<option key={opt.value} value={opt.value}>{opt.label}</option>
|
<option key={option.value} value={option.value}>
|
||||||
)) : children}
|
{option.label}
|
||||||
</Box>
|
</option>
|
||||||
{errorMessage && (
|
))}
|
||||||
<Text size="xs" color="text-warning-amber" mt={1}>
|
</select>
|
||||||
{errorMessage}
|
<div className="absolute inset-y-0 right-0 flex items-center px-2 pointer-events-none">
|
||||||
|
<svg className="w-4 h-4 text-[var(--ui-color-text-low)]" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M19 9l-7 7-7-7" />
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{error && (
|
||||||
|
<Box marginTop={1}>
|
||||||
|
<Text size="xs" variant="critical">
|
||||||
|
{error}
|
||||||
</Text>
|
</Text>
|
||||||
)}
|
</Box>
|
||||||
</Stack>
|
)}
|
||||||
);
|
{hint && !error && (
|
||||||
}
|
<Box marginTop={1}>
|
||||||
);
|
<Text size="xs" variant="low">
|
||||||
|
{hint}
|
||||||
|
</Text>
|
||||||
|
</Box>
|
||||||
|
)}
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
Select.displayName = 'Select';
|
Select.displayName = 'Select';
|
||||||
|
|||||||
@@ -1,37 +1,66 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { ChevronRight, LucideIcon } from 'lucide-react';
|
|
||||||
import { Box } from './primitives/Box';
|
import { Box } from './primitives/Box';
|
||||||
import { Text } from './Text';
|
import { Text } from './Text';
|
||||||
import { Icon } from './Icon';
|
import { Icon } from './Icon';
|
||||||
|
import { LucideIcon, ChevronRight } from 'lucide-react';
|
||||||
import { Link } from './Link';
|
import { Link } from './Link';
|
||||||
|
|
||||||
interface SidebarActionLinkProps {
|
export interface SidebarActionLinkProps {
|
||||||
href: string;
|
|
||||||
icon: LucideIcon;
|
|
||||||
label: string;
|
label: string;
|
||||||
iconColor?: string;
|
icon: LucideIcon;
|
||||||
iconBgColor?: string;
|
href: string;
|
||||||
|
isActive?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function SidebarActionLink({
|
export const SidebarActionLink = ({
|
||||||
|
label,
|
||||||
|
icon,
|
||||||
href,
|
href,
|
||||||
icon,
|
isActive = false
|
||||||
label,
|
}: SidebarActionLinkProps) => {
|
||||||
iconColor = 'text-primary-blue',
|
|
||||||
iconBgColor = 'bg-primary-blue/10',
|
|
||||||
}: SidebarActionLinkProps) {
|
|
||||||
return (
|
return (
|
||||||
<Link
|
<Link
|
||||||
href={href}
|
href={href}
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
block
|
underline="none"
|
||||||
className="flex items-center gap-3 p-3 rounded-lg bg-deep-graphite hover:bg-iron-gray/50 transition-all"
|
|
||||||
>
|
>
|
||||||
<Box p={2} className={iconBgColor} rounded="lg" display="flex" center>
|
<Box
|
||||||
<Icon icon={icon} size={4} className={iconColor} />
|
display="flex"
|
||||||
|
alignItems="center"
|
||||||
|
gap={3}
|
||||||
|
padding={3}
|
||||||
|
rounded="lg"
|
||||||
|
bg={isActive ? 'var(--ui-color-bg-surface-muted)' : 'transparent'}
|
||||||
|
className="group hover:bg-white/5 transition-colors"
|
||||||
|
>
|
||||||
|
<Box
|
||||||
|
padding={2}
|
||||||
|
rounded="md"
|
||||||
|
bg={isActive ? 'var(--ui-color-intent-primary)' : 'var(--ui-color-bg-surface-muted)'}
|
||||||
|
display="flex"
|
||||||
|
center
|
||||||
|
>
|
||||||
|
<Icon
|
||||||
|
icon={icon}
|
||||||
|
size={4}
|
||||||
|
intent={isActive ? 'high' : 'low'}
|
||||||
|
/>
|
||||||
|
</Box>
|
||||||
|
<Text
|
||||||
|
size="sm"
|
||||||
|
variant={isActive ? 'high' : 'med'}
|
||||||
|
weight={isActive ? 'bold' : 'medium'}
|
||||||
|
flexGrow={1}
|
||||||
|
>
|
||||||
|
{label}
|
||||||
|
</Text>
|
||||||
|
<Icon
|
||||||
|
icon={ChevronRight}
|
||||||
|
size={4}
|
||||||
|
intent="low"
|
||||||
|
className="opacity-0 group-hover:opacity-100 transition-opacity"
|
||||||
|
/>
|
||||||
</Box>
|
</Box>
|
||||||
<Text size="sm" color="text-white" flexGrow={1}>{label}</Text>
|
|
||||||
<Icon icon={ChevronRight} size={4} color="text-gray-500" />
|
|
||||||
</Link>
|
</Link>
|
||||||
);
|
);
|
||||||
}
|
};
|
||||||
|
|||||||
@@ -1,35 +1,31 @@
|
|||||||
import React from 'react';
|
import React, { forwardRef, ChangeEvent } from 'react';
|
||||||
import { Box } from './primitives/Box';
|
import { Box } from './primitives/Box';
|
||||||
|
|
||||||
interface CheckboxProps {
|
export interface SimpleCheckboxProps {
|
||||||
checked: boolean;
|
checked: boolean;
|
||||||
onChange: (checked: boolean) => void;
|
onChange: (checked: boolean) => void;
|
||||||
disabled?: boolean;
|
disabled?: boolean;
|
||||||
'aria-label'?: string;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
export const SimpleCheckbox = forwardRef<HTMLInputElement, SimpleCheckboxProps>(({
|
||||||
* SimpleCheckbox
|
checked,
|
||||||
*
|
onChange,
|
||||||
* A checkbox without a label for use in tables.
|
disabled = false
|
||||||
*/
|
}, ref) => {
|
||||||
export function SimpleCheckbox({ checked, onChange, disabled, 'aria-label': ariaLabel }: CheckboxProps) {
|
const handleChange = (e: ChangeEvent<HTMLInputElement>) => {
|
||||||
|
onChange(e.target.checked);
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box
|
<input
|
||||||
as="input"
|
ref={ref}
|
||||||
type="checkbox"
|
type="checkbox"
|
||||||
checked={checked}
|
checked={checked}
|
||||||
onChange={(e: React.ChangeEvent<HTMLInputElement>) => onChange(e.target.checked)}
|
onChange={handleChange}
|
||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
w="4"
|
className="w-4 h-4 rounded-none border-[var(--ui-color-border-default)] bg-[var(--ui-color-bg-surface)] text-[var(--ui-color-intent-primary)] focus:ring-[var(--ui-color-intent-primary)] cursor-pointer disabled:cursor-not-allowed"
|
||||||
h="4"
|
|
||||||
bg="bg-deep-graphite"
|
|
||||||
border
|
|
||||||
borderColor="border-charcoal-outline"
|
|
||||||
rounded="sm"
|
|
||||||
aria-label={ariaLabel}
|
|
||||||
ring="primary-blue"
|
|
||||||
color="text-primary-blue"
|
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
});
|
||||||
|
|
||||||
|
SimpleCheckbox.displayName = 'SimpleCheckbox';
|
||||||
|
|||||||
@@ -1,23 +1,44 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { Box } from './primitives/Box';
|
import { Box } from './primitives/Box';
|
||||||
|
|
||||||
interface SkeletonProps {
|
export interface SkeletonProps {
|
||||||
width?: string | number;
|
width?: string | number;
|
||||||
height?: string | number;
|
height?: string | number;
|
||||||
circle?: boolean;
|
variant?: 'text' | 'circular' | 'rectangular';
|
||||||
className?: string;
|
animation?: 'pulse' | 'wave' | 'none';
|
||||||
}
|
}
|
||||||
|
|
||||||
export function Skeleton({ width, height, circle, className = '' }: SkeletonProps) {
|
export const Skeleton = ({
|
||||||
|
width,
|
||||||
|
height,
|
||||||
|
variant = 'rectangular',
|
||||||
|
animation = 'pulse'
|
||||||
|
}: SkeletonProps) => {
|
||||||
|
const variantClasses = {
|
||||||
|
text: 'rounded-sm',
|
||||||
|
circular: 'rounded-full',
|
||||||
|
rectangular: 'rounded-none',
|
||||||
|
};
|
||||||
|
|
||||||
|
const animationClasses = {
|
||||||
|
pulse: 'animate-pulse',
|
||||||
|
wave: 'animate-shimmer', // Assuming shimmer is defined
|
||||||
|
none: '',
|
||||||
|
};
|
||||||
|
|
||||||
|
const classes = [
|
||||||
|
'bg-[var(--ui-color-bg-surface-muted)]',
|
||||||
|
variantClasses[variant],
|
||||||
|
animationClasses[animation],
|
||||||
|
].join(' ');
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box
|
<Box
|
||||||
w={width}
|
width={width}
|
||||||
h={height}
|
height={height}
|
||||||
rounded={circle ? 'full' : 'md'}
|
className={classes}
|
||||||
bg="bg-white/5"
|
|
||||||
className={`animate-pulse ${className}`}
|
|
||||||
role="status"
|
role="status"
|
||||||
aria-label="Loading..."
|
aria-label="Loading..."
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
};
|
||||||
|
|||||||
@@ -1,30 +1,38 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { LucideIcon } from 'lucide-react';
|
import { Surface } from './primitives/Surface';
|
||||||
import { Stack } from './primitives/Stack';
|
|
||||||
import { Box } from './primitives/Box';
|
import { Box } from './primitives/Box';
|
||||||
import { Text } from './Text';
|
import { Text } from './Text';
|
||||||
import { Icon } from './Icon';
|
import { Icon } from './Icon';
|
||||||
import { Surface } from './primitives/Surface';
|
import { LucideIcon } from 'lucide-react';
|
||||||
|
|
||||||
interface StatBoxProps {
|
export interface StatBoxProps {
|
||||||
icon: LucideIcon;
|
|
||||||
label: string;
|
label: string;
|
||||||
value: string | number;
|
value: string | number;
|
||||||
color?: string;
|
icon: LucideIcon;
|
||||||
|
intent?: 'primary' | 'success' | 'warning' | 'critical' | 'telemetry';
|
||||||
}
|
}
|
||||||
|
|
||||||
export function StatBox({ icon, label, value, color = 'text-primary-blue' }: StatBoxProps) {
|
export const StatBox = ({
|
||||||
|
label,
|
||||||
|
value,
|
||||||
|
icon,
|
||||||
|
intent = 'primary'
|
||||||
|
}: StatBoxProps) => {
|
||||||
return (
|
return (
|
||||||
<Surface variant="muted" rounded="xl" border padding={4}>
|
<Surface variant="muted" rounded="xl" padding={4} style={{ border: '1px solid var(--ui-color-border-default)' }}>
|
||||||
<Stack direction="row" align="center" gap={3}>
|
<Box display="flex" alignItems="center" gap={4}>
|
||||||
<Surface variant="muted" rounded="lg" padding={2}>
|
<Box padding={2} rounded="lg" bg="var(--ui-color-bg-surface-muted)">
|
||||||
<Icon icon={icon} size={5} color={color} />
|
<Icon icon={icon} size={5} intent={intent} />
|
||||||
</Surface>
|
|
||||||
<Box>
|
|
||||||
<Text size="2xl" weight="bold" color="text-white" block>{value}</Text>
|
|
||||||
<Text size="xs" color="text-gray-500" block>{label}</Text>
|
|
||||||
</Box>
|
</Box>
|
||||||
</Stack>
|
<Box>
|
||||||
|
<Text size="xs" weight="bold" variant="low" uppercase>
|
||||||
|
{label}
|
||||||
|
</Text>
|
||||||
|
<Text size="xl" weight="bold" variant="high" block marginTop={0.5}>
|
||||||
|
{value}
|
||||||
|
</Text>
|
||||||
|
</Box>
|
||||||
|
</Box>
|
||||||
</Surface>
|
</Surface>
|
||||||
);
|
);
|
||||||
}
|
};
|
||||||
|
|||||||
@@ -1,115 +1,68 @@
|
|||||||
import React, { ReactNode } from 'react';
|
import React, { ReactNode } from 'react';
|
||||||
import { Box } from './primitives/Box';
|
|
||||||
import { Stack } from './primitives/Stack';
|
|
||||||
import { Text } from './Text';
|
|
||||||
import { Card } from './Card';
|
import { Card } from './Card';
|
||||||
|
import { Box } from './primitives/Box';
|
||||||
|
import { Text } from './Text';
|
||||||
import { Icon } from './Icon';
|
import { Icon } from './Icon';
|
||||||
import { LucideIcon } from 'lucide-react';
|
import { LucideIcon } from 'lucide-react';
|
||||||
|
|
||||||
interface StatCardProps {
|
export interface StatCardProps {
|
||||||
label: string;
|
label: string;
|
||||||
value: string | number;
|
value: string | number;
|
||||||
icon?: LucideIcon;
|
icon?: LucideIcon;
|
||||||
|
intent?: 'primary' | 'success' | 'warning' | 'critical' | 'telemetry';
|
||||||
trend?: {
|
trend?: {
|
||||||
value: number;
|
value: number;
|
||||||
isPositive: boolean;
|
isPositive: boolean;
|
||||||
};
|
};
|
||||||
variant?: 'default' | 'primary' | 'success' | 'warning' | 'danger' | 'info';
|
footer?: ReactNode;
|
||||||
className?: string;
|
|
||||||
onClick?: () => void;
|
|
||||||
prefix?: string;
|
|
||||||
suffix?: string;
|
|
||||||
delay?: number;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function StatCard({
|
export const StatCard = ({
|
||||||
label,
|
label,
|
||||||
value,
|
value,
|
||||||
icon,
|
icon,
|
||||||
|
intent = 'primary',
|
||||||
trend,
|
trend,
|
||||||
variant = 'default',
|
footer
|
||||||
className = '',
|
}: StatCardProps) => {
|
||||||
onClick,
|
return (
|
||||||
prefix,
|
<Card variant="default">
|
||||||
suffix,
|
<Box display="flex" alignItems="start" justifyContent="between" marginBottom={4}>
|
||||||
delay,
|
<Box>
|
||||||
}: StatCardProps) {
|
<Text size="xs" weight="bold" variant="low" uppercase>
|
||||||
const variantClasses = {
|
|
||||||
default: 'bg-panel-gray border-border-gray',
|
|
||||||
primary: 'bg-primary-accent/5 border-primary-accent/20',
|
|
||||||
success: 'bg-success-green/5 border-success-green/20',
|
|
||||||
warning: 'bg-warning-amber/5 border-warning-amber/20',
|
|
||||||
danger: 'bg-critical-red/5 border-critical-red/20',
|
|
||||||
info: 'bg-telemetry-aqua/5 border-telemetry-aqua/20',
|
|
||||||
};
|
|
||||||
|
|
||||||
const iconBgClasses = {
|
|
||||||
default: 'bg-white/5',
|
|
||||||
primary: 'bg-primary-accent/10',
|
|
||||||
success: 'bg-success-green/10',
|
|
||||||
warning: 'bg-warning-amber/10',
|
|
||||||
danger: 'bg-critical-red/10',
|
|
||||||
info: 'bg-telemetry-aqua/10',
|
|
||||||
};
|
|
||||||
|
|
||||||
const iconColorClasses = {
|
|
||||||
default: 'text-gray-400',
|
|
||||||
primary: 'text-primary-accent',
|
|
||||||
success: 'text-success-green',
|
|
||||||
warning: 'text-warning-amber',
|
|
||||||
danger: 'text-critical-red',
|
|
||||||
info: 'text-telemetry-aqua',
|
|
||||||
};
|
|
||||||
|
|
||||||
const cardContent = (
|
|
||||||
<Card variant="default" p={5} className={`${variantClasses[variant]} ${className} h-full`}>
|
|
||||||
<Stack gap={3}>
|
|
||||||
<Stack direction="row" align="center" justify="between">
|
|
||||||
<Text size="xs" weight="bold" color="text-gray-500" uppercase letterSpacing="widest">
|
|
||||||
{label}
|
{label}
|
||||||
</Text>
|
</Text>
|
||||||
{icon && (
|
<Text size="2xl" weight="bold" variant="high" block marginTop={1}>
|
||||||
<Box
|
{value}
|
||||||
p={2}
|
|
||||||
rounded="lg"
|
|
||||||
bg={iconBgClasses[variant]}
|
|
||||||
className={iconColorClasses[variant]}
|
|
||||||
>
|
|
||||||
<Icon icon={icon} size={5} />
|
|
||||||
</Box>
|
|
||||||
)}
|
|
||||||
</Stack>
|
|
||||||
|
|
||||||
<Stack gap={1}>
|
|
||||||
<Text size="3xl" weight="bold" color="text-white">
|
|
||||||
{prefix}{value}{suffix}
|
|
||||||
</Text>
|
</Text>
|
||||||
{trend && (
|
</Box>
|
||||||
<Stack direction="row" align="center" gap={1}>
|
{icon && (
|
||||||
<Text
|
<Box padding={2} rounded="lg" bg="var(--ui-color-bg-surface-muted)">
|
||||||
size="xs"
|
<Icon icon={icon} size={5} intent={intent} />
|
||||||
weight="bold"
|
</Box>
|
||||||
color={trend.isPositive ? 'text-success-green' : 'text-critical-red'}
|
)}
|
||||||
>
|
</Box>
|
||||||
{trend.isPositive ? '+' : ''}{trend.value}%
|
|
||||||
</Text>
|
{trend && (
|
||||||
<Text size="xs" color="text-gray-500">
|
<Box display="flex" alignItems="center" gap={1} marginBottom={footer ? 4 : 0}>
|
||||||
vs last period
|
<Text
|
||||||
</Text>
|
size="xs"
|
||||||
</Stack>
|
weight="bold"
|
||||||
)}
|
variant={trend.isPositive ? 'success' : 'critical'}
|
||||||
</Stack>
|
>
|
||||||
</Stack>
|
{trend.isPositive ? '+' : '-'}{Math.abs(trend.value)}%
|
||||||
|
</Text>
|
||||||
|
<Text size="xs" variant="low">
|
||||||
|
vs last period
|
||||||
|
</Text>
|
||||||
|
</Box>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{footer && (
|
||||||
|
<Box borderTop paddingTop={4}>
|
||||||
|
{footer}
|
||||||
|
</Box>
|
||||||
|
)}
|
||||||
</Card>
|
</Card>
|
||||||
);
|
);
|
||||||
|
};
|
||||||
if (onClick) {
|
|
||||||
return (
|
|
||||||
<Box as="button" onClick={onClick} w="full" textAlign="left" className="focus:outline-none">
|
|
||||||
{cardContent}
|
|
||||||
</Box>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return cardContent;
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -1,58 +1,21 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { Grid } from './primitives/Grid';
|
import { Grid } from './primitives/Grid';
|
||||||
import { GridItem } from './primitives/GridItem';
|
import { StatBox, StatBoxProps } from './StatBox';
|
||||||
import { Surface } from './primitives/Surface';
|
|
||||||
import { Text } from './Text';
|
|
||||||
import { Stack } from './primitives/Stack';
|
|
||||||
|
|
||||||
type GridCols = 1 | 2 | 3 | 4 | 5 | 6 | 12;
|
export interface StatGridProps {
|
||||||
|
stats: StatBoxProps[];
|
||||||
interface StatItem {
|
columns?: number;
|
||||||
label: string;
|
|
||||||
value: string | number;
|
|
||||||
subValue?: string;
|
|
||||||
color?: string;
|
|
||||||
icon?: React.ElementType;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
interface StatGridProps {
|
export const StatGrid = ({
|
||||||
stats: StatItem[];
|
stats,
|
||||||
cols?: GridCols;
|
columns = 3
|
||||||
mdCols?: GridCols;
|
}: StatGridProps) => {
|
||||||
lgCols?: GridCols;
|
|
||||||
className?: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function StatGrid({ stats, cols = 2, mdCols = 3, lgCols = 4, className = '' }: StatGridProps) {
|
|
||||||
return (
|
return (
|
||||||
<Grid
|
<Grid columns={columns} gap={4}>
|
||||||
cols={cols}
|
|
||||||
mdCols={mdCols}
|
|
||||||
lgCols={lgCols}
|
|
||||||
gap={4}
|
|
||||||
className={className}
|
|
||||||
>
|
|
||||||
{stats.map((stat, index) => (
|
{stats.map((stat, index) => (
|
||||||
<GridItem key={index}>
|
<StatBox key={index} {...stat} />
|
||||||
<Surface variant="muted" padding={4} rounded="lg" border className="h-full">
|
|
||||||
<Stack gap={1}>
|
|
||||||
<Text size="xs" color="text-gray-500" uppercase weight="semibold" letterSpacing="wider">
|
|
||||||
{stat.label}
|
|
||||||
</Text>
|
|
||||||
<Stack direction="row" align="baseline" gap={2}>
|
|
||||||
<Text size="2xl" weight="bold" font="mono" color={stat.color || 'text-white'}>
|
|
||||||
{stat.value}
|
|
||||||
</Text>
|
|
||||||
{stat.subValue && (
|
|
||||||
<Text size="xs" color="text-gray-500" font="mono">
|
|
||||||
{stat.subValue}
|
|
||||||
</Text>
|
|
||||||
)}
|
|
||||||
</Stack>
|
|
||||||
</Stack>
|
|
||||||
</Surface>
|
|
||||||
</GridItem>
|
|
||||||
))}
|
))}
|
||||||
</Grid>
|
</Grid>
|
||||||
);
|
);
|
||||||
}
|
};
|
||||||
|
|||||||
@@ -1,36 +1,36 @@
|
|||||||
import React, { ReactNode } from 'react';
|
import React from 'react';
|
||||||
import { Box } from './primitives/Box';
|
import { Box } from './primitives/Box';
|
||||||
import { Stack } from './primitives/Stack';
|
|
||||||
import { Text } from './Text';
|
import { Text } from './Text';
|
||||||
import { Icon } from './Icon';
|
import { Stack } from './primitives/Stack';
|
||||||
import { LucideIcon } from 'lucide-react';
|
|
||||||
|
|
||||||
interface StatGridItemProps {
|
export interface StatGridItemProps {
|
||||||
label: string;
|
label: string;
|
||||||
value: string | number;
|
value: string | number;
|
||||||
icon?: LucideIcon;
|
icon?: React.ReactNode;
|
||||||
color?: string;
|
intent?: 'primary' | 'success' | 'warning' | 'critical' | 'high' | 'med' | 'low';
|
||||||
|
color?: string; // Alias for intent
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
export const StatGridItem = ({
|
||||||
* StatGridItem
|
label,
|
||||||
*
|
value,
|
||||||
* A simple stat display for use in a grid.
|
icon,
|
||||||
*/
|
intent = 'high',
|
||||||
export function StatGridItem({ label, value, icon, color = 'text-primary-blue' }: StatGridItemProps) {
|
color
|
||||||
|
}: StatGridItemProps) => {
|
||||||
return (
|
return (
|
||||||
<Box p={4} textAlign="center">
|
<Box padding={4} textAlign="center">
|
||||||
{icon && (
|
{icon && (
|
||||||
<Stack direction="row" align="center" justify="center" gap={2} mb={1} color={color}>
|
<Stack direction="row" align="center" justify="center" gap={2} marginBottom={1}>
|
||||||
<Icon icon={icon} size={4} />
|
{icon}
|
||||||
</Stack>
|
</Stack>
|
||||||
)}
|
)}
|
||||||
<Text size="2xl" weight="bold" color="text-white" block>
|
<Text size="2xl" weight="bold" variant={intent} color={color} block>
|
||||||
{value}
|
{value}
|
||||||
</Text>
|
</Text>
|
||||||
<Text size="xs" weight="medium" color="text-gray-500" uppercase letterSpacing="wider">
|
<Text size="xs" weight="medium" variant="low" uppercase letterSpacing="wider">
|
||||||
{label}
|
{label}
|
||||||
</Text>
|
</Text>
|
||||||
</Box>
|
</Box>
|
||||||
);
|
);
|
||||||
}
|
};
|
||||||
|
|||||||
@@ -2,18 +2,23 @@ import React from 'react';
|
|||||||
import { Box } from './primitives/Box';
|
import { Box } from './primitives/Box';
|
||||||
import { Text } from './Text';
|
import { Text } from './Text';
|
||||||
|
|
||||||
interface StatItemProps {
|
export interface StatItemProps {
|
||||||
label: string;
|
label: string;
|
||||||
value: string | number;
|
value: string | number;
|
||||||
color?: string;
|
intent?: 'primary' | 'success' | 'warning' | 'critical' | 'high' | 'med' | 'low';
|
||||||
align?: 'left' | 'center' | 'right';
|
align?: 'left' | 'center' | 'right';
|
||||||
}
|
}
|
||||||
|
|
||||||
export function StatItem({ label, value, color = 'text-white', align = 'left' }: StatItemProps) {
|
export const StatItem = ({
|
||||||
|
label,
|
||||||
|
value,
|
||||||
|
intent = 'high',
|
||||||
|
align = 'left'
|
||||||
|
}: StatItemProps) => {
|
||||||
return (
|
return (
|
||||||
<Box display="flex" flexDirection="column" alignItems={align === 'center' ? 'center' : align === 'right' ? 'flex-end' : 'flex-start'}>
|
<Box textAlign={align}>
|
||||||
<Text size="xs" color="text-gray-500" block mb={0.5}>{label}</Text>
|
<Text size="xs" variant="low" block marginBottom={0.5}>{label}</Text>
|
||||||
<Text size="sm" weight="semibold" color={color}>{value}</Text>
|
<Text size="sm" weight="semibold" variant={intent}>{value}</Text>
|
||||||
</Box>
|
</Box>
|
||||||
);
|
);
|
||||||
}
|
};
|
||||||
|
|||||||
@@ -3,36 +3,36 @@ import { Icon } from './Icon';
|
|||||||
import { LucideIcon } from 'lucide-react';
|
import { LucideIcon } from 'lucide-react';
|
||||||
import { Stack } from './primitives/Stack';
|
import { Stack } from './primitives/Stack';
|
||||||
|
|
||||||
interface StatusBadgeProps {
|
export interface StatusBadgeProps {
|
||||||
children: React.ReactNode;
|
children: React.ReactNode;
|
||||||
variant?: 'success' | 'warning' | 'error' | 'info' | 'neutral' | 'pending';
|
variant?: 'success' | 'warning' | 'error' | 'info' | 'neutral' | 'pending';
|
||||||
className?: string;
|
|
||||||
icon?: LucideIcon;
|
icon?: LucideIcon;
|
||||||
|
className?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function StatusBadge({
|
export function StatusBadge({
|
||||||
children,
|
children,
|
||||||
variant = 'success',
|
variant = 'success',
|
||||||
className = '',
|
|
||||||
icon,
|
icon,
|
||||||
|
className
|
||||||
}: StatusBadgeProps) {
|
}: StatusBadgeProps) {
|
||||||
const variantClasses = {
|
const variantClasses = {
|
||||||
success: 'bg-performance-green/20 text-performance-green border-performance-green/30',
|
success: 'bg-[var(--ui-color-intent-success)]/20 text-[var(--ui-color-intent-success)] border-[var(--ui-color-intent-success)]/30',
|
||||||
warning: 'bg-warning-amber/20 text-warning-amber border-warning-amber/30',
|
warning: 'bg-[var(--ui-color-intent-warning)]/20 text-[var(--ui-color-intent-warning)] border-[var(--ui-color-intent-warning)]/30',
|
||||||
error: 'bg-red-600/20 text-red-400 border-red-600/30',
|
error: 'bg-[var(--ui-color-intent-critical)]/20 text-[var(--ui-color-intent-critical)] border-[var(--ui-color-intent-critical)]/30',
|
||||||
info: 'bg-blue-500/20 text-blue-400 border-blue-500/30',
|
info: 'bg-[var(--ui-color-intent-primary)]/20 text-[var(--ui-color-intent-primary)] border-[var(--ui-color-intent-primary)]/30',
|
||||||
neutral: 'bg-iron-gray text-gray-400 border-charcoal-outline',
|
neutral: 'bg-[var(--ui-color-bg-surface-muted)] text-[var(--ui-color-text-med)] border-[var(--ui-color-border-default)]',
|
||||||
pending: 'bg-warning-amber/20 text-warning-amber border-warning-amber/30',
|
pending: 'bg-[var(--ui-color-intent-warning)]/20 text-[var(--ui-color-intent-warning)] border-[var(--ui-color-intent-warning)]/30',
|
||||||
};
|
};
|
||||||
|
|
||||||
const classes = [
|
const classes = [
|
||||||
'px-2 py-0.5 text-xs rounded-full border font-medium inline-flex items-center',
|
'px-2 py-0.5 text-[10px] uppercase tracking-wider rounded-full border font-bold inline-flex items-center',
|
||||||
variantClasses[variant],
|
variantClasses[variant],
|
||||||
className
|
className
|
||||||
].filter(Boolean).join(' ');
|
].join(' ');
|
||||||
|
|
||||||
const content = icon ? (
|
const content = icon ? (
|
||||||
<Stack direction="row" align="center" gap={1.5}>
|
<Stack direction="row" align="center" gap={1}>
|
||||||
<Icon icon={icon} size={3} />
|
<Icon icon={icon} size={3} />
|
||||||
{children}
|
{children}
|
||||||
</Stack>
|
</Stack>
|
||||||
@@ -43,4 +43,4 @@ export function StatusBadge({
|
|||||||
{content}
|
{content}
|
||||||
</span>
|
</span>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,11 +1,11 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { Box } from './primitives/Box';
|
import { Box } from './primitives/Box';
|
||||||
|
|
||||||
interface StatusDotProps {
|
export interface StatusDotProps {
|
||||||
color?: string;
|
intent?: 'primary' | 'success' | 'warning' | 'critical' | 'telemetry';
|
||||||
|
color?: string; // Alias for intent or custom color
|
||||||
pulse?: boolean;
|
pulse?: boolean;
|
||||||
size?: number;
|
size?: 'sm' | 'md' | 'lg' | number;
|
||||||
className?: string;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -14,28 +14,41 @@ interface StatusDotProps {
|
|||||||
* A simple status indicator dot with optional pulse effect.
|
* A simple status indicator dot with optional pulse effect.
|
||||||
*/
|
*/
|
||||||
export function StatusDot({
|
export function StatusDot({
|
||||||
color = '#4ED4E0',
|
intent = 'telemetry',
|
||||||
|
color: colorProp,
|
||||||
pulse = false,
|
pulse = false,
|
||||||
size = 2,
|
size = 'md',
|
||||||
className = ''
|
|
||||||
}: StatusDotProps) {
|
}: StatusDotProps) {
|
||||||
const sizeClass = `w-${size} h-${size}`;
|
const intentColorMap = {
|
||||||
|
primary: 'var(--ui-color-intent-primary)',
|
||||||
|
success: 'var(--ui-color-intent-success)',
|
||||||
|
warning: 'var(--ui-color-intent-warning)',
|
||||||
|
critical: 'var(--ui-color-intent-critical)',
|
||||||
|
telemetry: 'var(--ui-color-intent-telemetry)',
|
||||||
|
};
|
||||||
|
|
||||||
|
const sizeMap = {
|
||||||
|
sm: '0.375rem',
|
||||||
|
md: '0.5rem',
|
||||||
|
lg: '0.75rem',
|
||||||
|
};
|
||||||
|
|
||||||
|
const color = colorProp || intentColorMap[intent];
|
||||||
|
const dimension = typeof size === 'string' ? sizeMap[size] : `${size * 0.25}rem`;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box position="relative" className={`${sizeClass} ${className}`}>
|
<Box position="relative" width={dimension} height={dimension}>
|
||||||
<Box
|
<Box
|
||||||
w="full"
|
fullWidth
|
||||||
h="full"
|
fullHeight
|
||||||
rounded="full"
|
style={{ backgroundColor: color, borderRadius: '9999px' }}
|
||||||
style={{ backgroundColor: color }}
|
|
||||||
/>
|
/>
|
||||||
{pulse && (
|
{pulse && (
|
||||||
<Box
|
<Box
|
||||||
position="absolute"
|
position="absolute"
|
||||||
inset={0}
|
inset={0}
|
||||||
rounded="full"
|
|
||||||
className="animate-ping"
|
className="animate-ping"
|
||||||
style={{ backgroundColor: color, opacity: 0.75 }}
|
style={{ backgroundColor: color, opacity: 0.75, borderRadius: '9999px' }}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</Box>
|
</Box>
|
||||||
|
|||||||
@@ -1,110 +1,94 @@
|
|||||||
|
|
||||||
|
|
||||||
import { LucideIcon } from 'lucide-react';
|
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { Box } from './primitives/Box';
|
import { Box } from './primitives/Box';
|
||||||
import { Text } from './Text';
|
import { Text } from './Text';
|
||||||
import { Icon } from './Icon';
|
import { StatusDot } from './StatusDot';
|
||||||
|
import { Badge } from './Badge';
|
||||||
|
|
||||||
interface StatusIndicatorProps {
|
export { Badge };
|
||||||
icon: LucideIcon;
|
|
||||||
label: string;
|
export interface StatusIndicatorProps {
|
||||||
|
status?: 'live' | 'upcoming' | 'completed' | 'cancelled' | 'pending';
|
||||||
|
variant?: string; // Alias for status
|
||||||
|
label?: string;
|
||||||
subLabel?: string;
|
subLabel?: string;
|
||||||
variant: 'success' | 'warning' | 'danger' | 'info';
|
size?: 'sm' | 'md' | 'lg';
|
||||||
|
icon?: any; // Alias for status dot
|
||||||
}
|
}
|
||||||
|
|
||||||
export function StatusIndicator({ icon, label, subLabel, variant }: StatusIndicatorProps) {
|
export const StatusIndicator = ({
|
||||||
const colors = {
|
status,
|
||||||
success: {
|
variant,
|
||||||
text: 'text-performance-green',
|
label,
|
||||||
bg: 'bg-green-500/10',
|
subLabel,
|
||||||
border: 'border-green-500/30',
|
size = 'md',
|
||||||
icon: 'rgb(16, 185, 129)'
|
icon
|
||||||
|
}: StatusIndicatorProps) => {
|
||||||
|
const activeStatus = (status || variant || 'pending') as any;
|
||||||
|
|
||||||
|
const configMap: any = {
|
||||||
|
live: {
|
||||||
|
intent: 'success' as const,
|
||||||
|
pulse: true,
|
||||||
|
text: 'Live',
|
||||||
},
|
},
|
||||||
warning: {
|
upcoming: {
|
||||||
text: 'text-warning-amber',
|
intent: 'primary' as const,
|
||||||
bg: 'bg-yellow-500/10',
|
pulse: false,
|
||||||
border: 'border-yellow-500/30',
|
text: 'Upcoming',
|
||||||
icon: 'rgb(245, 158, 11)'
|
},
|
||||||
|
completed: {
|
||||||
|
intent: 'telemetry' as const,
|
||||||
|
pulse: false,
|
||||||
|
text: 'Completed',
|
||||||
|
},
|
||||||
|
cancelled: {
|
||||||
|
intent: 'critical' as const,
|
||||||
|
pulse: false,
|
||||||
|
text: 'Cancelled',
|
||||||
|
},
|
||||||
|
pending: {
|
||||||
|
intent: 'warning' as const,
|
||||||
|
pulse: false,
|
||||||
|
text: 'Pending',
|
||||||
|
},
|
||||||
|
success: {
|
||||||
|
intent: 'success' as const,
|
||||||
|
pulse: false,
|
||||||
|
text: 'Success',
|
||||||
},
|
},
|
||||||
danger: {
|
danger: {
|
||||||
text: 'text-red-400',
|
intent: 'critical' as const,
|
||||||
bg: 'bg-red-500/10',
|
pulse: false,
|
||||||
border: 'border-red-500/30',
|
text: 'Danger',
|
||||||
icon: 'rgb(239, 68, 68)'
|
},
|
||||||
|
warning: {
|
||||||
|
intent: 'warning' as const,
|
||||||
|
pulse: false,
|
||||||
|
text: 'Warning',
|
||||||
},
|
},
|
||||||
info: {
|
|
||||||
text: 'text-primary-blue',
|
|
||||||
bg: 'bg-blue-500/10',
|
|
||||||
border: 'border-blue-500/30',
|
|
||||||
icon: 'rgb(59, 130, 246)'
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const config = colors[variant];
|
const config = configMap[activeStatus] || configMap.pending;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box
|
<Box display="flex" alignItems="center" gap={2}>
|
||||||
display="flex"
|
<StatusDot intent={config.intent} pulse={config.pulse} size={size === 'lg' ? 'lg' : size === 'sm' ? 'sm' : 'md'} />
|
||||||
alignItems="center"
|
<Box>
|
||||||
justifyContent="between"
|
<Text size={size === 'lg' ? 'md' : 'sm'} weight="bold" variant="high" uppercase>
|
||||||
p={2}
|
{label || config.text}
|
||||||
rounded="lg"
|
|
||||||
bg={config.bg}
|
|
||||||
border
|
|
||||||
borderColor={config.border}
|
|
||||||
>
|
|
||||||
<Box display="flex" alignItems="center" gap={2}>
|
|
||||||
<Icon icon={icon} size={4} color={config.icon} />
|
|
||||||
<Text size="sm" weight="semibold" color="text-white">{label}</Text>
|
|
||||||
</Box>
|
|
||||||
{subLabel && (
|
|
||||||
<Text size="xs" color="text-gray-400">
|
|
||||||
{subLabel}
|
|
||||||
</Text>
|
</Text>
|
||||||
)}
|
{subLabel && <Text size="xs" variant="low">{subLabel}</Text>}
|
||||||
|
</Box>
|
||||||
</Box>
|
</Box>
|
||||||
);
|
);
|
||||||
}
|
};
|
||||||
|
|
||||||
interface StatRowProps {
|
export const StatRow = ({ label, value, subLabel, variant, valueColor, valueFont }: { label: string, value: string, subLabel?: string, variant?: string, valueColor?: string, valueFont?: string }) => (
|
||||||
label: string;
|
<Box display="flex" alignItems="center" justifyContent="between" paddingY={2} borderBottom>
|
||||||
value: string | number;
|
<Box>
|
||||||
valueColor?: string;
|
<Text size="sm" variant="high">{label}</Text>
|
||||||
valueFont?: 'sans' | 'mono';
|
{subLabel && <Text size="xs" variant="low">{subLabel}</Text>}
|
||||||
}
|
|
||||||
|
|
||||||
export function StatRow({ label, value, valueColor = 'text-white', valueFont = 'sans' }: StatRowProps) {
|
|
||||||
return (
|
|
||||||
<Box display="flex" alignItems="center" justifyContent="between">
|
|
||||||
<Text size="xs" color="text-gray-500">{label}</Text>
|
|
||||||
<Text size="xs" weight="bold" color={valueColor} font={valueFont}>
|
|
||||||
{value}
|
|
||||||
</Text>
|
|
||||||
</Box>
|
</Box>
|
||||||
);
|
<Text size="sm" weight="bold" variant={variant as any || 'high'} color={valueColor} font={valueFont as any}>{value}</Text>
|
||||||
}
|
</Box>
|
||||||
|
);
|
||||||
interface BadgeProps {
|
|
||||||
children: React.ReactNode;
|
|
||||||
variant: 'success' | 'warning' | 'danger' | 'info' | 'gray';
|
|
||||||
size?: 'xs' | 'sm';
|
|
||||||
}
|
|
||||||
|
|
||||||
export function Badge({ children, variant, size = 'xs' }: BadgeProps) {
|
|
||||||
const variants = {
|
|
||||||
success: 'bg-green-500/20 text-performance-green',
|
|
||||||
warning: 'bg-yellow-500/20 text-warning-amber',
|
|
||||||
danger: 'bg-red-500/20 text-red-400',
|
|
||||||
info: 'bg-blue-500/20 text-primary-blue',
|
|
||||||
gray: 'bg-gray-500/20 text-gray-400'
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Box px={1} rounded="sm" bg={variants[variant]}>
|
|
||||||
<Text size={size} color="inherit">
|
|
||||||
{children}
|
|
||||||
</Text>
|
|
||||||
</Box>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -1,65 +1,63 @@
|
|||||||
import { User, Camera, Check } from 'lucide-react';
|
import React from 'react';
|
||||||
|
import { Box } from './primitives/Box';
|
||||||
|
import { Text } from './Text';
|
||||||
|
import { Check } from 'lucide-react';
|
||||||
|
import { Icon } from './Icon';
|
||||||
|
|
||||||
interface Step {
|
export interface Step {
|
||||||
id: number;
|
id: string;
|
||||||
label: string;
|
label: string;
|
||||||
icon: React.ElementType;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
interface StepIndicatorProps {
|
export interface StepIndicatorProps {
|
||||||
currentStep: number;
|
steps: Step[];
|
||||||
steps?: Step[];
|
currentStepId: string;
|
||||||
|
completedStepIds: string[];
|
||||||
}
|
}
|
||||||
|
|
||||||
const DEFAULT_STEPS: Step[] = [
|
export const StepIndicator = ({
|
||||||
{ id: 1, label: 'Personal', icon: User },
|
steps,
|
||||||
{ id: 2, label: 'Avatar', icon: Camera },
|
currentStepId,
|
||||||
];
|
completedStepIds
|
||||||
|
}: StepIndicatorProps) => {
|
||||||
export function StepIndicator({ currentStep, steps = DEFAULT_STEPS }: StepIndicatorProps) {
|
|
||||||
return (
|
return (
|
||||||
<div className="flex items-center justify-center gap-2 mb-8">
|
<Box display="flex" alignItems="center" gap={4}>
|
||||||
{steps.map((step, index) => {
|
{steps.map((step, index) => {
|
||||||
const Icon = step.icon;
|
const isCurrent = step.id === currentStepId;
|
||||||
const isCompleted = step.id < currentStep;
|
const isCompleted = completedStepIds.includes(step.id);
|
||||||
const isCurrent = step.id === currentStep;
|
const isLast = index === steps.length - 1;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div key={step.id} className="flex items-center">
|
<React.Fragment key={step.id}>
|
||||||
<div className="flex flex-col items-center">
|
<Box display="flex" alignItems="center" gap={2}>
|
||||||
<div
|
<Box
|
||||||
className={`flex h-12 w-12 items-center justify-center rounded-full transition-all duration-300 ${
|
width="2rem"
|
||||||
isCurrent
|
height="2rem"
|
||||||
? 'bg-primary-blue text-white shadow-lg shadow-primary-blue/30'
|
display="flex"
|
||||||
: isCompleted
|
alignItems="center"
|
||||||
? 'bg-performance-green text-white'
|
justifyContent="center"
|
||||||
: 'bg-iron-gray border border-charcoal-outline text-gray-500'
|
rounded="full"
|
||||||
}`}
|
bg={isCompleted ? 'var(--ui-color-intent-success)' : isCurrent ? 'var(--ui-color-intent-primary)' : 'var(--ui-color-bg-surface-muted)'}
|
||||||
|
style={{ border: isCurrent ? '2px solid var(--ui-color-intent-primary)' : 'none' }}
|
||||||
>
|
>
|
||||||
{isCompleted ? (
|
{isCompleted ? (
|
||||||
<Check className="w-5 h-5" />
|
<Icon icon={Check} size={4} intent="high" />
|
||||||
) : (
|
) : (
|
||||||
<Icon className="w-5 h-5" />
|
<Text size="xs" weight="bold" variant={isCurrent ? 'high' : 'low'}>
|
||||||
|
{index + 1}
|
||||||
|
</Text>
|
||||||
)}
|
)}
|
||||||
</div>
|
</Box>
|
||||||
<span
|
<Text size="sm" weight={isCurrent ? 'bold' : 'medium'} variant={isCurrent ? 'high' : 'low'}>
|
||||||
className={`mt-2 text-xs font-medium ${
|
|
||||||
isCurrent ? 'text-white' : isCompleted ? 'text-performance-green' : 'text-gray-500'
|
|
||||||
}`}
|
|
||||||
>
|
|
||||||
{step.label}
|
{step.label}
|
||||||
</span>
|
</Text>
|
||||||
</div>
|
</Box>
|
||||||
{index < steps.length - 1 && (
|
{!isLast && (
|
||||||
<div
|
<Box flex={1} height="2px" bg="var(--ui-color-border-muted)" minWidth="2rem" />
|
||||||
className={`w-16 h-0.5 mx-4 mt-[-20px] ${
|
|
||||||
isCompleted ? 'bg-performance-green' : 'bg-charcoal-outline'
|
|
||||||
}`}
|
|
||||||
/>
|
|
||||||
)}
|
)}
|
||||||
</div>
|
</React.Fragment>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
</div>
|
</Box>
|
||||||
);
|
);
|
||||||
}
|
};
|
||||||
|
|||||||
@@ -1,56 +1,46 @@
|
|||||||
import React from 'react';
|
import React, { ReactNode } from 'react';
|
||||||
import { Box } from './primitives/Box';
|
import { Box } from './primitives/Box';
|
||||||
import { Text } from './Text';
|
import { Text } from './Text';
|
||||||
import { Surface } from './primitives/Surface';
|
import { Surface } from './primitives/Surface';
|
||||||
import { Icon } from './Icon';
|
import { Icon } from './Icon';
|
||||||
import { LucideIcon } from 'lucide-react';
|
import { LucideIcon } from 'lucide-react';
|
||||||
|
|
||||||
interface SummaryItemProps {
|
export interface SummaryItemProps {
|
||||||
label?: string;
|
label: string;
|
||||||
value?: string | number;
|
value: string | number;
|
||||||
icon?: LucideIcon;
|
icon: LucideIcon;
|
||||||
|
intent?: 'primary' | 'success' | 'warning' | 'critical' | 'telemetry';
|
||||||
onClick?: () => void;
|
onClick?: () => void;
|
||||||
title?: string;
|
|
||||||
subtitle?: string;
|
|
||||||
rightContent?: React.ReactNode;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function SummaryItem({ label, value, icon, onClick, title, subtitle, rightContent }: SummaryItemProps) {
|
export const SummaryItem = ({
|
||||||
|
label,
|
||||||
|
value,
|
||||||
|
icon,
|
||||||
|
intent = 'primary',
|
||||||
|
onClick
|
||||||
|
}: SummaryItemProps) => {
|
||||||
return (
|
return (
|
||||||
<Surface
|
<Surface
|
||||||
variant="muted"
|
variant="muted"
|
||||||
rounded="lg"
|
rounded="lg"
|
||||||
p={4}
|
padding={4}
|
||||||
display="flex"
|
|
||||||
alignItems="center"
|
|
||||||
gap={4}
|
|
||||||
cursor={onClick ? 'pointer' : 'default'}
|
|
||||||
onClick={onClick}
|
onClick={onClick}
|
||||||
hoverBg={onClick ? 'bg-white/5' : undefined}
|
style={{ cursor: onClick ? 'pointer' : 'default' }}
|
||||||
transition={!!onClick}
|
|
||||||
>
|
>
|
||||||
{icon && (
|
<Box display="flex" alignItems="center" gap={4}>
|
||||||
<Box p={2} rounded="lg" bg="bg-white/5">
|
<Box padding={2} rounded="lg" bg="var(--ui-color-bg-surface-muted)">
|
||||||
<Icon icon={icon} size={5} color="text-gray-400" />
|
<Icon icon={icon} size={5} intent={intent} />
|
||||||
</Box>
|
</Box>
|
||||||
)}
|
|
||||||
<Box flex={1}>
|
|
||||||
{(label || title) && (
|
|
||||||
<Text size="xs" color="text-gray-500" uppercase letterSpacing="wider" block>
|
|
||||||
{label || title}
|
|
||||||
</Text>
|
|
||||||
)}
|
|
||||||
{(value || subtitle) && (
|
|
||||||
<Text size="lg" weight="bold" color="text-white" block>
|
|
||||||
{value || subtitle}
|
|
||||||
</Text>
|
|
||||||
)}
|
|
||||||
</Box>
|
|
||||||
{rightContent && (
|
|
||||||
<Box>
|
<Box>
|
||||||
{rightContent}
|
<Text size="xs" weight="bold" variant="low" uppercase>
|
||||||
|
{label}
|
||||||
|
</Text>
|
||||||
|
<Text size="lg" weight="bold" variant="high" block marginTop={0.5}>
|
||||||
|
{value}
|
||||||
|
</Text>
|
||||||
</Box>
|
</Box>
|
||||||
)}
|
</Box>
|
||||||
</Surface>
|
</Surface>
|
||||||
);
|
);
|
||||||
}
|
};
|
||||||
|
|||||||
@@ -1,68 +1,46 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { Box } from './primitives/Box';
|
import { Box } from './primitives/Box';
|
||||||
import { Stack } from './primitives/Stack';
|
|
||||||
import { Surface } from './primitives/Surface';
|
|
||||||
import { Text } from './Text';
|
import { Text } from './Text';
|
||||||
|
import { Surface } from './primitives/Surface';
|
||||||
|
|
||||||
interface Tab {
|
export interface TabNavigationOption {
|
||||||
id: string;
|
id: string;
|
||||||
label: string;
|
label: string;
|
||||||
icon?: React.ReactNode;
|
icon?: React.ReactNode;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface TabNavigationProps {
|
export interface TabNavigationProps {
|
||||||
tabs: Tab[];
|
options: TabNavigationOption[];
|
||||||
activeTab: string;
|
activeId: string;
|
||||||
onTabChange: (tabId: string) => void;
|
onChange: (id: string) => void;
|
||||||
className?: string;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function TabNavigation({ tabs, activeTab, onTabChange, className = '' }: TabNavigationProps) {
|
export const TabNavigation = ({
|
||||||
|
options,
|
||||||
|
activeId,
|
||||||
|
onChange
|
||||||
|
}: TabNavigationProps) => {
|
||||||
return (
|
return (
|
||||||
<Surface
|
<Surface variant="muted" rounded="xl" padding={1} display="inline-flex">
|
||||||
variant="muted"
|
{options.map((option) => {
|
||||||
rounded="xl"
|
const isActive = option.id === activeId;
|
||||||
p={1}
|
return (
|
||||||
display="inline-flex"
|
<button
|
||||||
zIndex={10}
|
key={option.id}
|
||||||
className={className}
|
onClick={() => onChange(option.id)}
|
||||||
>
|
className={`px-4 py-2 text-xs font-bold uppercase tracking-widest transition-all rounded-lg ${
|
||||||
<Stack direction="row" gap={1}>
|
isActive
|
||||||
{tabs.map((tab) => {
|
? 'bg-[var(--ui-color-bg-surface)] text-[var(--ui-color-intent-primary)] shadow-sm'
|
||||||
const isActive = activeTab === tab.id;
|
: 'text-[var(--ui-color-text-low)] hover:text-[var(--ui-color-text-high)]'
|
||||||
return (
|
}`}
|
||||||
<Surface
|
>
|
||||||
key={tab.id}
|
<Box display="flex" alignItems="center" gap={2}>
|
||||||
as="button"
|
{option.icon}
|
||||||
onClick={() => onTabChange(tab.id)}
|
{option.label}
|
||||||
variant={isActive ? 'default' : 'ghost'}
|
</Box>
|
||||||
bg={isActive ? 'bg-primary-blue' : ''}
|
</button>
|
||||||
rounded="lg"
|
);
|
||||||
px={4}
|
})}
|
||||||
py={2}
|
|
||||||
transition="all 0.2s"
|
|
||||||
group
|
|
||||||
className={`select-none ${isActive ? 'shadow-lg shadow-primary-blue/25' : 'hover:bg-iron-gray/80'}`}
|
|
||||||
>
|
|
||||||
<Stack direction="row" align="center" gap={2}>
|
|
||||||
{tab.icon && (
|
|
||||||
<Box color={isActive ? 'text-white' : 'text-gray-400'} groupHoverTextColor={!isActive ? 'white' : undefined}>
|
|
||||||
{tab.icon}
|
|
||||||
</Box>
|
|
||||||
)}
|
|
||||||
<Text
|
|
||||||
size="sm"
|
|
||||||
weight="medium"
|
|
||||||
color={isActive ? 'text-white' : 'text-gray-400'}
|
|
||||||
groupHoverTextColor={!isActive ? 'white' : undefined}
|
|
||||||
>
|
|
||||||
{tab.label}
|
|
||||||
</Text>
|
|
||||||
</Stack>
|
|
||||||
</Surface>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</Stack>
|
|
||||||
</Surface>
|
</Surface>
|
||||||
);
|
);
|
||||||
}
|
};
|
||||||
|
|||||||
@@ -1,95 +1,87 @@
|
|||||||
import React, { ReactNode, ElementType } from 'react';
|
import React, { ReactNode } from 'react';
|
||||||
import { Box, BoxProps } from './primitives/Box';
|
import { Box } from './primitives/Box';
|
||||||
|
import { Surface } from './primitives/Surface';
|
||||||
|
|
||||||
interface TableProps extends BoxProps<'table'> {
|
export interface TableProps {
|
||||||
children: ReactNode;
|
children: ReactNode;
|
||||||
|
className?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function Table({ children, className = '', ...props }: TableProps) {
|
export const Table = ({ children, className }: TableProps) => {
|
||||||
const { border, translate, ...rest } = props;
|
|
||||||
return (
|
return (
|
||||||
<Box overflow="auto" border borderColor="border-border-gray" rounded="sm">
|
<Surface rounded="lg" shadow="sm" style={{ overflow: 'auto', border: '1px solid var(--ui-color-border-default)' }} className={className}>
|
||||||
<table className={`w-full border-collapse text-left ${className}`} {...(rest as any)}>
|
<table className="w-full border-collapse text-left">
|
||||||
{children}
|
{children}
|
||||||
</table>
|
</table>
|
||||||
</Box>
|
</Surface>
|
||||||
);
|
);
|
||||||
}
|
};
|
||||||
|
|
||||||
interface TableHeaderProps extends BoxProps<'thead'> {
|
export const TableHeader = ({ children, className, textAlign, w }: { children: ReactNode, className?: string, textAlign?: 'left' | 'center' | 'right', w?: string }) => {
|
||||||
children: ReactNode;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function TableHeader({ children, className = '', ...props }: TableHeaderProps) {
|
|
||||||
return (
|
return (
|
||||||
<Box as="thead" className={`bg-graphite-black border-b border-border-gray ${className}`} {...props}>
|
<thead className={`bg-[var(--ui-color-bg-base)] border-b border-[var(--ui-color-border-default)] ${className || ''}`}>
|
||||||
{children}
|
<tr>
|
||||||
</Box>
|
{React.Children.map(children, child => {
|
||||||
|
if (React.isValidElement(child)) {
|
||||||
|
return React.cloneElement(child as any, { textAlign: textAlign || (child.props as any).textAlign, w: w || (child.props as any).w });
|
||||||
|
}
|
||||||
|
return child;
|
||||||
|
})}
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
);
|
);
|
||||||
}
|
};
|
||||||
|
|
||||||
export const TableHead = TableHeader;
|
export const TableHead = TableHeader;
|
||||||
|
|
||||||
interface TableBodyProps extends BoxProps<'tbody'> {
|
export const TableBody = ({ children }: { children: ReactNode }) => {
|
||||||
children: ReactNode;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function TableBody({ children, className = '', ...props }: TableBodyProps) {
|
|
||||||
return (
|
return (
|
||||||
<Box as="tbody" className={`divide-y divide-border-gray/50 ${className}`} {...props}>
|
<tbody className="divide-y divide-[var(--ui-color-border-muted)]">
|
||||||
{children}
|
{children}
|
||||||
</Box>
|
</tbody>
|
||||||
);
|
);
|
||||||
}
|
};
|
||||||
|
|
||||||
interface TableRowProps extends BoxProps<'tr'> {
|
|
||||||
children: ReactNode;
|
|
||||||
hoverBg?: string;
|
|
||||||
clickable?: boolean;
|
|
||||||
variant?: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function TableRow({ children, className = '', hoverBg, clickable, variant, ...props }: TableRowProps) {
|
|
||||||
const classes = [
|
|
||||||
'transition-colors',
|
|
||||||
clickable || props.onClick ? 'cursor-pointer' : '',
|
|
||||||
hoverBg ? `hover:${hoverBg}` : (clickable || props.onClick ? 'hover:bg-white/5' : ''),
|
|
||||||
className
|
|
||||||
].filter(Boolean).join(' ');
|
|
||||||
|
|
||||||
|
export const TableRow = ({ children, onClick, className, variant, clickable, bg, ...props }: { children: ReactNode, onClick?: () => void, className?: string, variant?: string, clickable?: boolean, bg?: string, [key: string]: any }) => {
|
||||||
|
const isClickable = clickable || !!onClick;
|
||||||
return (
|
return (
|
||||||
<Box as="tr" className={classes} {...props}>
|
<tr
|
||||||
|
className={`${isClickable ? 'cursor-pointer hover:bg-white/5 transition-colors' : ''} ${variant === 'highlight' ? 'bg-white/5' : ''} ${className || ''}`}
|
||||||
|
onClick={onClick}
|
||||||
|
style={bg ? { backgroundColor: bg.startsWith('bg-') ? undefined : bg } : undefined}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
{children}
|
{children}
|
||||||
</Box>
|
</tr>
|
||||||
);
|
);
|
||||||
}
|
};
|
||||||
|
|
||||||
interface TableCellProps extends BoxProps<'td'> {
|
|
||||||
children: ReactNode;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function TableHeaderCell({ children, className = '', ...props }: TableCellProps) {
|
|
||||||
const classes = [
|
|
||||||
'px-4 py-3 text-xs font-bold text-gray-400 uppercase tracking-wider',
|
|
||||||
className
|
|
||||||
].filter(Boolean).join(' ');
|
|
||||||
|
|
||||||
|
export const TableHeaderCell = ({ children, textAlign, w, className }: { children: ReactNode, textAlign?: 'left' | 'center' | 'right', w?: string, className?: string }) => {
|
||||||
|
const alignClass = textAlign === 'center' ? 'text-center' : (textAlign === 'right' ? 'text-right' : 'text-left');
|
||||||
return (
|
return (
|
||||||
<Box as="th" className={classes} {...props}>
|
<th
|
||||||
|
className={`px-4 py-3 text-xs font-bold uppercase tracking-wider text-[var(--ui-color-text-low)] ${alignClass} ${className || ''}`}
|
||||||
|
style={w ? { width: w } : undefined}
|
||||||
|
>
|
||||||
{children}
|
{children}
|
||||||
</Box>
|
</th>
|
||||||
);
|
);
|
||||||
}
|
};
|
||||||
|
|
||||||
export function TableCell({ children, className = '', ...props }: TableCellProps) {
|
|
||||||
const classes = [
|
|
||||||
'px-4 py-4 text-sm text-gray-300',
|
|
||||||
className
|
|
||||||
].filter(Boolean).join(' ');
|
|
||||||
|
|
||||||
|
export const TableCell = ({ children, textAlign, className, py, colSpan, w, position, ...props }: { children: ReactNode, textAlign?: 'left' | 'center' | 'right', className?: string, py?: number, colSpan?: number, w?: string, position?: string, [key: string]: any }) => {
|
||||||
|
const alignClass = textAlign === 'center' ? 'text-center' : (textAlign === 'right' ? 'text-right' : 'text-left');
|
||||||
return (
|
return (
|
||||||
<Box as="td" className={classes} {...props}>
|
<td
|
||||||
|
className={`px-4 py-3 text-sm text-[var(--ui-color-text-high)] ${alignClass} ${className || ''}`}
|
||||||
|
colSpan={colSpan}
|
||||||
|
style={{
|
||||||
|
...(py !== undefined ? { paddingTop: `${py * 0.25}rem`, paddingBottom: `${py * 0.25}rem` } : {}),
|
||||||
|
...(w ? { width: w } : {}),
|
||||||
|
...(position ? { position: position as any } : {}),
|
||||||
|
}}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
{children}
|
{children}
|
||||||
</Box>
|
</td>
|
||||||
);
|
);
|
||||||
}
|
};
|
||||||
|
|||||||
@@ -1,206 +1,121 @@
|
|||||||
import React, { ReactNode, ElementType, ComponentPropsWithoutRef } from 'react';
|
import React, { ReactNode, forwardRef, ElementType } from 'react';
|
||||||
import { Box, BoxProps } from './primitives/Box';
|
import { Box, BoxProps, ResponsiveValue } from './primitives/Box';
|
||||||
|
|
||||||
type Spacing = 0 | 0.5 | 1 | 1.5 | 2 | 2.5 | 3 | 3.5 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 14 | 16 | 20 | 24 | 28 | 32 | 36 | 40 | 44 | 48 | 52 | 56 | 60 | 64 | 72 | 80 | 96;
|
export type TextSize = 'xs' | 'sm' | 'md' | 'lg' | 'xl' | '2xl' | '3xl' | '4xl' | 'base';
|
||||||
|
|
||||||
type TextSize = 'xs' | 'sm' | 'base' | 'lg' | 'xl' | '2xl' | '3xl' | '4xl';
|
export interface TextProps extends BoxProps<any> {
|
||||||
|
|
||||||
interface ResponsiveTextSize {
|
|
||||||
base?: TextSize;
|
|
||||||
sm?: TextSize;
|
|
||||||
md?: TextSize;
|
|
||||||
lg?: TextSize;
|
|
||||||
xl?: TextSize;
|
|
||||||
'2xl'?: TextSize;
|
|
||||||
}
|
|
||||||
|
|
||||||
type TextAlign = 'left' | 'center' | 'right';
|
|
||||||
|
|
||||||
interface ResponsiveTextAlign {
|
|
||||||
base?: TextAlign;
|
|
||||||
sm?: TextAlign;
|
|
||||||
md?: TextAlign;
|
|
||||||
lg?: TextAlign;
|
|
||||||
xl?: TextAlign;
|
|
||||||
'2xl'?: TextAlign;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface TextProps<T extends ElementType = 'span'> extends Omit<BoxProps<T>, 'children' | 'className' | 'size'> {
|
|
||||||
as?: T;
|
|
||||||
children: ReactNode;
|
children: ReactNode;
|
||||||
className?: string;
|
variant?: 'high' | 'med' | 'low' | 'primary' | 'success' | 'warning' | 'critical' | 'inherit';
|
||||||
size?: TextSize | ResponsiveTextSize;
|
size?: TextSize | ResponsiveValue<TextSize>;
|
||||||
weight?: 'light' | 'normal' | 'medium' | 'semibold' | 'bold' | string;
|
weight?: 'light' | 'normal' | 'medium' | 'semibold' | 'bold';
|
||||||
color?: string;
|
as?: ElementType;
|
||||||
font?: 'mono' | 'sans' | string;
|
align?: 'left' | 'center' | 'right';
|
||||||
align?: TextAlign | ResponsiveTextAlign;
|
|
||||||
truncate?: boolean;
|
|
||||||
uppercase?: boolean;
|
|
||||||
capitalize?: boolean;
|
|
||||||
letterSpacing?: 'tighter' | 'tight' | 'normal' | 'wide' | 'wider' | 'widest' | '0.05em' | string;
|
|
||||||
leading?: 'none' | 'tight' | 'snug' | 'normal' | 'relaxed' | 'loose';
|
|
||||||
fontSize?: string;
|
|
||||||
style?: React.CSSProperties;
|
|
||||||
block?: boolean;
|
|
||||||
italic?: boolean;
|
italic?: boolean;
|
||||||
lineClamp?: number;
|
mono?: boolean;
|
||||||
ml?: Spacing | ResponsiveSpacing;
|
block?: boolean;
|
||||||
mr?: Spacing | ResponsiveSpacing;
|
uppercase?: boolean;
|
||||||
mt?: Spacing | ResponsiveSpacing;
|
letterSpacing?: string;
|
||||||
mb?: Spacing | ResponsiveSpacing;
|
leading?: 'none' | 'tight' | 'snug' | 'normal' | 'relaxed' | 'loose';
|
||||||
|
truncate?: boolean;
|
||||||
|
lineHeight?: string | number;
|
||||||
|
font?: 'sans' | 'mono';
|
||||||
|
hoverTextColor?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface ResponsiveSpacing {
|
export const Text = forwardRef<HTMLElement, TextProps>(({
|
||||||
base?: Spacing;
|
|
||||||
sm?: Spacing;
|
|
||||||
md?: Spacing;
|
|
||||||
lg?: Spacing;
|
|
||||||
xl?: Spacing;
|
|
||||||
'2xl'?: Spacing;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function Text<T extends ElementType = 'span'>({
|
|
||||||
as,
|
|
||||||
children,
|
children,
|
||||||
className = '',
|
variant = 'med',
|
||||||
size = 'base',
|
size = 'md',
|
||||||
weight = 'normal',
|
weight = 'normal',
|
||||||
color = '',
|
as = 'p',
|
||||||
font = 'sans',
|
|
||||||
align = 'left',
|
align = 'left',
|
||||||
truncate = false,
|
italic = false,
|
||||||
|
mono = false,
|
||||||
|
block = false,
|
||||||
uppercase = false,
|
uppercase = false,
|
||||||
capitalize = false,
|
|
||||||
letterSpacing,
|
letterSpacing,
|
||||||
leading,
|
leading,
|
||||||
fontSize,
|
truncate = false,
|
||||||
style,
|
lineHeight,
|
||||||
block = false,
|
font,
|
||||||
italic = false,
|
hoverTextColor,
|
||||||
lineClamp,
|
|
||||||
ml, mr, mt, mb,
|
|
||||||
...props
|
...props
|
||||||
}: TextProps<T> & ComponentPropsWithoutRef<T>) {
|
}, ref) => {
|
||||||
const Tag = (as as ElementType) || 'span';
|
const variantClasses = {
|
||||||
|
high: 'text-[var(--ui-color-text-high)]',
|
||||||
const sizeClasses: Record<string, string> = {
|
med: 'text-[var(--ui-color-text-med)]',
|
||||||
|
low: 'text-[var(--ui-color-text-low)]',
|
||||||
|
primary: 'text-[var(--ui-color-intent-primary)]',
|
||||||
|
success: 'text-[var(--ui-color-intent-success)]',
|
||||||
|
warning: 'text-[var(--ui-color-intent-warning)]',
|
||||||
|
critical: 'text-[var(--ui-color-intent-critical)]',
|
||||||
|
inherit: 'text-inherit',
|
||||||
|
};
|
||||||
|
|
||||||
|
const sizeMap: Record<TextSize, string> = {
|
||||||
xs: 'text-xs',
|
xs: 'text-xs',
|
||||||
sm: 'text-sm',
|
sm: 'text-sm',
|
||||||
base: 'text-base',
|
base: 'text-base',
|
||||||
|
md: 'text-base',
|
||||||
lg: 'text-lg',
|
lg: 'text-lg',
|
||||||
xl: 'text-xl',
|
xl: 'text-xl',
|
||||||
'2xl': 'text-2xl',
|
'2xl': 'text-2xl',
|
||||||
'3xl': 'text-3xl',
|
'3xl': 'text-3xl',
|
||||||
'4xl': 'text-4xl'
|
'4xl': 'text-4xl',
|
||||||
};
|
};
|
||||||
|
|
||||||
const getSizeClasses = (value: TextSize | ResponsiveTextSize | undefined) => {
|
const getResponsiveSizeClasses = (value: TextSize | ResponsiveValue<TextSize>) => {
|
||||||
if (value === undefined) return '';
|
if (typeof value === 'string') return sizeMap[value];
|
||||||
if (typeof value === 'object') {
|
const classes = [];
|
||||||
const classes = [];
|
if (value.base) classes.push(sizeMap[value.base]);
|
||||||
if (value.base) classes.push(sizeClasses[value.base]);
|
if (value.sm) classes.push(`sm:${sizeMap[value.sm]}`);
|
||||||
if (value.sm) classes.push(`sm:${sizeClasses[value.sm]}`);
|
if (value.md) classes.push(`md:${sizeMap[value.md]}`);
|
||||||
if (value.md) classes.push(`md:${sizeClasses[value.md]}`);
|
if (value.lg) classes.push(`lg:${sizeMap[value.lg]}`);
|
||||||
if (value.lg) classes.push(`lg:${sizeClasses[value.lg]}`);
|
if (value.xl) classes.push(`xl:${sizeMap[value.xl]}`);
|
||||||
if (value.xl) classes.push(`xl:${sizeClasses[value.xl]}`);
|
return classes.join(' ');
|
||||||
if (value['2xl']) classes.push(`2xl:${sizeClasses[value['2xl']]}`);
|
|
||||||
return classes.join(' ');
|
|
||||||
}
|
|
||||||
return sizeClasses[value];
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const weightClasses: Record<string, string> = {
|
const weightClasses = {
|
||||||
light: 'font-light',
|
light: 'font-light',
|
||||||
normal: 'font-normal',
|
normal: 'font-normal',
|
||||||
medium: 'font-medium',
|
medium: 'font-medium',
|
||||||
semibold: 'font-semibold',
|
semibold: 'font-semibold',
|
||||||
bold: 'font-bold'
|
bold: 'font-bold',
|
||||||
};
|
|
||||||
|
|
||||||
const fontClasses: Record<string, string> = {
|
|
||||||
mono: 'font-mono',
|
|
||||||
sans: 'font-sans'
|
|
||||||
};
|
|
||||||
|
|
||||||
const alignClasses: Record<string, string> = {
|
|
||||||
left: 'text-left',
|
|
||||||
center: 'text-center',
|
|
||||||
right: 'text-right'
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const getAlignClasses = (value: TextAlign | ResponsiveTextAlign | undefined) => {
|
const leadingClasses = {
|
||||||
if (value === undefined) return '';
|
|
||||||
if (typeof value === 'object') {
|
|
||||||
const classes = [];
|
|
||||||
if (value.base) classes.push(alignClasses[value.base]);
|
|
||||||
if (value.sm) classes.push(`sm:${alignClasses[value.sm]}`);
|
|
||||||
if (value.md) classes.push(`md:${alignClasses[value.md]}`);
|
|
||||||
if (value.lg) classes.push(`lg:${alignClasses[value.lg]}`);
|
|
||||||
if (value.xl) classes.push(`xl:${alignClasses[value.xl]}`);
|
|
||||||
if (value['2xl']) classes.push(`2xl:${alignClasses[value['2xl']]}`);
|
|
||||||
return classes.join(' ');
|
|
||||||
}
|
|
||||||
return alignClasses[value];
|
|
||||||
};
|
|
||||||
|
|
||||||
const spacingMap: Record<number, string> = {
|
|
||||||
0: '0', 0.5: '0.5', 1: '1', 1.5: '1.5', 2: '2', 2.5: '2.5', 3: '3', 3.5: '3.5', 4: '4',
|
|
||||||
5: '5', 6: '6', 7: '7', 8: '8', 9: '9', 10: '10', 11: '11', 12: '12', 14: '14',
|
|
||||||
16: '16', 20: '20', 24: '24', 28: '28', 32: '32', 36: '36', 40: '40', 44: '44',
|
|
||||||
48: '48', 52: '52', 56: '56', 60: '60', 64: '64', 72: '72', 80: '80', 96: '96'
|
|
||||||
};
|
|
||||||
|
|
||||||
const leadingClasses: Record<string, string> = {
|
|
||||||
none: 'leading-none',
|
none: 'leading-none',
|
||||||
tight: 'leading-tight',
|
tight: 'leading-tight',
|
||||||
snug: 'leading-snug',
|
snug: 'leading-snug',
|
||||||
normal: 'leading-normal',
|
normal: 'leading-normal',
|
||||||
relaxed: 'leading-relaxed',
|
relaxed: 'leading-relaxed',
|
||||||
loose: 'leading-loose'
|
loose: 'leading-loose',
|
||||||
};
|
};
|
||||||
|
|
||||||
const getSpacingClass = (prefix: string, value: Spacing | ResponsiveSpacing | undefined) => {
|
|
||||||
if (value === undefined) return '';
|
|
||||||
if (typeof value === 'object') {
|
|
||||||
const classes = [];
|
|
||||||
if (value.base !== undefined) classes.push(`${prefix}-${spacingMap[value.base as number]}`);
|
|
||||||
if (value.sm !== undefined) classes.push(`sm:${prefix}-${spacingMap[value.sm as number]}`);
|
|
||||||
if (value.md !== undefined) classes.push(`md:${prefix}-${spacingMap[value.md as number]}`);
|
|
||||||
if (value.lg !== undefined) classes.push(`lg:${prefix}-${spacingMap[value.lg as number]}`);
|
|
||||||
if (value.xl !== undefined) classes.push(`xl:${prefix}-${spacingMap[value.xl as number]}`);
|
|
||||||
if (value['2xl'] !== undefined) classes.push(`2xl:${prefix}-${spacingMap[value['2xl'] as number]}`);
|
|
||||||
return classes.join(' ');
|
|
||||||
}
|
|
||||||
return `${prefix}-${spacingMap[value as number]}`;
|
|
||||||
};
|
|
||||||
|
|
||||||
const classes = [
|
const classes = [
|
||||||
block ? 'block' : 'inline',
|
variantClasses[variant],
|
||||||
getSizeClasses(size),
|
getResponsiveSizeClasses(size),
|
||||||
weightClasses[weight] || '',
|
weightClasses[weight],
|
||||||
fontClasses[font] || '',
|
align === 'center' ? 'text-center' : (align === 'right' ? 'text-right' : 'text-left'),
|
||||||
getAlignClasses(align),
|
|
||||||
leading ? leadingClasses[leading] : '',
|
|
||||||
color,
|
|
||||||
truncate ? 'truncate' : '',
|
|
||||||
uppercase ? 'uppercase' : '',
|
|
||||||
capitalize ? 'capitalize' : '',
|
|
||||||
italic ? 'italic' : '',
|
italic ? 'italic' : '',
|
||||||
lineClamp ? `line-clamp-${lineClamp}` : '',
|
(mono || font === 'mono') ? 'font-mono' : 'font-sans',
|
||||||
letterSpacing === '0.05em' ? 'tracking-wider' : letterSpacing ? `tracking-${letterSpacing}` : '',
|
block ? 'block' : 'inline',
|
||||||
getSpacingClass('ml', ml),
|
uppercase ? 'uppercase tracking-wider' : '',
|
||||||
getSpacingClass('mr', mr),
|
leading ? leadingClasses[leading] : '',
|
||||||
getSpacingClass('mt', mt),
|
truncate ? 'truncate' : '',
|
||||||
getSpacingClass('mb', mb),
|
hoverTextColor ? `hover:text-${hoverTextColor}` : '',
|
||||||
className
|
].join(' ');
|
||||||
].filter(Boolean).join(' ');
|
|
||||||
|
const style: React.CSSProperties = {
|
||||||
const combinedStyle = {
|
...(letterSpacing ? { letterSpacing } : {}),
|
||||||
...(fontSize ? { fontSize } : {}),
|
...(lineHeight ? { lineHeight } : {}),
|
||||||
...(weight && !weightClasses[weight] ? { fontWeight: weight } : {}),
|
|
||||||
...(font && !fontClasses[font] ? { fontFamily: font } : {}),
|
|
||||||
...style
|
|
||||||
};
|
};
|
||||||
|
|
||||||
return <Box as={Tag} className={classes} style={combinedStyle} {...props}>{children}</Box>;
|
return (
|
||||||
}
|
<Box as={as} ref={ref} className={classes} style={style} {...props}>
|
||||||
|
{children}
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
Text.displayName = 'Text';
|
||||||
|
|||||||
@@ -1,49 +1,61 @@
|
|||||||
import React, { forwardRef } from 'react';
|
import React, { forwardRef, TextareaHTMLAttributes } from 'react';
|
||||||
import { Box } from './primitives/Box';
|
import { Box } from './primitives/Box';
|
||||||
import { Stack } from './primitives/Stack';
|
|
||||||
import { Text } from './Text';
|
import { Text } from './Text';
|
||||||
|
|
||||||
interface TextAreaProps extends React.TextareaHTMLAttributes<HTMLTextAreaElement> {
|
export interface TextAreaProps extends TextareaHTMLAttributes<HTMLTextAreaElement> {
|
||||||
label?: string;
|
label?: string;
|
||||||
errorMessage?: string;
|
error?: string;
|
||||||
variant?: 'default' | 'error';
|
hint?: string;
|
||||||
fullWidth?: boolean;
|
fullWidth?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const TextArea = forwardRef<HTMLTextAreaElement, TextAreaProps>(
|
export const TextArea = forwardRef<HTMLTextAreaElement, TextAreaProps>(({
|
||||||
({ label, errorMessage, variant = 'default', fullWidth = true, className = '', ...props }, ref) => {
|
label,
|
||||||
const isError = variant === 'error' || !!errorMessage;
|
error,
|
||||||
|
hint,
|
||||||
return (
|
fullWidth = false,
|
||||||
<Stack gap={1.5} fullWidth={fullWidth}>
|
...props
|
||||||
{label && (
|
}, ref) => {
|
||||||
<Text as="label" size="xs" weight="bold" color="text-gray-500" uppercase letterSpacing="wider">
|
const baseClasses = 'bg-[var(--ui-color-bg-surface)] border border-[var(--ui-color-border-default)] text-[var(--ui-color-text-high)] placeholder-[var(--ui-color-text-low)] focus:outline-none focus:border-[var(--ui-color-intent-primary)] transition-colors p-3 text-sm min-h-[100px]';
|
||||||
|
const errorClasses = error ? 'border-[var(--ui-color-intent-critical)]' : '';
|
||||||
|
const widthClasses = fullWidth ? 'w-full' : '';
|
||||||
|
|
||||||
|
const classes = [
|
||||||
|
baseClasses,
|
||||||
|
errorClasses,
|
||||||
|
widthClasses,
|
||||||
|
].filter(Boolean).join(' ');
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Box width={fullWidth ? '100%' : undefined}>
|
||||||
|
{label && (
|
||||||
|
<Box marginBottom={1.5}>
|
||||||
|
<Text as="label" size="xs" weight="bold" variant="low">
|
||||||
{label}
|
{label}
|
||||||
</Text>
|
</Text>
|
||||||
)}
|
|
||||||
<Box position="relative" fullWidth={fullWidth}>
|
|
||||||
<Box
|
|
||||||
as="textarea"
|
|
||||||
ref={ref}
|
|
||||||
fullWidth={fullWidth}
|
|
||||||
p={3}
|
|
||||||
bg="bg-deep-graphite"
|
|
||||||
rounded="lg"
|
|
||||||
color="text-white"
|
|
||||||
border
|
|
||||||
borderColor={isError ? 'var(--warning-amber)' : 'rgba(38, 38, 38, 0.8)'}
|
|
||||||
className={`placeholder:text-gray-500 focus:ring-2 focus:ring-primary-blue transition-all duration-150 sm:text-sm ${className}`}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
{errorMessage && (
|
|
||||||
<Text size="xs" color="text-warning-amber" mt={1}>
|
|
||||||
{errorMessage}
|
|
||||||
</Text>
|
|
||||||
)}
|
|
||||||
</Box>
|
</Box>
|
||||||
</Stack>
|
)}
|
||||||
);
|
<textarea
|
||||||
}
|
ref={ref}
|
||||||
);
|
className={classes}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
{error && (
|
||||||
|
<Box marginTop={1}>
|
||||||
|
<Text size="xs" variant="critical">
|
||||||
|
{error}
|
||||||
|
</Text>
|
||||||
|
</Box>
|
||||||
|
)}
|
||||||
|
{hint && !error && (
|
||||||
|
<Box marginTop={1}>
|
||||||
|
<Text size="xs" variant="low">
|
||||||
|
{hint}
|
||||||
|
</Text>
|
||||||
|
</Box>
|
||||||
|
)}
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
TextArea.displayName = 'TextArea';
|
TextArea.displayName = 'TextArea';
|
||||||
|
|||||||
@@ -1,9 +1,8 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { Box } from './primitives/Box';
|
import { Box } from './primitives/Box';
|
||||||
import { Text } from './Text';
|
import { Text } from './Text';
|
||||||
import { motion } from 'framer-motion';
|
|
||||||
|
|
||||||
interface ToggleProps {
|
export interface ToggleProps {
|
||||||
label: string;
|
label: string;
|
||||||
description?: string;
|
description?: string;
|
||||||
checked: boolean;
|
checked: boolean;
|
||||||
@@ -11,64 +10,49 @@ interface ToggleProps {
|
|||||||
disabled?: boolean;
|
disabled?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function Toggle({ label, description, checked, onChange, disabled }: ToggleProps) {
|
export const Toggle = ({
|
||||||
|
label,
|
||||||
|
description,
|
||||||
|
checked,
|
||||||
|
onChange,
|
||||||
|
disabled = false
|
||||||
|
}: ToggleProps) => {
|
||||||
return (
|
return (
|
||||||
<Box
|
<Box
|
||||||
as="label"
|
as="label"
|
||||||
display="flex"
|
display="flex"
|
||||||
alignItems="start"
|
alignItems="center"
|
||||||
justifyContent="between"
|
justifyContent="between"
|
||||||
cursor={disabled ? 'not-allowed' : 'pointer'}
|
paddingY={3}
|
||||||
py={3}
|
|
||||||
borderBottom
|
borderBottom
|
||||||
borderColor="border-charcoal-outline/50"
|
style={{ cursor: disabled ? 'not-allowed' : 'pointer', opacity: disabled ? 0.5 : 1 }}
|
||||||
className="last:border-b-0"
|
|
||||||
opacity={disabled ? 0.5 : 1}
|
|
||||||
>
|
>
|
||||||
<Box flex={1} pr={4}>
|
<Box flex={1} paddingRight={4}>
|
||||||
<Text weight="medium" color="text-gray-200" block>{label}</Text>
|
<Text weight="medium" variant="high" block>{label}</Text>
|
||||||
{description && (
|
{description && (
|
||||||
<Text size="xs" color="text-gray-500" block mt={1}>
|
<Text size="xs" variant="low" block marginTop={1}>
|
||||||
{description}
|
{description}
|
||||||
</Text>
|
</Text>
|
||||||
)}
|
)}
|
||||||
</Box>
|
</Box>
|
||||||
|
|
||||||
<Box position="relative">
|
<button
|
||||||
<Box
|
type="button"
|
||||||
as="button"
|
role="switch"
|
||||||
type="button"
|
aria-checked={checked}
|
||||||
role="switch"
|
onClick={() => !disabled && onChange(!checked)}
|
||||||
aria-checked={checked}
|
disabled={disabled}
|
||||||
onClick={() => !disabled && onChange(!checked)}
|
className={`relative inline-flex h-6 w-11 flex-shrink-0 cursor-pointer rounded-full border-2 border-transparent transition-colors duration-200 ease-in-out focus:outline-none ${
|
||||||
disabled={disabled}
|
checked ? 'bg-[var(--ui-color-intent-primary)]' : 'bg-[var(--ui-color-bg-surface-muted)]'
|
||||||
w="12"
|
}`}
|
||||||
h="6"
|
>
|
||||||
rounded="full"
|
<span
|
||||||
transition="all 0.2s"
|
aria-hidden="true"
|
||||||
flexShrink={0}
|
className={`pointer-events-none inline-block h-5 w-5 transform rounded-full bg-white shadow ring-0 transition duration-200 ease-in-out ${
|
||||||
ring="primary-blue/50"
|
checked ? 'translate-x-5' : 'translate-x-0'
|
||||||
bg={checked ? 'bg-primary-blue/20' : 'bg-charcoal-outline'}
|
}`}
|
||||||
className="focus:outline-none focus:ring-2"
|
|
||||||
>
|
|
||||||
<motion.div
|
|
||||||
className="absolute inset-0 rounded-full bg-primary-blue"
|
|
||||||
initial={{ boxShadow: '0 0 0px rgba(25, 140, 255, 0)' }}
|
|
||||||
animate={{
|
|
||||||
opacity: checked ? 1 : 0,
|
|
||||||
boxShadow: checked ? '0 0 10px rgba(25, 140, 255, 0.4)' : '0 0 0px rgba(25, 140, 255, 0)'
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</Box>
|
|
||||||
<motion.span
|
|
||||||
className="absolute top-0.5 w-5 h-5 bg-white rounded-full shadow-md"
|
|
||||||
initial={false}
|
|
||||||
animate={{
|
|
||||||
left: checked ? '26px' : '2px',
|
|
||||||
}}
|
|
||||||
transition={{ type: "spring", stiffness: 500, damping: 30 }}
|
|
||||||
/>
|
/>
|
||||||
</Box>
|
</button>
|
||||||
</Box>
|
</Box>
|
||||||
);
|
);
|
||||||
}
|
};
|
||||||
|
|||||||
@@ -1,11 +1,11 @@
|
|||||||
import React, { forwardRef, ForwardedRef, ElementType, ComponentPropsWithoutRef } from 'react';
|
import React, { forwardRef, ForwardedRef, ElementType } from 'react';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* WARNING: DO NOT VIOLATE THE PURPOSE OF THIS PRIMITIVE.
|
* WARNING: DO NOT VIOLATE THE PURPOSE OF THIS PRIMITIVE.
|
||||||
*
|
*
|
||||||
* Box is a basic container primitive for spacing, sizing and basic styling.
|
* Box is a basic container primitive for spacing, sizing and basic layout.
|
||||||
*
|
*
|
||||||
* - DO NOT add layout props (flex, grid, gap) - use Stack or Grid instead.
|
* - DO NOT add decoration props (bg, border, shadow) - use Surface instead.
|
||||||
* - DO NOT add positioning props (absolute, top, zIndex) - create a specific component.
|
* - DO NOT add positioning props (absolute, top, zIndex) - create a specific component.
|
||||||
* - DO NOT add animation props - create a specific component.
|
* - DO NOT add animation props - create a specific component.
|
||||||
*
|
*
|
||||||
@@ -34,8 +34,23 @@ export type ResponsiveValue<T> = {
|
|||||||
export interface BoxProps<T extends ElementType> {
|
export interface BoxProps<T extends ElementType> {
|
||||||
as?: T;
|
as?: T;
|
||||||
children?: React.ReactNode;
|
children?: React.ReactNode;
|
||||||
className?: string;
|
|
||||||
// Spacing
|
// Spacing
|
||||||
|
margin?: Spacing | ResponsiveSpacing;
|
||||||
|
marginTop?: Spacing | ResponsiveSpacing;
|
||||||
|
marginBottom?: Spacing | ResponsiveSpacing;
|
||||||
|
marginLeft?: Spacing | 'auto' | ResponsiveSpacing;
|
||||||
|
marginRight?: Spacing | 'auto' | ResponsiveSpacing;
|
||||||
|
marginX?: Spacing | 'auto' | ResponsiveSpacing;
|
||||||
|
marginY?: Spacing | ResponsiveSpacing;
|
||||||
|
padding?: Spacing | ResponsiveSpacing;
|
||||||
|
paddingTop?: Spacing | ResponsiveSpacing;
|
||||||
|
paddingBottom?: Spacing | ResponsiveSpacing;
|
||||||
|
paddingLeft?: Spacing | ResponsiveSpacing;
|
||||||
|
paddingRight?: Spacing | ResponsiveSpacing;
|
||||||
|
paddingX?: Spacing | ResponsiveSpacing;
|
||||||
|
paddingY?: Spacing | ResponsiveSpacing;
|
||||||
|
|
||||||
|
// Aliases (Deprecated - use full names)
|
||||||
m?: Spacing | ResponsiveSpacing;
|
m?: Spacing | ResponsiveSpacing;
|
||||||
mt?: Spacing | ResponsiveSpacing;
|
mt?: Spacing | ResponsiveSpacing;
|
||||||
mb?: Spacing | ResponsiveSpacing;
|
mb?: Spacing | ResponsiveSpacing;
|
||||||
@@ -50,11 +65,10 @@ export interface BoxProps<T extends ElementType> {
|
|||||||
pr?: Spacing | ResponsiveSpacing;
|
pr?: Spacing | ResponsiveSpacing;
|
||||||
px?: Spacing | ResponsiveSpacing;
|
px?: Spacing | ResponsiveSpacing;
|
||||||
py?: Spacing | ResponsiveSpacing;
|
py?: Spacing | ResponsiveSpacing;
|
||||||
|
|
||||||
// Sizing
|
// Sizing
|
||||||
w?: string | number | ResponsiveValue<string | number>;
|
width?: string | number | ResponsiveValue<string | number>;
|
||||||
h?: string | number | ResponsiveValue<string | number>;
|
height?: string | number | ResponsiveValue<string | number>;
|
||||||
width?: string | number;
|
|
||||||
height?: string | number;
|
|
||||||
maxWidth?: string | ResponsiveValue<string>;
|
maxWidth?: string | ResponsiveValue<string>;
|
||||||
minWidth?: string | ResponsiveValue<string>;
|
minWidth?: string | ResponsiveValue<string>;
|
||||||
maxHeight?: string | ResponsiveValue<string>;
|
maxHeight?: string | ResponsiveValue<string>;
|
||||||
@@ -62,6 +76,11 @@ export interface BoxProps<T extends ElementType> {
|
|||||||
fullWidth?: boolean;
|
fullWidth?: boolean;
|
||||||
fullHeight?: boolean;
|
fullHeight?: boolean;
|
||||||
aspectRatio?: string;
|
aspectRatio?: string;
|
||||||
|
|
||||||
|
// Aliases
|
||||||
|
w?: string | number | ResponsiveValue<string | number>;
|
||||||
|
h?: string | number | ResponsiveValue<string | number>;
|
||||||
|
|
||||||
// Display
|
// Display
|
||||||
display?: 'block' | 'inline-block' | 'flex' | 'inline-flex' | 'grid' | 'none' | string | ResponsiveValue<'block' | 'inline-block' | 'flex' | 'inline-flex' | 'grid' | 'none' | string>;
|
display?: 'block' | 'inline-block' | 'flex' | 'inline-flex' | 'grid' | 'none' | string | ResponsiveValue<'block' | 'inline-block' | 'flex' | 'inline-flex' | 'grid' | 'none' | string>;
|
||||||
center?: boolean;
|
center?: boolean;
|
||||||
@@ -80,28 +99,6 @@ export interface BoxProps<T extends ElementType> {
|
|||||||
insetY?: string | number;
|
insetY?: string | number;
|
||||||
insetX?: string | number;
|
insetX?: string | number;
|
||||||
zIndex?: number;
|
zIndex?: number;
|
||||||
// Basic Styling
|
|
||||||
rounded?: 'none' | 'sm' | 'md' | 'lg' | 'xl' | '2xl' | 'full' | string | boolean;
|
|
||||||
border?: boolean | string;
|
|
||||||
borderTop?: boolean | string;
|
|
||||||
borderBottom?: boolean | string;
|
|
||||||
borderLeft?: boolean | string;
|
|
||||||
borderRight?: boolean | string;
|
|
||||||
borderWidth?: string | number;
|
|
||||||
borderStyle?: 'solid' | 'dashed' | 'dotted' | 'none' | string;
|
|
||||||
borderColor?: string;
|
|
||||||
borderOpacity?: number;
|
|
||||||
bg?: string;
|
|
||||||
backgroundColor?: string;
|
|
||||||
backgroundImage?: string;
|
|
||||||
backgroundSize?: string;
|
|
||||||
backgroundPosition?: string;
|
|
||||||
bgOpacity?: number;
|
|
||||||
color?: string;
|
|
||||||
shadow?: string;
|
|
||||||
opacity?: number;
|
|
||||||
blur?: 'none' | 'sm' | 'md' | 'lg' | 'xl' | '2xl' | '3xl' | string;
|
|
||||||
pointerEvents?: 'auto' | 'none' | string;
|
|
||||||
// Flex/Grid Item props
|
// Flex/Grid Item props
|
||||||
flex?: number | string;
|
flex?: number | string;
|
||||||
flexShrink?: number;
|
flexShrink?: number;
|
||||||
@@ -113,20 +110,41 @@ export interface BoxProps<T extends ElementType> {
|
|||||||
alignSelf?: 'auto' | 'start' | 'end' | 'center' | 'stretch' | 'baseline';
|
alignSelf?: 'auto' | 'start' | 'end' | 'center' | 'stretch' | 'baseline';
|
||||||
gap?: number | string | ResponsiveValue<number | string>;
|
gap?: number | string | ResponsiveValue<number | string>;
|
||||||
gridCols?: number | ResponsiveValue<number>;
|
gridCols?: number | ResponsiveValue<number>;
|
||||||
responsiveGridCols?: number | ResponsiveValue<number>;
|
|
||||||
colSpan?: number | ResponsiveValue<number>;
|
colSpan?: number | ResponsiveValue<number>;
|
||||||
responsiveColSpan?: number | ResponsiveValue<number>;
|
|
||||||
order?: number | string | ResponsiveValue<number | string>;
|
order?: number | string | ResponsiveValue<number | string>;
|
||||||
// Transform
|
// Interaction
|
||||||
transform?: string | boolean;
|
onClick?: React.MouseEventHandler<any>;
|
||||||
translate?: string;
|
onMouseEnter?: React.MouseEventHandler<any>;
|
||||||
translateX?: string;
|
onMouseLeave?: React.MouseEventHandler<any>;
|
||||||
translateY?: string;
|
id?: string;
|
||||||
// Animation (Framer Motion support)
|
role?: React.AriaRole;
|
||||||
initial?: any;
|
tabIndex?: number;
|
||||||
animate?: any;
|
// Internal use only
|
||||||
exit?: any;
|
style?: React.CSSProperties;
|
||||||
|
className?: string;
|
||||||
|
borderTop?: string | boolean;
|
||||||
|
borderBottom?: string | boolean;
|
||||||
|
borderLeft?: string | boolean;
|
||||||
|
borderRight?: string | boolean;
|
||||||
|
bg?: string;
|
||||||
|
rounded?: string | boolean;
|
||||||
|
borderColor?: string;
|
||||||
|
border?: string | boolean;
|
||||||
|
color?: string;
|
||||||
|
opacity?: number;
|
||||||
transition?: any;
|
transition?: any;
|
||||||
|
hoverBg?: string;
|
||||||
|
group?: boolean;
|
||||||
|
groupHoverOpacity?: number;
|
||||||
|
groupHoverBorderColor?: string;
|
||||||
|
groupHoverWidth?: string;
|
||||||
|
animate?: any;
|
||||||
|
blur?: string;
|
||||||
|
pointerEvents?: string;
|
||||||
|
bgOpacity?: number;
|
||||||
|
borderWidth?: string | number;
|
||||||
|
borderStyle?: string;
|
||||||
|
initial?: any;
|
||||||
variants?: any;
|
variants?: any;
|
||||||
whileHover?: any;
|
whileHover?: any;
|
||||||
whileTap?: any;
|
whileTap?: any;
|
||||||
@@ -135,39 +153,14 @@ export interface BoxProps<T extends ElementType> {
|
|||||||
whileInView?: any;
|
whileInView?: any;
|
||||||
viewport?: any;
|
viewport?: any;
|
||||||
custom?: any;
|
custom?: any;
|
||||||
// Interaction
|
exit?: any;
|
||||||
group?: boolean;
|
translateX?: string;
|
||||||
groupHoverTextColor?: string;
|
translateY?: string;
|
||||||
groupHoverScale?: boolean;
|
translate?: string;
|
||||||
groupHoverOpacity?: number;
|
|
||||||
groupHoverBorderColor?: string;
|
|
||||||
hoverBorderColor?: string;
|
|
||||||
hoverBg?: string;
|
|
||||||
hoverTextColor?: string;
|
|
||||||
hoverScale?: boolean | number;
|
|
||||||
clickable?: boolean;
|
|
||||||
// Events
|
|
||||||
onMouseEnter?: React.MouseEventHandler<any>;
|
|
||||||
onMouseLeave?: React.MouseEventHandler<any>;
|
|
||||||
onClick?: React.MouseEventHandler<any>;
|
|
||||||
onMouseDown?: React.MouseEventHandler<any>;
|
|
||||||
onMouseUp?: React.MouseEventHandler<any>;
|
|
||||||
onMouseMove?: React.MouseEventHandler<any>;
|
|
||||||
onKeyDown?: React.KeyboardEventHandler<any>;
|
|
||||||
onBlur?: React.FocusEventHandler<any>;
|
|
||||||
onSubmit?: React.FormEventHandler<any>;
|
|
||||||
onScroll?: React.UIEventHandler<any>;
|
|
||||||
style?: React.CSSProperties;
|
|
||||||
id?: string;
|
|
||||||
role?: React.AriaRole;
|
|
||||||
tabIndex?: number;
|
|
||||||
// Other
|
|
||||||
type?: 'button' | 'submit' | 'reset' | string;
|
|
||||||
disabled?: boolean;
|
|
||||||
cursor?: string;
|
cursor?: string;
|
||||||
fontSize?: string | ResponsiveValue<string>;
|
fontSize?: string | ResponsiveValue<string>;
|
||||||
weight?: string;
|
|
||||||
fontWeight?: string | number;
|
fontWeight?: string | number;
|
||||||
|
weight?: string | number;
|
||||||
letterSpacing?: string;
|
letterSpacing?: string;
|
||||||
lineHeight?: string | number;
|
lineHeight?: string | number;
|
||||||
font?: string;
|
font?: string;
|
||||||
@@ -176,19 +169,15 @@ export interface BoxProps<T extends ElementType> {
|
|||||||
truncate?: boolean;
|
truncate?: boolean;
|
||||||
src?: string;
|
src?: string;
|
||||||
alt?: string;
|
alt?: string;
|
||||||
draggable?: boolean;
|
draggable?: boolean | string;
|
||||||
min?: string | number;
|
min?: string | number;
|
||||||
max?: string | number;
|
max?: string | number;
|
||||||
step?: string | number;
|
step?: string | number;
|
||||||
value?: string | number;
|
value?: string | number;
|
||||||
onChange?: React.ChangeEventHandler<any>;
|
onChange?: React.ChangeEventHandler<any>;
|
||||||
|
onError?: React.ReactEventHandler<any>;
|
||||||
placeholder?: string;
|
placeholder?: string;
|
||||||
title?: string;
|
title?: string;
|
||||||
padding?: Spacing | ResponsiveSpacing;
|
|
||||||
paddingLeft?: Spacing | ResponsiveSpacing;
|
|
||||||
paddingRight?: Spacing | ResponsiveSpacing;
|
|
||||||
paddingTop?: Spacing | ResponsiveSpacing;
|
|
||||||
paddingBottom?: Spacing | ResponsiveSpacing;
|
|
||||||
size?: string | number | ResponsiveValue<string | number>;
|
size?: string | number | ResponsiveValue<string | number>;
|
||||||
accept?: string;
|
accept?: string;
|
||||||
autoPlay?: boolean;
|
autoPlay?: boolean;
|
||||||
@@ -196,16 +185,46 @@ export interface BoxProps<T extends ElementType> {
|
|||||||
muted?: boolean;
|
muted?: boolean;
|
||||||
playsInline?: boolean;
|
playsInline?: boolean;
|
||||||
objectFit?: 'fill' | 'contain' | 'cover' | 'none' | 'scale-down';
|
objectFit?: 'fill' | 'contain' | 'cover' | 'none' | 'scale-down';
|
||||||
|
type?: string;
|
||||||
|
checked?: boolean;
|
||||||
|
disabled?: boolean;
|
||||||
|
onSubmit?: React.FormEventHandler<any>;
|
||||||
|
onBlur?: React.FocusEventHandler<any>;
|
||||||
|
onKeyDown?: React.KeyboardEventHandler<any>;
|
||||||
|
onMouseDown?: React.MouseEventHandler<any>;
|
||||||
|
onMouseUp?: React.MouseEventHandler<any>;
|
||||||
|
onMouseMove?: React.MouseEventHandler<any>;
|
||||||
|
onScroll?: React.UIEventHandler<any>;
|
||||||
|
responsiveColSpan?: number | ResponsiveValue<number>;
|
||||||
|
responsiveGridCols?: number | ResponsiveValue<number>;
|
||||||
|
clickable?: boolean;
|
||||||
|
hoverScale?: boolean | number;
|
||||||
|
hoverBorderColor?: string;
|
||||||
|
hoverTextColor?: string;
|
||||||
|
groupHoverScale?: boolean;
|
||||||
|
groupHoverTextColor?: string;
|
||||||
|
shadow?: string;
|
||||||
|
transform?: boolean | string;
|
||||||
|
lineClamp?: number;
|
||||||
|
fill?: string | boolean;
|
||||||
|
viewBox?: string;
|
||||||
|
stroke?: string;
|
||||||
|
strokeWidth?: string | number;
|
||||||
|
backgroundSize?: string;
|
||||||
|
backgroundPosition?: string;
|
||||||
|
backgroundImage?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const Box = forwardRef(<T extends ElementType = 'div'>(
|
export const Box = forwardRef(<T extends ElementType = 'div'>(
|
||||||
{
|
{
|
||||||
as,
|
as,
|
||||||
children,
|
children,
|
||||||
className = '',
|
margin, marginTop, marginBottom, marginLeft, marginRight, marginX, marginY,
|
||||||
|
padding, paddingTop, paddingBottom, paddingLeft, paddingRight, paddingX, paddingY,
|
||||||
m, mt, mb, ml, mr, mx, my,
|
m, mt, mb, ml, mr, mx, my,
|
||||||
p, pt, pb, pl, pr, px, py,
|
p, pt, pb, pl, pr, px, py,
|
||||||
w, h, width, height,
|
width, height,
|
||||||
|
w, h,
|
||||||
maxWidth, minWidth, maxHeight, minHeight,
|
maxWidth, minWidth, maxHeight, minHeight,
|
||||||
fullWidth, fullHeight,
|
fullWidth, fullHeight,
|
||||||
aspectRatio,
|
aspectRatio,
|
||||||
@@ -218,27 +237,6 @@ export const Box = forwardRef(<T extends ElementType = 'div'>(
|
|||||||
top, right, bottom, left,
|
top, right, bottom, left,
|
||||||
inset, insetY, insetX,
|
inset, insetY, insetX,
|
||||||
zIndex,
|
zIndex,
|
||||||
rounded,
|
|
||||||
border,
|
|
||||||
borderTop,
|
|
||||||
borderBottom,
|
|
||||||
borderLeft,
|
|
||||||
borderRight,
|
|
||||||
borderWidth,
|
|
||||||
borderStyle,
|
|
||||||
borderColor,
|
|
||||||
borderOpacity,
|
|
||||||
bg,
|
|
||||||
backgroundColor,
|
|
||||||
backgroundImage,
|
|
||||||
backgroundSize,
|
|
||||||
backgroundPosition,
|
|
||||||
bgOpacity,
|
|
||||||
color,
|
|
||||||
shadow,
|
|
||||||
opacity,
|
|
||||||
blur,
|
|
||||||
pointerEvents,
|
|
||||||
flex,
|
flex,
|
||||||
flexShrink,
|
flexShrink,
|
||||||
flexGrow,
|
flexGrow,
|
||||||
@@ -249,18 +247,39 @@ export const Box = forwardRef(<T extends ElementType = 'div'>(
|
|||||||
alignSelf,
|
alignSelf,
|
||||||
gap,
|
gap,
|
||||||
gridCols,
|
gridCols,
|
||||||
responsiveGridCols,
|
|
||||||
colSpan,
|
colSpan,
|
||||||
responsiveColSpan,
|
|
||||||
order,
|
order,
|
||||||
transform,
|
onClick,
|
||||||
translate,
|
onMouseEnter,
|
||||||
translateX,
|
onMouseLeave,
|
||||||
translateY,
|
id,
|
||||||
initial,
|
role,
|
||||||
animate,
|
tabIndex,
|
||||||
exit,
|
style: styleProp,
|
||||||
|
className,
|
||||||
|
borderTop,
|
||||||
|
borderBottom,
|
||||||
|
borderLeft,
|
||||||
|
borderRight,
|
||||||
|
bg,
|
||||||
|
rounded,
|
||||||
|
borderColor,
|
||||||
|
border,
|
||||||
|
color,
|
||||||
|
opacity,
|
||||||
transition,
|
transition,
|
||||||
|
hoverBg,
|
||||||
|
group,
|
||||||
|
groupHoverOpacity,
|
||||||
|
groupHoverBorderColor,
|
||||||
|
groupHoverWidth,
|
||||||
|
animate,
|
||||||
|
blur,
|
||||||
|
pointerEvents,
|
||||||
|
bgOpacity,
|
||||||
|
borderWidth,
|
||||||
|
borderStyle,
|
||||||
|
initial,
|
||||||
variants,
|
variants,
|
||||||
whileHover,
|
whileHover,
|
||||||
whileTap,
|
whileTap,
|
||||||
@@ -269,36 +288,14 @@ export const Box = forwardRef(<T extends ElementType = 'div'>(
|
|||||||
whileInView,
|
whileInView,
|
||||||
viewport,
|
viewport,
|
||||||
custom,
|
custom,
|
||||||
group,
|
exit,
|
||||||
groupHoverTextColor,
|
translateX,
|
||||||
groupHoverScale,
|
translateY,
|
||||||
groupHoverOpacity,
|
translate,
|
||||||
groupHoverBorderColor,
|
|
||||||
hoverBorderColor,
|
|
||||||
hoverBg,
|
|
||||||
hoverTextColor,
|
|
||||||
hoverScale,
|
|
||||||
clickable,
|
|
||||||
onMouseEnter,
|
|
||||||
onMouseLeave,
|
|
||||||
onClick,
|
|
||||||
onMouseDown,
|
|
||||||
onMouseUp,
|
|
||||||
onMouseMove,
|
|
||||||
onKeyDown,
|
|
||||||
onBlur,
|
|
||||||
onSubmit,
|
|
||||||
onScroll,
|
|
||||||
style: styleProp,
|
|
||||||
id,
|
|
||||||
role,
|
|
||||||
tabIndex,
|
|
||||||
type,
|
|
||||||
disabled,
|
|
||||||
cursor,
|
cursor,
|
||||||
fontSize,
|
fontSize,
|
||||||
weight,
|
|
||||||
fontWeight,
|
fontWeight,
|
||||||
|
weight,
|
||||||
letterSpacing,
|
letterSpacing,
|
||||||
lineHeight,
|
lineHeight,
|
||||||
font,
|
font,
|
||||||
@@ -313,13 +310,9 @@ export const Box = forwardRef(<T extends ElementType = 'div'>(
|
|||||||
step,
|
step,
|
||||||
value,
|
value,
|
||||||
onChange,
|
onChange,
|
||||||
|
onError,
|
||||||
placeholder,
|
placeholder,
|
||||||
title,
|
title,
|
||||||
padding,
|
|
||||||
paddingLeft,
|
|
||||||
paddingRight,
|
|
||||||
paddingTop,
|
|
||||||
paddingBottom,
|
|
||||||
size,
|
size,
|
||||||
accept,
|
accept,
|
||||||
autoPlay,
|
autoPlay,
|
||||||
@@ -327,8 +320,36 @@ export const Box = forwardRef(<T extends ElementType = 'div'>(
|
|||||||
muted,
|
muted,
|
||||||
playsInline,
|
playsInline,
|
||||||
objectFit,
|
objectFit,
|
||||||
|
type,
|
||||||
|
checked,
|
||||||
|
disabled,
|
||||||
|
onSubmit,
|
||||||
|
onBlur,
|
||||||
|
onKeyDown,
|
||||||
|
onMouseDown,
|
||||||
|
onMouseUp,
|
||||||
|
onMouseMove,
|
||||||
|
onScroll,
|
||||||
|
responsiveColSpan,
|
||||||
|
responsiveGridCols,
|
||||||
|
clickable,
|
||||||
|
hoverScale,
|
||||||
|
hoverBorderColor,
|
||||||
|
hoverTextColor,
|
||||||
|
groupHoverScale,
|
||||||
|
groupHoverTextColor,
|
||||||
|
shadow,
|
||||||
|
transform,
|
||||||
|
lineClamp,
|
||||||
|
fill,
|
||||||
|
viewBox,
|
||||||
|
stroke,
|
||||||
|
strokeWidth,
|
||||||
|
backgroundSize,
|
||||||
|
backgroundPosition,
|
||||||
|
backgroundImage,
|
||||||
...props
|
...props
|
||||||
}: BoxProps<T> & ComponentPropsWithoutRef<T>,
|
}: BoxProps<T>,
|
||||||
ref: ForwardedRef<HTMLElement>
|
ref: ForwardedRef<HTMLElement>
|
||||||
) => {
|
) => {
|
||||||
const Tag = (as as ElementType) || 'div';
|
const Tag = (as as ElementType) || 'div';
|
||||||
@@ -371,22 +392,22 @@ export const Box = forwardRef(<T extends ElementType = 'div'>(
|
|||||||
};
|
};
|
||||||
|
|
||||||
const classes = [
|
const classes = [
|
||||||
getSpacingClass('m', m),
|
getSpacingClass('m', margin || m),
|
||||||
getSpacingClass('mt', mt),
|
getSpacingClass('mt', marginTop || mt),
|
||||||
getSpacingClass('mb', mb),
|
getSpacingClass('mb', marginBottom || mb),
|
||||||
getSpacingClass('ml', ml),
|
getSpacingClass('ml', marginLeft || ml),
|
||||||
getSpacingClass('mr', mr),
|
getSpacingClass('mr', marginRight || mr),
|
||||||
getSpacingClass('mx', mx),
|
getSpacingClass('mx', marginX || mx),
|
||||||
getSpacingClass('my', my),
|
getSpacingClass('my', marginY || my),
|
||||||
getSpacingClass('p', p || padding),
|
getSpacingClass('p', padding || p),
|
||||||
getSpacingClass('pt', pt || paddingTop),
|
getSpacingClass('pt', paddingTop || pt),
|
||||||
getSpacingClass('pb', pb || paddingBottom),
|
getSpacingClass('pb', paddingBottom || pb),
|
||||||
getSpacingClass('pl', pl || paddingLeft),
|
getSpacingClass('pl', paddingLeft || pl),
|
||||||
getSpacingClass('pr', pr || paddingRight),
|
getSpacingClass('pr', paddingRight || pr),
|
||||||
getSpacingClass('px', px),
|
getSpacingClass('px', paddingX || px),
|
||||||
getSpacingClass('py', py),
|
getSpacingClass('py', paddingY || py),
|
||||||
fullWidth ? 'w-full' : getResponsiveClasses('w', w),
|
fullWidth ? 'w-full' : getResponsiveClasses('w', width || w),
|
||||||
fullHeight ? 'h-full' : getResponsiveClasses('h', h),
|
fullHeight ? 'h-full' : getResponsiveClasses('h', height || h),
|
||||||
getResponsiveClasses('max-w', maxWidth),
|
getResponsiveClasses('max-w', maxWidth),
|
||||||
getResponsiveClasses('min-w', minWidth),
|
getResponsiveClasses('min-w', minWidth),
|
||||||
getResponsiveClasses('max-h', maxHeight),
|
getResponsiveClasses('max-h', maxHeight),
|
||||||
@@ -407,23 +428,6 @@ export const Box = forwardRef(<T extends ElementType = 'div'>(
|
|||||||
insetY !== undefined ? `inset-y-${insetY}` : '',
|
insetY !== undefined ? `inset-y-${insetY}` : '',
|
||||||
insetX !== undefined ? `inset-x-${insetX}` : '',
|
insetX !== undefined ? `inset-x-${insetX}` : '',
|
||||||
zIndex !== undefined ? `z-${zIndex}` : '',
|
zIndex !== undefined ? `z-${zIndex}` : '',
|
||||||
rounded === true ? 'rounded' : (rounded === false ? 'rounded-none' : (typeof rounded === 'string' ? (rounded.includes('-') ? rounded : `rounded-${rounded}`) : '')),
|
|
||||||
border === true ? 'border' : (typeof border === 'string' ? (border === 'none' ? 'border-none' : border) : ''),
|
|
||||||
borderTop === true ? 'border-t' : (typeof borderTop === 'string' ? borderTop : ''),
|
|
||||||
borderBottom === true ? 'border-b' : (typeof borderBottom === 'string' ? borderBottom : ''),
|
|
||||||
borderLeft === true ? 'border-l' : (typeof borderLeft === 'string' ? borderLeft : ''),
|
|
||||||
borderRight === true ? 'border-r' : (typeof borderRight === 'string' ? borderRight : ''),
|
|
||||||
borderStyle ? `border-${borderStyle}` : '',
|
|
||||||
borderColor ? borderColor : '',
|
|
||||||
borderOpacity !== undefined ? `border-opacity-${borderOpacity * 100}` : '',
|
|
||||||
bg ? bg : '',
|
|
||||||
backgroundColor ? backgroundColor : '',
|
|
||||||
bgOpacity !== undefined ? `bg-opacity-${bgOpacity * 100}` : '',
|
|
||||||
color ? color : '',
|
|
||||||
shadow ? shadow : '',
|
|
||||||
opacity !== undefined ? `opacity-${opacity * 100}` : '',
|
|
||||||
blur ? (blur === 'none' ? 'blur-none' : `blur-${blur}`) : '',
|
|
||||||
pointerEvents ? `pointer-events-${pointerEvents}` : '',
|
|
||||||
flex !== undefined ? `flex-${flex}` : '',
|
flex !== undefined ? `flex-${flex}` : '',
|
||||||
flexShrink !== undefined ? `flex-shrink-${flexShrink}` : '',
|
flexShrink !== undefined ? `flex-shrink-${flexShrink}` : '',
|
||||||
flexGrow !== undefined ? `flex-grow-${flexGrow}` : '',
|
flexGrow !== undefined ? `flex-grow-${flexGrow}` : '',
|
||||||
@@ -436,21 +440,14 @@ export const Box = forwardRef(<T extends ElementType = 'div'>(
|
|||||||
getResponsiveClasses('grid-cols', gridCols || responsiveGridCols),
|
getResponsiveClasses('grid-cols', gridCols || responsiveGridCols),
|
||||||
getResponsiveClasses('col-span', colSpan || responsiveColSpan),
|
getResponsiveClasses('col-span', colSpan || responsiveColSpan),
|
||||||
getResponsiveClasses('order', order),
|
getResponsiveClasses('order', order),
|
||||||
getResponsiveClasses('text', fontSize),
|
|
||||||
group ? 'group' : '',
|
group ? 'group' : '',
|
||||||
groupHoverTextColor ? `group-hover:text-${groupHoverTextColor}` : '',
|
animate === 'spin' ? 'animate-spin' : (animate === 'pulse' ? 'animate-pulse' : ''),
|
||||||
groupHoverScale ? 'group-hover:scale-105 transition-transform' : '',
|
blur ? `blur-${blur}` : '',
|
||||||
groupHoverOpacity !== undefined ? `group-hover:opacity-${groupHoverOpacity * 100}` : '',
|
pointerEvents ? `pointer-events-${pointerEvents}` : '',
|
||||||
groupHoverBorderColor ? `group-hover:border-${groupHoverBorderColor}` : '',
|
|
||||||
hoverBorderColor ? `hover:border-${hoverBorderColor}` : '',
|
|
||||||
hoverBg ? `hover:bg-${hoverBg}` : '',
|
|
||||||
hoverTextColor ? `hover:text-${hoverTextColor}` : '',
|
|
||||||
hoverScale === true ? 'hover:scale-105 transition-transform' : (typeof hoverScale === 'number' ? `hover:scale-${hoverScale} transition-transform` : ''),
|
|
||||||
clickable ? 'cursor-pointer active:opacity-80 transition-all' : '',
|
|
||||||
ring ? `ring-${ring}` : '',
|
|
||||||
hideScrollbar ? 'scrollbar-hide' : '',
|
hideScrollbar ? 'scrollbar-hide' : '',
|
||||||
truncate ? 'truncate' : '',
|
truncate ? 'truncate' : '',
|
||||||
transform === true ? 'transform' : (transform === false ? 'transform-none' : ''),
|
clickable ? 'cursor-pointer' : '',
|
||||||
|
lineClamp ? `line-clamp-${lineClamp}` : '',
|
||||||
className
|
className
|
||||||
].filter(Boolean).join(' ');
|
].filter(Boolean).join(' ');
|
||||||
|
|
||||||
@@ -466,23 +463,27 @@ export const Box = forwardRef(<T extends ElementType = 'div'>(
|
|||||||
...(typeof right === 'string' || typeof right === 'number' ? { right } : {}),
|
...(typeof right === 'string' || typeof right === 'number' ? { right } : {}),
|
||||||
...(typeof bottom === 'string' || typeof bottom === 'number' ? { bottom } : {}),
|
...(typeof bottom === 'string' || typeof bottom === 'number' ? { bottom } : {}),
|
||||||
...(typeof left === 'string' || typeof left === 'number' ? { left } : {}),
|
...(typeof left === 'string' || typeof left === 'number' ? { left } : {}),
|
||||||
...(borderWidth !== undefined ? { borderWidth } : {}),
|
...(typeof borderTop === 'string' ? { borderTop } : (borderTop === true ? { borderTop: '1px solid var(--ui-color-border-default)' } : {})),
|
||||||
...(typeof transform === 'string' ? { transform } : {}),
|
...(typeof borderBottom === 'string' ? { borderBottom } : (borderBottom === true ? { borderBottom: '1px solid var(--ui-color-border-default)' } : {})),
|
||||||
...(translate ? { translate } : {}),
|
...(typeof borderLeft === 'string' ? { borderLeft } : (borderLeft === true ? { borderLeft: '1px solid var(--ui-color-border-default)' } : {})),
|
||||||
...(translateX ? { transform: `translateX(${translateX})` } : {}),
|
...(typeof borderRight === 'string' ? { borderRight } : (borderRight === true ? { borderRight: '1px solid var(--ui-color-border-default)' } : {})),
|
||||||
...(translateY ? { transform: `translateY(${translateY})` } : {}),
|
...(bg ? { background: bg.startsWith('bg-') ? undefined : bg } : {}),
|
||||||
...(cursor ? { cursor } : {}),
|
...(rounded === true ? { borderRadius: 'var(--ui-radius-md)' } : (typeof rounded === 'string' ? { borderRadius: rounded.includes('rem') || rounded.includes('px') ? rounded : `var(--ui-radius-${rounded})` } : {})),
|
||||||
...(fontSize && typeof fontSize === 'string' && !fontSize.includes(':') ? { fontSize } : {}),
|
...(borderColor ? { borderColor: borderColor.startsWith('border-') ? undefined : borderColor } : {}),
|
||||||
...(weight ? { fontWeight: weight } : {}),
|
...(border === true ? { border: '1px solid var(--ui-color-border-default)' } : (typeof border === 'string' ? { border } : {})),
|
||||||
|
...(color ? { color: color.startsWith('text-') ? undefined : color } : {}),
|
||||||
|
...(opacity !== undefined ? { opacity } : {}),
|
||||||
|
...(fontSize ? (typeof fontSize === 'string' ? { fontSize } : {}) : {}),
|
||||||
...(fontWeight ? { fontWeight } : {}),
|
...(fontWeight ? { fontWeight } : {}),
|
||||||
...(letterSpacing ? { letterSpacing } : {}),
|
...(weight ? { fontWeight: weight } : {}),
|
||||||
...(lineHeight ? { lineHeight } : {}),
|
...(shadow ? { boxShadow: shadow.startsWith('shadow-') ? undefined : shadow } : {}),
|
||||||
...(font ? { fontFamily: font } : {}),
|
...(transform === true ? { transform: 'auto' } : (typeof transform === 'string' ? { transform } : {})),
|
||||||
...(typeof size === 'string' || typeof size === 'number' ? { width: size, height: size } : {}),
|
...(typeof fill === 'string' ? { fill } : (fill === true ? { fill: 'currentColor' } : {})),
|
||||||
...(backgroundImage ? { backgroundImage } : {}),
|
...(stroke ? { stroke } : {}),
|
||||||
|
...(strokeWidth ? { strokeWidth } : {}),
|
||||||
...(backgroundSize ? { backgroundSize } : {}),
|
...(backgroundSize ? { backgroundSize } : {}),
|
||||||
...(backgroundPosition ? { backgroundPosition } : {}),
|
...(backgroundPosition ? { backgroundPosition } : {}),
|
||||||
...(objectFit ? { objectFit } : {}),
|
...(backgroundImage ? { backgroundImage } : {}),
|
||||||
...(styleProp || {})
|
...(styleProp || {})
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -491,24 +492,24 @@ export const Box = forwardRef(<T extends ElementType = 'div'>(
|
|||||||
ref={ref as React.ForwardedRef<HTMLElement>}
|
ref={ref as React.ForwardedRef<HTMLElement>}
|
||||||
className={classes}
|
className={classes}
|
||||||
onClick={onClick}
|
onClick={onClick}
|
||||||
|
onMouseEnter={onMouseEnter}
|
||||||
|
onMouseLeave={onMouseLeave}
|
||||||
onMouseDown={onMouseDown}
|
onMouseDown={onMouseDown}
|
||||||
onMouseUp={onMouseUp}
|
onMouseUp={onMouseUp}
|
||||||
onMouseMove={onMouseMove}
|
onMouseMove={onMouseMove}
|
||||||
onKeyDown={onKeyDown}
|
onKeyDown={onKeyDown}
|
||||||
onBlur={onBlur}
|
onBlur={onBlur}
|
||||||
onSubmit={onSubmit}
|
onSubmit={onSubmit}
|
||||||
onMouseEnter={onMouseEnter}
|
|
||||||
onMouseLeave={onMouseLeave}
|
|
||||||
onScroll={onScroll}
|
onScroll={onScroll}
|
||||||
style={style}
|
onError={onError}
|
||||||
|
style={Object.keys(style).length > 0 ? style : undefined}
|
||||||
id={id}
|
id={id}
|
||||||
role={role}
|
role={role}
|
||||||
tabIndex={tabIndex}
|
tabIndex={tabIndex}
|
||||||
type={type}
|
type={type}
|
||||||
disabled={disabled}
|
|
||||||
src={src}
|
src={src}
|
||||||
alt={alt}
|
alt={alt}
|
||||||
draggable={draggable}
|
draggable={draggable as any}
|
||||||
min={min}
|
min={min}
|
||||||
max={max}
|
max={max}
|
||||||
step={step}
|
step={step}
|
||||||
@@ -520,6 +521,7 @@ export const Box = forwardRef(<T extends ElementType = 'div'>(
|
|||||||
loop={loop}
|
loop={loop}
|
||||||
muted={muted}
|
muted={muted}
|
||||||
playsInline={playsInline}
|
playsInline={playsInline}
|
||||||
|
viewBox={viewBox}
|
||||||
{...props}
|
{...props}
|
||||||
>
|
>
|
||||||
{children}
|
{children}
|
||||||
|
|||||||
@@ -13,57 +13,25 @@ import { Box, BoxProps, ResponsiveValue } from './Box';
|
|||||||
* If you need a more specific layout, create a new component in apps/website/components.
|
* If you need a more specific layout, create a new component in apps/website/components.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
export interface GridProps<T extends ElementType = 'div'> extends Omit<BoxProps<T>, 'children'> {
|
export interface GridProps<T extends ElementType = 'div'> extends BoxProps<T> {
|
||||||
children?: ReactNode;
|
children?: ReactNode;
|
||||||
cols?: 1 | 2 | 3 | 4 | 5 | 6 | 12;
|
columns?: number | ResponsiveValue<number>;
|
||||||
mdCols?: 1 | 2 | 3 | 4 | 5 | 6 | 12;
|
gap?: number | string | ResponsiveValue<number | string>;
|
||||||
lgCols?: 1 | 2 | 3 | 4 | 5 | 6 | 12;
|
|
||||||
gap?: 0 | 1 | 2 | 3 | 4 | 6 | 8 | 12 | 16;
|
|
||||||
className?: string;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function Grid<T extends ElementType = 'div'>({
|
export function Grid<T extends ElementType = 'div'>({
|
||||||
children,
|
children,
|
||||||
cols = 1,
|
columns = 1,
|
||||||
mdCols,
|
|
||||||
lgCols,
|
|
||||||
gap = 4,
|
gap = 4,
|
||||||
className = '',
|
|
||||||
...props
|
...props
|
||||||
}: GridProps<T>) {
|
}: GridProps<T>) {
|
||||||
const colClasses: Record<number, string> = {
|
|
||||||
1: 'grid-cols-1',
|
|
||||||
2: 'grid-cols-1 md:grid-cols-2',
|
|
||||||
3: 'grid-cols-1 md:grid-cols-3',
|
|
||||||
4: 'grid-cols-2 md:grid-cols-4',
|
|
||||||
5: 'grid-cols-2 md:grid-cols-3 lg:grid-cols-5',
|
|
||||||
6: 'grid-cols-2 md:grid-cols-3 lg:grid-cols-6',
|
|
||||||
12: 'grid-cols-12'
|
|
||||||
};
|
|
||||||
|
|
||||||
const gapClasses: Record<number, string> = {
|
|
||||||
0: 'gap-0',
|
|
||||||
1: 'gap-1',
|
|
||||||
2: 'gap-2',
|
|
||||||
3: 'gap-3',
|
|
||||||
4: 'gap-4',
|
|
||||||
6: 'gap-6',
|
|
||||||
8: 'gap-8',
|
|
||||||
12: 'gap-12',
|
|
||||||
16: 'gap-16'
|
|
||||||
};
|
|
||||||
|
|
||||||
const classes = [
|
|
||||||
'grid',
|
|
||||||
colClasses[cols] || 'grid-cols-1',
|
|
||||||
mdCols ? `md:grid-cols-${mdCols}` : '',
|
|
||||||
lgCols ? `lg:grid-cols-${lgCols}` : '',
|
|
||||||
gapClasses[gap] || 'gap-4',
|
|
||||||
className
|
|
||||||
].filter(Boolean).join(' ');
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box className={classes} {...props}>
|
<Box
|
||||||
|
display="grid"
|
||||||
|
gridCols={columns}
|
||||||
|
gap={gap}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
{children}
|
{children}
|
||||||
</Box>
|
</Box>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,35 +1,27 @@
|
|||||||
import React, { ElementType } from 'react';
|
import React, { ReactNode, ElementType } from 'react';
|
||||||
import { Box, BoxProps } from './Box';
|
import { Box, BoxProps, ResponsiveValue } from './Box';
|
||||||
|
|
||||||
/**
|
export interface GridItemProps<T extends ElementType = 'div'> extends BoxProps<T> {
|
||||||
* WARNING: DO NOT VIOLATE THE PURPOSE OF THIS PRIMITIVE.
|
children?: ReactNode;
|
||||||
*
|
colSpan?: number | ResponsiveValue<number>;
|
||||||
* GridItem is for items inside a Grid container.
|
rowSpan?: number | ResponsiveValue<number>;
|
||||||
*
|
lgSpan?: number; // Alias for colSpan.lg
|
||||||
* - DO NOT add positioning props (absolute, top, zIndex).
|
|
||||||
* - DO NOT add background/border props.
|
|
||||||
*
|
|
||||||
* If you need a more specific layout, create a new component in apps/website/components.
|
|
||||||
*/
|
|
||||||
|
|
||||||
export interface GridItemProps<T extends ElementType = 'div'> extends Omit<BoxProps<T>, 'children'> {
|
|
||||||
children?: React.ReactNode;
|
|
||||||
colSpan?: 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12;
|
|
||||||
mdSpan?: 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12;
|
|
||||||
lgSpan?: 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12;
|
|
||||||
className?: string;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function GridItem<T extends ElementType = 'div'>({ children, colSpan, mdSpan, lgSpan, className = '', ...props }: GridItemProps<T>) {
|
export function GridItem<T extends ElementType = 'div'>({
|
||||||
const spanClasses = [
|
children,
|
||||||
colSpan ? `col-span-${colSpan}` : '',
|
colSpan,
|
||||||
mdSpan ? `md:col-span-${mdSpan}` : '',
|
rowSpan,
|
||||||
lgSpan ? `lg:col-span-${lgSpan}` : '',
|
lgSpan,
|
||||||
className
|
...props
|
||||||
].filter(Boolean).join(' ');
|
}: GridItemProps<T>) {
|
||||||
|
const actualColSpan = lgSpan ? { base: colSpan as any, lg: lgSpan } : colSpan;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box className={spanClasses} {...props}>
|
<Box
|
||||||
|
colSpan={actualColSpan as any}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
{children}
|
{children}
|
||||||
</Box>
|
</Box>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -13,20 +13,10 @@ import { Box, BoxProps, ResponsiveValue } from './Box';
|
|||||||
* If you need a more specific layout, create a new component in apps/website/components.
|
* If you need a more specific layout, create a new component in apps/website/components.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
interface ResponsiveGap {
|
export interface StackProps<T extends ElementType> extends BoxProps<T> {
|
||||||
base?: number;
|
|
||||||
sm?: number;
|
|
||||||
md?: number;
|
|
||||||
lg?: number;
|
|
||||||
xl?: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface StackProps<T extends ElementType> extends Omit<BoxProps<T>, 'children'> {
|
|
||||||
as?: T;
|
as?: T;
|
||||||
children?: ReactNode;
|
children?: ReactNode;
|
||||||
className?: string;
|
direction?: 'row' | 'col' | ResponsiveValue<'row' | 'col'>;
|
||||||
direction?: 'row' | 'col' | { base?: 'row' | 'col'; md?: 'row' | 'col'; lg?: 'row' | 'col' };
|
|
||||||
gap?: number | string | ResponsiveGap;
|
|
||||||
align?: 'start' | 'center' | 'end' | 'stretch' | 'baseline' | ResponsiveValue<'start' | 'center' | 'end' | 'stretch' | 'baseline'>;
|
align?: 'start' | 'center' | 'end' | 'stretch' | 'baseline' | ResponsiveValue<'start' | 'center' | 'end' | 'stretch' | 'baseline'>;
|
||||||
justify?: 'start' | 'center' | 'end' | 'between' | 'around' | ResponsiveValue<'start' | 'center' | 'end' | 'between' | 'around'>;
|
justify?: 'start' | 'center' | 'end' | 'between' | 'around' | ResponsiveValue<'start' | 'center' | 'end' | 'between' | 'around'>;
|
||||||
wrap?: boolean;
|
wrap?: boolean;
|
||||||
@@ -35,7 +25,6 @@ export interface StackProps<T extends ElementType> extends Omit<BoxProps<T>, 'ch
|
|||||||
export const Stack = forwardRef(<T extends ElementType = 'div'>(
|
export const Stack = forwardRef(<T extends ElementType = 'div'>(
|
||||||
{
|
{
|
||||||
children,
|
children,
|
||||||
className = '',
|
|
||||||
direction = 'col',
|
direction = 'col',
|
||||||
gap = 4,
|
gap = 4,
|
||||||
align,
|
align,
|
||||||
@@ -46,91 +35,16 @@ export const Stack = forwardRef(<T extends ElementType = 'div'>(
|
|||||||
}: StackProps<T>,
|
}: StackProps<T>,
|
||||||
ref: ForwardedRef<HTMLElement>
|
ref: ForwardedRef<HTMLElement>
|
||||||
) => {
|
) => {
|
||||||
const gapClasses: Record<number, string> = {
|
|
||||||
0: 'gap-0',
|
|
||||||
1: 'gap-1',
|
|
||||||
2: 'gap-2',
|
|
||||||
3: 'gap-3',
|
|
||||||
4: 'gap-4',
|
|
||||||
5: 'gap-5',
|
|
||||||
6: 'gap-6',
|
|
||||||
8: 'gap-8',
|
|
||||||
10: 'gap-10',
|
|
||||||
12: 'gap-12',
|
|
||||||
16: 'gap-16'
|
|
||||||
};
|
|
||||||
|
|
||||||
const getGapClasses = (value: number | string | ResponsiveGap | undefined) => {
|
|
||||||
if (value === undefined) return '';
|
|
||||||
if (typeof value === 'object') {
|
|
||||||
const classes = [];
|
|
||||||
if (value.base !== undefined) classes.push(typeof value.base === 'number' ? gapClasses[value.base] : `gap-${value.base}`);
|
|
||||||
if (value.sm !== undefined) classes.push(typeof value.sm === 'number' ? `sm:${gapClasses[value.sm]}` : `sm:gap-${value.sm}`);
|
|
||||||
if (value.md !== undefined) classes.push(typeof value.md === 'number' ? `md:${gapClasses[value.md]}` : `md:gap-${value.md}`);
|
|
||||||
if (value.lg !== undefined) classes.push(typeof value.lg === 'number' ? `lg:${gapClasses[value.lg]}` : `lg:gap-${value.lg}`);
|
|
||||||
if (value.xl !== undefined) classes.push(typeof value.xl === 'number' ? `xl:${gapClasses[value.xl]}` : `xl:gap-${value.xl}`);
|
|
||||||
return classes.join(' ');
|
|
||||||
}
|
|
||||||
if (typeof value === 'number') return gapClasses[value];
|
|
||||||
return `gap-${value}`;
|
|
||||||
};
|
|
||||||
|
|
||||||
const classes = [
|
|
||||||
'flex',
|
|
||||||
typeof direction === 'string'
|
|
||||||
? (direction === 'col' ? 'flex-col' : 'flex-row')
|
|
||||||
: [
|
|
||||||
direction.base === 'col' ? 'flex-col' : (direction.base === 'row' ? 'flex-row' : ''),
|
|
||||||
direction.md === 'col' ? 'md:flex-col' : (direction.md === 'row' ? 'md:flex-row' : ''),
|
|
||||||
direction.lg === 'col' ? 'lg:flex-col' : (direction.lg === 'row' ? 'lg:flex-row' : ''),
|
|
||||||
].filter(Boolean).join(' '),
|
|
||||||
getGapClasses(gap) || 'gap-4',
|
|
||||||
wrap ? 'flex-wrap' : '',
|
|
||||||
className
|
|
||||||
].filter(Boolean).join(' ');
|
|
||||||
|
|
||||||
const getAlignItemsClass = (value: StackProps<ElementType>['align']) => {
|
|
||||||
if (!value) return '';
|
|
||||||
const map: Record<string, string> = { start: 'items-start', center: 'items-center', end: 'items-end', stretch: 'items-stretch', baseline: 'items-baseline' };
|
|
||||||
if (typeof value === 'object') {
|
|
||||||
const classes = [];
|
|
||||||
if (value.base) classes.push(map[value.base]);
|
|
||||||
if (value.sm) classes.push(`sm:${map[value.sm]}`);
|
|
||||||
if (value.md) classes.push(`md:${map[value.md]}`);
|
|
||||||
if (value.lg) classes.push(`lg:${map[value.lg]}`);
|
|
||||||
if (value.xl) classes.push(`xl:${map[value.xl]}`);
|
|
||||||
if (value['2xl']) classes.push(`2xl:${map[value['2xl']]}`);
|
|
||||||
return classes.join(' ');
|
|
||||||
}
|
|
||||||
return map[value];
|
|
||||||
};
|
|
||||||
|
|
||||||
const getJustifyContentClass = (value: StackProps<ElementType>['justify']) => {
|
|
||||||
if (!value) return '';
|
|
||||||
const map: Record<string, string> = { start: 'justify-start', center: 'justify-center', end: 'justify-end', between: 'justify-between', around: 'justify-around' };
|
|
||||||
if (typeof value === 'object') {
|
|
||||||
const classes = [];
|
|
||||||
if (value.base) classes.push(map[value.base]);
|
|
||||||
if (value.sm) classes.push(`sm:${map[value.sm]}`);
|
|
||||||
if (value.md) classes.push(`md:${map[value.md]}`);
|
|
||||||
if (value.lg) classes.push(`lg:${map[value.lg]}`);
|
|
||||||
if (value.xl) classes.push(`xl:${map[value.xl]}`);
|
|
||||||
if (value['2xl']) classes.push(`2xl:${map[value['2xl']]}`);
|
|
||||||
return classes.join(' ');
|
|
||||||
}
|
|
||||||
return map[value];
|
|
||||||
};
|
|
||||||
|
|
||||||
const layoutClasses = [
|
|
||||||
getAlignItemsClass(align),
|
|
||||||
getJustifyContentClass(justify)
|
|
||||||
].filter(Boolean).join(' ');
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box
|
<Box
|
||||||
as={as}
|
as={as}
|
||||||
ref={ref}
|
ref={ref}
|
||||||
className={`${classes} ${layoutClasses}`}
|
display="flex"
|
||||||
|
flexDirection={direction}
|
||||||
|
gap={gap}
|
||||||
|
alignItems={align}
|
||||||
|
justifyContent={justify}
|
||||||
|
flexWrap={wrap ? 'wrap' : 'nowrap'}
|
||||||
{...props}
|
{...props}
|
||||||
>
|
>
|
||||||
{children}
|
{children}
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import React, { ReactNode, ElementType, ComponentPropsWithoutRef, forwardRef, ForwardedRef } from 'react';
|
import React, { ReactNode, ElementType, forwardRef, ForwardedRef } from 'react';
|
||||||
import { Box, BoxProps } from './Box';
|
import { Box, BoxProps } from './Box';
|
||||||
|
import { ThemeRadii, ThemeShadows } from '../theme/Theme';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* WARNING: DO NOT VIOLATE THE PURPOSE OF THIS PRIMITIVE.
|
* WARNING: DO NOT VIOLATE THE PURPOSE OF THIS PRIMITIVE.
|
||||||
@@ -12,15 +13,12 @@ import { Box, BoxProps } from './Box';
|
|||||||
* If you need a more specific layout, create a new component in apps/website/components.
|
* If you need a more specific layout, create a new component in apps/website/components.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
export interface SurfaceProps<T extends ElementType = 'div'> extends Omit<BoxProps<T>, 'children' | 'padding'> {
|
export interface SurfaceProps<T extends ElementType = 'div'> extends BoxProps<T> {
|
||||||
as?: T;
|
as?: T;
|
||||||
children?: ReactNode;
|
children?: ReactNode;
|
||||||
variant?: 'default' | 'muted' | 'dark' | 'glass' | 'gradient-blue' | 'gradient-gold' | 'gradient-purple' | 'gradient-green' | 'discord' | 'discord-inner';
|
variant?: 'default' | 'dark' | 'muted' | 'glass' | 'discord' | 'gradient-blue' | 'gradient-gold' | 'gradient-purple' | 'gradient-green' | 'discord-inner' | 'outline';
|
||||||
rounded?: 'none' | 'sm' | 'md' | 'lg' | 'xl' | '2xl' | 'full' | string | boolean;
|
rounded?: keyof ThemeRadii | 'none';
|
||||||
border?: boolean | string;
|
shadow?: keyof ThemeShadows | 'none';
|
||||||
padding?: number;
|
|
||||||
className?: string;
|
|
||||||
shadow?: 'none' | 'sm' | 'md' | 'lg' | 'xl' | 'discord' | string;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export const Surface = forwardRef(<T extends ElementType = 'div'>(
|
export const Surface = forwardRef(<T extends ElementType = 'div'>(
|
||||||
@@ -29,69 +27,52 @@ export const Surface = forwardRef(<T extends ElementType = 'div'>(
|
|||||||
children,
|
children,
|
||||||
variant = 'default',
|
variant = 'default',
|
||||||
rounded = 'none',
|
rounded = 'none',
|
||||||
border = false,
|
|
||||||
padding = 0,
|
|
||||||
className = '',
|
|
||||||
shadow = 'none',
|
shadow = 'none',
|
||||||
...props
|
...props
|
||||||
}: SurfaceProps<T> & ComponentPropsWithoutRef<T>,
|
}: SurfaceProps<T>,
|
||||||
ref: ForwardedRef<HTMLElement>
|
ref: ForwardedRef<HTMLElement>
|
||||||
) => {
|
) => {
|
||||||
const variantClasses: Record<string, string> = {
|
const variantStyles: Record<string, React.CSSProperties> = {
|
||||||
default: 'bg-panel-gray',
|
default: { backgroundColor: 'var(--ui-color-bg-surface)' },
|
||||||
muted: 'bg-panel-gray/40',
|
dark: { backgroundColor: 'var(--ui-color-bg-base)' },
|
||||||
dark: 'bg-graphite-black',
|
muted: { backgroundColor: 'var(--ui-color-bg-surface-muted)' },
|
||||||
glass: 'bg-graphite-black/60 backdrop-blur-md',
|
glass: {
|
||||||
'gradient-blue': 'bg-gradient-to-br from-primary-accent/10 via-panel-gray/80 to-graphite-black',
|
backgroundColor: 'rgba(20, 22, 25, 0.6)',
|
||||||
'gradient-gold': 'bg-gradient-to-br from-warning-amber/10 via-panel-gray/80 to-graphite-black',
|
backdropFilter: 'blur(12px)',
|
||||||
'gradient-purple': 'bg-gradient-to-br from-purple-600/10 via-panel-gray/80 to-graphite-black',
|
WebkitBackdropFilter: 'blur(12px)'
|
||||||
'gradient-green': 'bg-gradient-to-br from-success-green/10 via-panel-gray/80 to-graphite-black',
|
},
|
||||||
'discord': 'bg-gradient-to-b from-graphite-black to-panel-gray',
|
discord: {
|
||||||
'discord-inner': 'bg-gradient-to-br from-panel-gray via-graphite-black to-panel-gray'
|
background: 'linear-gradient(to bottom, var(--ui-color-bg-base), var(--ui-color-bg-surface))'
|
||||||
|
},
|
||||||
|
'discord-inner': {
|
||||||
|
background: 'linear-gradient(to br, var(--ui-color-bg-surface), var(--ui-color-bg-base), var(--ui-color-bg-surface))'
|
||||||
|
},
|
||||||
|
'gradient-blue': {
|
||||||
|
background: 'linear-gradient(to br, rgba(25, 140, 255, 0.1), var(--ui-color-bg-surface), var(--ui-color-bg-base))'
|
||||||
|
},
|
||||||
|
'gradient-gold': {
|
||||||
|
background: 'linear-gradient(to br, rgba(255, 190, 77, 0.1), var(--ui-color-bg-surface), var(--ui-color-bg-base))'
|
||||||
|
},
|
||||||
|
'gradient-purple': {
|
||||||
|
background: 'linear-gradient(to br, rgba(147, 51, 234, 0.1), var(--ui-color-bg-surface), var(--ui-color-bg-base))'
|
||||||
|
},
|
||||||
|
'gradient-green': {
|
||||||
|
background: 'linear-gradient(to br, rgba(111, 227, 122, 0.1), var(--ui-color-bg-surface), var(--ui-color-bg-base))'
|
||||||
|
},
|
||||||
|
outline: {
|
||||||
|
backgroundColor: 'transparent',
|
||||||
|
border: '1px solid var(--ui-color-border-default)'
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const shadowClasses: Record<string, string> = {
|
const style: React.CSSProperties = {
|
||||||
none: '',
|
...variantStyles[variant],
|
||||||
sm: 'shadow-sm',
|
borderRadius: rounded !== 'none' ? `var(--ui-radius-${String(rounded)})` : undefined,
|
||||||
md: 'shadow-md',
|
boxShadow: shadow !== 'none' ? `var(--ui-shadow-${String(shadow)})` : undefined,
|
||||||
lg: 'shadow-lg',
|
|
||||||
xl: 'shadow-xl',
|
|
||||||
discord: 'shadow-[0_0_80px_rgba(88,101,242,0.15)]'
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const roundedClasses: Record<string, string> = {
|
|
||||||
none: 'rounded-none',
|
|
||||||
sm: 'rounded-sm',
|
|
||||||
md: 'rounded-md',
|
|
||||||
lg: 'rounded-lg',
|
|
||||||
xl: 'rounded-xl',
|
|
||||||
'2xl': 'rounded-2xl',
|
|
||||||
full: 'rounded-full'
|
|
||||||
};
|
|
||||||
|
|
||||||
const paddingClasses: Record<number, string> = {
|
|
||||||
0: 'p-0',
|
|
||||||
1: 'p-1',
|
|
||||||
2: 'p-2',
|
|
||||||
3: 'p-3',
|
|
||||||
4: 'p-4',
|
|
||||||
6: 'p-6',
|
|
||||||
8: 'p-8',
|
|
||||||
10: 'p-10',
|
|
||||||
12: 'p-12'
|
|
||||||
};
|
|
||||||
|
|
||||||
const classes = [
|
|
||||||
variantClasses[variant],
|
|
||||||
typeof rounded === 'string' && roundedClasses[rounded] ? roundedClasses[rounded] : '',
|
|
||||||
border ? 'border border-border-gray' : '',
|
|
||||||
paddingClasses[padding] || 'p-0',
|
|
||||||
shadowClasses[shadow],
|
|
||||||
className
|
|
||||||
].filter(Boolean).join(' ');
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box as={as} ref={ref} className={classes} {...props}>
|
<Box as={as} ref={ref} {...(props as any)} style={style}>
|
||||||
{children}
|
{children}
|
||||||
</Box>
|
</Box>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -47,6 +47,43 @@ export interface ThemeTypography {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface ThemeSpacing {
|
||||||
|
0: string;
|
||||||
|
0.5: string;
|
||||||
|
1: string;
|
||||||
|
1.5: string;
|
||||||
|
2: string;
|
||||||
|
2.5: string;
|
||||||
|
3: string;
|
||||||
|
3.5: string;
|
||||||
|
4: string;
|
||||||
|
5: string;
|
||||||
|
6: string;
|
||||||
|
7: string;
|
||||||
|
8: string;
|
||||||
|
9: string;
|
||||||
|
10: string;
|
||||||
|
11: string;
|
||||||
|
12: string;
|
||||||
|
14: string;
|
||||||
|
16: string;
|
||||||
|
20: string;
|
||||||
|
24: string;
|
||||||
|
28: string;
|
||||||
|
32: string;
|
||||||
|
36: string;
|
||||||
|
40: string;
|
||||||
|
44: string;
|
||||||
|
48: string;
|
||||||
|
52: string;
|
||||||
|
56: string;
|
||||||
|
60: string;
|
||||||
|
64: string;
|
||||||
|
72: string;
|
||||||
|
80: string;
|
||||||
|
96: string;
|
||||||
|
}
|
||||||
|
|
||||||
export interface Theme {
|
export interface Theme {
|
||||||
id: string;
|
id: string;
|
||||||
name: string;
|
name: string;
|
||||||
@@ -54,4 +91,5 @@ export interface Theme {
|
|||||||
radii: ThemeRadii;
|
radii: ThemeRadii;
|
||||||
shadows: ThemeShadows;
|
shadows: ThemeShadows;
|
||||||
typography: ThemeTypography;
|
typography: ThemeTypography;
|
||||||
|
spacing: ThemeSpacing;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -48,4 +48,40 @@ export const defaultTheme: Theme = {
|
|||||||
mono: "ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, 'Liberation Mono', 'Courier New', monospace",
|
mono: "ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, 'Liberation Mono', 'Courier New', monospace",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
spacing: {
|
||||||
|
0: '0',
|
||||||
|
0.5: '0.125rem',
|
||||||
|
1: '0.25rem',
|
||||||
|
1.5: '0.375rem',
|
||||||
|
2: '0.5rem',
|
||||||
|
2.5: '0.625rem',
|
||||||
|
3: '0.75rem',
|
||||||
|
3.5: '0.875rem',
|
||||||
|
4: '1rem',
|
||||||
|
5: '1.25rem',
|
||||||
|
6: '1.5rem',
|
||||||
|
7: '1.75rem',
|
||||||
|
8: '2rem',
|
||||||
|
9: '2.25rem',
|
||||||
|
10: '2.5rem',
|
||||||
|
11: '2.75rem',
|
||||||
|
12: '3rem',
|
||||||
|
14: '3.5rem',
|
||||||
|
16: '4rem',
|
||||||
|
20: '5rem',
|
||||||
|
24: '6rem',
|
||||||
|
28: '7rem',
|
||||||
|
32: '8rem',
|
||||||
|
36: '9rem',
|
||||||
|
40: '10rem',
|
||||||
|
44: '11rem',
|
||||||
|
48: '12rem',
|
||||||
|
52: '13rem',
|
||||||
|
56: '14rem',
|
||||||
|
60: '15rem',
|
||||||
|
64: '16rem',
|
||||||
|
72: '18rem',
|
||||||
|
80: '20rem',
|
||||||
|
96: '24rem',
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user