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
26 changes: 16 additions & 10 deletions src/app/work-mode.css
Original file line number Diff line number Diff line change
Expand Up @@ -658,7 +658,8 @@ html:not([data-mode-band]) [data-testid="chrome-safe-area-top"][data-work-band]
}
}

/* Round glass buttons: a 48px tap, a 44px glass circle. */
/* Round glass buttons: a 48px glass circle, the same size as the clinical
header's round buttons (Josh, 10 Oct 2026). */
.universal-header[data-work-frame] .universal-header-icon-control {
position: relative;
isolation: isolate;
Expand All @@ -671,7 +672,7 @@ html:not([data-mode-band]) [data-testid="chrome-safe-area-top"][data-work-band]
.universal-header[data-work-frame] .universal-header-icon-control::before {
content: "";
position: absolute;
inset: 2px;
inset: 0;
z-index: var(--work-z-under);
border-radius: var(--radius-pill);
background: var(--work-glass-fill);
Expand Down Expand Up @@ -756,37 +757,40 @@ html:not([data-mode-band]) [data-testid="chrome-safe-area-top"][data-work-band]
outline-offset: 2px;
}

/* The mode pill: glass, a flat mode-colour badge, page over area. */
/* The mode pill: glass, a flat mode-colour badge, page over area. The same
48px height as the clinical pill (Josh, 10 Oct 2026), and like it the pill
hugs its name with the chevron right after it, leaving no empty space
(Josh, 10 Oct 2026). A long name grows the pill up to its limit. */
.universal-header[data-work-frame] .universal-header-mode-button {
position: relative;
isolation: isolate;
width: auto;
min-width: 0;
/* Room for the longest area name ("Assessments") beside two round
buttons on a 390px phone, without truncating it, and for a short one
("Roster") at 320px now the pill names the area only. Never narrower than
a tap, so at large text sizes the badge still shows when the name cannot. */
/* Never narrower than a tap, so at large text sizes the badge still shows
when the name cannot. */
max-width: max(var(--spacing-tap, 3rem), min(15rem, calc(100vw - 11rem)));
/* The name's track sizes to the name itself, so a name that fits is never
cut short by rounding; it shortens only once the pill reaches its limit. */
grid-template-columns: auto minmax(0, max-content) auto;
gap: calc(var(--spacing) * 1.75);
padding: 0 calc(var(--spacing) * 2) 0 calc(var(--spacing) * 1.5);
gap: calc(var(--spacing) * 2);
padding: 0 calc(var(--spacing) * 2.5);
border-color: transparent;
background: transparent;
box-shadow: none;
}

@media (min-width: 640px) {
.universal-header[data-work-frame] .universal-header-mode-button {
width: auto;
max-width: 18rem;
padding-right: calc(var(--spacing) * 3);
}
}

.universal-header[data-work-frame] .universal-header-mode-button::before {
content: "";
position: absolute;
inset: 0.25rem 0;
inset: 0;
z-index: var(--work-z-under);
border-radius: var(--radius-pill);
background: var(--work-glass-fill);
Expand Down Expand Up @@ -860,6 +864,8 @@ html:not([data-mode-band]) [data-testid="chrome-safe-area-top"][data-work-band]
.universal-header[data-work-frame] .universal-header-mode-button {
grid-template-columns: auto minmax(0, 1fr);
gap: calc(var(--spacing) * 0.5);
width: auto;
min-width: 0;
max-width: 100%;
height: auto;
min-height: 48px;
Expand Down
10 changes: 0 additions & 10 deletions src/components/admin/admin-compliance-export-page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,6 @@ import { focusRing } from "@/components/card-recipes";
import { AccountSetupDialog } from "@/components/clinical-dashboard/account-setup-dialog";
import { InformationPageBreadcrumbs } from "@/components/information-page-shell";
import { PageTitleUnderBand, useModeBandHeading } from "@/components/mode-band/mode-band";
import { ModeNotice } from "@/components/mode-kit/notice";
import { WorkButton, WorkChip, WorkDock, WorkIconCircle, WorkSectionLabel } from "@/components/mode-kit/work";
import { WorkStateNotice } from "@/components/mode-kit/work-state";
import { announce } from "@/components/ui/live-announcer";
Expand All @@ -31,7 +30,6 @@ import { ADMIN_PAGE_HREFS } from "@/lib/admin/page-hrefs";
import { ADMIN_REQUIREMENTS_CATALOGUE } from "@/lib/admin/requirements";
import { buildXlsx, XLSX_MIME } from "@/lib/admin/xlsx-lite";
import { perthCalendarDate } from "@/lib/cme/cpd-year";
import { useExampleData } from "@/lib/example-data/store";
import { useOnCallEntries } from "@/lib/on-call/entry-store";
import { guardExampleAction } from "@/lib/example-data/guards";

Expand Down Expand Up @@ -68,8 +66,6 @@ const RANGE_OPTIONS = [
*/
export function AdminComplianceExportPage({ now: nowProp }: { now?: Date } = {}) {
const state = useOnCallEntries();
// The example data banner already says these are example records; this notice is for the demo build.
const examplesBanner = useExampleData("admin").active;
const mountedAt = useMemo(() => new Date(), []);
const now = nowProp ?? mountedAt;
const today = perthCalendarDate(now);
Expand Down Expand Up @@ -155,12 +151,6 @@ export function AdminComplianceExportPage({ now: nowProp }: { now?: Date } = {})
</>
) : (
<div className="grid min-w-0 gap-5" data-testid="admin-compliance-export-ready">
{state.demoMode && !examplesBanner ? (
<ModeNotice testId="admin-compliance-export-demo-notice">
Example records. These dates are made up, and nothing here is your own.
</ModeNotice>
) : null}

<section className={cn("work-card", adminStyles.exportFile)} aria-labelledby="export-preview">
<div className="work-row">
<WorkIconCircle icon={FileSpreadsheet} />
Expand Down
9 changes: 0 additions & 9 deletions src/components/admin/admin-compliance-page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,6 @@ import {
} from "@/components/admin/admin-status-tag";
import { AccountSetupDialog } from "@/components/clinical-dashboard/account-setup-dialog";
import { PageTitleUnderBand, useModeBandHeading } from "@/components/mode-band/mode-band";
import { ModeNotice } from "@/components/mode-kit/notice";
import { WorkButton, WorkCard, WorkChip, WorkDock, WorkEmpty, WorkIconCircle } from "@/components/mode-kit/work";
import { WorkStateNotice } from "@/components/mode-kit/work-state";
import {
Expand All @@ -36,7 +35,6 @@ import { ADMIN_PAGE_HREFS } from "@/lib/admin/page-hrefs";
import { formatDateEcho } from "@/lib/admin/renewal-dates";
import { ADMIN_REQUIREMENTS_CATALOGUE } from "@/lib/admin/requirements";
import { perthCalendarDate } from "@/lib/cme/cpd-year";
import { useExampleData } from "@/lib/example-data/store";
import { useOnCallEntries } from "@/lib/on-call/entry-store";

const RECORD_DATES_HREF = `${ADMIN_PAGE_HREFS.renewals}?record=missing`;
Expand Down Expand Up @@ -324,8 +322,6 @@ function FirstUse({ overview, canEdit }: { readonly overview: ComplianceOverview
*/
export function AdminCompliancePage({ now: nowProp }: { now?: Date } = {}) {
const state = useOnCallEntries();
// The example data banner already says these are example records; this notice is for the demo build.
const examplesBanner = useExampleData("admin").active;
const mountedAt = useMemo(() => new Date(), []);
const now = nowProp ?? mountedAt;
const today = perthCalendarDate(now);
Expand Down Expand Up @@ -371,11 +367,6 @@ export function AdminCompliancePage({ now: nowProp }: { now?: Date } = {}) {
</>
) : (
<div className={adminStyles.column} data-testid="admin-compliance-ready">
{state.demoMode && !examplesBanner ? (
<ModeNotice testId="admin-compliance-demo-notice">
Example records. These dates are made up, and nothing here is your own.
</ModeNotice>
) : null}
{firstUse ? (
<FirstUse overview={overview} canEdit={canEdit} />
) : (
Expand Down
9 changes: 0 additions & 9 deletions src/components/admin/admin-today-page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,6 @@ import { TodayRenewNextCard } from "@/components/admin/today/today-renew-next-ca
import { TodayOvertimeRow, TodayRequirementsModule } from "@/components/admin/today/today-requirements-module";
import { TodayStarred } from "@/components/admin/today/today-starred";
import { PageTitleUnderBand, useModeBandHeading, useModeBandShown } from "@/components/mode-band/mode-band";
import { ModeNotice } from "@/components/mode-kit/notice";
import { todayStateCopy } from "@/components/mode-kit/today/today-copy";
import { WorkButton, WorkCard, WorkDock, WorkIconCircle } from "@/components/mode-kit/work";
import { WorkStateNotice } from "@/components/mode-kit/work-state";
Expand All @@ -35,7 +34,6 @@ import {
selectRenewNext,
selectRequirementsSummary,
} from "@/lib/admin/today-selectors";
import { useExampleData } from "@/lib/example-data/store";
import { cacheOnCallEntries, readCachedOnCallEntries, useOnCallEntries } from "@/lib/on-call/entry-store";
import type { OnCallEntry } from "@/lib/on-call/entry-model";
import { msUntilNextOnCallLocalDay } from "@/lib/on-call/local-date";
Expand Down Expand Up @@ -111,8 +109,6 @@ function HelpRow() {
export function AdminTodayPage({ now: nowProp }: { now?: Date } = {}) {
const { zone } = useWorkTimeZone();
const state = useOnCallEntries();
// The example data banner already says these are example records; this notice is for the demo build.
const examplesBanner = useExampleData("admin").active;
const { isAuthenticated } = useAccountData();
const [tick, setTick] = useState(() => new Date());
const now = nowProp ?? tick;
Expand Down Expand Up @@ -167,11 +163,6 @@ export function AdminTodayPage({ now: nowProp }: { now?: Date } = {}) {
</PageTitleUnderBand>
<p className="text-sm text-[color:var(--text-muted)]">{dateEcho}</p>
</header>
{state.demoMode && load === "ready" && !examplesBanner ? (
<ModeNotice testId="admin-today-demo-notice">
Example records. These dates are made up, and nothing here is your own.
</ModeNotice>
) : null}

{load === "loading" ? (
<TodayLoadingSkeleton />
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2666,7 +2666,7 @@ export function MasterSearchHeader(props: MasterSearchHeaderProps) {
onClick={toggleModeMenu}
onKeyDown={handleModeTriggerKeyDown}
className={cn(
"universal-header-mode-button inline-grid h-12 w-[min(13rem,calc(100vw-9rem))] min-w-0 grid-cols-[auto_minmax(0,1fr)_auto] items-center gap-2 rounded-full border border-[color:var(--border)] bg-[color:var(--surface)] px-2.5 text-left transition hover:border-[color:var(--border-strong)] hover:bg-[color:var(--surface-subtle)] focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-[color:var(--focus)] sm:w-auto sm:min-w-[13rem] sm:pr-3",
"universal-header-mode-button inline-grid h-12 w-auto min-w-0 max-w-[calc(100vw-9rem)] grid-cols-[auto_minmax(0,1fr)_auto] items-center gap-2 rounded-full border border-[color:var(--border)] bg-[color:var(--surface)] px-2.5 text-left transition hover:border-[color:var(--border-strong)] hover:bg-[color:var(--surface-subtle)] focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-[color:var(--focus)] sm:pr-3",
)}
aria-haspopup="dialog"
aria-expanded={modeMenuOpen}
Expand Down
4 changes: 1 addition & 3 deletions src/components/clinical-dashboard/settings-dialog.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
"use client";

Check notice on line 1 in src/components/clinical-dashboard/settings-dialog.tsx

View check run for this annotation

CodeScene Access / CodeScene Code Health Review (main)

✅ Getting better: Lines of Code in a Single File

The lines of code decreases from 1560 to 1558, improve code health by reducing it to 1000 The number of Lines of Code in a single file. More Lines of Code lowers the code health.

import Link from "next/link";
import {
Expand Down Expand Up @@ -1736,9 +1736,7 @@
<SettingsToggleField
icon={Layers}
label="Example data"
description={
on ? exampleAreasLine(activeAreas.length) : "Made-up data in every work area, marked Example. Nothing is saved."
}
description={on ? exampleAreasLine(activeAreas.length) : "Made-up data in every work area. Nothing is saved."}
checked={on}
onChange={(next) => (next ? turnOn() : turnOff())}
/>
Expand Down
114 changes: 31 additions & 83 deletions src/components/example-data/example-data-banner.tsx
Original file line number Diff line number Diff line change
@@ -1,27 +1,14 @@
"use client";

import dynamic from "next/dynamic";
import { useRouter } from "next/navigation";
import { useCallback, useEffect, useState } from "react";

import { WorkButton, WorkTag, useWorkUndoToast } from "@/components/mode-kit/work";
import { WorkButton, useWorkUndoToast } from "@/components/mode-kit/work";
import { Sheet } from "@/components/ui/sheet";
import { EXAMPLE_BLOCKED_EVENT, type ExampleBlockedAction } from "@/lib/example-data/guards";
import {
restoreExampleData,
snapshotExampleData,
syncExampleCookie,
useAuthIfAvailable,
useExampleData,
} from "@/lib/example-data/store";
import { restoreExampleData, snapshotExampleData, syncExampleCookie, useExampleData } from "@/lib/example-data/store";
import type { WorkAreaId } from "@/lib/work-frame/areas";

// Loaded only when a signed-out visitor taps Sign in.
const AccountSetupDialog = dynamic(
() => import("@/components/clinical-dashboard/account-setup-dialog").then((m) => m.AccountSetupDialog),
{ ssr: false },
);

/** How the sheet words each blocked action: the title's past tense, and the body's verb. */
const BLOCKED_WORDS: Readonly<Record<ExampleBlockedAction, { readonly done: string; readonly verb: string }>> = {
export: { done: "exported", verb: "export" },
Expand All @@ -36,127 +23,88 @@
}

/**
* The slim strip under the work frame's band while an area shows example data
* (setup mockup, "Example data" frames). One mount, under the band: pages
* never draw their own. Turn off switches example data off everywhere and
* offers Undo.
* The example data host under the work frame's band. It draws no notice: the
* owner asked (10 Oct 2026) that nothing on screen calls the data example or
* made-up. Example data is switched off, and signed in to, from Settings.
*
* It also hosts the sheet that explains a blocked export, share, save, send or
* It hosts the sheet that explains a blocked export, share, save, send or
* copy: `guardExampleAction(active, kind)` fires `EXAMPLE_BLOCKED_EVENT`, and
* this opens "Example data can't be exported" with a way to turn it off. All
* of it is local, so it works offline.
*
* Mounted always (it draws nothing while the area shows real data), it keeps
* Mounted always, it keeps
* the server's example data cookie in step with what this browser shows, and
* refreshes server-rendered areas (Teaching, CPD) when that changes.
*/
export function ExampleDataBanner({ area }: { readonly area: WorkAreaId }) {
const { active, activeAreas, turnOff } = useExampleData(area);
const { activeAreas, turnOff } = useExampleData(area);
const router = useRouter();
const toast = useWorkUndoToast();
// A string key, so the effect runs when the set of areas changes, not on every new array.
const activeKey = activeAreas.join(".");
const auth = useAuthIfAvailable();
// The per-area sample notices carried the only sign-in prompt, so the banner offers it instead.
const signedOut = auth?.status === "signed_out" || auth?.status === "expired";
const [signInOpen, setSignInOpen] = useState(false);
const [blocked, setBlocked] = useState<ExampleBlockedAction | null>(null);
// Kept after the sheet closes so its words do not change while it leaves.
const [lastBlocked, setLastBlocked] = useState<ExampleBlockedAction>("export");

useEffect(() => {
function onBlocked(event: Event) {
const kind = (event as CustomEvent<unknown>).detail;
const next = isBlockedAction(kind) ? kind : "export";
setLastBlocked(next);
setBlocked(next);
}
window.addEventListener(EXAMPLE_BLOCKED_EVENT, onBlocked);
return () => window.removeEventListener(EXAMPLE_BLOCKED_EVENT, onBlocked);
}, []);

// Runs after every render (the store re-renders this on every change, account
// switches included). The sync is a string compare, and refreshes only when
// the server's cookie actually changed.
useEffect(() => {
const areas = activeKey ? (activeKey.split(".") as WorkAreaId[]) : [];
if (syncExampleCookie(areas)) router.refresh();
});

const switchOff = useCallback(() => {
// Undo puts back exactly what was there, so auto mode returns to auto
// rather than becoming an explicit on.
// The cookie sync above refreshes server-rendered areas for both.
const before = snapshotExampleData();
turnOff();
toast?.("Example data off", () => restoreExampleData(before));
}, [toast, turnOff]);

const closeSheet = useCallback(() => setBlocked(null), []);
const turnOffFromSheet = useCallback(() => {
setBlocked(null);
switchOff();
}, [switchOff]);

const words = BLOCKED_WORDS[blocked ?? lastBlocked];

return (
<>
{active ? (
<div
role="region"
aria-label="Example data"
data-testid="example-data-banner"
className="flex min-h-12 items-center gap-2 border-b border-[color:var(--work-line,var(--border))] bg-[color:var(--work-surface,var(--surface-raised))] pr-1 pl-3.5"
>
<WorkTag tone="amber">Example</WorkTag>
<p className="m-0 min-w-0 flex-1 py-1.5 text-xs leading-snug font-medium text-[color:var(--text)]">
Made-up data to look around. Nothing is saved.
</p>
{signedOut ? (
<button
type="button"
onClick={() => setSignInOpen(true)}
data-testid="example-data-banner-sign-in"
className="inline-flex min-h-12 flex-none items-center rounded-full px-3 text-sm-minus font-bold text-[color:var(--mode-identity)] focus-visible:outline-2 focus-visible:-outline-offset-2 focus-visible:outline-[color:var(--mode-identity)]"
>
Sign in
</button>
) : null}
<button
type="button"
onClick={switchOff}
aria-label="Turn off example data"
className="inline-flex min-h-12 flex-none items-center rounded-full px-3 text-sm-minus font-bold text-[color:var(--mode-identity)] focus-visible:outline-2 focus-visible:-outline-offset-2 focus-visible:outline-[color:var(--mode-identity)]"
>
Turn off
</button>
</div>
) : null}
{signInOpen ? <AccountSetupDialog open onClose={() => setSignInOpen(false)} /> : null}
<Sheet
open={blocked !== null}
onClose={closeSheet}
title={`Example data can't be ${words.done}`}
testId="example-blocked-sheet"
contentClassName="work-more-sheet"
headerClassName="work-more-sheet__header"
titleClassName="work-more-sheet__title"
closeButtonClassName="work-more-sheet__close"
bodyClassName="work-more-sheet__body"
>
<p className="mt-0 mb-4 text-sm-minus leading-normal text-[color:var(--text)]">
Turn it off to {words.verb} your own records.
</p>
<div className="grid gap-2">
<WorkButton size="wide" onClick={turnOffFromSheet}>
Turn off example data
</WorkButton>
<WorkButton size="wide" variant="secondary" onClick={closeSheet}>
Cancel
</WorkButton>
</div>
</Sheet>
</>
<Sheet
open={blocked !== null}
onClose={closeSheet}
title={`Example data can't be ${words.done}`}
testId="example-blocked-sheet"
contentClassName="work-more-sheet"
headerClassName="work-more-sheet__header"
titleClassName="work-more-sheet__title"
closeButtonClassName="work-more-sheet__close"
bodyClassName="work-more-sheet__body"
>
<p className="mt-0 mb-4 text-sm-minus leading-normal text-[color:var(--text)]">
Turn it off to {words.verb} your own records.
</p>
<div className="grid gap-2">
<WorkButton size="wide" onClick={turnOffFromSheet}>
Turn off example data
</WorkButton>
<WorkButton size="wide" variant="secondary" onClick={closeSheet}>
Cancel
</WorkButton>
</div>
</Sheet>

Check notice on line 108 in src/components/example-data/example-data-banner.tsx

View check run for this annotation

CodeScene Access / CodeScene Code Health Review (main)

✅ No longer an issue: Complex Method

ExampleDataBanner is no longer above the threshold for cyclomatic complexity
);
}
13 changes: 0 additions & 13 deletions src/components/example-data/example-tag.tsx

This file was deleted.

Loading
Loading