|
Gate 7 — Responsive Audit — Ticket #3269 (Password Reset Flow)
|
|
================================================================
|
|
Date: 2026-09-04
|
|
Feature files audited:
|
|
- middleware.ts (no UI — excluded from responsive checks)
|
|
- app/(auth)/forgot-password/page.tsx
|
|
- app/(auth)/verify-otp/page.tsx
|
|
- app/(auth)/reset-password/page.tsx
|
|
- components/auth/ForgotPasswordForm.tsx
|
|
- components/auth/VerifyOtpForm.tsx
|
|
- components/auth/CompleteResetForm.tsx
|
|
|
|
Related files inspected (not in feature_files but consumed by them):
|
|
- components/auth/BrandPanel.tsx (shared left-hand panel, w-[45%] shrink-0)
|
|
- components/ui/SubmitButton.tsx
|
|
- components/ui/fields/PhoneInput.tsx (country dropdown w-72)
|
|
- components/ui/fields/PasswordField.tsx (eye toggle touch target)
|
|
- components/ui/button.tsx (buttonVariants CVA)
|
|
- components/ui/Toast.tsx (fixed bottom-4 right-4)
|
|
- app/(auth)/layout.tsx (session guard only, no styling)
|
|
- app/(auth)/login/page.tsx + components/auth/SignInForm.tsx (UX-consistency baseline)
|
|
- app/globals.css (field-input/field-control/field-error layer components)
|
|
- tailwind.config.ts (breakpoints: default Tailwind sm:640 md:768 lg:1024 xl:1280)
|
|
|
|
screen_inventory: <missing> — UX-consistency audit limited to login page comparison.
|
|
|
|
Skills resolved (all via read_file, path 3 — no slash-command layer available):
|
|
1. nextjs-tailwind-responsive-components-audit — SKILL.md read ✓
|
|
2. nextjs-tailwind-responsive-desktop-audit — SKILL.md read ✓
|
|
3. nextjs-tailwind-responsive-mobile-audit — SKILL.md read ✓
|
|
4. nextjs-tailwind-responsive-tablet-breakpoint-audit — SKILL.md read ✓
|
|
5. nextjs-tailwind-responsive-typography-audit — SKILL.md read ✓
|
|
6. nextjs-tailwind-responsive-ux-consistency-audit — SKILL.md read ✓
|
|
|
|
========================================================================
|
|
MERGED FINDINGS (deduplicated, tagged by source audit)
|
|
========================================================================
|
|
|
|
--- FINDING 1 [P1] ---
|
|
Tags: [mobile][components][tablet]
|
|
File: app/(auth)/forgot-password/page.tsx, app/(auth)/verify-otp/page.tsx,
|
|
app/(auth)/reset-password/page.tsx (all three pages)
|
|
Root cause in: components/auth/BrandPanel.tsx (w-[45%] shrink-0, no responsive hide)
|
|
Component: Auth page layout (BrandPanel + form side-by-side)
|
|
Viewport(s) affected: 320-767px (all mobile and small-tablet widths)
|
|
Problem: BrandPanel renders at w-[45%] with shrink-0 at EVERY viewport — no
|
|
responsive hide/collapse. At 320px mobile, BrandPanel takes 144px and the form
|
|
area gets only 176px. At 375px, BrandPanel=169px / form=206px. At 430px,
|
|
BrandPanel=194px / form=236px. All three widths are far below the forms'
|
|
designed min content width (~360px). The forms are effectively unusable:
|
|
inputs are impossibly narrow, PhoneInput's country selector consumes most of
|
|
the available width, and the BrandPanel's own content (with left-12 + right-12
|
|
= 96px horizontal padding) has only 48px of usable text width at 320px — its
|
|
text-3xl heading is unreadable.
|
|
Evidence:
|
|
BrandPanel.tsx line 7: className="relative h-full w-[45%] shrink-0 overflow-hidden bg-accent"
|
|
forgot-password/page.tsx line 14: <div className="fixed inset-0 z-[100] flex overflow-hidden bg-surface">
|
|
No breakpoint-conditional rendering of BrandPanel. Same pattern on all three pages.
|
|
Login page (app/(auth)/login/page.tsx) uses the identical pattern — this is an
|
|
inherited layout, but it is still a P1 on these pages because they ship the
|
|
same broken mobile experience.
|
|
Why it matters: All three password-reset pages are completely unusable on any
|
|
phone. A user who forgot their password and is on mobile cannot complete the
|
|
reset flow.
|
|
Recommended fix: Add responsive hide to BrandPanel's root div:
|
|
className="relative hidden h-full w-[45%] shrink-0 overflow-hidden bg-accent md:block"
|
|
And ensure the form wrapper gets padding on mobile:
|
|
className="flex flex-grow items-center justify-center p-6 md:p-0"
|
|
(This also fixes the login page since BrandPanel is shared.)
|
|
Risk of change: Low — BrandPanel is purely decorative branding, hiding it on
|
|
mobile loses no functionality. The login page gains the same fix.
|
|
Fixed: false
|
|
|
|
--- FINDING 2 [P2] ---
|
|
Tags: [mobile]
|
|
File: app/(auth)/forgot-password/page.tsx, app/(auth)/verify-otp/page.tsx
|
|
Component: Form wrapper (flex-grow container around ForgotPasswordForm / VerifyOtpForm)
|
|
Viewport(s) affected: 320-767px
|
|
Problem: The form wrapper div has no horizontal padding:
|
|
<div className="flex flex-grow items-center justify-center">
|
|
If/when BrandPanel is hidden on mobile, the form (max-w-[360px] w-full)
|
|
would stretch to full viewport width with zero breathing room — inputs
|
|
would touch screen edges. The reset-password page correctly has p-6 on its
|
|
wrapper; these two pages do not.
|
|
Evidence:
|
|
forgot-password/page.tsx line 16: className="flex flex-grow items-center justify-center"
|
|
verify-otp/page.tsx line 37: className="flex flex-grow flex-col items-center justify-center gap-0"
|
|
reset-password/page.tsx line 37: className="flex flex-grow items-center justify-center overflow-y-auto p-6"
|
|
(reset-password has p-6, the other two do not)
|
|
Why it matters: Once the BrandPanel-hide fix is applied, these pages still lack
|
|
the safe-area padding mobile forms need.
|
|
Recommended fix: Add p-6 (or at minimum px-4 sm:px-6) to both page wrappers,
|
|
matching reset-password's existing pattern.
|
|
Risk of change: None — adding padding only narrows the max available width,
|
|
well within the form's own max-w-[360px].
|
|
Fixed: false
|
|
|
|
--- FINDING 3 [P2] ---
|
|
Tags: [mobile][components]
|
|
File: components/auth/VerifyOtpForm.tsx (line 120-126)
|
|
Component: Resend code button
|
|
Viewport(s) affected: 320-430px (all mobile)
|
|
Category: touch-targets
|
|
Problem: The "Resend code" / "Resend code in Xs" button is a bare text button
|
|
with no explicit height or padding:
|
|
className="text-sm font-medium text-accent hover:underline ..."
|
|
At text-sm (14px font, ~20px line-height), the rendered touch target is
|
|
approximately 20px tall — well below the 44px accessible minimum for touch
|
|
targets on mobile.
|
|
Evidence:
|
|
VerifyOtpForm.tsx lines 120-131: <button type="button" ... className="text-sm font-medium ...">
|
|
No h-*, py-*, min-h-*, or p-* classes.
|
|
Why it matters: On mobile, users will mis-tap or struggle to hit this button,
|
|
especially after a failed OTP attempt when they need to resend urgently.
|
|
Recommended fix: Add min-h-[44px] and py-2 to ensure the touch target meets
|
|
accessible minimums: className="min-h-[44px] py-2 text-sm font-medium ..."
|
|
Risk of change: Very low — only adds padding/height around existing text.
|
|
Fixed: false
|
|
|
|
--- FINDING 4 [P2] ---
|
|
Tags: [mobile][components]
|
|
File: components/ui/fields/PhoneInput.tsx (line 267-270)
|
|
Component: PhoneInput country dropdown
|
|
Viewport(s) affected: 320-430px
|
|
Category: layout
|
|
Problem: The country selector dropdown has a fixed width of w-72 (288px):
|
|
className="absolute left-0 z-40 mt-1 w-72 rounded-md ..."
|
|
On a 320px mobile viewport (even with BrandPanel hidden), after page padding
|
|
(24px each side), the available width is ~272px. The 288px dropdown overflows
|
|
its container by ~16px, getting clipped or causing horizontal scroll.
|
|
With BrandPanel present (Finding 1), only ~176px is available — massive overflow.
|
|
Evidence:
|
|
PhoneInput.tsx line 270: w-72 (18rem = 288px fixed)
|
|
Why it matters: Users on small phones selecting their country code will see a
|
|
clipped or partially off-screen dropdown, unable to read country names or tap
|
|
options near the right edge.
|
|
Recommended fix: Change w-72 to w-full or max-w-[min(18rem,calc(100vw-3rem))]
|
|
so the dropdown adapts to available width.
|
|
Risk of change: Low — dropdown content already wraps via truncate on country names.
|
|
Fixed: false
|
|
|
|
--- FINDING 5 [P2] ---
|
|
Tags: [tablet]
|
|
File: components/auth/CompleteResetForm.tsx (line 69)
|
|
Component: CompleteResetForm two-column grid
|
|
Exact width or range affected: 1024-1279px (lg: active, xl: not yet)
|
|
Category: grid
|
|
Problem: The password form grid transitions from single-column to two-column
|
|
at lg: (1024px viewport) via lg:grid-cols-[1fr_280px]. However, with BrandPanel
|
|
taking 45% of viewport width and the outer max-w-[600px] wrapper, the actual
|
|
available width for the 1fr column at 1024px is only ~195px:
|
|
1024px viewport * 55% = 563px right panel
|
|
- 48px padding (p-6 = 24px * 2) = 515px
|
|
- 280px policy card - 40px gap (gap-10) = 195px for password fields
|
|
At 195px, the password labels ("Re-enter New Password") wrap and the input
|
|
fields (minus 36px for the eye toggle, pr-9) have only ~159px of typing area.
|
|
Evidence:
|
|
CompleteResetForm.tsx line 69: className="grid grid-cols-1 gap-10 lg:grid-cols-[1fr_280px]"
|
|
reset-password/page.tsx line 38: className="w-full max-w-[600px]"
|
|
BrandPanel.tsx line 7: w-[45%] shrink-0
|
|
At 1280px viewport: 1280*0.55=704, -48=656, capped at 600, -280-40 = 280px — workable.
|
|
Why it matters: The two-column layout is cramped to the point of unusability in
|
|
the 1024-1279px range. Password fields at ~195px with the eye toggle are too
|
|
narrow for comfortable input.
|
|
Recommended fix: Change lg:grid-cols-[1fr_280px] to xl:grid-cols-[1fr_280px]
|
|
so the two-column layout only activates at 1280px+ where the form column
|
|
gets ~280px (acceptable). Single-column at lg: is perfectly fine — the
|
|
password policy card stacks below the form as it does on mobile.
|
|
Risk of change: Low — the single-column layout at lg: is the same as the
|
|
already-working mobile/tablet layout. Only changes when the side-by-side
|
|
view appears, not whether it appears at all.
|
|
Fixed: false
|
|
|
|
--- FINDING 6 [P3] ---
|
|
Tags: [typography]
|
|
File: components/auth/BrandPanel.tsx (lines 16-22)
|
|
Component: BrandPanel tagline heading
|
|
Viewport(s) affected: 640-1024px (tablet range where BrandPanel is visible)
|
|
Content type: heading
|
|
Problem: The tagline "Manage your orders, expenses and suppliers in one place."
|
|
uses hardcoded <br /> tags that produce correct line breaks only at one
|
|
specific width. At tablet widths (768px: BrandPanel=345px minus 96px padding
|
|
= 249px content width), these forced breaks produce awkward orphans or
|
|
imbalanced lines because the text wraps at different natural points than the
|
|
forced breaks assume.
|
|
Evidence:
|
|
BrandPanel.tsx lines 17-20:
|
|
<span className="text-3xl font-bold leading-[38px] text-accent-subtle">
|
|
Manage your orders,<br />expenses and suppliers<br />in one place.
|
|
</span>
|
|
Three hard <br /> breaks designed for ~300px+ content width.
|
|
Why it matters: At tablet-portrait width the heading wraps oddly — "in one
|
|
place." ends up on a very short line after a break that was meant to balance
|
|
three roughly-equal lines.
|
|
Recommended fix: Remove <br /> tags and let the text wrap naturally, or use
|
|
CSS (max-w-[Xch]) to constrain line width. If specific breaks are desired,
|
|
use <br className="hidden lg:inline" /> to make them conditional.
|
|
Risk of change: Very low — only affects visual line breaking of decorative text.
|
|
Fixed: false
|
|
|
|
--- FINDING 7 [P3] ---
|
|
Tags: [mobile]
|
|
File: components/ui/fields/PasswordField.tsx (lines 66-71)
|
|
Component: Password eye toggle button (used in CompleteResetForm + SignInForm)
|
|
Viewport(s) affected: 320-430px
|
|
Category: touch-targets
|
|
Problem: The show/hide password toggle button contains only a 16x16px icon
|
|
(h-4 w-4) with no explicit padding or minimum size on the button element:
|
|
className="absolute right-2 top-1/2 -translate-y-1/2 text-text-helper"
|
|
The rendered touch target is approximately 16x16px — far below the 44px
|
|
accessible minimum. (PasswordField.tsx is not in the feature_files list but is
|
|
consumed by CompleteResetForm and the login flow.)
|
|
Evidence:
|
|
PasswordField.tsx line 66-70: <button type="button" ...> containing
|
|
<EyeOff className="h-4 w-4" />
|
|
No min-h, min-w, p-*, or size classes on the button.
|
|
Why it matters: Users on mobile will struggle to toggle password visibility —
|
|
the target is ~3x smaller than the accessible minimum, risking mis-taps into
|
|
the password input itself.
|
|
Recommended fix: Add min-w-[44px] min-h-[44px] flex items-center justify-center
|
|
to the button, or add p-3 to give sufficient padding around the icon.
|
|
Risk of change: Low — only enlarges the invisible tap area. The icon position
|
|
may need right-offset adjustment to stay visually aligned.
|
|
Fixed: false
|
|
|
|
--- FINDING 8 [P3] ---
|
|
Tags: [ux-consistency]
|
|
File: app/(auth)/reset-password/page.tsx vs forgot-password/page.tsx, verify-otp/page.tsx
|
|
Component: Page heading placement
|
|
Viewports compared: all
|
|
Problem: ForgotPasswordForm and VerifyOtpForm render their own <h1> inside the
|
|
form component (at max-w-[360px]), while reset-password/page.tsx renders its
|
|
<h1> OUTSIDE CompleteResetForm in a wider max-w-[600px] wrapper. This means
|
|
the heading on the reset-password page is horizontally offset from the heading
|
|
position on the other two pages — the left edge of the heading aligns with
|
|
different points in the viewport across the three steps of the same flow.
|
|
Evidence:
|
|
forgot-password/page.tsx: <ForgotPasswordForm /> contains <h1> inside
|
|
className="flex w-full max-w-[360px] flex-col ..."
|
|
reset-password/page.tsx lines 38-42: <div className="w-full max-w-[600px]">
|
|
<h1 ...>Set New Password</h1> then <CompleteResetForm /> (form itself has
|
|
max-w-[1080px])
|
|
Why it matters: A user progressing through the 3-step flow sees the heading
|
|
"jump" horizontally when they reach step 3. The layout change is deliberate
|
|
(step 3 needs more width for the password policy card), but the heading
|
|
alignment inconsistency makes the transition feel like a different page rather
|
|
than the next step in a flow.
|
|
Recommended fix: Consider keeping the <h1> inside CompleteResetForm (matching
|
|
the other two forms) and applying the wider max-width only to the form body
|
|
below the heading, so all three headings start at the same horizontal position.
|
|
Alternatively, accept this as intentional given the wider form layout.
|
|
Risk of change: Low — heading placement only, no functional change.
|
|
Fixed: false
|
|
|
|
--- FINDING 9 [P3] ---
|
|
Tags: [desktop]
|
|
File: app/(auth)/reset-password/page.tsx, components/auth/CompleteResetForm.tsx
|
|
Component: CompleteResetForm submit button alignment
|
|
Viewport(s) affected: 1280px+ (desktop, where xl:grid-cols-[1fr_280px] would activate)
|
|
Category: content-width
|
|
Problem: The "Set New Password" SubmitButton sits in <div className="mt-8">
|
|
outside the grid, inheriting the form's full width. On desktop, the button
|
|
renders at its intrinsic content width while ForgotPasswordForm and
|
|
VerifyOtpForm both use className="w-full" on their submit buttons (constrained
|
|
by parent's max-w-[360px]). This produces a narrower button relative to the
|
|
form width on the reset page compared to the other two pages.
|
|
Evidence:
|
|
CompleteResetForm.tsx lines 167-170: <div className="mt-8"><SubmitButton ...>
|
|
No w-full on SubmitButton, no max-w constraint on its container.
|
|
ForgotPasswordForm.tsx line 55: <SubmitButton className="mt-6 w-full">
|
|
VerifyOtpForm.tsx line 114: <SubmitButton className="mt-6 w-full">
|
|
Why it matters: Minor visual inconsistency — the button occupies less of the
|
|
form's visual width than on the other two pages. Not a usability problem.
|
|
Recommended fix: No action strictly needed. Optionally constrain the button
|
|
wrapper with max-w-[460px] to align with the field column.
|
|
Risk of change: Negligible.
|
|
Fixed: false
|
|
|
|
========================================================================
|
|
SUMMARY
|
|
========================================================================
|
|
P1 findings: 1 (BrandPanel not hidden on mobile — all three pages unusable on phones)
|
|
P2 findings: 4 (missing form padding, touch target on resend button, dropdown overflow,
|
|
CompleteResetForm grid too early at lg:)
|
|
P3 findings: 4 (hardcoded br tags, password toggle touch target, heading placement,
|
|
button width)
|
|
Total: 9 findings
|
|
|
|
Breakpoints reviewed: base (mobile-first), sm (640px), md (768px), lg (1024px), xl (1280px)
|
|
|
|
GATE STATUS: FAIL
|
|
Reason: 1 unresolved P1 finding — the auth layout renders BrandPanel at all
|
|
viewports, making all three password-reset pages unusable on mobile (320-767px).
|
|
|
|
Auto-fix: Read-only access — no auto-fixes applied. All findings reported as-is.
|
|
|
|
Note: middleware.ts contains no UI/responsive code (it is Edge middleware for
|
|
auth routing) and was correctly excluded from all six audits.
|
|
|
|
Note: BrandPanel.tsx and PhoneInput.tsx are NOT in the feature_files list but
|
|
are directly consumed by the feature files. Findings on those components are
|
|
reported because they determine the responsive behavior of the audited pages.
|
|
The login page (app/(auth)/login/page.tsx) shares the same BrandPanel pattern
|
|
and would benefit from the same fix — but it is outside this ticket's scope.
|
|
|
|
Note: screen_inventory was <missing>, so the UX-consistency audit was limited to
|
|
comparing the three password-reset pages against each other and against the
|
|
login page (the only other auth-group page).
|