From e2496496561c2d759f2b0736bf727f9bfa6a0120 Mon Sep 17 00:00:00 2001 From: Mohamed Boudra Date: Thu, 23 Apr 2026 23:20:40 +0700 Subject: [PATCH] chore(lint): hoist inline arrays in app (jsx-no-new-array-as-prop) --- .../src/components/attachment-lightbox.tsx | 21 +++++++++------ .../app/src/components/attachment-pill.tsx | 8 ++++-- .../app/src/components/diff-scroll.web.tsx | 4 ++- .../components/draggable-list.web.test.tsx | 4 ++- .../app/src/components/file-drop-zone.tsx | 9 +++++-- .../headers/header-toggle-button.tsx | 6 +++-- .../src/components/headers/screen-title.tsx | 5 ++-- .../components/keyboard-shortcuts-dialog.tsx | 4 ++- .../components/sidebar/sidebar-header-row.tsx | 17 +++++++++++- .../sortable-inline-list.web.test.tsx | 4 ++- packages/app/src/components/ui/button.tsx | 27 ++++++++++++------- .../src/components/workspace-setup-dialog.tsx | 4 ++- .../components/desktop-permission-row.tsx | 8 +++++- .../desktop-permissions-section.tsx | 11 ++++---- .../components/integrations-section.tsx | 3 ++- packages/app/src/panels/setup-panel.tsx | 15 ++++++++--- .../src/screens/settings/settings-section.tsx | 5 ++-- .../workspace/workspace-draft-agent-tab.tsx | 7 ++++- .../workspace-scripts-button.test.tsx | 4 ++- 19 files changed, 120 insertions(+), 46 deletions(-) diff --git a/packages/app/src/components/attachment-lightbox.tsx b/packages/app/src/components/attachment-lightbox.tsx index dc67f40dc..531175420 100644 --- a/packages/app/src/components/attachment-lightbox.tsx +++ b/packages/app/src/components/attachment-lightbox.tsx @@ -1,4 +1,4 @@ -import { useEffect, useState } from "react"; +import { useEffect, useMemo, useState } from "react"; import { Modal, Pressable, Text, View } from "react-native"; import { StyleSheet, useUnistyles } from "react-native-unistyles"; import { useSafeAreaInsets } from "react-native-safe-area-context"; @@ -36,6 +36,17 @@ export function AttachmentLightbox({ metadata, onClose }: AttachmentLightboxProp }; }, [metadata, onClose]); + const closeButtonStyle = useMemo( + () => [ + styles.closeButton, + { + top: insets.top + theme.spacing[3], + right: insets.right + theme.spacing[3], + }, + ], + [insets.top, insets.right, theme.spacing], + ); + if (!metadata) { return null; } @@ -74,13 +85,7 @@ export function AttachmentLightbox({ metadata, onClose }: AttachmentLightboxProp accessibilityLabel="Close image" hitSlop={8} onPress={onClose} - style={[ - styles.closeButton, - { - top: insets.top + theme.spacing[3], - right: insets.right + theme.spacing[3], - }, - ]} + style={closeButtonStyle} > diff --git a/packages/app/src/components/attachment-pill.tsx b/packages/app/src/components/attachment-pill.tsx index 069b26000..0a9795ec3 100644 --- a/packages/app/src/components/attachment-pill.tsx +++ b/packages/app/src/components/attachment-pill.tsx @@ -1,4 +1,4 @@ -import { type ReactNode, useState } from "react"; +import { type ReactNode, useMemo, useState } from "react"; import { Pressable, View } from "react-native"; import { StyleSheet, useUnistyles } from "react-native-unistyles"; import { X } from "lucide-react-native"; @@ -30,6 +30,10 @@ export function AttachmentPill({ const [isCloseHovered, setIsCloseHovered] = useState(false); const alwaysShow = isNative || isCompact; const showRemove = alwaysShow || isBodyHovered || isCloseHovered; + const closeButtonStyle = useMemo( + () => [styles.closeButton, !showRemove && styles.closeButtonHidden], + [showRemove], + ); return ( diff --git a/packages/app/src/components/diff-scroll.web.tsx b/packages/app/src/components/diff-scroll.web.tsx index 364c752b2..80f61a763 100644 --- a/packages/app/src/components/diff-scroll.web.tsx +++ b/packages/app/src/components/diff-scroll.web.tsx @@ -1,3 +1,4 @@ +import { useMemo } from "react"; import { ScrollView, type LayoutChangeEvent, type StyleProp, type ViewStyle } from "react-native"; import { useWebScrollbarStyle } from "@/hooks/use-web-scrollbar-style"; @@ -16,13 +17,14 @@ export function DiffScroll({ contentContainerStyle, }: DiffScrollProps) { const webScrollbarStyle = useWebScrollbarStyle(); + const combinedStyle = useMemo(() => [style, webScrollbarStyle], [style, webScrollbarStyle]); return ( onScrollViewWidthChange(e.nativeEvent.layout.width)} > diff --git a/packages/app/src/components/draggable-list.web.test.tsx b/packages/app/src/components/draggable-list.web.test.tsx index 32b009142..a87e204a8 100644 --- a/packages/app/src/components/draggable-list.web.test.tsx +++ b/packages/app/src/components/draggable-list.web.test.tsx @@ -90,11 +90,13 @@ afterEach(() => { vi.unstubAllGlobals(); }); +const DATA: string[] = ["alpha", "beta"]; + function renderList(): void { act(() => { root?.render( item} onDragEnd={vi.fn()} renderItem={({ item, isActive }) => ( diff --git a/packages/app/src/components/file-drop-zone.tsx b/packages/app/src/components/file-drop-zone.tsx index 52302ae35..58b5ad523 100644 --- a/packages/app/src/components/file-drop-zone.tsx +++ b/packages/app/src/components/file-drop-zone.tsx @@ -1,7 +1,7 @@ import { View, Text } from "react-native"; import { StyleSheet, useUnistyles } from "react-native-unistyles"; import Animated, { useAnimatedStyle, withTiming, useSharedValue } from "react-native-reanimated"; -import { useEffect } from "react"; +import { useEffect, useMemo } from "react"; import { Upload } from "lucide-react-native"; import { useFileDropZone } from "@/hooks/use-file-drop-zone"; import type { ImageAttachment } from "./message-input"; @@ -33,6 +33,11 @@ export function FileDropZone({ children, onFilesDropped, disabled = false }: Fil pointerEvents: overlayOpacity.value > 0 ? "auto" : "none", })); + const overlayStyle = useMemo( + () => [styles.overlay, overlayAnimatedStyle], + [overlayAnimatedStyle], + ); + // On non-web platforms, just render children if (!IS_WEB) { return <>{children}; @@ -47,7 +52,7 @@ export function FileDropZone({ children, onFilesDropped, disabled = false }: Fil {children} {/* Drop overlay */} - + {/* Backdrop */} {/* Content */} diff --git a/packages/app/src/components/headers/header-toggle-button.tsx b/packages/app/src/components/headers/header-toggle-button.tsx index e0f5b2cc6..c1f6792f1 100644 --- a/packages/app/src/components/headers/header-toggle-button.tsx +++ b/packages/app/src/components/headers/header-toggle-button.tsx @@ -1,4 +1,4 @@ -import type { ReactElement, ReactNode } from "react"; +import { useMemo, type ReactElement, type ReactNode } from "react"; import { Text, View, type PressableProps, type StyleProp, type ViewStyle } from "react-native"; import { StyleSheet } from "react-native-unistyles"; import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui/tooltip"; @@ -42,6 +42,8 @@ export function HeaderToggleButton({ ? ({ "aria-expanded": expandedState } as any) : null; + const combinedStyle = useMemo(() => [headerIconSlotStyle.slot, style], [style]); + return ( { onPress(e); }} - style={[headerIconSlotStyle.slot, style]} + style={combinedStyle} > {typeof children === "function" ? (state: { pressed: boolean; hovered?: boolean }) => diff --git a/packages/app/src/components/headers/screen-title.tsx b/packages/app/src/components/headers/screen-title.tsx index de8b98449..338ad1961 100644 --- a/packages/app/src/components/headers/screen-title.tsx +++ b/packages/app/src/components/headers/screen-title.tsx @@ -1,4 +1,4 @@ -import type { ReactNode } from "react"; +import { useMemo, type ReactNode } from "react"; import { Text, type StyleProp, type TextStyle } from "react-native"; import { StyleSheet } from "react-native-unistyles"; @@ -15,8 +15,9 @@ interface ScreenTitleProps { * HeaderIconBadge) — never nested inside this component. */ export function ScreenTitle({ children, numberOfLines = 1, testID, style }: ScreenTitleProps) { + const combinedStyle = useMemo(() => [styles.text, style], [style]); return ( - + {children} ); diff --git a/packages/app/src/components/keyboard-shortcuts-dialog.tsx b/packages/app/src/components/keyboard-shortcuts-dialog.tsx index e882f87f6..92bc86e01 100644 --- a/packages/app/src/components/keyboard-shortcuts-dialog.tsx +++ b/packages/app/src/components/keyboard-shortcuts-dialog.tsx @@ -8,6 +8,8 @@ import { useKeyboardShortcutsStore } from "@/stores/keyboard-shortcuts-store"; import { getShortcutOs } from "@/utils/shortcut-platform"; import { buildKeyboardShortcutHelpSections } from "@/keyboard/keyboard-shortcuts"; +const SNAP_POINTS: string[] = ["70%", "92%"]; + export function KeyboardShortcutsDialog() { const open = useKeyboardShortcutsStore((s) => s.shortcutsDialogOpen); const setOpen = useKeyboardShortcutsStore((s) => s.setShortcutsDialogOpen); @@ -25,7 +27,7 @@ export function KeyboardShortcutsDialog() { visible={open} onClose={() => setOpen(false)} testID="keyboard-shortcuts-dialog" - snapPoints={["70%", "92%"]} + snapPoints={SNAP_POINTS} > {sections.map((section) => ( diff --git a/packages/app/src/components/sidebar/sidebar-header-row.tsx b/packages/app/src/components/sidebar/sidebar-header-row.tsx index 26e796221..94fad1fd5 100644 --- a/packages/app/src/components/sidebar/sidebar-header-row.tsx +++ b/packages/app/src/components/sidebar/sidebar-header-row.tsx @@ -1,3 +1,4 @@ +import { useMemo } from "react"; import { Pressable, Text, View } from "react-native"; import { StyleSheet, useUnistyles } from "react-native-unistyles"; import type { LucideIcon } from "lucide-react-native"; @@ -47,7 +48,7 @@ export function SidebarHeaderRow({ return ( <> - {label} + ); }} @@ -56,6 +57,20 @@ export function SidebarHeaderRow({ ); } +function SidebarHeaderRowLabel({ + label, + isHighlighted, +}: { + label: string; + isHighlighted: boolean; +}) { + const labelStyle = useMemo( + () => [styles.label, isHighlighted && styles.labelHighlighted], + [isHighlighted], + ); + return {label}; +} + const styles = StyleSheet.create((theme) => ({ container: { height: { diff --git a/packages/app/src/components/sortable-inline-list.web.test.tsx b/packages/app/src/components/sortable-inline-list.web.test.tsx index 29cfb6a56..50b20df5a 100644 --- a/packages/app/src/components/sortable-inline-list.web.test.tsx +++ b/packages/app/src/components/sortable-inline-list.web.test.tsx @@ -73,11 +73,13 @@ afterEach(() => { vi.unstubAllGlobals(); }); +const DATA: string[] = ["alpha", "beta"]; + function renderList(): void { act(() => { root?.render( item} onDragEnd={vi.fn()} renderItem={({ item, isActive }) => ( diff --git a/packages/app/src/components/ui/button.tsx b/packages/app/src/components/ui/button.tsx index 7da8dbfb5..97be1d105 100644 --- a/packages/app/src/components/ui/button.tsx +++ b/packages/app/src/components/ui/button.tsx @@ -1,4 +1,10 @@ -import { useState, type ComponentType, type PropsWithChildren, type ReactElement } from "react"; +import { + useMemo, + useState, + type ComponentType, + type PropsWithChildren, + type ReactElement, +} from "react"; import { Pressable, Text, View } from "react-native"; import type { PressableProps, StyleProp, TextStyle, ViewStyle } from "react-native"; import { StyleSheet, useUnistyles } from "react-native-unistyles"; @@ -119,14 +125,17 @@ export function Button({ const sizeStyle = size === "sm" ? styles.sm : size === "lg" ? styles.lg : styles.md; const isGhostHovered = hovered && variant === "ghost"; - const resolvedTextStyle = [ - styles.text, - variant === "default" ? styles.textDefault : null, - variant === "destructive" ? styles.textDestructive : null, - variant === "ghost" ? styles.textGhost : null, - textStyle, - isGhostHovered ? styles.textGhostHovered : null, - ]; + const resolvedTextStyle = useMemo( + () => [ + styles.text, + variant === "default" ? styles.textDefault : null, + variant === "destructive" ? styles.textDestructive : null, + variant === "ghost" ? styles.textGhost : null, + textStyle, + isGhostHovered ? styles.textGhostHovered : null, + ], + [variant, textStyle, isGhostHovered], + ); function renderIcon() { if (!leftIcon) return null; diff --git a/packages/app/src/components/workspace-setup-dialog.tsx b/packages/app/src/components/workspace-setup-dialog.tsx index 56fc9abc3..8a3f90537 100644 --- a/packages/app/src/components/workspace-setup-dialog.tsx +++ b/packages/app/src/components/workspace-setup-dialog.tsx @@ -26,6 +26,8 @@ function toProjectIconDataUri(icon: { mimeType: string; data: string } | null): return `data:${icon.mimeType};base64,${icon.data}`; } +const SNAP_POINTS: string[] = ["82%", "94%"]; + export function WorkspaceSetupDialog() { const { theme } = useUnistyles(); const toast = useToast(); @@ -284,7 +286,7 @@ export function WorkspaceSetupDialog() { subtitle={subtitleContent} visible={true} onClose={handleClose} - snapPoints={["82%", "94%"]} + snapPoints={SNAP_POINTS} testID="workspace-setup-dialog" desktopMaxWidth={640} onFilesDropped={handleFilesDropped} diff --git a/packages/app/src/desktop/components/desktop-permission-row.tsx b/packages/app/src/desktop/components/desktop-permission-row.tsx index a6036818b..0cf0eb389 100644 --- a/packages/app/src/desktop/components/desktop-permission-row.tsx +++ b/packages/app/src/desktop/components/desktop-permission-row.tsx @@ -1,3 +1,4 @@ +import { useMemo } from "react"; import { View, Text } from "react-native"; import { StyleSheet, useUnistyles } from "react-native-unistyles"; import { Check } from "lucide-react-native"; @@ -38,8 +39,13 @@ export function DesktopPermissionRow({ state !== "prompt" && state !== "not-granted"; + const rowStyle = useMemo( + () => [settingsStyles.row, showBorder && settingsStyles.rowBorder], + [showBorder], + ); + return ( - + {title} diff --git a/packages/app/src/desktop/components/desktop-permissions-section.tsx b/packages/app/src/desktop/components/desktop-permissions-section.tsx index e11d35041..85bbc87f7 100644 --- a/packages/app/src/desktop/components/desktop-permissions-section.tsx +++ b/packages/app/src/desktop/components/desktop-permissions-section.tsx @@ -1,3 +1,4 @@ +import { useMemo } from "react"; import { View, Text } from "react-native"; import { StyleSheet, useUnistyles } from "react-native-unistyles"; import { RotateCw } from "lucide-react-native"; @@ -27,6 +28,10 @@ export function DesktopPermissionsSection() { const isBusy = isRefreshing || requestingPermission !== null; const notificationsGranted = snapshot?.notifications.state === "granted"; + const errorTextStyle = useMemo( + () => [styles.errorText, { color: theme.colors.destructive }], + [theme.colors.destructive], + ); const refreshButton = (