@andreagiugni/tailwind-dashboard-ui 0.5.15 → 0.5.17

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/index.d.cts CHANGED
@@ -81,6 +81,8 @@ declare const AvatarText: React.FC<AvatarTextProps>;
81
81
  interface DropdownProps extends React.HTMLAttributes<HTMLDivElement> {
82
82
  isOpen: boolean;
83
83
  onClose: () => void;
84
+ /** Render under document.body so menus are not clipped by overflow containers. Default: true. */
85
+ portal?: boolean;
84
86
  }
85
87
  declare const Dropdown: React.FC<DropdownProps>;
86
88
 
package/dist/index.d.ts CHANGED
@@ -81,6 +81,8 @@ declare const AvatarText: React.FC<AvatarTextProps>;
81
81
  interface DropdownProps extends React.HTMLAttributes<HTMLDivElement> {
82
82
  isOpen: boolean;
83
83
  onClose: () => void;
84
+ /** Render under document.body so menus are not clipped by overflow containers. Default: true. */
85
+ portal?: boolean;
84
86
  }
85
87
  declare const Dropdown: React.FC<DropdownProps>;
86
88
 
package/dist/index.js CHANGED
@@ -9,7 +9,8 @@ import { layers } from './chunk-VX7S6VYG.js';
9
9
  import { cn } from './chunk-ZLIYUUA4.js';
10
10
  export { cn } from './chunk-ZLIYUUA4.js';
11
11
  import { jsxs, jsx, Fragment } from 'react/jsx-runtime';
12
- import React5, { useRef, useEffect, useState, useCallback, useId, useMemo } from 'react';
12
+ import React5, { useRef, useState, useEffect, useLayoutEffect, useCallback, useId, useMemo } from 'react';
13
+ import { createPortal } from 'react-dom';
13
14
  import flatpickr from 'flatpickr';
14
15
  import 'flatpickr/dist/flatpickr.css';
15
16
 
