@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,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
+ }