397 lines
14 KiB
CSS
397 lines
14 KiB
CSS
/* ==========================================================================
|
|
Table — Data display component for structured tabular data.
|
|
========================================================================== */
|
|
|
|
/* --------------------------------------------------------------------------
|
|
Table Root — Outer container for table + optional footer.
|
|
Primary variant: gray background with the table body as a white card inside.
|
|
-------------------------------------------------------------------------- */
|
|
.table-root {
|
|
@apply relative grid w-full overflow-clip;
|
|
grid-template-columns: minmax(0, 1fr);
|
|
}
|
|
|
|
/* --------------------------------------------------------------------------
|
|
Scroll Container — Allows the table to scroll horizontally while the
|
|
wrapper and footer remain at full width. The parent grid with
|
|
minmax(0, 1fr) creates a hard width boundary that table layout
|
|
cannot push beyond.
|
|
-------------------------------------------------------------------------- */
|
|
.table__scroll-container {
|
|
@apply scrollbar overflow-x-auto;
|
|
scrollbar-gutter: auto;
|
|
}
|
|
|
|
.table-root--primary {
|
|
@apply bg-surface-secondary px-1 pb-1;
|
|
border-radius: min(32px, calc(var(--radius) * 2.5));
|
|
}
|
|
|
|
.table-root--secondary {
|
|
/* No background, padding, or rounding on the root */
|
|
}
|
|
|
|
/* Secondary: header is a standalone rounded card.
|
|
Background is on cells (not <thead>) because Firefox does not support */
|
|
.table-root--secondary .table__header {
|
|
@apply border-b-0 bg-transparent;
|
|
}
|
|
|
|
.table-root--secondary .table__column {
|
|
@apply bg-surface-secondary;
|
|
}
|
|
|
|
/* Round only the truly first/last column header.
|
|
Non-virtualized: column is a <th> child of <tr>.
|
|
Virtualized: column is wrapped in a `VirtualizerItem` (role="presentation") so
|
|
`:first-child` / `:last-child` on `.table__column` would match every column
|
|
(each is the only child of its wrapper). Walk up to the row instead. */
|
|
.table-root--secondary
|
|
:is(
|
|
th.table__column:first-child,
|
|
[role="row"] > [role="presentation"]:first-of-type > .table__column
|
|
) {
|
|
border-start-start-radius: min(32px, var(--radius-2xl));
|
|
border-end-start-radius: min(32px, var(--radius-2xl));
|
|
}
|
|
|
|
.table-root--secondary
|
|
:is(
|
|
th.table__column:last-child,
|
|
[role="row"] > [role="presentation"]:last-of-type > .table__column
|
|
) {
|
|
border-start-end-radius: min(32px, var(--radius-2xl));
|
|
border-end-end-radius: min(32px, var(--radius-2xl));
|
|
}
|
|
|
|
/* Secondary: body has no shadow or rounded corners */
|
|
.table-root--secondary .table__body {
|
|
@apply shadow-none;
|
|
|
|
/* Non-virtualized */
|
|
& tr:first-child td:first-child,
|
|
& tr:first-child td:last-child,
|
|
& tr:last-child td:first-child,
|
|
& tr:last-child td:last-child {
|
|
@apply rounded-none;
|
|
}
|
|
|
|
/* Virtualized: undo the rounding applied by .table__body:not(tbody) */
|
|
&:not(tbody) {
|
|
@apply overflow-visible rounded-none;
|
|
}
|
|
}
|
|
|
|
/* Secondary: rows are transparent and keep bottom border on every row */
|
|
.table-root--secondary .table__row {
|
|
.table__cell {
|
|
@apply border-b border-separator-tertiary/50 bg-transparent;
|
|
}
|
|
|
|
@media (hover: hover) {
|
|
&:hover .table__cell,
|
|
&[data-hovered="true"] .table__cell {
|
|
@apply bg-default/50;
|
|
}
|
|
}
|
|
}
|
|
|
|
/* --------------------------------------------------------------------------
|
|
Table Content (<table> element) — the white card inside the root
|
|
-------------------------------------------------------------------------- */
|
|
.table__content {
|
|
@apply w-full border-separate border-spacing-0 text-sm;
|
|
}
|
|
|
|
/* Primary variant: table gets the white surface card look */
|
|
.table-root--primary .table__content {
|
|
@apply overflow-clip;
|
|
}
|
|
|
|
/* --------------------------------------------------------------------------
|
|
Table Header (<thead>)
|
|
-------------------------------------------------------------------------- */
|
|
.table__header {
|
|
@apply border-b border-separator/50 bg-surface-secondary;
|
|
}
|
|
|
|
/* --------------------------------------------------------------------------
|
|
Table Column (<th>)
|
|
-------------------------------------------------------------------------- */
|
|
.table__column {
|
|
@apply relative px-4 py-2.5 text-left text-xs font-medium text-muted;
|
|
|
|
/* Separator between columns — a short vertical line on the right edge */
|
|
&::after {
|
|
content: "";
|
|
@apply pointer-events-none absolute end-0 top-1/2 h-4 w-px -translate-y-1/2 rounded-sm bg-separator;
|
|
}
|
|
|
|
&:last-child:not(:only-child)::after {
|
|
content: none;
|
|
}
|
|
|
|
/* Sortable columns */
|
|
&[data-allows-sorting="true"] {
|
|
cursor: var(--cursor-interactive);
|
|
}
|
|
|
|
@media (hover: hover) {
|
|
&[data-allows-sorting="true"]:hover,
|
|
&[data-allows-sorting="true"][data-hovered="true"] {
|
|
@apply text-foreground;
|
|
}
|
|
}
|
|
|
|
/* Focus visible — inset shadow for consistent table focus styling */
|
|
&:focus-visible,
|
|
&[data-focus-visible="true"] {
|
|
@apply rounded-lg outline-none;
|
|
box-shadow: inset 0 0 0 2px var(--focus);
|
|
}
|
|
}
|
|
|
|
/* Virtualized: each column is wrapped in a `VirtualizerItem` (role="presentation"),
|
|
so `.table__column:last-child:not(:only-child)::after` above never matches.
|
|
Hide the trailing separator on the rightmost wrapper instead. */
|
|
[role="row"] > [role="presentation"]:last-of-type:not(:only-of-type) > .table__column::after {
|
|
content: none;
|
|
}
|
|
|
|
/* --------------------------------------------------------------------------
|
|
Sortable Column Header — wraps the column label and an optional indicator
|
|
icon. Rendered inside a sortable `Table.Column`'s render prop callback.
|
|
-------------------------------------------------------------------------- */
|
|
.table__sortable-column-header {
|
|
@apply flex items-center justify-between;
|
|
}
|
|
|
|
/* --------------------------------------------------------------------------
|
|
Sortable Column Indicator — chevron that flips based on `data-direction`.
|
|
Defaults to pointing up (ascending) and rotates 180° for descending.
|
|
-------------------------------------------------------------------------- */
|
|
.table__sortable-column-indicator {
|
|
@apply size-3 transform transition-transform duration-100 ease-out;
|
|
|
|
&[data-direction="descending"] {
|
|
@apply rotate-180;
|
|
}
|
|
}
|
|
|
|
/* --------------------------------------------------------------------------
|
|
Table Body (<tbody>)
|
|
-------------------------------------------------------------------------- */
|
|
.table__body {
|
|
/* Non-virtualized: round corners via first/last cells
|
|
(border-radius on <tbody> itself has no effect).
|
|
Logical properties so corners follow `dir` in RTL. */
|
|
& tr:first-child td:first-child {
|
|
border-start-start-radius: min(32px, var(--radius-2xl));
|
|
}
|
|
& tr:first-child td:last-child {
|
|
border-start-end-radius: min(32px, var(--radius-2xl));
|
|
}
|
|
& tr:last-child td:first-child {
|
|
border-end-start-radius: min(32px, var(--radius-2xl));
|
|
}
|
|
& tr:last-child td:last-child {
|
|
border-end-end-radius: min(32px, var(--radius-2xl));
|
|
}
|
|
|
|
/* Virtualized: elements are divs, not tr/td. VirtualizerItem wrappers
|
|
break :first-child/:last-child on rows and cells, so round the body
|
|
itself and clip overflow instead. */
|
|
&:not(tbody) {
|
|
@apply relative h-full overflow-clip;
|
|
border-radius: min(32px, var(--radius-2xl));
|
|
}
|
|
}
|
|
|
|
/* --------------------------------------------------------------------------
|
|
Table Row (<tr>)
|
|
-------------------------------------------------------------------------- */
|
|
.table__row {
|
|
@apply relative h-full;
|
|
|
|
/* Bottom separator */
|
|
@apply border-b border-separator/50;
|
|
|
|
/* Last row in body has no bottom border */
|
|
&:last-child {
|
|
@apply border-b-0;
|
|
}
|
|
|
|
/* Hover state — applied to cells so Firefox clips bg to border-radius */
|
|
@media (hover: hover) {
|
|
&:hover .table__cell,
|
|
&[data-hovered="true"] .table__cell {
|
|
@apply bg-surface/40;
|
|
}
|
|
}
|
|
|
|
/* Selected state */
|
|
&[data-selected="true"] .table__cell {
|
|
@apply bg-surface/10;
|
|
}
|
|
|
|
/* Disabled state */
|
|
&[aria-disabled="true"],
|
|
&[data-disabled="true"] {
|
|
@apply status-disabled;
|
|
}
|
|
|
|
/* Row focus ring: see “Table row focus ring” block below (after `.table__cell`). */
|
|
&:focus-visible,
|
|
&[data-focus-visible="true"] {
|
|
@apply outline-none;
|
|
}
|
|
|
|
/* Dragging state */
|
|
&[data-dragging="true"] {
|
|
@apply opacity-50;
|
|
}
|
|
|
|
/* Drop target state */
|
|
&[data-drop-target="true"] .table__cell {
|
|
@apply bg-accent-soft;
|
|
}
|
|
}
|
|
|
|
/* --------------------------------------------------------------------------
|
|
Table Cell (<td>)
|
|
-------------------------------------------------------------------------- */
|
|
.table__cell {
|
|
@apply h-full bg-surface px-4 py-3 align-middle text-sm text-foreground;
|
|
|
|
@apply border-b border-separator-tertiary/50;
|
|
|
|
/* Focus visible — inset shadow so focus stays within the cell boundaries */
|
|
&:focus-visible,
|
|
&[data-focus-visible="true"] {
|
|
@apply rounded-lg outline-none;
|
|
box-shadow: inset 0 0 0 2px var(--focus);
|
|
}
|
|
}
|
|
|
|
/* --------------------------------------------------------------------------
|
|
Table row focus ring — must follow `.table__cell` (cascade). One continuous
|
|
inset ring split across cells; `:is(.table__cell, .table__column)` + direct vs
|
|
`> *:…` covers virtualized wrappers. `:is(:focus-visible, [data-focus-visible])`
|
|
matches row keyboard focus from RAC.
|
|
-------------------------------------------------------------------------- */
|
|
.table__row:is(:focus-visible, [data-focus-visible="true"]) :is(.table__cell, .table__column) {
|
|
@apply shadow-none;
|
|
}
|
|
|
|
.table__row:is(:focus-visible, [data-focus-visible="true"])
|
|
> :is(.table__cell, .table__column):only-child,
|
|
.table__row:is(:focus-visible, [data-focus-visible="true"])
|
|
> *:only-child
|
|
:is(.table__cell, .table__column) {
|
|
@apply rounded-lg shadow-[inset_0_0_0_2px_var(--focus)] outline-none;
|
|
}
|
|
|
|
.table__row:is(:focus-visible, [data-focus-visible="true"])
|
|
> :is(.table__cell, .table__column):first-child:not(:only-child),
|
|
.table__row:is(:focus-visible, [data-focus-visible="true"])
|
|
> *:first-child:not(:only-child)
|
|
:is(.table__cell, .table__column) {
|
|
@apply rounded-l-lg shadow-[inset_2px_0_0_0_var(--focus),inset_0_2px_0_0_var(--focus),inset_0_-2px_0_0_var(--focus)] outline-none;
|
|
}
|
|
|
|
.table__row:is(:focus-visible, [data-focus-visible="true"])
|
|
> :is(.table__cell, .table__column):last-child:not(:only-child),
|
|
.table__row:is(:focus-visible, [data-focus-visible="true"])
|
|
> *:last-child:not(:only-child)
|
|
:is(.table__cell, .table__column) {
|
|
@apply rounded-r-lg shadow-[inset_-2px_0_0_0_var(--focus),inset_0_2px_0_0_var(--focus),inset_0_-2px_0_0_var(--focus)] outline-none;
|
|
}
|
|
|
|
.table__row:is(:focus-visible, [data-focus-visible="true"])
|
|
> :is(.table__cell, .table__column):not(:first-child):not(:last-child):not(:only-child),
|
|
.table__row:is(:focus-visible, [data-focus-visible="true"])
|
|
> *:not(:first-child):not(:last-child):not(:only-child)
|
|
:is(.table__cell, .table__column) {
|
|
@apply shadow-[inset_0_2px_0_0_var(--focus),inset_0_-2px_0_0_var(--focus)] outline-none;
|
|
}
|
|
|
|
/* --------------------------------------------------------------------------
|
|
Tree / expandable rows — indent the tree column by depth (any level).
|
|
React Aria sets `--table-row-level` (1-based) on each body row and
|
|
`data-level` on cells; tree column cells expose `[data-tree-column]`.
|
|
The row custom property inherits onto cells (1rem per level).
|
|
-------------------------------------------------------------------------- */
|
|
.table__cell[data-tree-column] {
|
|
padding-inline-start: calc(1rem * var(--table-row-level, 1));
|
|
}
|
|
|
|
/* --------------------------------------------------------------------------
|
|
Table Footer (div outside <table>, sits on gray bg)
|
|
-------------------------------------------------------------------------- */
|
|
.table__footer {
|
|
@apply flex items-center px-4 py-2.5;
|
|
}
|
|
|
|
/* --------------------------------------------------------------------------
|
|
Resizable Container — wraps table to enable column resizing via React Aria.
|
|
-------------------------------------------------------------------------- */
|
|
.table__resizable-container {
|
|
@apply relative scrollbar overflow-auto;
|
|
scrollbar-gutter: auto;
|
|
}
|
|
|
|
/* --------------------------------------------------------------------------
|
|
Column Resizer — drag handle between resizable columns.
|
|
Replaces the ::after separator when present.
|
|
-------------------------------------------------------------------------- */
|
|
.table__column-resizer {
|
|
/* Match the ::after separator: absolute, right edge, short vertical line */
|
|
@apply absolute end-0 top-1/2 h-4 w-px -translate-y-1/2 rounded-sm bg-separator;
|
|
/* Make it interactive — wider hit area with transparent padding */
|
|
@apply box-content translate-x-1/2 cursor-col-resize touch-none px-2;
|
|
/* Reset appearance */
|
|
@apply border-none bg-clip-content outline-none;
|
|
|
|
&[data-hovered="true"],
|
|
&:hover {
|
|
@apply h-full w-0.5 bg-accent;
|
|
}
|
|
|
|
&[data-resizing="true"] {
|
|
@apply h-full w-0.5 bg-accent;
|
|
}
|
|
|
|
&[data-focus-visible="true"],
|
|
&:focus-visible {
|
|
@apply h-full w-0.5 bg-focus;
|
|
}
|
|
}
|
|
|
|
/* Hide the ::after separator when a resizer is present in the column */
|
|
.table__column:has(.table__column-resizer)::after {
|
|
content: none;
|
|
}
|
|
|
|
/* --------------------------------------------------------------------------
|
|
Load More Item — sentinel row for infinite scrolling.
|
|
Non-virtualized: <tr> with <td colSpan=N> inside.
|
|
Virtualized: <div> with <div role="rowheader"> inside.
|
|
-------------------------------------------------------------------------- */
|
|
.table__load-more {
|
|
& td,
|
|
& [role="rowheader"] {
|
|
@apply py-3 text-center;
|
|
& > * {
|
|
@apply mx-auto;
|
|
}
|
|
}
|
|
}
|
|
|
|
/* --------------------------------------------------------------------------
|
|
Load More Content — styled container for the loading indicator inside
|
|
the load-more sentinel row.
|
|
-------------------------------------------------------------------------- */
|
|
.table__load-more-content {
|
|
@apply flex items-center justify-center gap-2 py-2;
|
|
}
|