@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,350 @@
1
+ import { cva, type VariantProps } from "class-variance-authority";
2
+ import React, { forwardRef } from "react";
3
+ import { Platform, StyleSheet, TouchableWithoutFeedback } from "react-native";
4
+ import { useTheme } from "../../lib/theme/theme";
5
+ import { InputPrimitive, InputPrimitiveProps } from "./primitives/input";
6
+
7
+ const inputVariants = cva("", {
8
+ variants: {
9
+ variant: {
10
+ default: "default",
11
+ filled: "filled",
12
+ underlined: "underlined",
13
+ },
14
+ size: {
15
+ sm: "sm",
16
+ md: "md",
17
+ lg: "lg",
18
+ },
19
+ },
20
+ defaultVariants: {
21
+ variant: "default",
22
+ size: "md",
23
+ },
24
+ });
25
+
26
+ export interface InputProps
27
+ extends Omit<InputPrimitiveProps, "style" | "error">,
28
+ VariantProps<typeof inputVariants> {
29
+ label?: string;
30
+ description?: string;
31
+ error?: string;
32
+ required?: boolean;
33
+ leftAddon?: React.ReactNode;
34
+ rightAddon?: React.ReactNode;
35
+ containerStyle?: any;
36
+ inputStyle?: any;
37
+ }
38
+
39
+ export const Input = forwardRef<any, InputProps>(
40
+ (
41
+ {
42
+ variant = "default",
43
+ size = "md",
44
+ label,
45
+ description,
46
+ error,
47
+ required = false,
48
+ leftAddon,
49
+ rightAddon,
50
+ disabled = false,
51
+ containerStyle,
52
+ inputStyle,
53
+ ...props
54
+ },
55
+ ref,
56
+ ) => {
57
+ const { theme } = useTheme();
58
+ const [isFocused, setIsFocused] = React.useState(false);
59
+ const inputRef = React.useRef<any>(null);
60
+
61
+ // Create dynamic styles based on theme
62
+ const styles = React.useMemo(() => createStyles(theme), [theme]);
63
+
64
+ // Get variant and size styles
65
+ const containerStyles = React.useMemo(() => {
66
+ const variantStyle = styles[`${variant}Container` as keyof typeof styles];
67
+ const sizeStyle = styles[`${size}Container` as keyof typeof styles];
68
+ const focusStyle = isFocused ? styles.focusedContainer : null;
69
+ return [variantStyle, sizeStyle, focusStyle];
70
+ }, [variant, size, styles, isFocused]);
71
+
72
+ const textStyles = React.useMemo(() => {
73
+ const variantTextStyle = styles[`${variant}Input` as keyof typeof styles];
74
+ const sizeTextStyle = styles[`${size}Input` as keyof typeof styles];
75
+ return [variantTextStyle, sizeTextStyle];
76
+ }, [variant, size, styles]);
77
+
78
+ const handleFocus = React.useCallback(
79
+ (event: any) => {
80
+ setIsFocused(true);
81
+ if (props.onFocus) {
82
+ props.onFocus(event);
83
+ }
84
+ },
85
+ [props.onFocus],
86
+ );
87
+
88
+ const handleBlur = React.useCallback(
89
+ (event: any) => {
90
+ setIsFocused(false);
91
+ if (props.onBlur) {
92
+ props.onBlur(event);
93
+ }
94
+ },
95
+ [props.onBlur],
96
+ );
97
+
98
+ const handleContainerPress = React.useCallback(() => {
99
+ if (inputRef.current && !disabled) {
100
+ inputRef.current.focus();
101
+ }
102
+ }, [disabled]);
103
+
104
+ const hasAddons = leftAddon || rightAddon;
105
+
106
+ if (hasAddons) {
107
+ return (
108
+ <InputPrimitive.Group>
109
+ {label && (
110
+ <InputPrimitive.Label
111
+ required={required}
112
+ disabled={disabled}
113
+ error={!!error}
114
+ >
115
+ {label}
116
+ </InputPrimitive.Label>
117
+ )}
118
+
119
+ <TouchableWithoutFeedback onPress={handleContainerPress}>
120
+ <InputPrimitive.Container
121
+ focused={isFocused}
122
+ error={!!error}
123
+ disabled={disabled}
124
+ style={[containerStyles, containerStyle, { padding: 0 }]}
125
+ >
126
+ {leftAddon && (
127
+ <InputPrimitive.Addon position="left">
128
+ {leftAddon}
129
+ </InputPrimitive.Addon>
130
+ )}
131
+
132
+ <InputPrimitive.Root
133
+ ref={(node) => {
134
+ inputRef.current = node;
135
+ if (ref) {
136
+ if (typeof ref === "function") {
137
+ ref(node);
138
+ } else {
139
+ ref.current = node;
140
+ }
141
+ }
142
+ }}
143
+ disabled={disabled}
144
+ error={!!error}
145
+ onFocus={handleFocus}
146
+ onBlur={handleBlur}
147
+ style={[
148
+ textStyles,
149
+ styles.inputInContainer,
150
+ inputStyle,
151
+ { outline: "none" },
152
+ ]}
153
+ placeholderTextColor={
154
+ disabled ? theme.colors.textDisabled : theme.colors.textMuted
155
+ }
156
+ {...props}
157
+ />
158
+
159
+ {rightAddon && (
160
+ <InputPrimitive.Addon position="right">
161
+ {rightAddon}
162
+ </InputPrimitive.Addon>
163
+ )}
164
+ </InputPrimitive.Container>
165
+ </TouchableWithoutFeedback>
166
+
167
+ {description && !error && (
168
+ <InputPrimitive.Description disabled={disabled}>
169
+ {description}
170
+ </InputPrimitive.Description>
171
+ )}
172
+
173
+ <InputPrimitive.Error visible={!!error}>{error}</InputPrimitive.Error>
174
+ </InputPrimitive.Group>
175
+ );
176
+ }
177
+
178
+ return (
179
+ <InputPrimitive.Group>
180
+ {label && (
181
+ <InputPrimitive.Label
182
+ required={required}
183
+ disabled={disabled}
184
+ error={!!error}
185
+ >
186
+ {label}
187
+ </InputPrimitive.Label>
188
+ )}
189
+
190
+ <InputPrimitive.Root
191
+ ref={(node) => {
192
+ inputRef.current = node;
193
+ if (ref) {
194
+ if (typeof ref === "function") {
195
+ ref(node);
196
+ } else {
197
+ ref.current = node;
198
+ }
199
+ }
200
+ }}
201
+ disabled={disabled}
202
+ error={!!error}
203
+ onFocus={handleFocus}
204
+ onBlur={handleBlur}
205
+ style={[containerStyles, textStyles, containerStyle, inputStyle]}
206
+ placeholderTextColor={
207
+ disabled ? theme.colors.textDisabled : theme.colors.textMuted
208
+ }
209
+ {...props}
210
+ />
211
+
212
+ {description && !error && (
213
+ <InputPrimitive.Description disabled={disabled}>
214
+ {description}
215
+ </InputPrimitive.Description>
216
+ )}
217
+
218
+ <InputPrimitive.Error visible={!!error}>{error}</InputPrimitive.Error>
219
+ </InputPrimitive.Group>
220
+ );
221
+ },
222
+ );
223
+
224
+ Input.displayName = "Input";
225
+
226
+ // Create theme-aware styles
227
+ function createStyles(theme: any) {
228
+ return StyleSheet.create({
229
+ // Variant styles for containers
230
+ defaultContainer: {
231
+ backgroundColor: theme.colors.background,
232
+ borderWidth: 1,
233
+ borderColor: theme.colors.border,
234
+ borderRadius: theme.borderRadius.md,
235
+ },
236
+
237
+ filledContainer: {
238
+ backgroundColor: theme.colors.muted,
239
+ borderWidth: 0,
240
+ borderRadius: theme.borderRadius.md,
241
+ },
242
+
243
+ underlinedContainer: {
244
+ backgroundColor: "transparent",
245
+ borderWidth: 0,
246
+ borderBottomWidth: 1,
247
+ borderBottomColor: theme.colors.border,
248
+ borderRadius: 0,
249
+ paddingHorizontal: 0,
250
+ },
251
+
252
+ // Variant styles for inputs
253
+ defaultInput: {
254
+ color: theme.colors.text,
255
+ backgroundColor: "transparent",
256
+ },
257
+
258
+ filledInput: {
259
+ color: theme.colors.text,
260
+ backgroundColor: "transparent",
261
+ },
262
+
263
+ underlinedInput: {
264
+ color: theme.colors.text,
265
+ backgroundColor: "transparent",
266
+ },
267
+
268
+ // Size styles for containers
269
+ smContainer: {
270
+ paddingHorizontal: theme.spacing[3],
271
+ paddingVertical: theme.spacing[2],
272
+ minHeight: theme.touchTargets.minimum - 8,
273
+ },
274
+
275
+ mdContainer: {
276
+ paddingHorizontal: theme.spacing[3],
277
+ paddingVertical: theme.spacing[3],
278
+ minHeight: theme.touchTargets.minimum,
279
+ },
280
+
281
+ lgContainer: {
282
+ paddingHorizontal: theme.spacing[4],
283
+ paddingVertical: theme.spacing[4],
284
+ minHeight: theme.touchTargets.comfortable,
285
+ },
286
+
287
+ // Size styles for inputs
288
+ smInput: {
289
+ fontSize: 14,
290
+ lineHeight: 18,
291
+ ...Platform.select({
292
+ ios: {
293
+ paddingVertical: 0,
294
+ },
295
+ android: {
296
+ paddingVertical: 0,
297
+ textAlignVertical: "center",
298
+ },
299
+ }),
300
+ },
301
+
302
+ mdInput: {
303
+ fontSize: 16,
304
+ lineHeight: 20,
305
+ ...Platform.select({
306
+ ios: {
307
+ paddingVertical: 0,
308
+ },
309
+ android: {
310
+ paddingVertical: 0,
311
+ textAlignVertical: "center",
312
+ },
313
+ }),
314
+ },
315
+
316
+ lgInput: {
317
+ fontSize: 18,
318
+ lineHeight: 22,
319
+ ...Platform.select({
320
+ ios: {
321
+ paddingVertical: 0,
322
+ },
323
+ android: {
324
+ paddingVertical: 0,
325
+ textAlignVertical: "center",
326
+ },
327
+ }),
328
+ },
329
+
330
+ // Special style for inputs inside containers
331
+ inputInContainer: {
332
+ flex: 1,
333
+ paddingHorizontal: 0,
334
+ paddingVertical: 0,
335
+ borderWidth: 0,
336
+ backgroundColor: "transparent",
337
+ minHeight: "auto",
338
+ borderRadius: 0,
339
+ },
340
+
341
+ // Focus styles
342
+ focusedContainer: {
343
+ borderColor: theme.colors.primary,
344
+ borderWidth: 1,
345
+ },
346
+ });
347
+ }
348
+
349
+ // Export input variants for external use
350
+ export { inputVariants };
@@ -0,0 +1,9 @@
1
+ import { ActivityIndicator as RNActivityIndicator } from "react-native";
2
+ import { useTheme } from "../../lib/theme";
3
+
4
+ export function Loader(
5
+ props: React.ComponentPropsWithoutRef<typeof RNActivityIndicator>,
6
+ ) {
7
+ const { theme } = useTheme();
8
+ return <RNActivityIndicator color={theme.colors.primary} {...props} />;
9
+ }
@@ -0,0 +1,292 @@
1
+ import React, { forwardRef } from "react";
2
+ import {
3
+ AccessibilityRole,
4
+ GestureResponderEvent,
5
+ StyleSheet,
6
+ Text,
7
+ TextProps,
8
+ TouchableOpacity,
9
+ TouchableOpacityProps,
10
+ View,
11
+ ViewProps,
12
+ } from "react-native";
13
+
14
+ // Base button primitive interface
15
+ export interface ButtonPrimitiveProps
16
+ extends Omit<TouchableOpacityProps, "onPress"> {
17
+ onPress?: (event: GestureResponderEvent) => void;
18
+ disabled?: boolean;
19
+ loading?: boolean;
20
+ accessibilityRole?: AccessibilityRole;
21
+ accessibilityLabel?: string;
22
+ accessibilityHint?: string;
23
+ testID?: string;
24
+ }
25
+
26
+ // Button root primitive - handles all touch interactions
27
+ export const ButtonRoot = forwardRef<
28
+ React.ComponentRef<typeof TouchableOpacity>,
29
+ ButtonPrimitiveProps
30
+ >(
31
+ (
32
+ {
33
+ children,
34
+ disabled = false,
35
+ loading = false,
36
+ onPress,
37
+ onPressIn,
38
+ onPressOut,
39
+ onLongPress,
40
+ accessibilityRole = "button",
41
+ accessibilityLabel,
42
+ accessibilityHint,
43
+ accessibilityState,
44
+ testID,
45
+ style,
46
+ activeOpacity = 0.7,
47
+ ...props
48
+ },
49
+ ref,
50
+ ) => {
51
+ const handlePress = React.useCallback(
52
+ (event: GestureResponderEvent) => {
53
+ if (!disabled && !loading && onPress) {
54
+ onPress(event);
55
+ }
56
+ },
57
+ [disabled, loading, onPress],
58
+ );
59
+
60
+ const handlePressIn = React.useCallback(
61
+ (event: GestureResponderEvent) => {
62
+ if (!disabled && !loading && onPressIn) {
63
+ onPressIn(event);
64
+ }
65
+ },
66
+ [disabled, loading, onPressIn],
67
+ );
68
+
69
+ const handlePressOut = React.useCallback(
70
+ (event: GestureResponderEvent) => {
71
+ if (!disabled && !loading && onPressOut) {
72
+ onPressOut(event);
73
+ }
74
+ },
75
+ [disabled, loading, onPressOut],
76
+ );
77
+
78
+ const handleLongPress = React.useCallback(
79
+ (event: GestureResponderEvent) => {
80
+ if (!disabled && !loading && onLongPress) {
81
+ onLongPress(event);
82
+ }
83
+ },
84
+ [disabled, loading, onLongPress],
85
+ );
86
+
87
+ return (
88
+ <TouchableOpacity
89
+ ref={ref}
90
+ onPress={handlePress}
91
+ onPressIn={handlePressIn}
92
+ onPressOut={handlePressOut}
93
+ onLongPress={handleLongPress}
94
+ disabled={disabled || loading}
95
+ activeOpacity={disabled || loading ? 1 : activeOpacity}
96
+ accessibilityRole={accessibilityRole}
97
+ accessibilityLabel={accessibilityLabel}
98
+ accessibilityHint={accessibilityHint}
99
+ accessibilityState={{
100
+ disabled: disabled || loading,
101
+ busy: loading,
102
+ ...accessibilityState,
103
+ }}
104
+ testID={testID}
105
+ style={[
106
+ primitiveStyles.button,
107
+ (disabled || loading) && primitiveStyles.disabled,
108
+ style,
109
+ ]}
110
+ {...props}
111
+ >
112
+ {children}
113
+ </TouchableOpacity>
114
+ );
115
+ },
116
+ );
117
+
118
+ ButtonRoot.displayName = "ButtonRoot";
119
+
120
+ // Button text primitive
121
+ export interface ButtonTextProps extends TextProps {
122
+ disabled?: boolean;
123
+ loading?: boolean;
124
+ }
125
+
126
+ export const ButtonText = forwardRef<Text, ButtonTextProps>(
127
+ ({ children, disabled, loading, style, ...props }, ref) => {
128
+ return (
129
+ <Text
130
+ ref={ref}
131
+ style={[
132
+ primitiveStyles.text,
133
+ (disabled || loading) && primitiveStyles.textDisabled,
134
+ style,
135
+ ]}
136
+ {...props}
137
+ >
138
+ {children}
139
+ </Text>
140
+ );
141
+ },
142
+ );
143
+
144
+ ButtonText.displayName = "ButtonText";
145
+
146
+ // Button icon primitive
147
+ export interface ButtonIconProps extends ViewProps {
148
+ position?: "left" | "right";
149
+ disabled?: boolean;
150
+ loading?: boolean;
151
+ }
152
+
153
+ export const ButtonIcon = forwardRef<View, ButtonIconProps>(
154
+ (
155
+ { children, position = "left", disabled, loading, style, ...props },
156
+ ref,
157
+ ) => {
158
+ return (
159
+ <View
160
+ ref={ref}
161
+ style={[
162
+ primitiveStyles.icon,
163
+ (disabled || loading) && primitiveStyles.iconDisabled,
164
+ style,
165
+ ]}
166
+ {...props}
167
+ >
168
+ {children}
169
+ </View>
170
+ );
171
+ },
172
+ );
173
+
174
+ ButtonIcon.displayName = "ButtonIcon";
175
+
176
+ // Button loading indicator primitive
177
+ export interface ButtonLoadingProps extends ViewProps {
178
+ visible?: boolean;
179
+ }
180
+
181
+ export const ButtonLoading = forwardRef<View, ButtonLoadingProps>(
182
+ ({ children, visible = false, style, ...props }, ref) => {
183
+ if (!visible) return null;
184
+
185
+ return (
186
+ <View ref={ref} style={[primitiveStyles.loading, style]} {...props}>
187
+ {children}
188
+ </View>
189
+ );
190
+ },
191
+ );
192
+
193
+ ButtonLoading.displayName = "ButtonLoading";
194
+
195
+ // Container for button content with flex layout
196
+ export interface ButtonContentProps extends ViewProps {
197
+ direction?: "row" | "column";
198
+ align?: "flex-start" | "center" | "flex-end";
199
+ justify?:
200
+ | "flex-start"
201
+ | "center"
202
+ | "flex-end"
203
+ | "space-between"
204
+ | "space-around";
205
+ }
206
+
207
+ export const ButtonContent = forwardRef<View, ButtonContentProps>(
208
+ (
209
+ {
210
+ children,
211
+ direction = "row",
212
+ align = "center",
213
+ justify = "center",
214
+ style,
215
+ ...props
216
+ },
217
+ ref,
218
+ ) => {
219
+ return (
220
+ <View
221
+ ref={ref}
222
+ style={[
223
+ primitiveStyles.content,
224
+ {
225
+ flexDirection: direction,
226
+ alignItems: align,
227
+ justifyContent: justify,
228
+ },
229
+ style,
230
+ ]}
231
+ {...props}
232
+ >
233
+ {children}
234
+ </View>
235
+ );
236
+ },
237
+ );
238
+
239
+ ButtonContent.displayName = "ButtonContent";
240
+
241
+ // Primitive styles (minimal, unstyled)
242
+ const primitiveStyles = StyleSheet.create({
243
+ button: {
244
+ flexDirection: "row",
245
+ alignItems: "center",
246
+ justifyContent: "center",
247
+ minHeight: 44, // iOS minimum touch target
248
+ minWidth: 44,
249
+ },
250
+ disabled: {
251
+ opacity: 0.5,
252
+ },
253
+ content: {
254
+ flexDirection: "row",
255
+ alignItems: "center",
256
+ justifyContent: "center",
257
+ flex: 1,
258
+ },
259
+ text: {
260
+ textAlign: "center" as const,
261
+ },
262
+ textDisabled: {
263
+ opacity: 0.5,
264
+ },
265
+ icon: {
266
+ alignItems: "center",
267
+ justifyContent: "center",
268
+ },
269
+ iconLeft: {
270
+ marginRight: 8,
271
+ },
272
+ iconRight: {
273
+ marginLeft: 8,
274
+ },
275
+ iconDisabled: {
276
+ opacity: 0.5,
277
+ },
278
+ loading: {
279
+ position: "absolute",
280
+ alignItems: "center",
281
+ justifyContent: "center",
282
+ },
283
+ });
284
+
285
+ // Export primitive collection
286
+ export const ButtonPrimitive = {
287
+ Root: ButtonRoot,
288
+ Text: ButtonText,
289
+ Icon: ButtonIcon,
290
+ Loading: ButtonLoading,
291
+ Content: ButtonContent,
292
+ };