convex-feedback-ui 0.1.5 → 0.1.7

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 (131) hide show
  1. package/README.md +143 -0
  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 +81 -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 +2 -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 +10 -5
  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 +4 -0
  36. package/dist/native/expo/index.d.ts.map +1 -1
  37. package/dist/native/expo/index.js +4 -0
  38. package/dist/native/expo/index.js.map +1 -1
  39. package/dist/native/expo/routes.d.ts +16 -0
  40. package/dist/native/expo/routes.d.ts.map +1 -0
  41. package/dist/native/expo/routes.js +23 -0
  42. package/dist/native/expo/routes.js.map +1 -0
  43. package/dist/native/expo/types.d.ts +53 -1
  44. package/dist/native/expo/types.d.ts.map +1 -1
  45. package/dist/native/native/FeedbackScreen.d.ts +1 -1
  46. package/dist/native/native/FeedbackScreen.d.ts.map +1 -1
  47. package/dist/native/native/FeedbackScreen.js +2 -2
  48. package/dist/native/native/FeedbackScreen.js.map +1 -1
  49. package/dist/native/shared/ui/BodyContent.d.ts +1 -1
  50. package/dist/native/shared/ui/BodyContent.d.ts.map +1 -1
  51. package/dist/native/shared/ui/BodyContent.js +2 -2
  52. package/dist/native/shared/ui/BodyContent.js.map +1 -1
  53. package/dist/native/shared/ui/Button.d.ts +2 -1
  54. package/dist/native/shared/ui/Button.d.ts.map +1 -1
  55. package/dist/native/shared/ui/Button.js +9 -4
  56. package/dist/native/shared/ui/Button.js.map +1 -1
  57. package/dist/native/shared/ui/FeedbackScreenHeader.d.ts.map +1 -1
  58. package/dist/native/shared/ui/FeedbackScreenHeader.js +8 -4
  59. package/dist/native/shared/ui/FeedbackScreenHeader.js.map +1 -1
  60. package/dist/native/shared/ui/FeedbackScreenList.d.ts +1 -1
  61. package/dist/native/shared/ui/FeedbackScreenList.d.ts.map +1 -1
  62. package/dist/native/shared/ui/FeedbackScreenList.js +18 -5
  63. package/dist/native/shared/ui/FeedbackScreenList.js.map +1 -1
  64. package/dist/native/shared/ui/NewEntry.d.ts +10 -0
  65. package/dist/native/shared/ui/NewEntry.d.ts.map +1 -1
  66. package/dist/native/shared/ui/NewEntry.js +7 -14
  67. package/dist/native/shared/ui/NewEntry.js.map +1 -1
  68. package/dist/native/shared/ui/primitives.d.ts +7 -1
  69. package/dist/native/shared/ui/primitives.d.ts.map +1 -1
  70. package/dist/native/shared/ui/primitives.js +38 -0
  71. package/dist/native/shared/ui/primitives.js.map +1 -1
  72. package/dist/react/primitives.d.ts +9 -1
  73. package/dist/react/primitives.d.ts.map +1 -1
  74. package/dist/react/primitives.js +12 -0
  75. package/dist/react/primitives.js.map +1 -1
  76. package/dist/react/screen.d.ts +1 -1
  77. package/dist/react/screen.d.ts.map +1 -1
  78. package/dist/react/screen.js +21 -10
  79. package/dist/react/screen.js.map +1 -1
  80. package/dist/react/styles.css +38 -1
  81. package/dist/shared/context/FeedbackBodyProvider.d.ts +1 -1
  82. package/dist/shared/context/FeedbackBodyProvider.d.ts.map +1 -1
  83. package/dist/shared/context/FeedbackBodyProvider.js +5 -1
  84. package/dist/shared/context/FeedbackBodyProvider.js.map +1 -1
  85. package/dist/shared/helpers.d.ts +5 -1
  86. package/dist/shared/helpers.d.ts.map +1 -1
  87. package/dist/shared/helpers.js +11 -0
  88. package/dist/shared/helpers.js.map +1 -1
  89. package/dist/shared/messages.d.ts.map +1 -1
  90. package/dist/shared/messages.js +1 -0
  91. package/dist/shared/messages.js.map +1 -1
  92. package/dist/shared/types/choice.d.ts +10 -0
  93. package/dist/shared/types/choice.d.ts.map +1 -0
  94. package/dist/shared/types/choice.js +2 -0
  95. package/dist/shared/types/choice.js.map +1 -0
  96. package/dist/shared/types/index.d.ts +1 -0
  97. package/dist/shared/types/index.d.ts.map +1 -1
  98. package/dist/shared/types/messages.d.ts +2 -0
  99. package/dist/shared/types/messages.d.ts.map +1 -1
  100. package/dist/shared/types/screen.d.ts +13 -2
  101. package/dist/shared/types/screen.d.ts.map +1 -1
  102. package/package.json +12 -8
  103. package/src/native/expo/ExpoFeedbackBody.tsx +6 -1
  104. package/src/native/expo/FeedbackCreateStackLayout.tsx +62 -0
  105. package/src/native/expo/FeedbackRoutes.tsx +238 -0
  106. package/src/native/expo/FeedbackScreen.tsx +2 -0
  107. package/src/native/expo/FeedbackStack.tsx +40 -5
  108. package/src/native/expo/FeedbackStackLayout.tsx +118 -0
  109. package/src/native/expo/README.md +24 -0
  110. package/src/native/expo/RoutedFeedbackContext.tsx +20 -0
  111. package/src/native/expo/RoutedFeedbackModalContext.tsx +14 -0
  112. package/src/native/expo/index.ts +4 -0
  113. package/src/native/expo/routes.ts +41 -0
  114. package/src/native/expo/types.ts +65 -1
  115. package/src/native/native/FeedbackScreen.tsx +2 -0
  116. package/src/native/shared/ui/BodyContent.tsx +2 -0
  117. package/src/native/shared/ui/Button.tsx +12 -3
  118. package/src/native/shared/ui/FeedbackScreenHeader.tsx +25 -4
  119. package/src/native/shared/ui/FeedbackScreenList.tsx +48 -8
  120. package/src/native/shared/ui/NewEntry.tsx +12 -30
  121. package/src/native/shared/ui/primitives.tsx +90 -0
  122. package/src/react/primitives.tsx +55 -0
  123. package/src/react/screen.tsx +67 -33
  124. package/src/react/styles.css +38 -1
  125. package/src/shared/context/FeedbackBodyProvider.tsx +6 -0
  126. package/src/shared/helpers.ts +21 -1
  127. package/src/shared/messages.ts +1 -0
  128. package/src/shared/types/choice.ts +10 -0
  129. package/src/shared/types/index.ts +1 -0
  130. package/src/shared/types/messages.ts +3 -0
  131. package/src/shared/types/screen.ts +15 -1
