@eifi1/ui-kit 0.37.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 (122) hide show
  1. package/README.md +11 -4
  2. package/dist/eslint/index.js +1 -1
  3. package/dist/feedback/feedback-comment-thread.d.ts +80 -0
  4. package/dist/feedback/feedback-comment-thread.js +162 -0
  5. package/dist/feedback/feedback-comment-thread.js.map +1 -0
  6. package/dist/feedback/feedback-dialog-labels.js +2 -1
  7. package/dist/feedback/feedback-dialog-labels.js.map +1 -1
  8. package/dist/feedback/feedback-dialog.d.ts +13 -0
  9. package/dist/feedback/feedback-dialog.js +12 -4
  10. package/dist/feedback/feedback-dialog.js.map +1 -1
  11. package/dist/feedback/feedback-menu-labels.js +2 -1
  12. package/dist/feedback/feedback-menu-labels.js.map +1 -1
  13. package/dist/feedback/feedback-menu.d.ts +17 -3
  14. package/dist/feedback/feedback-menu.js +11 -1
  15. package/dist/feedback/feedback-menu.js.map +1 -1
  16. package/dist/feedback/feedback-public-labels.d.ts +50 -0
  17. package/dist/feedback/feedback-public-labels.js +26 -0
  18. package/dist/feedback/feedback-public-labels.js.map +1 -0
  19. package/dist/feedback/feedback-public-list.d.ts +118 -0
  20. package/dist/feedback/feedback-public-list.js +248 -0
  21. package/dist/feedback/feedback-public-list.js.map +1 -0
  22. package/dist/feedback/feedback-public-switch.d.ts +51 -0
  23. package/dist/feedback/feedback-public-switch.js +50 -0
  24. package/dist/feedback/feedback-public-switch.js.map +1 -0
  25. package/dist/feedback/feedback-public-types.d.ts +75 -0
  26. package/dist/feedback/feedback-public-types.js +2 -0
  27. package/dist/feedback/feedback-public-types.js.map +1 -0
  28. package/dist/feedback/feedback-record.d.ts +32 -1
  29. package/dist/feedback/feedback-record.js +10 -0
  30. package/dist/feedback/feedback-record.js.map +1 -1
  31. package/dist/feedback/feedback-replies.d.ts +97 -0
  32. package/dist/feedback/feedback-replies.js +114 -0
  33. package/dist/feedback/feedback-replies.js.map +1 -0
  34. package/dist/feedback/feedback-row-detail-labels.js +1 -0
  35. package/dist/feedback/feedback-row-detail-labels.js.map +1 -1
  36. package/dist/feedback/feedback-row-detail.d.ts +38 -10
  37. package/dist/feedback/feedback-row-detail.js +50 -50
  38. package/dist/feedback/feedback-row-detail.js.map +1 -1
  39. package/dist/feedback/feedback-submit-dialog.d.ts +3 -1
  40. package/dist/feedback/feedback-submit-dialog.js +6 -1
  41. package/dist/feedback/feedback-submit-dialog.js.map +1 -1
  42. package/dist/feedback/feedback-submit.d.ts +8 -0
  43. package/dist/feedback/feedback-submit.js +3 -2
  44. package/dist/feedback/feedback-submit.js.map +1 -1
  45. package/dist/feedback/feedback-table.d.ts +15 -5
  46. package/dist/feedback/feedback-table.js +46 -5
  47. package/dist/feedback/feedback-table.js.map +1 -1
  48. package/dist/feedback/feedback-thread-parts-labels.d.ts +6 -0
  49. package/dist/feedback/feedback-thread-parts-labels.js +36 -0
  50. package/dist/feedback/feedback-thread-parts-labels.js.map +1 -0
  51. package/dist/feedback/feedback-thread.d.ts +8 -0
  52. package/dist/feedback/feedback-thread.js +7 -2
  53. package/dist/feedback/feedback-thread.js.map +1 -1
  54. package/dist/feedback/feedback-vote-button.d.ts +48 -0
  55. package/dist/feedback/feedback-vote-button.js +72 -0
  56. package/dist/feedback/feedback-vote-button.js.map +1 -0
  57. package/dist/feedback.d.ts +6 -0
  58. package/dist/feedback.js +6 -0
  59. package/dist/feedback.js.map +1 -1
  60. package/dist/i18n/defaults.js +12 -0
  61. package/dist/i18n/defaults.js.map +1 -1
  62. package/dist/i18n/german.js +64 -2
  63. package/dist/i18n/german.js.map +1 -1
  64. package/dist/i18n/kit-labels.d.ts +16 -0
  65. package/dist/i18n/kit-labels.js.map +1 -1
  66. package/dist/i18n/locales/en.js +79 -2
  67. package/dist/i18n/locales/en.js.map +3 -3
  68. package/dist/i18n/locales/es.js +64 -2
  69. package/dist/i18n/locales/es.js.map +1 -1
  70. package/dist/i18n/locales/fr.js +64 -2
  71. package/dist/i18n/locales/fr.js.map +1 -1
  72. package/dist/i18n/locales/hu.js +64 -2
  73. package/dist/i18n/locales/hu.js.map +1 -1
  74. package/dist/i18n/locales/it.js +64 -2
  75. package/dist/i18n/locales/it.js.map +1 -1
  76. package/dist/i18n/locales/zh.js +64 -2
  77. package/dist/i18n/locales/zh.js.map +1 -1
  78. package/dist/i18n/review.js +4 -0
  79. package/dist/i18n/review.js.map +1 -1
  80. package/dist/index.d.ts +2 -0
  81. package/dist/index.js +6 -0
  82. package/dist/index.js.map +1 -1
  83. package/dist/notifications/feedback-notification-types-labels.d.ts +15 -0
  84. package/dist/notifications/feedback-notification-types-labels.js +11 -0
  85. package/dist/notifications/feedback-notification-types-labels.js.map +1 -0
  86. package/dist/notifications/feedback-notification-types.d.ts +28 -0
  87. package/dist/notifications/feedback-notification-types.js +21 -0
  88. package/dist/notifications/feedback-notification-types.js.map +1 -0
  89. package/package.json +1 -1
  90. package/src/feedback/feedback-comment-thread.tsx +255 -0
  91. package/src/feedback/feedback-dialog-labels.ts +1 -0
  92. package/src/feedback/feedback-dialog.tsx +30 -2
  93. package/src/feedback/feedback-menu-labels.ts +1 -0
  94. package/src/feedback/feedback-menu.tsx +29 -3
  95. package/src/feedback/feedback-public-labels.ts +76 -0
  96. package/src/feedback/feedback-public-list.tsx +395 -0
  97. package/src/feedback/feedback-public-switch.tsx +102 -0
  98. package/src/feedback/feedback-public-types.ts +82 -0
  99. package/src/feedback/feedback-record.ts +39 -1
  100. package/src/feedback/feedback-replies.tsx +235 -0
  101. package/src/feedback/feedback-row-detail-labels.ts +1 -0
  102. package/src/feedback/feedback-row-detail.tsx +103 -74
  103. package/src/feedback/feedback-submit-dialog.tsx +9 -0
  104. package/src/feedback/feedback-submit.tsx +10 -1
  105. package/src/feedback/feedback-table.tsx +73 -6
  106. package/src/feedback/feedback-thread-parts-labels.ts +41 -0
  107. package/src/feedback/feedback-thread.tsx +18 -1
  108. package/src/feedback/feedback-vote-button.tsx +122 -0
  109. package/src/feedback.ts +10 -0
  110. package/src/i18n/defaults.ts +12 -0
  111. package/src/i18n/german.ts +62 -0
  112. package/src/i18n/kit-labels.tsx +16 -0
  113. package/src/i18n/locales/en.ts +7 -0
  114. package/src/i18n/locales/es.ts +62 -0
  115. package/src/i18n/locales/fr.ts +62 -0
  116. package/src/i18n/locales/hu.ts +62 -0
  117. package/src/i18n/locales/it.ts +62 -0
  118. package/src/i18n/locales/zh.ts +62 -0
  119. package/src/i18n/review.ts +4 -0
  120. package/src/index.ts +9 -0
  121. package/src/notifications/feedback-notification-types-labels.ts +24 -0
  122. package/src/notifications/feedback-notification-types.ts +46 -0
