@andreagiugni/tailwind-dashboard-ui 0.5.16 → 0.5.18

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/README.md CHANGED
@@ -167,11 +167,11 @@ Legend: **(+native)** = also extends the element's native HTML attributes.
167
167
  | `Alert` | `variant` (success/error/warning/info), `title`, `message`, `showLink`, `linkHref`, `linkText`, `icon?` (override icon), (+native div) |
168
168
  | `Avatar` | `src`, `alt`, `size` (xsmall…xxlarge), `status` (online/offline/busy/none), `statusColor?`, (+native img) |
169
169
  | `AvatarText` | `children`, `className`, (+native div) |
170
- | `Dropdown` | `isOpen`, `onClose`, `children`, `portal?` (default `true`, prevents clipping inside tables/overflow containers), (+native div) |
170
+ | `Dropdown` | `isOpen`, `onClose`, `children`, `portal?` (default `true`, prevents clipping inside cards/tables/overflow containers), `triggerRef?` (optional custom trigger anchor), (+native div) |
171
171
  | `DropdownItem` | `variant` (default/primary/outline, like Button), `icon?`, `bgColor`/`textColor`/`borderColor` overrides, `tag` (a/button), `href`, `onClick`, `onItemClick`, `baseClassName` (full override) |
172
172
  | `Modal` | `isOpen`, `onClose`, `title?` (wrapping header aligned with close button), `showCloseButton`, `isFullscreen`, `closeOnBackdrop?`, `closeOnEsc?`; **alert preset** → `variant` (`success`/`info`/`warning`/`danger`) + `title`, `description`, `actionText?`, `onAction?`; (+native div) |
173
173
  | `Table` (composable) | `TableHeader` / `TableBody` / `TableRow` / `TableCell` with `children`, `isHeader` (cell), (+native table elements; `onClick` / `onDoubleClick` on rows & cells) |
174
- | `Table` (data-driven) | pass `data` + `columns` (`key`, `header`, `sortable?`, `align?`, `render?`, `className?`) to get search + per-column sort + pagination: `rowsPerPage?` (max rows/page, default 10), `rowsPerPageOptions?`, `searchKeys?` (**search box shown only when provided**), `searchPlaceholder?`, `defaultSortKey?` / `defaultSortDirection?`, `pagination?`, `paginationAlign?` (`left`/`center`/`right`/`full`, default `right`), `showSizeSelector?`, `onRowClick?`, `getRowId?`, `emptyContent?` |
174
+ | `Table` (data-driven) | pass `data` + `columns` (`key`, `header`, `sortable?`, `align?`, `render?`, `className?`) to get search + three-state per-column sort (neutral/ascending/descending) + pagination: `rowsPerPage?` (max rows/page, default 10), `rowsPerPageOptions?`, `searchKeys?` (**search box shown only when provided**), `searchPlaceholder?`, `defaultSortKey?` / `defaultSortDirection?`, `pagination?`, `paginationAlign?` (`left`/`center`/`right`/`full`, default `right`), `showSizeSelector?`, `onRowClick?`, `getRowId?`, `emptyContent?` |
175
175
  | `Pagination` | `currentPage`, `totalPages`, `onPageChange`, `align?` (`left`/`center`/`right`/`full` — `full` pins Previous/Next to the edges with page indices centered) |
176
176
  | `Breadcrumb` | `pageTitle`, `items?`, (+native) |
177
177
  | `ThemeToggleButton` | `theme?` (controlled), `onToggle?`, (+native button) |
package/dist/index.cjs CHANGED
@@ -416,12 +416,20 @@ var AvatarText = ({
416
416
  }
417
417
  );
418
418
  };
