convex-feedback-ui 0.1.4 → 0.1.5

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 (91) hide show
  1. package/README.md +52 -3
  2. package/dist/.tsbuildinfo +1 -1
  3. package/dist/native/expo/FeedbackScreen.d.ts +1 -1
  4. package/dist/native/expo/FeedbackScreen.d.ts.map +1 -1
  5. package/dist/native/expo/FeedbackScreen.js +3 -2
  6. package/dist/native/expo/FeedbackScreen.js.map +1 -1
  7. package/dist/native/expo/index.d.ts +2 -0
  8. package/dist/native/expo/index.d.ts.map +1 -1
  9. package/dist/native/expo/index.js +2 -0
  10. package/dist/native/expo/index.js.map +1 -1
  11. package/dist/native/expo/metadata.d.ts +4 -0
  12. package/dist/native/expo/metadata.d.ts.map +1 -0
  13. package/dist/native/expo/metadata.js +44 -0
  14. package/dist/native/expo/metadata.js.map +1 -0
  15. package/dist/native/native/FeedbackScreen.d.ts +1 -1
  16. package/dist/native/native/FeedbackScreen.d.ts.map +1 -1
  17. package/dist/native/native/FeedbackScreen.js +3 -2
  18. package/dist/native/native/FeedbackScreen.js.map +1 -1
  19. package/dist/native/native/index.d.ts +2 -0
  20. package/dist/native/native/index.d.ts.map +1 -1
  21. package/dist/native/native/index.js +2 -0
  22. package/dist/native/native/index.js.map +1 -1
  23. package/dist/native/shared/metadata.d.ts +5 -0
  24. package/dist/native/shared/metadata.d.ts.map +1 -0
  25. package/dist/native/shared/metadata.js +31 -0
  26. package/dist/native/shared/metadata.js.map +1 -0
  27. package/dist/native/shared/ui/EntryDetail.d.ts.map +1 -1
  28. package/dist/native/shared/ui/EntryDetail.js +3 -1
  29. package/dist/native/shared/ui/EntryDetail.js.map +1 -1
  30. package/dist/native/shared/ui/MetadataModal.d.ts +6 -0
  31. package/dist/native/shared/ui/MetadataModal.d.ts.map +1 -0
  32. package/dist/native/shared/ui/MetadataModal.js +39 -0
  33. package/dist/native/shared/ui/MetadataModal.js.map +1 -0
  34. package/dist/native/shared/ui/NewEntry.d.ts.map +1 -1
  35. package/dist/native/shared/ui/NewEntry.js +9 -2
  36. package/dist/native/shared/ui/NewEntry.js.map +1 -1
  37. package/dist/react/index.d.ts +2 -0
  38. package/dist/react/index.d.ts.map +1 -1
  39. package/dist/react/index.js +2 -0
  40. package/dist/react/index.js.map +1 -1
  41. package/dist/react/metadata.d.ts +4 -0
  42. package/dist/react/metadata.d.ts.map +1 -0
  43. package/dist/react/metadata.js +40 -0
  44. package/dist/react/metadata.js.map +1 -0
  45. package/dist/react/screen.d.ts +1 -1
  46. package/dist/react/screen.d.ts.map +1 -1
  47. package/dist/react/screen.js +18 -4
  48. package/dist/react/screen.js.map +1 -1
  49. package/dist/react/styles.css +35 -0
  50. package/dist/shared/context/FeedbackBodyProvider.d.ts +1 -1
  51. package/dist/shared/context/FeedbackBodyProvider.d.ts.map +1 -1
  52. package/dist/shared/context/FeedbackBodyProvider.js +5 -1
  53. package/dist/shared/context/FeedbackBodyProvider.js.map +1 -1
  54. package/dist/shared/messages.d.ts.map +1 -1
  55. package/dist/shared/messages.js +9 -0
  56. package/dist/shared/messages.js.map +1 -1
  57. package/dist/shared/metadata.d.ts +6 -0
  58. package/dist/shared/metadata.d.ts.map +1 -0
  59. package/dist/shared/metadata.js +54 -0
  60. package/dist/shared/metadata.js.map +1 -0
  61. package/dist/shared/types/index.d.ts +1 -0
  62. package/dist/shared/types/index.d.ts.map +1 -1
  63. package/dist/shared/types/messages.d.ts +17 -0
  64. package/dist/shared/types/messages.d.ts.map +1 -1
  65. package/dist/shared/types/metadata.d.ts +22 -0
  66. package/dist/shared/types/metadata.d.ts.map +1 -0
  67. package/dist/shared/types/metadata.js +2 -0
  68. package/dist/shared/types/metadata.js.map +1 -0
  69. package/dist/shared/types/screen.d.ts +10 -1
  70. package/dist/shared/types/screen.d.ts.map +1 -1
  71. package/package.json +6 -2
  72. package/src/native/expo/FeedbackScreen.tsx +4 -0
  73. package/src/native/expo/index.ts +2 -0
  74. package/src/native/expo/metadata.ts +62 -0
  75. package/src/native/native/FeedbackScreen.tsx +4 -0
  76. package/src/native/native/index.ts +2 -0
  77. package/src/native/shared/metadata.ts +39 -0
  78. package/src/native/shared/ui/EntryDetail.tsx +14 -0
  79. package/src/native/shared/ui/MetadataModal.tsx +112 -0
  80. package/src/native/shared/ui/NewEntry.tsx +16 -2
  81. package/src/react/index.ts +2 -0
  82. package/src/react/metadata.ts +51 -0
  83. package/src/react/screen.tsx +89 -2
  84. package/src/react/styles.css +35 -0
  85. package/src/shared/context/FeedbackBodyProvider.tsx +8 -0
  86. package/src/shared/messages.ts +9 -0
  87. package/src/shared/metadata.ts +84 -0
  88. package/src/shared/types/index.ts +1 -0
  89. package/src/shared/types/messages.ts +24 -0
  90. package/src/shared/types/metadata.ts +55 -0
  91. package/src/shared/types/screen.ts +13 -2
