convex-feedback-ui 0.1.2 → 0.1.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (166) hide show
  1. package/README.md +115 -18
  2. package/dist/.tsbuildinfo +1 -1
  3. package/dist/native/expo/ExpoFeedbackBody.d.ts +4 -0
  4. package/dist/native/expo/ExpoFeedbackBody.d.ts.map +1 -0
  5. package/dist/native/expo/ExpoFeedbackBody.js +26 -0
  6. package/dist/native/expo/ExpoFeedbackBody.js.map +1 -0
  7. package/dist/native/expo/FeedbackScreen.d.ts +3 -0
  8. package/dist/native/expo/FeedbackScreen.d.ts.map +1 -0
  9. package/dist/native/expo/FeedbackScreen.js +9 -0
  10. package/dist/native/expo/FeedbackScreen.js.map +1 -0
  11. package/dist/native/expo/FeedbackStack.d.ts +3 -0
  12. package/dist/native/expo/FeedbackStack.d.ts.map +1 -0
  13. package/dist/native/expo/FeedbackStack.js +41 -0
  14. package/dist/native/expo/FeedbackStack.js.map +1 -0
  15. package/dist/native/expo/index.d.ts +9 -0
  16. package/dist/native/expo/index.d.ts.map +1 -0
  17. package/dist/native/expo/index.js +7 -0
  18. package/dist/native/expo/index.js.map +1 -0
  19. package/dist/native/expo/types.d.ts +20 -0
  20. package/dist/native/expo/types.d.ts.map +1 -0
  21. package/dist/native/expo/types.js +2 -0
  22. package/dist/native/expo/types.js.map +1 -0
  23. package/dist/native/native/FeedbackScreen.d.ts +3 -0
  24. package/dist/native/native/FeedbackScreen.d.ts.map +1 -0
  25. package/dist/native/native/FeedbackScreen.js +9 -0
  26. package/dist/native/native/FeedbackScreen.js.map +1 -0
  27. package/dist/native/native/index.d.ts +8 -0
  28. package/dist/native/native/index.d.ts.map +1 -0
  29. package/dist/native/native/index.js +7 -0
  30. package/dist/native/native/index.js.map +1 -0
  31. package/dist/native/shared/helpers.d.ts +3 -0
  32. package/dist/native/shared/helpers.d.ts.map +1 -0
  33. package/dist/native/shared/helpers.js +4 -0
  34. package/dist/native/shared/helpers.js.map +1 -0
  35. package/dist/native/shared/ui/BodyContent.d.ts +3 -0
  36. package/dist/native/shared/ui/BodyContent.d.ts.map +1 -0
  37. package/dist/native/shared/ui/BodyContent.js +18 -0
  38. package/dist/native/shared/ui/BodyContent.js.map +1 -0
  39. package/dist/native/shared/ui/Button.d.ts +5 -0
  40. package/dist/native/shared/ui/Button.d.ts.map +1 -0
  41. package/dist/native/shared/ui/Button.js +15 -0
  42. package/dist/native/shared/ui/Button.js.map +1 -0
  43. package/dist/native/shared/ui/CommentBranch.d.ts +3 -0
  44. package/dist/native/shared/ui/CommentBranch.d.ts.map +1 -0
  45. package/dist/native/shared/ui/CommentBranch.js +35 -0
  46. package/dist/native/shared/ui/CommentBranch.js.map +1 -0
  47. package/dist/native/shared/ui/EntryCard.d.ts +3 -0
  48. package/dist/native/shared/ui/EntryCard.d.ts.map +1 -0
  49. package/dist/native/shared/ui/EntryCard.js +13 -0
  50. package/dist/native/shared/ui/EntryCard.js.map +1 -0
  51. package/dist/native/shared/ui/EntryDetail.d.ts +3 -0
  52. package/dist/native/shared/ui/EntryDetail.d.ts.map +1 -0
  53. package/dist/native/shared/ui/EntryDetail.js +37 -0
  54. package/dist/native/shared/ui/EntryDetail.js.map +1 -0
  55. package/dist/native/shared/ui/FeedbackScreenHeader.d.ts +2 -0
  56. package/dist/native/shared/ui/FeedbackScreenHeader.d.ts.map +1 -0
  57. package/dist/native/shared/ui/FeedbackScreenHeader.js +20 -0
  58. package/dist/native/shared/ui/FeedbackScreenHeader.js.map +1 -0
  59. package/dist/native/shared/ui/FeedbackScreenList.d.ts +3 -0
  60. package/dist/native/shared/ui/FeedbackScreenList.d.ts.map +1 -0
  61. package/dist/native/shared/ui/FeedbackScreenList.js +19 -0
  62. package/dist/native/shared/ui/FeedbackScreenList.js.map +1 -0
  63. package/dist/native/shared/ui/NewEntry.d.ts +5 -0
  64. package/dist/native/shared/ui/NewEntry.d.ts.map +1 -0
  65. package/dist/native/shared/ui/NewEntry.js +158 -0
  66. package/dist/native/shared/ui/NewEntry.js.map +1 -0
  67. package/dist/native/shared/ui/ReplyList.d.ts +3 -0
  68. package/dist/native/shared/ui/ReplyList.d.ts.map +1 -0
  69. package/dist/native/shared/ui/ReplyList.js +19 -0
  70. package/dist/native/shared/ui/ReplyList.js.map +1 -0
  71. package/dist/native/{primitives.d.ts → shared/ui/primitives.d.ts} +10 -12
  72. package/dist/native/shared/ui/primitives.d.ts.map +1 -0
  73. package/dist/native/{primitives.js → shared/ui/primitives.js} +19 -25
  74. package/dist/native/shared/ui/primitives.js.map +1 -0
  75. package/dist/react/{context.d.ts → css.d.ts} +2 -4
  76. package/dist/react/css.d.ts.map +1 -0
  77. package/dist/react/css.js +18 -0
  78. package/dist/react/css.js.map +1 -0
  79. package/dist/react/index.d.ts +1 -1
  80. package/dist/react/index.d.ts.map +1 -1
  81. package/dist/react/index.js +1 -1
  82. package/dist/react/index.js.map +1 -1
  83. package/dist/react/primitives.d.ts.map +1 -1
  84. package/dist/react/primitives.js +2 -1
  85. package/dist/react/primitives.js.map +1 -1
  86. package/dist/react/screen.d.ts +3 -3
  87. package/dist/react/screen.d.ts.map +1 -1
  88. package/dist/react/screen.js +25 -22
  89. package/dist/react/screen.js.map +1 -1
  90. package/dist/shared/context/FeedbackBodyProvider.d.ts +5 -0
  91. package/dist/shared/context/FeedbackBodyProvider.d.ts.map +1 -0
  92. package/dist/shared/context/FeedbackBodyProvider.js +43 -0
  93. package/dist/shared/context/FeedbackBodyProvider.js.map +1 -0
  94. package/dist/{native/context.d.ts → shared/context/FeedbackProvider.d.ts} +2 -2
  95. package/dist/shared/context/FeedbackProvider.d.ts.map +1 -0
  96. package/dist/{native/context.js → shared/context/FeedbackProvider.js} +3 -3
  97. package/dist/shared/context/FeedbackProvider.js.map +1 -0
  98. package/dist/shared/helpers.d.ts +7 -0
  99. package/dist/shared/helpers.d.ts.map +1 -0
  100. package/dist/shared/helpers.js +24 -0
  101. package/dist/shared/helpers.js.map +1 -0
  102. package/dist/shared/messages.d.ts.map +1 -1
  103. package/dist/shared/messages.js +23 -14
  104. package/dist/shared/messages.js.map +1 -1
  105. package/dist/shared/theme.d.ts.map +1 -1
  106. package/dist/shared/theme.js +1 -0
  107. package/dist/shared/theme.js.map +1 -1
  108. package/dist/shared/types/context.d.ts +4 -0
  109. package/dist/shared/types/context.d.ts.map +1 -1
  110. package/dist/shared/types/messages.d.ts +12 -1
  111. package/dist/shared/types/messages.d.ts.map +1 -1
  112. package/dist/shared/types/screen.d.ts +55 -18
  113. package/dist/shared/types/screen.d.ts.map +1 -1
  114. package/dist/shared/types/theme.d.ts +4 -0
  115. package/dist/shared/types/theme.d.ts.map +1 -1
  116. package/package.json +27 -12
  117. package/src/native/expo/ExpoFeedbackBody.tsx +48 -0
  118. package/src/native/expo/FeedbackScreen.tsx +37 -0
  119. package/src/native/expo/FeedbackStack.tsx +99 -0
  120. package/src/native/expo/README.md +17 -0
  121. package/src/native/expo/index.ts +8 -0
  122. package/src/native/expo/types.ts +23 -0
  123. package/src/native/native/FeedbackScreen.tsx +37 -0
  124. package/src/native/native/index.ts +7 -0
  125. package/src/native/shared/helpers.ts +9 -0
  126. package/src/native/shared/ui/BodyContent.tsx +43 -0
  127. package/src/native/shared/ui/Button.tsx +29 -0
  128. package/src/native/shared/ui/CommentBranch.tsx +88 -0
  129. package/src/native/shared/ui/EntryCard.tsx +37 -0
  130. package/src/native/shared/ui/EntryDetail.tsx +103 -0
  131. package/src/native/shared/ui/FeedbackScreenHeader.tsx +40 -0
  132. package/src/native/shared/ui/FeedbackScreenList.tsx +61 -0
  133. package/src/native/shared/ui/NewEntry.tsx +375 -0
  134. package/src/native/shared/ui/ReplyList.tsx +41 -0
  135. package/src/native/{primitives.tsx → shared/ui/primitives.tsx} +84 -92
  136. package/src/react/{context.tsx → css.tsx} +2 -44
  137. package/src/react/index.ts +1 -1
  138. package/src/react/primitives.tsx +2 -1
  139. package/src/react/screen.tsx +67 -123
  140. package/src/shared/context/FeedbackBodyProvider.tsx +82 -0
  141. package/src/{native/context.tsx → shared/context/FeedbackProvider.tsx} +3 -3
  142. package/src/shared/helpers.ts +33 -0
  143. package/src/shared/messages.ts +23 -22
  144. package/src/shared/theme.ts +1 -0
  145. package/src/shared/types/context.ts +5 -0
  146. package/src/shared/types/messages.ts +16 -1
  147. package/src/shared/types/screen.ts +69 -23
  148. package/src/shared/types/theme.ts +5 -0
  149. package/dist/native/context.d.ts.map +0 -1
  150. package/dist/native/context.js.map +0 -1
  151. package/dist/native/index.d.ts +0 -7
  152. package/dist/native/index.d.ts.map +0 -1
  153. package/dist/native/index.js +0 -6
  154. package/dist/native/index.js.map +0 -1
  155. package/dist/native/primitives.d.ts.map +0 -1
  156. package/dist/native/primitives.js.map +0 -1
  157. package/dist/native/screen.d.ts +0 -3
  158. package/dist/native/screen.d.ts.map +0 -1
  159. package/dist/native/screen.js +0 -284
  160. package/dist/native/screen.js.map +0 -1
  161. package/dist/react/context.d.ts.map +0 -1
  162. package/dist/react/context.js +0 -41
  163. package/dist/react/context.js.map +0 -1
  164. package/src/native/index.ts +0 -6
  165. package/src/native/screen.tsx +0 -831
  166. /package/src/native/{README.md → native/README.md} +0 -0
