@jinaan/uikit 0.1.32 → 0.1.35
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/data-table.d.ts +12 -2
- package/dist/data-table.d.ts.map +1 -1
- package/dist/index.js +586 -557
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/src/data-table.tsx +91 -51
- package/src/table.tsx +1 -1
package/package.json
CHANGED
package/src/data-table.tsx
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import {
|
|
2
2
|
CaretDown,
|
|
3
|
+
CaretLeft,
|
|
4
|
+
CaretRight,
|
|
3
5
|
CaretUp,
|
|
4
6
|
CaretUpDown,
|
|
5
7
|
Columns as ColumnsIcon,
|
|
@@ -10,7 +12,7 @@ import {
|
|
|
10
12
|
WarningCircle,
|
|
11
13
|
X,
|
|
12
14
|
} from "@phosphor-icons/react";
|
|
13
|
-
import { useEffect, useMemo, useState, type ReactNode } from "react";
|
|
15
|
+
import { useEffect, useLayoutEffect, useMemo, useRef, useState, type ReactNode } from "react";
|
|
14
16
|
|
|
15
17
|
import { Avatar, AvatarFallback, AvatarImage } from "./avatar";
|
|
16
18
|
import { Button } from "./button";
|
|
@@ -52,7 +54,7 @@ export interface Column<T> {
|
|
|
52
54
|
defaultHidden?: boolean;
|
|
53
55
|
align?: "left" | "right";
|
|
54
56
|
className?: string;
|
|
55
|
-
/**
|
|
57
|
+
/** Advisory only: columns are content-sized; the layout ignores this. */
|
|
56
58
|
width?: string;
|
|
57
59
|
/** Required columns stay visible and cannot be hidden. */
|
|
58
60
|
required?: boolean;
|
|
@@ -132,6 +134,12 @@ interface DataTableProps<T> {
|
|
|
132
134
|
pageSize?: number;
|
|
133
135
|
pageSizeOptions?: number[];
|
|
134
136
|
|
|
137
|
+
/**
|
|
138
|
+
* `auto` (default) hides the pagination footer when all rows fit on one
|
|
139
|
+
* page; `always` keeps it visible whenever there is at least one row.
|
|
140
|
+
*/
|
|
141
|
+
pagination?: "auto" | "always";
|
|
142
|
+
|
|
135
143
|
/** Surface preset for the table card. */
|
|
136
144
|
tone?: TableSurfaceTone;
|
|
137
145
|
/** Optional action pinned to the top-right of the header band. */
|
|
@@ -179,6 +187,11 @@ export function DataTableIdentity({
|
|
|
179
187
|
);
|
|
180
188
|
}
|
|
181
189
|
|
|
190
|
+
/**
|
|
191
|
+
* Toolbar + single content-sized table inside a TableSurface. Columns fit
|
|
192
|
+
* their widest content (never clipping or overlapping cells) and the surface
|
|
193
|
+
* scrolls horizontally when the columns exceed the viewport.
|
|
194
|
+
*/
|
|
182
195
|
export function DataTable<T>({
|
|
183
196
|
rows,
|
|
184
197
|
columns,
|
|
@@ -201,6 +214,7 @@ export function DataTable<T>({
|
|
|
201
214
|
defaultSort,
|
|
202
215
|
pageSize: initialPageSize = 25,
|
|
203
216
|
pageSizeOptions = [10, 25, 50, 100],
|
|
217
|
+
pagination = "auto",
|
|
204
218
|
tone,
|
|
205
219
|
headerAction,
|
|
206
220
|
headerClassName,
|
|
@@ -282,6 +296,8 @@ export function DataTable<T>({
|
|
|
282
296
|
}, [filtered, sort, columns]);
|
|
283
297
|
|
|
284
298
|
const pageCount = Math.max(1, Math.ceil(sorted.length / pageSize));
|
|
299
|
+
const showPagination =
|
|
300
|
+
!loading && !error && sorted.length > 0 && (pagination === "always" || sorted.length > pageSize);
|
|
285
301
|
|
|
286
302
|
useEffect(() => {
|
|
287
303
|
setPage(0);
|
|
@@ -329,27 +345,44 @@ export function DataTable<T>({
|
|
|
329
345
|
|
|
330
346
|
const hasToolbar = Boolean(search || filters?.length || toolbarStart || toolbarEnd);
|
|
331
347
|
|
|
332
|
-
// Fixed layout keeps header and body columns aligned across the two tables.
|
|
333
|
-
// When no column declares a width, give the first column more room and split
|
|
334
|
-
// the rest evenly so tables don't render as uniform stripes.
|
|
335
|
-
const anyExplicitWidth = visibleColumns.some((c) => c.width);
|
|
336
|
-
const restShare =
|
|
337
|
-
visibleColumns.length > 1 ? (100 - 30) / (visibleColumns.length - 1) : 100;
|
|
338
|
-
const dataWidths = visibleColumns.map((c, i) => {
|
|
339
|
-
if (c.width) return c.width;
|
|
340
|
-
if (anyExplicitWidth) return "auto";
|
|
341
|
-
return `${i === 0 ? 30 : restShare}%`;
|
|
342
|
-
});
|
|
343
|
-
|
|
344
|
-
const colWidths = [
|
|
345
|
-
...(selectable ? ["2.75rem"] : []),
|
|
346
|
-
...dataWidths,
|
|
347
|
-
...(rowActions ? ["11rem"] : []),
|
|
348
|
-
];
|
|
349
|
-
|
|
350
348
|
const ariaSort = (c: Column<T>) =>
|
|
351
349
|
sort?.id === c.id ? (sort.dir === "asc" ? "ascending" : "descending") : undefined;
|
|
352
350
|
|
|
351
|
+
// The visible header lives in the TableSurface band while the body table
|
|
352
|
+
// sizes its columns to their content (table-auto). Keep the band aligned by
|
|
353
|
+
// measuring the rendered body columns and pinning the band colgroup to the
|
|
354
|
+
// same pixel widths; both tables share the surface's horizontal scroller.
|
|
355
|
+
const bodyTableRef = useRef<HTMLTableElement | null>(null);
|
|
356
|
+
const [columnWidths, setColumnWidths] = useState<number[]>([]);
|
|
357
|
+
// Deps must be primitives: visibleColumns is a fresh array every render and
|
|
358
|
+
// would re-run this effect (and its setState) in a loop.
|
|
359
|
+
const columnKey = visibleColumns.map((c) => c.id).join("\u0000");
|
|
360
|
+
|
|
361
|
+
useLayoutEffect(() => {
|
|
362
|
+
const table = bodyTableRef.current;
|
|
363
|
+
if (!table) return;
|
|
364
|
+
const measure = () => {
|
|
365
|
+
const cells = table.tBodies[0]?.rows.item(0)?.cells;
|
|
366
|
+
if (!cells || cells.length === 0) {
|
|
367
|
+
setColumnWidths((prev) => (prev.length === 0 ? prev : []));
|
|
368
|
+
return;
|
|
369
|
+
}
|
|
370
|
+
const widths = Array.from(cells, (cell) => Math.round(cell.getBoundingClientRect().width));
|
|
371
|
+
// Bail out when nothing changed: a new array would re-render, and the
|
|
372
|
+
// re-render would re-run this effect, looping forever.
|
|
373
|
+
setColumnWidths((prev) =>
|
|
374
|
+
prev.length === widths.length && prev.every((w, i) => w === widths[i]) ? prev : widths,
|
|
375
|
+
);
|
|
376
|
+
};
|
|
377
|
+
measure();
|
|
378
|
+
if (typeof ResizeObserver === "undefined") return;
|
|
379
|
+
const observer = new ResizeObserver(measure);
|
|
380
|
+
observer.observe(table);
|
|
381
|
+
return () => observer.disconnect();
|
|
382
|
+
}, [columnKey, selectable, rowActions, loading, error]);
|
|
383
|
+
|
|
384
|
+
const totalWidth = columnWidths.reduce((sum, width) => sum + width, 0);
|
|
385
|
+
|
|
353
386
|
return (
|
|
354
387
|
<div className={cn("space-y-4", className)}>
|
|
355
388
|
{tabs && tabs.length > 0 && (
|
|
@@ -616,12 +649,20 @@ export function DataTable<T>({
|
|
|
616
649
|
headerClassName={headerClassName}
|
|
617
650
|
panelClassName={panelClassName}
|
|
618
651
|
header={
|
|
619
|
-
// Visible header
|
|
620
|
-
//
|
|
621
|
-
|
|
652
|
+
// Visible header band. Presentational (aria-sort lives on the body's
|
|
653
|
+
// sr-only thead); widths are pinned to the measured body columns and
|
|
654
|
+
// hidden until the first measurement lands so columns never flash.
|
|
655
|
+
<table
|
|
656
|
+
role="presentation"
|
|
657
|
+
className="w-full table-fixed border-collapse"
|
|
658
|
+
style={{
|
|
659
|
+
width: totalWidth > 0 ? totalWidth : undefined,
|
|
660
|
+
visibility: totalWidth > 0 ? undefined : "hidden",
|
|
661
|
+
}}
|
|
662
|
+
>
|
|
622
663
|
<colgroup>
|
|
623
|
-
{
|
|
624
|
-
<col key={i} style={{ width
|
|
664
|
+
{columnWidths.map((width, i) => (
|
|
665
|
+
<col key={i} style={{ width }} />
|
|
625
666
|
))}
|
|
626
667
|
</colgroup>
|
|
627
668
|
<thead>
|
|
@@ -711,13 +752,8 @@ export function DataTable<T>({
|
|
|
711
752
|
action={empty?.action}
|
|
712
753
|
/>
|
|
713
754
|
) : (
|
|
714
|
-
<table className="w-full table-
|
|
715
|
-
|
|
716
|
-
{colWidths.map((w, i) => (
|
|
717
|
-
<col key={i} style={{ width: w }} />
|
|
718
|
-
))}
|
|
719
|
-
</colgroup>
|
|
720
|
-
{/* Semantic headers for assistive tech (visible header is presentational). */}
|
|
755
|
+
<table ref={bodyTableRef} className="w-full table-auto border-collapse text-left">
|
|
756
|
+
{/* Semantic headers for assistive tech; the visible band is presentational. */}
|
|
721
757
|
<thead className="sr-only">
|
|
722
758
|
<tr>
|
|
723
759
|
{selectable && <th scope="col">Select</th>}
|
|
@@ -771,40 +807,44 @@ export function DataTable<T>({
|
|
|
771
807
|
)}
|
|
772
808
|
</TableSurface>
|
|
773
809
|
|
|
774
|
-
{
|
|
810
|
+
{showPagination && (
|
|
775
811
|
<div className="flex flex-wrap items-center justify-between gap-3 text-sm">
|
|
776
812
|
<span className="text-muted">
|
|
777
813
|
{start + 1}–{Math.min(start + pageSize, sorted.length)} of {sorted.length}
|
|
778
814
|
</span>
|
|
779
815
|
<div className="flex items-center gap-2">
|
|
780
816
|
<Select value={String(pageSize)} onValueChange={(v) => setPageSize(Number(v))}>
|
|
781
|
-
<SelectTrigger aria-label="Rows per page" className="w-27.5">
|
|
817
|
+
<SelectTrigger aria-label="Rows per page" className="w-27.5" variant="inline">
|
|
782
818
|
<SelectValue />
|
|
783
819
|
</SelectTrigger>
|
|
784
820
|
<SelectContent>
|
|
785
821
|
{pageSizeOptions.map((n) => (
|
|
786
822
|
<SelectItem key={n} value={String(n)}>
|
|
787
|
-
{n}
|
|
823
|
+
{n}
|
|
788
824
|
</SelectItem>
|
|
789
825
|
))}
|
|
790
826
|
</SelectContent>
|
|
791
827
|
</Select>
|
|
792
|
-
<
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
828
|
+
<div className="flex items-center gap-1">
|
|
829
|
+
<Button
|
|
830
|
+
variant="secondary"
|
|
831
|
+
size="icon"
|
|
832
|
+
aria-label="Previous page"
|
|
833
|
+
disabled={page <= 0}
|
|
834
|
+
onClick={() => setPage((p) => Math.max(0, p - 1))}
|
|
835
|
+
>
|
|
836
|
+
<CaretLeft />
|
|
837
|
+
</Button>
|
|
838
|
+
<Button
|
|
839
|
+
variant="secondary"
|
|
840
|
+
size="icon"
|
|
841
|
+
aria-label="Next page"
|
|
842
|
+
disabled={page >= pageCount - 1}
|
|
843
|
+
onClick={() => setPage((p) => Math.min(pageCount - 1, p + 1))}
|
|
844
|
+
>
|
|
845
|
+
<CaretRight />
|
|
846
|
+
</Button>
|
|
847
|
+
</div>
|
|
808
848
|
</div>
|
|
809
849
|
</div>
|
|
810
850
|
)}
|
package/src/table.tsx
CHANGED
|
@@ -82,7 +82,7 @@ export function TableCell({ className, ...props }: TdHTMLAttributes<HTMLTableCel
|
|
|
82
82
|
return (
|
|
83
83
|
<td
|
|
84
84
|
className={cn(
|
|
85
|
-
"px-(--table-pad-x) py-3 align-middle text-sm text-table-row data-numeric:font-numeric data-numeric:tabular-nums",
|
|
85
|
+
"whitespace-nowrap px-(--table-pad-x) py-3 align-middle text-sm text-table-row data-numeric:font-numeric data-numeric:tabular-nums",
|
|
86
86
|
className,
|
|
87
87
|
)}
|
|
88
88
|
{...props}
|