fix(layout): WorkspaceSwitcher crewli-starter parity — no avatar jump, is-open bg, no resting border
Diagnosed against the crewli-starter SoT
(crewli-starter/src/components/layout/WorkspaceSwitcher.vue +
main.css .ws-switcher rules). Three issues Bert flagged in manual
smoke, one root cause:
ROOT CAUSE — the prior version swapped between TWO separate <button>
elements on collapse (a bare collapsed button vs a padded expanded
trigger) with different box models. Their vertical padding differed,
so the avatar's distance from the rail bottom changed by ~6px and
visibly jumped on collapse. crewli-starter instead renders ONE
`.trigger` button in both states and only hides `.meta` + `.chev`
(+ recentres) on collapse.
Fixes (all by adopting the single-trigger structure):
- Avatar no longer jumps: one `.trigger` button always; `py-2`
vertical padding constant across states; only horizontal
padding/justify changes on collapse. Avatar's vertical box is now
identical collapsed vs expanded.
- No resting border: trigger is `border-transparent` at rest and on
hover (hover only adds the grey bg), matching crewli-starter
`.trigger:hover { background }`. The wrapper has only the `border-t`
separator, no box border.
- is-open persistence: new `isOpen` ref synced from the PrimeVue
Popover `@show`/`@hide` events. While the dropdown is open the
trigger keeps the grey bg AND shows a visible border, matching
crewli-starter `.ws-switcher.is-open .trigger { background;
border-color }`. Persists until the popover closes (covers
programmatic hide via selectOrg + outside-click dismissal).
- Hover padding: trigger `px-[10px] py-2` inside a `p-[10px]` wrapper
reproduces crewli-starter's generous hover-card inset
(`.ws-switcher { padding:10px } .trigger { padding:8px 10px }`).
Collapsed alignment preserved: wrapper p-[10px] + trigger
`justify-center px-0` centres the 32px avatar at 16px from the rail's
left edge — still aligned with the SidebarHeader brand logo (px-4).
At rest the collapsed trigger is transparent, so it still reads as a
bare square mirroring the header logo; hover/open add the card.
Specs: replaced the now-obsolete "bare avatar button / no .trigger"
+ "collapsed wrapper px-4" locks with crewli-starter-structure specs:
single .trigger in both states, collapsed centres the lone avatar +
hides .meta/.chev, py-2 constant (the no-jump lock), and is-open
keeps bg+border. Sub-line + dropdown specs unchanged.
Suite delta: 569 → 571 (+2). vue-tsc clean. Scoped ESLint clean
(0 errors). Desktop only.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -120,6 +120,16 @@ const allOrgs = computed<WorkspaceDisplay[]>(() => {
|
||||
|
||||
const popoverRef = ref<InstanceType<typeof Popover> | null>(null)
|
||||
|
||||
/**
|
||||
* Tracks the popover open state so the trigger keeps the grey "active"
|
||||
* background + visible border while the workspace dropdown is open
|
||||
* (crewli-starter `.ws-switcher.is-open .trigger`). Synced from the
|
||||
* PrimeVue Popover's @show/@hide events rather than mirroring the manual
|
||||
* toggle, so programmatic hides (selectOrg) and outside-click dismissal
|
||||
* both keep it accurate.
|
||||
*/
|
||||
const isOpen = ref(false)
|
||||
|
||||
function toggle(event: MouseEvent): void {
|
||||
popoverRef.value?.toggle(event)
|
||||
}
|
||||
@@ -151,65 +161,46 @@ function inviteUser(): void {
|
||||
|
||||
<template>
|
||||
<!--
|
||||
Wrapper horizontal padding is `px-4` (16px) in BOTH states — same
|
||||
rhythm as the SidebarHeader brand row above (`h-[56px] px-4`).
|
||||
Crewli-starter-faithful structure (P6-styling-switcher-sub follow-up).
|
||||
ONE `.trigger` button renders in BOTH states — collapsing only hides
|
||||
`.meta` + `.chev` (v-if) and recentres the lone avatar. This is the
|
||||
key fix for the vertical "avatar jump": the prior version swapped
|
||||
between two separate <button> elements (a bare collapsed button vs a
|
||||
padded expanded trigger) with different box models, so the avatar's
|
||||
distance from the rail bottom differed by ~6px and visibly jumped on
|
||||
collapse. With a single trigger and constant vertical padding
|
||||
(`py-2` always), the avatar stays put.
|
||||
|
||||
Collapsed: wrapper is `h-[56px] flex items-center px-4`. The bare
|
||||
32px rounded-lg avatar sits at the px-4 offset, centred in the
|
||||
64px rail by the equation (rail = square + 2 × px-4). True bottom
|
||||
mirror of the brand square (see Option B in the brief).
|
||||
Wrapper: `border-t` separator + `p-[10px]` ALWAYS (constant vertical
|
||||
padding → no jump; matches crewli-starter `.ws-switcher { padding:
|
||||
10px }`). No border around the wrapper itself.
|
||||
|
||||
Expanded: wrapper is `px-4 py-2`. The full padded trigger (`w-full`
|
||||
rounded card) is now inset 16px from both rail edges, giving
|
||||
symmetric breathing room that matches the header's horizontal
|
||||
rhythm — prior version used `p-2` (8px) and the card felt stuck
|
||||
to the rail edges. Trade-off: with px-4 wrapper + p-2 internal
|
||||
trigger padding the avatar sits ~24px from the rail edge in
|
||||
expanded, ~8px deeper than the bare header logo at 16px — accepted
|
||||
as deliberate (the expanded trigger reads as a distinct button
|
||||
card; the bare-square mirror with the logo lives in the collapsed
|
||||
state, which is unchanged).
|
||||
Centring equation (collapsed): wrapper p-[10px] gives the trigger a
|
||||
44px content width inside the 64px rail; the trigger's `justify-center
|
||||
px-0` centres the 32px avatar → 16px from the rail's left edge,
|
||||
aligned with the SidebarHeader brand logo above (also 16px).
|
||||
-->
|
||||
<div
|
||||
class="relative flex-shrink-0 border-t border-[var(--p-content-border-color)]"
|
||||
:class="collapsed ? 'h-[56px] flex items-center px-4' : 'px-4 py-2'"
|
||||
>
|
||||
<div class="ws-switcher relative flex-shrink-0 border-t border-[var(--p-content-border-color)] p-[10px]">
|
||||
<!--
|
||||
Collapsed: a bare click target (transparent, p-0, no box) wrapping
|
||||
the EXACT SAME avatar span markup the expanded trigger uses, so the
|
||||
32px square is byte-identical across states (same classes, same
|
||||
gradient, same `.ws-logo-square` inset-shadow). P6-styling-switcher-sub
|
||||
Fix A: previously the collapsed avatar styling lived directly on the
|
||||
<button>, which let user-agent button rendering diverge subtly from
|
||||
the expanded <span> avatar. Extracting the shared span removes that
|
||||
drift. The wrapping button only adds the click target + focus ring.
|
||||
Trigger — single button for both states (crewli-starter `.trigger`).
|
||||
Resting: transparent border + transparent bg. Hover: grey bg, no
|
||||
border. Open (is-open): grey bg KEPT + visible border, persisting
|
||||
until the popover closes — crewli-starter
|
||||
`.ws-switcher.is-open .trigger { background; border-color }`.
|
||||
`py-2` is constant; only horizontal padding / justify changes on
|
||||
collapse, so the avatar never shifts vertically.
|
||||
-->
|
||||
<button
|
||||
v-if="collapsed && current"
|
||||
class="flex-shrink-0 border-0 bg-transparent p-0 cursor-pointer rounded-lg inline-flex focus-visible:outline focus-visible:outline-2 focus-visible:outline-[var(--p-primary-color)] focus-visible:outline-offset-2"
|
||||
:aria-label="`Workspace: ${current.name}`"
|
||||
aria-haspopup="true"
|
||||
@click="toggle"
|
||||
>
|
||||
<span
|
||||
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]"
|
||||
:style="{ background: `linear-gradient(135deg, ${current.gradient[0]}, ${current.gradient[1]})` }"
|
||||
>
|
||||
{{ current.initials }}
|
||||
</span>
|
||||
</button>
|
||||
|
||||
<!--
|
||||
Expanded trigger: full-width padded button with rounded hover bg,
|
||||
avatar on the left, then name and chevron. With the wrapper at
|
||||
px-4 and the trigger at internal p-2, the avatar sits ~24px from
|
||||
the rail edge (intentional — see wrapper comment above). The
|
||||
bare-square / logo mirror lives in the collapsed state.
|
||||
-->
|
||||
<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 bg-transparent py-2 text-[var(--p-text-color)] transition-[background,border-color] duration-150 focus-visible:outline focus-visible:outline-2 focus-visible:outline-[var(--p-primary-color)] focus-visible:outline-offset-2"
|
||||
:class="[
|
||||
collapsed ? 'justify-center px-0' : 'px-[10px]',
|
||||
isOpen
|
||||
? 'is-open border-[var(--p-content-border-color)] bg-[var(--p-content-hover-background)]'
|
||||
: 'border-transparent hover:bg-[var(--p-content-hover-background)]',
|
||||
]"
|
||||
aria-haspopup="true"
|
||||
:aria-expanded="isOpen"
|
||||
:aria-label="collapsed && current ? `Workspace: ${current.name}` : undefined"
|
||||
@click="toggle"
|
||||
>
|
||||
<!-- Avatar square — shared treatment with SidebarHeader logo (h-8 w-8 rounded-lg). Gradient background is bespoke per organisation (dynamic hex pair, RFC §7.4 inline-style). -->
|
||||
@@ -221,9 +212,9 @@ function inviteUser(): void {
|
||||
{{ current.initials }}
|
||||
</span>
|
||||
|
||||
<!-- Meta: name + placeholder sub line (P6-styling-switcher-sub). -->
|
||||
<!-- Meta: name + placeholder sub line (hidden when collapsed). -->
|
||||
<span
|
||||
v-if="current"
|
||||
v-if="!collapsed && 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)]">
|
||||
@@ -234,15 +225,21 @@ function inviteUser(): void {
|
||||
</span>
|
||||
</span>
|
||||
|
||||
<!-- Chevron: hidden when collapsed. -->
|
||||
<Icon
|
||||
v-if="!collapsed"
|
||||
name="tabler-chevron-down"
|
||||
:size="14"
|
||||
class="flex-shrink-0 text-[var(--p-text-muted-color)]"
|
||||
class="chev flex-shrink-0 text-[var(--p-text-muted-color)]"
|
||||
/>
|
||||
</button>
|
||||
|
||||
<!-- PrimeVue Popover — replaces crewli-starter's manual document.mousedown click-outside -->
|
||||
<Popover ref="popoverRef">
|
||||
<Popover
|
||||
ref="popoverRef"
|
||||
@show="isOpen = true"
|
||||
@hide="isOpen = false"
|
||||
>
|
||||
<!-- Header -->
|
||||
<div class="popover-head flex items-center justify-between border-b border-[var(--p-content-border-color)] px-[16px] py-[14px]">
|
||||
<span class="title text-[15px] font-bold tracking-[-0.01em]">Workspaces</span>
|
||||
|
||||
Reference in New Issue
Block a user