fix(app): preserve persisted theme styles after startup

Module-level style composites could materialize the temporary adaptive theme before persisted settings loaded. Keep Unistyles reads in render and guard against eager module-scope access.
This commit is contained in:
Mohamed Boudra
2026-07-18 17:45:02 +02:00
parent c0622a7046
commit e0e50c9a8e
31 changed files with 188 additions and 133 deletions
@@ -230,7 +230,6 @@ const styles = StyleSheet.create((theme) => ({
},
}));
const SEARCH_INPUT_STYLE = [styles.searchInput, isWeb && { outlineStyle: "none" }];
const WEB_EXIT_DURATION_MS = 160;
function SheetBackground({ style }: BottomSheetBackgroundProps) {
@@ -373,7 +372,7 @@ export function SheetHeaderView({
<Search size={theme.iconSize.md} color={theme.colors.foregroundMuted} />
<AdaptiveTextInput
// @ts-expect-error - outlineStyle is web-only
style={SEARCH_INPUT_STYLE}
style={[styles.searchInput, isWeb && { outlineStyle: "none" }]}
placeholder={search.placeholder ?? t("common.actions.search")}
resetKey={search.resetKey}
onChangeText={handleSearchChange}
@@ -430,7 +429,7 @@ export function InlineHeaderView({ header }: { header: SheetHeader }) {
<Search size={theme.iconSize.sm} color={theme.colors.foregroundMuted} />
<AdaptiveTextInput
// @ts-expect-error - outlineStyle is web-only
style={SEARCH_INPUT_STYLE}
style={[styles.searchInput, isWeb && { outlineStyle: "none" }]}
placeholder={header.search.placeholder ?? t("common.actions.search")}
resetKey={header.search.resetKey}
onChangeText={header.search.onChange}
+2 -5
View File
@@ -544,7 +544,7 @@ export function AgentList({
</Text>
<View style={styles.sheetButtonRow}>
<Pressable
style={SHEET_CANCEL_BUTTON_STYLE}
style={[styles.sheetButton, styles.sheetCancelButton]}
onPress={handleCloseActionSheet}
testID="agent-action-cancel"
>
@@ -552,7 +552,7 @@ export function AgentList({
</Pressable>
<Pressable
disabled={isActionDaemonUnavailable}
style={SHEET_ARCHIVE_BUTTON_STYLE}
style={[styles.sheetButton, styles.sheetArchiveButton]}
onPress={handleArchiveAgent}
testID="agent-action-archive"
>
@@ -789,6 +789,3 @@ const styles = StyleSheet.create((theme) => ({
fontSize: theme.fontSize.base,
},
}));
const SHEET_CANCEL_BUTTON_STYLE = [styles.sheetButton, styles.sheetCancelButton];
const SHEET_ARCHIVE_BUTTON_STYLE = [styles.sheetButton, styles.sheetArchiveButton];
@@ -105,7 +105,7 @@ export function DictationControls({
<Pressable
onPress={onRetry}
accessibilityLabel={t("message.dictation.retry")}
style={ACTION_CONFIRM_STYLE}
style={[styles.actionButton, styles.actionButtonConfirm]}
>
<RefreshCcw size={theme.iconSize.sm} color={theme.colors.surface0} />
</Pressable>
@@ -115,14 +115,14 @@ export function DictationControls({
<Pressable
onPress={onAccept}
accessibilityLabel={t("message.dictation.insert")}
style={ACTION_SECONDARY_STYLE}
style={[styles.actionButton, styles.actionButtonSecondary]}
>
<Check size={theme.iconSize.sm} color={theme.colors.foreground} />
</Pressable>
<Pressable
onPress={onAcceptAndSend}
accessibilityLabel={t("message.dictation.insertAndSend")}
style={ACTION_CONFIRM_STYLE}
style={[styles.actionButton, styles.actionButtonConfirm]}
>
<ArrowUp size={theme.iconSize.sm} color={theme.colors.surface0} />
</Pressable>
@@ -240,7 +240,7 @@ export function DictationOverlay({
onPress={onAccept}
accessibilityRole="button"
accessibilityLabel={t("message.dictation.insert")}
style={OVERLAY_ACCEPT_BUTTON_STYLE}
style={[overlayStyles.actionButton, OVERLAY_ACCEPT_BUTTON_BG]}
>
<Pencil
size={theme.iconSize.lg}
@@ -400,7 +400,4 @@ const overlayStyles = StyleSheet.create((theme) => ({
},
}));
const ACTION_CONFIRM_STYLE = [styles.actionButton, styles.actionButtonConfirm];
const ACTION_SECONDARY_STYLE = [styles.actionButton, styles.actionButtonSecondary];
const OVERLAY_ACCEPT_BUTTON_BG = { backgroundColor: "rgba(255, 255, 255, 0.25)" };
const OVERLAY_ACCEPT_BUTTON_STYLE = [overlayStyles.actionButton, OVERLAY_ACCEPT_BUTTON_BG];
@@ -211,7 +211,7 @@ export function ExplorerSidebar({
return (
<Animated.View style={desktopSidebarStyle}>
<View style={DESKTOP_SIDEBAR_BORDER_STYLE}>
<View style={[styles.desktopSidebarBorder, { flex: 1 }]}>
<SidebarResizeHandle
edge="left"
gesture={resizeGesture}
@@ -516,5 +516,3 @@ const styles = StyleSheet.create((theme) => ({
minHeight: 0,
},
}));
const DESKTOP_SIDEBAR_BORDER_STYLE = [styles.desktopSidebarBorder, { flex: 1 }];
@@ -575,7 +575,7 @@ function FileExplorerPaneContent(props: FileExplorerPaneContentProps) {
}
return (
<View style={TREE_PANE_CONTAINER_STYLE}>
<View style={[styles.treePane, styles.treePaneFill]}>
<View style={styles.paneHeader} testID="files-pane-header">
<Pressable onPress={handleSortCycle} style={sortTriggerStyleProp}>
<Text style={styles.sortTriggerText}>{currentSortLabel}</Text>
@@ -1253,5 +1253,3 @@ const styles = StyleSheet.create((theme) => ({
padding: theme.spacing[4],
},
}));
const TREE_PANE_CONTAINER_STYLE = [styles.treePane, styles.treePaneFill];
@@ -155,7 +155,7 @@ export function ProviderCatalogList({
accessibilityLabel={t("providerCatalog.search")}
placeholder={t("providerCatalog.search")}
// @ts-expect-error - outlineStyle is web-only
style={SEARCH_INPUT_STYLE}
style={[styles.searchInput, isWeb && { outlineStyle: "none" }]}
autoCapitalize="none"
autoCorrect={false}
/>
@@ -284,5 +284,3 @@ const styles = StyleSheet.create((theme) => ({
fontSize: theme.fontSize.sm,
},
}));
const SEARCH_INPUT_STYLE = [styles.searchInput, isWeb && { outlineStyle: "none" }];
@@ -225,7 +225,7 @@ function AddCustomModelSubSheet({
autoCorrect={false}
returnKeyType="done"
// @ts-expect-error - outlineStyle is web-only
style={FORM_INPUT_STYLE}
style={[sheetStyles.formInput, isWeb && { outlineStyle: "none" }]}
/>
{error ? <Text style={sheetStyles.errorText}>{error}</Text> : null}
<View style={sheetStyles.formActions}>
@@ -437,7 +437,9 @@ function renderProviderSheetFooter({
const contentStyle = isCompact ? sheetStyles.compactFooterContent : sheetStyles.footerContent;
const actionsStyle = isCompact ? sheetStyles.compactFooterActions : sheetStyles.footerActions;
const buttonStyle = isCompact ? sheetStyles.compactFooterButton : null;
const metaStyle = isCompact ? COMPACT_FOOTER_META_STYLE : sheetStyles.footerMeta;
const metaStyle = isCompact
? [sheetStyles.footerMeta, sheetStyles.compactFooterMeta]
: sheetStyles.footerMeta;
return (
<View style={contentStyle}>
@@ -871,9 +873,6 @@ const sheetStyles = StyleSheet.create((theme) => ({
},
}));
const FORM_INPUT_STYLE = [sheetStyles.formInput, isWeb && { outlineStyle: "none" }];
const COMPACT_FOOTER_META_STYLE = [sheetStyles.footerMeta, sheetStyles.compactFooterMeta];
const MAIN_SNAP_POINTS = ["65%", "92%"];
const ADD_SNAP_POINTS = ["40%"];
const DIAGNOSTIC_SNAP_POINTS = ["50%", "85%"];
@@ -21,7 +21,10 @@ const webResizeCursorStyle = isWeb
export function SidebarResizeHandle({ edge, gesture, testID }: SidebarResizeHandleProps) {
const [highlighted, setHighlighted] = useState(false);
const highlightTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null);
const hitAreaStyle = edge === "left" ? LEFT_HIT_AREA_STYLE : RIGHT_HIT_AREA_STYLE;
const hitAreaStyle =
edge === "left"
? [styles.hitArea, styles.leftEdge, webResizeCursorStyle]
: [styles.hitArea, styles.rightEdge, webResizeCursorStyle];
const cancelHighlightTimer = useCallback(() => {
if (highlightTimerRef.current === null) return;
@@ -84,6 +87,3 @@ const styles = StyleSheet.create((theme) => ({
opacity: 0.25,
},
}));
const LEFT_HIT_AREA_STYLE = [styles.hitArea, styles.leftEdge, webResizeCursorStyle];
const RIGHT_HIT_AREA_STYLE = [styles.hitArea, styles.rightEdge, webResizeCursorStyle];
@@ -315,7 +315,9 @@ export function PrBadge({ hint }: { hint: PrHint }) {
const handleHoverIn = useCallback(() => setIsHovered(true), []);
const handleHoverOut = useCallback(() => setIsHovered(false), []);
const textStyle = isHovered ? prBadgeTextHoveredCombined : prBadgeStyles.text;
const textStyle = isHovered
? [prBadgeStyles.text, prBadgeStyles.textHovered]
: prBadgeStyles.text;
const iconUniProps = isHovered ? foregroundColorMapping : getPrIconUniMapping(hint.state);
const presentation = getForgePresentation(normalizeForge(hint.forge));
@@ -395,8 +397,6 @@ const prBadgeStyles = StyleSheet.create((theme) => ({
},
}));
const prBadgeTextHoveredCombined = [prBadgeStyles.text, prBadgeStyles.textHovered];
function StatusDotOverlay({
dotColorStyle,
size,
@@ -711,7 +711,7 @@ function ErrorSection({ errorText, ds }: { errorText: string; ds: DetailStyles }
const { t } = useTranslation();
return (
<View style={styles.section}>
<Text style={SECTION_TITLE_ERROR_STYLE}>{t("toolCallDetails.error")}</Text>
<Text style={[styles.sectionTitle, styles.errorText]}>{t("toolCallDetails.error")}</Text>
<ScrollView
horizontal
nestedScrollEnabled
@@ -719,7 +719,11 @@ function ErrorSection({ errorText, ds }: { errorText: string; ds: DetailStyles }
contentContainerStyle={styles.jsonContent}
showsHorizontalScrollIndicator={true}
>
<Text selectable style={SCROLL_TEXT_ERROR_STYLE} dataSet={CODE_SURFACE_DATASET}>
<Text
selectable
style={[styles.scrollText, styles.errorText]}
dataSet={CODE_SURFACE_DATASET}
>
{errorText}
</Text>
</ScrollView>
@@ -948,6 +952,3 @@ const styles = StyleSheet.create((theme) => {
},
};
});
const SECTION_TITLE_ERROR_STYLE = [styles.sectionTitle, styles.errorText];
const SCROLL_TEXT_ERROR_STYLE = [styles.scrollText, styles.errorText];
@@ -51,7 +51,7 @@ export function TreeIndentGuides({ depth }: { depth: number }) {
/** Rotating disclosure chevron for a directory row (points right; rotates down when expanded). */
export function TreeChevron({ expanded }: { expanded: boolean }) {
return (
<View style={expanded ? CHEVRON_EXPANDED_STYLE : styles.chevron}>
<View style={expanded ? [styles.chevron, styles.chevronExpanded] : styles.chevron}>
<ThemedChevronRight size={16} uniProps={foregroundMutedIconColorMapping} />
</View>
);
@@ -76,7 +76,3 @@ const styles = StyleSheet.create((theme: Theme) => ({
transform: [{ rotate: "90deg" }],
},
}));
// Stable module-level style ref so TreeChevron passes a constant array, not one created
// per render — satisfies react-perf (no inline-array prop) without a per-render useMemo.
const CHEVRON_EXPANDED_STYLE = [styles.chevron, styles.chevronExpanded];
+2 -4
View File
@@ -199,7 +199,7 @@ export function SearchInput({
<AdaptiveTextInput
ref={inputRef}
// @ts-expect-error - outlineStyle is web-only
style={SEARCH_INPUT_STYLE}
style={[styles.searchInput, IS_WEB && { outlineStyle: "none" }]}
placeholder={placeholder}
resetKey={resetKey}
onChangeText={onChangeText}
@@ -212,7 +212,7 @@ export function SearchInput({
key={resetKey}
ref={inputRef}
// @ts-expect-error - outlineStyle is web-only
style={SEARCH_INPUT_STYLE}
style={[styles.searchInput, IS_WEB && { outlineStyle: "none" }]}
placeholder={placeholder}
placeholderTextColor={theme.colors.foregroundMuted}
onChangeText={onChangeText}
@@ -1711,5 +1711,3 @@ const styles = StyleSheet.create((theme) => ({
justifyContent: "flex-end",
},
}));
const SEARCH_INPUT_STYLE = [styles.searchInput, IS_WEB && { outlineStyle: "none" }];
@@ -418,7 +418,8 @@ function CopyableInfoRow({
if (copied || isHovered) {
iconUniProps = foregroundColorMapping;
}
const textStyle = copied || isHovered ? cardInfoTextHoveredCombined : styles.cardInfoText;
const textStyle =
copied || isHovered ? [styles.cardInfoText, styles.cardInfoTextHovered] : styles.cardInfoText;
return (
<Pressable
@@ -506,7 +507,9 @@ function ChecksSummaryContent({
const { t } = useTranslation();
const { passed, failed, pending } = getChecksSummaryCounts(checks);
const labelStyle = hovered ? checksSummaryLabelHoveredCombined : styles.checksSummaryLabel;
const labelStyle = hovered
? [styles.checksSummaryLabel, styles.checksSummaryLabelHovered]
: styles.checksSummaryLabel;
const iconUniProps = hovered ? foregroundColorMapping : foregroundMutedColorMapping;
const icon = getForgePresentation(normalizeForge(forge)).icon;
@@ -670,10 +673,3 @@ const styles = StyleSheet.create((theme) => ({
color: theme.colors.statusSuccess,
},
}));
const checksSummaryLabelHoveredCombined = [
styles.checksSummaryLabel,
styles.checksSummaryLabelHovered,
];
const cardInfoTextHoveredCombined = [styles.cardInfoText, styles.cardInfoTextHovered];