@xsolla/xui-date-picker 0.205.1 → 0.206.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/web/index.js CHANGED
@@ -34,18 +34,22 @@ __export(index_exports, {
34
34
  CalendarChips: () => import_xui_calendar2.CalendarChips,
35
35
  CalendarGrid: () => import_xui_calendar2.CalendarGrid,
36
36
  CalendarHeader: () => import_xui_calendar2.CalendarHeader,
37
+ DEFAULT_DATE_FORMAT: () => DEFAULT_DATE_FORMAT,
37
38
  DatePicker: () => DatePicker,
38
39
  DualCalendar: () => import_xui_calendar2.DualCalendar,
40
+ analyzeDateFormat: () => analyzeDateFormat,
39
41
  buildSegmentedValue: () => buildSegmentedValue,
40
42
  clampDatePart: () => clampDatePart,
41
43
  datePartsFromDate: () => datePartsFromDate,
42
44
  datePartsToDate: () => datePartsToDate,
43
45
  daysInMonth: () => daysInMonth,
44
- formatDate: () => formatDate,
46
+ describeDateFormatIssues: () => describeDateFormatIssues,
47
+ formatDate: () => import_xui_calendar2.formatDate,
45
48
  getDateSegments: () => getDateSegments,
46
49
  isDatePartsComplete: () => isDatePartsComplete,
47
50
  isSegmentComplete: () => isSegmentComplete,
48
51
  parseDateFormat: () => parseDateFormat,
52
+ resolveDateFormat: () => resolveDateFormat,
49
53
  stepDatePart: () => stepDatePart,
50
54
  stepDateSegment: () => stepDateSegment
51
55
  });
@@ -321,20 +325,11 @@ var isNative = false;
321
325
 
322
326
  // src/DatePicker.tsx
323
327
  var import_xui_input = require("@xsolla/xui-input");
324
- var import_date_fns2 = require("date-fns");
328
+ var import_date_fns = require("date-fns");
325
329
  var import_xui_calendar = require("@xsolla/xui-calendar");
326
330
  var import_xui_icons_base = require("@xsolla/xui-icons-base");
327
331
 
328
332
  // src/utils.ts
