Visual polish: Linear-tactical craft lift across web + mobile (#117) - #154
Conversation
App-wide decoration purge + logo swap + primitive reconciliation + Android widget token migration, inside the locked Linear-tactical anchor (DESIGN.md). Frontend only; net -444 lines. W1 (ban purge, both platforms): pro-badge/skeleton gradient shimmer to opacity pulse; fresh-start glow to hairline pulse; Goals card stripe/sheen/emoji; mobile completed-glow + hardcoded rgba badge tints; chat amber/blue rgba to token tints (fixes a red-on-red HIGH-severity bug); habit-summary glass + LinearGradient; em dashes (both locales); paywall shadow-overuse + raw-px radius; mobile delete red-fill to hairline + status-bad text; habit-calendar done to status-done; create-habit upsell de-carded. W2 (logo): AppLogo (web next/image, mobile RN Image) on logo-no-bg.png replaces all 6 SaturnDropcap sites; both glyph components deleted. W3 (primitives): StatusDot state required on both; MonoToggle rebuilt to the canonical monospaced ON/OFF pill (primary fill) on both; created missing mirrors (mobile skeleton/empty-state/settings-description, web info-row). W4 (partial): Today/habit-list empty-state reconciled to web; goal-metrics + tag-chip rgba to tokens; web onboarding-welcome subtitle parity; calendar DayStatus enum unified (none/upcoming to empty/missed). W5 (Android widget): hardcoded Kotlin THEMES palette to v8 tokens via the syncTheme bridge; gradients + 9 dead drawables removed; flat surfaces + hairline borders; no semantic red. Validated: web lint+type+1464 tests, mobile lint+type+411 tests, i18n 1607 keys in sync. Per-surface /animate + /polish (motion, incl. the remaining celebration glows) deferred to a follow-up pass per #117. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…ent/drag glow bans The celebration glow animations flagged in #117 (orbit-complete/creation/ perfect-day/ring-pulse + their .animate-* classes) were dead code with zero consumers, so they were deleted rather than redesigned. Mobile's unread creation/complete glow durations went with them. The live glow bans removed instead: - .ambient-glow primary radial wash on web (app)/(chat) shells and its mobile twin (the _layout ambient LinearGradient) -> flat near-black backgrounds - drag-reorder --shadow-glow halo -> neutral elevation shadow Removed the orphaned shadowGlow token chain (shared color-schemes x12 + type + theme.test, web theme-dom/layout setters, globals.css vars) and the dead --orbit-completion-peak/glow-scale + --orbit-route-tint-opacity vars. Refs #117. Web 1464 / mobile 411 / shared tests + type-check + lint all green. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Logging a habit now plays a signature completion motion in StatusDot: a --primary arc sweeps once around the dot, then the fill settles in (~500ms, ease-out-quart). Fires only on an interactive non-done -> done transition, never on mount of an already-done dot or on a read-only dot; web also respects prefers-reduced-motion. Web uses a CSS stroke-dashoffset keyframe; mobile mirrors it with react-native-svg + RN Animated (the useMemo'd Animated.Value pattern). Both use the render-phase adjust-state-on-prop-change pattern so the sweep starts in the same commit as the state flip (no fill flash). Adds createAnimatedComponent to the RN test mock (real RN has it) and a StatusDot web test covering the transition rules. Refs #117. Web 1464 / mobile 411 tests + type-check + lint green. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…weep Replace the two-phase completion motion (thin arc outline, then a separate disc blooming in to fill) with one continuous --primary wedge that fills the dot as it sweeps clockwise, landing on the solid done dot. The sweep now IS the fill, so there is no separate pop. ~420ms ease-out-quart. Drops the now-unused status-fill keyframe (web) and fillScale Animated value (mobile). Refs #117. Type-check + lint + StatusDot/habit-list tests green. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Mirror web's prefers-reduced-motion handling on mobile: read AccessibilityInfo.isReduceMotionEnabled (and subscribe to changes) and skip the StatusDot completion sweep when reduce-motion is enabled. Closes the accessibility gap flagged by the impeccable animate gate; both platforms now honor the setting. Refs #117. Mobile type-check + lint + habit-list render test green. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…ba gradients - Web StatusDot tap target 42px -> 44px (padding 10 -> 11), meeting the 44px minimum touch-target guideline. - Delete the dead `gradients` export from mobile lib/theme.ts (surfaceSheen, surfaceSheenChild, logButtonDone, statusDue, statusOverdue): zero consumers after the W1 sheen removal, and hardcoded rgba (a DESIGN ban). Drop the vestigial gradients key from the profile-screen test mock too. Refs #117. Type-check + lint + affected tests green on both platforms. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The drawer differentiator: a one-line mono summary strip (streak, linked goal, checklist X/Y) at the top of the habit detail drawer. Web gains it (it had none); mobile's hand-rolled strip moves onto its InfoRow primitive. Both derive the text from a new shared formatHabitDetailSummary helper, so the content is one source of truth and stays in parity. Refs #117. type-check + lint + tests green on web/mobile/shared (the lone web i18n-request failure under parallel load is a known timing flake; passes in isolation at 2.8s). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
HabitDetailStatsGrid (the icon Flame/Trophy/BarChart3 + "{n}d" stat grid)
had zero production consumers — the live habit drawer uses HabitDetailStatsRow.
Remove it along with its now-orphaned HabitDetailSectionStyles type and the
icon imports; repurpose its test to cover the live HabitDetailStatsRow.
Refs #117. Mobile type-check + lint + tests green.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The Goals differentiator: replace the generic linear progress bar with the app's orbital-ring vocabulary - a leading 36px hairline ParentRing fills with the goal's progress, and the percent readout moves to mono tabular numerals. ParentRing gains an optional color prop (second real consumer): completed/high keep status-done, streak keeps the amber flame identity, abandoned mutes to fg-3. A11y preserved: web keeps the sr-only progress element (and the data-progress-state hook moves to the ring wrapper); mobile keeps accessibilityRole progressbar + accessibilityValue, and the tour target now points at the ring. Also fixes a radius parity drift: mobile card 20px -> 12px to match web. Refs #117. Type-check + lint green; web 67 goal tests + mobile 412 tests pass. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The calendar legend lied on both platforms: it labeled the primary dot
"Upcoming" when the grid uses primary for TODAY, and the hollow partial ring
had no legend item at all. The legend now mirrors the grid vocabulary
exactly: today (primary), done (solid fg-1), partial (hollow ring), missed
(amber). calendar.legend.today/partial added, legend.upcoming deleted, and
the stale tour copy (green/yellow/red dots that do not exist) rewritten to
describe the real dots - both locales.
Parity fixes folded in: mobile gains web's month summary subtitle
("3/12 days (25%)" next to the month name), and the go-to-day button now
also renders on empty days on mobile (web's footer always shows it);
extracted GoToDayButton to avoid duplicating the pressable.
Refs #117. Static checks + all three suites green (web 150 / mobile 85 /
shared 66 test files, i18n key sync included).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Rewrite the mobile GoalMetricsPanel to mirror web exactly: SectionLabel + flush SettingsRow strip (tracking-status row with leading dot, projected completion, velocity/days-remaining) + hairline adherence rows (title, 3px bg-sunk track bar, mono percent). Kills the tinted status pill, the shadowed stat cards, the bgElev-on-bgElev bar track, and the redundant per-habit streak badge web never had; loading state now uses the SkeletonLine primitive (wires plan item 6 for this surface). The drawer-level "Status: Active" SettingsRow (web has no counterpart; it only rendered for active goals, so it was static) is gone along with its now-orphaned goals.detail.statusLabel key in both locales. Refs #117. Static checks green all 3 workspaces; mobile 85 + shared 66 test files pass (i18n key sync included). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…dialog Mobile's canonical ConfirmDialog still filled its confirm button with statusBad/statusOverdue - the exact semantic-fill ban W1 purged elsewhere, and a parity break with web (whose canonical is text actions, destructive = italic, "no semantic fill"). The mobile canonical now mirrors web's action treatment exactly: right-aligned text buttons, cancel fg-3/500, confirm fg-1/600, italic when destructive. Both canonicals gain a variant="info" mode (single close action, cancel hidden, onConfirm optional) which absorbs ConfirmDialogV2's info-only use: advanced.tsx's widget-info and revoke-key dialogs now use the canonical, and confirm-dialog-v2.tsx is deleted (zero refs; its unused eyebrow/children extras die with it). Refs #117. Type-check + lint green; web 150 + mobile 85 test files pass. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…yState The chat empty-state rings on both platforms were perfect circles spun with infinite linear rotation - invisible motion (a rotating circle looks identical), pure wasted GPU. Web drops the two spin animations (identical pixels); mobile inlines the same static composition (primary outer ring + hairline inner ring + static Sparkles), deleting AnimatedSparkle and its icon pulse loop. The misleading sparkleGlow style is renamed sparkleInnerRing (it was always a hairline ring, not a glow). Mobile also gains web's missing chat.empty.title and the prompt drops to web's 14px italic. Mobile goals empty state moves off its hand-rolled icon-disc block onto the EmptyState primitive (web's goals-view already uses its EmptyState), with the same card chrome web applies. Refs #117. Type-check + lint green; web 150 + mobile 85 test files pass. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…tions Wire the W3 SettingsDescription mirror into its consumers: ai-settings (aiMemory/aiSummary descriptions move off hand-rolled italic blocks) and calendar-sync (connectGoogleFirst hint). Close 4 content-parity gaps where web showed a description mobile lacked entirely: language, color scheme, week start day (preferences) and auto-sync (calendar-sync). Remaining italicText uses in calendar-sync/ai-settings are warnings and conditional states with status colors - different content class, left as-is. Refs #117. Mobile type-check + lint + 85 test files green. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Replace every Easing.out(Easing.cubic) with the spec ease-out-quart bezier (cubic-bezier(0.16,1,0.3,1) = motionEasings.enter) via a new toAnimatedEasing helper in lib/motion, giving lib/theme's until-now unused `easings` control points their consumers. The today-screen's local createAnimatedEasing (with its mock-guard fallback) folds into the helper; the RN test mock gains the missing Easing.bezier so no runtime guard is needed (same incompleteness as createAnimatedComponent earlier). Touches: status-dot sweep, confirm-dialog, anchored-menu, goal-card press, theme-provider transition, today screen date-label + timing configs. Refs #117. Mobile type-check + lint + 85 test files green. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Drop the scale(1.08) emphasis on the active color-scheme swatch in mobile onboarding; web marks the active swatch with a static ring only. The active fg-1 border already carries selection. Refs #117. Validated in the full final gate (lint + type-check + tests green across web/mobile/shared). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
|
The latest updates on your projects. Learn more about Vercel for GitHub. 1 Skipped Deployment
|
There was a problem hiding this comment.
One dead-code issue on the new mobile skeleton primitive: SkeletonCard and SkeletonAvatar are exported from the new apps/mobile/components/ui/skeleton.tsx with zero callers (only SkeletonLine is imported anywhere in mobile). The same-named SkeletonCard definitions in habit-list/empty-state.tsx and goals-view.tsx are local components with a different styles prop — they don't import from this module. Violates CLAUDE.md rule #2 ('no just-in-case exports'). Fix: drop the two dead exports from the primitive and add them back when they have real callers. Everything else in this PR — the ban purge, completion ring-sweep, parity closes, Kotlin widget token migration, logo swap, and primitive reconciliation — is clean.
… derived title The goal form has no title input - the title derives from the description or "quantity unit". validateGoalForm checked the derived title FIRST, so an empty form reported "title is required" for a field the user cannot see. Inputs are now validated before the derivation: missing quantity -> targetValueRequired (under Quantidade), missing unit -> unitRequired (under Unidade). A title remains optional everywhere; with quantity + unit set it always derives, so the title error is unreachable from the form flow. Updates the shared test that had encoded the old behavior and adds the reported repro cases (all-empty, and quantity-set + unit-empty). Refs #117 (rides the open goals PR). Shared 66 + mobile 85 test files green; web full suite green except the known i18n-request parallel-load flake (passes in isolation). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
There was a problem hiding this comment.
The dead-export issue from the prior review is still open: SkeletonCard and SkeletonAvatar are exported from apps/mobile/components/ui/skeleton.tsx with zero callers — only SkeletonLine is imported anywhere in mobile. The two commits added since (7e02e64 onboarding-swatch fix, 56b80a4 goal-form validation reorder) don't touch the skeleton file. Everything else in the PR is solid.
…r wrap
Two defects made the goal detail drawer read broken:
- The metrics block printed "Goal analysis" twice in a row: the section label
and the status row both used goals.metrics.title (pre-existing on web, and
faithfully mirrored onto mobile in the panel parity rewrite - the wrong
side of the pick, since mobile had the correct "Status" label). The status
row is now labeled "Status" on both platforms; goals.detail.statusLabel
restored in both locales.
- The web footer quiet-link actions wrapped mid-label into two cramped
centered lines ("Marcar como / Concluida"). Buttons are now nowrap and the
row itself wraps (mobile already did this via flexWrap).
Refs #117. Static checks green all 3 workspaces; shared 66 + mobile 85 test
files + web goals tests (67) pass.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
There was a problem hiding this comment.
The dead-export issue from the two prior reviews is still unresolved. SkeletonCard and SkeletonAvatar remain exported from apps/mobile/components/ui/skeleton.tsx with zero callers — the SkeletonCard names in habit-list/empty-state.tsx and goals-view.tsx are locally-defined components with a styles: prop, not imports from this module.
The latest commit (c10e5b7 — status-label fix + web footer nowrap) is clean: the goals.detail.statusLabel restoration on both platforms and the whiteSpace: 'nowrap' + flex-wrap row fix are correct. No new issues introduced.
Single blocker to unblock: drop SkeletonCard and SkeletonAvatar from the exports in apps/mobile/components/ui/skeleton.tsx. Add them back when they have real callers (CLAUDE.md rule 2 — no just-in-case exports).
…r vocabulary The goal drawer footer predated the redesign and read broken: Ask Astra was a non-clickable decoration on BOTH platforms (the habit drawer wraps the same block in a real button), and the actions were centered quiet-links that wrapped into two cramped lines with no hover states. Now mirrors the habit drawer exactly: - Ask Astra is a real button on both platforms - seeds the chat draft with the goal title and navigates to /chat; web gets the same hover lift, active scale, and trailing chevron as the habit drawer footer; mobile gets the TouchableOpacity + chevron treatment. - Actions become full-width hairline rows (15px labels, hover/pressed bg-elev, destructive = italic fg-3): Mark completed / Abandon (or Reactivate), Edit, Delete. GoalActionButton -> GoalActionRow on web; mobile GoalActionFooter rewritten to Pressable rows. - "Atualizar Progresso" gains a hover color shift. Tests: both drawer tests mock the new router dependency. Refs #117. Static checks green; web 150 (goals 67) + mobile 85 test files pass. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
There was a problem hiding this comment.
The dead-export issue flagged in the two prior reviews is still unresolved. SkeletonCard and SkeletonAvatar remain exported from apps/mobile/components/ui/skeleton.tsx with zero callers — the three SkeletonCard components in habit-list/empty-state.tsx, goals-view.tsx, and habit-list.tsx are locally-defined components with a styles: prop that don't import from this primitive. SkeletonAvatar has no callers at all.
Single blocker: delete SkeletonCard and SkeletonAvatar from apps/mobile/components/ui/skeleton.tsx. Re-add them when there's a real caller. CLAUDE.md rule 2 — no just-in-case exports.
Everything else in this PR — the ban purge, completion ring-sweep, parity closes, Kotlin widget token migration, logo swap, primitive reconciliation, and the latest goal drawer footer redesign — is clean and solid.
…, OTP cell overflow Add --bg-elev-pressed to web globals (mirrors mobile bgElevPressed) so hover/pressed states are visible on bg-elev modal surfaces; the old hover painted bg-elev on bg-elev. Goal drawer actions become icon+label menu rows with no internal dividers. Ask Astra hover fixed in both drawers. Code input digit cells get fixed 44px width + fieldset min-inline-size reset: the UA default min-content sizing let six size=20 inputs stretch the row to 2410px on desktop viewports. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Mobile skeleton primitive keeps only SkeletonLine (its sole consumer surface); web keeps SkeletonLine + SkeletonCard (goals-view caller) and drops SkeletonAvatar, which only its own test referenced. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
There was a problem hiding this comment.
The dead-export issue from the three prior reviews is still unresolved. SkeletonCard and SkeletonAvatar remain exported from apps/mobile/components/ui/skeleton.tsx with zero callers — only SkeletonLine is imported from this module anywhere in mobile. The local SkeletonCard definitions in habit-list/empty-state.tsx and goals-view.tsx take a styles: prop and are entirely separate components.
The two commits added since the last review (94e1cfb goal-drawer footer redesign and 9a78ec3 hover-states + OTP overflow fix) are clean — both land correctly on web and mobile with no new issues.
Single blocker: drop SkeletonCard and SkeletonAvatar from the exports in apps/mobile/components/ui/skeleton.tsx. Add them back when they have real callers (CLAUDE.md rule 2 — no just-in-case exports).
There was a problem hiding this comment.
The dead-export blocker from the prior four reviews is resolved: SkeletonCard and SkeletonAvatar are gone from the mobile primitive (only SkeletonLine remains, with real callers), and SkeletonAvatar is gone from the web primitive too. SkeletonCard stays in web with its goals-view.tsx caller intact.
The rest of this PR — ban purge (glows, shimmer, stripes, em-dashes), completion ring-sweep with reduce-motion parity, habit-drawer mono summary strip via shared formatHabitDetailSummary, goal-card ParentRing differentiator, truthful calendar legend + month-summary parity, goal-metrics-panel hairline-row rewrite, ConfirmDialogV2 consolidation, chat empty-state static rings, Android widget token migration, and the logo swap — is clean and solid.
Self-review of PR #154 against DESIGN.md found and fixed: - goal-card web hover used bg-elev/80 dim; now bg-elev-pressed lift - onboarding-complete trial box double hairline on both platforms - today-shell date label hover aligned to sibling bg treatment - profile delete-modal pressed states were bg-elev on a bg-elev sheet - widget filled-circle drawable dropped its glow halo layer - fresh-start: spring easing replaced with ease-out-quart on both platforms (web keyframe overshoot removed), mobile gains the reduce-motion guard and Geist type - all remaining Animated.spring swept to timing + ease-out (toasts, onboarding check pop); web --ease-spring token deleted with its last three usages retargeted to --ease-out - mobile goal-card text styles set Geist explicitly - habit drawer parity: Ask Astra prompt keyed to checklist items on both platforms; reminder rows use label + mono time value on both - mobile code-input mirrors fixed 44px centered cells Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
thomasluizon
left a comment
There was a problem hiding this comment.
Code Review: PR #154 (design-system conformance, issue #117)
Scope: All 120 changed files on feature/visual-polish-117, reviewed against DESIGN.md (v8 OKLCH tokens, Linear-tactical anchor, bans), root CLAUDE.md code standards, and web<->mobile parity. Review lenses: interactive-state visibility, divider/hairline overuse, token + type discipline, motion law, parity. Conducted with three parallel review lanes (web / mobile / shared+widget+parity) plus targeted gap re-reviews; every flagged finding was independently verified against the source before acting.
Recommendation: APPROVE (all findings fixed in 4045193, pushed to this branch)
Findings (all verified, all FIXED in 4045193)
High
apps/web/app/globals.css+ 5 mobile files: banned spring/elastic motion.--ease-spring(cubic-bezier(0.34, 1.56, 0.64, 1), overshoot) drove check-pop, tag-pop, and the fresh-start orb on web;Animated.springappeared 9 times across mobile (fresh-start, app-toast, achievement-toast, welcome-back-toast, onboarding check pop) while the web twins use plain ease-out. Fixed: all retargeted to ease-out-quart (--ease-out/toAnimatedEasing(easings.out)), the orb keyframe's baked 1.1 overshoot frame removed, and the now-unused--ease-springtoken deleted.apps/mobile/components/ui/fresh-start-animation.tsx: no reduce-motion guard on a 2.5s full-screen celebration (web twin already neutralizes underprefers-reduced-motion). Fixed:AccessibilityInfo.isReduceMotionEnabled()gate that skips rings/scale and keeps only the opacity fade.apps/mobile/app/(tabs)/profile.tsx:802,866: delete-account confirm buttons usedpressed ? tokens.bgElevinside the bottom-sheet (surface =bgElev) — invisible pressed state, the same bug class fixed earlier in the goal drawer. Fixed:tokens.bgElevPressed. (The same pattern inprofile-nav-card/profile-action-buttonwas verified CORRECT — those rows sit on the--bgscreen.)- Parity drift,
habit-detail-drawer: web keyed the Ask Astra prompt onchecklistItems.length, mobile onhasSubHabits— same habit produced different prompts per platform. Fixed: both platforms now key on checklist items.
Medium
apps/web/components/goals/goal-card.tsx:98: hover wasbg-[var(--bg-elev)]/80— a dimming blend toward the page color (sinks on hover instead of lifting). Fixed:--bg-elev-pressed.- Double hairline, onboarding trial box, BOTH platforms (
apps/web/components/onboarding/onboarding-complete.tsx,apps/mobile/components/onboarding/onboarding-complete.tsx): recap list's lastborderBottomstacked against the trial card'sborderTop. Fixed: trial card keeps only its bottom rule. apps/mobile/modules/orbit-widget/.../widget_circle_filled.xml: layer-list carried a literal "outer glow ring" (#33FFFFFFhalo) — DESIGN.md glow ban, and an explicit #117 widget acceptance criterion. Fixed: single solid 22dp oval (tinted at runtime as before).- Parity drift, reminder rows: web drawer interpolated time into the label (
...Atkeys) while its owndueTimerow directly above — and the mobile drawer — use the SettingsRow label + mono value vocabulary. Fixed: web drawer now renders plain label +displayTimemono value; the...Atkeys remain in use by both platforms' habit forms (correct context there). - Geist discipline (mobile):
goal-card.tsxtitle/progress/badge texts andfresh-start-animation.tsxtitle/subtitle set nofontFamily, falling back to the system font. Fixed: explicitGeist; fresh-start title moved from off-ramp 24px to--fs-lg22.
Low
apps/web/app/(app)/today-shell.tsx:180: date label hovered viaopacity-80while its sibling chevrons usehover:bg-[var(--bg-elev)]— inconsistent feedback inside one control cluster. Fixed: matched to the sibling treatment.
Findings raised by review lanes and REJECTED after verification
conflict-warning.tsx(mobile) hex-alpha severity tints: identical to the web twin's/10–/30Tailwind alphas; sanctioned severity-banner vocabulary, not a button fill.goal-card(mobile) status/deadline badgesbgElev-on-bgElev: exact mirror of web's badge classes; hairline-ringed Chip vocabulary where the ring, not the fill, carries the boundary.- Three "add explanatory comments" suggestions: rejected outright — root CLAUDE.md rule 5 bans narration comments (lint-enforced).
Parity verification
10 pairs read side by side: status-dot, confirm-dialog, goal-detail-drawer (+footer/styles), goal-card, calendar-shell, code-input, parent-ring, app-logo, goal-metrics-panel, habit-detail-drawer. 8 PAIRED as-is; the 2 drifts found (Ask Astra discriminator, reminder rows) are fixed above. i18n: en.json and pt-BR.json structurally paired, no missing keys either direction, zero em dashes in copy.
Widget acceptance (issue #117)
- No per-scheme palette remains in Kotlin: single neutral bootstrap fallback only; resolved token hexes arrive via
syncThemefromcreateTokensV2(Kotlin is presentation-only). - Status colors keyed to tokens (done = primary, overdue token, neutral empty); no red/amber fills; drawables are flat solids + strokes (glow halo removed in this review).
- Data contract (
GetHabitWidgetQuery, refresh, size classes) untouched.
Validation
| Check | Status |
|---|---|
| Lint (web + mobile + shared) | PASS |
| Type check (3 workspaces) | PASS |
| Web tests | PASS (150 files, 1465 tests) |
| Mobile tests | PASS (85 files, 412 tests) |
| i18n locale parity | PASS |
What's Good
The token system holds across every changed surface: hairlines carry structure, status reads from canonical --status-*, the single accent stays reserved, mono metadata is consistent, and the new --bg-elev-pressed token closes the invisible-state class on both platforms. The widget migration is clean single-source-of-truth work. Motion is now uniformly ease-out-quart with reduce-motion respected on every celebratory surface.
Recommendation
Fixes are on the branch as 4045193. Ready for the bot re-review and merge.
There was a problem hiding this comment.
The dead-export blocker (SkeletonCard/SkeletonAvatar) was resolved in the previous commit, and the final commit is clean.
The final commit's changes are all correct: profile pressed states use bgElevPressed (not bgElev) on the delete-modal sheet; Animated.spring swept to timing + ease-out-quart across toasts, onboarding-complete-habit, and fresh-start; the fresh-start reduce-motion guard follows the correct async/cancelled-flag pattern; Geist font explicitly set on goal-card text styles; the mobile habit-drawer Astra prompt keyed to checklistItems.length (exact parity with the web side at habit-detail-drawer.tsx:107); reminder rows restructured to label + mono value on both platforms; --ease-spring CSS token fully removed with all three callers retargeted to --ease-out; goal-card hover corrected to bg-elev-pressed; widget glow layer dropped.
The full scope of this PR — ban purge (glows, shimmer, animated circles, stripes, em-dashes, --ease-spring), completion ring-sweep with reduce-motion parity on both platforms, habit-drawer mono summary strip via shared formatHabitDetailSummary, goal-card ParentRing differentiator, truthful calendar legend + month-summary subtitle parity, goal-metrics-panel hairline-row rewrite, ConfirmDialogV2 consolidation, Android widget v8 token migration, and the AppLogo swap — all landed correctly.
The bottom-tab-bar FAB replaced the old floating button without carrying data-tour="tour-fab-button", so the habits tour timed out on step 5 and silently skipped it. StatusDot also rendered a <button> even with no onToggle, nesting a button inside GoalCard's motion.button (hydration error during the goals tour). Read-only dots now render a span with an img role, matching the mobile StatusDot's View branch. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
There was a problem hiding this comment.
One correctness bug in the new commit: the read-only span condition in status-dot.tsx is (!onToggle || disabled), but habit-row.tsx passes both onToggle and disabled together. When disabled=true, the dot now renders as a span with role=img even though it is a temporarily-disabled toggle button — wrong ARIA role for AT users. Fix: drop the || disabled clause and keep only !onToggle; the button branch already handles the disabled state correctly on lines 98-99. One-character fix. Everything else in the PR is solid.
Onboarding: web week-start selector rendered seven hardcoded English day initials with five dead chips; now mirrors mobile's two localized chips from ONBOARDING_WEEK_START_OPTIONS. Flow step content vertically centers via auto margins inside the scroll column. Section labels center on both platforms to match the hero composition. The features step referenced onboarding.flow.features.subHabits.* which existed in neither locale (web crashed with MISSING_MESSAGE; mobile pointed at a paywall object key) — keys added to both locales, mobile retargeted. StatusDot: review-bot catch — disabled interactive dots demoted to role=img instead of a disabled button. Read-only branch now gates on onToggle only; mobile Pressable branch gains disabled handling it never had (disabled prop, accessibilityState, dimmed style). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
There was a problem hiding this comment.
The StatusDot disabled-ARIA bug flagged in the previous review is correctly fixed on both platforms: the read-only branch now gates on !onToggle only, so temporarily-disabled interactive dots stay as <button disabled> (correct role for AT users) rather than being demoted to role=img. The mobile Pressable branch gains the matching disabled prop, accessibilityState, and 0.4 opacity dim it was missing. The rest of the final commit — onboarding week-start chip parity (ONBOARDING_WEEK_START_OPTIONS replacing the 7-hardcoded-initial row), missing subHabits i18n keys added to both en.json and pt-BR.json, and vertical centering — is all clean. This is a very large, well-executed craft lift: ban purge, completion ring-sweep with reduce-motion parity, habit-drawer mono summary strip via shared formatHabitDetailSummary, goal-card ParentRing differentiator, truthful calendar legend + month-summary subtitle parity, goal-metrics-panel hairline-row rewrite, ConfirmDialogV2 consolidation, Android widget v8 token migration, and the AppLogo swap. Approving.
tour-goals-tab, tour-calendar-month-nav, and tour-profile-streak had no DOM anchors after the polish, so the tour timed out and skipped goals step 1, calendar step 3, and profile step 1. SectionHeadTabs gains per-tab dataTour plus the tab-<id> element ids its keyboard focus restore already targeted; the calendar month-nav cluster and the profile streak row carry their anchors again. Mobile already registers all three via useTourTarget. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
There was a problem hiding this comment.
The three web tour anchors missing after the polish pass (tour-goals-tab on the goals SectionHeadTab, tour-calendar-month-nav on the month-nav cluster, tour-profile-streak on the profile streak row) are correctly restored. Mobile already had all three via useTourTarget — parity is intact. The id={tab-${tab.id}} addition to SectionHeadTabs is already consumed by today-shell.tsx:105 for keyboard focus restore (not dead code). No high-signal issues in any of the new commits. The full scope of this PR — ban purge, completion ring-sweep with reduce-motion parity, habit-drawer mono summary strip via shared formatHabitDetailSummary, goal-card ParentRing differentiator, truthful calendar legend + month-summary parity, goal-metrics-panel hairline-row rewrite, ConfirmDialogV2 consolidation, Android widget v8 token migration, AppLogo swap, StatusDot ARIA semantics, and these tour anchor restores — is solid.
SettingsDescription drops its hardcoded closing rule on both platforms; mobile SettingsRow gains the divider prop web already had. Calendar-sync, ai-settings, advanced, upgrade and preferences stop ruling sparse status notes, empty states and feature lists; web advanced also gates the Pro wrapper (free users saw an empty bordered box) and spaces the MCP instructions block. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
There was a problem hiding this comment.
The final commit (574099c) is a clean whitespace-over-hairlines cleanup: both SettingsDescription components drop their hardcoded closing rule, mobile SettingsRow gains the divider prop that web already had (default true, divider={false} on callers with following helper text), and web advanced correctly gates the Pro API-keys block so free users no longer see an empty bordered box. All changes land on both platforms. No new issues introduced. Full scope of this PR — ban purge, completion ring-sweep with reduce-motion parity, habit-drawer mono summary strip via shared formatHabitDetailSummary, goal-card ParentRing differentiator, truthful calendar legend + month-summary parity, goal-metrics-panel hairline-row rewrite, ConfirmDialogV2 consolidation, Android widget v8 token migration, AppLogo swap, StatusDot ARIA semantics, and tour anchor restores — is solid and ready to merge.
…Astra The lucide Sparkles glyph read as generic AI iconography. Every surface where it meant Astra/AI now uses lucide Orbit, stroke-only (the summary header previously filled it): Today summary, chat header/empty state, message bubbles, typing indicators, Ask-Astra drawer footers, retrospective, AI settings, onboarding, and the profile nav (shared iconKey sparkles renamed to orbit). Sparkles survives only where it means Pro shine (upgrade) or referral reward. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
There was a problem hiding this comment.
The final commit (4ac6d12) is a clean Sparkles → Orbit icon swap for all AI/Astra surfaces. Both web and mobile updated in parity: Today summary, chat header/empty state, message bubbles, typing indicators, Ask-Astra drawer footers (habits + goals), retrospective, AI settings, onboarding meet-astra and features, and the profile nav (shared iconKey 'sparkles' → 'orbit' with both ProfileNavIcon switch arms updated). fill removed from the Orbit glyph on today-ai-summary (stroke-only per design spec). Sparkles correctly preserved on upgrade/referral surfaces. Tests updated throughout. No dead code, no type violations, no parity gaps. Full scope of this PR — ban purge (glows, shimmer, animated circles, em-dashes, --ease-spring), completion ring-sweep with reduce-motion parity, habit-drawer mono summary strip via shared formatHabitDetailSummary, goal-card ParentRing differentiator, truthful calendar legend + month-summary parity, goal-metrics-panel hairline-row rewrite, ConfirmDialogV2 consolidation, Android widget v8 token migration, AppLogo swap, StatusDot ARIA semantics, tour anchor restores, and now the Orbit mark for Astra — is solid and ready to merge.
|



Summary
App-wide visual craft lift inside the locked Linear-tactical anchor (DESIGN.md) - purge of AI-slop decoration, exact web/mobile parity, the three signature differentiators, the real logo, and the Android widget token migration. One branch, one PR, per the umbrella issue.
Ban purge (W1 + motion pass)
Differentiators (one per priority surface)
Parity closed
Validation
turbo lint type-check testgreen across web / mobile / shared (web 150, mobile 85, shared 66 test files; i18n key sync included)Closes #117
🤖 Generated with Claude Code