@altruist03/ui 0.0.0-stage → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (205) hide show
  1. package/assets/images/noData.webp +0 -0
  2. package/dist/index.js +15318 -0
  3. package/dist/index.js.map +1 -0
  4. package/dist/noData-XPRHKMFR.webp +0 -0
  5. package/package.json +51 -4
  6. package/src/ThemeProvider/ThemeContext.tsx +75 -0
  7. package/src/ThemeProvider/index.ts +7 -0
  8. package/src/assets/images/noData.webp +0 -0
  9. package/src/assets.d.ts +10 -0
  10. package/src/components/Accordion/Accordion.stories.tsx +64 -0
  11. package/src/components/Accordion/Accordion.tsx +219 -0
  12. package/src/components/Accordion/Accordion.types.ts +16 -0
  13. package/src/components/Avatar/Avatar.stories.tsx +304 -0
  14. package/src/components/Avatar/Avatar.tsx +211 -0
  15. package/src/components/Avatar/Avatar.types.ts +33 -0
  16. package/src/components/Badge/Badge.stories.tsx +143 -0
  17. package/src/components/Badge/Badge.tsx +138 -0
  18. package/src/components/Badge/Badge.types.ts +24 -0
  19. package/src/components/Breadcrumb/Breadcrumb.stories.tsx +114 -0
  20. package/src/components/Breadcrumb/Breadcrumb.tsx +120 -0
  21. package/src/components/Breadcrumb/Breadcrumb.types.ts +22 -0
  22. package/src/components/Button/Button.stories.tsx +457 -0
  23. package/src/components/Button/Button.tsx +202 -0
  24. package/src/components/Button/Button.types.ts +18 -0
  25. package/src/components/Checkbox/Checkbox.stories.tsx +254 -0
  26. package/src/components/Checkbox/Checkbox.tsx +352 -0
  27. package/src/components/Checkbox/Checkbox.types.ts +123 -0
  28. package/src/components/CloseButton/CloseButton.stories.tsx +100 -0
  29. package/src/components/CloseButton/CloseButton.tsx +117 -0
  30. package/src/components/CloseButton/CloseButton.types.ts +15 -0
  31. package/src/components/CollapsibleSearch/CollapsibleSearch.tsx +120 -0
  32. package/src/components/DataTable/DataTable.stories.tsx +233 -0
  33. package/src/components/DataTable/DataTable.tsx +968 -0
  34. package/src/components/DataTable/DataTable.types.ts +49 -0
  35. package/src/components/DatePicker/DatePicker.stories.tsx +341 -0
  36. package/src/components/DatePicker/DatePicker.tsx +669 -0
  37. package/src/components/DatePicker/DatePicker.types.ts +105 -0
  38. package/src/components/DatePicker/DateTimePicker.tsx +35 -0
  39. package/src/components/DateRangePicker/DateRangePicker.tsx +179 -0
  40. package/src/components/DateRangePicker/DateRangePicker.types.ts +34 -0
  41. package/src/components/Drawer/Drawer.stories.tsx +243 -0
  42. package/src/components/Drawer/Drawer.tsx +196 -0
  43. package/src/components/Drawer/Drawer.types.ts +60 -0
  44. package/src/components/FileUpload/FileUpload.stories.tsx +162 -0
  45. package/src/components/FileUpload/FileUpload.tsx +216 -0
  46. package/src/components/FileUpload/FileUpload.types.ts +80 -0
  47. package/src/components/FilterPopover/FilterPopover.stories.tsx +173 -0
  48. package/src/components/FilterPopover/FilterPopover.tsx +310 -0
  49. package/src/components/FilterPopover/FilterPopover.types.ts +26 -0
  50. package/src/components/ImageViewer/ImageViewerToolbar.jsx +81 -0
  51. package/src/components/ImageViewer/useImagePanRotate.js +102 -0
  52. package/src/components/IpWhitelistField/IpWhitelistField.stories.tsx +138 -0
  53. package/src/components/IpWhitelistField/IpWhitelistField.tsx +164 -0
  54. package/src/components/IpWhitelistField/IpWhitelistField.types.ts +42 -0
  55. package/src/components/Loader/Loader.stories.tsx +95 -0
  56. package/src/components/Loader/Loader.tsx +132 -0
  57. package/src/components/Loader/Loader.types.ts +6 -0
  58. package/src/components/Menu/Menu.tsx +143 -0
  59. package/src/components/Menu/Menu.types.ts +28 -0
  60. package/src/components/MenuButton/MenuButton.stories.tsx +140 -0
  61. package/src/components/MenuButton/MenuButton.tsx +45 -0
  62. package/src/components/MenuButton/MenuButton.types.ts +21 -0
  63. package/src/components/Modal/Modal.stories.tsx +266 -0
  64. package/src/components/Modal/Modal.tsx +399 -0
  65. package/src/components/Modal/index.ts +2 -0
  66. package/src/components/Modal/types.ts +126 -0
  67. package/src/components/Pagination/Pagination.stories.tsx +167 -0
  68. package/src/components/Pagination/Pagination.tsx +266 -0
  69. package/src/components/Pagination/Pagination.types.ts +20 -0
  70. package/src/components/PeriodSelector/PeriodSelector.stories.tsx +50 -0
  71. package/src/components/PeriodSelector/PeriodSelector.tsx +47 -0
  72. package/src/components/PeriodSelector/PeriodSelector.types.ts +10 -0
  73. package/src/components/PhoneField/PhoneField.stories.tsx +141 -0
  74. package/src/components/PhoneField/PhoneField.tsx +284 -0
  75. package/src/components/PhoneField/PhoneField.types.ts +89 -0
  76. package/src/components/PhoneField/countryCodes.ts +236 -0
  77. package/src/components/Progress/Progress.stories.tsx +83 -0
  78. package/src/components/Progress/Progress.tsx +91 -0
  79. package/src/components/Progress/Progress.types.ts +22 -0
  80. package/src/components/RadioButton/RadioButton.stories.tsx +332 -0
  81. package/src/components/RadioButton/RadioButton.tsx +427 -0
  82. package/src/components/RadioButton/RadioButton.types.ts +117 -0
  83. package/src/components/ReportCard/ReportCard.tsx +183 -0
  84. package/src/components/ReportCard/ReportCard.types.ts +17 -0
  85. package/src/components/SectionIcon/SectionIcon.tsx +87 -0
  86. package/src/components/SectionIcon/SectionIcon.types.ts +23 -0
  87. package/src/components/SegmentedInputField/SegmentedInputField.stories.tsx +165 -0
  88. package/src/components/SegmentedInputField/SegmentedInputField.tsx +174 -0
  89. package/src/components/SegmentedInputField/SegmentedInputField.types.ts +51 -0
  90. package/src/components/SelectScroll/SelectScroll.stories.tsx +336 -0
  91. package/src/components/SelectScroll/SelectScroll.tsx +1119 -0
  92. package/src/components/SelectScroll/SelectScroll.types.ts +60 -0
  93. package/src/components/Selector/Selector.stories.tsx +382 -0
  94. package/src/components/Selector/Selector.tsx +1310 -0
  95. package/src/components/Selector/Selector.types.ts +71 -0
  96. package/src/components/Skeleton/Skeleton.stories.tsx +133 -0
  97. package/src/components/Skeleton/Skeleton.tsx +20 -0
  98. package/src/components/Skeleton/Skeleton.types.ts +3 -0
  99. package/src/components/Spinner/Spinner.stories.tsx +84 -0
  100. package/src/components/Spinner/Spinner.tsx +79 -0
  101. package/src/components/Spinner/Spinner.types.ts +8 -0
  102. package/src/components/StatCard/StatCard.stories.tsx +125 -0
  103. package/src/components/StatCard/StatCard.tsx +167 -0
  104. package/src/components/StatCard/StatCard.types.ts +20 -0
  105. package/src/components/StatusBadge/StatusBadge.stories.tsx +179 -0
  106. package/src/components/StatusBadge/StatusBadge.tsx +74 -0
  107. package/src/components/StatusBadge/StatusBadge.types.ts +23 -0
  108. package/src/components/Stepper/Stepper.stories.tsx +85 -0
  109. package/src/components/Stepper/Stepper.tsx +138 -0
  110. package/src/components/Stepper/Stepper.types.ts +16 -0
  111. package/src/components/SubTabs/SubTabs.stories.tsx +128 -0
  112. package/src/components/SubTabs/SubTabs.tsx +279 -0
  113. package/src/components/SubTabs/SubTabs.types.ts +44 -0
  114. package/src/components/SummaryCard/SummaryCard.stories.tsx +100 -0
  115. package/src/components/SummaryCard/SummaryCard.tsx +141 -0
  116. package/src/components/SummaryCard/SummaryCard.types.ts +22 -0
  117. package/src/components/Switch/Switch.stories.tsx +59 -0
  118. package/src/components/Switch/Switch.tsx +217 -0
  119. package/src/components/Switch/Switch.types.ts +22 -0
  120. package/src/components/Tabs/Tabs.stories.tsx +178 -0
  121. package/src/components/Tabs/Tabs.tsx +197 -0
  122. package/src/components/Tabs/Tabs.types.ts +34 -0
  123. package/src/components/TextField/TextField.stories.tsx +249 -0
  124. package/src/components/TextField/TextField.tsx +462 -0
  125. package/src/components/TextField/TextField.types.ts +141 -0
  126. package/src/components/Toast/Toast.stories.tsx +237 -0
  127. package/src/components/Toast/Toast.tsx +246 -0
  128. package/src/components/Toast/Toast.types.ts +31 -0
  129. package/src/components/ToggleButton/ToggleButton.stories.tsx +105 -0
  130. package/src/components/ToggleButton/ToggleButton.tsx +267 -0
  131. package/src/components/ToggleButton/ToggleButton.types.ts +19 -0
  132. package/src/components/UploadDropzone/UploadDropzone.jsx +78 -0
  133. package/src/components/UploadDropzone2/UploadDropzone.stories.tsx +94 -0
  134. package/src/components/UploadDropzone2/UploadDropzone.tsx +222 -0
  135. package/src/components/UploadDropzone2/UploadDropzone.types.ts +54 -0
  136. package/src/components/ViewEditToggle/ViewEditToggle.stories.tsx +40 -0
  137. package/src/components/ViewEditToggle/ViewEditToggle.tsx +239 -0
  138. package/src/components/ViewEditToggle/ViewEditToggle.types.ts +27 -0
  139. package/src/components/index.ts +157 -0
  140. package/src/icons/ReportDollarIcon.tsx +71 -0
  141. package/src/icons/index.ts +27 -0
  142. package/src/index.ts +214 -0
  143. package/src/storybook/preview.tsx +64 -0
  144. package/src/theme/index.ts +4 -0
  145. package/src/theme/types.d.ts +1 -0
  146. package/src/themes/cpc.ts +4 -0
  147. package/src/themes/firebird.ts +4 -0
  148. package/src/tokens/brands/cpc/themes/dark.json +82 -0
  149. package/src/tokens/brands/cpc/themes/light.json +1 -0
  150. package/src/tokens/brands/default/brand.json +20 -0
  151. package/src/tokens/brands/firebird/themes/dark.json +28 -0
  152. package/src/tokens/brands/firebird/themes/light.json +20 -0
  153. package/src/tokens/button.ts +24 -0
  154. package/src/tokens/components/accordion.json +52 -0
  155. package/src/tokens/components/avatar.json +40 -0
  156. package/src/tokens/components/badge.json +41 -0
  157. package/src/tokens/components/breadcrumb.json +13 -0
  158. package/src/tokens/components/button.json +67 -0
  159. package/src/tokens/components/card.json +39 -0
  160. package/src/tokens/components/checkbox.json +64 -0
  161. package/src/tokens/components/closebutton.json +55 -0
  162. package/src/tokens/components/datatable.json +40 -0
  163. package/src/tokens/components/datepicker.json +99 -0
  164. package/src/tokens/components/drawer.json +23 -0
  165. package/src/tokens/components/field.json +29 -0
  166. package/src/tokens/components/fileupload.json +57 -0
  167. package/src/tokens/components/filterpopover.json +29 -0
  168. package/src/tokens/components/input.json +31 -0
  169. package/src/tokens/components/ipwhitelistfield.json +16 -0
  170. package/src/tokens/components/loader.json +15 -0
  171. package/src/tokens/components/menu.json +21 -0
  172. package/src/tokens/components/modal.json +25 -0
  173. package/src/tokens/components/pagination.json +45 -0
  174. package/src/tokens/components/phonefield.json +41 -0
  175. package/src/tokens/components/progress.json +12 -0
  176. package/src/tokens/components/radio.json +87 -0
  177. package/src/tokens/components/reportcard.json +17 -0
  178. package/src/tokens/components/selector.json +87 -0
  179. package/src/tokens/components/selectscroll.json +65 -0
  180. package/src/tokens/components/skeleton.json +6 -0
  181. package/src/tokens/components/stepper.json +38 -0
  182. package/src/tokens/components/subtabs.json +45 -0
  183. package/src/tokens/components/switch.json +55 -0
  184. package/src/tokens/components/tabs.json +38 -0
  185. package/src/tokens/components/textfield.json +59 -0
  186. package/src/tokens/components/togglebutton.json +48 -0
  187. package/src/tokens/components/uploaddropzone.json +61 -0
  188. package/src/tokens/drawer.ts +37 -0
  189. package/src/tokens/foundation/breakpoints.json +10 -0
  190. package/src/tokens/foundation/colors.json +122 -0
  191. package/src/tokens/foundation/opacity.json +16 -0
  192. package/src/tokens/foundation/radius.json +14 -0
  193. package/src/tokens/foundation/size.json +38 -0
  194. package/src/tokens/foundation/spacing.json +19 -0
  195. package/src/tokens/foundation/typography.json +43 -0
  196. package/src/tokens/index.ts +3665 -0
  197. package/src/tokens/modal.ts +42 -0
  198. package/src/tokens/semantic/color.json +148 -0
  199. package/src/tokens/semantic/field.json +28 -0
  200. package/src/tokens/semantic/opacity.json +5 -0
  201. package/src/tokens/semantic/size.json +722 -0
  202. package/src/tokens/semantic/spacing.json +27 -0
  203. package/src/tokens/semantic/typography.json +87 -0
  204. package/src/utils/scrollLock.ts +44 -0
  205. package/README.md +0 -3
