Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
38 commits
Select commit Hold shift + click to select a range
a25bc2f
fix: align mode home responsive layout
BigSimmo Jul 24, 2026
1c1dd35
fix: idle phone reserve for hero-owned mode homes
BigSimmo Jul 24, 2026
a8abbc0
chore: keep dashboard within size budget
BigSimmo Jul 24, 2026
961003e
Merge branch 'main' into codex/apply-phone-layout-to-all-home-pages
BigSimmo Jul 24, 2026
c3ddc84
Merge branch 'main' into codex/apply-phone-layout-to-all-home-pages
BigSimmo Jul 24, 2026
5bbe3e3
fix: keep dock reserve when answer home has no hero slot
cursoragent Jul 24, 2026
b38a44a
Merge branch 'main' into codex/apply-phone-layout-to-all-home-pages
BigSimmo Jul 24, 2026
c3fe913
Merge remote-tracking branch 'origin/main' into HEAD
cursoragent Jul 24, 2026
9a53289
fix: keep guest favourites on dock reserve
cursoragent Jul 24, 2026
e3dc870
chore: note review reply permission denial
cursoragent Jul 24, 2026
769cbbc
docs: record PR babysit sweep
cursoragent Jul 24, 2026
93860a1
Merge remote-tracking branch 'origin/main' into HEAD
cursoragent Jul 24, 2026
759181b
Merge remote-tracking branch 'origin/main' into HEAD
cursoragent Jul 24, 2026
7c58149
Merge branch 'main' into codex/apply-phone-layout-to-all-home-pages
BigSimmo Jul 24, 2026
2cb98b4
Merge remote-tracking branch 'origin/main' into HEAD
BigSimmo Jul 24, 2026
6562a0f
docs: record Run PR behind-main sweep for #1124
BigSimmo Jul 24, 2026
e061ce2
fix: drop prescribing hero slot once draft query is typed
BigSimmo Jul 24, 2026
a0335df
docs: ledger Run PR babysit for #1124
BigSimmo Jul 24, 2026
bbd5aaf
merge: sync origin/main into apply-phone-layout
BigSimmo Jul 24, 2026
8adaaf2
docs: ledger post-main sync for #1124
BigSimmo Jul 24, 2026
d0a96ca
Merge remote-tracking branch 'origin/main' into codex/apply-phone-lay…
BigSimmo Jul 24, 2026
9d48f67
docs: ledger re-sync sweep for PR #1124
BigSimmo Jul 24, 2026
426760a
Merge origin/main into mode home layout PR
cursoragent Jul 24, 2026
c59ae82
fix: dock reserve for stale differentials drafts and ledger dup
cursoragent Jul 24, 2026
55a4a61
docs: ledger babysit outcome for PR #1124
cursoragent Jul 24, 2026
fb39314
Merge branch 'main' into codex/apply-phone-layout-to-all-home-pages
cursoragent Jul 24, 2026
58c1abe
Merge branch 'main' into codex/apply-phone-layout-to-all-home-pages
cursoragent Jul 24, 2026
466b42f
Merge branch 'main' into codex/apply-phone-layout-to-all-home-pages
cursoragent Jul 24, 2026
6653a30
Merge branch 'main' into codex/apply-phone-layout-to-all-home-pages
cursoragent Jul 24, 2026
caadd72
Merge remote-tracking branch 'origin/main' into codex/apply-phone-lay…
cursoragent Jul 24, 2026
382d4c3
Merge branch 'main' into codex/apply-phone-layout-to-all-home-pages
BigSimmo Jul 24, 2026
62c0a05
Merge remote-tracking branch 'origin/main' into codex/apply-phone-lay…
cursoragent Jul 24, 2026
581febd
fix: auto-sync open PR branches and clear postcss audit
cursoragent Jul 24, 2026
5ce9e96
Merge remote-tracking branch 'origin/main' into codex/apply-phone-lay…
cursoragent Jul 24, 2026
09e188e
Merge cursor/pr-queue-hygiene-72ec for postcss audit + PR branch sync
cursoragent Jul 24, 2026
fe5a187
Merge branch 'main' into codex/apply-phone-layout-to-all-home-pages
BigSimmo Jul 24, 2026
6262b64
Merge remote-tracking branch 'origin/main' into codex/apply-phone-lay…
cursoragent Jul 24, 2026
41d5582
fix(ledger): drop duplicate rows blocking #1124 Static PR
cursoragent Jul 24, 2026
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
13 changes: 10 additions & 3 deletions docs/branch-review-ledger.md

