mirror of
https://github.com/0xWheatyz/handler.git
synced 2026-08-30 09:46:24 +00:00
6319bf0fd4
Fetch the registered model backends (/claude/models) alongside the fleet poll and offer the enabled ones in a Model select on the spawn form, defaulting to the Claude subscription — the mobile counterpart of the web dashboard's per-spawn dropdown. The select is hidden when no backends are registered, and Select itself now takes value/label pairs as well as bare strings. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01731mKtVzsfeT4Vi3TvkR48
138 lines
3.7 KiB
TypeScript
138 lines
3.7 KiB
TypeScript
import React, { useState } from "react";
|
|
import {
|
|
Modal,
|
|
Pressable,
|
|
StyleSheet,
|
|
Text,
|
|
View,
|
|
} from "react-native";
|
|
import { fonts, radius, shadows, text } from "../theme/tokens";
|
|
import { useTheme } from "../theme/useTheme";
|
|
import { Icon } from "./Icon";
|
|
|
|
/**
|
|
* Leeworks Select, ported from components/forms/Select.jsx. Native <select>
|
|
* has no cross-platform styling in RN, so the field opens a bottom sheet of
|
|
* options — same visual field (value + chevron), real picking behavior.
|
|
* Options are either bare strings (value doubles as the label) or
|
|
* value/label pairs, matching the web dashboard's Select.
|
|
*/
|
|
export type SelectOption = string | { value: string; label: string };
|
|
|
|
export function Select({
|
|
label,
|
|
options,
|
|
value,
|
|
onChange,
|
|
}: {
|
|
label: string;
|
|
options: SelectOption[];
|
|
value: string;
|
|
onChange: (v: string) => void;
|
|
}) {
|
|
const { colors } = useTheme();
|
|
const [open, setOpen] = useState(false);
|
|
|
|
const opts = options.map((o) =>
|
|
typeof o === "string" ? { value: o, label: o } : o,
|
|
);
|
|
const current = opts.find((o) => o.value === value);
|
|
|
|
return (
|
|
<View style={{ gap: 6 }}>
|
|
<Text style={[styles.label, { color: colors.textHeading }]}>{label}</Text>
|
|
<Pressable
|
|
onPress={() => setOpen(true)}
|
|
style={[
|
|
styles.field,
|
|
{ backgroundColor: colors.surfaceCard, borderColor: colors.borderDefault },
|
|
]}
|
|
>
|
|
<Text style={[text.body, { color: colors.textHeading, flex: 1 }]}>
|
|
{current?.label ?? value}
|
|
</Text>
|
|
<Icon name="chevronDown" size={16} color={colors.textMuted} />
|
|
</Pressable>
|
|
|
|
<Modal visible={open} transparent animationType="fade" onRequestClose={() => setOpen(false)}>
|
|
<Pressable style={styles.backdrop} onPress={() => setOpen(false)}>
|
|
<Pressable
|
|
style={[
|
|
styles.sheet,
|
|
{ backgroundColor: colors.surfaceCard, borderColor: colors.borderSubtle },
|
|
shadows.raised,
|
|
]}
|
|
>
|
|
{opts.map((opt) => {
|
|
const on = opt.value === value;
|
|
return (
|
|
<Pressable
|
|
key={opt.value}
|
|
onPress={() => {
|
|
onChange(opt.value);
|
|
setOpen(false);
|
|
}}
|
|
style={({ pressed }) => [
|
|
styles.option,
|
|
pressed && { backgroundColor: colors.surfaceSunken },
|
|
]}
|
|
>
|
|
<Text
|
|
style={[
|
|
text.body,
|
|
{
|
|
color: colors.textHeading,
|
|
fontFamily: on ? fonts.bodySemiBold : fonts.bodyRegular,
|
|
flex: 1,
|
|
},
|
|
]}
|
|
>
|
|
{opt.label}
|
|
</Text>
|
|
{on && <Icon name="chevronRight" size={16} color={colors.textMuted} />}
|
|
</Pressable>
|
|
);
|
|
})}
|
|
</Pressable>
|
|
</Pressable>
|
|
</Modal>
|
|
</View>
|
|
);
|
|
}
|
|
|
|
const styles = StyleSheet.create({
|
|
label: {
|
|
fontFamily: fonts.bodySemiBold,
|
|
fontSize: 13,
|
|
lineHeight: 16,
|
|
},
|
|
field: {
|
|
height: 48,
|
|
flexDirection: "row",
|
|
alignItems: "center",
|
|
borderWidth: 1,
|
|
borderRadius: radius.md,
|
|
paddingHorizontal: 12,
|
|
gap: 8,
|
|
},
|
|
backdrop: {
|
|
flex: 1,
|
|
backgroundColor: "rgba(20,20,19,0.4)",
|
|
justifyContent: "flex-end",
|
|
padding: 20,
|
|
},
|
|
sheet: {
|
|
borderWidth: 1,
|
|
borderRadius: radius.xl,
|
|
overflow: "hidden",
|
|
marginBottom: 24,
|
|
},
|
|
option: {
|
|
flexDirection: "row",
|
|
alignItems: "center",
|
|
paddingVertical: 16,
|
|
paddingHorizontal: 20,
|
|
minHeight: 44,
|
|
},
|
|
});
|