Frontend UX: typography hierarchy + a real page <h1> per route + per-route document.title #57

Closed
opened 2026-06-06 18:53:06 +00:00 by logaritmisk · 1 comment
Owner

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.

**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._
Author
Owner

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.

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.
Sign in to join this conversation.
No Label
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: logaritmisk/biggus-dickus#57