import { useUniqueId } from "@charcuterie/logic" import type { ControlSize } from "@charcuterie/tokens" import type { ReactNode } from "react" import { useState } from "react" import { FOCUS_RING_CLASS } from "../intentStyles.ts" import { toClassName } from "../toClassName.ts" export type SwitchProps = { className?: string /** * **Initial** only. The `Switch` owns it from then on — the same * uncontrolled contract as `Checkbox` and `SegmentedControl`, so * `onChange` reports the new state and there is no controlled * `checked` prop to keep in step. */ isChecked?: boolean isDisabled?: boolean /** * Shows the state at full contrast but refuses to flip it — a * setting you may read here and change elsewhere. Unlike * `isDisabled` it does not dim, and it announces `aria-readonly`. */ isReadOnly?: boolean /** The visible text, and the switch's accessible name. */ label: ReactNode onChange?: (isChecked: boolean) => void size?: ControlSize } // Track and thumb are sized so `p-1` (4px) breathes evenly on every // side and the thumb is centred by arithmetic, not by eye: inner // height (track − 2·padding) equals the thumb, and the on-translate is // (track width − 2·padding − thumb). Break one of those and the thumb // clips an edge, which is what the first cut did. const TRACK_SIZE_CLASS: Record = { sm: "h-5 w-9", md: "h-6 w-11", lg: "h-7 w-14", } const THUMB_SIZE_CLASS: Record = { sm: "size-3", md: "size-4", lg: "size-5", } const THUMB_ON_TRANSLATE_CLASS: Record< ControlSize, string > = { sm: "translate-x-4", md: "translate-x-5", lg: "translate-x-7", } const TEXT_SIZE_CLASS: Record = { sm: "text-sm", md: "text-md", lg: "text-lg", } /** * A boolean that takes effect the moment it is flipped — a setting, * not a form value. * * A `Checkbox` and a `Switch` are the same state kind and a * different affordance, and the split is meaning rather than looks: * a checkbox is a value you submit ("I agree"), a switch is a state * you toggle and live with immediately ("dark mode on"). ARIA gives * the second its own role, and a screen reader announces "on/off" * for a `switch` where it announces "checked/unchecked" for a * checkbox. * * ### A `button role="switch"`, not an `input` * * The sliding track and thumb are the whole point of the affordance, * and a ` {label} ) }