@eifi1/ui-kit 0.38.0 → 0.38.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/eslint/index.js +1 -1
- package/dist/feedback/feedback-comment-thread.d.ts +13 -6
- package/dist/feedback/feedback-comment-thread.js +10 -2
- package/dist/feedback/feedback-comment-thread.js.map +1 -1
- package/dist/feedback/feedback-labels.d.ts +11 -0
- package/dist/feedback/feedback-labels.js +4 -1
- package/dist/feedback/feedback-labels.js.map +1 -1
- package/dist/feedback/feedback-public-labels.d.ts +3 -0
- package/dist/feedback/feedback-public-labels.js +2 -1
- package/dist/feedback/feedback-public-labels.js.map +1 -1
- package/dist/feedback/feedback-public-list.d.ts +25 -4
- package/dist/feedback/feedback-public-list.js +54 -3
- package/dist/feedback/feedback-public-list.js.map +1 -1
- package/dist/feedback/feedback-public-switch.js +3 -3
- package/dist/feedback/feedback-public-switch.js.map +1 -1
- package/dist/feedback/feedback-record.d.ts +7 -7
- package/dist/feedback/feedback-record.js.map +1 -1
- package/dist/feedback/feedback-replies-context.d.ts +7 -0
- package/dist/feedback/feedback-replies-context.js +7 -0
- package/dist/feedback/feedback-replies-context.js.map +1 -0
- package/dist/feedback/feedback-row-detail.d.ts +12 -3
- package/dist/feedback/feedback-row-detail.js +2 -1
- package/dist/feedback/feedback-row-detail.js.map +1 -1
- package/dist/feedback/feedback-table.js +2 -2
- package/dist/feedback/feedback-table.js.map +1 -1
- package/dist/feedback/feedback-vote-button.d.ts +2 -1
- package/dist/feedback/feedback-vote-button.js.map +1 -1
- package/dist/i18n/german.js +6 -2
- package/dist/i18n/german.js.map +1 -1
- package/dist/i18n/locales/en.js +8 -3
- package/dist/i18n/locales/en.js.map +2 -2
- package/dist/i18n/locales/es.js +6 -2
- package/dist/i18n/locales/es.js.map +1 -1
- package/dist/i18n/locales/fr.js +6 -2
- package/dist/i18n/locales/fr.js.map +1 -1
- package/dist/i18n/locales/hu.js +6 -2
- package/dist/i18n/locales/hu.js.map +1 -1
- package/dist/i18n/locales/it.js +6 -2
- package/dist/i18n/locales/it.js.map +1 -1
- package/dist/i18n/locales/zh.js +6 -2
- package/dist/i18n/locales/zh.js.map +1 -1
- package/dist/i18n/review.js +1 -0
- package/dist/i18n/review.js.map +1 -1
- package/package.json +1 -1
- package/src/feedback/feedback-comment-thread.tsx +29 -7
- package/src/feedback/feedback-labels.ts +14 -0
- package/src/feedback/feedback-public-labels.ts +4 -0
- package/src/feedback/feedback-public-list.tsx +88 -5
- package/src/feedback/feedback-public-switch.tsx +3 -3
- package/src/feedback/feedback-record.ts +7 -7
- package/src/feedback/feedback-replies-context.ts +9 -0
- package/src/feedback/feedback-row-detail.tsx +16 -4
- package/src/feedback/feedback-table.tsx +2 -2
- package/src/feedback/feedback-vote-button.tsx +2 -1
- package/src/i18n/german.ts +4 -0
- package/src/i18n/locales/en.ts +1 -0
- package/src/i18n/locales/es.ts +4 -0
- package/src/i18n/locales/fr.ts +4 -0
- package/src/i18n/locales/hu.ts +4 -0
- package/src/i18n/locales/it.ts +4 -0
- package/src/i18n/locales/zh.ts +4 -0
- package/src/i18n/review.ts +1 -0
package/dist/eslint/index.js
CHANGED
|
@@ -47,7 +47,7 @@ const roleUtilities = {
|
|
|
47
47
|
};
|
|
48
48
|
}
|
|
49
49
|
};
|
|
50
|
-
const VERSION = true ? "0.38.
|
|
50
|
+
const VERSION = true ? "0.38.1" : "0.0.0-source";
|
|
51
51
|
const recommended = {
|
|
52
52
|
name: "@eifi1/ui-kit/recommended",
|
|
53
53
|
// Not the tests, as check:tokens skips the kit's: a test names the old class to assert it
|
|
@@ -51,17 +51,24 @@ export interface FeedbackCommentsLabels {
|
|
|
51
51
|
}
|
|
52
52
|
export { DEFAULT_FEEDBACK_COMMENTS_LABELS };
|
|
53
53
|
export interface FeedbackCommentThreadProps {
|
|
54
|
-
/** The report's replies (§9.7), oldest first. `[]
|
|
55
|
-
|
|
54
|
+
/** The report's replies (§9.7), oldest first. Left out or `[]`: none.
|
|
55
|
+
*
|
|
56
|
+
* Inside `FeedbackRowDetail`'s `comments` they are dropped (0.38.1): the detail's
|
|
57
|
+
* Replies section draws them already, and the thread there is the comments alone —
|
|
58
|
+
* so passing them there cannot draw them twice. */
|
|
59
|
+
replies?: readonly FeedbackReply[];
|
|
56
60
|
/** `GET /feedback/{id}/comments`, oldest first. */
|
|
57
61
|
comments: readonly FeedbackComment[];
|
|
58
62
|
/** The viewer is the app's admin: Hide / Unhide, the hidden text struck through. */
|
|
59
63
|
isAdmin: boolean;
|
|
60
64
|
/** `POST /feedback/{id}/comments`. Return the promise: the box clears when it
|
|
61
|
-
* resolves
|
|
65
|
+
* resolves, and keeps the draft when it rejects. A failure is the app's to toast —
|
|
66
|
+
* the server's `detail`, else `feedbackToast.commentFailed` (0.38.1). Left out, no
|
|
67
|
+
* composer. */
|
|
62
68
|
onComment?: (body: string) => void | Promise<unknown>;
|
|
63
|
-
/** `PATCH /feedback/{id}/comments/{comment_id} {hidden}` — the admin's.
|
|
64
|
-
*
|
|
69
|
+
/** `PATCH /feedback/{id}/comments/{comment_id} {hidden}` — the admin's. A rejection
|
|
70
|
+
* is caught (0.38.1) and is the app's to toast (`feedbackToast.updateFailed`). Left
|
|
71
|
+
* out, no Hide / Unhide. */
|
|
65
72
|
onHide?: (comment: FeedbackComment, hidden: boolean) => void | Promise<unknown>;
|
|
66
73
|
/** `DELETE /feedback/{id}/comments/{comment_id}` — the writer's own, after the
|
|
67
74
|
* confirm. Left out, no Delete. */
|
|
@@ -77,4 +84,4 @@ export interface FeedbackCommentThreadProps {
|
|
|
77
84
|
labels?: LabelOverride<FeedbackCommentsLabels>;
|
|
78
85
|
className?: string;
|
|
79
86
|
}
|
|
80
|
-
export declare function FeedbackCommentThread({ replies, comments, isAdmin, onComment, onHide, onDelete, pending, loading, commit, now, labels: labelsProp, className, }: FeedbackCommentThreadProps): import("react").JSX.Element;
|
|
87
|
+
export declare function FeedbackCommentThread({ replies: repliesProp, comments, isAdmin, onComment, onHide, onDelete, pending, loading, commit, now, labels: labelsProp, className, }: FeedbackCommentThreadProps): import("react").JSX.Element;
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
import { jsx, jsxs } from "react/jsx-runtime";
|
|
3
|
+
import { useContext } from "react";
|
|
3
4
|
import { EyeOff } from "lucide-react";
|
|
4
5
|
import { Button } from "../components/button.js";
|
|
5
6
|
import { useConfirm } from "../components/confirm-dialog.js";
|
|
@@ -8,14 +9,18 @@ import { FeedbackStatusBadge } from "./feedback-inbox.js";
|
|
|
8
9
|
import { DEFAULT_FEEDBACK_PUBLIC_LABELS } from "./feedback-public-labels.js";
|
|
9
10
|
import { FEEDBACK_COMMENT_MAX } from "./feedback-record.js";
|
|
10
11
|
import { useFeedbackRepliesLabels } from "./feedback-replies.js";
|
|
12
|
+
import { FeedbackRepliesDrawnAboveContext } from "./feedback-replies-context.js";
|
|
11
13
|
import { ChatComposer, FeedbackThread } from "./feedback-thread.js";
|
|
12
14
|
import { DEFAULT_FEEDBACK_COMMENTS_LABELS } from "./feedback-thread-parts-labels.js";
|
|
15
|
+
const hideComment = async (onHide, comment) => {
|
|
16
|
+
await onHide(comment, !comment.hidden);
|
|
17
|
+
};
|
|
13
18
|
const instant = (iso) => {
|
|
14
19
|
const ms = Date.parse(iso);
|
|
15
20
|
return Number.isNaN(ms) ? 0 : ms;
|
|
16
21
|
};
|
|
17
22
|
function FeedbackCommentThread({
|
|
18
|
-
replies,
|
|
23
|
+
replies: repliesProp = [],
|
|
19
24
|
comments,
|
|
20
25
|
isAdmin,
|
|
21
26
|
onComment,
|
|
@@ -31,6 +36,8 @@ function FeedbackCommentThread({
|
|
|
31
36
|
const labels = useKitLabels("feedbackComments", DEFAULT_FEEDBACK_COMMENTS_LABELS, labelsProp);
|
|
32
37
|
const anonymous = useKitLabels("feedbackPublic", DEFAULT_FEEDBACK_PUBLIC_LABELS).anonymous;
|
|
33
38
|
const replyLabels = useFeedbackRepliesLabels();
|
|
39
|
+
const repliesDrawnAbove = useContext(FeedbackRepliesDrawnAboveContext);
|
|
40
|
+
const replies = repliesDrawnAbove ? [] : repliesProp;
|
|
34
41
|
const entries = [];
|
|
35
42
|
replies.forEach((reply, order) => {
|
|
36
43
|
entries.push({
|
|
@@ -71,7 +78,8 @@ function FeedbackCommentThread({
|
|
|
71
78
|
tone: "muted",
|
|
72
79
|
size: "sm",
|
|
73
80
|
commit,
|
|
74
|
-
onClick: () => void onHide
|
|
81
|
+
onClick: () => void hideComment(onHide, comment).catch(() => {
|
|
82
|
+
}),
|
|
75
83
|
children: comment.hidden ? labels.unhide : labels.hide
|
|
76
84
|
},
|
|
77
85
|
"hide"
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/feedback/feedback-comment-thread.tsx"],"sourcesContent":["import 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 } from \"./feedback-record\";\nimport { useFeedbackRepliesLabels } from \"./feedback-replies\";\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 * - **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. `[]` when it has none. */\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 /** `POST /feedback/{id}/comments`. Return the promise: the box clears when it\n * resolves. Left out, no composer. */\n onComment?: (body: string) => void | Promise<unknown>;\n /** `PATCH /feedback/{id}/comments/{comment_id} {hidden}` — the admin's. Left out, no\n * 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\nconst instant = (iso: string) => {\n const ms = Date.parse(iso);\n return Number.isNaN(ms) ? 0 : ms;\n};\n\nexport function FeedbackCommentThread({\n replies,\n comments,\n isAdmin,\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\n const entries: Entry[] = [];\n replies.forEach((reply, order) => {\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 onClick={() => void onHide(comment, !comment.hidden)}\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 { 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 } from \"./feedback-record\";\nimport { useFeedbackRepliesLabels } from \"./feedback-replies\";\nimport { FeedbackRepliesDrawnAboveContext } from \"./feedback-replies-context\";\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 * - **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 /** `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 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 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":";AA2JU,SACE,KADF;AA3JV,SAAS,kBAAkB;AAE3B,SAAS,cAAc;AACvB,SAAS,cAAc;AACvB,SAAS,kBAAkB;AAE3B,SAAS,oBAAwC;AACjD,SAAS,2BAA2B;AAEpC,SAAS,sCAAsC;AAC/C,SAAS,4BAA4B;AACrC,SAAS,gCAAgC;AACzC,SAAS,wCAAwC;AACjD,SAAS,cAAc,sBAAsB;AAE7C,SAAS,wCAAwC;AAgGjD,MAAM,cAAc,OAClB,QACA,YACG;AACH,QAAM,OAAO,SAAS,CAAC,QAAQ,MAAM;AACvC;AAEA,MAAM,UAAU,CAAC,QAAgB;AAC/B,QAAM,KAAK,KAAK,MAAM,GAAG;AACzB,SAAO,OAAO,MAAM,EAAE,IAAI,IAAI;AAChC;AAEO,SAAS,sBAAsB;AAAA,EACpC,SAAS,cAAc,CAAC;AAAA,EACxB;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA,UAAU;AAAA,EACV,UAAU;AAAA,EACV;AAAA,EACA;AAAA,EACA,QAAQ;AAAA,EACR;AACF,GAA+B;AAC7B,QAAM,SAAS,aAAa,oBAAoB,kCAAkC,UAAU;AAC5F,QAAM,YAAY,aAAa,kBAAkB,8BAA8B,EAAE;AACjF,QAAM,cAAc,yBAAyB;AAC7C,QAAM,oBAAoB,WAAW,gCAAgC;AACrE,QAAM,UAAU,oBAAoB,CAAC,IAAI;AAEzC,QAAM,UAAmB,CAAC;AAC1B,UAAQ,QAAQ,CAAC,OAAO,UAAU;AAChC,YAAQ,KAAK;AAAA,MACX,IAAI,QAAQ,MAAM,UAAU;AAAA,MAC5B;AAAA,MACA,SAAS;AAAA,QACP,IAAI,SAAS,MAAM,EAAE;AAAA,QACrB,QAAQ,MAAM,eAAe,YAAY;AAAA,QACzC,WAAW,MAAM;AAAA,QACjB,MAAM,MAAM;AAAA,QACZ,OAAO;AAAA,QACP,OAAO,MAAM,YACX,qBAAC,UAAK,WAAU,+DAA8D,8BAA4B,MAAM,WAC9G;AAAA,8BAAC,UAAK,WAAU,WAAW,sBAAY,eAAc;AAAA,UACrD,oBAAC,UAAK,eAAW,MAAC,oBAAC;AAAA,UACnB,oBAAC,uBAAoB,QAAQ,MAAM,WAAW;AAAA,WAChD,IACE;AAAA,MACN;AAAA,IACF,CAAC;AAAA,EACH,CAAC;AACD,WAAS,QAAQ,CAAC,SAAS,UAAU;AAEnC,UAAM,SAAS,QAAQ,UAAU,CAAC,QAAQ,QAAQ,CAAC;AACnD,UAAM,UAAuB,CAAC;AAC9B,QAAI,QAAQ,QAAQ;AAClB,cAAQ;AAAA,QACN,qBAAC,UAAkB,WAAU,kCAAiC,gCAA6B,IACzF;AAAA,8BAAC,UAAO,WAAU,UAAS,eAAW,MAAC;AAAA,UACtC,OAAO;AAAA,aAFA,QAGV;AAAA,MACF;AAAA,IACF;AACA,QAAI,WAAW,QAAQ;AACrB,cAAQ;AAAA,QACN;AAAA,UAAC;AAAA;AAAA,YAEC,MAAK;AAAA,YACL,SAAQ;AAAA,YACR,MAAK;AAAA,YACL,MAAK;AAAA,YACL;AAAA,YAGA,SAAS,MAAM,KAAK,YAAY,QAAQ,OAAO,EAAE,MAAM,MAAM;AAAA,YAAC,CAAC;AAAA,YAE9D,kBAAQ,SAAS,OAAO,SAAS,OAAO;AAAA;AAAA,UAVrC;AAAA,QAWN;AAAA,MACF;AAAA,IACF;AACA,QAAI,QAAQ,QAAQ,UAAU;AAC5B,cAAQ;AAAA,QACN,oBAAC,uBAAiC,SAAkB,UAAoB,QAAgB,UAA/D,QAA+E;AAAA,MAC1G;AAAA,IACF;AACA,YAAQ,KAAK;AAAA,MACX,IAAI,QAAQ,QAAQ,UAAU;AAAA;AAAA,MAE9B,OAAO,QAAQ,SAAS;AAAA,MACxB,SAAS;AAAA,QACP,IAAI,WAAW,QAAQ,EAAE;AAAA,QACzB,KAAK,QAAQ;AAAA,QACb,QAAQ,SAAS,SAAY,QAAQ,OAAO,SAAa,QAAQ,eAAe;AAAA,QAChF,WAAW,QAAQ;AAAA,QACnB,MAAM,SAAS,KAAM,QAAQ,QAAQ;AAAA,QACrC,OAAO,CAAC,UAAU,QAAQ;AAAA,QAC1B,QAAQ,QAAQ,UAAU,WAAW,CAAC;AAAA,QACtC,QAAQ,QAAQ,SAAS,IAAI,UAAU;AAAA,MACzC;AAAA,IACF,CAAC;AAAA,EACH,CAAC;AACD,UAAQ,KAAK,CAAC,GAAG,MAAM,EAAE,KAAK,EAAE,MAAM,EAAE,QAAQ,EAAE,KAAK;AAEvD,SACE,qBAAC,SAAI,WAAsB,gCAA6B,IACtD;AAAA;AAAA,MAAC;AAAA;AAAA,QACC,UAAU,QAAQ,IAAI,CAAC,UAAU,MAAM,OAAO;AAAA,QAC9C;AAAA,QACA;AAAA,QACA,QAAQ,EAAE,QAAQ,OAAO,UAAU,OAAO,OAAO,MAAM;AAAA;AAAA,IACzD;AAAA,IACC,aACC;AAAA,MAAC;AAAA;AAAA,QACC,WAAU;AAAA,QACV,QAAQ,CAAC,SAAS,UAAU,IAAI;AAAA,QAChC;AAAA,QACA;AAAA,QACA,WAAW;AAAA,QACX,WAAS;AAAA,QACT,QAAQ,EAAE,OAAO,OAAO,OAAO,aAAa,OAAO,aAAa,MAAM,OAAO,KAAK;AAAA;AAAA,IACpF;AAAA,KAEJ;AAEJ;AAKA,SAAS,oBAAoB;AAAA,EAC3B;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACF,GAKG;AACD,QAAM,UAAU,WAAW;AAC3B,QAAM,SAAS,YAAY;AACzB,UAAM,KAAK,MAAM,QAAQ;AAAA,MACvB,OAAO,OAAO;AAAA,MACd,MAAM,OAAO;AAAA,MACb,cAAc,OAAO;AAAA,MACrB,MAAM;AAAA,MACN;AAAA,IACF,CAAC;AACD,QAAI,GAAI,OAAM,SAAS,OAAO;AAAA,EAChC;AACA,SACE;AAAA,IAAC;AAAA;AAAA,MACC,MAAK;AAAA,MACL,SAAQ;AAAA,MACR,MAAK;AAAA,MACL,MAAK;AAAA,MACL;AAAA,MACA,SAAS,MAAM,KAAK,OAAO,EAAE,MAAM,MAAM;AAAA,MAAC,CAAC;AAAA,MAE1C,iBAAO;AAAA;AAAA,EACV;AAEJ;","names":[]}
|
|
@@ -71,6 +71,17 @@ export interface FeedbackToastLabels {
|
|
|
71
71
|
/** The Undo landed — `status` is the RESTORED status's label.
|
|
72
72
|
* de-CH: "Zurück auf „{{status}}“: {{title}}" */
|
|
73
73
|
statusRestored: (status: string, title: string) => string;
|
|
74
|
+
/** 0.38.1: `POST /feedback/{id}/comments` failed with no `detail` of its own — for
|
|
75
|
+
* `FeedbackCommentThread`'s `onComment`. de-CH: "Kommentar konnte nicht gesendet
|
|
76
|
+
* werden." */
|
|
77
|
+
commentFailed: string;
|
|
78
|
+
/** 0.38.1: `PUT` / `DELETE /feedback/{id}/vote` failed with no `detail` of its own —
|
|
79
|
+
* for `FeedbackVoteButton`'s `onVote` (the button rolls back; the app toasts).
|
|
80
|
+
* de-CH: "Stimme konnte nicht gespeichert werden." */
|
|
81
|
+
voteFailed: string;
|
|
82
|
+
/** 0.38.1: `POST /feedback/{id}/replies` failed with no `detail` of its own — for
|
|
83
|
+
* `FeedbackRowDetail`'s `onReply`. de-CH: "Antwort konnte nicht gesendet werden." */
|
|
84
|
+
replyFailed: string;
|
|
74
85
|
}
|
|
75
86
|
export declare const DEFAULT_FEEDBACK_TOAST_LABELS: FeedbackToastLabels;
|
|
76
87
|
/** `feedbackStatus` resolved: English, then `<UiKitProvider labels>`, then `labels`. For
|
|
@@ -27,7 +27,10 @@ const DEFAULT_FEEDBACK_TOAST_LABELS = {
|
|
|
27
27
|
updateFailed: "Could not save that change.",
|
|
28
28
|
statusChanged: (status, title) => `Set to \u201C${status}\u201D: ${title}`,
|
|
29
29
|
statusUndo: "Undo",
|
|
30
|
-
statusRestored: (status, title) => `Back to \u201C${status}\u201D: ${title}
|
|
30
|
+
statusRestored: (status, title) => `Back to \u201C${status}\u201D: ${title}`,
|
|
31
|
+
commentFailed: "Could not post the comment.",
|
|
32
|
+
voteFailed: "Could not save your vote.",
|
|
33
|
+
replyFailed: "Could not send the reply."
|
|
31
34
|
};
|
|
32
35
|
function useFeedbackStatusLabels(labels) {
|
|
33
36
|
return useKitLabels("feedbackStatus", DEFAULT_FEEDBACK_STATUS_LABELS, labels);
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/feedback/feedback-labels.ts"],"sourcesContent":["import { useKitLabels, type LabelOverride } from \"../i18n/kit-labels\";\nimport type { FeedbackCategory, FeedbackStatus } from \"./feedback-vocabulary\";\n\n/**\n * The words of a report's status and category, and the toasts a feedback page shows —\n * three `<UiKitProvider labels>` namespaces (0.27.0).\n *\n * Until 0.27 every app kept its own map from the enum to a translation key (keksdose\n * `STATUS_LABEL` and `categoryLabelKey`, \"the package has no translations\"), and the\n * three German maps had drifted apart: Kurvenschmiede said \"In Arbeit\", \"In Prüfung\",\n * \"Test auf der Live-Umgebung\" where keksdose and kastlan said \"In Bearbeitung\",\n * \"Zur Prüfung\", \"Live testen\". Marcel's feedback round (2026-10-04,\n * docs/feedback-harmonization.md §2.4) made keksdose's wording the canon and the kit its\n * owner, in all seven languages. The English defaults below are keksdose's\n * `frontend/src/shared/i18n/locales/en.json` (`feedback.*`); the de-CH canon for the\n * catalogues is noted on each key (keksdose `de-CH.json` — ss, never ß).\n *\n * `FeedbackStatusBadge`, `FeedbackCategoryBadge` and `FeedbackStatusTransitions` read\n * the first two by default, so an app drops its own maps; a `label` passed to them still\n * wins.\n */\n\n/**\n * `feedbackStatus` — one word per status, keyed by the enum value so\n * `labels[row.status]` is the whole lookup.\n *\n * de-CH canon: OPEN \"Offen\" · READY \"Bereit zur Umsetzung\" · IN_PROGRESS \"In Bearbeitung\" · IN_EVALUATION \"Zur Prüfung\"\n * · NEEDS_LIVE_TEST \"Live testen\" · POSTPONED \"Zurückgestellt\" · DONE \"Erledigt\" ·\n * WONT_DO \"Wird nicht umgesetzt\".\n */\nexport type FeedbackStatusLabels = Record<FeedbackStatus, string>;\n\nexport const DEFAULT_FEEDBACK_STATUS_LABELS: FeedbackStatusLabels = {\n OPEN: \"Open\",\n READY: \"Ready to implement\",\n IN_PROGRESS: \"In progress\",\n IN_EVALUATION: \"In evaluation\",\n NEEDS_LIVE_TEST: \"Test when live\",\n POSTPONED: \"Postponed\",\n DONE: \"Done\",\n WONT_DO: \"Won't do\",\n};\n\n/**\n * `feedbackCategory` — one word per category, keyed by the enum value. `CRASH` is never\n * pickable, but it is shown: on the badge, in the filter, in a row's detail.\n *\n * de-CH canon: CRASH \"Absturz\" · BUG \"Fehler\" · IDEA \"Idee\" · QUESTION \"Frage\" ·\n * OTHER \"Sonstiges\".\n */\nexport type FeedbackCategoryLabels = Record<FeedbackCategory, string>;\n\nexport const DEFAULT_FEEDBACK_CATEGORY_LABELS: FeedbackCategoryLabels = {\n CRASH: \"Crash\",\n BUG: \"Bug\",\n IDEA: \"Idea\",\n QUESTION: \"Question\",\n OTHER: \"Other\",\n};\n\n/**\n * `feedbackToast` — what the submit dialog, a failed save and a status change say in a\n * toast (§4.2, §4.4, §4.5 of the contract). A server's `detail` sentence, when it sent\n * one, wins over `submitFailed` and `updateFailed` (§3.7); that choice is the caller's.\n */\nexport interface FeedbackToastLabels {\n /** A report was filed. de-CH: \"Danke für Ihr Feedback!\" */\n submitted: string;\n /** `POST /feedback` failed with no `detail` of its own.\n * de-CH: \"Feedback konnte nicht gesendet werden\" */\n submitFailed: string;\n /** A picked file is not an image, a PDF or a text file (§3.5).\n * de-CH: \"Nur Bilder, PDF- oder Textdateien sind erlaubt\" */\n attachmentUnsupported: string;\n /** A picked file is over the 10 MB the contract allows (§3.5).\n * de-CH: \"Datei ist grösser als 10 MB\" */\n attachmentTooLarge: string;\n /** More files were picked than fit; `count` is how many fit.\n * de-CH: \"Es passen nur {{count}} Anhänge – die übrigen wurden weggelassen.\" */\n attachmentTooMany: (count: number) => string;\n /** The screenshot capture failed (or its optional peer is not installed).\n * de-CH: \"Screenshot konnte nicht aufgenommen werden\" */\n captureFailed: string;\n /** A `PATCH /feedback/{id}` failed with no `detail` of its own (§4.4).\n * de-CH: \"Änderung konnte nicht gespeichert werden.\" */\n updateFailed: string;\n /** A status change landed — the 8 s Undo toast (§4.5). `status` is the NEW status's\n * label, `title` the report's title. de-CH: \"Auf „{{status}}“ gesetzt: {{title}}\" */\n statusChanged: (status: string, title: string) => string;\n /** That toast's action. de-CH: \"Rückgängig\" */\n statusUndo: string;\n /** The Undo landed — `status` is the RESTORED status's label.\n * de-CH: \"Zurück auf „{{status}}“: {{title}}\" */\n statusRestored: (status: string, title: string) => string;\n}\n\nexport const DEFAULT_FEEDBACK_TOAST_LABELS: FeedbackToastLabels = {\n submitted: \"Thanks for the feedback!\",\n submitFailed: \"Could not submit feedback\",\n attachmentUnsupported: \"Only images, PDF or text files are allowed\",\n attachmentTooLarge: \"File is larger than 10 MB\",\n attachmentTooMany: (count) =>\n count === 1\n ? \"Only 1 attachment fits — the rest were left out.\"\n : `Only ${count} attachments fit — the rest were left out.`,\n captureFailed: \"Could not capture a screenshot\",\n updateFailed: \"Could not save that change.\",\n statusChanged: (status, title) => `Set to “${status}”: ${title}`,\n statusUndo: \"Undo\",\n statusRestored: (status, title) => `Back to “${status}”: ${title}`,\n};\n\n/** `feedbackStatus` resolved: English, then `<UiKitProvider labels>`, then `labels`. For\n * the places that need a status's name outside the kit's own parts — a filter's options,\n * `feedbackSwipePlan`'s panels, an app's overview tile. */\nexport function useFeedbackStatusLabels(labels?: LabelOverride<FeedbackStatusLabels>): FeedbackStatusLabels {\n return useKitLabels(\"feedbackStatus\", DEFAULT_FEEDBACK_STATUS_LABELS, labels);\n}\n\n/** `feedbackCategory` resolved: English, then the provider, then `labels`. */\nexport function useFeedbackCategoryLabels(labels?: LabelOverride<FeedbackCategoryLabels>): FeedbackCategoryLabels {\n return useKitLabels(\"feedbackCategory\", DEFAULT_FEEDBACK_CATEGORY_LABELS, labels);\n}\n\n/** `feedbackToast` resolved: English, then the provider, then `labels`. */\nexport function useFeedbackToastLabels(labels?: LabelOverride<FeedbackToastLabels>): FeedbackToastLabels {\n return useKitLabels(\"feedbackToast\", DEFAULT_FEEDBACK_TOAST_LABELS, labels);\n}\n"],"mappings":";AAAA,SAAS,oBAAwC;AAgC1C,MAAM,iCAAuD;AAAA,EAClE,MAAM;AAAA,EACN,OAAO;AAAA,EACP,aAAa;AAAA,EACb,eAAe;AAAA,EACf,iBAAiB;AAAA,EACjB,WAAW;AAAA,EACX,MAAM;AAAA,EACN,SAAS;AACX;AAWO,MAAM,mCAA2D;AAAA,EACtE,OAAO;AAAA,EACP,KAAK;AAAA,EACL,MAAM;AAAA,EACN,UAAU;AAAA,EACV,OAAO;AACT;
|
|
1
|
+
{"version":3,"sources":["../../src/feedback/feedback-labels.ts"],"sourcesContent":["import { useKitLabels, type LabelOverride } from \"../i18n/kit-labels\";\nimport type { FeedbackCategory, FeedbackStatus } from \"./feedback-vocabulary\";\n\n/**\n * The words of a report's status and category, and the toasts a feedback page shows —\n * three `<UiKitProvider labels>` namespaces (0.27.0).\n *\n * Until 0.27 every app kept its own map from the enum to a translation key (keksdose\n * `STATUS_LABEL` and `categoryLabelKey`, \"the package has no translations\"), and the\n * three German maps had drifted apart: Kurvenschmiede said \"In Arbeit\", \"In Prüfung\",\n * \"Test auf der Live-Umgebung\" where keksdose and kastlan said \"In Bearbeitung\",\n * \"Zur Prüfung\", \"Live testen\". Marcel's feedback round (2026-10-04,\n * docs/feedback-harmonization.md §2.4) made keksdose's wording the canon and the kit its\n * owner, in all seven languages. The English defaults below are keksdose's\n * `frontend/src/shared/i18n/locales/en.json` (`feedback.*`); the de-CH canon for the\n * catalogues is noted on each key (keksdose `de-CH.json` — ss, never ß).\n *\n * `FeedbackStatusBadge`, `FeedbackCategoryBadge` and `FeedbackStatusTransitions` read\n * the first two by default, so an app drops its own maps; a `label` passed to them still\n * wins.\n */\n\n/**\n * `feedbackStatus` — one word per status, keyed by the enum value so\n * `labels[row.status]` is the whole lookup.\n *\n * de-CH canon: OPEN \"Offen\" · READY \"Bereit zur Umsetzung\" · IN_PROGRESS \"In Bearbeitung\" · IN_EVALUATION \"Zur Prüfung\"\n * · NEEDS_LIVE_TEST \"Live testen\" · POSTPONED \"Zurückgestellt\" · DONE \"Erledigt\" ·\n * WONT_DO \"Wird nicht umgesetzt\".\n */\nexport type FeedbackStatusLabels = Record<FeedbackStatus, string>;\n\nexport const DEFAULT_FEEDBACK_STATUS_LABELS: FeedbackStatusLabels = {\n OPEN: \"Open\",\n READY: \"Ready to implement\",\n IN_PROGRESS: \"In progress\",\n IN_EVALUATION: \"In evaluation\",\n NEEDS_LIVE_TEST: \"Test when live\",\n POSTPONED: \"Postponed\",\n DONE: \"Done\",\n WONT_DO: \"Won't do\",\n};\n\n/**\n * `feedbackCategory` — one word per category, keyed by the enum value. `CRASH` is never\n * pickable, but it is shown: on the badge, in the filter, in a row's detail.\n *\n * de-CH canon: CRASH \"Absturz\" · BUG \"Fehler\" · IDEA \"Idee\" · QUESTION \"Frage\" ·\n * OTHER \"Sonstiges\".\n */\nexport type FeedbackCategoryLabels = Record<FeedbackCategory, string>;\n\nexport const DEFAULT_FEEDBACK_CATEGORY_LABELS: FeedbackCategoryLabels = {\n CRASH: \"Crash\",\n BUG: \"Bug\",\n IDEA: \"Idea\",\n QUESTION: \"Question\",\n OTHER: \"Other\",\n};\n\n/**\n * `feedbackToast` — what the submit dialog, a failed save and a status change say in a\n * toast (§4.2, §4.4, §4.5 of the contract). A server's `detail` sentence, when it sent\n * one, wins over `submitFailed` and `updateFailed` (§3.7); that choice is the caller's.\n */\nexport interface FeedbackToastLabels {\n /** A report was filed. de-CH: \"Danke für Ihr Feedback!\" */\n submitted: string;\n /** `POST /feedback` failed with no `detail` of its own.\n * de-CH: \"Feedback konnte nicht gesendet werden\" */\n submitFailed: string;\n /** A picked file is not an image, a PDF or a text file (§3.5).\n * de-CH: \"Nur Bilder, PDF- oder Textdateien sind erlaubt\" */\n attachmentUnsupported: string;\n /** A picked file is over the 10 MB the contract allows (§3.5).\n * de-CH: \"Datei ist grösser als 10 MB\" */\n attachmentTooLarge: string;\n /** More files were picked than fit; `count` is how many fit.\n * de-CH: \"Es passen nur {{count}} Anhänge – die übrigen wurden weggelassen.\" */\n attachmentTooMany: (count: number) => string;\n /** The screenshot capture failed (or its optional peer is not installed).\n * de-CH: \"Screenshot konnte nicht aufgenommen werden\" */\n captureFailed: string;\n /** A `PATCH /feedback/{id}` failed with no `detail` of its own (§4.4).\n * de-CH: \"Änderung konnte nicht gespeichert werden.\" */\n updateFailed: string;\n /** A status change landed — the 8 s Undo toast (§4.5). `status` is the NEW status's\n * label, `title` the report's title. de-CH: \"Auf „{{status}}“ gesetzt: {{title}}\" */\n statusChanged: (status: string, title: string) => string;\n /** That toast's action. de-CH: \"Rückgängig\" */\n statusUndo: string;\n /** The Undo landed — `status` is the RESTORED status's label.\n * de-CH: \"Zurück auf „{{status}}“: {{title}}\" */\n statusRestored: (status: string, title: string) => string;\n /** 0.38.1: `POST /feedback/{id}/comments` failed with no `detail` of its own — for\n * `FeedbackCommentThread`'s `onComment`. de-CH: \"Kommentar konnte nicht gesendet\n * werden.\" */\n commentFailed: string;\n /** 0.38.1: `PUT` / `DELETE /feedback/{id}/vote` failed with no `detail` of its own —\n * for `FeedbackVoteButton`'s `onVote` (the button rolls back; the app toasts).\n * de-CH: \"Stimme konnte nicht gespeichert werden.\" */\n voteFailed: string;\n /** 0.38.1: `POST /feedback/{id}/replies` failed with no `detail` of its own — for\n * `FeedbackRowDetail`'s `onReply`. de-CH: \"Antwort konnte nicht gesendet werden.\" */\n replyFailed: string;\n}\n\nexport const DEFAULT_FEEDBACK_TOAST_LABELS: FeedbackToastLabels = {\n submitted: \"Thanks for the feedback!\",\n submitFailed: \"Could not submit feedback\",\n attachmentUnsupported: \"Only images, PDF or text files are allowed\",\n attachmentTooLarge: \"File is larger than 10 MB\",\n attachmentTooMany: (count) =>\n count === 1\n ? \"Only 1 attachment fits — the rest were left out.\"\n : `Only ${count} attachments fit — the rest were left out.`,\n captureFailed: \"Could not capture a screenshot\",\n updateFailed: \"Could not save that change.\",\n statusChanged: (status, title) => `Set to “${status}”: ${title}`,\n statusUndo: \"Undo\",\n statusRestored: (status, title) => `Back to “${status}”: ${title}`,\n commentFailed: \"Could not post the comment.\",\n voteFailed: \"Could not save your vote.\",\n replyFailed: \"Could not send the reply.\",\n};\n\n/** `feedbackStatus` resolved: English, then `<UiKitProvider labels>`, then `labels`. For\n * the places that need a status's name outside the kit's own parts — a filter's options,\n * `feedbackSwipePlan`'s panels, an app's overview tile. */\nexport function useFeedbackStatusLabels(labels?: LabelOverride<FeedbackStatusLabels>): FeedbackStatusLabels {\n return useKitLabels(\"feedbackStatus\", DEFAULT_FEEDBACK_STATUS_LABELS, labels);\n}\n\n/** `feedbackCategory` resolved: English, then the provider, then `labels`. */\nexport function useFeedbackCategoryLabels(labels?: LabelOverride<FeedbackCategoryLabels>): FeedbackCategoryLabels {\n return useKitLabels(\"feedbackCategory\", DEFAULT_FEEDBACK_CATEGORY_LABELS, labels);\n}\n\n/** `feedbackToast` resolved: English, then the provider, then `labels`. */\nexport function useFeedbackToastLabels(labels?: LabelOverride<FeedbackToastLabels>): FeedbackToastLabels {\n return useKitLabels(\"feedbackToast\", DEFAULT_FEEDBACK_TOAST_LABELS, labels);\n}\n"],"mappings":";AAAA,SAAS,oBAAwC;AAgC1C,MAAM,iCAAuD;AAAA,EAClE,MAAM;AAAA,EACN,OAAO;AAAA,EACP,aAAa;AAAA,EACb,eAAe;AAAA,EACf,iBAAiB;AAAA,EACjB,WAAW;AAAA,EACX,MAAM;AAAA,EACN,SAAS;AACX;AAWO,MAAM,mCAA2D;AAAA,EACtE,OAAO;AAAA,EACP,KAAK;AAAA,EACL,MAAM;AAAA,EACN,UAAU;AAAA,EACV,OAAO;AACT;AAiDO,MAAM,gCAAqD;AAAA,EAChE,WAAW;AAAA,EACX,cAAc;AAAA,EACd,uBAAuB;AAAA,EACvB,oBAAoB;AAAA,EACpB,mBAAmB,CAAC,UAClB,UAAU,IACN,0DACA,QAAQ,KAAK;AAAA,EACnB,eAAe;AAAA,EACf,cAAc;AAAA,EACd,eAAe,CAAC,QAAQ,UAAU,gBAAW,MAAM,WAAM,KAAK;AAAA,EAC9D,YAAY;AAAA,EACZ,gBAAgB,CAAC,QAAQ,UAAU,iBAAY,MAAM,WAAM,KAAK;AAAA,EAChE,eAAe;AAAA,EACf,YAAY;AAAA,EACZ,aAAa;AACf;AAKO,SAAS,wBAAwB,QAAoE;AAC1G,SAAO,aAAa,kBAAkB,gCAAgC,MAAM;AAC9E;AAGO,SAAS,0BAA0B,QAAwE;AAChH,SAAO,aAAa,oBAAoB,kCAAkC,MAAM;AAClF;AAGO,SAAS,uBAAuB,QAAkE;AACvG,SAAO,aAAa,iBAAiB,+BAA+B,MAAM;AAC5E;","names":[]}
|
|
@@ -46,5 +46,8 @@ export interface FeedbackPublicLabels {
|
|
|
46
46
|
visibleHint: string;
|
|
47
47
|
/** Why the author cannot flip it now: the report is past its open window (§9.3). */
|
|
48
48
|
visibleLocked: string;
|
|
49
|
+
/** 0.38.1: the pager's "where am I" — `page` counts from 1. The server answers no
|
|
50
|
+
* total, so there is no "of N". */
|
|
51
|
+
page: (page: number) => string;
|
|
49
52
|
}
|
|
50
53
|
export declare const DEFAULT_FEEDBACK_PUBLIC_LABELS: FeedbackPublicLabels;
|
|
@@ -18,7 +18,8 @@ const DEFAULT_FEEDBACK_PUBLIC_LABELS = {
|
|
|
18
18
|
empty: "No reports yet",
|
|
19
19
|
visibleToOthers: "Visible to others",
|
|
20
20
|
visibleHint: "Other users see the title and text, never your screenshots or files.",
|
|
21
|
-
visibleLocked: "Only while the report is still open."
|
|
21
|
+
visibleLocked: "Only while the report is still open.",
|
|
22
|
+
page: (page) => `Page ${page}`
|
|
22
23
|
};
|
|
23
24
|
export {
|
|
24
25
|
DEFAULT_FEEDBACK_PUBLIC_LABELS
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/feedback/feedback-public-labels.ts"],"sourcesContent":["// The English words of the public feedback pages (round 0.38, docs/feedback-harmonization.md\n// §9.15) — `FeedbackPublicList`, `FeedbackPublicDetail` and `FeedbackPublicSwitch` — in a\n// module with no component code: the English catalogue (src/i18n/defaults.ts) imports\n// them without the components, as every `*-labels.ts` (0.34, KS U8).\n\n/**\n * `feedbackPublic` — the words of the pages every user reads (§9.3, §9.9): the list's\n * sort, search and count, the detail's sections and the \"Visible to others\" switch. The\n * table's column names that the inbox already has (#, Date, Category, Subject, Status)\n * stay `feedbackPage`'s, so the two lists cannot disagree; the vote button's words are\n * `feedbackVote`'s, the thread's `feedbackComments`'.\n *\n * de-CH canon (§9.2, §9.5): `title` \"Alle Rückmeldungen\", `anonymous` \"Eine Person\".\n */\nexport interface FeedbackPublicLabels {\n /** The page's heading — keksdose's `/all-feedback`, the menu's \"All feedback\". */\n title: string;\n /** The sort toggle's accessible name. */\n sort: string;\n /** `sort=newest`, the server's default. */\n sortNewest: string;\n /** `sort=votes`: most votes, then newest. */\n sortVotes: string;\n /** `sort=updated`. */\n sortUpdated: string;\n /** The search box's accessible name: it searches titles only (§9.9). */\n search: string;\n /** The Votes column's header. */\n columnVotes: string;\n /** How many reports the server has for this query (`total`). */\n total: (count: number) => string;\n /** A row's visible comments, the chip beside its title. */\n comments: (count: number) => string;\n /** A row's votes, the count on a phone card (the button is in the opened report). */\n votes: (count: number) => string;\n /** A `null` name (§9.5): an incomplete or erased account. */\n anonymous: string;\n /** Who filed it, with the server's first name (or {@link anonymous}). */\n filedBy: (name: string) => string;\n /** The detail's public body section. */\n body: string;\n /** The detail's merged replies and comments (§9.8, \"Order\"). */\n conversation: string;\n /** While the list or the comments load. */\n loading: string;\n /** No public report (yet, or for this search). */\n empty: string;\n /** `FeedbackPublicSwitch`'s label (§9.3, decision 6). */\n visibleToOthers: string;\n /** The line under it: what others see, and what never. */\n visibleHint: string;\n /** Why the author cannot flip it now: the report is past its open window (§9.3). */\n visibleLocked: string;\n}\n\nexport const DEFAULT_FEEDBACK_PUBLIC_LABELS: FeedbackPublicLabels = {\n title: \"All feedback\",\n sort: \"Sort\",\n sortNewest: \"Newest\",\n sortVotes: \"Most wanted\",\n sortUpdated: \"Recently updated\",\n search: \"Search titles\",\n columnVotes: \"Votes\",\n total: (count) => (count === 1 ? \"1 report\" : `${count} reports`),\n comments: (count) => (count === 1 ? \"1 comment\" : `${count} comments`),\n votes: (count) => (count === 1 ? \"1 vote\" : `${count} votes`),\n anonymous: \"A user\",\n filedBy: (name) => `From ${name}`,\n body: \"Description\",\n conversation: \"Answers and comments\",\n loading: \"Loading…\",\n empty: \"No reports yet\",\n visibleToOthers: \"Visible to others\",\n visibleHint: \"Other users see the title and text, never your screenshots or files.\",\n visibleLocked: \"Only while the report is still open.\",\n};\n"],"mappings":";
|
|
1
|
+
{"version":3,"sources":["../../src/feedback/feedback-public-labels.ts"],"sourcesContent":["// The English words of the public feedback pages (round 0.38, docs/feedback-harmonization.md\n// §9.15) — `FeedbackPublicList`, `FeedbackPublicDetail` and `FeedbackPublicSwitch` — in a\n// module with no component code: the English catalogue (src/i18n/defaults.ts) imports\n// them without the components, as every `*-labels.ts` (0.34, KS U8).\n\n/**\n * `feedbackPublic` — the words of the pages every user reads (§9.3, §9.9): the list's\n * sort, search and count, the detail's sections and the \"Visible to others\" switch. The\n * table's column names that the inbox already has (#, Date, Category, Subject, Status)\n * stay `feedbackPage`'s, so the two lists cannot disagree; the vote button's words are\n * `feedbackVote`'s, the thread's `feedbackComments`'.\n *\n * de-CH canon (§9.2, §9.5): `title` \"Alle Rückmeldungen\", `anonymous` \"Eine Person\".\n */\nexport interface FeedbackPublicLabels {\n /** The page's heading — keksdose's `/all-feedback`, the menu's \"All feedback\". */\n title: string;\n /** The sort toggle's accessible name. */\n sort: string;\n /** `sort=newest`, the server's default. */\n sortNewest: string;\n /** `sort=votes`: most votes, then newest. */\n sortVotes: string;\n /** `sort=updated`. */\n sortUpdated: string;\n /** The search box's accessible name: it searches titles only (§9.9). */\n search: string;\n /** The Votes column's header. */\n columnVotes: string;\n /** How many reports the server has for this query (`total`). */\n total: (count: number) => string;\n /** A row's visible comments, the chip beside its title. */\n comments: (count: number) => string;\n /** A row's votes, the count on a phone card (the button is in the opened report). */\n votes: (count: number) => string;\n /** A `null` name (§9.5): an incomplete or erased account. */\n anonymous: string;\n /** Who filed it, with the server's first name (or {@link anonymous}). */\n filedBy: (name: string) => string;\n /** The detail's public body section. */\n body: string;\n /** The detail's merged replies and comments (§9.8, \"Order\"). */\n conversation: string;\n /** While the list or the comments load. */\n loading: string;\n /** No public report (yet, or for this search). */\n empty: string;\n /** `FeedbackPublicSwitch`'s label (§9.3, decision 6). */\n visibleToOthers: string;\n /** The line under it: what others see, and what never. */\n visibleHint: string;\n /** Why the author cannot flip it now: the report is past its open window (§9.3). */\n visibleLocked: string;\n /** 0.38.1: the pager's \"where am I\" — `page` counts from 1. The server answers no\n * total, so there is no \"of N\". */\n page: (page: number) => string;\n}\n\nexport const DEFAULT_FEEDBACK_PUBLIC_LABELS: FeedbackPublicLabels = {\n title: \"All feedback\",\n sort: \"Sort\",\n sortNewest: \"Newest\",\n sortVotes: \"Most wanted\",\n sortUpdated: \"Recently updated\",\n search: \"Search titles\",\n columnVotes: \"Votes\",\n total: (count) => (count === 1 ? \"1 report\" : `${count} reports`),\n comments: (count) => (count === 1 ? \"1 comment\" : `${count} comments`),\n votes: (count) => (count === 1 ? \"1 vote\" : `${count} votes`),\n anonymous: \"A user\",\n filedBy: (name) => `From ${name}`,\n body: \"Description\",\n conversation: \"Answers and comments\",\n loading: \"Loading…\",\n empty: \"No reports yet\",\n visibleToOthers: \"Visible to others\",\n visibleHint: \"Other users see the title and text, never your screenshots or files.\",\n visibleLocked: \"Only while the report is still open.\",\n page: (page) => `Page ${page}`,\n};\n"],"mappings":";AA0DO,MAAM,iCAAuD;AAAA,EAClE,OAAO;AAAA,EACP,MAAM;AAAA,EACN,YAAY;AAAA,EACZ,WAAW;AAAA,EACX,aAAa;AAAA,EACb,QAAQ;AAAA,EACR,aAAa;AAAA,EACb,OAAO,CAAC,UAAW,UAAU,IAAI,aAAa,GAAG,KAAK;AAAA,EACtD,UAAU,CAAC,UAAW,UAAU,IAAI,cAAc,GAAG,KAAK;AAAA,EAC1D,OAAO,CAAC,UAAW,UAAU,IAAI,WAAW,GAAG,KAAK;AAAA,EACpD,WAAW;AAAA,EACX,SAAS,CAAC,SAAS,QAAQ,IAAI;AAAA,EAC/B,MAAM;AAAA,EACN,cAAc;AAAA,EACd,SAAS;AAAA,EACT,OAAO;AAAA,EACP,iBAAiB;AAAA,EACjB,aAAa;AAAA,EACb,eAAe;AAAA,EACf,MAAM,CAAC,SAAS,QAAQ,IAAI;AAC9B;","names":[]}
|
|
@@ -37,6 +37,26 @@ export interface FeedbackPublicListProps<T extends FeedbackPublicListItem = Feed
|
|
|
37
37
|
* keeps it itself. */
|
|
38
38
|
openId?: number | null;
|
|
39
39
|
onOpenChange?: (id: number | null) => void;
|
|
40
|
+
/**
|
|
41
|
+
* 0.38.1: the opened report when it is NOT on this page — a `?row=` link to an older
|
|
42
|
+
* one (Kurvenschmiede). The app fetches it (`GET /feedback/public/{id}`, a list item's
|
|
43
|
+
* fields and more) and passes it with `openId`; while `openId` is its id it is drawn
|
|
44
|
+
* first, above the page's rows, and opened with `renderDetail` (on the phone, in the
|
|
45
|
+
* full-screen dialog). Closed, it is gone. A row the page has already is never drawn
|
|
46
|
+
* twice: the page's own is opened instead.
|
|
47
|
+
*/
|
|
48
|
+
openRow?: T | null;
|
|
49
|
+
/**
|
|
50
|
+
* 0.38.1: the server's page, from 0 — for a pager where `/feedback/public` answers no
|
|
51
|
+
* total (Kurvenschmiede). With {@link onPageChange} a **Previous / Page n / Next**
|
|
52
|
+
* strip stands under the table; Next is off without {@link hasMore}. Left out, no
|
|
53
|
+
* pager: the server's `limit` is all there is.
|
|
54
|
+
*/
|
|
55
|
+
page?: number;
|
|
56
|
+
/** 0.38.1: there is a page after this one (e.g. the page came back full). */
|
|
57
|
+
hasMore?: boolean;
|
|
58
|
+
/** 0.38.1: go to another page (`page ± 1`); the app fetches it. Draws the pager. */
|
|
59
|
+
onPageChange?: (page: number) => void;
|
|
40
60
|
/** A vote COMMITS — see Button's `commit`. */
|
|
41
61
|
commit?: CommitScope;
|
|
42
62
|
/** See {@link FeedbackRenderDate}. Default: the kit's, with the weekday. */
|
|
@@ -66,11 +86,12 @@ export interface FeedbackPublicListProps<T extends FeedbackPublicListItem = Feed
|
|
|
66
86
|
* counts; the vote itself is in the opened report (a button inside the card, which is
|
|
67
87
|
* the row's own button, would be two controls in one).
|
|
68
88
|
*
|
|
69
|
-
* No paging
|
|
70
|
-
*
|
|
71
|
-
*
|
|
89
|
+
* No client paging: the server's `limit` is the page, and `total` says how many there
|
|
90
|
+
* are. For more than one page, `page` / `hasMore` / `onPageChange` draw a previous /
|
|
91
|
+
* next strip that needs no total (0.38.1); `openRow` shows a `?row=` report from
|
|
92
|
+
* another page. Needs a router, as every `DataTable` does.
|
|
72
93
|
*/
|
|
73
|
-
export declare function FeedbackPublicList<T extends FeedbackPublicListItem = FeedbackPublicListItem>({ rows, total, sort, onSortChange, search, onSearchChange, renderDetail, onVote, loading, openId: openIdProp, onOpenChange, commit, renderDate: renderDateProp, labels: labelsProp, pageLabels: pageLabelsProp, className, }: FeedbackPublicListProps<T>): import("react").JSX.Element;
|
|
94
|
+
export declare function FeedbackPublicList<T extends FeedbackPublicListItem = FeedbackPublicListItem>({ rows, total, sort, onSortChange, search, onSearchChange, renderDetail, onVote, loading, openId: openIdProp, onOpenChange, openRow, page: pageIndex, hasMore, onPageChange, commit, renderDate: renderDateProp, labels: labelsProp, pageLabels: pageLabelsProp, className, }: FeedbackPublicListProps<T>): import("react").JSX.Element;
|
|
74
95
|
export interface FeedbackPublicDetailProps {
|
|
75
96
|
/** `GET /feedback/public/{id}`: the public body (attachment URLs already stripped by
|
|
76
97
|
* the server, §9.4) and the replies. */
|
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
import { jsx, jsxs } from "react/jsx-runtime";
|
|
3
3
|
import { useMemo, useState } from "react";
|
|
4
|
-
import { MessageSquare, ThumbsUp } from "lucide-react";
|
|
4
|
+
import { ChevronLeft, ChevronRight, MessageSquare, ThumbsUp } from "lucide-react";
|
|
5
5
|
import { DataTable } from "../components/data-table.js";
|
|
6
|
+
import { DEFAULT_DATA_TABLE_LABELS } from "../components/data-table-labels.js";
|
|
7
|
+
import { IconButton } from "../components/icon-button.js";
|
|
6
8
|
import { SearchField } from "../components/search-field.js";
|
|
7
9
|
import { ToggleGroup } from "../components/toggle-group.js";
|
|
8
10
|
import { useKitLabels } from "../i18n/kit-labels.js";
|
|
@@ -47,6 +49,49 @@ function VoteCount({ count, voted, labels }) {
|
|
|
47
49
|
}
|
|
48
50
|
);
|
|
49
51
|
}
|
|
52
|
+
function PublicPager({
|
|
53
|
+
page,
|
|
54
|
+
hasMore,
|
|
55
|
+
onPageChange,
|
|
56
|
+
labels
|
|
57
|
+
}) {
|
|
58
|
+
const tableLabels = useKitLabels("dataTable", DEFAULT_DATA_TABLE_LABELS);
|
|
59
|
+
const name = labels.page(page + 1);
|
|
60
|
+
return /* @__PURE__ */ jsxs(
|
|
61
|
+
"div",
|
|
62
|
+
{
|
|
63
|
+
role: "group",
|
|
64
|
+
"aria-label": name,
|
|
65
|
+
className: "flex items-center justify-end gap-2 text-sm text-secondary",
|
|
66
|
+
"data-feedback-public-pager": page,
|
|
67
|
+
children: [
|
|
68
|
+
/* @__PURE__ */ jsx(
|
|
69
|
+
IconButton,
|
|
70
|
+
{
|
|
71
|
+
size: "sm",
|
|
72
|
+
variant: "secondary",
|
|
73
|
+
label: tableLabels.prevPage,
|
|
74
|
+
disabled: page <= 0,
|
|
75
|
+
onClick: () => onPageChange(page - 1),
|
|
76
|
+
children: /* @__PURE__ */ jsx(ChevronLeft, { className: "rtl:-scale-x-100" })
|
|
77
|
+
}
|
|
78
|
+
),
|
|
79
|
+
/* @__PURE__ */ jsx("span", { className: "tabular-nums", "aria-hidden": true, children: name }),
|
|
80
|
+
/* @__PURE__ */ jsx(
|
|
81
|
+
IconButton,
|
|
82
|
+
{
|
|
83
|
+
size: "sm",
|
|
84
|
+
variant: "secondary",
|
|
85
|
+
label: tableLabels.nextPage,
|
|
86
|
+
disabled: !hasMore,
|
|
87
|
+
onClick: () => onPageChange(page + 1),
|
|
88
|
+
children: /* @__PURE__ */ jsx(ChevronRight, { className: "rtl:-scale-x-100" })
|
|
89
|
+
}
|
|
90
|
+
)
|
|
91
|
+
]
|
|
92
|
+
}
|
|
93
|
+
);
|
|
94
|
+
}
|
|
50
95
|
function FeedbackPublicList({
|
|
51
96
|
rows,
|
|
52
97
|
total,
|
|
@@ -59,6 +104,10 @@ function FeedbackPublicList({
|
|
|
59
104
|
loading = false,
|
|
60
105
|
openId: openIdProp,
|
|
61
106
|
onOpenChange,
|
|
107
|
+
openRow,
|
|
108
|
+
page: pageIndex = 0,
|
|
109
|
+
hasMore = false,
|
|
110
|
+
onPageChange,
|
|
62
111
|
commit,
|
|
63
112
|
renderDate: renderDateProp,
|
|
64
113
|
labels: labelsProp,
|
|
@@ -75,6 +124,7 @@ function FeedbackPublicList({
|
|
|
75
124
|
if (openIdProp === void 0) setOwnOpenId(id);
|
|
76
125
|
onOpenChange?.(id);
|
|
77
126
|
};
|
|
127
|
+
const shownRows = openRow && openId === openRow.id && !rows.some((row) => row.id === openRow.id) ? [openRow, ...rows] : rows;
|
|
78
128
|
const columns = useMemo(
|
|
79
129
|
() => [
|
|
80
130
|
{
|
|
@@ -158,7 +208,7 @@ function FeedbackPublicList({
|
|
|
158
208
|
/* @__PURE__ */ jsx(
|
|
159
209
|
DataTable,
|
|
160
210
|
{
|
|
161
|
-
rows,
|
|
211
|
+
rows: shownRows,
|
|
162
212
|
columns,
|
|
163
213
|
rowKey: (row) => row.id,
|
|
164
214
|
rowName: (row) => row.title,
|
|
@@ -181,7 +231,8 @@ function FeedbackPublicList({
|
|
|
181
231
|
] })
|
|
182
232
|
] })
|
|
183
233
|
}
|
|
184
|
-
)
|
|
234
|
+
),
|
|
235
|
+
onPageChange && (pageIndex > 0 || hasMore) && /* @__PURE__ */ jsx(PublicPager, { page: pageIndex, hasMore, onPageChange, labels })
|
|
185
236
|
] });
|
|
186
237
|
}
|
|
187
238
|
function FeedbackPublicDetail({
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/feedback/feedback-public-list.tsx"],"sourcesContent":["import { useMemo, useState } from \"react\";\nimport type { ReactNode } from \"react\";\nimport { MessageSquare, ThumbsUp } from \"lucide-react\";\nimport { DataTable, type DataTableColumn } from \"../components/data-table\";\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 { 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/* ── 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 /** 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 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 paging of its own: the server's `limit` is the page, and `total` says how many\n * there are (an app with more than one page adds its pager below). Needs a router, as\n * 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 commit,\n renderDate: renderDateProp,\n labels: labelsProp,\n pageLabels: pageLabelsProp,\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\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 className={cn(\"flex flex-col gap-3\", className)} data-feedback-public-list=\"\" aria-busy={loading || undefined}>\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={rows}\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 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 </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). */}\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 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 { useKitLabels, type LabelOverride } from \"../i18n/kit-labels\";\nimport { cn } from \"../lib/cn\";\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 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 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 className={cn(\"flex flex-col gap-3\", className)} data-feedback-public-list=\"\" aria-busy={loading || undefined}>\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 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). */}\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 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":";AA4CqB,cAajB,YAbiB;AA5CrB,SAAS,SAAS,gBAAgB;AAElC,SAAS,aAAa,cAAc,eAAe,gBAAgB;AACnE,SAAS,iBAAuC;AAChD,SAAS,iCAAiC;AAC1C,SAAS,kBAAkB;AAC3B,SAAS,mBAAmB;AAC5B,SAAS,mBAAmB;AAE5B,SAAS,oBAAwC;AACjD,SAAS,UAAU;AACnB,SAAS,6BAA6B;AACtC,SAAS,uBAAuB,gBAAgB,uBAAuB,eAAe,2BAA2B;AACjH,SAAS,iCAAiC;AAC1C,SAAS,6BAA0D;AACnE,SAAS,sCAAiE;AAQ1E;AAAA,EACE;AAAA,EACA;AAAA,OAGK;AACP,SAAS,0BAA0B;AAM5B,SAAS,wBAAwB,QAAoE;AAC1G,SAAO,aAAa,kBAAkB,gCAAgC,MAAM;AAC9E;AAGA,MAAM,QAAuC,CAAC,UAAU,SAAS,SAAS;AAG1E,SAAS,WAAW,EAAE,OAAO,WAAW,GAAyE;AAC/G,MAAI,CAAC,MAAO,QAAO,oBAAC,UAAK,WAAU,oBAAoB,iCAAsB;AAC7E,SACE,oBAAC,UAAK,UAAU,OAAO,WAAU,6CAC9B,qBAAW,KAAK,GACnB;AAEJ;AAIA,SAAS,aAAa,EAAE,OAAO,OAAO,GAAoD;AACxF,MAAI,CAAC,MAAO,QAAO;AACnB,SACE,qBAAC,UAAK,WAAU,8DAA6D,+BAA6B,OACxG;AAAA,wBAAC,iBAAc,WAAU,YAAW,eAAW,MAAC;AAAA,IAChD,oBAAC,UAAK,eAAW,MAAC,WAAU,gBACzB,iBACH;AAAA,IACA,oBAAC,UAAK,WAAU,WAAW,iBAAO,SAAS,KAAK,GAAE;AAAA,KACpD;AAEJ;AAIA,SAAS,UAAU,EAAE,OAAO,OAAO,OAAO,GAAoE;AAC5G,SACE;AAAA,IAAC;AAAA;AAAA,MACC,WAAW,GAAG,2DAA2D,QAAQ,eAAe,YAAY;AAAA,MAC5G,4BAA0B;AAAA,MAE1B;AAAA,4BAAC,YAAS,WAAU,YAAW,eAAW,MAAC;AAAA,QAC3C,oBAAC,UAAK,eAAW,MAAC,WAAU,gBACzB,iBACH;AAAA,QACA,oBAAC,UAAK,WAAU,WAAW,iBAAO,MAAM,KAAK,GAAE;AAAA;AAAA;AAAA,EACjD;AAEJ;AAKA,SAAS,YAAY;AAAA,EACnB;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACF,GAKG;AACD,QAAM,cAAc,aAAa,aAAa,yBAAyB;AACvE,QAAM,OAAO,OAAO,KAAK,OAAO,CAAC;AACjC,SACE;AAAA,IAAC;AAAA;AAAA,MACC,MAAK;AAAA,MACL,cAAY;AAAA,MACZ,WAAU;AAAA,MACV,8BAA4B;AAAA,MAE5B;AAAA;AAAA,UAAC;AAAA;AAAA,YACC,MAAK;AAAA,YACL,SAAQ;AAAA,YACR,OAAO,YAAY;AAAA,YACnB,UAAU,QAAQ;AAAA,YAClB,SAAS,MAAM,aAAa,OAAO,CAAC;AAAA,YAGpC,8BAAC,eAAY,WAAU,oBAAmB;AAAA;AAAA,QAC5C;AAAA,QACA,oBAAC,UAAK,WAAU,gBAAe,eAAW,MACvC,gBACH;AAAA,QACA;AAAA,UAAC;AAAA;AAAA,YACC,MAAK;AAAA,YACL,SAAQ;AAAA,YACR,OAAO,YAAY;AAAA,YACnB,UAAU,CAAC;AAAA,YACX,SAAS,MAAM,aAAa,OAAO,CAAC;AAAA,YAEpC,8BAAC,gBAAa,WAAU,oBAAmB;AAAA;AAAA,QAC7C;AAAA;AAAA;AAAA,EACF;AAEJ;AAuFO,SAAS,mBAA8E;AAAA,EAC5F;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA,SAAS;AAAA,EACT;AAAA,EACA;AAAA,EACA;AAAA,EACA,UAAU;AAAA,EACV,QAAQ;AAAA,EACR;AAAA,EACA;AAAA,EACA,MAAM,YAAY;AAAA,EAClB,UAAU;AAAA,EACV;AAAA,EACA;AAAA,EACA,YAAY;AAAA,EACZ,QAAQ;AAAA,EACR,YAAY;AAAA,EACZ;AACF,GAA+B;AAC7B,QAAM,SAAS,wBAAwB,UAAU;AACjD,QAAM,OAAO,sBAAsB,cAAc;AACjD,QAAM,iBAAiB,0BAA0B;AACjD,QAAM,aAAa,sBAAsB,cAAc;AACvD,QAAM,CAAC,WAAW,YAAY,IAAI,SAAwB,IAAI;AAC9D,QAAM,SAAS,eAAe,SAAY,aAAa;AACvD,QAAM,UAAU,CAAC,OAAsB;AACrC,QAAI,eAAe,OAAW,cAAa,EAAE;AAC7C,mBAAe,EAAE;AAAA,EACnB;AAGA,QAAM,YACJ,WAAW,WAAW,QAAQ,MAAM,CAAC,KAAK,KAAK,CAAC,QAAQ,IAAI,OAAO,QAAQ,EAAE,IAAI,CAAC,SAAS,GAAG,IAAI,IAAI;AAExG,QAAM,UAAU;AAAA,IACd,MAAM;AAAA,MACJ;AAAA,QACE,KAAK;AAAA,QACL,QAAQ,KAAK;AAAA,QACb,MAAM,CAAC,QAAQ,oBAAC,UAAK,WAAU,gCAAgC,cAAI,IAAG;AAAA,QACtE,cAAc;AAAA,MAChB;AAAA,MACA;AAAA,QACE,KAAK;AAAA,QACL,QAAQ,KAAK;AAAA,QACb,MAAM,CAAC,QAAQ,oBAAC,cAAW,OAAO,IAAI,YAAY,YAAwB;AAAA,MAC5E;AAAA,MACA;AAAA,QACE,KAAK;AAAA,QACL,QAAQ,KAAK;AAAA,QACb,MAAM,CAAC,QAAQ,oBAAC,yBAAsB,UAAU,IAAI,UAAU,OAAO,eAAe,IAAI,QAAQ,GAAG;AAAA,MACrG;AAAA,MACA;AAAA,QACE,KAAK;AAAA,QACL,QAAQ,KAAK;AAAA,QACb,MAAM,CAAC,QACL,qBAAC,UAAK,WAAU,uEACd;AAAA,8BAAC,UAAK,WAAU,4CAA4C,cAAI,OAAM;AAAA,UACtE,oBAAC,gBAAa,OAAO,IAAI,eAAe,QAAgB;AAAA,WAC1D;AAAA,QAEF,eAAe;AAAA,MACjB;AAAA,MACA;AAAA,QACE,KAAK;AAAA,QACL,QAAQ,OAAO;AAAA,QACf,WAAW;AAAA;AAAA,QAEX,WAAW;AAAA,QACX,MAAM,CAAC,QACL;AAAA,UAAC;AAAA;AAAA,YACC,OAAO,IAAI;AAAA,YACX,OAAO,IAAI;AAAA,YACX,KAAK,IAAI;AAAA,YACT,QAAQ,CAAC,SAAS,OAAO,KAAK,IAAI;AAAA,YAClC;AAAA,YACA,MAAK;AAAA;AAAA,QACP;AAAA,MAEJ;AAAA,MACA;AAAA,QACE,KAAK;AAAA,QACL,QAAQ,KAAK;AAAA,QACb,WAAW;AAAA,QACX,MAAM,CAAC,QAAQ,oBAAC,uBAAoB,QAAQ,IAAI,QAAQ;AAAA,MAC1D;AAAA,IACF;AAAA,IACA,CAAC,MAAM,QAAQ,gBAAgB,YAAY,QAAQ,MAAM;AAAA,EAC3D;AAEA,SACE,qBAAC,SAAI,WAAW,GAAG,uBAAuB,SAAS,GAAG,6BAA0B,IAAG,aAAW,WAAW,QACvG;AAAA,yBAAC,SAAI,WAAU,qCACb;AAAA;AAAA,QAAC;AAAA;AAAA,UACC,cAAY,OAAO;AAAA,UACnB,OAAO;AAAA,UACP,UAAU,CAAC,UAAU,aAAa,KAAK;AAAA,UACvC,SAAS,MAAM,IAAI,CAAC,WAAW;AAAA,YAC7B;AAAA,YACA,OAAO,UAAU,WAAW,OAAO,aAAa,UAAU,UAAU,OAAO,YAAY,OAAO;AAAA,UAChG,EAAE;AAAA;AAAA,MACJ;AAAA,MACC,kBACC;AAAA,QAAC;AAAA;AAAA,UACC,cAAY,OAAO;AAAA,UACnB,aAAa,OAAO;AAAA,UACpB,OAAO;AAAA,UACP,UAAU;AAAA,UACV,WAAU;AAAA;AAAA,MACZ;AAAA,MAED,UAAU,UACT,oBAAC,UAAK,WAAU,8BAA6B,8BAA4B,OACtE,iBAAO,MAAM,KAAK,GACrB;AAAA,OAEJ;AAAA,IACA;AAAA,MAAC;AAAA;AAAA,QACC,MAAM;AAAA,QACN;AAAA,QACA,QAAQ,CAAC,QAAQ,IAAI;AAAA,QACrB,SAAS,CAAC,QAAQ,IAAI;AAAA,QACtB,YAAY,CAAC,QAAQ,QAAQ,WAAW,IAAI,KAAK,OAAO,IAAI,EAAE;AAAA,QAC9D,YAAY,CAAC,QAAQ,IAAI,OAAO;AAAA,QAChC,aAAa,CAAC,QAAQ,aAAa,GAAG;AAAA,QACtC,sBAAoB;AAAA,QACpB,WAAW;AAAA,QACX,OAAO,UAAU,OAAO,UAAU,OAAO;AAAA,QACzC,YAAY,CAAC,QACX,qBAAC,SAAI,WAAU,qBACb;AAAA,+BAAC,SAAI,WAAU,0CACb;AAAA,gCAAC,UAAK,WAAU,uDAAuD,cAAI,OAAM;AAAA,YACjF,oBAAC,uBAAoB,QAAQ,IAAI,QAAQ,WAAU,YAAW;AAAA,aAChE;AAAA,UACA,qBAAC,SAAI,WAAU,0EACb;AAAA,gCAAC,yBAAsB,UAAU,IAAI,UAAU,SAAO,MAAC,OAAO,eAAe,IAAI,QAAQ,GAAG;AAAA,YAC5F,oBAAC,cAAW,OAAO,IAAI,YAAY,YAAwB;AAAA,YAC3D,oBAAC,gBAAa,OAAO,IAAI,eAAe,QAAgB;AAAA,YAGxD,oBAAC,aAAU,OAAO,IAAI,OAAO,OAAO,IAAI,SAAS,IAAI,MAAM,QAAgB;AAAA,aAC7E;AAAA,WACF;AAAA;AAAA,IAEJ;AAAA,IACC,iBAAiB,YAAY,KAAK,YACjC,oBAAC,eAAY,MAAM,WAAW,SAAkB,cAA4B,QAAgB;AAAA,KAEhG;AAEJ;AAiDO,SAAS,qBAAqB;AAAA,EACnC;AAAA,EACA;AAAA,EACA;AAAA,EACA,UAAU;AAAA,EACV;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA,YAAY;AAAA,EACZ;AAAA,EACA,QAAQ;AAAA,EACR;AAAA,EACA;AACF,GAA8B;AAC5B,QAAM,SAAS,wBAAwB,UAAU;AACjD,QAAM,aAAa,sBAAsB,cAAc;AACvD,SACE,qBAAC,kBAAe,WACd;AAAA,yBAAC,SAAI,WAAU,+CAA8C,+BAA6B,IAAI,IAC5F;AAAA,0BAAC,UAAK,WAAU,0BAAyB,wBAAqB,IAC3D,iBAAO,QAAQ,IAAI,eAAe,OAAO,SAAS,GACrD;AAAA,MACA,oBAAC,cAAW,OAAO,IAAI,YAAY,YAAwB;AAAA,MAC3D;AAAA,QAAC;AAAA;AAAA,UACC,OAAO,IAAI;AAAA,UACX,OAAO,IAAI;AAAA,UACX,KAAK,IAAI;AAAA,UACT;AAAA,UACA,UAAU;AAAA,UACV;AAAA,UACA,WAAU;AAAA;AAAA,MACZ;AAAA,OACF;AAAA,IACA,oBAAC,yBAAsB,OAAO,OAAO,MAEnC,8BAAC,iBAAe,cAAI,MAAK,GAC3B;AAAA,IACA,oBAAC,yBAAsB,OAAO,OAAO,cACnC;AAAA,MAAC;AAAA;AAAA,QACC,SAAS,IAAI;AAAA,QACb,UAAU,YAAY,CAAC;AAAA,QACvB,SAAS,aAAa;AAAA,QACtB;AAAA,QACA;AAAA,QACA;AAAA,QACA;AAAA,QACA;AAAA,QACA;AAAA,QACA;AAAA,QACA,QAAQ;AAAA;AAAA,IACV,GACF;AAAA,KACF;AAEJ;","names":[]}
|
|
@@ -5,7 +5,7 @@ import { useKitLabels } from "../i18n/kit-labels.js";
|
|
|
5
5
|
import { FEEDBACK_AUTHOR_EDITABLE_STATUSES } from "./feedback-record.js";
|
|
6
6
|
import { DEFAULT_FEEDBACK_PUBLIC_LABELS } from "./feedback-public-labels.js";
|
|
7
7
|
function isAuthorOf(row, viewerId) {
|
|
8
|
-
if (row.mine
|
|
8
|
+
if (row.mine != null) return row.mine;
|
|
9
9
|
return row.user_id !== null && viewerId !== void 0 && viewerId !== null && row.user_id === viewerId;
|
|
10
10
|
}
|
|
11
11
|
function feedbackPublicSwitchAllowed(row, { isAdmin, viewerId }) {
|
|
@@ -25,7 +25,7 @@ function FeedbackPublicSwitch({
|
|
|
25
25
|
className
|
|
26
26
|
}) {
|
|
27
27
|
const labels = useKitLabels("feedbackPublic", DEFAULT_FEEDBACK_PUBLIC_LABELS, labelsProp);
|
|
28
|
-
if (row.category === "CRASH" || row.public
|
|
28
|
+
if (row.category === "CRASH" || row.public == null) return null;
|
|
29
29
|
if (!isAdmin && !isAuthorOf(row, viewerId)) return null;
|
|
30
30
|
const allowed = feedbackPublicSwitchAllowed(row, { isAdmin, viewerId });
|
|
31
31
|
return /* @__PURE__ */ jsx(
|
|
@@ -34,7 +34,7 @@ function FeedbackPublicSwitch({
|
|
|
34
34
|
className,
|
|
35
35
|
label: labels.visibleToOthers,
|
|
36
36
|
description: labels.visibleHint,
|
|
37
|
-
checked: row.public,
|
|
37
|
+
checked: row.public === true,
|
|
38
38
|
disabled: pending,
|
|
39
39
|
disabledReason: allowed ? void 0 : labels.visibleLocked,
|
|
40
40
|
commit,
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/feedback/feedback-public-switch.tsx"],"sourcesContent":["import { Switch } from \"../components/switch\";\nimport type { CommitScope } from \"../components/write-lock\";\nimport { useKitLabels, type LabelOverride } from \"../i18n/kit-labels\";\nimport { FEEDBACK_AUTHOR_EDITABLE_STATUSES, type FeedbackRecord } from \"./feedback-record\";\nimport { DEFAULT_FEEDBACK_PUBLIC_LABELS, type FeedbackPublicLabels } from \"./feedback-public-labels\";\n\n/** What the switch reads off a report: {@link FeedbackRecord}'s fields (§9.3). */\nexport type FeedbackPublicSwitchRow = Pick<\n FeedbackRecord,\n \"user_id\" | \"category\" | \"status\" | \"public\" | \"filed_private\" | \"mine\"\n>;\n\n/** Who is looking: the app's admin check, and the signed-in user's id. */\nexport interface FeedbackPublicSwitchViewer {\n isAdmin: boolean;\n viewerId?: number | null;\n}\n\n/** The viewer filed the row — `mine` when the server sent it, else the ids (never for an\n * erased author, `user_id: null`). */\nfunction isAuthorOf(row: FeedbackPublicSwitchRow, viewerId: number | null | undefined): boolean {\n if (row.mine
|
|
1
|
+
{"version":3,"sources":["../../src/feedback/feedback-public-switch.tsx"],"sourcesContent":["import { Switch } from \"../components/switch\";\nimport type { CommitScope } from \"../components/write-lock\";\nimport { useKitLabels, type LabelOverride } from \"../i18n/kit-labels\";\nimport { FEEDBACK_AUTHOR_EDITABLE_STATUSES, type FeedbackRecord } from \"./feedback-record\";\nimport { DEFAULT_FEEDBACK_PUBLIC_LABELS, type FeedbackPublicLabels } from \"./feedback-public-labels\";\n\n/** What the switch reads off a report: {@link FeedbackRecord}'s fields (§9.3). */\nexport type FeedbackPublicSwitchRow = Pick<\n FeedbackRecord,\n \"user_id\" | \"category\" | \"status\" | \"public\" | \"filed_private\" | \"mine\"\n>;\n\n/** Who is looking: the app's admin check, and the signed-in user's id. */\nexport interface FeedbackPublicSwitchViewer {\n isAdmin: boolean;\n viewerId?: number | null;\n}\n\n/** The viewer filed the row — `mine` when the server sent it, else the ids (never for an\n * erased author, `user_id: null`). */\nfunction isAuthorOf(row: FeedbackPublicSwitchRow, viewerId: number | null | undefined): boolean {\n if (row.mine != null) return row.mine;\n return row.user_id !== null && viewerId !== undefined && viewerId !== null && row.user_id === viewerId;\n}\n\n/**\n * Whether the viewer may flip a report's `public` flag — the server's rule (§9.3), so the\n * switch never offers what the `PATCH {public}` would refuse with a 403:\n *\n * - never on a CRASH (decision 9: a crash is private whatever its flag);\n * - an admin, at any status;\n * - the author while the status is OPEN, READY or IN_PROGRESS\n * (`FEEDBACK_AUTHOR_EDITABLE_STATUSES`), and on a `filed_private` report — one the\n * 0.38 migration made public — at any status: that is the opt-out the notice offers.\n */\nexport function feedbackPublicSwitchAllowed(\n row: FeedbackPublicSwitchRow,\n { isAdmin, viewerId }: FeedbackPublicSwitchViewer,\n): boolean {\n if (row.category === \"CRASH\") return false;\n if (isAdmin) return true;\n if (!isAuthorOf(row, viewerId)) return false;\n return row.filed_private === true || FEEDBACK_AUTHOR_EDITABLE_STATUSES.includes(row.status);\n}\n\nexport interface FeedbackPublicSwitchProps extends FeedbackPublicSwitchViewer {\n row: FeedbackPublicSwitchRow;\n /** `PATCH /feedback/{id} {\"public\": next}` — the app's mutation. */\n onChange: (next: boolean) => void;\n /** The PATCH is in flight: the switch holds still. */\n pending?: boolean;\n /** Flipping it SAVES: pass `true` to put it under the page's `WriteLockProvider`\n * (Switch's `commit`). Off by default, as on the inbox's status buttons. */\n commit?: CommitScope;\n /** Over the `feedbackPublic` namespace (`visibleToOthers`, `visibleHint`,\n * `visibleLocked`). */\n labels?: LabelOverride<FeedbackPublicLabels>;\n className?: string;\n}\n\n/**\n * **Visible to others** — a report's `public` flag (round 0.38, docs/feedback-harmonization.md\n * §9.3, decision 6), for the row detail (`FeedbackRowDetail`'s `onPublicChange`) or an\n * app's own page.\n *\n * - Drawn for the admin and the author only: nobody else is sent `public` (§9.10). Not\n * drawn on a CRASH, and not when the row carries no `public` at all (a server before\n * 0.38, or an app without public parts — kastlan).\n * - The author past the open window sees it **locked with the reason** (\"Only while the\n * report is open\"), focusable, so the reason reaches a keyboard; a `filed_private`\n * report is never locked for its author ({@link feedbackPublicSwitchAllowed}).\n * - The line under it says what \"visible\" means: the title and the text, never the\n * screenshots or files (decision 1).\n */\nexport function FeedbackPublicSwitch({\n row,\n isAdmin,\n viewerId,\n onChange,\n pending = false,\n commit,\n labels: labelsProp,\n className,\n}: FeedbackPublicSwitchProps) {\n const labels = useKitLabels(\"feedbackPublic\", DEFAULT_FEEDBACK_PUBLIC_LABELS, labelsProp);\n if (row.category === \"CRASH\" || row.public == null) return null;\n if (!isAdmin && !isAuthorOf(row, viewerId)) return null;\n const allowed = feedbackPublicSwitchAllowed(row, { isAdmin, viewerId });\n return (\n <Switch\n className={className}\n label={labels.visibleToOthers}\n description={labels.visibleHint}\n checked={row.public === true}\n disabled={pending}\n disabledReason={allowed ? undefined : labels.visibleLocked}\n commit={commit}\n onCheckedChange={(next) => onChange(next)}\n data-feedback-public={row.public ? \"on\" : \"off\"}\n />\n );\n}\n"],"mappings":";AAyFI;AAzFJ,SAAS,cAAc;AAEvB,SAAS,oBAAwC;AACjD,SAAS,yCAA8D;AACvE,SAAS,sCAAiE;AAgB1E,SAAS,WAAW,KAA8B,UAA8C;AAC9F,MAAI,IAAI,QAAQ,KAAM,QAAO,IAAI;AACjC,SAAO,IAAI,YAAY,QAAQ,aAAa,UAAa,aAAa,QAAQ,IAAI,YAAY;AAChG;AAYO,SAAS,4BACd,KACA,EAAE,SAAS,SAAS,GACX;AACT,MAAI,IAAI,aAAa,QAAS,QAAO;AACrC,MAAI,QAAS,QAAO;AACpB,MAAI,CAAC,WAAW,KAAK,QAAQ,EAAG,QAAO;AACvC,SAAO,IAAI,kBAAkB,QAAQ,kCAAkC,SAAS,IAAI,MAAM;AAC5F;AA+BO,SAAS,qBAAqB;AAAA,EACnC;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA,UAAU;AAAA,EACV;AAAA,EACA,QAAQ;AAAA,EACR;AACF,GAA8B;AAC5B,QAAM,SAAS,aAAa,kBAAkB,gCAAgC,UAAU;AACxF,MAAI,IAAI,aAAa,WAAW,IAAI,UAAU,KAAM,QAAO;AAC3D,MAAI,CAAC,WAAW,CAAC,WAAW,KAAK,QAAQ,EAAG,QAAO;AACnD,QAAM,UAAU,4BAA4B,KAAK,EAAE,SAAS,SAAS,CAAC;AACtE,SACE;AAAA,IAAC;AAAA;AAAA,MACC;AAAA,MACA,OAAO,OAAO;AAAA,MACd,aAAa,OAAO;AAAA,MACpB,SAAS,IAAI,WAAW;AAAA,MACxB,UAAU;AAAA,MACV,gBAAgB,UAAU,SAAY,OAAO;AAAA,MAC7C;AAAA,MACA,iBAAiB,CAAC,SAAS,SAAS,IAAI;AAAA,MACxC,wBAAsB,IAAI,SAAS,OAAO;AAAA;AAAA,EAC5C;AAEJ;","names":[]}
|