@godxjp/ui 13.12.0 → 13.14.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.
- package/dist/components/data-display/data-table.d.ts +9 -3
- package/dist/components/data-display/data-table.js +157 -147
- package/dist/components/feedback/sonner.js +7 -3
- package/dist/components/layout/page-container.d.ts +1 -1
- package/dist/components/layout/page-container.js +32 -1
- package/dist/props/components/layout.prop.d.ts +10 -0
- package/dist/styles/layout.css +25 -0
- package/dist/styles/table-layout.css +6 -0
- package/dist/tokens/components/control.css +8 -7
- package/package.json +1 -1
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import * as React from "react";
|
|
2
|
-
import type { ColumnDefProp,
|
|
3
|
-
export type Density =
|
|
2
|
+
import type { ColumnDefProp, DensityProp, SortStateProp } from "../../props/vocabulary";
|
|
3
|
+
export type Density = DensityProp;
|
|
4
4
|
export type ColumnDef<T> = ColumnDefProp<T>;
|
|
5
5
|
interface DataTableProps<T> {
|
|
6
6
|
data: T[];
|
|
@@ -19,10 +19,16 @@ interface DataTableProps<T> {
|
|
|
19
19
|
loading?: boolean;
|
|
20
20
|
/** Custom empty content when `data` is empty; defaults to a built-in EmptyState. */
|
|
21
21
|
empty?: React.ReactNode;
|
|
22
|
+
/** Zebra-stripe the body rows (even rows get a subtle fill). */
|
|
23
|
+
striped?: boolean;
|
|
24
|
+
/** Highlight a row on hover even when it is not clickable (no `onRowClick`). */
|
|
25
|
+
hoverable?: boolean;
|
|
26
|
+
/** Pin the header to the top while the body scrolls. Default true. */
|
|
27
|
+
stickyHeader?: boolean;
|
|
22
28
|
className?: string;
|
|
23
29
|
children?: React.ReactNode;
|
|
24
30
|
}
|
|
25
|
-
export declare function DataTable<T>({ data, columns, getRowId, selectable, selected: controlledSelected, onSelectChange, onRowClick, density: controlledDensity, onDensityChange, sort, onSortChange, loading, empty, className, children, }: DataTableProps<T>): import("react/jsx-runtime").JSX.Element;
|
|
31
|
+
export declare function DataTable<T>({ data, columns, getRowId, selectable, selected: controlledSelected, onSelectChange, onRowClick, density: controlledDensity, onDensityChange, sort, onSortChange, loading, empty, striped, hoverable, stickyHeader, className, children, }: DataTableProps<T>): import("react/jsx-runtime").JSX.Element;
|
|
26
32
|
export declare namespace DataTable {
|
|
27
33
|
var Toolbar: ({ children, className, }: {
|
|
28
34
|
children?: React.ReactNode;
|
|
@@ -50,6 +50,9 @@ function DataTable({
|
|
|
50
50
|
onSortChange,
|
|
51
51
|
loading = false,
|
|
52
52
|
empty,
|
|
53
|
+
striped = false,
|
|
54
|
+
hoverable = false,
|
|
55
|
+
stickyHeader = true,
|
|
53
56
|
className,
|
|
54
57
|
children
|
|
55
58
|
}) {
|
|
@@ -95,25 +98,18 @@ function DataTable({
|
|
|
95
98
|
onSortChange,
|
|
96
99
|
loading,
|
|
97
100
|
empty,
|
|
98
|
-
emptyColSpan
|
|
101
|
+
emptyColSpan,
|
|
102
|
+
striped,
|
|
103
|
+
hoverable,
|
|
104
|
+
stickyHeader
|
|
99
105
|
};
|
|
100
106
|
const hasContent = React.Children.toArray(children).some(
|
|
101
107
|
(c) => React.isValidElement(c) && c.type.displayName === "DataTable.Content"
|
|
102
108
|
);
|
|
103
|
-
return /* @__PURE__ */ jsx(DataTableContext.Provider, { value: ctx, children: /* @__PURE__ */ jsxs(
|
|
104
|
-
|
|
105
|
-
{
|
|
106
|
-
|
|
107
|
-
"ui-data-table-root",
|
|
108
|
-
densityClass[density === "compact" ? "compact" : "comfortable"],
|
|
109
|
-
className
|
|
110
|
-
),
|
|
111
|
-
children: [
|
|
112
|
-
children,
|
|
113
|
-
!hasContent && /* @__PURE__ */ jsx(DataTable.Content, {})
|
|
114
|
-
]
|
|
115
|
-
}
|
|
116
|
-
) });
|
|
109
|
+
return /* @__PURE__ */ jsx(DataTableContext.Provider, { value: ctx, children: /* @__PURE__ */ jsxs("div", { className: cn("ui-data-table-root", densityClass[density], className), children: [
|
|
110
|
+
children,
|
|
111
|
+
!hasContent && /* @__PURE__ */ jsx(DataTable.Content, {})
|
|
112
|
+
] }) });
|
|
117
113
|
}
|
|
118
114
|
DataTable.Toolbar = function DataTableToolbar({
|
|
119
115
|
children,
|
|
@@ -196,7 +192,10 @@ DataTable.Content = function DataTableContent() {
|
|
|
196
192
|
onSortChange,
|
|
197
193
|
loading,
|
|
198
194
|
empty,
|
|
199
|
-
emptyColSpan
|
|
195
|
+
emptyColSpan,
|
|
196
|
+
striped,
|
|
197
|
+
hoverable,
|
|
198
|
+
stickyHeader
|
|
200
199
|
} = useDataTableContext();
|
|
201
200
|
const { t } = useTranslation();
|
|
202
201
|
const rowPadding = tableRowHeightClass;
|
|
@@ -217,127 +216,60 @@ DataTable.Content = function DataTableContent() {
|
|
|
217
216
|
{
|
|
218
217
|
className: cn("ui-data-table-scroll", hasPinEnd && "ui-data-table-has-pin-end"),
|
|
219
218
|
"aria-busy": loading,
|
|
220
|
-
children: /* @__PURE__ */ jsx(
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
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(
|
|
268
|
-
TableCell,
|
|
269
|
-
{
|
|
270
|
-
className: cn(
|
|
271
|
-
cellPadding,
|
|
272
|
-
col.width,
|
|
273
|
-
col.align === "right" && "text-end",
|
|
274
|
-
col.align === "center" && "text-center",
|
|
275
|
-
col.hiddenOnMobile && "hidden md:table-cell",
|
|
276
|
-
col.pin === "end" && "ui-data-table-pin-end"
|
|
277
|
-
),
|
|
278
|
-
children: /* @__PURE__ */ jsx(
|
|
279
|
-
"div",
|
|
219
|
+
children: /* @__PURE__ */ jsx(
|
|
220
|
+
"div",
|
|
221
|
+
{
|
|
222
|
+
className: "ui-data-table-surface min-w-[640px] sm:min-w-0",
|
|
223
|
+
"data-striped": striped ? "" : void 0,
|
|
224
|
+
"data-hoverable": hoverable ? "" : void 0,
|
|
225
|
+
children: /* @__PURE__ */ jsxs(Table, { children: [
|
|
226
|
+
/* @__PURE__ */ jsx(TableHeader, { className: cn("bg-secondary", stickyHeader && "sticky top-0 z-10"), children: /* @__PURE__ */ jsxs(TableRow, { children: [
|
|
227
|
+
selectable && /* @__PURE__ */ jsx(TableHead, { className: "w-10", children: /* @__PURE__ */ jsx(DataTable.SelectAll, {}) }),
|
|
228
|
+
columns.map((col) => {
|
|
229
|
+
const isSortable = !!col.sortable && !!onSortChange;
|
|
230
|
+
const isActiveSort = isSortable && sort?.key === col.key;
|
|
231
|
+
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;
|
|
232
|
+
const label = /* @__PURE__ */ jsxs("span", { className: "ui-data-table-sort-label", children: [
|
|
233
|
+
col.header,
|
|
234
|
+
sortIndicator
|
|
235
|
+
] });
|
|
236
|
+
return /* @__PURE__ */ jsx(
|
|
237
|
+
TableHead,
|
|
280
238
|
{
|
|
239
|
+
"data-empty": !col.header || void 0,
|
|
240
|
+
"aria-sort": isSortable ? isActiveSort ? sort?.direction === "asc" ? "ascending" : "descending" : "none" : void 0,
|
|
281
241
|
className: cn(
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
col.align === "
|
|
285
|
-
col.
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
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(
|
|
242
|
+
col.width,
|
|
243
|
+
col.align === "right" && "text-end",
|
|
244
|
+
col.align === "center" && "text-center",
|
|
245
|
+
col.hiddenOnMobile && "hidden md:table-cell",
|
|
246
|
+
isSortable && "select-none",
|
|
247
|
+
col.pin === "end" && "ui-data-table-pin-end"
|
|
248
|
+
),
|
|
249
|
+
children: isSortable ? /* @__PURE__ */ jsx(
|
|
250
|
+
"button",
|
|
251
|
+
{
|
|
252
|
+
type: "button",
|
|
253
|
+
className: "ui-data-table-sort-button focus-visible:ring-ring rounded-sm focus-visible:ring-2",
|
|
254
|
+
onClick: () => {
|
|
255
|
+
onHeaderClick(col);
|
|
256
|
+
},
|
|
257
|
+
children: label
|
|
258
|
+
}
|
|
259
|
+
) : label
|
|
260
|
+
},
|
|
261
|
+
col.key
|
|
262
|
+
);
|
|
263
|
+
})
|
|
264
|
+
] }) }),
|
|
265
|
+
/* @__PURE__ */ jsx(TableBody, { children: loading ? (
|
|
266
|
+
// Shaped skeleton rows rendered INSIDE the real table grid so they
|
|
267
|
+
// share its borders + column widths — no second framed container
|
|
268
|
+
// (which double-borders when the table sits in a Card). Count is
|
|
269
|
+
// bounded to the previous page so the height barely shifts.
|
|
270
|
+
Array.from({ length: Math.min(Math.max(data.length, 6), 10) }).map((_, i) => /* @__PURE__ */ jsxs(TableRow, { className: cn(rowPadding, "hover:bg-transparent"), children: [
|
|
271
|
+
selectable && /* @__PURE__ */ jsx(TableCell, { className: cellPadding, children: /* @__PURE__ */ jsx("div", { className: "ui-skeleton-block size-4 rounded-sm" }) }),
|
|
272
|
+
columns.map((col, j) => /* @__PURE__ */ jsx(
|
|
341
273
|
TableCell,
|
|
342
274
|
{
|
|
343
275
|
className: cn(
|
|
@@ -348,21 +280,99 @@ DataTable.Content = function DataTableContent() {
|
|
|
348
280
|
col.hiddenOnMobile && "hidden md:table-cell",
|
|
349
281
|
col.pin === "end" && "ui-data-table-pin-end"
|
|
350
282
|
),
|
|
351
|
-
children:
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
283
|
+
children: /* @__PURE__ */ jsx(
|
|
284
|
+
"div",
|
|
285
|
+
{
|
|
286
|
+
className: cn(
|
|
287
|
+
"ui-skeleton-block h-4",
|
|
288
|
+
j === 0 ? "w-1/2" : "w-3/4",
|
|
289
|
+
col.align === "right" && "ms-auto",
|
|
290
|
+
col.align === "center" && "mx-auto"
|
|
291
|
+
)
|
|
292
|
+
}
|
|
293
|
+
)
|
|
357
294
|
},
|
|
358
295
|
col.key
|
|
359
296
|
))
|
|
360
|
-
]
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
297
|
+
] }, `skeleton-${i}`))
|
|
298
|
+
) : data.length === 0 ? /* @__PURE__ */ jsx(TableRow, { className: "hover:bg-transparent", children: /* @__PURE__ */ jsx(
|
|
299
|
+
TableCell,
|
|
300
|
+
{
|
|
301
|
+
colSpan: emptyColSpan,
|
|
302
|
+
className: "ui-data-table-empty",
|
|
303
|
+
"aria-live": "polite",
|
|
304
|
+
children: empty ?? /* @__PURE__ */ jsx(EmptyState, { title: t("dataTable.empty") })
|
|
305
|
+
}
|
|
306
|
+
) }) : data.map((row) => {
|
|
307
|
+
const id = getRowId(row);
|
|
308
|
+
const isSelected = selected.has(id);
|
|
309
|
+
const isInteractiveTarget = (target) => !!target.closest("button, a, input, select, textarea, [role=menuitem]");
|
|
310
|
+
return /* @__PURE__ */ jsxs(
|
|
311
|
+
TableRow,
|
|
312
|
+
{
|
|
313
|
+
"data-state": isSelected ? "selected" : void 0,
|
|
314
|
+
tabIndex: onRowClick ? 0 : void 0,
|
|
315
|
+
onClick: (e) => {
|
|
316
|
+
const target = e.target;
|
|
317
|
+
if (isInteractiveTarget(target)) return;
|
|
318
|
+
onRowClick?.(row);
|
|
319
|
+
},
|
|
320
|
+
onKeyDown: onRowClick ? (e) => {
|
|
321
|
+
if (e.key !== "Enter" && e.key !== " ") return;
|
|
322
|
+
if (e.target !== e.currentTarget) return;
|
|
323
|
+
e.preventDefault();
|
|
324
|
+
onRowClick?.(row);
|
|
325
|
+
} : void 0,
|
|
326
|
+
className: cn(
|
|
327
|
+
rowPadding,
|
|
328
|
+
// Hover highlight when rows are clickable OR explicitly hoverable…
|
|
329
|
+
(onRowClick || hoverable) && "hover:bg-muted/50",
|
|
330
|
+
// …but the affordance (cursor + focus ring) only when clickable.
|
|
331
|
+
onRowClick && "focus-visible:ring-ring cursor-pointer focus-visible:ring-2 focus-visible:outline-none focus-visible:ring-inset",
|
|
332
|
+
isSelected && "bg-muted/30"
|
|
333
|
+
),
|
|
334
|
+
children: [
|
|
335
|
+
selectable && /* @__PURE__ */ jsx(TableCell, { className: cellPadding, children: /* @__PURE__ */ jsx(
|
|
336
|
+
Checkbox,
|
|
337
|
+
{
|
|
338
|
+
checked: isSelected,
|
|
339
|
+
onCheckedChange: () => {
|
|
340
|
+
toggleSelect(id);
|
|
341
|
+
},
|
|
342
|
+
"aria-label": t("dataTable.selectRow", { id }),
|
|
343
|
+
onClick: (e) => {
|
|
344
|
+
e.stopPropagation();
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
) }),
|
|
348
|
+
columns.map((col) => /* @__PURE__ */ jsx(
|
|
349
|
+
TableCell,
|
|
350
|
+
{
|
|
351
|
+
className: cn(
|
|
352
|
+
cellPadding,
|
|
353
|
+
col.width,
|
|
354
|
+
col.align === "right" && "text-end",
|
|
355
|
+
col.align === "center" && "text-center",
|
|
356
|
+
col.hiddenOnMobile && "hidden md:table-cell",
|
|
357
|
+
col.pin === "end" && "ui-data-table-pin-end"
|
|
358
|
+
),
|
|
359
|
+
children: col.render ? col.render(row) : (() => {
|
|
360
|
+
const v = row[col.key];
|
|
361
|
+
if (v == null) return "\u2014";
|
|
362
|
+
if (typeof v === "string" || typeof v === "number") return String(v);
|
|
363
|
+
return "\u2014";
|
|
364
|
+
})()
|
|
365
|
+
},
|
|
366
|
+
col.key
|
|
367
|
+
))
|
|
368
|
+
]
|
|
369
|
+
},
|
|
370
|
+
id
|
|
371
|
+
);
|
|
372
|
+
}) })
|
|
373
|
+
] })
|
|
374
|
+
}
|
|
375
|
+
)
|
|
366
376
|
}
|
|
367
377
|
);
|
|
368
378
|
};
|
|
@@ -34,9 +34,13 @@ function Toaster({ ...props }) {
|
|
|
34
34
|
loading: /* @__PURE__ */ jsx(Loader2, { className: "size-4 animate-spin", "aria-hidden": "true" })
|
|
35
35
|
},
|
|
36
36
|
style: {
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
37
|
+
// Color tokens are raw HSL triplets (consumed as hsl(var(--token)));
|
|
38
|
+
// sonner uses these vars verbatim as CSS colors, so wrap with hsl()
|
|
39
|
+
// here — unwrapped they are invalid values and the toast renders
|
|
40
|
+
// transparent.
|
|
41
|
+
"--normal-bg": "hsl(var(--popover))",
|
|
42
|
+
"--normal-text": "hsl(var(--popover-foreground))",
|
|
43
|
+
"--normal-border": "hsl(var(--border))",
|
|
40
44
|
"--border-radius": "var(--radius)"
|
|
41
45
|
},
|
|
42
46
|
position: "bottom-right",
|
|
@@ -2,7 +2,7 @@ import type { PageContainerProp, PageInsetProp } from "../../props/components/la
|
|
|
2
2
|
export type { PageContainerProp, PageContainerProp as PageContainerProps, } from "../../props/components/layout.prop";
|
|
3
3
|
export type { BreadcrumbItemProp, BreadcrumbItemProp as BreadcrumbItem, } from "../../props/vocabulary/navigation.prop";
|
|
4
4
|
export declare function PageContainerInset({ className, children, ...props }: PageInsetProp): import("react/jsx-runtime").JSX.Element;
|
|
5
|
-
declare function PageContainerRoot({ title, subtitle, extra, footer, breadcrumb, linkComponent: LinkComponent, density, variant, stickyFooter, fill, children, className, }: PageContainerProp): import("react/jsx-runtime").JSX.Element;
|
|
5
|
+
declare function PageContainerRoot({ title, subtitle, extra, footer, breadcrumb, linkComponent: LinkComponent, density, variant, stickyFooter, footerReveal, fill, children, className, }: PageContainerProp): import("react/jsx-runtime").JSX.Element;
|
|
6
6
|
export declare const PageContainer: typeof PageContainerRoot & {
|
|
7
7
|
Inset: typeof PageContainerInset;
|
|
8
8
|
};
|
|
@@ -1,7 +1,33 @@
|
|
|
1
1
|
import { jsx, jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { useEffect, useRef, useState } from "react";
|
|
2
3
|
import { ChevronRight } from "lucide-react";
|
|
3
4
|
import { cn } from "../../lib/utils";
|
|
4
5
|
import { densityClass, pageContainerVariantClass } from "../../lib/variants";
|
|
6
|
+
function scrollParent(el) {
|
|
7
|
+
let node = el?.parentElement ?? null;
|
|
8
|
+
while (node) {
|
|
9
|
+
const overflowY = getComputedStyle(node).overflowY;
|
|
10
|
+
if (overflowY === "auto" || overflowY === "scroll" || overflowY === "overlay") return node;
|
|
11
|
+
node = node.parentElement;
|
|
12
|
+
}
|
|
13
|
+
return null;
|
|
14
|
+
}
|
|
15
|
+
function useFooterReveal(enabled) {
|
|
16
|
+
const headerRef = useRef(null);
|
|
17
|
+
const [revealed, setRevealed] = useState(false);
|
|
18
|
+
useEffect(() => {
|
|
19
|
+
if (!enabled) return;
|
|
20
|
+
const el = headerRef.current;
|
|
21
|
+
if (!el || typeof IntersectionObserver === "undefined") return;
|
|
22
|
+
const observer = new IntersectionObserver(
|
|
23
|
+
([entry]) => setRevealed(!entry.isIntersecting),
|
|
24
|
+
{ root: scrollParent(el), threshold: 0 }
|
|
25
|
+
);
|
|
26
|
+
observer.observe(el);
|
|
27
|
+
return () => observer.disconnect();
|
|
28
|
+
}, [enabled]);
|
|
29
|
+
return { headerRef, revealed: enabled && revealed };
|
|
30
|
+
}
|
|
5
31
|
function PageContainerInset({ className, children, ...props }) {
|
|
6
32
|
return /* @__PURE__ */ jsx("div", { className: cn("ui-page-container-inset", className), ...props, children });
|
|
7
33
|
}
|
|
@@ -15,23 +41,28 @@ function PageContainerRoot({
|
|
|
15
41
|
density = "default",
|
|
16
42
|
variant = "default",
|
|
17
43
|
stickyFooter = false,
|
|
44
|
+
footerReveal = "always",
|
|
18
45
|
fill = false,
|
|
19
46
|
children,
|
|
20
47
|
className
|
|
21
48
|
}) {
|
|
49
|
+
const reveal = stickyFooter && footer != null && footerReveal === "onScroll";
|
|
50
|
+
const { headerRef, revealed } = useFooterReveal(reveal);
|
|
22
51
|
return /* @__PURE__ */ jsxs(
|
|
23
52
|
"div",
|
|
24
53
|
{
|
|
54
|
+
"data-revealed": revealed ? "true" : void 0,
|
|
25
55
|
className: cn(
|
|
26
56
|
"ui-page-container",
|
|
27
57
|
densityClass[density],
|
|
28
58
|
pageContainerVariantClass[variant],
|
|
29
59
|
stickyFooter && "ui-page-container--sticky-footer",
|
|
60
|
+
reveal && "ui-page-container--reveal-footer",
|
|
30
61
|
fill && "ui-page-container--fill",
|
|
31
62
|
className
|
|
32
63
|
),
|
|
33
64
|
children: [
|
|
34
|
-
/* @__PURE__ */ jsxs("header", { className: "ui-page-header", children: [
|
|
65
|
+
/* @__PURE__ */ jsxs("header", { ref: headerRef, className: "ui-page-header", children: [
|
|
35
66
|
breadcrumb && breadcrumb.length > 0 && /* @__PURE__ */ jsx("nav", { "aria-label": "Breadcrumb", className: "ui-breadcrumb", children: /* @__PURE__ */ jsx("ol", { className: "ui-breadcrumb-list", children: breadcrumb.map((item, i) => {
|
|
36
67
|
const isLast = i === breadcrumb.length - 1;
|
|
37
68
|
return /* @__PURE__ */ jsxs("li", { className: "ui-inline-xs", children: [
|
|
@@ -14,6 +14,16 @@ export type PageContainerProp = {
|
|
|
14
14
|
variant?: PageContainerVariantProp;
|
|
15
15
|
/** Pin footer to viewport bottom on scroll — pairs well with `variant="narrow"`. */
|
|
16
16
|
stickyFooter?: boolean;
|
|
17
|
+
/**
|
|
18
|
+
* When the footer is sticky, control WHEN it shows. `"always"` (default)
|
|
19
|
+
* keeps it pinned the whole time. `"onScroll"` hides it until the header
|
|
20
|
+
* (title + `extra` actions) scrolls out of view, then slides it up — the
|
|
21
|
+
* standard edit/create "save bar" so the primary actions stay reachable as
|
|
22
|
+
* the form scrolls, without cluttering the top. The footer stays mounted
|
|
23
|
+
* (no layout reflow → no jitter); observed against the nearest scroll
|
|
24
|
+
* container.
|
|
25
|
+
*/
|
|
26
|
+
footerReveal?: "always" | "onScroll";
|
|
17
27
|
/**
|
|
18
28
|
* Grow the body to fill the remaining shell height. Default `false` (top-packed,
|
|
19
29
|
* content-height — short pages leave no stretched void, gh#103). Enable for a
|
package/dist/styles/layout.css
CHANGED
|
@@ -308,6 +308,31 @@
|
|
|
308
308
|
background-color: hsl(var(--background));
|
|
309
309
|
}
|
|
310
310
|
|
|
311
|
+
/* footerReveal="onScroll": the sticky footer stays mounted (reserving its
|
|
312
|
+
* flow space, so toggling never reflows the body → no scroll jitter) but is
|
|
313
|
+
* slid below the fold until the header leaves the viewport, then slides up.
|
|
314
|
+
* data-revealed is toggled by an IntersectionObserver on the header. */
|
|
315
|
+
.ui-page-container--reveal-footer .ui-page-footer {
|
|
316
|
+
transform: translateY(100%);
|
|
317
|
+
opacity: 0;
|
|
318
|
+
pointer-events: none;
|
|
319
|
+
transition:
|
|
320
|
+
transform 200ms ease-out,
|
|
321
|
+
opacity 200ms ease-out;
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
.ui-page-container--reveal-footer[data-revealed="true"] .ui-page-footer {
|
|
325
|
+
transform: none;
|
|
326
|
+
opacity: 1;
|
|
327
|
+
pointer-events: auto;
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
@media (prefers-reduced-motion: reduce) {
|
|
331
|
+
.ui-page-container--reveal-footer .ui-page-footer {
|
|
332
|
+
transition: none;
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
|
|
311
336
|
.ui-page-header {
|
|
312
337
|
display: flex;
|
|
313
338
|
flex-direction: column;
|
|
@@ -94,6 +94,12 @@
|
|
|
94
94
|
border-radius: var(--radius-md);
|
|
95
95
|
}
|
|
96
96
|
|
|
97
|
+
/* Zebra striping (opt-in via the `striped` prop). Selected rows keep their
|
|
98
|
+
* own emphasis, so they are excluded from the stripe. */
|
|
99
|
+
.ui-data-table-surface[data-striped] tbody tr:nth-child(even):not([data-state="selected"]) {
|
|
100
|
+
background-color: hsl(var(--muted) / 0.4);
|
|
101
|
+
}
|
|
102
|
+
|
|
97
103
|
/* Empty-state cell hosts the built-in EmptyState (which brings its own padding). */
|
|
98
104
|
.ui-data-table-empty {
|
|
99
105
|
padding: 0;
|
|
@@ -62,6 +62,14 @@
|
|
|
62
62
|
--search-input-end-padding: calc(
|
|
63
63
|
var(--search-input-edge-inset) + var(--control-icon-size) + var(--control-gap)
|
|
64
64
|
);
|
|
65
|
+
|
|
66
|
+
--choice-description-font-size: var(--font-size-xs);
|
|
67
|
+
--color-picker-hex-font-size: var(--font-size-xs);
|
|
68
|
+
--command-group-heading-font-size: var(--font-size-xs);
|
|
69
|
+
--search-input-label-font-size: var(--font-size-xs);
|
|
70
|
+
--tag-input-chip-font-size: var(--font-size-xs);
|
|
71
|
+
--toggle-sm-font-size: var(--font-size-xs);
|
|
72
|
+
--button-sm-font-size: var(--font-size-xs);
|
|
65
73
|
}
|
|
66
74
|
|
|
67
75
|
/* Rule #24 — on touch devices (coarse pointer) interactive controls keep a ≥44px tap target
|
|
@@ -72,11 +80,4 @@
|
|
|
72
80
|
--control-height-compact: 2.75rem;
|
|
73
81
|
--control-height-default: 2.75rem;
|
|
74
82
|
}
|
|
75
|
-
--choice-description-font-size: var(--font-size-xs);
|
|
76
|
-
--color-picker-hex-font-size: var(--font-size-xs);
|
|
77
|
-
--command-group-heading-font-size: var(--font-size-xs);
|
|
78
|
-
--search-input-label-font-size: var(--font-size-xs);
|
|
79
|
-
--tag-input-chip-font-size: var(--font-size-xs);
|
|
80
|
-
--toggle-sm-font-size: var(--font-size-xs);
|
|
81
|
-
--button-sm-font-size: var(--font-size-xs);
|
|
82
83
|
}
|