Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
17 commits
Select commit Hold shift + click to select a range
ebf82db
a11y: undo bars wait while focused, mode menu groups, label in name g…
claude Oct 26, 2026
20a8b8a
a11y: name rows and dial buttons by the words they show (WCAG 2.5.3)
claude Oct 26, 2026
cf4a754
a11y: more names match their words, unique landmarks (WCAG 2.5.3, 1.3.1)
claude Oct 26, 2026
25df29f
test: follow the controls' new spoken names
claude Oct 26, 2026
9f2bde0
style: format the stage C test
claude Oct 26, 2026
f6671d7
a11y: count moderate WCAG failures, skip target on the display screen
claude Oct 26, 2026
7de15bd
test: cross-mode card contract follows the listitem div
claude Oct 26, 2026
f446396
Merge remote-tracking branch 'origin/main' into claude/project-thread…
claude Oct 26, 2026
717eb30
Name My Day calendar cells by labelledby so warning bars keep a text …
claude Oct 26, 2026
a6b0db4
Expect named mode-menu groups, not headings, in the keyboard smoke test
claude Oct 26, 2026
fa39dbd
Match renewals group headings by their leading count in the admin jou…
claude Oct 26, 2026
bd407e4
Merge remote-tracking branch 'origin/main' into claude/project-thread…
claude Oct 26, 2026
0814d98
Merge remote-tracking branch 'origin/main' into claude/project-thread…
claude Oct 26, 2026
ab447fd
fix(a11y): readable dark mode tag in the More menu, gentle option (A1…
claude Oct 26, 2026
a8ed172
Merge remote-tracking branch 'origin/main' into claude/project-thread…
claude Oct 26, 2026
4550ea0
fix(a11y): open shifts calendar days from another month say their month
claude Oct 26, 2026
f83d075
chore: refresh repo awareness snapshot for the new decision entry
claude Oct 26, 2026
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
5 changes: 5 additions & 0 deletions data/repo-awareness-snapshot.json
Original file line number Diff line number Diff line change
Expand Up @@ -4706,6 +4706,11 @@
"section": "decisions",
"catalogued": true
},
{
"path": "docs/decisions/2026-10-26-gentle-accessibility-options.md",
"section": "decisions",
"catalogued": true
},
{
"path": "docs/skipped-tests-inventory.md",
"section": "root",
Expand Down
12 changes: 12 additions & 0 deletions docs/decisions/2026-10-26-gentle-accessibility-options.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
# Decision: Gentle accessibility fixes only, default look unchanged

- **Status:** decided 2026-10-26
- **Source:** the owner, in chat

On 26 Oct 2026 the owner reviewed the first round of accessibility mockups and said "lots of these make the design look worse". After a gentler second round, the owner said "Ok go ahead I approve gentle version but the actual main default doesn't change".

- The dark mode tag in the More menu uses the plain mode accent colour instead of the deep ink (3.9:1 to 7.6:1). Light mode is unchanged.
- Form and search box edges keep their current colour.
- The first-round proposals (darker box edges, rebuilt quick-action row, near-white tag, single-key shortcut switch, forced-colours Teaching action) are not built.

If this summary and the source ever differ, the source wins.
5 changes: 5 additions & 0 deletions src/app/work-mode.css
Original file line number Diff line number Diff line change
Expand Up @@ -1422,6 +1422,11 @@ html:not([data-mode-band]) [data-testid="chrome-safe-area-top"][data-work-band]
white-space: nowrap;
}

/* The deep ink is dark on dark here (3.9:1). The plain accent reads at 7.6:1 (A11Y-10, gentle option). */
.dark .work-more-area__tag {
color: var(--mode-identity);
}

