@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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@payglocal_ui/flux-ui",
3
- "version": "0.2.1",
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
  },
@@ -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
- /** `auto` lets columns breathe; `fixed` uses `colgroup` hints */
66
- tableLayout?: "auto" | "fixed";
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
- const rowCtaColWidth = compact ? 108 : 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;
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={{ tableLayout, width: "100%" }}
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
- {rowCta ? <col style={{ width: rowCtaColWidth }} /> : null}
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
- {rowCta ? <th className={cn(headPad, "w-[1%]")} aria-hidden /> : null}
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 + (rowCta ? 1 : 0)}
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 + (rowCta ? 1 : 0)}>
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
- rowCta &&
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
- {rowCta ? (
276
- <td
277
- className={cn(
278
- cellPad,
279
- "text-left align-middle whitespace-nowrap",
280
- comfortable
281
- ? "pl-2 pr-5"
282
- : compact
283
- ? snug
284
- ? "pl-1.5 pr-2"
285
- : "pl-1.5 pr-3"
286
- : "pl-3 pr-4"
287
- )}
288
- >
289
- <button
290
- type="button"
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
- {rowCta.label}
300
- </button>
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>