refactor: strip useUnistyles in favor of withUnistyles + StyleSheet

Replaces broad useUnistyles() subscriptions in composer, workspace, and
related components with withUnistyles wrappers and StyleSheet.create so
re-renders stay scoped to the tokens actually consumed. Lifts the design
scales (spacing, iconSize, fontSize, etc.) to top-level exports in
theme.ts so component literals share a single source of truth with the
unistyles theme.
This commit is contained in:
Mohamed Boudra
2026-04-26 16:12:09 +07:00
parent 941b4fb760
commit 01f89c7c36
22 changed files with 1014 additions and 720 deletions
+1 -1
View File
@@ -108,7 +108,7 @@ vi.mock("react-native-unistyles", () => {
StyleSheet: {
create: createPanelTestStyles,
},
useUnistyles: () => ({ theme }),
withUnistyles: <T,>(component: T) => component,
};
});
+16 -10
View File
@@ -3,7 +3,7 @@ import React, { useCallback, useEffect, useMemo, useRef, useState } from "react"
import { ActivityIndicator, Text, View } from "react-native";
import ReanimatedAnimated from "react-native-reanimated";
import { useSafeAreaInsets } from "react-native-safe-area-context";
import { StyleSheet, useUnistyles } from "react-native-unistyles";
import { StyleSheet, withUnistyles } from "react-native-unistyles";
import invariant from "tiny-invariant";
import { shallow, useShallow } from "zustand/shallow";
import { useStoreWithEqualityFn } from "zustand/traditional";
@@ -44,6 +44,7 @@ import {
import { useCreateFlowStore } from "@/stores/create-flow-store";
import { buildDraftStoreKey } from "@/stores/draft-keys";
import { type Agent, useSessionStore } from "@/stores/session-store";
import type { Theme } from "@/styles/theme";
import type { PendingPermission } from "@/types/shared";
import type { StreamItem } from "@/types/stream";
import { getInitDeferred, getInitKey } from "@/utils/agent-initialization";
@@ -125,9 +126,8 @@ function buildChatAgentFromState(
function renderChatAgentNonReadyView(args: {
viewState: AgentScreenViewState;
effectiveAgent: AgentScreenAgent | null;
theme: ReturnType<typeof useUnistyles>["theme"];
}): React.ReactElement | null {
const { viewState, effectiveAgent, theme } = args;
const { viewState, effectiveAgent } = args;
if (viewState.tag === "not_found") {
return (
<View style={styles.container} testID="agent-not-found">
@@ -151,7 +151,7 @@ function renderChatAgentNonReadyView(args: {
return (
<View style={styles.container} testID="agent-loading">
<View style={styles.errorContainer}>
<ActivityIndicator size="large" color={theme.colors.foregroundMuted} />
<ThemedActivityIndicator size="large" uniProps={foregroundMutedColorMapping} />
</View>
</View>
);
@@ -419,7 +419,6 @@ function AgentPanelBody({
connectionStatus: HostRuntimeConnectionStatus;
onOpenWorkspaceFile?: (input: { filePath: string }) => void;
}) {
const { theme } = useUnistyles();
const { isArchivingAgent: _isArchivingAgent } = useArchiveAgent();
const hasSession = useSessionStore((state) => Boolean(state.sessions[serverId]));
const projectPlacement = useStoreWithEqualityFn(
@@ -549,7 +548,7 @@ function AgentPanelBody({
return (
<View style={styles.container} testID="agent-loading">
<View style={styles.errorContainer}>
<ActivityIndicator size="large" color={theme.colors.foregroundMuted} />
<ThemedActivityIndicator size="large" uniProps={foregroundMutedColorMapping} />
</View>
</View>
);
@@ -585,7 +584,6 @@ function ChatAgentContent({
connectionStatus: HostRuntimeConnectionStatus;
onOpenWorkspaceFile?: (input: { filePath: string }) => void;
}) {
const { theme } = useUnistyles();
const panelToast = useToastHost();
const { isArchivingAgent } = useArchiveAgent();
const streamViewRef = useRef<AgentStreamViewHandle>(null);
@@ -945,7 +943,6 @@ function ChatAgentContent({
const nonReadyView = renderChatAgentNonReadyView({
viewState,
effectiveAgent,
theme,
});
if (nonReadyView) return nonReadyView;
invariant(effectiveAgent, "effectiveAgent is defined when the non-ready view is absent");
@@ -990,7 +987,7 @@ function ChatAgentContent({
viewState.sync.status === "catching_up" &&
viewState.sync.ui === "overlay" ? (
<View style={styles.historySyncOverlay} testID="agent-history-overlay">
<ActivityIndicator size="large" color={theme.colors.foregroundMuted} />
<ThemedActivityIndicator size="large" uniProps={foregroundMutedColorMapping} />
</View>
) : null}
@@ -1004,7 +1001,7 @@ function ChatAgentContent({
{isArchivingCurrentAgent ? (
<View style={styles.archivingOverlay} testID="agent-archiving-overlay">
<ActivityIndicator size="large" color={theme.colors.foreground} />
<ThemedActivityIndicator size="large" uniProps={foregroundColorMapping} />
<Text style={styles.archivingTitle}>Archiving agent...</Text>
<Text style={styles.archivingSubtitle}>Please wait while we archive this agent.</Text>
</View>
@@ -1340,6 +1337,15 @@ function AgentSessionUnavailableState({
);
}
const ThemedActivityIndicator = withUnistyles(ActivityIndicator);
const foregroundMutedColorMapping = (theme: Theme) => ({
color: theme.colors.foregroundMuted,
});
const foregroundColorMapping = (theme: Theme) => ({
color: theme.colors.foreground,
});
const styles = StyleSheet.create((theme) => ({
root: {
flex: 1,
+29 -14
View File
@@ -9,11 +9,12 @@ import {
View,
} from "react-native";
import invariant from "tiny-invariant";
import { StyleSheet, useUnistyles } from "react-native-unistyles";
import { StyleSheet, withUnistyles } from "react-native-unistyles";
import { Fonts } from "@/constants/theme";
import { usePaneContext } from "@/panels/pane-context";
import type { PanelDescriptor, PanelRegistration } from "@/panels/panel-registry";
import { buildWorkspaceTabPersistenceKey } from "@/stores/workspace-tabs-store";
import type { Theme } from "@/styles/theme";
import {
useWorkspaceSetupStore,
type WorkspaceSetupSnapshot,
@@ -62,15 +63,13 @@ function useSetupPanelDescriptor(
type CommandStatus = "running" | "completed" | "failed";
function CommandStatusIcon({ status }: { status: CommandStatus }) {
const { theme } = useUnistyles();
if (status === "running") {
return <ActivityIndicator size={14} color={theme.colors.foreground} />;
return <ThemedActivityIndicator size={14} uniProps={foregroundColorMapping} />;
}
if (status === "completed") {
return <CheckCircle2 size={14} color={theme.colors.palette.green[500]} />;
return <ThemedCheckCircle2 size={14} uniProps={greenColorMapping} />;
}
return <CircleAlert size={14} color={theme.colors.palette.red[500]} />;
return <ThemedCircleAlert size={14} uniProps={redColorMapping} />;
}
function formatDuration(ms: number): string {
@@ -151,7 +150,6 @@ function buildCommandRowState(args: BuildCommandRowPropsArgs) {
}
function SetupPanel() {
const { theme } = useUnistyles();
const { serverId, target } = usePaneContext();
invariant(target.kind === "setup", "SetupPanel requires setup target");
@@ -231,7 +229,7 @@ function SetupPanel() {
{isWaiting ? (
<View style={styles.waitingContainer}>
<ActivityIndicator size="large" color={theme.colors.foregroundMuted} />
<ThemedActivityIndicator size="large" uniProps={foregroundMutedColorMapping} />
<Text style={styles.waitingText}>Setting up workspace...</Text>
</View>
) : null}
@@ -269,7 +267,6 @@ function SetupPanel() {
hasError={rowState.hasError}
processedLog={rowState.processedLog}
errorMessage={snapshot?.error ?? null}
foregroundMutedColor={theme.colors.foregroundMuted}
onToggle={toggleExpanded}
/>
);
@@ -292,7 +289,6 @@ interface SetupCommandRowProps {
hasError: boolean;
processedLog: string;
errorMessage: string | null;
foregroundMutedColor: string;
onToggle: (index: number, isAutoExpanded: boolean) => void;
}
@@ -305,7 +301,6 @@ function SetupCommandRow({
hasError,
processedLog,
errorMessage,
foregroundMutedColor,
onToggle,
}: SetupCommandRowProps) {
const handlePress = useCallback(() => {
@@ -341,7 +336,7 @@ function SetupCommandRow({
{command.durationMs != null ? (
<Text style={styles.commandDuration}>{formatDuration(command.durationMs)}</Text>
) : null}
<SetupCommandChevron showDetail={showDetail} color={foregroundMutedColor} />
<SetupCommandChevron showDetail={showDetail} />
</Pressable>
{showDetail ? (
<View style={styles.commandDetail}>
@@ -388,12 +383,14 @@ export const setupPanelRegistration: PanelRegistration<"setup"> = {
useDescriptor: useSetupPanelDescriptor,
};
function SetupCommandChevron({ showDetail, color }: { showDetail: boolean; color: string }) {
function SetupCommandChevron({ showDetail }: { showDetail: boolean }) {
const chevronStyle = useMemo(
() => [styles.chevron, showDetail && styles.chevronExpanded],
[showDetail],
);
return <ChevronRight size={14} color={color} style={chevronStyle} />;
return (
<ThemedChevronRight size={14} uniProps={foregroundMutedColorMapping} style={chevronStyle} />
);
}
function StandaloneLogView({ commands, log }: { commands: SetupCommand[]; log: string }) {
@@ -432,6 +429,24 @@ function TopLevelSetupError({
);
}
const ThemedActivityIndicator = withUnistyles(ActivityIndicator);
const ThemedCheckCircle2 = withUnistyles(CheckCircle2);
const ThemedCircleAlert = withUnistyles(CircleAlert);
const ThemedChevronRight = withUnistyles(ChevronRight);
const foregroundColorMapping = (theme: Theme) => ({
color: theme.colors.foreground,
});
const foregroundMutedColorMapping = (theme: Theme) => ({
color: theme.colors.foregroundMuted,
});
const greenColorMapping = (theme: Theme) => ({
color: theme.colors.palette.green[500],
});
const redColorMapping = (theme: Theme) => ({
color: theme.colors.palette.red[500],
});
const styles = StyleSheet.create((theme) => ({
container: {
flex: 1,