@medway-ui/native 1.6.1 → 1.7.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (224) hide show
  1. package/README.md +27 -2
  2. package/lib/commonjs/components/Alert.js +3 -1
  3. package/lib/commonjs/components/Alert.js.map +1 -1
  4. package/lib/commonjs/components/AlertDialog.js +5 -2
  5. package/lib/commonjs/components/AlertDialog.js.map +1 -1
  6. package/lib/commonjs/components/Badge.js +37 -2
  7. package/lib/commonjs/components/Badge.js.map +1 -1
  8. package/lib/commonjs/components/Button.js +32 -1
  9. package/lib/commonjs/components/Button.js.map +1 -1
  10. package/lib/commonjs/components/Carousel.js +12 -2
  11. package/lib/commonjs/components/Carousel.js.map +1 -1
  12. package/lib/commonjs/components/Checkbox.js +2 -0
  13. package/lib/commonjs/components/Checkbox.js.map +1 -1
  14. package/lib/commonjs/components/Chip.js +208 -0
  15. package/lib/commonjs/components/Chip.js.map +1 -0
  16. package/lib/commonjs/components/Choice.js +116 -0
  17. package/lib/commonjs/components/Choice.js.map +1 -0
  18. package/lib/commonjs/components/Dialog.js +27 -16
  19. package/lib/commonjs/components/Dialog.js.map +1 -1
  20. package/lib/commonjs/components/DropdownMenu.js +73 -57
  21. package/lib/commonjs/components/DropdownMenu.js.map +1 -1
  22. package/lib/commonjs/components/EmptyState.js +9 -2
  23. package/lib/commonjs/components/EmptyState.js.map +1 -1
  24. package/lib/commonjs/components/Feed.js +7 -3
  25. package/lib/commonjs/components/Feed.js.map +1 -1
  26. package/lib/commonjs/components/Feedback.js +77 -0
  27. package/lib/commonjs/components/Feedback.js.map +1 -0
  28. package/lib/commonjs/components/Filter.js +93 -0
  29. package/lib/commonjs/components/Filter.js.map +1 -0
  30. package/lib/commonjs/components/IndexLevel.js +4 -3
  31. package/lib/commonjs/components/IndexLevel.js.map +1 -1
  32. package/lib/commonjs/components/Input.js +149 -45
  33. package/lib/commonjs/components/Input.js.map +1 -1
  34. package/lib/commonjs/components/InvitePopover.js +4 -1
  35. package/lib/commonjs/components/InvitePopover.js.map +1 -1
  36. package/lib/commonjs/components/MemberRow.js +3 -4
  37. package/lib/commonjs/components/MemberRow.js.map +1 -1
  38. package/lib/commonjs/components/PostForm.js +18 -8
  39. package/lib/commonjs/components/PostForm.js.map +1 -1
  40. package/lib/commonjs/components/PrizeCollection.js +1 -1
  41. package/lib/commonjs/components/RadioGroup.js +7 -0
  42. package/lib/commonjs/components/RadioGroup.js.map +1 -1
  43. package/lib/commonjs/components/Select.js +1 -1
  44. package/lib/commonjs/components/Sheet.js +4 -1
  45. package/lib/commonjs/components/Sheet.js.map +1 -1
  46. package/lib/commonjs/components/Skeleton.js +55 -4
  47. package/lib/commonjs/components/Skeleton.js.map +1 -1
  48. package/lib/commonjs/components/Switch.js +2 -0
  49. package/lib/commonjs/components/Switch.js.map +1 -1
  50. package/lib/commonjs/components/Tabs.js +2 -0
  51. package/lib/commonjs/components/Tabs.js.map +1 -1
  52. package/lib/commonjs/components/Toast.js +355 -79
  53. package/lib/commonjs/components/Toast.js.map +1 -1
  54. package/lib/commonjs/editor/NoteEditor.js +41 -14
  55. package/lib/commonjs/editor/NoteEditor.js.map +1 -1
  56. package/lib/commonjs/editor/editorHtml.generated.js +1 -1
  57. package/lib/commonjs/editor/editorHtml.generated.js.map +1 -1
  58. package/lib/commonjs/hooks/useThemeColor.js +693 -65
  59. package/lib/commonjs/hooks/useThemeColor.js.map +1 -1
  60. package/lib/commonjs/hooks/useTrackedText.js +21 -0
  61. package/lib/commonjs/hooks/useTrackedText.js.map +1 -0
  62. package/lib/commonjs/index.js +46 -0
  63. package/lib/commonjs/index.js.map +1 -1
  64. package/lib/commonjs/lib/optional-deps.js +20 -0
  65. package/lib/commonjs/lib/optional-deps.js.map +1 -0
  66. package/lib/commonjs/lib/touch-target.js +19 -0
  67. package/lib/commonjs/lib/touch-target.js.map +1 -0
  68. package/lib/module/components/Alert.js +3 -1
  69. package/lib/module/components/Alert.js.map +1 -1
  70. package/lib/module/components/AlertDialog.js +5 -2
  71. package/lib/module/components/AlertDialog.js.map +1 -1
  72. package/lib/module/components/Badge.js +36 -2
  73. package/lib/module/components/Badge.js.map +1 -1
  74. package/lib/module/components/Button.js +32 -1
  75. package/lib/module/components/Button.js.map +1 -1
  76. package/lib/module/components/Carousel.js +12 -2
  77. package/lib/module/components/Carousel.js.map +1 -1
  78. package/lib/module/components/Checkbox.js +2 -0
  79. package/lib/module/components/Checkbox.js.map +1 -1
  80. package/lib/module/components/Chip.js +204 -0
  81. package/lib/module/components/Chip.js.map +1 -0
  82. package/lib/module/components/Choice.js +112 -0
  83. package/lib/module/components/Choice.js.map +1 -0
  84. package/lib/module/components/Dialog.js +28 -17
  85. package/lib/module/components/Dialog.js.map +1 -1
  86. package/lib/module/components/DropdownMenu.js +74 -58
  87. package/lib/module/components/DropdownMenu.js.map +1 -1
  88. package/lib/module/components/EmptyState.js +9 -2
  89. package/lib/module/components/EmptyState.js.map +1 -1
  90. package/lib/module/components/Feed.js +7 -3
  91. package/lib/module/components/Feed.js.map +1 -1
  92. package/lib/module/components/Feedback.js +74 -0
  93. package/lib/module/components/Feedback.js.map +1 -0
  94. package/lib/module/components/Filter.js +89 -0
  95. package/lib/module/components/Filter.js.map +1 -0
  96. package/lib/module/components/IndexLevel.js +4 -3
  97. package/lib/module/components/IndexLevel.js.map +1 -1
  98. package/lib/module/components/Input.js +149 -45
  99. package/lib/module/components/Input.js.map +1 -1
  100. package/lib/module/components/InvitePopover.js +4 -1
  101. package/lib/module/components/InvitePopover.js.map +1 -1
  102. package/lib/module/components/MemberRow.js +3 -4
  103. package/lib/module/components/MemberRow.js.map +1 -1
  104. package/lib/module/components/PostForm.js +18 -8
  105. package/lib/module/components/PostForm.js.map +1 -1
  106. package/lib/module/components/PrizeCollection.js +1 -1
  107. package/lib/module/components/RadioGroup.js +7 -0
  108. package/lib/module/components/RadioGroup.js.map +1 -1
  109. package/lib/module/components/Select.js +1 -1
  110. package/lib/module/components/Sheet.js +4 -1
  111. package/lib/module/components/Sheet.js.map +1 -1
  112. package/lib/module/components/Skeleton.js +56 -5
  113. package/lib/module/components/Skeleton.js.map +1 -1
  114. package/lib/module/components/Switch.js +2 -0
  115. package/lib/module/components/Switch.js.map +1 -1
  116. package/lib/module/components/Tabs.js +2 -0
  117. package/lib/module/components/Tabs.js.map +1 -1
  118. package/lib/module/components/Toast.js +360 -84
  119. package/lib/module/components/Toast.js.map +1 -1
  120. package/lib/module/editor/NoteEditor.js +41 -14
  121. package/lib/module/editor/NoteEditor.js.map +1 -1
  122. package/lib/module/editor/editorHtml.generated.js +1 -1
  123. package/lib/module/editor/editorHtml.generated.js.map +1 -1
  124. package/lib/module/hooks/useThemeColor.js +693 -65
  125. package/lib/module/hooks/useThemeColor.js.map +1 -1
  126. package/lib/module/hooks/useTrackedText.js +18 -0
  127. package/lib/module/hooks/useTrackedText.js.map +1 -0
  128. package/lib/module/index.js +8 -0
  129. package/lib/module/index.js.map +1 -1
  130. package/lib/module/lib/optional-deps.js +16 -0
  131. package/lib/module/lib/optional-deps.js.map +1 -0
  132. package/lib/module/lib/touch-target.js +15 -0
  133. package/lib/module/lib/touch-target.js.map +1 -0
  134. package/lib/typescript/components/Alert.d.ts.map +1 -1
  135. package/lib/typescript/components/AlertDialog.d.ts.map +1 -1
  136. package/lib/typescript/components/Badge.d.ts +1 -1
  137. package/lib/typescript/components/Badge.d.ts.map +1 -1
  138. package/lib/typescript/components/Button.d.ts.map +1 -1
  139. package/lib/typescript/components/Carousel.d.ts +1 -1
  140. package/lib/typescript/components/Carousel.d.ts.map +1 -1
  141. package/lib/typescript/components/Checkbox.d.ts.map +1 -1
  142. package/lib/typescript/components/Chip.d.ts +30 -0
  143. package/lib/typescript/components/Chip.d.ts.map +1 -0
  144. package/lib/typescript/components/Choice.d.ts +17 -0
  145. package/lib/typescript/components/Choice.d.ts.map +1 -0
  146. package/lib/typescript/components/Dialog.d.ts.map +1 -1
  147. package/lib/typescript/components/DropdownMenu.d.ts +0 -7
  148. package/lib/typescript/components/DropdownMenu.d.ts.map +1 -1
  149. package/lib/typescript/components/EmptyState.d.ts +3 -2
  150. package/lib/typescript/components/EmptyState.d.ts.map +1 -1
  151. package/lib/typescript/components/Feed.d.ts.map +1 -1
  152. package/lib/typescript/components/Feedback.d.ts +17 -0
  153. package/lib/typescript/components/Feedback.d.ts.map +1 -0
  154. package/lib/typescript/components/Filter.d.ts +17 -0
  155. package/lib/typescript/components/Filter.d.ts.map +1 -0
  156. package/lib/typescript/components/IndexLevel.d.ts.map +1 -1
  157. package/lib/typescript/components/Input.d.ts +3 -3
  158. package/lib/typescript/components/Input.d.ts.map +1 -1
  159. package/lib/typescript/components/InvitePopover.d.ts.map +1 -1
  160. package/lib/typescript/components/MemberRow.d.ts.map +1 -1
  161. package/lib/typescript/components/PostForm.d.ts.map +1 -1
  162. package/lib/typescript/components/RadioGroup.d.ts.map +1 -1
  163. package/lib/typescript/components/Sheet.d.ts.map +1 -1
  164. package/lib/typescript/components/Skeleton.d.ts +1 -1
  165. package/lib/typescript/components/Skeleton.d.ts.map +1 -1
  166. package/lib/typescript/components/Switch.d.ts.map +1 -1
  167. package/lib/typescript/components/Tabs.d.ts.map +1 -1
  168. package/lib/typescript/components/Tag.d.ts +2 -2
  169. package/lib/typescript/components/ThemedIcon.d.ts +1 -1
  170. package/lib/typescript/components/ThemedIcon.d.ts.map +1 -1
  171. package/lib/typescript/components/Toast.d.ts +24 -13
  172. package/lib/typescript/components/Toast.d.ts.map +1 -1
  173. package/lib/typescript/editor/NoteEditor.d.ts.map +1 -1
  174. package/lib/typescript/editor/editorHtml.generated.d.ts.map +1 -1
  175. package/lib/typescript/hooks/useThemeColor.d.ts +0 -10
  176. package/lib/typescript/hooks/useThemeColor.d.ts.map +1 -1
  177. package/lib/typescript/hooks/useTrackedText.d.ts +6 -0
  178. package/lib/typescript/hooks/useTrackedText.d.ts.map +1 -0
  179. package/lib/typescript/index.d.ts +9 -1
  180. package/lib/typescript/index.d.ts.map +1 -1
  181. package/lib/typescript/lib/optional-deps.d.ts +23 -0
  182. package/lib/typescript/lib/optional-deps.d.ts.map +1 -0
  183. package/lib/typescript/lib/touch-target.d.ts +5 -0
  184. package/lib/typescript/lib/touch-target.d.ts.map +1 -0
  185. package/package.json +6 -1
  186. package/setup/createMedwayUiNavigator.tsx +4 -7
  187. package/setup/metro.js +12 -3
  188. package/setup/tokens.native.css +268 -258
  189. package/src/components/Alert.tsx +3 -1
  190. package/src/components/AlertDialog.tsx +8 -3
  191. package/src/components/Badge.tsx +51 -3
  192. package/src/components/Button.tsx +39 -1
  193. package/src/components/Carousel.tsx +17 -3
  194. package/src/components/Checkbox.tsx +2 -0
  195. package/src/components/Chip.tsx +227 -0
  196. package/src/components/Choice.tsx +139 -0
  197. package/src/components/Dialog.tsx +39 -23
  198. package/src/components/DropdownMenu.tsx +99 -87
  199. package/src/components/EmptyState.tsx +18 -3
  200. package/src/components/Feed.tsx +4 -3
  201. package/src/components/Feedback.tsx +111 -0
  202. package/src/components/Filter.tsx +106 -0
  203. package/src/components/IndexLevel.tsx +8 -10
  204. package/src/components/Input.tsx +142 -54
  205. package/src/components/InvitePopover.tsx +5 -1
  206. package/src/components/MemberRow.tsx +3 -5
  207. package/src/components/PostForm.tsx +16 -9
  208. package/src/components/PrizeCollection.tsx +1 -1
  209. package/src/components/RadioGroup.tsx +8 -0
  210. package/src/components/Select.tsx +1 -1
  211. package/src/components/Sheet.tsx +4 -1
  212. package/src/components/Skeleton.tsx +47 -6
  213. package/src/components/Switch.tsx +2 -0
  214. package/src/components/Tabs.tsx +2 -0
  215. package/src/components/ThemedIcon.tsx +1 -1
  216. package/src/components/Toast.tsx +489 -87
  217. package/src/editor/NoteEditor.tsx +50 -11
  218. package/src/editor/editorHtml.generated.ts +1 -1
  219. package/src/hooks/useThemeColor.ts +757 -64
  220. package/src/hooks/useTrackedText.ts +20 -0
  221. package/src/index.ts +36 -1
  222. package/src/lib/optional-deps.ts +27 -0
  223. package/src/lib/touch-target.ts +12 -0
  224. package/setup/index.tsx +0 -42
