Switchers
Searchable dropdown modals for switching context (partner, branch, organisation). Full-screen on mobile,
positioned below the trigger on desktop with items-start pt-[15vh].
Partner switcher
A modal with a search input and selectable list. The active item shows a checkmark. Each option displays an avatar and name. Used in the sidebar to switch between partners.
Switch partner
No partners found
<div x-data="{ open: false, search: '', selected: 'Koala Legal',
names: ['koala legal', 'greenfield solicitors'] }"
x-on:open-partner-switcher.window="search = ''; open = true;
$nextTick(() => $refs.partnerSearch?.focus())"
x-on:keydown.escape.window="if (open) { open = false; }">
<!-- Backdrop -->
<div x-show="open" x-cloak
class="fixed inset-0 z-40 bg-gray-900/50
dark:bg-gray-900/75"></div>
<!-- Panel -->
<div x-show="open" x-cloak
x-on:click.self="open = false"
class="fixed inset-0 z-50 flex items-center justify-center
sm:items-start sm:pt-[15vh] sm:p-4">
<div class="bg-white dark:bg-gray-800
sm:border border-gray-200 dark:border-gray-700
rounded-none sm:rounded-lg
w-full h-full sm:h-auto sm:max-w-md
overflow-y-auto">
<!-- Header -->
<div class="flex items-center justify-between p-4
border-b border-gray-200 dark:border-gray-700">
<h3 class="text-lg font-semibold text-gray-900
dark:text-white">Switch partner</h3>
<button type="button" x-on:click="open = false"
class="text-gray-400 hover:text-gray-500
dark:hover:text-gray-300">
<koala-icon name="X" />
</button>
</div>
<!-- Search -->
<div class="p-4 border-b border-gray-200
dark:border-gray-700">
<div class="relative">
<div class="absolute inset-y-0 start-0
flex items-center ps-3
pointer-events-none">
<koala-icon name="Search" size="Small"
class="text-gray-400" />
</div>
<input type="text"
x-ref="partnerSearch"
x-model="search"
placeholder="Search partners..."
class="bg-white dark:bg-gray-700
border border-gray-200
dark:border-gray-600
text-gray-900 dark:text-white
rounded-lg block w-full ps-9
px-3 py-2.5" />
</div>
</div>
<!-- Options -->
<div class="max-h-72 space-y-1 overflow-y-auto p-2">
@foreach (var partner in partners)
{
var isCurrent = partner.Id == currentBranch.PartnerId;
<form method="post" action="/partner/switch"
x-target.push="main"
data-name="@partner.Name.ToLowerInvariant()"
x-show="!search || $el.dataset.name
.includes(search.toLowerCase())">
<input type="hidden" name="partnerId"
value="@partner.Id" />
<button type="submit"
class="flex w-full items-center gap-3
px-3 py-2.5 text-left rounded-lg
@(isCurrent
? "bg-gray-100 ..."
: "text-gray-700 ...")">
<koala-partner-avatar size="Medium"
partner-id="@partner.Id"
name="@partner.Name"
has-avatar="@(...)" />
<span class="truncate flex-1">
@partner.Name
</span>
@if (isCurrent)
{
<!-- Checkmark SVG -->
}
</button>
</form>
}
<!-- Empty state -->
<div x-show="search && !names.some(
n => n.includes(search.toLowerCase()))"
x-cloak
class="px-4 py-6 text-center text-gray-500
dark:text-gray-400">
No partners found
</div>
</div>
</div>
</div>
</div>
Sidebar trigger
The switcher is opened from a trigger in the sidebar header. The trigger shows the current selection with an avatar and a chevron icon. It dispatches a custom window event to open the modal.
<a href="/partner/switch"
x-data
x-on:click="if (!$event.ctrlKey && !$event.metaKey) {
$event.preventDefault();
$dispatch('open-partner-switcher');
}"
class="flex w-full items-center gap-3 rounded-lg
px-2 py-1.5 hover:bg-white/8">
<koala-partner-avatar size="Small"
partner-id="@currentBranch.PartnerId"
name="@currentBranch.PartnerName"
has-avatar="@(...)" />
<div class="min-w-0 flex-1 text-left">
<div class="truncate text-sm text-secondary-300">
@currentBranch.PartnerName
</div>
</div>
<svg class="h-3.5 w-3.5 shrink-0 text-secondary-300">
<path d="M8 9l4-4 4 4m0 6l-4 4-4-4"></path>
</svg>
</a>
Key rules
- Full-screen on mobile, positioned with
sm:items-start sm:pt-[15vh]on desktop - Search input auto-focuses when the modal opens via
$nextTick(() => $refs.search?.focus()) - Options are filtered client-side using
x-showwithincludes() - The active option shows a checkmark icon (
text-fg-brand) - Each option is a
<form>that POSTs to a switch endpoint - Escape key closes the modal via
x-on:keydown.escape.window - Empty state shown when search yields no results
- Trigger uses
$dispatch()to open the modal, with Ctrl/Cmd+Click fallback to full page