@rufous/ui 0.3.11 → 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
@@ -3189,6 +3189,24 @@ var SpinnerPanel = ({
3189
3189
  },
3190
3190
  "PM"
3191
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
+ };
3192
3210
  var CalendarBody = ({
3193
3211
  viewMonth,
3194
3212
  viewYear,
@@ -3201,18 +3219,48 @@ var CalendarBody = ({
3201
3219
  onMonthSelect,
3202
3220
  onYearSelect,
3203
3221
  minDate,
3204
- maxDate
3222
+ maxDate,
3223
+ views,
3224
+ onFinalMonthSelect,
3225
+ onFinalYearSelect
3205
3226
  }) => {
3206
- const [pickerView, setPickerView] = (0, import_react21.useState)("calendar");
3207
- const handleMonthClick = () => setPickerView(pickerView === "month" ? "calendar" : "month");
3208
- 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
+ };
3209
3247
  const handleMonthPick = (month) => {
3210
3248
  onMonthSelect(month);
3211
- setPickerView("calendar");
3249
+ if (hasDayView) {
3250
+ setPickerView("calendar");
3251
+ } else {
3252
+ onFinalMonthSelect?.(month, viewYear);
3253
+ }
3212
3254
  };
3213
3255
  const handleYearPick = (year) => {
3214
3256
  onYearSelect(year);
3215
- setPickerView("calendar");
3257
+ if (!hasDayView && !hasMonthView) {
3258
+ onFinalYearSelect?.(year);
3259
+ } else if (hasMonthView) {
3260
+ setPickerView("month");
3261
+ } else {
3262
+ setPickerView("calendar");
3263
+ }
3216
3264
  };
3217
3265
  const currentYear = todayDate.getFullYear();
3218
3266
  const yearStart = viewYear - 6;
@@ -3236,18 +3284,35 @@ var CalendarBody = ({
3236
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(
3237
3285
  "span",
3238
3286
  {
3239
- 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(" "),
3240
3292
  onClick: handleMonthClick
3241
3293
  },
3242
3294
  MONTHS[viewMonth]
3243
3295
  ), /* @__PURE__ */ import_react21.default.createElement(
3244
3296
  "span",
3245
3297
  {
3246
- 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(" "),
3247
3303
  onClick: handleYearClick
3248
3304
  },
3249
3305
  viewYear
3250
- )), /* @__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) => {
3251
3316
  const monthDisabled = isMonthDisabled(idx);
3252
3317
  return /* @__PURE__ */ import_react21.default.createElement(
3253
3318
  "button",
@@ -3283,7 +3348,7 @@ var CalendarBody = ({
3283
3348
  },
3284
3349
  y
3285
3350
  );
3286
- })), 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) => {
3287
3352
  if (day === null) return /* @__PURE__ */ import_react21.default.createElement("div", { key: `e-${idx}`, className: "rf-date-picker__day rf-date-picker__day--empty" });
3288
3353
  const cellDate = new Date(viewYear, viewMonth, day);
3289
3354
  const isSelected = selectedDate ? isSameDay(cellDate, selectedDate) : false;
@@ -3326,8 +3391,12 @@ var DateField = ({
3326
3391
  placeholder,
3327
3392
  className = "",
3328
3393
  style,
3329
- sx
3394
+ sx,
3395
+ views: viewsProp
3330
3396
  }) => {
3397
+ const views = viewsProp ?? ["day", "month", "year"];
3398
+ const hasDayView = views.includes("day");
3399
+ const hasMonthView = views.includes("month");
3331
3400
  const minDate = normaliseBoundary(minDateProp);
3332
3401
  const maxDate = normaliseBoundary(maxDateProp);
3333
3402
  const sxClass = useSx(sx);
@@ -3357,6 +3426,8 @@ var DateField = ({
3357
3426
  if (!value) return "";
3358
3427
  const d = isoToDate(value);
3359
3428
  if (!d) return "";
3429
+ const viewsStr = formatViewsDisplay(d, views);
3430
+ if (viewsStr) return viewsStr;
3360
3431
  let str = formatDisplay(d, dateFormat);
3361
3432
  if (isDatetimeType(type)) {
3362
3433
  const t = parseTimeFromISO(value);
@@ -3387,15 +3458,20 @@ var DateField = ({
3387
3458
  if (d) {
3388
3459
  setViewYear(d.getFullYear());
3389
3460
  setViewMonth(d.getMonth());
3390
- let str = formatDisplay(d, dateFormat);
3391
- if (isDatetimeType(type)) {
3392
- const t = parseTimeFromISO(value);
3393
- setHour(t.h);
3394
- setMinute(t.m);
3395
- setAmpm(t.ampm);
3396
- 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);
3397
3474
  }
3398
- setInputStr(str);
3399
3475
  }
3400
3476
  }, [value, type]);
3401
3477
  (0, import_react21.useEffect)(() => {
@@ -3416,11 +3492,17 @@ var DateField = ({
3416
3492
  onChange?.("");
3417
3493
  return;
3418
3494
  }
3419
- let str = formatDisplay(d, dateFormat);
3420
- 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
+ }
3421
3503
  setInputStr(str);
3422
3504
  onChange?.(buildISO(d, type, h, m, ap));
3423
- }, [type, onChange, dateFormat]);
3505
+ }, [type, onChange, dateFormat, views]);
3424
3506
  const isOutOfRange = (d) => (minDate ? isBeforeDay(d, minDate) : false) || (maxDate ? isAfterDay(d, maxDate) : false);
3425
3507
  const handleDayClick = (day) => {
3426
3508
  const d = new Date(viewYear, viewMonth, day);
@@ -3434,49 +3516,102 @@ var DateField = ({
3434
3516
  };
3435
3517
  const handleToday = () => {
3436
3518
  const t = today();
3437
- if (isOutOfRange(t)) return;
3438
- setViewYear(t.getFullYear());
3439
- setViewMonth(t.getMonth());
3440
- commitDate(t, hour, minute, ampm);
3441
- 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
+ }
3442
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]);
3443
3559
  const handleClear = () => commitDate(null, hour, minute, ampm);
3444
3560
  const handleInputChange = (e) => {
3445
3561
  const raw = e.target.value;
3446
3562
  setInputStr(raw);
3447
- const dateWordCount = getDateWordCount(dateFormat);
3448
- const words = raw.split(" ");
3449
- const datePart = words.slice(0, dateWordCount).join(" ");
3450
- const timeParts = words.slice(dateWordCount);
3451
- const parsed = parseDisplay(datePart, dateFormat);
3452
- if (parsed && !isOutOfRange(parsed)) {
3453
- setSelectedDate(parsed);
3454
- setViewYear(parsed.getFullYear());
3455
- setViewMonth(parsed.getMonth());
3456
- let h = hour, m = minute, ap = ampm;
3457
- if (isDatetimeType(type) && timeParts.length >= 2) {
3458
- const timePart = timeParts[0];
3459
- const periodPart = timeParts[1]?.toUpperCase();
3460
- if (timePart?.includes(":")) {
3461
- const [hStr, mStr] = timePart.split(":");
3462
- const parsedH = parseInt(hStr, 10);
3463
- const parsedM = parseInt(mStr, 10);
3464
- if (!isNaN(parsedH) && parsedH >= 1 && parsedH <= 12) {
3465
- h = parsedH;
3466
- 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
+ }
3467
3594
  }
3468
- if (!isNaN(parsedM) && parsedM >= 0 && parsedM <= 59) {
3469
- m = parsedM;
3470
- setMinute(m);
3595
+ if (periodPart === "AM" || periodPart === "PM") {
3596
+ ap = periodPart;
3597
+ setAmpm(ap);
3471
3598
  }
3472
3599
  }
3473
- if (periodPart === "AM" || periodPart === "PM") {
3474
- ap = periodPart;
3475
- setAmpm(ap);
3476
- }
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?.("");
3477
3606
  }
3607
+ return;
3608
+ }
3609
+ if (parsed && !isOutOfRange(parsed)) {
3610
+ setSelectedDate(parsed);
3611
+ setViewYear(parsed.getFullYear());
3612
+ setViewMonth(parsed.getMonth());
3478
3613
  isInternalChange.current = true;
3479
- onChange?.(buildISO(parsed, type, h, m, ap));
3614
+ onChange?.(buildISO(parsed, type, hour, minute, ampm));
3480
3615
  } else if (!raw) {
3481
3616
  setSelectedDate(null);
3482
3617
  isInternalChange.current = true;
@@ -3662,7 +3797,10 @@ var DateField = ({
3662
3797
  onMonthSelect: setViewMonth,
3663
3798
  onYearSelect: setViewYear,
3664
3799
  minDate,
3665
- maxDate
3800
+ maxDate,
3801
+ views,
3802
+ onFinalMonthSelect: handleFinalMonthSelect,
3803
+ onFinalYearSelect: handleFinalYearSelect
3666
3804
  }
3667
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(
3668
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
@@ -3040,6 +3040,24 @@ var SpinnerPanel = ({
3040
3040
  },
3041
3041
  "PM"
3042
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
+ };
3043
3061
  var CalendarBody = ({
3044
3062
  viewMonth,
3045
3063
  viewYear,
@@ -3052,18 +3070,48 @@ var CalendarBody = ({
3052
3070
  onMonthSelect,
3053
3071
  onYearSelect,
3054
3072
  minDate,
3055
- maxDate
3073
+ maxDate,
3074
+ views,
3075
+ onFinalMonthSelect,
3076
+ onFinalYearSelect
3056
3077
  }) => {
3057
- const [pickerView, setPickerView] = useState7("calendar");
3058
- const handleMonthClick = () => setPickerView(pickerView === "month" ? "calendar" : "month");
3059
- 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
+ };
3060
3098
  const handleMonthPick = (month) => {
3061
3099
  onMonthSelect(month);
3062
- setPickerView("calendar");
3100
+ if (hasDayView) {
3101
+ setPickerView("calendar");
3102
+ } else {
3103
+ onFinalMonthSelect?.(month, viewYear);
3104
+ }
3063
3105
  };
3064
3106
  const handleYearPick = (year) => {
3065
3107
  onYearSelect(year);
3066
- setPickerView("calendar");
3108
+ if (!hasDayView && !hasMonthView) {
3109
+ onFinalYearSelect?.(year);
3110
+ } else if (hasMonthView) {
3111
+ setPickerView("month");
3112
+ } else {
3113
+ setPickerView("calendar");
3114
+ }
3067
3115
  };
3068
3116
  const currentYear = todayDate.getFullYear();
3069
3117
  const yearStart = viewYear - 6;
@@ -3087,18 +3135,35 @@ var CalendarBody = ({
3087
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(
3088
3136
  "span",
3089
3137
  {
3090
- 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(" "),
3091
3143
  onClick: handleMonthClick
3092
3144
  },
3093
3145
  MONTHS[viewMonth]
3094
3146
  ), /* @__PURE__ */ React72.createElement(
3095
3147
  "span",
3096
3148
  {
3097
- 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(" "),
3098
3154
  onClick: handleYearClick
3099
3155
  },
3100
3156
  viewYear
3101
- )), /* @__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) => {
3102
3167
  const monthDisabled = isMonthDisabled(idx);
3103
3168
  return /* @__PURE__ */ React72.createElement(
3104
3169
  "button",
@@ -3134,7 +3199,7 @@ var CalendarBody = ({
3134
3199
  },
3135
3200
  y
3136
3201
  );
3137
- })), 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) => {
3138
3203
  if (day === null) return /* @__PURE__ */ React72.createElement("div", { key: `e-${idx}`, className: "rf-date-picker__day rf-date-picker__day--empty" });
3139
3204
  const cellDate = new Date(viewYear, viewMonth, day);
3140
3205
  const isSelected = selectedDate ? isSameDay(cellDate, selectedDate) : false;
@@ -3177,8 +3242,12 @@ var DateField = ({
3177
3242
  placeholder,
3178
3243
  className = "",
3179
3244
  style,
3180
- sx
3245
+ sx,
3246
+ views: viewsProp
3181
3247
  }) => {
3248
+ const views = viewsProp ?? ["day", "month", "year"];
3249
+ const hasDayView = views.includes("day");
3250
+ const hasMonthView = views.includes("month");
3182
3251
  const minDate = normaliseBoundary(minDateProp);
3183
3252
  const maxDate = normaliseBoundary(maxDateProp);
3184
3253
  const sxClass = useSx(sx);
@@ -3208,6 +3277,8 @@ var DateField = ({
3208
3277
  if (!value) return "";
3209
3278
  const d = isoToDate(value);
3210
3279
  if (!d) return "";
3280
+ const viewsStr = formatViewsDisplay(d, views);
3281
+ if (viewsStr) return viewsStr;
3211
3282
  let str = formatDisplay(d, dateFormat);
3212
3283
  if (isDatetimeType(type)) {
3213
3284
  const t = parseTimeFromISO(value);
@@ -3238,15 +3309,20 @@ var DateField = ({
3238
3309
  if (d) {
3239
3310
  setViewYear(d.getFullYear());
3240
3311
  setViewMonth(d.getMonth());
3241
- let str = formatDisplay(d, dateFormat);
3242
- if (isDatetimeType(type)) {
3243
- const t = parseTimeFromISO(value);
3244
- setHour(t.h);
3245
- setMinute(t.m);
3246
- setAmpm(t.ampm);
3247
- 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);
3248
3325
  }
3249
- setInputStr(str);
3250
3326
  }
3251
3327
  }, [value, type]);
3252
3328
  useEffect7(() => {
@@ -3267,11 +3343,17 @@ var DateField = ({
3267
3343
  onChange?.("");
3268
3344
  return;
3269
3345
  }
3270
- let str = formatDisplay(d, dateFormat);
3271
- 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
+ }
3272
3354
  setInputStr(str);
3273
3355
  onChange?.(buildISO(d, type, h, m, ap));
3274
- }, [type, onChange, dateFormat]);
3356
+ }, [type, onChange, dateFormat, views]);
3275
3357
  const isOutOfRange = (d) => (minDate ? isBeforeDay(d, minDate) : false) || (maxDate ? isAfterDay(d, maxDate) : false);
3276
3358
  const handleDayClick = (day) => {
3277
3359
  const d = new Date(viewYear, viewMonth, day);
@@ -3285,49 +3367,102 @@ var DateField = ({
3285
3367
  };
3286
3368
  const handleToday = () => {
3287
3369
  const t = today();
3288
- if (isOutOfRange(t)) return;
3289
- setViewYear(t.getFullYear());
3290
- setViewMonth(t.getMonth());
3291
- commitDate(t, hour, minute, ampm);
3292
- 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
+ }
3293
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]);
3294
3410
  const handleClear = () => commitDate(null, hour, minute, ampm);
3295
3411
  const handleInputChange = (e) => {
3296
3412
  const raw = e.target.value;
3297
3413
  setInputStr(raw);
3298
- const dateWordCount = getDateWordCount(dateFormat);
3299
- const words = raw.split(" ");
3300
- const datePart = words.slice(0, dateWordCount).join(" ");
3301
- const timeParts = words.slice(dateWordCount);
3302
- const parsed = parseDisplay(datePart, dateFormat);
3303
- if (parsed && !isOutOfRange(parsed)) {
3304
- setSelectedDate(parsed);
3305
- setViewYear(parsed.getFullYear());
3306
- setViewMonth(parsed.getMonth());
3307
- let h = hour, m = minute, ap = ampm;
3308
- if (isDatetimeType(type) && timeParts.length >= 2) {
3309
- const timePart = timeParts[0];
3310
- const periodPart = timeParts[1]?.toUpperCase();
3311
- if (timePart?.includes(":")) {
3312
- const [hStr, mStr] = timePart.split(":");
3313
- const parsedH = parseInt(hStr, 10);
3314
- const parsedM = parseInt(mStr, 10);
3315
- if (!isNaN(parsedH) && parsedH >= 1 && parsedH <= 12) {
3316
- h = parsedH;
3317
- 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
+ }
3318
3445
  }
3319
- if (!isNaN(parsedM) && parsedM >= 0 && parsedM <= 59) {
3320
- m = parsedM;
3321
- setMinute(m);
3446
+ if (periodPart === "AM" || periodPart === "PM") {
3447
+ ap = periodPart;
3448
+ setAmpm(ap);
3322
3449
  }
3323
3450
  }
3324
- if (periodPart === "AM" || periodPart === "PM") {
3325
- ap = periodPart;
3326
- setAmpm(ap);
3327
- }
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?.("");
3328
3457
  }
3458
+ return;
3459
+ }
3460
+ if (parsed && !isOutOfRange(parsed)) {
3461
+ setSelectedDate(parsed);
3462
+ setViewYear(parsed.getFullYear());
3463
+ setViewMonth(parsed.getMonth());
3329
3464
  isInternalChange.current = true;
3330
- onChange?.(buildISO(parsed, type, h, m, ap));
3465
+ onChange?.(buildISO(parsed, type, hour, minute, ampm));
3331
3466
  } else if (!raw) {
3332
3467
  setSelectedDate(null);
3333
3468
  isInternalChange.current = true;
@@ -3513,7 +3648,10 @@ var DateField = ({
3513
3648
  onMonthSelect: setViewMonth,
3514
3649
  onYearSelect: setViewYear,
3515
3650
  minDate,
3516
- maxDate
3651
+ maxDate,
3652
+ views,
3653
+ onFinalMonthSelect: handleFinalMonthSelect,
3654
+ onFinalYearSelect: handleFinalYearSelect
3517
3655
  }
3518
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(
3519
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.11",
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
+ }