@eifi1/ui-kit 0.39.1 → 0.40.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (143) hide show
  1. package/README.md +8 -7
  2. package/dist/billing/billing-overview.d.ts +10 -0
  3. package/dist/components/autocomplete.js +5 -1
  4. package/dist/components/autocomplete.js.map +1 -1
  5. package/dist/components/combobox-core.d.ts +14 -1
  6. package/dist/components/combobox-core.js +4 -0
  7. package/dist/components/combobox-core.js.map +1 -1
  8. package/dist/components/combobox.js +9 -3
  9. package/dist/components/combobox.js.map +1 -1
  10. package/dist/components/data-table-labels.d.ts +5 -0
  11. package/dist/components/data-table-labels.js +2 -0
  12. package/dist/components/data-table-labels.js.map +1 -1
  13. package/dist/components/data-table-mobile.d.ts +36 -1
  14. package/dist/components/data-table-mobile.js +162 -15
  15. package/dist/components/data-table-mobile.js.map +1 -1
  16. package/dist/components/data-table.d.ts +39 -12
  17. package/dist/components/data-table.js +74 -13
  18. package/dist/components/data-table.js.map +1 -1
  19. package/dist/components/inline-entity-combobox.js +10 -3
  20. package/dist/components/inline-entity-combobox.js.map +1 -1
  21. package/dist/components/picker-sheet.d.ts +18 -1
  22. package/dist/components/picker-sheet.js +20 -2
  23. package/dist/components/picker-sheet.js.map +1 -1
  24. package/dist/components/tabs.d.ts +26 -2
  25. package/dist/components/tabs.js +41 -2
  26. package/dist/components/tabs.js.map +1 -1
  27. package/dist/components/translation-review-parts.d.ts +2 -1
  28. package/dist/components/translation-review-parts.js +1 -1
  29. package/dist/components/translation-review-parts.js.map +1 -1
  30. package/dist/components/ui-labels.js +3 -1
  31. package/dist/components/ui-labels.js.map +1 -1
  32. package/dist/eslint/index.js +1 -1
  33. package/dist/feedback/feedback-comment-thread.d.ts +8 -1
  34. package/dist/feedback/feedback-comment-thread.js +26 -1
  35. package/dist/feedback/feedback-comment-thread.js.map +1 -1
  36. package/dist/feedback/feedback-labels.d.ts +2 -1
  37. package/dist/feedback/feedback-labels.js +1 -0
  38. package/dist/feedback/feedback-labels.js.map +1 -1
  39. package/dist/feedback/feedback-menu-labels.js +3 -1
  40. package/dist/feedback/feedback-menu-labels.js.map +1 -1
  41. package/dist/feedback/feedback-menu.d.ts +31 -24
  42. package/dist/feedback/feedback-menu.js +16 -7
  43. package/dist/feedback/feedback-menu.js.map +1 -1
  44. package/dist/feedback/feedback-public-labels.d.ts +3 -2
  45. package/dist/feedback/feedback-public-labels.js +1 -1
  46. package/dist/feedback/feedback-public-labels.js.map +1 -1
  47. package/dist/feedback/feedback-public-list.js +1 -0
  48. package/dist/feedback/feedback-public-list.js.map +1 -1
  49. package/dist/feedback/feedback-public-types.d.ts +16 -3
  50. package/dist/feedback/feedback-record.d.ts +20 -3
  51. package/dist/feedback/feedback-record.js +15 -0
  52. package/dist/feedback/feedback-record.js.map +1 -1
  53. package/dist/feedback/feedback-replies.d.ts +13 -2
  54. package/dist/feedback/feedback-replies.js +67 -33
  55. package/dist/feedback/feedback-replies.js.map +1 -1
  56. package/dist/feedback/feedback-rework-entry-chip.d.ts +9 -0
  57. package/dist/feedback/feedback-rework-entry-chip.js +13 -0
  58. package/dist/feedback/feedback-rework-entry-chip.js.map +1 -0
  59. package/dist/feedback/feedback-row-detail.d.ts +31 -8
  60. package/dist/feedback/feedback-row-detail.js +31 -7
  61. package/dist/feedback/feedback-row-detail.js.map +1 -1
  62. package/dist/feedback/feedback-table-labels.js +1 -1
  63. package/dist/feedback/feedback-table-labels.js.map +1 -1
  64. package/dist/feedback/feedback-table.d.ts +5 -2
  65. package/dist/feedback/feedback-table.js +2 -2
  66. package/dist/feedback/feedback-table.js.map +1 -1
  67. package/dist/feedback/feedback-thread.js +3 -1
  68. package/dist/feedback/feedback-thread.js.map +1 -1
  69. package/dist/feedback/feedback-vocabulary.d.ts +9 -6
  70. package/dist/feedback/feedback-vocabulary.js +16 -1
  71. package/dist/feedback/feedback-vocabulary.js.map +1 -1
  72. package/dist/hooks/use-long-press.d.ts +30 -0
  73. package/dist/hooks/use-long-press.js +77 -0
  74. package/dist/hooks/use-long-press.js.map +1 -0
  75. package/dist/hooks/use-overlay-history.js +37 -3
  76. package/dist/hooks/use-overlay-history.js.map +1 -1
  77. package/dist/i18n/german.js +13 -5
  78. package/dist/i18n/german.js.map +1 -1
  79. package/dist/i18n/kit-labels.d.ts +3 -0
  80. package/dist/i18n/kit-labels.js.map +1 -1
  81. package/dist/i18n/locales/en.js +12 -5
  82. package/dist/i18n/locales/en.js.map +2 -2
  83. package/dist/i18n/locales/es.js +13 -5
  84. package/dist/i18n/locales/es.js.map +1 -1
  85. package/dist/i18n/locales/fr.js +13 -5
  86. package/dist/i18n/locales/fr.js.map +1 -1
  87. package/dist/i18n/locales/hu.js +13 -5
  88. package/dist/i18n/locales/hu.js.map +1 -1
  89. package/dist/i18n/locales/it.js +13 -5
  90. package/dist/i18n/locales/it.js.map +1 -1
  91. package/dist/i18n/locales/zh.js +13 -5
  92. package/dist/i18n/locales/zh.js.map +1 -1
  93. package/dist/i18n/shared-labels.js +1 -1
  94. package/dist/i18n/shared-labels.js.map +1 -1
  95. package/dist/shell/app-shell.js +9 -0
  96. package/dist/shell/app-shell.js.map +1 -1
  97. package/dist/shell/fill-height.d.ts +38 -0
  98. package/dist/shell/fill-height.js +12 -0
  99. package/dist/shell/fill-height.js.map +1 -0
  100. package/dist/shell.d.ts +2 -0
  101. package/dist/shell.js +3 -0
  102. package/dist/shell.js.map +1 -1
  103. package/package.json +1 -1
  104. package/src/billing/billing-overview.ts +10 -0
  105. package/src/components/autocomplete.tsx +5 -1
  106. package/src/components/combobox-core.tsx +20 -1
  107. package/src/components/combobox.tsx +9 -3
  108. package/src/components/data-table-labels.ts +7 -0
  109. package/src/components/data-table-mobile.tsx +193 -9
  110. package/src/components/data-table.tsx +141 -24
  111. package/src/components/inline-entity-combobox.tsx +12 -4
  112. package/src/components/picker-sheet.tsx +38 -1
  113. package/src/components/tabs.tsx +73 -3
  114. package/src/components/translation-review-parts.tsx +3 -2
  115. package/src/components/ui-labels.ts +2 -0
  116. package/src/feedback/feedback-comment-thread.tsx +39 -1
  117. package/src/feedback/feedback-labels.ts +3 -1
  118. package/src/feedback/feedback-menu-labels.ts +2 -0
  119. package/src/feedback/feedback-menu.tsx +48 -30
  120. package/src/feedback/feedback-public-labels.ts +4 -3
  121. package/src/feedback/feedback-public-list.tsx +4 -1
  122. package/src/feedback/feedback-public-types.ts +17 -3
  123. package/src/feedback/feedback-record.ts +34 -3
  124. package/src/feedback/feedback-replies.tsx +100 -31
  125. package/src/feedback/feedback-rework-entry-chip.tsx +18 -0
  126. package/src/feedback/feedback-row-detail.tsx +65 -12
  127. package/src/feedback/feedback-table-labels.ts +1 -1
  128. package/src/feedback/feedback-table.tsx +7 -4
  129. package/src/feedback/feedback-thread.tsx +5 -1
  130. package/src/feedback/feedback-vocabulary.ts +27 -6
  131. package/src/hooks/use-long-press.ts +126 -0
  132. package/src/hooks/use-overlay-history.ts +74 -3
  133. package/src/i18n/german.ts +10 -2
  134. package/src/i18n/kit-labels.tsx +3 -0
  135. package/src/i18n/locales/es.ts +10 -2
  136. package/src/i18n/locales/fr.ts +10 -2
  137. package/src/i18n/locales/hu.ts +10 -2
  138. package/src/i18n/locales/it.ts +10 -2
  139. package/src/i18n/locales/zh.ts +10 -2
  140. package/src/i18n/shared-labels.ts +1 -1
  141. package/src/shell/app-shell.tsx +9 -0
  142. package/src/shell/fill-height.tsx +45 -0
  143. package/src/shell.ts +3 -0
