import React, { useState } from 'react'; import type { Meta, StoryObj } from '@storybook/react-vite'; import { Icon } from '../Icon/Icon'; import { Tag } from '../Tag/Tag'; import { SegmentedControl } from './SegmentedControl'; import type { SegmentRenderContext } from './SegmentedControl'; const meta: Meta = { title: 'UI kit/SegmentedControl', component: SegmentedControl, tags: ['autodocs'], decorators: [ Story => (
), ], parameters: { docs: { description: { component: 'A compact mutual-exclusion selector for 2–5 options, built on top of the core-web RadioGroup. ' + 'Pass an `options` array and accessor functions — `getOptionValue`, `getOptionLabel`, ' + '`getOptionIcon`, etc. — following the same pattern as SingleSelect. Exactly one segment ' + 'is always selected. Use for view toggles, inline filters, and period pickers.', }, }, }, }; export default meta; // ─── Shared option shapes ───────────────────────────────────────────────────── interface StatusOption { id: string; label: string; count?: number; } interface PeriodOption { id: string; label: string; } interface ViewOption { id: string; label: string; icon: string; } const statusOptions: StatusOption[] = [ { id: 'all', label: 'Vše', count: 124 }, { id: 'active', label: 'Aktivní', count: 89 }, { id: 'archive', label: 'Archiv', count: 35 }, ]; const periodOptions: PeriodOption[] = [ { id: 'day', label: 'Den' }, { id: 'week', label: 'Týden' }, { id: 'month', label: 'Měsíc' }, { id: 'year', label: 'Rok' }, ]; const viewOptions: ViewOption[] = [ { id: 'table', label: 'Tabulka', icon: 'shp-list' }, { id: 'cards', label: 'Karty', icon: 'shp-listImage' }, { id: 'preview', label: 'Náhled', icon: 'shp-preview' }, ]; // ─── Default ───────────────────────────────────────────────────────────────── export const Default: StoryObj = { render: () => { const [value, setValue] = useState('active'); return ( option.id} getOptionLabel={option => option.label} value={value} onChange={v => setValue(v)} aria-label='Filter orders by status' /> ); }, }; // ─── Sizes ─────────────────────────────────────────────────────────────────── export const Sizes: StoryObj = { render: () => { const [value, setValue] = useState('month'); return (
{(['lg', 'md', 'sm'] as const).map(size => (
{size} option.id} getOptionLabel={option => option.label} value={value} onChange={v => setValue(v)} aria-label={`Period selector ${size}`} />
))}
); }, parameters: { docs: { description: { story: 'Three outer heights matching the Shoptet form scale: lg 48 px, md 32 px, sm 26 px. ' + 'sm supports textOnly only — 24×24 icons do not fit in the 22 px inner segment height.', }, }, }, }; // ─── Mode: textOnly ────────────────────────────────────────────────────────── export const TextOnly: StoryObj = { render: () => { const [value, setValue] = useState('all'); return ( option.id} getOptionLabel={option => option.label} value={value} onChange={v => setValue(v)} aria-label='Order status filter' /> ); }, parameters: { docs: { description: { story: 'Label-only mode. Use when options are self-explanatory by name. ' + 'The lightest visual weight and the only mode available in sm size.', }, }, }, }; // ─── Mode: iconText ────────────────────────────────────────────────────────── export const IconText: StoryObj = { render: () => { const [value, setValue] = useState('table'); return (
{(['lg', 'md'] as const).map(size => (
:{size} option.id} getOptionLabel={option => option.label} getOptionIcon={option => [0]['icon']} />} value={value} onChange={v => setValue(v)} aria-label={`Product view toggle ${size}`} />
))}
); }, parameters: { docs: { description: { story: 'Default mode combining a 24×24 icon and a short text label. Use when both meaning and quick recognition matter. ' + "Available in lg and md sizes (sm is textOnly-only — icon 24×24 doesn't fit in 22 px segment height).", }, }, }, }; // ─── Mode: iconOnly ────────────────────────────────────────────────────────── export const IconOnly: StoryObj = { render: () => { const [value, setValue] = useState('table'); return (
{(['lg', 'md'] as const).map(size => (
:{size} option.id} getOptionLabel={option => option.label} getOptionIcon={option => [0]['icon']} />} value={value} onChange={v => setValue(v)} aria-label={`Product view toggle ${size}`} />
))} Note: not available in :sm — icons would not meet 24×24 spec inside a 22 px segment.
); }, parameters: { docs: { description: { story: 'Square mode (lg 44×44, md 28×28) showing only the icon. Use when space is tight and icon meaning is unambiguous (view layouts, sort order). ' + "Always provide accessible label via aria-label. Not available in sm — icon 24×24 doesn't fit in 22 px segment.", }, }, }, }; // ─── renderOptionSuffix ─────────────────────────────────────────────────────── export const WithSuffix: StoryObj = { render: () => { const [value, setValue] = useState('all'); return ( option.id} getOptionLabel={option => option.label} renderOptionSuffix={option => (option.count === undefined ? null : {option.count})} value={value} onChange={v => setValue(v)} aria-label='Order status with counts' /> ); }, parameters: { docs: { description: { story: 'renderOptionSuffix appends any ReactNode after the default icon+label content — ' + 'a Tag counter, a status dot, or anything else. ' + 'Return null for options that need no suffix. ' + 'For fully custom layouts use renderOption instead.', }, }, }, }; // ─── getOptionDisabled ──────────────────────────────────────────────────────── export const DisabledOption: StoryObj = { render: () => { const [value, setValue] = useState('all'); const options: StatusOption[] = [ { id: 'all', label: 'Vše' }, { id: 'active', label: 'Aktivní' }, { id: 'archive', label: 'Archiv' }, ]; return ( option.id} getOptionLabel={option => option.label} getOptionDisabled={option => option.id === 'archive'} value={value} onChange={v => setValue(v)} aria-label='Order status — archive disabled' /> ); }, parameters: { docs: { description: { story: 'getOptionDisabled disables individual options while the group stays interactive.', }, }, }, }; // ─── Disabled group ─────────────────────────────────────────────────────────── export const DisabledGroup: StoryObj = { render: () => ( option.id} getOptionLabel={option => option.label} aria-label='Disabled group' /> ), parameters: { docs: { description: { story: 'Setting disabled on the control disables every segment.', }, }, }, }; // ─── Uncontrolled ──────────────────────────────────────────────────────────── export const Uncontrolled: StoryObj = { render: () => ( option.id} getOptionLabel={option => option.label} aria-label='Uncontrolled period selector' /> ), parameters: { docs: { description: { story: 'Uncontrolled — pass defaultValue and omit value/onChange. ' + 'The underlying RadioGroup manages selection state internally.', }, }, }, }; // ─── renderOption (custom content) ─────────────────────────────────────────── interface DensityOption { id: string; label: string; description: string; } const densityOptions: DensityOption[] = [ { id: 'comfortable', label: 'Pohodlné', description: 'Větší mezery' }, { id: 'cozy', label: 'Střední', description: 'Vyrovnané' }, { id: 'compact', label: 'Kompaktní', description: 'Více dat' }, ]; export const CustomRenderOption: StoryObj = { render: () => { const [value, setValue] = useState('cozy'); return ( option.id} getOptionLabel={option => option.label} renderOption={(option: DensityOption, { checked }: SegmentRenderContext) => ( {option.label} {option.description} )} value={value} onChange={v => setValue(v)} aria-label='Table density' /> ); }, parameters: { docs: { description: { story: 'renderOption replaces the entire default icon/label/badge layout. ' + 'Receives the option and a context with checked and disabled state. ' + 'The hidden radio input is always rendered before it.', }, }, }, }; // ─── Period selector (real-world) ──────────────────────────────────────────── export const PeriodSelector: StoryObj = { render: () => { const [period, setPeriod] = useState('month'); return (
Tržby option.id} getOptionLabel={option => option.label} value={period} onChange={v => setPeriod(v)} aria-label='Vyberte agregační období' />
); }, parameters: { docs: { description: { story: 'Compact sm period selector placed above a dashboard widget. ' + 'sm + textOnly is the canonical placement above charts.', }, }, }, }; // ─── View toggle (real-world) ──────────────────────────────────────────────── export const ViewToggle: StoryObj = { render: () => { const [view, setView] = useState('table'); const twoViewOptions = viewOptions.slice(0, 2); return ( option.id} getOptionLabel={option => option.label} getOptionIcon={option => [0]['icon']} />} value={view} onChange={v => setView(v)} aria-label='Přepnout zobrazení produktů' /> ); }, parameters: { docs: { description: { story: 'iconText md for switching between table and card views on a product list. ' + 'Selection persists within the section without page navigation.', }, }, }, };