feat(layout): Plan 2.5 P5 — shell parity fixes 1–5 + useBreadcrumb retire
Per RFC-WS-PRIMEVUE-PLAN-2-5 §5.1–§5.5 plus the AD-2.5-W1 option-A supersession (no sub on dropdown items either, accepted divergence). Atomic changes: - AppTopbar: brand block (gradient "C" mark + Crewli wordmark) removed per Fix 1; the #start slot now renders <AppBreadcrumb /> per Fix 2. Legacy meta-based useBreadcrumb consumption (breadcrumbModel computed, vue-router useRouter import, command-based PrimeVue Breadcrumb model) is gone; AppBreadcrumb owns the registry-driven path. Dead topbar-mark-shadow scoped CSS rule deleted. - AppBreadcrumb: import updated to the renamed useBreadcrumb. - AppSidebar: docstring updated to make the Fix 3 vertical order (Header → Nav → Switcher, switcher bottom-anchored) explicit. No template change needed: SidebarNav's root <nav class="flex-1"> already fills available column space, naturally pushing WorkspaceSwitcher to the bottom (two flex-1 siblings would split the column 50/50 and compress the nav — a separate spacer element is structurally wrong). - WorkspaceSwitcher: dropdown panel restructured per crewli-starter reference. Semantic class markers (.popover-head/.title/.link/.list/ .opt/.is-current/.ws-logo/.name/.check-mark/.foot) added alongside Tailwind utilities so specs assert structure with stable selectors. Footer buttons wired to placeholder createWorkspace / inviteUser handlers (console.warn + TODO) until the flows ship. Manage link stays a non-navigating label (no v2-workspaces-manage route yet). No sub line on any dropdown row (AD-2.5-W1 option A). Atomic legacy useBreadcrumb retirement (planned since P1): - Legacy route-meta-driven useBreadcrumb + toBreadcrumbItems + BreadcrumbRouteRecord types deleted entirely (only AppTopbar consumed it, and that consumption is gone after Fix 2). - useNavBreadcrumb → useBreadcrumb (single SoT for breadcrumb chain). - NavBreadcrumbItem → BreadcrumbItem. - AppBreadcrumb.vue import updated to the new name. - SidebarNav.vue docstring reference scrubbed to the new name. - useBreadcrumb.spec.ts: 10 legacy toBreadcrumbItems specs removed; 4 walkNavTree specs retained. AppTopbar.spec.ts: - vue-router mock simplified (route.matched no longer relevant). - AppBreadcrumb stubbed in #start; legacy command-vs-route assertion removed; new spec verifies AppBreadcrumb is rendered. WorkspaceSwitcher.spec.ts: 5 new dropdown specs (header / row count / current-row checkmark / footer buttons / no-sub on rows). Suite delta: 557 → 552 (−5 net: −10 legacy toBreadcrumbItems specs, +5 Fix 5 dropdown specs, −1 obsolete AppTopbar breadcrumb-model spec, +1 new AppTopbar AppBreadcrumb-presence spec). vue-tsc clean. Scoped ESLint clean (0 errors). All 3 re-grep checks returned 0 hits (useNavBreadcrumb/NavBreadcrumbItem, topbar brand selectors, standalone "sub" identifier in WorkspaceSwitcher — only documentation comments referencing the no-sub state remain, which describe absence by design). Manual smoke skipped (Auto Mode); coverage from the post-edit specs includes AppBreadcrumb-in-#start, dropdown structure, and trigger no-sub. Recommend Bert run `pnpm --filter crewli-app dev` and verify the 6 checks listed in the prompt before merging. Known divergence from crewli-starter (accepted): - Dropdown rows are ~16px shorter than crewli-starter (no sub line). Tracked as WORKSPACE-DROPDOWN-SUB-CONTENT for a future RFC with the required backend scope (organisations.type enum + metrics). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -8,10 +8,24 @@
|
||||
* Popover: PrimeVue <Popover> replaces the manual document.mousedown
|
||||
* click-outside listener from crewli-starter. Toggle via popoverRef.toggle($event).
|
||||
*
|
||||
* Plan 2.5 P5 Fix 5 (RFC §5.5): dropdown panel restructured per
|
||||
* crewli-starter reference, with AD-2.5-W1 option A applied — dropdown
|
||||
* rows render WITHOUT a sub line (organisation type / metrics deferred
|
||||
* to a future RFC with backend scope — tracked as
|
||||
* WORKSPACE-DROPDOWN-SUB-CONTENT). Known visual divergence: rows are
|
||||
* ~16px shorter than crewli-starter; accepted.
|
||||
*
|
||||
* Semantic class names (.popover-head, .title, .link, .list, .opt,
|
||||
* .is-current, .ws-logo, .name, .check-mark, .foot) are kept alongside
|
||||
* the Tailwind utility classes as structural markers — they carry no
|
||||
* scoped CSS rules (only the .ws-logo / .ws-logo-square-lg box-shadow
|
||||
* exceptions below) and exist so specs can assert structure with
|
||||
* stable selectors.
|
||||
*
|
||||
* Icons: Crewli Icon.vue convention — name="tabler-x" :size="N".
|
||||
*
|
||||
* Styling: crewli-starter CSS selectors translated to Tailwind utilities inline.
|
||||
* One <style scoped> block covers the two exceptions documented below.
|
||||
* One <style scoped> block covers the two box-shadow exceptions documented below.
|
||||
*/
|
||||
|
||||
import Popover from 'primevue/popover'
|
||||
@@ -103,12 +117,25 @@ function selectOrg(ws: WorkspaceDisplay): void {
|
||||
authStore.setActiveOrganisation(ws.id)
|
||||
popoverRef.value?.hide()
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Footer button placeholders — neither flow is wired in Plan 2.5 scope.
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
// TODO: wire to /v2/workspaces/new route or create-workspace dialog when ready
|
||||
function createWorkspace(): void {
|
||||
console.warn('WorkspaceSwitcher: createWorkspace not yet implemented')
|
||||
}
|
||||
|
||||
// TODO: wire to invite-user dialog when ready
|
||||
function inviteUser(): void {
|
||||
console.warn('WorkspaceSwitcher: inviteUser not yet implemented')
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="relative flex-shrink-0 border-t border-[var(--p-content-border-color)] p-[10px]">
|
||||
<!-- Trigger button -->
|
||||
<!-- .ws-switcher .trigger: flex, items-center, gap, w-full, px/py, rounded, border, bg-transparent, color, transition -->
|
||||
<button
|
||||
class="flex w-full items-center gap-[10px] rounded-[var(--p-border-radius)] border border-transparent bg-transparent px-[10px] py-[8px] text-[var(--p-text-color)] transition-colors duration-150 hover:bg-[var(--p-content-hover-background)]"
|
||||
:class="[
|
||||
@@ -120,25 +147,22 @@ function selectOrg(ws: WorkspaceDisplay): void {
|
||||
<!-- Logo square (gradient background is bespoke: dynamic hex pair cannot be a static Tailwind class — RFC §7.4 justified inline-style) -->
|
||||
<span
|
||||
v-if="current"
|
||||
class="ws-logo-square w-8 h-8 flex-shrink-0 rounded-[var(--p-border-radius)] inline-flex items-center justify-center text-white font-bold text-[12px]"
|
||||
class="ws-logo ws-logo-square w-8 h-8 flex-shrink-0 rounded-[var(--p-border-radius)] 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>
|
||||
|
||||
<!-- Meta: name (hidden in collapsed mode) -->
|
||||
<!-- .ws-switcher .meta: flex-1, min-w-0, flex-col, line-height, text-left -->
|
||||
<!-- Meta: name (hidden in collapsed mode). AD-2.5-W1 / P4: no sub. -->
|
||||
<span
|
||||
v-if="!collapsed && current"
|
||||
class="flex flex-1 min-w-0 flex-col text-left leading-[1.2]"
|
||||
>
|
||||
<!-- .ws-switcher .meta .name: text-[13.5px], font-semibold, truncate -->
|
||||
<span class="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)]">
|
||||
{{ current.name }}
|
||||
</span>
|
||||
</span>
|
||||
|
||||
<!-- Chevron (.ws-switcher .chev: color fg-subtle, flex-shrink-0) -->
|
||||
<Icon
|
||||
v-if="!collapsed"
|
||||
name="tabler-chevron-down"
|
||||
@@ -149,47 +173,53 @@ function selectOrg(ws: WorkspaceDisplay): void {
|
||||
|
||||
<!-- PrimeVue Popover — replaces crewli-starter's manual document.mousedown click-outside -->
|
||||
<Popover ref="popoverRef">
|
||||
<!-- popover-head: px-[16px] py-[14px], border-bottom, flex, items-center, justify-between -->
|
||||
<div class="flex items-center justify-between border-b border-[var(--p-content-border-color)] px-[16px] py-[14px]">
|
||||
<!-- .popover-head .title -->
|
||||
<span class="text-[15px] font-bold tracking-[-0.01em]">Workspaces</span>
|
||||
<!-- .popover-head .link — TODO TECH-WS-GUI-REDESIGN: no manage-workspaces route yet -->
|
||||
<span class="cursor-pointer text-[13px] font-medium text-[var(--p-primary-color)]">Manage</span>
|
||||
<!-- 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>
|
||||
<!--
|
||||
TODO TECH-WS-GUI-REDESIGN: no v2-workspaces-manage route yet —
|
||||
rendered as a non-navigating label per the routeName-undefined
|
||||
convention (registry-style). Becomes a RouterLink when the
|
||||
manage-workspaces page lands.
|
||||
-->
|
||||
<span class="link cursor-pointer text-[13px] font-medium text-[var(--p-primary-color)]">Manage</span>
|
||||
</div>
|
||||
|
||||
<!-- .pop-ws .list: p-[6px] -->
|
||||
<div class="min-w-[280px] p-[6px]">
|
||||
<!-- .pop-ws .opt: grid 3-col, gap, p, rounded, cursor-pointer -->
|
||||
<!-- List of organisations -->
|
||||
<div class="list min-w-[280px] p-[6px]">
|
||||
<!-- TODO TECH-WS-GUI-REDESIGN: full listbox/menu ARIA (role, roving tabindex, arrow-key nav) — tracked -->
|
||||
<button
|
||||
v-for="ws in allOrgs"
|
||||
:key="ws.id"
|
||||
type="button"
|
||||
class="grid w-full grid-cols-[36px_1fr_auto] cursor-pointer items-center gap-[12px] rounded-[var(--p-border-radius)] border-0 bg-transparent p-[10px] text-start hover:bg-[var(--p-content-hover-background)]"
|
||||
class="opt grid w-full grid-cols-[36px_1fr_auto] cursor-pointer items-center gap-[12px] rounded-[var(--p-border-radius)] border-0 bg-transparent p-[10px] text-start hover:bg-[var(--p-content-hover-background)]"
|
||||
:class="[
|
||||
ws.id === current?.id ? 'bg-[var(--p-primary-50)]' : '',
|
||||
ws.id === current?.id ? 'is-current bg-[var(--p-primary-50)]' : '',
|
||||
]"
|
||||
:aria-current="current?.id === ws.id ? 'true' : undefined"
|
||||
@click="selectOrg(ws)"
|
||||
>
|
||||
<!-- Org logo — larger variant (36px) with dynamic gradient (same inline-style justification as trigger) -->
|
||||
<!-- Org logo (large variant) — dynamic gradient via inline :style -->
|
||||
<span
|
||||
class="ws-logo-square-lg w-9 h-9 flex-shrink-0 rounded-[var(--p-border-radius)] inline-flex items-center justify-center text-white font-bold text-[13px]"
|
||||
class="ws-logo lg ws-logo-square-lg w-9 h-9 flex-shrink-0 rounded-[var(--p-border-radius)] inline-flex items-center justify-center text-white font-bold text-[13px]"
|
||||
:style="{ background: `linear-gradient(135deg, ${ws.gradient[0]}, ${ws.gradient[1]})` }"
|
||||
>{{ ws.initials }}</span>
|
||||
|
||||
<!-- Name -->
|
||||
<!--
|
||||
Name only. AD-2.5-W1 option A: no .sub line on dropdown items
|
||||
until WORKSPACE-DROPDOWN-SUB-CONTENT RFC lands with backend
|
||||
(organisations.type enum + metrics endpoint).
|
||||
-->
|
||||
<span>
|
||||
<!-- .pop-ws .opt .name -->
|
||||
<div class="text-[14px] font-semibold text-[var(--p-text-color)]">{{ ws.name }}</div>
|
||||
<div class="name text-[14px] font-semibold text-[var(--p-text-color)]">{{ ws.name }}</div>
|
||||
</span>
|
||||
|
||||
<!-- Check mark for active org (.pop-ws .opt .check-mark) -->
|
||||
<!-- Check mark for active org -->
|
||||
<Icon
|
||||
v-if="ws.id === current?.id"
|
||||
name="tabler-check"
|
||||
:size="18"
|
||||
class="text-[var(--p-primary-color)]"
|
||||
class="check-mark text-[var(--p-primary-color)]"
|
||||
/>
|
||||
<!-- Spacer when not current (keeps grid alignment) -->
|
||||
<span
|
||||
@@ -199,18 +229,24 @@ function selectOrg(ws: WorkspaceDisplay): void {
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Footer (.pop-ws .foot: p-[8px], border-top, flex, gap-[4px]) -->
|
||||
<div class="flex gap-[4px] border-t border-[var(--p-content-border-color)] p-[8px]">
|
||||
<!-- TODO TECH-WS-GUI-REDESIGN: create-workspace route not yet defined -->
|
||||
<button class="inline-flex flex-1 h-[36px] items-center justify-center gap-[6px] rounded-[var(--p-border-radius)] border-0 bg-transparent text-[13px] font-medium text-[var(--p-text-color)] hover:bg-[var(--p-content-hover-background)]">
|
||||
<!-- Footer -->
|
||||
<div class="foot flex gap-[4px] border-t border-[var(--p-content-border-color)] p-[8px]">
|
||||
<button
|
||||
type="button"
|
||||
class="inline-flex flex-1 h-[36px] items-center justify-center gap-[6px] rounded-[var(--p-border-radius)] border-0 bg-transparent text-[13px] font-medium text-[var(--p-text-color)] hover:bg-[var(--p-content-hover-background)]"
|
||||
@click="createWorkspace"
|
||||
>
|
||||
<Icon
|
||||
name="tabler-plus"
|
||||
:size="14"
|
||||
/>
|
||||
New workspace
|
||||
</button>
|
||||
<!-- TODO TECH-WS-GUI-REDESIGN: invite route not yet defined -->
|
||||
<button class="inline-flex flex-1 h-[36px] items-center justify-center gap-[6px] rounded-[var(--p-border-radius)] border-0 bg-transparent text-[13px] font-medium text-[var(--p-text-color)] hover:bg-[var(--p-content-hover-background)]">
|
||||
<button
|
||||
type="button"
|
||||
class="inline-flex flex-1 h-[36px] items-center justify-center gap-[6px] rounded-[var(--p-border-radius)] border-0 bg-transparent text-[13px] font-medium text-[var(--p-text-color)] hover:bg-[var(--p-content-hover-background)]"
|
||||
@click="inviteUser"
|
||||
>
|
||||
<Icon
|
||||
name="tabler-user-plus"
|
||||
:size="14"
|
||||
@@ -224,7 +260,7 @@ function selectOrg(ws: WorkspaceDisplay): void {
|
||||
|
||||
<style scoped>
|
||||
/**
|
||||
* FIX 2: ws-logo-square — width/height moved to Tailwind (w-8 h-8 on the element).
|
||||
* ws-logo-square — width/height moved to Tailwind (w-8 h-8 on the element).
|
||||
* Tailwind has no inset directional box-shadow utility at this granularity →
|
||||
* scoped CSS last resort per RFC §7.4.
|
||||
*/
|
||||
@@ -233,7 +269,7 @@ function selectOrg(ws: WorkspaceDisplay): void {
|
||||
}
|
||||
|
||||
/**
|
||||
* FIX 2: ws-logo-square-lg — width/height moved to Tailwind (w-9 h-9 on the element).
|
||||
* ws-logo-square-lg — width/height moved to Tailwind (w-9 h-9 on the element).
|
||||
* Same box-shadow exception as above (inset shadow has no Tailwind equivalent).
|
||||
* Background gradient set via inline :style on the element (dynamic hex pair).
|
||||
*/
|
||||
|
||||
Reference in New Issue
Block a user