@eifi1/ui-kit 0.37.0 → 0.38.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 (130) hide show
  1. package/README.md +11 -4
  2. package/dist/eslint/index.js +1 -1
  3. package/dist/feedback/feedback-comment-thread.d.ts +87 -0
  4. package/dist/feedback/feedback-comment-thread.js +170 -0
  5. package/dist/feedback/feedback-comment-thread.js.map +1 -0
  6. package/dist/feedback/feedback-dialog-labels.js +2 -1
  7. package/dist/feedback/feedback-dialog-labels.js.map +1 -1
  8. package/dist/feedback/feedback-dialog.d.ts +13 -0
  9. package/dist/feedback/feedback-dialog.js +12 -4
  10. package/dist/feedback/feedback-dialog.js.map +1 -1
  11. package/dist/feedback/feedback-labels.d.ts +11 -0
  12. package/dist/feedback/feedback-labels.js +4 -1
  13. package/dist/feedback/feedback-labels.js.map +1 -1
  14. package/dist/feedback/feedback-menu-labels.js +2 -1
  15. package/dist/feedback/feedback-menu-labels.js.map +1 -1
  16. package/dist/feedback/feedback-menu.d.ts +17 -3
  17. package/dist/feedback/feedback-menu.js +11 -1
  18. package/dist/feedback/feedback-menu.js.map +1 -1
  19. package/dist/feedback/feedback-public-labels.d.ts +53 -0
  20. package/dist/feedback/feedback-public-labels.js +27 -0
  21. package/dist/feedback/feedback-public-labels.js.map +1 -0
  22. package/dist/feedback/feedback-public-list.d.ts +139 -0
  23. package/dist/feedback/feedback-public-list.js +299 -0
  24. package/dist/feedback/feedback-public-list.js.map +1 -0
  25. package/dist/feedback/feedback-public-switch.d.ts +51 -0
  26. package/dist/feedback/feedback-public-switch.js +50 -0
  27. package/dist/feedback/feedback-public-switch.js.map +1 -0
  28. package/dist/feedback/feedback-public-types.d.ts +75 -0
  29. package/dist/feedback/feedback-public-types.js +2 -0
  30. package/dist/feedback/feedback-public-types.js.map +1 -0
  31. package/dist/feedback/feedback-record.d.ts +32 -1
  32. package/dist/feedback/feedback-record.js +10 -0
  33. package/dist/feedback/feedback-record.js.map +1 -1
  34. package/dist/feedback/feedback-replies-context.d.ts +7 -0
  35. package/dist/feedback/feedback-replies-context.js +7 -0
  36. package/dist/feedback/feedback-replies-context.js.map +1 -0
  37. package/dist/feedback/feedback-replies.d.ts +97 -0
  38. package/dist/feedback/feedback-replies.js +114 -0
  39. package/dist/feedback/feedback-replies.js.map +1 -0
  40. package/dist/feedback/feedback-row-detail-labels.js +1 -0
  41. package/dist/feedback/feedback-row-detail-labels.js.map +1 -1
  42. package/dist/feedback/feedback-row-detail.d.ts +47 -10
  43. package/dist/feedback/feedback-row-detail.js +51 -50
  44. package/dist/feedback/feedback-row-detail.js.map +1 -1
  45. package/dist/feedback/feedback-submit-dialog.d.ts +3 -1
  46. package/dist/feedback/feedback-submit-dialog.js +6 -1
  47. package/dist/feedback/feedback-submit-dialog.js.map +1 -1
  48. package/dist/feedback/feedback-submit.d.ts +8 -0
  49. package/dist/feedback/feedback-submit.js +3 -2
  50. package/dist/feedback/feedback-submit.js.map +1 -1
  51. package/dist/feedback/feedback-table.d.ts +15 -5
  52. package/dist/feedback/feedback-table.js +46 -5
  53. package/dist/feedback/feedback-table.js.map +1 -1
  54. package/dist/feedback/feedback-thread-parts-labels.d.ts +6 -0
  55. package/dist/feedback/feedback-thread-parts-labels.js +36 -0
  56. package/dist/feedback/feedback-thread-parts-labels.js.map +1 -0
  57. package/dist/feedback/feedback-thread.d.ts +8 -0
  58. package/dist/feedback/feedback-thread.js +7 -2
  59. package/dist/feedback/feedback-thread.js.map +1 -1
  60. package/dist/feedback/feedback-vote-button.d.ts +49 -0
  61. package/dist/feedback/feedback-vote-button.js +72 -0
  62. package/dist/feedback/feedback-vote-button.js.map +1 -0
  63. package/dist/feedback.d.ts +6 -0
  64. package/dist/feedback.js +6 -0
  65. package/dist/feedback.js.map +1 -1
  66. package/dist/i18n/defaults.js +12 -0
  67. package/dist/i18n/defaults.js.map +1 -1
  68. package/dist/i18n/german.js +69 -3
  69. package/dist/i18n/german.js.map +1 -1
  70. package/dist/i18n/kit-labels.d.ts +16 -0
  71. package/dist/i18n/kit-labels.js.map +1 -1
  72. package/dist/i18n/locales/en.js +85 -3
  73. package/dist/i18n/locales/en.js.map +3 -3
  74. package/dist/i18n/locales/es.js +69 -3
  75. package/dist/i18n/locales/es.js.map +1 -1
  76. package/dist/i18n/locales/fr.js +69 -3
  77. package/dist/i18n/locales/fr.js.map +1 -1
  78. package/dist/i18n/locales/hu.js +69 -3
  79. package/dist/i18n/locales/hu.js.map +1 -1
  80. package/dist/i18n/locales/it.js +69 -3
  81. package/dist/i18n/locales/it.js.map +1 -1
  82. package/dist/i18n/locales/zh.js +69 -3
  83. package/dist/i18n/locales/zh.js.map +1 -1
  84. package/dist/i18n/review.js +5 -0
  85. package/dist/i18n/review.js.map +1 -1
  86. package/dist/index.d.ts +2 -0
  87. package/dist/index.js +6 -0
  88. package/dist/index.js.map +1 -1
  89. package/dist/notifications/feedback-notification-types-labels.d.ts +15 -0
  90. package/dist/notifications/feedback-notification-types-labels.js +11 -0
  91. package/dist/notifications/feedback-notification-types-labels.js.map +1 -0
  92. package/dist/notifications/feedback-notification-types.d.ts +28 -0
  93. package/dist/notifications/feedback-notification-types.js +21 -0
  94. package/dist/notifications/feedback-notification-types.js.map +1 -0
  95. package/package.json +1 -1
  96. package/src/feedback/feedback-comment-thread.tsx +277 -0
  97. package/src/feedback/feedback-dialog-labels.ts +1 -0
  98. package/src/feedback/feedback-dialog.tsx +30 -2
  99. package/src/feedback/feedback-labels.ts +14 -0
  100. package/src/feedback/feedback-menu-labels.ts +1 -0
  101. package/src/feedback/feedback-menu.tsx +29 -3
  102. package/src/feedback/feedback-public-labels.ts +80 -0
  103. package/src/feedback/feedback-public-list.tsx +478 -0
  104. package/src/feedback/feedback-public-switch.tsx +102 -0
  105. package/src/feedback/feedback-public-types.ts +82 -0
  106. package/src/feedback/feedback-record.ts +39 -1
  107. package/src/feedback/feedback-replies-context.ts +9 -0
  108. package/src/feedback/feedback-replies.tsx +235 -0
  109. package/src/feedback/feedback-row-detail-labels.ts +1 -0
  110. package/src/feedback/feedback-row-detail.tsx +115 -74
  111. package/src/feedback/feedback-submit-dialog.tsx +9 -0
  112. package/src/feedback/feedback-submit.tsx +10 -1
  113. package/src/feedback/feedback-table.tsx +73 -6
  114. package/src/feedback/feedback-thread-parts-labels.ts +41 -0
  115. package/src/feedback/feedback-thread.tsx +18 -1
  116. package/src/feedback/feedback-vote-button.tsx +123 -0
  117. package/src/feedback.ts +10 -0
  118. package/src/i18n/defaults.ts +12 -0
  119. package/src/i18n/german.ts +66 -0
  120. package/src/i18n/kit-labels.tsx +16 -0
  121. package/src/i18n/locales/en.ts +8 -0
  122. package/src/i18n/locales/es.ts +66 -0
  123. package/src/i18n/locales/fr.ts +66 -0
  124. package/src/i18n/locales/hu.ts +66 -0
  125. package/src/i18n/locales/it.ts +66 -0
  126. package/src/i18n/locales/zh.ts +66 -0
  127. package/src/i18n/review.ts +5 -0
  128. package/src/index.ts +9 -0
  129. package/src/notifications/feedback-notification-types-labels.ts +24 -0
  130. package/src/notifications/feedback-notification-types.ts +46 -0
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../src/notifications/feedback-notification-types.ts"],"sourcesContent":["import { useMemo } from \"react\";\nimport { useKitLabels, type LabelOverride } from \"../i18n/kit-labels\";\nimport type { NotificationTypeEntry } from \"./notification-types\";\nimport {\n DEFAULT_FEEDBACK_NOTIFICATION_TYPES_LABELS,\n type FeedbackNotificationTypesLabels,\n} from \"./feedback-notification-types-labels\";\n\nexport { DEFAULT_FEEDBACK_NOTIFICATION_TYPES_LABELS };\nexport type { FeedbackNotificationTypesLabels };\n\n/** The two feedback notification ids (§9.11), as server-kit's `FEEDBACK_ANSWERED` and\n * `FEEDBACK_COMMENT` name them. */\nexport type FeedbackNotificationTypeId = \"feedback_answered\" | \"feedback_comment\";\n\n/**\n * The setting words of the two feedback notification types (round 0.38,\n * docs/feedback-harmonization.md §9.11), for `NotificationTypesSetting`'s `types`:\n *\n * ```tsx\n * const feedbackTypes = useFeedbackNotificationTypes();\n * <NotificationTypesSetting types={[...appTypes, ...feedbackTypes]} … />\n * ```\n *\n * - **\"Answers to your reports\"** (`feedback_answered`): a reply from the admin or a\n * loop, to the author and every earlier commenter.\n * - **\"Comments on reports you follow\"** (`feedback_comment`): a visible comment, to the\n * author and the earlier commenters.\n *\n * Both entries are always returned: the card draws a type only when the server answered\n * it (notifications §2.1), so kastlan, which does not declare `feedback_comment` while its\n * public parts are off, shows the first alone. Words: `<UiKitProvider labels={{\n * feedbackNotificationTypes }}>` over English, `labels` over both.\n */\nexport function useFeedbackNotificationTypes(\n labels?: LabelOverride<FeedbackNotificationTypesLabels>,\n): NotificationTypeEntry<FeedbackNotificationTypeId>[] {\n const words = useKitLabels(\"feedbackNotificationTypes\", DEFAULT_FEEDBACK_NOTIFICATION_TYPES_LABELS, labels);\n return useMemo(\n () => [\n { id: \"feedback_answered\", label: words.answered, description: words.answeredDescription },\n { id: \"feedback_comment\", label: words.comment, description: words.commentDescription },\n ],\n [words],\n );\n}\n"],"mappings":";AAAA,SAAS,eAAe;AACxB,SAAS,oBAAwC;AAEjD;AAAA,EACE;AAAA,OAEK;AA4BA,SAAS,6BACd,QACqD;AACrD,QAAM,QAAQ,aAAa,6BAA6B,4CAA4C,MAAM;AAC1G,SAAO;AAAA,IACL,MAAM;AAAA,MACJ,EAAE,IAAI,qBAAqB,OAAO,MAAM,UAAU,aAAa,MAAM,oBAAoB;AAAA,MACzF,EAAE,IAAI,oBAAoB,OAAO,MAAM,SAAS,aAAa,MAAM,mBAAmB;AAAA,IACxF;AAAA,IACA,CAAC,KAAK;AAAA,EACR;AACF;","names":[]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@eifi1/ui-kit",
3
- "version": "0.37.0",
3
+ "version": "0.38.1",
4
4
  "type": "module",
