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>
|
</script>
|
||||||
|
|
||||||
<template>
|
<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.
|
Collapsed: bare 32px rounded-lg button positioned at the same
|
||||||
Plan 2.5 P6-styling-fix: avatar stays anchored on collapse — the
|
`px-4` left offset as the brand logo. No trigger container, no
|
||||||
`justify-center` switch is gone (it caused the same flex-recentre
|
hover bg wider than the avatar — true mirror of the logo above.
|
||||||
slide as the SidebarHeader logo did). The trigger is ALWAYS
|
The `.ws-logo .ws-logo-square` classes carry the scoped
|
||||||
left-aligned; the name + chevron sit to the RIGHT of the avatar
|
inset-shadow (RFC §7.4) so the visual treatment matches the
|
||||||
and disappear via v-if on collapse without shifting the avatar.
|
avatar in the expanded trigger.
|
||||||
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.
|
|
||||||
-->
|
-->
|
||||||
<button
|
<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)]"
|
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"
|
aria-haspopup="true"
|
||||||
@click="toggle"
|
@click="toggle"
|
||||||
@@ -164,9 +193,9 @@ function inviteUser(): void {
|
|||||||
{{ current.initials }}
|
{{ current.initials }}
|
||||||
</span>
|
</span>
|
||||||
|
|
||||||
<!-- Meta: name (hidden in collapsed mode). AD-2.5-W1 / P4: no sub. -->
|
<!-- Meta: name (AD-2.5-W1 / P4: no sub). -->
|
||||||
<span
|
<span
|
||||||
v-if="!collapsed && current"
|
v-if="current"
|
||||||
class="meta 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)]">
|
||||||
@@ -175,7 +204,6 @@ function inviteUser(): void {
|
|||||||
</span>
|
</span>
|
||||||
|
|
||||||
<Icon
|
<Icon
|
||||||
v-if="!collapsed"
|
|
||||||
name="tabler-chevron-down"
|
name="tabler-chevron-down"
|
||||||
:size="14"
|
:size="14"
|
||||||
class="flex-shrink-0 text-[var(--p-text-muted-color)]"
|
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)', () => {
|
it('expanded trigger uses rounded-lg (matches the brand square in SidebarHeader)', () => {
|
||||||
const wrapper = mountSwitcher()
|
const wrapper = mountSwitcher({ collapsed: false })
|
||||||
|
|
||||||
expect(wrapper.find('.trigger').classes()).toContain('rounded-lg')
|
expect(wrapper.find('.trigger').classes()).toContain('rounded-lg')
|
||||||
})
|
})
|
||||||
|
|
||||||
it('trigger has no justify-center class in either state (avatar stays anchored on collapse)', () => {
|
it('expanded trigger has no justify-center (avatar stays anchored on the left)', () => {
|
||||||
const expanded = mountSwitcher({ collapsed: false })
|
const wrapper = mountSwitcher({ collapsed: false })
|
||||||
|
|
||||||
expect(expanded.find('.trigger').classes()).not.toContain('justify-center')
|
expect(wrapper.find('.trigger').classes()).not.toContain('justify-center')
|
||||||
|
|
||||||
const collapsed = mountSwitcher({ collapsed: true })
|
|
||||||
|
|
||||||
expect(collapsed.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 })
|
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('.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