@andreagiugni/tailwind-dashboard-ui 0.2.0 → 0.4.0

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
@@ -152,9 +152,9 @@ Legend: **(+native)** = also extends the element's native HTML attributes.
152
152
  | `Dropdown` | `isOpen`, `onClose`, `children`, (+native div) |
153
153
  | `DropdownItem` | `variant` (default/primary/outline, like Button), `bgColor`/`textColor`/`borderColor` overrides, `tag` (a/button), `href`, `onClick`, `onItemClick`, `baseClassName` (full override) |
154
154
  | `Modal` | `isOpen`, `onClose`, `showCloseButton`, `isFullscreen`, `closeOnBackdrop?`, `closeOnEsc?`; **alert preset** → `variant` (`success`/`info`/`warning`/`danger`) + `title`, `description`, `actionText?`, `onAction?`; (+native div) |
155
- | `Table` / `TableHeader` / `TableBody` / `TableRow` / `TableCell` | `children`, `isHeader` (cell), (+native table elements; `onClick` / `onDoubleClick` on rows & cells) |
156
- | `Pagination` | `currentPage`, `totalPages`, `onPageChange` |
157
- | `DataTable` | `data`, `columns` (`key`, `header`, `sortable?`, `align?`, `render?`), `rowsPerPage?`, `rowsPerPageOptions?`, `searchable?`, `searchKeys?`, `defaultSortKey?` / `defaultSortDirection?`, `pagination?`, `showSizeSelector?`, `onRowClick?`, `getRowId?`, `emptyContent?` generic, search + per-column sort + pagination built in |
155
+ | `Table` (composable) | `TableHeader` / `TableBody` / `TableRow` / `TableCell` with `children`, `isHeader` (cell), (+native table elements; `onClick` / `onDoubleClick` on rows & cells) |
156
+ | `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?` |
157
+ | `Pagination` | `currentPage`, `totalPages`, `onPageChange`, `align?` (`left`/`center`/`right`/`full` `full` pins Previous/Next to the edges with page indices centered) |
158
158
  | `Breadcrumb` | `pageTitle`, `items?`, (+native) |
159
159
  | `ThemeToggleButton` | `theme?` (controlled), `onToggle?`, (+native button) |
160
160
  | `Card` | `title?`, `description?`, `icon?`, `image?`, `imageAlt?`, `horizontal?`, `footer?`, `children?`, (+native div) (+override) |
