@rufous/ui 0.3.1 → 0.3.12

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/main.cjs CHANGED
@@ -3016,6 +3016,11 @@ var parseDisplay = (str, fmt = "MM/DD/YYYY") => {
3016
3016
  };
3017
3017
  var isoToDate = (iso) => {
3018
3018
  if (!iso) return null;
3019
+ if (iso.endsWith("Z") || /[+-]\d{2}:?\d{2}$/.test(iso)) {
3020
+ const d2 = new Date(iso);
3021
+ if (isNaN(d2.getTime())) return null;
3022
+ return new Date(d2.getFullYear(), d2.getMonth(), d2.getDate());
3023
+ }
3019
3024
  const [datePart] = iso.split("T");
3020
3025
  const [y, mo, d] = datePart.split("-").map(Number);
3021
3026
  if (!y || !mo || !d) return null;
@@ -3028,7 +3033,7 @@ var today = () => {
3028
3033
  var isSameDay = (a, b) => a.getFullYear() === b.getFullYear() && a.getMonth() === b.getMonth() && a.getDate() === b.getDate();
3029
3034
  var normaliseBoundary = (d) => {
3030
3035
  if (!d) return null;
3031
- const base = d instanceof Date ? d : isoToDate(typeof d === "string" ? d.split("T")[0] : d);
3036
+ const base = d instanceof Date ? d : isoToDate(d);
3032
3037
  if (!base) return null;
3033
3038
  return new Date(base.getFullYear(), base.getMonth(), base.getDate());
3034
3039
  };
@@ -3184,6 +3189,24 @@ var SpinnerPanel = ({
3184
3189
  },
3185
3190
  "PM"
3186
3191
  )));
