@diwauhris/ui 1.7.22 → 1.7.24

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/lib/assets/ui.css CHANGED
@@ -378,6 +378,10 @@
378
378
  right: 0.5rem;
379
379
  }
380
380
 
381
+ .right-2\.5 {
382
+ right: 0.625rem;
383
+ }
384
+
381
385
  .right-3 {
382
386
  right: 0.75rem;
383
387
  }
package/lib/index.cjs CHANGED
@@ -1398,7 +1398,7 @@ function getScrollParent(el) {
1398
1398
  if (["auto", "scroll"].includes(overflowY) || ["auto", "scroll"].includes(overflow)) return el;
1399
1399
  return getScrollParent(el.parentElement);
1400
1400
  }
1401
- function computePopupStyle(rect) {
1401
+ function computePopupStyle(rect, placement = "auto") {
1402
1402
  const spaceBelow = window.innerHeight - rect.bottom;
1403
1403
  const spaceAbove = rect.top;
1404
1404
  const isNarrow = window.innerWidth < 368;
@@ -1407,17 +1407,23 @@ function computePopupStyle(rect) {
1407
1407
  left: isNarrow ? 8 : Math.max(8, Math.min(rect.left, window.innerWidth - POPOVER_WIDTH - 8)),
1408
1408
  width: isNarrow ? `calc(100vw - 16px)` : void 0
1409
1409
  };
1410
- if (spaceAbove > spaceBelow && spaceBelow < CALENDAR_HEIGHT / 3) style.top = Math.max(8, rect.top - CALENDAR_HEIGHT - 6);
1411
- else style.top = rect.bottom + 6;
1410
+ if (placement === "below" ? true : placement === "above" ? false : spaceBelow >= CALENDAR_HEIGHT || spaceBelow >= spaceAbove) style.top = Math.min(rect.bottom + 6, window.innerHeight - CALENDAR_HEIGHT - 8);
1411
+ else style.top = Math.max(rect.top - CALENDAR_HEIGHT - 6, 8);
1412
1412
  return style;
1413
1413
  }
1414
- function DatePicker({ value, onChange, placeholder, className, disabled, error, minDate, maxDate, id, "aria-label": ariaLabel, "aria-labelledby": ariaLabelledBy, required }) {
1415
- const [isOpen, setIsOpen] = (0, react.useState)(false);
1414
+ function DatePicker({ value, onChange, placeholder, className, disabled, error, minDate, maxDate, id, "aria-label": ariaLabel, "aria-labelledby": ariaLabelledBy, required, dateFormat = "MMM d, yyyy", clearable = false, onOpenChange, placement = "auto", locale, name }) {
1415
+ const [isOpen, setIsOpenState] = (0, react.useState)(false);
1416
1416
  const [popupStyle, setPopupStyle] = (0, react.useState)({});
1417
1417
  const wrapperRef = (0, react.useRef)(null);
1418
1418
  const popoverRef = (0, react.useRef)(null);
1419
1419
  const inputRef = (0, react.useRef)(null);
1420
1420
  const internalId = (0, react.useId)();
1421
+ const setIsOpen = (0, react.useCallback)((next) => {
1422
+ setIsOpenState((prev) => {
1423
+ if (prev !== next) onOpenChange?.(next);
1424
+ return next;
1425
+ });
1426
+ }, [onOpenChange]);
1421
1427
  const ctx = useFieldContext();
1422
1428
  const inputId = id ?? ctx?.inputId ?? internalId;
1423
1429
  const resolvedRequired = required ?? ctx?.required ?? false;
@@ -1429,7 +1435,7 @@ function DatePicker({ value, onChange, placeholder, className, disabled, error,
1429
1435
  const d2 = new Date(value);
1430
1436
  return isNaN(d2.getTime()) ? void 0 : d2;
1431
1437
  })() : void 0;
1432
- const displayValue = selectedDate ? (0, date_fns.format)(selectedDate, "MMM d, yyyy") : "";
1438
+ const displayValue = selectedDate ? (0, date_fns.format)(selectedDate, dateFormat) : "";
1433
1439
  const skipNextOpen = (0, react.useRef)(false);
1434
1440
  const openPopup = (0, react.useCallback)(() => {
1435
1441
  if (disabled) return;
@@ -1441,12 +1447,16 @@ function DatePicker({ value, onChange, placeholder, className, disabled, error,
1441
1447
  if (!wrapperRef.current) return;
1442
1448
  requestAnimationFrame(() => {
1443
1449
  if (!wrapperRef.current) return;
1444
- const style = computePopupStyle(wrapperRef.current.getBoundingClientRect());
1450
+ const style = computePopupStyle(wrapperRef.current.getBoundingClientRect(), placement);
1445
1451
  setPopupStyle(style);
1446
1452
  setIsOpen(true);
1447
1453
  });
1448
1454
  }, 50);
1449
- }, [disabled]);
1455
+ }, [
1456
+ disabled,
1457
+ placement,
1458
+ setIsOpen
1459
+ ]);
1450
1460
  (0, react.useEffect)(() => {
1451
1461
  function handleClickOutside(e) {
1452
1462
  const target = e.target;
@@ -1504,6 +1514,7 @@ function DatePicker({ value, onChange, placeholder, className, disabled, error,
1504
1514
  children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
1505
1515
  ref: inputRef,
1506
1516
  id: inputId,
1517
+ name,
1507
1518
  type: "text",
1508
1519
  readOnly: true,
1509
1520
  value: displayValue,
@@ -1521,7 +1532,21 @@ function DatePicker({ value, onChange, placeholder, className, disabled, error,
1521
1532
  "aria-expanded": isOpen,
1522
1533
  role: "combobox",
1523
1534
  className: inputClasses
1524
- }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(lucide_react.CalendarDays, {
1535
+ }), clearable && value && !disabled ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
1536
+ type: "button",
1537
+ "aria-label": "Clear date",
1538
+ onMouseDown: (e) => {
1539
+ e.preventDefault();
1540
+ e.stopPropagation();
1541
+ },
1542
+ onClick: () => {
1543
+ onChange("");
1544
+ setIsOpen(false);
1545
+ inputRef.current?.focus();
1546
+ },
1547
+ className: "absolute right-2.5 top-1/2 -translate-y-1/2 flex h-5 w-5 items-center justify-center rounded-full text-slate-300 transition hover:bg-slate-100 hover:text-slate-600 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-brand-blue/30",
1548
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(lucide_react.X, { size: 14 })
1549
+ }) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)(lucide_react.CalendarDays, {
1525
1550
  size: 16,
1526
1551
  className: `absolute right-3 top-1/2 -translate-y-1/2 pointer-events-none ${isOpen ? "text-brand-blue" : "text-slate-300"}`,
1527
1552
  "aria-hidden": "true"
@@ -1546,6 +1571,7 @@ function DatePicker({ value, onChange, placeholder, className, disabled, error,
1546
1571
  defaultActiveStartDate: selectedDate ?? minDate,
1547
1572
  minDate,
1548
1573
  maxDate,
1574
+ locale,
1549
1575
  defaultView: "month",
1550
1576
  showNeighboringDecade: true,
1551
1577
  showNeighboringCentury: true,
@@ -2100,12 +2126,42 @@ var STYLES = {
2100
2126
  warning: "border-amber-200 bg-amber-50 text-amber-800",
2101
2127
  info: "border-sky-200 bg-sky-50 text-sky-800"
2102
2128
  };
2103
- function ErrorBanner({ message, tone = "error", className = "" }) {
2104
- return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
2129
+ function ErrorBanner({ message, title, tone = "error", className = "", icon, action, onDismiss, dismissLabel = "Dismiss" }) {
2130
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
2105
2131
  role: tone === "error" ? "alert" : void 0,
2106
2132
  "aria-live": tone !== "error" ? "polite" : void 0,
2107
- className: cn("rounded-2xl border px-5 py-4 text-sm font-bold", STYLES[tone], className),
2108
- children: message
2133
+ className: cn("flex items-start gap-3 rounded-2xl border px-5 py-4 text-sm font-bold", STYLES[tone], className),
2134
+ children: [
2135
+ icon && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
2136
+ className: "mt-0.5 shrink-0",
2137
+ "aria-hidden": "true",
2138
+ children: icon
2139
+ }),
2140
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
2141
+ className: "min-w-0 flex-1",
2142
+ children: [title && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
2143
+ className: "mb-0.5",
2144
+ children: title
2145
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
2146
+ className: cn(title && "font-medium"),
2147
+ children: message
2148
+ })]
2149
+ }),
2150
+ action && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
2151
+ className: "shrink-0",
2152
+ children: action
2153
+ }),
2154
+ onDismiss && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
2155
+ type: "button",
2156
+ onClick: onDismiss,
2157
+ className: "shrink-0 rounded-lg p-1 transition-colors hover:bg-black/5",
2158
+ "aria-label": dismissLabel,
2159
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(lucide_react.X, {
2160
+ size: 16,
2161
+ "aria-hidden": "true"
2162
+ })
2163
+ })
2164
+ ]
2109
2165
  });
