// ~/src/siteplane/components/checkbox/checkbox.tsx "use client"; import { Checkbox as CheckboxPrimitive } from "@base-ui/react/checkbox"; import { cva } from "class-variance-authority"; import * as React from "react"; import { type SiteplaneControlSize, useResolvedControlSize, } from "../../system/provider/siteplane-ui-provider"; import { ChoiceMenuItemCheck, choiceMenuItemCheckGridClassName, choiceMenuItemClassName, choiceMenuItemLabelClassName, } from "../../system/styles/choice-menu-item"; import { CheckboxGlyph } from "../../system/styles/checkbox-glyph"; import { cn, type WithStringClassName } from "../../system/utils/cn"; import { baseClass, slotClass } from "../../system/utils/slot-class"; import type { KitCheckboxProps } from "./checkbox-internal"; import "./checkbox.css"; // The look and the size come from checkbox.css through the attributes the // Checkbox renders (data-size), so the helper takes no size. export const checkboxVariants = cva( "sp-checkbox-motion relative inline-flex shrink-0 cursor-pointer items-center justify-center before:pointer-events-none before:absolute before:inset-0 before:rounded-[inherit] data-disabled:cursor-not-allowed", ); export const CheckboxSizeContext = React.createContext(null); export interface CheckboxProps extends Omit< WithStringClassName, "size" > { size?: SiteplaneControlSize; } export function Checkbox(checkboxProps: CheckboxProps): React.ReactElement { // Menu and drawer rows pass the internal menu-item profile through // KitCheckbox (checkbox-internal.ts), the same function. const { children, className, profile = "control", size, ...props } = checkboxProps as KitCheckboxProps; const groupSize = React.useContext(CheckboxSizeContext); const resolvedSize = useResolvedControlSize(size ?? groupSize); return ( {profile === "menu-item" ? ( <> {children} ) : ( ) => ( )} /> )} ); } export { CheckboxPrimitive }; // ~/src/siteplane/components/checkbox/checkbox-internal.ts import type * as React from "react"; import { Checkbox, type CheckboxProps } from "./checkbox"; /* * Kit-internal Checkbox. Not public API: the checkbox index does not export * this module, and its names and profiles may change in any release. Menu and * Drawer checkbox rows render a Checkbox with the menu-item profile. */ export type CheckboxProfile = "control" | "menu-item"; export type KitCheckboxProps = CheckboxProps & { profile?: CheckboxProfile }; /** Checkbox with the kit's composition profile; the same component. */ export const KitCheckbox = Checkbox as ( props: KitCheckboxProps, ) => React.ReactElement; // ~/src/siteplane/components/checkbox/checkbox.css @layer theme, base, components, siteplane, utilities; @layer components { .sp-checkbox-motion { color: var(--color-neutral-800); } .dark .sp-checkbox-motion, [data-theme="dark"] .sp-checkbox-motion { color: rgb(242 242 243); } .sp-checkbox-motion .sp-checkbox-indicator { background-color: transparent; opacity: 0; transform: scale(var(--motion-check-pop-scale)); transform-origin: center; transition-duration: var(--motion-duration-slow); transition-property: background-color, opacity, transform; transition-timing-function: var(--motion-ease-standard); } .sp-checkbox-motion .sp-checkbox-indicator[data-checked], .sp-checkbox-motion .sp-checkbox-indicator[data-indeterminate] { background-color: var(--sp-semantic-color-primary); opacity: 1; transform: scale(1); } .sp-checkbox-motion .sp-checkbox-indicator[data-starting-style], .sp-checkbox-motion .sp-checkbox-indicator[data-ending-style] { background-color: transparent; opacity: 0; transform: scale(var(--motion-check-pop-scale)); } /* A checked indicator lingers on its way out, as the Radio's does. */ .sp-checkbox-motion .sp-checkbox-indicator[data-ending-style] { transition-duration: var(--motion-duration-linger); } /* Rulebook I5: a checkbox before text centres on the first text line, also * when the text wraps. That is a checkbox that leads a label or a row in a * label, or that stands before a label, as in a row with a description. A * checkbox taller than the line starts at the line's top, so a one-line label * keeps its height. A component rule, so a margin utility on the checkbox * still replaces it. */ :where(label, .sp-label, .sp-field-label) > .sp-base-checkbox[data-profile="control"]:first-child, :where(label, .sp-label, .sp-field-label) > :where(span, div) > .sp-base-checkbox[data-profile="control"]:first-child, .sp-base-checkbox[data-profile="control"]:first-child:has( ~ :is(.sp-label, .sp-field-label), ~ * :is(.sp-label, .sp-field-label) ) { align-self: flex-start; margin-block: max(0px, calc((1lh - var(--sp-rw-choice-size)) / 2)); } /* Beside its label, the checkbox does not inherit the label's line but its * container's. It takes the 21 px line of a label next to a choice control * (S13), so the 1lh above measures the label's first line. */ .sp-base-checkbox[data-profile="control"]:first-child:has( ~ :is(.sp-label, .sp-field-label), ~ * :is(.sp-label, .sp-field-label) ) { line-height: var(--sp-rw-line-21); } /* The check and the indeterminate dash draw through the shared check motion * in motion.css, which the check of a menu row uses too. */ /* Reduced motion is instant in the three channels the kit has: the forced * document mode, the provider preset and the system setting. The system rule * leads with :root so the state rules above, which are as specific as the * indicator selector plus one attribute, do not outrank it. */ html[data-motion-reduce="true"] .sp-checkbox-motion .sp-checkbox-indicator, [data-sp-motion="reduced"] .sp-checkbox-motion .sp-checkbox-indicator { animation: none; transform: none; transition-duration: 1ms; } @media (prefers-reduced-motion: reduce) { :root .sp-checkbox-motion .sp-checkbox-indicator { animation: none; transform: none; transition-duration: 1ms; } } } // ~/src/siteplane/components/checkbox/index.ts export * from "./checkbox";