@altruist03/ui 0.0.0-stage → 0.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 (205) hide show
  1. package/assets/images/noData.webp +0 -0
  2. package/dist/index.js +15318 -0
  3. package/dist/index.js.map +1 -0
  4. package/dist/noData-XPRHKMFR.webp +0 -0
  5. package/package.json +51 -4
  6. package/src/ThemeProvider/ThemeContext.tsx +75 -0
  7. package/src/ThemeProvider/index.ts +7 -0
  8. package/src/assets/images/noData.webp +0 -0
  9. package/src/assets.d.ts +10 -0
  10. package/src/components/Accordion/Accordion.stories.tsx +64 -0
  11. package/src/components/Accordion/Accordion.tsx +219 -0
  12. package/src/components/Accordion/Accordion.types.ts +16 -0
  13. package/src/components/Avatar/Avatar.stories.tsx +304 -0
  14. package/src/components/Avatar/Avatar.tsx +211 -0
  15. package/src/components/Avatar/Avatar.types.ts +33 -0
  16. package/src/components/Badge/Badge.stories.tsx +143 -0
  17. package/src/components/Badge/Badge.tsx +138 -0
  18. package/src/components/Badge/Badge.types.ts +24 -0
  19. package/src/components/Breadcrumb/Breadcrumb.stories.tsx +114 -0
  20. package/src/components/Breadcrumb/Breadcrumb.tsx +120 -0
  21. package/src/components/Breadcrumb/Breadcrumb.types.ts +22 -0
  22. package/src/components/Button/Button.stories.tsx +457 -0
  23. package/src/components/Button/Button.tsx +202 -0
  24. package/src/components/Button/Button.types.ts +18 -0
  25. package/src/components/Checkbox/Checkbox.stories.tsx +254 -0
  26. package/src/components/Checkbox/Checkbox.tsx +352 -0
  27. package/src/components/Checkbox/Checkbox.types.ts +123 -0
  28. package/src/components/CloseButton/CloseButton.stories.tsx +100 -0
  29. package/src/components/CloseButton/CloseButton.tsx +117 -0
  30. package/src/components/CloseButton/CloseButton.types.ts +15 -0
  31. package/src/components/CollapsibleSearch/CollapsibleSearch.tsx +120 -0
  32. package/src/components/DataTable/DataTable.stories.tsx +233 -0
  33. package/src/components/DataTable/DataTable.tsx +968 -0
  34. package/src/components/DataTable/DataTable.types.ts +49 -0
  35. package/src/components/DatePicker/DatePicker.stories.tsx +341 -0
  36. package/src/components/DatePicker/DatePicker.tsx +669 -0
  37. package/src/components/DatePicker/DatePicker.types.ts +105 -0
  38. package/src/components/DatePicker/DateTimePicker.tsx +35 -0
  39. package/src/components/DateRangePicker/DateRangePicker.tsx +179 -0
  40. package/src/components/DateRangePicker/DateRangePicker.types.ts +34 -0
  41. package/src/components/Drawer/Drawer.stories.tsx +243 -0
  42. package/src/components/Drawer/Drawer.tsx +196 -0
  43. package/src/components/Drawer/Drawer.types.ts +60 -0
  44. package/src/components/FileUpload/FileUpload.stories.tsx +162 -0
  45. package/src/components/FileUpload/FileUpload.tsx +216 -0
  46. package/src/components/FileUpload/FileUpload.types.ts +80 -0
  47. package/src/components/FilterPopover/FilterPopover.stories.tsx +173 -0
  48. package/src/components/FilterPopover/FilterPopover.tsx +310 -0
  49. package/src/components/FilterPopover/FilterPopover.types.ts +26 -0
  50. package/src/components/ImageViewer/ImageViewerToolbar.jsx +81 -0
  51. package/src/components/ImageViewer/useImagePanRotate.js +102 -0
  52. package/src/components/IpWhitelistField/IpWhitelistField.stories.tsx +138 -0
  53. package/src/components/IpWhitelistField/IpWhitelistField.tsx +164 -0
  54. package/src/components/IpWhitelistField/IpWhitelistField.types.ts +42 -0
  55. package/src/components/Loader/Loader.stories.tsx +95 -0
  56. package/src/components/Loader/Loader.tsx +132 -0
  57. package/src/components/Loader/Loader.types.ts +6 -0
  58. package/src/components/Menu/Menu.tsx +143 -0
  59. package/src/components/Menu/Menu.types.ts +28 -0
  60. package/src/components/MenuButton/MenuButton.stories.tsx +140 -0
  61. package/src/components/MenuButton/MenuButton.tsx +45 -0
  62. package/src/components/MenuButton/MenuButton.types.ts +21 -0
  63. package/src/components/Modal/Modal.stories.tsx +266 -0
  64. package/src/components/Modal/Modal.tsx +399 -0
  65. package/src/components/Modal/index.ts +2 -0
  66. package/src/components/Modal/types.ts +126 -0
  67. package/src/components/Pagination/Pagination.stories.tsx +167 -0
  68. package/src/components/Pagination/Pagination.tsx +266 -0
  69. package/src/components/Pagination/Pagination.types.ts +20 -0
  70. package/src/components/PeriodSelector/PeriodSelector.stories.tsx +50 -0
  71. package/src/components/PeriodSelector/PeriodSelector.tsx +47 -0
  72. package/src/components/PeriodSelector/PeriodSelector.types.ts +10 -0
  73. package/src/components/PhoneField/PhoneField.stories.tsx +141 -0
  74. package/src/components/PhoneField/PhoneField.tsx +284 -0
  75. package/src/components/PhoneField/PhoneField.types.ts +89 -0
  76. package/src/components/PhoneField/countryCodes.ts +236 -0
  77. package/src/components/Progress/Progress.stories.tsx +83 -0
  78. package/src/components/Progress/Progress.tsx +91 -0
  79. package/src/components/Progress/Progress.types.ts +22 -0
  80. package/src/components/RadioButton/RadioButton.stories.tsx +332 -0
  81. package/src/components/RadioButton/RadioButton.tsx +427 -0
  82. package/src/components/RadioButton/RadioButton.types.ts +117 -0
  83. package/src/components/ReportCard/ReportCard.tsx +183 -0
  84. package/src/components/ReportCard/ReportCard.types.ts +17 -0
  85. package/src/components/SectionIcon/SectionIcon.tsx +87 -0
  86. package/src/components/SectionIcon/SectionIcon.types.ts +23 -0
  87. package/src/components/SegmentedInputField/SegmentedInputField.stories.tsx +165 -0
  88. package/src/components/SegmentedInputField/SegmentedInputField.tsx +174 -0
  89. package/src/components/SegmentedInputField/SegmentedInputField.types.ts +51 -0
  90. package/src/components/SelectScroll/SelectScroll.stories.tsx +336 -0
  91. package/src/components/SelectScroll/SelectScroll.tsx +1119 -0
  92. package/src/components/SelectScroll/SelectScroll.types.ts +60 -0
  93. package/src/components/Selector/Selector.stories.tsx +382 -0
  94. package/src/components/Selector/Selector.tsx +1310 -0
  95. package/src/components/Selector/Selector.types.ts +71 -0
  96. package/src/components/Skeleton/Skeleton.stories.tsx +133 -0
  97. package/src/components/Skeleton/Skeleton.tsx +20 -0
  98. package/src/components/Skeleton/Skeleton.types.ts +3 -0
  99. package/src/components/Spinner/Spinner.stories.tsx +84 -0
  100. package/src/components/Spinner/Spinner.tsx +79 -0
  101. package/src/components/Spinner/Spinner.types.ts +8 -0
  102. package/src/components/StatCard/StatCard.stories.tsx +125 -0
  103. package/src/components/StatCard/StatCard.tsx +167 -0
  104. package/src/components/StatCard/StatCard.types.ts +20 -0
  105. package/src/components/StatusBadge/StatusBadge.stories.tsx +179 -0
  106. package/src/components/StatusBadge/StatusBadge.tsx +74 -0
  107. package/src/components/StatusBadge/StatusBadge.types.ts +23 -0
  108. package/src/components/Stepper/Stepper.stories.tsx +85 -0
  109. package/src/components/Stepper/Stepper.tsx +138 -0
  110. package/src/components/Stepper/Stepper.types.ts +16 -0
  111. package/src/components/SubTabs/SubTabs.stories.tsx +128 -0
  112. package/src/components/SubTabs/SubTabs.tsx +279 -0
  113. package/src/components/SubTabs/SubTabs.types.ts +44 -0
  114. package/src/components/SummaryCard/SummaryCard.stories.tsx +100 -0
  115. package/src/components/SummaryCard/SummaryCard.tsx +141 -0
  116. package/src/components/SummaryCard/SummaryCard.types.ts +22 -0
  117. package/src/components/Switch/Switch.stories.tsx +59 -0
  118. package/src/components/Switch/Switch.tsx +217 -0
  119. package/src/components/Switch/Switch.types.ts +22 -0
  120. package/src/components/Tabs/Tabs.stories.tsx +178 -0
  121. package/src/components/Tabs/Tabs.tsx +197 -0
  122. package/src/components/Tabs/Tabs.types.ts +34 -0
  123. package/src/components/TextField/TextField.stories.tsx +249 -0
  124. package/src/components/TextField/TextField.tsx +462 -0
  125. package/src/components/TextField/TextField.types.ts +141 -0
  126. package/src/components/Toast/Toast.stories.tsx +237 -0
  127. package/src/components/Toast/Toast.tsx +246 -0
  128. package/src/components/Toast/Toast.types.ts +31 -0
  129. package/src/components/ToggleButton/ToggleButton.stories.tsx +105 -0
  130. package/src/components/ToggleButton/ToggleButton.tsx +267 -0
  131. package/src/components/ToggleButton/ToggleButton.types.ts +19 -0
  132. package/src/components/UploadDropzone/UploadDropzone.jsx +78 -0
  133. package/src/components/UploadDropzone2/UploadDropzone.stories.tsx +94 -0
  134. package/src/components/UploadDropzone2/UploadDropzone.tsx +222 -0
  135. package/src/components/UploadDropzone2/UploadDropzone.types.ts +54 -0
  136. package/src/components/ViewEditToggle/ViewEditToggle.stories.tsx +40 -0
  137. package/src/components/ViewEditToggle/ViewEditToggle.tsx +239 -0
  138. package/src/components/ViewEditToggle/ViewEditToggle.types.ts +27 -0
  139. package/src/components/index.ts +157 -0
  140. package/src/icons/ReportDollarIcon.tsx +71 -0
  141. package/src/icons/index.ts +27 -0
  142. package/src/index.ts +214 -0
  143. package/src/storybook/preview.tsx +64 -0
  144. package/src/theme/index.ts +4 -0
  145. package/src/theme/types.d.ts +1 -0
  146. package/src/themes/cpc.ts +4 -0
  147. package/src/themes/firebird.ts +4 -0
  148. package/src/tokens/brands/cpc/themes/dark.json +82 -0
  149. package/src/tokens/brands/cpc/themes/light.json +1 -0
  150. package/src/tokens/brands/default/brand.json +20 -0
  151. package/src/tokens/brands/firebird/themes/dark.json +28 -0
  152. package/src/tokens/brands/firebird/themes/light.json +20 -0
  153. package/src/tokens/button.ts +24 -0
  154. package/src/tokens/components/accordion.json +52 -0
  155. package/src/tokens/components/avatar.json +40 -0
  156. package/src/tokens/components/badge.json +41 -0
  157. package/src/tokens/components/breadcrumb.json +13 -0
  158. package/src/tokens/components/button.json +67 -0
  159. package/src/tokens/components/card.json +39 -0
  160. package/src/tokens/components/checkbox.json +64 -0
  161. package/src/tokens/components/closebutton.json +55 -0
  162. package/src/tokens/components/datatable.json +40 -0
  163. package/src/tokens/components/datepicker.json +99 -0
  164. package/src/tokens/components/drawer.json +23 -0
  165. package/src/tokens/components/field.json +29 -0
  166. package/src/tokens/components/fileupload.json +57 -0
  167. package/src/tokens/components/filterpopover.json +29 -0
  168. package/src/tokens/components/input.json +31 -0
  169. package/src/tokens/components/ipwhitelistfield.json +16 -0
  170. package/src/tokens/components/loader.json +15 -0
  171. package/src/tokens/components/menu.json +21 -0
  172. package/src/tokens/components/modal.json +25 -0
  173. package/src/tokens/components/pagination.json +45 -0
  174. package/src/tokens/components/phonefield.json +41 -0
  175. package/src/tokens/components/progress.json +12 -0
  176. package/src/tokens/components/radio.json +87 -0
  177. package/src/tokens/components/reportcard.json +17 -0
  178. package/src/tokens/components/selector.json +87 -0
  179. package/src/tokens/components/selectscroll.json +65 -0
  180. package/src/tokens/components/skeleton.json +6 -0
  181. package/src/tokens/components/stepper.json +38 -0
  182. package/src/tokens/components/subtabs.json +45 -0
  183. package/src/tokens/components/switch.json +55 -0
  184. package/src/tokens/components/tabs.json +38 -0
  185. package/src/tokens/components/textfield.json +59 -0
  186. package/src/tokens/components/togglebutton.json +48 -0
  187. package/src/tokens/components/uploaddropzone.json +61 -0
  188. package/src/tokens/drawer.ts +37 -0
  189. package/src/tokens/foundation/breakpoints.json +10 -0
  190. package/src/tokens/foundation/colors.json +122 -0
  191. package/src/tokens/foundation/opacity.json +16 -0
  192. package/src/tokens/foundation/radius.json +14 -0
  193. package/src/tokens/foundation/size.json +38 -0
  194. package/src/tokens/foundation/spacing.json +19 -0
  195. package/src/tokens/foundation/typography.json +43 -0
  196. package/src/tokens/index.ts +3665 -0
  197. package/src/tokens/modal.ts +42 -0
  198. package/src/tokens/semantic/color.json +148 -0
  199. package/src/tokens/semantic/field.json +28 -0
  200. package/src/tokens/semantic/opacity.json +5 -0
  201. package/src/tokens/semantic/size.json +722 -0
  202. package/src/tokens/semantic/spacing.json +27 -0
  203. package/src/tokens/semantic/typography.json +87 -0
  204. package/src/utils/scrollLock.ts +44 -0
  205. package/README.md +0 -3
