Project

General

Profile

Feature #3250 » responsive-audit-3250.txt

Bot AI, 09/01/2026 12:50 PM

 
================================================================================
GATE 7 — RESPONSIVE AUDIT (Next.js/Tailwind standalone) Ticket #3250
Branch: feat/3250-oms-ui-bug-fixes-chips-s
Run date: 2026-09-01
Result: FAIL (4 unresolved P1)
================================================================================

SKILL RESOLUTION
--------------------------------------------------------------------------------
No slash-command layer was available in this run. All six skills were resolved
via resolution path 3 (read the SKILL.md file directly) from:

cross-repo-orchestration/skills/nextjs-tailwind-responsive-<name>/SKILL.md

[components] path 3 — file read OK
[desktop] path 3 — file read OK
[mobile] path 3 — file read OK
[tablet] path 3 — file read OK
[typography] path 3 — file read OK
[ux-consistency] path 3 — file read OK

No skill was unreachable. Note that the gate doc points at
`.claude/skills/...` inside the product repo; oms-web has no `.claude/skills`
directory, so the shared copies under cross-repo-orchestration/skills/ were
used. Same text, different location.

INPUT GAP — screen_inventory was <missing>
--------------------------------------------------------------------------------
The [ux-consistency] audit is specified to compare this ticket's screens
against the wider screen inventory. With no inventory supplied it ran in a
reduced mode: the 10 feature screens were compared against EACH OTHER and
against the shared components they all render through. Cross-screen findings
involving screens outside feature_files may therefore be missed. This is a
coverage reduction, recorded here rather than passed off silently.

METHOD — measured, not reasoned
--------------------------------------------------------------------------------
Source review alone cannot settle whether `overflow-x-auto` actually engages,
so layout claims here were measured. The repo's own Tailwind config and
globals.css were compiled (`npx tailwindcss -c ./tailwind.config.ts`) against a
static HTML replica whose markup is copied from the real components
(app/(app)/layout.tsx, Sidebar, Header, ListToolbar, FilterBar, SearchInput,
DataTable, Pagination, RowActions, RepeaterTable, ExpenseForm,
ResetPasswordForm). The replica was measured in headless Chromium
(Playwright, /home/devops/.cache/ms-playwright/chromium-1234) at 16 viewport
widths: 320 360 375 390 430 600 640 768 820 900 1024 1280 1366 1440 1920 2560.

The DB was not seeded, so real routes were not rendered — every authenticated
route redirects to /login against an empty DB. The replica approach measures
layout/CSS behaviour, which is what this gate reviews. It does NOT replace
ui-verify's screenshot-vs-design check.

Two candidate fixes initially measured as no-ops because the candidate classes
were absent from the compiled CSS (Tailwind only emits classes it finds in
`content`). Those runs were discarded and re-measured with the classes
safelisted and their presence in out.css verified first. Only the corrected
numbers appear below.

BREAKPOINT COVERAGE CENSUS (the headline number)
--------------------------------------------------------------------------------
Responsive utilities (sm:/md:/lg:/xl:/2xl:) per file:

app/(app)/admin/items/page.tsx 0
app/(app)/admin/projects/page.tsx 0
app/(app)/admin/suppliers/page.tsx 0
app/(app)/admin/uom/page.tsx 0
app/(app)/admin/users/page.tsx 0
app/(app)/expense-summary/[project]/page.tsx 0
app/(app)/expense-summary/page.tsx 0
app/(app)/expense-summary/ExpenseSummaryRowActions.tsx 0
app/(app)/expenses/page.tsx 0
app/(app)/orders/page.tsx 0
components/forms/ExpenseForm.tsx 0
components/forms/ResetPasswordForm.tsx 1 lg:grid-cols-[1fr_280px]

Shell + shared components these pages render through:

app/(app)/layout.tsx 0 components/ui/Pagination.tsx 0
components/layout/Sidebar 0 components/ui/SearchInput.tsx 0
components/layout/Header 0 components/ui/RepeaterTable.tsx 0
components/ui/DataTable 0 components/ui/RowActions.tsx 0
components/ui/ListToolbar 0 components/ui/AttachmentChip 0
components/ui/FilterBar 0 app/globals.css 1 sm:text-sm

ONE responsive utility exists across the entire audited surface. Breakpoints
reviewed (sm 640 / md 768 / lg 1024 / xl 1280) are effectively unimplemented
rather than implemented-and-wrong.