3192
+ var formatViewsDisplay = (d, views) => {
3193
+ if (!views.includes("day") && !views.includes("month")) return String(d.getFullYear());
3194
+ if (!views.includes("day")) return `${MONTHS_SHORT[d.getMonth()]} ${d.getFullYear()}`;
3195
+ return "";
3196
+ };
3197
+ var parseYearDisplay = (str) => {
3198
+ const y = parseInt(str.trim(), 10);
3199
+ if (isNaN(y) || y < 1e3 || y > 9999) return null;
3200
+ return new Date(y, 0, 1);
3201
+ };
3202
+ var parseMonthYearDisplay = (str) => {
3203
+ const parts = str.trim().split(/\s+/);
3204
+ if (parts.length < 2) return null;
3205
+ const mm = parseMonthName(parts[0]);
3206
+ const yyyy = parseInt(parts[parts.length - 1], 10);
3207
+ if (mm < 0 || isNaN(yyyy) || yyyy < 1e3) return null;
3208
+ return new Date(yyyy, mm, 1);
3209
+ };
3187
3210
  var CalendarBody = ({
3188
3211
  viewMonth,
3189
3212
  viewYear,
@@ -3196,18 +3219,48 @@ var CalendarBody = ({
3196
3219
  onMonthSelect,
3197
3220
  onYearSelect,
3198
3221
  minDate,
3199
- maxDate
3222
+ maxDate,
3223
+ views,
3224
+ onFinalMonthSelect,
3225
+ onFinalYearSelect
3200
3226
  }) => {
3201
- const [pickerView, setPickerView] = (0, import_react21.useState)("calendar");
3202
- const handleMonthClick = () => setPickerView(pickerView === "month" ? "calendar" : "month");
3203
- const handleYearClick = () => setPickerView(pickerView === "year" ? "calendar" : "year");
3227
+ const hasDayView = views.includes("day");
3228
+ const hasMonthView = views.includes("month");
3229
+ const hasYearView = views.includes("year");
3230
+ const [pickerView, setPickerView] = (0, import_react21.useState)(() => {
3231
+ if (!hasDayView && !hasMonthView) return "year";
3232
+ if (!hasDayView) return "month";
3233
+ return "calendar";
3234
+ });
3235
+ const handleMonthClick = () => {
3236
+ if (!hasMonthView || !hasDayView) return;
3237
+ setPickerView((v) => v === "month" ? "calendar" : "month");
3238
+ };
3239
+ const handleYearClick = () => {
3240
+ if (!hasYearView) return;
3241
+ if (pickerView === "year") {
3242
+ setPickerView(hasDayView ? "calendar" : "month");
3243
+ } else {
3244
+ setPickerView("year");
3245
+ }
3246
+ };
3204
3247
  const handleMonthPick = (month) => {
3205
3248
  onMonthSelect(month);
3206
- setPickerView("calendar");
3249
+ if (hasDayView) {
3250
+ setPickerView("calendar");
3251
+ } else {
3252
+ onFinalMonthSelect?.(month, viewYear);
3253
+ }
3207
3254
  };
3208
3255
  const handleYearPick = (year) => {
3209
3256
  onYearSelect(year);
3210
- setPickerView("calendar");
3257
+ if (!hasDayView && !hasMonthView) {
3258
+ onFinalYearSelect?.(year);
3259
+ } else if (hasMonthView) {
3260
+ setPickerView("month");
3261
+ } else {
3262
+ setPickerView("calendar");
3263
+ }
3211
3264
  };
3212
3265
  const currentYear = todayDate.getFullYear();
3213
3266
  const yearStart = viewYear - 6;
@@ -3231,18 +3284,35 @@ var CalendarBody = ({
3231
3284
  return /* @__PURE__ */ import_react21.default.createElement(import_react21.default.Fragment, null, /* @__PURE__ */ import_react21.default.createElement("div", { className: "rf-date-picker__header" }, /* @__PURE__ */ import_react21.default.createElement("div", { className: "rf-date-picker__header-labels" }, /* @__PURE__ */ import_react21.default.createElement(
3232
3285
  "span",
3233
3286
  {
3234
- className: `rf-date-picker__month-label ${pickerView === "month" ? "rf-date-picker__month-label--active" : ""}`,
3287
+ className: [
3288
+ "rf-date-picker__month-label",
3289
+ pickerView === "month" ? "rf-date-picker__month-label--active" : "",
3290
+ !hasMonthView || !hasDayView ? "rf-date-picker__label--static" : ""
3291
+ ].filter(Boolean).join(" "),
3235
3292
  onClick: handleMonthClick
3236
3293
  },
3237
3294
  MONTHS[viewMonth]
3238
3295
  ), /* @__PURE__ */ import_react21.default.createElement(
3239
3296
  "span",
3240
3297
  {
3241
- className: `rf-date-picker__year-label ${pickerView === "year" ? "rf-date-picker__year-label--active" : ""}`,
3298
+ className: [
3299
+ "rf-date-picker__year-label",
3300
+ pickerView === "year" ? "rf-date-picker__year-label--active" : "",
3301
+ !hasYearView ? "rf-date-picker__label--static" : ""
3302
+ ].filter(Boolean).join(" "),
3242
3303
  onClick: handleYearClick
3243
3304
  },
3244
3305
  viewYear
3245
- )), /* @__PURE__ */ import_react21.default.createElement("div", { className: "rf-date-picker__nav" }, pickerView === "year" ? /* @__PURE__ */ import_react21.default.createElement(import_react21.default.Fragment, null, /* @__PURE__ */ import_react21.default.createElement("button", { type: "button", className: "rf-date-picker__nav-btn", onClick: () => onYearSelect(viewYear - 16), "aria-label": "Previous years" }, "\u2039"), /* @__PURE__ */ import_react21.default.createElement("button", { type: "button", className: "rf-date-picker__nav-btn", onClick: () => onYearSelect(viewYear + 16), "aria-label": "Next years" }, "\u203A")) : /* @__PURE__ */ import_react21.default.createElement(import_react21.default.Fragment, null, /* @__PURE__ */ import_react21.default.createElement("button", { type: "button", className: "rf-date-picker__nav-btn", onClick: onPrev, disabled: isPrevDisabled, "aria-label": "Previous month" }, "\u2039"), /* @__PURE__ */ import_react21.default.createElement("button", { type: "button", className: "rf-date-picker__nav-btn", onClick: onNext, disabled: isNextDisabled, "aria-label": "Next month" }, "\u203A")))), pickerView === "month" && /* @__PURE__ */ import_react21.default.createElement("div", { className: "rf-date-picker__month-grid" }, MONTHS_SHORT.map((m, idx) => {
3306
+ )), /* @__PURE__ */ import_react21.default.createElement("div", { className: "rf-date-picker__nav" }, pickerView === "year" ? (
3307
+ // Year page navigation
3308
+ /* @__PURE__ */ import_react21.default.createElement(import_react21.default.Fragment, null, /* @__PURE__ */ import_react21.default.createElement("button", { type: "button", className: "rf-date-picker__nav-btn", onClick: () => onYearSelect(viewYear - 16), "aria-label": "Previous years" }, "\u2039"), /* @__PURE__ */ import_react21.default.createElement("button", { type: "button", className: "rf-date-picker__nav-btn", onClick: () => onYearSelect(viewYear + 16), "aria-label": "Next years" }, "\u203A"))
3309
+ ) : pickerView === "month" && !hasDayView ? (
3310
+ // Month-only or month+year mode: ‹ › change year
3311
+ /* @__PURE__ */ import_react21.default.createElement(import_react21.default.Fragment, null, /* @__PURE__ */ import_react21.default.createElement("button", { type: "button", className: "rf-date-picker__nav-btn", onClick: () => onYearSelect(viewYear - 1), "aria-label": "Previous year" }, "\u2039"), /* @__PURE__ */ import_react21.default.createElement("button", { type: "button", className: "rf-date-picker__nav-btn", onClick: () => onYearSelect(viewYear + 1), "aria-label": "Next year" }, "\u203A"))
3312
+ ) : (
3313
+ // Normal month navigation
3314
+ /* @__PURE__ */ import_react21.default.createElement(import_react21.default.Fragment, null, /* @__PURE__ */ import_react21.default.createElement("button", { type: "button", className: "rf-date-picker__nav-btn", onClick: onPrev, disabled: isPrevDisabled, "aria-label": "Previous month" }, "\u2039"), /* @__PURE__ */ import_react21.default.createElement("button", { type: "button", className: "rf-date-picker__nav-btn", onClick: onNext, disabled: isNextDisabled, "aria-label": "Next month" }, "\u203A"))
3315
+ ))), pickerView === "month" && /* @__PURE__ */ import_react21.default.createElement("div", { className: "rf-date-picker__month-grid" }, MONTHS_SHORT.map((m, idx) => {
3246
3316
  const monthDisabled = isMonthDisabled(idx);
3247
3317
  return /* @__PURE__ */ import_react21.default.createElement(
3248
3318
  "button",
@@ -3278,7 +3348,7 @@ var CalendarBody = ({
3278
3348
  },
3279
3349
  y
3280
3350
  );
3281
- })), pickerView === "calendar" && /* @__PURE__ */ import_react21.default.createElement(import_react21.default.Fragment, null, /* @__PURE__ */ import_react21.default.createElement("div", { className: "rf-date-picker__weekdays" }, WEEKDAYS.map((w) => /* @__PURE__ */ import_react21.default.createElement("div", { key: w, className: "rf-date-picker__weekday" }, w))), /* @__PURE__ */ import_react21.default.createElement("div", { className: "rf-date-picker__grid" }, dayCells.map((day, idx) => {
3351
+ })), pickerView === "calendar" && hasDayView && /* @__PURE__ */ import_react21.default.createElement(import_react21.default.Fragment, null, /* @__PURE__ */ import_react21.default.createElement("div", { className: "rf-date-picker__weekdays" }, WEEKDAYS.map((w) => /* @__PURE__ */ import_react21.default.createElement("div", { key: w, className: "rf-date-picker__weekday" }, w))), /* @__PURE__ */ import_react21.default.createElement("div", { className: "rf-date-picker__grid" }, dayCells.map((day, idx) => {
3282
3352
  if (day === null) return /* @__PURE__ */ import_react21.default.createElement("div", { key: `e-${idx}`, className: "rf-date-picker__day rf-date-picker__day--empty" });
3283
3353
  const cellDate = new Date(viewYear, viewMonth, day);
3284
3354
  const isSelected = selectedDate ? isSameDay(cellDate, selectedDate) : false;
@@ -3321,8 +3391,12 @@ var DateField = ({
3321
3391
  placeholder,
3322
3392
  className = "",
3323
3393
  style,
3324
- sx
3394
+ sx,
3395
+ views: viewsProp
3325
3396
  }) => {
3397
+ const views = viewsProp ?? ["day", "month", "year"];
3398
+ const hasDayView = views.includes("day");
3399
+ const hasMonthView = views.includes("month");
3326
3400
  const minDate = normaliseBoundary(minDateProp);
3327
3401
  const maxDate = normaliseBoundary(maxDateProp);
3328
3402
  const sxClass = useSx(sx);
@@ -3352,6 +3426,8 @@ var DateField = ({
3352
3426
  if (!value) return "";
3353
3427
  const d = isoToDate(value);
3354
3428
  if (!d) return "";
3429
+ const viewsStr = formatViewsDisplay(d, views);
3430
+ if (viewsStr) return viewsStr;
3355
3431
  let str = formatDisplay(d, dateFormat);
3356
3432
  if (isDatetimeType(type)) {
3357
3433
  const t = parseTimeFromISO(value);
@@ -3382,15 +3458,20 @@ var DateField = ({
3382
3458
  if (d) {
3383
3459
  setViewYear(d.getFullYear());
3384
3460
  setViewMonth(d.getMonth());
3385
- let str = formatDisplay(d, dateFormat);
3386
- if (isDatetimeType(type)) {
3387
- const t = parseTimeFromISO(value);
3388
- setHour(t.h);
3389
- setMinute(t.m);
3390
- setAmpm(t.ampm);
3391
- str += " " + formatTimeDisplay(t.h, t.m, t.ampm);
3461
+ const viewsStr = formatViewsDisplay(d, views);
3462
+ if (viewsStr) {
3463
+ setInputStr(viewsStr);
3464
+ } else {
3465
+ let str = formatDisplay(d, dateFormat);
3466
+ if (isDatetimeType(type)) {
3467
+ const t = parseTimeFromISO(value);
3468
+ setHour(t.h);
3469
+ setMinute(t.m);
3470
+ setAmpm(t.ampm);
3471
+ str += " " + formatTimeDisplay(t.h, t.m, t.ampm);
3472
+ }
3473
+ setInputStr(str);
3392
3474
  }
3393
- setInputStr(str);
3394
3475
  }
3395
3476
  }, [value, type]);
3396
3477
  (0, import_react21.useEffect)(() => {
@@ -3411,11 +3492,17 @@ var DateField = ({
3411
3492
  onChange?.("");
3412
3493
  return;
3413
3494
  }
3414
- let str = formatDisplay(d, dateFormat);
3415
- if (isDatetimeType(type)) str += " " + formatTimeDisplay(h, m, ap);
3495
+ const viewsStr = formatViewsDisplay(d, views);
3496
+ let str;
3497
+ if (viewsStr) {
3498
+ str = viewsStr;
3499
+ } else {
3500
+ str = formatDisplay(d, dateFormat);
3501
+ if (isDatetimeType(type)) str += " " + formatTimeDisplay(h, m, ap);
3502
+ }
3416
3503
  setInputStr(str);
3417
3504
  onChange?.(buildISO(d, type, h, m, ap));
3418
- }, [type, onChange, dateFormat]);
3505
+ }, [type, onChange, dateFormat, views]);
3419
3506
  const isOutOfRange = (d) => (minDate ? isBeforeDay(d, minDate) : false) || (maxDate ? isAfterDay(d, maxDate) : false);
3420
3507
  const handleDayClick = (day) => {
3421
3508
  const d = new Date(viewYear, viewMonth, day);
@@ -3429,49 +3516,102 @@ var DateField = ({
3429
3516
  };
3430
3517
  const handleToday = () => {
3431
3518
  const t = today();
3432
- if (isOutOfRange(t)) return;
3433
- setViewYear(t.getFullYear());
3434
- setViewMonth(t.getMonth());
3435
- commitDate(t, hour, minute, ampm);
3436
- if (type === "date") setOpen(false);
3519
+ if (!hasDayView && !hasMonthView) {
3520
+ const d = new Date(t.getFullYear(), 0, 1);
3521
+ if (isOutOfRange(d)) return;
3522
+ setViewYear(t.getFullYear());
3523
+ commitDate(d, hour, minute, ampm);
3524
+ setOpen(false);
3525
+ } else if (!hasDayView) {
3526
+ const d = new Date(t.getFullYear(), t.getMonth(), 1);
3527
+ if (isOutOfRange(d)) return;
3528
+ setViewYear(t.getFullYear());
3529
+ setViewMonth(t.getMonth());
3530
+ commitDate(d, hour, minute, ampm);
3531
+ setOpen(false);
3532
+ } else {
3533
+ if (isOutOfRange(t)) return;
3534
+ setViewYear(t.getFullYear());
3535
+ setViewMonth(t.getMonth());
3536
+ commitDate(t, hour, minute, ampm);
3537
+ if (type === "date") setOpen(false);
3538
+ }
3437
3539
  };
3540
+ const handleFinalMonthSelect = (0, import_react21.useCallback)((month, year) => {
3541
+ const d = new Date(year, month, 1);
3542
+ if (isOutOfRange(d)) return;
3543
+ setSelectedDate(d);
3544
+ setViewYear(year);
3545
+ setViewMonth(month);
3546
+ setInputStr(`${MONTHS_SHORT[month]} ${year}`);
3547
+ onChange?.(buildISO(d, type, hour, minute, ampm));
3548
+ setOpen(false);
3549
+ }, [isOutOfRange, onChange, type, hour, minute, ampm]);
3550
+ const handleFinalYearSelect = (0, import_react21.useCallback)((year) => {
3551
+ const d = new Date(year, 0, 1);
3552
+ if (isOutOfRange(d)) return;
3553
+ setSelectedDate(d);
3554
+ setViewYear(year);
3555
+ setInputStr(String(year));
3556
+ onChange?.(buildISO(d, type, hour, minute, ampm));
3557
+ setOpen(false);
3558
+ }, [isOutOfRange, onChange, type, hour, minute, ampm]);
3438
3559
  const handleClear = () => commitDate(null, hour, minute, ampm);
3439
3560
  const handleInputChange = (e) => {
3440
3561
  const raw = e.target.value;
3441
3562
  setInputStr(raw);
3442
- const dateWordCount = getDateWordCount(dateFormat);
3443
- const words = raw.split(" ");
3444
- const datePart = words.slice(0, dateWordCount).join(" ");
3445
- const timeParts = words.slice(dateWordCount);
3446
- const parsed = parseDisplay(datePart, dateFormat);
3447
- if (parsed && !isOutOfRange(parsed)) {
3448
- setSelectedDate(parsed);
3449
- setViewYear(parsed.getFullYear());
3450
- setViewMonth(parsed.getMonth());
3451
- let h = hour, m = minute, ap = ampm;
3452
- if (isDatetimeType(type) && timeParts.length >= 2) {
3453
- const timePart = timeParts[0];
3454
- const periodPart = timeParts[1]?.toUpperCase();
3455
- if (timePart?.includes(":")) {
3456
- const [hStr, mStr] = timePart.split(":");
3457
- const parsedH = parseInt(hStr, 10);
3458
- const parsedM = parseInt(mStr, 10);
3459
- if (!isNaN(parsedH) && parsedH >= 1 && parsedH <= 12) {
3460
- h = parsedH;
3461
- setHour(h);
3563
+ let parsed = null;
3564
+ if (!hasDayView && !hasMonthView) {
3565
+ parsed = parseYearDisplay(raw);
3566
+ } else if (!hasDayView) {
3567
+ parsed = parseMonthYearDisplay(raw);
3568
+ } else {
3569
+ const dateWordCount = getDateWordCount(dateFormat);
3570
+ const words = raw.split(" ");
3571
+ const datePart = words.slice(0, dateWordCount).join(" ");
3572
+ const timeParts = words.slice(dateWordCount);
3573
+ parsed = parseDisplay(datePart, dateFormat);
3574
+ if (parsed && !isOutOfRange(parsed)) {
3575
+ setSelectedDate(parsed);
3576
+ setViewYear(parsed.getFullYear());
3577
+ setViewMonth(parsed.getMonth());
3578
+ let h = hour, m = minute, ap = ampm;
3579
+ if (isDatetimeType(type) && timeParts.length >= 2) {
3580
+ const timePart = timeParts[0];
3581
+ const periodPart = timeParts[1]?.toUpperCase();
3582
+ if (timePart?.includes(":")) {
3583
+ const [hStr, mStr] = timePart.split(":");
3584
+ const parsedH = parseInt(hStr, 10);
3585
+ const parsedM = parseInt(mStr, 10);
3586
+ if (!isNaN(parsedH) && parsedH >= 1 && parsedH <= 12) {
3587
+ h = parsedH;
3588
+ setHour(h);
3589
+ }
3590
+ if (!isNaN(parsedM) && parsedM >= 0 && parsedM <= 59) {
3591
+ m = parsedM;
3592
+ setMinute(m);
3593
+ }
3462
3594
  }
3463
- if (!isNaN(parsedM) && parsedM >= 0 && parsedM <= 59) {
3464
- m = parsedM;
3465
- setMinute(m);
3595
+ if (periodPart === "AM" || periodPart === "PM") {
3596
+ ap = periodPart;
3597
+ setAmpm(ap);
3466
3598
  }
3467
3599
  }
3468
- if (periodPart === "AM" || periodPart === "PM") {
3469
- ap = periodPart;
3470
- setAmpm(ap);
3471
- }
3600
+ isInternalChange.current = true;
3601
+ onChange?.(buildISO(parsed, type, h, m, ap));
3602
+ } else if (!raw) {
3603
+ setSelectedDate(null);
3604
+ isInternalChange.current = true;
3605
+ onChange?.("");
3472
3606
  }
3607
+ return;
3608
+ }
3609
+ if (parsed && !isOutOfRange(parsed)) {
3610
+ setSelectedDate(parsed);
3611
+ setViewYear(parsed.getFullYear());
3612
+ setViewMonth(parsed.getMonth());
3473
3613
  isInternalChange.current = true;
3474
- onChange?.(buildISO(parsed, type, h, m, ap));
3614
+ onChange?.(buildISO(parsed, type, hour, minute, ampm));
3475
3615
  } else if (!raw) {
3476
3616
  setSelectedDate(null);
3477
3617
  isInternalChange.current = true;
@@ -3657,7 +3797,10 @@ var DateField = ({
3657
3797
  onMonthSelect: setViewMonth,
3658
3798
  onYearSelect: setViewYear,
3659
3799
  minDate,
3660
- maxDate
3800
+ maxDate,
3801
+ views,
3802
+ onFinalMonthSelect: handleFinalMonthSelect,
3803
+ onFinalYearSelect: handleFinalYearSelect
3661
3804
  }
3662
3805
  ), type === "datetime" && /* @__PURE__ */ import_react21.default.createElement("div", { className: "rf-date-picker__time-section" }, /* @__PURE__ */ import_react21.default.createElement("div", { className: "rf-date-picker__time-label" }, "Time"), /* @__PURE__ */ import_react21.default.createElement(
3663
3806
  SpinnerPanel,
package/dist/main.css CHANGED
@@ -1344,6 +1344,13 @@ pre {
1344
1344
  background-color: rgba(164, 27, 6, 0.1);
1345
1345
  color: #a41b06;
1346
1346
  }
1347
+ .rf-date-picker__label--static {
1348
+ cursor: default !important;
1349
+ pointer-events: none;
1350
+ }
1351
+ .rf-date-picker__label--static:hover {
1352
+ background-color: transparent !important;
1353
+ }
1347
1354
  .rf-date-picker__nav {
1348
1355
  display: flex;
1349
1356
  align-items: center;
package/dist/main.d.cts CHANGED
@@ -658,6 +658,20 @@ interface DateFieldProps {
658
658
  placeholder?: string;
659
659
  className?: string;
660
660
  style?: CSSProperties;
661
+ /**
662
+ * Which selection panels are available in the picker.
663
+ * - `"day"` — day calendar (default)
664
+ * - `"month"` — month grid
665
+ * - `"year"` — year grid
666
+ *
667
+ * Common patterns:
668
+ * - `["month", "year"]` — month+year picker, no day calendar
669
+ * - `["year"]` — year-only picker
670
+ * - `["month"]` — month picker (‹ › navigate years)
671
+ *
672
+ * Defaults to `["day", "month", "year"]` (full picker).
673
+ */
674
+ views?: ("day" | "month" | "year")[];
661
675
  /** Scoped style overrides. Supports nested CSS selectors with & */
662
676
  sx?: SxProp;
663
677
  }
package/dist/main.d.ts CHANGED
@@ -658,6 +658,20 @@ interface DateFieldProps {
658
658
  placeholder?: string;
659
659
  className?: string;
660
660
  style?: CSSProperties;
661
+ /**
662
+ * Which selection panels are available in the picker.
663
+ * - `"day"` — day calendar (default)
664
+ * - `"month"` — month grid
665
+ * - `"year"` — year grid
666
+ *
667
+ * Common patterns:
668
+ * - `["month", "year"]` — month+year picker, no day calendar
669
+ * - `["year"]` — year-only picker
670
+ * - `["month"]` — month picker (‹ › navigate years)
671
+ *
672
+ * Defaults to `["day", "month", "year"]` (full picker).
673
+ */
674
+ views?: ("day" | "month" | "year")[];
661
675
  /** Scoped style overrides. Supports nested CSS selectors with & */
662
676
  sx?: SxProp;
663
677
  }
package/dist/main.js CHANGED
@@ -2867,6 +2867,11 @@ var parseDisplay = (str, fmt = "MM/DD/YYYY") => {
2867
2867
  };
2868
2868
  var isoToDate = (iso) => {
2869
2869
  if (!iso) return null;
2870
+ if (iso.endsWith("Z") || /[+-]\d{2}:?\d{2}$/.test(iso)) {
2871
+ const d2 = new Date(iso);
2872
+ if (isNaN(d2.getTime())) return null;
2873
+ return new Date(d2.getFullYear(), d2.getMonth(), d2.getDate());
2874
+ }
2870
2875
  const [datePart] = iso.split("T");
2871
2876
  const [y, mo, d] = datePart.split("-").map(Number);
2872
2877
  if (!y || !mo || !d) return null;
@@ -2879,7 +2884,7 @@ var today = () => {
2879
2884
  var isSameDay = (a, b) => a.getFullYear() === b.getFullYear() && a.getMonth() === b.getMonth() && a.getDate() === b.getDate();
2880
2885
  var normaliseBoundary = (d) => {
2881
2886
  if (!d) return null;
2882
- const base = d instanceof Date ? d : isoToDate(typeof d === "string" ? d.split("T")[0] : d);
2887
+ const base = d instanceof Date ? d : isoToDate(d);
2883
2888
  if (!base) return null;
2884
2889
  return new Date(base.getFullYear(), base.getMonth(), base.getDate());
2885
2890
  };
@@ -3035,6 +3040,24 @@ var SpinnerPanel = ({
3035
3040
  },
3036
3041
  "PM"
3037
3042
  )));
3043
+ var formatViewsDisplay = (d, views) => {
3044
+ if (!views.includes("day") && !views.includes("month")) return String(d.getFullYear());
3045
+ if (!views.includes("day")) return `${MONTHS_SHORT[d.getMonth()]} ${d.getFullYear()}`;
3046
+ return "";
3047
+ };
3048
+ var parseYearDisplay = (str) => {
3049
+ const y = parseInt(str.trim(), 10);
3050
+ if (isNaN(y) || y < 1e3 || y > 9999) return null;
3051
+ return new Date(y, 0, 1);
3052
+ };
3053
+ var parseMonthYearDisplay = (str) => {
3054
+ const parts = str.trim().split(/\s+/);
3055
+ if (parts.length < 2) return null;
3056
+ const mm = parseMonthName(parts[0]);
3057
+ const yyyy = parseInt(parts[parts.length - 1], 10);
3058
+ if (mm < 0 || isNaN(yyyy) || yyyy < 1e3) return null;
3059
+ return new Date(yyyy, mm, 1);
3060
+ };
3038
3061
  var CalendarBody = ({
3039
3062
  viewMonth,
3040
3063
  viewYear,
@@ -3047,18 +3070,48 @@ var CalendarBody = ({
3047
3070
  onMonthSelect,
3048
3071
  onYearSelect,
3049
3072
  minDate,
3050
- maxDate
3073
+ maxDate,
3074
+ views,
3075
+ onFinalMonthSelect,
3076
+ onFinalYearSelect
3051
3077
  }) => {
3052
- const [pickerView, setPickerView] = useState7("calendar");
3053
- const handleMonthClick = () => setPickerView(pickerView === "month" ? "calendar" : "month");
3054
- const handleYearClick = () => setPickerView(pickerView === "year" ? "calendar" : "year");
3078
+ const hasDayView = views.includes("day");
3079
+ const hasMonthView = views.includes("month");
3080
+ const hasYearView = views.includes("year");
3081
+ const [pickerView, setPickerView] = useState7(() => {
3082
+ if (!hasDayView && !hasMonthView) return "year";
3083
+ if (!hasDayView) return "month";
3084
+ return "calendar";
3085
+ });
3086
+ const handleMonthClick = () => {
3087
+ if (!hasMonthView || !hasDayView) return;
3088
+ setPickerView((v) => v === "month" ? "calendar" : "month");
3089
+ };
3090
+ const handleYearClick = () => {
3091
+ if (!hasYearView) return;
3092
+ if (pickerView === "year") {
3093
+ setPickerView(hasDayView ? "calendar" : "month");
3094
+ } else {
3095
+ setPickerView("year");
3096
+ }
3097
+ };
3055
3098
  const handleMonthPick = (month) => {
3056
3099
  onMonthSelect(month);
3057
- setPickerView("calendar");
3100
+ if (hasDayView) {
3101
+ setPickerView("calendar");
3102
+ } else {
3103
+ onFinalMonthSelect?.(month, viewYear);
3104
+ }
3058
3105
  };
3059
3106
  const handleYearPick = (year) => {
3060
3107
  onYearSelect(year);
3061
- setPickerView("calendar");
3108
+ if (!hasDayView && !hasMonthView) {
3109
+ onFinalYearSelect?.(year);
3110
+ } else if (hasMonthView) {
3111
+ setPickerView("month");
3112
+ } else {
3113
+ setPickerView("calendar");
3114
+ }
3062
3115
  };
3063
3116
  const currentYear = todayDate.getFullYear();
3064
3117
  const yearStart = viewYear - 6;
@@ -3082,18 +3135,35 @@ var CalendarBody = ({
3082
3135
  return /* @__PURE__ */ React72.createElement(React72.Fragment, null, /* @__PURE__ */ React72.createElement("div", { className: "rf-date-picker__header" }, /* @__PURE__ */ React72.createElement("div", { className: "rf-date-picker__header-labels" }, /* @__PURE__ */ React72.createElement(
3083
3136
  "span",
3084
3137
  {
3085
- className: `rf-date-picker__month-label ${pickerView === "month" ? "rf-date-picker__month-label--active" : ""}`,
3138
+ className: [
3139
+ "rf-date-picker__month-label",
3140
+ pickerView === "month" ? "rf-date-picker__month-label--active" : "",
3141
+ !hasMonthView || !hasDayView ? "rf-date-picker__label--static" : ""
3142
+ ].filter(Boolean).join(" "),
3086
3143
  onClick: handleMonthClick
3087
3144
  },
3088
3145
  MONTHS[viewMonth]
3089
3146
  ), /* @__PURE__ */ React72.createElement(
3090
3147
  "span",
3091
3148
  {
3092
- className: `rf-date-picker__year-label ${pickerView === "year" ? "rf-date-picker__year-label--active" : ""}`,
3149
+ className: [
3150
+ "rf-date-picker__year-label",
3151
+ pickerView === "year" ? "rf-date-picker__year-label--active" : "",
3152
+ !hasYearView ? "rf-date-picker__label--static" : ""
3153
+ ].filter(Boolean).join(" "),
3093
3154
  onClick: handleYearClick
3094
3155
  },
3095
3156
  viewYear
3096
- )), /* @__PURE__ */ React72.createElement("div", { className: "rf-date-picker__nav" }, pickerView === "year" ? /* @__PURE__ */ React72.createElement(React72.Fragment, null, /* @__PURE__ */ React72.createElement("button", { type: "button", className: "rf-date-picker__nav-btn", onClick: () => onYearSelect(viewYear - 16), "aria-label": "Previous years" }, "\u2039"), /* @__PURE__ */ React72.createElement("button", { type: "button", className: "rf-date-picker__nav-btn", onClick: () => onYearSelect(viewYear + 16), "aria-label": "Next years" }, "\u203A")) : /* @__PURE__ */ React72.createElement(React72.Fragment, null, /* @__PURE__ */ React72.createElement("button", { type: "button", className: "rf-date-picker__nav-btn", onClick: onPrev, disabled: isPrevDisabled, "aria-label": "Previous month" }, "\u2039"), /* @__PURE__ */ React72.createElement("button", { type: "button", className: "rf-date-picker__nav-btn", onClick: onNext, disabled: isNextDisabled, "aria-label": "Next month" }, "\u203A")))), pickerView === "month" && /* @__PURE__ */ React72.createElement("div", { className: "rf-date-picker__month-grid" }, MONTHS_SHORT.map((m, idx) => {
3157
+ )), /* @__PURE__ */ React72.createElement("div", { className: "rf-date-picker__nav" }, pickerView === "year" ? (
3158
+ // Year page navigation
3159
+ /* @__PURE__ */ React72.createElement(React72.Fragment, null, /* @__PURE__ */ React72.createElement("button", { type: "button", className: "rf-date-picker__nav-btn", onClick: () => onYearSelect(viewYear - 16), "aria-label": "Previous years" }, "\u2039"), /* @__PURE__ */ React72.createElement("button", { type: "button", className: "rf-date-picker__nav-btn", onClick: () => onYearSelect(viewYear + 16), "aria-label": "Next years" }, "\u203A"))
3160
+ ) : pickerView === "month" && !hasDayView ? (
3161
+ // Month-only or month+year mode: ‹ › change year
3162
+ /* @__PURE__ */ React72.createElement(React72.Fragment, null, /* @__PURE__ */ React72.createElement("button", { type: "button", className: "rf-date-picker__nav-btn", onClick: () => onYearSelect(viewYear - 1), "aria-label": "Previous year" }, "\u2039"), /* @__PURE__ */ React72.createElement("button", { type: "button", className: "rf-date-picker__nav-btn", onClick: () => onYearSelect(viewYear + 1), "aria-label": "Next year" }, "\u203A"))
3163
+ ) : (
3164
+ // Normal month navigation
3165
+ /* @__PURE__ */ React72.createElement(React72.Fragment, null, /* @__PURE__ */ React72.createElement("button", { type: "button", className: "rf-date-picker__nav-btn", onClick: onPrev, disabled: isPrevDisabled, "aria-label": "Previous month" }, "\u2039"), /* @__PURE__ */ React72.createElement("button", { type: "button", className: "rf-date-picker__nav-btn", onClick: onNext, disabled: isNextDisabled, "aria-label": "Next month" }, "\u203A"))
3166
+ ))), pickerView === "month" && /* @__PURE__ */ React72.createElement("div", { className: "rf-date-picker__month-grid" }, MONTHS_SHORT.map((m, idx) => {
3097
3167
  const monthDisabled = isMonthDisabled(idx);
3098
3168
  return /* @__PURE__ */ React72.createElement(
3099
3169
  "button",
@@ -3129,7 +3199,7 @@ var CalendarBody = ({
3129
3199
  },
3130
3200
  y
3131
3201
  );
3132
- })), pickerView === "calendar" && /* @__PURE__ */ React72.createElement(React72.Fragment, null, /* @__PURE__ */ React72.createElement("div", { className: "rf-date-picker__weekdays" }, WEEKDAYS.map((w) => /* @__PURE__ */ React72.createElement("div", { key: w, className: "rf-date-picker__weekday" }, w))), /* @__PURE__ */ React72.createElement("div", { className: "rf-date-picker__grid" }, dayCells.map((day, idx) => {
3202
+ })), pickerView === "calendar" && hasDayView && /* @__PURE__ */ React72.createElement(React72.Fragment, null, /* @__PURE__ */ React72.createElement("div", { className: "rf-date-picker__weekdays" }, WEEKDAYS.map((w) => /* @__PURE__ */ React72.createElement("div", { key: w, className: "rf-date-picker__weekday" }, w))), /* @__PURE__ */ React72.createElement("div", { className: "rf-date-picker__grid" }, dayCells.map((day, idx) => {
3133
3203
  if (day === null) return /* @__PURE__ */ React72.createElement("div", { key: `e-${idx}`, className: "rf-date-picker__day rf-date-picker__day--empty" });
3134
3204
  const cellDate = new Date(viewYear, viewMonth, day);
3135
3205
  const isSelected = selectedDate ? isSameDay(cellDate, selectedDate) : false;
@@ -3172,8 +3242,12 @@ var DateField = ({
3172
3242
  placeholder,
3173
3243
  className = "",
3174
3244
  style,
3175
- sx
3245
+ sx,
3246
+ views: viewsProp
3176
3247
  }) => {
3248
+ const views = viewsProp ?? ["day", "month", "year"];
3249
+ const hasDayView = views.includes("day");
3250
+ const hasMonthView = views.includes("month");
3177
3251
  const minDate = normaliseBoundary(minDateProp);
3178
3252
  const maxDate = normaliseBoundary(maxDateProp);
3179
3253
  const sxClass = useSx(sx);
@@ -3203,6 +3277,8 @@ var DateField = ({
3203
3277
  if (!value) return "";
3204
3278
  const d = isoToDate(value);
3205
3279
  if (!d) return "";
3280
+ const viewsStr = formatViewsDisplay(d, views);
3281
+ if (viewsStr) return viewsStr;
3206
3282
  let str = formatDisplay(d, dateFormat);
3207
3283
  if (isDatetimeType(type)) {
3208
3284
  const t = parseTimeFromISO(value);
@@ -3233,15 +3309,20 @@ var DateField = ({
3233
3309
  if (d) {
3234
3310
  setViewYear(d.getFullYear());
3235
3311
  setViewMonth(d.getMonth());
3236
- let str = formatDisplay(d, dateFormat);
3237
- if (isDatetimeType(type)) {
3238
- const t = parseTimeFromISO(value);
3239
- setHour(t.h);
3240
- setMinute(t.m);
3241
- setAmpm(t.ampm);
3242
- str += " " + formatTimeDisplay(t.h, t.m, t.ampm);
3312
+ const viewsStr = formatViewsDisplay(d, views);
3313
+ if (viewsStr) {
3314
+ setInputStr(viewsStr);
3315
+ } else {
3316
+ let str = formatDisplay(d, dateFormat);
3317
+ if (isDatetimeType(type)) {
3318
+ const t = parseTimeFromISO(value);
3319
+ setHour(t.h);
3320
+ setMinute(t.m);
3321
+ setAmpm(t.ampm);
3322
+ str += " " + formatTimeDisplay(t.h, t.m, t.ampm);
3323
+ }
3324
+ setInputStr(str);
3243
3325
  }
3244
- setInputStr(str);
3245
3326
  }
3246
3327
  }, [value, type]);
3247
3328
  useEffect7(() => {
@@ -3262,11 +3343,17 @@ var DateField = ({
3262
3343
  onChange?.("");
3263
3344
  return;
3264
3345
  }
3265
- let str = formatDisplay(d, dateFormat);
3266
- if (isDatetimeType(type)) str += " " + formatTimeDisplay(h, m, ap);
3346
+ const viewsStr = formatViewsDisplay(d, views);
3347
+ let str;
3348
+ if (viewsStr) {
3349
+ str = viewsStr;
3350
+ } else {
3351
+ str = formatDisplay(d, dateFormat);
3352
+ if (isDatetimeType(type)) str += " " + formatTimeDisplay(h, m, ap);
3353
+ }
3267
3354
  setInputStr(str);
3268
3355
  onChange?.(buildISO(d, type, h, m, ap));
3269
- }, [type, onChange, dateFormat]);
3356
+ }, [type, onChange, dateFormat, views]);
3270
3357
  const isOutOfRange = (d) => (minDate ? isBeforeDay(d, minDate) : false) || (maxDate ? isAfterDay(d, maxDate) : false);
3271
3358
  const handleDayClick = (day) => {
3272
3359
  const d = new Date(viewYear, viewMonth, day);
@@ -3280,49 +3367,102 @@ var DateField = ({
3280
3367
  };
3281
3368
  const handleToday = () => {
3282
3369
  const t = today();
3283
- if (isOutOfRange(t)) return;
3284
- setViewYear(t.getFullYear());
3285
- setViewMonth(t.getMonth());
3286
- commitDate(t, hour, minute, ampm);
3287
- if (type === "date") setOpen(false);
3370
+ if (!hasDayView && !hasMonthView) {
3371
+ const d = new Date(t.getFullYear(), 0, 1);
3372
+ if (isOutOfRange(d)) return;
3373
+ setViewYear(t.getFullYear());
3374
+ commitDate(d, hour, minute, ampm);
3375
+ setOpen(false);
3376
+ } else if (!hasDayView) {
3377
+ const d = new Date(t.getFullYear(), t.getMonth(), 1);
3378
+ if (isOutOfRange(d)) return;
3379
+ setViewYear(t.getFullYear());
3380
+ setViewMonth(t.getMonth());
3381
+ commitDate(d, hour, minute, ampm);
3382
+ setOpen(false);
3383
+ } else {
3384
+ if (isOutOfRange(t)) return;
3385
+ setViewYear(t.getFullYear());
3386
+ setViewMonth(t.getMonth());
3387
+ commitDate(t, hour, minute, ampm);
3388
+ if (type === "date") setOpen(false);
3389
+ }
3288
3390
  };
3391
+ const handleFinalMonthSelect = useCallback3((month, year) => {
3392
+ const d = new Date(year, month, 1);
3393
+ if (isOutOfRange(d)) return;
3394
+ setSelectedDate(d);
3395
+ setViewYear(year);
3396
+ setViewMonth(month);
3397
+ setInputStr(`${MONTHS_SHORT[month]} ${year}`);
3398
+ onChange?.(buildISO(d, type, hour, minute, ampm));
3399
+ setOpen(false);
3400
+ }, [isOutOfRange, onChange, type, hour, minute, ampm]);
3401
+ const handleFinalYearSelect = useCallback3((year) => {
3402
+ const d = new Date(year, 0, 1);
3403
+ if (isOutOfRange(d)) return;
3404
+ setSelectedDate(d);
3405
+ setViewYear(year);
3406
+ setInputStr(String(year));
3407
+ onChange?.(buildISO(d, type, hour, minute, ampm));
3408
+ setOpen(false);
3409
+ }, [isOutOfRange, onChange, type, hour, minute, ampm]);
3289
3410
  const handleClear = () => commitDate(null, hour, minute, ampm);
3290
3411
  const handleInputChange = (e) => {
3291
3412
  const raw = e.target.value;
3292
3413
  setInputStr(raw);
3293
- const dateWordCount = getDateWordCount(dateFormat);
3294
- const words = raw.split(" ");
3295
- const datePart = words.slice(0, dateWordCount).join(" ");
3296
- const timeParts = words.slice(dateWordCount);
3297
- const parsed = parseDisplay(datePart, dateFormat);
3298
- if (parsed && !isOutOfRange(parsed)) {
3299
- setSelectedDate(parsed);
3300
- setViewYear(parsed.getFullYear());
3301
- setViewMonth(parsed.getMonth());
3302
- let h = hour, m = minute, ap = ampm;
3303
- if (isDatetimeType(type) && timeParts.length >= 2) {
3304
- const timePart = timeParts[0];
3305
- const periodPart = timeParts[1]?.toUpperCase();
3306
- if (timePart?.includes(":")) {
3307
- const [hStr, mStr] = timePart.split(":");
3308
- const parsedH = parseInt(hStr, 10);
3309
- const parsedM = parseInt(mStr, 10);
3310
- if (!isNaN(parsedH) && parsedH >= 1 && parsedH <= 12) {
3311
- h = parsedH;
3312
- setHour(h);
3414
+ let parsed = null;
3415
+ if (!hasDayView && !hasMonthView) {
3416
+ parsed = parseYearDisplay(raw);
3417
+ } else if (!hasDayView) {
3418
+ parsed = parseMonthYearDisplay(raw);
3419
+ } else {
3420
+ const dateWordCount = getDateWordCount(dateFormat);
3421
+ const words = raw.split(" ");
3422
+ const datePart = words.slice(0, dateWordCount).join(" ");
3423
+ const timeParts = words.slice(dateWordCount);
3424
+ parsed = parseDisplay(datePart, dateFormat);
3425
+ if (parsed && !isOutOfRange(parsed)) {
3426
+ setSelectedDate(parsed);
3427
+ setViewYear(parsed.getFullYear());
3428
+ setViewMonth(parsed.getMonth());
3429
+ let h = hour, m = minute, ap = ampm;
3430
+ if (isDatetimeType(type) && timeParts.length >= 2) {
3431
+ const timePart = timeParts[0];
3432
+ const periodPart = timeParts[1]?.toUpperCase();
3433
+ if (timePart?.includes(":")) {
3434
+ const [hStr, mStr] = timePart.split(":");
3435
+ const parsedH = parseInt(hStr, 10);
3436
+ const parsedM = parseInt(mStr, 10);
3437
+ if (!isNaN(parsedH) && parsedH >= 1 && parsedH <= 12) {
3438
+ h = parsedH;
3439
+ setHour(h);
3440
+ }
3441
+ if (!isNaN(parsedM) && parsedM >= 0 && parsedM <= 59) {
3442
+ m = parsedM;
3443
+ setMinute(m);
3444
+ }
3313
3445
  }
3314
- if (!isNaN(parsedM) && parsedM >= 0 && parsedM <= 59) {
3315
- m = parsedM;
3316
- setMinute(m);
3446
+ if (periodPart === "AM" || periodPart === "PM") {
3447
+ ap = periodPart;
3448
+ setAmpm(ap);
3317
3449
  }
3318
3450
  }
3319
- if (periodPart === "AM" || periodPart === "PM") {
3320
- ap = periodPart;
3321
- setAmpm(ap);
3322
- }
3451
+ isInternalChange.current = true;
3452
+ onChange?.(buildISO(parsed, type, h, m, ap));
3453
+ } else if (!raw) {
3454
+ setSelectedDate(null);
3455
+ isInternalChange.current = true;
3456
+ onChange?.("");
3323
3457
  }
3458
+ return;
3459
+ }
3460
+ if (parsed && !isOutOfRange(parsed)) {
3461
+ setSelectedDate(parsed);
3462
+ setViewYear(parsed.getFullYear());
3463
+ setViewMonth(parsed.getMonth());
3324
3464
  isInternalChange.current = true;
3325
- onChange?.(buildISO(parsed, type, h, m, ap));
3465
+ onChange?.(buildISO(parsed, type, hour, minute, ampm));
3326
3466
  } else if (!raw) {
3327
3467
  setSelectedDate(null);
3328
3468
  isInternalChange.current = true;
@@ -3508,7 +3648,10 @@ var DateField = ({
3508
3648
  onMonthSelect: setViewMonth,
3509
3649
  onYearSelect: setViewYear,
3510
3650
  minDate,
3511
- maxDate
3651
+ maxDate,
3652
+ views,
3653
+ onFinalMonthSelect: handleFinalMonthSelect,
3654
+ onFinalYearSelect: handleFinalYearSelect
3512
3655
  }
3513
3656
  ), type === "datetime" && /* @__PURE__ */ React72.createElement("div", { className: "rf-date-picker__time-section" }, /* @__PURE__ */ React72.createElement("div", { className: "rf-date-picker__time-label" }, "Time"), /* @__PURE__ */ React72.createElement(
3514
3657
  SpinnerPanel,
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@rufous/ui",
3
3
  "private": false,
4
- "version": "0.3.01",
4
+ "version": "0.3.12",
5
5
  "type": "module",
6
6
  "description": "Experimental: A lightweight React UI component library (Beta)",
7
7
  "style": "./dist/main.css",
@@ -93,4 +93,4 @@
93
93
  "react": "^18.0.0 || ^19.0.0",
94
94
  "react-dom": "^18.0.0 || ^19.0.0"
95
95
  }
96
- }
96
+ }