convex-feedback-ui 0.2.0-beta.2 → 0.2.0-beta.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 (86) hide show
  1. package/README.md +40 -0
  2. package/dist/.tsbuildinfo +1 -1
  3. package/dist/native/expo/FeedbackRoutes.d.ts.map +1 -1
  4. package/dist/native/expo/FeedbackRoutes.js +15 -2
  5. package/dist/native/expo/FeedbackRoutes.js.map +1 -1
  6. package/dist/native/expo/RoadmapRoutes.d.ts +3 -0
  7. package/dist/native/expo/RoadmapRoutes.d.ts.map +1 -0
  8. package/dist/native/expo/RoadmapRoutes.js +61 -0
  9. package/dist/native/expo/RoadmapRoutes.js.map +1 -0
  10. package/dist/native/expo/RoadmapScreen.d.ts +24 -0
  11. package/dist/native/expo/RoadmapScreen.d.ts.map +1 -0
  12. package/dist/native/expo/RoadmapScreen.js +43 -0
  13. package/dist/native/expo/RoadmapScreen.js.map +1 -0
  14. package/dist/native/expo/RoadmapStackLayout.d.ts +8 -0
  15. package/dist/native/expo/RoadmapStackLayout.d.ts.map +1 -0
  16. package/dist/native/expo/RoadmapStackLayout.js +55 -0
  17. package/dist/native/expo/RoadmapStackLayout.js.map +1 -0
  18. package/dist/native/expo/RoutedRoadmapContext.d.ts +4 -0
  19. package/dist/native/expo/RoutedRoadmapContext.d.ts.map +1 -0
  20. package/dist/native/expo/RoutedRoadmapContext.js +11 -0
  21. package/dist/native/expo/RoutedRoadmapContext.js.map +1 -0
  22. package/dist/native/expo/index.d.ts +5 -1
  23. package/dist/native/expo/index.d.ts.map +1 -1
  24. package/dist/native/expo/index.js +5 -1
  25. package/dist/native/expo/index.js.map +1 -1
  26. package/dist/native/expo/routes.d.ts +17 -1
  27. package/dist/native/expo/routes.d.ts.map +1 -1
  28. package/dist/native/expo/routes.js +46 -0
  29. package/dist/native/expo/routes.js.map +1 -1
  30. package/dist/native/expo/types.d.ts +35 -1
  31. package/dist/native/expo/types.d.ts.map +1 -1
  32. package/dist/native/native/index.d.ts +1 -0
  33. package/dist/native/native/index.d.ts.map +1 -1
  34. package/dist/native/native/index.js +1 -0
  35. package/dist/native/native/index.js.map +1 -1
  36. package/dist/native/shared/ui/EntryCard.d.ts +1 -1
  37. package/dist/native/shared/ui/EntryCard.d.ts.map +1 -1
  38. package/dist/native/shared/ui/EntryCard.js +2 -2
  39. package/dist/native/shared/ui/EntryCard.js.map +1 -1
  40. package/dist/native/shared/ui/RoadmapBoard.d.ts +55 -0
  41. package/dist/native/shared/ui/RoadmapBoard.d.ts.map +1 -0
  42. package/dist/native/shared/ui/RoadmapBoard.js +89 -0
  43. package/dist/native/shared/ui/RoadmapBoard.js.map +1 -0
  44. package/dist/native/shared/ui/RoadmapScreen.d.ts +36 -1
  45. package/dist/native/shared/ui/RoadmapScreen.d.ts.map +1 -1
  46. package/dist/native/shared/ui/RoadmapScreen.js +143 -51
  47. package/dist/native/shared/ui/RoadmapScreen.js.map +1 -1
  48. package/dist/native/shared/ui/primitives.d.ts +1 -1
  49. package/dist/native/shared/ui/primitives.d.ts.map +1 -1
  50. package/dist/native/shared/ui/primitives.js +3 -1
  51. package/dist/native/shared/ui/primitives.js.map +1 -1
  52. package/dist/react/roadmap.d.ts.map +1 -1
  53. package/dist/react/roadmap.js +25 -13
  54. package/dist/react/roadmap.js.map +1 -1
  55. package/dist/react/styles.css +129 -0
  56. package/dist/shared/messages.d.ts.map +1 -1
  57. package/dist/shared/messages.js +24 -0
  58. package/dist/shared/messages.js.map +1 -1
  59. package/dist/shared/types/messages.d.ts +28 -1
  60. package/dist/shared/types/messages.d.ts.map +1 -1
  61. package/dist/shared/types/roadmap.d.ts +10 -1
  62. package/dist/shared/types/roadmap.d.ts.map +1 -1
  63. package/dist/shared/types/screen.d.ts +2 -0
  64. package/dist/shared/types/screen.d.ts.map +1 -1
  65. package/package.json +2 -2
  66. package/src/native/expo/FeedbackRoutes.tsx +30 -7
  67. package/src/native/expo/README.md +10 -2
  68. package/src/native/expo/RoadmapRoutes.tsx +171 -0
  69. package/src/native/expo/RoadmapScreen.tsx +177 -0
  70. package/src/native/expo/RoadmapStackLayout.tsx +110 -0
  71. package/src/native/expo/RoutedRoadmapContext.tsx +21 -0
  72. package/src/native/expo/index.ts +10 -1
  73. package/src/native/expo/routes.ts +69 -1
  74. package/src/native/expo/types.ts +43 -0
  75. package/src/native/native/README.md +9 -3
  76. package/src/native/native/index.ts +1 -0
  77. package/src/native/shared/ui/EntryCard.tsx +2 -1
  78. package/src/native/shared/ui/RoadmapBoard.tsx +225 -0
  79. package/src/native/shared/ui/RoadmapScreen.tsx +415 -242
  80. package/src/native/shared/ui/primitives.tsx +3 -0
  81. package/src/react/roadmap.tsx +140 -85
  82. package/src/react/styles.css +129 -0
  83. package/src/shared/messages.ts +25 -0
  84. package/src/shared/types/messages.ts +41 -1
  85. package/src/shared/types/roadmap.ts +18 -1
  86. package/src/shared/types/screen.ts +2 -0