Large diffs are not rendered by default.

24 changes: 17 additions & 7 deletions src/components/ClinicalDashboard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3054,15 +3054,24 @@ export function ClinicalDashboard({
const showDesktopHomeComposer =
!error &&
(activeModeResultKind === "tools" ||
activeModeResultKind === "favourites" ||
(activeModeResultKind === "favourites" && favouritesAccessible) ||
(!loading &&
(showAnswerHome ||
(searchMode === "documents" &&
activeModeResultKind === "documents" &&
documentMatches.length === 0 &&
!modeSearchSubmitted) ||
(searchMode === "prescribing" && activeModeResultKind === "documents" && !modeSearchSubmitted) ||
(activeModeResultKind === "differentials" && !modeSearchSubmitted))));
// Prescribing home unmounts as soon as the query is non-empty, so keep
// the hero/phone-composer slot only while MedicationHome actually mounts.
(searchMode === "prescribing" &&
activeModeResultKind === "documents" &&
!modeSearchSubmitted &&
!query.trim()) ||
// DifferentialsHome leaves ModeHomeTemplate when a draft query coincides
// with stale evidence matches — keep the hero slot only while home mounts.
(activeModeResultKind === "differentials" &&
!modeSearchSubmitted &&
!(query.trim() && documentMatches.length > 0)))));
const desktopHomeComposerSlotId = showDesktopHomeComposer ? modeHomeDesktopComposerSlotId : undefined;
// Favourites and Tools are content-rich hubs: they share the centred hero but
// stay top-aligned so their lists start in a stable position.
Expand All @@ -3076,13 +3085,15 @@ export function ClinicalDashboard({
((searchMode === "services" || searchMode === "forms") && !modeSearchSubmitted && !query.trim() && !loading);
const differentialsCompareAddonActive =
searchMode === "differentials" && modeSearchSubmitted && Boolean(query.trim());
const heroOwnsPhoneComposer = Boolean(desktopHomeComposerSlotId);
Comment thread
cursor[bot] marked this conversation as resolved.
Comment thread
cursor[bot] marked this conversation as resolved.
// Hidden dock pad must stay at 0rem — Safari toolbar safe-area recreates a blank band.
const mobileComposerReserve = resolveMobileComposerReserve(
bottomComposerHidden,
resolveDashboardVisibleMobileComposerReserve({
searchMode,
hasAnswerFollowUps: answerFollowUpSuggestions.length > 0,
differentialsCompareAddonActive,
heroOwnsPhoneComposer,
}),
);
const renderDegradedNotice = () => (
Expand Down Expand Up @@ -3362,10 +3373,9 @@ export function ClinicalDashboard({
differentialsCompareAddonActive ? differentialsMobileCompareAddonSlotId : undefined
}
desktopHomeComposerSlotId={desktopHomeComposerSlotId}
// Only the answer home ("How can I help?") keeps the in-flow hero
// pill + privacy notice on phones; every other mode home docks the
// compact pill to the bottom edge below sm.
heroComposerBreakpoint={showAnswerHome ? "all" : "sm-up"}
// Mode homes keep the composer in the centred hero at every breakpoint,
// sharing the phone/tablet structure instead of switching to a bottom dock.
heroComposerBreakpoint={heroOwnsPhoneComposer ? "all" : "sm-up"}
Comment thread
BigSimmo marked this conversation as resolved.
// Answer view: the header overlays the scrolling <main> at every width
// (main reserves matching top padding) so content frosts under the
// glass bar, and it slides away/returns with scroll direction. Other
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,15 @@ export function resolveDashboardVisibleMobileComposerReserve(input: {
searchMode: string;
hasAnswerFollowUps: boolean;
differentialsCompareAddonActive: boolean;
/** Hero owns the phone composer (no fixed bottom dock) — match shell idle pad. */
heroOwnsPhoneComposer?: boolean;
}): string {
// Mode homes / answer home keep the in-flow hero pill on phones, so there is
// no floating dock to clear — only the idle content pad (same as standalone
// shell mode homes). Using the dock reserve here opens a blank bottom band.
if (input.heroOwnsPhoneComposer) {
return mobileComposerIdleReserve;
}
if (input.searchMode === "answer") {
return input.hasAnswerFollowUps
? mobileComposerVisibleReserve.dashboardAnswerWithFollowUps
Expand Down
8 changes: 5 additions & 3 deletions src/components/mode-home-template.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -261,7 +261,7 @@ export function ModeHomeTemplate({
{actions?.length ? (
<section
aria-label={actionsLabel}
className="grid w-full max-w-none overflow-hidden rounded-none border-y border-[color:var(--border)] bg-[color:var(--surface)] shadow-[var(--shadow-card)] sm:max-w-none sm:grid-cols-[repeat(auto-fit,minmax(15rem,1fr))] sm:gap-3 sm:overflow-visible sm:rounded-xl sm:border sm:shadow-[var(--shadow-card)] lg:rounded-none lg:border-0 lg:bg-transparent lg:shadow-none"
className="grid w-full max-w-none overflow-hidden rounded-none border-y border-[color:var(--border)] bg-[color:var(--surface)] shadow-[var(--shadow-card)] lg:max-w-none lg:grid-cols-[repeat(auto-fit,minmax(15rem,1fr))] lg:gap-3 lg:overflow-visible lg:rounded-none lg:border-0 lg:bg-transparent lg:shadow-none"
>
{actions.map((action, index) => {
const ActionIcon = action.icon;
Expand All @@ -285,8 +285,10 @@ export function ModeHomeTemplate({
</>
);
const actionClassName = cn(
"mode-home-action group grid min-h-[4.4rem] w-full grid-cols-[2.5rem_minmax(0,1fr)_1.25rem] items-center gap-3 bg-[color:var(--surface)] px-4 py-3 text-left transition hover:bg-[color:var(--surface-subtle)] focus-visible:relative focus-visible:z-10 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-[-2px] focus-visible:outline-[color:var(--focus)] disabled:cursor-wait disabled:opacity-60 sm:min-h-[4.75rem] sm:grid-cols-[2.75rem_minmax(0,1fr)_1rem] sm:gap-3 sm:rounded-lg sm:border sm:border-[color:var(--border)] sm:px-4 sm:py-3.5 sm:shadow-[var(--shadow-card)] lg:min-h-[4.75rem] lg:px-5",
index > 0 && "border-t border-[color:var(--border)] sm:border-t-[color:var(--border)]",
"mode-home-action group grid min-h-[4.4rem] w-full grid-cols-[2.5rem_minmax(0,1fr)_1.25rem] items-center gap-3 bg-[color:var(--surface)] px-4 py-3 text-left transition hover:bg-[color:var(--surface-subtle)] focus-visible:relative focus-visible:z-10 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-[-2px] focus-visible:outline-[color:var(--focus)] disabled:cursor-wait disabled:opacity-60 sm:min-h-[4.4rem] sm:grid-cols-[2.5rem_minmax(0,1fr)_1.25rem] sm:px-4 sm:py-3 lg:min-h-[4.75rem] lg:grid-cols-[2.75rem_minmax(0,1fr)_1rem] lg:gap-3 lg:rounded-lg lg:border lg:border-[color:var(--border)] lg:px-5 lg:py-3.5 lg:shadow-[var(--shadow-card)]",
// Phone stack separator only — at lg each action is its own card
// with `lg:border`, so do not zero the card's top edge.
index > 0 && "max-lg:border-t max-lg:border-[color:var(--border)]",
);

if (action.href) {
Expand Down
69 changes: 68 additions & 1 deletion tests/mobile-composer-reserve.test.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
import { readFileSync } from "node:fs";
import { resolve } from "node:path";

import { describe, expect, it } from "vitest";

import {
Expand All @@ -12,6 +15,10 @@ import {
resolveShellVisibleMobileComposerReserve,
} from "@/components/clinical-dashboard/mobile-composer-reserve";

function source(relativePath: string): string {
return readFileSync(resolve(process.cwd(), relativePath), "utf8");
}

describe("mobile composer reserve contract", () => {
it("collapses to zero hidden pad without Safari toolbar safe-area", () => {
expect(mobileComposerHiddenReserve).toBe("0rem");
Expand Down Expand Up @@ -54,7 +61,7 @@ describe("mobile composer reserve contract", () => {
).toBe(mobileComposerIdleReserve);
});

it("uses the compact dock reserve for every non-answer dashboard dock (mode homes included)", () => {
it("uses the compact dock reserve for non-answer dashboard docks when the hero does not own phones", () => {
for (const searchMode of ["documents", "services", "forms", "tools", "favourites"]) {
expect(
resolveDashboardVisibleMobileComposerReserve({
Expand All @@ -66,6 +73,66 @@ describe("mobile composer reserve contract", () => {
}
});

it("keeps only the idle content pad when the dashboard hero owns the phone composer", () => {
expect(
resolveDashboardVisibleMobileComposerReserve({
searchMode: "documents",
hasAnswerFollowUps: false,
differentialsCompareAddonActive: false,
heroOwnsPhoneComposer: true,
}),
).toBe(mobileComposerIdleReserve);
expect(
resolveDashboardVisibleMobileComposerReserve({
searchMode: "tools",
hasAnswerFollowUps: false,
differentialsCompareAddonActive: false,
heroOwnsPhoneComposer: true,
}),
).toBe(mobileComposerIdleReserve);
expect(
resolveDashboardVisibleMobileComposerReserve({
searchMode: "answer",
hasAnswerFollowUps: false,
differentialsCompareAddonActive: false,
heroOwnsPhoneComposer: true,
}),
).toBe(mobileComposerIdleReserve);
});

it("derives hero phone ownership from the mounted hero slot, not answer-home alone", () => {
// Answer-home + !canRunSearch keeps showAnswerHome true while the hero slot
// is unset (showDesktopHomeComposer requires !error). Ownership must follow
// the slot so the dock reserve stays and the fixed composer cannot cover the
// setup/error message.
const dashboard = source("src/components/ClinicalDashboard.tsx");
const header = source("src/components/clinical-dashboard/master-search-header.tsx");
expect(dashboard).toContain('(activeModeResultKind === "favourites" && favouritesAccessible)');
expect(dashboard).toContain("const heroOwnsPhoneComposer = Boolean(desktopHomeComposerSlotId);");
expect(dashboard).not.toContain("const heroOwnsPhoneComposer = showDesktopHomeComposer || showAnswerHome;");
// Prescribing leaves MedicationHome as soon as the draft query is non-empty;
// keep the hero slot (and idle phone reserve) only while that home mounts.
expect(dashboard).toMatch(
/searchMode === "prescribing" &&\s*activeModeResultKind === "documents" &&\s*!modeSearchSubmitted &&\s*!query\.trim\(\)/,
);
// DifferentialsHome shows results (no mode-home slot) when a draft query
// coincides with stale evidence matches after clearing a submitted search.
expect(dashboard).toMatch(
/activeModeResultKind === "differentials" &&\s*!modeSearchSubmitted &&\s*!\(query\.trim\(\) && documentMatches\.length > 0\)/,
);
expect(header).toContain(
'const heroComposerOwnsPhones = Boolean(desktopHomeComposerSlotId) && heroComposerBreakpoint === "all";',
);
expect(
resolveDashboardVisibleMobileComposerReserve({
searchMode: "answer",
hasAnswerFollowUps: false,
differentialsCompareAddonActive: false,
heroOwnsPhoneComposer: false,
}),
).toBe(mobileComposerVisibleReserve.dashboardAnswer);
});

it("keeps the answer dock reserve compact, growing only for the follow-up chip row", () => {
expect(
resolveDashboardVisibleMobileComposerReserve({
Expand Down
2 changes: 1 addition & 1 deletion tests/therapy-compass-responsive-contract.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,7 @@ describe("Therapy Compass responsive contract", () => {
expect(responsiveStackCount(therapyCardSource)).toBeGreaterThanOrEqual(2);
expect(homeSource).toContain("ModeHomeMain");
expect(homeSource).toContain("ModeHomeTemplate");
expect(modeHomeTemplateSource).toContain("sm:grid-cols-[repeat(auto-fit,minmax(15rem,1fr))]");
expect(modeHomeTemplateSource).toContain("lg:grid-cols-[repeat(auto-fit,minmax(15rem,1fr))]");
expect(modeHomeTemplateSource).toContain("sm:flex-wrap");
expect(homeSource).toContain("desktopComposerSlotId={modeHomeDesktopComposerSlotId}");
expect(homeSource).toContain("ModeHomeVerificationFooter");
Expand Down
8 changes: 5 additions & 3 deletions tests/ui-smoke.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2965,9 +2965,11 @@ test.describe("Clinical KB UI smoke coverage", () => {
expect(startHereBox).not.toBeNull();
expect(documentsHeadingBox).not.toBeNull();
expect((documentsHeadingBox?.y ?? 0) + (documentsHeadingBox?.height ?? 0)).toBeLessThan(searchInputBox?.y ?? 0);
// Phones dock the compact composer at the bottom edge, below the hero content.
expect(searchInputBox?.y ?? 0).toBeGreaterThan(startHereBox?.y ?? 0);
await expect(page.locator('form.answer-footer-search-dock[data-footer-variant="compact"]')).toHaveCount(1);
// Phones keep the compact composer in the mode-home hero (above Start here),
// matching every other mode home — no fixed bottom dock on the empty home.
expect(searchInputBox?.y ?? 0).toBeLessThan(startHereBox?.y ?? 0);
await expect(page.locator('form.answer-footer-search-dock[data-footer-variant="compact"]')).toHaveCount(0);
await expect(page.locator(".mode-home-composer-slot").getByTestId("global-search-input")).toHaveCount(1);
const recentDocumentsButton = page.getByRole("button", { name: /Recent documents/i }).first();
const browseLibraryButton = page.getByRole("button", { name: /Browse library/i }).first();
const sourcePdfButton = page.getByRole("button", { name: /Open a source PDF/i }).first();
Expand Down
5 changes: 3 additions & 2 deletions tests/ui-tools.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -386,8 +386,9 @@ test.describe("Clinical KB tools launcher", () => {
await expect(page.getByLabel("Mode Tools")).toBeVisible();
await expect(visibleGlobalSearchInput(page)).toHaveCount(1);
if (viewport.name === "mobile") {
// Phones dock the compact shared search at the bottom edge.
await expect(page.locator("form.answer-footer-search-dock").getByTestId("global-search-input")).toBeVisible();
// Phones keep the compact shared search in the tools-home hero slot.
await expect(page.getByTestId("tools-home").getByTestId("global-search-input")).toBeVisible();
await expect(page.locator("form.answer-footer-search-dock")).toHaveCount(0);
} else {
await expect(page.getByTestId("tools-home").getByTestId("global-search-input")).toBeVisible();
}
Expand Down