|
Design Consistency Audit — Ticket #3273 (Forgot Password Flow)
|
|
Gate 6 | 2026-09-08
|
|
================================================================
|
|
|
|
Skill resolution:
|
|
- nextjs-tailwind-design-system-audit: Option 3 (read_file) —
|
|
/opt/drcarrot-deploy/cross-repo-orchestration/skills/nextjs-tailwind-design-system-audit/SKILL.md
|
|
- nextjs-tailwind-accessibility-audit: Option 3 (read_file) —
|
|
/opt/drcarrot-deploy/cross-repo-orchestration/skills/nextjs-tailwind-accessibility-audit/SKILL.md
|
|
|
|
Files audited:
|
|
- app/(auth)/forgot-password/page.tsx
|
|
- app/(auth)/forgot-password/verify/page.tsx
|
|
- app/(auth)/forgot-password/reset/page.tsx
|
|
- components/auth/ForgotPasswordForm.tsx
|
|
- components/auth/VerifyOtpForm.tsx
|
|
- components/auth/ResetPasswordForm.tsx
|
|
- components/auth/SignInForm.tsx
|
|
|
|
Design spec: docs/specs/ticket-3273-design.md
|
|
Tailwind config: tailwind.config.ts (read and cross-referenced)
|
|
|
|
================================================================
|
|
PART 1 — DESIGN SYSTEM AUDIT
|
|
================================================================
|
|
|
|
Methodology: Read tailwind.config.ts token definitions, then inspected every
|
|
Tailwind class in each feature file against the design spec's Token Mapping
|
|
table and the Layout / Spacing Values table. Checked for:
|
|
- Arbitrary hex/rgb values that duplicate an existing token
|
|
- Inconsistent typography/spacing for the same UI role
|
|
- Components that should reuse the shared component library but don't
|
|
|
|
Positive baseline (consistency confirmed):
|
|
- All four form components reuse BrandPanel, SubmitButton, Button, and
|
|
PasswordField from the shared component library where the design calls for
|
|
existing components.
|
|
- All semantic colors (accent, text, text-subtle, text-faint, border-field,
|
|
status-success, status-danger, surface) use their Tailwind token classes
|
|
consistently across all files.
|
|
- Typography scale is consistent: auth headings use text-[26px]/leading-8/
|
|
font-bold (same as SignInForm from #3252), reset heading uses text-[20px]/
|
|
font-semibold (per spec), body uses text-sm, footnotes use text-xs/
|
|
leading-[18px].
|
|
- Spacing matches the design spec's arbitrary-value table exactly: w-[360px]
|
|
(steps 1-2), max-w-[480px] (step 3), h-[52px] (OTP boxes), gap-[10px]
|
|
(OTP row), mb-[14px] (back nav), mt-[10px] (forgot link).
|
|
- The two-panel auth layout (fixed inset-0 bg-surface flex overflow-hidden
|
|
z-[100] + BrandPanel + flex-grow centered column) is structurally identical
|
|
across all four forms.
|
|
|
|
Findings:
|
|
|
|
[DS-1] P3 — SVG hardcoded stroke values (pre-existing pattern, not new)
|
|
File: components/auth/ResetPasswordForm.tsx, lines 138, 164
|
|
Problem: SVG check icons use inline stroke={rulesMet[i] ? "rgb(34,158,75)" :
|
|
"rgb(163,163,163)"} and stroke="rgb(163,163,163)" instead of Tailwind
|
|
stroke-status-success / stroke-text-faint classes.
|
|
Values correct: rgb(34,158,75) = #229e4b = status.success; rgb(163,163,163) =
|
|
#a3a3a3 = text.faint. Token values match exactly.
|
|
Context: Copied from components/forms/ResetPasswordForm.tsx (lines 130, 137)
|
|
as instructed by the design spec. Pre-existing pattern, not a regression.
|
|
Recommended fix: Refactor both files to use className="stroke-status-success"
|
|
/ className="stroke-text-faint" and remove inline stroke attributes.
|
|
Risk: None — values identical, only mechanism changes.
|
|
|
|
[DS-2] P3 — Inactive strength bar hardcoded color (pre-existing, documented)
|
|
File: components/auth/ResetPasswordForm.tsx, line 112
|
|
Problem: bg-[rgb(230,230,230)] for empty strength bar segments has no named
|
|
token.
|
|
Context: Design spec explicitly documents this as "Existing pattern — copy
|
|
verbatim." Same usage in components/forms/ResetPasswordForm.tsx. No token
|
|
needed per spec.
|
|
|
|
[DS-3] P3 — BrandPanel SVG fill attributes (pre-existing, not this ticket)
|
|
File: components/auth/BrandPanel.tsx, lines 33-39
|
|
Problem: Decorative SVG uses inline fill="rgb(226,205,176)" (accent.selected),
|
|
fill="rgb(247,241,233)" (accent.subtle), fill="rgb(69,40,40)"
|
|
(accent.DEFAULT) as raw values.
|
|
Context: BrandPanel predates this ticket (#3252). Decorative, aria-hidden.
|
|
SVG fill attributes in this pattern are standard.
|
|
|
|
No P1 or P2 design-system findings. All design-spec token mappings verified
|
|
correct. No off-token colors introduced by this ticket.
|
|
|
|
================================================================
|
|
PART 2 — ACCESSIBILITY AUDIT
|
|
================================================================
|
|
|
|
Methodology: Grepped for outline-none, onClick on non-interactive elements,
|
|
aria-describedby, aria-live, role attributes. Read each form component checking:
|
|
focus indicators, keyboard navigation, label association, semantic HTML, error
|
|
message association, state announcements, disabled-state correctness.
|
|
|
|
Positive baseline (correctly handled):
|
|
- All interactive elements are proper semantic HTML: <button>, <input>, <a>
|
|
(via Next.js <Link>). Zero clickable <div>/<span> instances.
|
|
- All text inputs have associated <label htmlFor> — username, mobile, password
|
|
fields all properly linked.
|
|
- OTP digit boxes each have aria-label={`Digit ${i+1} of ${OTP_LENGTH}`}.
|
|
- OTP auto-focus-advance (onInput) and back-on-Backspace (onKeyDown) provide
|
|
proper keyboard navigation. Paste handler covers bulk entry.
|
|
- PasswordField's show/hide toggle has aria-label="Show password"/"Hide
|
|
password" and uses <button type="button">.
|
|
- Decorative SVGs and icons have aria-hidden="true" throughout.
|
|
- <Button> component includes focus-visible:ring-2 focus-visible:ring-accent
|
|
focus-visible:ring-offset-2 — visible, themed focus ring.
|
|
- Disabled states use actual disabled attribute (SubmitButton, Resend button),
|
|
not just visual opacity.
|
|
- OTP digit focus state replaces outline-none with
|
|
focus:shadow-[inset_0_0_0_2px_theme(colors.accent.DEFAULT)] — visible,
|
|
clearly themed focus indicator.
|
|
|
|
Findings:
|
|
|
|
[A11Y-1] P2 — Error messages not programmatically linked to inputs
|
|
Files: ForgotPasswordForm.tsx (line 68), VerifyOtpForm.tsx (lines 148-150),
|
|
ResetPasswordForm.tsx (via PasswordField), SignInForm.tsx (line 56)
|
|
Category: forms
|
|
Problem: Error messages rendered with className="field-error" are visually
|
|
adjacent to their input fields but not linked via aria-describedby. Screen
|
|
readers navigating by form controls will not announce the error when the
|
|
user focuses the invalid field.
|
|
Evidence: grep for aria-describedby across all components/ returns zero
|
|
matches — this is a codebase-wide pre-existing pattern, not a regression
|
|
introduced by #3273.
|
|
WCAG: 3.3.1 (Error Identification) — partial failure. Errors are visible and
|
|
in DOM order but not programmatically associated.
|
|
Why it matters: Screen-reader users who Tab through form fields won't hear the
|
|
error until they navigate past the input into the error paragraph. On forms
|
|
with multiple fields (ResetPasswordForm), this can cause confusion about
|
|
which field has the error.
|
|
Recommended fix: Add id="<field>-error" to each .field-error <p>, and
|
|
aria-describedby="<field>-error" (conditional on error being truthy) to each
|
|
<input>. PasswordField could handle this internally for all consumers.
|
|
Risk: Low — additive attributes only, no visual change.
|
|
|
|
[A11Y-2] P2 — Dynamic status messages not announced to screen readers
|
|
File: VerifyOtpForm.tsx, lines 154-157 (codeSentMessage), line 150
|
|
(resendError); ForgotPasswordForm.tsx, lines 75-76 (success message)
|
|
Category: aria / states
|
|
Problem: The "A new code has been sent" confirmation, resend-error text, and
|
|
OTP-request success messages appear visually but have no aria-live="polite"
|
|
or role="status". Screen readers won't announce them.
|
|
Evidence: The repo has a Toast component (components/ui/Toast.tsx) with
|
|
role="status" aria-live="polite", but these inline messages don't use it.
|
|
WCAG: 4.1.3 (Status Messages) — failure for dynamically inserted status text
|
|
not in a live region.
|
|
Why it matters: A screen-reader user who clicks "Resend Code" won't know
|
|
whether it succeeded or failed until they manually navigate to the message.
|
|
Recommended fix: Wrap the status/confirmation region in a container with
|
|
aria-live="polite" that persists in the DOM (empty when no message), so
|
|
insertions are announced. Alternatively, fire the existing Toast for these
|
|
messages.
|
|
Risk: Low — additive, no visual change.
|
|
|
|
[A11Y-3] P3 — Mobile input focus indicator at container level
|
|
File: ForgotPasswordForm.tsx, line 50 (focus-within:border-accent), line 64
|
|
(outline-none, focus:outline-none)
|
|
Category: focus
|
|
Problem: The mobile number <input> has outline-none and focus:outline-none.
|
|
The parent <div> uses focus-within:border-accent as the visual focus
|
|
indicator. This means the focus ring appears on the container (the entire
|
|
+91 prefix + input box), not on the input itself.
|
|
Why it matters: Container-level focus indicators are acceptable per WCAG 2.4.7
|
|
(Focus Visible) — the indicator IS visible, it's just at a different
|
|
boundary than expected. Some keyboard users may find it less intuitive, but
|
|
it does not block functionality.
|
|
Recommended fix: No change required. This is an intentional design decision
|
|
for the composite input pattern (prefix + field share one border). If
|
|
desired, could add focus-within:ring-2 focus-within:ring-accent to the
|
|
container for a more prominent indicator.
|
|
Risk: N/A — cosmetic preference.
|
|
|
|
[A11Y-4] P3 — Strength meter not announced to screen readers
|
|
File: ResetPasswordForm.tsx, lines 103-123
|
|
Category: aria
|
|
Problem: The password strength label ("Weak"/"Fair"/"Good"/"Strong") and the
|
|
strength bar visuals update live as the user types, but there is no
|
|
aria-live region wrapping the strength output. Screen readers won't
|
|
announce the change.
|
|
Why it matters: Low impact — the adjacent policy checklist (which shows each
|
|
rule as met/unmet) provides the same information in a more granular way.
|
|
The strength label is a convenience summary, not the sole source of
|
|
feedback.
|
|
Recommended fix: Add aria-live="polite" to the strength meter wrapper <div>
|
|
(line 105).
|
|
Risk: None — additive.
|
|
|
|
[A11Y-5] P3 — Success message in ForgotPasswordForm not in live region
|
|
File: ForgotPasswordForm.tsx, lines 75-76
|
|
Category: aria
|
|
Problem: The success message from requestPasswordResetOtp (line 76,
|
|
text-status-success) is not in an aria-live region. Same issue as A11Y-2
|
|
but for a different form.
|
|
Why it matters: Low impact — after a successful OTP request, the page
|
|
redirects to /forgot-password/verify via the server action's redirect(),
|
|
so the success message is transient and the user quickly moves to a new
|
|
page.
|
|
Recommended fix: Same as A11Y-2 — wrap in aria-live="polite" container.
|
|
|
|
================================================================
|
|
AUTO-FIX ASSESSMENT
|
|
================================================================
|
|
|
|
Per gate instructions, auto-fix is limited to "clearly safe" changes. This
|
|
repo checkout is read-only, so fixes are noted but not applied.
|
|
|
|
Fixable (safe, would auto-fix if write access):
|
|
- None applied (read-only mode).
|
|
|
|
Would be safe to auto-fix:
|
|
- DS-1: Replace inline stroke={} with className on SVGs in
|
|
ResetPasswordForm.tsx — exact value-to-token mapping is verified.
|
|
- A11Y-2/A11Y-5: Add aria-live="polite" wrapper around status messages —
|
|
additive, no visual change.
|
|
- A11Y-4: Add aria-live="polite" to strength meter wrapper — additive.
|
|
|
|
Requires design judgment (not auto-fixable):
|
|
- A11Y-1: aria-describedby wiring requires touching PasswordField's API
|
|
(adding an error-id prop or auto-generating IDs) — cross-component change
|
|
that affects all PasswordField consumers codebase-wide. Safe but scoped
|
|
beyond a single-file auto-fix.
|
|
- DS-2/DS-3: Pre-existing patterns documented as intentional; no fix needed.
|
|
|
|
================================================================
|
|
SUMMARY
|
|
================================================================
|
|
|
|
P1 findings: 0
|
|
P2 findings: 2 (A11Y-1 aria-describedby, A11Y-2 aria-live on status messages)
|
|
P3 findings: 5 (DS-1, DS-2, DS-3, A11Y-3, A11Y-4, A11Y-5)
|
|
|
|
Gate status: PASS — zero unresolved P1 findings.
|
|
|
|
Both P2 findings are pre-existing codebase patterns (aria-describedby not used
|
|
anywhere; inline status messages instead of live regions) that this ticket
|
|
follows correctly but does not fix. No new accessibility regressions introduced.
|
|
All design token mappings verified correct against tailwind.config.ts.
|