feat: project settings page with paseo.json editor

Adds a Projects settings flow for inspecting and editing per-project
paseo.json (worktree setup/teardown, scripts, terminals) directly from the
app. Reads/writes go through new daemon RPCs with revision-based optimistic
concurrency, and the screen always keeps its header and host picker visible
so a hung host doesn't trap the user.
This commit is contained in:
Mohamed Boudra
2026-04-26 11:37:47 +07:00
parent 18903bcecb
commit 219050a7ab
36 changed files with 6447 additions and 81 deletions
+32 -12
View File
@@ -6,7 +6,7 @@ import {
type PropsWithChildren,
type ReactElement,
} from "react";
import { Pressable, Text, View } from "react-native";
import { ActivityIndicator, Pressable, Text, View } from "react-native";
import type {
PressableProps,
PressableStateCallbackType,
@@ -104,6 +104,7 @@ export function Button({
style,
textStyle,
disabled,
loading = false,
accessibilityRole,
...props
}: PropsWithChildren<
@@ -113,10 +114,12 @@ export function Button({
leftIcon?: LeftIcon;
style?: StyleProp<ViewStyle>;
textStyle?: StyleProp<TextStyle>;
loading?: boolean;
}
>) {
const [hovered, setHovered] = useState(false);
const { theme } = useUnistyles();
const isDisabled = disabled || loading;
let variantStyle: ViewStyle;
if (variant === "default") {
@@ -150,10 +153,10 @@ export function Button({
sizeStyle,
variantStyle,
pressed ? styles.pressed : null,
disabled ? styles.disabled : null,
isDisabled ? styles.disabled : null,
style,
],
[sizeStyle, variantStyle, disabled, style],
[sizeStyle, variantStyle, isDisabled, style],
);
const resolvedTextStyle = useMemo(
@@ -168,7 +171,30 @@ export function Button({
[variant, textStyle, isGhostHovered],
);
const accessibilityState = useMemo(
() => ({ disabled: isDisabled, busy: loading }),
[isDisabled, loading],
);
function resolveIconColor(): string {
if (variant === "default") {
return theme.colors.accentForeground;
}
if (variant === "ghost") {
return isGhostHovered ? theme.colors.foreground : theme.colors.foregroundMuted;
}
return theme.colors.foreground;
}
function renderIcon() {
if (loading) {
return (
<View>
<ActivityIndicator size="small" color={resolveIconColor()} />
</View>
);
}
if (!leftIcon) return null;
// Pre-rendered element — pass through
@@ -176,14 +202,7 @@ export function Button({
return <View>{leftIcon}</View>;
}
let color: string;
if (variant === "default") {
color = theme.colors.accentForeground;
} else if (variant === "ghost") {
color = isGhostHovered ? theme.colors.foreground : theme.colors.foregroundMuted;
} else {
color = theme.colors.foreground;
}
const color = resolveIconColor();
const iconSize = ICON_SIZE[size];
// Render function
@@ -208,7 +227,8 @@ export function Button({
<Pressable
{...props}
accessibilityRole={accessibilityRole ?? "button"}
disabled={disabled}
accessibilityState={accessibilityState}
disabled={isDisabled}
onHoverIn={handleHoverIn}
onHoverOut={handleHoverOut}
style={pressableStyle}