@comitor/ui 2.0.0 → 2.1.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 (131) hide show
  1. package/LICENSE +35 -0
  2. package/README.md +14 -6
  3. package/dist/components/ui/action-bar-constants.d.ts +31 -0
  4. package/dist/components/ui/action-bar-constants.js +9 -0
  5. package/dist/components/ui/action-bar-constants.js.map +1 -0
  6. package/dist/components/ui/action-bar.d.ts +67 -0
  7. package/dist/components/ui/action-bar.js +522 -0
  8. package/dist/components/ui/action-bar.js.map +1 -0
  9. package/dist/components/ui/badge-overflow.d.ts +30 -0
  10. package/dist/components/ui/badge-overflow.js +179 -0
  11. package/dist/components/ui/badge-overflow.js.map +1 -0
  12. package/dist/components/ui/banner.d.ts +79 -0
  13. package/dist/components/ui/banner.js +518 -0
  14. package/dist/components/ui/banner.js.map +1 -0
  15. package/dist/components/ui/circular-progress.d.ts +50 -0
  16. package/dist/components/ui/circular-progress.js +266 -0
  17. package/dist/components/ui/circular-progress.js.map +1 -0
  18. package/dist/components/ui/cropper-constants.d.ts +34 -0
  19. package/dist/components/ui/cropper-constants.js +10 -0
  20. package/dist/components/ui/cropper-constants.js.map +1 -0
  21. package/dist/components/ui/cropper.d.ts +106 -0
  22. package/dist/components/ui/cropper.js +1269 -0
  23. package/dist/components/ui/cropper.js.map +1 -0
  24. package/dist/components/ui/drawer.d.ts +1 -1
  25. package/dist/components/ui/drawer.js +2 -2
  26. package/dist/components/ui/drawer.js.map +1 -1
  27. package/dist/components/ui/kanban-constants.d.ts +61 -0
  28. package/dist/components/ui/kanban-constants.js +21 -0
  29. package/dist/components/ui/kanban-constants.js.map +1 -0
  30. package/dist/components/ui/kanban.d.ts +72 -0
  31. package/dist/components/ui/kanban.js +787 -0
  32. package/dist/components/ui/kanban.js.map +1 -0
  33. package/dist/components/ui/key-value-constants.d.ts +33 -0
  34. package/dist/components/ui/key-value-constants.js +11 -0
  35. package/dist/components/ui/key-value-constants.js.map +1 -0
  36. package/dist/components/ui/key-value.d.ts +96 -0
  37. package/dist/components/ui/key-value.js +641 -0
  38. package/dist/components/ui/key-value.js.map +1 -0
  39. package/dist/components/ui/mask-input-constants.d.ts +99 -0
  40. package/dist/components/ui/mask-input-constants.js +601 -0
  41. package/dist/components/ui/mask-input-constants.js.map +1 -0
  42. package/dist/components/ui/mask-input.d.ts +36 -0
  43. package/dist/components/ui/mask-input.js +558 -0
  44. package/dist/components/ui/mask-input.js.map +1 -0
  45. package/dist/components/ui/mention.d.ts +31 -0
  46. package/dist/components/ui/mention.js +128 -0
  47. package/dist/components/ui/mention.js.map +1 -0
  48. package/dist/components/ui/phone-input-constants.d.ts +54 -0
  49. package/dist/components/ui/phone-input-constants.js +9 -0
  50. package/dist/components/ui/phone-input-constants.js.map +1 -0
  51. package/dist/components/ui/phone-input.d.ts +85 -0
  52. package/dist/components/ui/phone-input.js +743 -0
  53. package/dist/components/ui/phone-input.js.map +1 -0
  54. package/dist/components/ui/rating.d.ts +56 -0
  55. package/dist/components/ui/rating.js +646 -0
  56. package/dist/components/ui/rating.js.map +1 -0
  57. package/dist/components/ui/responsive-dialog.d.ts +35 -0
  58. package/dist/components/ui/responsive-dialog.js +193 -0
  59. package/dist/components/ui/responsive-dialog.js.map +1 -0
  60. package/dist/components/ui/scroll-spy.d.ts +42 -0
  61. package/dist/components/ui/scroll-spy.js +334 -0
  62. package/dist/components/ui/scroll-spy.js.map +1 -0
  63. package/dist/components/ui/stepper-constants.d.ts +29 -0
  64. package/dist/components/ui/stepper-constants.js +9 -0
  65. package/dist/components/ui/stepper-constants.js.map +1 -0
  66. package/dist/components/ui/stepper.d.ts +80 -0
  67. package/dist/components/ui/stepper.js +902 -0
  68. package/dist/components/ui/stepper.js.map +1 -0
  69. package/dist/components/ui/tags-input.d.ts +41 -0
  70. package/dist/components/ui/tags-input.js +127 -0
  71. package/dist/components/ui/tags-input.js.map +1 -0
  72. package/dist/components/ui/time-picker-constants.d.ts +48 -0
  73. package/dist/components/ui/time-picker-constants.js +14 -0
  74. package/dist/components/ui/time-picker-constants.js.map +1 -0
  75. package/dist/components/ui/time-picker.d.ts +115 -0
  76. package/dist/components/ui/time-picker.js +1620 -0
  77. package/dist/components/ui/time-picker.js.map +1 -0
  78. package/dist/components/ui/timeline.d.ts +32 -0
  79. package/dist/components/ui/timeline.js +559 -0
  80. package/dist/components/ui/timeline.js.map +1 -0
  81. package/dist/components/ui/tour-constants.d.ts +39 -0
  82. package/dist/components/ui/tour-constants.js +12 -0
  83. package/dist/components/ui/tour-constants.js.map +1 -0
  84. package/dist/components/ui/tour.d.ts +124 -0
  85. package/dist/components/ui/tour.js +1207 -0
  86. package/dist/components/ui/tour.js.map +1 -0
  87. package/dist/data-grid/data-grid-cell-wrapper.js +1 -1
  88. package/dist/data-grid/data-grid-cell-wrapper.js.map +1 -1
  89. package/dist/data-grid/data-grid-context-menu.js +1 -1
  90. package/dist/data-grid/data-grid-context-menu.js.map +1 -1
  91. package/dist/data-grid/data-grid-paste-dialog.js +1 -1
  92. package/dist/data-grid/data-grid-paste-dialog.js.map +1 -1
  93. package/dist/data-grid/data-grid-row.js +1 -1
  94. package/dist/data-grid/data-grid-row.js.map +1 -1
  95. package/dist/data-grid/data-grid-search.js +1 -1
  96. package/dist/data-grid/data-grid-search.js.map +1 -1
  97. package/dist/data-grid/data-grid.js +1 -1
  98. package/dist/data-grid/data-grid.js.map +1 -1
  99. package/dist/data-grid/sortable.js +1 -1
  100. package/dist/data-grid/sortable.js.map +1 -1
  101. package/dist/data-grid/use-data-grid-undo-redo.js +2 -2
  102. package/dist/data-grid/use-data-grid-undo-redo.js.map +1 -1
  103. package/dist/data-grid/use-data-grid.js +3 -3
  104. package/dist/data-grid/use-data-grid.js.map +1 -1
  105. package/dist/index.d.ts +31 -0
  106. package/dist/index.js +42 -0
  107. package/dist/index.js.map +1 -1
  108. package/dist/lib/compose-refs.js.map +1 -0
  109. package/dist/lib/use-as-ref.js.map +1 -0
  110. package/dist/lib/use-is-mobile.d.ts +10 -0
  111. package/dist/lib/use-is-mobile.js +21 -0
  112. package/dist/lib/use-is-mobile.js.map +1 -0
  113. package/dist/lib/use-isomorphic-layout-effect.js.map +1 -0
  114. package/dist/lib/use-lazy-ref.js.map +1 -0
  115. package/dist/lib/visually-hidden-input.d.ts +12 -0
  116. package/dist/lib/visually-hidden-input.js +105 -0
  117. package/dist/lib/visually-hidden-input.js.map +1 -0
  118. package/package.json +5 -1
  119. package/styles.css +19 -0
  120. package/dist/data-grid/compose-refs.js.map +0 -1
  121. package/dist/data-grid/use-as-ref.js.map +0 -1
  122. package/dist/data-grid/use-isomorphic-layout-effect.js.map +0 -1
  123. package/dist/data-grid/use-lazy-ref.js.map +0 -1
  124. /package/dist/{data-grid → lib}/compose-refs.d.ts +0 -0
  125. /package/dist/{data-grid → lib}/compose-refs.js +0 -0
  126. /package/dist/{data-grid → lib}/use-as-ref.d.ts +0 -0
  127. /package/dist/{data-grid → lib}/use-as-ref.js +0 -0
  128. /package/dist/{data-grid → lib}/use-isomorphic-layout-effect.d.ts +0 -0
  129. /package/dist/{data-grid → lib}/use-isomorphic-layout-effect.js +0 -0
  130. /package/dist/{data-grid → lib}/use-lazy-ref.d.ts +0 -0
  131. /package/dist/{data-grid → lib}/use-lazy-ref.js +0 -0