@@ -1,6 +1,6 @@
1
1
  import { useMemo } from "react";
2
2
  import type { MouseEvent, ReactElement, ReactNode } from "react";
3
- import { ExternalLink } from "lucide-react";
3
+ import { ExternalLink, ThumbsUp } from "lucide-react";
4
4
  import { cn } from "../lib/cn";
5
5
  import { toLocalIso } from "../lib/dates";
6
6
  import { EMPTY_FORMATTED_VALUE, formatDate, toDate } from "../lib/format";
@@ -33,6 +33,7 @@ import type { FeedbackStatusChange } from "./feedback-status-undo";
33
33
  import { feedbackPageHref, feedbackPagePath } from "./feedback-row-detail";
34
34
  import type { CommitScope } from "../components/write-lock";
35
35
  import { DEFAULT_FEEDBACK_PAGE_LABELS } from "./feedback-table-labels";
36
+ import { DEFAULT_FEEDBACK_PUBLIC_LABELS } from "./feedback-public-labels";
36
37
 
37
38
  /**
38
39
  * The two feedback pages' TABLE — `/feedback` (the admin inbox) and `/my-feedback` (the
@@ -135,7 +136,17 @@ export function useFeedbackPageLabels(labels?: LabelOverride<FeedbackPageLabels>
135
136
  * row type with more of them passes as it is. */
136
137
  export type FeedbackTableRow = Pick<
137
138
  FeedbackRecord,
138
- "id" | "user_id" | "user_email" | "title" | "body" | "category" | "status" | "context" | "resolved_at" | "created_at"
139
+ | "id"
140
+ | "user_id"
141
+ | "user_email"
142
+ | "title"
143
+ | "body"
144
+ | "category"
145
+ | "status"
146
+ | "context"
147
+ | "resolved_at"
148
+ | "created_at"
149
+ | "votes"
139
150
  >;
140
151
 
141
152
  /** A context value as a trimmed string, or `""` — every key is optional on read (§3.2)
@@ -388,6 +399,23 @@ export interface FeedbackColumnsOptions {
388
399
  labels: FeedbackPageLabels;
389
400
  statusLabels: FeedbackStatusLabels;
390
401
  categoryLabels: FeedbackCategoryLabels;
402
+ /** 0.38 (§9.6): a sortable **Votes** column before Status — `row.votes`, a missing
403
+ * one as 0 — hidden on the phone (the card's chip, {@link FeedbackMobileCardProps.votes},
404
+ * carries it there). Off by default. */
405
+ votes?: boolean;
406
+ /** The Votes header — `feedbackPublic.columnVotes`, so the inbox and the public list
407
+ * say the same ({@link useFeedbackColumns} reads it). Default "Votes". */
408
+ votesHeader?: string;
409
+ }
410
+
411
+ /** The vote count, a quiet chip with the thumb (§9.6) — the Votes cell and the card's. */
412
+ function FeedbackVoteCount({ votes, className }: { votes: number | undefined; className?: string }) {
413
+ return (
414
+ <Chip size="sm" className={className} data-feedback-vote-count={votes ?? 0}>
415
+ <ThumbsUp className="size-3" aria-hidden />
416
+ <span className="tabular-nums">{votes ?? 0}</span>
417
+ </Chip>
418
+ );
391
419
  }
