@boostdev/design-system-components 1.1.2 → 1.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (169) hide show
  1. package/AGENTS.md +13 -10
  2. package/dist/client.cjs +77 -99
  3. package/dist/client.css +611 -580
  4. package/dist/client.d.cts +13 -16
  5. package/dist/client.d.ts +13 -16
  6. package/dist/client.js +87 -109
  7. package/dist/index.cjs +77 -99
  8. package/dist/index.css +611 -580
  9. package/dist/index.d.cts +13 -16
  10. package/dist/index.d.ts +13 -16
  11. package/dist/index.js +87 -109
  12. package/dist/native/index.cjs +9 -8
  13. package/dist/native/index.d.cts +1 -1
  14. package/dist/native/index.d.ts +1 -1
  15. package/dist/native/index.js +9 -8
  16. package/dist/web-components/{chunk-O4UFI2RX.js → chunk-3REOIRDW.js} +13 -11
  17. package/dist/web-components/{chunk-K5CE6HSB.js → chunk-6MH5UWUD.js} +74 -17
  18. package/dist/web-components/{chunk-VM3YHGQ2.js → chunk-AJSXNDAP.js} +1 -2
  19. package/dist/web-components/{chunk-LSC4N56Q.js → chunk-BX3IPVOB.js} +1 -2
  20. package/dist/web-components/{chunk-CJ5ZOO42.js → chunk-DI46Q2EA.js} +50 -1
  21. package/dist/web-components/{chunk-CAPI25CU.js → chunk-DZRSJGPB.js} +3 -3
  22. package/dist/web-components/{chunk-VSS5ITNE.js → chunk-EBJM3VD4.js} +1 -3
  23. package/dist/web-components/{chunk-O7I63SQX.js → chunk-HHHRF2PS.js} +6 -12
  24. package/dist/web-components/{chunk-OTX4COSV.js → chunk-JUKB3BUP.js} +3 -4
  25. package/dist/web-components/{chunk-IQGLIQSO.js → chunk-N6IMUOY4.js} +2 -5
  26. package/dist/web-components/{chunk-QDRH46TF.js → chunk-QLZ2362S.js} +1 -3
  27. package/dist/web-components/{chunk-IK3PTFZ4.js → chunk-T6AETZRP.js} +1 -3
  28. package/dist/web-components/{chunk-4BX6AWXX.js → chunk-X3FKVHLK.js} +2 -4
  29. package/dist/web-components/{chunk-A67LCT7C.js → chunk-XNA6WTXG.js} +3 -4
  30. package/dist/web-components/{chunk-XIENXR7H.js → chunk-YRXCVKHV.js} +1 -3
  31. package/dist/web-components/{chunk-6T3RKUKG.js → chunk-ZTC6GRP7.js} +1 -2
  32. package/dist/web-components/globals.js +1 -1
  33. package/dist/web-components/index.js +16 -16
  34. package/dist/web-components/interaction/bds-accordion.js +1 -1
  35. package/dist/web-components/interaction/bds-button.d.ts +2 -2
  36. package/dist/web-components/interaction/bds-button.js +1 -1
  37. package/dist/web-components/interaction/bds-dialog.js +1 -1
  38. package/dist/web-components/interaction/bds-drawer.js +1 -1
  39. package/dist/web-components/interaction/form/bds-checkbox.js +1 -1
  40. package/dist/web-components/interaction/form/bds-combobox.js +1 -1
  41. package/dist/web-components/interaction/form/bds-number-input.js +1 -1
  42. package/dist/web-components/interaction/form/bds-radio.js +1 -1
  43. package/dist/web-components/interaction/form/bds-segmented-control.d.ts +20 -3
  44. package/dist/web-components/interaction/form/bds-segmented-control.js +1 -1
  45. package/dist/web-components/interaction/form/bds-select.js +1 -1
  46. package/dist/web-components/interaction/form/bds-switch.js +1 -1
  47. package/dist/web-components/interaction/form/bds-textarea.js +1 -1
  48. package/dist/web-components/ui/bds-alert.js +1 -1
  49. package/dist/web-components/ui/bds-card.js +1 -1
  50. package/dist/web-components/ui/bds-notification-banner.js +1 -1
  51. package/package.json +2 -2
  52. package/src/components/interaction/Button/Button.module.css +12 -11
  53. package/src/components/interaction/Button/Button.native.tsx +10 -9
  54. package/src/components/interaction/Button/Button.tsx +1 -1
  55. package/src/components/interaction/Command/Command.module.css +3 -7
  56. package/src/components/interaction/Dialog/Dialog.module.css +7 -5
  57. package/src/components/interaction/Drawer/Drawer.module.css +49 -1
  58. package/src/components/interaction/DropdownMenu/DropdownMenu.module.css +1 -3
  59. package/src/components/interaction/Popover/Popover.module.css +1 -3
  60. package/src/components/interaction/form/Checkbox/Checkbox.module.css +1 -3
  61. package/src/components/interaction/form/Combobox/Combobox.module.css +2 -6
  62. package/src/components/interaction/form/FormInput/FormInput.module.css +1 -3
  63. package/src/components/interaction/form/NumberInput/NumberInput.module.css +16 -4
  64. package/src/components/interaction/form/Radio/Radio.module.css +6 -16
  65. package/src/components/interaction/form/SegmentedControl/SegmentedControl.mdx +70 -25
  66. package/src/components/interaction/form/SegmentedControl/SegmentedControl.module.css +55 -33
  67. package/src/components/interaction/form/SegmentedControl/SegmentedControl.spec.tsx +127 -56
  68. package/src/components/interaction/form/SegmentedControl/SegmentedControl.stories.tsx +89 -72
  69. package/src/components/interaction/form/SegmentedControl/SegmentedControl.tsx +40 -62
  70. package/src/components/interaction/form/SegmentedControl/index.ts +1 -1
  71. package/src/components/interaction/form/Select/Select.module.css +2 -4
  72. package/src/components/interaction/form/Switch/Switch.module.css +5 -7
  73. package/src/components/interaction/form/Switch/Switch.native.spec.tsx +9 -9
  74. package/src/components/interaction/form/Switch/Switch.native.tsx +2 -2
  75. package/src/components/interaction/form/Textarea/Textarea.module.css +1 -3
  76. package/src/components/interaction/form/atoms/InputContainer.stories.tsx +64 -0
  77. package/src/components/interaction/form/atoms/Label.stories.tsx +33 -0
  78. package/src/components/interaction/form/atoms/Message.stories.tsx +33 -0
  79. package/src/components/layout/Card/Card.module.css +1 -3
  80. package/src/components/ui/Accordion/Accordion.module.css +1 -2
  81. package/src/components/ui/Alert/Alert.module.css +1 -2
  82. package/src/components/ui/Calendar/Calendar.module.css +2 -5
  83. package/src/components/ui/Carousel/Carousel.module.css +1 -3
  84. package/src/components/ui/NotificationBanner/NotificationBanner.module.css +1 -2
  85. package/src/components/ui/Pagination/Pagination.module.css +1 -2
  86. package/src/components/ui/SkipLink/SkipLink.module.css +1 -2
  87. package/src/components/ui/Table/Table.module.css +1 -2
  88. package/src/css/bdc.css +16 -11
  89. package/src/index.ts +1 -1
  90. package/src/web-components/globals.ts +3 -3
  91. package/src/web-components/interaction/BdsAccordion.mdx +51 -0
  92. package/src/web-components/interaction/BdsAccordion.stories.tsx +85 -0
  93. package/src/web-components/interaction/BdsCollapsible.mdx +46 -0
  94. package/src/web-components/interaction/BdsCollapsible.stories.tsx +87 -0
  95. package/src/web-components/interaction/BdsDialog.mdx +49 -0
  96. package/src/web-components/interaction/BdsDialog.stories.tsx +75 -0
  97. package/src/web-components/interaction/BdsDrawer.mdx +50 -0
  98. package/src/web-components/interaction/BdsDrawer.stories.tsx +66 -0
  99. package/src/web-components/interaction/BdsSkipLink.mdx +34 -0
  100. package/src/web-components/interaction/BdsSkipLink.stories.tsx +55 -0
  101. package/src/web-components/interaction/BdsTabs.mdx +47 -0
  102. package/src/web-components/interaction/BdsTabs.stories.tsx +80 -0
  103. package/src/web-components/interaction/BdsTooltip.mdx +38 -0
  104. package/src/web-components/interaction/BdsTooltip.stories.tsx +68 -0
  105. package/src/web-components/interaction/bds-accordion.ts +1 -2
  106. package/src/web-components/interaction/bds-button.ts +15 -13
  107. package/src/web-components/interaction/bds-dialog.ts +2 -5
  108. package/src/web-components/interaction/bds-drawer.ts +50 -1
  109. package/src/web-components/interaction/form/BdsCheckbox.mdx +39 -0
  110. package/src/web-components/interaction/form/BdsCheckbox.stories.tsx +73 -0
  111. package/src/web-components/interaction/form/BdsCombobox.mdx +52 -0
  112. package/src/web-components/interaction/form/BdsCombobox.stories.tsx +91 -0
  113. package/src/web-components/interaction/form/BdsFileInput.mdx +44 -0
  114. package/src/web-components/interaction/form/BdsFileInput.stories.tsx +84 -0
  115. package/src/web-components/interaction/form/BdsNumberInput.mdx +41 -0
  116. package/src/web-components/interaction/form/BdsNumberInput.stories.tsx +90 -0
  117. package/src/web-components/interaction/form/BdsRadio.mdx +41 -0
  118. package/src/web-components/interaction/form/BdsRadio.stories.tsx +90 -0
  119. package/src/web-components/interaction/form/BdsSegmentedControl.mdx +42 -0
  120. package/src/web-components/interaction/form/BdsSegmentedControl.stories.tsx +92 -0
  121. package/src/web-components/interaction/form/BdsSelect.mdx +38 -0
  122. package/src/web-components/interaction/form/BdsSelect.stories.tsx +93 -0
  123. package/src/web-components/interaction/form/BdsSlider.mdx +49 -0
  124. package/src/web-components/interaction/form/BdsSlider.stories.tsx +89 -0
  125. package/src/web-components/interaction/form/BdsSwitch.mdx +39 -0
  126. package/src/web-components/interaction/form/BdsSwitch.stories.tsx +73 -0
  127. package/src/web-components/interaction/form/BdsTextarea.mdx +41 -0
  128. package/src/web-components/interaction/form/BdsTextarea.stories.tsx +79 -0
  129. package/src/web-components/interaction/form/bds-checkbox.ts +1 -3
  130. package/src/web-components/interaction/form/bds-combobox.ts +1 -3
  131. package/src/web-components/interaction/form/bds-number-input.ts +3 -4
  132. package/src/web-components/interaction/form/bds-radio.ts +6 -12
  133. package/src/web-components/interaction/form/bds-segmented-control.ts +91 -20
  134. package/src/web-components/interaction/form/bds-select.ts +2 -4
  135. package/src/web-components/interaction/form/bds-switch.ts +3 -4
  136. package/src/web-components/interaction/form/bds-textarea.ts +1 -3
  137. package/src/web-components/ui/BdsAvatar.mdx +67 -0
  138. package/src/web-components/ui/BdsAvatar.stories.tsx +62 -0
  139. package/src/web-components/ui/BdsBreadcrumb.mdx +63 -0
  140. package/src/web-components/ui/BdsBreadcrumb.stories.tsx +59 -0
  141. package/src/web-components/ui/BdsCard.mdx +83 -0
  142. package/src/web-components/ui/BdsCard.stories.tsx +90 -0
  143. package/src/web-components/ui/BdsDescriptionList.mdx +67 -0
  144. package/src/web-components/ui/BdsDescriptionList.stories.tsx +79 -0
  145. package/src/web-components/ui/BdsIconWrapper.mdx +70 -0
  146. package/src/web-components/ui/BdsIconWrapper.stories.tsx +76 -0
  147. package/src/web-components/ui/BdsLink.mdx +75 -0
  148. package/src/web-components/ui/BdsLink.stories.tsx +64 -0
  149. package/src/web-components/ui/BdsLoading.mdx +62 -0
  150. package/src/web-components/ui/BdsLoading.stories.tsx +37 -0
  151. package/src/web-components/ui/BdsNotificationBanner.mdx +90 -0
  152. package/src/web-components/ui/BdsNotificationBanner.stories.tsx +93 -0
  153. package/src/web-components/ui/BdsProgress.mdx +63 -0
  154. package/src/web-components/ui/BdsProgress.stories.tsx +70 -0
  155. package/src/web-components/ui/BdsProgressCircle.mdx +64 -0
  156. package/src/web-components/ui/BdsProgressCircle.stories.tsx +69 -0
  157. package/src/web-components/ui/BdsRating.mdx +61 -0
  158. package/src/web-components/ui/BdsRating.stories.tsx +39 -0
  159. package/src/web-components/ui/BdsSectionHeader.mdx +69 -0
  160. package/src/web-components/ui/BdsSectionHeader.stories.tsx +56 -0
  161. package/src/web-components/ui/BdsSeparator.mdx +63 -0
  162. package/src/web-components/ui/BdsSeparator.stories.tsx +47 -0
  163. package/src/web-components/ui/BdsSkeleton.mdx +67 -0
  164. package/src/web-components/ui/BdsSkeleton.stories.tsx +50 -0
  165. package/src/web-components/ui/BdsTypography.mdx +83 -0
  166. package/src/web-components/ui/BdsTypography.stories.tsx +51 -0
  167. package/src/web-components/ui/bds-alert.ts +1 -2
  168. package/src/web-components/ui/bds-card.ts +1 -3
  169. package/src/web-components/ui/bds-notification-banner.ts +1 -2
