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
+89
View File
@@ -0,0 +1,89 @@
import React from "react";
import { ScrollView, StyleSheet, Text, View } from "react-native";
import { useSafeAreaInsets } from "react-native-safe-area-context";
import { text } from "../theme/tokens";
import { useTheme } from "../theme/useTheme";
import { Chip } from "../components/Chip";
import { Mono, SectionLabel } from "../components/primitives";
import { TabBar } from "../components/TabBar";
import { useAppState, type LogFilter } from "../state/AppState";
import { allLog } from "../data/mock";
const FILTERS: { key: LogFilter; label: string }[] = [
{ key: "all", label: "All" },
{ key: "handler", label: "handler" },
{ key: "errors", label: "Errors" },
];
export function LogScreen() {
const { colors } = useTheme();
const insets = useSafeAreaInsets();
const { logFilter, setLogFilter } = useAppState();
const entries = allLog.filter((e) =>
logFilter === "all" ? true : logFilter === "errors" ? e.err : e.p === "handler"
);
return (
<View style={[styles.page, { backgroundColor: colors.surfacePage }]}>
<View style={{ height: insets.top }} />
<View style={styles.header}>
<Text style={[text.h3, { color: colors.textHeading }]}>Log</Text>
<View style={styles.filters}>
{FILTERS.map((f) => (
<Chip
key={f.key}
label={f.label}
selected={logFilter === f.key}
onPress={() => setLogFilter(f.key)}
/>
))}
</View>
</View>
<View style={styles.todayLabel}>
<SectionLabel>Today</SectionLabel>
</View>
<ScrollView
style={{ flex: 1 }}
contentContainerStyle={[
styles.feed,
{ backgroundColor: colors.surfaceSunken, borderTopColor: colors.borderSubtle },
]}
showsVerticalScrollIndicator={false}
>
{entries.map((e, i) => (
<View key={`${e.t}-${i}`} style={styles.logRow}>
<Mono style={[styles.mono, { color: colors.ink4 }]}>{e.t}</Mono>
<Mono style={[styles.mono, styles.idCol, { color: colors.ink6 }]}>
{e.id}
</Mono>
<Mono style={[styles.mono, { color: colors[e.color], flex: 1 }]}>
{e.msg}
</Mono>
</View>
))}
</ScrollView>
<TabBar active="log" />
</View>
);
}
const styles = StyleSheet.create({
page: { flex: 1 },
header: { paddingHorizontal: 20, paddingTop: 20, paddingBottom: 16 },
filters: { flexDirection: "row", gap: 8, marginTop: 14 },
todayLabel: { paddingHorizontal: 20, paddingBottom: 8 },
feed: {
borderTopWidth: 1,
paddingHorizontal: 20,
paddingVertical: 16,
flexGrow: 1,
},
logRow: { flexDirection: "row", gap: 10 },
mono: { fontSize: 12.5, lineHeight: 26 },
idCol: { width: 66 },
});