feat(nav): group rail modules + move chrome controls into the sidebar

- Split the rail into a content group (Feed/Channels/Playlists) and an admin
  group (Stats/Scheduler) separated by a divider (system group hidden for non-admins).
- Move the language switcher, About and notification bell out of the top header
  into an icon cluster above Settings (horizontal expanded, vertical collapsed).
  Their popovers portal to <body> and anchor right + above the button, escaping the
  nav's backdrop-filter. About is removed from the account popover (now in the cluster).
- LanguageSwitcher/NotificationCenter gain a 'rail' variant for the above.
- Also relocate the Toaster mount into the (now relative) content column.
This commit is contained in:
npeter83 2026-06-17 14:28:29 +02:00
parent dfaa1551dc
commit df0ca24a23
5 changed files with 194 additions and 54 deletions

View file

@ -86,7 +86,7 @@ function loadInitialFilters(): FeedFilters {
}
export default function App() {
const { t } = useTranslation();
const { t, i18n } = useTranslation();
const [theme, setThemeState] = useState<ThemePrefs>(() => loadLocalTheme());
const [filters, setFiltersState] = useState<FeedFilters>(loadInitialFilters);
const [view, setView] = useState<"grid" | "list">("grid");
@ -235,14 +235,17 @@ export default function App() {
page={page}
setPage={setPage}
onOpenAbout={() => setAboutOpen(true)}
onChangeLanguage={changeLanguage}
language={i18n.language as LangCode}
filters={filters}
setFilters={setFilters}
/>
<div className="flex-1 min-w-0 flex flex-col">
<div className="relative flex-1 min-w-0 flex flex-col">
<Header
me={meQuery.data!}
filters={filters}
setFilters={setFilters}
page={page}
onChangeLanguage={changeLanguage}
onGoToFullHistory={() => {
setChannelFilter("needs_full");
setPage("channels");
@ -298,6 +301,10 @@ export default function App() {
)}
</main>
</div>
{/* Toasts rise from the bottom-left, by the notification bell in the nav rail.
Anchored inside the content column so they clear the sidebar automatically
(collapsed or expanded) without tracking its width. */}
<Toaster />
</div>
{wizardOpen && meQuery.data && !meQuery.data.is_demo && (
<OnboardingWizard me={meQuery.data} onClose={() => setWizardOpen(false)} />
@ -314,7 +321,6 @@ export default function App() {
{notesOpen && (
<ReleaseNotes onClose={() => setNotesOpen(false)} highlight={notesHighlight} />
)}
<Toaster />
</div>
);
}