@altruist03/ui 0.1.0 → 0.2.1
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 +15237 -650
- package/dist/index.js.map +1 -1
- package/dist/noData-XPRHKMFR.webp +0 -0
- package/package.json +21 -7
- 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/dist/index.css +0 -514
- package/dist/index.css.map +0 -1
- package/dist/index.d.ts +0 -289
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
import React, { memo, useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
|
2
|
+
import { useTokens } from '../../ThemeProvider/ThemeContext';
|
|
3
|
+
import { getDrawerTokensFromResolved } from '../../tokens/drawer';
|
|
4
|
+
import type { DrawerProps, DrawerAnchor } from './Drawer.types';
|
|
5
|
+
import { lockScroll, unlockScroll } from '../../utils/scrollLock';
|
|
6
|
+
|
|
7
|
+
const ANCHOR_HORIZONTAL: DrawerAnchor[] = ['left', 'right'];
|
|
8
|
+
const ANCHOR_VERTICAL: DrawerAnchor[] = ['top', 'bottom'];
|
|
9
|
+
const MOBILE_MAX_WIDTH = 767;
|
|
10
|
+
|
|
11
|
+
function useIsMobileScreen() {
|
|
12
|
+
const [isMobile, setIsMobile] = useState(
|
|
13
|
+
() => typeof window !== 'undefined' && window.innerWidth <= MOBILE_MAX_WIDTH
|
|
14
|
+
);
|
|
15
|
+
|
|
16
|
+
useEffect(() => {
|
|
17
|
+
const mediaQuery = window.matchMedia(`(max-width: ${MOBILE_MAX_WIDTH}px)`);
|
|
18
|
+
const update = () => setIsMobile(mediaQuery.matches);
|
|
19
|
+
update();
|
|
20
|
+
mediaQuery.addEventListener('change', update);
|
|
21
|
+
return () => mediaQuery.removeEventListener('change', update);
|
|
22
|
+
}, []);
|
|
23
|
+
|
|
24
|
+
return isMobile;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const BOX_SHADOW = '0 10px 15px -3px rgba(0, 0, 0, 0.1), 0 4px 6px -2px rgba(0, 0, 0, 0.05)';
|
|
28
|
+
|
|
29
|
+
function getTranslateValue(anchor: DrawerAnchor, isAnimating: boolean): string {
|
|
30
|
+
if (!isAnimating) return '0';
|
|
31
|
+
if (anchor === 'right' || anchor === 'bottom') return '100%';
|
|
32
|
+
return '-100%';
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function getResolvedWidth(
|
|
36
|
+
width: string | number | undefined,
|
|
37
|
+
sizeToken: { width: string } | undefined,
|
|
38
|
+
fallback: string
|
|
39
|
+
): string {
|
|
40
|
+
if (width === undefined) return sizeToken?.width ?? fallback;
|
|
41
|
+
return typeof width === 'number' ? `${width}px` : String(width);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const DrawerComponent: React.FC<DrawerProps> = ({
|
|
45
|
+
open,
|
|
46
|
+
onClose,
|
|
47
|
+
children,
|
|
48
|
+
anchor = 'right',
|
|
49
|
+
size = 'lg',
|
|
50
|
+
width,
|
|
51
|
+
className = '',
|
|
52
|
+
disableBackdropClose = false,
|
|
53
|
+
transitionDuration,
|
|
54
|
+
}) => {
|
|
55
|
+
const resolvedTokens = useTokens();
|
|
56
|
+
const tokens = useMemo(() => getDrawerTokensFromResolved(resolvedTokens), [resolvedTokens]);
|
|
57
|
+
const isMobileScreen = useIsMobileScreen();
|
|
58
|
+
|
|
59
|
+
const [renderState, setRenderState] = useState<'unmounted' | 'entering' | 'entered' | 'exiting'>(
|
|
60
|
+
open ? 'entered' : 'unmounted'
|
|
61
|
+
);
|
|
62
|
+
const needsUnmountRef = useRef(false);
|
|
63
|
+
|
|
64
|
+
const durationMs = transitionDuration ?? 300;
|
|
65
|
+
|
|
66
|
+
useEffect(() => {
|
|
67
|
+
if (open) {
|
|
68
|
+
lockScroll();
|
|
69
|
+
setRenderState('entering');
|
|
70
|
+
const t = setTimeout(() => setRenderState('entered'), 10);
|
|
71
|
+
return () => {
|
|
72
|
+
clearTimeout(t);
|
|
73
|
+
unlockScroll();
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
setRenderState((prev) => {
|
|
77
|
+
if (prev === 'unmounted') return prev;
|
|
78
|
+
needsUnmountRef.current = true;
|
|
79
|
+
return 'exiting';
|
|
80
|
+
});
|
|
81
|
+
if (!needsUnmountRef.current) return;
|
|
82
|
+
needsUnmountRef.current = false;
|
|
83
|
+
const t = setTimeout(() => setRenderState('unmounted'), durationMs);
|
|
84
|
+
return () => clearTimeout(t);
|
|
85
|
+
}, [open, durationMs]);
|
|
86
|
+
|
|
87
|
+
useEffect(() => {
|
|
88
|
+
const handleEscape = (e: KeyboardEvent) => {
|
|
89
|
+
if (e.key === 'Escape' && open) onClose();
|
|
90
|
+
};
|
|
91
|
+
window.addEventListener('keydown', handleEscape);
|
|
92
|
+
return () => window.removeEventListener('keydown', handleEscape);
|
|
93
|
+
}, [open, onClose]);
|
|
94
|
+
|
|
95
|
+
const handleStopPropagation = useCallback((e: React.MouseEvent) => {
|
|
96
|
+
e.stopPropagation();
|
|
97
|
+
}, []);
|
|
98
|
+
|
|
99
|
+
const isHorizontal = ANCHOR_HORIZONTAL.includes(anchor);
|
|
100
|
+
const isAnimating = renderState === 'entering' || renderState === 'exiting';
|
|
101
|
+
const translateValue = getTranslateValue(anchor, isAnimating);
|
|
102
|
+
const resolvedWidth = getResolvedWidth(width, tokens.size[size], tokens.size.lg.width);
|
|
103
|
+
const cornerRadius = isMobileScreen ? 0 : tokens.radius;
|
|
104
|
+
|
|
105
|
+
const panelStyle = useMemo<React.CSSProperties>(
|
|
106
|
+
() => ({
|
|
107
|
+
position: 'fixed',
|
|
108
|
+
top:
|
|
109
|
+
ANCHOR_VERTICAL.includes(anchor) ?
|
|
110
|
+
anchor === 'bottom' ?
|
|
111
|
+
'auto'
|
|
112
|
+
: 0
|
|
113
|
+
: 0,
|
|
114
|
+
left:
|
|
115
|
+
isHorizontal ?
|
|
116
|
+
anchor === 'right' ?
|
|
117
|
+
'auto'
|
|
118
|
+
: 0
|
|
119
|
+
: 0,
|
|
120
|
+
right:
|
|
121
|
+
isHorizontal ?
|
|
122
|
+
anchor === 'right' ?
|
|
123
|
+
0
|
|
124
|
+
: 'auto'
|
|
125
|
+
: 0,
|
|
126
|
+
bottom:
|
|
127
|
+
ANCHOR_VERTICAL.includes(anchor) ?
|
|
128
|
+
anchor === 'bottom' ?
|
|
129
|
+
0
|
|
130
|
+
: 'auto'
|
|
131
|
+
: 0,
|
|
132
|
+
maxWidth: isHorizontal ? resolvedWidth : '100%',
|
|
133
|
+
width: '100%',
|
|
134
|
+
height: isHorizontal ? '100%' : resolvedWidth,
|
|
135
|
+
backgroundColor: tokens.background,
|
|
136
|
+
boxShadow: BOX_SHADOW,
|
|
137
|
+
overflowY: 'auto',
|
|
138
|
+
overflowX: 'hidden',
|
|
139
|
+
zIndex: 1030,
|
|
140
|
+
transform: isHorizontal ? `translateX(${translateValue})` : `translateY(${translateValue})`,
|
|
141
|
+
transition: `transform ${durationMs}ms ease-in-out`,
|
|
142
|
+
borderTopLeftRadius: anchor === 'right' || anchor === 'bottom' ? cornerRadius : 0,
|
|
143
|
+
borderBottomLeftRadius: anchor === 'right' ? cornerRadius : 0,
|
|
144
|
+
borderTopRightRadius: anchor === 'left' || anchor === 'bottom' ? cornerRadius : 0,
|
|
145
|
+
borderBottomRightRadius: anchor === 'left' ? cornerRadius : 0,
|
|
146
|
+
}),
|
|
147
|
+
[
|
|
148
|
+
anchor,
|
|
149
|
+
isHorizontal,
|
|
150
|
+
resolvedWidth,
|
|
151
|
+
tokens.background,
|
|
152
|
+
cornerRadius,
|
|
153
|
+
translateValue,
|
|
154
|
+
durationMs,
|
|
155
|
+
]
|
|
156
|
+
);
|
|
157
|
+
|
|
158
|
+
const backdropStyle = useMemo<React.CSSProperties>(
|
|
159
|
+
() => ({
|
|
160
|
+
position: 'fixed',
|
|
161
|
+
top: 0,
|
|
162
|
+
left: 0,
|
|
163
|
+
width: '100vw',
|
|
164
|
+
height: '100vh',
|
|
165
|
+
backgroundColor: tokens.backdrop.background,
|
|
166
|
+
zIndex: 1020,
|
|
167
|
+
opacity: Number(isAnimating ? tokens.backdrop.opacityHidden : tokens.backdrop.opacityVisible),
|
|
168
|
+
transition: 'opacity 0.3s ease-in-out',
|
|
169
|
+
}),
|
|
170
|
+
[isAnimating, tokens.backdrop]
|
|
171
|
+
);
|
|
172
|
+
|
|
173
|
+
if (renderState === 'unmounted') return null;
|
|
174
|
+
|
|
175
|
+
return (
|
|
176
|
+
<>
|
|
177
|
+
<div
|
|
178
|
+
role="presentation"
|
|
179
|
+
aria-hidden="true"
|
|
180
|
+
style={backdropStyle}
|
|
181
|
+
onClick={disableBackdropClose ? undefined : onClose}
|
|
182
|
+
/>
|
|
183
|
+
<div
|
|
184
|
+
className={className}
|
|
185
|
+
role="dialog"
|
|
186
|
+
aria-modal="true"
|
|
187
|
+
style={panelStyle}
|
|
188
|
+
onClick={handleStopPropagation}
|
|
189
|
+
>
|
|
190
|
+
{children}
|
|
191
|
+
</div>
|
|
192
|
+
</>
|
|
193
|
+
);
|
|
194
|
+
};
|
|
195
|
+
|
|
196
|
+
export const Drawer = memo(DrawerComponent);
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import type { ReactNode } from "react";
|
|
2
|
+
|
|
3
|
+
/** Anchor position for the drawer panel */
|
|
4
|
+
export type DrawerAnchor = "left" | "right" | "top" | "bottom";
|
|
5
|
+
|
|
6
|
+
/** Size preset for drawer width/height */
|
|
7
|
+
export type DrawerSize = "sm" | "md" | "lg";
|
|
8
|
+
|
|
9
|
+
export interface DrawerProps {
|
|
10
|
+
/**
|
|
11
|
+
* Whether the drawer is open.
|
|
12
|
+
*/
|
|
13
|
+
open: boolean;
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Callback fired when the drawer requests to be closed
|
|
17
|
+
* (e.g. backdrop click, escape key).
|
|
18
|
+
*/
|
|
19
|
+
onClose: () => void;
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Content to render inside the drawer panel.
|
|
23
|
+
*/
|
|
24
|
+
children: ReactNode;
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Side from which the drawer slides in.
|
|
28
|
+
* @default "right"
|
|
29
|
+
*/
|
|
30
|
+
anchor?: DrawerAnchor;
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Size preset for drawer width (when anchor is left/right) or height (when anchor is top/bottom).
|
|
34
|
+
* @default "lg"
|
|
35
|
+
*/
|
|
36
|
+
size?: DrawerSize;
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Custom width or height (overrides size when anchor is left/right or top/bottom).
|
|
40
|
+
* Use for pixel values like "1200px" or "80%".
|
|
41
|
+
*/
|
|
42
|
+
width?: string | number;
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Optional CSS class name for the drawer panel (Paper).
|
|
46
|
+
*/
|
|
47
|
+
className?: string;
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Disable closing the drawer when clicking the backdrop.
|
|
51
|
+
* @default false
|
|
52
|
+
*/
|
|
53
|
+
disableBackdropClose?: boolean;
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Transition duration in ms.
|
|
57
|
+
* @default 300
|
|
58
|
+
*/
|
|
59
|
+
transitionDuration?: number;
|
|
60
|
+
}
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
import type { Meta, StoryObj } from "@storybook/react";
|
|
2
|
+
import { FileUpload } from "./FileUpload";
|
|
3
|
+
import type { FileUploadProps } from "./FileUpload.types";
|
|
4
|
+
|
|
5
|
+
const meta: Meta<typeof FileUpload> = {
|
|
6
|
+
title: "Design System/FileUpload",
|
|
7
|
+
component: FileUpload,
|
|
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
|
+
label: "Upload File",
|
|
25
|
+
required: true,
|
|
26
|
+
placeholder: "No file chosen",
|
|
27
|
+
buttonText: "Choose Files",
|
|
28
|
+
fullWidth: true,
|
|
29
|
+
size: "md",
|
|
30
|
+
color: "grey",
|
|
31
|
+
} as FileUploadProps,
|
|
32
|
+
argTypes: {
|
|
33
|
+
color: {
|
|
34
|
+
control: { type: "radio" },
|
|
35
|
+
options: ["grey", "white"],
|
|
36
|
+
},
|
|
37
|
+
size: {
|
|
38
|
+
control: { type: "radio" },
|
|
39
|
+
options: ["sm", "md", "lg"],
|
|
40
|
+
},
|
|
41
|
+
label: {
|
|
42
|
+
control: { type: "text" },
|
|
43
|
+
},
|
|
44
|
+
required: {
|
|
45
|
+
control: { type: "boolean" },
|
|
46
|
+
},
|
|
47
|
+
placeholder: {
|
|
48
|
+
control: { type: "text" },
|
|
49
|
+
},
|
|
50
|
+
buttonText: {
|
|
51
|
+
control: { type: "text" },
|
|
52
|
+
},
|
|
53
|
+
accept: {
|
|
54
|
+
control: { type: "text" },
|
|
55
|
+
},
|
|
56
|
+
multiple: {
|
|
57
|
+
control: { type: "boolean" },
|
|
58
|
+
},
|
|
59
|
+
disabled: {
|
|
60
|
+
control: { type: "boolean" },
|
|
61
|
+
},
|
|
62
|
+
error: {
|
|
63
|
+
control: { type: "boolean" },
|
|
64
|
+
},
|
|
65
|
+
},
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
export default meta;
|
|
69
|
+
|
|
70
|
+
type Story = StoryObj<typeof FileUpload>;
|
|
71
|
+
|
|
72
|
+
export const Playground: Story = {
|
|
73
|
+
args: {
|
|
74
|
+
label: "Upload File",
|
|
75
|
+
required: true,
|
|
76
|
+
placeholder: "No file chosen",
|
|
77
|
+
buttonText: "Choose Files",
|
|
78
|
+
},
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
export const Showcase: Story = {
|
|
82
|
+
render: () => (
|
|
83
|
+
<div
|
|
84
|
+
style={{
|
|
85
|
+
display: "flex",
|
|
86
|
+
flexDirection: "column",
|
|
87
|
+
gap: "24px",
|
|
88
|
+
padding: "24px",
|
|
89
|
+
backgroundColor: "#E5E9EB",
|
|
90
|
+
borderRadius: "8px",
|
|
91
|
+
maxWidth: "420px",
|
|
92
|
+
}}
|
|
93
|
+
>
|
|
94
|
+
{/* Default / Required */}
|
|
95
|
+
<FileUpload
|
|
96
|
+
label="Upload File"
|
|
97
|
+
required
|
|
98
|
+
placeholder="No file chosen"
|
|
99
|
+
buttonText="Choose Files"
|
|
100
|
+
/>
|
|
101
|
+
|
|
102
|
+
{/* Optional label */}
|
|
103
|
+
<FileUpload
|
|
104
|
+
label="Upload File"
|
|
105
|
+
required={false}
|
|
106
|
+
placeholder="No file chosen"
|
|
107
|
+
buttonText="Choose Files"
|
|
108
|
+
/>
|
|
109
|
+
|
|
110
|
+
{/* Multiple files */}
|
|
111
|
+
<FileUpload
|
|
112
|
+
label="Upload Multiple Files"
|
|
113
|
+
required
|
|
114
|
+
placeholder="No files chosen"
|
|
115
|
+
buttonText="Choose Files"
|
|
116
|
+
multiple
|
|
117
|
+
/>
|
|
118
|
+
|
|
119
|
+
{/* Size variants */}
|
|
120
|
+
<FileUpload label="Small" size="sm" placeholder="No file chosen" buttonText="Choose Files" />
|
|
121
|
+
<FileUpload label="Medium" size="md" placeholder="No file chosen" buttonText="Choose Files" />
|
|
122
|
+
<FileUpload label="Large" size="lg" placeholder="No file chosen" buttonText="Choose Files" />
|
|
123
|
+
|
|
124
|
+
{/* With accept restriction */}
|
|
125
|
+
<FileUpload
|
|
126
|
+
label="Upload Document"
|
|
127
|
+
required
|
|
128
|
+
placeholder="No file chosen"
|
|
129
|
+
buttonText="Choose Files"
|
|
130
|
+
accept=".xlsx,.csv"
|
|
131
|
+
/>
|
|
132
|
+
|
|
133
|
+
{/* Error state */}
|
|
134
|
+
<FileUpload
|
|
135
|
+
label="Upload File"
|
|
136
|
+
required
|
|
137
|
+
placeholder="No file chosen"
|
|
138
|
+
buttonText="Choose Files"
|
|
139
|
+
error
|
|
140
|
+
helperText="Please select a file to continue"
|
|
141
|
+
/>
|
|
142
|
+
|
|
143
|
+
{/* Disabled state */}
|
|
144
|
+
<FileUpload
|
|
145
|
+
label="Upload File"
|
|
146
|
+
required
|
|
147
|
+
placeholder="No file chosen"
|
|
148
|
+
buttonText="Choose Files"
|
|
149
|
+
disabled
|
|
150
|
+
/>
|
|
151
|
+
|
|
152
|
+
{/* Color: white */}
|
|
153
|
+
<FileUpload
|
|
154
|
+
label="Upload File (white)"
|
|
155
|
+
required
|
|
156
|
+
placeholder="No file chosen"
|
|
157
|
+
buttonText="Choose Files"
|
|
158
|
+
color="white"
|
|
159
|
+
/>
|
|
160
|
+
</div>
|
|
161
|
+
),
|
|
162
|
+
};
|
|
@@ -0,0 +1,216 @@
|
|
|
1
|
+
import React, { useRef, useCallback, useEffect, useContext } from "react";
|
|
2
|
+
import type { FileUploadProps } from "./FileUpload.types";
|
|
3
|
+
import { useTokens, ThemeContext } from "../../ThemeProvider/ThemeContext";
|
|
4
|
+
import { getFileUploadTokensFromResolved } from "../../tokens";
|
|
5
|
+
import { Button } from "../Button/Button";
|
|
6
|
+
// import { Icon as FirebirdIcon } from "firebird-icon-lib";
|
|
7
|
+
import Skeleton from "react-loading-skeleton";
|
|
8
|
+
import "react-loading-skeleton/dist/skeleton.css";
|
|
9
|
+
|
|
10
|
+
export const FileUpload = ({
|
|
11
|
+
size = "md",
|
|
12
|
+
color = "grey",
|
|
13
|
+
label = "Upload File",
|
|
14
|
+
required = false,
|
|
15
|
+
placeholder = "No file chosen",
|
|
16
|
+
buttonText = "Choose Files",
|
|
17
|
+
accept,
|
|
18
|
+
multiple = false,
|
|
19
|
+
disabled = false,
|
|
20
|
+
fullWidth = true,
|
|
21
|
+
value,
|
|
22
|
+
defaultValue,
|
|
23
|
+
onChange,
|
|
24
|
+
id,
|
|
25
|
+
name,
|
|
26
|
+
error = false,
|
|
27
|
+
helperText,
|
|
28
|
+
inputRef: inputRefProp,
|
|
29
|
+
skeletonLoading = false,
|
|
30
|
+
}: FileUploadProps) => {
|
|
31
|
+
const internalInputRef = useRef<HTMLInputElement>(null);
|
|
32
|
+
const inputRef = inputRefProp ?? internalInputRef;
|
|
33
|
+
|
|
34
|
+
const theme = useContext(ThemeContext);
|
|
35
|
+
const resolvedTokens = useTokens();
|
|
36
|
+
const tokens = getFileUploadTokensFromResolved(resolvedTokens, size);
|
|
37
|
+
const buttonBackground =
|
|
38
|
+
theme.brand === "cpc" && theme.mode === "dark"
|
|
39
|
+
? tokens.buttonBackground
|
|
40
|
+
: "#ffffff";
|
|
41
|
+
|
|
42
|
+
const [internalFiles, setInternalFiles] = React.useState<
|
|
43
|
+
File | File[] | null
|
|
44
|
+
>(defaultValue ?? null);
|
|
45
|
+
|
|
46
|
+
const currentFiles = value !== undefined ? value : internalFiles;
|
|
47
|
+
const displayText = React.useMemo(() => {
|
|
48
|
+
if (!currentFiles) return placeholder;
|
|
49
|
+
if (Array.isArray(currentFiles)) {
|
|
50
|
+
if (currentFiles.length === 0) return placeholder;
|
|
51
|
+
const count = currentFiles.length;
|
|
52
|
+
return count === 1 ? currentFiles[0].name : `${count} files chosen`;
|
|
53
|
+
}
|
|
54
|
+
return currentFiles.name;
|
|
55
|
+
}, [currentFiles, placeholder]);
|
|
56
|
+
|
|
57
|
+
useEffect(() => {
|
|
58
|
+
if (value !== undefined) {
|
|
59
|
+
setInternalFiles(value);
|
|
60
|
+
}
|
|
61
|
+
}, [value]);
|
|
62
|
+
|
|
63
|
+
const handleChange = useCallback(
|
|
64
|
+
(e: React.ChangeEvent<HTMLInputElement>) => {
|
|
65
|
+
const files = e.target.files;
|
|
66
|
+
if (!files || files.length === 0) {
|
|
67
|
+
const result = null;
|
|
68
|
+
if (value === undefined) setInternalFiles(null);
|
|
69
|
+
onChange?.(result);
|
|
70
|
+
return;
|
|
71
|
+
}
|
|
72
|
+
const result = multiple ? Array.from(files) : files[0];
|
|
73
|
+
if (value === undefined) setInternalFiles(result);
|
|
74
|
+
onChange?.(result);
|
|
75
|
+
e.target.value = "";
|
|
76
|
+
},
|
|
77
|
+
[multiple, onChange, value],
|
|
78
|
+
);
|
|
79
|
+
|
|
80
|
+
const handleButtonClick = useCallback(() => {
|
|
81
|
+
if (disabled) return;
|
|
82
|
+
inputRef.current?.click();
|
|
83
|
+
}, [disabled, inputRef]);
|
|
84
|
+
|
|
85
|
+
const inputId =
|
|
86
|
+
id ??
|
|
87
|
+
name ??
|
|
88
|
+
(label ? String(label).replace(/\s+/g, "-").toLowerCase() : undefined);
|
|
89
|
+
|
|
90
|
+
const containerBackground =
|
|
91
|
+
color === "white"
|
|
92
|
+
? tokens.containerBackgroundWhite
|
|
93
|
+
: tokens.containerBackground;
|
|
94
|
+
const containerStyle: React.CSSProperties = {
|
|
95
|
+
display: "flex",
|
|
96
|
+
alignItems: "center",
|
|
97
|
+
width: fullWidth ? "100%" : undefined,
|
|
98
|
+
minHeight: tokens.containerHeight,
|
|
99
|
+
padding: `${tokens.containerPaddingY}`,
|
|
100
|
+
gap: tokens.containerGap,
|
|
101
|
+
background: containerBackground || "#FFFFFF",
|
|
102
|
+
border: error ? tokens.containerBorderError : tokens.containerBorder,
|
|
103
|
+
borderRadius: tokens.containerBorderRadius,
|
|
104
|
+
boxSizing: "border-box",
|
|
105
|
+
opacity: disabled ? tokens.disabledOpacity : 1,
|
|
106
|
+
cursor: disabled ? "not-allowed" : "pointer",
|
|
107
|
+
};
|
|
108
|
+
|
|
109
|
+
const labelStyle: React.CSSProperties = {
|
|
110
|
+
display: "block",
|
|
111
|
+
marginBottom: tokens.labelMarginBottom,
|
|
112
|
+
fontSize: tokens.labelFontSize,
|
|
113
|
+
fontWeight: tokens.labelFontWeight,
|
|
114
|
+
fontFamily: tokens.labelFontFamily,
|
|
115
|
+
color: tokens.labelColor,
|
|
116
|
+
};
|
|
117
|
+
|
|
118
|
+
const placeholderStyle: React.CSSProperties = {
|
|
119
|
+
flex: 1,
|
|
120
|
+
minWidth: 0,
|
|
121
|
+
fontSize: tokens.placeholderFontSize,
|
|
122
|
+
fontFamily: tokens.placeholderFontFamily,
|
|
123
|
+
color: tokens.placeholderColor,
|
|
124
|
+
overflow: "hidden",
|
|
125
|
+
textOverflow: "ellipsis",
|
|
126
|
+
whiteSpace: "nowrap",
|
|
127
|
+
};
|
|
128
|
+
|
|
129
|
+
const helperStyle: React.CSSProperties = {
|
|
130
|
+
marginTop: 8,
|
|
131
|
+
fontSize: tokens.placeholderFontSize,
|
|
132
|
+
fontFamily: tokens.placeholderFontFamily,
|
|
133
|
+
color: error ? tokens.labelRequiredColor : tokens.placeholderColor,
|
|
134
|
+
display: "flex",
|
|
135
|
+
// alignItems: "center",
|
|
136
|
+
gap: 6,
|
|
137
|
+
};
|
|
138
|
+
|
|
139
|
+
const containerHeight = parseInt(tokens.containerHeight, 10) || 48;
|
|
140
|
+
|
|
141
|
+
if (skeletonLoading) {
|
|
142
|
+
return (
|
|
143
|
+
<div style={{ width: fullWidth ? "100%" : undefined }}>
|
|
144
|
+
{label != null && label !== "" && (
|
|
145
|
+
<label style={labelStyle}>
|
|
146
|
+
{required && (
|
|
147
|
+
<span
|
|
148
|
+
style={{ color: tokens.labelRequiredColor, marginRight: 4 }}
|
|
149
|
+
>
|
|
150
|
+
*
|
|
151
|
+
</span>
|
|
152
|
+
)}
|
|
153
|
+
{label}
|
|
154
|
+
</label>
|
|
155
|
+
)}
|
|
156
|
+
<Skeleton height={containerHeight} borderRadius={8} />
|
|
157
|
+
</div>
|
|
158
|
+
);
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
return (
|
|
162
|
+
<div style={{ width: fullWidth ? "100%" : undefined }}>
|
|
163
|
+
{label != null && label !== "" && (
|
|
164
|
+
<label htmlFor={inputId} style={labelStyle}>
|
|
165
|
+
{required && (
|
|
166
|
+
<span style={{ color: tokens.labelRequiredColor, marginRight: 4 }}>
|
|
167
|
+
*
|
|
168
|
+
</span>
|
|
169
|
+
)}
|
|
170
|
+
{label}
|
|
171
|
+
</label>
|
|
172
|
+
)}
|
|
173
|
+
<div style={containerStyle} onClick={handleButtonClick}>
|
|
174
|
+
<Button
|
|
175
|
+
variant="grey"
|
|
176
|
+
size="sm"
|
|
177
|
+
disabled={disabled}
|
|
178
|
+
style={{
|
|
179
|
+
flexShrink: 0,
|
|
180
|
+
backgroundColor: buttonBackground,
|
|
181
|
+
border: tokens.buttonBorder?.startsWith("1px")
|
|
182
|
+
? tokens.buttonBorder
|
|
183
|
+
: `1px solid ${tokens.buttonBorder}`,
|
|
184
|
+
color: tokens.buttonColor,
|
|
185
|
+
}}
|
|
186
|
+
onClick={(e) => {
|
|
187
|
+
e.stopPropagation();
|
|
188
|
+
handleButtonClick();
|
|
189
|
+
}}
|
|
190
|
+
type="button"
|
|
191
|
+
>
|
|
192
|
+
{buttonText}
|
|
193
|
+
</Button>
|
|
194
|
+
<span style={placeholderStyle}>{displayText}</span>
|
|
195
|
+
</div>
|
|
196
|
+
<input
|
|
197
|
+
ref={inputRef as React.RefObject<HTMLInputElement>}
|
|
198
|
+
id={inputId}
|
|
199
|
+
name={name}
|
|
200
|
+
type="file"
|
|
201
|
+
accept={accept}
|
|
202
|
+
multiple={multiple}
|
|
203
|
+
onChange={handleChange}
|
|
204
|
+
disabled={disabled}
|
|
205
|
+
style={{ display: "none" }}
|
|
206
|
+
aria-hidden
|
|
207
|
+
/>
|
|
208
|
+
{helperText != null && helperText !== "" && (
|
|
209
|
+
<div style={helperStyle} role={error ? "alert" : undefined}>
|
|
210
|
+
{/* <FirebirdIcon name="info_circle" size={16} /> */}
|
|
211
|
+
<span>{helperText}</span>
|
|
212
|
+
</div>
|
|
213
|
+
)}
|
|
214
|
+
</div>
|
|
215
|
+
);
|
|
216
|
+
};
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
export type FileUploadSize = "sm" | "md" | "lg";
|
|
2
|
+
|
|
3
|
+
export interface FileUploadProps {
|
|
4
|
+
/**
|
|
5
|
+
* Size of the file upload component
|
|
6
|
+
*/
|
|
7
|
+
size?: FileUploadSize;
|
|
8
|
+
/**
|
|
9
|
+
* Background color variant
|
|
10
|
+
*/
|
|
11
|
+
color?: "grey" | "white";
|
|
12
|
+
/**
|
|
13
|
+
* Field label shown above the input
|
|
14
|
+
*/
|
|
15
|
+
label?: string;
|
|
16
|
+
/**
|
|
17
|
+
* Marks the field as required and appends "*" to the label
|
|
18
|
+
*/
|
|
19
|
+
required?: boolean;
|
|
20
|
+
/**
|
|
21
|
+
* Placeholder text when no file is selected
|
|
22
|
+
*/
|
|
23
|
+
placeholder?: string;
|
|
24
|
+
/**
|
|
25
|
+
* Button text for choosing files
|
|
26
|
+
*/
|
|
27
|
+
buttonText?: string;
|
|
28
|
+
/**
|
|
29
|
+
* Accepted file types (e.g. ".xlsx,.csv" or "image/*")
|
|
30
|
+
*/
|
|
31
|
+
accept?: string;
|
|
32
|
+
/**
|
|
33
|
+
* Whether multiple files can be selected
|
|
34
|
+
*/
|
|
35
|
+
multiple?: boolean;
|
|
36
|
+
/**
|
|
37
|
+
* Disables the field
|
|
38
|
+
*/
|
|
39
|
+
disabled?: boolean;
|
|
40
|
+
/**
|
|
41
|
+
* Makes the field stretch to full container width
|
|
42
|
+
*/
|
|
43
|
+
fullWidth?: boolean;
|
|
44
|
+
/**
|
|
45
|
+
* Selected file(s)
|
|
46
|
+
*/
|
|
47
|
+
value?: File | File[] | null;
|
|
48
|
+
/**
|
|
49
|
+
* Default file(s) for uncontrolled usage
|
|
50
|
+
*/
|
|
51
|
+
defaultValue?: File | File[] | null;
|
|
52
|
+
/**
|
|
53
|
+
* Callback when file(s) are selected
|
|
54
|
+
*/
|
|
55
|
+
onChange?: (files: File | File[] | null) => void;
|
|
56
|
+
/**
|
|
57
|
+
* HTML id attribute
|
|
58
|
+
*/
|
|
59
|
+
id?: string;
|
|
60
|
+
/**
|
|
61
|
+
* Input name attribute
|
|
62
|
+
*/
|
|
63
|
+
name?: string;
|
|
64
|
+
/**
|
|
65
|
+
* Shows error state
|
|
66
|
+
*/
|
|
67
|
+
error?: boolean;
|
|
68
|
+
/**
|
|
69
|
+
* Helper or error text displayed below the field
|
|
70
|
+
*/
|
|
71
|
+
helperText?: string;
|
|
72
|
+
/**
|
|
73
|
+
* Ref for the hidden file input element
|
|
74
|
+
*/
|
|
75
|
+
inputRef?: React.RefObject<HTMLInputElement | null>;
|
|
76
|
+
/**
|
|
77
|
+
* Loading state - shows skeleton instead of component
|
|
78
|
+
*/
|
|
79
|
+
skeletonLoading?: boolean;
|
|
80
|
+
}
|