@appsupportkit/support-sdk 0.1.0

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 (87) hide show
  1. package/lib/commonjs/api.js +93 -0
  2. package/lib/commonjs/api.js.map +1 -0
  3. package/lib/commonjs/components/BottomSheet.js +158 -0
  4. package/lib/commonjs/components/BottomSheet.js.map +1 -0
  5. package/lib/commonjs/components/SupportButton.js +244 -0
  6. package/lib/commonjs/components/SupportButton.js.map +1 -0
  7. package/lib/commonjs/components/icons.js +236 -0
  8. package/lib/commonjs/components/icons.js.map +1 -0
  9. package/lib/commonjs/components/primitives.js +284 -0
  10. package/lib/commonjs/components/primitives.js.map +1 -0
  11. package/lib/commonjs/components/screens/ComposeScreen.js +171 -0
  12. package/lib/commonjs/components/screens/ComposeScreen.js.map +1 -0
  13. package/lib/commonjs/components/screens/HomeScreen.js +227 -0
  14. package/lib/commonjs/components/screens/HomeScreen.js.map +1 -0
  15. package/lib/commonjs/components/screens/ThreadScreen.js +213 -0
  16. package/lib/commonjs/components/screens/ThreadScreen.js.map +1 -0
  17. package/lib/commonjs/index.js +58 -0
  18. package/lib/commonjs/index.js.map +1 -0
  19. package/lib/commonjs/package.json +1 -0
  20. package/lib/commonjs/theme.js +29 -0
  21. package/lib/commonjs/theme.js.map +1 -0
  22. package/lib/commonjs/types.js +14 -0
  23. package/lib/commonjs/types.js.map +1 -0
  24. package/lib/commonjs/utils.js +63 -0
  25. package/lib/commonjs/utils.js.map +1 -0
  26. package/lib/module/api.js +84 -0
  27. package/lib/module/api.js.map +1 -0
  28. package/lib/module/components/BottomSheet.js +153 -0
  29. package/lib/module/components/BottomSheet.js.map +1 -0
  30. package/lib/module/components/SupportButton.js +240 -0
  31. package/lib/module/components/SupportButton.js.map +1 -0
  32. package/lib/module/components/icons.js +225 -0
  33. package/lib/module/components/icons.js.map +1 -0
  34. package/lib/module/components/primitives.js +270 -0
  35. package/lib/module/components/primitives.js.map +1 -0
  36. package/lib/module/components/screens/ComposeScreen.js +167 -0
  37. package/lib/module/components/screens/ComposeScreen.js.map +1 -0
  38. package/lib/module/components/screens/HomeScreen.js +223 -0
  39. package/lib/module/components/screens/HomeScreen.js.map +1 -0
  40. package/lib/module/components/screens/ThreadScreen.js +209 -0
  41. package/lib/module/components/screens/ThreadScreen.js.map +1 -0
  42. package/lib/module/index.js +7 -0
  43. package/lib/module/index.js.map +1 -0
  44. package/lib/module/package.json +1 -0
  45. package/lib/module/theme.js +23 -0
  46. package/lib/module/theme.js.map +1 -0
  47. package/lib/module/types.js +10 -0
  48. package/lib/module/types.js.map +1 -0
  49. package/lib/module/utils.js +57 -0
  50. package/lib/module/utils.js.map +1 -0
  51. package/lib/typescript/api.d.ts +37 -0
  52. package/lib/typescript/api.d.ts.map +1 -0
  53. package/lib/typescript/components/BottomSheet.d.ts +9 -0
  54. package/lib/typescript/components/BottomSheet.d.ts.map +1 -0
  55. package/lib/typescript/components/SupportButton.d.ts +8 -0
  56. package/lib/typescript/components/SupportButton.d.ts.map +1 -0
  57. package/lib/typescript/components/icons.d.ts +32 -0
  58. package/lib/typescript/components/icons.d.ts.map +1 -0
  59. package/lib/typescript/components/primitives.d.ts +60 -0
  60. package/lib/typescript/components/primitives.d.ts.map +1 -0
  61. package/lib/typescript/components/screens/ComposeScreen.d.ts +10 -0
  62. package/lib/typescript/components/screens/ComposeScreen.d.ts.map +1 -0
  63. package/lib/typescript/components/screens/HomeScreen.d.ts +13 -0
  64. package/lib/typescript/components/screens/HomeScreen.d.ts.map +1 -0
  65. package/lib/typescript/components/screens/ThreadScreen.d.ts +10 -0
  66. package/lib/typescript/components/screens/ThreadScreen.d.ts.map +1 -0
  67. package/lib/typescript/index.d.ts +6 -0
  68. package/lib/typescript/index.d.ts.map +1 -0
  69. package/lib/typescript/theme.d.ts +13 -0
  70. package/lib/typescript/theme.d.ts.map +1 -0
  71. package/lib/typescript/types.d.ts +61 -0
  72. package/lib/typescript/types.d.ts.map +1 -0
  73. package/lib/typescript/utils.d.ts +11 -0
  74. package/lib/typescript/utils.d.ts.map +1 -0
  75. package/package.json +51 -0
  76. package/src/api.ts +125 -0
  77. package/src/components/BottomSheet.tsx +173 -0
  78. package/src/components/SupportButton.tsx +265 -0
  79. package/src/components/icons.tsx +189 -0
  80. package/src/components/primitives.tsx +298 -0
  81. package/src/components/screens/ComposeScreen.tsx +163 -0
  82. package/src/components/screens/HomeScreen.tsx +234 -0
  83. package/src/components/screens/ThreadScreen.tsx +227 -0
  84. package/src/index.tsx +24 -0
  85. package/src/theme.ts +39 -0
  86. package/src/types.ts +71 -0
  87. package/src/utils.ts +63 -0
