Repository navigation
feat: migrate Orbit (web + mobile) to v2 Linear-tactical design - #104
Conversation
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
|
The latest updates on your projects. Learn more about Vercel for GitHub. 1 Skipped Deployment
|
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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>
There was a problem hiding this comment.
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>
There was a problem hiding this comment.
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>
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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>
There was a problem hiding this comment.
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.
…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>
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
Two orphaned component files left by this PR:
-
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. -
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>
There was a problem hiding this comment.
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>
There was a problem hiding this comment.
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>
There was a problem hiding this comment.
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>
There was a problem hiding this comment.
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.
|
…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>


Closes #103.
Summary
Migrates both apps/web (Next.js 15) and apps/mobile (Expo SDK 53) to the v8 Linear-tactical design language:
/chat)RingMotifprimitive (7 celebration types)Backend (orbit-api) untouched — no API contract changes, no Zod schema changes, no query key changes.
Phases (committed)
Validation
use-habit-form.ts), vitest 1481/1481 across 151 filesEditionMasthead/SectionEyebrow/HabitCard/primaryTintBg/shadowGlow/Manrope→ zero hits in apps/ + packages/Claudein 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// TODO/// FIXME/// HACKintroduced by the migrationconsole.log/as anyNet change in Phase 7 alone: -4977 / +25 lines.
Known follow-ups (not blocking)
EditionMasthead/Manrope/habit-card-parent/habit-card-child/fresh-start-orb/habit-emoji-orbetc. — zero matches. No baseline screenshots exist (screenshot: 'only-on-failure'and notoMatchSnapshot/toHaveScreenshotcalls), so nothing to delete or regenerate. Tests should be run locally before merge to confirmdata-testidselectors still resolve against the migrated DOM. Seeapps/web/playwright.config.tsfor runner setup.ConfirmDialogv1/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 mobilestreak.tsx+advanced.tsxonly. 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
npm run test:e2elocally before merge — confirm no broken selectorsCo-Authored-By: Claude Opus 4.7 (1M context) noreply@anthropic.com