392
420
 
393
421
  /** A status's place in the chain, unknown ones last (as `feedbackCategoryRank`). */
@@ -434,8 +462,23 @@ export function feedbackColumns<T extends FeedbackTableRow>({
434
462
  labels,
435
463
  statusLabels,
436
464
  categoryLabels,
465
+ votes = false,
466
+ votesHeader = DEFAULT_FEEDBACK_PUBLIC_LABELS.columnVotes,
437
467
  }: FeedbackColumnsOptions): DataTableColumn<T>[] {
438
468
  const editable = canEdit && !mine;
469
+ const votesColumn: DataTableColumn<T>[] = votes
470
+ ? [
471
+ {
472
+ key: "votes",
473
+ header: votesHeader,
474
+ className: "whitespace-nowrap",
475
+ cell: (row) => <FeedbackVoteCount votes={row.votes} />,
476
+ sortBy: (row) => row.votes ?? 0,
477
+ filter: { type: "number", getValue: (row) => row.votes ?? 0 },
478
+ mobileHidden: true,
479
+ },
480
+ ]
481
+ : [];
439
482
  return [
440
483
  {
441
484
  key: "id",
@@ -521,6 +564,7 @@ export function feedbackColumns<T extends FeedbackTableRow>({
521
564
  filterBy: (row) => feedbackPagePath(row.context) ?? "",
522
565
  mobileHidden: true,
523
566
  },
567
+ ...votesColumn,
524
568
  {
525
569
  key: "status",
526
570
  header: labels.columnStatus,
@@ -560,7 +604,7 @@ export function feedbackColumns<T extends FeedbackTableRow>({
560
604
  /** What {@link useFeedbackColumns} takes: {@link FeedbackColumnsOptions} with the words
561
605
  * read from the provider, and `renderDate` optional. */
562
606
  export interface UseFeedbackColumnsOptions
563
- extends Omit<FeedbackColumnsOptions, "renderDate" | "labels" | "statusLabels" | "categoryLabels"> {
607
+ extends Omit<FeedbackColumnsOptions, "renderDate" | "labels" | "statusLabels" | "categoryLabels" | "votesHeader"> {
564
608
  /** Default: the kit's date with the weekday ({@link useFeedbackRenderDate}). */
565
609
  renderDate?: FeedbackRenderDate;
566
610
  /** Over the `feedbackPage` namespace. Keep it stable (module scope or memoised): it is
@@ -580,16 +624,29 @@ export function useFeedbackColumns<T extends FeedbackTableRow>({
580
624
  onStatus,
581
625
  renderDate: renderDateProp,
582
626
  commit,
627
+ votes,
583
628
  labels: labelsProp,
584
629
  }: UseFeedbackColumnsOptions): DataTableColumn<T>[] {
585
630
  const labels = useFeedbackPageLabels(labelsProp);
586
631
  const statusLabels = useFeedbackStatusLabels();
587
632
  const categoryLabels = useFeedbackCategoryLabels();
633
+ const votesHeader = useKitLabels("feedbackPublic", DEFAULT_FEEDBACK_PUBLIC_LABELS).columnVotes;
588
634
  const renderDate = useFeedbackRenderDate(renderDateProp);
589
635
  return useMemo(
590
636
  () =>
591
- feedbackColumns<T>({ mine, canEdit, onStatus, renderDate, commit, labels, statusLabels, categoryLabels }),
592
- [mine, canEdit, onStatus, renderDate, commit, labels, statusLabels, categoryLabels],
637
+ feedbackColumns<T>({
638
+ mine,
639
+ canEdit,
640
+ onStatus,
641
+ renderDate,
642
+ commit,
643
+ labels,
644
+ statusLabels,
645
+ categoryLabels,
646
+ votes,
647
+ votesHeader,
648
+ }),
649
+ [mine, canEdit, onStatus, renderDate, commit, labels, statusLabels, categoryLabels, votes, votesHeader],
593
650
  );
594
651
  }
595
652
 
@@ -617,6 +674,9 @@ export interface FeedbackMobileCardProps {
617
674
  /** See {@link FeedbackRenderDate}; the same function as the columns' and the day
618
675
  * headers'. Default: the kit's ({@link useFeedbackRenderDate}). */
619
676
  renderDate?: FeedbackRenderDate;
677
+ /** 0.38: the vote count as a chip beside the category — with `useFeedbackColumns`'
678
+ * `votes`, whose column the phone hides. */
679
+ votes?: boolean;
620
680
  /** Over the `feedbackPage` namespace. */
621
681
  labels?: LabelOverride<FeedbackPageLabels>;
622
682
  }
@@ -631,7 +691,13 @@ export interface FeedbackMobileCardProps {
631
691
  * chips itself: with a `mobileCard`, DataTable never renders the `mobilePrimary` cell,
632
692
  * which is how keksdose's cards lost them.
633
693
  */
634
- export function FeedbackMobileCard({ row, showSubmitter = false, renderDate: renderDateProp, labels: labelsProp }: FeedbackMobileCardProps) {
694
+ export function FeedbackMobileCard({
695
+ row,
696
+ showSubmitter = false,
697
+ renderDate: renderDateProp,
698
+ votes = false,
699
+ labels: labelsProp,
700
+ }: FeedbackMobileCardProps) {
635
701
  const labels = useFeedbackPageLabels(labelsProp);
636
702
  const renderDate = useFeedbackRenderDate(renderDateProp);
637
703
  return (
@@ -644,6 +710,7 @@ export function FeedbackMobileCard({ row, showSubmitter = false, renderDate: ren
644
710
  </div>
645
711
  <div className="flex min-w-0 flex-wrap items-center gap-x-2 gap-y-0.5 text-xs text-muted">
646
712
  <FeedbackCategoryBadge category={row.category} compact />
713
+ {votes && <FeedbackVoteCount votes={row.votes} />}
647
714
  <FeedbackDate value={row.created_at} renderDate={renderDate} />
648
715
  {showSubmitter && (
649
716
  // The short id form: "#7" says enough on a card that has room for little.
@@ -0,0 +1,41 @@
1
+ // The English words of round 0.38's thread parts — ./feedback-vote-button,
2
+ // ./feedback-comment-thread and ./feedback-replies — in a module with no component code:
3
+ // the English catalogue (src/i18n/defaults.ts) imports them without the components, so
4
+ // `en` loads as one chunk like the other languages (0.34, KS U8). Each component module
5
+ // re-exports its own.
6
+
7
+ import type { FeedbackCommentsLabels } from "./feedback-comment-thread";
8
+ import type { FeedbackRepliesLabels } from "./feedback-replies";
9
+ import type { FeedbackVoteLabels } from "./feedback-vote-button";
10
+
11
+ export const DEFAULT_FEEDBACK_VOTE_LABELS: FeedbackVoteLabels = {
12
+ vote: "I want this too",
13
+ own: "Your report",
14
+ };
15
+
16
+ export const DEFAULT_FEEDBACK_REPLIES_LABELS: FeedbackRepliesLabels = {
17
+ replies: "Replies",
18
+ empty: "No reply yet",
19
+ admin: "Admin",
20
+ statusChanged: "Status changed to",
21
+ field: "Write a reply",
22
+ placeholder: "Write a reply…",
23
+ reply: "Reply",
24
+ status: "Status with the reply",
25
+ noChange: "No change",
26
+ nudge: "The author has not been told. Add a reply.",
27
+ };
28
+
29
+ export const DEFAULT_FEEDBACK_COMMENTS_LABELS: FeedbackCommentsLabels = {
30
+ comments: "Comments",
31
+ empty: "No comments yet",
32
+ hidden: "Hidden by an admin",
33
+ hide: "Hide",
34
+ unhide: "Unhide",
35
+ delete: "Delete",
36
+ deleteTitle: "Delete this comment?",
37
+ deleteBody: "It is removed for everyone. This cannot be undone.",
38
+ field: "Write a comment",
39
+ placeholder: "Write a comment…",
40
+ send: "Comment",
41
+ };
@@ -119,6 +119,14 @@ export interface FeedbackThreadMessage {
119
119
  own?: boolean;
120
120
  /** Written by the team rather than a user: gets the "Staff" badge. */
121
121
  staff?: boolean;
122
+ /** 0.38: beside the author and time — a reply's "→ Done" status chip. Not
123
+ * `data-private`: it is the app's, not somebody's words. */
124
+ badge?: ReactNode;
125
+ /** 0.38: the body struck through and muted — a hidden comment as its admin sees it. */
126
+ struck?: boolean;
127
+ /** 0.38: under the body — a note ("Hidden by an admin") and the message's actions
128
+ * (Hide, Delete). Not `data-private`. */
129
+ footer?: ReactNode;
122
130
  }
123
131
 
124
132
  /**
@@ -296,13 +304,17 @@ export function FeedbackThread({
296
304
  {labels.staff}
297
305
  </span>
298
306
  )}
307
+ {message.badge}
299
308
  {time}
300
309
  </header>
301
310
  {message.body && (
302
311
  <p
303
312
  data-private={priv}
304
313
  dir="auto"
305
- className="whitespace-pre-wrap break-words text-start text-primary"
314
+ className={cn(
315
+ "whitespace-pre-wrap break-words text-start",
316
+ message.struck ? "text-muted line-through" : "text-primary",
317
+ )}
306
318
  >
307
319
  {message.body}
308
320
  </p>
@@ -320,6 +332,11 @@ export function FeedbackThread({
320
332
  ))}
321
333
  </ul>
322
334
  )}
335
+ {message.footer != null && (
336
+ <div className="mt-1.5 flex flex-wrap items-center gap-x-2 gap-y-1 text-caption text-muted">
337
+ {message.footer}
338
+ </div>
339
+ )}
323
340
  </article>
324
341
  </li>,
325
342
  );
@@ -0,0 +1,122 @@
1
+ import { useRef, useState } from "react";
2
+ import { ThumbsUp } from "lucide-react";
3
+ import { Button } from "../components/button";
4
+ import { Tooltip } from "../components/tooltip";
5
+ import type { CommitScope } from "../components/write-lock";
6
+ import { useKitLabels, useKitLocale, type LabelOverride } from "../i18n/kit-labels";
7
+ import { cn } from "../lib/cn";
8
+ import { formatNumber } from "../lib/format";
9
+ import type { FeedbackVoteState } from "./feedback-public-types";
10
+ import { DEFAULT_FEEDBACK_VOTE_LABELS } from "./feedback-thread-parts-labels";
11
+
12
+ /**
13
+ * "I want this too" — the one rating a report has (docs/feedback-harmonization.md §9.6,
14
+ * decision 2): an upvote, one per person, a count. No down-vote, no stars.
15
+ *
16
+ * - **A toggle** (`aria-pressed`): the thumb, the count, and "I want this too" as its
17
+ * name — written out at `size="md"`, the Tooltip and the accessible name at `"sm"`
18
+ * (a list row's cell).
19
+ * - **Optimistic with rollback:** the press shows the new state at once, `onVote(next)`
20
+ * sends `PUT` (`true`) or `DELETE` (`false`) and its answer — the server's count —
21
+ * replaces the guess; a rejection puts back what was there. The error is the app's
22
+ * to toast (its mutation's `onError`), as with every kit part that takes a promise.
23
+ * - **The author's own report** counts as their vote (§9.6, decision 20): `own` shows it
24
+ * pressed and `aria-disabled` with "Your report" as the reason (the server would
25
+ * answer 409).
26
+ * - A new `votes` / `voted` from the app (a refetch) wins over the last answer.
27
+ */
28
+
29
+ /** The `feedbackVote` namespace of `<UiKitProvider labels>`. */
30
+ export interface FeedbackVoteLabels {
31
+ /** The button's name. de-CH: "Will ich auch" */
32
+ vote: string;
33
+ /** Why the author's own button is disabled. de-CH: "Ihr Bericht" */
34
+ own: string;
35
+ }
36
+
37
+ export { DEFAULT_FEEDBACK_VOTE_LABELS };
38
+
39
+ export interface FeedbackVoteButtonProps {
40
+ /** People who want this — the author's own vote included. */
41
+ votes: number;
42
+ /** The viewer has voted. */
43
+ voted: boolean;
44
+ /** The viewer filed it: pressed, disabled, "Your report". */
45
+ own?: boolean;
46
+ /** `PUT` (`next` true) or `DELETE` (`false`) `/feedback/{id}/vote`; resolve with the
47
+ * answer, reject to roll back. */
48
+ onVote: (next: boolean) => Promise<FeedbackVoteState>;
49
+ /** Not now (e.g. a demo): plain `disabled`. A string is the reason, as Button's. */
50
+ disabled?: boolean | string;
51
+ /** The vote COMMITS — see Button's `commit`. */
52
+ commit?: CommitScope;
53
+ /** `"sm"`: the count alone, for a table cell. Default `"md"`. */
54
+ size?: "sm" | "md";
55
+ labels?: LabelOverride<FeedbackVoteLabels>;
56
+ className?: string;
57
+ }
58
+
59
+ export function FeedbackVoteButton({
60
+ votes,
61
+ voted,
62
+ own = false,
63
+ onVote,
64
+ disabled = false,
65
+ commit,
66
+ size = "md",
67
+ labels: labelsProp,
68
+ className,
69
+ }: FeedbackVoteButtonProps) {
70
+ const labels = useKitLabels("feedbackVote", DEFAULT_FEEDBACK_VOTE_LABELS, labelsProp);
71
+ const locale = useKitLocale();
72
+ // The last press's state, valid only while the app's props are still the ones it was
73
+ // pressed against: a refetch that brings new numbers is the truth.
74
+ const [shown, setShown] = useState<{ base: FeedbackVoteState; state: FeedbackVoteState } | null>(null);
75
+ const busy = useRef(false);
76
+ const current =
77
+ shown && shown.base.votes === votes && shown.base.voted === voted ? shown.state : { votes, voted };
78
+ const pressed = own || current.voted;
79
+
80
+ const press = () => {
81
+ if (own || disabled || busy.current) return;
82
+ const base = { votes, voted };
83
+ const before = current;
84
+ const next = !before.voted;
85
+ busy.current = true;
86
+ setShown({ base, state: { votes: Math.max(0, before.votes + (next ? 1 : -1)), voted: next } });
87
+ onVote(next).then(
88
+ (answer) => setShown({ base, state: { votes: answer.votes, voted: answer.voted } }),
89
+ () => setShown({ base, state: before }),
90
+ ).finally(() => {
91
+ busy.current = false;
92
+ });
93
+ };
94
+
95
+ const count = formatNumber(current.votes, { locale });
96
+ const button = (
97
+ <Button
98
+ type="button"
99
+ variant="secondary"
100
+ size="sm"
101
+ pressed={pressed}
102
+ commit={own ? undefined : commit}
103
+ disabled={disabled === true}
104
+ disabledReason={own ? labels.own : typeof disabled === "string" ? disabled : undefined}
105
+ onClick={(event) => {
106
+ // A row around it may expand on a click.
107
+ event.stopPropagation();
108
+ press();
109
+ }}
110
+ data-feedback-vote=""
111
+ className={cn("gap-1.5", className)}
112
+ >
113
+ <ThumbsUp className="size-3.5 shrink-0" aria-hidden />
114
+ <span className={size === "sm" ? "sr-only" : undefined}>{labels.vote}</span>
115
+ <span className="tabular-nums" data-feedback-votes="">
116
+ {count}
117
+ </span>
118
+ </Button>
119
+ );
120
+ // At "sm" the name is not on screen: the Tooltip shows it (the own reason has its own).
121
+ return size === "sm" && !own ? <Tooltip label={labels.vote}>{button}</Tooltip> : button;
122
+ }
package/src/feedback.ts CHANGED
@@ -20,3 +20,13 @@ export * from "./feedback/feedback-submit";
20
20
  export * from "./feedback/feedback-table";
21
21
  export * from "./feedback/feedback-row-detail";
22
22
  export * from "./feedback/feedback-crash";
23
+ // 0.38.0: the public pages (§9.15) — the "Visible to others" switch, the list every user
24
+ // reads and one report opened.
25
+ export * from "./feedback/feedback-public-switch";
26
+ export * from "./feedback/feedback-public-list";
27
+ // 0.38.0: public feedback (docs/feedback-harmonization.md §9) — the shapes, the vote, the
28
+ // comment thread, the replies' timeline and composer.
29
+ export * from "./feedback/feedback-public-types";
30
+ export * from "./feedback/feedback-vote-button";
31
+ export * from "./feedback/feedback-comment-thread";
32
+ export * from "./feedback/feedback-replies";
@@ -41,9 +41,16 @@ import {
41
41
  DEFAULT_FEEDBACK_TOAST_LABELS,
42
42
  } from "../feedback/feedback-labels";
43
43
  import { DEFAULT_FEEDBACK_MENU_LABELS } from "../feedback/feedback-menu-labels";
44
+ import { DEFAULT_FEEDBACK_PUBLIC_LABELS } from "../feedback/feedback-public-labels";
45
+ import { DEFAULT_FEEDBACK_NOTIFICATION_TYPES_LABELS } from "../notifications/feedback-notification-types-labels";
44
46
  import { DEFAULT_FEEDBACK_CONTEXT_LABELS } from "../feedback/feedback-context-labels";
45
47
  import { DEFAULT_FEEDBACK_PAGE_LABELS } from "../feedback/feedback-table-labels";
46
48
  import { DEFAULT_FEEDBACK_DETAIL_LABELS } from "../feedback/feedback-row-detail-labels";
49
+ import {
50
+ DEFAULT_FEEDBACK_COMMENTS_LABELS,
51
+ DEFAULT_FEEDBACK_REPLIES_LABELS,
52
+ DEFAULT_FEEDBACK_VOTE_LABELS,
53
+ } from "../feedback/feedback-thread-parts-labels";
47
54
  import { DEFAULT_ACCOUNT_SETTINGS_LABELS } from "../account/account-settings-labels";
48
55
  import { DEFAULT_COPY_BUTTON_LABELS } from "../components/copy-button-labels";
49
56
  import { DEFAULT_BULK_ACTION_BAR_LABELS } from "../components/bulk-action-bar-labels";
@@ -180,9 +187,14 @@ export const DEFAULT_UI_KIT_LABELS: UiKitLabels = {
180
187
  feedbackCategory: DEFAULT_FEEDBACK_CATEGORY_LABELS,
181
188
  feedbackToast: DEFAULT_FEEDBACK_TOAST_LABELS,
182
189
  feedbackMenu: DEFAULT_FEEDBACK_MENU_LABELS,
190
+ feedbackPublic: DEFAULT_FEEDBACK_PUBLIC_LABELS,
191
+ feedbackNotificationTypes: DEFAULT_FEEDBACK_NOTIFICATION_TYPES_LABELS,
183
192
  feedbackContext: DEFAULT_FEEDBACK_CONTEXT_LABELS,
184
193
  feedbackPage: DEFAULT_FEEDBACK_PAGE_LABELS,
185
194
  feedbackDetail: DEFAULT_FEEDBACK_DETAIL_LABELS,
195
+ feedbackVote: DEFAULT_FEEDBACK_VOTE_LABELS,
196
+ feedbackComments: DEFAULT_FEEDBACK_COMMENTS_LABELS,
197
+ feedbackReplies: DEFAULT_FEEDBACK_REPLIES_LABELS,
186
198
  accountSettings: DEFAULT_ACCOUNT_SETTINGS_LABELS,
187
199
  confirmDialog: DEFAULT_CONFIRM_DIALOG_LABELS,
188
200
  floatingPanel: DEFAULT_FLOATING_PANEL_LABELS,
@@ -59,6 +59,7 @@ export function germanLabels(numberLocale: string): UiKitLabels {
59
59
  submitHint: (apple) => (apple ? "⌘ Enter zum Senden" : "Strg+Enter zum Senden"),
60
60
  cancel: "Abbrechen",
61
61
  save: "Senden",
62
+ publicNotice: "Andere Personen sehen Titel und Text (nie Ihre Screenshots oder Dateien).",
62
63
  },
63
64
  feedbackThread: {
64
65
  thread: "Kommentare",
@@ -117,6 +118,36 @@ export function germanLabels(numberLocale: string): UiKitLabels {
117
118
  trigger: "Feedback senden",
118
119
  myFeedback: "Mein Feedback",
119
120
  viewFeedback: "Feedback ansehen",
121
+ allFeedback: "Alle Rückmeldungen",
122
+ },
123
+ // 0.38.0 — docs/feedback-harmonization.md §9: the public pages and the two feedback
124
+ // notification types. "Rückmeldungen" and "Eine Person" are the contract's (§9.2, §9.5).
125
+ feedbackPublic: {
126
+ title: "Alle Rückmeldungen",
127
+ sort: "Sortieren",
128
+ sortNewest: "Neueste",
129
+ sortVotes: "Meistgewünscht",
130
+ sortUpdated: "Zuletzt aktualisiert",
131
+ search: "Titel durchsuchen",
132
+ columnVotes: "Stimmen",
133
+ total: (count) => (count === 1 ? "1 Meldung" : `${n(count)} Meldungen`),
134
+ comments: (count) => (count === 1 ? "1 Kommentar" : `${n(count)} Kommentare`),
135
+ votes: (count) => (count === 1 ? "1 Stimme" : `${n(count)} Stimmen`),
136
+ anonymous: "Eine Person",
137
+ filedBy: (name) => `Von ${name}`,
138
+ body: "Beschreibung",
139
+ conversation: "Antworten und Kommentare",
140
+ loading: "Wird geladen…",
141
+ empty: "Noch keine Meldungen",
142
+ visibleToOthers: "Für andere sichtbar",
143
+ visibleHint: "Andere Personen sehen Titel und Text, nie Ihre Screenshots oder Dateien.",
144
+ visibleLocked: "Nur solange die Meldung noch offen ist.",
145
+ },
146
+ feedbackNotificationTypes: {
147
+ answered: "Antworten auf Ihre Meldungen",
148
+ answeredDescription: "Wenn eine Meldung, die Sie geschrieben oder kommentiert haben, eine Antwort erhält.",
149
+ comment: "Kommentare zu Meldungen, denen Sie folgen",
150
+ commentDescription: "Wenn jemand eine Meldung kommentiert, die Sie geschrieben oder kommentiert haben.",
120
151
  },
121
152
  feedbackContext: {
122
153
  user: "Nutzer",
@@ -157,6 +188,7 @@ export function germanLabels(numberLocale: string): UiKitLabels {
157
188
  download: (name) => `${name} herunterladen`,
158
189
  downloadFailed: "Der Anhang konnte nicht heruntergeladen werden.",
159
190
  outcome: "Ergebnis",
191
+ replies: "Antworten",
160
192
  resolvedAt: (date) => `Erledigt am: ${date}`,
161
193
  outcomeAdd: "Ergebnis hinzufügen",
162
194
  outcomeUpdate: "Aktualisieren",
@@ -170,6 +202,36 @@ export function germanLabels(numberLocale: string): UiKitLabels {
170
202
  status: "Status",
171
203
  note: "Notiz",
172
204
  },
205
+ // 0.38.0 — docs/feedback-harmonization.md §9.15: the vote, the comment thread, the replies.
206
+ feedbackVote: {
207
+ vote: "Will ich auch",
208
+ own: "Ihr Bericht",
209
+ },
210
+ feedbackComments: {
211
+ comments: "Kommentare",
212
+ empty: "Noch keine Kommentare",
213
+ hidden: "Von einem Admin ausgeblendet",
214
+ hide: "Ausblenden",
215
+ unhide: "Einblenden",
216
+ delete: "Löschen",
217
+ deleteTitle: "Diesen Kommentar löschen?",
218
+ deleteBody: "Er wird für alle entfernt. Das kann nicht rückgängig gemacht werden.",
219
+ field: "Kommentar schreiben",
220
+ placeholder: "Kommentar schreiben…",
221
+ send: "Kommentieren",
222
+ },
223
+ feedbackReplies: {
224
+ replies: "Antworten",
225
+ empty: "Noch keine Antwort",
226
+ admin: "Admin",
227
+ statusChanged: "Status geändert auf",
228
+ field: "Antwort schreiben",
229
+ placeholder: "Antwort schreiben…",
230
+ reply: "Antworten",
231
+ status: "Status mit der Antwort",
232
+ noChange: "Keine Änderung",
233
+ nudge: "Die meldende Person wurde nicht benachrichtigt. Schreiben Sie eine Antwort.",
234
+ },
173
235
  accountSettings: {
174
236
  profile: {
175
237
  title: "Profil",
@@ -50,9 +50,14 @@ import type {
50
50
  FeedbackToastLabels,
51
51
  } from "../feedback/feedback-labels";
52
52
  import type { FeedbackMenuLabels } from "../feedback/feedback-menu";
53
+ import type { FeedbackPublicLabels } from "../feedback/feedback-public-labels";
54
+ import type { FeedbackNotificationTypesLabels } from "../notifications/feedback-notification-types-labels";
53
55
  import type { FeedbackContextLabels } from "../feedback/feedback-context";
54
56
  import type { FeedbackPageLabels } from "../feedback/feedback-table";
55
57
  import type { FeedbackDetailLabels } from "../feedback/feedback-row-detail";
58
+ import type { FeedbackVoteLabels } from "../feedback/feedback-vote-button";
59
+ import type { FeedbackCommentsLabels } from "../feedback/feedback-comment-thread";
60
+ import type { FeedbackRepliesLabels } from "../feedback/feedback-replies";
56
61
  import type { AccountSettingsLabels } from "../account/account-settings-labels";
57
62
  import type { ConfirmDialogLabels } from "../components/confirm-dialog";
58
63
  import type { FloatingPanelLabels } from "../components/floating-panel";
@@ -338,9 +343,20 @@ export interface UiKitLabels {
338
343
  feedbackCategory: FeedbackCategoryLabels;
339
344
  feedbackToast: FeedbackToastLabels;
340
345
  feedbackMenu: FeedbackMenuLabels;
346
+ /** 0.38.0: the public feedback pages — `FeedbackPublicList`, `FeedbackPublicDetail`,
347
+ * `FeedbackPublicSwitch` (docs/feedback-harmonization.md §9.15). */
348
+ feedbackPublic: FeedbackPublicLabels;
349
+ /** 0.38.0: `useFeedbackNotificationTypes` — the two feedback types' setting words
350
+ * (§9.11). */
351
+ feedbackNotificationTypes: FeedbackNotificationTypesLabels;
341
352
  feedbackContext: FeedbackContextLabels;
342
353
  feedbackPage: FeedbackPageLabels;
343
354
  feedbackDetail: FeedbackDetailLabels;
355
+ /** 0.38.0: public feedback (docs/feedback-harmonization.md §9.15) — the vote button,
356
+ * the comment thread, the replies' timeline and composer. */
357
+ feedbackVote: FeedbackVoteLabels;
358
+ feedbackComments: FeedbackCommentsLabels;
359
+ feedbackReplies: FeedbackRepliesLabels;
344
360
  /** 0.12.0: `ProfileSetting`, `PasswordSetting`, `TwoFactorSetting`, `PasskeysSetting`
345
361
  * — one record per section. The provider takes a whole section record; the
346
362
  * components merge it key by key, as a prop. */
@@ -48,6 +48,13 @@ export function uiKitLabelsEn(numberLocale = "en-US"): UiKitLabels {
48
48
  // `userFallback`'s number is an id, not a count: it stays as the default prints it.
49
49
  reworkChip: (count) => (count === 1 ? "Rework" : `Rework ×${n(count)}`),
50
50
  },
51
+ // 0.38.0: the public list's two counts.
52
+ feedbackPublic: {
53
+ ...d.feedbackPublic,
54
+ total: (count) => (count === 1 ? "1 report" : `${n(count)} reports`),
55
+ comments: (count) => (count === 1 ? "1 comment" : `${n(count)} comments`),
56
+ votes: (count) => (count === 1 ? "1 vote" : `${n(count)} votes`),
57
+ },
51
58
  measuredGrid: {
52
59
  ...d.measuredGrid,
53
60
  points: (count) => (count === 1 ? "1 point" : `${n(count)} points`),
@@ -55,6 +55,7 @@ export function uiKitLabelsEs(numberLocale = "es-ES"): UiKitLabels {
55
55
  submitHint: (apple) => (apple ? "⌘ Intro para enviar" : "Ctrl+Intro para enviar"),
56
56
  cancel: "Cancelar",
57
57
  save: "Enviar",
58
+ publicNotice: "Otras personas verán el título y el texto (nunca sus capturas ni sus archivos).",
58
59
  },
59
60
  feedbackThread: {
60
61
  thread: "Comentarios",
@@ -108,6 +109,36 @@ export function uiKitLabelsEs(numberLocale = "es-ES"): UiKitLabels {
108
109
  trigger: "Enviar comentarios",
109
110
  myFeedback: "Mis comentarios",
110
111
  viewFeedback: "Ver comentarios",
112
+ allFeedback: "Todos los comentarios",
113
+ },
114
+ // 0.38.0 — docs/feedback-harmonization.md §9: the public pages and the two feedback
115
+ // notification types. "Informe" for one report, so it never reads as "comentario".
116
+ feedbackPublic: {
117
+ title: "Todos los comentarios",
118
+ sort: "Ordenar",
119
+ sortNewest: "Más recientes",
120
+ sortVotes: "Más deseados",
121
+ sortUpdated: "Actualizados recientemente",
122
+ search: "Buscar en los títulos",
123
+ columnVotes: "Votos",
124
+ total: (count) => `${n(count)} ${plural(count, "informe", "informes")}`,
125
+ comments: (count) => `${n(count)} ${plural(count, "comentario", "comentarios")}`,
126
+ votes: (count) => `${n(count)} ${plural(count, "voto", "votos")}`,
127
+ anonymous: "Una persona",
128
+ filedBy: (name) => `De ${name}`,
129
+ body: "Descripción",
130
+ conversation: "Respuestas y comentarios",
131
+ loading: "Cargando…",
132
+ empty: "Todavía no hay informes",
133
+ visibleToOthers: "Visible para otras personas",
134
+ visibleHint: "Otras personas ven el título y el texto, nunca sus capturas ni sus archivos.",
135
+ visibleLocked: "Solo mientras el informe siga abierto.",
136
+ },
137
+ feedbackNotificationTypes: {
138
+ answered: "Respuestas a sus informes",
139
+ answeredDescription: "Cuando un informe que usted escribió o comentó recibe una respuesta.",
140
+ comment: "Comentarios en informes que sigue",
141
+ commentDescription: "Cuando alguien comenta un informe que usted escribió o comentó.",
111
142
  },
112
143
  feedbackContext: {
113
144
  user: "Usuario",
@@ -148,6 +179,7 @@ export function uiKitLabelsEs(numberLocale = "es-ES"): UiKitLabels {
148
179
  download: (name) => `Descargar ${name}`,
149
180
  downloadFailed: "No se ha podido descargar el adjunto.",
150
181
  outcome: "Resultado",
182
+ replies: "Respuestas",
151
183
  resolvedAt: (date) => `Resuelto el ${date}`,
152
184
  outcomeAdd: "Añadir resultado",
153
185
  outcomeUpdate: "Actualizar",
@@ -161,6 +193,36 @@ export function uiKitLabelsEs(numberLocale = "es-ES"): UiKitLabels {
161
193
  status: "Estado",
162
194
  note: "Nota",
163
195
  },
196
+ // 0.38.0 — docs/feedback-harmonization.md §9.15: the vote, the comment thread, the replies.
197
+ feedbackVote: {
198
+ vote: "Yo también lo quiero",
199
+ own: "Su informe",
200
+ },
201
+ feedbackComments: {
202
+ comments: "Comentarios",
203
+ empty: "Aún no hay comentarios",
204
+ hidden: "Ocultado por un administrador",
205
+ hide: "Ocultar",
206
+ unhide: "Mostrar",
207
+ delete: "Eliminar",
208
+ deleteTitle: "¿Eliminar este comentario?",
209
+ deleteBody: "Se eliminará para todos. No se puede deshacer.",
210
+ field: "Escribir un comentario",
211
+ placeholder: "Escriba un comentario…",
212
+ send: "Comentar",
213
+ },
214
+ feedbackReplies: {
215
+ replies: "Respuestas",
216
+ empty: "Aún no hay respuesta",
217
+ admin: "Admin",
218
+ statusChanged: "Estado cambiado a",
219
+ field: "Escribir una respuesta",
220
+ placeholder: "Escriba una respuesta…",
221
+ reply: "Responder",
222
+ status: "Estado con la respuesta",
223
+ noChange: "Sin cambios",
224
+ nudge: "No se ha avisado a quien lo informó. Añada una respuesta.",
225
+ },
164
226
  accountSettings: {
165
227
  profile: {
166
228
  title: "Perfil",