@@ -0,0 +1,62 @@
1
+ import type { FeedbackMetadataValue } from "convex-feedback";
2
+ import Constants from "expo-constants";
3
+ import { Platform } from "react-native";
4
+
5
+ import type {
6
+ StandardMetadataKey,
7
+ EnabledStandardMetadataSelection,
8
+ } from "../../shared/types/metadata.js";
9
+ import {
10
+ collectNativeMetadata,
11
+ defaultNativeMetadataKeys,
12
+ } from "../shared/metadata.js";
13
+
14
+ const defaultExpoMetadataKeys: readonly StandardMetadataKey[] = [
15
+ ...defaultNativeMetadataKeys,
16
+ "appVersion",
17
+ "buildNumber",
18
+ "applicationId",
19
+ "expoRuntimeVersion",
20
+ "executionEnvironment",
21
+ ];
22
+
23
+ export function collectExpoMetadata(
24
+ selection: EnabledStandardMetadataSelection,
25
+ ): Partial<Record<StandardMetadataKey, FeedbackMetadataValue>> {
26
+ const selected = selection === true ? defaultExpoMetadataKeys : selection;
27
+ const keys = new Set(selected);
28
+ const metadata = collectNativeMetadata(selected);
29
+ const config = Constants.expoConfig;
30
+
31
+ if (keys.has("appVersion") && config?.version)
32
+ metadata.appVersion = config.version;
33
+
34
+ if (keys.has("buildNumber")) {
35
+ const buildNumber =
36
+ Platform.OS === "ios"
37
+ ? (Constants.platform?.ios?.buildNumber ?? config?.ios?.buildNumber)
38
+ : Platform.OS === "android"
39
+ ? (Constants.platform?.android?.versionCode ??
40
+ config?.android?.versionCode)
41
+ : undefined;
42
+ if (buildNumber !== undefined && buildNumber !== null)
43
+ metadata.buildNumber = buildNumber;
44
+ }
45
+
46
+ if (keys.has("applicationId")) {
47
+ const applicationId =
48
+ Platform.OS === "ios"
49
+ ? config?.ios?.bundleIdentifier
50
+ : Platform.OS === "android"
51
+ ? config?.android?.package
52
+ : undefined;
53
+ if (applicationId) metadata.applicationId = applicationId;
54
+ }
55
+
56
+ if (keys.has("expoRuntimeVersion") && Constants.expoRuntimeVersion)
57
+ metadata.expoRuntimeVersion = Constants.expoRuntimeVersion;
58
+ if (keys.has("executionEnvironment"))
59
+ metadata.executionEnvironment = Constants.executionEnvironment;
60
+
61
+ return metadata;
62
+ }
@@ -3,6 +3,7 @@ import { FeedbackProvider } from "../../shared/context/FeedbackProvider";
3
3
  import { kinds } from "../../shared/helpers";
