Skip to content

Responsive: adapt every screen from 768 to 2560, and fix what manual testing found - #94

Open
lionelchamorro wants to merge 134 commits into
developfrom
feature/responsive-screens
Open

Responsive: adapt every screen from 768 to 2560, and fix what manual testing found#94
lionelchamorro wants to merge 134 commits into
developfrom
feature/responsive-screens

Conversation

@lionelchamorro

@lionelchamorro lionelchamorro commented Aug 3, 2026

Copy link
Copy Markdown
Contributor

Two batches of work on the same branch. 124 commits over develop.

Batch 1 — make the screens responsive

The app was built against a single 1440px Figma frame, and that number was
frozen as maxWidth: "[1015px]" in the two layouts wrapping almost every
screen. On a 1920 or 2560 monitor two thirds of the width went unused, and below
1440 the two-panel layouts compressed with no collapse rule at all.

Design published a Pages → Responsive page (15 frames: the three step-3
validation screens, with and without side panel, at 1440/1920/2560). The
measurements came from frame metadata, not from eyeballing screenshots:

  • content grows to 1824px, side panels are a fixed 479px (flexShrink: 0)
  • three reading-column rules — min(100% - 96px, 1824px) without a panel,
    the same formula capped at 1672px for the transcript with a panel, and a
    genuinely percentage-based min(88%, 1520px) for documents with a panel
  • the toolbar belongs to the pane, not the viewport, so it shrinks when the
    panel opens

The 1440→2560 upscale needed no media queries: min() + max-width +
flexShrink: 0 resolve it declaratively, since the caps are relative to the
pane. Breakpoints were added only for the collapse below 1440, which has no
design and is marked as extrapolated.

New foundations: a desktop (1440px) breakpoint and sizes.content.* /
sizes.panel.* tokens in panda.config.ts, plus two primitives —
ReadingColumn and SidePanelColumn — that concentrate the rules so the rest
of the work is application rather than repetition.

Batch 2 — fix the 18 findings from manual testing

A manual pass across six widths (768 · 1024 · 1366 · 1440 · 1920 · 2560) found
13 responsive defects and 5 functional bugs. All are addressed except one that
cannot be fixed in this repo (below). Highlights, since several turned out not
to be what the report thought:

  • The side panel overlaid the content below lg instead of stacking, and at
    768 the "Modo Edición" toggle and the rename pencil were unreachable — you
    could enter edit mode and not leave it. Verified by hit-test, not by eye.
    Set de Datos turned out to have a different cause entirely: it never mounts
    that panel, so what looked like an overlay was its own grid splitting the
    viewport 50/50 in flow.
  • The anonymizer export was blocked, and the error message was one hardcoded
    string used for all nine issue codes — so it sent the report chasing offsets
    when the real cause was missing_canonical_entity_id. A staging run produced
    140 of those out of 187 labels and zero offset-shaped ones, and those same 140
    round-tripped to a valid ODT. The field is optional in the backend's own
    schemas, so treating its absence as fatal contradicted the schema it came
    from.
  • Three reported symptoms in the onboarding grid were one cause: the cards'
    200px non-shrinking image against 1fr tracks, which is minmax(auto, 1fr).
  • Overriding a @aymurai/ui style needs deliberate specificity: main.tsx
    imports index.css before the library's stylesheet and both declare
    @layer utilities, so at equal specificity the library wins and a plain
    className override is inert.

Fixes for things found while fixing the above, not in the original report: a
blank export error message, a contentEditable commit running through a stale
handler, a footer wiping the toast offset during route transitions, and a Radix
Select flipping between controlled and uncontrolled so re-selecting a cleared
label silently no-opped forever.

Verification

  • pnpm validate — biome + tsc --noEmit
  • pnpm test493 tests / 79 files (380 / 70 before this work)
  • pnpm test:responsive216 component tests across the six widths (18 before)
  • pnpm test:e2e:console — new; drives the real app and fails on any console error

The component tests assert numbers, not pixels: panel widths, column caps,
horizontal-overflow detection, hit-testability, and zero vertical skew between
sibling cards. Comparison against the Figma frames is visual and manual — the
frames mock browser chrome and use different data, so a pixel diff would be all
false positives.

Still open

  • The header stepper overlaps the feature name on narrow windows. It lives
    in @aymurai/ui, which ships only dist/ and exposes className on the
    AppHeader root alone, while the collision is between two of its internal
    children. Mitigated here by hiding the stepper below a per-tool, measured
    cutoff (840/880/920/1080 by title length) — which trades the overlap for a
    missing "step N of 4" and is not the fix. The upstream work is a compact
    stepper that keeps the wayfinding.
  • Most detected entities never reach the review panel. useEntityGroups
    skips mentions without a canonical_entity_id, which is the normal case, not
    a pending state — 140 of 187 in the test document. The user reviews 47 and 187
    get anonymized. Needs a UX decision before code, so it is deliberately not in
    this PR.
  • One shared isDismissed flag drives both the success and the error callout on
    the process screens, so dismissing one pre-dismisses the other.

Note for reviewers

The planning documents, per-issue contracts and staging evidence for this work
live under tasks/ and docs/responsive-test-2026-07-30/, both gitignored, so
they are not in this diff. Ask if you want any of it moved into the repo.

🤖 Generated with Claude Code

Summary by Sourcery

Introduce summarizer feature and responsive layout primitives, fix multiple UX and data-handling bugs in anonymizer and voice-to-text flows, and complete the migration from Stitches to Panda CSS with new tests and tooling.

New Features:

  • Add Summarizer flow with process, validation, and finish screens, summary context, streaming backend integration, and export to TXT/ODT/PDF.
  • Introduce ReadingColumn and SidePanelColumn layout primitives plus new size and breakpoint tokens to drive responsive desktop layouts across features.
  • Add a not-found screen using the app shell and localized copy for unknown routes.
  • Wire Summarizer into navigation, headers, feature menu, and home screen alongside existing tools.

