Skip to content

feat: migrate Orbit (web + mobile) to v2 Linear-tactical design - #104

Merged
thomasluizon merged 47 commits into
mainfrom
redesign/v2
May 26, 2026
Merged

thomasluizon merged 47 commits into
mainfrom
redesign/v2

Conversation

@thomasluizon

Copy link
Copy Markdown
Owner

Closes #103.

Summary

Migrates both apps/web (Next.js 15) and apps/mobile (Expo SDK 53) to the v8 Linear-tactical design language:

  • OKLCH-tinted neutrals + Geist Sans/Mono + 3 hairline shadow tiers
  • Dark mode default for new installs; existing scheme preference preserved
  • Astra branding for the AI assistant (user-facing strings only; routes stay /chat)
  • Saturn-ring celebrations via new RingMotif primitive (7 celebration types)
  • Linear-tight habit rows replacing card-based layouts
  • New primitives across both apps: AppBar, SectionLabel, HabitRow, StatusDot, ParentRing, Chip, SettingsRow, InfoRow, BottomTabBar (mobile) / WebNav (web), PullQuote, ConfirmDialog v2, RingMotif

Backend (orbit-api) untouched — no API contract changes, no Zod schema changes, no query key changes.

Phases (committed)

  1. Tokens + Geist fonts (additive)
  2. Primitive components (web + mobile)
  3. Core screens — Today, Astra, Calendar, Profile
  4. Detail drawers + sheets (habit/goal detail, create/edit, notifications, referral, feature guide)
  5. Sub-screens (Streak, Achievements, Retrospective, Upgrade, Preferences, Advanced, AI Settings, Calendar Sync, About, Privacy, Support)
  6. Flows + overlays + celebrations (login, 7-step onboarding incl. Meet Astra, modals, banners, toasts, Saturn-ring celebrations, edge banners)
  7. Cleanup audit + delete legacy — orphan components removed (habit-card / bulk-action-bar v1 / bottom-nav on both apps), ~250 lines of dead CSS dropped, AGENTS.md updated

Validation

  • Web: type-check clean, lint clean (1 pre-existing warning in use-habit-form.ts), vitest 1481/1481 across 151 files
  • Mobile: type-check clean, lint 0/0, vitest 350/350 across 80 files
  • All PRD §15 grep gates pass:
    • EditionMasthead / SectionEyebrow / HabitCard / primaryTintBg / shadowGlow / Manrope → zero hits in apps/ + packages/
    • Claude in user-facing strings → only legitimate third-party product names in MCP integration copy (Claude Web / Desktop / Code)
    • npm ls manrope + npm ls @expo-google-fonts/manrope → empty
    • No new // TODO / // FIXME / // HACK introduced by the migration
    • No new console.log / as any

Net change in Phase 7 alone: -4977 / +25 lines.

Known follow-ups (not blocking)

  • Playwright E2E: structurally blocked in this environment (requires running dev server + .NET API + test DB). The 22 spec files were grep-audited for EditionMasthead / Manrope / habit-card-parent / habit-card-child / fresh-start-orb / habit-emoji-orb etc. — zero matches. No baseline screenshots exist (screenshot: 'only-on-failure' and no toMatchSnapshot/toHaveScreenshot calls), so nothing to delete or regenerate. Tests should be run locally before merge to confirm data-testid selectors still resolve against the migrated DOM. See apps/web/playwright.config.ts for runner setup.
  • ConfirmDialog v1/v2 dual state: v1 (confirm-dialog.tsx) is still the dominant call site across both apps (14+ callers). v2 (confirm-dialog-v2.tsx) is wired into mobile streak.tsx + advanced.tsx only. Leaving the dual-naming state intact for this PR per the explicit instruction in the Phase 7 brief ("if anything still imports the old, leave it"); a future PR can finish migrating the remaining callers and drop v1.

Test plan

  • Boot mobile and web; both render dark mode by default
  • Toggle theme (dark/light) — smooth transition
  • Cycle through all 6 schemes in Preferences
  • Log/skip/delete a habit; sub-habit drill-in works
  • Open Astra chat — empty state, send message, embedded cards render
  • Streak detail — 80px hero number visible
  • Goal detail — metrics panel + linked habits
  • Swipe between days on Today (mobile) / arrow keys (web)
  • Bulk select + queued-delete undo
  • 7-step onboarding incl. "Meet Astra"
  • Manual responsive QA web at 360 / 768 / 1024 / 1440
  • Manual smoke test on Android device
  • Run npm run test:e2e locally before merge — confirm no broken selectors

Co-Authored-By: Claude Opus 4.7 (1M context) noreply@anthropic.com

thomasluizon and others added 16 commits May 21, 2026 17:52
PRD describes the dark-default Linear-tactical migration with Astra
branding and Saturn-ring celebrations. Plan translates the 7 PRD phases
into operational tasks with file inventories, parity matrix, validation
gates, and risks.

Refs #103
Lays the v2 design system foundation alongside the existing shape so
consumers can migrate incrementally across phases 2-6 without Phase 1
cascading rewrites. Phase 7 prunes the legacy exports after the last
component migrates.

- packages/shared: ColorSchemeV2 interface + v2 { hue, dark, light } block
  on all 6 schemes (purple/blue/green/rose/orange/cyan), values from the
  v8 spec at .tmp/render/colors_and_type.css
