@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,138 @@
1
+ import React, { useState } from "react";
2
+ import type { Meta, StoryObj } from "@storybook/react";
3
+ import { IpWhitelistField } from "./IpWhitelistField";
4
+ import type { IpWhitelistFieldProps, IpWhitelistRow } from "./IpWhitelistField.types";
5
+
6
+ const meta: Meta<typeof IpWhitelistField> = {
7
+ title: "Design System/IpWhitelistField",
8
+ component: IpWhitelistField,
9
+ tags: [],
10
+ decorators: [
11
+ (Story) => (
12
+ <div style={{ padding: "24px", maxWidth: "780px" }}>
13
+ <Story />
14
+ </div>
15
+ ),
16
+ ],
17
+ args: {
18
+ label: "Whitelisted source IP",
19
+ required: true,
20
+ disabled: false,
21
+ error: false,
22
+ allowWildcard: true,
23
+ minRows: 1,
24
+ rows: [{ input1: "", input2: "", input3: "", input4: "" }],
25
+ } as IpWhitelistFieldProps,
26
+ argTypes: {
27
+ required: {
28
+ control: { type: "boolean" },
29
+ description: "Marks the field as required",
30
+ },
31
+ disabled: {
32
+ control: { type: "boolean" },
33
+ description: "Disables the field",
34
+ },
35
+ error: {
36
+ control: { type: "boolean" },
37
+ description: "Shows error state styling on all segments",
38
+ },
39
+ allowWildcard: {
40
+ control: { type: "boolean" },
41
+ description: "Allow '*' wildcard per octet",
42
+ },
43
+ minRows: {
44
+ control: { type: "number" },
45
+ description: "Minimum rows to keep (remove hidden at/below this)",
46
+ },
47
+ maxRows: {
48
+ control: { type: "number" },
49
+ description: "Maximum rows allowed (add disabled at/above this)",
50
+ },
51
+ rows: {
52
+ control: { type: "object" },
53
+ description: "Controlled rows state",
54
+ },
55
+ onRowsChange: { table: { disable: true } },
56
+ addRowAriaLabel: { control: { type: "text" } },
57
+ removeRowAriaLabel: { control: { type: "text" } },
58
+ },
59
+ };
60
+
61
+ export default meta;
62
+
63
+ type Story = StoryObj<typeof IpWhitelistField>;
64
+
65
+ const ControlledIpWhitelistField = (props: IpWhitelistFieldProps) => {
66
+ const [rows, setRows] = useState<IpWhitelistRow[]>(props.rows);
67
+
68
+ React.useEffect(() => {
69
+ setRows(props.rows);
70
+ }, [props.rows]);
71
+
72
+ return <IpWhitelistField {...props} rows={rows} onRowsChange={setRows} />;
73
+ };
74
+
75
+ export const Playground: Story = {
76
+ render: (args) => {
77
+ return <ControlledIpWhitelistField {...args} />;
78
+ },
79
+ };
80
+
81
+ const ShowcaseComponent = () => {
82
+ const [rows1, setRows1] = useState<IpWhitelistRow[]>([
83
+ { input1: "", input2: "", input3: "", input4: "" },
84
+ ]);
85
+ const [rows2, setRows2] = useState<IpWhitelistRow[]>([
86
+ { input1: "10", input2: "0", input3: "*", input4: "1" },
87
+ ]);
88
+ const [rows3, setRows3] = useState<IpWhitelistRow[]>([
89
+ { input1: "192", input2: "168", input3: "1", input4: "*" },
90
+ { input1: "172", input2: "16", input3: "0", input4: "10" },
91
+ ]);
92
+
93
+ return (
94
+ <div style={{ display: "flex", flexDirection: "column", gap: "24px" }}>
95
+ <IpWhitelistField
96
+ label="Whitelisted source IP"
97
+ required
98
+ rows={rows1}
99
+ onRowsChange={setRows1}
100
+ />
101
+
102
+ <IpWhitelistField
103
+ label="Prefilled (wildcards)"
104
+ required
105
+ rows={rows2}
106
+ onRowsChange={setRows2}
107
+ />
108
+
109
+ <IpWhitelistField
110
+ label="Multiple rows + maxRows=2"
111
+ required
112
+ maxRows={2}
113
+ rows={rows3}
114
+ onRowsChange={setRows3}
115
+ />
116
+
117
+ <IpWhitelistField
118
+ label="Error state"
119
+ required
120
+ error
121
+ rows={rows2}
122
+ onRowsChange={setRows2}
123
+ />
124
+
125
+ <IpWhitelistField
126
+ label="Disabled"
127
+ required
128
+ disabled
129
+ rows={rows2}
130
+ onRowsChange={setRows2}
131
+ />
132
+ </div>
133
+ );
134
+ };
135
+
136
+ export const Showcase: Story = {
137
+ render: () => <ShowcaseComponent />,
138
+ };
@@ -0,0 +1,164 @@
1
+ import { useMemo } from "react";
2
+ import type {
3
+ IpWhitelistFieldProps,
4
+ IpWhitelistRow,
5
+ } from "./IpWhitelistField.types";
6
+ import { useTokens } from "../../ThemeProvider/ThemeContext";
7
+ import { getIpWhitelistFieldTokensFromResolved } from "../../tokens";
8
+ import { Button } from "../Button/Button";
9
+ import { SegmentedInputField } from "../SegmentedInputField/SegmentedInputField";
10
+ // import { Icon as FirebirdIcon } from "firebird-icon-lib";
11
+
12
+ const EMPTY_ROW: IpWhitelistRow = {
13
+ input1: "",
14
+ input2: "",
15
+ input3: "",
16
+ input4: "",
17
+ };
18
+
19
+ export const IpWhitelistField = ({
20
+ label,
21
+ required = false,
22
+ disabled = false,
23
+ error = false,
24
+ allowWildcard = true,
25
+ minRows = 1,
26
+ maxRows,
27
+ rows,
28
+ onRowsChange,
29
+ addRowAriaLabel = "Add IP row",
30
+ removeRowAriaLabel = "Remove IP row",
31
+ }: IpWhitelistFieldProps) => {
32
+ const resolvedTokens = useTokens();
33
+ const ipTokens = getIpWhitelistFieldTokensFromResolved(resolvedTokens);
34
+
35
+ const isAnyRowIncomplete = useMemo(
36
+ () =>
37
+ rows.some(
38
+ (r) =>
39
+ r.input1 === "" ||
40
+ r.input2 === "" ||
41
+ r.input3 === "" ||
42
+ r.input4 === "",
43
+ ),
44
+ [rows],
45
+ );
46
+
47
+ const updateRow = (index: number, patch: Partial<IpWhitelistRow>) => {
48
+ const next = [...rows];
49
+ next[index] = { ...next[index], ...patch };
50
+ onRowsChange(next);
51
+ };
52
+
53
+ const removeRow = (index: number) => {
54
+ onRowsChange(rows.filter((_, i) => i !== index));
55
+ };
56
+
57
+ const addRow = () => {
58
+ onRowsChange([...rows, { ...EMPTY_ROW }]);
59
+ };
60
+
61
+ const canAddMore = maxRows ? rows.length < maxRows : true;
62
+
63
+ const isAllowedOctetValue = (val: string) => {
64
+ if (val === "") return true;
65
+ if (allowWildcard && val === "*") return true;
66
+ const n = Number(val);
67
+ return Number.isInteger(n) && n >= 0 && n <= 255;
68
+ };
69
+
70
+ return (
71
+ <div style={{ width: "100%" }}>
72
+ <div style={{ marginTop: ipTokens.labelMarginTop }}>
73
+ <label style={{ display: "block", color: ipTokens.labelColor }}>
74
+ {label}
75
+ {required && (
76
+ <span
77
+ style={{
78
+ marginLeft: ipTokens.requiredGap,
79
+ color: ipTokens.requiredColor,
80
+ }}
81
+ >
82
+ *
83
+ </span>
84
+ )}
85
+ </label>
86
+ </div>
87
+
88
+ {rows.map((item, index) => (
89
+ <div key={index} style={{ marginTop: ipTokens.rowMarginTop }}>
90
+ <div
91
+ style={{
92
+ display: "flex",
93
+ alignItems: "center",
94
+ gap: ipTokens.groupGap,
95
+ }}
96
+ className="client_ip"
97
+ >
98
+ <SegmentedInputField
99
+ values={[item.input1, item.input2, item.input3, item.input4]}
100
+ onValuesChange={(next) => {
101
+ updateRow(index, {
102
+ input1: next[0] ?? "",
103
+ input2: next[1] ?? "",
104
+ input3: next[2] ?? "",
105
+ input4: next[3] ?? "",
106
+ });
107
+ }}
108
+ disabled={disabled}
109
+ error={error}
110
+ size="lg"
111
+ showSeparator
112
+ separator={<span className="spanMargin m-0">.</span>}
113
+ inputMode="numeric"
114
+ maxLength={3}
115
+ autoComplete="off"
116
+ getAriaLabel={(segmentIndex) => `IP part ${segmentIndex + 1}`}
117
+ sanitizeSegment={(raw) => {
118
+ if (allowWildcard && raw === "*") return raw;
119
+ return raw.replace(/[^0-9]/g, "").slice(0, 3);
120
+ }}
121
+ validateSegment={(nextValue) => isAllowedOctetValue(nextValue)}
122
+ isSegmentComplete={(segmentValue) =>
123
+ (allowWildcard && segmentValue === "*") ||
124
+ segmentValue.length === 3
125
+ }
126
+ />
127
+
128
+ <div
129
+ style={{
130
+ display: "flex",
131
+ alignItems: "center",
132
+ gap: ipTokens.buttonsGap,
133
+ }}
134
+ >
135
+ <Button
136
+ size="lg"
137
+ variant="blue"
138
+ disabled={disabled || isAnyRowIncomplete || !canAddMore}
139
+ onClick={addRow}
140
+ iconOnly
141
+ aria-label={addRowAriaLabel}
142
+ >
143
+ {/* <FirebirdIcon name="add_circle" size={20} /> */}
144
+ </Button>
145
+
146
+ {rows.length > minRows && (
147
+ <Button
148
+ size="lg"
149
+ variant="red"
150
+ iconOnly
151
+ disabled={disabled}
152
+ onClick={() => removeRow(index)}
153
+ aria-label={removeRowAriaLabel}
154
+ >
155
+ {/* <FirebirdIcon name="cancel_circle" size={20} /> */}
156
+ </Button>
157
+ )}
158
+ </div>
159
+ </div>
160
+ </div>
161
+ ))}
162
+ </div>
163
+ );
164
+ };
@@ -0,0 +1,42 @@
1
+ export type IpWhitelistOctet = string;
2
+
3
+ export interface IpWhitelistRow {
4
+ input1: IpWhitelistOctet;
5
+ input2: IpWhitelistOctet;
6
+ input3: IpWhitelistOctet;
7
+ input4: IpWhitelistOctet;
8
+ }
9
+
10
+ export interface IpWhitelistFieldProps {
11
+ /** Field label shown above the IP rows. */
12
+ label: string;
13
+ required?: boolean;
14
+ disabled?: boolean;
15
+ /** When true, shows error styling on the IP inputs. */
16
+ error?: boolean;
17
+ /**
18
+ * Allow "*" in an octet to represent wildcard.
19
+ * Defaults to true to match existing Create Account behavior.
20
+ */
21
+ allowWildcard?: boolean;
22
+ /**
23
+ * Minimum number of rows the field should keep.
24
+ * When `rows.length` is at/below this number, remove is hidden/disabled.
25
+ * Defaults to 1.
26
+ */
27
+ minRows?: number;
28
+ /**
29
+ * Maximum number of rows allowed. When reached, add is disabled.
30
+ */
31
+ maxRows?: number;
32
+ /**
33
+ * Controlled rows. Each row represents an IPv4 pattern (supports "*" per octet).
34
+ */
35
+ rows: IpWhitelistRow[];
36
+ onRowsChange: (nextRows: IpWhitelistRow[]) => void;
37
+
38
+ /** Optional aria-labels for icon-only buttons. */
39
+ addRowAriaLabel?: string;
40
+ removeRowAriaLabel?: string;
41
+ }
42
+
@@ -0,0 +1,95 @@
1
+ import type { Meta, StoryObj } from "@storybook/react";
2
+ import { Loader } from "./Loader";
3
+
4
+ const meta: Meta<typeof Loader> = {
5
+ title: "Design System/Loader",
6
+ component: Loader,
7
+ tags: [],
8
+ decorators: [(Story) => <Story />],
9
+ args: {
10
+ open: true,
11
+ },
12
+ argTypes: {
13
+ open: {
14
+ control: { type: "boolean" },
15
+ description: "Whether the loader overlay is visible",
16
+ table: {
17
+ type: { summary: "boolean" },
18
+ defaultValue: { summary: "false" },
19
+ },
20
+ },
21
+ zIndex: {
22
+ control: { type: "number" },
23
+ description: "Override z-index (default: 9999 from tokens)",
24
+ table: {
25
+ type: { summary: "number" },
26
+ defaultValue: { summary: "9999" },
27
+ },
28
+ },
29
+ },
30
+ };
31
+
32
+ export default meta;
33
+
34
+ type Story = StoryObj<typeof Loader>;
35
+
36
+ // Main interactive story with controls
37
+ export const Playground: Story = {
38
+ args: {
39
+ open: true,
40
+ },
41
+ };
42
+
43
+ // Showcase story
44
+ export const Showcase: Story = {
45
+ render: () => (
46
+ <div
47
+ style={{
48
+ display: "flex",
49
+ flexDirection: "column",
50
+ gap: "32px",
51
+ padding: "20px",
52
+ }}
53
+ >
54
+ <div>
55
+ <h3
56
+ style={{
57
+ fontSize: "18px",
58
+ fontWeight: "bold",
59
+ marginBottom: "16px",
60
+ }}
61
+ >
62
+ Loader (Fullscreen Overlay)
63
+ </h3>
64
+ <p style={{ fontSize: "14px", color: "#666", marginBottom: "16px" }}>
65
+ The Loader component renders a fullscreen overlay with animated dots.
66
+ Toggle the <code>open</code> prop in the Playground story to see it in
67
+ action.
68
+ </p>
69
+ <div
70
+ style={{
71
+ position: "relative",
72
+ width: "100%",
73
+ height: "300px",
74
+ border: "1px solid #e5e7eb",
75
+ borderRadius: "12px",
76
+ overflow: "hidden",
77
+ }}
78
+ >
79
+ <div
80
+ style={{
81
+ position: "absolute",
82
+ inset: 0,
83
+ display: "flex",
84
+ alignItems: "center",
85
+ justifyContent: "center",
86
+ backgroundColor: "rgba(0, 0, 0, 0.5)",
87
+ }}
88
+ >
89
+ <Loader open={true} zIndex={1} />
90
+ </div>
91
+ </div>
92
+ </div>
93
+ </div>
94
+ ),
95
+ };
@@ -0,0 +1,132 @@
1
+ import React, { useEffect, useRef } from "react";
2
+ import { getLoaderTokensFromResolved } from "../../tokens";
3
+ import { useTokens } from "../../ThemeProvider/ThemeContext";
4
+ import type { LoaderProps } from "./Loader.types";
5
+
6
+ const KEYFRAMES_ID = "ds-loader-dots-keyframes";
7
+
8
+ function parsePx(value: string | number): number {
9
+ if (typeof value === "number") return value;
10
+ const num = parseInt(String(value).replace(/px$/i, ""), 10);
11
+ return Number.isFinite(num) ? num : 0;
12
+ }
13
+
14
+ function ensureKeyframes(gap: number, activeSpread: number) {
15
+ if (typeof document === "undefined") return;
16
+ const existing = document.getElementById(KEYFRAMES_ID);
17
+ if (existing) return;
18
+
19
+ const g = gap;
20
+ const s = activeSpread;
21
+
22
+ const css = `
23
+ @keyframes dsLoaderDots {
24
+ 0% {
25
+ box-shadow:
26
+ ${g}px -${g}px 0 0px, ${g * 2}px -${g}px 0 0px, ${g * 3}px -${g}px 0 0px,
27
+ ${g}px 0 0 ${s}px, ${g * 2}px 0 0 ${s}px, ${g * 3}px 0 0 ${s}px,
28
+ ${g}px ${g}px 0 0px, ${g * 2}px ${g}px 0 0px, ${g * 3}px ${g}px 0 0px;
29
+ }
30
+ 12.5% {
31
+ box-shadow:
32
+ ${g}px -${g}px 0 0px, ${g * 2}px -${g}px 0 0px, ${g * 3}px -${g}px 0 ${s}px,
33
+ ${g}px 0 0 ${s}px, ${g * 2}px 0 0 0px, ${g * 3}px 0 0 ${s}px,
34
+ ${g}px ${g}px 0 0px, ${g * 2}px ${g}px 0 0px, ${g * 3}px ${g}px 0 0px;
35
+ }
36
+ 25% {
37
+ box-shadow:
38
+ ${g}px -${g}px 0 ${s}px, ${g * 2}px -${g}px 0 0px, ${g * 3}px -${g}px 0 ${s}px,
39
+ ${g}px 0 0 0px, ${g * 2}px 0 0 0px, ${g * 3}px 0 0 0px,
40
+ ${g}px ${g}px 0 0px, ${g * 2}px ${g}px 0 ${s}px, ${g * 3}px ${g}px 0 0px;
41
+ }
42
+ 50% {
43
+ box-shadow:
44
+ ${g}px -${g}px 0 ${s}px, ${g * 2}px -${g}px 0 ${s}px, ${g * 3}px -${g}px 0 0px,
45
+ ${g}px 0 0 0px, ${g * 2}px 0 0 0px, ${g * 3}px 0 0 0px,
46
+ ${g}px ${g}px 0 0px, ${g * 2}px ${g}px 0 0px, ${g * 3}px ${g}px 0 ${s}px;
47
+ }
48
+ 62.5% {
49
+ box-shadow:
50
+ ${g}px -${g}px 0 0px, ${g * 2}px -${g}px 0 0px, ${g * 3}px -${g}px 0 0px,
51
+ ${g}px 0 0 ${s}px, ${g * 2}px 0 0 0px, ${g * 3}px 0 0 0px,
52
+ ${g}px ${g}px 0 0px, ${g * 2}px ${g}px 0 ${s}px, ${g * 3}px ${g}px 0 ${s}px;
53
+ }
54
+ 75% {
55
+ box-shadow:
56
+ ${g}px -${g}px 0 0px, ${g * 2}px -${g}px 0 ${s}px, ${g * 3}px -${g}px 0 0px,
57
+ ${g}px 0 0 0px, ${g * 2}px 0 0 0px, ${g * 3}px 0 0 ${s}px,
58
+ ${g}px ${g}px 0 0px, ${g * 2}px ${g}px 0 0px, ${g * 3}px ${g}px 0 ${s}px;
59
+ }
60
+ 87.5% {
61
+ box-shadow:
62
+ ${g}px -${g}px 0 0px, ${g * 2}px -${g}px 0 ${s}px, ${g * 3}px -${g}px 0 0px,
63
+ ${g}px 0 0 0px, ${g * 2}px 0 0 ${s}px, ${g * 3}px 0 0 0px,
64
+ ${g}px ${g}px 0 ${s}px, ${g * 2}px ${g}px 0 0px, ${g * 3}px ${g}px 0 0px;
65
+ }
66
+ }`;
67
+
68
+ const style = document.createElement("style");
69
+ style.id = KEYFRAMES_ID;
70
+ style.textContent = css;
71
+ document.head.appendChild(style);
72
+ }
73
+
74
+ export const Loader = ({ open, zIndex = 9999 }: LoaderProps) => {
75
+ const resolvedTokens = useTokens();
76
+ const tokens = getLoaderTokensFromResolved(resolvedTokens);
77
+ const injectedRef = useRef(false);
78
+
79
+ const gap = parsePx(tokens.dotGap);
80
+ const activeSpread = parsePx(tokens.dotActiveSpread);
81
+
82
+ useEffect(() => {
83
+ if (!injectedRef.current) {
84
+ ensureKeyframes(gap, activeSpread);
85
+ injectedRef.current = true;
86
+ }
87
+ }, [gap, activeSpread]);
88
+
89
+ if (!open) return null;
90
+
91
+ const backdropStyle: React.CSSProperties = {
92
+ position: "fixed",
93
+ top: 0,
94
+ left: 0,
95
+ right: 0,
96
+ bottom: 0,
97
+ display: "flex",
98
+ alignItems: "center",
99
+ justifyContent: "center",
100
+ backgroundColor: tokens.backdropBackground,
101
+ opacity: Number.isFinite(parseFloat(tokens.backdropOpacity))
102
+ ? parseFloat(tokens.backdropOpacity)
103
+ : 0.5,
104
+ zIndex,
105
+ };
106
+
107
+ const dotStyle: React.CSSProperties = {
108
+ width: tokens.dotSize,
109
+ aspectRatio: "1",
110
+ borderRadius: "50%",
111
+ color: tokens.dotColor,
112
+ transform: `translateX(-${gap * 2}px)`,
113
+ animation: "dsLoaderDots 2s infinite linear",
114
+ boxShadow: [
115
+ `${gap}px -${gap}px 0 0px`,
116
+ `${gap * 2}px -${gap}px 0 0px`,
117
+ `${gap * 3}px -${gap}px 0 0px`,
118
+ `${gap}px 0 0 ${activeSpread}px`,
119
+ `${gap * 2}px 0 0 ${activeSpread}px`,
120
+ `${gap * 3}px 0 0 ${activeSpread}px`,
121
+ `${gap}px ${gap}px 0 0px`,
122
+ `${gap * 2}px ${gap}px 0 0px`,
123
+ `${gap * 3}px ${gap}px 0 0px`,
124
+ ].join(", "),
125
+ };
126
+
127
+ return (
128
+ <div style={backdropStyle}>
129
+ <div style={dotStyle} />
130
+ </div>
131
+ );
132
+ };
@@ -0,0 +1,6 @@
1
+ export interface LoaderProps {
2
+ /** Whether the loader overlay is visible */
3
+ open: boolean;
4
+ /** Override z-index. Default: from tokens (9999) */
5
+ zIndex?: number;
6
+ }