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
8 changes: 8 additions & 0 deletions src/app/globals.css
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,14 @@
--text-3xl-plus: 1.9rem;
--text-4xl-minus: 2.15rem;

/* Fluid display heading for the hero / mode-home titles (hybrid-fluid type:
the large display heading scales continuously; body / UI text stays on the
stepped scale above). The floor is deliberately 1.45rem so the phone
baseline (23.2px at ≤~393px) is unchanged, then it ramps to the 2.15rem cap
by ~1033px — no breakpoint jumps. Generates the `text-hero` utility.
clamp() font sizes are not flagged by scripts/check-type-scale.mjs. */
--text-hero: clamp(1.45rem, 1.02rem + 1.75vw, 2.15rem);

/* Font families: bind Tailwind's font-sans / font-mono to the loaded Geist
faces (variables set on <html> by next/font). font-mono is used for
clinical codes and IDs (guideline refs, chunk/page numbers, versions). */
Expand Down
1 change: 0 additions & 1 deletion src/components/applications-launcher-page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -754,7 +754,6 @@ export function ApplicationsLauncherWorkspace({
subtitle={copy.description}
icon={Grid2X2}
headingLevel={1}
compact
/>

{desktopComposerSlotId ? (
Expand Down
1 change: 0 additions & 1 deletion src/components/clinical-dashboard/favourites-hub.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -166,7 +166,6 @@ export function FavouritesHub({
subtitle="Notes, sources, medication pages, and clinical sets, ready to reuse."
icon={Heart}
headingLevel={headingLevel}
compact
/>

{desktopComposerSlotId ? (
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ import { medicationIdentityBadges, type MedicationRecord } from "@/lib/medicatio
import { medicationMatchesCommandScopes } from "@/lib/search-command-surface";
import { SEMANTIC_TONE_META } from "@/lib/semantic-tone";
import { isDeployedClinicalKb } from "@/lib/deployed-app";
import { cn, EmptyState } from "@/components/ui-primitives";
import { cn, EmptyState, pageContainer } from "@/components/ui-primitives";

type MedicationPrescribingWorkspaceProps = {
query: string;
Expand Down Expand Up @@ -421,7 +421,7 @@ function MedicationResults({
const activeFilterLabel = medicationResultFilters.find((filter) => filter.id === activeFilter)?.label ?? "filtered";

return (
<div className="mx-auto w-full max-w-7xl space-y-3 py-0 sm:py-2">
<div className={cn(pageContainer, "space-y-3 py-0 sm:py-2")}>
<div className="hidden lg:block">
<SearchResultsHeaderBand modeId="prescribing" query={query} matchCount={resultCount} />
</div>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ import {
type MedicationGovernance,
} from "@/lib/medication-badges";
import { medicationDetailTiles, type MedicationRecord, type MedicationSection } from "@/lib/medications";
import { cn } from "@/components/ui-primitives";
import { cn, pageContainer } from "@/components/ui-primitives";

const sectionIcons: Record<string, LucideIcon> = {
dose: CalendarDays,
Expand Down Expand Up @@ -176,7 +176,7 @@ function MedicationRecordDetail({
const activeSections = sectionsByTab[activeTab];

return (
<div className="mx-auto w-full max-w-7xl space-y-3 py-1 sm:py-2">
<div className={cn(pageContainer, "space-y-3 py-1 sm:py-2")}>
<div className="grid gap-4 lg:grid-cols-[minmax(0,1fr)_21rem]">
<div className="space-y-3.5">
<section className="scroll-mt-16 px-1 sm:px-0">
Expand Down
6 changes: 3 additions & 3 deletions src/components/differentials/differential-detail-page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ import {
import type { DifferentialRecordGovernance } from "@/components/clinical-dashboard/use-differential-catalog";
import { DiagnosisMapPanel } from "@/components/differentials/diagnosis-map-panel";
import { CopyAfterReviewButton } from "@/components/differentials/differential-presentation-actions";
import { cn, toneDanger, toneNeutral, toneWarning } from "@/components/ui-primitives";
import { cn, pageContainer, toneDanger, toneNeutral, toneWarning } from "@/components/ui-primitives";
import { appModeHomeHref } from "@/lib/app-modes";
import {
cleanDifferentialItem,
Expand Down Expand Up @@ -817,7 +817,7 @@ function IconForDiagnosis({ record }: { record: DifferentialRecord }) {
function HeaderChrome() {
return (
<header className="sticky top-0 z-30 border-b border-[color:var(--border)] bg-[color:var(--surface)] px-3 py-2 sm:px-6 lg:px-8">
<div className="mx-auto flex max-w-7xl items-center justify-between gap-3">
<div className={cn(pageContainer, "flex items-center justify-between gap-3")}>
<div className="flex items-center gap-3">
<Link
href="/differentials"
Expand Down Expand Up @@ -1004,7 +1004,7 @@ export function DifferentialDetailPage({
className="min-h-dvh bg-[color:var(--background)] pb-24 text-[color:var(--text)] lg:pb-6"
>
<HeaderChrome />
<div className="mx-auto grid w-full max-w-7xl gap-4 px-3 py-3 sm:px-6 sm:py-4 lg:gap-5 lg:px-8">
<div className={cn(pageContainer, "grid gap-4 px-3 py-3 sm:px-6 sm:py-4 lg:gap-5 lg:px-8")}>
<div className="flex flex-col gap-4 lg:flex-row lg:items-start lg:justify-between">
<div className="min-w-0">
<nav aria-label="Differential breadcrumbs" className="mb-3 flex items-center gap-2 text-xs font-semibold">
Expand Down
5 changes: 3 additions & 2 deletions src/components/forms/form-detail-page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ import {
codeText,
floatingControl,
metadataPill,
pageContainer,
primaryControl,
textMuted,
toneDanger,
Expand Down Expand Up @@ -493,7 +494,7 @@ export function FormDetailPage({ form }: { form: FormRecord }) {
data-testid="form-detail-page"
className="min-h-[calc(100dvh-4rem)] bg-[color:var(--background)] px-3 pb-[calc(8rem+env(safe-area-inset-bottom))] pt-4 text-[color:var(--text)] sm:px-5 sm:pb-10 sm:pt-6 lg:px-8"
>
<div className="mx-auto max-w-7xl">
<div className={pageContainer}>
{notice ? (
<div
role="status"
Expand Down Expand Up @@ -547,7 +548,7 @@ export function FormDetailPage({ form }: { form: FormRecord }) {
{code}
</div>
<div className="min-w-0">
<h1 className="max-w-4xl text-xl font-extrabold leading-[1.08] text-[color:var(--text-heading)] sm:text-4xl">
<h1 className="max-w-4xl text-3xl font-extrabold leading-[1.05] text-[color:var(--text-heading)] sm:text-4xl">
{form.title}
</h1>
<p className="mt-1.5 max-w-4xl text-xs font-medium leading-4 text-[color:var(--text-muted)] sm:mt-3 sm:text-base sm:leading-6">
Expand Down
3 changes: 2 additions & 1 deletion src/components/forms/forms-search-results-page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ import { useRegistryRecords, type RegistryRequestStatus } from "@/lib/use-regist
import {
cn,
codeText,
pageContainer,
searchFocusRing,
searchPageCanvas,
searchResultsSection,
Expand Down Expand Up @@ -626,7 +627,7 @@ function FormsSearchResultsPageContent({ query }: FormsSearchResultsPageProps) {

return (
<div className={cn("overflow-x-hidden", searchPageCanvas)}>
<main className="mx-auto grid w-full max-w-7xl gap-3 px-4 pt-3 sm:px-6 lg:gap-5 lg:px-8 lg:pb-8 lg:pt-6">
<main className={cn(pageContainer, "grid gap-3 px-4 pt-3 sm:px-6 lg:gap-5 lg:px-8 lg:pb-8 lg:pt-6")}>
<RegistryStatusNotice status={registry.status} />
{registryReady ? (
<>
Expand Down
8 changes: 4 additions & 4 deletions src/components/mode-home-page-skeleton.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,16 +12,16 @@ function SkeletonBlock({ className }: { className?: string }) {
export function ModeHomePageSkeleton() {
return (
<div
className="mx-auto grid w-full max-w-[64rem] justify-items-center gap-3.5 px-4 py-8 sm:gap-6"
className="mx-auto grid w-full max-w-[60rem] justify-items-center gap-3.5 px-4 py-8 sm:gap-6"
role="status"
aria-label="Loading"
>
<SkeletonBlock className="h-12 w-12 rounded-2xl sm:h-16 sm:w-16 lg:h-[4.75rem] lg:w-[4.75rem]" />
<SkeletonBlock className="h-11 w-11 rounded-2xl sm:h-12 sm:w-12 lg:h-14 lg:w-14" />
<div className="grid w-full justify-items-center gap-2">
<SkeletonBlock className="h-[1.7rem] w-2/3 max-w-sm sm:h-[2.6rem]" />
<SkeletonBlock className="h-7 w-2/3 max-w-sm sm:h-9 lg:h-10" />
<SkeletonBlock className="h-5 w-1/2 max-w-xs" />
</div>
<SkeletonBlock className="mt-2 h-[3.25rem] w-full max-w-xl rounded-full sm:h-14" />
<SkeletonBlock className="mt-2 h-14 w-full max-w-xl rounded-full" />
<div className="mt-4 grid w-full max-w-xl gap-3">
<SkeletonBlock className="h-16 w-full rounded-lg" />
<SkeletonBlock className="h-16 w-full rounded-lg" />
Expand Down
43 changes: 14 additions & 29 deletions src/components/mode-home-template.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -57,51 +57,36 @@ export function ModeHomeHero({
subtitle,
icon: Icon,
headingLevel = 1,
compact = false,
}: {
testId?: string;
title: string;
subtitle: string;
icon: LucideIcon;
headingLevel?: 1 | 2;
/**
* Mobile-only tightening used by ModeHomeTemplate so short mode homes fit a
* phone viewport without scrolling. All sm+/lg values are identical to the
* default treatment, so tablet and desktop render exactly the same.
*/
compact?: boolean;
}) {
const Heading = headingLevel === 1 ? "h1" : "h2";

// Sizing is a single mobile-first system: the icon and gaps step up sm→lg,
// and the display heading scales continuously via the fluid `text-hero` token
// (globals.css) so it never jumps at a breakpoint. The compact-only mobile
// tightening that short mode homes relied on is now the base treatment, so the
// hero still fits a phone viewport without scrolling.
return (
<section
className={cn("grid justify-items-center px-4 sm:gap-3 sm:px-0", compact ? "gap-1.5" : "gap-3")}
className="grid justify-items-center gap-1.5 px-4 sm:gap-3 sm:px-0"
aria-labelledby={`${testId ?? "mode-home"}-title`}
>
<span
className={cn(
"mode-home-icon grid 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)] sm:h-12 sm:w-12 lg:h-14 lg:w-14 lg:rounded-[1.15rem]",
compact ? "h-11 w-11" : "h-14 w-14",
)}
>
<Icon className={cn("sm:h-6 sm:w-6 lg:h-7 lg:w-7", compact ? "h-5 w-5" : "h-7 w-7")} aria-hidden="true" />
<span className="mode-home-icon grid h-11 w-11 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)] sm:h-12 sm:w-12 lg:h-14 lg:w-14 lg:rounded-[1.15rem]">
<Icon className="h-5 w-5 sm:h-6 sm:w-6 lg:h-7 lg:w-7" aria-hidden="true" />
</span>
<div className={cn("grid", compact ? "gap-1 sm:gap-1.5" : "gap-2")}>
<div className="grid gap-1 sm:gap-1.5">
<Heading
id={`${testId ?? "mode-home"}-title`}
className={cn(
"text-balance font-extrabold leading-[1.05] tracking-normal text-[color:var(--text-heading)] sm:text-3xl-plus lg:text-4xl-minus",
compact ? "text-2xl-sub" : "text-3xl-sub",
)}
className="text-balance text-hero font-extrabold leading-[1.05] tracking-normal text-[color:var(--text-heading)]"
>
{title}
</Heading>
<p
className={cn(
"mx-auto max-w-2xl text-pretty text-sm font-medium text-[color:var(--text-muted)] sm:text-sm-plus sm:leading-5 lg:text-base",
compact ? "leading-5" : "leading-6",
)}
>
<p className="mx-auto max-w-2xl text-pretty text-sm font-medium leading-5 text-[color:var(--text-muted)] sm:text-base-minus sm:leading-5 lg:text-base lg:leading-6">
{subtitle}
</p>
</div>
Expand Down Expand Up @@ -223,7 +208,7 @@ export function ModeHomeTemplate({
className,
)}
>
<ModeHomeHero testId={testId} title={title} subtitle={subtitle} icon={icon} headingLevel={headingLevel} compact />
<ModeHomeHero testId={testId} title={title} subtitle={subtitle} icon={icon} headingLevel={headingLevel} />

{desktopComposerSlotId ? (
<div
Expand All @@ -245,10 +230,10 @@ export function ModeHomeTemplate({
<ActionIcon className="h-5 w-5 sm:h-6 sm:w-6" aria-hidden="true" />
</span>
<span className="min-w-0">
<span className="block text-balance text-base-sub font-bold leading-5 text-[color:var(--text-heading)] [overflow-wrap:anywhere] sm:text-sm-plus">
<span className="block text-balance text-base-minus font-bold leading-5 text-[color:var(--text-heading)] [overflow-wrap:anywhere]">
{action.title}
</span>
<span className="mt-1 block text-xs font-medium leading-5 text-[color:var(--text-muted)] sm:text-xs-plus sm:leading-[1.3]">
<span className="mt-1 block text-xs font-medium leading-5 text-[color:var(--text-muted)] sm:text-sm-minus sm:leading-[1.3]">
{action.description}
</span>
</span>
Expand Down
3 changes: 2 additions & 1 deletion src/components/services/service-detail-page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ import {
cn,
floatingControl,
metadataPill,
pageContainer,
primaryControl,
textMuted,
toneDanger,
Expand Down Expand Up @@ -507,7 +508,7 @@ export function ServiceDetailPage({ service }: { service: ServiceRecord }) {
data-testid="service-detail-page"
className="min-h-[calc(100dvh-4rem)] bg-[color:var(--background)] px-3 py-4 pb-[calc(10rem+env(safe-area-inset-bottom))] text-[color:var(--text)] sm:px-5 sm:py-6 sm:pb-10 lg:px-8"
>
<div className="mx-auto max-w-7xl">
<div className={pageContainer}>
{notice ? (
<div
role="status"
Expand Down
7 changes: 7 additions & 0 deletions src/components/ui-primitives.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -148,6 +148,13 @@ export const searchPageCanvas = "bg-[color:var(--background)] text-[color:var(--
export const searchPageShell =
"min-h-[calc(100dvh-4rem)] overflow-x-hidden px-3 py-3 pb-[calc(12rem+env(safe-area-inset-bottom))] sm:px-5 sm:py-5 sm:pb-8 lg:px-6";
export const searchPageContainer = "mx-auto w-full max-w-[1500px]";
// Canonical content-page width. Detail pages (service / form / differential),
// medication record + prescribing workspace, and the forms results view converge
// on this so the reading measure is one source of truth instead of a scatter of
// `mx-auto max-w-7xl` literals. Width only — call sites keep their own padding and
// vertical rhythm via cn(). Intentionally-wider surfaces (the document viewer's
// 1440px viewer+rail, the differentials tables) keep their bespoke widths.
export const pageContainer = "mx-auto w-full max-w-7xl";
export const searchResultsBodyGrid = "grid gap-4 xl:grid-cols-[minmax(0,1fr)_22rem]";
export const searchResultsMainColumn = "search-results-main min-w-0";
export const searchResultsSidebar = "hidden w-[22rem] shrink-0 space-y-4 xl:block";
Expand Down