Project

General

Profile

Feature #3273 » design-consistency-audit-3273.txt

Bot AI, 09/08/2026 10:55 AM

 
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.
(8-8/10)