convex-feedback-ui 0.1.2 → 0.1.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (165) hide show
  1. package/README.md +119 -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 +5 -0
  44. package/dist/native/shared/ui/CommentBranch.d.ts.map +1 -0
  45. package/dist/native/shared/ui/CommentBranch.js +45 -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 +22 -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 +122 -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 +42 -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/{primitives.tsx → shared/ui/primitives.tsx} +84 -92
  135. package/src/react/{context.tsx → css.tsx} +2 -44
  136. package/src/react/index.ts +1 -1
  137. package/src/react/primitives.tsx +2 -1
  138. package/src/react/screen.tsx +67 -123
  139. package/src/shared/context/FeedbackBodyProvider.tsx +82 -0
  140. package/src/{native/context.tsx → shared/context/FeedbackProvider.tsx} +3 -3
  141. package/src/shared/helpers.ts +33 -0
  142. package/src/shared/messages.ts +23 -22
  143. package/src/shared/theme.ts +1 -0
  144. package/src/shared/types/context.ts +5 -0
  145. package/src/shared/types/messages.ts +16 -1
  146. package/src/shared/types/screen.ts +69 -23
  147. package/src/shared/types/theme.ts +5 -0
  148. package/dist/native/context.d.ts.map +0 -1
  149. package/dist/native/context.js.map +0 -1
  150. package/dist/native/index.d.ts +0 -7
  151. package/dist/native/index.d.ts.map +0 -1
  152. package/dist/native/index.js +0 -6
  153. package/dist/native/index.js.map +0 -1
  154. package/dist/native/primitives.d.ts.map +0 -1
  155. package/dist/native/primitives.js.map +0 -1
  156. package/dist/native/screen.d.ts +0 -3
  157. package/dist/native/screen.d.ts.map +0 -1
  158. package/dist/native/screen.js +0 -284
  159. package/dist/native/screen.js.map +0 -1
  160. package/dist/react/context.d.ts.map +0 -1
  161. package/dist/react/context.js +0 -41
  162. package/dist/react/context.js.map +0 -1
  163. package/src/native/index.ts +0 -6
  164. package/src/native/screen.tsx +0 -831
  165. /package/src/native/{README.md → native/README.md} +0 -0