Bug Fixes:

  • Prevent voice-to-text and dataset process screens from showing processing copy once runs are completed, and distinguish finished vs error states with separate callouts and titles.
  • Ensure anonymizer export validation reports only non-fatal missing canonical_entity_id issues, uses accurate per-issue-family messages, and logs structured summaries instead of blocking export.
  • Fix persona-speaker renaming and creation so labels and colours are stable per id, numbering can leave gaps without collisions, and initials remain distinct for double-digit speakers.
  • Resolve the Chromium audio duration Infinity/NaN cases in useAudioSnippet, avoiding silent 0-second previews and stale play-state when files change mid-play.
  • Stop the label manager close button and anon toolbar controls from being visually overlapped or misaligned at narrow widths, and make label controls accessible only when annotable.
  • Clamp anonymizer entity context menus inside the viewport so they don’t render off-screen near edges.
  • Keep toast notifications clear of footer controls by tracking footer height and applying a global offset.
  • Avoid stale summary saves and colliding document ids in summarizer validation so edits and per-document state persist correctly.

Enhancements:

  • Make all main flows (dataset, anonymizer, voice-to-text, home) respect new reading-column widths and responsive grids, eliminating horizontal overflow at target widths.
  • Refine toolbar and player alignment to share the same reading insets, keeping chrome lined up with content in transcription editor and anonymizer.
  • Stabilize turn-side-panel tests and reducer behaviors around persona numbering, initials, and turn-speaker identity with richer regression coverage.
  • Improve document search UX in the summarizer validation screen via a dedicated DocumentSearchPanel with keyboard-accessible Toolbar integration.
  • Replace router view transitions with safer defaults to avoid unhandled rejections, and add a robust unhandled rejection test harness.
  • Complete Stitches-to-Panda migration for core components (tabs, text, stack, FileCheck, spinner, validation form, layouts) with precise token mappings and documented pitfalls.
  • Update i18n locale files to cover new summarizer and process states, anonymizer search/bar labels, and shared document-format strings.

Build:

  • Pin @aymurai/ui to a specific commit SHA and update pnpm workspace allowBuilds to match the lockfile tarball.
  • Add Playwright component-test and e2e configs and scripts, wiring CT Vite aliases and dev-server-based e2e runs.
  • Tighten pnpm lint scripts to scope explicitly to the project tree and declare Node/pnpm engine versions.

Documentation:

  • Update CLAUDE.md, Panda CSS and Radix UI rules, and aymurai.md to reflect completed Panda migration, @aymurai/ui Radix wrappers, and new layout/token guidance.
  • Add CONVENTIONS.md capturing gate, styling, data, routing, and testing rules for future changes.

Tests:

  • Add extensive vitest coverage for transcription reducer persona behavior, turn-side-panel races, summarizer process/validation, export validation messaging, audio snippet edge cases, reading-column and side-panel primitives, label-manager header, FileCheck, Tab/Stack, summary validation client, and router options.
  • Introduce Playwright CT specs for transcription editor, anon toolbar and file annotator, dataset validation layout, how-it-works grids and modal, header stepper overlap, preview card width, side-panel stacking, toast-footers, FileCheck grid, and NotFound behavior.
  • Add a Playwright e2e spec to assert no console errors or unhandled rejections during real route navigation.

Chores:

  • Add a jsdom-compatible localStorage shim for newer Node versions to keep tests deterministic across environments.
  • Refine lefthook forbidden-pattern checks to use portable grep and cover restricted imports and debug constructs.

jansaldo and others added 30 commits July 21, 2026 17:50
The single-document preview redesign removed the last consumers of the
selection machinery: file-preview no longer toggles `selected` and the
route preview no longer filters unselected files. Remove the now-orphaned
action creators, their action types, and the reducer cases they backed.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
formatDuration accumulated everything into minutes, so a 2h05m clip read
'125 min. 24 seg.'. Break out an hours component ('1 h.' singular, 'N hs.'
plural) once the duration reaches an hour, keeping the existing minute/second
rendering below that.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The inner Stack wrapped a single TextField, so its gap did nothing —
TextField already owns its label-to-box spacing and spans the full column.
Render it directly under the form's Stack instead.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
lionelchamorro and others added 26 commits July 30, 2026 13:59
G1 tickets T1-T3 from the manual responsive testing report (issues 04 and 11):
at 768px SidePanelColumn was `position: absolute` below the `lg` breakpoint, so
it mounted over the content with no space reserved and no way out. Hit-testing
confirmed the "Modo Edición" switch and the "Editar título" pencil were both
unreachable — you could enter edit mode and not leave it.

Stacked (option a) rather than a drawer: a drawer needs Radix's portal,
role=dialog and focus trap, which is DOM structure decided by JS, and the
contract for this work forbids a useBreakpoint hook. Stacking is pure CSS over
the open/close state that already exists in React.

Set de Datos was left alone: `isAnnotable={false}` means labelManagerOpen is
always false there, so SidePanelColumn never mounts visible. What the report saw
as a "lower overlay" is validate-dataset's own gridTemplateRows splitting the
viewport 50/50 in flow — same symptom, different mechanism, separate fix.

New coverage: playwright/hit-test.ts plus side-panel-stacking.spec.tsx assert
reachability, not just widths. The three existing specs that hardcoded the 768px
overlay contract had their expectations updated — an intentional behaviour change
that raises coverage, not a softened gate.

Gates verified green: pnpm validate exit 0, 380 tests / 70 files passing.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Third adversarial review pass. The G1 fix (e81c4e2) has two halves that only
work together: SidePanelColumn became position:static with width:full,
maxWidth:[none] and flexShrink:0 below lg, and the two flex parents that hold
it switch to flexDirection:column there so a row is actually reserved.

side-panel-column.test.tsx pins the panel half exhaustively — seven cases
covering position, width, maxWidth, maxHeight, overflow, borders and
flexShrink. No vitest test asserted the parent half. Deleting
flexDirection={{ base: "column", lg: "row" }} from FileAnnotator's HStack and
flattening the editor's `content` to flexDir:"row" left all 380 tests passing,
and no type or lint rule can express "this prop must exist", so pnpm validate
is blind to it too. Only pnpm test:responsive would have caught it, and neither
gate runs it.

