@comergehq/studio 0.1.1 → 0.1.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (172) hide show
  1. package/dist/index.js +255 -245
  2. package/dist/index.js.map +1 -1
  3. package/dist/index.mjs +213 -203
  4. package/dist/index.mjs.map +1 -1
  5. package/package.json +9 -6
  6. package/src/components/chat/ChatComposer.tsx +277 -0
  7. package/src/components/chat/ChatHeader.tsx +31 -0
  8. package/src/components/chat/ChatMessageBubble.tsx +69 -0
  9. package/src/components/chat/ChatMessageList.tsx +137 -0
  10. package/src/components/chat/ChatPage.tsx +69 -0
  11. package/src/components/chat/ForkNoticeBanner.tsx +66 -0
  12. package/src/components/chat/MultilineTextInput.tsx +46 -0
  13. package/src/components/chat/ScrollToBottomButton.tsx +78 -0
  14. package/src/components/chat/TypingIndicator.tsx +54 -0
  15. package/src/components/chat/index.ts +28 -0
  16. package/src/components/comments/AppCommentsSheet.tsx +213 -0
  17. package/src/components/comments/CommentRow.tsx +63 -0
  18. package/src/components/comments/formatTimeAgo.ts +3 -0
  19. package/src/components/comments/index.ts +3 -0
  20. package/src/components/comments/useAppComments.ts +74 -0
  21. package/src/components/comments/useAppDetails.ts +35 -0
  22. package/src/components/comments/useIosKeyboardSnapFix.ts +24 -0
  23. package/src/components/dialogs/ConfirmMergeRequestDialog.tsx +156 -0
  24. package/src/components/dialogs/index.ts +4 -0
  25. package/src/components/draw/DrawColorPicker.tsx +77 -0
  26. package/src/components/draw/DrawModeOverlay.tsx +144 -0
  27. package/src/components/draw/DrawSurface.tsx +127 -0
  28. package/src/components/draw/DrawToolbar.tsx +253 -0
  29. package/src/components/draw/index.ts +15 -0
  30. package/src/components/draw/optionalHaptics.ts +15 -0
  31. package/src/components/draw/strokes.ts +21 -0
  32. package/src/components/draw/types.ts +9 -0
  33. package/src/components/floating-draggable-button/FloatingDraggableButton.tsx +323 -0
  34. package/src/components/floating-draggable-button/constants.ts +17 -0
  35. package/src/components/floating-draggable-button/index.ts +4 -0
  36. package/src/components/floating-draggable-button/types.ts +63 -0
  37. package/src/components/icons/MergeIcon.tsx +14 -0
  38. package/src/components/icons/StudioIcons.tsx +66 -0
  39. package/src/components/index.ts +17 -0
  40. package/src/components/merge-requests/MergeRequestStatusCard.tsx +179 -0
  41. package/src/components/merge-requests/ReviewMergeRequestActionButton.tsx +62 -0
  42. package/src/components/merge-requests/ReviewMergeRequestCard.tsx +192 -0
  43. package/src/components/merge-requests/ReviewMergeRequestCarousel.tsx +132 -0
  44. package/src/components/merge-requests/index.ts +7 -0
  45. package/src/components/merge-requests/mergeRequestStatusDisplay.ts +23 -0
  46. package/src/components/merge-requests/toIsoString.ts +9 -0
  47. package/src/components/merge-requests/useControlledExpansion.ts +16 -0
  48. package/src/components/models/index.ts +9 -0
  49. package/src/components/models/types.ts +43 -0
  50. package/src/components/overlays/EdgeGlowFrame.tsx +105 -0
  51. package/src/components/overlays/index.ts +4 -0
  52. package/src/components/preview/PreviewHeroCard.tsx +58 -0
  53. package/src/components/preview/PreviewImage.tsx +22 -0
  54. package/src/components/preview/PreviewMetaRow.tsx +70 -0
  55. package/src/components/preview/PreviewPage.tsx +36 -0
  56. package/src/components/preview/PreviewPlaceholder.tsx +72 -0
  57. package/src/components/preview/PreviewStatusBadge.tsx +63 -0
  58. package/src/components/preview/StatsBar.tsx +109 -0
  59. package/src/components/preview/index.ts +22 -0
  60. package/src/components/primitives/Avatar.tsx +68 -0
  61. package/src/components/primitives/Button.tsx +102 -0
  62. package/src/components/primitives/Card.tsx +30 -0
  63. package/src/components/primitives/Divider.tsx +17 -0
  64. package/src/components/primitives/Icon.tsx +40 -0
  65. package/src/components/primitives/MarkdownText.tsx +72 -0
  66. package/src/components/primitives/Modal.tsx +53 -0
  67. package/src/components/primitives/Surface.tsx +42 -0
  68. package/src/components/primitives/Text.tsx +83 -0
  69. package/src/components/primitives/index.ts +35 -0
  70. package/src/components/primitives/types.ts +30 -0
  71. package/src/components/studio-sheet/StudioBottomSheet.tsx +114 -0
  72. package/src/components/studio-sheet/StudioSheetBackground.tsx +63 -0
  73. package/src/components/studio-sheet/StudioSheetHeader.tsx +35 -0
  74. package/src/components/studio-sheet/StudioSheetHeaderIconButton.tsx +109 -0
  75. package/src/components/studio-sheet/StudioSheetPager.tsx +66 -0
  76. package/src/components/studio-sheet/index.ts +18 -0
  77. package/src/components/studio-sheet/types.ts +5 -0
  78. package/src/components/utils/color.ts +25 -0
  79. package/src/components/utils/formatTimeAgo.ts +19 -0
  80. package/src/core/logger.ts +42 -0
  81. package/src/core/services/http/baseUrl.ts +3 -0
  82. package/src/core/services/http/index.ts +128 -0
  83. package/src/core/services/http/public.ts +14 -0
  84. package/src/core/services/supabase/auth.ts +41 -0
  85. package/src/core/services/supabase/client.ts +43 -0
  86. package/src/core/services/supabase/index.ts +7 -0
  87. package/src/data/agent/remote.ts +30 -0
  88. package/src/data/agent/repository.ts +34 -0
  89. package/src/data/agent/types.ts +28 -0
  90. package/src/data/apps/bundles/remote.ts +47 -0
  91. package/src/data/apps/bundles/repository.ts +35 -0
  92. package/src/data/apps/bundles/types.ts +27 -0
  93. package/src/data/apps/images/remote.ts +61 -0
  94. package/src/data/apps/images/repository.ts +47 -0
  95. package/src/data/apps/remote.ts +97 -0
  96. package/src/data/apps/repository.ts +185 -0
  97. package/src/data/apps/types.ts +206 -0
  98. package/src/data/attachment/remote.ts +32 -0
  99. package/src/data/attachment/repository.ts +40 -0
  100. package/src/data/attachment/types.ts +42 -0
  101. package/src/data/base-remote.ts +3 -0
  102. package/src/data/base-repository.ts +11 -0
  103. package/src/data/comments/likes/remote.ts +87 -0
  104. package/src/data/comments/likes/repository.ts +61 -0
  105. package/src/data/comments/likes/types.ts +47 -0
  106. package/src/data/comments/remote.ts +71 -0
  107. package/src/data/comments/repository.ts +53 -0
  108. package/src/data/comments/types.ts +60 -0
  109. package/src/data/github/remote.ts +23 -0
  110. package/src/data/github/repository.ts +35 -0
  111. package/src/data/github/types.ts +23 -0
  112. package/src/data/home/remote.ts +24 -0
  113. package/src/data/home/repository.ts +28 -0
  114. package/src/data/home/types.ts +70 -0
  115. package/src/data/index.ts +3 -0
  116. package/src/data/likes/remote.ts +57 -0
  117. package/src/data/likes/repository.ts +47 -0
  118. package/src/data/likes/types.ts +46 -0
  119. package/src/data/me/remote.ts +28 -0
  120. package/src/data/me/repository.ts +30 -0
  121. package/src/data/me/types.ts +14 -0
  122. package/src/data/merge-requests/remote.ts +76 -0
  123. package/src/data/merge-requests/repository.ts +66 -0
  124. package/src/data/merge-requests/types.ts +33 -0
  125. package/src/data/messages/remote.ts +21 -0
  126. package/src/data/messages/repository.ts +104 -0
  127. package/src/data/messages/types.ts +20 -0
  128. package/src/data/public/studio-config/remote.ts +19 -0
  129. package/src/data/public/studio-config/repository.ts +23 -0
  130. package/src/data/public/studio-config/types.ts +6 -0
  131. package/src/data/ratings/remote.ts +76 -0
  132. package/src/data/ratings/repository.ts +63 -0
  133. package/src/data/ratings/types.ts +57 -0
  134. package/src/data/threads/remote.ts +40 -0
  135. package/src/data/threads/repository.ts +41 -0
  136. package/src/data/threads/types.ts +25 -0
  137. package/src/data/types.ts +8 -0
  138. package/src/data/users/remote.ts +31 -0
  139. package/src/data/users/repository.ts +45 -0
  140. package/src/data/users/types.ts +15 -0
  141. package/src/index.ts +6 -0
  142. package/src/studio/ComergeStudio.tsx +246 -0
  143. package/src/studio/bootstrap/StudioBootstrap.tsx +45 -0
  144. package/src/studio/bootstrap/useStudioBootstrap.ts +51 -0
  145. package/src/studio/hooks/useApp.ts +83 -0
  146. package/src/studio/hooks/useAppStats.ts +111 -0
  147. package/src/studio/hooks/useAttachmentUpload.ts +59 -0
  148. package/src/studio/hooks/useBundleManager.ts +389 -0
  149. package/src/studio/hooks/useMergeRequests.ts +173 -0
  150. package/src/studio/hooks/useStudioActions.ts +96 -0
  151. package/src/studio/hooks/useThreadMessages.ts +85 -0
  152. package/src/studio/lib/chat.ts +34 -0
  153. package/src/studio/ui/ChatPanel.tsx +154 -0
  154. package/src/studio/ui/ConfirmMergeFlow.tsx +55 -0
  155. package/src/studio/ui/PreviewPanel.tsx +131 -0
  156. package/src/studio/ui/RuntimeRenderer.tsx +40 -0
  157. package/src/studio/ui/StudioOverlay.tsx +257 -0
  158. package/src/studio/ui/preview-panel/PressableCardRow.tsx +49 -0
  159. package/src/studio/ui/preview-panel/PreviewCollaborateSection.tsx +174 -0
  160. package/src/studio/ui/preview-panel/PreviewCustomizeSection.tsx +160 -0
  161. package/src/studio/ui/preview-panel/PreviewHeroSection.tsx +56 -0
  162. package/src/studio/ui/preview-panel/PreviewMetaSection.tsx +67 -0
  163. package/src/studio/ui/preview-panel/PreviewPanelHeader.tsx +48 -0
  164. package/src/studio/ui/preview-panel/SectionTitle.tsx +31 -0
  165. package/src/studio/ui/preview-panel/usePreviewPanelData.ts +132 -0
  166. package/src/studio/ui/preview-panel/utils.ts +29 -0
  167. package/src/theme/index.ts +5 -0
  168. package/src/theme/tokens.ts +118 -0
  169. package/src/theme/types.ts +90 -0
  170. package/src/theme/useTheme.ts +11 -0
  171. package/dist/assets/images/merge.svg +0 -3
  172. package/dist/merge-72UG27QV.svg +0 -3
