@rovula/ui 0.0.76 → 0.0.78

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 (56) hide show
  1. package/dist/cjs/bundle.css +40 -0
  2. package/dist/cjs/bundle.js +3 -3
  3. package/dist/cjs/bundle.js.map +1 -1
  4. package/dist/cjs/types/components/Dropdown/Dropdown.stories.d.ts +7 -0
  5. package/dist/cjs/types/components/InputFilter/InputFilter.stories.d.ts +7 -0
  6. package/dist/cjs/types/components/MaskedTextInput/MaskedTextInput.d.ts +75 -0
  7. package/dist/cjs/types/components/MaskedTextInput/MaskedTextInput.stories.d.ts +491 -0
  8. package/dist/cjs/types/components/MaskedTextInput/index.d.ts +3 -0
  9. package/dist/cjs/types/components/NumberInput/NumberInput.d.ts +39 -0
  10. package/dist/cjs/types/components/NumberInput/NumberInput.stories.d.ts +18 -0
  11. package/dist/cjs/types/components/NumberInput/index.d.ts +2 -0
  12. package/dist/cjs/types/components/RadioGroup/RadioGroup.stories.d.ts +1 -1
  13. package/dist/cjs/types/components/Search/Search.stories.d.ts +7 -0
  14. package/dist/cjs/types/components/Slider/Slider.stories.d.ts +1 -1
  15. package/dist/cjs/types/components/TextInput/TextInput.d.ts +14 -0
  16. package/dist/cjs/types/components/TextInput/TextInput.stories.d.ts +14 -0
  17. package/dist/cjs/types/index.d.ts +4 -0
  18. package/dist/components/MaskedTextInput/MaskedTextInput.js +267 -0
  19. package/dist/components/MaskedTextInput/MaskedTextInput.stories.js +167 -0
  20. package/dist/components/MaskedTextInput/index.js +2 -0
  21. package/dist/components/NumberInput/NumberInput.js +254 -0
  22. package/dist/components/NumberInput/NumberInput.stories.js +212 -0
  23. package/dist/components/NumberInput/index.js +1 -0
  24. package/dist/components/TextInput/TextInput.js +13 -11
  25. package/dist/components/Toast/Toast.styles.js +1 -1
  26. package/dist/esm/bundle.css +40 -0
  27. package/dist/esm/bundle.js +3 -3
  28. package/dist/esm/bundle.js.map +1 -1
  29. package/dist/esm/types/components/Dropdown/Dropdown.stories.d.ts +7 -0
  30. package/dist/esm/types/components/InputFilter/InputFilter.stories.d.ts +7 -0
  31. package/dist/esm/types/components/MaskedTextInput/MaskedTextInput.d.ts +75 -0
  32. package/dist/esm/types/components/MaskedTextInput/MaskedTextInput.stories.d.ts +491 -0
  33. package/dist/esm/types/components/MaskedTextInput/index.d.ts +3 -0
  34. package/dist/esm/types/components/NumberInput/NumberInput.d.ts +39 -0
  35. package/dist/esm/types/components/NumberInput/NumberInput.stories.d.ts +18 -0
  36. package/dist/esm/types/components/NumberInput/index.d.ts +2 -0
  37. package/dist/esm/types/components/RadioGroup/RadioGroup.stories.d.ts +1 -1
  38. package/dist/esm/types/components/Search/Search.stories.d.ts +7 -0
  39. package/dist/esm/types/components/Slider/Slider.stories.d.ts +1 -1
  40. package/dist/esm/types/components/TextInput/TextInput.d.ts +14 -0
  41. package/dist/esm/types/components/TextInput/TextInput.stories.d.ts +14 -0
  42. package/dist/esm/types/index.d.ts +4 -0
  43. package/dist/index.d.ts +110 -1
  44. package/dist/index.js +2 -0
  45. package/dist/src/theme/global.css +51 -0
  46. package/package.json +1 -1
  47. package/src/components/MaskedTextInput/MaskedTextInput.stories.tsx +414 -0
  48. package/src/components/MaskedTextInput/MaskedTextInput.tsx +391 -0
  49. package/src/components/MaskedTextInput/README.md +202 -0
  50. package/src/components/MaskedTextInput/index.ts +3 -0
  51. package/src/components/NumberInput/NumberInput.stories.tsx +350 -0
  52. package/src/components/NumberInput/NumberInput.tsx +428 -0
  53. package/src/components/NumberInput/index.ts +2 -0
  54. package/src/components/TextInput/TextInput.tsx +54 -12
  55. package/src/components/Toast/Toast.styles.tsx +1 -1
  56. package/src/index.ts +7 -0