@@ -414,9 +415,13 @@ var Dropdown = ({
414
415
  onClose,
415
416
  children,
416
417
  className,
418
+ portal = true,
419
+ style,
417
420
  ...rest
418
421
  }) => {
419
422
  const dropdownRef = useRef(null);
423
+ const anchorRef = useRef(null);
424
+ const [position, setPosition] = useState();
420
425
  useEffect(() => {
421
426
  const handleClickOutside = (event) => {
422
427
  if (dropdownRef.current && !dropdownRef.current.contains(event.target) && !event.target.closest(".dropdown-toggle")) {
@@ -428,20 +433,57 @@ var Dropdown = ({
428
433
  document.removeEventListener("mousedown", handleClickOutside);
429
434
  };
430
435
  }, [onClose]);
436
+ useLayoutEffect(() => {
437
+ if (!isOpen || !portal) return;
438
+ const updatePosition = () => {
439
+ const anchor = anchorRef.current;
440
+ const menu2 = dropdownRef.current;
441
+ const trigger = anchor?.parentElement?.querySelector(".dropdown-toggle");
442
+ if (!anchor || !menu2) return;
443
+ const triggerRect = (trigger ?? anchor).getBoundingClientRect();
444
+ const menuRect = menu2.getBoundingClientRect();
445
+ const gap = 8;
446
+ const viewportPadding = 8;
447
+ const fitsBelow = triggerRect.bottom + gap + menuRect.height <= window.innerHeight - viewportPadding;
448
+ const top = fitsBelow ? triggerRect.bottom + gap : Math.max(viewportPadding, triggerRect.top - gap - menuRect.height);
449
+ const left = Math.min(
450
+ window.innerWidth - viewportPadding - menuRect.width,
451
+ Math.max(viewportPadding, triggerRect.right - menuRect.width)
452
+ );
453
+ setPosition({ top, left });
454
+ };
455
+ updatePosition();
456
+ window.addEventListener("resize", updatePosition);
457
+ window.addEventListener("scroll", updatePosition, true);
458
+ return () => {
459
+ window.removeEventListener("resize", updatePosition);
460
+ window.removeEventListener("scroll", updatePosition, true);
461
+ };
462
+ }, [isOpen, portal]);
431
463
  if (!isOpen) return null;
432
- return /* @__PURE__ */ jsx(
464
+ const menu = /* @__PURE__ */ jsx(
433
465
  "div",
434
466
  {
435
467
  ref: dropdownRef,
436
468
  className: cn(
437
- "absolute right-0 mt-2 rounded-xl border border-gray-200 bg-white shadow-theme-lg dark:border-gray-800 dark:bg-gray-dark",
438
- layers.menu,
469
+ "rounded-xl border border-gray-200 bg-white shadow-theme-lg dark:border-gray-800 dark:bg-gray-dark",
470
+ portal ? cn("fixed", layers.portal) : cn("absolute right-0 mt-2", layers.menu),
439
471
  className
440
472
  ),
473
+ style: {
474
+ ...portal && !position ? { visibility: "hidden" } : void 0,
475
+ ...portal ? position : void 0,
476
+ ...style
477
+ },
441
478
  ...rest,
442
479
  children
443
480
  }
444
481
  );
482
+ if (!portal || typeof document === "undefined") return menu;
483
+ return /* @__PURE__ */ jsxs(Fragment, { children: [
484
+ /* @__PURE__ */ jsx("span", { ref: anchorRef, "aria-hidden": "true", className: "pointer-events-none absolute" }),
485
+ createPortal(menu, document.body)
486
+ ] });
445
487
  };
446
488
  var variantBase = "flex w-full items-center gap-2 rounded-lg px-3 py-2 text-left text-sm font-medium transition";
447
489
  var variantClasses3 = {
@@ -691,15 +733,63 @@ var alignClass = {
691
733
  right: "text-right"
692
734
  };
693
735
  function SortIcon({
694
- active,
695
- direction
736
+ state
696
737
  }) {
697
- const on = "text-brand-500";
698
- const off = "text-gray-400 dark:text-gray-600";
699
- return /* @__PURE__ */ jsxs("span", { className: "inline-flex flex-col leading-none", children: [
700
- /* @__PURE__ */ jsx("svg", { width: "10", height: "6", viewBox: "0 0 10 6", fill: "currentColor", "aria-hidden": "true", className: active && direction === "asc" ? on : off, children: /* @__PURE__ */ jsx("path", { d: "M5 0 9 5H1z" }) }),
701
- /* @__PURE__ */ jsx("svg", { width: "10", height: "6", viewBox: "0 0 10 6", fill: "currentColor", "aria-hidden": "true", className: active && direction === "desc" ? on : off, children: /* @__PURE__ */ jsx("path", { d: "M5 6 1 1h8z" }) })
702
- ] });
738
+ const active = "text-brand-500";
739
+ const inactive = "text-gray-400 dark:text-gray-500";
740
+ return /* @__PURE__ */ jsxs(
741
+ "span",
742
+ {
743
+ "data-sort-state": state,
744
+ className: "inline-flex shrink-0 flex-col items-center -space-y-1",
745
+ children: [
746
+ /* @__PURE__ */ jsx(
747
+ "svg",
748
+ {
749
+ "data-sort-direction": "asc",
750
+ width: "12",
751
+ height: "12",
752
+ viewBox: "0 0 12 12",
753
+ fill: "none",
754
+ "aria-hidden": "true",
755
+ className: state === "asc" ? active : inactive,
756
+ children: /* @__PURE__ */ jsx(
757
+ "path",
758
+ {
759
+ d: "M3.5 7.25 6 4.75l2.5 2.5",
760
+ stroke: "currentColor",
761
+ strokeWidth: "1.5",
762
+ strokeLinecap: "round",
763
+ strokeLinejoin: "round"
764
+ }
765
+ )
766
+ }
767
+ ),
768
+ /* @__PURE__ */ jsx(
769
+ "svg",
770
+ {
771
+ "data-sort-direction": "desc",
772
+ width: "12",
773
+ height: "12",
774
+ viewBox: "0 0 12 12",
775
+ fill: "none",
776
+ "aria-hidden": "true",
777
+ className: state === "desc" ? active : inactive,
778
+ children: /* @__PURE__ */ jsx(
779
+ "path",
780
+ {
781
+ d: "m3.5 4.75 2.5 2.5 2.5-2.5",
782
+ stroke: "currentColor",
783
+ strokeWidth: "1.5",
784
+ strokeLinecap: "round",
785
+ strokeLinejoin: "round"
786
+ }
787
+ )
788
+ }
789
+ )
790
+ ]
791
+ }
792
+ );
703
793
  }
704
794
  function DataDrivenTable({
705
795
  data,
@@ -749,10 +839,14 @@ function DataDrivenTable({
749
839
  const pageRows = pagination ? filtered.slice(start, start + pageSize) : filtered;
750
840
  const toggleSort = (col) => {
751
841
  if (!col.sortable) return;
752
- if (sortKey === col.key) setSortDir((d) => d === "asc" ? "desc" : "asc");
753
- else {
842
+ if (sortKey !== col.key) {
754
843
  setSortKey(col.key);
755
844
  setSortDir("asc");
845
+ } else if (sortDir === "asc") {
846
+ setSortDir("desc");
847
+ } else {
848
+ setSortKey(void 0);
849
+ setSortDir("asc");
756
850
  }
757
851
  setPage(1);
758
852
  };
@@ -830,8 +924,9 @@ function DataDrivenTable({
830
924
  TableCell,
831
925
  {
832
926
  isHeader: true,
927
+ "aria-sort": col.sortable ? isActive ? sortDir === "asc" ? "ascending" : "descending" : "none" : void 0,
833
928
  className: cn(
834
- "whitespace-nowrap px-5 py-3 text-xs font-semibold text-gray-700 dark:text-gray-300",
929
+ "whitespace-nowrap px-5 py-3 text-xs font-semibold text-gray-500 dark:text-gray-400",
835
930
  alignClass[col.align ?? "left"],
836
931
  col.className
837
932
  ),
@@ -841,10 +936,10 @@ function DataDrivenTable({
841
936
  type: "button",
842
937
  onClick: () => toggleSort(col),
843
938
  "aria-label": `Sort by ${typeof col.header === "string" ? col.header : col.key}`,
844
- className: "inline-flex select-none items-center gap-1.5 hover:text-gray-900 dark:hover:text-white",
939
+ className: "inline-flex select-none items-center gap-2 text-gray-500 transition-colors hover:text-gray-700 dark:text-gray-400 dark:hover:text-gray-200",
845
940
  children: [
846
- col.header,
847
- /* @__PURE__ */ jsx(SortIcon, { active: isActive, direction: isActive ? sortDir : "asc" })
941
+ /* @__PURE__ */ jsx(SortIcon, { state: isActive ? sortDir : "none" }),
942
+ col.header
848
943
  ]
849
944
  }
850
945
  ) : col.header