@@ -0,0 +1,375 @@
1
+ import type { EntryKind } from "convex-feedback";
2
+ import { useMemo, useState } from "react";
3
+ import {
4
+ Alert,
5
+ Modal,
6
+ Platform,
7
+ Pressable,
8
+ ScrollView,
9
+ Text,
10
+ View,
11
+ } from "react-native";
12
+
13
+ import { useFeedbackBody } from "../../../shared/context/FeedbackBodyProvider";
14
+ import { useFeedbackUi } from "../../../shared/context/FeedbackProvider";
15
+ import type { FeedbackScreenEntryModalProps } from "../../../shared/types";
16
+ import { EntryDetail } from "./EntryDetail";
17
+ import { FeedbackForm } from "./primitives";
18
+
19
+ export function CreateEntryModal({
20
+ onRequestClose,
21
+ onCreated,
22
+ }: FeedbackScreenEntryModalProps & {
23
+ onRequestClose: () => void;
24
+ }) {
25
+ const { enabledKinds } = useFeedbackBody();
26
+ const { messages, theme } = useFeedbackUi();
27
+ const isIos = Platform.OS === "ios";
28
+
29
+ const [previewEntryId, setPreviewEntryId] = useState<string | null>(null);
30
+
31
+ const [kind, setKind] = useState<EntryKind>(enabledKinds[0] ?? "feedback");
32
+ const [title, setTitle] = useState("");
33
+ const [body, setBody] = useState("");
34
+
35
+ const modalTile =
36
+ enabledKinds.length === 1
37
+ ? messages.newFeedback[enabledKinds.at(0)!]
38
+ : messages.board.createEntry;
39
+
40
+ return (
41
+ <Modal
42
+ visible
43
+ animationType="slide"
44
+ presentationStyle={"pageSheet"}
45
+ allowSwipeDismissal={isIos}
46
+ onRequestClose={onRequestClose}
47
+ role="dialog"
48
+ >
49
+ <View
50
+ style={{
51
+ flex: 1,
52
+ justifyContent: "flex-end",
53
+ backgroundColor: isIos ? theme.colors.background : "transparent",
54
+ }}
55
+ >
56
+ {isIos && (
57
+ <Pressable
58
+ accessibilityRole="button"
59
+ accessibilityLabel={messages.form.cancel}
60
+ onPress={onRequestClose}
61
+ style={{
62
+ position: "absolute",
63
+ top: 0,
64
+ right: 0,
65
+ bottom: 0,
66
+ left: 0,
67
+ }}
68
+ />
69
+ )}
70
+
71
+ <View
72
+ style={{
73
+ flex: 1,
74
+ minHeight: 360,
75
+ overflow: "hidden",
76
+ borderTopLeftRadius: theme.radius * 2,
77
+ borderTopRightRadius: theme.radius * 2,
78
+ backgroundColor: theme.colors.background,
79
+ }}
80
+ >
81
+ <View
82
+ style={{
83
+ flexDirection: "row",
84
+ alignItems: "center",
85
+ justifyContent: "space-between",
86
+ paddingHorizontal: 20,
87
+ paddingVertical: 18,
88
+ borderBottomWidth: 1,
89
+ borderBottomColor: theme.colors.border,
90
+ }}
91
+ >
92
+ <Text
93
+ style={{
94
+ color: theme.colors.text,
95
+ fontSize: 18,
96
+ fontWeight: "700",
97
+ }}
98
+ >
99
+ {modalTile}
100
+ </Text>
101
+
102
+ <Pressable
103
+ accessibilityRole="button"
104
+ onPress={onRequestClose}
105
+ hitSlop={8}
106
+ >
107
+ <Text
108
+ style={{
109
+ color: theme.colors.primary,
110
+ fontWeight: "600",
111
+ }}
112
+ >
113
+ {messages.form.cancel}
114
+ </Text>
115
+ </Pressable>
116
+ </View>
117
+
118
+ <ScrollView
119
+ keyboardShouldPersistTaps="handled"
120
+ keyboardDismissMode={isIos ? "interactive" : "on-drag"}
121
+ contentInsetAdjustmentBehavior="automatic"
122
+ contentContainerStyle={{
123
+ padding: 16,
124
+ paddingBottom: 32,
125
+ }}
126
+ >
127
+ {previewEntryId !== null ? (
128
+ <EntryDetail
129
+ entryId={previewEntryId}
130
+ onBack={() => setPreviewEntryId(null)}
131
+ />
132
+ ) : (
133
+ <CreateForm
134
+ kind={kind}
135
+ onKindChange={setKind}
136
+ title={title}
137
+ onTitleChange={setTitle}
138
+ body={body}
139
+ onBodyChange={setBody}
140
+ onCreated={onCreated}
141
+ onOpenSuggestion={setPreviewEntryId}
142
+ />
143
+ )}
144
+ </ScrollView>
145
+ </View>
146
+ </View>
147
+ </Modal>
148
+ );
149
+ }
150
+
151
+ function CreateForm({
152
+ kind,
153
+ onKindChange,
154
+ title,
155
+ onTitleChange,
156
+ body,
157
+ onBodyChange,
158
+ onCreated,
159
+ onOpenSuggestion,
160
+ }: FeedbackScreenEntryModalProps & {
161
+ kind: EntryKind;
162
+ onKindChange: (kind: EntryKind) => void;
163
+ title: string;
164
+ onTitleChange: (title: string) => void;
165
+ body: string;
166
+ onBodyChange: (body: string) => void;
167
+ onOpenSuggestion: (id: string) => void;
168
+ }) {
169
+ const { hooks, enabledKinds } = useFeedbackBody();
170
+ const { messages, theme } = useFeedbackUi();
171
+ const [submitting, setSubmitting] = useState(false);
172
+ const create = hooks.useCreateEntry();
173
+ const similar = hooks.useSimilarEntries({ title, body, kind, limit: 3 });
174
+
175
+ const suggestions = useMemo(() => {
176
+ if (similar === undefined) return [];
177
+ return [...similar.exact, ...similar.similar];
178
+ }, [similar]);
179
+
180
+ const submit = async () => {
181
+ if (submitting || title.trim().length === 0 || body.trim().length === 0) {
182
+ return;
183
+ }
184
+
185
+ setSubmitting(true);
186
+
187
+ try {
188
+ onCreated(await create({ kind, title, body }));
189
+ } finally {
190
+ setSubmitting(false);
191
+ }
192
+ };
193
+
194
+ const requestSubmit = () => {
195
+ if (
196
+ submitting ||
197
+ similar === undefined ||
198
+ title.trim().length === 0 ||
199
+ body.trim().length === 0
200
+ ) {
201
+ return;
202
+ }
203
+
204
+ if (suggestions.length === 0) {
205
+ void submit();
206
+ return;
207
+ }
208
+
209
+ Alert.alert(
210
+ similar.exact.length > 0
211
+ ? messages.form.exactDuplicate
212
+ : messages.form.possibleDuplicates,
213
+ messages.form.duplicateWarning,
214
+ [
215
+ {
216
+ text: messages.form.cancel,
217
+ style: "cancel",
218
+ },
219
+ {
220
+ text: messages.form.submitAnyway,
221
+ onPress: () => {
222
+ void submit();
223
+ },
224
+ },
225
+ ],
226
+ );
227
+ };
228
+
229
+ return (
230
+ <FeedbackForm.Root>
231
+ {enabledKinds.length > 1 && (
232
+ <>
233
+ <Text style={{ color: theme.colors.text, fontWeight: "600" }}>
234
+ {messages.form.kind}
235
+ </Text>
236
+ <View style={{ flexDirection: "row", flexWrap: "wrap", gap: 7 }}>
237
+ {enabledKinds.map((value) => (
238
+ <Pressable
239
+ key={value}
240
+ onPress={() => onKindChange(value)}
241
+ style={{
242
+ paddingVertical: 6,
243
+ paddingHorizontal: 9,
244
+ borderRadius: 999,
245
+ borderWidth: 1,
246
+ borderColor:
247
+ value === kind ? theme.colors.primary : theme.colors.border,
248
+ }}
249
+ >
250
+ <Text
251
+ style={{
252
+ color:
253
+ value === kind
254
+ ? theme.colors.primary
255
+ : theme.colors.mutedText,
256
+ }}
257
+ >
258
+ {messages.kinds[value]}
259
+ </Text>
260
+ </Pressable>
261
+ ))}
262
+ </View>
263
+ </>
264
+ )}
265
+ <FeedbackForm.Input
266
+ value={title}
267
+ onChangeText={onTitleChange}
268
+ placeholder={messages.form.titlePlaceholder}
269
+ />
270
+ <FeedbackForm.Textarea
271
+ value={body}
272
+ onChangeText={onBodyChange}
273
+ placeholder={messages.form.bodyPlaceholder}
274
+ />
275
+ {suggestions.length > 0 ? (
276
+ <View
277
+ style={{
278
+ gap: 8,
279
+ padding: 10,
280
+ borderWidth: 1,
281
+ borderColor: theme.colors.border,
282
+ borderRadius: theme.radius,
283
+ backgroundColor: theme.colors.surfaceMuted,
284
+ }}
285
+ >
286
+ <Text
287
+ style={{
288
+ color: theme.colors.text,
289
+ fontWeight: "600",
290
+ }}
291
+ >
292
+ {similar?.exact.length
293
+ ? messages.form.exactDuplicate
294
+ : messages.form.possibleDuplicates}
295
+ </Text>
296
+
297
+ {suggestions.map((entry) => (
298
+ <Pressable
299
+ key={entry.id}
300
+ onPress={() => onOpenSuggestion(entry.id)}
301
+ style={{
302
+ gap: 4,
303
+ padding: 10,
304
+ borderWidth: 1,
305
+ borderColor: theme.colors.border,
306
+ borderRadius: Math.max(8, theme.radius - 2),
307
+ backgroundColor: theme.colors.surface,
308
+ }}
309
+ >
310
+ <View
311
+ style={{
312
+ flexDirection: "row",
313
+ alignItems: "center",
314
+ justifyContent: "space-between",
315
+ gap: 8,
316
+ }}
317
+ >
318
+ <Text
319
+ style={{
320
+ flexShrink: 1,
321
+ color: theme.colors.mutedText,
322
+ fontSize: 12,
323
+ fontWeight: "600",
324
+ }}
325
+ >
326
+ {messages.kinds[entry.kind]}
327
+ </Text>
328
+
329
+ <Text
330
+ style={{
331
+ color: theme.colors.mutedText,
332
+ fontSize: 12,
333
+ }}
334
+ >
335
+ {messages.statuses[entry.status]}
336
+ </Text>
337
+ </View>
338
+
339
+ <Text
340
+ numberOfLines={2}
341
+ style={{
342
+ color: theme.colors.text,
343
+ fontWeight: "700",
344
+ }}
345
+ >
346
+ {entry.title}
347
+ </Text>
348
+
349
+ <Text
350
+ numberOfLines={2}
351
+ style={{
352
+ color: theme.colors.mutedText,
353
+ lineHeight: 18,
354
+ }}
355
+ >
356
+ {entry.body}
357
+ </Text>
358
+
359
+ <Text
360
+ style={{
361
+ color: theme.colors.mutedText,
362
+ fontSize: 12,
363
+ }}
364
+ >
365
+ ▲ {entry.upvoteCount} ·{" "}
366
+ {messages.entry.comments(entry.commentCount)}
367
+ </Text>
368
+ </Pressable>
369
+ ))}
370
+ </View>
371
+ ) : null}
372
+ <FeedbackForm.Submit submitting={submitting} onPress={requestSubmit} />
373
+ </FeedbackForm.Root>
374
+ );
375
+ }
@@ -2,7 +2,13 @@ import type {
2
2
  FeedbackComment,
3
3
  FeedbackEntry as FeedbackEntryData,
4
4
  } from "convex-feedback";
