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:
@@ -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>
|
||||
|
||||
<!--
|
||||
|
||||
@@ -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)
|
||||
})
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user