Skip to content

chore(web): drive React Doctor to zero across web habits/ui components (#243) - #505

Merged
thomasluizon merged 2 commits into
mainfrom
chore/rd-web-habits-ui
Jul 14, 2026
Merged

chore(web): drive React Doctor to zero across web habits/ui components (#243)#505
thomasluizon merged 2 commits into
mainfrom
chore/rd-web-habits-ui

Conversation

@thomasluizon

Copy link
Copy Markdown
Owner

Burn down all React Doctor findings under apps/web/components/habits/** and apps/web/components/ui/** (bucket B1) — 93 findings to zero for this file-set. Branched off current main so it carries PR #500 (render-existing-reminders).

Fixed properly (34)

  • no-inline-exhaustive-style (13) — hoisted static style objects to module scope (spread a static base for the mixed static/dynamic cases).
  • js-set-map-lookups (5) — O(1) Set lookups instead of array.includes() inside .map().
  • button-has-type (5) — explicit type="button" on non-submit buttons.
  • only-export-components (4) — extracted pure helpers into sibling modules (app-bar-right-action.ts, date-group-label.ts, empty-state-message.ts) and un-exported the internal-only resolveReminderLabel.
  • prefer-module-scope-pure-function (2) / prefer-module-scope-static-value (1) — moved stateless helpers/styles out of the component body.
  • no-usememo-simple-expression (1) — dropped a useMemo whose reference is never relied on.
  • rerender-memo-with-default-value (2) — hoisted [] / {} prop defaults to module constants.
  • no-ref-current-in-render (1) — moved the latest-ref write into a useEffect (reads only happen in post-commit callbacks; behavior-preserving, concurrent-safe).
  • no-unguarded-browser-global (bulk-action-bar) — added a useIsClient guard so the createPortal target is only read on the client (this was the one genuinely unguarded portal — a real latent SSR-crash fix).

Justified react-doctor-disable-next-line suppressions (59, each WHY + #243)

  • use-lazy-motion (7) — LazyMotion migration is app-wide (shared provider + converting every motion.* across components/**); a partial per-file swap yields no bundle benefit and risks unprovided m. Consistent with chore(web): drive React Doctor to zero across web app routes (#243) #501.
  • no-unguarded-browser-global (9) — each createPortal is behind an existing useIsClient/mounted guard the rule cannot trace; unreachable during SSR.
  • exhaustive-deps (10) — values derived from profile/query members are recomputed every render and already listed; the rule unwraps them to the source member (false positive).
  • no-tiny-text (7) — intentional captions/badges/eyebrows per DESIGN.md (weekday headers, AI eyebrows, Kit Badge, disclosure fine-print).
  • no-giant-component (4) — cohesive modal/list surfaces; extraction deferred without visual QA.
  • dangerous-html-sink (2) — DOMPurify-sanitized markdown / linkified-description sinks with fixed allowlists.
  • no-array-index-as-key (2) — fixed-order, id-less habit sub-data (checklist items, scheduled reminders); index disambiguates a composite key.
  • prefer-html-dialog (2) — intentional non-modal bottom-anchored notification prompts with custom positioning.
  • no-prop-callback-in-effect (2) / no-prop-callback-in-render (1) / no-pass-data-to-parent (1) / no-pass-live-state-to-parent (1) — access-gate redirect + legit cross-component notifications / adjusting-state-during-render.
  • no-large-animated-blur (2) — intentional small (48px) glass control per DESIGN.md; blur is static.
  • nextjs-no-client-side-redirect (1) — gate depends on client-fetched profile; no server-side signal.
  • prefer-tag-over-role (1) — habit row wraps nested interactive controls a native <button> cannot legally contain (div+role=button with full keyboard handling).
  • rendering-hydration-mismatch-time (1) — grid renders only inside a client-only overlay; no-outline-none (1)focus-within ring present; click-events-have-key-events (1) — capture-phase wrapper over the real button; no-autofocus (1) — search field in a user-invoked picker.

Verification (all green, foreground)

  • apps/web typecheck (tsc --noEmit) — pass
  • apps/web ESLint — 0 errors (3 pre-existing warnings, outside this file-set)
  • apps/web Vitest — 2268/2268 pass
  • React Doctor --scope changed --base origin/main --blocking error — 0 new
  • Full apps/web React Doctor scan — this file-set at 0

Refs #243 (React Doctor burn-down: web habits/ui)

#243)

Burn down all React Doctor findings under apps/web/components/habits/** and
apps/web/components/ui/** (93 findings) to zero for this file-set.

Fixed properly (34):
- no-inline-exhaustive-style (13): hoisted static style objects to module
  scope (spreading a static base for the mixed static/dynamic cases).
- js-set-map-lookups (5): O(1) Set lookups instead of array.includes() in maps.
- button-has-type (5): explicit type="button" on non-submit buttons.
- only-export-components (4): extracted pure helpers to sibling modules
  (app-bar-right-action.ts, date-group-label.ts, empty-state-message.ts) and
  un-exported the internal-only resolveReminderLabel.
- prefer-module-scope-pure-function (2) / prefer-module-scope-static-value (1):
  moved stateless helpers/styles out of the component body.
- no-usememo-simple-expression (1): dropped a useMemo whose ref is never relied on.
- rerender-memo-with-default-value (2): hoisted [] / {} defaults to constants.
- no-ref-current-in-render (1): moved the latest-ref write into a useEffect.
- no-unguarded-browser-global (bulk-action-bar): added a useIsClient guard so
  the createPortal target is only read on the client (real SSR-crash guard).

Justified react-doctor-disable-next-line suppressions (59, WHY + #243):
- use-lazy-motion (7): LazyMotion migration is app-wide (shared provider +
  every motion.* across components/**); a partial per-file swap risks unprovided m.
- no-unguarded-browser-global (9): each createPortal is behind an existing
  useIsClient/mounted guard the rule cannot trace; unreachable during SSR.
- exhaustive-deps (10): values derived from profile/query members are recomputed
  and already listed; the rule unwraps them to the source member (false positive).
- no-tiny-text (7): intentional captions/badges/eyebrows per DESIGN.md.
- no-giant-component (4): cohesive modal/list surfaces; extraction deferred
  without visual QA.
- dangerous-html-sink (2): DOMPurify-sanitized markdown/link sinks.
- no-array-index-as-key (2): fixed-order id-less habit sub-data (checklist,
  reminders); index disambiguates a composite key.
- prefer-html-dialog (2): intentional non-modal bottom-anchored prompts.
- no-prop-callback-in-effect (2) / no-prop-callback-in-render (1) /
  no-pass-data-to-parent (1) / no-pass-live-state-to-parent (1): access-gate
  redirect and legit cross-component notifications / adjusting-state-during-render.
- no-large-animated-blur (2): intentional small glass control per DESIGN.md.
- nextjs-no-client-side-redirect (1): gate depends on client-fetched profile.
- prefer-tag-over-role (1): row wraps nested interactive controls a <button>
  cannot legally contain.
- rendering-hydration-mismatch-time (1): grid renders only inside a client-only
  overlay; no-outline-none (1): focus-within ring present;
  click-events-have-key-events (1): capture-phase wrapper over the real button;
  no-autofocus (1): search field in a user-invoked picker.

Refs #243 (React Doctor burn-down: web habits/ui)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@vercel

vercel Bot commented Jul 13, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

1 Skipped Deployment
Project Deployment Actions Updated (UTC)
orbit-ui-mobile-web Ignored Ignored Jul 14, 2026 12:54am

Request Review

@claude claude Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

/pr-review — PR #505: chore(web): drive React Doctor to zero across web habits/ui components (#243)

Recommendation: APPROVE

Mechanical React Doctor burn-down (bucket B1: apps/web/components/habits/** + apps/web/components/ui/**, 93 findings → 0) across 47 files. Reviewed the full diff against .claude/skills/pr-review/rubric.md, orchestrating design-reviewer and parity-checker.

Summary by dimension

  • Correctness — Two genuine (non-lint) fixes ship alongside the suppressions/hoists, both verified safe:
    • bulk-action-bar-v2.tsx: adds a useIsClient() mount guard before createPortal, matching the identical pattern already used in AppOverlay, CenteredOverlay, ConfirmDialog, ContextMenu, Popover. This was the one genuinely unguarded portal (real latent SSR-crash fix), not a behavior regression.
    • habit-list.tsx: moves promptDataRef.current = {...} from the render body into a useEffect. Confirmed every read site of promptDataRef is a post-mutation event-handler callback, never a render path — the timing change is safe, and it matches mobile's habit-list.tsx, which already does this in useEffect (parity-checker verified).
  • Dead coderesolveReminderLabel was un-exported (only-export-components fix); confirmed by grep it has zero external references and is only used inside habit-form-fields.tsx. No orphaned exports found.
  • Comment policy — All 59 react-doctor-disable-next-line suppressions carry a WHY clause plus an https://github.com/.../issues/243 URL, satisfying no-comments.cjs's URL_RE allowance regardless of the DIRECTIVE regex. Spot-checked several non-obvious suppression claims against the surrounding code and all hold:
    • field-input.tsx no-outline-nonefocus-within:shadow-[inset_0_0_0_2px_var(--primary)] on the wrapping div does supply the focus ring.
    • back-to-top.tsx no-large-animated-blur — control is 48×48px, blur is static (only opacity/transform transition).
    • trial-expired-modal.tsx no-unguarded-browser-global-in-render-or-hook-initlocalStorage.getItem sits behind a mounted && short-circuit earlier in the same expression.
  • Type safety / console.log — No any/as any escape hatches or console.log introduced.
  • DESIGN.md / AI-slop (design-reviewer) — All 12 hoisted style objects checked value-for-value against DESIGN.md's canon table: semantic tokens only, no raw --slate-*/hardcoded rgba, dimensions match documented primitives (e.g. CODE_BOX_STYLE matches the OTP spec byte-for-byte). PASS, no token/value drift.
  • Parity (parity-checker) — Web-only React Doctor burn-down; React Doctor has no mobile equivalent. The 2 behavior-adjacent changes are correctly N/A: the SSR guard is web-only (mobile has no hydration), and mobile's habit-list.tsx already uses the useEffect pattern. Precedented by the already-merged #501 (web routes) / #502 (mobile hooks) split for the same issue #243. No missing mirrors.
  • i18n — No new user-facing strings; all t() calls reference pre-existing keys.
  • Contract drift / Security / Backend hard rules / FEATURES.md — N/A, no packages/shared type changes, no orbit-api touched, no new/changed feature surface.

Findings

None at Critical/High. No concretely-actionable Medium findings survived — this is a clean, well-scoped mechanical refactor with two verified-safe incidental fixes.

@thomasluizon
thomasluizon merged commit c6f70c9 into main Jul 14, 2026
3 checks passed
@thomasluizon
thomasluizon deleted the chore/rd-web-habits-ui branch July 14, 2026 00:54
@sonarqubecloud

Copy link
Copy Markdown

Quality Gate Failed Quality Gate failed

Failed conditions
79.4% Coverage on New Code (required ≥ 80%)

See analysis details on SonarQube Cloud

@claude claude Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review: PR #505 — chore(web): drive React Doctor to zero across web habits/ui components (#243)

Scope: PR #505 in thomasluizon/orbit-ui-mobile (47 files, apps/web/components/habits/** + apps/web/components/ui/** + one test file)
Recommendation: APPROVE

Summary

This is a mechanical, lint-driven cleanup (React Doctor findings → 0 for this file-set): hoisting static inline style={{...}} objects to module-scope constants, array.includes()Set.has() inside .map(), explicit type="button", three pure-helper extractions to sibling modules, one genuine SSR-crash fix (useIsClient guard added to bulk-action-bar-v2.tsx before its createPortal), one ref-write moved from render into useEffect, and ~59 react-doctor-disable-next-line <rule> -- WHY … https://…/issues/243 suppression comments. Every changed hunk across all 47 files was read directly; the highest-risk claims were independently verified rather than trusted from the PR description. No new commits since a prior identical review on this PR — this re-confirms the same conclusion after the design-reviewer and parity-checker subagents (dispatched but slow to return during the original pass) both came back with independent PASS verdicts.

Findings

Critical

None.

High

None.

Medium

None.

Low / Info

  • Info: all react-doctor-disable-next-line comments carry a WHY + https://github.com/…/issues/243 URL, satisfying eslint-rules/no-comments.cjs's URL_RE allowance — spot-checked the rule file directly (eslint-rules/no-comments.cjs:15,22) to confirm this isn't relying on an unenforced convention.
  • Info: two of the more load-bearing suppression claims were verified against the actual code rather than taken at face value — field-input.tsx's no-outline-none claim (the well's focus-within:shadow-[...] ring) and create-habit-modal.tsx's exhaustive-deps/hasProAccess claim (derived value, listed by name in both callback deps) — both hold up.
  • Info: the habit-list.tsx promptDataRef write moved from synchronous render into a useEffect (a real timing change, not just a style hoist). Every read site was traced (checkAndPromptParentLog, only invoked from use-bulk-actions.ts inside a useCallback fired on bulk-mutation success): all reads happen from user-triggered, post-mount callbacks, never during the same render pass that writes the ref — behavior-preserving and concurrent-safe as claimed. The mobile mirror (apps/mobile/components/habit-list.tsx) already has the identical useEffect pattern with matching dependencies, confirmed by the parity-checker subagent.
  • Info (pre-existing, not introduced by this PR): apps/web/components/ui/settings-group.tsx PRO_BADGE_STYLE doesn't byte-match the kit's canonical Badge primitive spec in DESIGN.md — flagged by the design-reviewer subagent as a follow-up candidate, not a regression from this diff (untouched styling, purely mechanical hoist here).

Subagents

Agent Verdict
parity-checker PASS. The one genuine behavior change (habit-list.tsx ref-write timing) already has an identical useEffect pattern on the mobile mirror. The three extracted helpers (date-group-label.ts, empty-state-message.ts, app-bar-right-action.ts) all have pre-existing, logically identical mobile equivalents. The bulk-action-bar-v2.tsx SSR guard is web-only (no SSR/DOM portals on React Native) — no mirror needed.
design-reviewer PASS. Every hoisted style constant uses semantic tokens (var(--fg-*), var(--primary), var(--status-*-text), var(--hairline*)) — no raw hex/slate/hardcoded violet. No transition-all, no h-screen introduced. Radii/borders/status-text tokens match DESIGN.md's kit tables. A11y (aria-labels, 44px+ hit targets) intact. Two no-tiny-text suppressions in reschedule-sheet.tsx are legitimate caption/disclosure text, not shrunk body copy. One pre-existing (not-introduced-by-this-PR) PRO_BADGE_STYLE drift noted as a follow-up candidate, not a blocker.
i18n-syncer N/A — no new/changed user-facing strings; all t() calls reuse existing keys.
contract-aligner N/A — no packages/shared/src/types/*, endpoints.ts, or orbit-api files touched.
security-reviewer N/A — no orbit-api code touched.

Validation

Check Result
Lint / Type check / Tests Not re-run locally (CI runs Build / Unit Tests / SonarCloud as separate required checks per this workflow's instructions); PR self-reports 0 ESLint errors, tsc --noEmit pass, Vitest 2268/2268 pass, React Doctor 0 new / 0 remaining for this file-set.

Deferred — N/A dimensions

  • Backend hard rules / FEATURES.md parity: N/A, no orbit-api files, no user-facing feature surface change.
  • Cross-repo dimensions (contract-aligner's orbit-api side, backward-compat guard's orbit-api side): not verifiable in this CI job — orbit-api is not checked out here. No indication this diff needs it (no shared-type/endpoint changes).

What's good

  • The bulk-action-bar-v2.tsx fix is a real, previously-latent SSR/hydration bug (unguarded DOM query inside createPortal) fixed correctly and minimally.
  • Every suppression comment cites a specific, checkable reason with an issue link rather than a blanket "false positive."
  • Clean extractions with zero dangling references, matching pre-existing mobile equivalents — genuine cross-platform parity, not just claimed.

Recommendation

Safe to merge. No Critical/High findings, no contract or parity concerns. Both dispatched subagents (design-reviewer, parity-checker) independently confirmed PASS.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant