@yomologic/react-ui 0.6.2 → 0.6.4

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.
package/dist/index.mjs CHANGED
@@ -1,4 +1,69 @@
1
1
  "use client";
2
+ var __defProp = Object.defineProperty;
3
+ var __getOwnPropNames = Object.getOwnPropertyNames;
4
+ var __esm = (fn, res) => function __init() {
5
+ return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
6
+ };
7
+ var __export = (target, all) => {
8
+ for (var name in all)
9
+ __defProp(target, name, { get: all[name], enumerable: true });
10
+ };
11
+
12
+ // src/constants/validation.ts
13
+ var validation_exports = {};
14
+ __export(validation_exports, {
15
+ DATE_REGEX: () => DATE_REGEX,
16
+ EMAIL_REGEX: () => EMAIL_REGEX,
17
+ PHONE_REGEX: () => PHONE_REGEX,
18
+ URL_REGEX: () => URL_REGEX,
19
+ isValidDate: () => isValidDate,
20
+ isValidEmail: () => isValidEmail,
21
+ isValidPhone: () => isValidPhone,
22
+ isValidUrl: () => isValidUrl
23
+ });
24
+ var EMAIL_REGEX, URL_REGEX, PHONE_REGEX, isValidEmail, isValidUrl, DATE_REGEX, isValidPhone, isValidDate;
25
+ var init_validation = __esm({
26
+ "src/constants/validation.ts"() {
27
+ "use strict";
28
+ EMAIL_REGEX = /^[a-zA-Z0-9.!#$%&'*+/=?^_`{|}~-]+@[a-zA-Z0-9-]+(\.[a-zA-Z0-9-]+)*\.[a-zA-Z]{2,}$/;
29
+ URL_REGEX = /^https?:\/\/(www\.)?[-a-zA-Z0-9@:%._\+~#=]{1,256}\.[a-zA-Z0-9()]{1,6}\b([-a-zA-Z0-9()@:%_\+.~#?&//=]*)$/;
30
+ PHONE_REGEX = /^[\+]?[(]?[0-9]{3}[)]?[-\s\.]?[0-9]{3}[-\s\.]?[0-9]{4,6}$/;
31
+ isValidEmail = (email) => {
32
+ return EMAIL_REGEX.test(email);
33
+ };
34
+ isValidUrl = (url) => {
35
+ if (/^(javascript|data|vbscript|file|about):/i.test(url)) {
36
+ return false;
37
+ }
38
+ return URL_REGEX.test(url);
39
+ };
40
+ DATE_REGEX = /^(0[1-9]|1[0-2])\/(0[1-9]|[12][0-9]|3[01])\/\d{4}$/;
41
+ isValidPhone = (phone) => {
42
+ return PHONE_REGEX.test(phone);
43
+ };
44
+ isValidDate = (date) => {
45
+ if (!DATE_REGEX.test(date)) {
46
+ return false;
47
+ }
48
+ const [month, day, year] = date.split("/").map(Number);
49
+ if (year < 1900 || year > 2100) {
50
+ return false;
51
+ }
52
+ if (month < 1 || month > 12) {
53
+ return false;
54
+ }
55
+ const daysInMonth = [31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31];
56
+ const isLeapYear = year % 4 === 0 && year % 100 !== 0 || year % 400 === 0;
57
+ if (isLeapYear && month === 2) {
58
+ daysInMonth[1] = 29;
59
+ }
60
+ if (day < 1 || day > daysInMonth[month - 1]) {
61
+ return false;
62
+ }
63
+ return true;
64
+ };
65
+ }
66
+ });
2
67
 
3
68
  // src/ui/button.tsx
4
69
  import React from "react";
@@ -150,7 +215,112 @@ Button.displayName = "Button";
150
215
  // src/ui/input.tsx
151
216
  import React2 from "react";
152
217
 
218
+ // src/lib/formatting.ts
219
+ var formatPhoneUS = (value) => {
220
+ const digits = value.replace(/\D/g, "");
221
+ const limited = digits.slice(0, 10);
222
+ if (limited.length === 0) return "";
223
+ if (limited.length <= 3) return `(${limited}`;
224
+ if (limited.length <= 6)
225
+ return `(${limited.slice(0, 3)}) ${limited.slice(3)}`;
226
+ return `(${limited.slice(0, 3)}) ${limited.slice(3, 6)}-${limited.slice(6)}`;
227
+ };
228
+ var formatPhoneIntl = (value) => {
229
+ let cleaned = value.replace(/[^\d+]/g, "");
230
+ if (!cleaned.startsWith("+")) {
231
+ cleaned = "+" + cleaned;
232
+ }
233
+ cleaned = cleaned.slice(0, 16);
234
+ const digits = cleaned.slice(1);
235
+ if (digits.length === 0) return "+";
236
+ if (digits.length <= 1) return `+${digits}`;
237
+ if (digits.length <= 4) return `+${digits.slice(0, 1)} (${digits.slice(1)}`;
238
+ if (digits.length <= 7)
239
+ return `+${digits.slice(0, 1)} (${digits.slice(1, 4)}) ${digits.slice(4)}`;
240
+ return `+${digits.slice(0, 1)} (${digits.slice(1, 4)}) ${digits.slice(4, 7)}-${digits.slice(7)}`;
241
+ };
242
+ var formatCreditCard = (value) => {
243
+ const digits = value.replace(/\D/g, "");
244
+ const limited = digits.slice(0, 16);
245
+ return limited.replace(/(\d{4})/g, "$1 ").trim();
246
+ };
247
+ var formatDate = (value) => {
248
+ const digits = value.replace(/\D/g, "");
249
+ const limited = digits.slice(0, 8);
250
+ if (limited.length === 0) return "";
251
+ if (limited.length <= 2) return limited;
252
+ if (limited.length <= 4)
253
+ return `${limited.slice(0, 2)}/${limited.slice(2)}`;
254
+ return `${limited.slice(0, 2)}/${limited.slice(2, 4)}/${limited.slice(4)}`;
255
+ };
256
+ var formatDateTime = (value) => {
257
+ const digits = value.replace(/\D/g, "");
258
+ const limited = digits.slice(0, 12);
259
+ if (limited.length === 0) return "";
260
+ if (limited.length <= 2) return limited;
261
+ if (limited.length <= 4)
262
+ return `${limited.slice(0, 2)}/${limited.slice(2)}`;
263
+ if (limited.length <= 8)
264
+ return `${limited.slice(0, 2)}/${limited.slice(2, 4)}/${limited.slice(4)}`;
265
+ const datePart = `${limited.slice(0, 2)}/${limited.slice(2, 4)}/${limited.slice(4, 8)}`;
266
+ if (limited.length <= 10) return `${datePart} ${limited.slice(8)}`;
267
+ let hours = parseInt(limited.slice(8, 10));
268
+ const minutes = limited.slice(10, 12);
269
+ const ampm = hours >= 12 ? "PM" : "AM";
270
+ hours = hours % 12 || 12;
271
+ return `${datePart} ${hours}:${minutes} ${ampm}`;
272
+ };
273
+ var getRawValue = (value, format) => {
274
+ if (format === "date") {
275
+ const digits = value.replace(/\D/g, "");
276
+ if (digits.length === 8) {
277
+ const month = digits.slice(0, 2);
278
+ const day = digits.slice(2, 4);
279
+ const year = digits.slice(4, 8);
280
+ return `${year}-${month}-${day}`;
281
+ }
282
+ return digits;
283
+ }
284
+ if (format === "datetime") {
285
+ const digits = value.replace(/\D/g, "");
286
+ if (digits.length === 12) {
287
+ const month = digits.slice(0, 2);
288
+ const day = digits.slice(2, 4);
289
+ const year = digits.slice(4, 8);
290
+ const hours = digits.slice(8, 10);
291
+ const minutes = digits.slice(10, 12);
292
+ const date = new Date(
293
+ parseInt(year),
294
+ parseInt(month) - 1,
295
+ parseInt(day),
296
+ parseInt(hours),
297
+ parseInt(minutes)
298
+ );
299
+ return date.toISOString();
300
+ }
301
+ return digits;
302
+ }
303
+ return value.replace(/\D/g, "");
304
+ };
305
+ var applyFormat = (value, format) => {
306
+ switch (format) {
307
+ case "phone":
308
+ return formatPhoneUS(value);
309
+ case "phone-intl":
310
+ return formatPhoneIntl(value);
311
+ case "credit-card":
312
+ return formatCreditCard(value);
313
+ case "date":
314
+ return formatDate(value);
315
+ case "datetime":
316
+ return formatDateTime(value);
317
+ default:
318
+ return value;
319
+ }
320
+ };
321
+
153
322
  // src/ui/hooks/useFormField.ts
323
+ init_validation();
154
324
  import { useEffect, useId as useId2, useRef as useRef2, useState as useState3 } from "react";
155
325
 
156
326
  // src/ui/form.tsx
@@ -738,10 +908,13 @@ function useFormField2(options) {
738
908
  return errorMessages?.required || "This field is required";
739
909
  }
740
910
  if (value) {
741
- if (type === "email" && !value.includes("@")) {
911
+ if (type === "email" && !EMAIL_REGEX.test(value)) {
742
912
  return errorMessages?.email || "Please enter a valid email address";
743
913
  }
744
914
  if (type === "url") {
915
+ if (/^(javascript|data|vbscript|file|about):/i.test(value)) {
916
+ return errorMessages?.url || "Invalid URL protocol";
917
+ }
745
918
  try {
746
919
  new URL(value);
747
920
  } catch {
@@ -795,10 +968,15 @@ function useFormField2(options) {
795
968
  return errorMessages?.required || "This field is required";
796
969
  }
797
970
  if (value) {
798
- if (type === "email" && !value.includes("@")) {
971
+ if (type === "email" && !EMAIL_REGEX.test(value)) {
799
972
  return errorMessages?.email || "Please enter a valid email address";
800
973
  }
801
974
  if (type === "url") {
975
+ if (/^(javascript|data|vbscript|file|about):/i.test(
976
+ value
977
+ )) {
978
+ return errorMessages?.url || "Invalid URL protocol";
979
+ }
802
980
  try {
803
981
  new URL(value);
804
982
  } catch {
@@ -912,12 +1090,31 @@ var Input = React2.forwardRef(
912
1090
  validate,
913
1091
  onValidationError,
914
1092
  pattern,
1093
+ format,
915
1094
  errorMessages,
916
1095
  ...props
917
1096
  }, ref) => {
1097
+ const [internalValue, setInternalValue] = React2.useState("");
1098
+ const dateValidate = React2.useCallback(
1099
+ async (value) => {
1100
+ if (validate) {
1101
+ const customError = await validate(value);
1102
+ if (customError) return customError;
1103
+ }
1104
+ if (format === "date" && value && value.length === 8) {
1105
+ const { isValidDate: isValidDate2 } = await Promise.resolve().then(() => (init_validation(), validation_exports));
1106
+ const formatted = `${value.slice(0, 2)}/${value.slice(2, 4)}/${value.slice(4, 8)}`;
1107
+ if (!isValidDate2(formatted)) {
1108
+ return errorMessages?.date || "Please enter a valid date";
1109
+ }
1110
+ }
1111
+ return void 0;
1112
+ },
1113
+ [validate, format, errorMessages]
1114
+ );
918
1115
  const {
919
1116
  fieldId,
920
- value: inputValue,
1117
+ value: hookValue,
921
1118
  error: inputError,
922
1119
  isDisabled,
923
1120
  isRequired,
@@ -939,19 +1136,71 @@ var Input = React2.forwardRef(
939
1136
  min: props.min,
940
1137
  max: props.max,
941
1138
  pattern,
942
- validate,
1139
+ validate: format === "date" ? dateValidate : validate,
943
1140
  onValidationError,
944
1141
  errorMessages,
945
1142
  idPrefix: "input"
946
1143
  });
1144
+ const inputValue = hookValue !== void 0 ? hookValue : internalValue;
1145
+ const [cursorPosition, setCursorPosition] = React2.useState(null);
947
1146
  const handleChange = (e) => {
948
- hookHandleChange(e.target.value);
1147
+ const input = e.target;
1148
+ const newValue = input.value;
1149
+ const cursorPos = input.selectionStart || 0;
1150
+ if (format && typeof format === "string") {
1151
+ const cleaned = newValue.replace(/\D/g, "");
1152
+ const formatted = applyFormat(cleaned, format);
1153
+ if (hookValue !== void 0) {
1154
+ hookHandleChange(cleaned);
1155
+ } else {
1156
+ setInternalValue(cleaned);
1157
+ }
1158
+ let formattedPos = 0;
1159
+ let digitCount = 0;
1160
+ const targetDigits = cleaned.slice(
1161
+ 0,
1162
+ Math.min(cleaned.length, cursorPos)
1163
+ );
1164
+ for (let i = 0; i < formatted.length && digitCount < targetDigits.length; i++) {
1165
+ if (/\d/.test(formatted[i])) {
1166
+ digitCount++;
1167
+ }
1168
+ formattedPos = i + 1;
1169
+ }
1170
+ setCursorPosition(formattedPos);
1171
+ } else {
1172
+ if (hookValue !== void 0) {
1173
+ hookHandleChange(newValue);
1174
+ } else {
1175
+ setInternalValue(newValue);
1176
+ }
1177
+ }
949
1178
  onChange?.(e);
950
1179
  };
951
1180
  const handleBlur = (e) => {
952
- hookHandleBlur(e.target.value);
1181
+ const value = e.target.value;
1182
+ const blurValue = format && typeof format === "string" ? value.replace(/\D/g, "") : value;
1183
+ hookHandleBlur(blurValue);
953
1184
  onBlur?.(e);
954
1185
  };
1186
+ const displayValue = React2.useMemo(() => {
1187
+ if (format && inputValue) {
1188
+ if (typeof format === "function") {
1189
+ return format(inputValue);
1190
+ }
1191
+ return applyFormat(inputValue, format);
1192
+ }
1193
+ return inputValue;
1194
+ }, [format, inputValue]);
1195
+ React2.useEffect(() => {
1196
+ if (cursorPosition !== null && internalRef.current) {
1197
+ internalRef.current.setSelectionRange(
1198
+ cursorPosition,
1199
+ cursorPosition
1200
+ );
1201
+ setCursorPosition(null);
1202
+ }
1203
+ }, [cursorPosition, displayValue]);
955
1204
  return /* @__PURE__ */ jsxs3(
956
1205
  "div",
957
1206
  {
@@ -984,7 +1233,7 @@ var Input = React2.forwardRef(
984
1233
  },
985
1234
  type,
986
1235
  id: fieldId,
987
- value: inputValue,
1236
+ value: displayValue,
988
1237
  onChange: handleChange,
989
1238
  onBlur: handleBlur,
990
1239
  disabled: isDisabled,
@@ -5183,6 +5432,9 @@ function useTheme() {
5183
5432
  }
5184
5433
  return context;
5185
5434
  }
5435
+
5436
+ // src/index.ts
5437
+ init_validation();
5186
5438
  export {
5187
5439
  Alert,
5188
5440
  Badge,
@@ -5200,6 +5452,7 @@ export {
5200
5452
  CheckboxGroup,
5201
5453
  CodeSnippet,
5202
5454
  Container,
5455
+ DATE_REGEX,
5203
5456
  Dialog,
5204
5457
  DialogContent,
5205
5458
  DialogDescription,
@@ -5208,6 +5461,7 @@ export {
5208
5461
  DialogTitle,
5209
5462
  Divider,
5210
5463
  Drawer,
5464
+ EMAIL_REGEX,
5211
5465
  EmptyState,
5212
5466
  Form,
5213
5467
  FormControl,
@@ -5216,6 +5470,7 @@ export {
5216
5470
  Input,
5217
5471
  NativeSelect,
5218
5472
  Nav,
5473
+ PHONE_REGEX,
5219
5474
  RadioGroup,
5220
5475
  Rating,
5221
5476
  SectionLayout,
@@ -5226,7 +5481,19 @@ export {
5226
5481
  Switch,
5227
5482
  Textarea,
5228
5483
  ThemeProvider,
5484
+ URL_REGEX,
5485
+ applyFormat,
5229
5486
  cn,
5487
+ formatCreditCard,
5488
+ formatDate,
5489
+ formatDateTime,
5490
+ formatPhoneIntl,
5491
+ formatPhoneUS,
5492
+ getRawValue,
5493
+ isValidDate,
5494
+ isValidEmail,
5495
+ isValidPhone,
5496
+ isValidUrl,
5230
5497
  themes_default as themes,
5231
5498
  useForm,
5232
5499
  useFormContext,