@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
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/settings/settings-layout.tsx"],"sourcesContent":["import { useCallback, useEffect, useId, useMemo, useRef, useState } from \"react\";\nimport type { ReactElement, ReactNode } from \"react\";\nimport { Link, useHref, useLocation } from \"react-router\";\nimport { ChevronLeft, ChevronRight } from \"lucide-react\";\nimport { Button } from \"../components/button\";\nimport { Card } from \"../components/card\";\nimport { EmptyState } from \"../components/empty-state\";\nimport { Tabs } from \"../components/tabs\";\nimport { FOCUS_RING } from \"../components/focus-ring\";\nimport { Chip } from \"../components/chip\";\nimport { List, ListItem } from \"../components/list\";\nimport type { ListItemLinkProps } from \"../components/list\";\nimport { PageHeader } from \"../components/page-header\";\nimport { SearchField } from \"../components/search-field\";\nimport { cn } from \"../lib/cn\";\nimport { useKitLabels, type LabelOverride } from \"../i18n/kit-labels\";\nimport { createSearchIndex } from \"../search/search-index\";\nimport type { SearchEntry } from \"../search/search-index\";\nimport {\n normalizeSettingsBase,\n settingsHref,\n settingsKeywords,\n visibleSettingsEntries,\n} from \"./settings-catalogue\";\nimport type { SettingsEntry, SettingsGroup } from \"./settings-catalogue\";\nimport { SettingsLayoutContext } from \"./settings-context\";\nimport type { SettingsLayoutContextValue } from \"./settings-context\";\nimport { SettingsHeadingLevel } from \"./settings-heading\";\nimport { DEFAULT_SETTINGS_LABELS } from \"./settings-labels\";\nimport type { SettingsLabels } from \"./settings-labels\";\nimport { settingsFromListState, useSettingsRoute } from \"./use-settings-route\";\nimport type { SettingsLayoutMode } from \"./use-settings-route\";\nimport { useSettingsFocus } from \"./use-settings-focus\";\n\n/** The page's width cap (§3.2): `6xl` for settings, `7xl` for keksdose's admin, whose\n * user table scrolled sideways at 6xl (dev#488). */\nexport type SettingsLayoutWidth = \"6xl\" | \"7xl\";\n\n/** More entries than this and the search field shows by itself (§3.4): Kurvenschmiede has\n * about twelve, keksdose 26; a page of six needs none. */\nexport const SETTINGS_SEARCH_THRESHOLD = 8;\n\n/** The desktop sidebar's column: sticky beside a long group (§3.2). */\nconst SIDEBAR_COLUMN = \"mb-4 md:sticky md:top-6 md:mb-0 md:self-start\";\n\nexport interface SettingsLayoutProps<G extends string> {\n /** The page's `h1`: \"Settings\", \"Administration\". Default: `labels.title`. */\n title?: ReactNode;\n /** The groups, in order (§4.2). */\n groups: readonly SettingsGroup<G>[];\n /** One entry per setting (§4.1). Hidden entries leave search; a group left without a\n * visible one is hidden. */\n entries: readonly SettingsEntry<G>[];\n /** `/settings` or `/admin`. The layout owns every path below it. */\n basePath: string;\n /** The group `/settings` opens on a desktop. Default: the first visible one. */\n defaultGroup?: G;\n /** Retired group ids and their successors, replaced in the address (§3.1). */\n aliases?: Readonly<Partial<Record<string, G>>>;\n /**\n * The search field. Left out, it shows once more than {@link SETTINGS_SEARCH_THRESHOLD}\n * entries are visible; `true` or `false` wins over the count — keksdose's admin keeps\n * none (§3.4, §10.3). ⌘K gets the entries either way ({@link settingsSearchEntries}).\n */\n search?: boolean;\n /** See {@link SettingsLayoutWidth}. Default `\"6xl\"`. */\n width?: SettingsLayoutWidth;\n /** The group's cards, in one column. `sub` is the path segment after the group, which\n * a card owns (`useSettingsLayout().selectSub`). */\n renderGroup: (group: G, sub: string | null) => ReactNode;\n /**\n * Rows under the phone's group list that LEAVE settings — kastlan's \"Administration\",\n * its import page (§3.3, §5). `ListItem`s with an `href`; the layout puts them in a list\n * of their own, so they never read as a group. Not shown on a desktop, where the app's\n * own navigation carries them.\n */\n phoneFooter?: ReactNode;\n /**\n * 0.42.0 (docs/admin-harmonization.md §2.5 decision 16): content under the section\n * list on a desktop, in the same sticky column — the admin area's Tools list\n * (`AdminToolLinks`). Not shown on a phone, where `phoneFooter` carries the same links.\n */\n sidebarFooter?: ReactNode;\n /** Prop > `<UiKitProvider labels={{ settings }}>` > English. */\n labels?: LabelOverride<SettingsLabels>;\n /** See {@link SettingsLayoutMode}. Default `\"auto\"`: the screen decides. */\n layout?: SettingsLayoutMode;\n /**\n * The app's name, the last part of the window's title: \"Security · Settings ·\n * Kastlan\" (§3.1). Left out, the title ends at the page: \"Security · Settings\".\n */\n appName?: string;\n /**\n * The window's title (`document.title`, §3.1) — round 0.34 C8, KS U7. By default the\n * layout writes \"<group> · <page> · <appName>\" while a group is open and\n * \"<page> · <appName>\" on the phone's list, and puts back the title it found when it\n * unmounts. `<page>` is `title` when that is a string, else `labels.title`.\n *\n * A function writes another order or another separator from the same parts; `false`\n * leaves `document.title` to the app. Kastlan's `SettingsChrome` and Kurvenschmiede's\n * `GroupTitle` were this effect written by hand, and Kurvenschmiede's admin page, on\n * the same layout, had none — every admin section was titled \"Kurvenschmiede\".\n */\n documentTitle?: false | ((parts: SettingsDocumentTitleParts) => string);\n className?: string;\n}\n\n/** What {@link SettingsLayoutProps.documentTitle} is given to write the window's title. */\nexport interface SettingsDocumentTitleParts {\n /** The open group's name, or `null` on the phone's list of groups. */\n group: string | null;\n /** The page: `title` when it is a string, else `labels.title` (\"Settings\"). */\n page: string;\n /** The `appName` prop, or `undefined`. */\n appName: string | undefined;\n}\n\n/** \"Security · Settings · Kastlan\" (§3.1); a part that is missing is left out. */\nfunction defaultDocumentTitle({ group, page, appName }: SettingsDocumentTitleParts): string {\n return [group, page, appName].filter((part): part is string => Boolean(part)).join(\" · \");\n}\n\ninterface Searchable<G extends string> extends SearchEntry {\n anchor: string;\n settingsGroup: G;\n}\n\n/**\n * The settings page (docs/settings-harmonization.md §3), and the admin page built the\n * same way (§5). keksdose's page is the reference for the desktop; the phone pattern and\n * the path segments are new for all three apps.\n *\n * **Desktop** (§3.2): the page's `PageHeader`, the search field, then a 14rem sidebar —\n * the kit's vertical `Tabs`, a link per group, sticky so a long group keeps the list in\n * view — beside the open group: its heading (`h2`), its help line and its cards in ONE\n * column, never a masonry (keksdose dev#490/#493).\n *\n * **Phone** (§3.3, below `md`): a drill-down. `/settings` is a list of the groups (icon,\n * name, help line, chevron) with `phoneFooter` under it; a group is a page of its own with\n * a back link, its name as the `h1` and the cards under it. It replaces keksdose's chip\n * strip.\n *\n * **Routes, history and `?focus=`** are {@link useSettingsRoute}'s and\n * {@link useSettingsFocus}'s. **Search** (§3.4) runs the kit's matcher over the catalogue:\n * accents folded, ranked, over the title, the group's name and the keywords. On a desktop\n * the sidebar dims the groups without hits and counts the others' while the hits replace\n * the group; on a phone they replace the list. A hit is a link to its card.\n *\n * The cards inside read {@link useSettingsLayout} for `selectSub`, and their titles are\n * headings under the group's ({@link SettingsHeadingLevel}).\n */\nexport function SettingsLayout<G extends string>({\n title,\n groups,\n entries,\n basePath,\n defaultGroup,\n aliases,\n search,\n width = \"6xl\",\n renderGroup,\n phoneFooter,\n sidebarFooter,\n labels: labelsProp,\n layout = \"auto\",\n appName,\n documentTitle,\n className,\n}: SettingsLayoutProps<G>): ReactElement {\n const labels = useKitLabels(\"settings\", DEFAULT_SETTINGS_LABELS, labelsProp);\n const base = normalizeSettingsBase(basePath);\n const route = useSettingsRoute({ groups, entries, basePath: base, defaultGroup, aliases, layout });\n const location = useLocation();\n // The browser URL of an app path, for the sidebar's real links (a middle click opens a\n // tab): `#/settings/…` under a hash router, the basename in front under a browser one.\n const root = useHref(\"/\").replace(/\\/$/, \"\");\n const panelId = useId();\n const searchRef = useRef<HTMLInputElement>(null);\n const [query, setQuery] = useState(\"\");\n\n const visibleEntries = useMemo(() => visibleSettingsEntries(groups, entries), [groups, entries]);\n const showSearch = search ?? visibleEntries.length > SETTINGS_SEARCH_THRESHOLD;\n const trimmed = query.trim();\n const searching = showSearch && trimmed !== \"\";\n\n const groupById = useMemo(() => new Map(route.groups.map((g) => [g.id, g])), [route.groups]);\n const index = useMemo(\n () =>\n createSearchIndex<Searchable<G>>(\n visibleEntries.map((e) => ({\n id: e.id,\n title: e.title,\n keywords: [groupById.get(e.group)?.title ?? e.group, ...settingsKeywords(e.keywords)],\n group: e.group,\n anchor: e.anchor,\n settingsGroup: e.group,\n })),\n // Every hit: the list is the page's whole answer, not a palette's top few.\n { groupLimit: Infinity },\n ),\n [visibleEntries, groupById],\n );\n const hits = useMemo(() => (searching ? index.search(trimmed).map((h) => h.entry) : []), [searching, index, trimmed]);\n const hitsByGroup = useMemo(() => {\n const counts = new Map<G, number>();\n for (const hit of hits) counts.set(hit.settingsGroup, (counts.get(hit.settingsGroup) ?? 0) + 1);\n return counts;\n }, [hits]);\n\n const anchors = useMemo(() => new Set(entries.map((e) => e.anchor)), [entries]);\n const { phone, group, sub, selectSub, select } = route;\n const headingLevel = phone ? \"h2\" : \"h3\";\n const context = useMemo<SettingsLayoutContextValue>(\n () => ({\n basePath: base,\n group,\n sub,\n selectSub,\n headingLevel,\n hasAnchor: (anchor) => anchors.has(anchor),\n }),\n [base, group, sub, selectSub, headingLevel, anchors],\n );\n\n // Not while the address is about to be replaced (its removal of `focus` would undo the\n // replace), not on the list, and not while the hits stand where the cards would be (a\n // phone's group page has no search: a query left from its list does not count there).\n const hitsShown = searching && (!phone || group === null);\n useSettingsFocus(route.redirecting || group === null || hitsShown ? null : route.focus);\n\n // A link inside the page. On a phone it opens a page of its own — a push that marks the\n // list as the entry before it, so the group page's back is a real back. On a desktop\n // it switches the group in place — a replace.\n const { state } = location;\n const renderLink = useCallback(\n ({ href, ...props }: ListItemLinkProps) => (\n <Link to={href} replace={!phone} state={phone ? settingsFromListState() : state} {...props} />\n ),\n [phone, state],\n );\n\n const clearSearch = () => {\n setQuery(\"\");\n // The button that was pressed goes with the empty state; the field is where the\n // next query is typed.\n searchRef.current?.focus();\n };\n\n const pageTitle = title ?? labels.title;\n\n // The window's title (§3.1). Computed to a string during render so the effect below\n // runs when the words change, not whenever an inline `documentTitle` function is new.\n // Each run puts back the title it found before writing its own, so a group switch\n // ends where it began and the unmount leaves the title the page had before settings.\n const windowTitle =\n documentTitle === false\n ? null\n : (documentTitle ?? defaultDocumentTitle)({\n group: group === null ? null : (groupById.get(group)?.title ?? null),\n page: typeof title === \"string\" ? title : labels.title,\n appName,\n });\n useEffect(() => {\n if (windowTitle === null) return;\n const before = document.title;\n document.title = windowTitle;\n return () => {\n document.title = before;\n };\n }, [windowTitle]);\n\n const pageClass = cn(\"mx-auto w-full space-y-4 p-4 md:p-6\", width === \"7xl\" ? \"max-w-7xl\" : \"max-w-6xl\", className);\n\n const searchField = showSearch ? (\n <SearchField\n ref={searchRef}\n className=\"max-w-md\"\n value={query}\n onChange={setQuery}\n aria-label={labels.search}\n placeholder={labels.searchPlaceholder}\n />\n ) : null;\n\n const results =\n hits.length === 0 ? (\n <EmptyState\n title={labels.noMatches(trimmed)}\n action={\n <Button variant=\"secondary\" size=\"sm\" onClick={clearSearch}>\n {labels.clearSearch}\n </Button>\n }\n />\n ) : (\n <Card className=\"p-1\">\n <List separator=\"divider\" density=\"comfortable\" aria-label={labels.results}>\n {hits.map((hit) => (\n <ListItem\n key={hit.id}\n title={hit.title}\n trailing={groupById.get(hit.settingsGroup)?.title}\n href={settingsHref(base, hit.settingsGroup, hit.anchor)}\n renderLink={renderLink}\n // Leave search on the way: the hit's card is what the user asked for.\n onClick={() => setQuery(\"\")}\n />\n ))}\n </List>\n </Card>\n );\n\n const cards = (open: G) => (\n <SettingsHeadingLevel level={headingLevel}>\n <div className=\"space-y-4\">{renderGroup(open, sub)}</div>\n </SettingsHeadingLevel>\n );\n\n let body: ReactNode;\n if (phone) {\n const open = group === null ? undefined : groupById.get(group);\n body = open ? (\n <div className={pageClass} data-settings-view=\"group\">\n <PageHeader\n size=\"compact\"\n title={open.title}\n description={open.help}\n breadcrumbs={\n <Link\n to={base || \"/\"}\n aria-label={labels.back}\n onClick={(e) => {\n // A real link for a middle click; a plain click is the page's own back,\n // which is `navigate(-1)` when the list is the entry before this one.\n if (e.button !== 0 || e.metaKey || e.ctrlKey || e.shiftKey || e.altKey) return;\n e.preventDefault();\n route.back();\n }}\n className={cn(\"-ms-1 inline-flex items-center gap-0.5 self-start rounded-md pe-1 text-sm font-medium text-muted hover:text-primary\", FOCUS_RING)}\n >\n <ChevronLeft aria-hidden className=\"size-4 rtl:-scale-x-100\" />\n {pageTitle}\n </Link>\n }\n />\n {cards(open.id)}\n </div>\n ) : (\n <div className={pageClass} data-settings-view=\"list\">\n <PageHeader size=\"compact\" title={pageTitle} />\n {searchField}\n {searching ? (\n results\n ) : (\n <Card className=\"p-1\">\n <List separator=\"divider\" density=\"comfortable\" aria-label={labels.sections}>\n {route.groups.map((g) => (\n <ListItem\n key={g.id}\n href={settingsHref(base, g.id)}\n renderLink={renderLink}\n leading={\n <span\n aria-hidden\n className=\"flex size-8 shrink-0 items-center justify-center rounded-md bg-surface-2 text-secondary [&_svg]:size-4\"\n >\n {g.icon}\n </span>\n }\n title={g.title}\n subtitle={g.help}\n trailing={\n g.badge == null ? (\n <ChevronRight aria-hidden className=\"size-4 rtl:-scale-x-100\" />\n ) : (\n // The group's badge (0.48) before the chevron, the two as one end.\n <span className=\"flex items-center gap-2\">\n {g.badge}\n <ChevronRight aria-hidden className=\"size-4 rtl:-scale-x-100\" />\n </span>\n )\n }\n />\n ))}\n </List>\n </Card>\n )}\n {!searching && phoneFooter != null && (\n <Card className=\"p-1\">\n <List separator=\"divider\" density=\"comfortable\">\n {phoneFooter}\n </List>\n </Card>\n )}\n </div>\n );\n } else {\n const open = group === null ? undefined : groupById.get(group);\n // The section list alone, or (0.42) with the app's `sidebarFooter` under it in one\n // sticky column.\n const withSidebarFooter = (tabs: ReactNode) =>\n sidebarFooter == null ? (\n tabs\n ) : (\n <div className={cn(SIDEBAR_COLUMN, \"space-y-6\")}>\n {tabs}\n <div data-slot=\"settings-sidebar-footer\">{sidebarFooter}</div>\n </div>\n );\n body = (\n <div className={pageClass} data-settings-view=\"desktop\">\n <PageHeader size=\"compact\" title={pageTitle} />\n {searchField}\n <div className=\"md:grid md:grid-cols-[14rem_minmax(0,1fr)] md:gap-6\">\n {withSidebarFooter(\n <Tabs<G>\n orientation=\"vertical\"\n aria-label={labels.sections}\n // Sticky (new in the harmonisation, §3.2): keksdose's notifications group\n // scrolls for screens, and the list should stay in view beside it. With a\n // `sidebarFooter` the column round both is the sticky one.\n className={sidebarFooter == null ? SIDEBAR_COLUMN : undefined}\n panelId={open && !searching ? panelId : undefined}\n tabs={route.groups.map((g) => {\n const count = hitsByGroup.get(g.id) ?? 0;\n // While searching, the groups with nothing to show step back (keksdose).\n const dim = searching && count === 0;\n return {\n id: g.id,\n href: `${root}${settingsHref(base, g.id)}`,\n name: g.title,\n label: <span className={cn(dim && \"opacity-40\")}>{g.title}</span>,\n icon: <span className={cn(\"inline-flex [&_svg]:size-4\", dim && \"opacity-40\")}>{g.icon}</span>,\n // While searching, the hit count; otherwise the group's own badge (0.48).\n badge: searching ? (\n count > 0 ? (\n <Chip size=\"sm\" tone=\"brand\">\n <span aria-hidden>{count}</span>\n <span className=\"sr-only-fixed\">{labels.matchCount(count)}</span>\n </Chip>\n ) : undefined\n ) : (\n g.badge\n ),\n };\n })}\n active={(open?.id ?? route.groups[0]?.id) as G}\n onChange={(id) => {\n // While searching the hits stand where the group would; a click on a group\n // means \"go there\", so the search makes way (keksdose).\n setQuery(\"\");\n select(id);\n }}\n />,\n )}\n <div className=\"min-w-0 space-y-4\">\n {searching ? (\n results\n ) : open ? (\n <div\n id={panelId}\n role=\"tabpanel\"\n aria-labelledby={`${panelId}-tab`}\n className=\"space-y-4\"\n >\n <PageHeader as=\"h2\" size=\"sm\" title={open.title} description={open.help} />\n {cards(open.id)}\n </div>\n ) : null}\n </div>\n </div>\n </div>\n );\n }\n\n return <SettingsLayoutContext.Provider value={context}>{body}</SettingsLayoutContext.Provider>;\n}\n"],"mappings":"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","names":[]}
|
|
1
|
+
{"version":3,"sources":["../../src/settings/settings-layout.tsx"],"sourcesContent":["import { useCallback, useEffect, useId, useMemo, useRef, useState } from \"react\";\nimport type { ReactElement, ReactNode } from \"react\";\nimport { Link, useHref, useLocation } from \"react-router\";\nimport { ChevronLeft, ChevronRight } from \"lucide-react\";\nimport { Button } from \"../components/button\";\nimport { Card } from \"../components/card\";\nimport { EmptyState } from \"../components/empty-state\";\nimport { Tabs } from \"../components/tabs\";\nimport { FOCUS_RING } from \"../components/focus-ring\";\nimport { Chip } from \"../components/chip\";\nimport { List, ListItem } from \"../components/list\";\nimport type { ListItemLinkProps } from \"../components/list\";\nimport { PageHeader } from \"../components/page-header\";\nimport { SearchField } from \"../components/search-field\";\nimport { cn } from \"../lib/cn\";\nimport { useKitLabels, type LabelOverride } from \"../i18n/kit-labels\";\nimport { createSearchIndex } from \"../search/search-index\";\nimport type { SearchEntry } from \"../search/search-index\";\nimport {\n normalizeSettingsBase,\n settingsHref,\n settingsKeywords,\n visibleSettingsEntries,\n} from \"./settings-catalogue\";\nimport type { SettingsEntry, SettingsGroup } from \"./settings-catalogue\";\nimport { SettingsLayoutContext } from \"./settings-context\";\nimport type { SettingsLayoutContextValue } from \"./settings-context\";\nimport { SettingsHeadingLevel } from \"./settings-heading\";\nimport { DEFAULT_SETTINGS_LABELS } from \"./settings-labels\";\nimport type { SettingsLabels } from \"./settings-labels\";\nimport { settingsFromListState, useSettingsRoute } from \"./use-settings-route\";\nimport type { SettingsLayoutMode } from \"./use-settings-route\";\nimport { useSettingsFocus } from \"./use-settings-focus\";\n\n/** The page's width cap (§3.2): `6xl` for settings, `7xl` for keksdose's admin, whose\n * user table scrolled sideways at 6xl (dev#488). */\nexport type SettingsLayoutWidth = \"6xl\" | \"7xl\";\n\n/** More entries than this and the search field shows by itself (§3.4): Kurvenschmiede has\n * about twelve, keksdose 26; a page of six needs none. */\nexport const SETTINGS_SEARCH_THRESHOLD = 8;\n\n/** The desktop sidebar's column: sticky beside a long group (§3.2). */\nconst SIDEBAR_COLUMN = \"mb-4 md:sticky md:top-6 md:mb-0 md:self-start\";\n\nexport interface SettingsLayoutProps<G extends string> {\n /** The page's `h1`: \"Settings\", \"Administration\". Default: `labels.title`. */\n title?: ReactNode;\n /** The groups, in order (§4.2). */\n groups: readonly SettingsGroup<G>[];\n /** One entry per setting (§4.1). Hidden entries leave search; a group left without a\n * visible one is hidden. */\n entries: readonly SettingsEntry<G>[];\n /** `/settings` or `/admin`. The layout owns every path below it. */\n basePath: string;\n /** The group `/settings` opens on a desktop. Default: the first visible one. */\n defaultGroup?: G;\n /** Retired group ids and their successors, replaced in the address (§3.1). */\n aliases?: Readonly<Partial<Record<string, G>>>;\n /**\n * The search field. Left out, it shows once more than {@link SETTINGS_SEARCH_THRESHOLD}\n * entries are visible; `true` or `false` wins over the count — keksdose's admin keeps\n * none (§3.4, §10.3). ⌘K gets the entries either way ({@link settingsSearchEntries}).\n */\n search?: boolean;\n /** See {@link SettingsLayoutWidth}. Default `\"6xl\"`. */\n width?: SettingsLayoutWidth;\n /** The group's cards, in one column. `sub` is the path segment after the group, which\n * a card owns (`useSettingsLayout().selectSub`). */\n renderGroup: (group: G, sub: string | null) => ReactNode;\n /**\n * Rows under the phone's group list that LEAVE settings — kastlan's \"Administration\",\n * its import page (§3.3, §5). `ListItem`s with an `href`; the layout puts them in a list\n * of their own, so they never read as a group. Not shown on a desktop, where the app's\n * own navigation carries them.\n */\n phoneFooter?: ReactNode;\n /**\n * 0.42.0 (docs/admin-harmonization.md §2.5 decision 16): content under the section\n * list on a desktop, in the same sticky column — the admin area's Tools list\n * (`AdminToolLinks`). Not shown on a phone, where `phoneFooter` carries the same links.\n */\n sidebarFooter?: ReactNode;\n /** Prop > `<UiKitProvider labels={{ settings }}>` > English. */\n labels?: LabelOverride<SettingsLabels>;\n /** See {@link SettingsLayoutMode}. Default `\"auto\"`: the screen decides. */\n layout?: SettingsLayoutMode;\n /**\n * The app's name, the last part of the window's title: \"Security · Settings ·\n * Kastlan\" (§3.1). Left out, the title ends at the page: \"Security · Settings\".\n */\n appName?: string;\n /**\n * The window's title (`document.title`, §3.1) — round 0.34 C8, KS U7. By default the\n * layout writes \"<group> · <page> · <appName>\" while a group is open and\n * \"<page> · <appName>\" on the phone's list, and puts back the title it found when it\n * unmounts. `<page>` is `title` when that is a string, else `labels.title`.\n *\n * A function writes another order or another separator from the same parts; `false`\n * leaves `document.title` to the app. Kastlan's `SettingsChrome` and Kurvenschmiede's\n * `GroupTitle` were this effect written by hand, and Kurvenschmiede's admin page, on\n * the same layout, had none — every admin section was titled \"Kurvenschmiede\".\n */\n documentTitle?: false | ((parts: SettingsDocumentTitleParts) => string);\n className?: string;\n}\n\n/** What {@link SettingsLayoutProps.documentTitle} is given to write the window's title. */\nexport interface SettingsDocumentTitleParts {\n /** The open group's name, or `null` on the phone's list of groups. */\n group: string | null;\n /** The page: `title` when it is a string, else `labels.title` (\"Settings\"). */\n page: string;\n /** The `appName` prop, or `undefined`. */\n appName: string | undefined;\n}\n\n/** \"Security · Settings · Kastlan\" (§3.1); a part that is missing is left out. */\nfunction defaultDocumentTitle({ group, page, appName }: SettingsDocumentTitleParts): string {\n return [group, page, appName].filter((part): part is string => Boolean(part)).join(\" · \");\n}\n\ninterface Searchable<G extends string> extends SearchEntry {\n anchor: string;\n settingsGroup: G;\n}\n\n/**\n * The settings page (docs/settings-harmonization.md §3), and the admin page built the\n * same way (§5). keksdose's page is the reference for the desktop; the phone pattern and\n * the path segments are new for all three apps.\n *\n * **Desktop** (§3.2): the page's `PageHeader`, the search field, then a 14rem sidebar —\n * the kit's vertical `Tabs`, a link per group, sticky so a long group keeps the list in\n * view — beside the open group: its heading (`h2`), its help line and its cards in ONE\n * column, never a masonry (keksdose dev#490/#493).\n *\n * **Phone** (§3.3, below `md`): a drill-down. `/settings` is a list of the groups (icon,\n * name, help line, chevron) with `phoneFooter` under it; a group is a page of its own with\n * a back link, its name as the `h1` and the cards under it. It replaces keksdose's chip\n * strip.\n *\n * **Routes, history and `?focus=`** are {@link useSettingsRoute}'s and\n * {@link useSettingsFocus}'s. **Search** (§3.4) runs the kit's matcher over the catalogue:\n * accents folded, ranked, over the title, the group's name and the keywords. On a desktop\n * the sidebar dims the groups without hits and counts the others' while the hits replace\n * the group; on a phone they replace the list. A hit is a link to its card.\n *\n * The cards inside read {@link useSettingsLayout} for `selectSub`, and their titles are\n * headings under the group's ({@link SettingsHeadingLevel}).\n */\nexport function SettingsLayout<G extends string>({\n title,\n groups,\n entries,\n basePath,\n defaultGroup,\n aliases,\n search,\n width = \"6xl\",\n renderGroup,\n phoneFooter,\n sidebarFooter,\n labels: labelsProp,\n layout = \"auto\",\n appName,\n documentTitle,\n className,\n}: SettingsLayoutProps<G>): ReactElement {\n const labels = useKitLabels(\"settings\", DEFAULT_SETTINGS_LABELS, labelsProp);\n const base = normalizeSettingsBase(basePath);\n const route = useSettingsRoute({ groups, entries, basePath: base, defaultGroup, aliases, layout });\n const location = useLocation();\n // The browser URL of an app path, for the sidebar's real links (a middle click opens a\n // tab): `#/settings/…` under a hash router, the basename in front under a browser one.\n const root = useHref(\"/\").replace(/\\/$/, \"\");\n const panelId = useId();\n const searchRef = useRef<HTMLInputElement>(null);\n const [query, setQuery] = useState(\"\");\n\n const visibleEntries = useMemo(() => visibleSettingsEntries(groups, entries), [groups, entries]);\n const showSearch = search ?? visibleEntries.length > SETTINGS_SEARCH_THRESHOLD;\n const trimmed = query.trim();\n const searching = showSearch && trimmed !== \"\";\n\n const groupById = useMemo(() => new Map(route.groups.map((g) => [g.id, g])), [route.groups]);\n const index = useMemo(\n () =>\n createSearchIndex<Searchable<G>>(\n visibleEntries.map((e) => ({\n id: e.id,\n title: e.title,\n keywords: [groupById.get(e.group)?.title ?? e.group, ...settingsKeywords(e.keywords)],\n group: e.group,\n anchor: e.anchor,\n settingsGroup: e.group,\n })),\n // Every hit: the list is the page's whole answer, not a palette's top few.\n { groupLimit: Infinity },\n ),\n [visibleEntries, groupById],\n );\n const hits = useMemo(() => (searching ? index.search(trimmed).map((h) => h.entry) : []), [searching, index, trimmed]);\n const hitsByGroup = useMemo(() => {\n const counts = new Map<G, number>();\n for (const hit of hits) counts.set(hit.settingsGroup, (counts.get(hit.settingsGroup) ?? 0) + 1);\n return counts;\n }, [hits]);\n\n const anchors = useMemo(() => new Set(entries.map((e) => e.anchor)), [entries]);\n const { phone, group, sub, selectSub, select } = route;\n const headingLevel = phone ? \"h2\" : \"h3\";\n const context = useMemo<SettingsLayoutContextValue>(\n () => ({\n basePath: base,\n group,\n sub,\n selectSub,\n headingLevel,\n hasAnchor: (anchor) => anchors.has(anchor),\n }),\n [base, group, sub, selectSub, headingLevel, anchors],\n );\n\n // Not while the address is about to be replaced (its removal of `focus` would undo the\n // replace), not on the list, and not while the hits stand where the cards would be (a\n // phone's group page has no search: a query left from its list does not count there).\n const hitsShown = searching && (!phone || group === null);\n useSettingsFocus(route.redirecting || group === null || hitsShown ? null : route.focus);\n\n // A link inside the page. On a phone it opens a page of its own — a push that marks the\n // list as the entry before it, so the group page's back is a real back. On a desktop\n // it switches the group in place — a replace.\n const { state } = location;\n const renderLink = useCallback(\n ({ href, ...props }: ListItemLinkProps) => (\n <Link to={href} replace={!phone} state={phone ? settingsFromListState() : state} {...props} />\n ),\n [phone, state],\n );\n\n const clearSearch = () => {\n setQuery(\"\");\n // The button that was pressed goes with the empty state; the field is where the\n // next query is typed.\n searchRef.current?.focus();\n };\n\n const pageTitle = title ?? labels.title;\n\n // The window's title (§3.1). Computed to a string during render so the effect below\n // runs when the words change, not whenever an inline `documentTitle` function is new.\n // Each run puts back the title it found before writing its own, so a group switch\n // ends where it began and the unmount leaves the title the page had before settings.\n const windowTitle =\n documentTitle === false\n ? null\n : (documentTitle ?? defaultDocumentTitle)({\n group: group === null ? null : (groupById.get(group)?.title ?? null),\n page: typeof title === \"string\" ? title : labels.title,\n appName,\n });\n useEffect(() => {\n if (windowTitle === null) return;\n const before = document.title;\n document.title = windowTitle;\n return () => {\n document.title = before;\n };\n }, [windowTitle]);\n\n const pageClass = cn(\"mx-auto w-full space-y-4 p-4 md:p-6\", width === \"7xl\" ? \"max-w-7xl\" : \"max-w-6xl\", className);\n\n const searchField = showSearch ? (\n <SearchField\n ref={searchRef}\n className=\"max-w-md\"\n value={query}\n onChange={setQuery}\n aria-label={labels.search}\n placeholder={labels.searchPlaceholder}\n />\n ) : null;\n\n const results =\n hits.length === 0 ? (\n <EmptyState\n title={labels.noMatches(trimmed)}\n action={\n <Button variant=\"secondary\" size=\"sm\" onClick={clearSearch}>\n {labels.clearSearch}\n </Button>\n }\n />\n ) : (\n <Card className=\"p-1\">\n <List separator=\"divider\" density=\"comfortable\" aria-label={labels.results}>\n {hits.map((hit) => (\n <ListItem\n key={hit.id}\n title={hit.title}\n trailing={groupById.get(hit.settingsGroup)?.title}\n href={settingsHref(base, hit.settingsGroup, hit.anchor)}\n renderLink={renderLink}\n // Leave search on the way: the hit's card is what the user asked for.\n onClick={() => setQuery(\"\")}\n />\n ))}\n </List>\n </Card>\n );\n\n const cards = (open: G) => (\n <SettingsHeadingLevel level={headingLevel}>\n <div className=\"space-y-4\">{renderGroup(open, sub)}</div>\n </SettingsHeadingLevel>\n );\n\n let body: ReactNode;\n if (phone) {\n const open = group === null ? undefined : groupById.get(group);\n body = open ? (\n <div className={pageClass} data-settings-view=\"group\">\n <PageHeader\n size=\"compact\"\n documentTitle={false}\n title={open.title}\n description={open.help}\n breadcrumbs={\n <Link\n to={base || \"/\"}\n aria-label={labels.back}\n onClick={(e) => {\n // A real link for a middle click; a plain click is the page's own back,\n // which is `navigate(-1)` when the list is the entry before this one.\n if (e.button !== 0 || e.metaKey || e.ctrlKey || e.shiftKey || e.altKey) return;\n e.preventDefault();\n route.back();\n }}\n className={cn(\"-ms-1 inline-flex items-center gap-0.5 self-start rounded-md pe-1 text-sm font-medium text-muted hover:text-primary\", FOCUS_RING)}\n >\n <ChevronLeft aria-hidden className=\"size-4 rtl:-scale-x-100\" />\n {pageTitle}\n </Link>\n }\n />\n {cards(open.id)}\n </div>\n ) : (\n <div className={pageClass} data-settings-view=\"list\">\n <PageHeader size=\"compact\" documentTitle={false} title={pageTitle} />\n {searchField}\n {searching ? (\n results\n ) : (\n <Card className=\"p-1\">\n <List separator=\"divider\" density=\"comfortable\" aria-label={labels.sections}>\n {route.groups.map((g) => (\n <ListItem\n key={g.id}\n href={settingsHref(base, g.id)}\n renderLink={renderLink}\n leading={\n <span\n aria-hidden\n className=\"flex size-8 shrink-0 items-center justify-center rounded-md bg-surface-2 text-secondary [&_svg]:size-4\"\n >\n {g.icon}\n </span>\n }\n title={g.title}\n subtitle={g.help}\n trailing={\n g.badge == null ? (\n <ChevronRight aria-hidden className=\"size-4 rtl:-scale-x-100\" />\n ) : (\n // The group's badge (0.48) before the chevron, the two as one end.\n <span className=\"flex items-center gap-2\">\n {g.badge}\n <ChevronRight aria-hidden className=\"size-4 rtl:-scale-x-100\" />\n </span>\n )\n }\n />\n ))}\n </List>\n </Card>\n )}\n {!searching && phoneFooter != null && (\n <Card className=\"p-1\">\n <List separator=\"divider\" density=\"comfortable\">\n {phoneFooter}\n </List>\n </Card>\n )}\n </div>\n );\n } else {\n const open = group === null ? undefined : groupById.get(group);\n // The section list alone, or (0.42) with the app's `sidebarFooter` under it in one\n // sticky column.\n const withSidebarFooter = (tabs: ReactNode) =>\n sidebarFooter == null ? (\n tabs\n ) : (\n <div className={cn(SIDEBAR_COLUMN, \"space-y-6\")}>\n {tabs}\n <div data-slot=\"settings-sidebar-footer\">{sidebarFooter}</div>\n </div>\n );\n body = (\n <div className={pageClass} data-settings-view=\"desktop\">\n <PageHeader size=\"compact\" documentTitle={false} title={pageTitle} />\n {searchField}\n <div className=\"md:grid md:grid-cols-[14rem_minmax(0,1fr)] md:gap-6\">\n {withSidebarFooter(\n <Tabs<G>\n orientation=\"vertical\"\n aria-label={labels.sections}\n // Sticky (new in the harmonisation, §3.2): keksdose's notifications group\n // scrolls for screens, and the list should stay in view beside it. With a\n // `sidebarFooter` the column round both is the sticky one.\n className={sidebarFooter == null ? SIDEBAR_COLUMN : undefined}\n panelId={open && !searching ? panelId : undefined}\n tabs={route.groups.map((g) => {\n const count = hitsByGroup.get(g.id) ?? 0;\n // While searching, the groups with nothing to show step back (keksdose).\n const dim = searching && count === 0;\n return {\n id: g.id,\n href: `${root}${settingsHref(base, g.id)}`,\n name: g.title,\n label: <span className={cn(dim && \"opacity-40\")}>{g.title}</span>,\n icon: <span className={cn(\"inline-flex [&_svg]:size-4\", dim && \"opacity-40\")}>{g.icon}</span>,\n // While searching, the hit count; otherwise the group's own badge (0.48).\n badge: searching ? (\n count > 0 ? (\n <Chip size=\"sm\" tone=\"brand\">\n <span aria-hidden>{count}</span>\n <span className=\"sr-only-fixed\">{labels.matchCount(count)}</span>\n </Chip>\n ) : undefined\n ) : (\n g.badge\n ),\n };\n })}\n active={(open?.id ?? route.groups[0]?.id) as G}\n onChange={(id) => {\n // While searching the hits stand where the group would; a click on a group\n // means \"go there\", so the search makes way (keksdose).\n setQuery(\"\");\n select(id);\n }}\n />,\n )}\n <div className=\"min-w-0 space-y-4\">\n {searching ? (\n results\n ) : open ? (\n <div\n id={panelId}\n role=\"tabpanel\"\n aria-labelledby={`${panelId}-tab`}\n className=\"space-y-4\"\n >\n <PageHeader as=\"h2\" size=\"sm\" title={open.title} description={open.help} />\n {cards(open.id)}\n </div>\n ) : null}\n </div>\n </div>\n </div>\n );\n }\n\n return <SettingsLayoutContext.Provider value={context}>{body}</SettingsLayoutContext.Provider>;\n}\n"],"mappings":"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","names":[]}
|
|
@@ -35,7 +35,7 @@ export interface AccountMenuLanguageProps {
|
|
|
35
35
|
/**
|
|
36
36
|
* The language choice INSIDE the account menu (docs/top-bar-harmonization.md §2.3),
|
|
37
37
|
* keksdose's sub-list: a `Disclosure variant="menu"` row — the globe, "Language", the
|
|
38
|
-
* active language's flag and the chevron — that opens in place onto one checked
|
|
38
|
+
* active language's flag and code ("DE") and the chevron — that opens in place onto one checked
|
|
39
39
|
* `MenuItem` per language. Inline rather than a nested popover: a popover inside a
|
|
40
40
|
* popover fights the outside click and has nowhere to go on a phone (keksdose #140).
|
|
41
41
|
*
|
|
@@ -5,6 +5,7 @@ import { Globe } from "lucide-react";
|
|
|
5
5
|
import { Disclosure } from "../components/disclosure.js";
|
|
6
6
|
import { MenuItem } from "../components/menu-item.js";
|
|
7
7
|
import { DEFAULT_TOP_BAR_LABELS, useKitLabels } from "../i18n/kit-labels.js";
|
|
8
|
+
import { languageCodeLabel } from "./language-code.js";
|
|
8
9
|
function flagOf(lang) {
|
|
9
10
|
if (lang.flag !== void 0) return lang.flag;
|
|
10
11
|
if (!lang.country) return void 0;
|
|
@@ -39,7 +40,14 @@ function AccountMenuLanguage({
|
|
|
39
40
|
/* @__PURE__ */ jsx(Globe, { "aria-hidden": true, className: "size-4" }),
|
|
40
41
|
label ?? words.language
|
|
41
42
|
] }),
|
|
42
|
-
active &&
|
|
43
|
+
active && // The flag alone could not tell de-CH, fr-CH and it-CH apart (one Swiss
|
|
44
|
+
// flag each), so the code sits beside it (0.49 KIT-10); the row's name
|
|
45
|
+
// carries the language's own name.
|
|
46
|
+
/* @__PURE__ */ jsxs("span", { className: "flex items-center gap-1.5", children: [
|
|
47
|
+
flagOf(active),
|
|
48
|
+
/* @__PURE__ */ jsx("span", { "aria-hidden": true, className: "text-xs font-semibold tabular-nums tracking-wide text-secondary", children: languageCodeLabel(active.code) }),
|
|
49
|
+
/* @__PURE__ */ jsx("span", { className: "sr-only", children: active.label })
|
|
50
|
+
] })
|
|
43
51
|
] }),
|
|
44
52
|
children: /* @__PURE__ */ jsx("ul", { children: languages.map((lang) => /* @__PURE__ */ jsx("li", { children: /* @__PURE__ */ jsx(
|
|
45
53
|
MenuItem,
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/shell/account-menu-language.tsx"],"sourcesContent":["import { useState } from \"react\";\nimport type { ReactNode } from \"react\";\nimport { Globe } from \"lucide-react\";\nimport { Disclosure } from \"../components/disclosure\";\nimport { MenuItem } from \"../components/menu-item\";\nimport { DEFAULT_TOP_BAR_LABELS, useKitLabels } from \"../i18n/kit-labels\";\n\n/** One offered language in the account menu's sub-list. */\nexport interface AccountMenuLanguageOption {\n code: string;\n /** Its name, in its own language (\"Deutsch\", \"Français\"). */\n label: ReactNode;\n /**\n * A flag before the name, as the app draws it — keksdose's flag-icons span\n * (`<span aria-hidden className=\"fi fi-ch …\" />`). A node rather than a country code, so\n * the kit ships no flag set; decorative, so give it `aria-hidden`.\n */\n flag?: ReactNode;\n /**\n * Or the ISO 3166-1 alpha-2 country (lowercase) whose flag-icons flag to draw, as\n * `LanguageMenu`'s `LanguageOption` takes it — so one language list feeds both\n * (Kurvenschmiede's `LANGUAGES`). Needs the app's flag-icons CSS, as `LanguageMenu`\n * does. `flag` wins when both are given.\n */\n country?: string;\n}\n\n/** The option's flag: its own node, else the flag-icons flag of its country. */\nfunction flagOf(lang: AccountMenuLanguageOption): ReactNode {\n if (lang.flag !== undefined) return lang.flag;\n if (!lang.country) return undefined;\n return (\n <span\n aria-hidden\n data-country={lang.country}\n 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)]`}\n />\n );\n}\n\nexport interface AccountMenuLanguageProps {\n languages: readonly AccountMenuLanguageOption[];\n /** The active language's code. */\n value: string | undefined;\n /** A pick: the app sets the device's language and, signed in, the account's\n * (settings §6.2). */\n onPick: (code: string) => void;\n /** The account menu's `close` (from `TopBarActionMenu`'s `children`): a pick shuts it. */\n close?: () => void;\n /** The row's name. Default: `topBar.language` (\"Language\"). */\n label?: string;\n /** Open the sub-list at first. Default `false`. */\n defaultOpen?: boolean;\n}\n\n/**\n * The language choice INSIDE the account menu (docs/top-bar-harmonization.md §2.3),\n * keksdose's sub-list: a `Disclosure variant=\"menu\"` row — the globe, \"Language\", the\n * active language's flag and the chevron — that opens in place onto one checked\n * `MenuItem` per language. Inline rather than a nested popover: a popover inside a\n * popover fights the outside click and has nowhere to go on a phone (keksdose #140).\n *\n * It renders its own `<li>`: drop it into `TopBarActionMenu`'s `children` first after the\n * app's context rows (§6.5). Signed out there is no account menu; `LanguageMenu` stays\n * the bar's button (§2.3).\n */\nexport function AccountMenuLanguage({\n languages,\n value,\n onPick,\n close,\n label,\n defaultOpen = false,\n}: AccountMenuLanguageProps) {\n const words = useKitLabels(\"topBar\", DEFAULT_TOP_BAR_LABELS);\n const [open, setOpen] = useState(defaultOpen);\n const active = languages.find((l) => l.code === value);\n return (\n <li>\n <Disclosure\n variant=\"menu\"\n open={open}\n onOpenChange={setOpen}\n title={\n <span className=\"flex items-center justify-between gap-3\">\n <span className=\"flex items-center gap-2\">\n <Globe aria-hidden className=\"size-4\" />\n {label ?? words.language}\n </span>\n {active && flagOf(active)}\n </span>\n }\n >\n <ul>\n {languages.map((lang) => (\n <li key={lang.code}>\n <MenuItem\n checked={lang.code === value}\n onClick={() => {\n onPick(lang.code);\n setOpen(false);\n close?.();\n }}\n leading={flagOf(lang)}\n // Under the row's own text, past the globe.\n className=\"ps-9\"\n >\n {lang.label}\n </MenuItem>\n </li>\n ))}\n </ul>\n </Disclosure>\n </li>\n );\n}\n"],"mappings":";
|
|
1
|
+
{"version":3,"sources":["../../src/shell/account-menu-language.tsx"],"sourcesContent":["import { useState } from \"react\";\nimport type { ReactNode } from \"react\";\nimport { Globe } from \"lucide-react\";\nimport { Disclosure } from \"../components/disclosure\";\nimport { MenuItem } from \"../components/menu-item\";\nimport { DEFAULT_TOP_BAR_LABELS, useKitLabels } from \"../i18n/kit-labels\";\nimport { languageCodeLabel } from \"./language-code\";\n\n/** One offered language in the account menu's sub-list. */\nexport interface AccountMenuLanguageOption {\n code: string;\n /** Its name, in its own language (\"Deutsch\", \"Français\"). */\n label: ReactNode;\n /**\n * A flag before the name, as the app draws it — keksdose's flag-icons span\n * (`<span aria-hidden className=\"fi fi-ch …\" />`). A node rather than a country code, so\n * the kit ships no flag set; decorative, so give it `aria-hidden`.\n */\n flag?: ReactNode;\n /**\n * Or the ISO 3166-1 alpha-2 country (lowercase) whose flag-icons flag to draw, as\n * `LanguageMenu`'s `LanguageOption` takes it — so one language list feeds both\n * (Kurvenschmiede's `LANGUAGES`). Needs the app's flag-icons CSS, as `LanguageMenu`\n * does. `flag` wins when both are given.\n */\n country?: string;\n}\n\n/** The option's flag: its own node, else the flag-icons flag of its country. */\nfunction flagOf(lang: AccountMenuLanguageOption): ReactNode {\n if (lang.flag !== undefined) return lang.flag;\n if (!lang.country) return undefined;\n return (\n <span\n aria-hidden\n data-country={lang.country}\n 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)]`}\n />\n );\n}\n\nexport interface AccountMenuLanguageProps {\n languages: readonly AccountMenuLanguageOption[];\n /** The active language's code. */\n value: string | undefined;\n /** A pick: the app sets the device's language and, signed in, the account's\n * (settings §6.2). */\n onPick: (code: string) => void;\n /** The account menu's `close` (from `TopBarActionMenu`'s `children`): a pick shuts it. */\n close?: () => void;\n /** The row's name. Default: `topBar.language` (\"Language\"). */\n label?: string;\n /** Open the sub-list at first. Default `false`. */\n defaultOpen?: boolean;\n}\n\n/**\n * The language choice INSIDE the account menu (docs/top-bar-harmonization.md §2.3),\n * keksdose's sub-list: a `Disclosure variant=\"menu\"` row — the globe, \"Language\", the\n * active language's flag and code (\"DE\") and the chevron — that opens in place onto one checked\n * `MenuItem` per language. Inline rather than a nested popover: a popover inside a\n * popover fights the outside click and has nowhere to go on a phone (keksdose #140).\n *\n * It renders its own `<li>`: drop it into `TopBarActionMenu`'s `children` first after the\n * app's context rows (§6.5). Signed out there is no account menu; `LanguageMenu` stays\n * the bar's button (§2.3).\n */\nexport function AccountMenuLanguage({\n languages,\n value,\n onPick,\n close,\n label,\n defaultOpen = false,\n}: AccountMenuLanguageProps) {\n const words = useKitLabels(\"topBar\", DEFAULT_TOP_BAR_LABELS);\n const [open, setOpen] = useState(defaultOpen);\n const active = languages.find((l) => l.code === value);\n return (\n <li>\n <Disclosure\n variant=\"menu\"\n open={open}\n onOpenChange={setOpen}\n title={\n <span className=\"flex items-center justify-between gap-3\">\n <span className=\"flex items-center gap-2\">\n <Globe aria-hidden className=\"size-4\" />\n {label ?? words.language}\n </span>\n {active && (\n // The flag alone could not tell de-CH, fr-CH and it-CH apart (one Swiss\n // flag each), so the code sits beside it (0.49 KIT-10); the row's name\n // carries the language's own name.\n <span className=\"flex items-center gap-1.5\">\n {flagOf(active)}\n <span aria-hidden className=\"text-xs font-semibold tabular-nums tracking-wide text-secondary\">\n {languageCodeLabel(active.code)}\n </span>\n <span className=\"sr-only\">{active.label}</span>\n </span>\n )}\n </span>\n }\n >\n <ul>\n {languages.map((lang) => (\n <li key={lang.code}>\n <MenuItem\n checked={lang.code === value}\n onClick={() => {\n onPick(lang.code);\n setOpen(false);\n close?.();\n }}\n leading={flagOf(lang)}\n // Under the row's own text, past the globe.\n className=\"ps-9\"\n >\n {lang.label}\n </MenuItem>\n </li>\n ))}\n </ul>\n </Disclosure>\n </li>\n );\n}\n"],"mappings":";AAiCI,cAqDQ,YArDR;AAjCJ,SAAS,gBAAgB;AAEzB,SAAS,aAAa;AACtB,SAAS,kBAAkB;AAC3B,SAAS,gBAAgB;AACzB,SAAS,wBAAwB,oBAAoB;AACrD,SAAS,yBAAyB;AAuBlC,SAAS,OAAO,MAA4C;AAC1D,MAAI,KAAK,SAAS,OAAW,QAAO,KAAK;AACzC,MAAI,CAAC,KAAK,QAAS,QAAO;AAC1B,SACE;AAAA,IAAC;AAAA;AAAA,MACC,eAAW;AAAA,MACX,gBAAc,KAAK;AAAA,MACnB,WAAW,SAAS,KAAK,OAAO;AAAA;AAAA,EAClC;AAEJ;AA4BO,SAAS,oBAAoB;AAAA,EAClC;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA,cAAc;AAChB,GAA6B;AAC3B,QAAM,QAAQ,aAAa,UAAU,sBAAsB;AAC3D,QAAM,CAAC,MAAM,OAAO,IAAI,SAAS,WAAW;AAC5C,QAAM,SAAS,UAAU,KAAK,CAAC,MAAM,EAAE,SAAS,KAAK;AACrD,SACE,oBAAC,QACC;AAAA,IAAC;AAAA;AAAA,MACC,SAAQ;AAAA,MACR;AAAA,MACA,cAAc;AAAA,MACd,OACE,qBAAC,UAAK,WAAU,2CACd;AAAA,6BAAC,UAAK,WAAU,2BACd;AAAA,8BAAC,SAAM,eAAW,MAAC,WAAU,UAAS;AAAA,UACrC,SAAS,MAAM;AAAA,WAClB;AAAA,QACC;AAAA;AAAA;AAAA,QAIC,qBAAC,UAAK,WAAU,6BACb;AAAA,iBAAO,MAAM;AAAA,UACd,oBAAC,UAAK,eAAW,MAAC,WAAU,mEACzB,4BAAkB,OAAO,IAAI,GAChC;AAAA,UACA,oBAAC,UAAK,WAAU,WAAW,iBAAO,OAAM;AAAA,WAC1C;AAAA,SAEJ;AAAA,MAGF,8BAAC,QACE,oBAAU,IAAI,CAAC,SACd,oBAAC,QACC;AAAA,QAAC;AAAA;AAAA,UACC,SAAS,KAAK,SAAS;AAAA,UACvB,SAAS,MAAM;AACb,mBAAO,KAAK,IAAI;AAChB,oBAAQ,KAAK;AACb,oBAAQ;AAAA,UACV;AAAA,UACA,SAAS,OAAO,IAAI;AAAA,UAEpB,WAAU;AAAA,UAET,eAAK;AAAA;AAAA,MACR,KAbO,KAAK,IAcd,CACD,GACH;AAAA;AAAA,EACF,GACF;AAEJ;","names":[]}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/shell/app-shell-labels.ts"],"sourcesContent":["// The English words of ./app-shell, in a module with no component code: the English\n// catalogue (src/i18n/defaults.ts) imports them without the component, so `en` loads as one\n// chunk like the other languages (0.34, KS U8). ./app-shell re-exports them.\n\nimport type { AppShellMoreLabels } from \"./app-shell\";\n\nexport const DEFAULT_APP_SHELL_MORE_LABELS: AppShellMoreLabels = {\n more: \"More\",\n moreTitle: \"More pages\",\n};\n"],"mappings":";AAMO,MAAM,gCAAoD;AAAA,EAC/D,MAAM;AAAA,EACN,WAAW;
|
|
1
|
+
{"version":3,"sources":["../../src/shell/app-shell-labels.ts"],"sourcesContent":["// The English words of ./app-shell, in a module with no component code: the English\n// catalogue (src/i18n/defaults.ts) imports them without the component, so `en` loads as one\n// chunk like the other languages (0.34, KS U8). ./app-shell re-exports them.\n\nimport type { AppShellMoreLabels } from \"./app-shell\";\n\nexport const DEFAULT_APP_SHELL_MORE_LABELS: AppShellMoreLabels = {\n more: \"More\",\n moreTitle: \"More pages\",\n overview: \"Overview\",\n};\n"],"mappings":";AAMO,MAAM,gCAAoD;AAAA,EAC/D,MAAM;AAAA,EACN,WAAW;AAAA,EACX,UAAU;AACZ;","names":[]}
|
|
@@ -28,6 +28,19 @@ export interface AppShellNavItem {
|
|
|
28
28
|
* from Large up (keksdose dev#613, which read `useTextSize()` in its layout to decide).
|
|
29
29
|
* Default `"normal"`: the short label at every size, as before. */
|
|
30
30
|
shortLabelFrom?: TextSize;
|
|
31
|
+
/** 0.49: `shortLabel` is another word for the page, not a shortening of `label` —
|
|
32
|
+
* keksdose's "Activity" for "Transactions". Without it a development build warns once
|
|
33
|
+
* when `shortLabel` does not start with `label`'s first word (an abbreviation of that
|
|
34
|
+
* word, "Trans." for "Transactions", counts), because a bar cell that names a page
|
|
35
|
+
* differently from the sidebar, the title and the palette is a page the reader
|
|
36
|
+
* looks for twice. */
|
|
37
|
+
shortLabelIsAlias?: boolean;
|
|
38
|
+
/** 0.49: which entries sit in the phone bar when they do not all fit
|
|
39
|
+
* ({@link AppShellProps.mobileMaxItems}). Lower first: the entries with a priority take
|
|
40
|
+
* the bar's cells in its order, then the rest in the list's order; the bar still shows
|
|
41
|
+
* its entries in the list's order, and the others go under "More". Unset: the list's
|
|
42
|
+
* order decides (as before). */
|
|
43
|
+
mobilePriority?: number;
|
|
31
44
|
/** 0.31.0: leave this entry out of the MOBILE BOTTOM BAR; the sidebar keeps it.
|
|
32
45
|
* For an entry a phone reaches another way — the brand link to the start page —
|
|
33
46
|
* when one cell fewer is what keeps the others' labels whole (the bar divides its
|
|
@@ -43,8 +56,10 @@ export interface AppShellNavItem {
|
|
|
43
56
|
dataTour?: string;
|
|
44
57
|
/** Optional sub-items. When present, hovering (or focusing) the item opens a
|
|
45
58
|
* flyout to the right of the sidebar with a header (this item's label) and the
|
|
46
|
-
* sub-items as icon + title links. Works collapsed or expanded
|
|
47
|
-
*
|
|
59
|
+
* sub-items as icon + title links. Works collapsed or expanded. On the phone (0.49)
|
|
60
|
+
* the bar's cell opens a sheet of them instead of linking to `to` — the sheet lists
|
|
61
|
+
* `to` first as "Overview" when no sub-item is that page — and the pages of the group
|
|
62
|
+
* the reader is in show as the row above the bar (`mobileSubNav`). */
|
|
48
63
|
items?: AppShellSubItem[];
|
|
49
64
|
}
|
|
50
65
|
/**
|
|
@@ -106,14 +121,35 @@ export interface AppShellMoreLabels {
|
|
|
106
121
|
more: string;
|
|
107
122
|
/** The sheet's heading, and so its accessible name. */
|
|
108
123
|
moreTitle: string;
|
|
124
|
+
/** 0.49: a parent entry's own page in the sheet its bar cell opens, above its pages,
|
|
125
|
+
* when no page is that one — "Overview". */
|
|
126
|
+
overview: string;
|
|
109
127
|
}
|
|
110
128
|
export { DEFAULT_APP_SHELL_MORE_LABELS };
|
|
111
129
|
/**
|
|
112
|
-
* The most
|
|
113
|
-
*
|
|
114
|
-
*
|
|
130
|
+
* The most cells the phone bar holds, "More" included, per text size (0.49 KIT-6; 0.32
|
|
131
|
+
* §10.7): five at Normal and Large, four at Extra large. Eight entries are four and
|
|
132
|
+
* "More" at Normal; a fifth of a 390 px phone holds one word of the bar's type, and a
|
|
133
|
+
* sixth no longer does once the type grows. A fifth of a 360 px phone at 150 % is 48 px
|
|
134
|
+
* of Normal-sized room.
|
|
135
|
+
*/
|
|
136
|
+
export declare const DEFAULT_MOBILE_MAX_ITEMS: Readonly<Record<TextSize, number>>;
|
|
137
|
+
/**
|
|
138
|
+
* @deprecated 0.49 — use {@link DEFAULT_MOBILE_MAX_ITEMS} and `AppShell`'s
|
|
139
|
+
* `mobileMaxItems`: the bar is sized in cells, "More" included; removed in 0.51. This is
|
|
140
|
+
* the same default counted as entries before "More" (one fewer).
|
|
115
141
|
*/
|
|
116
142
|
export declare const DEFAULT_MOBILE_BAR_MAX: Readonly<Record<TextSize, number>>;
|
|
143
|
+
/**
|
|
144
|
+
* The phone bar's cells (0.49 KIT-6): every entry when there are at most `maxItems`,
|
|
145
|
+
* else `maxItems − 1` entries and a "More" cell for the rest. Which entries stay is
|
|
146
|
+
* `priority`'s call — lower first, entries without one after them in the list's order —
|
|
147
|
+
* and both halves keep the list's order. Never fewer than one entry beside More.
|
|
148
|
+
*/
|
|
149
|
+
export declare function pickMobileBar<T>(entries: readonly T[], maxItems: number, priority?: (entry: T) => number | undefined): {
|
|
150
|
+
shown: T[];
|
|
151
|
+
hidden: T[];
|
|
152
|
+
};
|
|
117
153
|
/**
|
|
118
154
|
* Split the bar's entries into the cells shown and the ones under "More", in the app's
|
|
119
155
|
* order (§10.7: the app puts its four most used first, or orders by role). One hidden
|
|
@@ -190,17 +226,23 @@ export interface AppShellProps extends ComponentPropsWithoutRef<"div"> {
|
|
|
190
226
|
*/
|
|
191
227
|
mobileSubNavLayout?: "wrap" | "scroll";
|
|
192
228
|
/**
|
|
193
|
-
* The most
|
|
194
|
-
*
|
|
195
|
-
*
|
|
196
|
-
*
|
|
197
|
-
* accountant see
|
|
229
|
+
* The most cells the phone bar holds, "More" included (0.49 KIT-6). When `nav` (after
|
|
230
|
+
* `mobileHidden`) has more entries, the bar keeps `mobileMaxItems − 1` of them and a
|
|
231
|
+
* last "More" cell opens a sheet with the rest, each group with its pages. Which
|
|
232
|
+
* entries stay is the items' `mobilePriority`, else the list's order (so the app puts
|
|
233
|
+
* its most used entries first, or orders by role: kastlan's manager and accountant see
|
|
234
|
+
* different firsts).
|
|
198
235
|
*
|
|
199
|
-
* Default {@link
|
|
200
|
-
*
|
|
201
|
-
*
|
|
202
|
-
|
|
203
|
-
|
|
236
|
+
* Default {@link DEFAULT_MOBILE_MAX_ITEMS}: 5 at Normal and Large, 4 at Extra large —
|
|
237
|
+
* eight entries are four and "More". A number replaces the default at every size;
|
|
238
|
+
* `{ xlarge: 5 }` replaces it at one size only. Never fewer than one entry beside More.
|
|
239
|
+
*/
|
|
240
|
+
mobileMaxItems?: number | Partial<Record<TextSize, number>>;
|
|
241
|
+
/**
|
|
242
|
+
* @deprecated 0.49 — use `mobileMaxItems`, which counts the bar's cells, "More"
|
|
243
|
+
* included: `mobileBarMax={4}` is `mobileMaxItems={5}`; removed in 0.51. Read as that
|
|
244
|
+
* (one more, per size), with a development warning; ignored when `mobileMaxItems` is
|
|
245
|
+
* given.
|
|
204
246
|
*/
|
|
205
247
|
mobileBarMax?: number | Partial<Record<TextSize, number>>;
|
|
206
248
|
/** The More cell's words over the provider's `appShellMore`. */
|
|
@@ -228,4 +270,4 @@ export interface AppShellProps extends ComponentPropsWithoutRef<"div"> {
|
|
|
228
270
|
* `NavLink`; domain-free — nav items, brand, footer and the top bar's actions
|
|
229
271
|
* are all supplied by the app.
|
|
230
272
|
*/
|
|
231
|
-
export declare function AppShell({ nav, topBar, children, footer, sidebarFooter, sidebarFooterItems, renderLink, collapseStorageKey: collapseStorageKeyProp, collapseLabel, expandLabel, subNav, mobileSubNav, mobileSubNavLayout: mobileSubNavLayoutProp, mobileBarMax, moreLabels, toggleGroupLabel, embedded: embeddedProp, className, ref, ...rest }: AppShellProps): import("react").JSX.Element;
|
|
273
|
+
export declare function AppShell({ nav, topBar, children, footer, sidebarFooter, sidebarFooterItems, renderLink, collapseStorageKey: collapseStorageKeyProp, collapseLabel, expandLabel, subNav, mobileSubNav, mobileSubNavLayout: mobileSubNavLayoutProp, mobileMaxItems, mobileBarMax, moreLabels, toggleGroupLabel, embedded: embeddedProp, className, ref, ...rest }: AppShellProps): import("react").JSX.Element;
|
package/dist/shell/app-shell.js
CHANGED
|
@@ -21,16 +21,83 @@ import { useBreakpoint } from "../hooks/use-breakpoint.js";
|
|
|
21
21
|
import { TEXT_SIZES, useTextSize } from "../theme/text-size.js";
|
|
22
22
|
import { DEFAULT_APP_SHELL_LABELS, useKitLabels, useKitLink } from "../i18n/kit-labels.js";
|
|
23
23
|
import { DEFAULT_APP_SHELL_MORE_LABELS } from "./app-shell-labels.js";
|
|
24
|
+
const DEFAULT_MOBILE_MAX_ITEMS = {
|
|
25
|
+
normal: 5,
|
|
26
|
+
large: 5,
|
|
27
|
+
xlarge: 4
|
|
28
|
+
};
|
|
24
29
|
const DEFAULT_MOBILE_BAR_MAX = {
|
|
25
|
-
normal:
|
|
26
|
-
large:
|
|
27
|
-
xlarge:
|
|
30
|
+
normal: DEFAULT_MOBILE_MAX_ITEMS.normal - 1,
|
|
31
|
+
large: DEFAULT_MOBILE_MAX_ITEMS.large - 1,
|
|
32
|
+
xlarge: DEFAULT_MOBILE_MAX_ITEMS.xlarge - 1
|
|
28
33
|
};
|
|
34
|
+
function pickMobileBar(entries, maxItems, priority) {
|
|
35
|
+
const cells = Math.max(1, Math.floor(maxItems));
|
|
36
|
+
if (entries.length <= cells) return { shown: [...entries], hidden: [] };
|
|
37
|
+
const keep = Math.max(1, cells - 1);
|
|
38
|
+
const rank = (i) => priority?.(entries[i]) ?? Number.POSITIVE_INFINITY;
|
|
39
|
+
const chosen = new Set(
|
|
40
|
+
entries.map((_, i) => i).sort((a, b) => rank(a) - rank(b) || a - b).slice(0, keep)
|
|
41
|
+
);
|
|
42
|
+
return {
|
|
43
|
+
shown: entries.filter((_, i) => chosen.has(i)),
|
|
44
|
+
hidden: entries.filter((_, i) => !chosen.has(i))
|
|
45
|
+
};
|
|
46
|
+
}
|
|
29
47
|
function splitMobileBar(entries, max) {
|
|
30
48
|
const limit = Math.max(1, Math.floor(max));
|
|
31
49
|
if (entries.length <= limit + 1) return { shown: [...entries], hidden: [] };
|
|
32
50
|
return { shown: entries.slice(0, limit), hidden: entries.slice(limit) };
|
|
33
51
|
}
|
|
52
|
+
function mobileCells(maxItems, barMax, size) {
|
|
53
|
+
if (maxItems !== void 0) {
|
|
54
|
+
return typeof maxItems === "number" ? maxItems : maxItems[size] ?? DEFAULT_MOBILE_MAX_ITEMS[size];
|
|
55
|
+
}
|
|
56
|
+
if (barMax !== void 0) {
|
|
57
|
+
const entries = typeof barMax === "number" ? barMax : barMax[size];
|
|
58
|
+
if (entries !== void 0) return entries + 1;
|
|
59
|
+
}
|
|
60
|
+
return DEFAULT_MOBILE_MAX_ITEMS[size];
|
|
61
|
+
}
|
|
62
|
+
function devWarnings() {
|
|
63
|
+
return Boolean(import.meta.env?.DEV) && !import.meta.env?.VITEST;
|
|
64
|
+
}
|
|
65
|
+
const warnedNav = /* @__PURE__ */ new Set();
|
|
66
|
+
function warnNavOnce(key, message) {
|
|
67
|
+
if (warnedNav.has(key)) return;
|
|
68
|
+
warnedNav.add(key);
|
|
69
|
+
console.warn(message);
|
|
70
|
+
}
|
|
71
|
+
function firstWord(text) {
|
|
72
|
+
return (text.trim().match(/^[\p{L}\p{N}]+/u)?.[0] ?? "").toLocaleLowerCase();
|
|
73
|
+
}
|
|
74
|
+
function shortLabelNamesLabel(short, label) {
|
|
75
|
+
const s = firstWord(short);
|
|
76
|
+
const l = firstWord(label);
|
|
77
|
+
return s === "" || l === "" || l.startsWith(s) || s.startsWith(l);
|
|
78
|
+
}
|
|
79
|
+
const TEXT_SIZE_WORDS = { normal: "Normal", large: "Large", xlarge: "Extra large" };
|
|
80
|
+
function useNavDevWarnings(nav, deprecatedBarMax) {
|
|
81
|
+
useEffect(() => {
|
|
82
|
+
if (!devWarnings()) return;
|
|
83
|
+
if (deprecatedBarMax) {
|
|
84
|
+
warnNavOnce(
|
|
85
|
+
"mobileBarMax",
|
|
86
|
+
`[ui-kit] AppShell mobileBarMax is deprecated: use mobileMaxItems, which counts the bar's cells with "More" (mobileBarMax={4} is mobileMaxItems={5}). Removed in 0.51.`
|
|
87
|
+
);
|
|
88
|
+
}
|
|
89
|
+
for (const item of nav) {
|
|
90
|
+
if (item.shortLabel === void 0 || item.shortLabelIsAlias) continue;
|
|
91
|
+
if (shortLabelNamesLabel(item.shortLabel, item.label)) continue;
|
|
92
|
+
const from = item.shortLabelFrom ?? "normal";
|
|
93
|
+
const where = from === "normal" ? "at every text size" : `from ${TEXT_SIZE_WORDS[from]} text up`;
|
|
94
|
+
warnNavOnce(
|
|
95
|
+
`shortLabel|${item.to}|${item.shortLabel}|${item.label}`,
|
|
96
|
+
`[ui-kit] AppShell nav "${item.to}": shortLabel "${item.shortLabel}" does not start with "${item.label.trim().split(/\s+/)[0]}", the first word of its label "${item.label}". The phone bar shows it ${where}, so the page has another name there than in the sidebar and its title. Shorten the label instead, or pass shortLabelIsAlias: true if the other word is meant.`
|
|
97
|
+
);
|
|
98
|
+
}
|
|
99
|
+
}, [nav, deprecatedBarMax]);
|
|
100
|
+
}
|
|
34
101
|
const AppShellNesting = createContext(false);
|
|
35
102
|
function useNavHeightVar(ref, shellRef, embedded) {
|
|
36
103
|
const isMdUp = useBreakpoint("md", false);
|
|
@@ -68,6 +135,7 @@ function AppShell({
|
|
|
68
135
|
subNav = "flyout",
|
|
69
136
|
mobileSubNav = true,
|
|
70
137
|
mobileSubNavLayout: mobileSubNavLayoutProp,
|
|
138
|
+
mobileMaxItems,
|
|
71
139
|
mobileBarMax,
|
|
72
140
|
moreLabels,
|
|
73
141
|
toggleGroupLabel,
|
|
@@ -80,11 +148,13 @@ function AppShell({
|
|
|
80
148
|
const { size: textSize } = useTextSize();
|
|
81
149
|
const large = textSize !== "normal";
|
|
82
150
|
const mobileSubNavLayout = mobileSubNavLayoutProp ?? (large ? "scroll" : "wrap");
|
|
83
|
-
const
|
|
84
|
-
const { shown: barNav, hidden: moreNav } =
|
|
151
|
+
const maxItems = mobileCells(mobileMaxItems, mobileBarMax, textSize);
|
|
152
|
+
const { shown: barNav, hidden: moreNav } = pickMobileBar(
|
|
85
153
|
nav.filter((item) => !item.mobileHidden),
|
|
86
|
-
|
|
154
|
+
maxItems,
|
|
155
|
+
(item) => item.mobilePriority
|
|
87
156
|
);
|
|
157
|
+
useNavDevWarnings(nav, mobileBarMax !== void 0 && mobileMaxItems === void 0);
|
|
88
158
|
const collapseStorageKey = collapseStorageKeyProp ?? (embedded ? null : "appLayout.sidebarCollapsed");
|
|
89
159
|
const labels = useKitLabels("appShell", DEFAULT_APP_SHELL_LABELS, {
|
|
90
160
|
collapse: collapseLabel,
|
|
@@ -186,7 +256,7 @@ function AppShell({
|
|
|
186
256
|
"main",
|
|
187
257
|
{
|
|
188
258
|
className: cn(
|
|
189
|
-
"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-
|
|
259
|
+
"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]",
|
|
190
260
|
// The list page's fill (0.40, docs/page-layout-harmonization.md §1.3): while
|
|
191
261
|
// a `fillHeight` DataTable is inside, a flex column — a banner above the
|
|
192
262
|
// page keeps its own height and the page (a `FillHeight`) takes the rest,
|
|
@@ -225,7 +295,9 @@ function AppShell({
|
|
|
225
295
|
className: "grid",
|
|
226
296
|
style: { gridTemplateColumns: `repeat(${barNav.length + (moreNav.length ? 1 : 0)}, minmax(0, 1fr))` },
|
|
227
297
|
children: [
|
|
228
|
-
barNav.map(
|
|
298
|
+
barNav.map(
|
|
299
|
+
(item) => item.items?.length ? /* @__PURE__ */ jsx(MobileGroupCell, { item, labels: moreLabels }, item.to) : /* @__PURE__ */ jsx(MobileNavItem, { item }, item.to)
|
|
300
|
+
),
|
|
229
301
|
moreNav.length > 0 && /* @__PURE__ */ jsx(MobileMoreCell, { hidden: moreNav, labels: moreLabels })
|
|
230
302
|
]
|
|
231
303
|
}
|
|
@@ -326,8 +398,7 @@ function MobileCellLabel({ children }) {
|
|
|
326
398
|
}
|
|
327
399
|
function MobileNavItem({ item }) {
|
|
328
400
|
const subActive = useSubItemActive(item);
|
|
329
|
-
const
|
|
330
|
-
const short = item.shortLabel !== void 0 && TEXT_SIZES.indexOf(size) >= TEXT_SIZES.indexOf(item.shortLabelFrom ?? "normal") ? item.shortLabel : void 0;
|
|
401
|
+
const label = useCellLabel(item);
|
|
331
402
|
return /* @__PURE__ */ jsxs(
|
|
332
403
|
NavLink,
|
|
333
404
|
{
|
|
@@ -337,11 +408,76 @@ function MobileNavItem({ item }) {
|
|
|
337
408
|
className: ({ isActive }) => mobileCellClass(isActive || subActive),
|
|
338
409
|
children: [
|
|
339
410
|
/* @__PURE__ */ jsx("span", { className: "relative", children: /* @__PURE__ */ jsx(item.icon, { className: "size-5" }) }),
|
|
340
|
-
/* @__PURE__ */ jsx(MobileCellLabel, { children:
|
|
411
|
+
/* @__PURE__ */ jsx(MobileCellLabel, { children: label })
|
|
412
|
+
]
|
|
413
|
+
}
|
|
414
|
+
);
|
|
415
|
+
}
|
|
416
|
+
function useCellLabel(item) {
|
|
417
|
+
const { size } = useTextSize();
|
|
418
|
+
return item.shortLabel !== void 0 && TEXT_SIZES.indexOf(size) >= TEXT_SIZES.indexOf(item.shortLabelFrom ?? "normal") ? item.shortLabel : item.label;
|
|
419
|
+
}
|
|
420
|
+
function SheetLink({
|
|
421
|
+
to,
|
|
422
|
+
end,
|
|
423
|
+
icon: Icon,
|
|
424
|
+
label,
|
|
425
|
+
onClick,
|
|
426
|
+
level
|
|
427
|
+
}) {
|
|
428
|
+
return /* @__PURE__ */ jsxs(
|
|
429
|
+
NavLink,
|
|
430
|
+
{
|
|
431
|
+
to,
|
|
432
|
+
end: end ?? true,
|
|
433
|
+
onClick,
|
|
434
|
+
className: ({ isActive }) => cn(
|
|
435
|
+
level === "entry" ? "flex min-h-11 items-center gap-3 rounded-md px-3 py-2 text-sm transition-colors" : "flex min-h-11 items-center gap-2.5 rounded-md px-2.5 py-1.5 text-sm transition-colors",
|
|
436
|
+
isActive ? "bg-inverse text-inverse" : level === "entry" ? "text-primary hover:bg-hover" : "text-secondary hover:bg-hover"
|
|
437
|
+
),
|
|
438
|
+
children: [
|
|
439
|
+
/* @__PURE__ */ jsx(Icon, { "aria-hidden": true, className: level === "entry" ? "size-5 shrink-0" : "size-4 shrink-0 opacity-70" }),
|
|
440
|
+
/* @__PURE__ */ jsx("span", { className: "min-w-0 break-words leading-snug", children: label })
|
|
341
441
|
]
|
|
342
442
|
}
|
|
343
443
|
);
|
|
344
444
|
}
|
|
445
|
+
function MobileGroupCell({
|
|
446
|
+
item,
|
|
447
|
+
labels: labelsProp
|
|
448
|
+
}) {
|
|
449
|
+
const labels = useKitLabels("appShellMore", DEFAULT_APP_SHELL_MORE_LABELS, labelsProp);
|
|
450
|
+
const { pathname } = useLocation();
|
|
451
|
+
const [open, setOpen] = useState(false);
|
|
452
|
+
const label = useCellLabel(item);
|
|
453
|
+
const active = entryIsCurrent(item, pathname);
|
|
454
|
+
const close = () => setOpen(false);
|
|
455
|
+
const pages = item.items ?? [];
|
|
456
|
+
const ownPageListed = pages.some((sub) => sub.to === item.to);
|
|
457
|
+
return /* @__PURE__ */ jsxs("div", { "data-slot": "app-shell-group", className: "relative min-w-0", children: [
|
|
458
|
+
/* @__PURE__ */ jsxs(
|
|
459
|
+
"button",
|
|
460
|
+
{
|
|
461
|
+
type: "button",
|
|
462
|
+
"aria-haspopup": "dialog",
|
|
463
|
+
"aria-expanded": open,
|
|
464
|
+
"aria-label": label === item.label ? void 0 : item.label,
|
|
465
|
+
onClick: () => setOpen(true),
|
|
466
|
+
className: mobileCellClass(active),
|
|
467
|
+
children: [
|
|
468
|
+
/* @__PURE__ */ jsx("span", { className: "relative", children: /* @__PURE__ */ jsx(item.icon, { "aria-hidden": true, className: "size-5" }) }),
|
|
469
|
+
/* @__PURE__ */ jsx(MobileCellLabel, { children: label })
|
|
470
|
+
]
|
|
471
|
+
}
|
|
472
|
+
),
|
|
473
|
+
/* @__PURE__ */ jsx(TourAnchor, { to: item.to, dataTour: item.dataTour }),
|
|
474
|
+
pages.filter((sub) => sub.to !== item.to).map((sub) => /* @__PURE__ */ jsx(TourAnchor, { to: sub.to }, sub.to)),
|
|
475
|
+
/* @__PURE__ */ jsx(DialogFrame, { open, onClose: close, title: item.label, closeButton: true, bodyClassName: "space-y-1", children: /* @__PURE__ */ jsxs("ul", { className: "space-y-1", children: [
|
|
476
|
+
!ownPageListed && /* @__PURE__ */ jsx("li", { children: /* @__PURE__ */ jsx(SheetLink, { to: item.to, end: item.end, icon: item.icon, label: labels.overview, onClick: close, level: "entry" }) }),
|
|
477
|
+
pages.map((sub) => /* @__PURE__ */ jsx("li", { children: /* @__PURE__ */ jsx(SheetLink, { to: sub.to, end: sub.end, icon: sub.icon, label: sub.label, onClick: close, level: "entry" }) }, sub.to))
|
|
478
|
+
] }) })
|
|
479
|
+
] });
|
|
480
|
+
}
|
|
345
481
|
function entryIsCurrent(item, pathname) {
|
|
346
482
|
return !!matchPath({ path: item.to, end: item.end ?? true }, pathname) || !!item.items?.some((sub) => matchPath({ path: sub.to, end: sub.end ?? true }, pathname));
|
|
347
483
|
}
|
|
@@ -390,38 +526,8 @@ function MobileMoreCell({
|
|
|
390
526
|
...(item.items ?? []).map((sub) => /* @__PURE__ */ jsx(TourAnchor, { to: sub.to }, `${item.to} ${sub.to}`))
|
|
391
527
|
]),
|
|
392
528
|
/* @__PURE__ */ jsx(DialogFrame, { open, onClose: close, title: labels.moreTitle, closeButton: true, bodyClassName: "space-y-1", children: /* @__PURE__ */ jsx("ul", { className: "space-y-1", children: hidden.map((item) => /* @__PURE__ */ jsxs("li", { children: [
|
|
393
|
-
/* @__PURE__ */
|
|
394
|
-
|
|
395
|
-
{
|
|
396
|
-
to: item.to,
|
|
397
|
-
end: item.end ?? true,
|
|
398
|
-
onClick: close,
|
|
399
|
-
className: ({ isActive }) => cn(
|
|
400
|
-
"flex min-h-11 items-center gap-3 rounded-md px-3 py-2 text-sm transition-colors",
|
|
401
|
-
isActive ? "bg-inverse text-inverse" : "text-primary hover:bg-hover"
|
|
402
|
-
),
|
|
403
|
-
children: [
|
|
404
|
-
/* @__PURE__ */ jsx(item.icon, { "aria-hidden": true, className: "size-5 shrink-0" }),
|
|
405
|
-
/* @__PURE__ */ jsx("span", { className: "min-w-0 break-words leading-snug", children: item.label })
|
|
406
|
-
]
|
|
407
|
-
}
|
|
408
|
-
),
|
|
409
|
-
!!item.items?.length && /* @__PURE__ */ jsx("ul", { "aria-label": item.label, className: "ms-[1.3rem] mt-0.5 space-y-0.5 border-s border-subtle ps-2", children: item.items.map((sub) => /* @__PURE__ */ jsx("li", { children: /* @__PURE__ */ jsxs(
|
|
410
|
-
NavLink,
|
|
411
|
-
{
|
|
412
|
-
to: sub.to,
|
|
413
|
-
end: sub.end ?? true,
|
|
414
|
-
onClick: close,
|
|
415
|
-
className: ({ isActive }) => cn(
|
|
416
|
-
"flex min-h-11 items-center gap-2.5 rounded-md px-2.5 py-1.5 text-sm transition-colors",
|
|
417
|
-
isActive ? "bg-inverse text-inverse" : "text-secondary hover:bg-hover"
|
|
418
|
-
),
|
|
419
|
-
children: [
|
|
420
|
-
/* @__PURE__ */ jsx(sub.icon, { "aria-hidden": true, className: "size-4 shrink-0 opacity-70" }),
|
|
421
|
-
/* @__PURE__ */ jsx("span", { className: "min-w-0 break-words leading-snug", children: sub.label })
|
|
422
|
-
]
|
|
423
|
-
}
|
|
424
|
-
) }, sub.to)) })
|
|
529
|
+
/* @__PURE__ */ jsx(SheetLink, { to: item.to, end: item.end, icon: item.icon, label: item.label, onClick: close, level: "entry" }),
|
|
530
|
+
!!item.items?.length && /* @__PURE__ */ jsx("ul", { "aria-label": item.label, className: "ms-[1.3rem] mt-0.5 space-y-0.5 border-s border-subtle ps-2", children: item.items.map((sub) => /* @__PURE__ */ jsx("li", { children: /* @__PURE__ */ jsx(SheetLink, { to: sub.to, end: sub.end, icon: sub.icon, label: sub.label, onClick: close, level: "page" }) }, sub.to)) })
|
|
425
531
|
] }, item.to)) }) })
|
|
426
532
|
] });
|
|
427
533
|
}
|
|
@@ -733,6 +839,8 @@ export {
|
|
|
733
839
|
AppShell,
|
|
734
840
|
DEFAULT_APP_SHELL_MORE_LABELS,
|
|
735
841
|
DEFAULT_MOBILE_BAR_MAX,
|
|
842
|
+
DEFAULT_MOBILE_MAX_ITEMS,
|
|
843
|
+
pickMobileBar,
|
|
736
844
|
splitMobileBar
|
|
737
845
|
};
|
|
738
846
|
//# sourceMappingURL=app-shell.js.map
|