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
@@ -8,11 +8,20 @@ import {
type ReactElement,
type ReactNode,
} from "react";
import { Dimensions, Platform, Text, View } from "react-native";
import {
Dimensions,
Platform,
Text,
View,
type StyleProp,
type TextStyle,
type ViewStyle,
} from "react-native";
import Animated, { FadeIn, FadeOut } from "react-native-reanimated";
import { StyleSheet, useUnistyles } from "react-native-unistyles";
import { StyleSheet, withUnistyles } from "react-native-unistyles";
import { ExternalLink } from "lucide-react-native";
import { GitHubIcon } from "@/components/icons/github-icon";
import type { Theme } from "@/styles/theme";
import { DiffStat } from "@/components/diff-stat";
import { Pressable } from "react-native";
import { Portal } from "@gorhom/portal";
@@ -207,7 +216,6 @@ function WorkspaceHoverCardContent({
triggerRef: React.RefObject<View | null>;
contentRef: React.RefObject<View | null>;
}): ReactElement | null {
const { theme } = useUnistyles();
const bottomSheetInternal = useBottomSheetModalInternal(true);
const [triggerRect, setTriggerRect] = useState<Rect | null>(null);
const [contentSize, setContentSize] = useState<{ width: number; height: number } | null>(null);
@@ -297,7 +305,7 @@ function WorkspaceHoverCardContent({
{prHint?.checks && prHint.checks.length > 0 ? (
<>
<View style={styles.separator} />
<ChecksSummaryPressable checks={prHint.checks} url={prHint.url} theme={theme} />
<ChecksSummaryPressable checks={prHint.checks} url={prHint.url} />
</>
) : null}
</Animated.View>
@@ -306,49 +314,49 @@ function WorkspaceHoverCardContent({
);
}
const ThemedExternalLink = withUnistyles(ExternalLink);
const ThemedGitHubIcon = withUnistyles(GitHubIcon);
const foregroundColorMapping = (theme: Theme) => ({ color: theme.colors.foreground });
const foregroundMutedColorMapping = (theme: Theme) => ({ color: theme.colors.foregroundMuted });
function ChecksSummaryContent({
checks,
theme,
hovered,
}: {
checks: NonNullable<PrHint["checks"]>;
theme: ReturnType<typeof useUnistyles>["theme"];
hovered: boolean;
}) {
const failed = checks.filter((c) => c.status === "failure").length;
const pending = checks.filter((c) => c.status === "pending").length;
let badgeColor: string;
let badgeLabel: string;
let dotStyle: StyleProp<ViewStyle>;
let statusTextStyle: StyleProp<TextStyle>;
if (failed > 0) {
badgeColor = theme.colors.palette.red[500];
badgeLabel = `${failed} failed`;
dotStyle = styles.checksDotFailed;
statusTextStyle = styles.checksStatusTextFailed;
} else if (pending > 0) {
badgeColor = theme.colors.palette.amber[500];
badgeLabel = `${pending} running`;
dotStyle = styles.checksDotPending;
statusTextStyle = styles.checksStatusTextPending;
} else {
badgeColor = theme.colors.palette.green[500];
badgeLabel = `${checks.length} passed`;
dotStyle = styles.checksDotPassed;
statusTextStyle = styles.checksStatusTextPassed;
}
const iconColor = hovered ? theme.colors.foreground : theme.colors.foregroundMuted;
const labelStyle = useMemo(
() => [styles.checksSummaryLabel, hovered && styles.checksSummaryLabelHovered],
[hovered],
);
const dotStyle = useMemo(() => [styles.checksDot, { backgroundColor: badgeColor }], [badgeColor]);
const statusTextStyle = useMemo(
() => [styles.checksStatusText, { color: badgeColor }],
[badgeColor],
);
const labelStyle = hovered ? checksSummaryLabelHoveredCombined : styles.checksSummaryLabel;
const iconUniProps = hovered ? foregroundColorMapping : foregroundMutedColorMapping;
return (
<>
{hovered ? (
<ExternalLink size={12} color={iconColor} />
<ThemedExternalLink size={12} uniProps={iconUniProps} />
) : (
<GitHubIcon size={12} color={iconColor} />
<ThemedGitHubIcon size={12} uniProps={iconUniProps} />
)}
<Text style={labelStyle}>Checks</Text>
<View style={styles.checksSummaryCounts}>
@@ -362,38 +370,32 @@ function ChecksSummaryContent({
function ChecksSummaryPressable({
checks,
url,
theme,
}: {
checks: NonNullable<PrHint["checks"]>;
url: string;
theme: ReturnType<typeof useUnistyles>["theme"];
}) {
const handlePress = useCallback(() => {
void openExternalUrl(`${url}/checks`);
}, [url]);
const pressableStyle = useCallback(
({ hovered }: { pressed: boolean; hovered?: boolean }) => [
styles.checksSummaryRow,
Boolean(hovered) && styles.listRowHovered,
],
[],
);
const renderChildren = useCallback(
({ hovered }: { pressed: boolean; hovered?: boolean }) => (
<ChecksSummaryContent checks={checks} theme={theme} hovered={Boolean(hovered)} />
<ChecksSummaryContent checks={checks} hovered={Boolean(hovered)} />
),
[checks, theme],
[checks],
);
return (
<Pressable style={pressableStyle} onPress={handlePress}>
<Pressable style={checksSummaryPressableStyle} onPress={handlePress}>
{renderChildren}
</Pressable>
);
}
function checksSummaryPressableStyle({ hovered = false }: { pressed: boolean; hovered?: boolean }) {
return [styles.checksSummaryRow, hovered && styles.listRowHovered];
}
const styles = StyleSheet.create((theme) => ({
portalOverlay: {
position: "absolute",
@@ -467,13 +469,42 @@ const styles = StyleSheet.create((theme) => ({
flex: 1,
justifyContent: "flex-end",
},
checksDot: {
checksDotFailed: {
width: 6,
height: 6,
borderRadius: 3,
backgroundColor: theme.colors.palette.red[500],
},
checksStatusText: {
checksDotPending: {
width: 6,
height: 6,
borderRadius: 3,
backgroundColor: theme.colors.palette.amber[500],
},
checksDotPassed: {
width: 6,
height: 6,
borderRadius: 3,
backgroundColor: theme.colors.palette.green[500],
},
checksStatusTextFailed: {
fontSize: theme.fontSize.xs,
fontWeight: theme.fontWeight.normal,
color: theme.colors.palette.red[500],
},
checksStatusTextPending: {
fontSize: theme.fontSize.xs,
fontWeight: theme.fontWeight.normal,
color: theme.colors.palette.amber[500],
},
checksStatusTextPassed: {
fontSize: theme.fontSize.xs,
fontWeight: theme.fontWeight.normal,
color: theme.colors.palette.green[500],
},
}));
const checksSummaryLabelHoveredCombined = [
styles.checksSummaryLabel,
styles.checksSummaryLabelHovered,
];