@eifi1/ui-kit 0.47.0 → 0.48.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 +5 -3
- package/dist/admin/admin-sections.d.ts +8 -0
- package/dist/admin/admin-sections.js +2 -1
- package/dist/admin/admin-sections.js.map +1 -1
- package/dist/auth/verify-email.d.ts +9 -1
- package/dist/auth/verify-email.js +22 -13
- package/dist/auth/verify-email.js.map +1 -1
- package/dist/components/alert-banner.d.ts +29 -1
- package/dist/components/alert-banner.js +84 -7
- package/dist/components/alert-banner.js.map +1 -1
- package/dist/components/button-look.d.ts +4 -0
- package/dist/components/button-look.js +2 -0
- package/dist/components/button-look.js.map +1 -1
- package/dist/components/combobox-core.js +5 -1
- package/dist/components/combobox-core.js.map +1 -1
- package/dist/components/data-table-column-settings.d.ts +6 -1
- package/dist/components/data-table-column-settings.js +12 -2
- package/dist/components/data-table-column-settings.js.map +1 -1
- package/dist/components/data-table-fit.d.ts +44 -0
- package/dist/components/data-table-fit.js +103 -0
- package/dist/components/data-table-fit.js.map +1 -0
- package/dist/components/data-table-groups.d.ts +95 -0
- package/dist/components/data-table-groups.js +165 -0
- package/dist/components/data-table-groups.js.map +1 -0
- package/dist/components/data-table-head.js +3 -1
- package/dist/components/data-table-head.js.map +1 -1
- package/dist/components/data-table-labels.d.ts +7 -0
- package/dist/components/data-table-labels.js +3 -0
- package/dist/components/data-table-labels.js.map +1 -1
- package/dist/components/data-table-leading-row.d.ts +17 -0
- package/dist/components/data-table-leading-row.js +41 -0
- package/dist/components/data-table-leading-row.js.map +1 -0
- package/dist/components/data-table-resize.js +1 -1
- package/dist/components/data-table-resize.js.map +1 -1
- package/dist/components/data-table.d.ts +78 -2
- package/dist/components/data-table.js +348 -257
- package/dist/components/data-table.js.map +1 -1
- package/dist/components/disclosure.d.ts +3 -1
- package/dist/components/disclosure.js +10 -4
- package/dist/components/disclosure.js.map +1 -1
- package/dist/components/dropdown.js +8 -1
- package/dist/components/dropdown.js.map +1 -1
- package/dist/components/floating-panel.d.ts +14 -1
- package/dist/components/floating-panel.js +7 -2
- package/dist/components/floating-panel.js.map +1 -1
- package/dist/components/hover-menu.js +5 -1
- package/dist/components/hover-menu.js.map +1 -1
- package/dist/components/icon-button.js +2 -1
- package/dist/components/icon-button.js.map +1 -1
- package/dist/components/popover.js +5 -1
- package/dist/components/popover.js.map +1 -1
- package/dist/components/table-skeleton.d.ts +35 -0
- package/dist/components/table-skeleton.js +75 -0
- package/dist/components/table-skeleton.js.map +1 -0
- package/dist/components/tabs.d.ts +10 -12
- package/dist/components/tabs.js +20 -10
- package/dist/components/tabs.js.map +1 -1
- package/dist/components/use-table-state.d.ts +8 -7
- package/dist/components/use-table-state.js +37 -3
- package/dist/components/use-table-state.js.map +1 -1
- package/dist/data-table.d.ts +1 -0
- package/dist/data-table.js.map +1 -1
- package/dist/eslint/index.js +1 -1
- package/dist/feedback/feedback-table.d.ts +5 -0
- package/dist/feedback/feedback-table.js +30 -7
- package/dist/feedback/feedback-table.js.map +1 -1
- package/dist/hooks/use-dismiss.d.ts +30 -1
- package/dist/hooks/use-dismiss.js +30 -2
- package/dist/hooks/use-dismiss.js.map +1 -1
- package/dist/i18n/german.js +6 -1
- package/dist/i18n/german.js.map +1 -1
- package/dist/i18n/kit-labels.d.ts +4 -0
- package/dist/i18n/kit-labels.js.map +1 -1
- package/dist/i18n/locales/en.js +6 -1
- package/dist/i18n/locales/en.js.map +2 -2
- package/dist/i18n/locales/es.js +6 -1
- package/dist/i18n/locales/es.js.map +1 -1
- package/dist/i18n/locales/fr.js +6 -1
- package/dist/i18n/locales/fr.js.map +1 -1
- package/dist/i18n/locales/hu.js +6 -1
- package/dist/i18n/locales/hu.js.map +1 -1
- package/dist/i18n/locales/it.js +6 -1
- package/dist/i18n/locales/it.js.map +1 -1
- package/dist/i18n/locales/zh.js +6 -1
- package/dist/i18n/locales/zh.js.map +1 -1
- package/dist/i18n/shared-labels.js +3 -1
- package/dist/i18n/shared-labels.js.map +1 -1
- package/dist/index.d.ts +2 -0
- package/dist/index.js +2 -0
- package/dist/index.js.map +1 -1
- package/dist/lib/float-clearance.d.ts +8 -0
- package/dist/lib/float-clearance.js +29 -0
- package/dist/lib/float-clearance.js.map +1 -0
- package/dist/settings/settings-catalogue.d.ts +10 -0
- package/dist/settings/settings-catalogue.js.map +1 -1
- package/dist/settings/settings-layout.js +10 -3
- package/dist/settings/settings-layout.js.map +1 -1
- package/dist/shell/app-shell.d.ts +5 -0
- package/dist/shell/app-shell.js +5 -3
- package/dist/shell/app-shell.js.map +1 -1
- package/dist/shell/option-switcher-menu.d.ts +2 -0
- package/dist/shell/option-switcher-menu.js +6 -2
- package/dist/shell/option-switcher-menu.js.map +1 -1
- package/dist/shell/topbar-action-menu.js +2 -1
- package/dist/shell/topbar-action-menu.js.map +1 -1
- package/dist/shell/topbar-controls.js +5 -3
- package/dist/shell/topbar-controls.js.map +1 -1
- package/dist/tour/tour.js +1 -0
- package/dist/tour/tour.js.map +1 -1
- package/package.json +1 -1
- package/src/admin/admin-sections.tsx +8 -0
- package/src/auth/verify-email.tsx +34 -9
- package/src/components/alert-banner.tsx +131 -7
- package/src/components/button-look.ts +6 -0
- package/src/components/combobox-core.tsx +7 -1
- package/src/components/data-table-column-settings.tsx +23 -2
- package/src/components/data-table-fit.ts +187 -0
- package/src/components/data-table-groups.tsx +275 -0
- package/src/components/data-table-head.tsx +4 -1
- package/src/components/data-table-labels.ts +13 -0
- package/src/components/data-table-leading-row.tsx +55 -0
- package/src/components/data-table-resize.ts +2 -1
- package/src/components/data-table.tsx +246 -43
- package/src/components/disclosure.tsx +22 -5
- package/src/components/dropdown.tsx +11 -1
- package/src/components/floating-panel.tsx +24 -1
- package/src/components/hover-menu.tsx +6 -1
- package/src/components/icon-button.tsx +3 -3
- package/src/components/popover.tsx +7 -1
- package/src/components/table-skeleton.tsx +133 -0
- package/src/components/tabs.tsx +67 -37
- package/src/components/use-table-state.ts +54 -3
- package/src/data-table.ts +2 -0
- package/src/feedback/feedback-table.tsx +33 -1
- package/src/hooks/use-dismiss.ts +65 -3
- package/src/i18n/german.ts +5 -0
- package/src/i18n/kit-labels.tsx +4 -0
- package/src/i18n/locales/es.ts +5 -0
- package/src/i18n/locales/fr.ts +5 -0
- package/src/i18n/locales/hu.ts +5 -0
- package/src/i18n/locales/it.ts +5 -0
- package/src/i18n/locales/zh.ts +5 -0
- package/src/i18n/shared-labels.ts +2 -0
- package/src/index.ts +3 -0
- package/src/lib/float-clearance.ts +54 -0
- package/src/settings/settings-catalogue.ts +10 -0
- package/src/settings/settings-layout.tsx +18 -4
- package/src/shell/app-shell.tsx +17 -3
- package/src/shell/option-switcher-menu.tsx +16 -4
- package/src/shell/topbar-action-menu.tsx +22 -10
- package/src/shell/topbar-controls.tsx +18 -12
- package/src/tour/tour.tsx +5 -0
|
@@ -0,0 +1,275 @@
|
|
|
1
|
+
import { useEffect, useLayoutEffect, useRef, useState } from "react";
|
|
2
|
+
import type { ReactNode, RefObject } from "react";
|
|
3
|
+
import { cn } from "../lib/cn";
|
|
4
|
+
import { Button } from "./button";
|
|
5
|
+
import { Spinner } from "./spinner";
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* One level of {@link DataTableProps.groupBy} (0.48, keksdose dev #605): the rows are cut
|
|
9
|
+
* into consecutive runs of equal `key` — the rows must already be ordered so equal keys
|
|
10
|
+
* are contiguous, as for `mobileGroupBy` — and each run is headed by a full-width band.
|
|
11
|
+
*
|
|
12
|
+
* The same object drives the phone: `mobileGroupBy` takes a level too, so
|
|
13
|
+
* `groupBy={[period, day]} mobileGroupBy={period}` is one definition for both layouts.
|
|
14
|
+
*/
|
|
15
|
+
export interface DataTableGroupBy<T> {
|
|
16
|
+
/** The group a row belongs to. */
|
|
17
|
+
key: (row: T) => string;
|
|
18
|
+
/** The band's label. Default: the key itself. */
|
|
19
|
+
label?: (key: string, rows: T[]) => ReactNode;
|
|
20
|
+
/**
|
|
21
|
+
* The band's end slot — a count, a total ("176 · +8'782.30 · −3'200.10"). `rows` are
|
|
22
|
+
* the group's rows the table holds; an app whose total covers rows not yet loaded
|
|
23
|
+
* looks it up by `key` instead.
|
|
24
|
+
*/
|
|
25
|
+
end?: (key: string, rows: T[]) => ReactNode;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* {@link DataTableProps.loadMore} (0.48, keksdose dev #605): a footer in place of the
|
|
30
|
+
* pager that loads the next slice of rows — on its own as it scrolls into view (an
|
|
31
|
+
* IntersectionObserver), and through a button that is always there for the keyboard and
|
|
32
|
+
* the screen reader. The owner appends the rows; the table renders every row it is given.
|
|
33
|
+
*/
|
|
34
|
+
export interface DataTableLoadMore {
|
|
35
|
+
/** Whether there is anything left to load. False draws the end state. */
|
|
36
|
+
hasMore: boolean;
|
|
37
|
+
/** Load the next slice. Called at most once per row count while `loading` is false. */
|
|
38
|
+
onLoadMore: () => void;
|
|
39
|
+
/** A load is in flight: the button says "Loading…" and ignores presses. */
|
|
40
|
+
loading?: boolean;
|
|
41
|
+
/** The button's words — what it loads ("Load August 2026"). Default: the kit's "Load more". */
|
|
42
|
+
label?: ReactNode;
|
|
43
|
+
/** What the footer says once `hasMore` is false. Default: the kit's "All rows loaded";
|
|
44
|
+
* `null` draws nothing. */
|
|
45
|
+
endLabel?: ReactNode;
|
|
46
|
+
/** Load as the footer scrolls into view. Default true; false leaves only the button. */
|
|
47
|
+
auto?: boolean;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/** A run of rows under one band; `start` is its first row's index in the slice. */
|
|
51
|
+
export interface RowGroup<T> {
|
|
52
|
+
key: string;
|
|
53
|
+
rows: T[];
|
|
54
|
+
start: number;
|
|
55
|
+
/** The second level's runs inside this one, or null with one level. */
|
|
56
|
+
children: RowGroup<T>[] | null;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/** Consecutive runs by `level.key`, nested once for a second level. */
|
|
60
|
+
export function groupRows<T>(rows: T[], levels: readonly DataTableGroupBy<T>[], offset = 0): RowGroup<T>[] {
|
|
61
|
+
const [level, ...rest] = levels;
|
|
62
|
+
if (!level) return [];
|
|
63
|
+
const out: RowGroup<T>[] = [];
|
|
64
|
+
rows.forEach((row, i) => {
|
|
65
|
+
const key = level.key(row);
|
|
66
|
+
const last = out[out.length - 1];
|
|
67
|
+
if (last && last.key === key) last.rows.push(row);
|
|
68
|
+
else out.push({ key, rows: [row], start: offset + i, children: null });
|
|
69
|
+
});
|
|
70
|
+
if (rest.length > 0) for (const g of out) g.children = groupRows(g.rows, rest.slice(0, 1), g.start);
|
|
71
|
+
return out;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/** `groupBy` as a list of at most two levels. */
|
|
75
|
+
export function groupLevels<T>(
|
|
76
|
+
groupBy: DataTableGroupBy<T> | readonly [DataTableGroupBy<T>, DataTableGroupBy<T>?] | undefined,
|
|
77
|
+
): DataTableGroupBy<T>[] {
|
|
78
|
+
if (!groupBy) return [];
|
|
79
|
+
const list = "key" in groupBy ? [groupBy] : (groupBy as readonly (DataTableGroupBy<T> | undefined)[]);
|
|
80
|
+
return list.filter((l): l is DataTableGroupBy<T> => !!l).slice(0, 2);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* The bands' sticky offsets: the first level under the header (and under a sticky
|
|
85
|
+
* `leadingRow`), the second under the first. Measured — the header grows at Large text,
|
|
86
|
+
* a band's end slot can wrap — and followed with a ResizeObserver.
|
|
87
|
+
*/
|
|
88
|
+
export function useGroupBandTops(
|
|
89
|
+
tableRef: RefObject<HTMLTableElement | null>,
|
|
90
|
+
enabled: boolean,
|
|
91
|
+
signature: string,
|
|
92
|
+
): [number, number] {
|
|
93
|
+
const [tops, setTops] = useState<[number, number]>([0, 0]);
|
|
94
|
+
useLayoutEffect(() => {
|
|
95
|
+
const table = tableRef.current;
|
|
96
|
+
if (!enabled || !table) return;
|
|
97
|
+
const parts = [
|
|
98
|
+
table.tHead,
|
|
99
|
+
table.querySelector<HTMLElement>("[data-leading-row='sticky']"),
|
|
100
|
+
table.querySelector<HTMLElement>("th[data-group-band='1']"),
|
|
101
|
+
];
|
|
102
|
+
const measure = () => {
|
|
103
|
+
const [head, lead, band] = parts.map((el) => (el ? el.getBoundingClientRect().height : 0));
|
|
104
|
+
setTops((prev) => (prev[0] === head + lead && prev[1] === head + lead + band ? prev : [head + lead, head + lead + band]));
|
|
105
|
+
};
|
|
106
|
+
measure();
|
|
107
|
+
if (typeof ResizeObserver === "undefined") return;
|
|
108
|
+
const observer = new ResizeObserver(measure);
|
|
109
|
+
for (const el of parts) if (el) observer.observe(el);
|
|
110
|
+
return () => observer.disconnect();
|
|
111
|
+
}, [tableRef, enabled, signature]);
|
|
112
|
+
return tops;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* A band row. A `<th scope="rowgroup">` spanning the row — the header of the `<tbody>` it
|
|
117
|
+
* opens, which a screen reader announces as the reader enters the group — and sticky on
|
|
118
|
+
* the cell, not the `<tr>`, as the header's and the leading row's are. Opaque, under the
|
|
119
|
+
* sticky leading row's `z-[5]`: the first level `z-[4]` over the second's `z-[3]`, so a
|
|
120
|
+
* period arriving from below covers the day still pinned under the one before. The line
|
|
121
|
+
* over it is the cell's own `::before`, which moves with the stuck cell.
|
|
122
|
+
*/
|
|
123
|
+
export function DataTableGroupBand({
|
|
124
|
+
level,
|
|
125
|
+
colSpan,
|
|
126
|
+
top,
|
|
127
|
+
compact,
|
|
128
|
+
label,
|
|
129
|
+
end,
|
|
130
|
+
}: {
|
|
131
|
+
level: 1 | 2;
|
|
132
|
+
colSpan: number;
|
|
133
|
+
top: number;
|
|
134
|
+
compact: boolean;
|
|
135
|
+
label: ReactNode;
|
|
136
|
+
end: ReactNode;
|
|
137
|
+
}) {
|
|
138
|
+
return (
|
|
139
|
+
<tr data-group-row={level}>
|
|
140
|
+
<th
|
|
141
|
+
scope="rowgroup"
|
|
142
|
+
colSpan={colSpan}
|
|
143
|
+
data-group-band={level}
|
|
144
|
+
style={{ top }}
|
|
145
|
+
className={cn(
|
|
146
|
+
"sticky p-0 text-start align-middle",
|
|
147
|
+
"before:pointer-events-none before:absolute before:inset-x-0 before:top-0 before:z-[1] before:h-px before:bg-[var(--border)] before:content-['']",
|
|
148
|
+
level === 1 ? "z-[4] bg-surface-2 font-semibold text-primary" : "z-[3] bg-surface font-medium text-secondary",
|
|
149
|
+
level === 1 ? (compact ? "text-xs" : "text-sm") : "text-xs",
|
|
150
|
+
)}
|
|
151
|
+
>
|
|
152
|
+
{/* Pinned to what the scroller shows, as the expansion row's panel is: the end
|
|
153
|
+
slot stays at the visible end of a table wider than its scroller. */}
|
|
154
|
+
<div className="w-0 min-w-full">
|
|
155
|
+
<div
|
|
156
|
+
className={cn(
|
|
157
|
+
"sticky start-0 flex w-[var(--dt-visible-w,100%)] max-w-full flex-wrap items-baseline justify-between gap-x-3",
|
|
158
|
+
compact ? "px-2 py-1" : level === 1 ? "px-3 py-2" : "px-3 py-1.5",
|
|
159
|
+
)}
|
|
160
|
+
>
|
|
161
|
+
<span className="min-w-0">{label}</span>
|
|
162
|
+
{end != null && end !== false && <span className="ms-auto tabular-nums">{end}</span>}
|
|
163
|
+
</div>
|
|
164
|
+
</div>
|
|
165
|
+
</th>
|
|
166
|
+
</tr>
|
|
167
|
+
);
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/**
|
|
171
|
+
* The load-more footer. Observed against the viewport (an implicit root sees through the
|
|
172
|
+
* clipping scroller, so it works in a bounded body, under `fillHeight`, and in a page that
|
|
173
|
+
* scrolls), and guarded: one call per row count, none while `loading`, so an observer
|
|
174
|
+
* that reports twice — or a footer still in view while the owner sets `loading` — loads
|
|
175
|
+
* once. When the rows arrive and the footer is still in view (a short slice), it loads
|
|
176
|
+
* again, until the scroller is full.
|
|
177
|
+
*/
|
|
178
|
+
export function DataTableLoadMoreFooter({
|
|
179
|
+
loadMore,
|
|
180
|
+
rowCount,
|
|
181
|
+
labels,
|
|
182
|
+
compact,
|
|
183
|
+
as: Tag = "div",
|
|
184
|
+
}: {
|
|
185
|
+
loadMore: DataTableLoadMore;
|
|
186
|
+
rowCount: number;
|
|
187
|
+
labels: { loadMore: string; allLoaded: string; loading: string };
|
|
188
|
+
compact: boolean;
|
|
189
|
+
as?: "div" | "li";
|
|
190
|
+
}) {
|
|
191
|
+
const { hasMore, onLoadMore, loading = false, auto = true, label, endLabel } = loadMore;
|
|
192
|
+
const rootRef = useRef<HTMLElement | null>(null);
|
|
193
|
+
const endRef = useRef<HTMLParagraphElement | null>(null);
|
|
194
|
+
const firedAt = useRef<number | null>(null);
|
|
195
|
+
const buttonFocused = useRef(false);
|
|
196
|
+
// What the observer's callback acts on: the latest props, not the ones it was made with.
|
|
197
|
+
const latest = useRef({ onLoadMore, loading, rowCount });
|
|
198
|
+
useEffect(() => {
|
|
199
|
+
latest.current = { onLoadMore, loading, rowCount };
|
|
200
|
+
});
|
|
201
|
+
|
|
202
|
+
// A fresh observer whenever the rows or the in-flight flag change: its first report is
|
|
203
|
+
// the footer's position AFTER the new rows are laid out. Acting on a remembered "in
|
|
204
|
+
// view" instead fired a second load the moment the rows arrived, while the footer was
|
|
205
|
+
// still where the old last row had been (headless Chromium caught it).
|
|
206
|
+
useEffect(() => {
|
|
207
|
+
const el = rootRef.current;
|
|
208
|
+
if (!auto || !hasMore || loading || !el || typeof IntersectionObserver === "undefined") return;
|
|
209
|
+
const io = new IntersectionObserver(
|
|
210
|
+
(entries) => {
|
|
211
|
+
const now = latest.current;
|
|
212
|
+
if (!entries.some((e) => e.isIntersecting) || now.loading || firedAt.current === now.rowCount) return;
|
|
213
|
+
firedAt.current = now.rowCount;
|
|
214
|
+
now.onLoadMore();
|
|
215
|
+
},
|
|
216
|
+
{ rootMargin: "200px" },
|
|
217
|
+
);
|
|
218
|
+
io.observe(el);
|
|
219
|
+
return () => io.disconnect();
|
|
220
|
+
}, [auto, hasMore, loading, rowCount]);
|
|
221
|
+
|
|
222
|
+
// A focused button that goes away (nothing left) hands its focus to the end line.
|
|
223
|
+
useEffect(() => {
|
|
224
|
+
if (!hasMore && buttonFocused.current) {
|
|
225
|
+
buttonFocused.current = false;
|
|
226
|
+
endRef.current?.focus();
|
|
227
|
+
}
|
|
228
|
+
}, [hasMore]);
|
|
229
|
+
|
|
230
|
+
const end = endLabel === undefined ? labels.allLoaded : endLabel;
|
|
231
|
+
return (
|
|
232
|
+
<Tag
|
|
233
|
+
ref={(el: HTMLElement | null) => {
|
|
234
|
+
rootRef.current = el;
|
|
235
|
+
}}
|
|
236
|
+
data-table-load-more=""
|
|
237
|
+
className={cn(
|
|
238
|
+
"sticky start-0 flex w-[var(--dt-visible-w,100%)] max-w-full flex-col items-center border-t border-subtle text-center",
|
|
239
|
+
compact ? "px-2 py-2" : "px-3 py-3",
|
|
240
|
+
)}
|
|
241
|
+
>
|
|
242
|
+
{hasMore ? (
|
|
243
|
+
<Button
|
|
244
|
+
variant="secondary"
|
|
245
|
+
size="sm"
|
|
246
|
+
aria-busy={loading || undefined}
|
|
247
|
+
onFocus={() => (buttonFocused.current = true)}
|
|
248
|
+
onBlur={() => (buttonFocused.current = false)}
|
|
249
|
+
onClick={() => {
|
|
250
|
+
if (loading) return;
|
|
251
|
+
firedAt.current = rowCount;
|
|
252
|
+
onLoadMore();
|
|
253
|
+
}}
|
|
254
|
+
>
|
|
255
|
+
{loading ? (
|
|
256
|
+
<span className="inline-flex items-center gap-2">
|
|
257
|
+
<Spinner label={null} className="size-4" />
|
|
258
|
+
<bdi>{labels.loading}</bdi>
|
|
259
|
+
</span>
|
|
260
|
+
) : (
|
|
261
|
+
(label ?? labels.loadMore)
|
|
262
|
+
)}
|
|
263
|
+
</Button>
|
|
264
|
+
) : end != null && end !== false ? (
|
|
265
|
+
<p ref={endRef} tabIndex={-1} className="text-xs text-muted outline-none">
|
|
266
|
+
{end}
|
|
267
|
+
</p>
|
|
268
|
+
) : null}
|
|
269
|
+
{/* Mounted throughout, so a screen reader hears the load start. */}
|
|
270
|
+
<span role="status" className="sr-only">
|
|
271
|
+
{loading ? labels.loading : ""}
|
|
272
|
+
</span>
|
|
273
|
+
</Tag>
|
|
274
|
+
);
|
|
275
|
+
}
|
|
@@ -10,6 +10,7 @@ import { Tooltip } from "./tooltip";
|
|
|
10
10
|
import type { DataTableLabels } from "./data-table-labels";
|
|
11
11
|
import { isTableOwnColumn } from "./data-table-row-actions";
|
|
12
12
|
import { columnLabel, logicalAlign, splitCellProps } from "./data-table-shared";
|
|
13
|
+
import { GROW_HEAD_CLASS } from "./data-table-fit";
|
|
13
14
|
import type { DataTableColumn, DataTableProps, FilterState } from "./data-table";
|
|
14
15
|
|
|
15
16
|
// DataTable's `<thead>` on a wide screen: the select-all box, and per column the sort
|
|
@@ -147,6 +148,7 @@ export function DataTableHead<T>({
|
|
|
147
148
|
// Each <th> carries its own bg so the row doesn't render
|
|
148
149
|
// transparent over the data rows underneath.
|
|
149
150
|
"sticky top-0 z-10 bg-surface-2 backdrop-blur-sm",
|
|
151
|
+
col.grow && GROW_HEAD_CLASS,
|
|
150
152
|
logicalAlign(ownHead.className),
|
|
151
153
|
headClass,
|
|
152
154
|
)}
|
|
@@ -223,7 +225,8 @@ export function DataTableHead<T>({
|
|
|
223
225
|
</Popover>
|
|
224
226
|
)}
|
|
225
227
|
</div>
|
|
226
|
-
{
|
|
228
|
+
{/* A `grow` column takes the rest: no width of its own to drag (0.48, KS N2). */}
|
|
229
|
+
{resizable && !isTableOwnColumn(col.key) && !col.grow && (
|
|
227
230
|
/* eslint-disable-next-line jsx-a11y/no-noninteractive-element-interactions -- a
|
|
228
231
|
pointer-only drag handle for a visual preference (column width); it is
|
|
229
232
|
deliberately not a tab stop, and no content or action depends on it. */
|
|
@@ -32,6 +32,9 @@ export interface DataTableLabels extends DataTableSortLabels {
|
|
|
32
32
|
*/
|
|
33
33
|
rowDetails: string;
|
|
34
34
|
autoSize: string;
|
|
35
|
+
/** Beside a column in the Columns panel that the table's width leaves out
|
|
36
|
+
* (`fitPriority`, 0.48): "hidden for width". */
|
|
37
|
+
hiddenForWidth: string;
|
|
35
38
|
loading: string;
|
|
36
39
|
filters: string;
|
|
37
40
|
clearAll: string;
|
|
@@ -112,6 +115,13 @@ export interface DataTableLabels extends DataTableSortLabels {
|
|
|
112
115
|
booleanTrue: string;
|
|
113
116
|
booleanFalse: string;
|
|
114
117
|
booleanUnset: string;
|
|
118
|
+
|
|
119
|
+
// ---- Load more (0.48, keksdose dev #605) ----
|
|
120
|
+
|
|
121
|
+
/** The `loadMore` footer's button when the app names nothing ("Load August 2026"). */
|
|
122
|
+
loadMore: string;
|
|
123
|
+
/** The `loadMore` footer once nothing is left, unless the app says `endLabel`. */
|
|
124
|
+
allLoaded: string;
|
|
115
125
|
}
|
|
116
126
|
|
|
117
127
|
export const DEFAULT_DATA_TABLE_LABELS: DataTableLabels = {
|
|
@@ -125,6 +135,7 @@ export const DEFAULT_DATA_TABLE_LABELS: DataTableLabels = {
|
|
|
125
135
|
cancelSelection: "Cancel",
|
|
126
136
|
rowDetails: "Details",
|
|
127
137
|
autoSize: "Auto-size columns",
|
|
138
|
+
hiddenForWidth: "hidden for width",
|
|
128
139
|
loading: "Loading…",
|
|
129
140
|
filters: "Filters",
|
|
130
141
|
clearAll: "Clear all",
|
|
@@ -174,6 +185,8 @@ export const DEFAULT_DATA_TABLE_LABELS: DataTableLabels = {
|
|
|
174
185
|
booleanTrue: "Yes",
|
|
175
186
|
booleanFalse: "No",
|
|
176
187
|
booleanUnset: "Not set",
|
|
188
|
+
loadMore: "Load more",
|
|
189
|
+
allLoaded: "All rows loaded",
|
|
177
190
|
// The phone sort control (0.22.0). The same words as `DEFAULT_DATA_TABLE_SORT_LABELS`
|
|
178
191
|
// in data-table.tsx, written out rather than spread in: importing that VALUE here
|
|
179
192
|
// would make data-table.tsx and this module import each other (data-table.tsx reads
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { useLayoutEffect, useRef, useState } from "react";
|
|
2
|
+
import type { ReactNode } from "react";
|
|
3
|
+
import { cn } from "../lib/cn";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* The desktop table's `leadingRow`: one full-width cell above the data rows. With
|
|
7
|
+
* `sticky` (0.48, keksdose dev #603) the cell is pinned under the sticky header while the
|
|
8
|
+
* rows scroll — `top` is the header's height, measured and followed (a header that wraps
|
|
9
|
+
* at Large text grows). On the cell, not the `<tr>`: position:sticky on a table row is not
|
|
10
|
+
* reliable across browsers, the reason the header's sticky sits on each `<th>`. Opaque
|
|
11
|
+
* (`bg-surface`), so the rows pass beneath it; `z-[5]`, under the header's `z-10` and over
|
|
12
|
+
* the rows' own sticky cells. The line over it is the cell's own `::before`, drawn over
|
|
13
|
+
* the content (a full-width disclosure header paints its own fill) and moving with the
|
|
14
|
+
* cell; a collapsed table's row border would stay behind at the row's place.
|
|
15
|
+
*/
|
|
16
|
+
export function DataTableLeadingRow({
|
|
17
|
+
colSpan,
|
|
18
|
+
sticky = false,
|
|
19
|
+
children,
|
|
20
|
+
}: {
|
|
21
|
+
colSpan: number;
|
|
22
|
+
sticky?: boolean;
|
|
23
|
+
children: ReactNode;
|
|
24
|
+
}) {
|
|
25
|
+
const cellRef = useRef<HTMLTableCellElement>(null);
|
|
26
|
+
const [top, setTop] = useState(0);
|
|
27
|
+
useLayoutEffect(() => {
|
|
28
|
+
if (!sticky) return;
|
|
29
|
+
const head = cellRef.current?.closest("table")?.tHead;
|
|
30
|
+
if (!head) return;
|
|
31
|
+
const measure = () => setTop(head.getBoundingClientRect().height);
|
|
32
|
+
measure();
|
|
33
|
+
if (typeof ResizeObserver === "undefined") return;
|
|
34
|
+
const observer = new ResizeObserver(measure);
|
|
35
|
+
observer.observe(head);
|
|
36
|
+
return () => observer.disconnect();
|
|
37
|
+
}, [sticky]);
|
|
38
|
+
return (
|
|
39
|
+
<tr className={cn(!sticky && "border-t border-subtle")}>
|
|
40
|
+
<td
|
|
41
|
+
ref={cellRef}
|
|
42
|
+
colSpan={colSpan}
|
|
43
|
+
data-leading-row={sticky ? "sticky" : ""}
|
|
44
|
+
className={cn(
|
|
45
|
+
"p-0",
|
|
46
|
+
sticky &&
|
|
47
|
+
"sticky z-[5] bg-surface before:pointer-events-none before:absolute before:inset-x-0 before:top-0 before:z-[1] before:h-px before:bg-[var(--border)] before:content-['']",
|
|
48
|
+
)}
|
|
49
|
+
style={sticky ? { top } : undefined}
|
|
50
|
+
>
|
|
51
|
+
{children}
|
|
52
|
+
</td>
|
|
53
|
+
</tr>
|
|
54
|
+
);
|
|
55
|
+
}
|
|
@@ -175,7 +175,8 @@ export function useColumnResize<T>({
|
|
|
175
175
|
const next: Record<string, number> = {};
|
|
176
176
|
const cols = columns.filter((c) => !hiddenCols.has(c.key));
|
|
177
177
|
cols.forEach((col, idx) => {
|
|
178
|
-
|
|
178
|
+
// A `grow` column takes the rest by design: pinning it would undo that (0.48).
|
|
179
|
+
if (idx === cols.length - 1 || col.grow) return;
|
|
179
180
|
const th = headRefs.get(col.key);
|
|
180
181
|
if (th) next[col.key] = Math.ceil(th.getBoundingClientRect().width);
|
|
181
182
|
});
|