fix(gui-v2): cleanup(b) — keep mobile workspace btn a free #end sibling (Plan-2 flex parity) + lock data-tb=search

This commit is contained in:
2026-05-18 13:36:12 +02:00
parent f03a3f16c6
commit 237afc89e6
2 changed files with 78 additions and 35 deletions

View File

@@ -324,42 +324,47 @@ const userMenuItems = computed<MenuItem[]>(() => [
.ws-mobile-btn: display none (>= 1024px). On mobile: display flex.
38x38, rounded, color #fff, font-bold 12px, gradient bg via inline :style
(dynamic hex pair RFC §7.4). Box-shadow via scoped CSS.
Plan-3 fix: free sibling in #end flex row NOT nested inside data-tb="search".
-->
<div data-tb="search">
<button
v-if="authStore.currentOrganisation"
type="button"
class="ws-mobile-btn-shadow lg:hidden inline-flex h-[38px] w-[38px] flex-shrink-0 items-center justify-center rounded-[var(--p-border-radius)] border-0 text-[12px] font-bold text-white"
:style="{
background: `linear-gradient(135deg, ${mobileOrgGradient[0]}, ${mobileOrgGradient[1]})`,
}"
:aria-label="`Workspace: ${authStore.currentOrganisation.name}`"
>
{{ mobileOrgInitials }}
</button>
<button
v-if="authStore.currentOrganisation"
type="button"
class="ws-mobile-btn-shadow lg:hidden inline-flex h-[38px] w-[38px] flex-shrink-0 items-center justify-center rounded-[var(--p-border-radius)] border-0 text-[12px] font-bold text-white"
:style="{
background: `linear-gradient(135deg, ${mobileOrgGradient[0]}, ${mobileOrgGradient[1]})`,
}"
:aria-label="`Workspace: ${authStore.currentOrganisation.name}`"
>
{{ mobileOrgInitials }}
</button>
<!--
Search static chrome, no backend.
.search: position relative, w-[320px], max-w-full
.search input: h-[38px], px-[12px] ps-[36px], bg-surface-alt, border rounded
.search .kbd: absolute right-2, text-[11px], px-[6px] py-[2px], border rounded
Hidden on smallest viewports per crewli-starter (<768px: width 0 / display:none)
-->
<div class="relative hidden sm:block w-[240px] lg:w-[320px] max-w-full">
<Icon
name="tabler-search"
:size="16"
class="pointer-events-none absolute left-[11px] top-1/2 -translate-y-1/2 text-[var(--p-text-muted-color)]"
/>
<InputText
class="h-[38px] w-full rounded-[var(--p-border-radius)] border border-transparent bg-[var(--p-content-hover-background)] ps-[36px] pe-[52px] text-[var(--p-text-color)] placeholder:text-[var(--p-text-muted-color)] focus:border-[var(--p-primary-color)] focus:bg-[var(--p-content-background)] focus:shadow-[0_0_0_3px_var(--p-primary-50)] focus:outline-none transition-[border-color,background,box-shadow] duration-150"
placeholder="Search artists, crew, events..."
:pt="{ root: { 'data-search-input': '' } }"
/>
<span class="pointer-events-none absolute right-2 top-1/2 -translate-y-1/2 rounded-[var(--p-border-radius-sm,4px)] border border-[var(--p-content-border-color)] bg-[var(--p-content-background)] px-[6px] py-[2px] font-mono text-[11px] text-[var(--p-text-muted-color)]">
K
</span>
</div>
<!--
Search static chrome, no backend.
data-tb="search" carries the original layout classes of the search block
so flex participation is identical to Plan-2 (direct #end flex child with
relative + hidden sm:block + width). No inner wrapper div needed.
.search: position relative, w-[240px] sm / w-[320px] lg, max-w-full
.search input: h-[38px], px-[12px] ps-[36px], bg-surface-alt, border rounded
.search .kbd: absolute right-2, text-[11px], px-[6px] py-[2px], border rounded
Hidden on smallest viewports per crewli-starter (<768px: width 0 / display:none)
-->
<div
data-tb="search"
class="relative hidden sm:block w-[240px] lg:w-[320px] max-w-full"
>
<Icon
name="tabler-search"
:size="16"
class="pointer-events-none absolute left-[11px] top-1/2 -translate-y-1/2 text-[var(--p-text-muted-color)]"
/>
<InputText
class="h-[38px] w-full rounded-[var(--p-border-radius)] border border-transparent bg-[var(--p-content-hover-background)] ps-[36px] pe-[52px] text-[var(--p-text-color)] placeholder:text-[var(--p-text-muted-color)] focus:border-[var(--p-primary-color)] focus:bg-[var(--p-content-background)] focus:shadow-[0_0_0_3px_var(--p-primary-50)] focus:outline-none transition-[border-color,background,box-shadow] duration-150"
placeholder="Search artists, crew, events..."
:pt="{ root: { 'data-search-input': '' } }"
/>
<span class="pointer-events-none absolute right-2 top-1/2 -translate-y-1/2 rounded-[var(--p-border-radius-sm,4px)] border border-[var(--p-content-border-color)] bg-[var(--p-content-background)] px-[6px] py-[2px] font-mono text-[11px] text-[var(--p-text-muted-color)]">
K
</span>
</div>
<!--

View File

@@ -309,11 +309,49 @@ describe('AppTopbar', () => {
expect(w.findComponent(MenubarStub).exists()).toBe(true)
})
it('breadcrumb lives in Menubar #start, user cluster in #end', () => {
it('breadcrumb lives in Menubar #start, user cluster in #end; search wrapper exists and mobile-ws-btn is NOT its descendant', async () => {
// Mount first so the component's pinia plugin is active, then get the
// store instance from that same pinia context.
const w = mountTopbar()
const bar = w.findComponent(MenubarStub)
expect(bar.find('[data-tb="breadcrumb"]').exists()).toBe(true)
expect(bar.find('[data-tb="user"]').exists()).toBe(true)
// AD-3 regression lock: data-tb="search" must exist as a direct #end sibling
expect(bar.find('[data-tb="search"]').exists()).toBe(true)
// Seed authStore with an organisation so authStore.currentOrganisation resolves
// (it is a computed derived from organisations[0]). setUser() is the public API
// that populates the organisations array.
// Must call useAuthStore() AFTER mountTopbar() so we use the same pinia instance.
const authStore = useAuthStore()
authStore.setUser({
id: 'u1',
first_name: 'Test',
last_name: 'User',
full_name: 'Test User',
date_of_birth: null,
email: 'test@example.com',
phone: null,
timezone: 'UTC',
locale: 'en',
avatar: null,
organisations: [{ id: 'org-1', name: 'Test Org', slug: 'test-org', role: 'org_admin' }],
app_roles: [],
permissions: [],
})
await w.vm.$nextTick()
const wsBtnSelector = 'button[aria-label^="Workspace:"]'
// The mobile workspace button must exist in the bar (currentOrganisation is set).
expect(bar.find(wsBtnSelector).exists()).toBe(true)
// The mobile workspace button must NOT be a descendant of data-tb="search".
// In the corrected structure it is a free sibling in the #end flex row.
expect(bar.find('[data-tb="search"]').find(wsBtnSelector).exists()).toBe(false)
})
})