style(layout): sidebar brand-square symmetry + WorkspaceSwitcher trigger polish
Plan 2.5 P6 follow-up. Closes two desktop shell-parity gaps from P6 manual smoke against crewli-starter SoT: - Sidebar logo decoupled from the collapse toggle. Expanded layout is now justify-between (brand group left, collapse chevron right) and collapsed layout is justify-center with the logo alone. The expand affordance becomes a small absolute-positioned circular button at the rail's right edge — solid background + border so the slight overlap with the centred logo reads as a tucked-in chip rather than a collision. Toggling collapsed no longer shifts the logo. - Brand square (SidebarHeader logo) and workspace avatar (WorkspaceSwitcher trigger) unified to the same rounded square (h-8 w-8 rounded-xl). Existing sizes were already consistent at 32px — radius bumped from rounded-lg (8px) / var(--p-border-radius) (~6px) to rounded-xl (12px) per the design direction. Collapsed rail now reads as a vertical mirror: brand square at the top, avatar square at the bottom, bracketing the nav icons. - WorkspaceSwitcher trigger restyled: rounded-xl (was the sharper var-radius), p-2 (was px-[10px] py-[8px]), hover background. The collapsed-variant gating of name + chevron is unchanged from P5. Edge-mounted overhang past the rail edge was not possible: the aside carries `overflow-hidden` (intentional, for the w-64 ⇄ w-16 width transition) which clips anything past the rail edge. The tucked-chip pattern (24px circle at end-0, solid bg) is the visual compromise — the affordance stays inside the rail, discoverable, and visually decoupled from the logo. Desktop only. Mobile drawer chrome (logo placement, drawer X button, missing switcher) tracked separately as MOBILE-SHELL-PARITY. Tests: - +2 WorkspaceSwitcher.spec.ts: trigger uses rounded-xl; collapsed trigger renders avatar only (hides .meta). - +1 SidebarHeader.spec.ts: collapsed row hides the .brand-name wordmark and toggles to justify-center (logo-stays-centred lock). Suite delta: 558 → 561 (+3). vue-tsc clean. Scoped ESLint clean (0 errors, pre-existing warnings only). Manual smoke pending Bert: collapse the rail, verify logo stays put and the expand chip appears at the right edge; verify the trigger shows rounded corners + hover bg; verify the collapsed avatar mirrors the brand square size/radius. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -12,29 +12,24 @@
|
|||||||
* - Desktop (>=lg): toggle the persistent sidebar between expanded/collapsed
|
* - Desktop (>=lg): toggle the persistent sidebar between expanded/collapsed
|
||||||
* → call toggleSidebar().
|
* → call toggleSidebar().
|
||||||
*
|
*
|
||||||
* CSS translation (main.css → Tailwind):
|
* Plan 2.5 P6-followup-styling: the collapse-toggle is decoupled from the
|
||||||
* .brand → flex items-center gap-[10px] h-[var(--topbar-h,56px)]
|
* brand group so the logo stays centered in the collapsed rail.
|
||||||
* px-4 border-b border-[var(--p-content-border-color)]
|
* - Expanded: justify-between row — [brand group] [collapse chevron].
|
||||||
* flex-shrink-0 relative
|
* - Collapsed: justify-center row — [logo only], with the expand chevron
|
||||||
* .brand.collapsed → justify-center (via :class when sidebarCollapsed)
|
* as a small floating circle absolute-positioned at the rail's right
|
||||||
* .mark → w-8 h-8 flex-shrink-0 rounded-lg
|
* edge (tucked over the logo's right side with a solid background
|
||||||
* bg-gradient-to-br from-primary-500 to-primary-700
|
* so the visual reads as an intentional chip, not an overlap). The
|
||||||
* inline-flex items-center justify-center
|
* aside's `overflow-hidden` (intentional, for the width transition)
|
||||||
* text-white font-bold text-sm
|
* prevents true overhang past the rail edge — the tucked pattern is
|
||||||
* .wordmark → font-bold text-base tracking-[-0.01em] whitespace-nowrap
|
* the visual compromise that keeps the affordance discoverable.
|
||||||
* .pill → text-[9px] font-semibold bg-primary-50 dark:bg-primary-950
|
|
||||||
* text-primary-600 dark:text-primary-400 px-1.5 py-px
|
|
||||||
* rounded-full uppercase tracking-[0.05em]
|
|
||||||
* .sidebar-collapse → ms-auto w-7 h-7 border-0 bg-transparent
|
|
||||||
* text-[var(--p-text-muted-color)] rounded-[var(--p-border-radius)]
|
|
||||||
* inline-flex items-center justify-center
|
|
||||||
* transition-[background,color] duration-150
|
|
||||||
* hover:bg-[var(--p-content-hover-background)]
|
|
||||||
* hover:text-[var(--p-text-color)]
|
|
||||||
*
|
*
|
||||||
* <style scoped> is used for the inset box-shadow on .mark because
|
* The logo square shares its size + radius with WorkspaceSwitcher's
|
||||||
* Tailwind has no inset directional shadow utility at this granularity
|
* avatar square (h-8 w-8 rounded-xl) so the collapsed rail reads as
|
||||||
* (per RFC §7.4 — last resort, with justification comment).
|
* symmetric: brand-square at top, workspace-square at bottom, bracketing
|
||||||
|
* the nav icons.
|
||||||
|
*
|
||||||
|
* <style scoped> covers the inset box-shadow on .mark only (no Tailwind
|
||||||
|
* utility at this granularity, per RFC §7.4 last-resort).
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { breakpointsTailwind, useBreakpoints } from '@vueuse/core'
|
import { breakpointsTailwind, useBreakpoints } from '@vueuse/core'
|
||||||
@@ -61,42 +56,67 @@ function handleCollapseClick(): void {
|
|||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div
|
<div
|
||||||
class="flex items-center gap-[10px] h-[56px] border-b border-[var(--p-content-border-color)] flex-shrink-0 relative transition-[padding,justify-content] duration-200"
|
class="flex items-center h-[56px] border-b border-[var(--p-content-border-color)] flex-shrink-0 relative transition-[padding,justify-content] duration-200"
|
||||||
:class="shell.sidebarCollapsed ? 'justify-center px-0' : 'px-4'"
|
:class="shell.sidebarCollapsed ? 'justify-center px-0' : 'justify-between px-4'"
|
||||||
>
|
>
|
||||||
<!-- Logo mark: "C" in a gradient square -->
|
<!--
|
||||||
<span class="mark w-8 h-8 flex-shrink-0 rounded-lg bg-gradient-to-br from-primary-500 to-primary-700 inline-flex items-center justify-center text-white font-bold text-sm">
|
Expanded: brand group on the left (logo + wordmark + Beta pill).
|
||||||
C
|
Collapsed: only the logo square, centered in the rail.
|
||||||
</span>
|
-->
|
||||||
|
<div class="flex items-center gap-[10px]">
|
||||||
|
<!-- Brand square — shared treatment with WorkspaceSwitcher avatar (h-8 w-8 rounded-xl). -->
|
||||||
|
<span class="brand-mark mark w-8 h-8 flex-shrink-0 rounded-xl bg-gradient-to-br from-primary-500 to-primary-700 inline-flex items-center justify-center text-white font-bold text-sm">
|
||||||
|
C
|
||||||
|
</span>
|
||||||
|
|
||||||
<!-- Wordmark + Beta pill: hidden when sidebar is collapsed -->
|
<span
|
||||||
<span
|
v-if="!shell.sidebarCollapsed"
|
||||||
v-if="!shell.sidebarCollapsed"
|
class="brand-name font-bold text-base tracking-[-0.01em] whitespace-nowrap text-[var(--p-text-color)]"
|
||||||
class="font-bold text-base tracking-[-0.01em] whitespace-nowrap text-[var(--p-text-color)]"
|
>
|
||||||
>
|
Crewli
|
||||||
Crewli
|
</span>
|
||||||
</span>
|
|
||||||
|
|
||||||
<span
|
<span
|
||||||
v-if="!shell.sidebarCollapsed"
|
v-if="!shell.sidebarCollapsed"
|
||||||
class="text-[9px] font-semibold bg-primary-50 dark:bg-primary-950 text-primary-600 dark:text-primary-400 px-1.5 py-px rounded-full uppercase tracking-[0.05em]"
|
class="text-[9px] font-semibold bg-primary-50 dark:bg-primary-950 text-primary-600 dark:text-primary-400 px-1.5 py-px rounded-full uppercase tracking-[0.05em]"
|
||||||
>
|
>
|
||||||
Beta
|
Beta
|
||||||
</span>
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- Collapse-toggle button: always visible; acts as expand affordance when collapsed -->
|
<!-- Expanded: inline collapse chevron on the right (justify-between sibling). -->
|
||||||
<button
|
<button
|
||||||
class="ms-auto w-7 h-7 border-0 bg-transparent text-[var(--p-text-muted-color)] rounded-[var(--p-border-radius)] inline-flex items-center justify-center transition-[background,color] duration-150 hover:bg-[var(--p-content-hover-background)] hover:text-[var(--p-text-color)] cursor-pointer"
|
v-if="!shell.sidebarCollapsed"
|
||||||
:class="shell.sidebarCollapsed ? '!ms-0' : ''"
|
class="w-7 h-7 border-0 bg-transparent text-[var(--p-text-muted-color)] rounded-[var(--p-border-radius)] inline-flex items-center justify-center transition-[background,color] duration-150 hover:bg-[var(--p-content-hover-background)] hover:text-[var(--p-text-color)] cursor-pointer"
|
||||||
:aria-label="shell.sidebarCollapsed ? 'Expand sidebar' : 'Collapse sidebar'"
|
aria-label="Collapse sidebar"
|
||||||
type="button"
|
type="button"
|
||||||
@click="handleCollapseClick"
|
@click="handleCollapseClick"
|
||||||
>
|
>
|
||||||
<Icon
|
<Icon
|
||||||
:name="shell.sidebarCollapsed ? 'tabler-chevron-right' : 'tabler-chevron-left'"
|
name="tabler-chevron-left"
|
||||||
:size="16"
|
:size="16"
|
||||||
/>
|
/>
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
|
<!--
|
||||||
|
Collapsed: expand chevron as a small floating chip at the rail's
|
||||||
|
right edge. Solid background + border so the slight overlap with
|
||||||
|
the centered logo reads as a tucked-in affordance rather than a
|
||||||
|
collision. Decoupled from the brand group so toggling does NOT
|
||||||
|
shift the logo's centering.
|
||||||
|
-->
|
||||||
|
<button
|
||||||
|
v-else
|
||||||
|
class="absolute end-0 top-1/2 -translate-y-1/2 w-6 h-6 rounded-full border border-[var(--p-content-border-color)] bg-[var(--p-content-background)] text-[var(--p-text-muted-color)] inline-flex items-center justify-center transition-[background,color] duration-150 hover:bg-[var(--p-content-hover-background)] hover:text-[var(--p-text-color)] cursor-pointer shadow-sm"
|
||||||
|
aria-label="Expand sidebar"
|
||||||
|
type="button"
|
||||||
|
@click="handleCollapseClick"
|
||||||
|
>
|
||||||
|
<Icon
|
||||||
|
name="tabler-chevron-right"
|
||||||
|
:size="12"
|
||||||
|
/>
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
|||||||
@@ -135,19 +135,27 @@ function inviteUser(): void {
|
|||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="relative flex-shrink-0 border-t border-[var(--p-content-border-color)] p-[10px]">
|
<div class="relative flex-shrink-0 border-t border-[var(--p-content-border-color)] p-[10px]">
|
||||||
<!-- Trigger button -->
|
<!--
|
||||||
|
Trigger button.
|
||||||
|
Plan 2.5 P6-followup-styling: rounded-xl (matches the brand square
|
||||||
|
in SidebarHeader), p-2 padding, hover background — replaces the
|
||||||
|
sharper rounded-[var(--p-border-radius)] / px-[10px] py-[8px] state.
|
||||||
|
Collapsed variant centres the avatar only, hiding the name and
|
||||||
|
chevron, so the collapsed rail reads as a vertical mirror: brand
|
||||||
|
square at the top, avatar square at the bottom.
|
||||||
|
-->
|
||||||
<button
|
<button
|
||||||
class="flex w-full items-center gap-[10px] rounded-[var(--p-border-radius)] border border-transparent bg-transparent px-[10px] py-[8px] text-[var(--p-text-color)] transition-colors duration-150 hover:bg-[var(--p-content-hover-background)]"
|
class="trigger flex w-full items-center gap-[10px] rounded-xl border border-transparent bg-transparent p-2 text-[var(--p-text-color)] transition-colors duration-150 hover:bg-[var(--p-content-hover-background)]"
|
||||||
:class="[
|
:class="[
|
||||||
collapsed ? 'justify-center' : '',
|
collapsed ? 'justify-center' : '',
|
||||||
]"
|
]"
|
||||||
aria-haspopup="true"
|
aria-haspopup="true"
|
||||||
@click="toggle"
|
@click="toggle"
|
||||||
>
|
>
|
||||||
<!-- Logo square (gradient background is bespoke: dynamic hex pair cannot be a static Tailwind class — RFC §7.4 justified inline-style) -->
|
<!-- Avatar square — shared treatment with SidebarHeader logo (h-8 w-8 rounded-xl). Gradient background is bespoke per organisation (dynamic hex pair, RFC §7.4 inline-style). -->
|
||||||
<span
|
<span
|
||||||
v-if="current"
|
v-if="current"
|
||||||
class="ws-logo ws-logo-square w-8 h-8 flex-shrink-0 rounded-[var(--p-border-radius)] inline-flex items-center justify-center text-white font-bold text-[12px]"
|
class="ws-logo ws-logo-square w-8 h-8 flex-shrink-0 rounded-xl inline-flex items-center justify-center text-white font-bold text-[12px]"
|
||||||
:style="{ background: `linear-gradient(135deg, ${current.gradient[0]}, ${current.gradient[1]})` }"
|
:style="{ background: `linear-gradient(135deg, ${current.gradient[0]}, ${current.gradient[1]})` }"
|
||||||
>
|
>
|
||||||
{{ current.initials }}
|
{{ current.initials }}
|
||||||
@@ -156,7 +164,7 @@ function inviteUser(): void {
|
|||||||
<!-- Meta: name (hidden in collapsed mode). AD-2.5-W1 / P4: no sub. -->
|
<!-- Meta: name (hidden in collapsed mode). AD-2.5-W1 / P4: no sub. -->
|
||||||
<span
|
<span
|
||||||
v-if="!collapsed && current"
|
v-if="!collapsed && current"
|
||||||
class="flex flex-1 min-w-0 flex-col text-left leading-[1.2]"
|
class="meta flex flex-1 min-w-0 flex-col text-left leading-[1.2]"
|
||||||
>
|
>
|
||||||
<span class="name truncate text-[13.5px] font-semibold text-[var(--p-text-color)]">
|
<span class="name truncate text-[13.5px] font-semibold text-[var(--p-text-color)]">
|
||||||
{{ current.name }}
|
{{ current.name }}
|
||||||
|
|||||||
@@ -193,4 +193,26 @@ describe('SidebarHeader', () => {
|
|||||||
|
|
||||||
expect(btn.attributes('aria-label')).toBe('Collapse sidebar')
|
expect(btn.attributes('aria-label')).toBe('Collapse sidebar')
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// P6-followup-styling: when collapsed, the brand-name (wordmark) is
|
||||||
|
// hidden and the row centres the logo. The expand button is a
|
||||||
|
// separate absolute-positioned sibling — its presence must not push
|
||||||
|
// the logo off-centre. This spec locks the "logo stays centred"
|
||||||
|
// contract by asserting the row's flex alignment + wordmark absence.
|
||||||
|
it('collapsed: hides the Crewli wordmark and centres the row', async () => {
|
||||||
|
const wrapper = mountHeader()
|
||||||
|
const shell = useShellUiStore()
|
||||||
|
|
||||||
|
shell.sidebarCollapsed = true
|
||||||
|
|
||||||
|
await wrapper.vm.$nextTick()
|
||||||
|
|
||||||
|
expect(wrapper.find('.brand-name').exists()).toBe(false)
|
||||||
|
expect(wrapper.text()).not.toContain('Crewli')
|
||||||
|
|
||||||
|
// The header row toggles to justify-center when collapsed.
|
||||||
|
const row = wrapper.find('div')
|
||||||
|
|
||||||
|
expect(row.classes()).toContain('justify-center')
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -49,7 +49,7 @@ const globalStubs = {
|
|||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
function mountSwitcher(opts: { orgs?: Organisation[] } = {}) {
|
function mountSwitcher(opts: { orgs?: Organisation[]; collapsed?: boolean } = {}) {
|
||||||
setActivePinia(createPinia())
|
setActivePinia(createPinia())
|
||||||
|
|
||||||
const auth = useAuthStore()
|
const auth = useAuthStore()
|
||||||
@@ -58,7 +58,7 @@ function mountSwitcher(opts: { orgs?: Organisation[] } = {}) {
|
|||||||
auth.organisations = opts.orgs ?? [orgA, orgB, orgC]
|
auth.organisations = opts.orgs ?? [orgA, orgB, orgC]
|
||||||
|
|
||||||
return mount(WorkspaceSwitcher, {
|
return mount(WorkspaceSwitcher, {
|
||||||
props: { collapsed: false },
|
props: { collapsed: opts.collapsed ?? false },
|
||||||
global: {
|
global: {
|
||||||
stubs: globalStubs,
|
stubs: globalStubs,
|
||||||
},
|
},
|
||||||
@@ -87,6 +87,23 @@ describe('WorkspaceSwitcher', () => {
|
|||||||
expect(wrapper.text()).toContain(orgA.name)
|
expect(wrapper.text()).toContain(orgA.name)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// -------------------------------------------------------------------------
|
||||||
|
// P6-followup-styling — trigger uses rounded-xl + collapsed = avatar only
|
||||||
|
// -------------------------------------------------------------------------
|
||||||
|
|
||||||
|
it('trigger uses rounded-xl (not the sharper PrimeVue var-radius)', () => {
|
||||||
|
const wrapper = mountSwitcher()
|
||||||
|
|
||||||
|
expect(wrapper.find('.trigger').classes()).toContain('rounded-xl')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('collapsed trigger renders avatar only, hides name + chevron', () => {
|
||||||
|
const wrapper = mountSwitcher({ collapsed: true })
|
||||||
|
|
||||||
|
expect(wrapper.find('.ws-logo').exists()).toBe(true)
|
||||||
|
expect(wrapper.find('.meta').exists()).toBe(false)
|
||||||
|
})
|
||||||
|
|
||||||
// -------------------------------------------------------------------------
|
// -------------------------------------------------------------------------
|
||||||
// Fix 5 — dropdown panel structure
|
// Fix 5 — dropdown panel structure
|
||||||
// -------------------------------------------------------------------------
|
// -------------------------------------------------------------------------
|
||||||
|
|||||||
Reference in New Issue
Block a user