Files
SkinbaseNova/.deploy/artwork-evolution-release/resources/js/components/ui/Checkbox.jsx
2026-04-18 17:02:56 +02:00

114 lines
3.7 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import React, { forwardRef } from 'react'
/**
* Nova Checkbox fully custom rendering (appearance-none + SVG tick).
* Avoids @tailwindcss/forms overriding the checked background colour.
*
* @prop {string} label - label text rendered alongside the box
* @prop {string} hint - small helper line below label
* @prop {string} error - inline error
* @prop {number|string} size - pixel size (default 18)
* @prop {string} variant - 'accent' | 'emerald' | 'sky'
*/
const variantStyles = {
accent: { checked: '#E07A21', ring: 'rgba(224,122,33,0.45)' },
emerald: { checked: '#10b981', ring: 'rgba(16,185,129,0.45)' },
sky: { checked: '#0ea5e9', ring: 'rgba(14,165,233,0.45)' },
}
const Checkbox = forwardRef(function Checkbox(
{ label, hint, error, size = 18, variant = 'accent', id, className = '', checked, disabled, onChange, ...rest },
ref,
) {
const dim = typeof size === 'number' ? `${size}px` : size
const numSize = typeof size === 'number' ? size : parseInt(size, 10)
const inputId = id ?? (label ? `cb-${label.toLowerCase().replace(/\s+/g, '-')}` : undefined)
const colors = variantStyles[variant] ?? variantStyles.accent
// Tick sizes relative to box
const tickInset = Math.round(numSize * 0.18)
const strokeWidth = Math.max(1.5, numSize * 0.1)
return (
<div className="flex flex-col gap-1">
<label
className={[
'inline-flex items-start gap-2.5 select-none',
disabled ? 'opacity-50 cursor-not-allowed' : 'cursor-pointer',
].join(' ')}
>
{/* Hidden native input keeps full a11y / form submission */}
<input
type="checkbox"
id={inputId}
ref={ref}
checked={checked}
disabled={disabled}
onChange={onChange}
className="sr-only"
aria-invalid={!!error}
{...rest}
/>
{/* Visual box */}
<span
aria-hidden="true"
style={{
width: dim,
height: dim,
minWidth: dim,
minHeight: dim,
aspectRatio: '1 / 1',
marginTop: label ? '1px' : undefined,
backgroundColor: checked ? colors.checked : 'rgba(255,255,255,0.06)',
borderColor: checked ? colors.checked : 'rgba(255,255,255,0.25)',
boxShadow: checked ? `0 0 0 0px ${colors.ring}` : undefined,
transition: 'background-color 150ms, border-color 150ms',
}}
className={[
'shrink-0 inline-flex items-center justify-center',
'rounded-md border',
className,
].join(' ')}
>
{/* SVG tick — only visible when checked */}
<svg
viewBox="0 0 12 12"
fill="none"
style={{
width: numSize - tickInset * 2,
height: numSize - tickInset * 2,
opacity: checked ? 1 : 0,
transition: 'opacity 100ms',
}}
aria-hidden="true"
>
<path
d="M1.5 6l3 3 6-6"
stroke="white"
strokeWidth={strokeWidth}
strokeLinecap="round"
strokeLinejoin="round"
/>
</svg>
</span>
{(label || hint) && (
<span className="flex flex-col gap-0.5">
{label && <span className="text-sm text-white/90 leading-snug">{label}</span>}
{hint && <span className="text-xs text-slate-500">{hint}</span>}
</span>
)}
</label>
{error && (
<p role="alert" className="text-xs text-red-400" style={{ paddingLeft: `calc(${dim} + 0.625rem)` }}>
{error}
</p>
)}
</div>
)
})
export default Checkbox