@godxjp/ui 13.10.0 → 13.11.0

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("div", { className: "ui-data-table-scroll", "aria-busy": loading, children: /* @__PURE__ */ jsx("div", { className: "ui-data-table-surface min-w-[640px] sm:min-w-0", children: /* @__PURE__ */ jsxs(Table, { children: [
215
- /* @__PURE__ */ jsx(TableHeader, { className: "bg-secondary sticky top-0 z-10", children: /* @__PURE__ */ jsxs(TableRow, { children: [
216
- selectable && /* @__PURE__ */ jsx(TableHead, { className: "w-10", children: /* @__PURE__ */ jsx(DataTable.SelectAll, {}) }),
217
- columns.map((col) => {
218
- const isSortable = !!col.sortable && !!onSortChange;
219
- const isActiveSort = isSortable && sort?.key === col.key;
220
- 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;
221
- const label = /* @__PURE__ */ jsxs("span", { className: "ui-data-table-sort-label", children: [
222
- col.header,
223
- sortIndicator
224
- ] });
225
- return /* @__PURE__ */ jsx(
226
- TableHead,
227
- {
228
- "data-empty": !col.header || void 0,
229
- "aria-sort": isSortable ? isActiveSort ? sort?.direction === "asc" ? "ascending" : "descending" : "none" : void 0,
230
- className: cn(
231
- col.width,
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
- checked: isSelected,
291
- onCheckedChange: () => {
292
- toggleSelect(id);
293
- },
294
- "aria-label": t("dataTable.selectRow", { id }),
295
- onClick: (e) => {
296
- e.stopPropagation();
297
- }
298
- }
299
- ) }),
300
- columns.map((col) => /* @__PURE__ */ jsx(
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: col.render ? col.render(row) : (() => {
311
- const v = row[col.key];
312
- if (v == null) return "\u2014";
313
- if (typeof v === "string" || typeof v === "number") return String(v);
314
- return "\u2014";
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
- id
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>;
@@ -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
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@godxjp/ui",
3
- "version": "13.10.0",
3
+ "version": "13.11.0",
4
4
  "type": "module",
5
5
  "packageManager": "pnpm@10.29.1",
6
6
  "sideEffects": false,