@ds-mo/ui 0.1.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 (213) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +81 -0
  3. package/dist/index.css +1 -0
  4. package/dist/index.d.ts +969 -0
  5. package/dist/index.js +2623 -0
  6. package/dist/index.js.map +1 -0
  7. package/dist/r/accordion.json +79 -0
  8. package/dist/r/badge.json +69 -0
  9. package/dist/r/banner.json +90 -0
  10. package/dist/r/breadcrumb.json +70 -0
  11. package/dist/r/button-group.json +65 -0
  12. package/dist/r/button.json +137 -0
  13. package/dist/r/card.json +82 -0
  14. package/dist/r/checkbox.json +91 -0
  15. package/dist/r/divider.json +57 -0
  16. package/dist/r/empty-state.json +69 -0
  17. package/dist/r/error-boundary.json +66 -0
  18. package/dist/r/fade.json +65 -0
  19. package/dist/r/field.json +71 -0
  20. package/dist/r/header.json +67 -0
  21. package/dist/r/input.json +77 -0
  22. package/dist/r/loader.json +54 -0
  23. package/dist/r/menu.json +130 -0
  24. package/dist/r/modal.json +93 -0
  25. package/dist/r/pagination.json +80 -0
  26. package/dist/r/radio.json +85 -0
  27. package/dist/r/registry.json +1600 -0
  28. package/dist/r/scrollbar.json +68 -0
  29. package/dist/r/select.json +93 -0
  30. package/dist/r/sidebar.json +105 -0
  31. package/dist/r/skeleton.json +72 -0
  32. package/dist/r/slider.json +87 -0
  33. package/dist/r/surface.json +97 -0
  34. package/dist/r/tab-group.json +72 -0
  35. package/dist/r/tab.json +71 -0
  36. package/dist/r/table.json +110 -0
  37. package/dist/r/tag.json +110 -0
  38. package/dist/r/text.json +94 -0
  39. package/dist/r/toast.json +77 -0
  40. package/dist/r/toggle-button-group.json +65 -0
  41. package/dist/r/toggle-button.json +94 -0
  42. package/dist/r/toggle.json +65 -0
  43. package/dist/r/tooltip.json +86 -0
  44. package/package.json +80 -0
  45. package/src/components/Accordion/Accordion.module.css +66 -0
  46. package/src/components/Accordion/Accordion.stories.tsx +95 -0
  47. package/src/components/Accordion/Accordion.tsx +122 -0
  48. package/src/components/Accordion/index.ts +2 -0
  49. package/src/components/Badge/Badge.module.css +6 -0
  50. package/src/components/Badge/Badge.stories.tsx +99 -0
  51. package/src/components/Badge/Badge.tsx +23 -0
  52. package/src/components/Badge/index.ts +2 -0
  53. package/src/components/Banner/Banner.module.css +67 -0
  54. package/src/components/Banner/Banner.stories.tsx +81 -0
  55. package/src/components/Banner/Banner.tsx +109 -0
  56. package/src/components/Banner/index.ts +2 -0
  57. package/src/components/Breadcrumb/Breadcrumb.module.css +46 -0
  58. package/src/components/Breadcrumb/Breadcrumb.stories.tsx +72 -0
  59. package/src/components/Breadcrumb/Breadcrumb.tsx +68 -0
  60. package/src/components/Breadcrumb/index.ts +2 -0
  61. package/src/components/Button/Button.module.css +343 -0
  62. package/src/components/Button/Button.stories.tsx +575 -0
  63. package/src/components/Button/Button.tsx +201 -0
  64. package/src/components/Button/index.ts +10 -0
  65. package/src/components/ButtonGroup/ButtonGroup.module.css +157 -0
  66. package/src/components/ButtonGroup/ButtonGroup.stories.tsx +384 -0
  67. package/src/components/ButtonGroup/ButtonGroup.tsx +122 -0
  68. package/src/components/ButtonGroup/index.ts +2 -0
  69. package/src/components/Card/Card.module.css +36 -0
  70. package/src/components/Card/Card.stories.tsx +78 -0
  71. package/src/components/Card/Card.tsx +53 -0
  72. package/src/components/Card/index.ts +2 -0
  73. package/src/components/Checkbox/Checkbox.module.css +47 -0
  74. package/src/components/Checkbox/Checkbox.stories.tsx +129 -0
  75. package/src/components/Checkbox/Checkbox.tsx +68 -0
  76. package/src/components/Checkbox/index.ts +2 -0
  77. package/src/components/Divider/Divider.module.css +19 -0
  78. package/src/components/Divider/Divider.stories.tsx +55 -0
  79. package/src/components/Divider/Divider.tsx +28 -0
  80. package/src/components/Divider/index.ts +2 -0
  81. package/src/components/EmptyState/EmptyState.module.css +8 -0
  82. package/src/components/EmptyState/EmptyState.stories.tsx +53 -0
  83. package/src/components/EmptyState/EmptyState.tsx +31 -0
  84. package/src/components/EmptyState/index.ts +2 -0
  85. package/src/components/ErrorBoundary/ErrorBoundary.module.css +18 -0
  86. package/src/components/ErrorBoundary/ErrorBoundary.stories.tsx +38 -0
  87. package/src/components/ErrorBoundary/ErrorBoundary.tsx +50 -0
  88. package/src/components/ErrorBoundary/index.ts +2 -0
  89. package/src/components/Fade/Fade.module.css +31 -0
  90. package/src/components/Fade/Fade.stories.tsx +35 -0
  91. package/src/components/Fade/Fade.tsx +33 -0
  92. package/src/components/Fade/index.ts +2 -0
  93. package/src/components/Field/Field.module.css +5 -0
  94. package/src/components/Field/Field.stories.tsx +111 -0
  95. package/src/components/Field/Field.tsx +22 -0
  96. package/src/components/Field/index.ts +2 -0
  97. package/src/components/Header/Header.module.css +33 -0
  98. package/src/components/Header/Header.stories.tsx +52 -0
  99. package/src/components/Header/Header.tsx +42 -0
  100. package/src/components/Header/index.ts +2 -0
  101. package/src/components/Input/Input.module.css +71 -0
  102. package/src/components/Input/Input.stories.tsx +135 -0
  103. package/src/components/Input/Input.tsx +76 -0
  104. package/src/components/Input/index.ts +2 -0
  105. package/src/components/LabelWrap/LabelWrap.module.css +34 -0
  106. package/src/components/LabelWrap/LabelWrap.tsx +32 -0
  107. package/src/components/LabelWrap/index.ts +1 -0
  108. package/src/components/Loader/Loader.module.css +9 -0
  109. package/src/components/Loader/Loader.stories.tsx +44 -0
  110. package/src/components/Loader/Loader.tsx +43 -0
  111. package/src/components/Loader/index.ts +2 -0
  112. package/src/components/Menu/DestructiveMenuItem.module.css +57 -0
  113. package/src/components/Menu/DestructiveMenuItem.tsx +64 -0
  114. package/src/components/Menu/Menu.module.css +49 -0
  115. package/src/components/Menu/Menu.stories.tsx +129 -0
  116. package/src/components/Menu/Menu.tsx +269 -0
  117. package/src/components/Menu/MenuItem.module.css +84 -0
  118. package/src/components/Menu/MenuItem.tsx +101 -0
  119. package/src/components/Menu/index.ts +6 -0
  120. package/src/components/Modal/Modal.module.css +67 -0
  121. package/src/components/Modal/Modal.stories.tsx +71 -0
  122. package/src/components/Modal/Modal.tsx +122 -0
  123. package/src/components/Modal/index.ts +2 -0
  124. package/src/components/Pagination/Pagination.module.css +64 -0
  125. package/src/components/Pagination/Pagination.stories.tsx +51 -0
  126. package/src/components/Pagination/Pagination.tsx +103 -0
  127. package/src/components/Pagination/index.ts +2 -0
  128. package/src/components/Radio/Radio.module.css +61 -0
  129. package/src/components/Radio/Radio.stories.tsx +101 -0
  130. package/src/components/Radio/Radio.tsx +108 -0
  131. package/src/components/Radio/index.ts +2 -0
  132. package/src/components/Scrollbar/Scrollbar.module.css +81 -0
  133. package/src/components/Scrollbar/Scrollbar.stories.tsx +46 -0
  134. package/src/components/Scrollbar/Scrollbar.tsx +187 -0
  135. package/src/components/Scrollbar/index.ts +2 -0
  136. package/src/components/Select/Select.module.css +31 -0
  137. package/src/components/Select/Select.stories.tsx +59 -0
  138. package/src/components/Select/Select.tsx +92 -0
  139. package/src/components/Select/index.ts +2 -0
  140. package/src/components/Sidebar/Sidebar.module.css +161 -0
  141. package/src/components/Sidebar/Sidebar.stories.tsx +125 -0
  142. package/src/components/Sidebar/Sidebar.tsx +129 -0
  143. package/src/components/Sidebar/SidebarItem.tsx +57 -0
  144. package/src/components/Sidebar/SidebarSection.tsx +42 -0
  145. package/src/components/Sidebar/index.ts +6 -0
  146. package/src/components/Skeleton/Skeleton.module.css +53 -0
  147. package/src/components/Skeleton/Skeleton.stories.tsx +89 -0
  148. package/src/components/Skeleton/Skeleton.tsx +66 -0
  149. package/src/components/Skeleton/index.ts +2 -0
  150. package/src/components/Slider/Slider.module.css +62 -0
  151. package/src/components/Slider/Slider.stories.tsx +120 -0
  152. package/src/components/Slider/Slider.tsx +74 -0
  153. package/src/components/Slider/index.ts +2 -0
  154. package/src/components/Surface/Surface.module.css +222 -0
  155. package/src/components/Surface/Surface.stories.tsx +213 -0
  156. package/src/components/Surface/Surface.tsx +219 -0
  157. package/src/components/Surface/index.ts +12 -0
  158. package/src/components/Tab/Tab.module.css +66 -0
  159. package/src/components/Tab/Tab.stories.tsx +103 -0
  160. package/src/components/Tab/Tab.tsx +41 -0
  161. package/src/components/Tab/index.ts +2 -0
  162. package/src/components/TabGroup/TabGroup.module.css +26 -0
  163. package/src/components/TabGroup/TabGroup.stories.tsx +60 -0
  164. package/src/components/TabGroup/TabGroup.tsx +83 -0
  165. package/src/components/TabGroup/index.ts +2 -0
  166. package/src/components/Table/Table.module.css +130 -0
  167. package/src/components/Table/Table.stories.tsx +111 -0
  168. package/src/components/Table/Table.tsx +226 -0
  169. package/src/components/Table/index.ts +11 -0
  170. package/src/components/Tag/Tag.module.css +270 -0
  171. package/src/components/Tag/Tag.stories.tsx +399 -0
  172. package/src/components/Tag/Tag.tsx +162 -0
  173. package/src/components/Tag/index.ts +2 -0
  174. package/src/components/Text/Text.module.css +166 -0
  175. package/src/components/Text/Text.stories.tsx +212 -0
  176. package/src/components/Text/Text.tsx +181 -0
  177. package/src/components/Text/index.ts +13 -0
  178. package/src/components/Toast/Toast.module.css +91 -0
  179. package/src/components/Toast/Toast.stories.tsx +66 -0
  180. package/src/components/Toast/Toast.tsx +182 -0
  181. package/src/components/Toast/index.ts +8 -0
  182. package/src/components/Toggle/Toggle.module.css +87 -0
  183. package/src/components/Toggle/Toggle.stories.tsx +85 -0
  184. package/src/components/Toggle/Toggle.tsx +52 -0
  185. package/src/components/Toggle/index.ts +2 -0
  186. package/src/components/ToggleButton/ToggleButton.module.css +172 -0
  187. package/src/components/ToggleButton/ToggleButton.stories.tsx +299 -0
  188. package/src/components/ToggleButton/ToggleButton.tsx +118 -0
  189. package/src/components/ToggleButton/index.ts +2 -0
  190. package/src/components/ToggleButtonGroup/ToggleButtonGroup.module.css +163 -0
  191. package/src/components/ToggleButtonGroup/ToggleButtonGroup.stories.tsx +341 -0
  192. package/src/components/ToggleButtonGroup/ToggleButtonGroup.tsx +111 -0
  193. package/src/components/ToggleButtonGroup/index.ts +2 -0
  194. package/src/components/Tooltip/Tooltip.module.css +45 -0
  195. package/src/components/Tooltip/Tooltip.stories.tsx +46 -0
  196. package/src/components/Tooltip/Tooltip.tsx +224 -0
  197. package/src/components/Tooltip/index.ts +2 -0
  198. package/src/css-modules.d.ts +4 -0
  199. package/src/docs/ColorUsage.mdx +195 -0
  200. package/src/docs/ElevationUsage.mdx +95 -0
  201. package/src/docs/Introduction.mdx +97 -0
  202. package/src/docs/OpticalSizing.mdx +178 -0
  203. package/src/docs/TypographyUsage.mdx +121 -0
  204. package/src/index.ts +154 -0
  205. package/src/stories/Colors.stories.tsx +528 -0
  206. package/src/stories/Dimensions.stories.tsx +200 -0
  207. package/src/stories/Effects.stories.tsx +231 -0
  208. package/src/stories/Icons.stories.tsx +159 -0
  209. package/src/stories/Typography.stories.tsx +157 -0
  210. package/src/types/icons.ts +15 -0
  211. package/src/types/index.ts +1 -0
  212. package/src/utils/cn.ts +6 -0
  213. package/src/utils/css-tokens.ts +22 -0
