convex-feedback-ui 0.1.4 → 0.1.6

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 (141) hide show
  1. package/README.md +176 -3
  2. package/dist/.tsbuildinfo +1 -1
  3. package/dist/native/expo/ExpoFeedbackBody.d.ts +1 -1
  4. package/dist/native/expo/ExpoFeedbackBody.d.ts.map +1 -1
  5. package/dist/native/expo/ExpoFeedbackBody.js +2 -2
  6. package/dist/native/expo/ExpoFeedbackBody.js.map +1 -1
  7. package/dist/native/expo/FeedbackCreateStackLayout.d.ts +3 -0
  8. package/dist/native/expo/FeedbackCreateStackLayout.d.ts.map +1 -0
  9. package/dist/native/expo/FeedbackCreateStackLayout.js +35 -0
  10. package/dist/native/expo/FeedbackCreateStackLayout.js.map +1 -0
  11. package/dist/native/expo/FeedbackRoutes.d.ts +4 -0
  12. package/dist/native/expo/FeedbackRoutes.d.ts.map +1 -0
  13. package/dist/native/expo/FeedbackRoutes.js +76 -0
  14. package/dist/native/expo/FeedbackRoutes.js.map +1 -0
  15. package/dist/native/expo/FeedbackScreen.d.ts +1 -1
  16. package/dist/native/expo/FeedbackScreen.d.ts.map +1 -1
  17. package/dist/native/expo/FeedbackScreen.js +3 -2
  18. package/dist/native/expo/FeedbackScreen.js.map +1 -1
  19. package/dist/native/expo/FeedbackStack.d.ts +1 -1
  20. package/dist/native/expo/FeedbackStack.d.ts.map +1 -1
  21. package/dist/native/expo/FeedbackStack.js +4 -2
  22. package/dist/native/expo/FeedbackStack.js.map +1 -1
  23. package/dist/native/expo/FeedbackStackLayout.d.ts +3 -0
  24. package/dist/native/expo/FeedbackStackLayout.d.ts.map +1 -0
  25. package/dist/native/expo/FeedbackStackLayout.js +54 -0
  26. package/dist/native/expo/FeedbackStackLayout.js.map +1 -0
  27. package/dist/native/expo/RoutedFeedbackContext.d.ts +4 -0
  28. package/dist/native/expo/RoutedFeedbackContext.d.ts.map +1 -0
  29. package/dist/native/expo/RoutedFeedbackContext.js +11 -0
  30. package/dist/native/expo/RoutedFeedbackContext.js.map +1 -0
  31. package/dist/native/expo/RoutedFeedbackModalContext.d.ts +7 -0
  32. package/dist/native/expo/RoutedFeedbackModalContext.d.ts.map +1 -0
  33. package/dist/native/expo/RoutedFeedbackModalContext.js +7 -0
  34. package/dist/native/expo/RoutedFeedbackModalContext.js.map +1 -0
  35. package/dist/native/expo/index.d.ts +6 -0
  36. package/dist/native/expo/index.d.ts.map +1 -1
  37. package/dist/native/expo/index.js +6 -0
  38. package/dist/native/expo/index.js.map +1 -1
  39. package/dist/native/expo/metadata.d.ts +4 -0
  40. package/dist/native/expo/metadata.d.ts.map +1 -0
  41. package/dist/native/expo/metadata.js +44 -0
  42. package/dist/native/expo/metadata.js.map +1 -0
  43. package/dist/native/expo/routes.d.ts +16 -0
  44. package/dist/native/expo/routes.d.ts.map +1 -0
  45. package/dist/native/expo/routes.js +23 -0
  46. package/dist/native/expo/routes.js.map +1 -0
  47. package/dist/native/expo/types.d.ts +51 -1
  48. package/dist/native/expo/types.d.ts.map +1 -1
  49. package/dist/native/native/FeedbackScreen.d.ts +1 -1
  50. package/dist/native/native/FeedbackScreen.d.ts.map +1 -1
  51. package/dist/native/native/FeedbackScreen.js +3 -2
  52. package/dist/native/native/FeedbackScreen.js.map +1 -1
  53. package/dist/native/native/index.d.ts +2 -0
  54. package/dist/native/native/index.d.ts.map +1 -1
  55. package/dist/native/native/index.js +2 -0
  56. package/dist/native/native/index.js.map +1 -1
  57. package/dist/native/shared/metadata.d.ts +5 -0
  58. package/dist/native/shared/metadata.d.ts.map +1 -0
  59. package/dist/native/shared/metadata.js +31 -0
  60. package/dist/native/shared/metadata.js.map +1 -0
  61. package/dist/native/shared/ui/EntryDetail.d.ts.map +1 -1
  62. package/dist/native/shared/ui/EntryDetail.js +3 -1
  63. package/dist/native/shared/ui/EntryDetail.js.map +1 -1
  64. package/dist/native/shared/ui/FeedbackScreenList.d.ts +1 -1
  65. package/dist/native/shared/ui/FeedbackScreenList.d.ts.map +1 -1
  66. package/dist/native/shared/ui/FeedbackScreenList.js +2 -2
  67. package/dist/native/shared/ui/FeedbackScreenList.js.map +1 -1
  68. package/dist/native/shared/ui/MetadataModal.d.ts +6 -0
  69. package/dist/native/shared/ui/MetadataModal.d.ts.map +1 -0
  70. package/dist/native/shared/ui/MetadataModal.js +39 -0
  71. package/dist/native/shared/ui/MetadataModal.js.map +1 -0
  72. package/dist/native/shared/ui/NewEntry.d.ts +10 -0
  73. package/dist/native/shared/ui/NewEntry.d.ts.map +1 -1
  74. package/dist/native/shared/ui/NewEntry.js +11 -4
  75. package/dist/native/shared/ui/NewEntry.js.map +1 -1
  76. package/dist/react/index.d.ts +2 -0
  77. package/dist/react/index.d.ts.map +1 -1
  78. package/dist/react/index.js +2 -0
  79. package/dist/react/index.js.map +1 -1
  80. package/dist/react/metadata.d.ts +4 -0
  81. package/dist/react/metadata.d.ts.map +1 -0
  82. package/dist/react/metadata.js +40 -0
  83. package/dist/react/metadata.js.map +1 -0
  84. package/dist/react/screen.d.ts +1 -1
  85. package/dist/react/screen.d.ts.map +1 -1
  86. package/dist/react/screen.js +18 -4
  87. package/dist/react/screen.js.map +1 -1
  88. package/dist/react/styles.css +35 -0
  89. package/dist/shared/context/FeedbackBodyProvider.d.ts +1 -1
  90. package/dist/shared/context/FeedbackBodyProvider.d.ts.map +1 -1
  91. package/dist/shared/context/FeedbackBodyProvider.js +5 -1
  92. package/dist/shared/context/FeedbackBodyProvider.js.map +1 -1
  93. package/dist/shared/messages.d.ts.map +1 -1
  94. package/dist/shared/messages.js +9 -0
  95. package/dist/shared/messages.js.map +1 -1
  96. package/dist/shared/metadata.d.ts +6 -0
  97. package/dist/shared/metadata.d.ts.map +1 -0
  98. package/dist/shared/metadata.js +54 -0
  99. package/dist/shared/metadata.js.map +1 -0
  100. package/dist/shared/types/index.d.ts +1 -0
  101. package/dist/shared/types/index.d.ts.map +1 -1
  102. package/dist/shared/types/messages.d.ts +17 -0
  103. package/dist/shared/types/messages.d.ts.map +1 -1
  104. package/dist/shared/types/metadata.d.ts +22 -0
  105. package/dist/shared/types/metadata.d.ts.map +1 -0
  106. package/dist/shared/types/metadata.js +2 -0
  107. package/dist/shared/types/metadata.js.map +1 -0
  108. package/dist/shared/types/screen.d.ts +14 -1
  109. package/dist/shared/types/screen.d.ts.map +1 -1
  110. package/package.json +12 -8
  111. package/src/native/expo/ExpoFeedbackBody.tsx +6 -1
  112. package/src/native/expo/FeedbackCreateStackLayout.tsx +62 -0
  113. package/src/native/expo/FeedbackRoutes.tsx +204 -0
  114. package/src/native/expo/FeedbackScreen.tsx +4 -0
  115. package/src/native/expo/FeedbackStack.tsx +16 -4
  116. package/src/native/expo/FeedbackStackLayout.tsx +116 -0
  117. package/src/native/expo/README.md +24 -0
  118. package/src/native/expo/RoutedFeedbackContext.tsx +20 -0
  119. package/src/native/expo/RoutedFeedbackModalContext.tsx +14 -0
  120. package/src/native/expo/index.ts +6 -0
  121. package/src/native/expo/metadata.ts +62 -0
  122. package/src/native/expo/routes.ts +41 -0
  123. package/src/native/expo/types.ts +63 -1
  124. package/src/native/native/FeedbackScreen.tsx +4 -0
  125. package/src/native/native/index.ts +2 -0
  126. package/src/native/shared/metadata.ts +39 -0
  127. package/src/native/shared/ui/EntryDetail.tsx +14 -0
  128. package/src/native/shared/ui/FeedbackScreenList.tsx +2 -1
  129. package/src/native/shared/ui/MetadataModal.tsx +112 -0
  130. package/src/native/shared/ui/NewEntry.tsx +18 -4
  131. package/src/react/index.ts +2 -0
  132. package/src/react/metadata.ts +51 -0
  133. package/src/react/screen.tsx +89 -2
  134. package/src/react/styles.css +35 -0
  135. package/src/shared/context/FeedbackBodyProvider.tsx +8 -0
  136. package/src/shared/messages.ts +9 -0
  137. package/src/shared/metadata.ts +84 -0
  138. package/src/shared/types/index.ts +1 -0
  139. package/src/shared/types/messages.ts +24 -0
  140. package/src/shared/types/metadata.ts +55 -0
  141. package/src/shared/types/screen.ts +17 -2
