@jielga/tmdatagrid 0.1.0 → 0.3.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/README.md +35 -2
- package/dist/index.d.ts +792 -327
- package/dist/index.js +1581 -892
- package/dist/index.js.map +1 -1
- package/dist/styles.css +1 -1
- package/package.json +15 -7
- package/skills/columns/SKILL.md +8 -7
- package/skills/features/SKILL.md +104 -16
- package/skills/getting-started/SKILL.md +78 -20
- package/skills/options/SKILL.md +12 -7
- package/skills/server-side/SKILL.md +5 -2
- package/src/tmdatagrid/TMDataGridContext.ts +1 -1
- package/src/tmdatagrid/components/TMDataGrid.module.css +73 -0
- package/src/tmdatagrid/{TMDataGrid.tsx → components/TMDataGrid.tsx} +15 -4
- package/src/tmdatagrid/components/TMDataGridColumnsPanel.module.css +25 -0
- package/src/tmdatagrid/{TMDataGridColumnsPanel.tsx → components/TMDataGridColumnsPanel.tsx} +10 -4
- package/src/tmdatagrid/components/TMDataGridFilterPanel.module.css +35 -0
- package/src/tmdatagrid/{TMDataGridFilterPanel.tsx → components/TMDataGridFilterPanel.tsx} +91 -14
- package/src/tmdatagrid/components/TMDataGridFilterPills.module.css +35 -0
- package/src/tmdatagrid/components/TMDataGridFilterPills.tsx +135 -0
- package/src/tmdatagrid/components/TMDataGridFooter.module.css +18 -0
- package/src/tmdatagrid/components/TMDataGridFooter.tsx +171 -0
- package/src/tmdatagrid/components/TMDataGridGroupColumn.module.css +48 -0
- package/src/tmdatagrid/components/TMDataGridGroupColumn.tsx +147 -0
- package/src/tmdatagrid/components/TMDataGridHeaderCell.module.css +159 -0
- package/src/tmdatagrid/{TMDataGridHeaderCell.tsx → components/TMDataGridHeaderCell.tsx} +99 -14
- package/src/tmdatagrid/components/TMDataGridSelectColumn.tsx +177 -0
- package/src/tmdatagrid/components/TMDataGridTable.module.css +137 -0
- package/src/tmdatagrid/components/TMDataGridTable.tsx +720 -0
- package/src/tmdatagrid/components/TMDataGridToolbar.module.css +12 -0
- package/src/tmdatagrid/{TMDataGridToolbar.tsx → components/TMDataGridToolbar.tsx} +8 -5
- package/src/tmdatagrid/{icons.ts → components/icons.ts} +5 -0
- package/src/tmdatagrid/components/sticky.module.css +45 -0
- package/src/tmdatagrid/core/capabilities.ts +215 -0
- package/src/tmdatagrid/{columnOrdering.ts → core/columnOrdering.ts} +2 -2
- package/src/tmdatagrid/{columnUtils.ts → core/columnUtils.ts} +1 -1
- package/src/tmdatagrid/{filterOperators.ts → core/filterOperators.ts} +25 -0
- package/src/tmdatagrid/core/grouping.ts +21 -0
- package/src/tmdatagrid/{persistence.ts → core/persistence.ts} +92 -2
- package/src/tmdatagrid/core/rowSelection.ts +202 -0
- package/src/tmdatagrid/index.ts +39 -9
- package/src/tmdatagrid/useTMDataGrid.tsx +318 -66
- package/src/tmdatagrid/TMDataGrid.module.css +0 -410
- package/src/tmdatagrid/TMDataGridFooter.tsx +0 -74
- package/src/tmdatagrid/TMDataGridTable.tsx +0 -297
- package/src/tmdatagrid/capabilities.ts +0 -117
- /package/src/tmdatagrid/{sizes.ts → core/sizes.ts} +0 -0
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
import { ActionIcon, Group, Select, Text, Tooltip } from "@mantine/core";
|
|
2
|
+
import type { RowData } from "@tanstack/react-table";
|
|
3
|
+
import { useSelector } from "@tanstack/react-store";
|
|
4
|
+
import type { ReactNode } from "react";
|
|
5
|
+
import classes from "./TMDataGridFooter.module.css";
|
|
6
|
+
import { useTMDataGridContext } from "../TMDataGridContext";
|
|
7
|
+
import { getGridCapabilities } from "../core/capabilities";
|
|
8
|
+
import { isPagingActive } from "../core/rowSelection";
|
|
9
|
+
import { ChevronLeftIcon, ChevronRightIcon } from "./icons";
|
|
10
|
+
import type { TMDataGridTable } from "../useTMDataGrid";
|
|
11
|
+
|
|
12
|
+
/** Shown on the greyed-out pager, and the reason it is greyed out. */
|
|
13
|
+
const PAGING_SUSPENDED_HINT =
|
|
14
|
+
"Paging is off while the rows are grouped: the whole tree is rendered and virtualized. Ungroup to page again.";
|
|
15
|
+
|
|
16
|
+
/** Distilled pagination state and actions for building a custom pager. */
|
|
17
|
+
export type TMDataGridPaginationApi = {
|
|
18
|
+
pageIndex: number;
|
|
19
|
+
pageSize: number;
|
|
20
|
+
/** `-1` when a manual grid declares `pageCount: -1` (unknown total). */
|
|
21
|
+
pageCount: number;
|
|
22
|
+
rowCount: number;
|
|
23
|
+
canPreviousPage: boolean;
|
|
24
|
+
canNextPage: boolean;
|
|
25
|
+
setPageIndex: (pageIndex: number) => void;
|
|
26
|
+
setPageSize: (pageSize: number) => void;
|
|
27
|
+
previousPage: () => void;
|
|
28
|
+
nextPage: () => void;
|
|
29
|
+
firstPage: () => void;
|
|
30
|
+
lastPage: () => void;
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Reads {@link TMDataGridPaginationApi} off a table. The Footer feeds it to the
|
|
35
|
+
* `pagination` render prop; a pager living outside the Footer can call it with
|
|
36
|
+
* the table from `useTMDataGrid` directly.
|
|
37
|
+
*/
|
|
38
|
+
export function getTMDataGridPaginationApi<TData extends RowData>(
|
|
39
|
+
table: TMDataGridTable<TData>,
|
|
40
|
+
): TMDataGridPaginationApi {
|
|
41
|
+
const { pageIndex, pageSize } = table.store.state.pagination;
|
|
42
|
+
return {
|
|
43
|
+
pageIndex,
|
|
44
|
+
pageSize,
|
|
45
|
+
pageCount: table.getPageCount(),
|
|
46
|
+
rowCount: table.getRowCount(),
|
|
47
|
+
canPreviousPage: table.getCanPreviousPage(),
|
|
48
|
+
canNextPage: table.getCanNextPage(),
|
|
49
|
+
setPageIndex: (index) => table.setPageIndex(index),
|
|
50
|
+
setPageSize: (size) => table.setPageSize(size),
|
|
51
|
+
previousPage: () => table.previousPage(),
|
|
52
|
+
nextPage: () => table.nextPage(),
|
|
53
|
+
firstPage: () => table.firstPage(),
|
|
54
|
+
lastPage: () => table.lastPage(),
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export type TMDataGridFooterProps = {
|
|
59
|
+
pageSizeOptions?: ReadonlyArray<number>;
|
|
60
|
+
/** Replaces the built-in pager. Receives the distilled pagination API. */
|
|
61
|
+
pagination?: (api: TMDataGridPaginationApi) => ReactNode;
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* MUI-style pager: "Rows per page · from–to of total · ‹ ›".
|
|
66
|
+
*
|
|
67
|
+
* Renders nothing unless pagination is enabled (`enablePagination` or
|
|
68
|
+
* `manualPagination`) — the footer is a pager; extra footer content belongs in
|
|
69
|
+
* your own layout.
|
|
70
|
+
*
|
|
71
|
+
* Row totals come from `table.getRowCount()`, which prefers `options.rowCount`
|
|
72
|
+
* — so a server-paged grid shows the server's total without changes here.
|
|
73
|
+
*/
|
|
74
|
+
export function TMDataGridFooter({
|
|
75
|
+
pageSizeOptions = [10, 25, 50, 100],
|
|
76
|
+
pagination,
|
|
77
|
+
}: TMDataGridFooterProps) {
|
|
78
|
+
const { table, features, controlSize } = useTMDataGridContext();
|
|
79
|
+
useSelector(table.store);
|
|
80
|
+
|
|
81
|
+
if (!getGridCapabilities(table, features).canPaginate) return null;
|
|
82
|
+
|
|
83
|
+
if (pagination) {
|
|
84
|
+
return (
|
|
85
|
+
<div className={classes.footer}>
|
|
86
|
+
{pagination(getTMDataGridPaginationApi(table))}
|
|
87
|
+
</div>
|
|
88
|
+
);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// Grouping suspends paging — see isPagingActive. The pager is greyed out
|
|
92
|
+
// rather than dropped, so that a footer going quiet reads as a state the grid
|
|
93
|
+
// is in rather than as something that broke.
|
|
94
|
+
const paging = isPagingActive(table, features);
|
|
95
|
+
const { pageIndex, pageSize } = table.store.state.pagination;
|
|
96
|
+
const total = table.getRowCount();
|
|
97
|
+
const from = total === 0 ? 0 : pageIndex * pageSize + 1;
|
|
98
|
+
const to = Math.min(total, (pageIndex + 1) * pageSize);
|
|
99
|
+
// A page size restored from storage, or set through `initialState`, need not
|
|
100
|
+
// be one of the offered sizes. Without it in the list the Select renders
|
|
101
|
+
// blank, so the current size is folded in rather than shown as nothing.
|
|
102
|
+
const sizeOptions = pageSizeOptions.includes(pageSize)
|
|
103
|
+
? pageSizeOptions
|
|
104
|
+
: [...pageSizeOptions, pageSize].sort((a, b) => a - b);
|
|
105
|
+
|
|
106
|
+
return (
|
|
107
|
+
<Tooltip
|
|
108
|
+
label={PAGING_SUSPENDED_HINT}
|
|
109
|
+
disabled={paging}
|
|
110
|
+
withArrow
|
|
111
|
+
multiline
|
|
112
|
+
w={260}
|
|
113
|
+
position="top"
|
|
114
|
+
>
|
|
115
|
+
<div className={classes.footer} data-paging-suspended={!paging}>
|
|
116
|
+
<Group gap="xs" wrap="nowrap">
|
|
117
|
+
<Text size={controlSize} c="dimmed">
|
|
118
|
+
Rows per page:
|
|
119
|
+
</Text>
|
|
120
|
+
<Select
|
|
121
|
+
size={controlSize}
|
|
122
|
+
w={78}
|
|
123
|
+
allowDeselect={false}
|
|
124
|
+
variant="unstyled"
|
|
125
|
+
disabled={!paging}
|
|
126
|
+
data={sizeOptions.map(String)}
|
|
127
|
+
value={String(pageSize)}
|
|
128
|
+
onChange={(value) => {
|
|
129
|
+
table.setPageSize(Number(value) || pageSizeOptions[0]);
|
|
130
|
+
table.setPageIndex(0);
|
|
131
|
+
}}
|
|
132
|
+
/>
|
|
133
|
+
</Group>
|
|
134
|
+
|
|
135
|
+
<Text size={controlSize} c="dimmed">
|
|
136
|
+
{paging ? (
|
|
137
|
+
<>
|
|
138
|
+
{from}–{to} of {total}
|
|
139
|
+
</>
|
|
140
|
+
) : (
|
|
141
|
+
// Not a range: nothing is being sliced, so a range would be a lie.
|
|
142
|
+
// Counted before grouping too — `getRowCount()` would be counting
|
|
143
|
+
// the tree, so a collapsed grid would claim to hold eight rows.
|
|
144
|
+
<>Grouped · all {table.getFilteredRowModel().rows.length} rows</>
|
|
145
|
+
)}
|
|
146
|
+
</Text>
|
|
147
|
+
|
|
148
|
+
<Group gap={4} wrap="nowrap">
|
|
149
|
+
<ActionIcon
|
|
150
|
+
variant="subtle"
|
|
151
|
+
color="gray"
|
|
152
|
+
aria-label="Previous page"
|
|
153
|
+
disabled={!paging || !table.getCanPreviousPage()}
|
|
154
|
+
onClick={() => table.previousPage()}
|
|
155
|
+
>
|
|
156
|
+
<ChevronLeftIcon size={18} stroke={1.6} />
|
|
157
|
+
</ActionIcon>
|
|
158
|
+
<ActionIcon
|
|
159
|
+
variant="subtle"
|
|
160
|
+
color="gray"
|
|
161
|
+
aria-label="Next page"
|
|
162
|
+
disabled={!paging || !table.getCanNextPage()}
|
|
163
|
+
onClick={() => table.nextPage()}
|
|
164
|
+
>
|
|
165
|
+
<ChevronRightIcon size={18} stroke={1.6} />
|
|
166
|
+
</ActionIcon>
|
|
167
|
+
</Group>
|
|
168
|
+
</div>
|
|
169
|
+
</Tooltip>
|
|
170
|
+
);
|
|
171
|
+
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
/* The tree lane. Fills its cell so the whole indented width is a hit target,
|
|
2
|
+
which is what makes a deeply nested group as easy to click as a top-level
|
|
3
|
+
one. */
|
|
4
|
+
.groupToggle {
|
|
5
|
+
display: flex;
|
|
6
|
+
align-items: center;
|
|
7
|
+
gap: 6px;
|
|
8
|
+
width: 100%;
|
|
9
|
+
min-width: 0;
|
|
10
|
+
font-size: var(--dg-font-size);
|
|
11
|
+
color: inherit;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
.chevron {
|
|
15
|
+
display: flex;
|
|
16
|
+
flex: 0 0 auto;
|
|
17
|
+
align-items: center;
|
|
18
|
+
color: var(--mantine-color-dimmed);
|
|
19
|
+
transition: transform 120ms ease;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
.chevron[data-expanded="true"] {
|
|
23
|
+
transform: rotate(90deg);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/* One icon rotated rather than two swapped: the turn is what reads as the
|
|
27
|
+
expand, and a swap would jump. */
|
|
28
|
+
@media (prefers-reduced-motion: reduce) {
|
|
29
|
+
.chevron {
|
|
30
|
+
transition: none;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
.groupLabel {
|
|
35
|
+
min-width: 0;
|
|
36
|
+
overflow: hidden;
|
|
37
|
+
white-space: nowrap;
|
|
38
|
+
text-overflow: ellipsis;
|
|
39
|
+
font-weight: 600;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/* Never squeezed out by a long group value — the count is the part that tells
|
|
43
|
+
you whether the group is worth opening. */
|
|
44
|
+
.groupCount {
|
|
45
|
+
flex: 0 0 auto;
|
|
46
|
+
color: var(--mantine-color-dimmed);
|
|
47
|
+
font-variant-numeric: tabular-nums;
|
|
48
|
+
}
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
import { UnstyledButton } from "@mantine/core";
|
|
2
|
+
import type { ColumnDef, Row, RowData } from "@tanstack/react-table";
|
|
3
|
+
import { useSelector } from "@tanstack/react-store";
|
|
4
|
+
import { shallow } from "@tanstack/store";
|
|
5
|
+
import classes from "./TMDataGridGroupColumn.module.css";
|
|
6
|
+
import { ChevronRightIcon } from "./icons";
|
|
7
|
+
import { getColumnLabel } from "../core/columnUtils";
|
|
8
|
+
import { getGroupDataRows } from "../core/grouping";
|
|
9
|
+
import type { TMDataGridFeatures, TMDataGridTable } from "../useTMDataGrid";
|
|
10
|
+
|
|
11
|
+
export const GROUP_COLUMN_ID = "__group__";
|
|
12
|
+
|
|
13
|
+
/** Indent added per level of nesting, in px. */
|
|
14
|
+
const INDENT_STEP = 16;
|
|
15
|
+
|
|
16
|
+
/** Shown for a group whose value is empty — `String(null)` would read as "null". */
|
|
17
|
+
const BLANK_GROUP_LABEL = "(Blank)";
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* How a grouping value is written into the tree cell.
|
|
21
|
+
*
|
|
22
|
+
* Deliberately not the grouped column's own `cell` renderer: that renderer is
|
|
23
|
+
* written for a data row and is free to reach into `row.original`, which on a
|
|
24
|
+
* group row is the first leaf's record rather than anything about the group.
|
|
25
|
+
*/
|
|
26
|
+
export function formatGroupValue(value: unknown): string {
|
|
27
|
+
if (value === null || value === undefined || value === "") {
|
|
28
|
+
return BLANK_GROUP_LABEL;
|
|
29
|
+
}
|
|
30
|
+
if (value instanceof Date) return value.toLocaleDateString();
|
|
31
|
+
return String(value);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* The tree cell: chevron, group value and leaf count, indented by depth.
|
|
36
|
+
*
|
|
37
|
+
* Expansion is read through a subscription rather than from `row.getIsExpanded()`
|
|
38
|
+
* in the component body for the same reason the select checkbox does it — the
|
|
39
|
+
* `row` identity survives an expand, so the React Compiler would cache the call
|
|
40
|
+
* along with it and the chevron would never turn. See TMDataGridSelectColumn.
|
|
41
|
+
*/
|
|
42
|
+
function GroupCell<TData extends RowData>({
|
|
43
|
+
row,
|
|
44
|
+
}: {
|
|
45
|
+
row: Row<TMDataGridFeatures, TData>;
|
|
46
|
+
}) {
|
|
47
|
+
const expanded = useSelector(row.table.store, () => row.getIsExpanded());
|
|
48
|
+
|
|
49
|
+
// Leaf rows keep the lane empty: their values are in the data columns, and
|
|
50
|
+
// the indent alone is what places them under their group.
|
|
51
|
+
if (!row.getIsGrouped()) return null;
|
|
52
|
+
|
|
53
|
+
const label = formatGroupValue(row.groupingValue);
|
|
54
|
+
|
|
55
|
+
return (
|
|
56
|
+
<UnstyledButton
|
|
57
|
+
className={classes.groupToggle}
|
|
58
|
+
// Padding rather than margin, so the whole indented width stays clickable.
|
|
59
|
+
style={{ paddingInlineStart: row.depth * INDENT_STEP }}
|
|
60
|
+
aria-expanded={expanded}
|
|
61
|
+
aria-label={`${expanded ? "Collapse" : "Expand"} ${label}`}
|
|
62
|
+
// The row underneath may select or highlight on click; expanding is its
|
|
63
|
+
// own gesture and must not also trigger those.
|
|
64
|
+
onClick={(event) => {
|
|
65
|
+
event.stopPropagation();
|
|
66
|
+
row.toggleExpanded();
|
|
67
|
+
}}
|
|
68
|
+
>
|
|
69
|
+
<span className={classes.chevron} data-expanded={expanded}>
|
|
70
|
+
<ChevronRightIcon size={16} stroke={1.6} />
|
|
71
|
+
</span>
|
|
72
|
+
<span className={classes.groupLabel}>{label}</span>
|
|
73
|
+
{/* Data rows, not direct children and not `getLeafRows()`: a nested group
|
|
74
|
+
counts the records under it, never the sub-groups in between. */}
|
|
75
|
+
<span className={classes.groupCount}>
|
|
76
|
+
({getGroupDataRows(row).length})
|
|
77
|
+
</span>
|
|
78
|
+
</UnstyledButton>
|
|
79
|
+
);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Names the columns currently grouped on, so the lane says what it is showing
|
|
84
|
+
* rather than a static "Group".
|
|
85
|
+
*/
|
|
86
|
+
function GroupHeader<TData extends RowData>({
|
|
87
|
+
table,
|
|
88
|
+
}: {
|
|
89
|
+
table: TMDataGridTable<TData>;
|
|
90
|
+
}) {
|
|
91
|
+
const grouping = useSelector(table.store, (state) => state.grouping, {
|
|
92
|
+
compare: shallow,
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
if (grouping.length === 0) return "Group";
|
|
96
|
+
return grouping
|
|
97
|
+
.map((columnId) => {
|
|
98
|
+
const column = table.getColumn(columnId);
|
|
99
|
+
return column ? getColumnLabel(column) : columnId;
|
|
100
|
+
})
|
|
101
|
+
.join(" / ");
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* The generated tree column, prepended whenever grouping is enabled and hidden
|
|
106
|
+
* again while `grouping` is empty — see the visibility effect in
|
|
107
|
+
* `useTMDataGrid`.
|
|
108
|
+
*
|
|
109
|
+
* It exists because TanStack ships no auto group column: `groupedColumnMode:
|
|
110
|
+
* "remove"` takes the grouped column out of the grid, so something has to hold
|
|
111
|
+
* the tree. Modelled on the checkbox column, which is generated the same way.
|
|
112
|
+
*
|
|
113
|
+
* Not groupable itself, and nothing had to be written to make that true —
|
|
114
|
+
* `column.getCanGroup()` requires an `accessorFn`, which a display column has
|
|
115
|
+
* no reason to have.
|
|
116
|
+
*/
|
|
117
|
+
export function createGroupColumn<TData extends RowData>(): ColumnDef<
|
|
118
|
+
TMDataGridFeatures,
|
|
119
|
+
TData,
|
|
120
|
+
unknown
|
|
121
|
+
> {
|
|
122
|
+
return {
|
|
123
|
+
id: GROUP_COLUMN_ID,
|
|
124
|
+
meta: {
|
|
125
|
+
label: "Group",
|
|
126
|
+
// Structurally the first column after the checkbox lane.
|
|
127
|
+
enableOrdering: false,
|
|
128
|
+
},
|
|
129
|
+
size: 260,
|
|
130
|
+
minSize: 180,
|
|
131
|
+
enableSorting: false,
|
|
132
|
+
enableColumnFilter: false,
|
|
133
|
+
enableGlobalFilter: false,
|
|
134
|
+
// Keeps it out of the columns panel and out of the header menu: its
|
|
135
|
+
// visibility is not the user's to set, it follows the grouping state.
|
|
136
|
+
enableHiding: false,
|
|
137
|
+
// Structurally pinned to the left; users shouldn't be able to move it.
|
|
138
|
+
enablePinning: false,
|
|
139
|
+
cell: ({ row }) => <GroupCell row={row} />,
|
|
140
|
+
// A group row has subRows, so every cell on it that is not the grouped
|
|
141
|
+
// column reports `getIsAggregated()` — this lane included. Without an
|
|
142
|
+
// `aggregatedCell` the body would take that as "nothing to summarise" and
|
|
143
|
+
// render the tree lane blank on exactly the rows it exists for.
|
|
144
|
+
aggregatedCell: ({ row }) => <GroupCell row={row} />,
|
|
145
|
+
header: ({ table }) => <GroupHeader table={table} />,
|
|
146
|
+
};
|
|
147
|
+
}
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
.headerCell {
|
|
2
|
+
position: relative;
|
|
3
|
+
display: flex;
|
|
4
|
+
align-items: center;
|
|
5
|
+
gap: 2px;
|
|
6
|
+
padding: 0 var(--dg-padding);
|
|
7
|
+
min-width: 0;
|
|
8
|
+
min-height: var(--dg-header-height);
|
|
9
|
+
font-weight: 600;
|
|
10
|
+
font-size: var(--dg-font-size);
|
|
11
|
+
background-color: var(--mantine-color-body);
|
|
12
|
+
border-bottom: 1px solid var(--mantine-color-default-border);
|
|
13
|
+
user-select: none;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
.headerCell[data-align="right"] {
|
|
17
|
+
justify-content: flex-end;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
.headerCell[data-align="center"] {
|
|
21
|
+
justify-content: center;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/* Same as the body cells: the select-all box sits in a fixed 48px track, which
|
|
25
|
+
the text padding would squeeze it out of at the larger sizes. */
|
|
26
|
+
.headerCell[data-select-column="true"] {
|
|
27
|
+
padding: 0;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/* Sorted or filtered columns read as active, like the MUI data grid. */
|
|
31
|
+
.headerCell[data-active="true"] .headerTitle {
|
|
32
|
+
color: var(--mantine-primary-color-filled);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
.headerTitle {
|
|
36
|
+
flex: 1 1 auto;
|
|
37
|
+
min-width: 0;
|
|
38
|
+
overflow: hidden;
|
|
39
|
+
text-overflow: ellipsis;
|
|
40
|
+
white-space: nowrap;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/* Right-aligned headers stay text so the ellipsis still works; centered ones
|
|
44
|
+
hold a control (the select-all checkbox), so they become a flex box. */
|
|
45
|
+
.headerCell[data-align="right"] .headerTitle {
|
|
46
|
+
text-align: right;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
.headerCell[data-align="center"] .headerTitle {
|
|
50
|
+
display: flex;
|
|
51
|
+
justify-content: center;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
.headerActions {
|
|
55
|
+
display: flex;
|
|
56
|
+
align-items: center;
|
|
57
|
+
gap: 0;
|
|
58
|
+
flex-shrink: 0;
|
|
59
|
+
margin-left: auto;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/* Actions always reserve their width. On right-aligned columns they move to the
|
|
63
|
+
leading edge so the header text stays flush with the cell values below it. */
|
|
64
|
+
.headerCell[data-align="right"] .headerActions {
|
|
65
|
+
order: -1;
|
|
66
|
+
margin-left: 0;
|
|
67
|
+
margin-right: auto;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/* Idle actions are removed from layout, not just faded — reserving their width
|
|
71
|
+
would truncate header text that otherwise fits. Indicators that must stay
|
|
72
|
+
visible (active sort, active filter, open menu) set data-pinned-visible. */
|
|
73
|
+
.headerAction {
|
|
74
|
+
display: none;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
.headerCell:hover .headerAction,
|
|
78
|
+
.headerCell:focus-within .headerAction,
|
|
79
|
+
.headerAction[data-pinned-visible="true"] {
|
|
80
|
+
display: flex;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
.headerCellSortable {
|
|
84
|
+
cursor: pointer;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/* Column reordering. The dragged header stays in place and dims — the browser
|
|
88
|
+
already carries a snapshot of it under the pointer — while the header being
|
|
89
|
+
dragged over draws a bar on the edge the column will land against. */
|
|
90
|
+
.headerCellDragging {
|
|
91
|
+
opacity: 0.4;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
.headerCellDropBefore::before,
|
|
95
|
+
.headerCellDropAfter::before {
|
|
96
|
+
content: "";
|
|
97
|
+
position: absolute;
|
|
98
|
+
top: 0;
|
|
99
|
+
bottom: 0;
|
|
100
|
+
width: 2px;
|
|
101
|
+
background-color: var(--mantine-primary-color-filled);
|
|
102
|
+
z-index: 5;
|
|
103
|
+
pointer-events: none;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
.headerCellDropBefore::before {
|
|
107
|
+
left: 0;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
.headerCellDropAfter::before {
|
|
111
|
+
right: 0;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/* Doubles as the column divider seen between every header cell. */
|
|
115
|
+
.columnSeparator {
|
|
116
|
+
position: absolute;
|
|
117
|
+
top: 25%;
|
|
118
|
+
right: 0;
|
|
119
|
+
height: 50%;
|
|
120
|
+
width: 10px;
|
|
121
|
+
transform: translateX(50%);
|
|
122
|
+
display: flex;
|
|
123
|
+
justify-content: center;
|
|
124
|
+
z-index: 1;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
.columnSeparatorResizable {
|
|
128
|
+
cursor: col-resize;
|
|
129
|
+
touch-action: none;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/* The divider is a hover affordance, not permanent chrome — it only appears on
|
|
133
|
+
the header cell you are pointing at, or on the column being resized. */
|
|
134
|
+
.columnSeparator::after {
|
|
135
|
+
content: "";
|
|
136
|
+
width: 1px;
|
|
137
|
+
height: 100%;
|
|
138
|
+
background-color: transparent;
|
|
139
|
+
transition:
|
|
140
|
+
background-color 120ms ease,
|
|
141
|
+
width 120ms ease;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
.headerCell:hover .columnSeparator::after,
|
|
145
|
+
.headerCell:focus-within .columnSeparator::after {
|
|
146
|
+
background-color: var(--mantine-color-default-border);
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
.columnSeparatorResizable:hover::after,
|
|
150
|
+
.columnSeparatorActive::after {
|
|
151
|
+
width: 2px;
|
|
152
|
+
background-color: var(--mantine-primary-color-filled);
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/* Marks the pin position a column is currently in. */
|
|
156
|
+
.menuItemActive {
|
|
157
|
+
background-color: var(--mantine-color-default-hover);
|
|
158
|
+
font-weight: 600;
|
|
159
|
+
}
|