feat(calendar): year arrows + week/N-day time-grids + stats redesign + sync polish (#8, #13) - #326
Conversation
…olish (#8, #13) Year-skip arrows on month nav (both platforms). Month-stats redesigned as a data-driven extensible StatTile grid. Web: view switcher (Month/Week/Range) + Google-Calendar-style time-grid (hourly rows, day columns, timed-habit blocks, all-day band, now-line) for week + custom contiguous-range; mobile keeps month. Sync screen: Sincronizar agora->Sincronizar, deselect-all text->icon button. web+mobile parity for shared parts; grids web-only by design; both locales. Co-Authored-By: Claude Opus 4.8 (1M context) <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.
Code Review: PR #326
Scope: PR #326 — feat(calendar): year arrows + week/N-day time-grids + stats redesign + sync polish (#8, #13)
Recommendation: APPROVE
Summary
PR #326 delivers four calendar enhancements across both platforms: year-skip arrows on the month-nav header, a data-driven StatTile stats grid replacing the flat settings-row list, sync-screen polish (label shortening + icon button for deselect-all), and — web-only by design — Month / Week / Range view switcher backed by a CalendarTimeGrid Google-Calendar-style hourly grid. The cross-platform parity is correct, i18n keys are fully in sync, and no shared contract types are changed. All adversarial passes came back refuted. One Medium finding stands: a stray paywall research file was accidentally committed into this calendar PR.
Findings
Critical
None.
High
None.
Medium
[MEDIUM] Stray .claude/research/paywall-redesign.md committed in wrong PR
- dimension: Dead/stale code (#2)
- location:
.claude/research/paywall-redesign.md(new file, 46 lines) - issue: This file is a design brief for the paywall/upgrade redesign (PR #6, task #7). It has no relationship to the calendar enhancements in this PR.
.claude/research/is tracked by git (not listed in.gitignore), so it ships with the merge. - risk: The paywall research document is permanently attached to this calendar feature commit, polluting the git log with an unrelated artifact.
- fix: Remove
.claude/research/paywall-redesign.mdfrom this branch before merging. Re-commit it on the paywall feature branch (PR #6/#7) where it belongs. - reference: CLAUDE.md rule 2 — "Delete unused code immediately."
Low / Info
None.
Subagents
| Agent | Verdict |
|---|---|
| parity-checker | PAIRED |
| i18n-syncer | IN SYNC |
| contract-aligner | N/A — no packages/shared/src/types/* or orbit-api changes |
| security-reviewer | N/A — no orbit-api changes |
Parity: Web-only features (CalendarTimeGrid, CalendarWeekView, CalendarRangeView, CalendarWeekNav) are correctly absent from mobile — PR body documents this as a design decision. All shared features (year arrows, StatTile stats grid, SelectAllToggle icon button, CalendarStats) are mirrored on both platforms.
i18n: All 12 new keys (common.previousWeek/nextWeek, calendar.goToCurrentWeek, calendar.view.*, calendar.timeGrid.*) are present in both en.json and pt-BR.json. The syncNow shortening is consistent in both locales.
Adversarial passes (all three refuted):
- CalendarTimeGrid 199 lines over hard cap — REFUTED.
TimedBlockandAllDayChipalready extracted; function body is 15 lines of logic + pure JSX composition for three tightly-coupled zones sharingperColumn/gridTemplate. Splitting would only add prop-drilling. - Mobile SelectAllToggle missing
accessibilityState— REFUTED. The self-describing label pattern ("Select all" / "Deselect all") is the intentional a11y signal confirmed by the test suite.accessibilityState={{ selected }}maps toaria-selected— wrong semantics for a toggle button. - Now-line stale after mount — REFUTED.
CalendarTimeGridunmounts/remounts on every view-switch (React conditional rendering), sonowMinutesis always fresh at mount. Habit-tracker usage pattern makes the stale-for-hours scenario implausible.
Validation
| Check | Result |
|---|---|
| Lint | PASS (per PR body: 0 errors) |
| Type check | PASS (per PR body: clean) |
| Tests | PASS (per PR body: web 1668 · mobile 640 · shared 1031 — all green) |
| Build (api) | N/A |
Deferred — N/A dimensions
- Comment policy (#4): N/A — no banned
//narrative comments in any changed file. - No-workaround (#5): N/A —
useCalendarRangecorrectly reuses existingfetchCalendarMonthendpoint; no client-side reshaping. - Type safety (#6): N/A — no
any,as any, or escape hatches in the diff. - console.log (#7): N/A — no
console.login production code. - Contract drift + backward-compat (#11): N/A —
packages/shared/src/types/*and orbit-api DTOs untouched. - Security (#12): N/A — no new auth boundaries or endpoints.
- Backend hard rules (#13): N/A — orbit-api not touched.
- DESIGN.md / AI-slop (#8): Checked and clean. All new components use semantic tokens only (
var(--primary),var(--fg-*),var(--hairline),var(--bg-card)).color-mix(in srgb, var(--primary) NN%, transparent)is the correct primary-tint idiom. Notransition-all, noh-screen, no raw--slate-*or hardcoded violet rgba. Scene-sentence: "A navy-cosmic weekly time grid with a violet now-line and status-colored habit blocks in Google Calendar columns" — Orbit-specific, not generic SaaS.
What's good
- StatTile grid replacing three
SettingsRowstubs: clean array-driven pattern, adding a stat is one object literal. useCalendarRangeQueryprivate helper: eliminates duplication betweenuseCalendarDataanduseCalendarRangewithout over-abstracting — both callers have clean, separately-exported APIs.CalendarGridrange-highlight (isInRange,isEndpoint,highlighted): correctly layered on top ofselectedwithout touching hit-targets or status-dot logic.layoutTimedcluster-flush algorithm: compact (44 lines), correct, handles overlap lanes and edge cases cleanly.- All tests assert behavior, not implementation details — exactly what the testing standard requires.
- Week-view intentionally absent from mobile — right call, clearly documented.
Recommendation
Remove .claude/research/paywall-redesign.md from this branch, then merge. All feature work is correct, cross-platform parity is complete, i18n is in sync, and no breaking contract changes exist.
🤖 Generated with Claude Code
# Conflicts: # apps/mobile/test-mocks/lucide-react-native.ts # apps/web/app/(app)/calendar/page.tsx
|
There was a problem hiding this comment.
Code Review: PR #326
Scope: PR #326 in thomasluizon/orbit-ui-mobile — feat(calendar): year arrows + week/N-day time-grids + stats redesign + sync polish (#8, #13)
Recommendation: APPROVE
Summary
PR #326 adds four coordinated calendar enhancements across web and mobile: year-skip arrows on the month-nav header, a data-driven StatTile-based month-stats section replacing the flat SettingsRow list, a SelectAllToggle icon-button replacing the overflowing text button on the sync screen, and (web-only by deliberate design) a Google-Calendar-style time grid powering new Week and Range views. The diff is well-factored, i18n-parity is clean in both locales, cross-platform parity is correct for all shared features, and no shared types or API contracts were touched. One Medium finding exists (stale "now" line in the time grid), but nothing blocks merge.
Findings
Critical
None
High
None
Medium
[MEDIUM] nowMinutes frozen at mount — now-line does not track real time
- dimension: Correctness (#1)
- location:
apps/web/components/calendar/calendar-time-grid.tsx:205-208 - issue:
nowMinutesis computed once viauseMemo(() => ..., [])with an empty dependency array. The calculated minute offset captures the clock at component mount and never updates. On a week/range view left open across a clock boundary (e.g. hour rollover, or simply left open for 30+ minutes), the violet "now" line will be visually wrong. - risk: The now-line drifts silently; no crash, no data loss, but users looking at the week grid an hour later will see the now-line pointing to the wrong hour.
- fix: Replace
useMemowithuseState+ asetIntervalthat refreshes every minute:const [nowMinutes, setNowMinutes] = useState(() => { const now = new Date() return getHours(now) * 60 + getMinutes(now) }) useEffect(() => { const id = setInterval(() => { const now = new Date() setNowMinutes(getHours(now) * 60 + getMinutes(now)) }, 60_000) return () => clearInterval(id) }, [])
- reference: CLAUDE.md rule 1 (Correctness / boundary conditions)
Low / Info
None (per signal gate — no nits posted)
Subagents
| Agent | Verdict |
|---|---|
| parity-checker | PAIRED — year arrows, CalendarStats, and SelectAllToggle all mirrored; week/range/time-grid are web-only per documented design decision in PR body |
| i18n-syncer | IN SYNC — all 10 new keys present in both en.json and pt-BR.json; syncNow copy change applied to both locales |
| contract-aligner | N/A — no packages/shared/src/types/* or endpoints.ts changes; no orbit-api changes |
| security-reviewer | N/A — no orbit-api changes in this PR |
Validation
| Check | Result |
|---|---|
| Lint | PASS (PR body: "lint 0 errors") |
| Type check | PASS (PR body: "type-check clean") |
| Tests | PASS (PR body: "web 1668 · mobile 640 · shared 1031 — all green") |
| Build (api) | N/A — no orbit-api changes |
Deferred — N/A dimensions & files not verdicted
- Dimension 13 (Backend hard rules) — N/A: no orbit-api files in this diff.
- Dimension 11 (Contract drift / backward-compat guard) — N/A: no
packages/shared/src/types/*or Zod schema lines changed; no field additions/removals to check. - Dimension 12 (Security — API side) — N/A: no orbit-api files changed. Frontend security checks (no
dangerouslySetInnerHTML, no raw user content in JSX, no auth state leakage) all pass. .claude/research/paywall-redesign.md— Internal design brief committed to.claude/research/(not shipped code); no behavior reviewed.
What's good
- Clean DRY refactor:
useCalendarRangeQueryprivate function extracted fromuseCalendarDatasouseCalendarDataanduseCalendarRangeshare a single query setup — exactly the right level per rule 10. - Intentional platform split documented: The PR body explicitly names the reason week/range views are web-only ("a multi-column hourly grid doesn't fit a phone"), satisfying the parity exception for platform adapters.
- Test coverage: New components (CalendarStats, SelectAllToggle, CalendarTimeGrid, CalendarWeekNav) all have focused behavior tests.
CalendarTimeGridtests usedata-testid+data-hourattributes rather than class names — correct per testing conventions. - Accessibility: Both
SelectAllToggleimplementations expose the toggled label as the accessible name (web:aria-label+aria-pressed; mobile:accessibilityLabel+accessibilityRole). Year-skip buttons carry descriptive labels. All icon-only controls have text alternatives. - Semantic tokens only:
color-mix(in srgb, var(--primary) …)for range highlighting,var(--fg-*),var(--hairline),var(--bg-card)throughout — no raw slate values or hardcoded rgba. layoutTimedlane algorithm: Correctly clusters overlapping timed events and assigns non-overlapping lanes — handles the edge case of entries starting at the exact end of a prior block (strict>=boundary).
Recommendation
One Medium finding (stale now-line) — does not block merge, but should be tracked as a follow-up. A setInterval-based refresh (60 s) fixes it in ~10 lines. Everything else is clean. APPROVE.
…339) #326/#333 shipped the calendar Week (7-day) and Interval (N-day) time-grid views to apps/web only. Mirror them to apps/mobile for cross-platform parity. Mobile: - Mês / Semana / Intervalo view switch on the calendar screen. - Week view: 7-day time grid with week-granularity nav + show-recurring toggle. - Interval view: range-picker mini-calendar (14-day cap + clamped notice) plus an N-day time grid + show-recurring toggle. - New CalendarTimeGrid, CalendarWeekView, CalendarRangeView, ShowRecurringToggle and CalendarWeekNav; CalendarGrid gains an optional range-highlight mode; useCalendarRange hook. Shared: - Lift clampRangeToMaxDays + MAX_RANGE_DAYS from apps/web/lib/calendar-range to packages/shared/src/utils/calendar-range.ts so both platforms consume one copy (web re-points its import; no duplication). filterRecurringEntries was already shared and is reused. Web: - Make the shared CalendarTimeGrid horizontally scrollable: day columns keep an ~80px minimum width and scroll left/right with the left time gutter and the header/all-day rows pinned, instead of compressing at narrow widths. Applies to both the web week and interval views; mobile mirrors the same behavior. Tests: shared 14-day clamp; mobile time-grid render (timed + all-day, N columns, header tap), view switch, and show-recurring filtering. Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>



Calendar enhancements (web + mobile)
Both platforms
StatTilegrid (best streak / total / missed) — adding a stat is one array entry.Web/desktop only (by design — a multi-column hourly grid doesn't fit a phone)
CalendarTimeGrid(time gutter + day columns, hourly rows, an all-day band for untimed habits, timed habits asdueTimeblocks with overlap lanes, a faint violet now-line). Powers Week (7 cols) and custom contiguous Range (pick a span in the mini-calendar → that many columns). Mobile keeps month.Validation
type-check clean · lint 0 errors · tests web 1668 · mobile 640 · shared 1031 (i18n parity) — all green. Semantic tokens only, reduced-motion-gated.
🤖 Generated with Claude Code