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
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
| 2026-08-18 | claude/dictionary-mode-ui-updates-uwoicy (PR #2132) | db3069a5312ac272da47199dcc97662f8f90b08b | Run PR sweep: CI fix + threads + drift | No fix needed: all required in-scope checks (static-pr, safety, coverage, build, production-ui-critical, production-ui, changes) passed on head db3069a5; PR required only failed because lighthouse-budget was cancelled with no real failure. 0 unresolved review threads (3 PR comments are bot rate-limit notices only, no action needed). Branch is behind main (cleanly mergeable per git merge-tree) so attempted git merge origin/main -> push; merge itself was clean with zero conflicts (docs/ledger files only, no code/package-lock changes), but the push was blocked by the local ledger-write-discipline guard (guard-push.mjs): merging main's later reconciliation commit alongside this PR's own still-pending docs/outstanding-issues-inbox/91536913-....json request made the guard see a 'partial reconciliation' (31 items moved to applied/, 1 retained) even though the retained item is this PR's own unrelated new request, not part of main's completed batch. Did not override with SKIP_LEDGER_WRITE_GUARD=1 per hard guardrail to heed pre-push blocks; reset the local unpushed merge commit back to origin's tip so no drift was introduced. PR head remains db3069a5 (unchanged), nothing pushed. | git merge-tree --write-tree origin/main <tip> (clean, exit 0); attempted git merge origin/main + git push (blocked by guard-push.mjs ledger-write-discipline check, not overridden); no provider-backed checks run |
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
| 2026-08-18 | claude/dictionary-mode-ui-updates-uwoicy | 31a2e4aaaf9bb991be51f9bbb735f57505ff70af | Dictionary a11y + zero-state + Browse header replay after #2114 merged mid-branch (PR #2132) | approved | lint, typecheck, test (673 files/7276 tests), ui-dictionary Chromium 6 passed, axe sweep of 6 dictionary routes |
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
{
"version": 2,
"id": "91536913-6cd9-4c61-a109-4d9aa3e76b9d",
"createdOn": "2026-08-18",
"action": "add",
"payload": {
"pri": "P3",
"type": "issue",
"summary": "Shared search chrome fails axe landmark rules on every mode: composer content sits outside any landmark and the universal header renders a second banner",
"detail": "An axe sweep of the six Dictionary routes at 1440px (2026-08-18, PR #2114) found two violations that are not Dictionary's: `region` (3 nodes — the composer label, the `#answer-composer-privacy-warning` span, and the privacy link, all outside any landmark) and `landmark-no-duplicate-banner` + `landmark-unique` on detail routes, where the global `#search` universal header renders a second banner landmark beside the app header. Both are moderate impact, both reproduce on routes that branch never touched (/dictionary/browse, /dictionary/compare), and both live in shared chrome — master-search-header / global-search-shell — so they affect all 13 modes, not one. Repo axe gates only fail on critical/serious, which is why this has never gone red. Next: wrap the composer privacy row in the composer's own landmark (or give it `role=\"group\"` with a label) and decide which of the two headers keeps `banner`; then widen tests/ui-accessibility.spec.ts to assert the landmark rules, not just critical/serious. Deliberately out of scope for PR #2114 — a shared-chrome change under a Dictionary PR is the wrong blast radius.",
"source": "Session 2026-08-18, PR #2114 axe sweep; @axe-core/playwright wcag2a+wcag2aa+wcag21a+wcag21aa+best-practice at 1440x900",
"issueUlid": "01M0AGQ074WJDQ0X6HCNKBS335"
}
}
73 changes: 60 additions & 13 deletions src/components/dictionary/dictionary-catalogue-pages.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -259,13 +259,18 @@ export function DictionarySearchPage() {
})}
</section>
) : (
<div className="px-4 py-12 text-center">
// The empty state wears the same card as the result list. Bare on
// the page background it read as a rendering failure rather than an
// answer, and the advice named a filter even when none was applied.
<div className="border-y border-[color:var(--border)] px-4 py-12 text-center sm:rounded-xl sm:border-x sm:bg-[color:var(--surface)]">
<Search className="mx-auto size-icon-xl text-[color:var(--decoration-soft)]" aria-hidden="true" />
<h2 className="mt-3 text-lg font-extrabold text-[color:var(--text-heading)]">
No matching dictionary entries
</h2>
<p className="mt-1 text-sm text-[color:var(--text-muted)]">
Keep the search term and remove a filter, or try a broader term.
<p className="mx-auto mt-1 max-w-md text-sm text-[color:var(--text-muted)]">
{activeCount
? "Keep the search term and remove a filter, or try a broader term."
: "Try a broader term, check the spelling, or browse the catalogue A–Z."}
</p>
{activeCount ? (
<button
Expand All @@ -275,7 +280,15 @@ export function DictionarySearchPage() {
>
Clear filters
</button>
) : null}
) : (
<Link
href="/dictionary/browse"
className="mt-4 inline-flex min-h-tap items-center gap-1.5 rounded-lg px-4 text-sm font-bold text-[color:var(--clinical-accent)]"
>
Browse all terms
<ArrowRight className="h-4 w-4" aria-hidden="true" />
</Link>
)}
</div>
)}
</div>
Expand Down Expand Up @@ -348,9 +361,6 @@ export function DictionaryBrowsePage() {
<h1 className="mt-1 text-3xl font-extrabold tracking-tight text-[color:var(--text-heading)] sm:text-4xl">
Browse terms
</h1>
<p className="mt-2 text-sm text-[color:var(--text-muted)]">
Scan the same source-linked result system by letter or abbreviation.
</p>
</header>
<div className="border-y border-[color:var(--border)] bg-[color:var(--surface)]">
<div className="mx-auto grid w-full max-w-[76rem] gap-3 px-4 py-3 sm:px-6">
Expand Down Expand Up @@ -388,7 +398,10 @@ export function DictionaryBrowsePage() {
<button
type="button"
onClick={() => setOne("sort", sort === "az" ? "za" : "az", "az")}
className="ml-auto inline-flex min-h-tap items-center gap-2 rounded-lg border border-[color:var(--border)] px-3 text-sm font-bold text-[color:var(--text-muted)] sm:min-h-10"
// `sm:ml-auto`, not `ml-auto`: on a phone the toolbar wraps and the
// pushed-right sort control landed alone on a second line, hanging
// off the right edge instead of reading as part of the group.
className="inline-flex min-h-tap items-center gap-2 rounded-lg border border-[color:var(--border)] px-3 text-sm font-bold text-[color:var(--text-muted)] sm:ml-auto sm:min-h-10"
>
{sort === "az" ? "A–Z" : "Z–A"}
<ChevronDown className="h-4 w-4" aria-hidden="true" />
Expand Down Expand Up @@ -472,6 +485,14 @@ export function DictionaryTopicsPage() {
.filter((kind): kind is DictionaryEntryKind => dictionaryEntryKinds.includes(kind as DictionaryEntryKind));
const sort = searchParams.get("sort") === "za" ? "za" : "az";
const [filterOpen, setFilterOpen] = useState(false);
// Clears both narrowing inputs: `kind` from the filter sheet and a `q` carried
// in from a deep link. Either one alone can empty the list, so a Clear that
// only dropped `kind` would leave a `?q=` visitor stuck on an empty page.
const clearTopicFilters = () =>
replace((next) => {
next.delete("kind");
next.delete("q");
});
const visible = dictionaryTopics
.filter((topic) => topicMatches(topic.slug, query, kinds))
.sort((a, b) => (sort === "az" ? a.title.localeCompare(b.title) : b.title.localeCompare(a.title)));
Expand All @@ -488,7 +509,9 @@ export function DictionaryTopicsPage() {
<>
<InformationPageShell width="bleed" gap={false} testId="dictionary-topics-main">
<div className="mx-auto grid w-full max-w-[76rem] gap-6 px-4 py-6 sm:px-6 lg:grid-cols-[minmax(0,1fr)_18rem] lg:py-8">
<main className="min-w-0">
{/* A `div`, not a `main`: `InformationPageShell` already renders the
route's `<main>`, and a nested one is a duplicate landmark. */}
<div className="min-w-0">
<header>
<p className="text-xs font-extrabold uppercase tracking-kicker text-[color:var(--clinical-accent)]">
Governed collections
Expand Down Expand Up @@ -558,6 +581,30 @@ export function DictionaryTopicsPage() {
</Link>
);
})}
{/* Reachable from a `?kind=`/`?q=` deep link. Without this the
section collapsed to a bare rule under "0 collections". */}
{visible.length ? null : (
<div className="px-4 py-12 text-center">
<Layers3 className="mx-auto size-icon-xl text-[color:var(--decoration-soft)]" aria-hidden="true" />
<h2 className="mt-3 text-lg font-extrabold text-[color:var(--text-heading)]">
No matching collections
</h2>
<p className="mx-auto mt-1 max-w-md text-sm text-[color:var(--text-muted)]">
{kinds.length
? "No governed collection carries a term of that kind."
: "Nothing in the governed collections matches that search."}
</p>
{kinds.length || query ? (
<button
type="button"
onClick={clearTopicFilters}
className="mt-4 min-h-tap rounded-lg px-4 text-sm font-bold text-[color:var(--clinical-accent)]"
>
Show all collections
</button>
) : null}
</div>
)}
</section>
<div className="mt-3 grid border-y border-[color:var(--border)] lg:hidden">
<DisclosureLink
Expand All @@ -571,7 +618,7 @@ export function DictionaryTopicsPage() {
href="/dictionary/compare"
/>
</div>
</main>
</div>
<aside className="hidden border-l border-[color:var(--border)] pl-6 lg:block">
<h2 className="text-sm font-extrabold text-[color:var(--text-heading)]">Browse by kind</h2>
<div className="mt-2 grid">
Expand Down Expand Up @@ -617,7 +664,7 @@ export function DictionaryTopicsPage() {
testId="dictionary-topic-filters"
title="Filter topics"
groups={groups}
onClearAll={kinds.length ? () => replace((next) => next.delete("kind")) : undefined}
onClearAll={kinds.length ? clearTopicFilters : undefined}
summary={{ count: visible.length, noun: visible.length === 1 ? "topic" : "topics" }}
onApply={() => setFilterOpen(false)}
/>
Expand Down Expand Up @@ -715,7 +762,7 @@ export function DictionaryTopicDetailPage({ topicSlug }: { topicSlug: string })
/>
<InformationPageShell width="bleed" gap={false} testId="dictionary-topic-detail-main">
<div className="mx-auto grid w-full max-w-[76rem] gap-6 px-4 py-6 sm:px-6 lg:grid-cols-[minmax(0,1fr)_18rem] lg:py-8">
<main id="dictionary-topic-terms" className="min-w-0 scroll-mt-page-section">
<div id="dictionary-topic-terms" className="min-w-0 scroll-mt-page-section">
<h1 className="text-3xl font-extrabold tracking-tight text-[color:var(--text-heading)] sm:text-4xl">
{topic.title}
</h1>
Expand Down Expand Up @@ -825,7 +872,7 @@ export function DictionaryTopicDetailPage({ topicSlug }: { topicSlug: string })
</div>
</details>
</div>
</main>
</div>
<aside
id="dictionary-topic-details-desktop"
aria-label="Collection details"
Expand Down
23 changes: 14 additions & 9 deletions src/components/dictionary/dictionary-sources-page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -63,17 +63,22 @@ function StatTile({ label, value, hint, compact }: { label: string; value: strin
return (
<div className="bg-[color:var(--surface)] px-4 py-3">
<dt className="text-3xs font-extrabold uppercase tracking-kicker text-[color:var(--text-muted)]">{label}</dt>
{/* `compact` is for the date: at `text-xl` it wrapped to two lines in a
{/* The hint lives inside the `<dd>`, not beside it: a `<div>` inside a
`<dl>` may contain only `<dt>`/`<dd>`, and a sibling `<p>` there is a
serious axe `definition-list` violation.
`compact` is for the date: at `text-xl` it wrapped to two lines in a
half-width phone tile and pulled the row out of alignment. */}
<dd
className={cn(
"mt-1 font-extrabold leading-tight text-[color:var(--text-heading)]",
compact ? "text-base sm:text-lg" : "text-xl",
)}
>
<span className="nums">{value}</span>
<dd className="mt-1">
<span
className={cn(
"nums block font-extrabold leading-tight text-[color:var(--text-heading)]",
compact ? "text-base sm:text-lg" : "text-xl",
)}
>
{value}
</span>
{hint ? <span className="mt-0.5 block text-xs leading-5 text-[color:var(--text-muted)]">{hint}</span> : null}
</dd>
{hint ? <p className="mt-0.5 text-xs leading-5 text-[color:var(--text-muted)]">{hint}</p> : null}
</div>
);
}
Expand Down
6 changes: 4 additions & 2 deletions src/components/dictionary/dictionary-term-page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -91,7 +91,9 @@ export function DictionaryTermPage({ entry }: { entry: DictionaryEntry }) {

<InformationPageShell width="bleed" gap={false} testId="dictionary-term-main">
<div className="mx-auto grid w-full max-w-[78rem] lg:grid-cols-[minmax(0,1fr)_19rem]">
<main className="min-w-0 px-4 py-5 sm:px-6 sm:py-7 lg:px-8">
{/* A `div`, not a `main`: `InformationPageShell` already renders the
route's `<main>`, and a nested one is a duplicate landmark. */}
<div className="min-w-0 px-4 py-5 sm:px-6 sm:py-7 lg:px-8">
<div className="flex flex-wrap items-center gap-x-2 gap-y-1 text-xs font-semibold text-[color:var(--text-muted)]">
<span>{dictionaryKindLabel(entry.kind)}</span>
<span aria-hidden="true">·</span>
Expand Down Expand Up @@ -288,7 +290,7 @@ export function DictionaryTermPage({ entry }: { entry: DictionaryEntry }) {
</div>
</EntrySection>
</div>
</main>
</div>

<aside aria-label="Entry details" className="hidden border-l border-[color:var(--border)] px-6 py-7 lg:block">
<h2 className="text-xs font-extrabold uppercase tracking-kicker text-[color:var(--text-muted)]">
Expand Down
Loading