Files
doormilxpress_astryx/src/components/ds/Toggle.jsx
2026-08-17 17:33:56 +05:30

153 lines
4.4 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 * 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.
*/
/** @param {any} props */
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).
*/
/**
* Two treatments for the same control.
*
* `brand` states a choice that changes what the data *says* — a date range, a
* scope — so the selection carries the page's accent colour and reads as a
* filter. `surface` states a choice that changes only how the same data is
* *drawn*, so the selection is a raised white chip on a sunken track: present
* enough to show which mode is on, quiet enough not to compete with the real
* filters sitting beside it in the same toolbar.
*/
const SEGMENT_VARIANTS = {
brand: {
track: 'bg-surface-sunken border-border',
active: 'bg-krow-blue text-white shadow-sm',
inactive: 'text-ink-3 hover:text-ink-1',
},
surface: {
track: 'bg-surface-sunken border-border',
active: 'bg-surface text-ink-1 shadow-xs',
inactive: 'text-ink-4 hover:text-ink-2',
},
};
/** @param {any} props */
export function SegmentedToggle({
options = [],
value = null,
onChange = null,
size = 'default',
variant = 'brand',
className = '',
ariaLabel = '',
}) {
const normalized = options.map((o) => (typeof o === 'string' ? { value: o, label: o } : o));
const tone = SEGMENT_VARIANTS[variant] || SEGMENT_VARIANTS.brand;
return (
<div
role="radiogroup"
aria-label={ariaLabel}
className={cn('inline-flex items-center gap-0.5 rounded-full border p-0.5', tone.track, className)}
>
{normalized.map((option) => {
const active = option.value === value;
const Icon = option.icon;
/* Icon-only options carry their name on the button instead of beside
it, so the control stays compact without becoming unlabelled. */
const iconOnly = Boolean(Icon) && !option.label;
return (
<button
key={option.value}
type="button"
role="radio"
aria-checked={active}
aria-label={option.ariaLabel || (iconOnly ? String(option.value) : undefined)}
title={option.ariaLabel}
onClick={() => onChange?.(option.value)}
className={cn(
'inline-flex items-center justify-center gap-1.5 rounded-full font-medium',
'transition-all duration-base ease-out focus-ring-inset',
iconOnly
? (size === 'sm' ? 'h-6 w-7' : 'h-7 w-8')
: (size === 'sm' ? 'px-2.5 py-1 text-caption' : 'px-3 py-1.5 text-body-sm'),
active ? tone.active : tone.inactive
)}
>
{Icon && <Icon className={size === 'sm' ? 'h-3.5 w-3.5' : 'h-4 w-4'} aria-hidden="true" />}
{option.label}
</button>
);
})}
</div>
);
}