5
- import { createContext, useContext, useMemo } from "react";
5
+ import {
6
+ createContext,
7
+ forwardRef,
8
+ useContext,
9
+ useMemo,
10
+ type PropsWithChildren,
11
+ } from "react";
6
12
  import {
7
13
  Pressable,
8
14
  ScrollView,
@@ -18,11 +24,11 @@ import {
18
24
  type ViewProps,
19
25
  type ViewStyle,
20
26
  } from "react-native";
21
- import {
22
- SafeAreaView,
23
- type SafeAreaViewProps,
24
- } from "react-native-safe-area-context";
25
27
 
28
+ import {
29
+ FeedbackNativeThemeScope,
30
+ useFeedbackUi,
31
+ } from "../../../shared/context/FeedbackProvider.js";
26
32
  import type {
27
33
  BoardSearchBaseProps,
28
34
  BoardSearchState,
@@ -37,21 +43,13 @@ import type {
37
43
  FormSubmitBaseProps,
38
44
  FormSubmitState,
39
45
  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
- }
46
+ } from "../../../shared/types";
47
+ import { combineStyle } from "../helpers.js";
50
48
 
51
49
  /**
52
50
  * Props for `FeedbackBoard.Root`.
53
51
  */
54
- export interface BoardRootProps extends SafeAreaViewProps, FeedbackColorProps {}
52
+ export type BoardRootProps = PropsWithChildren<FeedbackColorProps>;
55
53
 
56
54
  function BoardRoot({
57
55
  primaryColor,
@@ -62,10 +60,7 @@ function BoardRoot({
62
60
  borderColor,
63
61
  dangerColor,
64
62
  children,
65
- style,
66
- ...props
67
63
  }: BoardRootProps) {
68
- const { theme, unstyled } = useFeedbackUi();
69
64
  const colorOverrides = useMemo(
70
65
  () => ({
71
66
  ...(primaryColor === undefined ? {} : { primary: primaryColor }),
@@ -86,25 +81,10 @@ function BoardRoot({
86
81
  textColor,
87
82
  ],
88
83
  );
89
- const background = backgroundColor ?? theme.colors.background;
90
84
 
91
85
  return (
92
86
  <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>
87
+ {children}
108
88
  </FeedbackNativeThemeScope>
109
89
  );
110
90
  }
@@ -114,7 +94,11 @@ function BoardHeader({ style, ...props }: ViewProps) {
114
94
  return (
115
95
  <View
116
96
  {...props}
117
- style={combineStyle<ViewStyle>(!unstyled, { gap: theme.spacing }, style)}
97
+ style={combineStyle<ViewStyle>(
98
+ !unstyled,
99
+ { gap: theme.spacing, backgroundColor: theme.colors.background },
100
+ style,
101
+ )}
118
102
  />
119
103
  );
120
104
  }
@@ -143,59 +127,60 @@ export interface BoardSearchProps
143
127
  Omit<TextInputProps, "children" | "value" | "onChangeText">,
144
128
  BoardSearchBaseProps {}
145
129
 
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
- }
130
+ const BoardSearch = forwardRef<TextInput, BoardSearchProps>(
131
+ ({ value, onValueChange, children, style, ...props }, ref) => {
132
+ const { messages, theme, unstyled } = useFeedbackUi();
133
+ const state: BoardSearchState = { value, setValue: onValueChange };
134
+ if (typeof children === "function") return <>{children(state)}</>;
135
+ return (
136
+ <TextInput
137
+ ref={ref}
138
+ {...props}
139
+ value={value}
140
+ onChangeText={onValueChange}
141
+ placeholder={props.placeholder ?? messages.board.searchPlaceholder}
142
+ placeholderTextColor={
143
+ props.placeholderTextColor ?? theme.colors.mutedText
144
+ }
145
+ style={combineStyle<TextStyle>(
146
+ !unstyled,
147
+ {
148
+ color: theme.colors.text,
149
+ borderWidth: 1,
150
+ borderColor: theme.colors.border,
151
+ borderRadius: Math.max(8, theme.radius - 2),
152
+ paddingHorizontal: 12,
153
+ paddingVertical: 10,
154
+ backgroundColor: theme.colors.surface,
155
+ },
156
+ style,
157
+ )}
158
+ />
159
+ );
160
+ },
161
+ );
162
+ // change to forwardRef
181
163
 
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
- }
164
+ const BoardList = forwardRef<ScrollView, ScrollViewProps>(
165
+ ({ style, ...props }, ref) => {
166
+ const { theme, unstyled } = useFeedbackUi();
167
+ return (
168
+ <ScrollView
169
+ ref={ref}
170
+ keyboardShouldPersistTaps="handled"
171
+ contentInsetAdjustmentBehavior="automatic"
172
+ automaticallyAdjustKeyboardInsets
173
+ style={{ flex: 1 }}
174
+ {...props}
175
+ contentContainerStyle={combineStyle<ViewStyle>(
176
+ !unstyled,
177
+ { gap: Math.max(8, theme.spacing - 2) },
178
+ style,
179
+ )}
180
+ />
181
+ );
182
+ },
183
+ );
199
184
 
200
185
  export const FeedbackBoard = {
201
186
  Root: BoardRoot,
@@ -684,13 +669,15 @@ function FormTextarea({ style, ...props }: TextInputProps) {
684
669
 
685
670
  export interface FormSubmitProps
686
671
  extends Omit<PressableProps, "children" | "style">, FormSubmitBaseProps {
687
- primaryColor?: string;
672
+ backgroundColor?: string;
673
+ color?: string;
688
674
  style?: StyleProp<ViewStyle>;
689
675
  }
690
676
  function FormSubmit({
691
677
  submitting = false,
692
678
  children,
693
- primaryColor,
679
+ backgroundColor,
680
+ color,
694
681
  disabled,
695
682
  style,
696
683
  ...props
@@ -710,13 +697,18 @@ function FormSubmit({
710
697
  justifyContent: "center",
711
698
  borderRadius: Math.max(8, theme.radius - 2),
712
699
  paddingHorizontal: 12,
713
- backgroundColor: primaryColor ?? theme.colors.primary,
700
+ backgroundColor: backgroundColor ?? theme.colors.primary,
714
701
  opacity: (disabled ?? submitting) ? 0.55 : 1,
715
702
  },
716
703
  style,
717
704
  )}
718
705
  >
719
- <Text style={{ color: "#ffffff", fontWeight: "700" }}>
706
+ <Text
707
+ style={{
708
+ color: color ?? theme.colors.primaryForeground,
709
+ fontWeight: "700",
710
+ }}
711
+ >
720
712
  {children ?? messages.form.submit}
721
713
  </Text>
722
714
  </Pressable>
@@ -1,50 +1,8 @@
1
1
  "use client";
2
2
 
3
- import { createContext, useContext, useMemo, type CSSProperties } from "react";
3
+ import { type CSSProperties } from "react";
4
4
 
5
- import { mergeFeedbackMessages } from "../shared/messages.js";
6
- import { mergeFeedbackTheme } from "../shared/theme.js";
7
- import type {
8
- FeedbackColorProps,
9
- FeedbackProviderProps,
10
- FeedbackTheme,
11
- FeedbackUiContextValue,
12
- } from "../shared/types";
13
-
14
- const FeedbackUiContext = createContext<FeedbackUiContextValue | null>(null);
15
-
16
- export function FeedbackProvider({
17
- children,
18
- messages,
19
- theme,
20
- unstyled = false,
21
- }: FeedbackProviderProps) {
22
- const value = useMemo<FeedbackUiContextValue>(
23
- () => ({
24
- messages: mergeFeedbackMessages(messages),
25
- theme: mergeFeedbackTheme(theme),
26
- unstyled,
27
- }),
28
- [messages, theme, unstyled],
29
- );
30
- return (
31
- <FeedbackUiContext.Provider value={value}>
32
- {children}
33
- </FeedbackUiContext.Provider>
34
- );
35
- }
36
-
37
- export function useFeedbackUi(): FeedbackUiContextValue {
38
- const context = useContext(FeedbackUiContext);
39
- if (context === null) {
40
- return {
41
- messages: mergeFeedbackMessages(),
42
- theme: mergeFeedbackTheme(),
43
- unstyled: false,
44
- };
45
- }
46
- return context;
47
- }
5
+ import type { FeedbackColorProps, FeedbackTheme } from "../shared/types";
48
6
 
49
7
  /**
50
8
  * CSS custom properties produced for a feedback board.
@@ -1,6 +1,6 @@
1
1
  export * from "../shared/messages.js";
2
2
  export * from "../shared/theme.js";
3
3
  export type * from "../shared/types";
4
- export * from "./context.js";
4
+ export * from "./css.js";
5
5
  export * from "./primitives.js";
6
6
  export * from "./screen.js";