@eifi1/ui-kit 0.46.0 → 0.47.0

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 (148) hide show
  1. package/README.md +12 -11
  2. package/dist/admin/user-roster.d.ts +6 -2
  3. package/dist/admin/user-roster.js +7 -3
  4. package/dist/admin/user-roster.js.map +1 -1
  5. package/dist/components/data-table-row-actions.d.ts +22 -5
  6. package/dist/components/data-table-row-actions.js +14 -5
  7. package/dist/components/data-table-row-actions.js.map +1 -1
  8. package/dist/components/data-table.d.ts +16 -1
  9. package/dist/components/data-table.js +20 -1
  10. package/dist/components/data-table.js.map +1 -1
  11. package/dist/components/row-actions.d.ts +37 -5
  12. package/dist/components/row-actions.js +22 -5
  13. package/dist/components/row-actions.js.map +1 -1
  14. package/dist/components/tabs.d.ts +24 -16
  15. package/dist/components/tabs.js +37 -31
  16. package/dist/components/tabs.js.map +1 -1
  17. package/dist/components/translation-review.js +3 -1
  18. package/dist/components/translation-review.js.map +1 -1
  19. package/dist/components/ui-labels.js +1 -1
  20. package/dist/components/ui-labels.js.map +1 -1
  21. package/dist/data-table.d.ts +1 -0
  22. package/dist/data-table.js +2 -0
  23. package/dist/data-table.js.map +1 -1
  24. package/dist/eslint/index.js +1 -1
  25. package/dist/feedback/feedback-attachment-download.d.ts +19 -0
  26. package/dist/feedback/feedback-attachment-download.js +59 -0
  27. package/dist/feedback/feedback-attachment-download.js.map +1 -0
  28. package/dist/feedback/feedback-comment-thread.d.ts +17 -6
  29. package/dist/feedback/feedback-comment-thread.js +21 -2
  30. package/dist/feedback/feedback-comment-thread.js.map +1 -1
  31. package/dist/feedback/feedback-entry-file.d.ts +61 -0
  32. package/dist/feedback/feedback-entry-file.js +76 -0
  33. package/dist/feedback/feedback-entry-file.js.map +1 -0
  34. package/dist/feedback/feedback-public-list.d.ts +18 -5
  35. package/dist/feedback/feedback-public-list.js +8 -0
  36. package/dist/feedback/feedback-public-list.js.map +1 -1
  37. package/dist/feedback/feedback-public-types.d.ts +7 -2
  38. package/dist/feedback/feedback-replies.d.ts +26 -9
  39. package/dist/feedback/feedback-replies.js +20 -10
  40. package/dist/feedback/feedback-replies.js.map +1 -1
  41. package/dist/feedback/feedback-row-detail-labels.js +0 -1
  42. package/dist/feedback/feedback-row-detail-labels.js.map +1 -1
  43. package/dist/feedback/feedback-row-detail.d.ts +23 -10
  44. package/dist/feedback/feedback-row-detail.js +10 -54
  45. package/dist/feedback/feedback-row-detail.js.map +1 -1
  46. package/dist/feedback.d.ts +1 -0
  47. package/dist/feedback.js +1 -0
  48. package/dist/feedback.js.map +1 -1
  49. package/dist/i18n/german.js +1 -6
  50. package/dist/i18n/german.js.map +1 -1
  51. package/dist/i18n/locales/en.js +1 -6
  52. package/dist/i18n/locales/en.js.map +2 -2
  53. package/dist/i18n/locales/es.js +1 -6
  54. package/dist/i18n/locales/es.js.map +1 -1
  55. package/dist/i18n/locales/fr.js +1 -6
  56. package/dist/i18n/locales/fr.js.map +1 -1
  57. package/dist/i18n/locales/hu.js +1 -6
  58. package/dist/i18n/locales/hu.js.map +1 -1
  59. package/dist/i18n/locales/it.js +1 -6
  60. package/dist/i18n/locales/it.js.map +1 -1
  61. package/dist/i18n/locales/zh.js +1 -6
  62. package/dist/i18n/locales/zh.js.map +1 -1
  63. package/dist/i18n/review.js +0 -1
  64. package/dist/i18n/review.js.map +1 -1
  65. package/dist/index.d.ts +5 -7
  66. package/dist/index.js +14 -5
  67. package/dist/index.js.map +1 -1
  68. package/dist/notifications/device-zone-report.d.ts +7 -0
  69. package/dist/notifications/device-zone-report.js +8 -1
  70. package/dist/notifications/device-zone-report.js.map +1 -1
  71. package/dist/notifications/feedback-notification-types.d.ts +14 -1
  72. package/dist/notifications/feedback-notification-types.js +5 -1
  73. package/dist/notifications/feedback-notification-types.js.map +1 -1
  74. package/dist/notifications/notification-bell.d.ts +1 -1
  75. package/dist/notifications/notification-bell.js.map +1 -1
  76. package/dist/notifications/notification-inbox.d.ts +12 -3
  77. package/dist/notifications/notification-inbox.js +2 -2
  78. package/dist/notifications/notification-inbox.js.map +1 -1
  79. package/dist/notifications/notification-types.d.ts +0 -3
  80. package/dist/notifications/notifications-labels.d.ts +2 -13
  81. package/dist/notifications/notifications-labels.js +0 -4
  82. package/dist/notifications/notifications-labels.js.map +1 -1
  83. package/dist/notifications/quiet-hours-setting.d.ts +3 -8
  84. package/dist/notifications/quiet-hours-setting.js +8 -3
  85. package/dist/notifications/quiet-hours-setting.js.map +1 -1
  86. package/dist/offline/local-first.d.ts +9 -0
  87. package/dist/offline/local-first.js +14 -0
  88. package/dist/offline/local-first.js.map +1 -1
  89. package/dist/offline.d.ts +1 -1
  90. package/dist/offline.js +2 -1
  91. package/dist/offline.js.map +1 -1
  92. package/dist/sync/db.d.ts +17 -0
  93. package/dist/sync/db.js +10 -1
  94. package/dist/sync/db.js.map +1 -1
  95. package/dist/sync/engine.d.ts +11 -9
  96. package/dist/sync/engine.js.map +1 -1
  97. package/dist/sync/hooks.d.ts +5 -0
  98. package/dist/sync/hooks.js.map +1 -1
  99. package/dist/sync.d.ts +2 -2
  100. package/dist/sync.js +5 -2
  101. package/dist/sync.js.map +1 -1
  102. package/dist/theme/theme-store.d.ts +4 -4
  103. package/dist/theme/theme-store.js +1 -0
  104. package/dist/theme/theme-store.js.map +1 -1
  105. package/package.json +2 -2
  106. package/src/admin/user-roster.tsx +12 -3
  107. package/src/components/data-table-row-actions.tsx +33 -9
  108. package/src/components/data-table.tsx +46 -3
  109. package/src/components/row-actions.tsx +73 -10
  110. package/src/components/tabs.tsx +66 -44
  111. package/src/components/translation-review.tsx +3 -1
  112. package/src/components/ui-labels.ts +1 -1
  113. package/src/data-table.ts +3 -0
  114. package/src/feedback/feedback-attachment-download.tsx +77 -0
  115. package/src/feedback/feedback-comment-thread.tsx +42 -7
  116. package/src/feedback/feedback-entry-file.tsx +150 -0
  117. package/src/feedback/feedback-public-list.tsx +25 -4
  118. package/src/feedback/feedback-public-types.ts +7 -2
  119. package/src/feedback/feedback-replies.tsx +46 -19
  120. package/src/feedback/feedback-row-detail-labels.ts +0 -1
  121. package/src/feedback/feedback-row-detail.tsx +35 -86
  122. package/src/feedback.ts +1 -0
  123. package/src/i18n/german.ts +1 -6
  124. package/src/i18n/locales/es.ts +1 -6
  125. package/src/i18n/locales/fr.ts +1 -6
  126. package/src/i18n/locales/hu.ts +1 -6
  127. package/src/i18n/locales/it.ts +1 -6
  128. package/src/i18n/locales/zh.ts +1 -6
  129. package/src/i18n/review.ts +0 -1
  130. package/src/index.ts +19 -5
  131. package/src/notifications/device-zone-report.ts +14 -1
  132. package/src/notifications/feedback-notification-types.ts +19 -1
  133. package/src/notifications/notification-bell.tsx +1 -1
  134. package/src/notifications/notification-inbox.tsx +15 -5
  135. package/src/notifications/notification-types.ts +0 -3
  136. package/src/notifications/notifications-labels.ts +2 -17
  137. package/src/notifications/quiet-hours-setting.tsx +16 -11
  138. package/src/offline/local-first.ts +24 -0
  139. package/src/offline.ts +1 -1
  140. package/src/sync/db.ts +22 -0
  141. package/src/sync/engine.ts +11 -9
  142. package/src/sync/hooks.ts +5 -0
  143. package/src/sync.ts +2 -2
  144. package/src/theme/theme-store.ts +13 -5
  145. package/dist/notifications/time-zone-select.d.ts +0 -53
  146. package/dist/notifications/time-zone-select.js +0 -76
  147. package/dist/notifications/time-zone-select.js.map +0 -1
  148. package/src/notifications/time-zone-select.tsx +0 -129
@@ -1,6 +1,8 @@
1
1
  import type { CommitScope } from "../components/write-lock.js";
2
+ import type { AuthedFetcher } from "../hooks/use-authed-src.js";
2
3
  import { type LabelOverride } from "../i18n/kit-labels.js";
3
4
  import type { FeedbackComment, FeedbackReply } from "./feedback-public-types.js";
5
+ import { type FeedbackEntryUploadOptions } from "./feedback-entry-file.js";
4
6
  import { DEFAULT_FEEDBACK_COMMENTS_LABELS } from "./feedback-thread-parts-labels.js";
5
7
  /**
6
8
  * A report's conversation as everyone who may read it sees it
@@ -20,8 +22,13 @@ import { DEFAULT_FEEDBACK_COMMENTS_LABELS } from "./feedback-thread-parts-labels
20
22
  * sees their text with that line; the admin sees it struck through, with **Unhide**.
21
23
  * An admin has **Hide** on every visible comment; the writer **Delete** on their own,
22
24
  * after a confirm (it is a hard delete). Nobody edits a comment.
23
- * - **The composer** (1–4 000 characters, no files) only with `onComment`: left out for
24
- * a reader who may not write (a demo, a CRASH report — the app knows).
25
+ * - **The composer** (1–4 000 characters) only with `onComment`: left out for a reader
26
+ * who may not write (a demo, a CRASH report — the app knows). Since 0.47 (§12) with
27
+ * `onUpload` it takes one file as the rework note does — pasted into the box,
28
+ * captured or picked — uploaded first, its URL `onComment`'s second argument.
29
+ * - **Files** (0.47, §12): a comment's or an entry's file under its text, a picture
30
+ * INLINE — a thumbnail through `fetcher` that opens full size — any other file a
31
+ * download ({@link FeedbackEntryFile}). A veiled comment shows none.
25
32
  *
26
33
  * Delete asks through `useConfirm`, so a thread with `onDelete` and a comment of the
27
34
  * viewer's needs the app's `ConfirmProvider` (each app mounts one already).
@@ -54,7 +61,7 @@ export interface FeedbackCommentsLabels {
54
61
  send: string;
55
62
  }
56
63
  export { DEFAULT_FEEDBACK_COMMENTS_LABELS };
57
- export interface FeedbackCommentThreadProps {
64
+ export interface FeedbackCommentThreadProps extends FeedbackEntryUploadOptions {
58
65
  /** The report's replies (§9.7), oldest first. Left out or `[]`: none.
59
66
  *
60
67
  * Inside `FeedbackRowDetail`'s `comments` they are dropped (0.38.1): the detail's
@@ -71,8 +78,12 @@ export interface FeedbackCommentThreadProps {
71
78
  /** `POST /feedback/{id}/comments`. Return the promise: the box clears when it
72
79
  * resolves, and keeps the draft when it rejects. A failure is the app's to toast —
73
80
  * the server's `detail`, else `feedbackToast.commentFailed` (0.38.1). Left out, no
74
- * composer. */
75
- onComment?: (body: string) => void | Promise<unknown>;
81
+ * composer. 0.47 (§12): `attachmentUrl` is the one file uploaded with it (with
82
+ * `onUpload`), `undefined` for none — `attachment_url` in the body (server-kit 0.18). */
83
+ onComment?: (body: string, attachmentUrl?: string) => void | Promise<unknown>;
84
+ /** 0.47: the app's authenticated GET (`AuthedImage`'s) for the files in the thread.
85
+ * Left out, the browser's own fetch with same-origin cookies. */
86
+ fetcher?: AuthedFetcher | null;
76
87
  /** `PATCH /feedback/{id}/comments/{comment_id} {hidden}` — the admin's. A rejection
77
88
  * is caught (0.38.1) and is the app's to toast (`feedbackToast.updateFailed`). Left
78
89
  * out, no Hide / Unhide. */
@@ -91,4 +102,4 @@ export interface FeedbackCommentThreadProps {
91
102
  labels?: LabelOverride<FeedbackCommentsLabels>;
92
103
  className?: string;
93
104
  }
94
- export declare function FeedbackCommentThread({ replies: repliesProp, comments, isAdmin, isAuthor, onComment, onHide, onDelete, pending, loading, commit, now, labels: labelsProp, className, }: FeedbackCommentThreadProps): import("react").JSX.Element;
105
+ export declare function FeedbackCommentThread({ replies: repliesProp, comments, isAdmin, isAuthor, onComment, onHide, onDelete, pending, loading, commit, now, fetcher, onUpload, onCaptureScreenshot, attachment, labels: labelsProp, className, }: FeedbackCommentThreadProps): import("react").JSX.Element;
@@ -8,6 +8,7 @@ import { useKitLabels } from "../i18n/kit-labels.js";
8
8
  import { FeedbackStatusBadge } from "./feedback-inbox.js";
9
9
  import { DEFAULT_FEEDBACK_PUBLIC_LABELS } from "./feedback-public-labels.js";
10
10
  import { FEEDBACK_COMMENT_MAX, attachmentName } from "./feedback-record.js";
11
+ import { FeedbackEntryFile, useFeedbackEntryUpload } from "./feedback-entry-file.js";
11
12
  import { useFeedbackRepliesLabels } from "./feedback-replies.js";
