flet-box 0.0.0-stage → 1.0.2

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 (178) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +190 -2
  3. package/bin/cli.js +394 -0
  4. package/bin/commands/buildAndroid.js +200 -0
  5. package/bin/commands/buildIOS.js +237 -0
  6. package/bin/commands/create.js +406 -0
  7. package/bin/commands/createBundle.js +375 -0
  8. package/bin/commands/createComponent.js +115 -0
  9. package/bin/commands/createScreen.js +161 -0
  10. package/bin/commands/killServer.js +107 -0
  11. package/bin/commands/package.js +131 -0
  12. package/bin/commands/release.js +189 -0
  13. package/bin/commands/runBundle.js +62 -0
  14. package/bin/commands/runCloudflare.js +89 -0
  15. package/bin/commands/runNative.js +109 -0
  16. package/bin/commands/runServer.js +390 -0
  17. package/bin/ota/loader.js +361 -0
  18. package/bin/utils/bundleConfig.js +178 -0
  19. package/bin/utils/capacitor.js +137 -0
  20. package/bin/utils/colors.js +215 -0
  21. package/bin/utils/helpers.js +80 -0
  22. package/bin/utils/spinner.js +96 -0
  23. package/bin/utils/templates.js +1824 -0
  24. package/bin/utils/version.js +108 -0
  25. package/package.json +58 -4
  26. package/src/animations/AnimatedBox.js +607 -0
  27. package/src/animations/AnimatedText.js +105 -0
  28. package/src/animations/MatrixRain.js +220 -0
  29. package/src/animations/ParallaxBox.js +369 -0
  30. package/src/animations/animate.js +332 -0
  31. package/src/animations/index.js +25 -0
  32. package/src/app.js +41 -0
  33. package/src/assets/icon-1024-maskable.png +0 -0
  34. package/src/assets/icon-192-maskable.png +0 -0
  35. package/src/assets/icon-192.png +0 -0
  36. package/src/assets/icon-512-maskable.png +0 -0
  37. package/src/assets/icon-512.png +0 -0
  38. package/src/assets/logo.png +0 -0
  39. package/src/core/App.js +44 -0
  40. package/src/core/hmr-client.js +142 -0
  41. package/src/core/index.js +34 -0
  42. package/src/core/pwa.js +100 -0
  43. package/src/core/runApp.js +430 -0
  44. package/src/create-index.sh +237 -0
  45. package/src/fonts/MaterialIcons-Regular.woff2 +0 -0
  46. package/src/fonts/icons.css +23 -0
  47. package/src/index.d.ts +1930 -0
  48. package/src/index.js +288 -0
  49. package/src/navigations/AdaptiveScaffold.js +149 -0
  50. package/src/navigations/AppBar.js +331 -0
  51. package/src/navigations/BottomNavigation.js +298 -0
  52. package/src/navigations/CollapsibleSideBar.js +139 -0
  53. package/src/navigations/Drawer.js +333 -0
  54. package/src/navigations/DrawerItem.js +260 -0
  55. package/src/navigations/Router.js +475 -0
  56. package/src/navigations/Scaffold.js +537 -0
  57. package/src/navigations/Tabs.js +384 -0
  58. package/src/navigations/index.js +57 -0
  59. package/src/services/RamStore.js +213 -0
  60. package/src/services/Session.js +310 -0
  61. package/src/services/Storage.js +309 -0
  62. package/src/services/http.js +350 -0
  63. package/src/services/index.js +57 -0
  64. package/src/tools/animation.js +42 -0
  65. package/src/tools/array.js +95 -0
  66. package/src/tools/border.js +28 -0
  67. package/src/tools/clipboard.js +104 -0
  68. package/src/tools/color.js +329 -0
  69. package/src/tools/createList.js +215 -0
  70. package/src/tools/delay.js +55 -0
  71. package/src/tools/device.js +138 -0
  72. package/src/tools/dict.js +297 -0
  73. package/src/tools/dimensions.js +118 -0
  74. package/src/tools/filter.js +41 -0
  75. package/src/tools/flex.js +26 -0
  76. package/src/tools/gradient.js +34 -0
  77. package/src/tools/grid.js +57 -0
  78. package/src/tools/index.js +38 -0
  79. package/src/tools/mapList.js +107 -0
  80. package/src/tools/margin.js +93 -0
  81. package/src/tools/memo.js +60 -0
  82. package/src/tools/os.js +104 -0
  83. package/src/tools/padding.js +93 -0
  84. package/src/tools/print.js +69 -0
  85. package/src/tools/random.js +299 -0
  86. package/src/tools/ref.js +70 -0
  87. package/src/tools/rgba.js +4 -0
  88. package/src/tools/shadow.js +23 -0
  89. package/src/tools/string.js +85 -0
  90. package/src/tools/time.js +674 -0
  91. package/src/tools/transform.js +68 -0
  92. package/src/tools/transition.js +48 -0
  93. package/src/tools/useState.js +268 -0
  94. package/src/tools/uuid.js +42 -0
  95. package/src/utils/TextInputValidator.js +256 -0
  96. package/src/utils/composeUpdate.js +65 -0
  97. package/src/utils/getWidgetProps.js +35 -0
  98. package/src/utils/index.js +76 -0
  99. package/src/utils/markdownParser.js +368 -0
  100. package/src/utils/mediaTime.js +79 -0
  101. package/src/utils/navigation.js +263 -0
  102. package/src/utils/scriptInjector.js +73 -0
  103. package/src/utils/stackPosition.js +31 -0
  104. package/src/utils/stopWebRefresh.js +77 -0
  105. package/src/utils/styleInjector.js +38 -0
  106. package/src/utils/syntaxHighlight.js +339 -0
  107. package/src/utils/themes.js +347 -0
  108. package/src/utils/units.js +87 -0
  109. package/src/utils/visualEffects.js +169 -0
  110. package/src/widget-factory/addChildren.js +114 -0
  111. package/src/widget-factory/assignProps.js +256 -0
  112. package/src/widget-factory/createWidget.js +28 -0
  113. package/src/widget-factory/effects.js +132 -0
  114. package/src/widget-factory/index.js +7 -0
  115. package/src/widget-factory/lifecycle.js +210 -0
  116. package/src/widget-factory/makeParentable.js +89 -0
  117. package/src/widget-factory/pixTool.js +40 -0
  118. package/src/widget-factory/processProps.js +122 -0
  119. package/src/widget-factory/reactivity.js +98 -0
  120. package/src/widget-factory/remTool.js +34 -0
  121. package/src/widget-factory/stackPosition.js +87 -0
  122. package/src/widget-factory/stringTool.js +26 -0
  123. package/src/widget-factory/tools.js +249 -0
  124. package/src/widget-factory/translateProps.js +1049 -0
  125. package/src/widget-factory/warnings.js +284 -0
  126. package/src/widget-factory/widgetFactory.js +200 -0
  127. package/src/widgets/Accordion.js +562 -0
  128. package/src/widgets/AdMob.js +315 -0
  129. package/src/widgets/AdSense.js +268 -0
  130. package/src/widgets/AlertDialog.js +162 -0
  131. package/src/widgets/Audio.js +311 -0
  132. package/src/widgets/Avatar.js +210 -0
  133. package/src/widgets/Badge.js +189 -0
  134. package/src/widgets/BottomSheet.js +287 -0
  135. package/src/widgets/Button.js +247 -0
  136. package/src/widgets/Card.js +33 -0
  137. package/src/widgets/Carousel.js +301 -0
  138. package/src/widgets/Chart.js +632 -0
  139. package/src/widgets/Checkbox.js +106 -0
  140. package/src/widgets/Chip.js +153 -0
  141. package/src/widgets/CircularBar.js +352 -0
  142. package/src/widgets/CircularChart.js +549 -0
  143. package/src/widgets/CodeViewer.js +236 -0
  144. package/src/widgets/Column.js +29 -0
  145. package/src/widgets/Container.js +31 -0
  146. package/src/widgets/DataTable.js +257 -0
  147. package/src/widgets/Divider.js +93 -0
  148. package/src/widgets/DraggBox.js +115 -0
  149. package/src/widgets/Dropdown.js +505 -0
  150. package/src/widgets/DroppBox.js +180 -0
  151. package/src/widgets/FloatingActionButton.js +91 -0
  152. package/src/widgets/GridView.js +20 -0
  153. package/src/widgets/Icon.js +33 -0
  154. package/src/widgets/Image.js +15 -0
  155. package/src/widgets/Input.js +565 -0
  156. package/src/widgets/Inspector.js +254 -0
  157. package/src/widgets/InstallButton.js +110 -0
  158. package/src/widgets/ListTile.js +224 -0
  159. package/src/widgets/ListView.js +503 -0
  160. package/src/widgets/Markdown.js +1236 -0
  161. package/src/widgets/Modal.js +496 -0
  162. package/src/widgets/Pagination.js +195 -0
  163. package/src/widgets/ProgressBar.js +178 -0
  164. package/src/widgets/QRCode.js +467 -0
  165. package/src/widgets/Radio.js +132 -0
  166. package/src/widgets/Rating.js +261 -0
  167. package/src/widgets/Row.js +29 -0
  168. package/src/widgets/Skeleton.js +243 -0
  169. package/src/widgets/Slider.js +527 -0
  170. package/src/widgets/SnackBar.js +227 -0
  171. package/src/widgets/Stack.js +29 -0
  172. package/src/widgets/Stepper.js +296 -0
  173. package/src/widgets/Switch.js +102 -0
  174. package/src/widgets/Text.js +98 -0
  175. package/src/widgets/Tooltip.js +267 -0
  176. package/src/widgets/TreeView.js +370 -0
  177. package/src/widgets/Video.js +92 -0
  178. package/src/widgets/index.js +52 -0
