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.
- package/README.md +143 -0
- package/dist/.tsbuildinfo +1 -1
- package/dist/native/expo/ExpoFeedbackBody.d.ts +1 -1
- package/dist/native/expo/ExpoFeedbackBody.d.ts.map +1 -1
- package/dist/native/expo/ExpoFeedbackBody.js +2 -2
- package/dist/native/expo/ExpoFeedbackBody.js.map +1 -1
- package/dist/native/expo/FeedbackCreateStackLayout.d.ts +3 -0
- package/dist/native/expo/FeedbackCreateStackLayout.d.ts.map +1 -0
- package/dist/native/expo/FeedbackCreateStackLayout.js +35 -0
- package/dist/native/expo/FeedbackCreateStackLayout.js.map +1 -0
- package/dist/native/expo/FeedbackRoutes.d.ts +4 -0
- package/dist/native/expo/FeedbackRoutes.d.ts.map +1 -0
- package/dist/native/expo/FeedbackRoutes.js +81 -0
- package/dist/native/expo/FeedbackRoutes.js.map +1 -0
- package/dist/native/expo/FeedbackScreen.d.ts +1 -1
- package/dist/native/expo/FeedbackScreen.d.ts.map +1 -1
- package/dist/native/expo/FeedbackScreen.js +2 -2
- package/dist/native/expo/FeedbackScreen.js.map +1 -1
- package/dist/native/expo/FeedbackStack.d.ts +1 -1
- package/dist/native/expo/FeedbackStack.d.ts.map +1 -1
- package/dist/native/expo/FeedbackStack.js +10 -5
- package/dist/native/expo/FeedbackStack.js.map +1 -1
- package/dist/native/expo/FeedbackStackLayout.d.ts +3 -0
- package/dist/native/expo/FeedbackStackLayout.d.ts.map +1 -0
- package/dist/native/expo/FeedbackStackLayout.js +54 -0
- package/dist/native/expo/FeedbackStackLayout.js.map +1 -0
- package/dist/native/expo/RoutedFeedbackContext.d.ts +4 -0
- package/dist/native/expo/RoutedFeedbackContext.d.ts.map +1 -0
- package/dist/native/expo/RoutedFeedbackContext.js +11 -0
- package/dist/native/expo/RoutedFeedbackContext.js.map +1 -0
- package/dist/native/expo/RoutedFeedbackModalContext.d.ts +7 -0
- package/dist/native/expo/RoutedFeedbackModalContext.d.ts.map +1 -0
- package/dist/native/expo/RoutedFeedbackModalContext.js +7 -0
- package/dist/native/expo/RoutedFeedbackModalContext.js.map +1 -0
- package/dist/native/expo/index.d.ts +4 -0
- package/dist/native/expo/index.d.ts.map +1 -1
- package/dist/native/expo/index.js +4 -0
- package/dist/native/expo/index.js.map +1 -1
- package/dist/native/expo/routes.d.ts +16 -0
- package/dist/native/expo/routes.d.ts.map +1 -0
- package/dist/native/expo/routes.js +23 -0
- package/dist/native/expo/routes.js.map +1 -0
- package/dist/native/expo/types.d.ts +53 -1
- package/dist/native/expo/types.d.ts.map +1 -1
- package/dist/native/native/FeedbackScreen.d.ts +1 -1
- package/dist/native/native/FeedbackScreen.d.ts.map +1 -1
- package/dist/native/native/FeedbackScreen.js +2 -2
- package/dist/native/native/FeedbackScreen.js.map +1 -1
- package/dist/native/shared/ui/BodyContent.d.ts +1 -1
- package/dist/native/shared/ui/BodyContent.d.ts.map +1 -1
- package/dist/native/shared/ui/BodyContent.js +2 -2
- package/dist/native/shared/ui/BodyContent.js.map +1 -1
- package/dist/native/shared/ui/Button.d.ts +2 -1
- package/dist/native/shared/ui/Button.d.ts.map +1 -1
- package/dist/native/shared/ui/Button.js +9 -4
- package/dist/native/shared/ui/Button.js.map +1 -1
- package/dist/native/shared/ui/FeedbackScreenHeader.d.ts.map +1 -1
- package/dist/native/shared/ui/FeedbackScreenHeader.js +8 -4
- package/dist/native/shared/ui/FeedbackScreenHeader.js.map +1 -1
- package/dist/native/shared/ui/FeedbackScreenList.d.ts +1 -1
- package/dist/native/shared/ui/FeedbackScreenList.d.ts.map +1 -1
- package/dist/native/shared/ui/FeedbackScreenList.js +18 -5
- package/dist/native/shared/ui/FeedbackScreenList.js.map +1 -1
- package/dist/native/shared/ui/NewEntry.d.ts +10 -0
- package/dist/native/shared/ui/NewEntry.d.ts.map +1 -1
- package/dist/native/shared/ui/NewEntry.js +7 -14
- package/dist/native/shared/ui/NewEntry.js.map +1 -1
- package/dist/native/shared/ui/primitives.d.ts +7 -1
- package/dist/native/shared/ui/primitives.d.ts.map +1 -1
- package/dist/native/shared/ui/primitives.js +38 -0
- package/dist/native/shared/ui/primitives.js.map +1 -1
- package/dist/react/primitives.d.ts +9 -1
- package/dist/react/primitives.d.ts.map +1 -1
- package/dist/react/primitives.js +12 -0
- package/dist/react/primitives.js.map +1 -1
- package/dist/react/screen.d.ts +1 -1
- package/dist/react/screen.d.ts.map +1 -1
- package/dist/react/screen.js +21 -10
- package/dist/react/screen.js.map +1 -1
- package/dist/react/styles.css +38 -1
- package/dist/shared/context/FeedbackBodyProvider.d.ts +1 -1
- package/dist/shared/context/FeedbackBodyProvider.d.ts.map +1 -1
- package/dist/shared/context/FeedbackBodyProvider.js +5 -1
- package/dist/shared/context/FeedbackBodyProvider.js.map +1 -1
- package/dist/shared/helpers.d.ts +5 -1
- package/dist/shared/helpers.d.ts.map +1 -1
- package/dist/shared/helpers.js +11 -0
- package/dist/shared/helpers.js.map +1 -1
- package/dist/shared/messages.d.ts.map +1 -1
- package/dist/shared/messages.js +1 -0
- package/dist/shared/messages.js.map +1 -1
- package/dist/shared/types/choice.d.ts +10 -0
- package/dist/shared/types/choice.d.ts.map +1 -0
- package/dist/shared/types/choice.js +2 -0
- package/dist/shared/types/choice.js.map +1 -0
- package/dist/shared/types/index.d.ts +1 -0
- package/dist/shared/types/index.d.ts.map +1 -1
- package/dist/shared/types/messages.d.ts +2 -0
- package/dist/shared/types/messages.d.ts.map +1 -1
- package/dist/shared/types/screen.d.ts +13 -2
- package/dist/shared/types/screen.d.ts.map +1 -1
- package/package.json +12 -8
- package/src/native/expo/ExpoFeedbackBody.tsx +6 -1
- package/src/native/expo/FeedbackCreateStackLayout.tsx +62 -0
- package/src/native/expo/FeedbackRoutes.tsx +238 -0
- package/src/native/expo/FeedbackScreen.tsx +2 -0
- package/src/native/expo/FeedbackStack.tsx +40 -5
- package/src/native/expo/FeedbackStackLayout.tsx +118 -0
- package/src/native/expo/README.md +24 -0
- package/src/native/expo/RoutedFeedbackContext.tsx +20 -0
- package/src/native/expo/RoutedFeedbackModalContext.tsx +14 -0
- package/src/native/expo/index.ts +4 -0
- package/src/native/expo/routes.ts +41 -0
- package/src/native/expo/types.ts +65 -1
- package/src/native/native/FeedbackScreen.tsx +2 -0
- package/src/native/shared/ui/BodyContent.tsx +2 -0
- package/src/native/shared/ui/Button.tsx +12 -3
- package/src/native/shared/ui/FeedbackScreenHeader.tsx +25 -4
- package/src/native/shared/ui/FeedbackScreenList.tsx +48 -8
- package/src/native/shared/ui/NewEntry.tsx +12 -30
- package/src/native/shared/ui/primitives.tsx +90 -0
- package/src/react/primitives.tsx +55 -0
- package/src/react/screen.tsx +67 -33
- package/src/react/styles.css +38 -1
- package/src/shared/context/FeedbackBodyProvider.tsx +6 -0
- package/src/shared/helpers.ts +21 -1
- package/src/shared/messages.ts +1 -0
- package/src/shared/types/choice.ts +10 -0
- package/src/shared/types/index.ts +1 -0
- package/src/shared/types/messages.ts +3 -0
- package/src/shared/types/screen.ts +15 -1
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import type { Href } from "expo-router";
|
|
2
|
+
import type { FeedbackRouteNames } from "./types.js";
|
|
3
|
+
|
|
4
|
+
export const defaultFeedbackRoutes: FeedbackRouteNames = {
|
|
5
|
+
board: "index",
|
|
6
|
+
entry: "[entryId]",
|
|
7
|
+
create: "new",
|
|
8
|
+
};
|
|
9
|
+
|
|
10
|
+
export function resolveFeedbackRoutes(
|
|
11
|
+
routes: Partial<FeedbackRouteNames> = {},
|
|
12
|
+
): FeedbackRouteNames {
|
|
13
|
+
const resolved = { ...defaultFeedbackRoutes, ...routes };
|
|
14
|
+
|
|
15
|
+
if (!resolved.entry.includes("[entryId]")) {
|
|
16
|
+
throw new Error(
|
|
17
|
+
'The routed feedback entry route must contain the "[entryId]" dynamic segment.',
|
|
18
|
+
);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
return resolved;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function createFeedbackStackSettings(
|
|
25
|
+
routes?: Partial<FeedbackRouteNames>,
|
|
26
|
+
) {
|
|
27
|
+
return { anchor: resolveFeedbackRoutes(routes).board };
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export const feedbackStackSettings = createFeedbackStackSettings();
|
|
31
|
+
|
|
32
|
+
/** Anchor the create form beneath suggested-entry detail screens. */
|
|
33
|
+
export const feedbackCreateStackSettings = { anchor: "index" };
|
|
34
|
+
|
|
35
|
+
export function feedbackRouteHref(
|
|
36
|
+
route: string,
|
|
37
|
+
params?: Record<string, string>,
|
|
38
|
+
): Href {
|
|
39
|
+
const pathname = route.startsWith(".") ? route : `./${route}`;
|
|
40
|
+
return params === undefined ? pathname : { pathname, params };
|
|
41
|
+
}
|
package/src/native/expo/types.ts
CHANGED
|
@@ -1,14 +1,21 @@
|
|
|
1
1
|
import type { StackScreenProps } from "expo-router";
|
|
2
|
+
import type { ImageSourcePropType } from "react-native";
|
|
2
3
|
import type { SearchBarCommands } from "react-native-screens";
|
|
3
|
-
import type {
|
|
4
|
+
import type {
|
|
5
|
+
FeedbackColorProps,
|
|
6
|
+
FeedbackScreenRootProps,
|
|
7
|
+
} from "../../shared/types";
|
|
4
8
|
|
|
5
9
|
interface FeedbackScreenWithStack {
|
|
6
10
|
useStack?: true;
|
|
7
11
|
StackOptions?: StackScreenProps["options"];
|
|
12
|
+
/** Android image sources for toolbar actions. iOS continues to use SF Symbols. */
|
|
13
|
+
androidToolbarIcons?: FeedbackAndroidToolbarIcons;
|
|
8
14
|
}
|
|
9
15
|
interface FeedbackScreenWithoutStack {
|
|
10
16
|
useStack?: false;
|
|
11
17
|
StackOptions?: never;
|
|
18
|
+
androidToolbarIcons?: never;
|
|
12
19
|
}
|
|
13
20
|
export type FeedbackScreenStackProps =
|
|
14
21
|
FeedbackScreenWithStack | FeedbackScreenWithoutStack;
|
|
@@ -19,5 +26,62 @@ export type ExpoFeedbackScreenProps = FeedbackScreenStackProps &
|
|
|
19
26
|
export interface FeedbackStackProps {
|
|
20
27
|
searchRef: React.RefObject<SearchBarCommands | null>;
|
|
21
28
|
stackOptions?: StackScreenProps["options"];
|
|
29
|
+
androidToolbarIcons?: FeedbackAndroidToolbarIcons;
|
|
22
30
|
children: React.ReactNode;
|
|
23
31
|
}
|
|
32
|
+
|
|
33
|
+
/** File names used by the routed feedback stack. */
|
|
34
|
+
export interface FeedbackRouteNames {
|
|
35
|
+
/** Board route. @default "index" */
|
|
36
|
+
board: string;
|
|
37
|
+
/** Entry route. Must contain `[entryId]`. @default "[entryId]" */
|
|
38
|
+
entry: string;
|
|
39
|
+
/** Create-entry modal route. @default "new" */
|
|
40
|
+
create: string;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export type FeedbackStackScreenOptions = Exclude<
|
|
44
|
+
NonNullable<StackScreenProps["options"]>,
|
|
45
|
+
(...args: never[]) => unknown
|
|
46
|
+
>;
|
|
47
|
+
|
|
48
|
+
export interface FeedbackAndroidToolbarIcons {
|
|
49
|
+
/** Android image source for the create-entry action. */
|
|
50
|
+
create?: ImageSourcePropType;
|
|
51
|
+
/** Android image source for the back action. */
|
|
52
|
+
back?: ImageSourcePropType;
|
|
53
|
+
/** Android image source for modal close actions. */
|
|
54
|
+
close?: ImageSourcePropType;
|
|
55
|
+
/** Android image source for the open/closed status filter. */
|
|
56
|
+
filter?: ImageSourcePropType;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export interface FeedbackStackLayoutProps extends FeedbackScreenRootProps {
|
|
60
|
+
/** Partial overrides for the prescribed route file names. */
|
|
61
|
+
routes?: Partial<FeedbackRouteNames>;
|
|
62
|
+
/** Options shared by every feedback screen. */
|
|
63
|
+
screenOptions?: FeedbackStackScreenOptions;
|
|
64
|
+
/** Board screen option overrides. */
|
|
65
|
+
boardOptions?: FeedbackStackScreenOptions;
|
|
66
|
+
/** Entry-detail screen option overrides. */
|
|
67
|
+
entryOptions?: FeedbackStackScreenOptions;
|
|
68
|
+
/** Create-entry modal option overrides. */
|
|
69
|
+
createOptions?: FeedbackStackScreenOptions;
|
|
70
|
+
/** Android image sources for toolbar actions. iOS continues to use SF Symbols. */
|
|
71
|
+
androidToolbarIcons?: FeedbackAndroidToolbarIcons;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export interface FeedbackCreateStackLayoutProps {
|
|
75
|
+
/** Options shared by the create form and suggested-entry screens. */
|
|
76
|
+
screenOptions?: FeedbackStackScreenOptions;
|
|
77
|
+
/** Create-form screen option overrides. */
|
|
78
|
+
createOptions?: FeedbackStackScreenOptions;
|
|
79
|
+
/** Suggested-entry screen option overrides. */
|
|
80
|
+
entryOptions?: FeedbackStackScreenOptions;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
export interface RoutedFeedbackContextValue {
|
|
84
|
+
routes: FeedbackRouteNames;
|
|
85
|
+
colors: FeedbackColorProps;
|
|
86
|
+
androidToolbarIcons: FeedbackAndroidToolbarIcons;
|
|
87
|
+
}
|
|
@@ -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={collectNativeMetadata}
|
|
34
36
|
transformComments={transformComments}
|
|
35
37
|
renderActor={renderActor}
|
|
@@ -9,6 +9,7 @@ export function FeedbackScreenContent({
|
|
|
9
9
|
showHeader = true,
|
|
10
10
|
hideBackButton = false,
|
|
11
11
|
onEntryOpen,
|
|
12
|
+
onCreateEntry,
|
|
12
13
|
...colors
|
|
13
14
|
}: FeedbackScreenContentProps) {
|
|
14
15
|
const { showForm, setShowForm, setSelectedEntryId } = useFeedbackBody();
|
|
@@ -26,6 +27,7 @@ export function FeedbackScreenContent({
|
|
|
26
27
|
<FeedbackScreenList
|
|
27
28
|
hideBackButton={hideBackButton}
|
|
28
29
|
onEntryOpen={handleEntryOpen}
|
|
30
|
+
onCreateEntry={onCreateEntry}
|
|
29
31
|
/>
|
|
30
32
|
</FeedbackBoard.Root>
|
|
31
33
|
|
|
@@ -4,24 +4,33 @@ import { useFeedbackUi } from "../../../shared/context/FeedbackProvider.js";
|
|
|
4
4
|
export function Button({
|
|
5
5
|
label,
|
|
6
6
|
onPress,
|
|
7
|
+
variant = "default",
|
|
7
8
|
}: {
|
|
8
9
|
label: string;
|
|
9
10
|
onPress: () => void;
|
|
11
|
+
variant?: "default" | "primary";
|
|
10
12
|
}) {
|
|
11
13
|
const { theme } = useFeedbackUi();
|
|
14
|
+
const primary = variant === "primary";
|
|
12
15
|
return (
|
|
13
16
|
<Pressable
|
|
14
17
|
onPress={onPress}
|
|
15
18
|
style={{
|
|
16
|
-
alignSelf: "flex-start",
|
|
19
|
+
alignSelf: primary ? "center" : "flex-start",
|
|
17
20
|
paddingVertical: 8,
|
|
18
21
|
paddingHorizontal: 11,
|
|
19
22
|
borderRadius: 8,
|
|
20
23
|
borderWidth: 1,
|
|
21
|
-
borderColor: theme.colors.border,
|
|
24
|
+
borderColor: primary ? theme.colors.primary : theme.colors.border,
|
|
25
|
+
backgroundColor: primary ? theme.colors.primary : undefined,
|
|
22
26
|
}}
|
|
23
27
|
>
|
|
24
|
-
<Text
|
|
28
|
+
<Text
|
|
29
|
+
style={{
|
|
30
|
+
color: primary ? theme.colors.primaryForeground : theme.colors.text,
|
|
31
|
+
fontWeight: "600",
|
|
32
|
+
}}
|
|
33
|
+
>
|
|
25
34
|
{label}
|
|
26
35
|
</Text>
|
|
27
36
|
</Pressable>
|
|
@@ -2,10 +2,19 @@ import { Pressable, Text } from "react-native";
|
|
|
2
2
|
import { useSafeAreaInsets } from "react-native-safe-area-context";
|
|
3
3
|
import { useFeedbackBody } from "../../../shared/context/FeedbackBodyProvider";
|
|
4
4
|
import { useFeedbackUi } from "../../../shared/context/FeedbackProvider";
|
|
5
|
-
import {
|
|
5
|
+
import { createEntryLabel, entryStatusChoices } from "../../../shared/helpers";
|
|
6
|
+
import { ChoiceChips, FeedbackBoard } from "./primitives";
|
|
6
7
|
|
|
7
8
|
export function FeedbackScreenHeader() {
|
|
8
|
-
const {
|
|
9
|
+
const {
|
|
10
|
+
query,
|
|
11
|
+
setShowForm,
|
|
12
|
+
setQuery,
|
|
13
|
+
setIsSearching,
|
|
14
|
+
enabledKinds,
|
|
15
|
+
statusFilter,
|
|
16
|
+
setStatusFilter,
|
|
17
|
+
} = useFeedbackBody();
|
|
9
18
|
const { messages, theme } = useFeedbackUi();
|
|
10
19
|
const { top } = useSafeAreaInsets();
|
|
11
20
|
|
|
@@ -20,6 +29,12 @@ export function FeedbackScreenHeader() {
|
|
|
20
29
|
<Text style={{ color: theme.colors.mutedText }}>
|
|
21
30
|
{messages.board.subtitle}
|
|
22
31
|
</Text>
|
|
32
|
+
<ChoiceChips
|
|
33
|
+
accessibilityLabel={messages.board.statusFilter}
|
|
34
|
+
options={entryStatusChoices(messages)}
|
|
35
|
+
value={statusFilter}
|
|
36
|
+
onValueChange={setStatusFilter}
|
|
37
|
+
/>
|
|
23
38
|
<Pressable
|
|
24
39
|
onPress={() => setShowForm(true)}
|
|
25
40
|
style={{
|
|
@@ -33,10 +48,16 @@ export function FeedbackScreenHeader() {
|
|
|
33
48
|
<Text
|
|
34
49
|
style={{ color: theme.colors.primaryForeground, fontWeight: "700" }}
|
|
35
50
|
>
|
|
36
|
-
{messages
|
|
51
|
+
{createEntryLabel(enabledKinds, messages)}
|
|
37
52
|
</Text>
|
|
38
53
|
</Pressable>
|
|
39
|
-
<FeedbackBoard.Search
|
|
54
|
+
<FeedbackBoard.Search
|
|
55
|
+
value={query}
|
|
56
|
+
onValueChange={(q) => {
|
|
57
|
+
setQuery(q);
|
|
58
|
+
setIsSearching(q.trim().length > 0);
|
|
59
|
+
}}
|
|
60
|
+
/>
|
|
40
61
|
</FeedbackBoard.Header>
|
|
41
62
|
);
|
|
42
63
|
}
|
|
@@ -1,5 +1,8 @@
|
|
|
1
|
+
import { Text } from "react-native";
|
|
2
|
+
|
|
1
3
|
import { useFeedbackBody } from "../../../shared/context/FeedbackBodyProvider.js";
|
|
2
4
|
import { useFeedbackUi } from "../../../shared/context/FeedbackProvider.js";
|
|
5
|
+
import { createEntryLabel } from "../../../shared/helpers.js";
|
|
3
6
|
import type { FeedbackScreenListProps } from "../../../shared/types/index.js";
|
|
4
7
|
import { Button } from "./Button.js";
|
|
5
8
|
import { EntryCard } from "./EntryCard.js";
|
|
@@ -8,47 +11,84 @@ import { FeedbackBoard } from "./primitives.js";
|
|
|
8
11
|
|
|
9
12
|
export function FeedbackScreenList({
|
|
10
13
|
hideBackButton = false,
|
|
14
|
+
showSelectedEntry = true,
|
|
11
15
|
onEntryOpen,
|
|
16
|
+
onCreateEntry,
|
|
12
17
|
}: FeedbackScreenListProps) {
|
|
13
18
|
const {
|
|
14
19
|
hooks,
|
|
15
20
|
entrySort,
|
|
16
21
|
isSearching,
|
|
17
22
|
enabledKinds,
|
|
23
|
+
statusFilter,
|
|
18
24
|
selectedEntryId,
|
|
19
25
|
debouncedQuery,
|
|
20
26
|
setSelectedEntryId,
|
|
27
|
+
setShowForm,
|
|
28
|
+
emptyState,
|
|
21
29
|
} = useFeedbackBody();
|
|
22
30
|
const { messages, theme } = useFeedbackUi();
|
|
23
31
|
|
|
24
|
-
const list = hooks.useEntries({
|
|
32
|
+
const list = hooks.useEntries({
|
|
33
|
+
sort: entrySort,
|
|
34
|
+
kinds: enabledKinds,
|
|
35
|
+
statusFilter,
|
|
36
|
+
});
|
|
25
37
|
const search = hooks.useSearchEntries({
|
|
26
38
|
searchQuery: debouncedQuery,
|
|
27
39
|
kinds: enabledKinds,
|
|
40
|
+
statusFilter,
|
|
28
41
|
});
|
|
29
42
|
|
|
30
43
|
const entries = isSearching ? search : list.results;
|
|
44
|
+
const createEntry = onCreateEntry ?? (() => setShowForm(true));
|
|
45
|
+
|
|
46
|
+
const loading = isSearching
|
|
47
|
+
? search === undefined
|
|
48
|
+
: list.status === "LoadingFirstPage";
|
|
31
49
|
|
|
32
50
|
return (
|
|
33
51
|
<>
|
|
34
|
-
|
|
35
|
-
{
|
|
52
|
+
{showSelectedEntry && selectedEntryId ? (
|
|
53
|
+
<FeedbackBoard.List style={{ padding: theme.spacing }}>
|
|
36
54
|
<EntryDetail
|
|
37
55
|
entryId={selectedEntryId}
|
|
38
56
|
hideBackButton={hideBackButton}
|
|
39
57
|
onBack={() => setSelectedEntryId(null)}
|
|
40
58
|
/>
|
|
41
|
-
|
|
42
|
-
|
|
59
|
+
</FeedbackBoard.List>
|
|
60
|
+
) : loading ? (
|
|
61
|
+
<FeedbackBoard.State>
|
|
62
|
+
<Text style={{ color: theme.colors.mutedText }}>
|
|
63
|
+
{messages.board.loading}
|
|
64
|
+
</Text>
|
|
65
|
+
</FeedbackBoard.State>
|
|
66
|
+
) : entries !== undefined && entries.length === 0 ? (
|
|
67
|
+
<FeedbackBoard.State>
|
|
68
|
+
{emptyState}
|
|
69
|
+
<Text style={{ color: theme.colors.mutedText, textAlign: "center" }}>
|
|
70
|
+
{isSearching
|
|
71
|
+
? messages.board.noSearchResults
|
|
72
|
+
: messages.board.noEntries}
|
|
73
|
+
</Text>
|
|
74
|
+
<Button
|
|
75
|
+
label={createEntryLabel(enabledKinds, messages)}
|
|
76
|
+
onPress={createEntry}
|
|
77
|
+
variant="primary"
|
|
78
|
+
/>
|
|
79
|
+
</FeedbackBoard.State>
|
|
80
|
+
) : (
|
|
81
|
+
<FeedbackBoard.List style={{ padding: theme.spacing }}>
|
|
82
|
+
{(entries ?? []).map((entry) => (
|
|
43
83
|
<EntryCard
|
|
44
84
|
key={entry.id}
|
|
45
85
|
entry={entry}
|
|
46
86
|
hooks={hooks}
|
|
47
87
|
onOpen={() => onEntryOpen(entry.id)}
|
|
48
88
|
/>
|
|
49
|
-
))
|
|
50
|
-
|
|
51
|
-
|
|
89
|
+
))}
|
|
90
|
+
</FeedbackBoard.List>
|
|
91
|
+
)}
|
|
52
92
|
|
|
53
93
|
{!isSearching && list.status === "CanLoadMore" && (
|
|
54
94
|
<Button
|
|
@@ -15,7 +15,7 @@ import { useFeedbackUi } from "../../../shared/context/FeedbackProvider";
|
|
|
15
15
|
import type { FeedbackScreenEntryModalProps } from "../../../shared/types";
|
|
16
16
|
import { collectEntryMetadata } from "../../../shared/metadata";
|
|
17
17
|
import { EntryDetail } from "./EntryDetail";
|
|
18
|
-
import { FeedbackForm } from "./primitives";
|
|
18
|
+
import { ChoiceChips, FeedbackForm } from "./primitives";
|
|
19
19
|
|
|
20
20
|
export function CreateEntryModal({
|
|
21
21
|
onRequestClose,
|
|
@@ -131,7 +131,7 @@ export function CreateEntryModal({
|
|
|
131
131
|
onBack={() => setPreviewEntryId(null)}
|
|
132
132
|
/>
|
|
133
133
|
) : (
|
|
134
|
-
<
|
|
134
|
+
<CreateEntryForm
|
|
135
135
|
kind={kind}
|
|
136
136
|
onKindChange={setKind}
|
|
137
137
|
title={title}
|
|
@@ -149,7 +149,7 @@ export function CreateEntryModal({
|
|
|
149
149
|
);
|
|
150
150
|
}
|
|
151
151
|
|
|
152
|
-
function
|
|
152
|
+
export function CreateEntryForm({
|
|
153
153
|
kind,
|
|
154
154
|
onKindChange,
|
|
155
155
|
title,
|
|
@@ -247,33 +247,15 @@ function CreateForm({
|
|
|
247
247
|
<Text style={{ color: theme.colors.text, fontWeight: "600" }}>
|
|
248
248
|
{messages.form.kind}
|
|
249
249
|
</Text>
|
|
250
|
-
<
|
|
251
|
-
{
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
borderWidth: 1,
|
|
260
|
-
borderColor:
|
|
261
|
-
value === kind ? theme.colors.primary : theme.colors.border,
|
|
262
|
-
}}
|
|
263
|
-
>
|
|
264
|
-
<Text
|
|
265
|
-
style={{
|
|
266
|
-
color:
|
|
267
|
-
value === kind
|
|
268
|
-
? theme.colors.primary
|
|
269
|
-
: theme.colors.mutedText,
|
|
270
|
-
}}
|
|
271
|
-
>
|
|
272
|
-
{messages.kinds[value]}
|
|
273
|
-
</Text>
|
|
274
|
-
</Pressable>
|
|
275
|
-
))}
|
|
276
|
-
</View>
|
|
250
|
+
<ChoiceChips
|
|
251
|
+
accessibilityLabel={messages.form.kind}
|
|
252
|
+
options={enabledKinds.map((value) => ({
|
|
253
|
+
value,
|
|
254
|
+
label: messages.kinds[value],
|
|
255
|
+
}))}
|
|
256
|
+
value={kind}
|
|
257
|
+
onValueChange={onKindChange}
|
|
258
|
+
/>
|
|
277
259
|
</>
|
|
278
260
|
)}
|
|
279
261
|
<FeedbackForm.Input
|
|
@@ -32,6 +32,7 @@ import {
|
|
|
32
32
|
import type {
|
|
33
33
|
BoardSearchBaseProps,
|
|
34
34
|
BoardSearchState,
|
|
35
|
+
ChoiceChipsBaseProps,
|
|
35
36
|
CommentActionBaseProps,
|
|
36
37
|
CommentLikeBaseProps,
|
|
37
38
|
CommentLikeState,
|
|
@@ -182,14 +183,103 @@ const BoardList = forwardRef<ScrollView, ScrollViewProps>(
|
|
|
182
183
|
},
|
|
183
184
|
);
|
|
184
185
|
|
|
186
|
+
function BoardState({ style, ...props }: ViewProps) {
|
|
187
|
+
const { theme, unstyled } = useFeedbackUi();
|
|
188
|
+
return (
|
|
189
|
+
<View
|
|
190
|
+
{...props}
|
|
191
|
+
style={combineStyle<ViewStyle>(
|
|
192
|
+
!unstyled,
|
|
193
|
+
{
|
|
194
|
+
flex: 1,
|
|
195
|
+
alignItems: "center",
|
|
196
|
+
justifyContent: "center",
|
|
197
|
+
gap: theme.spacing,
|
|
198
|
+
padding: theme.spacing,
|
|
199
|
+
},
|
|
200
|
+
style,
|
|
201
|
+
)}
|
|
202
|
+
/>
|
|
203
|
+
);
|
|
204
|
+
}
|
|
205
|
+
|
|
185
206
|
export const FeedbackBoard = {
|
|
186
207
|
Root: BoardRoot,
|
|
187
208
|
Header: BoardHeader,
|
|
188
209
|
Title: BoardTitle,
|
|
189
210
|
Search: BoardSearch,
|
|
190
211
|
List: BoardList,
|
|
212
|
+
State: BoardState,
|
|
191
213
|
};
|
|
192
214
|
|
|
215
|
+
/** Props for the reusable native `ChoiceChips` primitive. */
|
|
216
|
+
export interface ChoiceChipsProps<Value extends string = string>
|
|
217
|
+
extends Omit<ViewProps, "children">, ChoiceChipsBaseProps<Value> {}
|
|
218
|
+
|
|
219
|
+
export function ChoiceChips<Value extends string>({
|
|
220
|
+
options,
|
|
221
|
+
value,
|
|
222
|
+
onValueChange,
|
|
223
|
+
style,
|
|
224
|
+
...props
|
|
225
|
+
}: ChoiceChipsProps<Value>) {
|
|
226
|
+
const { theme, unstyled } = useFeedbackUi();
|
|
227
|
+
|
|
228
|
+
return (
|
|
229
|
+
<View
|
|
230
|
+
{...props}
|
|
231
|
+
style={combineStyle<ViewStyle>(
|
|
232
|
+
!unstyled,
|
|
233
|
+
{ flexDirection: "row", flexWrap: "wrap", gap: 7 },
|
|
234
|
+
style,
|
|
235
|
+
)}
|
|
236
|
+
>
|
|
237
|
+
{options.map((option) => {
|
|
238
|
+
const selected = option.value === value;
|
|
239
|
+
return (
|
|
240
|
+
<Pressable
|
|
241
|
+
key={option.value}
|
|
242
|
+
accessibilityRole="radio"
|
|
243
|
+
accessibilityState={{ selected }}
|
|
244
|
+
onPress={() => onValueChange(option.value)}
|
|
245
|
+
style={
|
|
246
|
+
unstyled
|
|
247
|
+
? undefined
|
|
248
|
+
: {
|
|
249
|
+
paddingVertical: 6,
|
|
250
|
+
paddingHorizontal: 9,
|
|
251
|
+
borderRadius: 999,
|
|
252
|
+
borderWidth: 1,
|
|
253
|
+
borderColor: selected
|
|
254
|
+
? theme.colors.primary
|
|
255
|
+
: theme.colors.border,
|
|
256
|
+
backgroundColor: selected
|
|
257
|
+
? theme.colors.primary
|
|
258
|
+
: "transparent",
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
>
|
|
262
|
+
<Text
|
|
263
|
+
style={
|
|
264
|
+
unstyled
|
|
265
|
+
? undefined
|
|
266
|
+
: {
|
|
267
|
+
color: selected
|
|
268
|
+
? theme.colors.primaryForeground
|
|
269
|
+
: theme.colors.mutedText,
|
|
270
|
+
fontWeight: "600",
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
>
|
|
274
|
+
{option.label}
|
|
275
|
+
</Text>
|
|
276
|
+
</Pressable>
|
|
277
|
+
);
|
|
278
|
+
})}
|
|
279
|
+
</View>
|
|
280
|
+
);
|
|
281
|
+
}
|
|
282
|
+
|
|
193
283
|
const EntryContext = createContext<FeedbackEntryData | null>(null);
|
|
194
284
|
function useEntryContext(): FeedbackEntryData {
|
|
195
285
|
const value = useContext(EntryContext);
|
package/src/react/primitives.tsx
CHANGED
|
@@ -25,6 +25,7 @@ import type {
|
|
|
25
25
|
CommentLikeBaseProps,
|
|
26
26
|
CommentLikeState,
|
|
27
27
|
CommentRootBaseProps,
|
|
28
|
+
ChoiceChipsBaseProps,
|
|
28
29
|
EntryRootBaseProps,
|
|
29
30
|
EntryUpvoteBaseProps,
|
|
30
31
|
EntryUpvoteState,
|
|
@@ -147,14 +148,68 @@ function BoardList(props: HTMLAttributes<HTMLDivElement>) {
|
|
|
147
148
|
);
|
|
148
149
|
}
|
|
149
150
|
|
|
151
|
+
function BoardState(props: HTMLAttributes<HTMLDivElement>) {
|
|
152
|
+
const { unstyled } = useFeedbackUi();
|
|
153
|
+
return (
|
|
154
|
+
<div
|
|
155
|
+
{...props}
|
|
156
|
+
className={classes(unstyled, "cf-state", props.className)}
|
|
157
|
+
/>
|
|
158
|
+
);
|
|
159
|
+
}
|
|
160
|
+
|
|
150
161
|
export const FeedbackBoard = {
|
|
151
162
|
Root: BoardRoot,
|
|
152
163
|
Header: BoardHeader,
|
|
153
164
|
Title: BoardTitle,
|
|
154
165
|
Search: BoardSearch,
|
|
155
166
|
List: BoardList,
|
|
167
|
+
State: BoardState,
|
|
156
168
|
};
|
|
157
169
|
|
|
170
|
+
/**
|
|
171
|
+
* Props for the reusable web `ChoiceChips` primitive.
|
|
172
|
+
*/
|
|
173
|
+
export interface ChoiceChipsProps<Value extends string = string>
|
|
174
|
+
extends
|
|
175
|
+
Omit<HTMLAttributes<HTMLDivElement>, "children">,
|
|
176
|
+
ChoiceChipsBaseProps<Value> {}
|
|
177
|
+
|
|
178
|
+
export function ChoiceChips<Value extends string>({
|
|
179
|
+
options,
|
|
180
|
+
value,
|
|
181
|
+
onValueChange,
|
|
182
|
+
className,
|
|
183
|
+
role,
|
|
184
|
+
...props
|
|
185
|
+
}: ChoiceChipsProps<Value>) {
|
|
186
|
+
const { unstyled } = useFeedbackUi();
|
|
187
|
+
|
|
188
|
+
return (
|
|
189
|
+
<div
|
|
190
|
+
{...props}
|
|
191
|
+
role={role ?? "group"}
|
|
192
|
+
className={classes(unstyled, "cf-choice-chips", className)}
|
|
193
|
+
>
|
|
194
|
+
{options.map((option) => {
|
|
195
|
+
const selected = option.value === value;
|
|
196
|
+
return (
|
|
197
|
+
<button
|
|
198
|
+
key={option.value}
|
|
199
|
+
type="button"
|
|
200
|
+
className={classes(unstyled, "cf-choice-chip")}
|
|
201
|
+
data-selected={selected ? "true" : "false"}
|
|
202
|
+
aria-pressed={selected}
|
|
203
|
+
onClick={() => onValueChange(option.value)}
|
|
204
|
+
>
|
|
205
|
+
{option.label}
|
|
206
|
+
</button>
|
|
207
|
+
);
|
|
208
|
+
})}
|
|
209
|
+
</div>
|
|
210
|
+
);
|
|
211
|
+
}
|
|
212
|
+
|
|
158
213
|
const EntryContext = createContext<FeedbackEntryData | null>(null);
|
|
159
214
|
|
|
160
215
|
function useEntryContext(): FeedbackEntryData {
|