fix(app): close theme split escape hatches
This commit is contained in:
+39
-1
@@ -31,7 +31,7 @@ const styles = StyleSheet.create((theme) => ({
|
|||||||
|
|
||||||
On first mount this can paint with the current adaptive or initial theme. If app settings later load a persisted theme and call [`UnistylesRuntime.setTheme`](https://www.unistyl.es/v3/guides/theming#change-theme), the JS-side style proxy may report the new theme while the native content container keeps the old background. That is how the welcome screen ended up with a light background and dark foreground/buttons.
|
On first mount this can paint with the current adaptive or initial theme. If app settings later load a persisted theme and call [`UnistylesRuntime.setTheme`](https://www.unistyl.es/v3/guides/theming#change-theme), the JS-side style proxy may report the new theme while the native content container keeps the old background. That is how the welcome screen ended up with a light background and dark foreground/buttons.
|
||||||
|
|
||||||
This applies broadly to non-`style` props that carry theme-dependent values, such as component props named `color`, `trackColor`, `tintColor`, and library-specific style props. The [3rd-party view decision algorithm](https://www.unistyl.es/v3/references/3rd-party-views) recommends explicit handling for these cases, and [issue #1030](https://github.com/jpudysz/react-native-unistyles/issues/1030) shows a related native-prop update edge case around `Image.tintColor`. Treat these values as React props unless wrapped with `withUnistyles`.
|
This applies broadly to non-`style` props that carry theme-dependent values, such as component props named `color`, `trackColor`, `tintColor`, `backgroundStyle`, `handleIndicatorStyle`, and other library-specific style props. The [3rd-party view decision algorithm](https://www.unistyl.es/v3/references/3rd-party-views) recommends explicit handling for these cases, and [issue #1030](https://github.com/jpudysz/react-native-unistyles/issues/1030) shows a related native-prop update edge case around `Image.tintColor`. Treat these values as React props unless wrapped with `withUnistyles`.
|
||||||
|
|
||||||
## Fix Patterns
|
## Fix Patterns
|
||||||
|
|
||||||
@@ -105,6 +105,44 @@ const { theme } = useUnistyles();
|
|||||||
|
|
||||||
Keep layout and typography in `StyleSheet.create`; move only the stale theme-dependent value through React. If a larger subtree shows the same behavior, consider remounting the sheet on theme changes or moving the themed paint onto a wrapper that is mounted with the visible content.
|
Keep layout and typography in `StyleSheet.create`; move only the stale theme-dependent value through React. If a larger subtree shows the same behavior, consider remounting the sheet on theme changes or moving the themed paint onto a wrapper that is mounted with the visible content.
|
||||||
|
|
||||||
|
The same rule applies to bottom-sheet component props such as `backgroundStyle` and `handleIndicatorStyle`: they are library props, not the direct React Native `style` prop Unistyles registers. Prefer a custom `backgroundComponent` that calls `useUnistyles()`, or pass a small inline object from the hook theme.
|
||||||
|
|
||||||
|
## Memoized Style Objects
|
||||||
|
|
||||||
|
When a third-party library receives a plain style object, it is outside Unistyles' native tracking path. Make sure any memo that builds that style object depends on the actual theme values it reads.
|
||||||
|
|
||||||
|
Avoid indirect keys like this:
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
const { theme, rt } = useUnistyles();
|
||||||
|
const markdownStyles = useMemo(() => createMarkdownStyles(theme), [rt.themeName]);
|
||||||
|
```
|
||||||
|
|
||||||
|
On adaptive system-theme changes, the hook can provide a light/dark theme update while an indirect runtime key is not the value that invalidates the memo. That leaves the library rendering stale colors. Assistant markdown hit this exact failure: the workspace shell switched to light, but assistant text and code spans kept the old dark-theme markdown style object.
|
||||||
|
|
||||||
|
Prefer the hook theme itself, or explicit theme tokens, as the dependency:
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
const { theme } = useUnistyles();
|
||||||
|
const markdownStyles = useMemo(() => createMarkdownStyles(theme), [theme]);
|
||||||
|
```
|
||||||
|
|
||||||
|
If a style factory is cheap, skipping `useMemo` entirely is also fine.
|
||||||
|
|
||||||
|
## Static Theme Imports
|
||||||
|
|
||||||
|
Do not import `theme` from `@/styles/theme` for live UI colors. That export is a dark-theme compatibility default, so using it in render code leaves icons, placeholders, or third-party props pinned to dark colors in light mode.
|
||||||
|
|
||||||
|
Use `useUnistyles()` inside the component instead:
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
const { theme } = useUnistyles();
|
||||||
|
|
||||||
|
<ChevronDown size={theme.iconSize.md} color={theme.colors.foregroundMuted} />
|
||||||
|
```
|
||||||
|
|
||||||
|
Importing `baseColors`, theme-name constants, or `type Theme` is fine when the value is intentionally static or type-only.
|
||||||
|
|
||||||
## Adaptive Themes And Persisted Settings
|
## Adaptive Themes And Persisted Settings
|
||||||
|
|
||||||
Unistyles [`initialTheme`](https://www.unistyl.es/v3/guides/theming#select-theme) and [`adaptiveThemes`](https://www.unistyl.es/v3/guides/theming#adaptive-themes) are mutually exclusive. `initialTheme` can be a string or a synchronous function, but it cannot wait on async storage.
|
Unistyles [`initialTheme`](https://www.unistyl.es/v3/guides/theming#select-theme) and [`adaptiveThemes`](https://www.unistyl.es/v3/guides/theming#adaptive-themes) are mutually exclusive. `initialTheme` can be a string or a synchronous function, but it cannot wait on async storage.
|
||||||
|
|||||||
@@ -109,9 +109,6 @@ const styles = StyleSheet.create((theme) => ({
|
|||||||
gap: theme.spacing[4],
|
gap: theme.spacing[4],
|
||||||
flexGrow: 1,
|
flexGrow: 1,
|
||||||
},
|
},
|
||||||
bottomSheetHandle: {
|
|
||||||
backgroundColor: theme.colors.surface2,
|
|
||||||
},
|
|
||||||
bottomSheetHeader: {
|
bottomSheetHeader: {
|
||||||
paddingHorizontal: theme.spacing[6],
|
paddingHorizontal: theme.spacing[6],
|
||||||
paddingTop: theme.spacing[4],
|
paddingTop: theme.spacing[4],
|
||||||
@@ -243,7 +240,7 @@ export function AdaptiveModalSheet({
|
|||||||
enablePanDownToClose
|
enablePanDownToClose
|
||||||
stackBehavior={stackBehavior}
|
stackBehavior={stackBehavior}
|
||||||
backgroundComponent={SheetBackground}
|
backgroundComponent={SheetBackground}
|
||||||
handleIndicatorStyle={styles.bottomSheetHandle}
|
handleIndicatorStyle={{ backgroundColor: theme.colors.surface2 }}
|
||||||
keyboardBehavior="extend"
|
keyboardBehavior="extend"
|
||||||
keyboardBlurBehavior="restore"
|
keyboardBlurBehavior="restore"
|
||||||
accessible={false}
|
accessible={false}
|
||||||
|
|||||||
@@ -22,7 +22,6 @@ import {
|
|||||||
ShieldAlert,
|
ShieldAlert,
|
||||||
ShieldOff,
|
ShieldOff,
|
||||||
} from "lucide-react-native";
|
} from "lucide-react-native";
|
||||||
import { theme as defaultTheme } from "@/styles/theme";
|
|
||||||
import type {
|
import type {
|
||||||
AgentMode,
|
AgentMode,
|
||||||
AgentModelDefinition,
|
AgentModelDefinition,
|
||||||
@@ -86,6 +85,8 @@ export function DropdownField({
|
|||||||
renderTrigger,
|
renderTrigger,
|
||||||
testID,
|
testID,
|
||||||
}: DropdownFieldProps): ReactElement {
|
}: DropdownFieldProps): ReactElement {
|
||||||
|
const { theme } = useUnistyles();
|
||||||
|
|
||||||
if (renderTrigger) {
|
if (renderTrigger) {
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
@@ -115,7 +116,7 @@ export function DropdownField({
|
|||||||
<Text style={value ? styles.dropdownValue : styles.dropdownPlaceholder} numberOfLines={1}>
|
<Text style={value ? styles.dropdownValue : styles.dropdownPlaceholder} numberOfLines={1}>
|
||||||
{value || placeholder}
|
{value || placeholder}
|
||||||
</Text>
|
</Text>
|
||||||
<ChevronDown size={defaultTheme.iconSize.md} color={defaultTheme.colors.foregroundMuted} />
|
<ChevronDown size={theme.iconSize.md} color={theme.colors.foregroundMuted} />
|
||||||
</Pressable>
|
</Pressable>
|
||||||
{errorMessage ? <Text style={styles.errorText}>{errorMessage}</Text> : null}
|
{errorMessage ? <Text style={styles.errorText}>{errorMessage}</Text> : null}
|
||||||
{warningMessage ? <Text style={styles.warningText}>{warningMessage}</Text> : null}
|
{warningMessage ? <Text style={styles.warningText}>{warningMessage}</Text> : null}
|
||||||
@@ -151,6 +152,8 @@ export function SelectField({
|
|||||||
valueEllipsizeMode,
|
valueEllipsizeMode,
|
||||||
testID,
|
testID,
|
||||||
}: SelectFieldProps): ReactElement {
|
}: SelectFieldProps): ReactElement {
|
||||||
|
const { theme } = useUnistyles();
|
||||||
|
|
||||||
const getWebKey = useCallback((event: unknown): string | null => {
|
const getWebKey = useCallback((event: unknown): string | null => {
|
||||||
if (!event || typeof event !== "object") return null;
|
if (!event || typeof event !== "object") return null;
|
||||||
const eventWithNative = event as { nativeEvent?: unknown; key?: unknown };
|
const eventWithNative = event as { nativeEvent?: unknown; key?: unknown };
|
||||||
@@ -210,7 +213,7 @@ export function SelectField({
|
|||||||
{value || placeholder || "Select..."}
|
{value || placeholder || "Select..."}
|
||||||
</Text>
|
</Text>
|
||||||
</View>
|
</View>
|
||||||
<ChevronRight size={defaultTheme.iconSize.lg} color={defaultTheme.colors.foregroundMuted} />
|
<ChevronRight size={theme.iconSize.lg} color={theme.colors.foregroundMuted} />
|
||||||
</Pressable>
|
</Pressable>
|
||||||
{errorMessage ? <Text style={styles.errorText}>{errorMessage}</Text> : null}
|
{errorMessage ? <Text style={styles.errorText}>{errorMessage}</Text> : null}
|
||||||
{warningMessage ? <Text style={styles.warningText}>{warningMessage}</Text> : null}
|
{warningMessage ? <Text style={styles.warningText}>{warningMessage}</Text> : null}
|
||||||
@@ -229,7 +232,21 @@ interface DropdownSheetProps {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function DropdownSheetBackground({ style }: BottomSheetBackgroundProps) {
|
function DropdownSheetBackground({ style }: BottomSheetBackgroundProps) {
|
||||||
return <Animated.View pointerEvents="none" style={[style, styles.bottomSheetBackground]} />;
|
const { theme } = useUnistyles();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Animated.View
|
||||||
|
pointerEvents="none"
|
||||||
|
style={[
|
||||||
|
style,
|
||||||
|
{
|
||||||
|
backgroundColor: theme.colors.surface2,
|
||||||
|
borderTopLeftRadius: theme.borderRadius["2xl"],
|
||||||
|
borderTopRightRadius: theme.borderRadius["2xl"],
|
||||||
|
},
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function DropdownSheet({
|
export function DropdownSheet({
|
||||||
@@ -238,6 +255,7 @@ export function DropdownSheet({
|
|||||||
onClose,
|
onClose,
|
||||||
children,
|
children,
|
||||||
}: DropdownSheetProps): ReactElement {
|
}: DropdownSheetProps): ReactElement {
|
||||||
|
const { theme } = useUnistyles();
|
||||||
const bottomSheetRef = useRef<BottomSheetModal>(null);
|
const bottomSheetRef = useRef<BottomSheetModal>(null);
|
||||||
const snapPoints = useMemo(() => ["60%", "90%"], []);
|
const snapPoints = useMemo(() => ["60%", "90%"], []);
|
||||||
|
|
||||||
@@ -280,12 +298,12 @@ export function DropdownSheet({
|
|||||||
backdropComponent={renderBackdrop}
|
backdropComponent={renderBackdrop}
|
||||||
enablePanDownToClose
|
enablePanDownToClose
|
||||||
backgroundComponent={DropdownSheetBackground}
|
backgroundComponent={DropdownSheetBackground}
|
||||||
handleIndicatorStyle={styles.bottomSheetHandle}
|
handleIndicatorStyle={{ backgroundColor: theme.colors.palette.zinc[600] }}
|
||||||
keyboardBehavior="extend"
|
keyboardBehavior="extend"
|
||||||
keyboardBlurBehavior="restore"
|
keyboardBlurBehavior="restore"
|
||||||
>
|
>
|
||||||
<View style={styles.bottomSheetHeader}>
|
<View style={styles.bottomSheetHeader}>
|
||||||
<Text style={styles.dropdownSheetTitle}>{title}</Text>
|
<Text style={[styles.dropdownSheetTitle, { color: theme.colors.foreground }]}>{title}</Text>
|
||||||
<Pressable
|
<Pressable
|
||||||
accessibilityRole="button"
|
accessibilityRole="button"
|
||||||
accessibilityLabel="Close sheet"
|
accessibilityLabel="Close sheet"
|
||||||
@@ -293,7 +311,7 @@ export function DropdownSheet({
|
|||||||
hitSlop={10}
|
hitSlop={10}
|
||||||
testID="dropdown-sheet-close"
|
testID="dropdown-sheet-close"
|
||||||
>
|
>
|
||||||
<X size={defaultTheme.iconSize.md} color={defaultTheme.colors.foregroundMuted} />
|
<X size={theme.iconSize.md} color={theme.colors.foregroundMuted} />
|
||||||
</Pressable>
|
</Pressable>
|
||||||
</View>
|
</View>
|
||||||
<BottomSheetScrollView
|
<BottomSheetScrollView
|
||||||
@@ -413,6 +431,8 @@ export function FormSelectTrigger({
|
|||||||
valueEllipsizeMode,
|
valueEllipsizeMode,
|
||||||
testID,
|
testID,
|
||||||
}: CompactSelectFieldProps): ReactElement {
|
}: CompactSelectFieldProps): ReactElement {
|
||||||
|
const { theme } = useUnistyles();
|
||||||
|
|
||||||
const getWebKey = useCallback((event: unknown): string | null => {
|
const getWebKey = useCallback((event: unknown): string | null => {
|
||||||
if (!event || typeof event !== "object") return null;
|
if (!event || typeof event !== "object") return null;
|
||||||
const eventWithNative = event as { nativeEvent?: unknown; key?: unknown };
|
const eventWithNative = event as { nativeEvent?: unknown; key?: unknown };
|
||||||
@@ -469,7 +489,7 @@ export function FormSelectTrigger({
|
|||||||
<View style={styles.compactSelectValueContainer}>
|
<View style={styles.compactSelectValueContainer}>
|
||||||
{showLabel ? <Text style={styles.compactSelectLabel}>{label}</Text> : null}
|
{showLabel ? <Text style={styles.compactSelectLabel}>{label}</Text> : null}
|
||||||
{isLoading ? (
|
{isLoading ? (
|
||||||
<ActivityIndicator size="small" color={defaultTheme.colors.foregroundMuted} />
|
<ActivityIndicator size="small" color={theme.colors.foregroundMuted} />
|
||||||
) : (
|
) : (
|
||||||
<Text
|
<Text
|
||||||
style={hasConcreteValue ? styles.compactSelectValue : styles.compactSelectPlaceholder}
|
style={hasConcreteValue ? styles.compactSelectValue : styles.compactSelectPlaceholder}
|
||||||
@@ -480,7 +500,7 @@ export function FormSelectTrigger({
|
|||||||
</Text>
|
</Text>
|
||||||
)}
|
)}
|
||||||
</View>
|
</View>
|
||||||
<ChevronDown size={defaultTheme.iconSize.md} color={defaultTheme.colors.foregroundMuted} />
|
<ChevronDown size={theme.iconSize.md} color={theme.colors.foregroundMuted} />
|
||||||
</Pressable>
|
</Pressable>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -518,6 +538,8 @@ export function AgentConfigRow({
|
|||||||
onSelectThinkingOption,
|
onSelectThinkingOption,
|
||||||
disabled,
|
disabled,
|
||||||
}: AgentConfigRowProps): ReactElement {
|
}: AgentConfigRowProps): ReactElement {
|
||||||
|
const { theme } = useUnistyles();
|
||||||
|
|
||||||
const providerOptions: ComboSelectOption[] = useMemo(
|
const providerOptions: ComboSelectOption[] = useMemo(
|
||||||
() =>
|
() =>
|
||||||
providerDefinitions.map((def) => ({
|
providerDefinitions.map((def) => ({
|
||||||
@@ -576,7 +598,7 @@ export function AgentConfigRow({
|
|||||||
placeholder={providerOptions.length > 0 ? "Select..." : "No providers available"}
|
placeholder={providerOptions.length > 0 ? "Select..." : "No providers available"}
|
||||||
disabled={disabled || providerOptions.length === 0}
|
disabled={disabled || providerOptions.length === 0}
|
||||||
onSelect={onSelectProvider}
|
onSelect={onSelectProvider}
|
||||||
icon={<Bot size={defaultTheme.iconSize.md} color={defaultTheme.colors.foregroundMuted} />}
|
icon={<Bot size={theme.iconSize.md} color={theme.colors.foregroundMuted} />}
|
||||||
showLabel={false}
|
showLabel={false}
|
||||||
testID="draft-provider-select"
|
testID="draft-provider-select"
|
||||||
/>
|
/>
|
||||||
@@ -591,9 +613,7 @@ export function AgentConfigRow({
|
|||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
isLoading={isModelLoading}
|
isLoading={isModelLoading}
|
||||||
onSelect={onSelectModel}
|
onSelect={onSelectModel}
|
||||||
icon={
|
icon={<Brain size={theme.iconSize.md} color={theme.colors.foregroundMuted} />}
|
||||||
<Brain size={defaultTheme.iconSize.md} color={defaultTheme.colors.foregroundMuted} />
|
|
||||||
}
|
|
||||||
showLabel={false}
|
showLabel={false}
|
||||||
testID="draft-model-select"
|
testID="draft-model-select"
|
||||||
/>
|
/>
|
||||||
@@ -607,7 +627,7 @@ export function AgentConfigRow({
|
|||||||
placeholder="Default"
|
placeholder="Default"
|
||||||
disabled={disabled || modeOptions.length === 0}
|
disabled={disabled || modeOptions.length === 0}
|
||||||
onSelect={onSelectMode}
|
onSelect={onSelectMode}
|
||||||
icon={<ModeIcon size={defaultTheme.iconSize.md} color={modeIconColor} />}
|
icon={<ModeIcon size={theme.iconSize.md} color={modeIconColor} />}
|
||||||
showLabel={false}
|
showLabel={false}
|
||||||
testID="draft-mode-select"
|
testID="draft-mode-select"
|
||||||
/>
|
/>
|
||||||
@@ -622,9 +642,7 @@ export function AgentConfigRow({
|
|||||||
placeholder="Select..."
|
placeholder="Select..."
|
||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
onSelect={onSelectThinkingOption}
|
onSelect={onSelectThinkingOption}
|
||||||
icon={
|
icon={<Brain size={theme.iconSize.md} color={theme.colors.foregroundMuted} />}
|
||||||
<Brain size={defaultTheme.iconSize.md} color={defaultTheme.colors.foregroundMuted} />
|
|
||||||
}
|
|
||||||
showLabel={false}
|
showLabel={false}
|
||||||
/>
|
/>
|
||||||
</View>
|
</View>
|
||||||
@@ -969,6 +987,8 @@ export function GitOptionsSection({
|
|||||||
attachWorktreeError,
|
attachWorktreeError,
|
||||||
onSelectWorktreePath,
|
onSelectWorktreePath,
|
||||||
}: GitOptionsSectionProps): ReactElement {
|
}: GitOptionsSectionProps): ReactElement {
|
||||||
|
const { theme } = useUnistyles();
|
||||||
|
|
||||||
const isLoading = status === "loading";
|
const isLoading = status === "loading";
|
||||||
const isCreateMode = worktreeMode === "create";
|
const isCreateMode = worktreeMode === "create";
|
||||||
const isAttachMode = worktreeMode === "attach";
|
const isAttachMode = worktreeMode === "attach";
|
||||||
@@ -1103,7 +1123,7 @@ export function GitOptionsSection({
|
|||||||
autoCapitalize="none"
|
autoCapitalize="none"
|
||||||
autoCorrect={false}
|
autoCorrect={false}
|
||||||
placeholder="branch name"
|
placeholder="branch name"
|
||||||
placeholderTextColor={defaultTheme.colors.foregroundMuted}
|
placeholderTextColor={theme.colors.foregroundMuted}
|
||||||
onSubmitEditing={handleConfirmEdit}
|
onSubmitEditing={handleConfirmEdit}
|
||||||
/>
|
/>
|
||||||
<Pressable
|
<Pressable
|
||||||
@@ -1111,19 +1131,16 @@ export function GitOptionsSection({
|
|||||||
hitSlop={8}
|
hitSlop={8}
|
||||||
style={styles.baseBranchIconButton}
|
style={styles.baseBranchIconButton}
|
||||||
>
|
>
|
||||||
<Check
|
<Check size={theme.iconSize.md} color={theme.colors.palette.green[500]} />
|
||||||
size={defaultTheme.iconSize.md}
|
|
||||||
color={defaultTheme.colors.palette.green[500]}
|
|
||||||
/>
|
|
||||||
</Pressable>
|
</Pressable>
|
||||||
<Pressable onPress={handleCancelEdit} hitSlop={8} style={styles.baseBranchIconButton}>
|
<Pressable onPress={handleCancelEdit} hitSlop={8} style={styles.baseBranchIconButton}>
|
||||||
<X size={defaultTheme.iconSize.md} color={defaultTheme.colors.foregroundMuted} />
|
<X size={theme.iconSize.md} color={theme.colors.foregroundMuted} />
|
||||||
</Pressable>
|
</Pressable>
|
||||||
</View>
|
</View>
|
||||||
) : (
|
) : (
|
||||||
<Pressable onPress={handleStartEdit} style={styles.baseBranchValueRow}>
|
<Pressable onPress={handleStartEdit} style={styles.baseBranchValueRow}>
|
||||||
<Text style={styles.baseBranchValue}>{displayBranch}</Text>
|
<Text style={styles.baseBranchValue}>{displayBranch}</Text>
|
||||||
<Pencil size={defaultTheme.iconSize.sm} color={defaultTheme.colors.foregroundMuted} />
|
<Pencil size={theme.iconSize.sm} color={theme.colors.foregroundMuted} />
|
||||||
</Pressable>
|
</Pressable>
|
||||||
)}
|
)}
|
||||||
</View>
|
</View>
|
||||||
@@ -1171,14 +1188,6 @@ const styles = StyleSheet.create((theme) => ({
|
|||||||
color: theme.colors.foregroundMuted,
|
color: theme.colors.foregroundMuted,
|
||||||
fontSize: theme.fontSize.base,
|
fontSize: theme.fontSize.base,
|
||||||
},
|
},
|
||||||
bottomSheetBackground: {
|
|
||||||
backgroundColor: theme.colors.surface2,
|
|
||||||
borderTopLeftRadius: theme.borderRadius["2xl"],
|
|
||||||
borderTopRightRadius: theme.borderRadius["2xl"],
|
|
||||||
},
|
|
||||||
bottomSheetHandle: {
|
|
||||||
backgroundColor: theme.colors.palette.zinc[600],
|
|
||||||
},
|
|
||||||
bottomSheetHeader: {
|
bottomSheetHeader: {
|
||||||
flexDirection: "row",
|
flexDirection: "row",
|
||||||
alignItems: "center",
|
alignItems: "center",
|
||||||
|
|||||||
@@ -33,7 +33,7 @@ import {
|
|||||||
type MessageInputRef,
|
type MessageInputRef,
|
||||||
type AttachmentMenuItem,
|
type AttachmentMenuItem,
|
||||||
} from "./message-input";
|
} from "./message-input";
|
||||||
import { Theme } from "@/styles/theme";
|
import type { Theme } from "@/styles/theme";
|
||||||
import type { DraftCommandConfig } from "@/hooks/use-agent-commands-query";
|
import type { DraftCommandConfig } from "@/hooks/use-agent-commands-query";
|
||||||
import { encodeImages } from "@/utils/encode-images";
|
import { encodeImages } from "@/utils/encode-images";
|
||||||
import { focusWithRetries } from "@/utils/web-focus";
|
import { focusWithRetries } from "@/utils/web-focus";
|
||||||
|
|||||||
@@ -53,7 +53,6 @@ import Animated, {
|
|||||||
withTiming,
|
withTiming,
|
||||||
} from "react-native-reanimated";
|
} from "react-native-reanimated";
|
||||||
import Svg, { Defs, LinearGradient as SvgLinearGradient, Rect, Stop } from "react-native-svg";
|
import Svg, { Defs, LinearGradient as SvgLinearGradient, Rect, Stop } from "react-native-svg";
|
||||||
import { theme } from "@/styles/theme";
|
|
||||||
import { createMarkdownStyles } from "@/styles/markdown-styles";
|
import { createMarkdownStyles } from "@/styles/markdown-styles";
|
||||||
import { Colors, Fonts } from "@/constants/theme";
|
import { Colors, Fonts } from "@/constants/theme";
|
||||||
import * as Clipboard from "expo-clipboard";
|
import * as Clipboard from "expo-clipboard";
|
||||||
@@ -341,6 +340,7 @@ export const UserMessage = memo(function UserMessage({
|
|||||||
isLastInGroup = true,
|
isLastInGroup = true,
|
||||||
disableOuterSpacing,
|
disableOuterSpacing,
|
||||||
}: UserMessageProps) {
|
}: UserMessageProps) {
|
||||||
|
const { theme } = useUnistyles();
|
||||||
const isCompact = useIsCompactFormFactor();
|
const isCompact = useIsCompactFormFactor();
|
||||||
const [messageHovered, setMessageHovered] = useState(false);
|
const [messageHovered, setMessageHovered] = useState(false);
|
||||||
const [copyButtonHovered, setCopyButtonHovered] = useState(false);
|
const [copyButtonHovered, setCopyButtonHovered] = useState(false);
|
||||||
@@ -962,10 +962,10 @@ export const AssistantMessage = memo(function AssistantMessage({
|
|||||||
client,
|
client,
|
||||||
disableOuterSpacing,
|
disableOuterSpacing,
|
||||||
}: AssistantMessageProps) {
|
}: AssistantMessageProps) {
|
||||||
const { theme, rt } = useUnistyles();
|
const { theme } = useUnistyles();
|
||||||
const resolvedDisableOuterSpacing = useDisableOuterSpacing(disableOuterSpacing);
|
const resolvedDisableOuterSpacing = useDisableOuterSpacing(disableOuterSpacing);
|
||||||
|
|
||||||
const markdownStyles = useMemo(() => createMarkdownStyles(theme), [rt.themeName]);
|
const markdownStyles = useMemo(() => createMarkdownStyles(theme), [theme]);
|
||||||
|
|
||||||
const markdownParser = useMemo(() => {
|
const markdownParser = useMemo(() => {
|
||||||
const parser = MarkdownIt({ typographer: true, linkify: true });
|
const parser = MarkdownIt({ typographer: true, linkify: true });
|
||||||
|
|||||||
@@ -86,7 +86,7 @@ export function TerminalPane({ serverId, cwd, terminalId, isPaneFocused }: Termi
|
|||||||
const isScreenFocused = useIsFocused();
|
const isScreenFocused = useIsFocused();
|
||||||
const isAppVisible = useAppVisible();
|
const isAppVisible = useAppVisible();
|
||||||
const { theme } = useUnistyles();
|
const { theme } = useUnistyles();
|
||||||
const xtermTheme = useMemo(() => toXtermTheme(theme.colors.terminal), [theme.colors.terminal]);
|
const xtermTheme = useMemo(() => toXtermTheme(theme.colors.terminal), [theme]);
|
||||||
const isMobile = useIsCompactFormFactor();
|
const isMobile = useIsCompactFormFactor();
|
||||||
const mobileView = usePanelStore((state) => state.mobileView);
|
const mobileView = usePanelStore((state) => state.mobileView);
|
||||||
const openAgentList = usePanelStore((state) => state.openAgentList);
|
const openAgentList = usePanelStore((state) => state.openAgentList);
|
||||||
|
|||||||
@@ -59,6 +59,7 @@ interface ToolCallSheetProviderProps {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function ToolCallSheetProvider({ children }: ToolCallSheetProviderProps) {
|
export function ToolCallSheetProvider({ children }: ToolCallSheetProviderProps) {
|
||||||
|
const { theme } = useUnistyles();
|
||||||
const bottomSheetRef = useRef<BottomSheetModal>(null);
|
const bottomSheetRef = useRef<BottomSheetModal>(null);
|
||||||
const [sheetData, setSheetData] = React.useState<ToolCallSheetData | null>(null);
|
const [sheetData, setSheetData] = React.useState<ToolCallSheetData | null>(null);
|
||||||
|
|
||||||
@@ -103,8 +104,8 @@ export function ToolCallSheetProvider({ children }: ToolCallSheetProviderProps)
|
|||||||
onChange={handleSheetChange}
|
onChange={handleSheetChange}
|
||||||
backdropComponent={renderBackdrop}
|
backdropComponent={renderBackdrop}
|
||||||
enablePanDownToClose
|
enablePanDownToClose
|
||||||
backgroundStyle={styles.sheetBackground}
|
backgroundComponent={CustomSheetBackground}
|
||||||
handleIndicatorStyle={styles.handleIndicator}
|
handleIndicatorStyle={{ backgroundColor: theme.colors.palette.zinc[600] }}
|
||||||
>
|
>
|
||||||
{sheetData && <ToolCallSheetContent data={sheetData} onClose={closeToolCall} />}
|
{sheetData && <ToolCallSheetContent data={sheetData} onClose={closeToolCall} />}
|
||||||
</BottomSheetModal>
|
</BottomSheetModal>
|
||||||
@@ -120,6 +121,7 @@ interface ToolCallSheetContentProps {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function ToolCallSheetContent({ data, onClose }: ToolCallSheetContentProps) {
|
function ToolCallSheetContent({ data, onClose }: ToolCallSheetContentProps) {
|
||||||
|
const { theme } = useUnistyles();
|
||||||
const { toolName, displayName, detail, errorText, showLoadingSkeleton } = data;
|
const { toolName, displayName, detail, errorText, showLoadingSkeleton } = data;
|
||||||
|
|
||||||
const IconComponent = resolveToolCallIcon(toolName, detail);
|
const IconComponent = resolveToolCallIcon(toolName, detail);
|
||||||
@@ -129,13 +131,13 @@ function ToolCallSheetContent({ data, onClose }: ToolCallSheetContentProps) {
|
|||||||
{/* Header */}
|
{/* Header */}
|
||||||
<View style={styles.header}>
|
<View style={styles.header}>
|
||||||
<View style={styles.headerLeft}>
|
<View style={styles.headerLeft}>
|
||||||
<IconComponent size={20} color={styles.headerIcon.color} />
|
<IconComponent size={20} color={theme.colors.foreground} />
|
||||||
<Text style={styles.headerTitle} numberOfLines={1}>
|
<Text style={styles.headerTitle} numberOfLines={1}>
|
||||||
{displayName}
|
{displayName}
|
||||||
</Text>
|
</Text>
|
||||||
</View>
|
</View>
|
||||||
<Pressable onPress={onClose} style={styles.closeButton}>
|
<Pressable onPress={onClose} style={styles.closeButton}>
|
||||||
<X size={20} color={styles.closeIcon.color} />
|
<X size={20} color={theme.colors.foregroundMuted} />
|
||||||
</Pressable>
|
</Pressable>
|
||||||
</View>
|
</View>
|
||||||
|
|
||||||
@@ -155,12 +157,6 @@ function ToolCallSheetContent({ data, onClose }: ToolCallSheetContentProps) {
|
|||||||
// ----- Styles -----
|
// ----- Styles -----
|
||||||
|
|
||||||
const styles = StyleSheet.create((theme) => ({
|
const styles = StyleSheet.create((theme) => ({
|
||||||
sheetBackground: {
|
|
||||||
backgroundColor: theme.colors.surface2,
|
|
||||||
},
|
|
||||||
handleIndicator: {
|
|
||||||
backgroundColor: theme.colors.palette.zinc[600],
|
|
||||||
},
|
|
||||||
container: {
|
container: {
|
||||||
flex: 1,
|
flex: 1,
|
||||||
backgroundColor: theme.colors.surface2,
|
backgroundColor: theme.colors.surface2,
|
||||||
@@ -180,9 +176,6 @@ const styles = StyleSheet.create((theme) => ({
|
|||||||
gap: theme.spacing[2],
|
gap: theme.spacing[2],
|
||||||
flex: 1,
|
flex: 1,
|
||||||
},
|
},
|
||||||
headerIcon: {
|
|
||||||
color: theme.colors.foreground,
|
|
||||||
},
|
|
||||||
headerTitle: {
|
headerTitle: {
|
||||||
fontSize: theme.fontSize.lg,
|
fontSize: theme.fontSize.lg,
|
||||||
fontWeight: theme.fontWeight.semibold,
|
fontWeight: theme.fontWeight.semibold,
|
||||||
@@ -192,9 +185,6 @@ const styles = StyleSheet.create((theme) => ({
|
|||||||
closeButton: {
|
closeButton: {
|
||||||
padding: theme.spacing[2],
|
padding: theme.spacing[2],
|
||||||
},
|
},
|
||||||
closeIcon: {
|
|
||||||
color: theme.colors.foregroundMuted,
|
|
||||||
},
|
|
||||||
content: {
|
content: {
|
||||||
flex: 1,
|
flex: 1,
|
||||||
minHeight: 0,
|
minHeight: 0,
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import { useCallback, useEffect, useRef } from "react";
|
|||||||
import { ScrollView, Text, View, Pressable, type LayoutChangeEvent } from "react-native";
|
import { ScrollView, Text, View, Pressable, type LayoutChangeEvent } from "react-native";
|
||||||
import { StyleSheet, useUnistyles } from "react-native-unistyles";
|
import { StyleSheet, useUnistyles } from "react-native-unistyles";
|
||||||
import { File, Folder } from "lucide-react-native";
|
import { File, Folder } from "lucide-react-native";
|
||||||
import { Theme } from "@/styles/theme";
|
import type { Theme } from "@/styles/theme";
|
||||||
import { getAutocompleteScrollOffset } from "./autocomplete-utils";
|
import { getAutocompleteScrollOffset } from "./autocomplete-utils";
|
||||||
|
|
||||||
export interface AutocompleteOption {
|
export interface AutocompleteOption {
|
||||||
|
|||||||
@@ -101,7 +101,21 @@ function toNumericStyleValue(value: unknown): number | null {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function ComboboxSheetBackground({ style }: BottomSheetBackgroundProps) {
|
function ComboboxSheetBackground({ style }: BottomSheetBackgroundProps) {
|
||||||
return <Animated.View pointerEvents="none" style={[style, styles.bottomSheetBackground]} />;
|
const { theme } = useUnistyles();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Animated.View
|
||||||
|
pointerEvents="none"
|
||||||
|
style={[
|
||||||
|
style,
|
||||||
|
{
|
||||||
|
backgroundColor: theme.colors.surface0,
|
||||||
|
borderTopLeftRadius: theme.borderRadius["2xl"],
|
||||||
|
borderTopRightRadius: theme.borderRadius["2xl"],
|
||||||
|
},
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface SearchInputProps {
|
export interface SearchInputProps {
|
||||||
@@ -268,6 +282,7 @@ export function Combobox({
|
|||||||
anchorRef,
|
anchorRef,
|
||||||
children,
|
children,
|
||||||
}: ComboboxProps): ReactElement {
|
}: ComboboxProps): ReactElement {
|
||||||
|
const { theme } = useUnistyles();
|
||||||
const isMobile = useIsCompactFormFactor();
|
const isMobile = useIsCompactFormFactor();
|
||||||
const effectiveOptionsPosition = isMobile ? "below-search" : optionsPosition;
|
const effectiveOptionsPosition = isMobile ? "below-search" : optionsPosition;
|
||||||
const isDesktopAboveSearch = !isMobile && isWeb && effectiveOptionsPosition === "above-search";
|
const isDesktopAboveSearch = !isMobile && isWeb && effectiveOptionsPosition === "above-search";
|
||||||
@@ -714,12 +729,12 @@ export function Combobox({
|
|||||||
enableDismissOnClose={enableDismissOnClose}
|
enableDismissOnClose={enableDismissOnClose}
|
||||||
stackBehavior={stackBehavior}
|
stackBehavior={stackBehavior}
|
||||||
backgroundComponent={ComboboxSheetBackground}
|
backgroundComponent={ComboboxSheetBackground}
|
||||||
handleIndicatorStyle={styles.bottomSheetHandle}
|
handleIndicatorStyle={{ backgroundColor: theme.colors.palette.zinc[600] }}
|
||||||
keyboardBehavior="extend"
|
keyboardBehavior="extend"
|
||||||
keyboardBlurBehavior="restore"
|
keyboardBlurBehavior="restore"
|
||||||
>
|
>
|
||||||
<View style={styles.bottomSheetHeader}>
|
<View style={styles.bottomSheetHeader}>
|
||||||
<Text style={styles.comboboxTitle}>{title}</Text>
|
<Text style={[styles.comboboxTitle, { color: theme.colors.foreground }]}>{title}</Text>
|
||||||
</View>
|
</View>
|
||||||
{stickyHeader}
|
{stickyHeader}
|
||||||
{!children && searchable ? searchInput : null}
|
{!children && searchable ? searchInput : null}
|
||||||
@@ -909,14 +924,6 @@ const styles = StyleSheet.create((theme) => ({
|
|||||||
color: theme.colors.foregroundMuted,
|
color: theme.colors.foregroundMuted,
|
||||||
fontSize: theme.fontSize.sm,
|
fontSize: theme.fontSize.sm,
|
||||||
},
|
},
|
||||||
bottomSheetBackground: {
|
|
||||||
backgroundColor: theme.colors.surface0,
|
|
||||||
borderTopLeftRadius: theme.borderRadius["2xl"],
|
|
||||||
borderTopRightRadius: theme.borderRadius["2xl"],
|
|
||||||
},
|
|
||||||
bottomSheetHandle: {
|
|
||||||
backgroundColor: theme.colors.palette.zinc[600],
|
|
||||||
},
|
|
||||||
bottomSheetHeader: {
|
bottomSheetHeader: {
|
||||||
paddingHorizontal: theme.spacing[6],
|
paddingHorizontal: theme.spacing[6],
|
||||||
paddingBottom: theme.spacing[2],
|
paddingBottom: theme.spacing[2],
|
||||||
|
|||||||
@@ -349,6 +349,7 @@ export function ContextMenuContent({
|
|||||||
testID?: string;
|
testID?: string;
|
||||||
}>): ReactElement | null {
|
}>): ReactElement | null {
|
||||||
const context = useContextMenuContext("ContextMenuContent");
|
const context = useContextMenuContext("ContextMenuContent");
|
||||||
|
const { theme } = useUnistyles();
|
||||||
const webScrollbarStyle = useWebScrollbarStyle();
|
const webScrollbarStyle = useWebScrollbarStyle();
|
||||||
const isMobile = useIsCompactFormFactor();
|
const isMobile = useIsCompactFormFactor();
|
||||||
const useMobileSheet = isMobile && mobileMode === "sheet";
|
const useMobileSheet = isMobile && mobileMode === "sheet";
|
||||||
@@ -475,8 +476,14 @@ export function ContextMenuContent({
|
|||||||
onChange={handleSheetChange}
|
onChange={handleSheetChange}
|
||||||
backdropComponent={renderSheetBackdrop}
|
backdropComponent={renderSheetBackdrop}
|
||||||
enablePanDownToClose
|
enablePanDownToClose
|
||||||
backgroundStyle={styles.sheetBackground}
|
backgroundStyle={[
|
||||||
handleIndicatorStyle={styles.sheetHandle}
|
styles.sheetBackground,
|
||||||
|
{
|
||||||
|
backgroundColor: theme.colors.surface0,
|
||||||
|
borderColor: theme.colors.border,
|
||||||
|
},
|
||||||
|
]}
|
||||||
|
handleIndicatorStyle={[styles.sheetHandle, { backgroundColor: theme.colors.surface2 }]}
|
||||||
keyboardBehavior="extend"
|
keyboardBehavior="extend"
|
||||||
keyboardBlurBehavior="restore"
|
keyboardBlurBehavior="restore"
|
||||||
>
|
>
|
||||||
|
|||||||
Reference in New Issue
Block a user