mirror of
https://github.com/getpaseo/paseo.git
synced 2026-07-29 12:01:31 +00:00
chore(lint): hoist inline arrays in app (jsx-no-new-array-as-prop)
This commit is contained in:
@@ -101,14 +101,9 @@ describe("CalloutCard", () => {
|
||||
|
||||
it("renders one action when one is provided", () => {
|
||||
const onPress = vi.fn();
|
||||
const actions = [{ label: "Undo", onPress }];
|
||||
act(() => {
|
||||
root?.render(
|
||||
<CalloutCard
|
||||
description="Saved."
|
||||
actions={[{ label: "Undo", onPress }]}
|
||||
testID="callout"
|
||||
/>,
|
||||
);
|
||||
root?.render(<CalloutCard description="Saved." actions={actions} testID="callout" />);
|
||||
});
|
||||
|
||||
const button = container?.querySelector(
|
||||
@@ -119,15 +114,16 @@ describe("CalloutCard", () => {
|
||||
});
|
||||
|
||||
it("renders up to two actions", () => {
|
||||
const actions: React.ComponentProps<typeof CalloutCard>["actions"] = [
|
||||
{ label: "What's new", onPress: vi.fn() },
|
||||
{ label: "Install & restart", onPress: vi.fn(), variant: "primary" },
|
||||
];
|
||||
act(() => {
|
||||
root?.render(
|
||||
<CalloutCard
|
||||
title="Update available"
|
||||
description="v1 ready."
|
||||
actions={[
|
||||
{ label: "What's new", onPress: vi.fn() },
|
||||
{ label: "Install & restart", onPress: vi.fn(), variant: "primary" },
|
||||
]}
|
||||
actions={actions}
|
||||
testID="callout"
|
||||
/>,
|
||||
);
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { X } from "lucide-react-native";
|
||||
import type { ReactNode } from "react";
|
||||
import { useMemo, type ReactNode } from "react";
|
||||
import { Pressable, Text, View } from "react-native";
|
||||
import { StyleSheet, useUnistyles } from "react-native-unistyles";
|
||||
|
||||
@@ -43,7 +43,10 @@ export function CalloutCard({
|
||||
const hasHeader = title != null || icon != null;
|
||||
const hasDescription = description != null && description !== "";
|
||||
|
||||
const containerStyle = [styles.container, variant === "error" ? styles.containerError : null];
|
||||
const containerStyle = useMemo(
|
||||
() => [styles.container, variant === "error" ? styles.containerError : null],
|
||||
[variant],
|
||||
);
|
||||
|
||||
return (
|
||||
<View style={containerStyle} testID={testID} accessibilityRole="alert">
|
||||
@@ -106,6 +109,10 @@ export function CalloutCard({
|
||||
|
||||
function CalloutActionButton({ action, testID }: { action: CalloutAction; testID?: string }) {
|
||||
const isPrimary = action.variant === "primary";
|
||||
const labelStyle = useMemo(
|
||||
() => [styles.actionLabel, isPrimary ? styles.actionLabelPrimary : styles.actionLabelSecondary],
|
||||
[isPrimary],
|
||||
);
|
||||
return (
|
||||
<Pressable
|
||||
onPress={action.onPress}
|
||||
@@ -119,13 +126,7 @@ function CalloutActionButton({ action, testID }: { action: CalloutAction; testID
|
||||
action.disabled ? styles.actionButtonDisabled : null,
|
||||
]}
|
||||
>
|
||||
<Text
|
||||
style={[
|
||||
styles.actionLabel,
|
||||
isPrimary ? styles.actionLabelPrimary : styles.actionLabelSecondary,
|
||||
]}
|
||||
numberOfLines={1}
|
||||
>
|
||||
<Text style={labelStyle} numberOfLines={1}>
|
||||
{action.label}
|
||||
</Text>
|
||||
</Pressable>
|
||||
|
||||
@@ -16,7 +16,9 @@ import type { AgentModelDefinition, AgentProvider } from "@server/server/agent/a
|
||||
import type { AgentProviderDefinition } from "@server/server/agent/provider-manifest";
|
||||
const IS_WEB = platformIsWeb;
|
||||
|
||||
import { Combobox, ComboboxItem } from "@/components/ui/combobox";
|
||||
import { Combobox, ComboboxItem, type ComboboxOption } from "@/components/ui/combobox";
|
||||
|
||||
const EMPTY_COMBOBOX_OPTIONS: ReadonlyArray<ComboboxOption> = [];
|
||||
import { getProviderIcon } from "@/components/provider-icons";
|
||||
import {
|
||||
buildModelRows,
|
||||
@@ -356,13 +358,18 @@ function ProviderSearchInput({
|
||||
}
|
||||
}, [autoFocus]);
|
||||
|
||||
const inputStyle = useMemo(
|
||||
() => [styles.providerSearchInput, platformIsWeb && { outlineStyle: "none" }],
|
||||
[],
|
||||
);
|
||||
|
||||
return (
|
||||
<View style={styles.providerSearchContainer}>
|
||||
<Search size={theme.iconSize.md} color={theme.colors.foregroundMuted} />
|
||||
<InputComponent
|
||||
ref={inputRef as any}
|
||||
// @ts-expect-error - outlineStyle is web-only
|
||||
style={[styles.providerSearchInput, platformIsWeb && { outlineStyle: "none" }]}
|
||||
style={inputStyle}
|
||||
placeholder="Search models..."
|
||||
placeholderTextColor={theme.colors.foregroundMuted}
|
||||
value={value}
|
||||
@@ -657,7 +664,7 @@ export function CombinedModelSelector({
|
||||
)}
|
||||
</Pressable>
|
||||
<Combobox
|
||||
options={[]}
|
||||
options={EMPTY_COMBOBOX_OPTIONS as ComboboxOption[]}
|
||||
value=""
|
||||
onSelect={() => {}}
|
||||
open={isOpen}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { useEffect, useRef } from "react";
|
||||
import { useEffect, useMemo, useRef } from "react";
|
||||
import { ActivityIndicator, Pressable, Text, View } from "react-native";
|
||||
import { useSafeAreaInsets } from "react-native-safe-area-context";
|
||||
import { StyleSheet, useUnistyles } from "react-native-unistyles";
|
||||
@@ -36,15 +36,17 @@ export function DownloadToast() {
|
||||
};
|
||||
}, [activeDownload, dismissDownload]);
|
||||
|
||||
const containerStyle = useMemo(
|
||||
() => [styles.container, { bottom: theme.spacing[4] + insets.bottom }],
|
||||
[theme.spacing, insets.bottom],
|
||||
);
|
||||
|
||||
if (!activeDownload) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<View
|
||||
style={[styles.container, { bottom: theme.spacing[4] + insets.bottom }]}
|
||||
pointerEvents="box-none"
|
||||
>
|
||||
<View style={containerStyle} pointerEvents="box-none">
|
||||
<View style={styles.toast}>
|
||||
{activeDownload.status === "downloading" ? (
|
||||
<ActivityIndicator size="small" color={theme.colors.foreground} />
|
||||
@@ -68,12 +70,7 @@ export function DownloadToast() {
|
||||
</Text>
|
||||
{activeDownload.status === "downloading" && activeDownload.progress && (
|
||||
<View style={styles.progressBar}>
|
||||
<View
|
||||
style={[
|
||||
styles.progressFill,
|
||||
{ width: `${Math.round(activeDownload.progress.percent * 100)}%` },
|
||||
]}
|
||||
/>
|
||||
<ProgressFill percent={activeDownload.progress.percent} />
|
||||
</View>
|
||||
)}
|
||||
</View>
|
||||
@@ -91,6 +88,12 @@ export function DownloadToast() {
|
||||
);
|
||||
}
|
||||
|
||||
function ProgressFill({ percent }: { percent: number }) {
|
||||
const width: `${number}%` = `${Math.round(percent * 100)}%`;
|
||||
const fillStyle = useMemo(() => [styles.progressFill, { width }], [width]);
|
||||
return <View style={fillStyle} />;
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create((theme) => ({
|
||||
container: {
|
||||
position: "absolute",
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { RefreshControl } from "react-native";
|
||||
import { useCallback, useState } from "react";
|
||||
import { useCallback, useMemo, useState } from "react";
|
||||
import DraggableFlatList, {
|
||||
NestableDraggableFlatList,
|
||||
type RenderItemParams,
|
||||
@@ -37,7 +37,15 @@ export function DraggableList<T>({
|
||||
|
||||
// Pass the ref directly to DraggableFlatList - it handles gesture
|
||||
// coordination internally for nestable lists.
|
||||
const simultaneousHandlers = simultaneousGestureRef ? [simultaneousGestureRef] : undefined;
|
||||
const simultaneousHandlers = useMemo(
|
||||
() => (simultaneousGestureRef ? [simultaneousGestureRef] : undefined),
|
||||
[simultaneousGestureRef],
|
||||
);
|
||||
|
||||
const refreshColors = useMemo(
|
||||
() => [theme.colors.foregroundMuted],
|
||||
[theme.colors.foregroundMuted],
|
||||
);
|
||||
|
||||
const handleRenderItem = useCallback(
|
||||
({ item, drag, isActive, getIndex }: RenderItemParams<T>) => {
|
||||
@@ -106,7 +114,7 @@ export function DraggableList<T>({
|
||||
refreshing={refreshing ?? false}
|
||||
onRefresh={onRefresh}
|
||||
tintColor={theme.colors.foregroundMuted}
|
||||
colors={[theme.colors.foregroundMuted]}
|
||||
colors={refreshColors}
|
||||
/>
|
||||
) : undefined
|
||||
}
|
||||
|
||||
@@ -100,10 +100,15 @@ const CodeLine = React.memo(function CodeLine({
|
||||
colorMap,
|
||||
baseColor,
|
||||
}: CodeLineProps) {
|
||||
const gutterStyle = useMemo(() => [codeLineStyles.gutter, { width: gutterWidth }], [gutterWidth]);
|
||||
const gutterTextStyle = useMemo(
|
||||
() => [codeLineStyles.gutterText, { color: baseColor }],
|
||||
[baseColor],
|
||||
);
|
||||
return (
|
||||
<View style={codeLineStyles.line}>
|
||||
<View style={[codeLineStyles.gutter, { width: gutterWidth }]}>
|
||||
<Text style={[codeLineStyles.gutterText, { color: baseColor }]}>{String(lineNumber)}</Text>
|
||||
<View style={gutterStyle}>
|
||||
<Text style={gutterTextStyle}>{String(lineNumber)}</Text>
|
||||
</View>
|
||||
<Text selectable style={codeLineStyles.lineText}>
|
||||
{tokens.map((token, index) => (
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { useMemo } from "react";
|
||||
import { ActivityIndicator, Pressable, View } from "react-native";
|
||||
import { StyleSheet, useUnistyles } from "react-native-unistyles";
|
||||
import { Mic, MicOff, Square } from "lucide-react-native";
|
||||
@@ -23,6 +24,19 @@ export function RealtimeVoiceOverlay({
|
||||
}: RealtimeVoiceOverlayProps) {
|
||||
const { theme } = useUnistyles();
|
||||
const { volume, isSpeaking } = useVoiceTelemetry();
|
||||
const muteButtonStyle = useMemo(
|
||||
() => [
|
||||
styles.actionButton,
|
||||
styles.muteButton,
|
||||
isMuted ? styles.muteButtonMuted : undefined,
|
||||
isSwitching ? styles.buttonDisabled : undefined,
|
||||
],
|
||||
[isMuted, isSwitching],
|
||||
);
|
||||
const stopButtonStyle = useMemo(
|
||||
() => [styles.actionButton, styles.stopButton, isSwitching ? styles.buttonDisabled : undefined],
|
||||
[isSwitching],
|
||||
);
|
||||
return (
|
||||
<View style={styles.container}>
|
||||
<View style={styles.meterContainer}>
|
||||
@@ -40,12 +54,7 @@ export function RealtimeVoiceOverlay({
|
||||
disabled={isSwitching}
|
||||
accessibilityRole="button"
|
||||
accessibilityLabel={isMuted ? "Unmute realtime voice" : "Mute realtime voice"}
|
||||
style={[
|
||||
styles.actionButton,
|
||||
styles.muteButton,
|
||||
isMuted ? styles.muteButtonMuted : undefined,
|
||||
isSwitching ? styles.buttonDisabled : undefined,
|
||||
]}
|
||||
style={muteButtonStyle}
|
||||
>
|
||||
{isMuted ? (
|
||||
<MicOff size={theme.iconSize.lg} color={theme.colors.palette.white} strokeWidth={2.5} />
|
||||
@@ -59,11 +68,7 @@ export function RealtimeVoiceOverlay({
|
||||
disabled={isSwitching}
|
||||
accessibilityRole="button"
|
||||
accessibilityLabel="Stop realtime voice and interrupt turn"
|
||||
style={[
|
||||
styles.actionButton,
|
||||
styles.stopButton,
|
||||
isSwitching ? styles.buttonDisabled : undefined,
|
||||
]}
|
||||
style={stopButtonStyle}
|
||||
>
|
||||
{isSwitching ? (
|
||||
<ActivityIndicator size="small" color={theme.colors.palette.white} />
|
||||
|
||||
@@ -1,14 +1,52 @@
|
||||
import { useEffect, useRef } from "react";
|
||||
import { useEffect, useMemo, useRef } from "react";
|
||||
import { Animated, View, type StyleProp, type ViewStyle } from "react-native";
|
||||
import { StyleSheet } from "react-native-unistyles";
|
||||
|
||||
const SECTION_OPACITIES: readonly number[] = [1, 0.7, 0.4];
|
||||
|
||||
function SkeletonPulse({ pulse, style }: { pulse: Animated.Value; style: StyleProp<ViewStyle> }) {
|
||||
const opacity = pulse.interpolate({
|
||||
inputRange: [0, 1],
|
||||
outputRange: [0.4, 0.8],
|
||||
});
|
||||
|
||||
return <Animated.View style={[style, { opacity }]} />;
|
||||
const pulseStyle = useMemo(() => [style, { opacity }], [style, opacity]);
|
||||
|
||||
return <Animated.View style={pulseStyle} />;
|
||||
}
|
||||
|
||||
function SkeletonSection({
|
||||
pulse,
|
||||
sectionOpacity,
|
||||
sectionIdx,
|
||||
}: {
|
||||
pulse: Animated.Value;
|
||||
sectionOpacity: number;
|
||||
sectionIdx: number;
|
||||
}) {
|
||||
const sectionStyle = useMemo(
|
||||
() => [styles.section, { opacity: sectionOpacity }],
|
||||
[sectionOpacity],
|
||||
);
|
||||
return (
|
||||
<View style={sectionStyle}>
|
||||
<View style={styles.sectionHeader}>
|
||||
<SkeletonPulse pulse={pulse} style={styles.chevron} />
|
||||
<SkeletonPulse pulse={pulse} style={styles.projectIcon} />
|
||||
<SkeletonPulse pulse={pulse} style={styles.sectionTitle} />
|
||||
</View>
|
||||
|
||||
<View style={styles.rows}>
|
||||
{Array.from({ length: 3 }).map((__, rowIdx) => (
|
||||
<View key={`skeleton-row-${sectionIdx}-${rowIdx}`} style={styles.row}>
|
||||
<SkeletonPulse pulse={pulse} style={styles.rowDot} />
|
||||
<SkeletonPulse pulse={pulse} style={styles.rowTitle} />
|
||||
<SkeletonPulse pulse={pulse} style={styles.rowBadge} />
|
||||
</View>
|
||||
))}
|
||||
</View>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
export function SidebarAgentListSkeleton() {
|
||||
@@ -36,27 +74,13 @@ export function SidebarAgentListSkeleton() {
|
||||
|
||||
return (
|
||||
<View style={styles.container}>
|
||||
{[1, 0.7, 0.4].map((sectionOpacity, sectionIdx) => (
|
||||
<View
|
||||
{SECTION_OPACITIES.map((sectionOpacity, sectionIdx) => (
|
||||
<SkeletonSection
|
||||
key={`skeleton-section-${sectionIdx}`}
|
||||
style={[styles.section, { opacity: sectionOpacity }]}
|
||||
>
|
||||
<View style={styles.sectionHeader}>
|
||||
<SkeletonPulse pulse={pulse} style={styles.chevron} />
|
||||
<SkeletonPulse pulse={pulse} style={styles.projectIcon} />
|
||||
<SkeletonPulse pulse={pulse} style={styles.sectionTitle} />
|
||||
</View>
|
||||
|
||||
<View style={styles.rows}>
|
||||
{Array.from({ length: 3 }).map((__, rowIdx) => (
|
||||
<View key={`skeleton-row-${sectionIdx}-${rowIdx}`} style={styles.row}>
|
||||
<SkeletonPulse pulse={pulse} style={styles.rowDot} />
|
||||
<SkeletonPulse pulse={pulse} style={styles.rowTitle} />
|
||||
<SkeletonPulse pulse={pulse} style={styles.rowBadge} />
|
||||
</View>
|
||||
))}
|
||||
</View>
|
||||
</View>
|
||||
pulse={pulse}
|
||||
sectionOpacity={sectionOpacity}
|
||||
sectionIdx={sectionIdx}
|
||||
/>
|
||||
))}
|
||||
</View>
|
||||
);
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { useCallback, useState, type ReactElement } from "react";
|
||||
import { useCallback, useMemo, useState, type ReactElement } from "react";
|
||||
import {
|
||||
DndContext,
|
||||
closestCenter,
|
||||
@@ -25,6 +25,8 @@ const restrictToHorizontalAxis: Modifier = ({ transform }) => ({
|
||||
y: 0,
|
||||
});
|
||||
|
||||
const DND_MODIFIERS: Modifier[] = [restrictToHorizontalAxis];
|
||||
|
||||
function SortableItem<T>({
|
||||
id,
|
||||
item,
|
||||
@@ -183,7 +185,10 @@ export function SortableInlineList<T>({
|
||||
[clearDragState, disabled, items, keyExtractor, onDragEnd],
|
||||
);
|
||||
|
||||
const ids = items.map((item, index) => keyExtractor(item, index));
|
||||
const ids = useMemo(
|
||||
() => items.map((item, index) => keyExtractor(item, index)),
|
||||
[items, keyExtractor],
|
||||
);
|
||||
|
||||
const renderedItems = (
|
||||
<SortableContext items={ids} strategy={horizontalListSortingStrategy}>
|
||||
@@ -215,7 +220,7 @@ export function SortableInlineList<T>({
|
||||
<DndContext
|
||||
sensors={sensors}
|
||||
collisionDetection={closestCenter}
|
||||
modifiers={[restrictToHorizontalAxis]}
|
||||
modifiers={DND_MODIFIERS}
|
||||
onDragStart={handleDragStart}
|
||||
onDragCancel={clearDragState}
|
||||
onDragEnd={handleDragEnd}
|
||||
|
||||
@@ -7,7 +7,7 @@ import Animated, {
|
||||
withRepeat,
|
||||
withTiming,
|
||||
} from "react-native-reanimated";
|
||||
import { useEffect } from "react";
|
||||
import { useEffect, useMemo } from "react";
|
||||
|
||||
const SYNCED_LOADER_DURATION_MS = 950;
|
||||
const SYNCED_LOADER_EPOCH_MS = 0;
|
||||
@@ -66,6 +66,11 @@ export function SyncedLoader({ size = 10, color }: { size?: number; color: strin
|
||||
const gridWidth = dotSize * 2 + gap;
|
||||
const gridHeight = dotSize * 3 + gap * 2;
|
||||
|
||||
const gridStyle = useMemo(
|
||||
() => [animatedStyle, { width: gridWidth, height: gridHeight }],
|
||||
[animatedStyle, gridWidth, gridHeight],
|
||||
);
|
||||
|
||||
return (
|
||||
<View
|
||||
style={{
|
||||
@@ -75,15 +80,7 @@ export function SyncedLoader({ size = 10, color }: { size?: number; color: strin
|
||||
justifyContent: "center",
|
||||
}}
|
||||
>
|
||||
<Animated.View
|
||||
style={[
|
||||
animatedStyle,
|
||||
{
|
||||
width: gridWidth,
|
||||
height: gridHeight,
|
||||
},
|
||||
]}
|
||||
>
|
||||
<Animated.View style={gridStyle}>
|
||||
{Array.from({ length: DOT_COUNT }).map((_, dotIndex) => {
|
||||
const rowIndex = Math.floor(dotIndex / GRID_COLUMNS);
|
||||
const columnIndex = dotIndex % GRID_COLUMNS;
|
||||
@@ -144,18 +141,19 @@ function SpinnerDot({
|
||||
};
|
||||
});
|
||||
|
||||
return (
|
||||
<Animated.View
|
||||
style={[
|
||||
animatedStyle,
|
||||
{
|
||||
width: dotSize,
|
||||
height: dotSize,
|
||||
borderRadius: dotSize / 2,
|
||||
backgroundColor: color,
|
||||
},
|
||||
style,
|
||||
]}
|
||||
/>
|
||||
const dotStyle = useMemo(
|
||||
() => [
|
||||
animatedStyle,
|
||||
{
|
||||
width: dotSize,
|
||||
height: dotSize,
|
||||
borderRadius: dotSize / 2,
|
||||
backgroundColor: color,
|
||||
},
|
||||
style,
|
||||
],
|
||||
[animatedStyle, dotSize, color, style],
|
||||
);
|
||||
|
||||
return <Animated.View style={dotStyle} />;
|
||||
}
|
||||
|
||||
@@ -543,6 +543,11 @@ export function TerminalPane({
|
||||
],
|
||||
);
|
||||
|
||||
const containerStyle = useMemo(
|
||||
() => [styles.container, keyboardPaddingStyle],
|
||||
[keyboardPaddingStyle],
|
||||
);
|
||||
|
||||
if (!client || !isConnected) {
|
||||
return (
|
||||
<View style={styles.centerState}>
|
||||
@@ -552,7 +557,7 @@ export function TerminalPane({
|
||||
}
|
||||
|
||||
return (
|
||||
<Animated.View style={[styles.container, keyboardPaddingStyle]}>
|
||||
<Animated.View style={containerStyle}>
|
||||
<View style={styles.outputContainer}>
|
||||
{isWorkspaceFocused ? (
|
||||
<View style={styles.terminalGestureContainer}>
|
||||
|
||||
@@ -212,10 +212,6 @@ export function ToastViewport({
|
||||
};
|
||||
}, [clearTimer, opacity, scheduleDismiss, toast, translateY]);
|
||||
|
||||
if (!toast) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const headerHeight = isMobile ? HEADER_INNER_HEIGHT_MOBILE : HEADER_INNER_HEIGHT;
|
||||
const headerTopPadding = isMobile ? HEADER_TOP_PADDING_MOBILE : 0;
|
||||
const topOffset =
|
||||
@@ -223,6 +219,29 @@ export function ToastViewport({
|
||||
? insets.top + headerTopPadding + headerHeight + theme.spacing[2]
|
||||
: theme.spacing[3];
|
||||
|
||||
const toastVariant = toast?.variant;
|
||||
const toastAnimatedStyle = useMemo(
|
||||
() => [
|
||||
styles.toast,
|
||||
toastVariant === "success" ? styles.toastSuccess : null,
|
||||
toastVariant === "error" ? styles.toastError : null,
|
||||
{
|
||||
marginTop: topOffset,
|
||||
opacity,
|
||||
transform: [{ translateY }],
|
||||
},
|
||||
],
|
||||
[toastVariant, topOffset, opacity, translateY],
|
||||
);
|
||||
const toastMessageStyle = useMemo(
|
||||
() => [styles.message, toastVariant === "error" ? styles.messageError : null],
|
||||
[toastVariant],
|
||||
);
|
||||
|
||||
if (!toast) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const icon =
|
||||
toast.icon ??
|
||||
(toast.variant === "success" ? (
|
||||
@@ -237,24 +256,12 @@ export function ToastViewport({
|
||||
testID={toast.testID ?? "app-toast"}
|
||||
onPointerEnter={isWeb ? pauseDismiss : undefined}
|
||||
onPointerLeave={isWeb ? resumeDismiss : undefined}
|
||||
style={[
|
||||
styles.toast,
|
||||
toast.variant === "success" ? styles.toastSuccess : null,
|
||||
toast.variant === "error" ? styles.toastError : null,
|
||||
{
|
||||
marginTop: topOffset,
|
||||
opacity,
|
||||
transform: [{ translateY }],
|
||||
},
|
||||
]}
|
||||
style={toastAnimatedStyle}
|
||||
accessibilityRole="alert"
|
||||
>
|
||||
{icon ? <View style={styles.iconSlot}>{icon}</View> : null}
|
||||
{typeof toast.content === "string" ? (
|
||||
<Text
|
||||
testID="app-toast-message"
|
||||
style={[styles.message, toast.variant === "error" ? styles.messageError : null]}
|
||||
>
|
||||
<Text testID="app-toast-message" style={toastMessageStyle}>
|
||||
{toast.content}
|
||||
</Text>
|
||||
) : (
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import type { ReactNode } from "react";
|
||||
import { useMemo, type ReactNode } from "react";
|
||||
import { Pressable, Text, View } from "react-native";
|
||||
import type { StyleProp, ViewStyle } from "react-native";
|
||||
import type { StyleProp, TextStyle, ViewStyle } from "react-native";
|
||||
import { StyleSheet, useUnistyles } from "react-native-unistyles";
|
||||
|
||||
type SegmentedControlSize = "sm" | "md";
|
||||
@@ -40,53 +40,92 @@ export function SegmentedControl<T extends string>({
|
||||
const labelSizeStyle = size === "sm" ? styles.labelSm : styles.labelMd;
|
||||
const iconSize = size === "sm" ? theme.iconSize.sm : theme.iconSize.md;
|
||||
|
||||
const containerStyle = useMemo(
|
||||
() => [styles.container, containerSizeStyle, style],
|
||||
[containerSizeStyle, style],
|
||||
);
|
||||
|
||||
return (
|
||||
<View style={[styles.container, containerSizeStyle, style]} testID={testID}>
|
||||
<View style={containerStyle} testID={testID}>
|
||||
{options.map((option) => {
|
||||
const isSelected = option.value === value;
|
||||
const iconColor = isSelected ? theme.colors.foreground : theme.colors.foregroundMuted;
|
||||
|
||||
return (
|
||||
<Pressable
|
||||
<SegmentItem
|
||||
key={option.value}
|
||||
accessibilityRole="button"
|
||||
accessibilityState={{ selected: isSelected, disabled: option.disabled }}
|
||||
disabled={option.disabled}
|
||||
testID={option.testID}
|
||||
option={option}
|
||||
isSelected={isSelected}
|
||||
iconColor={iconColor}
|
||||
iconSize={iconSize}
|
||||
hideLabels={hideLabels}
|
||||
segmentSizeStyle={segmentSizeStyle}
|
||||
labelSizeStyle={labelSizeStyle}
|
||||
onPress={() => {
|
||||
if (!option.disabled && option.value !== value) {
|
||||
onValueChange(option.value);
|
||||
}
|
||||
}}
|
||||
style={({ hovered, pressed }) => [
|
||||
styles.segment,
|
||||
segmentSizeStyle,
|
||||
isSelected && styles.segmentSelected,
|
||||
hovered && !isSelected && styles.segmentHover,
|
||||
pressed && !isSelected && styles.segmentPressed,
|
||||
option.disabled && styles.segmentDisabled,
|
||||
]}
|
||||
>
|
||||
{option.icon ? (
|
||||
<View style={styles.iconContainer}>
|
||||
{option.icon({ color: iconColor, size: iconSize })}
|
||||
</View>
|
||||
) : null}
|
||||
{hideLabels ? null : (
|
||||
<Text
|
||||
style={[styles.label, labelSizeStyle, isSelected && styles.labelSelected]}
|
||||
numberOfLines={1}
|
||||
>
|
||||
{option.label}
|
||||
</Text>
|
||||
)}
|
||||
</Pressable>
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
function SegmentItem<T extends string>({
|
||||
option,
|
||||
isSelected,
|
||||
iconColor,
|
||||
iconSize,
|
||||
hideLabels,
|
||||
segmentSizeStyle,
|
||||
labelSizeStyle,
|
||||
onPress,
|
||||
}: {
|
||||
option: SegmentedControlOption<T>;
|
||||
isSelected: boolean;
|
||||
iconColor: string;
|
||||
iconSize: number;
|
||||
hideLabels: boolean;
|
||||
segmentSizeStyle: StyleProp<ViewStyle>;
|
||||
labelSizeStyle: StyleProp<TextStyle>;
|
||||
onPress: () => void;
|
||||
}) {
|
||||
const labelStyle = useMemo(
|
||||
() => [styles.label, labelSizeStyle, isSelected && styles.labelSelected],
|
||||
[labelSizeStyle, isSelected],
|
||||
);
|
||||
return (
|
||||
<Pressable
|
||||
accessibilityRole="button"
|
||||
accessibilityState={{ selected: isSelected, disabled: option.disabled }}
|
||||
disabled={option.disabled}
|
||||
testID={option.testID}
|
||||
onPress={onPress}
|
||||
style={({ hovered, pressed }) => [
|
||||
styles.segment,
|
||||
segmentSizeStyle,
|
||||
isSelected && styles.segmentSelected,
|
||||
hovered && !isSelected && styles.segmentHover,
|
||||
pressed && !isSelected && styles.segmentPressed,
|
||||
option.disabled && styles.segmentDisabled,
|
||||
]}
|
||||
>
|
||||
{option.icon ? (
|
||||
<View style={styles.iconContainer}>
|
||||
{option.icon({ color: iconColor, size: iconSize })}
|
||||
</View>
|
||||
) : null}
|
||||
{hideLabels ? null : (
|
||||
<Text style={labelStyle} numberOfLines={1}>
|
||||
{option.label}
|
||||
</Text>
|
||||
)}
|
||||
</Pressable>
|
||||
);
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create((theme) => ({
|
||||
container: {
|
||||
flexDirection: "row",
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { useMemo } from "react";
|
||||
import { View, Text } from "react-native";
|
||||
import { StyleSheet, useUnistyles } from "react-native-unistyles";
|
||||
import { StyleSheet } from "react-native-unistyles";
|
||||
|
||||
type StatusBadgeVariant = "success" | "error" | "muted";
|
||||
|
||||
@@ -9,25 +10,26 @@ interface StatusBadgeProps {
|
||||
}
|
||||
|
||||
export function StatusBadge({ label, variant = "muted" }: StatusBadgeProps) {
|
||||
const { theme } = useUnistyles();
|
||||
const pillStyle = useMemo(
|
||||
() => [
|
||||
styles.pill,
|
||||
variant === "success" && styles.pillSuccess,
|
||||
variant === "error" && styles.pillError,
|
||||
],
|
||||
[variant],
|
||||
);
|
||||
const textStyle = useMemo(
|
||||
() => [
|
||||
styles.pillText,
|
||||
variant === "success" && styles.pillTextSuccess,
|
||||
variant === "error" && styles.pillTextError,
|
||||
],
|
||||
[variant],
|
||||
);
|
||||
|
||||
return (
|
||||
<View
|
||||
style={[
|
||||
styles.pill,
|
||||
variant === "success" && styles.pillSuccess,
|
||||
variant === "error" && styles.pillError,
|
||||
]}
|
||||
>
|
||||
<Text
|
||||
style={[
|
||||
styles.pillText,
|
||||
variant === "success" && styles.pillTextSuccess,
|
||||
variant === "error" && styles.pillTextError,
|
||||
]}
|
||||
>
|
||||
{label}
|
||||
</Text>
|
||||
<View style={pillStyle}>
|
||||
<Text style={textStyle}>{label}</Text>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -480,6 +480,20 @@ export function TooltipContent({
|
||||
[],
|
||||
);
|
||||
|
||||
const contentStyle = useMemo(
|
||||
() => [
|
||||
styles.content,
|
||||
{ maxWidth },
|
||||
style,
|
||||
{
|
||||
position: "absolute" as const,
|
||||
top: position?.y ?? -9999,
|
||||
left: position?.x ?? -9999,
|
||||
},
|
||||
],
|
||||
[maxWidth, style, position?.x, position?.y],
|
||||
);
|
||||
|
||||
if (!ctx.open || !ctx.enabled) return null;
|
||||
|
||||
// On web, avoid React Native's <Modal/> implementation (it uses <dialog> and can
|
||||
@@ -496,16 +510,7 @@ export function TooltipContent({
|
||||
collapsable={false}
|
||||
testID={testID}
|
||||
onLayout={handleLayout}
|
||||
style={[
|
||||
styles.content,
|
||||
{ maxWidth },
|
||||
style,
|
||||
{
|
||||
position: "absolute",
|
||||
top: position?.y ?? -9999,
|
||||
left: position?.x ?? -9999,
|
||||
},
|
||||
]}
|
||||
style={contentStyle}
|
||||
>
|
||||
{children}
|
||||
</Animated.View>
|
||||
@@ -530,16 +535,7 @@ export function TooltipContent({
|
||||
collapsable={false}
|
||||
testID={testID}
|
||||
onLayout={handleLayout}
|
||||
style={[
|
||||
styles.content,
|
||||
{ maxWidth },
|
||||
style,
|
||||
{
|
||||
position: "absolute",
|
||||
top: position?.y ?? -9999,
|
||||
left: position?.x ?? -9999,
|
||||
},
|
||||
]}
|
||||
style={contentStyle}
|
||||
>
|
||||
{children}
|
||||
</Animated.View>
|
||||
|
||||
@@ -357,26 +357,51 @@ export function WebDesktopScrollbarOverlay({
|
||||
);
|
||||
const handleInsetTop = Math.max(0, (thumbRegionHeight - geometry.handleSize) / 2);
|
||||
|
||||
const thumbRegionStyle = useMemo(
|
||||
() => [
|
||||
styles.thumbRegion,
|
||||
{
|
||||
top: 0,
|
||||
height: thumbRegionHeight,
|
||||
transform: [{ translateY: thumbRegionOffset }],
|
||||
},
|
||||
platformIsWeb &&
|
||||
({
|
||||
cursor: handleCursor,
|
||||
touchAction: "none",
|
||||
userSelect: "none",
|
||||
transitionProperty: "transform",
|
||||
transitionDuration: `${handleTravelDurationMs}ms`,
|
||||
transitionTimingFunction: "linear",
|
||||
} as any),
|
||||
],
|
||||
[thumbRegionHeight, thumbRegionOffset, handleCursor, handleTravelDurationMs],
|
||||
);
|
||||
|
||||
const handleStyle = useMemo(
|
||||
() => [
|
||||
styles.handle,
|
||||
{
|
||||
marginTop: handleInsetTop,
|
||||
height: geometry.handleSize,
|
||||
width: handleWidth,
|
||||
backgroundColor: handleColor,
|
||||
opacity: handleOpacity,
|
||||
},
|
||||
platformIsWeb &&
|
||||
({
|
||||
transitionProperty: "opacity, width, background-color",
|
||||
transitionDuration: `${HANDLE_FADE_DURATION_MS}ms, ${HANDLE_WIDTH_TRANSITION_DURATION_MS}ms, ${HANDLE_FADE_DURATION_MS}ms`,
|
||||
transitionTimingFunction: "ease-out, cubic-bezier(0.22, 0.75, 0.2, 1), ease-out",
|
||||
} as any),
|
||||
],
|
||||
[handleInsetTop, geometry.handleSize, handleWidth, handleColor, handleOpacity],
|
||||
);
|
||||
|
||||
return (
|
||||
<View style={styles.overlay} pointerEvents="box-none">
|
||||
<View
|
||||
style={[
|
||||
styles.thumbRegion,
|
||||
{
|
||||
top: 0,
|
||||
height: thumbRegionHeight,
|
||||
transform: [{ translateY: thumbRegionOffset }],
|
||||
},
|
||||
platformIsWeb &&
|
||||
({
|
||||
cursor: handleCursor,
|
||||
touchAction: "none",
|
||||
userSelect: "none",
|
||||
transitionProperty: "transform",
|
||||
transitionDuration: `${handleTravelDurationMs}ms`,
|
||||
transitionTimingFunction: "linear",
|
||||
} as any),
|
||||
]}
|
||||
style={thumbRegionStyle}
|
||||
pointerEvents={handleVisible ? "auto" : "none"}
|
||||
{...(panResponder?.panHandlers ?? {})}
|
||||
{...(platformIsWeb
|
||||
@@ -389,25 +414,7 @@ export function WebDesktopScrollbarOverlay({
|
||||
} as any)
|
||||
: null)}
|
||||
>
|
||||
<View
|
||||
style={[
|
||||
styles.handle,
|
||||
{
|
||||
marginTop: handleInsetTop,
|
||||
height: geometry.handleSize,
|
||||
width: handleWidth,
|
||||
backgroundColor: handleColor,
|
||||
opacity: handleOpacity,
|
||||
},
|
||||
platformIsWeb &&
|
||||
({
|
||||
transitionProperty: "opacity, width, background-color",
|
||||
transitionDuration: `${HANDLE_FADE_DURATION_MS}ms, ${HANDLE_WIDTH_TRANSITION_DURATION_MS}ms, ${HANDLE_FADE_DURATION_MS}ms`,
|
||||
transitionTimingFunction: "ease-out, cubic-bezier(0.22, 0.75, 0.2, 1), ease-out",
|
||||
} as any),
|
||||
]}
|
||||
pointerEvents="none"
|
||||
/>
|
||||
<View style={handleStyle} pointerEvents="none" />
|
||||
</View>
|
||||
</View>
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user