update position page design

This commit is contained in:
2026-08-13 21:41:14 +05:30
parent 6340962907
commit 1c653264b5
124 changed files with 5746 additions and 596 deletions

View File

@@ -74,35 +74,67 @@ export function Toggle({
* 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 }) {
/**
* 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',
},
};
export function SegmentedToggle({
options = [], value, onChange, 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 bg-surface-sunken p-0.5 border border-border',
className
)}
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(
'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'
'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>
);