@@ -6,105 +6,122 @@ const meta = {
6
6
  title: 'React/Form/SegmentedControl',
7
7
  component: SegmentedControl,
8
8
  argTypes: {
9
- size: { control: 'radio', options: ['small', 'medium', 'large'] },
9
+ size: { control: 'radio', options: ['small', 'medium', 'large'] },
10
+ variant: { control: 'radio', options: ['outline', 'filled'] },
10
11
  },
11
12
  } satisfies Meta<typeof SegmentedControl>;
12
13
 
13
14
  export default meta;
14
15
  type Story = StoryObj<typeof meta>;
15
16
 
17
+ /** Default: outline variant (default), buttons driven by selectedIndex */
16
18
  export const Default: Story = {
17
- args: {
18
- name: 'view',
19
- options: [
20
- { value: 'day', label: 'Day' },
21
- { value: 'week', label: 'Week' },
22
- { value: 'month', label: 'Month' },
23
- ],
24
- defaultValue: 'week',
19
+ render: ({ size, variant, disabled }) => {
20
+ const [tab, setTab] = useState(1);
21
+ return (
22
+ <SegmentedControl selectedIndex={tab} size={size} variant={variant} disabled={disabled}>
23
+ <button onClick={() => setTab(0)}>Day</button>
24
+ <button onClick={() => setTab(1)}>Week</button>
25
+ <button onClick={() => setTab(2)}>Month</button>
26
+ </SegmentedControl>
27
+ );
25
28
  },
26
29
  };
27
30
 
28
- export const Small: Story = {
29
- args: {
30
- name: 'size-s',
31
- size: 'small',
32
- options: [
33
- { value: 'list', label: 'List' },
34
- { value: 'grid', label: 'Grid' },
35
- ],
36
- defaultValue: 'list',
31
+ /** Filled variant: sliding filled thumb */
32
+ export const Filled: Story = {
33
+ render: ({ size, disabled }) => {
34
+ const [tab, setTab] = useState(1);
35
+ return (
36
+ <SegmentedControl selectedIndex={tab} size={size} variant="filled" disabled={disabled}>
37
+ <button onClick={() => setTab(0)}>Day</button>
38
+ <button onClick={() => setTab(1)}>Week</button>
39
+ <button onClick={() => setTab(2)}>Month</button>
40
+ </SegmentedControl>
41
+ );
37
42
  },
38
43
  };
39
44
 
40
- export const Large: Story = {
41
- args: {
42
- name: 'size-l',
43
- size: 'large',
44
- options: [
45
- { value: 'personal', label: 'Personal plan' },
46
- { value: 'team', label: 'Team plan' },
47
- { value: 'enterprise', label: 'Enterprise plan' },
48
- ],
49
- defaultValue: 'team',
45
+ /**
46
+ * Links: active item is auto-detected from aria-current="page".
47
+ * No selectedIndex needed — the component reads the ARIA attribute.
48
+ */
49
+ export const WithLinks: Story = {
50
+ render: ({ size, variant, disabled }) => {
51
+ const [active, setActive] = useState('overview');
52
+ return (
53
+ <SegmentedControl size={size} variant={variant} disabled={disabled}>
54
+ <a href="#" aria-current={active === 'overview' ? 'page' : undefined} onClick={() => setActive('overview')}>Overview</a>
55
+ <a href="#" aria-current={active === 'usage' ? 'page' : undefined} onClick={() => setActive('usage')}>Usage</a>
56
+ <a href="#" aria-current={active === 'api' ? 'page' : undefined} onClick={() => setActive('api')}>API</a>
57
+ </SegmentedControl>
58
+ );
50
59
  },
51
60
  };
52
61
 
53
- export const WrappingLabels: Story = {
54
- args: {
55
- name: 'wrap',
56
- size: 'large',
57
- options: [
58
- { value: 'solar', label: 'Solar energy only' },
59
- { value: 'wind', label: 'Wind energy only' },
60
- { value: 'mixed', label: 'Mix of sustainable sources' },
61
- ],
62
- defaultValue: 'mixed',
62
+ /** Small size */
63
+ export const Small: Story = {
64
+ render: () => {
65
+ const [tab, setTab] = useState(0);
66
+ return (
67
+ <SegmentedControl selectedIndex={tab} size="small">
68
+ <button onClick={() => setTab(0)}>List</button>
69
+ <button onClick={() => setTab(1)}>Grid</button>
70
+ </SegmentedControl>
71
+ );
63
72
  },
64
- decorators: [Story => <div style={{ maxWidth: '360px' }}><Story /></div>],
65
73
  };
66
74
 
67
- export const WithDisabledOption: Story = {
68
- args: {
69
- name: 'disabled-opt',
70
- options: [
71
- { value: 'active', label: 'Active' },
72
- { value: 'paused', label: 'Paused' },
73
- { value: 'archived', label: 'Archived', disabled: true },
74
- ],
75
- defaultValue: 'active',
75
+ /** Large size with wrapping labels */
76
+ export const Large: Story = {
77
+ render: () => {
78
+ const [tab, setTab] = useState(1);
79
+ return (
80
+ <SegmentedControl selectedIndex={tab} size="large">
81
+ <button onClick={() => setTab(0)}>Personal plan</button>
82
+ <button onClick={() => setTab(1)}>Team plan</button>
83
+ <button onClick={() => setTab(2)}>Enterprise plan</button>
84
+ </SegmentedControl>
85
+ );
76
86
  },
77
87
  };
78
88
 
79
- export const AllDisabled: Story = {
80
- args: {
81
- name: 'all-disabled',
82
- options: [
83
- { value: 'a', label: 'Option A' },
84
- { value: 'b', label: 'Option B' },
85
- ],
86
- defaultValue: 'a',
87
- disabled: true,
89
+ /** Large with wrapping text in a constrained container */
90
+ export const WrappingLabels: Story = {
91
+ render: () => {
92
+ const [tab, setTab] = useState(2);
93
+ return (
94
+ <div style={{ maxWidth: '360px' }}>
95
+ <SegmentedControl selectedIndex={tab} size="large">
96
+ <button onClick={() => setTab(0)}>Solar energy only</button>
97
+ <button onClick={() => setTab(1)}>Wind energy only</button>
98
+ <button onClick={() => setTab(2)}>Mix of sustainable sources</button>
99
+ </SegmentedControl>
100
+ </div>
101
+ );
88
102
  },
89
103
  };
90
104
 
91
- export const Controlled: Story = {
105
+ /** Disable the entire control */
106
+ export const AllDisabled: Story = {
107
+ render: () => (
108
+ <SegmentedControl selectedIndex={0} disabled>
109
+ <button>Option A</button>
110
+ <button>Option B</button>
111
+ </SegmentedControl>
112
+ ),
113
+ };
114
+
115
+ /** aria-pressed drives auto-detection — no selectedIndex needed */
116
+ export const WithAriaPressed: Story = {
92
117
  render: () => {
93
- const [value, setValue] = useState('month');
118
+ const [tab, setTab] = useState('week');
94
119
  return (
95
- <div style={{ display: 'flex', flexDirection: 'column', gap: '1rem', alignItems: 'flex-start' }}>
96
- <SegmentedControl
97
- name="controlled"
98
- value={value}
99
- onChange={setValue}
100
- options={[
101
- { value: 'day', label: 'Day' },
102
- { value: 'week', label: 'Week' },
103
- { value: 'month', label: 'Month' },
104
- ]}
105
- />
106
- <span style={{ fontSize: '0.875rem', opacity: 0.6 }}>Selected: {value}</span>
107
- </div>
120
+ <SegmentedControl>
121
+ <button onClick={() => setTab('day')} aria-pressed={tab === 'day'}>Day</button>
122
+ <button onClick={() => setTab('week')} aria-pressed={tab === 'week'}>Week</button>
123
+ <button onClick={() => setTab('month')} aria-pressed={tab === 'month'}>Month</button>
124
+ </SegmentedControl>
108
125
  );
109
126
  },
110
127
  };
@@ -1,92 +1,70 @@
1
- import { InputHTMLAttributes, ReactNode, useId, useState } from 'react';
1
+ import { Children, ReactElement, ReactNode, cloneElement, isValidElement } from 'react';
2
2
  import css from './SegmentedControl.module.css';
3
3
  import { cn } from '@boostdev/design-system-foundation';
4
4
  import type { WithClassName } from '../../../../types';
5
5
 
6
- export interface SegmentedControlOption {
7
- value: string;
8
- label: ReactNode;
9
- disabled?: boolean;
10
- }
11
-
12
- export interface SegmentedControlProps
13
- extends WithClassName, Omit<InputHTMLAttributes<HTMLInputElement>, 'onChange' | 'value' | 'size'> {
14
- name: string;
15
- options: SegmentedControlOption[];
16
- /** Controlled selected value */
17
- value?: string;
18
- /** Uncontrolled initial value (defaults to first option) */
19
- defaultValue?: string;
20
- onChange?: (value: string) => void;
6
+ export interface SegmentedControlProps extends WithClassName {
7
+ children: ReactNode;
8
+ /**
9
+ * Zero-based index of the active item.
10
+ * When omitted, auto-detected from the first child that has
11
+ * aria-current="page", aria-pressed={true}, or aria-selected={true}.
12
+ */
13
+ selectedIndex?: number;
21
14
  disabled?: boolean;
22
15
  size?: 'small' | 'medium' | 'large';
16
+ /** "outline" (default) — sliding inset border; "filled" — sliding filled thumb */
17
+ variant?: 'outline' | 'filled';
23
18
  'aria-label'?: string;
24
19
  }
25
20
 
26
21
  export function SegmentedControl({
27
- name,
28
- options,
29
- value,
30
- defaultValue,
31
- onChange,
22
+ children,
23
+ selectedIndex: selectedIndexProp,
32
24
  disabled,
33
25
  size = 'medium',
26
+ variant = 'outline',
34
27
  className,
35
28
  'aria-label': ariaLabel,
36
- ...rest
37
29
  }: Readonly<SegmentedControlProps>) {
38
- const baseId = name + useId();
30
+ const validChildren = Children.toArray(children).filter(isValidElement) as ReactElement<Record<string, unknown>>[];
39
31
 
40
- const [internalValue, setInternalValue] = useState<string>(
41
- defaultValue ?? options[0]?.value ?? '',
42
- );
43
-
44
- const activeValue = value ?? internalValue;
45
- const selectedIndex = options.findIndex(o => o.value === activeValue);
32
+ // Auto-detect active index from standard ARIA attributes if not explicitly provided.
33
+ const autoIndex = selectedIndexProp === undefined
34
+ ? validChildren.findIndex(child => {
35
+ const p = child.props as Record<string, unknown>;
36
+ return p['aria-current'] === 'page' || p['aria-pressed'] === true || p['aria-selected'] === true;
37
+ })
38
+ : -1;
46
39
 
47
- const handleChange = (optionValue: string) => {
48
- if (value === undefined) setInternalValue(optionValue);
49
- onChange?.(optionValue);
50
- };
40
+ const activeIndex = Math.max(0, selectedIndexProp ?? (autoIndex >= 0 ? autoIndex : 0));
51
41
 
52
42
  return (
53
43
  <div
54
44
  role="group"
55
45
  aria-label={ariaLabel}
56
- className={cn(css.control, css[`--size_${size}`], className)}
46
+ className={cn(css.control, css[`--size_${size}`], css[`--variant_${variant}`], className)}
57
47
  style={{
58
- '--control_count': options.length,
59
- '--control_selected-index': Math.max(0, selectedIndex),
48
+ '--control_count': validChildren.length,
49
+ '--control_selected-index': activeIndex,
60
50
  } as React.CSSProperties}
61
51
  >
62
- {/* Sliding thumb — purely visual */}
63
52
  <span className={css.thumb} aria-hidden="true" />
53
+ <span className={css.indicator} aria-hidden="true" />
64
54
 
65
- {options.map((option, index) => {
66
- const id = `${baseId}-${index}`;
67
- const isChecked = option.value === activeValue;
68
- const isDisabled = disabled || option.disabled;
69
-
70
- return (
71
- <label
72
- key={option.value}
73
- htmlFor={id}
74
- className={cn(css.item, isChecked && css['--active'], isDisabled && css['--disabled'])}
75
- >
76
- <input
77
- {...rest}
78
- type="radio"
79
- id={id}
80
- name={name}
81
- value={option.value}
82
- checked={isChecked}
83
- disabled={isDisabled}
84
- className={css.radio}
85
- onChange={() => handleChange(option.value)}
86
- />
87
- <span className={css.label}>{option.label}</span>
88
- </label>
89
- );
55
+ {validChildren.map((child, index) => {
56
+ const isActive = index === activeIndex;
57
+ const p = child.props as Record<string, unknown>;
58
+ const isDisabled = disabled || !!p.disabled || p['aria-disabled'] === true;
59
+ return cloneElement(child, {
60
+ key: child.key ?? index,
61
+ className: cn(
62
+ css.item,
63
+ isActive && css['--active'],
64
+ isDisabled && css['--disabled'],
65
+ p.className as string | undefined,
66
+ ),
67
+ });
90
68
  })}
91
69
  </div>
92
70
  );
@@ -1,2 +1,2 @@
1
1
  export { SegmentedControl } from './SegmentedControl';
2
- export type { SegmentedControlProps, SegmentedControlOption } from './SegmentedControl';
2
+ export type { SegmentedControlProps } from './SegmentedControl';
@@ -21,10 +21,8 @@
21
21
  --bdc_color: currentcolor;
22
22
  --bdc-outline_radius: var(--bds-border_radius--xs);
23
23
 
24
- outline: var(--bdc-outline_width) solid var(--bdc_color);
25
- outline-offset: var(--bdc-outline_offset);
26
24
  border-radius: var(--select_radius, var(--bdc-outline_radius));
27
- box-shadow: var(--select_shadow, var(--bdc-outline_shadow));
25
+ box-shadow: inset 0 0 0 var(--bdc-outline_width) var(--bdc_color), var(--select_shadow, var(--bdc-outline_shadow));
28
26
  background-color: var(--select_color_bg, var(--bds-color_bg));
29
27
  color: var(--select_color, var(--bds-color_on-bg));
30
28
  cursor: pointer;
@@ -56,7 +54,7 @@
56
54
  display: flex;
57
55
  align-items: center;
58
56
  pointer-events: none;
59
- color: var(--select_color, var(--bds-color_on-bg));
57
+ color: var(--select_chevron-color, var(--bds-color_interactive));
60
58
  }
61
59
 
62
60
  .chevron svg {
@@ -56,14 +56,12 @@
56
56
  background-color: var(--switch_track-bg, var(--bds-color_grey--subtle));
57
57
  padding-inline: var(--switch_track-pad);
58
58
  pointer-events: none;
59
- outline: var(--bdc-outline_width) solid var(--bdc_color);
60
- outline-offset: var(--bds-outline_offset);
61
- box-shadow: var(--switch_shadow, var(--bdc-outline_shadow));
59
+ box-shadow: inset 0 0 0 var(--bdc-outline_width) var(--bdc_color), var(--switch_shadow, var(--bdc-outline_shadow));
62
60
  transition: --bdc_color var(--bds-animation_transition-duration) var(--bds-animation_easing),
63
61
  background-color var(--bds-animation_transition-duration) var(--bds-animation_easing),
64
62
  transform var(--bds-animation_transition-duration) var(--bds-animation_easing);
65
63
 
66
- --bdc_color: var(--switch_thumb-bg, var(--bds-color_grey));
64
+ --bdc_color: var(--switch_thumb-bg, var(--bds-color_interactive));
67
65
  }
68
66
 
69
67
  /* Thumb */
@@ -72,7 +70,7 @@
72
70
  inline-size: var(--switch_thumb-size);
73
71
  block-size: var(--switch_thumb-size);
74
72
  border-radius: 50%;
75
- background-color: var(--switch_thumb-bg, var(--bds-color_grey));
73
+ background-color: var(--switch_thumb-bg, var(--bds-color_interactive));
76
74
  box-shadow: var(--bds-shadow_s);
77
75
  transition: var(--bds-animation_transition);
78
76
  transform: translateX(0);
@@ -87,11 +85,11 @@
87
85
  .switch:checked + .track {
88
86
  background-color: var(--switch_track-bg--active, var(--bdc-color_active--subtle));
89
87
 
90
- --bdc_color: var(--switch_thumb-bg--active, var(--bdc-color_active--strong));
88
+ --bdc_color: var(--switch_thumb-bg--active, var(--bds-color_interactive));
91
89
  }
92
90
 
93
91
  .switch:checked + .track .thumb {
94
- background-color: var(--switch_thumb-bg--active, var(--bdc-color_active--strong));
92
+ background-color: var(--switch_thumb-bg--active, var(--bds-color_interactive));
95
93
  transform: translateX(var(--switch_thumb-size));
96
94
  }
97
95
 
@@ -38,17 +38,17 @@ describe('Switch (native)', () => {
38
38
  expect(getSwitchInput(container)).toBeChecked();
39
39
  });
40
40
 
41
- it('thumb uses colorOnBg when off (contrast regression: was colorBg on colorBgSubtle = 1.09:1)', () => {
42
- // In light mode: old thumbColor=colorBg=#fff on colorBgSubtle=#f3f4f6 = 1.09:1 (invisible).
43
- // Fixed: thumbColor=colorOnBg=#000 when off, which gives 19:1 contrast against #f3f4f6.
44
- const { container } = renderWithTheme(
41
+ it('renders thumb in both on and off states without error (thumb always uses colorInteractive)', () => {
42
+ // thumbColor is always colorInteractive interactive affordance is visible in both states.
43
+ // Track color provides the on/off visual feedback instead.
44
+ const { container: offContainer } = renderWithTheme(
45
45
  <Switch label="Toggle" name="toggle" value={false} />,
46
46
  );
47
- // The thumb div has its background-color set inline by react-native-web.
48
- const thumbDiv = container.querySelector<HTMLElement>('[style*="background-color"]');
49
- // Verify the component renders without error — visual contrast is verified by
50
- // the color token usage in source (colorOnBg), not the DOM attribute.
51
- expect(thumbDiv).toBeInTheDocument();
47
+ const { container: onContainer } = renderWithTheme(
48
+ <Switch label="Toggle2" name="toggle2" value={true} />,
49
+ );
50
+ expect(offContainer.querySelector('[style*="background-color"]')).toBeInTheDocument();
51
+ expect(onContainer.querySelector('[style*="background-color"]')).toBeInTheDocument();
52
52
  });
53
53
 
54
54
  it('renders hint message', () => {
@@ -47,8 +47,8 @@ export function Switch({
47
47
  accessibilityLabel={label}
48
48
  accessibilityRole="switch"
49
49
  accessibilityState={{ checked: value, disabled }}
50
- thumbColor={value ? colors.colorOnInteractive : colors.colorOnBg}
51
- trackColor={{ false: colors.colorBgSubtle, true: colors.colorInteractive }}
50
+ thumbColor={colors.colorInteractive}
51
+ trackColor={{ false: colors.colorBgSubtle, true: colors.colorBlueSubtle }}
52
52
  />
53
53
  <Label label={label} />
54
54
  </View>
@@ -13,10 +13,8 @@
13
13
  --bdc_color: currentcolor;
14
14
  --bdc-outline_radius: var(--bds-border_radius--xs);
15
15
 
16
- outline: var(--bdc-outline_width) solid var(--bdc_color);
17
- outline-offset: var(--bdc-outline_offset);
18
16
  border-radius: var(--textarea_radius, var(--bdc-outline_radius));
19
- box-shadow: var(--textarea_shadow, var(--bdc-outline_shadow));
17
+ box-shadow: inset 0 0 0 var(--bdc-outline_width) var(--bdc_color), var(--textarea_shadow, var(--bdc-outline_shadow));
20
18
  background-color: var(--textarea_color_bg, var(--bds-color_bg));
21
19
  color: var(--textarea_color, var(--bds-color_on-bg));
22
20
  resize: vertical;
@@ -0,0 +1,64 @@
1
+ import React from 'react';
2
+ import type { Meta, StoryObj } from '@storybook/react';
3
+ import { InputContainer } from './InputContainer';
4
+ import { Label } from './Label';
5
+ import { Message } from './Message';
6
+
7
+ const meta = {
8
+ title: 'React/Form/Atoms/InputContainer',
9
+ component: InputContainer,
10
+ parameters: { layout: 'padded' },
11
+ } satisfies Meta<typeof InputContainer>;
12
+
13
+ export default meta;
14
+ type Story = StoryObj<typeof meta>;
15
+
16
+ export const Default: Story = {
17
+ render: () => (
18
+ <InputContainer>
19
+ <Label id="demo-input" label="Email address" />
20
+ <input id="demo-input" type="email" placeholder="you@example.com" style={{ display: 'block', width: '100%', padding: '8px', boxSizing: 'border-box' }} />
21
+ </InputContainer>
22
+ ),
23
+ };
24
+
25
+ export const WithHint: Story = {
26
+ render: () => (
27
+ <InputContainer>
28
+ <Label id="demo-hint" label="Username" />
29
+ <input id="demo-hint" type="text" placeholder="Enter username" style={{ display: 'block', width: '100%', padding: '8px', boxSizing: 'border-box' }} />
30
+ <Message inputId="demo-hint" type="hint" message="At least 3 characters." />
31
+ </InputContainer>
32
+ ),
33
+ };
34
+
35
+ export const WithError: Story = {
36
+ render: () => (
37
+ <InputContainer>
38
+ <Label id="demo-error" label="Password" />
39
+ <input id="demo-error" type="password" placeholder="Enter password" style={{ display: 'block', width: '100%', padding: '8px', boxSizing: 'border-box' }} />
40
+ <Message inputId="demo-error" type="error" message="Password is too short." />
41
+ </InputContainer>
42
+ ),
43
+ };
44
+
45
+ export const AllVariants: Story = {
46
+ render: () => (
47
+ <div style={{ display: 'flex', flexDirection: 'column', gap: '16px', maxWidth: '320px' }}>
48
+ <InputContainer>
49
+ <Label id="v1" label="Plain input" />
50
+ <input id="v1" type="text" placeholder="Default" style={{ display: 'block', width: '100%', padding: '8px', boxSizing: 'border-box' }} />
51
+ </InputContainer>
52
+ <InputContainer>
53
+ <Label id="v2" label="With hint" />
54
+ <input id="v2" type="text" placeholder="Hint below" style={{ display: 'block', width: '100%', padding: '8px', boxSizing: 'border-box' }} />
55
+ <Message inputId="v2" type="hint" message="Helpful hint text." />
56
+ </InputContainer>
57
+ <InputContainer>
58
+ <Label id="v3" label="With error" />
59
+ <input id="v3" type="text" placeholder="Error state" style={{ display: 'block', width: '100%', padding: '8px', boxSizing: 'border-box' }} />
60
+ <Message inputId="v3" type="error" message="This field has an error." />
61
+ </InputContainer>
62
+ </div>
63
+ ),
64
+ };
@@ -0,0 +1,33 @@
1
+ import type { Meta, StoryObj } from '@storybook/react';
2
+ import { Label } from './Label';
3
+
4
+ const meta = {
5
+ title: 'React/Form/Atoms/Label',
6
+ component: Label,
7
+ parameters: { layout: 'padded' },
8
+ } satisfies Meta<typeof Label>;
9
+
10
+ export default meta;
11
+ type Story = StoryObj<typeof meta>;
12
+
13
+ export const Default: Story = { args: { id: 'input-1', label: 'Email address' } };
14
+ export const Required: Story = {
15
+ args: {
16
+ id: 'input-required',
17
+ label: (
18
+ <>
19
+ Full name <span aria-hidden="true" style={{ color: 'red' }}>*</span>
20
+ </>
21
+ ),
22
+ },
23
+ };
24
+
25
+ export const AllVariants: Story = {
26
+ render: () => (
27
+ <div style={{ display: 'flex', flexDirection: 'column', gap: '12px' }}>
28
+ <Label id="a" label="Plain label" />
29
+ <Label id="b" label="Label for a text input" />
30
+ <Label id="c" label={<><strong>Bold</strong> label with markup</>} />
31
+ </div>
32
+ ),
33
+ };
@@ -0,0 +1,33 @@
1
+ import type { Meta, StoryObj } from '@storybook/react';
2
+ import { Message } from './Message';
3
+
4
+ const meta = {
5
+ title: 'React/Form/Atoms/Message',
6
+ component: Message,
7
+ parameters: { layout: 'padded' },
8
+ argTypes: {
9
+ type: { control: 'select', options: ['hint', 'error'] },
10
+ },
11
+ } satisfies Meta<typeof Message>;
12
+
13
+ export default meta;
14
+ type Story = StoryObj<typeof meta>;
15
+
16
+ export const Hint: Story = {
17
+ args: { inputId: 'field-1', type: 'hint', message: 'Password must be at least 8 characters.' },
18
+ };
19
+ export const Error: Story = {
20
+ args: { inputId: 'field-2', type: 'error', message: 'This field is required.' },
21
+ };
22
+ export const NoMessage: Story = {
23
+ args: { inputId: 'field-3', type: 'hint', message: undefined },
24
+ };
25
+
26
+ export const AllVariants: Story = {
27
+ render: () => (
28
+ <div style={{ display: 'flex', flexDirection: 'column', gap: '8px' }}>
29
+ <Message inputId="v1" type="hint" message="Hint: Enter your full legal name." />
30
+ <Message inputId="v2" type="error" message="Error: This field is required." />
31
+ </div>
32
+ ),
33
+ };
@@ -19,9 +19,7 @@
19
19
  border: var(--card_border, none);
20
20
  --bdc_color: currentcolor;
21
21
 
22
- outline: var(--bdc-outline_width) solid var(--bdc_color);
23
- outline-offset: var(--bdc-outline_offset);
24
- box-shadow: var(--bds-shadow_s);
22
+ box-shadow: inset 0 0 0 var(--bdc-outline_width) var(--bdc_color), var(--bds-shadow_s);
25
23
  }
26
24
 
27
25
  .card.--clickable {
@@ -6,9 +6,8 @@
6
6
  --bdc_color: var(--bds-color_bg--subtle);
7
7
  --bdc-outline_radius: var(--bds-border_radius--s);
8
8
 
9
- outline: var(--bdc-outline_width) solid var(--bdc_color);
10
- outline-offset: var(--bdc-outline_offset);
11
9
  border-radius: var(--accordion_radius, var(--bdc-outline_radius));
10
+ box-shadow: inset 0 0 0 var(--bdc-outline_width) var(--bdc_color);
12
11
  overflow: hidden;
13
12
  }
14
13
 
@@ -68,8 +68,7 @@
68
68
  border-radius: 50%;
69
69
  --bdc_color: currentcolor;
70
70
 
71
- outline: var(--bdc-outline_width) solid var(--bdc_color);
72
- outline-offset: var(--bdc-outline_offset);
71
+ box-shadow: inset 0 0 0 var(--bdc-outline_width) var(--bdc_color);
73
72
 
74
73
  }
75
74
 
@@ -8,11 +8,9 @@
8
8
  --bdc_color: var(--bds-color_bg--subtle);
9
9
  --bdc-outline_radius: var(--bds-border_radius--m);
10
10
 
11
- outline: var(--bdc-outline_width) solid var(--bdc_color);
12
- outline-offset: var(--bdc-outline_offset);
13
11
  border-radius: var(--calendar_radius, var(--bdc-outline_radius));
14
12
  padding: var(--bds-space_m);
15
- box-shadow: var(--calendar_shadow, var(--bds-shadow_s));
13
+ box-shadow: inset 0 0 0 var(--bdc-outline_width) var(--bdc_color), var(--calendar_shadow, var(--bds-shadow_s));
16
14
  inline-size: 20rem;
17
15
  }
18
16
 
@@ -107,8 +105,7 @@
107
105
  --bdc_color: var(--bds-color_interactive);
108
106
 
109
107
  border: var(--calendar_today-border, none);
110
- outline: var(--bdc-outline_width) solid var(--bdc_color);
111
- outline-offset: var(--bdc-outline_offset);
108
+ box-shadow: inset 0 0 0 var(--bdc-outline_width) var(--bdc_color);
112
109
  }
113
110
 
114
111
  .selected {