@rufous/ui 0.2.94 → 0.2.96

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
@@ -1831,6 +1831,28 @@ var TextField = (0, import_react17.forwardRef)(({
1831
1831
  className
1832
1832
  ].filter(Boolean).join(" ");
1833
1833
  const internalRef = import_react17.default.useRef(null);
1834
+ const handleChange = (e) => {
1835
+ if (type === "number") {
1836
+ const raw = e.target.value;
1837
+ const inputMax = slotProps?.input?.max ?? props.max;
1838
+ const inputMaxLength = slotProps?.input?.maxLength ?? props.maxLength;
1839
+ if (inputMaxLength != null) {
1840
+ const digits = raw.replace(/[^0-9]/g, "");
1841
+ if (digits.length > Number(inputMaxLength)) return;
1842
+ }
1843
+ if (inputMax != null && raw !== "") {
1844
+ if (Number(raw) > Number(inputMax)) return;
1845
+ }
1846
+ const numericValue = raw === "" ? "" : Number(raw);
1847
+ const syntheticEvent = Object.assign({}, e, {
1848
+ target: Object.assign({}, e.target, { value: numericValue }),
1849
+ currentTarget: Object.assign({}, e.currentTarget, { value: numericValue })
1850
+ });
1851
+ onChange?.(syntheticEvent);
1852
+ return;
1853
+ }
1854
+ onChange?.(e);
1855
+ };
1834
1856
  const triggerChange = () => {
1835
1857
  if (internalRef.current) {
1836
1858
  const nativeInputValueSetter = Object.getOwnPropertyDescriptor(window.HTMLInputElement.prototype, "value")?.set;
@@ -1894,14 +1916,14 @@ var TextField = (0, import_react17.forwardRef)(({
1894
1916
  name,
1895
1917
  id: inputId,
1896
1918
  value,
1897
- onChange,
1898
1919
  required,
1899
1920
  disabled,
1900
1921
  readOnly,
1901
1922
  step: type === "number" && numberVariant ? STEP_BY_VARIANT[numberVariant] : void 0,
1902
1923
  min: type === "number" && numberVariant ? MIN_BY_VARIANT[numberVariant] : void 0,
1903
1924
  ...slotProps?.input,
1904
- ...props
1925
+ ...props,
1926
+ onChange: handleChange
1905
1927
  }
1906
1928
  ), InputProps?.endAdornment && /* @__PURE__ */ import_react17.default.createElement("div", { className: "rf-text-field__adornment rf-text-field__adornment--end" }, InputProps.endAdornment), type === "number" && !disabled && !readOnly && /* @__PURE__ */ import_react17.default.createElement("div", { className: "rf-text-field__number-controls" }, /* @__PURE__ */ import_react17.default.createElement("button", { type: "button", tabIndex: -1, onClick: handleIncrement, className: "rf-text-field__number-btn" }, /* @__PURE__ */ import_react17.default.createElement("svg", { width: "8", height: "5", viewBox: "0 0 8 5", fill: "currentColor" }, /* @__PURE__ */ import_react17.default.createElement("path", { d: "M4 0L8 5H0L4 0Z" }))), /* @__PURE__ */ import_react17.default.createElement("button", { type: "button", tabIndex: -1, onClick: handleDecrement, className: "rf-text-field__number-btn", style: { marginTop: 2 } }, /* @__PURE__ */ import_react17.default.createElement("svg", { width: "8", height: "5", viewBox: "0 0 8 5", fill: "currentColor" }, /* @__PURE__ */ import_react17.default.createElement("path", { d: "M4 5L0 0H8L4 5Z" })))), hasLabel && /* @__PURE__ */ import_react17.default.createElement("label", { htmlFor: inputId, className: "rf-text-field__label" }, label, " ", required && /* @__PURE__ */ import_react17.default.createElement("span", { className: "rf-text-field__asterisk" }, "*")), variant === "outlined" && /* @__PURE__ */ import_react17.default.createElement("fieldset", { className: "rf-text-field__notch" }, hasLabel ? /* @__PURE__ */ import_react17.default.createElement("legend", { className: "rf-text-field__legend" }, /* @__PURE__ */ import_react17.default.createElement("span", null, label, " ", required ? "*" : "")) : /* @__PURE__ */ import_react17.default.createElement("legend", { className: "rf-text-field__legend--empty" }))), helperText && /* @__PURE__ */ import_react17.default.createElement("div", { className: "rf-text-field__helper-text" }, helperText));
1907
1929
  });
@@ -2967,6 +2989,14 @@ var today = () => {
2967
2989
  return new Date(t.getFullYear(), t.getMonth(), t.getDate());
2968
2990
  };
2969
2991
  var isSameDay = (a, b) => a.getFullYear() === b.getFullYear() && a.getMonth() === b.getMonth() && a.getDate() === b.getDate();
2992
+ var normaliseBoundary = (d) => {
2993
+ if (!d) return null;
2994
+ const base = d instanceof Date ? d : isoToDate(typeof d === "string" ? d.split("T")[0] : d);
2995
+ if (!base) return null;
2996
+ return new Date(base.getFullYear(), base.getMonth(), base.getDate());
2997
+ };
2998
+ var isBeforeDay = (a, b) => a.getFullYear() < b.getFullYear() || a.getFullYear() === b.getFullYear() && a.getMonth() < b.getMonth() || a.getFullYear() === b.getFullYear() && a.getMonth() === b.getMonth() && a.getDate() < b.getDate();
2999
+ var isAfterDay = (a, b) => a.getFullYear() > b.getFullYear() || a.getFullYear() === b.getFullYear() && a.getMonth() > b.getMonth() || a.getFullYear() === b.getFullYear() && a.getMonth() === b.getMonth() && a.getDate() > b.getDate();
2970
3000
  var isDatetimeType = (t) => t !== "date";
2971
3001
  var buildISO = (date, type, hour, minute, ampm) => {
2972
3002
  const y = date.getFullYear();
@@ -3127,15 +3157,13 @@ var CalendarBody = ({
3127
3157
  onPrev,
3128
3158
  onNext,
3129
3159
  onMonthSelect,
3130
- onYearSelect
3160
+ onYearSelect,
3161
+ minDate,
3162
+ maxDate
3131
3163
  }) => {
3132
3164
  const [pickerView, setPickerView] = (0, import_react21.useState)("calendar");
3133
- const handleMonthClick = () => {
3134
- setPickerView(pickerView === "month" ? "calendar" : "month");
3135
- };
3136
- const handleYearClick = () => {
3137
- setPickerView(pickerView === "year" ? "calendar" : "year");
3138
- };
3165
+ const handleMonthClick = () => setPickerView(pickerView === "month" ? "calendar" : "month");
3166
+ const handleYearClick = () => setPickerView(pickerView === "year" ? "calendar" : "year");
3139
3167
  const handleMonthPick = (month) => {
3140
3168
  onMonthSelect(month);
3141
3169
  setPickerView("calendar");
@@ -3147,6 +3175,22 @@ var CalendarBody = ({
3147
3175
  const currentYear = todayDate.getFullYear();
3148
3176
  const yearStart = viewYear - 6;
3149
3177
  const years = Array.from({ length: 16 }, (_, i) => yearStart + i);
3178
+ const prevMonthLastDay = new Date(viewYear, viewMonth, 0);
3179
+ const isPrevDisabled = minDate ? isBeforeDay(prevMonthLastDay, minDate) : false;
3180
+ const nextMonthFirstDay = new Date(viewYear, viewMonth + 1, 1);
3181
+ const isNextDisabled = maxDate ? isAfterDay(nextMonthFirstDay, maxDate) : false;
3182
+ const isMonthDisabled = (idx) => {
3183
+ const lastDayOfMonth = new Date(viewYear, idx + 1, 0);
3184
+ const firstDayOfMonth = new Date(viewYear, idx, 1);
3185
+ if (minDate && isBeforeDay(lastDayOfMonth, minDate)) return true;
3186
+ if (maxDate && isAfterDay(firstDayOfMonth, maxDate)) return true;
3187
+ return false;
3188
+ };
3189
+ const isYearDisabled = (y) => {
3190
+ if (minDate && y < minDate.getFullYear()) return true;
3191
+ if (maxDate && y > maxDate.getFullYear()) return true;
3192
+ return false;
3193
+ };
3150
3194
  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(
3151
3195
  "span",
3152
3196
  {
@@ -3161,48 +3205,61 @@ var CalendarBody = ({
3161
3205
  onClick: handleYearClick
3162
3206
  },
3163
3207
  viewYear
3164
- )), /* @__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, "aria-label": "Previous month" }, "\u2039"), /* @__PURE__ */ import_react21.default.createElement("button", { type: "button", className: "rf-date-picker__nav-btn", onClick: onNext, "aria-label": "Next month" }, "\u203A")))), pickerView === "month" && /* @__PURE__ */ import_react21.default.createElement("div", { className: "rf-date-picker__month-grid" }, MONTHS_SHORT.map((m, idx) => /* @__PURE__ */ import_react21.default.createElement(
3165
- "button",
3166
- {
3167
- key: m,
3168
- type: "button",
3169
- className: [
3170
- "rf-date-picker__month-cell",
3171
- idx === viewMonth ? "rf-date-picker__month-cell--selected" : "",
3172
- idx === todayDate.getMonth() && viewYear === currentYear ? "rf-date-picker__month-cell--current" : ""
3173
- ].filter(Boolean).join(" "),
3174
- onClick: () => handleMonthPick(idx)
3175
- },
3176
- m
3177
- ))), pickerView === "year" && /* @__PURE__ */ import_react21.default.createElement("div", { className: "rf-date-picker__year-grid" }, years.map((y) => /* @__PURE__ */ import_react21.default.createElement(
3178
- "button",
3179
- {
3180
- key: y,
3181
- type: "button",
3182
- className: [
3183
- "rf-date-picker__year-cell",
3184
- y === viewYear ? "rf-date-picker__year-cell--selected" : "",
3185
- y === currentYear ? "rf-date-picker__year-cell--current" : ""
3186
- ].filter(Boolean).join(" "),
3187
- onClick: () => handleYearPick(y)
3188
- },
3189
- y
3190
- ))), 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) => {
3208
+ )), /* @__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) => {
3209
+ const monthDisabled = isMonthDisabled(idx);
3210
+ return /* @__PURE__ */ import_react21.default.createElement(
3211
+ "button",
3212
+ {
3213
+ key: m,
3214
+ type: "button",
3215
+ disabled: monthDisabled,
3216
+ className: [
3217
+ "rf-date-picker__month-cell",
3218
+ idx === viewMonth ? "rf-date-picker__month-cell--selected" : "",
3219
+ idx === todayDate.getMonth() && viewYear === currentYear ? "rf-date-picker__month-cell--current" : "",
3220
+ monthDisabled ? "rf-date-picker__month-cell--disabled" : ""
3221
+ ].filter(Boolean).join(" "),
3222
+ onClick: () => !monthDisabled && handleMonthPick(idx)
3223
+ },
3224
+ m
3225
+ );
3226
+ })), pickerView === "year" && /* @__PURE__ */ import_react21.default.createElement("div", { className: "rf-date-picker__year-grid" }, years.map((y) => {
3227
+ const yearDisabled = isYearDisabled(y);
3228
+ return /* @__PURE__ */ import_react21.default.createElement(
3229
+ "button",
3230
+ {
3231
+ key: y,
3232
+ type: "button",
3233
+ disabled: yearDisabled,
3234
+ className: [
3235
+ "rf-date-picker__year-cell",
3236
+ y === viewYear ? "rf-date-picker__year-cell--selected" : "",
3237
+ y === currentYear ? "rf-date-picker__year-cell--current" : "",
3238
+ yearDisabled ? "rf-date-picker__year-cell--disabled" : ""
3239
+ ].filter(Boolean).join(" "),
3240
+ onClick: () => !yearDisabled && handleYearPick(y)
3241
+ },
3242
+ y
3243
+ );
3244
+ })), 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) => {
3191
3245
  if (day === null) return /* @__PURE__ */ import_react21.default.createElement("div", { key: `e-${idx}`, className: "rf-date-picker__day rf-date-picker__day--empty" });
3192
3246
  const cellDate = new Date(viewYear, viewMonth, day);
3193
3247
  const isSelected = selectedDate ? isSameDay(cellDate, selectedDate) : false;
3194
3248
  const isToday = isSameDay(cellDate, todayDate);
3249
+ const dayDisabled = (minDate ? isBeforeDay(cellDate, minDate) : false) || (maxDate ? isAfterDay(cellDate, maxDate) : false);
3195
3250
  return /* @__PURE__ */ import_react21.default.createElement(
3196
3251
  "button",
3197
3252
  {
3198
3253
  key: day,
3199
3254
  type: "button",
3255
+ disabled: dayDisabled,
3200
3256
  className: [
3201
3257
  "rf-date-picker__day",
3202
3258
  isSelected ? "rf-date-picker__day--selected" : "",
3203
- isToday && !isSelected ? "rf-date-picker__day--today" : ""
3259
+ isToday && !isSelected ? "rf-date-picker__day--today" : "",
3260
+ dayDisabled ? "rf-date-picker__day--disabled" : ""
3204
3261
  ].filter(Boolean).join(" "),
3205
- onClick: () => onDayClick(day)
3262
+ onClick: () => !dayDisabled && onDayClick(day)
3206
3263
  },
3207
3264
  day
3208
3265
  );
@@ -3214,6 +3271,8 @@ var DateField = ({
3214
3271
  onChange,
3215
3272
  type = "date",
3216
3273
  dateFormat = "MM/DD/YYYY",
3274
+ minDate: minDateProp,
3275
+ maxDate: maxDateProp,
3217
3276
  variant = "outlined",
3218
3277
  size = "medium",
3219
3278
  color = "primary",
@@ -3227,6 +3286,8 @@ var DateField = ({
3227
3286
  style,
3228
3287
  sx
3229
3288
  }) => {
3289
+ const minDate = normaliseBoundary(minDateProp);
3290
+ const maxDate = normaliseBoundary(maxDateProp);
3230
3291
  const sxClass = useSx(sx);
3231
3292
  const [open, setOpen] = (0, import_react21.useState)(false);
3232
3293
  const [selectedDate, setSelectedDate] = (0, import_react21.useState)(() => value ? isoToDate(value) : null);
@@ -3326,8 +3387,10 @@ var DateField = ({
3326
3387
  setInputStr(str);
3327
3388
  onChange?.(buildISO(d, type, h, m, ap));
3328
3389
  }, [type, onChange, dateFormat]);
3390
+ const isOutOfRange = (d) => (minDate ? isBeforeDay(d, minDate) : false) || (maxDate ? isAfterDay(d, maxDate) : false);
3329
3391
  const handleDayClick = (day) => {
3330
3392
  const d = new Date(viewYear, viewMonth, day);
3393
+ if (isOutOfRange(d)) return;
3331
3394
  setSelectedDate(d);
3332
3395
  let str = formatDisplay(d, dateFormat);
3333
3396
  if (isDatetimeType(type)) str += " " + formatTimeDisplay(hour, minute, ampm);
@@ -3337,6 +3400,7 @@ var DateField = ({
3337
3400
  };
3338
3401
  const handleToday = () => {
3339
3402
  const t = today();
3403
+ if (isOutOfRange(t)) return;
3340
3404
  setViewYear(t.getFullYear());
3341
3405
  setViewMonth(t.getMonth());
3342
3406
  commitDate(t, hour, minute, ampm);
@@ -3351,7 +3415,7 @@ var DateField = ({
3351
3415
  const datePart = words.slice(0, dateWordCount).join(" ");
3352
3416
  const timeParts = words.slice(dateWordCount);
3353
3417
  const parsed = parseDisplay(datePart, dateFormat);
3354
- if (parsed) {
3418
+ if (parsed && !isOutOfRange(parsed)) {
3355
3419
  setSelectedDate(parsed);
3356
3420
  setViewYear(parsed.getFullYear());
3357
3421
  setViewMonth(parsed.getMonth());
@@ -3545,7 +3609,9 @@ var DateField = ({
3545
3609
  onPrev: prevMonth,
3546
3610
  onNext: nextMonth,
3547
3611
  onMonthSelect: setViewMonth,
3548
- onYearSelect: setViewYear
3612
+ onYearSelect: setViewYear,
3613
+ minDate,
3614
+ maxDate
3549
3615
  }
3550
3616
  ), 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(
3551
3617
  SpinnerPanel,
@@ -3559,7 +3625,7 @@ var DateField = ({
3559
3625
  onMinuteInput: handleMinuteInput,
3560
3626
  onAmpmToggle: handleAmpmToggle
3561
3627
  }
3562
- )), /* @__PURE__ */ import_react21.default.createElement("div", { className: "rf-date-picker__divider" }), /* @__PURE__ */ import_react21.default.createElement("div", { className: "rf-date-picker__footer" }, /* @__PURE__ */ import_react21.default.createElement("button", { type: "button", className: "rf-date-picker__footer-btn", onClick: handleToday }, "Today"), /* @__PURE__ */ import_react21.default.createElement("button", { type: "button", className: "rf-date-picker__footer-btn rf-date-picker__footer-btn--clear", onClick: handleClear }, "Clear"))),
3628
+ )), /* @__PURE__ */ import_react21.default.createElement("div", { className: "rf-date-picker__divider" }), /* @__PURE__ */ import_react21.default.createElement("div", { className: "rf-date-picker__footer" }, /* @__PURE__ */ import_react21.default.createElement("button", { type: "button", className: "rf-date-picker__footer-btn", onClick: handleToday, disabled: isOutOfRange(todayDate) }, "Today"), /* @__PURE__ */ import_react21.default.createElement("button", { type: "button", className: "rf-date-picker__footer-btn rf-date-picker__footer-btn--clear", onClick: handleClear }, "Clear"))),
3563
3629
  type === "datetime-side" && /* @__PURE__ */ import_react21.default.createElement("div", { className: "rf-date-picker__side-panel" }, /* @__PURE__ */ import_react21.default.createElement("div", { className: "rf-date-picker__side-label" }, "Time"), /* @__PURE__ */ import_react21.default.createElement("div", { className: "rf-date-picker__side-spinner" }, /* @__PURE__ */ import_react21.default.createElement(
3564
3630
  SpinnerPanel,
3565
3631
  {
package/dist/main.css CHANGED
@@ -1758,6 +1758,34 @@ pre {
1758
1758
  color: #a41b06;
1759
1759
  font-weight: 600;
1760
1760
  }
1761
+ .rf-date-picker__day--disabled,
1762
+ .rf-date-picker__day:disabled {
1763
+ opacity: 0.3;
1764
+ cursor: not-allowed;
1765
+ pointer-events: none;
1766
+ }
1767
+ .rf-date-picker__month-cell--disabled,
1768
+ .rf-date-picker__month-cell:disabled {
1769
+ opacity: 0.3;
1770
+ cursor: not-allowed;
1771
+ pointer-events: none;
1772
+ }
1773
+ .rf-date-picker__year-cell--disabled,
1774
+ .rf-date-picker__year-cell:disabled {
1775
+ opacity: 0.3;
1776
+ cursor: not-allowed;
1777
+ pointer-events: none;
1778
+ }
1779
+ .rf-date-picker__nav-btn:disabled {
1780
+ opacity: 0.3;
1781
+ cursor: not-allowed;
1782
+ pointer-events: none;
1783
+ }
1784
+ .rf-date-picker__footer-btn:disabled {
1785
+ opacity: 0.3;
1786
+ cursor: not-allowed;
1787
+ pointer-events: none;
1788
+ }
1761
1789
 
1762
1790
  /* lib/styles/date-range-field.css */
1763
1791
  .rf-date-range-field {
package/dist/main.d.cts CHANGED
@@ -627,6 +627,10 @@ interface DateFieldProps {
627
627
  type?: "date" | "datetime" | "datetime-side" | "datetime-scroll";
628
628
  /** Display format for the date in the input field. Default: "MM/DD/YYYY" */
629
629
  dateFormat?: DateFormatString;
630
+ /** Earliest selectable date — ISO string "YYYY-MM-DD" or a Date object */
631
+ minDate?: string | Date;
632
+ /** Latest selectable date — ISO string "YYYY-MM-DD" or a Date object */
633
+ maxDate?: string | Date;
630
634
  variant?: "outlined" | "filled" | "standard" | "compact";
631
635
  size?: "small" | "medium";
632
636
  color?: "primary" | "secondary" | "error" | "success" | "info" | "warning";
package/dist/main.d.ts CHANGED
@@ -627,6 +627,10 @@ interface DateFieldProps {
627
627
  type?: "date" | "datetime" | "datetime-side" | "datetime-scroll";
628
628
  /** Display format for the date in the input field. Default: "MM/DD/YYYY" */
629
629
  dateFormat?: DateFormatString;
630
+ /** Earliest selectable date — ISO string "YYYY-MM-DD" or a Date object */
631
+ minDate?: string | Date;
632
+ /** Latest selectable date — ISO string "YYYY-MM-DD" or a Date object */
633
+ maxDate?: string | Date;
630
634
  variant?: "outlined" | "filled" | "standard" | "compact";
631
635
  size?: "small" | "medium";
632
636
  color?: "primary" | "secondary" | "error" | "success" | "info" | "warning";
package/dist/main.js CHANGED
@@ -1665,6 +1665,28 @@ var TextField = forwardRef3(({
1665
1665
  className
1666
1666
  ].filter(Boolean).join(" ");
1667
1667
  const internalRef = React68.useRef(null);
1668
+ const handleChange = (e) => {
1669
+ if (type === "number") {
1670
+ const raw = e.target.value;
1671
+ const inputMax = slotProps?.input?.max ?? props.max;
1672
+ const inputMaxLength = slotProps?.input?.maxLength ?? props.maxLength;
1673
+ if (inputMaxLength != null) {
1674
+ const digits = raw.replace(/[^0-9]/g, "");
1675
+ if (digits.length > Number(inputMaxLength)) return;
1676
+ }
1677
+ if (inputMax != null && raw !== "") {
1678
+ if (Number(raw) > Number(inputMax)) return;
1679
+ }
1680
+ const numericValue = raw === "" ? "" : Number(raw);
1681
+ const syntheticEvent = Object.assign({}, e, {
1682
+ target: Object.assign({}, e.target, { value: numericValue }),
1683
+ currentTarget: Object.assign({}, e.currentTarget, { value: numericValue })
1684
+ });
1685
+ onChange?.(syntheticEvent);
1686
+ return;
1687
+ }
1688
+ onChange?.(e);
1689
+ };
1668
1690
  const triggerChange = () => {
1669
1691
  if (internalRef.current) {
1670
1692
  const nativeInputValueSetter = Object.getOwnPropertyDescriptor(window.HTMLInputElement.prototype, "value")?.set;
@@ -1728,14 +1750,14 @@ var TextField = forwardRef3(({
1728
1750
  name,
1729
1751
  id: inputId,
1730
1752
  value,
1731
- onChange,
1732
1753
  required,
1733
1754
  disabled,
1734
1755
  readOnly,
1735
1756
  step: type === "number" && numberVariant ? STEP_BY_VARIANT[numberVariant] : void 0,
1736
1757
  min: type === "number" && numberVariant ? MIN_BY_VARIANT[numberVariant] : void 0,
1737
1758
  ...slotProps?.input,
1738
- ...props
1759
+ ...props,
1760
+ onChange: handleChange
1739
1761
  }
1740
1762
  ), InputProps?.endAdornment && /* @__PURE__ */ React68.createElement("div", { className: "rf-text-field__adornment rf-text-field__adornment--end" }, InputProps.endAdornment), type === "number" && !disabled && !readOnly && /* @__PURE__ */ React68.createElement("div", { className: "rf-text-field__number-controls" }, /* @__PURE__ */ React68.createElement("button", { type: "button", tabIndex: -1, onClick: handleIncrement, className: "rf-text-field__number-btn" }, /* @__PURE__ */ React68.createElement("svg", { width: "8", height: "5", viewBox: "0 0 8 5", fill: "currentColor" }, /* @__PURE__ */ React68.createElement("path", { d: "M4 0L8 5H0L4 0Z" }))), /* @__PURE__ */ React68.createElement("button", { type: "button", tabIndex: -1, onClick: handleDecrement, className: "rf-text-field__number-btn", style: { marginTop: 2 } }, /* @__PURE__ */ React68.createElement("svg", { width: "8", height: "5", viewBox: "0 0 8 5", fill: "currentColor" }, /* @__PURE__ */ React68.createElement("path", { d: "M4 5L0 0H8L4 5Z" })))), hasLabel && /* @__PURE__ */ React68.createElement("label", { htmlFor: inputId, className: "rf-text-field__label" }, label, " ", required && /* @__PURE__ */ React68.createElement("span", { className: "rf-text-field__asterisk" }, "*")), variant === "outlined" && /* @__PURE__ */ React68.createElement("fieldset", { className: "rf-text-field__notch" }, hasLabel ? /* @__PURE__ */ React68.createElement("legend", { className: "rf-text-field__legend" }, /* @__PURE__ */ React68.createElement("span", null, label, " ", required ? "*" : "")) : /* @__PURE__ */ React68.createElement("legend", { className: "rf-text-field__legend--empty" }))), helperText && /* @__PURE__ */ React68.createElement("div", { className: "rf-text-field__helper-text" }, helperText));
1741
1763
  });
@@ -2816,6 +2838,14 @@ var today = () => {
2816
2838
  return new Date(t.getFullYear(), t.getMonth(), t.getDate());
2817
2839
  };
2818
2840
  var isSameDay = (a, b) => a.getFullYear() === b.getFullYear() && a.getMonth() === b.getMonth() && a.getDate() === b.getDate();
2841
+ var normaliseBoundary = (d) => {
2842
+ if (!d) return null;
2843
+ const base = d instanceof Date ? d : isoToDate(typeof d === "string" ? d.split("T")[0] : d);
2844
+ if (!base) return null;
2845
+ return new Date(base.getFullYear(), base.getMonth(), base.getDate());
2846
+ };
2847
+ var isBeforeDay = (a, b) => a.getFullYear() < b.getFullYear() || a.getFullYear() === b.getFullYear() && a.getMonth() < b.getMonth() || a.getFullYear() === b.getFullYear() && a.getMonth() === b.getMonth() && a.getDate() < b.getDate();
2848
+ var isAfterDay = (a, b) => a.getFullYear() > b.getFullYear() || a.getFullYear() === b.getFullYear() && a.getMonth() > b.getMonth() || a.getFullYear() === b.getFullYear() && a.getMonth() === b.getMonth() && a.getDate() > b.getDate();
2819
2849
  var isDatetimeType = (t) => t !== "date";
2820
2850
  var buildISO = (date, type, hour, minute, ampm) => {
2821
2851
  const y = date.getFullYear();
@@ -2976,15 +3006,13 @@ var CalendarBody = ({
2976
3006
  onPrev,
2977
3007
  onNext,
2978
3008
  onMonthSelect,
2979
- onYearSelect
3009
+ onYearSelect,
3010
+ minDate,
3011
+ maxDate
2980
3012
  }) => {
2981
3013
  const [pickerView, setPickerView] = useState7("calendar");
2982
- const handleMonthClick = () => {
2983
- setPickerView(pickerView === "month" ? "calendar" : "month");
2984
- };
2985
- const handleYearClick = () => {
2986
- setPickerView(pickerView === "year" ? "calendar" : "year");
2987
- };
3014
+ const handleMonthClick = () => setPickerView(pickerView === "month" ? "calendar" : "month");
3015
+ const handleYearClick = () => setPickerView(pickerView === "year" ? "calendar" : "year");
2988
3016
  const handleMonthPick = (month) => {
2989
3017
  onMonthSelect(month);
2990
3018
  setPickerView("calendar");
@@ -2996,6 +3024,22 @@ var CalendarBody = ({
2996
3024
  const currentYear = todayDate.getFullYear();
2997
3025
  const yearStart = viewYear - 6;
2998
3026
  const years = Array.from({ length: 16 }, (_, i) => yearStart + i);
3027
+ const prevMonthLastDay = new Date(viewYear, viewMonth, 0);
3028
+ const isPrevDisabled = minDate ? isBeforeDay(prevMonthLastDay, minDate) : false;
3029
+ const nextMonthFirstDay = new Date(viewYear, viewMonth + 1, 1);
3030
+ const isNextDisabled = maxDate ? isAfterDay(nextMonthFirstDay, maxDate) : false;
3031
+ const isMonthDisabled = (idx) => {
3032
+ const lastDayOfMonth = new Date(viewYear, idx + 1, 0);
3033
+ const firstDayOfMonth = new Date(viewYear, idx, 1);
3034
+ if (minDate && isBeforeDay(lastDayOfMonth, minDate)) return true;
3035
+ if (maxDate && isAfterDay(firstDayOfMonth, maxDate)) return true;
3036
+ return false;
3037
+ };
3038
+ const isYearDisabled = (y) => {
3039
+ if (minDate && y < minDate.getFullYear()) return true;
3040
+ if (maxDate && y > maxDate.getFullYear()) return true;
3041
+ return false;
3042
+ };
2999
3043
  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(
3000
3044
  "span",
3001
3045
  {
@@ -3010,48 +3054,61 @@ var CalendarBody = ({
3010
3054
  onClick: handleYearClick
3011
3055
  },
3012
3056
  viewYear
3013
- )), /* @__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, "aria-label": "Previous month" }, "\u2039"), /* @__PURE__ */ React72.createElement("button", { type: "button", className: "rf-date-picker__nav-btn", onClick: onNext, "aria-label": "Next month" }, "\u203A")))), pickerView === "month" && /* @__PURE__ */ React72.createElement("div", { className: "rf-date-picker__month-grid" }, MONTHS_SHORT.map((m, idx) => /* @__PURE__ */ React72.createElement(
3014
- "button",
3015
- {
3016
- key: m,
3017
- type: "button",
3018
- className: [
3019
- "rf-date-picker__month-cell",
3020
- idx === viewMonth ? "rf-date-picker__month-cell--selected" : "",
3021
- idx === todayDate.getMonth() && viewYear === currentYear ? "rf-date-picker__month-cell--current" : ""
3022
- ].filter(Boolean).join(" "),
3023
- onClick: () => handleMonthPick(idx)
3024
- },
3025
- m
3026
- ))), pickerView === "year" && /* @__PURE__ */ React72.createElement("div", { className: "rf-date-picker__year-grid" }, years.map((y) => /* @__PURE__ */ React72.createElement(
3027
- "button",
3028
- {
3029
- key: y,
3030
- type: "button",
3031
- className: [
3032
- "rf-date-picker__year-cell",
3033
- y === viewYear ? "rf-date-picker__year-cell--selected" : "",
3034
- y === currentYear ? "rf-date-picker__year-cell--current" : ""
3035
- ].filter(Boolean).join(" "),
3036
- onClick: () => handleYearPick(y)
3037
- },
3038
- y
3039
- ))), 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) => {
3057
+ )), /* @__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) => {
3058
+ const monthDisabled = isMonthDisabled(idx);
3059
+ return /* @__PURE__ */ React72.createElement(
3060
+ "button",
3061
+ {
3062
+ key: m,
3063
+ type: "button",
3064
+ disabled: monthDisabled,
3065
+ className: [
3066
+ "rf-date-picker__month-cell",
3067
+ idx === viewMonth ? "rf-date-picker__month-cell--selected" : "",
3068
+ idx === todayDate.getMonth() && viewYear === currentYear ? "rf-date-picker__month-cell--current" : "",
3069
+ monthDisabled ? "rf-date-picker__month-cell--disabled" : ""
3070
+ ].filter(Boolean).join(" "),
3071
+ onClick: () => !monthDisabled && handleMonthPick(idx)
3072
+ },
3073
+ m
3074
+ );
3075
+ })), pickerView === "year" && /* @__PURE__ */ React72.createElement("div", { className: "rf-date-picker__year-grid" }, years.map((y) => {
3076
+ const yearDisabled = isYearDisabled(y);
3077
+ return /* @__PURE__ */ React72.createElement(
3078
+ "button",
3079
+ {
3080
+ key: y,
3081
+ type: "button",
3082
+ disabled: yearDisabled,
3083
+ className: [
3084
+ "rf-date-picker__year-cell",
3085
+ y === viewYear ? "rf-date-picker__year-cell--selected" : "",
3086
+ y === currentYear ? "rf-date-picker__year-cell--current" : "",
3087
+ yearDisabled ? "rf-date-picker__year-cell--disabled" : ""
3088
+ ].filter(Boolean).join(" "),
3089
+ onClick: () => !yearDisabled && handleYearPick(y)
3090
+ },
3091
+ y
3092
+ );
3093
+ })), 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) => {
3040
3094
  if (day === null) return /* @__PURE__ */ React72.createElement("div", { key: `e-${idx}`, className: "rf-date-picker__day rf-date-picker__day--empty" });
3041
3095
  const cellDate = new Date(viewYear, viewMonth, day);
3042
3096
  const isSelected = selectedDate ? isSameDay(cellDate, selectedDate) : false;
3043
3097
  const isToday = isSameDay(cellDate, todayDate);
3098
+ const dayDisabled = (minDate ? isBeforeDay(cellDate, minDate) : false) || (maxDate ? isAfterDay(cellDate, maxDate) : false);
3044
3099
  return /* @__PURE__ */ React72.createElement(
3045
3100
  "button",
3046
3101
  {
3047
3102
  key: day,
3048
3103
  type: "button",
3104
+ disabled: dayDisabled,
3049
3105
  className: [
3050
3106
  "rf-date-picker__day",
3051
3107
  isSelected ? "rf-date-picker__day--selected" : "",
3052
- isToday && !isSelected ? "rf-date-picker__day--today" : ""
3108
+ isToday && !isSelected ? "rf-date-picker__day--today" : "",
3109
+ dayDisabled ? "rf-date-picker__day--disabled" : ""
3053
3110
  ].filter(Boolean).join(" "),
3054
- onClick: () => onDayClick(day)
3111
+ onClick: () => !dayDisabled && onDayClick(day)
3055
3112
  },
3056
3113
  day
3057
3114
  );
@@ -3063,6 +3120,8 @@ var DateField = ({
3063
3120
  onChange,
3064
3121
  type = "date",
3065
3122
  dateFormat = "MM/DD/YYYY",
3123
+ minDate: minDateProp,
3124
+ maxDate: maxDateProp,
3066
3125
  variant = "outlined",
3067
3126
  size = "medium",
3068
3127
  color = "primary",
@@ -3076,6 +3135,8 @@ var DateField = ({
3076
3135
  style,
3077
3136
  sx
3078
3137
  }) => {
3138
+ const minDate = normaliseBoundary(minDateProp);
3139
+ const maxDate = normaliseBoundary(maxDateProp);
3079
3140
  const sxClass = useSx(sx);
3080
3141
  const [open, setOpen] = useState7(false);
3081
3142
  const [selectedDate, setSelectedDate] = useState7(() => value ? isoToDate(value) : null);
@@ -3175,8 +3236,10 @@ var DateField = ({
3175
3236
  setInputStr(str);
3176
3237
  onChange?.(buildISO(d, type, h, m, ap));
3177
3238
  }, [type, onChange, dateFormat]);
3239
+ const isOutOfRange = (d) => (minDate ? isBeforeDay(d, minDate) : false) || (maxDate ? isAfterDay(d, maxDate) : false);
3178
3240
  const handleDayClick = (day) => {
3179
3241
  const d = new Date(viewYear, viewMonth, day);
3242
+ if (isOutOfRange(d)) return;
3180
3243
  setSelectedDate(d);
3181
3244
  let str = formatDisplay(d, dateFormat);
3182
3245
  if (isDatetimeType(type)) str += " " + formatTimeDisplay(hour, minute, ampm);
@@ -3186,6 +3249,7 @@ var DateField = ({
3186
3249
  };
3187
3250
  const handleToday = () => {
3188
3251
  const t = today();
3252
+ if (isOutOfRange(t)) return;
3189
3253
  setViewYear(t.getFullYear());
3190
3254
  setViewMonth(t.getMonth());
3191
3255
  commitDate(t, hour, minute, ampm);
@@ -3200,7 +3264,7 @@ var DateField = ({
3200
3264
  const datePart = words.slice(0, dateWordCount).join(" ");
3201
3265
  const timeParts = words.slice(dateWordCount);
3202
3266
  const parsed = parseDisplay(datePart, dateFormat);
3203
- if (parsed) {
3267
+ if (parsed && !isOutOfRange(parsed)) {
3204
3268
  setSelectedDate(parsed);
3205
3269
  setViewYear(parsed.getFullYear());
3206
3270
  setViewMonth(parsed.getMonth());
@@ -3394,7 +3458,9 @@ var DateField = ({
3394
3458
  onPrev: prevMonth,
3395
3459
  onNext: nextMonth,
3396
3460
  onMonthSelect: setViewMonth,
3397
- onYearSelect: setViewYear
3461
+ onYearSelect: setViewYear,
3462
+ minDate,
3463
+ maxDate
3398
3464
  }
3399
3465
  ), 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(
3400
3466
  SpinnerPanel,
@@ -3408,7 +3474,7 @@ var DateField = ({
3408
3474
  onMinuteInput: handleMinuteInput,
3409
3475
  onAmpmToggle: handleAmpmToggle
3410
3476
  }
3411
- )), /* @__PURE__ */ React72.createElement("div", { className: "rf-date-picker__divider" }), /* @__PURE__ */ React72.createElement("div", { className: "rf-date-picker__footer" }, /* @__PURE__ */ React72.createElement("button", { type: "button", className: "rf-date-picker__footer-btn", onClick: handleToday }, "Today"), /* @__PURE__ */ React72.createElement("button", { type: "button", className: "rf-date-picker__footer-btn rf-date-picker__footer-btn--clear", onClick: handleClear }, "Clear"))),
3477
+ )), /* @__PURE__ */ React72.createElement("div", { className: "rf-date-picker__divider" }), /* @__PURE__ */ React72.createElement("div", { className: "rf-date-picker__footer" }, /* @__PURE__ */ React72.createElement("button", { type: "button", className: "rf-date-picker__footer-btn", onClick: handleToday, disabled: isOutOfRange(todayDate) }, "Today"), /* @__PURE__ */ React72.createElement("button", { type: "button", className: "rf-date-picker__footer-btn rf-date-picker__footer-btn--clear", onClick: handleClear }, "Clear"))),
3412
3478
  type === "datetime-side" && /* @__PURE__ */ React72.createElement("div", { className: "rf-date-picker__side-panel" }, /* @__PURE__ */ React72.createElement("div", { className: "rf-date-picker__side-label" }, "Time"), /* @__PURE__ */ React72.createElement("div", { className: "rf-date-picker__side-spinner" }, /* @__PURE__ */ React72.createElement(
3413
3479
  SpinnerPanel,
3414
3480
  {
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@rufous/ui",
3
3
  "private": false,
4
- "version": "0.2.94",
4
+ "version": "0.2.96",
5
5
  "type": "module",
6
6
  "description": "Experimental: A lightweight React UI component library (Beta)",
7
7
  "style": "./dist/main.css",