@comergehq/studio 0.1.2 → 0.1.3

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 (172) hide show
  1. package/dist/index.js +255 -245
  2. package/dist/index.js.map +1 -1
  3. package/dist/index.mjs +213 -203
  4. package/dist/index.mjs.map +1 -1
  5. package/package.json +8 -5
  6. package/src/components/chat/ChatComposer.tsx +277 -0
  7. package/src/components/chat/ChatHeader.tsx +31 -0
  8. package/src/components/chat/ChatMessageBubble.tsx +69 -0
  9. package/src/components/chat/ChatMessageList.tsx +137 -0
  10. package/src/components/chat/ChatPage.tsx +69 -0
  11. package/src/components/chat/ForkNoticeBanner.tsx +66 -0
  12. package/src/components/chat/MultilineTextInput.tsx +46 -0
  13. package/src/components/chat/ScrollToBottomButton.tsx +78 -0
  14. package/src/components/chat/TypingIndicator.tsx +54 -0
  15. package/src/components/chat/index.ts +28 -0
  16. package/src/components/comments/AppCommentsSheet.tsx +213 -0
  17. package/src/components/comments/CommentRow.tsx +63 -0
  18. package/src/components/comments/formatTimeAgo.ts +3 -0
  19. package/src/components/comments/index.ts +3 -0
  20. package/src/components/comments/useAppComments.ts +74 -0
  21. package/src/components/comments/useAppDetails.ts +35 -0
  22. package/src/components/comments/useIosKeyboardSnapFix.ts +24 -0
  23. package/src/components/dialogs/ConfirmMergeRequestDialog.tsx +156 -0
  24. package/src/components/dialogs/index.ts +4 -0
  25. package/src/components/draw/DrawColorPicker.tsx +77 -0
  26. package/src/components/draw/DrawModeOverlay.tsx +144 -0
  27. package/src/components/draw/DrawSurface.tsx +127 -0
  28. package/src/components/draw/DrawToolbar.tsx +253 -0
  29. package/src/components/draw/index.ts +15 -0
  30. package/src/components/draw/optionalHaptics.ts +15 -0
  31. package/src/components/draw/strokes.ts +21 -0
  32. package/src/components/draw/types.ts +9 -0
  33. package/src/components/floating-draggable-button/FloatingDraggableButton.tsx +323 -0
  34. package/src/components/floating-draggable-button/constants.ts +17 -0
  35. package/src/components/floating-draggable-button/index.ts +4 -0
  36. package/src/components/floating-draggable-button/types.ts +63 -0
  37. package/src/components/icons/MergeIcon.tsx +14 -0
  38. package/src/components/icons/StudioIcons.tsx +66 -0
  39. package/src/components/index.ts +17 -0
  40. package/src/components/merge-requests/MergeRequestStatusCard.tsx +179 -0
  41. package/src/components/merge-requests/ReviewMergeRequestActionButton.tsx +62 -0
  42. package/src/components/merge-requests/ReviewMergeRequestCard.tsx +192 -0
  43. package/src/components/merge-requests/ReviewMergeRequestCarousel.tsx +132 -0
  44. package/src/components/merge-requests/index.ts +7 -0
  45. package/src/components/merge-requests/mergeRequestStatusDisplay.ts +23 -0
  46. package/src/components/merge-requests/toIsoString.ts +9 -0
  47. package/src/components/merge-requests/useControlledExpansion.ts +16 -0
  48. package/src/components/models/index.ts +9 -0
  49. package/src/components/models/types.ts +43 -0
  50. package/src/components/overlays/EdgeGlowFrame.tsx +105 -0
  51. package/src/components/overlays/index.ts +4 -0
  52. package/src/components/preview/PreviewHeroCard.tsx +58 -0
  53. package/src/components/preview/PreviewImage.tsx +22 -0
  54. package/src/components/preview/PreviewMetaRow.tsx +70 -0
  55. package/src/components/preview/PreviewPage.tsx +36 -0
  56. package/src/components/preview/PreviewPlaceholder.tsx +72 -0
  57. package/src/components/preview/PreviewStatusBadge.tsx +63 -0
  58. package/src/components/preview/StatsBar.tsx +109 -0
  59. package/src/components/preview/index.ts +22 -0
  60. package/src/components/primitives/Avatar.tsx +68 -0
  61. package/src/components/primitives/Button.tsx +102 -0
  62. package/src/components/primitives/Card.tsx +30 -0
  63. package/src/components/primitives/Divider.tsx +17 -0
  64. package/src/components/primitives/Icon.tsx +40 -0
  65. package/src/components/primitives/MarkdownText.tsx +72 -0
  66. package/src/components/primitives/Modal.tsx +53 -0
  67. package/src/components/primitives/Surface.tsx +42 -0
  68. package/src/components/primitives/Text.tsx +83 -0
  69. package/src/components/primitives/index.ts +35 -0
  70. package/src/components/primitives/types.ts +30 -0
  71. package/src/components/studio-sheet/StudioBottomSheet.tsx +114 -0
  72. package/src/components/studio-sheet/StudioSheetBackground.tsx +63 -0
  73. package/src/components/studio-sheet/StudioSheetHeader.tsx +35 -0
  74. package/src/components/studio-sheet/StudioSheetHeaderIconButton.tsx +109 -0
  75. package/src/components/studio-sheet/StudioSheetPager.tsx +66 -0
  76. package/src/components/studio-sheet/index.ts +18 -0
  77. package/src/components/studio-sheet/types.ts +5 -0
  78. package/src/components/utils/color.ts +25 -0
  79. package/src/components/utils/formatTimeAgo.ts +19 -0
  80. package/src/core/logger.ts +42 -0
  81. package/src/core/services/http/baseUrl.ts +3 -0
  82. package/src/core/services/http/index.ts +128 -0
  83. package/src/core/services/http/public.ts +14 -0
  84. package/src/core/services/supabase/auth.ts +41 -0
  85. package/src/core/services/supabase/client.ts +43 -0
  86. package/src/core/services/supabase/index.ts +7 -0
  87. package/src/data/agent/remote.ts +30 -0
  88. package/src/data/agent/repository.ts +34 -0
  89. package/src/data/agent/types.ts +28 -0
  90. package/src/data/apps/bundles/remote.ts +47 -0
  91. package/src/data/apps/bundles/repository.ts +35 -0
  92. package/src/data/apps/bundles/types.ts +27 -0
  93. package/src/data/apps/images/remote.ts +61 -0
  94. package/src/data/apps/images/repository.ts +47 -0
  95. package/src/data/apps/remote.ts +97 -0
  96. package/src/data/apps/repository.ts +185 -0
  97. package/src/data/apps/types.ts +206 -0
  98. package/src/data/attachment/remote.ts +32 -0
  99. package/src/data/attachment/repository.ts +40 -0
  100. package/src/data/attachment/types.ts +42 -0
  101. package/src/data/base-remote.ts +3 -0
  102. package/src/data/base-repository.ts +11 -0
  103. package/src/data/comments/likes/remote.ts +87 -0
  104. package/src/data/comments/likes/repository.ts +61 -0
  105. package/src/data/comments/likes/types.ts +47 -0
  106. package/src/data/comments/remote.ts +71 -0
  107. package/src/data/comments/repository.ts +53 -0
  108. package/src/data/comments/types.ts +60 -0
  109. package/src/data/github/remote.ts +23 -0
  110. package/src/data/github/repository.ts +35 -0
  111. package/src/data/github/types.ts +23 -0
  112. package/src/data/home/remote.ts +24 -0
  113. package/src/data/home/repository.ts +28 -0
  114. package/src/data/home/types.ts +70 -0
  115. package/src/data/index.ts +3 -0
  116. package/src/data/likes/remote.ts +57 -0
  117. package/src/data/likes/repository.ts +47 -0
  118. package/src/data/likes/types.ts +46 -0
  119. package/src/data/me/remote.ts +28 -0
  120. package/src/data/me/repository.ts +30 -0
  121. package/src/data/me/types.ts +14 -0
  122. package/src/data/merge-requests/remote.ts +76 -0
  123. package/src/data/merge-requests/repository.ts +66 -0
  124. package/src/data/merge-requests/types.ts +33 -0
  125. package/src/data/messages/remote.ts +21 -0
  126. package/src/data/messages/repository.ts +104 -0
  127. package/src/data/messages/types.ts +20 -0
  128. package/src/data/public/studio-config/remote.ts +19 -0
  129. package/src/data/public/studio-config/repository.ts +23 -0
  130. package/src/data/public/studio-config/types.ts +6 -0
  131. package/src/data/ratings/remote.ts +76 -0
  132. package/src/data/ratings/repository.ts +63 -0
  133. package/src/data/ratings/types.ts +57 -0
  134. package/src/data/threads/remote.ts +40 -0
  135. package/src/data/threads/repository.ts +41 -0
  136. package/src/data/threads/types.ts +25 -0
  137. package/src/data/types.ts +8 -0
  138. package/src/data/users/remote.ts +31 -0
  139. package/src/data/users/repository.ts +45 -0
  140. package/src/data/users/types.ts +15 -0
  141. package/src/index.ts +6 -0
  142. package/src/studio/ComergeStudio.tsx +246 -0
  143. package/src/studio/bootstrap/StudioBootstrap.tsx +45 -0
  144. package/src/studio/bootstrap/useStudioBootstrap.ts +51 -0
  145. package/src/studio/hooks/useApp.ts +83 -0
  146. package/src/studio/hooks/useAppStats.ts +111 -0
  147. package/src/studio/hooks/useAttachmentUpload.ts +59 -0
  148. package/src/studio/hooks/useBundleManager.ts +389 -0
  149. package/src/studio/hooks/useMergeRequests.ts +173 -0
  150. package/src/studio/hooks/useStudioActions.ts +96 -0
  151. package/src/studio/hooks/useThreadMessages.ts +85 -0
  152. package/src/studio/lib/chat.ts +34 -0
  153. package/src/studio/ui/ChatPanel.tsx +154 -0
  154. package/src/studio/ui/ConfirmMergeFlow.tsx +55 -0
  155. package/src/studio/ui/PreviewPanel.tsx +131 -0
  156. package/src/studio/ui/RuntimeRenderer.tsx +40 -0
  157. package/src/studio/ui/StudioOverlay.tsx +257 -0
  158. package/src/studio/ui/preview-panel/PressableCardRow.tsx +49 -0
  159. package/src/studio/ui/preview-panel/PreviewCollaborateSection.tsx +174 -0
  160. package/src/studio/ui/preview-panel/PreviewCustomizeSection.tsx +160 -0
  161. package/src/studio/ui/preview-panel/PreviewHeroSection.tsx +56 -0
  162. package/src/studio/ui/preview-panel/PreviewMetaSection.tsx +67 -0
  163. package/src/studio/ui/preview-panel/PreviewPanelHeader.tsx +48 -0
  164. package/src/studio/ui/preview-panel/SectionTitle.tsx +31 -0
  165. package/src/studio/ui/preview-panel/usePreviewPanelData.ts +132 -0
  166. package/src/studio/ui/preview-panel/utils.ts +29 -0
  167. package/src/theme/index.ts +5 -0
  168. package/src/theme/tokens.ts +118 -0
  169. package/src/theme/types.ts +90 -0
  170. package/src/theme/useTheme.ts +11 -0
  171. package/dist/assets/images/merge.svg +0 -3
  172. package/dist/merge-72UG27QV.svg +0 -3
