@eifi1/ui-kit 0.37.0 → 0.38.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +11 -4
- package/dist/eslint/index.js +1 -1
- package/dist/feedback/feedback-comment-thread.d.ts +87 -0
- package/dist/feedback/feedback-comment-thread.js +170 -0
- package/dist/feedback/feedback-comment-thread.js.map +1 -0
- package/dist/feedback/feedback-dialog-labels.js +2 -1
- package/dist/feedback/feedback-dialog-labels.js.map +1 -1
- package/dist/feedback/feedback-dialog.d.ts +13 -0
- package/dist/feedback/feedback-dialog.js +12 -4
- package/dist/feedback/feedback-dialog.js.map +1 -1
- package/dist/feedback/feedback-labels.d.ts +11 -0
- package/dist/feedback/feedback-labels.js +4 -1
- package/dist/feedback/feedback-labels.js.map +1 -1
- package/dist/feedback/feedback-menu-labels.js +2 -1
- package/dist/feedback/feedback-menu-labels.js.map +1 -1
- package/dist/feedback/feedback-menu.d.ts +17 -3
- package/dist/feedback/feedback-menu.js +11 -1
- package/dist/feedback/feedback-menu.js.map +1 -1
- package/dist/feedback/feedback-public-labels.d.ts +53 -0
- package/dist/feedback/feedback-public-labels.js +27 -0
- package/dist/feedback/feedback-public-labels.js.map +1 -0
- package/dist/feedback/feedback-public-list.d.ts +139 -0
- package/dist/feedback/feedback-public-list.js +299 -0
- package/dist/feedback/feedback-public-list.js.map +1 -0
- package/dist/feedback/feedback-public-switch.d.ts +51 -0
- package/dist/feedback/feedback-public-switch.js +50 -0
- package/dist/feedback/feedback-public-switch.js.map +1 -0
- package/dist/feedback/feedback-public-types.d.ts +75 -0
- package/dist/feedback/feedback-public-types.js +2 -0
- package/dist/feedback/feedback-public-types.js.map +1 -0
- package/dist/feedback/feedback-record.d.ts +32 -1
- package/dist/feedback/feedback-record.js +10 -0
- package/dist/feedback/feedback-record.js.map +1 -1
- package/dist/feedback/feedback-replies-context.d.ts +7 -0
- package/dist/feedback/feedback-replies-context.js +7 -0
- package/dist/feedback/feedback-replies-context.js.map +1 -0
- package/dist/feedback/feedback-replies.d.ts +97 -0
- package/dist/feedback/feedback-replies.js +114 -0
- package/dist/feedback/feedback-replies.js.map +1 -0
- package/dist/feedback/feedback-row-detail-labels.js +1 -0
- package/dist/feedback/feedback-row-detail-labels.js.map +1 -1
- package/dist/feedback/feedback-row-detail.d.ts +47 -10
- package/dist/feedback/feedback-row-detail.js +51 -50
- package/dist/feedback/feedback-row-detail.js.map +1 -1
- package/dist/feedback/feedback-submit-dialog.d.ts +3 -1
- package/dist/feedback/feedback-submit-dialog.js +6 -1
- package/dist/feedback/feedback-submit-dialog.js.map +1 -1
- package/dist/feedback/feedback-submit.d.ts +8 -0
- package/dist/feedback/feedback-submit.js +3 -2
- package/dist/feedback/feedback-submit.js.map +1 -1
- package/dist/feedback/feedback-table.d.ts +15 -5
- package/dist/feedback/feedback-table.js +46 -5
- package/dist/feedback/feedback-table.js.map +1 -1
- package/dist/feedback/feedback-thread-parts-labels.d.ts +6 -0
- package/dist/feedback/feedback-thread-parts-labels.js +36 -0
- package/dist/feedback/feedback-thread-parts-labels.js.map +1 -0
- package/dist/feedback/feedback-thread.d.ts +8 -0
- package/dist/feedback/feedback-thread.js +7 -2
- package/dist/feedback/feedback-thread.js.map +1 -1
- package/dist/feedback/feedback-vote-button.d.ts +49 -0
- package/dist/feedback/feedback-vote-button.js +72 -0
- package/dist/feedback/feedback-vote-button.js.map +1 -0
- package/dist/feedback.d.ts +6 -0
- package/dist/feedback.js +6 -0
- package/dist/feedback.js.map +1 -1
- package/dist/i18n/defaults.js +12 -0
- package/dist/i18n/defaults.js.map +1 -1
- package/dist/i18n/german.js +69 -3
- package/dist/i18n/german.js.map +1 -1
- package/dist/i18n/kit-labels.d.ts +16 -0
- package/dist/i18n/kit-labels.js.map +1 -1
- package/dist/i18n/locales/en.js +85 -3
- package/dist/i18n/locales/en.js.map +3 -3
- package/dist/i18n/locales/es.js +69 -3
- package/dist/i18n/locales/es.js.map +1 -1
- package/dist/i18n/locales/fr.js +69 -3
- package/dist/i18n/locales/fr.js.map +1 -1
- package/dist/i18n/locales/hu.js +69 -3
- package/dist/i18n/locales/hu.js.map +1 -1
- package/dist/i18n/locales/it.js +69 -3
- package/dist/i18n/locales/it.js.map +1 -1
- package/dist/i18n/locales/zh.js +69 -3
- package/dist/i18n/locales/zh.js.map +1 -1
- package/dist/i18n/review.js +5 -0
- package/dist/i18n/review.js.map +1 -1
- package/dist/index.d.ts +2 -0
- package/dist/index.js +6 -0
- package/dist/index.js.map +1 -1
- package/dist/notifications/feedback-notification-types-labels.d.ts +15 -0
- package/dist/notifications/feedback-notification-types-labels.js +11 -0
- package/dist/notifications/feedback-notification-types-labels.js.map +1 -0
- package/dist/notifications/feedback-notification-types.d.ts +28 -0
- package/dist/notifications/feedback-notification-types.js +21 -0
- package/dist/notifications/feedback-notification-types.js.map +1 -0
- package/package.json +1 -1
- package/src/feedback/feedback-comment-thread.tsx +277 -0
- package/src/feedback/feedback-dialog-labels.ts +1 -0
- package/src/feedback/feedback-dialog.tsx +30 -2
- package/src/feedback/feedback-labels.ts +14 -0
- package/src/feedback/feedback-menu-labels.ts +1 -0
- package/src/feedback/feedback-menu.tsx +29 -3
- package/src/feedback/feedback-public-labels.ts +80 -0
- package/src/feedback/feedback-public-list.tsx +478 -0
- package/src/feedback/feedback-public-switch.tsx +102 -0
- package/src/feedback/feedback-public-types.ts +82 -0
- package/src/feedback/feedback-record.ts +39 -1
- package/src/feedback/feedback-replies-context.ts +9 -0
- package/src/feedback/feedback-replies.tsx +235 -0
- package/src/feedback/feedback-row-detail-labels.ts +1 -0
- package/src/feedback/feedback-row-detail.tsx +115 -74
- package/src/feedback/feedback-submit-dialog.tsx +9 -0
- package/src/feedback/feedback-submit.tsx +10 -1
- package/src/feedback/feedback-table.tsx +73 -6
- package/src/feedback/feedback-thread-parts-labels.ts +41 -0
- package/src/feedback/feedback-thread.tsx +18 -1
- package/src/feedback/feedback-vote-button.tsx +123 -0
- package/src/feedback.ts +10 -0
- package/src/i18n/defaults.ts +12 -0
- package/src/i18n/german.ts +66 -0
- package/src/i18n/kit-labels.tsx +16 -0
- package/src/i18n/locales/en.ts +8 -0
- package/src/i18n/locales/es.ts +66 -0
- package/src/i18n/locales/fr.ts +66 -0
- package/src/i18n/locales/hu.ts +66 -0
- package/src/i18n/locales/it.ts +66 -0
- package/src/i18n/locales/zh.ts +66 -0
- package/src/i18n/review.ts +5 -0
- package/src/index.ts +9 -0
- package/src/notifications/feedback-notification-types-labels.ts +24 -0
- package/src/notifications/feedback-notification-types.ts +46 -0
|
@@ -74,6 +74,14 @@ export interface UseFeedbackSubmitOptions<R = unknown> {
|
|
|
74
74
|
* `modern-screenshot`. A rejection becomes the `captureFailed` toast.
|
|
75
75
|
*/
|
|
76
76
|
capture?: false | (() => Promise<File | null>);
|
|
77
|
+
/**
|
|
78
|
+
* The app's reports are public (0.38, docs/feedback-harmonization.md §9.2 decision 6):
|
|
79
|
+
* `true` shows **"Other users will see the title and text (never your screenshots or
|
|
80
|
+
* files)"** (`feedbackDialog.publicNotice`) under the body. keksdose and Kurvenschmiede
|
|
81
|
+
* pass it — for a viewer who passes the app's `public_viewer` — and kastlan, whose
|
|
82
|
+
* public parts are off, does not. Default `false`.
|
|
83
|
+
*/
|
|
84
|
+
publicNotice?: boolean;
|
|
77
85
|
}
|
|
78
86
|
|
|
79
87
|
export interface FeedbackSubmit {
|
|
@@ -134,7 +142,7 @@ export interface FeedbackSubmit {
|
|
|
134
142
|
* new draft is somebody's next report.
|
|
135
143
|
*/
|
|
136
144
|
export function useFeedbackSubmit<R = unknown>(options: UseFeedbackSubmitOptions<R>): FeedbackSubmit {
|
|
137
|
-
const { upload, create, user, environment, version, onSubmitted, errorMessage, capture } = options;
|
|
145
|
+
const { upload, create, user, environment, version, onSubmitted, errorMessage, capture, publicNotice } = options;
|
|
138
146
|
const toastText = useFeedbackToastLabels();
|
|
139
147
|
const [isOpen, setIsOpen] = useState(false);
|
|
140
148
|
const [category, setCategory] = useState<FeedbackCategory>("OTHER");
|
|
@@ -206,6 +214,7 @@ export function useFeedbackSubmit<R = unknown>(options: UseFeedbackSubmitOptions
|
|
|
206
214
|
environment={environment}
|
|
207
215
|
version={version}
|
|
208
216
|
capture={capture}
|
|
217
|
+
publicNotice={publicNotice}
|
|
209
218
|
/>
|
|
210
219
|
</Suspense>
|
|
211
220
|
) : null;
|
|
@@ -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
|
-
|
|
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 ?? undefined} />,
|
|
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>({
|
|
592
|
-
|
|
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({
|
|
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 ?? undefined} />}
|
|
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=
|
|
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,123 @@
|
|
|
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 server's `detail`, else `feedbackToast.voteFailed` (0.38.1).
|
|
24
|
+
* - **The author's own report** counts as their vote (§9.6, decision 20): `own` shows it
|
|
25
|
+
* pressed and `aria-disabled` with "Your report" as the reason (the server would
|
|
26
|
+
* answer 409).
|
|
27
|
+
* - A new `votes` / `voted` from the app (a refetch) wins over the last answer.
|
|
28
|
+
*/
|
|
29
|
+
|
|
30
|
+
/** The `feedbackVote` namespace of `<UiKitProvider labels>`. */
|
|
31
|
+
export interface FeedbackVoteLabels {
|
|
32
|
+
/** The button's name. de-CH: "Will ich auch" */
|
|
33
|
+
vote: string;
|
|
34
|
+
/** Why the author's own button is disabled. de-CH: "Ihr Bericht" */
|
|
35
|
+
own: string;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export { DEFAULT_FEEDBACK_VOTE_LABELS };
|
|
39
|
+
|
|
40
|
+
export interface FeedbackVoteButtonProps {
|
|
41
|
+
/** People who want this — the author's own vote included. */
|
|
42
|
+
votes: number;
|
|
43
|
+
/** The viewer has voted. */
|
|
44
|
+
voted: boolean;
|
|
45
|
+
/** The viewer filed it: pressed, disabled, "Your report". */
|
|
46
|
+
own?: boolean;
|
|
47
|
+
/** `PUT` (`next` true) or `DELETE` (`false`) `/feedback/{id}/vote`; resolve with the
|
|
48
|
+
* answer, reject to roll back. */
|
|
49
|
+
onVote: (next: boolean) => Promise<FeedbackVoteState>;
|
|
50
|
+
/** Not now (e.g. a demo): plain `disabled`. A string is the reason, as Button's. */
|
|
51
|
+
disabled?: boolean | string;
|
|
52
|
+
/** The vote COMMITS — see Button's `commit`. */
|
|
53
|
+
commit?: CommitScope;
|
|
54
|
+
/** `"sm"`: the count alone, for a table cell. Default `"md"`. */
|
|
55
|
+
size?: "sm" | "md";
|
|
56
|
+
labels?: LabelOverride<FeedbackVoteLabels>;
|
|
57
|
+
className?: string;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export function FeedbackVoteButton({
|
|
61
|
+
votes,
|
|
62
|
+
voted,
|
|
63
|
+
own = false,
|
|
64
|
+
onVote,
|
|
65
|
+
disabled = false,
|
|
66
|
+
commit,
|
|
67
|
+
size = "md",
|
|
68
|
+
labels: labelsProp,
|
|
69
|
+
className,
|
|
70
|
+
}: FeedbackVoteButtonProps) {
|
|
71
|
+
const labels = useKitLabels("feedbackVote", DEFAULT_FEEDBACK_VOTE_LABELS, labelsProp);
|
|
72
|
+
const locale = useKitLocale();
|
|
73
|
+
// The last press's state, valid only while the app's props are still the ones it was
|
|
74
|
+
// pressed against: a refetch that brings new numbers is the truth.
|
|
75
|
+
const [shown, setShown] = useState<{ base: FeedbackVoteState; state: FeedbackVoteState } | null>(null);
|
|
76
|
+
const busy = useRef(false);
|
|
77
|
+
const current =
|
|
78
|
+
shown && shown.base.votes === votes && shown.base.voted === voted ? shown.state : { votes, voted };
|
|
79
|
+
const pressed = own || current.voted;
|
|
80
|
+
|
|
81
|
+
const press = () => {
|
|
82
|
+
if (own || disabled || busy.current) return;
|
|
83
|
+
const base = { votes, voted };
|
|
84
|
+
const before = current;
|
|
85
|
+
const next = !before.voted;
|
|
86
|
+
busy.current = true;
|
|
87
|
+
setShown({ base, state: { votes: Math.max(0, before.votes + (next ? 1 : -1)), voted: next } });
|
|
88
|
+
onVote(next).then(
|
|
89
|
+
(answer) => setShown({ base, state: { votes: answer.votes, voted: answer.voted } }),
|
|
90
|
+
() => setShown({ base, state: before }),
|
|
91
|
+
).finally(() => {
|
|
92
|
+
busy.current = false;
|
|
93
|
+
});
|
|
94
|
+
};
|
|
95
|
+
|
|
96
|
+
const count = formatNumber(current.votes, { locale });
|
|
97
|
+
const button = (
|
|
98
|
+
<Button
|
|
99
|
+
type="button"
|
|
100
|
+
variant="secondary"
|
|
101
|
+
size="sm"
|
|
102
|
+
pressed={pressed}
|
|
103
|
+
commit={own ? undefined : commit}
|
|
104
|
+
disabled={disabled === true}
|
|
105
|
+
disabledReason={own ? labels.own : typeof disabled === "string" ? disabled : undefined}
|
|
106
|
+
onClick={(event) => {
|
|
107
|
+
// A row around it may expand on a click.
|
|
108
|
+
event.stopPropagation();
|
|
109
|
+
press();
|
|
110
|
+
}}
|
|
111
|
+
data-feedback-vote=""
|
|
112
|
+
className={cn("gap-1.5", className)}
|
|
113
|
+
>
|
|
114
|
+
<ThumbsUp className="size-3.5 shrink-0" aria-hidden />
|
|
115
|
+
<span className={size === "sm" ? "sr-only" : undefined}>{labels.vote}</span>
|
|
116
|
+
<span className="tabular-nums" data-feedback-votes="">
|
|
117
|
+
{count}
|
|
118
|
+
</span>
|
|
119
|
+
</Button>
|
|
120
|
+
);
|
|
121
|
+
// At "sm" the name is not on screen: the Tooltip shows it (the own reason has its own).
|
|
122
|
+
return size === "sm" && !own ? <Tooltip label={labels.vote}>{button}</Tooltip> : button;
|
|
123
|
+
}
|
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";
|
package/src/i18n/defaults.ts
CHANGED
|
@@ -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,
|
package/src/i18n/german.ts
CHANGED
|
@@ -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",
|
|
@@ -112,11 +113,45 @@ export function germanLabels(numberLocale: string): UiKitLabels {
|
|
|
112
113
|
statusChanged: (status, title) => `Auf „${status}“ gesetzt: ${title}`,
|
|
113
114
|
statusUndo: "Rückgängig",
|
|
114
115
|
statusRestored: (status, title) => `Zurück auf „${status}“: ${title}`,
|
|
116
|
+
commentFailed: "Kommentar konnte nicht gesendet werden.",
|
|
117
|
+
voteFailed: "Stimme konnte nicht gespeichert werden.",
|
|
118
|
+
replyFailed: "Antwort konnte nicht gesendet werden.",
|
|
115
119
|
},
|
|
116
120
|
feedbackMenu: {
|
|
117
121
|
trigger: "Feedback senden",
|
|
118
122
|
myFeedback: "Mein Feedback",
|
|
119
123
|
viewFeedback: "Feedback ansehen",
|
|
124
|
+
allFeedback: "Alle Rückmeldungen",
|
|
125
|
+
},
|
|
126
|
+
// 0.38.0 — docs/feedback-harmonization.md §9: the public pages and the two feedback
|
|
127
|
+
// notification types. "Rückmeldungen" and "Eine Person" are the contract's (§9.2, §9.5).
|
|
128
|
+
feedbackPublic: {
|
|
129
|
+
title: "Alle Rückmeldungen",
|
|
130
|
+
sort: "Sortieren",
|
|
131
|
+
sortNewest: "Neueste",
|
|
132
|
+
sortVotes: "Meistgewünscht",
|
|
133
|
+
sortUpdated: "Zuletzt aktualisiert",
|
|
134
|
+
search: "Titel durchsuchen",
|
|
135
|
+
columnVotes: "Stimmen",
|
|
136
|
+
total: (count) => (count === 1 ? "1 Meldung" : `${n(count)} Meldungen`),
|
|
137
|
+
comments: (count) => (count === 1 ? "1 Kommentar" : `${n(count)} Kommentare`),
|
|
138
|
+
votes: (count) => (count === 1 ? "1 Stimme" : `${n(count)} Stimmen`),
|
|
139
|
+
anonymous: "Eine Person",
|
|
140
|
+
filedBy: (name) => `Von ${name}`,
|
|
141
|
+
body: "Beschreibung",
|
|
142
|
+
conversation: "Antworten und Kommentare",
|
|
143
|
+
loading: "Wird geladen…",
|
|
144
|
+
empty: "Noch keine Meldungen",
|
|
145
|
+
visibleToOthers: "Für andere sichtbar",
|
|
146
|
+
visibleHint: "Andere Personen sehen Titel und Text, nie Ihre Screenshots oder Dateien.",
|
|
147
|
+
visibleLocked: "Nur solange die Meldung noch offen ist.",
|
|
148
|
+
page: (page) => `Seite ${n(page)}`,
|
|
149
|
+
},
|
|
150
|
+
feedbackNotificationTypes: {
|
|
151
|
+
answered: "Antworten auf Ihre Meldungen",
|
|
152
|
+
answeredDescription: "Wenn eine Meldung, die Sie geschrieben oder kommentiert haben, eine Antwort erhält.",
|
|
153
|
+
comment: "Kommentare zu Meldungen, denen Sie folgen",
|
|
154
|
+
commentDescription: "Wenn jemand eine Meldung kommentiert, die Sie geschrieben oder kommentiert haben.",
|
|
120
155
|
},
|
|
121
156
|
feedbackContext: {
|
|
122
157
|
user: "Nutzer",
|
|
@@ -157,6 +192,7 @@ export function germanLabels(numberLocale: string): UiKitLabels {
|
|
|
157
192
|
download: (name) => `${name} herunterladen`,
|
|
158
193
|
downloadFailed: "Der Anhang konnte nicht heruntergeladen werden.",
|
|
159
194
|
outcome: "Ergebnis",
|
|
195
|
+
replies: "Antworten",
|
|
160
196
|
resolvedAt: (date) => `Erledigt am: ${date}`,
|
|
161
197
|
outcomeAdd: "Ergebnis hinzufügen",
|
|
162
198
|
outcomeUpdate: "Aktualisieren",
|
|
@@ -170,6 +206,36 @@ export function germanLabels(numberLocale: string): UiKitLabels {
|
|
|
170
206
|
status: "Status",
|
|
171
207
|
note: "Notiz",
|
|
172
208
|
},
|
|
209
|
+
// 0.38.0 — docs/feedback-harmonization.md §9.15: the vote, the comment thread, the replies.
|
|
210
|
+
feedbackVote: {
|
|
211
|
+
vote: "Will ich auch",
|
|
212
|
+
own: "Ihr Bericht",
|
|
213
|
+
},
|
|
214
|
+
feedbackComments: {
|
|
215
|
+
comments: "Kommentare",
|
|
216
|
+
empty: "Noch keine Kommentare",
|
|
217
|
+
hidden: "Von einem Admin ausgeblendet",
|
|
218
|
+
hide: "Ausblenden",
|
|
219
|
+
unhide: "Einblenden",
|
|
220
|
+
delete: "Löschen",
|
|
221
|
+
deleteTitle: "Diesen Kommentar löschen?",
|
|
222
|
+
deleteBody: "Er wird für alle entfernt. Das kann nicht rückgängig gemacht werden.",
|
|
223
|
+
field: "Kommentar schreiben",
|
|
224
|
+
placeholder: "Kommentar schreiben…",
|
|
225
|
+
send: "Kommentieren",
|
|
226
|
+
},
|
|
227
|
+
feedbackReplies: {
|
|
228
|
+
replies: "Antworten",
|
|
229
|
+
empty: "Noch keine Antwort",
|
|
230
|
+
admin: "Admin",
|
|
231
|
+
statusChanged: "Status geändert auf",
|
|
232
|
+
field: "Antwort schreiben",
|
|
233
|
+
placeholder: "Antwort schreiben…",
|
|
234
|
+
reply: "Antworten",
|
|
235
|
+
status: "Status mit der Antwort",
|
|
236
|
+
noChange: "Keine Änderung",
|
|
237
|
+
nudge: "Die meldende Person wurde nicht benachrichtigt. Schreiben Sie eine Antwort.",
|
|
238
|
+
},
|
|
173
239
|
accountSettings: {
|
|
174
240
|
profile: {
|
|
175
241
|
title: "Profil",
|
package/src/i18n/kit-labels.tsx
CHANGED
|
@@ -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. */
|
package/src/i18n/locales/en.ts
CHANGED
|
@@ -48,6 +48,14 @@ 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
|
+
page: (page) => `Page ${n(page)}`,
|
|
58
|
+
},
|
|
51
59
|
measuredGrid: {
|
|
52
60
|
...d.measuredGrid,
|
|
53
61
|
points: (count) => (count === 1 ? "1 point" : `${n(count)} points`),
|