convex-feedback-ui 0.1.1

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 (118) hide show
  1. package/README.md +187 -0
  2. package/dist/.tsbuildinfo +1 -0
  3. package/dist/native/context.d.ts +10 -0
  4. package/dist/native/context.d.ts.map +1 -0
  5. package/dist/native/context.js +33 -0
  6. package/dist/native/context.js.map +1 -0
  7. package/dist/native/index.d.ts +7 -0
  8. package/dist/native/index.d.ts.map +1 -0
  9. package/dist/native/index.js +6 -0
  10. package/dist/native/index.js.map +1 -0
  11. package/dist/native/primitives.d.ts +94 -0
  12. package/dist/native/primitives.d.ts.map +1 -0
  13. package/dist/native/primitives.js +303 -0
  14. package/dist/native/primitives.js.map +1 -0
  15. package/dist/native/screen.d.ts +3 -0
  16. package/dist/native/screen.d.ts.map +1 -0
  17. package/dist/native/screen.js +284 -0
  18. package/dist/native/screen.js.map +1 -0
  19. package/dist/react/context.d.ts +22 -0
  20. package/dist/react/context.d.ts.map +1 -0
  21. package/dist/react/context.js +41 -0
  22. package/dist/react/context.js.map +1 -0
  23. package/dist/react/index.d.ts +7 -0
  24. package/dist/react/index.d.ts.map +1 -0
  25. package/dist/react/index.js +6 -0
  26. package/dist/react/index.js.map +1 -0
  27. package/dist/react/primitives.d.ts +105 -0
  28. package/dist/react/primitives.d.ts.map +1 -0
  29. package/dist/react/primitives.js +217 -0
  30. package/dist/react/primitives.js.map +1 -0
  31. package/dist/react/screen.d.ts +6 -0
  32. package/dist/react/screen.d.ts.map +1 -0
  33. package/dist/react/screen.js +152 -0
  34. package/dist/react/screen.js.map +1 -0
  35. package/dist/react/styles.css +299 -0
  36. package/dist/shared/messages.d.ts +4 -0
  37. package/dist/shared/messages.d.ts.map +1 -0
  38. package/dist/shared/messages.js +85 -0
  39. package/dist/shared/messages.js.map +1 -0
  40. package/dist/shared/render.d.ts +4 -0
  41. package/dist/shared/render.d.ts.map +1 -0
  42. package/dist/shared/render.js +6 -0
  43. package/dist/shared/render.js.map +1 -0
  44. package/dist/shared/theme.d.ts +4 -0
  45. package/dist/shared/theme.d.ts.map +1 -0
  46. package/dist/shared/theme.js +23 -0
  47. package/dist/shared/theme.js.map +1 -0
  48. package/dist/shared/types/board.d.ts +43 -0
  49. package/dist/shared/types/board.d.ts.map +1 -0
  50. package/dist/shared/types/board.js +2 -0
  51. package/dist/shared/types/board.js.map +1 -0
  52. package/dist/shared/types/comment.d.ts +85 -0
  53. package/dist/shared/types/comment.d.ts.map +1 -0
  54. package/dist/shared/types/comment.js +2 -0
  55. package/dist/shared/types/comment.js.map +1 -0
  56. package/dist/shared/types/context.d.ts +68 -0
  57. package/dist/shared/types/context.d.ts.map +1 -0
  58. package/dist/shared/types/context.js +2 -0
  59. package/dist/shared/types/context.js.map +1 -0
  60. package/dist/shared/types/entry.d.ts +41 -0
  61. package/dist/shared/types/entry.d.ts.map +1 -0
  62. package/dist/shared/types/entry.js +2 -0
  63. package/dist/shared/types/entry.js.map +1 -0
  64. package/dist/shared/types/form.d.ts +25 -0
  65. package/dist/shared/types/form.d.ts.map +1 -0
  66. package/dist/shared/types/form.js +2 -0
  67. package/dist/shared/types/form.js.map +1 -0
  68. package/dist/shared/types/helpers.d.ts +2 -0
  69. package/dist/shared/types/helpers.d.ts.map +1 -0
  70. package/dist/shared/types/helpers.js +2 -0
  71. package/dist/shared/types/helpers.js.map +1 -0
  72. package/dist/shared/types/index.d.ts +10 -0
  73. package/dist/shared/types/index.d.ts.map +1 -0
  74. package/dist/shared/types/index.js +2 -0
  75. package/dist/shared/types/index.js.map +1 -0
  76. package/dist/shared/types/messages.d.ts +117 -0
  77. package/dist/shared/types/messages.d.ts.map +1 -0
  78. package/dist/shared/types/messages.js +2 -0
  79. package/dist/shared/types/messages.js.map +1 -0
  80. package/dist/shared/types/render.d.ts +12 -0
  81. package/dist/shared/types/render.d.ts.map +1 -0
  82. package/dist/shared/types/render.js +2 -0
  83. package/dist/shared/types/render.js.map +1 -0
  84. package/dist/shared/types/screen.d.ts +95 -0
  85. package/dist/shared/types/screen.d.ts.map +1 -0
  86. package/dist/shared/types/screen.js +2 -0
  87. package/dist/shared/types/screen.js.map +1 -0
  88. package/dist/shared/types/theme.d.ts +48 -0
  89. package/dist/shared/types/theme.d.ts.map +1 -0
  90. package/dist/shared/types/theme.js +2 -0
  91. package/dist/shared/types/theme.js.map +1 -0
  92. package/package.json +58 -0
  93. package/src/native/README.md +17 -0
  94. package/src/native/context.tsx +74 -0
  95. package/src/native/index.ts +39 -0
  96. package/src/native/primitives.tsx +730 -0
  97. package/src/native/screen.tsx +831 -0
  98. package/src/react/README.md +7 -0
  99. package/src/react/context.tsx +83 -0
  100. package/src/react/index.ts +42 -0
  101. package/src/react/primitives.tsx +589 -0
  102. package/src/react/screen.tsx +633 -0
  103. package/src/react/styles.css +299 -0
  104. package/src/shared/README.md +9 -0
  105. package/src/shared/messages.ts +99 -0
  106. package/src/shared/render.ts +12 -0
  107. package/src/shared/theme.ts +27 -0
  108. package/src/shared/types/board.ts +48 -0
  109. package/src/shared/types/comment.ts +101 -0
  110. package/src/shared/types/context.ts +81 -0
  111. package/src/shared/types/entry.ts +48 -0
  112. package/src/shared/types/form.ts +27 -0
  113. package/src/shared/types/helpers.ts +1 -0
  114. package/src/shared/types/index.ts +45 -0
  115. package/src/shared/types/messages.ts +160 -0
  116. package/src/shared/types/render.ts +12 -0
  117. package/src/shared/types/screen.ts +123 -0
  118. package/src/shared/types/theme.ts +61 -0