.work-more-area__chev {
width: 1rem;
height: 1rem;
Expand Down
40 changes: 24 additions & 16 deletions src/components/admin/admin-kit.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -135,30 +135,45 @@ export function AdminRow({
readonly disabled?: boolean;
readonly className?: string;
}) {
// A row that acts is named by its title and end status; the sub line and tags describe it,
// so "Copy for workforce" is the button's name, not the whole paragraph under it.
// A row that acts is named by every line it shows, in the order it shows them, so a
// voice-control user can say any of it (WCAG 2.5.3, A11Y-02). An icon lead adds nothing;
// a lead with words ("Start renewing:") is read first, as it is drawn.
const baseId = useId();
const hasEnd = end !== undefined && end !== null;
const labelledBy = hasEnd ? `${baseId}-t ${baseId}-e` : `${baseId}-t`;
const describedBy = [sub ? `${baseId}-s` : null, tags ? `${baseId}-g` : null].filter(Boolean).join(" ") || undefined;
const labelledBy = [
lead ? `${baseId}-l` : null,
`${baseId}-t`,
sub ? `${baseId}-s` : null,
tags ? `${baseId}-g` : null,
hasEnd ? `${baseId}-e` : null,
]
.filter(Boolean)
.join(" ");
const body = (
<>
{lead}
{/* `contents`, so the lead keeps its place in the row while the name can point at it.
The {" "} between parts are word breaks for the spoken and visible text; flex and
grid draw nothing for them. */}
{lead ? (
<span className="contents" id={`${baseId}-l`}>
{lead}
</span>
) : null}{" "}
<span className="work-row__text">
<span className="work-row__title" id={`${baseId}-t`}>
{title}
</span>
</span>{" "}
{sub ? (
<span className="work-row__sub" id={`${baseId}-s`}>
{sub}
</span>
) : null}
) : null}{" "}
{tags ? (
<span className={styles.rowTags} id={`${baseId}-g`}>
{tags}
</span>
) : null}
</span>
</span>{" "}
{hasEnd ? (
<span className="work-row__end" id={`${baseId}-e`}>
{end}
Expand All @@ -174,13 +189,7 @@ export function AdminRow({
</>
);
const control = href ? (
<Link
href={href}
className="work-row"
aria-labelledby={labelledBy}
aria-describedby={describedBy}
data-testid={testId}
>
<Link href={href} className="work-row" aria-labelledby={labelledBy} data-testid={testId}>
{body}
</Link>
) : onClick ? (
Expand All @@ -191,7 +200,6 @@ export function AdminRow({
className={cn("work-row", ariaExpanded !== undefined && styles.disclosure)}
aria-expanded={ariaExpanded}
aria-labelledby={labelledBy}
aria-describedby={describedBy}
data-testid={testId}
>
{body}
Expand Down
8 changes: 5 additions & 3 deletions src/components/admin/admin-renewals-page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,7 @@ import { onCallEntrySchema, type OnCallEntry } from "@/lib/on-call/entry-model";
import { cacheOnCallEntries, readCachedOnCallEntries, useOnCallEntries } from "@/lib/on-call/entry-store";
import { parseApiErrorResponse } from "@/lib/api-client-error";
import { guardExampleAction } from "@/lib/example-data/guards";
import { useHoldWhileEngaged } from "@/components/use-hold-while-engaged";

type CatalogueItem = (typeof ADMIN_REQUIREMENTS_CATALOGUE)[number];
type RenewSubject = { entry: OnCallEntry | null; createItem?: CatalogueItem };
Expand Down Expand Up @@ -183,11 +184,12 @@ export function AdminRenewalsPage({ now: nowProp }: { now?: Date } = {}) {
const [undoBusy, setUndoBusy] = useState(false);
const [failedAction, setFailedAction] = useState<FailedAction | null>(null);

const { held: undoHeld, holdProps: undoHoldProps } = useHoldWhileEngaged(undoBar !== null);
useEffect(() => {
if (!undoBar || undoBar.failed) return;
if (!undoBar || undoBar.failed || undoHeld) return;
const timer = setTimeout(() => setUndoBar(null), UNDO_WINDOW_MS);
return () => clearTimeout(timer);
}, [undoBar]);
}, [undoBar, undoHeld]);

// Items marked not for this job are left out here: they show once, in their own closing section.
const rows = useMemo(() => requirementChecklistRowsForJob(ADMIN_REQUIREMENTS_CATALOGUE, own), [own]);
Expand Down Expand Up @@ -598,7 +600,7 @@ export function AdminRenewalsPage({ now: nowProp }: { now?: Date } = {}) {
)}

{undoBar ? (
<div data-testid="admin-renewals-undo-bar" className={adminStyles.undoBar} role="status">
<div data-testid="admin-renewals-undo-bar" className={adminStyles.undoBar} role="status" {...undoHoldProps}>
<span className={adminStyles.undoText}>
{undoBar.failed ? `Undo didn't save · ${undoBar.message}` : undoBar.message}
</span>
Expand Down
5 changes: 4 additions & 1 deletion src/components/admin/new-job/admin-records-page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -62,8 +62,11 @@ function RecordRow({ row }: { row: AdminRecordsRow }) {

function RecordGroup({ section, expandAll }: { section: AdminRecordsSection; expandAll: boolean }) {
const id = SECTION_ID[section.label];
// The Admin area band is already a region called "Admin", and two regions with one
// name confuse landmark lists (A11Y-07), so this group is named "Other admin".
const regionName = section.label === "Admin" ? "Other admin" : section.label;
return (
<section id={id} className={cn(inPageAnchor, adminStyles.section)} aria-label={section.label}>
<section id={id} className={cn(inPageAnchor, adminStyles.section)} aria-label={regionName}>
<WorkSectionLabel>
{section.label}
<span className="tabular-nums" data-testid={`${id}-count`}>{` · ${section.rows.length}`}</span>
Expand Down
11 changes: 8 additions & 3 deletions src/components/admin/new-job/admin-saved-undo-bar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,15 @@ import { useEffect, useRef } from "react";

import { adminStyles } from "@/components/admin/admin-kit";
import { cn } from "@/components/ui-primitives";
import { useHoldWhileEngaged } from "@/components/use-hold-while-engaged";

const AUTO_DISMISS_MS = 6_000;

/**
* The one "Saved · Undo" bar every real toggle on New job shares (standard
* §7, ui-lane-rules "Undo"): 48px, sits above where a floating Add would be,
* and clears itself after about six seconds if nobody taps Undo.
* and clears itself after about six seconds if nobody taps Undo. It waits while
* the pointer or keyboard focus is on it.
*/
export function AdminSavedUndoBar({
label = "Saved",
Expand All @@ -28,13 +30,16 @@ export function AdminSavedUndoBar({
dismissRef.current = onDismiss;
}, [onDismiss]);

// Held open while the pointer or keyboard focus is on the bar (WCAG 2.2.1).
const { held, holdProps } = useHoldWhileEngaged(true);
useEffect(() => {
if (held) return;
const timer = window.setTimeout(() => dismissRef.current(), AUTO_DISMISS_MS);
return () => window.clearTimeout(timer);
}, []);
}, [held]);

return (
<div role="status" data-testid={testId} className={cn(adminStyles.undoBar, "print:hidden")}>
<div role="status" data-testid={testId} {...holdProps} className={cn(adminStyles.undoBar, "print:hidden")}>
<span className={adminStyles.undoText}>{label}</span>
<button type="button" onClick={onUndo} data-testid={`${testId}-undo`} className={adminStyles.undoAction}>
Undo
Expand Down
5 changes: 2 additions & 3 deletions src/components/admin/renewals/checklist-list.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -274,14 +274,13 @@ export function ChecklistList({
type="button"
className={cn("work-row", adminStyles.disclosure)}
aria-expanded={open}
aria-label={label}
aria-describedby={subId}
onClick={() => toggle(group)}
data-testid={`${testId}-group-${group}-toggle`}
>
<WorkIconCircle icon={Icon} />
<span className="work-row__text">
<span className="work-row__title">{label}</span>
{/* Named by what it shows, so voice control can say it (WCAG 2.5.3, A11Y-02). */}
<span className="work-row__title">{label}</span>{" "}
<span className="work-row__sub" id={subId}>
{groupSummary(groupRows, today)}
</span>
Expand Down
3 changes: 2 additions & 1 deletion src/components/admin/today/today-at-a-glance.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,8 @@ export function TodayAtAGlance({ counts }: { counts: Record<RenewalsShowFilter,
<li key={filter} className="min-w-0">
<Link
href={renewalsShowHref(filter)}
aria-label={`${label}: ${count}`}
// Starts with the words it shows, so voice control can say them (WCAG 2.5.3, A11Y-02).
aria-label={`${count} ${TALLY_WORDS[filter].toLowerCase()}. ${label}`}
className={adminStyles.glanceWord}
data-testid={`admin-today-at-a-glance-${filter}`}
data-zero={count === 0 ? "" : undefined}
Expand Down
21 changes: 10 additions & 11 deletions src/components/calculators/search-page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -144,7 +144,6 @@ function CalculatorTile({
// Focus-return anchor: closing the sheet re-renders this list, so the page
// finds the tile again by calculator id rather than holding a node ref.
data-calculator-open={calc.id}
aria-label={`Open ${calc.abbrev} — ${calc.name}`}
// A compact row of the list's one hairline card (Clinical audit, 13 Oct
// 2026, item 12), the same row Therapy search uses: name, one line,
// tags, and the whole row opens the calculator.
Expand All @@ -153,37 +152,38 @@ function CalculatorTile({
focusRing,
)}
>
{/* Named by what the row shows, after what it does (WCAG 2.5.3, A11Y-02). */}
<span className="sr-only">Open</span>{" "}
<span
className={cn(
"mt-0.5 grid size-9 shrink-0 place-items-center rounded-lg bg-[color:var(--clinical-accent-soft)] text-[color:var(--clinical-accent)]",
)}
>
<Icon className="size-icon-md" aria-hidden="true" />
</span>

<span className="min-w-0">
<span className="flex flex-wrap items-center gap-x-2 gap-y-1">
<span className="text-base font-extrabold leading-6 text-[color:var(--text-heading)]">{calc.abbrev}</span>
<span className="text-base font-extrabold leading-6 text-[color:var(--text-heading)]">{calc.abbrev}</span>{" "}
<span className="inline-flex min-h-5 items-center rounded-md bg-[color:var(--surface-subtle)] px-1.5 text-3xs font-bold uppercase tracking-label text-[color:var(--text-muted)]">
{domainLabels[calc.domain]}
</span>
</span>{" "}
{derived.started ? (
<SeverityPill tone={derived.result.tone} label={`${derived.score} · ${derived.result.label}`} />
) : null}
</span>
</span>{" "}
<span className="mt-0.5 block truncate text-2xs font-semibold leading-4 text-[color:var(--text-muted)]">
{calc.name}
</span>
</span>{" "}
{compact ? null : (
<span className="mt-0.5 line-clamp-1 block text-sm-minus font-medium leading-5 text-[color:var(--text-muted)]">
{calc.indication}
</span>
)}
)}{" "}
<span className="mt-1.5 flex flex-wrap items-center gap-1.5">
<MetaPill icon={ListChecks} label={`${calc.items.length} items`} />
<MetaPill icon={Clock3} label={calc.timeEstimate} />
<MetaPill icon={ListChecks} label={`${calc.items.length} items`} />{" "}
<MetaPill icon={Clock3} label={calc.timeEstimate} />{" "}
{compact ? null : <MetaPill icon={Sigma} label={`${calc.minScore}–${calc.maxScore}`} />}
</span>
</span>{" "}
{context ? (
<span className="mt-2 flex min-w-0 items-center gap-1.5 text-2xs font-medium leading-4 text-[color:var(--text-muted)]">
<Search className="size-icon-xs shrink-0 text-[color:var(--decoration-soft)]" aria-hidden="true" />
Expand All @@ -193,7 +193,6 @@ function CalculatorTile({
</span>
) : null}
</span>

<ArrowRight
className="size-icon-md shrink-0 self-center text-[color:var(--decoration-soft)] transition group-hover:translate-x-0.5 group-hover:text-[color:var(--clinical-accent)] motion-reduce:transition-none motion-reduce:group-hover:translate-x-0"
aria-hidden="true"
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 @@ -49,7 +49,7 @@ export { AnswerSkeleton } from "@/components/clinical-dashboard/answer-skeleton"
*/
const sharedHomeCatalogueChips: Partial<Record<AppModeId, { label?: string; ariaLabel: string }>> = {
calculators: { ariaLabel: "Show all calculators" },
sources: { label: "Browse catalogue", ariaLabel: "Browse the source catalogue" },
sources: { label: "Browse catalogue", ariaLabel: "Browse catalogue of sources" },
};

export function SharedHomeEmptyState({
Expand Down
1 change: 0 additions & 1 deletion src/components/clinical-dashboard/auth-panel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -279,7 +279,6 @@ function ProviderButton({
type="button"
onClick={onClick}
disabled={busy}
aria-label={`Continue with ${provider}`}
className={cn(
controlDisabled,
"flex min-h-tap w-full items-center gap-3 rounded-lg border border-[color:var(--border)] bg-[color:var(--surface-lux)] px-3 text-left text-sm font-semibold text-[color:var(--text-heading)] shadow-[var(--shadow-inset)] 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)] disabled:cursor-wait disabled:hover:border-[color:var(--border)] disabled:hover:bg-[color:var(--surface-lux)]",
Expand Down
8 changes: 4 additions & 4 deletions src/components/clinical-dashboard/cross-mode-links.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,7 @@ function CrossModeLinkCard({ link, Icon, query, onModeSearch }: CrossModeLinkCar
const extraBadge = link.badges[0] ?? null;

return (
<article
<div
role="listitem"
data-category-accent={APP_MODE_ACCENT[link.modeId]}
className={cn(sourceCard, "relative flex min-h-12 min-w-0 items-center gap-2.5 px-2.5 py-1.5", "md:max-w-full")}
Expand Down Expand Up @@ -135,7 +135,7 @@ function CrossModeLinkCard({ link, Icon, query, onModeSearch }: CrossModeLinkCar
<ArrowUpRight className="h-4 w-4 shrink-0" aria-hidden />
</Link>
</span>
</article>
</div>
);
}

Expand All @@ -145,7 +145,7 @@ function CrossModeLinkCard({ link, Icon, query, onModeSearch }: CrossModeLinkCar
// space matters (e.g. the documents search page).
function CrossModeLinkChip({ link, Icon, query, onModeSearch }: CrossModeLinkCardProps) {
return (
<article
<div
role="listitem"
data-category-accent={APP_MODE_ACCENT[link.modeId]}
className="flex shrink-0 items-stretch overflow-hidden rounded-lg border border-[color:var(--border-lux)] bg-[color:var(--surface-raised)] shadow-[var(--shadow-inset)] transition hover:border-[color:var(--cat-border)] hover:shadow-[var(--e1)]"
Expand Down Expand Up @@ -177,7 +177,7 @@ function CrossModeLinkChip({ link, Icon, query, onModeSearch }: CrossModeLinkCar
>
<Search className="h-3.5 w-3.5" aria-hidden />
</button>
</article>
</div>
);
}

Expand Down
Loading
Loading