@@ -7,21 +7,26 @@ import { DEFAULT_FEEDBACK_MENU_LABELS } from "./feedback-menu-labels.js";
7
7
  /**
8
8
  * `feedbackMenu` — the top-bar feedback menu's own words (0.27.0). The four category rows
9
9
  * are `feedbackCategory`'s, shared with the badges and the dialog's picker. de-CH canon
10
- * (keksdose `de-CH.json`): `trigger` "Feedback senden", `myFeedback` "Mein Feedback",
11
- * `viewFeedback` "Feedback ansehen".
10
+ * (keksdose `de-CH.json`, §10.6): `trigger` "Feedback senden", `discussion`
11
+ * "Feedback-Diskussion", `myFeedback` "Mein Feedback", `inbox` "Feedback-Eingang".
12
12
  */
13
13
  export interface FeedbackMenuLabels {
14
14
  /** The trigger's accessible name, and the menu's. keksdose `feedback.send`. */
15
15
  trigger: string;
16
- /** The link to the reporter's own reports — everyone but an admin. keksdose
17
- * `feedback.my_title`. */
16
+ /** The link to the viewer's own reports — everyone's, an admin's too since 0.40
17
+ * (§10.2 decision 3). keksdose `feedback.my_title`. */
18
18
  myFeedback: string;
19
- /** The link to the inbox — an admin's one list link, in place of "My feedback".
20
- * keksdose `feedback.view_all`. */
21
- viewFeedback: string;
22
- /** The link to every public report, {@link FeedbackMenuProps.publicHref} (0.38,
23
- * docs/feedback-harmonization.md §9.2 decision 21). Optional, so a complete
24
- * `UiKitLabels` typed before 0.38 still compiles. de-CH "Alle Rückmeldungen". */
19
+ /** 0.40: the link to the public list, {@link FeedbackMenuProps.publicHref} (§10.2
20
+ * decision 3; "All feedback" before). de-CH "Feedback-Diskussion". */
21
+ discussion: string;
22
+ /** 0.40: the admin's link to the triage list, {@link FeedbackMenuProps.inboxHref}
23
+ * ("View feedback" before). de-CH "Feedback-Eingang". */
24
+ inbox: string;
25
+ /** @deprecated 0.40 — use {@link inbox}; removed in 0.41. Unused: the inbox row reads
26
+ * `inbox` (feedback §10.6). */
27
+ viewFeedback?: string;
28
+ /** @deprecated 0.40 — use {@link discussion}; removed in 0.41. Unused: the public row
29
+ * reads `discussion` (feedback §10.6). */
25
30
  allFeedback?: string;
26
31
  }
27
32
  export { DEFAULT_FEEDBACK_MENU_LABELS };
