code quality
Some checks failed
CI / lint-typecheck (pull_request) Failing after 13s
CI / tests (pull_request) Has been skipped
CI / contract-tests (pull_request) Has been skipped
CI / e2e-tests (pull_request) Has been skipped
CI / comment-pr (pull_request) Has been skipped
CI / commit-types (pull_request) Has been skipped
Some checks failed
CI / lint-typecheck (pull_request) Failing after 13s
CI / tests (pull_request) Has been skipped
CI / contract-tests (pull_request) Has been skipped
CI / e2e-tests (pull_request) Has been skipped
CI / comment-pr (pull_request) Has been skipped
CI / commit-types (pull_request) Has been skipped
This commit is contained in:
@@ -64,7 +64,7 @@ function getEnvironment(): string {
|
||||
function validateEnvironment(
|
||||
env: string
|
||||
): env is keyof FeatureFlagConfig {
|
||||
const validEnvs = ['development', 'test', 'staging', 'production'];
|
||||
const validEnvs = ['development', 'test', 'e2e', 'staging', 'production'];
|
||||
if (!validEnvs.includes(env)) {
|
||||
throw new Error(
|
||||
`Invalid environment: "${env}". Valid environments: ${validEnvs.join(', ')}`
|
||||
|
||||
@@ -32,6 +32,7 @@ export interface EnvironmentConfig {
|
||||
export interface FeatureFlagConfig {
|
||||
development: EnvironmentConfig;
|
||||
test: EnvironmentConfig;
|
||||
e2e: EnvironmentConfig;
|
||||
staging: EnvironmentConfig;
|
||||
production: EnvironmentConfig;
|
||||
}
|
||||
|
||||
@@ -129,6 +129,43 @@ export const featureConfig: FeatureFlagConfig = {
|
||||
},
|
||||
},
|
||||
|
||||
// E2E environment - same as test
|
||||
e2e: {
|
||||
platform: {
|
||||
dashboard: 'enabled',
|
||||
leagues: 'enabled',
|
||||
teams: 'enabled',
|
||||
drivers: 'enabled',
|
||||
races: 'enabled',
|
||||
leaderboards: 'enabled',
|
||||
},
|
||||
auth: {
|
||||
signup: 'enabled',
|
||||
login: 'enabled',
|
||||
forgotPassword: 'enabled',
|
||||
resetPassword: 'enabled',
|
||||
},
|
||||
onboarding: {
|
||||
wizard: 'enabled',
|
||||
},
|
||||
sponsors: {
|
||||
portal: 'enabled',
|
||||
dashboard: 'enabled',
|
||||
management: 'enabled',
|
||||
campaigns: 'enabled',
|
||||
billing: 'enabled',
|
||||
},
|
||||
admin: {
|
||||
dashboard: 'enabled',
|
||||
userManagement: 'enabled',
|
||||
analytics: 'enabled',
|
||||
},
|
||||
beta: {
|
||||
newUI: 'disabled',
|
||||
experimental: 'disabled',
|
||||
},
|
||||
},
|
||||
|
||||
// Staging environment - controlled feature rollout
|
||||
staging: {
|
||||
// Core platform features
|
||||
|
||||
@@ -7,16 +7,17 @@ import React from 'react';
|
||||
interface AuthFormProps {
|
||||
children: React.ReactNode;
|
||||
onSubmit: (e: React.FormEvent<HTMLFormElement>) => void;
|
||||
'data-testid'?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* AuthForm
|
||||
*
|
||||
*
|
||||
* Semantic form wrapper for auth flows.
|
||||
*/
|
||||
export function AuthForm({ children, onSubmit }: AuthFormProps) {
|
||||
export function AuthForm({ children, onSubmit, 'data-testid': testId }: AuthFormProps) {
|
||||
return (
|
||||
<Form onSubmit={onSubmit}>
|
||||
<Form onSubmit={onSubmit} data-testid={testId}>
|
||||
<Group direction="column" gap={6}>
|
||||
{children}
|
||||
</Group>
|
||||
|
||||
@@ -8,25 +8,28 @@ interface KpiItem {
|
||||
|
||||
interface DashboardKpiRowProps {
|
||||
items: KpiItem[];
|
||||
'data-testid'?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* DashboardKpiRow
|
||||
*
|
||||
*
|
||||
* A horizontal row of key performance indicators with telemetry styling.
|
||||
*/
|
||||
export function DashboardKpiRow({ items }: DashboardKpiRowProps) {
|
||||
export function DashboardKpiRow({ items, 'data-testid': testId }: DashboardKpiRowProps) {
|
||||
return (
|
||||
<StatGrid
|
||||
variant="card"
|
||||
cardVariant="dark"
|
||||
font="mono"
|
||||
columns={{ base: 2, md: 3, lg: 6 }}
|
||||
stats={items.map(item => ({
|
||||
stats={items.map((item, index) => ({
|
||||
label: item.label,
|
||||
value: item.value,
|
||||
intent: item.intent as any
|
||||
intent: item.intent as any,
|
||||
'data-testid': `stat-${item.label.toLowerCase()}`
|
||||
}))}
|
||||
data-testid={testId}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -43,8 +43,8 @@ export function RecentActivityTable({ items }: RecentActivityTableProps) {
|
||||
</TableHead>
|
||||
<TableBody>
|
||||
{items.map((item) => (
|
||||
<TableRow key={item.id}>
|
||||
<TableCell>
|
||||
<TableRow key={item.id} data-testid={`activity-item-${item.id}`}>
|
||||
<TableCell data-testid="activity-race-result-link">
|
||||
<Text font="mono" variant="telemetry" size="xs">{item.type}</Text>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
|
||||
@@ -5,16 +5,17 @@ import React from 'react';
|
||||
interface TelemetryPanelProps {
|
||||
title: string;
|
||||
children: React.ReactNode;
|
||||
'data-testid'?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* TelemetryPanel
|
||||
*
|
||||
*
|
||||
* A dense, instrument-grade panel for displaying data and controls.
|
||||
*/
|
||||
export function TelemetryPanel({ title, children }: TelemetryPanelProps) {
|
||||
export function TelemetryPanel({ title, children, 'data-testid': testId }: TelemetryPanelProps) {
|
||||
return (
|
||||
<Panel title={title} variant="dark" padding={4}>
|
||||
<Panel title={title} variant="dark" padding={4} data-testid={testId}>
|
||||
<Text size="sm" variant="med">
|
||||
{children}
|
||||
</Text>
|
||||
|
||||
@@ -91,7 +91,7 @@ export function AvatarStep({ avatarInfo, setAvatarInfo, errors, setErrors, onGen
|
||||
};
|
||||
|
||||
return (
|
||||
<Stack gap={8}>
|
||||
<Stack gap={8} data-testid="avatar-creation-form">
|
||||
{/* Photo Upload */}
|
||||
<Stack>
|
||||
<Text as="label" size="sm" weight="medium" color="text-gray-300" block mb={3}>
|
||||
@@ -100,6 +100,7 @@ export function AvatarStep({ avatarInfo, setAvatarInfo, errors, setErrors, onGen
|
||||
<Stack direction="row" gap={6}>
|
||||
{/* Upload Area */}
|
||||
<Stack
|
||||
data-testid="photo-upload-area"
|
||||
onClick={() => fileInputRef.current?.click()}
|
||||
flex={1}
|
||||
display="flex"
|
||||
@@ -126,6 +127,7 @@ export function AvatarStep({ avatarInfo, setAvatarInfo, errors, setErrors, onGen
|
||||
accept="image/*"
|
||||
onChange={handleFileSelect}
|
||||
display="none"
|
||||
data-testid="photo-upload-input"
|
||||
/>
|
||||
|
||||
{avatarInfo.isValidating ? (
|
||||
@@ -144,6 +146,7 @@ export function AvatarStep({ avatarInfo, setAvatarInfo, errors, setErrors, onGen
|
||||
objectFit="cover"
|
||||
fullWidth
|
||||
fullHeight
|
||||
data-testid="photo-preview"
|
||||
/>
|
||||
</Stack>
|
||||
<Text size="sm" color="text-performance-green" block>
|
||||
@@ -199,11 +202,12 @@ export function AvatarStep({ avatarInfo, setAvatarInfo, errors, setErrors, onGen
|
||||
Racing Suit Color
|
||||
</Stack>
|
||||
</Text>
|
||||
<Stack flexDirection="row" flexWrap="wrap" gap={2}>
|
||||
<Stack flexDirection="row" flexWrap="wrap" gap={2} data-testid="suit-color-options">
|
||||
{SUIT_COLORS.map((color) => (
|
||||
<Button
|
||||
key={color.value}
|
||||
type="button"
|
||||
data-testid={`suit-color-${color.value}`}
|
||||
onClick={() => setAvatarInfo({ ...avatarInfo, suitColor: color.value })}
|
||||
rounded="lg"
|
||||
transition
|
||||
@@ -235,6 +239,7 @@ export function AvatarStep({ avatarInfo, setAvatarInfo, errors, setErrors, onGen
|
||||
<Stack>
|
||||
<Button
|
||||
type="button"
|
||||
data-testid="generate-avatars-btn"
|
||||
variant="primary"
|
||||
onClick={onGenerateAvatars}
|
||||
disabled={avatarInfo.isGenerating || avatarInfo.isValidating}
|
||||
|
||||
@@ -49,6 +49,7 @@ export function OnboardingPrimaryActions({
|
||||
onClick={onNext}
|
||||
disabled={isLoading || !canNext}
|
||||
w="40"
|
||||
data-testid={isLastStep ? 'complete-onboarding-btn' : 'next-btn'}
|
||||
>
|
||||
<Stack direction="row" align="center" gap={2}>
|
||||
{isLoading ? 'Processing...' : isLastStep ? 'Complete Setup' : nextLabel}
|
||||
|
||||
@@ -17,7 +17,7 @@ interface OnboardingShellProps {
|
||||
*/
|
||||
export function OnboardingShell({ children, header, footer, sidebar }: OnboardingShellProps) {
|
||||
return (
|
||||
<Box minHeight="100vh" bg="rgba(10,10,10,1)" color="white">
|
||||
<Box minHeight="100vh" bg="rgba(10,10,10,1)" color="white" data-testid="onboarding-wizard">
|
||||
{header && (
|
||||
<Box borderBottom borderColor="rgba(255,255,255,0.1)" py={4} bg="rgba(20,22,25,1)">
|
||||
<Container size="md">
|
||||
|
||||
@@ -15,8 +15,9 @@ interface OnboardingStepPanelProps {
|
||||
* Provides a consistent header and surface.
|
||||
*/
|
||||
export function OnboardingStepPanel({ title, description, children }: OnboardingStepPanelProps) {
|
||||
const testId = title.toLowerCase().includes('personal') ? 'step-1-personal-info' : 'step-2-avatar';
|
||||
return (
|
||||
<Stack gap={6}>
|
||||
<Stack gap={6} data-testid={testId}>
|
||||
<Stack gap={1}>
|
||||
<Text as="h2" size="2xl" weight="bold" color="text-white" letterSpacing="tight">
|
||||
{title}
|
||||
|
||||
@@ -49,6 +49,7 @@ export function PersonalInfoStep({ personalInfo, setPersonalInfo, errors, loadin
|
||||
</Text>
|
||||
<Input
|
||||
id="firstName"
|
||||
data-testid="first-name-input"
|
||||
type="text"
|
||||
value={personalInfo.firstName}
|
||||
onChange={(e: React.ChangeEvent<HTMLInputElement>) =>
|
||||
@@ -67,6 +68,7 @@ export function PersonalInfoStep({ personalInfo, setPersonalInfo, errors, loadin
|
||||
</Text>
|
||||
<Input
|
||||
id="lastName"
|
||||
data-testid="last-name-input"
|
||||
type="text"
|
||||
value={personalInfo.lastName}
|
||||
onChange={(e: React.ChangeEvent<HTMLInputElement>) =>
|
||||
@@ -86,6 +88,7 @@ export function PersonalInfoStep({ personalInfo, setPersonalInfo, errors, loadin
|
||||
</Text>
|
||||
<Input
|
||||
id="displayName"
|
||||
data-testid="display-name-input"
|
||||
type="text"
|
||||
value={personalInfo.displayName}
|
||||
onChange={(e: React.ChangeEvent<HTMLInputElement>) =>
|
||||
@@ -104,6 +107,7 @@ export function PersonalInfoStep({ personalInfo, setPersonalInfo, errors, loadin
|
||||
Country *
|
||||
</Text>
|
||||
<CountrySelect
|
||||
data-testid="country-select"
|
||||
value={personalInfo.country}
|
||||
onChange={(value: string) =>
|
||||
setPersonalInfo({ ...personalInfo, country: value })
|
||||
|
||||
@@ -57,34 +57,34 @@ export function DashboardTemplate({
|
||||
return (
|
||||
<Stack gap={6}>
|
||||
{/* KPI Overview */}
|
||||
<DashboardKpiRow items={kpiItems} />
|
||||
<DashboardKpiRow items={kpiItems} data-testid="dashboard-stats" />
|
||||
|
||||
<Grid responsiveGridCols={{ base: 1, lg: 12 }} gap={6}>
|
||||
{/* Main Content Column */}
|
||||
<Box responsiveColSpan={{ base: 1, lg: 8 }}>
|
||||
<Stack direction="col" gap={6}>
|
||||
{nextRace && (
|
||||
<TelemetryPanel title="Active Session">
|
||||
<TelemetryPanel title="Active Session" data-testid="next-race-section">
|
||||
<Box display="flex" alignItems="center" justifyContent="between">
|
||||
<Box>
|
||||
<Text size="xs" variant="low" mb={1} block>Next Event</Text>
|
||||
<Text size="lg" weight="bold" block>{nextRace.track}</Text>
|
||||
<Text size="xs" variant="primary" font="mono" block>{nextRace.car}</Text>
|
||||
<Text size="lg" weight="bold" block data-testid="next-race-track">{nextRace.track}</Text>
|
||||
<Text size="xs" variant="primary" font="mono" block data-testid="next-race-car">{nextRace.car}</Text>
|
||||
</Box>
|
||||
<Box textAlign="right">
|
||||
<Text size="xs" variant="low" mb={1} block>Starts In</Text>
|
||||
<Text size="xl" font="mono" weight="bold" variant="warning" block>{nextRace.timeUntil}</Text>
|
||||
<Text size="xs" variant="low" mb={1} block data-testid="next-race-time">{nextRace.formattedDate} @ {nextRace.formattedTime}</Text>
|
||||
<Text size="xl" font="mono" weight="bold" variant="warning" block data-testid="next-race-countdown">{nextRace.timeUntil}</Text>
|
||||
<Text size="xs" variant="low" block>{nextRace.formattedDate} @ {nextRace.formattedTime}</Text>
|
||||
</Box>
|
||||
</Box>
|
||||
</TelemetryPanel>
|
||||
)}
|
||||
|
||||
<TelemetryPanel title="Recent Activity">
|
||||
<TelemetryPanel title="Recent Activity" data-testid="activity-feed-section">
|
||||
{hasFeedItems ? (
|
||||
<RecentActivityTable items={activityItems} />
|
||||
) : (
|
||||
<Box py={8} textAlign="center">
|
||||
<Box py={8} textAlign="center" data-testid="activity-empty">
|
||||
<Text italic variant="low">No recent activity recorded.</Text>
|
||||
</Box>
|
||||
)}
|
||||
@@ -95,12 +95,22 @@ export function DashboardTemplate({
|
||||
{/* Sidebar Column */}
|
||||
<Box responsiveColSpan={{ base: 1, lg: 4 }}>
|
||||
<Stack direction="col" gap={6}>
|
||||
<TelemetryPanel title="Championship Standings">
|
||||
<TelemetryPanel title="Championship Standings" data-testid="championship-standings-section">
|
||||
{hasLeagueStandings ? (
|
||||
<Stack direction="col" gap={3}>
|
||||
{leagueStandings.map((standing) => (
|
||||
<Box key={standing.leagueId} display="flex" alignItems="center" justifyContent="between" borderBottom borderColor="var(--ui-color-border-muted)" pb={2}>
|
||||
<Box>
|
||||
<Box
|
||||
key={standing.leagueId}
|
||||
display="flex"
|
||||
alignItems="center"
|
||||
justifyContent="between"
|
||||
borderBottom
|
||||
borderColor="var(--ui-color-border-muted)"
|
||||
pb={2}
|
||||
data-testid={`league-standing-${standing.leagueId}`}
|
||||
cursor="pointer"
|
||||
>
|
||||
<Box data-testid="league-standing-link">
|
||||
<Text size="xs" weight="bold" truncate block maxWidth="180px">{standing.leagueName}</Text>
|
||||
<Text size="xs" variant="low" block>Pos: {standing.position} / {standing.totalDrivers}</Text>
|
||||
</Box>
|
||||
@@ -109,30 +119,37 @@ export function DashboardTemplate({
|
||||
))}
|
||||
</Stack>
|
||||
) : (
|
||||
<Box py={4} textAlign="center">
|
||||
<Box py={4} textAlign="center" data-testid="standings-empty">
|
||||
<Text italic variant="low">No active championships.</Text>
|
||||
</Box>
|
||||
)}
|
||||
</TelemetryPanel>
|
||||
|
||||
<TelemetryPanel title="Upcoming Schedule">
|
||||
<TelemetryPanel title="Upcoming Schedule" data-testid="upcoming-races-section">
|
||||
<Stack direction="col" gap={4}>
|
||||
{upcomingRaces.slice(0, 3).map((race) => (
|
||||
<Box key={race.id} cursor="pointer">
|
||||
<Box display="flex" justifyContent="between" alignItems="start" mb={1}>
|
||||
<Text size="xs" weight="bold">{race.track}</Text>
|
||||
<Text size="xs" font="mono" variant="low">{race.timeUntil}</Text>
|
||||
</Box>
|
||||
<Box display="flex" justifyContent="between">
|
||||
<Text size="xs" variant="low">{race.car}</Text>
|
||||
<Text size="xs" variant="low">{race.formattedDate}</Text>
|
||||
{upcomingRaces.length > 0 ? (
|
||||
upcomingRaces.slice(0, 3).map((race) => (
|
||||
<Box key={race.id} cursor="pointer" data-testid={`upcoming-race-${race.id}`}>
|
||||
<Box display="flex" justifyContent="between" alignItems="start" mb={1} data-testid="upcoming-race-link">
|
||||
<Text size="xs" weight="bold">{race.track}</Text>
|
||||
<Text size="xs" font="mono" variant="low">{race.timeUntil}</Text>
|
||||
</Box>
|
||||
<Box display="flex" justifyContent="between">
|
||||
<Text size="xs" variant="low">{race.car}</Text>
|
||||
<Text size="xs" variant="low">{race.formattedDate}</Text>
|
||||
</Box>
|
||||
</Box>
|
||||
))
|
||||
) : (
|
||||
<Box py={4} textAlign="center" data-testid="upcoming-races-empty">
|
||||
<Text italic variant="low">No upcoming races.</Text>
|
||||
</Box>
|
||||
))}
|
||||
<Button
|
||||
variant="secondary"
|
||||
fullWidth
|
||||
)}
|
||||
<Button
|
||||
variant="secondary"
|
||||
fullWidth
|
||||
onClick={onNavigateToRaces}
|
||||
data-testid="view-full-schedule-link"
|
||||
>
|
||||
<Text size="xs" weight="bold" uppercase letterSpacing="widest">View Full Schedule</Text>
|
||||
</Button>
|
||||
|
||||
@@ -45,8 +45,8 @@ export function RacesIndexTemplate({
|
||||
const hasRaces = viewData.racesByDate.length > 0;
|
||||
|
||||
return (
|
||||
<Section variant="default" padding="md">
|
||||
<PageHeader
|
||||
<Section variant="default" padding="md" data-testid="races-list">
|
||||
<PageHeader
|
||||
title="Races"
|
||||
description="Live Sessions & Upcoming Events"
|
||||
icon={Flag}
|
||||
|
||||
@@ -42,7 +42,7 @@ export function LoginTemplate({ viewData, formActions, mutationState }: LoginTem
|
||||
title="Welcome Back"
|
||||
description="Sign in to access your racing dashboard"
|
||||
>
|
||||
<AuthForm onSubmit={formActions.handleSubmit}>
|
||||
<AuthForm onSubmit={formActions.handleSubmit} data-testid="login-form">
|
||||
<Group direction="column" gap={4} fullWidth>
|
||||
<Input
|
||||
label="Email Address"
|
||||
@@ -56,6 +56,7 @@ export function LoginTemplate({ viewData, formActions, mutationState }: LoginTem
|
||||
disabled={isSubmitting}
|
||||
autoComplete="email"
|
||||
icon={<Mail size={16} />}
|
||||
data-testid="email-input"
|
||||
/>
|
||||
|
||||
<Group direction="column" gap={1.5} fullWidth>
|
||||
@@ -71,6 +72,7 @@ export function LoginTemplate({ viewData, formActions, mutationState }: LoginTem
|
||||
autoComplete="current-password"
|
||||
showPassword={viewData.showPassword}
|
||||
onTogglePassword={() => formActions.setShowPassword(!viewData.showPassword)}
|
||||
data-testid="password-input"
|
||||
/>
|
||||
<Group justify="end" fullWidth>
|
||||
<Link href={routes.auth.forgotPassword}>
|
||||
@@ -127,6 +129,7 @@ export function LoginTemplate({ viewData, formActions, mutationState }: LoginTem
|
||||
disabled={isSubmitting}
|
||||
fullWidth
|
||||
icon={isSubmitting ? <LoadingSpinner size={4} /> : <LogIn size={16} />}
|
||||
data-testid="login-submit"
|
||||
>
|
||||
{isSubmitting ? 'Signing in...' : 'Sign In'}
|
||||
</Button>
|
||||
|
||||
@@ -91,8 +91,9 @@ export const Button = forwardRef<HTMLButtonElement | HTMLAnchorElement, ButtonPr
|
||||
borderWidth,
|
||||
aspectRatio,
|
||||
border,
|
||||
...props
|
||||
}, ref) => {
|
||||
const baseClasses = 'inline-flex items-center justify-center focus-visible:outline focus-visible:outline-1 focus-visible:outline-offset-2 active:opacity-80 uppercase tracking-widest font-bold transition-all duration-150 ease-in-out';
|
||||
const baseClasses = 'inline-flex items-center justify-center focus-visible:outline focus-visible:outline-1 focus-visible:outline-offset-2 active:opacity-80 uppercase tracking-widest font-bold transition-all duration-150 ease-in-out';
|
||||
|
||||
const variantClasses = {
|
||||
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.2)]',
|
||||
@@ -154,6 +155,8 @@ export const Button = forwardRef<HTMLButtonElement | HTMLAnchorElement, ButtonPr
|
||||
|
||||
const Tag = as === 'a' ? 'a' : 'button';
|
||||
|
||||
const { 'data-testid': testId } = (props as any) || {};
|
||||
|
||||
return (
|
||||
<Tag
|
||||
ref={ref as any}
|
||||
@@ -166,6 +169,7 @@ export const Button = forwardRef<HTMLButtonElement | HTMLAnchorElement, ButtonPr
|
||||
disabled={as === 'a' ? undefined : (disabled || isLoading)}
|
||||
style={combinedStyle}
|
||||
title={title}
|
||||
data-testid={testId}
|
||||
>
|
||||
{content}
|
||||
</Tag>
|
||||
|
||||
@@ -93,6 +93,7 @@ export const Card = forwardRef<HTMLDivElement, CardProps>(({
|
||||
gap,
|
||||
borderLeft,
|
||||
justifyContent,
|
||||
...props
|
||||
}, ref) => {
|
||||
const variantClasses = {
|
||||
default: 'bg-[var(--ui-color-bg-surface)] border-[var(--ui-color-border-default)] shadow-sm',
|
||||
@@ -152,11 +153,12 @@ export const Card = forwardRef<HTMLDivElement, CardProps>(({
|
||||
};
|
||||
|
||||
return (
|
||||
<div
|
||||
<div
|
||||
ref={ref}
|
||||
className={classes}
|
||||
onClick={onClick}
|
||||
style={Object.keys(style).length > 0 ? style : undefined}
|
||||
{...props}
|
||||
>
|
||||
{title && (
|
||||
<div className={`border-b border-[var(--ui-color-border-muted)] ${typeof padding === 'string' ? (paddingClasses[padding] || paddingClasses.md) : ''}`}>
|
||||
|
||||
@@ -5,21 +5,24 @@ export interface FormProps {
|
||||
onSubmit?: FormEventHandler<HTMLFormElement>;
|
||||
noValidate?: boolean;
|
||||
className?: string;
|
||||
'data-testid'?: string;
|
||||
}
|
||||
|
||||
export const Form = forwardRef<HTMLFormElement, FormProps>(({
|
||||
children,
|
||||
onSubmit,
|
||||
export const Form = forwardRef<HTMLFormElement, FormProps>(({
|
||||
children,
|
||||
onSubmit,
|
||||
noValidate = true,
|
||||
className
|
||||
className,
|
||||
'data-testid': testId
|
||||
}, ref) => {
|
||||
return (
|
||||
<form
|
||||
<form
|
||||
ref={ref}
|
||||
onSubmit={onSubmit}
|
||||
onSubmit={onSubmit}
|
||||
noValidate={noValidate}
|
||||
className={className}
|
||||
style={{ width: '100%' }}
|
||||
data-testid={testId}
|
||||
>
|
||||
{children}
|
||||
</form>
|
||||
|
||||
@@ -28,8 +28,9 @@ export const Input = forwardRef<HTMLInputElement, InputProps>(({
|
||||
hint,
|
||||
id,
|
||||
size,
|
||||
...props
|
||||
...props
|
||||
}, ref) => {
|
||||
const { 'data-testid': testId, ...restProps } = props as any;
|
||||
const variantClasses = {
|
||||
default: 'bg-surface-charcoal border border-outline-steel focus:border-primary-accent',
|
||||
ghost: 'bg-transparent border-none',
|
||||
@@ -80,7 +81,8 @@ export const Input = forwardRef<HTMLInputElement, InputProps>(({
|
||||
ref={ref}
|
||||
id={inputId}
|
||||
className="bg-transparent border-none outline-none text-sm w-full text-text-high placeholder:text-text-low/50 h-full"
|
||||
{...props}
|
||||
data-testid={testId}
|
||||
{...restProps}
|
||||
/>
|
||||
|
||||
{rightElement}
|
||||
|
||||
@@ -18,9 +18,9 @@ export interface PanelProps {
|
||||
bg?: string;
|
||||
}
|
||||
|
||||
export function Panel({
|
||||
children,
|
||||
variant = 'default',
|
||||
export function Panel({
|
||||
children,
|
||||
variant = 'default',
|
||||
padding = 'md',
|
||||
onClick,
|
||||
style,
|
||||
@@ -30,8 +30,9 @@ export function Panel({
|
||||
footer,
|
||||
border,
|
||||
rounded,
|
||||
className
|
||||
}: PanelProps) {
|
||||
className,
|
||||
...props
|
||||
}: PanelProps & { [key: string]: any }) {
|
||||
const variantClasses = {
|
||||
default: 'bg-[var(--ui-color-bg-surface)] border border-[var(--ui-color-border-default)] shadow-sm',
|
||||
muted: 'bg-[var(--ui-color-bg-surface-muted)] border border-[var(--ui-color-border-muted)]',
|
||||
@@ -61,13 +62,14 @@ export function Panel({
|
||||
: '';
|
||||
|
||||
return (
|
||||
<div
|
||||
<div
|
||||
className={`${variantClasses[variant]} ${getPaddingClass(padding)} ${interactiveClasses} ${rounded ? `rounded-${rounded}` : 'rounded-md'} ${border ? 'border' : ''} ${className || ''}`}
|
||||
onClick={onClick}
|
||||
style={{
|
||||
...style,
|
||||
...(typeof padding === 'number' ? { padding: `${padding * 0.25}rem` } : {})
|
||||
}}
|
||||
{...props}
|
||||
>
|
||||
{(title || actions) && (
|
||||
<div className="flex items-center justify-between mb-6 border-b border-[var(--ui-color-border-muted)] pb-4">
|
||||
|
||||
@@ -22,10 +22,10 @@ export interface StatCardProps {
|
||||
delay?: number;
|
||||
}
|
||||
|
||||
export const StatCard = ({
|
||||
label,
|
||||
value,
|
||||
icon,
|
||||
export const StatCard = ({
|
||||
label,
|
||||
value,
|
||||
icon,
|
||||
intent: intentProp,
|
||||
variant = 'default',
|
||||
font = 'sans',
|
||||
@@ -33,8 +33,9 @@ export const StatCard = ({
|
||||
footer,
|
||||
suffix,
|
||||
prefix,
|
||||
delay
|
||||
}: StatCardProps) => {
|
||||
delay,
|
||||
...props
|
||||
}: StatCardProps & { [key: string]: any }) => {
|
||||
const variantMap: Record<string, { variant: any, intent: any }> = {
|
||||
blue: { variant: 'default', intent: 'primary' },
|
||||
green: { variant: 'default', intent: 'success' },
|
||||
@@ -46,7 +47,7 @@ export const StatCard = ({
|
||||
const finalIntent = mapped.intent;
|
||||
|
||||
return (
|
||||
<Card variant={finalVariant}>
|
||||
<Card variant={finalVariant} {...props}>
|
||||
<Box display="flex" alignItems="start" justifyContent="between" marginBottom={4}>
|
||||
<Box>
|
||||
<Text size="xs" weight="bold" variant="low" uppercase>
|
||||
|
||||
@@ -11,24 +11,25 @@ export interface StatGridProps {
|
||||
font?: 'sans' | 'mono';
|
||||
}
|
||||
|
||||
export const StatGrid = ({
|
||||
stats,
|
||||
export const StatGrid = ({
|
||||
stats,
|
||||
columns = 3,
|
||||
variant = 'box',
|
||||
cardVariant,
|
||||
font
|
||||
}: StatGridProps) => {
|
||||
font,
|
||||
...props
|
||||
}: StatGridProps & { [key: string]: any }) => {
|
||||
return (
|
||||
<Grid cols={columns} gap={4}>
|
||||
<Grid cols={columns} gap={4} {...props}>
|
||||
{stats.map((stat, index) => (
|
||||
variant === 'box' ? (
|
||||
<StatBox key={index} {...(stat as StatBoxProps)} />
|
||||
) : (
|
||||
<StatCard
|
||||
key={index}
|
||||
variant={cardVariant}
|
||||
font={font}
|
||||
{...(stat as StatCardProps)}
|
||||
<StatCard
|
||||
key={index}
|
||||
variant={cardVariant}
|
||||
font={font}
|
||||
{...(stat as StatCardProps)}
|
||||
/>
|
||||
)
|
||||
))}
|
||||
|
||||
Reference in New Issue
Block a user