Mega search
The shared workspace mega-search chrome — the top-bar search box, page scrim, and results bubble, wired to one shared keyboard-nav script so the Portal and this site can't drift.
#ws-form
and #ws-results ids, so a second instance would collide with the search box
already mounted in this page's own top bar. That top-bar search is the live demo: press
/ right now to try it.
Canonical
<koala-mega-search action="/search" placeholder="Search quotes, transactions, clients, branches…">
<input type="hidden" name="handler" value="Workspace" />
<input type="hidden" name="area" value="conveyancing" />
</koala-mega-search>
Mounted once, in the app layout's top bar — never on an individual page. The component renders an
Alpine-AJAX GET form; each app supplies its own search endpoint via
action and its own results partial, which morphs into the results
bubble. Hidden inputs passed as child content ride along on every request, so each app scopes its own
query — the Portal scopes by handler and area; this site's top bar passes none and simply GETs
/component-search.
Anatomy
The helper renders the whole shell; the .ws-* chrome classes live in
the shared tokens.css and the behaviour in the shared
workspace-search.js Alpine object (workspaceSearch).
Apps never write any of this themselves:
- Trigger input (
.ws-box,#ws-form) — the visible search box: leading search icon, the input itself, and a / keyboard hint. Focusing it opens the search and fires an immediate empty-query request; typing re-queries with a 200ms debounce. Focus styling sits on the box, matching a standard Koala input. - Scrim (
.ws-scrim) — dims the page behind open results on desktop, like a modal backdrop. Belowmdthe box collapses to an icon button and opening expands a full-screen overlay instead (with its own back button). - Results panel (
.ws-bubble) — the floating dialog under the box, containing the#ws-resultsmorph target the app's results partial swaps into (x-merge="morph", so the highlight doesn't flicker between keystrokes).
Keyboard behaviour (shared, app-agnostic):
/ opens the search from
anywhere on the page (ignored while typing in a field, so "/" stays typeable);
↑↓
move the row highlight; Enter
activates the highlighted row; Esc
clears the query first, then closes; clicking outside closes. There is no ⌘K binding in the shared
script — this site's mobile-only ⌘K palette is separate chrome. Programmatic open is a
workspace-search:open window event, optionally carrying a
detail.scope that pre-sets the app's #ws-type hidden field.
Results contract — the app's results partial must
render the shape the keyboard nav expects: the #ws-results morph
target containing a [data-ws-list] scroller, a
[data-ws-highlight] overlay, and [data-row]
rows. Row styling stays app-local because the data differs; everything around the rows is shared.
<div id="ws-results">
<div data-ws-list class="(app-local scroller)">
<div data-ws-highlight class="(app-local highlight overlay)"></div>
<a data-row href="/quotes/…">First result row</a>
<a data-row href="/quotes/…">Second result row</a>
</div>
</div>
Props
4 attributes| Attribute | Values | Notes |
|---|---|---|
| action | URL | Required. The form action — the app's search endpoint. The form GETs it via Alpine-AJAX on focus and on every (debounced) keystroke. |
| placeholder | string | Defaults to Search…. Input placeholder text. |
| results-id | string | Defaults to ws-results. The morph-target id the form swaps into; the app's results partial must render the same id. |
| aria-label | string | Defaults to Workspace search. Accessible label on the results dialog. |
Child content is rendered inside the form as hidden fields (handler, area, type, …) so each app can scope its own query — see the Canonical example.
Do & don't
<form id="ws-form">…