@eifi1/ui-kit 0.36.0 → 0.38.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 (147) hide show
  1. package/README.md +11 -4
  2. package/dist/components/field-parts.d.ts +3 -3
  3. package/dist/components/field-parts.js.map +1 -1
  4. package/dist/components/help-popover.d.ts +1 -1
  5. package/dist/components/help-popover.js.map +1 -1
  6. package/dist/components/numeric-field.d.ts +1 -1
  7. package/dist/components/numeric-field.js.map +1 -1
  8. package/dist/eslint/index.js +1 -1
  9. package/dist/eslint/role-utilities.d.ts +34 -2
  10. package/dist/eslint/role-utilities.js +30 -9
  11. package/dist/eslint/role-utilities.js.map +1 -1
  12. package/dist/feedback/feedback-comment-thread.d.ts +80 -0
  13. package/dist/feedback/feedback-comment-thread.js +162 -0
  14. package/dist/feedback/feedback-comment-thread.js.map +1 -0
  15. package/dist/feedback/feedback-dialog-labels.js +2 -1
  16. package/dist/feedback/feedback-dialog-labels.js.map +1 -1
  17. package/dist/feedback/feedback-dialog.d.ts +13 -0
  18. package/dist/feedback/feedback-dialog.js +12 -4
  19. package/dist/feedback/feedback-dialog.js.map +1 -1
  20. package/dist/feedback/feedback-menu-labels.js +2 -1
  21. package/dist/feedback/feedback-menu-labels.js.map +1 -1
  22. package/dist/feedback/feedback-menu.d.ts +17 -3
  23. package/dist/feedback/feedback-menu.js +11 -1
  24. package/dist/feedback/feedback-menu.js.map +1 -1
  25. package/dist/feedback/feedback-public-labels.d.ts +50 -0
  26. package/dist/feedback/feedback-public-labels.js +26 -0
  27. package/dist/feedback/feedback-public-labels.js.map +1 -0
  28. package/dist/feedback/feedback-public-list.d.ts +118 -0
  29. package/dist/feedback/feedback-public-list.js +248 -0
  30. package/dist/feedback/feedback-public-list.js.map +1 -0
  31. package/dist/feedback/feedback-public-switch.d.ts +51 -0
  32. package/dist/feedback/feedback-public-switch.js +50 -0
  33. package/dist/feedback/feedback-public-switch.js.map +1 -0
  34. package/dist/feedback/feedback-public-types.d.ts +75 -0
  35. package/dist/feedback/feedback-public-types.js +2 -0
  36. package/dist/feedback/feedback-public-types.js.map +1 -0
  37. package/dist/feedback/feedback-record.d.ts +32 -1
  38. package/dist/feedback/feedback-record.js +10 -0
  39. package/dist/feedback/feedback-record.js.map +1 -1
  40. package/dist/feedback/feedback-replies.d.ts +97 -0
  41. package/dist/feedback/feedback-replies.js +114 -0
  42. package/dist/feedback/feedback-replies.js.map +1 -0
  43. package/dist/feedback/feedback-row-detail-labels.js +1 -0
  44. package/dist/feedback/feedback-row-detail-labels.js.map +1 -1
  45. package/dist/feedback/feedback-row-detail.d.ts +38 -10
  46. package/dist/feedback/feedback-row-detail.js +50 -50
  47. package/dist/feedback/feedback-row-detail.js.map +1 -1
  48. package/dist/feedback/feedback-submit-dialog.d.ts +3 -1
  49. package/dist/feedback/feedback-submit-dialog.js +6 -1
  50. package/dist/feedback/feedback-submit-dialog.js.map +1 -1
  51. package/dist/feedback/feedback-submit.d.ts +8 -0
  52. package/dist/feedback/feedback-submit.js +3 -2
  53. package/dist/feedback/feedback-submit.js.map +1 -1
  54. package/dist/feedback/feedback-table.d.ts +15 -5
  55. package/dist/feedback/feedback-table.js +46 -5
  56. package/dist/feedback/feedback-table.js.map +1 -1
  57. package/dist/feedback/feedback-thread-parts-labels.d.ts +6 -0
  58. package/dist/feedback/feedback-thread-parts-labels.js +36 -0
  59. package/dist/feedback/feedback-thread-parts-labels.js.map +1 -0
  60. package/dist/feedback/feedback-thread.d.ts +8 -0
  61. package/dist/feedback/feedback-thread.js +7 -2
  62. package/dist/feedback/feedback-thread.js.map +1 -1
  63. package/dist/feedback/feedback-vote-button.d.ts +48 -0
  64. package/dist/feedback/feedback-vote-button.js +72 -0
  65. package/dist/feedback/feedback-vote-button.js.map +1 -0
  66. package/dist/feedback.d.ts +6 -0
  67. package/dist/feedback.js +6 -0
  68. package/dist/feedback.js.map +1 -1
  69. package/dist/i18n/defaults.js +12 -0
  70. package/dist/i18n/defaults.js.map +1 -1
  71. package/dist/i18n/german.js +64 -2
  72. package/dist/i18n/german.js.map +1 -1
  73. package/dist/i18n/kit-labels.d.ts +20 -3
  74. package/dist/i18n/kit-labels.js.map +1 -1
  75. package/dist/i18n/locales/en.js +80 -3
  76. package/dist/i18n/locales/en.js.map +3 -3
  77. package/dist/i18n/locales/es.js +64 -2
  78. package/dist/i18n/locales/es.js.map +1 -1
  79. package/dist/i18n/locales/fr.js +64 -2
  80. package/dist/i18n/locales/fr.js.map +1 -1
  81. package/dist/i18n/locales/hu.js +64 -2
  82. package/dist/i18n/locales/hu.js.map +1 -1
  83. package/dist/i18n/locales/it.js +64 -2
  84. package/dist/i18n/locales/it.js.map +1 -1
  85. package/dist/i18n/locales/zh.js +64 -2
  86. package/dist/i18n/locales/zh.js.map +1 -1
  87. package/dist/i18n/review.js +4 -0
  88. package/dist/i18n/review.js.map +1 -1
  89. package/dist/i18n/shared-labels.js +1 -1
  90. package/dist/i18n/shared-labels.js.map +1 -1
  91. package/dist/index.d.ts +2 -0
  92. package/dist/index.js +6 -0
  93. package/dist/index.js.map +1 -1
  94. package/dist/lib/plain-text.d.ts +1 -1
  95. package/dist/lib/plain-text.js.map +1 -1
  96. package/dist/notifications/feedback-notification-types-labels.d.ts +15 -0
  97. package/dist/notifications/feedback-notification-types-labels.js +11 -0
  98. package/dist/notifications/feedback-notification-types-labels.js.map +1 -0
  99. package/dist/notifications/feedback-notification-types.d.ts +28 -0
  100. package/dist/notifications/feedback-notification-types.js +21 -0
  101. package/dist/notifications/feedback-notification-types.js.map +1 -0
  102. package/dist/rhf/fields.d.ts +1 -1
  103. package/dist/rhf/fields.js.map +1 -1
  104. package/dist/settings/device-reset-setting.d.ts +4 -4
  105. package/dist/settings/device-reset-setting.js.map +1 -1
  106. package/package.json +1 -1
  107. package/src/components/field-parts.tsx +4 -4
  108. package/src/components/help-popover.tsx +1 -1
  109. package/src/components/numeric-field.ts +1 -1
  110. package/src/eslint/role-utilities.ts +67 -11
  111. package/src/feedback/feedback-comment-thread.tsx +255 -0
  112. package/src/feedback/feedback-dialog-labels.ts +1 -0
  113. package/src/feedback/feedback-dialog.tsx +30 -2
  114. package/src/feedback/feedback-menu-labels.ts +1 -0
  115. package/src/feedback/feedback-menu.tsx +29 -3
  116. package/src/feedback/feedback-public-labels.ts +76 -0
  117. package/src/feedback/feedback-public-list.tsx +395 -0
  118. package/src/feedback/feedback-public-switch.tsx +102 -0
  119. package/src/feedback/feedback-public-types.ts +82 -0
  120. package/src/feedback/feedback-record.ts +39 -1
  121. package/src/feedback/feedback-replies.tsx +235 -0
  122. package/src/feedback/feedback-row-detail-labels.ts +1 -0
  123. package/src/feedback/feedback-row-detail.tsx +103 -74
  124. package/src/feedback/feedback-submit-dialog.tsx +9 -0
  125. package/src/feedback/feedback-submit.tsx +10 -1
  126. package/src/feedback/feedback-table.tsx +73 -6
  127. package/src/feedback/feedback-thread-parts-labels.ts +41 -0
  128. package/src/feedback/feedback-thread.tsx +18 -1
  129. package/src/feedback/feedback-vote-button.tsx +122 -0
  130. package/src/feedback.ts +10 -0
  131. package/src/i18n/defaults.ts +12 -0
  132. package/src/i18n/german.ts +62 -0
  133. package/src/i18n/kit-labels.tsx +20 -3
  134. package/src/i18n/locales/en.ts +7 -0
  135. package/src/i18n/locales/es.ts +62 -0
  136. package/src/i18n/locales/fr.ts +62 -0
  137. package/src/i18n/locales/hu.ts +62 -0
  138. package/src/i18n/locales/it.ts +62 -0
  139. package/src/i18n/locales/zh.ts +62 -0
  140. package/src/i18n/review.ts +4 -0
  141. package/src/i18n/shared-labels.ts +1 -1
  142. package/src/index.ts +9 -0
  143. package/src/lib/plain-text.ts +1 -1
  144. package/src/notifications/feedback-notification-types-labels.ts +24 -0
  145. package/src/notifications/feedback-notification-types.ts +46 -0
  146. package/src/rhf/fields.tsx +1 -1
  147. package/src/settings/device-reset-setting.tsx +4 -4
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/feedback/feedback-menu.tsx"],"sourcesContent":["import { Inbox, MessageSquare } 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}\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 * `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 /** 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. 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 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 {\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":";AA6Gc;AA7Gd,SAAS,OAAO,qBAAqB;AAErC,SAAS,oBAAwC;AACjD,SAAS,wBAA8C;AAIvD,SAAS,8BAAqD;AAC9D,SAAS,iCAAiC;AAC1C,SAAS,oCAAoC;AAC7C,SAAS,oCAAoC;AAkFtC,SAAS,aAAa;AAAA,EAC3B;AAAA,EACA,UAAU;AAAA,EACV;AAAA,EACA,iBAAiB;AAAA,EACjB,YAAY;AAAA,EACZ;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;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, 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":[]}
@@ -0,0 +1,50 @@
1
+ /**
2
+ * `feedbackPublic` — the words of the pages every user reads (§9.3, §9.9): the list's
3
+ * sort, search and count, the detail's sections and the "Visible to others" switch. The
4
+ * table's column names that the inbox already has (#, Date, Category, Subject, Status)
5
+ * stay `feedbackPage`'s, so the two lists cannot disagree; the vote button's words are
6
+ * `feedbackVote`'s, the thread's `feedbackComments`'.
7
+ *
8
+ * de-CH canon (§9.2, §9.5): `title` "Alle Rückmeldungen", `anonymous` "Eine Person".
9
+ */
10
+ export interface FeedbackPublicLabels {
11
+ /** The page's heading — keksdose's `/all-feedback`, the menu's "All feedback". */
12
+ title: string;
13
+ /** The sort toggle's accessible name. */
14
+ sort: string;
15
+ /** `sort=newest`, the server's default. */
16
+ sortNewest: string;
17
+ /** `sort=votes`: most votes, then newest. */
18
+ sortVotes: string;
19
+ /** `sort=updated`. */
20
+ sortUpdated: string;
21
+ /** The search box's accessible name: it searches titles only (§9.9). */
22
+ search: string;
23
+ /** The Votes column's header. */
24
+ columnVotes: string;
25
+ /** How many reports the server has for this query (`total`). */
26
+ total: (count: number) => string;
27
+ /** A row's visible comments, the chip beside its title. */
28
+ comments: (count: number) => string;
29
+ /** A row's votes, the count on a phone card (the button is in the opened report). */
30
+ votes: (count: number) => string;
31
+ /** A `null` name (§9.5): an incomplete or erased account. */
32
+ anonymous: string;
33
+ /** Who filed it, with the server's first name (or {@link anonymous}). */
34
+ filedBy: (name: string) => string;
35
+ /** The detail's public body section. */
36
+ body: string;
37
+ /** The detail's merged replies and comments (§9.8, "Order"). */
38
+ conversation: string;
39
+ /** While the list or the comments load. */
40
+ loading: string;
41
+ /** No public report (yet, or for this search). */
42
+ empty: string;
43
+ /** `FeedbackPublicSwitch`'s label (§9.3, decision 6). */
44
+ visibleToOthers: string;
45
+ /** The line under it: what others see, and what never. */
46
+ visibleHint: string;
47
+ /** Why the author cannot flip it now: the report is past its open window (§9.3). */
48
+ visibleLocked: string;
49
+ }
50
+ export declare const DEFAULT_FEEDBACK_PUBLIC_LABELS: FeedbackPublicLabels;
@@ -0,0 +1,26 @@
1
+ "use client";
2
+ const DEFAULT_FEEDBACK_PUBLIC_LABELS = {
3
+ title: "All feedback",
4
+ sort: "Sort",
5
+ sortNewest: "Newest",
6
+ sortVotes: "Most wanted",
7
+ sortUpdated: "Recently updated",
8
+ search: "Search titles",
9
+ columnVotes: "Votes",
10
+ total: (count) => count === 1 ? "1 report" : `${count} reports`,
11
+ comments: (count) => count === 1 ? "1 comment" : `${count} comments`,
12
+ votes: (count) => count === 1 ? "1 vote" : `${count} votes`,
13
+ anonymous: "A user",
14
+ filedBy: (name) => `From ${name}`,
15
+ body: "Description",
16
+ conversation: "Answers and comments",
17
+ loading: "Loading\u2026",
18
+ empty: "No reports yet",
19
+ visibleToOthers: "Visible to others",
20
+ visibleHint: "Other users see the title and text, never your screenshots or files.",
21
+ visibleLocked: "Only while the report is still open."
22
+ };
23
+ export {
24
+ DEFAULT_FEEDBACK_PUBLIC_LABELS
25
+ };
26
+ //# sourceMappingURL=feedback-public-labels.js.map
@@ -0,0 +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":";AAuDO,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;AACjB;","names":[]}
@@ -0,0 +1,118 @@
1
+ import type { ReactNode } from "react";
2
+ import type { CommitScope } from "../components/write-lock.js";
3
+ import { type LabelOverride } from "../i18n/kit-labels.js";
4
+ import { type FeedbackCommentsLabels } from "./feedback-comment-thread.js";
5
+ import { DEFAULT_FEEDBACK_PUBLIC_LABELS, type FeedbackPublicLabels } from "./feedback-public-labels.js";
6
+ import type { FeedbackComment, FeedbackPublicListItem, FeedbackPublicRecord, FeedbackPublicSort, FeedbackVoteState } from "./feedback-public-types.js";
7
+ import { type FeedbackPageLabels, type FeedbackRenderDate } from "./feedback-table.js";
8
+ export { DEFAULT_FEEDBACK_PUBLIC_LABELS };
9
+ export type { FeedbackPublicLabels };
10
+ /** `feedbackPublic` resolved: English, then `<UiKitProvider labels>`, then `labels`. */
11
+ export declare function useFeedbackPublicLabels(labels?: LabelOverride<FeedbackPublicLabels>): FeedbackPublicLabels;
12
+ export interface FeedbackPublicListProps<T extends FeedbackPublicListItem = FeedbackPublicListItem> {
13
+ /** `GET /feedback/public?sort=&q=` — the server's page, in the server's order. */
14
+ rows: T[];
15
+ /** How many the query has in all — the line beside the sort. Left out, no line. */
16
+ total?: number;
17
+ /** The server sorts (§9.6): the toggle only says which way. */
18
+ sort: FeedbackPublicSort;
19
+ onSortChange: (sort: FeedbackPublicSort) => void;
20
+ /** The search box's text (`q`, titles only). */
21
+ search?: string;
22
+ /**
23
+ * The search box is drawn ONLY with this handler: where the app's titles are sealed
24
+ * (Kurvenschmiede, `FeedbackDeps.public_search=False`, §9.9) there is nothing to search
25
+ * on and the server answers `q` with a 422 — so it passes none.
26
+ */
27
+ onSearchChange?: (search: string) => void;
28
+ /** The opened row: the app's {@link FeedbackPublicDetail} with the row's
29
+ * `GET /feedback/public/{id}` and its comments. */
30
+ renderDetail: (row: T) => ReactNode;
31
+ /** `PUT` / `DELETE /feedback/{id}/vote` for a row's button (`next`: the new state);
32
+ * resolve with the answer, reject to roll back. */
33
+ onVote: (row: T, next: boolean) => Promise<FeedbackVoteState>;
34
+ /** The page is loading: the table says so where its rows would be. */
35
+ loading?: boolean;
36
+ /** The opened row's id, when the app keeps it (a `?row=` param). Left out, the list
37
+ * keeps it itself. */
38
+ openId?: number | null;
39
+ onOpenChange?: (id: number | null) => void;
40
+ /** A vote COMMITS — see Button's `commit`. */
41
+ commit?: CommitScope;
42
+ /** See {@link FeedbackRenderDate}. Default: the kit's, with the weekday. */
43
+ renderDate?: FeedbackRenderDate;
44
+ /** Over the `feedbackPublic` namespace. The shared column names (#, Date, Category,
45
+ * Subject, Status) are `feedbackPage`'s: pass `pageLabels` for those. */
46
+ labels?: LabelOverride<FeedbackPublicLabels>;
47
+ pageLabels?: LabelOverride<FeedbackPageLabels>;
48
+ className?: string;
49
+ }
50
+ /**
51
+ * **All feedback** — the reports every user the app lets in may read (round 0.38,
52
+ * docs/feedback-harmonization.md §9.3, §9.9; keksdose's `/all-feedback`).
53
+ *
54
+ * - **Titles only** (decision 23): #, Date, Category, Subject (with the visible
55
+ * comments), Votes, Status. No body, no submitter, no file — the list item carries
56
+ * none of them (§9.10).
57
+ * - **The server sorts**: the toggle **Newest / Most wanted / Recently updated** sets
58
+ * `sort`, and the columns themselves do not sort (a page of 50 sorted in the browser
59
+ * would not be the 50 the sort asks for).
60
+ * - **The search box only with `onSearchChange`**: an app whose titles are sealed
61
+ * passes none (§9.9).
62
+ * - **Votes** are `FeedbackVoteButton`s, optimistic, the viewer's own report pressed
63
+ * and locked ("Your report", §9.6).
64
+ * - A row opens in place with `renderDetail` — on the phone, in a full-screen dialog.
65
+ * The phone cards carry the title, the status, the category, the date and the two
66
+ * counts; the vote itself is in the opened report (a button inside the card, which is
67
+ * the row's own button, would be two controls in one).
68
+ *
69
+ * No paging of its own: the server's `limit` is the page, and `total` says how many
70
+ * there are (an app with more than one page adds its pager below). Needs a router, as
71
+ * every `DataTable` does.
72
+ */
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;
74
+ export interface FeedbackPublicDetailProps {
75
+ /** `GET /feedback/public/{id}`: the public body (attachment URLs already stripped by
76
+ * the server, §9.4) and the replies. */
77
+ row: FeedbackPublicRecord;
78
+ /** `GET /feedback/{id}/comments`, oldest first; `undefined` while it loads. */
79
+ comments: readonly FeedbackComment[] | undefined;
80
+ /** `PUT` / `DELETE /feedback/{id}/vote`; resolve with the answer, reject to roll back. */
81
+ onVote: (next: boolean) => Promise<FeedbackVoteState>;
82
+ /** The viewer is the app's admin: Hide / Unhide on the comments. */
83
+ isAdmin?: boolean;
84
+ /** `POST /feedback/{id}/comments`. Left out — a demo, a reader who may not write — no
85
+ * composer. */
86
+ onComment?: (body: string) => void | Promise<unknown>;
87
+ /** The admin's hide (`PATCH …/comments/{id} {hidden}`). */
88
+ onHide?: (comment: FeedbackComment, hidden: boolean) => void | Promise<unknown>;
89
+ /** The writer's delete (`DELETE …/comments/{id}`), after a confirm. */
90
+ onDelete?: (comment: FeedbackComment) => void | Promise<unknown>;
91
+ /** A comment is being sent. */
92
+ pending?: boolean;
93
+ /** The vote and the thread's buttons COMMIT — see Button's `commit`. */
94
+ commit?: CommitScope;
95
+ /** Voting is off for this viewer (a demo): `true`, or the reason. */
96
+ voteDisabled?: boolean | string;
97
+ /** See {@link FeedbackRenderDate}. */
98
+ renderDate?: FeedbackRenderDate;
99
+ /** "Now" for the thread's relative times — pin it in tests and stories. */
100
+ now?: Date | number;
101
+ /** Over the `feedbackPublic` namespace. */
102
+ labels?: LabelOverride<FeedbackPublicLabels>;
103
+ /** Over the `feedbackComments` namespace, for the thread. */
104
+ commentLabels?: LabelOverride<FeedbackCommentsLabels>;
105
+ className?: string;
106
+ }
107
+ /**
108
+ * One public report opened (round 0.38, docs/feedback-harmonization.md §9.3, §9.10): who
109
+ * filed it — the first name the server sent, else **"A user"** (§9.5) — and when; the
110
+ * **vote**; the **public body**; and the **conversation**, the admin's replies and other
111
+ * people's comments merged oldest first (§9.8, `FeedbackCommentThread`), with the box to
112
+ * add one.
113
+ *
114
+ * What it never draws, because the public record never carries it (§9.10): an address,
115
+ * a last name, a screenshot, a file, the page the report was filed on. The body is the
116
+ * server's `public_body` — the client strips nothing and adds nothing.
117
+ */
118
+ export declare function FeedbackPublicDetail({ row, comments, onVote, isAdmin, onComment, onHide, onDelete, pending, commit, voteDisabled, renderDate: renderDateProp, now, labels: labelsProp, commentLabels, className, }: FeedbackPublicDetailProps): import("react").JSX.Element;
@@ -0,0 +1,248 @@
1
+ "use client";
2
+ import { jsx, jsxs } from "react/jsx-runtime";
3
+ import { useMemo, useState } from "react";
4
+ import { MessageSquare, ThumbsUp } from "lucide-react";
5
+ import { DataTable } from "../components/data-table.js";
6
+ import { SearchField } from "../components/search-field.js";
7
+ import { ToggleGroup } from "../components/toggle-group.js";
8
+ import { useKitLabels } from "../i18n/kit-labels.js";
9
+ import { cn } from "../lib/cn.js";
10
+ import { EMPTY_FORMATTED_VALUE } from "../lib/format.js";
11
+ import { FeedbackCategoryBadge, FeedbackDetail, FeedbackDetailSection, FeedbackProse, FeedbackStatusBadge } from "./feedback-inbox.js";
12
+ import { useFeedbackCategoryLabels } from "./feedback-labels.js";
13
+ import { FeedbackCommentThread } from "./feedback-comment-thread.js";
14
+ import { DEFAULT_FEEDBACK_PUBLIC_LABELS } from "./feedback-public-labels.js";
15
+ import {
16
+ useFeedbackPageLabels,
17
+ useFeedbackRenderDate
18
+ } from "./feedback-table.js";
19
+ import { FeedbackVoteButton } from "./feedback-vote-button.js";
20
+ function useFeedbackPublicLabels(labels) {
21
+ return useKitLabels("feedbackPublic", DEFAULT_FEEDBACK_PUBLIC_LABELS, labels);
22
+ }
23
+ const SORTS = ["newest", "votes", "updated"];
24
+ function PublicDate({ value, renderDate }) {
25
+ if (!value) return /* @__PURE__ */ jsx("span", { className: "text-placeholder", children: EMPTY_FORMATTED_VALUE });
26
+ return /* @__PURE__ */ jsx("time", { dateTime: value, className: "whitespace-nowrap tabular-nums text-muted", children: renderDate(value) });
27
+ }
28
+ function CommentCount({ count, labels }) {
29
+ if (!count) return null;
30
+ return /* @__PURE__ */ jsxs("span", { className: "inline-flex shrink-0 items-center gap-1 text-xs text-muted", "data-feedback-comment-count": count, children: [
31
+ /* @__PURE__ */ jsx(MessageSquare, { className: "size-3.5", "aria-hidden": true }),
32
+ /* @__PURE__ */ jsx("span", { "aria-hidden": true, className: "tabular-nums", children: count }),
33
+ /* @__PURE__ */ jsx("span", { className: "sr-only", children: labels.comments(count) })
34
+ ] });
35
+ }
36
+ function VoteCount({ count, voted, labels }) {
37
+ return /* @__PURE__ */ jsxs(
38
+ "span",
39
+ {
40
+ className: cn("ms-auto inline-flex shrink-0 items-center gap-1 text-xs", voted ? "text-brand" : "text-muted"),
41
+ "data-feedback-vote-count": count,
42
+ children: [
43
+ /* @__PURE__ */ jsx(ThumbsUp, { className: "size-3.5", "aria-hidden": true }),
44
+ /* @__PURE__ */ jsx("span", { "aria-hidden": true, className: "tabular-nums", children: count }),
45
+ /* @__PURE__ */ jsx("span", { className: "sr-only", children: labels.votes(count) })
46
+ ]
47
+ }
48
+ );
49
+ }
50
+ function FeedbackPublicList({
51
+ rows,
52
+ total,
53
+ sort,
54
+ onSortChange,
55
+ search = "",
56
+ onSearchChange,
57
+ renderDetail,
58
+ onVote,
59
+ loading = false,
60
+ openId: openIdProp,
61
+ onOpenChange,
62
+ commit,
63
+ renderDate: renderDateProp,
64
+ labels: labelsProp,
65
+ pageLabels: pageLabelsProp,
66
+ className
67
+ }) {
68
+ const labels = useFeedbackPublicLabels(labelsProp);
69
+ const page = useFeedbackPageLabels(pageLabelsProp);
70
+ const categoryLabels = useFeedbackCategoryLabels();
71
+ const renderDate = useFeedbackRenderDate(renderDateProp);
72
+ const [ownOpenId, setOwnOpenId] = useState(null);
73
+ const openId = openIdProp !== void 0 ? openIdProp : ownOpenId;
74
+ const setOpen = (id) => {
75
+ if (openIdProp === void 0) setOwnOpenId(id);
76
+ onOpenChange?.(id);
77
+ };
78
+ const columns = useMemo(
79
+ () => [
80
+ {
81
+ key: "id",
82
+ header: page.columnId,
83
+ cell: (row) => /* @__PURE__ */ jsx("span", { className: "font-mono text-xs text-muted", children: row.id }),
84
+ mobileHidden: true
85
+ },
86
+ {
87
+ key: "date",
88
+ header: page.columnDate,
89
+ cell: (row) => /* @__PURE__ */ jsx(PublicDate, { value: row.created_at, renderDate })
90
+ },
91
+ {
92
+ key: "category",
93
+ header: page.columnCategory,
94
+ cell: (row) => /* @__PURE__ */ jsx(FeedbackCategoryBadge, { category: row.category, label: categoryLabels[row.category] })
95
+ },
96
+ {
97
+ key: "title",
98
+ header: page.columnSubject,
99
+ cell: (row) => /* @__PURE__ */ jsxs("span", { className: "inline-flex min-w-0 max-w-full items-center gap-2 large:items-start", children: [
100
+ /* @__PURE__ */ jsx("span", { className: "min-w-0 truncate-until-large font-medium", children: row.title }),
101
+ /* @__PURE__ */ jsx(CommentCount, { count: row.comment_count, labels })
102
+ ] }),
103
+ mobilePrimary: true
104
+ },
105
+ {
106
+ key: "votes",
107
+ header: labels.columnVotes,
108
+ className: "whitespace-nowrap",
109
+ // The cell is a button: the row's own link must never wrap it.
110
+ noRowLink: true,
111
+ cell: (row) => /* @__PURE__ */ jsx(
112
+ FeedbackVoteButton,
113
+ {
114
+ votes: row.votes,
115
+ voted: row.voted,
116
+ own: row.mine,
117
+ onVote: (next) => onVote(row, next),
118
+ commit,
119
+ size: "sm"
120
+ }
121
+ )
122
+ },
123
+ {
124
+ key: "status",
125
+ header: page.columnStatus,
126
+ className: "whitespace-nowrap",
127
+ cell: (row) => /* @__PURE__ */ jsx(FeedbackStatusBadge, { status: row.status })
128
+ }
129
+ ],
130
+ [page, labels, categoryLabels, renderDate, onVote, commit]
131
+ );
132
+ return /* @__PURE__ */ jsxs("div", { className: cn("flex flex-col gap-3", className), "data-feedback-public-list": "", "aria-busy": loading || void 0, children: [
133
+ /* @__PURE__ */ jsxs("div", { className: "flex flex-wrap items-center gap-2", children: [
134
+ /* @__PURE__ */ jsx(
135
+ ToggleGroup,
136
+ {
137
+ "aria-label": labels.sort,
138
+ value: sort,
139
+ onChange: (value) => onSortChange(value),
140
+ options: SORTS.map((value) => ({
141
+ value,
142
+ label: value === "newest" ? labels.sortNewest : value === "votes" ? labels.sortVotes : labels.sortUpdated
143
+ }))
144
+ }
145
+ ),
146
+ onSearchChange && /* @__PURE__ */ jsx(
147
+ SearchField,
148
+ {
149
+ "aria-label": labels.search,
150
+ placeholder: labels.search,
151
+ value: search,
152
+ onChange: onSearchChange,
153
+ className: "min-w-[12rem] flex-1 md:max-w-xs"
154
+ }
155
+ ),
156
+ total !== void 0 && /* @__PURE__ */ jsx("span", { className: "ms-auto text-sm text-muted", "data-feedback-public-total": total, children: labels.total(total) })
157
+ ] }),
158
+ /* @__PURE__ */ jsx(
159
+ DataTable,
160
+ {
161
+ rows,
162
+ columns,
163
+ rowKey: (row) => row.id,
164
+ rowName: (row) => row.title,
165
+ onRowClick: (row) => setOpen(openId === row.id ? null : row.id),
166
+ isExpanded: (row) => row.id === openId,
167
+ expandedRow: (row) => renderDetail(row),
168
+ mobileExpandAsDialog: true,
169
+ paginated: false,
170
+ empty: loading ? labels.loading : labels.empty,
171
+ mobileCard: (row) => /* @__PURE__ */ jsxs("div", { className: "min-w-0 space-y-1", children: [
172
+ /* @__PURE__ */ jsxs("div", { className: "flex items-start justify-between gap-2", children: [
173
+ /* @__PURE__ */ jsx("span", { className: "min-w-0 flex-1 font-medium [overflow-wrap:anywhere]", children: row.title }),
174
+ /* @__PURE__ */ jsx(FeedbackStatusBadge, { status: row.status, className: "shrink-0" })
175
+ ] }),
176
+ /* @__PURE__ */ jsxs("div", { className: "flex min-w-0 flex-wrap items-center gap-x-2 gap-y-1 text-xs text-muted", children: [
177
+ /* @__PURE__ */ jsx(FeedbackCategoryBadge, { category: row.category, compact: true, label: categoryLabels[row.category] }),
178
+ /* @__PURE__ */ jsx(PublicDate, { value: row.created_at, renderDate }),
179
+ /* @__PURE__ */ jsx(CommentCount, { count: row.comment_count, labels }),
180
+ /* @__PURE__ */ jsx(VoteCount, { count: row.votes, voted: row.voted || row.mine, labels })
181
+ ] })
182
+ ] })
183
+ }
184
+ )
185
+ ] });
186
+ }
187
+ function FeedbackPublicDetail({
188
+ row,
189
+ comments,
190
+ onVote,
191
+ isAdmin = false,
192
+ onComment,
193
+ onHide,
194
+ onDelete,
195
+ pending,
196
+ commit,
197
+ voteDisabled,
198
+ renderDate: renderDateProp,
199
+ now,
200
+ labels: labelsProp,
201
+ commentLabels,
202
+ className
203
+ }) {
204
+ const labels = useFeedbackPublicLabels(labelsProp);
205
+ const renderDate = useFeedbackRenderDate(renderDateProp);
206
+ return /* @__PURE__ */ jsxs(FeedbackDetail, { className, children: [
207
+ /* @__PURE__ */ jsxs("div", { className: "flex flex-wrap items-center gap-x-3 gap-y-2", "data-feedback-public-detail": row.id, children: [
208
+ /* @__PURE__ */ jsx("span", { className: "text-sm text-secondary", "data-feedback-author": "", children: labels.filedBy(row.author_name ?? labels.anonymous) }),
209
+ /* @__PURE__ */ jsx(PublicDate, { value: row.created_at, renderDate }),
210
+ /* @__PURE__ */ jsx(
211
+ FeedbackVoteButton,
212
+ {
213
+ votes: row.votes,
214
+ voted: row.voted,
215
+ own: row.mine,
216
+ onVote,
217
+ disabled: voteDisabled,
218
+ commit,
219
+ className: "ms-auto"
220
+ }
221
+ )
222
+ ] }),
223
+ /* @__PURE__ */ jsx(FeedbackDetailSection, { title: labels.body, children: /* @__PURE__ */ jsx(FeedbackProse, { children: row.body }) }),
224
+ /* @__PURE__ */ jsx(FeedbackDetailSection, { title: labels.conversation, children: /* @__PURE__ */ jsx(
225
+ FeedbackCommentThread,
226
+ {
227
+ replies: row.replies,
228
+ comments: comments ?? [],
229
+ loading: comments === void 0,
230
+ isAdmin,
231
+ onComment,
232
+ onHide,
233
+ onDelete,
234
+ pending,
235
+ commit,
236
+ now,
237
+ labels: commentLabels
238
+ }
239
+ ) })
240
+ ] });
241
+ }
242
+ export {
243
+ DEFAULT_FEEDBACK_PUBLIC_LABELS,
244
+ FeedbackPublicDetail,
245
+ FeedbackPublicList,
246
+ useFeedbackPublicLabels
247
+ };
248
+ //# sourceMappingURL=feedback-public-list.js.map
@@ -0,0 +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":[]}
@@ -0,0 +1,51 @@
1
+ import type { CommitScope } from "../components/write-lock.js";
2
+ import { type LabelOverride } from "../i18n/kit-labels.js";
3
+ import { type FeedbackRecord } from "./feedback-record.js";
4
+ import { type FeedbackPublicLabels } from "./feedback-public-labels.js";
5
+ /** What the switch reads off a report: {@link FeedbackRecord}'s fields (§9.3). */
6
+ export type FeedbackPublicSwitchRow = Pick<FeedbackRecord, "user_id" | "category" | "status" | "public" | "filed_private" | "mine">;
7
+ /** Who is looking: the app's admin check, and the signed-in user's id. */
8
+ export interface FeedbackPublicSwitchViewer {
9
+ isAdmin: boolean;
10
+ viewerId?: number | null;
11
+ }
12
+ /**
13
+ * Whether the viewer may flip a report's `public` flag — the server's rule (§9.3), so the
14
+ * switch never offers what the `PATCH {public}` would refuse with a 403:
15
+ *
16
+ * - never on a CRASH (decision 9: a crash is private whatever its flag);
17
+ * - an admin, at any status;
18
+ * - the author while the status is OPEN, READY or IN_PROGRESS
19
+ * (`FEEDBACK_AUTHOR_EDITABLE_STATUSES`), and on a `filed_private` report — one the
20
+ * 0.38 migration made public — at any status: that is the opt-out the notice offers.
21
+ */
22
+ export declare function feedbackPublicSwitchAllowed(row: FeedbackPublicSwitchRow, { isAdmin, viewerId }: FeedbackPublicSwitchViewer): boolean;
23
+ export interface FeedbackPublicSwitchProps extends FeedbackPublicSwitchViewer {
24
+ row: FeedbackPublicSwitchRow;
25
+ /** `PATCH /feedback/{id} {"public": next}` — the app's mutation. */
26
+ onChange: (next: boolean) => void;
27
+ /** The PATCH is in flight: the switch holds still. */
28
+ pending?: boolean;
29
+ /** Flipping it SAVES: pass `true` to put it under the page's `WriteLockProvider`
30
+ * (Switch's `commit`). Off by default, as on the inbox's status buttons. */
31
+ commit?: CommitScope;
32
+ /** Over the `feedbackPublic` namespace (`visibleToOthers`, `visibleHint`,
33
+ * `visibleLocked`). */
34
+ labels?: LabelOverride<FeedbackPublicLabels>;
35
+ className?: string;
36
+ }
37
+ /**
38
+ * **Visible to others** — a report's `public` flag (round 0.38, docs/feedback-harmonization.md
39
+ * §9.3, decision 6), for the row detail (`FeedbackRowDetail`'s `onPublicChange`) or an
40
+ * app's own page.
41
+ *
42
+ * - Drawn for the admin and the author only: nobody else is sent `public` (§9.10). Not
43
+ * drawn on a CRASH, and not when the row carries no `public` at all (a server before
44
+ * 0.38, or an app without public parts — kastlan).
45
+ * - The author past the open window sees it **locked with the reason** ("Only while the
46
+ * report is open"), focusable, so the reason reaches a keyboard; a `filed_private`
47
+ * report is never locked for its author ({@link feedbackPublicSwitchAllowed}).
48
+ * - The line under it says what "visible" means: the title and the text, never the
49
+ * screenshots or files (decision 1).
50
+ */
51
+ export declare function FeedbackPublicSwitch({ row, isAdmin, viewerId, onChange, pending, commit, labels: labelsProp, className, }: FeedbackPublicSwitchProps): import("react").JSX.Element | null;