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.
- package/README.md +40 -0
- package/dist/.tsbuildinfo +1 -1
- package/dist/native/expo/FeedbackRoutes.d.ts.map +1 -1
- package/dist/native/expo/FeedbackRoutes.js +15 -2
- package/dist/native/expo/FeedbackRoutes.js.map +1 -1
- package/dist/native/expo/RoadmapRoutes.d.ts +3 -0
- package/dist/native/expo/RoadmapRoutes.d.ts.map +1 -0
- package/dist/native/expo/RoadmapRoutes.js +61 -0
- package/dist/native/expo/RoadmapRoutes.js.map +1 -0
- package/dist/native/expo/RoadmapScreen.d.ts +24 -0
- package/dist/native/expo/RoadmapScreen.d.ts.map +1 -0
- package/dist/native/expo/RoadmapScreen.js +43 -0
- package/dist/native/expo/RoadmapScreen.js.map +1 -0
- package/dist/native/expo/RoadmapStackLayout.d.ts +8 -0
- package/dist/native/expo/RoadmapStackLayout.d.ts.map +1 -0
- package/dist/native/expo/RoadmapStackLayout.js +55 -0
- package/dist/native/expo/RoadmapStackLayout.js.map +1 -0
- package/dist/native/expo/RoutedRoadmapContext.d.ts +4 -0
- package/dist/native/expo/RoutedRoadmapContext.d.ts.map +1 -0
- package/dist/native/expo/RoutedRoadmapContext.js +11 -0
- package/dist/native/expo/RoutedRoadmapContext.js.map +1 -0
- package/dist/native/expo/index.d.ts +5 -1
- package/dist/native/expo/index.d.ts.map +1 -1
- package/dist/native/expo/index.js +5 -1
- package/dist/native/expo/index.js.map +1 -1
- package/dist/native/expo/routes.d.ts +17 -1
- package/dist/native/expo/routes.d.ts.map +1 -1
- package/dist/native/expo/routes.js +46 -0
- package/dist/native/expo/routes.js.map +1 -1
- package/dist/native/expo/types.d.ts +35 -1
- package/dist/native/expo/types.d.ts.map +1 -1
- package/dist/native/native/index.d.ts +1 -0
- package/dist/native/native/index.d.ts.map +1 -1
- package/dist/native/native/index.js +1 -0
- package/dist/native/native/index.js.map +1 -1
- package/dist/native/shared/ui/EntryCard.d.ts +1 -1
- package/dist/native/shared/ui/EntryCard.d.ts.map +1 -1
- package/dist/native/shared/ui/EntryCard.js +2 -2
- package/dist/native/shared/ui/EntryCard.js.map +1 -1
- package/dist/native/shared/ui/RoadmapBoard.d.ts +55 -0
- package/dist/native/shared/ui/RoadmapBoard.d.ts.map +1 -0
- package/dist/native/shared/ui/RoadmapBoard.js +89 -0
- package/dist/native/shared/ui/RoadmapBoard.js.map +1 -0
- package/dist/native/shared/ui/RoadmapScreen.d.ts +36 -1
- package/dist/native/shared/ui/RoadmapScreen.d.ts.map +1 -1
- package/dist/native/shared/ui/RoadmapScreen.js +143 -51
- package/dist/native/shared/ui/RoadmapScreen.js.map +1 -1
- package/dist/native/shared/ui/primitives.d.ts +1 -1
- package/dist/native/shared/ui/primitives.d.ts.map +1 -1
- package/dist/native/shared/ui/primitives.js +3 -1
- package/dist/native/shared/ui/primitives.js.map +1 -1
- package/dist/react/roadmap.d.ts.map +1 -1
- package/dist/react/roadmap.js +25 -13
- package/dist/react/roadmap.js.map +1 -1
- package/dist/react/styles.css +129 -0
- package/dist/shared/messages.d.ts.map +1 -1
- package/dist/shared/messages.js +24 -0
- package/dist/shared/messages.js.map +1 -1
- package/dist/shared/types/messages.d.ts +28 -1
- package/dist/shared/types/messages.d.ts.map +1 -1
- package/dist/shared/types/roadmap.d.ts +10 -1
- package/dist/shared/types/roadmap.d.ts.map +1 -1
- package/dist/shared/types/screen.d.ts +2 -0
- package/dist/shared/types/screen.d.ts.map +1 -1
- package/package.json +2 -2
- package/src/native/expo/FeedbackRoutes.tsx +30 -7
- package/src/native/expo/README.md +10 -2
- package/src/native/expo/RoadmapRoutes.tsx +171 -0
- package/src/native/expo/RoadmapScreen.tsx +177 -0
- package/src/native/expo/RoadmapStackLayout.tsx +110 -0
- package/src/native/expo/RoutedRoadmapContext.tsx +21 -0
- package/src/native/expo/index.ts +10 -1
- package/src/native/expo/routes.ts +69 -1
- package/src/native/expo/types.ts +43 -0
- package/src/native/native/README.md +9 -3
- package/src/native/native/index.ts +1 -0
- package/src/native/shared/ui/EntryCard.tsx +2 -1
- package/src/native/shared/ui/RoadmapBoard.tsx +225 -0
- package/src/native/shared/ui/RoadmapScreen.tsx +415 -242
- package/src/native/shared/ui/primitives.tsx +3 -0
- package/src/react/roadmap.tsx +140 -85
- package/src/react/styles.css +129 -0
- package/src/shared/messages.ts +25 -0
- package/src/shared/types/messages.ts +41 -1
- package/src/shared/types/roadmap.ts +18 -1
- 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.
|
|
56
|
-
|
|
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
|
+
}
|
package/src/native/expo/index.ts
CHANGED
|
@@ -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/
|
|
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 {
|
|
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
|
+
}
|
package/src/native/expo/types.ts
CHANGED
|
@@ -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
|
|
20
|
-
the
|
|
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";
|