@@ -1,5 +1,22 @@
1
- import { useState, useEffect, type ReactElement, type ReactNode } from "react";
2
- import { View, Pressable, type ViewProps } from "react-native";
1
+ import {
2
+ createContext,
3
+ useContext,
4
+ useState,
5
+ useEffect,
6
+ useRef,
7
+ type ReactElement,
8
+ type ReactNode,
9
+ } from "react";
10
+ import {
11
+ View,
12
+ Pressable,
13
+ StyleSheet,
14
+ type ViewProps,
15
+ type StyleProp,
16
+ type ViewStyle,
17
+ } from "react-native";
18
+ import { useUnstableNativeVariable } from "nativewind";
19
+ import { SafeAreaInsetsContext } from "react-native-safe-area-context";
3
20
 
4
21
  import { Text } from "@/components/Text";
5
22
  import { cva } from "class-variance-authority";
@@ -7,22 +24,41 @@ import Animated, {
7
24
  useSharedValue,
8
25
  useAnimatedStyle,
9
26
  withTiming,
10
- withDelay,
11
27
  runOnJS,
12
28
  } from "react-native-reanimated";
13
29
  import { cn } from "@/lib/utils";
30
+ import { loadLinearGradient } from "@/lib/optional-deps";
14
31
  import {
15
32
  CircleCheck,
16
33
  CloseMd,
17
34
  Info,
18
35
  OctagonWarning,
36
+ Rocket,
19
37
  TriangleWarning,
20
38
  } from "@medway-ui/icons/native";
