convex-feedback-ui 0.1.6 → 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 +19 -0
- package/dist/.tsbuildinfo +1 -1
- package/dist/native/expo/FeedbackRoutes.d.ts.map +1 -1
- package/dist/native/expo/FeedbackRoutes.js +10 -5
- package/dist/native/expo/FeedbackRoutes.js.map +1 -1
- 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.map +1 -1
- package/dist/native/expo/FeedbackStack.js +7 -4
- package/dist/native/expo/FeedbackStack.js.map +1 -1
- package/dist/native/expo/FeedbackStackLayout.d.ts +1 -1
- package/dist/native/expo/FeedbackStackLayout.d.ts.map +1 -1
- package/dist/native/expo/FeedbackStackLayout.js +2 -2
- package/dist/native/expo/FeedbackStackLayout.js.map +1 -1
- package/dist/native/expo/types.d.ts +2 -0
- 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.map +1 -1
- package/dist/native/shared/ui/NewEntry.js +5 -12
- 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 +9 -2
- package/dist/shared/types/screen.d.ts.map +1 -1
- package/package.json +6 -2
- package/src/native/expo/FeedbackRoutes.tsx +37 -3
- package/src/native/expo/FeedbackScreen.tsx +2 -0
- package/src/native/expo/FeedbackStack.tsx +25 -2
- package/src/native/expo/FeedbackStackLayout.tsx +2 -0
- package/src/native/expo/README.md +2 -2
- package/src/native/expo/types.ts +2 -0
- 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 +47 -8
- package/src/native/shared/ui/NewEntry.tsx +10 -28
- 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 +11 -1
|
@@ -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,
|
|
@@ -247,33 +247,15 @@ export function CreateEntryForm({
|
|
|
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 {
|
package/src/react/screen.tsx
CHANGED
|
@@ -11,6 +11,7 @@ import {
|
|
|
11
11
|
FeedbackProvider,
|
|
12
12
|
useFeedbackUi,
|
|
13
13
|
} from "../shared/context/FeedbackProvider";
|
|
14
|
+
import { createEntryLabel, entryStatusChoices } from "../shared/helpers.js";
|
|
14
15
|
import type {
|
|
15
16
|
FeedbackScreenCommentBranchProps,
|
|
16
17
|
FeedbackScreenContentProps,
|
|
@@ -22,6 +23,7 @@ import type {
|
|
|
22
23
|
} from "../shared/types/";
|
|
23
24
|
import {
|
|
24
25
|
Comment,
|
|
26
|
+
ChoiceChips,
|
|
25
27
|
FeedbackBoard,
|
|
26
28
|
FeedbackEntry,
|
|
27
29
|
FeedbackForm,
|
|
@@ -52,6 +54,7 @@ export function FeedbackScreen({
|
|
|
52
54
|
renderActor,
|
|
53
55
|
debounceDuration = 300,
|
|
54
56
|
collectMetadata,
|
|
57
|
+
emptyState,
|
|
55
58
|
...props
|
|
56
59
|
}: FeedbackScreenProps) {
|
|
57
60
|
return (
|
|
@@ -64,6 +67,7 @@ export function FeedbackScreen({
|
|
|
64
67
|
maxCommentDepth={maxCommentDepth}
|
|
65
68
|
debounceDuration={debounceDuration}
|
|
66
69
|
collectMetadata={collectMetadata}
|
|
70
|
+
emptyState={emptyState}
|
|
67
71
|
collectStandardMetadata={collectWebMetadata}
|
|
68
72
|
transformComments={transformComments}
|
|
69
73
|
renderActor={renderActor}
|
|
@@ -90,16 +94,24 @@ function FeedbackScreenInner({
|
|
|
90
94
|
setShowForm,
|
|
91
95
|
setSelectedEntryId,
|
|
92
96
|
selectedEntryId,
|
|
97
|
+
statusFilter,
|
|
98
|
+
setStatusFilter,
|
|
93
99
|
query,
|
|
94
100
|
setQuery,
|
|
95
101
|
debouncedQuery,
|
|
102
|
+
emptyState,
|
|
96
103
|
} = useFeedbackBody();
|
|
97
104
|
const { messages } = useFeedbackUi();
|
|
98
105
|
|
|
99
|
-
const list = hooks.useEntries({
|
|
106
|
+
const list = hooks.useEntries({
|
|
107
|
+
sort: entrySort,
|
|
108
|
+
kinds: enabledKinds,
|
|
109
|
+
statusFilter,
|
|
110
|
+
});
|
|
100
111
|
const searchResults = hooks.useSearchEntries({
|
|
101
112
|
searchQuery: debouncedQuery,
|
|
102
113
|
kinds: enabledKinds,
|
|
114
|
+
statusFilter,
|
|
103
115
|
});
|
|
104
116
|
|
|
105
117
|
if (selectedEntryId !== null) {
|
|
@@ -125,13 +137,21 @@ function FeedbackScreenInner({
|
|
|
125
137
|
<div>
|
|
126
138
|
<FeedbackBoard.Title />
|
|
127
139
|
<p className="cf-board__subtitle">{messages.board.subtitle}</p>
|
|
140
|
+
<ChoiceChips
|
|
141
|
+
aria-label={messages.board.statusFilter}
|
|
142
|
+
options={entryStatusChoices(messages)}
|
|
143
|
+
value={statusFilter}
|
|
144
|
+
onValueChange={setStatusFilter}
|
|
145
|
+
/>
|
|
128
146
|
</div>
|
|
129
147
|
<button
|
|
130
148
|
type="button"
|
|
131
149
|
className="cf-button cf-button--primary"
|
|
132
150
|
onClick={() => setShowForm((current) => !current)}
|
|
133
151
|
>
|
|
134
|
-
{showForm
|
|
152
|
+
{showForm
|
|
153
|
+
? messages.form.cancel
|
|
154
|
+
: createEntryLabel(enabledKinds, messages)}
|
|
135
155
|
</button>
|
|
136
156
|
</FeedbackBoard.Header>
|
|
137
157
|
|
|
@@ -146,26 +166,39 @@ function FeedbackScreenInner({
|
|
|
146
166
|
|
|
147
167
|
<FeedbackBoard.Search value={query} onValueChange={setQuery} />
|
|
148
168
|
|
|
149
|
-
{loading
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
169
|
+
{loading ? (
|
|
170
|
+
<FeedbackBoard.State>
|
|
171
|
+
<span className="cf-state__message">{messages.board.loading}</span>
|
|
172
|
+
</FeedbackBoard.State>
|
|
173
|
+
) : entries !== undefined && entries.length === 0 ? (
|
|
174
|
+
<FeedbackBoard.State>
|
|
175
|
+
{emptyState}
|
|
176
|
+
<span className="cf-state__message">
|
|
177
|
+
{searching
|
|
178
|
+
? messages.board.noSearchResults
|
|
179
|
+
: messages.board.noEntries}
|
|
180
|
+
</span>
|
|
181
|
+
<button
|
|
182
|
+
type="button"
|
|
183
|
+
className="cf-button cf-button--primary"
|
|
184
|
+
onClick={() => setShowForm(true)}
|
|
185
|
+
>
|
|
186
|
+
{createEntryLabel(enabledKinds, messages)}
|
|
187
|
+
</button>
|
|
188
|
+
</FeedbackBoard.State>
|
|
189
|
+
) : (
|
|
190
|
+
<FeedbackBoard.List>
|
|
191
|
+
{(entries ?? []).map((entry) => (
|
|
192
|
+
<EntryCard
|
|
193
|
+
key={entry.id}
|
|
194
|
+
entry={entry}
|
|
195
|
+
hooks={hooks}
|
|
196
|
+
onOpen={() => setSelectedEntryId(entry.id)}
|
|
197
|
+
/>
|
|
198
|
+
))}
|
|
199
|
+
</FeedbackBoard.List>
|
|
156
200
|
)}
|
|
157
201
|
|
|
158
|
-
<FeedbackBoard.List>
|
|
159
|
-
{(entries ?? []).map((entry) => (
|
|
160
|
-
<EntryCard
|
|
161
|
-
key={entry.id}
|
|
162
|
-
entry={entry}
|
|
163
|
-
hooks={hooks}
|
|
164
|
-
onOpen={() => setSelectedEntryId(entry.id)}
|
|
165
|
-
/>
|
|
166
|
-
))}
|
|
167
|
-
</FeedbackBoard.List>
|
|
168
|
-
|
|
169
202
|
{!searching && list.status === "CanLoadMore" && (
|
|
170
203
|
<button
|
|
171
204
|
type="button"
|
|
@@ -273,19 +306,20 @@ function CreateEntryForm({ onCreated }: FeedbackScreenEntryModalProps) {
|
|
|
273
306
|
|
|
274
307
|
return (
|
|
275
308
|
<FeedbackForm.Root onSubmit={submit}>
|
|
276
|
-
|
|
277
|
-
<
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
309
|
+
{enabledKinds.length > 1 && (
|
|
310
|
+
<label className="cf-field">
|
|
311
|
+
<span>{messages.form.kind}</span>
|
|
312
|
+
<ChoiceChips
|
|
313
|
+
aria-label={messages.form.kind}
|
|
314
|
+
options={enabledKinds.map((value) => ({
|
|
315
|
+
value,
|
|
316
|
+
label: messages.kinds[value],
|
|
317
|
+
}))}
|
|
318
|
+
value={kind}
|
|
319
|
+
onValueChange={setKind}
|
|
320
|
+
/>
|
|
321
|
+
</label>
|
|
322
|
+
)}
|
|
289
323
|
<label className="cf-field">
|
|
290
324
|
<span>{messages.form.title}</span>
|
|
291
325
|
<FeedbackForm.Input
|
package/src/react/styles.css
CHANGED
|
@@ -30,6 +30,33 @@
|
|
|
30
30
|
margin: 0.4rem 0 0;
|
|
31
31
|
color: var(--cf-muted);
|
|
32
32
|
}
|
|
33
|
+
:where(.cf-board) :where(.cf-choice-chips) {
|
|
34
|
+
display: flex;
|
|
35
|
+
flex-wrap: wrap;
|
|
36
|
+
align-items: center;
|
|
37
|
+
gap: 7px;
|
|
38
|
+
margin-top: 0.75rem;
|
|
39
|
+
}
|
|
40
|
+
:where(.cf-board) :where(.cf-choice-chip) {
|
|
41
|
+
border: 1px solid var(--cf-border);
|
|
42
|
+
border-radius: 999px;
|
|
43
|
+
padding: 6px 10px;
|
|
44
|
+
background: var(--cf-surface);
|
|
45
|
+
color: var(--cf-muted);
|
|
46
|
+
font: inherit;
|
|
47
|
+
font-size: 0.85rem;
|
|
48
|
+
font-weight: 600;
|
|
49
|
+
cursor: pointer;
|
|
50
|
+
}
|
|
51
|
+
:where(.cf-board) :where(.cf-choice-chip[data-selected="true"]) {
|
|
52
|
+
border-color: var(--cf-primary);
|
|
53
|
+
background: color-mix(in srgb, var(--cf-primary) 10%, var(--cf-surface));
|
|
54
|
+
color: var(--cf-primary);
|
|
55
|
+
}
|
|
56
|
+
:where(.cf-board) :where(.cf-choice-chip:focus-visible) {
|
|
57
|
+
outline: 3px solid color-mix(in srgb, var(--cf-primary) 25%, transparent);
|
|
58
|
+
outline-offset: 2px;
|
|
59
|
+
}
|
|
33
60
|
:where(.cf-board) :where(.cf-board__list),
|
|
34
61
|
:where(.cf-board) :where(.cf-comments) {
|
|
35
62
|
display: grid;
|
|
@@ -198,8 +225,18 @@
|
|
|
198
225
|
-webkit-box-orient: vertical;
|
|
199
226
|
}
|
|
200
227
|
:where(.cf-board) :where(.cf-state) {
|
|
228
|
+
display: flex;
|
|
229
|
+
min-height: 180px;
|
|
230
|
+
flex-direction: column;
|
|
231
|
+
align-items: center;
|
|
232
|
+
justify-content: center;
|
|
233
|
+
gap: var(--cf-space);
|
|
234
|
+
padding: calc(var(--cf-space) * 1.5);
|
|
201
235
|
color: var(--cf-muted);
|
|
202
|
-
|
|
236
|
+
text-align: center;
|
|
237
|
+
}
|
|
238
|
+
:where(.cf-board) :where(.cf-state__message) {
|
|
239
|
+
margin: 0;
|
|
203
240
|
}
|
|
204
241
|
:where(.cf-board) :where(.cf-detail),
|
|
205
242
|
:where(.cf-board) :where(.cf-discussion) {
|
|
@@ -6,6 +6,7 @@ import {
|
|
|
6
6
|
useMemo,
|
|
7
7
|
useState,
|
|
8
8
|
} from "react";
|
|
9
|
+
import type { EntryStatusFilter } from "convex-feedback";
|
|
9
10
|
import { debounce } from "../helpers";
|
|
10
11
|
import type {
|
|
11
12
|
FeedbackScreenBodyContextValue,
|
|
@@ -23,6 +24,7 @@ export function FeedbackBodyProvider({
|
|
|
23
24
|
maxCommentDepth,
|
|
24
25
|
debounceDuration,
|
|
25
26
|
collectMetadata,
|
|
27
|
+
emptyState,
|
|
26
28
|
collectStandardMetadata,
|
|
27
29
|
transformComments,
|
|
28
30
|
renderActor,
|
|
@@ -33,6 +35,7 @@ export function FeedbackBodyProvider({
|
|
|
33
35
|
const [isSearching, setIsSearching] = useState(false);
|
|
34
36
|
const [query, setQuery] = useState("");
|
|
35
37
|
const [debouncedQuery, setDebouncedQuery] = useState("");
|
|
38
|
+
const [statusFilter, setStatusFilter] = useState<EntryStatusFilter>("open");
|
|
36
39
|
|
|
37
40
|
const updateDebouncedQuery = useMemo(
|
|
38
41
|
() => debounce(setDebouncedQuery, debounceDuration),
|
|
@@ -51,12 +54,14 @@ export function FeedbackBodyProvider({
|
|
|
51
54
|
showForm,
|
|
52
55
|
isSearching,
|
|
53
56
|
selectedEntryId,
|
|
57
|
+
statusFilter,
|
|
54
58
|
|
|
55
59
|
setQuery,
|
|
56
60
|
setDebouncedQuery,
|
|
57
61
|
setShowForm,
|
|
58
62
|
setIsSearching,
|
|
59
63
|
setSelectedEntryId,
|
|
64
|
+
setStatusFilter,
|
|
60
65
|
|
|
61
66
|
hooks,
|
|
62
67
|
entrySort,
|
|
@@ -65,6 +70,7 @@ export function FeedbackBodyProvider({
|
|
|
65
70
|
maxCommentDepth,
|
|
66
71
|
debounceDuration,
|
|
67
72
|
collectMetadata,
|
|
73
|
+
emptyState,
|
|
68
74
|
collectStandardMetadata,
|
|
69
75
|
transformComments,
|
|
70
76
|
renderActor,
|
package/src/shared/helpers.ts
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
|
-
import type { EntryKind } from "convex-feedback";
|
|
1
|
+
import type { EntryKind, EntryStatusFilter } from "convex-feedback";
|
|
2
|
+
import type { FeedbackMessages } from "./types/messages.js";
|
|
3
|
+
import type { ChoiceChipOption } from "./types/choice.js";
|
|
2
4
|
|
|
3
5
|
export const kinds: readonly EntryKind[] = [
|
|
4
6
|
"feedback",
|
|
@@ -6,6 +8,24 @@ export const kinds: readonly EntryKind[] = [
|
|
|
6
8
|
"bug_report",
|
|
7
9
|
];
|
|
8
10
|
|
|
11
|
+
export function createEntryLabel(
|
|
12
|
+
enabledKinds: readonly EntryKind[],
|
|
13
|
+
messages: FeedbackMessages,
|
|
14
|
+
): string {
|
|
15
|
+
return enabledKinds.length === 1
|
|
16
|
+
? messages.newFeedback[enabledKinds[0]!]
|
|
17
|
+
: messages.board.createEntry;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function entryStatusChoices(
|
|
21
|
+
messages: FeedbackMessages,
|
|
22
|
+
): readonly ChoiceChipOption<EntryStatusFilter>[] {
|
|
23
|
+
return [
|
|
24
|
+
{ value: "open", label: messages.statuses.open },
|
|
25
|
+
{ value: "closed", label: messages.statuses.closed },
|
|
26
|
+
];
|
|
27
|
+
}
|
|
28
|
+
|
|
9
29
|
export function debounce<TArgs extends unknown[]>(
|
|
10
30
|
callback: (...args: TArgs) => void,
|
|
11
31
|
delay: number,
|
package/src/shared/messages.ts
CHANGED
|
@@ -5,6 +5,7 @@ export const englishFeedbackMessages: FeedbackMessages = {
|
|
|
5
5
|
title: "Feedback",
|
|
6
6
|
subtitle: "Share ideas, report problems, and help shape what comes next.",
|
|
7
7
|
searchPlaceholder: "Search feedback…",
|
|
8
|
+
statusFilter: "Filter by status",
|
|
8
9
|
createEntry: "Create feedback",
|
|
9
10
|
noEntries: "No feedback yet.",
|
|
10
11
|
noSearchResults: "No matching feedback found.",
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
export interface ChoiceChipOption<Value extends string = string> {
|
|
2
|
+
value: Value;
|
|
3
|
+
label: string;
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
export interface ChoiceChipsBaseProps<Value extends string = string> {
|
|
7
|
+
options: readonly ChoiceChipOption<Value>[];
|
|
8
|
+
value: Value;
|
|
9
|
+
onValueChange: (value: Value) => void;
|
|
10
|
+
}
|
|
@@ -18,6 +18,9 @@ export interface FeedbackMessages {
|
|
|
18
18
|
/** Search input placeholder. */
|
|
19
19
|
searchPlaceholder: string;
|
|
20
20
|
|
|
21
|
+
/** Accessible label for the open/closed entry status filter. */
|
|
22
|
+
statusFilter: string;
|
|
23
|
+
|
|
21
24
|
/** CTA for opening the create-entry flow.
|
|
22
25
|
* This is shown when enabled kinds are two or more.
|
|
23
26
|
* Use messages.newFeedback for a single-kind entry.
|
|
@@ -2,6 +2,7 @@ import type {
|
|
|
2
2
|
CommentSort,
|
|
3
3
|
EntryKind,
|
|
4
4
|
EntrySort,
|
|
5
|
+
EntryStatusFilter,
|
|
5
6
|
FeedbackComment,
|
|
6
7
|
FeedbackEntry,
|
|
7
8
|
} from "convex-feedback";
|
|
@@ -95,6 +96,9 @@ export interface FeedbackScreenBaseProps {
|
|
|
95
96
|
* entry kind. Disabled by default.
|
|
96
97
|
*/
|
|
97
98
|
collectMetadata?: CollectMetadata;
|
|
99
|
+
|
|
100
|
+
/** Optional host-owned content rendered above the empty-state message. */
|
|
101
|
+
emptyState?: ReactNode;
|
|
98
102
|
}
|
|
99
103
|
|
|
100
104
|
export interface FeedbackScreenRootProps
|
|
@@ -106,9 +110,10 @@ export interface FeedbackScreenRootProps
|
|
|
106
110
|
|
|
107
111
|
export interface FeedbackScreenProviderProps
|
|
108
112
|
extends
|
|
109
|
-
Required<Omit<FeedbackScreenBaseProps, "collectMetadata">>,
|
|
113
|
+
Required<Omit<FeedbackScreenBaseProps, "collectMetadata" | "emptyState">>,
|
|
110
114
|
FeedbackScreenTransformationProps {
|
|
111
115
|
collectMetadata?: CollectMetadata;
|
|
116
|
+
emptyState?: ReactNode;
|
|
112
117
|
collectStandardMetadata: StandardMetadataCollector;
|
|
113
118
|
}
|
|
114
119
|
|
|
@@ -119,11 +124,13 @@ export interface FeedbackScreenBodyContextValue
|
|
|
119
124
|
showForm: boolean;
|
|
120
125
|
isSearching: boolean;
|
|
121
126
|
selectedEntryId: string | null;
|
|
127
|
+
statusFilter: EntryStatusFilter;
|
|
122
128
|
setQuery: React.Dispatch<React.SetStateAction<string>>;
|
|
123
129
|
setDebouncedQuery: React.Dispatch<React.SetStateAction<string>>;
|
|
124
130
|
setShowForm: React.Dispatch<React.SetStateAction<boolean>>;
|
|
125
131
|
setIsSearching: React.Dispatch<React.SetStateAction<boolean>>;
|
|
126
132
|
setSelectedEntryId: React.Dispatch<React.SetStateAction<string | null>>;
|
|
133
|
+
setStatusFilter: React.Dispatch<React.SetStateAction<EntryStatusFilter>>;
|
|
127
134
|
}
|
|
128
135
|
|
|
129
136
|
export type FeedbackScreenBodyProps = FeedbackColorProps;
|
|
@@ -143,6 +150,9 @@ export interface FeedbackScreenListProps {
|
|
|
143
150
|
* @default true
|
|
144
151
|
*/
|
|
145
152
|
showSelectedEntry?: boolean;
|
|
153
|
+
|
|
154
|
+
/** Callback for the empty-state create-entry action. */
|
|
155
|
+
onCreateEntry?: () => void;
|
|
146
156
|
}
|
|
147
157
|
|
|
148
158
|
export interface FeedbackScreenContentProps
|