@eifi1/ui-kit 0.46.1 → 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 +8 -6
- 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 +355 -247
- 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/row-actions.d.ts +25 -0
- package/dist/components/row-actions.js +13 -3
- package/dist/components/row-actions.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 +22 -16
- package/dist/components/tabs.js +47 -31
- package/dist/components/tabs.js.map +1 -1
- package/dist/components/ui-labels.js +1 -1
- package/dist/components/ui-labels.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-attachment-download.d.ts +19 -0
- package/dist/feedback/feedback-attachment-download.js +59 -0
- package/dist/feedback/feedback-attachment-download.js.map +1 -0
- package/dist/feedback/feedback-comment-thread.d.ts +17 -6
- package/dist/feedback/feedback-comment-thread.js +21 -2
- package/dist/feedback/feedback-comment-thread.js.map +1 -1
- package/dist/feedback/feedback-entry-file.d.ts +61 -0
- package/dist/feedback/feedback-entry-file.js +76 -0
- package/dist/feedback/feedback-entry-file.js.map +1 -0
- package/dist/feedback/feedback-public-list.d.ts +18 -5
- package/dist/feedback/feedback-public-list.js +8 -0
- package/dist/feedback/feedback-public-list.js.map +1 -1
- package/dist/feedback/feedback-public-types.d.ts +7 -2
- package/dist/feedback/feedback-replies.d.ts +26 -9
- package/dist/feedback/feedback-replies.js +20 -10
- package/dist/feedback/feedback-replies.js.map +1 -1
- package/dist/feedback/feedback-row-detail-labels.js +0 -1
- package/dist/feedback/feedback-row-detail-labels.js.map +1 -1
- package/dist/feedback/feedback-row-detail.d.ts +23 -10
- package/dist/feedback/feedback-row-detail.js +10 -54
- package/dist/feedback/feedback-row-detail.js.map +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/feedback.d.ts +1 -0
- package/dist/feedback.js +1 -0
- package/dist/feedback.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 +7 -7
- 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 +7 -7
- package/dist/i18n/locales/en.js.map +2 -2
- package/dist/i18n/locales/es.js +7 -7
- package/dist/i18n/locales/es.js.map +1 -1
- package/dist/i18n/locales/fr.js +7 -7
- package/dist/i18n/locales/fr.js.map +1 -1
- package/dist/i18n/locales/hu.js +7 -7
- package/dist/i18n/locales/hu.js.map +1 -1
- package/dist/i18n/locales/it.js +7 -7
- package/dist/i18n/locales/it.js.map +1 -1
- package/dist/i18n/locales/zh.js +7 -7
- package/dist/i18n/locales/zh.js.map +1 -1
- package/dist/i18n/review.js +0 -1
- package/dist/i18n/review.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 +4 -4
- package/dist/index.js +13 -4
- 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/notifications/device-zone-report.d.ts +1 -1
- package/dist/notifications/device-zone-report.js.map +1 -1
- package/dist/notifications/notification-types.d.ts +0 -3
- package/dist/notifications/notifications-labels.d.ts +2 -15
- package/dist/notifications/notifications-labels.js +0 -4
- package/dist/notifications/notifications-labels.js.map +1 -1
- package/dist/notifications/quiet-hours-setting.d.ts +3 -8
- package/dist/notifications/quiet-hours-setting.js +8 -3
- package/dist/notifications/quiet-hours-setting.js.map +1 -1
- 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/theme/theme-store.d.ts +4 -4
- package/dist/theme/theme-store.js +1 -0
- package/dist/theme/theme-store.js.map +1 -1
- package/dist/tour/tour.js +1 -0
- package/dist/tour/tour.js.map +1 -1
- package/package.json +2 -2
- 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 +274 -45
- 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/row-actions.tsx +42 -3
- package/src/components/table-skeleton.tsx +133 -0
- package/src/components/tabs.tsx +102 -50
- package/src/components/ui-labels.ts +1 -1
- package/src/components/use-table-state.ts +54 -3
- package/src/data-table.ts +2 -0
- package/src/feedback/feedback-attachment-download.tsx +77 -0
- package/src/feedback/feedback-comment-thread.tsx +42 -7
- package/src/feedback/feedback-entry-file.tsx +150 -0
- package/src/feedback/feedback-public-list.tsx +25 -4
- package/src/feedback/feedback-public-types.ts +7 -2
- package/src/feedback/feedback-replies.tsx +46 -19
- package/src/feedback/feedback-row-detail-labels.ts +0 -1
- package/src/feedback/feedback-row-detail.tsx +35 -86
- package/src/feedback/feedback-table.tsx +33 -1
- package/src/feedback.ts +1 -0
- package/src/hooks/use-dismiss.ts +65 -3
- package/src/i18n/german.ts +6 -6
- package/src/i18n/kit-labels.tsx +4 -0
- package/src/i18n/locales/es.ts +6 -6
- package/src/i18n/locales/fr.ts +6 -6
- package/src/i18n/locales/hu.ts +6 -6
- package/src/i18n/locales/it.ts +6 -6
- package/src/i18n/locales/zh.ts +6 -6
- package/src/i18n/review.ts +0 -1
- package/src/i18n/shared-labels.ts +2 -0
- package/src/index.ts +13 -3
- package/src/lib/float-clearance.ts +54 -0
- package/src/notifications/device-zone-report.ts +1 -1
- package/src/notifications/notification-types.ts +0 -3
- package/src/notifications/notifications-labels.ts +2 -19
- package/src/notifications/quiet-hours-setting.tsx +16 -11
- 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/theme/theme-store.ts +13 -5
- package/src/tour/tour.tsx +5 -0
- package/dist/notifications/time-zone-select.d.ts +0 -54
- package/dist/notifications/time-zone-select.js +0 -71
- package/dist/notifications/time-zone-select.js.map +0 -1
- package/src/notifications/time-zone-select.tsx +0 -125
|
@@ -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
|
|
@@ -13,6 +13,7 @@ import { useCommitReason, type CommitScope } from "./write-lock";
|
|
|
13
13
|
import type { IconButtonGlyphSize, IconButtonSize } from "./icon-button";
|
|
14
14
|
import type { TooltipSide } from "./tooltip";
|
|
15
15
|
import type { DataTableColumn } from "./data-table";
|
|
16
|
+
import { TableHeaderCell, type TableHeaderCellProps } from "./table";
|
|
16
17
|
import { DEFAULT_ROW_ACTIONS_LABELS } from "./row-actions-labels";
|
|
17
18
|
|
|
18
19
|
/**
|
|
@@ -538,6 +539,43 @@ function RowActionsHeader({
|
|
|
538
539
|
return hidden ? <span className="sr-only">{word}</span> : <>{word}</>;
|
|
539
540
|
}
|
|
540
541
|
|
|
542
|
+
/** The actions cell's classes — `rowActionsColumn`'s `className`: the column hugs its
|
|
543
|
+
* buttons, on one line, at the row's end. A hand-built table's `<TableCell>` takes them
|
|
544
|
+
* around its {@link RowActions} (0.47, keksdose N17). */
|
|
545
|
+
export const ROW_ACTIONS_CELL_CLASS = "w-px whitespace-nowrap text-end";
|
|
546
|
+
/** The actions heading's classes — `rowActionsColumn`'s `headClassName`, which
|
|
547
|
+
* {@link RowActionsHead} draws: hugging, end-aligned over the buttons (0.47, N17). */
|
|
548
|
+
export const ROW_ACTIONS_HEAD_CLASS = "w-px text-end";
|
|
549
|
+
|
|
550
|
+
export interface RowActionsHeadProps extends Omit<TableHeaderCellProps, "children"> {
|
|
551
|
+
/** The heading. Default: the provider's `rowActions.actions` ("Actions"). */
|
|
552
|
+
children?: ReactNode;
|
|
553
|
+
/** Named for a screen reader only — `rowActionsColumn`'s `header: false`. */
|
|
554
|
+
hideLabel?: boolean;
|
|
555
|
+
labels?: LabelOverride<RowActionsLabels>;
|
|
556
|
+
}
|
|
557
|
+
|
|
558
|
+
/**
|
|
559
|
+
* A hand-built table's actions heading (0.47, keksdose N17): the `<th>` `rowActionsColumn`
|
|
560
|
+
* gives a DataTable — "Actions" on screen in the provider's words, end-aligned over the
|
|
561
|
+
* buttons, hugging them. Pair it with `<TableCell className={ROW_ACTIONS_CELL_CLASS}>`
|
|
562
|
+
* around each row's {@link RowActions}. Every other `TableHeaderCell` prop passes on
|
|
563
|
+
* (`size`, the app's `className`, merged last).
|
|
564
|
+
*
|
|
565
|
+
* <RowActionsHead size="sm" className={TH} />
|
|
566
|
+
*/
|
|
567
|
+
export function RowActionsHead({ children, hideLabel, labels, className, ...rest }: RowActionsHeadProps) {
|
|
568
|
+
return (
|
|
569
|
+
<TableHeaderCell align="end" {...rest} className={cn(ROW_ACTIONS_HEAD_CLASS, className)}>
|
|
570
|
+
{typeof children === "string" || children == null ? (
|
|
571
|
+
<RowActionsHeader text={children ?? undefined} labels={labels} hidden={hideLabel} />
|
|
572
|
+
) : (
|
|
573
|
+
children
|
|
574
|
+
)}
|
|
575
|
+
</TableHeaderCell>
|
|
576
|
+
);
|
|
577
|
+
}
|
|
578
|
+
|
|
541
579
|
export interface RowActionsColumnOptions<T> {
|
|
542
580
|
/** The column's key. Default `"actions"`. */
|
|
543
581
|
key?: string;
|
|
@@ -617,9 +655,10 @@ export function rowActionsColumn<T>({
|
|
|
617
655
|
labels={labels}
|
|
618
656
|
/>
|
|
619
657
|
),
|
|
620
|
-
className:
|
|
621
|
-
// The heading over the buttons, at the row's end (0.46.1)
|
|
622
|
-
|
|
658
|
+
className: ROW_ACTIONS_CELL_CLASS,
|
|
659
|
+
// The heading over the buttons, at the row's end (0.46.1); a hand-built table's
|
|
660
|
+
// `RowActionsHead` (0.47).
|
|
661
|
+
headClassName: ROW_ACTIONS_HEAD_CLASS,
|
|
623
662
|
noRowLink: true,
|
|
624
663
|
...column,
|
|
625
664
|
};
|
|
@@ -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,10 +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
|
-
|
|
54
|
+
/** The `overflowCue` ‹ before the strip's start, while tabs lie before it: "Previous tabs". */
|
|
55
|
+
previous: string;
|
|
55
56
|
}
|
|
56
57
|
|
|
57
58
|
// Defined in ./ui-labels, which holds no component code (0.34, KS U8).
|
|
@@ -78,20 +79,26 @@ export interface TabsProps<T extends string>
|
|
|
78
79
|
onChange: (id: T) => void;
|
|
79
80
|
className?: string;
|
|
80
81
|
/**
|
|
81
|
-
*
|
|
82
|
-
*
|
|
83
|
-
*
|
|
84
|
-
*
|
|
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
|
|
84
|
+
* its start while tabs lie before it — each there only while something is hidden
|
|
85
|
+
* past ITS edge (1 px slack, so a few cut-off pixels count), each a step of three
|
|
86
|
+
* quarters of the strip's width toward its own side. Neither ever changes direction:
|
|
87
|
+
* until 0.46 one caret turned round into "back to the first" at the end, and read as
|
|
88
|
+
* an arrow pointing the wrong way. Start and end follow the reading direction.
|
|
85
89
|
*
|
|
86
|
-
* The edge fade alone could read as the end of the row:
|
|
87
|
-
*
|
|
88
|
-
*
|
|
89
|
-
*
|
|
90
|
-
*
|
|
91
|
-
*
|
|
92
|
-
*
|
|
93
|
-
*
|
|
94
|
-
*
|
|
90
|
+
* The edge fade alone could read as the end of the row: the last visible tab can
|
|
91
|
+
* finish exactly where the fade begins, so nothing looks cut off (keksdose live #410).
|
|
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 —
|
|
98
|
+
* the keyboard walks the strip with the arrow keys, which bring the focused tab into
|
|
99
|
+
* view. At every width. Ignored on a vertical strip (a side nav is a column, not a
|
|
100
|
+
* row). Names: `tabs.more` / `tabs.previous`. keksdose's `scroll-edge-cues.tsx` is
|
|
101
|
+
* the reference this replaces.
|
|
95
102
|
*/
|
|
96
103
|
overflowCue?: boolean;
|
|
97
104
|
/**
|
|
@@ -254,36 +261,72 @@ export function Tabs<T extends string>({
|
|
|
254
261
|
const stripRef = useRef<HTMLDivElement>(null);
|
|
255
262
|
const setStrip = useMemo(() => mergeRefs(stripRef, ref), [ref]);
|
|
256
263
|
const fade = useStripFade(stripRef);
|
|
257
|
-
// See `overflowCue
|
|
258
|
-
|
|
259
|
-
const
|
|
260
|
-
const
|
|
264
|
+
// See `overflowCue`: a ‹ before the strip while tabs lie before it, a › after it
|
|
265
|
+
// while tabs lie past it — `fade.overflow` names the cut ends in reading-direction terms.
|
|
266
|
+
const cue = overflowCue && !vertical;
|
|
267
|
+
const cueStart = cue && (fade.overflow === "start" || fade.overflow === "both");
|
|
268
|
+
const cueEnd = cue && (fade.overflow === "end" || fade.overflow === "both");
|
|
269
|
+
// Three quarters of the visible width toward `side`; an RTL strip runs leftward.
|
|
270
|
+
const onCue = (side: "start" | "end") => {
|
|
261
271
|
const strip = stripRef.current;
|
|
262
272
|
if (!strip) return;
|
|
263
273
|
const behavior: ScrollBehavior = prefersReducedMotion() ? "auto" : "smooth";
|
|
264
|
-
if (cueAtEnd) {
|
|
265
|
-
strip.scrollTo({ left: 0, behavior });
|
|
266
|
-
return;
|
|
267
|
-
}
|
|
268
274
|
const rtl = getComputedStyle(strip).direction === "rtl";
|
|
269
|
-
|
|
275
|
+
const toward = (side === "end" ? 1 : -1) * (rtl ? -1 : 1);
|
|
276
|
+
strip.scrollBy({ left: toward * Math.round(strip.clientWidth * 0.75), behavior });
|
|
270
277
|
};
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
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) => (
|
|
300
|
+
<div
|
|
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
|
+
)}
|
|
306
|
+
>
|
|
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
|
+
)}
|
|
285
328
|
</div>
|
|
286
|
-
)
|
|
329
|
+
);
|
|
287
330
|
// The OPEN tab is scrolled into the strip whenever it changes — on mount too, for a
|
|
288
331
|
// deep link to the seventh report. A selection behind the cut edge was the worst
|
|
289
332
|
// case of the overflow above: the one tab the panel below belongs to, sliced to
|
|
@@ -407,7 +450,7 @@ export function Tabs<T extends string>({
|
|
|
407
450
|
// — it is the box a caller's margin or width is meant for.
|
|
408
451
|
className={cn(
|
|
409
452
|
vertical ? TABLIST_VERTICAL_CLASSES : TABLIST_CLASSES,
|
|
410
|
-
|
|
453
|
+
cue ? "min-w-0 flex-1" : onAdd ? "min-w-0" : className,
|
|
411
454
|
)}
|
|
412
455
|
>
|
|
413
456
|
{tabs.map((tab) => {
|
|
@@ -575,16 +618,26 @@ export function Tabs<T extends string>({
|
|
|
575
618
|
{text.remove(nameOf(activeTab))}
|
|
576
619
|
</button>
|
|
577
620
|
) : null;
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
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;
|
|
625
|
+
const stripBox = cue ? (
|
|
626
|
+
<div className={cn("relative flex min-w-0", !onAdd && "flex-1", gutters && "px-[24px]")}>
|
|
627
|
+
{tablist}
|
|
628
|
+
{gutters && gutterAt("start", cueStart)}
|
|
629
|
+
{gutters && gutterAt("end", cueEnd)}
|
|
630
|
+
</div>
|
|
631
|
+
) : (
|
|
632
|
+
tablist
|
|
633
|
+
);
|
|
634
|
+
if (!onAdd && cue) {
|
|
635
|
+
// `className` goes on the row around the strip — it is the box a caller's margin or
|
|
636
|
+
// width is meant for, as with an add button — and stays there at every scroll position.
|
|
583
637
|
return (
|
|
584
638
|
<div className={cn("flex items-end", className)}>
|
|
585
|
-
{
|
|
639
|
+
{stripBox}
|
|
586
640
|
{removeButton}
|
|
587
|
-
{cueButton}
|
|
588
641
|
</div>
|
|
589
642
|
);
|
|
590
643
|
}
|
|
@@ -614,9 +667,8 @@ export function Tabs<T extends string>({
|
|
|
614
667
|
}
|
|
615
668
|
return (
|
|
616
669
|
<div className={cn("flex items-end", className)}>
|
|
617
|
-
{
|
|
670
|
+
{stripBox}
|
|
618
671
|
{removeButton}
|
|
619
|
-
{cueButton}
|
|
620
672
|
{/* Outside the tablist: an action, not a tab — and a tablist may own only
|
|
621
673
|
tabs. The rule under the strip carries on beneath it. */}
|
|
622
674
|
<div
|