@altruist03/ui 0.1.0 → 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 (207) hide show
  1. package/assets/images/noData.webp +0 -0
  2. package/dist/index.js +15237 -650
  3. package/dist/index.js.map +1 -1
  4. package/dist/noData-XPRHKMFR.webp +0 -0
  5. package/package.json +21 -7
  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/dist/index.css +0 -514
  206. package/dist/index.css.map +0 -1
  207. package/dist/index.d.ts +0 -289
@@ -0,0 +1,143 @@
1
+ import React, { useEffect, useRef, useCallback } from "react";
2
+ import { useTokens } from "../../ThemeProvider/ThemeContext";
3
+ import { getMenuTokensFromResolved } from "../../tokens";
4
+ import type { MenuProps } from "./Menu.types";
5
+
6
+ export function Menu({
7
+ open,
8
+ onClose,
9
+ items,
10
+ value,
11
+ onSelect,
12
+ minWidth,
13
+ anchorRef,
14
+ closeOnSelect = true,
15
+ }: MenuProps) {
16
+ const resolvedTokens = useTokens();
17
+ const t = getMenuTokensFromResolved(resolvedTokens);
18
+ const resolvedMinWidth =
19
+ minWidth ??
20
+ (typeof t.minWidth === "string" && /^\d+$/.test(t.minWidth)
21
+ ? `${t.minWidth}px`
22
+ : t.minWidth);
23
+ const menuRef = useRef<HTMLDivElement>(null);
24
+
25
+ const handleClickOutside = useCallback(
26
+ (e: MouseEvent) => {
27
+ const target = e.target as Node;
28
+ if (
29
+ !anchorRef.current?.contains(target) &&
30
+ !menuRef.current?.contains(target)
31
+ ) {
32
+ onClose();
33
+ }
34
+ },
35
+ [anchorRef, onClose]
36
+ );
37
+
38
+ useEffect(() => {
39
+ if (!open) return;
40
+ document.addEventListener("mousedown", handleClickOutside);
41
+ return () => document.removeEventListener("mousedown", handleClickOutside);
42
+ }, [open, handleClickOutside]);
43
+
44
+ const handleItemClick = useCallback(
45
+ (itemValue: string) => {
46
+ onSelect?.(itemValue);
47
+ if (closeOnSelect) onClose();
48
+ },
49
+ [onSelect, onClose, closeOnSelect]
50
+ );
51
+
52
+ if (!open) return null;
53
+
54
+ const panelStyle: React.CSSProperties = {
55
+ position: "absolute",
56
+ top: "100%",
57
+ right: 0,
58
+ marginTop: t.marginTop,
59
+ minWidth:
60
+ typeof resolvedMinWidth === "number"
61
+ ? `${resolvedMinWidth}px`
62
+ : resolvedMinWidth,
63
+ maxHeight: /^\d+$/.test(String(t.listMaxHeight))
64
+ ? `${t.listMaxHeight}px`
65
+ : t.listMaxHeight,
66
+ overflowY: "auto",
67
+ background: t.background,
68
+ borderRadius: t.borderRadius,
69
+ boxShadow: "0 4px 12px rgba(0, 0, 0, 0.12)",
70
+ padding: t.padding,
71
+ listStyle: "none",
72
+ zIndex: 1000,
73
+ };
74
+
75
+ const panel = (
76
+ <div
77
+ ref={menuRef}
78
+ role="menu"
79
+ aria-orientation="vertical"
80
+ style={panelStyle}
81
+ >
82
+ {items.map((item) => {
83
+ const selected = value != null && item.value === value;
84
+ return (
85
+ <div
86
+ key={item.value}
87
+ role="menuitem"
88
+ aria-selected={selected}
89
+ aria-disabled={item.disabled}
90
+ style={{
91
+ display: "flex",
92
+ alignItems: "center",
93
+ gap: t.itemIconLabelGap,
94
+ padding: `${t.itemPaddingY} ${t.itemPaddingX}`,
95
+ borderRadius: t.itemBorderRadius,
96
+ fontFamily: t.fontFamily,
97
+ fontSize: t.fontSize,
98
+ fontWeight: t.fontWeight,
99
+ lineHeight: t.lineHeight,
100
+ color: t.textColor,
101
+ cursor: item.disabled ? "not-allowed" : "pointer",
102
+ backgroundColor: selected ? t.selectedBackground : "transparent",
103
+ opacity: item.disabled ? 0.6 : 1,
104
+ }}
105
+ onMouseEnter={(e) => {
106
+ if (!item.disabled)
107
+ e.currentTarget.style.backgroundColor = t.hoverBackground;
108
+ }}
109
+ onMouseLeave={(e) => {
110
+ e.currentTarget.style.backgroundColor = selected
111
+ ? t.selectedBackground
112
+ : "transparent";
113
+ }}
114
+ onMouseDown={(e) => {
115
+ e.preventDefault();
116
+ if (!item.disabled) handleItemClick(item.value);
117
+ }}
118
+ >
119
+ {item.icon != null && (
120
+ <span
121
+ style={{
122
+ display: "flex",
123
+ alignItems: "center",
124
+ justifyContent: "center",
125
+ fontSize: 20,
126
+ width: 20,
127
+ height: 20,
128
+ color: "inherit",
129
+ flexShrink: 0,
130
+ }}
131
+ >
132
+ {item.icon}
133
+ </span>
134
+ )}
135
+ <span style={{ flex: 1, textAlign: "left" }}>{item.label}</span>
136
+ </div>
137
+ );
138
+ })}
139
+ </div>
140
+ );
141
+
142
+ return panel;
143
+ }
@@ -0,0 +1,28 @@
1
+ import type { ReactNode, RefObject } from "react";
2
+
3
+ export interface MenuItem {
4
+ value: string;
5
+ label: string;
6
+ /** Optional icon (e.g. MUI icon) shown left of label */
7
+ icon?: ReactNode;
8
+ disabled?: boolean;
9
+ }
10
+
11
+ export interface MenuProps {
12
+ /** Whether the menu is open */
13
+ open: boolean;
14
+ /** Called when menu should close (outside click or selection) */
15
+ onClose: () => void;
16
+ /** Menu options (icon + label); selected state derived from value */
17
+ items: MenuItem[];
18
+ /** Currently selected value */
19
+ value?: string | null;
20
+ /** Called when an item is selected */
21
+ onSelect?: (value: string) => void;
22
+ /** Minimum width of the dropdown panel (default from design) */
23
+ minWidth?: number | string;
24
+ /** Ref of the trigger element to position menu below */
25
+ anchorRef: RefObject<HTMLElement | null>;
26
+ /** Close when an item is selected (default true) */
27
+ closeOnSelect?: boolean;
28
+ }
@@ -0,0 +1,140 @@
1
+ import React from "react";
2
+ import type { Meta, StoryObj } from "@storybook/react";
3
+ import { useState, useRef } from "react";
4
+ import { MenuButton } from "./MenuButton";
5
+ import { Menu } from "../Menu/Menu";
6
+ import type { MenuItem } from "../Menu/Menu.types";
7
+ import type { MenuButtonProps } from "./MenuButton.types";
8
+ import BusinessIcon from "@mui/icons-material/Business";
9
+ import LockIcon from "@mui/icons-material/Lock";
10
+ import ScheduleIcon from "@mui/icons-material/Schedule";
11
+
12
+ const meta: Meta<typeof MenuButton> = {
13
+ title: "Design System/MenuButton",
14
+ component: MenuButton,
15
+ tags: [],
16
+ decorators: [
17
+ (Story) => (
18
+ <div style={{ padding: "24px", maxWidth: "420px" }}>
19
+ <Story />
20
+ </div>
21
+ ),
22
+ ],
23
+ args: {
24
+ children: "Button",
25
+ open: false,
26
+ disabled: false,
27
+ minWidth: 120,
28
+ variant: "blue",
29
+ size: "md",
30
+ useDefaultLabel: false,
31
+ } as MenuButtonProps,
32
+ argTypes: {
33
+ variant: {
34
+ control: { type: "select" },
35
+ options: ["blue", "red", "grey"],
36
+ },
37
+ size: {
38
+ control: { type: "radio" },
39
+ options: ["sm", "md", "lg"],
40
+ },
41
+ open: {
42
+ control: { type: "boolean" },
43
+ description: "Whether the menu is open (rotates chevron)",
44
+ },
45
+ disabled: {
46
+ control: { type: "boolean" },
47
+ },
48
+ minWidth: {
49
+ control: { type: "number" },
50
+ },
51
+ children: {
52
+ control: { type: "text" },
53
+ description: "Trigger label",
54
+ },
55
+ useDefaultLabel: {
56
+ control: { type: "boolean" },
57
+ description: "When true, show default label 'Button' instead of children",
58
+ },
59
+ },
60
+ };
61
+
62
+ export default meta;
63
+
64
+ type Story = StoryObj<typeof MenuButton>;
65
+
66
+ export const Playground: Story = {
67
+ args: {
68
+ children: "Button",
69
+ open: false,
70
+ minWidth: 120,
71
+ variant: "blue",
72
+ size: "md",
73
+ },
74
+ };
75
+
76
+ const menuItems: MenuItem[] = [
77
+ { value: "company", label: "Company", icon: <BusinessIcon /> },
78
+ { value: "security", label: "Security", icon: <LockIcon /> },
79
+ { value: "short-visit", label: "Short visit", icon: <ScheduleIcon /> },
80
+ ];
81
+
82
+ export const Showcase: Story = {
83
+ render: () => (
84
+ <div style={{ display: "flex", flexDirection: "column", gap: "24px" }}>
85
+ {/* Default / Closed */}
86
+ <MenuButton open={false} minWidth={120}>
87
+ Button
88
+ </MenuButton>
89
+
90
+ {/* Open (chevron rotated) */}
91
+ <MenuButton open minWidth={120}>
92
+ Button
93
+ </MenuButton>
94
+
95
+ {/* With Menu – interactive */}
96
+ <WithMenuExample />
97
+
98
+ {/* Disabled */}
99
+ <MenuButton open={false} disabled minWidth={120}>
100
+ Button
101
+ </MenuButton>
102
+
103
+ {/* Grey variant */}
104
+ <MenuButton open={false} variant="grey" minWidth={120}>
105
+ Options
106
+ </MenuButton>
107
+
108
+ {/* useDefaultLabel: children is "Security" but label shows "Button" */}
109
+ <MenuButton open={false} minWidth={120} useDefaultLabel>
110
+ Security
111
+ </MenuButton>
112
+ </div>
113
+ ),
114
+ };
115
+
116
+ function WithMenuExample() {
117
+ const [open, setOpen] = useState(false);
118
+ const [value, setValue] = useState<string | null>("security");
119
+ const anchorRef = useRef<HTMLDivElement>(null);
120
+
121
+ return (
122
+ <div
123
+ ref={anchorRef}
124
+ style={{ position: "relative", display: "inline-block" }}
125
+ >
126
+ <MenuButton open={open} onClick={() => setOpen((o) => !o)} minWidth={120}>
127
+ {(menuItems.find((i) => i.value === value) ?? menuItems[0]).label}
128
+ </MenuButton>
129
+ <Menu
130
+ open={open}
131
+ onClose={() => setOpen(false)}
132
+ anchorRef={anchorRef}
133
+ items={menuItems}
134
+ value={value}
135
+ onSelect={setValue}
136
+ minWidth={180}
137
+ />
138
+ </div>
139
+ );
140
+ }
@@ -0,0 +1,45 @@
1
+ import { Button } from '../Button/Button';
2
+ import type { MenuButtonProps } from './MenuButton.types';
3
+ import ExpandMoreIcon from '@mui/icons-material/ExpandMore';
4
+
5
+ const DEFAULT_MIN_WIDTH = 120;
6
+
7
+ export function MenuButton({
8
+ children,
9
+ open = false,
10
+ variant = 'blue',
11
+ size = 'md',
12
+ disabled = false,
13
+ minWidth = DEFAULT_MIN_WIDTH,
14
+ style,
15
+ leftIcon,
16
+ ...rest
17
+ }: MenuButtonProps) {
18
+ return (
19
+ <Button
20
+ type="button"
21
+ variant={variant}
22
+ size={size}
23
+ shape="pill"
24
+ disabled={disabled}
25
+ leftIcon={leftIcon ?? null}
26
+ rightIcon={
27
+ <ExpandMoreIcon
28
+ style={{
29
+ transform: open ? 'rotate(180deg)' : 'rotate(0deg)',
30
+ transition: 'transform 0.2s ease',
31
+ }}
32
+ />
33
+ }
34
+ style={{
35
+ minWidth: typeof minWidth === 'number' ? `${minWidth}px` : minWidth,
36
+ ...style,
37
+ }}
38
+ aria-expanded={open}
39
+ aria-haspopup="menu"
40
+ {...rest}
41
+ >
42
+ {children}
43
+ </Button>
44
+ );
45
+ }
@@ -0,0 +1,21 @@
1
+ import type { ButtonVariant, ButtonSize } from "../Button/Button.types";
2
+ import type { ButtonHTMLAttributes, ReactNode, MouseEvent } from "react";
3
+
4
+ export interface MenuButtonProps
5
+ extends Omit<ButtonHTMLAttributes<HTMLButtonElement>, "color" | "children"> {
6
+ /** Trigger label (e.g. "Button", or current menu selection) */
7
+ children: ReactNode;
8
+ /** Whether the menu is open (used to rotate chevron) */
9
+ open?: boolean;
10
+ /** Button variant; default "blue" per Figma */
11
+ variant?: ButtonVariant;
12
+ size?: ButtonSize;
13
+ disabled?: boolean;
14
+ leftIcon?: ReactNode;
15
+ /** Minimum width of the button (default from design) */
16
+ minWidth?: number | string;
17
+ /** When true, show default label "Button" and do not change it on menu item click. Default false. */
18
+ useDefaultLabel?: boolean;
19
+ /** Click handler */
20
+ onClick?: (e: MouseEvent<HTMLButtonElement>) => void;
21
+ }
@@ -0,0 +1,266 @@
1
+ import React, { useState } from 'react';
2
+ import type { Meta, StoryObj } from '@storybook/react';
3
+ import Modal from './Modal';
4
+ import { Button } from '../Button/Button';
5
+
6
+ const meta: Meta<typeof Modal> = {
7
+ title: 'Design System/Modal',
8
+ component: Modal,
9
+ tags: ['autodocs'],
10
+ decorators: [
11
+ (Story) => (
12
+ <div style={{ padding: '24px', fontFamily: 'Inter, sans-serif' }}>
13
+ <Story />
14
+ </div>
15
+ ),
16
+ ],
17
+ args: {
18
+ title: 'Modal Title',
19
+ size: 'sm',
20
+ },
21
+ argTypes: {
22
+ size: {
23
+ control: { type: 'radio' },
24
+ options: ['sm', 'lg'],
25
+ },
26
+ title: {
27
+ control: { type: 'text' },
28
+ },
29
+ },
30
+ };
31
+
32
+ export default meta;
33
+
34
+ type Story = StoryObj<typeof Modal>;
35
+
36
+ export const Playground: Story = {
37
+ render: (args) => {
38
+ // eslint-disable-next-line react-hooks/rules-of-hooks
39
+ const [isOpen, setIsOpen] = useState(false);
40
+ return (
41
+ <>
42
+ <Button onClick={() => setIsOpen(true)}>Open Playground Modal</Button>
43
+ <Modal {...args} isOpen={isOpen} onClose={() => setIsOpen(false)}>
44
+ <div style={{ padding: '20px' }}>
45
+ <p>
46
+ This is the playground modal. You can adjust its size, title, and other properties
47
+ through the Storybook controls below.
48
+ </p>
49
+ </div>
50
+ </Modal>
51
+ </>
52
+ );
53
+ },
54
+ args: {
55
+ title: 'Playground Modal',
56
+ size: 'sm',
57
+ },
58
+ };
59
+
60
+ export const Showcase: Story = {
61
+ render: () => {
62
+ // eslint-disable-next-line react-hooks/rules-of-hooks
63
+ const [openSmall, setOpenSmall] = useState(false);
64
+ // eslint-disable-next-line react-hooks/rules-of-hooks
65
+ const [openLarge, setOpenLarge] = useState(false);
66
+ // eslint-disable-next-line react-hooks/rules-of-hooks
67
+ const [openConfirmation, setOpenConfirmation] = useState(false);
68
+ // eslint-disable-next-line react-hooks/rules-of-hooks
69
+ const [openError, setOpenError] = useState(false);
70
+ // eslint-disable-next-line react-hooks/rules-of-hooks
71
+ const [openAlert, setOpenAlert] = useState(false);
72
+
73
+ return (
74
+ <div style={{ display: 'flex', flexDirection: 'column', gap: '24px' }}>
75
+ <div style={{ display: 'flex', gap: '16px', flexWrap: 'wrap' }}>
76
+ <Button onClick={() => setOpenSmall(true)}>Open Default (Small)</Button>
77
+ <Button onClick={() => setOpenLarge(true)}>Open Large (1200px)</Button>
78
+ <Button onClick={() => setOpenConfirmation(true)}>Open Confirmation Variant</Button>
79
+ <Button variant="red" onClick={() => setOpenError(true)}>
80
+ Open Error Variant
81
+ </Button>
82
+ <Button variant="grey" onClick={() => setOpenAlert(true)}>
83
+ Open Alert (reset form)
84
+ </Button>
85
+ </div>
86
+
87
+ {/* Small Modal */}
88
+ <Modal
89
+ title="Default Size Modal"
90
+ size="sm"
91
+ isOpen={openSmall}
92
+ onClose={() => setOpenSmall(false)}
93
+ >
94
+ <div style={{ padding: '20px' }}>
95
+ <p>
96
+ This is the standard modal content. It uses the design system's spacing and typography
97
+ sizes automatically.
98
+ </p>
99
+ </div>
100
+ </Modal>
101
+
102
+ {/* Large Modal with Footer */}
103
+ <Modal
104
+ title="Large Modal Title"
105
+ size="lg"
106
+ isOpen={openLarge}
107
+ onClose={() => setOpenLarge(false)}
108
+ >
109
+ <div style={{ padding: '20px', display: 'flex', flexDirection: 'column', gap: '16px' }}>
110
+ <p>
111
+ This is a standard "lg" size modal. The backdrop overlay covers the entire viewport.
112
+ This example includes a custom action section in the body.
113
+ </p>
114
+ <div
115
+ style={{
116
+ display: 'flex',
117
+ justifyContent: 'flex-end',
118
+ gap: '10px',
119
+ borderTop: '1px solid #e5e7eb',
120
+ paddingTop: '12px',
121
+ }}
122
+ >
123
+ <Button variant="grey" onClick={() => setOpenLarge(false)}>
124
+ Cancel
125
+ </Button>
126
+ <Button onClick={() => setOpenLarge(false)}>Save Changes</Button>
127
+ </div>
128
+ </div>
129
+ </Modal>
130
+
131
+ {/* Confirmation Variant */}
132
+ <Modal
133
+ title="Confirm Changes"
134
+ size="sm"
135
+ isOpen={openConfirmation}
136
+ onClose={() => setOpenConfirmation(false)}
137
+ variant="confirmation"
138
+ >
139
+ <div style={{ padding: '20px', display: 'flex', flexDirection: 'column', gap: '18px' }}>
140
+ <div style={{ display: 'flex', alignItems: 'center', gap: '12px' }}>
141
+ <div
142
+ style={{
143
+ width: '36px',
144
+ height: '36px',
145
+ borderRadius: '999px',
146
+ backgroundColor: '#d1fadf',
147
+ color: '#027a48',
148
+ display: 'grid',
149
+ placeItems: 'center',
150
+ fontWeight: 700,
151
+ fontSize: '16px',
152
+ }}
153
+ >
154
+ Y
155
+ </div>
156
+ <div>
157
+ <div style={{ fontWeight: 600, color: '#111827' }}>Everything looks good</div>
158
+ <div style={{ fontSize: '14px', color: '#4b5563', marginTop: '2px' }}>
159
+ Please confirm to apply the updated client information.
160
+ </div>
161
+ </div>
162
+ </div>
163
+ <div
164
+ style={{
165
+ backgroundColor: '#f8fafc',
166
+ border: '1px solid #e2e8f0',
167
+ borderRadius: '10px',
168
+ padding: '12px',
169
+ fontSize: '14px',
170
+ color: '#475467',
171
+ }}
172
+ >
173
+ This action will immediately save changes and refresh the client profile details.
174
+ </div>
175
+ <div
176
+ style={{
177
+ display: 'flex',
178
+ justifyContent: 'flex-end',
179
+ gap: '10px',
180
+ borderTop: '1px solid #e5e7eb',
181
+ paddingTop: '12px',
182
+ }}
183
+ >
184
+ <Button variant="grey" onClick={() => setOpenConfirmation(false)}>
185
+ Review Again
186
+ </Button>
187
+ <Button onClick={() => setOpenConfirmation(false)}>Confirm</Button>
188
+ </div>
189
+ </div>
190
+ </Modal>
191
+
192
+ {/* Alert variant — centered reset-style confirmation */}
193
+ <Modal
194
+ variant="alert"
195
+ title="Reset Form"
196
+ isOpen={openAlert}
197
+ onClose={() => setOpenAlert(false)}
198
+ onAlertConfirm={() => setOpenAlert(false)}
199
+ />
200
+
201
+ {/* Error Variant */}
202
+ <Modal
203
+ title="Unable to Continue"
204
+ size="sm"
205
+ isOpen={openError}
206
+ onClose={() => setOpenError(false)}
207
+ variant="error"
208
+ >
209
+ <div style={{ padding: '20px', display: 'flex', flexDirection: 'column', gap: '18px' }}>
210
+ <div style={{ display: 'flex', alignItems: 'center', gap: '12px' }}>
211
+ <div
212
+ style={{
213
+ width: '36px',
214
+ height: '36px',
215
+ borderRadius: '999px',
216
+ backgroundColor: '#fee4e2',
217
+ color: '#b42318',
218
+ display: 'grid',
219
+ placeItems: 'center',
220
+ fontWeight: 700,
221
+ fontSize: '16px',
222
+ }}
223
+ >
224
+ !
225
+ </div>
226
+ <div>
227
+ <div style={{ fontWeight: 600, color: '#111827' }}>Submission failed</div>
228
+ <div style={{ fontSize: '14px', color: '#4b5563', marginTop: '2px' }}>
229
+ We could not save your request because of a validation error.
230
+ </div>
231
+ </div>
232
+ </div>
233
+ <div
234
+ style={{
235
+ backgroundColor: '#fffbfa',
236
+ border: '1px solid #fecdca',
237
+ borderRadius: '10px',
238
+ padding: '12px',
239
+ fontSize: '14px',
240
+ color: '#b42318',
241
+ }}
242
+ >
243
+ Please verify required fields and try again. If this continues, contact support.
244
+ </div>
245
+ <div
246
+ style={{
247
+ display: 'flex',
248
+ justifyContent: 'flex-end',
249
+ gap: '10px',
250
+ borderTop: '1px solid #e5e7eb',
251
+ paddingTop: '12px',
252
+ }}
253
+ >
254
+ <Button variant="grey" onClick={() => setOpenError(false)}>
255
+ Dismiss
256
+ </Button>
257
+ <Button variant="red" onClick={() => setOpenError(false)}>
258
+ Try Again
259
+ </Button>
260
+ </div>
261
+ </div>
262
+ </Modal>
263
+ </div>
264
+ );
265
+ },
266
+ };