@eifi1/ui-kit 0.47.0 → 0.48.1
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 +95 -2
- package/dist/components/data-table.js +360 -267
- 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 +279 -54
- 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
|
@@ -3,6 +3,7 @@ import type {
|
|
|
3
3
|
ComponentPropsWithoutRef,
|
|
4
4
|
ComponentPropsWithRef,
|
|
5
5
|
KeyboardEvent as ReactKeyboardEvent,
|
|
6
|
+
PointerEvent as ReactPointerEvent,
|
|
6
7
|
ReactNode,
|
|
7
8
|
Ref,
|
|
8
9
|
RefObject,
|
|
@@ -10,7 +11,7 @@ import type {
|
|
|
10
11
|
import { createPortal } from "react-dom";
|
|
11
12
|
import { Search } from "lucide-react";
|
|
12
13
|
import { anchoredInline, useAnchoredPanel } from "../hooks/use-anchored-panel";
|
|
13
|
-
import { runOwnKeyDown, takeEscape, useDismissLayer, useOutsideClick } from "../hooks/use-dismiss";
|
|
14
|
+
import { pressInside, runOwnKeyDown, takeEscape, useDismissLayer, useOutsideClick } from "../hooks/use-dismiss";
|
|
14
15
|
import type { DismissLayer } from "../hooks/use-dismiss";
|
|
15
16
|
import { useOverlayHistory } from "../hooks/use-overlay-history";
|
|
16
17
|
import { cn } from "../lib/cn";
|
|
@@ -371,6 +372,13 @@ export function DropdownPanel({
|
|
|
371
372
|
runOwnKeyDown(e, onKeyDown, panelRef && listLayers.get(panelRef));
|
|
372
373
|
takeEscape(e);
|
|
373
374
|
};
|
|
375
|
+
// A press in a panel opened from a row (a popover, its lists) is inside the list for
|
|
376
|
+
// `useDropdown`'s outside press, though portalled elsewhere (dev#623).
|
|
377
|
+
const markInside = pressInside(panelRef);
|
|
378
|
+
const handlePointerDownCapture = (e: ReactPointerEvent<HTMLDivElement>) => {
|
|
379
|
+
rest.onPointerDownCapture?.(e);
|
|
380
|
+
markInside?.(e);
|
|
381
|
+
};
|
|
374
382
|
const body = (
|
|
375
383
|
<>
|
|
376
384
|
{header}
|
|
@@ -399,6 +407,7 @@ export function DropdownPanel({
|
|
|
399
407
|
style={style}
|
|
400
408
|
ref={setPanel}
|
|
401
409
|
onKeyDown={handleKeyDown}
|
|
410
|
+
onPointerDownCapture={handlePointerDownCapture}
|
|
402
411
|
className={cn(
|
|
403
412
|
"absolute z-30 mt-1 rounded-md border border-subtle bg-surface shadow-lg",
|
|
404
413
|
"[&>ul]:max-h-64",
|
|
@@ -425,6 +434,7 @@ export function DropdownPanel({
|
|
|
425
434
|
ref={setPanel}
|
|
426
435
|
dir={dir}
|
|
427
436
|
onKeyDown={handleKeyDown}
|
|
437
|
+
onPointerDownCapture={handlePointerDownCapture}
|
|
428
438
|
// The caller's `style` is kept underneath, but the placement is measured rather
|
|
429
439
|
// than chosen (dev#548: an `overflow` ancestor clipped a third of this panel
|
|
430
440
|
// away), so these values are the panel's own. Flipped above, it hangs from its
|
|
@@ -17,6 +17,7 @@ import { cn } from "../lib/cn";
|
|
|
17
17
|
import { FOCUS_RING } from "./focus-ring";
|
|
18
18
|
import { dirOf, type Direction } from "../lib/direction";
|
|
19
19
|
import { registerFloating } from "../lib/floating-stack";
|
|
20
|
+
import { useFloatClearance } from "../lib/float-clearance";
|
|
20
21
|
import { isComposing } from "../lib/ime";
|
|
21
22
|
import { takeEscape } from "../hooks/use-dismiss";
|
|
22
23
|
import { useKitLabels, useKitLink } from "../i18n/kit-labels";
|
|
@@ -179,6 +180,11 @@ export interface FloatingActionButtonProps extends Omit<ButtonHTMLAttributes<HTM
|
|
|
179
180
|
*
|
|
180
181
|
* While mounted it reports its box (`lib/floating-stack.ts`), so `ServerWakeNotice`
|
|
181
182
|
* stacks above it rather than over it.
|
|
183
|
+
*
|
|
184
|
+
* 0.48: while mounted, `hidden` or not, it also publishes the room it takes above the
|
|
185
|
+
* nav (`offset` + 3rem + a 0.5rem gap) as `--app-float-clearance` on `<html>` (the
|
|
186
|
+
* largest of several wins), and `AppShell` pads its `<main>` past it on a phone, so the
|
|
187
|
+
* page's last row no longer ends under the button (keksdose dev#612).
|
|
182
188
|
*/
|
|
183
189
|
export function FloatingActionButton({
|
|
184
190
|
label,
|
|
@@ -206,6 +212,10 @@ export function FloatingActionButton({
|
|
|
206
212
|
// The fixed element — the button, or the wrapper when there is a tooltip.
|
|
207
213
|
const [fixed, setFixed] = useState<HTMLElement | null>(null);
|
|
208
214
|
useEffect(() => (fixed ? registerFloating(fixed) : undefined), [fixed]);
|
|
215
|
+
// The room it takes above the nav, for AppShell's <main> to pad past (keksdose
|
|
216
|
+
// dev#612). Hidden too: the room is the launcher's while it is mounted, so the page
|
|
217
|
+
// does not change length under the reader as it hides and shows.
|
|
218
|
+
useFloatClearance(offset);
|
|
209
219
|
const buttonRef = useCallback(
|
|
210
220
|
(node: HTMLButtonElement | null) => {
|
|
211
221
|
assignRef(ref, node);
|
|
@@ -332,6 +342,14 @@ export interface FloatingPanelProps {
|
|
|
332
342
|
className?: string;
|
|
333
343
|
/** Extra classes for the scrolling body. */
|
|
334
344
|
bodyClassName?: string;
|
|
345
|
+
/**
|
|
346
|
+
* 0.48: take the FAB and the panel off screen while keeping both mounted — the open
|
|
347
|
+
* state and the bottom room (`--app-float-clearance`) stay, and an open panel is back
|
|
348
|
+
* when this turns `false`. keksdose's assistant launcher steps aside while an editor
|
|
349
|
+
* or a modal covers its corner (dev#612); unmounting it instead would give the room
|
|
350
|
+
* back and shorten the page under the reader's thumb.
|
|
351
|
+
*/
|
|
352
|
+
hidden?: boolean;
|
|
335
353
|
children: ReactNode;
|
|
336
354
|
}
|
|
337
355
|
|
|
@@ -382,6 +400,7 @@ export function FloatingPanel({
|
|
|
382
400
|
initialFocus,
|
|
383
401
|
className,
|
|
384
402
|
bodyClassName,
|
|
403
|
+
hidden = false,
|
|
385
404
|
children,
|
|
386
405
|
}: FloatingPanelProps) {
|
|
387
406
|
const labels = useKitLabels(
|
|
@@ -457,8 +476,9 @@ export function FloatingPanel({
|
|
|
457
476
|
offset={offset}
|
|
458
477
|
nativeTitle={fabNativeTitle}
|
|
459
478
|
tooltip={fabTooltip}
|
|
479
|
+
hidden={hidden || undefined}
|
|
460
480
|
aria-expanded={open}
|
|
461
|
-
aria-controls={open ? panelId : undefined}
|
|
481
|
+
aria-controls={open && !hidden ? panelId : undefined}
|
|
462
482
|
onClick={() => {
|
|
463
483
|
// A press ON the FAB closing the panel: the FAB keeps focus by being clicked,
|
|
464
484
|
// nothing to hand back.
|
|
@@ -467,6 +487,7 @@ export function FloatingPanel({
|
|
|
467
487
|
}}
|
|
468
488
|
/>
|
|
469
489
|
{open &&
|
|
490
|
+
!hidden &&
|
|
470
491
|
typeof document !== "undefined" &&
|
|
471
492
|
createPortal(
|
|
472
493
|
// A container for Escape bubbling up from anything inside it, as a dialog is.
|
|
@@ -572,6 +593,8 @@ export function FloatingActionGroup({
|
|
|
572
593
|
...rest
|
|
573
594
|
}: FloatingActionGroupProps) {
|
|
574
595
|
const [marker, dir] = usePortalDir();
|
|
596
|
+
// As the FAB's (keksdose dev#612): its 3rem members plus the frame's 1px borders.
|
|
597
|
+
useFloatClearance(offset, "2px");
|
|
575
598
|
return (
|
|
576
599
|
<>
|
|
577
600
|
<span ref={marker} hidden />
|
|
@@ -163,7 +163,8 @@ export function HoverMenu({
|
|
|
163
163
|
};
|
|
164
164
|
}, [open, close]);
|
|
165
165
|
|
|
166
|
-
|
|
166
|
+
// A press in a panel opened from a row (a popover, its lists) is inside (dev#623).
|
|
167
|
+
const markInside = useOutsideClick(wrapperRef, close, open);
|
|
167
168
|
|
|
168
169
|
const menuRef = useRef<HTMLDivElement>(null);
|
|
169
170
|
/** Set by a keyboard press on the trigger, so the open that follows moves focus into
|
|
@@ -290,6 +291,10 @@ export function HoverMenu({
|
|
|
290
291
|
className={cn("relative", className)}
|
|
291
292
|
onMouseEnter={handleMouseEnter}
|
|
292
293
|
onMouseLeave={handleMouseLeave}
|
|
294
|
+
onPointerDownCapture={(e) => {
|
|
295
|
+
rest.onPointerDownCapture?.(e);
|
|
296
|
+
markInside(e);
|
|
297
|
+
}}
|
|
293
298
|
onKeyDown={(e) => {
|
|
294
299
|
// The caller's handler sees Escape only while this menu is the layer on top: a
|
|
295
300
|
// tooltip on one of its rows takes that press first (round 0.35 R2, the rule
|
|
@@ -25,6 +25,7 @@ import {
|
|
|
25
25
|
HOVER_INK,
|
|
26
26
|
ICON_BUTTON_PRESSED,
|
|
27
27
|
RenderedButtonLink,
|
|
28
|
+
HIT_AREA_LARGE,
|
|
28
29
|
TOUCH_TARGET_LARGE,
|
|
29
30
|
buttonVariantClasses,
|
|
30
31
|
} from "./button-look";
|
|
@@ -106,7 +107,7 @@ const ICON_BUTTON_TEXT_STRETCH_SIZES: Record<IconButtonSize, string> = {
|
|
|
106
107
|
// (2.75rem is 55 px at Large). `md` … `xs` grow their box to it. `2xs` is the action ON
|
|
107
108
|
// a chip or a tab, whose box cannot grow without growing the chip: it keeps its box and
|
|
108
109
|
// gets a 48 px hit area instead — an invisible `::after` centred on it, which takes the
|
|
109
|
-
// taps a finger lands round the glyph.
|
|
110
|
+
// taps a finger lands round the glyph (`HIT_AREA_LARGE`).
|
|
110
111
|
const ICON_BUTTON_TOUCH_LARGE: Record<IconButtonSize, string> = {
|
|
111
112
|
"2xl": "",
|
|
112
113
|
xl: "",
|
|
@@ -114,8 +115,7 @@ const ICON_BUTTON_TOUCH_LARGE: Record<IconButtonSize, string> = {
|
|
|
114
115
|
md: TOUCH_TARGET_LARGE,
|
|
115
116
|
sm: TOUCH_TARGET_LARGE,
|
|
116
117
|
xs: TOUCH_TARGET_LARGE,
|
|
117
|
-
"2xs":
|
|
118
|
-
"relative large:after:absolute large:after:inset-[calc((100%-48px)/2)] large:after:content-['']",
|
|
118
|
+
"2xs": HIT_AREA_LARGE,
|
|
119
119
|
};
|
|
120
120
|
|
|
121
121
|
// A tone re-colours the glyph without changing what the variant draws around it.
|
|
@@ -157,7 +157,9 @@ export function Popover({
|
|
|
157
157
|
}
|
|
158
158
|
: null;
|
|
159
159
|
|
|
160
|
-
|
|
160
|
+
// A press in a list opened from inside — a CurrencySelect's, a combobox's, another
|
|
161
|
+
// popover's — reaches the panel through the React tree and is inside (dev#623).
|
|
162
|
+
const markInside = useOutsideClick([triggerRef, panelRef], close, open);
|
|
161
163
|
// A layer on the Escape stack while open (Wave 2, DUP-1): a list opened inside closes
|
|
162
164
|
// first, and the dialog this popover sits in only after it — the calculator on an
|
|
163
165
|
// amount field in a Modal is exactly that.
|
|
@@ -216,6 +218,10 @@ export function Popover({
|
|
|
216
218
|
// silently do nothing on a panel whose children are not yet tabbable.
|
|
217
219
|
tabIndex={-1}
|
|
218
220
|
onKeyDown={handleKeyDown}
|
|
221
|
+
onPointerDownCapture={(e) => {
|
|
222
|
+
rest.onPointerDownCapture?.(e);
|
|
223
|
+
markInside(e);
|
|
224
|
+
}}
|
|
219
225
|
style={{
|
|
220
226
|
// Merged over the caller's, never replaced by it: these are measured
|
|
221
227
|
// against the trigger every scroll and resize, and a panel that dropped
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
import type { ComponentPropsWithRef } from "react";
|
|
2
|
+
import { cn } from "../lib/cn";
|
|
3
|
+
import { DEFAULT_COMMON_LABELS, useKitLabels } from "../i18n/kit-labels";
|
|
4
|
+
import { Skeleton } from "./skeleton";
|
|
5
|
+
|
|
6
|
+
export interface TableSkeletonProps extends Omit<ComponentPropsWithRef<"div">, "children"> {
|
|
7
|
+
/** Rows per group (or in all, without `groups`). Default 5. */
|
|
8
|
+
rows?: number;
|
|
9
|
+
/** Columns, the name column included. Default 3. */
|
|
10
|
+
columns?: number;
|
|
11
|
+
/**
|
|
12
|
+
* `list` (default): a name bar of varying width, then the other columns as figures at
|
|
13
|
+
* the row's end — from three columns up they go under the name on a phone, as a card
|
|
14
|
+
* list draws them. `grid`: a label column, then one equal block per column, clipped
|
|
15
|
+
* at the edge — a heatmap or a matrix.
|
|
16
|
+
*/
|
|
17
|
+
variant?: "list" | "grid";
|
|
18
|
+
/** This many group header bars, each over `rows` rows (a budget's groups). Default none. */
|
|
19
|
+
groups?: number;
|
|
20
|
+
/** A row of this many stat-tile outlines above the panel. Default none. */
|
|
21
|
+
tiles?: number;
|
|
22
|
+
/** A title and a hint line at the top of the panel, as a card's head. */
|
|
23
|
+
heading?: boolean;
|
|
24
|
+
/** The panel's border and surface. Default `true`; `false` for a panel the caller frames. */
|
|
25
|
+
framed?: boolean;
|
|
26
|
+
/** What is said, once. Default `common.loading` ("Loading…") from the provider. */
|
|
27
|
+
label?: string;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/** Name widths that differ from row to row, so the outline reads as a list rather than
|
|
31
|
+
* a striped block. */
|
|
32
|
+
const NAME_WIDTHS = ["w-2/5", "w-1/2", "w-1/3", "w-5/12"];
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* The outline of a table or a list while its data loads (0.48, keksdose dev #628) — so a
|
|
36
|
+
* page does not jump from a centred spinner to a long list when the data lands.
|
|
37
|
+
* keksdose hand-built three of these (TabSkeleton, BudgetSkeleton,
|
|
38
|
+
* VarianceHeatmapSkeleton), each a stack of `Skeleton`s with its own sr-only line.
|
|
39
|
+
*
|
|
40
|
+
* ONE `role="status"` region says "Loading…" once; every placeholder inside is
|
|
41
|
+
* `aria-hidden` — not one announcement per row. The pulse stops under reduced motion
|
|
42
|
+
* (`Skeleton`'s own rule); the shapes stay. Props and `ref` go to the region.
|
|
43
|
+
*/
|
|
44
|
+
export function TableSkeleton({
|
|
45
|
+
rows = 5,
|
|
46
|
+
columns = 3,
|
|
47
|
+
variant = "list",
|
|
48
|
+
groups = 0,
|
|
49
|
+
tiles = 0,
|
|
50
|
+
heading = false,
|
|
51
|
+
framed = true,
|
|
52
|
+
label,
|
|
53
|
+
className,
|
|
54
|
+
...rest
|
|
55
|
+
}: TableSkeletonProps) {
|
|
56
|
+
const common = useKitLabels("common", DEFAULT_COMMON_LABELS);
|
|
57
|
+
const figures = Math.max(0, columns - 1);
|
|
58
|
+
const grid = variant === "grid";
|
|
59
|
+
// Three figures don't fit beside a name on a phone; one or two do.
|
|
60
|
+
const stack = !grid && figures >= 2;
|
|
61
|
+
|
|
62
|
+
const row = (key: number) =>
|
|
63
|
+
grid ? (
|
|
64
|
+
<div key={key} className="flex items-center gap-1" data-skeleton-row="">
|
|
65
|
+
<Skeleton className="h-7 w-28 shrink-0" />
|
|
66
|
+
{Array.from({ length: figures }, (_, c) => (
|
|
67
|
+
<Skeleton key={c} className="h-7 w-12 shrink-0" />
|
|
68
|
+
))}
|
|
69
|
+
</div>
|
|
70
|
+
) : (
|
|
71
|
+
<div
|
|
72
|
+
key={key}
|
|
73
|
+
className={cn("flex items-center gap-4 px-4 py-3", stack && "max-sm:flex-col max-sm:items-stretch max-sm:gap-2")}
|
|
74
|
+
data-skeleton-row=""
|
|
75
|
+
>
|
|
76
|
+
<Skeleton className={cn("h-4", NAME_WIDTHS[key % NAME_WIDTHS.length])} />
|
|
77
|
+
{figures > 0 && (
|
|
78
|
+
<div className={cn("ms-auto flex gap-6", stack && "max-sm:ms-0 max-sm:justify-between max-sm:gap-2")}>
|
|
79
|
+
{Array.from({ length: figures }, (_, f) => (
|
|
80
|
+
<Skeleton key={f} className="h-4 w-16 md:w-24" />
|
|
81
|
+
))}
|
|
82
|
+
</div>
|
|
83
|
+
)}
|
|
84
|
+
</div>
|
|
85
|
+
);
|
|
86
|
+
|
|
87
|
+
const block = (offset: number) => (
|
|
88
|
+
<div className={grid ? "space-y-1 overflow-hidden" : "divide-y divide-subtle"}>
|
|
89
|
+
{Array.from({ length: rows }, (_, r) => row(offset + r))}
|
|
90
|
+
</div>
|
|
91
|
+
);
|
|
92
|
+
|
|
93
|
+
return (
|
|
94
|
+
// `relative`: the containing block for the sr-only text (sr-only-containment.test).
|
|
95
|
+
<div {...rest} role="status" className={cn("relative", className)} data-skeleton-preset="table">
|
|
96
|
+
<span className="sr-only">{label ?? common.loading}</span>
|
|
97
|
+
<div aria-hidden="true" className="flex flex-col gap-4">
|
|
98
|
+
{tiles > 0 && (
|
|
99
|
+
<div className="grid grid-cols-[repeat(auto-fit,minmax(9rem,1fr))] gap-3">
|
|
100
|
+
{Array.from({ length: tiles }, (_, i) => (
|
|
101
|
+
<div key={i} className="rounded-lg border border-subtle bg-surface p-4" data-skeleton-tile="">
|
|
102
|
+
<Skeleton className="h-3 w-24 max-w-full" />
|
|
103
|
+
<Skeleton className="mt-3 h-7 w-32 max-w-full" />
|
|
104
|
+
</div>
|
|
105
|
+
))}
|
|
106
|
+
</div>
|
|
107
|
+
)}
|
|
108
|
+
<div
|
|
109
|
+
className={cn("overflow-clip", framed && "rounded-lg border border-subtle bg-surface", grid && "p-3")}
|
|
110
|
+
data-skeleton-panel=""
|
|
111
|
+
>
|
|
112
|
+
{heading && (
|
|
113
|
+
<div className={cn(grid ? "mb-3" : "px-4 pt-4 pb-2")} data-skeleton-heading="">
|
|
114
|
+
<Skeleton className="h-4 w-40 max-w-full" />
|
|
115
|
+
<Skeleton className="mt-2 h-3 w-64 max-w-full" />
|
|
116
|
+
</div>
|
|
117
|
+
)}
|
|
118
|
+
{groups > 0
|
|
119
|
+
? Array.from({ length: groups }, (_, g) => (
|
|
120
|
+
<div key={g} className={cn(g > 0 && "border-t border-subtle")}>
|
|
121
|
+
<div className="flex items-center gap-3 bg-surface-2 px-4 py-2.5" data-skeleton-group="">
|
|
122
|
+
<Skeleton className={cn("h-4", g % 2 ? "w-24" : "w-28")} />
|
|
123
|
+
<Skeleton className="ms-auto h-4 w-12" />
|
|
124
|
+
</div>
|
|
125
|
+
{block(g * rows)}
|
|
126
|
+
</div>
|
|
127
|
+
))
|
|
128
|
+
: block(0)}
|
|
129
|
+
</div>
|
|
130
|
+
</div>
|
|
131
|
+
</div>
|
|
132
|
+
);
|
|
133
|
+
}
|
package/src/components/tabs.tsx
CHANGED
|
@@ -11,6 +11,7 @@ import { useKitLabels, type LabelOverride } from "../i18n/kit-labels";
|
|
|
11
11
|
import { mergeRefs } from "./field-parts";
|
|
12
12
|
import { DEFAULT_TABS_LABELS } from "./ui-labels";
|
|
13
13
|
import { IconButton } from "./icon-button";
|
|
14
|
+
import { HIT_AREA_LARGE } from "./button-look";
|
|
14
15
|
|
|
15
16
|
/** One tab of a {@link Tabs} strip. */
|
|
16
17
|
export interface TabItem<T extends string> {
|
|
@@ -48,16 +49,10 @@ export interface TabsLabels {
|
|
|
48
49
|
add: string;
|
|
49
50
|
/** Accessible name of a tab's × — "Remove <tab>". */
|
|
50
51
|
remove: (tab: string) => string;
|
|
51
|
-
/** The `overflowCue` ›
|
|
52
|
+
/** The `overflowCue` › after the strip's end, while tabs lie past it: "More tabs". */
|
|
52
53
|
more: string;
|
|
53
|
-
/** The `overflowCue` ‹
|
|
54
|
+
/** The `overflowCue` ‹ before the strip's start, while tabs lie before it: "Previous tabs". */
|
|
54
55
|
previous: string;
|
|
55
|
-
/**
|
|
56
|
-
* @deprecated 0.47: not drawn any more — the caret that turned into "back to the
|
|
57
|
-
* first tab" at the strip's end is now a ‹ of its own (`previous`). Accepted and
|
|
58
|
-
* ignored; removed in 0.48.
|
|
59
|
-
*/
|
|
60
|
-
backToFirst?: string;
|
|
61
56
|
}
|
|
62
57
|
|
|
63
58
|
// Defined in ./ui-labels, which holds no component code (0.34, KS U8).
|
|
@@ -84,8 +79,8 @@ export interface TabsProps<T extends string>
|
|
|
84
79
|
onChange: (id: T) => void;
|
|
85
80
|
className?: string;
|
|
86
81
|
/**
|
|
87
|
-
* Carets
|
|
88
|
-
* live #410 round 4): a ›
|
|
82
|
+
* Carets at the ends of a SCROLLING strip (0.40, kk run 79 gap 1; 0.47, keksdose
|
|
83
|
+
* live #410 round 4): a › after the strip's end while tabs lie past it, and a ‹ before
|
|
89
84
|
* its start while tabs lie before it — each there only while something is hidden
|
|
90
85
|
* past ITS edge (1 px slack, so a few cut-off pixels count), each a step of three
|
|
91
86
|
* quarters of the strip's width toward its own side. Neither ever changes direction:
|
|
@@ -94,8 +89,12 @@ export interface TabsProps<T extends string>
|
|
|
94
89
|
*
|
|
95
90
|
* The edge fade alone could read as the end of the row: the last visible tab can
|
|
96
91
|
* finish exactly where the fade begins, so nothing looks cut off (keksdose live #410).
|
|
97
|
-
*
|
|
98
|
-
* it,
|
|
92
|
+
* Since 0.48 (live #410 rounds 5–6) the carets are bare glyphs BESIDE the strip — the ‹
|
|
93
|
+
* before it, the › after it — centred on the labels, muted and primary on hover, with
|
|
94
|
+
* no frame, fill or shadow; until 0.47 they were framed buttons laid over the strip's
|
|
95
|
+
* last visible tabs. While the strip overflows at all it keeps a 24px gutter at both
|
|
96
|
+
* ends, and each caret sits in its own, so scrolling never changes the strip's width
|
|
97
|
+
* or moves its labels; a strip that fits has no gutters. Out of the tab order —
|
|
99
98
|
* the keyboard walks the strip with the arrow keys, which bring the focused tab into
|
|
100
99
|
* view. At every width. Ignored on a vertical strip (a side nav is a column, not a
|
|
101
100
|
* row). Names: `tabs.more` / `tabs.previous`. keksdose's `scroll-edge-cues.tsx` is
|
|
@@ -262,7 +261,7 @@ export function Tabs<T extends string>({
|
|
|
262
261
|
const stripRef = useRef<HTMLDivElement>(null);
|
|
263
262
|
const setStrip = useMemo(() => mergeRefs(stripRef, ref), [ref]);
|
|
264
263
|
const fade = useStripFade(stripRef);
|
|
265
|
-
// See `overflowCue`: a ‹
|
|
264
|
+
// See `overflowCue`: a ‹ before the strip while tabs lie before it, a › after it
|
|
266
265
|
// while tabs lie past it — `fade.overflow` names the cut ends in reading-direction terms.
|
|
267
266
|
const cue = overflowCue && !vertical;
|
|
268
267
|
const cueStart = cue && (fade.overflow === "start" || fade.overflow === "both");
|
|
@@ -276,27 +275,56 @@ export function Tabs<T extends string>({
|
|
|
276
275
|
const toward = (side === "end" ? 1 : -1) * (rtl ? -1 : 1);
|
|
277
276
|
strip.scrollBy({ left: toward * Math.round(strip.clientWidth * 0.75), behavior });
|
|
278
277
|
};
|
|
279
|
-
//
|
|
280
|
-
//
|
|
281
|
-
//
|
|
282
|
-
//
|
|
283
|
-
|
|
278
|
+
// In a GUTTER of its own at the strip's end (0.48, keksdose live #410 rounds 5–6): a
|
|
279
|
+
// bare glyph — no frame, fill or shadow — muted until the pointer comes. Round 5 put
|
|
280
|
+
// each cue in the flow beside the strip, so it took 24px the moment it appeared and the
|
|
281
|
+
// labels jumped sideways at every edge the scroll crossed ("It has problems with moving
|
|
282
|
+
// to left and right especially when moving the scroll bar"). Now the strip box keeps a
|
|
283
|
+
// 24px gutter at BOTH ends for as long as the strip overflows at all (`px-[24px]`, see
|
|
284
|
+
// `stripBox`), and each gutter is absolute in that padding, so a cue coming or going
|
|
285
|
+
// never moves the strip, its labels or the scroll position under the thumb. A strip
|
|
286
|
+
// that fits has no gutters: its width changes only when it starts or stops overflowing.
|
|
287
|
+
// The gutter carries the strip's 1px rule on beneath the glyph, and lifts the glyph
|
|
288
|
+
// onto the LABELS' centre rather than the box's: a tab's 2px underline (`border-b-2`)
|
|
289
|
+
// sits on the rule (`-mb-px`), so the labels are centred 2px above the box's bottom —
|
|
290
|
+
// the gutter's 1px rule (`border-b`) plus 1px more (`pb-px`).
|
|
291
|
+
// 24px wide at every text size (KS N3): at Large `sm`'s 48px minimum made each cue
|
|
292
|
+
// 48px, and at 360px the two left a two-tab strip 100px. 24 PIXELS, not `w-6`: the rem
|
|
293
|
+
// grows with the text size (24 / 30 / 36px), and at Extra large on a 360px phone the two
|
|
294
|
+
// gutters took 72px of a 226px strip and a sheet tab wrapped to seven lines (0.48 build
|
|
295
|
+
// review 2, KS N1). The 48px touch target is an
|
|
296
|
+
// invisible hit area round the glyph instead (`HIT_AREA_LARGE`, as `2xs`), over the
|
|
297
|
+
// strip's faded edge, not room taken from the row. The wrapper inside the gutter is
|
|
298
|
+
// ours because IconButton puts its tooltip's span around the button.
|
|
299
|
+
const gutterAt = (side: "start" | "end", shown: boolean) => (
|
|
284
300
|
<div
|
|
285
|
-
data-tabs-
|
|
286
|
-
className={cn(
|
|
301
|
+
data-tabs-cue-gutter={side}
|
|
302
|
+
className={cn(
|
|
303
|
+
"absolute inset-y-0 flex w-[24px] items-center justify-center border-b border-subtle pb-px",
|
|
304
|
+
side === "start" ? "start-0" : "end-0",
|
|
305
|
+
)}
|
|
287
306
|
>
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
307
|
+
{shown && (
|
|
308
|
+
<div data-tabs-overflow-cue={side} className="flex">
|
|
309
|
+
<IconButton
|
|
310
|
+
size="sm"
|
|
311
|
+
variant="ghost"
|
|
312
|
+
className={cn(
|
|
313
|
+
"w-[24px] large:min-w-0 bg-transparent text-muted hover:bg-transparent hover:text-primary",
|
|
314
|
+
// Over the strip's faded edge, for ‹ too.
|
|
315
|
+
HIT_AREA_LARGE,
|
|
316
|
+
"large:after:z-[1]",
|
|
317
|
+
)}
|
|
318
|
+
tabIndex={-1}
|
|
319
|
+
type="button"
|
|
320
|
+
label={side === "start" ? text.previous : text.more}
|
|
321
|
+
labelVisible={false}
|
|
322
|
+
onClick={() => onCue(side)}
|
|
323
|
+
>
|
|
324
|
+
{side === "start" ? <ChevronLeft className="rtl:-scale-x-100" /> : <ChevronRight className="rtl:-scale-x-100" />}
|
|
325
|
+
</IconButton>
|
|
326
|
+
</div>
|
|
327
|
+
)}
|
|
300
328
|
</div>
|
|
301
329
|
);
|
|
302
330
|
// The OPEN tab is scrolled into the strip whenever it changes — on mount too, for a
|
|
@@ -590,13 +618,15 @@ export function Tabs<T extends string>({
|
|
|
590
618
|
{text.remove(nameOf(activeTab))}
|
|
591
619
|
</button>
|
|
592
620
|
) : null;
|
|
593
|
-
// The
|
|
594
|
-
// There whether or not the strip overflows right now, so a resize
|
|
621
|
+
// The box the cues sit in, ‹ in the gutter before the strip and › in the one after it
|
|
622
|
+
// (see `gutterAt`). There whether or not the strip overflows right now, so a resize
|
|
623
|
+
// never re-parents it; the gutters only while it overflows at all.
|
|
624
|
+
const gutters = cue && fade.overflow !== undefined;
|
|
595
625
|
const stripBox = cue ? (
|
|
596
|
-
<div className={cn("relative flex min-w-0", !onAdd && "flex-1")}>
|
|
626
|
+
<div className={cn("relative flex min-w-0", !onAdd && "flex-1", gutters && "px-[24px]")}>
|
|
597
627
|
{tablist}
|
|
598
|
-
{
|
|
599
|
-
{
|
|
628
|
+
{gutters && gutterAt("start", cueStart)}
|
|
629
|
+
{gutters && gutterAt("end", cueEnd)}
|
|
600
630
|
</div>
|
|
601
631
|
) : (
|
|
602
632
|
tablist
|
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import { useEffect, useMemo, useRef, useState } from "react";
|
|
1
|
+
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
|
2
|
+
import type { SetStateAction } from "react";
|
|
2
3
|
import { useLocation, useSearchParams } from "react-router";
|
|
3
4
|
import { useKitTableStorageKeyPrefix } from "../i18n/kit-labels";
|
|
4
5
|
import { readStored, writeStored } from "../lib/safe-storage";
|
|
@@ -24,6 +25,17 @@ interface PersistedState {
|
|
|
24
25
|
pageSize: number | "all";
|
|
25
26
|
widths?: Record<string, number>;
|
|
26
27
|
hidden?: string[];
|
|
28
|
+
/** 0.48: the `defaultHidden` columns the user ticked back on — an entry that beats the
|
|
29
|
+
* default. Written only when non-empty, so a table without such columns stores what
|
|
30
|
+
* it always did. */
|
|
31
|
+
shown?: string[];
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/** The hidden set as drawn: the stored one plus every `defaultHidden` column the view
|
|
35
|
+
* holds no entry for (neither hidden nor `shown`). */
|
|
36
|
+
function withDefaultHidden(view: { hidden: Set<string>; shown: Set<string> }, defaults: string[]): Set<string> {
|
|
37
|
+
const extra = defaults.filter((k) => !view.hidden.has(k) && !view.shown.has(k));
|
|
38
|
+
return extra.length === 0 ? view.hidden : new Set([...view.hidden, ...extra]);
|
|
27
39
|
}
|
|
28
40
|
|
|
29
41
|
export const DEFAULT_PERSIST_PREFIX = "hbui-table:";
|
|
@@ -338,7 +350,45 @@ export function useTableState<T>({
|
|
|
338
350
|
);
|
|
339
351
|
const pageSize = pageSizeProp ?? internalPageSize;
|
|
340
352
|
const [widths, setWidths] = useState<Record<string, number>>(() => initial.widths ?? {});
|
|
341
|
-
|
|
353
|
+
// Hidden columns (0.48, keksdose #617): what the user chose — `hidden`, and `shown`
|
|
354
|
+
// for a `defaultHidden` column ticked back on — kept apart from the defaults, which
|
|
355
|
+
// are applied on read and never stored. Keyed on the default keys as a string: every
|
|
356
|
+
// call site builds `columns` inline, so the array is new on every render.
|
|
357
|
+
const [view, setView] = useState(() => ({
|
|
358
|
+
hidden: new Set<string>(initial.hidden ?? []),
|
|
359
|
+
shown: new Set<string>(initial.shown ?? []),
|
|
360
|
+
}));
|
|
361
|
+
const defaultHiddenSig = columns
|
|
362
|
+
.filter((c) => c.defaultHidden)
|
|
363
|
+
.map((c) => c.key)
|
|
364
|
+
.join("\u0000");
|
|
365
|
+
const hiddenCols = useMemo(
|
|
366
|
+
() => withDefaultHidden(view, defaultHiddenSig ? defaultHiddenSig.split("\u0000") : []),
|
|
367
|
+
[view, defaultHiddenSig],
|
|
368
|
+
);
|
|
369
|
+
// The panel hands over the next DRAWN set; what changed is recorded as a choice.
|
|
370
|
+
const setHiddenCols = useCallback(
|
|
371
|
+
(action: SetStateAction<Set<string>>) =>
|
|
372
|
+
setView((v) => {
|
|
373
|
+
const defaults = defaultHiddenSig ? defaultHiddenSig.split("\u0000") : [];
|
|
374
|
+
const prev = withDefaultHidden(v, defaults);
|
|
375
|
+
const next = typeof action === "function" ? action(prev) : action;
|
|
376
|
+
const hidden = new Set(v.hidden);
|
|
377
|
+
const shown = new Set(v.shown);
|
|
378
|
+
for (const k of next) {
|
|
379
|
+
if (prev.has(k)) continue;
|
|
380
|
+
hidden.add(k);
|
|
381
|
+
shown.delete(k);
|
|
382
|
+
}
|
|
383
|
+
for (const k of prev) {
|
|
384
|
+
if (next.has(k)) continue;
|
|
385
|
+
hidden.delete(k);
|
|
386
|
+
if (defaults.includes(k)) shown.add(k);
|
|
387
|
+
}
|
|
388
|
+
return { hidden, shown };
|
|
389
|
+
}),
|
|
390
|
+
[defaultHiddenSig],
|
|
391
|
+
);
|
|
342
392
|
const [showSettings, setShowSettings] = useState(false);
|
|
343
393
|
const headRefs = useMemo(() => new Map<string, HTMLTableCellElement>(), []);
|
|
344
394
|
|
|
@@ -360,7 +410,8 @@ export function useTableState<T>({
|
|
|
360
410
|
filters,
|
|
361
411
|
pageSize: pageSize === Infinity ? "all" : pageSize,
|
|
362
412
|
widths,
|
|
363
|
-
hidden: Array.from(
|
|
413
|
+
hidden: Array.from(view.hidden),
|
|
414
|
+
...(view.shown.size > 0 ? { shown: Array.from(view.shown) } : {}),
|
|
364
415
|
});
|
|
365
416
|
useEffect(() => {
|
|
366
417
|
if (!storageKey) return;
|
package/src/data-table.ts
CHANGED
|
@@ -30,6 +30,8 @@ export type {
|
|
|
30
30
|
// 0.46.1 (keksdose N3): a swipe plan as `RowActions`' actions — the desktop column's own
|
|
31
31
|
// derivation, for an app's hand-built actions column.
|
|
32
32
|
export { swipePlanRowActions } from "./components/data-table-row-actions";
|
|
33
|
+
// 0.48 (keksdose dev #605): a `groupBy` level (also `mobileGroupBy`'s) and the `loadMore` footer.
|
|
34
|
+
export type { DataTableGroupBy, DataTableLoadMore } from "./components/data-table-groups";
|
|
33
35
|
export * from "./components/data-table-cells";
|
|
34
36
|
export { useTableUrlState, readTableUrlState } from "./components/use-table-state";
|
|
35
37
|
export type {
|