@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.
- package/dist/index.js +255 -245
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +213 -203
- package/dist/index.mjs.map +1 -1
- package/package.json +8 -5
- package/src/components/chat/ChatComposer.tsx +277 -0
- package/src/components/chat/ChatHeader.tsx +31 -0
- package/src/components/chat/ChatMessageBubble.tsx +69 -0
- package/src/components/chat/ChatMessageList.tsx +137 -0
- package/src/components/chat/ChatPage.tsx +69 -0
- package/src/components/chat/ForkNoticeBanner.tsx +66 -0
- package/src/components/chat/MultilineTextInput.tsx +46 -0
- package/src/components/chat/ScrollToBottomButton.tsx +78 -0
- package/src/components/chat/TypingIndicator.tsx +54 -0
- package/src/components/chat/index.ts +28 -0
- package/src/components/comments/AppCommentsSheet.tsx +213 -0
- package/src/components/comments/CommentRow.tsx +63 -0
- package/src/components/comments/formatTimeAgo.ts +3 -0
- package/src/components/comments/index.ts +3 -0
- package/src/components/comments/useAppComments.ts +74 -0
- package/src/components/comments/useAppDetails.ts +35 -0
- package/src/components/comments/useIosKeyboardSnapFix.ts +24 -0
- package/src/components/dialogs/ConfirmMergeRequestDialog.tsx +156 -0
- package/src/components/dialogs/index.ts +4 -0
- package/src/components/draw/DrawColorPicker.tsx +77 -0
- package/src/components/draw/DrawModeOverlay.tsx +144 -0
- package/src/components/draw/DrawSurface.tsx +127 -0
- package/src/components/draw/DrawToolbar.tsx +253 -0
- package/src/components/draw/index.ts +15 -0
- package/src/components/draw/optionalHaptics.ts +15 -0
- package/src/components/draw/strokes.ts +21 -0
- package/src/components/draw/types.ts +9 -0
- package/src/components/floating-draggable-button/FloatingDraggableButton.tsx +323 -0
- package/src/components/floating-draggable-button/constants.ts +17 -0
- package/src/components/floating-draggable-button/index.ts +4 -0
- package/src/components/floating-draggable-button/types.ts +63 -0
- package/src/components/icons/MergeIcon.tsx +14 -0
- package/src/components/icons/StudioIcons.tsx +66 -0
- package/src/components/index.ts +17 -0
- package/src/components/merge-requests/MergeRequestStatusCard.tsx +179 -0
- package/src/components/merge-requests/ReviewMergeRequestActionButton.tsx +62 -0
- package/src/components/merge-requests/ReviewMergeRequestCard.tsx +192 -0
- package/src/components/merge-requests/ReviewMergeRequestCarousel.tsx +132 -0
- package/src/components/merge-requests/index.ts +7 -0
- package/src/components/merge-requests/mergeRequestStatusDisplay.ts +23 -0
- package/src/components/merge-requests/toIsoString.ts +9 -0
- package/src/components/merge-requests/useControlledExpansion.ts +16 -0
- package/src/components/models/index.ts +9 -0
- package/src/components/models/types.ts +43 -0
- package/src/components/overlays/EdgeGlowFrame.tsx +105 -0
- package/src/components/overlays/index.ts +4 -0
- package/src/components/preview/PreviewHeroCard.tsx +58 -0
- package/src/components/preview/PreviewImage.tsx +22 -0
- package/src/components/preview/PreviewMetaRow.tsx +70 -0
- package/src/components/preview/PreviewPage.tsx +36 -0
- package/src/components/preview/PreviewPlaceholder.tsx +72 -0
- package/src/components/preview/PreviewStatusBadge.tsx +63 -0
- package/src/components/preview/StatsBar.tsx +109 -0
- package/src/components/preview/index.ts +22 -0
- package/src/components/primitives/Avatar.tsx +68 -0
- package/src/components/primitives/Button.tsx +102 -0
- package/src/components/primitives/Card.tsx +30 -0
- package/src/components/primitives/Divider.tsx +17 -0
- package/src/components/primitives/Icon.tsx +40 -0
- package/src/components/primitives/MarkdownText.tsx +72 -0
- package/src/components/primitives/Modal.tsx +53 -0
- package/src/components/primitives/Surface.tsx +42 -0
- package/src/components/primitives/Text.tsx +83 -0
- package/src/components/primitives/index.ts +35 -0
- package/src/components/primitives/types.ts +30 -0
- package/src/components/studio-sheet/StudioBottomSheet.tsx +114 -0
- package/src/components/studio-sheet/StudioSheetBackground.tsx +63 -0
- package/src/components/studio-sheet/StudioSheetHeader.tsx +35 -0
- package/src/components/studio-sheet/StudioSheetHeaderIconButton.tsx +109 -0
- package/src/components/studio-sheet/StudioSheetPager.tsx +66 -0
- package/src/components/studio-sheet/index.ts +18 -0
- package/src/components/studio-sheet/types.ts +5 -0
- package/src/components/utils/color.ts +25 -0
- package/src/components/utils/formatTimeAgo.ts +19 -0
- package/src/core/logger.ts +42 -0
- package/src/core/services/http/baseUrl.ts +3 -0
- package/src/core/services/http/index.ts +128 -0
- package/src/core/services/http/public.ts +14 -0
- package/src/core/services/supabase/auth.ts +41 -0
- package/src/core/services/supabase/client.ts +43 -0
- package/src/core/services/supabase/index.ts +7 -0
- package/src/data/agent/remote.ts +30 -0
- package/src/data/agent/repository.ts +34 -0
- package/src/data/agent/types.ts +28 -0
- package/src/data/apps/bundles/remote.ts +47 -0
- package/src/data/apps/bundles/repository.ts +35 -0
- package/src/data/apps/bundles/types.ts +27 -0
- package/src/data/apps/images/remote.ts +61 -0
- package/src/data/apps/images/repository.ts +47 -0
- package/src/data/apps/remote.ts +97 -0
- package/src/data/apps/repository.ts +185 -0
- package/src/data/apps/types.ts +206 -0
- package/src/data/attachment/remote.ts +32 -0
- package/src/data/attachment/repository.ts +40 -0
- package/src/data/attachment/types.ts +42 -0
- package/src/data/base-remote.ts +3 -0
- package/src/data/base-repository.ts +11 -0
- package/src/data/comments/likes/remote.ts +87 -0
- package/src/data/comments/likes/repository.ts +61 -0
- package/src/data/comments/likes/types.ts +47 -0
- package/src/data/comments/remote.ts +71 -0
- package/src/data/comments/repository.ts +53 -0
- package/src/data/comments/types.ts +60 -0
- package/src/data/github/remote.ts +23 -0
- package/src/data/github/repository.ts +35 -0
- package/src/data/github/types.ts +23 -0
- package/src/data/home/remote.ts +24 -0
- package/src/data/home/repository.ts +28 -0
- package/src/data/home/types.ts +70 -0
- package/src/data/index.ts +3 -0
- package/src/data/likes/remote.ts +57 -0
- package/src/data/likes/repository.ts +47 -0
- package/src/data/likes/types.ts +46 -0
- package/src/data/me/remote.ts +28 -0
- package/src/data/me/repository.ts +30 -0
- package/src/data/me/types.ts +14 -0
- package/src/data/merge-requests/remote.ts +76 -0
- package/src/data/merge-requests/repository.ts +66 -0
- package/src/data/merge-requests/types.ts +33 -0
- package/src/data/messages/remote.ts +21 -0
- package/src/data/messages/repository.ts +104 -0
- package/src/data/messages/types.ts +20 -0
- package/src/data/public/studio-config/remote.ts +19 -0
- package/src/data/public/studio-config/repository.ts +23 -0
- package/src/data/public/studio-config/types.ts +6 -0
- package/src/data/ratings/remote.ts +76 -0
- package/src/data/ratings/repository.ts +63 -0
- package/src/data/ratings/types.ts +57 -0
- package/src/data/threads/remote.ts +40 -0
- package/src/data/threads/repository.ts +41 -0
- package/src/data/threads/types.ts +25 -0
- package/src/data/types.ts +8 -0
- package/src/data/users/remote.ts +31 -0
- package/src/data/users/repository.ts +45 -0
- package/src/data/users/types.ts +15 -0
- package/src/index.ts +6 -0
- package/src/studio/ComergeStudio.tsx +246 -0
- package/src/studio/bootstrap/StudioBootstrap.tsx +45 -0
- package/src/studio/bootstrap/useStudioBootstrap.ts +51 -0
- package/src/studio/hooks/useApp.ts +83 -0
- package/src/studio/hooks/useAppStats.ts +111 -0
- package/src/studio/hooks/useAttachmentUpload.ts +59 -0
- package/src/studio/hooks/useBundleManager.ts +389 -0
- package/src/studio/hooks/useMergeRequests.ts +173 -0
- package/src/studio/hooks/useStudioActions.ts +96 -0
- package/src/studio/hooks/useThreadMessages.ts +85 -0
- package/src/studio/lib/chat.ts +34 -0
- package/src/studio/ui/ChatPanel.tsx +154 -0
- package/src/studio/ui/ConfirmMergeFlow.tsx +55 -0
- package/src/studio/ui/PreviewPanel.tsx +131 -0
- package/src/studio/ui/RuntimeRenderer.tsx +40 -0
- package/src/studio/ui/StudioOverlay.tsx +257 -0
- package/src/studio/ui/preview-panel/PressableCardRow.tsx +49 -0
- package/src/studio/ui/preview-panel/PreviewCollaborateSection.tsx +174 -0
- package/src/studio/ui/preview-panel/PreviewCustomizeSection.tsx +160 -0
- package/src/studio/ui/preview-panel/PreviewHeroSection.tsx +56 -0
- package/src/studio/ui/preview-panel/PreviewMetaSection.tsx +67 -0
- package/src/studio/ui/preview-panel/PreviewPanelHeader.tsx +48 -0
- package/src/studio/ui/preview-panel/SectionTitle.tsx +31 -0
- package/src/studio/ui/preview-panel/usePreviewPanelData.ts +132 -0
- package/src/studio/ui/preview-panel/utils.ts +29 -0
- package/src/theme/index.ts +5 -0
- package/src/theme/tokens.ts +118 -0
- package/src/theme/types.ts +90 -0
- package/src/theme/useTheme.ts +11 -0
- package/dist/assets/images/merge.svg +0 -3
- 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,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,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
|
+
|