Measure the label column, and the box the columns sit in - #541
Merged
Conversation
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
There was a problem hiding this comment.
🟢 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
ResizeObserverand 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
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>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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
ScoreBreakdowngave 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, becausescoreParts.tsxmay only export components under the react-refresh rule) takes the character count of the longest label, and the width is that manych— onechis 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 clearPENALTY_EPS, so a dropped-out category cannot put the two cards' bars at different x. Pinned bylabelColumn.test.ts(4 cases).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 wasuseColumnWidth, which derived the processing-stage columns fromwindow.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.useColumnWidthnow measures the container it is handed (a callback ref, because both views mount their container only once their payload lands) through aResizeObserver, so a breakpoint change or the modal's own scrollbar re-measures too.clampColumnWidthkeeps its 120 px floor and itscapceiling and now takes the available width rather than the viewport. On top, the dialog goes full-screen belowmd— the same rule and the sameuseMediaQuerythe 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
MarkDialog(⚑ markieren)KorbPanel(Auftragskorb drawer)SetupWizardStepperoverflows the paper by 7 px and is clipped by theoverflowX: '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.RederiveAllDialogwas not opened: it is a bulk-write surface and out of the „opened from the same view" scope.Verification
/verify-frontendagainst 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 <= innerWidthholds at both widths. (At 1440 the document reports 1441 — a 1 px rounding on the letter page'sh1behind the modal, present before this change and unrelated to it.)scrollWidth111 in a 110.56 px box).node scripts/type-floor.mjsclean on all 11 public routes; a sweep inside the open dialog finds nothing under 14 px.ResizeObserverloop warnings across repeated viewport changes.npm run lint·npm run test(296 passing, 4 new) ·npm run buildgreen;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.mdgets the two facts its module inventory would otherwise state wrongly: thatuseColumnWidthmeasures the container, and that the label width lives inquality/labelColumn.ts.🤖 Generated with Claude Code
https://claude.ai/code/session_01UEScQMZFvxxNNyNJYryfa3