Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions docs/branch-review-ledger.md
Original file line number Diff line number Diff line change
Expand Up @@ -43,3 +43,4 @@ Use this ledger to prevent repeated branch and PR reviews when the reviewed HEAD
| 2026-07-11 | PR #483 / claude/differentials-page-review-a3daaf | 36cca1bf7c13718dcc60a61b75272c7c4fa5cd44 | open-PR review, unresolved comments, and CI | P2 fixed: authenticated diagnosis detail responses now derive related links, overlap links, and comparison presentation from the owner's current diagnosis and presentation rows rather than the bundled snapshot. Added an owner-only catalog regression test. No additional high-confidence defect was found in the changed scope. | Focused differentials route/catalog Vitest (26/26); TypeScript; focused Prettier; `git diff --check`. Production readiness ran fail-closed with provider variables cleared and reported only expected missing provider configuration. |
| 2026-07-11 | PR #489 / claude/document-viewer-redesign-55b68b | 9130c8b15a22dbbc965464a247ae930c04f2da62 | open-PR review, unresolved comments, and CI | P2 fixed: document deep links now expand the mobile indexed-text details and scroll the branch-specific visible mobile or desktop chunk instead of the first duplicated DOM match. Added focused desktop/mobile assertions. No additional high-confidence defect was found in the three-file diff. | Focused Prettier; TypeScript; `git diff --check`. Browser proof delegated to hosted CI because Turbopack rejects the isolated worktree's external node_modules junction. |
| 2026-07-11 | PR #488 / claude/code-review-42a2c3 | 7a8ea145013444f7cc29631499f48a8b0454937a | open-PR review, unresolved comments, and CI | Confirmed the remaining public error-code finding was already fixed at the reviewed head. Added the two focused advisory UI assertion stabilizations required by the hosted failure logs; no additional high-confidence defect was found in the changed scope. | `tests/http-error-response.test.ts` (3/3); Prettier check on affected files; `git diff --check`; hosted required CI passed before the test-only fix. Browser rerun deferred to hosted CI because Turbopack rejects the isolated worktree's external node_modules junction. |
| 2026-07-11 | PR #473 / claude/mobile-search-bar-popup-bx163m | 7cef01852a9713ec51184578868212df5805adbf | open-PR review, unresolved comments, and CI | P1 merge-conflict markers removed from the shared search header while retaining the all-viewport hero portal and inline fallback. P2 fixed: phone-hidden command results can no longer open, report expanded state, receive keyboard navigation, or execute an invisible selection. The launcher and global-shell conflict findings were already resolved at the reviewed head. | No conflict markers; TypeScript; focused Prettier; app-mode/search/universal-search Vitest (37/37); `git diff --check`. Browser proof delegated to hosted CI because Turbopack rejects the isolated worktree's external node_modules junction. |
7 changes: 4 additions & 3 deletions src/components/ClinicalDashboard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3253,7 +3253,6 @@ export function ClinicalDashboard({
differentialsCompareAddonActive ? differentialsMobileCompareAddonSlotId : undefined
}
desktopHomeComposerSlotId={desktopHomeComposerSlotId}
heroComposerFromTablet={Boolean(desktopHomeComposerSlotId)}
// Phone-only: the header sits above the internally scrolling <main>,
// so hiding must collapse its layout space to hand it to content.
hideOnScroll={{ strategy: "collapse", scrollHidden: phoneScrollHide.hidden }}
Expand Down Expand Up @@ -3287,7 +3286,9 @@ export function ClinicalDashboard({
? differentialsCompareAddonActive
? "mb-[calc(8.75rem+env(safe-area-inset-bottom))] sm:mb-0"
: "mb-[calc(5rem+env(safe-area-inset-bottom))] sm:mb-0"
: compactMobileModeHome
: // Mode homes keep the composer in the hero (in-flow at every
// width), so phones need no bottom-dock clearance on them.
compactMobileModeHome || showDesktopHomeComposer
? "mb-0"
: "mb-[calc(5.25rem+env(safe-area-inset-bottom))] sm:mb-0"
: "mb-0",
Expand All @@ -3310,7 +3311,7 @@ export function ClinicalDashboard({
: hasMobileBottomSearch
? compactMobileModeHome
? "pb-4 sm:pb-10 lg:pb-12"
: compactMobileBottomSearch
: compactMobileBottomSearch || showDesktopHomeComposer
? "pb-8 sm:pb-10 lg:pb-12"
: "pb-32 sm:pb-10 lg:pb-12"
: "pb-8 sm:pb-10 lg:pb-12",
Expand Down
3 changes: 3 additions & 0 deletions src/components/applications-launcher-page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -733,6 +733,9 @@ export function ApplicationsLauncherWorkspace({
aria-labelledby="tools-home-title"
className={cn(
"mx-auto w-full max-w-[90rem] overflow-x-hidden px-4 pb-8 text-[color:var(--text)] sm:px-6 lg:px-8",
// The composer sits in the hero at every width, so phones only need a
// small safe-area cushion rather than bottom-dock clearance.
"pb-[calc(2rem+env(safe-area-inset-bottom))] sm:pb-8",
"pt-7 sm:pt-10 lg:pt-14",
className,
)}
Expand Down
23 changes: 11 additions & 12 deletions src/components/clinical-dashboard/global-mockup-search-shell.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -255,18 +255,18 @@ function GlobalMockupStandaloneSearchShellClient({
const effectiveSidebarCollapsed = isDifferentialPresentationWorkflow ? true : sidebarCollapsed;
const effectiveSidebarWidth = shouldShowDesktopSidebar ? (effectiveSidebarCollapsed ? "5.25rem" : "20rem") : "0px";
const shouldShowSearchComposer = searchComposerVisible && !isDifferentialPresentationWorkflow;
const usesInlineModeHomeComposer = shouldShowSearchComposer && isStandaloneModeHome;
const reservesFloatingComposer = shouldShowSearchComposer && !usesInlineModeHomeComposer;
// Standalone mode homes use an inline hero composer from `sm` upward, but
// phones still render the fixed bottom dock. Reserve its full height on the
// phone layout even though the wider layouts do not need floating clearance.
const reservesFloatingComposer = shouldShowSearchComposer && !isStandaloneModeHome;
// Standalone mode homes portal the composer into the hero (in-flow at every
// width), so phones need no bottom-dock clearance there.
const mobileComposerReserve = !shouldShowSearchComposer
? "2rem"
: searchMode === "answer"
? "calc(9rem + env(safe-area-inset-bottom))"
: useCompactBottomSearch
? "calc(5.5rem + env(safe-area-inset-bottom))"
: "calc(9rem + env(safe-area-inset-bottom))";
: isStandaloneModeHome
? "2rem"
: searchMode === "answer"
? "calc(9rem + env(safe-area-inset-bottom))"
: useCompactBottomSearch
? "calc(5.5rem + env(safe-area-inset-bottom))"
: "calc(9rem + env(safe-area-inset-bottom))";

useEffect(() => {
// Re-derive the mode and query from the URL, but only when the search string
Expand Down Expand Up @@ -528,7 +528,6 @@ function GlobalMockupStandaloneSearchShellClient({
desktopSearchPlacement={desktopSearchPlacement === "hero" && isStandaloneModeHome ? "hero" : "default"}
searchComposerVisible={shouldShowSearchComposer}
desktopHomeComposerSlotId={isStandaloneModeHome ? modeHomeDesktopComposerSlotId : undefined}
heroComposerFromTablet={isStandaloneModeHome}
// Phone-only: #main-content owns vertical scroll, so hide-on-scroll
// collapses the header/composer to hand space back to content.
hideOnScroll={{ strategy: "collapse", scrollHidden: phoneScrollHide.hidden }}
Expand Down Expand Up @@ -556,7 +555,7 @@ function GlobalMockupStandaloneSearchShellClient({
? "max-sm:pb-[var(--mobile-composer-reserve)] sm:pb-[calc(9rem+env(safe-area-inset-bottom))]"
: useCompactBottomSearch
? "max-sm:pb-[var(--mobile-composer-reserve)] sm:pb-8"
: "max-sm:pb-[var(--mobile-composer-reserve)] sm:pb-8",
: "max-sm:pb-[var(--mobile-composer-reserve)] sm:pb-[calc(9rem+env(safe-area-inset-bottom))] sm:pb-8",
)}
>
<div className="max-sm:flex max-sm:min-h-0 max-sm:flex-1 max-sm:flex-col">
Expand Down
60 changes: 40 additions & 20 deletions src/components/clinical-dashboard/master-search-header.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import {
useState,
type FocusEvent as ReactFocusEvent,
type KeyboardEvent as ReactKeyboardEvent,
type Ref,
type RefObject,
} from "react";
import { createPortal } from "react-dom";

Expand Down Expand Up @@ -71,10 +71,6 @@ import { tagSearchText } from "@/lib/document-tags";
const phoneSearchLayoutMediaQuery = "(max-width: 639px)";
const scopeSheetMediaQuery = "(max-width: 1023px)";
const desktopHomeComposerMediaQuery = "(min-width: 1024px)";
// Mode-home shells centre the composer in the hero at every width (see
// heroComposerFromTablet): phones share the hero-centred landing design that
// tests/ui-tools.spec.ts "mode home routes center the shared search on mobile"
// encodes, so the query intentionally always matches.
const modeHomeComposerMediaQuery = "(min-width: 0px)";
const defaultVisibleAppModeOptions = visibleAppModeDefinitions();

Expand Down Expand Up @@ -179,7 +175,6 @@ export function MasterSearchHeader({
searchComposerVisible = true,
desktopHomeComposerSlotId,
mobileBottomSearchAddonSlotId,
heroComposerFromTablet = false,
mobileLeadingAction = "menu",
onMobileBack,
hideOnScroll,
Expand Down Expand Up @@ -211,7 +206,7 @@ export function MasterSearchHeader({
onNewChat?: () => void;
onOpenMobileSidebar?: () => void;
queryModeOptions: Array<{ value: ClinicalQueryMode; label: string }>;
queryInputRef?: Ref<HTMLInputElement>;
queryInputRef?: RefObject<HTMLInputElement | null>;
queryInputAutoFocus?: boolean;
/** Overrides the mode's default input placeholder (e.g. "Ask a follow-up..." mid-thread). */
composerPlaceholder?: string;
Expand All @@ -231,12 +226,12 @@ export function MasterSearchHeader({
mobileBottomSearchVariant?: "default" | "compact";
desktopSearchPlacement?: "default" | "hero";
searchComposerVisible?: boolean;
/** Mode-home slot the composer portals into at every viewport width, so the
* search pill sits in the middle of the hero on phones as well as desktop
* instead of docking to the bottom edge. */
desktopHomeComposerSlotId?: string;
/** Phone-only slot rendered above the bottom search pill for page-specific dock addons. */
mobileBottomSearchAddonSlotId?: string;
/** Portal the composer into the hero slot from the tablet breakpoint (sm) up,
* rather than the default desktop (lg) breakpoint. */
heroComposerFromTablet?: boolean;
mobileLeadingAction?: "menu" | "back";
onMobileBack?: () => void;
/** Phone-only hide-on-scroll for the universal header and bottom search dock.
Expand Down Expand Up @@ -305,8 +300,13 @@ export function MasterSearchHeader({
const scrollHidden = hideOnScroll?.scrollHidden !== undefined ? hideOnScroll.scrollHidden : internalScrollHidden;
const headerChromeHidden =
scrollHidden && !modeMenuOpen && !actionMenuOpen && !scopeOpen && !scopeSheetOpen && !headerChromeFocused;
// Mode homes portal the composer into the hero slot at every width, so the
// phone bottom dock only exists when no hero slot is provided.
const phoneBottomSearchDockActive =
usesPhoneSearchLayout && searchComposerVisible && (isAnswerFooterComposer || mobileSearchPlacement === "bottom");
usesPhoneSearchLayout &&
searchComposerVisible &&
!desktopHomeComposerSlotId &&
(isAnswerFooterComposer || mobileSearchPlacement === "bottom");
const bottomComposerScrollHiddenActive = Boolean(hideOnScroll && phoneBottomSearchDockActive);
const bottomComposerHidden =
bottomComposerScrollHiddenActive &&
Expand Down Expand Up @@ -614,6 +614,22 @@ export function MasterSearchHeader({
visibleAppModeOptions.findIndex((mode) => mode.id === selectedAppMode.id),
);

// Both the hero-portal composer and the default composer bind the caller's
// queryInputRef. During home <-> result transitions the two briefly coexist,
// and React nulls a plain shared ref when the outgoing composer unmounts —
// clobbering the surviving input's binding (quote follow-up focus broke).
// A cleanup-function ref only clears the binding it still owns.
const bindQueryInputRef = useCallback(
(element: HTMLInputElement | null) => {
if (!element || !queryInputRef) return undefined;
queryInputRef.current = element;
return () => {
if (queryInputRef.current === element) queryInputRef.current = null;
};
},
[queryInputRef],
);

function focusModeOption(index: number) {
const nextIndex = (index + visibleAppModeOptions.length) % visibleAppModeOptions.length;
modeOptionRefs.current[nextIndex]?.focus();
Expand Down Expand Up @@ -711,13 +727,17 @@ export function MasterSearchHeader({
// into it made React reconcile the portal against a container that another
// part of the tree had already removed, throwing a null-parentNode error.
// Because the host is stable, React's portal container never disappears.
// The slot is used at every viewport width — phones included — so mode
// homes keep the composer in the middle of the hero instead of docking it
// to the bottom edge.
const host = document.createElement("div");
// Layout-transparent so the composer lays out as a direct child of the slot.
host.style.display = "contents";

const mediaQuery = window.matchMedia(
heroComposerFromTablet ? modeHomeComposerMediaQuery : desktopHomeComposerMediaQuery,
desktopHomeComposerSlotId ? modeHomeComposerMediaQuery : desktopHomeComposerMediaQuery,
);

let frame: number | null = null;
let retryTimeout: number | null = null;
let portalRetryCount = 0;
Expand Down Expand Up @@ -767,7 +787,7 @@ export function MasterSearchHeader({
setDesktopHomeComposerFallback(false);
setDesktopHomeComposerHost(null);
};
}, [desktopHomeComposerSlotId, heroComposerFromTablet]);
}, [desktopHomeComposerSlotId]);

const dismissModeMenu = useCallback(() => setModeMenuOpen(false), []);
function dismissScope(reason: "outside" | "escape") {
Expand Down Expand Up @@ -1098,7 +1118,11 @@ export function MasterSearchHeader({
onSubmit={submit}
data-footer-variant={usesPhoneFooterDock ? (usesCompactMobileBottomStyle ? "compact" : "default") : undefined}
data-footer-addon={usesPhoneFooterDock && mobileBottomSearchAddonSlotId ? "differentials-compare" : undefined}
data-command-open={usesBottomComposerPlacement && commandDropdownOpen ? "true" : undefined}
data-command-open={
// Phones never show the command dropdown, so the dock scrim must not
// grow for it — gate the open attribute to widths that can display it.
usesBottomComposerPlacement && !usesPhoneSearchLayout && commandDropdownOpen ? "true" : undefined
}
data-scroll-hidden={shouldHideBottomOnScroll && bottomComposerHidden ? "true" : undefined}
{...(shouldHideBottomOnScroll ? composerFocusProps : undefined)}
className={cn(
Expand Down Expand Up @@ -1176,11 +1200,7 @@ export function MasterSearchHeader({
onRunModeAction={runModeAction}
onCommandScopesChange={(scopes) => onCommandScopesChange?.(scopes)}
onListboxIdReady={setCommandListboxId}
onFocusSearchInput={() => {
if (queryInputRef && "current" in queryInputRef) {
queryInputRef.current?.focus();
}
}}
onFocusSearchInput={() => queryInputRef?.current?.focus()}
>
<div
data-menu-placement={actionMenuOpen ? actionMenuPlacement : undefined}
Expand Down Expand Up @@ -1222,7 +1242,7 @@ export function MasterSearchHeader({
pr-* utility, which let text run under an overlaid button. */}
<label className="flex min-w-0 flex-1 items-center overflow-hidden">
<input
ref={queryInputRef}
ref={bindQueryInputRef}
data-testid="global-search-input"
autoFocus={queryInputAutoFocus}
value={query}
Expand Down
Loading