5
5
  "description": "App-agnostic shared design system: domain-free React components, theme and tokens.",
6
6
  "_sideEffects": "The barrel (src/index.ts) re-exports EVERY component, including components/chart, which statically imports recharts. Without this field a bundler must assume each of those modules has side effects, so it cannot drop the unused ones: importing a Button from '@eifi1/ui-kit' dragged all 433KB of recharts into the consumer's ENTRY chunk, and Keksdose was preloading it on every page load, login included. Nothing in src/ imports for side effects (no bare imports, no CSS imports from JS, no module-level global writes) — the token stylesheet is its own './tokens.css' export — so this is safe. CSS is still listed so any future stylesheet survives. Re-checked when `flag-icons` left `dependencies` and tsup started emitting a `\"use client\"` banner: neither touches this. The flag stylesheet was never imported from JS in the first place (that was the finding), and a directive prologue is a string literal every bundler lifts out before it decides whether a module is inert.",
@@ -0,0 +1,277 @@
1
+ import { useContext } from "react";
2
+ import type { ReactNode } from "react";
3
+ import { EyeOff } from "lucide-react";
4
+ import { Button } from "../components/button";
5
+ import { useConfirm } from "../components/confirm-dialog";
6
+ import type { CommitScope } from "../components/write-lock";
7
+ import { useKitLabels, type LabelOverride } from "../i18n/kit-labels";
8
+ import { FeedbackStatusBadge } from "./feedback-inbox";
9
+ import type { FeedbackComment, FeedbackReply } from "./feedback-public-types";
10
+ import { DEFAULT_FEEDBACK_PUBLIC_LABELS } from "./feedback-public-labels";
11
+ import { FEEDBACK_COMMENT_MAX } from "./feedback-record";
12
+ import { useFeedbackRepliesLabels } from "./feedback-replies";
13
+ import { FeedbackRepliesDrawnAboveContext } from "./feedback-replies-context";
14
+ import { ChatComposer, FeedbackThread } from "./feedback-thread";
15
+ import type { FeedbackThreadMessage } from "./feedback-thread";
16
+ import { DEFAULT_FEEDBACK_COMMENTS_LABELS } from "./feedback-thread-parts-labels";
17
+
18
+ /**
19
+ * A report's conversation as everyone who may read it sees it
20
+ * (docs/feedback-harmonization.md §9.8): the admin's replies and other people's
21
+ * comments, each its own row, oldest first — merged by time, on the
22
+ * {@link FeedbackThread} look — and the box to add one.
23
+ *
24
+ * - **Replies** are drawn as staff posts with the status they set ("→ Done"); a reply
25
+ * without an author is "Admin" (§9.7).
26
+ * - **Comments**: the viewer's own at the reading end ("You"); someone else's under the
27
+ * first name the server sent, `null` as "A user" (§9.5, `feedbackPublic.anonymous`).
28
+ * - **Hidden** (§9.8): others see "Hidden by an admin" and no name, no text; the writer
29
+ * sees their text with that line; the admin sees it struck through, with **Unhide**.
30
+ * An admin has **Hide** on every visible comment; the writer **Delete** on their own,
31
+ * after a confirm (it is a hard delete). Nobody edits a comment.
32
+ * - **The composer** (1–4 000 characters, no files) only with `onComment`: left out for
33
+ * a reader who may not write (a demo, a CRASH report — the app knows).
34
+ *
35
+ * Delete asks through `useConfirm`, so a thread with `onDelete` and a comment of the
36
+ * viewer's needs the app's `ConfirmProvider` (each app mounts one already).
37
+ */
38
+
39
+ /* ── Labels ───────────────────────────────────────────────────────────────── */
40
+
41
+ /** The `feedbackComments` namespace of `<UiKitProvider labels>`. A `null` name is
42
+ * `feedbackPublic.anonymous`, the public pages' word, so the two say the same. */
43
+ export interface FeedbackCommentsLabels {
44
+ /** The thread's accessible name. de-CH: "Kommentare" */
45
+ comments: string;
46
+ /** Nothing yet. de-CH: "Noch keine Kommentare" */
47
+ empty: string;
48
+ /** In place of a hidden comment. de-CH: "Von einem Admin ausgeblendet" */
49
+ hidden: string;
50
+ /** The admin's button. de-CH: "Ausblenden" */
51
+ hide: string;
52
+ /** The admin's button on a hidden comment. de-CH: "Einblenden" */
53
+ unhide: string;
54
+ /** The writer's button. de-CH: "Löschen" */
55
+ delete: string;
56
+ /** The confirm's question. de-CH: "Diesen Kommentar löschen?" */
57
+ deleteTitle: string;
58
+ /** The confirm's line. de-CH: "Er wird für alle entfernt. Das kann nicht rückgängig
59
+ * gemacht werden." */
60
+ deleteBody: string;
61
+ /** The box's accessible name. de-CH: "Kommentar schreiben" */
62
+ field: string;
63
+ /** de-CH: "Kommentar schreiben…" */
64
+ placeholder: string;
65
+ /** The send button. de-CH: "Kommentieren" */
66
+ send: string;
67
+ }
68
+
69
+ export { DEFAULT_FEEDBACK_COMMENTS_LABELS };
70
+
71
+ /* ── The thread ───────────────────────────────────────────────────────────── */
72
+
73
+ export interface FeedbackCommentThreadProps {
74
+ /** The report's replies (§9.7), oldest first. Left out or `[]`: none.
75
+ *
76
+ * Inside `FeedbackRowDetail`'s `comments` they are dropped (0.38.1): the detail's
77
+ * Replies section draws them already, and the thread there is the comments alone —
78
+ * so passing them there cannot draw them twice. */
79
+ replies?: readonly FeedbackReply[];
80
+ /** `GET /feedback/{id}/comments`, oldest first. */
81
+ comments: readonly FeedbackComment[];
82
+ /** The viewer is the app's admin: Hide / Unhide, the hidden text struck through. */
83
+ isAdmin: boolean;
84
+ /** `POST /feedback/{id}/comments`. Return the promise: the box clears when it
85
+ * resolves, and keeps the draft when it rejects. A failure is the app's to toast —
86
+ * the server's `detail`, else `feedbackToast.commentFailed` (0.38.1). Left out, no
87
+ * composer. */
88
+ onComment?: (body: string) => void | Promise<unknown>;
89
+ /** `PATCH /feedback/{id}/comments/{comment_id} {hidden}` — the admin's. A rejection
90
+ * is caught (0.38.1) and is the app's to toast (`feedbackToast.updateFailed`). Left
91
+ * out, no Hide / Unhide. */
92
+ onHide?: (comment: FeedbackComment, hidden: boolean) => void | Promise<unknown>;
93
+ /** `DELETE /feedback/{id}/comments/{comment_id}` — the writer's own, after the
94
+ * confirm. Left out, no Delete. */
95
+ onDelete?: (comment: FeedbackComment) => void | Promise<unknown>;
96
+ /** A comment is being sent. */
97
+ pending?: boolean;
98
+ /** The comments are loading. */
99
+ loading?: boolean;
100
+ /** Comment, Hide and Delete COMMIT — see Button's `commit`. */
101
+ commit?: CommitScope;
102
+ /** "Now" for the relative times — pin it in tests and stories. */
103
+ now?: Date | number;
104
+ labels?: LabelOverride<FeedbackCommentsLabels>;
105
+ className?: string;
106
+ }
107
+
108
+ type Entry = { at: number; order: number; message: FeedbackThreadMessage };
109
+
110
+ /** The admin's Hide / Unhide as a promise — a sync throw or a `void` return included —
111
+ * so the click can catch it as Delete's does. */
112
+ const hideComment = async (
113
+ onHide: NonNullable<FeedbackCommentThreadProps["onHide"]>,
114
+ comment: FeedbackComment,
115
+ ) => {
116
+ await onHide(comment, !comment.hidden);
117
+ };
118
+
119
+ const instant = (iso: string) => {
120
+ const ms = Date.parse(iso);
121
+ return Number.isNaN(ms) ? 0 : ms;
122
+ };
123
+
124
+ export function FeedbackCommentThread({
125
+ replies: repliesProp = [],
126
+ comments,
127
+ isAdmin,
128
+ onComment,
129
+ onHide,
130
+ onDelete,
131
+ pending = false,
132
+ loading = false,
133
+ commit,
134
+ now,
135
+ labels: labelsProp,
136
+ className,
137
+ }: FeedbackCommentThreadProps) {
138
+ const labels = useKitLabels("feedbackComments", DEFAULT_FEEDBACK_COMMENTS_LABELS, labelsProp);
139
+ const anonymous = useKitLabels("feedbackPublic", DEFAULT_FEEDBACK_PUBLIC_LABELS).anonymous;
140
+ const replyLabels = useFeedbackRepliesLabels();
141
+ const repliesDrawnAbove = useContext(FeedbackRepliesDrawnAboveContext);
142
+ const replies = repliesDrawnAbove ? [] : repliesProp;
143
+
144
+ const entries: Entry[] = [];
145
+ replies.forEach((reply, order) => {
146
+ entries.push({
147
+ at: instant(reply.created_at),
148
+ order,
149
+ message: {
150
+ id: `reply-${reply.id}`,
151
+ author: reply.author_name ?? replyLabels.admin,
152
+ createdAt: reply.created_at,
153
+ body: reply.body,
154
+ staff: true,
155
+ badge: reply.status_to ? (
156
+ <span className="inline-flex min-w-0 max-w-full flex-wrap items-center gap-1" data-feedback-reply-status={reply.status_to}>
157
+ <span className="sr-only">{replyLabels.statusChanged}</span>
158
+ <span aria-hidden>→</span>
159
+ <FeedbackStatusBadge status={reply.status_to} />
160
+ </span>
161
+ ) : undefined,
162
+ },
163
+ });
164
+ });
165
+ comments.forEach((comment, index) => {
166
+ // Shown as hidden — no name, no text — to anyone but its writer and an admin.
167
+ const veiled = comment.hidden && !comment.mine && !isAdmin;
168
+ const actions: ReactNode[] = [];
169
+ if (comment.hidden) {
170
+ actions.push(
171
+ <span key="hidden" className="inline-flex items-center gap-1" data-feedback-comment-hidden="">
172
+ <EyeOff className="size-3" aria-hidden />
173
+ {labels.hidden}
174
+ </span>,
175
+ );
176
+ }
177
+ if (isAdmin && onHide) {
178
+ actions.push(
179
+ <Button
180
+ key="hide"
181
+ type="button"
182
+ variant="link"
183
+ tone="muted"
184
+ size="sm"
185
+ commit={commit}
186
+ // Caught as Delete's is (0.38.1): a rejection is the app's to toast — its
187
+ // mutation's `onError` — and must not surface as an unhandled one.
188
+ onClick={() => void hideComment(onHide, comment).catch(() => {})}
189
+ >
190
+ {comment.hidden ? labels.unhide : labels.hide}
191
+ </Button>,
192
+ );
193
+ }
194
+ if (comment.mine && onDelete) {
195
+ actions.push(
196
+ <DeleteCommentButton key="delete" comment={comment} onDelete={onDelete} commit={commit} labels={labels} />,
197
+ );
198
+ }
199
+ entries.push({
200
+ at: instant(comment.created_at),
201
+ // Replies first on a tie, then the comments in their order.
202
+ order: replies.length + index,
203
+ message: {
204
+ id: `comment-${comment.id}`,
205
+ own: comment.mine,
206
+ author: veiled ? undefined : comment.mine ? undefined : (comment.author_name ?? anonymous),
207
+ createdAt: comment.created_at,
208
+ body: veiled ? "" : (comment.body ?? ""),
209
+ staff: !veiled && comment.staff,
210
+ struck: comment.hidden && isAdmin && !veiled,
211
+ footer: actions.length > 0 ? actions : undefined,
212
+ },
213
+ });
214
+ });
215
+ entries.sort((a, b) => a.at - b.at || a.order - b.order);
216
+
217
+ return (
218
+ <div className={className} data-feedback-comment-thread="">
219
+ <FeedbackThread
220
+ messages={entries.map((entry) => entry.message)}
221
+ loading={loading}
222
+ now={now}
223
+ labels={{ thread: labels.comments, empty: labels.empty }}
224
+ />
225
+ {onComment && (
226
+ <ChatComposer
227
+ className="mt-3"
228
+ onSend={(body) => onComment(body)}
229
+ pending={pending}
230
+ commit={commit}
231
+ maxLength={FEEDBACK_COMMENT_MAX}
232
+ showCount
233
+ labels={{ field: labels.field, placeholder: labels.placeholder, send: labels.send }}
234
+ />
235
+ )}
236
+ </div>
237
+ );
238
+ }
239
+
240
+ /** The writer's Delete: a confirm (danger tone, focus on Cancel), then `onDelete`. Its
241
+ * own component, so `useConfirm` — which needs a `ConfirmProvider` — is only called
242
+ * where there is something to delete. */
243
+ function DeleteCommentButton({
244
+ comment,
245
+ onDelete,
246
+ commit,
247
+ labels,
248
+ }: {
249
+ comment: FeedbackComment;
250
+ onDelete: (comment: FeedbackComment) => void | Promise<unknown>;
251
+ commit?: CommitScope;
252
+ labels: FeedbackCommentsLabels;
253
+ }) {
254
+ const confirm = useConfirm();
255
+ const remove = async () => {
256
+ const ok = await confirm({
257
+ title: labels.deleteTitle,
258
+ body: labels.deleteBody,
259
+ confirmLabel: labels.delete,
260
+ tone: "danger",
261
+ commit,
262
+ });
263
+ if (ok) await onDelete(comment);
264
+ };
265
+ return (
266
+ <Button
267
+ type="button"
268
+ variant="link"
269
+ tone="danger"
270
+ size="sm"
271
+ commit={commit}
272
+ onClick={() => void remove().catch(() => {})}
273
+ >
274
+ {labels.delete}
275
+ </Button>
276
+ );
277
+ }
@@ -25,4 +25,5 @@ export const DEFAULT_FEEDBACK_DIALOG_LABELS: FeedbackDialogTextLabels = {
25
25
  submitHint: (apple) => (apple ? "⌘ Enter to send" : "Ctrl+Enter to send"),
26
26
  cancel: "Cancel",
27
27
  save: "Send",
28
+ publicNotice: "Other users will see the title and text (never your screenshots or files).",
28
29
  };
