feat(appshell): add org-switcher card and bump sidebar width to w-72
Introduces SidebarHeader.vue — a PrimeVue-only org-switcher that replaces the centered Crewli wordmark at the top of the sidebar. The component mirrors the legacy Vuetify OrganisationSwitcher (avatar with org initials, organisation name, plan-tier placeholder, dropdown chevron, PrimeVue Menu of available orgs) but cannot reuse it directly per the R-10 layout-shell-isolation invariant. Plan-tier shows a hardcoded "Pro" placeholder until the backend Organisation resource exposes a plan field — tracked separately, not in F3.5 scope. When the user has no active organisation (portal users, fresh super_admin), the component degrades to the original title block so PortalLayout continues to read "Crewli Portal". Desktop sidebar width bumped w-64 → w-72 (256 → 288 px) to give the org-switcher card breathing room and accommodate the user-info card arriving in B3. Mobile Drawer width bumped 16rem → 18rem to match. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -94,10 +94,8 @@ function toggleUserMenu(event: Event) {
|
||||
<template>
|
||||
<div class="crewli-app-shell flex min-h-screen">
|
||||
<!-- Desktop sidebar (lg+) -->
|
||||
<aside class="hidden lg:flex w-64 flex-col border-r border-surface-200 bg-surface-0">
|
||||
<div class="flex h-16 items-center justify-center border-b border-surface-200">
|
||||
<span class="text-xl font-semibold text-primary-500">{{ title }}</span>
|
||||
</div>
|
||||
<aside class="hidden lg:flex w-72 flex-col border-r border-surface-200 bg-surface-0">
|
||||
<SidebarHeader :title="title" />
|
||||
<nav class="flex-1 overflow-y-auto p-3">
|
||||
<template
|
||||
v-for="(item, idx) in navItems"
|
||||
@@ -130,12 +128,12 @@ function toggleUserMenu(event: Event) {
|
||||
v-model:visible="mobileNavOpen"
|
||||
position="left"
|
||||
class="lg:hidden"
|
||||
:pt="{ root: { style: { width: '16rem' } } }"
|
||||
:pt="{ root: { style: { width: '18rem' } }, header: { class: 'p-0' } }"
|
||||
>
|
||||
<template #header>
|
||||
<span class="text-lg font-semibold text-primary-500">{{ title }}</span>
|
||||
<SidebarHeader :title="title" />
|
||||
</template>
|
||||
<nav class="flex flex-col">
|
||||
<nav class="flex flex-col p-3">
|
||||
<template
|
||||
v-for="(item, idx) in navItems"
|
||||
:key="idx"
|
||||
|
||||
104
apps/app/src/layouts/components/SidebarHeader.vue
Normal file
104
apps/app/src/layouts/components/SidebarHeader.vue
Normal file
@@ -0,0 +1,104 @@
|
||||
<script setup lang="ts">
|
||||
// SidebarHeader — top-of-sidebar org-switcher card introduced in F3.5
|
||||
// per the AppShell mockup-parity sprint. PrimeVue-only rewrite of the
|
||||
// legacy Vuetify OrganisationSwitcher (apps/app/src/components/layout/
|
||||
// OrganisationSwitcher.vue), which cannot be reused inside AppShell
|
||||
// per the R-10 layout-shell-isolation invariant.
|
||||
//
|
||||
// When the auth store has no active organisation (e.g. portal users,
|
||||
// or a fresh super_admin without an org), the component degrades to a
|
||||
// plain title block — the same visual the F3 AppShell shipped with.
|
||||
|
||||
import { computed, ref } from 'vue'
|
||||
import Menu from 'primevue/menu'
|
||||
import Avatar from 'primevue/avatar'
|
||||
import Icon from '@/components/Icon.vue'
|
||||
import { useAuthStore } from '@/stores/useAuthStore'
|
||||
|
||||
interface Props {
|
||||
title?: string
|
||||
}
|
||||
|
||||
withDefaults(defineProps<Props>(), {
|
||||
title: 'Crewli',
|
||||
})
|
||||
|
||||
const authStore = useAuthStore()
|
||||
const menuRef = ref<InstanceType<typeof Menu> | null>(null)
|
||||
|
||||
const currentOrg = computed(() => authStore.currentOrganisation)
|
||||
const organisations = computed(() => authStore.organisations)
|
||||
const hasSwitcher = computed(() => !!currentOrg.value)
|
||||
const hasMultiple = computed(() => organisations.value.length > 1)
|
||||
|
||||
const orgInitials = computed(() => {
|
||||
const name = currentOrg.value?.name ?? ''
|
||||
const parts = name.split(/\s+/).filter(Boolean).slice(0, 2)
|
||||
const initials = parts.map(w => w[0]?.toUpperCase() ?? '').join('')
|
||||
|
||||
return initials || '?'
|
||||
})
|
||||
|
||||
// Plan tier placeholder. Backend `Organisation` resource does not yet
|
||||
// expose a plan field (see types/auth.ts:17-27); 'Pro' is hardcoded
|
||||
// per Bert's A6.1 decision until that field lands.
|
||||
const planLabel = 'Pro'
|
||||
|
||||
const menuItems = computed(() =>
|
||||
organisations.value.map(org => ({
|
||||
label: org.name,
|
||||
command: () => authStore.setActiveOrganisation(org.id),
|
||||
})),
|
||||
)
|
||||
|
||||
function toggleMenu(event: Event) {
|
||||
if (!hasMultiple.value)
|
||||
return
|
||||
menuRef.value?.toggle(event)
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="border-b border-surface-200">
|
||||
<button
|
||||
v-if="hasSwitcher"
|
||||
type="button"
|
||||
class="flex w-full items-center gap-3 px-3 py-3 text-left transition enabled:hover:bg-surface-100 disabled:cursor-default"
|
||||
:disabled="!hasMultiple"
|
||||
:aria-label="hasMultiple ? 'Organisatie wisselen' : currentOrg?.name"
|
||||
:aria-haspopup="hasMultiple ? 'menu' : undefined"
|
||||
@click="toggleMenu"
|
||||
>
|
||||
<Avatar
|
||||
:label="orgInitials"
|
||||
shape="circle"
|
||||
class="shrink-0 bg-primary-500 text-white"
|
||||
/>
|
||||
<div class="flex min-w-0 flex-1 flex-col">
|
||||
<span class="truncate text-sm font-semibold text-surface-900">
|
||||
{{ currentOrg?.name }}
|
||||
</span>
|
||||
<span class="text-xs text-surface-500">{{ planLabel }}</span>
|
||||
</div>
|
||||
<Icon
|
||||
v-if="hasMultiple"
|
||||
name="tabler-chevron-down"
|
||||
size="18"
|
||||
class="shrink-0 text-surface-500"
|
||||
/>
|
||||
</button>
|
||||
|
||||
<div
|
||||
v-else
|
||||
class="flex h-16 items-center justify-center px-3"
|
||||
>
|
||||
<span class="text-xl font-semibold text-primary-500">{{ title }}</span>
|
||||
</div>
|
||||
|
||||
<Menu
|
||||
ref="menuRef"
|
||||
:model="menuItems"
|
||||
:popup="true"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
Reference in New Issue
Block a user