@streamplace/components 0.6.37 → 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 (138) hide show
  1. package/dist/components/chat/chat-box.js +109 -0
  2. package/dist/components/chat/chat-message.js +76 -0
  3. package/dist/components/chat/chat.js +56 -0
  4. package/dist/components/chat/mention-suggestions.js +39 -0
  5. package/dist/components/chat/mod-view.js +33 -0
  6. package/dist/components/mobile-player/fullscreen.js +69 -0
  7. package/dist/components/mobile-player/fullscreen.native.js +151 -0
  8. package/dist/components/mobile-player/player.js +103 -0
  9. package/dist/components/mobile-player/props.js +1 -0
  10. package/dist/components/mobile-player/shared.js +51 -0
  11. package/dist/components/mobile-player/ui/countdown.js +79 -0
  12. package/dist/components/mobile-player/ui/index.js +5 -0
  13. package/dist/components/mobile-player/ui/input.js +38 -0
  14. package/dist/components/mobile-player/ui/metrics.js +40 -0
  15. package/dist/components/mobile-player/ui/streamer-context-menu.js +4 -0
  16. package/dist/components/mobile-player/ui/viewer-context-menu.js +20 -0
  17. package/dist/components/mobile-player/use-webrtc.js +232 -0
  18. package/dist/components/mobile-player/video.js +375 -0
  19. package/dist/components/mobile-player/video.native.js +238 -0
  20. package/dist/components/mobile-player/webrtc-diagnostics.js +106 -0
  21. package/dist/components/mobile-player/webrtc-primitives.js +25 -0
  22. package/dist/components/mobile-player/webrtc-primitives.native.js +1 -0
  23. package/dist/components/ui/button.js +220 -0
  24. package/dist/components/ui/dialog.js +203 -0
  25. package/dist/components/ui/dropdown.js +148 -0
  26. package/dist/components/ui/icons.js +22 -0
  27. package/dist/components/ui/index.js +22 -0
  28. package/dist/components/ui/input.js +202 -0
  29. package/dist/components/ui/loader.js +7 -0
  30. package/dist/components/ui/primitives/button.js +121 -0
  31. package/dist/components/ui/primitives/input.js +202 -0
  32. package/dist/components/ui/primitives/modal.js +203 -0
  33. package/dist/components/ui/primitives/text.js +286 -0
  34. package/dist/components/ui/resizeable.js +101 -0
  35. package/dist/components/ui/text.js +175 -0
  36. package/dist/components/ui/textarea.js +17 -0
  37. package/dist/components/ui/toast.js +129 -0
  38. package/dist/components/ui/view.js +250 -0
  39. package/dist/hooks/index.js +9 -0
  40. package/dist/hooks/useAvatars.js +32 -0
  41. package/dist/hooks/useCameraToggle.js +9 -0
  42. package/dist/hooks/useKeyboard.js +33 -0
  43. package/dist/hooks/useKeyboardSlide.js +11 -0
  44. package/dist/hooks/useLivestreamInfo.js +62 -0
  45. package/dist/hooks/useOuterAndInnerDimensions.js +27 -0
  46. package/dist/hooks/usePlayerDimensions.js +19 -0
  47. package/dist/hooks/useSegmentTiming.js +62 -0
  48. package/dist/index.js +10 -0
  49. package/dist/lib/facet.js +88 -0
  50. package/dist/lib/theme/atoms.js +620 -0
  51. package/dist/lib/theme/atoms.types.js +5 -0
  52. package/dist/lib/theme/index.js +9 -0
  53. package/dist/lib/theme/theme.js +248 -0
  54. package/dist/lib/theme/tokens.js +383 -0
  55. package/dist/lib/utils.js +94 -0
  56. package/dist/livestream-provider/index.js +8 -3
  57. package/dist/livestream-store/chat.js +89 -65
  58. package/dist/livestream-store/index.js +1 -0
  59. package/dist/livestream-store/livestream-store.js +3 -0
  60. package/dist/livestream-store/stream-key.js +115 -0
  61. package/dist/player-store/player-provider.js +0 -1
  62. package/dist/player-store/player-store.js +13 -0
  63. package/dist/streamplace-store/block.js +23 -0
  64. package/dist/streamplace-store/index.js +1 -0
  65. package/dist/streamplace-store/stream.js +193 -0
  66. package/node-compile-cache/v22.15.0-x64-efe9a9df-0/37be0eec +0 -0
  67. package/node-compile-cache/v22.15.0-x64-efe9a9df-0/56540125 +0 -0
  68. package/node-compile-cache/v22.15.0-x64-efe9a9df-0/67b1eb60 +0 -0
  69. package/node-compile-cache/v22.15.0-x64-efe9a9df-0/7c275f90 +0 -0
  70. package/package.json +20 -4
  71. package/src/components/chat/chat-box.tsx +195 -0
  72. package/src/components/chat/chat-message.tsx +192 -0
  73. package/src/components/chat/chat.tsx +128 -0
  74. package/src/components/chat/mention-suggestions.tsx +71 -0
  75. package/src/components/chat/mod-view.tsx +118 -0
  76. package/src/components/mobile-player/fullscreen.native.tsx +193 -0
  77. package/src/components/mobile-player/fullscreen.tsx +79 -0
  78. package/src/components/mobile-player/player.tsx +134 -0
  79. package/src/components/mobile-player/props.tsx +11 -0
  80. package/src/components/mobile-player/shared.tsx +56 -0
  81. package/src/components/mobile-player/ui/countdown.tsx +119 -0
  82. package/src/components/mobile-player/ui/index.ts +5 -0
  83. package/src/components/mobile-player/ui/input.tsx +85 -0
  84. package/src/components/mobile-player/ui/metrics.tsx +69 -0
  85. package/src/components/mobile-player/ui/streamer-context-menu.tsx +3 -0
  86. package/src/components/mobile-player/ui/viewer-context-menu.tsx +70 -0
  87. package/src/components/mobile-player/use-webrtc.tsx +282 -0
  88. package/src/components/mobile-player/video.native.tsx +360 -0
  89. package/src/components/mobile-player/video.tsx +557 -0
  90. package/src/components/mobile-player/webrtc-diagnostics.tsx +149 -0
  91. package/src/components/mobile-player/webrtc-primitives.native.tsx +6 -0
  92. package/src/components/mobile-player/webrtc-primitives.tsx +33 -0
  93. package/src/components/ui/button.tsx +309 -0
  94. package/src/components/ui/dialog.tsx +376 -0
  95. package/src/components/ui/dropdown.tsx +399 -0
  96. package/src/components/ui/icons.tsx +50 -0
  97. package/src/components/ui/index.ts +33 -0
  98. package/src/components/ui/input.tsx +350 -0
  99. package/src/components/ui/loader.tsx +9 -0
  100. package/src/components/ui/primitives/button.tsx +292 -0
  101. package/src/components/ui/primitives/input.tsx +422 -0
  102. package/src/components/ui/primitives/modal.tsx +421 -0
  103. package/src/components/ui/primitives/text.tsx +499 -0
  104. package/src/components/ui/resizeable.tsx +169 -0
  105. package/src/components/ui/text.tsx +330 -0
  106. package/src/components/ui/textarea.tsx +34 -0
  107. package/src/components/ui/toast.tsx +203 -0
  108. package/src/components/ui/view.tsx +344 -0
  109. package/src/hooks/index.ts +9 -0
  110. package/src/hooks/useAvatars.tsx +44 -0
  111. package/src/hooks/useCameraToggle.ts +12 -0
  112. package/src/hooks/useKeyboard.tsx +41 -0
  113. package/src/hooks/useKeyboardSlide.ts +12 -0
  114. package/src/hooks/useLivestreamInfo.ts +67 -0
  115. package/src/hooks/useOuterAndInnerDimensions.tsx +32 -0
  116. package/src/hooks/usePlayerDimensions.ts +23 -0
  117. package/src/hooks/useSegmentTiming.tsx +88 -0
  118. package/src/index.tsx +21 -0
  119. package/src/lib/facet.ts +131 -0
  120. package/src/lib/theme/atoms.ts +760 -0
  121. package/src/lib/theme/atoms.types.ts +258 -0
  122. package/src/lib/theme/index.ts +48 -0
  123. package/src/lib/theme/theme.tsx +436 -0
  124. package/src/lib/theme/tokens.ts +409 -0
  125. package/src/lib/utils.ts +132 -0
  126. package/src/livestream-provider/index.tsx +13 -2
  127. package/src/livestream-store/chat.tsx +115 -78
  128. package/src/livestream-store/index.tsx +1 -0
  129. package/src/livestream-store/livestream-state.tsx +3 -0
  130. package/src/livestream-store/livestream-store.tsx +3 -0
  131. package/src/livestream-store/stream-key.tsx +124 -0
  132. package/src/player-store/player-provider.tsx +0 -1
  133. package/src/player-store/player-state.tsx +28 -0
  134. package/src/player-store/player-store.tsx +22 -0
  135. package/src/streamplace-store/block.tsx +29 -0
  136. package/src/streamplace-store/index.tsx +1 -0
  137. package/src/streamplace-store/stream.tsx +262 -0
  138. package/tsconfig.tsbuildinfo +1 -1
@@ -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
+ };