Files
crewli-old/apps/app/src/components-v2/layout/SidebarHeader.vue
bert.hausmans 8e16651232 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>
2026-05-21 00:15:34 +02:00

134 lines
5.4 KiB
Vue

<script setup lang="ts">
/**
* SidebarHeader — ported from crewli-starter AppSidebar.vue `.brand` block.
*
* Renders the logo mark ("C"), "Crewli" wordmark, "Beta" pill, and the
* sidebar collapse-toggle button. All state is read from / written to
* useShellUiStore — no props needed.
*
* Collapse-button behaviour (mirrors crewli-starter `handleCollapse`):
* - Mobile (<lg, i.e. max-width: 1023px): the drawer is the active sidebar
* → call setMobileOpen(false) to close it.
* - Desktop (>=lg): toggle the persistent sidebar between expanded/collapsed
* → call toggleSidebar().
*
* Plan 2.5 P6-followup-styling: the collapse-toggle is decoupled from the
* brand group so the logo stays centered in the collapsed rail.
* - Expanded: justify-between row — [brand group] [collapse chevron].
* - Collapsed: justify-center row — [logo only], with the expand chevron
* as a small floating circle absolute-positioned at the rail's right
* edge (tucked over the logo's right side with a solid background
* so the visual reads as an intentional chip, not an overlap). The
* aside's `overflow-hidden` (intentional, for the width transition)
* prevents true overhang past the rail edge — the tucked pattern is
* the visual compromise that keeps the affordance discoverable.
*
* The logo square shares its size + radius with WorkspaceSwitcher's
* avatar square (h-8 w-8 rounded-xl) so the collapsed rail reads as
* 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 Icon from '@/components/Icon.vue'
import { useShellUiStore } from '@/stores/useShellUiStore'
const shell = useShellUiStore()
// Use the canonical Tailwind lg breakpoint (1024px) — consistent with AppSidebar's
// v-if="isMobile" guard so both components agree on the desktop/mobile boundary.
const isMobile = useBreakpoints(breakpointsTailwind).smaller('lg')
function handleCollapseClick(): void {
if (isMobile.value) {
// Mobile: the Drawer is the active sidebar — close it
shell.setMobileOpen(false)
}
else {
// Desktop: toggle the persistent sidebar width
shell.toggleSidebar()
}
}
</script>
<template>
<div
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' : 'justify-between px-4'"
>
<!--
Expanded: brand group on the left (logo + wordmark + Beta pill).
Collapsed: only the logo square, centered in the rail.
-->
<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>
<span
v-if="!shell.sidebarCollapsed"
class="brand-name font-bold text-base tracking-[-0.01em] whitespace-nowrap text-[var(--p-text-color)]"
>
Crewli
</span>
<span
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]"
>
Beta
</span>
</div>
<!-- Expanded: inline collapse chevron on the right (justify-between sibling). -->
<button
v-if="!shell.sidebarCollapsed"
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="Collapse sidebar"
type="button"
@click="handleCollapseClick"
>
<Icon
name="tabler-chevron-left"
:size="16"
/>
</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>
</template>
<style scoped>
/*
* The .mark inset box-shadow (inset 0 -2px 0 rgba(0,0,0,0.08)) has no
* Tailwind equivalent — Tailwind's shadow utilities only support outer shadows
* and cannot express inset directional shadows at this opacity. Last resort per
* RFC §7.4.
*/
.mark {
box-shadow: inset 0 -2px 0 rgb(0 0 0 / 8%);
}
</style>