@@ -0,0 +1,30 @@
1
+ import type { StyleProp, TextStyle, ViewStyle } from 'react-native';
2
+
3
+ export type WithStyle<T> = {
4
+ style?: StyleProp<T>;
5
+ };
6
+
7
+ export type PressStateStyle<T> = {
8
+ style?: StyleProp<T> | ((state: { pressed: boolean; disabled?: boolean }) => StyleProp<T>);
9
+ };
10
+
11
+ export type TextAlign = 'auto' | 'left' | 'right' | 'center' | 'justify';
12
+
13
+ export type TextVariant = 'body' | 'bodyMuted' | 'caption' | 'captionMuted' | 'title';
14
+
15
+ export type ButtonVariant = 'primary' | 'neutral' | 'danger' | 'ghost';
16
+
17
+ export type ButtonSize = 'sm' | 'md' | 'icon';
18
+
19
+ export type SurfaceVariant = 'background' | 'surface' | 'surfaceRaised' | 'floating';
20
+
21
+ export type CardVariant = 'surface' | 'surfaceRaised';
22
+
23
+ export type DividerVariant = 'subtle' | 'default';
24
+
25
+ export type IconColorRole = 'default' | 'muted' | 'primary' | 'danger' | 'success' | 'warning';
26
+
27
+ export type TextStyleProp = StyleProp<TextStyle>;
28
+ export type ViewStyleProp = StyleProp<ViewStyle>;
29
+
30
+
@@ -0,0 +1,114 @@
1
+ import * as React from 'react';
2
+ import { View } from 'react-native';
3
+ import BottomSheet, { type BottomSheetBackgroundProps, type BottomSheetProps } from '@gorhom/bottom-sheet';
4
+ import { useSafeAreaInsets } from 'react-native-safe-area-context';
5
+
6
+ import { useTheme } from '../../theme';
7
+ import { StudioSheetBackground, type StudioSheetBackgroundProps } from './StudioSheetBackground';
8
+ import type { StudioSheetSnapPoints } from './types';
9
+
10
+ export type StudioBottomSheetProps = {
11
+ /**
12
+ * Controlled open state.
13
+ */
14
+ open: boolean;
15
+ onOpenChange?: (open: boolean) => void;
16
+
17
+ /**
18
+ * Snap points for the sheet.
19
+ */
20
+ snapPoints?: StudioSheetSnapPoints;
21
+
22
+ /**
23
+ * Optional ref forwarding to control the BottomSheet imperatively.
24
+ */
25
+ sheetRef?: React.RefObject<BottomSheet | null>;
26
+
27
+ /**
28
+ * Provide a custom background renderer (e.g. BlurView).
29
+ */
30
+ background?: Pick<StudioSheetBackgroundProps, 'renderBackground'>;
31
+
32
+ /**
33
+ * Content inside the sheet.
34
+ */
35
+ children: React.ReactNode;
36
+
37
+ /**
38
+ * Additional BottomSheet props, for advanced tuning.
39
+ * We intentionally do not expose everything as first-class props to keep SRP.
40
+ */
41
+ bottomSheetProps?: Omit<
42
+ BottomSheetProps,
43
+ | 'ref'
44
+ | 'index'
45
+ | 'snapPoints'
46
+ | 'enablePanDownToClose'
47
+ | 'backgroundComponent'
48
+ | 'topInset'
49
+ | 'bottomInset'
50
+ | 'handleIndicatorStyle'
51
+ | 'onChange'
52
+ | 'children'
53
+ >;
54
+ };
55
+
56
+ export function StudioBottomSheet({
57
+ open,
58
+ onOpenChange,
59
+ snapPoints = ['80%', '100%'],
60
+ sheetRef,
61
+ background,
62
+ children,
63
+ bottomSheetProps,
64
+ }: StudioBottomSheetProps) {
65
+ const theme = useTheme();
66
+ const insets = useSafeAreaInsets();
67
+ const internalSheetRef = React.useRef<BottomSheet | null>(null);
68
+ const resolvedSheetRef = sheetRef ?? internalSheetRef;
69
+
70
+ // Gorhom BottomSheet `index` is not reliably "fully controlled" across versions.
71
+ // Ensure the visual sheet actually opens/closes when `open` changes (e.g. via header X button).
72
+ React.useEffect(() => {
73
+ const sheet = resolvedSheetRef.current;
74
+ if (!sheet) return;
75
+
76
+ if (open) {
77
+ // Open to the highest snap point by default.
78
+ sheet.snapToIndex(snapPoints.length - 1);
79
+ } else {
80
+ sheet.close();
81
+ }
82
+ }, [open, resolvedSheetRef, snapPoints.length]);
83
+
84
+ const handleChange = React.useCallback(
85
+ (index: number) => {
86
+ onOpenChange?.(index >= 0);
87
+ },
88
+ [onOpenChange]
89
+ );
90
+
91
+ return (
92
+ <BottomSheet
93
+ ref={resolvedSheetRef}
94
+ index={open ? snapPoints.length - 1 : -1}
95
+ snapPoints={snapPoints}
96
+ enablePanDownToClose
97
+ keyboardBehavior="extend"
98
+ keyboardBlurBehavior="restore"
99
+ android_keyboardInputMode="adjustResize"
100
+ backgroundComponent={(props: BottomSheetBackgroundProps) => (
101
+ <StudioSheetBackground {...props} renderBackground={background?.renderBackground} />
102
+ )}
103
+ topInset={insets.top}
104
+ bottomInset={insets.bottom}
105
+ handleIndicatorStyle={{ backgroundColor: theme.colors.handleIndicator }}
106
+ onChange={handleChange}
107
+ {...bottomSheetProps}
108
+ >
109
+ <View style={{ flex: 1, overflow: 'hidden' }}>{children}</View>
110
+ </BottomSheet>
111
+ );
112
+ }
113
+
114
+
@@ -0,0 +1,63 @@
1
+ import * as React from 'react';
2
+ import { Platform, View, type ViewStyle } from 'react-native';
3
+ import type { BottomSheetBackgroundProps } from '@gorhom/bottom-sheet';
4
+ import { LiquidGlassView, isLiquidGlassSupported } from '@callstack/liquid-glass';
5
+
6
+ import { useTheme } from '../../theme';
7
+
8
+ export type StudioSheetBackgroundProps = BottomSheetBackgroundProps & {
9
+ /**
10
+ * Optional override to render a custom background (e.g. BlurView).
11
+ * If provided, it receives the computed container style.
12
+ */
13
+ renderBackground?: (params: { style: ViewStyle }) => React.ReactNode;
14
+ };
15
+
16
+ export function StudioSheetBackground({
17
+ style,
18
+ renderBackground,
19
+ }: StudioSheetBackgroundProps) {
20
+ const theme = useTheme();
21
+ const radius = Platform.OS === 'ios' ? 39 : 16;
22
+ const fallbackBgColor = theme.scheme === 'dark' ? 'rgba(11, 8, 15, 0.85)' : 'rgba(255, 255, 255, 0.85)';
23
+ const secondaryBgBaseColor = theme.scheme === 'dark' ? 'rgb(24, 24, 27)' : 'rgb(173, 173, 173)';
24
+
25
+ const containerStyle: ViewStyle = {
26
+ ...(style as ViewStyle),
27
+ borderTopLeftRadius: radius,
28
+ borderTopRightRadius: radius,
29
+ overflow: 'hidden',
30
+ };
31
+
32
+ if (renderBackground) {
33
+ return <>{renderBackground({ style: containerStyle })}</>;
34
+ }
35
+
36
+ return (
37
+ <>
38
+ <LiquidGlassView
39
+ style={[containerStyle, !isLiquidGlassSupported && { backgroundColor: fallbackBgColor }]}
40
+ effect="regular"
41
+ />
42
+ {isLiquidGlassSupported && (
43
+ <View
44
+ style={[
45
+ containerStyle,
46
+ {
47
+ backgroundColor: secondaryBgBaseColor,
48
+ opacity: 0.4,
49
+ position: 'absolute',
50
+ top: 0,
51
+ left: 0,
52
+ right: 0,
53
+ bottom: 0,
54
+ pointerEvents: 'none',
55
+ },
56
+ ]}
57
+ />
58
+ )}
59
+ </>
60
+ );
61
+ }
62
+
63
+
@@ -0,0 +1,35 @@
1
+ import * as React from 'react';
2
+ import { View, type ViewStyle } from 'react-native';
3
+
4
+ import { useTheme } from '../../theme';
5
+
6
+ export type StudioSheetHeaderProps = {
7
+ left?: React.ReactNode;
8
+ right?: React.ReactNode;
9
+ center?: React.ReactNode;
10
+ style?: ViewStyle;
11
+ };
12
+
13
+ export function StudioSheetHeader({ left, center, right, style }: StudioSheetHeaderProps) {
14
+ const theme = useTheme();
15
+ return (
16
+ <View
17
+ style={[
18
+ {
19
+ flexDirection: 'row',
20
+ alignItems: 'center',
21
+ justifyContent: 'space-between',
22
+ paddingHorizontal: theme.spacing.lg,
23
+ paddingBottom: theme.spacing.sm,
24
+ },
25
+ style,
26
+ ]}
27
+ >
28
+ <View style={{ flexDirection: 'row', alignItems: 'center' }}>{left}</View>
29
+ <View style={{ flex: 1, alignItems: 'center' }}>{center}</View>
30
+ <View style={{ flexDirection: 'row', alignItems: 'center' }}>{right}</View>
31
+ </View>
32
+ );
33
+ }
34
+
35
+
@@ -0,0 +1,109 @@
1
+ import * as React from 'react';
2
+ import { Pressable, View, type ViewStyle } from 'react-native';
3
+ import { LiquidGlassView, isLiquidGlassSupported } from '@callstack/liquid-glass';
4
+
5
+ import { useTheme } from '../../theme';
6
+
7
+ export type StudioSheetHeaderIconButtonProps = {
8
+ onPress: () => void;
9
+ disabled?: boolean;
10
+ children: React.ReactNode;
11
+ style?: ViewStyle;
12
+ accessibilityLabel?: string;
13
+ intent?: 'neutral' | 'primary' | 'danger';
14
+ appearance?: 'glass' | 'solid';
15
+ };
16
+
17
+ export function StudioSheetHeaderIconButton({
18
+ onPress,
19
+ disabled,
20
+ children,
21
+ style,
22
+ accessibilityLabel,
23
+ intent = 'neutral',
24
+ appearance = 'solid',
25
+ }: StudioSheetHeaderIconButtonProps) {
26
+ const theme = useTheme();
27
+ const size = 44;
28
+ const [pressed, setPressed] = React.useState(false);
29
+
30
+ const solidBg =
31
+ intent === 'danger'
32
+ ? theme.colors.danger
33
+ : intent === 'primary'
34
+ ? theme.colors.primary
35
+ : theme.colors.neutral;
36
+
37
+ const glassFallbackBg = theme.scheme === 'dark' ? '#18181B' : '#F6F6F6';
38
+ const glassInnerBg = intent === 'danger' ? theme.colors.danger : theme.colors.primary;
39
+
40
+ const resolvedOpacity = disabled ? 0.6 : pressed ? 0.9 : 1;
41
+
42
+ return (
43
+ <View style={style}>
44
+ {appearance === 'glass' ? (
45
+ <LiquidGlassView
46
+ style={[{ borderRadius: 100 }, !isLiquidGlassSupported && { backgroundColor: glassFallbackBg }]}
47
+ interactive
48
+ effect="clear"
49
+ >
50
+ <View
51
+ style={{
52
+ width: size,
53
+ height: size,
54
+ borderRadius: 100,
55
+ alignItems: 'center',
56
+ justifyContent: 'center',
57
+ backgroundColor: glassInnerBg,
58
+ opacity: resolvedOpacity,
59
+ }}
60
+ >
61
+ <Pressable
62
+ accessibilityRole="button"
63
+ accessibilityLabel={accessibilityLabel}
64
+ disabled={disabled}
65
+ onPress={onPress}
66
+ onPressIn={() => {
67
+ if (!disabled) setPressed(true);
68
+ }}
69
+ onPressOut={() => setPressed(false)}
70
+ hitSlop={8}
71
+ style={{ flex: 1, alignItems: 'center', justifyContent: 'center', width: '100%' }}
72
+ >
73
+ {children}
74
+ </Pressable>
75
+ </View>
76
+ </LiquidGlassView>
77
+ ) : (
78
+ <View
79
+ style={{
80
+ width: size,
81
+ height: size,
82
+ borderRadius: 100,
83
+ alignItems: 'center',
84
+ justifyContent: 'center',
85
+ backgroundColor: solidBg,
86
+ opacity: resolvedOpacity,
87
+ }}
88
+ >
89
+ <Pressable
90
+ accessibilityRole="button"
91
+ accessibilityLabel={accessibilityLabel}
92
+ disabled={disabled}
93
+ onPress={onPress}
94
+ onPressIn={() => {
95
+ if (!disabled) setPressed(true);
96
+ }}
97
+ onPressOut={() => setPressed(false)}
98
+ hitSlop={8}
99
+ style={{ flex: 1, alignItems: 'center', justifyContent: 'center', width: '100%' }}
100
+ >
101
+ {children}
102
+ </Pressable>
103
+ </View>
104
+ )}
105
+ </View>
106
+ );
107
+ }
108
+
109
+
@@ -0,0 +1,66 @@
1
+ import * as React from 'react';
2
+ import { Animated, type ViewStyle } from 'react-native';
3
+
4
+ import type { StudioSheetPage } from './types';
5
+
6
+ export type StudioSheetPagerProps = {
7
+ activePage: StudioSheetPage;
8
+ width: number;
9
+ preview: React.ReactNode;
10
+ chat: React.ReactNode;
11
+ style?: ViewStyle;
12
+ };
13
+
14
+ export function StudioSheetPager({ activePage, width, preview, chat, style }: StudioSheetPagerProps) {
15
+ const anim = React.useRef(new Animated.Value(activePage === 'chat' ? 1 : 0)).current;
16
+
17
+ React.useEffect(() => {
18
+ Animated.spring(anim, {
19
+ toValue: activePage === 'chat' ? 1 : 0,
20
+ useNativeDriver: true,
21
+ tension: 65,
22
+ friction: 11,
23
+ }).start();
24
+ }, [activePage, anim]);
25
+
26
+ const previewTranslateX = anim.interpolate({ inputRange: [0, 1], outputRange: [0, -width] });
27
+ const chatTranslateX = anim.interpolate({ inputRange: [0, 1], outputRange: [width, 0] });
28
+
29
+ return (
30
+ <Animated.View style={[{ flex: 1 }, style]}>
31
+ <Animated.View
32
+ style={[
33
+ {
34
+ position: 'absolute',
35
+ top: 0,
36
+ left: 0,
37
+ right: 0,
38
+ bottom: 0,
39
+ transform: [{ translateX: previewTranslateX }],
40
+ },
41
+ ]}
42
+ pointerEvents={activePage === 'preview' ? 'auto' : 'none'}
43
+ >
44
+ {preview}
45
+ </Animated.View>
46
+
47
+ <Animated.View
48
+ style={[
49
+ {
50
+ position: 'absolute',
51
+ top: 0,
52
+ left: 0,
53
+ right: 0,
54
+ bottom: 0,
55
+ transform: [{ translateX: chatTranslateX }],
56
+ },
57
+ ]}
58
+ pointerEvents={activePage === 'chat' ? 'auto' : 'none'}
59
+ >
60
+ {chat}
61
+ </Animated.View>
62
+ </Animated.View>
63
+ );
64
+ }
65
+
66
+
@@ -0,0 +1,18 @@
1
+ export { StudioBottomSheet } from './StudioBottomSheet';
2
+ export type { StudioBottomSheetProps } from './StudioBottomSheet';
3
+
4
+ export { StudioSheetBackground } from './StudioSheetBackground';
5
+ export type { StudioSheetBackgroundProps } from './StudioSheetBackground';
6
+
7
+ export { StudioSheetPager } from './StudioSheetPager';
8
+ export type { StudioSheetPagerProps } from './StudioSheetPager';
9
+
10
+ export { StudioSheetHeader } from './StudioSheetHeader';
11
+ export type { StudioSheetHeaderProps } from './StudioSheetHeader';
12
+
13
+ export { StudioSheetHeaderIconButton } from './StudioSheetHeaderIconButton';
14
+ export type { StudioSheetHeaderIconButtonProps } from './StudioSheetHeaderIconButton';
15
+
16
+ export type { StudioSheetPage, StudioSheetSnapPoints } from './types';
17
+
18
+
@@ -0,0 +1,5 @@
1
+ export type StudioSheetPage = 'preview' | 'chat';
2
+
3
+ export type StudioSheetSnapPoints = readonly (string | number)[];
4
+
5
+
@@ -0,0 +1,25 @@
1
+ export function withAlpha(color: string, alpha: number): string {
2
+ const a = Math.max(0, Math.min(1, alpha));
3
+ const hex = color.trim();
4
+ if (!hex.startsWith('#')) return color;
5
+
6
+ const raw = hex.slice(1);
7
+ const expanded =
8
+ raw.length === 3
9
+ ? raw
10
+ .split('')
11
+ .map((c) => c + c)
12
+ .join('')
13
+ : raw;
14
+
15
+ if (expanded.length !== 6) return color;
16
+
17
+ const r = Number.parseInt(expanded.slice(0, 2), 16);
18
+ const g = Number.parseInt(expanded.slice(2, 4), 16);
19
+ const b = Number.parseInt(expanded.slice(4, 6), 16);
20
+
21
+ if ([r, g, b].some((n) => Number.isNaN(n))) return color;
22
+ return `rgba(${r}, ${g}, ${b}, ${a})`;
23
+ }
24
+
25
+
@@ -0,0 +1,19 @@
1
+ export function formatTimeAgo(iso: string): string {
2
+ const then = new Date(iso).getTime();
3
+ const now = Date.now();
4
+ const seconds = Math.max(1, Math.floor((now - then) / 1000));
5
+ const minutes = Math.floor(seconds / 60);
6
+ const hours = Math.floor(minutes / 60);
7
+ const days = Math.floor(hours / 24);
8
+ const months = Math.floor(days / 30);
9
+ const years = Math.floor(days / 365);
10
+
11
+ if (years > 0) return `${years}y ago`;
12
+ if (months > 0) return `${months}mo ago`;
13
+ if (days > 0) return `${days}d ago`;
14
+ if (hours > 0) return `${hours}h ago`;
15
+ if (minutes > 0) return `${minutes}m ago`;
16
+ return `${seconds}s ago`;
17
+ }
18
+
19
+
@@ -0,0 +1,42 @@
1
+ import { logger, consoleTransport } from 'react-native-logs';
2
+
3
+ export type StudioLogger = {
4
+ debug: (...args: unknown[]) => void;
5
+ info: (...args: unknown[]) => void;
6
+ warn: (...args: unknown[]) => void;
7
+ error: (...args: unknown[]) => void;
8
+ extend: (extension: string) => Pick<StudioLogger, 'debug' | 'info' | 'warn' | 'error'>;
9
+ enable: (extension?: string) => boolean;
10
+ disable: (extension?: string) => boolean;
11
+ getExtensions: () => string[];
12
+ setSeverity: (level: string) => string;
13
+ getSeverity: () => string;
14
+ patchConsole: () => void;
15
+ };
16
+
17
+ export const log: StudioLogger = logger.createLogger({
18
+ levels: {
19
+ debug: 0,
20
+ info: 1,
21
+ warn: 2,
22
+ error: 3,
23
+ },
24
+ severity: "debug",
25
+ transport: consoleTransport,
26
+ transportOptions: {
27
+ colors: {
28
+ info: "blueBright",
29
+ warn: "yellowBright",
30
+ error: "redBright",
31
+ },
32
+ },
33
+ async: true,
34
+ dateFormat: "time",
35
+ printLevel: true,
36
+ printDate: true,
37
+ fixedExtLvlLength: false,
38
+ enabled: true,
39
+ }
40
+ ) as unknown as StudioLogger;
41
+
42
+
@@ -0,0 +1,3 @@
1
+ export const BASE_URL = "http://192.168.8.175:8080";
2
+
3
+
@@ -0,0 +1,128 @@
1
+ import axios, {
2
+ AxiosError,
3
+ AxiosInstance,
4
+ InternalAxiosRequestConfig,
5
+ AxiosResponse,
6
+ } from 'axios';
7
+ import { getSupabaseClient } from '../supabase';
8
+ import { log } from '../../logger';
9
+ import { BASE_URL } from './baseUrl';
10
+
11
+ declare module 'axios' {
12
+ export interface AxiosRequestConfig {
13
+ _retried?: boolean;
14
+ }
15
+ }
16
+
17
+ export const createApiClient = (baseURL: string): AxiosInstance => {
18
+ const apiClient = axios.create({
19
+ baseURL,
20
+ timeout: 3 * 60 * 1000,
21
+ headers: {
22
+ Accept: 'application/json',
23
+ 'Content-Type': 'application/json',
24
+ },
25
+ });
26
+
27
+ const maskAuthHeader = (headers: unknown) => {
28
+ if (!headers || typeof headers !== 'object') return headers;
29
+ const copy: Record<string, unknown> = { ...(headers as any) };
30
+ const auth = (copy.Authorization ?? copy.authorization) as unknown;
31
+ if (typeof auth === 'string' && auth.startsWith('Bearer ')) {
32
+ copy.Authorization = 'Bearer [REDACTED]';
33
+ }
34
+ return copy;
35
+ };
36
+
37
+ apiClient.interceptors.request.use(
38
+ async (config: InternalAxiosRequestConfig) => {
39
+ try {
40
+ const supabase = getSupabaseClient();
41
+ const { data } = await supabase.auth.getSession();
42
+ const accessToken = data.session?.access_token;
43
+ if (accessToken) {
44
+ config.headers = config.headers ?? {};
45
+ (config.headers).Authorization = `Bearer ${accessToken}`;
46
+ }
47
+ } catch (err) {
48
+ log.warn('Failed to attach auth token to request', err);
49
+ }
50
+
51
+ log.debug('Request:', {
52
+ url: config.url,
53
+ method: config.method,
54
+ headers: maskAuthHeader(config.headers),
55
+ data: config.data,
56
+ });
57
+ return config;
58
+ },
59
+ (error: AxiosError) => {
60
+ log.error('Request Error:', error);
61
+ return Promise.reject(error);
62
+ }
63
+ );
64
+
65
+ apiClient.interceptors.response.use(
66
+ (response: AxiosResponse) => {
67
+ log.debug('Response:', {
68
+ url: response.config?.url,
69
+ status: response.status,
70
+ headers: response.headers,
71
+ data: response.data,
72
+ });
73
+ return response;
74
+ },
75
+ async (error: AxiosError) => {
76
+ const originalRequest = error.config as
77
+ | (InternalAxiosRequestConfig & { _retried?: boolean })
78
+ | undefined;
79
+ log.error('Response Error:', {
80
+ message: error.message,
81
+ code: error.code,
82
+ url: originalRequest?.url,
83
+ method: originalRequest?.method,
84
+ requestHeaders: maskAuthHeader(originalRequest?.headers),
85
+ requestData: originalRequest?.data,
86
+ status: error.response?.status,
87
+ statusText: (error.response as any)?.statusText,
88
+ responseHeaders: error.response?.headers,
89
+ responseData: error.response?.data,
90
+ });
91
+
92
+ if (!originalRequest) {
93
+ return Promise.reject(error);
94
+ }
95
+
96
+ const authHeader = (originalRequest.headers as any)?.Authorization as string | undefined;
97
+ const hasBearerToken = Boolean(authHeader && authHeader.startsWith('Bearer '));
98
+
99
+ if (error.response?.status === 401 && hasBearerToken && !originalRequest._retried) {
100
+ originalRequest._retried = true;
101
+ try {
102
+ const supabase = getSupabaseClient();
103
+ const { data, error: refreshError } = await supabase.auth.refreshSession();
104
+ if (refreshError) throw refreshError;
105
+ const newToken = data.session?.access_token;
106
+ if (newToken && originalRequest.headers) {
107
+ (originalRequest.headers as any).Authorization = `Bearer ${newToken}`;
108
+ }
109
+ await new Promise((resolve) => setTimeout(resolve, 500));
110
+ return apiClient(originalRequest);
111
+ } catch (refreshErr) {
112
+ log.warn('Token refresh failed', refreshErr);
113
+ return Promise.reject(refreshErr);
114
+ }
115
+ }
116
+
117
+ return Promise.reject(error);
118
+ }
119
+ );
120
+
121
+ return apiClient;
122
+ };
123
+
124
+ export const api = createApiClient(BASE_URL);
125
+
126
+ export default createApiClient;
127
+
128
+
@@ -0,0 +1,14 @@
1
+ import axios from "axios";
2
+
3
+ import { BASE_URL } from "./baseUrl";
4
+
5
+ export const publicApi = axios.create({
6
+ baseURL: BASE_URL,
7
+ timeout: 30_000,
8
+ headers: {
9
+ Accept: "application/json",
10
+ "Content-Type": "application/json",
11
+ },
12
+ });
13
+
14
+