@@ -0,0 +1,730 @@
1
+ import type {
2
+ FeedbackComment,
3
+ FeedbackEntry as FeedbackEntryData,
4
+ } from "convex-feedback";
5
+ import { createContext, useContext, useMemo } from "react";
6
+ import {
7
+ Pressable,
8
+ ScrollView,
9
+ Text,
10
+ TextInput,
11
+ View,
12
+ type PressableProps,
13
+ type ScrollViewProps,
14
+ type StyleProp,
15
+ type TextInputProps,
16
+ type TextProps,
17
+ type TextStyle,
18
+ type ViewProps,
19
+ type ViewStyle,
20
+ } from "react-native";
21
+ import {
22
+ SafeAreaView,
23
+ type SafeAreaViewProps,
24
+ } from "react-native-safe-area-context";
25
+
26
+ import type {
27
+ BoardSearchBaseProps,
28
+ BoardSearchState,
29
+ CommentActionBaseProps,
30
+ CommentLikeBaseProps,
31
+ CommentLikeState,
32
+ CommentRootBaseProps,
33
+ EntryRootBaseProps,
34
+ EntryUpvoteBaseProps,
35
+ EntryUpvoteState,
36
+ FeedbackColorProps,
37
+ FormSubmitBaseProps,
38
+ FormSubmitState,
39
+ RepliesButtonBaseProps,
40
+ } from "../shared/types";
41
+ import { FeedbackNativeThemeScope, useFeedbackUi } from "./context.js";
42
+
43
+ function combineStyle<T>(
44
+ enabled: boolean,
45
+ fallback: StyleProp<T>,
46
+ style: StyleProp<T>,
47
+ ): StyleProp<T> {
48
+ return enabled ? [fallback, style] : style;
49
+ }
50
+
51
+ /**
52
+ * Props for `FeedbackBoard.Root`.
53
+ */
54
+ export interface BoardRootProps extends SafeAreaViewProps, FeedbackColorProps {}
55
+
56
+ function BoardRoot({
57
+ primaryColor,
58
+ backgroundColor,
59
+ surfaceColor,
60
+ textColor,
61
+ mutedColor,
62
+ borderColor,
63
+ dangerColor,
64
+ children,
65
+ style,
66
+ ...props
67
+ }: BoardRootProps) {
68
+ const { theme, unstyled } = useFeedbackUi();
69
+ const colorOverrides = useMemo(
70
+ () => ({
71
+ ...(primaryColor === undefined ? {} : { primary: primaryColor }),
72
+ ...(backgroundColor === undefined ? {} : { background: backgroundColor }),
73
+ ...(surfaceColor === undefined ? {} : { surface: surfaceColor }),
74
+ ...(textColor === undefined ? {} : { text: textColor }),
75
+ ...(mutedColor === undefined ? {} : { mutedText: mutedColor }),
76
+ ...(borderColor === undefined ? {} : { border: borderColor }),
77
+ ...(dangerColor === undefined ? {} : { danger: dangerColor }),
78
+ }),
79
+ [
80
+ backgroundColor,
81
+ borderColor,
82
+ dangerColor,
83
+ mutedColor,
84
+ primaryColor,
85
+ surfaceColor,
86
+ textColor,
87
+ ],
88
+ );
89
+ const background = backgroundColor ?? theme.colors.background;
90
+
91
+ return (
92
+ <FeedbackNativeThemeScope colors={colorOverrides}>
93
+ <SafeAreaView
94
+ {...props}
95
+ style={combineStyle<ViewStyle>(
96
+ !unstyled,
97
+ {
98
+ flex: 1,
99
+ gap: theme.spacing,
100
+ padding: theme.spacing,
101
+ backgroundColor: background,
102
+ },
103
+ style,
104
+ )}
105
+ >
106
+ {children}
107
+ </SafeAreaView>
108
+ </FeedbackNativeThemeScope>
109
+ );
110
+ }
111
+
112
+ function BoardHeader({ style, ...props }: ViewProps) {
113
+ const { theme, unstyled } = useFeedbackUi();
114
+ return (
115
+ <View
116
+ {...props}
117
+ style={combineStyle<ViewStyle>(!unstyled, { gap: theme.spacing }, style)}
118
+ />
119
+ );
120
+ }
121
+
122
+ function BoardTitle({ style, children, ...props }: TextProps) {
123
+ const { messages, theme, unstyled } = useFeedbackUi();
124
+ return (
125
+ <Text
126
+ {...props}
127
+ style={combineStyle<TextStyle>(
128
+ !unstyled,
129
+ { color: theme.colors.text, fontSize: 24, fontWeight: "700" },
130
+ style,
131
+ )}
132
+ >
133
+ {children ?? messages.board.title}
134
+ </Text>
135
+ );
136
+ }
137
+
138
+ /**
139
+ * Props for native `FeedbackBoard.Search`.
140
+ */
141
+ export interface BoardSearchProps
142
+ extends
143
+ Omit<TextInputProps, "children" | "value" | "onChangeText">,
144
+ BoardSearchBaseProps {}
145
+
146
+ function BoardSearch({
147
+ value,
148
+ onValueChange,
149
+ children,
150
+ style,
151
+ ...props
152
+ }: BoardSearchProps) {
153
+ const { messages, theme, unstyled } = useFeedbackUi();
154
+ const state: BoardSearchState = { value, setValue: onValueChange };
155
+ if (typeof children === "function") return <>{children(state)}</>;
156
+ return (
157
+ <TextInput
158
+ {...props}
159
+ value={value}
160
+ onChangeText={onValueChange}
161
+ placeholder={props.placeholder ?? messages.board.searchPlaceholder}
162
+ placeholderTextColor={
163
+ props.placeholderTextColor ?? theme.colors.mutedText
164
+ }
165
+ style={combineStyle<TextStyle>(
166
+ !unstyled,
167
+ {
168
+ color: theme.colors.text,
169
+ borderWidth: 1,
170
+ borderColor: theme.colors.border,
171
+ borderRadius: Math.max(8, theme.radius - 2),
172
+ paddingHorizontal: 12,
173
+ paddingVertical: 10,
174
+ backgroundColor: theme.colors.surface,
175
+ },
176
+ style,
177
+ )}
178
+ />
179
+ );
180
+ }
181
+
182
+ function BoardList({ style, ...props }: ScrollViewProps) {
183
+ const { theme, unstyled } = useFeedbackUi();
184
+ return (
185
+ <ScrollView
186
+ keyboardShouldPersistTaps="handled"
187
+ contentInsetAdjustmentBehavior="automatic"
188
+ automaticallyAdjustKeyboardInsets
189
+ style={{ flex: 1 }}
190
+ {...props}
191
+ contentContainerStyle={combineStyle<ViewStyle>(
192
+ !unstyled,
193
+ { gap: Math.max(8, theme.spacing - 2) },
194
+ style,
195
+ )}
196
+ />
197
+ );
198
+ }
199
+
200
+ export const FeedbackBoard = {
201
+ Root: BoardRoot,
202
+ Header: BoardHeader,
203
+ Title: BoardTitle,
204
+ Search: BoardSearch,
205
+ List: BoardList,
206
+ };
207
+
208
+ const EntryContext = createContext<FeedbackEntryData | null>(null);
209
+ function useEntryContext(): FeedbackEntryData {
210
+ const value = useContext(EntryContext);
211
+ if (value === null)
212
+ throw new Error(
213
+ "FeedbackEntry compound must be inside FeedbackEntry.Root.",
214
+ );
215
+ return value;
216
+ }
217
+
218
+ /**
219
+ * Props for native `FeedbackEntry.Root`.
220
+ */
221
+ export interface EntryRootProps extends ViewProps, EntryRootBaseProps {}
222
+
223
+ function EntryRoot({ entry, style, ...props }: EntryRootProps) {
224
+ const { theme, unstyled } = useFeedbackUi();
225
+ return (
226
+ <EntryContext.Provider value={entry}>
227
+ <View
228
+ {...props}
229
+ style={combineStyle<ViewStyle>(
230
+ !unstyled,
231
+ {
232
+ flexDirection: "row",
233
+ gap: 12,
234
+ padding: 12,
235
+ borderWidth: 1,
236
+ borderColor: theme.colors.border,
237
+ borderRadius: theme.radius,
238
+ backgroundColor: theme.colors.surface,
239
+ },
240
+ style,
241
+ )}
242
+ />
243
+ </EntryContext.Provider>
244
+ );
245
+ }
246
+
247
+ export interface EntryUpvoteProps
248
+ extends
249
+ Omit<PressableProps, "children" | "onPress" | "style">,
250
+ EntryUpvoteBaseProps {
251
+ primaryColor?: string;
252
+ style?: StyleProp<ViewStyle>;
253
+ }
254
+
255
+ function EntryUpvote({
256
+ onToggle,
257
+ children,
258
+ primaryColor,
259
+ style,
260
+ accessibilityLabel,
261
+ ...props
262
+ }: EntryUpvoteProps) {
263
+ const entry = useEntryContext();
264
+ const { theme, unstyled, messages } = useFeedbackUi();
265
+ const activeColor = primaryColor ?? theme.colors.primary;
266
+ const toggle = () => onToggle(!entry.viewerHasUpvoted);
267
+ const state: EntryUpvoteState = {
268
+ entry,
269
+ active: entry.viewerHasUpvoted,
270
+ count: entry.upvoteCount,
271
+ toggle,
272
+ };
273
+ if (typeof children === "function") return <>{children(state)}</>;
274
+ return (
275
+ <Pressable
276
+ {...props}
277
+ accessibilityLabel={
278
+ accessibilityLabel ??
279
+ (entry.viewerHasUpvoted
280
+ ? messages.entry.removeUpvote
281
+ : messages.entry.upvote)
282
+ }
283
+ onPress={toggle}
284
+ style={combineStyle<ViewStyle>(
285
+ !unstyled,
286
+ {
287
+ width: 48,
288
+ minHeight: 54,
289
+ alignItems: "center",
290
+ justifyContent: "center",
291
+ borderWidth: 1,
292
+ borderColor: entry.viewerHasUpvoted
293
+ ? activeColor
294
+ : theme.colors.border,
295
+ borderRadius: 10,
296
+ backgroundColor: theme.colors.surfaceMuted,
297
+ },
298
+ style,
299
+ )}
300
+ >
301
+ {children ?? (
302
+ <>
303
+ <Text
304
+ style={{
305
+ color: entry.viewerHasUpvoted
306
+ ? activeColor
307
+ : theme.colors.mutedText,
308
+ }}
309
+ >
310
+
311
+ </Text>
312
+ <Text
313
+ style={{
314
+ color: entry.viewerHasUpvoted ? activeColor : theme.colors.text,
315
+ fontWeight: "700",
316
+ }}
317
+ >
318
+ {entry.upvoteCount}
319
+ </Text>
320
+ </>
321
+ )}
322
+ </Pressable>
323
+ );
324
+ }
325
+ function EntryKind({ style, children, ...props }: TextProps) {
326
+ const entry = useEntryContext();
327
+ const { messages, theme, unstyled } = useFeedbackUi();
328
+
329
+ return (
330
+ <Text
331
+ {...props}
332
+ style={combineStyle<TextStyle>(
333
+ !unstyled,
334
+ {
335
+ color: theme.colors.mutedText,
336
+ fontSize: 12,
337
+ fontWeight: "600",
338
+ },
339
+ style,
340
+ )}
341
+ >
342
+ {children ?? messages.kinds[entry.kind]}
343
+ </Text>
344
+ );
345
+ }
346
+ function EntryContent({ style, ...props }: ViewProps) {
347
+ return <View {...props} style={[{ flex: 1, minWidth: 0 }, style]} />;
348
+ }
349
+ function EntryTitle({ style, children, ...props }: TextProps) {
350
+ const entry = useEntryContext();
351
+ const { theme, unstyled } = useFeedbackUi();
352
+ return (
353
+ <Text
354
+ {...props}
355
+ style={combineStyle<TextStyle>(
356
+ !unstyled,
357
+ { color: theme.colors.text, fontWeight: "700", fontSize: 16 },
358
+ style,
359
+ )}
360
+ >
361
+ {children ?? entry.title}
362
+ </Text>
363
+ );
364
+ }
365
+ function EntryBody({ style, children, ...props }: TextProps) {
366
+ const entry = useEntryContext();
367
+ const { theme, unstyled } = useFeedbackUi();
368
+ return (
369
+ <Text
370
+ {...props}
371
+ style={combineStyle<TextStyle>(
372
+ !unstyled,
373
+ { color: theme.colors.mutedText, lineHeight: 20 },
374
+ style,
375
+ )}
376
+ >
377
+ {children ?? entry.body}
378
+ </Text>
379
+ );
380
+ }
381
+ function EntryStatus({ style, children, ...props }: TextProps) {
382
+ const entry = useEntryContext();
383
+ const { messages, theme, unstyled } = useFeedbackUi();
384
+ return (
385
+ <Text
386
+ {...props}
387
+ style={combineStyle<TextStyle>(
388
+ !unstyled,
389
+ { color: theme.colors.mutedText, fontSize: 12, fontWeight: "600" },
390
+ style,
391
+ )}
392
+ >
393
+ {children ?? messages.statuses[entry.status]}
394
+ </Text>
395
+ );
396
+ }
397
+ function EntryCommentCount({ style, children, ...props }: TextProps) {
398
+ const entry = useEntryContext();
399
+ const { messages, theme, unstyled } = useFeedbackUi();
400
+ return (
401
+ <Text
402
+ {...props}
403
+ style={combineStyle<TextStyle>(
404
+ !unstyled,
405
+ { color: theme.colors.mutedText, fontSize: 12 },
406
+ style,
407
+ )}
408
+ >
409
+ {children ?? messages.entry.comments(entry.commentCount)}
410
+ </Text>
411
+ );
412
+ }
413
+ export const FeedbackEntry = {
414
+ Root: EntryRoot,
415
+ Upvote: EntryUpvote,
416
+ Content: EntryContent,
417
+ Kind: EntryKind,
418
+ Title: EntryTitle,
419
+ Body: EntryBody,
420
+ Status: EntryStatus,
421
+ CommentCount: EntryCommentCount,
422
+ };
423
+
424
+ const CommentContext = createContext<FeedbackComment | null>(null);
425
+ function useCommentContext(): FeedbackComment {
426
+ const value = useContext(CommentContext);
427
+ if (value === null)
428
+ throw new Error("Comment compound must be inside Comment.Root.");
429
+ return value;
430
+ }
431
+ export interface CommentRootProps extends ViewProps, CommentRootBaseProps {}
432
+
433
+ function CommentRoot({ comment, style, ...props }: CommentRootProps) {
434
+ const { theme, unstyled } = useFeedbackUi();
435
+ return (
436
+ <CommentContext.Provider value={comment}>
437
+ <View
438
+ {...props}
439
+ style={combineStyle<ViewStyle>(
440
+ !unstyled,
441
+ {
442
+ gap: 7,
443
+ padding: 10,
444
+ borderWidth: 1,
445
+ borderColor: theme.colors.border,
446
+ borderRadius: Math.max(8, theme.radius - 2),
447
+ backgroundColor: theme.colors.surface,
448
+ },
449
+ style,
450
+ )}
451
+ />
452
+ </CommentContext.Provider>
453
+ );
454
+ }
455
+ function CommentBody({ style, children, ...props }: TextProps) {
456
+ const comment = useCommentContext();
457
+ const { messages, theme, unstyled } = useFeedbackUi();
458
+ return (
459
+ <Text
460
+ {...props}
461
+ style={combineStyle<TextStyle>(
462
+ !unstyled,
463
+ {
464
+ color:
465
+ comment.body === null ? theme.colors.mutedText : theme.colors.text,
466
+ lineHeight: 20,
467
+ },
468
+ style,
469
+ )}
470
+ >
471
+ {children ?? comment.body ?? messages.comments.deleted}
472
+ </Text>
473
+ );
474
+ }
475
+
476
+ export interface CommentLikeProps
477
+ extends
478
+ Omit<PressableProps, "children" | "onPress" | "style">,
479
+ CommentLikeBaseProps {
480
+ primaryColor?: string;
481
+ style?: StyleProp<ViewStyle>;
482
+ }
483
+ function CommentLike({
484
+ onToggle,
485
+ children,
486
+ primaryColor,
487
+ style,
488
+ accessibilityLabel,
489
+ ...props
490
+ }: CommentLikeProps) {
491
+ const comment = useCommentContext();
492
+ const { theme, unstyled, messages } = useFeedbackUi();
493
+ const color = primaryColor ?? theme.colors.primary;
494
+ const toggle = () => onToggle(!comment.viewerHasLiked);
495
+ const state: CommentLikeState = {
496
+ comment,
497
+ active: comment.viewerHasLiked,
498
+ count: comment.likeCount,
499
+ toggle,
500
+ };
501
+ if (typeof children === "function") return <>{children(state)}</>;
502
+ return (
503
+ <Pressable
504
+ {...props}
505
+ accessibilityLabel={
506
+ accessibilityLabel ??
507
+ props["aria-label"] ??
508
+ (comment.viewerHasLiked
509
+ ? messages.comments.unlike
510
+ : messages.comments.like)
511
+ }
512
+ onPress={toggle}
513
+ style={combineStyle<ViewStyle>(
514
+ !unstyled,
515
+ {
516
+ flexDirection: "row",
517
+ gap: 4,
518
+ alignItems: "center",
519
+ paddingVertical: 4,
520
+ paddingHorizontal: 5,
521
+ },
522
+ style,
523
+ )}
524
+ >
525
+ {children ?? (
526
+ <>
527
+ <Text
528
+ style={{
529
+ color: comment.viewerHasLiked ? color : theme.colors.mutedText,
530
+ }}
531
+ >
532
+ {comment.viewerHasLiked ? "♥" : "♡"}
533
+ </Text>
534
+ <Text
535
+ style={{
536
+ color: comment.viewerHasLiked ? color : theme.colors.mutedText,
537
+ fontSize: 12,
538
+ }}
539
+ >
540
+ {comment.likeCount}
541
+ </Text>
542
+ </>
543
+ )}
544
+ </Pressable>
545
+ );
546
+ }
547
+
548
+ export interface CommentActionProps
549
+ extends
550
+ Omit<PressableProps, "children" | "onPress" | "style">,
551
+ CommentActionBaseProps {
552
+ style?: StyleProp<ViewStyle>;
553
+ }
554
+ function CommentReply({
555
+ onActivate,
556
+ children,
557
+ style,
558
+ ...props
559
+ }: CommentActionProps) {
560
+ const comment = useCommentContext();
561
+ const { messages, theme, unstyled } = useFeedbackUi();
562
+ const state = { comment, activate: onActivate };
563
+ if (typeof children === "function") return <>{children(state)}</>;
564
+ return (
565
+ <Pressable
566
+ {...props}
567
+ onPress={onActivate}
568
+ style={combineStyle<ViewStyle>(!unstyled, { padding: 5 }, style)}
569
+ >
570
+ <Text style={{ color: theme.colors.mutedText, fontSize: 12 }}>
571
+ {children ?? messages.comments.reply}
572
+ </Text>
573
+ </Pressable>
574
+ );
575
+ }
576
+
577
+ export interface RepliesButtonProps
578
+ extends
579
+ Omit<PressableProps, "children" | "onPress" | "style">,
580
+ RepliesButtonBaseProps {
581
+ style?: StyleProp<ViewStyle>;
582
+ }
583
+ function CommentRepliesButton({
584
+ expanded,
585
+ onExpandedChange,
586
+ children,
587
+ style,
588
+ ...props
589
+ }: RepliesButtonProps) {
590
+ const comment = useCommentContext();
591
+ const { messages, theme, unstyled } = useFeedbackUi();
592
+ const activate = () => onExpandedChange(!expanded);
593
+ const state = { comment, expanded, count: comment.replyCount, activate };
594
+ if (typeof children === "function") return <>{children(state)}</>;
595
+ if (comment.replyCount === 0) return null;
596
+ return (
597
+ <Pressable
598
+ {...props}
599
+ onPress={activate}
600
+ style={combineStyle<ViewStyle>(!unstyled, { padding: 5 }, style)}
601
+ >
602
+ <Text
603
+ style={{ color: theme.colors.primary, fontSize: 12, fontWeight: "600" }}
604
+ >
605
+ {children ??
606
+ (expanded
607
+ ? messages.comments.hideReplies
608
+ : messages.comments.viewReplies(comment.replyCount))}
609
+ </Text>
610
+ </Pressable>
611
+ );
612
+ }
613
+ function CommentChildren({ style, ...props }: ViewProps) {
614
+ const { theme, unstyled } = useFeedbackUi();
615
+ return (
616
+ <View
617
+ {...props}
618
+ style={combineStyle<ViewStyle>(
619
+ !unstyled,
620
+ {
621
+ gap: 8,
622
+ marginLeft: 14,
623
+ paddingLeft: 10,
624
+ borderLeftWidth: 2,
625
+ borderLeftColor: theme.colors.border,
626
+ },
627
+ style,
628
+ )}
629
+ />
630
+ );
631
+ }
632
+ export const Comment = {
633
+ Root: CommentRoot,
634
+ Body: CommentBody,
635
+ Like: CommentLike,
636
+ Reply: CommentReply,
637
+ RepliesButton: CommentRepliesButton,
638
+ Children: CommentChildren,
639
+ };
640
+
641
+ function FormRoot({ style, ...props }: ViewProps) {
642
+ const { unstyled } = useFeedbackUi();
643
+ return (
644
+ <View
645
+ {...props}
646
+ style={combineStyle<ViewStyle>(!unstyled, { gap: 9 }, style)}
647
+ />
648
+ );
649
+ }
650
+ function FormInput({ style, ...props }: TextInputProps) {
651
+ const { theme, unstyled } = useFeedbackUi();
652
+ return (
653
+ <TextInput
654
+ {...props}
655
+ placeholderTextColor={
656
+ props.placeholderTextColor ?? theme.colors.mutedText
657
+ }
658
+ style={combineStyle<TextStyle>(
659
+ !unstyled,
660
+ {
661
+ color: theme.colors.text,
662
+ borderWidth: 1,
663
+ borderColor: theme.colors.border,
664
+ borderRadius: Math.max(8, theme.radius - 2),
665
+ paddingHorizontal: 11,
666
+ paddingVertical: 9,
667
+ backgroundColor: theme.colors.surface,
668
+ },
669
+ style,
670
+ )}
671
+ />
672
+ );
673
+ }
674
+ function FormTextarea({ style, ...props }: TextInputProps) {
675
+ return (
676
+ <FormInput
677
+ {...props}
678
+ multiline
679
+ textAlignVertical={props.textAlignVertical ?? "top"}
680
+ style={[{ minHeight: 90 }, style]}
681
+ />
682
+ );
683
+ }
684
+
685
+ export interface FormSubmitProps
686
+ extends Omit<PressableProps, "children" | "style">, FormSubmitBaseProps {
687
+ primaryColor?: string;
688
+ style?: StyleProp<ViewStyle>;
689
+ }
690
+ function FormSubmit({
691
+ submitting = false,
692
+ children,
693
+ primaryColor,
694
+ disabled,
695
+ style,
696
+ ...props
697
+ }: FormSubmitProps) {
698
+ const { messages, theme, unstyled } = useFeedbackUi();
699
+ const state: FormSubmitState = { submitting };
700
+ if (typeof children === "function") return <>{children(state)}</>;
701
+ return (
702
+ <Pressable
703
+ {...props}
704
+ disabled={disabled ?? submitting}
705
+ style={combineStyle<ViewStyle>(
706
+ !unstyled,
707
+ {
708
+ minHeight: 42,
709
+ alignItems: "center",
710
+ justifyContent: "center",
711
+ borderRadius: Math.max(8, theme.radius - 2),
712
+ paddingHorizontal: 12,
713
+ backgroundColor: primaryColor ?? theme.colors.primary,
714
+ opacity: (disabled ?? submitting) ? 0.55 : 1,
715
+ },
716
+ style,
717
+ )}
718
+ >
719
+ <Text style={{ color: "#ffffff", fontWeight: "700" }}>
720
+ {children ?? messages.form.submit}
721
+ </Text>
722
+ </Pressable>
723
+ );
724
+ }
725
+ export const FeedbackForm = {
726
+ Root: FormRoot,
727
+ Input: FormInput,
728
+ Textarea: FormTextarea,
729
+ Submit: FormSubmit,
730
+ };