MEASUREMENT 1 — list screen (app shell + ListToolbar + DataTable + Pagination)
--------------------------------------------------------------------------------
vw doc.scrollWidth overflow main.clientW DataTable scroll/client
320 1458 1138 1202 1152/1152 NOT scrolling
375 1458 1083 1202 1152/1152 NOT scrolling
430 1458 1028 1202 1152/1152 NOT scrolling
640 1458 818 1202 1152/1152 NOT scrolling
768 1458 690 1202 1152/1152 NOT scrolling
820 1458 638 1202 1152/1152 NOT scrolling
1024 1458 434 1202 1152/1152 NOT scrolling
1280 1458 178 1202 1152/1152 NOT scrolling
1366 1458 92 1202 1152/1152 NOT scrolling
1440 1458 18 1202 1152/1152 NOT scrolling
1920 1920 0 1664 1614/1614
2560 2560 0 2304 2254/2254

The document is a CONSTANT 1458px wide from 320px to 1440px. The layout has a
hard floor and does not respond at all below it. DataTable's scrollWidth always
equals its clientWidth — its `overflow-x-auto` never engages at any width.

MEASUREMENT 2 — root-cause counterfactual (adding min-w-0 to layout.tsx:33)
--------------------------------------------------------------------------------
vw BEFORE AFTER +min-w-0
320 doc 1458 table 1152/1152 doc 657 table 1152/14 scrolls
375 doc 1458 table 1152/1152 doc 657 table 1152/69 scrolls
768 doc 1458 table 1152/1152 doc 768 table 1152/462 scrolls
1024 doc 1458 table 1152/1152 doc 1024 table 1152/718 scrolls
1440 doc 1458 table 1152/1152 doc 1440 table 1152/1134 scrolls

Confirms the root cause. It also shows a SECOND, independent cause: at 320/375
the document still floors at 657px after the fix — that residual is the fixed
256px sidebar plus padding. Both are P1 and neither alone is sufficient.

MEASUREMENT 3 — form screen (ExpenseForm + ResetPasswordForm)
--------------------------------------------------------------------------------
vw doc.scrollWidth overflow RepeaterTable width pwgrid columns
320 975 655 669 670.656px (1 col)
375 975 600 669 670.656px (1 col)
430 975 545 669 670.656px (1 col)
768 975 207 669 670.656px (1 col)
900 975 75 669 670.656px (1 col)
1024 1024 0 718 400px 280px (2 col)
1440 1440 0 766 760px 280px (2 col)

ResetPasswordForm's grid switches correctly at lg (1024). ExpenseForm's
RepeaterTable holds a hard ~669px floor with no scroll affordance.

MEASUREMENT 4 — touch targets @375px
--------------------------------------------------------------------------------
RowActions view icon 28 x 28 px below minimum
RowActions edit icon 28 x 28 px below minimum
RowActions delete icon 28 x 28 px below minimum
Pagination page link 32 x 30 px below minimum
FilterBar toggle 40 x 40 px ok

Candidate `inline-flex h-11 w-11 items-center justify-center` -> 44 x 44 px
(measured). Cost: Actions cell content width 100px for three icons.

MEASUREMENT 5 — fix validation, ExpenseForm grid (shell corrected first)
--------------------------------------------------------------------------------
vw grid-cols-2 (current) grid-cols-1 sm:grid-cols-2 (candidate)
375 field 27.5px field 71px
430 field 55px field 126px
640 field 160px field 160px identical
768 field 224px field 224px identical
1024 field 352px field 352px identical

The candidate is free above 640px and materially better below it.

MEASUREMENT 6 — RepeaterTable scroll wrapper: fixes overflow, breaks dropdowns
--------------------------------------------------------------------------------
vw doc before -> after adding overflow-x-auto computed overflow-y
320 950 -> 392 auto
375 950 -> 412 auto
768 768 -> 768 auto
1024 1024 -> 1024 auto

It does fix the overflow, but computed overflow-y becomes `auto` in every case
— which is precisely the Listbox/DatePicker dropdown-clipping regression that
RepeaterTable.tsx:41-54 documents as the reason the wrapper was removed. So
this is NOT a safe mechanical fix; it needs a real decision. Reported, not
applied.

MEASUREMENT 7 — variable-length table content @1440px
--------------------------------------------------------------------------------
Suppliers Address cell (whitespace-nowrap, no truncate): 518px of a 1152px table
With truncate + max-w-[16rem]: 326px cell, 1134px table

================================================================================
MERGED FINDINGS
================================================================================

--- P1 -------------------------------------------------------------------------

