|
================================================================================
|
|
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.
|
|
================================================================================
|