style(layout): align WorkspaceSwitcher avatar to the header logo recipe
Manual smoke after the logo-anchor fix (63300e5f) showed the footer
WorkspaceSwitcher used different spacing and a boxed avatar vs the
header logo. Applied the identical alignment recipe so the workspace
avatar mirrors the brand logo in both states (Option B from the
brief — bare square when collapsed, padded trigger only when
expanded).
Changes (WorkspaceSwitcher.vue only):
- Wrapper toggles `h-[56px] flex items-center px-4` when collapsed
(mirrors the SidebarHeader brand row) and stays `p-2` when
expanded (room for the padded trigger).
- Collapsed: a BARE rounded-lg avatar button at the same 16px left
offset as the header logo. No `.trigger` container, no rounded
hover-bg box wider than the avatar — the button IS the visible
square (`.ws-logo .ws-logo-square w-8 h-8 rounded-lg`). True
top/bottom mirror of the brand square.
- Expanded: unchanged padded `.trigger` button with avatar + name +
chevron + hover bg. Avatar's left offset stays at 16px from the
rail (wrapper p-2 + trigger p-2) so the expanded avatar also
lines up vertically with the header logo.
Same alignment equation as the header recipe:
rail_collapsed (64px) = square (32px) + 2 × px-4 (2 × 16px)
In both states the avatar's left edge sits at x=16px from the
rail's left — identical to the brand logo above. Vertical line
down the left side now reads as a single column of squares.
Desktop only. Mobile drawer chrome stays as MOBILE-SHELL-PARITY.
Tests adapted:
- `expanded trigger uses rounded-lg` (was tested in both states; the
collapsed render no longer has a `.trigger` container).
- `expanded trigger has no justify-center` (split from the
prior two-state assertion).
- New: `collapsed renders a bare avatar button (no .trigger
container, just .ws-logo)` — locks the bare-square contract.
- New: `collapsed wrapper uses px-4` — locks the
centring-equation invariant (rail=square+2×px-4) against
accidental wrapper-padding regressions.
Suite delta: 563 → 564 (+1 net: +2 new collapsed-shape asserts,
−1 redundant two-state assert).
vue-tsc clean. Scoped ESLint clean (0 errors, pre-existing
warnings only). Manual smoke pending Bert — draw a vertical line
down the rail's left edge and verify the brand square and the
workspace square left edges sit on it in both states; in collapsed
mode verify the avatar is a bare square (no boxed button), same
visual treatment as the bare logo above.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -134,23 +134,52 @@ function inviteUser(): void {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="relative flex-shrink-0 border-t border-[var(--p-content-border-color)] p-2">
|
||||
<!--
|
||||
Wrapper height + horizontal padding mirror the SidebarHeader brand
|
||||
row (`h-[56px] px-4`) in collapsed mode so the workspace square
|
||||
appears as a true bottom mirror of the brand square: same 32px
|
||||
rounded-lg square at the same 16px left offset from the rail,
|
||||
centred in the 64px collapsed rail by the same equation
|
||||
(rail = square + 2 × px-4). In expanded mode the wrapper relaxes
|
||||
to `p-2` so the full trigger (avatar + name + chevron + rounded
|
||||
hover bg) fits naturally.
|
||||
|
||||
Plan 2.5 P6-styling-switcher-align — Option B: collapsed renders
|
||||
a BARE clickable avatar (no trigger box, no hover-bg container)
|
||||
so it visually mirrors the bare brand logo above. Expanded keeps
|
||||
the padded trigger box for the name/chevron/hover affordance.
|
||||
-->
|
||||
<div
|
||||
class="relative flex-shrink-0 border-t border-[var(--p-content-border-color)]"
|
||||
:class="collapsed ? 'h-[56px] flex items-center px-4' : 'p-2'"
|
||||
>
|
||||
<!--
|
||||
Trigger button.
|
||||
Plan 2.5 P6-styling-fix: avatar stays anchored on collapse — the
|
||||
`justify-center` switch is gone (it caused the same flex-recentre
|
||||
slide as the SidebarHeader logo did). The trigger is ALWAYS
|
||||
left-aligned; the name + chevron sit to the RIGHT of the avatar
|
||||
and disappear via v-if on collapse without shifting the avatar.
|
||||
Combined with the WorkspaceSwitcher wrapper's `p-2` (was
|
||||
`p-[10px]`) the avatar's left offset is 8px (wrapper) + 8px
|
||||
(trigger) = 16px — identical to SidebarHeader's `px-4` logo
|
||||
offset, so the brand square and the workspace avatar are
|
||||
vertically aligned in the collapsed rail.
|
||||
Radius is `rounded-lg` (8px) — matches the brand mark; crisper
|
||||
than the prior `rounded-xl` per design review.
|
||||
Collapsed: bare 32px rounded-lg button positioned at the same
|
||||
`px-4` left offset as the brand logo. No trigger container, no
|
||||
hover bg wider than the avatar — true mirror of the logo above.
|
||||
The `.ws-logo .ws-logo-square` classes carry the scoped
|
||||
inset-shadow (RFC §7.4) so the visual treatment matches the
|
||||
avatar in the expanded trigger.
|
||||
-->
|
||||
<button
|
||||
v-if="collapsed && current"
|
||||
class="ws-logo ws-logo-square w-8 h-8 flex-shrink-0 rounded-lg inline-flex items-center justify-center text-white font-bold text-[12px] border-0 cursor-pointer focus-visible:outline focus-visible:outline-2 focus-visible:outline-[var(--p-primary-color)] focus-visible:outline-offset-2"
|
||||
:style="{ background: `linear-gradient(135deg, ${current.gradient[0]}, ${current.gradient[1]})` }"
|
||||
:aria-label="`Workspace: ${current.name}`"
|
||||
aria-haspopup="true"
|
||||
@click="toggle"
|
||||
>
|
||||
{{ current.initials }}
|
||||
</button>
|
||||
|
||||
<!--
|
||||
Expanded trigger: full-width padded button with rounded hover bg,
|
||||
avatar on the left (at wrapper p-2 + trigger p-2 = 16px from rail,
|
||||
matching the collapsed avatar's offset and the header logo above),
|
||||
then name and chevron.
|
||||
-->
|
||||
<button
|
||||
v-else-if="!collapsed"
|
||||
class="trigger flex w-full items-center gap-[10px] rounded-lg border border-transparent bg-transparent p-2 text-[var(--p-text-color)] transition-colors duration-150 hover:bg-[var(--p-content-hover-background)]"
|
||||
aria-haspopup="true"
|
||||
@click="toggle"
|
||||
@@ -164,9 +193,9 @@ function inviteUser(): void {
|
||||
{{ current.initials }}
|
||||
</span>
|
||||
|
||||
<!-- Meta: name (hidden in collapsed mode). AD-2.5-W1 / P4: no sub. -->
|
||||
<!-- Meta: name (AD-2.5-W1 / P4: no sub). -->
|
||||
<span
|
||||
v-if="!collapsed && current"
|
||||
v-if="current"
|
||||
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)]">
|
||||
@@ -175,7 +204,6 @@ function inviteUser(): void {
|
||||
</span>
|
||||
|
||||
<Icon
|
||||
v-if="!collapsed"
|
||||
name="tabler-chevron-down"
|
||||
:size="14"
|
||||
class="flex-shrink-0 text-[var(--p-text-muted-color)]"
|
||||
|
||||
@@ -88,30 +88,44 @@ describe('WorkspaceSwitcher', () => {
|
||||
})
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// P6-followup-styling — trigger uses rounded-xl + collapsed = avatar only
|
||||
// P6-styling-switcher-align — expanded trigger uses rounded-lg + has no
|
||||
// justify-center; collapsed renders a BARE avatar button (no .trigger
|
||||
// container) mirroring the SidebarHeader brand square's offset.
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
it('trigger uses rounded-lg (matches the brand square in SidebarHeader)', () => {
|
||||
const wrapper = mountSwitcher()
|
||||
it('expanded trigger uses rounded-lg (matches the brand square in SidebarHeader)', () => {
|
||||
const wrapper = mountSwitcher({ collapsed: false })
|
||||
|
||||
expect(wrapper.find('.trigger').classes()).toContain('rounded-lg')
|
||||
})
|
||||
|
||||
it('trigger has no justify-center class in either state (avatar stays anchored on collapse)', () => {
|
||||
const expanded = mountSwitcher({ collapsed: false })
|
||||
it('expanded trigger has no justify-center (avatar stays anchored on the left)', () => {
|
||||
const wrapper = mountSwitcher({ collapsed: false })
|
||||
|
||||
expect(expanded.find('.trigger').classes()).not.toContain('justify-center')
|
||||
|
||||
const collapsed = mountSwitcher({ collapsed: true })
|
||||
|
||||
expect(collapsed.find('.trigger').classes()).not.toContain('justify-center')
|
||||
expect(wrapper.find('.trigger').classes()).not.toContain('justify-center')
|
||||
})
|
||||
|
||||
it('collapsed trigger renders avatar only, hides name + chevron', () => {
|
||||
it('collapsed renders a bare avatar button (no .trigger container, just .ws-logo)', () => {
|
||||
const wrapper = mountSwitcher({ collapsed: true })
|
||||
|
||||
expect(wrapper.find('.ws-logo').exists()).toBe(true)
|
||||
// The padded trigger container with name/chevron is gone — no .trigger,
|
||||
// no .meta. The avatar IS the button (.ws-logo + .ws-logo-square on the
|
||||
// bare <button>), so it visually mirrors the bare SidebarHeader logo.
|
||||
expect(wrapper.find('.trigger').exists()).toBe(false)
|
||||
expect(wrapper.find('.meta').exists()).toBe(false)
|
||||
expect(wrapper.find('button.ws-logo').exists()).toBe(true)
|
||||
})
|
||||
|
||||
it('collapsed wrapper uses px-4 (matches the SidebarHeader brand row offset)', () => {
|
||||
const wrapper = mountSwitcher({ collapsed: true })
|
||||
|
||||
// The wrapper toggles to `h-[56px] flex items-center px-4` when
|
||||
// collapsed so the avatar's left edge lands at 16px from the rail
|
||||
// — identical to the header logo offset.
|
||||
const root = wrapper.find('button.ws-logo').element.parentElement
|
||||
|
||||
expect(root?.classList.contains('px-4')).toBe(true)
|
||||
expect(root?.classList.contains('h-[56px]')).toBe(true)
|
||||
})
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
Reference in New Issue
Block a user