diff --git a/docs/search-chrome-behaviour.md b/docs/search-chrome-behaviour.md
index 10ef2f4ee0..2aef0f38a4 100644
--- a/docs/search-chrome-behaviour.md
+++ b/docs/search-chrome-behaviour.md
@@ -4,19 +4,20 @@ This repo uses one shared search experience across the global shell, dashboard r
## Page ownership model
-| Page state | Composer placement | Reserve owner |
-| ------------------------------------------------------------ | ------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------ |
-| Shared home (`/`, any mode) / standalone mode homes | In-flow hero composer on phones and larger breakpoints | Page content; no fixed phone dock reserve |
-| Tools directory (`/tools`) and legacy alias (`/?mode=tools`) | No shared composer or phone dock; browse and filter through page-local catalogue controls | Idle shell padding only |
-| Therapy Recommend (`/therapy-compass/recommend`) | In-flow clinical-situation composer; no shared composer or phone dock | Idle shell padding only |
-| Submitted/search-result views | Compact bottom dock on phones; in normal page flow on tablets and desktops | Shell/dashboard `--mobile-composer-reserve` on phones; page content on desktop |
-| Answer result view | Overlaid glass header plus answer composer dock | Dashboard `#main-content` top/bottom reserves |
-| Document detail/source routes | `DocumentViewer` floating composer | `DocumentViewer` content padding |
-| Document section navigation | Header row disclosure (phone sheet) + rail index card at `lg` | None — adds no chrome and no reserve |
-| Record page breadcrumb header | Same header row without the disclosure or track; view mode inline from `sm` | None — portals into the phone collapse row, sticky at `sm+` |
-| Calculators (`/calculators`) | In-flow hero composer at home; shared compact dock on `/calculators/search` (browse or submitted) | Page content at home; shell reserve for the catalogue and submitted results |
-| Info/detail pages with no composer | No fixed composer | Idle shell padding only |
-| Guide Centre dialog (`GuideDialog`) | No composer — tour-action dock inside the Sheet footer; Sheet footer band from `sm` | `[data-guide-content]` bottom pad (`guide-tour-dock` reserve owner) |
+| Page state | Composer placement | Reserve owner |
+| ------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------ |
+| Shared home (`/`, any mode) / standalone mode homes | In-flow hero composer on phones and larger breakpoints | Page content; no fixed phone dock reserve |
+| Tools directory (`/tools`) and legacy alias (`/?mode=tools`) | No shared composer or phone dock; browse and filter through page-local catalogue controls | Idle shell padding only |
+| Therapy Recommend (`/therapy-compass/recommend`) | In-flow clinical-situation composer; no shared composer or phone dock | Idle shell padding only |
+| Submitted/search-result views | Compact bottom dock on phones; in normal page flow on tablets and desktops | Shell/dashboard `--mobile-composer-reserve` on phones; page content on desktop |
+| Answer result view | Overlaid glass header plus answer composer dock | Dashboard `#main-content` top/bottom reserves |
+| Document detail/source routes | `DocumentViewer` floating composer | `DocumentViewer` content padding |
+| Document section navigation | Header row disclosure (phone sheet) + rail index card at `lg` | None — adds no chrome and no reserve |
+| Record page breadcrumb header | Same header row without the disclosure or track; view mode inline from `sm` | None — portals into the phone collapse row, sticky at `sm+` |
+| Calculators (`/calculators`) | In-flow hero composer at home; shared compact dock on `/calculators/search` (browse or submitted) | Page content at home; shell reserve for the catalogue and submitted results |
+| Dictionary catalogue (`/dictionary/search`) | Compact bottom dock on phones; in-flow shared composer from `sm` up, under mode nav and above the Filter band | Shell `--mobile-composer-reserve` on phones; page content on desktop |
+| Info/detail pages with no composer | No fixed composer | Idle shell padding only |
+| Guide Centre dialog (`GuideDialog`) | No composer — tour-action dock inside the Sheet footer; Sheet footer band from `sm` | `[data-guide-content]` bottom pad (`guide-tour-dock` reserve owner) |
The Tools row is scoped to the **mounted Tools directory**, not to `resultKind: "tools"`. Factsheets,
Dictionary and Therapy Compass borrow that result kind purely as a benign search kind, and on the
diff --git a/src/components/clinical-dashboard/global-search-shell.tsx b/src/components/clinical-dashboard/global-search-shell.tsx
index b19977bf80..42e268739f 100644
--- a/src/components/clinical-dashboard/global-search-shell.tsx
+++ b/src/components/clinical-dashboard/global-search-shell.tsx
@@ -113,6 +113,7 @@ import { readSearchNavigationContext, type SearchNavigationOptions } from "@/lib
import {
isAlwaysStandaloneShellPath,
isDashboardOwnedModeHomePath,
+ isDictionaryCataloguePath,
isStandaloneModeHomePath,
shouldRenderClinicalDashboard,
shouldRenderDashboardSearch,
@@ -468,6 +469,7 @@ function GlobalStandaloneSearchShellBody({
// searchMode before router.push landed, which made isStandaloneModeHome false
// for one frame (dock reserve + 200ms padding transition = choppy resize).
const isStandaloneModeHome = !hasSubmittedModeSearch && !rendersDashboardSearch && isStandaloneModeHomePath(pathname);
+ const isDictionaryCatalogue = isDictionaryCataloguePath(pathname);
const isDifferentialPresentationWorkflow = pathname.startsWith("/differentials/presentations/");
const shouldShowDesktopSidebar = !hideDesktopSidebar;
const effectiveSidebarCollapsed = isDifferentialPresentationWorkflow ? true : sidebarCollapsed;
@@ -493,7 +495,9 @@ function GlobalStandaloneSearchShellBody({
// This flag controls sm+ padding for standalone mode homes. Tools has no
// shared composer, so it cannot reserve floating-composer space. Phone
// clearance is resolved separately from heroOwnsPhoneComposer below.
- const reservesFloatingComposer = shouldShowSearchComposer && !isStandaloneModeHome;
+ // Dictionary catalogue keeps the usual compact phone dock; sm+ still
+ // portals into the in-page slot under mode nav (`desktopHomeComposerSlotId`).
+ const reservesFloatingComposer = shouldShowSearchComposer && !isStandaloneModeHome && !isDictionaryCatalogue;
// Most standalone mode homes keep the in-flow hero pill at every width. Tools
// deliberately has no shared composer. Document viewer routes own their own
// floating composer, so
@@ -963,13 +967,21 @@ function GlobalStandaloneSearchShellBody({
desktopSearchPlacement={desktopSearchPlacement === "hero" && isStandaloneModeHome ? "hero" : "default"}
showPhoneSuggestionTickerOnHome={isStandaloneModeHome || (pathname === "/" && !hasSubmittedModeSearch)}
searchComposerVisible={shouldShowSearchComposer}
- desktopHomeComposerSlotId={isStandaloneModeHome ? modeHomeDesktopComposerSlotId : undefined}
+ desktopHomeComposerSlotId={
+ isStandaloneModeHome || isDictionaryCatalogue ? modeHomeDesktopComposerSlotId : undefined
+ }
desktopPageComposerSlotId={
- shouldShowSearchComposer && !isStandaloneModeHome ? desktopPageComposerSlotId : undefined
+ shouldShowSearchComposer && !isStandaloneModeHome && !isDictionaryCatalogue
+ ? desktopPageComposerSlotId
+ : undefined
}
// Most standalone homes keep the in-flow hero pill at every width.
// Tools suppresses the shared composer at every breakpoint.
- heroComposerBreakpoint={mobileHomeComposerPlacement === "footer" ? "sm-up" : "all"}
+ // Dictionary catalogue uses the usual compact phone dock; sm+
+ // still portals into the in-page slot under mode nav.
+ heroComposerBreakpoint={
+ mobileHomeComposerPlacement === "footer" || isDictionaryCatalogue ? "sm-up" : "all"
+ }
// Phones: #main-content owns vertical scroll, so hide-on-scroll
// collapses the top bar to hand space back to content.
// Tablet and desktop portal search into normal page flow. The outer
@@ -1046,7 +1058,7 @@ function GlobalStandaloneSearchShellBody({
data-testid="mobile-composer-reserve-pad"
className="max-sm:pt-[var(--phone-overlay-chrome-h)] max-sm:pb-[var(--mobile-composer-reserve)]"
>
- {shouldShowSearchComposer && !isStandaloneModeHome ? (
+ {shouldShowSearchComposer && !isStandaloneModeHome && !isDictionaryCatalogue ? (
-
- {displayQuery}
-
+ {showQuerySubject ? (
+ <>
+
+
+ {displayQuery}
+
+ >
+ ) : (
+
+ )}
{hasUtilities ? (
diff --git a/src/components/dictionary/dictionary-catalogue-pages.tsx b/src/components/dictionary/dictionary-catalogue-pages.tsx
index 97496639bd..6ae8d32c89 100644
--- a/src/components/dictionary/dictionary-catalogue-pages.tsx
+++ b/src/components/dictionary/dictionary-catalogue-pages.tsx
@@ -23,6 +23,7 @@ import {
type ResultFilterOption,
} from "@/components/clinical-dashboard/result-filter-control";
import { SearchResultsHeaderBand } from "@/components/clinical-dashboard/search-results-header-band";
+import { DesktopComposerPortalSlot } from "@/components/desktop-composer-portal-slot";
import { DictionaryResultRow } from "@/components/dictionary/dictionary-result-row";
import { InPageNavHeader } from "@/components/in-page-nav/in-page-nav-header";
import { type PageSection } from "@/components/in-page-nav/page-section-index";
@@ -47,6 +48,7 @@ import {
dictionaryTopics,
type DictionaryEntryKind,
} from "@/lib/dictionary-data";
+import { modeHomeComposerReservePendingValue, modeHomeDesktopComposerSlotId } from "@/lib/mode-home-composer";
const scopeOptions = [
{ value: "definitions", label: "Terms" },
@@ -116,14 +118,12 @@ function catalogueNoun(scope: DictionaryCatalogueScope, count: number) {
* list, and clearing it restores the catalogue. `/dictionary/browse` survives
* only as a redirect for existing links.
*
- * The shared bottom composer is this page's ONLY search input. Do not add a
- * second one — `docs/search-chrome-behaviour.md`'s one-composer-per-page rule is
- * a hard constraint with committed tests behind it.
- *
- * The phone control row is sized to its own labels rather than to the viewport.
- * It keeps the complete “Abbreviations” and “Filter” wordmarks, with tighter
- * phone-only horizontal padding so the idle controls remain one balanced row at
- * common phone widths and wrap, rather than clip, on compact screens.
+ * The Filter band is always on this page (count, optional query, Filter). Compact
+ * Terms / Abbreviations and A–Z sit under that band. From `sm` up, the shared
+ * composer portals into this page under mode nav and above the Filter band.
+ * Phones keep the usual compact bottom dock. Do not add a second search field —
+ * `docs/search-chrome-behaviour.md`'s one-composer-per-page rule is a hard
+ * constraint with committed tests behind it.
*/
export function DictionaryCataloguePage() {
const { searchParams, replace, setOne, toggleMany } = useDictionaryUrl();
@@ -173,11 +173,8 @@ export function DictionaryCataloguePage() {
});
// Clearing the query drops `run` with it, because the shell re-derives the
// composer's value from the URL on every search-string change and a leftover
- // submitted marker would restore the results view the reader just dismissed —
- // and it drops `letter`, because while searching that key is both invisible
- // and inert, so keeping it would hand back a catalogue narrowed to one initial
- // under a control that promises the whole thing. The key list is
- // `dictionaryClearedQueryKeys`, next to the predicate that creates the hazard.
+ // submitted marker would restore the results view the reader just dismissed.
+ // Letter and facets stay: they remain visible under the band / in Filter.
const clearQuery = () =>
replace((next) => {
for (const key of dictionaryClearedQueryKeys) next.delete(key);
@@ -285,48 +282,47 @@ export function DictionaryCataloguePage() {
})),
];
- /* Sized to its own labels, not to the viewport, and joined into one border
- with no gap. The counts sit inline, so the row needs no summary line of its
- own while browsing. */
+ /* Joined two-cell toggle: one border, no gap, sized to its own labels.
+ Each segment keeps the shared 48px tap target while compact text preserves
+ the row's visual density. `aria-label` keeps the accessible name
+ "Terms (2)" rather than concatenated "Terms2". Focus uses an inset outline
+ because the joined track clips an outset ring. */
const scopeToggle = (
);
- /* The phone's whole alphabet in one 63px control. 27 chips cost a band and a
- horizontal scroll; the rail below is the same control at a width that can
- afford it. */
+ /* Phone A–Z: compact to match the toggle. The desktop rail below is the same
+ control at a width that can afford 27 chips. */
const letterChip = (