Project

General

Profile

Feature #3248 » responsive-audit-3248.txt

Bot AI, 08/30/2026 03:46 PM

 
================================================================================
GATE 7 — RESPONSIVE AUDIT (W16_gate_responsive_audit)
Ticket : #3248 oms-ui-ux-and-functional
Branch : feat/3248-oms-ui-ux-and-functional @ e8fb724
Stack : nextjs-tailwind-standalone (oms-web)
Result : FAIL — 3 P1, 5 P2, 4 P3
================================================================================

--------------------------------------------------------------------------------
0. SKILL RESOLUTION (gate file "Skill resolution", steps 1-4)
--------------------------------------------------------------------------------
All six audit skills were searched for through all three resolution paths:

1. /nextjs-tailwind-<name> -> no slash-command layer present
2. /<name> (repo's own tuned copy) -> no .claude/ directory in oms-web
3. read_file(".claude/skills/nextjs-tailwind-<name>/SKILL.md")

scanned, per skill, for:
./.claude/skills/nextjs-tailwind-responsive-<name>-audit/SKILL.md NOT FOUND
./.claude/skills/responsive-<name>-audit/SKILL.md NOT FOUND
/opt/drcarrot-deploy/.claude/skills/nextjs-tailwind-... NOT FOUND

Repo-wide: `find /opt/drcarrot-deploy -name SKILL.md -path "*responsive*"` -> 0 hits.
The only skills/ trees on this host are /opt/drcarrot-deploy/.claude/skills,
.../dr.carrot-v3-react/.claude/skills and .../emr_v1_l12/.claude/skills; none
contains a responsive-audit skill.

=> All six skills UNREACHABLE. Per gate step 4 this is reported, not skipped
silently. Each audit below was executed directly from the gate file's own
one-line definition of that audit (gate lines 27-38), against the real
files, with real measurements. No audit was dropped.

screen_inventory input was <missing>. The [ux-consistency] audit therefore
compares screens against each other within feature_files (the Add/Edit form
family, the List family) rather than against an external inventory. Noted as a
scope reduction, not a pass.

--------------------------------------------------------------------------------
1. METHOD — measured, not reasoned
--------------------------------------------------------------------------------
Live-server route was attempted first and abandoned honestly:
- Next dev server up on 127.0.0.1:3947 (HTTP 200 /login).
- Login with .env SUPER_ADMIN_* -> "Invalid username or password."
- Cause: `prisma.user.findMany()` returns [] — the database is EMPTY.
Seeding is a write; this node runs read-only, so the live route was dropped
rather than worked around.

Substitute (equal or better fidelity), all of it real, none of it hand-mocked:
- Stylesheet: rebuilt from source with the project's own Tailwind CLI —
./node_modules/.bin/tailwindcss -c tailwind.config.ts -i app/globals.css
(26,890 bytes; matches the .next build output size class).
- Markup: the REAL components imported and rendered through
react-dom/server (esbuild bundle, next/link + next/navigation stubbed):
DataTable, StatusBadge, PageHeader, ListToolbar, FilterBar, Pagination,
RowActions, SearchInput, Sidebar, Modal, SupplierForm, ItemForm, OrderForm
inside the verbatim app shell from app/layout.tsx:38-41.
Column sets copied verbatim from app/orders/page.tsx:27-35 and
app/admin/users/page.tsx:38-45. These are the shipped components, not replicas.
- Geometry read with Playwright/Chromium via getBoundingClientRect() +
getComputedStyle() at 8 viewports: 320, 375, 414, 640, 768, 1024, 1280, 1920.
- 5 screens x 8 viewports = 40 measured rows -> /tmp/w16b/measurements.json

Breakpoints reviewed: sm(640) md(768) lg(1024) xl(1280), plus 320/375/414/1920.

--------------------------------------------------------------------------------
2. AGGREGATE FACT THAT DRIVES MOST OF THIS REPORT
--------------------------------------------------------------------------------
$ grep -rnoE '\b(md|lg|xl|2xl):[a-z-]+' app components | wc -l
0

ZERO md:, lg:, xl: or 2xl: utilities exist anywhere in app/ or components/.
The entire responsive vocabulary of this ticket is two `sm:` usages:
app/globals.css:28 sm:text-sm (in .field-input/.field-control)
components/ui/fields/TextField sm:w-[350px] (via each form's fieldWidth)

Everything else adapts only as an accident of flexbox, never by intent.

--------------------------------------------------------------------------------
3. MEASURED DATA (excerpt — full set in /tmp/w16b/measurements.json)
--------------------------------------------------------------------------------
screen vw aside main inner docScrollW tablePaneHiddenPx
orders-list 320 256 1288.1 1240.1 1544 0
orders-list 375 256 1288.1 1240.1 1544 0
orders-list 768 256 1288.1 1240.1 1544 0
orders-list 1024 256 1288.1 1240.1 1544 0
orders-list 1280 256 1288.1 1240.1 1544 0
orders-list 1920 256 1664 1616 1920 0
users-list 320 256 1051.8 1003.8 1308 0
users-list 1280 256 1051.8 1003.8 1308 0
supplier-new 320 256 281.1 233.1 537 0
supplier-new 1024 256 768 720 1024 0
order-new 320 256 551.5 503.5 808 0
order-new 1024 256 768 720 1024 0

Read the docScrollW column against vw: the document is wider than the
viewport at every breakpoint up to and including 1280 on both list screens.
tablePaneHiddenPx is 0 everywhere — DataTable's own overflow-x-auto is
carrying nothing. Both facts have the same single root cause (F1).

--------------------------------------------------------------------------------
4. FINDINGS
--------------------------------------------------------------------------------

### F1 — P1 — [components][mobile][tablet][desktop]
`<main>` is a flex child with no `min-w-0`, so DataTable's `overflow-x-auto`
is inert and the whole PAGE scrolls sideways instead of the table.

Site : app/layout.tsx:40 `<main className="flex-1 p-6">`
Collateral: components/ui/DataTable.tsx:42 `<div className="card overflow-x-auto">`
— present, correct, and never able to fire.

A flex item defaults to min-width:auto, so `flex-1` cannot shrink below its
content's intrinsic width. DataTable is `min-w-full` + `whitespace-nowrap`
(DataTable.tsx:43,53,82), so <main> is forced to the table's natural width and
the overflow escapes to the document.

Measured, orders-list: main = 1288.1px at EVERY viewport 320 -> 1280.
docScrollW pinned at 1544px. At 1280 (a normal laptop) the page still scrolls
horizontally by 264px. This is not a mobile-only defect.

Proven fix (applied in-browser, re-measured, not assumed) — adding `min-w-0`
to <main>:
vw= 375 docSW 1544 -> 782 main 1288.1 -> 119 panePinned 0 -> 1169
vw= 768 docSW 1544 -> 778 main 1288.1 -> 512 panePinned 0 -> 776
vw=1024 docSW 1544 -> 1024 main 1288.1 -> 768 panePinned 0 -> 520
vw=1280 docSW 1544 -> 1280 main 1288.1 -> 1024 panePinned 0 -> 264
At 1024 and 1280 docScrollW becomes exactly the viewport width — page-level
horizontal scroll gone, table scrolls inside its own card as designed.
Residual overflow at 375/768 is F2, not this.

### F2 — P1 — [mobile][components]
Sidebar has no mobile variant; it is 256px at 320px wide.

Site: components/layout/Sidebar.tsx:71-74
clsx("shrink-0 ...", collapsed ? "w-16" : "w-64")

Measured asideW = 256.0 at all 8 viewports incl. 320. `collapsed` is React
state seeded from localStorage (Sidebar.tsx:47-55) — a user preference, not a
viewport response — and defaults to false on first paint on every device.

At 320px the nav consumes 80% of the screen. Even after F1's min-w-0 fix
<main> is left 119px at vw=375 (measured above). There is no off-canvas
drawer, no `hidden md:flex`, no hamburger. Below `md` the app has no usable
content column.

Not auto-fixable: this is the layout redesign the gate's step 3 explicitly
says to report rather than guess at.

### F3 — P1 — [mobile][tablet][components]
`grid grid-cols-2` / `grid-cols-4` with no breakpoint prefix — never reflows.

components/forms/OrderForm.tsx:119 grid grid-cols-2 gap-4
components/forms/ExpenseForm.tsx grid grid-cols-2 gap-4
components/forms/OrderFulfillmentForm.tsx grid grid-cols-4 gap-x-6 gap-y-4

Measured gridTemplateColumns on order-new:
vw= 320 "243.766px 243.766px" (docScrollW 808 — 2.5x the viewport)
vw= 375 "243.766px 243.766px"
vw= 768 "243.766px 243.766px"
vw=1024 "352px 352px"
vw=1920 "376px 376px"

Two side-by-side inputs at 243.8px each on a 320px phone; the 4-column
fulfillment grid is worse. Needs `grid-cols-1 md:grid-cols-2`
(and `sm:grid-cols-2 lg:grid-cols-4` for fulfillment).

### F4 — P2 — [tablet][desktop]
The two-column field layout only engages at ~1078px — iPad landscape gets one column.

Sites: SupplierForm.tsx:50,59 / ItemForm.tsx:56,64 / UomForm.tsx:41,49 /
ProjectForm.tsx:40,48 / UserForm.tsx:56,63
`flex flex-wrap gap-x-6 gap-y-4 max-w-[772px]` + `w-full sm:w-[350px]`

Two fields need 350 + 24(gap-x-6) + 350 = 724px of container. Measured
container width vs. fields-per-row on supplier-new:
vw=1000 -> 646px -> 1 per row
vw=1024 -> 670px -> 1 per row <-- iPad landscape
vw=1032 -> 678px -> 1 per row
vw=1040 -> 686px -> 1 per row
vw=1100 -> 746px -> 2 per row <-- threshold ~1078px

At 1024 the container is 720px — 4px short of the 724px needed. The design's
two-column form silently degrades to a single 350px column with ~320px of
dead space beside it on every tablet and on 1024-wide laptops.

### F5 — P2 — [ux-consistency]
Two different, contradictory responsive strategies inside one screen family.

Add/Edit forms split cleanly in two:
reflows (flex flex-wrap + sm:w-[350px]) :
SupplierForm, ItemForm, UomForm, ProjectForm, UserForm
never reflows (grid grid-cols-2, no prefix) :
OrderForm, ExpenseForm

Same family, same user task, opposite behavior at the same viewport: at 375px
a Supplier field is one full-width column while an Order field is two 243.8px
columns. Whichever is correct, both should not ship. This is exactly the
inconsistency the [ux-consistency] audit is defined to catch (gate line 37).

### F6 — P2 — [mobile]
Interactive targets below touch minimums; some below the WCAG 2.5.8 AA floor.

Measured on orders-list @375 (CSS px, live rects):
28.0 x 28.0 View / Edit / Delete row actions RowActions.tsx:41,47,55
32.0 x 30.0 pagination page numbers Pagination.tsx:104-113
35.8 x 30.0 pagination page numbers (2-digit) Pagination.tsx:104-113
56.5 x 30.0 Prev / 58.6 x 30.0 Next Pagination.tsx:96,120
40.0 x 40.0 Filters toggle FilterBar.tsx:54
78.3 x 20.0 sortable column header link DataTable.tsx:56-62
48.5 x 20.0 sortable column header link DataTable.tsx:56-62
121.8 x 16.0 row link (order no) app/orders/page.tsx:28

- 44x44 (WCAG 2.5.5 AAA / iOS-Android guidance): everything above fails.
- 24x24 (WCAG 2.5.8 AA, WCAG 2.2): the 20px-tall column-header links and the
16px-tall row links FAIL. The 28x28 row actions pass AA but not AAA.
The 20/16px heights are the ones worth treating as a real accessibility defect
rather than a comfort issue.

### F7 — P2 — [mobile]
Modal spans edge-to-edge below 414px — no gutter.

Site: components/ui/Modal.tsx:33-38
wrapper `fixed inset-0 z-50 flex items-center justify-center` (no padding)
dialog `relative card shadow-lg w-full p-5` + max-w-sm/max-w-lg

Measured (quick-create-modal, real Modal + real embedded ItemForm):
vw=320 dialog w=320.0 left=0 <-- full bleed, border+radius clipped
vw=375 dialog w=375.0 left=0 <-- full bleed
vw=414 dialog w=414.0 left=0 <-- full bleed
vw=640 dialog w=512.0 left=64
`max-w-lg` (512px) never binds below 512px, so `w-full` wins and the card's
rounded border sits flush against both screen edges. Wrapper needs `p-4`.

DISPROVED, and therefore NOT reported as a finding: I hypothesised the same
modal would clip vertically (no max-height, no overflow-y-auto, centred in a
fixed inset-0 parent). Tested at 812x375, 740x360 and 375x667 landscape/short
viewports — tallest real content (ItemForm) measures 345-349px and fits every
time; clipsAbove=false, clipsBelow=false in all three. The risk is latent for
future taller content but is not a defect today.

### F8 — P2 — [components]
FilterBar fields are a hard `w-[200px]`, not fluid.

Site: components/ui/FilterBar.tsx:68 `<div className="w-[200px]">`

Four filter fields on Orders (page.tsx:57-70) at a fixed 200px + gap-[25px]
(FilterBar.tsx:66) = 875px of intrinsic width in a `flex-wrap` row. Wraps
rather than shrinks, and at mobile widths contributes the residual overflow
that survives F1's fix. Should be `w-full sm:w-[200px]` or a min-w-0 basis.

### F9 — P3 — [typography]
No responsive type scale exists.

Measured h1 font-size, orders-list and supplier-new, all 8 viewports:
320 -> 20px 375 -> 20px 414 -> 20px 640 -> 20px
768 -> 20px 1024 -> 20px 1280 -> 20px 1920 -> 20px
Distinct rendered sizes across the whole surface: 14px/20px, 16px/24px,
20px/28px. Nothing scales. PageHeader.tsx:13 is a flat `text-xl`.

Held at P3 deliberately: a dense admin/data UI legitimately uses a fixed
scale, and a 20px h1 is not broken at 1920. Flagged because the [typography]
audit is defined as "type scale that doesn't adapt across breakpoints"
(gate line 35) and the answer here is a measured, unambiguous "it doesn't".

Counter-note, in the implementation's favour: `.field-input`/.field-control
carry `sm:text-sm` (globals.css:28,38), i.e. 16px below 640px and 14px above.
That is the correct idiom — 16px inputs prevent iOS auto-zoom on focus.
Measured 16px/24px on supplier-new at mobile widths. Working as intended.

### F10 — P3 — [tablet]
The `md` breakpoint is entirely unimplemented.

0 occurrences of md: in app/ + components/ (section 2). Consequence, measured:
every geometry on every screen at vw=768 is byte-identical to vw=320 —
orders-list main 1288.1 / docSW 1544 at both; order-new grid
"243.766px 243.766px" at both. Tablet portrait renders the mobile layout with
no adaptation whatsoever. This is the aggregate cause behind F1/F2/F3 rather
than a separate defect site, recorded because tablet is the breakpoint this
gate calls out as most often left untested (gate lines 33-34).

### F11 — P3 — [desktop]
List screens have no max-width; content stretches unbounded.

Measured orders-list at 1920: main 1664px, inner content 1616px, and the
6 data columns stretch to fill it. Forms are bounded (max-w-[772px],
OrderForm max-w-3xl) but list screens are not, so row scan-distance grows
without limit on wide monitors.

### F12 — P3 — [desktop][mobile]
Collapsed sidebar rail depends on a hover tooltip for its labels.

Site: components/layout/Sidebar.tsx:113 `title={collapsed ? item.label : undefined}`
In the collapsed (w-16) rail the link's only visible content is an
aria-hidden icon; the label exists solely as a `title` tooltip. Accessible
name is fine (title is the accname fallback), but a touch user has no hover,
so a collapsed rail on a touch device is unlabelled icons. Non-hover fallback
is what the [desktop] audit asks for (gate line 30).

--------------------------------------------------------------------------------
5. TALLY
--------------------------------------------------------------------------------
P1 : 3 (F1 F2 F3)
P2 : 5 (F4 F5 F6 F7 F8)
P3 : 4 (F9 F10 F11 F12)

By audit tag (findings carry multiple tags; counted once each per gate step 2):
[components] F1 F2 F3 F8
[desktop] F1 F4 F11 F12
[mobile] F1 F2 F3 F6 F7 F12
[tablet] F1 F3 F4 F10
[typography] F9
[ux-consistency] F5

gate_status = FAIL — 3 unresolved P1 findings (gate line 85: PASS requires zero).

--------------------------------------------------------------------------------
6. AUTO-FIX (gate step 3) — NOT APPLIED
--------------------------------------------------------------------------------
The gate's step 3 asks for safe fixes to be applied directly. This node was
invoked with explicit read-only scope ("You have no write access, by design"),
which overrides that step for this run. Every finding is therefore reported with
fixed=false. No source file was modified; the only write performed is this
evidence file, which the returns contract requires.

Had writes been permitted, F1 (add `min-w-0` to <main>) was the one fix already
verified end-to-end by re-measurement (section 4, F1) and would have been safe to
apply. F3 and F8 are mechanical prefix additions but were not re-measured post-fix
and should not have been applied blind. F2 is a redesign — report only, per step 3.

--------------------------------------------------------------------------------
7. REPRODUCTION
--------------------------------------------------------------------------------
./node_modules/.bin/tailwindcss -c tailwind.config.ts -i app/globals.css \
-o /tmp/w16b/app.css --minify
node /tmp/w16b/entry.cjs # render real components -> /tmp/w16b/out/*.html
node /tmp/w16b/measure.cjs # 5 screens x 8 viewports -> measurements.json
node /tmp/w16b/verify.cjs # F1 min-w-0 proof + F7 clipping disproof
node /tmp/w16b/grid.cjs # F3 grid track widths
node /tmp/w16b/thresh.cjs # F4 two-column threshold sweep

Harness lives outside the checkout (/tmp/w16b) so the repo stays clean.
================================================================================
(7-7/8)