@payglocal_ui/flux-ui 0.2.0 → 0.2.2
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/index.cjs +51 -29
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +24 -3
- package/dist/index.d.ts +24 -3
- package/dist/index.js +51 -29
- package/dist/index.js.map +1 -1
- package/package.json +2 -1
- package/src/data-table.tsx +105 -35
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@payglocal_ui/flux-ui",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.2",
|
|
4
4
|
"description": "Flux UI primitives — inputs, fields, dialog, data table, charts, calendar, and more (Tailwind v4 + Radix).",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
@@ -67,6 +67,7 @@
|
|
|
67
67
|
"devDependencies": {
|
|
68
68
|
"@types/react": "^19",
|
|
69
69
|
"@types/react-dom": "^19",
|
|
70
|
+
"esbuild": "^0.28.1",
|
|
70
71
|
"tsup": "^8.5.1",
|
|
71
72
|
"typescript": "^5"
|
|
72
73
|
},
|
package/src/data-table.tsx
CHANGED
|
@@ -60,10 +60,31 @@ interface DataTableProps<T> {
|
|
|
60
60
|
label: string;
|
|
61
61
|
onClick?: (row: T) => void;
|
|
62
62
|
};
|
|
63
|
+
/**
|
|
64
|
+
* Custom action revealed on row hover — typically a `<Button>` or
|
|
65
|
+
* `<ButtonGroup>`, but any `ReactNode` is accepted. Pass a **function** to
|
|
66
|
+
* render per-row: it receives `(row, index)`, so the action always has the
|
|
67
|
+
* record for its row (e.g. to navigate or open a drawer for that row).
|
|
68
|
+
*
|
|
69
|
+
* It is not a real column: it floats as an overlay **pinned to the right edge
|
|
70
|
+
* of the viewport**, so it stays in view as the table scrolls horizontally
|
|
71
|
+
* (no scrolling to the end to reach it) while the last data column stays
|
|
72
|
+
* flush with nothing trailing it. Takes precedence over `rowCta`.
|
|
73
|
+
*/
|
|
74
|
+
rowAction?: ReactNode | ((row: T, index: number) => ReactNode);
|
|
63
75
|
/** Row / cell vertical rhythm and horizontal gutters */
|
|
64
76
|
density?: DataTableDensity;
|
|
65
|
-
/**
|
|
66
|
-
|
|
77
|
+
/**
|
|
78
|
+
* Column-sizing strategy:
|
|
79
|
+
* - `fixed` — widths come only from `colgroup` hints (`width`/`minWidth`/
|
|
80
|
+
* `maxWidth`); content is ignored and overflow is clipped. Table fills 100%.
|
|
81
|
+
* - `auto` — columns size to content but the table still fills 100%, so any
|
|
82
|
+
* leftover space is distributed into the columns (they stretch).
|
|
83
|
+
* - `content` — columns size to their content's intrinsic width and the table
|
|
84
|
+
* shrinks to fit. Leftover space stays empty to the right of the last
|
|
85
|
+
* column; it scrolls horizontally only once content exceeds the container.
|
|
86
|
+
*/
|
|
87
|
+
tableLayout?: "auto" | "fixed" | "content";
|
|
67
88
|
theadClassName?: string;
|
|
68
89
|
headerStyle?: DataTableHeaderStyle;
|
|
69
90
|
/** Footer: paginated range vs simple `n items` */
|
|
@@ -88,6 +109,7 @@ export function DataTable<T>({
|
|
|
88
109
|
className,
|
|
89
110
|
rowKey,
|
|
90
111
|
rowCta,
|
|
112
|
+
rowAction,
|
|
91
113
|
density = "default",
|
|
92
114
|
tableLayout = "fixed",
|
|
93
115
|
theadClassName,
|
|
@@ -103,10 +125,33 @@ export function DataTable<T>({
|
|
|
103
125
|
? (p: number) => onPageChange?.(p)
|
|
104
126
|
: (p: number) => setInternalPage(p);
|
|
105
127
|
|
|
128
|
+
// The right-pinned row action. `rowAction` takes precedence over `rowCta`.
|
|
129
|
+
// It is NOT a real column: it renders as a per-row overlay floating at the
|
|
130
|
+
// right edge of the viewport (sticky), so the last data column stays flush
|
|
131
|
+
// and there's no reserved/empty trailing column when scrolled to the end.
|
|
132
|
+
const hasAction = rowAction != null || rowCta != null;
|
|
133
|
+
|
|
134
|
+
// `content` layout: a greedy, empty trailing column that absorbs leftover
|
|
135
|
+
// horizontal space so the data columns stay at their content (minimum)
|
|
136
|
+
// width while the table still spans the full container width.
|
|
137
|
+
const hasSpacer = tableLayout === "content";
|
|
138
|
+
|
|
106
139
|
const total = totalRows ?? data.length;
|
|
107
140
|
const totalPages = Math.ceil(total / pageSize);
|
|
108
141
|
const paginated = isControlled ? data : data.slice((page - 1) * pageSize, page * pageSize);
|
|
109
142
|
|
|
143
|
+
// Guard against duplicate / non-unique rowKey() results. React silently fails
|
|
144
|
+
// to unmount old <tr> nodes when sibling keys collide, leaving stale rows
|
|
145
|
+
// rendered on top of new data (or the empty state). De-dupe by suffixing
|
|
146
|
+
// repeats so every rendered row gets a unique, stable key.
|
|
147
|
+
const seenKeys = new Map<string, number>();
|
|
148
|
+
const rowKeys = paginated.map((row) => {
|
|
149
|
+
const base = rowKey(row);
|
|
150
|
+
const seen = seenKeys.get(base) ?? 0;
|
|
151
|
+
seenKeys.set(base, seen + 1);
|
|
152
|
+
return seen === 0 ? base : `${base}__${seen}`;
|
|
153
|
+
});
|
|
154
|
+
|
|
110
155
|
const comfortable = density === "comfortable";
|
|
111
156
|
const compact = density === "compact";
|
|
112
157
|
const compactCellPad = compact
|
|
@@ -127,7 +172,9 @@ export function DataTable<T>({
|
|
|
127
172
|
: compact
|
|
128
173
|
? "text-[11px] font-semibold text-muted-foreground"
|
|
129
174
|
: "text-[11px] font-semibold text-foreground/75 dark:text-foreground/85";
|
|
130
|
-
|
|
175
|
+
// Action overlay geometry: the action floats this many px in from the right
|
|
176
|
+
// edge of the viewport (it has no reserved column — it overlays the row).
|
|
177
|
+
const actionGutter = comfortable ? 20 : compact ? 12 : 16;
|
|
131
178
|
|
|
132
179
|
return (
|
|
133
180
|
<div
|
|
@@ -148,7 +195,13 @@ export function DataTable<T>({
|
|
|
148
195
|
>
|
|
149
196
|
<table
|
|
150
197
|
className={cn(tableLayout === "auto" && "min-w-[920px]")}
|
|
151
|
-
style={{
|
|
198
|
+
style={{
|
|
199
|
+
// `content` uses the automatic algorithm but stays 100% wide; a
|
|
200
|
+
// greedy spacer column (below) soaks up the slack so the data
|
|
201
|
+
// columns collapse to their content width while the table fills.
|
|
202
|
+
tableLayout: tableLayout === "fixed" ? "fixed" : "auto",
|
|
203
|
+
width: "100%",
|
|
204
|
+
}}
|
|
152
205
|
>
|
|
153
206
|
{tableLayout === "fixed" && (
|
|
154
207
|
<colgroup>
|
|
@@ -162,7 +215,9 @@ export function DataTable<T>({
|
|
|
162
215
|
}}
|
|
163
216
|
/>
|
|
164
217
|
))}
|
|
165
|
-
{
|
|
218
|
+
{/* Zero-width column: the action floats out of it as an overlay,
|
|
219
|
+
so the last data column stays flush and nothing trails it. */}
|
|
220
|
+
{hasAction ? <col style={{ width: 0 }} /> : null}
|
|
166
221
|
</colgroup>
|
|
167
222
|
)}
|
|
168
223
|
|
|
@@ -197,7 +252,10 @@ export function DataTable<T>({
|
|
|
197
252
|
{col.header}
|
|
198
253
|
</th>
|
|
199
254
|
))}
|
|
200
|
-
{
|
|
255
|
+
{hasSpacer ? <th className="w-full p-0" aria-hidden /> : null}
|
|
256
|
+
{hasAction ? (
|
|
257
|
+
<th className="sticky right-0 z-[1] w-0 p-0" aria-hidden />
|
|
258
|
+
) : null}
|
|
201
259
|
</tr>
|
|
202
260
|
</thead>
|
|
203
261
|
|
|
@@ -206,27 +264,27 @@ export function DataTable<T>({
|
|
|
206
264
|
Array.from({ length: skeletonRows }).map((_, i) => (
|
|
207
265
|
<TableRowSkeleton
|
|
208
266
|
key={i}
|
|
209
|
-
cols={columns.length
|
|
267
|
+
cols={columns.length}
|
|
210
268
|
density={density}
|
|
211
269
|
snug={snug}
|
|
212
270
|
/>
|
|
213
271
|
))
|
|
214
272
|
) : paginated.length === 0 ? (
|
|
215
273
|
<tr>
|
|
216
|
-
<td colSpan={columns.length + (
|
|
274
|
+
<td colSpan={columns.length + (hasSpacer ? 1 : 0) + (hasAction ? 1 : 0)}>
|
|
217
275
|
<EmptyState title={emptyTitle} description={emptyDescription} />
|
|
218
276
|
</td>
|
|
219
277
|
</tr>
|
|
220
278
|
) : (
|
|
221
279
|
paginated.map((row, i) => (
|
|
222
280
|
<tr
|
|
223
|
-
key={
|
|
281
|
+
key={rowKeys[i]}
|
|
224
282
|
className={cn(
|
|
225
283
|
"group transition-colors duration-150 border-b border-border/60 last:border-b-0",
|
|
226
284
|
comfortable && "min-h-[56px]",
|
|
227
285
|
compact && "min-h-[44px]",
|
|
228
286
|
"hover:bg-muted/40 dark:hover:bg-muted/25",
|
|
229
|
-
|
|
287
|
+
hasAction &&
|
|
230
288
|
"hover:shadow-[0_1px_0_rgba(0,0,0,0.04)] dark:hover:shadow-none"
|
|
231
289
|
)}
|
|
232
290
|
>
|
|
@@ -260,32 +318,44 @@ export function DataTable<T>({
|
|
|
260
318
|
</td>
|
|
261
319
|
))}
|
|
262
320
|
|
|
263
|
-
{
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
onClick={() => rowCta.onClick?.(row)}
|
|
280
|
-
className={cn(
|
|
281
|
-
"opacity-0 group-hover:opacity-100 transition-opacity duration-150 inline-flex items-center font-medium text-foreground bg-card rounded-lg border border-border hover:border-muted-foreground/50 whitespace-nowrap shadow-sm",
|
|
282
|
-
compact
|
|
283
|
-
? "px-2.5 py-1 text-[11px]"
|
|
284
|
-
: "px-3 py-1.5 text-[12px]"
|
|
285
|
-
)}
|
|
321
|
+
{hasSpacer ? <td className="p-0" aria-hidden /> : null}
|
|
322
|
+
|
|
323
|
+
{hasAction ? (
|
|
324
|
+
// Zero-width sticky cell pinned to the right edge of the
|
|
325
|
+
// viewport. Its children are positioned absolutely so they
|
|
326
|
+
// float over the row (out of the 0-width cell) — the action
|
|
327
|
+
// stays in view while scrolling and nothing trails the last
|
|
328
|
+
// data column. Everything is revealed on hover only.
|
|
329
|
+
<td className="sticky right-0 z-[1] w-0 p-0 align-middle">
|
|
330
|
+
{/* The action itself — `rowAction(row, i)` so it always
|
|
331
|
+
receives this row's record. Vertically centered in the
|
|
332
|
+
row and anchored `actionGutter`px from the right edge,
|
|
333
|
+
overflowing left over the row content. Revealed on hover. */}
|
|
334
|
+
<span
|
|
335
|
+
className="absolute top-1/2 -translate-y-1/2 z-[1] inline-flex items-center opacity-0 transition-opacity duration-150 group-hover:opacity-100 group-focus-within:opacity-100"
|
|
336
|
+
style={{ right: actionGutter }}
|
|
286
337
|
>
|
|
287
|
-
{
|
|
288
|
-
|
|
338
|
+
{rowAction != null ? (
|
|
339
|
+
typeof rowAction === "function" ? (
|
|
340
|
+
rowAction(row, i)
|
|
341
|
+
) : (
|
|
342
|
+
rowAction
|
|
343
|
+
)
|
|
344
|
+
) : (
|
|
345
|
+
<button
|
|
346
|
+
type="button"
|
|
347
|
+
onClick={() => rowCta?.onClick?.(row)}
|
|
348
|
+
className={cn(
|
|
349
|
+
"inline-flex items-center font-medium text-foreground bg-card rounded-lg border border-border hover:border-muted-foreground/50 whitespace-nowrap shadow-sm",
|
|
350
|
+
compact
|
|
351
|
+
? "px-2.5 py-1 text-[11px]"
|
|
352
|
+
: "px-3 py-1.5 text-[12px]"
|
|
353
|
+
)}
|
|
354
|
+
>
|
|
355
|
+
{rowCta?.label}
|
|
356
|
+
</button>
|
|
357
|
+
)}
|
|
358
|
+
</span>
|
|
289
359
|
</td>
|
|
290
360
|
) : null}
|
|
291
361
|
</tr>
|