@@ -0,0 +1,565 @@
1
+ /**
2
+ * @file Input.js
3
+ * @description Text input with label, validation and controlled/uncontrolled value.
4
+ */
5
+ import { WidgetFactory } from "../widget-factory/index.js";
6
+ import { colors } from "../utils/themes.js";
7
+ import { Icon } from "./Icon.js";
8
+ import { TextInputValidator } from "../utils/TextInputValidator.js";
9
+
10
+ /**
11
+ * @typedef {Object} InputProps
12
+ * @property {string} [value=""] - Initial value of the input field.
13
+ * @property {string} [placeholder=""] - Placeholder text shown when the field is empty.
14
+ * @property {string} [type="text"] - HTML input type (e.g. "text", "password", "email").
15
+ * @property {string} [label] - Label text rendered above the input.
16
+ * @property {boolean|string} [error=false] - If truthy, renders the input in error state. A string value is used as the error message.
17
+ * @property {boolean} [disabled=false] - Disables interaction with the input.
18
+ * @property {boolean} [readonly=false] - Makes the input read-only (focusable but not editable).
19
+ * @property {"small"|"medium"|"large"} [size="medium"] - Controls padding and font size.
20
+ * @property {"outlined"|"filled"|"underlined"} [variant="outlined"] - Visual style of the input border.
21
+ * @property {boolean} [fullWidth=false] - When true, the input stretches to fill its container.
22
+ * @property {number} [borderRadius=24] - Border radius of the input wrapper in pixels.
23
+ * @property {string} [iconLeft] - Material icon name to show on the left side.
24
+ * @property {string} [iconRight] - Material icon name to show on the right side.
25
+ * @property {string} [iconColor] - Color of the icons.
26
+ * @property {number} [iconSize=20] - Size of the icons in pixels.
27
+ * @property {Function} [onIconPress] - Called when either icon is pressed.
28
+ * @property {"none"|"letters"|"numbers"|"email"|"alphanumeric"|"safe"|"custom"} [validation="none"] - Validation mode applied on each keystroke and on blur.
29
+ * @property {number} [maxLength] - Maximum number of characters allowed.
30
+ * @property {boolean} [required=false] - Marks the field as required; validation fails on empty values.
31
+ * @property {string} [customPattern] - RegExp string used when validation is "custom".
32
+ * @property {Function} [onValidated] - Called with (isValid, message) after every validation run.
33
+ * @property {boolean} [showValidationMessage=true] - Whether to display the inline validation message below the input.
34
+ * @property {boolean} [showValidationIcon=true] - Whether to show the ✓/✗ icon inside the input.
35
+ * @property {Function} [onInput] - Called with (value, event) on every keystroke.
36
+ * @property {Function} [onChange] - Called with (value, event) after filtering/validation.
37
+ * @property {Function} [onFocus] - Called when the input receives focus.
38
+ * @property {Function} [onBlur] - Called when the input loses focus and final validation runs.
39
+ */
40
+
41
+ /**
42
+ * A fully-featured text input widget with built-in validation, icons, and theme support.
43
+ *
44
+ * Supports multiple visual variants (outlined, filled, underlined), configurable sizes,
45
+ * optional left/right icons, and a rich validation system driven by `TextInputValidator`.
46
+ * The public API exposed on the returned element allows programmatic control of the value,
47
+ * validation state, focus, and blur.
48
+ *
49
+ * @param {InputProps} props
50
+ * @returns {HTMLElement} A `<div>` acting as the input container, augmented with public methods:
51
+ * - `getValue()` → current string value
52
+ * - `setValue(newValue)` → updates value and reruns validation
53
+ * - `isValid()` → returns whether the current value passes validation
54
+ * - `validate()` → runs validation and updates the UI, returns `{ valid, message }`
55
+ * - `reset()` → clears the value
56
+ * - `focus()` / `blur()` → delegate to the native `<input>`
57
+ * - `_input` → reference to the underlying native `<input>` element
58
+ */
59
+ export const Input = (props) => {
60
+ // ========== PROPS ==========
61
+ const {
62
+ value = "",
63
+ placeholder = "",
64
+ type = "text",
65
+ label,
66
+ error = false,
67
+ disabled = false,
68
+ readonly = false,
69
+ size = "medium",
70
+ variant = "outlined",
71
+ fullWidth = false,
72
+ borderRadius = 24,
73
+
74
+ iconLeft,
75
+ iconRight,
76
+ iconColor = colors.textSecondary,
77
+ iconSize = 20,
78
+ onIconPress,
79
+
80
+ validation = "none",
81
+ maxLength,
82
+ required = false,
83
+ customPattern,
84
+ onValidated,
85
+ showValidationMessage = true,
86
+ showValidationIcon = true,
87
+
88
+ onInput,
89
+ onChange,
90
+ onFocus,
91
+ onBlur,
92
+ ...rest
93
+ } = props;
94
+
95
+ // ========== INTERNAL STATE ==========
96
+ let currentValue = value;
97
+ let isValid = true;
98
+ let validationMessage = "";
99
+
100
+ // DOM element references — populated during the build phase below
101
+ let inputElement = null;
102
+ let validationIcon = null;
103
+ let errorMessageElement = null;
104
+ let inputWrapper = null;
105
+ let container = null;
106
+
107
+ // Whether the native input currently holds focus. Used to keep the focus
108
+ // ring (a box-shadow) in sync with validation colors without ever changing
109
+ // the border width — changing the width would resize the box and shift layout.
110
+ const isFocused = () => document.activeElement === inputElement;
111
+
112
+ // ========== SIZES ==========
113
+ // Maps the `size` prop to concrete CSS values so the rest of the code stays size-agnostic.
114
+ const sizes = {
115
+ small: { padding: "6px 12px", fontSize: "12px", iconSize: 16 },
116
+ medium: { padding: "10px 14px", fontSize: "14px", iconSize: 20 },
117
+ large: { padding: "14px 18px", fontSize: "16px", iconSize: 24 },
118
+ };
119
+ const sz = sizes[size] || sizes.medium;
120
+
121
+ // ========== VALIDATION ==========
122
+
123
+ /**
124
+ * Validates a value against the current validation rules.
125
+ * Does NOT mutate state — just returns the result so it can be used
126
+ * both inline (on input) and on blur.
127
+ *
128
+ * @param {string} val - The value to validate.
129
+ * @returns {{ valid: boolean, message: string }}
130
+ */
131
+ const validateValue = (val) => {
132
+ let valid = true;
133
+ let message = "";
134
+
135
+ if (required && !val) {
136
+ valid = false;
137
+ message = "This field is required";
138
+ } else if (val && valid) {
139
+ switch (validation) {
140
+ case "letters":
141
+ if (!TextInputValidator.isOnlyLetters(val)) {
142
+ valid = false;
143
+ message = "Only letters allowed";
144
+ }
145
+ break;
146
+ case "numbers":
147
+ if (!TextInputValidator.isOnlyNumbers(val)) {
148
+ valid = false;
149
+ message = "Only numbers allowed";
150
+ }
151
+ break;
152
+ case "email":
153
+ if (!TextInputValidator.isEmail(val)) {
154
+ valid = false;
155
+ message = "Invalid email address";
156
+ }
157
+ break;
158
+ case "alphanumeric":
159
+ if (!/^[a-zA-Z0-9\s]+$/.test(val)) {
160
+ valid = false;
161
+ message = "Only letters and numbers allowed";
162
+ }
163
+ break;
164
+ case "safe":
165
+ if (!TextInputValidator.isSafe(val)) {
166
+ valid = false;
167
+ message = "Contains unsafe characters";
168
+ }
169
+ break;
170
+ case "custom":
171
+ if (customPattern && !new RegExp(customPattern).test(val)) {
172
+ valid = false;
173
+ message = "Invalid format";
174
+ }
175
+ break;
176
+ }
177
+ }
178
+ return { valid, message };
179
+ };
180
+
181
+ /**
182
+ * Strips disallowed characters from a raw input value based on the active
183
+ * validation mode. This is a live filter applied on every keystroke so the
184
+ * user cannot type invalid characters in the first place.
185
+ *
186
+ * @param {string} val - Raw value from the input event.
187
+ * @returns {string} Filtered value safe to store and display.
188
+ */
189
+ const filterValue = (val) => {
190
+ if (!val) return val;
191
+ switch (validation) {
192
+ case "letters":
193
+ return TextInputValidator.onlyLetters(val);
194
+ case "numbers":
195
+ return TextInputValidator.onlyNumbers(val);
196
+ case "email":
197
+ return TextInputValidator.filterEmail(val);
198
+ case "alphanumeric":
199
+ return TextInputValidator.onlyAlphanumeric(val);
200
+ case "safe":
201
+ return TextInputValidator.safeText(val);
202
+ default:
203
+ return val;
204
+ }
205
+ };
206
+
207
+ // ========== UPDATE VALIDATION UI ==========
208
+
209
+ /**
210
+ * Syncs the visible validation state (border color, icon, error message) with
211
+ * the current `isValid` / `validationMessage` / `error` prop values.
212
+ * Should be called after every state change that could affect the UI.
213
+ *
214
+ * @param {boolean} valid - Whether the current value passes validation.
215
+ */
216
+ const updateValidationUI = (valid) => {
217
+ if (!inputWrapper) return;
218
+
219
+ // Border color logic:
220
+ // - error prop takes precedence over internal validation
221
+ // - green border when valid and a non-trivial validation mode is active
222
+ let borderColorValue = colors.border;
223
+ if (error) {
224
+ borderColorValue = colors.danger;
225
+ } else if (!valid && currentValue) {
226
+ borderColorValue = colors.danger;
227
+ } else if (currentValue && valid && validation !== "none") {
228
+ borderColorValue = colors.success;
229
+ }
230
+
231
+ // The border keeps a fixed 1px width in every state; focus thickness is
232
+ // drawn as a 1px box-shadow ring instead. This changes the look on focus
233
+ // without altering the element's box size, so nothing gets pushed around.
234
+ if (variant === "outlined") {
235
+ inputWrapper.style.borderColor = borderColorValue;
236
+ inputWrapper.style.boxShadow = isFocused()
237
+ ? `0 0 0 1px ${borderColorValue}`
238
+ : "none";
239
+ } else if (variant === "underlined") {
240
+ inputWrapper.style.borderBottomColor = borderColorValue;
241
+ inputWrapper.style.boxShadow = isFocused()
242
+ ? `inset 0 -1px 0 0 ${borderColorValue}`
243
+ : "none";
244
+ }
245
+
246
+ if (showValidationIcon && validationIcon) {
247
+ if (currentValue && validation !== "none") {
248
+ validationIcon.textContent = isValid ? "✓" : "✗";
249
+ validationIcon.style.color = isValid ? colors.success : colors.danger;
250
+ validationIcon.style.display = "flex";
251
+ } else {
252
+ validationIcon.style.display = "none";
253
+ }
254
+ }
255
+
256
+ if (errorMessageElement) {
257
+ if (validationMessage && showValidationMessage && !isValid && !error) {
258
+ errorMessageElement.textContent = validationMessage;
259
+ errorMessageElement.style.color = colors.danger;
260
+ errorMessageElement.style.display = "block";
261
+ } else if (error) {
262
+ errorMessageElement.textContent =
263
+ error === true ? "Invalid value" : error;
264
+ errorMessageElement.style.color = colors.danger;
265
+ errorMessageElement.style.display = "block";
266
+ } else {
267
+ errorMessageElement.style.display = "none";
268
+ }
269
+ }
270
+ };
271
+
272
+ /**
273
+ * Central handler for every input event. Filters the raw value, enforces
274
+ * `maxLength`, runs validation, updates the UI, and fires all relevant
275
+ * callbacks in the correct order.
276
+ *
277
+ * @param {string} val - The raw value from the native input event.
278
+ * @param {Event} event - The originating DOM event (passed through to callbacks).
279
+ */
280
+ const handleInput = (val, event) => {
281
+ let filtered = filterValue(val);
282
+ if (maxLength) filtered = filtered.slice(0, maxLength);
283
+
284
+ currentValue = filtered;
285
+ if (inputElement) {
286
+ inputElement.value = filtered;
287
+ }
288
+
289
+ const result = validateValue(filtered);
290
+ isValid = result.valid;
291
+ validationMessage = result.message;
292
+
293
+ updateValidationUI(isValid);
294
+
295
+ if (onInput) onInput(filtered, event);
296
+ if (onValidated) onValidated(isValid, validationMessage);
297
+ if (onChange) onChange(filtered, event);
298
+ };
299
+
300
+ // ========== BUILD UI ==========
301
+
302
+ // --- Main container — uses WidgetFactory so it inherits layout helpers ---
303
+ container = WidgetFactory({
304
+ tag: "div",
305
+ widgetName: "Input",
306
+ display: "inline-flex",
307
+ flexDirection: "column",
308
+ gap: 4,
309
+ width: fullWidth ? "100%" : "auto",
310
+ disableTransform: true,
311
+ pointerEvents: "auto",
312
+ touchAction: "manipulation",
313
+ ...rest,
314
+ });
315
+
316
+ // --- Optional label rendered above the input ---
317
+ if (label) {
318
+ const requiredMark = required ? " *" : "";
319
+ const labelEl = WidgetFactory({
320
+ tag: "label",
321
+ text: label + requiredMark,
322
+ fontSize: 12,
323
+ fontWeight: 500,
324
+ color: error ? colors.danger : colors.textSecondary,
325
+ marginBottom: 2,
326
+ cursor: "text",
327
+ });
328
+ // Clicking the label focuses the native input — mirrors native <label for> behavior
329
+ labelEl.addEventListener("click", () => {
330
+ inputElement?.focus();
331
+ });
332
+ container.appendChild(labelEl);
333
+ }
334
+
335
+ // --- Wrapper — contains icons + native input, owns the border/fill style ---
336
+ let wrapperStyles = {
337
+ display: "flex",
338
+ alignItems: "center",
339
+ width: "100%",
340
+ transition: "all 0.2s ease",
341
+ pointerEvents: "auto",
342
+ touchAction: "manipulation",
343
+ };
344
+
345
+ if (variant === "filled") {
346
+ wrapperStyles.backgroundColor = colors.gray100;
347
+ wrapperStyles.borderRadius =
348
+ typeof borderRadius === "number" ? `${borderRadius}px` : borderRadius;
349
+ wrapperStyles.border = "none";
350
+ } else if (variant === "underlined") {
351
+ wrapperStyles.backgroundColor = "transparent";
352
+ wrapperStyles.borderBottom = `1px solid ${error ? colors.danger : colors.border}`;
353
+ wrapperStyles.borderRadius = "0";
354
+ } else {
355
+ // Default: outlined
356
+ wrapperStyles.backgroundColor = "transparent";
357
+ wrapperStyles.borderRadius =
358
+ typeof borderRadius === "number" ? `${borderRadius}px` : borderRadius;
359
+ wrapperStyles.border = `1px solid ${error ? colors.danger : colors.border}`;
360
+ }
361
+
362
+ inputWrapper = WidgetFactory({
363
+ tag: "div",
364
+ disableTransform: true,
365
+ ...wrapperStyles,
366
+ });
367
+
368
+ // --- Optional left icon ---
369
+ if (iconLeft) {
370
+ const iconEl = Icon({
371
+ name: iconLeft,
372
+ size: iconSize || sz.iconSize,
373
+ color: iconColor,
374
+ marginLeft: "12px",
375
+ flexShrink: 0,
376
+ pointerEvents: "auto",
377
+ });
378
+ if (onIconPress) iconEl.onclick = onIconPress;
379
+ inputWrapper.appendChild(iconEl);
380
+ }
381
+
382
+ // --- Native <input> element ---
383
+ // Using a real <input> (not a div) is critical for:
384
+ // - Mobile keyboard triggering
385
+ // - Accessibility (label association, screen readers)
386
+ // - Browser autofill support
387
+ inputElement = document.createElement("input");
388
+ inputElement.type = type;
389
+ inputElement.placeholder = placeholder || "";
390
+ inputElement.disabled = disabled || false;
391
+ inputElement.readOnly = readonly || false;
392
+ inputElement.value = currentValue;
393
+
394
+ inputElement.style.flex = "1";
395
+ inputElement.style.padding = sz.padding;
396
+ inputElement.style.fontSize = sz.fontSize;
397
+ inputElement.style.border = "none";
398
+ inputElement.style.outline = "none";
399
+ inputElement.style.backgroundColor = "transparent";
400
+ inputElement.style.fontFamily = "inherit";
401
+ inputElement.style.width = "100%";
402
+ inputElement.style.boxSizing = "border-box";
403
+ inputElement.style.touchAction = "manipulation";
404
+ inputElement.style.userSelect = "text";
405
+ inputElement.style.color = disabled ? colors.textDisabled : colors.text;
406
+ inputElement.style.cursor = disabled ? "not-allowed" : "text";
407
+
408
+ // tabIndex ensures the element participates in keyboard navigation
409
+ inputElement.tabIndex = 0;
410
+
411
+ // --- Input events ---
412
+ inputElement.addEventListener("input", (e) => {
413
+ handleInput(e.target.value, e);
414
+ });
415
+
416
+ inputElement.addEventListener("focus", (e) => {
417
+ // Highlight the border on focus to signal the active field.
418
+ // The border width stays at 1px; the thicker focus look comes from a
419
+ // 1px box-shadow ring so focusing never resizes the box or shifts layout.
420
+ if (variant === "outlined") {
421
+ inputWrapper.style.borderColor = colors.primary;
422
+ inputWrapper.style.boxShadow = `0 0 0 1px ${colors.primary}`;
423
+ } else if (variant === "underlined") {
424
+ inputWrapper.style.borderBottomColor = colors.primary;
425
+ inputWrapper.style.boxShadow = `inset 0 -1px 0 0 ${colors.primary}`;
426
+ }
427
+ if (onFocus) onFocus(e);
428
+ });
429
+
430
+ inputElement.addEventListener("blur", (e) => {
431
+ // Restore normal border and run final validation when the user leaves the field
432
+ if (variant === "outlined") {
433
+ inputWrapper.style.borderColor = error ? colors.danger : colors.border;
434
+ inputWrapper.style.boxShadow = "none";
435
+ } else if (variant === "underlined") {
436
+ inputWrapper.style.borderBottomColor = error
437
+ ? colors.danger
438
+ : colors.border;
439
+ inputWrapper.style.boxShadow = "none";
440
+ }
441
+ const result = validateValue(currentValue);
442
+ isValid = result.valid;
443
+ validationMessage = result.message;
444
+ updateValidationUI(isValid);
445
+ if (onBlur) onBlur(e);
446
+ });
447
+
448
+ inputWrapper.appendChild(inputElement);
449
+
450
+ // --- Inline validation icon (✓ / ✗) shown inside the input on the right ---
451
+ if (showValidationIcon && validation !== "none") {
452
+ validationIcon = document.createElement("span");
453
+ validationIcon.textContent = "";
454
+ validationIcon.style.width = "24px";
455
+ validationIcon.style.fontSize = "16px";
456
+ validationIcon.style.display = "none";
457
+ validationIcon.style.alignItems = "center";
458
+ validationIcon.style.justifyContent = "center";
459
+ validationIcon.style.marginRight = "8px";
460
+ validationIcon.style.flexShrink = "0";
461
+ inputWrapper.appendChild(validationIcon);
462
+ }
463
+
464
+ // --- Optional right icon ---
465
+ if (iconRight) {
466
+ const iconEl = Icon({
467
+ name: iconRight,
468
+ size: iconSize || sz.iconSize,
469
+ color: iconColor,
470
+ marginRight: "12px",
471
+ flexShrink: 0,
472
+ pointerEvents: "auto",
473
+ });
474
+ if (onIconPress) iconEl.onclick = onIconPress;
475
+ inputWrapper.appendChild(iconEl);
476
+ }
477
+
478
+ container.appendChild(inputWrapper);
479
+
480
+ // --- Error message element rendered below the input ---
481
+ errorMessageElement = document.createElement("span");
482
+ errorMessageElement.textContent = "";
483
+ errorMessageElement.style.fontSize = "11px";
484
+ errorMessageElement.style.color = colors.danger;
485
+ errorMessageElement.style.marginTop = "2px";
486
+ errorMessageElement.style.display = "none";
487
+ container.appendChild(errorMessageElement);
488
+
489
+ // ========== FORCE FOCUS (mobile and desktop) ==========
490
+ // Clicking anywhere in the wrapper (not just on the <input>) should focus it.
491
+ // This matters most on mobile where touch targets need to be larger.
492
+ inputWrapper.addEventListener("click", (e) => {
493
+ if (e.target !== inputElement) {
494
+ inputElement.focus();
495
+ }
496
+ });
497
+
498
+ inputWrapper.addEventListener(
499
+ "touchstart",
500
+ (e) => {
501
+ if (e.target !== inputElement) {
502
+ inputElement.focus();
503
+ }
504
+ },
505
+ { passive: true },
506
+ );
507
+
508
+ container.addEventListener("click", (e) => {
509
+ if (e.target === container || e.target === label) {
510
+ inputElement.focus();
511
+ }
512
+ });
513
+
514
+ // ========== PUBLIC API ==========
515
+ // These methods are attached directly to the container DOM node so callers
516
+ // can interact with the input without holding a separate reference.
517
+
518
+ /** @returns {string} The current value of the input. */
519
+ container.getValue = () => inputElement?.value || "";
520
+
521
+ /**
522
+ * Programmatically sets the input value and reruns validation.
523
+ * @param {string} newValue - The value to set.
524
+ */
525
+ container.setValue = (newValue) => {
526
+ currentValue = newValue;
527
+ if (inputElement) {
528
+ inputElement.value = newValue;
529
+ }
530
+ const result = validateValue(newValue);
531
+ isValid = result.valid;
532
+ validationMessage = result.message;
533
+ updateValidationUI(isValid);
534
+ };
535
+
536
+ /** @returns {boolean} Whether the current value passes all validation rules. */
537
+ container.isValid = () => validateValue(currentValue).valid;
538
+
539
+ /**
540
+ * Runs validation against the current value and updates the UI.
541
+ * Useful for triggering validation on form submit without waiting for blur.
542
+ * @returns {{ valid: boolean, message: string }}
543
+ */
544
+ container.validate = () => {
545
+ const result = validateValue(currentValue);
546
+ updateValidationUI(result.valid);
547
+ return result;
548
+ };
549
+
550
+ /** Clears the input value (equivalent to `setValue("")`). */
551
+ container.reset = () => container.setValue("");
552
+
553
+ /** Focuses the native input element. */
554
+ container.focus = () => inputElement?.focus();
555
+
556
+ /** Removes focus from the native input element. */
557
+ container.blur = () => inputElement?.blur();
558
+
559
+ // Direct reference to the native <input> for advanced use cases
560
+ container._input = inputElement;
561
+
562
+ return container;
563
+ };
564
+
565
+ export default Input;