@streamplace/components 0.9.0 → 0.9.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 (138) hide show
  1. package/dist/components/chat/chat-box.d.ts.map +1 -1
  2. package/dist/components/chat/chat-box.js +90 -34
  3. package/dist/components/chat/chat-box.js.map +1 -1
  4. package/dist/components/chat/chat-message.d.ts +4 -0
  5. package/dist/components/chat/chat-message.d.ts.map +1 -1
  6. package/dist/components/chat/chat-message.js +3 -2
  7. package/dist/components/chat/chat-message.js.map +1 -1
  8. package/dist/components/chat/chat.d.ts.map +1 -1
  9. package/dist/components/chat/chat.js +56 -3
  10. package/dist/components/chat/chat.js.map +1 -1
  11. package/dist/components/chat/emoji-suggestions.d.ts.map +1 -1
  12. package/dist/components/chat/emoji-suggestions.js +11 -11
  13. package/dist/components/chat/emoji-suggestions.js.map +1 -1
  14. package/dist/components/chat/mention-suggestions.d.ts.map +1 -1
  15. package/dist/components/chat/mention-suggestions.js +20 -19
  16. package/dist/components/chat/mention-suggestions.js.map +1 -1
  17. package/dist/components/chat/system-message.d.ts +5 -1
  18. package/dist/components/chat/system-message.d.ts.map +1 -1
  19. package/dist/components/chat/system-message.js +4 -4
  20. package/dist/components/chat/system-message.js.map +1 -1
  21. package/dist/components/mobile-player/shared.d.ts +1 -1
  22. package/dist/components/mobile-player/shared.d.ts.map +1 -1
  23. package/dist/components/mobile-player/shared.js +11 -10
  24. package/dist/components/mobile-player/shared.js.map +1 -1
  25. package/dist/components/mobile-player/ui/viewer-context-menu.d.ts +1 -1
  26. package/dist/components/mobile-player/ui/viewer-context-menu.d.ts.map +1 -1
  27. package/dist/components/mobile-player/ui/viewer-context-menu.js +60 -43
  28. package/dist/components/mobile-player/ui/viewer-context-menu.js.map +1 -1
  29. package/dist/components/stream-notification/index.d.ts +3 -0
  30. package/dist/components/stream-notification/index.d.ts.map +1 -0
  31. package/dist/components/stream-notification/index.js +9 -0
  32. package/dist/components/stream-notification/index.js.map +1 -0
  33. package/dist/components/stream-notification/stream-notification-manager.d.ts +36 -0
  34. package/dist/components/stream-notification/stream-notification-manager.d.ts.map +1 -0
  35. package/dist/components/stream-notification/stream-notification-manager.js +96 -0
  36. package/dist/components/stream-notification/stream-notification-manager.js.map +1 -0
  37. package/dist/components/stream-notification/stream-notification.d.ts +5 -0
  38. package/dist/components/stream-notification/stream-notification.d.ts.map +1 -0
  39. package/dist/components/stream-notification/stream-notification.js +146 -0
  40. package/dist/components/stream-notification/stream-notification.js.map +1 -0
  41. package/dist/components/stream-notification/teleport-notification.d.ts +8 -0
  42. package/dist/components/stream-notification/teleport-notification.d.ts.map +1 -0
  43. package/dist/components/stream-notification/teleport-notification.js +116 -0
  44. package/dist/components/stream-notification/teleport-notification.js.map +1 -0
  45. package/dist/components/ui/button.d.ts +1 -1
  46. package/dist/components/ui/button.d.ts.map +1 -1
  47. package/dist/components/ui/button.js +7 -0
  48. package/dist/components/ui/button.js.map +1 -1
  49. package/dist/components/ui/dialog.d.ts +2 -2
  50. package/dist/components/ui/dropdown.d.ts +4 -0
  51. package/dist/components/ui/dropdown.d.ts.map +1 -1
  52. package/dist/components/ui/dropdown.js +41 -15
  53. package/dist/components/ui/dropdown.js.map +1 -1
  54. package/dist/components/ui/index.d.ts +1 -0
  55. package/dist/components/ui/index.d.ts.map +1 -1
  56. package/dist/components/ui/index.js +1 -0
  57. package/dist/components/ui/index.js.map +1 -1
  58. package/dist/components/ui/portal.d.ts +2 -0
  59. package/dist/components/ui/portal.d.ts.map +1 -0
  60. package/dist/components/ui/portal.js +5 -0
  61. package/dist/components/ui/portal.js.map +1 -0
  62. package/dist/components/ui/portal.web.d.ts +11 -0
  63. package/dist/components/ui/portal.web.d.ts.map +1 -0
  64. package/dist/components/ui/portal.web.js +22 -0
  65. package/dist/components/ui/portal.web.js.map +1 -0
  66. package/dist/components/ui/resizeable.d.ts +2 -1
  67. package/dist/components/ui/resizeable.d.ts.map +1 -1
  68. package/dist/components/ui/resizeable.js +68 -26
  69. package/dist/components/ui/resizeable.js.map +1 -1
  70. package/dist/components/ui/text.d.ts +1 -1
  71. package/dist/components/ui/view.d.ts +3 -3
  72. package/dist/index.d.ts +2 -0
  73. package/dist/index.d.ts.map +1 -1
  74. package/dist/index.js +2 -0
  75. package/dist/index.js.map +1 -1
  76. package/dist/lib/slash-commands/teleport.d.ts +4 -0
  77. package/dist/lib/slash-commands/teleport.d.ts.map +1 -0
  78. package/dist/lib/slash-commands/teleport.js +110 -0
  79. package/dist/lib/slash-commands/teleport.js.map +1 -0
  80. package/dist/lib/slash-commands.d.ts +16 -0
  81. package/dist/lib/slash-commands.d.ts.map +1 -0
  82. package/dist/lib/slash-commands.js +46 -0
  83. package/dist/lib/slash-commands.js.map +1 -0
  84. package/dist/lib/stream-notifications.d.ts +13 -0
  85. package/dist/lib/stream-notifications.d.ts.map +1 -0
  86. package/dist/lib/stream-notifications.js +46 -0
  87. package/dist/lib/stream-notifications.js.map +1 -0
  88. package/dist/lib/system-messages.d.ts +4 -8
  89. package/dist/lib/system-messages.d.ts.map +1 -1
  90. package/dist/lib/system-messages.js +38 -2
  91. package/dist/lib/system-messages.js.map +1 -1
  92. package/dist/lib/theme/atoms.d.ts +193 -193
  93. package/dist/livestream-provider/index.d.ts +7 -2
  94. package/dist/livestream-provider/index.d.ts.map +1 -1
  95. package/dist/livestream-provider/index.js +72 -4
  96. package/dist/livestream-provider/index.js.map +1 -1
  97. package/dist/livestream-store/livestream-state.d.ts +4 -1
  98. package/dist/livestream-store/livestream-state.d.ts.map +1 -1
  99. package/dist/livestream-store/livestream-store.d.ts.map +1 -1
  100. package/dist/livestream-store/livestream-store.js +3 -0
  101. package/dist/livestream-store/livestream-store.js.map +1 -1
  102. package/dist/livestream-store/websocket-consumer.d.ts.map +1 -1
  103. package/dist/livestream-store/websocket-consumer.js +30 -43
  104. package/dist/livestream-store/websocket-consumer.js.map +1 -1
  105. package/dist/streamplace-store/index.d.ts +1 -0
  106. package/dist/streamplace-store/index.d.ts.map +1 -1
  107. package/dist/streamplace-store/index.js +1 -0
  108. package/dist/streamplace-store/index.js.map +1 -1
  109. package/node-compile-cache/v22.15.0-x64-efe9a9df-0/37be0eec +0 -0
  110. package/package.json +4 -2
  111. package/src/components/chat/chat-box.tsx +126 -53
  112. package/src/components/chat/chat-message.tsx +1 -1
  113. package/src/components/chat/chat.tsx +79 -5
  114. package/src/components/chat/emoji-suggestions.tsx +27 -25
  115. package/src/components/chat/mention-suggestions.tsx +36 -33
  116. package/src/components/chat/system-message.tsx +14 -5
  117. package/src/components/mobile-player/shared.tsx +2 -1
  118. package/src/components/mobile-player/ui/viewer-context-menu.tsx +192 -166
  119. package/src/components/stream-notification/index.ts +5 -0
  120. package/src/components/stream-notification/stream-notification-manager.ts +140 -0
  121. package/src/components/stream-notification/stream-notification.tsx +227 -0
  122. package/src/components/stream-notification/teleport-notification.tsx +187 -0
  123. package/src/components/ui/button.tsx +7 -0
  124. package/src/components/ui/dropdown.tsx +96 -26
  125. package/src/components/ui/index.ts +1 -0
  126. package/src/components/ui/portal.tsx +1 -0
  127. package/src/components/ui/portal.web.tsx +37 -0
  128. package/src/components/ui/resizeable.tsx +89 -35
  129. package/src/index.tsx +3 -0
  130. package/src/lib/slash-commands/teleport.ts +136 -0
  131. package/src/lib/slash-commands.ts +65 -0
  132. package/src/lib/stream-notifications.ts +51 -0
  133. package/src/lib/system-messages.ts +52 -2
  134. package/src/livestream-provider/index.tsx +106 -3
  135. package/src/livestream-store/livestream-state.tsx +4 -0
  136. package/src/livestream-store/livestream-store.tsx +3 -0
  137. package/src/livestream-store/websocket-consumer.tsx +35 -54
  138. package/src/streamplace-store/index.tsx +1 -0