21
39
  import { Button, type ButtonProps } from "@/components/Button";
40
+ import { Loading } from "@/components/Loading";
22
41
  import { useToast } from "@/hooks/use-toast";
23
42
  import { useThemeColor } from "@/hooks/useThemeColor";
24
43
 
25
- type ToastVariant = "primary" | "secondary" | "success" | "warning" | "error";
44
+ const GRADIENT_VARIANTS = [
45
+ "green-gradient",
46
+ "gold-gradient",
47
+ "silver-gradient",
48
+ "bronze-gradient",
49
+ ] as const;
50
+
51
+ type ToastGradientVariant = (typeof GRADIENT_VARIANTS)[number];
52
+
53
+ type ToastSolidVariant =
54
+ | "primary"
55
+ | "secondary"
56
+ | "success"
57
+ | "warning"
58
+ | "error";
59
+
60
+ type ToastVariant = ToastSolidVariant | ToastGradientVariant;
61
+
26
62
  type ToastActionVariant = ToastVariant;
27
63
 
28
64
  type ToastActionElement = ReactElement<typeof ToastAction>;
@@ -33,55 +69,174 @@ interface ToastProps extends ViewProps {
33
69
  duration?: number;
34
70
  open?: boolean;
35
71
  onOpenChange?: (open: boolean) => void;
72
+ icon?: ReactNode;
36
73
  children?: ReactNode;
37
74
  }
