Skip to content

Measure the label column, and the box the columns sit in - #541

Merged
MarkusNeusinger merged 1 commit into
mainfrom
fix/diagnose-modal-narrow
Sep 5, 2026
Merged

Measure the label column, and the box the columns sit in#541
MarkusNeusinger merged 1 commit into
mainfrom
fix/diagnose-modal-narrow

Conversation

@MarkusNeusinger

Copy link
Copy Markdown
Owner

Two side findings from PR #533, both the same shape: a width that was assumed instead of measured.

The label column ran over its own bar

ScoreBreakdown gave its labels a hard-coded 78 px — about nine characters of the monospace face they are set in. „Deckungslücke", the longest category of the Sütterlin naturalness metric, is 13, so it painted 32 px into the bar it belongs to, at every viewport (measured: label box 78 px, text 110 px, bar starting 8 px after the box).

The column is now derived from the labels themselves: labelColumnChars (its own module, because scoreParts.tsx may only export components under the react-refresh rule) takes the character count of the longest label, and the width is that many ch — one ch is one character in a monospace face, so no DOM measurement is needed and a renamed category re-measures itself. Sized over all categories rather than the rows that happen to clear PENALTY_EPS, so a dropped-out category cannot put the two cards' bars at different x. Pinned by labelColumn.test.ts (4 cases).

Note on the brief: the two script metrics do not have different part names — Kurrent's metric returns no components at all, so its cards carry no per-category breakdown. The six naturalness categories are the whole surface the breakdown ever shows. Verified against the live payloads for loth-1866 and suetterlin-1922.

The modal scrolled sideways at 390 px

Not the score cards — they were already stacking at that width (measured: both at left: 48, one above the other). The cause was useColumnWidth, which derived the processing-stage columns from window.innerWidth − 64. That 64 px is the page's gutter; the modal's paper is a further 32 px narrower and pads another 32 px inside, so at 390 px each column stood at 326 px inside a 279 px box — 47 px too wide. The dialog's scroller measured 342 × 311 and the crop was cut off at the right edge.

useColumnWidth now measures the container it is handed (a callback ref, because both views mount their container only once their payload lands) through a ResizeObserver, so a breakpoint change or the modal's own scrollbar re-measures too. clampColumnWidth keeps its 120 px floor and its cap ceiling and now takes the available width rather than the viewport. On top, the dialog goes full-screen below md — the same rule and the same useMediaQuery the setup wizard already uses — which buys back the 64 px of margin.

Desktop is unchanged: 420 px columns at 1440 px, cards side by side, same paper height.

Sibling dialogs at 390 px, checked in the same breath

Dialog Result
MarkDialog (⚑ markieren) clean, no overflow
KorbPanel (Auftragskorb drawer) clean, no overflow
SetupWizard does not scroll — but its Stepper overflows the paper by 7 px and is clipped by the overflowX: 'hidden' the paper already carries. Different cause (the step-label row, not a column width), pre-existing, and the wizard is the authoring surface — left alone rather than folded in here.

RederiveAllDialog was not opened: it is a bulk-write surface and out of the „opened from the same view" scope.

Verification

/verify-frontend against the live API, admin reads only — no write was performed; every request in the run was a GET (…/diagnostic, …/fit, …/quality, crops), and „Neu ableiten & speichern" was never clicked.