@@ -52,5 +52,13 @@ The existing `FeedbackScreen` remains the self-contained API for conditional
52
52
  in-screen navigation.
53
53
 
54
54
  `RoadmapScreen` is also exported from this entry point for public roadmap
55
- browsing. Pass `onEntryOpen` for host navigation and `onUnauthenticated` for
56
- the host's sign-in UI.
55
+ browsing. It uses the current Expo Router stack by default, including its
56
+ native Stack title, search bar, and toolbar back action; pass `useStack={false}`
57
+ for the plain React Native implementation. Pass `onEntryOpen` for host navigation and
58
+ `onUnauthenticated` for the host's sign-in UI. For real Expo Router pages, use
59
+ `RoadmapStackLayout` with `RoadmapBoardScreen` and `RoadmapItemScreen`, just
60
+ as the routed feedback integration uses its own stack layout.
61
+
62
+ The routed entry page includes an explicit Stack back action so navigation into
63
+ feedback from a roadmap item remains reversible even when the host uses nested
64
+ Expo Router stacks.
@@ -0,0 +1,171 @@
1
+ import { Stack, useLocalSearchParams, useRouter } from "expo-router";
2
+ import { useRef, useState } from "react";
3
+ import { ActivityIndicator } from "react-native";
4
+ import type { SearchBarCommands } from "react-native-screens";
5
+ import { useSafeAreaInsets } from "react-native-safe-area-context";
6
+
7
+ import { useFeedbackBody } from "../../shared/context/FeedbackBodyProvider.js";
8
+ import { useFeedbackUi } from "../../shared/context/FeedbackProvider.js";
9
+ import { FeedbackBoard } from "../shared/ui/primitives.js";
10
+ import {
11
+ RoadmapBoardContent,
12
+ RoadmapItemContent,
13
+ } from "../shared/ui/RoadmapScreen.js";
14
+ import {
15
+ roadmapRouteHref,
16
+ roadmapRouteParams,
17
+ parseRoadmapRouteItem,
18
+ } from "./routes.js";
19
+ import { useRoutedRoadmap } from "./RoutedRoadmapContext.js";
20
+
21
+ export function RoadmapBoardScreen() {
22
+ const { messages, theme } = useFeedbackUi();
23
+ const { routes, colors, androidToolbarIcons, pageSize } = useRoutedRoadmap();
24
+ const router = useRouter();
25
+ const [query, setQuery] = useState("");
26
+ const searchRef = useRef<SearchBarCommands>(null);
27
+
28
+ return (
29
+ <>
30
+ <Stack.Screen
31
+ options={{
32
+ headerTitle: messages.roadmap.title,
33
+ headerBackVisible: false,
34
+ }}
35
+ />
36
+ <FeedbackBoard.Root {...colors}>
37
+ <RoadmapBoardContent
38
+ pageSize={pageSize}
39
+ query={query}
40
+ onQueryChange={setQuery}
41
+ showHeader={false}
42
+ onItemOpen={(item) => {
43
+ searchRef.current?.blur();
44
+ router.push(
45
+ roadmapRouteHref(routes.item, roadmapRouteParams(item)),
46
+ { relativeToDirectory: true },
47
+ );
48
+ }}
49
+ />
50
+ </FeedbackBoard.Root>
51
+
52
+ <Stack.SearchBar
53
+ ref={searchRef}
54
+ placeholder={messages.roadmap.searchPlaceholder}
55
+ onChangeText={(event) =>
56
+ setQuery(
57
+ (event as unknown as { nativeEvent: { text: string } }).nativeEvent
58
+ .text,
59
+ )
60
+ }
61
+ obscureBackground={false}
62
+ allowToolbarIntegration
63
+ hideNavigationBar={false}
64
+ textColor={theme.colors.text}
65
+ />
66
+ <Stack.Toolbar placement="bottom">
67
+ <Stack.Toolbar.SearchBarSlot />
68
+ </Stack.Toolbar>
69
+ <Stack.Toolbar placement="left">
70
+ <Stack.Toolbar.Button
71
+ hidden={!router.canGoBack()}
72
+ icon={
73
+ process.env.EXPO_OS === "ios"
74
+ ? "chevron.backward"
75
+ : androidToolbarIcons.back
76
+ }
77
+ accessibilityLabel={messages.entry.back}
78
+ onPress={() => router.back()}
79
+ tintColor={theme.colors.text}
80
+ >
81
+ {messages.entry.back}
82
+ </Stack.Toolbar.Button>
83
+ </Stack.Toolbar>
84
+ </>
85
+ );
86
+ }
87
+
88
+ export function RoadmapItemScreen() {
89
+ const params = useLocalSearchParams<{
90
+ roadmapId?: string | string[];
91
+ item?: string | string[];
92
+ }>();
93
+ const { hooks } = useFeedbackBody();
94
+ const { messages, theme } = useFeedbackUi();
95
+ const { routes, colors, androidToolbarIcons, entryPageSize, onEntryOpen } =
96
+ useRoutedRoadmap();
97
+ const router = useRouter();
98
+ const insets = useSafeAreaInsets();
99
+ const roadmapId = Array.isArray(params.roadmapId)
100
+ ? params.roadmapId[0]
101
+ : params.roadmapId;
102
+ const routeItem = parseRoadmapRouteItem(params.item);
103
+ const roadmap = hooks.useRoadmap();
104
+ const item =
105
+ roadmap.results.find((candidate) => candidate.id === roadmapId) ??
106
+ routeItem;
107
+
108
+ if (!roadmapId) {
109
+ throw new Error(
110
+ 'RoadmapItemScreen requires a "roadmapId" dynamic route parameter.',
111
+ );
112
+ }
113
+
114
+ return (
115
+ <>
116
+ {item ? (
117
+ <>
118
+ <Stack.Screen
119
+ options={{ headerTitle: item.title, headerBackVisible: false }}
120
+ />
121
+ <FeedbackBoard.Root {...colors}>
122
+ <FeedbackBoard.List
123
+ style={{
124
+ padding: theme.spacing,
125
+ paddingBottom: insets.bottom + theme.spacing * 2,
126
+ backgroundColor: theme.colors.background,
127
+ }}
128
+ >
129
+ <RoadmapItemContent
130
+ item={item}
131
+ entryPageSize={entryPageSize}
132
+ onEntryOpen={onEntryOpen}
133
+ />
134
+ </FeedbackBoard.List>
135
+ </FeedbackBoard.Root>
136
+ </>
137
+ ) : (
138
+ <FeedbackBoard.Root {...colors}>
139
+ <FeedbackBoard.State>
140
+ {roadmap.status === "LoadingFirstPage" ? (
141
+ <ActivityIndicator color={theme.colors.primary} />
142
+ ) : (
143
+ messages.roadmap.noItems
144
+ )}
145
+ </FeedbackBoard.State>
146
+ </FeedbackBoard.Root>
147
+ )}
148
+
149
+ <Stack.Toolbar placement="left">
150
+ <Stack.Toolbar.Button
151
+ icon={
152
+ process.env.EXPO_OS === "ios"
153
+ ? "chevron.backward"
154
+ : androidToolbarIcons.back
155
+ }
156
+ accessibilityLabel={messages.entry.back}
157
+ onPress={() => {
158
+ if (router.canGoBack()) {
159
+ router.back();
160
+ } else {
161
+ router.replace(roadmapRouteHref(routes.board));
162
+ }
163
+ }}
164
+ tintColor={theme.colors.text}
165
+ >
166
+ {messages.entry.back}
167
+ </Stack.Toolbar.Button>
168
+ </Stack.Toolbar>
169
+ </>
170
+ );
171
+ }
@@ -0,0 +1,177 @@
1
+ import { Stack, useRouter } from "expo-router";
2
+ import { useRef, useState } from "react";
3
+ import type { SearchBarCommands } from "react-native-screens";
4
+
5
+ import { useFeedbackUi } from "../../shared/context/FeedbackProvider.js";
6
+ import type { mergeFeedbackMessages } from "../../shared/messages.js";
7
+ import type { mergeFeedbackTheme } from "../../shared/theme.js";
8
+ import type { RoadmapScreenProps } from "../../shared/types/index.js";
9
+ import {
10
+ RoadmapProvider,
11
+ RoadmapScreen as NativeRoadmapScreen,
12
+ RoadmapScreenContent,
13
+ } from "../shared/ui/RoadmapScreen.js";
14
+ import type {
15
+ RoadmapAndroidToolbarIcons,
16
+ RoadmapStackScreenOptions,
17
+ } from "./types.js";
18
+
19
+ interface RoadmapScreenWithStack {
20
+ /** Enables the current Expo Router stack integration. @default true */
21
+ useStack?: true;
22
+ /** Options applied to the current Expo Router screen. */
23
+ StackOptions?: RoadmapStackScreenOptions;
24
+ /** Android image source for the toolbar back action. */
25
+ androidToolbarIcons?: RoadmapAndroidToolbarIcons;
26
+ }
27
+
28
+ interface RoadmapScreenWithoutStack {
29
+ useStack: false;
30
+ StackOptions?: never;
31
+ androidToolbarIcons?: never;
32
+ }
33
+
34
+ export type ExpoRoadmapScreenProps = RoadmapScreenProps &
35
+ (RoadmapScreenWithStack | RoadmapScreenWithoutStack);
36
+
37
+ /**
38
+ * Convenience roadmap screen for a single Expo route. Use
39
+ * `RoadmapStackLayout` when the board and item detail should be separate
40
+ * discovered Expo Router pages.
41
+ */
42
+ export function RoadmapScreen(props: ExpoRoadmapScreenProps) {
43
+ if (props.useStack === false) {
44
+ return <NativeRoadmapScreen {...props} />;
45
+ }
46
+
47
+ return (
48
+ <RoadmapProvider
49
+ hooks={props.hooks}
50
+ messages={props.messages}
51
+ theme={props.theme}
52
+ unstyled={props.unstyled}
53
+ commentSort={props.commentSort}
54
+ maxCommentDepth={props.maxCommentDepth}
55
+ transformComments={props.transformComments}
56
+ renderActor={props.renderActor}
57
+ onUnauthenticated={props.onUnauthenticated}
58
+ >
59
+ <StackedRoadmapScreen props={props} />
60
+ </RoadmapProvider>
61
+ );
62
+ }
63
+
64
+ function StackedRoadmapScreen({
65
+ props,
66
+ }: {
67
+ props: RoadmapScreenWithStack & RoadmapScreenProps;
68
+ }) {
69
+ const {
70
+ androidToolbarIcons = {},
71
+ hooks,
72
+ pageSize,
73
+ entryPageSize,
74
+ onEntryOpen,
75
+ onUnauthenticated,
76
+ primaryColor,
77
+ primaryForeground,
78
+ backgroundColor,
79
+ surfaceColor,
80
+ textColor,
81
+ mutedColor,
82
+ borderColor,
83
+ dangerColor,
84
+ } = props;
85
+ const { messages, theme } = useFeedbackUi();
86
+ const [query, setQuery] = useState("");
87
+ const searchRef = useRef<SearchBarCommands>(null);
88
+
89
+ return (
90
+ <>
91
+ <Stack.Screen
92
+ options={{
93
+ headerTitle: messages.roadmap.title,
94
+ headerShown: true,
95
+ headerTransparent: false,
96
+ headerShadowVisible: true,
97
+ headerBackButtonDisplayMode: "minimal",
98
+ headerBackVisible: false,
99
+ headerTintColor: textColor ?? theme.colors.text,
100
+ contentStyle: {
101
+ backgroundColor: backgroundColor ?? theme.colors.background,
102
+ },
103
+ ...props.StackOptions,
104
+ }}
105
+ />
106
+ <Stack.SearchBar
107
+ ref={searchRef}
108
+ placeholder={messages.roadmap.searchPlaceholder}
109
+ onChangeText={(event) =>
110
+ setQuery(
111
+ (event as unknown as { nativeEvent: { text: string } }).nativeEvent
112
+ .text,
113
+ )
114
+ }
115
+ obscureBackground={false}
116
+ allowToolbarIntegration
117
+ hideNavigationBar={false}
118
+ textColor={theme.colors.text}
119
+ />
120
+ <Stack.Toolbar placement="bottom">
121
+ <Stack.Toolbar.SearchBarSlot />
122
+ </Stack.Toolbar>
123
+ <RoadmapToolbar
124
+ androidToolbarIcons={androidToolbarIcons}
125
+ messages={messages}
126
+ theme={theme}
127
+ />
128
+ <RoadmapScreenContent
129
+ pageSize={pageSize ?? hooks.pageSizes.roadmap}
130
+ entryPageSize={entryPageSize ?? hooks.pageSizes.entries}
131
+ query={query}
132
+ onQueryChange={setQuery}
133
+ showBoardHeader={false}
134
+ onEntryOpen={onEntryOpen}
135
+ onUnauthenticated={onUnauthenticated}
136
+ primaryColor={primaryColor}
137
+ primaryForeground={primaryForeground}
138
+ backgroundColor={backgroundColor}
139
+ surfaceColor={surfaceColor}
140
+ textColor={textColor}
141
+ mutedColor={mutedColor}
142
+ borderColor={borderColor}
143
+ dangerColor={dangerColor}
144
+ />
145
+ </>
146
+ );
147
+ }
148
+
149
+ function RoadmapToolbar({
150
+ androidToolbarIcons,
151
+ messages,
152
+ theme,
153
+ }: {
154
+ androidToolbarIcons: RoadmapAndroidToolbarIcons;
155
+ messages: ReturnType<typeof mergeFeedbackMessages>;
156
+ theme: ReturnType<typeof mergeFeedbackTheme>;
157
+ }) {
158
+ const router = useRouter();
159
+
160
+ return (
161
+ <Stack.Toolbar placement="left">
162
+ <Stack.Toolbar.Button
163
+ hidden={!router.canGoBack()}
164
+ icon={
165
+ process.env.EXPO_OS === "ios"
166
+ ? "chevron.backward"
167
+ : androidToolbarIcons.back
168
+ }
169
+ accessibilityLabel={messages.entry.back}
170
+ onPress={() => router.back()}
171
+ tintColor={theme.colors.text}
172
+ >
173
+ {messages.entry.back}
174
+ </Stack.Toolbar.Button>
175
+ </Stack.Toolbar>
176
+ );
177
+ }
@@ -0,0 +1,110 @@
1
+ import { Stack, type NativeStackNavigationOptions } from "expo-router";
2
+
3
+ import { mergeFeedbackMessages } from "../../shared/messages.js";
4
+ import { mergeFeedbackTheme } from "../../shared/theme.js";
5
+ import { RoadmapProvider } from "../shared/ui/RoadmapScreen.js";
6
+ import { RoutedRoadmapProvider } from "./RoutedRoadmapContext.js";
7
+ import { resolveRoadmapRoutes } from "./routes.js";
8
+ import type { RoadmapStackLayoutProps } from "./types.js";
9
+
10
+ /**
11
+ * Provider and native-stack layout for real Expo Router roadmap pages.
12
+ * Consumers supply the small route files; this component keeps the roadmap
13
+ * query and entry-detail context mounted while a detail page is pushed.
14
+ */
15
+ export function RoadmapStackLayout({
16
+ hooks,
17
+ messages,
18
+ theme,
19
+ unstyled,
20
+ onEntryOpen,
21
+ onUnauthenticated,
22
+ commentSort,
23
+ maxCommentDepth,
24
+ transformComments,
25
+ renderActor,
26
+ pageSize = hooks.pageSizes.roadmap,
27
+ entryPageSize = hooks.pageSizes.entries,
28
+ routes: routeOverrides,
29
+ screenOptions,
30
+ boardOptions,
31
+ itemOptions,
32
+ androidToolbarIcons = {},
33
+ primaryColor,
34
+ primaryForeground,
35
+ backgroundColor,
36
+ surfaceColor,
37
+ textColor,
38
+ mutedColor,
39
+ borderColor,
40
+ dangerColor,
41
+ }: RoadmapStackLayoutProps) {
42
+ const routes = resolveRoadmapRoutes(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 defaults: NativeStackNavigationOptions = {
56
+ headerShown: true,
57
+ headerShadowVisible: true,
58
+ headerBackButtonDisplayMode: "minimal",
59
+ headerTintColor: textColor ?? resolvedTheme.colors.text,
60
+ contentStyle: {
61
+ backgroundColor: backgroundColor ?? resolvedTheme.colors.background,
62
+ },
63
+ };
64
+
65
+ return (
66
+ <RoadmapProvider
67
+ hooks={hooks}
68
+ messages={messages}
69
+ theme={theme}
70
+ unstyled={unstyled}
71
+ commentSort={commentSort}
72
+ maxCommentDepth={maxCommentDepth}
73
+ transformComments={transformComments}
74
+ renderActor={renderActor}
75
+ onUnauthenticated={onUnauthenticated}
76
+ >
77
+ <RoutedRoadmapProvider
78
+ value={{
79
+ routes,
80
+ colors,
81
+ androidToolbarIcons,
82
+ pageSize,
83
+ entryPageSize,
84
+ onEntryOpen,
85
+ }}
86
+ >
87
+ <Stack>
88
+ <Stack.Screen
89
+ name={routes.board}
90
+ options={{
91
+ ...defaults,
92
+ headerTitle: resolvedMessages.roadmap.title,
93
+ ...screenOptions,
94
+ ...boardOptions,
95
+ }}
96
+ />
97
+ <Stack.Screen
98
+ name={routes.item}
99
+ options={{
100
+ ...defaults,
101
+ headerTitle: resolvedMessages.roadmap.title,
102
+ ...screenOptions,
103
+ ...itemOptions,
104
+ }}
105
+ />
106
+ </Stack>
107
+ </RoutedRoadmapProvider>
108
+ </RoadmapProvider>
109
+ );
110
+ }
@@ -0,0 +1,21 @@
1
+ import { createContext, useContext } from "react";
2
+
3
+ import type { RoutedRoadmapContextValue } from "./types.js";
4
+
5
+ const RoutedRoadmapContext = createContext<RoutedRoadmapContextValue | null>(
6
+ null,
7
+ );
8
+
9
+ export const RoutedRoadmapProvider = RoutedRoadmapContext.Provider;
10
+
11
+ export function useRoutedRoadmap(): RoutedRoadmapContextValue {
12
+ const value = useContext(RoutedRoadmapContext);
13
+
14
+ if (value === null) {
15
+ throw new Error(
16
+ "Routed roadmap screens must be rendered inside RoadmapStackLayout.",
17
+ );
18
+ }
19
+
20
+ return value;
21
+ }
@@ -5,11 +5,20 @@ export * from "../../shared/metadata.js";
5
5
  export * from "../../shared/theme.js";
6
6
  export type * from "../../shared/types/index.js";
7
7
  export * from "../shared/ui/primitives.js";
8
- export * from "../shared/ui/RoadmapScreen.js";
8
+ export * from "../shared/ui/RoadmapBoard.js";
9
+ export {
10
+ RoadmapBoardContent,
11
+ RoadmapItemContent,
12
+ RoadmapProvider,
13
+ RoadmapScreenContent,
14
+ } from "../shared/ui/RoadmapScreen.js";
9
15
  export * from "./metadata.js";
10
16
  export * from "./FeedbackScreen.js";
11
17
  export * from "./FeedbackCreateStackLayout.js";
12
18
  export * from "./FeedbackStackLayout.js";
13
19
  export * from "./FeedbackRoutes.js";
20
+ export * from "./RoadmapScreen.js";
21
+ export * from "./RoadmapStackLayout.js";
22
+ export * from "./RoadmapRoutes.js";
14
23
  export * from "./routes.js";
15
24
  export type * from "./types.js";
@@ -1,5 +1,6 @@
1
1
  import type { Href } from "expo-router";
2
- import type { FeedbackRouteNames } from "./types.js";
2
+ import type { RoadmapItem } from "convex-feedback";
3
+ import type { FeedbackRouteNames, RoadmapRouteNames } from "./types.js";
3
4
 
4
5
  export const defaultFeedbackRoutes: FeedbackRouteNames = {
5
6
  board: "index",
@@ -39,3 +40,70 @@ export function feedbackRouteHref(
39
40
  const pathname = route.startsWith(".") ? route : `./${route}`;
40
41
  return params === undefined ? pathname : { pathname, params };
41
42
  }
43
+
44
+ export const defaultRoadmapRoutes: RoadmapRouteNames = {
45
+ board: "index",
46
+ item: "[roadmapId]",
47
+ };
48
+
49
+ export function resolveRoadmapRoutes(
50
+ routes: Partial<RoadmapRouteNames> = {},
51
+ ): RoadmapRouteNames {
52
+ const resolved = { ...defaultRoadmapRoutes, ...routes };
53
+
54
+ if (!resolved.item.includes("[roadmapId]")) {
55
+ throw new Error(
56
+ 'The routed roadmap item route must contain the "[roadmapId]" dynamic segment.',
57
+ );
58
+ }
59
+
60
+ return resolved;
61
+ }
62
+
63
+ export function createRoadmapStackSettings(
64
+ routes?: Partial<RoadmapRouteNames>,
65
+ ) {
66
+ return { anchor: resolveRoadmapRoutes(routes).board };
67
+ }
68
+
69
+ export const roadmapStackSettings = createRoadmapStackSettings();
70
+
71
+ export function roadmapRouteHref(
72
+ route: string,
73
+ params?: Record<string, string>,
74
+ ): Href {
75
+ const pathname = route.startsWith(".") ? route : `./${route}`;
76
+ return params === undefined ? pathname : { pathname, params };
77
+ }
78
+
79
+ /** Pass the already loaded item through a push so a detail route renders immediately. */
80
+ export function roadmapRouteParams(item: RoadmapItem) {
81
+ return { roadmapId: item.id, item: JSON.stringify(item) };
82
+ }
83
+
84
+ function firstParam(value: string | string[] | undefined): string | undefined {
85
+ return Array.isArray(value) ? value[0] : value;
86
+ }
87
+
88
+ export function parseRoadmapRouteItem(
89
+ value: string | string[] | undefined,
90
+ ): RoadmapItem | undefined {
91
+ const serialized = firstParam(value);
92
+ if (!serialized) return undefined;
93
+
94
+ try {
95
+ const parsed: unknown = JSON.parse(serialized);
96
+ if (!parsed || typeof parsed !== "object") return undefined;
97
+ const item = parsed as Partial<RoadmapItem>;
98
+ if (
99
+ typeof item.id !== "string" ||
100
+ typeof item.title !== "string" ||
101
+ typeof item.status !== "string"
102
+ ) {
103
+ return undefined;
104
+ }
105
+ return item as RoadmapItem;
106
+ } catch {
107
+ return undefined;
108
+ }
109
+ }
@@ -5,6 +5,7 @@ import type { SearchBarCommands } from "react-native-screens";
5
5
  import type {
6
6
  FeedbackColorProps,
7
7
  FeedbackScreenRootProps,
8
+ RoadmapScreenProps,
8
9
  } from "../../shared/types";
9
10
 
10
11
  /** Optional wrapper used around Expo Router bottom toolbars. */
@@ -65,6 +66,13 @@ export interface FeedbackAndroidToolbarIcons {
65
66
  filter?: ImageSourcePropType;
66
67
  }
67
68
 
69
+ export interface RoadmapAndroidToolbarIcons {
70
+ /** Android image source for back actions. */
71
+ back?: ImageSourcePropType;
72
+ /** Android image source for modal close actions. */
73
+ close?: ImageSourcePropType;
74
+ }
75
+
68
76
  export interface FeedbackStackLayoutProps extends FeedbackScreenRootProps {
69
77
  /** Partial overrides for the prescribed route file names. */
70
78
  routes?: Partial<FeedbackRouteNames>;
@@ -91,9 +99,44 @@ export interface FeedbackCreateStackLayoutProps {
91
99
  entryOptions?: FeedbackStackScreenOptions;
92
100
  }
93
101
 
102
+ /** File names used by the routed public roadmap stack. */
103
+ export interface RoadmapRouteNames {
104
+ /** Board route. @default "index" */
105
+ board: string;
106
+ /** Roadmap item route. Must contain `[roadmapId]`. @default "[roadmapId]" */
107
+ item: string;
108
+ }
109
+
110
+ export type RoadmapStackScreenOptions = Exclude<
111
+ NonNullable<StackScreenProps["options"]>,
112
+ (...args: never[]) => unknown
113
+ >;
114
+
115
+ export interface RoadmapStackLayoutProps extends RoadmapScreenProps {
116
+ /** Partial overrides for the prescribed route file names. */
117
+ routes?: Partial<RoadmapRouteNames>;
118
+ /** Options shared by every roadmap screen. */
119
+ screenOptions?: RoadmapStackScreenOptions;
120
+ /** Roadmap board screen option overrides. */
121
+ boardOptions?: RoadmapStackScreenOptions;
122
+ /** Roadmap item screen option overrides. */
123
+ itemOptions?: RoadmapStackScreenOptions;
124
+ /** Android image sources for toolbar actions. iOS continues to use SF Symbols. */
125
+ androidToolbarIcons?: RoadmapAndroidToolbarIcons;
126
+ }
127
+
94
128
  export interface RoutedFeedbackContextValue {
95
129
  routes: FeedbackRouteNames;
96
130
  colors: FeedbackColorProps;
97
131
  androidToolbarIcons: FeedbackAndroidToolbarIcons;
98
132
  bottomToolbarWrapper?: BottomToolbarWrapper;
99
133
  }
134
+
135
+ export interface RoutedRoadmapContextValue {
136
+ routes: RoadmapRouteNames;
137
+ colors: FeedbackColorProps;
138
+ androidToolbarIcons: RoadmapAndroidToolbarIcons;
139
+ pageSize: number;
140
+ entryPageSize: number;
141
+ onEntryOpen?: RoadmapScreenProps["onEntryOpen"];
142
+ }
@@ -16,6 +16,12 @@ To replace a control with Expo UI, use render-function children on an interactiv
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
18
 
19
- `RoadmapScreen` provides the public roadmap and renders attached entries with
20
- the same `FeedbackEntry` primitives. Pass `onUnauthenticated` to handle auth
21
- when an anonymous visitor votes.
19
+ `RoadmapScreen` provides the public roadmap, groups items by delivery stage,
20
+ and renders attached entries with the existing `EntryCard` and `EntryDetail`
21
+ views. Pass `onUnauthenticated` to handle auth when an anonymous visitor votes
22
+ or comments.
23
+
24
+ The exported `RoadmapBoard` is the reusable admin-style horizontal board
25
+ layout. Supply ordered stages, colors, and a `renderItem` callback to share the
26
+ same column scrolling behavior while customizing cards for an admin or public
27
+ surface.
@@ -5,6 +5,7 @@ export * from "../../shared/metadata.js";
5
5
  export * from "../../shared/theme.js";
6
6
  export type * from "../../shared/types/index.js";
7
7
  export * from "../shared/ui/primitives.js";
8
+ export * from "../shared/ui/RoadmapBoard.js";
8
9
  export * from "../shared/metadata.js";
9
10
  export * from "./FeedbackScreen.js";
10
11
  export * from "../shared/ui/RoadmapScreen.js";