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:
Claude
2026-07-20 18:48:30 +00:00
parent f143959b52
commit daecaa7e6b
31 changed files with 13423 additions and 0 deletions
+83
View File
@@ -0,0 +1,83 @@
import React, { useState } from "react";
import {
StyleSheet,
TextInput,
View,
type StyleProp,
type ViewStyle,
} from "react-native";
import { fonts, radius } from "../theme/tokens";
import { useTheme } from "../theme/useTheme";
/**
* Text field covering both the Leeworks single-line Input (answer reply) and
* the multiline task textarea on the spawn screen. Focus swaps the border to
* `--border-strong` per the Input component.
*/
export function TextField({
value,
onChangeText,
placeholder,
multiline = false,
height = 48,
style,
}: {
value: string;
onChangeText: (t: string) => void;
placeholder?: string;
multiline?: boolean;
/** For single-line this is the control height; for multiline, the box height. */
height?: number;
style?: StyleProp<ViewStyle>;
}) {
const { colors } = useTheme();
const [focus, setFocus] = useState(false);
return (
<View
style={[
styles.wrap,
{
height,
backgroundColor: colors.surfaceCard,
borderColor: focus ? colors.borderStrong : colors.borderDefault,
alignItems: multiline ? "stretch" : "center",
paddingVertical: multiline ? 12 : 0,
},
style,
]}
>
<TextInput
value={value}
onChangeText={onChangeText}
placeholder={placeholder}
placeholderTextColor={colors.textMuted}
multiline={multiline}
onFocus={() => setFocus(true)}
onBlur={() => setFocus(false)}
style={[
styles.input,
{
color: colors.textHeading,
textAlignVertical: multiline ? "top" : "center",
},
]}
/>
</View>
);
}
const styles = StyleSheet.create({
wrap: {
flexDirection: "row",
borderWidth: 1,
borderRadius: radius.md,
paddingHorizontal: 14,
},
input: {
flex: 1,
alignSelf: "stretch",
fontFamily: fonts.bodyRegular,
fontSize: 15,
padding: 0,
},
});