@comergehq/studio 0.1.2 → 0.1.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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 +8 -5
  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,63 @@
1
+ import type * as React from 'react';
2
+ import type { StyleProp, ViewStyle } from 'react-native';
3
+
4
+ export type FloatingButtonOffset = {
5
+ /** Distance from the left edge (in px). */
6
+ left?: number;
7
+ /** Distance from the bottom edge (in px). */
8
+ bottom?: number;
9
+ };
10
+
11
+ export type FloatingDraggableButtonProps = {
12
+ /**
13
+ * Whether the button should be shown. When toggled, the button animates in/out.
14
+ * The component stays mounted to preserve its last drag position.
15
+ */
16
+ visible?: boolean;
17
+
18
+ /** Called after the "press → animate out" sequence completes. */
19
+ onPress?: () => void | Promise<void>;
20
+
21
+ disabled?: boolean;
22
+
23
+ /** Button diameter (square hit area). */
24
+ size?: number;
25
+
26
+ /** Padding from screen edges while dragging. */
27
+ edgePadding?: number;
28
+
29
+ /**
30
+ * Initial placement when it animates in.
31
+ * `left` is measured from the left edge; `bottom` from the bottom edge.
32
+ */
33
+ offset?: FloatingButtonOffset;
34
+
35
+ /** Accessible label for screen readers (kept as `ariaLabel` for compatibility). */
36
+ ariaLabel?: string;
37
+
38
+ /** Small badge rendered in the top-right corner. */
39
+ badgeCount?: number;
40
+
41
+ /** When true, renders a pulsing border ring. */
42
+ isLoading?: boolean;
43
+
44
+ variant?: 'default' | 'danger';
45
+
46
+ /** Override button background color (takes precedence over `variant`). */
47
+ backgroundColor?: string;
48
+
49
+ /**
50
+ * Increment to force the button to animate in again.
51
+ */
52
+ forceShowTrigger?: number;
53
+
54
+ /** Optional custom content inside the button (icon, etc.). */
55
+ children?: React.ReactNode;
56
+
57
+ /** Optional style for the outer container (positioning handled internally). */
58
+ style?: StyleProp<ViewStyle>;
59
+
60
+ testID?: string;
61
+ };
62
+
63
+
@@ -0,0 +1,14 @@
1
+ import Svg, { Path, type SvgProps } from 'react-native-svg';
2
+
3
+ export function MergeIcon({ color = 'currentColor', width = 24, height = 24, ...props }: SvgProps) {
4
+ return (
5
+ <Svg viewBox="0 0 486 486" width={width} height={height} {...props}>
6
+ <Path
7
+ d="M237.025 0H243.664C254.876 95.0361 275.236 175.597 304.743 241.684C334.249 307.478 367.002 357.774 403 392.572L389.722 486C361.691 458.22 338.233 429.417 319.349 399.59C300.464 369.764 284.531 335.843 271.548 297.829C258.565 259.522 246.615 214.343 235.697 162.292L237.91 161.415C228.468 214.928 217.993 261.569 206.485 301.338C194.978 341.107 179.634 375.904 160.455 405.731C141.571 435.265 115.752 462.022 83 486L96.278 392.572C124.014 369.179 147.62 336.72 167.094 295.197C186.864 253.381 202.65 206.886 214.452 155.713C226.255 104.247 233.779 52.343 237.025 0Z"
8
+ fill={color}
9
+ />
10
+ </Svg>
11
+ );
12
+ }
13
+
14
+
@@ -0,0 +1,66 @@
1
+ import * as React from 'react';
2
+ import type { LucideProps } from 'lucide-react-native';
3
+ import {
4
+ ArrowDown,
5
+ ChevronLeft,
6
+ ChevronRight,
7
+ ChevronDown,
8
+ Home,
9
+ MessageSquare,
10
+ Pencil,
11
+ Play,
12
+ Send,
13
+ X,
14
+ Check,
15
+ } from 'lucide-react-native';
16
+
17
+ import { useTheme } from '../../theme';
18
+
19
+ export type StudioIconProps = Omit<LucideProps, 'color'> & {
20
+ colorToken?: 'floatingContent' | 'text' | 'textMuted' | 'primary' | 'danger' | 'onPrimary' | 'onDanger';
21
+ };
22
+
23
+ function useResolvedIconColor(token: NonNullable<StudioIconProps['colorToken']>) {
24
+ const theme = useTheme();
25
+ switch (token) {
26
+ case 'text':
27
+ return theme.colors.text;
28
+ case 'textMuted':
29
+ return theme.colors.textMuted;
30
+ case 'primary':
31
+ return theme.colors.primary;
32
+ case 'danger':
33
+ return theme.colors.danger;
34
+ case 'onPrimary':
35
+ return theme.colors.onPrimary;
36
+ case 'onDanger':
37
+ return theme.colors.onDanger;
38
+ case 'floatingContent':
39
+ default:
40
+ return theme.colors.floatingContent;
41
+ }
42
+ }
43
+
44
+ function makeIcon(Comp: React.ComponentType<LucideProps>) {
45
+ return function StudioIcon({ size = 20, strokeWidth = 2, colorToken = 'floatingContent', ...rest }: StudioIconProps) {
46
+ const color = useResolvedIconColor(colorToken);
47
+ return <Comp size={size} strokeWidth={strokeWidth} color={color} {...rest} />;
48
+ };
49
+ }
50
+
51
+ // Header / nav
52
+ export const IconHome = makeIcon(Home);
53
+ export const IconClose = makeIcon(X);
54
+ export const IconBack = makeIcon(ChevronLeft);
55
+ export const IconChevronRight = makeIcon(ChevronRight);
56
+ export const IconChevronDown = makeIcon(ChevronDown);
57
+
58
+ // Actions
59
+ export const IconChat = makeIcon(MessageSquare);
60
+ export const IconDraw = makeIcon(Pencil);
61
+ export const IconSend = makeIcon(Send);
62
+ export const IconPlay = makeIcon(Play);
63
+ export const IconArrowDown = makeIcon(ArrowDown);
64
+ export const IconApprove = makeIcon(Check);
65
+
66
+
@@ -0,0 +1,17 @@
1
+ export { FloatingDraggableButton } from './floating-draggable-button';
2
+ export type { FloatingDraggableButtonProps, FloatingButtonOffset } from './floating-draggable-button';
3
+
4
+ export * from './icons/StudioIcons';
5
+
6
+ export * from './primitives';
7
+ export * from './comments';
8
+ export * from './studio-sheet';
9
+ export * from './preview';
10
+ export * from './models';
11
+ export * from './merge-requests';
12
+ export * from './chat';
13
+ export * from './dialogs';
14
+ export * from './overlays';
15
+ export * from './draw';
16
+
17
+
@@ -0,0 +1,179 @@
1
+ import * as React from 'react';
2
+ import { Animated, Pressable, View, type ViewStyle } from 'react-native';
3
+ import { Ban, Check, CheckCheck, ChevronDown } from 'lucide-react-native';
4
+
5
+ import type { MergeRequestSummary } from '../models/types';
6
+ import { useTheme } from '../../theme';
7
+ import { withAlpha } from '../utils/color';
8
+ import { Card } from '../primitives/Card';
9
+ import { MarkdownText } from '../primitives/MarkdownText';
10
+ import { Text } from '../primitives/Text';
11
+ import { formatTimeAgo } from '../utils/formatTimeAgo';
12
+ import { getMergeRequestStatusDisplay } from './mergeRequestStatusDisplay';
13
+ import { toIsoString } from './toIsoString';
14
+ import { useControlledExpansion } from './useControlledExpansion';
15
+
16
+ export type MergeRequestStatusCardProps = {
17
+ mergeRequest: MergeRequestSummary;
18
+ expanded?: boolean;
19
+ onExpandedChange?: (expanded: boolean) => void;
20
+ headerRight?: React.ReactNode;
21
+ style?: ViewStyle;
22
+ };
23
+
24
+ export function MergeRequestStatusCard({
25
+ mergeRequest,
26
+ expanded: expandedProp,
27
+ onExpandedChange,
28
+ headerRight,
29
+ style,
30
+ }: MergeRequestStatusCardProps) {
31
+ const theme = useTheme();
32
+ const { expanded, setExpanded } = useControlledExpansion({ expanded: expandedProp, onExpandedChange });
33
+ const isDark = theme.scheme === 'dark';
34
+ const textColor = isDark ? '#FFFFFF' : '#000000';
35
+ const subTextColor = isDark ? '#A1A1AA' : '#71717A';
36
+ const status = React.useMemo(() => getMergeRequestStatusDisplay(String(mergeRequest.status)), [mergeRequest.status]);
37
+
38
+ const { StatusIcon, iconColor, bgColor, statusText } = React.useMemo(() => {
39
+ switch (mergeRequest.status) {
40
+ case 'approved':
41
+ case 'merged':
42
+ return {
43
+ StatusIcon: CheckCheck,
44
+ iconColor: '#10B981',
45
+ bgColor: 'rgba(16, 185, 129, 0.1)',
46
+ statusText: 'Edit approved by developer',
47
+ };
48
+ case 'rejected':
49
+ return {
50
+ StatusIcon: Ban,
51
+ iconColor: '#F43F5E',
52
+ bgColor: 'rgba(244, 63, 94, 0.1)',
53
+ statusText: 'Edit rejected by developer',
54
+ };
55
+ case 'open':
56
+ default:
57
+ return {
58
+ StatusIcon: Check,
59
+ iconColor: '#FACC15',
60
+ bgColor: 'rgba(250, 204, 21, 0.1)',
61
+ statusText: 'Edit submitted to developer',
62
+ };
63
+ }
64
+ }, [mergeRequest.status]);
65
+
66
+ const updatedIso = toIsoString(mergeRequest.updatedAt ?? null) ?? toIsoString(mergeRequest.createdAt ?? null);
67
+ const createdIso = toIsoString(mergeRequest.createdAt ?? null);
68
+ const headerTimeAgo = updatedIso ? formatTimeAgo(updatedIso) : '';
69
+ const createdTimeAgo = createdIso ? formatTimeAgo(createdIso) : '';
70
+
71
+ const rotate = React.useRef(new Animated.Value(expanded ? 1 : 0)).current;
72
+ React.useEffect(() => {
73
+ Animated.timing(rotate, {
74
+ toValue: expanded ? 1 : 0,
75
+ duration: 200,
76
+ useNativeDriver: true,
77
+ }).start();
78
+ }, [expanded, rotate]);
79
+
80
+ return (
81
+ <Pressable onPress={() => setExpanded(!expanded)} style={({ pressed }) => [{ opacity: pressed ? 0.95 : 1 }]}>
82
+ <Card
83
+ padded={false}
84
+ border={false}
85
+ style={[
86
+ {
87
+ padding: theme.spacing.lg,
88
+ backgroundColor: withAlpha(theme.colors.surfaceRaised, 0.5),
89
+ } as any,
90
+ style,
91
+ ]}
92
+ >
93
+ <View style={{ flexDirection: 'row', alignItems: 'center', gap: theme.spacing.lg }}>
94
+ <View style={{ width: 40, height: 40, borderRadius: 999, alignItems: 'center', justifyContent: 'center', backgroundColor: bgColor }}>
95
+ <StatusIcon size={20} color={iconColor} />
96
+ </View>
97
+
98
+ <View style={{ flex: 1, minWidth: 0 }}>
99
+ <View style={{ flexDirection: 'row', alignItems: 'center', justifyContent: 'space-between' }}>
100
+ <Text
101
+ style={{
102
+ fontSize: 16,
103
+ lineHeight: 20,
104
+ fontWeight: theme.typography.fontWeight.semibold,
105
+ color: theme.colors.text,
106
+ flex: 1,
107
+ }}
108
+ numberOfLines={1}
109
+ >
110
+ {statusText}
111
+ </Text>
112
+ {headerTimeAgo ? (
113
+ <Text style={{ fontSize: 10, lineHeight: 14, marginLeft: theme.spacing.sm, color: withAlpha(theme.colors.textMuted, 0.6) }}>
114
+ {headerTimeAgo}
115
+ </Text>
116
+ ) : null}
117
+ </View>
118
+
119
+ <Text style={{ fontSize: 12, lineHeight: 16, color: theme.colors.textMuted }} numberOfLines={1}>
120
+ {mergeRequest.title ?? 'Untitled merge request'}
121
+ </Text>
122
+ </View>
123
+
124
+ {headerRight ? (
125
+ <View>{headerRight}</View>
126
+ ) : (
127
+ <Animated.View
128
+ style={{
129
+ transform: [
130
+ {
131
+ rotate: rotate.interpolate({ inputRange: [0, 1], outputRange: ['0deg', '180deg'] }),
132
+ },
133
+ ],
134
+ }}
135
+ >
136
+ <ChevronDown size={20} color={withAlpha(theme.colors.textMuted, 0.4)} />
137
+ </Animated.View>
138
+ )}
139
+ </View>
140
+
141
+ {expanded ? (
142
+ <View style={{ marginTop: 16, marginLeft: 56 }}>
143
+ <Text
144
+ style={{
145
+ fontSize: 10,
146
+ fontWeight: '700',
147
+ textTransform: 'uppercase',
148
+ letterSpacing: 0.5,
149
+ color: status.color,
150
+ marginBottom: 2,
151
+ }}
152
+ >
153
+ {status.text}
154
+ </Text>
155
+ {createdTimeAgo ? (
156
+ <Text
157
+ style={{
158
+ fontSize: 11,
159
+ color: subTextColor,
160
+ marginBottom: 8,
161
+ }}
162
+ >
163
+ {createdTimeAgo}
164
+ </Text>
165
+ ) : null}
166
+
167
+ <Text style={{ fontSize: 16, fontWeight: '600', color: textColor, marginBottom: 8 }}>
168
+ {mergeRequest.title ?? 'Untitled merge request'}
169
+ </Text>
170
+
171
+ {mergeRequest.description ? <MarkdownText markdown={mergeRequest.description} variant="mergeRequest" /> : null}
172
+ </View>
173
+ ) : null}
174
+ </Card>
175
+ </Pressable>
176
+ );
177
+ }
178
+
179
+
@@ -0,0 +1,62 @@
1
+ import * as React from 'react';
2
+ import { Pressable, View } from 'react-native';
3
+
4
+ export function ReviewMergeRequestActionButton({
5
+ accessibilityLabel,
6
+ backgroundColor,
7
+ disabled,
8
+ onPress,
9
+ children,
10
+ iconOnly,
11
+ }: {
12
+ accessibilityLabel: string;
13
+ backgroundColor: string;
14
+ disabled?: boolean;
15
+ onPress: () => void;
16
+ children: React.ReactNode;
17
+ iconOnly: boolean;
18
+ }) {
19
+ const [pressed, setPressed] = React.useState(false);
20
+ const height = iconOnly ? 36 : 40;
21
+ const width = iconOnly ? 36 : undefined;
22
+ const paddingHorizontal = iconOnly ? 0 : 16;
23
+ const paddingVertical = iconOnly ? 0 : 8;
24
+ const opacity = disabled ? 0.5 : pressed ? 0.9 : 1;
25
+
26
+ return (
27
+ <View
28
+ style={{
29
+ width,
30
+ minWidth: width,
31
+ height,
32
+ minHeight: height,
33
+ borderRadius: 999,
34
+ backgroundColor,
35
+ opacity,
36
+ paddingHorizontal,
37
+ paddingVertical,
38
+ justifyContent: 'center',
39
+ }}
40
+ >
41
+ <Pressable
42
+ accessibilityRole="button"
43
+ accessibilityLabel={accessibilityLabel}
44
+ disabled={Boolean(disabled)}
45
+ onPress={onPress}
46
+ onPressIn={() => setPressed(true)}
47
+ onPressOut={() => setPressed(false)}
48
+ style={{
49
+ height: '100%',
50
+ width: '100%',
51
+ alignItems: 'center',
52
+ justifyContent: 'center',
53
+ }}
54
+ hitSlop={8}
55
+ >
56
+ {children}
57
+ </Pressable>
58
+ </View>
59
+ );
60
+ }
61
+
62
+
@@ -0,0 +1,192 @@
1
+ import * as React from 'react';
2
+ import { ActivityIndicator, Animated, Pressable, View } from 'react-native';
3
+ import { Check, ChevronDown, Play, X } from 'lucide-react-native';
4
+
5
+ import type { MergeRequest } from '../../data/merge-requests/types';
6
+ import type { UserStats } from '../../data/users/types';
7
+ import { useTheme } from '../../theme';
8
+ import { Avatar } from '../primitives/Avatar';
9
+ import { Card } from '../primitives/Card';
10
+ import { MarkdownText } from '../primitives/MarkdownText';
11
+ import { Text } from '../primitives/Text';
12
+ import { withAlpha } from '../utils/color';
13
+ import { getMergeRequestStatusDisplay } from './mergeRequestStatusDisplay';
14
+ import { ReviewMergeRequestActionButton } from './ReviewMergeRequestActionButton';
15
+
16
+ export type ReviewMergeRequestCardProps = {
17
+ mr: MergeRequest;
18
+ index: number;
19
+ total: number;
20
+ creator?: UserStats;
21
+ isExpanded: boolean;
22
+ isProcessing: boolean;
23
+ isAnyProcessing: boolean;
24
+ isBuilding: boolean;
25
+ isTestingThis: boolean;
26
+ onToggle: () => void;
27
+ onReject: () => void;
28
+ onApprove: () => void;
29
+ onTest: () => void;
30
+ };
31
+
32
+ export function ReviewMergeRequestCard({
33
+ mr,
34
+ index,
35
+ total,
36
+ creator,
37
+ isExpanded,
38
+ isProcessing,
39
+ isAnyProcessing,
40
+ isBuilding,
41
+ isTestingThis,
42
+ onToggle,
43
+ onReject,
44
+ onApprove,
45
+ onTest,
46
+ }: ReviewMergeRequestCardProps) {
47
+ const theme = useTheme();
48
+ const status = React.useMemo(() => getMergeRequestStatusDisplay(mr.status), [mr.status]);
49
+ const canAct = mr.status === 'open';
50
+
51
+ const rotate = React.useRef(new Animated.Value(isExpanded ? 1 : 0)).current;
52
+ React.useEffect(() => {
53
+ Animated.timing(rotate, { toValue: isExpanded ? 1 : 0, duration: 200, useNativeDriver: true }).start();
54
+ }, [isExpanded, rotate]);
55
+
56
+ const position = total > 1 ? `${index + 1}/${total}` : 'Merge request';
57
+
58
+ return (
59
+ <Pressable onPress={onToggle} style={({ pressed }) => ({ opacity: pressed ? 0.95 : 1 })}>
60
+ <Card
61
+ padded={false}
62
+ style={[
63
+ {
64
+ padding: 16,
65
+ backgroundColor: withAlpha(theme.colors.surfaceRaised, 0.5),
66
+ borderWidth: 1,
67
+ borderColor: withAlpha('#3700B3', 0.2),
68
+ } as any,
69
+ ]}
70
+ >
71
+ {/* Collapsed header */}
72
+ <View style={{ flexDirection: 'row', alignItems: 'center', gap: 12 }}>
73
+ <Avatar size={40} uri={creator?.avatar ?? null} name={creator?.name ?? undefined} />
74
+ <View style={{ flex: 1, minWidth: 0 }}>
75
+ <Text
76
+ style={{ fontWeight: theme.typography.fontWeight.semibold, color: theme.colors.text, fontSize: 16, lineHeight: 20 }}
77
+ numberOfLines={isExpanded ? undefined : 1}
78
+ >
79
+ {mr.title ?? 'Untitled merge request'}
80
+ </Text>
81
+ <Text style={{ color: theme.colors.textMuted, fontSize: 12, lineHeight: 16 }} numberOfLines={1}>
82
+ {creator?.name ?? 'Loading...'} · {position}
83
+ </Text>
84
+ </View>
85
+ <Animated.View
86
+ style={{
87
+ transform: [{ rotate: rotate.interpolate({ inputRange: [0, 1], outputRange: ['0deg', '180deg'] }) }],
88
+ }}
89
+ >
90
+ <ChevronDown size={20} color={withAlpha(theme.colors.textMuted, 0.4)} />
91
+ </Animated.View>
92
+ </View>
93
+
94
+ {/* Expanded content */}
95
+ {isExpanded ? (
96
+ <View style={{ marginTop: 16 }}>
97
+ <Text
98
+ style={{
99
+ fontSize: 10,
100
+ fontWeight: '700',
101
+ textTransform: 'uppercase',
102
+ letterSpacing: 0.5,
103
+ color: status.color,
104
+ marginBottom: 8,
105
+ }}
106
+ >
107
+ {status.text}
108
+ </Text>
109
+
110
+ <Text style={{ color: theme.colors.textMuted, fontSize: 12, lineHeight: 16, marginBottom: 12 }}>
111
+ {creator
112
+ ? `${creator.approvedOpenedMergeRequests} approved merge${creator.approvedOpenedMergeRequests !== 1 ? 's' : ''}`
113
+ : 'Loading stats...'}
114
+ </Text>
115
+
116
+ {mr.description ? <MarkdownText markdown={mr.description} variant="mergeRequest" /> : null}
117
+ </View>
118
+ ) : null}
119
+
120
+ {/* Separator */}
121
+ <View style={{ height: 1, backgroundColor: withAlpha(theme.colors.borderStrong, 0.5), marginTop: 12, marginBottom: 12 }} />
122
+
123
+ {/* Action buttons - always visible */}
124
+ <View style={{ flexDirection: 'row', alignItems: 'center', justifyContent: 'space-between' }}>
125
+ <View style={{ flexDirection: 'row', gap: 8 }}>
126
+ <ReviewMergeRequestActionButton
127
+ accessibilityLabel="Reject"
128
+ backgroundColor={theme.colors.danger}
129
+ disabled={!canAct || isAnyProcessing}
130
+ onPress={onReject}
131
+ iconOnly={!isExpanded}
132
+ >
133
+ <View style={{ flexDirection: 'row', alignItems: 'center', gap: isExpanded ? 4 : 0 }}>
134
+ <X size={18} color="#FFFFFF" />
135
+ {isExpanded ? (
136
+ <Text style={{ fontSize: 13, color: '#FFFFFF', fontWeight: theme.typography.fontWeight.semibold }}>Reject</Text>
137
+ ) : null}
138
+ </View>
139
+ </ReviewMergeRequestActionButton>
140
+
141
+ <ReviewMergeRequestActionButton
142
+ accessibilityLabel={!canAct ? 'Not actionable' : isProcessing ? 'Processing' : 'Approve'}
143
+ backgroundColor="#16A34A"
144
+ disabled={!canAct || isAnyProcessing}
145
+ onPress={onApprove}
146
+ iconOnly={!isExpanded}
147
+ >
148
+ {isProcessing ? (
149
+ <View style={{ flexDirection: 'row', alignItems: 'center', gap: isExpanded ? 4 : 0 }}>
150
+ <ActivityIndicator size="small" color="#FFFFFF" />
151
+ {isExpanded ? (
152
+ <Text style={{ fontSize: 13, color: '#FFFFFF', fontWeight: theme.typography.fontWeight.semibold }}>
153
+ Processing
154
+ </Text>
155
+ ) : null}
156
+ </View>
157
+ ) : (
158
+ <View style={{ flexDirection: 'row', alignItems: 'center', gap: isExpanded ? 4 : 0 }}>
159
+ <Check size={18} color="#FFFFFF" />
160
+ {isExpanded ? (
161
+ <Text style={{ fontSize: 13, color: '#FFFFFF', fontWeight: theme.typography.fontWeight.semibold }}>Approve</Text>
162
+ ) : null}
163
+ </View>
164
+ )}
165
+ </ReviewMergeRequestActionButton>
166
+ </View>
167
+
168
+ <ReviewMergeRequestActionButton
169
+ accessibilityLabel="Test"
170
+ backgroundColor={theme.colors.neutral}
171
+ disabled={isBuilding || isTestingThis}
172
+ onPress={onTest}
173
+ iconOnly={!isExpanded}
174
+ >
175
+ {isTestingThis ? (
176
+ <ActivityIndicator size="small" color="#888" />
177
+ ) : (
178
+ <View style={{ flexDirection: 'row', alignItems: 'center', gap: isExpanded ? 4 : 0 }}>
179
+ <Play size={14} color={theme.colors.text} />
180
+ {isExpanded ? (
181
+ <Text style={{ fontSize: 13, color: theme.colors.text, fontWeight: theme.typography.fontWeight.semibold }}>Test</Text>
182
+ ) : null}
183
+ </View>
184
+ )}
185
+ </ReviewMergeRequestActionButton>
186
+ </View>
187
+ </Card>
188
+ </Pressable>
189
+ );
190
+ }
191
+
192
+