@@ -0,0 +1,304 @@
1
+ import type { Meta, StoryObj } from "@storybook/react";
2
+ import PersonIcon from "@mui/icons-material/Person";
3
+ import { Avatar } from "./Avatar";
4
+ import type { AvatarProps } from "./Avatar.types";
5
+
6
+ const meta: Meta<typeof Avatar> = {
7
+ title: "Design System/Avatar",
8
+ component: Avatar,
9
+ tags: [],
10
+ decorators: [
11
+ (Story) => (
12
+ <div style={{ padding: "24px" }}>
13
+ <Story />
14
+ </div>
15
+ ),
16
+ ],
17
+ args: {
18
+ size: "md",
19
+ shape: "circle",
20
+ variant: "text",
21
+ initials: "AB",
22
+ alt: "",
23
+ } as AvatarProps,
24
+ argTypes: {
25
+ size: {
26
+ control: { type: "radio" },
27
+ options: ["xs", "sm", "md", "lg", "xl", "xxl", "xxxl"],
28
+ },
29
+ shape: {
30
+ control: { type: "radio" },
31
+ options: ["circle", "square"],
32
+ },
33
+ variant: {
34
+ control: { type: "radio" },
35
+ options: ["text", "icon", "image"],
36
+ },
37
+ initials: {
38
+ control: { type: "text" },
39
+ },
40
+ src: {
41
+ control: { type: "text" },
42
+ },
43
+ alt: {
44
+ control: { type: "text" },
45
+ },
46
+ iconType: {
47
+ control: { type: "radio" },
48
+ options: ["male", "female", "male-covered", "female-covered", "placeholder"],
49
+ },
50
+ },
51
+ };
52
+
53
+ export default meta;
54
+
55
+ type Story = StoryObj<typeof Avatar>;
56
+
57
+ export const Playground: Story = {
58
+ args: {
59
+ size: "md",
60
+ shape: "circle",
61
+ variant: "text",
62
+ initials: "AB",
63
+ },
64
+ };
65
+
66
+ const SIZES = ["xs", "sm", "md", "lg", "xl", "xxl", "xxxl"] as const;
67
+
68
+ /** Grid matching Figma (node 43-19436): 7 columns = 7 sizes, rows = variants. Compare with Figma side-by-side. */
69
+ const SIZE_LABELS: Record<(typeof SIZES)[number], string> = {
70
+ xs: "20px",
71
+ sm: "28px",
72
+ md: "36px",
73
+ lg: "44px",
74
+ xl: "60px",
75
+ xxl: "76px",
76
+ xxxl: "116px",
77
+ };
78
+ const gridRowStyle = {
79
+ display: "grid" as const,
80
+ gridTemplateColumns: "repeat(7, minmax(0, 1fr))",
81
+ gap: 16,
82
+ alignItems: "center",
83
+ justifyItems: "center",
84
+ marginBottom: 16,
85
+ };
86
+
87
+ export const FigmaGrid: Story = {
88
+ parameters: {
89
+ docs: {
90
+ description: {
91
+ story:
92
+ "Layout matches Figma Avatar grid (7 sizes × variants). Sizes: xs=20px, sm=28px, md=36px, lg=44px, xl=60px, xxl=76px, xxxl=116px.",
93
+ },
94
+ },
95
+ },
96
+ render: () => (
97
+ <div style={{ padding: "8px 0" }}>
98
+ {/* Size labels row */}
99
+ <div style={{ ...gridRowStyle, marginBottom: 8, fontSize: 11, color: "#666" }}>
100
+ {SIZES.map((s) => (
101
+ <span key={s}>
102
+ {s} ({SIZE_LABELS[s]})
103
+ </span>
104
+ ))}
105
+ </div>
106
+ {/* Row 1: Text, circle */}
107
+ <div style={gridRowStyle}>
108
+ {SIZES.map((s) => (
109
+ <Avatar key={s} size={s} shape="circle" variant="text" initials="AB" />
110
+ ))}
111
+ </div>
112
+ {/* Row 2 & 3: Icon (male/female) circle – use same icon for both for now */}
113
+ <div style={gridRowStyle}>
114
+ {SIZES.map((s) => (
115
+ <Avatar
116
+ key={s}
117
+ size={s}
118
+ shape="circle"
119
+ variant="icon"
120
+ icon={<PersonIcon style={{ width: "100%", height: "100%" }} />}
121
+ />
122
+ ))}
123
+ </div>
124
+ <div style={gridRowStyle}>
125
+ {SIZES.map((s) => (
126
+ <Avatar
127
+ key={s}
128
+ size={s}
129
+ shape="circle"
130
+ variant="icon"
131
+ icon={<PersonIcon style={{ width: "100%", height: "100%" }} />}
132
+ />
133
+ ))}
134
+ </div>
135
+ {/* Row 4: Image placeholder, circle */}
136
+ <div style={gridRowStyle}>
137
+ {SIZES.map((s) => (
138
+ <Avatar key={s} size={s} shape="circle" variant="image" />
139
+ ))}
140
+ </div>
141
+ {/* Row 5: Text, square */}
142
+ <div style={gridRowStyle}>
143
+ {SIZES.map((s) => (
144
+ <Avatar key={s} size={s} shape="square" variant="text" initials="AB" />
145
+ ))}
146
+ </div>
147
+ {/* Row 6 & 7: Icon, square */}
148
+ <div style={gridRowStyle}>
149
+ {SIZES.map((s) => (
150
+ <Avatar
151
+ key={s}
152
+ size={s}
153
+ shape="square"
154
+ variant="icon"
155
+ icon={<PersonIcon style={{ width: "100%", height: "100%" }} />}
156
+ />
157
+ ))}
158
+ </div>
159
+ <div style={gridRowStyle}>
160
+ {SIZES.map((s) => (
161
+ <Avatar
162
+ key={s}
163
+ size={s}
164
+ shape="square"
165
+ variant="icon"
166
+ icon={<PersonIcon style={{ width: "100%", height: "100%" }} />}
167
+ />
168
+ ))}
169
+ </div>
170
+ {/* Row 8: Image placeholder, square */}
171
+ <div style={gridRowStyle}>
172
+ {SIZES.map((s) => (
173
+ <Avatar key={s} size={s} shape="square" variant="image" />
174
+ ))}
175
+ </div>
176
+ </div>
177
+ ),
178
+ };
179
+
180
+ export const Showcase: Story = {
181
+ render: () => (
182
+ <div style={{ display: "flex", flexDirection: "column", gap: "24px" }}>
183
+ {/* Text avatars – circular – all sizes */}
184
+ <div>
185
+ <h3 style={{ marginBottom: 12, fontSize: 14, fontWeight: 600 }}>
186
+ Text (circle)
187
+ </h3>
188
+ <div
189
+ style={{
190
+ display: "flex",
191
+ gap: 16,
192
+ alignItems: "center",
193
+ flexWrap: "wrap",
194
+ }}
195
+ >
196
+ {SIZES.map((s) => (
197
+ <Avatar key={s} size={s} shape="circle" variant="text" initials="AB" />
198
+ ))}
199
+ </div>
200
+ </div>
201
+
202
+ {/* Text avatars – square – all sizes */}
203
+ <div>
204
+ <h3 style={{ marginBottom: 12, fontSize: 14, fontWeight: 600 }}>
205
+ Text (square)
206
+ </h3>
207
+ <div
208
+ style={{
209
+ display: "flex",
210
+ gap: 16,
211
+ alignItems: "center",
212
+ flexWrap: "wrap",
213
+ }}
214
+ >
215
+ {SIZES.map((s) => (
216
+ <Avatar key={s} size={s} shape="square" variant="text" initials="AB" />
217
+ ))}
218
+ </div>
219
+ </div>
220
+
221
+ {/* Icon avatars – circular – all sizes */}
222
+ <div>
223
+ <h3 style={{ marginBottom: 12, fontSize: 14, fontWeight: 600 }}>
224
+ Icon (circle)
225
+ </h3>
226
+ <div
227
+ style={{
228
+ display: "flex",
229
+ gap: 16,
230
+ alignItems: "center",
231
+ flexWrap: "wrap",
232
+ }}
233
+ >
234
+ {SIZES.map((s) => (
235
+ <Avatar
236
+ key={s}
237
+ size={s}
238
+ shape="circle"
239
+ variant="icon"
240
+ icon={<PersonIcon style={{ width: "100%", height: "100%" }} />}
241
+ />
242
+ ))}
243
+ </div>
244
+ </div>
245
+
246
+ {/* Icon avatars – square – all sizes */}
247
+ <div>
248
+ <h3 style={{ marginBottom: 12, fontSize: 14, fontWeight: 600 }}>
249
+ Icon (square)
250
+ </h3>
251
+ <div
252
+ style={{
253
+ display: "flex",
254
+ gap: 16,
255
+ alignItems: "center",
256
+ flexWrap: "wrap",
257
+ }}
258
+ >
259
+ {SIZES.map((s) => (
260
+ <Avatar
261
+ key={s}
262
+ size={s}
263
+ shape="square"
264
+ variant="icon"
265
+ icon={<PersonIcon style={{ width: "100%", height: "100%" }} />}
266
+ />
267
+ ))}
268
+ </div>
269
+ </div>
270
+
271
+ {/* Image placeholder – circular & square – all sizes */}
272
+ <div>
273
+ <h3 style={{ marginBottom: 12, fontSize: 14, fontWeight: 600 }}>
274
+ Image placeholder (no src)
275
+ </h3>
276
+ <div
277
+ style={{
278
+ display: "flex",
279
+ gap: 16,
280
+ alignItems: "center",
281
+ flexWrap: "wrap",
282
+ }}
283
+ >
284
+ {SIZES.map((s) => (
285
+ <Avatar key={s} size={s} shape="circle" variant="image" />
286
+ ))}
287
+ </div>
288
+ <div
289
+ style={{
290
+ display: "flex",
291
+ gap: 16,
292
+ alignItems: "center",
293
+ flexWrap: "wrap",
294
+ marginTop: 12,
295
+ }}
296
+ >
297
+ {SIZES.map((s) => (
298
+ <Avatar key={s} size={s} shape="square" variant="image" />
299
+ ))}
300
+ </div>
301
+ </div>
302
+ </div>
303
+ ),
304
+ };
@@ -0,0 +1,211 @@
1
+ import React, { useState, useCallback } from "react";
2
+ import { AVATAR_TOKENS } from "../../tokens";
3
+ import type { AvatarSize } from "../../tokens";
4
+ import type { AvatarProps } from "./Avatar.types";
5
+
6
+ function toPxString(value: string | number): string {
7
+ if (typeof value === "number") return `${value}px`;
8
+ const s = String(value).trim();
9
+ if (/^\d+$/.test(s)) return `${s}px`;
10
+ return s.endsWith("px") ? s : s ? `${s}px` : "16px";
11
+ }
12
+
13
+ function parsePx(value: string | number, fallback: number): number {
14
+ if (typeof value === "number") return value;
15
+ const match = String(value).match(/^(-?\d+)/);
16
+ return match ? parseInt(match[1], 10) : fallback;
17
+ }
18
+
19
+ const AVATAR_SIZE_KEYS: Record<AvatarSize, string> = {
20
+ xs: "Xs",
21
+ sm: "Sm",
22
+ md: "Md",
23
+ lg: "Lg",
24
+ xl: "Xl",
25
+ xxl: "Xxl",
26
+ xxxl: "Xxxl",
27
+ };
28
+
29
+ function getSizeTokenKey(size: AvatarSize): string {
30
+ return AVATAR_SIZE_KEYS[size] ?? "Md";
31
+ }
32
+
33
+ /** Default placeholder SVG (wave/mountain shape) for image fallback */
34
+ function PlaceholderSvg({
35
+ color,
36
+ backgroundColor,
37
+ sizePx,
38
+ }: {
39
+ color: string;
40
+ backgroundColor: string;
41
+ sizePx: number;
42
+ }) {
43
+ const s = sizePx;
44
+ return (
45
+ <svg
46
+ width={s}
47
+ height={s}
48
+ viewBox={`0 0 ${s} ${s}`}
49
+ fill="none"
50
+ xmlns="http://www.w3.org/2000/svg"
51
+ aria-hidden
52
+ >
53
+ <rect width={s} height={s} fill={backgroundColor} />
54
+ <path
55
+ d={`M 0 ${s * 0.6} Q ${s * 0.25} ${s * 0.35} ${s * 0.5} ${s * 0.55} T ${s} ${s * 0.5} V ${s} H 0 Z`}
56
+ fill={color}
57
+ />
58
+ <circle cx={s * 0.25} cy={s * 0.3} r={s * 0.05} fill={color} />
59
+ </svg>
60
+ );
61
+ }
62
+
63
+ /** Simple person silhouette for icon variant when no custom icon */
64
+ function PersonIconSvg({
65
+ color,
66
+ sizePx,
67
+ }: {
68
+ color: string;
69
+ sizePx: number;
70
+ }) {
71
+ const s = sizePx;
72
+ const stroke = Math.max(1, Math.round(sizePx / 24));
73
+ return (
74
+ <svg
75
+ width={s}
76
+ height={s}
77
+ viewBox="0 0 24 24"
78
+ fill="none"
79
+ stroke={color}
80
+ strokeWidth={stroke}
81
+ strokeLinecap="round"
82
+ strokeLinejoin="round"
83
+ xmlns="http://www.w3.org/2000/svg"
84
+ aria-hidden
85
+ >
86
+ <circle cx="12" cy="8" r="3" />
87
+ <path d="M5 21v-2a5 5 0 0 1 5-5h4a5 5 0 0 1 5 5v2" />
88
+ </svg>
89
+ );
90
+ }
91
+
92
+ export const Avatar = ({
93
+ size = "md",
94
+ shape = "circle",
95
+ variant = "text",
96
+ initials = "AB",
97
+ src = null,
98
+ alt = "",
99
+ iconType = "male",
100
+ icon,
101
+ style,
102
+ ...rest
103
+ }: AvatarProps) => {
104
+ const [imageError, setImageError] = useState(false);
105
+ const t = AVATAR_TOKENS;
106
+
107
+ const sizeKey = getSizeTokenKey(size as AvatarSize);
108
+ const sizeVal = toPxString(
109
+ (t[`size${sizeKey}` as keyof typeof t] as string) ?? t.sizeMd
110
+ );
111
+ const fontSizeVal = toPxString(
112
+ (t[`fontSize${sizeKey}` as keyof typeof t] as string) ?? t.fontSizeMd
113
+ );
114
+ const iconSizeVal = toPxString(
115
+ (t[`iconSize${sizeKey}` as keyof typeof t] as string) ?? t.iconSizeMd
116
+ );
117
+
118
+ const borderRadius =
119
+ shape === "circle" ? t.shapeCircle : t.shapeSquare;
120
+ const sizePx = parsePx(sizeVal, 28);
121
+ const iconSizePx = parsePx(iconSizeVal, 16);
122
+
123
+ const showImage = variant === "image" && src && !imageError;
124
+ const showPlaceholder =
125
+ variant === "image" && (!src || imageError);
126
+ const handleImageError = useCallback(() => setImageError(true), []);
127
+
128
+ const baseStyle: React.CSSProperties = {
129
+ width: sizeVal,
130
+ height: sizeVal,
131
+ minWidth: sizeVal,
132
+ minHeight: sizeVal,
133
+ borderRadius,
134
+ backgroundColor: t.background,
135
+ display: "inline-flex",
136
+ alignItems: "center",
137
+ justifyContent: "center",
138
+ overflow: "hidden",
139
+ boxSizing: "border-box",
140
+ ...style,
141
+ };
142
+
143
+ return (
144
+ <div
145
+ role="img"
146
+ aria-label={alt || (variant === "text" ? initials : undefined)}
147
+ style={baseStyle}
148
+ {...rest}
149
+ >
150
+ {variant === "text" && (
151
+ <span
152
+ style={{
153
+ color: t.textColor,
154
+ fontSize: fontSizeVal,
155
+ fontWeight: 600,
156
+ lineHeight: 1,
157
+ userSelect: "none",
158
+ }}
159
+ >
160
+ {initials.slice(0, 2).toUpperCase()}
161
+ </span>
162
+ )}
163
+
164
+ {variant === "icon" && (
165
+ <>
166
+ {icon !== undefined && icon !== null ? (
167
+ <span
168
+ style={{
169
+ display: "inline-flex",
170
+ alignItems: "center",
171
+ justifyContent: "center",
172
+ width: iconSizeVal,
173
+ height: iconSizeVal,
174
+ color: t.iconColor,
175
+ }}
176
+ >
177
+ {icon}
178
+ </span>
179
+ ) : (
180
+ <PersonIconSvg
181
+ color={t.iconColor}
182
+ sizePx={iconSizePx}
183
+ />
184
+ )}
185
+ </>
186
+ )}
187
+
188
+ {variant === "image" && showImage && (
189
+ <img
190
+ src={src!}
191
+ alt={alt}
192
+ onError={handleImageError}
193
+ style={{
194
+ width: "100%",
195
+ height: "100%",
196
+ objectFit: "cover",
197
+ display: "block",
198
+ }}
199
+ />
200
+ )}
201
+
202
+ {variant === "image" && showPlaceholder && (
203
+ <PlaceholderSvg
204
+ color={t.placeholderShape}
205
+ backgroundColor={t.placeholderBackground}
206
+ sizePx={sizePx}
207
+ />
208
+ )}
209
+ </div>
210
+ );
211
+ };
@@ -0,0 +1,33 @@
1
+ import type { AvatarSize } from "../../tokens";
2
+
3
+ export type AvatarShape = "circle" | "square";
4
+
5
+ export type AvatarVariant = "text" | "icon" | "image";
6
+
7
+ /** Built-in icon types for icon variant when no custom icon is provided */
8
+ export type AvatarIconType =
9
+ | "male"
10
+ | "female"
11
+ | "male-covered"
12
+ | "female-covered"
13
+ | "placeholder";
14
+
15
+ export interface AvatarProps
16
+ extends Omit<React.HTMLAttributes<HTMLDivElement>, "color"> {
17
+ /** Size of the avatar */
18
+ size?: AvatarSize;
19
+ /** Shape: circle (default) or square */
20
+ shape?: AvatarShape;
21
+ /** Content variant: text (initials), icon, or image */
22
+ variant?: AvatarVariant;
23
+ /** Initials for variant="text" (e.g. "AB") */
24
+ initials?: string;
25
+ /** Image URL for variant="image". Falls back to placeholder on load error. */
26
+ src?: string | null;
27
+ /** Alt text for variant="image" */
28
+ alt?: string;
29
+ /** Built-in icon type for variant="icon" when icon prop is not provided */
30
+ iconType?: AvatarIconType;
31
+ /** Custom icon element for variant="icon". Overrides iconType when provided. */
32
+ icon?: React.ReactNode;
33
+ }
@@ -0,0 +1,143 @@
1
+ import type { Meta, StoryObj } from '@storybook/react';
2
+ import { Badge } from './Badge';
3
+ import type { BadgeProps } from './Badge.types';
4
+
5
+ const meta: Meta<typeof Badge> = {
6
+ title: 'Design System/Badge',
7
+ component: Badge,
8
+ tags: [],
9
+ decorators: [
10
+ (Story) => (
11
+ <div
12
+ style={{
13
+ padding: '24px',
14
+ maxWidth: '420px',
15
+ backgroundColor: '#E5E9EB',
16
+ borderRadius: '8px',
17
+ }}
18
+ >
19
+ <Story />
20
+ </div>
21
+ ),
22
+ ],
23
+ args: {
24
+ children: 'Verified',
25
+ variant: 'default',
26
+ size: 'md',
27
+ disabled: false,
28
+ color: 'grey',
29
+ } as BadgeProps,
30
+ argTypes: {
31
+ variant: {
32
+ control: { type: 'radio' },
33
+ options: ['default', 'circle'],
34
+ },
35
+ color: {
36
+ control: { type: 'radio' },
37
+ options: ['grey', 'white'],
38
+ },
39
+ size: {
40
+ control: { type: 'radio' },
41
+ options: ['xs', 'sm', 'md', 'lg'],
42
+ },
43
+ children: {
44
+ control: { type: 'text' },
45
+ },
46
+ disabled: {
47
+ control: { type: 'boolean' },
48
+ },
49
+ },
50
+ };
51
+
52
+ export default meta;
53
+
54
+ type Story = StoryObj<typeof Badge>;
55
+
56
+ export const Playground: Story = {
57
+ args: {
58
+ children: 'Verified',
59
+ size: 'md',
60
+ },
61
+ };
62
+
63
+ export const Showcase: Story = {
64
+ render: () => (
65
+ <div style={{ display: 'flex', flexDirection: 'column', gap: '24px' }}>
66
+ {/* Default / Non-removable */}
67
+ <div style={{ display: 'flex', flexWrap: 'wrap', gap: '12px', alignItems: 'center' }}>
68
+ <Badge size="xs">Extra small</Badge>
69
+ <Badge size="sm">Small</Badge>
70
+ <Badge size="md">Medium</Badge>
71
+ <Badge size="lg">Large</Badge>
72
+ </div>
73
+
74
+ {/* Default / Non-removable - single */}
75
+ <Badge size="md">Verified</Badge>
76
+
77
+ {/* Removable */}
78
+ <div style={{ display: 'flex', flexWrap: 'wrap', gap: '12px', alignItems: 'center' }}>
79
+ <Badge size="xs" onRemove={() => {}}>
80
+ Extra small removable
81
+ </Badge>
82
+ <Badge size="sm" onRemove={() => {}}>
83
+ Small removable
84
+ </Badge>
85
+ <Badge size="md" onRemove={() => {}}>
86
+ Verified
87
+ </Badge>
88
+ <Badge size="lg" onRemove={() => {}}>
89
+ Large removable
90
+ </Badge>
91
+ </div>
92
+
93
+ {/* Removable - single */}
94
+ <Badge size="md" onRemove={() => {}}>
95
+ Verified
96
+ </Badge>
97
+
98
+ {/* Disabled */}
99
+ <div style={{ display: 'flex', flexWrap: 'wrap', gap: '12px', alignItems: 'center' }}>
100
+ <Badge size="md" disabled>
101
+ Disabled
102
+ </Badge>
103
+ <Badge size="md" onRemove={() => {}} disabled>
104
+ Disabled removable
105
+ </Badge>
106
+ </div>
107
+
108
+ {/* Badge with count */}
109
+ <div style={{ display: 'flex', flexWrap: 'wrap', gap: '12px', alignItems: 'center' }}>
110
+ <Badge size="xs">1</Badge>
111
+ <Badge size="sm">1</Badge>
112
+ <Badge size="md">2</Badge>
113
+ <Badge size="lg">4</Badge>
114
+ </div>
115
+
116
+ {/* Circle with border */}
117
+ <div style={{ display: 'flex', flexWrap: 'wrap', gap: '12px', alignItems: 'center' }}>
118
+ <Badge size="xs" variant="circle">
119
+ 1
120
+ </Badge>
121
+ <Badge size="sm" variant="circle">
122
+ 1
123
+ </Badge>
124
+ <Badge size="md" variant="circle">
125
+ 2
126
+ </Badge>
127
+ <Badge size="lg" variant="circle">
128
+ 4
129
+ </Badge>
130
+ </div>
131
+
132
+ {/* Color: white background */}
133
+ <div style={{ display: 'flex', flexWrap: 'wrap', gap: '12px', alignItems: 'center' }}>
134
+ <Badge size="md" color="white">
135
+ White
136
+ </Badge>
137
+ <Badge size="md" color="white" onRemove={() => {}}>
138
+ White removable
139
+ </Badge>
140
+ </div>
141
+ </div>
142
+ ),
143
+ };