@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.
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,249 @@
1
+ import React, { useState } from "react";
2
+ import type { Meta, StoryObj } from "@storybook/react";
3
+ import { TextField } from "./TextField";
4
+ import type { TextFieldProps } from "./TextField.types";
5
+ import PersonIcon from "@mui/icons-material/Person";
6
+ import SearchIcon from "@mui/icons-material/Search";
7
+
8
+ const meta: Meta<typeof TextField> = {
9
+ title: "Design System/TextField",
10
+ component: TextField,
11
+ tags: [],
12
+ decorators: [
13
+ (Story) => (
14
+ <div style={{ padding: "24px", maxWidth: "420px" }}>
15
+ <Story />
16
+ </div>
17
+ ),
18
+ ],
19
+ args: {
20
+ label: "First Name",
21
+ placeholder: "First Name",
22
+ required: false,
23
+ error: false,
24
+ helperText: "",
25
+ size: "md",
26
+ fullWidth: true,
27
+ color: "white",
28
+ } as TextFieldProps,
29
+ argTypes: {
30
+ color: {
31
+ control: { type: "radio" },
32
+ options: ["white", "grey"],
33
+ },
34
+ size: {
35
+ control: { type: "radio" },
36
+ options: ["sm", "md", "lg"],
37
+ description: "Size of the text field",
38
+ },
39
+ error: {
40
+ control: { type: "boolean" },
41
+ description: "Shows error state and styles",
42
+ },
43
+ helperText: {
44
+ control: { type: "text" },
45
+ description: "Helper or error text displayed below the field",
46
+ },
47
+ disabled: {
48
+ control: { type: "boolean" },
49
+ description: "Disables the field",
50
+ },
51
+ required: {
52
+ control: { type: "boolean" },
53
+ description: "Marks the field as required",
54
+ },
55
+ multiline: {
56
+ control: { type: "boolean" },
57
+ description: "Renders a multiline textarea",
58
+ },
59
+ startIcon: {
60
+ control: { type: "select" },
61
+ options: ["none", "SearchIcon", "PersonIcon"],
62
+ mapping: {
63
+ none: undefined,
64
+ SearchIcon: <SearchIcon fontSize="small" />,
65
+ PersonIcon: <PersonIcon fontSize="small" />,
66
+ },
67
+ description:
68
+ "Icon at the start (left) of the input. Pass any MUI or firebird-icon-lib SVG component.",
69
+ },
70
+ endIcon: {
71
+ control: { type: "select" },
72
+ options: ["none", "SearchIcon", "PersonIcon"],
73
+ mapping: {
74
+ none: undefined,
75
+ SearchIcon: <SearchIcon fontSize="small" />,
76
+ PersonIcon: <PersonIcon fontSize="small" />,
77
+ },
78
+ description:
79
+ "Icon at the end (right) of the input. Pass any MUI or firebird-icon-lib SVG component.",
80
+ },
81
+ value: {
82
+ control: { type: "text" },
83
+ },
84
+ },
85
+ };
86
+
87
+ export default meta;
88
+
89
+ type Story = StoryObj<typeof TextField>;
90
+
91
+ export const Playground: Story = {
92
+ args: {
93
+ label: "First Name",
94
+ placeholder: "Enter first name",
95
+ required: true,
96
+ startIcon: <SearchIcon fontSize="small" />,
97
+ },
98
+ render: (args) => {
99
+ // Storybook `render` is not guaranteed to be treated as a React component by eslint,
100
+ // so keep hooks inside a component to satisfy `rules-of-hooks`.
101
+ const PlaygroundWithState = (props: typeof args) => {
102
+ const [value, setValue] = useState<string>(
103
+ typeof props.value === "string" ? props.value : "",
104
+ );
105
+
106
+ React.useEffect(() => {
107
+ if (typeof props.value === "string") setValue(props.value);
108
+ }, [props.value]);
109
+
110
+ return (
111
+ <TextField
112
+ {...props}
113
+ value={value}
114
+ onChange={(e) => setValue(e.target.value)}
115
+ />
116
+ );
117
+ };
118
+
119
+ return <PlaygroundWithState {...args} />;
120
+ },
121
+ };
122
+
123
+ const ShowcaseComponent = () => {
124
+ const [value1, setValue1] = useState("");
125
+ const [value2, setValue2] = useState("John Doe");
126
+ const [value3, setValue3] = useState("");
127
+ const [value4, setValue4] = useState("");
128
+ const [value5, setValue5] = useState("Finance");
129
+ const [value6, setValue6] = useState("John Doe");
130
+ const [value7, setValue7] = useState("");
131
+
132
+ return (
133
+ <div style={{ display: "flex", flexDirection: "column", gap: "24px" }}>
134
+ {/* Default — no icon */}
135
+ <TextField
136
+ label="First Name"
137
+ placeholder="Enter first name"
138
+ required
139
+ value={value1}
140
+ onChange={(e) => setValue1(e.target.value)}
141
+ />
142
+
143
+ {/* With start icon */}
144
+ <TextField
145
+ label="Search"
146
+ placeholder="Search..."
147
+ startIcon={<SearchIcon fontSize="small" />}
148
+ value={value1}
149
+ onChange={(e) => setValue1(e.target.value)}
150
+ />
151
+
152
+ {/* With end icon */}
153
+ <TextField
154
+ label="First Name"
155
+ placeholder="Enter first name"
156
+ required
157
+ endIcon={<PersonIcon fontSize="small" />}
158
+ value={value2}
159
+ onChange={(e) => setValue2(e.target.value)}
160
+ />
161
+
162
+ {/* With both icons */}
163
+ <TextField
164
+ label="Search People"
165
+ placeholder="Search..."
166
+ startIcon={<SearchIcon fontSize="small" />}
167
+ endIcon={<PersonIcon fontSize="small" />}
168
+ value={value1}
169
+ onChange={(e) => setValue1(e.target.value)}
170
+ />
171
+
172
+ {/* Error state */}
173
+ <TextField
174
+ label="Department"
175
+ placeholder="Enter department"
176
+ required
177
+ startIcon={<PersonIcon fontSize="small" />}
178
+ error
179
+ helperText="Department name already exists"
180
+ value={value5}
181
+ onChange={(e) => setValue5(e.target.value)}
182
+ />
183
+
184
+ {/* Helper text */}
185
+ <TextField
186
+ label="Email"
187
+ placeholder="Enter email"
188
+ helperText="We'll never share your email"
189
+ value={value4}
190
+ onChange={(e) => setValue4(e.target.value)}
191
+ />
192
+
193
+ {/* Disabled with value */}
194
+ <TextField
195
+ label="First Name"
196
+ placeholder="Enter first name"
197
+ required
198
+ disabled
199
+ value={value6}
200
+ onChange={(e) => setValue6(e.target.value)}
201
+ />
202
+
203
+ {/* Multiline */}
204
+ <TextField
205
+ label="Description"
206
+ placeholder="Enter description"
207
+ required
208
+ multiline
209
+ rows={3}
210
+ value={value3}
211
+ onChange={(e) => setValue3(e.target.value)}
212
+ />
213
+
214
+ {/* Small size */}
215
+ <TextField
216
+ label="First Name (sm)"
217
+ placeholder="Enter first name"
218
+ size="sm"
219
+ startIcon={<PersonIcon fontSize="small" />}
220
+ value={value7}
221
+ onChange={(e) => setValue7(e.target.value)}
222
+ />
223
+
224
+ {/* Large size */}
225
+ <TextField
226
+ label="First Name (lg)"
227
+ placeholder="Enter first name"
228
+ size="lg"
229
+ startIcon={<PersonIcon fontSize="small" />}
230
+ value={value7}
231
+ onChange={(e) => setValue7(e.target.value)}
232
+ />
233
+
234
+ {/* Color: white */}
235
+ <TextField
236
+ label="First Name (white)"
237
+ placeholder="Enter first name"
238
+ required
239
+ color="white"
240
+ value={value1}
241
+ onChange={(e) => setValue1(e.target.value)}
242
+ />
243
+ </div>
244
+ );
245
+ };
246
+
247
+ export const Showcase: Story = {
248
+ render: () => <ShowcaseComponent />,
249
+ };
@@ -0,0 +1,462 @@
1
+ import React, { useState, useEffect, useRef } from 'react';
2
+ import type { TextFieldProps } from './TextField.types';
3
+ import { getTextFieldTokensFromResolved } from '../../tokens';
4
+ import { useTokens } from '../../ThemeProvider/ThemeContext';
5
+ // import { Icon as FirebirdIcon } from "firebird-icon-lib";
6
+ import { Skeleton } from '../Skeleton/Skeleton';
7
+
8
+ export const TextField = ({
9
+ label,
10
+ value,
11
+ defaultValue,
12
+ placeholder,
13
+ helperText,
14
+ required = false,
15
+ error = false,
16
+ disabled = false,
17
+ fullWidth = true,
18
+ type = 'text',
19
+ name,
20
+ id,
21
+ onChange,
22
+ onBlur,
23
+ onFocus,
24
+ multiline = false,
25
+ rows = 3,
26
+ maxRows,
27
+ minRows,
28
+ autoFocus = false,
29
+ autoComplete,
30
+ size = 'md',
31
+ color = 'white',
32
+ inputProps,
33
+ inputRef: inputRefProp,
34
+ InputProps,
35
+ skeletonLoading = false,
36
+ prefix,
37
+ suffix,
38
+ startIcon,
39
+ endIcon,
40
+ }: TextFieldProps) => {
41
+ const [internalValue, setInternalValue] = useState<string>(
42
+ value !== undefined ? value : (defaultValue ?? '')
43
+ );
44
+ const [isFocused, setIsFocused] = useState(false);
45
+ const internalInputRef = useRef<HTMLInputElement | HTMLTextAreaElement>(null);
46
+
47
+ useEffect(() => {
48
+ if (value !== undefined) {
49
+ setInternalValue(value);
50
+ }
51
+ }, [value]);
52
+
53
+ const inputId =
54
+ id ?? name ?? (label ? String(label).replace(/\s+/g, '-').toLowerCase() : undefined);
55
+ const currentValue = value !== undefined ? value : internalValue;
56
+
57
+ const handleChange = (e: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement>) => {
58
+ const newValue = e.target.value;
59
+ if (value === undefined) {
60
+ setInternalValue(newValue);
61
+ }
62
+ onChange?.(e);
63
+ };
64
+
65
+ const handleFocusInternal = (e: React.FocusEvent<HTMLInputElement | HTMLTextAreaElement>) => {
66
+ setIsFocused(true);
67
+ onFocus?.(e);
68
+ };
69
+
70
+ const handleBlurInternal = (e: React.FocusEvent<HTMLInputElement | HTMLTextAreaElement>) => {
71
+ setIsFocused(false);
72
+ onBlur?.(e);
73
+ };
74
+
75
+ const resolvedTokens = useTokens();
76
+ const tokens = getTextFieldTokensFromResolved(resolvedTokens);
77
+ const inputHeight =
78
+ size === 'sm' ? parseInt(tokens.heightSm, 10)
79
+ : size === 'lg' ? parseInt(tokens.heightLg, 10)
80
+ : parseInt(tokens.heightMd, 10);
81
+ const inputPaddingRaw =
82
+ size === 'sm' ? tokens.paddingSm
83
+ : size === 'md' ? tokens.paddingMd
84
+ : tokens.paddingLg;
85
+
86
+ // Parse a CSS shorthand padding string into explicit sides to avoid
87
+ // shorthand vs longhand conflicts when adding icon offset later.
88
+ const parsePadding = (p: string) => {
89
+ const parts = p.trim().split(/\s+/);
90
+ if (parts.length === 1)
91
+ return {
92
+ top: parts[0],
93
+ right: parts[0],
94
+ bottom: parts[0],
95
+ left: parts[0],
96
+ };
97
+ if (parts.length === 2)
98
+ return {
99
+ top: parts[0],
100
+ right: parts[1],
101
+ bottom: parts[0],
102
+ left: parts[1],
103
+ };
104
+ if (parts.length === 3)
105
+ return {
106
+ top: parts[0],
107
+ right: parts[1],
108
+ bottom: parts[2],
109
+ left: parts[1],
110
+ };
111
+ return { top: parts[0], right: parts[1], bottom: parts[2], left: parts[3] };
112
+ };
113
+ const parsedPadding = parsePadding(inputPaddingRaw);
114
+ const inputFontSize =
115
+ size === 'sm' ? tokens.fontSizeSm
116
+ : size === 'lg' ? tokens.fontSizeLg
117
+ : tokens.fontSizeMd;
118
+
119
+ const labelStyle: React.CSSProperties = {
120
+ display: 'block',
121
+ marginBottom: parseInt(tokens.labelMarginBottom, 10),
122
+ fontSize: tokens.labelFontSize,
123
+ fontWeight: tokens.labelFontWeight,
124
+ color: tokens.labelColor,
125
+ fontFamily: tokens.fontFamily,
126
+ };
127
+
128
+ const resolvedBackgroundColor =
129
+ color === 'white' ? tokens.backgroundColorWhite : tokens.backgroundColorWhite;
130
+ const inputStyle: React.CSSProperties = {
131
+ width: fullWidth ? '100%' : undefined,
132
+ // Use explicit sides (no shorthand) so paddingLeft/paddingRight
133
+ // overrides in adjustedInputStyle work reliably.
134
+ paddingTop: parsedPadding.top,
135
+ paddingRight: parsedPadding.right,
136
+ paddingBottom: parsedPadding.bottom,
137
+ paddingLeft: parsedPadding.left,
138
+ fontSize: inputFontSize,
139
+ color: tokens.inputTextColor,
140
+ fontFamily: tokens.fontFamily,
141
+ backgroundColor: resolvedBackgroundColor,
142
+ border:
143
+ error ? `1px solid ${tokens.outlineError}`
144
+ : isFocused ? `1px solid ${tokens.outlineActive}`
145
+ : `1px solid ${tokens.outlineDefault}`,
146
+ borderRadius:
147
+ size === 'sm' ? tokens.borderRadiusSm
148
+ : size === 'lg' ? tokens.borderRadiusLg
149
+ : tokens.borderRadiusMd,
150
+ outline: 'none',
151
+ boxSizing: 'border-box',
152
+ minHeight: multiline ? undefined : inputHeight,
153
+ opacity: disabled ? parseFloat(tokens.disabledOpacity) : 1,
154
+ cursor: disabled ? 'not-allowed' : undefined,
155
+ lineHeight: 1,
156
+ };
157
+
158
+ const helperStyle: React.CSSProperties = {
159
+ marginTop: 4,
160
+ fontSize: tokens.helperFontSize,
161
+ color: error ? tokens.errorColor : tokens.labelColor,
162
+ fontFamily: tokens.fontFamily,
163
+ };
164
+
165
+ const rowsNum = typeof rows === 'string' ? parseInt(rows, 10) : rows;
166
+ const minRowsNum =
167
+ minRows != null ?
168
+ typeof minRows === 'string' ?
169
+ parseInt(minRows, 10)
170
+ : minRows
171
+ : rowsNum;
172
+ const maxRowsNum =
173
+ maxRows != null ?
174
+ typeof maxRows === 'string' ?
175
+ parseInt(maxRows, 10)
176
+ : maxRows
177
+ : undefined;
178
+ const lineHeight = 20;
179
+
180
+ const setInputRef = (el: HTMLInputElement | HTMLTextAreaElement | null) => {
181
+ (
182
+ internalInputRef as React.MutableRefObject<HTMLInputElement | HTMLTextAreaElement | null>
183
+ ).current = el;
184
+ if (inputRefProp) {
185
+ if (typeof inputRefProp === 'function') {
186
+ inputRefProp(el);
187
+ } else {
188
+ (
189
+ inputRefProp as React.MutableRefObject<HTMLInputElement | HTMLTextAreaElement | null>
190
+ ).current = el;
191
+ }
192
+ }
193
+ };
194
+
195
+ const resolvedStartAdornment = prefix ? undefined : (startIcon ?? InputProps?.startAdornment);
196
+ const resolvedEndAdornment = suffix ? undefined : (endIcon ?? InputProps?.endAdornment);
197
+ const hasPrefix = prefix != null && prefix !== '';
198
+ const hasSuffix = suffix != null && suffix !== '';
199
+ const hasAffix = hasPrefix || hasSuffix;
200
+ const hasAdornment = resolvedStartAdornment || resolvedEndAdornment;
201
+
202
+ const borderRadius =
203
+ size === 'sm' ? tokens.borderRadiusSm
204
+ : size === 'lg' ? tokens.borderRadiusLg
205
+ : tokens.borderRadiusMd;
206
+
207
+ const outlineColor =
208
+ error ? tokens.outlineError
209
+ : isFocused ? tokens.outlineActive
210
+ : tokens.outlineDefault;
211
+
212
+ const inputWrapperStyle: React.CSSProperties = {
213
+ position: 'relative',
214
+ width: fullWidth ? '100%' : undefined,
215
+ display: 'flex',
216
+ alignItems: 'center',
217
+ cursor: disabled ? 'not-allowed' : undefined,
218
+ };
219
+
220
+ const prefixWrapperStyle: React.CSSProperties = {
221
+ display: 'flex',
222
+ alignItems: 'stretch',
223
+ width: fullWidth ? '100%' : undefined,
224
+ minHeight: multiline ? undefined : inputHeight,
225
+ border: `1px solid ${outlineColor}`,
226
+ borderRadius,
227
+ overflow: 'hidden',
228
+ boxSizing: 'border-box',
229
+ backgroundColor: resolvedBackgroundColor,
230
+ opacity: disabled ? parseFloat(tokens.disabledOpacity) : 1,
231
+ cursor: disabled ? 'not-allowed' : undefined,
232
+ };
233
+
234
+ const prefixBoxStyle: React.CSSProperties = {
235
+ display: 'flex',
236
+ alignItems: 'center',
237
+ justifyContent: 'center',
238
+ flexShrink: 0,
239
+ padding: `0 ${parsedPadding.left}`,
240
+ backgroundColor: '#F1F5F9',
241
+ borderRight: `1px solid ${tokens.outlineDefault}`,
242
+ color: '#64748B',
243
+ fontSize: inputFontSize,
244
+ fontWeight: 500,
245
+ fontFamily: tokens.fontFamily,
246
+ pointerEvents: 'none',
247
+ };
248
+
249
+ const suffixBoxStyle: React.CSSProperties = {
250
+ ...prefixBoxStyle,
251
+ borderRight: 'none',
252
+ borderLeft: `1px solid ${tokens.outlineDefault}`,
253
+ };
254
+
255
+ const prefixInputStyle: React.CSSProperties = {
256
+ ...inputStyle,
257
+ flex: 1,
258
+ minWidth: 0,
259
+ border: 'none',
260
+ borderRadius: 0,
261
+ minHeight: multiline ? undefined : inputHeight,
262
+ opacity: 1,
263
+ ...(resolvedEndAdornment && { paddingRight: tokens.iconPaddingLeft }),
264
+ };
265
+
266
+ const adjustedInputStyle: React.CSSProperties = {
267
+ ...inputStyle,
268
+ ...(resolvedEndAdornment && { paddingRight: tokens.iconPaddingLeft }),
269
+ ...(resolvedStartAdornment && { paddingLeft: tokens.iconPaddingLeft }),
270
+ };
271
+
272
+ const startAdornmentStyle: React.CSSProperties = {
273
+ position: 'absolute',
274
+ left: tokens.iconStartLeft,
275
+ top: '50%',
276
+ transform: 'translateY(-50%)',
277
+ display: 'flex',
278
+ alignItems: 'center',
279
+ zIndex: 1,
280
+ pointerEvents: 'none',
281
+ color: tokens.placeholderColor,
282
+ };
283
+
284
+ const endAdornmentStyle: React.CSSProperties = {
285
+ position: 'absolute',
286
+ right: tokens.iconEndRight,
287
+ top: '50%',
288
+ transform: 'translateY(-50%)',
289
+ display: 'flex',
290
+ alignItems: 'center',
291
+ zIndex: 1,
292
+ color: tokens.placeholderColor,
293
+ };
294
+
295
+ return (
296
+ <div style={{ width: fullWidth ? '100%' : undefined }}>
297
+ <style>
298
+ {`.text-field-input::placeholder {
299
+ color: ${tokens.placeholderColor};
300
+ }`}
301
+ </style>
302
+ {label != null && label !== '' && (
303
+ <label htmlFor={inputId} style={labelStyle}>
304
+ {label}
305
+ {required && <span style={{ color: tokens.errorColor, marginRight: 4 }}>*</span>}
306
+ </label>
307
+ )}
308
+ {skeletonLoading ?
309
+ <Skeleton height={inputHeight} borderRadius={8} />
310
+ : hasAffix ?
311
+ <div style={prefixWrapperStyle}>
312
+ {hasPrefix && <div style={prefixBoxStyle}>{prefix}</div>}
313
+ <div style={{ position: 'relative', flex: 1, display: 'flex', alignItems: 'center' }}>
314
+ {multiline ?
315
+ <textarea
316
+ ref={setInputRef as React.Ref<HTMLTextAreaElement>}
317
+ id={inputId}
318
+ name={name}
319
+ required={required}
320
+ value={currentValue}
321
+ placeholder={placeholder}
322
+ onChange={handleChange}
323
+ onBlur={handleBlurInternal}
324
+ onFocus={handleFocusInternal}
325
+ onInvalid={(e) => {
326
+ e.preventDefault();
327
+ }}
328
+ disabled={disabled}
329
+ rows={minRowsNum}
330
+ autoFocus={autoFocus}
331
+ autoComplete={autoComplete}
332
+ aria-invalid={error}
333
+ aria-required={required}
334
+ aria-describedby={helperText ? `${inputId}-helper` : undefined}
335
+ className="text-field-input"
336
+ style={{
337
+ ...prefixInputStyle,
338
+ resize: 'vertical',
339
+ minHeight: minRowsNum * lineHeight,
340
+ ...(maxRowsNum != null && {
341
+ maxHeight: maxRowsNum * lineHeight,
342
+ }),
343
+ width: '100%',
344
+ }}
345
+ {...(inputProps as React.TextareaHTMLAttributes<HTMLTextAreaElement>)}
346
+ />
347
+ : <input
348
+ required={required}
349
+ ref={setInputRef as React.Ref<HTMLInputElement>}
350
+ id={inputId}
351
+ name={name}
352
+ type={type}
353
+ value={currentValue}
354
+ placeholder={placeholder}
355
+ onChange={handleChange}
356
+ onBlur={handleBlurInternal}
357
+ onFocus={handleFocusInternal}
358
+ onInvalid={(e) => {
359
+ e.preventDefault();
360
+ }}
361
+ disabled={disabled}
362
+ autoFocus={autoFocus}
363
+ autoComplete={autoComplete}
364
+ aria-invalid={error}
365
+ aria-required={required}
366
+ aria-describedby={helperText ? `${inputId}-helper` : undefined}
367
+ className="text-field-input"
368
+ style={{ ...prefixInputStyle, width: '100%' }}
369
+ {...(inputProps as React.InputHTMLAttributes<HTMLInputElement>)}
370
+ />
371
+ }
372
+ {resolvedEndAdornment && <div style={endAdornmentStyle}>{resolvedEndAdornment}</div>}
373
+ </div>
374
+ {hasSuffix && <div style={suffixBoxStyle}>{suffix}</div>}
375
+ </div>
376
+ : <>
377
+ <div style={hasAdornment ? inputWrapperStyle : undefined}>
378
+ {resolvedStartAdornment && (
379
+ <div style={startAdornmentStyle}>{resolvedStartAdornment}</div>
380
+ )}
381
+ {multiline ?
382
+ <textarea
383
+ ref={setInputRef as React.Ref<HTMLTextAreaElement>}
384
+ id={inputId}
385
+ name={name}
386
+ required={required}
387
+ value={currentValue}
388
+ placeholder={placeholder}
389
+ onChange={handleChange}
390
+ onBlur={handleBlurInternal}
391
+ onFocus={handleFocusInternal}
392
+ onInvalid={(e) => {
393
+ // Prevent browser-native validation bubble so apps can show custom helper text.
394
+ e.preventDefault();
395
+ }}
396
+ disabled={disabled}
397
+ rows={minRowsNum}
398
+ autoFocus={autoFocus}
399
+ autoComplete={autoComplete}
400
+ aria-invalid={error}
401
+ aria-required={required}
402
+ aria-describedby={helperText ? `${inputId}-helper` : undefined}
403
+ className="text-field-input"
404
+ style={{
405
+ ...adjustedInputStyle,
406
+ resize: 'vertical',
407
+ minHeight: minRowsNum * lineHeight,
408
+ ...(maxRowsNum != null && {
409
+ maxHeight: maxRowsNum * lineHeight,
410
+ }),
411
+ width: hasAdornment ? '100%' : inputStyle.width,
412
+ }}
413
+ {...(inputProps as React.TextareaHTMLAttributes<HTMLTextAreaElement>)}
414
+ />
415
+ : <input
416
+ required={required}
417
+ ref={setInputRef as React.Ref<HTMLInputElement>}
418
+ id={inputId}
419
+ name={name}
420
+ type={type}
421
+ value={currentValue}
422
+ placeholder={placeholder}
423
+ onChange={handleChange}
424
+ onBlur={handleBlurInternal}
425
+ onFocus={handleFocusInternal}
426
+ onInvalid={(e) => {
427
+ // Prevent browser-native validation bubble so apps can show custom helper text.
428
+ e.preventDefault();
429
+ }}
430
+ disabled={disabled}
431
+ autoFocus={autoFocus}
432
+ autoComplete={autoComplete}
433
+ aria-invalid={error}
434
+ aria-required={required}
435
+ aria-describedby={helperText ? `${inputId}-helper` : undefined}
436
+ className="text-field-input"
437
+ style={{
438
+ ...adjustedInputStyle,
439
+ width: hasAdornment ? '100%' : inputStyle.width,
440
+ }}
441
+ {...(inputProps as React.InputHTMLAttributes<HTMLInputElement>)}
442
+ />
443
+ }
444
+ {resolvedEndAdornment && <div style={endAdornmentStyle}>{resolvedEndAdornment}</div>}
445
+ </div>
446
+ </>
447
+ }
448
+
449
+ {helperText != null && helperText !== '' && (
450
+ <div
451
+ id={inputId ? `${inputId}-helper` : undefined}
452
+ style={helperStyle}
453
+ className="d-flex gap-1"
454
+ role={error ? 'alert' : undefined}
455
+ >
456
+ {/* <FirebirdIcon name="info_circle" size={16} /> */}
457
+ <span>{helperText}</span>
458
+ </div>
459
+ )}
460
+ </div>
461
+ );
462
+ };