419
+ function getTriggerElement(anchor, triggerRef) {
420
+ if (triggerRef?.current) return triggerRef.current;
421
+ if (!anchor) return null;
422
+ const previous = anchor.previousElementSibling;
423
+ const adjacentTrigger = previous?.matches(".dropdown-toggle, button, [aria-haspopup='menu']") ? previous : previous?.querySelector(".dropdown-toggle, button, [aria-haspopup='menu']");
424
+ return adjacentTrigger ?? anchor.parentElement?.querySelector(".dropdown-toggle");
425
+ }
419
426
  var Dropdown = ({
420
427
  isOpen,
421
428
  onClose,
422
429
  children,
423
430
  className,
424
431
  portal = true,
432
+ triggerRef,
425
433
  style,
426
434
  ...rest
427
435
  }) => {
@@ -430,7 +438,8 @@ var Dropdown = ({
430
438
  const [position, setPosition] = React5.useState();
431
439
  React5.useEffect(() => {
432
440
  const handleClickOutside = (event) => {
433
- if (dropdownRef.current && !dropdownRef.current.contains(event.target) && !event.target.closest(".dropdown-toggle")) {
441
+ const trigger = getTriggerElement(anchorRef.current, triggerRef);
442
+ if (dropdownRef.current && !dropdownRef.current.contains(event.target) && !trigger?.contains(event.target)) {
434
443
  onClose();
435
444
  }
436
445
  };
@@ -438,13 +447,13 @@ var Dropdown = ({
438
447
  return () => {
439
448
  document.removeEventListener("mousedown", handleClickOutside);
440
449
  };
441
- }, [onClose]);
450
+ }, [onClose, triggerRef]);
442
451
  React5.useLayoutEffect(() => {
443
452
  if (!isOpen || !portal) return;
444
453
  const updatePosition = () => {
445
454
  const anchor = anchorRef.current;
446
455
  const menu2 = dropdownRef.current;
447
- const trigger = anchor?.parentElement?.querySelector(".dropdown-toggle");
456
+ const trigger = getTriggerElement(anchor, triggerRef);
448
457
  if (!anchor || !menu2) return;
449
458
  const triggerRect = (trigger ?? anchor).getBoundingClientRect();
450
459
  const menuRect = menu2.getBoundingClientRect();
@@ -465,7 +474,7 @@ var Dropdown = ({
465
474
  window.removeEventListener("resize", updatePosition);
466
475
  window.removeEventListener("scroll", updatePosition, true);
467
476
  };
468
- }, [isOpen, portal]);
477
+ }, [isOpen, portal, triggerRef]);
469
478
  if (!isOpen) return null;
470
479
  const menu = /* @__PURE__ */ jsxRuntime.jsx(
471
480
  "div",
@@ -739,15 +748,63 @@ var alignClass = {
739
748
  right: "text-right"
740
749
  };
741
750
  function SortIcon({
742
- active,
743
- direction
751
+ state
744
752
  }) {
745
- const on = "text-brand-500";
746
- const off = "text-gray-400 dark:text-gray-600";
747
- return /* @__PURE__ */ jsxRuntime.jsxs("span", { className: "inline-flex flex-col leading-none", children: [
748
- /* @__PURE__ */ jsxRuntime.jsx("svg", { width: "10", height: "6", viewBox: "0 0 10 6", fill: "currentColor", "aria-hidden": "true", className: active && direction === "asc" ? on : off, children: /* @__PURE__ */ jsxRuntime.jsx("path", { d: "M5 0 9 5H1z" }) }),
749
- /* @__PURE__ */ jsxRuntime.jsx("svg", { width: "10", height: "6", viewBox: "0 0 10 6", fill: "currentColor", "aria-hidden": "true", className: active && direction === "desc" ? on : off, children: /* @__PURE__ */ jsxRuntime.jsx("path", { d: "M5 6 1 1h8z" }) })
750
- ] });
753
+ const active = "text-brand-500";
754
+ const inactive = "text-gray-400 dark:text-gray-500";
755
+ return /* @__PURE__ */ jsxRuntime.jsxs(
756
+ "span",
757
+ {
758
+ "data-sort-state": state,
759
+ className: "inline-flex shrink-0 flex-col items-center -space-y-1",
760
+ children: [
761
+ /* @__PURE__ */ jsxRuntime.jsx(
762
+ "svg",
763
+ {
764
+ "data-sort-direction": "asc",
765
+ width: "12",
766
+ height: "12",
767
+ viewBox: "0 0 12 12",
768
+ fill: "none",
769
+ "aria-hidden": "true",
770
+ className: state === "asc" ? active : inactive,
771
+ children: /* @__PURE__ */ jsxRuntime.jsx(
772
+ "path",
773
+ {
774
+ d: "M3.5 7.25 6 4.75l2.5 2.5",
775
+ stroke: "currentColor",
776
+ strokeWidth: "1.5",
777
+ strokeLinecap: "round",
778
+ strokeLinejoin: "round"
779
+ }
780
+ )
781
+ }
782
+ ),
783
+ /* @__PURE__ */ jsxRuntime.jsx(
784
+ "svg",
785
+ {
786
+ "data-sort-direction": "desc",
787
+ width: "12",
788
+ height: "12",
789
+ viewBox: "0 0 12 12",
790
+ fill: "none",
791
+ "aria-hidden": "true",
792
+ className: state === "desc" ? active : inactive,
793
+ children: /* @__PURE__ */ jsxRuntime.jsx(
794
+ "path",
795
+ {
796
+ d: "m3.5 4.75 2.5 2.5 2.5-2.5",
797
+ stroke: "currentColor",
798
+ strokeWidth: "1.5",
799
+ strokeLinecap: "round",
800
+ strokeLinejoin: "round"
801
+ }
802
+ )
803
+ }
804
+ )
805
+ ]
806
+ }
807
+ );
751
808
  }
752
809
  function DataDrivenTable({
753
810
  data,
@@ -797,10 +854,14 @@ function DataDrivenTable({
797
854
  const pageRows = pagination ? filtered.slice(start, start + pageSize) : filtered;
798
855
  const toggleSort = (col) => {
799
856
  if (!col.sortable) return;
800
- if (sortKey === col.key) setSortDir((d) => d === "asc" ? "desc" : "asc");
801
- else {
857
+ if (sortKey !== col.key) {
802
858
  setSortKey(col.key);
803
859
  setSortDir("asc");
860
+ } else if (sortDir === "asc") {
861
+ setSortDir("desc");
862
+ } else {
863
+ setSortKey(void 0);
864
+ setSortDir("asc");
804
865
  }
805
866
  setPage(1);
806
867
  };
@@ -878,8 +939,9 @@ function DataDrivenTable({
878
939
  TableCell,
879
940
  {
880
941
  isHeader: true,
942
+ "aria-sort": col.sortable ? isActive ? sortDir === "asc" ? "ascending" : "descending" : "none" : void 0,
881
943
  className: chunkYERNSNT4_cjs.cn(
882
- "whitespace-nowrap px-5 py-3 text-xs font-semibold text-gray-700 dark:text-gray-300",
944
+ "whitespace-nowrap px-5 py-3 text-xs font-semibold text-gray-500 dark:text-gray-400",
883
945
  alignClass[col.align ?? "left"],
884
946
  col.className
885
947
  ),
@@ -889,10 +951,10 @@ function DataDrivenTable({
889
951
  type: "button",
890
952
  onClick: () => toggleSort(col),
891
953
  "aria-label": `Sort by ${typeof col.header === "string" ? col.header : col.key}`,
892
- className: "inline-flex select-none items-center gap-1.5 hover:text-gray-900 dark:hover:text-white",
954
+ 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",
893
955
  children: [
894
- col.header,
895
- /* @__PURE__ */ jsxRuntime.jsx(SortIcon, { active: isActive, direction: isActive ? sortDir : "asc" })
956
+ /* @__PURE__ */ jsxRuntime.jsx(SortIcon, { state: isActive ? sortDir : "none" }),
957
+ col.header
896
958
  ]
897
959
  }
898
960
  ) : col.header