12
13
  import { FeedbackRepliesDrawnAboveContext } from "./feedback-replies-context.js";
13
14
  import { FeedbackReworkEntryChip } from "./feedback-rework-entry-chip.js";
@@ -32,9 +33,18 @@ function FeedbackCommentThread({
32
33
  loading = false,
33
34
  commit,
34
35
  now,
36
+ fetcher,
37
+ onUpload,
38
+ onCaptureScreenshot,
39
+ attachment,
35
40
  labels: labelsProp,
36
41
  className
37
42
  }) {
43
+ const { composerAttachment, upload, uploading } = useFeedbackEntryUpload({
44
+ onUpload,
45
+ onCaptureScreenshot,
46
+ attachment
47
+ });
38
48
  const labels = useKitLabels("feedbackComments", DEFAULT_FEEDBACK_COMMENTS_LABELS, labelsProp);
39
49
  const anonymous = useKitLabels("feedbackPublic", DEFAULT_FEEDBACK_PUBLIC_LABELS).anonymous;
40
50
  const replyLabels = useFeedbackRepliesLabels();
@@ -73,6 +83,8 @@ function FeedbackCommentThread({
73
83
  author: reply.author_name ?? replyLabels.admin,
74
84
  createdAt: reply.created_at,
75
85
  body: reply.body,
86
+ // 0.47 (§12): an admin reply's file, where the view carries it.
87
+ attachments: reply.attachment_url ? [{ name: attachmentName(reply.attachment_url), url: reply.attachment_url }] : void 0,
76
88
  staff: true,
77
89
  badge: reply.status_to ? /* @__PURE__ */ jsxs("span", { className: "inline-flex min-w-0 max-w-full flex-wrap items-center gap-1", "data-feedback-reply-status": reply.status_to, children: [
78
90
  /* @__PURE__ */ jsx("span", { className: "sr-only", children: replyLabels.statusChanged }),
@@ -126,6 +138,8 @@ function FeedbackCommentThread({
126
138
  author: veiled ? void 0 : comment.mine ? void 0 : comment.author_name ?? anonymous,
127
139
  createdAt: comment.created_at,
128
140
  body: veiled ? "" : comment.body ?? "",
141
+ // 0.47 (§12): its one file; none when veiled (the server sends none either).
142
+ attachments: !veiled && comment.attachment_url ? [{ name: attachmentName(comment.attachment_url), url: comment.attachment_url }] : void 0,
129
143
  staff: !veiled && comment.staff,
130
144
  struck: comment.hidden && isAdmin && !veiled,
131
145
  footer: actions.length > 0 ? actions : void 0
@@ -140,6 +154,7 @@ function FeedbackCommentThread({
140
154
  messages: entries.map((entry) => entry.message),
141
155
  loading,
142
156
  now,
157
+ renderAttachment: (file) => file.url ? /* @__PURE__ */ jsx(FeedbackEntryFile, { url: file.url, fetcher }) : file.name,
143
158
  labels: { thread: labels.comments, empty: labels.empty }
144
159
  }
145
160
  ),
@@ -147,8 +162,12 @@ function FeedbackCommentThread({
147
162
  ChatComposer,
148
163
  {
149
164
  className: "mt-3",
150
- onSend: (body) => onComment(body),
151
- pending,
165
+ onSend: async (body, file) => {
166
+ const url = await upload(file);
167
+ return url === void 0 ? onComment(body) : onComment(body, url);
168
+ },
169
+ pending: pending || uploading,
170
+ attachment: composerAttachment,
152
171
  commit,
153
172
  maxLength: FEEDBACK_COMMENT_MAX,
154
173
  showCount: true,
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/feedback/feedback-comment-thread.tsx"],"sourcesContent":["import { useContext } from \"react\";\nimport type { ReactNode } from \"react\";\nimport { EyeOff } from \"lucide-react\";\nimport { Button } from \"../components/button\";\nimport { useConfirm } from \"../components/confirm-dialog\";\nimport type { CommitScope } from \"../components/write-lock\";\nimport { useKitLabels, type LabelOverride } from \"../i18n/kit-labels\";\nimport { FeedbackStatusBadge } from \"./feedback-inbox\";\nimport type { FeedbackComment, FeedbackReply } from \"./feedback-public-types\";\nimport { DEFAULT_FEEDBACK_PUBLIC_LABELS } from \"./feedback-public-labels\";\nimport { FEEDBACK_COMMENT_MAX, attachmentName } from \"./feedback-record\";\nimport { useFeedbackRepliesLabels } from \"./feedback-replies\";\nimport { FeedbackRepliesDrawnAboveContext } from \"./feedback-replies-context\";\nimport { FeedbackReworkEntryChip } from \"./feedback-rework-entry-chip\";\nimport { ChatComposer, FeedbackThread } from \"./feedback-thread\";\nimport type { FeedbackThreadMessage } from \"./feedback-thread\";\nimport { DEFAULT_FEEDBACK_COMMENTS_LABELS } from \"./feedback-thread-parts-labels\";\n\n/**\n * A report's conversation as everyone who may read it sees it\n * (docs/feedback-harmonization.md §9.8): the admin's replies and other people's\n * comments, each its own row, oldest first — merged by time, on the\n * {@link FeedbackThread} look — and the box to add one.\n *\n * - **Replies** are drawn as staff posts with the status they set (\"→ Done\"); a reply\n * without an author is \"Admin\" (§9.7).\n * - **Reworks** (0.40, §10.3, `kind: \"rework\"`) are the report's author's: the\n * **Rework** chip, \"→ status\" when the entry moved one (a migrated one did not), the\n * file when the view carries it; at the reading end for the author ({@link\n * FeedbackCommentThreadProps.isAuthor}), and a `null` name is \"A user\", never \"Admin\".\n * - **Comments**: the viewer's own at the reading end (\"You\"); someone else's under the\n * first name the server sent, `null` as \"A user\" (§9.5, `feedbackPublic.anonymous`).\n * - **Hidden** (§9.8): others see \"Hidden by an admin\" and no name, no text; the writer\n * sees their text with that line; the admin sees it struck through, with **Unhide**.\n * An admin has **Hide** on every visible comment; the writer **Delete** on their own,\n * after a confirm (it is a hard delete). Nobody edits a comment.\n * - **The composer** (1–4 000 characters, no files) only with `onComment`: left out for\n * a reader who may not write (a demo, a CRASH report — the app knows).\n *\n * Delete asks through `useConfirm`, so a thread with `onDelete` and a comment of the\n * viewer's needs the app's `ConfirmProvider` (each app mounts one already).\n */\n\n/* ── Labels ───────────────────────────────────────────────────────────────── */\n\n/** The `feedbackComments` namespace of `<UiKitProvider labels>`. A `null` name is\n * `feedbackPublic.anonymous`, the public pages' word, so the two say the same. */\nexport interface FeedbackCommentsLabels {\n /** The thread's accessible name. de-CH: \"Kommentare\" */\n comments: string;\n /** Nothing yet. de-CH: \"Noch keine Kommentare\" */\n empty: string;\n /** In place of a hidden comment. de-CH: \"Von einem Admin ausgeblendet\" */\n hidden: string;\n /** The admin's button. de-CH: \"Ausblenden\" */\n hide: string;\n /** The admin's button on a hidden comment. de-CH: \"Einblenden\" */\n unhide: string;\n /** The writer's button. de-CH: \"Löschen\" */\n delete: string;\n /** The confirm's question. de-CH: \"Diesen Kommentar löschen?\" */\n deleteTitle: string;\n /** The confirm's line. de-CH: \"Er wird für alle entfernt. Das kann nicht rückgängig\n * gemacht werden.\" */\n deleteBody: string;\n /** The box's accessible name. de-CH: \"Kommentar schreiben\" */\n field: string;\n /** de-CH: \"Kommentar schreiben…\" */\n placeholder: string;\n /** The send button. de-CH: \"Kommentieren\" */\n send: string;\n}\n\nexport { DEFAULT_FEEDBACK_COMMENTS_LABELS };\n\n/* ── The thread ───────────────────────────────────────────────────────────── */\n\nexport interface FeedbackCommentThreadProps {\n /** The report's replies (§9.7), oldest first. Left out or `[]`: none.\n *\n * Inside `FeedbackRowDetail`'s `comments` they are dropped (0.38.1): the detail's\n * Replies section draws them already, and the thread there is the comments alone —\n * so passing them there cannot draw them twice. */\n replies?: readonly FeedbackReply[];\n /** `GET /feedback/{id}/comments`, oldest first. */\n comments: readonly FeedbackComment[];\n /** The viewer is the app's admin: Hide / Unhide, the hidden text struck through. */\n isAdmin: boolean;\n /** 0.40: the viewer filed the report (the record's `mine`) — its rework entries are\n * theirs, drawn at the reading end as \"You\". Default `false`. */\n isAuthor?: boolean;\n /** `POST /feedback/{id}/comments`. Return the promise: the box clears when it\n * resolves, and keeps the draft when it rejects. A failure is the app's to toast —\n * the server's `detail`, else `feedbackToast.commentFailed` (0.38.1). Left out, no\n * composer. */\n onComment?: (body: string) => void | Promise<unknown>;\n /** `PATCH /feedback/{id}/comments/{comment_id} {hidden}` — the admin's. A rejection\n * is caught (0.38.1) and is the app's to toast (`feedbackToast.updateFailed`). Left\n * out, no Hide / Unhide. */\n onHide?: (comment: FeedbackComment, hidden: boolean) => void | Promise<unknown>;\n /** `DELETE /feedback/{id}/comments/{comment_id}` — the writer's own, after the\n * confirm. Left out, no Delete. */\n onDelete?: (comment: FeedbackComment) => void | Promise<unknown>;\n /** A comment is being sent. */\n pending?: boolean;\n /** The comments are loading. */\n loading?: boolean;\n /** Comment, Hide and Delete COMMIT — see Button's `commit`. */\n commit?: CommitScope;\n /** \"Now\" for the relative times — pin it in tests and stories. */\n now?: Date | number;\n labels?: LabelOverride<FeedbackCommentsLabels>;\n className?: string;\n}\n\ntype Entry = { at: number; order: number; message: FeedbackThreadMessage };\n\n/** The admin's Hide / Unhide as a promise — a sync throw or a `void` return included —\n * so the click can catch it as Delete's does. */\nconst hideComment = async (\n onHide: NonNullable<FeedbackCommentThreadProps[\"onHide\"]>,\n comment: FeedbackComment,\n) => {\n await onHide(comment, !comment.hidden);\n};\n\nconst instant = (iso: string) => {\n const ms = Date.parse(iso);\n return Number.isNaN(ms) ? 0 : ms;\n};\n\nexport function FeedbackCommentThread({\n replies: repliesProp = [],\n comments,\n isAdmin,\n isAuthor = false,\n onComment,\n onHide,\n onDelete,\n pending = false,\n loading = false,\n commit,\n now,\n labels: labelsProp,\n className,\n}: FeedbackCommentThreadProps) {\n const labels = useKitLabels(\"feedbackComments\", DEFAULT_FEEDBACK_COMMENTS_LABELS, labelsProp);\n const anonymous = useKitLabels(\"feedbackPublic\", DEFAULT_FEEDBACK_PUBLIC_LABELS).anonymous;\n const replyLabels = useFeedbackRepliesLabels();\n const repliesDrawnAbove = useContext(FeedbackRepliesDrawnAboveContext);\n const replies = repliesDrawnAbove ? [] : repliesProp;\n\n const entries: Entry[] = [];\n replies.forEach((reply, order) => {\n if (reply.kind === \"rework\") {\n entries.push({\n at: instant(reply.created_at),\n order,\n message: {\n id: `reply-${reply.id}`,\n own: isAuthor,\n author: isAuthor ? undefined : (reply.author_name ?? anonymous),\n createdAt: reply.created_at,\n body: reply.body,\n attachments: reply.attachment_url\n ? [{ name: attachmentName(reply.attachment_url), url: reply.attachment_url }]\n : undefined,\n badge: (\n <span className=\"inline-flex min-w-0 max-w-full flex-wrap items-center gap-1\" data-feedback-entry-kind=\"rework\">\n <FeedbackReworkEntryChip />\n {reply.status_to && (\n <span className=\"inline-flex items-center gap-1\" data-feedback-reply-status={reply.status_to}>\n <span className=\"sr-only\">{replyLabels.statusChanged}</span>\n <span aria-hidden>→</span>\n <FeedbackStatusBadge status={reply.status_to} />\n </span>\n )}\n </span>\n ),\n },\n });\n return;\n }\n entries.push({\n at: instant(reply.created_at),\n order,\n message: {\n id: `reply-${reply.id}`,\n author: reply.author_name ?? replyLabels.admin,\n createdAt: reply.created_at,\n body: reply.body,\n staff: true,\n badge: reply.status_to ? (\n <span className=\"inline-flex min-w-0 max-w-full flex-wrap items-center gap-1\" data-feedback-reply-status={reply.status_to}>\n <span className=\"sr-only\">{replyLabels.statusChanged}</span>\n <span aria-hidden>→</span>\n <FeedbackStatusBadge status={reply.status_to} />\n </span>\n ) : undefined,\n },\n });\n });\n comments.forEach((comment, index) => {\n // Shown as hidden — no name, no text — to anyone but its writer and an admin.\n const veiled = comment.hidden && !comment.mine && !isAdmin;\n const actions: ReactNode[] = [];\n if (comment.hidden) {\n actions.push(\n <span key=\"hidden\" className=\"inline-flex items-center gap-1\" data-feedback-comment-hidden=\"\">\n <EyeOff className=\"size-3\" aria-hidden />\n {labels.hidden}\n </span>,\n );\n }\n if (isAdmin && onHide) {\n actions.push(\n <Button\n key=\"hide\"\n type=\"button\"\n variant=\"link\"\n tone=\"muted\"\n size=\"sm\"\n commit={commit}\n // Caught as Delete's is (0.38.1): a rejection is the app's to toast — its\n // mutation's `onError` — and must not surface as an unhandled one.\n onClick={() => void hideComment(onHide, comment).catch(() => {})}\n >\n {comment.hidden ? labels.unhide : labels.hide}\n </Button>,\n );\n }\n if (comment.mine && onDelete) {\n actions.push(\n <DeleteCommentButton key=\"delete\" comment={comment} onDelete={onDelete} commit={commit} labels={labels} />,\n );\n }\n entries.push({\n at: instant(comment.created_at),\n // Replies first on a tie, then the comments in their order.\n order: replies.length + index,\n message: {\n id: `comment-${comment.id}`,\n own: comment.mine,\n author: veiled ? undefined : comment.mine ? undefined : (comment.author_name ?? anonymous),\n createdAt: comment.created_at,\n body: veiled ? \"\" : (comment.body ?? \"\"),\n staff: !veiled && comment.staff,\n struck: comment.hidden && isAdmin && !veiled,\n footer: actions.length > 0 ? actions : undefined,\n },\n });\n });\n entries.sort((a, b) => a.at - b.at || a.order - b.order);\n\n return (\n <div className={className} data-feedback-comment-thread=\"\">\n <FeedbackThread\n messages={entries.map((entry) => entry.message)}\n loading={loading}\n now={now}\n labels={{ thread: labels.comments, empty: labels.empty }}\n />\n {onComment && (\n <ChatComposer\n className=\"mt-3\"\n onSend={(body) => onComment(body)}\n pending={pending}\n commit={commit}\n maxLength={FEEDBACK_COMMENT_MAX}\n showCount\n labels={{ field: labels.field, placeholder: labels.placeholder, send: labels.send }}\n />\n )}\n </div>\n );\n}\n\n/** The writer's Delete: a confirm (danger tone, focus on Cancel), then `onDelete`. Its\n * own component, so `useConfirm` — which needs a `ConfirmProvider` — is only called\n * where there is something to delete. */\nfunction DeleteCommentButton({\n comment,\n onDelete,\n commit,\n labels,\n}: {\n comment: FeedbackComment;\n onDelete: (comment: FeedbackComment) => void | Promise<unknown>;\n commit?: CommitScope;\n labels: FeedbackCommentsLabels;\n}) {\n const confirm = useConfirm();\n const remove = async () => {\n const ok = await confirm({\n title: labels.deleteTitle,\n body: labels.deleteBody,\n confirmLabel: labels.delete,\n tone: \"danger\",\n commit,\n });\n if (ok) await onDelete(comment);\n };\n return (\n <Button\n type=\"button\"\n variant=\"link\"\n tone=\"danger\"\n size=\"sm\"\n commit={commit}\n onClick={() => void remove().catch(() => {})}\n >\n {labels.delete}\n </Button>\n );\n}\n"],"mappings":"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","names":[]}
1
+ {"version":3,"sources":["../../src/feedback/feedback-comment-thread.tsx"],"sourcesContent":["import { useContext } from \"react\";\nimport type { ReactNode } from \"react\";\nimport { EyeOff } from \"lucide-react\";\nimport { Button } from \"../components/button\";\nimport { useConfirm } from \"../components/confirm-dialog\";\nimport type { CommitScope } from \"../components/write-lock\";\nimport type { AuthedFetcher } from \"../hooks/use-authed-src\";\nimport { useKitLabels, type LabelOverride } from \"../i18n/kit-labels\";\nimport { FeedbackStatusBadge } from \"./feedback-inbox\";\nimport type { FeedbackComment, FeedbackReply } from \"./feedback-public-types\";\nimport { DEFAULT_FEEDBACK_PUBLIC_LABELS } from \"./feedback-public-labels\";\nimport { FEEDBACK_COMMENT_MAX, attachmentName } from \"./feedback-record\";\nimport { FeedbackEntryFile, useFeedbackEntryUpload, type FeedbackEntryUploadOptions } from \"./feedback-entry-file\";\nimport { useFeedbackRepliesLabels } from \"./feedback-replies\";\nimport { FeedbackRepliesDrawnAboveContext } from \"./feedback-replies-context\";\nimport { FeedbackReworkEntryChip } from \"./feedback-rework-entry-chip\";\nimport { ChatComposer, FeedbackThread } from \"./feedback-thread\";\nimport type { FeedbackThreadMessage } from \"./feedback-thread\";\nimport { DEFAULT_FEEDBACK_COMMENTS_LABELS } from \"./feedback-thread-parts-labels\";\n\n/**\n * A report's conversation as everyone who may read it sees it\n * (docs/feedback-harmonization.md §9.8): the admin's replies and other people's\n * comments, each its own row, oldest first — merged by time, on the\n * {@link FeedbackThread} look — and the box to add one.\n *\n * - **Replies** are drawn as staff posts with the status they set (\"→ Done\"); a reply\n * without an author is \"Admin\" (§9.7).\n * - **Reworks** (0.40, §10.3, `kind: \"rework\"`) are the report's author's: the\n * **Rework** chip, \"→ status\" when the entry moved one (a migrated one did not), the\n * file when the view carries it; at the reading end for the author ({@link\n * FeedbackCommentThreadProps.isAuthor}), and a `null` name is \"A user\", never \"Admin\".\n * - **Comments**: the viewer's own at the reading end (\"You\"); someone else's under the\n * first name the server sent, `null` as \"A user\" (§9.5, `feedbackPublic.anonymous`).\n * - **Hidden** (§9.8): others see \"Hidden by an admin\" and no name, no text; the writer\n * sees their text with that line; the admin sees it struck through, with **Unhide**.\n * An admin has **Hide** on every visible comment; the writer **Delete** on their own,\n * after a confirm (it is a hard delete). Nobody edits a comment.\n * - **The composer** (1–4 000 characters) only with `onComment`: left out for a reader\n * who may not write (a demo, a CRASH report — the app knows). Since 0.47 (§12) with\n * `onUpload` it takes one file as the rework note does — pasted into the box,\n * captured or picked — uploaded first, its URL `onComment`'s second argument.\n * - **Files** (0.47, §12): a comment's or an entry's file under its text, a picture\n * INLINE — a thumbnail through `fetcher` that opens full size — any other file a\n * download ({@link FeedbackEntryFile}). A veiled comment shows none.\n *\n * Delete asks through `useConfirm`, so a thread with `onDelete` and a comment of the\n * viewer's needs the app's `ConfirmProvider` (each app mounts one already).\n */\n\n/* ── Labels ───────────────────────────────────────────────────────────────── */\n\n/** The `feedbackComments` namespace of `<UiKitProvider labels>`. A `null` name is\n * `feedbackPublic.anonymous`, the public pages' word, so the two say the same. */\nexport interface FeedbackCommentsLabels {\n /** The thread's accessible name. de-CH: \"Kommentare\" */\n comments: string;\n /** Nothing yet. de-CH: \"Noch keine Kommentare\" */\n empty: string;\n /** In place of a hidden comment. de-CH: \"Von einem Admin ausgeblendet\" */\n hidden: string;\n /** The admin's button. de-CH: \"Ausblenden\" */\n hide: string;\n /** The admin's button on a hidden comment. de-CH: \"Einblenden\" */\n unhide: string;\n /** The writer's button. de-CH: \"Löschen\" */\n delete: string;\n /** The confirm's question. de-CH: \"Diesen Kommentar löschen?\" */\n deleteTitle: string;\n /** The confirm's line. de-CH: \"Er wird für alle entfernt. Das kann nicht rückgängig\n * gemacht werden.\" */\n deleteBody: string;\n /** The box's accessible name. de-CH: \"Kommentar schreiben\" */\n field: string;\n /** de-CH: \"Kommentar schreiben…\" */\n placeholder: string;\n /** The send button. de-CH: \"Kommentieren\" */\n send: string;\n}\n\nexport { DEFAULT_FEEDBACK_COMMENTS_LABELS };\n\n/* ── The thread ───────────────────────────────────────────────────────────── */\n\nexport interface FeedbackCommentThreadProps extends FeedbackEntryUploadOptions {\n /** The report's replies (§9.7), oldest first. Left out or `[]`: none.\n *\n * Inside `FeedbackRowDetail`'s `comments` they are dropped (0.38.1): the detail's\n * Replies section draws them already, and the thread there is the comments alone —\n * so passing them there cannot draw them twice. */\n replies?: readonly FeedbackReply[];\n /** `GET /feedback/{id}/comments`, oldest first. */\n comments: readonly FeedbackComment[];\n /** The viewer is the app's admin: Hide / Unhide, the hidden text struck through. */\n isAdmin: boolean;\n /** 0.40: the viewer filed the report (the record's `mine`) — its rework entries are\n * theirs, drawn at the reading end as \"You\". Default `false`. */\n isAuthor?: boolean;\n /** `POST /feedback/{id}/comments`. Return the promise: the box clears when it\n * resolves, and keeps the draft when it rejects. A failure is the app's to toast —\n * the server's `detail`, else `feedbackToast.commentFailed` (0.38.1). Left out, no\n * composer. 0.47 (§12): `attachmentUrl` is the one file uploaded with it (with\n * `onUpload`), `undefined` for none — `attachment_url` in the body (server-kit 0.18). */\n onComment?: (body: string, attachmentUrl?: string) => void | Promise<unknown>;\n /** 0.47: the app's authenticated GET (`AuthedImage`'s) for the files in the thread.\n * Left out, the browser's own fetch with same-origin cookies. */\n fetcher?: AuthedFetcher | null;\n /** `PATCH /feedback/{id}/comments/{comment_id} {hidden}` — the admin's. A rejection\n * is caught (0.38.1) and is the app's to toast (`feedbackToast.updateFailed`). Left\n * out, no Hide / Unhide. */\n onHide?: (comment: FeedbackComment, hidden: boolean) => void | Promise<unknown>;\n /** `DELETE /feedback/{id}/comments/{comment_id}` — the writer's own, after the\n * confirm. Left out, no Delete. */\n onDelete?: (comment: FeedbackComment) => void | Promise<unknown>;\n /** A comment is being sent. */\n pending?: boolean;\n /** The comments are loading. */\n loading?: boolean;\n /** Comment, Hide and Delete COMMIT — see Button's `commit`. */\n commit?: CommitScope;\n /** \"Now\" for the relative times — pin it in tests and stories. */\n now?: Date | number;\n labels?: LabelOverride<FeedbackCommentsLabels>;\n className?: string;\n}\n\ntype Entry = { at: number; order: number; message: FeedbackThreadMessage };\n\n/** The admin's Hide / Unhide as a promise — a sync throw or a `void` return included —\n * so the click can catch it as Delete's does. */\nconst hideComment = async (\n onHide: NonNullable<FeedbackCommentThreadProps[\"onHide\"]>,\n comment: FeedbackComment,\n) => {\n await onHide(comment, !comment.hidden);\n};\n\nconst instant = (iso: string) => {\n const ms = Date.parse(iso);\n return Number.isNaN(ms) ? 0 : ms;\n};\n\nexport function FeedbackCommentThread({\n replies: repliesProp = [],\n comments,\n isAdmin,\n isAuthor = false,\n onComment,\n onHide,\n onDelete,\n pending = false,\n loading = false,\n commit,\n now,\n fetcher,\n onUpload,\n onCaptureScreenshot,\n attachment,\n labels: labelsProp,\n className,\n}: FeedbackCommentThreadProps) {\n const { composerAttachment, upload, uploading } = useFeedbackEntryUpload({\n onUpload,\n onCaptureScreenshot,\n attachment,\n });\n const labels = useKitLabels(\"feedbackComments\", DEFAULT_FEEDBACK_COMMENTS_LABELS, labelsProp);\n const anonymous = useKitLabels(\"feedbackPublic\", DEFAULT_FEEDBACK_PUBLIC_LABELS).anonymous;\n const replyLabels = useFeedbackRepliesLabels();\n const repliesDrawnAbove = useContext(FeedbackRepliesDrawnAboveContext);\n const replies = repliesDrawnAbove ? [] : repliesProp;\n\n const entries: Entry[] = [];\n replies.forEach((reply, order) => {\n if (reply.kind === \"rework\") {\n entries.push({\n at: instant(reply.created_at),\n order,\n message: {\n id: `reply-${reply.id}`,\n own: isAuthor,\n author: isAuthor ? undefined : (reply.author_name ?? anonymous),\n createdAt: reply.created_at,\n body: reply.body,\n attachments: reply.attachment_url\n ? [{ name: attachmentName(reply.attachment_url), url: reply.attachment_url }]\n : undefined,\n badge: (\n <span className=\"inline-flex min-w-0 max-w-full flex-wrap items-center gap-1\" data-feedback-entry-kind=\"rework\">\n <FeedbackReworkEntryChip />\n {reply.status_to && (\n <span className=\"inline-flex items-center gap-1\" data-feedback-reply-status={reply.status_to}>\n <span className=\"sr-only\">{replyLabels.statusChanged}</span>\n <span aria-hidden>→</span>\n <FeedbackStatusBadge status={reply.status_to} />\n </span>\n )}\n </span>\n ),\n },\n });\n return;\n }\n entries.push({\n at: instant(reply.created_at),\n order,\n message: {\n id: `reply-${reply.id}`,\n author: reply.author_name ?? replyLabels.admin,\n createdAt: reply.created_at,\n body: reply.body,\n // 0.47 (§12): an admin reply's file, where the view carries it.\n attachments: reply.attachment_url\n ? [{ name: attachmentName(reply.attachment_url), url: reply.attachment_url }]\n : undefined,\n staff: true,\n badge: reply.status_to ? (\n <span className=\"inline-flex min-w-0 max-w-full flex-wrap items-center gap-1\" data-feedback-reply-status={reply.status_to}>\n <span className=\"sr-only\">{replyLabels.statusChanged}</span>\n <span aria-hidden>→</span>\n <FeedbackStatusBadge status={reply.status_to} />\n </span>\n ) : undefined,\n },\n });\n });\n comments.forEach((comment, index) => {\n // Shown as hidden — no name, no text — to anyone but its writer and an admin.\n const veiled = comment.hidden && !comment.mine && !isAdmin;\n const actions: ReactNode[] = [];\n if (comment.hidden) {\n actions.push(\n <span key=\"hidden\" className=\"inline-flex items-center gap-1\" data-feedback-comment-hidden=\"\">\n <EyeOff className=\"size-3\" aria-hidden />\n {labels.hidden}\n </span>,\n );\n }\n if (isAdmin && onHide) {\n actions.push(\n <Button\n key=\"hide\"\n type=\"button\"\n variant=\"link\"\n tone=\"muted\"\n size=\"sm\"\n commit={commit}\n // Caught as Delete's is (0.38.1): a rejection is the app's to toast — its\n // mutation's `onError` — and must not surface as an unhandled one.\n onClick={() => void hideComment(onHide, comment).catch(() => {})}\n >\n {comment.hidden ? labels.unhide : labels.hide}\n </Button>,\n );\n }\n if (comment.mine && onDelete) {\n actions.push(\n <DeleteCommentButton key=\"delete\" comment={comment} onDelete={onDelete} commit={commit} labels={labels} />,\n );\n }\n entries.push({\n at: instant(comment.created_at),\n // Replies first on a tie, then the comments in their order.\n order: replies.length + index,\n message: {\n id: `comment-${comment.id}`,\n own: comment.mine,\n author: veiled ? undefined : comment.mine ? undefined : (comment.author_name ?? anonymous),\n createdAt: comment.created_at,\n body: veiled ? \"\" : (comment.body ?? \"\"),\n // 0.47 (§12): its one file; none when veiled (the server sends none either).\n attachments:\n !veiled && comment.attachment_url\n ? [{ name: attachmentName(comment.attachment_url), url: comment.attachment_url }]\n : undefined,\n staff: !veiled && comment.staff,\n struck: comment.hidden && isAdmin && !veiled,\n footer: actions.length > 0 ? actions : undefined,\n },\n });\n });\n entries.sort((a, b) => a.at - b.at || a.order - b.order);\n\n return (\n <div className={className} data-feedback-comment-thread=\"\">\n <FeedbackThread\n messages={entries.map((entry) => entry.message)}\n loading={loading}\n now={now}\n renderAttachment={(file) => (file.url ? <FeedbackEntryFile url={file.url} fetcher={fetcher} /> : file.name)}\n labels={{ thread: labels.comments, empty: labels.empty }}\n />\n {onComment && (\n <ChatComposer\n className=\"mt-3\"\n onSend={async (body, file) => {\n const url = await upload(file);\n // The second argument only with a file, so a text comment calls as before 0.47.\n return url === undefined ? onComment(body) : onComment(body, url);\n }}\n pending={pending || uploading}\n attachment={composerAttachment}\n commit={commit}\n maxLength={FEEDBACK_COMMENT_MAX}\n showCount\n labels={{ field: labels.field, placeholder: labels.placeholder, send: labels.send }}\n />\n )}\n </div>\n );\n}\n\n/** The writer's Delete: a confirm (danger tone, focus on Cancel), then `onDelete`. Its\n * own component, so `useConfirm` — which needs a `ConfirmProvider` — is only called\n * where there is something to delete. */\nfunction DeleteCommentButton({\n comment,\n onDelete,\n commit,\n labels,\n}: {\n comment: FeedbackComment;\n onDelete: (comment: FeedbackComment) => void | Promise<unknown>;\n commit?: CommitScope;\n labels: FeedbackCommentsLabels;\n}) {\n const confirm = useConfirm();\n const remove = async () => {\n const ok = await confirm({\n title: labels.deleteTitle,\n body: labels.deleteBody,\n confirmLabel: labels.delete,\n tone: \"danger\",\n commit,\n });\n if (ok) await onDelete(comment);\n };\n return (\n <Button\n type=\"button\"\n variant=\"link\"\n tone=\"danger\"\n size=\"sm\"\n commit={commit}\n onClick={() => void remove().catch(() => {})}\n >\n {labels.delete}\n </Button>\n );\n}\n"],"mappings":"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","names":[]}
@@ -0,0 +1,61 @@
1
+ import type { AuthedFetcher } from "../hooks/use-authed-src.js";
2
+ import type { FeedbackNoteAttachment } from "./feedback-inbox.js";
3
+ import type { FeedbackComposerAttachment } from "./feedback-thread.js";
4
+ /**
5
+ * One file of a conversation entry, and the upload behind an entry's composer
6
+ * (docs/feedback-harmonization.md §12, round 0.47; keksdose run 82 R1, live #447).
7
+ *
8
+ * - {@link FeedbackEntryFile}: a picture drawn INLINE — a thumbnail fetched through the
9
+ * app's authenticated GET, a tap opens it full size in a tab — and any other file a
10
+ * download button, as the row detail's Attachment section draws them. The reply
11
+ * timeline, the rework entries and the comment thread all draw their file with it.
12
+ * - {@link useFeedbackEntryUpload}: the rework note's one-file field (paste from the
13
+ * clipboard, capture, pick; the contract's types and 10 MB, §3.5) for a
14
+ * {@link ChatComposer}, and the upload that runs FIRST on send, so an entry never names
15
+ * a file that did not arrive.
16
+ */
17
+ export interface FeedbackEntryFileProps {
18
+ /** The entry's `attachment_url` — `/api/v1/feedback/attachments/<key>`. */
19
+ url: string;
20
+ /** The app's authenticated GET (`AuthedImage`'s): the attachment route wants the
21
+ * bearer token (§3.5). Left out, the browser's own fetch with same-origin cookies. */
22
+ fetcher?: AuthedFetcher | null;
23
+ className?: string;
24
+ }
25
+ /**
26
+ * An entry's one file (§12): a picture (`png`, `jpg`, `webp`, `gif` — by the key's
27
+ * extension, which the server minted from the sniffed bytes) as an inline thumbnail that
28
+ * opens full size in a new tab (`AuthedImage`'s `link`: the `blob:` URL, never the API
29
+ * path); anything else a download button. A picture that will not load falls back to the
30
+ * download button, so a broken file still says it is there. `alt`:
31
+ * `feedbackDetail.attachment`.
32
+ */
33
+ export declare function FeedbackEntryFile({ url, fetcher, className }: FeedbackEntryFileProps): import("react").JSX.Element;
34
+ /** `POST /feedback/attachments` — the file, answered with its URL. */
35
+ export type FeedbackEntryUpload = (file: File) => Promise<string>;
36
+ export interface FeedbackEntryUploadOptions {
37
+ /** The upload. Left out, the composer is text only. */
38
+ onUpload?: FeedbackEntryUpload;
39
+ /** The "Capture screenshot" button — the kit's `captureAppScreenshot` or the app's
40
+ * own. A rejection toasts `feedbackToast.captureFailed` and adds nothing. */
41
+ onCaptureScreenshot?: () => Promise<File | null>;
42
+ /** Over the field's defaults: the contract's types and 10 MB (§3.5), the
43
+ * `feedbackToast` refusals. The same object the rework section takes. */
44
+ attachment?: Partial<FeedbackNoteAttachment>;
45
+ }
46
+ export interface FeedbackEntryUploadState {
47
+ /** For {@link ChatComposer}'s `attachment`; `undefined` without `onUpload`. */
48
+ composerAttachment: FeedbackComposerAttachment | undefined;
49
+ /** Upload the picked file, if any: its URL, `undefined` for none. Rejects after a
50
+ * toast (`feedbackAttachment.attachmentUploadFailed`) when the upload fails — the
51
+ * send then stops and the composer keeps the draft and the file. */
52
+ upload: (file: File | null) => Promise<string | undefined>;
53
+ /** An upload is running — add it to the composer's `pending`. */
54
+ uploading: boolean;
55
+ }
56
+ /**
57
+ * The rework note's file field for a reply or a comment composer (§12): one file, added,
58
+ * captured or pasted from the clipboard into the box, the contract's types and 10 MB; and
59
+ * the upload, awaited before the entry is sent.
60
+ */
61
+ export declare function useFeedbackEntryUpload({ onUpload, onCaptureScreenshot, attachment, }: FeedbackEntryUploadOptions): FeedbackEntryUploadState;
@@ -0,0 +1,76 @@
1
+ "use client";
2
+ import { jsx } from "react/jsx-runtime";
3
+ import { useState } from "react";
4
+ import { AuthedImage } from "../components/authed-image.js";
5
+ import { toast } from "../components/toast.js";
6
+ import { useKitLabels } from "../i18n/kit-labels.js";
7
+ import { cn } from "../lib/cn.js";
8
+ import { DEFAULT_FEEDBACK_ATTACHMENT_LABELS, DEFAULT_MAX_ATTACHMENT_BYTES } from "./feedback-attachment.js";
9
+ import { useFeedbackToastLabels } from "./feedback-labels.js";
10
+ import { FeedbackAttachmentDownload, plainFeedbackFetch } from "./feedback-attachment-download.js";
11
+ import { FEEDBACK_ATTACHMENT_ACCEPT, isImageAttachment } from "./feedback-record.js";
12
+ import { DEFAULT_FEEDBACK_DETAIL_LABELS } from "./feedback-row-detail-labels.js";
13
+ function FeedbackEntryFile({ url, fetcher, className }) {
14
+ const labels = useKitLabels("feedbackDetail", DEFAULT_FEEDBACK_DETAIL_LABELS);
15
+ const fetchFile = fetcher ?? plainFeedbackFetch;
16
+ return /* @__PURE__ */ jsx("span", { className: cn("inline-block min-w-0 max-w-full", className), "data-feedback-entry-file": url, children: isImageAttachment(url) ? /* @__PURE__ */ jsx(
17
+ AuthedImage,
18
+ {
19
+ src: url,
20
+ alt: labels.attachment,
21
+ fetcher: fetchFile,
22
+ link: true,
23
+ stopPropagation: true,
24
+ errorFallback: /* @__PURE__ */ jsx(FeedbackAttachmentDownload, { url, fetcher: fetchFile }),
25
+ wrapperClassName: "inline-block max-w-full",
26
+ className: "max-h-40 max-w-full rounded border border-subtle"
27
+ }
28
+ ) : /* @__PURE__ */ jsx(FeedbackAttachmentDownload, { url, fetcher: fetchFile }) });
29
+ }
30
+ function useFeedbackEntryUpload({
31
+ onUpload,
32
+ onCaptureScreenshot,
33
+ attachment
34
+ }) {
35
+ const toasts = useFeedbackToastLabels();
36
+ const fieldLabels = useKitLabels("feedbackAttachment", DEFAULT_FEEDBACK_ATTACHMENT_LABELS);
37
+ const [uploading, setUploading] = useState(false);
38
+ const capture = onCaptureScreenshot ? async () => {
39
+ try {
40
+ return await onCaptureScreenshot();
41
+ } catch {
42
+ toast.error(toasts.captureFailed);
43
+ return null;
44
+ }
45
+ } : void 0;
46
+ const composerAttachment = onUpload ? {
47
+ accept: FEEDBACK_ATTACHMENT_ACCEPT,
48
+ maxBytes: DEFAULT_MAX_ATTACHMENT_BYTES,
49
+ onError: (kind) => {
50
+ toast.error(kind === "type" ? toasts.attachmentUnsupported : toasts.attachmentTooLarge);
51
+ },
52
+ onCaptureScreenshot: capture,
53
+ buttonVariant: "ghost",
54
+ buttonSize: "sm",
55
+ ...attachment
56
+ } : void 0;
57
+ const upload = async (file) => {
58
+ if (!file || !onUpload) return void 0;
59
+ setUploading(true);
60
+ try {
61
+ return await onUpload(file);
62
+ } catch (error) {
63
+ const failed = fieldLabels.attachmentUploadFailed ?? DEFAULT_FEEDBACK_ATTACHMENT_LABELS.attachmentUploadFailed;
64
+ toast.error(failed(file.name));
65
+ throw error;
66
+ } finally {
67
+ setUploading(false);
68
+ }
69
+ };
70
+ return { composerAttachment, upload, uploading };
71
+ }
72
+ export {
73
+ FeedbackEntryFile,
74
+ useFeedbackEntryUpload
75
+ };
76
+ //# sourceMappingURL=feedback-entry-file.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../src/feedback/feedback-entry-file.tsx"],"sourcesContent":["import { useState } from \"react\";\nimport { AuthedImage } from \"../components/authed-image\";\nimport { toast } from \"../components/toast\";\nimport type { AuthedFetcher } from \"../hooks/use-authed-src\";\nimport { useKitLabels } from \"../i18n/kit-labels\";\nimport { cn } from \"../lib/cn\";\nimport { DEFAULT_FEEDBACK_ATTACHMENT_LABELS, DEFAULT_MAX_ATTACHMENT_BYTES } from \"./feedback-attachment\";\nimport type { FeedbackNoteAttachment } from \"./feedback-inbox\";\nimport { useFeedbackToastLabels } from \"./feedback-labels\";\nimport { FeedbackAttachmentDownload, plainFeedbackFetch } from \"./feedback-attachment-download\";\nimport { FEEDBACK_ATTACHMENT_ACCEPT, isImageAttachment } from \"./feedback-record\";\nimport { DEFAULT_FEEDBACK_DETAIL_LABELS } from \"./feedback-row-detail-labels\";\nimport type { FeedbackComposerAttachment } from \"./feedback-thread\";\n\n/**\n * One file of a conversation entry, and the upload behind an entry's composer\n * (docs/feedback-harmonization.md §12, round 0.47; keksdose run 82 R1, live #447).\n *\n * - {@link FeedbackEntryFile}: a picture drawn INLINE — a thumbnail fetched through the\n * app's authenticated GET, a tap opens it full size in a tab — and any other file a\n * download button, as the row detail's Attachment section draws them. The reply\n * timeline, the rework entries and the comment thread all draw their file with it.\n * - {@link useFeedbackEntryUpload}: the rework note's one-file field (paste from the\n * clipboard, capture, pick; the contract's types and 10 MB, §3.5) for a\n * {@link ChatComposer}, and the upload that runs FIRST on send, so an entry never names\n * a file that did not arrive.\n */\n\nexport interface FeedbackEntryFileProps {\n /** The entry's `attachment_url` — `/api/v1/feedback/attachments/<key>`. */\n url: string;\n /** The app's authenticated GET (`AuthedImage`'s): the attachment route wants the\n * bearer token (§3.5). Left out, the browser's own fetch with same-origin cookies. */\n fetcher?: AuthedFetcher | null;\n className?: string;\n}\n\n/**\n * An entry's one file (§12): a picture (`png`, `jpg`, `webp`, `gif` — by the key's\n * extension, which the server minted from the sniffed bytes) as an inline thumbnail that\n * opens full size in a new tab (`AuthedImage`'s `link`: the `blob:` URL, never the API\n * path); anything else a download button. A picture that will not load falls back to the\n * download button, so a broken file still says it is there. `alt`:\n * `feedbackDetail.attachment`.\n */\nexport function FeedbackEntryFile({ url, fetcher, className }: FeedbackEntryFileProps) {\n const labels = useKitLabels(\"feedbackDetail\", DEFAULT_FEEDBACK_DETAIL_LABELS);\n const fetchFile = fetcher ?? plainFeedbackFetch;\n return (\n <span className={cn(\"inline-block min-w-0 max-w-full\", className)} data-feedback-entry-file={url}>\n {isImageAttachment(url) ? (\n <AuthedImage\n src={url}\n alt={labels.attachment}\n fetcher={fetchFile}\n link\n stopPropagation\n errorFallback={<FeedbackAttachmentDownload url={url} fetcher={fetchFile} />}\n wrapperClassName=\"inline-block max-w-full\"\n className=\"max-h-40 max-w-full rounded border border-subtle\"\n />\n ) : (\n <FeedbackAttachmentDownload url={url} fetcher={fetchFile} />\n )}\n </span>\n );\n}\n\n/* ── The composer's one file ──────────────────────────────────────────────── */\n\n/** `POST /feedback/attachments` — the file, answered with its URL. */\nexport type FeedbackEntryUpload = (file: File) => Promise<string>;\n\nexport interface FeedbackEntryUploadOptions {\n /** The upload. Left out, the composer is text only. */\n onUpload?: FeedbackEntryUpload;\n /** The \"Capture screenshot\" button — the kit's `captureAppScreenshot` or the app's\n * own. A rejection toasts `feedbackToast.captureFailed` and adds nothing. */\n onCaptureScreenshot?: () => Promise<File | null>;\n /** Over the field's defaults: the contract's types and 10 MB (§3.5), the\n * `feedbackToast` refusals. The same object the rework section takes. */\n attachment?: Partial<FeedbackNoteAttachment>;\n}\n\nexport interface FeedbackEntryUploadState {\n /** For {@link ChatComposer}'s `attachment`; `undefined` without `onUpload`. */\n composerAttachment: FeedbackComposerAttachment | undefined;\n /** Upload the picked file, if any: its URL, `undefined` for none. Rejects after a\n * toast (`feedbackAttachment.attachmentUploadFailed`) when the upload fails — the\n * send then stops and the composer keeps the draft and the file. */\n upload: (file: File | null) => Promise<string | undefined>;\n /** An upload is running — add it to the composer's `pending`. */\n uploading: boolean;\n}\n\n/**\n * The rework note's file field for a reply or a comment composer (§12): one file, added,\n * captured or pasted from the clipboard into the box, the contract's types and 10 MB; and\n * the upload, awaited before the entry is sent.\n */\nexport function useFeedbackEntryUpload({\n onUpload,\n onCaptureScreenshot,\n attachment,\n}: FeedbackEntryUploadOptions): FeedbackEntryUploadState {\n const toasts = useFeedbackToastLabels();\n const fieldLabels = useKitLabels(\"feedbackAttachment\", DEFAULT_FEEDBACK_ATTACHMENT_LABELS);\n const [uploading, setUploading] = useState(false);\n\n const capture = onCaptureScreenshot\n ? async () => {\n try {\n return await onCaptureScreenshot();\n } catch {\n toast.error(toasts.captureFailed);\n return null;\n }\n }\n : undefined;\n\n const composerAttachment: FeedbackComposerAttachment | undefined = onUpload\n ? {\n accept: FEEDBACK_ATTACHMENT_ACCEPT,\n maxBytes: DEFAULT_MAX_ATTACHMENT_BYTES,\n onError: (kind) => {\n toast.error(kind === \"type\" ? toasts.attachmentUnsupported : toasts.attachmentTooLarge);\n },\n onCaptureScreenshot: capture,\n buttonVariant: \"ghost\",\n buttonSize: \"sm\",\n ...attachment,\n }\n : undefined;\n\n const upload = async (file: File | null) => {\n if (!file || !onUpload) return undefined;\n setUploading(true);\n try {\n return await onUpload(file);\n } catch (error) {\n const failed = fieldLabels.attachmentUploadFailed ?? DEFAULT_FEEDBACK_ATTACHMENT_LABELS.attachmentUploadFailed!;\n toast.error(failed(file.name));\n throw error;\n } finally {\n setUploading(false);\n }\n };\n\n return { composerAttachment, upload, uploading };\n}\n"],"mappings":";AAyDyB;AAzDzB,SAAS,gBAAgB;AACzB,SAAS,mBAAmB;AAC5B,SAAS,aAAa;AAEtB,SAAS,oBAAoB;AAC7B,SAAS,UAAU;AACnB,SAAS,oCAAoC,oCAAoC;AAEjF,SAAS,8BAA8B;AACvC,SAAS,4BAA4B,0BAA0B;AAC/D,SAAS,4BAA4B,yBAAyB;AAC9D,SAAS,sCAAsC;AAkCxC,SAAS,kBAAkB,EAAE,KAAK,SAAS,UAAU,GAA2B;AACrF,QAAM,SAAS,aAAa,kBAAkB,8BAA8B;AAC5E,QAAM,YAAY,WAAW;AAC7B,SACE,oBAAC,UAAK,WAAW,GAAG,mCAAmC,SAAS,GAAG,4BAA0B,KAC1F,4BAAkB,GAAG,IACpB;AAAA,IAAC;AAAA;AAAA,MACC,KAAK;AAAA,MACL,KAAK,OAAO;AAAA,MACZ,SAAS;AAAA,MACT,MAAI;AAAA,MACJ,iBAAe;AAAA,MACf,eAAe,oBAAC,8BAA2B,KAAU,SAAS,WAAW;AAAA,MACzE,kBAAiB;AAAA,MACjB,WAAU;AAAA;AAAA,EACZ,IAEA,oBAAC,8BAA2B,KAAU,SAAS,WAAW,GAE9D;AAEJ;AAkCO,SAAS,uBAAuB;AAAA,EACrC;AAAA,EACA;AAAA,EACA;AACF,GAAyD;AACvD,QAAM,SAAS,uBAAuB;AACtC,QAAM,cAAc,aAAa,sBAAsB,kCAAkC;AACzF,QAAM,CAAC,WAAW,YAAY,IAAI,SAAS,KAAK;AAEhD,QAAM,UAAU,sBACZ,YAAY;AACV,QAAI;AACF,aAAO,MAAM,oBAAoB;AAAA,IACnC,QAAQ;AACN,YAAM,MAAM,OAAO,aAAa;AAChC,aAAO;AAAA,IACT;AAAA,EACF,IACA;AAEJ,QAAM,qBAA6D,WAC/D;AAAA,IACE,QAAQ;AAAA,IACR,UAAU;AAAA,IACV,SAAS,CAAC,SAAS;AACjB,YAAM,MAAM,SAAS,SAAS,OAAO,wBAAwB,OAAO,kBAAkB;AAAA,IACxF;AAAA,IACA,qBAAqB;AAAA,IACrB,eAAe;AAAA,IACf,YAAY;AAAA,IACZ,GAAG;AAAA,EACL,IACA;AAEJ,QAAM,SAAS,OAAO,SAAsB;AAC1C,QAAI,CAAC,QAAQ,CAAC,SAAU,QAAO;AAC/B,iBAAa,IAAI;AACjB,QAAI;AACF,aAAO,MAAM,SAAS,IAAI;AAAA,IAC5B,SAAS,OAAO;AACd,YAAM,SAAS,YAAY,0BAA0B,mCAAmC;AACxF,YAAM,MAAM,OAAO,KAAK,IAAI,CAAC;AAC7B,YAAM;AAAA,IACR,UAAE;AACA,mBAAa,KAAK;AAAA,IACpB;AAAA,EACF;AAEA,SAAO,EAAE,oBAAoB,QAAQ,UAAU;AACjD;","names":[]}
@@ -1,7 +1,9 @@
1
1
  import type { ReactNode } from "react";
2
2
  import type { CommitScope } from "../components/write-lock.js";
3
+ import type { AuthedFetcher } from "../hooks/use-authed-src.js";
3
4
  import { type LabelOverride } from "../i18n/kit-labels.js";
4
5
  import { type FeedbackCommentsLabels } from "./feedback-comment-thread.js";
6
+ import type { FeedbackEntryUploadOptions } from "./feedback-entry-file.js";
5
7
  import { DEFAULT_FEEDBACK_PUBLIC_LABELS, type FeedbackPublicLabels } from "./feedback-public-labels.js";
6
8
  import type { FeedbackComment, FeedbackPublicListItem, FeedbackPublicRecord, FeedbackPublicSort, FeedbackVoteState } from "./feedback-public-types.js";
7
9
  import { type FeedbackPageLabels, type FeedbackRenderDate } from "./feedback-table.js";
@@ -106,8 +108,18 @@ export interface FeedbackPublicDetailProps {
106
108
  /** The viewer is the app's admin: Hide / Unhide on the comments. */
107
109
  isAdmin?: boolean;
108
110
  /** `POST /feedback/{id}/comments`. Left out — a demo, a reader who may not write — no
109
- * composer. */
110
- onComment?: (body: string) => void | Promise<unknown>;
111
+ * composer. 0.47 (§12): `attachmentUrl` is the one file uploaded with it (with
112
+ * `onUpload`). */
113
+ onComment?: (body: string, attachmentUrl?: string) => void | Promise<unknown>;
114
+ /** 0.47 (§12): `POST /feedback/attachments` — the comment box then takes one file,
115
+ * pasted, captured or picked, as the rework note does. Left out, text only. */
116
+ onUpload?: FeedbackEntryUploadOptions["onUpload"];
117
+ /** 0.47: the comment box's "Capture screenshot". */
118
+ onCaptureScreenshot?: FeedbackEntryUploadOptions["onCaptureScreenshot"];
119
+ /** 0.47: the comment box's file field, over the contract's defaults. */
120
+ attachment?: FeedbackEntryUploadOptions["attachment"];
121
+ /** 0.47: the app's authenticated GET for the comments' pictures, drawn inline. */
122
+ fetcher?: AuthedFetcher | null;
111
123
  /** The admin's hide (`PATCH …/comments/{id} {hidden}`). */
112
124
  onHide?: (comment: FeedbackComment, hidden: boolean) => void | Promise<unknown>;
113
125
  /** The writer's delete (`DELETE …/comments/{id}`), after a confirm. */
@@ -136,7 +148,8 @@ export interface FeedbackPublicDetailProps {
136
148
  * add one.
137
149
  *
138
150
  * What it never draws, because the public record never carries it (§9.10): an address,
139
- * a last name, a screenshot, a file, the page the report was filed on. The body is the
140
- * server's `public_body` — the client strips nothing and adds nothing.
151
+ * a last name, the report's screenshot or files, the page the report was filed on. The
152
+ * body is the server's `public_body` — the client strips nothing and adds nothing. The
153
+ * one file it shows (0.47, §12) is a comment's own, posted with it to be seen.
141
154
  */
142
- export declare function FeedbackPublicDetail({ row, comments, onVote, isAdmin, onComment, onHide, onDelete, pending, commit, voteDisabled, renderDate: renderDateProp, now, labels: labelsProp, commentLabels, className, }: FeedbackPublicDetailProps): import("react").JSX.Element;
155
+ export declare function FeedbackPublicDetail({ row, comments, onVote, isAdmin, onComment, onHide, onDelete, onUpload, onCaptureScreenshot, attachment, fetcher, pending, commit, voteDisabled, renderDate: renderDateProp, now, labels: labelsProp, commentLabels, className, }: FeedbackPublicDetailProps): import("react").JSX.Element;
@@ -254,6 +254,10 @@ function FeedbackPublicDetail({
254
254
  onComment,
255
255
  onHide,
256
256
  onDelete,
257
+ onUpload,
258
+ onCaptureScreenshot,
259
+ attachment,
260
+ fetcher,
257
261
  pending,
258
262
  commit,
259
263
  voteDisabled,
@@ -294,6 +298,10 @@ function FeedbackPublicDetail({
294
298
  onComment,
295
299
  onHide,
296
300
  onDelete,
301
+ onUpload,
302
+ onCaptureScreenshot,
303
+ attachment,
304
+ fetcher,
297
305
  pending,
298
306
  commit,
299
307
  now,
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/feedback/feedback-public-list.tsx"],"sourcesContent":["import { useMemo, useState } from \"react\";\nimport type { ReactNode } from \"react\";\nimport { ChevronLeft, ChevronRight, MessageSquare, ThumbsUp } from \"lucide-react\";\nimport { DataTable, type DataTableColumn } from \"../components/data-table\";\nimport { DEFAULT_DATA_TABLE_LABELS } from \"../components/data-table-labels\";\nimport { IconButton } from \"../components/icon-button\";\nimport { SearchField } from \"../components/search-field\";\nimport { ToggleGroup } from \"../components/toggle-group\";\nimport type { CommitScope } from \"../components/write-lock\";\nimport { useKitLabels, type LabelOverride } from \"../i18n/kit-labels\";\nimport { cn } from \"../lib/cn\";\nimport { FILL_HEIGHT_CLASS } from \"../shell/fill-height\";\nimport { EMPTY_FORMATTED_VALUE } from \"../lib/format\";\nimport { FeedbackCategoryBadge, FeedbackDetail, FeedbackDetailSection, FeedbackProse, FeedbackStatusBadge } from \"./feedback-inbox\";\nimport { useFeedbackCategoryLabels } from \"./feedback-labels\";\nimport { FeedbackCommentThread, type FeedbackCommentsLabels } from \"./feedback-comment-thread\";\nimport { DEFAULT_FEEDBACK_PUBLIC_LABELS, type FeedbackPublicLabels } from \"./feedback-public-labels\";\nimport type {\n FeedbackComment,\n FeedbackPublicListItem,\n FeedbackPublicRecord,\n FeedbackPublicSort,\n FeedbackVoteState,\n} from \"./feedback-public-types\";\nimport {\n useFeedbackPageLabels,\n useFeedbackRenderDate,\n type FeedbackPageLabels,\n type FeedbackRenderDate,\n} from \"./feedback-table\";\nimport { FeedbackVoteButton } from \"./feedback-vote-button\";\n\nexport { DEFAULT_FEEDBACK_PUBLIC_LABELS };\nexport type { FeedbackPublicLabels };\n\n/** `feedbackPublic` resolved: English, then `<UiKitProvider labels>`, then `labels`. */\nexport function useFeedbackPublicLabels(labels?: LabelOverride<FeedbackPublicLabels>): FeedbackPublicLabels {\n return useKitLabels(\"feedbackPublic\", DEFAULT_FEEDBACK_PUBLIC_LABELS, labels);\n}\n\n/** The sort toggle's order: the server's default first (§9.6). */\nconst SORTS: readonly FeedbackPublicSort[] = [\"newest\", \"votes\", \"updated\"];\n\n/** A date in a cell, as the inbox writes it: `<time>`, nowrap, tabular figures. */\nfunction PublicDate({ value, renderDate }: { value: string | null | undefined; renderDate: FeedbackRenderDate }) {\n if (!value) return <span className=\"text-placeholder\">{EMPTY_FORMATTED_VALUE}</span>;\n return (\n <time dateTime={value} className=\"whitespace-nowrap tabular-nums text-muted\">\n {renderDate(value)}\n </time>\n );\n}\n\n/** The visible comments as a small chip beside the title — the count in figures, the\n * words for a screen reader. Nothing at 0. */\nfunction CommentCount({ count, labels }: { count: number; labels: FeedbackPublicLabels }) {\n if (!count) return null;\n return (\n <span className=\"inline-flex shrink-0 items-center gap-1 text-xs text-muted\" data-feedback-comment-count={count}>\n <MessageSquare className=\"size-3.5\" aria-hidden />\n <span aria-hidden className=\"tabular-nums\">\n {count}\n </span>\n <span className=\"sr-only\">{labels.comments(count)}</span>\n </span>\n );\n}\n\n/** The votes on a phone card: the thumb and the count, in the brand colour when the\n * viewer's own. */\nfunction VoteCount({ count, voted, labels }: { count: number; voted: boolean; labels: FeedbackPublicLabels }) {\n return (\n <span\n className={cn(\"ms-auto inline-flex shrink-0 items-center gap-1 text-xs\", voted ? \"text-brand\" : \"text-muted\")}\n data-feedback-vote-count={count}\n >\n <ThumbsUp className=\"size-3.5\" aria-hidden />\n <span aria-hidden className=\"tabular-nums\">\n {count}\n </span>\n <span className=\"sr-only\">{labels.votes(count)}</span>\n </span>\n );\n}\n\n/** 0.38.1: previous / next over the server's pages when it answers no total\n * (`GET /feedback/public` — Kurvenschmiede): \"Page 3\" between two arrows. The arrows'\n * names are `dataTable`'s, the strip's; the page's is `feedbackPublic.page`. */\nfunction PublicPager({\n page,\n hasMore,\n onPageChange,\n labels,\n}: {\n page: number;\n hasMore: boolean;\n onPageChange: (page: number) => void;\n labels: FeedbackPublicLabels;\n}) {\n const tableLabels = useKitLabels(\"dataTable\", DEFAULT_DATA_TABLE_LABELS);\n const name = labels.page(page + 1);\n return (\n <div\n role=\"group\"\n aria-label={name}\n className=\"flex items-center justify-end gap-2 text-sm text-secondary\"\n data-feedback-public-pager={page}\n >\n <IconButton\n size=\"sm\"\n variant=\"secondary\"\n label={tableLabels.prevPage}\n disabled={page <= 0}\n onClick={() => onPageChange(page - 1)}\n >\n {/* \"Previous\" points back along the line — left in LTR, right in RTL. */}\n <ChevronLeft className=\"rtl:-scale-x-100\" />\n </IconButton>\n <span className=\"tabular-nums\" aria-hidden>\n {name}\n </span>\n <IconButton\n size=\"sm\"\n variant=\"secondary\"\n label={tableLabels.nextPage}\n disabled={!hasMore}\n onClick={() => onPageChange(page + 1)}\n >\n <ChevronRight className=\"rtl:-scale-x-100\" />\n </IconButton>\n </div>\n );\n}\n\n/* ── The list ─────────────────────────────────────────────────────────────── */\n\nexport interface FeedbackPublicListProps<T extends FeedbackPublicListItem = FeedbackPublicListItem> {\n /** `GET /feedback/public?sort=&q=` — the server's page, in the server's order. */\n rows: T[];\n /** How many the query has in all — the line beside the sort. Left out, no line. */\n total?: number;\n /** The server sorts (§9.6): the toggle only says which way. */\n sort: FeedbackPublicSort;\n onSortChange: (sort: FeedbackPublicSort) => void;\n /** The search box's text (`q`, titles only). */\n search?: string;\n /**\n * The search box is drawn ONLY with this handler: where the app's titles are sealed\n * (Kurvenschmiede, `FeedbackDeps.public_search=False`, §9.9) there is nothing to search\n * on and the server answers `q` with a 422 — so it passes none.\n */\n onSearchChange?: (search: string) => void;\n /** The opened row: the app's {@link FeedbackPublicDetail} with the row's\n * `GET /feedback/public/{id}` and its comments. */\n renderDetail: (row: T) => ReactNode;\n /** `PUT` / `DELETE /feedback/{id}/vote` for a row's button (`next`: the new state);\n * resolve with the answer, reject to roll back. */\n onVote: (row: T, next: boolean) => Promise<FeedbackVoteState>;\n /** The page is loading: the table says so where its rows would be. */\n loading?: boolean;\n /** The opened row's id, when the app keeps it (a `?row=` param). Left out, the list\n * keeps it itself. */\n openId?: number | null;\n onOpenChange?: (id: number | null) => void;\n /**\n * 0.38.1: the opened report when it is NOT on this page — a `?row=` link to an older\n * one (Kurvenschmiede). The app fetches it (`GET /feedback/public/{id}`, a list item's\n * fields and more) and passes it with `openId`; while `openId` is its id it is drawn\n * first, above the page's rows, and opened with `renderDetail` (on the phone, in the\n * full-screen dialog). Closed, it is gone. A row the page has already is never drawn\n * twice: the page's own is opened instead.\n */\n openRow?: T | null;\n /**\n * 0.38.1: the server's page, from 0 — for a pager where `/feedback/public` answers no\n * total (Kurvenschmiede). With {@link onPageChange} a **Previous / Page n / Next**\n * strip stands under the table; Next is off without {@link hasMore}. Left out, no\n * pager: the server's `limit` is all there is.\n */\n page?: number;\n /** 0.38.1: there is a page after this one (e.g. the page came back full). */\n hasMore?: boolean;\n /** 0.38.1: go to another page (`page ± 1`); the app fetches it. Draws the pager. */\n onPageChange?: (page: number) => void;\n /** A vote COMMITS — see Button's `commit`. */\n commit?: CommitScope;\n /** See {@link FeedbackRenderDate}. Default: the kit's, with the weekday. */\n renderDate?: FeedbackRenderDate;\n /** Over the `feedbackPublic` namespace. The shared column names (#, Date, Category,\n * Subject, Status) are `feedbackPage`'s: pass `pageLabels` for those. */\n labels?: LabelOverride<FeedbackPublicLabels>;\n pageLabels?: LabelOverride<FeedbackPageLabels>;\n /** 0.40.1: the list fills the window as DataTable's `fillHeight` (page layout §1) — the\n * public feedback page passes §1.2's rule. Below md the page scrolls, as everywhere. */\n fillHeight?: boolean;\n className?: string;\n}\n\n/**\n * **All feedback** — the reports every user the app lets in may read (round 0.38,\n * docs/feedback-harmonization.md §9.3, §9.9; keksdose's `/all-feedback`).\n *\n * - **Titles only** (decision 23): #, Date, Category, Subject (with the visible\n * comments), Votes, Status. No body, no submitter, no file — the list item carries\n * none of them (§9.10).\n * - **The server sorts**: the toggle **Newest / Most wanted / Recently updated** sets\n * `sort`, and the columns themselves do not sort (a page of 50 sorted in the browser\n * would not be the 50 the sort asks for).\n * - **The search box only with `onSearchChange`**: an app whose titles are sealed\n * passes none (§9.9).\n * - **Votes** are `FeedbackVoteButton`s, optimistic, the viewer's own report pressed\n * and locked (\"Your report\", §9.6).\n * - A row opens in place with `renderDetail` — on the phone, in a full-screen dialog.\n * The phone cards carry the title, the status, the category, the date and the two\n * counts; the vote itself is in the opened report (a button inside the card, which is\n * the row's own button, would be two controls in one).\n *\n * No client paging: the server's `limit` is the page, and `total` says how many there\n * are. For more than one page, `page` / `hasMore` / `onPageChange` draw a previous /\n * next strip that needs no total (0.38.1); `openRow` shows a `?row=` report from\n * another page. Needs a router, as every `DataTable` does.\n */\nexport function FeedbackPublicList<T extends FeedbackPublicListItem = FeedbackPublicListItem>({\n rows,\n total,\n sort,\n onSortChange,\n search = \"\",\n onSearchChange,\n renderDetail,\n onVote,\n loading = false,\n openId: openIdProp,\n onOpenChange,\n openRow,\n page: pageIndex = 0,\n hasMore = false,\n onPageChange,\n commit,\n renderDate: renderDateProp,\n labels: labelsProp,\n pageLabels: pageLabelsProp,\n fillHeight,\n className,\n}: FeedbackPublicListProps<T>) {\n const labels = useFeedbackPublicLabels(labelsProp);\n const page = useFeedbackPageLabels(pageLabelsProp);\n const categoryLabels = useFeedbackCategoryLabels();\n const renderDate = useFeedbackRenderDate(renderDateProp);\n const [ownOpenId, setOwnOpenId] = useState<number | null>(null);\n const openId = openIdProp !== undefined ? openIdProp : ownOpenId;\n const setOpen = (id: number | null) => {\n if (openIdProp === undefined) setOwnOpenId(id);\n onOpenChange?.(id);\n };\n // A `?row=` report from another page: first, while it is the open one, and only when\n // the page does not have it itself.\n const shownRows =\n openRow && openId === openRow.id && !rows.some((row) => row.id === openRow.id) ? [openRow, ...rows] : rows;\n\n const columns = useMemo<DataTableColumn<T>[]>(\n () => [\n {\n key: \"id\",\n header: page.columnId,\n cell: (row) => <span className=\"font-mono text-xs text-muted\">{row.id}</span>,\n mobileHidden: true,\n },\n {\n key: \"date\",\n header: page.columnDate,\n cell: (row) => <PublicDate value={row.created_at} renderDate={renderDate} />,\n },\n {\n key: \"category\",\n header: page.columnCategory,\n cell: (row) => <FeedbackCategoryBadge category={row.category} label={categoryLabels[row.category]} />,\n },\n {\n key: \"title\",\n header: page.columnSubject,\n cell: (row) => (\n <span className=\"inline-flex min-w-0 max-w-full items-center gap-2 large:items-start\">\n <span className=\"min-w-0 truncate-until-large font-medium\">{row.title}</span>\n <CommentCount count={row.comment_count} labels={labels} />\n </span>\n ),\n mobilePrimary: true,\n },\n {\n key: \"votes\",\n header: labels.columnVotes,\n className: \"whitespace-nowrap\",\n // The cell is a button: the row's own link must never wrap it.\n noRowLink: true,\n cell: (row) => (\n <FeedbackVoteButton\n votes={row.votes}\n voted={row.voted}\n own={row.mine}\n onVote={(next) => onVote(row, next)}\n commit={commit}\n size=\"sm\"\n />\n ),\n },\n {\n key: \"status\",\n header: page.columnStatus,\n className: \"whitespace-nowrap\",\n cell: (row) => <FeedbackStatusBadge status={row.status} />,\n },\n ],\n [page, labels, categoryLabels, renderDate, onVote, commit],\n );\n\n return (\n <div\n className={cn(\"flex flex-col gap-3\", fillHeight && FILL_HEIGHT_CLASS, className)}\n data-feedback-public-list=\"\"\n aria-busy={loading || undefined}\n >\n <div className=\"flex flex-wrap items-center gap-2\">\n <ToggleGroup\n aria-label={labels.sort}\n value={sort}\n onChange={(value) => onSortChange(value)}\n options={SORTS.map((value) => ({\n value,\n label: value === \"newest\" ? labels.sortNewest : value === \"votes\" ? labels.sortVotes : labels.sortUpdated,\n }))}\n />\n {onSearchChange && (\n <SearchField\n aria-label={labels.search}\n placeholder={labels.search}\n value={search}\n onChange={onSearchChange}\n className=\"min-w-[12rem] flex-1 md:max-w-xs\"\n />\n )}\n {total !== undefined && (\n <span className=\"ms-auto text-sm text-muted\" data-feedback-public-total={total}>\n {labels.total(total)}\n </span>\n )}\n </div>\n <DataTable\n rows={shownRows}\n columns={columns}\n rowKey={(row) => row.id}\n rowName={(row) => row.title}\n onRowClick={(row) => setOpen(openId === row.id ? null : row.id)}\n isExpanded={(row) => row.id === openId}\n expandedRow={(row) => renderDetail(row)}\n mobileExpandAsDialog\n paginated={false}\n fillHeight={fillHeight}\n empty={loading ? labels.loading : labels.empty}\n mobileCard={(row) => (\n <div className=\"min-w-0 space-y-1\">\n <div className=\"flex items-start justify-between gap-2\">\n <span className=\"min-w-0 flex-1 font-medium [overflow-wrap:anywhere]\">{row.title}</span>\n <FeedbackStatusBadge status={row.status} className=\"shrink-0\" />\n </div>\n <div className=\"flex min-w-0 flex-wrap items-center gap-x-2 gap-y-1 text-xs text-muted\">\n <FeedbackCategoryBadge category={row.category} compact label={categoryLabels[row.category]} />\n <PublicDate value={row.created_at} renderDate={renderDate} />\n <CommentCount count={row.comment_count} labels={labels} />\n {/* A count, not the button: the whole card is the row's button, and a button\n inside it is two controls in one. The vote is in the opened report. */}\n <VoteCount count={row.votes} voted={row.voted || row.mine} labels={labels} />\n </div>\n </div>\n )}\n />\n {onPageChange && (pageIndex > 0 || hasMore) && (\n <PublicPager page={pageIndex} hasMore={hasMore} onPageChange={onPageChange} labels={labels} />\n )}\n </div>\n );\n}\n\n/* ── The detail ───────────────────────────────────────────────────────────── */\n\nexport interface FeedbackPublicDetailProps {\n /** `GET /feedback/public/{id}`: the public body (attachment URLs already stripped by\n * the server, §9.4) and the replies. */\n row: FeedbackPublicRecord;\n /** `GET /feedback/{id}/comments`, oldest first; `undefined` while it loads. */\n comments: readonly FeedbackComment[] | undefined;\n /** `PUT` / `DELETE /feedback/{id}/vote`; resolve with the answer, reject to roll back. */\n onVote: (next: boolean) => Promise<FeedbackVoteState>;\n /** The viewer is the app's admin: Hide / Unhide on the comments. */\n isAdmin?: boolean;\n /** `POST /feedback/{id}/comments`. Left out — a demo, a reader who may not write — no\n * composer. */\n onComment?: (body: string) => void | Promise<unknown>;\n /** The admin's hide (`PATCH …/comments/{id} {hidden}`). */\n onHide?: (comment: FeedbackComment, hidden: boolean) => void | Promise<unknown>;\n /** The writer's delete (`DELETE …/comments/{id}`), after a confirm. */\n onDelete?: (comment: FeedbackComment) => void | Promise<unknown>;\n /** A comment is being sent. */\n pending?: boolean;\n /** The vote and the thread's buttons COMMIT — see Button's `commit`. */\n commit?: CommitScope;\n /** Voting is off for this viewer (a demo): `true`, or the reason. */\n voteDisabled?: boolean | string;\n /** See {@link FeedbackRenderDate}. */\n renderDate?: FeedbackRenderDate;\n /** \"Now\" for the thread's relative times — pin it in tests and stories. */\n now?: Date | number;\n /** Over the `feedbackPublic` namespace. */\n labels?: LabelOverride<FeedbackPublicLabels>;\n /** Over the `feedbackComments` namespace, for the thread. */\n commentLabels?: LabelOverride<FeedbackCommentsLabels>;\n className?: string;\n}\n\n/**\n * One public report opened (round 0.38, docs/feedback-harmonization.md §9.3, §9.10): who\n * filed it — the first name the server sent, else **\"A user\"** (§9.5) — and when; the\n * **vote**; the **public body**; and the **conversation**, the admin's replies and other\n * people's comments merged oldest first (§9.8, `FeedbackCommentThread`), with the box to\n * add one.\n *\n * What it never draws, because the public record never carries it (§9.10): an address,\n * a last name, a screenshot, a file, the page the report was filed on. The body is the\n * server's `public_body` — the client strips nothing and adds nothing.\n */\nexport function FeedbackPublicDetail({\n row,\n comments,\n onVote,\n isAdmin = false,\n onComment,\n onHide,\n onDelete,\n pending,\n commit,\n voteDisabled,\n renderDate: renderDateProp,\n now,\n labels: labelsProp,\n commentLabels,\n className,\n}: FeedbackPublicDetailProps) {\n const labels = useFeedbackPublicLabels(labelsProp);\n const renderDate = useFeedbackRenderDate(renderDateProp);\n return (\n <FeedbackDetail className={className}>\n <div className=\"flex flex-wrap items-center gap-x-3 gap-y-2\" data-feedback-public-detail={row.id}>\n <span className=\"text-sm text-secondary\" data-feedback-author=\"\">\n {labels.filedBy(row.author_name ?? labels.anonymous)}\n </span>\n <PublicDate value={row.created_at} renderDate={renderDate} />\n <FeedbackVoteButton\n votes={row.votes}\n voted={row.voted}\n own={row.mine}\n onVote={onVote}\n disabled={voteDisabled}\n commit={commit}\n className=\"ms-auto\"\n />\n </div>\n <FeedbackDetailSection title={labels.body}>\n {/* Rework rounds stay in it — the author's own words (§9.4) — on a server before\n server-kit 0.13; since then each is an entry in the conversation (§10.3). */}\n <FeedbackProse>{row.body}</FeedbackProse>\n </FeedbackDetailSection>\n <FeedbackDetailSection title={labels.conversation}>\n <FeedbackCommentThread\n replies={row.replies}\n comments={comments ?? []}\n loading={comments === undefined}\n isAdmin={isAdmin}\n // 0.40 (§10.3): the author's rework entries are theirs — at the reading end.\n isAuthor={row.mine}\n onComment={onComment}\n onHide={onHide}\n onDelete={onDelete}\n pending={pending}\n commit={commit}\n now={now}\n labels={commentLabels}\n />\n </FeedbackDetailSection>\n </FeedbackDetail>\n );\n}\n"],"mappings":"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","names":[]}
1
+ {"version":3,"sources":["../../src/feedback/feedback-public-list.tsx"],"sourcesContent":["import { useMemo, useState } from \"react\";\nimport type { ReactNode } from \"react\";\nimport { ChevronLeft, ChevronRight, MessageSquare, ThumbsUp } from \"lucide-react\";\nimport { DataTable, type DataTableColumn } from \"../components/data-table\";\nimport { DEFAULT_DATA_TABLE_LABELS } from \"../components/data-table-labels\";\nimport { IconButton } from \"../components/icon-button\";\nimport { SearchField } from \"../components/search-field\";\nimport { ToggleGroup } from \"../components/toggle-group\";\nimport type { CommitScope } from \"../components/write-lock\";\nimport type { AuthedFetcher } from \"../hooks/use-authed-src\";\nimport { useKitLabels, type LabelOverride } from \"../i18n/kit-labels\";\nimport { cn } from \"../lib/cn\";\nimport { FILL_HEIGHT_CLASS } from \"../shell/fill-height\";\nimport { EMPTY_FORMATTED_VALUE } from \"../lib/format\";\nimport { FeedbackCategoryBadge, FeedbackDetail, FeedbackDetailSection, FeedbackProse, FeedbackStatusBadge } from \"./feedback-inbox\";\nimport { useFeedbackCategoryLabels } from \"./feedback-labels\";\nimport { FeedbackCommentThread, type FeedbackCommentsLabels } from \"./feedback-comment-thread\";\nimport type { FeedbackEntryUploadOptions } from \"./feedback-entry-file\";\nimport { DEFAULT_FEEDBACK_PUBLIC_LABELS, type FeedbackPublicLabels } from \"./feedback-public-labels\";\nimport type {\n FeedbackComment,\n FeedbackPublicListItem,\n FeedbackPublicRecord,\n FeedbackPublicSort,\n FeedbackVoteState,\n} from \"./feedback-public-types\";\nimport {\n useFeedbackPageLabels,\n useFeedbackRenderDate,\n type FeedbackPageLabels,\n type FeedbackRenderDate,\n} from \"./feedback-table\";\nimport { FeedbackVoteButton } from \"./feedback-vote-button\";\n\nexport { DEFAULT_FEEDBACK_PUBLIC_LABELS };\nexport type { FeedbackPublicLabels };\n\n/** `feedbackPublic` resolved: English, then `<UiKitProvider labels>`, then `labels`. */\nexport function useFeedbackPublicLabels(labels?: LabelOverride<FeedbackPublicLabels>): FeedbackPublicLabels {\n return useKitLabels(\"feedbackPublic\", DEFAULT_FEEDBACK_PUBLIC_LABELS, labels);\n}\n\n/** The sort toggle's order: the server's default first (§9.6). */\nconst SORTS: readonly FeedbackPublicSort[] = [\"newest\", \"votes\", \"updated\"];\n\n/** A date in a cell, as the inbox writes it: `<time>`, nowrap, tabular figures. */\nfunction PublicDate({ value, renderDate }: { value: string | null | undefined; renderDate: FeedbackRenderDate }) {\n if (!value) return <span className=\"text-placeholder\">{EMPTY_FORMATTED_VALUE}</span>;\n return (\n <time dateTime={value} className=\"whitespace-nowrap tabular-nums text-muted\">\n {renderDate(value)}\n </time>\n );\n}\n\n/** The visible comments as a small chip beside the title — the count in figures, the\n * words for a screen reader. Nothing at 0. */\nfunction CommentCount({ count, labels }: { count: number; labels: FeedbackPublicLabels }) {\n if (!count) return null;\n return (\n <span className=\"inline-flex shrink-0 items-center gap-1 text-xs text-muted\" data-feedback-comment-count={count}>\n <MessageSquare className=\"size-3.5\" aria-hidden />\n <span aria-hidden className=\"tabular-nums\">\n {count}\n </span>\n <span className=\"sr-only\">{labels.comments(count)}</span>\n </span>\n );\n}\n\n/** The votes on a phone card: the thumb and the count, in the brand colour when the\n * viewer's own. */\nfunction VoteCount({ count, voted, labels }: { count: number; voted: boolean; labels: FeedbackPublicLabels }) {\n return (\n <span\n className={cn(\"ms-auto inline-flex shrink-0 items-center gap-1 text-xs\", voted ? \"text-brand\" : \"text-muted\")}\n data-feedback-vote-count={count}\n >\n <ThumbsUp className=\"size-3.5\" aria-hidden />\n <span aria-hidden className=\"tabular-nums\">\n {count}\n </span>\n <span className=\"sr-only\">{labels.votes(count)}</span>\n </span>\n );\n}\n\n/** 0.38.1: previous / next over the server's pages when it answers no total\n * (`GET /feedback/public` — Kurvenschmiede): \"Page 3\" between two arrows. The arrows'\n * names are `dataTable`'s, the strip's; the page's is `feedbackPublic.page`. */\nfunction PublicPager({\n page,\n hasMore,\n onPageChange,\n labels,\n}: {\n page: number;\n hasMore: boolean;\n onPageChange: (page: number) => void;\n labels: FeedbackPublicLabels;\n}) {\n const tableLabels = useKitLabels(\"dataTable\", DEFAULT_DATA_TABLE_LABELS);\n const name = labels.page(page + 1);\n return (\n <div\n role=\"group\"\n aria-label={name}\n className=\"flex items-center justify-end gap-2 text-sm text-secondary\"\n data-feedback-public-pager={page}\n >\n <IconButton\n size=\"sm\"\n variant=\"secondary\"\n label={tableLabels.prevPage}\n disabled={page <= 0}\n onClick={() => onPageChange(page - 1)}\n >\n {/* \"Previous\" points back along the line — left in LTR, right in RTL. */}\n <ChevronLeft className=\"rtl:-scale-x-100\" />\n </IconButton>\n <span className=\"tabular-nums\" aria-hidden>\n {name}\n </span>\n <IconButton\n size=\"sm\"\n variant=\"secondary\"\n label={tableLabels.nextPage}\n disabled={!hasMore}\n onClick={() => onPageChange(page + 1)}\n >\n <ChevronRight className=\"rtl:-scale-x-100\" />\n </IconButton>\n </div>\n );\n}\n\n/* ── The list ─────────────────────────────────────────────────────────────── */\n\nexport interface FeedbackPublicListProps<T extends FeedbackPublicListItem = FeedbackPublicListItem> {\n /** `GET /feedback/public?sort=&q=` — the server's page, in the server's order. */\n rows: T[];\n /** How many the query has in all — the line beside the sort. Left out, no line. */\n total?: number;\n /** The server sorts (§9.6): the toggle only says which way. */\n sort: FeedbackPublicSort;\n onSortChange: (sort: FeedbackPublicSort) => void;\n /** The search box's text (`q`, titles only). */\n search?: string;\n /**\n * The search box is drawn ONLY with this handler: where the app's titles are sealed\n * (Kurvenschmiede, `FeedbackDeps.public_search=False`, §9.9) there is nothing to search\n * on and the server answers `q` with a 422 — so it passes none.\n */\n onSearchChange?: (search: string) => void;\n /** The opened row: the app's {@link FeedbackPublicDetail} with the row's\n * `GET /feedback/public/{id}` and its comments. */\n renderDetail: (row: T) => ReactNode;\n /** `PUT` / `DELETE /feedback/{id}/vote` for a row's button (`next`: the new state);\n * resolve with the answer, reject to roll back. */\n onVote: (row: T, next: boolean) => Promise<FeedbackVoteState>;\n /** The page is loading: the table says so where its rows would be. */\n loading?: boolean;\n /** The opened row's id, when the app keeps it (a `?row=` param). Left out, the list\n * keeps it itself. */\n openId?: number | null;\n onOpenChange?: (id: number | null) => void;\n /**\n * 0.38.1: the opened report when it is NOT on this page — a `?row=` link to an older\n * one (Kurvenschmiede). The app fetches it (`GET /feedback/public/{id}`, a list item's\n * fields and more) and passes it with `openId`; while `openId` is its id it is drawn\n * first, above the page's rows, and opened with `renderDetail` (on the phone, in the\n * full-screen dialog). Closed, it is gone. A row the page has already is never drawn\n * twice: the page's own is opened instead.\n */\n openRow?: T | null;\n /**\n * 0.38.1: the server's page, from 0 — for a pager where `/feedback/public` answers no\n * total (Kurvenschmiede). With {@link onPageChange} a **Previous / Page n / Next**\n * strip stands under the table; Next is off without {@link hasMore}. Left out, no\n * pager: the server's `limit` is all there is.\n */\n page?: number;\n /** 0.38.1: there is a page after this one (e.g. the page came back full). */\n hasMore?: boolean;\n /** 0.38.1: go to another page (`page ± 1`); the app fetches it. Draws the pager. */\n onPageChange?: (page: number) => void;\n /** A vote COMMITS — see Button's `commit`. */\n commit?: CommitScope;\n /** See {@link FeedbackRenderDate}. Default: the kit's, with the weekday. */\n renderDate?: FeedbackRenderDate;\n /** Over the `feedbackPublic` namespace. The shared column names (#, Date, Category,\n * Subject, Status) are `feedbackPage`'s: pass `pageLabels` for those. */\n labels?: LabelOverride<FeedbackPublicLabels>;\n pageLabels?: LabelOverride<FeedbackPageLabels>;\n /** 0.40.1: the list fills the window as DataTable's `fillHeight` (page layout §1) — the\n * public feedback page passes §1.2's rule. Below md the page scrolls, as everywhere. */\n fillHeight?: boolean;\n className?: string;\n}\n\n/**\n * **All feedback** — the reports every user the app lets in may read (round 0.38,\n * docs/feedback-harmonization.md §9.3, §9.9; keksdose's `/all-feedback`).\n *\n * - **Titles only** (decision 23): #, Date, Category, Subject (with the visible\n * comments), Votes, Status. No body, no submitter, no file — the list item carries\n * none of them (§9.10).\n * - **The server sorts**: the toggle **Newest / Most wanted / Recently updated** sets\n * `sort`, and the columns themselves do not sort (a page of 50 sorted in the browser\n * would not be the 50 the sort asks for).\n * - **The search box only with `onSearchChange`**: an app whose titles are sealed\n * passes none (§9.9).\n * - **Votes** are `FeedbackVoteButton`s, optimistic, the viewer's own report pressed\n * and locked (\"Your report\", §9.6).\n * - A row opens in place with `renderDetail` — on the phone, in a full-screen dialog.\n * The phone cards carry the title, the status, the category, the date and the two\n * counts; the vote itself is in the opened report (a button inside the card, which is\n * the row's own button, would be two controls in one).\n *\n * No client paging: the server's `limit` is the page, and `total` says how many there\n * are. For more than one page, `page` / `hasMore` / `onPageChange` draw a previous /\n * next strip that needs no total (0.38.1); `openRow` shows a `?row=` report from\n * another page. Needs a router, as every `DataTable` does.\n */\nexport function FeedbackPublicList<T extends FeedbackPublicListItem = FeedbackPublicListItem>({\n rows,\n total,\n sort,\n onSortChange,\n search = \"\",\n onSearchChange,\n renderDetail,\n onVote,\n loading = false,\n openId: openIdProp,\n onOpenChange,\n openRow,\n page: pageIndex = 0,\n hasMore = false,\n onPageChange,\n commit,\n renderDate: renderDateProp,\n labels: labelsProp,\n pageLabels: pageLabelsProp,\n fillHeight,\n className,\n}: FeedbackPublicListProps<T>) {\n const labels = useFeedbackPublicLabels(labelsProp);\n const page = useFeedbackPageLabels(pageLabelsProp);\n const categoryLabels = useFeedbackCategoryLabels();\n const renderDate = useFeedbackRenderDate(renderDateProp);\n const [ownOpenId, setOwnOpenId] = useState<number | null>(null);\n const openId = openIdProp !== undefined ? openIdProp : ownOpenId;\n const setOpen = (id: number | null) => {\n if (openIdProp === undefined) setOwnOpenId(id);\n onOpenChange?.(id);\n };\n // A `?row=` report from another page: first, while it is the open one, and only when\n // the page does not have it itself.\n const shownRows =\n openRow && openId === openRow.id && !rows.some((row) => row.id === openRow.id) ? [openRow, ...rows] : rows;\n\n const columns = useMemo<DataTableColumn<T>[]>(\n () => [\n {\n key: \"id\",\n header: page.columnId,\n cell: (row) => <span className=\"font-mono text-xs text-muted\">{row.id}</span>,\n mobileHidden: true,\n },\n {\n key: \"date\",\n header: page.columnDate,\n cell: (row) => <PublicDate value={row.created_at} renderDate={renderDate} />,\n },\n {\n key: \"category\",\n header: page.columnCategory,\n cell: (row) => <FeedbackCategoryBadge category={row.category} label={categoryLabels[row.category]} />,\n },\n {\n key: \"title\",\n header: page.columnSubject,\n cell: (row) => (\n <span className=\"inline-flex min-w-0 max-w-full items-center gap-2 large:items-start\">\n <span className=\"min-w-0 truncate-until-large font-medium\">{row.title}</span>\n <CommentCount count={row.comment_count} labels={labels} />\n </span>\n ),\n mobilePrimary: true,\n },\n {\n key: \"votes\",\n header: labels.columnVotes,\n className: \"whitespace-nowrap\",\n // The cell is a button: the row's own link must never wrap it.\n noRowLink: true,\n cell: (row) => (\n <FeedbackVoteButton\n votes={row.votes}\n voted={row.voted}\n own={row.mine}\n onVote={(next) => onVote(row, next)}\n commit={commit}\n size=\"sm\"\n />\n ),\n },\n {\n key: \"status\",\n header: page.columnStatus,\n className: \"whitespace-nowrap\",\n cell: (row) => <FeedbackStatusBadge status={row.status} />,\n },\n ],\n [page, labels, categoryLabels, renderDate, onVote, commit],\n );\n\n return (\n <div\n className={cn(\"flex flex-col gap-3\", fillHeight && FILL_HEIGHT_CLASS, className)}\n data-feedback-public-list=\"\"\n aria-busy={loading || undefined}\n >\n <div className=\"flex flex-wrap items-center gap-2\">\n <ToggleGroup\n aria-label={labels.sort}\n value={sort}\n onChange={(value) => onSortChange(value)}\n options={SORTS.map((value) => ({\n value,\n label: value === \"newest\" ? labels.sortNewest : value === \"votes\" ? labels.sortVotes : labels.sortUpdated,\n }))}\n />\n {onSearchChange && (\n <SearchField\n aria-label={labels.search}\n placeholder={labels.search}\n value={search}\n onChange={onSearchChange}\n className=\"min-w-[12rem] flex-1 md:max-w-xs\"\n />\n )}\n {total !== undefined && (\n <span className=\"ms-auto text-sm text-muted\" data-feedback-public-total={total}>\n {labels.total(total)}\n </span>\n )}\n </div>\n <DataTable\n rows={shownRows}\n columns={columns}\n rowKey={(row) => row.id}\n rowName={(row) => row.title}\n onRowClick={(row) => setOpen(openId === row.id ? null : row.id)}\n isExpanded={(row) => row.id === openId}\n expandedRow={(row) => renderDetail(row)}\n mobileExpandAsDialog\n paginated={false}\n fillHeight={fillHeight}\n empty={loading ? labels.loading : labels.empty}\n mobileCard={(row) => (\n <div className=\"min-w-0 space-y-1\">\n <div className=\"flex items-start justify-between gap-2\">\n <span className=\"min-w-0 flex-1 font-medium [overflow-wrap:anywhere]\">{row.title}</span>\n <FeedbackStatusBadge status={row.status} className=\"shrink-0\" />\n </div>\n <div className=\"flex min-w-0 flex-wrap items-center gap-x-2 gap-y-1 text-xs text-muted\">\n <FeedbackCategoryBadge category={row.category} compact label={categoryLabels[row.category]} />\n <PublicDate value={row.created_at} renderDate={renderDate} />\n <CommentCount count={row.comment_count} labels={labels} />\n {/* A count, not the button: the whole card is the row's button, and a button\n inside it is two controls in one. The vote is in the opened report. */}\n <VoteCount count={row.votes} voted={row.voted || row.mine} labels={labels} />\n </div>\n </div>\n )}\n />\n {onPageChange && (pageIndex > 0 || hasMore) && (\n <PublicPager page={pageIndex} hasMore={hasMore} onPageChange={onPageChange} labels={labels} />\n )}\n </div>\n );\n}\n\n/* ── The detail ───────────────────────────────────────────────────────────── */\n\nexport interface FeedbackPublicDetailProps {\n /** `GET /feedback/public/{id}`: the public body (attachment URLs already stripped by\n * the server, §9.4) and the replies. */\n row: FeedbackPublicRecord;\n /** `GET /feedback/{id}/comments`, oldest first; `undefined` while it loads. */\n comments: readonly FeedbackComment[] | undefined;\n /** `PUT` / `DELETE /feedback/{id}/vote`; resolve with the answer, reject to roll back. */\n onVote: (next: boolean) => Promise<FeedbackVoteState>;\n /** The viewer is the app's admin: Hide / Unhide on the comments. */\n isAdmin?: boolean;\n /** `POST /feedback/{id}/comments`. Left out — a demo, a reader who may not write — no\n * composer. 0.47 (§12): `attachmentUrl` is the one file uploaded with it (with\n * `onUpload`). */\n onComment?: (body: string, attachmentUrl?: string) => void | Promise<unknown>;\n /** 0.47 (§12): `POST /feedback/attachments` — the comment box then takes one file,\n * pasted, captured or picked, as the rework note does. Left out, text only. */\n onUpload?: FeedbackEntryUploadOptions[\"onUpload\"];\n /** 0.47: the comment box's \"Capture screenshot\". */\n onCaptureScreenshot?: FeedbackEntryUploadOptions[\"onCaptureScreenshot\"];\n /** 0.47: the comment box's file field, over the contract's defaults. */\n attachment?: FeedbackEntryUploadOptions[\"attachment\"];\n /** 0.47: the app's authenticated GET for the comments' pictures, drawn inline. */\n fetcher?: AuthedFetcher | null;\n /** The admin's hide (`PATCH …/comments/{id} {hidden}`). */\n onHide?: (comment: FeedbackComment, hidden: boolean) => void | Promise<unknown>;\n /** The writer's delete (`DELETE …/comments/{id}`), after a confirm. */\n onDelete?: (comment: FeedbackComment) => void | Promise<unknown>;\n /** A comment is being sent. */\n pending?: boolean;\n /** The vote and the thread's buttons COMMIT — see Button's `commit`. */\n commit?: CommitScope;\n /** Voting is off for this viewer (a demo): `true`, or the reason. */\n voteDisabled?: boolean | string;\n /** See {@link FeedbackRenderDate}. */\n renderDate?: FeedbackRenderDate;\n /** \"Now\" for the thread's relative times — pin it in tests and stories. */\n now?: Date | number;\n /** Over the `feedbackPublic` namespace. */\n labels?: LabelOverride<FeedbackPublicLabels>;\n /** Over the `feedbackComments` namespace, for the thread. */\n commentLabels?: LabelOverride<FeedbackCommentsLabels>;\n className?: string;\n}\n\n/**\n * One public report opened (round 0.38, docs/feedback-harmonization.md §9.3, §9.10): who\n * filed it — the first name the server sent, else **\"A user\"** (§9.5) — and when; the\n * **vote**; the **public body**; and the **conversation**, the admin's replies and other\n * people's comments merged oldest first (§9.8, `FeedbackCommentThread`), with the box to\n * add one.\n *\n * What it never draws, because the public record never carries it (§9.10): an address,\n * a last name, the report's screenshot or files, the page the report was filed on. The\n * body is the server's `public_body` — the client strips nothing and adds nothing. The\n * one file it shows (0.47, §12) is a comment's own, posted with it to be seen.\n */\nexport function FeedbackPublicDetail({\n row,\n comments,\n onVote,\n isAdmin = false,\n onComment,\n onHide,\n onDelete,\n onUpload,\n onCaptureScreenshot,\n attachment,\n fetcher,\n pending,\n commit,\n voteDisabled,\n renderDate: renderDateProp,\n now,\n labels: labelsProp,\n commentLabels,\n className,\n}: FeedbackPublicDetailProps) {\n const labels = useFeedbackPublicLabels(labelsProp);\n const renderDate = useFeedbackRenderDate(renderDateProp);\n return (\n <FeedbackDetail className={className}>\n <div className=\"flex flex-wrap items-center gap-x-3 gap-y-2\" data-feedback-public-detail={row.id}>\n <span className=\"text-sm text-secondary\" data-feedback-author=\"\">\n {labels.filedBy(row.author_name ?? labels.anonymous)}\n </span>\n <PublicDate value={row.created_at} renderDate={renderDate} />\n <FeedbackVoteButton\n votes={row.votes}\n voted={row.voted}\n own={row.mine}\n onVote={onVote}\n disabled={voteDisabled}\n commit={commit}\n className=\"ms-auto\"\n />\n </div>\n <FeedbackDetailSection title={labels.body}>\n {/* Rework rounds stay in it — the author's own words (§9.4) — on a server before\n server-kit 0.13; since then each is an entry in the conversation (§10.3). */}\n <FeedbackProse>{row.body}</FeedbackProse>\n </FeedbackDetailSection>\n <FeedbackDetailSection title={labels.conversation}>\n <FeedbackCommentThread\n replies={row.replies}\n comments={comments ?? []}\n loading={comments === undefined}\n isAdmin={isAdmin}\n // 0.40 (§10.3): the author's rework entries are theirs — at the reading end.\n isAuthor={row.mine}\n onComment={onComment}\n onHide={onHide}\n onDelete={onDelete}\n onUpload={onUpload}\n onCaptureScreenshot={onCaptureScreenshot}\n attachment={attachment}\n fetcher={fetcher}\n pending={pending}\n commit={commit}\n now={now}\n labels={commentLabels}\n />\n </FeedbackDetailSection>\n </FeedbackDetail>\n );\n}\n"],"mappings":"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","names":[]}
@@ -25,8 +25,9 @@ export interface FeedbackReply {
25
25
  /** 0.40 (§10.3): `"reply"` (absent: a server before server-kit 0.13, all replies) or
26
26
  * `"rework"`, the author's entry. */
27
27
  kind?: FeedbackEntryKind;
28
- /** 0.40: a rework entry's one file (`/api/v1/feedback/attachments/<key>`) — the admin
29
- * and author views only; `null` or absent otherwise (never in the public view, §9.10). */
28
+ /** The entry's one file (`/api/v1/feedback/attachments/<key>`): a rework's (0.40) or,
29
+ * since 0.47 (§12, server-kit 0.18), an admin reply's — the admin and author views
30
+ * only; `null` or absent otherwise (never in the public view, §9.10). */
30
31
  attachment_url?: string | null;
31
32
  /** The status this reply moved the report to; `null` = no status change — and on a
32
33
  * rework migrated out of the body (§10.4), whose move was never recorded. */
@@ -48,6 +49,10 @@ export interface FeedbackComment {
48
49
  /** `null` when hidden, but for its writer and an admin. */
49
50
  body: string | null;
50
51
  hidden: boolean;
52
+ /** 0.47 (§12, server-kit 0.18): the comment's one file, shown with it to whoever reads
53
+ * its body; `null` when it has none or is hidden from this viewer, absent from an
54
+ * older server. */
55
+ attachment_url?: string | null;
51
56
  /** ISO 8601. */
52
57
  created_at: string;
53
58
  }