@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
|
@@ -7,6 +7,7 @@ import { EMPTY_FORMATTED_VALUE, formatDate, toDate } from "../lib/format";
|
|
|
7
7
|
import { useKitDateFormatter, useKitLabels, useKitLink, useKitLocale, type LabelOverride } from "../i18n/kit-labels";
|
|
8
8
|
import type { KitLinkProps } from "../i18n/kit-labels";
|
|
9
9
|
import { Chip } from "../components/chip";
|
|
10
|
+
import { Tooltip } from "../components/tooltip";
|
|
10
11
|
import { FloatingAction, FloatingActionGroup } from "../components/floating-panel";
|
|
11
12
|
import type { FloatingCorner } from "../components/floating-panel";
|
|
12
13
|
import { pickLinkRenderer } from "../components/text-link";
|
|
@@ -449,6 +450,11 @@ function statusRank(status: FeedbackStatus): number {
|
|
|
449
450
|
* filter that never offered it.
|
|
450
451
|
* - The status column owns the status filter, and only it (keksdose live #330's revert:
|
|
451
452
|
* the phone's filter sheet already exposes every column filter).
|
|
453
|
+
* - Fitted to the table's width (0.48, keksdose live #451): Subject `grow`s and
|
|
454
|
+
* ellipsises, and a narrow table leaves out URL, then Email, then Resolved
|
|
455
|
+
* (`fitPriority` 1–3) — the open report repeats all three — then Votes and Category
|
|
456
|
+
* (4, 5) before Subject would drop below 10rem. Drop the two keys with
|
|
457
|
+
* `{ ...col, grow: undefined, fitPriority: undefined }` for the 0.47 layout.
|
|
452
458
|
* - `/my-feedback` shows the same nine: the Email cell's context fallback exists for it
|
|
453
459
|
* (the admin list's join is absent there, §3.1), and its phone card drops the submitter
|
|
454
460
|
* instead ({@link FeedbackMobileCardProps.showSubmitter}).
|
|
@@ -479,6 +485,9 @@ export function feedbackColumns<T extends FeedbackTableRow>({
|
|
|
479
485
|
sortBy: (row) => row.votes ?? 0,
|
|
480
486
|
filter: { type: "number", getValue: (row) => row.votes ?? 0 },
|
|
481
487
|
mobileHidden: true,
|
|
488
|
+
// Fourth to go (Kurvenschmiede S1, 0.48): with only the first three, Subject was
|
|
489
|
+
// crushed to 24px at 1100px. Still reachable from the Columns panel.
|
|
490
|
+
fitPriority: 4,
|
|
482
491
|
},
|
|
483
492
|
]
|
|
484
493
|
: [];
|
|
@@ -508,6 +517,9 @@ export function feedbackColumns<T extends FeedbackTableRow>({
|
|
|
508
517
|
getValue: (row) => row.category,
|
|
509
518
|
options: FEEDBACK_CATEGORY_ORDER.map((value) => ({ value, label: categoryLabels[value] })),
|
|
510
519
|
},
|
|
520
|
+
// Last to go (Kurvenschmiede S1, 0.48), before Subject would drop below 10rem:
|
|
521
|
+
// the open report's Subject section repeats the badge.
|
|
522
|
+
fitPriority: 5,
|
|
511
523
|
},
|
|
512
524
|
{
|
|
513
525
|
key: "title",
|
|
@@ -521,11 +533,25 @@ export function feedbackColumns<T extends FeedbackTableRow>({
|
|
|
521
533
|
return env ? `${row.title} ${labels.environment(env)}` : row.title;
|
|
522
534
|
},
|
|
523
535
|
mobilePrimary: true,
|
|
536
|
+
// The rest of the row, one line and an ellipsis (0.48, keksdose live #451: one long
|
|
537
|
+
// crash title pushed Status and the row actions off the edge).
|
|
538
|
+
grow: true,
|
|
524
539
|
},
|
|
525
540
|
{
|
|
526
541
|
key: "user",
|
|
527
542
|
header: labels.columnUser,
|
|
528
|
-
|
|
543
|
+
// At most 12rem, one line and an ellipsis, the whole value in the kit's Tooltip (0.48 build
|
|
544
|
+
// review 2, keksdose N2): beside the grow Subject every other cell keeps one line, so
|
|
545
|
+
// an email standing in for a missing name widened the column and cost Votes and
|
|
546
|
+
// Category their room first. The open report shows both in full.
|
|
547
|
+
cell: (row) => {
|
|
548
|
+
const who = feedbackSubmitter(row, labels);
|
|
549
|
+
return (
|
|
550
|
+
<Tooltip label={who} portal lazy>
|
|
551
|
+
<span className="block max-w-[12rem] truncate-until-large text-secondary">{who}</span>
|
|
552
|
+
</Tooltip>
|
|
553
|
+
);
|
|
554
|
+
},
|
|
529
555
|
sortBy: (row) => feedbackSubmitter(row, labels),
|
|
530
556
|
// Name AND email, so either finds the person; the erased ones by the placeholder
|
|
531
557
|
// (their context holds no name any more).
|
|
@@ -552,6 +578,8 @@ export function feedbackColumns<T extends FeedbackTableRow>({
|
|
|
552
578
|
sortBy: feedbackSubmitterEmail,
|
|
553
579
|
filterBy: feedbackSubmitterEmail,
|
|
554
580
|
mobileHidden: true,
|
|
581
|
+
// Second to go when the table is narrow: the open report shows the submitter.
|
|
582
|
+
fitPriority: 2,
|
|
555
583
|
},
|
|
556
584
|
{
|
|
557
585
|
key: "url",
|
|
@@ -566,6 +594,8 @@ export function feedbackColumns<T extends FeedbackTableRow>({
|
|
|
566
594
|
sortBy: (row) => feedbackPagePath(row.context) ?? "",
|
|
567
595
|
filterBy: (row) => feedbackPagePath(row.context) ?? "",
|
|
568
596
|
mobileHidden: true,
|
|
597
|
+
// First to go when the table is narrow (0.48): the open report repeats the page.
|
|
598
|
+
fitPriority: 1,
|
|
569
599
|
},
|
|
570
600
|
...votesColumn,
|
|
571
601
|
{
|
|
@@ -600,6 +630,8 @@ export function feedbackColumns<T extends FeedbackTableRow>({
|
|
|
600
630
|
sortBy: (row) => instant(row.resolved_at),
|
|
601
631
|
filter: { type: "date", getValue: (row) => row.resolved_at },
|
|
602
632
|
mobileHidden: true,
|
|
633
|
+
// Third to go (0.48).
|
|
634
|
+
fitPriority: 3,
|
|
603
635
|
},
|
|
604
636
|
];
|
|
605
637
|
}
|
package/src/hooks/use-dismiss.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { useEffect, useRef } from "react";
|
|
1
|
+
import { useCallback, useEffect, useRef } from "react";
|
|
2
2
|
import type { KeyboardEvent as ReactKeyboardEvent, RefObject } from "react";
|
|
3
3
|
import { isComposing } from "../lib/ime";
|
|
4
4
|
|
|
@@ -220,6 +220,29 @@ export function useEscapeKey(handler: () => void, enabled = true): void {
|
|
|
220
220
|
useDismissLayer(enabled, handler);
|
|
221
221
|
}
|
|
222
222
|
|
|
223
|
+
/** Called with each native press an owner's React tree saw — see {@link useOutsideClick}. */
|
|
224
|
+
type PressMark = (press: Event) => void;
|
|
225
|
+
|
|
226
|
+
/** The outside-click hooks watching each ref, for {@link pressInside}. */
|
|
227
|
+
const watchers = new WeakMap<RefObject<HTMLElement | null>, Set<PressMark>>();
|
|
228
|
+
|
|
229
|
+
/** What an `onPointerDownCapture` hands over: a React event, its press inside. */
|
|
230
|
+
type ReactPress = { nativeEvent: Event };
|
|
231
|
+
|
|
232
|
+
/**
|
|
233
|
+
* An `onPointerDownCapture` for a panel whose `ref` an outside-click hook watches but
|
|
234
|
+
* whose element the hook's owner does not render — {@link DropdownPanel} and
|
|
235
|
+
* `ComboboxPanel` take `panelRef` from `useDropdown` / `useComboboxCore`. Every press
|
|
236
|
+
* that reaches the panel through the React tree, a portalled list opened inside it
|
|
237
|
+
* included, is marked inside for each hook watching `ref`. See {@link useOutsideClick}.
|
|
238
|
+
*/
|
|
239
|
+
export function pressInside(ref: RefObject<HTMLElement | null> | undefined): ((e: ReactPress) => void) | undefined {
|
|
240
|
+
if (!ref) return undefined;
|
|
241
|
+
return (e) => {
|
|
242
|
+
watchers.get(ref)?.forEach((mark) => mark(e.nativeEvent));
|
|
243
|
+
};
|
|
244
|
+
}
|
|
245
|
+
|
|
223
246
|
/**
|
|
224
247
|
* Calls `handler` on a press that lands outside every supplied element, while
|
|
225
248
|
* `enabled` — the outside-click-to-close half of the same dismissal story. Pass one
|
|
@@ -227,6 +250,23 @@ export function useEscapeKey(handler: () => void, enabled = true): void {
|
|
|
227
250
|
* `handler` are read live, so an inline `[triggerRef, panelRef]` array is fine and
|
|
228
251
|
* won't churn the listener.
|
|
229
252
|
*
|
|
253
|
+
* ## A panel opened from inside is inside (0.48, keksdose dev#623)
|
|
254
|
+
*
|
|
255
|
+
* A CurrencySelect in a Popover: the currency list is portalled to `<body>`, so in the
|
|
256
|
+
* DOM it is outside the popover's panel, and the first press on a currency closed the
|
|
257
|
+
* POPOVER at pointerdown — the list went with it and the choice never landed. The same
|
|
258
|
+
* for a combobox, a menu, a date picker's popover, a dropdown, at any depth.
|
|
259
|
+
*
|
|
260
|
+
* In the React tree the portal is still where it was opened from, and React passes a
|
|
261
|
+
* press up that tree, through portals. So the hook hands back an
|
|
262
|
+
* `onPointerDownCapture`: put it on the element the press should count as inside of —
|
|
263
|
+
* the kit's Popover panel, HoverMenu, DropdownPanel and ComboboxPanel do — and every
|
|
264
|
+
* press that travels through that element in React, portalled panels opened inside it
|
|
265
|
+
* included, is not outside. React's listeners sit on the root and on each portal's
|
|
266
|
+
* container, so they hear the press before this document listener does. No owner chain
|
|
267
|
+
* to keep: depth is free, and an app's own portal inside a kit panel is covered too. A
|
|
268
|
+
* panel the hook's owner does not render takes {@link pressInside} with the watched ref.
|
|
269
|
+
*
|
|
230
270
|
* **`pointerdown`, not `mousedown`** — one event covering mouse, touch and pen.
|
|
231
271
|
* This listened for `mousedown` alone, which on a touch platform is a SYNTHESISED
|
|
232
272
|
* event: the browser emits it for most taps, which is why nothing obviously broke,
|
|
@@ -245,17 +285,35 @@ export function useOutsideClick(
|
|
|
245
285
|
refs: RefObject<HTMLElement | null> | RefObject<HTMLElement | null>[],
|
|
246
286
|
handler: () => void,
|
|
247
287
|
enabled = true,
|
|
248
|
-
): void {
|
|
288
|
+
): (e: ReactPress) => void {
|
|
249
289
|
const refsRef = useRef(refs);
|
|
250
290
|
const handlerRef = useRef(handler);
|
|
291
|
+
// The last press this owner's React tree saw: the one the document listener is about
|
|
292
|
+
// to hear, when it went through a panel the owner marked.
|
|
293
|
+
const insideRef = useRef<Event | null>(null);
|
|
251
294
|
// In an effect rather than during render — see the note in useDismissLayer.
|
|
252
295
|
useEffect(() => {
|
|
253
296
|
refsRef.current = refs;
|
|
254
297
|
handlerRef.current = handler;
|
|
255
298
|
});
|
|
299
|
+
const markInside = useCallback((e: ReactPress) => {
|
|
300
|
+
insideRef.current = e.nativeEvent;
|
|
301
|
+
}, []);
|
|
256
302
|
useEffect(() => {
|
|
257
303
|
if (!enabled) return;
|
|
304
|
+
const mark: PressMark = (press) => {
|
|
305
|
+
insideRef.current = press;
|
|
306
|
+
};
|
|
307
|
+
const watched = Array.isArray(refsRef.current) ? refsRef.current : [refsRef.current];
|
|
308
|
+
for (const r of watched) {
|
|
309
|
+
const set = watchers.get(r) ?? new Set<PressMark>();
|
|
310
|
+
set.add(mark);
|
|
311
|
+
watchers.set(r, set);
|
|
312
|
+
}
|
|
258
313
|
const onPointerDown = (e: Event) => {
|
|
314
|
+
const inside = insideRef.current === e;
|
|
315
|
+
insideRef.current = null;
|
|
316
|
+
if (inside) return;
|
|
259
317
|
const target = e.target as Node;
|
|
260
318
|
const list = Array.isArray(refsRef.current) ? refsRef.current : [refsRef.current];
|
|
261
319
|
for (const r of list) {
|
|
@@ -264,6 +322,10 @@ export function useOutsideClick(
|
|
|
264
322
|
handlerRef.current();
|
|
265
323
|
};
|
|
266
324
|
document.addEventListener("pointerdown", onPointerDown);
|
|
267
|
-
return () =>
|
|
325
|
+
return () => {
|
|
326
|
+
document.removeEventListener("pointerdown", onPointerDown);
|
|
327
|
+
for (const r of watched) watchers.get(r)?.delete(mark);
|
|
328
|
+
};
|
|
268
329
|
}, [enabled]);
|
|
330
|
+
return markInside;
|
|
269
331
|
}
|
package/src/i18n/german.ts
CHANGED
|
@@ -339,6 +339,8 @@ export function germanLabels(numberLocale: string): UiKitLabels {
|
|
|
339
339
|
opensInNewTab: "öffnet in einem neuen Tab",
|
|
340
340
|
aboutField: (field) => `Hilfe zu „${field}“`,
|
|
341
341
|
help: "Hilfe",
|
|
342
|
+
showMore: "Mehr",
|
|
343
|
+
showLess: "Weniger",
|
|
342
344
|
},
|
|
343
345
|
dataTable: {
|
|
344
346
|
columns: "Spalten",
|
|
@@ -351,6 +353,7 @@ export function germanLabels(numberLocale: string): UiKitLabels {
|
|
|
351
353
|
cancelSelection: "Abbrechen",
|
|
352
354
|
rowDetails: "Details",
|
|
353
355
|
autoSize: "Spaltenbreiten anpassen",
|
|
356
|
+
hiddenForWidth: "aus Platzgründen ausgeblendet",
|
|
354
357
|
loading: "Wird geladen…",
|
|
355
358
|
filters: "Filter",
|
|
356
359
|
// Resets every active filter; "Alle löschen" would read as deleting the filters.
|
|
@@ -402,6 +405,8 @@ export function germanLabels(numberLocale: string): UiKitLabels {
|
|
|
402
405
|
booleanTrue: "Ja",
|
|
403
406
|
booleanFalse: "Nein",
|
|
404
407
|
booleanUnset: "Nicht festgelegt",
|
|
408
|
+
loadMore: "Mehr laden",
|
|
409
|
+
allLoaded: "Alle Zeilen geladen",
|
|
405
410
|
sortBy: "Sortieren nach",
|
|
406
411
|
sortDefault: "Standardreihenfolge",
|
|
407
412
|
sortAscending: "Aufsteigend",
|
package/src/i18n/kit-labels.tsx
CHANGED
|
@@ -189,6 +189,10 @@ export interface CommonLabels {
|
|
|
189
189
|
aboutField: (field: string) => string;
|
|
190
190
|
/** …and of a `help` "?" on a field with no label and no `aria-label` to name it by. */
|
|
191
191
|
help: string;
|
|
192
|
+
/** 0.48: the toggle that shows the rest of a clamped text (`AlertBanner clampMobile`)… */
|
|
193
|
+
showMore: string;
|
|
194
|
+
/** …and folds it back to its one line. */
|
|
195
|
+
showLess: string;
|
|
192
196
|
}
|
|
193
197
|
|
|
194
198
|
/** `DatePicker` / `DateRangePicker` chrome. The calendar inside has its own
|
package/src/i18n/locales/es.ts
CHANGED
|
@@ -330,6 +330,8 @@ export function uiKitLabelsEs(numberLocale = "es-ES"): UiKitLabels {
|
|
|
330
330
|
opensInNewTab: "se abre en una pestaña nueva",
|
|
331
331
|
aboutField: (field) => `Acerca de «${field}»`,
|
|
332
332
|
help: "Ayuda",
|
|
333
|
+
showMore: "Más",
|
|
334
|
+
showLess: "Menos",
|
|
333
335
|
},
|
|
334
336
|
dataTable: {
|
|
335
337
|
columns: "Columnas",
|
|
@@ -342,6 +344,7 @@ export function uiKitLabelsEs(numberLocale = "es-ES"): UiKitLabels {
|
|
|
342
344
|
cancelSelection: "Cancelar",
|
|
343
345
|
rowDetails: "Detalles",
|
|
344
346
|
autoSize: "Ajustar ancho de columnas",
|
|
347
|
+
hiddenForWidth: "oculta por falta de espacio",
|
|
345
348
|
loading: "Cargando…",
|
|
346
349
|
filters: "Filtros",
|
|
347
350
|
clearAll: "Borrar todo",
|
|
@@ -391,6 +394,8 @@ export function uiKitLabelsEs(numberLocale = "es-ES"): UiKitLabels {
|
|
|
391
394
|
booleanTrue: "Sí",
|
|
392
395
|
booleanFalse: "No",
|
|
393
396
|
booleanUnset: "Sin definir",
|
|
397
|
+
loadMore: "Cargar más",
|
|
398
|
+
allLoaded: "Todas las filas cargadas",
|
|
394
399
|
sortBy: "Ordenar por",
|
|
395
400
|
sortDefault: "Orden predeterminado",
|
|
396
401
|
sortAscending: "Ascendente",
|
package/src/i18n/locales/fr.ts
CHANGED
|
@@ -336,6 +336,8 @@ export function uiKitLabelsFr(numberLocale = "fr-FR"): UiKitLabels {
|
|
|
336
336
|
opensInNewTab: "s’ouvre dans un nouvel onglet",
|
|
337
337
|
aboutField: (field) => `À propos de «\u202f${field}\u202f»`,
|
|
338
338
|
help: "Aide",
|
|
339
|
+
showMore: "Plus",
|
|
340
|
+
showLess: "Moins",
|
|
339
341
|
},
|
|
340
342
|
dataTable: {
|
|
341
343
|
columns: "Colonnes",
|
|
@@ -348,6 +350,7 @@ export function uiKitLabelsFr(numberLocale = "fr-FR"): UiKitLabels {
|
|
|
348
350
|
cancelSelection: "Annuler",
|
|
349
351
|
rowDetails: "Détails",
|
|
350
352
|
autoSize: "Ajuster la largeur des colonnes",
|
|
353
|
+
hiddenForWidth: "masquée faute de place",
|
|
351
354
|
loading: "Chargement…",
|
|
352
355
|
filters: "Filtres",
|
|
353
356
|
clearAll: "Tout effacer",
|
|
@@ -397,6 +400,8 @@ export function uiKitLabelsFr(numberLocale = "fr-FR"): UiKitLabels {
|
|
|
397
400
|
booleanTrue: "Oui",
|
|
398
401
|
booleanFalse: "Non",
|
|
399
402
|
booleanUnset: "Non défini",
|
|
403
|
+
loadMore: "Charger plus",
|
|
404
|
+
allLoaded: "Toutes les lignes sont chargées",
|
|
400
405
|
sortBy: "Trier par",
|
|
401
406
|
sortDefault: "Ordre par défaut",
|
|
402
407
|
sortAscending: "Croissant",
|
package/src/i18n/locales/hu.ts
CHANGED
|
@@ -328,6 +328,8 @@ export function uiKitLabelsHu(numberLocale = "hu-HU"): UiKitLabels {
|
|
|
328
328
|
opensInNewTab: "új lapon nyílik meg",
|
|
329
329
|
aboutField: (field) => `Súgó: ${field}`,
|
|
330
330
|
help: "Súgó",
|
|
331
|
+
showMore: "Több",
|
|
332
|
+
showLess: "Kevesebb",
|
|
331
333
|
},
|
|
332
334
|
dataTable: {
|
|
333
335
|
columns: "Oszlopok",
|
|
@@ -340,6 +342,7 @@ export function uiKitLabelsHu(numberLocale = "hu-HU"): UiKitLabels {
|
|
|
340
342
|
cancelSelection: "Mégse",
|
|
341
343
|
rowDetails: "Részletek",
|
|
342
344
|
autoSize: "Oszlopszélességek igazítása",
|
|
345
|
+
hiddenForWidth: "szélesség miatt rejtve",
|
|
343
346
|
loading: "Betöltés…",
|
|
344
347
|
filters: "Szűrők",
|
|
345
348
|
clearAll: "Összes törlése",
|
|
@@ -389,6 +392,8 @@ export function uiKitLabelsHu(numberLocale = "hu-HU"): UiKitLabels {
|
|
|
389
392
|
booleanTrue: "Igen",
|
|
390
393
|
booleanFalse: "Nem",
|
|
391
394
|
booleanUnset: "Nincs megadva",
|
|
395
|
+
loadMore: "Továbbiak betöltése",
|
|
396
|
+
allLoaded: "Minden sor betöltve",
|
|
392
397
|
sortBy: "Rendezés alapja",
|
|
393
398
|
sortDefault: "Alapértelmezett sorrend",
|
|
394
399
|
sortAscending: "Növekvő",
|
package/src/i18n/locales/it.ts
CHANGED
|
@@ -332,6 +332,8 @@ export function uiKitLabelsIt(numberLocale = "it-IT"): UiKitLabels {
|
|
|
332
332
|
opensInNewTab: "si apre in una nuova scheda",
|
|
333
333
|
aboutField: (field) => `Informazioni su «${field}»`,
|
|
334
334
|
help: "Aiuto",
|
|
335
|
+
showMore: "Altro",
|
|
336
|
+
showLess: "Meno",
|
|
335
337
|
},
|
|
336
338
|
dataTable: {
|
|
337
339
|
columns: "Colonne",
|
|
@@ -344,6 +346,7 @@ export function uiKitLabelsIt(numberLocale = "it-IT"): UiKitLabels {
|
|
|
344
346
|
cancelSelection: "Annulla",
|
|
345
347
|
rowDetails: "Dettagli",
|
|
346
348
|
autoSize: "Adatta larghezza colonne",
|
|
349
|
+
hiddenForWidth: "nascosta per mancanza di spazio",
|
|
347
350
|
loading: "Caricamento…",
|
|
348
351
|
filters: "Filtri",
|
|
349
352
|
clearAll: "Cancella tutto",
|
|
@@ -393,6 +396,8 @@ export function uiKitLabelsIt(numberLocale = "it-IT"): UiKitLabels {
|
|
|
393
396
|
booleanTrue: "Sì",
|
|
394
397
|
booleanFalse: "No",
|
|
395
398
|
booleanUnset: "Non impostato",
|
|
399
|
+
loadMore: "Carica altro",
|
|
400
|
+
allLoaded: "Tutte le righe caricate",
|
|
396
401
|
sortBy: "Ordina per",
|
|
397
402
|
sortDefault: "Ordine predefinito",
|
|
398
403
|
sortAscending: "Crescente",
|
package/src/i18n/locales/zh.ts
CHANGED
|
@@ -320,6 +320,8 @@ export function uiKitLabelsZh(numberLocale = "zh-CN"): UiKitLabels {
|
|
|
320
320
|
opensInNewTab: "在新标签页中打开",
|
|
321
321
|
aboutField: (field) => `关于“${field}”`,
|
|
322
322
|
help: "帮助",
|
|
323
|
+
showMore: "更多",
|
|
324
|
+
showLess: "收起",
|
|
323
325
|
},
|
|
324
326
|
dataTable: {
|
|
325
327
|
columns: "列",
|
|
@@ -332,6 +334,7 @@ export function uiKitLabelsZh(numberLocale = "zh-CN"): UiKitLabels {
|
|
|
332
334
|
cancelSelection: "取消",
|
|
333
335
|
rowDetails: "详情",
|
|
334
336
|
autoSize: "自动调整列宽",
|
|
337
|
+
hiddenForWidth: "宽度不足,已隐藏",
|
|
335
338
|
loading: "加载中…",
|
|
336
339
|
filters: "筛选条件",
|
|
337
340
|
clearAll: "全部清除",
|
|
@@ -380,6 +383,8 @@ export function uiKitLabelsZh(numberLocale = "zh-CN"): UiKitLabels {
|
|
|
380
383
|
booleanTrue: "是",
|
|
381
384
|
booleanFalse: "否",
|
|
382
385
|
booleanUnset: "未设置",
|
|
386
|
+
loadMore: "加载更多",
|
|
387
|
+
allLoaded: "已加载全部行",
|
|
383
388
|
sortBy: "排序依据",
|
|
384
389
|
sortDefault: "默认顺序",
|
|
385
390
|
sortAscending: "升序",
|
|
@@ -39,6 +39,8 @@ export const DEFAULT_COMMON_LABELS: CommonLabels = {
|
|
|
39
39
|
opensInNewTab: "opens in a new tab",
|
|
40
40
|
aboutField: (field) => `About “${field}”`,
|
|
41
41
|
help: "Help",
|
|
42
|
+
showMore: "More",
|
|
43
|
+
showLess: "Less",
|
|
42
44
|
};
|
|
43
45
|
|
|
44
46
|
export const DEFAULT_DATE_PICKER_LABELS: DatePickerLabels = {
|
package/src/index.ts
CHANGED
|
@@ -430,6 +430,9 @@ export * from "./components/progress-bar";
|
|
|
430
430
|
// Named: skeleton.tsx also holds SKELETON_CLASS, the look StatTile shares — internal.
|
|
431
431
|
export { Skeleton } from "./components/skeleton";
|
|
432
432
|
export type { SkeletonProps, SkeletonShape } from "./components/skeleton";
|
|
433
|
+
// 0.48 (keksdose dev #628): the table/list loading preset, one status label.
|
|
434
|
+
export { TableSkeleton } from "./components/table-skeleton";
|
|
435
|
+
export type { TableSkeletonProps } from "./components/table-skeleton";
|
|
433
436
|
// 0.12.0: a centred spinner with words, signed figures and deltas, a render-error fallback.
|
|
434
437
|
export * from "./components/loading-state";
|
|
435
438
|
export * from "./components/signed-amount";
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import { useEffect } from "react";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* The room the floating launchers take above the bottom nav, published as
|
|
5
|
+
* `--app-float-clearance` on `<html>` so `AppShell` can pad `<main>` past them.
|
|
6
|
+
*
|
|
7
|
+
* keksdose dev#612 (UX-1.3, "the help button covers content"): the shell padded `<main>`
|
|
8
|
+
* for the nav plus 1.5rem, so on a phone the last row of every page ended under the help
|
|
9
|
+
* launcher — and on /transactions under the action group below it, exactly where the
|
|
10
|
+
* row's amount is. keksdose patched it with its own `--help-launcher-clearance` and an
|
|
11
|
+
* unlayered app.css rule. Here every kit launcher (`FloatingActionButton`, so
|
|
12
|
+
* `FloatingPanel`'s too, and `FloatingActionGroup`) declares its own room while it is
|
|
13
|
+
* mounted and shown, and the shell takes the largest.
|
|
14
|
+
*
|
|
15
|
+
* Declared, not measured: a launcher knows its `offset` and its 3rem height exactly, as
|
|
16
|
+
* CSS lengths, and the variable then follows the nav and the text size by itself. A
|
|
17
|
+
* `hidden` launcher keeps its claim: the room is its own while it is mounted, so the page
|
|
18
|
+
* does not change length as it hides and shows (keksdose's launcher steps aside while an
|
|
19
|
+
* editor covers its corner). Unmount it to give the room back.
|
|
20
|
+
*
|
|
21
|
+
* Internal: not exported from the barrel.
|
|
22
|
+
*/
|
|
23
|
+
|
|
24
|
+
const claims = new Map<symbol, string>();
|
|
25
|
+
|
|
26
|
+
function publish() {
|
|
27
|
+
if (typeof document === "undefined") return;
|
|
28
|
+
const root = document.documentElement;
|
|
29
|
+
const values = [...claims.values()];
|
|
30
|
+
if (values.length === 0) root.style.removeProperty("--app-float-clearance");
|
|
31
|
+
else root.style.setProperty("--app-float-clearance", values.length === 1 ? values[0] : `max(${values.join(", ")})`);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/** Claim `value` (a CSS length: the launcher's top edge above the nav, plus a gap) until
|
|
35
|
+
* the returned function is called. */
|
|
36
|
+
export function claimFloatClearance(value: string): () => void {
|
|
37
|
+
const key = Symbol("float-clearance");
|
|
38
|
+
claims.set(key, value);
|
|
39
|
+
publish();
|
|
40
|
+
return () => {
|
|
41
|
+
claims.delete(key);
|
|
42
|
+
publish();
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* A launcher at `offset` above the nav claims its 3rem height plus a 0.5rem gap while
|
|
48
|
+
* mounted (`extra`: what its frame adds, the group's 1px borders).
|
|
49
|
+
*/
|
|
50
|
+
export function useFloatClearance(offset: string, extra?: string): void {
|
|
51
|
+
useEffect(() => {
|
|
52
|
+
return claimFloatClearance(`calc(${offset} + 3.5rem${extra ? ` + ${extra}` : ""})`);
|
|
53
|
+
}, [offset, extra]);
|
|
54
|
+
}
|
|
@@ -23,6 +23,16 @@ export interface SettingsGroup<G extends string = string> {
|
|
|
23
23
|
title: string;
|
|
24
24
|
/** One line under the heading and in the phone list, where it is cut to one line. */
|
|
25
25
|
help: string;
|
|
26
|
+
/**
|
|
27
|
+
* 0.48 (keksdose live #450): a count or a mark drawn on the group's ROW — at the end of
|
|
28
|
+
* its desktop sidebar row and before the chevron of its phone list row — never on the
|
|
29
|
+
* heading over its cards. The admin area's waiting work: `<AdminWaitingBadge count={n} />`
|
|
30
|
+
* (nothing for 0), so the account menu's badge leads to the section that holds it. It
|
|
31
|
+
* sits inside the row's link and is read with its name, so say what the number is (the
|
|
32
|
+
* waiting pill does: "3, 3 waiting"). While the page is searched the sidebar shows the
|
|
33
|
+
* hit count in its place. Default: none.
|
|
34
|
+
*/
|
|
35
|
+
badge?: ReactNode;
|
|
26
36
|
/**
|
|
27
37
|
* `false` hides the group: no row, no hits, and its path acts as an unknown one — it
|
|
28
38
|
* lands on the default group (desktop) or the list (phone), never on a 404 (§3.1).
|
|
@@ -369,7 +369,17 @@ export function SettingsLayout<G extends string>({
|
|
|
369
369
|
}
|
|
370
370
|
title={g.title}
|
|
371
371
|
subtitle={g.help}
|
|
372
|
-
trailing={
|
|
372
|
+
trailing={
|
|
373
|
+
g.badge == null ? (
|
|
374
|
+
<ChevronRight aria-hidden className="size-4 rtl:-scale-x-100" />
|
|
375
|
+
) : (
|
|
376
|
+
// The group's badge (0.48) before the chevron, the two as one end.
|
|
377
|
+
<span className="flex items-center gap-2">
|
|
378
|
+
{g.badge}
|
|
379
|
+
<ChevronRight aria-hidden className="size-4 rtl:-scale-x-100" />
|
|
380
|
+
</span>
|
|
381
|
+
)
|
|
382
|
+
}
|
|
373
383
|
/>
|
|
374
384
|
))}
|
|
375
385
|
</List>
|
|
@@ -421,13 +431,17 @@ export function SettingsLayout<G extends string>({
|
|
|
421
431
|
name: g.title,
|
|
422
432
|
label: <span className={cn(dim && "opacity-40")}>{g.title}</span>,
|
|
423
433
|
icon: <span className={cn("inline-flex [&_svg]:size-4", dim && "opacity-40")}>{g.icon}</span>,
|
|
424
|
-
badge
|
|
425
|
-
|
|
434
|
+
// While searching, the hit count; otherwise the group's own badge (0.48).
|
|
435
|
+
badge: searching ? (
|
|
436
|
+
count > 0 ? (
|
|
426
437
|
<Chip size="sm" tone="brand">
|
|
427
438
|
<span aria-hidden>{count}</span>
|
|
428
439
|
<span className="sr-only-fixed">{labels.matchCount(count)}</span>
|
|
429
440
|
</Chip>
|
|
430
|
-
) : undefined
|
|
441
|
+
) : undefined
|
|
442
|
+
) : (
|
|
443
|
+
g.badge
|
|
444
|
+
),
|
|
431
445
|
};
|
|
432
446
|
})}
|
|
433
447
|
active={(open?.id ?? route.groups[0]?.id) as G}
|
package/src/shell/app-shell.tsx
CHANGED
|
@@ -18,7 +18,7 @@ import { useAnchoredRect } from "../hooks/use-anchored-rect";
|
|
|
18
18
|
import { anchoredInline } from "../hooks/use-anchored-panel";
|
|
19
19
|
import { useDismissLayer } from "../hooks/use-dismiss";
|
|
20
20
|
import { useBreakpoint } from "../hooks/use-breakpoint";
|
|
21
|
-
import { useTextSize } from "../theme/text-size";
|
|
21
|
+
import { TEXT_SIZES, useTextSize } from "../theme/text-size";
|
|
22
22
|
import type { TextSize } from "../theme/text-size";
|
|
23
23
|
import { DEFAULT_APP_SHELL_LABELS, useKitLabels, useKitLink, type LabelOverride } from "../i18n/kit-labels";
|
|
24
24
|
import type { KitLinkComponent, KitLinkProps } from "../i18n/kit-labels";
|
|
@@ -45,6 +45,11 @@ export interface AppShellNavItem {
|
|
|
45
45
|
* shortening the real label, which the sidebar has ample room for and which is the
|
|
46
46
|
* name the rest of the product uses. */
|
|
47
47
|
shortLabel?: string;
|
|
48
|
+
/** 0.48: the text size from which the bar uses {@link shortLabel} — `"large"` keeps the
|
|
49
|
+
* full label at Normal, where five cells hold "Transactions" whole, and shortens it
|
|
50
|
+
* from Large up (keksdose dev#613, which read `useTextSize()` in its layout to decide).
|
|
51
|
+
* Default `"normal"`: the short label at every size, as before. */
|
|
52
|
+
shortLabelFrom?: TextSize;
|
|
48
53
|
/** 0.31.0: leave this entry out of the MOBILE BOTTOM BAR; the sidebar keeps it.
|
|
49
54
|
* For an entry a phone reaches another way — the brand link to the start page —
|
|
50
55
|
* when one cell fewer is what keeps the others' labels whole (the bar divides its
|
|
@@ -544,9 +549,13 @@ export function AppShell({
|
|
|
544
549
|
one that scrolls into empty space. Guarding it per component (see
|
|
545
550
|
sr-only-containment.test) could never be complete: any page's own
|
|
546
551
|
`sr-only` span re-opened it. Here it is closed for every child at once. */}
|
|
552
|
+
{/* Below `md` the bottom room is the nav plus the larger of 1.5rem and what the
|
|
553
|
+
floating launchers take above it (`--app-float-clearance`, published by
|
|
554
|
+
every kit FAB, FloatingPanel and FloatingActionGroup while mounted — keksdose
|
|
555
|
+
dev#612: the last row ended under the help launcher). */}
|
|
547
556
|
<main
|
|
548
557
|
className={cn(
|
|
549
|
-
"relative flex-1 max-w-full overflow-x-clip pb-[calc(var(--app-nav-h,4rem)+1.5rem)] md:pb-0 md:min-h-0 md:overflow-y-auto md:[scrollbar-gutter:stable_both-edges]",
|
|
558
|
+
"relative flex-1 max-w-full overflow-x-clip pb-[calc(var(--app-nav-h,4rem)+max(1.5rem,var(--app-float-clearance,0px)))] md:pb-0 md:min-h-0 md:overflow-y-auto md:[scrollbar-gutter:stable_both-edges]",
|
|
550
559
|
// The list page's fill (0.40, docs/page-layout-harmonization.md §1.3): while
|
|
551
560
|
// a `fillHeight` DataTable is inside, a flex column — a banner above the
|
|
552
561
|
// page keeps its own height and the page (a `FillHeight`) takes the rest,
|
|
@@ -764,6 +773,11 @@ function MobileCellLabel({ children }: { children: ReactNode }) {
|
|
|
764
773
|
* nothing marked on every sub-page (the same defect the sidebar had). */
|
|
765
774
|
function MobileNavItem({ item }: { item: AppShellNavItem }) {
|
|
766
775
|
const subActive = useSubItemActive(item);
|
|
776
|
+
const { size } = useTextSize();
|
|
777
|
+
const short =
|
|
778
|
+
item.shortLabel !== undefined && TEXT_SIZES.indexOf(size) >= TEXT_SIZES.indexOf(item.shortLabelFrom ?? "normal")
|
|
779
|
+
? item.shortLabel
|
|
780
|
+
: undefined;
|
|
767
781
|
return (
|
|
768
782
|
<NavLink
|
|
769
783
|
to={item.to}
|
|
@@ -774,7 +788,7 @@ function MobileNavItem({ item }: { item: AppShellNavItem }) {
|
|
|
774
788
|
<span className="relative">
|
|
775
789
|
<item.icon className="size-5" />
|
|
776
790
|
</span>
|
|
777
|
-
<MobileCellLabel>{
|
|
791
|
+
<MobileCellLabel>{short ?? item.label}</MobileCellLabel>
|
|
778
792
|
</NavLink>
|
|
779
793
|
);
|
|
780
794
|
}
|
|
@@ -28,6 +28,8 @@ export function OptionSwitcherMenu<T extends string>({
|
|
|
28
28
|
/** Names the trigger and the menu. Required: the trigger is an icon (refactor audit
|
|
29
29
|
* 2026-10, API-1 — the `ariaLabel` spelling went in 0.35). */
|
|
30
30
|
"aria-label": string;
|
|
31
|
+
/** The trigger's tooltip. Default: `aria-label` (0.48) — the name, shown on hover and
|
|
32
|
+
* focus. A different text also describes the trigger (`aria-describedby`). */
|
|
31
33
|
title?: string;
|
|
32
34
|
heading?: string;
|
|
33
35
|
options: OptionSwitcherOption<T>[];
|
|
@@ -37,13 +39,23 @@ export function OptionSwitcherMenu<T extends string>({
|
|
|
37
39
|
return (
|
|
38
40
|
<HoverMenu
|
|
39
41
|
aria-label={ariaLabel}
|
|
40
|
-
trigger={({ toggle }) =>
|
|
41
|
-
|
|
42
|
+
trigger={({ toggle }) => {
|
|
43
|
+
const button = (
|
|
42
44
|
<button type="button" onClick={toggle} aria-label={ariaLabel} className={TOPBAR_TRIGGER_CLASS}>
|
|
43
45
|
{icon}
|
|
44
46
|
</button>
|
|
45
|
-
|
|
46
|
-
|
|
47
|
+
);
|
|
48
|
+
// The name on hover and focus even without a `title` (0.48, keksdose dev#610). A
|
|
49
|
+
// bubble that repeats the name sits round the button in a fragment, as IconButton's
|
|
50
|
+
// does, so it does not describe the button by its own name — a reader heard "Text
|
|
51
|
+
// size, button, Text size". A `title` that says more still describes it.
|
|
52
|
+
const tip = title ?? ariaLabel;
|
|
53
|
+
return (
|
|
54
|
+
<Tooltip label={tip} portal>
|
|
55
|
+
{tip === ariaLabel ? <>{button}</> : button}
|
|
56
|
+
</Tooltip>
|
|
57
|
+
);
|
|
58
|
+
}}
|
|
47
59
|
>
|
|
48
60
|
{(close) => (
|
|
49
61
|
<ul className="py-1">
|
|
@@ -6,6 +6,7 @@ import { HoverMenu, type HoverMenuProps } from "../components/hover-menu";
|
|
|
6
6
|
import { MenuItem } from "../components/menu-item";
|
|
7
7
|
import type { MenuItemTone } from "../components/menu-item";
|
|
8
8
|
import { StatusDot } from "../components/status-dot";
|
|
9
|
+
import { Tooltip } from "../components/tooltip";
|
|
9
10
|
import type { UserAvatarBadge } from "../components/user-avatar";
|
|
10
11
|
import { TOPBAR_TRIGGER_CLASS } from "./topbar-controls";
|
|
11
12
|
|
|
@@ -296,16 +297,27 @@ export function TopBarActionMenu({
|
|
|
296
297
|
trigger={({ open, toggle, close }) => {
|
|
297
298
|
// One element for both kinds: the router's link when `href` is set, else the
|
|
298
299
|
// kit's button. Same classes and face either way.
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
300
|
+
//
|
|
301
|
+
// Its name in the kit's Tooltip on hover and focus (0.48, keksdose dev#610): an
|
|
302
|
+
// icon alone told a sighted user nothing the screen reader was told. In a
|
|
303
|
+
// fragment, as IconButton does it, so the bubble is not the trigger's
|
|
304
|
+
// `aria-describedby` — the name it repeats is the trigger's own, and a reader
|
|
305
|
+
// would hear it twice. Portalled, like the top bar's other triggers.
|
|
306
|
+
const control = (className: string, children: ReactNode, label?: string) => (
|
|
307
|
+
<Tooltip label={ariaLabel} portal>
|
|
308
|
+
<>
|
|
309
|
+
{href ? (
|
|
310
|
+
<Link {...triggerAttributes} to={href} onClick={close} aria-label={label} className={className}>
|
|
311
|
+
{children}
|
|
312
|
+
</Link>
|
|
313
|
+
) : (
|
|
314
|
+
<button {...triggerAttributes} type="button" onClick={toggle} aria-label={label} className={className}>
|
|
315
|
+
{children}
|
|
316
|
+
</button>
|
|
317
|
+
)}
|
|
318
|
+
</>
|
|
319
|
+
</Tooltip>
|
|
320
|
+
);
|
|
309
321
|
if (trigger) {
|
|
310
322
|
return control(
|
|
311
323
|
cn(TOPBAR_TRIGGER_CLASS, "relative", triggerClassName),
|