@@ -1,19 +1,25 @@
1
1
  "use client";
2
2
 
3
- import type { CommentSort, EntryKind, EntrySort } from "convex-feedback";
4
- import type { FeedbackHooks } from "convex-feedback/react";
3
+ import type { EntryKind } from "convex-feedback";
5
4
  import { useMemo, useState, type SyntheticEvent } from "react";
6
5
 
6
+ import {
7
+ FeedbackBodyProvider,
8
+ useFeedbackBody,
9
+ } from "../shared/context/FeedbackBodyProvider.js";
10
+ import {
11
+ FeedbackProvider,
12
+ useFeedbackUi,
13
+ } from "../shared/context/FeedbackProvider";
7
14
  import type {
8
- FeedbackColorProps,
9
15
  FeedbackScreenCommentBranchProps,
16
+ FeedbackScreenContentProps,
10
17
  FeedbackScreenEntryCardProps,
11
18
  FeedbackScreenEntryDetailProps,
12
19
  FeedbackScreenEntryModalProps,
13
20
  FeedbackScreenReplyListProps,
14
21
  FeedbackScreenRootProps,
15
22
  } from "../shared/types/";
16
- import { FeedbackProvider, useFeedbackUi } from "./context.js";
17
23
  import {
18
24
  Comment,
19
25
  FeedbackBoard,
@@ -27,8 +33,7 @@ const entryKinds: readonly EntryKind[] = [
27
33
  "bug_report",
28
34
  ];
29
35
 
30
- export interface FeedbackScreenProps
31
- extends FeedbackColorProps, FeedbackScreenRootProps {
36
+ export interface FeedbackScreenProps extends FeedbackScreenRootProps {
32
37
  className?: string | undefined;
33
38
  }
34
39
 
@@ -43,55 +48,52 @@ export function FeedbackScreen({
43
48
  maxCommentDepth = 5,
44
49
  transformComments,
45
50
  renderActor,
46
- className,
47
- ...colors
51
+ debounceDuration = 300,
52
+ ...props
48
53
  }: FeedbackScreenProps) {
49
54
  return (
50
55
  <FeedbackProvider messages={messages} theme={theme} unstyled={unstyled}>
51
- <FeedbackScreenInner
56
+ <FeedbackBodyProvider
52
57
  hooks={hooks}
53
58
  entrySort={entrySort}
54
59
  commentSort={commentSort}
55
60
  enabledKinds={enabledKinds}
56
61
  maxCommentDepth={maxCommentDepth}
62
+ debounceDuration={debounceDuration}
57
63
  transformComments={transformComments}
58
64
  renderActor={renderActor}
59
- className={className}
60
- {...colors}
61
- />
65
+ >
66
+ <FeedbackScreenInner {...props} />
67
+ </FeedbackBodyProvider>
62
68
  </FeedbackProvider>
63
69
  );
64
70
  }
65
71
 
66
- interface FeedbackScreenInnerProps extends FeedbackColorProps {
67
- hooks: FeedbackHooks;
68
- entrySort: EntrySort;
69
- commentSort: CommentSort;
70
- enabledKinds: readonly EntryKind[];
71
- maxCommentDepth: number;
72
- transformComments?: FeedbackScreenProps["transformComments"];
73
- renderActor?: FeedbackScreenProps["renderActor"];
72
+ interface FeedbackScreenInnerProps extends FeedbackScreenContentProps {
74
73
  className?: string | undefined;
75
74
  }
76
75
 
77
76
  function FeedbackScreenInner({
78
- hooks,
79
- entrySort,
80
- commentSort,
81
- enabledKinds,
82
- maxCommentDepth,
83
- transformComments,
84
- renderActor,
85
77
  className,
86
78
  ...colors
87
79
  }: FeedbackScreenInnerProps) {
80
+ const {
81
+ hooks,
82
+ entrySort,
83
+ enabledKinds,
84
+ showForm,
85
+ setShowForm,
86
+ setSelectedEntryId,
87
+ selectedEntryId,
88
+ query,
89
+ setQuery,
90
+ debouncedQuery,
91
+ } = useFeedbackBody();
88
92
  const { messages } = useFeedbackUi();
89
- const [searchQuery, setSearchQuery] = useState("");
90
- const [showForm, setShowForm] = useState(false);
91
- const [selectedEntryId, setSelectedEntryId] = useState<string | null>(null);
93
+
92
94
  const list = hooks.useEntries({ sort: entrySort, kinds: enabledKinds });
93
95
  const searchResults = hooks.useSearchEntries({
94
- searchQuery,
96
+ searchQuery: debouncedQuery,
95
97
  kinds: enabledKinds,
96
98
  });
97
99
 
@@ -99,19 +101,14 @@ function FeedbackScreenInner({
99
101
  return (
100
102
  <FeedbackBoard.Root className={className} {...colors}>
101
103
  <EntryDetail
102
- hooks={hooks}
103
104
  entryId={selectedEntryId}
104
105
  onBack={() => setSelectedEntryId(null)}
105
- commentSort={commentSort}
106
- maxCommentDepth={maxCommentDepth}
107
- transformComments={transformComments}
108
- renderActor={renderActor}
109
106
  />
110
107
  </FeedbackBoard.Root>
111
108
  );
112
109
  }
113
110
 
114
- const searching = searchQuery.trim().length > 0;
111
+ const searching = query.trim().length > 0;
115
112
  const entries = searching ? searchResults : list.results;
116
113
  const loading = searching
117
114
  ? searchResults === undefined
@@ -133,30 +130,25 @@ function FeedbackScreenInner({
133
130
  </button>
134
131
  </FeedbackBoard.Header>
135
132
 
136
- {showForm ? (
133
+ {showForm && (
137
134
  <CreateEntryForm
138
- hooks={hooks}
139
- enabledKinds={enabledKinds}
140
135
  onCreated={(entryId) => {
141
136
  setShowForm(false);
142
137
  setSelectedEntryId(entryId);
143
138
  }}
144
139
  />
145
- ) : null}
140
+ )}
146
141
 
147
- <FeedbackBoard.Search
148
- value={searchQuery}
149
- onValueChange={setSearchQuery}
150
- />
142
+ <FeedbackBoard.Search value={query} onValueChange={setQuery} />
151
143
 
152
- {loading ? <p className="cf-state">{messages.board.loading}</p> : null}
153
- {!loading && entries !== undefined && entries.length === 0 ? (
144
+ {loading && <p className="cf-state">{messages.board.loading}</p>}
145
+ {!loading && entries !== undefined && entries.length === 0 && (
154
146
  <p className="cf-state">
155
147
  {searching
156
148
  ? messages.board.noSearchResults
157
149
  : messages.board.noEntries}
158
150
  </p>
159
- ) : null}
151
+ )}
160
152
 
161
153
  <FeedbackBoard.List>
162
154
  {(entries ?? []).map((entry) => (
@@ -169,7 +161,7 @@ function FeedbackScreenInner({
169
161
  ))}
170
162
  </FeedbackBoard.List>
171
163
 
172
- {!searching && list.status === "CanLoadMore" ? (
164
+ {!searching && list.status === "CanLoadMore" && (
173
165
  <button
174
166
  type="button"
175
167
  className="cf-button"
@@ -177,7 +169,7 @@ function FeedbackScreenInner({
177
169
  >
178
170
  {messages.board.loadMore}
179
171
  </button>
180
- ) : null}
172
+ )}
181
173
  </FeedbackBoard.Root>
182
174
  );
183
175
  }
@@ -214,11 +206,8 @@ function EntryCard({ entry, hooks, onOpen }: FeedbackScreenEntryCardProps) {
214
206
  );
215
207
  }
216
208
 
217
- function CreateEntryForm({
218
- hooks,
219
- enabledKinds,
220
- onCreated,
221
- }: FeedbackScreenEntryModalProps) {
209
+ function CreateEntryForm({ onCreated }: FeedbackScreenEntryModalProps) {
210
+ const { hooks, enabledKinds } = useFeedbackBody();
222
211
  const { messages } = useFeedbackUi();
223
212
  const [kind, setKind] = useState<EntryKind>(enabledKinds[0] ?? "feedback");
224
213
  const [title, setTitle] = useState("");
@@ -385,15 +374,8 @@ function CreateEntryForm({
385
374
  );
386
375
  }
387
376
 
388
- function EntryDetail({
389
- hooks,
390
- entryId,
391
- onBack,
392
- commentSort,
393
- maxCommentDepth,
394
- transformComments,
395
- renderActor,
396
- }: FeedbackScreenEntryDetailProps) {
377
+ function EntryDetail({ entryId, onBack }: FeedbackScreenEntryDetailProps) {
378
+ const { hooks, commentSort, transformComments } = useFeedbackBody();
397
379
  const { messages } = useFeedbackUi();
398
380
  const entry = hooks.useEntry(entryId);
399
381
  const setUpvote = hooks.useSetEntryUpvote();
@@ -461,28 +443,23 @@ function EntryDetail({
461
443
  </button>
462
444
  </FeedbackForm.Root>
463
445
 
464
- {comments.status === "LoadingFirstPage" ? (
446
+ {comments.status === "LoadingFirstPage" && (
465
447
  <p className="cf-state">{messages.board.loading}</p>
466
- ) : null}
448
+ )}
467
449
  {comments.status !== "LoadingFirstPage" &&
468
- visibleComments.length === 0 ? (
469
- <p className="cf-state">{messages.comments.noComments}</p>
470
- ) : null}
450
+ visibleComments.length === 0 && (
451
+ <p className="cf-state">{messages.comments.noComments}</p>
452
+ )}
471
453
  <div className="cf-comments">
472
454
  {visibleComments.map((comment) => (
473
455
  <CommentBranch
474
456
  key={comment.id}
475
457
  comment={comment}
476
458
  entryId={entryId}
477
- hooks={hooks}
478
- commentSort={commentSort}
479
- maxCommentDepth={maxCommentDepth}
480
- transformComments={transformComments}
481
- renderActor={renderActor}
482
459
  />
483
460
  ))}
484
461
  </div>
485
- {comments.status === "CanLoadMore" ? (
462
+ {comments.status === "CanLoadMore" && (
486
463
  <button
487
464
  type="button"
488
465
  className="cf-button"
@@ -490,21 +467,14 @@ function EntryDetail({
490
467
  >
491
468
  {messages.comments.loadMore}
492
469
  </button>
493
- ) : null}
470
+ )}
494
471
  </section>
495
472
  </div>
496
473
  );
497
474
  }
498
475
 
499
- function CommentBranch({
500
- comment,
501
- entryId,
502
- hooks,
503
- commentSort,
504
- maxCommentDepth,
505
- transformComments,
506
- renderActor,
507
- }: FeedbackScreenCommentBranchProps) {
476
+ function CommentBranch({ comment, entryId }: FeedbackScreenCommentBranchProps) {
477
+ const { hooks, maxCommentDepth, renderActor } = useFeedbackBody();
508
478
  const { messages } = useFeedbackUi();
509
479
  const [expanded, setExpanded] = useState(false);
510
480
  const [replying, setReplying] = useState(false);
@@ -524,15 +494,15 @@ function CommentBranch({
524
494
  void setLike({ commentId: comment.id, desiredState: active })
525
495
  }
526
496
  />
527
- {comment.body !== null && comment.depth < maxCommentDepth ? (
497
+ {comment.body !== null && comment.depth < maxCommentDepth && (
528
498
  <Comment.Reply onActivate={() => setReplying((value) => !value)} />
529
- ) : null}
499
+ )}
530
500
  <Comment.RepliesButton
531
501
  expanded={expanded}
532
502
  onExpandedChange={setExpanded}
533
503
  />
534
504
  </div>
535
- {replying ? (
505
+ {replying && (
536
506
  <FeedbackForm.Root
537
507
  onSubmit={async (event) => {
538
508
  event.preventDefault();
@@ -566,31 +536,14 @@ function CommentBranch({
566
536
  </button>
567
537
  </div>
568
538
  </FeedbackForm.Root>
569
- ) : null}
570
- {expanded ? (
571
- <ReplyList
572
- hooks={hooks}
573
- entryId={entryId}
574
- parentCommentId={comment.id}
575
- commentSort={commentSort}
576
- maxCommentDepth={maxCommentDepth}
577
- transformComments={transformComments}
578
- renderActor={renderActor}
579
- />
580
- ) : null}
539
+ )}
540
+ {expanded && <ReplyList entryId={entryId} parentCommentId={comment.id} />}
581
541
  </Comment.Root>
582
542
  );
583
543
  }
584
544
 
585
- function ReplyList({
586
- hooks,
587
- entryId,
588
- parentCommentId,
589
- commentSort,
590
- maxCommentDepth,
591
- transformComments,
592
- renderActor,
593
- }: FeedbackScreenReplyListProps) {
545
+ function ReplyList({ entryId, parentCommentId }: FeedbackScreenReplyListProps) {
546
+ const { hooks, commentSort, transformComments } = useFeedbackBody();
594
547
  const { messages } = useFeedbackUi();
595
548
  const replies = hooks.useComments({
596
549
  entryId,
@@ -604,22 +557,13 @@ function ReplyList({
604
557
 
605
558
  return (
606
559
  <Comment.Children>
607
- {replies.status === "LoadingFirstPage" ? (
560
+ {replies.status === "LoadingFirstPage" && (
608
561
  <p className="cf-state">{messages.board.loading}</p>
609
- ) : null}
562
+ )}
610
563
  {visibleReplies.map((reply) => (
611
- <CommentBranch
612
- key={reply.id}
613
- comment={reply}
614
- entryId={entryId}
615
- hooks={hooks}
616
- commentSort={commentSort}
617
- maxCommentDepth={maxCommentDepth}
618
- transformComments={transformComments}
619
- renderActor={renderActor}
620
- />
564
+ <CommentBranch key={reply.id} comment={reply} entryId={entryId} />
621
565
  ))}
622
- {replies.status === "CanLoadMore" ? (
566
+ {replies.status === "CanLoadMore" && (
623
567
  <button
624
568
  type="button"
625
569
  className="cf-button"
@@ -627,7 +571,7 @@ function ReplyList({
627
571
  >
628
572
  {messages.comments.loadMore}
629
573
  </button>
630
- ) : null}
574
+ )}
631
575
  </Comment.Children>
632
576
  );
633
577
  }
@@ -0,0 +1,82 @@
1
+ import {
2
+ createContext,
3
+ type PropsWithChildren,
4
+ useContext,
5
+ useEffect,
6
+ useMemo,
7
+ useState,
8
+ } from "react";
9
+ import { debounce } from "../helpers";
10
+ import type {
11
+ FeedbackScreenBodyContextValue,
12
+ FeedbackScreenProviderProps,
13
+ } from "../types";
14
+
15
+ const FeedbackBodyContext =
16
+ createContext<FeedbackScreenBodyContextValue | null>(null);
17
+
18
+ export function FeedbackBodyProvider({
19
+ hooks,
20
+ entrySort,
21
+ commentSort,
22
+ enabledKinds,
23
+ maxCommentDepth,
24
+ debounceDuration,
25
+ children,
26
+ }: PropsWithChildren<FeedbackScreenProviderProps>) {
27
+ const [selectedEntryId, setSelectedEntryId] = useState<string | null>(null);
28
+ const [showForm, setShowForm] = useState(false);
29
+ const [isSearching, setIsSearching] = useState(false);
30
+ const [query, setQuery] = useState("");
31
+ const [debouncedQuery, setDebouncedQuery] = useState("");
32
+
33
+ const updateDebouncedQuery = useMemo(
34
+ () => debounce(setDebouncedQuery, debounceDuration),
35
+ [],
36
+ );
37
+
38
+ useEffect(() => {
39
+ updateDebouncedQuery(query);
40
+
41
+ return updateDebouncedQuery.cancel;
42
+ }, [query, updateDebouncedQuery]);
43
+
44
+ const value = {
45
+ query,
46
+ debouncedQuery,
47
+ showForm,
48
+ isSearching,
49
+ selectedEntryId,
50
+
51
+ setQuery,
52
+ setDebouncedQuery,
53
+ setShowForm,
54
+ setIsSearching,
55
+ setSelectedEntryId,
56
+
57
+ hooks,
58
+ entrySort,
59
+ commentSort,
60
+ enabledKinds,
61
+ maxCommentDepth,
62
+ debounceDuration,
63
+ } satisfies FeedbackScreenBodyContextValue;
64
+
65
+ return (
66
+ <FeedbackBodyContext.Provider value={value}>
67
+ {children}
68
+ </FeedbackBodyContext.Provider>
69
+ );
70
+ }
71
+
72
+ export function useFeedbackBody(): FeedbackScreenBodyContextValue {
73
+ const context = useContext(FeedbackBodyContext);
74
+
75
+ if (!context) {
76
+ throw new Error(
77
+ "useOnboardingFlow must be used inside OnboardingFlowProvider",
78
+ );
79
+ }
80
+
81
+ return context;
82
+ }
@@ -1,13 +1,13 @@
1
1
  import { createContext, useContext, useMemo, type ReactNode } from "react";
2
2
 
3
- import { mergeFeedbackMessages } from "../shared/messages.js";
3
+ import { mergeFeedbackMessages } from "../../shared/messages.js";
4
4
 
5
- import { mergeFeedbackTheme } from "../shared/theme.js";
5
+ import { mergeFeedbackTheme } from "../../shared/theme.js";
6
6
  import type {
7
7
  FeedbackProviderProps,
8
8
  FeedbackTheme,
9
9
  FeedbackUiContextValue,
10
- } from "../shared/types";
10
+ } from "../../shared/types/index.js";
11
11
 
12
12
  const FeedbackNativeContext = createContext<FeedbackUiContextValue | null>(
13
13
  null,
@@ -0,0 +1,33 @@
1
+ import type { EntryKind } from "convex-feedback";
2
+
3
+ export const kinds: readonly EntryKind[] = [
4
+ "feedback",
5
+ "feature_request",
6
+ "bug_report",
7
+ ];
8
+
9
+ export function debounce<TArgs extends unknown[]>(
10
+ callback: (...args: TArgs) => void,
11
+ delay: number,
12
+ ) {
13
+ let timeout: ReturnType<typeof setTimeout> | undefined;
14
+
15
+ const debounced = (...args: TArgs) => {
16
+ if (timeout !== undefined) {
17
+ clearTimeout(timeout);
18
+ }
19
+
20
+ timeout = setTimeout(() => {
21
+ callback(...args);
22
+ }, delay);
23
+ };
24
+
25
+ debounced.cancel = () => {
26
+ if (timeout !== undefined) {
27
+ clearTimeout(timeout);
28
+ timeout = undefined;
29
+ }
30
+ };
31
+
32
+ return debounced;
33
+ }
@@ -16,6 +16,16 @@ export const englishFeedbackMessages: FeedbackMessages = {
16
16
  feature_request: "Feature request",
17
17
  bug_report: "Bug report",
18
18
  },
19
+ kindsPlural: {
20
+ feedback: "Feedback",
21
+ feature_request: "Feature requests",
22
+ bug_report: "Bug reports",
23
+ },
24
+ newFeedback: {
25
+ feedback: "New feedback",
26
+ feature_request: "New feature request",
27
+ bug_report: "New bug report",
28
+ },
19
29
  statuses: {
20
30
  open: "Open",
21
31
  under_review: "Under review",
@@ -29,7 +39,7 @@ export const englishFeedbackMessages: FeedbackMessages = {
29
39
  removeUpvote: "Remove upvote",
30
40
  comments: (count) => `${count} ${count === 1 ? "comment" : "comments"}`,
31
41
  open: "Open",
32
- back: "Back to feedback",
42
+ back: "Go back",
33
43
  },
34
44
  form: {
35
45
  kind: "Type",
@@ -68,29 +78,20 @@ export function mergeFeedbackMessages(
68
78
  return {
69
79
  board: { ...englishFeedbackMessages.board, ...overrides.board },
70
80
  kinds: {
71
- feedback:
72
- overrides.kinds?.feedback ?? englishFeedbackMessages.kinds.feedback,
73
- feature_request:
74
- overrides.kinds?.feature_request ??
75
- englishFeedbackMessages.kinds.feature_request,
76
- bug_report:
77
- overrides.kinds?.bug_report ?? englishFeedbackMessages.kinds.bug_report,
81
+ ...englishFeedbackMessages.kinds,
82
+ ...overrides.kinds,
83
+ },
84
+ kindsPlural: {
85
+ ...englishFeedbackMessages.kindsPlural,
86
+ ...overrides.kindsPlural,
87
+ },
88
+ newFeedback: {
89
+ ...englishFeedbackMessages.newFeedback,
90
+ ...overrides.newFeedback,
78
91
  },
79
92
  statuses: {
80
- open: overrides.statuses?.open ?? englishFeedbackMessages.statuses.open,
81
- under_review:
82
- overrides.statuses?.under_review ??
83
- englishFeedbackMessages.statuses.under_review,
84
- planned:
85
- overrides.statuses?.planned ?? englishFeedbackMessages.statuses.planned,
86
- in_progress:
87
- overrides.statuses?.in_progress ??
88
- englishFeedbackMessages.statuses.in_progress,
89
- completed:
90
- overrides.statuses?.completed ??
91
- englishFeedbackMessages.statuses.completed,
92
- closed:
93
- overrides.statuses?.closed ?? englishFeedbackMessages.statuses.closed,
93
+ ...englishFeedbackMessages.statuses,
94
+ ...overrides.statuses,
94
95
  },
95
96
  entry: { ...englishFeedbackMessages.entry, ...overrides.entry },
96
97
  form: { ...englishFeedbackMessages.form, ...overrides.form },
@@ -3,6 +3,7 @@ import type { FeedbackTheme, FeedbackThemeOverride } from "./types";
3
3
  export const defaultFeedbackTheme: FeedbackTheme = {
4
4
  colors: {
5
5
  primary: "#5b5bd6",
6
+ primaryForeground: "#ffffff",
6
7
  background: "#ffffff",
7
8
  surface: "#ffffff",
8
9
  surfaceMuted: "#f6f6f8",
@@ -61,6 +61,11 @@ export interface FeedbackColorProps {
61
61
  /** Primary interaction and accent color. */
62
62
  primaryColor?: string;
63
63
 
64
+ /**
65
+ * Foreground color for primary interactions and accents.
66
+ */
67
+ primaryForeground?: string;
68
+
64
69
  /** Main background color. */
65
70
  backgroundColor?: string;
66
71
 
@@ -18,7 +18,10 @@ export interface FeedbackMessages {
18
18
  /** Search input placeholder. */
19
19
  searchPlaceholder: string;
20
20
 
21
- /** CTA for opening the create-entry flow. */
21
+ /** CTA for opening the create-entry flow.
22
+ * This is shown when enabled kinds are two or more.
23
+ * Use messages.newFeedback for a single-kind entry.
24
+ */
22
25
  createEntry: string;
23
26
 
24
27
  /** Empty-state copy when no entries exist. */
@@ -37,6 +40,12 @@ export interface FeedbackMessages {
37
40
  /** Localized display labels for each fixed entry kind. */
38
41
  kinds: Record<EntryKind, string>;
39
42
 
43
+ /** Localized plural display labels for each fixed entry kind. */
44
+ kindsPlural: Record<EntryKind, string>;
45
+
46
+ /** Localized display labels for creating a new entry. */
47
+ newFeedback: Record<EntryKind, string>;
48
+
40
49
  /** Localized display labels for each fixed workflow status. */
41
50
  statuses: Record<EntryStatus, string>;
42
51
 
@@ -146,6 +155,12 @@ export interface FeedbackMessageOverrides {
146
155
  /** Entry-kind label overrides. */
147
156
  kinds?: Partial<FeedbackMessages["kinds"]> | undefined;
148
157
 
158
+ /** Entry-kind plural label overrides. */
159
+ kindsPlural?: Partial<FeedbackMessages["kindsPlural"]> | undefined;
160
+
161
+ /** Create single entry copy overrides. */
162
+ newFeedback?: Partial<FeedbackMessages["newFeedback"]> | undefined;
163
+
149
164
  /** Workflow-status label overrides. */
150
165
  statuses?: Partial<FeedbackMessages["statuses"]> | undefined;
151
166