refactor(gui-v2): cleanup(b) — AppTopbar wraps PrimeVue Menubar per RFC AD-3
This commit is contained in:
@@ -24,6 +24,10 @@
|
||||
*
|
||||
* Icon convention: import Icon from '@/components/Icon.vue', <Icon name="tabler-x" :size="N" />
|
||||
*
|
||||
* RFC AD-3: PrimeVue Menubar is the chrome root (wrap-don't-rewrite).
|
||||
* Breadcrumb block → #start slot; search/notifications/user cluster → #end slot.
|
||||
* :model="[]" — no Menubar menu items; Menubar is the AD-3 chrome shell only.
|
||||
*
|
||||
* Styling: crewli-starter CSS translated to Tailwind inline.
|
||||
* <style scoped> used only for:
|
||||
* 1. topbar-mark-shadow — inset box-shadow (no Tailwind utility at this granularity, RFC §7.4)
|
||||
@@ -33,6 +37,7 @@
|
||||
import Avatar from 'primevue/avatar'
|
||||
import Breadcrumb from 'primevue/breadcrumb'
|
||||
import InputText from 'primevue/inputtext'
|
||||
import Menubar from 'primevue/menubar'
|
||||
import Menu from 'primevue/menu'
|
||||
import OverlayBadge from 'primevue/overlaybadge'
|
||||
import Popover from 'primevue/popover'
|
||||
@@ -240,12 +245,21 @@ const userMenuItems = computed<MenuItem[]>(() => [
|
||||
|
||||
<template>
|
||||
<!--
|
||||
.topbar: sticky top-0, h-[var(--topbar-h)], flex items-center, gap-3,
|
||||
bg-[var(--surface)], border-b border-[var(--border)], z-30, px-5
|
||||
RFC AD-3: PrimeVue Menubar is the chrome root.
|
||||
:model="[]" — no Menubar menu items; Menubar is the AD-3 chrome shell only.
|
||||
:pt maps Menubar internal regions to the existing hand-rolled chrome classes
|
||||
so visual layout is not gratuitously altered (pixel parity deferred to Bert).
|
||||
-->
|
||||
<header class="sticky top-0 z-30 flex h-[var(--topbar-h,56px)] items-center gap-3 border-b border-[var(--p-content-border-color)] bg-[var(--p-content-background)] px-5">
|
||||
<!-- Left group: hamburger + brand + breadcrumb -->
|
||||
<div class="flex items-center gap-[6px]">
|
||||
<Menubar
|
||||
:model="[]"
|
||||
:pt="{
|
||||
root: 'sticky top-0 z-30 h-[var(--topbar-h,56px)] border-0 border-b border-[var(--p-content-border-color)] rounded-none bg-[var(--p-content-background)] px-5',
|
||||
start: 'flex items-center gap-[6px] min-w-0',
|
||||
end: 'ms-auto flex items-center gap-[6px]',
|
||||
}"
|
||||
>
|
||||
<template #start>
|
||||
<div data-tb="breadcrumb">
|
||||
<!--
|
||||
Hamburger — mobile only (lg:hidden).
|
||||
.hamburger: display none at >=lg, display inline-flex at <768px
|
||||
@@ -302,15 +316,16 @@ const userMenuItems = computed<MenuItem[]>(() => [
|
||||
/>
|
||||
</nav>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<!-- Right group: ws-mobile-btn, search, density, theme, notifications, user -->
|
||||
<div class="ms-auto flex items-center gap-[6px]">
|
||||
<template #end>
|
||||
<!--
|
||||
Mobile workspace button — visible only <lg.
|
||||
.ws-mobile-btn: display none (>= 1024px). On mobile: display flex.
|
||||
38x38, rounded, color #fff, font-bold 12px, gradient bg via inline :style
|
||||
(dynamic hex pair — RFC §7.4). Box-shadow via scoped CSS.
|
||||
-->
|
||||
<div data-tb="search">
|
||||
<button
|
||||
v-if="authStore.currentOrganisation"
|
||||
type="button"
|
||||
@@ -345,6 +360,7 @@ const userMenuItems = computed<MenuItem[]>(() => [
|
||||
⌘K
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!--
|
||||
Density toggle.
|
||||
@@ -380,6 +396,7 @@ const userMenuItems = computed<MenuItem[]>(() => [
|
||||
PrimeVue Popover replaces crewli-starter's manual document.addEventListener('mousedown', …)
|
||||
TODO TECH-WS-GUI-REDESIGN: real notification feed — not foundation scope
|
||||
-->
|
||||
<div data-tb="notifications">
|
||||
<OverlayBadge
|
||||
:value="notifCount > 0 ? String(notifCount) : undefined"
|
||||
severity="danger"
|
||||
@@ -402,6 +419,7 @@ const userMenuItems = computed<MenuItem[]>(() => [
|
||||
No notifications
|
||||
</div>
|
||||
</Popover>
|
||||
</div>
|
||||
|
||||
<!--
|
||||
User menu — PrimeVue Avatar + Menu.
|
||||
@@ -409,6 +427,7 @@ const userMenuItems = computed<MenuItem[]>(() => [
|
||||
Menu: PrimeVue Menu replaces crewli-starter's manual click-outside listener.
|
||||
#start slot used for user info header.
|
||||
-->
|
||||
<div data-tb="user">
|
||||
<Avatar
|
||||
:label="userInitials"
|
||||
shape="circle"
|
||||
@@ -442,7 +461,8 @@ const userMenuItems = computed<MenuItem[]>(() => [
|
||||
</template>
|
||||
</Menu>
|
||||
</div>
|
||||
</header>
|
||||
</template>
|
||||
</Menubar>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
|
||||
@@ -50,7 +50,19 @@ vi.mock('vue-router', () => ({
|
||||
// Stubs for PrimeVue components
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* MenubarStub — mirrors the PrimeVue Menubar slot contract used by AppTopbar
|
||||
* (RFC AD-3). Renders #start and #end named slots so [data-tb="breadcrumb"]
|
||||
* and [data-tb="user"] are inspectable inside the stub.
|
||||
*/
|
||||
const MenubarStub = {
|
||||
name: 'Menubar',
|
||||
props: ['model', 'pt'],
|
||||
template: '<div class="menubar-stub"><slot name="start" /><slot name="end" /></div>',
|
||||
}
|
||||
|
||||
const globalStubs = {
|
||||
Menubar: MenubarStub,
|
||||
Breadcrumb: { name: 'Breadcrumb', props: ['model'], template: '<nav class="breadcrumb-stub" />' },
|
||||
InputText: { name: 'InputText', template: '<input class="input-text-stub" />' },
|
||||
OverlayBadge: { name: 'OverlayBadge', props: ['value', 'severity'], template: '<div class="overlay-badge-stub"><slot /></div>' },
|
||||
@@ -286,4 +298,22 @@ describe('AppTopbar', () => {
|
||||
expect(lastItem.command).toBeUndefined()
|
||||
expect('route' in lastItem).toBe(false)
|
||||
})
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// RFC AD-3: AppTopbar wraps PrimeVue Menubar as chrome root
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
it('renders the top bar inside a PrimeVue Menubar (RFC AD-3)', () => {
|
||||
const w = mountTopbar()
|
||||
|
||||
expect(w.findComponent(MenubarStub).exists()).toBe(true)
|
||||
})
|
||||
|
||||
it('breadcrumb lives in Menubar #start, user cluster in #end', () => {
|
||||
const w = mountTopbar()
|
||||
const bar = w.findComponent(MenubarStub)
|
||||
|
||||
expect(bar.find('[data-tb="breadcrumb"]').exists()).toBe(true)
|
||||
expect(bar.find('[data-tb="user"]').exists()).toBe(true)
|
||||
})
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user