@@ -0,0 +1,227 @@
1
+ import { X } from "lucide-react-native";
2
+ import { useEffect, useState } from "react";
3
+ import { Pressable, StyleSheet, View } from "react-native";
4
+ import Animated, {
5
+ Easing,
6
+ useAnimatedStyle,
7
+ useSharedValue,
8
+ withTiming,
9
+ } from "react-native-reanimated";
10
+ import { Text, useTheme } from "../../";
11
+ import {
12
+ StreamNotification,
13
+ streamNotificationManager,
14
+ } from "./stream-notification-manager";
15
+
16
+ export function StreamNotificationProvider({
17
+ children = <></>,
18
+ position = "top",
19
+ }: {
20
+ children?: React.ReactNode;
21
+ position?: "top" | "bottom";
22
+ }) {
23
+ const [notifications, setNotifications] = useState(
24
+ streamNotificationManager.getAll(),
25
+ );
26
+
27
+ useEffect(() => {
28
+ return streamNotificationManager.subscribe(setNotifications);
29
+ }, []);
30
+
31
+ return (
32
+ <View style={styles.container}>
33
+ {children}
34
+ {notifications.map((notification, index) => (
35
+ <NotificationItem
36
+ key={notification.id}
37
+ notification={notification}
38
+ index={index}
39
+ position={position}
40
+ />
41
+ ))}
42
+ </View>
43
+ );
44
+ }
45
+
46
+ function NotificationItem({
47
+ notification,
48
+ index,
49
+ position,
50
+ }: {
51
+ notification: StreamNotification;
52
+ index: number;
53
+ position: "top" | "bottom";
54
+ }) {
55
+ const { theme } = useTheme();
56
+ const translateY = useSharedValue(position === "top" ? -100 : 100);
57
+ const opacity = useSharedValue(0);
58
+ const [isExiting, setIsExiting] = useState(false);
59
+
60
+ const NOTIFICATION_HEIGHT = 60;
61
+ const NOTIFICATION_GAP = 8;
62
+ const offset = 16 + index * (NOTIFICATION_HEIGHT + NOTIFICATION_GAP);
63
+
64
+ useEffect(() => {
65
+ translateY.value = withTiming(position === "top" ? offset : -offset, {
66
+ duration: 300,
67
+ easing: Easing.out(Easing.cubic),
68
+ });
69
+ opacity.value = withTiming(1, {
70
+ duration: 200,
71
+ });
72
+ }, [offset, position]);
73
+
74
+ useEffect(() => {
75
+ if (notification.shouldDismiss && !isExiting) {
76
+ setIsExiting(true);
77
+ setTimeout(() => {
78
+ streamNotificationManager.hide(
79
+ notification.id,
80
+ notification.dismissReason || "auto",
81
+ );
82
+ }, 200);
83
+ }
84
+ }, [
85
+ notification.shouldDismiss,
86
+ isExiting,
87
+ notification.id,
88
+ notification.dismissReason,
89
+ ]);
90
+
91
+ useEffect(() => {
92
+ if (isExiting) {
93
+ translateY.value = withTiming(position === "top" ? -100 : 100, {
94
+ duration: 200,
95
+ easing: Easing.in(Easing.cubic),
96
+ });
97
+ opacity.value = withTiming(0, {
98
+ duration: 200,
99
+ });
100
+ }
101
+ }, [isExiting, position]);
102
+
103
+ const animatedStyle = useAnimatedStyle(() => ({
104
+ transform: [{ translateY: translateY.value }],
105
+ opacity: opacity.value,
106
+ }));
107
+
108
+ const variantStyles = {
109
+ default: {
110
+ backgroundColor: theme.colors.card,
111
+ borderColor: theme.colors.border,
112
+ },
113
+ info: {
114
+ backgroundColor: theme.colors.info,
115
+ borderColor: theme.colors.info,
116
+ },
117
+ warning: {
118
+ backgroundColor: theme.colors.warning,
119
+ borderColor: theme.colors.warning,
120
+ },
121
+ };
122
+
123
+ const handleDismiss = (reason: "user" | "auto" = "user") => {
124
+ console.log("Dismissing notification:", notification.id);
125
+ setIsExiting(true);
126
+ setTimeout(() => {
127
+ console.log("Requesting dismiss for notification:", notification.id);
128
+ streamNotificationManager.hide(notification.id, reason);
129
+ }, 200);
130
+ console.log(streamNotificationManager.getAll());
131
+ };
132
+
133
+ const handleAction = () => {
134
+ notification.onAction?.();
135
+ streamNotificationManager.hide(notification.id, "user");
136
+ };
137
+
138
+ const positionStyle = position === "top" ? { top: 0 } : { bottom: 0 };
139
+
140
+ return (
141
+ <Animated.View
142
+ style={[
143
+ styles.notification,
144
+ positionStyle,
145
+ notification.render
146
+ ? {}
147
+ : variantStyles[notification.variant || "default"],
148
+ { margin: 0, padding: 0 },
149
+ animatedStyle,
150
+ ]}
151
+ >
152
+ {notification.render ? (
153
+ notification.render(isExiting, handleDismiss, notification.startTime)
154
+ ) : (
155
+ <View style={styles.content}>
156
+ <Text style={[styles.message, { color: theme.colors.foreground }]}>
157
+ {notification.message}
158
+ </Text>
159
+
160
+ <View style={styles.actions}>
161
+ {notification.actionLabel && (
162
+ <Pressable onPress={handleAction}>
163
+ <Text
164
+ style={[styles.actionButton, { color: theme.colors.primary }]}
165
+ >
166
+ {notification.actionLabel}
167
+ </Text>
168
+ </Pressable>
169
+ )}
170
+
171
+ <Pressable
172
+ onPress={() => handleDismiss("user")}
173
+ style={styles.closeButton}
174
+ >
175
+ <X size={16} color={theme.colors.mutedForeground} />
176
+ </Pressable>
177
+ </View>
178
+ </View>
179
+ )}
180
+ </Animated.View>
181
+ );
182
+ }
183
+
184
+ const styles = StyleSheet.create({
185
+ container: {
186
+ flex: 1,
187
+ pointerEvents: "box-none",
188
+ },
189
+ notification: {
190
+ position: "absolute",
191
+ top: 0,
192
+ left: 16,
193
+ right: 16,
194
+ zIndex: 9999,
195
+ borderRadius: 8,
196
+ borderWidth: 1,
197
+ padding: 12,
198
+ shadowColor: "#000",
199
+ shadowOffset: { width: 0, height: 2 },
200
+ shadowOpacity: 0.25,
201
+ shadowRadius: 8,
202
+ elevation: 5,
203
+ },
204
+ content: {
205
+ flexDirection: "row",
206
+ alignItems: "center",
207
+ justifyContent: "space-between",
208
+ gap: 12,
209
+ },
210
+ message: {
211
+ flex: 1,
212
+ fontSize: 14,
213
+ fontWeight: "500",
214
+ },
215
+ actions: {
216
+ flexDirection: "row",
217
+ alignItems: "center",
218
+ gap: 12,
219
+ },
220
+ actionButton: {
221
+ fontSize: 14,
222
+ fontWeight: "600",
223
+ },
224
+ closeButton: {
225
+ padding: 4,
226
+ },
227
+ });
@@ -0,0 +1,187 @@
1
+ import { useEffect, useState } from "react";
2
+ import { useWindowDimensions, View } from "react-native";
3
+ import Animated, {
4
+ Easing,
5
+ useAnimatedStyle,
6
+ useSharedValue,
7
+ withRepeat,
8
+ withTiming,
9
+ } from "react-native-reanimated";
10
+ import { Button, Text, useTheme, zero } from "../../";
11
+
12
+ export function TeleportNotification({
13
+ targetHandle,
14
+ countdown,
15
+ canCancel,
16
+ startTime,
17
+ onDismiss,
18
+ }: {
19
+ targetHandle: string;
20
+ countdown: number;
21
+ canCancel: boolean;
22
+ startTime?: number;
23
+ onDismiss: (reason?: "user" | "auto") => void;
24
+ }) {
25
+ const { zero: z } = useTheme();
26
+ const w = useWindowDimensions().width;
27
+
28
+ const [start, setStart] = useState(Date.now());
29
+ const [now, setNow] = useState(Date.now());
30
+ const [dismissed, setDismissed] = useState(false);
31
+
32
+ useEffect(() => {
33
+ const interval = setInterval(() => {
34
+ setNow(Date.now());
35
+ }, 100);
36
+ return () => clearInterval(interval);
37
+ }, []);
38
+
39
+ const timeLeft = Math.max(0, countdown - Math.floor((now - start) / 1000));
40
+
41
+ useEffect(() => {
42
+ if (dismissed) {
43
+ return;
44
+ }
45
+ if (timeLeft <= 0) {
46
+ setDismissed(true);
47
+ onDismiss("auto");
48
+ }
49
+ }, [dismissed, onDismiss, timeLeft]);
50
+
51
+ // if we're past 5 seconds from start, stripes should already be hidden
52
+ const elapsedTime = startTime ? (Date.now() - startTime) / 1000 : 0;
53
+ const [showStripes, setShowStripes] = useState(elapsedTime < 5);
54
+
55
+ const stripeX = useSharedValue(0);
56
+ const stripeOpacity = useSharedValue(1);
57
+ const progressWidth = useSharedValue(100);
58
+
59
+ useEffect(() => {
60
+ // if stripes are already hidden, fade out asap and return
61
+ if (!showStripes) {
62
+ stripeOpacity.value = withTiming(0, { duration: 0 });
63
+ return;
64
+ }
65
+ // warning stripes animation
66
+ stripeX.value = withRepeat(
67
+ withTiming(30 * 2, {
68
+ duration: 1000,
69
+ easing: Easing.linear,
70
+ }),
71
+ 3,
72
+ false,
73
+ );
74
+
75
+ // hide stripes after 500ms
76
+ const stripesTimer = setTimeout(() => {
77
+ // woosh the stripes off to the right before hiding
78
+ stripeX.value = withTiming(30 * 80, {
79
+ duration: 1500,
80
+ easing: Easing.cubic,
81
+ });
82
+ // after animation, set stripes as hidden
83
+ setTimeout(() => {
84
+ setShowStripes(false);
85
+ }, 350);
86
+ }, 1500);
87
+
88
+ return () => clearTimeout(stripesTimer);
89
+ }, []);
90
+
91
+ useEffect(() => {
92
+ if (showStripes) return;
93
+
94
+ // animate progress bar
95
+ const percentage = (timeLeft / countdown) * 100;
96
+ progressWidth.value = withTiming(percentage, {
97
+ duration: 1000,
98
+ easing: Easing.linear,
99
+ });
100
+ }, [timeLeft, countdown, showStripes]);
101
+
102
+ const stripesStyle = useAnimatedStyle(() => ({
103
+ opacity: stripeOpacity.value,
104
+ transform: [{ translateX: stripeX.value }],
105
+ }));
106
+
107
+ const progressStyle = useAnimatedStyle(() => ({
108
+ width: `${progressWidth.value}%`,
109
+ }));
110
+
111
+ return (
112
+ <View style={[{ overflow: "hidden" }, zero.r.lg, zero.bg.neutral[900]]}>
113
+ <View
114
+ style={[
115
+ zero.layout.flex.row,
116
+ zero.layout.flex.alignCenter,
117
+ zero.layout.flex.spaceBetween,
118
+ zero.px[3],
119
+ w > 650 ? zero.py[4] : zero.py[2],
120
+ ]}
121
+ >
122
+ <Text size={w > 650 ? "xl" : "base"}>
123
+ Teleporting to @{targetHandle}
124
+ </Text>
125
+ <View
126
+ style={[
127
+ zero.layout.flex.row,
128
+ zero.layout.flex.alignCenter,
129
+ zero.gap.all[3],
130
+ ]}
131
+ >
132
+ <Text color="muted">{timeLeft}s</Text>
133
+ {canCancel && (
134
+ <Button
135
+ onPress={() => onDismiss("user")}
136
+ width="min"
137
+ variant="destructive"
138
+ >
139
+ Cancel
140
+ </Button>
141
+ )}
142
+ </View>
143
+ </View>
144
+ <View
145
+ style={{
146
+ height: 4,
147
+ width: "100%",
148
+ borderRadius: 2,
149
+ overflow: "hidden",
150
+ backgroundColor: "#0f0f1e",
151
+ }}
152
+ >
153
+ <Animated.View
154
+ style={[
155
+ { height: "100%", borderRadius: 2, backgroundColor: "#16f4d0" },
156
+ progressStyle,
157
+ ]}
158
+ />
159
+ </View>
160
+ <Animated.View
161
+ style={[
162
+ {
163
+ position: "absolute",
164
+ flexDirection: "row",
165
+ height: 180,
166
+ width: "200%",
167
+ //clickthrough
168
+ pointerEvents: "none",
169
+ },
170
+ stripesStyle,
171
+ ]}
172
+ >
173
+ {[...Array(80)].map((_, i) => (
174
+ <View
175
+ key={i}
176
+ style={{
177
+ width: 30,
178
+ height: "100%",
179
+ backgroundColor: i % 2 === 0 ? "#FFA500" : "#000000",
180
+ transform: [{ skewX: "-45deg" }, { translateX: -30 * 8 }],
181
+ }}
182
+ />
183
+ ))}
184
+ </Animated.View>
185
+ </View>
186
+ );
187
+ }
@@ -23,6 +23,7 @@ const buttonVariants = cva("", {
23
23
  lg: "lg",
24
24
  xl: "xl",
25
25
  pill: "pill",
26
+ icon: "icon",
26
27
  },
27
28
  },