@@ -1,4 +1,4 @@
1
- import { useState } from "react";
1
+ import { useId, useState } from "react";
2
2
  import type { ReactNode } from "react";
3
3
  import { Button } from "../components/button";
4
4
  import { Input } from "../components/input";
@@ -93,6 +93,11 @@ export interface FeedbackDialogTextLabels {
93
93
  submitHint: FeedbackSubmitHint;
94
94
  cancel: string;
95
95
  save: string;
96
+ /** 0.38 (docs/feedback-harmonization.md §9.2 decision 6): the line under the body when
97
+ * reports are public — `useFeedbackSubmit({ publicNotice: true })` shows it. Optional,
98
+ * so a complete `UiKitLabels` typed before 0.38 still compiles. de-CH "Andere Personen
99
+ * sehen Titel und Text (nie Ihre Screenshots oder Dateien)." */
100
+ publicNotice?: string;
96
101
  }
97
102
 
98
103
  // Defined in ./feedback-dialog-labels, which holds no component code (0.34, KS U8).
@@ -161,6 +166,14 @@ interface FeedbackDialogBaseProps {
161
166
  /** The attachment field's `buttonSize` (0.25.0) — with {@link attachmentButtonVariant}.
162
167
  * Default `"md"`; `"sm"` draws the paperclip and camera at 14px. */
163
168
  attachmentButtonSize?: ButtonSize;
169
+ /**
170
+ * A muted line under the body saying who will read it (0.38, docs/feedback-harmonization.md
171
+ * §9.2 decision 6) — the kit's sentence is `feedbackDialog.publicNotice`, "Other users
172
+ * will see the title and text (never your screenshots or files)", which
173
+ * `useFeedbackSubmit({ publicNotice: true })` passes. Attached to the body field with
174
+ * `aria-describedby`, so it is heard where the text is written. Left out: no line.
175
+ */
176
+ publicNotice?: ReactNode;
164
177
  }