329
- var import_date_fns = require("date-fns");
330
- var locales = __toESM(require("date-fns/locale"));
331
- var defaultLocale = "enUS";
332
- function formatDate(date, formatStr, locale = defaultLocale) {
333
- const localeObj = locales[locale] || locales[defaultLocale];
334
- return (0, import_date_fns.format)(date, formatStr, {
335
- locale: localeObj
336
- });
337
- }
338
333
  var SEGMENT_WIDTH = {
339
334
  day: 2,
340
335
  month: 2,
@@ -345,21 +340,109 @@ var SEGMENT_TOKEN = {
345
340
  month: "mm",
346
341
  year: "yyyy"
347
342
  };
348
- function parseDateFormat(format2) {
343
+ var DEFAULT_DATE_FORMAT = "MM/dd/yyyy";
344
+ var SEGMENT_LETTER = {
345
+ d: "day",
346
+ M: "month",
347
+ y: "year"
348
+ };
349
+ var CASING_REPAIRS = { D: "d", Y: "y", m: "M" };
350
+ var SUPPORTED_TOKENS_HINT = "supported tokens are `d`/`dd` (day), `M`/`MM` (month) and `y`/`yyyy` (year); any other character is a literal separator. Textual and localised tokens (`MMM`, `MMMM`, `P`, `PP`) are not supported \u2014 the field renders numeric segments only.";
351
+ var ALL_SEGMENTS = ["day", "month", "year"];
352
+ var isLetter = (c) => /[A-Za-z]/.test(c);
353
+ var isSupportedRun = (letter, length) => {
354
+ if (letter === "y") return true;
355
+ if (letter === "d" || letter === "M") return length <= 2;
356
+ return false;
357
+ };
358
+ function analyzeDateFormat(format, fallback = DEFAULT_DATE_FORMAT) {
359
+ const issues = [];
360
+ const used = /* @__PURE__ */ new Set();
361
+ for (let i2 = 0; i2 < format.length; i2++) {
362
+ if (isLetter(format[i2])) used.add(format[i2]);
363
+ }
364
+ let repaired = "";
365
+ let hasUnsupported = false;
366
+ let i = 0;
367
+ while (i < format.length) {
368
+ const c = format[i];
369
+ if (!isLetter(c)) {
370
+ repaired += c;
371
+ i++;
372
+ continue;
373
+ }
374
+ let end = i;
375
+ while (end < format.length && format[end] === c) end++;
376
+ const run = format.slice(i, end);
377
+ i = end;
378
+ if (isSupportedRun(c, run.length)) {
379
+ repaired += run;
380
+ continue;
381
+ }
382
+ const repair = CASING_REPAIRS[c];
383
+ if (repair && !used.has(repair) && isSupportedRun(repair, run.length)) {
384
+ const fixed = repair.repeat(run.length);
385
+ issues.push({ token: run, kind: "casing", suggestion: fixed });
386
+ repaired += fixed;
387
+ continue;
388
+ }
389
+ issues.push({ token: run, kind: "unsupported" });
390
+ hasUnsupported = true;
391
+ repaired += run;
392
+ }
393
+ const present = new Set(
394
+ parseDateFormat(repaired).filter(
395
+ (t) => t.kind === "segment"
396
+ ).map((t) => t.type)
397
+ );
398
+ const missing = ALL_SEGMENTS.filter((type) => !present.has(type));
399
+ for (const type of missing) issues.push({ token: type, kind: "missing" });
400
+ const usable = !hasUnsupported && missing.length === 0;
401
+ return {
402
+ format: usable ? repaired : fallback,
403
+ usedFallback: !usable,
404
+ issues
405
+ };
406
+ }
407
+ function describeDateFormatIssues(format, analysis) {
408
+ const clauses = analysis.issues.map((issue) => {
409
+ if (issue.kind === "missing") {
410
+ return `it has no ${issue.token} segment, so the field can never be completed`;
411
+ }
412
+ if (issue.kind === "casing") {
413
+ return `\`${issue.token}\` is not a \`date-fns\` date token \u2014 use \`${issue.suggestion}\``;
414
+ }
415
+ return `\`${issue.token}\` is not a supported token`;
416
+ });
417
+ const resolution = analysis.usedFallback ? `Falling back to \`${analysis.format}\`.` : `Reading it as \`${analysis.format}\`.`;
418
+ return `[DatePicker] Invalid \`dateFormat\` \`${format}\`: ${clauses.join("; ")}. ${resolution} Note ${SUPPORTED_TOKENS_HINT}`;
419
+ }
420
+ var warnedFormats = /* @__PURE__ */ new Set();
421
+ function resolveDateFormat(format, fallback = DEFAULT_DATE_FORMAT) {
422
+ const analysis = analyzeDateFormat(format, fallback);
423
+ if (analysis.issues.length > 0) {
424
+ const key = `${format}|${fallback}`;
425
+ const isProduction = globalThis.process?.env?.NODE_ENV === "production";
426
+ if (!isProduction && !warnedFormats.has(key)) {
427
+ warnedFormats.add(key);
428
+ console.warn(describeDateFormatIssues(format, analysis));
429
+ }
430
+ }
431
+ return analysis.format;
432
+ }
433
+ function parseDateFormat(format) {
349
434
  const tokens = [];
350
435
  let i = 0;
351
- while (i < format2.length) {
352
- const c = format2[i];
353
- if (c === "d" || c === "M" || c === "y") {
354
- while (i < format2.length && format2[i] === c) i++;
355
- tokens.push({
356
- kind: "segment",
357
- type: c === "d" ? "day" : c === "M" ? "month" : "year"
358
- });
436
+ while (i < format.length) {
437
+ const c = format[i];
438
+ const type = SEGMENT_LETTER[c];
439
+ if (type) {
440
+ while (i < format.length && format[i] === c) i++;
441
+ tokens.push({ kind: "segment", type });
359
442
  } else {
360
443
  let text = "";
361
- while (i < format2.length && !"dMy".includes(format2[i])) {
362
- text += format2[i];
444
+ while (i < format.length && !SEGMENT_LETTER[format[i]]) {
445
+ text += format[i];
363
446
  i++;
364
447
  }
365
448
  tokens.push({ kind: "literal", text });
@@ -403,8 +486,8 @@ function stepDatePart(type, current, delta, parts) {
403
486
  const dim = daysInMonth(year, monthIndex);
404
487
  return ((current - 1 + delta) % dim + dim) % dim + 1;
405
488
  }
406
- function buildSegmentedValue(format2, parts, active) {
407
- const tokens = parseDateFormat(format2);
489
+ function buildSegmentedValue(format, parts, active) {
490
+ const tokens = parseDateFormat(format);
408
491
  let text = "";
409
492
  const ranges = [];
410
493
  for (const token of tokens) {
@@ -445,14 +528,14 @@ function isSegmentComplete(type, buffer) {
445
528
  function daysInMonth(year, monthIndex) {
446
529
  return new Date(year, monthIndex + 1, 0).getDate();
447
530
  }
448
- function getDateSegments(format2, value) {
531
+ function getDateSegments(format, value) {
449
532
  const segments = [];
450
533
  let fi = 0;
451
534
  let vi = 0;
452
- while (fi < format2.length && vi < value.length) {
453
- const fc = format2[fi];
535
+ while (fi < format.length && vi < value.length) {
536
+ const fc = format[fi];
454
537
  if (fc === "d" || fc === "M" || fc === "y") {
455
- while (fi < format2.length && format2[fi] === fc) fi++;
538
+ while (fi < format.length && format[fi] === fc) fi++;
456
539
  const start = vi;
457
540
  while (vi < value.length && value[vi] >= "0" && value[vi] <= "9") vi++;
458
541
  if (vi > start) {
@@ -497,7 +580,6 @@ function stepDateSegment(date, type, delta) {
497
580
  // src/DatePicker.tsx
498
581
  var import_jsx_runtime2 = require("react/jsx-runtime");
499
582
  var PLACEMENT_OFFSET = 4;
500
- var DEFAULT_DATE_FORMAT = "MM/dd/yyyy";
501
583
  var RANGE_SEPARATOR = /\s*[-–—]\s*/;
502
584
  var RANGE_DISPLAY_SEP = " - ";
503
585
  var CalendarFieldIcon = () => /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(import_xui_icons_base.Calendar, { size: "100%" });
@@ -599,6 +681,10 @@ var DatePicker = (0, import_react3.forwardRef)(
599
681
  const editable = !readOnly && !rest.disabled;
600
682
  const isDual = variant === "dual";
601
683
  const isRange = isDual || selectsRange;
684
+ const resolvedFormat = (0, import_react3.useMemo)(
685
+ () => resolveDateFormat(dateFormat),
686
+ [dateFormat]
687
+ );
602
688
  const propGroups = (0, import_react3.useCallback)(
603
689
  () => isRange ? [datePartsFromDate(startDate), datePartsFromDate(endDate)] : [datePartsFromDate(selectedDate)],
604
690
  [isRange, startDate, endDate, selectedDate]
@@ -646,7 +732,7 @@ var DatePicker = (0, import_react3.forwardRef)(
646
732
  };
647
733
  const composeField = () => {
648
734
  const build = (group) => buildSegmentedValue(
649
- dateFormat,
735
+ resolvedFormat,
650
736
  groups[group] ?? { day: null, month: null, year: null },
651
737
  edit && edit.group === group ? { type: edit.type, buffer: edit.buffer } : void 0
652
738
  );
@@ -893,8 +979,8 @@ var DatePicker = (0, import_react3.forwardRef)(
893
979
  const parseOne = (token) => {
894
980
  const trimmed = token.trim();
895
981
  if (!trimmed) return null;
896
- const parsed = (0, import_date_fns2.parse)(trimmed, dateFormat, /* @__PURE__ */ new Date());
897
- return (0, import_date_fns2.isValid)(parsed) ? parsed : null;
982
+ const parsed = (0, import_date_fns.parse)(trimmed, resolvedFormat, /* @__PURE__ */ new Date());
983
+ return (0, import_date_fns.isValid)(parsed) ? parsed : null;
898
984
  };
899
985
  const handlePaste = (e) => {
900
986
  onPasteProp?.(e);
@@ -910,7 +996,7 @@ var DatePicker = (0, import_react3.forwardRef)(
910
996
  const end = parseOne(endText ?? "");
911
997
  if (!start) {
912
998
  setParseError(
913
- `Enter a valid date range (${dateFormat} - ${dateFormat})`
999
+ `Enter a valid date range (${resolvedFormat} - ${resolvedFormat})`
914
1000
  );
915
1001
  return;
916
1002
  }
@@ -918,7 +1004,7 @@ var DatePicker = (0, import_react3.forwardRef)(
918
1004
  } else {
919
1005
  const date = parseOne(text);
920
1006
  if (!date) {
921
- setParseError(`Enter a valid date (${dateFormat})`);
1007
+ setParseError(`Enter a valid date (${resolvedFormat})`);
922
1008
  return;
923
1009
  }
924
1010
  next = [datePartsFromDate(date)];
@@ -1114,7 +1200,7 @@ var DatePicker = (0, import_react3.forwardRef)(
1114
1200
  ...rest,
1115
1201
  ref: inputRef,
1116
1202
  size,
1117
- placeholder: placeholder || (isRange ? "Enter a date range" : "Choose a date"),
1203
+ placeholder: placeholder || (isRange ? "Choose a date range" : "Choose a date"),
1118
1204
  value: inputValue,
1119
1205
  readOnly,
1120
1206
  onFocus: handleFieldFocus,
@@ -1162,18 +1248,22 @@ DatePicker.displayName = "DatePicker";
1162
1248
  CalendarChips,
1163
1249
  CalendarGrid,
1164
1250
  CalendarHeader,
1251
+ DEFAULT_DATE_FORMAT,
1165
1252
  DatePicker,
1166
1253
  DualCalendar,
1254
+ analyzeDateFormat,
1167
1255
  buildSegmentedValue,
1168
1256
  clampDatePart,
1169
1257
  datePartsFromDate,
1170
1258
  datePartsToDate,
1171
1259
  daysInMonth,
1260
+ describeDateFormatIssues,
1172
1261
  formatDate,
1173
1262
  getDateSegments,
1174
1263
  isDatePartsComplete,
1175
1264
  isSegmentComplete,
1176
1265
  parseDateFormat,
1266
+ resolveDateFormat,
1177
1267
  stepDatePart,
1178
1268
  stepDateSegment
1179
1269
  });