mirror of
https://github.com/0xWheatyz/handler.git
synced 2026-08-31 11:16:24 +00:00
Implement Handler mobile app (React Native + Expo iOS)
Build the committed 2a interactive prototype from Handler Mobile.dc.html as a real Expo app: six wired screens (Fleet, Agent detail, Answer, Spawn, Log, Settings) with the exact state logic ported from the design's renderVals(). - Port Leeworks tokens (colors light/dark, typography, spacing, radii, shadows) to typed RN values in src/theme. - Reimplement the design-system components used by the screens (Button, Badge, Icon, Switch, Select, TextField, segmented control, chip, tab bar). - Shared store mirrors the prototype's single-screen navigation and the Waiting -> Running flip when agt-7a1d is answered. - Real OS status bar / home indicator (safe-area insets) replace the mock phone chrome; dark mode follows system appearance. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01PdF2hHVQ4UUBFPtpeMQ81o
This commit is contained in:
@@ -0,0 +1,111 @@
|
||||
import React from "react";
|
||||
import {
|
||||
Pressable,
|
||||
StyleSheet,
|
||||
Text,
|
||||
View,
|
||||
type StyleProp,
|
||||
type ViewStyle,
|
||||
} from "react-native";
|
||||
import { fonts, radius } from "../theme/tokens";
|
||||
import { useTheme } from "../theme/useTheme";
|
||||
|
||||
/**
|
||||
* Leeworks Button, ported from components/forms/Button.jsx. Hover collapses
|
||||
* into press on touch; the primary/secondary/danger press tints are kept.
|
||||
*/
|
||||
|
||||
export type ButtonVariant = "primary" | "secondary" | "ghost" | "danger";
|
||||
export type ButtonSize = "sm" | "md" | "lg";
|
||||
|
||||
interface ButtonProps {
|
||||
children: string;
|
||||
variant?: ButtonVariant;
|
||||
size?: ButtonSize;
|
||||
onPress?: () => void;
|
||||
style?: StyleProp<ViewStyle>;
|
||||
}
|
||||
|
||||
const HEIGHT: Record<ButtonSize, number> = { sm: 32, md: 40, lg: 48 };
|
||||
const PAD_X: Record<ButtonSize, number> = { sm: 12, md: 16, lg: 20 };
|
||||
const FONT: Record<ButtonSize, number> = { sm: 13, md: 14, lg: 15 };
|
||||
|
||||
export function Button({
|
||||
children,
|
||||
variant = "primary",
|
||||
size = "md",
|
||||
onPress,
|
||||
style,
|
||||
}: ButtonProps) {
|
||||
const { colors } = useTheme();
|
||||
|
||||
return (
|
||||
<Pressable
|
||||
onPress={onPress}
|
||||
style={({ pressed }) => {
|
||||
const base: ViewStyle = {
|
||||
height: HEIGHT[size],
|
||||
paddingHorizontal: PAD_X[size],
|
||||
borderRadius: radius.md,
|
||||
};
|
||||
const byVariant: Record<ButtonVariant, ViewStyle> = {
|
||||
primary: {
|
||||
backgroundColor: pressed
|
||||
? colors.interactivePress
|
||||
: colors.interactive,
|
||||
},
|
||||
secondary: {
|
||||
backgroundColor: pressed ? "rgba(20,20,19,0.05)" : colors.surfaceCard,
|
||||
borderWidth: 1,
|
||||
borderColor: colors.borderDefault,
|
||||
},
|
||||
ghost: {
|
||||
backgroundColor: pressed ? "rgba(20,20,19,0.05)" : "transparent",
|
||||
},
|
||||
danger: {
|
||||
backgroundColor: pressed ? "#8c2f25" : colors.danger,
|
||||
},
|
||||
};
|
||||
return [styles.base, base, byVariant[variant], style];
|
||||
}}
|
||||
>
|
||||
<View style={styles.inner}>
|
||||
<Text
|
||||
numberOfLines={1}
|
||||
style={[
|
||||
styles.label,
|
||||
{
|
||||
fontSize: FONT[size],
|
||||
color:
|
||||
variant === "primary"
|
||||
? colors.textInverse
|
||||
: variant === "danger"
|
||||
? "#ffffff"
|
||||
: variant === "secondary"
|
||||
? colors.textHeading
|
||||
: colors.ink9,
|
||||
},
|
||||
]}
|
||||
>
|
||||
{children}
|
||||
</Text>
|
||||
</View>
|
||||
</Pressable>
|
||||
);
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
base: {
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
},
|
||||
inner: {
|
||||
flexDirection: "row",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
gap: 8,
|
||||
},
|
||||
label: {
|
||||
fontFamily: fonts.bodySemiBold,
|
||||
},
|
||||
});
|
||||
Reference in New Issue
Block a user