diff --git a/apps/app/src/components-v2/layout/WorkspaceSwitcher.vue b/apps/app/src/components-v2/layout/WorkspaceSwitcher.vue index 35d94891..e371917c 100644 --- a/apps/app/src/components-v2/layout/WorkspaceSwitcher.vue +++ b/apps/app/src/components-v2/layout/WorkspaceSwitcher.vue @@ -57,9 +57,24 @@ interface WorkspaceDisplay { id: string initials: string name: string + /** + * Light-grey second line under the name. Plan 2.5 P6-styling-switcher-sub + * re-adds the two-line layout (reverses AD-2.5-W1 option A) after visual + * review against the crewli-starter SoT. The value is a neutral + * PLACEHOLDER — the real org type + metrics (e.g. "Festival · 12 days · + * 14 stages") require the organisations.type enum + a metrics endpoint, + * which stay deferred under WORKSPACE-DROPDOWN-SUB-CONTENT. The org + * object exposes no field that reads well as a subtitle today (only + * id/name/slug/role; role is an access identifier, not a description), + * so a static neutral string is used rather than fabricated metrics. + */ + sub: string gradient: [string, string] } +// TODO: replace with org type + metrics when WORKSPACE-DROPDOWN-SUB-CONTENT backend lands +const SUB_PLACEHOLDER = 'Organisatie' + function buildDisplay(org: Organisation): WorkspaceDisplay { const words = org.name.trim().split(/\s+/) @@ -72,6 +87,7 @@ function buildDisplay(org: Organisation): WorkspaceDisplay { id: org.id, initials, name: org.name, + sub: SUB_PLACEHOLDER, gradient: computeOrgGradient(org.id), } } @@ -159,22 +175,28 @@ function inviteUser(): void { :class="collapsed ? 'h-[56px] flex items-center px-4' : 'px-4 py-2'" > + {{ current.name }} + + {{ current.sub }} + {{ ws.initials }} - -
{{ ws.name }}
+ +
{{ ws.name }}
+
{{ ws.sub }}
diff --git a/apps/app/src/components-v2/layout/__tests__/WorkspaceSwitcher.spec.ts b/apps/app/src/components-v2/layout/__tests__/WorkspaceSwitcher.spec.ts index d72695a8..84c7f549 100644 --- a/apps/app/src/components-v2/layout/__tests__/WorkspaceSwitcher.spec.ts +++ b/apps/app/src/components-v2/layout/__tests__/WorkspaceSwitcher.spec.ts @@ -2,8 +2,10 @@ * WorkspaceSwitcher.spec.ts * * Locks two slices of contract: - * 1. AD-2.5-W1 + AD-2.5-W1 option A: NO sub line on trigger OR on any - * dropdown row. + * 1. Sub line (P6-styling-switcher-sub reverses AD-2.5-W1 option A): + * a placeholder sub line renders under the name in the expanded + * trigger AND on each dropdown row; the collapsed trigger stays + * bare-avatar-only (no sub). * 2. Plan 2.5 P5 Fix 5: dropdown panel structure per crewli-starter — * header (title + manage link), list (one .opt per org, .is-current * + .check-mark on active), footer (two buttons). @@ -71,14 +73,36 @@ describe('WorkspaceSwitcher', () => { }) // ------------------------------------------------------------------------- - // AD-2.5-W1 — no sub on trigger + // P6-styling-switcher-sub — placeholder sub line (reverses option A) // ------------------------------------------------------------------------- - it('does not render a sub line on the trigger (AD-2.5-W1)', () => { - const wrapper = mountSwitcher({ orgs: [orgA] }) + it('renders a placeholder sub line under the name in the expanded trigger', () => { + const wrapper = mountSwitcher({ orgs: [orgA], collapsed: false }) - expect(wrapper.text()).not.toContain(orgA.role) - expect(wrapper.html()).not.toMatch(/workspace-sub|ws-sub|meta-sub/) + const sub = wrapper.find('.meta .sub') + + expect(sub.exists()).toBe(true) + expect(sub.text().length).toBeGreaterThan(0) + }) + + it('the trigger sub is a neutral placeholder, NOT the org role', () => { + const wrapper = mountSwitcher({ orgs: [orgA], collapsed: false }) + + // The placeholder must not leak the access-control role string — + // that was the original AD-2.5-W1 concern; the reversal restores the + // line but with honest placeholder content, not org.role. + expect(wrapper.find('.meta .sub').text()).not.toContain(orgA.role) + }) + + it('does not render a sub line in the collapsed trigger (bare avatar only)', () => { + const wrapper = mountSwitcher({ orgs: [orgA], collapsed: true }) + + // The trigger sub lives inside `.meta`, which only the expanded + // trigger renders. The dropdown's `.opt .sub` rows render inline + // (Popover stubbed) regardless of collapse state, so assert against + // the trigger-scoped `.meta .sub` rather than a bare `.sub`. + expect(wrapper.find('.meta').exists()).toBe(false) + expect(wrapper.find('.meta .sub').exists()).toBe(false) }) it('renders the workspace name on the trigger', () => { @@ -105,15 +129,17 @@ describe('WorkspaceSwitcher', () => { expect(wrapper.find('.trigger').classes()).not.toContain('justify-center') }) - it('collapsed renders a bare avatar button (no .trigger container, just .ws-logo)', () => { + it('collapsed renders a bare avatar button (no .trigger container, no .meta)', () => { const wrapper = mountSwitcher({ collapsed: 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