@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,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,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
|
+
|