From 3ef1d8935fe783f503818777a995d10622821b8c Mon Sep 17 00:00:00 2001 From: Sadiq Ansari Date: Thu, 13 Aug 2026 16:33:47 +0530 Subject: [PATCH 1/5] refactor(shell): each page owns its header, the way the shells expect MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit AppShell hand-rolled one global header for every page, plus an ad-hoc `#header-actions` teleport target that three pages reached into. Both MobileShell and DesktopShell already render a PageHeaderTarget above their scroll region for exactly this: a page declares its own header and it teleports there. We used neither. That bypass is what cost every mobile page its last 48px. Our header sat inside MobileShell's scroll area rather than above it, so the bottom of every page hid behind the nav bar, and the workaround was a hand-managed flex column. Owning the header per page removes the class of bug and turns on what the components already do: a back button, a centred two-line title, tap-the-header to scroll top, tap-the-active-tab to scroll top. The pattern is taken from frappe/gameplan, which is where frappe-ui's shell components were extracted from. Slot names drift between versions — beta.24 takes #left/#right where later releases take #prefix/#suffix — and Vue drops an unknown named slot silently, so these are written against the installed source rather than copied from gameplan's templates. Two things follow from the shell owning the scroll region: Pages hand it their own scroll boxes behind `sm:` prefixes, which is what makes tapping the already-active bottom-nav tab scroll to top. And scroll position is now reset by the router, not by pages remounting. The shell's scroll div outlives the route, so without it a new page opened wherever the last one was left. Only a real path change resets — a query-only replace is the same page and keeps its place. The mobile header's title also doubles as the section switcher: the bottom bar holds four tabs, so Servers, Services, Team and the billing pages were otherwise reachable only by going back to Home first. Home keeps a plain title, since that page already is the list, and takes the mark on the left instead. useBreadcrumbs loses its last consumer here and goes with them. ServerFilters comes along because the servers page now hands it region options grouped by provider, and a prop type has to move with the thing that passes it. Co-Authored-By: Claude Opus 5 --- .../components/navigation/NavDrawerTitle.vue | 43 ++++++++ .../src/components/navigation/Sidebar.vue | 42 ++++++-- dashboard/src/components/navigation/list.ts | 10 +- .../src/components/servers/ServerFilters.vue | 22 +++-- .../components/settings/forms/TeamForm.vue | 10 +- dashboard/src/components/team/TeamList.vue | 30 +++++- dashboard/src/composables/useBreadcrumbs.ts | 18 ---- dashboard/src/composables/useSettings.ts | 73 ++++++++++---- dashboard/src/layouts/AppShell.vue | 66 ++++--------- dashboard/src/pages/addons/AIInference.vue | 29 +++++- dashboard/src/pages/addons/Page.vue | 17 +++- .../src/pages/billing/BillingOverviewPage.vue | 26 +++-- .../src/pages/billing/BillingReportsPage.vue | 33 ++++++- dashboard/src/pages/home/HomePage.vue | 17 ++++ .../pages/notifications/NotificationsPage.vue | 54 +++++++++-- dashboard/src/pages/servers/NewServerPage.vue | 43 ++++++-- dashboard/src/pages/servers/ServersPage.vue | 88 +++++++++++++---- .../src/pages/settings/SettingsDetailPage.vue | 67 ++++++++----- .../pages/settings/SettingsDialogRoute.vue | 97 +++++++++++++++++++ dashboard/src/pages/settings/SettingsPage.vue | 45 +++++++-- dashboard/src/pages/team/AccessPage.vue | 32 +++++- dashboard/src/pages/team/InvitationsPage.vue | 16 ++- dashboard/src/router/index.ts | 38 ++++++-- 23 files changed, 720 insertions(+), 196 deletions(-) create mode 100644 dashboard/src/components/navigation/NavDrawerTitle.vue delete mode 100644 dashboard/src/composables/useBreadcrumbs.ts create mode 100644 dashboard/src/pages/settings/SettingsDialogRoute.vue diff --git a/dashboard/src/components/navigation/NavDrawerTitle.vue b/dashboard/src/components/navigation/NavDrawerTitle.vue new file mode 100644 index 00000000..aaaf7b73 --- /dev/null +++ b/dashboard/src/components/navigation/NavDrawerTitle.vue @@ -0,0 +1,43 @@ + + + diff --git a/dashboard/src/components/navigation/Sidebar.vue b/dashboard/src/components/navigation/Sidebar.vue index 2e7959e4..6be73bc8 100644 --- a/dashboard/src/components/navigation/Sidebar.vue +++ b/dashboard/src/components/navigation/Sidebar.vue @@ -7,7 +7,7 @@ import { SidebarItem, SidebarLabel, } from 'frappe-ui' -import { onScopeDispose, ref, watch } from 'vue' +import { computed, onScopeDispose, ref, watch } from 'vue' import { useRoute } from 'vue-router' import frappeCloudLogo from '@/assets/fc-logo.svg' import { useAppMenu } from '@/composables/useAppMenu' @@ -17,6 +17,21 @@ import { sidebarSections } from './list' const props = defineProps<{ isMobile?: boolean }>() +// Search and Notifications are their own tabs in the mobile bottom bar, so the +// drawer drops them and shows only what the bar can't reach. A section left +// empty by that drops out with them. +const sections = computed(() => + sidebarSections.value + .map((section) => ({ + ...section, + items: section.items.filter( + (item) => + item.condition !== false && !(props.isMobile && item.hideOnMobile), + ), + })) + .filter((section) => section.items.length > 0), +) + const { activeTeamLabel } = useSession() const { currentUser, headerMenuItems, footerMenuItems } = useAppMenu() const { profile } = useMyProfile() @@ -80,10 +95,7 @@ onScopeDispose(() => cancelAnimationFrame(edgeRaf)) />