@comergehq/studio 0.1.1 → 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 +9 -6
  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,257 @@
1
+ import * as React from 'react';
2
+ import { Keyboard, View, useWindowDimensions } from 'react-native';
3
+
4
+ import type { App } from '../../data/apps/types';
5
+ import type { MergeRequest } from '../../data/merge-requests/types';
6
+ import { StudioBottomSheet } from '../../components/studio-sheet/StudioBottomSheet';
7
+ import { StudioSheetPager } from '../../components/studio-sheet/StudioSheetPager';
8
+ import { FloatingDraggableButton } from '../../components/floating-draggable-button/FloatingDraggableButton';
9
+ import { EdgeGlowFrame } from '../../components/overlays/EdgeGlowFrame';
10
+ import { DrawModeOverlay } from '../../components/draw/DrawModeOverlay';
11
+ import { AppCommentsSheet } from '../../components/comments/AppCommentsSheet';
12
+ import { PreviewPanel } from './PreviewPanel';
13
+ import { ChatPanel } from './ChatPanel';
14
+ import { ConfirmMergeFlow } from './ConfirmMergeFlow';
15
+ import type { MergeRequestSummary } from '../../components/models/types';
16
+ import { useTheme } from '../../theme';
17
+
18
+ import { MergeIcon } from '../../components/icons/MergeIcon';
19
+
20
+ export type StudioOverlayProps = {
21
+ captureTargetRef: React.RefObject<View | null>;
22
+
23
+ app: App | null;
24
+ appLoading?: boolean;
25
+
26
+ // Studio state
27
+ isOwner: boolean;
28
+ shouldForkOnEdit: boolean;
29
+
30
+ // Bundle testing (glow + restore)
31
+ isTesting: boolean;
32
+ onRestoreBase: () => void | Promise<void>;
33
+
34
+ // Merge requests
35
+ incomingMergeRequests: MergeRequest[];
36
+ outgoingMergeRequests: MergeRequest[];
37
+ creatorStatsById: Record<string, import('../../data/users/types').UserStats>;
38
+ processingMrId?: string | null;
39
+ isBuildingMrTest?: boolean;
40
+ testingMrId?: string | null;
41
+ toMergeRequestSummary: (mr: MergeRequest) => MergeRequestSummary;
42
+ onSubmitMergeRequest?: () => void | Promise<void>;
43
+ onApprove?: (mr: MergeRequest) => void | Promise<void>;
44
+ onReject?: (mr: MergeRequest) => void | Promise<void>;
45
+ onTestMr?: (mr: MergeRequest) => void | Promise<void>;
46
+
47
+ // Chat
48
+ chatMessages: import('../../components/models/types').ChatMessage[];
49
+ chatLoading?: boolean;
50
+ chatSendDisabled?: boolean;
51
+ chatForking?: boolean;
52
+ chatSending?: boolean;
53
+ chatShowTypingIndicator?: boolean;
54
+ onSendChat: (text: string, attachments?: string[]) => void | Promise<void>;
55
+
56
+ // Navigation callbacks
57
+ onNavigateHome?: () => void;
58
+ };
59
+
60
+ type SheetPage = 'preview' | 'chat';
61
+
62
+ export function StudioOverlay({
63
+ captureTargetRef,
64
+ app,
65
+ appLoading,
66
+ isOwner,
67
+ shouldForkOnEdit,
68
+ isTesting,
69
+ onRestoreBase,
70
+ incomingMergeRequests,
71
+ outgoingMergeRequests,
72
+ creatorStatsById,
73
+ processingMrId,
74
+ isBuildingMrTest,
75
+ testingMrId,
76
+ toMergeRequestSummary,
77
+ onSubmitMergeRequest,
78
+ onApprove,
79
+ onReject,
80
+ onTestMr,
81
+ chatMessages,
82
+ chatLoading,
83
+ chatSendDisabled,
84
+ chatForking,
85
+ chatSending,
86
+ chatShowTypingIndicator,
87
+ onSendChat,
88
+ onNavigateHome,
89
+ }: StudioOverlayProps) {
90
+ const theme = useTheme();
91
+ const { width } = useWindowDimensions();
92
+
93
+ const [sheetOpen, setSheetOpen] = React.useState(false);
94
+ const [activePage, setActivePage] = React.useState<SheetPage>('preview');
95
+
96
+ const [drawing, setDrawing] = React.useState(false);
97
+ const [chatAttachments, setChatAttachments] = React.useState<string[]>([]);
98
+ const [commentsAppId, setCommentsAppId] = React.useState<string | null>(null);
99
+ const [commentsCount, setCommentsCount] = React.useState<number | null>(null);
100
+
101
+ const [confirmMrId, setConfirmMrId] = React.useState<string | null>(null);
102
+ const confirmMr = React.useMemo(
103
+ () => (confirmMrId ? incomingMergeRequests.find((m) => m.id === confirmMrId) ?? null : null),
104
+ [confirmMrId, incomingMergeRequests]
105
+ );
106
+
107
+ const closeSheet = React.useCallback(() => {
108
+ setSheetOpen(false);
109
+ Keyboard.dismiss();
110
+ }, []);
111
+
112
+ const openSheet = React.useCallback(() => setSheetOpen(true), []);
113
+
114
+ const goToChat = React.useCallback(() => {
115
+ setActivePage('chat');
116
+ openSheet();
117
+ }, [openSheet]);
118
+
119
+ const backToPreview = React.useCallback(() => {
120
+ Keyboard.dismiss();
121
+ setActivePage('preview');
122
+ }, []);
123
+
124
+ const startDraw = React.useCallback(() => {
125
+ setDrawing(true);
126
+ closeSheet();
127
+ }, [closeSheet]);
128
+
129
+ const handleDrawCapture = React.useCallback(
130
+ (dataUrl: string) => {
131
+ setChatAttachments((prev) => [...prev, dataUrl]);
132
+ setDrawing(false);
133
+ setActivePage('chat');
134
+ openSheet();
135
+ },
136
+ [openSheet]
137
+ );
138
+
139
+ const toggleSheet = React.useCallback(async () => {
140
+ if (!sheetOpen) {
141
+ const shouldExitTest = Boolean(testingMrId) || isTesting;
142
+ if (shouldExitTest) {
143
+ void Promise.resolve(onRestoreBase()).catch(() => {});
144
+ }
145
+ setSheetOpen(true);
146
+ } else {
147
+ closeSheet();
148
+ }
149
+ }, [closeSheet, isTesting, onRestoreBase, sheetOpen, testingMrId]);
150
+
151
+ const handleTestMr = React.useCallback(
152
+ async (mr: MergeRequest) => {
153
+ if (!onTestMr) return;
154
+ await onTestMr(mr);
155
+ closeSheet();
156
+ },
157
+ [closeSheet, onTestMr]
158
+ );
159
+
160
+ return (
161
+ <>
162
+ {/* Testing glow around runtime */}
163
+ <EdgeGlowFrame visible={isTesting} role="accent" thickness={40} intensity={1} />
164
+
165
+ <StudioBottomSheet open={sheetOpen} onOpenChange={setSheetOpen}>
166
+ <StudioSheetPager
167
+ activePage={activePage}
168
+ width={width}
169
+ preview={
170
+ <PreviewPanel
171
+ app={app}
172
+ loading={appLoading}
173
+ isOwner={isOwner}
174
+ shouldForkOnEdit={shouldForkOnEdit}
175
+ incomingMergeRequests={incomingMergeRequests}
176
+ outgoingMergeRequests={outgoingMergeRequests}
177
+ creatorStatsById={creatorStatsById}
178
+ processingMrId={processingMrId}
179
+ isBuildingMrTest={isBuildingMrTest}
180
+ testingMrId={testingMrId}
181
+ toMergeRequestSummary={toMergeRequestSummary}
182
+ onClose={closeSheet}
183
+ onNavigateHome={onNavigateHome}
184
+ onGoToChat={goToChat}
185
+ onStartDraw={isOwner ? startDraw : undefined}
186
+ onSubmitMergeRequest={onSubmitMergeRequest}
187
+ onRequestApprove={(mr) => setConfirmMrId(mr.id)}
188
+ onReject={onReject}
189
+ onTestMr={handleTestMr}
190
+ onOpenComments={() => setCommentsAppId(app?.id ?? null)}
191
+ commentCountOverride={commentsCount ?? undefined}
192
+ />
193
+ }
194
+ chat={
195
+ <ChatPanel
196
+ messages={chatMessages}
197
+ showTypingIndicator={chatShowTypingIndicator}
198
+ loading={chatLoading}
199
+ sendDisabled={chatSendDisabled}
200
+ forking={chatForking}
201
+ sending={chatSending}
202
+ autoFocusComposer={sheetOpen && activePage === 'chat'}
203
+ shouldForkOnEdit={shouldForkOnEdit}
204
+ attachments={chatAttachments}
205
+ onRemoveAttachment={(idx) => setChatAttachments((prev) => prev.filter((_, i) => i !== idx))}
206
+ onClearAttachments={() => setChatAttachments([])}
207
+ onBack={backToPreview}
208
+ onClose={closeSheet}
209
+ onNavigateHome={onNavigateHome}
210
+ onStartDraw={startDraw}
211
+ onSend={onSendChat}
212
+ />
213
+ }
214
+ />
215
+ </StudioBottomSheet>
216
+
217
+ <FloatingDraggableButton
218
+ visible={!sheetOpen && !drawing}
219
+ ariaLabel={sheetOpen ? 'Hide studio' : 'Show studio'}
220
+ badgeCount={incomingMergeRequests.length}
221
+ onPress={toggleSheet}
222
+ isLoading={app?.status === 'editing'}
223
+ >
224
+ <View style={{ width: 28, height: 28, alignItems: 'center', justifyContent: 'center' }}>
225
+ <MergeIcon width={24} height={24} color={theme.colors.floatingContent} />
226
+ </View>
227
+ </FloatingDraggableButton>
228
+
229
+ <DrawModeOverlay
230
+ visible={drawing}
231
+ captureTargetRef={captureTargetRef}
232
+ onCancel={() => setDrawing(false)}
233
+ onCapture={handleDrawCapture}
234
+ />
235
+
236
+ <ConfirmMergeFlow
237
+ visible={Boolean(confirmMr)}
238
+ onOpenChange={(open) => {
239
+ if (!open) setConfirmMrId(null);
240
+ }}
241
+ mergeRequest={confirmMr}
242
+ toSummary={toMergeRequestSummary}
243
+ onConfirm={(mr) => onApprove?.(mr)}
244
+ onTestFirst={handleTestMr}
245
+ />
246
+
247
+ <AppCommentsSheet
248
+ appId={commentsAppId}
249
+ onClose={() => setCommentsAppId(null)}
250
+ onCountChange={(count) => setCommentsCount(count)}
251
+ onPlayApp={() => setCommentsAppId(null)}
252
+ />
253
+ </>
254
+ );
255
+ }
256
+
257
+
@@ -0,0 +1,49 @@
1
+ import * as React from 'react';
2
+ import { Pressable, View, type ViewStyle } from 'react-native';
3
+
4
+ import { Card } from '../../../components/primitives/Card';
5
+
6
+ export type PressableCardRowProps = {
7
+ accessibilityLabel: string;
8
+ onPress: () => void;
9
+ disabled?: boolean;
10
+ left: React.ReactNode;
11
+ title: React.ReactNode;
12
+ subtitle?: React.ReactNode;
13
+ right?: React.ReactNode;
14
+ style?: ViewStyle;
15
+ };
16
+
17
+ export function PressableCardRow({
18
+ accessibilityLabel,
19
+ onPress,
20
+ disabled,
21
+ left,
22
+ title,
23
+ subtitle,
24
+ right,
25
+ style,
26
+ }: PressableCardRowProps) {
27
+ return (
28
+ <Pressable
29
+ accessibilityRole="button"
30
+ accessibilityLabel={accessibilityLabel}
31
+ disabled={disabled}
32
+ onPress={onPress}
33
+ style={({ pressed }) => ({ opacity: disabled ? 0.6 : pressed ? 0.85 : 1 })}
34
+ >
35
+ <Card padded={false} border={false} style={style}>
36
+ <View style={{ flexDirection: 'row', alignItems: 'center' }}>
37
+ {left}
38
+ <View style={{ flex: 1, minWidth: 0 }}>
39
+ {title}
40
+ {subtitle ? subtitle : null}
41
+ </View>
42
+ {right ? <View style={{ marginLeft: 16 }}>{right}</View> : null}
43
+ </View>
44
+ </Card>
45
+ </Pressable>
46
+ );
47
+ }
48
+
49
+
@@ -0,0 +1,174 @@
1
+ import * as React from 'react';
2
+ import { ActivityIndicator, Alert, View } from 'react-native';
3
+ import { Send } from 'lucide-react-native';
4
+
5
+ import type { MergeRequest } from '../../../data/merge-requests/types';
6
+ import type { UserStats } from '../../../data/users/types';
7
+ import { MergeRequestStatusCard } from '../../../components/merge-requests/MergeRequestStatusCard';
8
+ import { ReviewMergeRequestCarousel } from '../../../components/merge-requests/ReviewMergeRequestCarousel';
9
+ import { Text } from '../../../components/primitives/Text';
10
+ import { withAlpha } from '../../../components/utils/color';
11
+ import { useTheme } from '../../../theme';
12
+ import { PressableCardRow } from './PressableCardRow';
13
+ import { SectionTitle } from './SectionTitle';
14
+
15
+ import { MergeIcon } from '../../../components/icons/MergeIcon';
16
+
17
+ export type PreviewCollaborateSectionProps = {
18
+ canSubmitMergeRequest: boolean;
19
+ incomingMergeRequests: MergeRequest[];
20
+ outgoingMergeRequests: MergeRequest[];
21
+ creatorStatsById: Record<string, UserStats>;
22
+ processingMrId?: string | null;
23
+ isBuildingMrTest?: boolean;
24
+ testingMrId?: string | null;
25
+ toMergeRequestSummary: (mr: MergeRequest) => import('../../../components/models/types').MergeRequestSummary;
26
+ onSubmitMergeRequest?: () => void | Promise<void>;
27
+ onRequestApprove?: (mr: MergeRequest) => void;
28
+ onReject?: (mr: MergeRequest) => void | Promise<void>;
29
+ onTestMr?: (mr: MergeRequest) => void | Promise<void>;
30
+ };
31
+
32
+ export function PreviewCollaborateSection({
33
+ canSubmitMergeRequest,
34
+ incomingMergeRequests,
35
+ outgoingMergeRequests,
36
+ creatorStatsById,
37
+ processingMrId,
38
+ isBuildingMrTest,
39
+ testingMrId,
40
+ toMergeRequestSummary,
41
+ onSubmitMergeRequest,
42
+ onRequestApprove,
43
+ onReject,
44
+ onTestMr,
45
+ }: PreviewCollaborateSectionProps) {
46
+ const theme = useTheme();
47
+ const [submittingMr, setSubmittingMr] = React.useState(false);
48
+
49
+ const hasSection = canSubmitMergeRequest || incomingMergeRequests.length > 0 || outgoingMergeRequests.length > 0;
50
+ if (!hasSection) return null;
51
+
52
+ const showActionsSubtitle = (canSubmitMergeRequest && onSubmitMergeRequest) || (onTestMr && incomingMergeRequests.length > 0);
53
+
54
+ return (
55
+ <>
56
+ <SectionTitle marginTop={theme.spacing.xl}>Collaborate</SectionTitle>
57
+
58
+ {showActionsSubtitle ? (
59
+ <Text
60
+ style={{
61
+ color: withAlpha(theme.colors.textMuted, 0.7),
62
+ fontSize: 10,
63
+ lineHeight: 14,
64
+ textTransform: 'uppercase',
65
+ letterSpacing: 0.8,
66
+ marginBottom: theme.spacing.sm,
67
+ fontWeight: theme.typography.fontWeight.semibold,
68
+ }}
69
+ >
70
+ Actions
71
+ </Text>
72
+ ) : null}
73
+
74
+ {canSubmitMergeRequest && onSubmitMergeRequest ? (
75
+ <PressableCardRow
76
+ accessibilityLabel="Submit merge request"
77
+ disabled={submittingMr}
78
+ onPress={() => {
79
+ Alert.alert(
80
+ 'Submit Merge Request',
81
+ 'Are you sure you want to submit your changes to the original app?',
82
+ [
83
+ { text: 'Cancel', style: 'cancel' },
84
+ {
85
+ text: 'Submit',
86
+ style: 'destructive',
87
+ onPress: () => {
88
+ setSubmittingMr(true);
89
+ Promise.resolve(onSubmitMergeRequest())
90
+ .catch(() => {})
91
+ .finally(() => setSubmittingMr(false));
92
+ },
93
+ },
94
+ ]
95
+ );
96
+ }}
97
+ style={{
98
+ padding: theme.spacing.lg,
99
+ borderRadius: theme.radii.lg,
100
+ backgroundColor: withAlpha(theme.colors.surfaceRaised, 0.5),
101
+ borderWidth: 1,
102
+ borderColor: withAlpha('#03DAC6', 0.2),
103
+ marginBottom: theme.spacing.sm,
104
+ }}
105
+ left={
106
+ <View
107
+ style={{
108
+ width: 40,
109
+ height: 40,
110
+ borderRadius: 999,
111
+ alignItems: 'center',
112
+ justifyContent: 'center',
113
+ backgroundColor: withAlpha('#03DAC6', 0.1),
114
+ marginRight: theme.spacing.lg,
115
+ }}
116
+ >
117
+ {submittingMr ? <ActivityIndicator color="#03DAC6" size="small" /> : <MergeIcon width={20} height={20} color="#03DAC6" />}
118
+ </View>
119
+ }
120
+ title={
121
+ <Text style={{ color: theme.colors.text, fontSize: 16, lineHeight: 20, fontWeight: theme.typography.fontWeight.semibold }}>
122
+ Submit your new changes
123
+ </Text>
124
+ }
125
+ subtitle={
126
+ <Text style={{ color: theme.colors.textMuted, fontSize: 12, lineHeight: 16, marginTop: 2 }}>
127
+ Ask to merge this remix to the original app
128
+ </Text>
129
+ }
130
+ right={<Send size={16} color="#03DAC6" />}
131
+ />
132
+ ) : null}
133
+
134
+ {onTestMr && incomingMergeRequests.length > 0 ? (
135
+ <ReviewMergeRequestCarousel
136
+ mergeRequests={incomingMergeRequests}
137
+ creatorStatsById={creatorStatsById}
138
+ processingMrId={processingMrId}
139
+ isBuilding={Boolean(isBuildingMrTest)}
140
+ testingMrId={testingMrId}
141
+ onReject={(mr) => (onReject ? onReject(mr) : undefined)}
142
+ onApprove={(mr) => onRequestApprove?.(mr)}
143
+ onTest={(mr) => (onTestMr ? onTestMr(mr) : undefined)}
144
+ />
145
+ ) : null}
146
+
147
+ {outgoingMergeRequests.length > 0 ? (
148
+ <>
149
+ <Text
150
+ style={{
151
+ color: withAlpha(theme.colors.textMuted, 0.7),
152
+ fontSize: 10,
153
+ lineHeight: 14,
154
+ textTransform: 'uppercase',
155
+ letterSpacing: 0.8,
156
+ marginTop: theme.spacing.lg,
157
+ marginBottom: theme.spacing.sm,
158
+ fontWeight: theme.typography.fontWeight.semibold,
159
+ }}
160
+ >
161
+ History
162
+ </Text>
163
+ {outgoingMergeRequests.map((mr) => (
164
+ <View key={mr.id} style={{ marginBottom: theme.spacing.sm }}>
165
+ <MergeRequestStatusCard mergeRequest={toMergeRequestSummary(mr)} />
166
+ </View>
167
+ ))}
168
+ </>
169
+ ) : null}
170
+ </>
171
+ );
172
+ }
173
+
174
+
@@ -0,0 +1,160 @@
1
+ import * as React from 'react';
2
+ import { ActivityIndicator, View } from 'react-native';
3
+
4
+ import type { App } from '../../../data/apps/types';
5
+ import { Text } from '../../../components/primitives/Text';
6
+ import { IconChat, IconChevronRight, IconDraw } from '../../../components/icons/StudioIcons';
7
+ import { withAlpha } from '../../../components/utils/color';
8
+ import { useTheme } from '../../../theme';
9
+ import { PressableCardRow } from './PressableCardRow';
10
+ import { SectionTitle } from './SectionTitle';
11
+ import { statusDescription } from './utils';
12
+
13
+ export type PreviewCustomizeSectionProps = {
14
+ app: App;
15
+ isOwner: boolean;
16
+ shouldForkOnEdit: boolean;
17
+ showProcessing: boolean;
18
+ onGoToChat: () => void;
19
+ onStartDraw?: () => void;
20
+ };
21
+
22
+ export function PreviewCustomizeSection({
23
+ app,
24
+ isOwner,
25
+ shouldForkOnEdit,
26
+ showProcessing,
27
+ onGoToChat,
28
+ onStartDraw,
29
+ }: PreviewCustomizeSectionProps) {
30
+ const theme = useTheme();
31
+
32
+ return (
33
+ <>
34
+ <SectionTitle>Customize</SectionTitle>
35
+
36
+ {showProcessing ? (
37
+ <View
38
+ style={{
39
+ flexDirection: 'row',
40
+ alignItems: 'center',
41
+ padding: theme.spacing.lg,
42
+ borderRadius: theme.radii.lg,
43
+ backgroundColor: withAlpha(theme.colors.surfaceRaised, 0.5),
44
+ borderWidth: 1,
45
+ borderColor: withAlpha(theme.colors.warning, 0.2),
46
+ marginBottom: theme.spacing.sm,
47
+ }}
48
+ >
49
+ <View
50
+ style={{
51
+ width: 40,
52
+ height: 40,
53
+ borderRadius: 999,
54
+ alignItems: 'center',
55
+ justifyContent: 'center',
56
+ backgroundColor: withAlpha(theme.colors.warning, 0.1),
57
+ marginRight: theme.spacing.lg,
58
+ }}
59
+ >
60
+ <ActivityIndicator color={theme.colors.warning} size="small" />
61
+ </View>
62
+ <View style={{ flex: 1, minWidth: 0 }}>
63
+ <Text style={{ color: theme.colors.text, fontSize: 16, lineHeight: 20, fontWeight: theme.typography.fontWeight.semibold }}>
64
+ {app.status === 'error' ? 'Error' : 'Processing'}
65
+ </Text>
66
+ <Text style={{ color: theme.colors.textMuted, fontSize: 12, lineHeight: 16, marginTop: 2 }}>
67
+ {statusDescription(app.status, app.statusError)}
68
+ </Text>
69
+ </View>
70
+ </View>
71
+ ) : null}
72
+
73
+ <PressableCardRow
74
+ accessibilityLabel={isOwner ? 'Edit app' : 'Remix app'}
75
+ onPress={onGoToChat}
76
+ style={{
77
+ padding: theme.spacing.lg,
78
+ borderRadius: theme.radii.lg,
79
+ backgroundColor: withAlpha(theme.colors.surfaceRaised, 0.5),
80
+ borderWidth: 1,
81
+ borderColor: withAlpha(theme.colors.primary, 0.1),
82
+ marginBottom: theme.spacing.sm,
83
+ }}
84
+ left={
85
+ <View
86
+ style={{
87
+ width: 40,
88
+ height: 40,
89
+ borderRadius: 999,
90
+ alignItems: 'center',
91
+ justifyContent: 'center',
92
+ backgroundColor: withAlpha(theme.colors.primary, 0.1),
93
+ marginRight: theme.spacing.lg,
94
+ }}
95
+ >
96
+ <IconChat size={20} colorToken="primary" />
97
+ </View>
98
+ }
99
+ title={
100
+ <Text style={{ color: theme.colors.text, fontSize: 16, lineHeight: 20, fontWeight: theme.typography.fontWeight.semibold }}>
101
+ {isOwner ? (app.forkedFromAppId ? 'Edit your Remix' : 'Edit Your App') : 'Remix App'}
102
+ </Text>
103
+ }
104
+ subtitle={
105
+ <Text style={{ color: theme.colors.textMuted, fontSize: 12, lineHeight: 16, marginTop: 2 }}>
106
+ {isOwner && app.forkedFromAppId
107
+ ? 'Make changes to your remix with chat'
108
+ : shouldForkOnEdit
109
+ ? 'Chat to create your own copy and edit it'
110
+ : 'Chat to apply changes'}
111
+ </Text>
112
+ }
113
+ right={<IconChevronRight size={20} colorToken="textMuted" />}
114
+ />
115
+
116
+ {isOwner && onStartDraw ? (
117
+ <PressableCardRow
118
+ accessibilityLabel="Draw changes"
119
+ onPress={onStartDraw}
120
+ style={{
121
+ padding: theme.spacing.lg,
122
+ borderRadius: theme.radii.lg,
123
+ backgroundColor: withAlpha(theme.colors.surfaceRaised, 0.5),
124
+ borderWidth: 1,
125
+ borderColor: withAlpha(theme.colors.danger, 0.1),
126
+ marginBottom: theme.spacing.sm,
127
+ }}
128
+ left={
129
+ <View
130
+ style={{
131
+ width: 40,
132
+ height: 40,
133
+ borderRadius: 999,
134
+ alignItems: 'center',
135
+ justifyContent: 'center',
136
+ backgroundColor: withAlpha(theme.colors.danger, 0.1),
137
+ marginRight: theme.spacing.lg,
138
+ }}
139
+ >
140
+ <IconDraw size={20} colorToken="danger" />
141
+ </View>
142
+ }
143
+ title={
144
+ <Text style={{ color: theme.colors.text, fontSize: 16, lineHeight: 20, fontWeight: theme.typography.fontWeight.semibold }}>
145
+ Draw Changes
146
+ </Text>
147
+ }
148
+ subtitle={
149
+ <Text style={{ color: theme.colors.textMuted, fontSize: 12, lineHeight: 16, marginTop: 2 }}>
150
+ Annotate the app with drawings
151
+ </Text>
152
+ }
153
+ right={<IconChevronRight size={20} colorToken="textMuted" />}
154
+ />
155
+ ) : null}
156
+ </>
157
+ );
158
+ }
159
+
160
+
@@ -0,0 +1,56 @@
1
+ import * as React from 'react';
2
+
3
+ import type { App } from '../../../data/apps/types';
4
+ import { PreviewHeroCard } from '../../../components/preview/PreviewHeroCard';
5
+ import { PreviewPlaceholder } from '../../../components/preview/PreviewPlaceholder';
6
+ import { PreviewImage } from '../../../components/preview/PreviewImage';
7
+ import { StatsBar } from '../../../components/preview/StatsBar';
8
+ import { PreviewStatusBadge } from '../../../components/preview/PreviewStatusBadge';
9
+
10
+ export type PreviewHeroSectionProps = {
11
+ appStatus: App['status'];
12
+ showProcessing: boolean;
13
+ imageUrl: string | null;
14
+ imageLoaded: boolean;
15
+ onImageLoad: () => void;
16
+ stats: {
17
+ likeCount: number;
18
+ commentCount: number;
19
+ forkCount: number;
20
+ isLiked: boolean;
21
+ handleLike: () => Promise<void> | void;
22
+ handleOpenComments: () => void;
23
+ };
24
+ };
25
+
26
+ export function PreviewHeroSection({
27
+ appStatus,
28
+ showProcessing,
29
+ imageUrl,
30
+ imageLoaded,
31
+ onImageLoad,
32
+ stats,
33
+ }: PreviewHeroSectionProps) {
34
+ return (
35
+ <PreviewHeroCard
36
+ overlayTopLeft={showProcessing ? <PreviewStatusBadge status={appStatus} /> : null}
37
+ background={<PreviewPlaceholder visible={!imageLoaded} />}
38
+ image={<PreviewImage uri={imageUrl} onLoad={onImageLoad} />}
39
+ overlayBottom={
40
+ <StatsBar
41
+ likeCount={stats.likeCount}
42
+ commentCount={stats.commentCount}
43
+ forkCount={stats.forkCount}
44
+ isLiked={stats.isLiked}
45
+ onPressLike={() => void stats.handleLike()}
46
+ onPressComments={stats.handleOpenComments}
47
+ centered
48
+ fixedWidth={160}
49
+ />
50
+ }
51
+ style={{ marginBottom: 16 }}
52
+ />
53
+ );
54
+ }
55
+
56
+