diff --git a/apps/mobile/app/(tabs)/calendar/_components/calendar-day-detail.tsx b/apps/mobile/app/(tabs)/calendar/_components/calendar-day-detail.tsx index d05070224..9fe69d07a 100644 --- a/apps/mobile/app/(tabs)/calendar/_components/calendar-day-detail.tsx +++ b/apps/mobile/app/(tabs)/calendar/_components/calendar-day-detail.tsx @@ -58,7 +58,7 @@ export function CalendarDayDetail({ displayTime, t, tokens, -}: CalendarDayDetailProps) { +}: Readonly) { const styles = useMemo(() => createStyles(tokens), [tokens]); const renderEntry = (item: CalendarDayEntry, index: number) => { diff --git a/apps/mobile/app/(tabs)/calendar/_components/calendar-day-entry.tsx b/apps/mobile/app/(tabs)/calendar/_components/calendar-day-entry.tsx index a481d7380..4753964f7 100644 --- a/apps/mobile/app/(tabs)/calendar/_components/calendar-day-entry.tsx +++ b/apps/mobile/app/(tabs)/calendar/_components/calendar-day-entry.tsx @@ -106,7 +106,7 @@ export function CalendarDayEntryRow({ statusAccessibilityLabel, displayTime, isLast, -}: CalendarDayEntryRowProps) { +}: Readonly) { const styles = useMemo(() => createStyles(tokens), [tokens]); return ( diff --git a/apps/mobile/app/(tabs)/calendar/_components/calendar-grid.tsx b/apps/mobile/app/(tabs)/calendar/_components/calendar-grid.tsx index 25fbdcf51..42eda2be3 100644 --- a/apps/mobile/app/(tabs)/calendar/_components/calendar-grid.tsx +++ b/apps/mobile/app/(tabs)/calendar/_components/calendar-grid.tsx @@ -95,7 +95,7 @@ export function CalendarGrid({ language, t, tokens, -}: CalendarGridProps) { +}: Readonly) { const styles = useMemo(() => createStyles(tokens), [tokens]); const grid = ( diff --git a/apps/mobile/app/(tabs)/calendar/_components/calendar-shell.tsx b/apps/mobile/app/(tabs)/calendar/_components/calendar-shell.tsx index 6a17f672b..f454eb481 100644 --- a/apps/mobile/app/(tabs)/calendar/_components/calendar-shell.tsx +++ b/apps/mobile/app/(tabs)/calendar/_components/calendar-shell.tsx @@ -193,7 +193,7 @@ export function CalendarHeader({ onCurrentMonth, onSelectYear, tokens, -}: CalendarHeaderProps) { +}: Readonly) { const styles = useMemo(() => createStyles(tokens), [tokens]); const monthNavRef = useRef(null); useTourTarget("tour-calendar-month-nav", monthNavRef); @@ -292,7 +292,7 @@ export function CalendarWeekNav({ onNextWeek, onCurrentWeek, tokens, -}: CalendarWeekNavProps) { +}: Readonly) { const styles = useMemo(() => createStyles(tokens), [tokens]); return ( @@ -345,7 +345,7 @@ export function CalendarLegend({ partialLabel, missedLabel, tokens, -}: CalendarLegendProps) { +}: Readonly) { const styles = useMemo(() => createStyles(tokens), [tokens]); const legendRef = useRef(null); useTourTarget("tour-calendar-legend", legendRef); diff --git a/apps/mobile/app/login-atoms.tsx b/apps/mobile/app/login-atoms.tsx index 7c8fd2070..4bca77813 100644 --- a/apps/mobile/app/login-atoms.tsx +++ b/apps/mobile/app/login-atoms.tsx @@ -4,7 +4,7 @@ import Svg, { Circle, Path } from 'react-native-svg' const AnimatedSvg = Animated.createAnimatedComponent(Svg) -export function Spinner({ size = 16, color }: { size?: number; color: string }) { +export function Spinner({ size = 16, color }: Readonly<{ size?: number; color: string }>) { const rotation = useMemo(() => new Animated.Value(0), []) useEffect(() => { diff --git a/apps/mobile/components/bottom-sheet-modal.tsx b/apps/mobile/components/bottom-sheet-modal.tsx index 8c9c1b9ea..349260337 100644 --- a/apps/mobile/components/bottom-sheet-modal.tsx +++ b/apps/mobile/components/bottom-sheet-modal.tsx @@ -52,7 +52,7 @@ export function BottomSheetModal({ onAttemptDismiss, contentManagesScroll = false, children, -}: BottomSheetModalProps) { +}: Readonly) { const { currentScheme, currentTheme } = useAppTheme() const { t } = useTranslation() const tokens = useMemo( diff --git a/apps/mobile/components/goal-card.tsx b/apps/mobile/components/goal-card.tsx index 5b2551a86..f67fd2c44 100644 --- a/apps/mobile/components/goal-card.tsx +++ b/apps/mobile/components/goal-card.tsx @@ -18,7 +18,7 @@ interface GoalCardProps { tourTargetId?: string } -export function GoalCard({ goal, onPress, onLongPress, tourTargetId }: GoalCardProps) { +export function GoalCard({ goal, onPress, onLongPress, tourTargetId }: Readonly) { const { t } = useTranslation() const { currentScheme, currentTheme } = useAppTheme() const tokens = useMemo( diff --git a/apps/mobile/components/goals/create-goal-modal.tsx b/apps/mobile/components/goals/create-goal-modal.tsx index 3bf5ecacc..19c5638a6 100644 --- a/apps/mobile/components/goals/create-goal-modal.tsx +++ b/apps/mobile/components/goals/create-goal-modal.tsx @@ -43,7 +43,7 @@ interface CreateGoalRequest { type?: 'Standard' | 'Streak' } -export function CreateGoalModal({ open, onClose }: CreateGoalModalProps) { +export function CreateGoalModal({ open, onClose }: Readonly) { const { t } = useTranslation() const translate = useCallback( (key: string, values?: Record) => t(key, values), diff --git a/apps/mobile/components/goals/edit-goal-modal.tsx b/apps/mobile/components/goals/edit-goal-modal.tsx index 52fff26fb..41d5adacc 100644 --- a/apps/mobile/components/goals/edit-goal-modal.tsx +++ b/apps/mobile/components/goals/edit-goal-modal.tsx @@ -47,7 +47,7 @@ interface UpdateGoalRequest { deadline?: string | null } -export function EditGoalModal({ open, onClose, goal }: EditGoalModalProps) { +export function EditGoalModal({ open, onClose, goal }: Readonly) { const { t } = useTranslation() const translate = useCallback( (key: string, values?: Record) => t(key, values), diff --git a/apps/mobile/components/goals/goal-detail-drawer.tsx b/apps/mobile/components/goals/goal-detail-drawer.tsx index 656333105..362505217 100644 --- a/apps/mobile/components/goals/goal-detail-drawer.tsx +++ b/apps/mobile/components/goals/goal-detail-drawer.tsx @@ -44,7 +44,7 @@ export function GoalDetailDrawer({ open, onClose, goalId, -}: GoalDetailDrawerProps) { +}: Readonly) { const { t, i18n } = useTranslation() const translate = useCallback( (key: string, values?: Record) => t(key, values), diff --git a/apps/mobile/components/habit-list/confirm-dialogs.tsx b/apps/mobile/components/habit-list/confirm-dialogs.tsx index 57e353d1f..7497c0f43 100644 --- a/apps/mobile/components/habit-list/confirm-dialogs.tsx +++ b/apps/mobile/components/habit-list/confirm-dialogs.tsx @@ -59,7 +59,7 @@ export function HabitListConfirmDialogs({ autoLogParentName, onConfirmAutoLogParent, onCancelAutoLogParent, -}: HabitListConfirmDialogsProps) { +}: Readonly) { const isSkipParent = autoLogParentMode === 'skip' return ( <> diff --git a/apps/mobile/components/habit-list/date-group-section.tsx b/apps/mobile/components/habit-list/date-group-section.tsx index 6da4c9978..3eeb391d3 100644 --- a/apps/mobile/components/habit-list/date-group-section.tsx +++ b/apps/mobile/components/habit-list/date-group-section.tsx @@ -46,7 +46,7 @@ export function HabitListDateGroupSection({ group, overdueLabel, renderHabit, -}: HabitListDateGroupSectionProps) { +}: Readonly) { const { currentScheme, currentTheme } = useAppTheme() const tokens = createTokensV2(currentScheme, currentTheme) diff --git a/apps/mobile/components/habit-list/drill-panel.tsx b/apps/mobile/components/habit-list/drill-panel.tsx index c228e4fd8..de58476c1 100644 --- a/apps/mobile/components/habit-list/drill-panel.tsx +++ b/apps/mobile/components/habit-list/drill-panel.tsx @@ -38,7 +38,7 @@ export function DrillHabitItem({ styles, getDrillChildren, renderHabitCard, -}: DrillHabitItemProps) { +}: Readonly) { const grandChildren = getDrillChildren(child.id) return ( @@ -60,7 +60,7 @@ interface DrillFooterProps { } /** Add-sub-habit affordance shown below the drill list. */ -export function DrillFooter({ styles, label, onAddSubHabit }: DrillFooterProps) { +export function DrillFooter({ styles, label, onAddSubHabit }: Readonly) { const { currentScheme, currentTheme } = useAppTheme() const tokens = createTokensV2(currentScheme, currentTheme) diff --git a/apps/mobile/components/habit-list/empty-state.tsx b/apps/mobile/components/habit-list/empty-state.tsx index a9254bd7c..c2604855e 100644 --- a/apps/mobile/components/habit-list/empty-state.tsx +++ b/apps/mobile/components/habit-list/empty-state.tsx @@ -38,7 +38,7 @@ export function HabitListEmptyState({ askAstraLabel, onAskAstra, variant = 'primary', -}: HabitListEmptyStateProps) { +}: Readonly) { const { currentScheme, currentTheme } = useAppTheme() const tokens = createTokensV2(currentScheme, currentTheme) const isAstraPrompt = variant === 'primary' @@ -109,7 +109,7 @@ interface SkeletonCardStyles { skeletonCheck: StyleProp } -export function SkeletonCard({ styles: cardStyles }: { styles: SkeletonCardStyles }) { +export function SkeletonCard({ styles: cardStyles }: Readonly<{ styles: SkeletonCardStyles }>) { const prefersReducedMotion = usePrefersReducedMotion() const pulse = useSharedValue(1) diff --git a/apps/mobile/components/habit-list/move-parent-dialog.tsx b/apps/mobile/components/habit-list/move-parent-dialog.tsx index ba4332720..aad8ffa98 100644 --- a/apps/mobile/components/habit-list/move-parent-dialog.tsx +++ b/apps/mobile/components/habit-list/move-parent-dialog.tsx @@ -121,7 +121,7 @@ export function MoveParentDialog({ onClose, onConfirm, onSelectOption, -}: MoveParentDialogProps) { +}: Readonly) { const { currentScheme, currentTheme } = useAppTheme() const tokens = createTokensV2(currentScheme, currentTheme) const styles = createStyles(tokens) diff --git a/apps/mobile/components/habits/habit-checklist.tsx b/apps/mobile/components/habits/habit-checklist.tsx index d67bde99e..f469b0fe2 100644 --- a/apps/mobile/components/habits/habit-checklist.tsx +++ b/apps/mobile/components/habits/habit-checklist.tsx @@ -61,7 +61,7 @@ function EditableChecklistItem({ isLast, styles, tokens, -}: EditableChecklistItemProps) { +}: Readonly) { const { t } = useTranslation() const [localText, setLocalText] = useState(text) diff --git a/apps/mobile/components/profile/profile-nav-icon.tsx b/apps/mobile/components/profile/profile-nav-icon.tsx index 239798f03..e003f7335 100644 --- a/apps/mobile/components/profile/profile-nav-icon.tsx +++ b/apps/mobile/components/profile/profile-nav-icon.tsx @@ -16,10 +16,10 @@ import type { ProfileNavIconKey } from '@orbit/shared/utils/profile-navigation' export function ProfileNavIcon({ iconKey, color, -}: { +}: Readonly<{ iconKey: ProfileNavIconKey color: string -}) { +}>) { switch (iconKey) { case 'settings': return diff --git a/apps/mobile/components/tour/tour-provider.tsx b/apps/mobile/components/tour/tour-provider.tsx index 785d60351..60e257f8f 100644 --- a/apps/mobile/components/tour/tour-provider.tsx +++ b/apps/mobile/components/tour/tour-provider.tsx @@ -24,7 +24,7 @@ const HABITS_TOUR_SCROLL_Y = 220 * mock data, and cleanup. * Mirrors the web TourProvider: global registry, scroll into view, continuous re-measurement. */ -export function TourProvider({ children }: { children: React.ReactNode }) { +export function TourProvider({ children }: Readonly<{ children: React.ReactNode }>) { const router = useRouter() const pathname = usePathname() const insets = useSafeAreaInsets() diff --git a/apps/mobile/components/tour/tour-replay-modal.tsx b/apps/mobile/components/tour/tour-replay-modal.tsx index 39084e64a..0bbbbf5be 100644 --- a/apps/mobile/components/tour/tour-replay-modal.tsx +++ b/apps/mobile/components/tour/tour-replay-modal.tsx @@ -40,7 +40,7 @@ interface TourReplayModalProps { onClose: () => void } -export function TourReplayModal({ visible, onClose }: TourReplayModalProps) { +export function TourReplayModal({ visible, onClose }: Readonly) { const { t } = useTranslation() const { currentScheme, currentTheme } = useAppTheme() const tokens = useMemo( diff --git a/apps/mobile/components/tour/tour-spotlight.tsx b/apps/mobile/components/tour/tour-spotlight.tsx index ba6d677a5..c6b8d5dc1 100644 --- a/apps/mobile/components/tour/tour-spotlight.tsx +++ b/apps/mobile/components/tour/tour-spotlight.tsx @@ -17,7 +17,7 @@ interface TourSpotlightProps { targetRect: TourTargetRect | null } -export function TourSpotlight({ targetRect }: TourSpotlightProps) { +export function TourSpotlight({ targetRect }: Readonly) { const { width: screenWidth, height: screenHeight } = useWindowDimensions() const { currentScheme, currentTheme } = useAppTheme() const tokens = useMemo( diff --git a/apps/mobile/components/tour/tour-tooltip.tsx b/apps/mobile/components/tour/tour-tooltip.tsx index 34fc698d4..579541005 100644 --- a/apps/mobile/components/tour/tour-tooltip.tsx +++ b/apps/mobile/components/tour/tour-tooltip.tsx @@ -63,7 +63,7 @@ export function TourTooltip({ onNext, onPrev, onSkip, -}: TourTooltipProps) { +}: Readonly) { const { t } = useTranslation() const { currentScheme, currentTheme } = useAppTheme() const tokens = useMemo( @@ -228,11 +228,11 @@ function ProgressDot({ state, color, reduceMotion, -}: { +}: Readonly<{ state: ProgressDotState color: string reduceMotion: boolean -}) { +}>) { const [scaleX] = useState(() => new Animated.Value(state === 'active' ? 1 : 0.5)) useEffect(() => { diff --git a/apps/mobile/components/ui/year-picker.tsx b/apps/mobile/components/ui/year-picker.tsx index 4a3625bb2..812e5ee0f 100644 --- a/apps/mobile/components/ui/year-picker.tsx +++ b/apps/mobile/components/ui/year-picker.tsx @@ -21,7 +21,7 @@ export function YearPicker({ selectedYear, onSelectYear, tokens, -}: YearPickerProps) { +}: Readonly) { const scrollRef = useRef(null) const years = useMemo(() => buildYearRange(selectedYear), [selectedYear]) diff --git a/apps/web/app/(public)/u/[slug]/page.tsx b/apps/web/app/(public)/u/[slug]/page.tsx index d05f165ff..32635635f 100644 --- a/apps/web/app/(public)/u/[slug]/page.tsx +++ b/apps/web/app/(public)/u/[slug]/page.tsx @@ -61,7 +61,7 @@ export async function generateMetadata({ params }: PageParams): Promise) { const { slug } = await params const view = await fetchView(slug) if (!view) notFound() diff --git a/apps/web/components/tour/tour-tooltip.tsx b/apps/web/components/tour/tour-tooltip.tsx index 8fb7e1384..bc94c19ff 100644 --- a/apps/web/components/tour/tour-tooltip.tsx +++ b/apps/web/components/tour/tour-tooltip.tsx @@ -201,7 +201,7 @@ export function TourTooltip({ onNext, onPrev, onSkip, -}: TourTooltipProps) { +}: Readonly) { const t = useTranslations() const [pos, setPos] = useState<{ top: number; left: number } | null>(null) const [mode, setMode] = useState<'float' | 'sheet-top' | 'sheet-bottom'>('sheet-bottom') diff --git a/sonar-project.properties b/sonar-project.properties index 5341ea5bb..21da9ef5b 100644 --- a/sonar-project.properties +++ b/sonar-project.properties @@ -40,4 +40,6 @@ sonar.coverage.exclusions=apps/web/app/**/layout.tsx,apps/mobile/app/**/_layout. # apps/mobile/lib/plural.ts one-line re-export of @orbit/shared/utils/plural. # apps/mobile/lib/theme.ts createTokensV2 per-layer token derivation; repetitive shapes mirror the scheme structure. # packages/shared/src/theme/color-schemes.ts 6 near-identical per-scheme literal blocks; data, not logic. -sonar.cpd.exclusions=apps/web/app/actions/**,apps/mobile/stores/**,apps/mobile/lib/habit-request-builders.ts,apps/mobile/lib/habit-optimistic-helpers.ts,apps/mobile/lib/plural.ts,apps/mobile/lib/theme.ts,packages/shared/src/theme/color-schemes.ts +# apps/mobile/components/habit-list/confirm-dialogs.tsx verbatim web/mobile HabitListConfirmDialogs parity mirror: identical props-driven JSX body, +# apps/web/components/habits/habit-list/confirm-dialogs.tsx but ConfirmDialog + t() are platform-specific (NativeWind vs shadcn / i18next vs next-intl), so it can't move to packages/shared. +sonar.cpd.exclusions=apps/web/app/actions/**,apps/mobile/stores/**,apps/mobile/lib/habit-request-builders.ts,apps/mobile/lib/habit-optimistic-helpers.ts,apps/mobile/lib/plural.ts,apps/mobile/lib/theme.ts,packages/shared/src/theme/color-schemes.ts,apps/mobile/components/habit-list/confirm-dialogs.tsx,apps/web/components/habits/habit-list/confirm-dialogs.tsx