689 lines
25 KiB
TypeScript
689 lines
25 KiB
TypeScript
'use client';
|
||
|
||
import type { LeagueConfigFormModel } from '@/lib/types/LeagueConfigFormModel';
|
||
import { Box } from '@/ui/Box';
|
||
import { Heading } from '@/ui/Heading';
|
||
import { Icon } from '@/ui/Icon';
|
||
import { Stack } from '@/ui/Stack';
|
||
import { Text } from '@/ui/Text';
|
||
import { Check, HelpCircle, TrendingDown, X, Zap } from 'lucide-react';
|
||
import React, { useEffect, useRef, useState } from 'react';
|
||
import { createPortal } from 'react-dom';
|
||
|
||
// ============================================================================
|
||
// INFO FLYOUT (duplicated for self-contained component)
|
||
// ============================================================================
|
||
|
||
interface InfoFlyoutProps {
|
||
isOpen: boolean;
|
||
onClose: () => void;
|
||
title: string;
|
||
children: React.ReactNode;
|
||
anchorRef: React.RefObject<HTMLElement>;
|
||
}
|
||
|
||
function InfoFlyout({ isOpen, onClose, title, children, anchorRef }: InfoFlyoutProps) {
|
||
const [position, setPosition] = useState({ top: 0, left: 0 });
|
||
const [mounted, setMounted] = useState(false);
|
||
const flyoutRef = useRef<HTMLDivElement>(null);
|
||
|
||
useEffect(() => {
|
||
setMounted(true);
|
||
}, []);
|
||
|
||
useEffect(() => {
|
||
if (isOpen && anchorRef.current && mounted) {
|
||
const rect = anchorRef.current.getBoundingClientRect();
|
||
const flyoutWidth = Math.min(380, window.innerWidth - 40);
|
||
const flyoutHeight = 450;
|
||
const padding = 16;
|
||
|
||
let left = rect.right + 12;
|
||
let top = rect.top;
|
||
|
||
if (left + flyoutWidth > window.innerWidth - padding) {
|
||
left = rect.left - flyoutWidth - 12;
|
||
}
|
||
if (left < padding) {
|
||
left = Math.max(padding, (window.innerWidth - flyoutWidth) / 2);
|
||
}
|
||
|
||
top = rect.top - flyoutHeight / 3;
|
||
if (top + flyoutHeight > window.innerHeight - padding) {
|
||
top = window.innerHeight - flyoutHeight - padding;
|
||
}
|
||
if (top < padding) top = padding;
|
||
|
||
left = Math.max(padding, Math.min(left, window.innerWidth - flyoutWidth - padding));
|
||
|
||
setPosition({ top, left });
|
||
}
|
||
}, [isOpen, anchorRef, mounted]);
|
||
|
||
useEffect(() => {
|
||
const handleEscape = (e: KeyboardEvent) => {
|
||
if (e.key === 'Escape') onClose();
|
||
};
|
||
const handleClickOutside = (e: MouseEvent) => {
|
||
if (flyoutRef.current && !flyoutRef.current.contains(e.target as Node)) {
|
||
onClose();
|
||
}
|
||
};
|
||
|
||
if (isOpen) {
|
||
document.addEventListener('keydown', handleEscape);
|
||
document.addEventListener('mousedown', handleClickOutside);
|
||
}
|
||
return () => {
|
||
document.removeEventListener('keydown', handleEscape);
|
||
document.removeEventListener('mousedown', handleClickOutside);
|
||
};
|
||
}, [isOpen, onClose]);
|
||
|
||
if (!isOpen) return null;
|
||
|
||
return createPortal(
|
||
<Box
|
||
ref={flyoutRef}
|
||
position="fixed"
|
||
zIndex={50}
|
||
w="380px"
|
||
bg="bg-iron-gray"
|
||
border
|
||
borderColor="border-charcoal-outline"
|
||
rounded="xl"
|
||
shadow="2xl"
|
||
// eslint-disable-next-line gridpilot-rules/component-classification
|
||
style={{ top: position.top, left: position.left, maxHeight: '80vh', overflowY: 'auto' }}
|
||
>
|
||
<Box
|
||
display="flex"
|
||
alignItems="center"
|
||
justifyContent="between"
|
||
p={4}
|
||
borderBottom
|
||
borderColor="border-charcoal-outline/50"
|
||
position="sticky"
|
||
top="0"
|
||
bg="bg-iron-gray"
|
||
zIndex={10}
|
||
>
|
||
<Stack direction="row" align="center" gap={2}>
|
||
<Icon icon={HelpCircle} size={4} color="text-primary-blue" />
|
||
<Text size="sm" weight="semibold" color="text-white">{title}</Text>
|
||
</Stack>
|
||
<Box
|
||
as="button"
|
||
type="button"
|
||
onClick={onClose}
|
||
display="flex"
|
||
h="6"
|
||
w="6"
|
||
alignItems="center"
|
||
justifyContent="center"
|
||
rounded="md"
|
||
transition
|
||
hoverBg="bg-charcoal-outline"
|
||
>
|
||
<Icon icon={X} size={4} color="text-gray-400" />
|
||
</Box>
|
||
</Box>
|
||
<Box p={4}>
|
||
{children}
|
||
</Box>
|
||
</Box>,
|
||
document.body
|
||
);
|
||
}
|
||
|
||
function InfoButton({ onClick, buttonRef }: { onClick: () => void; buttonRef: React.Ref<HTMLButtonElement> }) {
|
||
return (
|
||
<Box
|
||
as="button"
|
||
ref={buttonRef}
|
||
type="button"
|
||
onClick={onClick}
|
||
display="flex"
|
||
h="5"
|
||
w="5"
|
||
alignItems="center"
|
||
justifyContent="center"
|
||
rounded="full"
|
||
transition
|
||
color="text-gray-500"
|
||
hoverTextColor="text-primary-blue"
|
||
hoverBg="bg-primary-blue/10"
|
||
>
|
||
<Icon icon={HelpCircle} size={3.5} />
|
||
</Box>
|
||
);
|
||
}
|
||
|
||
// Drop Rules Mockup
|
||
function DropRulesMockup() {
|
||
const results = [
|
||
{ round: 'R1', pts: 25, dropped: false },
|
||
{ round: 'R2', pts: 18, dropped: false },
|
||
{ round: 'R3', pts: 4, dropped: true },
|
||
{ round: 'R4', pts: 15, dropped: false },
|
||
{ round: 'R5', pts: 12, dropped: false },
|
||
{ round: 'R6', pts: 0, dropped: true },
|
||
];
|
||
|
||
const total = results.filter(r => !r.dropped).reduce((sum, r) => sum + r.pts, 0);
|
||
const wouldBe = results.reduce((sum, r) => sum + r.pts, 0);
|
||
|
||
return (
|
||
<Box bg="bg-deep-graphite" rounded="lg" p={4}>
|
||
<Box display="flex" alignItems="center" gap={2} mb={3} pb={2} borderBottom borderColor="border-charcoal-outline/50" opacity={0.5}>
|
||
<Text size="xs" weight="semibold" color="text-white">Best 4 of 6 Results</Text>
|
||
</Box>
|
||
<Box display="flex" gap={1} mb={3}>
|
||
{results.map((r, i) => (
|
||
<Box
|
||
key={i}
|
||
flexGrow={1}
|
||
p={2}
|
||
rounded="lg"
|
||
textAlign="center"
|
||
border
|
||
transition
|
||
bg={r.dropped ? 'bg-charcoal-outline/20' : 'bg-performance-green/10'}
|
||
borderColor={r.dropped ? 'border-charcoal-outline/50' : 'border-performance-green/30'}
|
||
opacity={r.dropped ? 0.5 : 1}
|
||
// eslint-disable-next-line gridpilot-rules/component-classification
|
||
className={r.dropped ? 'border-dashed' : ''}
|
||
>
|
||
<Text
|
||
// eslint-disable-next-line gridpilot-rules/component-classification
|
||
style={{ fontSize: '9px' }}
|
||
color="text-gray-500"
|
||
block
|
||
>
|
||
{r.round}
|
||
</Text>
|
||
<Text font="mono" weight="semibold" size="xs" color={r.dropped ? 'text-gray-500' : 'text-white'}
|
||
// eslint-disable-next-line gridpilot-rules/component-classification
|
||
className={r.dropped ? 'line-through' : ''}
|
||
block
|
||
>
|
||
{r.pts}
|
||
</Text>
|
||
</Box>
|
||
))}
|
||
</Box>
|
||
<Box display="flex" justifyContent="between" alignItems="center">
|
||
<Text size="xs" color="text-gray-500">Total counted:</Text>
|
||
<Text font="mono" weight="semibold" color="text-performance-green" size="xs">{total} pts</Text>
|
||
</Box>
|
||
<Box display="flex" justifyContent="between" alignItems="center" mt={1}>
|
||
<Text
|
||
// eslint-disable-next-line gridpilot-rules/component-classification
|
||
style={{ fontSize: '10px' }}
|
||
color="text-gray-500"
|
||
>
|
||
Without drops:
|
||
</Text>
|
||
<Text font="mono"
|
||
// eslint-disable-next-line gridpilot-rules/component-classification
|
||
style={{ fontSize: '10px' }}
|
||
color="text-gray-500"
|
||
>
|
||
{wouldBe} pts
|
||
</Text>
|
||
</Box>
|
||
</Box>
|
||
);
|
||
}
|
||
|
||
interface LeagueDropSectionProps {
|
||
form: LeagueConfigFormModel;
|
||
onChange?: (form: LeagueConfigFormModel) => void;
|
||
readOnly?: boolean;
|
||
}
|
||
|
||
type DropStrategy = 'none' | 'bestNResults' | 'dropWorstN';
|
||
|
||
// Drop rule info content
|
||
const DROP_RULE_INFO: Record<DropStrategy, { title: string; description: string; details: string[]; example: string }> = {
|
||
none: {
|
||
title: 'All Results Count',
|
||
description: 'Every race result affects the championship standings with no exceptions.',
|
||
details: [
|
||
'All race results count toward final standings',
|
||
'No safety net for bad races or DNFs',
|
||
'Rewards consistency across entire season',
|
||
'Best for shorter seasons (4-6 races)',
|
||
],
|
||
example: '8 races × your points = your total',
|
||
},
|
||
bestNResults: {
|
||
title: 'Best N Results',
|
||
description: 'Only your top N race results count toward the championship.',
|
||
details: [
|
||
'Choose how many of your best races count',
|
||
'Extra races become "bonus" opportunities',
|
||
'Protects against occasional bad days',
|
||
'Encourages trying even when behind',
|
||
],
|
||
example: 'Best 6 of 8 races count',
|
||
},
|
||
dropWorstN: {
|
||
title: 'Drop Worst N Results',
|
||
description: 'Your N worst race results are excluded from championship calculations.',
|
||
details: [
|
||
'Automatically removes your worst performances',
|
||
'Great for handling DNFs or incidents',
|
||
'All other races count normally',
|
||
'Common in real-world championships',
|
||
],
|
||
example: 'Drop 2 worst → 6 of 8 count',
|
||
},
|
||
};
|
||
|
||
const DROP_OPTIONS: Array<{
|
||
value: DropStrategy;
|
||
label: string;
|
||
emoji: string;
|
||
description: string;
|
||
defaultN?: number;
|
||
}> = [
|
||
{
|
||
value: 'none',
|
||
label: 'All count',
|
||
emoji: '✓',
|
||
description: 'Every race counts',
|
||
},
|
||
{
|
||
value: 'bestNResults',
|
||
label: 'Best N',
|
||
emoji: '🏆',
|
||
description: 'Only best results',
|
||
defaultN: 6,
|
||
},
|
||
{
|
||
value: 'dropWorstN',
|
||
label: 'Drop worst',
|
||
emoji: '🗑️',
|
||
description: 'Exclude worst races',
|
||
defaultN: 2,
|
||
},
|
||
];
|
||
|
||
export function LeagueDropSection({
|
||
form,
|
||
onChange,
|
||
readOnly,
|
||
}: LeagueDropSectionProps) {
|
||
const disabled = readOnly || !onChange;
|
||
const dropPolicy = form.dropPolicy || { strategy: 'none' as const };
|
||
const [showDropFlyout, setShowDropFlyout] = useState(false);
|
||
const [activeDropRuleFlyout, setActiveDropRuleFlyout] = useState<DropStrategy | null>(null);
|
||
const dropInfoRef = useRef<HTMLButtonElement>(null!);
|
||
const dropRuleRefs = useRef<Record<DropStrategy, HTMLButtonElement | null>>({
|
||
none: null,
|
||
bestNResults: null,
|
||
dropWorstN: null,
|
||
});
|
||
|
||
const handleStrategyChange = (strategy: DropStrategy) => {
|
||
if (disabled || !onChange) return;
|
||
|
||
const option = DROP_OPTIONS.find((o) => o.value === strategy);
|
||
const next: LeagueConfigFormModel = {
|
||
...form,
|
||
dropPolicy:
|
||
strategy === 'none'
|
||
? {
|
||
strategy,
|
||
}
|
||
: {
|
||
strategy,
|
||
n: dropPolicy.n ?? option?.defaultN ?? 1,
|
||
},
|
||
};
|
||
onChange(next);
|
||
};
|
||
|
||
const handleNChange = (delta: number) => {
|
||
if (disabled || !onChange || dropPolicy.strategy === 'none') return;
|
||
const current = dropPolicy.n ?? 1;
|
||
const newValue = Math.max(1, current + delta);
|
||
onChange({
|
||
...form,
|
||
dropPolicy: {
|
||
...dropPolicy,
|
||
n: newValue,
|
||
},
|
||
});
|
||
};
|
||
|
||
const needsN = dropPolicy.strategy !== 'none';
|
||
|
||
return (
|
||
<Stack gap={4}>
|
||
{/* Section header */}
|
||
<Box display="flex" alignItems="center" gap={3}>
|
||
<Box display="flex" h="10" w="10" alignItems="center" justifyContent="center" rounded="xl" bg="bg-primary-blue/10">
|
||
<Icon icon={TrendingDown} size={5} color="text-primary-blue" />
|
||
</Box>
|
||
<Box flexGrow={1}>
|
||
<Box display="flex" alignItems="center" gap={2}>
|
||
<Heading level={3}>Drop Rules</Heading>
|
||
<InfoButton buttonRef={dropInfoRef} onClick={() => setShowDropFlyout(true)} />
|
||
</Box>
|
||
<Text size="xs" color="text-gray-500">Protect from bad races</Text>
|
||
</Box>
|
||
</Box>
|
||
|
||
{/* Drop Rules Flyout */}
|
||
<InfoFlyout
|
||
isOpen={showDropFlyout}
|
||
onClose={() => setShowDropFlyout(false)}
|
||
title="Drop Rules Explained"
|
||
anchorRef={dropInfoRef}
|
||
>
|
||
<Stack gap={4}>
|
||
<Text size="xs" color="text-gray-400" block>
|
||
Drop rules allow drivers to exclude their worst results from championship calculations.
|
||
This protects against mechanical failures, bad luck, or occasional poor performances.
|
||
</Text>
|
||
|
||
<Box>
|
||
<Text size="xs" weight="bold" color="text-gray-500" transform="uppercase"
|
||
// eslint-disable-next-line gridpilot-rules/component-classification
|
||
className="tracking-wide"
|
||
block
|
||
mb={2}
|
||
// eslint-disable-next-line gridpilot-rules/component-classification
|
||
style={{ fontSize: '10px' }}
|
||
>
|
||
Visual Example
|
||
</Text>
|
||
<DropRulesMockup />
|
||
</Box>
|
||
|
||
<Stack gap={2}>
|
||
<Text size="xs" weight="bold" color="text-gray-500" transform="uppercase"
|
||
// eslint-disable-next-line gridpilot-rules/component-classification
|
||
className="tracking-wide"
|
||
block
|
||
// eslint-disable-next-line gridpilot-rules/component-classification
|
||
style={{ fontSize: '10px' }}
|
||
>
|
||
Drop Strategies
|
||
</Text>
|
||
<Stack gap={2}>
|
||
<Box display="flex" alignItems="start" gap={2} p={2} rounded="lg" bg="bg-deep-graphite" border borderColor="border-charcoal-outline/30">
|
||
<Text size="base">✓</Text>
|
||
<Box>
|
||
<Text size="xs" weight="medium" color="text-white" block
|
||
// eslint-disable-next-line gridpilot-rules/component-classification
|
||
style={{ fontSize: '10px' }}
|
||
>
|
||
All Count
|
||
</Text>
|
||
<Text size="xs" color="text-gray-500" block
|
||
// eslint-disable-next-line gridpilot-rules/component-classification
|
||
style={{ fontSize: '9px' }}
|
||
>
|
||
Every race affects standings. Best for short seasons.
|
||
</Text>
|
||
</Box>
|
||
</Box>
|
||
<Box display="flex" alignItems="start" gap={2} p={2} rounded="lg" bg="bg-deep-graphite" border borderColor="border-charcoal-outline/30">
|
||
<Text size="base">🏆</Text>
|
||
<Box>
|
||
<Text size="xs" weight="medium" color="text-white" block
|
||
// eslint-disable-next-line gridpilot-rules/component-classification
|
||
style={{ fontSize: '10px' }}
|
||
>
|
||
Best N Results
|
||
</Text>
|
||
<Text size="xs" color="text-gray-500" block
|
||
// eslint-disable-next-line gridpilot-rules/component-classification
|
||
style={{ fontSize: '9px' }}
|
||
>
|
||
Only your top N races count. Extra races are optional.
|
||
</Text>
|
||
</Box>
|
||
</Box>
|
||
<Box display="flex" alignItems="start" gap={2} p={2} rounded="lg" bg="bg-deep-graphite" border borderColor="border-charcoal-outline/30">
|
||
<Text size="base">🗑️</Text>
|
||
<Box>
|
||
<Text size="xs" weight="medium" color="text-white" block
|
||
// eslint-disable-next-line gridpilot-rules/component-classification
|
||
style={{ fontSize: '10px' }}
|
||
>
|
||
Drop Worst N
|
||
</Text>
|
||
<Text size="xs" color="text-gray-500" block
|
||
// eslint-disable-next-line gridpilot-rules/component-classification
|
||
style={{ fontSize: '9px' }}
|
||
>
|
||
Exclude your N worst results. Forgives bad days.
|
||
</Text>
|
||
</Box>
|
||
</Box>
|
||
</Stack>
|
||
</Stack>
|
||
|
||
<Box rounded="lg" bg="bg-primary-blue/5" border borderColor="border-primary-blue/20" p={3}>
|
||
<Box display="flex" alignItems="start" gap={2}>
|
||
<Icon icon={Zap} size={3.5} color="text-primary-blue" flexShrink={0} mt={0.5} />
|
||
<Box>
|
||
<Text size="xs" color="text-gray-400"
|
||
// eslint-disable-next-line gridpilot-rules/component-classification
|
||
style={{ fontSize: '11px' }}
|
||
>
|
||
<Text weight="medium" color="text-primary-blue">Pro tip:</Text> For an 8-round season,
|
||
"Best 6" or "Drop 2" are popular choices.
|
||
</Text>
|
||
</Box>
|
||
</Box>
|
||
</Box>
|
||
</Stack>
|
||
</InfoFlyout>
|
||
|
||
{/* Strategy buttons + N stepper inline */}
|
||
<Box display="flex" flexWrap="wrap" alignItems="center" gap={2}>
|
||
{DROP_OPTIONS.map((option) => {
|
||
const isSelected = dropPolicy.strategy === option.value;
|
||
const ruleInfo = DROP_RULE_INFO[option.value];
|
||
return (
|
||
<Box key={option.value} display="flex" alignItems="center" position="relative">
|
||
<Box
|
||
as="button"
|
||
type="button"
|
||
disabled={disabled}
|
||
onClick={() => handleStrategyChange(option.value)}
|
||
display="flex"
|
||
alignItems="center"
|
||
gap={2}
|
||
px={3}
|
||
py={2}
|
||
rounded="lg"
|
||
border
|
||
borderWidth="2px"
|
||
transition
|
||
borderColor={isSelected ? 'border-primary-blue' : 'border-charcoal-outline/40'}
|
||
bg={isSelected ? 'bg-primary-blue/10' : 'bg-iron-gray/20'}
|
||
hoverBorderColor={!isSelected && !disabled ? 'border-charcoal-outline' : undefined}
|
||
hoverBg={!isSelected && !disabled ? 'bg-iron-gray/30' : undefined}
|
||
cursor={disabled ? 'default' : 'pointer'}
|
||
opacity={disabled ? 0.6 : 1}
|
||
// eslint-disable-next-line gridpilot-rules/component-classification
|
||
style={{ borderRightWidth: 0, borderTopRightRadius: 0, borderBottomRightRadius: 0 }}
|
||
>
|
||
{/* Radio indicator */}
|
||
<Box
|
||
display="flex"
|
||
h="4"
|
||
w="4"
|
||
alignItems="center"
|
||
justifyContent="center"
|
||
rounded="full"
|
||
border
|
||
borderColor={isSelected ? 'border-primary-blue' : 'border-gray-500'}
|
||
bg={isSelected ? 'bg-primary-blue' : ''}
|
||
flexShrink={0}
|
||
transition
|
||
>
|
||
{isSelected && <Icon icon={Check} size={2.5} color="text-white" />}
|
||
</Box>
|
||
|
||
<Text size="sm">{option.emoji}</Text>
|
||
<Text size="sm" weight="medium" color={isSelected ? 'text-white' : 'text-gray-400'}>
|
||
{option.label}
|
||
</Text>
|
||
</Box>
|
||
|
||
{/* Info button - separate from main button */}
|
||
<Box
|
||
as="button"
|
||
ref={(el: HTMLButtonElement | null) => { dropRuleRefs.current[option.value] = el; }}
|
||
type="button"
|
||
onClick={(e: React.MouseEvent) => {
|
||
e.stopPropagation();
|
||
setActiveDropRuleFlyout(activeDropRuleFlyout === option.value ? null : option.value);
|
||
}}
|
||
display="flex"
|
||
alignItems="center"
|
||
justifyContent="center"
|
||
px={2}
|
||
py={2}
|
||
rounded="lg"
|
||
border
|
||
borderWidth="2px"
|
||
transition
|
||
borderColor={isSelected ? 'border-primary-blue' : 'border-charcoal-outline/40'}
|
||
bg={isSelected ? 'bg-primary-blue/10' : 'bg-iron-gray/20'}
|
||
hoverBorderColor={!isSelected && !disabled ? 'border-charcoal-outline' : undefined}
|
||
hoverBg={!isSelected && !disabled ? 'bg-iron-gray/30' : undefined}
|
||
cursor={disabled ? 'default' : 'pointer'}
|
||
opacity={disabled ? 0.6 : 1}
|
||
// eslint-disable-next-line gridpilot-rules/component-classification
|
||
style={{ borderLeftWidth: 0, borderTopLeftRadius: 0, borderBottomLeftRadius: 0, height: '100%' }}
|
||
>
|
||
<Icon icon={HelpCircle} size={3.5} color="text-gray-500"
|
||
// eslint-disable-next-line gridpilot-rules/component-classification
|
||
className="hover:text-primary-blue transition-colors"
|
||
/>
|
||
</Box>
|
||
|
||
{/* Drop Rule Info Flyout */}
|
||
<InfoFlyout
|
||
isOpen={activeDropRuleFlyout === option.value}
|
||
onClose={() => setActiveDropRuleFlyout(null)}
|
||
title={ruleInfo.title}
|
||
anchorRef={{ current: (dropRuleRefs.current[option.value] as HTMLElement | null) ?? dropInfoRef.current }}
|
||
>
|
||
<Stack gap={4}>
|
||
<Text size="xs" color="text-gray-400" block>{ruleInfo.description}</Text>
|
||
|
||
<Stack gap={2}>
|
||
<Text size="xs" weight="bold" color="text-gray-500" transform="uppercase"
|
||
// eslint-disable-next-line gridpilot-rules/component-classification
|
||
className="tracking-wide"
|
||
block
|
||
mb={2}
|
||
// eslint-disable-next-line gridpilot-rules/component-classification
|
||
style={{ fontSize: '10px' }}
|
||
>
|
||
How It Works
|
||
</Text>
|
||
<Stack gap={1.5}>
|
||
{ruleInfo.details.map((detail, idx) => (
|
||
<Box key={idx} display="flex" alignItems="start" gap={2}>
|
||
<Icon icon={Check} size={3} color="text-performance-green" flexShrink={0} mt={0.5} />
|
||
<Text size="xs" color="text-gray-400">{detail}</Text>
|
||
</Box>
|
||
))}
|
||
</Stack>
|
||
</Stack>
|
||
|
||
<Box rounded="lg" bg="bg-deep-graphite" border borderColor="border-charcoal-outline/30" p={3}>
|
||
<Box display="flex" alignItems="center" gap={2}>
|
||
<Text size="base">{option.emoji}</Text>
|
||
<Box>
|
||
<Text size="xs" color="text-gray-400" block
|
||
// eslint-disable-next-line gridpilot-rules/component-classification
|
||
style={{ fontSize: '10px' }}
|
||
>
|
||
Example
|
||
</Text>
|
||
<Text size="xs" weight="medium" color="text-white" block>{ruleInfo.example}</Text>
|
||
</Box>
|
||
</Box>
|
||
</Box>
|
||
</Stack>
|
||
</InfoFlyout>
|
||
</Box>
|
||
);
|
||
})}
|
||
|
||
{/* N Stepper - only show when needed */}
|
||
{needsN && (
|
||
<Box display="flex" alignItems="center" gap={1} ml={2}>
|
||
<Text size="xs" color="text-gray-500" mr={1}>N =</Text>
|
||
<Box
|
||
as="button"
|
||
type="button"
|
||
disabled={disabled || (dropPolicy.n ?? 1) <= 1}
|
||
onClick={() => handleNChange(-1)}
|
||
display="flex"
|
||
h="7"
|
||
w="7"
|
||
alignItems="center"
|
||
justifyContent="center"
|
||
rounded="md"
|
||
bg="bg-iron-gray"
|
||
border
|
||
borderColor="border-charcoal-outline"
|
||
color="text-gray-400"
|
||
transition
|
||
hoverTextColor={!disabled ? 'text-white' : undefined}
|
||
hoverBorderColor={!disabled ? 'border-primary-blue' : undefined}
|
||
opacity={disabled || (dropPolicy.n ?? 1) <= 1 ? 0.4 : 1}
|
||
>
|
||
−
|
||
</Box>
|
||
<Box display="flex" h="7" w="10" alignItems="center" justifyContent="center" rounded="md" bg="bg-iron-gray/50" border borderColor="border-charcoal-outline/50">
|
||
<Text size="sm" weight="semibold" color="text-white">{dropPolicy.n ?? 1}</Text>
|
||
</Box>
|
||
<Box
|
||
as="button"
|
||
type="button"
|
||
disabled={disabled}
|
||
onClick={() => handleNChange(1)}
|
||
display="flex"
|
||
h="7"
|
||
w="7"
|
||
alignItems="center"
|
||
justifyContent="center"
|
||
rounded="md"
|
||
bg="bg-iron-gray"
|
||
border
|
||
borderColor="border-charcoal-outline"
|
||
color="text-gray-400"
|
||
transition
|
||
hoverTextColor={!disabled ? 'text-white' : undefined}
|
||
hoverBorderColor={!disabled ? 'border-primary-blue' : undefined}
|
||
opacity={disabled ? 0.4 : 1}
|
||
>
|
||
+
|
||
</Box>
|
||
</Box>
|
||
)}
|
||
</Box>
|
||
|
||
{/* Explanation text */}
|
||
<Text size="xs" color="text-gray-500" block>
|
||
{dropPolicy.strategy === 'none' && 'Every race result affects the championship standings.'}
|
||
{dropPolicy.strategy === 'bestNResults' && `Only your best ${dropPolicy.n ?? 1} results will count.`}
|
||
{dropPolicy.strategy === 'dropWorstN' && `Your worst ${dropPolicy.n ?? 1} results will be excluded.`}
|
||
</Text>
|
||
</Stack>
|
||
);
|
||
}
|