2110
2166
  }
2111
2167
  //#endregion
package/lib/index.mjs CHANGED
@@ -1374,7 +1374,7 @@ function getScrollParent(el) {
1374
1374
  if (["auto", "scroll"].includes(overflowY) || ["auto", "scroll"].includes(overflow)) return el;
1375
1375
  return getScrollParent(el.parentElement);
1376
1376
  }
1377
- function computePopupStyle(rect) {
1377
+ function computePopupStyle(rect, placement = "auto") {
1378
1378
  const spaceBelow = window.innerHeight - rect.bottom;
1379
1379
  const spaceAbove = rect.top;
1380
1380
  const isNarrow = window.innerWidth < 368;
@@ -1383,17 +1383,23 @@ function computePopupStyle(rect) {
1383
1383
  left: isNarrow ? 8 : Math.max(8, Math.min(rect.left, window.innerWidth - POPOVER_WIDTH - 8)),
1384
1384
  width: isNarrow ? `calc(100vw - 16px)` : void 0
1385
1385
  };
1386
- if (spaceAbove > spaceBelow && spaceBelow < CALENDAR_HEIGHT / 3) style.top = Math.max(8, rect.top - CALENDAR_HEIGHT - 6);
1387
- else style.top = rect.bottom + 6;
1386
+ if (placement === "below" ? true : placement === "above" ? false : spaceBelow >= CALENDAR_HEIGHT || spaceBelow >= spaceAbove) style.top = Math.min(rect.bottom + 6, window.innerHeight - CALENDAR_HEIGHT - 8);
1387
+ else style.top = Math.max(rect.top - CALENDAR_HEIGHT - 6, 8);
1388
1388
  return style;
1389
1389
  }
1390
- function DatePicker({ value, onChange, placeholder, className, disabled, error, minDate, maxDate, id, "aria-label": ariaLabel, "aria-labelledby": ariaLabelledBy, required }) {
1391
- const [isOpen, setIsOpen] = useState(false);
1390
+ function DatePicker({ value, onChange, placeholder, className, disabled, error, minDate, maxDate, id, "aria-label": ariaLabel, "aria-labelledby": ariaLabelledBy, required, dateFormat = "MMM d, yyyy", clearable = false, onOpenChange, placement = "auto", locale, name }) {
1391
+ const [isOpen, setIsOpenState] = useState(false);
1392
1392
  const [popupStyle, setPopupStyle] = useState({});
1393
1393
  const wrapperRef = useRef(null);
1394
1394
  const popoverRef = useRef(null);
1395
1395
  const inputRef = useRef(null);
1396
1396
  const internalId = useId();
1397
+ const setIsOpen = useCallback((next) => {
1398
+ setIsOpenState((prev) => {
1399
+ if (prev !== next) onOpenChange?.(next);
1400
+ return next;
1401
+ });
1402
+ }, [onOpenChange]);
1397
1403
  const ctx = useFieldContext();
1398
1404
  const inputId = id ?? ctx?.inputId ?? internalId;
1399
1405
  const resolvedRequired = required ?? ctx?.required ?? false;
@@ -1405,7 +1411,7 @@ function DatePicker({ value, onChange, placeholder, className, disabled, error,
1405
1411
  const d2 = new Date(value);
1406
1412
  return isNaN(d2.getTime()) ? void 0 : d2;
1407
1413
  })() : void 0;
1408
- const displayValue = selectedDate ? format(selectedDate, "MMM d, yyyy") : "";
1414
+ const displayValue = selectedDate ? format(selectedDate, dateFormat) : "";
1409
1415
  const skipNextOpen = useRef(false);
1410
1416
  const openPopup = useCallback(() => {
1411
1417
  if (disabled) return;
@@ -1417,12 +1423,16 @@ function DatePicker({ value, onChange, placeholder, className, disabled, error,
1417
1423
  if (!wrapperRef.current) return;
1418
1424
  requestAnimationFrame(() => {
1419
1425
  if (!wrapperRef.current) return;
1420
- const style = computePopupStyle(wrapperRef.current.getBoundingClientRect());
1426
+ const style = computePopupStyle(wrapperRef.current.getBoundingClientRect(), placement);
1421
1427
  setPopupStyle(style);
1422
1428
  setIsOpen(true);
1423
1429
  });
1424
1430
  }, 50);
1425
- }, [disabled]);
1431
+ }, [
1432
+ disabled,
1433
+ placement,
1434
+ setIsOpen
1435
+ ]);
1426
1436
  useEffect(() => {
1427
1437
  function handleClickOutside(e) {
1428
1438
  const target = e.target;
@@ -1480,6 +1490,7 @@ function DatePicker({ value, onChange, placeholder, className, disabled, error,
1480
1490
  children: [/* @__PURE__ */ jsx("input", {
1481
1491
  ref: inputRef,
1482
1492
  id: inputId,
1493
+ name,
1483
1494
  type: "text",
1484
1495
  readOnly: true,
1485
1496
  value: displayValue,
@@ -1497,7 +1508,21 @@ function DatePicker({ value, onChange, placeholder, className, disabled, error,
1497
1508
  "aria-expanded": isOpen,
1498
1509
  role: "combobox",
1499
1510
  className: inputClasses
1500
- }), /* @__PURE__ */ jsx(CalendarDays, {
1511
+ }), clearable && value && !disabled ? /* @__PURE__ */ jsx("button", {
1512
+ type: "button",
1513
+ "aria-label": "Clear date",
1514
+ onMouseDown: (e) => {
1515
+ e.preventDefault();
1516
+ e.stopPropagation();
1517
+ },
1518
+ onClick: () => {
1519
+ onChange("");
1520
+ setIsOpen(false);
1521
+ inputRef.current?.focus();
1522
+ },
1523
+ className: "absolute right-2.5 top-1/2 -translate-y-1/2 flex h-5 w-5 items-center justify-center rounded-full text-slate-300 transition hover:bg-slate-100 hover:text-slate-600 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-brand-blue/30",
1524
+ children: /* @__PURE__ */ jsx(X, { size: 14 })
1525
+ }) : /* @__PURE__ */ jsx(CalendarDays, {
1501
1526
  size: 16,
1502
1527
  className: `absolute right-3 top-1/2 -translate-y-1/2 pointer-events-none ${isOpen ? "text-brand-blue" : "text-slate-300"}`,
1503
1528
  "aria-hidden": "true"
@@ -1522,6 +1547,7 @@ function DatePicker({ value, onChange, placeholder, className, disabled, error,
1522
1547
  defaultActiveStartDate: selectedDate ?? minDate,
1523
1548
  minDate,
1524
1549
  maxDate,
1550
+ locale,
1525
1551
  defaultView: "month",
1526
1552
  showNeighboringDecade: true,
1527
1553
  showNeighboringCentury: true,
@@ -2076,12 +2102,42 @@ var STYLES = {
2076
2102
  warning: "border-amber-200 bg-amber-50 text-amber-800",
2077
2103
  info: "border-sky-200 bg-sky-50 text-sky-800"
2078
2104
  };
2079
- function ErrorBanner({ message, tone = "error", className = "" }) {
2080
- return /* @__PURE__ */ jsx("div", {
2105
+ function ErrorBanner({ message, title, tone = "error", className = "", icon, action, onDismiss, dismissLabel = "Dismiss" }) {
2106
+ return /* @__PURE__ */ jsxs("div", {
2081
2107
  role: tone === "error" ? "alert" : void 0,
2082
2108
  "aria-live": tone !== "error" ? "polite" : void 0,
2083
- className: cn("rounded-2xl border px-5 py-4 text-sm font-bold", STYLES[tone], className),
2084
- children: message
2109
+ className: cn("flex items-start gap-3 rounded-2xl border px-5 py-4 text-sm font-bold", STYLES[tone], className),
2110
+ children: [
2111
+ icon && /* @__PURE__ */ jsx("span", {
2112
+ className: "mt-0.5 shrink-0",
2113
+ "aria-hidden": "true",
2114
+ children: icon
2115
+ }),
2116
+ /* @__PURE__ */ jsxs("div", {
2117
+ className: "min-w-0 flex-1",
2118
+ children: [title && /* @__PURE__ */ jsx("div", {
2119
+ className: "mb-0.5",
2120
+ children: title
2121
+ }), /* @__PURE__ */ jsx("span", {
2122
+ className: cn(title && "font-medium"),
2123
+ children: message
2124
+ })]
2125
+ }),
2126
+ action && /* @__PURE__ */ jsx("div", {
2127
+ className: "shrink-0",
2128
+ children: action
2129
+ }),
2130
+ onDismiss && /* @__PURE__ */ jsx("button", {
2131
+ type: "button",
2132
+ onClick: onDismiss,
2133
+ className: "shrink-0 rounded-lg p-1 transition-colors hover:bg-black/5",
2134
+ "aria-label": dismissLabel,
2135
+ children: /* @__PURE__ */ jsx(X, {
2136
+ size: 16,
2137
+ "aria-hidden": "true"
2138
+ })
2139
+ })
2140
+ ]
2085
2141
  });
2086
2142
  }
2087
2143
  //#endregion
@@ -12,6 +12,27 @@ interface DatePickerProps {
12
12
  'aria-label'?: string;
13
13
  'aria-labelledby'?: string;
14
14
  required?: boolean;
15
+ /**
16
+ * date-fns format string for the text shown in the trigger.
17
+ * The committed `onChange` value is always ISO `yyyy-MM-dd` regardless of this.
18
+ * Default: 'MMM d, yyyy'.
19
+ */
20
+ dateFormat?: string;
21
+ /** Show a clear (×) button that resets the value to '' when a date is set. */
22
+ clearable?: boolean;
23
+ /** Notified whenever the calendar popover opens or closes. */
24
+ onOpenChange?: (open: boolean) => void;
25
+ /**
26
+ * Popover placement relative to the trigger.
27
+ * 'auto' (default) flips above/below based on viewport space;
28
+ * 'below'/'above' force a side (still clamped to stay on-screen).
29
+ */
30
+ placement?: 'auto' | 'below' | 'above';
31
+ /** BCP-47 locale forwarded to the calendar (e.g. 'en-US', 'fr-FR'). */
32
+ locale?: string;
33
+ /** Name forwarded to the trigger input for form association. */
34
+ name?: string;
15
35
  }
16
- export declare function DatePicker({ value, onChange, placeholder, className, disabled, error, minDate, maxDate, id, 'aria-label': ariaLabel, 'aria-labelledby': ariaLabelledBy, required, }: DatePickerProps): import("react").JSX.Element;
36
+ export declare function computePopupStyle(rect: DOMRect, placement?: 'auto' | 'below' | 'above'): React.CSSProperties;
37
+ export declare function DatePicker({ value, onChange, placeholder, className, disabled, error, minDate, maxDate, id, 'aria-label': ariaLabel, 'aria-labelledby': ariaLabelledBy, required, dateFormat, clearable, onOpenChange, placement, locale, name, }: DatePickerProps): import("react").JSX.Element;
17
38
  export {};
@@ -16,13 +16,25 @@
16
16
  * <ErrorBanner message="Failed to save changes." />
17
17
  * <ErrorBanner message="Draft saved." tone="info" />
18
18
  * <ErrorBanner message="This action cannot be undone." tone="warning" />
19
+ * <ErrorBanner message="Failed to save." onDismiss={clearError} />
20
+ * <ErrorBanner title="Upload failed" message="The file was too large." icon={<AlertTriangle size={18} />} action={<Button onClick={retry}>Retry</Button>} />
19
21
  */
20
22
  import type { ReactNode } from 'react';
21
23
  export type ErrorBannerTone = 'error' | 'warning' | 'info';
22
24
  export interface ErrorBannerProps {
23
25
  /** Primary message text */
24
26
  message: ReactNode;
27
+ /** Optional bold heading rendered above the message. */
28
+ title?: ReactNode;
25
29
  tone?: ErrorBannerTone;
26
30
  className?: string;
31
+ /** Optional leading icon rendered before the text. */
32
+ icon?: ReactNode;
33
+ /** Optional trailing action (e.g. a Retry button) rendered before the close button. */
34
+ action?: ReactNode;
35
+ /** When provided, renders a close button that invokes this handler. */
36
+ onDismiss?: () => void;
37
+ /** Accessible label for the close button. Defaults to "Dismiss". */
38
+ dismissLabel?: string;
27
39
  }
28
- export declare function ErrorBanner({ message, tone, className }: ErrorBannerProps): import("react").JSX.Element;
40
+ export declare function ErrorBanner({ message, title, tone, className, icon, action, onDismiss, dismissLabel, }: ErrorBannerProps): import("react").JSX.Element;
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@diwauhris/ui",
3
3
  "private": false,
4
- "version": "1.7.22",
4
+ "version": "1.7.24",
5
5
  "type": "module",
6
6
  "description": "DIWA UHRIS UI component library — React + Tailwind CSS",
7
7
  "keywords": [