Merge pull request 'fix: group mobile drawer sections (remove bottom-pinned void)' (#28) from fix/mobile-drawer-spacing into main

Merge PR #28: group mobile drawer sections (remove bottom-pinned void)

MOBILE-SHELL-PARITY follow-up: in the mobile drawer the nav's flex-1 pushed
WorkspaceSwitcher to the bottom, leaving a void between menu and switcher.
SidebarNav gains a `grow` prop (default true = desktop bottom-anchor); the
mobile drawer passes grow=false (flex-initial) so logo/menu/switcher group at
the top and a long menu still scrolls internally. Plus a Storybook router fix.
Reviewer PASS; 583 frontend tests green.
This commit was merged in pull request #28.
This commit is contained in:
2026-06-03 14:14:56 +02:00
4 changed files with 56 additions and 6 deletions

View File

@@ -15,6 +15,9 @@ const routeNames = [
'organisation-companies', 'organisation-form-failures',
'organisation-settings', 'platform', 'platform-organisations',
'platform-users', 'platform-form-failures', 'platform-activity-log',
// v2 shell nav targets (APP_NAVIGATION uses v2-prefixed route names) —
// without these, AppSidebar/SidebarNav RouterLinks throw "No match for".
'v2-dashboard',
]
export const storyRouter = createRouter({

View File

@@ -165,7 +165,10 @@ const mobileVisible = computed<boolean>({
}"
>
<SidebarHeader />
<SidebarNav :collapsed="false" />
<SidebarNav
:collapsed="false"
:grow="false"
/>
<WorkspaceSwitcher :collapsed="false" />
</Drawer>
</template>

View File

@@ -22,9 +22,21 @@ import type { RouteLocationRaw } from 'unplugin-vue-router'
import Icon from '@/components/Icon.vue'
import { APP_NAVIGATION, type NavItem } from '@/config/navigation'
defineProps<{
withDefaults(defineProps<{
collapsed: boolean
}>()
/**
* When true (default), the nav fills available vertical space (`flex-1`)
* so a bottom-anchored sibling (WorkspaceSwitcher) pins to the bottom — the
* desktop sidebar behaviour. Set false in the mobile drawer (`flex-initial`)
* so the nav takes its natural height — logo + menu + switcher group together
* at the top instead of leaving a void above a bottom-pinned switcher
* (MOBILE-SHELL-PARITY follow-up). `flex-initial` (not `flex-none`) keeps
* `flex-shrink: 1`, so if the menu ever grows past the panel height the nav
* shrinks and scrolls internally (`min-h-0 overflow-y-auto`) rather than
* pushing the switcher out of the `overflow-hidden` drawer content.
*/
grow?: boolean
}>(), { grow: true })
const route = useRoute()
@@ -52,7 +64,10 @@ function itemTo(item: NavItem): RouteLocationRaw {
</script>
<template>
<nav class="flex-1 min-h-0 overflow-y-auto py-3.5 px-2.5 [scrollbar-width:thin]">
<nav
class="min-h-0 overflow-y-auto py-3.5 px-2.5 [scrollbar-width:thin]"
:class="grow ? 'flex-1' : 'flex-initial'"
>
<template
v-for="topItem in APP_NAVIGATION"
:key="topItem.key"

View File

@@ -66,8 +66,8 @@ const globalStubs = {
SidebarHeader: { name: 'SidebarHeader', template: '<div class="sidebar-header-stub" />' },
SidebarNav: {
name: 'SidebarNav',
props: ['collapsed'],
template: '<div class="sidebar-nav-stub" :data-collapsed="collapsed" />',
props: ['collapsed', 'grow'],
template: '<div class="sidebar-nav-stub" :data-collapsed="collapsed" :data-grow="grow" />',
},
WorkspaceSwitcher: {
name: 'WorkspaceSwitcher',
@@ -203,6 +203,35 @@ describe('AppSidebar', () => {
expect(drawer.find('.workspace-switcher-stub').exists()).toBe(true)
})
it('mobile: SidebarNav is told NOT to grow (grow=false) so logo/menu/switcher group at the top', async () => {
mockIsMobileRef.value = true
const wrapper = mountSidebar()
await wrapper.vm.$nextTick()
// grow=false → nav takes natural height, no flex-1 fill → no void above a
// bottom-pinned WorkspaceSwitcher (they group together at the top instead).
const navInDrawer = wrapper.find('.drawer-stub').find('.sidebar-nav-stub')
expect(navInDrawer.attributes('data-grow')).toBe('false')
})
it('desktop: SidebarNav is allowed to grow (WorkspaceSwitcher stays bottom-anchored)', async () => {
mockIsMobileRef.value = false
const wrapper = mountSidebar()
await wrapper.vm.$nextTick()
// The desktop <aside> passes no grow prop → defaults to true (flex-1),
// keeping the switcher pinned at the bottom. The stub does not apply the
// component default, so we assert grow is NOT explicitly false here.
const navInAside = wrapper.find('aside').find('.sidebar-nav-stub')
expect(navInAside.attributes('data-grow')).not.toBe('false')
})
// -------------------------------------------------------------------------
// Desktop <aside> width class based on sidebarCollapsed
// -------------------------------------------------------------------------