@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.
- package/assets/images/noData.webp +0 -0
- package/dist/index.js +15318 -0
- package/dist/index.js.map +1 -0
- package/dist/noData-XPRHKMFR.webp +0 -0
- package/package.json +51 -4
- package/src/ThemeProvider/ThemeContext.tsx +75 -0
- package/src/ThemeProvider/index.ts +7 -0
- package/src/assets/images/noData.webp +0 -0
- package/src/assets.d.ts +10 -0
- package/src/components/Accordion/Accordion.stories.tsx +64 -0
- package/src/components/Accordion/Accordion.tsx +219 -0
- package/src/components/Accordion/Accordion.types.ts +16 -0
- package/src/components/Avatar/Avatar.stories.tsx +304 -0
- package/src/components/Avatar/Avatar.tsx +211 -0
- package/src/components/Avatar/Avatar.types.ts +33 -0
- package/src/components/Badge/Badge.stories.tsx +143 -0
- package/src/components/Badge/Badge.tsx +138 -0
- package/src/components/Badge/Badge.types.ts +24 -0
- package/src/components/Breadcrumb/Breadcrumb.stories.tsx +114 -0
- package/src/components/Breadcrumb/Breadcrumb.tsx +120 -0
- package/src/components/Breadcrumb/Breadcrumb.types.ts +22 -0
- package/src/components/Button/Button.stories.tsx +457 -0
- package/src/components/Button/Button.tsx +202 -0
- package/src/components/Button/Button.types.ts +18 -0
- package/src/components/Checkbox/Checkbox.stories.tsx +254 -0
- package/src/components/Checkbox/Checkbox.tsx +352 -0
- package/src/components/Checkbox/Checkbox.types.ts +123 -0
- package/src/components/CloseButton/CloseButton.stories.tsx +100 -0
- package/src/components/CloseButton/CloseButton.tsx +117 -0
- package/src/components/CloseButton/CloseButton.types.ts +15 -0
- package/src/components/CollapsibleSearch/CollapsibleSearch.tsx +120 -0
- package/src/components/DataTable/DataTable.stories.tsx +233 -0
- package/src/components/DataTable/DataTable.tsx +968 -0
- package/src/components/DataTable/DataTable.types.ts +49 -0
- package/src/components/DatePicker/DatePicker.stories.tsx +341 -0
- package/src/components/DatePicker/DatePicker.tsx +669 -0
- package/src/components/DatePicker/DatePicker.types.ts +105 -0
- package/src/components/DatePicker/DateTimePicker.tsx +35 -0
- package/src/components/DateRangePicker/DateRangePicker.tsx +179 -0
- package/src/components/DateRangePicker/DateRangePicker.types.ts +34 -0
- package/src/components/Drawer/Drawer.stories.tsx +243 -0
- package/src/components/Drawer/Drawer.tsx +196 -0
- package/src/components/Drawer/Drawer.types.ts +60 -0
- package/src/components/FileUpload/FileUpload.stories.tsx +162 -0
- package/src/components/FileUpload/FileUpload.tsx +216 -0
- package/src/components/FileUpload/FileUpload.types.ts +80 -0
- package/src/components/FilterPopover/FilterPopover.stories.tsx +173 -0
- package/src/components/FilterPopover/FilterPopover.tsx +310 -0
- package/src/components/FilterPopover/FilterPopover.types.ts +26 -0
- package/src/components/ImageViewer/ImageViewerToolbar.jsx +81 -0
- package/src/components/ImageViewer/useImagePanRotate.js +102 -0
- package/src/components/IpWhitelistField/IpWhitelistField.stories.tsx +138 -0
- package/src/components/IpWhitelistField/IpWhitelistField.tsx +164 -0
- package/src/components/IpWhitelistField/IpWhitelistField.types.ts +42 -0
- package/src/components/Loader/Loader.stories.tsx +95 -0
- package/src/components/Loader/Loader.tsx +132 -0
- package/src/components/Loader/Loader.types.ts +6 -0
- package/src/components/Menu/Menu.tsx +143 -0
- package/src/components/Menu/Menu.types.ts +28 -0
- package/src/components/MenuButton/MenuButton.stories.tsx +140 -0
- package/src/components/MenuButton/MenuButton.tsx +45 -0
- package/src/components/MenuButton/MenuButton.types.ts +21 -0
- package/src/components/Modal/Modal.stories.tsx +266 -0
- package/src/components/Modal/Modal.tsx +399 -0
- package/src/components/Modal/index.ts +2 -0
- package/src/components/Modal/types.ts +126 -0
- package/src/components/Pagination/Pagination.stories.tsx +167 -0
- package/src/components/Pagination/Pagination.tsx +266 -0
- package/src/components/Pagination/Pagination.types.ts +20 -0
- package/src/components/PeriodSelector/PeriodSelector.stories.tsx +50 -0
- package/src/components/PeriodSelector/PeriodSelector.tsx +47 -0
- package/src/components/PeriodSelector/PeriodSelector.types.ts +10 -0
- package/src/components/PhoneField/PhoneField.stories.tsx +141 -0
- package/src/components/PhoneField/PhoneField.tsx +284 -0
- package/src/components/PhoneField/PhoneField.types.ts +89 -0
- package/src/components/PhoneField/countryCodes.ts +236 -0
- package/src/components/Progress/Progress.stories.tsx +83 -0
- package/src/components/Progress/Progress.tsx +91 -0
- package/src/components/Progress/Progress.types.ts +22 -0
- package/src/components/RadioButton/RadioButton.stories.tsx +332 -0
- package/src/components/RadioButton/RadioButton.tsx +427 -0
- package/src/components/RadioButton/RadioButton.types.ts +117 -0
- package/src/components/ReportCard/ReportCard.tsx +183 -0
- package/src/components/ReportCard/ReportCard.types.ts +17 -0
- package/src/components/SectionIcon/SectionIcon.tsx +87 -0
- package/src/components/SectionIcon/SectionIcon.types.ts +23 -0
- package/src/components/SegmentedInputField/SegmentedInputField.stories.tsx +165 -0
- package/src/components/SegmentedInputField/SegmentedInputField.tsx +174 -0
- package/src/components/SegmentedInputField/SegmentedInputField.types.ts +51 -0
- package/src/components/SelectScroll/SelectScroll.stories.tsx +336 -0
- package/src/components/SelectScroll/SelectScroll.tsx +1119 -0
- package/src/components/SelectScroll/SelectScroll.types.ts +60 -0
- package/src/components/Selector/Selector.stories.tsx +382 -0
- package/src/components/Selector/Selector.tsx +1310 -0
- package/src/components/Selector/Selector.types.ts +71 -0
- package/src/components/Skeleton/Skeleton.stories.tsx +133 -0
- package/src/components/Skeleton/Skeleton.tsx +20 -0
- package/src/components/Skeleton/Skeleton.types.ts +3 -0
- package/src/components/Spinner/Spinner.stories.tsx +84 -0
- package/src/components/Spinner/Spinner.tsx +79 -0
- package/src/components/Spinner/Spinner.types.ts +8 -0
- package/src/components/StatCard/StatCard.stories.tsx +125 -0
- package/src/components/StatCard/StatCard.tsx +167 -0
- package/src/components/StatCard/StatCard.types.ts +20 -0
- package/src/components/StatusBadge/StatusBadge.stories.tsx +179 -0
- package/src/components/StatusBadge/StatusBadge.tsx +74 -0
- package/src/components/StatusBadge/StatusBadge.types.ts +23 -0
- package/src/components/Stepper/Stepper.stories.tsx +85 -0
- package/src/components/Stepper/Stepper.tsx +138 -0
- package/src/components/Stepper/Stepper.types.ts +16 -0
- package/src/components/SubTabs/SubTabs.stories.tsx +128 -0
- package/src/components/SubTabs/SubTabs.tsx +279 -0
- package/src/components/SubTabs/SubTabs.types.ts +44 -0
- package/src/components/SummaryCard/SummaryCard.stories.tsx +100 -0
- package/src/components/SummaryCard/SummaryCard.tsx +141 -0
- package/src/components/SummaryCard/SummaryCard.types.ts +22 -0
- package/src/components/Switch/Switch.stories.tsx +59 -0
- package/src/components/Switch/Switch.tsx +217 -0
- package/src/components/Switch/Switch.types.ts +22 -0
- package/src/components/Tabs/Tabs.stories.tsx +178 -0
- package/src/components/Tabs/Tabs.tsx +197 -0
- package/src/components/Tabs/Tabs.types.ts +34 -0
- package/src/components/TextField/TextField.stories.tsx +249 -0
- package/src/components/TextField/TextField.tsx +462 -0
- package/src/components/TextField/TextField.types.ts +141 -0
- package/src/components/Toast/Toast.stories.tsx +237 -0
- package/src/components/Toast/Toast.tsx +246 -0
- package/src/components/Toast/Toast.types.ts +31 -0
- package/src/components/ToggleButton/ToggleButton.stories.tsx +105 -0
- package/src/components/ToggleButton/ToggleButton.tsx +267 -0
- package/src/components/ToggleButton/ToggleButton.types.ts +19 -0
- package/src/components/UploadDropzone/UploadDropzone.jsx +78 -0
- package/src/components/UploadDropzone2/UploadDropzone.stories.tsx +94 -0
- package/src/components/UploadDropzone2/UploadDropzone.tsx +222 -0
- package/src/components/UploadDropzone2/UploadDropzone.types.ts +54 -0
- package/src/components/ViewEditToggle/ViewEditToggle.stories.tsx +40 -0
- package/src/components/ViewEditToggle/ViewEditToggle.tsx +239 -0
- package/src/components/ViewEditToggle/ViewEditToggle.types.ts +27 -0
- package/src/components/index.ts +157 -0
- package/src/icons/ReportDollarIcon.tsx +71 -0
- package/src/icons/index.ts +27 -0
- package/src/index.ts +214 -0
- package/src/storybook/preview.tsx +64 -0
- package/src/theme/index.ts +4 -0
- package/src/theme/types.d.ts +1 -0
- package/src/themes/cpc.ts +4 -0
- package/src/themes/firebird.ts +4 -0
- package/src/tokens/brands/cpc/themes/dark.json +82 -0
- package/src/tokens/brands/cpc/themes/light.json +1 -0
- package/src/tokens/brands/default/brand.json +20 -0
- package/src/tokens/brands/firebird/themes/dark.json +28 -0
- package/src/tokens/brands/firebird/themes/light.json +20 -0
- package/src/tokens/button.ts +24 -0
- package/src/tokens/components/accordion.json +52 -0
- package/src/tokens/components/avatar.json +40 -0
- package/src/tokens/components/badge.json +41 -0
- package/src/tokens/components/breadcrumb.json +13 -0
- package/src/tokens/components/button.json +67 -0
- package/src/tokens/components/card.json +39 -0
- package/src/tokens/components/checkbox.json +64 -0
- package/src/tokens/components/closebutton.json +55 -0
- package/src/tokens/components/datatable.json +40 -0
- package/src/tokens/components/datepicker.json +99 -0
- package/src/tokens/components/drawer.json +23 -0
- package/src/tokens/components/field.json +29 -0
- package/src/tokens/components/fileupload.json +57 -0
- package/src/tokens/components/filterpopover.json +29 -0
- package/src/tokens/components/input.json +31 -0
- package/src/tokens/components/ipwhitelistfield.json +16 -0
- package/src/tokens/components/loader.json +15 -0
- package/src/tokens/components/menu.json +21 -0
- package/src/tokens/components/modal.json +25 -0
- package/src/tokens/components/pagination.json +45 -0
- package/src/tokens/components/phonefield.json +41 -0
- package/src/tokens/components/progress.json +12 -0
- package/src/tokens/components/radio.json +87 -0
- package/src/tokens/components/reportcard.json +17 -0
- package/src/tokens/components/selector.json +87 -0
- package/src/tokens/components/selectscroll.json +65 -0
- package/src/tokens/components/skeleton.json +6 -0
- package/src/tokens/components/stepper.json +38 -0
- package/src/tokens/components/subtabs.json +45 -0
- package/src/tokens/components/switch.json +55 -0
- package/src/tokens/components/tabs.json +38 -0
- package/src/tokens/components/textfield.json +59 -0
- package/src/tokens/components/togglebutton.json +48 -0
- package/src/tokens/components/uploaddropzone.json +61 -0
- package/src/tokens/drawer.ts +37 -0
- package/src/tokens/foundation/breakpoints.json +10 -0
- package/src/tokens/foundation/colors.json +122 -0
- package/src/tokens/foundation/opacity.json +16 -0
- package/src/tokens/foundation/radius.json +14 -0
- package/src/tokens/foundation/size.json +38 -0
- package/src/tokens/foundation/spacing.json +19 -0
- package/src/tokens/foundation/typography.json +43 -0
- package/src/tokens/index.ts +3665 -0
- package/src/tokens/modal.ts +42 -0
- package/src/tokens/semantic/color.json +148 -0
- package/src/tokens/semantic/field.json +28 -0
- package/src/tokens/semantic/opacity.json +5 -0
- package/src/tokens/semantic/size.json +722 -0
- package/src/tokens/semantic/spacing.json +27 -0
- package/src/tokens/semantic/typography.json +87 -0
- package/src/utils/scrollLock.ts +44 -0
- package/README.md +0 -3
|
@@ -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
|
+
};
|