28
29
  defaultVariants: {
@@ -143,6 +144,12 @@ export const Button = forwardRef<any, ButtonProps>(
143
144
  inner: { gap: 4 },
144
145
  text: zero.typography.universal.xs,
145
146
  };
147
+ case "icon":
148
+ return {
149
+ button: [zero.p[2], { borderRadius: zero.borderRadius.md }],
150
+ inner: { gap: 0 },
151
+ text: zero.typography.universal.sm,
152
+ };
146
153
  case "md":
147
154
  default:
148
155
  return {
@@ -1,3 +1,4 @@
1
+ import * as RadixDropdownMenu from "@radix-ui/react-dropdown-menu";
1
2
  import * as DropdownMenuPrimitive from "@rn-primitives/dropdown-menu";
2
3
  import {
3
4
  Check,
@@ -109,32 +110,83 @@ export const DropdownMenuSubTrigger = forwardRef<
109
110
 
110
111
  export const DropdownMenuSubContent = forwardRef<
111
112
  any,
112
- DropdownMenuPrimitive.SubContentProps & { children?: ReactNode }
113
- >(({ children, ...props }, ref) => {
114
- const { zero: zt } = useTheme();
113
+ DropdownMenuPrimitive.SubContentProps & {
114
+ children?: ReactNode;
115
+ portalHost?: string;
116
+ sideOffset?: number;
117
+ alignOffset?: number;
118
+ avoidCollisions?: boolean;
119
+ }
120
+ >(
121
+ (
122
+ {
123
+ children,
124
+ portalHost,
125
+ sideOffset,
126
+ alignOffset,
127
+ avoidCollisions = true,
128
+ ...props
129
+ },
130
+ ref,
131
+ ) => {
132
+ const { zero: zt } = useTheme();
115
133
 
116
- return (
117
- <DropdownMenuPrimitive.SubContent
118
- ref={ref}
119
- style={[
120
- a.zIndex[50],
121
- a.sizes.minWidth[64],
122
- a.sizes.maxWidth[64],
123
- a.overflow.hidden,
124
- a.radius.all.md,
125
- a.borders.width.thin,
126
- zt.border.default,
127
- mt[1],
128
- zt.bg.popover,
129
- p[1],
130
- a.shadows.md,
131
- ]}
132
- {...props}
133
- >
134
- {children}
135
- </DropdownMenuPrimitive.SubContent>
136
- );
137
- });
134
+ const [portalContainer, setPortalContainer] =
135
+ React.useState<HTMLElement | null>(null);
136
+
137
+ React.useEffect(() => {
138
+ if (Platform.OS === "web" && portalHost) {
139
+ const element = document.querySelector<HTMLElement>(
140
+ `[data-portal-host="${portalHost}"]`,
141
+ );
142
+ setPortalContainer(element);
143
+ }
144
+ }, [portalHost]);
145
+
146
+ const styles = [
147
+ a.sizes.minWidth[64],
148
+ a.sizes.maxWidth[64],
149
+ a.overflow.hidden,
150
+ a.radius.all.md,
151
+ a.borders.width.thin,
152
+ zt.border.default,
153
+ mt[1],
154
+ zt.bg.popover,
155
+ p[1],
156
+ a.shadows.md,
157
+ ];
158
+
159
+ // On web, use Radix directly to support custom portal container
160
+ if (Platform.OS === "web") {
161
+ const { forceMount } = props;
162
+ // Flatten RN style array into a plain CSS object for DOM
163
+ const flattenedStyles = StyleSheet.flatten(styles);
164
+ return (
165
+ <RadixDropdownMenu.Portal
166
+ {...(portalContainer ? { container: portalContainer } : {})}
167
+ >
168
+ <RadixDropdownMenu.SubContent
169
+ ref={ref}
170
+ style={flattenedStyles as React.CSSProperties}
171
+ forceMount={forceMount}
172
+ sideOffset={sideOffset}
173
+ alignOffset={alignOffset}
174
+ avoidCollisions={avoidCollisions}
175
+ >
176
+ {children}
177
+ </RadixDropdownMenu.SubContent>
178
+ </RadixDropdownMenu.Portal>
179
+ );
180
+ }
181
+
182
+ // On native, use rn-primitives
183
+ return (
184
+ <DropdownMenuPrimitive.SubContent ref={ref} style={styles} {...props}>
185
+ {children}
186
+ </DropdownMenuPrimitive.SubContent>
187
+ );
188
+ },
189
+ );
138
190
 
139
191
  export const DropdownMenuContent = forwardRef<
140
192
  any,
@@ -147,8 +199,26 @@ export const DropdownMenuContent = forwardRef<
147
199
  const { height } = useWindowDimensions();
148
200
  const maxHeight = height * 0.9;
149
201
 
202
+ const [portalContainer, setPortalContainer] =
203
+ React.useState<HTMLElement | null>(null);
204
+
205
+ React.useEffect(() => {
206
+ if (Platform.OS === "web" && portalHost) {
207
+ const element = document.querySelector<HTMLElement>(
208
+ `[data-portal-host="${portalHost}"]`,
209
+ );
210
+ setPortalContainer(element);
211
+ console.log("set portal container to", element);
212
+ }
213
+ }, [portalHost]);
214
+
150
215
  return (
151
- <DropdownMenuPrimitive.Portal hostName={portalHost}>
216
+ <DropdownMenuPrimitive.Portal
217
+ hostName={portalHost}
218
+ {...(Platform.OS === "web" && portalContainer
219
+ ? { container: portalContainer }
220
+ : {})}
221
+ >
152
222
  <DropdownMenuPrimitive.Overlay
153
223
  style={[
154
224
  Platform.OS !== "web" ? StyleSheet.absoluteFill : undefined,
@@ -15,6 +15,7 @@ export * from "./info-row";
15
15
  export * from "./input";
16
16
  export * from "./loader";
17
17
  export * from "./menu";
18
+ export * from "./portal";
18
19
  export * from "./resizeable";
19
20
  export * from "./slider";
20
21
  export * from "./text";
@@ -0,0 +1 @@
1
+ export * from "@rn-primitives/portal";
@@ -0,0 +1,37 @@
1
+ import React, { useEffect, useState } from "react";
2
+ import { createPortal } from "react-dom";
3
+
4
+ function Portal({
5
+ children,
6
+ hostName = "INTERNAL_PRIMITIVE_DEFAULT_HOST_NAME",
7
+ }: {
8
+ children: React.ReactNode;
9
+ hostName?: string;
10
+ }) {
11
+ const [hostElement, setHostElement] = useState<HTMLElement | null>(null);
12
+
13
+ useEffect(() => {
14
+ const element = document.querySelector<HTMLElement>(
15
+ `[data-portal-host="${hostName}"]`,
16
+ );
17
+ setHostElement(element);
18
+ }, [hostName]);
19
+
20
+ if (!hostElement) {
21
+ return null;
22
+ }
23
+
24
+ return createPortal(children, hostElement);
25
+ }
26
+
27
+ interface PortalHostProps {
28
+ name?: string;
29
+ }
30
+
31
+ function PortalHost({
32
+ name = "INTERNAL_PRIMITIVE_DEFAULT_HOST_NAME",
33
+ }: PortalHostProps) {
34
+ return <div data-portal-host={name} />;
35
+ }
36
+
37
+ export { Portal, PortalHost };