165
178
 
166
179
  /** One attachment — a screenshot, a picked or a pasted image; a second replaces
@@ -228,6 +241,7 @@ export function FeedbackDialog(props: FeedbackDialogProps) {
228
241
  requireBody = true,
229
242
  attachmentButtonVariant,
230
243
  attachmentButtonSize,
244
+ publicNotice,
231
245
  } = props;
232
246
  const [title, setTitle] = useState("");
233
247
  const [body, setBody] = useState("");
@@ -236,6 +250,8 @@ export function FeedbackDialog(props: FeedbackDialogProps) {
236
250
  const [screenshot, setScreenshot] = useState<File | null>(null);
237
251
  const [files, setFiles] = useState<File[]>([]);
238
252
  const isMobile = usePhoneLayout();
253
+ const noticeId = useId();
254
+ const hasNotice = publicNotice !== undefined && publicNotice !== null && publicNotice !== false && publicNotice !== "";
239
255
  const labels = useKitLabels("feedbackDialog", DEFAULT_FEEDBACK_DIALOG_LABELS, labelsProp);
240
256
  const attachmentText = useKitLabels("feedbackAttachment", DEFAULT_FEEDBACK_ATTACHMENT_LABELS, labelsProp);
241
257
  // Under `multiple` the heading is `attachmentList` — a screenshot AND photos — which
@@ -295,14 +311,26 @@ export function FeedbackDialog(props: FeedbackDialogProps) {
295
311
  const subjectField = (
296
312
  <Input label={labels.subject} value={title} onChange={(e) => setTitle(e.target.value)} variant="display" />
297
313
  );
298
- const bodyField = (
314
+ const bodyTextarea = (
299
315
  <Textarea
300
316
  rows={5}
301
317
  label={requireBody ? labels.body : (labelsProp?.body ?? labels.bodyOptional ?? labels.body)}
302
318
  value={body}
303
319
  onChange={(e) => setBody(e.target.value)}
320
+ aria-describedby={hasNotice ? noticeId : undefined}
304
321
  />
305
322
  );
323
+ // 0.38: who will read it, right under what they are writing (§9.2 decision 6).
324
+ const bodyField = hasNotice ? (
325
+ <div className="space-y-1">
326
+ {bodyTextarea}
327
+ <p id={noticeId} className="text-xs text-muted" data-feedback-public-notice="">
328
+ {publicNotice}
329
+ </p>
330
+ </div>
331
+ ) : (
332
+ bodyTextarea
333
+ );
306
334
  const fieldProps = {
307
335
  labels: attachmentLabels,
308
336
  accept: attachmentAccept,
@@ -92,6 +92,17 @@ export interface FeedbackToastLabels {
92
92
  /** The Undo landed — `status` is the RESTORED status's label.
93
93
  * de-CH: "Zurück auf „{{status}}“: {{title}}" */
