convex-feedback-ui 0.2.0-beta.3 → 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 +3 -1
- 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.map +1 -1
- package/dist/native/expo/RoadmapRoutes.js +22 -5
- package/dist/native/expo/RoadmapRoutes.js.map +1 -1
- package/dist/native/expo/RoadmapScreen.d.ts +1 -1
- package/dist/native/expo/RoadmapScreen.d.ts.map +1 -1
- package/dist/native/expo/RoadmapScreen.js +19 -12
- package/dist/native/expo/RoadmapScreen.js.map +1 -1
- package/dist/native/expo/RoadmapStackLayout.d.ts.map +1 -1
- package/dist/native/expo/RoadmapStackLayout.js +0 -1
- package/dist/native/expo/RoadmapStackLayout.js.map +1 -1
- package/dist/native/expo/index.d.ts +2 -1
- package/dist/native/expo/index.d.ts.map +1 -1
- package/dist/native/expo/index.js +2 -1
- package/dist/native/expo/index.js.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 +16 -1
- package/dist/native/shared/ui/RoadmapScreen.d.ts.map +1 -1
- package/dist/native/shared/ui/RoadmapScreen.js +26 -112
- package/dist/native/shared/ui/RoadmapScreen.js.map +1 -1
- package/dist/react/roadmap.js +3 -3
- package/dist/react/roadmap.js.map +1 -1
- package/dist/react/styles.css +6 -0
- package/dist/shared/messages.d.ts.map +1 -1
- package/dist/shared/messages.js +4 -3
- package/dist/shared/messages.js.map +1 -1
- package/dist/shared/types/messages.d.ts +4 -2
- package/dist/shared/types/messages.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 +6 -2
- package/src/native/expo/RoadmapRoutes.tsx +45 -7
- package/src/native/expo/RoadmapScreen.tsx +93 -17
- package/src/native/expo/RoadmapStackLayout.tsx +0 -1
- package/src/native/expo/index.ts +2 -0
- package/src/native/native/README.md +5 -0
- 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 +94 -243
- package/src/react/roadmap.tsx +1 -6
- package/src/react/styles.css +6 -0
- package/src/shared/messages.ts +4 -3
- package/src/shared/types/messages.ts +5 -2
- package/src/shared/types/screen.ts +2 -0
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import { Stack, useLocalSearchParams, useRouter } from "expo-router";
|
|
2
|
+
import { useRef, useState } from "react";
|
|
2
3
|
import { ActivityIndicator } from "react-native";
|
|
4
|
+
import type { SearchBarCommands } from "react-native-screens";
|
|
3
5
|
import { useSafeAreaInsets } from "react-native-safe-area-context";
|
|
4
6
|
|
|
5
7
|
import { useFeedbackBody } from "../../shared/context/FeedbackBodyProvider.js";
|
|
@@ -20,21 +22,50 @@ export function RoadmapBoardScreen() {
|
|
|
20
22
|
const { messages, theme } = useFeedbackUi();
|
|
21
23
|
const { routes, colors, androidToolbarIcons, pageSize } = useRoutedRoadmap();
|
|
22
24
|
const router = useRouter();
|
|
25
|
+
const [query, setQuery] = useState("");
|
|
26
|
+
const searchRef = useRef<SearchBarCommands>(null);
|
|
23
27
|
|
|
24
28
|
return (
|
|
25
29
|
<>
|
|
30
|
+
<Stack.Screen
|
|
31
|
+
options={{
|
|
32
|
+
headerTitle: messages.roadmap.title,
|
|
33
|
+
headerBackVisible: false,
|
|
34
|
+
}}
|
|
35
|
+
/>
|
|
26
36
|
<FeedbackBoard.Root {...colors}>
|
|
27
37
|
<RoadmapBoardContent
|
|
28
38
|
pageSize={pageSize}
|
|
29
|
-
|
|
39
|
+
query={query}
|
|
40
|
+
onQueryChange={setQuery}
|
|
41
|
+
showHeader={false}
|
|
42
|
+
onItemOpen={(item) => {
|
|
43
|
+
searchRef.current?.blur();
|
|
30
44
|
router.push(
|
|
31
45
|
roadmapRouteHref(routes.item, roadmapRouteParams(item)),
|
|
32
46
|
{ relativeToDirectory: true },
|
|
33
|
-
)
|
|
34
|
-
}
|
|
47
|
+
);
|
|
48
|
+
}}
|
|
35
49
|
/>
|
|
36
50
|
</FeedbackBoard.Root>
|
|
37
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>
|
|
38
69
|
<Stack.Toolbar placement="left">
|
|
39
70
|
<Stack.Toolbar.Button
|
|
40
71
|
hidden={!router.canGoBack()}
|
|
@@ -61,7 +92,7 @@ export function RoadmapItemScreen() {
|
|
|
61
92
|
}>();
|
|
62
93
|
const { hooks } = useFeedbackBody();
|
|
63
94
|
const { messages, theme } = useFeedbackUi();
|
|
64
|
-
const { colors, androidToolbarIcons, entryPageSize, onEntryOpen } =
|
|
95
|
+
const { routes, colors, androidToolbarIcons, entryPageSize, onEntryOpen } =
|
|
65
96
|
useRoutedRoadmap();
|
|
66
97
|
const router = useRouter();
|
|
67
98
|
const insets = useSafeAreaInsets();
|
|
@@ -84,7 +115,9 @@ export function RoadmapItemScreen() {
|
|
|
84
115
|
<>
|
|
85
116
|
{item ? (
|
|
86
117
|
<>
|
|
87
|
-
<Stack.Screen
|
|
118
|
+
<Stack.Screen
|
|
119
|
+
options={{ headerTitle: item.title, headerBackVisible: false }}
|
|
120
|
+
/>
|
|
88
121
|
<FeedbackBoard.Root {...colors}>
|
|
89
122
|
<FeedbackBoard.List
|
|
90
123
|
style={{
|
|
@@ -115,14 +148,19 @@ export function RoadmapItemScreen() {
|
|
|
115
148
|
|
|
116
149
|
<Stack.Toolbar placement="left">
|
|
117
150
|
<Stack.Toolbar.Button
|
|
118
|
-
hidden={!router.canGoBack()}
|
|
119
151
|
icon={
|
|
120
152
|
process.env.EXPO_OS === "ios"
|
|
121
153
|
? "chevron.backward"
|
|
122
154
|
: androidToolbarIcons.back
|
|
123
155
|
}
|
|
124
156
|
accessibilityLabel={messages.entry.back}
|
|
125
|
-
onPress={() =>
|
|
157
|
+
onPress={() => {
|
|
158
|
+
if (router.canGoBack()) {
|
|
159
|
+
router.back();
|
|
160
|
+
} else {
|
|
161
|
+
router.replace(roadmapRouteHref(routes.board));
|
|
162
|
+
}
|
|
163
|
+
}}
|
|
126
164
|
tintColor={theme.colors.text}
|
|
127
165
|
>
|
|
128
166
|
{messages.entry.back}
|
|
@@ -1,9 +1,16 @@
|
|
|
1
1
|
import { Stack, useRouter } from "expo-router";
|
|
2
|
+
import { useRef, useState } from "react";
|
|
3
|
+
import type { SearchBarCommands } from "react-native-screens";
|
|
2
4
|
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
5
|
+
import { useFeedbackUi } from "../../shared/context/FeedbackProvider.js";
|
|
6
|
+
import type { mergeFeedbackMessages } from "../../shared/messages.js";
|
|
7
|
+
import type { mergeFeedbackTheme } from "../../shared/theme.js";
|
|
6
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";
|
|
7
14
|
import type {
|
|
8
15
|
RoadmapAndroidToolbarIcons,
|
|
9
16
|
RoadmapStackScreenOptions,
|
|
@@ -32,16 +39,52 @@ export type ExpoRoadmapScreenProps = RoadmapScreenProps &
|
|
|
32
39
|
* `RoadmapStackLayout` when the board and item detail should be separate
|
|
33
40
|
* discovered Expo Router pages.
|
|
34
41
|
*/
|
|
35
|
-
export function RoadmapScreen({
|
|
36
|
-
useStack
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
...props
|
|
40
|
-
}: ExpoRoadmapScreenProps) {
|
|
41
|
-
if (!useStack) return <NativeRoadmapScreen {...props} />;
|
|
42
|
+
export function RoadmapScreen(props: ExpoRoadmapScreenProps) {
|
|
43
|
+
if (props.useStack === false) {
|
|
44
|
+
return <NativeRoadmapScreen {...props} />;
|
|
45
|
+
}
|
|
42
46
|
|
|
43
|
-
|
|
44
|
-
|
|
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);
|
|
45
88
|
|
|
46
89
|
return (
|
|
47
90
|
<>
|
|
@@ -49,23 +92,56 @@ export function RoadmapScreen({
|
|
|
49
92
|
options={{
|
|
50
93
|
headerTitle: messages.roadmap.title,
|
|
51
94
|
headerShown: true,
|
|
52
|
-
headerTransparent:
|
|
95
|
+
headerTransparent: false,
|
|
53
96
|
headerShadowVisible: true,
|
|
54
97
|
headerBackButtonDisplayMode: "minimal",
|
|
55
98
|
headerBackVisible: false,
|
|
56
|
-
headerTintColor:
|
|
99
|
+
headerTintColor: textColor ?? theme.colors.text,
|
|
57
100
|
contentStyle: {
|
|
58
|
-
backgroundColor:
|
|
101
|
+
backgroundColor: backgroundColor ?? theme.colors.background,
|
|
59
102
|
},
|
|
60
|
-
...StackOptions,
|
|
103
|
+
...props.StackOptions,
|
|
61
104
|
}}
|
|
62
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>
|
|
63
123
|
<RoadmapToolbar
|
|
64
124
|
androidToolbarIcons={androidToolbarIcons}
|
|
65
125
|
messages={messages}
|
|
66
126
|
theme={theme}
|
|
67
127
|
/>
|
|
68
|
-
<
|
|
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
|
+
/>
|
|
69
145
|
</>
|
|
70
146
|
);
|
|
71
147
|
}
|
|
@@ -54,7 +54,6 @@ export function RoadmapStackLayout({
|
|
|
54
54
|
};
|
|
55
55
|
const defaults: NativeStackNavigationOptions = {
|
|
56
56
|
headerShown: true,
|
|
57
|
-
headerTransparent: true,
|
|
58
57
|
headerShadowVisible: true,
|
|
59
58
|
headerBackButtonDisplayMode: "minimal",
|
|
60
59
|
headerTintColor: textColor ?? resolvedTheme.colors.text,
|
package/src/native/expo/index.ts
CHANGED
|
@@ -5,10 +5,12 @@ 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 {
|
|
9
10
|
RoadmapBoardContent,
|
|
10
11
|
RoadmapItemContent,
|
|
11
12
|
RoadmapProvider,
|
|
13
|
+
RoadmapScreenContent,
|
|
12
14
|
} from "../shared/ui/RoadmapScreen.js";
|
|
13
15
|
export * from "./metadata.js";
|
|
14
16
|
export * from "./FeedbackScreen.js";
|
|
@@ -20,3 +20,8 @@ Containers such as `FeedbackEntry.Content`, `FeedbackForm.Root`, and `Comment.Ch
|
|
|
20
20
|
and renders attached entries with the existing `EntryCard` and `EntryDetail`
|
|
21
21
|
views. Pass `onUnauthenticated` to handle auth when an anonymous visitor votes
|
|
22
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";
|
|
@@ -10,6 +10,7 @@ export function EntryCard({
|
|
|
10
10
|
entry,
|
|
11
11
|
hooks,
|
|
12
12
|
onOpen,
|
|
13
|
+
titleNumberOfLines,
|
|
13
14
|
}: FeedbackScreenEntryCardProps) {
|
|
14
15
|
const { isAuthenticated, onUnauthenticated } = useFeedbackBody();
|
|
15
16
|
const setUpvote = hooks.useSetEntryUpvote();
|
|
@@ -46,7 +47,7 @@ export function EntryCard({
|
|
|
46
47
|
<FeedbackEntry.Kind />
|
|
47
48
|
<FeedbackEntry.Status />
|
|
48
49
|
</View>
|
|
49
|
-
<FeedbackEntry.Title />
|
|
50
|
+
<FeedbackEntry.Title numberOfLines={titleNumberOfLines} />
|
|
50
51
|
<FeedbackEntry.Body numberOfLines={3} />
|
|
51
52
|
<FeedbackEntry.CommentCount />
|
|
52
53
|
</Pressable>
|
|
@@ -0,0 +1,225 @@
|
|
|
1
|
+
import type { RoadmapItem, RoadmapStatus } from "convex-feedback";
|
|
2
|
+
import type { ReactElement, ReactNode } from "react";
|
|
3
|
+
import {
|
|
4
|
+
ActivityIndicator,
|
|
5
|
+
FlatList,
|
|
6
|
+
Pressable,
|
|
7
|
+
ScrollView,
|
|
8
|
+
StyleSheet,
|
|
9
|
+
Text,
|
|
10
|
+
View,
|
|
11
|
+
type StyleProp,
|
|
12
|
+
type ViewProps,
|
|
13
|
+
type ViewStyle,
|
|
14
|
+
} from "react-native";
|
|
15
|
+
import { useSafeAreaInsets } from "react-native-safe-area-context";
|
|
16
|
+
|
|
17
|
+
export interface RoadmapBoardStage {
|
|
18
|
+
value: RoadmapStatus;
|
|
19
|
+
label: string;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export interface RoadmapBoardColors {
|
|
23
|
+
background: string;
|
|
24
|
+
surface: string;
|
|
25
|
+
text: string;
|
|
26
|
+
muted: string;
|
|
27
|
+
border: string;
|
|
28
|
+
primary: string;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export interface RoadmapBoardItemRenderArgs {
|
|
32
|
+
item: RoadmapItem;
|
|
33
|
+
stage: RoadmapBoardStage;
|
|
34
|
+
stageIndex: number;
|
|
35
|
+
onOpen: () => void;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export interface RoadmapBoardProps {
|
|
39
|
+
/** Items are grouped into columns using their current status. */
|
|
40
|
+
items: readonly RoadmapItem[];
|
|
41
|
+
/** Ordered columns rendered from left to right. */
|
|
42
|
+
stages: readonly RoadmapBoardStage[];
|
|
43
|
+
/** Colors used by the shared board frame and its loading/empty states. */
|
|
44
|
+
colors: RoadmapBoardColors;
|
|
45
|
+
/** Shows the same full-board loading state as the admin roadmap. */
|
|
46
|
+
loading?: boolean;
|
|
47
|
+
/** Copy shown when a column does not contain any items. */
|
|
48
|
+
emptyLabel: string;
|
|
49
|
+
/** Opens a roadmap item from any rendered card. */
|
|
50
|
+
onItemOpen: (item: RoadmapItem) => void;
|
|
51
|
+
/** Supplies the card UI so admin and user-facing cards can differ. */
|
|
52
|
+
renderItem: (args: RoadmapBoardItemRenderArgs) => ReactElement | null;
|
|
53
|
+
style?: StyleProp<ViewStyle>;
|
|
54
|
+
contentContainerStyle?: StyleProp<ViewStyle>;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export interface RoadmapBoardCardProps extends Pick<
|
|
58
|
+
ViewProps,
|
|
59
|
+
"accessibilityLabel"
|
|
60
|
+
> {
|
|
61
|
+
colors: Pick<RoadmapBoardColors, "border" | "surface">;
|
|
62
|
+
onPress: () => void;
|
|
63
|
+
children: ReactNode;
|
|
64
|
+
style?: StyleProp<ViewStyle>;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/** Shared admin-style pressable card shell with configurable contents. */
|
|
68
|
+
export function RoadmapBoardCard({
|
|
69
|
+
colors,
|
|
70
|
+
onPress,
|
|
71
|
+
accessibilityLabel,
|
|
72
|
+
children,
|
|
73
|
+
style,
|
|
74
|
+
}: RoadmapBoardCardProps) {
|
|
75
|
+
return (
|
|
76
|
+
<Pressable
|
|
77
|
+
accessibilityRole="button"
|
|
78
|
+
accessibilityLabel={accessibilityLabel}
|
|
79
|
+
onPress={onPress}
|
|
80
|
+
style={({ pressed }) => [
|
|
81
|
+
styles.card,
|
|
82
|
+
{
|
|
83
|
+
borderColor: colors.border,
|
|
84
|
+
backgroundColor: colors.surface,
|
|
85
|
+
},
|
|
86
|
+
style,
|
|
87
|
+
pressed && styles.pressed,
|
|
88
|
+
]}
|
|
89
|
+
>
|
|
90
|
+
{children}
|
|
91
|
+
</Pressable>
|
|
92
|
+
);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Shared stage-board layout used by the public roadmap and admin app.
|
|
97
|
+
*
|
|
98
|
+
* The outer list scrolls horizontally while each stage owns its vertical
|
|
99
|
+
* `FlatList`, matching the admin board's bounded nested-scroll structure.
|
|
100
|
+
*/
|
|
101
|
+
export function RoadmapBoard({
|
|
102
|
+
items,
|
|
103
|
+
stages,
|
|
104
|
+
colors,
|
|
105
|
+
loading = false,
|
|
106
|
+
emptyLabel,
|
|
107
|
+
onItemOpen,
|
|
108
|
+
renderItem,
|
|
109
|
+
style,
|
|
110
|
+
contentContainerStyle,
|
|
111
|
+
}: RoadmapBoardProps) {
|
|
112
|
+
const insets = useSafeAreaInsets();
|
|
113
|
+
|
|
114
|
+
if (loading) {
|
|
115
|
+
return (
|
|
116
|
+
<ActivityIndicator
|
|
117
|
+
style={[styles.loader, { backgroundColor: colors.background }, style]}
|
|
118
|
+
color={colors.primary}
|
|
119
|
+
/>
|
|
120
|
+
);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
return (
|
|
124
|
+
<ScrollView
|
|
125
|
+
horizontal
|
|
126
|
+
style={[
|
|
127
|
+
styles.boardScroll,
|
|
128
|
+
{ backgroundColor: colors.background },
|
|
129
|
+
style,
|
|
130
|
+
]}
|
|
131
|
+
contentInsetAdjustmentBehavior="automatic"
|
|
132
|
+
showsHorizontalScrollIndicator={false}
|
|
133
|
+
contentContainerStyle={[
|
|
134
|
+
styles.board,
|
|
135
|
+
{
|
|
136
|
+
paddingLeft: 12 + insets.left,
|
|
137
|
+
paddingRight: 12 + insets.right,
|
|
138
|
+
paddingBottom: 24 + insets.bottom,
|
|
139
|
+
},
|
|
140
|
+
contentContainerStyle,
|
|
141
|
+
]}
|
|
142
|
+
>
|
|
143
|
+
{stages.map((stage, stageIndex) => {
|
|
144
|
+
const stageItems = items
|
|
145
|
+
.filter((item) => item.status === stage.value)
|
|
146
|
+
.sort((a, b) => a.position - b.position);
|
|
147
|
+
|
|
148
|
+
return (
|
|
149
|
+
<View
|
|
150
|
+
key={stage.value}
|
|
151
|
+
style={[
|
|
152
|
+
styles.column,
|
|
153
|
+
{
|
|
154
|
+
borderColor: colors.border,
|
|
155
|
+
backgroundColor: colors.surface,
|
|
156
|
+
},
|
|
157
|
+
]}
|
|
158
|
+
>
|
|
159
|
+
<View style={[styles.columnHeader, { borderColor: colors.border }]}>
|
|
160
|
+
<Text style={[styles.columnTitle, { color: colors.text }]}>
|
|
161
|
+
{stage.label}
|
|
162
|
+
</Text>
|
|
163
|
+
<Text style={[styles.count, { color: colors.muted }]}>
|
|
164
|
+
{stageItems.length}
|
|
165
|
+
</Text>
|
|
166
|
+
</View>
|
|
167
|
+
<FlatList
|
|
168
|
+
data={stageItems}
|
|
169
|
+
nestedScrollEnabled
|
|
170
|
+
contentInsetAdjustmentBehavior="never"
|
|
171
|
+
keyExtractor={(item) => item.id}
|
|
172
|
+
contentContainerStyle={[
|
|
173
|
+
styles.cards,
|
|
174
|
+
{ paddingBottom: 16 + insets.bottom },
|
|
175
|
+
]}
|
|
176
|
+
ListEmptyComponent={
|
|
177
|
+
<Text style={[styles.empty, { color: colors.muted }]}>
|
|
178
|
+
{emptyLabel}
|
|
179
|
+
</Text>
|
|
180
|
+
}
|
|
181
|
+
renderItem={({ item }) =>
|
|
182
|
+
renderItem({
|
|
183
|
+
item,
|
|
184
|
+
stage,
|
|
185
|
+
stageIndex,
|
|
186
|
+
onOpen: () => onItemOpen(item),
|
|
187
|
+
})
|
|
188
|
+
}
|
|
189
|
+
/>
|
|
190
|
+
</View>
|
|
191
|
+
);
|
|
192
|
+
})}
|
|
193
|
+
</ScrollView>
|
|
194
|
+
);
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
const styles = StyleSheet.create({
|
|
198
|
+
boardScroll: { flex: 1 },
|
|
199
|
+
loader: { flex: 1 },
|
|
200
|
+
board: { gap: 12, paddingTop: 12 },
|
|
201
|
+
column: {
|
|
202
|
+
width: 310,
|
|
203
|
+
borderWidth: 1,
|
|
204
|
+
borderRadius: 16,
|
|
205
|
+
overflow: "hidden",
|
|
206
|
+
},
|
|
207
|
+
columnHeader: {
|
|
208
|
+
flexDirection: "row",
|
|
209
|
+
alignItems: "center",
|
|
210
|
+
justifyContent: "space-between",
|
|
211
|
+
borderBottomWidth: 1,
|
|
212
|
+
padding: 14,
|
|
213
|
+
},
|
|
214
|
+
columnTitle: { fontSize: 14, fontWeight: "700" },
|
|
215
|
+
count: { fontSize: 12 },
|
|
216
|
+
cards: { gap: 10, padding: 10 },
|
|
217
|
+
empty: { padding: 18, textAlign: "center" },
|
|
218
|
+
card: {
|
|
219
|
+
gap: 8,
|
|
220
|
+
borderWidth: 1,
|
|
221
|
+
borderRadius: 12,
|
|
222
|
+
padding: 13,
|
|
223
|
+
},
|
|
224
|
+
pressed: { opacity: 0.7 },
|
|
225
|
+
});
|