Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion apps/web/__tests__/components/ui/app-bar.test.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { describe, expect, it } from 'vitest'
import { resolveAppBarRightActionLabel } from '@/components/ui/app-bar'
import { resolveAppBarRightActionLabel } from '@/components/ui/app-bar-right-action'

describe('resolveAppBarRightActionLabel', () => {
const t = (key: string) => key
Expand Down
30 changes: 19 additions & 11 deletions apps/web/components/habits/bulk-action-bar-v2.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,11 +8,25 @@ import {
X,
type LucideIcon,
} from 'lucide-react'
// react-doctor-disable-next-line use-lazy-motion -- LazyMotion migration is app-wide (needs a shared provider + converting every motion.* across components/**); a partial per-file swap yields no bundle benefit and risks unprovided m https://github.com/thomasluizon/orbit-ui-mobile/issues/243
import { motion, useReducedMotion } from 'motion/react'
import { useTranslations } from 'next-intl'
import { resolveMotionPreset } from '@orbit/shared/theme'
import { useIsClient } from '@/hooks/use-is-client'
import { plural } from '@/lib/plural'

const SELECT_ALL_BUTTON_STYLE = {
fontFamily: 'var(--font-sans)',
fontSize: 12,
fontWeight: 500,
padding: '14px 10px',
margin: '-10px -4px',
textDecoration: 'underline',
textUnderlineOffset: 3,
textDecorationColor: 'var(--hairline-strong)',
textDecorationThickness: 1,
} as const

/** Floating bulk action toolbar on an elevated solid sheet surface. */
export interface BulkActionBarV2Props {
selectedCount: number
Expand Down Expand Up @@ -72,6 +86,9 @@ export function BulkActionBarV2({
const prefersReducedMotion = useReducedMotion()
const motionPreset = resolveMotionPreset('selection', Boolean(prefersReducedMotion))
const nothingSelected = selectedCount === 0
const mounted = useIsClient()

if (!mounted) return null

return createPortal(
<motion.div
Expand Down Expand Up @@ -127,17 +144,7 @@ export function BulkActionBarV2({
type="button"
onClick={allSelected ? onDeselectAll : onSelectAll}
className="appearance-none border-0 bg-transparent cursor-pointer text-[var(--fg-3)] hover:text-[var(--fg-1)] active:scale-[0.96] transition-[color,transform] duration-[var(--dur-fast)] ease-[var(--ease-standard)]"
style={{
fontFamily: 'var(--font-sans)',
fontSize: 12,
fontWeight: 500,
padding: '14px 10px',
margin: '-10px -4px',
textDecoration: 'underline',
textUnderlineOffset: 3,
textDecorationColor: 'var(--hairline-strong)',
textDecorationThickness: 1,
}}
style={SELECT_ALL_BUTTON_STYLE}
>
{allSelected ? t('common.deselectAll') : t('common.selectAll')}
</button>
Expand Down Expand Up @@ -173,6 +180,7 @@ export function BulkActionBarV2({
/>
</div>
</motion.div>,
// react-doctor-disable-next-line no-unguarded-browser-global-in-render-or-hook-init -- unreachable during SSR: the `if (!mounted) return null` above (useIsClient) returns before this createPortal on the server and first hydration render https://github.com/thomasluizon/orbit-ui-mobile/issues/243
document.querySelector('main') ?? document.body,
)
}
22 changes: 12 additions & 10 deletions apps/web/components/habits/controls-menu.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,17 @@ export function ControlsMenu({
)
}

const MENU_ROW_STYLE = {
padding: '10px 12px',
gap: 12,
fontFamily: 'var(--font-sans)',
fontSize: 14,
fontWeight: 500,
color: 'var(--fg-1)',
textAlign: 'left',
borderRadius: 8,
} as const

interface MenuRowProps {
icon: React.ReactNode
label: string
Expand All @@ -114,16 +125,7 @@ function MenuRow({
disabled={disabled}
onClick={onClick}
className="w-full appearance-none border-0 bg-transparent cursor-pointer flex items-center transition-colors hover:bg-[var(--bg-sunk)] disabled:opacity-50 disabled:hover:bg-transparent disabled:cursor-not-allowed"
style={{
padding: '10px 12px',
gap: 12,
fontFamily: 'var(--font-sans)',
fontSize: 14,
fontWeight: 500,
color: 'var(--fg-1)',
textAlign: 'left',
borderRadius: 8,
}}
style={MENU_ROW_STYLE}
>
<span className="shrink-0 inline-flex" style={{ color: 'var(--fg-2)' }}>
{icon}
Expand Down
5 changes: 5 additions & 0 deletions apps/web/components/habits/create-habit-modal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ interface CreateHabitModalProps {
parentHabit?: NormalizedHabit | null
}

// react-doctor-disable-next-line no-giant-component -- create/sub-habit modal orchestrating the shared form, tag/goal/sub-habit/reminder state, AI-suggest, and dismiss-guard as one flow; extraction deferred to avoid regression without visual QA https://github.com/thomasluizon/orbit-ui-mobile/issues/243
export function CreateHabitModal({
open,
onOpenChange,
Expand Down Expand Up @@ -119,7 +120,9 @@ export function CreateHabitModal({
useEffect(() => {
if (!open || !isSubHabitMode || !profile || profile.hasProAccess) return

// react-doctor-disable-next-line no-prop-callback-in-effect -- pro-access gate, not a render-sync of local state: closes the modal only when a non-pro user opens sub-habit mode, then redirects to /upgrade https://github.com/thomasluizon/orbit-ui-mobile/issues/243
onOpenChange(false)
// react-doctor-disable-next-line nextjs-no-client-side-redirect -- gate depends on client-fetched profile.hasProAccess (useProfile); there is no server-side signal to redirect on https://github.com/thomasluizon/orbit-ui-mobile/issues/243
router.push('/upgrade')
}, [isSubHabitMode, onOpenChange, open, profile, router])

Expand Down Expand Up @@ -239,6 +242,7 @@ export function CreateHabitModal({
)
}
},
// react-doctor-disable-next-line exhaustive-deps -- hasProAccess is derived from profile.hasProAccess every render and already listed; the callback keys off the resolved boolean, not the raw profile member https://github.com/thomasluizon/orbit-ui-mobile/issues/243
[createHabit, createSubHabit, formHelpers, hasProAccess, isSubHabitMode, onOpenChange, parentHabit, reminderTimes, router, selectedGoalIds, showError, subHabits, tags, translate],
)

Expand Down Expand Up @@ -287,6 +291,7 @@ export function CreateHabitModal({
)
}
},
// react-doctor-disable-next-line exhaustive-deps -- hasProAccess is derived from profile.hasProAccess every render and already listed; the callback keys off the resolved boolean, not the raw profile member https://github.com/thomasluizon/orbit-ui-mobile/issues/243
[formHelpers, hasProAccess, locale, showError, showInfo, showSuccess, suggestion, t],
)

Expand Down
3 changes: 2 additions & 1 deletion apps/web/components/habits/goal-linking-field.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ export function GoalLinkingField({
})

const activeGoals = goals?.filter((g) => g.status === 'Active') ?? []
const selectedGoalIdSet = new Set(selectedGoalIds)

return (
<div className="space-y-2">
Expand All @@ -44,7 +45,7 @@ export function GoalLinkingField({
{activeGoals.length > 0 ? (
<div className="flex flex-wrap" style={{ gap: 8 }}>
{activeGoals.map((goal) => {
const isSelected = selectedGoalIds.includes(goal.id)
const isSelected = selectedGoalIdSet.has(goal.id)
const isDimmed = !isSelected && atGoalLimit
return (
<button
Expand Down
3 changes: 3 additions & 0 deletions apps/web/components/habits/habit-calendar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -48,11 +48,13 @@ export function HabitCalendar({ habitId, logs: externalLogs }: Readonly<HabitCal
key,
label: t(`dates.daysShort.${key}`).charAt(0),
})),
// react-doctor-disable-next-line exhaustive-deps -- weekStartsOn is derived from profile.weekStartDay every render and already listed; no staleness possible https://github.com/thomasluizon/orbit-ui-mobile/issues/243
[t, weekStartsOn],
)

const calendarDays = useMemo(
() => buildHabitCalendarDayCells(currentMonth, weekStartsOn, logDates),
// react-doctor-disable-next-line exhaustive-deps -- weekStartsOn is derived from profile.weekStartDay every render and already listed; no staleness possible https://github.com/thomasluizon/orbit-ui-mobile/issues/243
[currentMonth, logDates, weekStartsOn],
)

Expand Down Expand Up @@ -136,6 +138,7 @@ export function HabitCalendar({ habitId, logs: externalLogs }: Readonly<HabitCal
className="text-center uppercase py-1"
style={{
fontFamily: 'var(--font-mono)',
// react-doctor-disable-next-line no-tiny-text -- intentional single-letter weekday header caption (mono meta scale per DESIGN.md), not body text https://github.com/thomasluizon/orbit-ui-mobile/issues/243
fontSize: 11,
fontWeight: 500,
letterSpacing: '0.04em',
Expand Down
1 change: 1 addition & 0 deletions apps/web/components/habits/habit-checklist.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -204,6 +204,7 @@ export function HabitChecklist({
>
{items.map((item, index) => (
<InteractiveChecklistItem
// react-doctor-disable-next-line no-array-index-as-key -- fixed-order display of the habit's checklist items in this non-editable interactive path (no reorder/filter); ChecklistItem has no id and text may repeat, so the index disambiguates the composite key https://github.com/thomasluizon/orbit-ui-mobile/issues/243
key={`${item.text}-${index}`}
item={item}
index={index}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ export function HabitAskAstraButton({
<span
style={{
fontFamily: 'var(--font-mono)',
// react-doctor-disable-next-line no-tiny-text -- intentional mono eyebrow label above the Ask-Astra prompt (meta scale per DESIGN.md), not body text https://github.com/thomasluizon/orbit-ui-mobile/issues/243
fontSize: 10.5,
fontWeight: 500,
letterSpacing: '0.06em',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ export function HabitDetailReminders({
)}
{habit.scheduledReminders.map((sr, idx) => (
<SettingsRow
// react-doctor-disable-next-line no-array-index-as-key -- read-only display of the habit's fixed-order scheduled reminders (no reorder/filter); ScheduledReminderTime has no id, so the index disambiguates the composite when/time key https://github.com/thomasluizon/orbit-ui-mobile/issues/243
key={`${sr.when}-${sr.time}-${idx}`}
label={
sr.when === 'day_before'
Expand Down
12 changes: 8 additions & 4 deletions apps/web/components/habits/habit-form-fields.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ function shouldShowScheduledReminders(
return !isGeneral && (!dueTime || hasScheduledReminders)
}

export function resolveReminderLabel(
function resolveReminderLabel(
minutes: number,
t: ReturnType<typeof useTranslations>,
): string {
Expand Down Expand Up @@ -93,6 +93,7 @@ interface HabitFormFieldsProps {
children?: ReactNode
}

// react-doctor-disable-next-line no-giant-component -- the full habit form (title, emoji, color, frequency cards, days, dates, tags, goals, reminders, checklist, slip-alerts) rendered as one cohesive surface; extraction deferred to avoid regression without visual QA https://github.com/thomasluizon/orbit-ui-mobile/issues/243
export function HabitFormFields({
formHelpers,
titleInputRef,
Expand Down Expand Up @@ -221,6 +222,9 @@ export function HabitFormFields({
}
}

const watchedDaySet = new Set(watchedDays)
const selectedTagIdSet = new Set(tags.selectedTagIds)

return (
<div className="space-y-7">
<div className="space-y-7">
Expand Down Expand Up @@ -351,7 +355,7 @@ export function HabitFormFields({
options={daysList.map((day) => ({
key: day.value,
label: day.label,
active: watchedDays.includes(day.value),
active: watchedDaySet.has(day.value),
onClick: () => toggleDay(day.value),
}))}
/>
Expand Down Expand Up @@ -399,8 +403,8 @@ export function HabitFormFields({
<HabitTagChip
key={tag.id}
tag={tag}
selected={tags.selectedTagIds.includes(tag.id)}
atLimit={!tags.selectedTagIds.includes(tag.id) && tags.atTagLimit}
selected={selectedTagIdSet.has(tag.id)}
atLimit={!selectedTagIdSet.has(tag.id) && tags.atTagLimit}
animationClassName={justToggledTagId === tag.id ? 'animate-tag-pop' : ''}
disabled={isTagMutationPending}
onToggle={() => handleTagToggle(tag.id)}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { useEffect, useMemo, useRef } from 'react'
import { useEffect, useRef } from 'react'
import { ChevronLeft, ChevronRight, CalendarCheck, Repeat, Shuffle, Infinity } from 'lucide-react'
import { useTranslations } from 'next-intl'

Expand Down Expand Up @@ -39,12 +39,12 @@ export function FrequencyTypeCards({
return 'recurring'
})()

const frequencyHandlers: Record<string, () => void> = useMemo(() => ({
const frequencyHandlers: Record<string, () => void> = {
'one-time': onSetOneTime,
recurring: onSetRecurring,
flexible: onSetFlexible,
general: onSetGeneral,
}), [onSetOneTime, onSetRecurring, onSetFlexible, onSetGeneral])
}

const frequencyTrackRef = useRef<HTMLDivElement>(null)
const hasPositionedFrequencyRef = useRef(false)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,7 @@ export function HabitEmojiSelector({ selectedEmoji, onSelect }: Readonly<HabitEm

<div className="space-y-3 p-4">
<input
// react-doctor-disable-next-line no-autofocus -- emoji search field inside a user-invoked picker overlay; the user explicitly opened the picker to search, so focusing the search box on open is the intended interaction https://github.com/thomasluizon/orbit-ui-mobile/issues/243
autoFocus
value={query}
onChange={(event) => setQuery(event.target.value)}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ export function useExpandAdvancedSignal(
const [previousSignal, setPreviousSignal] = useState(expandAdvancedSignal)
if (expandAdvancedSignal !== previousSignal) {
setPreviousSignal(expandAdvancedSignal)
// react-doctor-disable-next-line no-prop-callback-in-render -- deliberate adjusting-state-during-render sync (React "storing information from previous renders" pattern): gated by the previousSignal comparison so onExpand fires exactly once per signal bump https://github.com/thomasluizon/orbit-ui-mobile/issues/243
if (expandAdvancedSignal > 0) onExpand()
}
}
27 changes: 18 additions & 9 deletions apps/web/components/habits/habit-list.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -27,15 +27,15 @@ import { CreateHabitModal } from './create-habit-modal'
import { EditHabitModal } from './edit-habit-modal'
import { RescheduleSheet } from './reschedule-sheet'
import {
getEmptyHabitsMessage,
HabitListEmptyState,
HabitListSkeleton,
} from './habit-list/empty-state'
import { getEmptyHabitsMessage } from './habit-list/empty-state-message'
import {
formatDateGroupLabel,
HabitListDateGroupSection,
type HabitListDateGroup,
} from './habit-list/date-group-section'
import { formatDateGroupLabel } from './habit-list/date-group-label'
import { HabitListConfirmDialogs } from './habit-list/confirm-dialogs'
import { HabitListDrillContent } from './habit-list/drill-content'
import { MoveParentOverlay, type MoveParentOption } from './habit-list/move-parent-overlay'
Expand Down Expand Up @@ -110,6 +110,7 @@ export interface HabitListHandle {

const TOUR_FEATURED_HABIT_ID = 'tour-habit-2'

// react-doctor-disable-next-line no-giant-component -- top-level habit-list surface owning query data, visibility, drill navigation, collapse state, and the full confirm-dialog cluster as one imperative-handle unit; extraction deferred to avoid regression without visual QA https://github.com/thomasluizon/orbit-ui-mobile/issues/243
export const HabitList = forwardRef<HabitListHandle, HabitListProps>(function HabitList({
view = 'today',
selectedDate,
Expand Down Expand Up @@ -258,6 +259,7 @@ export const HabitList = forwardRef<HabitListHandle, HabitListProps>(function Ha
const allCollapsed = expandableIds.length > 0 && expandableIds.every((id) => collapsedIds.has(id))

useEffect(() => {
// react-doctor-disable-next-line no-pass-data-to-parent, no-pass-live-state-to-parent, no-prop-callback-in-effect -- allCollapsed is derived from both collapsedIds (local) and expandableIds (data-driven); the parent toolbar must reflect it, and no single event handler covers the data-driven changes, so a notify-effect is the correct channel https://github.com/thomasluizon/orbit-ui-mobile/issues/243
onAllCollapsedChange?.(allCollapsed)
}, [allCollapsed, onAllCollapsedChange])

Expand All @@ -283,20 +285,24 @@ export const HabitList = forwardRef<HabitListHandle, HabitListProps>(function Ha
}
if (showCompleted) return topLevelHabits
return topLevelHabits.filter((h) => visibility.hasVisibleContent(h))
// react-doctor-disable-next-line exhaustive-deps -- topLevelHabits is destructured from the query data every render and already listed; the memo keys off the resolved array, not data.topLevelHabits https://github.com/thomasluizon/orbit-ui-mobile/issues/243
}, [topLevelHabits, view, showCompleted, recentlyCompletedIds, visibility])

const allLoadedIds = useMemo(() => {
return collectVisibleHabitTreeIds(habits, getVisibleChildren)
}, [getVisibleChildren, habits])

const isListView = view === 'all' || view === 'general'
promptDataRef.current = {
getChildren,
isListView,
visibility,
habitsById,
selectedDateStr,
}
useEffect(() => {
promptDataRef.current = {
getChildren,
isListView,
visibility,
habitsById,
selectedDateStr,
}
// react-doctor-disable-next-line exhaustive-deps -- getChildren and habitsById are aliased from the query result every render and already listed; the effect only mirrors the current render values into a ref, so no staleness is possible https://github.com/thomasluizon/orbit-ui-mobile/issues/243
}, [promptDataRef, getChildren, isListView, visibility, habitsById, selectedDateStr])

const childrenProgressMap = useMemo(() => {
const map = new Map<string, { done: number; total: number }>()
Expand Down Expand Up @@ -356,6 +362,7 @@ export const HabitList = forwardRef<HabitListHandle, HabitListProps>(function Ha
}

return map
// react-doctor-disable-next-line exhaustive-deps -- getChildren is aliased from habitsQuery.getChildren every render and already listed; the memo keys off the resolved function, not habitsQuery.getChildren https://github.com/thomasluizon/orbit-ui-mobile/issues/243
}, [habitsById, getChildren, isListView, visibility])

const getChildrenProgress = useCallback(
Expand Down Expand Up @@ -914,6 +921,7 @@ const isPostponeAction = useMemo(() => {
<>
<div className="flex items-center" style={{ padding: '4px 20px 10px', gap: 12 }}>
<button
type="button"
aria-label={t('common.goBack')}
className="touch-target shrink-0 appearance-none border-0 bg-transparent cursor-pointer flex items-center justify-center text-[var(--fg-1)] transition-[background-color] duration-[var(--dur-fast)] ease-[var(--ease-standard)] hover:bg-[var(--bg-elev)]"
style={{
Expand Down Expand Up @@ -957,6 +965,7 @@ const isPostponeAction = useMemo(() => {

{drill.drillStack.length > 1 && (
<button
type="button"
className="flex items-center appearance-none border-0 bg-transparent cursor-pointer text-[var(--primary)] hover:text-[var(--primary-pressed)] transition-colors"
style={{
gap: 6,
Expand Down
1 change: 1 addition & 0 deletions apps/web/components/habits/habit-list/confirm-dialogs.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ interface HabitListConfirmDialogsProps {
/** The cluster of habit-list confirmation dialogs (delete / duplicate / skip /
* force-log / auto-log-parent). Driven entirely by open-flag props and handlers
* owned by the parent HabitList. */
// react-doctor-disable-next-line no-many-boolean-props -- confirmation-dialog cluster driven by independent open-flags owned by the parent HabitList; each boolean is a distinct dialog's visibility, not a configuration explosion of one component https://github.com/thomasluizon/orbit-ui-mobile/issues/243
export function HabitListConfirmDialogs({
t,
showDeleteConfirm,
Expand Down
Loading
Loading