@payglocal_ui/flux-ui 0.2.1 → 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 +43 -28
- 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 +43 -28
- package/dist/index.js.map +1 -1
- package/package.json +2 -1
- package/src/data-table.tsx +92 -34
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,6 +125,17 @@ 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);
|
|
@@ -139,7 +172,9 @@ export function DataTable<T>({
|
|
|
139
172
|
: compact
|
|
140
173
|
? "text-[11px] font-semibold text-muted-foreground"
|
|
141
174
|
: "text-[11px] font-semibold text-foreground/75 dark:text-foreground/85";
|
|
142
|
-
|
|
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;
|
|
143
178
|
|
|
144
179
|
return (
|
|
145
180
|
<div
|
|
@@ -160,7 +195,13 @@ export function DataTable<T>({
|
|
|
160
195
|
>
|
|
161
196
|
<table
|
|
162
197
|
className={cn(tableLayout === "auto" && "min-w-[920px]")}
|
|
163
|
-
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
|
+
}}
|
|
164
205
|
>
|
|
165
206
|
{tableLayout === "fixed" && (
|
|
166
207
|
<colgroup>
|
|
@@ -174,7 +215,9 @@ export function DataTable<T>({
|
|
|
174
215
|
}}
|
|
175
216
|
/>
|
|
176
217
|
))}
|
|
177
|
-
{
|
|
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}
|
|
178
221
|
</colgroup>
|
|
179
222
|
)}
|
|
180
223
|
|
|
@@ -209,7 +252,10 @@ export function DataTable<T>({
|
|
|
209
252
|
{col.header}
|
|
210
253
|
</th>
|
|
211
254
|
))}
|
|
212
|
-
{
|
|
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}
|
|
213
259
|
</tr>
|
|
214
260
|
</thead>
|
|
215
261
|
|
|
@@ -218,14 +264,14 @@ export function DataTable<T>({
|
|
|
218
264
|
Array.from({ length: skeletonRows }).map((_, i) => (
|
|
219
265
|
<TableRowSkeleton
|
|
220
266
|
key={i}
|
|
221
|
-
cols={columns.length
|
|
267
|
+
cols={columns.length}
|
|
222
268
|
density={density}
|
|
223
269
|
snug={snug}
|
|
224
270
|
/>
|
|
225
271
|
))
|
|
226
272
|
) : paginated.length === 0 ? (
|
|
227
273
|
<tr>
|
|
228
|
-
<td colSpan={columns.length + (
|
|
274
|
+
<td colSpan={columns.length + (hasSpacer ? 1 : 0) + (hasAction ? 1 : 0)}>
|
|
229
275
|
<EmptyState title={emptyTitle} description={emptyDescription} />
|
|
230
276
|
</td>
|
|
231
277
|
</tr>
|
|
@@ -238,7 +284,7 @@ export function DataTable<T>({
|
|
|
238
284
|
comfortable && "min-h-[56px]",
|
|
239
285
|
compact && "min-h-[44px]",
|
|
240
286
|
"hover:bg-muted/40 dark:hover:bg-muted/25",
|
|
241
|
-
|
|
287
|
+
hasAction &&
|
|
242
288
|
"hover:shadow-[0_1px_0_rgba(0,0,0,0.04)] dark:hover:shadow-none"
|
|
243
289
|
)}
|
|
244
290
|
>
|
|
@@ -272,32 +318,44 @@ export function DataTable<T>({
|
|
|
272
318
|
</td>
|
|
273
319
|
))}
|
|
274
320
|
|
|
275
|
-
{
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
onClick={() => rowCta.onClick?.(row)}
|
|
292
|
-
className={cn(
|
|
293
|
-
"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",
|
|
294
|
-
compact
|
|
295
|
-
? "px-2.5 py-1 text-[11px]"
|
|
296
|
-
: "px-3 py-1.5 text-[12px]"
|
|
297
|
-
)}
|
|
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 }}
|
|
298
337
|
>
|
|
299
|
-
{
|
|
300
|
-
|
|
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>
|
|
301
359
|
</td>
|
|
302
360
|
) : null}
|
|
303
361
|
</tr>
|