@@ -29,8 +34,8 @@ export interface FeedbackMenuProps {
29
34
  /** A category row was chosen — open the submit dialog on it (`useFeedbackSubmit`'s
30
35
  * `open`). Never called with `CRASH`: there is no row for it. */
31
36
  onFile: (category: FeedbackCategory) => void;
32
- /** The list link is "View feedback" → {@link inboxHref} INSTEAD of "My feedback" (§4.1,
33
- * keksdose `top-bar.tsx:205-214`). The admin check is the app's (kastlan's is
37
+ /** Adds the **Feedback inbox** row → {@link inboxHref} under "My feedback" (0.40, §10.2
38
+ * decision 11; until 0.39 it replaced it). The admin check is the app's (kastlan's is
34
39
  * company-scoped). Default `false`. */
35
40
  isAdmin?: boolean;
36
41
  /**
@@ -41,16 +46,16 @@ export interface FeedbackMenuProps {
41
46
  * `all-feedback`, `my-feedback`, `view-feedback`).
42
47
  */
43
48
  extraEntries?: TopBarMenuEntry[];
44
- /** The user's own reports, linked for everyone but an admin. Default `"/my-feedback"`
45
- * (§2.3). */
49
+ /** The viewer's own reports, linked for everyone (an admin too since 0.40). Default
50
+ * `"/my-feedback"` (§2.3). */
46
51
  myFeedbackHref?: string;
47
52
  /** The admin inbox, linked for an admin. Default `"/feedback"` (§2.3). */
48
53
  inboxHref?: string;
49
54
  /**
50
55
  * The public list (0.38, docs/feedback-harmonization.md §9.2 decision 21): when passed,
51
- * an **All feedback** row (`Users`, key `all-feedback`) above the list link, for
52
- * everyone — the admin too, who reads what the others read there. keksdose:
53
- * `"/all-feedback"`; "My feedback" stays as it is. Leave it out where the app has no
56
+ * a **Feedback discussion** row (`Users`, key `all-feedback`; "All feedback" until 0.40)
57
+ * first of the list links, for everyone — the admin too, who reads what the others read
58
+ * there. keksdose: `"/all-feedback"`. Leave it out where the app has no
54
59
  * public parts (kastlan) or the viewer may not see them (a Kurvenschmiede customer, a
55
60
  * demo — §9.2 decisions 7, 8).
56
61
  */
@@ -73,17 +78,19 @@ export interface FeedbackMenuProps {
73
78
  * the category badges' own glyphs), each calling `onFile(category)`;
74
79
  * 3. a divider, always;
75
80
  * 4. the app's `extraEntries`;
76
- * 5. with `publicHref` (0.38), **All feedback** → the public list, for everyone;
77
- * 6. ONE list link, `Inbox`: an admin's is "View feedback" → `/feedback`, everyone
78
- * else's "My feedback" → `/my-feedback` — keksdose's (`top-bar.tsx:205-214`, feedback
79
- * #331: admins get the management view, everyone else their own submissions).
81
+ * 5. the list links (0.40, §10.2 decision 11), in this order: **Feedback discussion**
82
+ * (`Users`, key `all-feedback`) → `publicHref`, when passed; **My feedback** (`User`,
83
+ * key `my-feedback`) → `/my-feedback`, for everyone, admins included; **Feedback
84
+ * inbox** (`Inbox`, key `view-feedback`) → `/feedback`, for an admin. An admin with
85
+ * public parts sees three, a member two; without them an admin two and anyone else
86
+ * one. Until 0.39 an admin had the inbox in place of their own list (feedback #331);
87
+ * Marcel's kk #419 gave them both.
80
88
  *
81
89
  * WHY A PRESET. Three apps had drawn three menus on the same primitive and they disagreed:
82
90
  * kastlan headed it "New submission", Kurvenschmiede "Send feedback" with a single link
83
91
  * that changed its words by role, keksdose had no heading and one link by role. keksdose
84
- * is the source (§2.4, §7.11): no heading, one link — the inbox for an admin, who reaches
85
- * their own reports there too — and the kit draws it, so the row order, the icons and the
86
- * words cannot drift again. Where the menu sits in the bar stays each app's.
92
+ * is the source (§2.4, §7.11): no heading, the list links by role — and the kit draws
93
+ * it, so the row order, the icons and the words cannot drift again. Where the menu sits in the bar stays each app's.
87
94
  *
88
95
  * `CRASH` has no row and never will: crash reports are filed by the error boundary with
89
96
  * the stack and a fingerprint (keksdose feedback #160), and a hand-filed "crash" would sort
@@ -1,6 +1,6 @@
1
1
  "use client";
2
2
  import { jsx } from "react/jsx-runtime";
3
- import { Inbox, MessageSquare, Users } from "lucide-react";
3
+ import { Inbox, MessageSquare, User, Users } from "lucide-react";
4
4
  import { useKitLabels } from "../i18n/kit-labels.js";
5
5
  import { TopBarActionMenu } from "../shell/topbar-action-menu.js";
6
6
  import { FEEDBACK_CATEGORY_META } from "./feedback-vocabulary.js";
@@ -38,16 +38,25 @@ function FeedbackMenu({
38
38
  key: "all-feedback",
39
39
  to: publicHref,
40
40
  icon: /* @__PURE__ */ jsx(Users, { className: "size-4" }),
41
- label: labels.allFeedback ?? DEFAULT_FEEDBACK_MENU_LABELS.allFeedback
41
+ label: labels.discussion
42
42
  }
43
43
  ] : [],
44
44
  {
45
45
  kind: "link",
46
- key: isAdmin ? "view-feedback" : "my-feedback",
47
- to: isAdmin ? inboxHref : myFeedbackHref,
48
- icon: /* @__PURE__ */ jsx(Inbox, { className: "size-4" }),
49
- label: isAdmin ? labels.viewFeedback : labels.myFeedback
50
- }
46
+ key: "my-feedback",
47
+ to: myFeedbackHref,
48
+ icon: /* @__PURE__ */ jsx(User, { className: "size-4" }),
49
+ label: labels.myFeedback
50
+ },
51
+ ...isAdmin ? [
52
+ {
53
+ kind: "link",
54
+ key: "view-feedback",
55
+ to: inboxHref,
56
+ icon: /* @__PURE__ */ jsx(Inbox, { className: "size-4" }),
57
+ label: labels.inbox
58
+ }
59
+ ] : []
51
60
  ];
52
61
  return /* @__PURE__ */ jsx(
53
62
  TopBarActionMenu,
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/feedback/feedback-menu.tsx"],"sourcesContent":["import { Inbox, MessageSquare, Users } from \"lucide-react\";\nimport type { UserAvatarBadge } from \"../components/user-avatar\";\nimport { useKitLabels, type LabelOverride } from \"../i18n/kit-labels\";\nimport { TopBarActionMenu, type TopBarMenuEntry } from \"../shell/topbar-action-menu\";\nimport type { HoverMenuProps } from \"../components/hover-menu\";\n// The vocabulary module, not the inbox: this menu is in every app's top bar, the inbox\n// is a page (MOD-1).\nimport { FEEDBACK_CATEGORY_META, type FeedbackCategory } from \"./feedback-vocabulary\";\nimport { useFeedbackCategoryLabels } from \"./feedback-labels\";\nimport { FEEDBACK_PICKABLE_CATEGORIES } from \"./feedback-record\";\nimport { DEFAULT_FEEDBACK_MENU_LABELS } from \"./feedback-menu-labels\";\n\n/**\n * `feedbackMenu` — the top-bar feedback menu's own words (0.27.0). The four category rows\n * are `feedbackCategory`'s, shared with the badges and the dialog's picker. de-CH canon\n * (keksdose `de-CH.json`): `trigger` \"Feedback senden\", `myFeedback` \"Mein Feedback\",\n * `viewFeedback` \"Feedback ansehen\".\n */\nexport interface FeedbackMenuLabels {\n /** The trigger's accessible name, and the menu's. keksdose `feedback.send`. */\n trigger: string;\n /** The link to the reporter's own reports — everyone but an admin. keksdose\n * `feedback.my_title`. */\n myFeedback: string;\n /** The link to the inbox — an admin's one list link, in place of \"My feedback\".\n * keksdose `feedback.view_all`. */\n viewFeedback: string;\n /** The link to every public report, {@link FeedbackMenuProps.publicHref} (0.38,\n * docs/feedback-harmonization.md §9.2 decision 21). Optional, so a complete\n * `UiKitLabels` typed before 0.38 still compiles. de-CH \"Alle Rückmeldungen\". */\n allFeedback?: string;\n}\n\n// Defined in ./feedback-menu-labels, which holds no component code (0.34, KS U8).\nexport { DEFAULT_FEEDBACK_MENU_LABELS };\n\nexport interface FeedbackMenuProps {\n /** A category row was chosen — open the submit dialog on it (`useFeedbackSubmit`'s\n * `open`). Never called with `CRASH`: there is no row for it. */\n onFile: (category: FeedbackCategory) => void;\n /** The list link is \"View feedback\" → {@link inboxHref} INSTEAD of \"My feedback\" (§4.1,\n * keksdose `top-bar.tsx:205-214`). The admin check is the app's (kastlan's is\n * company-scoped). Default `false`. */\n isAdmin?: boolean;\n /**\n * Rows of the app's own, between the divider and the list link — keksdose's\n * `Sparkles` Help assistant → `/assistant` and `MessagesSquare` Support chat →\n * `/support` with its unread chip. Any {@link TopBarMenuEntry}; keep their `key`s clear\n * of the menu's own (`BUG`, `IDEA`, `QUESTION`, `OTHER`, `feedback-divider`,\n * `all-feedback`, `my-feedback`, `view-feedback`).\n */\n extraEntries?: TopBarMenuEntry[];\n /** The user's own reports, linked for everyone but an admin. Default `\"/my-feedback\"`\n * (§2.3). */\n myFeedbackHref?: string;\n /** The admin inbox, linked for an admin. Default `\"/feedback\"` (§2.3). */\n inboxHref?: string;\n /**\n * The public list (0.38, docs/feedback-harmonization.md §9.2 decision 21): when passed,\n * an **All feedback** row (`Users`, key `all-feedback`) above the list link, for\n * everyone — the admin too, who reads what the others read there. keksdose:\n * `\"/all-feedback\"`; \"My feedback\" stays as it is. Leave it out where the app has no\n * public parts (kastlan) or the viewer may not see them (a Kurvenschmiede customer, a\n * demo — §9.2 decisions 7, 8).\n */\n publicHref?: string;\n /** A dot on the trigger — keksdose's unread support replies (`tone: \"danger\"`). Its\n * `label` joins the trigger's name (\"Send feedback 2 unread\"). The words are the app's. */\n iconBadge?: UserAvatarBadge | null;\n /** Prop > `<UiKitProvider labels={{ feedbackMenu }}>` > English. */\n labels?: LabelOverride<FeedbackMenuLabels>;\n /** Which edge of the trigger the panel lines up with, `start` or `end` in the reading\n * direction (see `HoverMenu`, whose type this is). Default `end`. */\n align?: HoverMenuProps[\"align\"];\n}\n\n/**\n * The top-bar feedback menu, the same in every app (0.27.0, docs/feedback-harmonization.md\n * §4.1) — keksdose `frontend/src/app/top-bar.tsx:156` on the kit's `TopBarActionMenu`:\n *\n * 1. a `MessageSquare` trigger named \"Send feedback\", with an optional `iconBadge`;\n * 2. Bug · Idea · Question · Other (`Bug`, `Lightbulb`, `HelpCircle`, `MoreHorizontal` —\n * the category badges' own glyphs), each calling `onFile(category)`;\n * 3. a divider, always;\n * 4. the app's `extraEntries`;\n * 5. with `publicHref` (0.38), **All feedback** → the public list, for everyone;\n * 6. ONE list link, `Inbox`: an admin's is \"View feedback\" → `/feedback`, everyone\n * else's \"My feedback\" → `/my-feedback` — keksdose's (`top-bar.tsx:205-214`, feedback\n * #331: admins get the management view, everyone else their own submissions).\n *\n * WHY A PRESET. Three apps had drawn three menus on the same primitive and they disagreed:\n * kastlan headed it \"New submission\", Kurvenschmiede \"Send feedback\" with a single link\n * that changed its words by role, keksdose had no heading and one link by role. keksdose\n * is the source (§2.4, §7.11): no heading, one link — the inbox for an admin, who reaches\n * their own reports there too — and the kit draws it, so the row order, the icons and the\n * words cannot drift again. Where the menu sits in the bar stays each app's.\n *\n * `CRASH` has no row and never will: crash reports are filed by the error boundary with\n * the stack and a fingerprint (keksdose feedback #160), and a hand-filed \"crash\" would sort\n * to the top like one with none of what makes it actionable. The server refuses it with a\n * 422 anyway (§3.4).\n *\n * The menu forwards nothing to its trigger, as `TopBarActionMenu` does not: keksdose's\n * tour anchor (`data-tour=\"feedback-menu\"`) stays on a wrapping `<span>`. Needs a router,\n * as every `TopBarActionMenu` with link rows does.\n */\nexport function FeedbackMenu({\n onFile,\n isAdmin = false,\n extraEntries,\n myFeedbackHref = \"/my-feedback\",\n inboxHref = \"/feedback\",\n publicHref,\n iconBadge,\n labels: labelsProp,\n align,\n}: FeedbackMenuProps) {\n const labels = useKitLabels(\"feedbackMenu\", DEFAULT_FEEDBACK_MENU_LABELS, labelsProp);\n const categoryLabels = useFeedbackCategoryLabels();\n const entries: TopBarMenuEntry[] = [\n ...FEEDBACK_PICKABLE_CATEGORIES.map((category): TopBarMenuEntry => {\n const Icon = FEEDBACK_CATEGORY_META[category].icon;\n return {\n key: category,\n icon: <Icon className=\"size-4\" />,\n label: categoryLabels[category],\n onSelect: () => onFile(category),\n };\n }),\n { kind: \"divider\", key: \"feedback-divider\" },\n ...(extraEntries ?? []),\n ...(publicHref\n ? [\n {\n kind: \"link\",\n key: \"all-feedback\",\n to: publicHref,\n icon: <Users className=\"size-4\" />,\n label: labels.allFeedback ?? DEFAULT_FEEDBACK_MENU_LABELS.allFeedback!,\n } satisfies TopBarMenuEntry,\n ]\n : []),\n {\n kind: \"link\",\n key: isAdmin ? \"view-feedback\" : \"my-feedback\",\n to: isAdmin ? inboxHref : myFeedbackHref,\n icon: <Inbox className=\"size-4\" />,\n label: isAdmin ? labels.viewFeedback : labels.myFeedback,\n },\n ];\n return (\n <TopBarActionMenu\n icon={<MessageSquare className=\"size-5\" />}\n iconBadge={iconBadge}\n aria-label={labels.trigger}\n entries={entries}\n align={align}\n />\n );\n}\n"],"mappings":";AA4Hc;AA5Hd,SAAS,OAAO,eAAe,aAAa;AAE5C,SAAS,oBAAwC;AACjD,SAAS,wBAA8C;AAIvD,SAAS,8BAAqD;AAC9D,SAAS,iCAAiC;AAC1C,SAAS,oCAAoC;AAC7C,SAAS,oCAAoC;AAgGtC,SAAS,aAAa;AAAA,EAC3B;AAAA,EACA,UAAU;AAAA,EACV;AAAA,EACA,iBAAiB;AAAA,EACjB,YAAY;AAAA,EACZ;AAAA,EACA;AAAA,EACA,QAAQ;AAAA,EACR;AACF,GAAsB;AACpB,QAAM,SAAS,aAAa,gBAAgB,8BAA8B,UAAU;AACpF,QAAM,iBAAiB,0BAA0B;AACjD,QAAM,UAA6B;AAAA,IACjC,GAAG,6BAA6B,IAAI,CAAC,aAA8B;AACjE,YAAM,OAAO,uBAAuB,QAAQ,EAAE;AAC9C,aAAO;AAAA,QACL,KAAK;AAAA,QACL,MAAM,oBAAC,QAAK,WAAU,UAAS;AAAA,QAC/B,OAAO,eAAe,QAAQ;AAAA,QAC9B,UAAU,MAAM,OAAO,QAAQ;AAAA,MACjC;AAAA,IACF,CAAC;AAAA,IACD,EAAE,MAAM,WAAW,KAAK,mBAAmB;AAAA,IAC3C,GAAI,gBAAgB,CAAC;AAAA,IACrB,GAAI,aACA;AAAA,MACE;AAAA,QACE,MAAM;AAAA,QACN,KAAK;AAAA,QACL,IAAI;AAAA,QACJ,MAAM,oBAAC,SAAM,WAAU,UAAS;AAAA,QAChC,OAAO,OAAO,eAAe,6BAA6B;AAAA,MAC5D;AAAA,IACF,IACA,CAAC;AAAA,IACL;AAAA,MACE,MAAM;AAAA,MACN,KAAK,UAAU,kBAAkB;AAAA,MACjC,IAAI,UAAU,YAAY;AAAA,MAC1B,MAAM,oBAAC,SAAM,WAAU,UAAS;AAAA,MAChC,OAAO,UAAU,OAAO,eAAe,OAAO;AAAA,IAChD;AAAA,EACF;AACA,SACE;AAAA,IAAC;AAAA;AAAA,MACC,MAAM,oBAAC,iBAAc,WAAU,UAAS;AAAA,MACxC;AAAA,MACA,cAAY,OAAO;AAAA,MACnB;AAAA,MACA;AAAA;AAAA,EACF;AAEJ;","names":[]}
1
+ {"version":3,"sources":["../../src/feedback/feedback-menu.tsx"],"sourcesContent":["import { Inbox, MessageSquare, User, Users } from \"lucide-react\";\nimport type { UserAvatarBadge } from \"../components/user-avatar\";\nimport { useKitLabels, type LabelOverride } from \"../i18n/kit-labels\";\nimport { TopBarActionMenu, type TopBarMenuEntry } from \"../shell/topbar-action-menu\";\nimport type { HoverMenuProps } from \"../components/hover-menu\";\n// The vocabulary module, not the inbox: this menu is in every app's top bar, the inbox\n// is a page (MOD-1).\nimport { FEEDBACK_CATEGORY_META, type FeedbackCategory } from \"./feedback-vocabulary\";\nimport { useFeedbackCategoryLabels } from \"./feedback-labels\";\nimport { FEEDBACK_PICKABLE_CATEGORIES } from \"./feedback-record\";\nimport { DEFAULT_FEEDBACK_MENU_LABELS } from \"./feedback-menu-labels\";\n\n/**\n * `feedbackMenu` — the top-bar feedback menu's own words (0.27.0). The four category rows\n * are `feedbackCategory`'s, shared with the badges and the dialog's picker. de-CH canon\n * (keksdose `de-CH.json`, §10.6): `trigger` \"Feedback senden\", `discussion`\n * \"Feedback-Diskussion\", `myFeedback` \"Mein Feedback\", `inbox` \"Feedback-Eingang\".\n */\nexport interface FeedbackMenuLabels {\n /** The trigger's accessible name, and the menu's. keksdose `feedback.send`. */\n trigger: string;\n /** The link to the viewer's own reports — everyone's, an admin's too since 0.40\n * (§10.2 decision 3). keksdose `feedback.my_title`. */\n myFeedback: string;\n /** 0.40: the link to the public list, {@link FeedbackMenuProps.publicHref} (§10.2\n * decision 3; \"All feedback\" before). de-CH \"Feedback-Diskussion\". */\n discussion: string;\n /** 0.40: the admin's link to the triage list, {@link FeedbackMenuProps.inboxHref}\n * (\"View feedback\" before). de-CH \"Feedback-Eingang\". */\n inbox: string;\n /** @deprecated 0.40 — use {@link inbox}; removed in 0.41. Unused: the inbox row reads\n * `inbox` (feedback §10.6). */\n viewFeedback?: string;\n /** @deprecated 0.40 — use {@link discussion}; removed in 0.41. Unused: the public row\n * reads `discussion` (feedback §10.6). */\n allFeedback?: string;\n}\n\n// Defined in ./feedback-menu-labels, which holds no component code (0.34, KS U8).\nexport { DEFAULT_FEEDBACK_MENU_LABELS };\n\nexport interface FeedbackMenuProps {\n /** A category row was chosen — open the submit dialog on it (`useFeedbackSubmit`'s\n * `open`). Never called with `CRASH`: there is no row for it. */\n onFile: (category: FeedbackCategory) => void;\n /** Adds the **Feedback inbox** row → {@link inboxHref} under \"My feedback\" (0.40, §10.2\n * decision 11; until 0.39 it replaced it). The admin check is the app's (kastlan's is\n * company-scoped). Default `false`. */\n isAdmin?: boolean;\n /**\n * Rows of the app's own, between the divider and the list link — keksdose's\n * `Sparkles` Help assistant → `/assistant` and `MessagesSquare` Support chat →\n * `/support` with its unread chip. Any {@link TopBarMenuEntry}; keep their `key`s clear\n * of the menu's own (`BUG`, `IDEA`, `QUESTION`, `OTHER`, `feedback-divider`,\n * `all-feedback`, `my-feedback`, `view-feedback`).\n */\n extraEntries?: TopBarMenuEntry[];\n /** The viewer's own reports, linked for everyone (an admin too since 0.40). Default\n * `\"/my-feedback\"` (§2.3). */\n myFeedbackHref?: string;\n /** The admin inbox, linked for an admin. Default `\"/feedback\"` (§2.3). */\n inboxHref?: string;\n /**\n * The public list (0.38, docs/feedback-harmonization.md §9.2 decision 21): when passed,\n * a **Feedback discussion** row (`Users`, key `all-feedback`; \"All feedback\" until 0.40)\n * first of the list links, for everyone — the admin too, who reads what the others read\n * there. keksdose: `\"/all-feedback\"`. Leave it out where the app has no\n * public parts (kastlan) or the viewer may not see them (a Kurvenschmiede customer, a\n * demo — §9.2 decisions 7, 8).\n */\n publicHref?: string;\n /** A dot on the trigger — keksdose's unread support replies (`tone: \"danger\"`). Its\n * `label` joins the trigger's name (\"Send feedback 2 unread\"). The words are the app's. */\n iconBadge?: UserAvatarBadge | null;\n /** Prop > `<UiKitProvider labels={{ feedbackMenu }}>` > English. */\n labels?: LabelOverride<FeedbackMenuLabels>;\n /** Which edge of the trigger the panel lines up with, `start` or `end` in the reading\n * direction (see `HoverMenu`, whose type this is). Default `end`. */\n align?: HoverMenuProps[\"align\"];\n}\n\n/**\n * The top-bar feedback menu, the same in every app (0.27.0, docs/feedback-harmonization.md\n * §4.1) — keksdose `frontend/src/app/top-bar.tsx:156` on the kit's `TopBarActionMenu`:\n *\n * 1. a `MessageSquare` trigger named \"Send feedback\", with an optional `iconBadge`;\n * 2. Bug · Idea · Question · Other (`Bug`, `Lightbulb`, `HelpCircle`, `MoreHorizontal` —\n * the category badges' own glyphs), each calling `onFile(category)`;\n * 3. a divider, always;\n * 4. the app's `extraEntries`;\n * 5. the list links (0.40, §10.2 decision 11), in this order: **Feedback discussion**\n * (`Users`, key `all-feedback`) → `publicHref`, when passed; **My feedback** (`User`,\n * key `my-feedback`) → `/my-feedback`, for everyone, admins included; **Feedback\n * inbox** (`Inbox`, key `view-feedback`) → `/feedback`, for an admin. An admin with\n * public parts sees three, a member two; without them an admin two and anyone else\n * one. Until 0.39 an admin had the inbox in place of their own list (feedback #331);\n * Marcel's kk #419 gave them both.\n *\n * WHY A PRESET. Three apps had drawn three menus on the same primitive and they disagreed:\n * kastlan headed it \"New submission\", Kurvenschmiede \"Send feedback\" with a single link\n * that changed its words by role, keksdose had no heading and one link by role. keksdose\n * is the source (§2.4, §7.11): no heading, the list links by role — and the kit draws\n * it, so the row order, the icons and the words cannot drift again. Where the menu sits in the bar stays each app's.\n *\n * `CRASH` has no row and never will: crash reports are filed by the error boundary with\n * the stack and a fingerprint (keksdose feedback #160), and a hand-filed \"crash\" would sort\n * to the top like one with none of what makes it actionable. The server refuses it with a\n * 422 anyway (§3.4).\n *\n * The menu forwards nothing to its trigger, as `TopBarActionMenu` does not: keksdose's\n * tour anchor (`data-tour=\"feedback-menu\"`) stays on a wrapping `<span>`. Needs a router,\n * as every `TopBarActionMenu` with link rows does.\n */\nexport function FeedbackMenu({\n onFile,\n isAdmin = false,\n extraEntries,\n myFeedbackHref = \"/my-feedback\",\n inboxHref = \"/feedback\",\n publicHref,\n iconBadge,\n labels: labelsProp,\n align,\n}: FeedbackMenuProps) {\n const labels = useKitLabels(\"feedbackMenu\", DEFAULT_FEEDBACK_MENU_LABELS, labelsProp);\n const categoryLabels = useFeedbackCategoryLabels();\n const entries: TopBarMenuEntry[] = [\n ...FEEDBACK_PICKABLE_CATEGORIES.map((category): TopBarMenuEntry => {\n const Icon = FEEDBACK_CATEGORY_META[category].icon;\n return {\n key: category,\n icon: <Icon className=\"size-4\" />,\n label: categoryLabels[category],\n onSelect: () => onFile(category),\n };\n }),\n { kind: \"divider\", key: \"feedback-divider\" },\n ...(extraEntries ?? []),\n ...(publicHref\n ? [\n {\n kind: \"link\",\n key: \"all-feedback\",\n to: publicHref,\n icon: <Users className=\"size-4\" />,\n label: labels.discussion,\n } satisfies TopBarMenuEntry,\n ]\n : []),\n {\n kind: \"link\",\n key: \"my-feedback\",\n to: myFeedbackHref,\n icon: <User className=\"size-4\" />,\n label: labels.myFeedback,\n },\n ...(isAdmin\n ? [\n {\n kind: \"link\",\n key: \"view-feedback\",\n to: inboxHref,\n icon: <Inbox className=\"size-4\" />,\n label: labels.inbox,\n } satisfies TopBarMenuEntry,\n ]\n : []),\n ];\n return (\n <TopBarActionMenu\n icon={<MessageSquare className=\"size-5\" />}\n iconBadge={iconBadge}\n aria-label={labels.trigger}\n entries={entries}\n align={align}\n />\n );\n}\n"],"mappings":";AAmIc;AAnId,SAAS,OAAO,eAAe,MAAM,aAAa;AAElD,SAAS,oBAAwC;AACjD,SAAS,wBAA8C;AAIvD,SAAS,8BAAqD;AAC9D,SAAS,iCAAiC;AAC1C,SAAS,oCAAoC;AAC7C,SAAS,oCAAoC;AAuGtC,SAAS,aAAa;AAAA,EAC3B;AAAA,EACA,UAAU;AAAA,EACV;AAAA,EACA,iBAAiB;AAAA,EACjB,YAAY;AAAA,EACZ;AAAA,EACA;AAAA,EACA,QAAQ;AAAA,EACR;AACF,GAAsB;AACpB,QAAM,SAAS,aAAa,gBAAgB,8BAA8B,UAAU;AACpF,QAAM,iBAAiB,0BAA0B;AACjD,QAAM,UAA6B;AAAA,IACjC,GAAG,6BAA6B,IAAI,CAAC,aAA8B;AACjE,YAAM,OAAO,uBAAuB,QAAQ,EAAE;AAC9C,aAAO;AAAA,QACL,KAAK;AAAA,QACL,MAAM,oBAAC,QAAK,WAAU,UAAS;AAAA,QAC/B,OAAO,eAAe,QAAQ;AAAA,QAC9B,UAAU,MAAM,OAAO,QAAQ;AAAA,MACjC;AAAA,IACF,CAAC;AAAA,IACD,EAAE,MAAM,WAAW,KAAK,mBAAmB;AAAA,IAC3C,GAAI,gBAAgB,CAAC;AAAA,IACrB,GAAI,aACA;AAAA,MACE;AAAA,QACE,MAAM;AAAA,QACN,KAAK;AAAA,QACL,IAAI;AAAA,QACJ,MAAM,oBAAC,SAAM,WAAU,UAAS;AAAA,QAChC,OAAO,OAAO;AAAA,MAChB;AAAA,IACF,IACA,CAAC;AAAA,IACL;AAAA,MACE,MAAM;AAAA,MACN,KAAK;AAAA,MACL,IAAI;AAAA,MACJ,MAAM,oBAAC,QAAK,WAAU,UAAS;AAAA,MAC/B,OAAO,OAAO;AAAA,IAChB;AAAA,IACA,GAAI,UACA;AAAA,MACE;AAAA,QACE,MAAM;AAAA,QACN,KAAK;AAAA,QACL,IAAI;AAAA,QACJ,MAAM,oBAAC,SAAM,WAAU,UAAS;AAAA,QAChC,OAAO,OAAO;AAAA,MAChB;AAAA,IACF,IACA,CAAC;AAAA,EACP;AACA,SACE;AAAA,IAAC;AAAA;AAAA,MACC,MAAM,oBAAC,iBAAc,WAAU,UAAS;AAAA,MACxC;AAAA,MACA,cAAY,OAAO;AAAA,MACnB;AAAA,MACA;AAAA;AAAA,EACF;AAEJ;","names":[]}
@@ -5,10 +5,11 @@
5
5
  * stay `feedbackPage`'s, so the two lists cannot disagree; the vote button's words are
6
6
  * `feedbackVote`'s, the thread's `feedbackComments`'.
7
7
  *
8
- * de-CH canon (§9.2, §9.5): `title` "Alle Rückmeldungen", `anonymous` "Eine Person".
8
+ * de-CH canon (§9.2, §9.5, §10.6): `title` "Feedback-Diskussion", `anonymous` "Eine Person".
9
9
  */
10
10
  export interface FeedbackPublicLabels {
11
- /** The page's heading — keksdose's `/all-feedback`, the menu's "All feedback". */
11
+ /** The page's heading — keksdose's `/all-feedback`, the menu's "Feedback discussion"
12
+ * (0.40, §10.2 decision 11; "All feedback" before). de-CH "Feedback-Diskussion". */
12
13
  title: string;
13
14
  /** The sort toggle's accessible name. */
14
15
  sort: string;
@@ -1,6 +1,6 @@
1
1
  "use client";
2
2
  const DEFAULT_FEEDBACK_PUBLIC_LABELS = {
3
- title: "All feedback",
3
+ title: "Feedback discussion",
4
4
  sort: "Sort",
5
5
  sortNewest: "Newest",
6
6
  sortVotes: "Most wanted",
@@ -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 /** 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":[]}
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, §10.6): `title` \"Feedback-Diskussion\", `anonymous` \"Eine Person\".\n */\nexport interface FeedbackPublicLabels {\n /** The page's heading — keksdose's `/all-feedback`, the menu's \"Feedback discussion\"\n * (0.40, §10.2 decision 11; \"All feedback\" before). de-CH \"Feedback-Diskussion\". */\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: \"Feedback discussion\",\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":";AA2DO,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":[]}
@@ -279,6 +279,7 @@ function FeedbackPublicDetail({
279
279
  comments: comments ?? [],
280
280
  loading: comments === void 0,
281
281
  isAdmin,
282
+ isAuthor: row.mine,
282
283
  onComment,
283
284
  onHide,
284
285
  onDelete,
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/feedback/feedback-public-list.tsx"],"sourcesContent":["import { useMemo, useState } from \"react\";\nimport type { ReactNode } from \"react\";\nimport { ChevronLeft, ChevronRight, MessageSquare, ThumbsUp } from \"lucide-react\";\nimport { DataTable, type DataTableColumn } from \"../components/data-table\";\nimport { DEFAULT_DATA_TABLE_LABELS } from \"../components/data-table-labels\";\nimport { IconButton } from \"../components/icon-button\";\nimport { SearchField } from \"../components/search-field\";\nimport { ToggleGroup } from \"../components/toggle-group\";\nimport type { CommitScope } from \"../components/write-lock\";\nimport { useKitLabels, type LabelOverride } from \"../i18n/kit-labels\";\nimport { cn } from \"../lib/cn\";\nimport { 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":"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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) — on a server before\n server-kit 0.13; since then each is an entry in the conversation (§10.3). */}\n <FeedbackProse>{row.body}</FeedbackProse>\n </FeedbackDetailSection>\n <FeedbackDetailSection title={labels.conversation}>\n <FeedbackCommentThread\n replies={row.replies}\n comments={comments ?? []}\n loading={comments === undefined}\n isAdmin={isAdmin}\n // 0.40 (§10.3): the author's rework entries are theirs — at the reading end.\n isAuthor={row.mine}\n onComment={onComment}\n onHide={onHide}\n onDelete={onDelete}\n pending={pending}\n commit={commit}\n now={now}\n labels={commentLabels}\n />\n </FeedbackDetailSection>\n </FeedbackDetail>\n );\n}\n"],"mappings":"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","names":[]}
@@ -9,14 +9,27 @@ import type { FeedbackCategory, FeedbackStatus } from "./feedback-vocabulary.js"
9
9
  * full name for an admin, and `null` when there is none to show (an erased or incomplete
10
10
  * account, a loop's reply). The client never builds one from an id.
11
11
  */
12
- /** One answer of the admin's — `FeedbackReplyResponse`, §9.7. Oldest first. */
12
+ /** The two kinds of entry in a report's timeline (0.40, §10.2 decision 7): the admin's
13
+ * `reply`, and the author's `rework` — the report sent back with a note. */
14
+ export type FeedbackEntryKind = "reply" | "rework";
15
+ /** One answer of the admin's — `FeedbackReplyResponse`, §9.7 — or, since 0.40, one
16
+ * rework of the author's (§10.3, `kind: "rework"`). Oldest first. */
13
17
  export interface FeedbackReply {
14
18
  id: number;
15
19
  /** `null`: a loop's reply, a migrated `outcome`, or an erased admin — drawn as the
16
- * app's word for its admin ("Admin"). */
20
+ * app's word for its admin ("Admin"). On a rework entry `null` is an erased or
21
+ * unattributed author, drawn as "A user" (`feedbackPublic.anonymous`), never as the
22
+ * admin word. */
17
23
  author_name: string | null;
18
24
  body: string;
19
- /** The status this reply moved the report to; `null` = no status change. */
25
+ /** 0.40 (§10.3): `"reply"` (absent: a server before server-kit 0.13, all replies) or
26
+ * `"rework"`, the author's entry. */
27
+ kind?: FeedbackEntryKind;
28
+ /** 0.40: a rework entry's one file (`/api/v1/feedback/attachments/<key>`) — the admin
29
+ * and author views only; `null` or absent otherwise (never in the public view, §9.10). */
30
+ attachment_url?: string | null;
31
+ /** The status this reply moved the report to; `null` = no status change — and on a
32
+ * rework migrated out of the body (§10.4), whose move was never recorded. */
20
33
  status_to: FeedbackStatus | null;
21
34
  /** Where it moved from — the admin view only. */
22
35
  status_from?: FeedbackStatus | null;
@@ -152,11 +152,18 @@ export declare const FEEDBACK_ATTACHMENT_ACCEPT: string[];
152
152
  export declare const FEEDBACK_PICKABLE_CATEGORIES: readonly FeedbackCategory[];
153
153
  /**
154
154
  * The statuses a report can be sent back for **rework** from (§3.4): everything the team
155
- * has answered — `IN_EVALUATION`, `NEEDS_LIVE_TEST`, `POSTPONED`, `DONE`, `WONT_DO`. On
156
- * these the server reopens the row when the body grows by a rework block; on `OPEN`,
157
- * `READY` and `IN_PROGRESS` there is nothing to send back yet.
155
+ * has answered — `IN_EVALUATION`, `NEEDS_LIVE_TEST`, `POSTPONED`, `DONE`, `WONT_DO` — and,
156
+ * since 0.40, `REWORK` itself (§10.2 decision 4: a second rework adds another entry). On
157
+ * `OPEN`, `READY` and `IN_PROGRESS` there is nothing to send back yet.
158
158
  */
159
159
  export declare const FEEDBACK_REWORKABLE_STATUSES: readonly FeedbackStatus[];
160
+ /**
161
+ * The loops' queue (0.40, §10.2 decision 4; server-kit `QUEUE_STATUSES`): released for
162
+ * work (`READY`), sent back and released again (`REWORK`), and being worked
163
+ * (`IN_PROGRESS`). Kurvenschmiede's loop adds `OPEN` (decision 9) — its own difference,
164
+ * not this list's.
165
+ */
166
+ export declare const FEEDBACK_QUEUE_STATUSES: readonly FeedbackStatus[];
160
167
  /**
161
168
  * The statuses in which the AUTHOR may still edit title, body and category (§3.4,
162
169
  * keksdose `canAuthorEdit`, `feedback-page.tsx:398`): `OPEN`, `READY` and `IN_PROGRESS`.
@@ -219,6 +226,16 @@ export declare function appendRework(body: string, note: string, url?: string |
219
226
  * comments do not count.
220
227
  */
221
228
  export declare function reworkCount(body: string): number;
229
+ /**
230
+ * How many times a report has been sent back, since 0.40 (§10.3): its `kind: "rework"`
231
+ * entries when the row carries `replies`, else {@link reworkCount} off the body (a server
232
+ * before server-kit 0.13, whose reworks are still `--- REWORK … ---` blocks). What the
233
+ * Rework chip counts.
234
+ */
235
+ export declare function feedbackReworkCount(row: {
236
+ body?: string | null;
237
+ replies?: readonly Pick<FeedbackReply, "kind">[] | null;
238
+ }): number;
222
239
  /**
223
240
  * A body split into the prose to render and the files to show beside it, in body order.
224
241
  *
@@ -12,12 +12,18 @@ const FEEDBACK_PICKABLE_CATEGORIES = Object.freeze(
12
12
  FEEDBACK_CATEGORY_ORDER.filter((category) => category !== "CRASH")
13
13
  );
14
14
  const FEEDBACK_REWORKABLE_STATUSES = Object.freeze([
15
+ "REWORK",
15
16
  "IN_EVALUATION",
16
17
  "NEEDS_LIVE_TEST",
17
18
  "POSTPONED",
18
19
  "DONE",
19
20
  "WONT_DO"
20
21
  ]);
22
+ const FEEDBACK_QUEUE_STATUSES = Object.freeze([
23
+ "READY",
24
+ "REWORK",
25
+ "IN_PROGRESS"
26
+ ]);
21
27
  const FEEDBACK_AUTHOR_EDITABLE_STATUSES = Object.freeze([
22
28
  "OPEN",
23
29
  "READY",
@@ -61,6 +67,13 @@ function reworkCount(body) {
61
67
  for (const line of body.split("\n")) if (REWORK_RULE.test(line.trim())) n += 1;
62
68
  return n;
63
69
  }
70
+ function feedbackReworkCount(row) {
71
+ if (row.replies) {
72
+ const entries = row.replies.filter((reply) => reply.kind === "rework").length;
73
+ if (entries > 0) return entries;
74
+ }
75
+ return reworkCount(row.body ?? "");
76
+ }
64
77
  function splitBodyAttachments(body) {
65
78
  const urls = [];
66
79
  const kept = [];
@@ -107,11 +120,13 @@ export {
107
120
  FEEDBACK_COMMENT_MAX,
108
121
  FEEDBACK_DECIDED_STATUSES,
109
122
  FEEDBACK_PICKABLE_CATEGORIES,
123
+ FEEDBACK_QUEUE_STATUSES,
110
124
  FEEDBACK_REPLY_MAX,
111
125
  FEEDBACK_REWORKABLE_STATUSES,
112
126
  appendRework,
113
127
  attachmentName,
114
128
  feedbackAttachmentLine,
129
+ feedbackReworkCount,
115
130
  isImageAttachment,
116
131
  replaceDescription,
117
132
  reworkCount,