diff --git a/apps/web/app/(app)/ai-settings/page.tsx b/apps/web/app/(app)/ai-settings/page.tsx index 21bf98d3e..9f3310308 100644 --- a/apps/web/app/(app)/ai-settings/page.tsx +++ b/apps/web/app/(app)/ai-settings/page.tsx @@ -356,10 +356,18 @@ export default function AiSettingsPage() { {pagedFacts.map((fact) => (
toggleFactSelection(fact.id) : undefined} + onKeyDown={selectMode ? (e) => { + if (e.key === 'Enter' || e.key === ' ') { + e.preventDefault() + toggleFactSelection(fact.id) + } + } : undefined} > {selectMode && ( +
, document.body ) } diff --git a/apps/web/components/gamification/goal-completed-celebration.tsx b/apps/web/components/gamification/goal-completed-celebration.tsx index d39f0a7cd..b78d205df 100644 --- a/apps/web/components/gamification/goal-completed-celebration.tsx +++ b/apps/web/components/gamification/goal-completed-celebration.tsx @@ -50,14 +50,17 @@ export function GoalCompletedCelebration() { if (!mounted || !shouldRender) return null return createPortal( -
+
+
, document.body ) diff --git a/apps/web/components/gamification/streak-celebration.tsx b/apps/web/components/gamification/streak-celebration.tsx index a6df370e9..27163a596 100644 --- a/apps/web/components/gamification/streak-celebration.tsx +++ b/apps/web/components/gamification/streak-celebration.tsx @@ -65,15 +65,17 @@ export function StreakCelebration() { if (!mounted || !shouldRender) return null return createPortal( - +
+ +
, document.body ) } diff --git a/apps/web/components/gamification/streak-freeze-celebration.tsx b/apps/web/components/gamification/streak-freeze-celebration.tsx index f9af0e74f..ef62b8e37 100644 --- a/apps/web/components/gamification/streak-freeze-celebration.tsx +++ b/apps/web/components/gamification/streak-freeze-celebration.tsx @@ -49,14 +49,17 @@ export const StreakFreezeCelebration = forwardRef if (!mounted || !shouldRender) return null return createPortal( -
+
+
, document.body ) diff --git a/apps/web/components/gamification/welcome-back-toast.tsx b/apps/web/components/gamification/welcome-back-toast.tsx index 0a87f64e9..af21868af 100644 --- a/apps/web/components/gamification/welcome-back-toast.tsx +++ b/apps/web/components/gamification/welcome-back-toast.tsx @@ -74,8 +74,10 @@ export function WelcomeBackToast() { if (!mounted || !shouldRender) return null return createPortal( -
-
, + , document.body ) } diff --git a/apps/web/components/goals/goal-list.tsx b/apps/web/components/goals/goal-list.tsx index 04bb556ba..30ed7bd5c 100644 --- a/apps/web/components/goals/goal-list.tsx +++ b/apps/web/components/goals/goal-list.tsx @@ -203,6 +203,8 @@ export function GoalList({ goals }: GoalListProps) { {goals.map((goal, index) => (
handleDragStart(index)} diff --git a/apps/web/components/habits/checklist-templates.tsx b/apps/web/components/habits/checklist-templates.tsx index 38c2c9879..97280cb71 100644 --- a/apps/web/components/habits/checklist-templates.tsx +++ b/apps/web/components/habits/checklist-templates.tsx @@ -107,22 +107,26 @@ export function ChecklistTemplates({ items, onLoad }: ChecklistTemplatesProps) { {t('habits.form.templates')}: {templates.map((tmpl) => ( - + + + ))}
diff --git a/apps/web/components/habits/habit-card.tsx b/apps/web/components/habits/habit-card.tsx index bee3d91d2..03e49a951 100644 --- a/apps/web/components/habits/habit-card.tsx +++ b/apps/web/components/habits/habit-card.tsx @@ -65,6 +65,166 @@ function truncate(text: string, max = 20): string { return text.length > max ? text.slice(0, max) + '...' : text } +// --------------------------------------------------------------------------- +// Sub-components (extracted to reduce cognitive complexity - S3776) +// --------------------------------------------------------------------------- + +function CompletionSparks({ positions }: { positions: Array<{ x: string; y: string }> }) { + return ( + <> + {positions.map((pos, i) => ( + + ))} + + ) +} + +function BadHabitBadge() { + const t = useTranslations() + return ( + + {t('habits.badHabit')} + + ) +} + +function TagBadge({ tag, searchQuery }: { tag: { id: string; name: string; color: string }; searchQuery: string }) { + return ( + + + + ) +} + +function StreakBadgeInline({ streak }: { streak: number | null | undefined }) { + if (streak == null || streak < 2) return null + return ( + + + {streak} + + ) +} + +function ChecklistBadge({ items, checkedCount, hasBorder }: { + items: Array<{ label: string; isChecked: boolean }> | undefined | null + checkedCount: number + hasBorder?: boolean +}) { + if (!items || items.length === 0) return null + return ( + + + {checkedCount}/{items.length} + + ) +} + +interface TopLevelBadgesProps { + habit: NormalizedHabit + frequencyLabel: string + flexibleProgressLabel: string | null + statusBadge: { text: string; color: string; bg: string } | null + checkedCount: number + searchQuery: string + matchBadges: Array<{ label: string }> + displayTime: (time: string) => string +} + +function TopLevelBadges({ + habit, frequencyLabel, flexibleProgressLabel, statusBadge, + checkedCount, searchQuery, matchBadges, displayTime, +}: TopLevelBadgesProps) { + return ( +
+ + {frequencyLabel} + + {flexibleProgressLabel && ( + + {flexibleProgressLabel} + + )} + {habit.dueTime && ( + + {displayTime(habit.dueTime)} + {habit.dueEndTime ? ` - ${displayTime(habit.dueEndTime)}` : ''} + + )} + {statusBadge && ( + + {statusBadge.text} + + )} + {habit.isBadHabit && } + {habit.tags?.map((tag) => ( + + ))} + {(habit.linkedGoals ?? []).map((goal) => ( + + {goal.title} + + ))} + + + {matchBadges.map((badge, i) => ( + + {badge.label} + + ))} +
+ ) +} + +function ChildBadHabitBadges({ habit, searchQuery, checkedCount }: { + habit: NormalizedHabit; searchQuery: string; checkedCount: number +}) { + return ( +
+ + {habit.tags?.map((tag) => ( + + ))} + + +
+ ) +} + +function ChildDefaultBadges({ habit, frequencyLabel, statusBadge, searchQuery, checkedCount }: { + habit: NormalizedHabit; frequencyLabel: string + statusBadge: { text: string; color: string; bg: string } | null + searchQuery: string; checkedCount: number +}) { + return ( +
+ + {frequencyLabel} + + {statusBadge && ( + + {statusBadge.text} + + )} + {habit.tags?.map((tag) => ( + + ))} + + +
+ ) +} + // --------------------------------------------------------------------------- // Component // --------------------------------------------------------------------------- @@ -298,15 +458,15 @@ export function HabitCard({ document.addEventListener('pointerdown', handlePointerDown) document.addEventListener('keydown', handleKeydown) document.addEventListener('scroll', handleScroll, true) - window.addEventListener('resize', handleViewportChange) - window.addEventListener('orientationchange', handleViewportChange) + globalThis.addEventListener('resize', handleViewportChange) + globalThis.addEventListener('orientationchange', handleViewportChange) return () => { document.removeEventListener('pointerdown', handlePointerDown) document.removeEventListener('keydown', handleKeydown) document.removeEventListener('scroll', handleScroll, true) - window.removeEventListener('resize', handleViewportChange) - window.removeEventListener('orientationchange', handleViewportChange) + globalThis.removeEventListener('resize', handleViewportChange) + globalThis.removeEventListener('orientationchange', handleViewportChange) } }, [showActionsMenu]) @@ -337,6 +497,7 @@ export function HabitCard({ <>
{ @@ -742,6 +904,7 @@ export function HabitCard({ createPortal(
{/* Backdrop to close */} -
setIsOpen(false)} /> +
setIsOpen(false)} /> )} diff --git a/apps/web/components/ui/app-date-picker.tsx b/apps/web/components/ui/app-date-picker.tsx index 44f10b48e..f73b3c001 100644 --- a/apps/web/components/ui/app-date-picker.tsx +++ b/apps/web/components/ui/app-date-picker.tsx @@ -125,6 +125,7 @@ export function AppDatePicker({ <> {/* Backdrop to close */}
setIsOpen(false)} /> diff --git a/apps/web/components/ui/app-overlay.tsx b/apps/web/components/ui/app-overlay.tsx index 8be8967b7..b6e6cf8c2 100644 --- a/apps/web/components/ui/app-overlay.tsx +++ b/apps/web/components/ui/app-overlay.tsx @@ -92,7 +92,7 @@ export function AppOverlay({ }, [open, mounted]) // eslint-disable-line react-hooks/exhaustive-deps const lockBodyScroll = useCallback(() => { - savedScrollY.current = window.scrollY + savedScrollY.current = globalThis.scrollY document.body.style.position = 'fixed' document.body.style.top = `-${savedScrollY.current}px` document.body.style.width = '100%' @@ -108,7 +108,7 @@ export function AppOverlay({ document.body.style.overflow = '' document.documentElement.style.overflow = '' document.documentElement.style.overscrollBehavior = '' - window.scrollTo(0, savedScrollY.current) + globalThis.scrollTo(0, savedScrollY.current) }, []) // Focus trap @@ -205,12 +205,14 @@ export function AppOverlay({ const overlay = (
{/* Backdrop */}