From bcaff007afb99e64e28741d742d6c418a0b71ba2 Mon Sep 17 00:00:00 2001 From: BigSimmo <87357024+BigSimmo@users.noreply.github.com> Date: Fri, 17 Jul 2026 21:35:16 +0800 Subject: [PATCH 1/4] fix: suppress phone search command panel Gate the desktop command dropdown on both width and fine-pointer capability, with unit and touch-phone coverage. --- .../universal-search-command-surface.tsx | 40 +++++++++++++++---- src/lib/search-command-surface.ts | 13 ++++++ tests/search-command-surface.test.ts | 10 +++++ tests/ui-tools.spec.ts | 19 ++++----- tests/ui-universal-search.spec.ts | 27 +++++++++---- 5 files changed, 83 insertions(+), 26 deletions(-) diff --git a/src/components/clinical-dashboard/universal-search-command-surface.tsx b/src/components/clinical-dashboard/universal-search-command-surface.tsx index b52b97a04..fc925f4a3 100644 --- a/src/components/clinical-dashboard/universal-search-command-surface.tsx +++ b/src/components/clinical-dashboard/universal-search-command-surface.tsx @@ -21,10 +21,12 @@ import { cn } from "@/components/ui-primitives"; import { appModeDefinition, type AppModeId } from "@/lib/app-modes"; import { appModeIcons } from "@/lib/app-mode-icons"; import { + commandDropdownDisplayMediaQuery, differentialRedFlagTerms, filteredSuggestions, isFormCodeQuery, searchCommandSurfaceConfig, + type CommandSurfacePlacement, } from "@/lib/search-command-surface"; import type { UniversalSearchDomain } from "@/lib/universal-search"; import { universalSearchModeForDomain } from "@/lib/universal-search-mode-context"; @@ -146,8 +148,6 @@ function OptionShell({ active, children, hint }: { active: boolean; children: Re ); } -export type CommandSurfacePlacement = "bottom-dock" | "inline"; - function SmartRotatingHint({ examples, modeLabel }: { examples: string[]; modeLabel: string }) { const [activeExampleIndex, setActiveExampleIndex] = useState(0); const activeExample = examples[activeExampleIndex % examples.length]; @@ -220,7 +220,7 @@ function CommandDropdown({ // template, so without it the section headings inherit text-center. "universal-command-dropdown absolute left-0 right-0 z-30 overflow-hidden rounded-2xl border border-[color:var(--border-strong)] bg-[color:var(--surface)] text-left shadow-[var(--shadow-elevated)]", opensUpward ? "bottom-[calc(100%+0.5rem)] top-auto" : "top-[calc(100%+0.5rem)]", - "block max-sm:rounded-xl", + placement === "bottom-dock" ? "hidden sm:block" : "hidden lg:block", )} role="presentation" > @@ -380,11 +380,29 @@ export function UniversalSearchCommandSurface({ const [activeIndex, setActiveIndex] = useState(-1); const trimmedQuery = query.trim(); const mode = appModeDefinition(modeId); + // The dropdown is a fine-pointer desktop enhancement. Width-only checks let + // wide, zoomed, or desktop-mode phones open it over the page. + const dropdownMediaQuery = commandDropdownDisplayMediaQuery(placement); + const [dropdownDisplayable, setDropdownDisplayable] = useState(false); + useEffect(() => { + if (typeof window.matchMedia !== "function") return; + const mediaQuery = window.matchMedia(dropdownMediaQuery); + const sync = () => { + setDropdownDisplayable(mediaQuery.matches); + if (!mediaQuery.matches) { + onDropdownOpenChange(false); + setActiveIndex(-1); + } + }; + sync(); + mediaQuery.addEventListener("change", sync); + return () => mediaQuery.removeEventListener("change", sync); + }, [dropdownMediaQuery, onDropdownOpenChange]); // A true "everything" view: the active mode's own domain is included (no excludeDomain) so // the palette surfaces every entity type, ordered by the server's intent-aware domainOrder. const universal = useUniversalSearch({ query: trimmedQuery, - enabled: dropdownOpen && Boolean(config), + enabled: dropdownOpen && dropdownDisplayable && Boolean(config), contextMode: modeId, }); const savedRegistryFavourites = useSavedRegistryFavourites(); @@ -850,6 +868,14 @@ export function UniversalSearchCommandSurface({ const activeItemId = activeIndex >= 0 && activeIndex < flatItems.length ? flatItems[activeIndex].id : null; function handleComposerKeyDown(event: ReactKeyboardEvent) { + if (!dropdownDisplayable) { + if (event.key === "Escape") { + onDropdownOpenChange(false); + setActiveIndex(-1); + } + onInputKeyDown?.(event); + return; + } if (event.key === "ArrowDown") { event.preventDefault(); onDropdownOpenChange(true); @@ -946,7 +972,7 @@ export function UniversalSearchCommandSurface({ } }} onFocusCapture={() => { - onDropdownOpenChange(true); + if (dropdownDisplayable) onDropdownOpenChange(true); }} onBlurCapture={(event) => { if (!event.currentTarget.contains(event.relatedTarget as Node | null)) { @@ -956,7 +982,7 @@ export function UniversalSearchCommandSurface({ }} > {children} - {dropdownOpen ? ( + {dropdownOpen && dropdownDisplayable ? ( { onQueryChange(example); - onDropdownOpenChange(true); + if (dropdownDisplayable) onDropdownOpenChange(true); onFocusSearchInput?.(); }} /> diff --git a/src/lib/search-command-surface.ts b/src/lib/search-command-surface.ts index f387fa44c..62fe52df0 100644 --- a/src/lib/search-command-surface.ts +++ b/src/lib/search-command-surface.ts @@ -19,6 +19,19 @@ export type SearchCommandSurfaceConfig = { crossModes: AppModeId[]; }; +export type CommandSurfacePlacement = "bottom-dock" | "inline"; + +/** + * The command panel is a desktop enhancement. Width alone is not enough to + * identify that environment: phones can report a wide viewport in landscape, + * display-zoom, or desktop-site modes. Requiring a hover-capable fine pointer + * keeps the panel closed on touch-first phones while preserving it for desktop. + */ +export function commandDropdownDisplayMediaQuery(placement: CommandSurfacePlacement) { + const minimumWidth = placement === "bottom-dock" ? "640px" : "1024px"; + return `(min-width: ${minimumWidth}) and (hover: hover) and (pointer: fine)`; +} + const searchCommandSurfaceByMode: Partial> = { documents: { examples: ["clozapine ANC thresholds", "lithium monitoring table", "QT prolongation quote"], diff --git a/tests/search-command-surface.test.ts b/tests/search-command-surface.test.ts index 9615d0589..402b65e30 100644 --- a/tests/search-command-surface.test.ts +++ b/tests/search-command-surface.test.ts @@ -1,6 +1,7 @@ import { describe, expect, it } from "vitest"; import { + commandDropdownDisplayMediaQuery, differentialRedFlagTerms, favouriteMatchesCommandScopes, filteredSuggestions, @@ -28,6 +29,15 @@ function serviceRecord(overrides: Partial = {}): ServiceRecord { } describe("search command surface", () => { + it("requires desktop pointer capabilities before displaying the command dropdown", () => { + expect(commandDropdownDisplayMediaQuery("bottom-dock")).toBe( + "(min-width: 640px) and (hover: hover) and (pointer: fine)", + ); + expect(commandDropdownDisplayMediaQuery("inline")).toBe( + "(min-width: 1024px) and (hover: hover) and (pointer: fine)", + ); + }); + it("returns mode-specific command surface config", () => { const documents = searchCommandSurfaceConfig("documents"); expect(documents?.examples.length).toBeGreaterThan(0); diff --git a/tests/ui-tools.spec.ts b/tests/ui-tools.spec.ts index 956e766e6..4760c1dba 100644 --- a/tests/ui-tools.spec.ts +++ b/tests/ui-tools.spec.ts @@ -693,23 +693,20 @@ test.describe("Clinical KB tools launcher", () => { }); } - test("phone bottom-dock search opens the bounded command results sheet", async ({ page }) => { + test("phone bottom-dock search keeps the command results sheet hidden", async ({ page }) => { await page.setViewportSize({ width: 390, height: 820 }); await gotoLauncher(page, "/services?q=13YARN&focus=1&run=1"); await expect(page.getByRole("button", { name: "Mode Services" })).toBeVisible(); const input = visibleGlobalSearchInput(page).first(); await expect(input).toBeVisible(); - // Phone searches use the same accessible listbox as larger viewports, bounded - // above the bottom dock so results stay reachable without horizontal overflow. + // Phones keep the full search results in the page instead of opening a + // command sheet over the small viewport. await input.click(); await input.press("ArrowDown"); - await expect(page.locator(".universal-command-dropdown:visible")).toHaveCount(1); - await expect(page.getByRole("listbox", { name: "Services search suggestions" })).toBeVisible(); - await expectNoPageHorizontalOverflow(page); - - await page.evaluate(() => window.dispatchEvent(new Event("scroll"))); await expect(page.locator(".universal-command-dropdown:visible")).toHaveCount(0); + await expect(page.getByRole("listbox", { name: "Services search suggestions" })).toHaveCount(0); + await expectNoPageHorizontalOverflow(page); }); test("phone mode homes keep the shared search in the hero, not the bottom dock", async ({ page }) => { @@ -728,11 +725,11 @@ test.describe("Clinical KB tools launcher", () => { expect(geometry.top).toBeGreaterThan(0); expect(geometry.bottom).toBeLessThan(geometry.viewportHeight - 40); - // Hero composers expose the same bounded universal results sheet on phones. + // Phone hero composers must not cover the page with the universal sheet. await heroInput.click(); await heroInput.press("ArrowDown"); - await expect(page.locator(".universal-command-dropdown:visible")).toHaveCount(1); - await expect(page.getByRole("listbox")).toBeVisible(); + await expect(page.locator(".universal-command-dropdown:visible")).toHaveCount(0); + await expect(page.getByRole("listbox")).toHaveCount(0); await expectNoPageHorizontalOverflow(page); } }); diff --git a/tests/ui-universal-search.spec.ts b/tests/ui-universal-search.spec.ts index 821fa4176..068a39661 100644 --- a/tests/ui-universal-search.spec.ts +++ b/tests/ui-universal-search.spec.ts @@ -190,14 +190,25 @@ test.describe("universal search typeahead", () => { await expect(page.getByText("Saved").first()).toBeVisible(); }); - test("shows cross-mode typeahead on a phone viewport", async ({ page }) => { - await page.setViewportSize({ width: 390, height: 844 }); - await mockUniversalSearch(page); - const input = await openComposer(page); - await input.fill("acamprosate"); - - await expect(page.getByText(/Current mode · Documents · 1/)).toBeVisible(); - await expect(page.getByText("Also in Medication · Medications · 1")).toBeVisible(); + test("keeps cross-mode typeahead hidden on a landscape touch phone", async ({ browser, baseURL }) => { + const context = await browser.newContext({ + ...(baseURL ? { baseURL } : {}), + hasTouch: true, + viewport: { width: 844, height: 390 }, + }); + const page = await context.newPage(); + + try { + await mockUniversalSearch(page); + const input = await openComposer(page); + await input.fill("acamprosate"); + + await expect(page.locator(".universal-command-dropdown:visible")).toHaveCount(0); + await expect(page.getByText(/Current mode · Documents · 1/)).toHaveCount(0); + await expect(page.getByText("Also in Medication · Medications · 1")).toHaveCount(0); + } finally { + await context.close(); + } }); test("keeps compact cross-mode matches visible after submission", async ({ page }) => { From 1c03d148f09ee82b9bf2a439cf9082efbb15fb0d Mon Sep 17 00:00:00 2001 From: BigSimmo <87357024+BigSimmo@users.noreply.github.com> Date: Fri, 17 Jul 2026 21:42:26 +0800 Subject: [PATCH 2/4] docs: record phone search recovery review --- docs/branch-review-ledger.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/branch-review-ledger.md b/docs/branch-review-ledger.md index fff042348..abc9b2cd6 100644 --- a/docs/branch-review-ledger.md +++ b/docs/branch-review-ledger.md @@ -574,3 +574,4 @@ Use this ledger to prevent repeated branch and PR reviews when the reviewed HEAD | 2026-07-17 | PR #713 / codex/chat-workflow-ideas-0916 | b52112df6aa36311d7420189064acd79dcf2c3f5 + reviewed follow-up diff | workflow toolkit review follow-up | Fixed all 14 actionable Codex and CodeRabbit threads: cross-platform path fixtures, installation-managed preflight guidance, complete Supabase-backed API database scoping, per-command approval boundaries, plugin-ignore narrowing, isolated CI-scope proof, remote-Git command guarding, repository-skill verification classification, `TypeError` diagnosis, strict CLI option values, machine-parseable JSON evidence output, and preservation of baseline database/clinical approval gates in the RAG lab. No unresolved actionable finding remains in the reviewed scope. | `npm run verify:cheap` passed with 273 files and 2,599 tests; focused toolkit Vitest 20/20; CI-scope self-test; plugin-ignore proof; `git diff --check`. Exact-head hosted CI remains required after the follow-up push. No Supabase, OpenAI, or other live product-provider command was run. | | 2026-07-17 | PR #699 / codex/test-reliability-hardening | 6202835ab1cf3703af311b9afdf372f74c63e040 | branch-cleanup-superseded | Closed as fully superseded by merged PR #705 (`e5caaa46c`). Range-diff maps the original implementation commit to #705's first integration commit; #705 then adds seven focused reliability fixes, while #699's remaining commit is merge-only and contributes no unique relevant patch. The exact-SHA remote ref and the unregistered local predecessor ref (`b518c1de9`) were deleted after final rechecks. | Fresh GitHub PR/head/status inventory; exact `ls-remote` and local-ref checks; cherry-pick-aware log; range-diff against PR #705's merged head; merge-parent verification; exact leased remote deletion and exact-old-value local `update-ref` deletion. No Supabase/OpenAI/product-provider checks run. | | 2026-07-17 | PR #716 / codex/pr-process-hardening-20260717 | 220de891f10f82df11eb2e8137367514c139a206 + reviewed implementation/follow-up diff | PR metadata, CI triage, review routing, Actions permissions, secret scanning, and branch-hygiene hardening | No remaining high-confidence P0-P2 defect in the reviewed process diff. Added a trusted base-SHA PR metadata/risk policy with draft and merge-queue handling; corrected CI triage to compare only the same workflow's latest completed `main` run; wired self-tests into local/hosted gates; documented the policy; created the missing durable Codex routing labels; removed four unused per-head routing labels; and applied read-only default Actions tokens, immutable Action pinning, no Actions-authored approvals, push protection, and automatic merged-branch deletion. Four policy defects were fixed before merge: API-only paths no longer trigger UI evidence, slash-bearing outcome titles are accepted, all seven governance items are required exactly, and placeholder risk/rollback text is rejected. | Offline `check:pr-policy`, `check:ci-triage`, `check:ci-scope`, action-pin check, scoped Prettier, and `git diff --check` passed. Initial hosted CI passed static, safety, coverage, build, images, Semgrep, Gitleaks, GitGuardian, and the aggregate gate; exact-head CI is required after the review fix. Broader local gates were deferred because other registered worktrees repeatedly held the heavyweight lock. GitHub provider inspection/settings changes were explicitly authorized. No Supabase/OpenAI/product-provider command ran. | +| 2026-07-17 | codex/mobile-search-phone-refresh-20260717 (supersedes PR #700) | bcaff007abe336508278236e7f5bc33ba4b0ddb3 + current-main integration | phone universal-search command-panel recovery and merge-readiness review | Recovered the still-useful behavior from PR #700 onto current `main`: the command dropdown now requires its placement breakpoint plus hover/fine-pointer capabilities, stays hydration-safe with deterministic initial state, suppresses fetching/opening/keyboard interception on touch phones, and covers a wide landscape touch context. The old PR's only actionable hydration finding and wide-touch review note are both incorporated. No remaining high-confidence P0-P2 defect was found in the scoped diff. | Focused Vitest 7/7; `npm run ensure` verified the project at `http://localhost:3751`; `format:changed -- --check`; `git diff --check`. Focused Playwright was attempted repeatedly but correctly blocked by active cross-worktree heavyweight locks, so exact-head hosted Production UI remains required before merge. No Supabase/OpenAI/product-provider command ran. | From 42a3e3ce65dc5a0e1dce386e0b91fccd23d13d6c Mon Sep 17 00:00:00 2001 From: BigSimmo <87357024+BigSimmo@users.noreply.github.com> Date: Fri, 17 Jul 2026 22:00:14 +0800 Subject: [PATCH 3/4] fix: preserve desktop search command panel --- docs/branch-review-ledger.md | 2 +- .../universal-search-command-surface.tsx | 28 +++++++++++++------ src/lib/search-command-surface.ts | 25 +++++++++++++---- tests/search-command-surface.test.ts | 24 ++++++++++++---- 4 files changed, 59 insertions(+), 20 deletions(-) diff --git a/docs/branch-review-ledger.md b/docs/branch-review-ledger.md index abc9b2cd6..ad1f01fbf 100644 --- a/docs/branch-review-ledger.md +++ b/docs/branch-review-ledger.md @@ -574,4 +574,4 @@ Use this ledger to prevent repeated branch and PR reviews when the reviewed HEAD | 2026-07-17 | PR #713 / codex/chat-workflow-ideas-0916 | b52112df6aa36311d7420189064acd79dcf2c3f5 + reviewed follow-up diff | workflow toolkit review follow-up | Fixed all 14 actionable Codex and CodeRabbit threads: cross-platform path fixtures, installation-managed preflight guidance, complete Supabase-backed API database scoping, per-command approval boundaries, plugin-ignore narrowing, isolated CI-scope proof, remote-Git command guarding, repository-skill verification classification, `TypeError` diagnosis, strict CLI option values, machine-parseable JSON evidence output, and preservation of baseline database/clinical approval gates in the RAG lab. No unresolved actionable finding remains in the reviewed scope. | `npm run verify:cheap` passed with 273 files and 2,599 tests; focused toolkit Vitest 20/20; CI-scope self-test; plugin-ignore proof; `git diff --check`. Exact-head hosted CI remains required after the follow-up push. No Supabase, OpenAI, or other live product-provider command was run. | | 2026-07-17 | PR #699 / codex/test-reliability-hardening | 6202835ab1cf3703af311b9afdf372f74c63e040 | branch-cleanup-superseded | Closed as fully superseded by merged PR #705 (`e5caaa46c`). Range-diff maps the original implementation commit to #705's first integration commit; #705 then adds seven focused reliability fixes, while #699's remaining commit is merge-only and contributes no unique relevant patch. The exact-SHA remote ref and the unregistered local predecessor ref (`b518c1de9`) were deleted after final rechecks. | Fresh GitHub PR/head/status inventory; exact `ls-remote` and local-ref checks; cherry-pick-aware log; range-diff against PR #705's merged head; merge-parent verification; exact leased remote deletion and exact-old-value local `update-ref` deletion. No Supabase/OpenAI/product-provider checks run. | | 2026-07-17 | PR #716 / codex/pr-process-hardening-20260717 | 220de891f10f82df11eb2e8137367514c139a206 + reviewed implementation/follow-up diff | PR metadata, CI triage, review routing, Actions permissions, secret scanning, and branch-hygiene hardening | No remaining high-confidence P0-P2 defect in the reviewed process diff. Added a trusted base-SHA PR metadata/risk policy with draft and merge-queue handling; corrected CI triage to compare only the same workflow's latest completed `main` run; wired self-tests into local/hosted gates; documented the policy; created the missing durable Codex routing labels; removed four unused per-head routing labels; and applied read-only default Actions tokens, immutable Action pinning, no Actions-authored approvals, push protection, and automatic merged-branch deletion. Four policy defects were fixed before merge: API-only paths no longer trigger UI evidence, slash-bearing outcome titles are accepted, all seven governance items are required exactly, and placeholder risk/rollback text is rejected. | Offline `check:pr-policy`, `check:ci-triage`, `check:ci-scope`, action-pin check, scoped Prettier, and `git diff --check` passed. Initial hosted CI passed static, safety, coverage, build, images, Semgrep, Gitleaks, GitGuardian, and the aggregate gate; exact-head CI is required after the review fix. Broader local gates were deferred because other registered worktrees repeatedly held the heavyweight lock. GitHub provider inspection/settings changes were explicitly authorized. No Supabase/OpenAI/product-provider command ran. | -| 2026-07-17 | codex/mobile-search-phone-refresh-20260717 (supersedes PR #700) | bcaff007abe336508278236e7f5bc33ba4b0ddb3 + current-main integration | phone universal-search command-panel recovery and merge-readiness review | Recovered the still-useful behavior from PR #700 onto current `main`: the command dropdown now requires its placement breakpoint plus hover/fine-pointer capabilities, stays hydration-safe with deterministic initial state, suppresses fetching/opening/keyboard interception on touch phones, and covers a wide landscape touch context. The old PR's only actionable hydration finding and wide-touch review note are both incorporated. No remaining high-confidence P0-P2 defect was found in the scoped diff. | Focused Vitest 7/7; `npm run ensure` verified the project at `http://localhost:3751`; `format:changed -- --check`; `git diff --check`. Focused Playwright was attempted repeatedly but correctly blocked by active cross-worktree heavyweight locks, so exact-head hosted Production UI remains required before merge. No Supabase/OpenAI/product-provider command ran. | +| 2026-07-17 | codex/mobile-search-phone-refresh-20260717 (supersedes PR #700) | 1c03d148f09ee82b9bf2a439cf9082efbb15fb0d + reviewed follow-up diff | phone universal-search command-panel recovery and merge-readiness review | Recovered the still-useful behavior from PR #700 onto current `main`, including its hydration fix and wide-touch regression coverage. Hosted Production UI then exposed one desktop capability defect: headless Chromium reports neither a fine pointer nor touch input, so the first media-query-only implementation hid the command panel on desktop. The follow-up now requires the placement breakpoint plus either a fine pointer or a zero-touch desktop fallback; wide touch devices remain suppressed while headless, remote, hybrid, and conventional desktops retain the panel. No remaining high-confidence P0-P2 defect was found in the scoped diff. | Initial focused Vitest 7/7; `npm run ensure` verified the project at `http://localhost:3751`; hosted static, safety, coverage, build, advisory UI, Semgrep, Gitleaks, and GitGuardian passed; the first hosted Production UI run isolated the nine desktop regressions; current `format:changed -- --check` and `git diff --check` passed. The current focused Vitest/Playwright rerun is blocked by another registered worktree's active heavyweight lock, so exact-head hosted Production UI remains required before merge. No Supabase/OpenAI/product-provider command ran. | diff --git a/src/components/clinical-dashboard/universal-search-command-surface.tsx b/src/components/clinical-dashboard/universal-search-command-surface.tsx index fc925f4a3..9affcc4f0 100644 --- a/src/components/clinical-dashboard/universal-search-command-surface.tsx +++ b/src/components/clinical-dashboard/universal-search-command-surface.tsx @@ -21,7 +21,9 @@ import { cn } from "@/components/ui-primitives"; import { appModeDefinition, type AppModeId } from "@/lib/app-modes"; import { appModeIcons } from "@/lib/app-mode-icons"; import { - commandDropdownDisplayMediaQuery, + commandDropdownCanDisplay, + commandDropdownMinimumWidthMediaQuery, + commandDropdownPointerMediaQuery, differentialRedFlagTerms, filteredSuggestions, isFormCodeQuery, @@ -382,22 +384,32 @@ export function UniversalSearchCommandSurface({ const mode = appModeDefinition(modeId); // The dropdown is a fine-pointer desktop enhancement. Width-only checks let // wide, zoomed, or desktop-mode phones open it over the page. - const dropdownMediaQuery = commandDropdownDisplayMediaQuery(placement); + const dropdownMinimumWidthQuery = commandDropdownMinimumWidthMediaQuery(placement); const [dropdownDisplayable, setDropdownDisplayable] = useState(false); useEffect(() => { if (typeof window.matchMedia !== "function") return; - const mediaQuery = window.matchMedia(dropdownMediaQuery); + const minimumWidthMedia = window.matchMedia(dropdownMinimumWidthQuery); + const pointerMedia = window.matchMedia(commandDropdownPointerMediaQuery); const sync = () => { - setDropdownDisplayable(mediaQuery.matches); - if (!mediaQuery.matches) { + const displayable = commandDropdownCanDisplay({ + minimumWidthMatches: minimumWidthMedia.matches, + pointerMatches: pointerMedia.matches, + maxTouchPoints: navigator.maxTouchPoints, + }); + setDropdownDisplayable(displayable); + if (!displayable) { onDropdownOpenChange(false); setActiveIndex(-1); } }; sync(); - mediaQuery.addEventListener("change", sync); - return () => mediaQuery.removeEventListener("change", sync); - }, [dropdownMediaQuery, onDropdownOpenChange]); + minimumWidthMedia.addEventListener("change", sync); + pointerMedia.addEventListener("change", sync); + return () => { + minimumWidthMedia.removeEventListener("change", sync); + pointerMedia.removeEventListener("change", sync); + }; + }, [dropdownMinimumWidthQuery, onDropdownOpenChange]); // A true "everything" view: the active mode's own domain is included (no excludeDomain) so // the palette surfaces every entity type, ordered by the server's intent-aware domainOrder. const universal = useUniversalSearch({ diff --git a/src/lib/search-command-surface.ts b/src/lib/search-command-surface.ts index 62fe52df0..771e334ec 100644 --- a/src/lib/search-command-surface.ts +++ b/src/lib/search-command-surface.ts @@ -21,15 +21,30 @@ export type SearchCommandSurfaceConfig = { export type CommandSurfacePlacement = "bottom-dock" | "inline"; +export function commandDropdownMinimumWidthMediaQuery(placement: CommandSurfacePlacement) { + const minimumWidth = placement === "bottom-dock" ? "640px" : "1024px"; + return `(min-width: ${minimumWidth})`; +} + +export const commandDropdownPointerMediaQuery = "(hover: hover) and (pointer: fine)"; + /** * The command panel is a desktop enhancement. Width alone is not enough to * identify that environment: phones can report a wide viewport in landscape, - * display-zoom, or desktop-site modes. Requiring a hover-capable fine pointer - * keeps the panel closed on touch-first phones while preserving it for desktop. + * display-zoom, or desktop-site modes. A fine pointer enables the panel on + * hybrid desktops; a zero-touch fallback keeps headless/remote desktops usable + * when the browser reports no pointer hardware at all. */ -export function commandDropdownDisplayMediaQuery(placement: CommandSurfacePlacement) { - const minimumWidth = placement === "bottom-dock" ? "640px" : "1024px"; - return `(min-width: ${minimumWidth}) and (hover: hover) and (pointer: fine)`; +export function commandDropdownCanDisplay({ + minimumWidthMatches, + pointerMatches, + maxTouchPoints, +}: { + minimumWidthMatches: boolean; + pointerMatches: boolean; + maxTouchPoints: number; +}) { + return minimumWidthMatches && (pointerMatches || maxTouchPoints === 0); } const searchCommandSurfaceByMode: Partial> = { diff --git a/tests/search-command-surface.test.ts b/tests/search-command-surface.test.ts index 402b65e30..acf7b99d2 100644 --- a/tests/search-command-surface.test.ts +++ b/tests/search-command-surface.test.ts @@ -1,7 +1,9 @@ import { describe, expect, it } from "vitest"; import { - commandDropdownDisplayMediaQuery, + commandDropdownCanDisplay, + commandDropdownMinimumWidthMediaQuery, + commandDropdownPointerMediaQuery, differentialRedFlagTerms, favouriteMatchesCommandScopes, filteredSuggestions, @@ -29,12 +31,22 @@ function serviceRecord(overrides: Partial = {}): ServiceRecord { } describe("search command surface", () => { - it("requires desktop pointer capabilities before displaying the command dropdown", () => { - expect(commandDropdownDisplayMediaQuery("bottom-dock")).toBe( - "(min-width: 640px) and (hover: hover) and (pointer: fine)", + it("requires a desktop-sized non-touch or fine-pointer environment for the command dropdown", () => { + expect(commandDropdownMinimumWidthMediaQuery("bottom-dock")).toBe("(min-width: 640px)"); + expect(commandDropdownMinimumWidthMediaQuery("inline")).toBe("(min-width: 1024px)"); + expect(commandDropdownPointerMediaQuery).toBe("(hover: hover) and (pointer: fine)"); + + expect(commandDropdownCanDisplay({ minimumWidthMatches: true, pointerMatches: true, maxTouchPoints: 5 })).toBe( + true, + ); + expect(commandDropdownCanDisplay({ minimumWidthMatches: true, pointerMatches: false, maxTouchPoints: 0 })).toBe( + true, + ); + expect(commandDropdownCanDisplay({ minimumWidthMatches: true, pointerMatches: false, maxTouchPoints: 5 })).toBe( + false, ); - expect(commandDropdownDisplayMediaQuery("inline")).toBe( - "(min-width: 1024px) and (hover: hover) and (pointer: fine)", + expect(commandDropdownCanDisplay({ minimumWidthMatches: false, pointerMatches: true, maxTouchPoints: 0 })).toBe( + false, ); }); From 4ccced811371dc21f528b5308770b37d63c4157f Mon Sep 17 00:00:00 2001 From: BigSimmo <87357024+BigSimmo@users.noreply.github.com> Date: Fri, 17 Jul 2026 22:12:53 +0800 Subject: [PATCH 4/4] fix: open desktop search panel on first focus --- docs/branch-review-ledger.md | 2 +- .../universal-search-command-surface.tsx | 12 +++++++++++- 2 files changed, 12 insertions(+), 2 deletions(-) diff --git a/docs/branch-review-ledger.md b/docs/branch-review-ledger.md index ad1f01fbf..359c6ba1d 100644 --- a/docs/branch-review-ledger.md +++ b/docs/branch-review-ledger.md @@ -574,4 +574,4 @@ Use this ledger to prevent repeated branch and PR reviews when the reviewed HEAD | 2026-07-17 | PR #713 / codex/chat-workflow-ideas-0916 | b52112df6aa36311d7420189064acd79dcf2c3f5 + reviewed follow-up diff | workflow toolkit review follow-up | Fixed all 14 actionable Codex and CodeRabbit threads: cross-platform path fixtures, installation-managed preflight guidance, complete Supabase-backed API database scoping, per-command approval boundaries, plugin-ignore narrowing, isolated CI-scope proof, remote-Git command guarding, repository-skill verification classification, `TypeError` diagnosis, strict CLI option values, machine-parseable JSON evidence output, and preservation of baseline database/clinical approval gates in the RAG lab. No unresolved actionable finding remains in the reviewed scope. | `npm run verify:cheap` passed with 273 files and 2,599 tests; focused toolkit Vitest 20/20; CI-scope self-test; plugin-ignore proof; `git diff --check`. Exact-head hosted CI remains required after the follow-up push. No Supabase, OpenAI, or other live product-provider command was run. | | 2026-07-17 | PR #699 / codex/test-reliability-hardening | 6202835ab1cf3703af311b9afdf372f74c63e040 | branch-cleanup-superseded | Closed as fully superseded by merged PR #705 (`e5caaa46c`). Range-diff maps the original implementation commit to #705's first integration commit; #705 then adds seven focused reliability fixes, while #699's remaining commit is merge-only and contributes no unique relevant patch. The exact-SHA remote ref and the unregistered local predecessor ref (`b518c1de9`) were deleted after final rechecks. | Fresh GitHub PR/head/status inventory; exact `ls-remote` and local-ref checks; cherry-pick-aware log; range-diff against PR #705's merged head; merge-parent verification; exact leased remote deletion and exact-old-value local `update-ref` deletion. No Supabase/OpenAI/product-provider checks run. | | 2026-07-17 | PR #716 / codex/pr-process-hardening-20260717 | 220de891f10f82df11eb2e8137367514c139a206 + reviewed implementation/follow-up diff | PR metadata, CI triage, review routing, Actions permissions, secret scanning, and branch-hygiene hardening | No remaining high-confidence P0-P2 defect in the reviewed process diff. Added a trusted base-SHA PR metadata/risk policy with draft and merge-queue handling; corrected CI triage to compare only the same workflow's latest completed `main` run; wired self-tests into local/hosted gates; documented the policy; created the missing durable Codex routing labels; removed four unused per-head routing labels; and applied read-only default Actions tokens, immutable Action pinning, no Actions-authored approvals, push protection, and automatic merged-branch deletion. Four policy defects were fixed before merge: API-only paths no longer trigger UI evidence, slash-bearing outcome titles are accepted, all seven governance items are required exactly, and placeholder risk/rollback text is rejected. | Offline `check:pr-policy`, `check:ci-triage`, `check:ci-scope`, action-pin check, scoped Prettier, and `git diff --check` passed. Initial hosted CI passed static, safety, coverage, build, images, Semgrep, Gitleaks, GitGuardian, and the aggregate gate; exact-head CI is required after the review fix. Broader local gates were deferred because other registered worktrees repeatedly held the heavyweight lock. GitHub provider inspection/settings changes were explicitly authorized. No Supabase/OpenAI/product-provider command ran. | -| 2026-07-17 | codex/mobile-search-phone-refresh-20260717 (supersedes PR #700) | 1c03d148f09ee82b9bf2a439cf9082efbb15fb0d + reviewed follow-up diff | phone universal-search command-panel recovery and merge-readiness review | Recovered the still-useful behavior from PR #700 onto current `main`, including its hydration fix and wide-touch regression coverage. Hosted Production UI then exposed one desktop capability defect: headless Chromium reports neither a fine pointer nor touch input, so the first media-query-only implementation hid the command panel on desktop. The follow-up now requires the placement breakpoint plus either a fine pointer or a zero-touch desktop fallback; wide touch devices remain suppressed while headless, remote, hybrid, and conventional desktops retain the panel. No remaining high-confidence P0-P2 defect was found in the scoped diff. | Initial focused Vitest 7/7; `npm run ensure` verified the project at `http://localhost:3751`; hosted static, safety, coverage, build, advisory UI, Semgrep, Gitleaks, and GitGuardian passed; the first hosted Production UI run isolated the nine desktop regressions; current `format:changed -- --check` and `git diff --check` passed. The current focused Vitest/Playwright rerun is blocked by another registered worktree's active heavyweight lock, so exact-head hosted Production UI remains required before merge. No Supabase/OpenAI/product-provider command ran. | +| 2026-07-17 | codex/mobile-search-phone-refresh-20260717 (supersedes PR #700) | 42a3e3ce65dc5a0e1dce386e0b91fccd23d13d6c + reviewed follow-up diff | phone universal-search command-panel recovery and merge-readiness review | Recovered the still-useful behavior from PR #700 onto current `main`, including its hydration fix and wide-touch regression coverage. Hosted Production UI then exposed one desktop focus race: capability state intentionally initializes false for hydration safety, but an input could receive focus before the post-hydration effect synchronized the real browser state. The follow-up recomputes the same guarded predicate synchronously on focus; it requires the placement breakpoint plus either a fine pointer or a zero-touch desktop fallback, so wide touch devices remain suppressed while desktop keeps the first command-panel interaction. No remaining high-confidence P0-P2 defect was found in the scoped diff. | Focused Vitest 7/7; `npm run ensure` verified the project at `http://localhost:3751`; hosted static, safety, coverage, build, advisory UI, Semgrep, Gitleaks, and GitGuardian passed; the first hosted Production UI run isolated the nine desktop regressions. The focused browser proof reproduced the desktop race while the wide-touch regression passed, and exact-head hosted Production UI remains required after the focus fix. `format:changed -- --check` and `git diff --check` passed before the final follow-up. No Supabase/OpenAI/product-provider command ran. | diff --git a/src/components/clinical-dashboard/universal-search-command-surface.tsx b/src/components/clinical-dashboard/universal-search-command-surface.tsx index 9affcc4f0..3389983d1 100644 --- a/src/components/clinical-dashboard/universal-search-command-surface.tsx +++ b/src/components/clinical-dashboard/universal-search-command-surface.tsx @@ -984,7 +984,17 @@ export function UniversalSearchCommandSurface({ } }} onFocusCapture={() => { - if (dropdownDisplayable) onDropdownOpenChange(true); + // Focus can arrive before the post-hydration effect has synchronized + // the conservative false initial state. Re-evaluate synchronously so + // desktop input never loses its first command-panel interaction. + if (typeof window.matchMedia !== "function") return; + const displayable = commandDropdownCanDisplay({ + minimumWidthMatches: window.matchMedia(dropdownMinimumWidthQuery).matches, + pointerMatches: window.matchMedia(commandDropdownPointerMediaQuery).matches, + maxTouchPoints: navigator.maxTouchPoints, + }); + setDropdownDisplayable(displayable); + if (displayable) onDropdownOpenChange(true); }} onBlurCapture={(event) => { if (!event.currentTarget.contains(event.relatedTarget as Node | null)) {