That mutation is worse than the bug G1 fixed: below lg the panel goes back to
being a sibling in a row, but now as a width:100%, flexShrink:0 item, so at 768
it demands the full viewport width next to a document whose container carries
minWidth:520px — the pair overflows an overflow:hidden box and the panel is
clipped off the right edge, reproducing Issues 04/11 (panel unreachable at
768) with no gate noticing.

Both new cases assert flex-d_column + lg:flex-d_row on the panel's parent and,
to catch a flat revert, not.toContain("flex-d_row"). Verified both ways: they
fail under the mutation above and pass on the restored tree.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
pnpm-workspace.yaml still used the v10-era onlyBuiltDependencies-shaped
allowBuilds entry, which broke `pnpm install` on pnpm 11 (the git-hosted
@aymurai/ui build script was rejected with ERR_PNPM_GIT_DEP_PREPARE_NOT_ALLOWED).
Updates the allowlist entry to the commit hash pnpm 11 actually resolves,
drops the now-ignored package.json "pnpm" field, and pins packageManager
so every machine installs with the same pnpm version.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…st guards

Completes G1 (issues 04 and 11 from the manual responsive report) with T4/T5 and
the integrator's repairs from the governance pass.

Set de Datos never went through SidePanelColumn at all — `isAnnotable={false}`
means labelManagerOpen is always false there, so what the report saw as a "lower
overlay" was validate-dataset's own gridTemplateRows splitting the viewport
50/50 in flow. Base tier is now `minmax(0, 62%) minmax(0, 1fr)` so the document
pane keeps usable height at 768; the `lg` tier and every gridTemplateColumns
value are untouched.

Test guards added for the parts the deterministic gates could not see:
- validate-dataset/index.test.tsx asserted only grid-tc_* (columns); it now
  guards grid-tr_* too, so reverting the 62/38 split fails a test.
- the CT harness that calibrates criterion 3 omitted the Header, which ate most
  of the calibration margin — fixed in the fixture.

Gates: pnpm validate exit 0, 384 tests / 70 files, 48 CT tests across the six
viewports all passing.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…modal

G2 (issues 02 and 03 from the manual responsive report). Three reported symptoms
turned out to be one root cause: the tutorial cards' 200px non-shrinking image
against `1fr` tracks, which is `minmax(auto, 1fr)`. That also explains the 7px
page overflow on /app/DATA_SET/onboarding — the nodes the report flagged are the
cards.

Corrections to the premise, verified against node_modules/@aymurai/ui/dist:
TutorialGrid does have a collapse breakpoint — `{base: 1fr, md: repeat(2,1fr)}`
— but Panda's `md` is min-width 768px, so 768 is exactly the first width that
does NOT collapse. And the vertical centring lives on the library's card recipe,
not the row, where a root className never reaches.

The override needs specificity, not just a className. main.tsx imports
index.css before @aymurai/ui/styles.css and both declare `@layer utilities`, so
at equal specificity the library wins: a plain gridTemplateColumns override is
inert at 768. Both properties are now pinned behind a self-referencing selector
— (0,2,0), order-independent — with regression tests that fail if the doubled
class or the direct-child selector is removed, because neither is visible to the
deterministic gates otherwise.

TutorialDialog exposes no className, so the modal is recomposed from the
library's own Dialog parts (the same pattern label-manager/merge-dialog.tsx
already uses). No new dependency: @radix-ui/react-dialog is not one, that
wrapper lives inside @aymurai/ui.

.claude/rules/radix-ui.md was documenting ten local `ui/` wrappers of which two
exist; corrected as part of the governance pass.

Gates: pnpm validate exit 0, 390 tests / 70 files, 96 CT tests across the six
viewports.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The governance pass on G2 flagged .claude/rules/radix-ui.md as contract drift and
fixed it; CLAUDE.md's pointer to the same claim was outside that finding's scope.
Both now say the same thing: Dialog/Popover/Select/Switch/Tooltip come from
@aymurai/ui, and components/ui/ holds only scroll-area and back-button.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Match transcribeStream's cancellation so Detener reports 'stopped', not 'error'.
Matches ArchiveProgress's stopped state instead of hiding the button.
Matches the transcription export's paragraph gaps, which used to render tight.
It was silently dropped, unlike the ODT/PDF formats and the transcription export.
Dedupes copy that was repeated verbatim in common.ts, unlike the other feature cards.
…lose button

G3 (issues 05 and 06 from the manual responsive report).

The divider was the real defect in issue 05: @aymurai/ui's Toolbar renders the
1px divider in rightSlot's wrapper unconditionally for context='anonimizador'
(verified in the compiled component, not the docblock), so when the row wrapped
at 1024/1366 the separator ended up alone at the start of the second line with
nothing to its left. A separator only means something when it separates two
things on the same line.

Issue 06: the close button was a typographic `X` — a glyph of the Archivo font
inside a <button> — sitting 16px above the tabs' vertical centre while the rest
of the UI uses SVG icons. Now a phosphor <X> with an i18n'd aria-label and a
tooltip, centred with the tabs.

