diff --git a/src/app/globals.css b/src/app/globals.css index 6c1ffb27c..2a884ad9e 100644 --- a/src/app/globals.css +++ b/src/app/globals.css @@ -2085,6 +2085,66 @@ summary::-webkit-details-marker { white-space: nowrap; } +/* Home history is a quiet convenience rather than a second set of primary + actions. Keep the shared chip behaviour and touch target, but use a compact + rectangular treatment that takes up less visual and vertical space. */ +.home-recent-searches { + gap: calc(var(--spacing-icon-xs) / 2) var(--radius-md); + padding-inline: 0; +} + +.home-recent-searches .answer-suggestion-label { + color: var(--text-heading); + font-size: var(--text-3xs); + font-weight: 650; + letter-spacing: 0.025em; +} + +.home-recent-searches .answer-suggestion-chips { + justify-content: center; + gap: calc(var(--spacing-icon-xs) / 2); +} + +.home-recent-searches .answer-suggestion-chip { + min-height: calc(var(--spacing-tap) * 7 / 12); + border-color: color-mix(in srgb, var(--border) 58%, transparent); + border-radius: var(--radius-md); + background: color-mix(in srgb, var(--surface-inset) 42%, transparent); + box-shadow: none; + padding-inline: var(--radius-md); + color: color-mix(in srgb, var(--text-muted) 90%, transparent); + font-size: var(--text-2xs); + font-weight: 550; +} + +.home-recent-searches .answer-suggestion-chip-icon { + width: var(--text-2xs); + height: var(--text-2xs); + margin-right: var(--radius-xs); +} + +.home-recent-searches .answer-suggestion-chip:hover:not(:disabled) { + border-color: color-mix(in srgb, var(--clinical-accent-border) 68%, transparent); + background: color-mix(in srgb, var(--clinical-accent-soft) 52%, transparent); +} + +@media (pointer: coarse) { + .home-recent-searches .answer-suggestion-chip { + min-height: var(--spacing-tap); + } +} + +@media (max-width: 639px) { + .home-recent-searches { + align-items: flex-start; + flex-direction: column; + } + + .home-recent-searches .answer-suggestion-chips { + justify-content: flex-start; + } +} + .answer-footer-search-edge .answer-suggestion-row { padding-inline: 0.25rem; } diff --git a/src/components/clinical-dashboard/answer-status.tsx b/src/components/clinical-dashboard/answer-status.tsx index aa08a073b..d9d4e3556 100644 --- a/src/components/clinical-dashboard/answer-status.tsx +++ b/src/components/clinical-dashboard/answer-status.tsx @@ -90,7 +90,7 @@ export function SharedHomeEmptyState({ onPick={(entry) => onSelectRecent?.(entry)} label={sharedHomeEmptyState.recentLabel} layout="wrap" - className="justify-center" + className="home-recent-searches justify-center" icon={History} /> )} diff --git a/src/lib/differentials.ts b/src/lib/differentials.ts index cb7216258..5717f0869 100644 --- a/src/lib/differentials.ts +++ b/src/lib/differentials.ts @@ -324,6 +324,13 @@ export function resolveDifferentialCompareHandoff(ids: Iterable, query = export function resolveDifferentialCompareLaunchHref(ids: Iterable, query = ""): string { const handoff = resolveDifferentialCompareHandoff(ids, query); if (handoff.kind === "presentation") return handoff.href; + if (handoff.selection.diagnosisIds.length === 1) { + return differentialCompareHref( + "/differentials/presentations/acute-confusion-encephalopathy", + query, + handoff.selection.diagnosisIds, + ); + } const params = new URLSearchParams(); const trimmedQuery = query.trim(); if (trimmedQuery) params.set("q", trimmedQuery); diff --git a/tests/differentials-navigation.test.ts b/tests/differentials-navigation.test.ts index c935317e4..f6f4ee0aa 100644 --- a/tests/differentials-navigation.test.ts +++ b/tests/differentials-navigation.test.ts @@ -104,6 +104,13 @@ describe("differentials navigation", () => { expect(href).toContain("ids="); }); + it("launches single-diagnosis compare into the default presentation workflow", () => { + const href = resolveDifferentialCompareLaunchHref(["wernicke-encephalopathy"], "Pain"); + expect(href).toMatch(/^\/differentials\/presentations\//); + expect(href).toContain("acute-confusion-encephalopathy"); + expect(href).toContain("ids=wernicke-encephalopathy"); + }); + it("builds compare queue titles from known diagnosis slugs only", () => { const items = differentialCompareQueueItems([ "delirium", diff --git a/tests/helpers/style-contracts.ts b/tests/helpers/style-contracts.ts index d24a23946..62210f00d 100644 --- a/tests/helpers/style-contracts.ts +++ b/tests/helpers/style-contracts.ts @@ -1,3 +1,6 @@ +import { guestAnswerThreadOwnerId } from "@/lib/answer-thread-storage"; +import { demoRecentQueryOwnerId, recentQueryStorageKey } from "@/lib/recent-query-storage"; + /** * The cascade-layer inertness registry (ledger #094). * @@ -55,6 +58,13 @@ export type UnlayeredVisualClass = { readonly unmediated: boolean; }; +type StyleContractSessionBootstrap = { + readonly sessionStorage?: ReadonlyArray<{ + readonly key: string; + readonly value: readonly string[]; + }>; +}; + /** * Class rules in `css` that sit outside every `@layer` block and set a visual * property. @@ -164,6 +174,7 @@ export type StyleEffectContract = { readonly route: string; /** Playwright selector for an element carrying `className`. */ readonly selector: string; + readonly bootstrap?: StyleContractSessionBootstrap; /** Computed values that must match exactly. */ readonly computed: Readonly>; /** A computed colour that must resolve to the named CSS custom-property token. */ @@ -180,6 +191,15 @@ export type StyleEffectContract = { readonly nonInert?: readonly string[]; }; +const HOME_RECENT_QUERIES = ["clozapine monitoring schedule"] as const; +const HOME_RECENT_QUERY_STORAGE_KEYS = [ + `${recentQueryStorageKey}:${guestAnswerThreadOwnerId}`, + `${recentQueryStorageKey}:${demoRecentQueryOwnerId}`, +] as const; +const homeRecentQueriesContractBootstrap: StyleContractSessionBootstrap = { + sessionStorage: HOME_RECENT_QUERY_STORAGE_KEYS.map((key) => ({ key, value: HOME_RECENT_QUERIES })), +}; + /** * Rendered-effect contracts, asserted in Chromium by `tests/ui-style-contract.spec.ts`. * @@ -260,6 +280,28 @@ export const STYLE_EFFECT_CONTRACTS: readonly StyleEffectContract[] = [ nonInert: ["color"], colorToken: { property: "color", token: "--text-heading" }, }, + { + className: "home-recent-searches", + description: "home recent-searches surface keeps compact desktop gap and phone-first mobile column flow", + route: "/", + selector: '[data-testid="shared-home-recent-queries"]', + bootstrap: homeRecentQueriesContractBootstrap, + computed: { + rowGap: "6px", + columnGap: "10px", + display: "flex", + }, + }, + { + className: "answer-suggestion-label", + description: "home recent-search labels use readable text contrast when nested under compact rail", + route: "/", + selector: '[data-testid="shared-home-recent-queries"] .answer-suggestion-label', + bootstrap: homeRecentQueriesContractBootstrap, + computed: {}, + nonInert: ["color"], + colorToken: { property: "color", token: "--text-heading" }, + }, ]; /** @@ -305,7 +347,6 @@ export const STYLE_CONTRACT_EXEMPTIONS: Readonly> = { // Answer suggestions and smart search. "answer-suggestion-chip": "answer suggestion rail — no effect contract yet (#094)", "answer-suggestion-chip-icon": "answer suggestion rail — no effect contract yet (#094)", - "answer-suggestion-label": "answer suggestion rail — no effect contract yet (#094)", "smart-search-rotating-query": "rotating placeholder — reduced-motion behaviour covered by ui-accessibility", "smart-search-rotating-text": "rotating placeholder — reduced-motion behaviour covered by ui-accessibility", "smart-search-phone-ticker": "smart-search ticker title line — no contract yet (#094)", diff --git a/tests/ui-smoke.spec.ts b/tests/ui-smoke.spec.ts index 928bca9b6..16a5d8a4c 100644 --- a/tests/ui-smoke.spec.ts +++ b/tests/ui-smoke.spec.ts @@ -2617,6 +2617,22 @@ test.describe("Clinical KB UI smoke coverage", () => { await expect(page.getByTestId("plain-answer-response")).toBeVisible(); expect(answerRequests).toContain(recent); await expectNoPageHorizontalOverflow(page); + + await page.setViewportSize({ width: 390, height: 844 }); + const newChat = page.getByRole("button", { name: /new chat|new comparison/i }); + await expect(newChat).toBeVisible(); + await newChat.click(); + await waitForDemoDashboardReady(page); + + const homeRecentSearches = page.getByTestId("shared-home-recent-queries"); + await homeRecentSearches.scrollIntoViewIfNeeded(); + await expect(homeRecentSearches).toBeVisible(); + const homeRecentDirection = await homeRecentSearches.evaluate((node) => getComputedStyle(node).flexDirection); + expect(homeRecentDirection, "home recent-searches should stack on phone width").toBe("column"); + + const chipsGroup = homeRecentSearches.locator(".answer-suggestion-chips"); + const mobileJustify = await chipsGroup.evaluate((node) => getComputedStyle(node).justifyContent); + expect(mobileJustify, "phone home recent-search chips should align to flex-start").toBe("flex-start"); }); test("legacy unscoped recent-query storage is purged and never displayed @critical", async ({ page }) => { diff --git a/tests/ui-style-contract.spec.ts b/tests/ui-style-contract.spec.ts index 6e8dd973f..a09281802 100644 --- a/tests/ui-style-contract.spec.ts +++ b/tests/ui-style-contract.spec.ts @@ -23,6 +23,17 @@ test.describe("unlayered style rules render their effect", () => { for (const contract of STYLE_EFFECT_CONTRACTS) { test(contract.description, async ({ page }) => { + if (contract.bootstrap?.sessionStorage?.length) { + await page.addInitScript( + ({ sessionStorage }) => { + for (const item of sessionStorage ?? []) { + window.sessionStorage.setItem(item.key, JSON.stringify(item.value)); + } + }, + { sessionStorage: contract.bootstrap?.sessionStorage }, + ); + } + await page.goto(contract.route, { waitUntil: "domcontentloaded" }); const target = page.locator(contract.selector).first();