@@ -0,0 +1,601 @@
1
+ const PAST_YEARS_LIMIT = 120;
2
+ const FUTURE_YEARS_LIMIT = 10;
3
+ const DEFAULT_CURRENCY = "VND";
4
+ const DEFAULT_LOCALE = "vi-VN";
5
+ const NUMERIC_MASK_PATTERNS = /^(phone|zipCode|zipCodeExtended|ssn|ein|time|date|creditCard|creditCardExpiry)$/;
6
+ const CURRENCY_PERCENTAGE_SYMBOLS = /[€$%]/;
7
+ const formattersCache = /* @__PURE__ */ new Map();
8
+ const currencyAtEndCache = /* @__PURE__ */ new Map();
9
+ const currencySymbolsCache = /* @__PURE__ */ new Map();
10
+ const fractionDigitsCache = /* @__PURE__ */ new Map();
11
+ const daysInMonthCache = [31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31];
12
+ const REGEX_CACHE = {
13
+ digitsOnly: /^\d+$/,
14
+ nonDigits: /\D/g,
15
+ nonAlphaNumeric: /[^A-Z0-9]/gi,
16
+ nonNumericDot: /[^0-9.]/g,
17
+ nonCurrencyChars: /[^\d.,]/g,
18
+ hashPattern: /#/g,
19
+ currencyAtEnd: /\d\s*[^\d\s]+$/,
20
+ percentageChars: /[^\d.]/g,
21
+ phone: /^\d{10}$/,
22
+ ssn: /^\d{9}$/,
23
+ zipCode: /^\d{5}$/,
24
+ zipCodeExtended: /^\d{9}$/,
25
+ isbn: /^\d{13}$/,
26
+ ein: /^\d{9}$/,
27
+ time: /^\d{4}$/,
28
+ creditCard: /^\d{13,19}$/,
29
+ creditCardExpiry: /^\d{4}$/,
30
+ licensePlate: /^[A-Z0-9]{6}$/,
31
+ macAddress: /^[A-F0-9]{12}$/,
32
+ currencyValidation: /^\d+(\.\d{1,2})?$/,
33
+ ipv4Segment: /^\d{1,3}$/
34
+ };
35
+ function getCachedFormatter(locale, opts) {
36
+ const { currency, minimumFractionDigits = 0, maximumFractionDigits = 2 } = opts;
37
+ const key = `${locale}|${currency}|${minimumFractionDigits}|${maximumFractionDigits}`;
38
+ if (!formattersCache.has(key)) {
39
+ try {
40
+ formattersCache.set(
41
+ key,
42
+ new Intl.NumberFormat(locale, {
43
+ style: "currency",
44
+ currency,
45
+ ...opts
46
+ })
47
+ );
48
+ } catch {
49
+ formattersCache.set(
50
+ key,
51
+ new Intl.NumberFormat(DEFAULT_LOCALE, {
52
+ style: "currency",
53
+ currency: DEFAULT_CURRENCY,
54
+ ...opts
55
+ })
56
+ );
57
+ }
58
+ }
59
+ const formatter = formattersCache.get(key);
60
+ if (!formatter) {
61
+ throw new Error(`Kh\xF4ng t\u1EA1o \u0111\u01B0\u1EE3c b\u1ED9 \u0111\u1ECBnh d\u1EA1ng ti\u1EC1n t\u1EC7 cho ${key}`);
62
+ }
63
+ return formatter;
64
+ }
65
+ function getCachedCurrencySymbols(opts) {
66
+ const { locale, currency } = opts;
67
+ const key = `${locale}|${currency}`;
68
+ const cached = currencySymbolsCache.get(key);
69
+ if (cached) {
70
+ return cached;
71
+ }
72
+ let currencySymbol = "$";
73
+ let decimalSeparator = ".";
74
+ let groupSeparator = ",";
75
+ try {
76
+ const formatter = getCachedFormatter(locale, {
77
+ currency,
78
+ minimumFractionDigits: 0,
79
+ maximumFractionDigits: 2
80
+ });
81
+ const parts = formatter.formatToParts(1234.5);
82
+ const currencyPart = parts.find((part) => part.type === "currency");
83
+ const decimalPart = parts.find((part) => part.type === "decimal");
84
+ const groupPart = parts.find((part) => part.type === "group");
85
+ if (currencyPart) currencySymbol = currencyPart.value;
86
+ if (decimalPart) decimalSeparator = decimalPart.value;
87
+ if (groupPart) groupSeparator = groupPart.value;
88
+ } catch {
89
+ }
90
+ const symbols = {
91
+ currency: currencySymbol,
92
+ decimal: decimalSeparator,
93
+ group: groupSeparator
94
+ };
95
+ currencySymbolsCache.set(key, symbols);
96
+ return symbols;
97
+ }
98
+ function getCurrencyFractionDigits(opts) {
99
+ const { locale, currency } = opts;
100
+ const key = `${locale}|${currency}`;
101
+ const cached = fractionDigitsCache.get(key);
102
+ if (cached !== void 0) {
103
+ return cached;
104
+ }
105
+ let digits = 2;
106
+ try {
107
+ const resolved = new Intl.NumberFormat(locale, { style: "currency", currency }).resolvedOptions();
108
+ digits = Math.min(2, resolved.maximumFractionDigits ?? 2);
109
+ } catch {
110
+ }
111
+ fractionDigitsCache.set(key, digits);
112
+ return digits;
113
+ }
114
+ function isCurrencyAtEnd(opts) {
115
+ const { locale, currency } = opts;
116
+ const key = `${locale}|${currency}`;
117
+ const cached = currencyAtEndCache.get(key);
118
+ if (cached !== void 0) {
119
+ return cached;
120
+ }
121
+ try {
122
+ const formatter = getCachedFormatter(locale, {
123
+ currency,
124
+ minimumFractionDigits: 0,
125
+ maximumFractionDigits: 0
126
+ });
127
+ const sample = formatter.format(123);
128
+ const result = REGEX_CACHE.currencyAtEnd.test(sample);
129
+ currencyAtEndCache.set(key, result);
130
+ return result;
131
+ } catch {
132
+ currencyAtEndCache.set(key, false);
133
+ return false;
134
+ }
135
+ }
136
+ function isCurrencyMask(opts) {
137
+ const { mask, pattern } = opts;
138
+ return mask === "currency" || Boolean(pattern && (pattern.includes("$") || pattern.includes("\u20AC")));
139
+ }
140
+ const MASK_PATTERNS = {
141
+ /**
142
+ * Số điện thoại **Hoa Kỳ** `(###) ###-####`, đúng 10 chữ số.
143
+ *
144
+ * ⚠ Không dùng cho số điện thoại ở app Comitor: số Việt Nam (và mọi số quốc tế) đi qua
145
+ * `PhoneInput` — có chọn quốc gia, tự nhận mã vùng và lưu dạng `+84…`.
146
+ */
147
+ phone: {
148
+ pattern: "(###) ###-####",
149
+ transform: (value) => value.replace(REGEX_CACHE.nonDigits, ""),
150
+ validate: (value) => REGEX_CACHE.phone.test(value.replace(REGEX_CACHE.nonDigits, ""))
151
+ },
152
+ ssn: {
153
+ pattern: "###-##-####",
154
+ transform: (value) => value.replace(REGEX_CACHE.nonDigits, ""),
155
+ validate: (value) => REGEX_CACHE.ssn.test(value.replace(REGEX_CACHE.nonDigits, ""))
156
+ },
157
+ /**
158
+ * Ngày kiểu **Hoa Kỳ** `MM/DD/YYYY` — `validate` đọc 2 chữ số ĐẦU là THÁNG. Ngày kiểu Việt Nam
159
+ * (`dd/MM/yyyy`) thì dùng `DatePicker` của gói, hoặc một `MaskPattern` riêng.
160
+ */
161
+ date: {
162
+ pattern: "##/##/####",
163
+ transform: (value) => value.replace(REGEX_CACHE.nonDigits, ""),
164
+ validate: (value) => {
165
+ const cleaned = value.replace(REGEX_CACHE.nonDigits, "");
166
+ if (cleaned.length !== 8) return false;
167
+ const month = Number.parseInt(cleaned.substring(0, 2), 10);
168
+ const day = Number.parseInt(cleaned.substring(2, 4), 10);
169
+ const year = Number.parseInt(cleaned.substring(4, 8), 10);
170
+ const currentYear = (/* @__PURE__ */ new Date()).getFullYear();
171
+ const minYear = currentYear - PAST_YEARS_LIMIT;
172
+ const maxYear = currentYear + FUTURE_YEARS_LIMIT;
173
+ if (month < 1 || month > 12 || day < 1 || year < minYear || year > maxYear) return false;
174
+ const maxDays = month === 2 && (year % 4 === 0 && year % 100 !== 0 || year % 400 === 0) ? 29 : daysInMonthCache[month - 1] ?? 31;
175
+ return day <= maxDays;
176
+ }
177
+ },
178
+ time: {
179
+ pattern: "##:##",
180
+ transform: (value) => value.replace(REGEX_CACHE.nonDigits, ""),
181
+ validate: (value) => {
182
+ const cleaned = value.replace(REGEX_CACHE.nonDigits, "");
183
+ if (!REGEX_CACHE.time.test(cleaned)) return false;
184
+ const hours = Number.parseInt(cleaned.substring(0, 2), 10);
185
+ const minutes = Number.parseInt(cleaned.substring(2, 4), 10);
186
+ return hours <= 23 && minutes <= 59;
187
+ }
188
+ },
189
+ creditCard: {
190
+ pattern: "#### #### #### ####",
191
+ transform: (value) => value.replace(REGEX_CACHE.nonDigits, ""),
192
+ validate: (value) => {
193
+ const cleaned = value.replace(REGEX_CACHE.nonDigits, "");
194
+ if (!REGEX_CACHE.creditCard.test(cleaned)) return false;
195
+ let sum = 0;
196
+ let isEven = false;
197
+ for (let i = cleaned.length - 1; i >= 0; i--) {
198
+ const digitChar = cleaned[i];
199
+ if (!digitChar) continue;
200
+ let digit = Number.parseInt(digitChar, 10);
201
+ if (isEven) {
202
+ digit *= 2;
203
+ if (digit > 9) {
204
+ digit -= 9;
205
+ }
206
+ }
207
+ sum += digit;
208
+ isEven = !isEven;
209
+ }
210
+ return sum % 10 === 0;
211
+ }
212
+ },
213
+ creditCardExpiry: {
214
+ pattern: "##/##",
215
+ transform: (value) => value.replace(REGEX_CACHE.nonDigits, ""),
216
+ validate: (value) => {
217
+ const cleaned = value.replace(REGEX_CACHE.nonDigits, "");
218
+ if (!REGEX_CACHE.creditCardExpiry.test(cleaned)) return false;
219
+ const month = Number.parseInt(cleaned.substring(0, 2), 10);
220
+ const year = Number.parseInt(cleaned.substring(2, 4), 10);
221
+ if (month < 1 || month > 12) return false;
222
+ const now = /* @__PURE__ */ new Date();
223
+ const currentYear = now.getFullYear();
224
+ const currentMonth = now.getMonth() + 1;
225
+ const fullYear = year <= 75 ? 2e3 + year : 1900 + year;
226
+ if (fullYear < currentYear || fullYear === currentYear && month < currentMonth) {
227
+ return false;
228
+ }
229
+ const maxYear = currentYear + 50;
230
+ if (fullYear > maxYear) {
231
+ return false;
232
+ }
233
+ return true;
234
+ }
235
+ },
236
+ zipCode: {
237
+ pattern: "#####",
238
+ transform: (value) => value.replace(REGEX_CACHE.nonDigits, ""),
239
+ validate: (value) => REGEX_CACHE.zipCode.test(value.replace(REGEX_CACHE.nonDigits, ""))
240
+ },
241
+ zipCodeExtended: {
242
+ pattern: "#####-####",
243
+ transform: (value) => value.replace(REGEX_CACHE.nonDigits, ""),
244
+ validate: (value) => REGEX_CACHE.zipCodeExtended.test(value.replace(REGEX_CACHE.nonDigits, ""))
245
+ },
246
+ /**
247
+ * Tiền tệ — định dạng thật đi qua `Intl.NumberFormat` theo `currency`/`locale` (mặc định VND /
248
+ * vi-VN → `1.234.567 ₫`). Chuỗi `pattern` ở đây chỉ là DẤU NHẬN DẠNG cho nhánh tiền tệ, không
249
+ * phải khuôn chữ số.
250
+ */
251
+ currency: {
252
+ pattern: "$###,###.##",
253
+ transform: (value, { currency = DEFAULT_CURRENCY, locale = DEFAULT_LOCALE } = {}) => {
254
+ if (getCurrencyFractionDigits({ currency, locale }) === 0) {
255
+ return value.replace(REGEX_CACHE.nonDigits, "");
256
+ }
257
+ let localeDecimalSeparator = ".";
258
+ try {
259
+ const formatter = getCachedFormatter(locale, {
260
+ currency,
261
+ minimumFractionDigits: 0,
262
+ maximumFractionDigits: 2
263
+ });
264
+ const parts = formatter.formatToParts(1234.5);
265
+ const decimalPart = parts.find((part) => part.type === "decimal");
266
+ if (decimalPart) localeDecimalSeparator = decimalPart.value;
267
+ } catch {
268
+ }
269
+ const cleaned = value.replace(REGEX_CACHE.nonCurrencyChars, "");
270
+ const dotIndex = cleaned.indexOf(".");
271
+ const commaIndex = cleaned.indexOf(",");
272
+ let hasDecimalSeparator = false;
273
+ let decimalIndex = -1;
274
+ if (localeDecimalSeparator === ",") {
275
+ const lastCommaIndex = cleaned.lastIndexOf(",");
276
+ if (lastCommaIndex !== -1) {
277
+ const afterComma = cleaned.substring(lastCommaIndex + 1);
278
+ if (afterComma.length <= 2 && /^\d*$/.test(afterComma)) {
279
+ hasDecimalSeparator = true;
280
+ decimalIndex = lastCommaIndex;
281
+ }
282
+ }
283
+ if (!hasDecimalSeparator && dotIndex !== -1) {
284
+ const afterDot = cleaned.substring(dotIndex + 1);
285
+ if (afterDot.length <= 2 && /^\d*$/.test(afterDot)) {
286
+ hasDecimalSeparator = true;
287
+ decimalIndex = dotIndex;
288
+ }
289
+ }
290
+ if (!hasDecimalSeparator && cleaned.length >= 4) {
291
+ const match = cleaned.match(/^(\d+)\.(\d{3})(\d{1,2})$/);
292
+ if (match) {
293
+ const [, beforeDot, thousandsPart, decimalPart] = match;
294
+ const integerPart = (beforeDot ?? "") + (thousandsPart ?? "");
295
+ const result = `${integerPart}.${decimalPart}`;
296
+ return result;
297
+ }
298
+ }
299
+ } else {
300
+ const lastDotIndex = cleaned.lastIndexOf(".");
301
+ if (lastDotIndex !== -1) {
302
+ const afterDot = cleaned.substring(lastDotIndex + 1);
303
+ if (afterDot.length <= 2 && /^\d*$/.test(afterDot)) {
304
+ hasDecimalSeparator = true;
305
+ decimalIndex = lastDotIndex;
306
+ }
307
+ }
308
+ if (!hasDecimalSeparator && commaIndex !== -1) {
309
+ const afterComma = cleaned.substring(commaIndex + 1);
310
+ const looksLikeThousands = commaIndex <= 3 && afterComma.length >= 3;
311
+ if (!looksLikeThousands && afterComma.length <= 2 && /^\d*$/.test(afterComma)) {
312
+ hasDecimalSeparator = true;
313
+ decimalIndex = commaIndex;
314
+ }
315
+ }
316
+ }
317
+ if (hasDecimalSeparator && decimalIndex !== -1) {
318
+ const beforeDecimal = cleaned.substring(0, decimalIndex).replace(/[.,]/g, "");
319
+ const afterDecimal = cleaned.substring(decimalIndex + 1).replace(/[.,]/g, "");
320
+ if (afterDecimal === "") {
321
+ const result2 = `${beforeDecimal}.`;
322
+ return result2;
323
+ }
324
+ const result = `${beforeDecimal}.${afterDecimal.substring(0, 2)}`;
325
+ return result;
326
+ }
327
+ const digitsOnly = cleaned.replace(/[.,]/g, "");
328
+ return digitsOnly;
329
+ },
330
+ validate: (value) => {
331
+ if (!REGEX_CACHE.currencyValidation.test(value)) return false;
332
+ const num = Number.parseFloat(value);
333
+ return !Number.isNaN(num) && num >= 0;
334
+ }
335
+ },
336
+ /**
337
+ * Phần trăm `##.##%` — dấu thập phân là `.` cố định, KHÔNG theo `locale` (dấu `,` bị bỏ qua).
338
+ */
339
+ percentage: {
340
+ pattern: "##.##%",
341
+ transform: (value) => {
342
+ const cleaned = value.replace(REGEX_CACHE.percentageChars, "");
343
+ const parts = cleaned.split(".");
344
+ if (parts.length > 2) {
345
+ return `${parts[0]}.${parts.slice(1).join("")}`;
346
+ }
347
+ if (parts[1] && parts[1].length > 2) {
348
+ return `${parts[0]}.${parts[1].substring(0, 2)}`;
349
+ }
350
+ return cleaned;
351
+ },
352
+ validate: (value, opts = {}) => {
353
+ const num = Number.parseFloat(value);
354
+ const min = opts.min ?? 0;
355
+ const max = opts.max ?? 100;
356
+ return !Number.isNaN(num) && num >= min && num <= max;
357
+ }
358
+ },
359
+ licensePlate: {
360
+ pattern: "###-###",
361
+ transform: (value) => value.replace(REGEX_CACHE.nonAlphaNumeric, "").toUpperCase(),
362
+ validate: (value) => REGEX_CACHE.licensePlate.test(value)
363
+ },
364
+ ipv4: {
365
+ pattern: "###.###.###.###",
366
+ transform: (value) => value.replace(REGEX_CACHE.nonNumericDot, ""),
367
+ validate: (value) => {
368
+ if (value.includes(".")) {
369
+ const segments = value.split(".");
370
+ if (segments.length > 4) return false;
371
+ return segments.every((segment) => {
372
+ if (segment === "") return true;
373
+ if (!REGEX_CACHE.ipv4Segment.test(segment)) return false;
374
+ const num = Number.parseInt(segment, 10);
375
+ return num <= 255;
376
+ });
377
+ }
378
+ if (!REGEX_CACHE.digitsOnly.test(value)) return false;
379
+ if (value.length > 12) return false;
380
+ const chunks = [];
381
+ for (let i = 0; i < value.length; i += 3) {
382
+ chunks.push(value.substring(i, i + 3));
383
+ }
384
+ if (chunks.length > 4) return false;
385
+ return chunks.every((chunk) => {
386
+ const num = Number.parseInt(chunk, 10);
387
+ return num >= 0 && num <= 255;
388
+ });
389
+ }
390
+ },
391
+ macAddress: {
392
+ pattern: "##:##:##:##:##:##",
393
+ transform: (value) => value.replace(REGEX_CACHE.nonAlphaNumeric, "").toUpperCase(),
394
+ validate: (value) => REGEX_CACHE.macAddress.test(value)
395
+ },
396
+ isbn: {
397
+ pattern: "###-#-###-#####-#",
398
+ transform: (value) => value.replace(REGEX_CACHE.nonDigits, ""),
399
+ validate: (value) => REGEX_CACHE.isbn.test(value.replace(REGEX_CACHE.nonDigits, ""))
400
+ },
401
+ ein: {
402
+ pattern: "##-#######",
403
+ transform: (value) => value.replace(REGEX_CACHE.nonDigits, ""),
404
+ validate: (value) => REGEX_CACHE.ein.test(value.replace(REGEX_CACHE.nonDigits, ""))
405
+ }
406
+ };
407
+ function applyMask(opts) {
408
+ const { value, pattern, currency, locale, mask } = opts;
409
+ const cleanValue = value;
410
+ if (pattern.includes("$") || pattern.includes("\u20AC") || mask === "currency") {
411
+ return applyCurrencyMask({
412
+ value: cleanValue,
413
+ currency: currency ?? DEFAULT_CURRENCY,
414
+ locale: locale ?? DEFAULT_LOCALE
415
+ });
416
+ }
417
+ if (pattern.includes("%")) {
418
+ return applyPercentageMask(cleanValue);
419
+ }
420
+ if (mask === "ipv4") {
421
+ return cleanValue;
422
+ }
423
+ const maskedChars = [];
424
+ let valueIndex = 0;
425
+ for (let i = 0; i < pattern.length && valueIndex < cleanValue.length; i++) {
426
+ const patternChar = pattern[i];
427
+ const valueChar = cleanValue[valueIndex];
428
+ if (patternChar === "#" && valueChar) {
429
+ maskedChars.push(valueChar);
430
+ valueIndex++;
431
+ } else if (patternChar) {
432
+ maskedChars.push(patternChar);
433
+ }
434
+ }
435
+ return maskedChars.join("");
436
+ }
437
+ function applyCurrencyMask(opts) {
438
+ const { value, currency = DEFAULT_CURRENCY, locale = DEFAULT_LOCALE } = opts;
439
+ if (!value) return "";
440
+ const {
441
+ currency: currencySymbol,
442
+ decimal: decimalSeparator,
443
+ group: groupSeparator
444
+ } = getCachedCurrencySymbols({ locale, currency });
445
+ const normalizedValue = value.replace(new RegExp(`\\${groupSeparator.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}`, "g"), "").replace(decimalSeparator, ".");
446
+ const parts = normalizedValue.split(".");
447
+ const integerPart = parts[0] ?? "";
448
+ const fractionalPart = parts[1] ?? "";
449
+ if (!integerPart && !fractionalPart) return "";
450
+ const fractionDigits = getCurrencyFractionDigits({ currency, locale });
451
+ const intValue = integerPart ?? "0";
452
+ const fracValue = fractionalPart.slice(0, fractionDigits);
453
+ const num = Number(`${intValue}.${fracValue ?? ""}`);
454
+ if (Number.isNaN(num)) {
455
+ const cleanedDigits = value.replace(/[^\d]/g, "");
456
+ if (!cleanedDigits) return "";
457
+ return `${currencySymbol}${cleanedDigits}`;
458
+ }
459
+ const hasExplicitDecimal = fractionDigits > 0 && (value.includes(".") || value.includes(decimalSeparator));
460
+ try {
461
+ const formatter = getCachedFormatter(locale, {
462
+ currency,
463
+ minimumFractionDigits: fracValue ? fracValue.length : 0,
464
+ maximumFractionDigits: 2
465
+ });
466
+ const result = formatter.format(num);
467
+ if (hasExplicitDecimal && !fracValue) {
468
+ if (result.match(/^[^\d\s]+/)) {
469
+ const finalResult2 = result.replace(/(\d)$/, `$1${decimalSeparator}`);
470
+ return finalResult2;
471
+ }
472
+ const finalResult = result.replace(/(\d)(\s*)([^\d\s]+)$/, `$1${decimalSeparator}$2$3`);
473
+ return finalResult;
474
+ }
475
+ return result;
476
+ } catch {
477
+ const formattedInt = intValue.replace(/\B(?=(\d{3})+(?!\d))/g, groupSeparator);
478
+ let result = `${currencySymbol}${formattedInt}`;
479
+ if (hasExplicitDecimal) {
480
+ result += `${decimalSeparator}${fracValue}`;
481
+ }
482
+ return result;
483
+ }
484
+ }
485
+ function applyPercentageMask(value) {
486
+ if (!value) return "";
487
+ const parts = value.split(".");
488
+ let result = parts[0] ?? "0";
489
+ if (value.includes(".")) {
490
+ result += `.${(parts[1] ?? "").substring(0, 2)}`;
491
+ }
492
+ return `${result}%`;
493
+ }
494
+ function getUnmaskedValue(opts) {
495
+ const { value, transform, currency, locale } = opts;
496
+ return transform ? transform(value, { currency, locale }) : value.replace(REGEX_CACHE.nonDigits, "");
497
+ }
498
+ function toUnmaskedIndex(opts) {
499
+ const { masked, pattern, caret } = opts;
500
+ let idx = 0;
501
+ for (let i = 0; i < caret && i < masked.length && i < pattern.length; i++) {
502
+ if (pattern[i] === "#") {
503
+ idx++;
504
+ }
505
+ }
506
+ return idx;
507
+ }
508
+ function fromUnmaskedIndex(opts) {
509
+ const { masked, pattern, unmaskedIndex } = opts;
510
+ let seen = 0;
511
+ for (let i = 0; i < masked.length && i < pattern.length; i++) {
512
+ if (pattern[i] === "#") {
513
+ seen++;
514
+ if (seen === unmaskedIndex) {
515
+ return i + 1;
516
+ }
517
+ }
518
+ }
519
+ return masked.length;
520
+ }
521
+ function getCurrencyCaretPosition(opts) {
522
+ const { newValue, mask, transformOpts, oldCursorPosition, oldValue, previousUnmasked } = opts;
523
+ if (oldCursorPosition !== void 0 && oldValue && previousUnmasked !== void 0) {
524
+ if (oldCursorPosition < oldValue.length) {
525
+ const digitsBeforeCursor = oldValue.substring(0, oldCursorPosition).replace(/\D/g, "").length;
526
+ let digitCount = 0;
527
+ for (let i = 0; i < newValue.length; i++) {
528
+ if (/\d/.test(newValue[i] ?? "")) {
529
+ digitCount++;
530
+ if (digitCount === digitsBeforeCursor) {
531
+ return i + 1;
532
+ }
533
+ }
534
+ }
535
+ }
536
+ }
537
+ if (mask === "currency") {
538
+ const currencyAtEnd = isCurrencyAtEnd(transformOpts);
539
+ if (currencyAtEnd) {
540
+ const match = newValue.match(/(\d)\s*([^\d\s]+)$/);
541
+ if (match?.[1]) {
542
+ return newValue.lastIndexOf(match[1]) + 1;
543
+ }
544
+ return newValue.length;
545
+ }
546
+ return newValue.length;
547
+ }
548
+ return newValue.length;
549
+ }
550
+ function getPatternCaretPosition(opts) {
551
+ const { newValue, maskPattern, currentUnmasked, oldCursorPosition, oldValue, previousUnmasked } = opts;
552
+ let position = 0;
553
+ let unmaskedCount = 0;
554
+ if (oldCursorPosition !== void 0 && oldValue && previousUnmasked !== void 0) {
555
+ const oldUnmaskedIndex = toUnmaskedIndex({
556
+ masked: oldValue,
557
+ pattern: maskPattern.pattern,
558
+ caret: oldCursorPosition
559
+ });
560
+ if (oldCursorPosition < oldValue.length) {
561
+ const targetUnmaskedIndex = Math.min(oldUnmaskedIndex, currentUnmasked.length);
562
+ for (let i = 0; i < maskPattern.pattern.length && i < newValue.length; i++) {
563
+ if (maskPattern.pattern[i] === "#") {
564
+ unmaskedCount++;
565
+ if (unmaskedCount <= targetUnmaskedIndex) {
566
+ position = i + 1;
567
+ }
568
+ }
569
+ }
570
+ return position;
571
+ }
572
+ }
573
+ for (let i = 0; i < maskPattern.pattern.length && i < newValue.length; i++) {
574
+ if (maskPattern.pattern[i] === "#") {
575
+ unmaskedCount++;
576
+ if (unmaskedCount <= currentUnmasked.length) {
577
+ position = i + 1;
578
+ }
579
+ }
580
+ }
581
+ return position;
582
+ }
583
+ export {
584
+ CURRENCY_PERCENTAGE_SYMBOLS,
585
+ DEFAULT_CURRENCY,
586
+ DEFAULT_LOCALE,
587
+ MASK_PATTERNS,
588
+ NUMERIC_MASK_PATTERNS,
589
+ REGEX_CACHE,
590
+ applyCurrencyMask,
591
+ applyMask,
592
+ applyPercentageMask,
593
+ fromUnmaskedIndex,
594
+ getCurrencyCaretPosition,
595
+ getPatternCaretPosition,
596
+ getUnmaskedValue,
597
+ isCurrencyAtEnd,
598
+ isCurrencyMask,
599
+ toUnmaskedIndex
600
+ };
601
+ //# sourceMappingURL=mask-input-constants.js.map