The i18n strings came out of the governance pass, not the original change:
SearchBar had four hardcoded Spanish strings ('Buscar en el documento', 'Aplicar
etiquetas', 'Etiqueta', 'Gestor de etiquetas') even though G3 names that file and
lists the i18next rule as an explicit constraint. They now live in
constants/i18n/locales/es/anonymizer.ts.

Gates: pnpm validate exit 0, 396 tests / 71 files, 108 CT tests across the six
viewports.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
G8 (F1, F2, F3 from the manual responsive report), on its own branch.

F1 was not what the report said. `AnonymizerExportValidationError` used one
hardcoded string — "hay entidades con offsets inválidos, duplicados o
solapados" — for all nine issue codes, so the message sent the report chasing
the wrong question. A staging run of docs/SIN_ANONIMIZAR.docx (125 paragraphs,
full predict → disambiguate) produced 140 `missing_canonical_entity_id` issues
out of 187 labels and ZERO offset-shaped ones; the backend only assigns
canonical_entity_id to 47, all FECHA. Those same 140 ungrouped labels
round-tripped through /anonymizer/anonymize-document as HTTP 200 with a valid
44283-byte ODT, and the field is optional in the backend's own schemas. So
treating its absence as fatal contradicted the schema it came from: it is no
longer in the fatal list, and the message is now built from the fatal issues
actually present.

That fix initially reintroduced the same defect silently: before i18n.init(),
i18n.t() returns undefined — not the key, as the original docblock claimed — so
the message came out blank. Every family now falls back to the locale data
module, with regression tests deliberately kept in a file that does not import
@/constants/i18n, since that is the condition under test.

F2/F3: useAudioSnippet could report `playing` for a file that is not playing
when a play() promise settled after the [file] effect tore its element down;
both handlers now bail when audioRef.current is no longer their element. The
process screens no longer show a progress banner over already-rendered content,
or a success banner above a failed row.

Verified end-to-end, not by fixture: real app in Chromium against staging, the
document driven through all six screens without editing an entity, both
downloads intercepted — ODT 44281 bytes (OpenDocument Text), PDF 138978 bytes
(20 pages). Written up in tasks/responsive-fixes/G8-e2e-anonymizer-staging.md.

Gates: pnpm validate exit 0, 440 tests / 75 files, 108 CT tests.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
G4 (issues 07, 12, 13 plus the 2560px and form-input observations from the
manual responsive report). One theme: measurements in px that never grow or
shrink, so at 2560 there are hundreds of empty pixels beside an element that is
cutting off its own content.

- the export error message was clamped to 150px with nowrap + ellipsis and no
  `title`, while the text needed 512 — the user saw that something failed and
  could not find out what
- the transcription title input was a fixed 361px at every width; with a
  108-character title its scrollWidth reached 2211px against 361 visible, and at
  2560 there were 1312px free beside it
- a 33-character person name overflowed the "Turno seleccionado" card by 24px
  and put a horizontal scrollbar inside the panel

The governance pass found a runtime defect that has nothing to do with widths:
EditableTurnText kept stale callback props across memoized rerenders, so a
contentEditable commit could run through an outdated handler. Both the critic and
the adversary reproduced it independently. Fixed by including the callbacks in
the memo comparison and stabilising TurnBlock's commit handler.

Gates: pnpm validate exit 0, 401 tests / 71 files, 150 CT tests across the six
viewports.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…ast clear

G5 (issues 08 and 09 from the manual responsive report).

Issue 08: the transcription block carried its own centred max-width while the
search bar and the player ran full-bleed, so the left edges did not line up and
the gap changed with the width — 24px out at 1366, 313px out at 2560. The
transcription was the one obeying the Figma contract (rule A, min(100% - 96px,
1824px) centred, which lands at x=371 at 2560); the toolbar and player are now
in the same reading column instead of the viewport.

Issue 09: the toast rendered inside the footer row, overlapping, covering 61% of
"Volver" without reserving any space. Footers now publish their measured height
as a document-level offset that the toast container clears.

The governance pass found a real defect in that mechanism: two Footer instances
can coexist during a route view transition, and the unmounting one wiped the
global offset the live one still owned — reproduced independently by the critic
and the adversary. Footers now publish through a registry, and unmount cleanup
recomputes the offset from whatever footers remain.

Gates: pnpm validate exit 0, 412 tests / 71 files, 186 CT tests across the six
viewports.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
G7 T1 (F4 from the manual responsive report). Rapid clicks on "+ Nuevo" created
several personas sharing one label — four chips all reading "Persona 4", then
three reading "Persona 7" — because `nextPersonaLabel(speakers)` and
`SPEAKER_PALETTE[speakers.length]` both read the prop closure, so N clicks that
land before a rerender all compute from the same stale list. The fully-formed
speaker was then handed to a reducer that only appended it.

The derivation now happens inside the reducer, against the state it is about to
extend, so the label and colour come from the list as it actually is at that
moment rather than as the last render saw it.

Landed unverified by the flow: the G7 run hit its 8-hour duration budget during
rate-limit backoff, right after this ticket was implemented and before
ticket_qa. Committed as a checkpoint because both gates are green — pnpm
validate exit 0, 423 tests / 71 files — and the relaunch's reviewers audit the
tree including this.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
G7 (F4 and F5 from the manual responsive report), completing the checkpoint
landed in a4e8efb.

F4 had a second creation path the first fix did not cover: speaker-picker.tsx's
`ensureSpeaker` carried the same race that turn-side-panel.tsx did — it was even
documented in a comment as out of scope — so rapid creation there still produced
personas sharing a label. Both paths now derive label and colour inside the
reducer, against the state being extended.

F5 was not only in the panel: asrMapper.ts:57-65 built speaker initials as
`P${no}`.slice(...), so every persona from index 10 up rendered the same "P1"
badge on the ASR import path, before the UI was ever involved. Initials are now
derived so double digits stay distinguishable.

Both were found by the governance pass, which reproduced each one rather than
reading the diff — the panel fix alone would have shipped looking complete.

Gates: pnpm validate exit 0, 440 tests / 72 files, 186 CT tests.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
… errors

G6 (issue 10 and the console-noise note from the manual responsive report).

__root.tsx defined no notFoundComponent, so any unknown route fell through to
TanStack Router's default — the literal string "Not Found", with no layout, no
header and no styles. There is now a 404 inside the theme with a link back to
/home/features, its copy in i18next like the rest of the UI.

The InvalidStateError fired on every step navigation could not be caught by the
component tests, since it needs the real router and real transitions. Hence
playwright-e2e.config.ts and `pnpm test:e2e:console`, which starts the dev
server itself, walks the flow and fails on any console error or pageerror.
Deliberately a separate config from the CT suite: different harness, different
lifecycle. Verified passing.

The governance pass reproduced a horizontal overflow on the new 404 at a real
dev server rather than in a fixture; fixed.

Gates: pnpm validate exit 0, 446 tests / 75 files, 192 CT tests, and the new
e2e console check green.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
fix/pipeline-bugs branched off 8331992 and carried G8 alone: the anonymizer
export fix (F1), the audio-snippet stale-play guard, and the process-screen
state fixes. Keeping it separate made its diff reviewable, but it also meant F1
was invisible to anyone testing this branch — a QA retest on 2026-08-02 found
the export still blocked here, correctly, because the fix lived only there.

Merging so a single tree carries every fix from the manual-testing batch.
Two pieces of setup scaffolding that were always meant to be temporary.

- pnpm-workspace.yaml: remove the 31 `@tiptap/*@3.29.2` minimumReleaseAge
  excludes. They were added because those packages were published inside the
  24h window and blocked `pnpm install` outright; 3.29.2 is now 5 days old, so
  the entries are dead weight. Verified: install succeeds without them and the
  lockfile does not move.
- package.json: pin @aymurai/ui to commit 93821cce rather than the mutable
  `#v0.5.0` tag. That tag already moved once, which desynced the allowBuilds
  key from the lockfile and broke install for everyone. Resolution is
  unchanged (same tarball SHA), so this is inert today and removes the failure
  class going forward.
- features.md: drop the console.log bullet for validate-dataset/form-group,
  which was fixed independently, and refresh the counts it cites.

Gates after the change: pnpm test 79 files / 493 tests, pnpm validate exit 0.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Issue 01 of docs/responsive-test-2026-07-30/REPORT.md, open since 30/07 because
the real fix has to happen in @aymurai/ui: AppHeader centres the stepper with
position:absolute on the header root, so it does not participate in the flex row
and lands on top of a long featureName. The library ships only dist/ and exposes
className on the header root alone, so the two children cannot be reflowed from
here. Mitigate by hiding the stepper on narrow windows, which removes the
overlap and lets the title render in full.

- src/renderer/src/index.css: unlayered media query, hung off the role="list" +
  aria-label="Progress" markup Stepper renders itself. Semantic markup rather
  than a hashed aym-* class, so it survives a library rebuild. Unlayered on
  purpose: Panda emits globalCss into @layer base, while the library's aym-*
  classes live in @layer utilities — later in the layer order, so a globalCss
  rule would have lost the cascade regardless of selector specificity.
- Breakpoint is xl (1280), not lg (1024). The report called 1024 healthy, but it
  measured "Anonimizador"; sweeping the longest title, "Resumen de Documento",
  the overlap only reaches zero at 1048px (12px at 1024, 2px at 1044). 1048 is
  not used as the cutoff because it leaves no margin for a font fallback or a
  longer future feature name.
- playwright/header-stepper.spec.tsx: 4 tools x 6 widths. Asserts computed
  display:none below xl, the stepper still visible with 4 badges at and above
  it, and zero title/badge intersection everywhere. Uses a DOM locator rather
  than getByRole because display:none drops the element from the accessibility
  tree, which would make toBeHidden() pass for an element never found.

Verified the guard bites: neutralising the media query turns it red at 768 and
1024 for all four tools. Full CT suite 216 passed, pnpm test 493, validate 0.

This does not close the issue. It trades an overlap for the loss of the "step N
of 4" indicator between 768 and 1279; the compact stepper in the upstream ticket
is still the correct fix and cannot be emulated here, since the badge digit
comes from the array index.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
G10 — the third symptom of issue 05, which G3 left behind. Not a regression: the
original report described three symptoms and G3's acceptance criteria only
covered two, so the orphaned divider and the two-line label were fixed while the
row splitting into two visually unrelated blocks was never measured. It passed
five criteria legitimately and left this.

At 1024 the search input and the "Aplicar etiquetas" group do not fit on one
row, and `ml: auto` + `justifyContent: flex-end` pushed the group to the far
right while the search stayed left. The group still wraps, but now starts at the
same left edge as the search input (x≈48 for both). G3's prop swap — passing the
group as Toolbar's `children` rather than `rightSlot`, which is what removes the
library's unconditional divider — is untouched.

Guarded in both panel states, which is what the missing criterion needed: the
code's own comment notes the wrap point depends on whether the entities panel is
open, so a test measuring only the closed state can pass without fixing the real
case. The spec asserts the exact control count per state before its geometry
loop, so it cannot start measuring nothing.

Unrelated bug found by the same pass: AnonymizerLabelSelect fed Radix `value`
that goes undefined when no label is selected, flipping it between uncontrolled
and controlled. Radix keeps its own uncontrolled value and falls back to it, so
re-selecting a label that had been cleared silently no-opped forever for that
one option while every other label kept working. Now always controlled.

Gates: pnpm validate exit 0, 493 tests / 79 files, 216 CT tests, e2e console
check green.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…st case

The issue-01 mitigation hid the stepper below `xl` (1280px) for every tool. That
cutoff was calibrated to the longest title, "Resumen de Documento", which only
stops colliding at ~1048px — so the three shorter names lost their progress
indicator for hundreds of pixels where they fit perfectly. Visible on a ~1217px
window showing "Voz a Texto": zero overlap for every tool, stepper gone anyway,
and "Voz a Texto" is the one name that never overlapped at any usable width.

Swept in Chromium at 4px steps with the rule neutralised, the width at which the
title↔badge intersection first reaches 0:

  Voz a Texto          ~800px    →  now hidden below 840
  Set de Datos         ~840px    →  now hidden below 880
  Anonimizador         ~880px    →  now hidden below 920
  Resumen de Documento ~1048px   →  now hidden below 1080

Each cutoff keeps ~40px over its measurement so a font fallback cannot silently
reintroduce the overlap. Header emits a `.stepper-hide-below-<n>` class from
STEPPER_HIDE_BELOW and index.css carries one mechanical block per value, so the
tool→width mapping lives in exactly one place.

That map is its own module rather than an export from header.tsx because
Playwright CT cannot mount a component whose import statement also carries a
named export — it rewrites component imports and then treats Header as declared
in the test file.

header-stepper.spec.tsx imports the same map, so the two cannot claim different
numbers; a value with no matching CSS rule still turns it red, and the
per-width overlap assertion still catches a number that is wrong for the real
glyphs. 24 checks, 4 tools × 6 widths.

This is still a mitigation, not the fix: between 768 and its cutoff each tool
trades an overlap for a missing "step N of 4". The compact stepper in
tasks/responsive-fixes/ui-components-issue-01.md remains the upstream work.

Gates: pnpm validate exit 0, 493 tests / 79 files, 216 CT tests, e2e console
check green.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

@sourcery-ai sourcery-ai Bot 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.

Sorry @lionelchamorro, your pull request is larger than the review limit of 150000 diff characters

@sourcery-ai

sourcery-ai Bot commented Aug 3, 2026

Copy link
Copy Markdown

Reviewer's Guide

Makes the three main flows (Dataset, Anonymizer, Voice-to-Text) responsive between 768 and 2560 using new ReadingColumn/SidePanelColumn layout primitives and Panda tokens, fixes the side-panel stacking/toolbar alignment/Stepper overlap and audio preview duration bugs, and introduces the Summarizer flow (process/validation/finish) with server/mocked streaming support and summary export while tightening a number of correctness and styling contracts around speakers, validation, and toast/footer behavior.

File-Level Changes

Change Details Files
Introduce ReadingColumn and SidePanelColumn layout primitives and new Panda layout tokens to centralize responsive gutter/cap and panel sizing rules.
  • Add sizes.content.{max,split,doc} and sizes.panel.{side,form,sideCompact} tokens plus desktop breakpoint in Panda config
  • Implement ReadingColumn with three variants (full/split/doc) using two-node gutter+cap shape and reusable inset helpers
  • Implement SidePanelColumn as a shared, static, responsive panel wrapper used by transcription and anonymizer side panels
  • Update MainContent, FileSelectionLayout, home features screen, dataset validation grid, VTT editor and finish screens to consume ReadingColumn/SidePanelColumn instead of hardcoded maxWidth/padding
panda.config.ts
src/renderer/src/components/layout/reading-column.tsx
src/renderer/src/components/layout/reading-column.test.tsx
src/renderer/src/components/layout/side-panel-column.tsx
src/renderer/src/components/layout/side-panel-column.test.tsx
src/renderer/src/components/layout/main-content.tsx
src/renderer/src/components/layout/file-selection-layout.tsx
src/renderer/src/routes/home/features.tsx
src/renderer/src/components/validate-dataset/index.tsx
src/renderer/src/components/validate-dataset/index.test.tsx
src/renderer/src/components/voice-to-text/transcription-editor/index.tsx
src/renderer/src/components/voice-to-text/transcription-editor/index.test.tsx
src/renderer/src/components/file-annotator/index.tsx
src/renderer/src/components/file-annotator/FileAnnotator.styles.ts
src/renderer/src/components/voice-to-text/finish.tsx
src/renderer/src/components/finish/finish-main-content.tsx
src/renderer/src/layout/home.tsx
Fix side panels and toolbars so they stack correctly below lg, align with reading columns, and no longer overlay or misalign content.
  • Change TranscriptionEditor and FileAnnotator container flexDirections to column below lg so panels stack under content
  • Make SidePanelColumn always position: static and full-width below lg with its own maxHeight+scroll
  • Add readingInsetToolbarOverride and readingInsetChildOverride to align Toolbar and AudioPlayer content to reading columns, fixing G5 chrome alignment
  • Tighten SearchBar layout: move label controls to Toolbar children, remove ml:auto, add responsive wrapping and i18n labels
src/renderer/src/components/voice-to-text/transcription-editor/index.tsx
src/renderer/src/components/voice-to-text/transcription-editor/index.test.tsx
src/renderer/src/components/voice-to-text/transcription-editor/turn-side-panel.tsx
src/renderer/src/components/voice-to-text/transcription-editor/turn-side-panel.test.tsx
src/renderer/src/components/file-annotator/index.tsx
src/renderer/src/components/file-annotator/index.test.tsx
src/renderer/src/components/file-annotator/SearchBar/index.tsx
src/renderer/src/components/file-annotator/SearchBar/index.test.tsx
src/renderer/src/components/layout/reading-column.tsx
src/renderer/src/components/layout/reading-column.test.tsx
src/renderer/src/components/file-annotator/FileAnnotator.styles.ts
Harden transcription speaker identity, labeling, and initials logic and fix race conditions in persona creation and turn assignment.
  • Replace renumberPersonaSpeakers with stable label behavior so renames/merges never reassign other speakers’ labels
  • Change nextPersonaLabel to derive from PERSONA_LABEL_TEMPLATE and highest existing number, tolerating gaps and casing differences
  • Rework computeInitials to support numeric suffixes (Persona 10/11/12) up to 3 chars and document accepted collisions beyond 99
  • Introduce ADD_PERSONA_SPEAKER reducer action that derives label/initials/color from accumulated state, fixing the '+ Nuevo' race and id stability
  • Update turn-side-panel to use ADD_PERSONA_SPEAKER and add RealReducerHarness-based tests for G7 criteria on numbering, badges, and turn assignment
  • Align ASR mapping and schema limits with new initials and persona rules
src/renderer/src/reducers/transcription/index.ts
src/renderer/src/reducers/transcription/actions.ts
src/renderer/src/reducers/transcription/index.test.ts
src/renderer/src/components/voice-to-text/transcription-editor/turn-side-panel.tsx
src/renderer/src/components/voice-to-text/transcription-editor/turn-side-panel.test.tsx
src/renderer/src/services/aymurai/asrMapper.ts
src/renderer/src/services/aymurai/asrMapper.test.ts
src/renderer/src/schema/transcription.ts
src/renderer/src/schema/transcription.test.ts
Refine voice-to-text process, audio-preview duration handling, and header stepper visibility to remove misleading status and unhandled errors.
  • Adjust VoiceProcess titles/subtitles/callouts to switch between processing/finished/error/stopped copies based on status, avoiding stale 'processing' messages
  • Introduce Chromium Infinity/NaN duration handling and single-report guards in useAudioSnippet, plus play() race handling when file changes mid-play
  • Add per-feature stepper hide-below classes and CSS rules keyed on width to mitigate title/stepper overlap without changing AppHeader internals
  • Disable TanStack router’s default view transition to avoid unhandled AbortError, and add an e2e spec to fail on console errors
src/renderer/src/components/voice-to-text/process.tsx
src/renderer/src/components/voice-to-text/process.test.tsx
src/renderer/src/components/voice-to-text/use-audio-snippet.ts
src/renderer/src/components/voice-to-text/use-audio-snippet.test.tsx
src/renderer/src/index.css
src/renderer/src/components/layout/header.tsx
src/renderer/src/components/layout/stepper-visibility.ts
src/renderer/src/routes/home/features.test.tsx
src/renderer/src/app.tsx
src/renderer/src/app.test.tsx
playwright/no-console-errors.spec.ts
playwright-e2e.config.ts
Clarify anonymizer export validation semantics and error messaging, making missing canonical IDs non-fatal while issuing per-family errors.
  • Introduce FATAL_CODES set excluding missing_canonical_entity_id and split issues into fatal/non-fatal, warning on non-fatal occurrences
  • Map each fatal code to an export-issue family and build messages from translated anonymizer.export family keys, with a generic fallback
  • Ensure i18n fallback to Spanish text when i18n.t() returns undefined due to uninitialized instance, and add separate tests for initialized and uninitialized cases
  • Change assertValidAnonymizerExportState to throw only on fatal issues and log grouped summaries via console.error/console.warn
src/renderer/src/utils/anonymizer/export-validation.ts
src/renderer/src/utils/anonymizer/__tests__/export-validation.test.ts
src/renderer/src/utils/anonymizer/__tests__/export-validation-message.test.ts
src/renderer/src/constants/i18n/locales/es/anonymizer.ts
Introduce the Summarizer feature flow (process, validation, finish) with streaming/mocked summarization, local validation persistence, and export to txt/odt/pdf.
  • Add FeatureFlowEnum.Summarizer, icon, routing branches for process/validation/finish, and Summarizer cards in home and features menu
  • Create Summary context for status/partialText/document/title, plus useSummarize hook with streaming mutation and abort handling
  • Implement summary-process screen mirroring voice-to-text, with preview follow-along behavior and status-aware copy
  • Implement summary-validation screen with DocumentSearchPanel (ReadingColumn-based) and RichTextEditor, wired to summaryValidationClient and a localStorage-backed noop client
  • Implement summary-finish preview + export options, using summary-specific txt/odt/pdf formatters and shared watermark constants
  • Wire summarizeDocumentStream to backend streaming and mockSummarizeStream via USE_MOCK_SUMMARIZE config
src/renderer/src/types/features.ts
src/renderer/src/constants/config.ts
src/renderer/src/constants/i18n/locales/es/index.ts
src/renderer/src/constants/i18n/locales/es/summarizer.ts
src/renderer/src/constants/i18n/locales/es/common.ts
src/renderer/src/constants/i18n/locales/es/document-formats.ts
src/renderer/src/components/summarizer/summary-process.tsx
src/renderer/src/components/summarizer/summary-process.test.tsx
src/renderer/src/components/summarizer/summary-validation.tsx
src/renderer/src/components/summarizer/summary-validation.test.tsx
src/renderer/src/components/summarizer/document-search-panel.tsx
src/renderer/src/components/summarizer/document-search-panel.test.tsx
src/renderer/src/components/summarizer/summary-finish.tsx
src/renderer/src/components/summarizer/summary-finish.test.tsx
src/renderer/src/context/Summary.tsx
src/renderer/src/context/Summary.test.tsx
src/renderer/src/hooks/useSummarize.ts
src/renderer/src/hooks/useSummarize.test.tsx
src/renderer/src/services/aymurai/summarization.ts
src/renderer/src/services/aymurai/summarization.test.ts
src/renderer/src/services/aymurai/summarize.ts
src/renderer/src/services/aymurai/mockSummarize.ts
src/renderer/src/services/export/export-summary.ts
src/renderer/src/services/export/export-summary.test.ts
src/renderer/src/services/export/summary-formatters/txt.ts
src/renderer/src/services/export/summary-formatters/txt.test.ts
src/renderer/src/services/export/summary-formatters/odt.ts
src/renderer/src/services/export/summary-formatters/odt.test.ts
src/renderer/src/services/export/watermark.ts
src/renderer/src/services/aymurai/summaryValidation.ts
src/renderer/src/services/aymurai/noopSummaryValidation.ts
src/renderer/src/services/aymurai/summaryValidationClient.ts
src/renderer/src/services/aymurai/summaryValidationClient.test.ts
src/renderer/src/features/RequireSummary.tsx
src/renderer/src/routes/app.$feature/process.tsx
src/renderer/src/routes/app.$feature/validation.tsx
src/renderer/src/routes/app.$feature/finish.tsx
src/renderer/src/routes/app.$feature/route.tsx
Fix toast/footer overlap so bottom-center toasts never cover footer controls and keep offsets consistent across multiple footers.
  • Extend Footer to measure its height via ResizeObserver and publish a CSS var (--app-footer-toast-offset) reflecting the tallest mounted footer
  • Replace direct Toaster usage with AppToaster, which reads the footer offset var and adjusts toast containerStyle bottom accordingly
  • Add Playwright CT fixtures and specs that mount the real VoiceFinish + AppToaster and assert no overlap between warning toasts and footer buttons
src/renderer/src/components/layout/footer.tsx
src/renderer/src/components/layout/footer.test.tsx
src/renderer/src/components/layout/app-toaster.tsx
src/renderer/src/app.tsx
src/renderer/src/test/playwright-fixtures/toast-footer-fixture.tsx
playwright/toast-footer.spec.tsx
Finish the Stitches→Panda CSS migration and clean up remaining styling primitives and tests.
  • Replace styled from @/styles with @/styled/jsx/css in Tabs, FileCheck, Text, Stack, ErrorText, ValidationForm, and related components
  • Document the measured Stitches→Panda token mapping and pitfalls in .claude/rules/panda-css.md and SKILL.md
  • Add cssgen-based token-value tests for global colours and layout tokens to guard against future preset changes
  • Remove legacy Stitches files, imports, and package dependency from the renderer and workspace config
src/renderer/src/components/tabs/index.ts
src/renderer/src/components/tabs/index.test.tsx
src/renderer/src/components/file-check/FileCheck.styles.ts
src/renderer/src/components/file-check/index.tsx
src/renderer/src/components/file-check/index.test.tsx
src/renderer/src/components/file-check/ErrorText.ts
src/renderer/src/components/file-check/ErrorText.tsx
src/renderer/src/components/text/index.ts
src/renderer/src/components/text/index.tsx
src/renderer/src/components/stack/index.ts
src/renderer/src/components/stack/index.test.tsx
src/renderer/src/components/validation-form/index.tsx
src/renderer/src/components/validation-form/index.test.tsx
src/renderer/src/components/theme-provider/index.tsx
src/renderer/src/components/layout/footer.tsx
src/renderer/src/components/layout/footer.test.tsx
.claude/rules/panda-css.md
.claude/skills/panda-css/SKILL.md
src/renderer/src/global-styles-tokens.test.ts
package.json
pnpm-lock.yaml
pnpm-workspace.yaml
.claude/CLAUDE.md
docs/aymurai.md
src/renderer/src/components/index.ts
src/renderer/src/styles/* (deleted)
Clean up smaller correctness and UX issues around FileCheck, HowItWorks, header not-found, router shell height, and localStorage in tests.
  • Make FileCheck's error message i18n-sourced, non-empty, and ellipsize only the filename with a tooltip
  • Ensure HowItWorks page and modal share the same tutorialGridOverride class and grid alignment, with CT guards against cascade regressions
  • Add a themed NotFound layout wired into the root route’s notFoundComponent, and guard its behavior via CT and Playwright
  • Switch app route shells and VoiceValidation to use 100dvh/full width instead of 100vh/screen to avoid mobile viewport issues
  • Shim localStorage in Vitest setup on Node versions where the ambient global is unusable
src/renderer/src/components/file-check/index.tsx
src/renderer/src/components/file-check/index.test.tsx
src/renderer/src/components/how-it-works.tsx
src/renderer/src/components/how-it-works.test.tsx
src/renderer/src/components/how-it-works-modal.tsx
playwright/how-it-works.spec.tsx
src/renderer/src/components/layout/not-found.tsx
src/renderer/src/components/layout/not-found.test.tsx
src/renderer/src/routes/__root.tsx
src/renderer/src/routes/__root.test.tsx
playwright/not-found.spec.tsx
src/renderer/src/routes/app.$feature/route.tsx
src/renderer/src/routes/app.$feature/route.test.tsx
src/renderer/src/components/voice-to-text/validation.tsx
src/renderer/src/components/voice-to-text/validation.test.tsx
src/renderer/src/test/setup.ts

Tips and commands

Interacting with Sourcery

  • Trigger a new review: Comment @sourcery-ai review on the pull request.
  • Continue discussions: Reply directly to Sourcery's review comments.
  • Generate a GitHub issue from a review comment: Ask Sourcery to create an
    issue from a review comment by replying to it. You can also reply to a
    review comment with @sourcery-ai issue to create an issue from it.
  • Generate a pull request title: Write @sourcery-ai anywhere in the pull
    request title to generate a title at any time. You can also comment
    @sourcery-ai title on the pull request to (re-)generate the title at any time.
  • Generate a pull request summary: Write @sourcery-ai summary anywhere in
    the pull request body to generate a PR summary at any time exactly where you
    want it. You can also comment @sourcery-ai summary on the pull request to
    (re-)generate the summary at any time.
  • Generate reviewer's guide: Comment @sourcery-ai guide on the pull
    request to (re-)generate the reviewer's guide at any time.
  • Resolve all Sourcery comments: Comment @sourcery-ai resolve on the
    pull request to resolve all Sourcery comments. Useful if you've already
    addressed all the comments and don't want to see them anymore.
  • Dismiss all Sourcery reviews: Comment @sourcery-ai dismiss on the pull
    request to dismiss all existing Sourcery reviews. Especially useful if you
    want to start fresh with a new review - don't forget to comment
    @sourcery-ai review to trigger a new review!

Customizing Your Experience

Access your dashboard to:

  • Enable or disable review features such as the Sourcery-generated pull request
    summary, the reviewer's guide, and others.
  • Change the review language.
  • Add, remove or edit custom review instructions.
  • Adjust other review settings.

Getting Help

…eature/responsive-screens

# Conflicts:
#	package.json
#	pnpm-workspace.yaml
#	src/renderer/src/components/summarizer/summary-process.test.tsx
#	src/renderer/src/routes/home/features.tsx
…-screens

# Conflicts:
#	package.json
#	pnpm-workspace.yaml
#	src/renderer/src/components/summarizer/document-search-panel.test.tsx
#	src/renderer/src/components/summarizer/document-search-panel.tsx
#	src/renderer/src/components/summarizer/summary-finish.tsx
#	src/renderer/src/components/summarizer/summary-process.test.tsx
#	src/renderer/src/components/summarizer/summary-process.tsx
#	src/renderer/src/components/summarizer/summary-validation.test.tsx
#	src/renderer/src/components/summarizer/summary-validation.tsx
#	src/renderer/src/constants/i18n/locales/es/summarizer.ts
#	src/renderer/src/routes/home/features.test.tsx
#	src/renderer/src/routes/home/features.tsx
#	src/renderer/src/services/aymurai/asrMapper.ts
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