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
11 changes: 11 additions & 0 deletions src/app/globals.css
Original file line number Diff line number Diff line change
Expand Up @@ -1793,6 +1793,17 @@ summary::-webkit-details-marker {
box-shadow: 0 0 0 4px color-mix(in srgb, var(--focus) 25%, transparent) !important;
}

/* Underline-tab focus: an inset rounded ring that hugs the label instead of the
default full-height box, which collides with the tablist border. */
.focus-ring-tab {
border-radius: var(--radius-md);
}
.focus-ring-tab:focus-visible {
outline: 2px solid var(--focus) !important;
outline-offset: -4px !important;
box-shadow: none !important;
}

/* Premium hover transitions for source capsules */
.source-capsule-hover {
transition:
Expand Down
12 changes: 10 additions & 2 deletions src/components/DocumentTagCloud.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,15 @@ function confidenceTitle(tag: SmartDocumentTag) {
return `${tag.group}: ${tag.source} tag, ${Math.round(tag.confidence * 100)}% confidence`;
}

/**
* Renders a document tag with group-specific styling and optional search interaction.
*
* @param tag - The document tag to display.
* @param compact - Whether to use compact sizing.
* @param selected - Whether to indicate the tag as selected.
* @param onTagClick - Optional callback invoked when the tag is clicked.
* @returns The rendered document tag chip.
*/
function DocumentTagChip({
tag,
compact,
Expand All @@ -79,8 +88,7 @@ function DocumentTagChip({
groupTone[tag.group],
tag.queryMatched && "ring-2 ring-[color:var(--focus)]/25",
selected && "ring-2 ring-[color:var(--primary)]/35",
onTagClick &&
"cursor-pointer transition hover:-translate-y-0.5 hover:border-[color:var(--border-strong)] focus:outline-none focus:ring-2 focus:ring-[color:var(--focus)]/40",
onTagClick && "cursor-pointer transition hover:-translate-y-0.5 hover:border-[color:var(--border-strong)]",
);
const content = (
<>
Expand Down
10 changes: 8 additions & 2 deletions src/components/applications-launcher-page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -322,6 +322,12 @@ function QuickActions({ onSelect, mobile }: { onSelect: (id: string) => void; mo
);
}

/**
* Renders responsive tabs for selecting a tool category.
*
* @param activeFilter - The currently selected tool category.
* @param onFilterChange - Called with the selected category when a tab is activated.
*/
function FilterTabs({
activeFilter,
onFilterChange,
Expand All @@ -344,7 +350,7 @@ function FilterTabs({
aria-controls="launcher-results-panel"
onClick={() => onFilterChange(filter.id)}
className={cn(
"inline-flex min-h-9 items-center justify-center rounded-lg border px-4 text-xs font-bold transition",
"inline-flex min-h-11 items-center justify-center whitespace-nowrap rounded-lg border px-4 text-xs font-bold transition",
active
? "border-[color:var(--clinical-accent)] bg-[color:var(--clinical-accent)] text-[color:var(--clinical-accent-contrast)] shadow-[var(--shadow-tight)]"
: "border-[color:var(--border)] bg-[color:var(--surface-lux)] text-[color:var(--text-muted)] hover:bg-[color:var(--surface-subtle)] hover:text-[color:var(--text-heading)]",
Expand All @@ -369,7 +375,7 @@ function FilterTabs({
aria-controls="launcher-results-panel"
onClick={() => onFilterChange(filter.id)}
className={cn(
"inline-flex min-h-7 shrink-0 items-center justify-center gap-0.5 rounded-lg border px-2 text-4xs font-bold transition",
"inline-flex min-h-11 shrink-0 items-center justify-center gap-0.5 whitespace-nowrap rounded-lg border px-2 text-4xs font-bold transition",
active
? "border-[color:var(--clinical-accent)] bg-[color:var(--clinical-accent)] text-[color:var(--clinical-accent-contrast)] shadow-[var(--shadow-tight)]"
: "border-[color:var(--border)] bg-[color:var(--surface-lux)] text-[color:var(--text-muted)]",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -362,6 +362,11 @@ function ContinueStrip({ item, onSelect }: { item: FavouriteItem; onSelect: (id:
);
}

/**
* Renders a menu of actions for a favourite item.
*
* @param item - The favourite item associated with the available actions
*/
function RowActionsMenu({ item }: { item: FavouriteItem }) {
const [open, setOpen] = useState(false);
const buttonRef = useRef<HTMLButtonElement>(null);
Expand All @@ -386,7 +391,7 @@ function RowActionsMenu({ item }: { item: FavouriteItem }) {
aria-label={`More actions for ${item.title}`}
onClick={() => setOpen((current) => !current)}
className={cn(
"grid h-9 w-9 place-items-center rounded-lg text-[color:var(--text-muted)] hover:bg-[color:var(--surface-subtle)]",
"grid h-11 w-11 place-items-center rounded-lg text-[color:var(--text-muted)] hover:bg-[color:var(--surface-subtle)]",
focusRing,
)}
>
Expand Down
10 changes: 8 additions & 2 deletions src/components/clinical-dashboard/favourites-library-nav.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -210,7 +210,7 @@ function SidebarRow({ entry }: { entry: SidebarEntry }) {
aria-pressed={entry.active}
onClick={entry.onClick}
className={cn(
"flex min-h-9 w-full items-center gap-2.5 rounded-lg border px-2.5 text-left text-sm-minus font-semibold transition",
"flex min-h-11 w-full items-center gap-2.5 rounded-lg border px-2.5 text-left text-sm-minus font-semibold transition",
entry.active
? "border-[color:var(--clinical-accent-border)] bg-[color:var(--clinical-accent-soft)] text-[color:var(--clinical-accent)] shadow-[inset_2px_0_0_var(--clinical-accent)]"
: "border-transparent text-[color:var(--text-muted)] hover:bg-[color:var(--surface-subtle)] hover:text-[color:var(--text)]",
Expand All @@ -224,6 +224,12 @@ function SidebarRow({ entry }: { entry: SidebarEntry }) {
);
}

/**
* Renders the favourites library navigation sidebar in expanded or compact mode.
*
* @param collapsed - Whether to display the compact sidebar layout.
* @param onCollapsedChange - Called when the user requests a different sidebar layout.
*/
export function FavouritesSidebar({
collapsed,
onCollapsedChange,
Expand Down Expand Up @@ -302,7 +308,7 @@ export function FavouritesSidebar({
type="button"
onClick={() => onCollapsedChange(true)}
className={cn(
"grid h-9 w-9 shrink-0 place-items-center rounded-lg border border-transparent text-[color:var(--text-muted)] transition hover:border-[color:var(--border)] hover:bg-[color:var(--surface)] hover:text-[color:var(--text)]",
"grid h-11 w-11 shrink-0 place-items-center rounded-lg border border-transparent text-[color:var(--text-muted)] transition hover:border-[color:var(--border)] hover:bg-[color:var(--surface)] hover:text-[color:var(--text)]",
focusRing,
)}
aria-label="Compact library sidebar"
Expand Down
36 changes: 29 additions & 7 deletions src/components/differentials/differential-detail-page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -126,6 +126,14 @@ const sectionItemIconClass: Partial<Record<DifferentialSection["tone"], string>>
test: "text-[color:var(--info)]",
};

/**
* Renders section items according to the section tone.
*
* @param section - The section whose tone determines the item layout and styling
* @param items - The items to display
* @param overlapLinks - Maps overlap item labels to diagnosis slugs for linked items
* @returns The rendered section item list
*/
function SectionItems({
section,
items,
Expand Down Expand Up @@ -160,13 +168,13 @@ function SectionItems({
{slug ? (
<Link
href={`/differentials/diagnoses/${slug}`}
className="inline-flex min-h-9 items-center gap-1 rounded-md border border-[color:var(--clinical-accent-border)] bg-[color:var(--clinical-accent-soft)] px-2.5 text-xs font-bold text-[color:var(--clinical-accent)] hover:border-[color:var(--clinical-accent)]"
className="inline-flex min-h-11 items-center gap-1 rounded-md border border-[color:var(--clinical-accent-border)] bg-[color:var(--clinical-accent-soft)] px-2.5 text-xs font-bold text-[color:var(--clinical-accent)] hover:border-[color:var(--clinical-accent)]"
>
{item}
<ChevronRight className="h-3.5 w-3.5" aria-hidden />
</Link>
) : (
<span className="inline-flex min-h-9 items-center rounded-md border border-[color:var(--border)] bg-[color:var(--surface-subtle)] px-2.5 text-xs font-semibold text-[color:var(--text-muted)]">
<span className="inline-flex min-h-11 items-center rounded-md border border-[color:var(--border)] bg-[color:var(--surface-subtle)] px-2.5 text-xs font-semibold text-[color:var(--text-muted)]">
{item}
</span>
)}
Expand Down Expand Up @@ -702,6 +710,14 @@ function FooterStatus({
);
}

/**
* Renders desktop actions for comparing, copying, and saving a diagnosis.
*
* @param record - The diagnosis record whose content is copied.
* @param saved - Whether the diagnosis is currently saved.
* @param onToggleSaved - Called when the saved state is toggled.
* @param onCompare - Called when comparison is requested.
*/
function TopActions({
record,
saved,
Expand All @@ -714,11 +730,11 @@ function TopActions({
onCompare: () => void;
}) {
return (
<div className="hidden items-center gap-3 lg:flex">
<div className="hidden shrink-0 items-center gap-3 lg:flex">
<button
type="button"
onClick={onCompare}
className="inline-flex min-h-11 items-center gap-2 rounded-lg border border-[color:var(--border-lux)] bg-[color:var(--surface)] px-4 text-sm font-bold text-[color:var(--clinical-accent)] shadow-[var(--shadow-inset)] hover:bg-[color:var(--surface-subtle)]"
className="inline-flex min-h-11 items-center gap-2 whitespace-nowrap rounded-lg border border-[color:var(--border-lux)] bg-[color:var(--surface)] px-4 text-sm font-bold text-[color:var(--clinical-accent)] shadow-[var(--shadow-inset)] hover:bg-[color:var(--surface-subtle)]"
>
<GitCompareArrows className="h-4 w-4" aria-hidden />
Compare
Expand Down Expand Up @@ -833,6 +849,12 @@ const detailTabs: Array<{ id: DifferentialDetailTabId; label: string }> = [
{ id: "source", label: "Source" },
];

/**
* Renders keyboard-navigable tabs for the diagnosis detail sections.
*
* @param active - The currently selected tab.
* @param onChange - Called when the selected tab changes.
*/
function Tabs({
active,
onChange,
Expand Down Expand Up @@ -886,10 +908,10 @@ function Tabs({
tabIndex={isActive ? 0 : -1}
onClick={() => onChange(tab.id)}
className={cn(
"min-h-11 flex-1 whitespace-nowrap px-1 py-3 text-center text-xs sm:flex-none sm:px-4 sm:text-sm",
"focus-ring-tab min-h-11 flex-1 whitespace-nowrap border-b-2 px-1 py-3 text-center text-xs sm:flex-none sm:px-4 sm:text-sm",
isActive
? "border-b-2 border-[color:var(--clinical-accent)] text-[color:var(--clinical-accent)]"
: "hover:text-[color:var(--text-heading)]",
? "border-[color:var(--clinical-accent)] text-[color:var(--clinical-accent)]"
: "border-transparent hover:text-[color:var(--text-heading)]",
)}
>
{tab.label}
Expand Down
8 changes: 7 additions & 1 deletion src/components/services/service-detail-page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -264,6 +264,12 @@ function SummaryCard({ card }: { card: ServiceSummaryCard }) {
);
}

/**
* Displays referral information rows with their values and copy actions.
*
* @param rows - The referral information rows to display
* @param onCopy - Callback invoked with a row value and feedback label when copying is requested
*/
function ReferralTable({
rows,
onCopy,
Expand Down Expand Up @@ -315,7 +321,7 @@ function ReferralTable({
disabled={!hasText(row.value)}
onClick={() => onCopy(row.value, `${row.label} copied`)}
aria-label={`Copy ${row.label}`}
className="inline-grid h-10 w-10 place-items-center rounded-lg border border-[color:var(--border)] bg-[color:var(--surface-raised)] text-[color:var(--text-muted)] shadow-[var(--shadow-inset)] transition hover:border-[color:var(--clinical-accent-border)] hover:text-[color:var(--clinical-accent)] focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-[color:var(--focus)] disabled:cursor-not-allowed disabled:opacity-40"
className="inline-grid h-11 w-11 place-items-center rounded-lg border border-[color:var(--border)] bg-[color:var(--surface-raised)] text-[color:var(--text-muted)] shadow-[var(--shadow-inset)] transition hover:border-[color:var(--clinical-accent-border)] hover:text-[color:var(--clinical-accent)] focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-[color:var(--focus)] disabled:cursor-not-allowed disabled:opacity-40"
>
<Copy className="h-4 w-4" aria-hidden />
</button>
Expand Down
44 changes: 44 additions & 0 deletions tests/ui-tools.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1259,6 +1259,50 @@ test.describe("Clinical KB tools launcher", () => {
await expectNoPageHorizontalOverflow(page);
});

test("diagnosis detail actions stay tappable and tabs stay single-line", async ({ page }) => {
await page.setViewportSize({ width: 1024, height: 800 });
await gotoLauncher(page, "/differentials/diagnoses/delirium");
await expect(page.getByTestId("differential-detail-page")).toBeVisible();
// The desktop action cluster must keep its intrinsic width (shrink-0) so the
// icon action does not get crushed below the 44px tap standard.
await expectMinTouchTarget(page.getByRole("button", { name: "Save diagnosis" }));

// Tabs: no page overflow and single-line labels at the narrowest width.
await page.setViewportSize({ width: 320, height: 700 });
await expect(page.getByTestId("differential-detail-page")).toBeVisible();
await expectNoPageHorizontalOverflow(page);
const overviewTab = page.getByRole("tab", { name: "Overview" });
await expect(overviewTab).toBeVisible();
// Count rendered label lines from text-node rects (an icon rect would bridge
// two wrapped lines and mask a wrap); the tab label must stay on one line.
const overviewLineCount = await overviewTab.evaluate((element) => {
const walker = document.createTreeWalker(element, NodeFilter.SHOW_TEXT);
const rects: DOMRect[] = [];
while (walker.nextNode()) {
const node = walker.currentNode;
if (!(node.textContent ?? "").trim()) continue;
const range = document.createRange();
range.selectNodeContents(node);
for (const rect of range.getClientRects()) {
if (rect.height > 4 && rect.width > 1) rects.push(rect);
}
}
rects.sort((a, b) => a.top - b.top);
let lines = 0;
let lineBottom = Number.NEGATIVE_INFINITY;
for (const rect of rects) {
if (rect.top >= lineBottom - 4) {
lines += 1;
lineBottom = rect.bottom;
} else {
lineBottom = Math.max(lineBottom, rect.bottom);
}
}
return lines;
});
expect(overviewLineCount).toBe(1);
});

test("differentials presentation comparison page stays wired to differentials mode", async ({ page }) => {
await page.setViewportSize({ width: 1440, height: 920 });
const workflow = acuteConfusionPresentationWorkflow;
Expand Down