@@ -0,0 +1,391 @@
1
+ import React, {
2
+ FC,
3
+ forwardRef,
4
+ useCallback,
5
+ useImperativeHandle,
6
+ useRef,
7
+ useState,
8
+ useEffect,
9
+ } from "react";
10
+ import TextInput, { InputProps } from "../TextInput/TextInput";
11
+ import { cn } from "@/utils/cn";
12
+
13
+ export type MaskRule = {
14
+ pattern: RegExp;
15
+ placeholder: string;
16
+ isLiteral?: boolean;
17
+ validator?: (char: string) => boolean;
18
+ };
19
+
20
+ export type MaskedTextInputProps = InputProps & {
21
+ mask?: string;
22
+ maskChar?: string;
23
+ showMask?: boolean;
24
+ guide?: boolean;
25
+ keepCharPositions?: boolean;
26
+ rules?: Record<string, RegExp | ((char: string) => boolean)>;
27
+ onMaskedChange?: (value: string, rawValue: string) => void;
28
+ };
29
+
30
+ // Kendo UI style mask patterns
31
+ export const MASK_PATTERNS = {
32
+ PHONE: "(000) 000-0000",
33
+ PHONE_INTL: "+000 000 000 0000",
34
+ CREDIT_CARD: "0000 0000 0000 0000",
35
+ DATE: "00/00/0000",
36
+ TIME: "00:00",
37
+ SSN: "000-00-0000",
38
+ ZIP_CODE: "00000",
39
+ ZIP_CODE_EXT: "00000-0000",
40
+ CURRENCY: "$000,000.00",
41
+ PERCENTAGE: "000%",
42
+ LICENSE_PLATE: "AAA-0000",
43
+ PRODUCT_CODE: "AA-0000-AA",
44
+ ALPHANUMERIC: "AAAA-0000",
45
+ } as const;
46
+
47
+ // Kendo UI mask rules
48
+ const KENDO_RULES: Record<string, { pattern: RegExp; placeholder: string }> = {
49
+ "0": { pattern: /[0-9]/, placeholder: "0" }, // Any digit 0-9
50
+ "9": { pattern: /[0-9\s]/, placeholder: "9" }, // Any digit 0-9 or space
51
+ "#": { pattern: /[0-9\s+\-]/, placeholder: "#" }, // Any digit, space, +, or -
52
+ L: { pattern: /[a-zA-Z]/, placeholder: "L" }, // Any letter
53
+ "?": { pattern: /[a-zA-Z\s]/, placeholder: "?" }, // Any letter or space
54
+ "&": { pattern: /[^\s]/, placeholder: "&" }, // Any character except space
55
+ C: { pattern: /./, placeholder: "C" }, // Any character including space
56
+ A: { pattern: /[a-zA-Z0-9]/, placeholder: "A" }, // Any alphanumeric
57
+ a: { pattern: /[a-zA-Z0-9\s]/, placeholder: "a" }, // Any alphanumeric or space
58
+ };
59
+
60
+ // Helper function to create mask pattern from string using Kendo UI rules
61
+ const createMaskPattern = (
62
+ mask: string,
63
+ customRules?: Record<string, RegExp | ((char: string) => boolean)>
64
+ ): MaskRule[] => {
65
+ const rules = { ...KENDO_RULES, ...customRules };
66
+ const pattern: MaskRule[] = [];
67
+ let i = 0;
68
+
69
+ while (i < mask.length) {
70
+ const char = mask[i];
71
+
72
+ if (char === "\\" && i + 1 < mask.length) {
73
+ // Escaped character - treat as literal
74
+ const nextChar = mask[i + 1];
75
+ pattern.push({
76
+ pattern: new RegExp(`^${nextChar}$`),
77
+ placeholder: nextChar,
78
+ isLiteral: true,
79
+ });
80
+ i += 2;
81
+ } else if (rules[char]) {
82
+ // Apply Kendo rule
83
+ const rule = rules[char];
84
+ if (typeof rule === "function") {
85
+ pattern.push({
86
+ pattern: /./, // Accept any character, validate with function
87
+ placeholder: char,
88
+ validator: rule,
89
+ });
90
+ } else if (rule instanceof RegExp) {
91
+ pattern.push({
92
+ pattern: rule,
93
+ placeholder: char,
94
+ });
95
+ } else {
96
+ pattern.push({
97
+ pattern: rule.pattern,
98
+ placeholder: rule.placeholder,
99
+ });
100
+ }
101
+ i++;
102
+ } else {
103
+ // Literal character
104
+ pattern.push({
105
+ pattern: new RegExp(`^${char.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}$`),
106
+ placeholder: char,
107
+ isLiteral: true,
108
+ });
109
+ i++;
110
+ }
111
+ }
112
+
113
+ return pattern;
114
+ };
115
+
116
+ // Helper function to apply mask to value using Kendo UI rules
117
+ const applyMask = (
118
+ value: string,
119
+ maskPattern: MaskRule[],
120
+ maskChar: string = "_",
121
+ showMask: boolean = true,
122
+ guide: boolean = true
123
+ ): string => {
124
+ let maskedValue = "";
125
+ let valueIndex = 0;
126
+
127
+ for (let i = 0; i < maskPattern.length; i++) {
128
+ const rule = maskPattern[i];
129
+
130
+ if (rule.isLiteral) {
131
+ // Literal character - always add
132
+ maskedValue += rule.placeholder;
133
+ } else {
134
+ // Input character - find next valid character
135
+ let foundValid = false;
136
+ while (valueIndex < value.length) {
137
+ const char = value[valueIndex];
138
+ valueIndex++;
139
+
140
+ // Check with validator function first if exists
141
+ const isValid = rule.validator
142
+ ? rule.validator(char)
143
+ : rule.pattern.test(char);
144
+
145
+ if (isValid) {
146
+ maskedValue += char;
147
+ foundValid = true;
148
+ break;
149
+ }
150
+ // Skip invalid characters and continue searching
151
+ }
152
+
153
+ if (!foundValid && guide && showMask) {
154
+ // No valid character found, fill with placeholder
155
+ maskedValue += maskChar;
156
+ } else if (!foundValid) {
157
+ // No placeholder needed, stop building the mask
158
+ break;
159
+ }
160
+ }
161
+ }
162
+
163
+ return maskedValue;
164
+ };
165
+
166
+ // Helper function to extract raw value from masked value
167
+ const extractRawValue = (
168
+ maskedValue: string,
169
+ maskPattern: MaskRule[]
170
+ ): string => {
171
+ let rawValue = "";
172
+
173
+ for (let i = 0; i < maskedValue.length && i < maskPattern.length; i++) {
174
+ const rule = maskPattern[i];
175
+ const char = maskedValue[i];
176
+
177
+ if (!rule.isLiteral) {
178
+ // Only include non-literal characters in raw value
179
+ rawValue += char;
180
+ }
181
+ }
182
+
183
+ return rawValue;
184
+ };
185
+
186
+ // Helper function to get cursor position after masking
187
+ const getCursorPosition = (
188
+ maskedValue: string,
189
+ rawInputLength: number,
190
+ maskPattern: MaskRule[]
191
+ ): number => {
192
+ let inputCount = 0;
193
+ let cursorPos = 0;
194
+
195
+ for (let i = 0; i < maskPattern.length && i < maskedValue.length; i++) {
196
+ const rule = maskPattern[i];
197
+ const char = maskedValue[i];
198
+
199
+ if (rule.isLiteral) {
200
+ cursorPos = i + 1;
201
+ } else {
202
+ inputCount++;
203
+ cursorPos = i + 1;
204
+
205
+ if (inputCount >= rawInputLength) {
206
+ break;
207
+ }
208
+ }
209
+ }
210
+
211
+ return cursorPos;
212
+ };
213
+
214
+ export const MaskedTextInput = forwardRef<
215
+ HTMLInputElement,
216
+ MaskedTextInputProps
217
+ >(
218
+ (
219
+ {
220
+ mask,
221
+ maskChar = "_",
222
+ showMask = true,
223
+ guide = true,
224
+ keepCharPositions = false,
225
+ rules,
226
+ onMaskedChange,
227
+ onChange,
228
+ value,
229
+ defaultValue,
230
+ ...props
231
+ },
232
+ ref
233
+ ) => {
234
+ const inputRef = useRef<HTMLInputElement>(null);
235
+ const [maskedValue, setMaskedValue] = useState<string>("");
236
+ const [rawValue, setRawValue] = useState<string>("");
237
+
238
+ // Parse mask pattern using Kendo UI rules
239
+ const maskPattern = React.useMemo(() => {
240
+ if (!mask) return null;
241
+
242
+ return createMaskPattern(mask, rules);
243
+ }, [mask, rules]);
244
+
245
+ // Initialize values
246
+ useEffect(() => {
247
+ const initialValue = value || defaultValue || "";
248
+ if (maskPattern && initialValue) {
249
+ const masked = applyMask(
250
+ initialValue as string,
251
+ maskPattern,
252
+ maskChar,
253
+ showMask,
254
+ guide
255
+ );
256
+ const raw = extractRawValue(masked, maskPattern);
257
+ setMaskedValue(masked);
258
+ setRawValue(raw);
259
+ } else {
260
+ setMaskedValue(initialValue as string);
261
+ setRawValue(initialValue as string);
262
+ }
263
+ }, [maskPattern, maskChar, showMask, guide, value, defaultValue]);
264
+
265
+ useImperativeHandle(ref, () => inputRef?.current as HTMLInputElement);
266
+
267
+ const handleChange = useCallback(
268
+ (event: React.ChangeEvent<HTMLInputElement>) => {
269
+ const inputValue = event.target.value;
270
+
271
+ if (!maskPattern) {
272
+ setMaskedValue(inputValue);
273
+ setRawValue(inputValue);
274
+ onChange?.(event);
275
+ onMaskedChange?.(inputValue, inputValue);
276
+ return;
277
+ }
278
+
279
+ const newMaskedValue = applyMask(
280
+ inputValue,
281
+ maskPattern,
282
+ maskChar,
283
+ showMask,
284
+ guide
285
+ );
286
+ const newRawValue = extractRawValue(newMaskedValue, maskPattern);
287
+
288
+ setMaskedValue(newMaskedValue);
289
+ setRawValue(newRawValue);
290
+
291
+ // Create synthetic event with masked value
292
+ const syntheticEvent = {
293
+ ...event,
294
+ target: {
295
+ ...event.target,
296
+ value: newMaskedValue,
297
+ },
298
+ } as React.ChangeEvent<HTMLInputElement>;
299
+
300
+ onChange?.(syntheticEvent);
301
+ onMaskedChange?.(newMaskedValue, newRawValue);
302
+
303
+ // Set cursor position after state update
304
+ setTimeout(() => {
305
+ if (inputRef.current) {
306
+ const rawLength = newRawValue.replace(/[_\s]/g, "").length;
307
+ const newCursorPos = getCursorPosition(
308
+ newMaskedValue,
309
+ rawLength,
310
+ maskPattern
311
+ );
312
+ inputRef.current.setSelectionRange(newCursorPos, newCursorPos);
313
+ }
314
+ }, 0);
315
+ },
316
+ [maskPattern, maskChar, showMask, guide, onChange, onMaskedChange]
317
+ );
318
+
319
+ const handleKeyDown = useCallback(
320
+ (event: React.KeyboardEvent<HTMLInputElement>) => {
321
+ if (!maskPattern) {
322
+ props.onKeyDown?.(event);
323
+ return;
324
+ }
325
+
326
+ const { key, ctrlKey, metaKey } = event;
327
+ const input = event.target as HTMLInputElement;
328
+ const cursorPos = input.selectionStart || 0;
329
+
330
+ // Allow navigation and editing keys
331
+ if (
332
+ key === "Backspace" ||
333
+ key === "Delete" ||
334
+ key === "ArrowLeft" ||
335
+ key === "ArrowRight" ||
336
+ key === "Home" ||
337
+ key === "End" ||
338
+ key === "Tab" ||
339
+ key.length > 1 || // Allow other special keys
340
+ ctrlKey ||
341
+ metaKey
342
+ ) {
343
+ props.onKeyDown?.(event);
344
+ return;
345
+ }
346
+
347
+ // Find the next non-literal position from cursor
348
+ let targetPos = cursorPos;
349
+ while (
350
+ targetPos < maskPattern.length &&
351
+ maskPattern[targetPos].isLiteral
352
+ ) {
353
+ targetPos++;
354
+ }
355
+
356
+ // Check if we have a valid position and the key matches
357
+ if (targetPos < maskPattern.length) {
358
+ const targetRule = maskPattern[targetPos];
359
+
360
+ // Check with validator function first if exists
361
+ const isValid = targetRule.validator
362
+ ? targetRule.validator(key)
363
+ : targetRule.pattern.test(key);
364
+
365
+ if (!isValid) {
366
+ event.preventDefault();
367
+ return;
368
+ }
369
+ }
370
+
371
+ props.onKeyDown?.(event);
372
+ },
373
+ [maskPattern, props]
374
+ );
375
+
376
+ return (
377
+ <TextInput
378
+ {...props}
379
+ ref={inputRef}
380
+ value={maskedValue}
381
+ onChange={handleChange}
382
+ onKeyDown={handleKeyDown}
383
+ className={cn(props.className)}
384
+ />
385
+ );
386
+ }
387
+ );
388
+
389
+ MaskedTextInput.displayName = "MaskedTextInput";
390
+
391
+ export default MaskedTextInput;
@@ -0,0 +1,202 @@
1
+ # MaskedTextInput Component
2
+
3
+ A powerful masked input component following Kendo UI masking rules with full TypeScript support.
4
+
5
+ ## Features
6
+
7
+ - ✅ Kendo UI compatible mask rules (0, 9, #, L, ?, &, C, A, a)
8
+ - ✅ Custom mask rules support (RegExp or function-based)
9
+ - ✅ Proper cursor positioning
10
+ - ✅ Automatic literal character insertion
11
+ - ✅ Smart character validation
12
+ - ✅ Raw value extraction
13
+ - ✅ Full TypeScript support
14
+
15
+ ## Kendo UI Mask Rules
16
+
17
+ | Rule | Description | Example |
18
+ |------|-------------|---------|
19
+ | `0` | Any digit 0-9 (required) | `000-000-0000` |
20
+ | `L` | Any letter (a-z, A-Z) (required) | `LLL-LLLL` |
21
+ | `A` | Any alphanumeric (required) | `AAAA-0000` |
22
+ | `#` | Digit, space, +, or - | `#00-000-0000` |
23
+ | `9` | Any digit or space (optional) ⚠️ | `999-999-9999` |
24
+ | `?` | Any letter or space (optional) ⚠️ | `???-????` |
25
+ | `a` | Any alphanumeric or space (optional) ⚠️ | `aaaa-aaaa` |
26
+ | `&` | Any character except space | `&&&-&&&` |
27
+ | `C` | Any character including space | `CCC CCC` |
28
+
29
+ > ⚠️ **Note on Optional Rules (9, ?, a):** These rules accept space, making the position optional. Pressing space will skip to the next position. Use `guide={false}` for better UX with optional rules.
30
+
31
+ ## Usage
32
+
33
+ ### Basic Usage
34
+
35
+ ```tsx
36
+ import { MaskedTextInput, MASK_PATTERNS } from '@rovula/ui';
37
+
38
+ // Phone number
39
+ <MaskedTextInput
40
+ label="Phone Number"
41
+ mask={MASK_PATTERNS.PHONE}
42
+ />
43
+
44
+ // Custom mask
45
+ <MaskedTextInput
46
+ label="License Plate"
47
+ mask="AAA-0000"
48
+ />
49
+ ```
50
+
51
+ ### With Callbacks
52
+
53
+ ```tsx
54
+ const [maskedValue, setMaskedValue] = useState("");
55
+ const [rawValue, setRawValue] = useState("");
56
+
57
+ <MaskedTextInput
58
+ label="Phone"
59
+ mask="(000) 000-0000"
60
+ onMaskedChange={(masked, raw) => {
61
+ setMaskedValue(masked); // "(123) 456-7890"
62
+ setRawValue(raw); // "1234567890"
63
+ }}
64
+ />
65
+ ```
66
+
67
+ ### Custom Rules
68
+
69
+ ```tsx
70
+ // Using RegExp - only digits 3-9
71
+ <MaskedTextInput
72
+ mask="~-~-~"
73
+ rules={{
74
+ "~": /[3-9]/
75
+ }}
76
+ />
77
+
78
+ // Using function - only uppercase letters
79
+ <MaskedTextInput
80
+ mask="*-*-*"
81
+ rules={{
82
+ "*": (char: string) => char === char.toUpperCase() && /[A-Z]/.test(char)
83
+ }}
84
+ />
85
+
86
+ // Multiple custom rules
87
+ <MaskedTextInput
88
+ mask="~*-~*-~*"
89
+ rules={{
90
+ "~": /[0-9]/, // Digit
91
+ "*": /[A-Z]/ // Uppercase letter
92
+ }}
93
+ />
94
+ ```
95
+
96
+ ### Escaping Characters
97
+
98
+ Use backslash `\` to escape mask characters and treat them as literals:
99
+
100
+ ```tsx
101
+ <MaskedTextInput mask="\0\0\0-0000" />
102
+ // Result: "000-1234" (first 000 is literal)
103
+ ```
104
+
105
+ ## Props
106
+
107
+ | Prop | Type | Default | Description |
108
+ |------|------|---------|-------------|
109
+ | `mask` | `string` | - | Mask pattern using Kendo UI rules |
110
+ | `maskChar` | `string` | `"_"` | Placeholder character for empty positions |
111
+ | `showMask` | `boolean` | `true` | Show mask placeholders |
112
+ | `guide` | `boolean` | `true` | Show guide placeholders |
113
+ | `rules` | `Record<string, RegExp \| Function>` | - | Custom mask rules |
114
+ | `onMaskedChange` | `(masked: string, raw: string) => void` | - | Callback with masked and raw values |
115
+
116
+ ## Pre-defined Patterns
117
+
118
+ ```tsx
119
+ export const MASK_PATTERNS = {
120
+ PHONE: "(000) 000-0000",
121
+ PHONE_INTL: "+000 000 000 0000",
122
+ CREDIT_CARD: "0000 0000 0000 0000",
123
+ DATE: "00/00/0000",
124
+ TIME: "00:00",
125
+ SSN: "000-00-0000",
126
+ ZIP_CODE: "00000",
127
+ ZIP_CODE_EXT: "00000-0000",
128
+ CURRENCY: "$000,000.00",
129
+ PERCENTAGE: "000%",
130
+ LICENSE_PLATE: "AAA-0000",
131
+ PRODUCT_CODE: "AA-0000-AA",
132
+ ALPHANUMERIC: "AAAA-0000",
133
+ };
134
+ ```
135
+
136
+ ## Examples
137
+
138
+ ### Phone Number
139
+ ```tsx
140
+ <MaskedTextInput mask="(000) 000-0000" label="Phone" />
141
+ // Input: 1234567890
142
+ // Display: (123) 456-7890
143
+ ```
144
+
145
+ ### License Plate
146
+ ```tsx
147
+ <MaskedTextInput mask="LLL-0000" label="License Plate" />
148
+ // Input: ABC1234
149
+ // Display: ABC-1234
150
+ ```
151
+
152
+ ### Mixed Pattern
153
+ ```tsx
154
+ <MaskedTextInput mask="AA-0000-AA" label="Product Code" />
155
+ // Input: AB1234CD
156
+ // Display: AB-1234-CD
157
+ ```
158
+
159
+ ### Optional Input (with space)
160
+ ```tsx
161
+ <MaskedTextInput
162
+ mask="999-999-9999"
163
+ label="Optional Phone"
164
+ guide={false}
165
+ />
166
+ // Input: 123 (then press space to skip)
167
+ // Display: 123- - (spaces are allowed)
168
+ // Good for: Optional/partial input
169
+ ```
170
+
171
+ ## Technical Details
172
+
173
+ ### How It Works
174
+
175
+ 1. **Input Processing**: Characters are validated against mask rules
176
+ 2. **Literal Insertion**: Static characters are automatically added
177
+ 3. **Cursor Management**: Cursor is positioned correctly after input
178
+ 4. **Value Extraction**: Raw value (without literals) is maintained
179
+
180
+ ### Key Improvements
181
+
182
+ - ✅ Fixed typing issues - can now type in all mask patterns
183
+ - ✅ Fixed cursor positioning - cursor stays in correct position
184
+ - ✅ Improved character validation - better handling of invalid input
185
+ - ✅ Smart literal skipping - automatically moves past literal characters
186
+ - ✅ Better performance - optimized mask application logic
187
+
188
+ ## Browser Support
189
+
190
+ Works in all modern browsers that support ES6+ and React 16.8+.
191
+
192
+ ## TypeScript
193
+
194
+ Fully typed with TypeScript. Export types:
195
+
196
+ ```tsx
197
+ import type {
198
+ MaskedTextInputProps,
199
+ MaskRule
200
+ } from '@rovula/ui';
201
+ ```
202
+
@@ -0,0 +1,3 @@
1
+ export { default } from "./MaskedTextInput";
2
+ export type { MaskedTextInputProps, MaskRule } from "./MaskedTextInput";
3
+ export { MASK_PATTERNS } from "./MaskedTextInput";