@streamplace/components 0.0.1 → 0.7.0

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 (169) hide show
  1. package/LICENSE +18 -0
  2. package/README.md +35 -0
  3. package/dist/components/chat/chat-box.js +109 -0
  4. package/dist/components/chat/chat-message.js +76 -0
  5. package/dist/components/chat/chat.js +56 -0
  6. package/dist/components/chat/mention-suggestions.js +39 -0
  7. package/dist/components/chat/mod-view.js +33 -0
  8. package/dist/components/mobile-player/fullscreen.js +69 -0
  9. package/dist/components/mobile-player/fullscreen.native.js +151 -0
  10. package/dist/components/mobile-player/player.js +103 -0
  11. package/dist/components/mobile-player/props.js +1 -0
  12. package/dist/components/mobile-player/shared.js +51 -0
  13. package/dist/components/mobile-player/ui/countdown.js +79 -0
  14. package/dist/components/mobile-player/ui/index.js +5 -0
  15. package/dist/components/mobile-player/ui/input.js +38 -0
  16. package/dist/components/mobile-player/ui/metrics.js +40 -0
  17. package/dist/components/mobile-player/ui/streamer-context-menu.js +4 -0
  18. package/dist/components/mobile-player/ui/viewer-context-menu.js +20 -0
  19. package/dist/components/mobile-player/use-webrtc.js +232 -0
  20. package/dist/components/mobile-player/video.js +375 -0
  21. package/dist/components/mobile-player/video.native.js +238 -0
  22. package/dist/components/mobile-player/webrtc-diagnostics.js +106 -0
  23. package/dist/components/mobile-player/webrtc-primitives.js +25 -0
  24. package/dist/components/mobile-player/webrtc-primitives.native.js +1 -0
  25. package/dist/components/ui/button.js +220 -0
  26. package/dist/components/ui/dialog.js +203 -0
  27. package/dist/components/ui/dropdown.js +148 -0
  28. package/dist/components/ui/icons.js +22 -0
  29. package/dist/components/ui/index.js +22 -0
  30. package/dist/components/ui/input.js +202 -0
  31. package/dist/components/ui/loader.js +7 -0
  32. package/dist/components/ui/primitives/button.js +121 -0
  33. package/dist/components/ui/primitives/input.js +202 -0
  34. package/dist/components/ui/primitives/modal.js +203 -0
  35. package/dist/components/ui/primitives/text.js +286 -0
  36. package/dist/components/ui/resizeable.js +101 -0
  37. package/dist/components/ui/text.js +175 -0
  38. package/dist/components/ui/textarea.js +17 -0
  39. package/dist/components/ui/toast.js +129 -0
  40. package/dist/components/ui/view.js +250 -0
  41. package/dist/hooks/index.js +9 -0
  42. package/dist/hooks/useAvatars.js +32 -0
  43. package/dist/hooks/useCameraToggle.js +9 -0
  44. package/dist/hooks/useKeyboard.js +33 -0
  45. package/dist/hooks/useKeyboardSlide.js +11 -0
  46. package/dist/hooks/useLivestreamInfo.js +62 -0
  47. package/dist/hooks/useOuterAndInnerDimensions.js +27 -0
  48. package/dist/hooks/usePlayerDimensions.js +19 -0
  49. package/dist/hooks/useSegmentTiming.js +62 -0
  50. package/dist/index.js +16 -0
  51. package/dist/lib/facet.js +88 -0
  52. package/dist/lib/theme/atoms.js +620 -0
  53. package/dist/lib/theme/atoms.types.js +5 -0
  54. package/dist/lib/theme/index.js +9 -0
  55. package/dist/lib/theme/theme.js +248 -0
  56. package/dist/lib/theme/tokens.js +383 -0
  57. package/dist/lib/utils.js +94 -0
  58. package/dist/livestream-provider/index.js +25 -0
  59. package/dist/livestream-provider/websocket.js +41 -0
  60. package/dist/livestream-store/chat.js +186 -0
  61. package/dist/livestream-store/context.js +2 -0
  62. package/dist/livestream-store/index.js +4 -0
  63. package/dist/livestream-store/livestream-state.js +1 -0
  64. package/dist/livestream-store/livestream-store.js +42 -0
  65. package/dist/livestream-store/stream-key.js +115 -0
  66. package/dist/livestream-store/websocket-consumer.js +55 -0
  67. package/dist/player-store/context.js +2 -0
  68. package/dist/player-store/index.js +6 -0
  69. package/dist/player-store/player-provider.js +52 -0
  70. package/dist/player-store/player-state.js +22 -0
  71. package/dist/player-store/player-store.js +159 -0
  72. package/dist/player-store/single-player-provider.js +109 -0
  73. package/dist/streamplace-provider/context.js +2 -0
  74. package/dist/streamplace-provider/index.js +16 -0
  75. package/dist/streamplace-provider/poller.js +46 -0
  76. package/dist/streamplace-provider/xrpc.js +0 -0
  77. package/dist/streamplace-store/block.js +23 -0
  78. package/dist/streamplace-store/index.js +3 -0
  79. package/dist/streamplace-store/stream.js +193 -0
  80. package/dist/streamplace-store/streamplace-store.js +37 -0
  81. package/dist/streamplace-store/user.js +47 -0
  82. package/dist/streamplace-store/xrpc.js +12 -0
  83. package/node-compile-cache/v22.15.0-x64-efe9a9df-0/37be0eec +0 -0
  84. package/node-compile-cache/v22.15.0-x64-efe9a9df-0/56540125 +0 -0
  85. package/node-compile-cache/v22.15.0-x64-efe9a9df-0/67b1eb60 +0 -0
  86. package/node-compile-cache/v22.15.0-x64-efe9a9df-0/7c275f90 +0 -0
  87. package/package.json +50 -8
  88. package/src/components/chat/chat-box.tsx +195 -0
  89. package/src/components/chat/chat-message.tsx +192 -0
  90. package/src/components/chat/chat.tsx +128 -0
  91. package/src/components/chat/mention-suggestions.tsx +71 -0
  92. package/src/components/chat/mod-view.tsx +118 -0
  93. package/src/components/mobile-player/fullscreen.native.tsx +193 -0
  94. package/src/components/mobile-player/fullscreen.tsx +79 -0
  95. package/src/components/mobile-player/player.tsx +134 -0
  96. package/src/components/mobile-player/props.tsx +11 -0
  97. package/src/components/mobile-player/shared.tsx +56 -0
  98. package/src/components/mobile-player/ui/countdown.tsx +119 -0
  99. package/src/components/mobile-player/ui/index.ts +5 -0
  100. package/src/components/mobile-player/ui/input.tsx +85 -0
  101. package/src/components/mobile-player/ui/metrics.tsx +69 -0
  102. package/src/components/mobile-player/ui/streamer-context-menu.tsx +3 -0
  103. package/src/components/mobile-player/ui/viewer-context-menu.tsx +70 -0
  104. package/src/components/mobile-player/use-webrtc.tsx +282 -0
  105. package/src/components/mobile-player/video.native.tsx +360 -0
  106. package/src/components/mobile-player/video.tsx +557 -0
  107. package/src/components/mobile-player/webrtc-diagnostics.tsx +149 -0
  108. package/src/components/mobile-player/webrtc-primitives.native.tsx +6 -0
  109. package/src/components/mobile-player/webrtc-primitives.tsx +33 -0
  110. package/src/components/ui/button.tsx +309 -0
  111. package/src/components/ui/dialog.tsx +376 -0
  112. package/src/components/ui/dropdown.tsx +399 -0
  113. package/src/components/ui/icons.tsx +50 -0
  114. package/src/components/ui/index.ts +33 -0
  115. package/src/components/ui/input.tsx +350 -0
  116. package/src/components/ui/loader.tsx +9 -0
  117. package/src/components/ui/primitives/button.tsx +292 -0
  118. package/src/components/ui/primitives/input.tsx +422 -0
  119. package/src/components/ui/primitives/modal.tsx +421 -0
  120. package/src/components/ui/primitives/text.tsx +499 -0
  121. package/src/components/ui/resizeable.tsx +169 -0
  122. package/src/components/ui/text.tsx +330 -0
  123. package/src/components/ui/textarea.tsx +34 -0
  124. package/src/components/ui/toast.tsx +203 -0
  125. package/src/components/ui/view.tsx +344 -0
  126. package/src/hooks/index.ts +9 -0
  127. package/src/hooks/useAvatars.tsx +44 -0
  128. package/src/hooks/useCameraToggle.ts +12 -0
  129. package/src/hooks/useKeyboard.tsx +41 -0
  130. package/src/hooks/useKeyboardSlide.ts +12 -0
  131. package/src/hooks/useLivestreamInfo.ts +67 -0
  132. package/src/hooks/useOuterAndInnerDimensions.tsx +32 -0
  133. package/src/hooks/usePlayerDimensions.ts +23 -0
  134. package/src/hooks/useSegmentTiming.tsx +88 -0
  135. package/src/index.tsx +27 -0
  136. package/src/lib/facet.ts +131 -0
  137. package/src/lib/theme/atoms.ts +760 -0
  138. package/src/lib/theme/atoms.types.ts +258 -0
  139. package/src/lib/theme/index.ts +48 -0
  140. package/src/lib/theme/theme.tsx +436 -0
  141. package/src/lib/theme/tokens.ts +409 -0
  142. package/src/lib/utils.ts +132 -0
  143. package/src/livestream-provider/index.tsx +48 -0
  144. package/src/livestream-provider/websocket.tsx +47 -0
  145. package/src/livestream-store/chat.tsx +261 -0
  146. package/src/livestream-store/context.tsx +10 -0
  147. package/src/livestream-store/index.tsx +4 -0
  148. package/src/livestream-store/livestream-state.tsx +21 -0
  149. package/src/livestream-store/livestream-store.tsx +59 -0
  150. package/src/livestream-store/stream-key.tsx +124 -0
  151. package/src/livestream-store/websocket-consumer.tsx +62 -0
  152. package/src/player-store/context.tsx +11 -0
  153. package/src/player-store/index.tsx +6 -0
  154. package/src/player-store/player-provider.tsx +89 -0
  155. package/src/player-store/player-state.tsx +187 -0
  156. package/src/player-store/player-store.tsx +239 -0
  157. package/src/player-store/single-player-provider.tsx +181 -0
  158. package/src/streamplace-provider/context.tsx +10 -0
  159. package/src/streamplace-provider/index.tsx +32 -0
  160. package/src/streamplace-provider/poller.tsx +55 -0
  161. package/src/streamplace-provider/xrpc.tsx +0 -0
  162. package/src/streamplace-store/block.tsx +29 -0
  163. package/src/streamplace-store/index.tsx +3 -0
  164. package/src/streamplace-store/stream.tsx +262 -0
  165. package/src/streamplace-store/streamplace-store.tsx +89 -0
  166. package/src/streamplace-store/user.tsx +57 -0
  167. package/src/streamplace-store/xrpc.tsx +15 -0
  168. package/tsconfig.json +9 -0
  169. package/tsconfig.tsbuildinfo +1 -0
