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
60 changes: 60 additions & 0 deletions src/app/globals.css
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
Expand Down
2 changes: 1 addition & 1 deletion src/components/clinical-dashboard/answer-status.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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}
/>
)}
Expand Down
7 changes: 7 additions & 0 deletions src/lib/differentials.ts
Original file line number Diff line number Diff line change
Expand Up @@ -324,6 +324,13 @@ export function resolveDifferentialCompareHandoff(ids: Iterable<string>, query =
export function resolveDifferentialCompareLaunchHref(ids: Iterable<string>, 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);
Expand Down
7 changes: 7 additions & 0 deletions tests/differentials-navigation.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
43 changes: 42 additions & 1 deletion tests/helpers/style-contracts.ts
Original file line number Diff line number Diff line change
@@ -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).
*
Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -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<Record<string, string>>;
/** A computed colour that must resolve to the named CSS custom-property token. */
Expand All @@ -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`.
*
Expand Down Expand Up @@ -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" },
},
];

/**
Expand Down Expand Up @@ -305,7 +347,6 @@ export const STYLE_CONTRACT_EXEMPTIONS: Readonly<Record<string, string>> = {
// 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)",
Expand Down
16 changes: 16 additions & 0 deletions tests/ui-smoke.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 }) => {
Expand Down
11 changes: 11 additions & 0 deletions tests/ui-style-contract.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down
Loading