@comergehq/studio 0.1.2 → 0.1.4
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 +2 -10
- package/dist/index.d.ts +2 -10
- package/dist/index.js +293 -264
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +251 -222
- 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 +33 -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 +239 -0
- package/src/studio/bootstrap/StudioBootstrap.tsx +45 -0
- package/src/studio/bootstrap/useStudioBootstrap.ts +55 -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,132 @@
|
|
|
1
|
+
import * as React from 'react';
|
|
2
|
+
import { Animated, FlatList, View, useWindowDimensions, type ViewStyle } from 'react-native';
|
|
3
|
+
|
|
4
|
+
import type { MergeRequest } from '../../data/merge-requests/types';
|
|
5
|
+
import type { UserStats } from '../../data/users/types';
|
|
6
|
+
import { useTheme } from '../../theme';
|
|
7
|
+
import { ReviewMergeRequestCard } from './ReviewMergeRequestCard';
|
|
8
|
+
|
|
9
|
+
export type ReviewMergeRequestCarouselProps = {
|
|
10
|
+
mergeRequests: MergeRequest[];
|
|
11
|
+
creatorStatsById: Record<string, UserStats>;
|
|
12
|
+
processingMrId?: string | null;
|
|
13
|
+
isBuilding?: boolean;
|
|
14
|
+
testingMrId?: string | null;
|
|
15
|
+
onReject: (mr: MergeRequest) => void | Promise<void>;
|
|
16
|
+
onApprove: (mr: MergeRequest) => void;
|
|
17
|
+
onTest: (mr: MergeRequest) => void | Promise<void>;
|
|
18
|
+
style?: ViewStyle;
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
type CardRenderItem = { mr: MergeRequest; index: number; total: number };
|
|
22
|
+
|
|
23
|
+
export function ReviewMergeRequestCarousel({
|
|
24
|
+
mergeRequests,
|
|
25
|
+
creatorStatsById,
|
|
26
|
+
processingMrId,
|
|
27
|
+
isBuilding,
|
|
28
|
+
testingMrId,
|
|
29
|
+
onReject,
|
|
30
|
+
onApprove,
|
|
31
|
+
onTest,
|
|
32
|
+
style,
|
|
33
|
+
}: ReviewMergeRequestCarouselProps) {
|
|
34
|
+
const theme = useTheme();
|
|
35
|
+
const { width } = useWindowDimensions();
|
|
36
|
+
const [expanded, setExpanded] = React.useState<Record<string, boolean>>({});
|
|
37
|
+
const carouselScrollX = React.useRef(new Animated.Value(0)).current;
|
|
38
|
+
|
|
39
|
+
const peekAmount = 24;
|
|
40
|
+
const gap = 16;
|
|
41
|
+
const cardWidth = React.useMemo(() => Math.max(1, width - theme.spacing.lg * 2 - peekAmount), [peekAmount, theme.spacing.lg, width]);
|
|
42
|
+
const snapInterval = cardWidth + gap;
|
|
43
|
+
const dotColor = theme.scheme === 'dark' ? '#FFFFFF' : '#000000';
|
|
44
|
+
|
|
45
|
+
if (mergeRequests.length === 0) return null;
|
|
46
|
+
|
|
47
|
+
return (
|
|
48
|
+
<View style={[{ marginHorizontal: -theme.spacing.lg }, style]}>
|
|
49
|
+
<FlatList
|
|
50
|
+
horizontal
|
|
51
|
+
data={mergeRequests}
|
|
52
|
+
keyExtractor={(mr) => mr.id}
|
|
53
|
+
showsHorizontalScrollIndicator={false}
|
|
54
|
+
contentContainerStyle={{ paddingHorizontal: theme.spacing.lg, paddingVertical: theme.spacing.sm }}
|
|
55
|
+
ItemSeparatorComponent={() => <View style={{ width: gap }} />}
|
|
56
|
+
snapToAlignment="start"
|
|
57
|
+
decelerationRate="fast"
|
|
58
|
+
snapToInterval={snapInterval}
|
|
59
|
+
disableIntervalMomentum
|
|
60
|
+
style={{ paddingRight: peekAmount }}
|
|
61
|
+
ListFooterComponent={<View style={{ width: peekAmount }} />}
|
|
62
|
+
onScroll={Animated.event([{ nativeEvent: { contentOffset: { x: carouselScrollX } } }], {
|
|
63
|
+
useNativeDriver: false,
|
|
64
|
+
})}
|
|
65
|
+
scrollEventThrottle={16}
|
|
66
|
+
renderItem={({ item, index }) => {
|
|
67
|
+
const total = mergeRequests.length;
|
|
68
|
+
const creator = creatorStatsById[item.createdBy];
|
|
69
|
+
const isExpanded = Boolean(expanded[item.id]);
|
|
70
|
+
const isProcessing = Boolean(processingMrId && processingMrId === item.id);
|
|
71
|
+
const isAnyProcessing = Boolean(processingMrId);
|
|
72
|
+
const isTestingThis = Boolean(testingMrId && testingMrId === item.id);
|
|
73
|
+
return (
|
|
74
|
+
<View style={{ width: cardWidth }}>
|
|
75
|
+
<ReviewMergeRequestCard
|
|
76
|
+
mr={item}
|
|
77
|
+
index={index}
|
|
78
|
+
total={total}
|
|
79
|
+
creator={creator}
|
|
80
|
+
isExpanded={isExpanded}
|
|
81
|
+
isProcessing={isProcessing}
|
|
82
|
+
isAnyProcessing={isAnyProcessing}
|
|
83
|
+
isBuilding={Boolean(isBuilding)}
|
|
84
|
+
isTestingThis={isTestingThis}
|
|
85
|
+
onToggle={() => setExpanded((prev) => ({ ...prev, [item.id]: !prev[item.id] }))}
|
|
86
|
+
onReject={() => void onReject(item)}
|
|
87
|
+
onApprove={() => onApprove(item)}
|
|
88
|
+
onTest={() => void onTest(item)}
|
|
89
|
+
/>
|
|
90
|
+
</View>
|
|
91
|
+
);
|
|
92
|
+
}}
|
|
93
|
+
/>
|
|
94
|
+
|
|
95
|
+
{mergeRequests.length >= 1 ? (
|
|
96
|
+
<View style={{ flexDirection: 'row', justifyContent: 'center', columnGap: 8, marginTop: theme.spacing.md }}>
|
|
97
|
+
{mergeRequests.map((mr, index) => {
|
|
98
|
+
const inputRange = [(index - 1) * snapInterval, index * snapInterval, (index + 1) * snapInterval];
|
|
99
|
+
|
|
100
|
+
const scale = carouselScrollX.interpolate({
|
|
101
|
+
inputRange,
|
|
102
|
+
outputRange: [0.8, 1.2, 0.8],
|
|
103
|
+
extrapolate: 'clamp',
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
const opacity = carouselScrollX.interpolate({
|
|
107
|
+
inputRange,
|
|
108
|
+
outputRange: [0.4, 1, 0.4],
|
|
109
|
+
extrapolate: 'clamp',
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
return (
|
|
113
|
+
<Animated.View
|
|
114
|
+
key={mr.id}
|
|
115
|
+
style={{
|
|
116
|
+
width: 8,
|
|
117
|
+
height: 8,
|
|
118
|
+
borderRadius: 999,
|
|
119
|
+
backgroundColor: dotColor,
|
|
120
|
+
transform: [{ scale }],
|
|
121
|
+
opacity,
|
|
122
|
+
}}
|
|
123
|
+
/>
|
|
124
|
+
);
|
|
125
|
+
})}
|
|
126
|
+
</View>
|
|
127
|
+
) : null}
|
|
128
|
+
</View>
|
|
129
|
+
);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
export { MergeRequestStatusCard } from './MergeRequestStatusCard';
|
|
2
|
+
export type { MergeRequestStatusCardProps } from './MergeRequestStatusCard';
|
|
3
|
+
|
|
4
|
+
export { ReviewMergeRequestCarousel } from './ReviewMergeRequestCarousel';
|
|
5
|
+
export type { ReviewMergeRequestCarouselProps } from './ReviewMergeRequestCarousel';
|
|
6
|
+
|
|
7
|
+
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
export type MergeRequestStatusDisplay = {
|
|
2
|
+
text: string;
|
|
3
|
+
color: string;
|
|
4
|
+
};
|
|
5
|
+
|
|
6
|
+
export function getMergeRequestStatusDisplay(status: string): MergeRequestStatusDisplay {
|
|
7
|
+
switch (status) {
|
|
8
|
+
case 'open':
|
|
9
|
+
return { text: 'Merge request is pending approval', color: '#FACC15' };
|
|
10
|
+
case 'approved':
|
|
11
|
+
return { text: 'Merge approved', color: '#10B981' };
|
|
12
|
+
case 'rejected':
|
|
13
|
+
return { text: 'Merge request rejected', color: '#F43F5E' };
|
|
14
|
+
case 'merged':
|
|
15
|
+
return { text: 'Your edit was merged to the original app', color: '#10B981' };
|
|
16
|
+
case 'closed':
|
|
17
|
+
return { text: 'Merge closed', color: '#10B981' };
|
|
18
|
+
default:
|
|
19
|
+
return { text: status, color: '#898994' };
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
export function toIsoString(input: unknown): string | null {
|
|
2
|
+
if (!input) return null;
|
|
3
|
+
if (typeof input === 'string') return input;
|
|
4
|
+
if (typeof input === 'number') return new Date(input).toISOString();
|
|
5
|
+
if (input instanceof Date) return input.toISOString();
|
|
6
|
+
return null;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import * as React from 'react';
|
|
2
|
+
|
|
3
|
+
export function useControlledExpansion(props: { expanded?: boolean; onExpandedChange?: (expanded: boolean) => void }) {
|
|
4
|
+
const [uncontrolled, setUncontrolled] = React.useState(false);
|
|
5
|
+
const expanded = props.expanded ?? uncontrolled;
|
|
6
|
+
const setExpanded = React.useCallback(
|
|
7
|
+
(next: boolean) => {
|
|
8
|
+
props.onExpandedChange?.(next);
|
|
9
|
+
if (props.expanded === undefined) setUncontrolled(next);
|
|
10
|
+
},
|
|
11
|
+
[props]
|
|
12
|
+
);
|
|
13
|
+
return { expanded, setExpanded };
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
export type UserSummary = {
|
|
2
|
+
id: string;
|
|
3
|
+
name?: string | null;
|
|
4
|
+
avatarUri?: string | null;
|
|
5
|
+
};
|
|
6
|
+
|
|
7
|
+
export type MergeRequestStatus = 'open' | 'approved' | 'rejected' | 'merged';
|
|
8
|
+
|
|
9
|
+
export type MergeRequestSummary = {
|
|
10
|
+
id: string;
|
|
11
|
+
title?: string | null;
|
|
12
|
+
description?: string | null;
|
|
13
|
+
status: MergeRequestStatus;
|
|
14
|
+
creator?: UserSummary | null;
|
|
15
|
+
createdAt?: string | number | Date | null;
|
|
16
|
+
updatedAt?: string | number | Date | null;
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
export type ChatAuthor = 'human' | 'assistant';
|
|
20
|
+
|
|
21
|
+
export type ChatMessageMetaStatus = 'success' | 'error' | 'info' | 'warning';
|
|
22
|
+
|
|
23
|
+
export type ChatMessageMeta = {
|
|
24
|
+
kind?: string;
|
|
25
|
+
event?: string;
|
|
26
|
+
status?: ChatMessageMetaStatus;
|
|
27
|
+
mergeRequestId?: string;
|
|
28
|
+
sourceAppId?: string;
|
|
29
|
+
targetAppId?: string;
|
|
30
|
+
appId?: string;
|
|
31
|
+
threadId?: string;
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
export type ChatMessage = {
|
|
35
|
+
id: string;
|
|
36
|
+
author: ChatAuthor;
|
|
37
|
+
content: string;
|
|
38
|
+
createdAt?: string | number | Date | null;
|
|
39
|
+
kind?: string | null;
|
|
40
|
+
meta?: ChatMessageMeta | null;
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
import * as React from 'react';
|
|
2
|
+
import { Animated, View, type ViewStyle } from 'react-native';
|
|
3
|
+
import { LinearGradient } from 'expo-linear-gradient';
|
|
4
|
+
|
|
5
|
+
import { useTheme } from '../../theme';
|
|
6
|
+
import { withAlpha } from '../utils/color';
|
|
7
|
+
|
|
8
|
+
export type EdgeGlowFrameProps = {
|
|
9
|
+
visible: boolean;
|
|
10
|
+
/**
|
|
11
|
+
* Which semantic color to use for the glow.
|
|
12
|
+
*/
|
|
13
|
+
role?: 'accent' | 'danger' | 'success' | 'warning';
|
|
14
|
+
/**
|
|
15
|
+
* Thickness of each edge glow in dp.
|
|
16
|
+
*/
|
|
17
|
+
thickness?: number;
|
|
18
|
+
/**
|
|
19
|
+
* Optional intensity multiplier for alpha (0..1).
|
|
20
|
+
*/
|
|
21
|
+
intensity?: number;
|
|
22
|
+
style?: ViewStyle;
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
function baseColor(role: NonNullable<EdgeGlowFrameProps['role']>, theme: ReturnType<typeof useTheme>) {
|
|
26
|
+
switch (role) {
|
|
27
|
+
case 'danger':
|
|
28
|
+
return theme.colors.danger;
|
|
29
|
+
case 'success':
|
|
30
|
+
return theme.colors.success;
|
|
31
|
+
case 'warning':
|
|
32
|
+
return theme.colors.warning;
|
|
33
|
+
case 'accent':
|
|
34
|
+
default:
|
|
35
|
+
return '#A855F7';
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export function EdgeGlowFrame({
|
|
40
|
+
visible,
|
|
41
|
+
role = 'accent',
|
|
42
|
+
thickness = 40,
|
|
43
|
+
intensity = 1,
|
|
44
|
+
style,
|
|
45
|
+
}: EdgeGlowFrameProps) {
|
|
46
|
+
const theme = useTheme();
|
|
47
|
+
const alpha = Math.max(0, Math.min(1, intensity));
|
|
48
|
+
|
|
49
|
+
const anim = React.useRef(new Animated.Value(visible ? 1 : 0)).current;
|
|
50
|
+
|
|
51
|
+
React.useEffect(() => {
|
|
52
|
+
Animated.timing(anim, {
|
|
53
|
+
toValue: visible ? 1 : 0,
|
|
54
|
+
duration: 300,
|
|
55
|
+
useNativeDriver: true,
|
|
56
|
+
}).start();
|
|
57
|
+
}, [anim, visible]);
|
|
58
|
+
|
|
59
|
+
const c = baseColor(role, theme);
|
|
60
|
+
const strong = withAlpha(c, 0.6 * alpha);
|
|
61
|
+
const soft = withAlpha(c, 0.22 * alpha);
|
|
62
|
+
|
|
63
|
+
return (
|
|
64
|
+
<Animated.View pointerEvents="none" style={[{ position: 'absolute', inset: 0, opacity: anim }, style]}>
|
|
65
|
+
{/* Top */}
|
|
66
|
+
<View style={{ position: 'absolute', top: 0, left: 0, right: 0, height: thickness }}>
|
|
67
|
+
<LinearGradient
|
|
68
|
+
colors={[strong, soft, 'transparent']}
|
|
69
|
+
start={{ x: 0, y: 0 }}
|
|
70
|
+
end={{ x: 0, y: 1 }}
|
|
71
|
+
style={{ width: '100%', height: '100%' }}
|
|
72
|
+
/>
|
|
73
|
+
</View>
|
|
74
|
+
{/* Bottom */}
|
|
75
|
+
<View style={{ position: 'absolute', bottom: 0, left: 0, right: 0, height: thickness }}>
|
|
76
|
+
<LinearGradient
|
|
77
|
+
colors={['transparent', soft, strong]}
|
|
78
|
+
start={{ x: 0, y: 0 }}
|
|
79
|
+
end={{ x: 0, y: 1 }}
|
|
80
|
+
style={{ width: '100%', height: '100%' }}
|
|
81
|
+
/>
|
|
82
|
+
</View>
|
|
83
|
+
{/* Left */}
|
|
84
|
+
<View style={{ position: 'absolute', top: 0, bottom: 0, left: 0, width: thickness }}>
|
|
85
|
+
<LinearGradient
|
|
86
|
+
colors={[strong, soft, 'transparent']}
|
|
87
|
+
start={{ x: 0, y: 0 }}
|
|
88
|
+
end={{ x: 1, y: 0 }}
|
|
89
|
+
style={{ width: '100%', height: '100%' }}
|
|
90
|
+
/>
|
|
91
|
+
</View>
|
|
92
|
+
{/* Right */}
|
|
93
|
+
<View style={{ position: 'absolute', top: 0, bottom: 0, right: 0, width: thickness }}>
|
|
94
|
+
<LinearGradient
|
|
95
|
+
colors={['transparent', soft, strong]}
|
|
96
|
+
start={{ x: 0, y: 0 }}
|
|
97
|
+
end={{ x: 1, y: 0 }}
|
|
98
|
+
style={{ width: '100%', height: '100%' }}
|
|
99
|
+
/>
|
|
100
|
+
</View>
|
|
101
|
+
</Animated.View>
|
|
102
|
+
);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import * as React from 'react';
|
|
2
|
+
import { View, type ViewStyle } from 'react-native';
|
|
3
|
+
|
|
4
|
+
import { useTheme } from '../../theme';
|
|
5
|
+
import { Card } from '../primitives/Card';
|
|
6
|
+
|
|
7
|
+
export type PreviewHeroCardProps = {
|
|
8
|
+
aspectRatio?: number;
|
|
9
|
+
overlayTopLeft?: React.ReactNode;
|
|
10
|
+
background?: React.ReactNode;
|
|
11
|
+
image?: React.ReactNode;
|
|
12
|
+
overlayBottom?: React.ReactNode;
|
|
13
|
+
style?: ViewStyle;
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
export function PreviewHeroCard({
|
|
17
|
+
aspectRatio = 4 / 3,
|
|
18
|
+
overlayTopLeft,
|
|
19
|
+
background,
|
|
20
|
+
image,
|
|
21
|
+
overlayBottom,
|
|
22
|
+
style,
|
|
23
|
+
}: PreviewHeroCardProps) {
|
|
24
|
+
const theme = useTheme();
|
|
25
|
+
const radius = 16;
|
|
26
|
+
|
|
27
|
+
return (
|
|
28
|
+
<Card
|
|
29
|
+
variant="surfaceRaised"
|
|
30
|
+
padded={false}
|
|
31
|
+
border={false}
|
|
32
|
+
style={[
|
|
33
|
+
{
|
|
34
|
+
width: '100%',
|
|
35
|
+
aspectRatio,
|
|
36
|
+
borderRadius: radius,
|
|
37
|
+
overflow: 'hidden',
|
|
38
|
+
},
|
|
39
|
+
style,
|
|
40
|
+
]}
|
|
41
|
+
>
|
|
42
|
+
<View style={{ flex: 1 }}>
|
|
43
|
+
{background ? <View style={{ position: 'absolute', inset: 0 }}>{background}</View> : null}
|
|
44
|
+
{image ? <View style={{ position: 'absolute', inset: 0 }}>{image}</View> : null}
|
|
45
|
+
|
|
46
|
+
{overlayTopLeft ? (
|
|
47
|
+
<View style={{ position: 'absolute', top: theme.spacing.sm, left: theme.spacing.sm, zIndex: 2 }}>
|
|
48
|
+
{overlayTopLeft}
|
|
49
|
+
</View>
|
|
50
|
+
) : null}
|
|
51
|
+
|
|
52
|
+
{overlayBottom ? <View style={{ flex: 1, justifyContent: 'flex-end' }}>{overlayBottom}</View> : null}
|
|
53
|
+
</View>
|
|
54
|
+
</Card>
|
|
55
|
+
);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import * as React from 'react';
|
|
2
|
+
import { Image, type ImageStyle } from 'react-native';
|
|
3
|
+
|
|
4
|
+
export type PreviewImageProps = {
|
|
5
|
+
uri?: string | null;
|
|
6
|
+
onLoad?: () => void;
|
|
7
|
+
style?: ImageStyle;
|
|
8
|
+
};
|
|
9
|
+
|
|
10
|
+
export function PreviewImage({ uri, onLoad, style }: PreviewImageProps) {
|
|
11
|
+
if (!uri) return null;
|
|
12
|
+
return (
|
|
13
|
+
<Image
|
|
14
|
+
source={{ uri }}
|
|
15
|
+
resizeMode="cover"
|
|
16
|
+
onLoad={onLoad}
|
|
17
|
+
style={[{ width: '100%', height: '100%' }, style]}
|
|
18
|
+
/>
|
|
19
|
+
);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import * as React from 'react';
|
|
2
|
+
import { View, type ViewStyle } from 'react-native';
|
|
3
|
+
|
|
4
|
+
import { useTheme } from '../../theme';
|
|
5
|
+
import { Avatar } from '../primitives/Avatar';
|
|
6
|
+
import { Text } from '../primitives/Text';
|
|
7
|
+
|
|
8
|
+
export type PreviewMetaRowProps = {
|
|
9
|
+
avatarUri?: string | null;
|
|
10
|
+
creatorName?: string | null;
|
|
11
|
+
title: string;
|
|
12
|
+
subtitle?: string | null;
|
|
13
|
+
tag?: React.ReactNode;
|
|
14
|
+
rightMetric?: React.ReactNode;
|
|
15
|
+
style?: ViewStyle;
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
export function PreviewMetaRow({
|
|
19
|
+
avatarUri,
|
|
20
|
+
creatorName,
|
|
21
|
+
title,
|
|
22
|
+
subtitle,
|
|
23
|
+
tag,
|
|
24
|
+
rightMetric,
|
|
25
|
+
style,
|
|
26
|
+
}: PreviewMetaRowProps) {
|
|
27
|
+
const theme = useTheme();
|
|
28
|
+
|
|
29
|
+
return (
|
|
30
|
+
<View style={[{ alignSelf: 'stretch' }, style]}>
|
|
31
|
+
<View style={{ flexDirection: 'row', alignItems: 'center' }}>
|
|
32
|
+
<Avatar uri={avatarUri} name={creatorName} size={24} style={{ marginRight: theme.spacing.sm }} />
|
|
33
|
+
|
|
34
|
+
<View style={{ flexDirection: 'row', alignItems: 'center', flex: 1, minWidth: 0, marginRight: theme.spacing.sm }}>
|
|
35
|
+
<Text
|
|
36
|
+
numberOfLines={1}
|
|
37
|
+
style={{
|
|
38
|
+
flexShrink: 1,
|
|
39
|
+
color: theme.colors.text,
|
|
40
|
+
fontSize: 16,
|
|
41
|
+
lineHeight: 20,
|
|
42
|
+
fontWeight: theme.typography.fontWeight.semibold,
|
|
43
|
+
}}
|
|
44
|
+
>
|
|
45
|
+
{title}
|
|
46
|
+
</Text>
|
|
47
|
+
{tag ? <View style={{ marginLeft: theme.spacing.sm }}>{tag}</View> : null}
|
|
48
|
+
</View>
|
|
49
|
+
|
|
50
|
+
{rightMetric ? <View>{rightMetric}</View> : null}
|
|
51
|
+
</View>
|
|
52
|
+
|
|
53
|
+
{subtitle ? (
|
|
54
|
+
<Text
|
|
55
|
+
numberOfLines={2}
|
|
56
|
+
style={{
|
|
57
|
+
marginTop: theme.spacing.sm,
|
|
58
|
+
color: theme.colors.textMuted,
|
|
59
|
+
fontSize: 14,
|
|
60
|
+
lineHeight: 18,
|
|
61
|
+
}}
|
|
62
|
+
>
|
|
63
|
+
{subtitle}
|
|
64
|
+
</Text>
|
|
65
|
+
) : null}
|
|
66
|
+
</View>
|
|
67
|
+
);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import * as React from 'react';
|
|
2
|
+
import { View, type ViewStyle } from 'react-native';
|
|
3
|
+
import { BottomSheetScrollView } from '@gorhom/bottom-sheet';
|
|
4
|
+
|
|
5
|
+
import { useTheme } from '../../theme';
|
|
6
|
+
|
|
7
|
+
export type PreviewPageProps = {
|
|
8
|
+
header?: React.ReactNode;
|
|
9
|
+
children: React.ReactNode;
|
|
10
|
+
contentStyle?: ViewStyle;
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
export function PreviewPage({ header, children, contentStyle }: PreviewPageProps) {
|
|
14
|
+
const theme = useTheme();
|
|
15
|
+
|
|
16
|
+
return (
|
|
17
|
+
<View style={{ flex: 1 }}>
|
|
18
|
+
{header ? <View>{header}</View> : null}
|
|
19
|
+
<BottomSheetScrollView
|
|
20
|
+
style={{ flex: 1 }}
|
|
21
|
+
contentContainerStyle={[
|
|
22
|
+
{
|
|
23
|
+
paddingHorizontal: theme.spacing.lg,
|
|
24
|
+
paddingBottom: theme.spacing.xl,
|
|
25
|
+
flexGrow: 1,
|
|
26
|
+
},
|
|
27
|
+
contentStyle,
|
|
28
|
+
]}
|
|
29
|
+
>
|
|
30
|
+
{children}
|
|
31
|
+
</BottomSheetScrollView>
|
|
32
|
+
</View>
|
|
33
|
+
);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import * as React from 'react';
|
|
2
|
+
import { Animated, type ViewStyle } from 'react-native';
|
|
3
|
+
import { LinearGradient } from 'expo-linear-gradient';
|
|
4
|
+
|
|
5
|
+
export type PreviewPlaceholderProps = {
|
|
6
|
+
visible: boolean;
|
|
7
|
+
style?: ViewStyle;
|
|
8
|
+
};
|
|
9
|
+
|
|
10
|
+
export function PreviewPlaceholder({ visible, style }: PreviewPlaceholderProps) {
|
|
11
|
+
if (!visible) return null;
|
|
12
|
+
|
|
13
|
+
const opacityAnim = React.useRef(new Animated.Value(0)).current;
|
|
14
|
+
|
|
15
|
+
React.useEffect(() => {
|
|
16
|
+
if (!visible) return;
|
|
17
|
+
const animation = Animated.loop(
|
|
18
|
+
Animated.sequence([
|
|
19
|
+
Animated.timing(opacityAnim, { toValue: 1, duration: 1500, useNativeDriver: true }),
|
|
20
|
+
Animated.timing(opacityAnim, { toValue: 2, duration: 1500, useNativeDriver: true }),
|
|
21
|
+
Animated.timing(opacityAnim, { toValue: 3, duration: 1500, useNativeDriver: true }),
|
|
22
|
+
Animated.timing(opacityAnim, { toValue: 0, duration: 1500, useNativeDriver: true }),
|
|
23
|
+
])
|
|
24
|
+
);
|
|
25
|
+
animation.start();
|
|
26
|
+
return () => animation.stop();
|
|
27
|
+
}, [opacityAnim, visible]);
|
|
28
|
+
|
|
29
|
+
const opacity1 = opacityAnim.interpolate({ inputRange: [0, 1, 2, 3], outputRange: [1, 0, 0, 0.3] });
|
|
30
|
+
const opacity2 = opacityAnim.interpolate({ inputRange: [0, 1, 2, 3], outputRange: [0, 1, 0, 0] });
|
|
31
|
+
const opacity3 = opacityAnim.interpolate({ inputRange: [0, 1, 2, 3], outputRange: [0, 0, 1, 0] });
|
|
32
|
+
const opacity4 = opacityAnim.interpolate({ inputRange: [0, 1, 2, 3], outputRange: [0, 0, 0, 1] });
|
|
33
|
+
|
|
34
|
+
return (
|
|
35
|
+
<>
|
|
36
|
+
<Animated.View style={[{ position: 'absolute', inset: 0, opacity: opacity1 }, style]}>
|
|
37
|
+
<LinearGradient
|
|
38
|
+
colors={['rgba(98, 0, 238, 0.45)', 'rgba(168, 85, 247, 0.35)']}
|
|
39
|
+
start={{ x: 0, y: 0 }}
|
|
40
|
+
end={{ x: 1, y: 1 }}
|
|
41
|
+
style={{ width: '100%', height: '100%' }}
|
|
42
|
+
/>
|
|
43
|
+
</Animated.View>
|
|
44
|
+
<Animated.View style={[{ position: 'absolute', inset: 0, opacity: opacity2 }, style]}>
|
|
45
|
+
<LinearGradient
|
|
46
|
+
colors={['rgba(168, 85, 247, 0.45)', 'rgba(139, 92, 246, 0.35)']}
|
|
47
|
+
start={{ x: 1, y: 0 }}
|
|
48
|
+
end={{ x: 0, y: 1 }}
|
|
49
|
+
style={{ width: '100%', height: '100%' }}
|
|
50
|
+
/>
|
|
51
|
+
</Animated.View>
|
|
52
|
+
<Animated.View style={[{ position: 'absolute', inset: 0, opacity: opacity3 }, style]}>
|
|
53
|
+
<LinearGradient
|
|
54
|
+
colors={['rgba(139, 92, 246, 0.45)', 'rgba(126, 34, 206, 0.35)']}
|
|
55
|
+
start={{ x: 0, y: 1 }}
|
|
56
|
+
end={{ x: 1, y: 0 }}
|
|
57
|
+
style={{ width: '100%', height: '100%' }}
|
|
58
|
+
/>
|
|
59
|
+
</Animated.View>
|
|
60
|
+
<Animated.View style={[{ position: 'absolute', inset: 0, opacity: opacity4 }, style]}>
|
|
61
|
+
<LinearGradient
|
|
62
|
+
colors={['rgba(126, 34, 206, 0.45)', 'rgba(98, 0, 238, 0.35)']}
|
|
63
|
+
start={{ x: 0.5, y: 0 }}
|
|
64
|
+
end={{ x: 0.5, y: 1 }}
|
|
65
|
+
style={{ width: '100%', height: '100%' }}
|
|
66
|
+
/>
|
|
67
|
+
</Animated.View>
|
|
68
|
+
</>
|
|
69
|
+
);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import * as React from 'react';
|
|
2
|
+
import { View } from 'react-native';
|
|
3
|
+
import {
|
|
4
|
+
AlertTriangle,
|
|
5
|
+
Archive,
|
|
6
|
+
CheckCircle2,
|
|
7
|
+
GitFork,
|
|
8
|
+
GitMerge,
|
|
9
|
+
Pencil,
|
|
10
|
+
Sparkles,
|
|
11
|
+
type LucideIcon,
|
|
12
|
+
} from 'lucide-react-native';
|
|
13
|
+
|
|
14
|
+
import type { AppStatus } from '../../data/apps/types';
|
|
15
|
+
import { APP_STATUS_LABEL } from '../../data/apps/types';
|
|
16
|
+
import { Text } from '../primitives/Text';
|
|
17
|
+
|
|
18
|
+
const STATUS_BG: Record<AppStatus, string> = {
|
|
19
|
+
ready: '#10B981', // emerald-500
|
|
20
|
+
creating: '#3B82F6', // blue-500
|
|
21
|
+
editing: '#F59E0B', // amber-500
|
|
22
|
+
forking: '#8B5CF6', // violet-500
|
|
23
|
+
merging: '#06B6D4', // cyan-500
|
|
24
|
+
error: '#F43F5E', // rose-500
|
|
25
|
+
archived: '#71717A', // zinc-500
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
const STATUS_ICON: Record<AppStatus, LucideIcon> = {
|
|
29
|
+
ready: CheckCircle2,
|
|
30
|
+
creating: Sparkles,
|
|
31
|
+
editing: Pencil,
|
|
32
|
+
forking: GitFork,
|
|
33
|
+
merging: GitMerge,
|
|
34
|
+
error: AlertTriangle,
|
|
35
|
+
archived: Archive,
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
export type PreviewStatusBadgeProps = {
|
|
39
|
+
status: AppStatus;
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
export function PreviewStatusBadge({ status }: PreviewStatusBadgeProps) {
|
|
43
|
+
const IconComp = STATUS_ICON[status];
|
|
44
|
+
const label = APP_STATUS_LABEL[status] ?? status;
|
|
45
|
+
|
|
46
|
+
return (
|
|
47
|
+
<View
|
|
48
|
+
style={{
|
|
49
|
+
flexDirection: 'row',
|
|
50
|
+
alignItems: 'center',
|
|
51
|
+
borderRadius: 999,
|
|
52
|
+
paddingHorizontal: 10,
|
|
53
|
+
paddingVertical: 4,
|
|
54
|
+
backgroundColor: STATUS_BG[status],
|
|
55
|
+
}}
|
|
56
|
+
>
|
|
57
|
+
<IconComp size={12} color="#FFFFFF" style={{ marginRight: 4 }} />
|
|
58
|
+
<Text style={{ color: '#FFFFFF', fontSize: 11, lineHeight: 14 }}>{label}</Text>
|
|
59
|
+
</View>
|
|
60
|
+
);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
|