94
94
  statusRestored: (status: string, title: string) => string;
95
+ /** 0.38.1: `POST /feedback/{id}/comments` failed with no `detail` of its own — for
96
+ * `FeedbackCommentThread`'s `onComment`. de-CH: "Kommentar konnte nicht gesendet
97
+ * werden." */
98
+ commentFailed: string;
99
+ /** 0.38.1: `PUT` / `DELETE /feedback/{id}/vote` failed with no `detail` of its own —
100
+ * for `FeedbackVoteButton`'s `onVote` (the button rolls back; the app toasts).
101
+ * de-CH: "Stimme konnte nicht gespeichert werden." */
102
+ voteFailed: string;
103
+ /** 0.38.1: `POST /feedback/{id}/replies` failed with no `detail` of its own — for
104
+ * `FeedbackRowDetail`'s `onReply`. de-CH: "Antwort konnte nicht gesendet werden." */
105
+ replyFailed: string;
95
106
  }
96
107
 
97
108
  export const DEFAULT_FEEDBACK_TOAST_LABELS: FeedbackToastLabels = {
@@ -108,6 +119,9 @@ export const DEFAULT_FEEDBACK_TOAST_LABELS: FeedbackToastLabels = {
108
119
  statusChanged: (status, title) => `Set to “${status}”: ${title}`,
109
120
  statusUndo: "Undo",
110
121
  statusRestored: (status, title) => `Back to “${status}”: ${title}`,
122
+ commentFailed: "Could not post the comment.",
123
+ voteFailed: "Could not save your vote.",
124
+ replyFailed: "Could not send the reply.",
111
125
  };
112
126
 
113
127
  /** `feedbackStatus` resolved: English, then `<UiKitProvider labels>`, then `labels`. For
@@ -8,4 +8,5 @@ export const DEFAULT_FEEDBACK_MENU_LABELS: FeedbackMenuLabels = {
8
8
  trigger: "Send feedback",
9
9
  myFeedback: "My feedback",
10
10
  viewFeedback: "View feedback",
11
+ allFeedback: "All feedback",
11
12
  };
@@ -1,4 +1,4 @@
1
- import { Inbox, MessageSquare } from "lucide-react";
1
+ import { Inbox, MessageSquare, Users } from "lucide-react";
2
2
  import type { UserAvatarBadge } from "../components/user-avatar";
3
3
  import { useKitLabels, type LabelOverride } from "../i18n/kit-labels";
4
4
  import { TopBarActionMenu, type TopBarMenuEntry } from "../shell/topbar-action-menu";
@@ -25,6 +25,10 @@ export interface FeedbackMenuLabels {
25
25
  /** The link to the inbox — an admin's one list link, in place of "My feedback".
26
26
  * keksdose `feedback.view_all`. */
27
27
  viewFeedback: string;
28
+ /** The link to every public report, {@link FeedbackMenuProps.publicHref} (0.38,
29
+ * docs/feedback-harmonization.md §9.2 decision 21). Optional, so a complete
30
+ * `UiKitLabels` typed before 0.38 still compiles. de-CH "Alle Rückmeldungen". */
31
+ allFeedback?: string;
28
32
  }