- apps/mobile/lib/oklch.ts: hand-rolled OKLCH -> OKLab -> linear sRGB
  hex converter (no new runtime dep)
- apps/mobile/lib/theme.ts: createTokensV2(scheme,mode), tokens proxy,
  3 hairline shadow tiers (shadowsV2); legacy colors/surfaces/shadows
  stay until Phase 7 cleanup
- apps/mobile: install @expo-google-fonts/geist + geist-mono + expo-font;
  useFonts gate in providers.tsx loads all weights before render
- apps/mobile/tailwind.config.js: Geist sans/mono families + v2 semantic
  color tokens
- apps/web/app/globals.css: v2 @theme tokens, .scheme-*.dark|.light
  classes per scheme/mode, oklch() neutral computations driven by
  --hue/--chroma-bg/--chroma-fg, .t-eyebrow/.t-h1/.t-row/.t-meta/etc.
  semantic type classes
- apps/web/app/layout.tsx: Manrope -> Geist via next/font; bootstrap
  script tags documentElement with scheme-{name} class
- apps/web/lib/theme-dom.ts: VALID_COLOR_SCHEMES iteration toggles
  scheme-{name} class; light class set alongside dark removal

Refs #103
… + react-hooks v7

Migrating from `next lint` (removed in Next 16) to flat ESLint config
surfaced 71 React 19 / React Compiler errors that were previously
masked. This commit applies the recommended fixes (lazy useState,
useSyncExternalStore, useIsClient helper, "adjusting state when a prop
changes" pattern, etc.) without changing runtime behavior.

Adds hooks/use-is-client.ts (useSyncExternalStore-based hydration
guard) and replaces the legacy `mounted` setState-in-effect pattern
across 14 portal-rendering components. The HabitList imperative
handle no longer exposes `allCollapsed` via render-time ref read; it
publishes via an onAllCollapsedChange callback so the parent mirrors
it in state. The use-gamification milestone tracker moves its
previous-value comparison and ref writes from render into an effect.

Tests adjusted: use-offline tests now also mutate navigator.onLine
to match the more correct useSyncExternalStore snapshot behavior.

All 1552 tests pass, type-check clean, lint reports 0 errors and 1
acceptable warning (react-hook-form's `watch()` is incompatible with
React Compiler memoization and requires a structural change to fix).

Refs #103

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…-core

- apps/web: replace removed-in-next-16 `next lint` script with `eslint .`
  + flat eslint.config.mjs extending eslint-config-next; this surfaces
  the lint errors that the prior commit fixed
- apps/mobile: add expo-modules-core as direct dep so workspace
  hoisting resolves the NotificationPermissionsStatus extends
  PermissionResponse type chain (was nested under node_modules/expo/,
  breaking type-check)
- apps/mobile: commit the auto-generated eslint.config.js produced by
  expo-cli so the lint command is reproducible

Refs #103
Adds the new v8 primitives (AppBar, HabitRow, StatusDot, ParentRing,
Chip/TagChip, SelectCheck, SectionLabel, BottomTabBar, WebNav,
PullQuote, ConfirmDialog-v2, RingMotif). Old components untouched;
screens migrate in Phase 3.

Refs #103
Adds the new v8 primitives (AppBar, HabitRow with Animated.View
stabilizer, StatusDot, ParentRing, Chip/TagChip, SelectCheck,
SectionLabel, BottomTabBar, PullQuote, ConfirmDialog-v2, RingMotif).
Old components untouched; screens migrate in Phase 3.

Refs #103

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Migrates the four high-usage web screens to consume the Phase 2
primitives (AppBar, HabitRow, PullQuote, WebNav, etc.). Old
habit-card / bottom-nav / nav-card components are no longer
rendered but stay in tree until Phase 7 cleanup.

Refs #103

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Migration to eslint-config-expo/flat surfaced 363 React 19 / react-hooks v7
errors and 59 warnings across apps/mobile. This commit applies the standard
fix patterns without changing runtime behavior:

- useMemo for Animated.Value initialization (replaces useRef-then-.current
  pattern in ~23 files; resolves react-hooks/refs).
- Lazy useState for arrays of Animated.Value plus Math.random (sidesteps
  react-hooks/purity for ember/confetti seed pools).
- "Adjusting state when a prop changes" pattern in render (replaces
  setState-in-effect for prop-mirror cases).
- HabitList publishes allCollapsed / allLoadedIds via callbacks so the
  Today screen can read them in render-time state rather than via
  ref.current (mirrors the same web fix from bdf4a30).
- Inner walker functions for recursive useCallback bodies (fixes
  use-before-declare in getSubtreeMaxDepth and renderAllViewChildren).
- Hoisted Spinner/GoogleIcon out of LoginScreen to avoid
  create-components-in-render.
- Reanimated 4 / Android @gorhom/bottom-sheet stabilizer wrappers
  preserved. Reanimated sharedValue.value assignments get focused
  eslint-disable comments because the immutability rule mis-classifies
  them.

lint: 0 errors, 0 warnings (was 363 errors, 59 warnings).
type-check: clean.
vitest: 360/360 passing.

Refs #103
Migrates Streak, Achievements, Retrospective, Upgrade, Preferences,
Advanced, AI Settings, Calendar Sync, About, Privacy, Support to v8
chrome. Astra rename applied to AI Settings ("What Astra knows").

Refs #103

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Migrates the four high-usage mobile screens to consume the Phase 2
primitives (AppBar, HabitRow with Animated.View stabilizer,
BottomTabBar, PullQuote, etc.). Old habit-card / bottom-nav stay in
tree until Phase 7 cleanup.

Refs #103

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Rewrites habit/goal detail drawers, create/edit modals, notifications
inbox, referral drawer, feature guide, and controls menu to v8 chrome
while preserving all form validation, optimistic updates, and query
keys.

Refs #103

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Migrates habit/goal detail drawers, create/edit sheets, notifications
inbox, referral drawer, feature guide, and 3-dot menu to v8 chrome
while preserving all form validation, optimistic updates, and
@gorhom/bottom-sheet integrations.

Refs #103

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Migrates login, 7-step onboarding (now including "Meet Astra"),
fresh start, delete account, trial expired, push prompt, version
update drawer, API key modal, toasts (4 kinds), Saturn-ring
celebrations (7 types), and edge banners to v8 chrome.

The Astra interstitial is a web-only insertion to keep the shared
onboarding step constants stable; mobile parity for step 2 follows
in a separate phase.

Refs #103

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Migrates Streak, Achievements, Retrospective, Upgrade, Preferences,
Advanced, AI Settings, Calendar Sync, About, Privacy, Support to v8
chrome.

Refs #103

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Migrates login, 7-step onboarding (including Meet Astra), fresh
start, delete account, trial expired, push prompt, version update,
API key modal, toasts (4 kinds), Saturn-ring celebrations (7 types),
and edge banners to v8 chrome.

Refs #103

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Removes orphaned components that the new HabitRow / BottomTabBar / WebNav
primitives replaced, and the matching CSS rules from globals.css.

- Delete orphaned web components: habits/habit-card.tsx (+ test),
  habits/bulk-action-bar.tsx (replaced by -v2), navigation/bottom-nav.tsx
  (+ test). Move HabitCardActions interface into habit-row.tsx (renamed
  HabitRowActions, mirrors mobile shape).
- Delete orphaned mobile components: habit-card.tsx (+ test),
  navigation/bottom-nav.tsx.
- Drop ~250 lines of obsolete globals.css under HABIT RITUAL STRIPS and
  the matching light-theme overrides (.habit-card-parent/.habit-card-child
  variants, .habit-emoji-orb, .log-btn / .log-btn-done, .progress-ring-glow).
  .habit-actions-menu kept — still used by controls-menu + popover.
- Update AGENTS.md Key Files table to point at v2 primitives, web-nav
  (web) / bottom-tab-bar (mobile), and the OKLCH design tokens.

PRD §15 grep gates pass:
- EditionMasthead / SectionEyebrow / HabitCard / primaryTintBg /
  shadowGlow / Manrope → zero hits across apps/ + packages/
- Claude in user-facing strings → only legitimate third-party product
  names (Claude Web / Desktop / Code) in MCP integration copy
- npm ls manrope + @expo-google-fonts/manrope → empty
- No new // TODO / // FIXME / // HACK introduced by the migration
- No new console.log / `as any` introduced

Validation: web type-check + lint clean (1 pre-existing warning),
mobile type-check + lint 0/0, vitest 1481/1481 (web, 151 files)
and 350/350 (mobile, 80 files). Net change after deletions: -56 tests
(habit-card.test.tsx × 2 + bottom-nav.test.tsx) — all asserted against
components that no longer exist.

Refs #103
@vercel

vercel Bot commented May 22, 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 May 26, 2026 5:48pm

Comment thread apps/mobile/components/navigation/bottom-tab-bar.tsx
Comment thread apps/mobile/components/navigation/bottom-tab-bar.tsx Outdated
Comment thread apps/web/components/navigation/bottom-tab-bar.tsx
Comment thread apps/mobile/components/ui/app-bar.tsx Outdated

@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.

One hard-rule violation: the new BottomTabBar components (both apps/web and apps/mobile) hardcode user-facing navigation labels ('Home', 'Calendar', 'You', 'Create') and the new AppBar components hardcode the default back-button accessibility label ('Back'). The components they replace (bottom-nav.tsx on both platforms) used t('nav.*') / useTranslations() for all of those strings — so this is a localization regression, not a pre-existing pattern. AGENTS.md is explicit: "All user-facing strings through i18n. Never hardcode display text." The rest of the redesign — OKLCH token pipeline, Geist font wiring, the 7-phase parity matrix, ESLint 9 migration, 1481/1481 tests — is clean and well-structured. No console.log, no as any, no security issues, full web/mobile parity everywhere else. Fix the i18n gap and this is good to merge.

Addresses Claude's review on PR #104. The replacement bottom-nav
components hardcoded 'Home' / 'Calendar' / 'You' / 'Create' and the
new AppBar defaulted its accessibility label to 'Back', regressing
localization on pt-BR.

- nav.home / nav.astra / nav.you / nav.create added to both locales
- BottomTabBar (web): useTranslations('nav') resolves labels at render
- BottomTabBar (mobile): useTranslation() resolves nav.* keys
- AppBar (web): 'use client' + useTranslations('common') with backLabel
  defaulting to t('back'); existing common.back already 'Voltar' in pt-BR
- AppBar (mobile): useTranslation() with backLabel defaulting to
  t('common.back')

Refs #103, #104
The 30-turn limit on the anthropics/claude-code-action was tripping on
large PRs (e.g. PR #104, 208 files), exiting with "Reached maximum
number of turns" after the review had already been posted. The action
defaults to no hard cap when --max-turns is omitted; rely on that.

Refs #104
thomasluizon added a commit that referenced this pull request May 22, 2026
The 30-turn limit on the anthropics/claude-code-action was tripping on
large PRs (e.g. PR #104, 208 files), exiting with "Reached maximum
number of turns" after the review had already been posted. The action
defaults to no hard cap when --max-turns is omitted; rely on that.

Refs #104

@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.

The i18n regression flagged in the previous review is fully resolved. Both BottomTabBar implementations now call useTranslations/useTranslation and resolve all tab labels and the FAB accessibility label through nav.* keys; both AppBar implementations default backLabel to t('common.back'). The rest of the PR — OKLCH token pipeline, Geist font wiring, full 7-phase parity matrix across web and mobile, ESLint 9 / flat config migration, 1481 web + 350 mobile tests passing, no as any, no console.log — is clean and well-structured. Good to merge.

- Web globals.css @theme aliases V1 (--color-background, bg-surface-*,
  text-text-*, border-*) to V8 OKLCH (--bg, --bg-elev, --fg-1/2/3,
  --hairline). Bootstrap script no longer overrides via setProperty.
  Mobile theme.ts createColors() now resolves the V1 names through
  createTokensV2 so apps/mobile picks up V8 colors with no callsite edit.
- TodayHeader leading icon: Sun -> SaturnDropcap (web + mobile).
- ConfirmDialog rebuilt to the v8 spec: text-only Cancel/Action with
  italic destructive action, no semantic-color icon container or filled
  pill. Old V1 amber/red/green pill is gone.
- ThemeToggle, StreakBadge, NotificationBell (web + mobile) rebuilt:
  flat 36x36 icon button, primary 6x6 unread dot, hairline streak pill
  with --status-bad outline flame and --primary mono count. Orange
  gradient + streak-badge.css deleted.
- useDeviceLocale (web + mobile) deleted; 23 callsites read locale
  directly via next-intl useLocale() (web) or react-i18next
  i18n.language (mobile). Date strings now render in the app locale,
  not OS regional (Fri, May 22 -> sex, 22 mai in pt-BR).
- 6 .toLocaleString() / .toLocaleDateString() calls now pass the app
  locale (achievements web + mobile; calendar-sync mobile).
- Hardcoded Tailwind palette (text-red-400, bg-amber-500/10,
  text-green-500/10, etc.) replaced with v8 status tokens
  (text-[var(--status-bad/overdue/done)]) across 21 web files;
  3 test-pinned files migrated together (conflict-warning, action-chips,
  push-notification-settings) with data-attribute assertions.
- transition-all banned per DESIGN.md; ~20 callsites replaced with
  precise property lists (transition-colors, transition-transform,
  transition-[bg,border,transform,opacity]).
- Web AppLayout: bottom nav stays bottom-fixed at every width
  (removed md:sidebar branch), bar bg/hairline live on the full-width
  wrapper so the bar reads as a system bezel; tabs/FAB stay
  --app-max-w-centered.
- Zustand UI store skipHydration:true + Providers triggers rehydrate()
  in a mount effect, eliminating the persist-vs-default SSR mismatch.
- Habit detail "Ask Astra" pull-quote is now a button (chevron, hover,
  active scale) that prefills the chat draft and routes to /chat
  (web + mobile).
- Mobile lint: 15 react-hooks/refs and react-hooks/set-state-in-effect
  errors fixed in use-gamification (effect+state instead of render-time
  ref I/O), use-drill-navigation + use-push-notifications (microtask
  defer for callbacks that synchronously setState), use-speech-to-text
  (timer id moved to state, redundant isSupported effect removed),
  theme-provider (Animated.Value via useMemo not useRef.current; profile
  sync via "Adjusting state when a prop changes" pattern).
- Missing i18n keys added to en.json + pt-BR.json:
  profile.freshStart.{heading,confirmHeading,willDelete,willKeep} and
  profile.deleteAccount.{headingAreYouSure,headingConfirmCode,
  headingDeactivated}. Resolves MISSING_MESSAGE runtime errors.
- DESIGN.md (new, repo root): Linear-tactical anchor, OKLCH token
  table, primitives list, bans. AGENTS.md frontend rules replaced with
  a pointer. CLAUDE.md points to DESIGN.md + impeccable working model.
- .gitignore: ignore "temporary screenshots/" from chrome-devtools work.

Checks: web tsc + lint + 1476/1476 tests; mobile tsc + lint +
350/350 tests; shared 810/810 tests.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

@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.

The v8 token migration commit is clean. V1 CSS custom properties now alias to V8 OKLCH vars in globals.css, and createColors() on mobile threads through createTokensV2 — both platforms pick up the new palette with no per-file callsite edits. useDeviceLocale is deleted and all 23 callsites are migrated to next-intl useLocale() (web) or i18n.language (mobile), fixing date/time strings to respect the app locale rather than the OS regional setting. The rebuilt ConfirmDialog, ThemeToggle, StreakBadge, and NotificationBell all land with correct web/mobile parity. The Ask Astra pull-quote→button upgrade writes to localStorage/AsyncStorage before routing, consistent with how use-chat-composer reads the key. Missing i18n keys (freshStart.heading/confirmHeading/willDelete/willKeep, deleteAccount.headingAreYouSure/headingConfirmCode/headingDeactivated) are added to both locales. React 19 lint fixes in use-gamification, use-drill-navigation, use-push-notifications, use-speech-to-text, and theme-provider are all correct pattern migrations. No console.log, no as any, no transition-all, no hardcoded display strings. Good to merge.

Brings the harness overhaul (#106) into redesign/v2: scoped CLAUDE.md split, 6 PostToolUse hooks, 5 new skills, 4 new subagents, C# LSP wiring, multi-issue workflow.

Conflict resolutions:
- AGENTS.md: deleted (main wins; replaced by scoped CLAUDE.md files).
- CLAUDE.md: took main's lean cross-cutting version with 10-rule Code Standards.
- apps/web/eslint.config.mjs: combined main's three-config spread (next + core-web-vitals + typescript) with redesign/v2's broader ignores (e2e/, coverage/, public/, *.config.*).
- apps/web/package.json: added eslint v9 declared devDep from main.
- package-lock.json: regenerated from main with --legacy-peer-deps (pre-existing react-native-worklets peer mismatch in apps/mobile).

Validation:
- npm run type-check: PASS (3/3 workspaces).
- npm test: PASS (1476 web tests).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Comment thread apps/web/app/(app)/page.tsx Outdated

@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.

The phantom AppBar button fix in d0f016a is correct: {back || onBack ? <button> : <span aria-hidden>} properly gates interactivity so screens without back navigation no longer expose a focusable no-op button. That finding is resolved.

One issue from the prior review remains open: title="Orbit" is hardcoded in both apps/mobile/app/(tabs)/today-shell.tsx:53 and apps/web/app/(app)/page.tsx:485. Both scoped CLAUDE.md files are explicit — "All user-facing strings through i18n. Never hardcode display text." The fix landed in d0f016a and was then reverted by 1c0d259/790e3d9. The revert resolves the parity gap (both platforms consistently show the same title) but not the i18n rule. Add "appName": "Orbit" under the common block in both locale files and replace both literals with t('common.appName') — a four-line change across four files.

Mirrors the existing nav.astra precedent for brand names: literal "Orbit"
resolves through i18n on both platforms but stays "Orbit" in both locales.
No user-visible change; satisfies the i18n rule without view-aware wiring.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

@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.

The final commit routes the hardcoded "Orbit" title through common.appName in both apps/mobile/app/(tabs)/today-shell.tsx and apps/web/app/(app)/page.tsx, with the key added to both en.json and pt-BR.json. The other post-last-review commits are equally clean: the mobile kebab menu uses correct i18n keys (verified), the bgElevPressed token is properly defined for light and dark, the calendar lazy-load refactor keeps both platforms in parity (grid renders immediately, only status dots are suppressed while loading), and the dead CalendarLoadingSkeleton was properly excised. No any, no console.log, no hardcoded strings, no unreachable branches, no dead exports left behind.

Comment thread apps/mobile/app/streak.tsx
Comment thread apps/mobile/app/(tabs)/today-shell.tsx Outdated

@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.

Two new hard-rule violations in the commits since the last approval, plus the outstanding _isLastChild finding from the previous two reviews.

New finding 1 — hardcoded user-facing strings (apps/mobile/app/streak.tsx:135–138): The tier labels 'Legendary', 'Strong', 'Steady', 'Normal' are rendered directly as label={tier} in a SettingsGroupRow. The streak ≥ 365 branch immediately above already uses t('streakDisplay.profile.encouragement365'), so this is an inconsistency within the same useMemo. Fix: four new i18n keys in both locales + replace the literals (suggestion posted inline).

New finding 2 — dead export + banned backwards-compat shim (apps/mobile/app/(tabs)/today-shell.tsx:249–252): export type TodayShellStyles = Record<string, never> has zero callers anywhere in the repo. CLAUDE.md rule 2 bans "just in case" exports; the root CLAUDE.md also explicitly bans re-exporting stale types as a backwards-compat shim. The comment above it references the current migration task (CLAUDE.md rule 5, banned). Fix: delete the four lines (suggestion posted inline).

Outstanding from prior reviews — _isLastChild in apps/mobile/components/habits/habit-row.tsx:93: The _ prefix rename is the backwards-compatibility hack CLAUDE.md explicitly prohibits ("Avoid backwards-compatibility hacks like renaming unused _vars"). The correct fix is to delete the parameter from the interface (line 69), the destructuring (line 93), the isLastChild computation in habit-list.tsx (line 580), and the two pass-through call sites (lines 1281, 1443).

… dead TodayShellStyles

- HabitRow no longer accepts isLastChild on either platform; deleted the
  prop from the interface, destructuring, and every call site / build step
  across web + mobile habit-list. The _isLastChild rename was a banned
  backwards-compat hack per root CLAUDE.md.
- Mobile streak tier ("Legendary" / "Strong" / "Steady" / "Normal") now
  resolves through streakDisplay.detail.tier{Legendary,Strong,Steady,Normal}
  i18n keys (added in both locales). Web's tier is a CSS-class identifier,
  not display text, so no parity change there.
- Deleted dead TodayShellStyles export and its compat-shim JSDoc from
  apps/mobile/app/(tabs)/today-shell.tsx — zero callers anywhere.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

@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.

All three findings from the previous review are cleanly resolved in commit 066ae05. The isLastChild dead parameter is fully deleted — not just renamed — across both platforms: removed from HabitRowProps, the DragItem interface, the computation loop, and every call site in both habit-list.tsx files. The four streak tier labels now flow through t('streakDisplay.detail.tier*') with matching keys in both en.json and pt-BR.json. The dead TodayShellStyles export and its stale compatibility comment are gone. No new issues introduced. Good to merge.

thomasluizon and others added 2 commits May 25, 2026 22:53
…isible

Wrap each category in SettingsGroup, mirroring profile/streak. Move earned
date from cramped trailing slot to spacious subtitle so it never truncates.
Locked rows show description (italic) + small trailing lock icon. Drop
hairline under level/XP header to match the profile user-block treatment.
Delete dead AchievementCard components and tests on both platforms.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Earned rows now show the achievement description, same as locked. Drops
the "Earned {date}" line so the description (the most evocative info)
is always visible. Removes the orphaned earnedOn i18n key from both
locales.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

@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.

Commits 4b70785 and 77a975f refactor achievements to card-grouped rows. Both web and mobile rows are structurally identical (glyph, name, description always shown, Lock icon for locked). Old AchievementCard files deleted with zero remaining refs. The earnedOn i18n key removed from both locales with no callers. No new workarounds, no hardcoded strings, no any or console.log. Good to merge.

Comment thread apps/web/components/habits/today-filters.tsx Outdated
Comment thread apps/web/components/gamification/profile-streak-card.tsx Outdated

@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.

Two orphaned component files left by this PR:

  1. apps/web/components/habits/today-filters.tsx — its only caller (apps/web/app/(app)/page.tsx) was removed by this PR; the file was updated in the token-migration sweep but never deleted. Delete the file and its test.

  2. apps/web/components/gamification/profile-streak-card.tsx — same pattern: removed from its only caller (apps/web/app/(app)/profile/page.tsx) by the profile redesign, then touched in the token-migration sweep. Delete the file.

Everything else is clean: the i18n regressions from the first round (BottomTabBar, AppBar) are fully resolved, streak tier labels are i18n-routed, isLastChild is completely removed from both platforms, no console.log, no as any, no hardcoded user-facing strings, full web/mobile parity on all redesigned surfaces.

Both components became orphans during the redesign: profile page now uses
an inline streak row via SettingsGroupRow, and the today shell inlines its
own search/filter bar. Drop the components, their tests, and the leftover
profile-page test mock.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

@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.

The two orphaned components flagged in the prior round — profile-streak-card.tsx and today-filters.tsx — are fully deleted along with their tests and the stale mock in profile-page.test.tsx. No dangling references remain (the today-filters-shell hits are mobile testID strings, unrelated to the deleted web component). The deletion commit is pure cleanup with zero additions. All earlier concerns are resolved; the v8 redesign lands clean.

Re-add Select (CheckCircle2) and Open sub-habits (ChevronRight), reorder
to match the old design (add sub-habit → move to → skip → edit → duplicate
→ select → delete → open sub-habits), color Skip amber via statusOverdue,
color Delete red via statusBad, and insert a hairline divider before Delete.
Open sub-habits is gated on hasChildren; Select hides while already in
select mode.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

@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.

The single post-approval commit restores the full habit kebab menu on both web and mobile. The implementation is clean: canSelect/canDrillInto are correctly derived from the existing selectMode/isSelectMode prop and hasChildren, both new i18n keys (common.select, habits.actions.openSubHabits) land in en.json and pt-BR.json, the legacy FolderInput/SkipForward icons are fully removed, and the destructive boolean on web's MenuItem is cleanly replaced by the tone enum. Cross-platform parity is maintained, no any, no dead code.

Android nested-Pressable quirk: tapping the kebab fires both the inner
Pressable's onPress (open menu) and the outer row Pressable's onPress
(open detail), so the user landed on the detail screen with the menu
stacked on top. Stamp a timestamp ref on every menu interaction
(onPressIn + openMenu + closeMenu) and have the row's handlePress
swallow presses within 500ms of the last interaction. Web mirror: add
event.stopPropagation() to menu item onClick as a defensive guard.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

@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.

The fix commit (4ba068b) since the last approval is clean. The Android nested-Pressable bug — where tapping the kebab fired both the inner Pressable's onPress (open menu) and the outer row's onPress (navigate to detail) — is correctly addressed with a timestamp ref (menuActivityAt) stamped on onPressIn, openMenu, and closeMenu, guarding handlePress against presses within 500ms of any menu interaction. This is the right approach since React Native's Pressable has no stopPropagation. The WHY comment in the code is warranted given the non-obvious platform quirk. Web's event.stopPropagation() on MenuItem is a clean defensive guard. No as any, no console.log, no dead code, full web/mobile parity. All prior findings from this review cycle (i18n regressions, dead exports, isLastChild dead parameter, unreachable calendar guard, hardcoded strings, orphaned components) remain resolved.

Add optional onClear prop to AppTimePicker on both platforms. When a value
is set and onClear is provided, an X button replaces the clock icon
(mobile: side-by-side Pressables; web: absolute-positioned button over the
native input). Wire it into the habit form: clearing dueTime cascades to
dueEndTime, clearing dueEndTime is isolated.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

@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.

The single post-approval commit (a09c0ed) cleanly adds opt-in clear behaviour to AppTimePicker on both platforms. common.clear is present in both locale files, both habit-form-fields.tsx files are updated with matching onClear callbacks, the cascade logic (clearing dueTime also resets dueEndTime) is correct, and mobile tests cover the new clear button. No console.log, no as any in production code, full web/mobile parity. Good to merge.

@sonarqubecloud

Copy link
Copy Markdown

Quality Gate Failed Quality Gate failed

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

See analysis details on SonarQube Cloud

@thomasluizon
thomasluizon merged commit 73dc102 into main May 26, 2026
7 of 8 checks passed
@thomasluizon
thomasluizon deleted the redesign/v2 branch May 26, 2026 17:57
thomasluizon added a commit that referenced this pull request May 26, 2026
…113)

* chore(shared): i18n parity guard + remove orphan locale keys (#109)

Add a key-set + placeholder parity guard test to i18n.test.ts so en.json and pt-BR.json can never silently drift. Remove 215 orphan keys per locale (46 verified clusters + 169 tail), each confirmed zero-reference across the git-tracked source+test tree; common/nav/meta next-intl-scoped keys kept conservatively to avoid runtime MISSING_MESSAGE.

Refs #109 (Sweep 1)

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

* chore(web): remove dead legacy --color-* CSS tokens + fix prose vars (#109)

Delete unused @theme --color-* aliases + the Nuxt --color-primary-NNN scale from globals.css; migrate the remaining consumers (skeleton-shimmer, scrollbars, orbit-ring, loading-bar, focus-visible, body) to the v8 canon (--bg/--bg-elev/--fg-1/--primary). Fix .prose-orbit's undefined --text-primary/--surface-elevated/--border/--text-muted refs and replace the DESIGN-banned colored blockquote left-bar with a neutral hairline. Remove the now-dead setProperty writes (plus a corrupted --color-text-[var(--primary)] property name) from theme-dom.ts and the 11 dead Nuxt-scale writes from layout.tsx.

--primary is defined per scheme+mode in static CSS so the migration is value-preserving; next build + type-check + lint pass.

Refs #109 (Sweep 3)

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

* chore(mobile): trim dead legacy theme bag (#109)

Remove the orphaned pre-v8 compat layer from theme.ts: AppNav/createNav/nav (zero consumers), ~57 unused AppColors fields (kept the 7 createSurfaces needs), AppSurfaces ground/card/glow + the glow shadow stubs, plus now-orphaned helpers (blendRgbOverHex/flattenedTint). Drop colors/nav from the useTheme() context — no component reads them (all 27 hook consumers use currentScheme/currentTheme). Update use-app-theme.ts (a 4th createColors caller the audit missed) + theme.test.ts in lockstep.

type-check + lint + 351 tests green.

Refs #109 (Sweep 3)

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

* chore: remove orphan web ConfirmDialogV2 + stale mobile tab-bar comments (#109)

Delete apps/web/components/ui/confirm-dialog-v2.tsx (zero importers; mobile's variant stays — it is used by streak/advanced). Replace the bottom-tab-bar doc block carrying a banned 'Phase 3 wires it' task reference and a factually-wrong 'Astra uses Sparkles' note (it renders MessageCircle) with a clean one-line doc.

Refs #109 (Sweep 3)

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

* fix(profile): render freshStart in primary tone + drop dead icon prop (#109)

ProfileActionButton's primary tone was a no-op on both platforms (web fell through to --fg-1; mobile had an identical-branch ternary), so freshStart never rendered as a primary action. Map primary -> var(--primary) (web) / tokens.primary (mobile). Remove the dead icon prop from the web interface + its three icon={null} call sites (mobile never had it). danger (logout/delete) unchanged.

Refs #109 (Sweep 2)

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

* feat(mobile): view-aware Today header subtitle for web parity (#109)

Mobile's Today subtitle was date-only while web's reflects the active view. Mirror web's logic (all->habits.viewAll, general->habits.viewGeneral, goals->goals.tab, else the formatted date) driven by the existing currentActiveView. Title stays t('common.appName') ('Orbit') and is NOT view-aware, per the saved product decision.

Refs #109 (Sweep 2)

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

* chore(mobile): remove unused deps from Today controls useMemo (#109)

The controls-row useMemo listed activeView (distinct from the used currentActiveView) and setSelectedFrequency (managed via handlers), both unreferenced in the memo body. Removing them clears the last react-hooks/exhaustive-deps warning; lint re-run confirms no missing-dep warning surfaced.

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

* refactor(mobile): extract habit-form-fields inline subcomponents (#107)

Move the 9 self-contained inline subcomponents (ChoiceButtonRow, HabitEmojiSelector, TagColorPicker, TagEditorRow, BufferedSheetInput, HabitTagChip, ReminderSection, ScheduledReminderSection, SlipAlertSection) + the createStyles/createSectionStyles factories out of the 2448-line monolith into habit-form-fields/ subfiles. Entry module stays the public import and drops to 839 lines. Pure structural move: HabitFormFields signature unchanged, all 3 consumers' imports unchanged. Added an assembly smoke test (6->7).

Refs #107 (habit-form-fields step 1/2; render-body per-section split follows to bring the export under the size cap)

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

* refactor(mobile): split habit-form-fields render body into sections (#107)

Extract the ~729-line HabitFormFields render body into 8 per-section components (title, frequency-type-cards, frequency-detail, active-days, due-date, tags, more-options-toggle, advanced) + a shared types.ts, composing the previously-extracted leaves. Entry file 839->238 lines; the HabitFormFields assembler is now ~192 lines of flat section composition + form/flush coordination -- a deliberate stop (further splitting would force artificial seams; rule 6 favors a flat assembler over a bad abstraction). Public signature + all 3 consumers unchanged; full mobile suite (352) green.

Refs #107 (habit-form-fields step 2/2)

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

* refactor(mobile): extract create-habit-modal sub-habit editor + add test (#107)

Add the previously-missing test suite (4 cases: closed, open title, sub-habit mode, non-pro ProBadge) as the safety net for the zero-coverage file. Extract the sub-habit editor block to create-habit-modal/sub-habit-editor.tsx (101 lines). Kept the form/dirty-snapshot/dismiss/submit orchestration in the main component: reminderTimes is load-bearing across the open-reset, dirty comparison, and submit paths, so a reminder hook would be a pass-through that fractures the dirty-tracking invariant (rule 6 > forcing rule 7). File 592->538; full mobile suite 356 green.

Refs #107 (create-habit-modal)

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

* refactor(mobile): split goal-detail-drawer into panel components (#107)

Extend the thin test (1->4: progress block, progress-form open, action footer) as the net, then extract goal-progress-block/goal-progress-form/goal-action-footer + styles into goal-detail-drawer/. goal-linked-habits left inline (a 6-line wrapper over the already-extracted GoalLinkedHabitsSection -- pass-through). File 877->530; the mutation + dirty-guard orchestration (11 callbacks) stays in main by design. Full mobile suite 359 green.

Refs #107 (goal-detail-drawer)

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

* refactor(web): extract login page into steps + useLoginFlow hook (#107)

Move the LoginPage handler/state bundle into a useLoginFlow hook and EmailStep/CodeStep/atoms/pure-helpers into sibling files. page.tsx 860->234; LoginPage is now a thin composition over the hook. login-atoms is shared by both step files (genuine seam, not over-split). Added an error-path test (resolveLoginErrorState) the suite was missing (9->10). Full web suite 1445 green; testids + @/app/(auth)/login/page import path preserved.

Refs #107 (login)

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

* refactor(web): extract today midnight-sync hook + co-locate tab keydown (#107)

The #104 v2 migration already extracted the header/tab-strip/date-nav/utility-row into today-shell.tsx, so page.tsx was already 660 lines (not the plan's stale 726). Extract the remaining inline midnight-advance effect into useFollowTodaySync; fold handleTabKeydown into TodayTabs (co-located with the tab strip it serves) + export getTodayTabLabel. page.tsx 660->646; TodayPage's residual ~592 lines is bulk-action/confirm/selection orchestration the plan mandates keeping on the page (not over-split). testids + goals free/pro routing preserved; web suite 1445 green.

Refs #107 (today page)

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

* refactor: split habit-list empty-state/date-group/confirm-dialogs (web+mobile) (#107)

Converge both platforms onto habit-list/{empty-state,date-group-section,confirm-dialogs}.tsx: split the prior combined habit-list-sections.tsx into the two named section files and extract the confirm-dialog cluster (delete/skip/duplicate/force-log/auto-log) behind a shared HabitListConfirmDialogs prop shape. Same decomposition shape on both; no implementation shared across the web/mobile boundary (RN vs DOM primitives differ). renderHabitCard, drag wrappers, the useImperativeHandle HabitListHandle API, and all state stay in main. Mains: web 1328->1230, mobile 2188->2116; tests 19/20 green; type-check + lint clean both.

Refs #107 (habit-list pair, run 1/2)

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

* refactor: split habit-list drill + move-parent (web+mobile) + web sortable-item (#107)

Final habit-list seams: extract drill (web drill-content.tsx / mobile drill-panel.tsx) and move-parent (web move-parent-overlay.tsx / mobile move-parent-dialog.tsx) on both platforms, plus the web-only SortableHabitItem (@dnd-kit wrapper) to sortable-habit-item.tsx. All presentational: renderHabitCard (passed as a prop), drag orchestration (DndContext / DraggableFlatList+HabitRowTourAnchor), the useImperativeHandle HabitListHandle API, and all state stay in main. Mains web->1243, mobile->2066 (kept as the accepted stateful list orchestrators). Tests 19/20 green; type-check + lint clean both.

Refs #107 (habit-list pair, run 2/2)

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

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
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.

Migrate Orbit (web + mobile) to v2 Linear-tactical design

1 participant