@@ -0,0 +1,62 @@
1
+ import {
2
+ Stack,
3
+ useNavigation,
4
+ type NativeStackNavigationOptions,
5
+ } from "expo-router";
6
+ import { useCallback, useMemo } from "react";
7
+
8
+ import { useFeedbackBody } from "../../shared/context/FeedbackBodyProvider.js";
9
+ import { useFeedbackUi } from "../../shared/context/FeedbackProvider.js";
10
+ import { RoutedFeedbackModalProvider } from "./RoutedFeedbackModalContext.js";
11
+ import { useRoutedFeedback } from "./RoutedFeedbackContext.js";
12
+ import type { FeedbackCreateStackLayoutProps } from "./types.js";
13
+
14
+ export function FeedbackCreateStackLayout({
15
+ screenOptions,
16
+ createOptions,
17
+ entryOptions,
18
+ }: FeedbackCreateStackLayoutProps) {
19
+ const parentNavigation = useNavigation();
20
+ const { enabledKinds } = useFeedbackBody();
21
+ const { messages, theme } = useFeedbackUi();
22
+ const { routes } = useRoutedFeedback();
23
+ const dismiss = useCallback(
24
+ () => parentNavigation.goBack(),
25
+ [parentNavigation],
26
+ );
27
+ const modal = useMemo(() => ({ dismiss }), [dismiss]);
28
+ const createTitle =
29
+ enabledKinds.length === 1
30
+ ? messages.newFeedback[enabledKinds.at(0)!]
31
+ : messages.board.createEntry;
32
+ const defaults: NativeStackNavigationOptions = {
33
+ headerShown: true,
34
+ headerBackButtonDisplayMode: "minimal",
35
+ contentStyle: { backgroundColor: theme.colors.background },
36
+ };
37
+
38
+ return (
39
+ <RoutedFeedbackModalProvider value={modal}>
40
+ <Stack>
41
+ <Stack.Screen
42
+ name="index"
43
+ options={{
44
+ ...defaults,
45
+ headerTitle: createTitle,
46
+ ...screenOptions,
47
+ ...createOptions,
48
+ }}
49
+ />
50
+ <Stack.Screen
51
+ name={routes.entry}
52
+ options={{
53
+ ...defaults,
54
+ headerTitle: messages.board.title,
55
+ ...screenOptions,
56
+ ...entryOptions,
57
+ }}
58
+ />
59
+ </Stack>
60
+ </RoutedFeedbackModalProvider>
61
+ );
62
+ }
@@ -0,0 +1,204 @@
1
+ import { Stack, useLocalSearchParams, useRouter } from "expo-router";
2
+ import { useEffect, useRef, useState } from "react";
3
+ import type { SearchBarCommands } from "react-native-screens";
4
+ import type { EntryKind } from "convex-feedback";
5
+
6
+ import { useFeedbackBody } from "../../shared/context/FeedbackBodyProvider.js";
7
+ import { useFeedbackUi } from "../../shared/context/FeedbackProvider.js";
8
+ import { EntryDetail } from "../shared/ui/EntryDetail.js";
9
+ import { FeedbackScreenList } from "../shared/ui/FeedbackScreenList.js";
10
+ import { CreateEntryForm } from "../shared/ui/NewEntry.js";
11
+ import { FeedbackBoard } from "../shared/ui/primitives.js";
12
+ import { feedbackRouteHref } from "./routes.js";
13
+ import { useRoutedFeedbackModal } from "./RoutedFeedbackModalContext.js";
14
+ import { useRoutedFeedback } from "./RoutedFeedbackContext.js";
15
+
16
+ export function FeedbackBoardScreen() {
17
+ const { query, isSearching, setQuery, setIsSearching } = useFeedbackBody();
18
+ const { messages, theme } = useFeedbackUi();
19
+ const { routes, colors, androidToolbarIcons } = useRoutedFeedback();
20
+ const router = useRouter();
21
+ const searchRef = useRef<SearchBarCommands>(null);
22
+
23
+ useEffect(() => {
24
+ const searching = query.trim().length > 0;
25
+ if (searching !== isSearching) setIsSearching(searching);
26
+ }, [query, isSearching, setIsSearching]);
27
+
28
+ const openEntry = (entryId: string) => {
29
+ if ((query || isSearching) && searchRef.current) {
30
+ searchRef.current.blur();
31
+ }
32
+ router.push(feedbackRouteHref(routes.entry, { entryId }), {
33
+ relativeToDirectory: true,
34
+ });
35
+ };
36
+
37
+ return (
38
+ <>
39
+ <FeedbackBoard.Root {...colors}>
40
+ <FeedbackScreenList
41
+ showSelectedEntry={false}
42
+ hideBackButton
43
+ onEntryOpen={openEntry}
44
+ />
45
+ </FeedbackBoard.Root>
46
+
47
+ <Stack.Toolbar placement="right">
48
+ <Stack.Toolbar.Button
49
+ icon={
50
+ process.env.EXPO_OS === "ios" ? "plus" : androidToolbarIcons.create
51
+ }
52
+ variant="prominent"
53
+ accessibilityLabel={messages.board.createEntry}
54
+ onPress={() =>
55
+ router.push(feedbackRouteHref(routes.create), {
56
+ relativeToDirectory: true,
57
+ })
58
+ }
59
+ tintColor={theme.colors.primary}
60
+ >
61
+ {messages.board.createEntry}
62
+ </Stack.Toolbar.Button>
63
+ </Stack.Toolbar>
64
+ <Stack.Toolbar placement="left">
65
+ <Stack.Toolbar.Button
66
+ hidden={!router.canGoBack()}
67
+ icon={
68
+ process.env.EXPO_OS === "ios"
69
+ ? "chevron.backward"
70
+ : androidToolbarIcons.back
71
+ }
72
+ accessibilityLabel={messages.entry.back}
73
+ onPress={() => router.back()}
74
+ >
75
+ {messages.entry.back}
76
+ </Stack.Toolbar.Button>
77
+ </Stack.Toolbar>
78
+ <Stack.SearchBar
79
+ ref={searchRef}
80
+ placeholder={messages.board.searchPlaceholder}
81
+ onChangeText={(event) => setQuery(event.nativeEvent.text)}
82
+ onFocus={() => setIsSearching(true)}
83
+ onBlur={() => setIsSearching(query.trim().length > 0)}
84
+ obscureBackground={false}
85
+ allowToolbarIntegration
86
+ hideNavigationBar={false}
87
+ textColor={theme.colors.text}
88
+ />
89
+ <Stack.Toolbar placement="bottom">
90
+ <Stack.Toolbar.SearchBarSlot />
91
+ </Stack.Toolbar>
92
+ </>
93
+ );
94
+ }
95
+
96
+ export function FeedbackEntryScreen() {
97
+ const params = useLocalSearchParams<{
98
+ entryId?: string | string[];
99
+ }>();
100
+ const entryId = Array.isArray(params.entryId)
101
+ ? params.entryId[0]
102
+ : params.entryId;
103
+
104
+ if (!entryId) {
105
+ throw new Error(
106
+ 'FeedbackEntryScreen requires an "entryId" dynamic route parameter.',
107
+ );
108
+ }
109
+
110
+ return <FeedbackEntryRouteContent entryId={entryId} />;
111
+ }
112
+
113
+ function FeedbackEntryRouteContent({ entryId }: { entryId: string }) {
114
+ const { hooks } = useFeedbackBody();
115
+ const { messages, theme } = useFeedbackUi();
116
+ const { colors, androidToolbarIcons } = useRoutedFeedback();
117
+ const modal = useRoutedFeedbackModal();
118
+ const router = useRouter();
119
+ const entry = hooks.useEntry(entryId);
120
+
121
+ return (
122
+ <>
123
+ {entry && <Stack.Screen options={{ headerTitle: entry.title }} />}
124
+ <FeedbackBoard.Root {...colors}>
125
+ <FeedbackBoard.List style={{ padding: theme.spacing }}>
126
+ <EntryDetail
127
+ entryId={entryId}
128
+ hideBackButton
129
+ onBack={() => router.back()}
130
+ />
131
+ </FeedbackBoard.List>
132
+ </FeedbackBoard.Root>
133
+ {modal && (
134
+ <Stack.Toolbar placement="right">
135
+ <Stack.Toolbar.Button
136
+ icon={
137
+ process.env.EXPO_OS === "ios"
138
+ ? "xmark"
139
+ : androidToolbarIcons.close
140
+ }
141
+ accessibilityLabel={messages.form.cancel}
142
+ onPress={modal.dismiss}
143
+ >
144
+ {messages.form.cancel}
145
+ </Stack.Toolbar.Button>
146
+ </Stack.Toolbar>
147
+ )}
148
+ </>
149
+ );
150
+ }
151
+
152
+ export function CreateFeedbackScreen() {
153
+ const { enabledKinds } = useFeedbackBody();
154
+ const { messages, theme } = useFeedbackUi();
155
+ const { routes, colors, androidToolbarIcons } = useRoutedFeedback();
156
+ const modal = useRoutedFeedbackModal();
157
+ const router = useRouter();
158
+ const [kind, setKind] = useState<EntryKind>(enabledKinds[0] ?? "feedback");
159
+ const [title, setTitle] = useState("");
160
+ const [body, setBody] = useState("");
161
+
162
+ const entryHref = (entryId: string) =>
163
+ feedbackRouteHref(routes.entry, { entryId });
164
+
165
+ return (
166
+ <>
167
+ <FeedbackBoard.Root {...colors}>
168
+ <FeedbackBoard.List
169
+ style={{
170
+ padding: theme.spacing,
171
+ paddingBottom: theme.spacing * 2,
172
+ }}
173
+ >
174
+ <CreateEntryForm
175
+ kind={kind}
176
+ onKindChange={setKind}
177
+ title={title}
178
+ onTitleChange={setTitle}
179
+ body={body}
180
+ onBodyChange={setBody}
181
+ onOpenSuggestion={(entryId) =>
182
+ router.push(entryHref(entryId), { relativeToDirectory: true })
183
+ }
184
+ onCreated={(entryId) => {
185
+ router.dismissTo(entryHref(entryId));
186
+ }}
187
+ />
188
+ </FeedbackBoard.List>
189
+ </FeedbackBoard.Root>
190
+
191
+ <Stack.Toolbar placement="left">
192
+ <Stack.Toolbar.Button
193
+ icon={
194
+ process.env.EXPO_OS === "ios" ? "xmark" : androidToolbarIcons.close
195
+ }
196
+ accessibilityLabel={messages.form.cancel}
197
+ onPress={() => (modal ? modal.dismiss() : router.dismiss())}
198
+ >
199
+ {messages.form.cancel}
200
+ </Stack.Toolbar.Button>
201
+ </Stack.Toolbar>
202
+ </>
203
+ );
204
+ }
@@ -3,6 +3,7 @@ import { FeedbackProvider } from "../../shared/context/FeedbackProvider";
3
3
  import { kinds } from "../../shared/helpers";