@@ -0,0 +1,384 @@
1
+ import type { Meta, StoryObj } from '@storybook/react-vite';
2
+ import React from 'react';
3
+ import { ButtonGroup } from './ButtonGroup';
4
+ import { Button } from '@/components/Button';
5
+ import type { ButtonContrast, ButtonIntent } from '@/components/Button';
6
+ import type { IconComponent } from '@/types/icons';
7
+
8
+ const PlaceholderIcon: IconComponent = ({ size = 20 }) => (
9
+ <svg width={size} height={size} viewBox="0 0 20 20" fill="currentColor">
10
+ <circle cx="10" cy="10" r="6" />
11
+ </svg>
12
+ );
13
+
14
+ interface PlaygroundArgs {
15
+ variant: 'primary' | 'secondary';
16
+ elevation: 'elevated' | 'flat' | 'none' | 'floating';
17
+ intent: ButtonIntent;
18
+ contrast: ButtonContrast;
19
+ size: 'xs' | 'sm' | 'md' | 'lg';
20
+ rounded: boolean;
21
+ showIcon: boolean;
22
+ }
23
+
24
+ const meta: Meta<typeof ButtonGroup> = {
25
+ title: 'Primitives (Reviewed)/ButtonGroup',
26
+ component: ButtonGroup,
27
+ args: {
28
+ variant: 'primary',
29
+ elevation: 'elevated',
30
+ intent: 'brand',
31
+ contrast: 'bold',
32
+ size: 'md',
33
+ rounded: false,
34
+ showIcon: false,
35
+ } as PlaygroundArgs,
36
+ argTypes: {
37
+ variant: { control: 'select', options: ['primary', 'secondary'] },
38
+ elevation: { control: 'select', options: ['elevated', 'flat', 'none', 'floating'] },
39
+ intent: { control: 'select', options: ['none', 'neutral', 'brand', 'ai', 'negative', 'warning', 'caution', 'positive'] },
40
+ contrast: { control: 'select', options: ['strong', 'bold', 'medium', 'faint'] },
41
+ size: { control: 'select', options: ['lg', 'md', 'sm', 'xs'] },
42
+ rounded: { control: 'boolean' },
43
+ showIcon: { control: 'boolean' },
44
+ } as Record<string, unknown>,
45
+ };
46
+
47
+ export default meta;
48
+ type Story = StoryObj<typeof ButtonGroup>;
49
+
50
+ // ─── Shared layout helpers ────────────────────────────────────────────────────
51
+
52
+ const col: React.CSSProperties = { display: 'flex', flexDirection: 'column', gap: 16, alignItems: 'flex-start' };
53
+ const row: React.CSSProperties = { display: 'flex', gap: 16, alignItems: 'center' };
54
+ const lbl: React.CSSProperties = { fontSize: 10, fontFamily: 'monospace', color: '#888', minWidth: 120, flexShrink: 0 };
55
+ const section = (text: string) => (
56
+ <div style={{ fontSize: 11, fontWeight: 600, letterSpacing: '0.05em', textTransform: 'uppercase', color: '#555', marginTop: 4 }}>
57
+ {text}
58
+ </div>
59
+ );
60
+ const divider = <hr style={{ border: 'none', borderTop: '1px solid #e0e0e0', margin: '4px 0 12px' }} />;
61
+
62
+ // ─── Playground ──────────────────────────────────────────────────────────────
63
+
64
+ export const Playground: Story = {
65
+ render: (args) => {
66
+ const a = args as unknown as PlaygroundArgs;
67
+ const labels = ['First', 'Second', 'Third'];
68
+ return (
69
+ <ButtonGroup>
70
+ {labels.map(label => (
71
+ <Button
72
+ key={label}
73
+ variant={a.variant}
74
+ elevation={a.elevation}
75
+ intent={a.intent}
76
+ contrast={a.contrast}
77
+ size={a.size}
78
+ rounded={a.rounded}
79
+ icon={a.showIcon ? PlaceholderIcon : undefined}
80
+ label={label}
81
+ />
82
+ ))}
83
+ </ButtonGroup>
84
+ );
85
+ },
86
+ };
87
+
88
+ // ─── Matrix ───────────────────────────────────────────────────────────────────
89
+
90
+ export const Matrix: Story = {
91
+ parameters: { layout: 'padded' },
92
+ render: () => (
93
+ <div style={{ ...col, fontFamily: 'sans-serif', gap: 0 }}>
94
+
95
+ {/* ── Variants & elevation ── */}
96
+ {section('Elevation — full-height vs short divider')}
97
+ {divider}
98
+ <div style={col}>
99
+ <div style={row}>
100
+ <span style={lbl}>secondary / flat</span>
101
+ <ButtonGroup>
102
+ <Button variant="secondary" elevation="flat" label="Day" />
103
+ <Button variant="secondary" elevation="flat" label="Week" />
104
+ <Button variant="secondary" elevation="flat" label="Month" />
105
+ </ButtonGroup>
106
+ </div>
107
+ <div style={row}>
108
+ <span style={lbl}>secondary / elevated</span>
109
+ <ButtonGroup>
110
+ <Button variant="secondary" elevation="elevated" label="Day" />
111
+ <Button variant="secondary" elevation="elevated" label="Week" />
112
+ <Button variant="secondary" elevation="elevated" label="Month" />
113
+ </ButtonGroup>
114
+ </div>
115
+ <div style={row}>
116
+ <span style={lbl}>primary / none intent</span>
117
+ <ButtonGroup>
118
+ <Button variant="primary" intent="none" label="Day" />
119
+ <Button variant="primary" intent="none" label="Week" />
120
+ <Button variant="primary" intent="none" label="Month" />
121
+ </ButtonGroup>
122
+ </div>
123
+ <div style={row}>
124
+ <span style={lbl}>secondary / none ↓ short</span>
125
+ <ButtonGroup>
126
+ <Button variant="secondary" elevation="none" label="Day" />
127
+ <Button variant="secondary" elevation="none" label="Week" />
128
+ <Button variant="secondary" elevation="none" label="Month" />
129
+ </ButtonGroup>
130
+ </div>
131
+ </div>
132
+
133
+ {/* ── Ghost: short divider at each size ── */}
134
+ {section('Ghost (elevation=none) — short divider scales with size')}
135
+ {divider}
136
+ <div style={col}>
137
+ {(['lg', 'md', 'sm', 'xs'] as const).map(size => (
138
+ <div key={size} style={row}>
139
+ <span style={lbl}>{size}</span>
140
+ <ButtonGroup>
141
+ <Button variant="secondary" elevation="none" size={size} label="One" />
142
+ <Button variant="secondary" elevation="none" size={size} label="Two" />
143
+ <Button variant="secondary" elevation="none" size={size} label="Three" />
144
+ </ButtonGroup>
145
+ </div>
146
+ ))}
147
+ </div>
148
+
149
+ {/* ── Primary intents ── */}
150
+ {section('Primary — homogeneous intent + contrast (on-bg divider)')}
151
+ {divider}
152
+ <div style={col}>
153
+ {([
154
+ { intent: 'brand', contrast: 'bold' },
155
+ { intent: 'negative', contrast: 'bold' },
156
+ { intent: 'positive', contrast: 'bold' },
157
+ { intent: 'brand', contrast: 'medium' },
158
+ { intent: 'brand', contrast: 'faint' },
159
+ ] as { intent: ButtonIntent; contrast: ButtonContrast }[]).map(({ intent, contrast }) => (
160
+ <div key={`${intent}-${contrast}`} style={row}>
161
+ <span style={lbl}>{intent} / {contrast}</span>
162
+ <ButtonGroup>
163
+ <Button variant="primary" intent={intent} contrast={contrast} label="Save" />
164
+ <Button variant="primary" intent={intent} contrast={contrast} label="Copy" />
165
+ <Button variant="primary" intent={intent} contrast={contrast} label="Export" />
166
+ </ButtonGroup>
167
+ </div>
168
+ ))}
169
+ </div>
170
+
171
+ {/* ── Sizes ── */}
172
+ {section('Sizes — secondary / flat')}
173
+ {divider}
174
+ <div style={col}>
175
+ {(['lg', 'md', 'sm', 'xs'] as const).map(size => (
176
+ <div key={size} style={row}>
177
+ <span style={lbl}>{size}</span>
178
+ <ButtonGroup>
179
+ <Button variant="secondary" size={size} label="One" />
180
+ <Button variant="secondary" size={size} label="Two" />
181
+ <Button variant="secondary" size={size} label="Three" />
182
+ </ButtonGroup>
183
+ </div>
184
+ ))}
185
+ </div>
186
+
187
+ {/* ── With Icons ── */}
188
+ {section('With Icons')}
189
+ {divider}
190
+ <div style={col}>
191
+ <div style={row}>
192
+ <span style={lbl}>icon + label</span>
193
+ <ButtonGroup>
194
+ <Button variant="secondary" icon={PlaceholderIcon} label="Grid" />
195
+ <Button variant="secondary" icon={PlaceholderIcon} label="List" />
196
+ <Button variant="secondary" icon={PlaceholderIcon} label="Table" />
197
+ </ButtonGroup>
198
+ </div>
199
+ <div style={row}>
200
+ <span style={lbl}>icon only</span>
201
+ <ButtonGroup>
202
+ <Button variant="secondary" icon={PlaceholderIcon} aria-label="Grid" />
203
+ <Button variant="secondary" icon={PlaceholderIcon} aria-label="List" />
204
+ <Button variant="secondary" icon={PlaceholderIcon} aria-label="Table" />
205
+ </ButtonGroup>
206
+ </div>
207
+ <div style={row}>
208
+ <span style={lbl}>ghost icon</span>
209
+ <ButtonGroup>
210
+ <Button variant="secondary" elevation="none" icon={PlaceholderIcon} aria-label="Grid" />
211
+ <Button variant="secondary" elevation="none" icon={PlaceholderIcon} aria-label="List" />
212
+ <Button variant="secondary" elevation="none" icon={PlaceholderIcon} aria-label="Table" />
213
+ </ButtonGroup>
214
+ </div>
215
+ </div>
216
+
217
+ {/* ── Rounded ── */}
218
+ {section('Rounded — outer pill padding, inner base padding')}
219
+ {divider}
220
+ <div style={col}>
221
+ <div style={row}>
222
+ <span style={lbl}>secondary / flat</span>
223
+ <ButtonGroup>
224
+ <Button variant="secondary" label="Day" rounded />
225
+ <Button variant="secondary" label="Week" rounded />
226
+ <Button variant="secondary" label="Month" rounded />
227
+ </ButtonGroup>
228
+ </div>
229
+ <div style={row}>
230
+ <span style={lbl}>ghost</span>
231
+ <ButtonGroup>
232
+ <Button variant="secondary" elevation="none" label="Day" rounded />
233
+ <Button variant="secondary" elevation="none" label="Week" rounded />
234
+ <Button variant="secondary" elevation="none" label="Month" rounded />
235
+ </ButtonGroup>
236
+ </div>
237
+ <div style={row}>
238
+ <span style={lbl}>icon + label</span>
239
+ <ButtonGroup>
240
+ <Button variant="secondary" icon={PlaceholderIcon} label="Grid" rounded />
241
+ <Button variant="secondary" icon={PlaceholderIcon} label="List" rounded />
242
+ <Button variant="secondary" icon={PlaceholderIcon} label="Table" rounded />
243
+ </ButtonGroup>
244
+ </div>
245
+ <div style={row}>
246
+ <span style={lbl}>icon only</span>
247
+ <ButtonGroup>
248
+ <Button variant="secondary" icon={PlaceholderIcon} rounded aria-label="Grid" />
249
+ <Button variant="secondary" icon={PlaceholderIcon} rounded aria-label="List" />
250
+ <Button variant="secondary" icon={PlaceholderIcon} rounded aria-label="Table" />
251
+ </ButtonGroup>
252
+ </div>
253
+ {(['lg', 'md', 'sm'] as const).map(size => (
254
+ <div key={size} style={row}>
255
+ <span style={lbl}>rounded {size}</span>
256
+ <ButtonGroup>
257
+ <Button variant="secondary" size={size} label="One" rounded />
258
+ <Button variant="secondary" size={size} label="Two" rounded />
259
+ <Button variant="secondary" size={size} label="Three" rounded />
260
+ </ButtonGroup>
261
+ </div>
262
+ ))}
263
+ </div>
264
+
265
+ {/* ── States ── */}
266
+ {section('States')}
267
+ {divider}
268
+ <div style={col}>
269
+ <div style={row}>
270
+ <span style={lbl}>with inactive</span>
271
+ <ButtonGroup>
272
+ <Button variant="secondary" label="One" />
273
+ <Button variant="secondary" label="Two" inactive />
274
+ <Button variant="secondary" label="Three" />
275
+ </ButtonGroup>
276
+ </div>
277
+ <div style={row}>
278
+ <span style={lbl}>two items</span>
279
+ <ButtonGroup>
280
+ <Button variant="secondary" label="Left" />
281
+ <Button variant="secondary" label="Right" />
282
+ </ButtonGroup>
283
+ </div>
284
+ </div>
285
+
286
+ </div>
287
+ ),
288
+ };
289
+
290
+ // ─── Individual stories ───────────────────────────────────────────────────────
291
+
292
+ export const Secondary: Story = {
293
+ render: () => (
294
+ <ButtonGroup>
295
+ <Button variant="secondary" label="Day" />
296
+ <Button variant="secondary" label="Week" />
297
+ <Button variant="secondary" label="Month" />
298
+ </ButtonGroup>
299
+ ),
300
+ };
301
+
302
+ export const Ghost: Story = {
303
+ render: () => (
304
+ <div style={{ display: 'flex', flexDirection: 'column', gap: 12, alignItems: 'flex-start' }}>
305
+ {(['lg', 'md', 'sm', 'xs'] as const).map(size => (
306
+ <ButtonGroup key={size}>
307
+ <Button variant="secondary" elevation="none" size={size} label="Grid" />
308
+ <Button variant="secondary" elevation="none" size={size} label="List" />
309
+ <Button variant="secondary" elevation="none" size={size} label="Table" />
310
+ </ButtonGroup>
311
+ ))}
312
+ </div>
313
+ ),
314
+ };
315
+
316
+ export const PrimaryIntents: Story = {
317
+ render: () => (
318
+ <div style={{ display: 'flex', flexDirection: 'column', gap: 12, alignItems: 'flex-start' }}>
319
+ <ButtonGroup>
320
+ <Button variant="primary" intent="brand" label="Save" />
321
+ <Button variant="primary" intent="brand" label="Duplicate" />
322
+ <Button variant="primary" intent="brand" label="Export" />
323
+ </ButtonGroup>
324
+ <ButtonGroup>
325
+ <Button variant="primary" intent="negative" label="Delete" />
326
+ <Button variant="primary" intent="negative" label="Archive" />
327
+ </ButtonGroup>
328
+ <ButtonGroup>
329
+ <Button variant="primary" intent="brand" contrast="medium" label="Save" />
330
+ <Button variant="primary" intent="brand" contrast="medium" label="Duplicate" />
331
+ <Button variant="primary" intent="brand" contrast="medium" label="Export" />
332
+ </ButtonGroup>
333
+ <ButtonGroup>
334
+ <Button variant="primary" intent="brand" contrast="faint" label="Save" />
335
+ <Button variant="primary" intent="brand" contrast="faint" label="Duplicate" />
336
+ <Button variant="primary" intent="brand" contrast="faint" label="Export" />
337
+ </ButtonGroup>
338
+ </div>
339
+ ),
340
+ };
341
+
342
+ export const Rounded: Story = {
343
+ render: () => (
344
+ <div style={{ display: 'flex', flexDirection: 'column', gap: 12, alignItems: 'flex-start' }}>
345
+ <ButtonGroup>
346
+ <Button variant="secondary" label="Day" rounded />
347
+ <Button variant="secondary" label="Week" rounded />
348
+ <Button variant="secondary" label="Month" rounded />
349
+ </ButtonGroup>
350
+ <ButtonGroup>
351
+ <Button variant="secondary" elevation="none" label="Day" rounded />
352
+ <Button variant="secondary" elevation="none" label="Week" rounded />
353
+ <Button variant="secondary" elevation="none" label="Month" rounded />
354
+ </ButtonGroup>
355
+ <ButtonGroup>
356
+ <Button variant="secondary" icon={PlaceholderIcon} label="Grid" rounded />
357
+ <Button variant="secondary" icon={PlaceholderIcon} label="List" rounded />
358
+ <Button variant="secondary" icon={PlaceholderIcon} label="Table" rounded />
359
+ </ButtonGroup>
360
+ <ButtonGroup>
361
+ <Button variant="secondary" icon={PlaceholderIcon} rounded aria-label="Grid" />
362
+ <Button variant="secondary" icon={PlaceholderIcon} rounded aria-label="List" />
363
+ <Button variant="secondary" icon={PlaceholderIcon} rounded aria-label="Table" />
364
+ </ButtonGroup>
365
+ </div>
366
+ ),
367
+ };
368
+
369
+ export const IconOnly: Story = {
370
+ render: () => (
371
+ <div style={{ display: 'flex', flexDirection: 'column', gap: 12, alignItems: 'flex-start' }}>
372
+ <ButtonGroup>
373
+ <Button variant="secondary" icon={PlaceholderIcon} aria-label="Grid view" />
374
+ <Button variant="secondary" icon={PlaceholderIcon} aria-label="List view" />
375
+ <Button variant="secondary" icon={PlaceholderIcon} aria-label="Table view" />
376
+ </ButtonGroup>
377
+ <ButtonGroup>
378
+ <Button variant="secondary" elevation="none" icon={PlaceholderIcon} aria-label="Grid view" />
379
+ <Button variant="secondary" elevation="none" icon={PlaceholderIcon} aria-label="List view" />
380
+ <Button variant="secondary" elevation="none" icon={PlaceholderIcon} aria-label="Table view" />
381
+ </ButtonGroup>
382
+ </div>
383
+ ),
384
+ };
@@ -0,0 +1,122 @@
1
+ import React from 'react';
2
+ import { cn } from '@/utils/cn';
3
+ import styles from './ButtonGroup.module.css';
4
+
5
+ export interface ButtonGroupProps {
6
+ children: React.ReactNode;
7
+ className?: string;
8
+ }
9
+
10
+ /**
11
+ * Renders Button children flush side-by-side with a 1px divider between them.
12
+ * Outer corners keep the button's own border-radius; inner corners are flattened.
13
+ *
14
+ * Individual button chrome (shadow/border) is suppressed — the group container
15
+ * carries the elevation so it wraps the whole control as one visual unit.
16
+ *
17
+ * Divider behaviour by variant:
18
+ * secondary / primary+none — full-height color-border-tertiary line
19
+ * primary (with intent) — color-border-tertiary-on-{contrast}-background
20
+ * ghost (secondary+none) — short centred line (icon/text height)
21
+ *
22
+ * Rounded groups: outer edges keep pill padding; inner edges revert to base padding.
23
+ * Mix of intents is only valid when variant="secondary"; primary groups must be
24
+ * homogeneous (same intent + contrast on all items).
25
+ */
26
+ export const ButtonGroup = ({ children, className }: ButtonGroupProps) => {
27
+ const items = React.Children.toArray(children).filter(React.isValidElement);
28
+ const count = items.length;
29
+
30
+ // Read elevation + rounded from first child to drive the group container style.
31
+ const firstEl = items[0] as React.ReactElement<Record<string, unknown>> | undefined;
32
+ const firstProps = firstEl?.props as { variant?: string; elevation?: string; rounded?: boolean } | undefined;
33
+ const firstVariant = firstProps?.variant ?? 'secondary';
34
+ const firstElevation = firstProps?.elevation ?? (firstVariant === 'primary' ? 'none' : 'flat');
35
+ const groupRounded = !!firstProps?.rounded;
36
+ const groupElevKey = firstElevation.charAt(0).toUpperCase() + firstElevation.slice(1);
37
+
38
+ return (
39
+ <div
40
+ className={cn(
41
+ styles.group,
42
+ (styles as Record<string, string>)[`group${groupElevKey}`],
43
+ groupRounded && styles.groupRounded,
44
+ className,
45
+ )}
46
+ role="group"
47
+ >
48
+ {items.map((child, i) => {
49
+ const el = child as React.ReactElement<Record<string, unknown>>;
50
+ const p = el.props as {
51
+ className?: string;
52
+ variant?: string;
53
+ elevation?: string;
54
+ size?: string;
55
+ intent?: string;
56
+ contrast?: string;
57
+ rounded?: boolean;
58
+ icon?: unknown;
59
+ label?: unknown;
60
+ };
61
+
62
+ const isFirst = i === 0;
63
+ const isLast = i === count - 1;
64
+ const isMid = !isFirst && !isLast;
65
+
66
+ const variant = p.variant ?? 'secondary';
67
+ const size = (p.size ?? 'md') as 'xs' | 'sm' | 'md' | 'lg';
68
+ const contrast = p.contrast ?? 'bold';
69
+ const intent = p.intent ?? 'brand';
70
+ const isRounded = !!p.rounded;
71
+ const isIconOnly = !!p.icon && !p.label;
72
+ // Ghost: secondary button with elevation='none' (replaces old tertiary variant)
73
+ const isGhost = variant === 'secondary' && (p.elevation ?? 'flat') === 'none';
74
+ const isPrimary = variant === 'primary' && intent !== 'none';
75
+
76
+ // Contrast class suffix — capitalised first letter
77
+ const contrastKey = contrast.charAt(0).toUpperCase() + contrast.slice(1);
78
+
79
+ // Size suffix for rounded padding correction — uppercase
80
+ // XS rounded has no extra padding in Button so needs no correction
81
+ const sizeKey = size.toUpperCase() as 'MD' | 'SM' | 'LG' | 'XS';
82
+ const needsRoundedCorrection = isRounded && !isIconOnly && size !== 'xs';
83
+
84
+ // Divider injected as a separate element so it's never affected by
85
+ // the adjacent button's opacity (e.g. disabled state).
86
+ const dividerClasses = cn(
87
+ styles.divider,
88
+ isGhost && styles.dividerGhost,
89
+ isGhost && size === 'xs' && styles.dividerGhostXS,
90
+ isGhost && size === 'sm' && styles.dividerGhostSM,
91
+ isGhost && size === 'lg' && styles.dividerGhostLG,
92
+ isPrimary && (styles as Record<string, string>)[`divider${contrastKey}`],
93
+ );
94
+
95
+ return (
96
+ <React.Fragment key={i}>
97
+ {i > 0 && <div className={dividerClasses} aria-hidden="true" />}
98
+ {React.cloneElement(el, {
99
+ className: cn(
100
+ p.className,
101
+ styles.item,
102
+ isFirst && styles.first,
103
+ isLast && styles.last,
104
+ isMid && styles.middle,
105
+
106
+ // Ghost (secondary+elevation=none): ::after overlay gets 4px radius treatment
107
+ isGhost && styles.itemShort,
108
+
109
+ // Rounded: revert inner-edge padding to base (non-pill) value
110
+ needsRoundedCorrection && isFirst && (styles as Record<string, string>)[`roundedFirst${sizeKey}`],
111
+ needsRoundedCorrection && isLast && (styles as Record<string, string>)[`roundedLast${sizeKey}`],
112
+ needsRoundedCorrection && isMid && (styles as Record<string, string>)[`roundedMid${sizeKey}`],
113
+ ),
114
+ })}
115
+ </React.Fragment>
116
+ );
117
+ })}
118
+ </div>
119
+ );
120
+ };
121
+
122
+ ButtonGroup.displayName = 'ButtonGroup';
@@ -0,0 +1,2 @@
1
+ export { ButtonGroup } from './ButtonGroup';
2
+ export type { ButtonGroupProps } from './ButtonGroup';
@@ -0,0 +1,36 @@
1
+ /**
2
+ * Card — structured container with optional header/footer.
3
+ * Uses TokoMo tokens directly (not built on Surface).
4
+ */
5
+
6
+ .card {
7
+ box-sizing: border-box;
8
+ background-color: var(--color-background-primary);
9
+ overflow: hidden;
10
+ }
11
+
12
+ /* Elevation */
13
+ .elevationFlat { box-shadow: var(--effect-elevation-elevated-none); }
14
+ .elevationElevated { box-shadow: var(--effect-elevation-elevated-sm); }
15
+ .elevationFloating { box-shadow: var(--effect-elevation-elevated-floating); }
16
+
17
+ /* Radius */
18
+ .radiusSm { border-radius: var(--dimension-radius-075); }
19
+ .radiusMd { border-radius: var(--dimension-radius-100); }
20
+ .radiusLg { border-radius: var(--dimension-radius-125); }
21
+ .radiusXl { border-radius: var(--dimension-radius-175); }
22
+
23
+ /* Sections */
24
+ .header {
25
+ padding: var(--dimension-space-150) var(--dimension-space-200);
26
+ border-bottom: var(--dimension-stroke-width-015) solid var(--color-border-primary);
27
+ }
28
+
29
+ .body {
30
+ padding: var(--dimension-space-200);
31
+ }
32
+
33
+ .footer {
34
+ padding: var(--dimension-space-150) var(--dimension-space-200);
35
+ border-top: var(--dimension-stroke-width-015) solid var(--color-border-primary);
36
+ }
@@ -0,0 +1,78 @@
1
+ import type { Meta, StoryObj } from '@storybook/react-vite';
2
+ import { Card } from './Card';
3
+
4
+ const meta: Meta<typeof Card> = {
5
+ title: 'Components/Card',
6
+ component: Card,
7
+ argTypes: {
8
+ elevation: {
9
+ control: 'select',
10
+ options: ['flat', 'elevated', 'floating'],
11
+ },
12
+ radius: {
13
+ control: 'select',
14
+ options: ['sm', 'md', 'lg', 'xl'],
15
+ },
16
+ },
17
+ };
18
+
19
+ export default meta;
20
+ type Story = StoryObj<typeof Card>;
21
+
22
+ export const Default: Story = {
23
+ args: {
24
+ children: 'Card body content goes here.',
25
+ style: { width: 360 },
26
+ },
27
+ };
28
+
29
+ export const WithHeader: Story = {
30
+ args: {
31
+ header: <strong>Account Settings</strong>,
32
+ children: 'Form fields and content go here.',
33
+ style: { width: 360 },
34
+ },
35
+ };
36
+
37
+ export const WithHeaderAndFooter: Story = {
38
+ args: {
39
+ header: <strong>Edit Profile</strong>,
40
+ children: (
41
+ <div style={{ display: 'flex', flexDirection: 'column', gap: 12 }}>
42
+ <div>Name: Zain Adeel</div>
43
+ <div>Email: zain@example.com</div>
44
+ </div>
45
+ ),
46
+ footer: (
47
+ <div style={{ display: 'flex', justifyContent: 'flex-end', gap: 8 }}>
48
+ <button>Cancel</button>
49
+ <button>Save</button>
50
+ </div>
51
+ ),
52
+ style: { width: 360 },
53
+ },
54
+ };
55
+
56
+ export const Elevations: Story = {
57
+ render: () => (
58
+ <div style={{ display: 'flex', gap: 24, padding: 24 }}>
59
+ {(['flat', 'elevated', 'floating'] as const).map(elevation => (
60
+ <Card key={elevation} elevation={elevation} style={{ width: 200 }}>
61
+ {elevation}
62
+ </Card>
63
+ ))}
64
+ </div>
65
+ ),
66
+ };
67
+
68
+ export const Radii: Story = {
69
+ render: () => (
70
+ <div style={{ display: 'flex', gap: 24, padding: 24 }}>
71
+ {(['sm', 'md', 'lg', 'xl'] as const).map(radius => (
72
+ <Card key={radius} radius={radius} style={{ width: 160 }}>
73
+ radius: {radius}
74
+ </Card>
75
+ ))}
76
+ </div>
77
+ ),
78
+ };
@@ -0,0 +1,53 @@
1
+ import React, { forwardRef } from 'react';
2
+ import styles from './Card.module.css';
3
+ import { cn } from '@/utils/cn';
4
+
5
+ export type CardElevation = 'flat' | 'elevated' | 'floating';
6
+ export type CardRadius = 'sm' | 'md' | 'lg' | 'xl';
7
+
8
+ export interface CardProps {
9
+ children: React.ReactNode;
10
+ /** Optional header content — rendered with a bottom divider. */
11
+ header?: React.ReactNode;
12
+ /** Optional footer content — rendered with a top divider. */
13
+ footer?: React.ReactNode;
14
+ /** Elevation level. Defaults to 'elevated'. */
15
+ elevation?: CardElevation;
16
+ /** Corner radius. Defaults to 'lg'. */
17
+ radius?: CardRadius;
18
+ /** Additional CSS classes. */
19
+ className?: string;
20
+ /** Inline styles. */
21
+ style?: React.CSSProperties;
22
+ }
23
+
24
+ const elevationClassMap: Record<CardElevation, string> = {
25
+ flat: styles.elevationFlat,
26
+ elevated: styles.elevationElevated,
27
+ floating: styles.elevationFloating,
28
+ };
29
+
30
+ const radiusClassMap: Record<CardRadius, string> = {
31
+ sm: styles.radiusSm,
32
+ md: styles.radiusMd,
33
+ lg: styles.radiusLg,
34
+ xl: styles.radiusXl,
35
+ };
36
+
37
+ export const Card = forwardRef<HTMLDivElement, CardProps>(
38
+ ({ children, header, footer, elevation = 'elevated', radius = 'lg', className, style }, ref) => {
39
+ return (
40
+ <div
41
+ ref={ref}
42
+ className={cn(styles.card, elevationClassMap[elevation], radiusClassMap[radius], className)}
43
+ style={style}
44
+ >
45
+ {header && <div className={styles.header}>{header}</div>}
46
+ <div className={styles.body}>{children}</div>
47
+ {footer && <div className={styles.footer}>{footer}</div>}
48
+ </div>
49
+ );
50
+ }
51
+ );
52
+
53
+ Card.displayName = 'Card';