/**
* Switch (web). A binary toggle — the on/off sibling of Checkbox, for
* settings and instant-apply options. Track fills with the accent when
* on; the thumb slides with a short ease-out.
*
*
*
*/
import { useState, type CSSProperties, type ReactNode } from "react";
import { useTheme } from "@plyxui/styles";
export type SwitchSize = "sm" | "md";
const sizeMap: Record = {
sm: { w: 34, h: 20, thumb: 14, pad: 3 },
md: { w: 44, h: 26, thumb: 20, pad: 3 },
};
export interface SwitchProps {
checked?: boolean;
defaultChecked?: boolean;
onChange?: (checked: boolean) => void;
disabled?: boolean;
size?: SwitchSize;
label?: ReactNode;
id?: string;
"aria-label"?: string;
}
export function Switch({ checked, defaultChecked, onChange, disabled, size = "md", label, id, ...aria }: SwitchProps) {
const { colors } = useTheme();
const [internal, setInternal] = useState(defaultChecked ?? false);
const isControlled = checked !== undefined;
const on = isControlled ? checked : internal;
const dims = sizeMap[size];
const toggle = () => {
if (disabled) return;
if (!isControlled) setInternal(!on);
onChange?.(!on);
};
const track: CSSProperties = {
width: dims.w,
height: dims.h,
borderRadius: dims.h,
background: on ? colors.primaryOrange : colors.stroke,
border: "none",
position: "relative",
cursor: disabled ? "default" : "pointer",
opacity: disabled ? 0.55 : 1,
transition: "background 0.2s cubic-bezier(0.23,1,0.32,1)",
flex: "none",
padding: 0,
};
const thumb: CSSProperties = {
position: "absolute",
top: dims.pad,
left: dims.pad,
width: dims.thumb,
height: dims.thumb,
borderRadius: "50%",
background: "#fff",
boxShadow: "0 1px 2px rgba(0,0,0,.25)",
transform: on ? `translateX(${dims.w - dims.thumb - dims.pad * 2}px)` : "translateX(0)",
transition: "transform 0.2s cubic-bezier(0.23,1,0.32,1)",
};
const control = (
);
if (!label) return control;
return (
);
}