@@ -0,0 +1,330 @@
1
+ import { cva, type VariantProps } from "class-variance-authority";
2
+ import React, { forwardRef } from "react";
3
+ import { StyleSheet } from "react-native";
4
+ import { colors, borderRadius as radius, spacing } from "../../lib/theme/atoms";
5
+ import { TextPrimitive, TextPrimitiveProps } from "./primitives/text";
6
+
7
+ // Text variants using class-variance-authority pattern
8
+ const textVariants = cva("", {
9
+ variants: {
10
+ variant: {
11
+ h1: "h1",
12
+ h2: "h2",
13
+ h3: "h3",
14
+ h4: "h4",
15
+ h5: "h5",
16
+ h6: "h6",
17
+ subtitle1: "subtitle1",
18
+ subtitle2: "subtitle2",
19
+ body1: "body1",
20
+ body2: "body2",
21
+ caption: "caption",
22
+ overline: "overline",
23
+ },
24
+ size: {
25
+ xs: "xs",
26
+ sm: "sm",
27
+ base: "base",
28
+ lg: "lg",
29
+ xl: "xl",
30
+ "2xl": "2xl",
31
+ "3xl": "3xl",
32
+ "4xl": "4xl",
33
+ },
34
+ weight: {
35
+ thin: "thin",
36
+ light: "light",
37
+ normal: "normal",
38
+ medium: "medium",
39
+ semibold: "semibold",
40
+ bold: "bold",
41
+ extrabold: "extrabold",
42
+ black: "black",
43
+ },
44
+ color: {
45
+ default: "default",
46
+ muted: "muted",
47
+ primary: "primary",
48
+ secondary: "secondary",
49
+ destructive: "destructive",
50
+ success: "success",
51
+ warning: "warning",
52
+ },
53
+ },
54
+ defaultVariants: {
55
+ variant: "body1",
56
+ size: "base",
57
+ weight: "normal",
58
+ color: "default",
59
+ },
60
+ });
61
+
62
+ export interface TextProps
63
+ extends Omit<TextPrimitiveProps, "variant" | "size" | "weight" | "color">,
64
+ VariantProps<typeof textVariants> {
65
+ // Additional convenience props
66
+ muted?: boolean;
67
+ bold?: boolean;
68
+ italic?: boolean;
69
+ underline?: boolean;
70
+ strikethrough?: boolean;
71
+ uppercase?: boolean;
72
+ lowercase?: boolean;
73
+ capitalize?: boolean;
74
+ center?: boolean;
75
+ right?: boolean;
76
+ justify?: boolean;
77
+ // Custom color override
78
+ customColor?: string;
79
+ }
80
+
81
+ export const Text = forwardRef<any, TextProps>(
82
+ (
83
+ {
84
+ variant = undefined,
85
+ size = undefined,
86
+ weight = undefined,
87
+ color = undefined,
88
+ muted = false,
89
+ bold = false,
90
+ italic = false,
91
+ underline = false,
92
+ strikethrough = false,
93
+ uppercase = false,
94
+ lowercase = false,
95
+ capitalize = false,
96
+ center = false,
97
+ right = false,
98
+ justify = false,
99
+ customColor,
100
+ style,
101
+ children,
102
+ ...props
103
+ },
104
+ ref,
105
+ ) => {
106
+ // Create dynamic styles based on atoms
107
+ const styles = React.useMemo(() => createStyles(), []);
108
+
109
+ // Override props based on convenience props
110
+ const finalColor = customColor ? customColor : muted ? "muted" : color;
111
+
112
+ const finalTransform = uppercase
113
+ ? "uppercase"
114
+ : lowercase
115
+ ? "lowercase"
116
+ : capitalize
117
+ ? "capitalize"
118
+ : "none";
119
+
120
+ const finalDecoration =
121
+ underline && strikethrough
122
+ ? "underline line-through"
123
+ : underline
124
+ ? "underline"
125
+ : strikethrough
126
+ ? "line-through"
127
+ : "none";
128
+
129
+ const finalAlign = center
130
+ ? "center"
131
+ : right
132
+ ? "right"
133
+ : justify
134
+ ? "justify"
135
+ : "left";
136
+
137
+ // Get variant-specific styles
138
+ const variantStyle = styles[`${variant}Style` as keyof typeof styles] || {};
139
+
140
+ const styleArr = (
141
+ Array.isArray(style) ? style : [style || undefined]
142
+ ).filter((s) => s !== undefined);
143
+
144
+ return (
145
+ <TextPrimitive.Root
146
+ ref={ref}
147
+ variant={variant || "body1"}
148
+ size={size || "base"}
149
+ color={finalColor || "default"}
150
+ align={finalAlign}
151
+ transform={finalTransform}
152
+ decoration={finalDecoration as any}
153
+ italic={italic}
154
+ style={[variantStyle, ...styleArr]}
155
+ {...props}
156
+ >
157
+ {children}
158
+ </TextPrimitive.Root>
159
+ );
160
+ },
161
+ );
162
+
163
+ Text.displayName = "Text";
164
+
165
+ // Convenience components for common text elements
166
+ export const Heading = forwardRef<
167
+ any,
168
+ Omit<TextProps, "variant"> & { level?: 1 | 2 | 3 | 4 | 5 | 6 }
169
+ >(({ level = 1, ...props }, ref) => (
170
+ <Text ref={ref} variant={`h${level}` as any} {...props} />
171
+ ));
172
+
173
+ Heading.displayName = "Heading";
174
+
175
+ export const Subtitle = forwardRef<
176
+ any,
177
+ Omit<TextProps, "variant"> & { level?: 1 | 2 }
178
+ >(({ level = 1, ...props }, ref) => (
179
+ <Text
180
+ ref={ref}
181
+ variant={level === 1 ? "subtitle1" : "subtitle2"}
182
+ {...props}
183
+ />
184
+ ));
185
+
186
+ Subtitle.displayName = "Subtitle";
187
+
188
+ export const Body = forwardRef<
189
+ any,
190
+ Omit<TextProps, "variant"> & { level?: 1 | 2 }
191
+ >(({ level = 1, ...props }, ref) => (
192
+ <Text ref={ref} variant={level === 1 ? "body1" : "body2"} {...props} />
193
+ ));
194
+
195
+ Body.displayName = "Body";
196
+
197
+ export const Caption = forwardRef<any, Omit<TextProps, "variant">>(
198
+ (props, ref) => <Text ref={ref} variant="caption" {...props} />,
199
+ );
200
+
201
+ Caption.displayName = "Caption";
202
+
203
+ export const Label = forwardRef<any, Omit<TextProps, "variant">>(
204
+ (props, ref) => (
205
+ <Text ref={ref} variant="subtitle1" weight="medium" {...props} />
206
+ ),
207
+ );
208
+
209
+ Label.displayName = "Label";
210
+
211
+ export const Code = forwardRef<any, TextProps>(({ style, ...props }, ref) => {
212
+ const styles = React.useMemo(() => createStyles(), []);
213
+ // if style is not an array, convert it to an array
214
+ const styleArr = (Array.isArray(style) ? style : [style || undefined]).filter(
215
+ (s) => s !== undefined,
216
+ );
217
+
218
+ return <Text ref={ref} style={[styles.codeStyle, ...styleArr]} {...props} />;
219
+ });
220
+
221
+ Code.displayName = "Code";
222
+
223
+ // Span component for inline text styling (inherits from parent)
224
+ export const Span = forwardRef<
225
+ any,
226
+ Omit<TextProps, "variant" | "size" | "weight" | "color"> & {
227
+ variant?:
228
+ | "h1"
229
+ | "h2"
230
+ | "h3"
231
+ | "h4"
232
+ | "h5"
233
+ | "h6"
234
+ | "subtitle1"
235
+ | "subtitle2"
236
+ | "body1"
237
+ | "body2"
238
+ | "caption"
239
+ | "overline";
240
+ size?: "xs" | "sm" | "base" | "lg" | "xl" | "2xl" | "3xl" | "4xl";
241
+ weight?:
242
+ | "thin"
243
+ | "light"
244
+ | "normal"
245
+ | "medium"
246
+ | "semibold"
247
+ | "bold"
248
+ | "extrabold"
249
+ | "black";
250
+ color?:
251
+ | "default"
252
+ | "muted"
253
+ | "primary"
254
+ | "secondary"
255
+ | "destructive"
256
+ | "success"
257
+ | "warning";
258
+ }
259
+ >(({ children, ...props }, ref) => (
260
+ <TextPrimitive.Span ref={ref} {...props}>
261
+ {children}
262
+ </TextPrimitive.Span>
263
+ ));
264
+
265
+ Span.displayName = "Span";
266
+
267
+ // Create atom-based styles
268
+ function createStyles() {
269
+ return StyleSheet.create({
270
+ // Variant-specific styles
271
+ h1Style: {
272
+ marginBottom: spacing[4],
273
+ },
274
+ h2Style: {
275
+ marginBottom: spacing[3],
276
+ },
277
+ h3Style: {
278
+ marginBottom: spacing[3],
279
+ },
280
+ h4Style: {
281
+ marginBottom: spacing[2],
282
+ },
283
+ h5Style: {
284
+ marginBottom: spacing[2],
285
+ },
286
+ h6Style: {
287
+ marginBottom: spacing[2],
288
+ },
289
+ subtitle1Style: {
290
+ marginBottom: spacing[2],
291
+ },
292
+ subtitle2Style: {
293
+ marginBottom: spacing[1],
294
+ },
295
+ body1Style: {
296
+ marginBottom: spacing[3],
297
+ },
298
+ body2Style: {
299
+ marginBottom: spacing[2],
300
+ },
301
+ captionStyle: {
302
+ marginBottom: spacing[1],
303
+ },
304
+ overlineStyle: {
305
+ marginBottom: spacing[1],
306
+ textTransform: "uppercase",
307
+ letterSpacing: 1,
308
+ },
309
+ labelStyle: {
310
+ marginBottom: spacing[1],
311
+ },
312
+ buttonStyle: {
313
+ textAlign: "center",
314
+ },
315
+ codeStyle: {
316
+ fontFamily: "monospace",
317
+ backgroundColor: colors["muted"],
318
+ paddingHorizontal: spacing[1],
319
+ paddingVertical: 2,
320
+ borderRadius: radius.sm,
321
+ fontSize: 14,
322
+ },
323
+ });
324
+ }
325
+
326
+ // Export text variants for external use
327
+ export { textVariants };
328
+
329
+ // Re-export primitive components for advanced usage
330
+ export { TextPrimitive };
@@ -0,0 +1,34 @@
1
+ import * as React from "react";
2
+ import { TextInput, type TextInputProps } from "react-native";
3
+ import { bg, borders, flex, p, text } from "../../lib/theme/atoms";
4
+
5
+ function Textarea({
6
+ style,
7
+ multiline = true,
8
+ numberOfLines = 4,
9
+ ...props
10
+ }: TextInputProps & {
11
+ ref?: React.RefObject<TextInput>;
12
+ }) {
13
+ return (
14
+ <TextInput
15
+ style={[
16
+ flex.values[1],
17
+ borders.width.thin,
18
+ borders.color.gray[400],
19
+ bg.gray[900],
20
+ p[3],
21
+ text.gray[200],
22
+ props.editable === false && { opacity: 0.5 },
23
+ { borderRadius: 10 },
24
+ style,
25
+ ]}
26
+ multiline={multiline}
27
+ numberOfLines={numberOfLines}
28
+ textAlignVertical="top"
29
+ {...props}
30
+ />
31
+ );
32
+ }
33
+
34
+ export { Textarea };
@@ -0,0 +1,203 @@
1
+ import { Portal } from "@rn-primitives/portal";
2
+ import { useEffect, useState } from "react";
3
+ import {
4
+ Animated,
5
+ Platform,
6
+ Pressable,
7
+ StyleSheet,
8
+ Text,
9
+ useWindowDimensions,
10
+ View,
11
+ ViewStyle,
12
+ } from "react-native";
13
+ import { useSafeAreaInsets } from "react-native-safe-area-context";
14
+ import { useTheme } from "../../lib/theme/theme";
15
+
16
+ type ToastProps = {
17
+ open: boolean;
18
+ onOpenChange: (open: boolean) => void;
19
+ title: string;
20
+ description?: string;
21
+ actionLabel?: string;
22
+ onAction?: () => void;
23
+ duration?: number; // seconds
24
+ };
25
+
26
+ export function Toast({
27
+ open,
28
+ onOpenChange,
29
+ title,
30
+ description,
31
+ actionLabel = "Action",
32
+ onAction,
33
+ duration = 3,
34
+ }: ToastProps) {
35
+ const [seconds, setSeconds] = useState(duration);
36
+ const insets = useSafeAreaInsets();
37
+ const { theme } = useTheme();
38
+ const [fadeAnim] = useState(new Animated.Value(0));
39
+ const { width } = useWindowDimensions();
40
+ const isWeb = Platform.OS === "web";
41
+ const isDesktop = isWeb && width >= 768;
42
+
43
+ const containerPosition: ViewStyle = isDesktop
44
+ ? {
45
+ top: undefined,
46
+ bottom: theme.spacing[4],
47
+ right: theme.spacing[4], // <-- use spacing, not 1
48
+ alignItems: "flex-end",
49
+ minWidth: 400,
50
+ width: 400,
51
+ // Do NOT set left at all
52
+ }
53
+ : {
54
+ bottom: insets.bottom + theme.spacing[1],
55
+ left: 0,
56
+ right: 0,
57
+ alignItems: "center",
58
+ width: "100%",
59
+ maxWidth: undefined,
60
+ };
61
+
62
+ useEffect(() => {
63
+ let interval: ReturnType<typeof setInterval> | null = null;
64
+
65
+ if (open) {
66
+ setSeconds(duration);
67
+ Animated.timing(fadeAnim, {
68
+ toValue: 1,
69
+ duration: 200,
70
+ useNativeDriver: true,
71
+ }).start();
72
+
73
+ interval = setInterval(() => {
74
+ setSeconds((prev) => {
75
+ if (prev <= 1) {
76
+ onOpenChange(false);
77
+ if (interval) clearInterval(interval);
78
+ return duration;
79
+ }
80
+ return prev - 1;
81
+ });
82
+ }, 1000);
83
+ } else {
84
+ if (interval) clearInterval(interval);
85
+ Animated.timing(fadeAnim, {
86
+ toValue: 0,
87
+ duration: 150,
88
+ useNativeDriver: true,
89
+ }).start();
90
+ setSeconds(duration);
91
+ }
92
+
93
+ return () => {
94
+ if (interval) clearInterval(interval);
95
+ };
96
+ // eslint-disable-next-line
97
+ }, [open, duration]);
98
+
99
+ if (!open) return null;
100
+
101
+ return (
102
+ <Portal name="toast">
103
+ <Animated.View
104
+ style={[styles.container, containerPosition, { opacity: fadeAnim }]}
105
+ pointerEvents="box-none"
106
+ >
107
+ <View
108
+ style={[
109
+ styles.toast,
110
+ {
111
+ backgroundColor: theme.colors.secondary,
112
+ borderColor: theme.colors.border,
113
+ borderRadius: theme.borderRadius.xl,
114
+ flexDirection: "column",
115
+ justifyContent: "space-between",
116
+ alignItems: "center",
117
+ padding: theme.spacing[4],
118
+ width: isDesktop ? "100%" : "95%",
119
+ },
120
+ ]}
121
+ >
122
+ <View style={{ gap: theme.spacing[1], width: "100%" }}>
123
+ <Text
124
+ style={[
125
+ {
126
+ color: theme.colors.foreground,
127
+ fontSize: 16,
128
+ fontWeight: "500",
129
+ },
130
+ ]}
131
+ >
132
+ {title}
133
+ </Text>
134
+ {description ? (
135
+ <Text style={[{ color: theme.colors.foreground, fontSize: 14 }]}>
136
+ {description}
137
+ </Text>
138
+ ) : null}
139
+ </View>
140
+ <View
141
+ style={{
142
+ gap: theme.spacing[1],
143
+ flexDirection: "row",
144
+ justifyContent: "flex-end",
145
+ width: "100%",
146
+ }}
147
+ >
148
+ {onAction && (
149
+ <Pressable
150
+ style={[
151
+ styles.button,
152
+ {
153
+ borderColor: theme.colors.primary,
154
+ paddingHorizontal: theme.spacing[4],
155
+ paddingVertical: theme.spacing[2],
156
+ },
157
+ ]}
158
+ onPress={onAction}
159
+ >
160
+ <Text style={{ color: theme.colors.foreground }}>
161
+ {actionLabel}
162
+ </Text>
163
+ </Pressable>
164
+ )}
165
+ <Pressable
166
+ style={[
167
+ styles.button,
168
+ {
169
+ borderColor: theme.colors.primary,
170
+ paddingHorizontal: theme.spacing[4],
171
+ paddingVertical: theme.spacing[2],
172
+ },
173
+ ]}
174
+ onPress={() => onOpenChange(false)}
175
+ >
176
+ <Text style={{ color: theme.colors.foreground }}>Close</Text>
177
+ </Pressable>
178
+ </View>
179
+ </View>
180
+ </Animated.View>
181
+ </Portal>
182
+ );
183
+ }
184
+
185
+ const styles = StyleSheet.create({
186
+ container: {
187
+ position: "absolute",
188
+ zIndex: 1000,
189
+ paddingHorizontal: 16,
190
+ },
191
+ toast: {
192
+ opacity: 0.95,
193
+ borderWidth: 1,
194
+ gap: 8,
195
+ },
196
+ button: {
197
+ borderWidth: 1,
198
+ borderRadius: 8,
199
+ alignItems: "center",
200
+ justifyContent: "center",
201
+ backgroundColor: "transparent",
202
+ },
203
+ });