@@ -0,0 +1,222 @@
1
+ import React, { useRef, useCallback, useState } from "react";
2
+ import type { UploadDropzoneProps } from "./UploadDropzone.types";
3
+ import { useTokens } from "../../ThemeProvider/ThemeContext";
4
+ import { getUploadDropzoneTokensFromResolved } from "../../tokens";
5
+ import CloudUploadIcon from '@mui/icons-material/CloudUpload';
6
+
7
+ export const UploadDropzone = ({
8
+ title = "Visa",
9
+ instructionText = "Drag & drop or click to upload",
10
+ hintText = "JPG, PNG or PDF",
11
+ buttonText = "Required",
12
+ accept,
13
+ multiple = false,
14
+ disabled = false,
15
+ fullWidth = true,
16
+ value,
17
+ onChange,
18
+ id,
19
+ name,
20
+ inputRef: inputRefProp,
21
+ }: UploadDropzoneProps) => {
22
+ const internalInputRef = useRef<HTMLInputElement>(null);
23
+ const inputRef = inputRefProp ?? internalInputRef;
24
+
25
+ const [isDragOver, setIsDragOver] = useState(false);
26
+
27
+ const resolvedTokens = useTokens();
28
+ const tokens = getUploadDropzoneTokensFromResolved(resolvedTokens);
29
+ const currentFiles = value ?? null;
30
+ const hasUploadedFile = Array.isArray(currentFiles)
31
+ ? currentFiles.length > 0
32
+ : Boolean(currentFiles);
33
+ const uploadedBorder = tokens.zoneBorderActive.includes("dashed")
34
+ ? tokens.zoneBorderActive.replace("dashed", "solid")
35
+ : tokens.zoneBorderActive;
36
+
37
+ const handleChange = useCallback(
38
+ (e: React.ChangeEvent<HTMLInputElement>) => {
39
+ const files = e.target.files;
40
+ if (!files || files.length === 0) {
41
+ onChange?.(null);
42
+ return;
43
+ }
44
+ const result = multiple ? Array.from(files) : files[0];
45
+ onChange?.(result);
46
+ e.target.value = "";
47
+ },
48
+ [multiple, onChange],
49
+ );
50
+
51
+ const handleClick = useCallback(() => {
52
+ if (disabled) return;
53
+ inputRef.current?.click();
54
+ }, [disabled, inputRef]);
55
+
56
+ const handleDrop = useCallback(
57
+ (e: React.DragEvent) => {
58
+ e.preventDefault();
59
+ e.stopPropagation();
60
+ setIsDragOver(false);
61
+ if (disabled) return;
62
+ const files = Array.from(e.dataTransfer.files || []);
63
+ if (files.length === 0) return;
64
+ const result = multiple ? files : files[0];
65
+ onChange?.(result);
66
+ },
67
+ [disabled, multiple, onChange],
68
+ );
69
+
70
+ const handleDragOver = useCallback((e: React.DragEvent) => {
71
+ e.preventDefault();
72
+ e.stopPropagation();
73
+ setIsDragOver(true);
74
+ }, []);
75
+
76
+ const handleDragLeave = useCallback((e: React.DragEvent) => {
77
+ e.preventDefault();
78
+ e.stopPropagation();
79
+ setIsDragOver(false);
80
+ }, []);
81
+
82
+ const inputId = id ?? name ?? "upload-dropzone";
83
+
84
+ const zoneStyle: React.CSSProperties = {
85
+ display: "flex",
86
+ flexDirection: "column",
87
+ alignItems: "center",
88
+ justifyContent: "center",
89
+ position: "relative",
90
+ width: fullWidth ? "100%" : undefined,
91
+ minHeight: "230px",
92
+ padding: `${tokens.zonePaddingY} ${tokens.zonePaddingX}`,
93
+ gap: tokens.zoneGap,
94
+ background:
95
+ isDragOver || hasUploadedFile
96
+ ? tokens.zoneBackgroundActive
97
+ : tokens.zoneBackground,
98
+ border:
99
+ hasUploadedFile && !isDragOver
100
+ ? uploadedBorder
101
+ : isDragOver
102
+ ? tokens.zoneBorderActive
103
+ : tokens.zoneBorder,
104
+ borderRadius: tokens.zoneBorderRadius,
105
+ cursor: disabled ? "not-allowed" : "pointer",
106
+ opacity: disabled ? tokens.disabledOpacity : 1,
107
+ boxSizing: "border-box",
108
+ transition: "all 0.2s ease",
109
+ };
110
+
111
+ const iconStyle: React.CSSProperties = {
112
+ width: tokens.iconSize,
113
+ height: tokens.iconSize,
114
+ display: "flex",
115
+ alignItems: "center",
116
+ justifyContent: "center",
117
+ color: tokens.iconColor,
118
+ marginBottom: tokens.iconMarginBottom,
119
+ };
120
+
121
+ const titleStyle: React.CSSProperties = {
122
+ fontSize: tokens.titleFontSize,
123
+ fontWeight: tokens.titleFontWeight,
124
+ fontFamily: tokens.titleFontFamily,
125
+ lineHeight: tokens.titleLineHeight,
126
+ color: tokens.titleColor,
127
+ marginBottom: tokens.titleMarginBottom,
128
+ textAlign: "center",
129
+ };
130
+
131
+ const instructionStyle: React.CSSProperties = {
132
+ fontSize: tokens.instructionFontSize,
133
+ fontWeight: tokens.instructionFontWeight,
134
+ fontFamily: tokens.instructionFontFamily,
135
+ lineHeight: tokens.instructionLineHeight,
136
+ color: tokens.instructionColor,
137
+ marginBottom: tokens.instructionMarginBottom,
138
+ textAlign: "center",
139
+ };
140
+
141
+ const hintStyle: React.CSSProperties = {
142
+ fontSize: tokens.hintFontSize,
143
+ fontWeight: tokens.hintFontWeight,
144
+ fontFamily: tokens.hintFontFamily,
145
+ lineHeight: tokens.hintLineHeight,
146
+ color: tokens.hintColor,
147
+ marginBottom: tokens.hintMarginBottom,
148
+ textAlign: "center",
149
+ };
150
+
151
+ const requiredBadgeStyle: React.CSSProperties = {
152
+ fontSize: tokens.buttonFontSize,
153
+ fontWeight: tokens.buttonFontWeight,
154
+ fontFamily: tokens.buttonFontFamily,
155
+ lineHeight: tokens.instructionLineHeight,
156
+ color: tokens.buttonColor,
157
+ backgroundColor: tokens.buttonBackground,
158
+ border: tokens.buttonBorder,
159
+ borderRadius: tokens.buttonBorderRadius,
160
+ padding: `${tokens.buttonPaddingY} ${tokens.buttonPaddingX}`,
161
+ marginTop: tokens.selectedFileMarginTop,
162
+ };
163
+
164
+ const doneBadgeStyle: React.CSSProperties = {
165
+ position: "absolute",
166
+ top: "12px",
167
+ right: "12px",
168
+ backgroundColor: tokens.buttonColor,
169
+ color: tokens.buttonBackground,
170
+ borderRadius: "999px",
171
+ padding: "6px 12px",
172
+ fontSize: tokens.buttonFontSize,
173
+ fontWeight: tokens.buttonFontWeight,
174
+ lineHeight: tokens.instructionLineHeight,
175
+ display: "flex",
176
+ alignItems: "center",
177
+ gap: "4px",
178
+ };
179
+
180
+ return (
181
+ <div style={{ width: fullWidth ? "100%" : undefined }}>
182
+ <div
183
+ role="button"
184
+ tabIndex={disabled ? -1 : 0}
185
+ onClick={handleClick}
186
+ onDrop={handleDrop}
187
+ onDragOver={handleDragOver}
188
+ onDragLeave={handleDragLeave}
189
+ onKeyDown={(e) => {
190
+ if (e.key === "Enter" || e.key === " ") {
191
+ e.preventDefault();
192
+ handleClick();
193
+ }
194
+ }}
195
+ style={zoneStyle}
196
+ aria-label={instructionText}
197
+ aria-disabled={disabled}
198
+ >
199
+ {hasUploadedFile && <div style={doneBadgeStyle}>✓ Done</div>}
200
+ <div style={iconStyle}>
201
+ <CloudUploadIcon sx={{ fontSize: tokens.iconSize, color: tokens.iconColor }} />
202
+ </div>
203
+ {title && <div style={titleStyle}>{title}</div>}
204
+ <div style={instructionStyle}>{instructionText}</div>
205
+ <div style={hintStyle}>{hintText}</div>
206
+ <div style={requiredBadgeStyle}>{buttonText}</div>
207
+ </div>
208
+ <input
209
+ ref={inputRef as React.RefObject<HTMLInputElement>}
210
+ id={inputId}
211
+ name={name}
212
+ type="file"
213
+ accept={accept}
214
+ multiple={multiple}
215
+ onChange={handleChange}
216
+ disabled={disabled}
217
+ style={{ display: "none" }}
218
+ aria-hidden
219
+ />
220
+ </div>
221
+ );
222
+ };
@@ -0,0 +1,54 @@
1
+ export interface UploadDropzoneProps {
2
+ /**
3
+ * Title shown above the drop zone
4
+ */
5
+ title?: string;
6
+ /**
7
+ * Main instruction text (e.g. "Drag & Drop Or Choose Files to Upload")
8
+ */
9
+ instructionText?: string;
10
+ /**
11
+ * Hint/restriction text (e.g. "Max file size - 5MB Allowed file extension - txt, csv, xlsx")
12
+ */
13
+ hintText?: string;
14
+ /**
15
+ * Button text
16
+ */
17
+ buttonText?: string;
18
+ /**
19
+ * Accepted file types (e.g. ".txt,.csv,.xlsx")
20
+ */
21
+ accept?: string;
22
+ /**
23
+ * Whether multiple files can be selected
24
+ */
25
+ multiple?: boolean;
26
+ /**
27
+ * Disables the drop zone
28
+ */
29
+ disabled?: boolean;
30
+ /**
31
+ * Makes the drop zone stretch to full container width
32
+ */
33
+ fullWidth?: boolean;
34
+ /**
35
+ * Selected file(s)
36
+ */
37
+ value?: File | File[] | null;
38
+ /**
39
+ * Callback when file(s) are selected
40
+ */
41
+ onChange?: (files: File | File[] | null) => void;
42
+ /**
43
+ * HTML id attribute
44
+ */
45
+ id?: string;
46
+ /**
47
+ * Input name attribute
48
+ */
49
+ name?: string;
50
+ /**
51
+ * Ref for the hidden file input element
52
+ */
53
+ inputRef?: React.RefObject<HTMLInputElement | null>;
54
+ }
@@ -0,0 +1,40 @@
1
+ import type { Meta, StoryObj } from '@storybook/react';
2
+ import React, { useState } from 'react';
3
+
4
+ import { ViewEditToggle } from './ViewEditToggle';
5
+ import type { ViewEditToggleProps } from './ViewEditToggle.types';
6
+
7
+ const meta: Meta<typeof ViewEditToggle> = {
8
+ title: 'Design System/ViewEditToggle',
9
+ component: ViewEditToggle,
10
+ decorators: [
11
+ (Story) => (
12
+ <div style={{ padding: 24, display: 'flex', justifyContent: 'flex-end' }}>
13
+ <Story />
14
+ </div>
15
+ ),
16
+ ],
17
+ args: {
18
+ value: 'view',
19
+ ready: true,
20
+ size: 'md',
21
+ } as ViewEditToggleProps,
22
+ };
23
+
24
+ export default meta;
25
+
26
+ type Story = StoryObj<typeof ViewEditToggle>;
27
+
28
+ export const Playground: Story = {
29
+ render: function PlaygroundRender(args) {
30
+ const [value, setValue] = useState(args.value);
31
+ return <ViewEditToggle {...args} value={value} onChange={setValue} />;
32
+ },
33
+ };
34
+
35
+ export const EditMode: Story = {
36
+ render: function EditModeRender() {
37
+ const [value, setValue] = useState<'view' | 'edit'>('edit');
38
+ return <ViewEditToggle value={value} onChange={setValue} ready />;
39
+ },
40
+ };
@@ -0,0 +1,239 @@
1
+ import React, { useEffect, useRef, useState } from 'react';
2
+ import { createPortal } from 'react-dom';
3
+
4
+ import { ToggleButton } from '../ToggleButton/ToggleButton';
5
+ import { useTokens } from '../../ThemeProvider/ThemeContext';
6
+ import type { ViewEditToggleProps } from './ViewEditToggle.types';
7
+
8
+ const VIEW_EDIT_OPTIONS = [
9
+ { value: 'view', label: 'View' },
10
+ { value: 'edit', label: 'Edit' },
11
+ ] as const;
12
+
13
+ const DEFAULT_SPOTLIGHT_MESSAGE = (
14
+ <>
15
+ Switch between <strong style={{ color: '#111827' }}>View</strong> and{' '}
16
+ <strong style={{ color: '#52760A' }}>Edit</strong> mode to update employee details
17
+ </>
18
+ );
19
+
20
+ function defaultIsDismissed(key: string): boolean {
21
+ try {
22
+ return localStorage.getItem(key) === 'true';
23
+ } catch {
24
+ return false;
25
+ }
26
+ }
27
+
28
+ function defaultOnDismiss(key: string): void {
29
+ try {
30
+ localStorage.setItem(key, 'true');
31
+ } catch {
32
+ /* ignore */
33
+ }
34
+ }
35
+
36
+ export const ViewEditToggle = ({
37
+ value,
38
+ onChange,
39
+ ready = false,
40
+ size = 'md',
41
+ disabled = false,
42
+ showView = true,
43
+ showEdit = true,
44
+ spotlight,
45
+ ...rest
46
+ }: ViewEditToggleProps) => {
47
+ const toggleRef = useRef<HTMLDivElement>(null);
48
+ const [spotlightRect, setSpotlightRect] = useState<DOMRect | null>(null);
49
+ const [showSpotlight, setShowSpotlight] = useState(false);
50
+
51
+ const isDismissed = spotlight?.isDismissed ?? defaultIsDismissed;
52
+ const onDismiss = spotlight?.onDismiss ?? defaultOnDismiss;
53
+
54
+ const options = VIEW_EDIT_OPTIONS.filter((option) => {
55
+ if (option.value === 'view') return showView;
56
+ if (option.value === 'edit') return showEdit;
57
+ return true;
58
+ });
59
+
60
+ const canShowToggle = options.length > 0;
61
+ const canShowSpotlight = canShowToggle && showView && showEdit;
62
+
63
+ useEffect(() => {
64
+ if (!ready || !spotlight?.storageKey || !canShowSpotlight) return;
65
+ if (isDismissed(spotlight.storageKey)) return;
66
+
67
+ const timer = window.setTimeout(() => {
68
+ if (toggleRef.current) {
69
+ setSpotlightRect(toggleRef.current.getBoundingClientRect());
70
+ setShowSpotlight(true);
71
+ }
72
+ }, 400);
73
+
74
+ return () => window.clearTimeout(timer);
75
+ }, [ready, spotlight?.storageKey, isDismissed, canShowSpotlight]);
76
+
77
+ const handleSkip = () => {
78
+ if (spotlight?.storageKey) {
79
+ onDismiss(spotlight.storageKey);
80
+ }
81
+ setShowSpotlight(false);
82
+ };
83
+
84
+ if (!canShowToggle) return null;
85
+
86
+ return (
87
+ <>
88
+ <div ref={toggleRef} {...rest}>
89
+ <ToggleButton
90
+ options={[...options]}
91
+ value={value}
92
+ onChange={(next) => onChange(next as 'view' | 'edit')}
93
+ size={size}
94
+ variant="pill"
95
+ disabled={disabled}
96
+ />
97
+ </div>
98
+
99
+ {showSpotlight && spotlightRect && spotlight && canShowSpotlight && (
100
+ <SpotlightOverlay
101
+ rect={spotlightRect}
102
+ onSkip={handleSkip}
103
+ message={spotlight.message ?? DEFAULT_SPOTLIGHT_MESSAGE}
104
+ dismissLabel={spotlight.dismissLabel ?? 'Skip'}
105
+ />
106
+ )}
107
+ </>
108
+ );
109
+ };
110
+
111
+ interface SpotlightOverlayProps {
112
+ rect: DOMRect;
113
+ onSkip: () => void;
114
+ message: React.ReactNode;
115
+ dismissLabel: string;
116
+ }
117
+
118
+ function SpotlightOverlay({ rect, onSkip, message, dismissLabel }: SpotlightOverlayProps) {
119
+ const resolvedTokens = useTokens();
120
+ const accentColor =
121
+ resolvedTokens.semantic?.color?.primary?.default ??
122
+ resolvedTokens.foundation?.colors?.green?.['500'] ??
123
+ '#52760A';
124
+
125
+ const pad = 10;
126
+ const x = rect.left - pad;
127
+ const y = rect.top - pad;
128
+ const w = rect.width + pad * 2;
129
+ const h = rect.height + pad * 2;
130
+ const rx = (h / 2).toFixed(1);
131
+
132
+ const tooltipLeft = Math.min(Math.max(x + w / 2, 120), window.innerWidth - 120);
133
+
134
+ return createPortal(
135
+ <div style={{ position: 'fixed', inset: 0, zIndex: 9999 }} onClick={onSkip}>
136
+ <svg
137
+ style={{ position: 'absolute', inset: 0, width: '100%', height: '100%' }}
138
+ xmlns="http://www.w3.org/2000/svg"
139
+ >
140
+ <defs>
141
+ <mask id="ds-ve-spotlight-mask">
142
+ <rect width="100%" height="100%" fill="white" />
143
+ <rect x={x} y={y} width={w} height={h} rx={rx} ry={rx} fill="black" />
144
+ </mask>
145
+ </defs>
146
+ <rect
147
+ width="100%"
148
+ height="100%"
149
+ fill="rgba(0,0,0,0.58)"
150
+ mask="url(#ds-ve-spotlight-mask)"
151
+ />
152
+ </svg>
153
+
154
+ <div
155
+ style={{
156
+ position: 'absolute',
157
+ left: x - 3,
158
+ top: y - 3,
159
+ width: w + 6,
160
+ height: h + 6,
161
+ borderRadius: `calc(${rx}px + 3px)`,
162
+ border: `2px solid color-mix(in srgb, ${accentColor} 80%, transparent)`,
163
+ boxShadow: `0 0 0 4px color-mix(in srgb, ${accentColor} 20%, transparent)`,
164
+ pointerEvents: 'none',
165
+ animation: 'ds-ve-pulse 1.6s ease-in-out infinite',
166
+ }}
167
+ />
168
+
169
+ <div
170
+ style={{
171
+ position: 'absolute',
172
+ top: y + h + 16,
173
+ left: tooltipLeft,
174
+ transform: 'translateX(-50%)',
175
+ background: '#FFFFFF',
176
+ borderRadius: 10,
177
+ padding: '14px 18px',
178
+ boxShadow: '0 6px 24px rgba(0,0,0,0.18)',
179
+ minWidth: 240,
180
+ maxWidth: 280,
181
+ textAlign: 'center',
182
+ pointerEvents: 'auto',
183
+ }}
184
+ onClick={(e) => e.stopPropagation()}
185
+ >
186
+ <div
187
+ style={{
188
+ position: 'absolute',
189
+ top: -7,
190
+ left: '50%',
191
+ transform: 'translateX(-50%)',
192
+ width: 0,
193
+ height: 0,
194
+ borderLeft: '8px solid transparent',
195
+ borderRight: '8px solid transparent',
196
+ borderBottom: '8px solid #FFFFFF',
197
+ }}
198
+ />
199
+
200
+ <p
201
+ style={{
202
+ margin: '0 0 12px',
203
+ fontSize: 13,
204
+ color: '#374151',
205
+ fontWeight: 500,
206
+ lineHeight: 1.5,
207
+ }}
208
+ >
209
+ {message}
210
+ </p>
211
+
212
+ <button
213
+ type="button"
214
+ onClick={onSkip}
215
+ style={{
216
+ background: accentColor,
217
+ color: '#FFFFFF',
218
+ border: 'none',
219
+ borderRadius: 6,
220
+ padding: '6px 20px',
221
+ fontSize: 13,
222
+ cursor: 'pointer',
223
+ fontWeight: 600,
224
+ }}
225
+ >
226
+ {dismissLabel}
227
+ </button>
228
+ </div>
229
+
230
+ <style>{`
231
+ @keyframes ds-ve-pulse {
232
+ 0%, 100% { box-shadow: 0 0 0 4px color-mix(in srgb, ${accentColor} 20%, transparent); }
233
+ 50% { box-shadow: 0 0 0 8px color-mix(in srgb, ${accentColor} 8%, transparent); }
234
+ }
235
+ `}</style>
236
+ </div>,
237
+ document.body
238
+ );
239
+ }
@@ -0,0 +1,27 @@
1
+ import type { ReactNode } from 'react';
2
+
3
+ export type ViewEditMode = 'view' | 'edit';
4
+
5
+ export interface ViewEditToggleSpotlightProps {
6
+ storageKey: string;
7
+ isDismissed?: (key: string) => boolean;
8
+ onDismiss?: (key: string) => void;
9
+ message?: ReactNode;
10
+ dismissLabel?: string;
11
+ }
12
+
13
+ export interface ViewEditToggleProps extends Omit<
14
+ React.HTMLAttributes<HTMLDivElement>,
15
+ 'onChange'
16
+ > {
17
+ value: ViewEditMode;
18
+ onChange: (value: ViewEditMode) => void;
19
+ ready?: boolean;
20
+ size?: 'sm' | 'md' | 'lg';
21
+ disabled?: boolean;
22
+ /** When false, hides the View option. Defaults to true. */
23
+ showView?: boolean;
24
+ /** When false, hides the Edit option. Defaults to true. */
25
+ showEdit?: boolean;
26
+ spotlight?: ViewEditToggleSpotlightProps;
27
+ }