@eifi1/ui-kit 0.47.0 → 0.48.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (152) hide show
  1. package/README.md +5 -3
  2. package/dist/admin/admin-sections.d.ts +8 -0
  3. package/dist/admin/admin-sections.js +2 -1
  4. package/dist/admin/admin-sections.js.map +1 -1
  5. package/dist/auth/verify-email.d.ts +9 -1
  6. package/dist/auth/verify-email.js +22 -13
  7. package/dist/auth/verify-email.js.map +1 -1
  8. package/dist/components/alert-banner.d.ts +29 -1
  9. package/dist/components/alert-banner.js +84 -7
  10. package/dist/components/alert-banner.js.map +1 -1
  11. package/dist/components/button-look.d.ts +4 -0
  12. package/dist/components/button-look.js +2 -0
  13. package/dist/components/button-look.js.map +1 -1
  14. package/dist/components/combobox-core.js +5 -1
  15. package/dist/components/combobox-core.js.map +1 -1
  16. package/dist/components/data-table-column-settings.d.ts +6 -1
  17. package/dist/components/data-table-column-settings.js +12 -2
  18. package/dist/components/data-table-column-settings.js.map +1 -1
  19. package/dist/components/data-table-fit.d.ts +44 -0
  20. package/dist/components/data-table-fit.js +103 -0
  21. package/dist/components/data-table-fit.js.map +1 -0
  22. package/dist/components/data-table-groups.d.ts +95 -0
  23. package/dist/components/data-table-groups.js +165 -0
  24. package/dist/components/data-table-groups.js.map +1 -0
  25. package/dist/components/data-table-head.js +3 -1
  26. package/dist/components/data-table-head.js.map +1 -1
  27. package/dist/components/data-table-labels.d.ts +7 -0
  28. package/dist/components/data-table-labels.js +3 -0
  29. package/dist/components/data-table-labels.js.map +1 -1
  30. package/dist/components/data-table-leading-row.d.ts +17 -0
  31. package/dist/components/data-table-leading-row.js +41 -0
  32. package/dist/components/data-table-leading-row.js.map +1 -0
  33. package/dist/components/data-table-resize.js +1 -1
  34. package/dist/components/data-table-resize.js.map +1 -1
  35. package/dist/components/data-table.d.ts +78 -2
  36. package/dist/components/data-table.js +348 -257
  37. package/dist/components/data-table.js.map +1 -1
  38. package/dist/components/disclosure.d.ts +3 -1
  39. package/dist/components/disclosure.js +10 -4
  40. package/dist/components/disclosure.js.map +1 -1
  41. package/dist/components/dropdown.js +8 -1
  42. package/dist/components/dropdown.js.map +1 -1
  43. package/dist/components/floating-panel.d.ts +14 -1
  44. package/dist/components/floating-panel.js +7 -2
  45. package/dist/components/floating-panel.js.map +1 -1
  46. package/dist/components/hover-menu.js +5 -1
  47. package/dist/components/hover-menu.js.map +1 -1
  48. package/dist/components/icon-button.js +2 -1
  49. package/dist/components/icon-button.js.map +1 -1
  50. package/dist/components/popover.js +5 -1
  51. package/dist/components/popover.js.map +1 -1
  52. package/dist/components/table-skeleton.d.ts +35 -0
  53. package/dist/components/table-skeleton.js +75 -0
  54. package/dist/components/table-skeleton.js.map +1 -0
  55. package/dist/components/tabs.d.ts +10 -12
  56. package/dist/components/tabs.js +20 -10
  57. package/dist/components/tabs.js.map +1 -1
  58. package/dist/components/use-table-state.d.ts +8 -7
  59. package/dist/components/use-table-state.js +37 -3
  60. package/dist/components/use-table-state.js.map +1 -1
  61. package/dist/data-table.d.ts +1 -0
  62. package/dist/data-table.js.map +1 -1
  63. package/dist/eslint/index.js +1 -1
  64. package/dist/feedback/feedback-table.d.ts +5 -0
  65. package/dist/feedback/feedback-table.js +30 -7
  66. package/dist/feedback/feedback-table.js.map +1 -1
  67. package/dist/hooks/use-dismiss.d.ts +30 -1
  68. package/dist/hooks/use-dismiss.js +30 -2
  69. package/dist/hooks/use-dismiss.js.map +1 -1
  70. package/dist/i18n/german.js +6 -1
  71. package/dist/i18n/german.js.map +1 -1
  72. package/dist/i18n/kit-labels.d.ts +4 -0
  73. package/dist/i18n/kit-labels.js.map +1 -1
  74. package/dist/i18n/locales/en.js +6 -1
  75. package/dist/i18n/locales/en.js.map +2 -2
  76. package/dist/i18n/locales/es.js +6 -1
  77. package/dist/i18n/locales/es.js.map +1 -1
  78. package/dist/i18n/locales/fr.js +6 -1
  79. package/dist/i18n/locales/fr.js.map +1 -1
  80. package/dist/i18n/locales/hu.js +6 -1
  81. package/dist/i18n/locales/hu.js.map +1 -1
  82. package/dist/i18n/locales/it.js +6 -1
  83. package/dist/i18n/locales/it.js.map +1 -1
  84. package/dist/i18n/locales/zh.js +6 -1
  85. package/dist/i18n/locales/zh.js.map +1 -1
  86. package/dist/i18n/shared-labels.js +3 -1
  87. package/dist/i18n/shared-labels.js.map +1 -1
  88. package/dist/index.d.ts +2 -0
  89. package/dist/index.js +2 -0
  90. package/dist/index.js.map +1 -1
  91. package/dist/lib/float-clearance.d.ts +8 -0
  92. package/dist/lib/float-clearance.js +29 -0
  93. package/dist/lib/float-clearance.js.map +1 -0
  94. package/dist/settings/settings-catalogue.d.ts +10 -0
  95. package/dist/settings/settings-catalogue.js.map +1 -1
  96. package/dist/settings/settings-layout.js +10 -3
  97. package/dist/settings/settings-layout.js.map +1 -1
  98. package/dist/shell/app-shell.d.ts +5 -0
  99. package/dist/shell/app-shell.js +5 -3
  100. package/dist/shell/app-shell.js.map +1 -1
  101. package/dist/shell/option-switcher-menu.d.ts +2 -0
  102. package/dist/shell/option-switcher-menu.js +6 -2
  103. package/dist/shell/option-switcher-menu.js.map +1 -1
  104. package/dist/shell/topbar-action-menu.js +2 -1
  105. package/dist/shell/topbar-action-menu.js.map +1 -1
  106. package/dist/shell/topbar-controls.js +5 -3
  107. package/dist/shell/topbar-controls.js.map +1 -1
  108. package/dist/tour/tour.js +1 -0
  109. package/dist/tour/tour.js.map +1 -1
  110. package/package.json +1 -1
  111. package/src/admin/admin-sections.tsx +8 -0
  112. package/src/auth/verify-email.tsx +34 -9
  113. package/src/components/alert-banner.tsx +131 -7
  114. package/src/components/button-look.ts +6 -0
  115. package/src/components/combobox-core.tsx +7 -1
  116. package/src/components/data-table-column-settings.tsx +23 -2
  117. package/src/components/data-table-fit.ts +187 -0
  118. package/src/components/data-table-groups.tsx +275 -0
  119. package/src/components/data-table-head.tsx +4 -1
  120. package/src/components/data-table-labels.ts +13 -0
  121. package/src/components/data-table-leading-row.tsx +55 -0
  122. package/src/components/data-table-resize.ts +2 -1
  123. package/src/components/data-table.tsx +246 -43
  124. package/src/components/disclosure.tsx +22 -5
  125. package/src/components/dropdown.tsx +11 -1
  126. package/src/components/floating-panel.tsx +24 -1
  127. package/src/components/hover-menu.tsx +6 -1
  128. package/src/components/icon-button.tsx +3 -3
  129. package/src/components/popover.tsx +7 -1
  130. package/src/components/table-skeleton.tsx +133 -0
  131. package/src/components/tabs.tsx +67 -37
  132. package/src/components/use-table-state.ts +54 -3
  133. package/src/data-table.ts +2 -0
  134. package/src/feedback/feedback-table.tsx +33 -1
  135. package/src/hooks/use-dismiss.ts +65 -3
  136. package/src/i18n/german.ts +5 -0
  137. package/src/i18n/kit-labels.tsx +4 -0
  138. package/src/i18n/locales/es.ts +5 -0
  139. package/src/i18n/locales/fr.ts +5 -0
  140. package/src/i18n/locales/hu.ts +5 -0
  141. package/src/i18n/locales/it.ts +5 -0
  142. package/src/i18n/locales/zh.ts +5 -0
  143. package/src/i18n/shared-labels.ts +2 -0
  144. package/src/index.ts +3 -0
  145. package/src/lib/float-clearance.ts +54 -0
  146. package/src/settings/settings-catalogue.ts +10 -0
  147. package/src/settings/settings-layout.tsx +18 -4
  148. package/src/shell/app-shell.tsx +17 -3
  149. package/src/shell/option-switcher-menu.tsx +16 -4
  150. package/src/shell/topbar-action-menu.tsx +22 -10
  151. package/src/shell/topbar-controls.tsx +18 -12
  152. package/src/tour/tour.tsx +5 -0