4
4
  import type { FeedbackScreenRootProps } from "../../shared/types";
5
5
  import { FeedbackScreenContent } from "../shared/ui/BodyContent";
6
+ import { collectNativeMetadata } from "../shared/metadata";
6
7
 
7
8
  export function FeedbackScreen({
8
9
  hooks,
@@ -14,6 +15,7 @@ export function FeedbackScreen({
14
15
  enabledKinds = kinds,
15
16
  maxCommentDepth = 5,
16
17
  debounceDuration = 300,
18
+ collectMetadata,
17
19
  transformComments,
18
20
  renderActor,
19
21
  ...props
@@ -27,6 +29,8 @@ export function FeedbackScreen({
27
29
  enabledKinds={enabledKinds}
28
30
  maxCommentDepth={maxCommentDepth}
29
31
  debounceDuration={debounceDuration}
32
+ collectMetadata={collectMetadata}
33
+ collectStandardMetadata={collectNativeMetadata}
30
34
  transformComments={transformComments}
31
35
  renderActor={renderActor}
32
36
  >
@@ -1,7 +1,9 @@
1
1
  export * from "../../shared/context/FeedbackBodyProvider.js";
2
2
  export * from "../../shared/context/FeedbackProvider.js";
3
3
  export * from "../../shared/messages.js";
4
+ export * from "../../shared/metadata.js";
4
5
  export * from "../../shared/theme.js";
5
6
  export type * from "../../shared/types/index.js";
6
7
  export * from "../shared/ui/primitives.js";
8
+ export * from "../shared/metadata.js";
7
9
  export * from "./FeedbackScreen.js";
@@ -0,0 +1,39 @@
1
+ import type { FeedbackMetadataValue } from "convex-feedback";
2
+ import { Dimensions, PixelRatio, Platform } from "react-native";
3
+
4
+ import type {
5
+ StandardMetadataKey,
6
+ EnabledStandardMetadataSelection,
7
+ } from "../../shared/types/metadata.js";
8
+
9
+ export const defaultNativeMetadataKeys: readonly StandardMetadataKey[] = [
10
+ "platform",
11
+ "osVersion",
12
+ "deviceModel",
13
+ "screenWidth",
14
+ "screenHeight",
15
+ "pixelRatio",
16
+ "fontScale",
17
+ ];
18
+
19
+ export function collectNativeMetadata(
20
+ selection: EnabledStandardMetadataSelection,
21
+ ): Partial<Record<StandardMetadataKey, FeedbackMetadataValue>> {
22
+ const keys = new Set(
23
+ selection === true ? defaultNativeMetadataKeys : selection,
24
+ );
25
+ const metadata: Partial<Record<StandardMetadataKey, FeedbackMetadataValue>> =
26
+ {};
27
+ const screen = Dimensions.get("screen");
28
+
29
+ if (keys.has("platform")) metadata.platform = Platform.OS;
30
+ if (keys.has("osVersion")) metadata.osVersion = Platform.Version;
31
+ if (keys.has("deviceModel") && Platform.OS === "android")
32
+ metadata.deviceModel = Platform.constants.Model;
33
+ if (keys.has("screenWidth")) metadata.screenWidth = screen.width;
34
+ if (keys.has("screenHeight")) metadata.screenHeight = screen.height;
35
+ if (keys.has("pixelRatio")) metadata.pixelRatio = PixelRatio.get();
36
+ if (keys.has("fontScale")) metadata.fontScale = PixelRatio.getFontScale();
37
+
38
+ return metadata;
39
+ }
@@ -7,6 +7,7 @@ import type { FeedbackScreenEntryDetailProps } from "../../../shared/types";
7
7
  import { Button } from "./Button";
8
8
  import { CommentBranch } from "./CommentBranch";
9
9
  import { FeedbackEntry, FeedbackForm } from "./primitives";
10
+ import { MetadataModal } from "./MetadataModal";
10
11
 
11
12
  export function EntryDetail({
12
13
  entryId,
@@ -21,6 +22,7 @@ export function EntryDetail({
21
22
  const setUpvote = hooks.useSetEntryUpvote();
22
23
  const createComment = hooks.useCreateComment();
23
24
  const [body, setBody] = useState("");
25
+ const [showMetadata, setShowMetadata] = useState(false);
24
26
  const visible = useMemo(
25
27
  () => transformComments?.(comments.results) ?? comments.results,
26
28
  [comments.results, transformComments],
@@ -66,6 +68,18 @@ export function EntryDetail({
66
68
  <FeedbackEntry.CommentCount />
67
69
  </FeedbackEntry.Content>
68
70
  </FeedbackEntry.Root>
71
+ {entry.metadata !== undefined && (
72
+ <Button
73
+ label={messages.metadata.view}
74
+ onPress={() => setShowMetadata(true)}
75
+ />
76
+ )}
77
+ {showMetadata && entry.metadata !== undefined && (
78
+ <MetadataModal
79
+ metadata={entry.metadata}
80
+ onRequestClose={() => setShowMetadata(false)}
81
+ />
82
+ )}
69
83
  <Text
70
84
  style={{ color: theme.colors.text, fontSize: 18, fontWeight: "700" }}
71
85
  >
@@ -0,0 +1,112 @@
1
+ import type { FeedbackMetadata } from "convex-feedback";
2
+ import { Modal, Pressable, ScrollView, Text, View } from "react-native";
3
+
4
+ import { useFeedbackUi } from "../../../shared/context/FeedbackProvider";
5
+ import { formatMetadataKey } from "../../../shared/metadata";
6
+ import { Button } from "./Button";
7
+
8
+ export function MetadataModal({
9
+ metadata,
10
+ onRequestClose,
11
+ }: {
12
+ metadata: FeedbackMetadata;
13
+ onRequestClose: () => void;
14
+ }) {
15
+ const { messages, theme } = useFeedbackUi();
16
+ const sections = [
17
+ [messages.metadata.standard, metadata.standard],
18
+ [messages.metadata.additional, metadata.additional],
19
+ ] as const;
20
+ const hasValues = sections.some(
21
+ ([, values]) => values !== undefined && Object.keys(values).length > 0,
22
+ );
23
+
24
+ return (
25
+ <Modal
26
+ visible
27
+ transparent
28
+ animationType="fade"
29
+ onRequestClose={onRequestClose}
30
+ role="dialog"
31
+ >
32
+ <Pressable
33
+ accessible={false}
34
+ focusable={false}
35
+ onPress={onRequestClose}
36
+ style={{
37
+ flex: 1,
38
+ justifyContent: "center",
39
+ padding: 20,
40
+ backgroundColor: "rgba(0, 0, 0, 0.4)",
41
+ }}
42
+ >
43
+ <Pressable
44
+ accessible={false}
45
+ focusable={false}
46
+ onPress={(event) => event.stopPropagation()}
47
+ style={{
48
+ maxHeight: "80%",
49
+ gap: 14,
50
+ padding: 18,
51
+ borderRadius: theme.radius,
52
+ backgroundColor: theme.colors.surface,
53
+ }}
54
+ >
55
+ <Text
56
+ style={{
57
+ color: theme.colors.text,
58
+ fontSize: 18,
59
+ fontWeight: "700",
60
+ }}
61
+ >
62
+ {messages.metadata.title}
63
+ </Text>
64
+ <ScrollView contentContainerStyle={{ gap: 16 }}>
65
+ {hasValues ? (
66
+ sections.map(([label, values]) =>
67
+ values === undefined ||
68
+ Object.keys(values).length === 0 ? null : (
69
+ <View key={label} style={{ gap: 8 }}>
70
+ <Text
71
+ style={{ color: theme.colors.text, fontWeight: "700" }}
72
+ >
73
+ {label}
74
+ </Text>
75
+ {Object.entries(values).map(([key, value]) => (
76
+ <View
77
+ key={key}
78
+ style={{
79
+ gap: 3,
80
+ paddingBottom: 8,
81
+ borderBottomWidth: 1,
82
+ borderBottomColor: theme.colors.border,
83
+ }}
84
+ >
85
+ <Text
86
+ style={{
87
+ color: theme.colors.mutedText,
88
+ fontWeight: "600",
89
+ }}
90
+ >
91
+ {formatMetadataKey(key)}
92
+ </Text>
93
+ <Text selectable style={{ color: theme.colors.text }}>
94
+ {String(value)}
95
+ </Text>
96
+ </View>
97
+ ))}
98
+ </View>
99
+ ),
100
+ )
101
+ ) : (
102
+ <Text style={{ color: theme.colors.mutedText }}>
103
+ {messages.metadata.empty}
104
+ </Text>
105
+ )}
106
+ </ScrollView>
107
+ <Button label={messages.metadata.close} onPress={onRequestClose} />
108
+ </Pressable>
109
+ </Pressable>
110
+ </Modal>
111
+ );
112
+ }
@@ -13,6 +13,7 @@ import {
13
13
  import { useFeedbackBody } from "../../../shared/context/FeedbackBodyProvider";
14
14
  import { useFeedbackUi } from "../../../shared/context/FeedbackProvider";
15
15
  import type { FeedbackScreenEntryModalProps } from "../../../shared/types";
16
+ import { collectEntryMetadata } from "../../../shared/metadata";
16
17
  import { EntryDetail } from "./EntryDetail";
17
18
  import { FeedbackForm } from "./primitives";
18
19
 
@@ -166,7 +167,8 @@ function CreateForm({
166
167
  onBodyChange: (body: string) => void;
167
168
  onOpenSuggestion: (id: string) => void;
168
169
  }) {
169
- const { hooks, enabledKinds } = useFeedbackBody();
170
+ const { hooks, enabledKinds, collectMetadata, collectStandardMetadata } =
171
+ useFeedbackBody();
170
172
  const { messages, theme } = useFeedbackUi();
171
173
  const [submitting, setSubmitting] = useState(false);
172
174
  const create = hooks.useCreateEntry();
@@ -185,7 +187,19 @@ function CreateForm({
185
187
  setSubmitting(true);
186
188
 
187
189
  try {
188
- onCreated(await create({ kind, title, body }));
190
+ const metadata = await collectEntryMetadata(
191
+ collectMetadata,
192
+ kind,
193
+ collectStandardMetadata,
194
+ );
195
+ onCreated(
196
+ await create({
197
+ kind,
198
+ title,
199
+ body,
200
+ ...(metadata === undefined ? {} : { metadata }),
201
+ }),
202
+ );
189
203
  } finally {
190
204
  setSubmitting(false);
191
205
  }
@@ -1,6 +1,8 @@
1
1
  export * from "../shared/messages.js";
2
+ export * from "../shared/metadata.js";
2
3
  export * from "../shared/theme.js";
3
4
  export type * from "../shared/types";
4
5
  export * from "./css.js";
6
+ export * from "./metadata.js";
5
7
  export * from "./primitives.js";
6
8
  export * from "./screen.js";
@@ -0,0 +1,51 @@
1
+ import type { FeedbackMetadataValue } from "convex-feedback";
2
+
3
+ import type {
4
+ StandardMetadataKey,
5
+ EnabledStandardMetadataSelection,
6
+ } from "../shared/types/metadata.js";
7
+
8
+ const defaultWebMetadataKeys: readonly StandardMetadataKey[] = [
9
+ "platform",
10
+ "userAgent",
11
+ "language",
12
+ "timezone",
13
+ "screenWidth",
14
+ "screenHeight",
15
+ "viewportWidth",
16
+ "viewportHeight",
17
+ "devicePixelRatio",
18
+ ];
19
+
20
+ export function collectWebMetadata(
21
+ selection: EnabledStandardMetadataSelection,
22
+ ): Partial<Record<StandardMetadataKey, FeedbackMetadataValue>> {
23
+ const keys = new Set(selection === true ? defaultWebMetadataKeys : selection);
24
+ const metadata: Partial<Record<StandardMetadataKey, FeedbackMetadataValue>> =
25
+ {};
26
+ const browserNavigator =
27
+ typeof navigator === "undefined" ? undefined : navigator;
28
+ const browserWindow = typeof window === "undefined" ? undefined : window;
29
+
30
+ if (keys.has("platform")) metadata.platform = "web";
31
+ if (keys.has("userAgent") && browserNavigator?.userAgent)
32
+ metadata.userAgent = browserNavigator.userAgent;
33
+ if (keys.has("language") && browserNavigator?.language)
34
+ metadata.language = browserNavigator.language;
35
+ if (keys.has("timezone")) {
36
+ const timezone = Intl.DateTimeFormat().resolvedOptions().timeZone;
37
+ if (timezone) metadata.timezone = timezone;
38
+ }
39
+ if (keys.has("screenWidth") && browserWindow?.screen)
40
+ metadata.screenWidth = browserWindow.screen.width;
41
+ if (keys.has("screenHeight") && browserWindow?.screen)
42
+ metadata.screenHeight = browserWindow.screen.height;
43
+ if (keys.has("viewportWidth") && browserWindow)
44
+ metadata.viewportWidth = browserWindow.innerWidth;
45
+ if (keys.has("viewportHeight") && browserWindow)
46
+ metadata.viewportHeight = browserWindow.innerHeight;
47
+ if (keys.has("devicePixelRatio") && browserWindow)
48
+ metadata.devicePixelRatio = browserWindow.devicePixelRatio;
49
+
50
+ return metadata;
51
+ }
@@ -1,6 +1,6 @@
1
1
  "use client";
2
2
 
3
- import type { EntryKind } from "convex-feedback";
3
+ import type { EntryKind, FeedbackMetadata } from "convex-feedback";
4
4
  import { useMemo, useState, type SyntheticEvent } from "react";
5
5
 
6
6
  import {
@@ -26,6 +26,8 @@ import {
26
26
  FeedbackEntry,
27
27
  FeedbackForm,
28
28
  } from "./primitives.js";
29
+ import { collectWebMetadata } from "./metadata.js";
30
+ import { collectEntryMetadata, formatMetadataKey } from "../shared/metadata.js";
29
31
 
30
32
  const entryKinds: readonly EntryKind[] = [
31
33
  "feedback",
@@ -49,6 +51,7 @@ export function FeedbackScreen({
49
51
  transformComments,
50
52
  renderActor,
51
53
  debounceDuration = 300,
54
+ collectMetadata,
52
55
  ...props
53
56
  }: FeedbackScreenProps) {
54
57
  return (
@@ -60,6 +63,8 @@ export function FeedbackScreen({
60
63
  enabledKinds={enabledKinds}
61
64
  maxCommentDepth={maxCommentDepth}
62
65
  debounceDuration={debounceDuration}
66
+ collectMetadata={collectMetadata}
67
+ collectStandardMetadata={collectWebMetadata}
63
68
  transformComments={transformComments}
64
69
  renderActor={renderActor}
65
70
  >
@@ -207,7 +212,8 @@ function EntryCard({ entry, hooks, onOpen }: FeedbackScreenEntryCardProps) {
207
212
  }
208
213
 
209
214
  function CreateEntryForm({ onCreated }: FeedbackScreenEntryModalProps) {
210
- const { hooks, enabledKinds } = useFeedbackBody();
215
+ const { hooks, enabledKinds, collectMetadata, collectStandardMetadata } =
216
+ useFeedbackBody();
211
217
  const { messages } = useFeedbackUi();
212
218
  const [kind, setKind] = useState<EntryKind>(enabledKinds[0] ?? "feedback");
213
219
  const [title, setTitle] = useState("");
@@ -242,10 +248,16 @@ function CreateEntryForm({ onCreated }: FeedbackScreenEntryModalProps) {
242
248
  setSubmitting(true);
243
249
 
244
250
  try {
251
+ const metadata = await collectEntryMetadata(
252
+ collectMetadata,
253
+ kind,
254
+ collectStandardMetadata,
255
+ );
245
256
  const entryId = await createEntry({
246
257
  kind,
247
258
  title,
248
259
  body,
260
+ ...(metadata === undefined ? {} : { metadata }),
249
261
  });
250
262
 
251
263
  onCreated(entryId);
@@ -382,6 +394,7 @@ function EntryDetail({ entryId, onBack }: FeedbackScreenEntryDetailProps) {
382
394
  const comments = hooks.useComments({ entryId, sort: commentSort });
383
395
  const createComment = hooks.useCreateComment();
384
396
  const [body, setBody] = useState("");
397
+ const [showMetadata, setShowMetadata] = useState(false);
385
398
 
386
399
  const visibleComments = useMemo(
387
400
  () => transformComments?.(comments.results) ?? comments.results,
@@ -422,6 +435,25 @@ function EntryDetail({ entryId, onBack }: FeedbackScreenEntryDetailProps) {
422
435
  </FeedbackEntry.Content>
423
436
  </FeedbackEntry.Root>
424
437
 
438
+ {entry.metadata !== undefined && (
439
+ <div>
440
+ <button
441
+ type="button"
442
+ className="cf-button"
443
+ onClick={() => setShowMetadata(true)}
444
+ >
445
+ {messages.metadata.view}
446
+ </button>
447
+ </div>
448
+ )}
449
+
450
+ {showMetadata && entry.metadata !== undefined && (
451
+ <MetadataDialog
452
+ metadata={entry.metadata}
453
+ onClose={() => setShowMetadata(false)}
454
+ />
455
+ )}
456
+
425
457
  <section className="cf-discussion">
426
458
  <h3>{messages.comments.title}</h3>
427
459
  <FeedbackForm.Root
@@ -473,6 +505,61 @@ function EntryDetail({ entryId, onBack }: FeedbackScreenEntryDetailProps) {
473
505
  );
474
506
  }
475
507
 
508
+ function MetadataDialog({
509
+ metadata,
510
+ onClose,
511
+ }: {
512
+ metadata: FeedbackMetadata;
513
+ onClose: () => void;
514
+ }) {
515
+ const { messages } = useFeedbackUi();
516
+ const sections = [
517
+ [messages.metadata.standard, metadata.standard],
518
+ [messages.metadata.additional, metadata.additional],
519
+ ] as const;
520
+ const hasValues = sections.some(
521
+ ([, values]) => values !== undefined && Object.keys(values).length > 0,
522
+ );
523
+
524
+ return (
525
+ <div className="cf-confirm-backdrop" onMouseDown={onClose}>
526
+ <div
527
+ className="cf-confirm cf-metadata-dialog"
528
+ role="dialog"
529
+ aria-modal="true"
530
+ aria-labelledby="cf-metadata-title"
531
+ onMouseDown={(event) => event.stopPropagation()}
532
+ >
533
+ <h3 id="cf-metadata-title">{messages.metadata.title}</h3>
534
+ {hasValues ? (
535
+ sections.map(([label, values]) =>
536
+ values === undefined || Object.keys(values).length === 0 ? null : (
537
+ <section key={label} className="cf-metadata-section">
538
+ <h4>{label}</h4>
539
+ <dl>
540
+ {Object.entries(values).map(([key, value]) => (
541
+ <div key={key} className="cf-metadata-row">
542
+ <dt>{formatMetadataKey(key)}</dt>
543
+ <dd>{String(value)}</dd>
544
+ </div>
545
+ ))}
546
+ </dl>
547
+ </section>
548
+ ),
549
+ )
550
+ ) : (
551
+ <p>{messages.metadata.empty}</p>
552
+ )}
553
+ <div className="cf-inline-actions">
554
+ <button type="button" className="cf-button" onClick={onClose}>
555
+ {messages.metadata.close}
556
+ </button>
557
+ </div>
558
+ </div>
559
+ </div>
560
+ );
561
+ }
562
+
476
563
  function CommentBranch({ comment, entryId }: FeedbackScreenCommentBranchProps) {
477
564
  const { hooks, maxCommentDepth, renderActor } = useFeedbackBody();
478
565
  const { messages } = useFeedbackUi();
@@ -297,3 +297,38 @@
297
297
  color: var(--cf-muted);
298
298
  line-height: 1.5;
299
299
  }
300
+
301
+ :where(.cf-board) :where(.cf-metadata-dialog) {
302
+ width: min(100%, 560px);
303
+ max-height: min(80vh, 720px);
304
+ overflow: auto;
305
+ }
306
+
307
+ :where(.cf-board) :where(.cf-metadata-dialog h3),
308
+ :where(.cf-board) :where(.cf-metadata-dialog h4),
309
+ :where(.cf-board) :where(.cf-metadata-dialog dl) {
310
+ margin: 0;
311
+ }
312
+
313
+ :where(.cf-board) :where(.cf-metadata-section) {
314
+ display: grid;
315
+ gap: 8px;
316
+ }
317
+
318
+ :where(.cf-board) :where(.cf-metadata-row) {
319
+ display: grid;
320
+ grid-template-columns: minmax(120px, 0.8fr) minmax(0, 1.2fr);
321
+ gap: 12px;
322
+ padding: 8px 0;
323
+ border-bottom: 1px solid var(--cf-border);
324
+ }
325
+
326
+ :where(.cf-board) :where(.cf-metadata-row dt) {
327
+ color: var(--cf-muted);
328
+ font-weight: 600;
329
+ }
330
+
331
+ :where(.cf-board) :where(.cf-metadata-row dd) {
332
+ min-width: 0;
333
+ overflow-wrap: anywhere;
334
+ }
@@ -22,6 +22,10 @@ export function FeedbackBodyProvider({
22
22
  enabledKinds,
23
23
  maxCommentDepth,
24
24
  debounceDuration,
25
+ collectMetadata,
26
+ collectStandardMetadata,
27
+ transformComments,
28
+ renderActor,
25
29
  children,
26
30
  }: PropsWithChildren<FeedbackScreenProviderProps>) {
27
31
  const [selectedEntryId, setSelectedEntryId] = useState<string | null>(null);
@@ -60,6 +64,10 @@ export function FeedbackBodyProvider({
60
64
  enabledKinds,
61
65
  maxCommentDepth,
62
66
  debounceDuration,
67
+ collectMetadata,
68
+ collectStandardMetadata,
69
+ transformComments,
70
+ renderActor,
63
71
  } satisfies FeedbackScreenBodyContextValue;
64
72
 
65
73
  return (
@@ -41,6 +41,14 @@ export const englishFeedbackMessages: FeedbackMessages = {
41
41
  open: "Open",
42
42
  back: "Go back",
43
43
  },
44
+ metadata: {
45
+ view: "View metadata",
46
+ title: "Diagnostic metadata",
47
+ standard: "Standard",
48
+ additional: "Additional",
49
+ empty: "No metadata values were collected.",
50
+ close: "Close",
51
+ },
44
52
  form: {
45
53
  kind: "Type",
46
54
  title: "Title",
@@ -94,6 +102,7 @@ export function mergeFeedbackMessages(
94
102
  ...overrides.statuses,
95
103
  },
96
104
  entry: { ...englishFeedbackMessages.entry, ...overrides.entry },
105
+ metadata: { ...englishFeedbackMessages.metadata, ...overrides.metadata },
97
106
  form: { ...englishFeedbackMessages.form, ...overrides.form },
98
107
  comments: { ...englishFeedbackMessages.comments, ...overrides.comments },
99
108
  };