4
4
  import { ExpoFeedbackBody } from "./ExpoFeedbackBody";
5
5
  import type { ExpoFeedbackScreenProps } from "./types";
6
+ import { collectExpoMetadata } from "./metadata";
6
7
 
7
8
  export function FeedbackScreen({
8
9
  hooks,
@@ -14,6 +15,7 @@ export function FeedbackScreen({
14
15
  enabledKinds = kinds,
15
16
  maxCommentDepth = 5,
16
17
  debounceDuration = 300,
18
+ collectMetadata,
17
19
  transformComments,
18
20
  renderActor,
19
21
  ...props
@@ -27,6 +29,8 @@ export function FeedbackScreen({
27
29
  enabledKinds={enabledKinds}
28
30
  maxCommentDepth={maxCommentDepth}
29
31
  debounceDuration={debounceDuration}
32
+ collectMetadata={collectMetadata}
33
+ collectStandardMetadata={collectExpoMetadata}
30
34
  transformComments={transformComments}
31
35
  renderActor={renderActor}
32
36
  >
@@ -6,6 +6,7 @@ import type { FeedbackStackProps } from "./types";
6
6
  export function FeedbackStack({
7
7
  searchRef,
8
8
  stackOptions,
9
+ androidToolbarIcons = {},
9
10
  children,
10
11
  }: FeedbackStackProps) {
11
12
  const {
@@ -54,19 +55,30 @@ export function FeedbackStack({
54
55
  />
55
56
  <Stack.Toolbar placement="right">
56
57
  <Stack.Toolbar.Button
57
- icon="plus"
58
+ icon={
59
+ process.env.EXPO_OS === "ios" ? "plus" : androidToolbarIcons.create
60
+ }
58
61
  variant="prominent"
59
62
  accessibilityLabel={messages.board.createEntry}
60
63
  onPress={() => setShowForm(true)}
61
64
  tintColor={theme.colors.primary}
62
- />
65
+ >
66
+ {messages.board.createEntry}
67
+ </Stack.Toolbar.Button>
63
68
  </Stack.Toolbar>
64
69
  <Stack.Toolbar placement="left">
65
70
  <Stack.Toolbar.Button
66
71
  hidden={!canGoBack}
67
- icon="chevron.backward"
72
+ icon={
73
+ process.env.EXPO_OS === "ios"
74
+ ? "chevron.backward"
75
+ : androidToolbarIcons.back
76
+ }
77
+ accessibilityLabel={messages.entry.back}
68
78
  onPress={handleBackPress}
69
- />
79
+ >
80
+ {messages.entry.back}
81
+ </Stack.Toolbar.Button>
70
82
  </Stack.Toolbar>
71
83
 
72
84
  {children}
@@ -0,0 +1,116 @@
1
+ import { Stack, type NativeStackNavigationOptions } from "expo-router";
2
+
3
+ import { FeedbackBodyProvider } from "../../shared/context/FeedbackBodyProvider.js";
4
+ import { FeedbackProvider } from "../../shared/context/FeedbackProvider.js";
5
+ import { kinds } from "../../shared/helpers.js";
6
+ import { mergeFeedbackMessages } from "../../shared/messages.js";
7
+ import { mergeFeedbackTheme } from "../../shared/theme.js";
8
+ import { collectExpoMetadata } from "./metadata.js";
9
+ import { RoutedFeedbackProvider } from "./RoutedFeedbackContext.js";
10
+ import { resolveFeedbackRoutes } from "./routes.js";
11
+ import type { FeedbackStackLayoutProps } from "./types.js";
12
+
13
+ export function FeedbackStackLayout({
14
+ hooks,
15
+ messages,
16
+ theme,
17
+ unstyled,
18
+ entrySort = "top",
19
+ commentSort = "top",
20
+ enabledKinds = kinds,
21
+ maxCommentDepth = 5,
22
+ debounceDuration = 300,
23
+ collectMetadata,
24
+ transformComments,
25
+ renderActor,
26
+ routes: routeOverrides,
27
+ screenOptions,
28
+ boardOptions,
29
+ entryOptions,
30
+ createOptions,
31
+ androidToolbarIcons = {},
32
+ primaryColor,
33
+ primaryForeground,
34
+ backgroundColor,
35
+ surfaceColor,
36
+ textColor,
37
+ mutedColor,
38
+ borderColor,
39
+ dangerColor,
40
+ }: FeedbackStackLayoutProps) {
41
+ const routes = resolveFeedbackRoutes(routeOverrides);
42
+ const resolvedMessages = mergeFeedbackMessages(messages);
43
+ const resolvedTheme = mergeFeedbackTheme(theme);
44
+ const colors = {
45
+ primaryColor,
46
+ primaryForeground,
47
+ backgroundColor,
48
+ surfaceColor,
49
+ textColor,
50
+ mutedColor,
51
+ borderColor,
52
+ dangerColor,
53
+ };
54
+ const background = backgroundColor ?? resolvedTheme.colors.background;
55
+ const boardTitle =
56
+ enabledKinds.length === 1
57
+ ? resolvedMessages.kindsPlural[enabledKinds.at(0)!]
58
+ : resolvedMessages.board.title;
59
+ const defaults: NativeStackNavigationOptions = {
60
+ headerShown: true,
61
+ headerBackButtonDisplayMode: "minimal",
62
+ contentStyle: { backgroundColor: background },
63
+ };
64
+
65
+ return (
66
+ <FeedbackProvider messages={messages} theme={theme} unstyled={unstyled}>
67
+ <FeedbackBodyProvider
68
+ hooks={hooks}
69
+ entrySort={entrySort}
70
+ commentSort={commentSort}
71
+ enabledKinds={enabledKinds}
72
+ maxCommentDepth={maxCommentDepth}
73
+ debounceDuration={debounceDuration}
74
+ collectMetadata={collectMetadata}
75
+ collectStandardMetadata={collectExpoMetadata}
76
+ transformComments={transformComments}
77
+ renderActor={renderActor}
78
+ >
79
+ <RoutedFeedbackProvider value={{ routes, colors, androidToolbarIcons }}>
80
+ <Stack>
81
+ <Stack.Screen
82
+ name={routes.board}
83
+ options={{
84
+ ...defaults,
85
+ headerTitle: boardTitle,
86
+ headerTransparent: true,
87
+ headerShadowVisible: true,
88
+ ...screenOptions,
89
+ ...boardOptions,
90
+ }}
91
+ />
92
+ <Stack.Screen
93
+ name={routes.entry}
94
+ options={{
95
+ ...defaults,
96
+ headerTitle: boardTitle,
97
+ ...screenOptions,
98
+ ...entryOptions,
99
+ }}
100
+ />
101
+ <Stack.Screen
102
+ name={routes.create}
103
+ options={{
104
+ ...defaults,
105
+ headerShown: false,
106
+ presentation: "modal",
107
+ ...screenOptions,
108
+ ...createOptions,
109
+ }}
110
+ />
111
+ </Stack>
112
+ </RoutedFeedbackProvider>
113
+ </FeedbackBodyProvider>
114
+ </FeedbackProvider>
115
+ );
116
+ }
@@ -15,3 +15,27 @@ To replace a control with Expo UI, use render-function children on an interactiv
15
15
  ```
16
16
 
17
17
  Containers such as `FeedbackEntry.Content`, `FeedbackForm.Root`, and `Comment.Children` accept arbitrary children, so custom Expo UI fields/buttons can also be placed directly inside the compound layout.
18
+
19
+ ## Routed screens
20
+
21
+ Use `FeedbackStackLayout` with `FeedbackBoardScreen`,
22
+ `FeedbackCreateStackLayout`, `FeedbackEntryScreen`, and
23
+ `CreateFeedbackScreen` when board state should be shared across real Expo
24
+ Router routes. Consumer applications must create the corresponding board and
25
+ entry files plus a nested create directory containing `_layout.tsx`,
26
+ `index.tsx`, and `[entryId].tsx`, because Expo Router discovers pages from the
27
+ application's route directory.
28
+
29
+ Re-export `feedbackStackSettings` from the route layout as
30
+ `unstable_settings` so a deep link to the create modal anchors the board behind
31
+ it. Re-export `feedbackCreateStackSettings` from the nested create layout so
32
+ suggested entries retain the form behind them. Use
33
+ `createFeedbackStackSettings(routes)` when overriding the default route names.
34
+
35
+ Toolbar actions retain their package-provided SF Symbols on iOS. Pass
36
+ `androidToolbarIcons` to `FeedbackScreen` or `FeedbackStackLayout` with Android
37
+ `ImageSourcePropType` values for the create, back, and close actions. Missing
38
+ Android images fall back to text labels.
39
+
40
+ The existing `FeedbackScreen` remains the self-contained API for conditional
41
+ in-screen navigation.
@@ -0,0 +1,20 @@
1
+ import { createContext, useContext } from "react";
2
+ import type { RoutedFeedbackContextValue } from "./types.js";
3
+
4
+ const RoutedFeedbackContext = createContext<RoutedFeedbackContextValue | null>(
5
+ null,
6
+ );
7
+
8
+ export const RoutedFeedbackProvider = RoutedFeedbackContext.Provider;
9
+
10
+ export function useRoutedFeedback(): RoutedFeedbackContextValue {
11
+ const value = useContext(RoutedFeedbackContext);
12
+
13
+ if (value === null) {
14
+ throw new Error(
15
+ "Routed feedback screens must be rendered inside FeedbackStackLayout.",
16
+ );
17
+ }
18
+
19
+ return value;
20
+ }
@@ -0,0 +1,14 @@
1
+ import { createContext, useContext } from "react";
2
+
3
+ interface RoutedFeedbackModalContextValue {
4
+ dismiss: () => void;
5
+ }
6
+
7
+ const RoutedFeedbackModalContext =
8
+ createContext<RoutedFeedbackModalContextValue | null>(null);
9
+
10
+ export const RoutedFeedbackModalProvider = RoutedFeedbackModalContext.Provider;
11
+
12
+ export function useRoutedFeedbackModal() {
13
+ return useContext(RoutedFeedbackModalContext);
14
+ }
@@ -1,8 +1,14 @@
1
1
  export * from "../../shared/context/FeedbackBodyProvider.js";
2
2
  export * from "../../shared/context/FeedbackProvider.js";
3
3
  export * from "../../shared/messages.js";
4
+ export * from "../../shared/metadata.js";
4
5
  export * from "../../shared/theme.js";
5
6
  export type * from "../../shared/types/index.js";
6
7
  export * from "../shared/ui/primitives.js";
8
+ export * from "./metadata.js";
7
9
  export * from "./FeedbackScreen.js";
10
+ export * from "./FeedbackCreateStackLayout.js";
11
+ export * from "./FeedbackStackLayout.js";
12
+ export * from "./FeedbackRoutes.js";
13
+ export * from "./routes.js";
8
14
  export type * from "./types.js";
@@ -0,0 +1,62 @@
1
+ import type { FeedbackMetadataValue } from "convex-feedback";
2
+ import Constants from "expo-constants";
3
+ import { Platform } from "react-native";
4
+
5
+ import type {
6
+ StandardMetadataKey,
7
+ EnabledStandardMetadataSelection,
8
+ } from "../../shared/types/metadata.js";
9
+ import {
10
+ collectNativeMetadata,
11
+ defaultNativeMetadataKeys,
12
+ } from "../shared/metadata.js";
13
+
14
+ const defaultExpoMetadataKeys: readonly StandardMetadataKey[] = [
15
+ ...defaultNativeMetadataKeys,
16
+ "appVersion",
17
+ "buildNumber",
18
+ "applicationId",
19
+ "expoRuntimeVersion",
20
+ "executionEnvironment",
21
+ ];
22
+
23
+ export function collectExpoMetadata(
24
+ selection: EnabledStandardMetadataSelection,
25
+ ): Partial<Record<StandardMetadataKey, FeedbackMetadataValue>> {
26
+ const selected = selection === true ? defaultExpoMetadataKeys : selection;
27
+ const keys = new Set(selected);
28
+ const metadata = collectNativeMetadata(selected);
29
+ const config = Constants.expoConfig;
30
+
31
+ if (keys.has("appVersion") && config?.version)
32
+ metadata.appVersion = config.version;
33
+
34
+ if (keys.has("buildNumber")) {
35
+ const buildNumber =
36
+ Platform.OS === "ios"
37
+ ? (Constants.platform?.ios?.buildNumber ?? config?.ios?.buildNumber)
38
+ : Platform.OS === "android"
39
+ ? (Constants.platform?.android?.versionCode ??
40
+ config?.android?.versionCode)
41
+ : undefined;
42
+ if (buildNumber !== undefined && buildNumber !== null)
43
+ metadata.buildNumber = buildNumber;
44
+ }
45
+
46
+ if (keys.has("applicationId")) {
47
+ const applicationId =
48
+ Platform.OS === "ios"
49
+ ? config?.ios?.bundleIdentifier
50
+ : Platform.OS === "android"
51
+ ? config?.android?.package
52
+ : undefined;
53
+ if (applicationId) metadata.applicationId = applicationId;
54
+ }
55
+
56
+ if (keys.has("expoRuntimeVersion") && Constants.expoRuntimeVersion)
57
+ metadata.expoRuntimeVersion = Constants.expoRuntimeVersion;
58
+ if (keys.has("executionEnvironment"))
59
+ metadata.executionEnvironment = Constants.executionEnvironment;
60
+
61
+ return metadata;
62
+ }
@@ -0,0 +1,41 @@
1
+ import type { Href } from "expo-router";
2
+ import type { FeedbackRouteNames } from "./types.js";
3
+
4
+ export const defaultFeedbackRoutes: FeedbackRouteNames = {
5
+ board: "index",
6
+ entry: "[entryId]",
7
+ create: "new",
8
+ };
9
+
10
+ export function resolveFeedbackRoutes(
11
+ routes: Partial<FeedbackRouteNames> = {},
12
+ ): FeedbackRouteNames {
13
+ const resolved = { ...defaultFeedbackRoutes, ...routes };
14
+
15
+ if (!resolved.entry.includes("[entryId]")) {
16
+ throw new Error(
17
+ 'The routed feedback entry route must contain the "[entryId]" dynamic segment.',
18
+ );
19
+ }
20
+
21
+ return resolved;
22
+ }
23
+
24
+ export function createFeedbackStackSettings(
25
+ routes?: Partial<FeedbackRouteNames>,
26
+ ) {
27
+ return { anchor: resolveFeedbackRoutes(routes).board };
28
+ }
29
+
30
+ export const feedbackStackSettings = createFeedbackStackSettings();
31
+
32
+ /** Anchor the create form beneath suggested-entry detail screens. */
33
+ export const feedbackCreateStackSettings = { anchor: "index" };
34
+
35
+ export function feedbackRouteHref(
36
+ route: string,
37
+ params?: Record<string, string>,
38
+ ): Href {
39
+ const pathname = route.startsWith(".") ? route : `./${route}`;
40
+ return params === undefined ? pathname : { pathname, params };
41
+ }