Flat type scale. Text is almost entirely text-sm (~61×) and text-xs (~27×); only 3 headings exist in the whole app and they're inconsistent (login-page.tsx:34<h1 text-2xl>, object-detail.tsx:60<h2 text-xl>, vocabulary-terms.tsx:52<h3 text-sm uppercase> — an <h3> used as a caption). The list pages (objects, vocab, authorities, fields, search) have no page <h1> at all and no visible page title.
Static browser tab title.web/index.html hardcodes <title>Collection</title> and nothing ever sets document.title (grep → zero). Every tab / history entry / bookmark reads "Collection" — power users keeping multiple object tabs can't tell them apart.
Suggested fix
Define a small type scale (page title / section / body / caption) in the token/shared layer; give each route a semantic <h1> page title with consistent styling; stop using <h3> as an uppercase caption (use the shared label utility).
Set document.title per route (e.g. Objects — {object_number} | {app_name}) via a small hook in the shell / detail pages.
Source: frontend UX/design audit, 2026-06-06.
**Severity: Medium.** _From a frontend UX audit._
## Problems
- **Flat type scale.** Text is almost entirely `text-sm` (~61×) and `text-xs` (~27×); only 3 headings exist in the whole app and they're inconsistent (`login-page.tsx:34` `<h1 text-2xl>`, `object-detail.tsx:60` `<h2 text-xl>`, `vocabulary-terms.tsx:52` `<h3 text-sm uppercase>` — an `<h3>` used as a caption). The list pages (objects, vocab, authorities, fields, search) have **no page `<h1>`** at all and no visible page title.
- **Static browser tab title.** `web/index.html` hardcodes `<title>Collection</title>` and nothing ever sets `document.title` (grep → zero). Every tab / history entry / bookmark reads "Collection" — power users keeping multiple object tabs can't tell them apart.
## Suggested fix
- Define a small type scale (page title / section / body / caption) in the token/shared layer; give each route a semantic `<h1>` page title with consistent styling; stop using `<h3>` as an uppercase caption (use the shared label utility).
- Set `document.title` per route (e.g. `Objects — {object_number} | {app_name}`) via a small hook in the shell / detail pages.
_Source: frontend UX/design audit, 2026-06-06._
New PageTitle component (web/src/components/ui/page-title.tsx) — a semantic <h1> at text-2xl font-semibold tracking-tight. Each AppShell route now renders exactly one page <h1> (objects, object-new, vocabularies, authorities, fields, search), reusing existing i18n keys (nav.* / objects.new / fields.title) — no new strings.
Type scale is now: page title (h1) · section (h2, the object-detail object_name) · body (text-sm) · caption (.label-caption).
Fixed the misused <h3> in vocabulary-terms → <div className="label-caption"> (it's a caption, not a heading). Exactly one <h1> per page preserved across all routes (master-detail: the list/search page owns the <h1>, the detail pane keeps its <h2>).
Per-route document.title:
New useDocumentTitle hook (web/src/lib/use-document-title.ts) sets "{Page} | {AppName}" (app name from useConfig), and restores the prior title on unmount.
Object-detail routes (/objects/:id, /search/:id) override the tab to the object's object_number and revert when closed — so a curator's many object tabs are now distinguishable. (Resolved cleanly via an inner ObjectDetailLoaded component so the hook only runs post-load.)
Login sets its own tab title (app.name, standalone/pre-auth).
Pure additive UI — no layout/column restructure, no new dependency, en/sv parity untouched. Gate green: typecheck, lint, 186 tests, build, check:size (184.7 KB gz), check:colors; no codename.
Follow-ups (not in scope): header breadcrumb/wayfinding + global search entry (#54); broader per-screen density/spacing redesign.
Done — merged to `main` (`9b3a587`).
**Typography hierarchy + page `<h1>`:**
- New `PageTitle` component (`web/src/components/ui/page-title.tsx`) — a semantic `<h1>` at `text-2xl font-semibold tracking-tight`. Each AppShell route now renders exactly one page `<h1>` (objects, object-new, vocabularies, authorities, fields, search), reusing existing i18n keys (`nav.*` / `objects.new` / `fields.title`) — **no new strings**.
- Type scale is now: page title (h1) · section (h2, the object-detail `object_name`) · body (`text-sm`) · caption (`.label-caption`).
- Fixed the misused `<h3>` in `vocabulary-terms` → `<div className="label-caption">` (it's a caption, not a heading). Exactly one `<h1>` per page preserved across all routes (master-detail: the list/search page owns the `<h1>`, the detail pane keeps its `<h2>`).
**Per-route `document.title`:**
- New `useDocumentTitle` hook (`web/src/lib/use-document-title.ts`) sets `"{Page} | {AppName}"` (app name from `useConfig`), and **restores the prior title on unmount**.
- Object-detail routes (`/objects/:id`, `/search/:id`) override the tab to the object's **`object_number`** and revert when closed — so a curator's many object tabs are now distinguishable. (Resolved cleanly via an inner `ObjectDetailLoaded` component so the hook only runs post-load.)
- Login sets its own tab title (`app.name`, standalone/pre-auth).
Pure additive UI — no layout/column restructure, no new dependency, en/sv parity untouched. Gate green: typecheck, lint, 186 tests, build, check:size (184.7 KB gz), check:colors; no codename.
Follow-ups (not in scope): header breadcrumb/wayfinding + global search entry (#54); broader per-screen density/spacing redesign.
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.
Severity: Medium. From a frontend UX audit.
Problems
text-sm(~61×) andtext-xs(~27×); only 3 headings exist in the whole app and they're inconsistent (login-page.tsx:34<h1 text-2xl>,object-detail.tsx:60<h2 text-xl>,vocabulary-terms.tsx:52<h3 text-sm uppercase>— an<h3>used as a caption). The list pages (objects, vocab, authorities, fields, search) have no page<h1>at all and no visible page title.web/index.htmlhardcodes<title>Collection</title>and nothing ever setsdocument.title(grep → zero). Every tab / history entry / bookmark reads "Collection" — power users keeping multiple object tabs can't tell them apart.Suggested fix
<h1>page title with consistent styling; stop using<h3>as an uppercase caption (use the shared label utility).document.titleper route (e.g.Objects — {object_number} | {app_name}) via a small hook in the shell / detail pages.Source: frontend UX/design audit, 2026-06-06.
Done — merged to
main(9b3a587).Typography hierarchy + page
<h1>:PageTitlecomponent (web/src/components/ui/page-title.tsx) — a semantic<h1>attext-2xl font-semibold tracking-tight. Each AppShell route now renders exactly one page<h1>(objects, object-new, vocabularies, authorities, fields, search), reusing existing i18n keys (nav.*/objects.new/fields.title) — no new strings.object_name) · body (text-sm) · caption (.label-caption).<h3>invocabulary-terms→<div className="label-caption">(it's a caption, not a heading). Exactly one<h1>per page preserved across all routes (master-detail: the list/search page owns the<h1>, the detail pane keeps its<h2>).Per-route
document.title:useDocumentTitlehook (web/src/lib/use-document-title.ts) sets"{Page} | {AppName}"(app name fromuseConfig), and restores the prior title on unmount./objects/:id,/search/:id) override the tab to the object'sobject_numberand revert when closed — so a curator's many object tabs are now distinguishable. (Resolved cleanly via an innerObjectDetailLoadedcomponent so the hook only runs post-load.)app.name, standalone/pre-auth).Pure additive UI — no layout/column restructure, no new dependency, en/sv parity untouched. Gate green: typecheck, lint, 186 tests, build, check:size (184.7 KB gz), check:colors; no codename.
Follow-ups (not in scope): header breadcrumb/wayfinding + global search entry (#54); broader per-screen density/spacing redesign.