convex-feedback-ui 0.1.1
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 +187 -0
- package/dist/.tsbuildinfo +1 -0
- package/dist/native/context.d.ts +10 -0
- package/dist/native/context.d.ts.map +1 -0
- package/dist/native/context.js +33 -0
- package/dist/native/context.js.map +1 -0
- package/dist/native/index.d.ts +7 -0
- package/dist/native/index.d.ts.map +1 -0
- package/dist/native/index.js +6 -0
- package/dist/native/index.js.map +1 -0
- package/dist/native/primitives.d.ts +94 -0
- package/dist/native/primitives.d.ts.map +1 -0
- package/dist/native/primitives.js +303 -0
- package/dist/native/primitives.js.map +1 -0
- package/dist/native/screen.d.ts +3 -0
- package/dist/native/screen.d.ts.map +1 -0
- package/dist/native/screen.js +284 -0
- package/dist/native/screen.js.map +1 -0
- package/dist/react/context.d.ts +22 -0
- package/dist/react/context.d.ts.map +1 -0
- package/dist/react/context.js +41 -0
- package/dist/react/context.js.map +1 -0
- package/dist/react/index.d.ts +7 -0
- package/dist/react/index.d.ts.map +1 -0
- package/dist/react/index.js +6 -0
- package/dist/react/index.js.map +1 -0
- package/dist/react/primitives.d.ts +105 -0
- package/dist/react/primitives.d.ts.map +1 -0
- package/dist/react/primitives.js +217 -0
- package/dist/react/primitives.js.map +1 -0
- package/dist/react/screen.d.ts +6 -0
- package/dist/react/screen.d.ts.map +1 -0
- package/dist/react/screen.js +152 -0
- package/dist/react/screen.js.map +1 -0
- package/dist/react/styles.css +299 -0
- package/dist/shared/messages.d.ts +4 -0
- package/dist/shared/messages.d.ts.map +1 -0
- package/dist/shared/messages.js +85 -0
- package/dist/shared/messages.js.map +1 -0
- package/dist/shared/render.d.ts +4 -0
- package/dist/shared/render.d.ts.map +1 -0
- package/dist/shared/render.js +6 -0
- package/dist/shared/render.js.map +1 -0
- package/dist/shared/theme.d.ts +4 -0
- package/dist/shared/theme.d.ts.map +1 -0
- package/dist/shared/theme.js +23 -0
- package/dist/shared/theme.js.map +1 -0
- package/dist/shared/types/board.d.ts +43 -0
- package/dist/shared/types/board.d.ts.map +1 -0
- package/dist/shared/types/board.js +2 -0
- package/dist/shared/types/board.js.map +1 -0
- package/dist/shared/types/comment.d.ts +85 -0
- package/dist/shared/types/comment.d.ts.map +1 -0
- package/dist/shared/types/comment.js +2 -0
- package/dist/shared/types/comment.js.map +1 -0
- package/dist/shared/types/context.d.ts +68 -0
- package/dist/shared/types/context.d.ts.map +1 -0
- package/dist/shared/types/context.js +2 -0
- package/dist/shared/types/context.js.map +1 -0
- package/dist/shared/types/entry.d.ts +41 -0
- package/dist/shared/types/entry.d.ts.map +1 -0
- package/dist/shared/types/entry.js +2 -0
- package/dist/shared/types/entry.js.map +1 -0
- package/dist/shared/types/form.d.ts +25 -0
- package/dist/shared/types/form.d.ts.map +1 -0
- package/dist/shared/types/form.js +2 -0
- package/dist/shared/types/form.js.map +1 -0
- package/dist/shared/types/helpers.d.ts +2 -0
- package/dist/shared/types/helpers.d.ts.map +1 -0
- package/dist/shared/types/helpers.js +2 -0
- package/dist/shared/types/helpers.js.map +1 -0
- package/dist/shared/types/index.d.ts +10 -0
- package/dist/shared/types/index.d.ts.map +1 -0
- package/dist/shared/types/index.js +2 -0
- package/dist/shared/types/index.js.map +1 -0
- package/dist/shared/types/messages.d.ts +117 -0
- package/dist/shared/types/messages.d.ts.map +1 -0
- package/dist/shared/types/messages.js +2 -0
- package/dist/shared/types/messages.js.map +1 -0
- package/dist/shared/types/render.d.ts +12 -0
- package/dist/shared/types/render.d.ts.map +1 -0
- package/dist/shared/types/render.js +2 -0
- package/dist/shared/types/render.js.map +1 -0
- package/dist/shared/types/screen.d.ts +95 -0
- package/dist/shared/types/screen.d.ts.map +1 -0
- package/dist/shared/types/screen.js +2 -0
- package/dist/shared/types/screen.js.map +1 -0
- package/dist/shared/types/theme.d.ts +48 -0
- package/dist/shared/types/theme.d.ts.map +1 -0
- package/dist/shared/types/theme.js +2 -0
- package/dist/shared/types/theme.js.map +1 -0
- package/package.json +58 -0
- package/src/native/README.md +17 -0
- package/src/native/context.tsx +74 -0
- package/src/native/index.ts +39 -0
- package/src/native/primitives.tsx +730 -0
- package/src/native/screen.tsx +831 -0
- package/src/react/README.md +7 -0
- package/src/react/context.tsx +83 -0
- package/src/react/index.ts +42 -0
- package/src/react/primitives.tsx +589 -0
- package/src/react/screen.tsx +633 -0
- package/src/react/styles.css +299 -0
- package/src/shared/README.md +9 -0
- package/src/shared/messages.ts +99 -0
- package/src/shared/render.ts +12 -0
- package/src/shared/theme.ts +27 -0
- package/src/shared/types/board.ts +48 -0
- package/src/shared/types/comment.ts +101 -0
- package/src/shared/types/context.ts +81 -0
- package/src/shared/types/entry.ts +48 -0
- package/src/shared/types/form.ts +27 -0
- package/src/shared/types/helpers.ts +1 -0
- package/src/shared/types/index.ts +45 -0
- package/src/shared/types/messages.ts +160 -0
- package/src/shared/types/render.ts +12 -0
- package/src/shared/types/screen.ts +123 -0
- package/src/shared/types/theme.ts +61 -0
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
# React web UI
|
|
2
|
+
|
|
3
|
+
The web layer is CSS-framework agnostic. Import `convex-feedback-ui/styles.css` for the default appearance or omit it and style the semantic classes yourself.
|
|
4
|
+
|
|
5
|
+
Default selectors are scoped beneath `.cf-board` and use low-specificity `:where(...)` selectors so host CSS, CSS Modules, Tailwind classes, or inline styles can override them without a specificity fight.
|
|
6
|
+
|
|
7
|
+
Use compound primitives for custom layouts or `FeedbackScreen` for the prebuilt experience. Interactive leaf primitives accept render-function children to replace their visual implementation while retaining typed state and actions.
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { createContext, useContext, useMemo, type CSSProperties } from "react";
|
|
4
|
+
|
|
5
|
+
import { mergeFeedbackMessages } from "../shared/messages.js";
|
|
6
|
+
import { mergeFeedbackTheme } from "../shared/theme.js";
|
|
7
|
+
import type {
|
|
8
|
+
FeedbackColorProps,
|
|
9
|
+
FeedbackProviderProps,
|
|
10
|
+
FeedbackTheme,
|
|
11
|
+
FeedbackUiContextValue,
|
|
12
|
+
} from "../shared/types";
|
|
13
|
+
|
|
14
|
+
const FeedbackUiContext = createContext<FeedbackUiContextValue | null>(null);
|
|
15
|
+
|
|
16
|
+
export function FeedbackProvider({
|
|
17
|
+
children,
|
|
18
|
+
messages,
|
|
19
|
+
theme,
|
|
20
|
+
unstyled = false,
|
|
21
|
+
}: FeedbackProviderProps) {
|
|
22
|
+
const value = useMemo<FeedbackUiContextValue>(
|
|
23
|
+
() => ({
|
|
24
|
+
messages: mergeFeedbackMessages(messages),
|
|
25
|
+
theme: mergeFeedbackTheme(theme),
|
|
26
|
+
unstyled,
|
|
27
|
+
}),
|
|
28
|
+
[messages, theme, unstyled],
|
|
29
|
+
);
|
|
30
|
+
return (
|
|
31
|
+
<FeedbackUiContext.Provider value={value}>
|
|
32
|
+
{children}
|
|
33
|
+
</FeedbackUiContext.Provider>
|
|
34
|
+
);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export function useFeedbackUi(): FeedbackUiContextValue {
|
|
38
|
+
const context = useContext(FeedbackUiContext);
|
|
39
|
+
if (context === null) {
|
|
40
|
+
return {
|
|
41
|
+
messages: mergeFeedbackMessages(),
|
|
42
|
+
theme: mergeFeedbackTheme(),
|
|
43
|
+
unstyled: false,
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
return context;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* CSS custom properties produced for a feedback board.
|
|
51
|
+
*/
|
|
52
|
+
export interface FeedbackCssVariables extends CSSProperties {
|
|
53
|
+
"--cf-primary"?: string;
|
|
54
|
+
"--cf-background"?: string;
|
|
55
|
+
"--cf-surface"?: string;
|
|
56
|
+
"--cf-surface-muted"?: string;
|
|
57
|
+
"--cf-text"?: string;
|
|
58
|
+
"--cf-muted"?: string;
|
|
59
|
+
"--cf-border"?: string;
|
|
60
|
+
"--cf-danger"?: string;
|
|
61
|
+
"--cf-success"?: string;
|
|
62
|
+
"--cf-radius"?: string;
|
|
63
|
+
"--cf-space"?: string;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export function feedbackCssVariables(
|
|
67
|
+
theme: FeedbackTheme,
|
|
68
|
+
colors: FeedbackColorProps,
|
|
69
|
+
): FeedbackCssVariables {
|
|
70
|
+
return {
|
|
71
|
+
"--cf-primary": colors.primaryColor ?? theme.colors.primary,
|
|
72
|
+
"--cf-background": colors.backgroundColor ?? theme.colors.background,
|
|
73
|
+
"--cf-surface": colors.surfaceColor ?? theme.colors.surface,
|
|
74
|
+
"--cf-surface-muted": theme.colors.surfaceMuted,
|
|
75
|
+
"--cf-text": colors.textColor ?? theme.colors.text,
|
|
76
|
+
"--cf-muted": colors.mutedColor ?? theme.colors.mutedText,
|
|
77
|
+
"--cf-border": colors.borderColor ?? theme.colors.border,
|
|
78
|
+
"--cf-danger": colors.dangerColor ?? theme.colors.danger,
|
|
79
|
+
"--cf-success": theme.colors.success,
|
|
80
|
+
"--cf-radius": `${theme.radius}px`,
|
|
81
|
+
"--cf-space": `${theme.spacing}px`,
|
|
82
|
+
};
|
|
83
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
export {
|
|
2
|
+
englishFeedbackMessages,
|
|
3
|
+
mergeFeedbackMessages,
|
|
4
|
+
} from "../shared/messages.js";
|
|
5
|
+
export { defaultFeedbackTheme, mergeFeedbackTheme } from "../shared/theme.js";
|
|
6
|
+
export type {
|
|
7
|
+
BoardSearchState,
|
|
8
|
+
CommentActionState,
|
|
9
|
+
CommentLikeState,
|
|
10
|
+
EntryUpvoteState,
|
|
11
|
+
FeedbackColorProps,
|
|
12
|
+
FeedbackColors,
|
|
13
|
+
FeedbackMessageOverrides,
|
|
14
|
+
FeedbackMessages,
|
|
15
|
+
FeedbackProviderProps,
|
|
16
|
+
FeedbackTheme,
|
|
17
|
+
FeedbackThemeOverride,
|
|
18
|
+
FormSubmitState,
|
|
19
|
+
RenderChildren,
|
|
20
|
+
RepliesButtonState,
|
|
21
|
+
} from "../shared/types";
|
|
22
|
+
export {
|
|
23
|
+
feedbackCssVariables,
|
|
24
|
+
FeedbackProvider,
|
|
25
|
+
useFeedbackUi,
|
|
26
|
+
type FeedbackCssVariables,
|
|
27
|
+
} from "./context.js";
|
|
28
|
+
export {
|
|
29
|
+
Comment,
|
|
30
|
+
FeedbackBoard,
|
|
31
|
+
FeedbackEntry,
|
|
32
|
+
FeedbackForm,
|
|
33
|
+
type BoardRootProps,
|
|
34
|
+
type BoardSearchProps,
|
|
35
|
+
type CommentActionProps,
|
|
36
|
+
type CommentLikeProps,
|
|
37
|
+
type EntryRootProps,
|
|
38
|
+
type EntryUpvoteProps,
|
|
39
|
+
type FormSubmitProps,
|
|
40
|
+
type RepliesButtonProps,
|
|
41
|
+
} from "./primitives.js";
|
|
42
|
+
export { FeedbackScreen, type FeedbackScreenProps } from "./screen.js";
|
|
@@ -0,0 +1,589 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import type {
|
|
4
|
+
FeedbackComment,
|
|
5
|
+
FeedbackEntry as FeedbackEntryData,
|
|
6
|
+
} from "convex-feedback";
|
|
7
|
+
import {
|
|
8
|
+
createContext,
|
|
9
|
+
useContext,
|
|
10
|
+
type ButtonHTMLAttributes,
|
|
11
|
+
type FormHTMLAttributes,
|
|
12
|
+
type HTMLAttributes,
|
|
13
|
+
type InputHTMLAttributes,
|
|
14
|
+
type SelectHTMLAttributes,
|
|
15
|
+
type TextareaHTMLAttributes,
|
|
16
|
+
} from "react";
|
|
17
|
+
|
|
18
|
+
import type {
|
|
19
|
+
BoardRootBaseProps,
|
|
20
|
+
BoardSearchBaseProps,
|
|
21
|
+
BoardSearchState,
|
|
22
|
+
CommentActionBaseProps,
|
|
23
|
+
CommentActionState,
|
|
24
|
+
CommentLikeBaseProps,
|
|
25
|
+
CommentLikeState,
|
|
26
|
+
CommentRootBaseProps,
|
|
27
|
+
EntryRootBaseProps,
|
|
28
|
+
EntryUpvoteBaseProps,
|
|
29
|
+
EntryUpvoteState,
|
|
30
|
+
FormSubmitBaseProps,
|
|
31
|
+
FormSubmitState,
|
|
32
|
+
RepliesButtonBaseProps,
|
|
33
|
+
RepliesButtonState,
|
|
34
|
+
} from "../shared/types";
|
|
35
|
+
import { feedbackCssVariables, useFeedbackUi } from "./context.js";
|
|
36
|
+
|
|
37
|
+
function classes(
|
|
38
|
+
unstyled: boolean,
|
|
39
|
+
base: string,
|
|
40
|
+
className?: string,
|
|
41
|
+
): string | undefined {
|
|
42
|
+
if (unstyled) return className;
|
|
43
|
+
return className === undefined ? base : `${base} ${className}`;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Props for `FeedbackBoard.Root`.
|
|
48
|
+
*/
|
|
49
|
+
export interface BoardRootProps
|
|
50
|
+
extends HTMLAttributes<HTMLDivElement>, BoardRootBaseProps {}
|
|
51
|
+
|
|
52
|
+
function BoardRoot({
|
|
53
|
+
primaryColor,
|
|
54
|
+
backgroundColor,
|
|
55
|
+
surfaceColor,
|
|
56
|
+
textColor,
|
|
57
|
+
mutedColor,
|
|
58
|
+
borderColor,
|
|
59
|
+
dangerColor,
|
|
60
|
+
unstyled: localUnstyled,
|
|
61
|
+
className,
|
|
62
|
+
style,
|
|
63
|
+
...props
|
|
64
|
+
}: BoardRootProps) {
|
|
65
|
+
const ui = useFeedbackUi();
|
|
66
|
+
const unstyled = localUnstyled ?? ui.unstyled;
|
|
67
|
+
const variables = feedbackCssVariables(ui.theme, {
|
|
68
|
+
...(primaryColor === undefined ? {} : { primaryColor }),
|
|
69
|
+
...(backgroundColor === undefined ? {} : { backgroundColor }),
|
|
70
|
+
...(surfaceColor === undefined ? {} : { surfaceColor }),
|
|
71
|
+
...(textColor === undefined ? {} : { textColor }),
|
|
72
|
+
...(mutedColor === undefined ? {} : { mutedColor }),
|
|
73
|
+
...(borderColor === undefined ? {} : { borderColor }),
|
|
74
|
+
...(dangerColor === undefined ? {} : { dangerColor }),
|
|
75
|
+
});
|
|
76
|
+
return (
|
|
77
|
+
<div
|
|
78
|
+
{...props}
|
|
79
|
+
className={classes(unstyled, "cf-board", className)}
|
|
80
|
+
style={{ ...variables, ...style }}
|
|
81
|
+
/>
|
|
82
|
+
);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function BoardHeader(props: HTMLAttributes<HTMLDivElement>) {
|
|
86
|
+
const { unstyled } = useFeedbackUi();
|
|
87
|
+
return (
|
|
88
|
+
<div
|
|
89
|
+
{...props}
|
|
90
|
+
className={classes(unstyled, "cf-board__header", props.className)}
|
|
91
|
+
/>
|
|
92
|
+
);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function BoardTitle(props: HTMLAttributes<HTMLHeadingElement>) {
|
|
96
|
+
const { messages, unstyled } = useFeedbackUi();
|
|
97
|
+
return (
|
|
98
|
+
<h2
|
|
99
|
+
{...props}
|
|
100
|
+
className={classes(unstyled, "cf-board__title", props.className)}
|
|
101
|
+
>
|
|
102
|
+
{props.children ?? messages.board.title}
|
|
103
|
+
</h2>
|
|
104
|
+
);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* Props for `FeedbackBoard.Search`.
|
|
109
|
+
*/
|
|
110
|
+
export interface BoardSearchProps
|
|
111
|
+
extends
|
|
112
|
+
Omit<
|
|
113
|
+
InputHTMLAttributes<HTMLInputElement>,
|
|
114
|
+
"children" | "value" | "onChange"
|
|
115
|
+
>,
|
|
116
|
+
BoardSearchBaseProps {}
|
|
117
|
+
|
|
118
|
+
function BoardSearch({
|
|
119
|
+
value,
|
|
120
|
+
onValueChange,
|
|
121
|
+
children,
|
|
122
|
+
className,
|
|
123
|
+
...props
|
|
124
|
+
}: BoardSearchProps) {
|
|
125
|
+
const { messages, unstyled } = useFeedbackUi();
|
|
126
|
+
const state: BoardSearchState = { value, setValue: onValueChange };
|
|
127
|
+
if (typeof children === "function") return <>{children(state)}</>;
|
|
128
|
+
return (
|
|
129
|
+
<input
|
|
130
|
+
{...props}
|
|
131
|
+
className={classes(unstyled, "cf-search", className)}
|
|
132
|
+
value={value}
|
|
133
|
+
onChange={(event) => onValueChange(event.currentTarget.value)}
|
|
134
|
+
placeholder={props.placeholder ?? messages.board.searchPlaceholder}
|
|
135
|
+
/>
|
|
136
|
+
);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
function BoardList(props: HTMLAttributes<HTMLDivElement>) {
|
|
140
|
+
const { unstyled } = useFeedbackUi();
|
|
141
|
+
return (
|
|
142
|
+
<div
|
|
143
|
+
{...props}
|
|
144
|
+
className={classes(unstyled, "cf-board__list", props.className)}
|
|
145
|
+
/>
|
|
146
|
+
);
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
export const FeedbackBoard = {
|
|
150
|
+
Root: BoardRoot,
|
|
151
|
+
Header: BoardHeader,
|
|
152
|
+
Title: BoardTitle,
|
|
153
|
+
Search: BoardSearch,
|
|
154
|
+
List: BoardList,
|
|
155
|
+
};
|
|
156
|
+
|
|
157
|
+
const EntryContext = createContext<FeedbackEntryData | null>(null);
|
|
158
|
+
|
|
159
|
+
function useEntryContext(): FeedbackEntryData {
|
|
160
|
+
const entry = useContext(EntryContext);
|
|
161
|
+
if (entry === null)
|
|
162
|
+
throw new Error(
|
|
163
|
+
"FeedbackEntry compound must be inside FeedbackEntry.Root.",
|
|
164
|
+
);
|
|
165
|
+
return entry;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/**
|
|
169
|
+
* Props for `FeedbackEntry.Root`.
|
|
170
|
+
*/
|
|
171
|
+
export interface EntryRootProps
|
|
172
|
+
extends HTMLAttributes<HTMLElement>, EntryRootBaseProps {}
|
|
173
|
+
|
|
174
|
+
function EntryRoot({ entry, className, ...props }: EntryRootProps) {
|
|
175
|
+
const { unstyled } = useFeedbackUi();
|
|
176
|
+
return (
|
|
177
|
+
<EntryContext.Provider value={entry}>
|
|
178
|
+
<article
|
|
179
|
+
{...props}
|
|
180
|
+
className={classes(unstyled, "cf-entry", className)}
|
|
181
|
+
/>
|
|
182
|
+
</EntryContext.Provider>
|
|
183
|
+
);
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
/**
|
|
187
|
+
* Props for `FeedbackEntry.Upvote`.
|
|
188
|
+
*/
|
|
189
|
+
export interface EntryUpvoteProps
|
|
190
|
+
extends
|
|
191
|
+
Omit<
|
|
192
|
+
ButtonHTMLAttributes<HTMLButtonElement>,
|
|
193
|
+
"children" | "onClick" | "onToggle"
|
|
194
|
+
>,
|
|
195
|
+
EntryUpvoteBaseProps {}
|
|
196
|
+
|
|
197
|
+
function EntryUpvote({
|
|
198
|
+
onToggle,
|
|
199
|
+
children,
|
|
200
|
+
"aria-label": accessibilityLabel,
|
|
201
|
+
className,
|
|
202
|
+
...props
|
|
203
|
+
}: EntryUpvoteProps) {
|
|
204
|
+
const entry = useEntryContext();
|
|
205
|
+
const { messages, unstyled } = useFeedbackUi();
|
|
206
|
+
const toggle = () => onToggle(!entry.viewerHasUpvoted);
|
|
207
|
+
const state: EntryUpvoteState = {
|
|
208
|
+
entry,
|
|
209
|
+
active: entry.viewerHasUpvoted,
|
|
210
|
+
count: entry.upvoteCount,
|
|
211
|
+
toggle,
|
|
212
|
+
};
|
|
213
|
+
if (typeof children === "function") return <>{children(state)}</>;
|
|
214
|
+
return (
|
|
215
|
+
<button
|
|
216
|
+
{...props}
|
|
217
|
+
type={props.type ?? "button"}
|
|
218
|
+
className={classes(unstyled, "cf-upvote", className)}
|
|
219
|
+
data-active={entry.viewerHasUpvoted ? "true" : "false"}
|
|
220
|
+
aria-label={
|
|
221
|
+
accessibilityLabel ??
|
|
222
|
+
(entry.viewerHasUpvoted
|
|
223
|
+
? messages.entry.removeUpvote
|
|
224
|
+
: messages.entry.upvote)
|
|
225
|
+
}
|
|
226
|
+
onClick={toggle}
|
|
227
|
+
>
|
|
228
|
+
{children ?? (
|
|
229
|
+
<>
|
|
230
|
+
<span aria-hidden="true">▲</span>
|
|
231
|
+
<strong>{entry.upvoteCount}</strong>
|
|
232
|
+
</>
|
|
233
|
+
)}
|
|
234
|
+
</button>
|
|
235
|
+
);
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
function EntryContent(props: HTMLAttributes<HTMLDivElement>) {
|
|
239
|
+
const { unstyled } = useFeedbackUi();
|
|
240
|
+
return (
|
|
241
|
+
<div
|
|
242
|
+
{...props}
|
|
243
|
+
className={classes(unstyled, "cf-entry__content", props.className)}
|
|
244
|
+
/>
|
|
245
|
+
);
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
function EntryKind(props: HTMLAttributes<HTMLSpanElement>) {
|
|
249
|
+
const entry = useEntryContext();
|
|
250
|
+
const { messages, unstyled } = useFeedbackUi();
|
|
251
|
+
|
|
252
|
+
return (
|
|
253
|
+
<span
|
|
254
|
+
{...props}
|
|
255
|
+
className={classes(unstyled, "cf-entry__kind", props.className)}
|
|
256
|
+
>
|
|
257
|
+
{props.children ?? messages.kinds[entry.kind]}
|
|
258
|
+
</span>
|
|
259
|
+
);
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
function EntryTitle(props: HTMLAttributes<HTMLHeadingElement>) {
|
|
263
|
+
const entry = useEntryContext();
|
|
264
|
+
const { unstyled } = useFeedbackUi();
|
|
265
|
+
return (
|
|
266
|
+
<h3
|
|
267
|
+
{...props}
|
|
268
|
+
className={classes(unstyled, "cf-entry__title", props.className)}
|
|
269
|
+
>
|
|
270
|
+
{props.children ?? entry.title}
|
|
271
|
+
</h3>
|
|
272
|
+
);
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
function EntryBody(props: HTMLAttributes<HTMLParagraphElement>) {
|
|
276
|
+
const entry = useEntryContext();
|
|
277
|
+
const { unstyled } = useFeedbackUi();
|
|
278
|
+
return (
|
|
279
|
+
<p
|
|
280
|
+
{...props}
|
|
281
|
+
className={classes(unstyled, "cf-entry__body", props.className)}
|
|
282
|
+
>
|
|
283
|
+
{props.children ?? entry.body}
|
|
284
|
+
</p>
|
|
285
|
+
);
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
function EntryStatus(props: HTMLAttributes<HTMLSpanElement>) {
|
|
289
|
+
const entry = useEntryContext();
|
|
290
|
+
const { messages, unstyled } = useFeedbackUi();
|
|
291
|
+
return (
|
|
292
|
+
<span
|
|
293
|
+
{...props}
|
|
294
|
+
className={classes(unstyled, "cf-status", props.className)}
|
|
295
|
+
>
|
|
296
|
+
{props.children ?? messages.statuses[entry.status]}
|
|
297
|
+
</span>
|
|
298
|
+
);
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
function EntryCommentCount(props: HTMLAttributes<HTMLSpanElement>) {
|
|
302
|
+
const entry = useEntryContext();
|
|
303
|
+
const { messages, unstyled } = useFeedbackUi();
|
|
304
|
+
return (
|
|
305
|
+
<span
|
|
306
|
+
{...props}
|
|
307
|
+
className={classes(unstyled, "cf-entry__comments", props.className)}
|
|
308
|
+
>
|
|
309
|
+
{props.children ?? messages.entry.comments(entry.commentCount)}
|
|
310
|
+
</span>
|
|
311
|
+
);
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
export const FeedbackEntry = {
|
|
315
|
+
Root: EntryRoot,
|
|
316
|
+
Upvote: EntryUpvote,
|
|
317
|
+
Content: EntryContent,
|
|
318
|
+
Kind: EntryKind,
|
|
319
|
+
Title: EntryTitle,
|
|
320
|
+
Body: EntryBody,
|
|
321
|
+
Status: EntryStatus,
|
|
322
|
+
CommentCount: EntryCommentCount,
|
|
323
|
+
};
|
|
324
|
+
|
|
325
|
+
const CommentContext = createContext<FeedbackComment | null>(null);
|
|
326
|
+
|
|
327
|
+
function useCommentContext(): FeedbackComment {
|
|
328
|
+
const comment = useContext(CommentContext);
|
|
329
|
+
if (comment === null)
|
|
330
|
+
throw new Error("Comment compound must be inside Comment.Root.");
|
|
331
|
+
return comment;
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
/**
|
|
335
|
+
* Props for `Comment.Root`.
|
|
336
|
+
*/
|
|
337
|
+
export interface CommentRootProps
|
|
338
|
+
extends HTMLAttributes<HTMLElement>, CommentRootBaseProps {}
|
|
339
|
+
|
|
340
|
+
function CommentRoot({ comment, className, ...props }: CommentRootProps) {
|
|
341
|
+
const { unstyled } = useFeedbackUi();
|
|
342
|
+
return (
|
|
343
|
+
<CommentContext.Provider value={comment}>
|
|
344
|
+
<article
|
|
345
|
+
{...props}
|
|
346
|
+
className={classes(unstyled, "cf-comment", className)}
|
|
347
|
+
/>
|
|
348
|
+
</CommentContext.Provider>
|
|
349
|
+
);
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
function CommentBody(props: HTMLAttributes<HTMLParagraphElement>) {
|
|
353
|
+
const comment = useCommentContext();
|
|
354
|
+
const { messages, unstyled } = useFeedbackUi();
|
|
355
|
+
return (
|
|
356
|
+
<p
|
|
357
|
+
{...props}
|
|
358
|
+
className={classes(unstyled, "cf-comment__body", props.className)}
|
|
359
|
+
>
|
|
360
|
+
{props.children ?? comment.body ?? messages.comments.deleted}
|
|
361
|
+
</p>
|
|
362
|
+
);
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
/**
|
|
366
|
+
* Props for `Comment.Like`.
|
|
367
|
+
*/
|
|
368
|
+
export interface CommentLikeProps
|
|
369
|
+
extends
|
|
370
|
+
Omit<
|
|
371
|
+
ButtonHTMLAttributes<HTMLButtonElement>,
|
|
372
|
+
"children" | "onClick" | "onToggle"
|
|
373
|
+
>,
|
|
374
|
+
CommentLikeBaseProps {}
|
|
375
|
+
|
|
376
|
+
function CommentLike({
|
|
377
|
+
onToggle,
|
|
378
|
+
children,
|
|
379
|
+
"aria-label": accessibilityLabel,
|
|
380
|
+
className,
|
|
381
|
+
...props
|
|
382
|
+
}: CommentLikeProps) {
|
|
383
|
+
const comment = useCommentContext();
|
|
384
|
+
const { messages, unstyled } = useFeedbackUi();
|
|
385
|
+
const toggle = () => onToggle(!comment.viewerHasLiked);
|
|
386
|
+
const state: CommentLikeState = {
|
|
387
|
+
comment,
|
|
388
|
+
active: comment.viewerHasLiked,
|
|
389
|
+
count: comment.likeCount,
|
|
390
|
+
toggle,
|
|
391
|
+
};
|
|
392
|
+
if (typeof children === "function") return <>{children(state)}</>;
|
|
393
|
+
return (
|
|
394
|
+
<button
|
|
395
|
+
{...props}
|
|
396
|
+
type={props.type ?? "button"}
|
|
397
|
+
className={classes(unstyled, "cf-comment__like", className)}
|
|
398
|
+
data-active={comment.viewerHasLiked ? "true" : "false"}
|
|
399
|
+
aria-label={
|
|
400
|
+
accessibilityLabel ??
|
|
401
|
+
(comment.viewerHasLiked
|
|
402
|
+
? messages.comments.unlike
|
|
403
|
+
: messages.comments.like)
|
|
404
|
+
}
|
|
405
|
+
onClick={toggle}
|
|
406
|
+
>
|
|
407
|
+
{children ?? (
|
|
408
|
+
<>
|
|
409
|
+
<span aria-hidden="true">{comment.viewerHasLiked ? "♥" : "♡"}</span>
|
|
410
|
+
<span>{comment.likeCount}</span>
|
|
411
|
+
</>
|
|
412
|
+
)}
|
|
413
|
+
</button>
|
|
414
|
+
);
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
/**
|
|
418
|
+
* Props for a simple comment action.
|
|
419
|
+
*/
|
|
420
|
+
export interface CommentActionProps
|
|
421
|
+
extends
|
|
422
|
+
Omit<
|
|
423
|
+
ButtonHTMLAttributes<HTMLButtonElement>,
|
|
424
|
+
"children" | "onClick" | "onToggle"
|
|
425
|
+
>,
|
|
426
|
+
CommentActionBaseProps {}
|
|
427
|
+
|
|
428
|
+
function CommentReply({
|
|
429
|
+
onActivate,
|
|
430
|
+
children,
|
|
431
|
+
className,
|
|
432
|
+
...props
|
|
433
|
+
}: CommentActionProps) {
|
|
434
|
+
const comment = useCommentContext();
|
|
435
|
+
const { messages, unstyled } = useFeedbackUi();
|
|
436
|
+
const state: CommentActionState = { comment, activate: onActivate };
|
|
437
|
+
return typeof children === "function" ? (
|
|
438
|
+
<>{children(state)}</>
|
|
439
|
+
) : (
|
|
440
|
+
<button
|
|
441
|
+
{...props}
|
|
442
|
+
type={props.type ?? "button"}
|
|
443
|
+
className={classes(unstyled, "cf-comment__action", className)}
|
|
444
|
+
onClick={onActivate}
|
|
445
|
+
>
|
|
446
|
+
{children ?? messages.comments.reply}
|
|
447
|
+
</button>
|
|
448
|
+
);
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
/**
|
|
452
|
+
* Props for `Comment.RepliesButton`.
|
|
453
|
+
*/
|
|
454
|
+
export interface RepliesButtonProps
|
|
455
|
+
extends
|
|
456
|
+
Omit<
|
|
457
|
+
ButtonHTMLAttributes<HTMLButtonElement>,
|
|
458
|
+
"children" | "onClick" | "onToggle"
|
|
459
|
+
>,
|
|
460
|
+
RepliesButtonBaseProps {}
|
|
461
|
+
|
|
462
|
+
function CommentRepliesButton({
|
|
463
|
+
expanded,
|
|
464
|
+
onExpandedChange,
|
|
465
|
+
children,
|
|
466
|
+
className,
|
|
467
|
+
...props
|
|
468
|
+
}: RepliesButtonProps) {
|
|
469
|
+
const comment = useCommentContext();
|
|
470
|
+
const { messages, unstyled } = useFeedbackUi();
|
|
471
|
+
const activate = () => onExpandedChange(!expanded);
|
|
472
|
+
const state: RepliesButtonState = {
|
|
473
|
+
comment,
|
|
474
|
+
expanded,
|
|
475
|
+
count: comment.replyCount,
|
|
476
|
+
activate,
|
|
477
|
+
};
|
|
478
|
+
if (typeof children === "function") return <>{children(state)}</>;
|
|
479
|
+
if (comment.replyCount === 0) return null;
|
|
480
|
+
return (
|
|
481
|
+
<button
|
|
482
|
+
{...props}
|
|
483
|
+
type={props.type ?? "button"}
|
|
484
|
+
className={classes(unstyled, "cf-comment__replies", className)}
|
|
485
|
+
onClick={activate}
|
|
486
|
+
>
|
|
487
|
+
{children ??
|
|
488
|
+
(expanded
|
|
489
|
+
? messages.comments.hideReplies
|
|
490
|
+
: messages.comments.viewReplies(comment.replyCount))}
|
|
491
|
+
</button>
|
|
492
|
+
);
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
function CommentChildren(props: HTMLAttributes<HTMLDivElement>) {
|
|
496
|
+
const { unstyled } = useFeedbackUi();
|
|
497
|
+
return (
|
|
498
|
+
<div
|
|
499
|
+
{...props}
|
|
500
|
+
className={classes(unstyled, "cf-comment__children", props.className)}
|
|
501
|
+
/>
|
|
502
|
+
);
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
export const Comment = {
|
|
506
|
+
Root: CommentRoot,
|
|
507
|
+
Body: CommentBody,
|
|
508
|
+
Like: CommentLike,
|
|
509
|
+
Reply: CommentReply,
|
|
510
|
+
RepliesButton: CommentRepliesButton,
|
|
511
|
+
Children: CommentChildren,
|
|
512
|
+
};
|
|
513
|
+
|
|
514
|
+
function FormRoot(props: FormHTMLAttributes<HTMLFormElement>) {
|
|
515
|
+
const { unstyled } = useFeedbackUi();
|
|
516
|
+
return (
|
|
517
|
+
<form
|
|
518
|
+
{...props}
|
|
519
|
+
className={classes(unstyled, "cf-form", props.className)}
|
|
520
|
+
/>
|
|
521
|
+
);
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
function FormInput(props: InputHTMLAttributes<HTMLInputElement>) {
|
|
525
|
+
const { unstyled } = useFeedbackUi();
|
|
526
|
+
return (
|
|
527
|
+
<input
|
|
528
|
+
{...props}
|
|
529
|
+
className={classes(unstyled, "cf-input", props.className)}
|
|
530
|
+
/>
|
|
531
|
+
);
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
function FormTextarea(props: TextareaHTMLAttributes<HTMLTextAreaElement>) {
|
|
535
|
+
const { unstyled } = useFeedbackUi();
|
|
536
|
+
return (
|
|
537
|
+
<textarea
|
|
538
|
+
{...props}
|
|
539
|
+
className={classes(unstyled, "cf-textarea", props.className)}
|
|
540
|
+
/>
|
|
541
|
+
);
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
function FormSelect(props: SelectHTMLAttributes<HTMLSelectElement>) {
|
|
545
|
+
const { unstyled } = useFeedbackUi();
|
|
546
|
+
return (
|
|
547
|
+
<select
|
|
548
|
+
{...props}
|
|
549
|
+
className={classes(unstyled, "cf-select", props.className)}
|
|
550
|
+
/>
|
|
551
|
+
);
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
/**
|
|
555
|
+
* Props for `FeedbackForm.Submit`.
|
|
556
|
+
*/
|
|
557
|
+
export interface FormSubmitProps
|
|
558
|
+
extends
|
|
559
|
+
Omit<ButtonHTMLAttributes<HTMLButtonElement>, "children">,
|
|
560
|
+
FormSubmitBaseProps {}
|
|
561
|
+
|
|
562
|
+
function FormSubmit({
|
|
563
|
+
submitting = false,
|
|
564
|
+
children,
|
|
565
|
+
className,
|
|
566
|
+
...props
|
|
567
|
+
}: FormSubmitProps) {
|
|
568
|
+
const { messages, unstyled } = useFeedbackUi();
|
|
569
|
+
const state: FormSubmitState = { submitting };
|
|
570
|
+
if (typeof children === "function") return <>{children(state)}</>;
|
|
571
|
+
return (
|
|
572
|
+
<button
|
|
573
|
+
{...props}
|
|
574
|
+
type={props.type ?? "submit"}
|
|
575
|
+
disabled={props.disabled ?? submitting}
|
|
576
|
+
className={classes(unstyled, "cf-button cf-button--primary", className)}
|
|
577
|
+
>
|
|
578
|
+
{children ?? messages.form.submit}
|
|
579
|
+
</button>
|
|
580
|
+
);
|
|
581
|
+
}
|
|
582
|
+
|
|
583
|
+
export const FeedbackForm = {
|
|
584
|
+
Root: FormRoot,
|
|
585
|
+
Input: FormInput,
|
|
586
|
+
Textarea: FormTextarea,
|
|
587
|
+
Select: FormSelect,
|
|
588
|
+
Submit: FormSubmit,
|
|
589
|
+
};
|