@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/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`, (+native div) |
170
+ | `Dropdown` | `isOpen`, `onClose`, `children`, `portal?` (default `true`, prevents clipping inside tables/overflow containers), (+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
@@ -11,6 +11,7 @@ var chunkTF3G3E72_cjs = require('./chunk-TF3G3E72.cjs');
11
11
  var chunkYERNSNT4_cjs = require('./chunk-YERNSNT4.cjs');
12
12
  var jsxRuntime = require('react/jsx-runtime');
13
13
  var React5 = require('react');
14
+ var reactDom = require('react-dom');
14
15
  var flatpickr = require('flatpickr');
15
16
  require('flatpickr/dist/flatpickr.css');
16
17
 
@@ -420,9 +421,13 @@ var Dropdown = ({
420
421
  onClose,
421
422
  children,
422
423
  className,
424
+ portal = true,
425
+ style,
423
426
  ...rest
424
427
  }) => {
425
428
  const dropdownRef = React5.useRef(null);
429
+ const anchorRef = React5.useRef(null);
430
+ const [position, setPosition] = React5.useState();
426
431
  React5.useEffect(() => {
427
432
  const handleClickOutside = (event) => {
428
433
  if (dropdownRef.current && !dropdownRef.current.contains(event.target) && !event.target.closest(".dropdown-toggle")) {
@@ -434,20 +439,57 @@ var Dropdown = ({
434
439
  document.removeEventListener("mousedown", handleClickOutside);
435
440
  };
436
441
  }, [onClose]);
442
+ React5.useLayoutEffect(() => {
443
+ if (!isOpen || !portal) return;
444
+ const updatePosition = () => {
445
+ const anchor = anchorRef.current;
446
+ const menu2 = dropdownRef.current;
447
+ const trigger = anchor?.parentElement?.querySelector(".dropdown-toggle");
448
+ if (!anchor || !menu2) return;
449
+ const triggerRect = (trigger ?? anchor).getBoundingClientRect();
450
+ const menuRect = menu2.getBoundingClientRect();
451
+ const gap = 8;
452
+ const viewportPadding = 8;
453
+ const fitsBelow = triggerRect.bottom + gap + menuRect.height <= window.innerHeight - viewportPadding;
454
+ const top = fitsBelow ? triggerRect.bottom + gap : Math.max(viewportPadding, triggerRect.top - gap - menuRect.height);
455
+ const left = Math.min(
456
+ window.innerWidth - viewportPadding - menuRect.width,
457
+ Math.max(viewportPadding, triggerRect.right - menuRect.width)
458
+ );
459
+ setPosition({ top, left });
460
+ };
461
+ updatePosition();
462
+ window.addEventListener("resize", updatePosition);
463
+ window.addEventListener("scroll", updatePosition, true);
464
+ return () => {
465
+ window.removeEventListener("resize", updatePosition);
466
+ window.removeEventListener("scroll", updatePosition, true);
467
+ };
468
+ }, [isOpen, portal]);
437
469
  if (!isOpen) return null;
438
- return /* @__PURE__ */ jsxRuntime.jsx(
470
+ const menu = /* @__PURE__ */ jsxRuntime.jsx(
439
471
  "div",
440
472
  {
441
473
  ref: dropdownRef,
442
474
  className: chunkYERNSNT4_cjs.cn(
443
- "absolute right-0 mt-2 rounded-xl border border-gray-200 bg-white shadow-theme-lg dark:border-gray-800 dark:bg-gray-dark",
444
- chunkTF3G3E72_cjs.layers.menu,
475
+ "rounded-xl border border-gray-200 bg-white shadow-theme-lg dark:border-gray-800 dark:bg-gray-dark",
476
+ portal ? chunkYERNSNT4_cjs.cn("fixed", chunkTF3G3E72_cjs.layers.portal) : chunkYERNSNT4_cjs.cn("absolute right-0 mt-2", chunkTF3G3E72_cjs.layers.menu),
445
477
  className
446
478
  ),
479
+ style: {
480
+ ...portal && !position ? { visibility: "hidden" } : void 0,
481
+ ...portal ? position : void 0,
482
+ ...style
483
+ },
447
484
  ...rest,
448
485
  children
449
486
  }
450
487
  );
488
+ if (!portal || typeof document === "undefined") return menu;
489
+ return /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
490
+ /* @__PURE__ */ jsxRuntime.jsx("span", { ref: anchorRef, "aria-hidden": "true", className: "pointer-events-none absolute" }),
491
+ reactDom.createPortal(menu, document.body)
492
+ ] });
451
493
  };
452
494
  var variantBase = "flex w-full items-center gap-2 rounded-lg px-3 py-2 text-left text-sm font-medium transition";
453
495
  var variantClasses3 = {
@@ -697,15 +739,63 @@ var alignClass = {
697
739
  right: "text-right"
698
740
  };
699
741
  function SortIcon({
700
- active,
701
- direction
742
+ state
702
743
  }) {
703
- const on = "text-brand-500";
704
- const off = "text-gray-400 dark:text-gray-600";
705
- return /* @__PURE__ */ jsxRuntime.jsxs("span", { className: "inline-flex flex-col leading-none", children: [
706
- /* @__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" }) }),
707
- /* @__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" }) })
708
- ] });
744
+ const active = "text-brand-500";
745
+ const inactive = "text-gray-400 dark:text-gray-500";
746
+ return /* @__PURE__ */ jsxRuntime.jsxs(
747
+ "span",
748
+ {
749
+ "data-sort-state": state,
750
+ className: "inline-flex shrink-0 flex-col items-center -space-y-1",
751
+ children: [
752
+ /* @__PURE__ */ jsxRuntime.jsx(
753
+ "svg",
754
+ {
755
+ "data-sort-direction": "asc",
756
+ width: "12",
757
+ height: "12",
758
+ viewBox: "0 0 12 12",
759
+ fill: "none",
760
+ "aria-hidden": "true",
761
+ className: state === "asc" ? active : inactive,
762
+ children: /* @__PURE__ */ jsxRuntime.jsx(
763
+ "path",
764
+ {
765
+ d: "M3.5 7.25 6 4.75l2.5 2.5",
766
+ stroke: "currentColor",
767
+ strokeWidth: "1.5",
768
+ strokeLinecap: "round",
769
+ strokeLinejoin: "round"
770
+ }
771
+ )
772
+ }
773
+ ),
774
+ /* @__PURE__ */ jsxRuntime.jsx(
775
+ "svg",
776
+ {
777
+ "data-sort-direction": "desc",
778
+ width: "12",
779
+ height: "12",
780
+ viewBox: "0 0 12 12",
781
+ fill: "none",
782
+ "aria-hidden": "true",
783
+ className: state === "desc" ? active : inactive,
784
+ children: /* @__PURE__ */ jsxRuntime.jsx(
785
+ "path",
786
+ {
787
+ d: "m3.5 4.75 2.5 2.5 2.5-2.5",
788
+ stroke: "currentColor",
789
+ strokeWidth: "1.5",
790
+ strokeLinecap: "round",
791
+ strokeLinejoin: "round"
792
+ }
793
+ )
794
+ }
795
+ )
796
+ ]
797
+ }
798
+ );
709
799
  }
710
800
  function DataDrivenTable({
711
801
  data,
@@ -755,10 +845,14 @@ function DataDrivenTable({
755
845
  const pageRows = pagination ? filtered.slice(start, start + pageSize) : filtered;
756
846
  const toggleSort = (col) => {
757
847
  if (!col.sortable) return;
758
- if (sortKey === col.key) setSortDir((d) => d === "asc" ? "desc" : "asc");
759
- else {
848
+ if (sortKey !== col.key) {
760
849
  setSortKey(col.key);
761
850
  setSortDir("asc");
851
+ } else if (sortDir === "asc") {
852
+ setSortDir("desc");
853
+ } else {
854
+ setSortKey(void 0);
855
+ setSortDir("asc");
762
856
  }
763
857
  setPage(1);
764
858
  };
@@ -836,8 +930,9 @@ function DataDrivenTable({
836
930
  TableCell,
837
931
  {
838
932
  isHeader: true,
933
+ "aria-sort": col.sortable ? isActive ? sortDir === "asc" ? "ascending" : "descending" : "none" : void 0,
839
934
  className: chunkYERNSNT4_cjs.cn(
840
- "whitespace-nowrap px-5 py-3 text-xs font-semibold text-gray-700 dark:text-gray-300",
935
+ "whitespace-nowrap px-5 py-3 text-xs font-semibold text-gray-500 dark:text-gray-400",
841
936
  alignClass[col.align ?? "left"],
842
937
  col.className
843
938
  ),
@@ -847,10 +942,10 @@ function DataDrivenTable({
847
942
  type: "button",
848
943
  onClick: () => toggleSort(col),
849
944
  "aria-label": `Sort by ${typeof col.header === "string" ? col.header : col.key}`,
850
- className: "inline-flex select-none items-center gap-1.5 hover:text-gray-900 dark:hover:text-white",
945
+ 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",
851
946
  children: [
852
- col.header,
853
- /* @__PURE__ */ jsxRuntime.jsx(SortIcon, { active: isActive, direction: isActive ? sortDir : "asc" })
947
+ /* @__PURE__ */ jsxRuntime.jsx(SortIcon, { state: isActive ? sortDir : "none" }),
948
+ col.header
854
949
  ]
855
950
  }
856
951
  ) : col.header