1import { useEffect } from "react";
2import { AccessibilityInfo, Platform, Pressable, View } from "react-native";
3
4import { cn } from "../../lib/cn";
5import { AppText } from "./AppText";
6import { SymbolView, type AppSymbolName } from "./AppSymbol";
7import { GlassSurface } from "./GlassSurface";
8
9export function Notice(props: {
10 readonly message: string;
11 readonly tone?: "default" | "danger";
12 readonly icon?: AppSymbolName;
13 readonly iconTone?: "default" | "danger";
14 readonly floating?: boolean;
15 readonly onDismiss?: () => void;
16 readonly dismissLabel?: string;
17}) {
18 const danger = props.tone === "danger";
19 // oxlint-disable-next-line t3code/no-unreasoned-state-sync -- announces the message to VoiceOver through AccessibilityInfo
20 useEffect(() => {
21 // Live regions reach TalkBack; VoiceOver needs an explicit announcement.
22 if (Platform.OS === "ios") AccessibilityInfo.announceForAccessibility(props.message);
23 }, [props.message]);
24
25 const content = (
26 <View className="flex-row items-start gap-3">
27 {props.icon ? (
28 <View className="pt-0.5">
29 <SymbolView
30 name={props.icon}
31 size={16}
32 tintColorClassName={
33 danger || props.iconTone === "danger"
34 ? "accent-danger-foreground"
35 : "accent-icon-muted"
36 }
37 />
38 </View>
39 ) : null}
40 <AppText
41 selectable
42 textRole="footnote"
43 accessibilityLiveRegion="polite"
44 className={cn(
45 "min-w-0 flex-1",
46 danger ? "text-danger-foreground" : "text-foreground",
47 !props.floating && "font-t3-medium",
48 )}
49 >
50 {props.message}
51 </AppText>
52 {props.onDismiss ? (
53 <Pressable
54 accessibilityRole="button"
55 accessibilityLabel={props.dismissLabel ?? "Dismiss message"}
56 onPress={props.onDismiss}
57 hitSlop={10}
58 className="min-h-6 min-w-6 items-center justify-center active:opacity-60"
59 >
60 <SymbolView
61 name="xmark"
62 size={14}
63 tintColorClassName={danger ? "accent-danger-foreground" : "accent-icon-muted"}
64 />
65 </Pressable>
66 ) : null}
67 </View>
68 );
69 if (props.floating && Platform.OS === "ios")
70 return (
71 <GlassSurface className="p-4" style={{ borderRadius: 20 }}>
72 {content}
73 </GlassSurface>
74 );
75 return (
76 <View
77 className={cn(
78 "rounded-2xl border px-3.5 py-3",
79 danger ? "border-danger-border bg-danger" : "border-border bg-card",
80 props.floating && "p-4",
81 )}
82 >
83 {content}
84 </View>
85 );
86}