@eifi1/ui-kit 0.48.1 → 0.49.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 +28 -13
- package/dist/admin/admin-action-confirm.js +1 -1
- package/dist/admin/admin-action-confirm.js.map +1 -1
- package/dist/admin/invitations-panel.js +5 -1
- package/dist/admin/invitations-panel.js.map +1 -1
- package/dist/admin/user-roster.d.ts +11 -5
- package/dist/admin/user-roster.js +15 -4
- package/dist/admin/user-roster.js.map +1 -1
- package/dist/auth/accept-invitation.js +2 -2
- package/dist/auth/accept-invitation.js.map +1 -1
- package/dist/auth/not-found-page.js +1 -1
- package/dist/auth/not-found-page.js.map +1 -1
- package/dist/auth/reset-password-form.js +2 -2
- package/dist/auth/reset-password-form.js.map +1 -1
- package/dist/auth/verify-email.js +2 -2
- package/dist/auth/verify-email.js.map +1 -1
- package/dist/billing/plan-card.js +2 -2
- package/dist/billing/plan-card.js.map +1 -1
- package/dist/components/breadcrumbs.d.ts +19 -1
- package/dist/components/breadcrumbs.js +11 -4
- package/dist/components/breadcrumbs.js.map +1 -1
- package/dist/components/button-look.d.ts +37 -7
- package/dist/components/button-look.js +33 -6
- package/dist/components/button-look.js.map +1 -1
- package/dist/components/button.d.ts +4 -1
- package/dist/components/button.js +2 -2
- package/dist/components/button.js.map +1 -1
- package/dist/components/calendar-heatmap.js +2 -1
- package/dist/components/calendar-heatmap.js.map +1 -1
- package/dist/components/chart-edge.d.ts +10 -0
- package/dist/components/chart-edge.js +57 -0
- package/dist/components/chart-edge.js.map +1 -0
- package/dist/components/chart.js +2 -1
- package/dist/components/chart.js.map +1 -1
- package/dist/components/chip.d.ts +8 -1
- package/dist/components/chip.js +4 -1
- package/dist/components/chip.js.map +1 -1
- package/dist/components/confirm-context.d.ts +5 -0
- package/dist/components/confirm-context.js +7 -0
- package/dist/components/confirm-context.js.map +1 -0
- package/dist/components/confirm-dialog.js +3 -3
- package/dist/components/confirm-dialog.js.map +1 -1
- package/dist/components/danger-confirm.d.ts +2 -2
- package/dist/components/danger-confirm.js +1 -1
- package/dist/components/danger-confirm.js.map +1 -1
- package/dist/components/data-table-fit.d.ts +8 -1
- package/dist/components/data-table-fit.js +28 -4
- package/dist/components/data-table-fit.js.map +1 -1
- package/dist/components/data-table-groups.d.ts +3 -1
- package/dist/components/data-table-groups.js +11 -5
- package/dist/components/data-table-groups.js.map +1 -1
- package/dist/components/data-table-head.d.ts +8 -1
- package/dist/components/data-table-head.js +11 -3
- package/dist/components/data-table-head.js.map +1 -1
- package/dist/components/data-table-leading-row.d.ts +3 -1
- package/dist/components/data-table-leading-row.js +2 -1
- package/dist/components/data-table-leading-row.js.map +1 -1
- package/dist/components/data-table-mobile.d.ts +48 -2
- package/dist/components/data-table-mobile.js +87 -10
- package/dist/components/data-table-mobile.js.map +1 -1
- package/dist/components/data-table-row-actions.d.ts +2 -1
- package/dist/components/data-table-row-actions.js +9 -2
- package/dist/components/data-table-row-actions.js.map +1 -1
- package/dist/components/data-table-row.d.ts +84 -0
- package/dist/components/data-table-row.js +261 -0
- package/dist/components/data-table-row.js.map +1 -0
- package/dist/components/data-table-selection.d.ts +3 -0
- package/dist/components/data-table-selection.js +12 -1
- package/dist/components/data-table-selection.js.map +1 -1
- package/dist/components/data-table-virtual.d.ts +98 -0
- package/dist/components/data-table-virtual.js +369 -0
- package/dist/components/data-table-virtual.js.map +1 -0
- package/dist/components/data-table.d.ts +74 -2
- package/dist/components/data-table.js +241 -221
- package/dist/components/data-table.js.map +1 -1
- package/dist/components/error-boundary.js +2 -2
- package/dist/components/error-boundary.js.map +1 -1
- package/dist/components/floating-panel.d.ts +36 -13
- package/dist/components/floating-panel.js +27 -7
- package/dist/components/floating-panel.js.map +1 -1
- package/dist/components/form-actions.d.ts +1 -1
- package/dist/components/form-actions.js +1 -1
- package/dist/components/form-actions.js.map +1 -1
- package/dist/components/icon-button.d.ts +11 -0
- package/dist/components/icon-button.js +7 -5
- package/dist/components/icon-button.js.map +1 -1
- package/dist/components/nav-pills.d.ts +8 -1
- package/dist/components/nav-pills.js +61 -38
- package/dist/components/nav-pills.js.map +1 -1
- package/dist/components/page-header-context.d.ts +8 -0
- package/dist/components/page-header-context.js +7 -0
- package/dist/components/page-header-context.js.map +1 -0
- package/dist/components/page-header.d.ts +14 -2
- package/dist/components/page-header.js +10 -1
- package/dist/components/page-header.js.map +1 -1
- package/dist/components/pie-chart.js +2 -1
- package/dist/components/pie-chart.js.map +1 -1
- package/dist/components/progress-bar.js +5 -4
- package/dist/components/progress-bar.js.map +1 -1
- package/dist/components/reauth-dialog.js +1 -1
- package/dist/components/reauth-dialog.js.map +1 -1
- package/dist/components/row-actions-labels.js +3 -1
- package/dist/components/row-actions-labels.js.map +1 -1
- package/dist/components/row-actions.d.ts +43 -9
- package/dist/components/row-actions.js +78 -15
- package/dist/components/row-actions.js.map +1 -1
- package/dist/components/section-nav.d.ts +37 -0
- package/dist/components/section-nav.js +33 -0
- package/dist/components/section-nav.js.map +1 -0
- package/dist/components/series-chart.js +8 -3
- package/dist/components/series-chart.js.map +1 -1
- package/dist/components/share-card.js +1 -1
- package/dist/components/share-card.js.map +1 -1
- package/dist/components/signature-pad.js +1 -1
- package/dist/components/signature-pad.js.map +1 -1
- package/dist/components/sparkline.js +2 -1
- package/dist/components/sparkline.js.map +1 -1
- package/dist/components/stat-tile.js +14 -7
- package/dist/components/stat-tile.js.map +1 -1
- package/dist/components/status-dot.d.ts +5 -1
- package/dist/components/status-dot.js +4 -1
- package/dist/components/status-dot.js.map +1 -1
- package/dist/components/tooltip.js +1 -1
- package/dist/components/tooltip.js.map +1 -1
- package/dist/components/translation-review-editor.js +2 -2
- package/dist/components/translation-review-editor.js.map +1 -1
- package/dist/components/translation-review.js +2 -2
- package/dist/components/translation-review.js.map +1 -1
- package/dist/components/treemap.d.ts +5 -2
- package/dist/components/treemap.js +26 -6
- package/dist/components/treemap.js.map +1 -1
- package/dist/components/use-table-state.js +37 -6
- 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/demo/demo-banner.js +1 -1
- package/dist/demo/demo-banner.js.map +1 -1
- package/dist/demo/demo-ended.js +1 -1
- package/dist/demo/demo-ended.js.map +1 -1
- package/dist/demo/demo-start.js +1 -1
- package/dist/demo/demo-start.js.map +1 -1
- package/dist/eslint/index.js +1 -1
- package/dist/feedback/feedback-inbox.js +1 -1
- package/dist/feedback/feedback-inbox.js.map +1 -1
- package/dist/hooks/use-anchored-panel.js +2 -1
- package/dist/hooks/use-anchored-panel.js.map +1 -1
- package/dist/hooks/use-dismiss.d.ts +9 -1
- package/dist/hooks/use-dismiss.js +25 -5
- package/dist/hooks/use-dismiss.js.map +1 -1
- package/dist/hooks/use-document-title.d.ts +14 -0
- package/dist/hooks/use-document-title.js +22 -0
- package/dist/hooks/use-document-title.js.map +1 -0
- package/dist/i18n/german.js +6 -2
- package/dist/i18n/german.js.map +1 -1
- package/dist/i18n/kit-labels.d.ts +31 -1
- package/dist/i18n/kit-labels.js +43 -2
- package/dist/i18n/kit-labels.js.map +1 -1
- package/dist/i18n/locales/en.js +6 -2
- package/dist/i18n/locales/en.js.map +2 -2
- package/dist/i18n/locales/es.js +6 -2
- package/dist/i18n/locales/es.js.map +1 -1
- package/dist/i18n/locales/fr.js +6 -2
- package/dist/i18n/locales/fr.js.map +1 -1
- package/dist/i18n/locales/hu.js +6 -2
- package/dist/i18n/locales/hu.js.map +1 -1
- package/dist/i18n/locales/it.js +6 -2
- package/dist/i18n/locales/it.js.map +1 -1
- package/dist/i18n/locales/zh.js +6 -2
- package/dist/i18n/locales/zh.js.map +1 -1
- package/dist/i18n/review.js +2 -0
- package/dist/i18n/review.js.map +1 -1
- package/dist/i18n/shared-labels.js +1 -0
- package/dist/i18n/shared-labels.js.map +1 -1
- package/dist/index.d.ts +8 -1
- package/dist/index.js +11 -0
- package/dist/index.js.map +1 -1
- package/dist/landing/landing-actions.js +2 -2
- package/dist/landing/landing-actions.js.map +1 -1
- package/dist/landing/public-header.js +2 -2
- package/dist/landing/public-header.js.map +1 -1
- package/dist/lib/clipboard-minus.d.ts +27 -0
- package/dist/lib/clipboard-minus.js +63 -0
- package/dist/lib/clipboard-minus.js.map +1 -0
- package/dist/lib/document-title.d.ts +17 -0
- package/dist/lib/document-title.js +29 -0
- package/dist/lib/document-title.js.map +1 -0
- package/dist/lib/float-clearance.js +8 -2
- package/dist/lib/float-clearance.js.map +1 -1
- package/dist/lib/format.d.ts +35 -10
- package/dist/lib/format.js +55 -11
- package/dist/lib/format.js.map +1 -1
- package/dist/lib/minus.d.ts +22 -0
- package/dist/lib/minus.js +14 -0
- package/dist/lib/minus.js.map +1 -0
- package/dist/lib/status-tone.d.ts +30 -0
- package/dist/lib/status-tone.js +17 -0
- package/dist/lib/status-tone.js.map +1 -0
- package/dist/settings/settings-layout.js +3 -2
- package/dist/settings/settings-layout.js.map +1 -1
- package/dist/shell/account-menu-language.d.ts +1 -1
- package/dist/shell/account-menu-language.js +9 -1
- package/dist/shell/account-menu-language.js.map +1 -1
- package/dist/shell/app-shell-labels.js +2 -1
- package/dist/shell/app-shell-labels.js.map +1 -1
- package/dist/shell/app-shell.d.ts +58 -16
- package/dist/shell/app-shell.js +158 -44
- package/dist/shell/app-shell.js.map +1 -1
- package/dist/shell/language-code.d.ts +3 -0
- package/dist/shell/language-code.js +8 -0
- package/dist/shell/language-code.js.map +1 -0
- package/dist/shell/topbar-controls.d.ts +14 -6
- package/dist/shell/topbar-controls.js +14 -17
- package/dist/shell/topbar-controls.js.map +1 -1
- package/dist/testing/css-variables.generated.d.ts +1 -1
- package/dist/testing/css-variables.generated.js +1 -1
- package/dist/testing/css-variables.generated.js.map +1 -1
- package/dist/theme/chart-palette.d.ts +82 -15
- package/dist/theme/chart-palette.js +51 -24
- package/dist/theme/chart-palette.js.map +1 -1
- package/dist/theme/palette-presets.d.ts +9 -0
- package/dist/theme/palette-presets.js +77 -62
- package/dist/theme/palette-presets.js.map +1 -1
- package/dist/tour/tour.js +1 -1
- package/dist/tour/tour.js.map +1 -1
- package/dist/wizard/stepper-nav.d.ts +1 -1
- package/dist/wizard/stepper-nav.js +3 -3
- package/dist/wizard/stepper-nav.js.map +1 -1
- package/package.json +1 -1
- package/src/admin/admin-action-confirm.tsx +1 -1
- package/src/admin/invitations-panel.tsx +7 -3
- package/src/admin/user-roster.tsx +23 -6
- package/src/auth/accept-invitation.tsx +3 -3
- package/src/auth/not-found-page.tsx +1 -1
- package/src/auth/reset-password-form.tsx +3 -3
- package/src/auth/verify-email.tsx +3 -3
- package/src/billing/plan-card.tsx +2 -2
- package/src/components/breadcrumbs.tsx +32 -4
- package/src/components/button-look.ts +86 -15
- package/src/components/button.tsx +6 -3
- package/src/components/calendar-heatmap.tsx +2 -1
- package/src/components/chart-edge.ts +73 -0
- package/src/components/chart.tsx +3 -1
- package/src/components/chip.tsx +10 -1
- package/src/components/confirm-context.ts +7 -0
- package/src/components/confirm-dialog.tsx +3 -3
- package/src/components/danger-confirm.tsx +3 -3
- package/src/components/data-table-fit.ts +58 -8
- package/src/components/data-table-groups.tsx +15 -4
- package/src/components/data-table-head.tsx +25 -2
- package/src/components/data-table-leading-row.tsx +4 -1
- package/src/components/data-table-mobile.tsx +162 -11
- package/src/components/data-table-row-actions.tsx +12 -3
- package/src/components/data-table-row.tsx +485 -0
- package/src/components/data-table-selection.ts +17 -1
- package/src/components/data-table-virtual.ts +547 -0
- package/src/components/data-table.tsx +367 -326
- package/src/components/error-boundary.tsx +2 -2
- package/src/components/floating-panel.tsx +67 -16
- package/src/components/form-actions.tsx +2 -2
- package/src/components/icon-button.tsx +21 -8
- package/src/components/nav-pills.tsx +29 -2
- package/src/components/page-header-context.ts +8 -0
- package/src/components/page-header.tsx +34 -2
- package/src/components/pie-chart.tsx +2 -1
- package/src/components/progress-bar.tsx +6 -3
- package/src/components/reauth-dialog.tsx +1 -1
- package/src/components/row-actions-labels.ts +2 -0
- package/src/components/row-actions.tsx +154 -33
- package/src/components/section-nav.tsx +65 -0
- package/src/components/series-chart.tsx +12 -3
- package/src/components/share-card.tsx +1 -1
- package/src/components/signature-pad.tsx +1 -1
- package/src/components/sparkline.tsx +2 -1
- package/src/components/stat-tile.tsx +19 -7
- package/src/components/status-dot.tsx +7 -1
- package/src/components/tooltip.tsx +3 -1
- package/src/components/translation-review-editor.tsx +2 -2
- package/src/components/translation-review.tsx +2 -2
- package/src/components/treemap.tsx +34 -8
- package/src/components/use-table-state.ts +63 -8
- package/src/data-table.ts +5 -3
- package/src/demo/demo-banner.tsx +1 -1
- package/src/demo/demo-ended.tsx +1 -1
- package/src/demo/demo-start.tsx +1 -1
- package/src/feedback/feedback-inbox.tsx +1 -1
- package/src/hooks/use-anchored-panel.ts +9 -1
- package/src/hooks/use-dismiss.ts +43 -4
- package/src/hooks/use-document-title.ts +31 -0
- package/src/i18n/german.ts +4 -0
- package/src/i18n/kit-labels.tsx +76 -1
- package/src/i18n/locales/es.ts +4 -0
- package/src/i18n/locales/fr.ts +4 -0
- package/src/i18n/locales/hu.ts +4 -0
- package/src/i18n/locales/it.ts +4 -0
- package/src/i18n/locales/zh.ts +4 -0
- package/src/i18n/review.ts +2 -0
- package/src/i18n/shared-labels.ts +1 -0
- package/src/index.ts +13 -0
- package/src/landing/landing-actions.tsx +2 -2
- package/src/landing/public-header.tsx +2 -2
- package/src/lib/clipboard-minus.ts +94 -0
- package/src/lib/document-title.ts +41 -0
- package/src/lib/float-clearance.ts +19 -3
- package/src/lib/format.ts +115 -30
- package/src/lib/minus.ts +40 -0
- package/src/lib/status-tone.ts +43 -0
- package/src/settings/settings-layout.tsx +3 -2
- package/src/shell/account-menu-language.tsx +14 -2
- package/src/shell/app-shell-labels.ts +1 -0
- package/src/shell/app-shell.tsx +298 -69
- package/src/shell/language-code.ts +5 -0
- package/src/shell/topbar-controls.tsx +35 -15
- package/src/testing/css-variables.generated.ts +6 -6
- package/src/theme/chart-palette.ts +102 -20
- package/src/theme/palette-presets.ts +121 -84
- package/src/tour/tour.tsx +1 -1
- package/src/wizard/stepper-nav.tsx +4 -4
- package/tokens.css +28 -18
|
@@ -4,6 +4,7 @@ import { Globe } from "lucide-react";
|
|
|
4
4
|
import { Disclosure } from "../components/disclosure";
|
|
5
5
|
import { MenuItem } from "../components/menu-item";
|
|
6
6
|
import { DEFAULT_TOP_BAR_LABELS, useKitLabels } from "../i18n/kit-labels";
|
|
7
|
+
import { languageCodeLabel } from "./language-code";
|
|
7
8
|
|
|
8
9
|
/** One offered language in the account menu's sub-list. */
|
|
9
10
|
export interface AccountMenuLanguageOption {
|
|
@@ -56,7 +57,7 @@ export interface AccountMenuLanguageProps {
|
|
|
56
57
|
/**
|
|
57
58
|
* The language choice INSIDE the account menu (docs/top-bar-harmonization.md §2.3),
|
|
58
59
|
* keksdose's sub-list: a `Disclosure variant="menu"` row — the globe, "Language", the
|
|
59
|
-
* active language's flag and the chevron — that opens in place onto one checked
|
|
60
|
+
* active language's flag and code ("DE") and the chevron — that opens in place onto one checked
|
|
60
61
|
* `MenuItem` per language. Inline rather than a nested popover: a popover inside a
|
|
61
62
|
* popover fights the outside click and has nowhere to go on a phone (keksdose #140).
|
|
62
63
|
*
|
|
@@ -87,7 +88,18 @@ export function AccountMenuLanguage({
|
|
|
87
88
|
<Globe aria-hidden className="size-4" />
|
|
88
89
|
{label ?? words.language}
|
|
89
90
|
</span>
|
|
90
|
-
{active &&
|
|
91
|
+
{active && (
|
|
92
|
+
// The flag alone could not tell de-CH, fr-CH and it-CH apart (one Swiss
|
|
93
|
+
// flag each), so the code sits beside it (0.49 KIT-10); the row's name
|
|
94
|
+
// carries the language's own name.
|
|
95
|
+
<span className="flex items-center gap-1.5">
|
|
96
|
+
{flagOf(active)}
|
|
97
|
+
<span aria-hidden className="text-xs font-semibold tabular-nums tracking-wide text-secondary">
|
|
98
|
+
{languageCodeLabel(active.code)}
|
|
99
|
+
</span>
|
|
100
|
+
<span className="sr-only">{active.label}</span>
|
|
101
|
+
</span>
|
|
102
|
+
)}
|
|
91
103
|
</span>
|
|
92
104
|
}
|
|
93
105
|
>
|
package/src/shell/app-shell.tsx
CHANGED
|
@@ -50,6 +50,19 @@ export interface AppShellNavItem {
|
|
|
50
50
|
* from Large up (keksdose dev#613, which read `useTextSize()` in its layout to decide).
|
|
51
51
|
* Default `"normal"`: the short label at every size, as before. */
|
|
52
52
|
shortLabelFrom?: TextSize;
|
|
53
|
+
/** 0.49: `shortLabel` is another word for the page, not a shortening of `label` —
|
|
54
|
+
* keksdose's "Activity" for "Transactions". Without it a development build warns once
|
|
55
|
+
* when `shortLabel` does not start with `label`'s first word (an abbreviation of that
|
|
56
|
+
* word, "Trans." for "Transactions", counts), because a bar cell that names a page
|
|
57
|
+
* differently from the sidebar, the title and the palette is a page the reader
|
|
58
|
+
* looks for twice. */
|
|
59
|
+
shortLabelIsAlias?: boolean;
|
|
60
|
+
/** 0.49: which entries sit in the phone bar when they do not all fit
|
|
61
|
+
* ({@link AppShellProps.mobileMaxItems}). Lower first: the entries with a priority take
|
|
62
|
+
* the bar's cells in its order, then the rest in the list's order; the bar still shows
|
|
63
|
+
* its entries in the list's order, and the others go under "More". Unset: the list's
|
|
64
|
+
* order decides (as before). */
|
|
65
|
+
mobilePriority?: number;
|
|
53
66
|
/** 0.31.0: leave this entry out of the MOBILE BOTTOM BAR; the sidebar keeps it.
|
|
54
67
|
* For an entry a phone reaches another way — the brand link to the start page —
|
|
55
68
|
* when one cell fewer is what keeps the others' labels whole (the bar divides its
|
|
@@ -65,8 +78,10 @@ export interface AppShellNavItem {
|
|
|
65
78
|
dataTour?: string;
|
|
66
79
|
/** Optional sub-items. When present, hovering (or focusing) the item opens a
|
|
67
80
|
* flyout to the right of the sidebar with a header (this item's label) and the
|
|
68
|
-
* sub-items as icon + title links. Works collapsed or expanded
|
|
69
|
-
*
|
|
81
|
+
* sub-items as icon + title links. Works collapsed or expanded. On the phone (0.49)
|
|
82
|
+
* the bar's cell opens a sheet of them instead of linking to `to` — the sheet lists
|
|
83
|
+
* `to` first as "Overview" when no sub-item is that page — and the pages of the group
|
|
84
|
+
* the reader is in show as the row above the bar (`mobileSubNav`). */
|
|
70
85
|
items?: AppShellSubItem[];
|
|
71
86
|
}
|
|
72
87
|
|
|
@@ -128,22 +143,65 @@ export interface AppShellMoreLabels {
|
|
|
128
143
|
more: string;
|
|
129
144
|
/** The sheet's heading, and so its accessible name. */
|
|
130
145
|
moreTitle: string;
|
|
146
|
+
/** 0.49: a parent entry's own page in the sheet its bar cell opens, above its pages,
|
|
147
|
+
* when no page is that one — "Overview". */
|
|
148
|
+
overview: string;
|
|
131
149
|
}
|
|
132
150
|
|
|
133
151
|
// Defined in ./app-shell-labels, which holds no component code (0.34, KS U8).
|
|
134
152
|
export { DEFAULT_APP_SHELL_MORE_LABELS };
|
|
135
153
|
|
|
136
154
|
/**
|
|
137
|
-
* The most
|
|
138
|
-
*
|
|
139
|
-
*
|
|
155
|
+
* The most cells the phone bar holds, "More" included, per text size (0.49 KIT-6; 0.32
|
|
156
|
+
* §10.7): five at Normal and Large, four at Extra large. Eight entries are four and
|
|
157
|
+
* "More" at Normal; a fifth of a 390 px phone holds one word of the bar's type, and a
|
|
158
|
+
* sixth no longer does once the type grows. A fifth of a 360 px phone at 150 % is 48 px
|
|
159
|
+
* of Normal-sized room.
|
|
160
|
+
*/
|
|
161
|
+
export const DEFAULT_MOBILE_MAX_ITEMS: Readonly<Record<TextSize, number>> = {
|
|
162
|
+
normal: 5,
|
|
163
|
+
large: 5,
|
|
164
|
+
xlarge: 4,
|
|
165
|
+
};
|
|
166
|
+
|
|
167
|
+
/**
|
|
168
|
+
* @deprecated 0.49 — use {@link DEFAULT_MOBILE_MAX_ITEMS} and `AppShell`'s
|
|
169
|
+
* `mobileMaxItems`: the bar is sized in cells, "More" included; removed in 0.51. This is
|
|
170
|
+
* the same default counted as entries before "More" (one fewer).
|
|
140
171
|
*/
|
|
141
172
|
export const DEFAULT_MOBILE_BAR_MAX: Readonly<Record<TextSize, number>> = {
|
|
142
|
-
normal:
|
|
143
|
-
large:
|
|
144
|
-
xlarge:
|
|
173
|
+
normal: DEFAULT_MOBILE_MAX_ITEMS.normal - 1,
|
|
174
|
+
large: DEFAULT_MOBILE_MAX_ITEMS.large - 1,
|
|
175
|
+
xlarge: DEFAULT_MOBILE_MAX_ITEMS.xlarge - 1,
|
|
145
176
|
};
|
|
146
177
|
|
|
178
|
+
/**
|
|
179
|
+
* The phone bar's cells (0.49 KIT-6): every entry when there are at most `maxItems`,
|
|
180
|
+
* else `maxItems − 1` entries and a "More" cell for the rest. Which entries stay is
|
|
181
|
+
* `priority`'s call — lower first, entries without one after them in the list's order —
|
|
182
|
+
* and both halves keep the list's order. Never fewer than one entry beside More.
|
|
183
|
+
*/
|
|
184
|
+
export function pickMobileBar<T>(
|
|
185
|
+
entries: readonly T[],
|
|
186
|
+
maxItems: number,
|
|
187
|
+
priority?: (entry: T) => number | undefined,
|
|
188
|
+
): { shown: T[]; hidden: T[] } {
|
|
189
|
+
const cells = Math.max(1, Math.floor(maxItems));
|
|
190
|
+
if (entries.length <= cells) return { shown: [...entries], hidden: [] };
|
|
191
|
+
const keep = Math.max(1, cells - 1);
|
|
192
|
+
const rank = (i: number) => priority?.(entries[i]!) ?? Number.POSITIVE_INFINITY;
|
|
193
|
+
const chosen = new Set(
|
|
194
|
+
entries
|
|
195
|
+
.map((_, i) => i)
|
|
196
|
+
.sort((a, b) => rank(a) - rank(b) || a - b)
|
|
197
|
+
.slice(0, keep),
|
|
198
|
+
);
|
|
199
|
+
return {
|
|
200
|
+
shown: entries.filter((_, i) => chosen.has(i)),
|
|
201
|
+
hidden: entries.filter((_, i) => !chosen.has(i)),
|
|
202
|
+
};
|
|
203
|
+
}
|
|
204
|
+
|
|
147
205
|
/**
|
|
148
206
|
* Split the bar's entries into the cells shown and the ones under "More", in the app's
|
|
149
207
|
* order (§10.7: the app puts its four most used first, or orders by role). One hidden
|
|
@@ -151,11 +209,87 @@ export const DEFAULT_MOBILE_BAR_MAX: Readonly<Record<TextSize, number>> = {
|
|
|
151
209
|
* all: the cell count is the same either way, and a real entry beats a detour.
|
|
152
210
|
*/
|
|
153
211
|
export function splitMobileBar<T>(entries: readonly T[], max: number): { shown: T[]; hidden: T[] } {
|
|
212
|
+
// Entries before More, as 0.32 counted them; {@link pickMobileBar} counts cells.
|
|
154
213
|
const limit = Math.max(1, Math.floor(max));
|
|
155
214
|
if (entries.length <= limit + 1) return { shown: [...entries], hidden: [] };
|
|
156
215
|
return { shown: entries.slice(0, limit), hidden: entries.slice(limit) };
|
|
157
216
|
}
|
|
158
217
|
|
|
218
|
+
/** The bar's cell count at `size`: `mobileMaxItems`, else the deprecated `mobileBarMax`
|
|
219
|
+
* read as cells (one more), else the default. */
|
|
220
|
+
function mobileCells(
|
|
221
|
+
maxItems: AppShellProps["mobileMaxItems"],
|
|
222
|
+
barMax: AppShellProps["mobileBarMax"],
|
|
223
|
+
size: TextSize,
|
|
224
|
+
): number {
|
|
225
|
+
if (maxItems !== undefined) {
|
|
226
|
+
return typeof maxItems === "number" ? maxItems : (maxItems[size] ?? DEFAULT_MOBILE_MAX_ITEMS[size]);
|
|
227
|
+
}
|
|
228
|
+
if (barMax !== undefined) {
|
|
229
|
+
const entries = typeof barMax === "number" ? barMax : barMax[size];
|
|
230
|
+
if (entries !== undefined) return entries + 1;
|
|
231
|
+
}
|
|
232
|
+
return DEFAULT_MOBILE_MAX_ITEMS[size];
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
/** The kit's dev switch, as `lib/logger.ts` reads it: a development build, never a
|
|
236
|
+
* production one, never under vitest. Read per call so a test can switch it on. */
|
|
237
|
+
function devWarnings(): boolean {
|
|
238
|
+
return Boolean(import.meta.env?.DEV) && !import.meta.env?.VITEST;
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
const warnedNav = new Set<string>();
|
|
242
|
+
|
|
243
|
+
function warnNavOnce(key: string, message: string): void {
|
|
244
|
+
if (warnedNav.has(key)) return;
|
|
245
|
+
warnedNav.add(key);
|
|
246
|
+
console.warn(message);
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
/** The first word of a label, lower-cased: letters and digits up to the first other
|
|
250
|
+
* character ("Import & Export" → "import", "Trans." → "trans"). */
|
|
251
|
+
function firstWord(text: string): string {
|
|
252
|
+
return (text.trim().match(/^[\p{L}\p{N}]+/u)?.[0] ?? "").toLocaleLowerCase();
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
/** Whether `short` names `label`: its first word is `label`'s first word or an
|
|
256
|
+
* abbreviation of it ("Trans." for "Transactions"), or the other way round. */
|
|
257
|
+
function shortLabelNamesLabel(short: string, label: string): boolean {
|
|
258
|
+
const s = firstWord(short);
|
|
259
|
+
const l = firstWord(label);
|
|
260
|
+
return s === "" || l === "" || l.startsWith(s) || s.startsWith(l);
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
const TEXT_SIZE_WORDS: Record<TextSize, string> = { normal: "Normal", large: "Large", xlarge: "Extra large" };
|
|
264
|
+
|
|
265
|
+
/** A development build's notes on `nav` (0.49 KIT-6): a `shortLabel` that is another word
|
|
266
|
+
* than its `label` without `shortLabelIsAlias`, and the deprecated `mobileBarMax`. */
|
|
267
|
+
function useNavDevWarnings(nav: AppShellNavItem[], deprecatedBarMax: boolean): void {
|
|
268
|
+
useEffect(() => {
|
|
269
|
+
if (!devWarnings()) return;
|
|
270
|
+
if (deprecatedBarMax) {
|
|
271
|
+
warnNavOnce(
|
|
272
|
+
"mobileBarMax",
|
|
273
|
+
"[ui-kit] AppShell mobileBarMax is deprecated: use mobileMaxItems, which counts the bar's cells with " +
|
|
274
|
+
'"More" (mobileBarMax={4} is mobileMaxItems={5}). Removed in 0.51.',
|
|
275
|
+
);
|
|
276
|
+
}
|
|
277
|
+
for (const item of nav) {
|
|
278
|
+
if (item.shortLabel === undefined || item.shortLabelIsAlias) continue;
|
|
279
|
+
if (shortLabelNamesLabel(item.shortLabel, item.label)) continue;
|
|
280
|
+
const from = item.shortLabelFrom ?? "normal";
|
|
281
|
+
const where = from === "normal" ? "at every text size" : `from ${TEXT_SIZE_WORDS[from]} text up`;
|
|
282
|
+
warnNavOnce(
|
|
283
|
+
`shortLabel|${item.to}|${item.shortLabel}|${item.label}`,
|
|
284
|
+
`[ui-kit] AppShell nav "${item.to}": shortLabel "${item.shortLabel}" does not start with ` +
|
|
285
|
+
`"${item.label.trim().split(/\s+/)[0]}", the first word of its label "${item.label}". The phone bar ` +
|
|
286
|
+
`shows it ${where}, so the page has another name there than in the sidebar and its title. ` +
|
|
287
|
+
"Shorten the label instead, or pass shortLabelIsAlias: true if the other word is meant.",
|
|
288
|
+
);
|
|
289
|
+
}
|
|
290
|
+
}, [nav, deprecatedBarMax]);
|
|
291
|
+
}
|
|
292
|
+
|
|
159
293
|
/**
|
|
160
294
|
* Exported and `<div>`-shaped. This is the outermost element of every consuming app, so
|
|
161
295
|
* there is nothing above it to hang an id, a landmark label or a `data-tour` anchor on —
|
|
@@ -222,17 +356,23 @@ export interface AppShellProps extends ComponentPropsWithoutRef<"div"> {
|
|
|
222
356
|
*/
|
|
223
357
|
mobileSubNavLayout?: "wrap" | "scroll";
|
|
224
358
|
/**
|
|
225
|
-
* The most
|
|
226
|
-
*
|
|
227
|
-
*
|
|
228
|
-
*
|
|
229
|
-
* accountant see
|
|
359
|
+
* The most cells the phone bar holds, "More" included (0.49 KIT-6). When `nav` (after
|
|
360
|
+
* `mobileHidden`) has more entries, the bar keeps `mobileMaxItems − 1` of them and a
|
|
361
|
+
* last "More" cell opens a sheet with the rest, each group with its pages. Which
|
|
362
|
+
* entries stay is the items' `mobilePriority`, else the list's order (so the app puts
|
|
363
|
+
* its most used entries first, or orders by role: kastlan's manager and accountant see
|
|
364
|
+
* different firsts).
|
|
230
365
|
*
|
|
231
|
-
* Default {@link
|
|
232
|
-
*
|
|
233
|
-
*
|
|
234
|
-
|
|
235
|
-
|
|
366
|
+
* Default {@link DEFAULT_MOBILE_MAX_ITEMS}: 5 at Normal and Large, 4 at Extra large —
|
|
367
|
+
* eight entries are four and "More". A number replaces the default at every size;
|
|
368
|
+
* `{ xlarge: 5 }` replaces it at one size only. Never fewer than one entry beside More.
|
|
369
|
+
*/
|
|
370
|
+
mobileMaxItems?: number | Partial<Record<TextSize, number>>;
|
|
371
|
+
/**
|
|
372
|
+
* @deprecated 0.49 — use `mobileMaxItems`, which counts the bar's cells, "More"
|
|
373
|
+
* included: `mobileBarMax={4}` is `mobileMaxItems={5}`; removed in 0.51. Read as that
|
|
374
|
+
* (one more, per size), with a development warning; ignored when `mobileMaxItems` is
|
|
375
|
+
* given.
|
|
236
376
|
*/
|
|
237
377
|
mobileBarMax?: number | Partial<Record<TextSize, number>>;
|
|
238
378
|
/** The More cell's words over the provider's `appShellMore`. */
|
|
@@ -357,6 +497,7 @@ export function AppShell({
|
|
|
357
497
|
subNav = "flyout",
|
|
358
498
|
mobileSubNav = true,
|
|
359
499
|
mobileSubNavLayout: mobileSubNavLayoutProp,
|
|
500
|
+
mobileMaxItems,
|
|
360
501
|
mobileBarMax,
|
|
361
502
|
moreLabels,
|
|
362
503
|
toggleGroupLabel,
|
|
@@ -370,15 +511,14 @@ export function AppShell({
|
|
|
370
511
|
const large = textSize !== "normal";
|
|
371
512
|
const mobileSubNavLayout = mobileSubNavLayoutProp ?? (large ? "scroll" : "wrap");
|
|
372
513
|
// The bottom bar's cells: every entry the phone does not reach another way, up to the
|
|
373
|
-
// text size's limit; the rest go under More (§10.7).
|
|
374
|
-
const
|
|
375
|
-
|
|
376
|
-
? mobileBarMax
|
|
377
|
-
: (mobileBarMax?.[textSize] ?? DEFAULT_MOBILE_BAR_MAX[textSize]);
|
|
378
|
-
const { shown: barNav, hidden: moreNav } = splitMobileBar(
|
|
514
|
+
// text size's limit, More included; the rest go under More (§10.7, KIT-6).
|
|
515
|
+
const maxItems = mobileCells(mobileMaxItems, mobileBarMax, textSize);
|
|
516
|
+
const { shown: barNav, hidden: moreNav } = pickMobileBar(
|
|
379
517
|
nav.filter((item) => !item.mobileHidden),
|
|
380
|
-
|
|
518
|
+
maxItems,
|
|
519
|
+
(item) => item.mobilePriority,
|
|
381
520
|
);
|
|
521
|
+
useNavDevWarnings(nav, mobileBarMax !== undefined && mobileMaxItems === undefined);
|
|
382
522
|
// An embedded shell sharing the default key would overwrite the outer app's sidebar
|
|
383
523
|
// preference with its own — so it persists only when given a key of its own.
|
|
384
524
|
const collapseStorageKey =
|
|
@@ -552,10 +692,12 @@ export function AppShell({
|
|
|
552
692
|
{/* Below `md` the bottom room is the nav plus the larger of 1.5rem and what the
|
|
553
693
|
floating launchers take above it (`--app-float-clearance`, published by
|
|
554
694
|
every kit FAB, FloatingPanel and FloatingActionGroup while mounted — keksdose
|
|
555
|
-
dev#612: the last row ended under the help launcher).
|
|
695
|
+
dev#612: the last row ended under the help launcher). From `md` up, where
|
|
696
|
+
there is no nav, the launchers' room alone (0.49 KIT-11): the last row
|
|
697
|
+
scrolls clear of a corner button there too. */}
|
|
556
698
|
<main
|
|
557
699
|
className={cn(
|
|
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-
|
|
700
|
+
"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-[var(--app-float-clearance,0px)] md:min-h-0 md:overflow-y-auto md:[scrollbar-gutter:stable_both-edges]",
|
|
559
701
|
// The list page's fill (0.40, docs/page-layout-harmonization.md §1.3): while
|
|
560
702
|
// a `fillHeight` DataTable is inside, a flex column — a banner above the
|
|
561
703
|
// page keeps its own height and the page (a `FillHeight`) takes the rest,
|
|
@@ -564,7 +706,13 @@ export function AppShell({
|
|
|
564
706
|
// into the rest from a zero basis, and a window too short for the page's
|
|
565
707
|
// floor scrolls `<main>`. Every other page lays out as before. Below `md`
|
|
566
708
|
// the document scrolls and nothing fills.
|
|
567
|
-
|
|
709
|
+
// The children's `shrink-0` carries no `:has`: `flex-shrink` does nothing
|
|
710
|
+
// outside a flex container, and a `:has(…) > *` rule cost a style recalc of
|
|
711
|
+
// the whole page on every DOM insertion under it (17–45 ms per scrolled step
|
|
712
|
+
// of a `virtualize` table, 0.48.2). `[&&>*]` doubles the class, so it stays
|
|
713
|
+
// as specific as FillHeight's `has-[…]:flex-1` (whose `flex: 1` would set
|
|
714
|
+
// shrink 1) and, emitted after it, still wins.
|
|
715
|
+
"md:has-[[data-fill-height]]:flex md:has-[[data-fill-height]]:flex-col md:[&&>*]:shrink-0",
|
|
568
716
|
// Embedded there is no document scroll to lean on below `md` either.
|
|
569
717
|
embedded && "min-h-0 overflow-y-auto",
|
|
570
718
|
)}
|
|
@@ -591,9 +739,13 @@ export function AppShell({
|
|
|
591
739
|
className="grid"
|
|
592
740
|
style={{ gridTemplateColumns: `repeat(${barNav.length + (moreNav.length ? 1 : 0)}, minmax(0, 1fr))` }}
|
|
593
741
|
>
|
|
594
|
-
{barNav.map((item) =>
|
|
595
|
-
|
|
596
|
-
|
|
742
|
+
{barNav.map((item) =>
|
|
743
|
+
item.items?.length ? (
|
|
744
|
+
<MobileGroupCell key={item.to} item={item} labels={moreLabels} />
|
|
745
|
+
) : (
|
|
746
|
+
<MobileNavItem key={item.to} item={item} />
|
|
747
|
+
),
|
|
748
|
+
)}
|
|
597
749
|
{moreNav.length > 0 && <MobileMoreCell hidden={moreNav} labels={moreLabels} />}
|
|
598
750
|
</nav>
|
|
599
751
|
</div>
|
|
@@ -773,11 +925,7 @@ function MobileCellLabel({ children }: { children: ReactNode }) {
|
|
|
773
925
|
* nothing marked on every sub-page (the same defect the sidebar had). */
|
|
774
926
|
function MobileNavItem({ item }: { item: AppShellNavItem }) {
|
|
775
927
|
const subActive = useSubItemActive(item);
|
|
776
|
-
const
|
|
777
|
-
const short =
|
|
778
|
-
item.shortLabel !== undefined && TEXT_SIZES.indexOf(size) >= TEXT_SIZES.indexOf(item.shortLabelFrom ?? "normal")
|
|
779
|
-
? item.shortLabel
|
|
780
|
-
: undefined;
|
|
928
|
+
const label = useCellLabel(item);
|
|
781
929
|
return (
|
|
782
930
|
<NavLink
|
|
783
931
|
to={item.to}
|
|
@@ -788,11 +936,122 @@ function MobileNavItem({ item }: { item: AppShellNavItem }) {
|
|
|
788
936
|
<span className="relative">
|
|
789
937
|
<item.icon className="size-5" />
|
|
790
938
|
</span>
|
|
791
|
-
<MobileCellLabel>{
|
|
939
|
+
<MobileCellLabel>{label}</MobileCellLabel>
|
|
940
|
+
</NavLink>
|
|
941
|
+
);
|
|
942
|
+
}
|
|
943
|
+
|
|
944
|
+
/** The bar cell's label: `shortLabel` from its text size up (0.48), else `label`. */
|
|
945
|
+
function useCellLabel(item: AppShellNavItem): string {
|
|
946
|
+
const { size } = useTextSize();
|
|
947
|
+
return item.shortLabel !== undefined &&
|
|
948
|
+
TEXT_SIZES.indexOf(size) >= TEXT_SIZES.indexOf(item.shortLabelFrom ?? "normal")
|
|
949
|
+
? item.shortLabel
|
|
950
|
+
: item.label;
|
|
951
|
+
}
|
|
952
|
+
|
|
953
|
+
/** A link in one of the phone's sheets — an entry (`level="entry"`) or a page under one. */
|
|
954
|
+
function SheetLink({
|
|
955
|
+
to,
|
|
956
|
+
end,
|
|
957
|
+
icon: Icon,
|
|
958
|
+
label,
|
|
959
|
+
onClick,
|
|
960
|
+
level,
|
|
961
|
+
}: {
|
|
962
|
+
to: string;
|
|
963
|
+
end?: boolean;
|
|
964
|
+
icon: LucideIcon;
|
|
965
|
+
label: string;
|
|
966
|
+
onClick: () => void;
|
|
967
|
+
level: "entry" | "page";
|
|
968
|
+
}) {
|
|
969
|
+
return (
|
|
970
|
+
<NavLink
|
|
971
|
+
to={to}
|
|
972
|
+
end={end ?? true}
|
|
973
|
+
onClick={onClick}
|
|
974
|
+
className={({ isActive }) =>
|
|
975
|
+
cn(
|
|
976
|
+
level === "entry"
|
|
977
|
+
? "flex min-h-11 items-center gap-3 rounded-md px-3 py-2 text-sm transition-colors"
|
|
978
|
+
: "flex min-h-11 items-center gap-2.5 rounded-md px-2.5 py-1.5 text-sm transition-colors",
|
|
979
|
+
isActive ? "bg-inverse text-inverse" : level === "entry" ? "text-primary hover:bg-hover" : "text-secondary hover:bg-hover",
|
|
980
|
+
)
|
|
981
|
+
}
|
|
982
|
+
>
|
|
983
|
+
<Icon aria-hidden className={level === "entry" ? "size-5 shrink-0" : "size-4 shrink-0 opacity-70"} />
|
|
984
|
+
<span className="min-w-0 break-words leading-snug">{label}</span>
|
|
792
985
|
</NavLink>
|
|
793
986
|
);
|
|
794
987
|
}
|
|
795
988
|
|
|
989
|
+
/**
|
|
990
|
+
* A bar cell for an entry with pages (0.49 KIT-6): on a phone the entry opens a sheet of
|
|
991
|
+
* its pages instead of linking to its own `to` — a tap on "Reports" used to land on the
|
|
992
|
+
* overview with the pages one more step away, in the row above the bar, which only
|
|
993
|
+
* appears once you are in the group. The sheet lists the entry's own page first, as
|
|
994
|
+
* "Overview", when none of the pages is that page. Marked while the entry's page or one
|
|
995
|
+
* of its pages is current, as the link was.
|
|
996
|
+
*
|
|
997
|
+
* Tours find the entry by `a[href]` or its `dataTour`, as on the More cell: an inert,
|
|
998
|
+
* pointer-transparent anchor over the button carries them (see {@link MobileMoreCell}).
|
|
999
|
+
*/
|
|
1000
|
+
function MobileGroupCell({
|
|
1001
|
+
item,
|
|
1002
|
+
labels: labelsProp,
|
|
1003
|
+
}: {
|
|
1004
|
+
item: AppShellNavItem;
|
|
1005
|
+
labels?: LabelOverride<AppShellMoreLabels>;
|
|
1006
|
+
}) {
|
|
1007
|
+
const labels: AppShellMoreLabels = useKitLabels("appShellMore", DEFAULT_APP_SHELL_MORE_LABELS, labelsProp);
|
|
1008
|
+
const { pathname } = useLocation();
|
|
1009
|
+
const [open, setOpen] = useState(false);
|
|
1010
|
+
const label = useCellLabel(item);
|
|
1011
|
+
const active = entryIsCurrent(item, pathname);
|
|
1012
|
+
const close = () => setOpen(false);
|
|
1013
|
+
const pages = item.items ?? [];
|
|
1014
|
+
const ownPageListed = pages.some((sub) => sub.to === item.to);
|
|
1015
|
+
return (
|
|
1016
|
+
<div data-slot="app-shell-group" className="relative min-w-0">
|
|
1017
|
+
<button
|
|
1018
|
+
type="button"
|
|
1019
|
+
aria-haspopup="dialog"
|
|
1020
|
+
aria-expanded={open}
|
|
1021
|
+
// The cell may show a shortLabel; the button's name is the entry's.
|
|
1022
|
+
aria-label={label === item.label ? undefined : item.label}
|
|
1023
|
+
onClick={() => setOpen(true)}
|
|
1024
|
+
className={mobileCellClass(active)}
|
|
1025
|
+
>
|
|
1026
|
+
<span className="relative">
|
|
1027
|
+
<item.icon aria-hidden className="size-5" />
|
|
1028
|
+
</span>
|
|
1029
|
+
<MobileCellLabel>{label}</MobileCellLabel>
|
|
1030
|
+
</button>
|
|
1031
|
+
<TourAnchor to={item.to} dataTour={item.dataTour} />
|
|
1032
|
+
{pages
|
|
1033
|
+
.filter((sub) => sub.to !== item.to)
|
|
1034
|
+
.map((sub) => (
|
|
1035
|
+
<TourAnchor key={sub.to} to={sub.to} />
|
|
1036
|
+
))}
|
|
1037
|
+
<DialogFrame open={open} onClose={close} title={item.label} closeButton bodyClassName="space-y-1">
|
|
1038
|
+
<ul className="space-y-1">
|
|
1039
|
+
{!ownPageListed && (
|
|
1040
|
+
<li>
|
|
1041
|
+
<SheetLink to={item.to} end={item.end} icon={item.icon} label={labels.overview} onClick={close} level="entry" />
|
|
1042
|
+
</li>
|
|
1043
|
+
)}
|
|
1044
|
+
{pages.map((sub) => (
|
|
1045
|
+
<li key={sub.to}>
|
|
1046
|
+
<SheetLink to={sub.to} end={sub.end} icon={sub.icon} label={sub.label} onClick={close} level="entry" />
|
|
1047
|
+
</li>
|
|
1048
|
+
))}
|
|
1049
|
+
</ul>
|
|
1050
|
+
</DialogFrame>
|
|
1051
|
+
</div>
|
|
1052
|
+
);
|
|
1053
|
+
}
|
|
1054
|
+
|
|
796
1055
|
/** Whether the current route is `item`'s page or one of its pages. */
|
|
797
1056
|
function entryIsCurrent(item: AppShellNavItem, pathname: string): boolean {
|
|
798
1057
|
return (
|
|
@@ -868,42 +1127,12 @@ function MobileMoreCell({
|
|
|
868
1127
|
<ul className="space-y-1">
|
|
869
1128
|
{hidden.map((item) => (
|
|
870
1129
|
<li key={item.to}>
|
|
871
|
-
<
|
|
872
|
-
to={item.to}
|
|
873
|
-
end={item.end ?? true}
|
|
874
|
-
onClick={close}
|
|
875
|
-
className={({ isActive }) =>
|
|
876
|
-
cn(
|
|
877
|
-
"flex min-h-11 items-center gap-3 rounded-md px-3 py-2 text-sm transition-colors",
|
|
878
|
-
isActive
|
|
879
|
-
? "bg-inverse text-inverse"
|
|
880
|
-
: "text-primary hover:bg-hover",
|
|
881
|
-
)
|
|
882
|
-
}
|
|
883
|
-
>
|
|
884
|
-
<item.icon aria-hidden className="size-5 shrink-0" />
|
|
885
|
-
<span className="min-w-0 break-words leading-snug">{item.label}</span>
|
|
886
|
-
</NavLink>
|
|
1130
|
+
<SheetLink to={item.to} end={item.end} icon={item.icon} label={item.label} onClick={close} level="entry" />
|
|
887
1131
|
{!!item.items?.length && (
|
|
888
1132
|
<ul aria-label={item.label} className="ms-[1.3rem] mt-0.5 space-y-0.5 border-s border-subtle ps-2">
|
|
889
1133
|
{item.items.map((sub) => (
|
|
890
1134
|
<li key={sub.to}>
|
|
891
|
-
<
|
|
892
|
-
to={sub.to}
|
|
893
|
-
end={sub.end ?? true}
|
|
894
|
-
onClick={close}
|
|
895
|
-
className={({ isActive }) =>
|
|
896
|
-
cn(
|
|
897
|
-
"flex min-h-11 items-center gap-2.5 rounded-md px-2.5 py-1.5 text-sm transition-colors",
|
|
898
|
-
isActive
|
|
899
|
-
? "bg-inverse text-inverse"
|
|
900
|
-
: "text-secondary hover:bg-hover",
|
|
901
|
-
)
|
|
902
|
-
}
|
|
903
|
-
>
|
|
904
|
-
<sub.icon aria-hidden className="size-4 shrink-0 opacity-70" />
|
|
905
|
-
<span className="min-w-0 break-words leading-snug">{sub.label}</span>
|
|
906
|
-
</NavLink>
|
|
1135
|
+
<SheetLink to={sub.to} end={sub.end} icon={sub.icon} label={sub.label} onClick={close} level="page" />
|
|
907
1136
|
</li>
|
|
908
1137
|
))}
|
|
909
1138
|
</ul>
|
|
@@ -5,6 +5,7 @@ import type { PalettePreset } from "../theme/palette-presets";
|
|
|
5
5
|
import type { ThemeMode } from "../theme/theme-store";
|
|
6
6
|
import { Tooltip } from "../components/tooltip";
|
|
7
7
|
import { DEFAULT_TOP_BAR_LABELS, useKitLabels } from "../i18n/kit-labels";
|
|
8
|
+
import { languageCodeLabel } from "./language-code";
|
|
8
9
|
|
|
9
10
|
/** Shared square icon-button styling for top-bar triggers, so app-owned controls
|
|
10
11
|
* (account/feedback menus) line up with the shared ones. */
|
|
@@ -179,33 +180,51 @@ export interface LanguageOption {
|
|
|
179
180
|
country: string;
|
|
180
181
|
}
|
|
181
182
|
|
|
183
|
+
/** What {@link LanguageMenu}'s trigger shows of the active language (0.49 KIT-10). */
|
|
184
|
+
export type LanguageMenuTrigger = "flag+code" | "code" | "flag";
|
|
185
|
+
|
|
186
|
+
/** The trigger with a code: as tall as the others, as wide as flag and code need. */
|
|
187
|
+
const LANGUAGE_TRIGGER_WIDE_CLASS = TOPBAR_TRIGGER_CLASS.replace("size-9", "h-9 min-w-9 gap-1.5 px-2");
|
|
188
|
+
|
|
189
|
+
const FLAG_CLASS =
|
|
190
|
+
"inline-block h-[0.9375rem] w-5 shrink-0 rounded-sm shadow-[0_0_0_1px_rgba(0,0,0,0.08)]";
|
|
191
|
+
|
|
182
192
|
/** Language switcher: a hover menu of options with flags and a check on the
|
|
183
193
|
* active one. Controlled — the app owns the current code and change handler.
|
|
184
194
|
*
|
|
185
|
-
* The trigger is the *active* option's flag
|
|
195
|
+
* The trigger is the *active* option's flag and its language code in capitals
|
|
196
|
+
* ("DE", "FR", "IT"; `trigger`, 0.49 KIT-10), not an icon: lucide's `Languages`
|
|
186
197
|
* glyph (文A) is the same mark Chrome puts in the omnibox for "translate this
|
|
187
198
|
* page", so it read as machine-translation rather than "pick the UI language"
|
|
188
|
-
* (Keksdose feedback #121).
|
|
189
|
-
*
|
|
190
|
-
*
|
|
199
|
+
* (Keksdose feedback #121). A flag alone could not tell de-CH, fr-CH and it-CH
|
|
200
|
+
* apart — one Swiss flag each — so the code is beside it. It is named "Choose
|
|
201
|
+
* language, current: Deutsch" (`topBar.languageCurrent`). A globe covers the
|
|
202
|
+
* case where `current` matches nothing in `options`. */
|
|
191
203
|
export function LanguageMenu({
|
|
192
204
|
options,
|
|
193
205
|
current,
|
|
194
206
|
onChange,
|
|
195
207
|
"aria-label": ariaLabel,
|
|
196
208
|
heading,
|
|
209
|
+
trigger = "flag+code",
|
|
197
210
|
}: {
|
|
198
211
|
options: LanguageOption[];
|
|
199
212
|
current: string | undefined;
|
|
200
213
|
onChange: (code: string) => void;
|
|
201
|
-
/**
|
|
214
|
+
/** The menu's name, and the trigger's as given. Default: the menu `topBar.language`,
|
|
215
|
+
* the trigger `topBar.languageCurrent(<the active label>)`, from the
|
|
216
|
+
* {@link UiKitProvider}, else English. */
|
|
202
217
|
"aria-label"?: string;
|
|
203
218
|
/** Optional small heading row above the list — as on `TopBarActionMenu`,
|
|
204
219
|
* `OptionSwitcherMenu` and `PaletteMenu`. */
|
|
205
220
|
heading?: string;
|
|
221
|
+
/** What the trigger shows: `"flag+code"` (default), `"code"` or `"flag"`. */
|
|
222
|
+
trigger?: LanguageMenuTrigger;
|
|
206
223
|
}) {
|
|
207
224
|
const labels = useKitLabels("topBar", DEFAULT_TOP_BAR_LABELS, { language: ariaLabel });
|
|
208
225
|
const active = options.find((o) => o.code === current);
|
|
226
|
+
const triggerName = ariaLabel ?? (active ? labels.languageCurrent(active.label) : labels.language);
|
|
227
|
+
const showsCode = active !== undefined && trigger !== "flag";
|
|
209
228
|
return (
|
|
210
229
|
<HoverMenu
|
|
211
230
|
aria-label={labels.language}
|
|
@@ -214,14 +233,18 @@ export function LanguageMenu({
|
|
|
214
233
|
<button
|
|
215
234
|
type="button"
|
|
216
235
|
onClick={toggle}
|
|
217
|
-
aria-label={
|
|
218
|
-
className={TOPBAR_TRIGGER_CLASS}
|
|
236
|
+
aria-label={triggerName}
|
|
237
|
+
className={showsCode ? LANGUAGE_TRIGGER_WIDE_CLASS : TOPBAR_TRIGGER_CLASS}
|
|
219
238
|
>
|
|
220
239
|
{active ? (
|
|
221
|
-
|
|
222
|
-
aria-hidden
|
|
223
|
-
|
|
224
|
-
|
|
240
|
+
<>
|
|
241
|
+
{trigger !== "code" && <span aria-hidden className={`fi fi-${active.country} ${FLAG_CLASS}`} />}
|
|
242
|
+
{showsCode && (
|
|
243
|
+
<span aria-hidden className="text-xs font-semibold tabular-nums tracking-wide">
|
|
244
|
+
{languageCodeLabel(active.code)}
|
|
245
|
+
</span>
|
|
246
|
+
)}
|
|
247
|
+
</>
|
|
225
248
|
) : (
|
|
226
249
|
<Globe className="size-5" />
|
|
227
250
|
)}
|
|
@@ -245,10 +268,7 @@ export function LanguageMenu({
|
|
|
245
268
|
className={TOPBAR_MENU_ITEM_CLASS}
|
|
246
269
|
>
|
|
247
270
|
<span className="flex items-center gap-2">
|
|
248
|
-
<span
|
|
249
|
-
aria-hidden
|
|
250
|
-
className={`fi fi-${lang.country} inline-block h-[0.9375rem] w-5 shrink-0 rounded-sm shadow-[0_0_0_1px_rgba(0,0,0,0.08)]`}
|
|
251
|
-
/>
|
|
271
|
+
<span aria-hidden className={`fi fi-${lang.country} ${FLAG_CLASS}`} />
|
|
252
272
|
{lang.label}
|
|
253
273
|
</span>
|
|
254
274
|
{isActive && <Check className="size-4 text-secondary" />}
|