@appsupportkit/support-sdk 0.1.2 → 0.2.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 (94) hide show
  1. package/lib/commonjs/components/BottomSheet.js +67 -47
  2. package/lib/commonjs/components/BottomSheet.js.map +1 -1
  3. package/lib/commonjs/components/LauncherButton.js +177 -0
  4. package/lib/commonjs/components/LauncherButton.js.map +1 -0
  5. package/lib/commonjs/components/SupportButton.js +54 -176
  6. package/lib/commonjs/components/SupportButton.js.map +1 -1
  7. package/lib/commonjs/components/icons.js +74 -23
  8. package/lib/commonjs/components/icons.js.map +1 -1
  9. package/lib/commonjs/components/primitives.js +183 -38
  10. package/lib/commonjs/components/primitives.js.map +1 -1
  11. package/lib/commonjs/components/screens/ComposeScreen.js +115 -58
  12. package/lib/commonjs/components/screens/ComposeScreen.js.map +1 -1
  13. package/lib/commonjs/components/screens/HomeScreen.js +170 -115
  14. package/lib/commonjs/components/screens/HomeScreen.js.map +1 -1
  15. package/lib/commonjs/components/screens/ThreadScreen.js +222 -108
  16. package/lib/commonjs/components/screens/ThreadScreen.js.map +1 -1
  17. package/lib/commonjs/index.js.map +1 -1
  18. package/lib/commonjs/theme.js +136 -17
  19. package/lib/commonjs/theme.js.map +1 -1
  20. package/lib/commonjs/types.js +7 -0
  21. package/lib/commonjs/types.js.map +1 -1
  22. package/lib/commonjs/unread.js +92 -0
  23. package/lib/commonjs/unread.js.map +1 -0
  24. package/lib/commonjs/useSupportThreads.js +286 -0
  25. package/lib/commonjs/useSupportThreads.js.map +1 -0
  26. package/lib/commonjs/utils.js +33 -0
  27. package/lib/commonjs/utils.js.map +1 -1
  28. package/lib/module/components/BottomSheet.js +68 -48
  29. package/lib/module/components/BottomSheet.js.map +1 -1
  30. package/lib/module/components/LauncherButton.js +172 -0
  31. package/lib/module/components/LauncherButton.js.map +1 -0
  32. package/lib/module/components/SupportButton.js +57 -179
  33. package/lib/module/components/SupportButton.js.map +1 -1
  34. package/lib/module/components/icons.js +72 -22
  35. package/lib/module/components/icons.js.map +1 -1
  36. package/lib/module/components/primitives.js +179 -39
  37. package/lib/module/components/primitives.js.map +1 -1
  38. package/lib/module/components/screens/ComposeScreen.js +118 -61
  39. package/lib/module/components/screens/ComposeScreen.js.map +1 -1
  40. package/lib/module/components/screens/HomeScreen.js +172 -117
  41. package/lib/module/components/screens/HomeScreen.js.map +1 -1
  42. package/lib/module/components/screens/ThreadScreen.js +228 -112
  43. package/lib/module/components/screens/ThreadScreen.js.map +1 -1
  44. package/lib/module/index.js.map +1 -1
  45. package/lib/module/theme.js +135 -16
  46. package/lib/module/theme.js.map +1 -1
  47. package/lib/module/types.js +7 -0
  48. package/lib/module/types.js.map +1 -1
  49. package/lib/module/unread.js +83 -0
  50. package/lib/module/unread.js.map +1 -0
  51. package/lib/module/useSupportThreads.js +281 -0
  52. package/lib/module/useSupportThreads.js.map +1 -0
  53. package/lib/module/utils.js +30 -0
  54. package/lib/module/utils.js.map +1 -1
  55. package/lib/typescript/components/BottomSheet.d.ts.map +1 -1
  56. package/lib/typescript/components/LauncherButton.d.ts +14 -0
  57. package/lib/typescript/components/LauncherButton.d.ts.map +1 -0
  58. package/lib/typescript/components/SupportButton.d.ts +3 -3
  59. package/lib/typescript/components/SupportButton.d.ts.map +1 -1
  60. package/lib/typescript/components/icons.d.ts +13 -2
  61. package/lib/typescript/components/icons.d.ts.map +1 -1
  62. package/lib/typescript/components/primitives.d.ts +35 -4
  63. package/lib/typescript/components/primitives.d.ts.map +1 -1
  64. package/lib/typescript/components/screens/ComposeScreen.d.ts.map +1 -1
  65. package/lib/typescript/components/screens/HomeScreen.d.ts +3 -2
  66. package/lib/typescript/components/screens/HomeScreen.d.ts.map +1 -1
  67. package/lib/typescript/components/screens/ThreadScreen.d.ts.map +1 -1
  68. package/lib/typescript/index.d.ts +1 -1
  69. package/lib/typescript/index.d.ts.map +1 -1
  70. package/lib/typescript/theme.d.ts +44 -3
  71. package/lib/typescript/theme.d.ts.map +1 -1
  72. package/lib/typescript/types.d.ts +22 -0
  73. package/lib/typescript/types.d.ts.map +1 -1
  74. package/lib/typescript/unread.d.ts +22 -0
  75. package/lib/typescript/unread.d.ts.map +1 -0
  76. package/lib/typescript/useSupportThreads.d.ts +28 -0
  77. package/lib/typescript/useSupportThreads.d.ts.map +1 -0
  78. package/lib/typescript/utils.d.ts +5 -0
  79. package/lib/typescript/utils.d.ts.map +1 -1
  80. package/package.json +2 -2
  81. package/src/components/BottomSheet.tsx +61 -43
  82. package/src/components/LauncherButton.tsx +217 -0
  83. package/src/components/SupportButton.tsx +52 -196
  84. package/src/components/icons.tsx +77 -19
  85. package/src/components/primitives.tsx +217 -36
  86. package/src/components/screens/ComposeScreen.tsx +131 -61
  87. package/src/components/screens/HomeScreen.tsx +168 -117
  88. package/src/components/screens/ThreadScreen.tsx +289 -119
  89. package/src/index.tsx +1 -0
  90. package/src/theme.ts +165 -18
  91. package/src/types.ts +24 -0
  92. package/src/unread.ts +91 -0
  93. package/src/useSupportThreads.ts +372 -0
  94. package/src/utils.ts +33 -0
