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
23 changes: 12 additions & 11 deletions docs/branch-review-ledger.md

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
Expand Up @@ -861,6 +861,7 @@ export function MasterSearchHeader({

function closeModeSurfaces() {
setActionMenuOpen(false);
setCommandDropdownOpen(false);
closeScope(false);
setScopeSheetOpen(false);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1000,7 +1000,15 @@ export function UniversalSearchCommandSurface({
handleComposerKeyDown(event as unknown as ReactKeyboardEvent<HTMLInputElement>);
}
}}
onFocusCapture={() => {
onFocusCapture={(event) => {
// Only the search input opens the command panel. Focus restore onto the
// integrated + / pins trigger (e.g. after Escape closes Scope) must not
// reopen the listbox — that overlay covers mode-home actions such as
// Browse library and breaks the deferred-request critical journey.
const target = event.target;
if (!(target instanceof HTMLElement) || target.dataset.testid !== "global-search-input") {
return;
}
// 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.
Expand Down
67 changes: 41 additions & 26 deletions src/components/patient-safety-plan.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -593,32 +593,47 @@ export function PatientSafetyPlan() {
"focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-[-2px] focus-visible:outline-[color:var(--focus)]",
)}
>
{/* Tool header */}
<header className="border-b border-[color:var(--border)] bg-[color:var(--surface)]">
<div className="mx-auto grid max-w-7xl gap-4 px-4 py-4 sm:px-6 lg:grid-cols-[minmax(0,1fr)_auto] lg:items-center lg:px-8">
<div className="grid grid-cols-[auto_auto_minmax(0,1fr)] items-start gap-3">
<NavigationBackButton
className="size-tap"
fallbackHref={appModeHomeHref("tools")}
onBeforeNavigate={() => {
if (!isDirty) return true;
return window.confirm(
"Leave this safety plan? Your entries are only in this browser tab and will be lost.",
);
}}
/>
<span className="grid size-tap shrink-0 place-items-center rounded-2xl border border-[color:var(--clinical-accent-border)] bg-[color:var(--clinical-accent-soft)] text-[color:var(--clinical-accent)] shadow-[var(--shadow-inset)]">
<ShieldCheck className="size-icon-lg" aria-hidden="true" />
</span>
<div className="min-w-0">
<p className={eyebrowText}>Clinical KB · Clinical tool</p>
<h1 className="mt-0.5 text-2xl-minus font-extrabold leading-tight text-[color:var(--text-heading)]">
Safety plan generator
</h1>
<p className="mt-1 max-w-xl text-sm-minus font-medium leading-5 text-[color:var(--text-muted)]">
Build an identifier-free safety plan <em>with</em> your patient — the six prioritised steps — then
export it through your approved clinical workflow.
</p>
{/*
Safety plan sits outside the search shell, so this header owns the OS top
inset via max(safe-area-top) — same contract as /privacy and colour-coding.
Avoid axis py-* here so it cannot fight the side-specific pt-* pad.
*/}
<header
data-testid="safety-plan-tool-header"
className="border-b border-[color:var(--border)] bg-[color:var(--surface)] pt-[max(0.75rem,var(--safe-area-top))] sm:pt-[max(1.25rem,var(--safe-area-top))]"
>
<div className="mx-auto grid max-w-7xl gap-4 px-4 pb-4 pt-3 sm:px-6 lg:grid-cols-[minmax(0,1fr)_auto] lg:items-center lg:px-8">
{/*
Phone: back on its own min-h-tap row, then shield + title.
lg+: lg:contents folds back/shield/title into one three-column row.
*/}
<div className="grid gap-3 lg:grid-cols-[auto_auto_minmax(0,1fr)] lg:items-start lg:gap-3">
<div className="flex min-h-tap items-center lg:contents">
<NavigationBackButton
className="size-tap"
fallbackHref={appModeHomeHref("tools")}
onBeforeNavigate={() => {
if (!isDirty) return true;
return window.confirm(
"Leave this safety plan? Your entries are only in this browser tab and will be lost.",
);
}}
/>
</div>
<div className="grid grid-cols-[auto_minmax(0,1fr)] items-start gap-3 lg:contents">
<span className="grid size-tap shrink-0 place-items-center rounded-2xl border border-[color:var(--clinical-accent-border)] bg-[color:var(--clinical-accent-soft)] text-[color:var(--clinical-accent)] shadow-[var(--shadow-inset)]">
<ShieldCheck className="size-icon-lg" aria-hidden="true" />
</span>
<div className="min-w-0">
<p className={eyebrowText}>Clinical KB · Clinical tool</p>
<h1 className="mt-0.5 text-2xl-minus font-extrabold leading-tight text-[color:var(--text-heading)]">
Safety plan generator
</h1>
<p className="mt-1 max-w-xl text-sm-minus font-medium leading-5 text-[color:var(--text-muted)]">
Build an identifier-free safety plan <em>with</em> your patient — the six prioritised steps — then
export it through your approved clinical workflow.
</p>
</div>
</div>
</div>

Expand Down
14 changes: 14 additions & 0 deletions tests/patient-safety-plan.dom.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,20 @@ const exampleExportPattern =
/\*\*\* EXAMPLE — SAMPLE SAFETY PLAN WITH NON-WORKING NUMBERS, NOT FOR PATIENT HANDOVER \*\*\*/;

describe("PatientSafetyPlan — incomplete-plan draft guard", () => {
it("owns the OS top inset outside the search shell", () => {
// /safety-plan is a standalone tool route (no chrome-safe-area-top host),
// so the tool header must bake max(safe-area-top) into its top pad.
const { container } = render(<PatientSafetyPlan />);
const header = container.querySelector('[data-testid="safety-plan-tool-header"]');
expect(header).toBeTruthy();
const className = header?.getAttribute("class") ?? "";
expect(className).toContain("pt-[max(0.75rem,var(--safe-area-top))]");
expect(className).toContain("sm:pt-[max(1.25rem,var(--safe-area-top))]");
expect(className).not.toMatch(/(?:^|\s)py-\S+/);
Comment thread
cursor[bot] marked this conversation as resolved.
expect(className).not.toMatch(/(?:^|\s)sm:py-\S+/);
expect(screen.getByRole("heading", { name: /Safety plan generator/i })).toBeTruthy();
});

it("flags the patient copy as a draft until every step is complete", async () => {
render(<PatientSafetyPlan />);

Expand Down
5 changes: 5 additions & 0 deletions tests/ui-smoke.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3892,8 +3892,13 @@ test.describe("Clinical KB UI smoke coverage", () => {
expect(requestCounts.batches).toBe(0);
expect(requestCounts.quality).toBe(0);
await page.keyboard.press("Escape");
await expect(page.getByTestId("scope-command-popover")).toHaveCount(0);
// Scope restore can land on the composer + trigger; the command listbox must
// stay closed so it cannot cover Start-here actions (Browse library).
await expect(page.getByRole("listbox", { name: /search suggestions/i })).toHaveCount(0);

await switchToDocumentSearchMode(page);
await expect(page.getByRole("listbox", { name: /search suggestions/i })).toHaveCount(0);
await page
.getByRole("button", { name: /Browse library/i })
.first()
Expand Down
Loading