@@ -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",
@@ -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
@@ -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",
@@ -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",
@@ -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ő",
@@ -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",
@@ -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={<ChevronRight aria-hidden className="size-4 rtl:-scale-x-100" />}
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
- searching && count > 0 ? (
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}
@@ -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>{item.shortLabel ?? item.label}</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
- <Tooltip label={title} portal>
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
- </Tooltip>
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
- const control = (className: string, children: ReactNode, label?: string) =>
300
- href ? (
301
- <Link {...triggerAttributes} to={href} onClick={close} aria-label={label} className={className}>
302
- {children}
303
- </Link>
304
- ) : (
305
- <button {...triggerAttributes} type="button" onClick={toggle} aria-label={label} className={className}>
306
- {children}
307
- </button>
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),
@@ -58,19 +58,25 @@ export function ThemeToggle({
58
58
  // icon button with no name — a reader heard "button" and nothing else.
59
59
  const labels = useKitLabels("topBar", DEFAULT_TOP_BAR_LABELS, { theme: ariaLabel });
60
60
  const Icon = mode === "dark" ? Moon : Sun;
61
+ const button = (
62
+ <button
63
+ type="button"
64
+ onClick={() => {
65
+ onChange?.(mode === "dark" ? "light" : "dark");
66
+ onToggle?.();
67
+ }}
68
+ aria-label={labels.theme}
69
+ className={cn(TOPBAR_TRIGGER_CLASS, className)}
70
+ >
71
+ <Icon className="size-5" />
72
+ </button>
73
+ );
74
+ // The name on hover and focus even without a `title` (0.48, keksdose dev#610); a bubble
75
+ // that only repeats it is visual, in a fragment as on IconButton — see OptionSwitcherMenu.
76
+ const tip = title ?? labels.theme;
61
77
  return (
62
- <Tooltip label={title} portal>
63
- <button
64
- type="button"
65
- onClick={() => {
66
- onChange?.(mode === "dark" ? "light" : "dark");
67
- onToggle?.();
68
- }}
69
- aria-label={labels.theme}
70
- className={cn(TOPBAR_TRIGGER_CLASS, className)}
71
- >
72
- <Icon className="size-5" />
73
- </button>
78
+ <Tooltip label={tip} portal>
79
+ {tip === labels.theme ? <>{button}</> : button}
74
80
  </Tooltip>
75
81
  );
76
82
  }
package/src/tour/tour.tsx CHANGED
@@ -615,6 +615,11 @@ function TourOverlay({
615
615
  className={cn("pointer-events-none fixed inset-0", interactive ? "z-[45]" : "z-[60]")}
616
616
  role="dialog"
617
617
  aria-modal={interactive ? undefined : "true"}
618
+ // A stable marker for the overlay, on every step (0.48, keksdose dev#611–613): an
619
+ // app that hides its floating help launcher while a modal is open excepts the
620
+ // tour, which may spotlight that launcher — `[aria-modal="true"]:not([data-tour-overlay])`.
621
+ // keksdose matched the `pointer-events-none` class until now, which is the kit's to change.
622
+ data-tour-overlay=""
618
623
  data-tour-interactive={interactive ? "" : undefined}
619
624
  aria-label={step.title}
620
625
  dir={dir}