29
33
 
30
34
  // Defined in ./feedback-menu-labels, which holds no component code (0.34, KS U8).
@@ -43,7 +47,7 @@ export interface FeedbackMenuProps {
43
47
  * `Sparkles` Help assistant → `/assistant` and `MessagesSquare` Support chat →
44
48
  * `/support` with its unread chip. Any {@link TopBarMenuEntry}; keep their `key`s clear
45
49
  * of the menu's own (`BUG`, `IDEA`, `QUESTION`, `OTHER`, `feedback-divider`,
46
- * `my-feedback`, `view-feedback`).
50
+ * `all-feedback`, `my-feedback`, `view-feedback`).
47
51
  */
48
52
  extraEntries?: TopBarMenuEntry[];
49
53
  /** The user's own reports, linked for everyone but an admin. Default `"/my-feedback"`
@@ -51,6 +55,15 @@ export interface FeedbackMenuProps {
51
55
  myFeedbackHref?: string;
52
56
  /** The admin inbox, linked for an admin. Default `"/feedback"` (§2.3). */
53
57
  inboxHref?: string;
58
+ /**
59
+ * The public list (0.38, docs/feedback-harmonization.md §9.2 decision 21): when passed,
60
+ * an **All feedback** row (`Users`, key `all-feedback`) above the list link, for
61
+ * everyone — the admin too, who reads what the others read there. keksdose:
62
+ * `"/all-feedback"`; "My feedback" stays as it is. Leave it out where the app has no
63
+ * public parts (kastlan) or the viewer may not see them (a Kurvenschmiede customer, a
64
+ * demo — §9.2 decisions 7, 8).
65
+ */
66
+ publicHref?: string;
54
67
  /** A dot on the trigger — keksdose's unread support replies (`tone: "danger"`). Its
55
68
  * `label` joins the trigger's name ("Send feedback 2 unread"). The words are the app's. */