@@ -0,0 +1,156 @@
1
+ import * as React from 'react';
2
+ import { Pressable, View } from 'react-native';
3
+
4
+ import type { MergeRequestSummary } from '../models/types';
5
+ import { Modal } from '../primitives/Modal';
6
+ import { Text } from '../primitives/Text';
7
+ import { useTheme } from '../../theme';
8
+
9
+ export type ConfirmMergeRequestDialogProps = {
10
+ visible: boolean;
11
+ onOpenChange: (open: boolean) => void;
12
+ mergeRequest: MergeRequestSummary | null;
13
+ approveDisabled?: boolean;
14
+ /**
15
+ * disables the "Test edits first" button and changes its label to "Preparing…".
16
+ */
17
+ isBuilding?: boolean;
18
+ onConfirm: () => void | Promise<void>;
19
+ onTestFirst: (mr: MergeRequestSummary) => void | Promise<void>;
20
+ };
21
+
22
+ export function ConfirmMergeRequestDialog({
23
+ visible,
24
+ onOpenChange,
25
+ mergeRequest,
26
+ approveDisabled,
27
+ isBuilding,
28
+ onConfirm,
29
+ onTestFirst,
30
+ }: ConfirmMergeRequestDialogProps) {
31
+ const theme = useTheme();
32
+
33
+ const close = React.useCallback(() => onOpenChange(false), [onOpenChange]);
34
+
35
+ const canConfirm = Boolean(mergeRequest) && !approveDisabled;
36
+
37
+ const handleConfirm = React.useCallback(() => {
38
+ if (!mergeRequest) return;
39
+ onOpenChange(false);
40
+ void onConfirm();
41
+ }, [mergeRequest, onConfirm, onOpenChange]);
42
+
43
+ const handleTestFirst = React.useCallback(() => {
44
+ if (!mergeRequest) return;
45
+ onOpenChange(false);
46
+ void onTestFirst(mergeRequest);
47
+ }, [mergeRequest, onOpenChange, onTestFirst]);
48
+
49
+ const fullWidthButtonBase = {
50
+ height: 40,
51
+ borderRadius: 999,
52
+ alignItems: 'center' as const,
53
+ justifyContent: 'center' as const,
54
+ alignSelf: 'stretch' as const,
55
+ };
56
+
57
+ return (
58
+ <Modal
59
+ visible={visible}
60
+ onRequestClose={close}
61
+ contentStyle={{
62
+ borderRadius: theme.radii.sm,
63
+ padding: 24,
64
+ backgroundColor: theme.colors.background,
65
+ }}
66
+ >
67
+ <View>
68
+ <Text
69
+ style={{
70
+ color: theme.colors.text,
71
+ fontSize: 18,
72
+ lineHeight: 24,
73
+ fontWeight: theme.typography.fontWeight.semibold,
74
+ }}
75
+ >
76
+ Are you sure you want to approve this merge request?
77
+ </Text>
78
+ </View>
79
+
80
+ <View style={{ marginTop: 16 }}>
81
+ {/* Primary */}
82
+ <View
83
+ style={[
84
+ fullWidthButtonBase,
85
+ {
86
+ backgroundColor: theme.colors.primary,
87
+ opacity: canConfirm ? 1 : 0.5,
88
+ },
89
+ ]}
90
+ >
91
+ <Pressable
92
+ accessibilityRole="button"
93
+ accessibilityLabel="Approve Merge"
94
+ disabled={!canConfirm}
95
+ onPress={handleConfirm}
96
+ style={[fullWidthButtonBase, { flex: 1 }]}
97
+ >
98
+ <Text style={{ textAlign: 'center', color: theme.colors.onPrimary }}>
99
+ Approve Merge
100
+ </Text>
101
+ </Pressable>
102
+ </View>
103
+
104
+ {/* Test first (outline) */}
105
+ <View style={{ height: 8 }} />
106
+ <View
107
+ style={[
108
+ fullWidthButtonBase,
109
+ {
110
+ backgroundColor: theme.colors.background,
111
+ borderWidth: 1,
112
+ borderColor: theme.colors.border,
113
+ opacity: isBuilding || !mergeRequest ? 0.5 : 1,
114
+ },
115
+ ]}
116
+ >
117
+ <Pressable
118
+ accessibilityRole="button"
119
+ accessibilityLabel={isBuilding ? 'Preparing…' : 'Test edits first'}
120
+ disabled={isBuilding || !mergeRequest}
121
+ onPress={handleTestFirst}
122
+ style={[fullWidthButtonBase, { flex: 1 }]}
123
+ >
124
+ <Text style={{ textAlign: 'center', color: theme.colors.text }}>
125
+ {isBuilding ? 'Preparing…' : 'Test edits first'}
126
+ </Text>
127
+ </Pressable>
128
+ </View>
129
+
130
+ {/* Cancel (outline) */}
131
+ <View style={{ height: 8 }} />
132
+ <View
133
+ style={[
134
+ fullWidthButtonBase,
135
+ {
136
+ backgroundColor: theme.colors.background,
137
+ borderWidth: 1,
138
+ borderColor: theme.colors.border,
139
+ },
140
+ ]}
141
+ >
142
+ <Pressable
143
+ accessibilityRole="button"
144
+ accessibilityLabel="Cancel"
145
+ onPress={close}
146
+ style={[fullWidthButtonBase, { flex: 1 }]}
147
+ >
148
+ <Text style={{ textAlign: 'center', color: theme.colors.text }}>Cancel</Text>
149
+ </Pressable>
150
+ </View>
151
+ </View>
152
+ </Modal>
153
+ );
154
+ }
155
+
156
+
@@ -0,0 +1,4 @@
1
+ export { ConfirmMergeRequestDialog } from './ConfirmMergeRequestDialog';
2
+ export type { ConfirmMergeRequestDialogProps } from './ConfirmMergeRequestDialog';
3
+
4
+
@@ -0,0 +1,77 @@
1
+ import * as React from 'react';
2
+ import { Pressable, View, type ViewStyle } from 'react-native';
3
+
4
+ import { useTheme } from '../../theme';
5
+
6
+ export type DrawColorPickerProps = {
7
+ colors: string[];
8
+ selected: string;
9
+ expanded: boolean;
10
+ onToggle: () => void;
11
+ onSelect: (color: string) => void;
12
+ style?: ViewStyle;
13
+ };
14
+
15
+ export function DrawColorPicker({
16
+ colors,
17
+ selected,
18
+ expanded,
19
+ onToggle,
20
+ onSelect,
21
+ style,
22
+ }: DrawColorPickerProps) {
23
+ useTheme();
24
+
25
+ const isWhite = (c: string) => c.toUpperCase() === '#FFFFFF';
26
+
27
+ const swatchStyle = (c: string, isSelected: boolean): ViewStyle => {
28
+ const base: ViewStyle = {
29
+ width: 28,
30
+ height: 28,
31
+ borderRadius: 14,
32
+ backgroundColor: c,
33
+ borderWidth: 2,
34
+ borderColor: 'transparent',
35
+ alignItems: 'center',
36
+ justifyContent: 'center',
37
+ };
38
+
39
+ const selectedStyle: ViewStyle = isSelected
40
+ ? {
41
+ borderColor: '#FFFFFF',
42
+ shadowColor: '#000',
43
+ shadowOffset: { width: 0, height: 2 },
44
+ shadowOpacity: 0.3,
45
+ shadowRadius: 3,
46
+ elevation: 4,
47
+ }
48
+ : {};
49
+
50
+ const whiteStyle: ViewStyle = isWhite(c) ? { borderColor: 'rgba(0, 0, 0, 0.2)' } : {};
51
+
52
+ return { ...base, ...selectedStyle, ...whiteStyle };
53
+ };
54
+
55
+ if (!expanded) {
56
+ return (
57
+ <Pressable onPress={onToggle} style={[swatchStyle(selected, true), style]} />
58
+ );
59
+ }
60
+
61
+ return (
62
+ <View style={[{ flexDirection: 'row', alignItems: 'center', gap: 8 }, style]}>
63
+ {colors.map((c, idx) => (
64
+ <Pressable
65
+ key={`${c}-${idx}`}
66
+ onPress={() => {
67
+ onSelect(c);
68
+ onToggle();
69
+ }}
70
+ style={swatchStyle(c, selected === c)}
71
+ />
72
+ ))}
73
+ </View>
74
+ );
75
+ }
76
+
77
+
@@ -0,0 +1,144 @@
1
+ import * as React from 'react';
2
+ import { StyleSheet, View, type ViewStyle } from 'react-native';
3
+ import { captureRef } from 'react-native-view-shot';
4
+
5
+ import { useTheme } from '../../theme';
6
+ import { EdgeGlowFrame } from '../overlays/EdgeGlowFrame';
7
+ import { DrawSurface } from './DrawSurface';
8
+ import { DrawToolbar } from './DrawToolbar';
9
+ import type { Stroke } from './types';
10
+
11
+ export type DrawModeOverlayProps = {
12
+ visible: boolean;
13
+ captureTargetRef: React.RefObject<View | null>;
14
+ onCancel: () => void;
15
+ onCapture: (dataUrl: string) => void;
16
+ /**
17
+ * Custom palette (theme-first). If omitted, uses a theme-derived palette.
18
+ */
19
+ palette?: string[];
20
+ strokeWidth?: number;
21
+ style?: ViewStyle;
22
+ /**
23
+ * Render icons for toolbar actions.
24
+ */
25
+ renderUndoIcon?: () => React.ReactNode;
26
+ renderCancelIcon?: () => React.ReactNode;
27
+ renderDoneIcon?: () => React.ReactNode;
28
+ renderDragHandle?: () => React.ReactNode;
29
+ };
30
+
31
+ export function DrawModeOverlay({
32
+ visible,
33
+ captureTargetRef,
34
+ onCancel,
35
+ onCapture,
36
+ palette,
37
+ strokeWidth = 4,
38
+ style,
39
+ renderUndoIcon,
40
+ renderCancelIcon,
41
+ renderDoneIcon,
42
+ renderDragHandle,
43
+ }: DrawModeOverlayProps) {
44
+ const theme = useTheme();
45
+
46
+ const defaultPalette = React.useMemo(
47
+ () => [
48
+ '#EF4444', // Red
49
+ '#EAB308', // Yellow
50
+ '#22C55E', // Green
51
+ '#3B82F6', // Blue
52
+ '#FFFFFF', // White
53
+ '#000000', // Black
54
+ ],
55
+ []
56
+ );
57
+
58
+ const colors = palette && palette.length > 0 ? palette : defaultPalette;
59
+ const [selectedColor, setSelectedColor] = React.useState(colors[0] ?? '#EF4444');
60
+ const [strokes, setStrokes] = React.useState<Stroke[]>([]);
61
+ const [capturing, setCapturing] = React.useState(false);
62
+ const [hideUi, setHideUi] = React.useState(false);
63
+
64
+ React.useEffect(() => {
65
+ if (!visible) return;
66
+ setStrokes([]);
67
+ setSelectedColor(colors[0] ?? '#EF4444');
68
+ setCapturing(false);
69
+ setHideUi(false);
70
+ }, [colors, visible]);
71
+
72
+ const canUndo = strokes.length > 0;
73
+
74
+ const handleUndo = React.useCallback(() => {
75
+ setStrokes((prev) => prev.slice(0, -1));
76
+ }, []);
77
+
78
+ const handleCancel = React.useCallback(() => {
79
+ setStrokes([]);
80
+ onCancel();
81
+ }, [onCancel]);
82
+
83
+ const handleDone = React.useCallback(async () => {
84
+ if (!captureTargetRef.current || capturing) return;
85
+ try {
86
+ setCapturing(true);
87
+ setHideUi(true);
88
+ await new Promise((r) => requestAnimationFrame(() => r(null)));
89
+ await new Promise((r) => requestAnimationFrame(() => r(null)));
90
+
91
+ const base64 = await captureRef(captureTargetRef, {
92
+ format: 'png',
93
+ quality: 0.9,
94
+ result: 'base64',
95
+ });
96
+ setCapturing(false);
97
+ setHideUi(false);
98
+ setStrokes([]);
99
+ onCapture(`data:image/png;base64,${base64}`);
100
+ } catch {
101
+ setCapturing(false);
102
+ setHideUi(false);
103
+ }
104
+ }, [captureTargetRef, capturing, onCapture]);
105
+
106
+ if (!visible) return null;
107
+
108
+ return (
109
+ <View style={[StyleSheet.absoluteFill, styles.root, style]} pointerEvents="box-none">
110
+ <EdgeGlowFrame visible={!hideUi} role="danger" thickness={50} intensity={1} />
111
+
112
+ <DrawSurface
113
+ color={selectedColor}
114
+ strokeWidth={strokeWidth}
115
+ strokes={strokes}
116
+ onAddStroke={(stroke) => setStrokes((prev) => [...prev, stroke])}
117
+ />
118
+
119
+ <DrawToolbar
120
+ hidden={hideUi}
121
+ capturing={capturing}
122
+ colors={colors}
123
+ selectedColor={selectedColor}
124
+ onSelectColor={setSelectedColor}
125
+ canUndo={canUndo}
126
+ onUndo={handleUndo}
127
+ onCancel={handleCancel}
128
+ onDone={() => void handleDone()}
129
+ renderUndoIcon={renderUndoIcon}
130
+ renderCancelIcon={renderCancelIcon}
131
+ renderDoneIcon={renderDoneIcon}
132
+ renderDragHandle={renderDragHandle}
133
+ />
134
+ </View>
135
+ );
136
+ }
137
+
138
+ const styles = StyleSheet.create({
139
+ root: {
140
+ zIndex: 9999,
141
+ },
142
+ });
143
+
144
+
@@ -0,0 +1,127 @@
1
+ import * as React from 'react';
2
+ import { PanResponder, StyleSheet, View, type GestureResponderEvent, type PanResponderGestureState, type ViewStyle } from 'react-native';
3
+ import Svg, { Path } from 'react-native-svg';
4
+
5
+ import type { Point, Stroke } from './types';
6
+ import { pointsToSmoothPath } from './strokes';
7
+
8
+ export type DrawSurfaceProps = {
9
+ color: string;
10
+ strokeWidth: number;
11
+ strokes: Stroke[];
12
+ onAddStroke: (stroke: Stroke) => void;
13
+ style?: ViewStyle;
14
+ minDistance?: number;
15
+ };
16
+
17
+ export function DrawSurface({
18
+ color,
19
+ strokeWidth,
20
+ strokes,
21
+ onAddStroke,
22
+ style,
23
+ minDistance = 1,
24
+ }: DrawSurfaceProps) {
25
+ const [renderTick, setRenderTick] = React.useState(0);
26
+ const currentPointsRef = React.useRef<Point[]>([]);
27
+ const rafRef = React.useRef<number | null>(null);
28
+
29
+ const triggerRender = React.useCallback(() => {
30
+ if (rafRef.current !== null) return;
31
+ rafRef.current = requestAnimationFrame(() => {
32
+ rafRef.current = null;
33
+ setRenderTick((n) => n + 1);
34
+ });
35
+ }, []);
36
+
37
+ React.useEffect(() => () => {
38
+ if (rafRef.current !== null) cancelAnimationFrame(rafRef.current);
39
+ }, []);
40
+
41
+ const onStart = React.useCallback((e: GestureResponderEvent) => {
42
+ const { locationX, locationY } = e.nativeEvent;
43
+ currentPointsRef.current = [{ x: locationX, y: locationY }];
44
+ triggerRender();
45
+ }, [triggerRender]);
46
+
47
+ const onMove = React.useCallback((e: GestureResponderEvent, _g: PanResponderGestureState) => {
48
+ const { locationX, locationY } = e.nativeEvent;
49
+ const pts = currentPointsRef.current;
50
+ if (pts.length > 0) {
51
+ const last = pts[pts.length - 1];
52
+ const dx = locationX - last.x;
53
+ const dy = locationY - last.y;
54
+ const dist = Math.sqrt(dx * dx + dy * dy);
55
+ if (dist < minDistance) return;
56
+ }
57
+ currentPointsRef.current = [...pts, { x: locationX, y: locationY }];
58
+ triggerRender();
59
+ }, [minDistance, triggerRender]);
60
+
61
+ const onEnd = React.useCallback(() => {
62
+ const points = currentPointsRef.current;
63
+ if (points.length > 0) {
64
+ onAddStroke({ points, color, width: strokeWidth });
65
+ }
66
+ currentPointsRef.current = [];
67
+ triggerRender();
68
+ }, [color, onAddStroke, strokeWidth, triggerRender]);
69
+
70
+ const panResponder = React.useMemo(
71
+ () =>
72
+ PanResponder.create({
73
+ onStartShouldSetPanResponder: () => true,
74
+ onMoveShouldSetPanResponder: () => true,
75
+ onPanResponderGrant: onStart,
76
+ onPanResponderMove: onMove,
77
+ onPanResponderRelease: onEnd,
78
+ onPanResponderTerminate: onEnd,
79
+ }),
80
+ [onEnd, onMove, onStart]
81
+ );
82
+
83
+ const currentPath = pointsToSmoothPath(currentPointsRef.current);
84
+
85
+ // renderTick is used to force re-render when refs change
86
+ void renderTick;
87
+
88
+ return (
89
+ <View style={[StyleSheet.absoluteFill, styles.container, style]} {...panResponder.panHandlers}>
90
+ <Svg style={StyleSheet.absoluteFill} width="100%" height="100%">
91
+ {strokes.map((s, idx) => {
92
+ const d = pointsToSmoothPath(s.points);
93
+ if (!d) return null;
94
+ return (
95
+ <Path
96
+ key={idx}
97
+ d={d}
98
+ stroke={s.color}
99
+ strokeWidth={s.width}
100
+ strokeLinecap="round"
101
+ strokeLinejoin="round"
102
+ fill="none"
103
+ />
104
+ );
105
+ })}
106
+ {currentPath ? (
107
+ <Path
108
+ d={currentPath}
109
+ stroke={color}
110
+ strokeWidth={strokeWidth}
111
+ strokeLinecap="round"
112
+ strokeLinejoin="round"
113
+ fill="none"
114
+ />
115
+ ) : null}
116
+ </Svg>
117
+ </View>
118
+ );
119
+ }
120
+
121
+ const styles = StyleSheet.create({
122
+ container: {
123
+ zIndex: 5,
124
+ },
125
+ });
126
+
127
+