chore(lint): hoist inline arrays in app (jsx-no-new-array-as-prop)

This commit is contained in:
Mohamed Boudra
2026-04-23 23:20:40 +07:00
parent 6f6c8a607c
commit e249649656
19 changed files with 120 additions and 46 deletions

View File

@@ -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}
>
<X size={16} color={theme.colors.foregroundMuted} />
</Pressable>

View File

@@ -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 (
<View style={styles.wrapper}>
<Pressable
@@ -52,7 +56,7 @@ export function AttachmentPill({
hitSlop={8}
accessibilityRole="button"
accessibilityLabel={removeAccessibilityLabel}
style={[styles.closeButton, !showRemove && styles.closeButtonHidden]}
style={closeButtonStyle}
>
<X size={12} color={theme.colors.foregroundMuted} />
</Pressable>

View File

@@ -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 (
<ScrollView
horizontal
nestedScrollEnabled
showsHorizontalScrollIndicator
style={[style, webScrollbarStyle]}
style={combinedStyle}
contentContainerStyle={contentContainerStyle}
onLayout={(e: LayoutChangeEvent) => onScrollViewWidthChange(e.nativeEvent.layout.width)}
>

View File

@@ -90,11 +90,13 @@ afterEach(() => {
vi.unstubAllGlobals();
});
const DATA: string[] = ["alpha", "beta"];
function renderList(): void {
act(() => {
root?.render(
<DraggableList
data={["alpha", "beta"]}
data={DATA}
keyExtractor={(item) => item}
onDragEnd={vi.fn()}
renderItem={({ item, isActive }) => (

View File

@@ -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 */}
<Animated.View style={[styles.overlay, overlayAnimatedStyle]}>
<Animated.View style={overlayStyle}>
{/* Backdrop */}
<View style={styles.backdrop} />
{/* Content */}

View File

@@ -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 (
<Tooltip delayDuration={tooltipDelayDuration} enabledOnDesktop enabledOnMobile={false}>
<TooltipTrigger
@@ -51,7 +53,7 @@ export function HeaderToggleButton({
onPress={(e) => {
onPress(e);
}}
style={[headerIconSlotStyle.slot, style]}
style={combinedStyle}
>
{typeof children === "function"
? (state: { pressed: boolean; hovered?: boolean }) =>

View File

@@ -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 (
<Text style={[styles.text, style]} numberOfLines={numberOfLines} testID={testID}>
<Text style={combinedStyle} numberOfLines={numberOfLines} testID={testID}>
{children}
</Text>
);

View File

@@ -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}
>
<View testID="keyboard-shortcuts-dialog-content" style={styles.content}>
{sections.map((section) => (

View File

@@ -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 (
<>
<Icon size={theme.iconSize.md} color={iconColor} />
<Text style={[styles.label, isHighlighted && styles.labelHighlighted]}>{label}</Text>
<SidebarHeaderRowLabel label={label} isHighlighted={isHighlighted} />
</>
);
}}
@@ -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 <Text style={labelStyle}>{label}</Text>;
}
const styles = StyleSheet.create((theme) => ({
container: {
height: {

View File

@@ -73,11 +73,13 @@ afterEach(() => {
vi.unstubAllGlobals();
});
const DATA: string[] = ["alpha", "beta"];
function renderList(): void {
act(() => {
root?.render(
<SortableInlineList
data={["alpha", "beta"]}
data={DATA}
keyExtractor={(item) => item}
onDragEnd={vi.fn()}
renderItem={({ item, isActive }) => (

View File

@@ -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;

View File

@@ -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}

View File

@@ -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 (
<View style={[settingsStyles.row, showBorder && settingsStyles.rowBorder]}>
<View style={rowStyle}>
<View style={settingsStyles.rowContent}>
<Text style={settingsStyles.rowTitle}>{title}</Text>
</View>

View File

@@ -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 = (
<Button
@@ -60,11 +65,7 @@ export function DesktopPermissionsSection() {
void sendTestNotification();
}}
/>
{testNotificationError ? (
<Text style={[styles.errorText, { color: theme.colors.destructive }]}>
{testNotificationError}
</Text>
) : null}
{testNotificationError ? <Text style={errorTextStyle}>{testNotificationError}</Text> : null}
<DesktopPermissionRow
title="Microphone"
showBorder

View File

@@ -18,6 +18,7 @@ import {
const CLI_DOCS_URL = "https://paseo.sh/docs/cli";
const SKILLS_DOCS_URL = "https://paseo.sh/docs/skills";
const ROW_WITH_BORDER_STYLE = [settingsStyles.row, settingsStyles.rowBorder];
export function IntegrationsSection() {
const { theme } = useUnistyles();
@@ -136,7 +137,7 @@ export function IntegrationsSection() {
</Button>
)}
</View>
<View style={[settingsStyles.row, settingsStyles.rowBorder]}>
<View style={ROW_WITH_BORDER_STYLE}>
<View style={settingsStyles.rowContent}>
<View style={styles.rowTitleRow}>
<Blocks size={theme.iconSize.md} color={theme.colors.foreground} />

View File

@@ -1,4 +1,4 @@
import { useCallback, useEffect, useRef, useState } from "react";
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
import { CheckCircle2, ChevronRight, CircleAlert, SquareTerminal } from "lucide-react-native";
import { ActivityIndicator, Pressable, ScrollView, Text, View } from "react-native";
import invariant from "tiny-invariant";
@@ -243,10 +243,9 @@ function SetupPanel() {
{command.durationMs != null ? (
<Text style={styles.commandDuration}>{formatDuration(command.durationMs)}</Text>
) : null}
<ChevronRight
size={14}
<SetupCommandChevron
showDetail={showDetail}
color={theme.colors.foregroundMuted}
style={[styles.chevron, showDetail && styles.chevronExpanded]}
/>
</Pressable>
{showDetail ? (
@@ -324,6 +323,14 @@ export const setupPanelRegistration: PanelRegistration<"setup"> = {
useDescriptor: useSetupPanelDescriptor,
};
function SetupCommandChevron({ showDetail, color }: { showDetail: boolean; color: string }) {
const chevronStyle = useMemo(
() => [styles.chevron, showDetail && styles.chevronExpanded],
[showDetail],
);
return <ChevronRight size={14} color={color} style={chevronStyle} />;
}
const styles = StyleSheet.create((theme) => ({
container: {
flex: 1,

View File

@@ -1,4 +1,4 @@
import type { ReactNode } from "react";
import { useMemo, type ReactNode } from "react";
import { Text, View, type StyleProp, type ViewStyle } from "react-native";
import { StyleSheet } from "react-native-unistyles";
import { settingsStyles } from "@/styles/settings";
@@ -23,8 +23,9 @@ export function SettingsSection({
style,
children,
}: SettingsSectionProps) {
const sectionStyle = useMemo(() => [settingsStyles.section, style], [style]);
return (
<View style={[settingsStyles.section, style]} testID={testID}>
<View style={sectionStyle} testID={testID}>
<View style={styles.header}>
<Text style={settingsStyles.sectionHeaderTitle}>{title}</Text>
{trailing}

View File

@@ -337,6 +337,11 @@ export function WorkspaceDraftAgentTab({
[composerState],
);
const inputAreaWrapperStyle = useMemo(
() => [styles.inputAreaWrapper, { paddingBottom: insets.bottom }],
[insets.bottom],
);
return (
<FileDropZone onFilesDropped={handleFilesDropped}>
<View style={styles.container}>
@@ -368,7 +373,7 @@ export function WorkspaceDraftAgentTab({
)}
</View>
<View style={[styles.inputAreaWrapper, { paddingBottom: insets.bottom }]}>
<View style={inputAreaWrapperStyle}>
<Composer
agentId={tabId}
serverId={serverId}

View File

@@ -137,6 +137,8 @@ function script(
};
}
const LIVE_TERMINAL_IDS: string[] = ["terminal-script-1"];
function renderScripts(scripts: WorkspaceScriptPayload[]): {
rerender: (nextScripts: WorkspaceScriptPayload[]) => Promise<void>;
unmount: () => void;
@@ -158,7 +160,7 @@ function renderScripts(scripts: WorkspaceScriptPayload[]): {
serverId="test-server"
workspaceId="workspace-1"
scripts={nextScripts}
liveTerminalIds={["terminal-script-1"]}
liveTerminalIds={LIVE_TERMINAL_IDS}
/>
</QueryClientProvider>
);