import { useEffect, useId, type ReactNode } from 'react'; import { ImageIcon, InfoIcon } from 'lucide-react'; import { Controller, useWatch, type Control, type FieldPath, type FieldValues, type SetValueConfig, type UseFormRegister, } from 'react-hook-form'; import { classNames } from '../../lib/utils'; import { Button } from '../ui/button'; import { Input } from '../ui/input'; import { Label } from '../ui/label'; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue, } from '../ui/select'; import { Switch } from '../ui/switch'; import { Textarea } from '../ui/textarea'; import { Tooltip, TooltipContent, TooltipTrigger } from '../ui/tooltip'; import { templateWarnings } from '../../lib/seo/template-warnings'; import { useOptionHighlight, useOptionRegister, useOptionSection, } from './settings-search'; import { VariablesMenu, type VariableOption } from './variables-menu'; /** * How a setting explains itself: the label and the description on the left * of its row, and a tooltip with the effect, the reason for the default or * an example on the info icon. */ export interface OptionCopy { label: string; description?: string; tip?: string; } export interface OptionChoice { value: string; label: string; } /** * react-hook-form's setValue narrowed to string fields: the generic form * cannot be typed against an unknown T, and every field written here is text. */ export type SetStringValue = ( name: FieldPath, value: string, options?: SetValueConfig ) => void; export const OptionTip = ({ children, }: { children: ReactNode; }): JSX.Element => ( {children} ); /** * One setting, laid out the way Rank Math lays out its options: the label * with its tip in the left column, the control in the right one and the * description under the control, so the eye reads name, value, meaning. */ export const OptionRow = ({ copy, htmlFor, labelId, onLabelClick, error, hint, children, }: { copy: OptionCopy; htmlFor?: string; labelId?: string; onLabelClick?: () => void; error?: string; hint?: ReactNode; children: ReactNode; }): JSX.Element => { const rowId = useId(); const section = useOptionSection(); const register = useOptionRegister(); const highlighted = useOptionHighlight() === rowId; useEffect( () => register?.({ id: rowId, section, label: copy.label, description: copy.description ?? '', tip: copy.tip ?? '', }), [register, rowId, section, copy.label, copy.description, copy.tip] ); return (
{copy.tip && ( {copy.tip} )}
{children} {copy.description && (

{copy.description}

)} {error ? (

{error}

) : ( hint &&
{hint}
)}
); }; /** * A group of related options, one row under the other. A title marks a * second group inside a section; the first group of a section has none, * the section's header above already says what it is for. */ export const OptionSection = ({ title, description, children, }: { title?: string; description?: ReactNode; children: ReactNode; }): JSX.Element => (
{title && (

{title}

{description && (

{description}

)}
)}
{children}
); export function ToggleRow({ control, name, copy, disabled, onLabel = 'On', offLabel = 'Off', }: { control: Control; name: FieldPath; copy: OptionCopy; disabled?: boolean; onLabel?: string; offLabel?: string; }): JSX.Element { const labelId = useId(); return ( ( field.onChange(!field.value) } >
{field.value ? onLabel : offLabel}
)} /> ); } /** * Whether an item is in a list field, as a toggle: "Include in sitemap" for * one post type flips its slug in and out of the post_types list. */ export function ListToggleRow({ control, name, item, copy, }: { control: Control; name: FieldPath; item: string; copy: OptionCopy; }): JSX.Element { const labelId = useId(); return ( { const list: string[] = Array.isArray(field.value) ? field.value : []; const included = list.includes(item); const toggle = (next: boolean): void => field.onChange( next ? [...list.filter(entry => entry !== item), item] : list.filter(entry => entry !== item) ); return ( toggle(!included)} >
{included ? 'Included' : 'Left out'}
); }} /> ); } export function TextRow({ register, name, copy, multiline, placeholder, error, hint, className, inputMode, }: { register: UseFormRegister; name: FieldPath; copy: OptionCopy; multiline?: boolean; placeholder?: string; error?: string; hint?: ReactNode; className?: string; inputMode?: 'numeric' | 'text' | 'url' | 'email' | 'tel'; }): JSX.Element { return ( {multiline ? (