38
75
 
39
76
  interface ToastActionProps extends Omit<ButtonProps, "variant"> {
40
77
  variant?: ToastActionVariant;
41
78
  altText?: string;
42
- onPress?: () => void;
43
79
  }
44
80
 
45
- interface ToastCloseProps extends Omit<ViewProps, "children"> {
46
- onPress?: () => void;
81
+ interface ToastCloseProps extends Omit<ButtonProps, "variant" | "children"> {
47
82
  label?: string;
83
+ variant?: ToastVariant;
48
84
  }
49
85
 
86
+ type GradientStyle = {
87
+ /** Design tokens for LinearGradient color stops (aligned with web/Figma). */
88
+ colorTokens: [string, string];
89
+ locations: [number, number];
90
+ angleDeg: number;
91
+ textClassName: string;
92
+ pressedClassName: string;
93
+ iconToken: string;
94
+ solidFallbackClassName: string;
95
+ /** Optional solid layer under the gradient (green uses white underlay on web). */
96
+ underlayClassName?: string;
97
+ };
98
+
99
+ /** Podium / green gradient visuals — stops & angles match `@medway-ui/core` Toast. */
100
+ const GRADIENT_STYLES: Record<ToastGradientVariant, GradientStyle> = {
101
+ "green-gradient": {
102
+ colorTokens: ["secondary", "secondary-12"],
103
+ locations: [0.29, 1],
104
+ angleDeg: 124,
105
+ textClassName: "text-secondary-darken-4",
106
+ pressedClassName: "active:bg-black-8",
107
+ iconToken: "secondary-darken-4",
108
+ solidFallbackClassName: "bg-secondary",
109
+ underlayClassName: "bg-white",
110
+ },
111
+ "gold-gradient": {
112
+ // Literal hex from Figma replaced by podium tokens on web (#72)
113
+ colorTokens: ["gold", "gold-lighten-6"],
114
+ locations: [0.29, 1],
115
+ angleDeg: 124,
116
+ textClassName: "text-gold-foreground-onfill",
117
+ pressedClassName: "active:bg-black-8",
118
+ iconToken: "gold-foreground-onfill",
119
+ solidFallbackClassName: "bg-gold",
120
+ },
121
+ "silver-gradient": {
122
+ colorTokens: ["silver-darken-4", "silver"],
123
+ locations: [0.05, 1],
124
+ angleDeg: 100,
125
+ textClassName: "text-white",
126
+ pressedClassName: "active:bg-white-16",
127
+ iconToken: "white",
128
+ solidFallbackClassName: "bg-silver-darken-4",
129
+ },
130
+ "bronze-gradient": {
131
+ colorTokens: ["bronze-darken-2", "bronze"],
132
+ locations: [0.02, 1],
133
+ angleDeg: 100,
134
+ textClassName: "text-white",
135
+ pressedClassName: "active:bg-white-16",
136
+ iconToken: "white",
137
+ solidFallbackClassName: "bg-bronze",
138
+ },
139
+ };
140
+
141
+ type ToastContextValue = {
142
+ variant: ToastVariant;
143
+ onOpenChange?: (open: boolean) => void;
144
+ };
145
+
146
+ const ToastContext = createContext<ToastContextValue>({
147
+ variant: "primary",
148
+ });
149
+
150
+ function useToastContext(override?: ToastVariant): ToastContextValue {
151
+ const ctx = useContext(ToastContext);
152
+ return {
153
+ variant: override ?? ctx.variant,
154
+ onOpenChange: ctx.onOpenChange,
155
+ };
156
+ }
157
+
158
+ const isGradientVariant = (v: ToastVariant): v is ToastGradientVariant =>
159
+ (GRADIENT_VARIANTS as readonly string[]).includes(v);
160
+
161
+ /** CSS angle (0deg = up, clockwise) → expo-linear-gradient start/end. */
162
+ function cssAngleToPoints(angleDeg: number) {
163
+ const rad = (angleDeg * Math.PI) / 180;
164
+ const dx = Math.sin(rad);
165
+ const dy = -Math.cos(rad);
166
+ return {
167
+ start: { x: 0.5 - dx / 2, y: 0.5 - dy / 2 },
168
+ end: { x: 0.5 + dx / 2, y: 0.5 + dy / 2 },
169
+ };
170
+ }
171
+
172
+ const RADIUS_LG_FALLBACK = 16;
173
+
174
+ function useRadiusLg(): number {
175
+ const value = useUnstableNativeVariable("--radius-lg");
176
+ if (typeof value === "number" && Number.isFinite(value)) return value;
177
+ if (typeof value === "string") {
178
+ const parsed = Number.parseFloat(value);
179
+ if (Number.isFinite(parsed)) return parsed;
180
+ }
181
+ return RADIUS_LG_FALLBACK;
182
+ }
183
+
184
+ /** Mirrors web `toastVariants` solid/gradient chrome (minus Radix animation attrs). */
50
185
  const toastVariants = cva(
51
- "flex-row items-center gap-3 w-full rounded-md bg-surface-elevated p-3 shadow-lg",
186
+ "flex-row items-center justify-between gap-4 w-full overflow-hidden shadow-lg",
52
187
  {
53
188
  variants: {
189
+ appearance: {
190
+ solid: "rounded-md bg-surface-elevated p-2 pl-4",
191
+ gradient: "rounded-lg p-4",
192
+ },
54
193
  variant: {
55
- primary: "border-l-4 border-primary-foreground",
56
- secondary: "border-l-4 border-secondary-foreground",
57
- success: "border-l-4 border-feedback-success-foreground",
58
- warning: "border-l-4 border-feedback-warning-foreground",
59
- error: "border-l-4 border-feedback-error-foreground",
194
+ primary: "",
195
+ secondary: "",
196
+ success: "",
197
+ warning: "",
198
+ error: "",
199
+ "green-gradient": "",
200
+ "gold-gradient": "",
201
+ "silver-gradient": "",
202
+ "bronze-gradient": "",
60
203
  },
61
204
  },
62
- defaultVariants: { variant: "primary" },
205
+ defaultVariants: { appearance: "solid", variant: "primary" },
63
206
  }
64
207
  );
65
208
 
66
- const toastIconVariants = cva("shrink-0", {
67
- variants: {
68
- variant: {
69
- primary: "text-primary-foreground",
70
- secondary: "text-secondary-foreground",
71
- success: "text-feedback-success-foreground",
72
- warning: "text-feedback-warning-foreground",
73
- error: "text-feedback-error-foreground",
74
- },
75
- },
76
- defaultVariants: { variant: "primary" },
77
- });
209
+ /** Solid title/description colors — web inherits from the root; RN Text does not. */
210
+ const toastSolidTextClassName: Record<ToastSolidVariant, string> = {
211
+ primary: "text-primary-foreground",
212
+ secondary: "text-secondary-foreground",
213
+ success: "text-feedback-success-foreground",
214
+ warning: "text-feedback-warning-foreground",
215
+ error: "text-feedback-error-foreground",
216
+ };
78
217
 
79
- const toastActionButtonVariantMap: Record<ToastActionVariant, ButtonProps["variant"]> = {
218
+ /** Soft fills for solid icons — approximates web `fill-*-background-soft` on SVG. */
219
+ const toastIconSoftFillClassName: Record<ToastSolidVariant, string> = {
220
+ primary: "bg-primary-background-soft",
221
+ secondary: "bg-secondary-background-soft",
222
+ success: "bg-feedback-success-background-soft",
223
+ warning: "bg-feedback-warning-background-soft",
224
+ error: "bg-feedback-error-background-soft",
225
+ };
226
+
227
+ const toastActionButtonVariantMap: Record<
228
+ ToastActionVariant,
229
+ ButtonProps["variant"]
230
+ > = {
80
231
  primary: "regular-soft",
81
232
  secondary: "emphasis-soft",
82
233
  success: "success-soft",
83
234
  warning: "warning-soft",
84
235
  error: "error-soft",
236
+ "green-gradient": "regular-basic",
237
+ "gold-gradient": "regular-basic",
238
+ "silver-gradient": "regular-basic",
239
+ "bronze-gradient": "regular-basic",
85
240
  };
86
241
 
87
242
  const toastIconColorTokens: Record<ToastVariant, string> = {
@@ -90,6 +245,10 @@ const toastIconColorTokens: Record<ToastVariant, string> = {
90
245
  success: "feedback-success-foreground",
91
246
  warning: "feedback-warning-foreground",
92
247
  error: "feedback-error-foreground",
248
+ "green-gradient": GRADIENT_STYLES["green-gradient"].iconToken,
249
+ "gold-gradient": GRADIENT_STYLES["gold-gradient"].iconToken,
250
+ "silver-gradient": GRADIENT_STYLES["silver-gradient"].iconToken,
251
+ "bronze-gradient": GRADIENT_STYLES["bronze-gradient"].iconToken,
93
252
  };
94
253
 
95
254
  const variantIcons = {
@@ -98,61 +257,190 @@ const variantIcons = {
98
257
  success: CircleCheck,
99
258
  warning: TriangleWarning,
100
259
  error: OctagonWarning,
260
+ "green-gradient": Rocket,
261
+ "gold-gradient": Rocket,
262
+ "silver-gradient": Rocket,
263
+ "bronze-gradient": Rocket,
101
264
  } as const;
102
265
 
266
+ const ACTION_SIZE_CLASSES: Record<"sm" | "md" | "lg" | "xl", string> = {
267
+ sm: "min-h-8 min-w-8 px-3 py-1",
268
+ md: "min-h-10 min-w-10 px-3.5 py-2",
269
+ lg: "min-h-12 min-w-12 px-4 py-3",
270
+ xl: "min-h-14 min-w-14 px-[18px] py-3.5",
271
+ };
272
+
273
+ const ACTION_TEXT_SIZE: Record<"sm" | "md" | "lg" | "xl", string> = {
274
+ sm: "text-xs",
275
+ md: "text-sm",
276
+ lg: "text-md",
277
+ xl: "text-lg",
278
+ };
279
+
280
+ const DEFAULT_TOAST_DURATION = 5000;
281
+ const TOAST_ENTER_MS = 300;
282
+ const TOAST_EXIT_MS = 400;
283
+
284
+ function ToastClose({
285
+ className,
286
+ onPress,
287
+ label = "Fechar",
288
+ variant: variantProp,
289
+ ...props
290
+ }: ToastCloseProps) {
291
+ const { variant, onOpenChange } = useToastContext(variantProp);
292
+ const gradient = isGradientVariant(variant) ? GRADIENT_STYLES[variant] : null;
293
+ // Web solid close uses `regular-basic` → primary-foreground (not gray).
294
+ const closeIconColor = useThemeColor(
295
+ gradient ? gradient.iconToken : "primary-foreground"
296
+ );
297
+
298
+ return (
299
+ <Button
300
+ onPress={onPress ?? (() => onOpenChange?.(false))}
301
+ accessibilityLabel={label}
302
+ size="sm"
303
+ variant="regular-basic"
304
+ iconLeft={
305
+ closeIconColor ? (
306
+ <CloseMd width={16} height={16} color={closeIconColor} />
307
+ ) : undefined
308
+ }
309
+ className={cn(
310
+ "shrink-0",
311
+ // Match web `gradientActionClassName` (fixed text + pressed bg on gradients).
312
+ gradient?.textClassName,
313
+ gradient?.pressedClassName,
314
+ className
315
+ )}
316
+ {...props}
317
+ />
318
+ );
319
+ }
320
+
321
+ function ToastGradientFill({ variant }: { variant: ToastGradientVariant }) {
322
+ const style = GRADIENT_STYLES[variant];
323
+ const LinearGradient = loadLinearGradient();
324
+ const borderRadius = useRadiusLg();
325
+ const color0 = useThemeColor(style.colorTokens[0]);
326
+ const color1 = useThemeColor(style.colorTokens[1]);
327
+ const { start, end } = cssAngleToPoints(style.angleDeg);
328
+
329
+ if (!LinearGradient || !color0 || !color1) {
330
+ return (
331
+ <View
332
+ pointerEvents="none"
333
+ className={style.solidFallbackClassName}
334
+ style={[StyleSheet.absoluteFill, { borderRadius }]}
335
+ />
336
+ );
337
+ }
338
+
339
+ return (
340
+ <>
341
+ {style.underlayClassName ? (
342
+ <View
343
+ pointerEvents="none"
344
+ className={style.underlayClassName}
345
+ style={[StyleSheet.absoluteFill, { borderRadius }]}
346
+ />
347
+ ) : null}
348
+ <LinearGradient
349
+ colors={[color0, color1]}
350
+ locations={style.locations}
351
+ start={start}
352
+ end={end}
353
+ pointerEvents="none"
354
+ style={[StyleSheet.absoluteFill, { borderRadius }]}
355
+ />
356
+ </>
357
+ );
358
+ }
359
+
360
+ function ToastDefaultIcon({ variant }: { variant: ToastVariant }) {
361
+ const IconComponent = variantIcons[variant];
362
+ const iconColor = useThemeColor(toastIconColorTokens[variant]);
363
+ if (!iconColor) return null;
364
+
365
+ const softFill = !isGradientVariant(variant)
366
+ ? toastIconSoftFillClassName[variant]
367
+ : null;
368
+
369
+ return (
370
+ <View className="size-6 shrink-0 items-center justify-center">
371
+ {softFill ? (
372
+ <View
373
+ className={cn("absolute h-[18px] w-[18px] rounded-full", softFill)}
374
+ />
375
+ ) : null}
376
+ <IconComponent width={24} height={24} color={iconColor} />
377
+ </View>
378
+ );
379
+ }
380
+
103
381
  function ToastItem({
104
382
  className,
105
383
  variant = "primary",
106
384
  showIcon = true,
385
+ duration = DEFAULT_TOAST_DURATION,
107
386
  open = true,
108
387
  onOpenChange,
388
+ icon,
109
389
  children,
110
390
  ...props
111
391
  }: ToastProps) {
112
392
  const [visible, setVisible] = useState(open);
393
+ const onOpenChangeRef = useRef(onOpenChange);
394
+ onOpenChangeRef.current = onOpenChange;
113
395
  const translateY = useSharedValue(open ? 0 : -80);
114
396
  const opacity = useSharedValue(open ? 1 : 0);
397
+ const gradient = isGradientVariant(variant);
115
398
 
116
399
  const animatedStyle = useAnimatedStyle(() => ({
117
400
  transform: [{ translateY: translateY.value }],
118
401
  opacity: opacity.value,
119
402
  }));
120
- const iconColor = useThemeColor(toastIconColorTokens[variant]);
121
- const closeIconColor = useThemeColor("gray-foreground");
122
403
 
123
404
  useEffect(() => {
124
405
  if (open) {
125
406
  setVisible(true);
126
- translateY.value = withTiming(0, { duration: 300 });
127
- opacity.value = withTiming(1, { duration: 300 });
407
+ translateY.value = withTiming(0, { duration: TOAST_ENTER_MS });
408
+ opacity.value = withTiming(1, { duration: TOAST_ENTER_MS });
128
409
  } else {
129
- translateY.value = withDelay(50, withTiming(-80, { duration: 200 }));
130
- opacity.value = withTiming(0, { duration: 200 }, (finished) => {
410
+ translateY.value = withTiming(-80, { duration: TOAST_EXIT_MS });
411
+ opacity.value = withTiming(0, { duration: TOAST_EXIT_MS }, (finished) => {
131
412
  if (finished) runOnJS(setVisible)(false);
132
413
  });
133
414
  }
134
415
  }, [open]);
135
416
 
417
+ useEffect(() => {
418
+ if (!open || !Number.isFinite(duration) || duration <= 0) return;
419
+
420
+ const timeout = setTimeout(() => onOpenChangeRef.current?.(false), duration);
421
+ return () => clearTimeout(timeout);
422
+ }, [open, duration]);
423
+
136
424
  if (!visible) return null;
137
425
 
138
- const IconComponent = variantIcons[variant];
426
+ const resolvedIcon = icon ?? <ToastDefaultIcon variant={variant} />;
427
+ const appearance = gradient ? "gradient" : "solid";
428
+ const containerClassName = cn(
429
+ toastVariants({ appearance, variant }),
430
+ className
431
+ );
139
432
 
140
433
  return (
141
434
  <Animated.View style={animatedStyle}>
142
- <View className={cn(toastVariants({ variant }), className)} {...props}>
143
- {showIcon && iconColor && (
144
- <IconComponent width={20} height={20} color={iconColor} />
145
- )}
146
- <View className="flex-1 flex-row items-center justify-between gap-3">
147
- {children}
435
+ <ToastContext.Provider value={{ variant, onOpenChange }}>
436
+ <View className={containerClassName} {...props}>
437
+ {gradient ? <ToastGradientFill variant={variant} /> : null}
438
+ {showIcon && resolvedIcon}
439
+ <View className="min-w-0 flex-1 flex-row items-center justify-between gap-4">
440
+ {children}
441
+ </View>
148
442
  </View>
149
- <Pressable
150
- onPress={() => onOpenChange?.(false)}
151
- className="rounded-pill p-1 active:bg-gray-background-soft"
152
- >
153
- {closeIconColor && <CloseMd width={16} height={16} color={closeIconColor} />}
154
- </Pressable>
155
- </View>
443
+ </ToastContext.Provider>
156
444
  </Animated.View>
157
445
  );
158
446
  }
@@ -163,6 +451,7 @@ function Toast({
163
451
  duration,
164
452
  open,
165
453
  onOpenChange,
454
+ icon,
166
455
  children,
167
456
  ...props
168
457
  }: ToastProps) {
@@ -173,6 +462,7 @@ function Toast({
173
462
  duration={duration}
174
463
  open={open}
175
464
  onOpenChange={onOpenChange}
465
+ icon={icon}
176
466
  {...props}
177
467
  >
178
468
  {children}
@@ -182,17 +472,73 @@ function Toast({
182
472
 
183
473
  function ToastAction({
184
474
  className,
185
- variant = "primary",
475
+ variant: variantProp,
186
476
  size = "sm",
187
- altText,
477
+ altText = "Toast action",
188
478
  children,
479
+ loading,
480
+ iconLeft,
481
+ iconRight,
482
+ disabled,
483
+ containerStyle: _containerStyle,
484
+ badge: _badge,
485
+ alert: _alert,
189
486
  ...props
190
487
  }: ToastActionProps) {
488
+ const { variant } = useToastContext(variantProp);
489
+ const resolvedSize = size ?? "sm";
490
+
491
+ if (isGradientVariant(variant)) {
492
+ const gradient = GRADIENT_STYLES[variant];
493
+ const isDisabled = Boolean(disabled || loading);
494
+
495
+ return (
496
+ <Pressable
497
+ accessibilityLabel={altText}
498
+ accessibilityRole="button"
499
+ accessibilityState={{ disabled: isDisabled, busy: Boolean(loading) }}
500
+ disabled={isDisabled}
501
+ className={cn(
502
+ "flex-row items-center justify-center gap-2 rounded-pill",
503
+ ACTION_SIZE_CLASSES[resolvedSize],
504
+ gradient.pressedClassName,
505
+ className
506
+ )}
507
+ {...props}
508
+ >
509
+ {loading ? (
510
+ <Loading size={resolvedSize === "sm" ? 16 : 20} />
511
+ ) : (
512
+ <>
513
+ {iconLeft}
514
+ {children != null && (
515
+ <Text
516
+ className={cn(
517
+ "font-semibold shrink-0",
518
+ ACTION_TEXT_SIZE[resolvedSize],
519
+ gradient.textClassName
520
+ )}
521
+ >
522
+ {children as ReactNode}
523
+ </Text>
524
+ )}
525
+ {iconRight}
526
+ </>
527
+ )}
528
+ </Pressable>
529
+ );
530
+ }
531
+
191
532
  return (
192
533
  <Button
193
534
  size={size}
194
535
  variant={toastActionButtonVariantMap[variant]}
195
536
  className={cn(className)}
537
+ loading={loading}
538
+ iconLeft={iconLeft}
539
+ iconRight={iconRight}
540
+ disabled={disabled}
541
+ accessibilityLabel={altText}
196
542
  {...props}
197
543
  >
198
544
  {children as ReactNode}
@@ -200,27 +546,53 @@ function ToastAction({
200
546
  );
201
547
  }
202
548
 
203
- function ToastTitle({ className, children }: ViewProps & { children?: ReactNode }) {
549
+ function ToastTitle({
550
+ className,
551
+ children,
552
+ }: ViewProps & { children?: ReactNode }) {
553
+ const { variant } = useToastContext();
554
+ const textClassName = isGradientVariant(variant)
555
+ ? GRADIENT_STYLES[variant].textClassName
556
+ : toastSolidTextClassName[variant];
557
+
204
558
  return (
205
- <Text className={cn("text-md font-medium text-content-title flex-1", className)}>
559
+ <Text
560
+ className={cn("text-md font-semibold flex-1", textClassName, className)}
561
+ >
206
562
  {children}
207
563
  </Text>
208
564
  );
209
565
  }
210
566
 
211
- function ToastDescription({ className, children }: ViewProps & { children?: ReactNode }) {
567
+ function ToastDescription({
568
+ className,
569
+ children,
570
+ }: ViewProps & { children?: ReactNode }) {
571
+ const { variant } = useToastContext();
572
+ const textClassName = isGradientVariant(variant)
573
+ ? GRADIENT_STYLES[variant].textClassName
574
+ : toastSolidTextClassName[variant];
575
+
212
576
  return (
213
- <Text className={cn("text-xs text-content-paragraph", className)}>
577
+ <Text className={cn("text-xs font-medium", textClassName, className)}>
214
578
  {children}
215
579
  </Text>
216
580
  );
217
581
  }
218
582
 
219
- function ToastViewport({ className, children, ...props }: ViewProps & { children?: ReactNode }) {
583
+ const TOAST_VIEWPORT_EXTRA_TOP_GAP = 8;
584
+ /** Matches previous `pt-12` (48) when SafeAreaProvider is absent: 40 + 8. */
585
+ const TOAST_VIEWPORT_FALLBACK_TOP = 40;
586
+
587
+ function ToastViewport({
588
+ className,
589
+ children,
590
+ ...props
591
+ }: ViewProps & { children?: ReactNode }) {
220
592
  return (
221
593
  <View
222
594
  pointerEvents="box-none"
223
- className={cn("absolute top-0 left-0 right-0 z-50 px-4 pt-12 gap-2", className)}
595
+ className={cn("absolute top-0 left-0 right-0 z-50 px-4 gap-2", className)}
224
596
  {...props}
225
597
  >
226
598
  {children}
@@ -228,41 +600,62 @@ function ToastViewport({ className, children, ...props }: ViewProps & { children
228
600
  );
229
601
  }
230
602
 
231
- function ToastClose({ className, onPress, label = "Close", ...props }: ToastCloseProps) {
232
- const closeIconColor = useThemeColor("gray-foreground");
233
-
234
- return (
235
- <Pressable
236
- onPress={onPress}
237
- accessibilityLabel={label}
238
- className={cn("rounded-pill p-1 active:bg-gray-background-soft", className)}
239
- {...props}
240
- >
241
- {closeIconColor && <CloseMd width={16} height={16} color={closeIconColor} />}
242
- </Pressable>
243
- );
244
- }
603
+ type ToastProviderProps = {
604
+ /** Extra gap below the top safe-area inset (default: 8). */
605
+ topOffset?: number;
606
+ style?: StyleProp<ViewStyle>;
607
+ className?: string;
608
+ };
245
609
 
246
- function ToastProvider() {
610
+ function ToastProvider({
611
+ topOffset = TOAST_VIEWPORT_EXTRA_TOP_GAP,
612
+ style,
613
+ className,
614
+ }: ToastProviderProps = {}) {
247
615
  const { toasts, dismiss } = useToast();
616
+ // Soft read: does not throw without SafeAreaProvider (keeps prior pt-12 fallback).
617
+ const insets = useContext(SafeAreaInsetsContext);
618
+ const paddingTop = (insets?.top ?? TOAST_VIEWPORT_FALLBACK_TOP) + topOffset;
248
619
 
249
620
  return (
250
- <ToastViewport>
251
- {toasts.map(({ id, title, description, action, open = true, variant, showIcon }) => (
252
- <Toast
253
- key={id}
254
- variant={variant}
255
- showIcon={showIcon}
256
- open={open}
257
- onOpenChange={(isOpen) => !isOpen && dismiss(id)}
258
- >
259
- <View className="flex-col gap-0.5">
260
- {title && <ToastTitle>{title}</ToastTitle>}
261
- {description && <ToastDescription>{description}</ToastDescription>}
262
- </View>
263
- {action}
264
- </Toast>
265
- ))}
621
+ <ToastViewport
622
+ className={className}
623
+ style={[{ paddingTop }, style]}
624
+ >
625
+ {toasts.map(
626
+ ({
627
+ id,
628
+ title,
629
+ description,
630
+ action,
631
+ open = true,
632
+ variant = "primary",
633
+ showIcon,
634
+ icon,
635
+ duration,
636
+ }) => (
637
+ <Toast
638
+ key={id}
639
+ variant={variant}
640
+ showIcon={showIcon}
641
+ icon={icon}
642
+ duration={duration}
643
+ open={open}
644
+ onOpenChange={(isOpen) => !isOpen && dismiss(id)}
645
+ >
646
+ <View className="min-w-0 flex-1 flex-col">
647
+ {title && <ToastTitle>{title}</ToastTitle>}
648
+ {description && (
649
+ <ToastDescription>{description}</ToastDescription>
650
+ )}
651
+ </View>
652
+ <View className="flex-row items-center gap-2">
653
+ {action}
654
+ <ToastClose variant={variant} />
655
+ </View>
656
+ </Toast>
657
+ )
658
+ )}
266
659
  </ToastViewport>
267
660
  );
268
661
  }
@@ -277,4 +670,13 @@ export {
277
670
  ToastViewport,
278
671
  toastVariants,
279
672
  };
280
- export type { ToastActionElement, ToastProps };
673
+ export type {
674
+ ToastActionElement,
675
+ ToastActionProps,
676
+ ToastActionVariant,
677
+ ToastCloseProps,
678
+ ToastGradientVariant,
679
+ ToastProps,
680
+ ToastProviderProps,
681
+ ToastVariant,
682
+ };