@comergehq/studio 0.1.28 → 0.1.30
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.d.mts +9 -1
- package/dist/index.d.ts +9 -1
- package/dist/index.js +1144 -755
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +1101 -712
- package/dist/index.mjs.map +1 -1
- package/package.json +2 -2
- package/src/components/studio-sheet/StudioBottomSheet.tsx +6 -1
- package/src/data/apps/remote.ts +7 -0
- package/src/data/apps/repository.ts +7 -0
- package/src/data/apps/types.ts +8 -0
- package/src/studio/ComergeStudio.tsx +112 -0
- package/src/studio/analytics/events.ts +20 -0
- package/src/studio/analytics/track.ts +42 -0
- package/src/studio/hooks/useBundleManager.ts +39 -6
- package/src/studio/hooks/useRelatedApps.ts +60 -0
- package/src/studio/ui/PreviewPanel.tsx +16 -1
- package/src/studio/ui/RuntimeRenderer.tsx +8 -1
- package/src/studio/ui/StudioOverlay.tsx +65 -4
- package/src/studio/ui/preview-panel/PreviewPanelHeader.tsx +212 -37
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import * as React from 'react';
|
|
2
|
-
import { Keyboard, Platform, View, useWindowDimensions } from 'react-native';
|
|
2
|
+
import { InteractionManager, Keyboard, Platform, View, useWindowDimensions } from 'react-native';
|
|
3
3
|
|
|
4
4
|
import type { App } from '../../data/apps/types';
|
|
5
|
+
import type { RelatedApps } from '../../data/apps/types';
|
|
5
6
|
import type { MergeRequest } from '../../data/merge-requests/types';
|
|
6
7
|
import { StudioBottomSheet } from '../../components/studio-sheet/StudioBottomSheet';
|
|
7
8
|
import { StudioSheetPager } from '../../components/studio-sheet/StudioSheetPager';
|
|
@@ -70,6 +71,11 @@ export type StudioOverlayProps = {
|
|
|
70
71
|
onNavigateHome?: () => void;
|
|
71
72
|
showBubble: boolean;
|
|
72
73
|
studioControlOptions?: StudioControlOptions;
|
|
74
|
+
relatedApps?: RelatedApps | null;
|
|
75
|
+
relatedAppsLoading?: boolean;
|
|
76
|
+
switchingRelatedAppId?: string | null;
|
|
77
|
+
onOpenRelatedApps?: () => void;
|
|
78
|
+
onSwitchRelatedApp?: (targetAppId: string) => void;
|
|
73
79
|
};
|
|
74
80
|
|
|
75
81
|
type SheetPage = 'preview' | 'chat';
|
|
@@ -110,12 +116,18 @@ export function StudioOverlay({
|
|
|
110
116
|
onNavigateHome,
|
|
111
117
|
showBubble,
|
|
112
118
|
studioControlOptions,
|
|
119
|
+
relatedApps,
|
|
120
|
+
relatedAppsLoading,
|
|
121
|
+
switchingRelatedAppId,
|
|
122
|
+
onOpenRelatedApps,
|
|
123
|
+
onSwitchRelatedApp,
|
|
113
124
|
}: StudioOverlayProps) {
|
|
114
125
|
const theme = useTheme();
|
|
115
126
|
const { width } = useWindowDimensions();
|
|
116
127
|
|
|
117
128
|
const [sheetOpen, setSheetOpen] = React.useState(false);
|
|
118
129
|
const sheetOpenRef = React.useRef(sheetOpen);
|
|
130
|
+
const pendingNavigateHomeRef = React.useRef(false);
|
|
119
131
|
const [activePage, setActivePage] = React.useState<SheetPage>('preview');
|
|
120
132
|
|
|
121
133
|
const [drawing, setDrawing] = React.useState(false);
|
|
@@ -214,6 +226,50 @@ export function StudioOverlay({
|
|
|
214
226
|
[closeSheet, onTestMr]
|
|
215
227
|
);
|
|
216
228
|
|
|
229
|
+
const handleNavigateHome = React.useCallback(() => {
|
|
230
|
+
if (!onNavigateHome) return;
|
|
231
|
+
|
|
232
|
+
if (Platform.OS !== 'android') {
|
|
233
|
+
onNavigateHome();
|
|
234
|
+
return;
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
// On Android Fabric, navigate only after the sheet fully dismisses.
|
|
238
|
+
if (!sheetOpenRef.current) {
|
|
239
|
+
InteractionManager.runAfterInteractions(() => {
|
|
240
|
+
onNavigateHome();
|
|
241
|
+
});
|
|
242
|
+
return;
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
pendingNavigateHomeRef.current = true;
|
|
246
|
+
Keyboard.dismiss();
|
|
247
|
+
setActivePage('preview');
|
|
248
|
+
closeSheet();
|
|
249
|
+
}, [closeSheet, onNavigateHome]);
|
|
250
|
+
|
|
251
|
+
const handleSheetDismiss = React.useCallback(() => {
|
|
252
|
+
if (Platform.OS !== 'android') return;
|
|
253
|
+
if (!pendingNavigateHomeRef.current) return;
|
|
254
|
+
pendingNavigateHomeRef.current = false;
|
|
255
|
+
InteractionManager.runAfterInteractions(() => {
|
|
256
|
+
onNavigateHome?.();
|
|
257
|
+
});
|
|
258
|
+
}, [onNavigateHome]);
|
|
259
|
+
|
|
260
|
+
React.useEffect(() => {
|
|
261
|
+
if (!sheetOpen) {
|
|
262
|
+
return;
|
|
263
|
+
}
|
|
264
|
+
pendingNavigateHomeRef.current = false;
|
|
265
|
+
}, [sheetOpen]);
|
|
266
|
+
|
|
267
|
+
React.useEffect(() => {
|
|
268
|
+
return () => {
|
|
269
|
+
pendingNavigateHomeRef.current = false;
|
|
270
|
+
};
|
|
271
|
+
}, []);
|
|
272
|
+
|
|
217
273
|
React.useEffect(() => {
|
|
218
274
|
sheetOpenRef.current = sheetOpen;
|
|
219
275
|
}, [sheetOpen]);
|
|
@@ -236,7 +292,7 @@ export function StudioOverlay({
|
|
|
236
292
|
{/* Testing glow around runtime */}
|
|
237
293
|
<EdgeGlowFrame visible={isTesting} role="accent" thickness={40} intensity={1} />
|
|
238
294
|
|
|
239
|
-
<StudioBottomSheet open={sheetOpen} onOpenChange={handleSheetOpenChange}>
|
|
295
|
+
<StudioBottomSheet open={sheetOpen} onOpenChange={handleSheetOpenChange} onDismiss={handleSheetDismiss}>
|
|
240
296
|
<StudioSheetPager
|
|
241
297
|
activePage={activePage}
|
|
242
298
|
width={width}
|
|
@@ -254,7 +310,7 @@ export function StudioOverlay({
|
|
|
254
310
|
testingMrId={testingMrId}
|
|
255
311
|
toMergeRequestSummary={toMergeRequestSummary}
|
|
256
312
|
onClose={closeSheet}
|
|
257
|
-
onNavigateHome={onNavigateHome}
|
|
313
|
+
onNavigateHome={onNavigateHome ? handleNavigateHome : undefined}
|
|
258
314
|
onGoToChat={goToChat}
|
|
259
315
|
onStartDraw={isOwner ? startDraw : undefined}
|
|
260
316
|
onSubmitMergeRequest={onSubmitMergeRequest}
|
|
@@ -266,6 +322,11 @@ export function StudioOverlay({
|
|
|
266
322
|
onTestMr={handleTestMr}
|
|
267
323
|
onOpenComments={() => setCommentsAppId(app?.id ?? null)}
|
|
268
324
|
commentCountOverride={commentsCount ?? undefined}
|
|
325
|
+
relatedApps={relatedApps}
|
|
326
|
+
relatedAppsLoading={relatedAppsLoading}
|
|
327
|
+
switchingRelatedAppId={switchingRelatedAppId}
|
|
328
|
+
onOpenRelatedApps={onOpenRelatedApps}
|
|
329
|
+
onSwitchRelatedApp={onSwitchRelatedApp}
|
|
269
330
|
/>
|
|
270
331
|
}
|
|
271
332
|
chat={
|
|
@@ -282,7 +343,7 @@ export function StudioOverlay({
|
|
|
282
343
|
onClearAttachments={() => setChatAttachments([])}
|
|
283
344
|
onBack={backToPreview}
|
|
284
345
|
onClose={closeSheet}
|
|
285
|
-
onNavigateHome={onNavigateHome}
|
|
346
|
+
onNavigateHome={onNavigateHome ? handleNavigateHome : undefined}
|
|
286
347
|
onStartDraw={startDraw}
|
|
287
348
|
onSend={optimistic.onSend}
|
|
288
349
|
onRetryMessage={optimistic.onRetry}
|
|
@@ -1,9 +1,14 @@
|
|
|
1
1
|
import * as React from 'react';
|
|
2
|
-
import { View } from 'react-native';
|
|
2
|
+
import { ActivityIndicator, Pressable, View } from 'react-native';
|
|
3
3
|
|
|
4
|
+
import type { RelatedApp, RelatedApps } from '../../../data/apps/types';
|
|
4
5
|
import { StudioSheetHeader } from '../../../components/studio-sheet/StudioSheetHeader';
|
|
5
6
|
import { StudioSheetHeaderIconButton } from '../../../components/studio-sheet/StudioSheetHeaderIconButton';
|
|
6
|
-
import { IconChat, IconClose, IconHome, IconShare } from '../../../components/icons/StudioIcons';
|
|
7
|
+
import { IconChat, IconChevronDown, IconClose, IconHome, IconShare } from '../../../components/icons/StudioIcons';
|
|
8
|
+
import { Modal } from '../../../components/primitives/Modal';
|
|
9
|
+
import { Text } from '../../../components/primitives/Text';
|
|
10
|
+
import { PreviewStatusBadge } from '../../../components/preview/PreviewStatusBadge';
|
|
11
|
+
import { useTheme } from '../../../theme';
|
|
7
12
|
|
|
8
13
|
export type PreviewPanelHeaderProps = {
|
|
9
14
|
isOwner: boolean;
|
|
@@ -12,6 +17,16 @@ export type PreviewPanelHeaderProps = {
|
|
|
12
17
|
onNavigateHome?: () => void;
|
|
13
18
|
onGoToChat: () => void;
|
|
14
19
|
onShare?: () => void;
|
|
20
|
+
relatedApps?: RelatedApps | null;
|
|
21
|
+
relatedAppsLoading?: boolean;
|
|
22
|
+
switchingRelatedAppId?: string | null;
|
|
23
|
+
onOpenRelatedApps?: () => void;
|
|
24
|
+
onSwitchRelatedApp?: (targetAppId: string) => void;
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
type RelatedAppListItem = {
|
|
28
|
+
app: RelatedApp;
|
|
29
|
+
section: 'original' | 'remix';
|
|
15
30
|
};
|
|
16
31
|
|
|
17
32
|
export function PreviewPanelHeader({
|
|
@@ -21,47 +36,207 @@ export function PreviewPanelHeader({
|
|
|
21
36
|
onNavigateHome,
|
|
22
37
|
onGoToChat,
|
|
23
38
|
onShare,
|
|
39
|
+
relatedApps,
|
|
40
|
+
relatedAppsLoading,
|
|
41
|
+
switchingRelatedAppId,
|
|
42
|
+
onOpenRelatedApps,
|
|
43
|
+
onSwitchRelatedApp,
|
|
24
44
|
}: PreviewPanelHeaderProps) {
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
45
|
+
const theme = useTheme();
|
|
46
|
+
const [relatedAppsOpen, setRelatedAppsOpen] = React.useState(false);
|
|
47
|
+
|
|
48
|
+
const relatedAppItems = React.useMemo((): RelatedAppListItem[] => {
|
|
49
|
+
if (!relatedApps) return [];
|
|
50
|
+
const items: RelatedAppListItem[] = [];
|
|
51
|
+
if (relatedApps.original) {
|
|
52
|
+
items.push({ app: relatedApps.original, section: 'original' });
|
|
53
|
+
}
|
|
54
|
+
for (const remix of relatedApps.remixes) {
|
|
55
|
+
items.push({ app: remix, section: 'remix' });
|
|
56
|
+
}
|
|
57
|
+
return items;
|
|
58
|
+
}, [relatedApps]);
|
|
59
|
+
|
|
60
|
+
const uniqueRelatedCount = React.useMemo(() => {
|
|
61
|
+
return new Set(relatedAppItems.map((item) => item.app.id)).size;
|
|
62
|
+
}, [relatedAppItems]);
|
|
63
|
+
|
|
64
|
+
const shouldShowRelatedApps = uniqueRelatedCount >= 2;
|
|
65
|
+
|
|
66
|
+
const currentAppId = relatedApps?.current.id;
|
|
67
|
+
const originalAppId = relatedApps?.original?.id ?? null;
|
|
68
|
+
|
|
69
|
+
const sectionedRelatedApps = React.useMemo(() => {
|
|
70
|
+
const original: RelatedAppListItem[] = [];
|
|
71
|
+
const remixes: RelatedAppListItem[] = [];
|
|
72
|
+
const seenIds = new Set<string>();
|
|
73
|
+
for (const item of relatedAppItems) {
|
|
74
|
+
if (seenIds.has(item.app.id)) continue;
|
|
75
|
+
seenIds.add(item.app.id);
|
|
76
|
+
if (item.section === 'original') {
|
|
77
|
+
original.push(item);
|
|
78
|
+
} else {
|
|
79
|
+
remixes.push(item);
|
|
33
80
|
}
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
81
|
+
}
|
|
82
|
+
return { original, remixes };
|
|
83
|
+
}, [relatedAppItems]);
|
|
84
|
+
|
|
85
|
+
const openRelatedApps = React.useCallback(() => {
|
|
86
|
+
setRelatedAppsOpen(true);
|
|
87
|
+
onOpenRelatedApps?.();
|
|
88
|
+
}, [onOpenRelatedApps]);
|
|
89
|
+
|
|
90
|
+
const closeRelatedApps = React.useCallback(() => {
|
|
91
|
+
setRelatedAppsOpen(false);
|
|
92
|
+
}, []);
|
|
93
|
+
|
|
94
|
+
const handleSelectRelatedApp = React.useCallback(
|
|
95
|
+
(targetAppId: string) => {
|
|
96
|
+
if (!relatedApps) return;
|
|
97
|
+
if (targetAppId === relatedApps.current.id) return;
|
|
98
|
+
onSwitchRelatedApp?.(targetAppId);
|
|
99
|
+
setRelatedAppsOpen(false);
|
|
100
|
+
},
|
|
101
|
+
[onSwitchRelatedApp, relatedApps]
|
|
102
|
+
);
|
|
103
|
+
|
|
104
|
+
const renderRelatedRow = React.useCallback(
|
|
105
|
+
(item: RelatedAppListItem) => {
|
|
106
|
+
const app = item.app;
|
|
107
|
+
const isCurrent = app.id === currentAppId;
|
|
108
|
+
const isOriginal = app.id === originalAppId;
|
|
109
|
+
const isReady = app.status === 'ready';
|
|
110
|
+
const isSwitching = switchingRelatedAppId === app.id;
|
|
111
|
+
const disabled = isCurrent || !isReady || Boolean(switchingRelatedAppId);
|
|
112
|
+
|
|
113
|
+
return (
|
|
114
|
+
<Pressable
|
|
115
|
+
key={app.id}
|
|
116
|
+
accessibilityRole="button"
|
|
117
|
+
accessibilityLabel={`Switch to ${app.name}`}
|
|
118
|
+
disabled={disabled}
|
|
119
|
+
onPress={() => handleSelectRelatedApp(app.id)}
|
|
120
|
+
style={{
|
|
121
|
+
borderRadius: theme.radii.md,
|
|
122
|
+
borderWidth: 1,
|
|
123
|
+
borderColor: theme.colors.border,
|
|
124
|
+
backgroundColor: theme.colors.surface,
|
|
125
|
+
paddingHorizontal: theme.spacing.md,
|
|
126
|
+
paddingVertical: theme.spacing.sm,
|
|
127
|
+
marginBottom: theme.spacing.sm,
|
|
128
|
+
opacity: disabled ? 0.6 : 1,
|
|
129
|
+
}}
|
|
130
|
+
>
|
|
131
|
+
<View style={{ flexDirection: 'row', alignItems: 'center', justifyContent: 'space-between', gap: 8 }}>
|
|
132
|
+
<View style={{ flex: 1, paddingRight: 8 }}>
|
|
133
|
+
<Text numberOfLines={1} style={{ color: theme.colors.text, fontWeight: theme.typography.fontWeight.semibold }}>
|
|
134
|
+
{app.name}
|
|
135
|
+
</Text>
|
|
136
|
+
<View style={{ height: 4 }} />
|
|
137
|
+
<View style={{ flexDirection: 'row', alignItems: 'center', flexWrap: 'wrap', gap: 6 }}>
|
|
138
|
+
{isOriginal ? (
|
|
139
|
+
<View style={{ borderRadius: 999, paddingHorizontal: 8, paddingVertical: 2, backgroundColor: theme.colors.neutral }}>
|
|
140
|
+
<Text style={{ color: theme.colors.textMuted, fontSize: 11 }}>Original</Text>
|
|
141
|
+
</View>
|
|
142
|
+
) : null}
|
|
143
|
+
{isCurrent ? (
|
|
144
|
+
<View style={{ borderRadius: 999, paddingHorizontal: 8, paddingVertical: 2, backgroundColor: theme.colors.primary }}>
|
|
145
|
+
<Text style={{ color: theme.colors.onPrimary, fontSize: 11 }}>Current</Text>
|
|
146
|
+
</View>
|
|
147
|
+
) : null}
|
|
148
|
+
</View>
|
|
149
|
+
</View>
|
|
150
|
+
<View style={{ alignItems: 'flex-end', gap: 8 }}>
|
|
151
|
+
{app.status ? <PreviewStatusBadge status={app.status} /> : null}
|
|
152
|
+
{isSwitching ? <ActivityIndicator size="small" color={theme.colors.primary} /> : null}
|
|
153
|
+
</View>
|
|
154
|
+
</View>
|
|
155
|
+
</Pressable>
|
|
156
|
+
);
|
|
157
|
+
},
|
|
158
|
+
[currentAppId, handleSelectRelatedApp, originalAppId, switchingRelatedAppId, theme]
|
|
159
|
+
);
|
|
160
|
+
|
|
161
|
+
return (
|
|
162
|
+
<>
|
|
163
|
+
<StudioSheetHeader
|
|
164
|
+
left={
|
|
165
|
+
onNavigateHome ? (
|
|
166
|
+
<StudioSheetHeaderIconButton onPress={onNavigateHome} accessibilityLabel="Home" appearance="glass" intent="primary">
|
|
167
|
+
<IconHome size={20} colorToken="onPrimary" />
|
|
46
168
|
</StudioSheetHeaderIconButton>
|
|
47
|
-
) : null
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
169
|
+
) : null
|
|
170
|
+
}
|
|
171
|
+
center={null}
|
|
172
|
+
right={
|
|
173
|
+
<View style={{ flexDirection: 'row', alignItems: 'center' }}>
|
|
174
|
+
{isOwner ? (
|
|
175
|
+
<StudioSheetHeaderIconButton
|
|
176
|
+
onPress={onGoToChat}
|
|
177
|
+
accessibilityLabel="Chat"
|
|
178
|
+
intent="primary"
|
|
179
|
+
appearance="glass"
|
|
180
|
+
style={{ marginRight: 8 }}
|
|
181
|
+
>
|
|
182
|
+
<IconChat size={20} colorToken="onPrimary" />
|
|
183
|
+
</StudioSheetHeaderIconButton>
|
|
184
|
+
) : null}
|
|
185
|
+
{isPublic && onShare ? (
|
|
186
|
+
<StudioSheetHeaderIconButton
|
|
187
|
+
onPress={onShare}
|
|
188
|
+
accessibilityLabel="Share"
|
|
189
|
+
intent="primary"
|
|
190
|
+
appearance="glass"
|
|
191
|
+
style={{ marginRight: 8 }}
|
|
192
|
+
>
|
|
193
|
+
<IconShare size={20} colorToken="onPrimary" />
|
|
194
|
+
</StudioSheetHeaderIconButton>
|
|
195
|
+
) : null}
|
|
196
|
+
{shouldShowRelatedApps ? (
|
|
197
|
+
<StudioSheetHeaderIconButton
|
|
198
|
+
onPress={openRelatedApps}
|
|
199
|
+
accessibilityLabel="Related apps"
|
|
200
|
+
intent="primary"
|
|
201
|
+
appearance="glass"
|
|
202
|
+
style={{ marginRight: 8 }}
|
|
203
|
+
>
|
|
204
|
+
{relatedAppsLoading ? (
|
|
205
|
+
<ActivityIndicator size="small" color={theme.colors.onPrimary} />
|
|
206
|
+
) : (
|
|
207
|
+
<IconChevronDown size={20} colorToken="onPrimary" />
|
|
208
|
+
)}
|
|
209
|
+
</StudioSheetHeaderIconButton>
|
|
210
|
+
) : null}
|
|
211
|
+
<StudioSheetHeaderIconButton onPress={onClose} accessibilityLabel="Close" appearance="glass" intent="primary">
|
|
212
|
+
<IconClose size={20} colorToken="onPrimary" />
|
|
57
213
|
</StudioSheetHeaderIconButton>
|
|
214
|
+
</View>
|
|
215
|
+
}
|
|
216
|
+
/>
|
|
217
|
+
|
|
218
|
+
<Modal visible={relatedAppsOpen} onRequestClose={closeRelatedApps}>
|
|
219
|
+
<View style={{ gap: theme.spacing.sm }}>
|
|
220
|
+
<Text style={{ color: theme.colors.text, fontSize: 18, fontWeight: theme.typography.fontWeight.semibold }}>
|
|
221
|
+
Related apps
|
|
222
|
+
</Text>
|
|
223
|
+
|
|
224
|
+
{sectionedRelatedApps.original.length > 0 ? (
|
|
225
|
+
<View>
|
|
226
|
+
<Text style={{ color: theme.colors.textMuted, marginBottom: theme.spacing.xs }}>Original</Text>
|
|
227
|
+
{sectionedRelatedApps.original.map(renderRelatedRow)}
|
|
228
|
+
</View>
|
|
229
|
+
) : null}
|
|
230
|
+
|
|
231
|
+
{sectionedRelatedApps.remixes.length > 0 ? (
|
|
232
|
+
<View>
|
|
233
|
+
<Text style={{ color: theme.colors.textMuted, marginBottom: theme.spacing.xs }}>Remixes</Text>
|
|
234
|
+
{sectionedRelatedApps.remixes.map(renderRelatedRow)}
|
|
235
|
+
</View>
|
|
58
236
|
) : null}
|
|
59
|
-
<StudioSheetHeaderIconButton onPress={onClose} accessibilityLabel="Close" appearance="glass" intent="primary">
|
|
60
|
-
<IconClose size={20} colorToken="onPrimary" />
|
|
61
|
-
</StudioSheetHeaderIconButton>
|
|
62
237
|
</View>
|
|
63
|
-
|
|
64
|
-
|
|
238
|
+
</Modal>
|
|
239
|
+
</>
|
|
65
240
|
);
|
|
66
241
|
}
|
|
67
242
|
|