import { createContext, useCallback, useContext, useEffect, useState, type ReactNode } from "react"; import type { TeamAccentColor, TeamSummary } from "../../../../api-client"; import { Badge, Button, Checkbox, SectionHeading, TextField, type ButtonVariant } from "../../../../components"; import { colors } from "../../../../theme/colors"; import { Box, Span, Text, TextAttributes, type BoxRenderable } from "../../../../ui"; import { TEAM_ACCENT_COLORS, teamAccentHex, teamPrefix } from "./model"; /** * Every control in the pane registers under a field id. The pane keeps one * active id and walks the ring with Tab and the arrows; Enter fires the * registered action. Mouse users click; keyboard users reach every control. */ export interface TeamPaneFocus { activeField: string | null; setActiveField: (id: string) => void; /** True while the pane itself has focus; inputs only take keys then. */ focused: boolean; register: (id: string, action: (() => void) | null) => () => void; /** Where the control sits, so the pane can scroll it into view. */ registerNode: (id: string, node: BoxRenderable | null) => void; } export const TeamPaneFocusContext = createContext({ activeField: null, setActiveField: () => {}, focused: false, register: () => () => {}, registerNode: () => {}, }); export function useTeamPaneFocus(): TeamPaneFocus { return useContext(TeamPaneFocusContext); } function useFieldAction(id: string, action: (() => void) | null) { const { register } = useTeamPaneFocus(); useEffect(() => register(id, action), [action, id, register]); } function useFieldNode(id: string): (node: BoxRenderable | null) => void { const { registerNode } = useTeamPaneFocus(); return useCallback((node: BoxRenderable | null) => registerNode(id, node), [id, registerNode]); } /** A labeled single-line input, part of the keyboard ring. */ export function PaneField({ id, label, value, placeholder, width, labelWidth = 12, onChange, onSubmit, hint, hintWidth, }: { id: string; label: string; value: string; placeholder?: string; width: number; labelWidth?: number; onChange: (value: string) => void; onSubmit?: () => void; hint?: string; /** The hint may run wider than a short input; defaults to the field width. */ hintWidth?: number; }) { const focus = useTeamPaneFocus(); const active = focus.activeField === id; useFieldAction(id, null); const nodeRef = useFieldNode(id); const inputWidth = Math.max(8, width - labelWidth - 1); return ( focus.setActiveField(id)}> {`${active ? "> " : " "}${label}`} focus.setActiveField(id)} /> {hint ? ( {hint} ) : null} ); } /** A button in the ring: highlighted when active, fired by Enter or a click. */ export function PaneButton({ id, label, variant = "secondary", disabled = false, compact = false, onPress, width, }: { id: string; label: string; variant?: ButtonVariant; disabled?: boolean; compact?: boolean; onPress: () => void; width?: number; }) { const focus = useTeamPaneFocus(); const active = focus.activeField === id; useFieldAction(id, disabled ? null : onPress); const nodeRef = useFieldNode(id); return ( ); })} {value} {active ? {"← →"} : null} {`${teamPrefix(team)} ${team.name}`} ● {` ${team.shortName}`} ▎ {"Main"} ); } /** * Destructive actions confirm in place: the button turns into the question * and a pair of answers, so nothing pops over the pane. */ export function ConfirmAction({ id, label, question, confirmLabel, variant = "danger", busy = false, onConfirm, }: { id: string; label: string; question: string; confirmLabel: string; variant?: ButtonVariant; busy?: boolean; onConfirm: () => void; }) { const [armed, setArmed] = useState(false); const focus = useTeamPaneFocus(); const active = focus.activeField === id; useEffect(() => { if (!active) setArmed(false); }, [active]); if (!armed) { return setArmed(true)} />; } return ( {question} { onConfirm(); setArmed(false); }} />