Files
han-app/VM4_Expo-mobile/src/ui.tsx
T

69 lines
4.3 KiB
TypeScript

import React from "react";
import { ActivityIndicator, Pressable, StyleSheet, Text, TextInput, View } from "react-native";
import { ApiError } from "./api";
import { colors, radii, spacing } from "./theme";
export const styles = StyleSheet.create({
page: { flexGrow: 1, backgroundColor: colors.background },
scrollContent: { padding: spacing.lg, gap: spacing.lg },
title: { fontSize: 20, fontWeight: "500", color: colors.foreground },
heading: { fontSize: 16, fontWeight: "500", color: colors.foreground },
text: { fontSize: 14, lineHeight: 20, color: colors.foreground },
muted: { fontSize: 12, lineHeight: 18, color: colors.mutedForeground },
card: { padding: spacing.lg, gap: spacing.md, borderWidth: 1, borderColor: colors.border, borderRadius: radii.lg, backgroundColor: colors.card },
input: { minHeight: 48, borderWidth: 1, borderColor: colors.border, borderRadius: radii.md, padding: spacing.md, fontSize: 16, backgroundColor: colors.inputBackground, color: colors.foreground },
textarea: { minHeight: 104, textAlignVertical: "top" },
row: { flexDirection: "row", flexWrap: "wrap", gap: spacing.sm, alignItems: "center" },
button: { minHeight: 44, justifyContent: "center", paddingHorizontal: 18, borderRadius: radii.lg, backgroundColor: colors.primary },
buttonSecondary: { backgroundColor: colors.secondary },
buttonDanger: { backgroundColor: colors.destructive },
buttonDisabled: { opacity: 0.5 },
buttonText: { color: colors.primaryForeground, fontWeight: "500", fontSize: 14, textAlign: "center" },
buttonTextSecondary: { color: colors.secondaryForeground },
link: { color: colors.primary, fontSize: 14, textDecorationLine: "underline", paddingVertical: spacing.sm },
badge: { borderRadius: radii.full, backgroundColor: colors.muted, color: colors.foreground, paddingHorizontal: 10, paddingVertical: 5, fontSize: 12, alignSelf: "flex-start" },
error: { borderLeftWidth: 4, borderColor: colors.destructive, backgroundColor: "#fef2f2", padding: spacing.md, color: "#7f1d1d", fontSize: 14 },
success: { borderLeftWidth: 4, borderColor: colors.success, backgroundColor: "#f0fdf4", padding: spacing.md, color: "#14532d", fontSize: 14 },
modalBackdrop: { position: "absolute", top: 0, right: 0, bottom: 0, left: 0, zIndex: 10, backgroundColor: "rgba(3, 2, 19, 0.45)", alignItems: "center", justifyContent: "center", padding: spacing.lg },
modal: { width: "100%", maxWidth: 360, borderRadius: radii.xl, backgroundColor: colors.card, padding: spacing.lg, gap: spacing.md, borderWidth: 1, borderColor: colors.border },
});
export function Button({ title, onPress, disabled, secondary, danger }: {
title: string; onPress: () => void; disabled?: boolean; secondary?: boolean; danger?: boolean;
}) {
return <Pressable
accessibilityRole="button"
disabled={disabled}
onPress={onPress}
style={({ pressed }) => [
styles.button, secondary && styles.buttonSecondary, danger && styles.buttonDanger,
disabled && styles.buttonDisabled, pressed && { opacity: 0.8 },
]}
>
<Text style={[styles.buttonText, secondary && styles.buttonTextSecondary]}>{title}</Text>
</Pressable>;
}
export function Field(props: React.ComponentProps<typeof TextInput> & { label: string; error?: string }) {
return <View style={{ gap: 6 }}>
<Text style={styles.text}>{props.label}</Text>
<TextInput accessibilityLabel={props.label} {...props} style={[styles.input, props.multiline && styles.textarea, props.style]} placeholderTextColor={colors.mutedForeground} />
{props.error && <Text accessibilityRole="alert" style={styles.error}>{props.error}</Text>}
</View>;
}
export function Loading() {
return <View accessibilityRole="progressbar" style={styles.row}><ActivityIndicator color={colors.primary} /><Text style={styles.muted}>Загрузка…</Text></View>;
}
export function ErrorNotice({ error, retry }: { error: unknown; retry?: () => void }) {
const requestId = error instanceof ApiError ? error.requestId : undefined;
return <View style={{ gap: spacing.sm }}>
<Text accessibilityRole="alert" style={styles.error}>
{error instanceof ApiError ? error.message : error instanceof Error ? error.message : "Произошла ошибка"}
{requestId ? `\nКод обращения: ${requestId}` : ""}
</Text>
{retry && <Button title="Повторить" secondary onPress={retry} />}
</View>;
}