feat(WP-67): merge behandelportal into this repo as a monorepo

Restructures into apps/ssp + apps/behandelportal (two Angular projects)
plus libs/shared + libs/beheer (cross-app libraries), replacing WP-61's
separate sibling repo. That split had already produced real drift: a
hand-vendored copy of the backend's OpenAPI doc, a shared/ui+layout tree
forked and silently diverging (7 files), and beheer + the styles.scss
token bridge duplicated byte-for-byte across both repos.

- git mv the SSP's src/app/* into apps/ssp/; fold shared/, beheer/,
  environments/, the Storybook docs/*.mdx, and styles.scss into
  libs/shared + libs/beheer (all confirmed identical between the two
  repos before merging). auth stays deliberately duplicated per
  ADR-0002 (actor-specific, expected to diverge) - amended there.
- One generated API client (libs/shared), no more vendored swagger.json.
- .dependency-cruiser split into a base factory + one config per app,
  and Storybook into .storybook-ssp/.storybook-behandelportal - both
  forced by the @auth/* alias resolving to different directories per app.
- SiteHeaderComponent/ShellComponent gained HEADER_NAV_ITEMS/
  HEADER_ADMIN_LINKS/DEBUG_PANEL injection tokens so each app supplies
  its own nav/admin-links/dev-panel instead of one being hardcoded.
- CLAUDE.md, ARCHITECTURE.md, dependencies.md, and ADR-0002 updated;
  WP-67 backlog entry documents the full decision trail.

npm run ci green (lint, dep:check x2, 360 tests across ssp/
behandelportal/shared/beheer, both localized builds, backend tests,
snippet + api-client drift); both dev servers, both Storybook
instances, and docker compose verified working.

The old sibling repo (/home/eho/repos/behandelportal) is left
untouched, not deleted.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
eho
2026-08-02 21:01:57 +02:00
co-authored by Claude Sonnet 5
parent d3f3b13345
commit e7156c5132
403 changed files with 7103 additions and 60917 deletions
+69
View File
@@ -0,0 +1,69 @@
import { Meta, Canvas } from '@storybook/addon-docs/blocks';
import * as AlertStories from '../src/ui/alert/alert.stories';
import * as FormFieldStories from '../src/ui/form-field/form-field.stories';
<Meta title="Foundations/Accessibility" />
# Accessibility
No single tool catches every a11y class of bug, so this repo layers four, each catching
what the ones below/above it can't.
## The layers
1. **Axe on every story** (WP-01) — `@storybook/addon-a11y` in the panel, plus
`@storybook/test-runner` + `axe-playwright` gating CI (`npm run test-storybook:ci`).
Catches structural/contrast/ARIA-shape violations on every component, automatically,
as soon as a story exists. Escape hatch: `parameters: { a11y: { disable: true } }`,
only with an inline justification comment + a cross-reference to the WP that will fix
it (see e.g. `task-list.stories.ts`).
2. **Template a11y lint** (WP-17) — `angular-eslint`'s `templateAccessibility` config
(`alt-text`, `label-has-associated-control`, `click`/`mouse-events-have-key-events`,
`interactive-supports-focus`, `valid-aria`, `no-autofocus`, …) running on every inline
template via `angular.processInlineTemplates` (this repo has no `.html` files — every
template is a string in the `@Component` decorator; the processor extracts each one
into a virtual file the template rules can lint). Catches missing alt text, unlabelled
controls, and interactive elements that can't be reached by keyboard — at lint time,
before a story even exists.
3. **Play tests** (WP-16) — Storybook stories assert the wiring axe/lint can't see:
`form-field.stories.ts`'s canonical composition asserts `aria-describedby` joins
`-desc`/`-error` in the right order; `alert.stories.ts` asserts `role="alert"` for
errors vs `role="status"` for info/ok/warning. These run as part of the same
`test-storybook:ci` gate as the axe checks, so a regression fails CI, not just a panel.
4. **Manual WCAG checklist** (`docs/reference/wcag-checklist.md`) — what none of the above can see:
tab order across a whole page, focus traps, 200%-zoom reflow, and how a real screen
reader narrates a flow. A living per-page checklist, not a one-time audit — it already
caught a real bug (a dashboard alert overflowing at 320px) that no automated layer here
would have flagged.
## Component wiring this protects
<Canvas of={FormFieldStories.WithDescriptionAndError} />
The description (`-desc`) and error (`-error`) ids are joined in a pinned order so a
screen reader announces the hint, then the error, never neither. See
`text-input.component.ts`'s `describedBy()`.
<Canvas of={AlertStories.Error} />
Errors are `role="alert"` (assertive — interrupts, because the user needs to know
_now_); info/ok/warning stay `role="status"` (polite) so they don't interrupt whatever
the user is doing. See `alert.component.ts`.
## Route-change focus
Client-side routing has no page (re)load, so a screen reader/keyboard user's focus stays
wherever it was — usually the link they just clicked, now detached from any content that
matters. `shared/layout/route-focus.ts` moves focus to the new page's `<h1>` (every page
has exactly one via `page-shell`) on every navigation after the initial load, deferred via
`afterNextRender` so it doesn't race the view-transition DOM swap. Scroll position resets
the same way (`withInMemoryScrolling`), both wired once in `app.config.ts` — not per page.
## Where the skip register lives
`npm run lint` fails the build on a real template a11y violation, and `test-storybook:ci`
fails it on a real axe violation. Both can be locally disabled — the lint rule via a
normal ESLint disable comment, axe via `parameters: { a11y: { disable: true } }` — but
only with a comment naming _why_ and a cross-reference to the WP expected to remove the
skip (see `docs/project/backlog/WP-13-cibg-gap-register.md`'s marker convention, reused here).
Grep `a11y: { disable: true }` in `*.stories.ts` for the current list.