feat(app): management foundation - client exposure, manage hub, shared shell

Groundwork for the full admin surface on mobile: the API client gains
the user-account and Claude-management types (skills, connectors,
plugins, permissions, users/auth, owner fields) plus an unauthenticated
authApi helper; AppState exposes the client and the new management
screen names; ManageShell/Field/ErrorNotice and a useResource hook give
the subscreens one shared page/fetch pattern; Settings gains Manage and
Account rows leading to the new hub.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01731mKtVzsfeT4Vi3TvkR48
This commit is contained in:
Claude
2026-08-13 13:48:50 +00:00
parent 25cf6c9f5b
commit 804baadddd
7 changed files with 447 additions and 4 deletions
+116
View File
@@ -0,0 +1,116 @@
import React from "react";
import {
KeyboardAvoidingView,
Platform,
ScrollView,
StyleSheet,
Text,
View,
} from "react-native";
import { useSafeAreaInsets } from "react-native-safe-area-context";
import { radius, text } from "../theme/tokens";
import { useTheme } from "../theme/useTheme";
import { PageHeader } from "./PageHeader";
import { useAppState, type Screen } from "../state/AppState";
/**
* Page chrome shared by every management subscreen (Settings → Manage → …):
* safe-area top, back header, title/subtitle, keyboard-aware scroll body.
*/
export function ManageShell({
title,
subtitle,
backTo = "manage",
children,
}: {
title: string;
subtitle?: string;
backTo?: Screen;
children: React.ReactNode;
}) {
const { colors } = useTheme();
const insets = useSafeAreaInsets();
const { go } = useAppState();
return (
<View style={[styles.page, { backgroundColor: colors.surfacePage }]}>
<View style={{ height: insets.top }} />
<KeyboardAvoidingView
style={styles.flex}
behavior={Platform.OS === "ios" ? "padding" : undefined}
>
<ScrollView
style={styles.flex}
contentContainerStyle={[
styles.content,
{ paddingBottom: insets.bottom + 24 },
]}
showsVerticalScrollIndicator={false}
keyboardShouldPersistTaps="handled"
>
<PageHeader leading="back" onLeadingPress={() => go(backTo)} title={title} />
{subtitle ? (
<Text style={[text.bodySm, { color: colors.textMuted, marginBottom: 16 }]}>
{subtitle}
</Text>
) : null}
{children}
</ScrollView>
</KeyboardAvoidingView>
</View>
);
}
/** Inline danger notice used by the manage screens for mutation errors. */
export function ErrorNotice({ message }: { message: string | null }) {
const { colors } = useTheme();
if (!message) return null;
return (
<View
style={[
styles.notice,
{ backgroundColor: colors.dangerTint, borderColor: colors.danger },
]}
>
<Text style={[text.bodySm, { color: colors.danger }]}>{message}</Text>
</View>
);
}
/** Labelled block wrapping a TextField (the manage forms' field-with-label pattern). */
export function Field({
label,
hint,
children,
}: {
label: string;
hint?: string;
children: React.ReactNode;
}) {
const { colors } = useTheme();
return (
<View>
<Text style={[text.label, { color: colors.textHeading, marginBottom: 6 }]}>
{label}
</Text>
{children}
{hint ? (
<Text style={[text.caption, { color: colors.textMuted, marginTop: 6 }]}>
{hint}
</Text>
) : null}
</View>
);
}
const styles = StyleSheet.create({
page: { flex: 1 },
flex: { flex: 1 },
content: { paddingTop: 8, paddingHorizontal: 20 },
notice: {
borderWidth: 1,
borderRadius: radius.md,
padding: 12,
marginBottom: 16,
},
});