tpmjs/packages/ui/src/Checkbox/Checkbox.tsx
Ajax Davis ded939e643 feat(ui): add comprehensive form component library with playground showcase
Implemented 8 production-ready form components with full accessibility:

Components Added:
- Textarea: Multi-line text input with character counter
- Checkbox: Custom styled with indeterminate state support
- Radio & RadioGroup: Context-based radio button groups
- Switch: Toggle with animated thumb and loading state
- Select: Native select with custom styling and option groups
- Slider: Range input with marks, value display, cross-browser support
- FormField: Wrapper component with label, error, and helper text

Features:
- Full accessibility (ARIA attributes, semantic HTML)
- Controlled/uncontrolled patterns via useControlled hook
- Dark mode support with semantic tokens
- Design tokens and shared variant system
- Comprehensive test coverage (856 tests passing)
- Form-specific design tokens (formTokens)
- Shared form variant base classes (formVariants)

Playground Updates:
- Added comprehensive Forms section showcasing all components
- Interactive examples with state management
- Complete form composition example
- All components fully functional and themed

Test Coverage:
- 10+ describe blocks per component
- All edge cases covered
- Accessibility testing
- Cross-browser compatibility

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-27 01:56:54 +10:00

158 lines
3.8 KiB
TypeScript

import { cn } from '@tpmjs/utils/cn';
import { forwardRef, useEffect, useRef } from 'react';
import type { CheckboxProps } from './types';
import {
checkboxLabelVariants,
checkboxUIVariants,
checkboxVariants,
checkmarkVariants,
indeterminateVariants,
} from './variants';
/**
* Checkbox component
*
* A custom-styled checkbox with support for indeterminate state,
* different sizes, states, and optional labels.
*
* @example
* ```tsx
* import { Checkbox } from '@tpmjs/ui/Checkbox/Checkbox';
*
* function MyComponent() {
* return (
* <Checkbox
* label="Accept terms and conditions"
* state="default"
* size="md"
* />
* );
* }
* ```
*
* @example
* ```tsx
* // Indeterminate state (for partial selections)
* <Checkbox
* label="Select all"
* indeterminate={true}
* onChange={handleSelectAll}
* />
* ```
*/
export const Checkbox = forwardRef<HTMLInputElement, CheckboxProps>(
(
{
className,
state = 'default',
size = 'md',
indeterminate = false,
label,
labelPosition = 'right',
disabled = false,
id,
...props
},
ref
) => {
const inputRef = useRef<HTMLInputElement | null>(null);
// Handle indeterminate state via DOM property
useEffect(() => {
const input = inputRef.current;
if (input) {
input.indeterminate = indeterminate;
// Set data attribute for CSS styling
if (indeterminate) {
input.setAttribute('data-indeterminate', 'true');
} else {
input.removeAttribute('data-indeterminate');
}
}
}, [indeterminate]);
// Generate unique ID if not provided
const checkboxId = id || `checkbox-${Math.random().toString(36).substring(2, 9)}`;
const checkboxInput = (
<input
ref={(node) => {
inputRef.current = node;
if (typeof ref === 'function') {
ref(node);
} else if (ref) {
ref.current = node;
}
}}
type="checkbox"
id={checkboxId}
className={cn(checkboxVariants({ state, size }), className)}
disabled={disabled}
aria-checked={indeterminate ? 'mixed' : undefined}
{...props}
/>
);
const checkboxUI = (
<span className={cn(checkboxUIVariants({ state, size }))}>
{/* Checkmark icon */}
<svg
className={cn(checkmarkVariants({ size }))}
viewBox="0 0 16 16"
fill="none"
xmlns="http://www.w3.org/2000/svg"
aria-hidden="true"
>
<path
d="M13.5 4.5L6 12L2.5 8.5"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
/>
</svg>
{/* Indeterminate line */}
<svg
className={cn(indeterminateVariants({ size }))}
viewBox="0 0 16 16"
fill="none"
xmlns="http://www.w3.org/2000/svg"
aria-hidden="true"
>
<path
d="M4 8H12"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
/>
</svg>
</span>
);
const labelElement = label ? (
<label
htmlFor={checkboxId}
className={cn(
checkboxLabelVariants({ size }),
labelPosition === 'left' ? 'mr-2' : 'ml-2'
)}
>
{label}
</label>
) : null;
return (
<div className={cn('inline-flex items-center', disabled && 'cursor-not-allowed')}>
{labelPosition === 'left' && labelElement}
<div className="relative inline-flex">
{checkboxInput}
{checkboxUI}
</div>
{labelPosition === 'right' && labelElement}
</div>
);
}
);
Checkbox.displayName = 'Checkbox';