175 lines
5.2 KiB
TypeScript
175 lines
5.2 KiB
TypeScript
// components/ui/Switch.tsx
|
|
'use client'
|
|
|
|
import * as React from 'react'
|
|
import clsx from 'clsx'
|
|
|
|
type SwitchSize = 'default' | 'short'
|
|
type SwitchVariant = 'simple' | 'icon'
|
|
|
|
export type SwitchProps = {
|
|
/** Controlled */
|
|
checked: boolean
|
|
onChange: (checked: boolean) => void
|
|
|
|
/** Optional wiring */
|
|
id?: string
|
|
name?: string
|
|
disabled?: boolean
|
|
required?: boolean
|
|
|
|
/** Labeling / a11y */
|
|
ariaLabel?: string
|
|
ariaLabelledby?: string
|
|
ariaDescribedby?: string
|
|
|
|
/** UI */
|
|
size?: SwitchSize
|
|
variant?: SwitchVariant
|
|
className?: string
|
|
}
|
|
|
|
/**
|
|
* Switch / Toggle (Tailwind, ohne Headless UI)
|
|
* - size="default" (w-11) wie Simple toggle
|
|
* - size="short" (h-5 w-10) wie Short toggle
|
|
* - variant="icon" zeigt X/Check Icon im Thumb
|
|
*/
|
|
export default function Switch({
|
|
checked,
|
|
onChange,
|
|
id,
|
|
name,
|
|
disabled,
|
|
required,
|
|
ariaLabel,
|
|
ariaLabelledby,
|
|
ariaDescribedby,
|
|
size = 'default',
|
|
variant = 'simple',
|
|
className,
|
|
}: SwitchProps) {
|
|
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
|
if (disabled) return
|
|
onChange(e.target.checked)
|
|
}
|
|
|
|
const baseInput = clsx(
|
|
'absolute inset-0 size-full appearance-none focus:outline-hidden',
|
|
disabled && 'cursor-not-allowed'
|
|
)
|
|
|
|
if (size === 'short') {
|
|
// Short toggle Beispiel
|
|
return (
|
|
<div
|
|
className={clsx(
|
|
'group relative inline-flex h-5 w-10 shrink-0 items-center justify-center rounded-full outline-offset-2 outline-indigo-600 has-focus-visible:outline-2 dark:outline-indigo-500',
|
|
disabled && 'opacity-60',
|
|
className
|
|
)}
|
|
>
|
|
<span
|
|
className={clsx(
|
|
'absolute mx-auto h-4 w-9 rounded-full bg-gray-200 inset-ring inset-ring-gray-900/5 transition-colors duration-200 ease-in-out dark:bg-gray-800/50 dark:inset-ring-white/10',
|
|
checked && 'bg-indigo-600 dark:bg-indigo-500'
|
|
)}
|
|
/>
|
|
<span
|
|
className={clsx(
|
|
'absolute left-0 size-5 rounded-full border border-gray-300 bg-white shadow-xs transition-transform duration-200 ease-in-out dark:shadow-none',
|
|
checked && 'translate-x-5'
|
|
)}
|
|
/>
|
|
<input
|
|
id={id}
|
|
name={name}
|
|
type="checkbox"
|
|
checked={checked}
|
|
onChange={handleChange}
|
|
disabled={disabled}
|
|
required={required}
|
|
aria-label={ariaLabel}
|
|
aria-labelledby={ariaLabelledby}
|
|
aria-describedby={ariaDescribedby}
|
|
className={baseInput}
|
|
/>
|
|
</div>
|
|
)
|
|
}
|
|
|
|
// Default size (simple / icon) Beispiele
|
|
return (
|
|
<div
|
|
className={clsx(
|
|
'group relative inline-flex w-11 shrink-0 rounded-full bg-gray-200 p-0.5 inset-ring inset-ring-gray-900/5 outline-offset-2 outline-indigo-600 transition-colors duration-200 ease-in-out has-focus-visible:outline-2 dark:bg-white/5 dark:inset-ring-white/10 dark:outline-indigo-500',
|
|
checked && 'bg-indigo-600 dark:bg-indigo-500',
|
|
disabled && 'opacity-60',
|
|
className
|
|
)}
|
|
>
|
|
{variant === 'icon' ? (
|
|
<span
|
|
className={clsx(
|
|
'relative size-5 rounded-full bg-white shadow-xs ring-1 ring-gray-900/5 transition-transform duration-200 ease-in-out',
|
|
checked && 'translate-x-5'
|
|
)}
|
|
>
|
|
{/* Off icon */}
|
|
<span
|
|
aria-hidden="true"
|
|
className={clsx(
|
|
'absolute inset-0 flex size-full items-center justify-center transition-opacity ease-in',
|
|
checked ? 'opacity-0 duration-100' : 'opacity-100 duration-200'
|
|
)}
|
|
>
|
|
<svg fill="none" viewBox="0 0 12 12" className="size-3 text-gray-400 dark:text-gray-600">
|
|
<path
|
|
d="M4 8l2-2m0 0l2-2M6 6L4 4m2 2l2 2"
|
|
stroke="currentColor"
|
|
strokeWidth={2}
|
|
strokeLinecap="round"
|
|
strokeLinejoin="round"
|
|
/>
|
|
</svg>
|
|
</span>
|
|
|
|
{/* On icon */}
|
|
<span
|
|
aria-hidden="true"
|
|
className={clsx(
|
|
'absolute inset-0 flex size-full items-center justify-center transition-opacity ease-out',
|
|
checked ? 'opacity-100 duration-200' : 'opacity-0 duration-100'
|
|
)}
|
|
>
|
|
<svg fill="currentColor" viewBox="0 0 12 12" className="size-3 text-indigo-600 dark:text-indigo-500">
|
|
<path d="M3.707 5.293a1 1 0 00-1.414 1.414l1.414-1.414zM5 8l-.707.707a1 1 0 001.414 0L5 8zm4.707-3.293a1 1 0 00-1.414-1.414l1.414 1.414zm-7.414 2l2 2 1.414-1.414-2-2-1.414 1.414zm3.414 2l4-4-1.414-1.414-4 4 1.414 1.414z" />
|
|
</svg>
|
|
</span>
|
|
</span>
|
|
) : (
|
|
<span
|
|
className={clsx(
|
|
'size-5 rounded-full bg-white shadow-xs ring-1 ring-gray-900/5 transition-transform duration-200 ease-in-out',
|
|
checked && 'translate-x-5'
|
|
)}
|
|
/>
|
|
)}
|
|
|
|
<input
|
|
id={id}
|
|
name={name}
|
|
type="checkbox"
|
|
checked={checked}
|
|
onChange={handleChange}
|
|
disabled={disabled}
|
|
required={required}
|
|
aria-label={ariaLabel}
|
|
aria-labelledby={ariaLabelledby}
|
|
aria-describedby={ariaDescribedby}
|
|
className={baseInput}
|
|
/>
|
|
</div>
|
|
)
|
|
}
|