@godxjp/ui 13.10.0 → 13.11.1
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.
|
@@ -201,6 +201,7 @@ DataTable.Content = function DataTableContent() {
|
|
|
201
201
|
const { t } = useTranslation();
|
|
202
202
|
const rowPadding = tableRowHeightClass;
|
|
203
203
|
const cellPadding = tableCellPaddingClass;
|
|
204
|
+
const hasPinEnd = columns.some((col) => col.pin === "end");
|
|
204
205
|
const onHeaderClick = (col) => {
|
|
205
206
|
if (!col.sortable || !onSortChange) return;
|
|
206
207
|
if (sort?.key !== col.key) {
|
|
@@ -211,93 +212,59 @@ DataTable.Content = function DataTableContent() {
|
|
|
211
212
|
onSortChange(void 0);
|
|
212
213
|
}
|
|
213
214
|
};
|
|
214
|
-
return /* @__PURE__ */ jsx(
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
col
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
col.align === "right" && "text-end",
|
|
233
|
-
col.align === "center" && "text-center",
|
|
234
|
-
col.hiddenOnMobile && "hidden md:table-cell",
|
|
235
|
-
isSortable && "select-none"
|
|
236
|
-
),
|
|
237
|
-
children: isSortable ? /* @__PURE__ */ jsx(
|
|
238
|
-
"button",
|
|
239
|
-
{
|
|
240
|
-
type: "button",
|
|
241
|
-
className: "ui-data-table-sort-button focus-visible:ring-ring rounded-sm focus-visible:ring-2",
|
|
242
|
-
onClick: () => {
|
|
243
|
-
onHeaderClick(col);
|
|
244
|
-
},
|
|
245
|
-
children: label
|
|
246
|
-
}
|
|
247
|
-
) : label
|
|
248
|
-
},
|
|
249
|
-
col.key
|
|
250
|
-
);
|
|
251
|
-
})
|
|
252
|
-
] }) }),
|
|
253
|
-
/* @__PURE__ */ jsx(TableBody, { children: loading ? /* @__PURE__ */ jsx(TableRow, { className: "hover:bg-transparent", children: /* @__PURE__ */ jsx(TableCell, { colSpan: emptyColSpan, className: "h-32 text-center", "aria-live": "polite", children: /* @__PURE__ */ jsx("span", { className: "text-muted-foreground text-sm", children: t("dataTable.loading") }) }) }) : data.length === 0 ? /* @__PURE__ */ jsx(TableRow, { className: "hover:bg-transparent", children: /* @__PURE__ */ jsx(
|
|
254
|
-
TableCell,
|
|
255
|
-
{
|
|
256
|
-
colSpan: emptyColSpan,
|
|
257
|
-
className: "ui-data-table-empty",
|
|
258
|
-
"aria-live": "polite",
|
|
259
|
-
children: empty ?? /* @__PURE__ */ jsx(EmptyState, { title: t("dataTable.empty") })
|
|
260
|
-
}
|
|
261
|
-
) }) : data.map((row) => {
|
|
262
|
-
const id = getRowId(row);
|
|
263
|
-
const isSelected = selected.has(id);
|
|
264
|
-
const isInteractiveTarget = (target) => !!target.closest("button, a, input, select, textarea, [role=menuitem]");
|
|
265
|
-
return /* @__PURE__ */ jsxs(
|
|
266
|
-
TableRow,
|
|
267
|
-
{
|
|
268
|
-
"data-state": isSelected ? "selected" : void 0,
|
|
269
|
-
tabIndex: onRowClick ? 0 : void 0,
|
|
270
|
-
onClick: (e) => {
|
|
271
|
-
const target = e.target;
|
|
272
|
-
if (isInteractiveTarget(target)) return;
|
|
273
|
-
onRowClick?.(row);
|
|
274
|
-
},
|
|
275
|
-
onKeyDown: onRowClick ? (e) => {
|
|
276
|
-
if (e.key !== "Enter" && e.key !== " ") return;
|
|
277
|
-
if (e.target !== e.currentTarget) return;
|
|
278
|
-
e.preventDefault();
|
|
279
|
-
onRowClick?.(row);
|
|
280
|
-
} : void 0,
|
|
281
|
-
className: cn(
|
|
282
|
-
rowPadding,
|
|
283
|
-
onRowClick && "hover:bg-muted/50 focus-visible:ring-ring cursor-pointer focus-visible:ring-2 focus-visible:outline-none focus-visible:ring-inset",
|
|
284
|
-
isSelected && "bg-muted/30"
|
|
285
|
-
),
|
|
286
|
-
children: [
|
|
287
|
-
selectable && /* @__PURE__ */ jsx(TableCell, { className: cellPadding, children: /* @__PURE__ */ jsx(
|
|
288
|
-
Checkbox,
|
|
215
|
+
return /* @__PURE__ */ jsx(
|
|
216
|
+
"div",
|
|
217
|
+
{
|
|
218
|
+
className: cn("ui-data-table-scroll", hasPinEnd && "ui-data-table-has-pin-end"),
|
|
219
|
+
"aria-busy": loading,
|
|
220
|
+
children: /* @__PURE__ */ jsx("div", { className: "ui-data-table-surface min-w-[640px] sm:min-w-0", children: /* @__PURE__ */ jsxs(Table, { children: [
|
|
221
|
+
/* @__PURE__ */ jsx(TableHeader, { className: "bg-secondary sticky top-0 z-10", children: /* @__PURE__ */ jsxs(TableRow, { children: [
|
|
222
|
+
selectable && /* @__PURE__ */ jsx(TableHead, { className: "w-10", children: /* @__PURE__ */ jsx(DataTable.SelectAll, {}) }),
|
|
223
|
+
columns.map((col) => {
|
|
224
|
+
const isSortable = !!col.sortable && !!onSortChange;
|
|
225
|
+
const isActiveSort = isSortable && sort?.key === col.key;
|
|
226
|
+
const sortIndicator = isSortable ? isActiveSort ? sort?.direction === "asc" ? /* @__PURE__ */ jsx(ArrowUp, { className: "size-3", "aria-hidden": "true" }) : /* @__PURE__ */ jsx(ArrowDown, { className: "size-3", "aria-hidden": "true" }) : /* @__PURE__ */ jsx(ChevronsUpDown, { className: "text-muted-foreground size-3", "aria-hidden": "true" }) : null;
|
|
227
|
+
const label = /* @__PURE__ */ jsxs("span", { className: "ui-data-table-sort-label", children: [
|
|
228
|
+
col.header,
|
|
229
|
+
sortIndicator
|
|
230
|
+
] });
|
|
231
|
+
return /* @__PURE__ */ jsx(
|
|
232
|
+
TableHead,
|
|
289
233
|
{
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
234
|
+
"data-empty": !col.header || void 0,
|
|
235
|
+
"aria-sort": isSortable ? isActiveSort ? sort?.direction === "asc" ? "ascending" : "descending" : "none" : void 0,
|
|
236
|
+
className: cn(
|
|
237
|
+
col.width,
|
|
238
|
+
col.align === "right" && "text-end",
|
|
239
|
+
col.align === "center" && "text-center",
|
|
240
|
+
col.hiddenOnMobile && "hidden md:table-cell",
|
|
241
|
+
isSortable && "select-none",
|
|
242
|
+
col.pin === "end" && "ui-data-table-pin-end"
|
|
243
|
+
),
|
|
244
|
+
children: isSortable ? /* @__PURE__ */ jsx(
|
|
245
|
+
"button",
|
|
246
|
+
{
|
|
247
|
+
type: "button",
|
|
248
|
+
className: "ui-data-table-sort-button focus-visible:ring-ring rounded-sm focus-visible:ring-2",
|
|
249
|
+
onClick: () => {
|
|
250
|
+
onHeaderClick(col);
|
|
251
|
+
},
|
|
252
|
+
children: label
|
|
253
|
+
}
|
|
254
|
+
) : label
|
|
255
|
+
},
|
|
256
|
+
col.key
|
|
257
|
+
);
|
|
258
|
+
})
|
|
259
|
+
] }) }),
|
|
260
|
+
/* @__PURE__ */ jsx(TableBody, { children: loading ? (
|
|
261
|
+
// Shaped skeleton rows rendered INSIDE the real table grid so they
|
|
262
|
+
// share its borders + column widths — no second framed container
|
|
263
|
+
// (which double-borders when the table sits in a Card). Count is
|
|
264
|
+
// bounded to the previous page so the height barely shifts.
|
|
265
|
+
Array.from({ length: Math.min(Math.max(data.length, 6), 10) }).map((_, i) => /* @__PURE__ */ jsxs(TableRow, { className: cn(rowPadding, "hover:bg-transparent"), children: [
|
|
266
|
+
selectable && /* @__PURE__ */ jsx(TableCell, { className: cellPadding, children: /* @__PURE__ */ jsx("div", { className: "ui-skeleton-block size-4 rounded-sm" }) }),
|
|
267
|
+
columns.map((col, j) => /* @__PURE__ */ jsx(
|
|
301
268
|
TableCell,
|
|
302
269
|
{
|
|
303
270
|
className: cn(
|
|
@@ -305,23 +272,99 @@ DataTable.Content = function DataTableContent() {
|
|
|
305
272
|
col.width,
|
|
306
273
|
col.align === "right" && "text-end",
|
|
307
274
|
col.align === "center" && "text-center",
|
|
308
|
-
col.hiddenOnMobile && "hidden md:table-cell"
|
|
275
|
+
col.hiddenOnMobile && "hidden md:table-cell",
|
|
276
|
+
col.pin === "end" && "ui-data-table-pin-end"
|
|
309
277
|
),
|
|
310
|
-
children:
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
278
|
+
children: /* @__PURE__ */ jsx(
|
|
279
|
+
"div",
|
|
280
|
+
{
|
|
281
|
+
className: cn(
|
|
282
|
+
"ui-skeleton-block h-4",
|
|
283
|
+
j === 0 ? "w-1/2" : "w-3/4",
|
|
284
|
+
col.align === "right" && "ms-auto",
|
|
285
|
+
col.align === "center" && "mx-auto"
|
|
286
|
+
)
|
|
287
|
+
}
|
|
288
|
+
)
|
|
316
289
|
},
|
|
317
290
|
col.key
|
|
318
291
|
))
|
|
319
|
-
]
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
292
|
+
] }, `skeleton-${i}`))
|
|
293
|
+
) : data.length === 0 ? /* @__PURE__ */ jsx(TableRow, { className: "hover:bg-transparent", children: /* @__PURE__ */ jsx(
|
|
294
|
+
TableCell,
|
|
295
|
+
{
|
|
296
|
+
colSpan: emptyColSpan,
|
|
297
|
+
className: "ui-data-table-empty",
|
|
298
|
+
"aria-live": "polite",
|
|
299
|
+
children: empty ?? /* @__PURE__ */ jsx(EmptyState, { title: t("dataTable.empty") })
|
|
300
|
+
}
|
|
301
|
+
) }) : data.map((row) => {
|
|
302
|
+
const id = getRowId(row);
|
|
303
|
+
const isSelected = selected.has(id);
|
|
304
|
+
const isInteractiveTarget = (target) => !!target.closest("button, a, input, select, textarea, [role=menuitem]");
|
|
305
|
+
return /* @__PURE__ */ jsxs(
|
|
306
|
+
TableRow,
|
|
307
|
+
{
|
|
308
|
+
"data-state": isSelected ? "selected" : void 0,
|
|
309
|
+
tabIndex: onRowClick ? 0 : void 0,
|
|
310
|
+
onClick: (e) => {
|
|
311
|
+
const target = e.target;
|
|
312
|
+
if (isInteractiveTarget(target)) return;
|
|
313
|
+
onRowClick?.(row);
|
|
314
|
+
},
|
|
315
|
+
onKeyDown: onRowClick ? (e) => {
|
|
316
|
+
if (e.key !== "Enter" && e.key !== " ") return;
|
|
317
|
+
if (e.target !== e.currentTarget) return;
|
|
318
|
+
e.preventDefault();
|
|
319
|
+
onRowClick?.(row);
|
|
320
|
+
} : void 0,
|
|
321
|
+
className: cn(
|
|
322
|
+
rowPadding,
|
|
323
|
+
onRowClick && "hover:bg-muted/50 focus-visible:ring-ring cursor-pointer focus-visible:ring-2 focus-visible:outline-none focus-visible:ring-inset",
|
|
324
|
+
isSelected && "bg-muted/30"
|
|
325
|
+
),
|
|
326
|
+
children: [
|
|
327
|
+
selectable && /* @__PURE__ */ jsx(TableCell, { className: cellPadding, children: /* @__PURE__ */ jsx(
|
|
328
|
+
Checkbox,
|
|
329
|
+
{
|
|
330
|
+
checked: isSelected,
|
|
331
|
+
onCheckedChange: () => {
|
|
332
|
+
toggleSelect(id);
|
|
333
|
+
},
|
|
334
|
+
"aria-label": t("dataTable.selectRow", { id }),
|
|
335
|
+
onClick: (e) => {
|
|
336
|
+
e.stopPropagation();
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
) }),
|
|
340
|
+
columns.map((col) => /* @__PURE__ */ jsx(
|
|
341
|
+
TableCell,
|
|
342
|
+
{
|
|
343
|
+
className: cn(
|
|
344
|
+
cellPadding,
|
|
345
|
+
col.width,
|
|
346
|
+
col.align === "right" && "text-end",
|
|
347
|
+
col.align === "center" && "text-center",
|
|
348
|
+
col.hiddenOnMobile && "hidden md:table-cell",
|
|
349
|
+
col.pin === "end" && "ui-data-table-pin-end"
|
|
350
|
+
),
|
|
351
|
+
children: col.render ? col.render(row) : (() => {
|
|
352
|
+
const v = row[col.key];
|
|
353
|
+
if (v == null) return "\u2014";
|
|
354
|
+
if (typeof v === "string" || typeof v === "number") return String(v);
|
|
355
|
+
return "\u2014";
|
|
356
|
+
})()
|
|
357
|
+
},
|
|
358
|
+
col.key
|
|
359
|
+
))
|
|
360
|
+
]
|
|
361
|
+
},
|
|
362
|
+
id
|
|
363
|
+
);
|
|
364
|
+
}) })
|
|
365
|
+
] }) })
|
|
366
|
+
}
|
|
367
|
+
);
|
|
325
368
|
};
|
|
326
369
|
DataTable.Content.displayName = "DataTable.Content";
|
|
327
370
|
DataTable.Pagination = function DataTablePagination({
|
|
@@ -18,6 +18,13 @@ export type ColumnDefProp<T> = {
|
|
|
18
18
|
width?: string;
|
|
19
19
|
align?: ColumnAlignProp;
|
|
20
20
|
hiddenOnMobile?: boolean;
|
|
21
|
+
/**
|
|
22
|
+
* Pin the column to the inline-end edge so it stays visible while the rest of
|
|
23
|
+
* the table scrolls horizontally — the standard home for a row-actions column.
|
|
24
|
+
* The pinned cell keeps an opaque, hover/selection-aware background and casts a
|
|
25
|
+
* separating shadow. Pin at most one column per table.
|
|
26
|
+
*/
|
|
27
|
+
pin?: "end";
|
|
21
28
|
};
|
|
22
29
|
/** Set of selected row IDs. */
|
|
23
30
|
export type SelectedIdsProp = Set<string>;
|
package/dist/styles/control.css
CHANGED
|
@@ -217,7 +217,11 @@
|
|
|
217
217
|
.ui-choice-group[data-orientation="horizontal"] {
|
|
218
218
|
display: flex;
|
|
219
219
|
flex-wrap: wrap;
|
|
220
|
-
|
|
220
|
+
/* Occupy the control height and centre the row so a horizontal radio/checkbox
|
|
221
|
+
* group lines up with sibling inputs in a horizontal FormField (its options
|
|
222
|
+
* are only ~icon-height tall, which otherwise sat above the field label). */
|
|
223
|
+
align-items: center;
|
|
224
|
+
min-height: var(--control-height);
|
|
221
225
|
column-gap: var(--choice-group-gap-x);
|
|
222
226
|
row-gap: var(--choice-group-gap-y);
|
|
223
227
|
}
|
|
@@ -157,4 +157,42 @@
|
|
|
157
157
|
inset-inline-end: 0;
|
|
158
158
|
}
|
|
159
159
|
}
|
|
160
|
+
|
|
161
|
+
/* ── Pinned (sticky) inline-end column ────────────────────────────────
|
|
162
|
+
* The standard home for a row-actions column: it stays put while the rest
|
|
163
|
+
* of the table scrolls horizontally. The cell carries an opaque, state-aware
|
|
164
|
+
* background so scrolling cells don't bleed through, plus a token shadow that
|
|
165
|
+
* separates it from the body. When present, the scroll fade is hidden (the
|
|
166
|
+
* pin's own shadow is the affordance). RTL-correct via logical properties. */
|
|
167
|
+
.ui-data-table-has-pin-end::after {
|
|
168
|
+
display: none;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
.ui-data-table-pin-end {
|
|
172
|
+
position: sticky;
|
|
173
|
+
inset-inline-end: 0;
|
|
174
|
+
z-index: 2;
|
|
175
|
+
background-color: hsl(var(--background));
|
|
176
|
+
box-shadow: var(--table-pin-shadow);
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
th.ui-data-table-pin-end {
|
|
180
|
+
background-color: hsl(var(--secondary));
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
/* box-shadow has no logical offset — flip its inline direction by hand in RTL,
|
|
184
|
+
* where the pinned column sits on the visual left and the shadow points right. */
|
|
185
|
+
[dir="rtl"] .ui-data-table-pin-end {
|
|
186
|
+
box-shadow: 6px 0 6px -5px hsl(var(--foreground) / 0.12);
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
/* Mirror the row's hover / selected tint — a translucent muted wash layered
|
|
190
|
+
* over the opaque base, so the pinned cell matches the rest of its row. */
|
|
191
|
+
.ui-data-table-surface tbody tr:hover > .ui-data-table-pin-end {
|
|
192
|
+
background-image: linear-gradient(hsl(var(--muted) / 0.5), hsl(var(--muted) / 0.5));
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
.ui-data-table-surface tbody tr[data-state="selected"] > .ui-data-table-pin-end {
|
|
196
|
+
background-image: linear-gradient(hsl(var(--muted) / 0.3), hsl(var(--muted) / 0.3));
|
|
197
|
+
}
|
|
160
198
|
}
|
|
@@ -8,4 +8,6 @@
|
|
|
8
8
|
--table-cell-padding-y: var(--space-2);
|
|
9
9
|
--table-cell-space-x: var(--control-padding-x);
|
|
10
10
|
--table-head-font-size: var(--font-size-xs);
|
|
11
|
+
/* Inline-end shadow that lifts a pinned (sticky) action column off the body it scrolls over. */
|
|
12
|
+
--table-pin-shadow: -6px 0 6px -5px hsl(var(--foreground) / 0.12);
|
|
11
13
|
}
|