Diagnose modal for a Sütterlin glyph (suetterlin-1922, breakdown present) and a Kurrent glyph (loth-1866, no breakdown), at 1440 × 900 and 390 × 844:

  • document.documentElement.scrollWidth <= innerWidth holds at both widths. (At 1440 the document reports 1441 — a 1 px rounding on the letter page's h1 behind the modal, present before this change and unrelated to it.)
  • The dialog's own scroller: 342 × 311 before at 390 px, 375 × 375 after — no horizontal scroll.
  • Label vs bar rects: text right edge 158 px against a bar starting at 134.56 px before; 127 px against 134.56 px after — non-overlapping, and the label no longer clips (scrollWidth 111 in a 110.56 px box).
  • Type floor: node scripts/type-floor.mjs clean on all 11 public routes; a sweep inside the open dialog finds nothing under 14 px.
  • Console clean (only Vite's dev connect lines and the React DevTools notice); no ResizeObserver loop warnings across repeated viewport changes.
  • npm run lint · npm run test (296 passing, 4 new) · npm run build green; uv run --extra test pytest (2381 passed) and both ruff gates green.

Screenshots (before/after, local): /tmp/kurrentschrift-ui/{before,after}-diagnose-*.png.

Docs

docs/reference/frontend-stack.md gets the two facts its module inventory would otherwise state wrongly: that useColumnWidth measures the container, and that the label width lives in quality/labelColumn.ts.

🤖 Generated with Claude Code

https://claude.ai/code/session_01UEScQMZFvxxNNyNJYryfa3

Two layout faults the Diagnose modal carried since PR #533, both from a
width that was assumed instead of measured.

The penalty breakdown gave its labels a hard-coded 78 px column — about
nine characters of the monospace face they are set in — so the longest
category of the Sütterlin naturalness metric, "Deckungslücke", ran 32 px
past its box and painted straight over the bar it belongs to, at every
viewport. The column is derived from the labels themselves now
(`labelColumnChars`, in `ch` because the face is monospace) and over the
whole category set rather than the rows that clear `PENALTY_EPS`, so the
bars stay aligned across both cards and a renamed category re-measures
itself. Kurrent's metric returns no `components` at all, so this one set
of labels is the whole surface the breakdown ever shows.

The processing-stage columns were sized from `window.innerWidth - 64`,
which is the page's gutter and not the modal's: the paper is a further
32 px narrower and pads another 32 px inside, so at 390 px every column
stood 47 px wider than the box holding it, the dialog scrolled sideways
and the crop was cut off at the right edge. `useColumnWidth` now measures
the container it is handed, through a ResizeObserver so a breakpoint
change or the modal's own scrollbar re-measures too, and the dialog goes
full-screen below `md` like the setup wizard, which buys back the 64 px
of margin. Desktop is untouched: 420 px columns at 1440 px, as before.

The two score cards were already stacking at 390 px; they were not the
cause of the sideways scroll.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UEScQMZFvxxNNyNJYryfa3
Copilot AI balanced review requested due to automatic review settings September 5, 2026 12:49

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟢 Approval recommended

The sizing fixes are coherent, documented, and adequately covered by focused tests and responsive verification.

Pull request overview

This PR fixes two responsive sizing issues in the admin Diagnose modal.

Changes:

  • Derives penalty-label width from localized labels, with unit tests.
  • Measures diagnostic columns via ResizeObserver and uses a full-screen mobile dialog.
  • Updates frontend documentation and changelog.
File summaries
File Description
app/src/sections/admin/diagnostics/DiagnosticDialog.tsx Makes the dialog full-screen below md.
app/src/sections/admin/diagnostics/DiagnosticView.tsx Connects container-based column measurement.
app/src/sections/admin/diagnostics/FitView.tsx Connects container-based fit sizing.
app/src/sections/admin/diagnostics/useColumnWidth.ts Measures available container width responsively.
app/src/sections/admin/quality/scoreParts.tsx Applies label-derived column width.
app/src/sections/admin/quality/labelColumn.ts Calculates the longest normalized label length.
app/src/sections/admin/quality/labelColumn.test.ts Tests label-width calculation.
docs/reference/frontend-stack.md Documents the responsive sizing mechanisms.
changelog.d/diagnose-modal-narrow.md Records both fixes.
Review details
  • Files reviewed: 9/9 changed files
  • Comments generated: 0
  • Review effort level: Balanced

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

@MarkusNeusinger
MarkusNeusinger merged commit 04269ed into main Sep 5, 2026
9 checks passed
@MarkusNeusinger
MarkusNeusinger deleted the fix/diagnose-modal-narrow branch September 5, 2026 12:53
MarkusNeusinger added a commit that referenced this pull request Sep 5, 2026
## What

Brings the register table in `docs/notes/audit-2026-09-02-synthese.md`
up to
date with everything that happened 2026-09-03 → 2026-09-05. Per
`docs/dokument-status.md`, a `Befund-Journal` freezes its findings text
and
only the `Erledigungsstand` section is the living part — this PR touches
only
that section (heading, summary paragraph, the 39-row table, the F6
answer,
and the "Was offen bleibt" narrative), never the 39 findings themselves,
the
tracks, or the ranked write-up below it.

## Verified against `gh pr view` and the merged commit log

- **13** — erledigt: `#513` decides A12 (locked glyph stays offered and
  marked; overwrite needs an explicit "Trotzdem überschreiben").
- **17** — erledigt: `#521` moves §14 word-for-word into
`messjournal.md`
(resolves A17 — as the file's only section, "move before §15" no longer
applies); A16 stays documented as `undeklariert` (the Nachtrag already
in
  the file, carried over by the move).
- **18** — erledigt: `#527` has `pairlab` record-and-replay the
production
  connector instead of hand-mirroring it (89 of 248 joins moved,
  `gen_chamfer` 0.0434→0.0392).
- **23** — erledigt: Trigger-Pfadliste checked live 2026-09-04 —
  `app/prerender/**` is in the `deploy-api` includes; no PR changed it.
- **25** — erledigt: `git ls-remote --heads origin | wc -l` = 1
(2026-09-05);
  the 121 stale branches are gone.
- **28** — erledigt: `#535` (collision fix, measured width, wrap at a 14
px
x-height floor) + `#537` (postcard: 480 chars, size ladder, paragraphs).
- **29** — erledigt: `#539` extends the bundled example-word bank 42→77
words, 64 of 66 glyphs now have a modern bank word (c/q deliberately
keep
  their cluster word).
- **30** — erledigt: `#522` re-measures the CLS cause in production (the
late section mount, not the GLKurrent initial — already loaded before
the
shift) and closes it with `TafelSkeleton` (mobile 0.0969→0.0007, desktop
  0.1125→0.0004); the font-display point is therefore moot, the author
  declines the change (veto).
- **33** — teilweise (was "offen: Entscheid"): F6/A4 decided 2026-09-03;
  `#530` measures both halves — the Anstrich class rule for t/ſ/k/ß is
  measured and rejected (an honest negative), the generated d-Auslauf
  measures green and is recommended but ships off pending the author's
  apply + golden re-bake.
- **35** — teilweise, updated: `#529` adds the "unter dem Boden"
inventory
  flag and re-measures LF12 dry — four lines are under the proof floor
today, not two (`P`, `S`, `p`, `s`); the write stays the author's call.
- **36** — erledigt: `#507` merged.
- **37** — erledigt: `#510` makes `dev_ids()` reproducible
  (`tuple(sorted(...))`, loud failure instead of a silent fallback).
- **39** — erledigt: `#481` (`apiFetch` timeout) + `#515` (lint parking
lot
  69→0, `--max-warnings 0` gate).
- **6** — still teilweise: CSP moved to the nonce path (`#532`), still
  Report-Only, scharf frühestens 2026-09-10 (unchanged).
- **19, 20, 34** left as they were; 20's text is refreshed to note the
plate-nib round is now pre-registered and built (`#526`), verdict still
  open.

New dated block for four post-audit A-items that never had a row: A28 →
`#534` (g/p reading trap + versioned Lesart key), A30 → `#536` + anyplot
`#11221` (site container origin gate, ships off), A29b → `#532` +
anyplot
`#11220` (CSP nonce), A39 → `#533` (Diagnose modal confirmation), plus
their
immediate follow-ups `#540`, `#541`, `#542`, `#538`.

Summary counts recomputed and cross-checked against the table itself: 33
erledigt + 5 teilweise + 1 gemessen & verworfen = 39.

## Not evidenced / left alone

Everything in the task list had a citable PR or a directly reproducible
command — nothing was left unwritten for lack of evidence.

## Verification

- `uv run python -m tools.changelog check --base origin/main` — green
- `uv run python -m tools.docs_budget check` — green (55,712 / 60,852)
- `uv run python -m tools.docs_register check` — green
- `uv run --extra test pytest` — 2433 passed, 18 skipped
- `uv run --extra dev ruff check .` + `ruff format --check .` — clean
- `tests/test_agent_instructions.py` — 58 passed (doc anchors
unaffected;
  nothing links into the edited section by anchor)

No `app/` diff, so no frontend gates.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

https://claude.ai/code/session_01UEScQMZFvxxNNyNJYryfa3

---------

Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants