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.
Files changed (118) hide show
  1. package/README.md +187 -0
  2. package/dist/.tsbuildinfo +1 -0
  3. package/dist/native/context.d.ts +10 -0
  4. package/dist/native/context.d.ts.map +1 -0
  5. package/dist/native/context.js +33 -0
  6. package/dist/native/context.js.map +1 -0
  7. package/dist/native/index.d.ts +7 -0
  8. package/dist/native/index.d.ts.map +1 -0
  9. package/dist/native/index.js +6 -0
  10. package/dist/native/index.js.map +1 -0
  11. package/dist/native/primitives.d.ts +94 -0
  12. package/dist/native/primitives.d.ts.map +1 -0
  13. package/dist/native/primitives.js +303 -0
  14. package/dist/native/primitives.js.map +1 -0
  15. package/dist/native/screen.d.ts +3 -0
  16. package/dist/native/screen.d.ts.map +1 -0
  17. package/dist/native/screen.js +284 -0
  18. package/dist/native/screen.js.map +1 -0
  19. package/dist/react/context.d.ts +22 -0
  20. package/dist/react/context.d.ts.map +1 -0
  21. package/dist/react/context.js +41 -0
  22. package/dist/react/context.js.map +1 -0
  23. package/dist/react/index.d.ts +7 -0
  24. package/dist/react/index.d.ts.map +1 -0
  25. package/dist/react/index.js +6 -0
  26. package/dist/react/index.js.map +1 -0
  27. package/dist/react/primitives.d.ts +105 -0
  28. package/dist/react/primitives.d.ts.map +1 -0
  29. package/dist/react/primitives.js +217 -0
  30. package/dist/react/primitives.js.map +1 -0
  31. package/dist/react/screen.d.ts +6 -0
  32. package/dist/react/screen.d.ts.map +1 -0
  33. package/dist/react/screen.js +152 -0
  34. package/dist/react/screen.js.map +1 -0
  35. package/dist/react/styles.css +299 -0
  36. package/dist/shared/messages.d.ts +4 -0
  37. package/dist/shared/messages.d.ts.map +1 -0
  38. package/dist/shared/messages.js +85 -0
  39. package/dist/shared/messages.js.map +1 -0
  40. package/dist/shared/render.d.ts +4 -0
  41. package/dist/shared/render.d.ts.map +1 -0
  42. package/dist/shared/render.js +6 -0
  43. package/dist/shared/render.js.map +1 -0
  44. package/dist/shared/theme.d.ts +4 -0
  45. package/dist/shared/theme.d.ts.map +1 -0
  46. package/dist/shared/theme.js +23 -0
  47. package/dist/shared/theme.js.map +1 -0
  48. package/dist/shared/types/board.d.ts +43 -0
  49. package/dist/shared/types/board.d.ts.map +1 -0
  50. package/dist/shared/types/board.js +2 -0
  51. package/dist/shared/types/board.js.map +1 -0
  52. package/dist/shared/types/comment.d.ts +85 -0
  53. package/dist/shared/types/comment.d.ts.map +1 -0
  54. package/dist/shared/types/comment.js +2 -0
  55. package/dist/shared/types/comment.js.map +1 -0
  56. package/dist/shared/types/context.d.ts +68 -0
  57. package/dist/shared/types/context.d.ts.map +1 -0
  58. package/dist/shared/types/context.js +2 -0
  59. package/dist/shared/types/context.js.map +1 -0
  60. package/dist/shared/types/entry.d.ts +41 -0
  61. package/dist/shared/types/entry.d.ts.map +1 -0
  62. package/dist/shared/types/entry.js +2 -0
  63. package/dist/shared/types/entry.js.map +1 -0
  64. package/dist/shared/types/form.d.ts +25 -0
  65. package/dist/shared/types/form.d.ts.map +1 -0
  66. package/dist/shared/types/form.js +2 -0
  67. package/dist/shared/types/form.js.map +1 -0
  68. package/dist/shared/types/helpers.d.ts +2 -0
  69. package/dist/shared/types/helpers.d.ts.map +1 -0
  70. package/dist/shared/types/helpers.js +2 -0
  71. package/dist/shared/types/helpers.js.map +1 -0
  72. package/dist/shared/types/index.d.ts +10 -0
  73. package/dist/shared/types/index.d.ts.map +1 -0
  74. package/dist/shared/types/index.js +2 -0
  75. package/dist/shared/types/index.js.map +1 -0
  76. package/dist/shared/types/messages.d.ts +117 -0
  77. package/dist/shared/types/messages.d.ts.map +1 -0
  78. package/dist/shared/types/messages.js +2 -0
  79. package/dist/shared/types/messages.js.map +1 -0
  80. package/dist/shared/types/render.d.ts +12 -0
  81. package/dist/shared/types/render.d.ts.map +1 -0
  82. package/dist/shared/types/render.js +2 -0
  83. package/dist/shared/types/render.js.map +1 -0
  84. package/dist/shared/types/screen.d.ts +95 -0
  85. package/dist/shared/types/screen.d.ts.map +1 -0
  86. package/dist/shared/types/screen.js +2 -0
  87. package/dist/shared/types/screen.js.map +1 -0
  88. package/dist/shared/types/theme.d.ts +48 -0
  89. package/dist/shared/types/theme.d.ts.map +1 -0
  90. package/dist/shared/types/theme.js +2 -0
  91. package/dist/shared/types/theme.js.map +1 -0
  92. package/package.json +58 -0
  93. package/src/native/README.md +17 -0
  94. package/src/native/context.tsx +74 -0
  95. package/src/native/index.ts +39 -0
  96. package/src/native/primitives.tsx +730 -0
  97. package/src/native/screen.tsx +831 -0
  98. package/src/react/README.md +7 -0
  99. package/src/react/context.tsx +83 -0
  100. package/src/react/index.ts +42 -0
  101. package/src/react/primitives.tsx +589 -0
  102. package/src/react/screen.tsx +633 -0
  103. package/src/react/styles.css +299 -0
  104. package/src/shared/README.md +9 -0
  105. package/src/shared/messages.ts +99 -0
  106. package/src/shared/render.ts +12 -0
  107. package/src/shared/theme.ts +27 -0
  108. package/src/shared/types/board.ts +48 -0
  109. package/src/shared/types/comment.ts +101 -0
  110. package/src/shared/types/context.ts +81 -0
  111. package/src/shared/types/entry.ts +48 -0
  112. package/src/shared/types/form.ts +27 -0
  113. package/src/shared/types/helpers.ts +1 -0
  114. package/src/shared/types/index.ts +45 -0
  115. package/src/shared/types/messages.ts +160 -0
  116. package/src/shared/types/render.ts +12 -0
  117. package/src/shared/types/screen.ts +123 -0
  118. 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
+ };