@@ -0,0 +1,189 @@
1
+ // Minimal geometric icons built from Views — keeps the SDK free of
2
+ // react-native-svg / icon-font dependencies. 1.5px strokes to match the
3
+ // Lucide icons used on the web dashboard.
4
+ import { View, type ViewStyle } from 'react-native';
5
+
6
+ const STROKE = 1.5;
7
+
8
+ export function ChatIcon({ color, size = 16 }: { color: string; size?: number }) {
9
+ const dot: ViewStyle = {
10
+ width: size * 0.11,
11
+ height: size * 0.11,
12
+ borderRadius: size,
13
+ backgroundColor: color,
14
+ };
15
+ return (
16
+ <View
17
+ style={{
18
+ width: size,
19
+ height: size * 0.8,
20
+ borderWidth: STROKE,
21
+ borderColor: color,
22
+ borderRadius: size * 0.25,
23
+ flexDirection: 'row',
24
+ alignItems: 'center',
25
+ justifyContent: 'center',
26
+ gap: size * 0.11,
27
+ }}
28
+ >
29
+ <View style={dot} />
30
+ <View style={dot} />
31
+ <View style={dot} />
32
+ </View>
33
+ );
34
+ }
35
+
36
+ export function PlusCircleIcon({ color, size = 16 }: { color: string; size?: number }) {
37
+ const bar = size * 0.45;
38
+ return (
39
+ <View
40
+ style={{
41
+ width: size,
42
+ height: size,
43
+ borderWidth: STROKE,
44
+ borderColor: color,
45
+ borderRadius: size,
46
+ alignItems: 'center',
47
+ justifyContent: 'center',
48
+ }}
49
+ >
50
+ <View style={{ width: bar, height: STROKE, backgroundColor: color }} />
51
+ <View
52
+ style={{
53
+ width: STROKE,
54
+ height: bar,
55
+ backgroundColor: color,
56
+ position: 'absolute',
57
+ }}
58
+ />
59
+ </View>
60
+ );
61
+ }
62
+
63
+ export function AlertCircleIcon({ color, size = 16 }: { color: string; size?: number }) {
64
+ return (
65
+ <View
66
+ style={{
67
+ width: size,
68
+ height: size,
69
+ borderWidth: STROKE,
70
+ borderColor: color,
71
+ borderRadius: size,
72
+ alignItems: 'center',
73
+ paddingTop: size * 0.2,
74
+ }}
75
+ >
76
+ <View style={{ width: STROKE, height: size * 0.32, backgroundColor: color }} />
77
+ <View
78
+ style={{
79
+ width: STROKE + 0.5,
80
+ height: STROKE + 0.5,
81
+ borderRadius: 2,
82
+ backgroundColor: color,
83
+ marginTop: size * 0.09,
84
+ }}
85
+ />
86
+ </View>
87
+ );
88
+ }
89
+
90
+ /** Chevron built from two borders, rotated. direction: right | left | down. */
91
+ export function Chevron({
92
+ color,
93
+ size = 10,
94
+ direction = 'right',
95
+ }: {
96
+ color: string;
97
+ size?: number;
98
+ direction?: 'right' | 'left' | 'down';
99
+ }) {
100
+ const rotation =
101
+ direction === 'right' ? '-45deg' : direction === 'left' ? '135deg' : '45deg';
102
+ return (
103
+ <View
104
+ style={{
105
+ width: size * 0.7,
106
+ height: size * 0.7,
107
+ borderRightWidth: STROKE,
108
+ borderBottomWidth: STROKE,
109
+ borderColor: color,
110
+ transform: [{ rotate: rotation }],
111
+ }}
112
+ />
113
+ );
114
+ }
115
+
116
+ export function CloseIcon({ color, size = 14 }: { color: string; size?: number }) {
117
+ const bar: ViewStyle = {
118
+ position: 'absolute',
119
+ width: size,
120
+ height: STROKE,
121
+ backgroundColor: color,
122
+ borderRadius: 1,
123
+ };
124
+ return (
125
+ <View style={{ width: size, height: size, alignItems: 'center', justifyContent: 'center' }}>
126
+ <View style={[bar, { transform: [{ rotate: '45deg' }] }]} />
127
+ <View style={[bar, { transform: [{ rotate: '-45deg' }] }]} />
128
+ </View>
129
+ );
130
+ }
131
+
132
+ export function CheckIcon({ color, size = 16 }: { color: string; size?: number }) {
133
+ return (
134
+ <View
135
+ style={{
136
+ width: size * 0.9,
137
+ height: size * 0.5,
138
+ borderLeftWidth: STROKE + 0.5,
139
+ borderBottomWidth: STROKE + 0.5,
140
+ borderColor: color,
141
+ transform: [{ rotate: '-45deg' }],
142
+ marginBottom: size * 0.15,
143
+ }}
144
+ />
145
+ );
146
+ }
147
+
148
+ /** The chat-bubble mark on the floating support button. */
149
+ export function BubbleMark({ color, size = 20 }: { color: string; size?: number }) {
150
+ const dot: ViewStyle = {
151
+ width: size * 0.12,
152
+ height: size * 0.12,
153
+ borderRadius: size,
154
+ backgroundColor: color,
155
+ };
156
+ return (
157
+ <View style={{ alignItems: 'flex-start' }}>
158
+ <View
159
+ style={{
160
+ width: size,
161
+ height: size * 0.78,
162
+ borderWidth: STROKE + 0.25,
163
+ borderColor: color,
164
+ borderRadius: size * 0.28,
165
+ flexDirection: 'row',
166
+ alignItems: 'center',
167
+ justifyContent: 'center',
168
+ gap: size * 0.1,
169
+ }}
170
+ >
171
+ <View style={dot} />
172
+ <View style={dot} />
173
+ <View style={dot} />
174
+ </View>
175
+ <View
176
+ style={{
177
+ width: size * 0.22,
178
+ height: size * 0.22,
179
+ borderLeftWidth: STROKE + 0.25,
180
+ borderBottomWidth: STROKE + 0.25,
181
+ borderColor: color,
182
+ marginLeft: size * 0.18,
183
+ marginTop: -STROKE,
184
+ transform: [{ skewX: '-12deg' }],
185
+ }}
186
+ />
187
+ </View>
188
+ );
189
+ }
@@ -0,0 +1,298 @@
1
+ import { useEffect, useRef, type ReactNode } from 'react';
2
+ import {
3
+ Animated,
4
+ Pressable,
5
+ Text,
6
+ View,
7
+ type StyleProp,
8
+ type TextStyle,
9
+ type ViewStyle,
10
+ } from 'react-native';
11
+ import type { SupportTheme } from '../theme';
12
+ import type { QueryStatus, QueryType } from '../types';
13
+ import { QUERY_TYPE_LABEL } from '../types';
14
+ import { AlertCircleIcon, ChatIcon, PlusCircleIcon } from './icons';
15
+
16
+ export function typeIcon(type: QueryType, color: string, size = 15): ReactNode {
17
+ if (type === 'bug_report') return <AlertCircleIcon color={color} size={size} />;
18
+ if (type === 'feature_request') return <PlusCircleIcon color={color} size={size} />;
19
+ return <ChatIcon color={color} size={size} />;
20
+ }
21
+
22
+ export function typeColor(type: QueryType, theme: SupportTheme): string {
23
+ if (type === 'bug_report') return theme.colors.statusDanger;
24
+ if (type === 'feature_request') return theme.colors.statusProgress;
25
+ return theme.colors.statusOpen;
26
+ }
27
+
28
+ /** Status dot + label — same visual language as the dashboard. */
29
+ export function StatusIndicator({
30
+ status,
31
+ theme,
32
+ }: {
33
+ status: QueryStatus;
34
+ theme: SupportTheme;
35
+ }) {
36
+ const color =
37
+ status === 'open' ? theme.colors.statusOpen : theme.colors.statusResolved;
38
+ return (
39
+ <View style={{ flexDirection: 'row', alignItems: 'center', gap: 6 }}>
40
+ <View
41
+ style={{ width: 6, height: 6, borderRadius: 3, backgroundColor: color }}
42
+ />
43
+ <Text style={{ fontSize: 12, color }}>
44
+ {status === 'open' ? 'Open' : 'Resolved'}
45
+ </Text>
46
+ </View>
47
+ );
48
+ }
49
+
50
+ /** Pressable row with the standard hover/press wash. */
51
+ export function PressableRow({
52
+ onPress,
53
+ children,
54
+ theme,
55
+ style,
56
+ accessibilityLabel,
57
+ }: {
58
+ onPress: () => void;
59
+ children: ReactNode;
60
+ theme: SupportTheme;
61
+ style?: StyleProp<ViewStyle>;
62
+ accessibilityLabel?: string;
63
+ }) {
64
+ return (
65
+ <Pressable
66
+ onPress={onPress}
67
+ accessibilityRole="button"
68
+ accessibilityLabel={accessibilityLabel}
69
+ style={({ pressed }) => [
70
+ { backgroundColor: pressed ? theme.colors.surfaceHover : 'transparent' },
71
+ style,
72
+ ]}
73
+ >
74
+ {children}
75
+ </Pressable>
76
+ );
77
+ }
78
+
79
+ export function PrimaryButton({
80
+ label,
81
+ onPress,
82
+ disabled,
83
+ theme,
84
+ }: {
85
+ label: string;
86
+ onPress: () => void;
87
+ disabled?: boolean;
88
+ theme: SupportTheme;
89
+ }) {
90
+ return (
91
+ <Pressable
92
+ onPress={onPress}
93
+ disabled={disabled}
94
+ accessibilityRole="button"
95
+ style={({ pressed }) => ({
96
+ height: 36,
97
+ paddingHorizontal: 14,
98
+ borderRadius: theme.radii.control,
99
+ backgroundColor: pressed
100
+ ? theme.colors.accentHover
101
+ : theme.colors.accent,
102
+ opacity: disabled ? 0.5 : 1,
103
+ alignItems: 'center',
104
+ justifyContent: 'center',
105
+ })}
106
+ >
107
+ <Text
108
+ style={{
109
+ color: theme.colors.accentForeground,
110
+ fontSize: 13,
111
+ fontWeight: '500',
112
+ }}
113
+ >
114
+ {label}
115
+ </Text>
116
+ </Pressable>
117
+ );
118
+ }
119
+
120
+ export function SecondaryButton({
121
+ label,
122
+ onPress,
123
+ theme,
124
+ }: {
125
+ label: string;
126
+ onPress: () => void;
127
+ theme: SupportTheme;
128
+ }) {
129
+ return (
130
+ <Pressable
131
+ onPress={onPress}
132
+ accessibilityRole="button"
133
+ style={({ pressed }) => ({
134
+ height: 32,
135
+ paddingHorizontal: 12,
136
+ borderRadius: theme.radii.control,
137
+ borderWidth: 1,
138
+ borderColor: theme.colors.border,
139
+ backgroundColor: pressed
140
+ ? theme.colors.surfaceHover
141
+ : theme.colors.surface,
142
+ alignItems: 'center',
143
+ justifyContent: 'center',
144
+ })}
145
+ >
146
+ <Text style={{ color: theme.colors.textPrimary, fontSize: 12, fontWeight: '500' }}>
147
+ {label}
148
+ </Text>
149
+ </Pressable>
150
+ );
151
+ }
152
+
153
+ /** Small muted icon + one line — no illustrations. */
154
+ export function EmptyState({
155
+ message,
156
+ theme,
157
+ icon,
158
+ }: {
159
+ message: string;
160
+ theme: SupportTheme;
161
+ icon?: ReactNode;
162
+ }) {
163
+ return (
164
+ <View style={{ alignItems: 'center', paddingVertical: 40, gap: 8 }}>
165
+ {icon ?? <ChatIcon color={theme.colors.textTertiary} size={16} />}
166
+ <Text style={{ fontSize: 13, color: theme.colors.textSecondary }}>
167
+ {message}
168
+ </Text>
169
+ </View>
170
+ );
171
+ }
172
+
173
+ /** Inline failure state with retry — errors are designed, not toasted. */
174
+ export function ErrorState({
175
+ message,
176
+ onRetry,
177
+ theme,
178
+ }: {
179
+ message: string;
180
+ onRetry: () => void;
181
+ theme: SupportTheme;
182
+ }) {
183
+ return (
184
+ <View style={{ alignItems: 'center', paddingVertical: 32, gap: 10 }}>
185
+ <Text style={{ fontSize: 13, color: theme.colors.textSecondary }}>
186
+ {message}
187
+ </Text>
188
+ <SecondaryButton label="Retry" onPress={onRetry} theme={theme} />
189
+ </View>
190
+ );
191
+ }
192
+
193
+ /** Pulsing block matching final layout dimensions. */
194
+ export function SkeletonBlock({
195
+ width,
196
+ height,
197
+ theme,
198
+ style,
199
+ }: {
200
+ width: number | `${number}%`;
201
+ height: number;
202
+ theme: SupportTheme;
203
+ style?: StyleProp<ViewStyle>;
204
+ }) {
205
+ const opacity = useRef(new Animated.Value(0.5)).current;
206
+
207
+ useEffect(() => {
208
+ const loop = Animated.loop(
209
+ Animated.sequence([
210
+ Animated.timing(opacity, {
211
+ toValue: 1,
212
+ duration: 600,
213
+ useNativeDriver: true,
214
+ }),
215
+ Animated.timing(opacity, {
216
+ toValue: 0.5,
217
+ duration: 600,
218
+ useNativeDriver: true,
219
+ }),
220
+ ]),
221
+ );
222
+ loop.start();
223
+ return () => loop.stop();
224
+ }, [opacity]);
225
+
226
+ return (
227
+ <Animated.View
228
+ style={[
229
+ {
230
+ width,
231
+ height,
232
+ borderRadius: theme.radii.control,
233
+ backgroundColor: theme.colors.surfaceActive,
234
+ opacity,
235
+ },
236
+ style,
237
+ ]}
238
+ />
239
+ );
240
+ }
241
+
242
+ export const textStyles = {
243
+ title: (theme: SupportTheme): TextStyle => ({
244
+ fontSize: 16,
245
+ fontWeight: '600',
246
+ letterSpacing: -0.2,
247
+ color: theme.colors.textPrimary,
248
+ }),
249
+ body: (theme: SupportTheme): TextStyle => ({
250
+ fontSize: 14,
251
+ color: theme.colors.textPrimary,
252
+ lineHeight: 20,
253
+ }),
254
+ secondary: (theme: SupportTheme): TextStyle => ({
255
+ fontSize: 13,
256
+ color: theme.colors.textSecondary,
257
+ }),
258
+ meta: (theme: SupportTheme): TextStyle => ({
259
+ fontSize: 11,
260
+ color: theme.colors.textTertiary,
261
+ fontVariant: ['tabular-nums'],
262
+ }),
263
+ };
264
+
265
+ export function TypeChipView({
266
+ type,
267
+ theme,
268
+ }: {
269
+ type: QueryType;
270
+ theme: SupportTheme;
271
+ }) {
272
+ const color = typeColor(type, theme);
273
+ const bg =
274
+ type === 'bug_report'
275
+ ? theme.colors.statusDangerBg
276
+ : type === 'feature_request'
277
+ ? theme.colors.statusProgressBg
278
+ : theme.colors.statusOpenBg;
279
+ return (
280
+ <View
281
+ style={{
282
+ flexDirection: 'row',
283
+ alignItems: 'center',
284
+ gap: 5,
285
+ paddingHorizontal: 6,
286
+ paddingVertical: 2,
287
+ borderRadius: theme.radii.control,
288
+ backgroundColor: bg,
289
+ alignSelf: 'flex-start',
290
+ }}
291
+ >
292
+ {typeIcon(type, color, 11)}
293
+ <Text style={{ fontSize: 11, fontWeight: '500', color }}>
294
+ {QUERY_TYPE_LABEL[type]}
295
+ </Text>
296
+ </View>
297
+ );
298
+ }
@@ -0,0 +1,163 @@
1
+ import { useState } from 'react';
2
+ import { KeyboardAvoidingView, Platform, Text, TextInput, View } from 'react-native';
3
+ import type { SupportTheme } from '../../theme';
4
+ import type { QueryType } from '../../types';
5
+ import { QUERY_TYPE_LABEL, USER_MESSAGE_MAX, USER_MESSAGE_MIN } from '../../types';
6
+ import { CheckIcon, Chevron } from '../icons';
7
+ import { PressableRow, PrimaryButton, textStyles } from '../primitives';
8
+
9
+ export function ComposeScreen({
10
+ theme,
11
+ type,
12
+ onBack,
13
+ onSubmit,
14
+ }: {
15
+ theme: SupportTheme;
16
+ type: QueryType;
17
+ onBack: () => void;
18
+ /** Resolves true on success. */
19
+ onSubmit: (message: string) => Promise<boolean>;
20
+ }) {
21
+ const [message, setMessage] = useState('');
22
+ const [submitting, setSubmitting] = useState(false);
23
+ const [failed, setFailed] = useState(false);
24
+ const [sent, setSent] = useState(false);
25
+
26
+ const trimmed = message.trim();
27
+ const valid =
28
+ trimmed.length >= USER_MESSAGE_MIN && trimmed.length <= USER_MESSAGE_MAX;
29
+ const overLimit = message.length > USER_MESSAGE_MAX;
30
+
31
+ async function handleSubmit() {
32
+ if (!valid || submitting) return;
33
+ setSubmitting(true);
34
+ setFailed(false);
35
+ const ok = await onSubmit(trimmed);
36
+ setSubmitting(false);
37
+ if (ok) setSent(true);
38
+ else setFailed(true);
39
+ }
40
+
41
+ if (sent) {
42
+ return (
43
+ <View style={{ flex: 1, alignItems: 'center', justifyContent: 'center', gap: 12, padding: 24 }}>
44
+ <View
45
+ style={{
46
+ width: 40,
47
+ height: 40,
48
+ borderRadius: 20,
49
+ borderWidth: 1,
50
+ borderColor: theme.colors.statusResolved,
51
+ alignItems: 'center',
52
+ justifyContent: 'center',
53
+ }}
54
+ >
55
+ <CheckIcon color={theme.colors.statusResolved} size={14} />
56
+ </View>
57
+ <Text style={textStyles.body(theme)}>Sent</Text>
58
+ <Text style={[textStyles.secondary(theme), { textAlign: 'center' }]}>
59
+ We&apos;ll reply here. You&apos;ll see the thread under Open.
60
+ </Text>
61
+ <View style={{ marginTop: 8 }}>
62
+ <PrimaryButton label="Done" onPress={onBack} theme={theme} />
63
+ </View>
64
+ </View>
65
+ );
66
+ }
67
+
68
+ return (
69
+ <KeyboardAvoidingView
70
+ style={{ flex: 1 }}
71
+ behavior={Platform.OS === 'ios' ? 'padding' : undefined}
72
+ >
73
+ {/* Header: back + read-only chosen type */}
74
+ <View
75
+ style={{
76
+ flexDirection: 'row',
77
+ alignItems: 'center',
78
+ gap: 8,
79
+ paddingHorizontal: 12,
80
+ paddingBottom: 12,
81
+ }}
82
+ >
83
+ <PressableRow
84
+ onPress={onBack}
85
+ theme={theme}
86
+ accessibilityLabel="Back"
87
+ style={{ padding: 6, borderRadius: theme.radii.control }}
88
+ >
89
+ <Chevron color={theme.colors.textSecondary} direction="left" size={12} />
90
+ </PressableRow>
91
+ <Text style={textStyles.title(theme)}>{QUERY_TYPE_LABEL[type]}</Text>
92
+ </View>
93
+
94
+ <View style={{ flex: 1, paddingHorizontal: 16 }}>
95
+ <TextInput
96
+ value={message}
97
+ onChangeText={(v) => {
98
+ setMessage(v);
99
+ setFailed(false);
100
+ }}
101
+ multiline
102
+ autoFocus
103
+ placeholder={
104
+ type === 'bug_report'
105
+ ? 'What happened, and what did you expect?'
106
+ : type === 'feature_request'
107
+ ? 'What would you like to see?'
108
+ : 'What should we know?'
109
+ }
110
+ placeholderTextColor={theme.colors.textTertiary}
111
+ style={{
112
+ minHeight: 120,
113
+ maxHeight: 220,
114
+ borderWidth: 1,
115
+ borderColor: theme.colors.border,
116
+ borderRadius: theme.radii.control,
117
+ backgroundColor: theme.colors.surface,
118
+ color: theme.colors.textPrimary,
119
+ fontSize: 14,
120
+ lineHeight: 20,
121
+ padding: 12,
122
+ textAlignVertical: 'top',
123
+ }}
124
+ />
125
+ <View
126
+ style={{
127
+ flexDirection: 'row',
128
+ alignItems: 'center',
129
+ marginTop: 8,
130
+ gap: 12,
131
+ }}
132
+ >
133
+ <Text
134
+ style={[
135
+ textStyles.meta(theme),
136
+ overLimit && { color: theme.colors.statusDanger },
137
+ ]}
138
+ >
139
+ {message.length}/{USER_MESSAGE_MAX}
140
+ </Text>
141
+ {trimmed.length > 0 && trimmed.length < USER_MESSAGE_MIN && (
142
+ <Text style={textStyles.meta(theme)}>
143
+ At least {USER_MESSAGE_MIN} characters
144
+ </Text>
145
+ )}
146
+ {failed && (
147
+ <Text style={{ fontSize: 11, color: theme.colors.statusDanger }}>
148
+ Couldn&apos;t send — try again
149
+ </Text>
150
+ )}
151
+ <View style={{ marginLeft: 'auto' }}>
152
+ <PrimaryButton
153
+ label={submitting ? 'Sending…' : 'Send'}
154
+ onPress={handleSubmit}
155
+ disabled={!valid || submitting}
156
+ theme={theme}
157
+ />
158
+ </View>
159
+ </View>
160
+ </View>
161
+ </KeyboardAvoidingView>
162
+ );
163
+ }