import React from 'react'; import { Button } from '@/components/ui/button'; import { Checkbox } from '@/components/ui/checkbox'; import { Radio } from '@/components/ui/radio'; import { dropdownTriggerVariants } from '@/components/ui/dropdown-trigger'; import { cn } from '@/lib/utils'; import { SettingsInfoHint } from './SettingsInfoHint'; /** Settings select trigger: full column width in stacked cells; capped in field rows via parent. */ export const SETTINGS_SELECT_TRIGGER_CLASS = 'w-full min-w-40 max-w-48'; export const SETTINGS_SELECT_SIZE = 'settings' as const; /** Fixed-width select used inside full-width SettingsFieldRow control columns. */ export const SETTINGS_SELECT_ROW_TRIGGER_CLASS = 'w-full min-w-40 max-w-48'; /** Compact reset / icon action next to a settings control (matches h-8 controls). */ export const SETTINGS_ICON_BUTTON_CLASS = 'h-8 w-8 px-0 text-muted-foreground hover:text-foreground'; /** Custom dropdown triggers (ModelSelector / AgentSelector) in settings field rows. */ // eslint-disable-next-line react-refresh/only-export-components export const SETTINGS_CUSTOM_TRIGGER_CLASS = cn( dropdownTriggerVariants(), 'w-full min-w-40 max-w-48', ); /** Shared width for stacked control clusters (select/input + reset). */ export const SETTINGS_CONTROL_CLUSTER_CLASS = 'w-full max-w-[24rem]'; /** Fill a control cluster the same way as a full-width select. */ export const SETTINGS_CLUSTER_CONTROL_CLASS = 'min-w-0 flex-1'; /** * Row wrapping a NumberInput + unit label + reset. * Keep the stepper intrinsic — never flex-grow it or +/- buttons stretch unevenly. */ export const SETTINGS_NUMBER_STEPPER_ROW_CLASS = 'flex w-full min-w-0 items-center gap-2'; /** Unit suffix next to a settings number stepper (%, px, …). */ export const SETTINGS_NUMBER_UNIT_CLASS = 'typography-meta shrink-0 text-muted-foreground tabular-nums'; /** Vertical stack spacing for fields inside a column. */ export const SETTINGS_FIELDS_STACK_CLASS = 'space-y-4'; /** Compact checkbox / radio list stack. */ export const SETTINGS_OPTION_STACK_CLASS = 'space-y-1.5'; /** * Settings heading classes by context (size + default color). * Prefer these over ad-hoc typography-* + color combinations. */ /** L1 — page / detail-pane title (larger, quieter than section titles). */ export const SETTINGS_PAGE_TITLE_CLASS = 'typography-settings-page-title text-muted-foreground'; /** L2 — section title inside a settings page. */ export const SETTINGS_SECTION_TITLE_CLASS = 'typography-settings-section-title text-foreground'; /** Split-pane sidebar panel title — same level as section titles. */ export const SETTINGS_PANEL_TITLE_CLASS = SETTINGS_SECTION_TITLE_CLASS; /** L3 — control-group heading inside a section. */ const SETTINGS_GROUP_TITLE_CLASS = 'typography-settings-group-title text-foreground'; /** L4 — field / control labels. */ export const SETTINGS_FIELD_LABEL_CLASS = 'typography-settings-field-label text-foreground'; /** Supporting copy under page or section titles. */ export const SETTINGS_DESCRIPTION_CLASS = 'typography-settings-description text-muted-foreground'; /** Supporting copy under group titles and fields. */ export const SETTINGS_HELPER_CLASS = 'typography-meta text-muted-foreground'; /** Callout / alert headline inside a section (not a control-group title). */ export const SETTINGS_CALLOUT_TITLE_CLASS = 'typography-meta font-medium text-foreground'; /** Brand / product name under a logo — quieter than L1 page title. */ export const SETTINGS_BRAND_TITLE_CLASS = 'typography-settings-section-title text-foreground'; interface SettingsSectionProps { /** Section title. Strings render as the shared h2 style. */ title?: React.ReactNode; /** Optional supporting text under the title. */ description?: React.ReactNode; /** Optional icon/badge next to the title. */ titleAccessory?: React.ReactNode; /** Helper text hidden behind an info icon next to the title. */ info?: React.ReactNode; /** Optional action aligned to the right of the header. */ headerAction?: React.ReactNode; children: React.ReactNode; /** * Show a top border divider. * Use `false` for the first section under the page header. * @default true */ divider?: boolean; className?: string; contentClassName?: string; settingsItem?: string; } /** * Shared settings section chrome: single-style header + optional divider. */ export const SettingsSection: React.FC = ({ title, description, titleAccessory, info, headerAction, children, divider = true, className, contentClassName, settingsItem, }) => { const hasHeader = title != null || description != null || headerAction != null || info != null; return (
{hasHeader ? (
{title != null ? (
{typeof title === 'string' || typeof title === 'number' ? (

{title}

) : ( title )} {titleAccessory} {info != null ? {info} : null}
) : null} {description != null ? (
{description}
) : null}
{headerAction ?
{headerAction}
: null}
) : null}
{children}
); }; interface SettingsTwoColumnProps { children: React.ReactNode; className?: string; } /** Responsive two-column settings grid used when space allows. */ export const SettingsTwoColumn: React.FC = ({ children, className, }) => { return (
{children}
); }; interface SettingsGroupTitleProps { children: React.ReactNode; className?: string; as?: 'h2' | 'h3' | 'div'; } /** In-section control-group heading (quieter than SettingsSection title). */ export const SettingsGroupTitle: React.FC = ({ children, className, as: Tag = 'h3', }) => { return ( {children} ); }; interface SettingsControlGroupProps { title?: React.ReactNode; description?: React.ReactNode; /** Helper text hidden behind an info icon next to the group title. */ info?: React.ReactNode; children: React.ReactNode; className?: string; contentClassName?: string; settingsItem?: string; } /** Labeled control cluster inside a section (radios, chips, stacked fields). */ export const SettingsControlGroup: React.FC = ({ title, description, info, children, className, contentClassName, settingsItem, }) => { return (
{title != null || description != null || info != null ? (
{title != null ? (
{title} {info != null ? {info} : null}
) : null} {description != null ? (

{description}

) : null}
) : null}
{children}
); }; interface SettingsStackedFieldProps { label: React.ReactNode; description?: React.ReactNode; /** Helper text hidden behind an info icon next to the label. */ info?: React.ReactNode; /** Where helper text sits relative to the control. @default 'before' */ descriptionPlacement?: 'before' | 'after'; children: React.ReactNode; settingsItem?: string; className?: string; controlClassName?: string; } /** * Label (+ optional description) above a control — for two-column cells. * Prefer this over SettingsFieldRow inside SettingsTwoColumn (FieldRow overflows half-width columns). */ export const SettingsStackedField: React.FC = ({ label, description, info, descriptionPlacement = 'before', children, settingsItem, className, controlClassName, }) => { const descriptionNode = description != null ? (

{description}

) : null; return (
{label}
{info != null ? {info} : null}
{descriptionPlacement === 'before' ? descriptionNode : null}
{children}
{descriptionPlacement === 'after' ? descriptionNode : null}
); }; interface SettingsFieldRowProps { label: React.ReactNode; description?: React.ReactNode; /** Helper text hidden behind an info icon next to the label. */ info?: React.ReactNode; children: React.ReactNode; settingsItem?: string; className?: string; controlClassName?: string; /** Align control to the trailing edge on desktop. @default true */ alignEnd?: boolean; } /** * Side-by-side form row: fixed label column + control cluster. * Use inside full-width sections or single columns. */ export const SettingsFieldRow: React.FC = ({ label, description, info, children, settingsItem, className, controlClassName, alignEnd = true, }) => { return (
{label}
{info != null ? {info} : null}
{description != null ? (

{description}

) : null}
{children}
); }; interface SettingsInsetProps { children: React.ReactNode; className?: string; settingsItem?: string; } /** Optional inset block under a section (spacing only; section dividers own borders). */ export const SettingsInset: React.FC = ({ children, className, settingsItem, }) => { return (
{children}
); }; interface SettingsCheckboxRowProps { checked: boolean; onChange: (checked: boolean) => void; label: React.ReactNode; description?: React.ReactNode; disabled?: boolean; ariaLabel?: string; settingsItem?: string; className?: string; labelAccessory?: React.ReactNode; /** Helper text hidden behind an info icon next to the label. */ info?: React.ReactNode; } /** Shared checkbox setting row with keyboard support. */ export const SettingsCheckboxRow: React.FC = ({ checked, onChange, label, description, disabled = false, ariaLabel, settingsItem, className, labelAccessory, info, }) => { const toggle = () => { if (!disabled) onChange(!checked); }; const hasDescription = description != null; return (
{ if (event.key === ' ' || event.key === 'Enter') { event.preventDefault(); toggle(); } }} >
{label} {labelAccessory} {info != null ? {info} : null}
{hasDescription ? ( {description} ) : null}
); }; interface SettingsRadioOptionProps { selected: boolean; onSelect: () => void; label: React.ReactNode; description?: React.ReactNode; ariaLabel?: string; disabled?: boolean; className?: string; } /** Single radio option row used inside SettingsRadioGroup. */ export const SettingsRadioOption: React.FC = ({ selected, onSelect, label, description, ariaLabel, disabled = false, className, }) => { return (
{ if (!disabled) onSelect(); }} onKeyDown={(event) => { if (event.key === ' ' || event.key === 'Enter') { event.preventDefault(); if (!disabled) onSelect(); } }} >
{label} {description != null ? ( {description} ) : null}
); }; interface SettingsRadioGroupProps { 'aria-label': string; children: React.ReactNode; className?: string; } /** Accessible radio group wrapper with compact vertical spacing. */ export const SettingsRadioGroup: React.FC = ({ 'aria-label': ariaLabel, children, className, }) => { return (
{children}
); }; interface SettingsChipOption { value: T; label: React.ReactNode; disabled?: boolean; } interface SettingsChipGroupProps { value: T; options: Array>; onChange: (value: T) => void; className?: string; 'aria-label'?: string; } /** Compact chip / segmented enum picker. */ export function SettingsChipGroup({ value, options, onChange, className, 'aria-label': ariaLabel, }: SettingsChipGroupProps) { return (
{options.map((option) => ( ))}
); }