Skip to content

[UX] members — Release notes ​

Draft from /ux-audit on 2026-07-30 (unattended batch run). Not filed. Repo: bcc-nancy/members · Branch: develop @ 2c22f5a · Files reviewed: 4 Patterns: data-display/timeline

Summary ​

A read-only version-history page: useReleaseNotes eagerly globs bundled release-notes/*.md, renders each through marked into v-html, and a sticky sidebar scroll-spies the active version. It works and its IntersectionObserver is correctly torn down on unmount (NAV-02 passes). The one real defect is heading structure: the injected markdown re-declares each version as its own heading — <h1> in the older files — so the page carries several <h1>s and prints every version number twice. Everything else is polish. No forms, no async fetch, so the FORM-* and the project-wide MSG-06 error-gate theme do not apply here.

Findings ​

1. Injected markdown produces multiple <h1>s and duplicate version headings — Medium · A11Y-04 ​

Where: admin/src/client/views/ReleaseNotes.vue:4,22,27-30 + admin/src/client/release-notes/1.1.3.md:1 (also 1.1.5 / 1.2.0 / 1.2.3 / 1.2.4 / 1.2.6 / 1.3.0) What: The view already renders a page <h1> ("Notes de version", line 4) and, per release, a styled <h2> with the version number (line 22). It then injects release.html verbatim via v-html (lines 27-30). The markdown files are inconsistent: the seven oldest (1.1.3–1.3.0) begin with # (an <h1>), the newer ones with ## (an <h2>). So the rendered page contains one <h1> per old release on top of the page <h1> (WCAG "exactly one h1" broken), and the injected leading heading duplicates the version string the card header already shows — e.g. "Version 1.1.3" (styled h2) immediately followed by a large "1.1.3 — 2025-10-25" heading inside the body. Why it matters: Screen-reader users navigating by heading get a broken outline (several top-level headings, no single document title) and hear each version announced twice; sighted users see the redundant heading as visual clutter directly under the card title — against the "calm, no cognitive overhead" design intent in CLAUDE.md. Fix: Normalise in useReleaseNotes.ts: strip the leading H1/H2 from each parsed file (the version + date are better surfaced as structured fields — see finding 4) before rendering, or downshift all injected headings by two levels so the version card <h2> owns the section and body headings start at <h4>. Do not leave heading level to the discretion of each markdown author.

2. Active-version nav is signalled by colour only, with no programmatic current state — Low · A11Y-07 (proposed) ​

Where: admin/src/client/views/ReleaseNotes.vue:39-50What: The sidebar "Versions" links mark the active entry purely by swapping classes to bg-brand-700 text-white (line 44). There is no aria-current on the active link, so the scroll-spy state is invisible to assistive tech, and the distinction rests on colour alone. The timeline pattern's accessibility guidance is explicit: "do not rely on color alone to convey … selection state." Why it matters: A screen-reader or high-contrast user cannot tell which version they are currently viewing from the nav. Fix: Add :aria-current="release.version === currentVersion ? 'true' : undefined" to the anchor and pair the colour with a non-colour affordance (e.g. a leading marker or weight change).

3. mb-[100vh] leaves a full-viewport blank gap below the content — Low · polish ​

Where: admin/src/client/views/ReleaseNotes.vue:11What: The two-column wrapper carries mb-[100vh], a whole viewport of empty space after the last release. It exists so the final (short) release can scroll to the top for the scroll-spy to highlight it, but it renders as a large dead area the user can scroll into with nothing there. Why it matters: Reads as a layout bug / "did the page break?"; undermines the calm-confidence aesthetic and makes the scrollbar misrepresent content length. Fix: Use scroll-mt/scroll-padding (already present as scroll-mt-6 on the sections) or a bounded spacer sized to 100vh - lastSectionHeight, rather than a static full-viewport margin.

4. Version date is hidden inside the body instead of surfaced in the list/nav — Low · timeline pattern ​

Where: admin/src/client/views/ReleaseNotes.vue:22,49; admin/src/client/composables/useReleaseNotes.ts:26-39What: The composable derives version from the filename only; the release date lives solely inside the markdown heading (## 2.1.0 — 2026-07-12) and is therefore buried in the v-html body. The card header (line 22) and the sidebar nav (line 49) show bare version numbers with no dates. For a chronological history the date is the primary scanning axis (per the timeline pattern's "Time label" anatomy). Why it matters: Staff cannot answer "what changed and when" without reading into each card; the sidebar is a list of opaque numbers. Fix: Parse the date out of the leading heading into the ReleaseNote type and render it in the card header and (compactly) in the sidebar. This also pairs naturally with the finding-1 fix that strips the leading heading.

Unverified ​

  • A11Y-01 (contrast): needs a contrast tool — check text-neutral-500 labels, text-neutral-800/80 inactive nav links, and white-on-brand-700 active nav text.
  • A11Y-06 (responsive / short viewport): the lg:flex-row + lg:sticky lg:top-6 sidebar and the mb-[100vh] gap need a rendered viewport (esp. the single-column mobile stack and short-height behaviour).
  • A11Y-03 focus indicator on the nav anchors: they are real <a href="#…"> (keyboard-reachable, so A11Y-03 largely passes), but no explicit :focus-visible style is set here; whether a visible ring survives depends on global assets/index.css, which was not read.
  • v-html + marked (no sanitiser): not raised as a finding — the source is developer-authored markdown bundled at build time via import.meta.glob, not user input, so there is no injection surface. Would become relevant only if release notes ever come from an untrusted source.

Baseline additions ​

  • A11Y-07 (proposed) — selection / current state must be exposed programmatically (e.g. aria-current) and never by colour alone. (Reconcile with the A11Y-07 family already proposed in PROJECT-LEVEL.md: "selection state not colour-only" / "toggle state exposed programmatically".)
  • Candidate content rule (already implied by MSG-04/timeline, no new ID needed): injected/authored HTML must not re-declare document-level headings the host view already renders — surfaced here as an A11Y-04 instance.

Not applicable / project-level ​

  • CONTENT-01 — not-applicable, see project-level i18n finding (members is single-locale French by design).
  • NAV-03 — fails project-wide (static « BCC Nancy Admin » title, no per-route mechanism); see PROJECT-LEVEL.md. Not re-filed here.
  • MSG-06 (loading gate ignores error branch) — not applicable: this feature has no async fetch. Release notes are eagerly bundled at build time (useReleaseNotes.ts:9), so there is no load/empty/error lifecycle to mishandle.

Cross-project note ​

The specific defect — a page that renders an authored <h1>/heading on top of the view's own page heading — is a members markdown-injection quirk and is unlikely to recur verbatim elsewhere. The generic A11Y-04 "more than one <h1> / broken outline" theme is already confirmed cross-project (playout VTitle hardcodes <h1>; customer-portal auth shells). The colour-only selection state (finding 2) is squarely in the cross-project A11Y-07 family flagged for all four projects in PROJECT-LEVEL.md.