@jinaan/uikit 0.1.34 → 0.1.36
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 +594 -555
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/src/data-table.tsx +101 -50
- 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,54 @@ 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 bandTableRef = useRef<HTMLTableElement | null>(null);
|
|
357
|
+
const [columnWidths, setColumnWidths] = useState<number[]>([]);
|
|
358
|
+
// Deps must be primitives: visibleColumns is a fresh array every render and
|
|
359
|
+
// would re-run this effect (and its setState) in a loop.
|
|
360
|
+
const columnKey = visibleColumns.map((c) => c.id).join("\u0000");
|
|
361
|
+
|
|
362
|
+
useLayoutEffect(() => {
|
|
363
|
+
const table = bodyTableRef.current;
|
|
364
|
+
if (!table) return;
|
|
365
|
+
const measure = () => {
|
|
366
|
+
const cells = table.tBodies[0]?.rows.item(0)?.cells;
|
|
367
|
+
// The band may be squeezed narrower than its own sort-button content by
|
|
368
|
+
// the body-measured widths; scrollWidth reports the true minimum, so it
|
|
369
|
+
// floors each column and stops titles from overflowing their neighbors.
|
|
370
|
+
const bandCells = bandTableRef.current?.querySelectorAll("thead th");
|
|
371
|
+
if (!cells || cells.length === 0) {
|
|
372
|
+
setColumnWidths((prev) => (prev.length === 0 ? prev : []));
|
|
373
|
+
return;
|
|
374
|
+
}
|
|
375
|
+
const widths = Array.from(cells, (cell, i) =>
|
|
376
|
+
Math.max(
|
|
377
|
+
Math.round(cell.getBoundingClientRect().width),
|
|
378
|
+
Math.round(bandCells?.item(i)?.scrollWidth ?? 0),
|
|
379
|
+
),
|
|
380
|
+
);
|
|
381
|
+
// Bail out when nothing changed: a new array would re-render, and the
|
|
382
|
+
// re-render would re-run this effect, looping forever.
|
|
383
|
+
setColumnWidths((prev) =>
|
|
384
|
+
prev.length === widths.length && prev.every((w, i) => w === widths[i]) ? prev : widths,
|
|
385
|
+
);
|
|
386
|
+
};
|
|
387
|
+
measure();
|
|
388
|
+
if (typeof ResizeObserver === "undefined") return;
|
|
389
|
+
const observer = new ResizeObserver(measure);
|
|
390
|
+
observer.observe(table);
|
|
391
|
+
return () => observer.disconnect();
|
|
392
|
+
}, [columnKey, selectable, rowActions, loading, error]);
|
|
393
|
+
|
|
394
|
+
const totalWidth = columnWidths.reduce((sum, width) => sum + width, 0);
|
|
395
|
+
|
|
353
396
|
return (
|
|
354
397
|
<div className={cn("space-y-4", className)}>
|
|
355
398
|
{tabs && tabs.length > 0 && (
|
|
@@ -616,12 +659,21 @@ export function DataTable<T>({
|
|
|
616
659
|
headerClassName={headerClassName}
|
|
617
660
|
panelClassName={panelClassName}
|
|
618
661
|
header={
|
|
619
|
-
// Visible header
|
|
620
|
-
//
|
|
621
|
-
|
|
662
|
+
// Visible header band. Presentational (aria-sort lives on the body's
|
|
663
|
+
// sr-only thead); widths are pinned to the measured body columns and
|
|
664
|
+
// hidden until the first measurement lands so columns never flash.
|
|
665
|
+
<table
|
|
666
|
+
ref={bandTableRef}
|
|
667
|
+
role="presentation"
|
|
668
|
+
className="w-full table-fixed border-collapse"
|
|
669
|
+
style={{
|
|
670
|
+
width: totalWidth > 0 ? totalWidth : undefined,
|
|
671
|
+
visibility: totalWidth > 0 ? undefined : "hidden",
|
|
672
|
+
}}
|
|
673
|
+
>
|
|
622
674
|
<colgroup>
|
|
623
|
-
{
|
|
624
|
-
<col key={i} style={{ width
|
|
675
|
+
{columnWidths.map((width, i) => (
|
|
676
|
+
<col key={i} style={{ width }} />
|
|
625
677
|
))}
|
|
626
678
|
</colgroup>
|
|
627
679
|
<thead>
|
|
@@ -711,13 +763,8 @@ export function DataTable<T>({
|
|
|
711
763
|
action={empty?.action}
|
|
712
764
|
/>
|
|
713
765
|
) : (
|
|
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). */}
|
|
766
|
+
<table ref={bodyTableRef} className="w-full table-auto border-collapse text-left">
|
|
767
|
+
{/* Semantic headers for assistive tech; the visible band is presentational. */}
|
|
721
768
|
<thead className="sr-only">
|
|
722
769
|
<tr>
|
|
723
770
|
{selectable && <th scope="col">Select</th>}
|
|
@@ -771,7 +818,7 @@ export function DataTable<T>({
|
|
|
771
818
|
)}
|
|
772
819
|
</TableSurface>
|
|
773
820
|
|
|
774
|
-
{
|
|
821
|
+
{showPagination && (
|
|
775
822
|
<div className="flex flex-wrap items-center justify-between gap-3 text-sm">
|
|
776
823
|
<span className="text-muted">
|
|
777
824
|
{start + 1}–{Math.min(start + pageSize, sorted.length)} of {sorted.length}
|
|
@@ -784,27 +831,31 @@ export function DataTable<T>({
|
|
|
784
831
|
<SelectContent>
|
|
785
832
|
{pageSizeOptions.map((n) => (
|
|
786
833
|
<SelectItem key={n} value={String(n)}>
|
|
787
|
-
{n}
|
|
834
|
+
{n}
|
|
788
835
|
</SelectItem>
|
|
789
836
|
))}
|
|
790
837
|
</SelectContent>
|
|
791
838
|
</Select>
|
|
792
|
-
<
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
839
|
+
<div className="flex items-center gap-1">
|
|
840
|
+
<Button
|
|
841
|
+
variant="secondary"
|
|
842
|
+
size="icon"
|
|
843
|
+
aria-label="Previous page"
|
|
844
|
+
disabled={page <= 0}
|
|
845
|
+
onClick={() => setPage((p) => Math.max(0, p - 1))}
|
|
846
|
+
>
|
|
847
|
+
<CaretLeft />
|
|
848
|
+
</Button>
|
|
849
|
+
<Button
|
|
850
|
+
variant="secondary"
|
|
851
|
+
size="icon"
|
|
852
|
+
aria-label="Next page"
|
|
853
|
+
disabled={page >= pageCount - 1}
|
|
854
|
+
onClick={() => setPage((p) => Math.min(pageCount - 1, p + 1))}
|
|
855
|
+
>
|
|
856
|
+
<CaretRight />
|
|
857
|
+
</Button>
|
|
858
|
+
</div>
|
|
808
859
|
</div>
|
|
809
860
|
</div>
|
|
810
861
|
)}
|
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}
|