combined: habits redesign + streak merit + audit fixes - #52
thomasluizon wants to merge 35 commits into
Conversation
… i18n Threads an optional `icon` string through every habit schema and request builder so both apps can carry the emoji. Adds platform-agnostic helpers for recent-emoji tracking and a grapheme-aware title initial fallback so the redesigned card always has an identity. - Types: icon on baseHabitFieldsSchema + create/update/subHabit/bulk request schemas - Validation: validateHabitIcon + normalizeHabitIcon (1..32 chars, no control chars), MAX_HABIT_ICON_LENGTH constant, threaded into validateHabitForm - Request builders: icon normalized into create/sub-habit/update payloads; update only writes icon when the form field is defined so untouched forms stay lean - Utils: emoji-recents (parse/add/remove/stringify with 24-item limit and dedupe) and getHabitInitial (Intl.Segmenter with UTF-16 fallback) - i18n: habits.form.icon* and habits.emojiPicker.* keys in en + pt-BR with proper diacritics - Tests: 37 new unit tests covering null/emoji/ZWJ/flag/over-length/control-char paths and recents CRUD Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Introduces a frimousse-backed emoji picker inside AppOverlay so the habit form gains a real Unicode icon field, and threads the picked emoji through the card as a 40px avatar tile with title-initial fallback. - frimousse@0.3.0 wraps EmojiPicker.Root/Search/Viewport/List with Tailwind-styled primitives, Notion-style recents row, and skin-tone selector - HabitEmojiPicker renders inside existing AppOverlay (Dialog on desktop, Drawer on mobile) so it matches every other modal surface - HabitIconField trigger in habit-form-fields.tsx wires icon into react-hook-form via setValue with shouldDirty+shouldValidate; errors flow through errors.icon.message as an i18n key - HabitEmoji renders icon or getHabitInitial() in a themed tile; used on card header for parent habits (hidden on narrow mobile for now) - useEmojiRecents hook persists last 24 emojis to localStorage via the shared addRecent/parseRecents helpers - useHabitForm and buildEmptyHabitFormValues / buildEditHabitFormState default icon to null; edit modal seeds from habit.icon so existing habits survive - Shared utils index re-exports getHabitInitial + emoji-recents helpers for mobile parity Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…card Mirrors the web emoji picker on mobile using rn-emoji-keyboard's modal EmojiPicker themed to the active color scheme, plus a HabitEmoji avatar tile that renders the picked emoji or the title initial fallback. - rn-emoji-keyboard@1.7.0 EmojiPicker with enableSearchBar + enableRecentlyUsed + categoryPosition top, themed via active scheme tokens (primary, surface, surfaceElevated, textPrimary/Secondary/Muted, borderEmphasis) - HabitEmojiPicker swaps the en/pt translation packs based on i18n.language so PT and EN users see translated category labels - HabitIconField trigger uses HabitEmoji + ChevronDown row on form, with an inline Clear button when an icon is set - HabitEmoji uses theme primary_15 for tint, primary for filled state, red500_10/danger for bad habits, red500_30 ring for overdue - habit-card renders the avatar tile for parent habits (not children, not in select mode, not when there are sub-habit progress rings) - useHabitForm seeds icon: null in defaultValues; useEmojiRecents persists to AsyncStorage via shared addRecent/parseRecents helpers - habit-form-state empty-values test updated to include icon: null Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…io utils
Moves getHabitProgressStrokeDasharray from apps/mobile/lib into
packages/shared/src/utils/habit-progress.ts alongside two new helpers
used by the Avatar+Arc redesign:
- getHabitProgressRatio(habit, { hasChildren, childrenDone,
childrenTotal }) - 0..1 fill ratio for the arc, covering completed,
parent-with-children, and flexible habits.
- shouldShowHabitProgressArc(habit, context) - whether the arc should
render around the avatar tile at all.
Keeps apps/mobile/lib/habit-progress.ts as a thin re-export so the
existing mobile test and any direct imports keep working.
Full visual rewrite of the Today / habits list card:
- New 52px rounded-square avatar tile anchors the card on the left;
renders the chosen emoji or the title's initial as fallback. Single
accent surface per card so a 15-card Today view stays calm.
- Tapping the avatar logs / unlogs the habit (or toggles selection in
select-mode, or force-logs a parent with unfinished children).
- Optional thin SVG progress arc wraps the tile for parent-with-
children and flexible habits, sharing the shared progress helpers.
- Slim 3px left bar marks due-today / overdue status; 1px destructive
ring on the tile hints overdue.
- Completed state: 0.55 opacity, solid primary tile, tiny check badge
in the bottom-right corner with a surface ring so it reads clean
against the background.
- Compact meta row with overflow +N collapse keeps the Today view
dense; tags / streak / checklist / goals all flow through one row.
- Checklist progress renders as a 3px full-bleed bottom strip instead
of a chip badge, staying out of the meta row.
- Menu (kebab) extracted to habit-card-menu.tsx with portal,
keyboard navigation, viewport flip, and bottom-nav awareness.
- Card drops 900+ lines of legacy layered-glass styling in globals.css
in favour of a cleaner v4 block with CSS variables and hover/press
transitions that respect motion-reduction.
- habit-card.tsx itself drops from 1,218 to ~440 lines by leaning on
habit-avatar-tile, habit-meta-row, and habit-card-menu primitives.
Preserves every existing behavior: long-press, drag-to-reorder hooks
(data-no-drag), bulk select mode, checklist progress, parent/sub-habit
nesting, tour anchors (tour-habit-card / tour-habit-tags), and the
portal-based actions menu with ArrowUp / ArrowDown / Home / End nav.
Tests: the existing 43 behavioral tests pass against the new
implementation (one assertion tweak for the renamed completed state
class); 8 new tests cover emoji rendering, initial fallback, arc
presence, tile-tap log/unlog, and status-bar class for overdue.
Mobile counterpart to the web rewrite. Same visual direction, same
interaction model, same cross-platform parity:
- New HabitAvatarTile (components/habits/habit-avatar-tile.tsx):
52 px rounded-square surface with the user's emoji or the title's
initial fallback. Tap to log. Optional 2 px SVG arc wraps the tile
via react-native-svg using the shared getHabitProgressStrokeDasharray
and getHabitProgressRatio helpers.
- HabitMetaRow (components/habits/habit-meta-row.tsx): compact chip
strip with +N overflow, mirroring the web layout.
- HabitCard rebuilt from scratch (~1,524 LOC -> ~570 LOC) around the
shared primitives. LinearGradient-heavy v3 chrome + log-button
ceremony replaced by a calm neutral surface with a single accent
in the avatar tile. Slim 3 px status bar marks due-today / overdue.
- Tile pulse on completion (spring scale 1 -> 1.08 -> 1) and tile
creation glow moved off the whole card onto the tile itself so the
attention lands where the identity is.
- Behavior preserved verbatim: long-press to drag, bulk select mode
with checkbox in tile slot, anchored actions menu, parent/sub-habit
expand toggle, checklist progress strip along the bottom edge,
tour anchors (tour-habit-card / tour-habit-tags).
- Child habits drop the arc and status bar; indent and smaller
rounded radius preserve the nesting signal.
Tests: all 5 existing mobile habit-card behavioral tests still pass,
plus 3 new tests exercising emoji rendering, initial fallback, and
tile-tap log flow.
Polish pass on the create / edit habit modals so every section reads
with a steady rhythm:
- Outer <form> bumped from space-y-4 / space-y-5 to space-y-6 (1.5rem
between every top-level section). User screenshot showed the gap
after FREQUÊNCIA, around TAGS, and around the 'Mais opções' divider
felt cramped at the previous gap.
- The 'Mais opções' divider now uses mt-2 + pt-3 with a tighter inner
py-2 button so the toggle has breathing room above and below the
horizontal rule instead of being glued to it.
- Inside the collapsible advanced section, pt-4 instead of pt-2 so the
Description / CHECKLIST sections start cleanly below the toggle.
- Checklist + Templates inner gap bumped from mt-3 to mt-4 so MODELOS
sits clearly below the input row instead of glued to it.
No structural changes; pure spacing polish. All 70 affected
form-fields / create-modal / edit-modal tests still pass.
Mirror of the web spacing polish so both platforms read the same:
- Container gap bumped from 22 to 24 so every top-level section sits
on the same vertical rhythm as the web 1.5rem (space-y-6).
- fieldGroup gap bumped from 8 to 10 so each section's label sits
further from its input.
- 'Mais opções' divider expanded its paddingTop from 8 to 14 with a
fresh marginTop: 4 so the horizontal rule isn't glued to the
section above; the button itself uses paddingVertical 6 so the
toggle row stays compact.
- advancedSection gap bumped from 24 to 28 with paddingTop 12 so the
expanded options breathe and start cleanly below the toggle.
- Bad-habit checkboxRow paddingVertical bumped from 4 to 6 to match
the looser cadence of its neighbors.
No new tests required for a pure spacing pass.
Mirror the API contract change: freezes are earned (1 per 7-day streak, cap 3 held) and usage is capped at 3 per calendar month. Display both caps separately and show earn-progress with a 7-step bar. Shared: - streakInfoSchema + streakFreezeResponseSchema extended with the new merit-based fields (balance, held cap, days/progress to next, etc.). - deriveStreakFreezeState returns the extra fields plus canEarnMore and monthlyLimitReached; canFreeze now requires a positive balance. - formatFreezeEarnProgress helper; factories gain createMockStreakInfo. - i18n: new keys balanceHeld, monthlyUsage, earnProgress*, capped*, noneEarned*, earnedCelebration*; confirmBody rewritten to surface held and monthlyRemaining; new toast errors for the two error codes (pt-BR with proper diacritics). Web + Mobile parity: - use-gamification applies server-authoritative balance/usage values on freeze success and returns the full derived state. - Streak page / screen renders held + monthly usage side-by-side and a 7-dot progress bar (or the "max held" banner when capped). - Profile streak card shows "Next freeze in N days" or the held cap. - Freeze error handling maps NO_STREAK_FREEZES_EARNED and STREAK_FREEZE_MONTHLY_LIMIT_REACHED to distinct i18n messages.
…ferral/JWT
Security hardening on the web BFF auth layer:
- Dedicated /api/auth/{send-code,verify-code,google,logout} routes now call
buildForwardedClientHeaders(request) so X-Forwarded-For / CF-Connecting-IP /
X-Orbit-Country-Code / Accept-Language / X-Orbit-Time-Zone reach the .NET
backend. Previously these routes sent no client context, so the backend's
DistributedRateLimitFilter partitioned every anonymous auth request under
the Next.js server IP — collapsing the per-client 5 req/min bucket.
- auth_token cookie maxAge changed from 86_400 (1 day) to 604_800 (7 days) so
it matches the backend JwtSettings.ExpiryHours=168. Prevents daily forced
token refresh round-trips.
- Length-cap the JWT payload segment in /api/auth/session before JSON.parse
(4 KB ceiling). Blocks a memory-amplification vector where a malicious
cookie contains a megabyte-scale base64 segment.
- Cap referral query-param at 40 chars before cookie write on the login page.
Includes new unit tests for send-code header forwarding and updates
auth-api.test.ts to assert the 7-day maxAge.
Covers frontend plan Area A #1, #2, #4, #5.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
formatChatMessage previously only ran a hand-rolled regex that escaped & < > ". It was then passed straight into dangerouslySetInnerHTML. That is safe today, but: - Single-quote and backtick were not escaped. - A malicious AI reply could emit RTL overrides, zero-width joiners, or homograph URL fragments that the regex never touches. The formatter now: 1. Escapes &, <, >, ", ', and `. 2. Applies the **bold** / *italic* markdown replacements. 3. Runs DOMPurify as defense-in-depth with ALLOWED_TAGS=['strong','em'] and no attributes, so even if future markdown rules introduce a tag (e.g. <a>), it cannot leak executable markup. DOMPurify was already a direct dependency (used in retrospective/page.tsx and app-overlay.tsx), so this adds no new package weight. Adds tests for single-quote/backtick escaping and DOMPurify's strip-unknown- tag behavior. Covers frontend plan Area A #3 (P0 #7). Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
useHabitDetail (HabitDetail shape) and useHabitFullDetail (HabitFullDetail shape) both cached under habitKeys.detail(id). Whichever hook ran second won the cache entry; the other consumer then read mis-typed data, risking runtime crashes where fields were missing. Adds habitKeys.fullDetail(id) and retargets useHabitFullDetail on web + mobile. Cache invalidations in use-habits and habit-mutation-helpers now invalidate both keys on habit mutation. Covers frontend plan P0 #3 / Area C #8. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…lidators Covers the P1 missing-validation findings in the audit: - validateGoalDeadline(deadline, today): blocks past dates, rejects garbage strings (goals.form.deadlinePast / goals.form.deadlineInvalid i18n keys). - validateReminderTimes(minutes): enforces the backend contract that minutes-before-due offsets are integers in [0, 1440] and unique. - validateDueTimes(dueTime, dueEndTime): blocks the case where a user sets only the end time without a start time. validateReminderSelection and validateHabitForm now delegate into the new validators so inline errors fire on the existing form flows without touching each component. Adds en/pt-BR i18n keys with proper diacritics on the Portuguese strings. Extends validation.test.ts and goal-form.test.ts with exhaustive boundary cases. Covers frontend plan Area D #1, #2, #3. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
DraggableFlatList previously had no getItemLayout, so React Native had to measure every row before rendering during initial scroll and drag events. With 40+ habits on mid-tier Android this produces visible jank. Adds an approximate ESTIMATED_HABIT_ITEM_HEIGHT=104 and wires getItemLayout onto the primary DraggableFlatList driver. The value does not need to be perfect: telling RN that rows are of roughly uniform height unlocks scroll-to-offset fast-paths and smoother drag. Covers frontend plan Area C #3 (P0 #9). Note: Splitting habit-list.tsx + habit-card.tsx into smaller files (Area C #1, #2) is deferred as a TODO — it is a large refactor across 2,119 + 1,511 LOC and is better done in a dedicated PR with profiling measurements. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Non-shippable fixes from the audit that require architectural decisions, new migrations, or large refactors have TODO comments citing PLAN.md: - auth-api.ts: tryRefreshSession mutex — needs single-flight dedup (Area A #6) - actions/chat.ts: server-persisted conversations — needs DB migration + API contract change (coordinated with backend F4 / P0 #5) - habit-list.tsx: split into smaller components (Area C #1) - habit-list.tsx: mobile parity — port bulk-action-bar, controls-menu, today-filters from web (Area E #1) Each TODO references PLAN.md with the audit section + priority so the follow-up PR author can find the context. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Single-flight map in apps/web/lib/auth-api.ts keyed by refresh-token value. Five concurrent 401 retries now issue ONE /auth/refresh request and receive the same result, so backend token rotation cannot invalidate overlapping refreshes. Test covers 5 concurrent callers. - hasInvalidVerificationCodeChars helper in @orbit/shared/utils. use-login-code-entry (web + mobile) now surfaces an inline error when the paste/input contains non-digits, instead of silently stripping. - Added i18n key auth.errors.codeMustBeDigits in en + pt-BR locales. Login UI on both platforms renders the error inline with role=alert. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Mobile parity ports of the web-only components and hooks the audit
flagged. Each is a faithful port (same i18n keys, same prop names,
same behaviour) using NativeWind + RN primitives instead of HTML.
Mobile components added:
- habits/bulk-action-bar.tsx — floating select-mode action bar
- habits/controls-menu.tsx — Modal sheet for select / collapse /
refresh / show-completed
- habits/today-filters.tsx — search bar + frequency / tag chips
- calendar/calendar-grid.tsx — month grid with status colouring
- calendar/calendar-day-detail.tsx — bottom-sheet day detail
Mobile hooks added (parity surface for cross-platform call sites):
- use-summary.ts — re-exports useSummary + adds useInvalidateSummary
- use-chat-composer.ts — debounced AsyncStorage draft persistence
- use-calendar-data.ts — direct parity with web's useCalendarData
- use-color-scheme.ts — wraps useAppTheme with the web hook surface
- use-popover-menu.ts — open/close/toggle (sheet equivalent on RN)
- use-push-notification-preferences.ts — wraps usePushNotifications
Web addition:
- tour/tour-replay-modal.tsx — back-port of the mobile-only modal so
both platforms expose the same tour-replay UX. Uses AppOverlay so
it adapts to Dialog (desktop) / Drawer (mobile <640px).
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…rker - Shared chat-store now exposes conversationId + setConversationId (server is the source of truth; clients only persist a pointer). - Web chat composer and mobile chat screen now send conversationId on every turn after the first and capture the server-issued id from the response. Legacy 'history' field still sent for one release. - ChatResponse Zod schema gains optional conversationId so TypeScript consumers see the field on res.data. - Web chat.ts now imports types via subpath (@orbit/shared/types/chat) to bypass a tsconfig path-mapping quirk in worktrees that resolves the bare '@orbit/shared' import to the main repo's symlink instead of the worktree copy. - The TODO(task3-p1) marker on chat.ts is removed; the doc comment is updated to describe the new conversationId flow. - Mobile + web chat-store clearMessages also resets conversationId so 'New chat' starts a brand-new server-side conversation. Co-Authored-By: Claude Opus 4.6 (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.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 8ef07deb26
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| cy="18" | ||
| r="15" | ||
| fill="none" | ||
| stroke={isCompleted || progressRatio === 1 ? colors.primary : primaryRgba(0.7)} |
There was a problem hiding this comment.
Use active theme color for mobile avatar arc
The incomplete progress arc uses primaryRgba(0.7), which defaults to a hardcoded purple instead of the currently selected theme color. On non-purple schemes, the tile background/badge follow colors.primary but the arc (and related glow path in this component) stays purple, causing inconsistent accent colors in the habit card UI. Derive the tint from colors.primary (or pass its RGB into primaryRgba) so arc/glow stay in sync with the active scheme.
Useful? React with 👍 / 👎.
Adds editorial depth to the habit card v4: a subtle vertical gradient, 1px top-edge highlight, and hover lift with a hint of accent-tinted shadow. Replaces the muted "Atrasado" chip with a colored status label (warm coral) plus a 6px breathing pulse dot — urgent but not alarming — and matching variants for Today and Completed. Refines the type chip with small-caps + tighter tracking so the title holds focus, and adds a hover-only accent shadow on the avatar tile with a gradient-feel ring. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Mirrors the web card polish: a 1px top-edge highlight overlay, a warm coral for overdue (vs. hot alarm red), a more restrained 0.985 press-in scale, richer shadows, and slightly stronger dark-mode hover borders. The meta row now renders Today / Overdue / Completed as a colored label with a 6px dot instead of a destructive chip, and the type chip reads slightly de-emphasized so the title holds focus. Completed state also desaturates the emoji/initial on the tile to match web parity. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Adds a native <dialog open> reset in globals.css so the picker's panel properly participates in the backdrop's flex centering (was pushed to one side by the UA margin: auto left/right). Introduces a Notion-style active-emoji name bar in the footer, driven by frimousse's useActiveEmoji so it tracks hover and keyboard focus with a colon-style shortcode hint. Refines the search input with a leading magnifier + soft focus ring, gives the category headers tighter tracking, adds a fading top scroll shadow on the grid, and the "no results" empty state now uses a subtle glyph illustration with a helper hint. Adds en and pt-BR keys for hoverHint and noResultsHint with proper diacritics. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Adds a preview / confirm flow in rn-emoji-keyboard's customButtons slot so mobile users see the emoji's name bar react to their taps before committing — the RN platform has no hover event, so this is the closest analogue to the web's Notion-style hover name bar. The name bar shows the currently tapped emoji's name (or the saved icon as a fallback), with explicit Confirm and Clear actions. Threads currentIcon and onClear through habit-icon-field so the picker can preview existing icons and clear them from inside the sheet. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The Avatar+Arc redesign layered a card-wide <button> at z-0 beneath
title/meta content at z-10, which meant clicks on the actual text never
reached the click handler. Effect on users:
- In select mode, tapping the habit title/body did nothing; only the
avatar tile toggled selection.
- Outside select mode, tapping the habit title/body did not open the
detail modal.
Fix: move the click handler onto the card container itself (role="button"
+ tabIndex for keyboard parity), and let interactive children
(expand toggle, avatar tile, kebab menu) stopPropagation on their own
click handlers. Belt-and-braces: a data-card-click-ignore attribute on
those wrappers makes the card-level handler bail out via closest(), so
a descendant forgetting stopPropagation (e.g. portal) still won't
double-fire.
The SelectionCircle is now presentational only — the card-level click
owns the single source of truth for selection toggling, and its
pointer-events-none wrapper guarantees the click bubbles up from the
avatar slot.
Regression guards added:
- Tapping the title text opens the detail modal outside select mode
- Tapping the title text toggles selection in select mode
- Avatar tile stopsPropagation so detail does not double-open
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The search field used the shared `form-input` utility which bakes in `px-4`. Combined with `pl-9`, the magnifier glyph sat on top of the placeholder/value text depending on CSS output ordering. Replace `form-input` with explicit padding + border + focus ring on the input itself, and bump the left padding to `pl-10` (2.5 rem) so the icon (left-3 + 16 px wide) always has an 8 px gap to the text. The wrapper stays `relative` with the absolutely-positioned icon vertically centered via top-1/2 + -translate-y-1/2. Mobile uses a third-party emoji keyboard (`rn-emoji-keyboard`) with its own themed search bar, so no mobile counterpart is needed. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The habit form's outer rhythm is space-y-6 / gap 24, but the Frequency
section had its cards, flexible-hint (`<p class="-mt-1">`), quantity +
unit row, and active-days picker all as separate siblings of the form.
That meant the 24 px gap lived BETWEEN each frequency piece too, and
the flexible hint's -1 rem pulled the next block upward — so Dates &
Times read as glued to whatever frequency block was visible above it.
Wrap the Frequency cards + all of their type-adaptive sub-fields in
one container with a tighter internal rhythm:
- Web: `<div className="space-y-3">` around the group. Removed the
`-mt-1` from the flexible hint.
- Mobile: a new `frequencyGroup` style with `gap: 12`, and removed
the `marginTop: -12` from the flexibleHint.
The outer container's 24 px gap now lands cleanly between the
Frequency group and the Dates & Times row on every state
(one-time / recurring / flexible / general).
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Today-view cards already imply due-today context; the explicit label was noise. Overdue and completed statuses still surface. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The avatar progress arc was rendered for every completed/logged habit, including simple daily ones, drawing a grey track ring and a stroke-cap dot even when progress was zero. The solid-primary completed tile plus the corner check badge already communicate completion; a surrounding ring adds noise without information. Changes: - shouldShowHabitProgressArc no longer returns true just because a habit is completed or logged. Arc is reserved for habits where progress is a meaningful number (parents-with-children, flexible habits). - Removed the grey track circle from both platforms. The arc now renders only when there is an actual filled portion to show. - Guard against rendering the stroke at all when progressRatio is 0 so strokeLinecap="round" can't leak a tiny dot artifact. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Avatar tile's interactive button now explicitly bg-transparent with border-0/p-0 and rounded-[14px] matching the inner tile radius. Some browsers (notably Windows) default <button> to a buttonface background that showed as a faint square halo around the rounded tile. - Quantity+Unit and DueDate+DueTime rows unified to div+label on both sides. The fieldset/legend on one side had different baseline behavior than the div/label on the other, producing a subtle vertical misalignment inside the 2-col grid. - Added items-start to both rows so fields align to their tops regardless of future content height differences. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Touch taps were leaving a sticky hover state that rendered a visible 1px outline ring around the avatar tile and a translated+shadowed card surface. That made the Meditate tile look like it had a squared frame behind it after a tap. Moved the hover rules under @media (hover: hover) and (pointer: fine) so only real pointers trigger them, and dropped the explicit 0 0 0 1px outline from the avatar hover shadow (the soft drop shadow alone gives the lift without a visible ring). Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The 1 px ring-primary/25 applied to every tile rendered as a visible rounded frame around the emoji, more noticeable around colorful emojis than around the monochrome initial fallback. The inner-highlight radial gradient and the arc drop-shadow filter were stacking additional subtle outlines on top. Kept only meaningful rings now: accent ring on the completed state and destructive ring on overdue. Everything else is a flat colored tile. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The wrapper was 56 px to reserve room for the 2 px SVG arc to draw around the 52 px tile. On habits where no arc renders (simple recurring, or parents with no children logged), those 4 px of extra space picked up the native <button> rendering (Windows/Chromium draws an inset background + border on buttons even with bg-transparent), showing as a darker squared halo around the tile. Now the wrapper collapses to exactly 52 px when no arc is drawn, and adds appearance-none + background: none + m-0 to fully neutralize the native button. Full 56 px is only used while the arc is visible. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Reverts the stripdown of ring and inner-highlight (user confirmed those weren't the cause and the flat tile looked worse). Adds overflow-hidden on the inner tile so any visual bleed inside the tile (emoji glyph bounds, anti-aliasing artifacts, native rendering) is clipped to the rounded-14px shape. Combined with the earlier wrapper-size-collapse, nothing can render a sharp-cornered square behind the rounded tile. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…tile The Avatar+Arc redesign merged the log action with the emoji identity into one tile. Separate them back so each card reads as: [status-bar][chevron?][LOG button][EMOJI tile?][title + meta][kebab] - Add HabitLogButton: interactive 52 px square, owns completion, progress arc for parent-with-children + flexible habits, center "x/n" label, overdue ring, bad-habit variant, pulse / glow animations, and the tap-to-log / force-log-parent / unlog / toggle-selection logic. - Trim HabitAvatarTile down to a purely decorative 52 px emoji tile (no onClick, arc, centerLabel, or state overrides). The card omits the tile entirely when the habit has no icon so horizontal space is reclaimed for title + meta. - Update habit-card.tsx to render the pair with `gap-2` between the two tiles. Emoji tile sits outside `data-card-click-ignore` so taps on it fall through to the card handler (opens detail / toggles selection). - Update tests to cover: emoji-only when icon is set, no tile when icon is null, and decorative emoji tile is non-interactive. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…ji tile Mirror the web split so both platforms render the classic layout: [status-bar][chevron?][LOG button][EMOJI tile?][title + meta][kebab] - Add HabitLogButton (mobile): 52 px TouchableOpacity wrapping the SVG progress arc + rounded-square surface, owns tap-to-log / force-log / unlog behavior plus pulse + glow animations. Surface tokens match the web button (primary/14 → solid primary on completion, coral ring when overdue, destructive tint for bad habits). - Trim HabitAvatarTile down to a decorative 52 px emoji tile (no onPress, arc, centerLabel). The card skips rendering it when the habit has no icon to save horizontal space on the row. - Wire the pair inside habit-card.tsx with an 8 px gap; kept selection box behavior unchanged. - Update tests to cover: emoji-only when icon is set, no tile when icon is null. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Two visual refinements requested on the new split-tile layout: - Log button no longer renders a tiny centered dot as an idle "tap to log" hint. It now shows either the Check, the x/n counter, or nothing. The dot was reading as an unintended artifact. - Emoji avatar is no longer wrapped in its own tinted rounded-square surface. The log button owns the "action" square; the emoji is the habit's identity, so it renders as a bare 26 px glyph floating in its slot. No background, no ring, no inner highlight. Smaller than the previous 22 px inside a 52 px tile because without a container the emoji reads at its natural character size — so we bumped the glyph itself up slightly while removing the visual weight of the box. - Card already omits the slot entirely when the habit has no icon; the fallback-initial-letter branch is therefore dropped from the tile. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Combined Changes
Bundles three worktree branches' frontend work (web + mobile + shared):
frimousseweb /rn-emoji-keyboardmobile), full habit-card rewrite with "Avatar + Arc" design (web 1218→446 LOC, mobile 1524→568 LOC), shared emoji utils + habit-progress utils, consistent spacing pass on habit form modalmaxAge1d→7d, DOMPurify on chat rendering, separatehabitKeys.fullDetail(id), goal deadline + reminder minutes + due-time validators, mobilegetItemLayout, single-flight refresh-token mutex, verification-code paste error, mobile parity ports (bulk-action-bar, controls-menu, today-filters, calendar-grid, calendar-day-detail + 6 hooks), web tour-replay-modal back-port, server-authoritativeconversationIdwiringSkipped
5fa7a69(mobile habit-card/habit-list split) — fully obsoleted by task1's habit-card rewrite (568 LOC is cleaner than task3's 868 LOC split). TwoTODO(task3-p1)markers remain inapps/mobile/components/habit-list.tsx— one accurately describes the un-split list (2100+ LOC), one is stale.Conflicts Resolved
packages/shared/src/__tests__/validation.test.ts— additive merge (icon + due-time + reminder tests coexist)Cross-Platform Parity
Every behavioral / validation change landed on both web and mobile. Shared package holds all Zod schemas, query keys, i18n, utilities.
Test Plan
npm install(new deps:frimousse,rn-emoji-keyboard)npx turbo run type-check— should pass cleanlynpx vitest run packages/shared— 835+ tests expectednpx vitest run apps/mobile/__tests__/components/habits/Paired Backend PR
thomasluizon/orbit-api#150
🤖 Generated with Claude Code