@@ -232,14 +232,18 @@ const Editor = dynamic(
232
232
  />
233
233
  ```
234
234
 
235
- ## Data table
235
+ ## Table
236
236
 
237
- `DataTable` is a generic, batteries-included table: pass your `data` array and a `columns`
238
- definition and you get live search, per-column click-to-sort, a "Show N entries" selector and
239
- pagination out of the box. Light/dark styling follows the standard `.dark` variant.
237
+ `Table` has **two modes**, both under the same component:
238
+
239
+ 1. **Composable** use `Table` / `TableHeader` / `TableBody` / `TableRow` / `TableCell` as thin,
240
+ styled wrappers around the native table elements when you want full control over the markup.
241
+ 2. **Data-driven** — pass a `data` array and a `columns` definition and you get live search,
242
+ per-column click-to-sort, a "Show N entries" selector and pagination out of the box. The mode
243
+ switches on automatically as soon as `columns` is provided.
240
244
 
241
245
  ```tsx
242
- import { DataTable, type DataTableColumn } from '@andreagiugni/tailwind-dashboard-ui';
246
+ import { Table, type TableColumn } from '@andreagiugni/tailwind-dashboard-ui';
243
247
 
244
248
  type Person = { name: string; office: string; salary: number };
245
249
 
@@ -249,7 +253,7 @@ const people: Person[] = [
249
253
  // …
250
254
  ];
251
255
 
252
- const columns: DataTableColumn<Person>[] = [
256
+ const columns: TableColumn<Person>[] = [
253
257
  { key: 'name', header: 'User', sortable: true },
254
258
  { key: 'office', header: 'Office', sortable: true },
255
259
  {
@@ -261,23 +265,26 @@ const columns: DataTableColumn<Person>[] = [
261
265
  },
262
266
  ];
263
267
 
264
- <DataTable
268
+ <Table
265
269
  data={people}
266
270
  columns={columns}
267
- rowsPerPage={5}
271
+ rowsPerPage={5} // max rows per page (default 10)
268
272
  rowsPerPageOptions={[5, 10, 25]}
269
273
  defaultSortKey="name"
270
- searchKeys={['name', 'office']}
274
+ searchKeys={['name', 'office']} // omit to hide the search box
271
275
  onRowClick={(row) => console.log(row)}
272
276
  />
273
277
  ```
274
278
 
275
- Key props: `data`, `columns` (each: `key`, `header`, `sortable?`, `align?`, `render?`,
279
+ Key data-driven props: `data`, `columns` (each: `key`, `header`, `sortable?`, `align?`, `render?`,
276
280
  `className?`), `rowsPerPage` (default 10), `rowsPerPageOptions` (default `[5,10,25,50]`),
277
- `searchable` / `searchPlaceholder` / `searchKeys`, `defaultSortKey` / `defaultSortDirection`,
278
- `pagination`, `showSizeSelector`, `onRowClick`, `getRowId`, `emptyContent`. It is built on the
279
- exported `Table`, `Input` and `Pagination` primitives — drop down to those directly if you need
280
- full control over the markup.
281
+ `searchKeys` / `searchPlaceholder`, `defaultSortKey` / `defaultSortDirection`, `pagination`,
282
+ `paginationAlign` (`left` / `center` / `right` / `full`, default `right`), `showSizeSelector`,
283
+ `onRowClick`, `getRowId`, `emptyContent`.
284
+
285
+ > **Search visibility:** the search input renders **only** when you pass a non-empty `searchKeys`
286
+ > array — those are also the exact fields the query matches. Omit `searchKeys` (or pass `[]`) to
287
+ > hide search entirely.
281
288
 
282
289
  ## License
283
290
 
package/dist/index.cjs CHANGED
@@ -31,7 +31,7 @@ function styleOverride({
31
31
  }
32
32
  var sizeClasses = {
33
33
  sm: "px-4 py-3 text-sm",
34
- md: "px-5 py-3.5 text-sm"
34
+ md: "px-5 py-2.5 text-sm"
35
35
  };
36
36
  var variantClasses = {
37
37
  primary: "bg-brand-500 text-white shadow-theme-xs hover:bg-brand-600 disabled:bg-brand-300",
@@ -552,77 +552,87 @@ var Dropzone = ({
552
552
  }
553
553
  );
554
554
  };
555
- var Table = ({ children, className, ...rest }) => /* @__PURE__ */ jsxRuntime.jsx("table", { className: chunkYERNSNT4_cjs.cn("min-w-full", className), ...rest, children });
556
- var TableHeader = ({ children, className, ...rest }) => /* @__PURE__ */ jsxRuntime.jsx("thead", { className, ...rest, children });
557
- var TableBody = ({ children, className, ...rest }) => /* @__PURE__ */ jsxRuntime.jsx("tbody", { className, ...rest, children });
558
- var TableRow = ({ children, className, ...rest }) => /* @__PURE__ */ jsxRuntime.jsx("tr", { className, ...rest, children });
559
- var TableCell = ({
560
- children,
561
- isHeader = false,
562
- className,
563
- ...rest
564
- }) => {
565
- const Tag = isHeader ? "th" : "td";
566
- return /* @__PURE__ */ jsxRuntime.jsx(Tag, { className, ...rest, children });
555
+ var navButton = "flex h-10 items-center justify-center rounded-lg border border-gray-300 bg-white px-3.5 py-2.5 text-sm text-gray-700 shadow-theme-xs hover:bg-gray-50 disabled:opacity-50 dark:border-gray-700 dark:bg-gray-800 dark:text-gray-400 dark:hover:bg-white/[0.03]";
556
+ var justifyByAlign = {
557
+ left: "justify-start",
558
+ center: "justify-center",
559
+ right: "justify-end"
567
560
  };
568
561
  var Pagination = ({
569
562
  currentPage,
570
563
  totalPages,
571
- onPageChange
564
+ onPageChange,
565
+ align
572
566
  }) => {
573
567
  const pagesAroundCurrent = Array.from(
574
568
  { length: Math.min(3, totalPages) },
575
569
  (_, i) => i + Math.max(currentPage - 1, 1)
576
570
  );
577
- return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center ", children: [
578
- /* @__PURE__ */ jsxRuntime.jsx(
579
- "button",
580
- {
581
- onClick: () => onPageChange(currentPage - 1),
582
- disabled: currentPage === 1,
583
- "aria-label": "Previous",
584
- className: chunkYERNSNT4_cjs.cn(
585
- "mr-2.5 flex items-center h-10 justify-center rounded-lg border border-gray-300 bg-white px-3.5 py-2.5 text-gray-700 shadow-theme-xs hover:bg-gray-50 disabled:opacity-50 dark:border-gray-700 dark:bg-gray-800 dark:text-gray-400 dark:hover:bg-white/[0.03] text-sm"
586
- ),
587
- children: "Previous"
588
- }
589
- ),
590
- /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center gap-2", children: [
591
- currentPage > 3 && /* @__PURE__ */ jsxRuntime.jsx("span", { className: "px-2", children: "..." }),
592
- pagesAroundCurrent.map((page) => /* @__PURE__ */ jsxRuntime.jsx(
593
- "button",
594
- {
595
- onClick: () => onPageChange(page),
596
- "aria-current": currentPage === page ? "page" : void 0,
597
- className: chunkYERNSNT4_cjs.cn(
598
- "flex w-10 items-center justify-center h-10 rounded-lg text-sm font-medium",
599
- currentPage === page ? (
600
- // active page keeps its color on hover (no hover restyle)
601
- "bg-brand-500 text-white"
602
- ) : (
603
- // hover effect applies only to non-active page indices
604
- "text-gray-700 hover:bg-blue-500/[0.08] hover:text-brand-500 dark:text-gray-400 dark:hover:text-brand-500"
605
- )
606
- ),
607
- children: page
608
- },
609
- page
610
- )),
611
- currentPage < totalPages - 2 && /* @__PURE__ */ jsxRuntime.jsx("span", { className: "px-2", children: "..." })
612
- ] }),
613
- /* @__PURE__ */ jsxRuntime.jsx(
571
+ const prev = /* @__PURE__ */ jsxRuntime.jsx(
572
+ "button",
573
+ {
574
+ onClick: () => onPageChange(currentPage - 1),
575
+ disabled: currentPage === 1,
576
+ "aria-label": "Previous",
577
+ className: navButton,
578
+ children: "Previous"
579
+ }
580
+ );
581
+ const indices = /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center gap-2", children: [
582
+ currentPage > 3 && /* @__PURE__ */ jsxRuntime.jsx("span", { className: "px-2", children: "..." }),
583
+ pagesAroundCurrent.map((page) => /* @__PURE__ */ jsxRuntime.jsx(
614
584
  "button",
615
585
  {
616
- onClick: () => onPageChange(currentPage + 1),
617
- disabled: currentPage === totalPages,
618
- "aria-label": "Next",
586
+ onClick: () => onPageChange(page),
587
+ "aria-current": currentPage === page ? "page" : void 0,
619
588
  className: chunkYERNSNT4_cjs.cn(
620
- "ml-2.5 flex items-center justify-center rounded-lg border border-gray-300 bg-white px-3.5 py-2.5 text-gray-700 shadow-theme-xs text-sm hover:bg-gray-50 h-10 disabled:opacity-50 dark:border-gray-700 dark:bg-gray-800 dark:text-gray-400 dark:hover:bg-white/[0.03]"
589
+ "flex w-10 items-center justify-center h-10 rounded-lg text-sm font-medium",
590
+ currentPage === page ? (
591
+ // active page keeps its color on hover (no hover restyle)
592
+ "bg-brand-500 text-white"
593
+ ) : (
594
+ // hover effect applies only to non-active page indices
595
+ "text-gray-700 hover:bg-blue-500/[0.08] hover:text-brand-500 dark:text-gray-400 dark:hover:text-brand-500"
596
+ )
621
597
  ),
622
- children: "Next"
623
- }
624
- )
598
+ children: page
599
+ },
600
+ page
601
+ )),
602
+ currentPage < totalPages - 2 && /* @__PURE__ */ jsxRuntime.jsx("span", { className: "px-2", children: "..." })
625
603
  ] });
604
+ const next = /* @__PURE__ */ jsxRuntime.jsx(
605
+ "button",
606
+ {
607
+ onClick: () => onPageChange(currentPage + 1),
608
+ disabled: currentPage === totalPages,
609
+ "aria-label": "Next",
610
+ className: navButton,
611
+ children: "Next"
612
+ }
613
+ );
614
+ if (align === "full") {
615
+ return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex w-full items-center justify-between", children: [
616
+ prev,
617
+ indices,
618
+ next
619
+ ] });
620
+ }
621
+ return /* @__PURE__ */ jsxRuntime.jsxs(
622
+ "div",
623
+ {
624
+ className: chunkYERNSNT4_cjs.cn(
625
+ "flex items-center gap-2.5",
626
+ align && "w-full",
627
+ align && justifyByAlign[align]
628
+ ),
629
+ children: [
630
+ prev,
631
+ indices,
632
+ next
633
+ ]
634
+ }
635
+ );
626
636
  };
627
637
  var Input = ({
628
638
  className,
@@ -654,6 +664,18 @@ var Input = ({
654
664
  hint && /* @__PURE__ */ jsxRuntime.jsx("p", { className: chunkYERNSNT4_cjs.cn("mt-1.5 text-xs", error ? "text-error-500" : success ? "text-success-500" : "text-gray-500"), children: hint })
655
665
  ] });
656
666
  };
667
+ var TableHeader = ({ children, className, ...rest }) => /* @__PURE__ */ jsxRuntime.jsx("thead", { className, ...rest, children });
668
+ var TableBody = ({ children, className, ...rest }) => /* @__PURE__ */ jsxRuntime.jsx("tbody", { className, ...rest, children });
669
+ var TableRow = ({ children, className, ...rest }) => /* @__PURE__ */ jsxRuntime.jsx("tr", { className, ...rest, children });
670
+ var TableCell = ({
671
+ children,
672
+ isHeader = false,
673
+ className,
674
+ ...rest
675
+ }) => {
676
+ const Tag = isHeader ? "th" : "td";
677
+ return /* @__PURE__ */ jsxRuntime.jsx(Tag, { className, ...rest, children });
678
+ };
657
679
  var alignClass = {
658
680
  left: "text-left",
659
681
  center: "text-center",
@@ -670,16 +692,16 @@ function SortIcon({
670
692
  /* @__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" }) })
671
693
  ] });
672
694
  }
673
- function DataTable({
695
+ function DataDrivenTable({
674
696
  data,
675
697
  columns,
676
698
  rowsPerPage = 10,
677
699
  rowsPerPageOptions = [5, 10, 25, 50],
678
700
  pagination = true,
701
+ paginationAlign = "right",
679
702
  showSizeSelector = true,
680
- searchable = true,
681
- searchPlaceholder = "Search...",
682
703
  searchKeys,
704
+ searchPlaceholder = "Search...",
683
705
  defaultSortKey,
684
706
  defaultSortDirection = "asc",
685
707
  getRowId,
@@ -692,13 +714,13 @@ function DataTable({
692
714
  const [sortKey, setSortKey] = React5.useState(defaultSortKey);
693
715
  const [sortDir, setSortDir] = React5.useState(defaultSortDirection);
694
716
  const [page, setPage] = React5.useState(1);
717
+ const showSearch = !!(searchKeys && searchKeys.length > 0);
695
718
  const filtered = React5.useMemo(() => {
696
- const keys = searchKeys ?? columns.map((c) => c.key);
697
719
  const q = search.trim().toLowerCase();
698
720
  let rows = data;
699
- if (searchable && q) {
721
+ if (showSearch && q) {
700
722
  rows = rows.filter(
701
- (row) => keys.some((k) => String(row[k] ?? "").toLowerCase().includes(q))
723
+ (row) => searchKeys.some((k) => String(row[k] ?? "").toLowerCase().includes(q))
702
724
  );
703
725
  }
704
726
  if (sortKey) {
@@ -710,7 +732,7 @@ function DataTable({
710
732
  });
711
733
  }
712
734
  return rows;
713
- }, [data, columns, search, searchable, searchKeys, sortKey, sortDir]);
735
+ }, [data, search, showSearch, searchKeys, sortKey, sortDir]);
714
736
  const total = filtered.length;
715
737
  const totalPages = pagination ? Math.max(1, Math.ceil(total / pageSize)) : 1;
716
738
  const current = Math.min(page, totalPages);
@@ -727,7 +749,8 @@ function DataTable({
727
749
  };
728
750
  const sizeOptions = rowsPerPageOptions.length ? rowsPerPageOptions : [];
729
751
  const showSelector = pagination && showSizeSelector && sizeOptions.length > 1;
730
- const showControls = searchable || showSelector;
752
+ const showControls = showSearch || showSelector;
753
+ const counterText = total === 0 ? "Showing 0 entries" : `Showing ${start + 1} to ${Math.min(start + pageSize, total)} of ${total} entries`;
731
754
  return /* @__PURE__ */ jsxRuntime.jsxs(
732
755
  "div",
733
756
  {
@@ -754,7 +777,7 @@ function DataTable({
754
777
  ),
755
778
  "entries"
756
779
  ] }) : /* @__PURE__ */ jsxRuntime.jsx("span", {}),
757
- searchable && /* @__PURE__ */ jsxRuntime.jsx(
780
+ showSearch && /* @__PURE__ */ jsxRuntime.jsx(
758
781
  Input,
759
782
  {
760
783
  type: "search",
@@ -769,7 +792,7 @@ function DataTable({
769
792
  }
770
793
  )
771
794
  ] }),
772
- /* @__PURE__ */ jsxRuntime.jsx("div", { className: "overflow-x-auto border-y border-gray-100 dark:border-white/[0.05]", children: /* @__PURE__ */ jsxRuntime.jsxs(Table, { children: [
795
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: "overflow-x-auto border-y border-gray-100 dark:border-white/[0.05]", children: /* @__PURE__ */ jsxRuntime.jsxs("table", { className: "min-w-full", children: [
773
796
  /* @__PURE__ */ jsxRuntime.jsx(TableHeader, { className: "border-b border-gray-100 bg-gray-50 dark:border-white/[0.05] dark:bg-white/[0.06]", children: /* @__PURE__ */ jsxRuntime.jsx(TableRow, { children: columns.map((col) => {
774
797
  const isActive = sortKey === col.key;
775
798
  return /* @__PURE__ */ jsxRuntime.jsx(
@@ -826,14 +849,83 @@ function DataTable({
826
849
  pageRows.length === 0 && /* @__PURE__ */ jsxRuntime.jsx(TableRow, { children: /* @__PURE__ */ jsxRuntime.jsx(TableCell, { colSpan: columns.length, className: "px-5 py-6 text-center text-sm text-gray-400", children: emptyContent }) })
827
850
  ] })
828
851
  ] }) }),
829
- pagination && /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex flex-wrap items-center justify-between gap-3 p-4", children: [
830
- /* @__PURE__ */ jsxRuntime.jsx("p", { className: "text-sm text-gray-500 dark:text-gray-400", children: total === 0 ? "Showing 0 entries" : `Showing ${start + 1} to ${Math.min(start + pageSize, total)} of ${total} entries` }),
831
- totalPages > 1 && /* @__PURE__ */ jsxRuntime.jsx(Pagination, { currentPage: current, totalPages, onPageChange: setPage })
832
- ] })
852
+ pagination && /* @__PURE__ */ jsxRuntime.jsx("div", { className: "p-4", children: paginationAlign === "right" ? (
853
+ // Default layout: entry counter on the left, controls on the right.
854
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex flex-wrap items-center justify-between gap-3", children: [
855
+ /* @__PURE__ */ jsxRuntime.jsx("p", { className: "text-sm text-gray-500 dark:text-gray-400", children: counterText }),
856
+ totalPages > 1 && /* @__PURE__ */ jsxRuntime.jsx(Pagination, { currentPage: current, totalPages, onPageChange: setPage })
857
+ ] })
858
+ ) : (
859
+ // Aligned layout: controls aligned/stretched, counter underneath.
860
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex flex-col gap-3", children: [
861
+ totalPages > 1 && /* @__PURE__ */ jsxRuntime.jsx(
862
+ Pagination,
863
+ {
864
+ align: paginationAlign,
865
+ currentPage: current,
866
+ totalPages,
867
+ onPageChange: setPage
868
+ }
869
+ ),
870
+ /* @__PURE__ */ jsxRuntime.jsx(
871
+ "p",
872
+ {
873
+ className: chunkYERNSNT4_cjs.cn(
874
+ "text-sm text-gray-500 dark:text-gray-400",
875
+ paginationAlign === "center" && "text-center"
876
+ ),
877
+ children: counterText
878
+ }
879
+ )
880
+ ] })
881
+ ) })
833
882
  ]
834
883
  }
835
884
  );
836
885
  }
886
+ function Table({
887
+ children,
888
+ data,
889
+ columns,
890
+ rowsPerPage,
891
+ rowsPerPageOptions,
892
+ pagination,
893
+ paginationAlign,
894
+ showSizeSelector,
895
+ searchKeys,
896
+ searchPlaceholder,
897
+ defaultSortKey,
898
+ defaultSortDirection,
899
+ getRowId,
900
+ onRowClick,
901
+ emptyContent,
902
+ className,
903
+ ...rest
904
+ }) {
905
+ if (columns && data) {
906
+ return /* @__PURE__ */ jsxRuntime.jsx(
907
+ DataDrivenTable,
908
+ {
909
+ data,
910
+ columns,
911
+ rowsPerPage,
912
+ rowsPerPageOptions,
913
+ pagination,
914
+ paginationAlign,
915
+ showSizeSelector,
916
+ searchKeys,
917
+ searchPlaceholder,
918
+ defaultSortKey,
919
+ defaultSortDirection,
920
+ getRowId,
921
+ onRowClick,
922
+ emptyContent,
923
+ className
924
+ }
925
+ );
926
+ }
927
+ return /* @__PURE__ */ jsxRuntime.jsx("table", { className: chunkYERNSNT4_cjs.cn("min-w-full", className), ...rest, children });
928
+ }
837
929
  var ChevronIcon = () => /* @__PURE__ */ jsxRuntime.jsx(
838
930
  "svg",
839
931
  {
@@ -3081,7 +3173,6 @@ exports.Card = Card;
3081
3173
  exports.Checkbox = Checkbox;
3082
3174
  exports.Chip = Chip;
3083
3175
  exports.Code = Code;
3084
- exports.DataTable = DataTable;
3085
3176
  exports.DatePicker = DatePicker;
3086
3177
  exports.DateTimePicker = DateTimePicker;
3087
3178
  exports.Drawer = Drawer;