Initial commit
This commit is contained in:
112
src/components/ds/Toggle.jsx
Normal file
112
src/components/ds/Toggle.jsx
Normal file
@@ -0,0 +1,112 @@
|
||||
import * as React from 'react';
|
||||
import { cn } from '@/lib/utils';
|
||||
import { Switch } from '@/components/ui/switch';
|
||||
|
||||
/**
|
||||
* Toggle — a labelled switch row for settings.
|
||||
*
|
||||
* Wraps the Switch primitive with the label/description layout it is almost
|
||||
* always used with, and makes the whole row a label so the hit target is the
|
||||
* full width rather than the 36px control.
|
||||
*/
|
||||
export function Toggle({
|
||||
checked,
|
||||
onCheckedChange,
|
||||
label,
|
||||
description,
|
||||
disabled,
|
||||
/** `end` puts the switch on the right (settings rows); `start` on the left. */
|
||||
align = 'end',
|
||||
className,
|
||||
id: providedId,
|
||||
}) {
|
||||
const reactId = React.useId();
|
||||
const id = providedId || reactId;
|
||||
const descriptionId = description ? `${id}-description` : undefined;
|
||||
|
||||
const control = (
|
||||
<Switch
|
||||
id={id}
|
||||
checked={checked}
|
||||
onCheckedChange={onCheckedChange}
|
||||
disabled={disabled}
|
||||
aria-describedby={descriptionId}
|
||||
/>
|
||||
);
|
||||
|
||||
const text = (
|
||||
<span className="min-w-0">
|
||||
<span className="block text-body-sm font-medium text-ink-2">{label}</span>
|
||||
{description && (
|
||||
<span id={descriptionId} className="block text-caption text-ink-3 mt-0.5">
|
||||
{description}
|
||||
</span>
|
||||
)}
|
||||
</span>
|
||||
);
|
||||
|
||||
return (
|
||||
<label
|
||||
htmlFor={id}
|
||||
className={cn(
|
||||
'flex items-center gap-3 cursor-pointer select-none',
|
||||
disabled && 'cursor-not-allowed opacity-60',
|
||||
align === 'end' && 'justify-between',
|
||||
className
|
||||
)}
|
||||
>
|
||||
{align === 'start' ? (
|
||||
<>
|
||||
{control}
|
||||
{text}
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
{text}
|
||||
{control}
|
||||
</>
|
||||
)}
|
||||
</label>
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* SegmentedToggle — a small pill group for switching between 2–4 mutually
|
||||
* exclusive views. Used where Tabs would be too heavy (chart ranges, EN/ES).
|
||||
*/
|
||||
export function SegmentedToggle({ options = [], value, onChange, size = 'default', className, ariaLabel }) {
|
||||
const normalized = options.map((o) => (typeof o === 'string' ? { value: o, label: o } : o));
|
||||
|
||||
return (
|
||||
<div
|
||||
role="radiogroup"
|
||||
aria-label={ariaLabel}
|
||||
className={cn(
|
||||
'inline-flex items-center gap-0.5 rounded-full bg-surface-sunken p-0.5 border border-border',
|
||||
className
|
||||
)}
|
||||
>
|
||||
{normalized.map((option) => {
|
||||
const active = option.value === value;
|
||||
return (
|
||||
<button
|
||||
key={option.value}
|
||||
type="button"
|
||||
role="radio"
|
||||
aria-checked={active}
|
||||
onClick={() => onChange?.(option.value)}
|
||||
className={cn(
|
||||
'rounded-full font-medium transition-all duration-base ease-out focus-ring-inset',
|
||||
size === 'sm' ? 'px-2.5 py-1 text-caption' : 'px-3 py-1.5 text-body-sm',
|
||||
active
|
||||
? 'bg-krow-blue text-white shadow-sm'
|
||||
: 'text-ink-3 hover:text-ink-1'
|
||||
)}
|
||||
>
|
||||
{option.label}
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user