[P1-1] tags: [mobile] [tablet] [desktop] [components] [ux-consistency]
File: app/(app)/layout.tsx:33 (root cause)
Affects: all 10 list-page feature files
Problem: `<div className="flex flex-1 flex-col">` has no `min-w-0`. A flex item
defaults to min-width:auto, so this column refuses to shrink below its
content's intrinsic width. DataTable's `overflow-x-auto` is therefore
never able to engage, and the whole document scrolls horizontally
instead of just the table.
Evidence: doc.scrollWidth pinned at 1458px for every viewport 320-1440px;
DataTable scroll/client 1152/1152 (never scrolls). Adding min-w-0
returns doc to viewport width at 768/1024/1440 and the table starts
scrolling internally (1152/462 at 768px). See Measurements 1 and 2.
Why it matters: this is not a mobile-only problem — a 1366px laptop, the single
most common desktop width, overflows by 92px. Every list screen in the
ticket is affected. The table-scroll pattern the code already chose is
silently inert.
Fix: add `min-w-0` to the flex column wrapper.
Risk: low, and measured — the layout is unchanged at 1920/2560 where no
overflow existed.
Fixed: NO (read-only run)

[P1-2] tags: [mobile] [components]
File: components/layout/Sidebar.tsx:99-104 (root cause)
Affects: all 12 feature files
Problem: the sidebar is `shrink-0` at a fixed `w-64` (256px) with zero
responsive variants. There is no hamburger, drawer, or off-canvas
pattern; it is always rendered inline and never shrinks.
Evidence: 0 responsive utilities in Sidebar.tsx. After P1-1 is fixed, the
document still floors at 657px at 320/375px — the residual is this
sidebar. 256px is 80% of a 320px viewport and 68% of a 375px viewport.
Why it matters: the app is unusable on any phone. This is the largest single
contributor to mobile breakage after P1-1.
Fix: genuine responsive work, not a utility swap — hide below md and add a
drawer/overlay with a toggle in Header. The manual collapse toggle that
exists is a user preference, not a responsive behaviour, and defaults
to expanded.
Risk: medium — new interactive surface; needs its own ui-verify pass.
Fixed: NO (report-only by design — genuine layout work, not a safe auto-fix)

[P1-3] tags: [mobile] [tablet] [components]
File: components/ui/RepeaterTable.tsx:55 / components/forms/ExpenseForm.tsx:245
Problem: the Expense Items repeater has a hard ~669px minimum width (Type w-56
224px + Amount w-32 128px + Attachment + remove w-10, all cells
`whitespace-nowrap`) and deliberately carries no scroll wrapper.
Evidence: form-page doc.scrollWidth constant at 975px from 320px through 900px
— 600px of overflow at 375px, still 207px at 768px (iPad portrait) and
75px at 900px. See Measurement 3.
The comment at RepeaterTable.tsx:52-54 asserts "This table's column set
is narrow/bounded enough that horizontal overflow isn't a real risk in
practice." That is measurably false at every width below 1024px.
Why it matters: the Add/Edit Expense form cannot be completed on a phone or an
iPad in portrait; content is pushed off-viewport with no affordance.
Fix: NOT the obvious one. Measurement 6 shows `overflow-x-auto` resolves the
overflow but forces computed overflow-y:auto, reintroducing the
documented dropdown-clipping regression. Needs either a mobile card
transformation of the repeater, or a portalled dropdown for
Listbox/DatePicker so the scroll wrapper becomes safe.
Risk: high if done naively — the naive fix regresses a previously-fixed bug.
Fixed: NO (genuine redesign — explicitly out of auto-fix scope)

