@eifi1/ui-kit 0.48.2 → 0.50.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 +34 -16
- 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 +24 -12
- package/dist/admin/user-roster.js +21 -6
- 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.d.ts +9 -1
- package/dist/auth/verify-email.js +5 -3
- 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/amount-input.d.ts +2 -1
- package/dist/components/amount-input.js +3 -2
- package/dist/components/amount-input.js.map +1 -1
- package/dist/components/autocomplete.js +1 -0
- package/dist/components/autocomplete.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/combobox-core.js +1 -0
- package/dist/components/combobox-core.js.map +1 -1
- package/dist/components/combobox-suggestions.js +1 -0
- package/dist/components/combobox-suggestions.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/currency-select.d.ts +28 -2
- package/dist/components/currency-select.js +56 -10
- package/dist/components/currency-select.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.js +21 -3
- package/dist/components/data-table-fit.js.map +1 -1
- package/dist/components/data-table-mobile.d.ts +58 -3
- package/dist/components/data-table-mobile.js +147 -16
- 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 +7 -0
- package/dist/components/data-table-row-actions.js.map +1 -1
- package/dist/components/data-table-row.d.ts +1 -1
- package/dist/components/data-table-row.js.map +1 -1
- package/dist/components/data-table.d.ts +34 -1
- package/dist/components/data-table.js +37 -10
- package/dist/components/data-table.js.map +1 -1
- package/dist/components/dropdown.js +1 -0
- package/dist/components/dropdown.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/full-bleed-dialog.js +1 -0
- package/dist/components/full-bleed-dialog.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/modal.js +1 -0
- package/dist/components/modal.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/number-input.d.ts +2 -1
- package/dist/components/number-input.js +6 -2
- package/dist/components/number-input.js.map +1 -1
- package/dist/components/numpad-sheet.js +2 -0
- package/dist/components/numpad-sheet.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/picker-sheet.js +2 -0
- package/dist/components/picker-sheet.js.map +1 -1
- package/dist/components/pie-chart.js +2 -1
- package/dist/components/pie-chart.js.map +1 -1
- package/dist/components/popover.js +1 -0
- package/dist/components/popover.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.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-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/defaults.js +2 -0
- package/dist/i18n/defaults.js.map +1 -1
- package/dist/i18n/german.js +20 -3
- package/dist/i18n/german.js.map +1 -1
- package/dist/i18n/kit-labels.d.ts +34 -1
- package/dist/i18n/kit-labels.js +43 -2
- package/dist/i18n/kit-labels.js.map +1 -1
- package/dist/i18n/locales/en.js +22 -3
- package/dist/i18n/locales/en.js.map +3 -3
- package/dist/i18n/locales/es.js +20 -3
- package/dist/i18n/locales/es.js.map +1 -1
- package/dist/i18n/locales/fr.js +20 -3
- package/dist/i18n/locales/fr.js.map +1 -1
- package/dist/i18n/locales/hu.js +20 -3
- package/dist/i18n/locales/hu.js.map +1 -1
- package/dist/i18n/locales/it.js +20 -3
- package/dist/i18n/locales/it.js.map +1 -1
- package/dist/i18n/locales/zh.js +20 -3
- package/dist/i18n/locales/zh.js.map +1 -1
- package/dist/i18n/review.js +3 -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/data-attributes.d.ts +11 -0
- package/dist/lib/data-attributes.js +10 -0
- package/dist/lib/data-attributes.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 +36 -10
- package/dist/lib/format.js +57 -11
- package/dist/lib/format.js.map +1 -1
- package/dist/lib/minus.d.ts +32 -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 +151 -43
- 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/testing/react.d.ts +25 -0
- package/dist/testing/react.js +19 -0
- package/dist/testing/react.js.map +1 -0
- 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-dock.d.ts +42 -0
- package/dist/tour/tour-dock.js +80 -0
- package/dist/tour/tour-dock.js.map +1 -0
- package/dist/tour/tour-gates.d.ts +71 -0
- package/dist/tour/tour-gates.js +114 -0
- package/dist/tour/tour-gates.js.map +1 -0
- package/dist/tour/tour-guide-button.d.ts +27 -0
- package/dist/tour/tour-guide-button.js +45 -0
- package/dist/tour/tour-guide-button.js.map +1 -0
- package/dist/tour/tour-guide-labels.d.ts +8 -0
- package/dist/tour/tour-guide-labels.js +9 -0
- package/dist/tour/tour-guide-labels.js.map +1 -0
- package/dist/tour/tour-labels.js +9 -1
- package/dist/tour/tour-labels.js.map +1 -1
- package/dist/tour/tour.d.ts +133 -9
- package/dist/tour/tour.js +594 -70
- package/dist/tour/tour.js.map +1 -1
- package/dist/tour/use-tour-url-sync.d.ts +5 -2
- package/dist/tour/use-tour-url-sync.js +7 -1
- package/dist/tour/use-tour-url-sync.js.map +1 -1
- package/dist/tour.d.ts +2 -0
- package/dist/tour.js +3 -0
- package/dist/tour.js.map +1 -1
- package/dist/wizard/stepper-nav.d.ts +1 -1
- package/dist/wizard/stepper-nav.js +18 -5
- package/dist/wizard/stepper-nav.js.map +1 -1
- package/dist/wizard/types.d.ts +11 -0
- package/dist/wizard/types.js.map +1 -1
- package/dist/wizard/use-wizard.js +11 -5
- package/dist/wizard/use-wizard.js.map +1 -1
- package/package.json +9 -1
- package/src/admin/admin-action-confirm.tsx +1 -1
- package/src/admin/invitations-panel.tsx +7 -3
- package/src/admin/user-roster.tsx +41 -14
- 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 +13 -3
- package/src/billing/plan-card.tsx +2 -2
- package/src/components/amount-input.tsx +5 -4
- package/src/components/autocomplete.tsx +2 -0
- 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/combobox-core.tsx +2 -0
- package/src/components/combobox-suggestions.tsx +2 -0
- package/src/components/confirm-context.ts +7 -0
- package/src/components/confirm-dialog.tsx +3 -3
- package/src/components/currency-select.tsx +95 -6
- package/src/components/danger-confirm.tsx +3 -3
- package/src/components/data-table-fit.ts +41 -6
- package/src/components/data-table-mobile.tsx +259 -17
- package/src/components/data-table-row-actions.tsx +9 -1
- package/src/components/data-table-row.tsx +3 -0
- package/src/components/data-table.tsx +91 -19
- package/src/components/dropdown.tsx +2 -0
- 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/full-bleed-dialog.tsx +3 -0
- package/src/components/icon-button.tsx +21 -8
- package/src/components/modal.tsx +3 -0
- package/src/components/nav-pills.tsx +29 -2
- package/src/components/number-input.tsx +8 -3
- package/src/components/numpad-sheet.tsx +4 -0
- package/src/components/page-header-context.ts +8 -0
- package/src/components/page-header.tsx +34 -2
- package/src/components/picker-sheet.tsx +4 -0
- package/src/components/pie-chart.tsx +2 -1
- package/src/components/popover.tsx +2 -0
- 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 +3 -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-dismiss.ts +43 -4
- package/src/hooks/use-document-title.ts +31 -0
- package/src/i18n/defaults.ts +2 -0
- package/src/i18n/german.ts +17 -0
- package/src/i18n/kit-labels.tsx +79 -1
- package/src/i18n/locales/es.ts +17 -0
- package/src/i18n/locales/fr.ts +17 -0
- package/src/i18n/locales/hu.ts +17 -0
- package/src/i18n/locales/it.ts +17 -0
- package/src/i18n/locales/zh.ts +17 -0
- package/src/i18n/review.ts +3 -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/data-attributes.ts +14 -0
- package/src/lib/document-title.ts +41 -0
- package/src/lib/float-clearance.ts +19 -3
- package/src/lib/format.ts +117 -30
- package/src/lib/minus.ts +50 -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 +291 -68
- 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/testing/react.ts +59 -0
- package/src/theme/chart-palette.ts +102 -20
- package/src/theme/palette-presets.ts +121 -84
- package/src/tour/tour-dock.ts +132 -0
- package/src/tour/tour-gates.ts +195 -0
- package/src/tour/tour-guide-button.tsx +64 -0
- package/src/tour/tour-guide-labels.ts +15 -0
- package/src/tour/tour-labels.ts +8 -0
- package/src/tour/tour.tsx +949 -86
- package/src/tour/use-tour-url-sync.ts +14 -4
- package/src/tour.ts +3 -0
- package/src/wizard/stepper-nav.tsx +27 -7
- package/src/wizard/types.ts +12 -0
- package/src/wizard/use-wizard.ts +16 -9
- package/tokens.css +28 -18
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,
|
|
@@ -597,9 +739,13 @@ export function AppShell({
|
|
|
597
739
|
className="grid"
|
|
598
740
|
style={{ gridTemplateColumns: `repeat(${barNav.length + (moreNav.length ? 1 : 0)}, minmax(0, 1fr))` }}
|
|
599
741
|
>
|
|
600
|
-
{barNav.map((item) =>
|
|
601
|
-
|
|
602
|
-
|
|
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
|
+
)}
|
|
603
749
|
{moreNav.length > 0 && <MobileMoreCell hidden={moreNav} labels={moreLabels} />}
|
|
604
750
|
</nav>
|
|
605
751
|
</div>
|
|
@@ -779,11 +925,7 @@ function MobileCellLabel({ children }: { children: ReactNode }) {
|
|
|
779
925
|
* nothing marked on every sub-page (the same defect the sidebar had). */
|
|
780
926
|
function MobileNavItem({ item }: { item: AppShellNavItem }) {
|
|
781
927
|
const subActive = useSubItemActive(item);
|
|
782
|
-
const
|
|
783
|
-
const short =
|
|
784
|
-
item.shortLabel !== undefined && TEXT_SIZES.indexOf(size) >= TEXT_SIZES.indexOf(item.shortLabelFrom ?? "normal")
|
|
785
|
-
? item.shortLabel
|
|
786
|
-
: undefined;
|
|
928
|
+
const label = useCellLabel(item);
|
|
787
929
|
return (
|
|
788
930
|
<NavLink
|
|
789
931
|
to={item.to}
|
|
@@ -794,11 +936,122 @@ function MobileNavItem({ item }: { item: AppShellNavItem }) {
|
|
|
794
936
|
<span className="relative">
|
|
795
937
|
<item.icon className="size-5" />
|
|
796
938
|
</span>
|
|
797
|
-
<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>
|
|
798
985
|
</NavLink>
|
|
799
986
|
);
|
|
800
987
|
}
|
|
801
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
|
+
|
|
802
1055
|
/** Whether the current route is `item`'s page or one of its pages. */
|
|
803
1056
|
function entryIsCurrent(item: AppShellNavItem, pathname: string): boolean {
|
|
804
1057
|
return (
|
|
@@ -874,42 +1127,12 @@ function MobileMoreCell({
|
|
|
874
1127
|
<ul className="space-y-1">
|
|
875
1128
|
{hidden.map((item) => (
|
|
876
1129
|
<li key={item.to}>
|
|
877
|
-
<
|
|
878
|
-
to={item.to}
|
|
879
|
-
end={item.end ?? true}
|
|
880
|
-
onClick={close}
|
|
881
|
-
className={({ isActive }) =>
|
|
882
|
-
cn(
|
|
883
|
-
"flex min-h-11 items-center gap-3 rounded-md px-3 py-2 text-sm transition-colors",
|
|
884
|
-
isActive
|
|
885
|
-
? "bg-inverse text-inverse"
|
|
886
|
-
: "text-primary hover:bg-hover",
|
|
887
|
-
)
|
|
888
|
-
}
|
|
889
|
-
>
|
|
890
|
-
<item.icon aria-hidden className="size-5 shrink-0" />
|
|
891
|
-
<span className="min-w-0 break-words leading-snug">{item.label}</span>
|
|
892
|
-
</NavLink>
|
|
1130
|
+
<SheetLink to={item.to} end={item.end} icon={item.icon} label={item.label} onClick={close} level="entry" />
|
|
893
1131
|
{!!item.items?.length && (
|
|
894
1132
|
<ul aria-label={item.label} className="ms-[1.3rem] mt-0.5 space-y-0.5 border-s border-subtle ps-2">
|
|
895
1133
|
{item.items.map((sub) => (
|
|
896
1134
|
<li key={sub.to}>
|
|
897
|
-
<
|
|
898
|
-
to={sub.to}
|
|
899
|
-
end={sub.end ?? true}
|
|
900
|
-
onClick={close}
|
|
901
|
-
className={({ isActive }) =>
|
|
902
|
-
cn(
|
|
903
|
-
"flex min-h-11 items-center gap-2.5 rounded-md px-2.5 py-1.5 text-sm transition-colors",
|
|
904
|
-
isActive
|
|
905
|
-
? "bg-inverse text-inverse"
|
|
906
|
-
: "text-secondary hover:bg-hover",
|
|
907
|
-
)
|
|
908
|
-
}
|
|
909
|
-
>
|
|
910
|
-
<sub.icon aria-hidden className="size-4 shrink-0 opacity-70" />
|
|
911
|
-
<span className="min-w-0 break-words leading-snug">{sub.label}</span>
|
|
912
|
-
</NavLink>
|
|
1135
|
+
<SheetLink to={sub.to} end={sub.end} icon={sub.icon} label={sub.label} onClick={close} level="page" />
|
|
913
1136
|
</li>
|
|
914
1137
|
))}
|
|
915
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" />}
|
|
@@ -3,16 +3,16 @@
|
|
|
3
3
|
// fails while this is stale.
|
|
4
4
|
|
|
5
5
|
/** Every custom property tokens.css declares, `@theme inline` names and `--app-nav-h`
|
|
6
|
-
* left out (
|
|
6
|
+
* left out (102 names). */
|
|
7
7
|
export const KIT_NAMES =
|
|
8
8
|
"--bg-active --bg-hover --bg-inverse --bg-page --bg-surface --bg-surface-2 --border " +
|
|
9
9
|
"--border-strong --brand --brand-bg --brand-bg-hover --brand-contrast --brand-hover --brand-muted " +
|
|
10
10
|
"--breakpoint-3xl --chart-1 --chart-2 --chart-3 --chart-4 --chart-5 --chart-6 --chart-7 --chart-8 " +
|
|
11
|
-
"--chart-9 --danger --danger-bg --danger-border --danger-border-strong
|
|
12
|
-
"--danger-hover --flyout-from --focus-ring-width --hue-blue --hue-blue-bg
|
|
13
|
-
"--hue-blue-
|
|
14
|
-
"--hue-orange --hue-orange-bg --hue-orange-border --hue-orange-contrast
|
|
15
|
-
"--hue-purple-bg --hue-purple-border --hue-purple-contrast --hue-teal --hue-teal-bg " +
|
|
11
|
+
"--chart-9 --chart-ink --chart-muted --danger --danger-bg --danger-border --danger-border-strong " +
|
|
12
|
+
"--danger-contrast --danger-hover --flyout-from --focus-ring-width --hue-blue --hue-blue-bg " +
|
|
13
|
+
"--hue-blue-border --hue-blue-contrast --hue-indigo --hue-indigo-bg --hue-indigo-border " +
|
|
14
|
+
"--hue-indigo-contrast --hue-orange --hue-orange-bg --hue-orange-border --hue-orange-contrast " +
|
|
15
|
+
"--hue-purple --hue-purple-bg --hue-purple-border --hue-purple-contrast --hue-teal --hue-teal-bg " +
|
|
16
16
|
"--hue-teal-border --hue-teal-contrast --info --info-bg --info-border --info-contrast --media-ink " +
|
|
17
17
|
"--media-scrim --media-scrim-hover --money-expense --money-expense-contrast --money-income " +
|
|
18
18
|
"--money-income-contrast --money-net --money-net-contrast --money-neutral " +
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import { fireEvent, screen, within } from "@testing-library/react";
|
|
2
|
+
|
|
3
|
+
// `@eifi1/ui-kit/testing/react` (0.50) — helpers that drive the kit's components through
|
|
4
|
+
// Testing Library, from an app's own test suite.
|
|
5
|
+
//
|
|
6
|
+
// A sibling of `@eifi1/ui-kit/testing`, not part of it: that entry is pure (it reaches no
|
|
7
|
+
// package, so it runs under node or jsdom alike), and this one needs
|
|
8
|
+
// `@testing-library/react`, an optional peer only a test suite installs. Neither is part of
|
|
9
|
+
// the barrel, so an app's production bundle never carries either.
|
|
10
|
+
|
|
11
|
+
/** The words of the row's "⋯" button, when the app's tests render translated kit labels. */
|
|
12
|
+
export interface RowMenuLabels {
|
|
13
|
+
/** The button's name on a row with no `name`. Default "Actions". */
|
|
14
|
+
actions?: string;
|
|
15
|
+
/** The button's name given the row's. Default `Actions for ${name}`. */
|
|
16
|
+
actionsFor?: (name: string) => string;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
// The kit's English defaults (DEFAULT_ROW_ACTIONS_LABELS, and the user roster's), written
|
|
20
|
+
// out rather than imported: the label module's type import would walk this entry into the
|
|
21
|
+
// components. src/testing/__tests__/react.test.tsx holds them equal.
|
|
22
|
+
const ACTIONS = "Actions";
|
|
23
|
+
const ACTIONS_FOR = (name: string) => `Actions for ${name}`;
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Open a row's "⋯" menu and find one of its actions — the entry, unclicked, so a test can
|
|
27
|
+
* check it (locked, described) before it clicks it.
|
|
28
|
+
*
|
|
29
|
+
* `row` is the row's name as the app gave it to `RowActions` / `rowActionsColumn` /
|
|
30
|
+
* `UserRoster` ("Actions for <row>"); `undefined` takes the FIRST "⋯" on the page, named
|
|
31
|
+
* or not. Since 0.49 (KIT-12) a `tone: "danger"` action lives in that menu rather than as
|
|
32
|
+
* an icon in the row, and at Large text every action does.
|
|
33
|
+
*
|
|
34
|
+
* The entries are BUTTONS in a dialog popover named like the "⋯" button — not `menuitem`s:
|
|
35
|
+
* `getByRole("menuitem")` finds nothing there. A danger action asks for confirmation once
|
|
36
|
+
* clicked (an `alertdialog`), unless the app turned that off.
|
|
37
|
+
*
|
|
38
|
+
* @example
|
|
39
|
+
* fireEvent.click(await rowMenuAction("Ada Example", "Delete"));
|
|
40
|
+
* fireEvent.click(await screen.findByRole("button", { name: "Confirm" }));
|
|
41
|
+
*/
|
|
42
|
+
export async function rowMenuAction(
|
|
43
|
+
row: string | undefined,
|
|
44
|
+
action: string | RegExp,
|
|
45
|
+
labels: RowMenuLabels = {},
|
|
46
|
+
): Promise<HTMLElement> {
|
|
47
|
+
const actions = labels.actions ?? ACTIONS;
|
|
48
|
+
const actionsFor = labels.actionsFor ?? ACTIONS_FOR;
|
|
49
|
+
// Any row's name: the words around the name, whatever the language puts first.
|
|
50
|
+
const [before, after] = actionsFor("\u0000").split("\u0000");
|
|
51
|
+
const anyRow = (name: string) =>
|
|
52
|
+
name === actions ||
|
|
53
|
+
(name.length > before.length + after.length && name.startsWith(before) && name.endsWith(after));
|
|
54
|
+
const trigger = (await screen.findAllByRole("button", { name: row === undefined ? anyRow : actionsFor(row) }))[0];
|
|
55
|
+
fireEvent.click(trigger);
|
|
56
|
+
const title = trigger.getAttribute("aria-label") ?? trigger.textContent ?? "";
|
|
57
|
+
const menu = await screen.findByRole("dialog", { name: title });
|
|
58
|
+
return within(menu).getByRole("button", { name: action });
|
|
59
|
+
}
|