@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.
Files changed (174) hide show
  1. package/dist/index.d.mts +2 -10
  2. package/dist/index.d.ts +2 -10
  3. package/dist/index.js +293 -264
  4. package/dist/index.js.map +1 -1
  5. package/dist/index.mjs +251 -222
  6. package/dist/index.mjs.map +1 -1
  7. package/package.json +8 -5
  8. package/src/components/chat/ChatComposer.tsx +277 -0
  9. package/src/components/chat/ChatHeader.tsx +31 -0
  10. package/src/components/chat/ChatMessageBubble.tsx +69 -0
  11. package/src/components/chat/ChatMessageList.tsx +137 -0
  12. package/src/components/chat/ChatPage.tsx +69 -0
  13. package/src/components/chat/ForkNoticeBanner.tsx +66 -0
  14. package/src/components/chat/MultilineTextInput.tsx +46 -0
  15. package/src/components/chat/ScrollToBottomButton.tsx +78 -0
  16. package/src/components/chat/TypingIndicator.tsx +54 -0
  17. package/src/components/chat/index.ts +28 -0
  18. package/src/components/comments/AppCommentsSheet.tsx +213 -0
  19. package/src/components/comments/CommentRow.tsx +63 -0
  20. package/src/components/comments/formatTimeAgo.ts +3 -0
  21. package/src/components/comments/index.ts +3 -0
  22. package/src/components/comments/useAppComments.ts +74 -0
  23. package/src/components/comments/useAppDetails.ts +35 -0
  24. package/src/components/comments/useIosKeyboardSnapFix.ts +24 -0
  25. package/src/components/dialogs/ConfirmMergeRequestDialog.tsx +156 -0
  26. package/src/components/dialogs/index.ts +4 -0
  27. package/src/components/draw/DrawColorPicker.tsx +77 -0
  28. package/src/components/draw/DrawModeOverlay.tsx +144 -0
  29. package/src/components/draw/DrawSurface.tsx +127 -0
  30. package/src/components/draw/DrawToolbar.tsx +253 -0
  31. package/src/components/draw/index.ts +15 -0
  32. package/src/components/draw/optionalHaptics.ts +15 -0
  33. package/src/components/draw/strokes.ts +21 -0
  34. package/src/components/draw/types.ts +9 -0
  35. package/src/components/floating-draggable-button/FloatingDraggableButton.tsx +323 -0
  36. package/src/components/floating-draggable-button/constants.ts +17 -0
  37. package/src/components/floating-draggable-button/index.ts +4 -0
  38. package/src/components/floating-draggable-button/types.ts +63 -0
  39. package/src/components/icons/MergeIcon.tsx +14 -0
  40. package/src/components/icons/StudioIcons.tsx +66 -0
  41. package/src/components/index.ts +17 -0
  42. package/src/components/merge-requests/MergeRequestStatusCard.tsx +179 -0
  43. package/src/components/merge-requests/ReviewMergeRequestActionButton.tsx +62 -0
  44. package/src/components/merge-requests/ReviewMergeRequestCard.tsx +192 -0
  45. package/src/components/merge-requests/ReviewMergeRequestCarousel.tsx +132 -0
  46. package/src/components/merge-requests/index.ts +7 -0
  47. package/src/components/merge-requests/mergeRequestStatusDisplay.ts +23 -0
  48. package/src/components/merge-requests/toIsoString.ts +9 -0
  49. package/src/components/merge-requests/useControlledExpansion.ts +16 -0
  50. package/src/components/models/index.ts +9 -0
  51. package/src/components/models/types.ts +43 -0
  52. package/src/components/overlays/EdgeGlowFrame.tsx +105 -0
  53. package/src/components/overlays/index.ts +4 -0
  54. package/src/components/preview/PreviewHeroCard.tsx +58 -0
  55. package/src/components/preview/PreviewImage.tsx +22 -0
  56. package/src/components/preview/PreviewMetaRow.tsx +70 -0
  57. package/src/components/preview/PreviewPage.tsx +36 -0
  58. package/src/components/preview/PreviewPlaceholder.tsx +72 -0
  59. package/src/components/preview/PreviewStatusBadge.tsx +63 -0
  60. package/src/components/preview/StatsBar.tsx +109 -0
  61. package/src/components/preview/index.ts +22 -0
  62. package/src/components/primitives/Avatar.tsx +68 -0
  63. package/src/components/primitives/Button.tsx +102 -0
  64. package/src/components/primitives/Card.tsx +30 -0
  65. package/src/components/primitives/Divider.tsx +17 -0
  66. package/src/components/primitives/Icon.tsx +40 -0
  67. package/src/components/primitives/MarkdownText.tsx +72 -0
  68. package/src/components/primitives/Modal.tsx +53 -0
  69. package/src/components/primitives/Surface.tsx +42 -0
  70. package/src/components/primitives/Text.tsx +83 -0
  71. package/src/components/primitives/index.ts +35 -0
  72. package/src/components/primitives/types.ts +30 -0
  73. package/src/components/studio-sheet/StudioBottomSheet.tsx +114 -0
  74. package/src/components/studio-sheet/StudioSheetBackground.tsx +63 -0
  75. package/src/components/studio-sheet/StudioSheetHeader.tsx +35 -0
  76. package/src/components/studio-sheet/StudioSheetHeaderIconButton.tsx +109 -0
  77. package/src/components/studio-sheet/StudioSheetPager.tsx +66 -0
  78. package/src/components/studio-sheet/index.ts +18 -0
  79. package/src/components/studio-sheet/types.ts +5 -0
  80. package/src/components/utils/color.ts +25 -0
  81. package/src/components/utils/formatTimeAgo.ts +19 -0
  82. package/src/core/logger.ts +42 -0
  83. package/src/core/services/http/baseUrl.ts +3 -0
  84. package/src/core/services/http/index.ts +128 -0
  85. package/src/core/services/http/public.ts +33 -0
  86. package/src/core/services/supabase/auth.ts +41 -0
  87. package/src/core/services/supabase/client.ts +43 -0
  88. package/src/core/services/supabase/index.ts +7 -0
  89. package/src/data/agent/remote.ts +30 -0
  90. package/src/data/agent/repository.ts +34 -0
  91. package/src/data/agent/types.ts +28 -0
  92. package/src/data/apps/bundles/remote.ts +47 -0
  93. package/src/data/apps/bundles/repository.ts +35 -0
  94. package/src/data/apps/bundles/types.ts +27 -0
  95. package/src/data/apps/images/remote.ts +61 -0
  96. package/src/data/apps/images/repository.ts +47 -0
  97. package/src/data/apps/remote.ts +97 -0
  98. package/src/data/apps/repository.ts +185 -0
  99. package/src/data/apps/types.ts +206 -0
  100. package/src/data/attachment/remote.ts +32 -0
  101. package/src/data/attachment/repository.ts +40 -0
  102. package/src/data/attachment/types.ts +42 -0
  103. package/src/data/base-remote.ts +3 -0
  104. package/src/data/base-repository.ts +11 -0
  105. package/src/data/comments/likes/remote.ts +87 -0
  106. package/src/data/comments/likes/repository.ts +61 -0
  107. package/src/data/comments/likes/types.ts +47 -0
  108. package/src/data/comments/remote.ts +71 -0
  109. package/src/data/comments/repository.ts +53 -0
  110. package/src/data/comments/types.ts +60 -0
  111. package/src/data/github/remote.ts +23 -0
  112. package/src/data/github/repository.ts +35 -0
  113. package/src/data/github/types.ts +23 -0
  114. package/src/data/home/remote.ts +24 -0
  115. package/src/data/home/repository.ts +28 -0
  116. package/src/data/home/types.ts +70 -0
  117. package/src/data/index.ts +3 -0
  118. package/src/data/likes/remote.ts +57 -0
  119. package/src/data/likes/repository.ts +47 -0
  120. package/src/data/likes/types.ts +46 -0
  121. package/src/data/me/remote.ts +28 -0
  122. package/src/data/me/repository.ts +30 -0
  123. package/src/data/me/types.ts +14 -0
  124. package/src/data/merge-requests/remote.ts +76 -0
  125. package/src/data/merge-requests/repository.ts +66 -0
  126. package/src/data/merge-requests/types.ts +33 -0
  127. package/src/data/messages/remote.ts +21 -0
  128. package/src/data/messages/repository.ts +104 -0
  129. package/src/data/messages/types.ts +20 -0
  130. package/src/data/public/studio-config/remote.ts +19 -0
  131. package/src/data/public/studio-config/repository.ts +23 -0
  132. package/src/data/public/studio-config/types.ts +6 -0
  133. package/src/data/ratings/remote.ts +76 -0
  134. package/src/data/ratings/repository.ts +63 -0
  135. package/src/data/ratings/types.ts +57 -0
  136. package/src/data/threads/remote.ts +40 -0
  137. package/src/data/threads/repository.ts +41 -0
  138. package/src/data/threads/types.ts +25 -0
  139. package/src/data/types.ts +8 -0
  140. package/src/data/users/remote.ts +31 -0
  141. package/src/data/users/repository.ts +45 -0
  142. package/src/data/users/types.ts +15 -0
  143. package/src/index.ts +6 -0
  144. package/src/studio/ComergeStudio.tsx +239 -0
  145. package/src/studio/bootstrap/StudioBootstrap.tsx +45 -0
  146. package/src/studio/bootstrap/useStudioBootstrap.ts +55 -0
  147. package/src/studio/hooks/useApp.ts +83 -0
  148. package/src/studio/hooks/useAppStats.ts +111 -0
  149. package/src/studio/hooks/useAttachmentUpload.ts +59 -0
  150. package/src/studio/hooks/useBundleManager.ts +389 -0
  151. package/src/studio/hooks/useMergeRequests.ts +173 -0
  152. package/src/studio/hooks/useStudioActions.ts +96 -0
  153. package/src/studio/hooks/useThreadMessages.ts +85 -0
  154. package/src/studio/lib/chat.ts +34 -0
  155. package/src/studio/ui/ChatPanel.tsx +154 -0
  156. package/src/studio/ui/ConfirmMergeFlow.tsx +55 -0
  157. package/src/studio/ui/PreviewPanel.tsx +131 -0
  158. package/src/studio/ui/RuntimeRenderer.tsx +40 -0
  159. package/src/studio/ui/StudioOverlay.tsx +257 -0
  160. package/src/studio/ui/preview-panel/PressableCardRow.tsx +49 -0
  161. package/src/studio/ui/preview-panel/PreviewCollaborateSection.tsx +174 -0
  162. package/src/studio/ui/preview-panel/PreviewCustomizeSection.tsx +160 -0
  163. package/src/studio/ui/preview-panel/PreviewHeroSection.tsx +56 -0
  164. package/src/studio/ui/preview-panel/PreviewMetaSection.tsx +67 -0
  165. package/src/studio/ui/preview-panel/PreviewPanelHeader.tsx +48 -0
  166. package/src/studio/ui/preview-panel/SectionTitle.tsx +31 -0
  167. package/src/studio/ui/preview-panel/usePreviewPanelData.ts +132 -0
  168. package/src/studio/ui/preview-panel/utils.ts +29 -0
  169. package/src/theme/index.ts +5 -0
  170. package/src/theme/tokens.ts +118 -0
  171. package/src/theme/types.ts +90 -0
  172. package/src/theme/useTheme.ts +11 -0
  173. package/dist/assets/images/merge.svg +0 -3
  174. 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,9 @@
1
+ export type {
2
+ ChatAuthor,
3
+ ChatMessage,
4
+ MergeRequestStatus,
5
+ MergeRequestSummary,
6
+ UserSummary,
7
+ } from './types';
8
+
9
+
@@ -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,4 @@
1
+ export { EdgeGlowFrame } from './EdgeGlowFrame';
2
+ export type { EdgeGlowFrameProps } from './EdgeGlowFrame';
3
+
4
+
@@ -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
+