[P1-4] tags: [mobile] [ux-consistency]
File: components/forms/ExpenseForm.tsx:207
Problem: `<div className="grid grid-cols-2 gap-4">` for the Date / Project pair
never stacks. Two columns at every width including 320px.
Evidence: with the shell corrected so the grid itself is what's measured, fields
are 27.5px wide at 375px and 55px at 430px. See Measurement 5.
The mobile skill names this case explicitly ("multi-field forms that
don't stack vertically on mobile").
Consistency angle: ResetPasswordForm.tsx:127 — the other form in this same
changeset — does this correctly (`grid-cols-1 ... lg:grid-cols-[1fr_280px]`,
verified switching at 1024px in Measurement 3). Two forms, same ticket,
opposite treatment. This is the classic "only one viewport was actually
designed for" pattern the ux-consistency skill exists to catch.
Fix: `grid grid-cols-1 sm:grid-cols-2 gap-4` — measured identical at 640px
and above, so there is no tablet/desktop cost.
Risk: very low; measured no-op above 640px.
Fixed: NO (read-only run)

--- P2 -------------------------------------------------------------------------

[P2-1] tags: [mobile]
File: app/(app)/expense-summary/ExpenseSummaryRowActions.tsx:9
app/(app)/orders/OrderRowActions.tsx:12
app/(app)/expenses/ExpenseRowActions.tsx:13
components/ui/RowActions.tsx:33,37,45
components/ui/RepeaterTable.tsx:99
Problem: every row-action icon control is `p-1.5 -m-1.5` around a `h-4 w-4`
glyph, giving a 28x28px tappable area — below the ~40px practical
floor and the 44x44 WCAG 2.5.5 target. RowActions packs three of them
at gap-3 (12px), so mis-taps are likely.
Evidence: measured 28x28px at 375px for all three RowActions icons
(Measurement 4). Candidate `inline-flex h-11 w-11 items-center
justify-center` measures exactly 44x44px.
Note: ExpenseSummaryRowActions.tsx is NEW in this ticket (#3250 R5) and
copies the pattern, so the ticket adds a new instance rather than
inheriting only existing ones.
Risk: low, but not zero — the Actions cell content grows to 100px for three
icons, which is a visible table-layout change and should go through
ui-verify rather than being applied blind.
Fixed: NO (read-only run)

[P2-2] tags: [mobile]
File: components/ui/Pagination.tsx:97-110
Problem: numbered page links are `min-w-[2rem] px-2 py-1`, measuring 32x30px —
below the touch minimum, and rendered as a row of up to 7 adjacent
small targets.
Evidence: measured 32x30px at 375px (Measurement 4).
Why it matters: introduced by #3247's numbered-pagination work; every list page
in this ticket renders it.
Fix: raise to a 40-44px min target at mobile widths.
Fixed: NO (read-only run)

[P2-3] tags: [tablet]
File: components/ui/SearchInput.tsx:109-113 with components/ui/ListToolbar.tsx:24
Problem: the expanded search box is a hard `w-80` (320px) and `shrink-0`, with
no responsive variant. With the 256px sidebar and 48px of main padding,
the toolbar row needs 624px before the New button and filter toggle are
accounted for, so on `flex-wrap justify-end` the New button drops to
its own line through the intermediate range.
Evidence: w-80 + shrink-0 in source; sidebar 256px and p-6 confirmed in
Measurement 1 (main.clientW). NOTE: this specific wrap point was NOT
directly measured — the shell bug (P1-1) masks it until P1-1 is fixed.
Stated as arithmetic, flagged as such rather than presented as a
measurement.
Fix: let the expanded width be fluid with a max (e.g. `w-full max-w-xs`)
below lg.
Fixed: NO (read-only run)

--- P3 -------------------------------------------------------------------------

[P3-1] tags: [typography] [mobile] [tablet]
File: components/ui/DataTable.tsx:89 (via components/ui/table.tsx:62)
Problem: every table cell is `whitespace-nowrap` with no `truncate` or
max-width. Columns holding free-text real-world data — Suppliers
`addressLine1`, Items/Projects `name`, Orders supplier/project names —
can grow without bound and drive the table's width.
Evidence: the Suppliers Address cell measured 518px of a 1152px table at 1440px;
with `truncate max-w-[16rem]` it drops to 326px (Measurement 7).
Fix: cap and truncate the free-text columns; keep nowrap for dates, amounts,
status and actions where it is correct.
Fixed: NO (read-only run)

[P3-2] tags: [typography]
File: components/ui/AttachmentChip.tsx:44
Problem: the anchor carries `min-w-0` but neither `truncate` nor
`overflow-hidden`, so `min-w-0` has no effect and a long filename never
truncates. Sits inside a `whitespace-nowrap` cell in
expense-summary/[project] and in every ExpenseForm repeater row.
Fix: add `truncate` (or `overflow-hidden text-ellipsis`) alongside min-w-0,
with the full name in a title attribute.
Fixed: NO (read-only run)

[P3-3] tags: [desktop]
File: app/(app)/layout.tsx:35
Problem: `<main className="flex-1 p-6">` has no max-width. Content stretches to
the full viewport at any width.
Evidence: main.clientWidth measured 2304px at a 2560px viewport (Measurement 1).
Expense Summary renders only three columns (Project Name, Total,
Actions) across that width.
Judgement: acceptable for wide data tables, genuinely poor for the 3-column
Expense Summary list. Low priority; noted rather than pressed.
Fixed: NO (read-only run)

[P3-4] tags: [desktop] [tablet]
File: components/layout/Sidebar.tsx:99-104
Problem: the 256px sidebar is 25% of a 1024px laptop viewport — the
desktop skill's named "sidebar eats a disproportionate share of a
smaller laptop" case. Partially mitigated by the manual rail toggle
(persisted to localStorage), but that defaults to expanded and is a
user preference rather than a responsive rule.
Fix: default to the rail variant below xl.
Fixed: NO (read-only run)

[P3-5] tags: [ux-consistency]
File: app/(app)/expense-summary/page.tsx
Problem: it is the only list screen of the nine with no <Pagination>. Every
other list page renders one.
(items/projects/suppliers/uom/users/expenses/orders/[project] = yes.)
Responsive consequence: an unbounded row count on the narrowest viewports,
where each row already costs the most vertical space.
Caveat: this is as much a data-layer decision (expenseSummaryByProject returns
no `total`) as a responsive one, and is at the edge of this gate's
scope. Recorded for the record, deliberately kept at P3.
Fixed: NO (read-only run)

[P3-6] tags: [ux-consistency] [components]
File: components/ui/FilterBar.tsx:139
Problem: filter fields are fixed `w-[200px]` inside a `flex-wrap` row. They wrap
rather than overflow, so this degrades gracefully — but with three
filters (Users, Orders, Expenses) the row is 650px, forcing a
three-row stack on narrow viewports with no reflow to full-width
fields, which is the more usual mobile filter treatment.
Judgement: wraps rather than breaks. Genuinely minor.
Fixed: NO (read-only run)

================================================================================
POSITIVE BASELINE (checked, not defects)
================================================================================
- [desktop] No hover-only interactions. RowActions icons carry resting colours
(text-text / text-status-info / text-status-danger) by explicit design
decision in #3248, so nothing is discoverable only on hover. Row `hover:` on
the table is decorative only. This check passes cleanly.
- [typography] No oversized fixed headings. The largest type on the audited
surface is HeaderTitle's `text-xl` and the Expense Summary total's `text-2xl`;
neither overflows or overwhelms a 320px viewport. Per the typography skill's
own guidance, no responsive type scale is warranted here and none is
recommended — adding one would be noise.
- [typography] No unbroken-string overflow risk in headings or body copy; the
variable-length risk is confined to table cells and the attachment chip,
reported as P3-1/P3-2.
- [mobile/performance] No heavy animation or unoptimised imagery. The
SearchInput and FilterBar transitions are CSS width/opacity/grid-rows only;
all iconography is inline SVG via lucide-react.
- [components] ResetPasswordForm.tsx is the one genuinely responsive file in the
changeset and is correct: measured 1-column below 1024px, `1fr 280px` at and
above it, with the policy card stacking beneath the fields. It is the model
the rest of the changeset should follow.
- [components] DataTable chose the right strategy in principle — horizontal
scroll, not column hiding, so no information is silently dropped on small
screens. The strategy is sound; it is simply defeated by P1-1.

================================================================================
STEP 3 — AUTO-FIX: NOT PERFORMED
================================================================================
The gate's Step 3 allows fixing clearly-safe findings in place. This node was
invoked with explicit read-only repo access ("You have no write access, by
design"), so no source file was modified. Every finding above is marked
Fixed: NO. This evidence file is the gate's own output artifact, not a source
change.

Two findings would have qualified as safe auto-fixes under a writable run, and
both have measured fix validation ready: P1-1 (`min-w-0`, Measurement 2) and
P1-4 (`grid-cols-1 sm:grid-cols-2`, Measurement 5). P1-2 and P1-3 would NOT
have been auto-fixed even with write access — both are genuine layout redesigns,
and for P1-3 the obvious fix is measurably a regression (Measurement 6).

================================================================================
STEP 5 — REDMINE NOTE: NOT POSTED
================================================================================
REDMINE_URL and TASK_ID are both unset in this environment (an API key is
present, but a key alone cannot address a request). The PUT in the gate spec
could not be constructed. No note was posted and none was faked. Posting is
also an outward-facing action that this read-only node should not take
unprompted. The gate result is carried by this file and the returned JSON.

================================================================================
SUMMARY
================================================================================
Breakpoints reviewed: sm(640) md(768) lg(1024) xl(1280), plus 320/360/375/390/
430 mobile, 600/820/900 intermediate, 1366/1440/1920/2560 desktop.

P1: 4 P2: 3 P3: 6 Total: 13

gate_status: FAIL — 4 unresolved P1 findings.

Bottom line: this is not a set of scattered responsive nits. Exactly one
responsive utility exists across the whole audited surface, and the document
measures a constant 1458px wide from 320px to 1440px. The screens in this
ticket were built at one viewport. Two of the four P1s (P1-1, P1-4) are small,
measured, low-risk changes; the other two (P1-2 sidebar, P1-3 repeater) are real
design work and should be scoped as such rather than patched.
(6-6/7)