@@ -1,6 +1,7 @@
1
1
  import { useEffect, useRef, type ReactNode } from 'react';
2
2
  import {
3
3
  Animated,
4
+ Easing,
4
5
  Pressable,
5
6
  Text,
6
7
  View,
@@ -25,6 +26,38 @@ export function typeColor(type: QueryType, theme: SupportTheme): string {
25
26
  return theme.colors.statusOpen;
26
27
  }
27
28
 
29
+ export function typeBackground(type: QueryType, theme: SupportTheme): string {
30
+ if (type === 'bug_report') return theme.colors.statusDangerBg;
31
+ if (type === 'feature_request') return theme.colors.statusProgressBg;
32
+ return theme.colors.statusOpenBg;
33
+ }
34
+
35
+ /** Tinted round chip holding a type icon — the anchor of every list row. */
36
+ export function TypeGlyph({
37
+ type,
38
+ theme,
39
+ size = 36,
40
+ }: {
41
+ type: QueryType;
42
+ theme: SupportTheme;
43
+ size?: number;
44
+ }) {
45
+ return (
46
+ <View
47
+ style={{
48
+ width: size,
49
+ height: size,
50
+ borderRadius: size / 2,
51
+ backgroundColor: typeBackground(type, theme),
52
+ alignItems: 'center',
53
+ justifyContent: 'center',
54
+ }}
55
+ >
56
+ {typeIcon(type, typeColor(type, theme), size * 0.44)}
57
+ </View>
58
+ );
59
+ }
60
+
28
61
  /** Status dot + label — same visual language as the dashboard. */
29
62
  export function StatusIndicator({
30
63
  status,
@@ -47,7 +80,7 @@ export function StatusIndicator({
47
80
  );
48
81
  }
49
82
 
50
- /** Pressable row with the standard hover/press wash. */
83
+ /** Pressable row with the standard press wash. */
51
84
  export function PressableRow({
52
85
  onPress,
53
86
  children,
@@ -76,30 +109,83 @@ export function PressableRow({
76
109
  );
77
110
  }
78
111
 
112
+ /** Circular icon button — back, close, send. */
113
+ export function IconButton({
114
+ onPress,
115
+ children,
116
+ theme,
117
+ accessibilityLabel,
118
+ size = 34,
119
+ disabled,
120
+ tone = 'surface',
121
+ }: {
122
+ onPress: () => void;
123
+ children: ReactNode;
124
+ theme: SupportTheme;
125
+ accessibilityLabel: string;
126
+ size?: number;
127
+ disabled?: boolean;
128
+ tone?: 'surface' | 'accent';
129
+ }) {
130
+ const accent = tone === 'accent';
131
+ return (
132
+ <Pressable
133
+ onPress={onPress}
134
+ disabled={disabled}
135
+ hitSlop={6}
136
+ accessibilityRole="button"
137
+ accessibilityLabel={accessibilityLabel}
138
+ style={({ pressed }) => ({
139
+ width: size,
140
+ height: size,
141
+ borderRadius: size / 2,
142
+ alignItems: 'center',
143
+ justifyContent: 'center',
144
+ opacity: disabled ? 0.45 : 1,
145
+ backgroundColor: accent
146
+ ? pressed
147
+ ? theme.colors.accentHover
148
+ : theme.colors.accent
149
+ : pressed
150
+ ? theme.colors.surfaceActive
151
+ : theme.colors.surface,
152
+ borderWidth: accent ? 0 : 1,
153
+ borderColor: theme.colors.border,
154
+ })}
155
+ >
156
+ {children}
157
+ </Pressable>
158
+ );
159
+ }
160
+
79
161
  export function PrimaryButton({
80
162
  label,
81
163
  onPress,
82
164
  disabled,
83
165
  theme,
166
+ fullWidth,
84
167
  }: {
85
168
  label: string;
86
169
  onPress: () => void;
87
170
  disabled?: boolean;
88
171
  theme: SupportTheme;
172
+ fullWidth?: boolean;
89
173
  }) {
90
174
  return (
91
175
  <Pressable
92
176
  onPress={onPress}
93
177
  disabled={disabled}
94
178
  accessibilityRole="button"
179
+ accessibilityState={{ disabled: Boolean(disabled) }}
95
180
  style={({ pressed }) => ({
96
- height: 36,
97
- paddingHorizontal: 14,
181
+ height: 44,
182
+ alignSelf: fullWidth ? 'stretch' : 'flex-start',
183
+ paddingHorizontal: 18,
98
184
  borderRadius: theme.radii.control,
99
185
  backgroundColor: pressed
100
186
  ? theme.colors.accentHover
101
187
  : theme.colors.accent,
102
- opacity: disabled ? 0.5 : 1,
188
+ opacity: disabled ? 0.45 : 1,
103
189
  alignItems: 'center',
104
190
  justifyContent: 'center',
105
191
  })}
@@ -107,8 +193,9 @@ export function PrimaryButton({
107
193
  <Text
108
194
  style={{
109
195
  color: theme.colors.accentForeground,
110
- fontSize: 13,
111
- fontWeight: '500',
196
+ fontSize: 14,
197
+ fontWeight: '600',
198
+ letterSpacing: -0.1,
112
199
  }}
113
200
  >
114
201
  {label}
@@ -131,8 +218,8 @@ export function SecondaryButton({
131
218
  onPress={onPress}
132
219
  accessibilityRole="button"
133
220
  style={({ pressed }) => ({
134
- height: 32,
135
- paddingHorizontal: 12,
221
+ height: 38,
222
+ paddingHorizontal: 16,
136
223
  borderRadius: theme.radii.control,
137
224
  borderWidth: 1,
138
225
  borderColor: theme.colors.border,
@@ -143,29 +230,118 @@ export function SecondaryButton({
143
230
  justifyContent: 'center',
144
231
  })}
145
232
  >
146
- <Text style={{ color: theme.colors.textPrimary, fontSize: 12, fontWeight: '500' }}>
233
+ <Text style={{ color: theme.colors.textPrimary, fontSize: 13, fontWeight: '500' }}>
147
234
  {label}
148
235
  </Text>
149
236
  </Pressable>
150
237
  );
151
238
  }
152
239
 
240
+ /** The count pill on a thread row. */
241
+ export function UnreadPill({
242
+ count,
243
+ theme,
244
+ }: {
245
+ count: number;
246
+ theme: SupportTheme;
247
+ }) {
248
+ return (
249
+ <View
250
+ style={{
251
+ minWidth: 20,
252
+ height: 20,
253
+ paddingHorizontal: 6,
254
+ borderRadius: theme.radii.pill,
255
+ backgroundColor: theme.colors.accent,
256
+ alignItems: 'center',
257
+ justifyContent: 'center',
258
+ }}
259
+ >
260
+ <Text
261
+ style={{
262
+ fontSize: 11,
263
+ lineHeight: 14,
264
+ fontWeight: '700',
265
+ color: theme.colors.accentForeground,
266
+ fontVariant: ['tabular-nums'],
267
+ }}
268
+ >
269
+ {count > 99 ? '99+' : count}
270
+ </Text>
271
+ </View>
272
+ );
273
+ }
274
+
275
+ export function SectionLabel({
276
+ children,
277
+ theme,
278
+ style,
279
+ }: {
280
+ children: ReactNode;
281
+ theme: SupportTheme;
282
+ style?: StyleProp<TextStyle>;
283
+ }) {
284
+ return (
285
+ <Text
286
+ style={[
287
+ {
288
+ fontSize: 11,
289
+ fontWeight: '600',
290
+ textTransform: 'uppercase',
291
+ letterSpacing: 0.6,
292
+ color: theme.colors.textTertiary,
293
+ },
294
+ style,
295
+ ]}
296
+ >
297
+ {children}
298
+ </Text>
299
+ );
300
+ }
301
+
153
302
  /** Small muted icon + one line — no illustrations. */
154
303
  export function EmptyState({
155
304
  message,
305
+ hint,
156
306
  theme,
157
307
  icon,
158
308
  }: {
159
309
  message: string;
310
+ hint?: string;
160
311
  theme: SupportTheme;
161
312
  icon?: ReactNode;
162
313
  }) {
163
314
  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 }}>
315
+ <View style={{ alignItems: 'center', paddingVertical: 44, paddingHorizontal: 32, gap: 10 }}>
316
+ <View
317
+ style={{
318
+ width: 44,
319
+ height: 44,
320
+ borderRadius: 22,
321
+ alignItems: 'center',
322
+ justifyContent: 'center',
323
+ backgroundColor: theme.colors.surface,
324
+ borderWidth: 1,
325
+ borderColor: theme.colors.border,
326
+ }}
327
+ >
328
+ {icon ?? <ChatIcon color={theme.colors.textTertiary} size={18} />}
329
+ </View>
330
+ <Text style={{ fontSize: 15, fontWeight: '600', color: theme.colors.textPrimary }}>
167
331
  {message}
168
332
  </Text>
333
+ {hint !== undefined && (
334
+ <Text
335
+ style={{
336
+ fontSize: 13,
337
+ lineHeight: 19,
338
+ color: theme.colors.textSecondary,
339
+ textAlign: 'center',
340
+ }}
341
+ >
342
+ {hint}
343
+ </Text>
344
+ )}
169
345
  </View>
170
346
  );
171
347
  }
@@ -181,11 +357,11 @@ export function ErrorState({
181
357
  theme: SupportTheme;
182
358
  }) {
183
359
  return (
184
- <View style={{ alignItems: 'center', paddingVertical: 32, gap: 10 }}>
185
- <Text style={{ fontSize: 13, color: theme.colors.textSecondary }}>
360
+ <View style={{ alignItems: 'center', paddingVertical: 40, gap: 12 }}>
361
+ <Text style={{ fontSize: 14, color: theme.colors.textSecondary }}>
186
362
  {message}
187
363
  </Text>
188
- <SecondaryButton label="Retry" onPress={onRetry} theme={theme} />
364
+ <SecondaryButton label="Try again" onPress={onRetry} theme={theme} />
189
365
  </View>
190
366
  );
191
367
  }
@@ -195,26 +371,30 @@ export function SkeletonBlock({
195
371
  width,
196
372
  height,
197
373
  theme,
374
+ radius,
198
375
  style,
199
376
  }: {
200
377
  width: number | `${number}%`;
201
378
  height: number;
202
379
  theme: SupportTheme;
380
+ radius?: number;
203
381
  style?: StyleProp<ViewStyle>;
204
382
  }) {
205
- const opacity = useRef(new Animated.Value(0.5)).current;
383
+ const opacity = useRef(new Animated.Value(0.4)).current;
206
384
 
207
385
  useEffect(() => {
208
386
  const loop = Animated.loop(
209
387
  Animated.sequence([
210
388
  Animated.timing(opacity, {
211
- toValue: 1,
212
- duration: 600,
389
+ toValue: 0.9,
390
+ duration: 700,
391
+ easing: Easing.inOut(Easing.quad),
213
392
  useNativeDriver: true,
214
393
  }),
215
394
  Animated.timing(opacity, {
216
- toValue: 0.5,
217
- duration: 600,
395
+ toValue: 0.4,
396
+ duration: 700,
397
+ easing: Easing.inOut(Easing.quad),
218
398
  useNativeDriver: true,
219
399
  }),
220
400
  ]),
@@ -229,7 +409,7 @@ export function SkeletonBlock({
229
409
  {
230
410
  width,
231
411
  height,
232
- borderRadius: theme.radii.control,
412
+ borderRadius: radius ?? theme.radii.control,
233
413
  backgroundColor: theme.colors.surfaceActive,
234
414
  opacity,
235
415
  },
@@ -241,22 +421,29 @@ export function SkeletonBlock({
241
421
 
242
422
  export const textStyles = {
243
423
  title: (theme: SupportTheme): TextStyle => ({
244
- fontSize: 16,
424
+ fontSize: 19,
425
+ fontWeight: '700',
426
+ letterSpacing: -0.4,
427
+ color: theme.colors.textPrimary,
428
+ }),
429
+ heading: (theme: SupportTheme): TextStyle => ({
430
+ fontSize: 15,
245
431
  fontWeight: '600',
246
432
  letterSpacing: -0.2,
247
433
  color: theme.colors.textPrimary,
248
434
  }),
249
435
  body: (theme: SupportTheme): TextStyle => ({
250
- fontSize: 14,
436
+ fontSize: 15,
251
437
  color: theme.colors.textPrimary,
252
- lineHeight: 20,
438
+ lineHeight: 21,
253
439
  }),
254
440
  secondary: (theme: SupportTheme): TextStyle => ({
255
441
  fontSize: 13,
442
+ lineHeight: 19,
256
443
  color: theme.colors.textSecondary,
257
444
  }),
258
445
  meta: (theme: SupportTheme): TextStyle => ({
259
- fontSize: 11,
446
+ fontSize: 11.5,
260
447
  color: theme.colors.textTertiary,
261
448
  fontVariant: ['tabular-nums'],
262
449
  }),
@@ -270,27 +457,21 @@ export function TypeChipView({
270
457
  theme: SupportTheme;
271
458
  }) {
272
459
  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
460
  return (
280
461
  <View
281
462
  style={{
282
463
  flexDirection: 'row',
283
464
  alignItems: 'center',
284
465
  gap: 5,
285
- paddingHorizontal: 6,
286
- paddingVertical: 2,
287
- borderRadius: theme.radii.control,
288
- backgroundColor: bg,
466
+ paddingHorizontal: 8,
467
+ paddingVertical: 4,
468
+ borderRadius: theme.radii.pill,
469
+ backgroundColor: typeBackground(type, theme),
289
470
  alignSelf: 'flex-start',
290
471
  }}
291
472
  >
292
473
  {typeIcon(type, color, 11)}
293
- <Text style={{ fontSize: 11, fontWeight: '500', color }}>
474
+ <Text style={{ fontSize: 11.5, fontWeight: '600', color }}>
294
475
  {QUERY_TYPE_LABEL[type]}
295
476
  </Text>
296
477
  </View>
@@ -1,10 +1,77 @@
1
- import { useState } from 'react';
2
- import { KeyboardAvoidingView, Platform, Text, TextInput, View } from 'react-native';
1
+ import { useEffect, useRef, useState } from 'react';
2
+ import {
3
+ Animated,
4
+ Easing,
5
+ KeyboardAvoidingView,
6
+ Platform,
7
+ Text,
8
+ TextInput,
9
+ View,
10
+ } from 'react-native';
3
11
  import type { SupportTheme } from '../../theme';
4
12
  import type { QueryType } from '../../types';
5
13
  import { QUERY_TYPE_LABEL, USER_MESSAGE_MAX, USER_MESSAGE_MIN } from '../../types';
6
14
  import { CheckIcon, Chevron } from '../icons';
7
- import { PressableRow, PrimaryButton, textStyles } from '../primitives';
15
+ import {
16
+ IconButton,
17
+ PrimaryButton,
18
+ textStyles,
19
+ TypeGlyph,
20
+ } from '../primitives';
21
+
22
+ const PROMPT: Record<QueryType, string> = {
23
+ feedback: 'What should we know?',
24
+ feature_request: 'What would you like to see?',
25
+ bug_report: 'What happened, and what did you expect?',
26
+ };
27
+
28
+ const SUBTITLE: Record<QueryType, string> = {
29
+ feedback: 'Good or bad — it goes straight to the team.',
30
+ feature_request: 'Tell us what you are trying to do.',
31
+ bug_report: 'Steps, screen, and what you expected all help.',
32
+ };
33
+
34
+ /** Scales in once on success — the only celebratory motion in the SDK. */
35
+ function SentMark({ theme }: { theme: SupportTheme }) {
36
+ const scale = useRef(new Animated.Value(0.6)).current;
37
+ const opacity = useRef(new Animated.Value(0)).current;
38
+
39
+ useEffect(() => {
40
+ Animated.parallel([
41
+ Animated.timing(scale, {
42
+ toValue: 1,
43
+ duration: 320,
44
+ easing: Easing.bezier(0.16, 1, 0.3, 1),
45
+ useNativeDriver: true,
46
+ }),
47
+ Animated.timing(opacity, {
48
+ toValue: 1,
49
+ duration: 200,
50
+ easing: Easing.out(Easing.cubic),
51
+ useNativeDriver: true,
52
+ }),
53
+ ]).start();
54
+ }, [scale, opacity]);
55
+
56
+ return (
57
+ <Animated.View
58
+ style={{
59
+ width: 56,
60
+ height: 56,
61
+ borderRadius: 28,
62
+ backgroundColor: theme.colors.statusResolvedBg,
63
+ borderWidth: 1,
64
+ borderColor: theme.colors.statusResolved,
65
+ alignItems: 'center',
66
+ justifyContent: 'center',
67
+ opacity,
68
+ transform: [{ scale }],
69
+ }}
70
+ >
71
+ <CheckIcon color={theme.colors.statusResolved} size={20} />
72
+ </Animated.View>
73
+ );
74
+ }
8
75
 
9
76
  export function ComposeScreen({
10
77
  theme,
@@ -27,6 +94,7 @@ export function ComposeScreen({
27
94
  const valid =
28
95
  trimmed.length >= USER_MESSAGE_MIN && trimmed.length <= USER_MESSAGE_MAX;
29
96
  const overLimit = message.length > USER_MESSAGE_MAX;
97
+ const tooShort = trimmed.length > 0 && trimmed.length < USER_MESSAGE_MIN;
30
98
 
31
99
  async function handleSubmit() {
32
100
  if (!valid || submitting) return;
@@ -40,25 +108,23 @@ export function ComposeScreen({
40
108
 
41
109
  if (sent) {
42
110
  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>
111
+ <View
112
+ style={{
113
+ flex: 1,
114
+ alignItems: 'center',
115
+ justifyContent: 'center',
116
+ gap: 14,
117
+ paddingHorizontal: 32,
118
+ paddingBottom: 40,
119
+ }}
120
+ >
121
+ <SentMark theme={theme} />
122
+ <Text style={textStyles.title(theme)}>Sent</Text>
58
123
  <Text style={[textStyles.secondary(theme), { textAlign: 'center' }]}>
59
- We&apos;ll reply here. You&apos;ll see the thread under Open.
124
+ We&apos;ll reply right here. You&apos;ll find the conversation under
125
+ Open, and the button shows a badge when we answer.
60
126
  </Text>
61
- <View style={{ marginTop: 8 }}>
127
+ <View style={{ marginTop: 6 }}>
62
128
  <PrimaryButton label="Done" onPress={onBack} theme={theme} />
63
129
  </View>
64
130
  </View>
@@ -70,25 +136,29 @@ export function ComposeScreen({
70
136
  style={{ flex: 1 }}
71
137
  behavior={Platform.OS === 'ios' ? 'padding' : undefined}
72
138
  >
73
- {/* Header: back + read-only chosen type */}
139
+ {/* Header: back + the chosen type */}
74
140
  <View
75
141
  style={{
76
142
  flexDirection: 'row',
77
143
  alignItems: 'center',
78
- gap: 8,
144
+ gap: 10,
79
145
  paddingHorizontal: 12,
80
- paddingBottom: 12,
146
+ paddingBottom: 14,
81
147
  }}
82
148
  >
83
- <PressableRow
149
+ <IconButton
84
150
  onPress={onBack}
85
151
  theme={theme}
86
- accessibilityLabel="Back"
87
- style={{ padding: 6, borderRadius: theme.radii.control }}
152
+ accessibilityLabel="Back to conversations"
153
+ size={32}
88
154
  >
89
155
  <Chevron color={theme.colors.textSecondary} direction="left" size={12} />
90
- </PressableRow>
91
- <Text style={textStyles.title(theme)}>{QUERY_TYPE_LABEL[type]}</Text>
156
+ </IconButton>
157
+ <TypeGlyph type={type} theme={theme} size={32} />
158
+ <View style={{ flex: 1, gap: 1 }}>
159
+ <Text style={textStyles.heading(theme)}>{QUERY_TYPE_LABEL[type]}</Text>
160
+ <Text style={textStyles.meta(theme)}>{SUBTITLE[type]}</Text>
161
+ </View>
92
162
  </View>
93
163
 
94
164
  <View style={{ flex: 1, paddingHorizontal: 16 }}>
@@ -100,62 +170,62 @@ export function ComposeScreen({
100
170
  }}
101
171
  multiline
102
172
  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
- }
173
+ editable={!submitting}
174
+ placeholder={PROMPT[type]}
110
175
  placeholderTextColor={theme.colors.textTertiary}
111
176
  style={{
112
- minHeight: 120,
113
- maxHeight: 220,
177
+ minHeight: 140,
178
+ maxHeight: 240,
114
179
  borderWidth: 1,
115
180
  borderColor: theme.colors.border,
116
- borderRadius: theme.radii.control,
181
+ borderRadius: theme.radii.card,
117
182
  backgroundColor: theme.colors.surface,
118
183
  color: theme.colors.textPrimary,
119
- fontSize: 14,
120
- lineHeight: 20,
121
- padding: 12,
184
+ fontSize: 15,
185
+ lineHeight: 21,
186
+ padding: 14,
122
187
  textAlignVertical: 'top',
123
188
  }}
124
189
  />
190
+
125
191
  <View
126
192
  style={{
127
193
  flexDirection: 'row',
128
194
  alignItems: 'center',
129
- marginTop: 8,
130
- gap: 12,
195
+ gap: 10,
196
+ paddingTop: 10,
197
+ paddingHorizontal: 2,
198
+ minHeight: 20,
131
199
  }}
132
200
  >
201
+ {failedMessage !== null ? (
202
+ <Text style={{ fontSize: 12, color: theme.colors.statusDanger }}>
203
+ {failedMessage}
204
+ </Text>
205
+ ) : tooShort ? (
206
+ <Text style={textStyles.meta(theme)}>
207
+ At least {USER_MESSAGE_MIN} characters
208
+ </Text>
209
+ ) : null}
133
210
  <Text
134
211
  style={[
135
212
  textStyles.meta(theme),
213
+ { marginLeft: 'auto' },
136
214
  overLimit && { color: theme.colors.statusDanger },
137
215
  ]}
138
216
  >
139
217
  {message.length}/{USER_MESSAGE_MAX}
140
218
  </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
- {failedMessage !== null && (
147
- <Text style={{ fontSize: 11, color: theme.colors.statusDanger }}>
148
- {failedMessage}
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>
219
+ </View>
220
+
221
+ <View style={{ paddingTop: 12 }}>
222
+ <PrimaryButton
223
+ label={submitting ? 'Sending…' : 'Send'}
224
+ onPress={handleSubmit}
225
+ disabled={!valid || submitting}
226
+ theme={theme}
227
+ fullWidth
228
+ />
159
229
  </View>
160
230
  </View>
161
231
  </KeyboardAvoidingView>