================================================================================ 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- -> no slash-command layer present 2. / (repo's own tuned copy) -> no .claude/ directory in oms-web 3. read_file(".claude/skills/nextjs-tailwind-/SKILL.md") scanned, per skill, for: ./.claude/skills/nextjs-tailwind-responsive--audit/SKILL.md NOT FOUND ./.claude/skills/responsive--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 . 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] `
` 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 `
` Collateral: components/ui/DataTable.tsx:42 `
` — 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
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
: 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
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 `
` 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
) 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. ================================================================================