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(
-
+
+
{/* Backdrop */}
@@ -99,6 +102,7 @@ export function GoalCompletedCelebration() {
{t('goals.completedCelebrationSubtitle', { name: goalName })}
+
,
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(
-
+
+
{/* Backdrop */}
@@ -95,6 +98,7 @@ export const StreakFreezeCelebration = forwardRef
{t('streakDisplay.freeze.celebrationSubtitle')}
+
,
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) => (
-
- handleLoad(tmpl.id)}>{tmpl.name}
- {
- e.stopPropagation()
- handleDelete(tmpl.id)
- }}
+ handleLoad(tmpl.id)}
+ >
+ {tmpl.name}
+
+ handleDelete(tmpl.id)}
>
-
-
+
+
))}
>
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 = (