package/package.json CHANGED
@@ -1,8 +1,12 @@
1
1
  {
2
2
  "name": "convex-feedback-ui",
3
- "version": "0.1.5",
3
+ "version": "0.1.7",
4
4
  "description": "Customizable React and React Native UI primitives for convex-feedback.",
5
5
  "license": "Apache-2.0",
6
+ "repository": {
7
+ "type": "git",
8
+ "url": "https://github.com/Moumen-io/convex-feedback.git"
9
+ },
6
10
  "type": "module",
7
11
  "sideEffects": [
8
12
  "./dist/react/styles.css"
@@ -36,7 +40,7 @@
36
40
  "prepack": "npm run build"
37
41
  },
38
42
  "peerDependencies": {
39
- "convex-feedback": "^0.1.4",
43
+ "convex-feedback": "^0.1.5",
40
44
  "react": ">=18.2.0",
41
45
  "react-native": ">=0.83.0",
42
46
  "react-native-safe-area-context": ">=5.4.0",
@@ -68,12 +72,12 @@
68
72
  "devDependencies": {
69
73
  "@types/react": "^19.2.18",
70
74
  "convex-feedback": "*",
71
- "react": "^19.2.0",
72
- "react-native": "^0.83.10",
73
- "react-native-safe-area-context": "~5.6.2",
74
- "react-native-screens": "^4.23.0",
75
- "expo": "^55.0.0",
76
- "expo-router": "^55.0.18"
75
+ "react": "19.2.3",
76
+ "react-native": "0.86.3",
77
+ "react-native-safe-area-context": "~5.7.0",
78
+ "react-native-screens": "~4.26.0",
79
+ "expo": "~57.0.18",
80
+ "expo-router": "~57.0.17"
77
81
  },
78
82
  "types": "./dist/react/index.d.ts",
79
83
  "module": "./dist/react/index.js"
@@ -11,6 +11,7 @@ import type { FeedbackScreenStackProps } from "./types.js";
11
11
  export function ExpoFeedbackBody({
12
12
  useStack = true,
13
13
  StackOptions,
14
+ androidToolbarIcons,
14
15
  ...props
15
16
  }: FeedbackScreenBodyProps & FeedbackScreenStackProps) {
16
17
  const { query, isSearching, setIsSearching, setSelectedEntryId } =
@@ -36,7 +37,11 @@ export function ExpoFeedbackBody({
36
37
  return <FeedbackScreenContent onEntryOpen={onEntryOpen} {...props} />;
37
38
 
38
39
  return (
39
- <FeedbackStack stackOptions={StackOptions} searchRef={searchRef}>
40
+ <FeedbackStack
41
+ stackOptions={StackOptions}
42
+ searchRef={searchRef}
43
+ androidToolbarIcons={androidToolbarIcons}
44
+ >
40
45
  <FeedbackScreenContent
41
46
  hideBackButton
42
47
  showHeader={false}
@@ -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,238 @@
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 { createEntryLabel, entryStatusChoices } from "../../shared/helpers.js";
9
+ import { EntryDetail } from "../shared/ui/EntryDetail.js";
10
+ import { FeedbackScreenList } from "../shared/ui/FeedbackScreenList.js";
11
+ import { CreateEntryForm } from "../shared/ui/NewEntry.js";
12
+ import { FeedbackBoard } from "../shared/ui/primitives.js";
13
+ import { feedbackRouteHref } from "./routes.js";
14
+ import { useRoutedFeedbackModal } from "./RoutedFeedbackModalContext.js";
15
+ import { useRoutedFeedback } from "./RoutedFeedbackContext.js";
16
+
17
+ export function FeedbackBoardScreen() {
18
+ const {
19
+ query,
20
+ isSearching,
21
+ setQuery,
22
+ setIsSearching,
23
+ enabledKinds,
24
+ statusFilter,
25
+ setStatusFilter,
26
+ } = useFeedbackBody();
27
+ const { messages, theme } = useFeedbackUi();
28
+ const { routes, colors, androidToolbarIcons } = useRoutedFeedback();
29
+ const router = useRouter();
30
+ const searchRef = useRef<SearchBarCommands>(null);
31
+
32
+ useEffect(() => {
33
+ const searching = query.trim().length > 0;
34
+ if (searching !== isSearching) setIsSearching(searching);
35
+ }, [query, isSearching, setIsSearching]);
36
+
37
+ const openEntry = (entryId: string) => {
38
+ if ((query || isSearching) && searchRef.current) {
39
+ searchRef.current.blur();
40
+ }
41
+ router.push(feedbackRouteHref(routes.entry, { entryId }), {
42
+ relativeToDirectory: true,
43
+ });
44
+ };
45
+
46
+ return (
47
+ <>
48
+ <FeedbackBoard.Root {...colors}>
49
+ <FeedbackScreenList
50
+ showSelectedEntry={false}
51
+ hideBackButton
52
+ onEntryOpen={openEntry}
53
+ onCreateEntry={() =>
54
+ router.push(feedbackRouteHref(routes.create), {
55
+ relativeToDirectory: true,
56
+ })
57
+ }
58
+ />
59
+ </FeedbackBoard.Root>
60
+
61
+ <Stack.Toolbar placement="right">
62
+ <Stack.Toolbar.Button
63
+ icon={
64
+ process.env.EXPO_OS === "ios" ? "plus" : androidToolbarIcons.create
65
+ }
66
+ variant="prominent"
67
+ accessibilityLabel={createEntryLabel(enabledKinds, messages)}
68
+ onPress={() =>
69
+ router.push(feedbackRouteHref(routes.create), {
70
+ relativeToDirectory: true,
71
+ })
72
+ }
73
+ tintColor={theme.colors.primary}
74
+ >
75
+ {createEntryLabel(enabledKinds, messages)}
76
+ </Stack.Toolbar.Button>
77
+ </Stack.Toolbar>
78
+ <Stack.Toolbar placement="left">
79
+ <Stack.Toolbar.Button
80
+ hidden={!router.canGoBack()}
81
+ icon={
82
+ process.env.EXPO_OS === "ios"
83
+ ? "chevron.backward"
84
+ : androidToolbarIcons.back
85
+ }
86
+ accessibilityLabel={messages.entry.back}
87
+ onPress={() => router.back()}
88
+ >
89
+ {messages.entry.back}
90
+ </Stack.Toolbar.Button>
91
+ </Stack.Toolbar>
92
+ <Stack.SearchBar
93
+ ref={searchRef}
94
+ placeholder={messages.board.searchPlaceholder}
95
+ onChangeText={(event) => setQuery(event.nativeEvent.text)}
96
+ onFocus={() => setIsSearching(true)}
97
+ onBlur={() => setIsSearching(query.trim().length > 0)}
98
+ obscureBackground={false}
99
+ allowToolbarIntegration
100
+ hideNavigationBar={false}
101
+ textColor={theme.colors.text}
102
+ />
103
+ <Stack.Toolbar placement="bottom">
104
+ <Stack.Toolbar.SearchBarSlot />
105
+ <Stack.Toolbar.Menu
106
+ icon={
107
+ process.env.EXPO_OS === "ios"
108
+ ? "line.3.horizontal.decrease"
109
+ : androidToolbarIcons.filter
110
+ }
111
+ title={messages.board.statusFilter}
112
+ accessibilityLabel={messages.board.statusFilter}
113
+ tintColor={theme.colors.primary}
114
+ >
115
+ {entryStatusChoices(messages).map(({ value, label }) => (
116
+ <Stack.Toolbar.MenuAction
117
+ key={value}
118
+ isOn={statusFilter === value}
119
+ onPress={() => setStatusFilter(value)}
120
+ >
121
+ {label}
122
+ </Stack.Toolbar.MenuAction>
123
+ ))}
124
+ </Stack.Toolbar.Menu>
125
+ </Stack.Toolbar>
126
+ </>
127
+ );
128
+ }
129
+
130
+ export function FeedbackEntryScreen() {
131
+ const params = useLocalSearchParams<{
132
+ entryId?: string | string[];
133
+ }>();
134
+ const entryId = Array.isArray(params.entryId)
135
+ ? params.entryId[0]
136
+ : params.entryId;
137
+
138
+ if (!entryId) {
139
+ throw new Error(
140
+ 'FeedbackEntryScreen requires an "entryId" dynamic route parameter.',
141
+ );
142
+ }
143
+
144
+ return <FeedbackEntryRouteContent entryId={entryId} />;
145
+ }
146
+
147
+ function FeedbackEntryRouteContent({ entryId }: { entryId: string }) {
148
+ const { hooks } = useFeedbackBody();
149
+ const { messages, theme } = useFeedbackUi();
150
+ const { colors, androidToolbarIcons } = useRoutedFeedback();
151
+ const modal = useRoutedFeedbackModal();
152
+ const router = useRouter();
153
+ const entry = hooks.useEntry(entryId);
154
+
155
+ return (
156
+ <>
157
+ {entry && <Stack.Screen options={{ headerTitle: entry.title }} />}
158
+ <FeedbackBoard.Root {...colors}>
159
+ <FeedbackBoard.List style={{ padding: theme.spacing }}>
160
+ <EntryDetail
161
+ entryId={entryId}
162
+ hideBackButton
163
+ onBack={() => router.back()}
164
+ />
165
+ </FeedbackBoard.List>
166
+ </FeedbackBoard.Root>
167
+ {modal && (
168
+ <Stack.Toolbar placement="right">
169
+ <Stack.Toolbar.Button
170
+ icon={
171
+ process.env.EXPO_OS === "ios"
172
+ ? "xmark"
173
+ : androidToolbarIcons.close
174
+ }
175
+ accessibilityLabel={messages.form.cancel}
176
+ onPress={modal.dismiss}
177
+ >
178
+ {messages.form.cancel}
179
+ </Stack.Toolbar.Button>
180
+ </Stack.Toolbar>
181
+ )}
182
+ </>
183
+ );
184
+ }
185
+
186
+ export function CreateFeedbackScreen() {
187
+ const { enabledKinds } = useFeedbackBody();
188
+ const { messages, theme } = useFeedbackUi();
189
+ const { routes, colors, androidToolbarIcons } = useRoutedFeedback();
190
+ const modal = useRoutedFeedbackModal();
191
+ const router = useRouter();
192
+ const [kind, setKind] = useState<EntryKind>(enabledKinds[0] ?? "feedback");
193
+ const [title, setTitle] = useState("");
194
+ const [body, setBody] = useState("");
195
+
196
+ const entryHref = (entryId: string) =>
197
+ feedbackRouteHref(routes.entry, { entryId });
198
+
199
+ return (
200
+ <>
201
+ <FeedbackBoard.Root {...colors}>
202
+ <FeedbackBoard.List
203
+ style={{
204
+ padding: theme.spacing,
205
+ paddingBottom: theme.spacing * 2,
206
+ }}
207
+ >
208
+ <CreateEntryForm
209
+ kind={kind}
210
+ onKindChange={setKind}
211
+ title={title}
212
+ onTitleChange={setTitle}
213
+ body={body}
214
+ onBodyChange={setBody}
215
+ onOpenSuggestion={(entryId) =>
216
+ router.push(entryHref(entryId), { relativeToDirectory: true })
217
+ }
218
+ onCreated={(entryId) => {
219
+ router.dismissTo(entryHref(entryId));
220
+ }}
221
+ />
222
+ </FeedbackBoard.List>
223
+ </FeedbackBoard.Root>
224
+
225
+ <Stack.Toolbar placement="left">
226
+ <Stack.Toolbar.Button
227
+ icon={
228
+ process.env.EXPO_OS === "ios" ? "xmark" : androidToolbarIcons.close
229
+ }
230
+ accessibilityLabel={messages.form.cancel}
231
+ onPress={() => (modal ? modal.dismiss() : router.dismiss())}
232
+ >
233
+ {messages.form.cancel}
234
+ </Stack.Toolbar.Button>
235
+ </Stack.Toolbar>
236
+ </>
237
+ );
238
+ }
@@ -16,6 +16,7 @@ export function FeedbackScreen({
16
16
  maxCommentDepth = 5,
17
17
  debounceDuration = 300,
18
18
  collectMetadata,
19
+ emptyState,
19
20
  transformComments,
20
21
  renderActor,
21
22
  ...props
@@ -30,6 +31,7 @@ export function FeedbackScreen({
30
31
  maxCommentDepth={maxCommentDepth}
31
32
  debounceDuration={debounceDuration}
32
33
  collectMetadata={collectMetadata}
34
+ emptyState={emptyState}
33
35
  collectStandardMetadata={collectExpoMetadata}
34
36
  transformComments={transformComments}
35
37
  renderActor={renderActor}
@@ -1,11 +1,13 @@
1
1
  import { Stack, useRouter } from "expo-router";
2
2
  import { useFeedbackBody } from "../../shared/context/FeedbackBodyProvider";
3
3
  import { useFeedbackUi } from "../../shared/context/FeedbackProvider";
4
+ import { createEntryLabel, entryStatusChoices } from "../../shared/helpers";
4
5
  import type { FeedbackStackProps } from "./types";
5
6
 
6
7
  export function FeedbackStack({
7
8
  searchRef,
8
9
  stackOptions,
10
+ androidToolbarIcons = {},
9
11
  children,
10
12
  }: FeedbackStackProps) {
11
13
  const {
@@ -16,6 +18,8 @@ export function FeedbackStack({
16
18
  setShowForm,
17
19
  setIsSearching,
18
20
  setSelectedEntryId,
21
+ statusFilter,
22
+ setStatusFilter,
19
23
  } = useFeedbackBody();
20
24
  const { messages, theme } = useFeedbackUi();
21
25
  const router = useRouter();
@@ -54,19 +58,30 @@ export function FeedbackStack({
54
58
  />
55
59
  <Stack.Toolbar placement="right">
56
60
  <Stack.Toolbar.Button
57
- icon="plus"
61
+ icon={
62
+ process.env.EXPO_OS === "ios" ? "plus" : androidToolbarIcons.create
63
+ }
58
64
  variant="prominent"
59
- accessibilityLabel={messages.board.createEntry}
65
+ accessibilityLabel={createEntryLabel(enabledKinds, messages)}
60
66
  onPress={() => setShowForm(true)}
61
67
  tintColor={theme.colors.primary}
62
- />
68
+ >
69
+ {createEntryLabel(enabledKinds, messages)}
70
+ </Stack.Toolbar.Button>
63
71
  </Stack.Toolbar>
64
72
  <Stack.Toolbar placement="left">
65
73
  <Stack.Toolbar.Button
66
74
  hidden={!canGoBack}
67
- icon="chevron.backward"
75
+ icon={
76
+ process.env.EXPO_OS === "ios"
77
+ ? "chevron.backward"
78
+ : androidToolbarIcons.back
79
+ }
80
+ accessibilityLabel={messages.entry.back}
68
81
  onPress={handleBackPress}
69
- />
82
+ >
83
+ {messages.entry.back}
84
+ </Stack.Toolbar.Button>
70
85
  </Stack.Toolbar>
71
86
 
72
87
  {children}
@@ -93,6 +108,26 @@ export function FeedbackStack({
93
108
  />
94
109
  <Stack.Toolbar placement="bottom">
95
110
  <Stack.Toolbar.SearchBarSlot />
111
+ <Stack.Toolbar.Menu
112
+ icon={
113
+ process.env.EXPO_OS === "ios"
114
+ ? "line.3.horizontal.decrease"
115
+ : androidToolbarIcons.filter
116
+ }
117
+ title={messages.board.statusFilter}
118
+ accessibilityLabel={messages.board.statusFilter}
119
+ tintColor={theme.colors.primary}
120
+ >
121
+ {entryStatusChoices(messages).map(({ value, label }) => (
122
+ <Stack.Toolbar.MenuAction
123
+ key={value}
124
+ isOn={statusFilter === value}
125
+ onPress={() => setStatusFilter(value)}
126
+ >
127
+ {label}
128
+ </Stack.Toolbar.MenuAction>
129
+ ))}
130
+ </Stack.Toolbar.Menu>
96
131
  </Stack.Toolbar>
97
132
  </>
98
133
  );
@@ -0,0 +1,118 @@
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
+ emptyState,
25
+ transformComments,
26
+ renderActor,
27
+ routes: routeOverrides,
28
+ screenOptions,
29
+ boardOptions,
30
+ entryOptions,
31
+ createOptions,
32
+ androidToolbarIcons = {},
33
+ primaryColor,
34
+ primaryForeground,
35
+ backgroundColor,
36
+ surfaceColor,
37
+ textColor,
38
+ mutedColor,
39
+ borderColor,
40
+ dangerColor,
41
+ }: FeedbackStackLayoutProps) {
42
+ const routes = resolveFeedbackRoutes(routeOverrides);
43
+ const resolvedMessages = mergeFeedbackMessages(messages);
44
+ const resolvedTheme = mergeFeedbackTheme(theme);
45
+ const colors = {
46
+ primaryColor,
47
+ primaryForeground,
48
+ backgroundColor,
49
+ surfaceColor,
50
+ textColor,
51
+ mutedColor,
52
+ borderColor,
53
+ dangerColor,
54
+ };
55
+ const background = backgroundColor ?? resolvedTheme.colors.background;
56
+ const boardTitle =
57
+ enabledKinds.length === 1
58
+ ? resolvedMessages.kindsPlural[enabledKinds.at(0)!]
59
+ : resolvedMessages.board.title;
60
+ const defaults: NativeStackNavigationOptions = {
61
+ headerShown: true,
62
+ headerBackButtonDisplayMode: "minimal",
63
+ contentStyle: { backgroundColor: background },
64
+ };
65
+
66
+ return (
67
+ <FeedbackProvider messages={messages} theme={theme} unstyled={unstyled}>
68
+ <FeedbackBodyProvider
69
+ hooks={hooks}
70
+ entrySort={entrySort}
71
+ commentSort={commentSort}
72
+ enabledKinds={enabledKinds}
73
+ maxCommentDepth={maxCommentDepth}
74
+ debounceDuration={debounceDuration}
75
+ collectMetadata={collectMetadata}
76
+ emptyState={emptyState}
77
+ collectStandardMetadata={collectExpoMetadata}
78
+ transformComments={transformComments}
79
+ renderActor={renderActor}
80
+ >
81
+ <RoutedFeedbackProvider value={{ routes, colors, androidToolbarIcons }}>
82
+ <Stack>
83
+ <Stack.Screen
84
+ name={routes.board}
85
+ options={{
86
+ ...defaults,
87
+ headerTitle: boardTitle,
88
+ headerTransparent: true,
89
+ headerShadowVisible: true,
90
+ ...screenOptions,
91
+ ...boardOptions,
92
+ }}
93
+ />
94
+ <Stack.Screen
95
+ name={routes.entry}
96
+ options={{
97
+ ...defaults,
98
+ headerTitle: boardTitle,
99
+ ...screenOptions,
100
+ ...entryOptions,
101
+ }}
102
+ />
103
+ <Stack.Screen
104
+ name={routes.create}
105
+ options={{
106
+ ...defaults,
107
+ headerShown: false,
108
+ presentation: "modal",
109
+ ...screenOptions,
110
+ ...createOptions,
111
+ }}
112
+ />
113
+ </Stack>
114
+ </RoutedFeedbackProvider>
115
+ </FeedbackBodyProvider>
116
+ </FeedbackProvider>
117
+ );
118
+ }
@@ -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, close, and filter actions.
38
+ Missing 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
+ }
@@ -7,4 +7,8 @@ export type * from "../../shared/types/index.js";
7
7
  export * from "../shared/ui/primitives.js";
8
8
  export * from "./metadata.js";
9
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";
10
14
  export type * from "./types.js";