56
69
  iconBadge?: UserAvatarBadge | null;
@@ -70,7 +83,8 @@ export interface FeedbackMenuProps {
70
83
  * the category badges' own glyphs), each calling `onFile(category)`;
71
84
  * 3. a divider, always;
72
85
  * 4. the app's `extraEntries`;
73
- * 5. ONE list link, `Inbox`: an admin's is "View feedback" → `/feedback`, everyone
86
+ * 5. with `publicHref` (0.38), **All feedback** → the public list, for everyone;
87
+ * 6. ONE list link, `Inbox`: an admin's is "View feedback" → `/feedback`, everyone
74
88
  * else's "My feedback" → `/my-feedback` — keksdose's (`top-bar.tsx:205-214`, feedback
75
89
  * #331: admins get the management view, everyone else their own submissions).
76
90
  *
@@ -96,6 +110,7 @@ export function FeedbackMenu({
96
110
  extraEntries,
97
111
  myFeedbackHref = "/my-feedback",
98
112
  inboxHref = "/feedback",
113
+ publicHref,
99
114
  iconBadge,
100
115
  labels: labelsProp,
101
116
  align,
@@ -114,6 +129,17 @@ export function FeedbackMenu({
114
129
  }),
115
130
  { kind: "divider", key: "feedback-divider" },
116
131
  ...(extraEntries ?? []),
132
+ ...(publicHref
133
+ ? [
134
+ {
135
+ kind: "link",
136
+ key: "all-feedback",
137
+ to: publicHref,
138
+ icon: <Users className="size-4" />,
139
+ label: labels.allFeedback ?? DEFAULT_FEEDBACK_MENU_LABELS.allFeedback!,
140
+ } satisfies TopBarMenuEntry,
141
+ ]
142
+ : []),
117
143
  {
118
144
  kind: "link",
119
145
  key: isAdmin ? "view-feedback" : "my-feedback",
@@ -0,0 +1,80 @@
1
+ // The English words of the public feedback pages (round 0.38, docs/feedback-harmonization.md
2
+ // §9.15) — `FeedbackPublicList`, `FeedbackPublicDetail` and `FeedbackPublicSwitch` — in a
3
+ // module with no component code: the English catalogue (src/i18n/defaults.ts) imports
4
+ // them without the components, as every `*-labels.ts` (0.34, KS U8).
5
+
6
+ /**
7
+ * `feedbackPublic` — the words of the pages every user reads (§9.3, §9.9): the list's
8
+ * sort, search and count, the detail's sections and the "Visible to others" switch. The
9
+ * table's column names that the inbox already has (#, Date, Category, Subject, Status)
10
+ * stay `feedbackPage`'s, so the two lists cannot disagree; the vote button's words are
11
+ * `feedbackVote`'s, the thread's `feedbackComments`'.
12
+ *
13
+ * de-CH canon (§9.2, §9.5): `title` "Alle Rückmeldungen", `anonymous` "Eine Person".
14
+ */
15
+ export interface FeedbackPublicLabels {
16
+ /** The page's heading — keksdose's `/all-feedback`, the menu's "All feedback". */
17
+ title: string;
18
+ /** The sort toggle's accessible name. */
19
+ sort: string;
20
+ /** `sort=newest`, the server's default. */
21
+ sortNewest: string;
22
+ /** `sort=votes`: most votes, then newest. */
23
+ sortVotes: string;
24
+ /** `sort=updated`. */
25
+ sortUpdated: string;
26
+ /** The search box's accessible name: it searches titles only (§9.9). */
27
+ search: string;
28
+ /** The Votes column's header. */
29
+ columnVotes: string;
30
+ /** How many reports the server has for this query (`total`). */
31
+ total: (count: number) => string;
32
+ /** A row's visible comments, the chip beside its title. */
33
+ comments: (count: number) => string;
34
+ /** A row's votes, the count on a phone card (the button is in the opened report). */
35
+ votes: (count: number) => string;
36
+ /** A `null` name (§9.5): an incomplete or erased account. */
37
+ anonymous: string;
38
+ /** Who filed it, with the server's first name (or {@link anonymous}). */
39
+ filedBy: (name: string) => string;
40
+ /** The detail's public body section. */
41
+ body: string;
42
+ /** The detail's merged replies and comments (§9.8, "Order"). */
43
+ conversation: string;
44
+ /** While the list or the comments load. */
45
+ loading: string;
46
+ /** No public report (yet, or for this search). */
47
+ empty: string;
48
+ /** `FeedbackPublicSwitch`'s label (§9.3, decision 6). */
49
+ visibleToOthers: string;
50
+ /** The line under it: what others see, and what never. */
51
+ visibleHint: string;
52
+ /** Why the author cannot flip it now: the report is past its open window (§9.3). */
53
+ visibleLocked: string;
54
+ /** 0.38.1: the pager's "where am I" — `page` counts from 1. The server answers no
55
+ * total, so there is no "of N". */
56
+ page: (page: number) => string;
57
+ }
58
+
59
+ export const DEFAULT_FEEDBACK_PUBLIC_LABELS: FeedbackPublicLabels = {
60
+ title: "All feedback",
61
+ sort: "Sort",
62
+ sortNewest: "Newest",
63
+ sortVotes: "Most wanted",
64
+ sortUpdated: "Recently updated",
65
+ search: "Search titles",
66
+ columnVotes: "Votes",
67
+ total: (count) => (count === 1 ? "1 report" : `${count} reports`),
68
+ comments: (count) => (count === 1 ? "1 comment" : `${count} comments`),
69
+ votes: (count) => (count === 1 ? "1 vote" : `${count} votes`),
70
+ anonymous: "A user",
71
+ filedBy: (name) => `From ${name}`,
72
+ body: "Description",
73
+ conversation: "Answers and comments",
74
+ loading: "Loading…",
75
+ empty: "No reports yet",
76
+ visibleToOthers: "Visible to others",
77
+ visibleHint: "Other users see the title and text, never your screenshots or files.",
78
+ visibleLocked: "Only while the report is still open.",
79
+ page: (page) => `Page ${page}`,
80
+ };