69 lines
4.3 KiB
TypeScript
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>;
|
|
}
|