diff --git a/src/components/cme/cme-dashboard.tsx b/src/components/cme/cme-dashboard.tsx index e538dc1b40..08f8056541 100644 --- a/src/components/cme/cme-dashboard.tsx +++ b/src/components/cme/cme-dashboard.tsx @@ -32,6 +32,7 @@ import { CmeYearInWeeks } from "@/components/cme/cme-year-in-weeks"; import { WorkColumns } from "@/components/work-frame/work-desktop-layout"; import { Button } from "@/components/ui/button"; import { NewWorkModeOnly } from "@/components/work-mode-launch/work-mode-launch-provider"; +import { WorkSetupRow } from "@/components/work-setup/work-setup-row"; import { addDays, expandEvents } from "@/lib/calendar/calendar-event"; import { cmeCalendarEvents } from "@/lib/cme/calendar-events"; import { buildCmeCatchUpPlan } from "@/lib/cme/catch-up-plan"; @@ -637,6 +638,9 @@ export function CmeDashboard({ the dates and the year's details on the right (11 Oct 2026 layout audit). */}
+ + + {summary} {nextStepNode} {toLog} diff --git a/src/components/mode-kit/work-empty-line.tsx b/src/components/mode-kit/work-empty-line.tsx new file mode 100644 index 0000000000..ce5e493e1b --- /dev/null +++ b/src/components/mode-kit/work-empty-line.tsx @@ -0,0 +1,61 @@ +import Link from "next/link"; +import type { ReactNode } from "react"; + +import { focusRing } from "@/components/card-recipes"; +import { cn } from "@/components/ui-primitives"; + +/** + * The one inline empty state for a row or a short block in a work area (site + * review 12 Oct 2026, item 2): a plain sentence and, when there is somewhere + * real to go, one action. Before this a row could end at "Not set up for this + * hospital" with nothing to tap. + * + * The page-sized empty state stays `WorkEmpty` / `WorkStateNotice`; this is + * for a line inside a card. With no action it draws the sentence only, so a + * caller never invents a destination to fill the slot. + */ +export type WorkEmptyLineAction = + | { readonly label: string; readonly href: string; readonly onClick?: never; readonly testId?: string } + | { readonly label: string; readonly onClick: () => void; readonly href?: never; readonly testId?: string }; + +export const workEmptyLineActionClass = + "inline-flex min-h-12 shrink-0 items-center rounded-sm px-1 text-sm font-semibold text-[color:var(--mode-identity)] no-underline"; + +export function WorkEmptyLine({ + children, + action, + className, + testId, +}: { + /** The sentence: plain words that say what is missing. */ + readonly children: ReactNode; + readonly action?: WorkEmptyLineAction; + readonly className?: string; + readonly testId?: string; +}) { + return ( +
+

{children}

+ {action ? ( + action.href !== undefined ? ( + + {action.label} + + ) : ( + + ) + ) : null} +
+ ); +} diff --git a/src/components/mode-kit/work-sign-in-notice.tsx b/src/components/mode-kit/work-sign-in-notice.tsx index 8befc5552b..5f9c6d12f7 100644 --- a/src/components/mode-kit/work-sign-in-notice.tsx +++ b/src/components/mode-kit/work-sign-in-notice.tsx @@ -1,9 +1,10 @@ "use client"; -import type { LucideIcon } from "lucide-react"; +import { LogIn, type LucideIcon } from "lucide-react"; import { useEffect, useState, type ReactNode } from "react"; import { AccountSetupDialog } from "@/components/clinical-dashboard/account-setup-dialog"; +import { WorkButton } from "@/components/mode-kit/work"; import { WorkStateNotice } from "@/components/mode-kit/work-state"; import { useAuthSession } from "@/lib/supabase/client"; @@ -31,6 +32,43 @@ function RefreshOnSignIn({ onSignedIn }: { readonly onSignedIn: () => void }) { return null; } +/** + * The slim signed-out line: one sentence and Sign in, in a flat hairline card + * at the top of the page content (site review 12 Oct 2026, item 2). For a page + * whose own content still shows signed out (On Call keeps its public crisis + * lines and examples below), so the sign-in ask does not push them half a + * screen down the way the full `WorkSignInNotice` card did. + */ +export function WorkSignInLine({ + title, + signInLabel = "Sign in", + signInTestId, + testId, +}: { + readonly title: ReactNode; + readonly signInLabel?: string; + readonly signInTestId?: string; + readonly testId?: string; +}) { + const [open, setOpen] = useState(false); + return ( + <> +
+

+ {title} +

+ setOpen(true)} testId={signInTestId}> + {signInLabel} + +
+ {open ? setOpen(false)} /> : null} + + ); +} + /** * The shared signed-out state with the way in: "Sign in" opens the app's own * account dialog (there is no sign-in page), the same in every work area. diff --git a/src/components/my-day/my-day-today-cards.tsx b/src/components/my-day/my-day-today-cards.tsx index be5058614c..efc31a876b 100644 --- a/src/components/my-day/my-day-today-cards.tsx +++ b/src/components/my-day/my-day-today-cards.tsx @@ -9,6 +9,7 @@ import { Clock, FileText, GraduationCap, + ListChecks, Moon, Phone, Shield, @@ -751,10 +752,13 @@ const MY_DAY_QUICK_ACTIONS: readonly MyDayQuickAction[] = [ { label: "Roster", href: "/roster", icon: CalendarDays, testId: "my-day-qa-roster", mode: "roster" }, ]; -/** During on call (the mockup's 03:10 screen): Handover and Remind me take CPD's and Roster's places. */ +/** + * During on call the hero already offers Log a call and Handover, so the tiles + * offer what it does not (site review 12 Oct 2026, item 7). + */ const MY_DAY_ON_CALL_ACTIONS: readonly MyDayQuickAction[] = [ - MY_DAY_QUICK_ACTIONS[0]!, - { label: "Handover", href: "/on-call/handover", icon: FileText, testId: "my-day-qa-handover", mode: "on-call" }, + { label: "Playbook", href: "/on-call/playbook", icon: ListChecks, testId: "my-day-qa-playbook", mode: "on-call" }, + { label: "Contacts", href: "/on-call/contacts", icon: Phone, testId: "my-day-qa-contacts", mode: "on-call" }, MY_DAY_QUICK_ACTIONS[2]!, { label: "Remind me", icon: Bell, testId: "my-day-qa-remind", mode: "my-day" }, ]; diff --git a/src/components/on-call/kit/handbook-state.tsx b/src/components/on-call/kit/handbook-state.tsx index 0a506ad17f..b58cea3a7d 100644 --- a/src/components/on-call/kit/handbook-state.tsx +++ b/src/components/on-call/kit/handbook-state.tsx @@ -14,7 +14,7 @@ import type { HospitalHandbookStatus, } from "@/components/on-call/use-hospital-handbook"; import { WorkButton } from "@/components/mode-kit/work"; -import { WorkSignInNotice } from "@/components/mode-kit/work-sign-in-notice"; +import { WorkSignInLine } from "@/components/mode-kit/work-sign-in-notice"; import { WorkStateNotice } from "@/components/mode-kit/work-state"; import { cn } from "@/components/ui-primitives"; @@ -27,7 +27,7 @@ const ON_CALL_SERVICE_HREF = "/on-call/service"; * (`tests/on-call-hub-wording.test.ts`). */ const SIGN_IN_TITLES: Partial> = { - "signed-out": "Hospital numbers are for signed-in members.", + "signed-out": "Sign in to see your hospital's numbers", expired: "Your session ended. Sign in again to see your hospital's numbers.", }; @@ -59,7 +59,9 @@ export function OnCallHandbookState({ }) { const testId = `on-call-handbook-state-${handbook.status}`; const signIn = SIGN_IN_TITLES[handbook.status]; - if (signIn) return ; + // One slim line at the top, not a page-sized card (site review 12 Oct 2026, item 2): the + // public crisis lines and the page's examples below it stay where they were. + if (signIn) return ; switch (handbook.status) { case "ready": return null; diff --git a/src/components/on-call/kit/state-label.tsx b/src/components/on-call/kit/state-label.tsx index 251b444aa1..1f83217b29 100644 --- a/src/components/on-call/kit/state-label.tsx +++ b/src/components/on-call/kit/state-label.tsx @@ -34,6 +34,16 @@ function words(state: OnCallRowState): string { } } +/** + * The one action a "Not set up for this hospital" line offers (site review 12 Oct 2026, item 2): + * the service handbook page, where the service's own entries are kept. It is an existing page; + * nothing new is promised. + */ +export const ON_CALL_SERVICE_HANDBOOK_ACTION = { + label: "Service handbook", + href: "/on-call/service", +} as const; + export function OnCallStateLabel({ state }: { readonly state: OnCallRowState }) { const warning = state.kind === "removed"; return ( diff --git a/src/components/on-call/now/right-now.tsx b/src/components/on-call/now/right-now.tsx index a8438b28b3..f6892dd9bc 100644 --- a/src/components/on-call/now/right-now.tsx +++ b/src/components/on-call/now/right-now.tsx @@ -8,7 +8,8 @@ import { OnCallTrackBar } from "@/components/on-call/kit/track-bar"; import { onCallOutlineButton } from "@/components/on-call/kit/calm"; import { OnCallDialSheet, onCallCallRoute } from "@/components/on-call/kit/dial-sheet"; import { OnCallGroupActionControl } from "@/components/on-call/kit/grouped-list"; -import { OnCallStateLabel } from "@/components/on-call/kit/state-label"; +import { ON_CALL_SERVICE_HANDBOOK_ACTION, OnCallStateLabel } from "@/components/on-call/kit/state-label"; +import { WorkEmptyLine } from "@/components/mode-kit/work-empty-line"; import { useOnCallYouCalledAt } from "@/components/on-call/kit/use-you-called"; import { useOnCallHospitalPhone } from "@/components/on-call/call/call-device-stores"; import { modeHeadingText, modeNumberText, modeSecondaryText } from "@/components/mode-kit/type"; @@ -153,9 +154,13 @@ export function NowRightNow({ ) : null}
) : ( -

+ -

+ )} {cover ? ( diff --git a/src/components/on-call/now/shift-lists.tsx b/src/components/on-call/now/shift-lists.tsx index 02189b093c..9fda612373 100644 --- a/src/components/on-call/now/shift-lists.tsx +++ b/src/components/on-call/now/shift-lists.tsx @@ -5,7 +5,8 @@ import { useId, useState } from "react"; import { focusRing } from "@/components/card-recipes"; import { modeInsetHairline, modeModuleSurface, modePressable, modeRowHeight } from "@/components/mode-kit/recipes"; import { OnCallTrackBar } from "@/components/on-call/kit/track-bar"; -import { OnCallStateLabel } from "@/components/on-call/kit/state-label"; +import { ON_CALL_SERVICE_HANDBOOK_ACTION, OnCallStateLabel } from "@/components/on-call/kit/state-label"; +import { WorkEmptyLine } from "@/components/mode-kit/work-empty-line"; import { onCallLeadingIcon } from "@/components/on-call/kit/calm"; import { modeNameText, modeNumberText, modeSecondaryText } from "@/components/mode-kit/type"; import { OnCallNextShift } from "@/components/on-call/on-call-next-shift"; @@ -116,11 +117,14 @@ export function NowShiftLists({ shifts, items, now, + handbookReady = true, }: { readonly context: OnCallShiftContext; readonly shifts: RosterShiftsState; readonly items: readonly HandbookItem[]; readonly now: Date; + /** False while the handbook is loading, signed out or unavailable: an empty list is then unconfirmed. */ + readonly handbookReady?: boolean; }) { const [open, setOpen] = useState(false); const [ticks, setTicks] = useState<{ readonly shiftKey: string; readonly ids: ReadonlySet }>(() => ({ @@ -176,8 +180,12 @@ export function NowShiftLists({ className="w-16 shrink-0" /> - ) : ( + ) : handbookReady ? ( + ) : ( + + Not available right now + )}