@eifi1/ui-kit 0.39.0 → 0.40.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (148) hide show
  1. package/README.md +8 -7
  2. package/dist/billing/billing-overview.d.ts +12 -2
  3. package/dist/billing/pay-page.js +1 -1
  4. package/dist/billing/pay-page.js.map +1 -1
  5. package/dist/bin/eifi1-pay-page.mjs +1 -1
  6. package/dist/components/autocomplete.js +5 -1
  7. package/dist/components/autocomplete.js.map +1 -1
  8. package/dist/components/combobox-core.d.ts +14 -1
  9. package/dist/components/combobox-core.js +4 -0
  10. package/dist/components/combobox-core.js.map +1 -1
  11. package/dist/components/combobox.js +9 -3
  12. package/dist/components/combobox.js.map +1 -1
  13. package/dist/components/data-table-labels.d.ts +5 -0
  14. package/dist/components/data-table-labels.js +2 -0
  15. package/dist/components/data-table-labels.js.map +1 -1
  16. package/dist/components/data-table-mobile.d.ts +36 -1
  17. package/dist/components/data-table-mobile.js +162 -15
  18. package/dist/components/data-table-mobile.js.map +1 -1
  19. package/dist/components/data-table.d.ts +39 -12
  20. package/dist/components/data-table.js +74 -13
  21. package/dist/components/data-table.js.map +1 -1
  22. package/dist/components/inline-entity-combobox.js +10 -3
  23. package/dist/components/inline-entity-combobox.js.map +1 -1
  24. package/dist/components/picker-sheet.d.ts +18 -1
  25. package/dist/components/picker-sheet.js +20 -2
  26. package/dist/components/picker-sheet.js.map +1 -1
  27. package/dist/components/tabs.d.ts +26 -2
  28. package/dist/components/tabs.js +41 -2
  29. package/dist/components/tabs.js.map +1 -1
  30. package/dist/components/translation-review-parts.d.ts +2 -1
  31. package/dist/components/translation-review-parts.js +1 -1
  32. package/dist/components/translation-review-parts.js.map +1 -1
  33. package/dist/components/ui-labels.js +3 -1
  34. package/dist/components/ui-labels.js.map +1 -1
  35. package/dist/eslint/index.js +1 -1
  36. package/dist/feedback/feedback-comment-thread.d.ts +8 -1
  37. package/dist/feedback/feedback-comment-thread.js +26 -1
  38. package/dist/feedback/feedback-comment-thread.js.map +1 -1
  39. package/dist/feedback/feedback-labels.d.ts +2 -1
  40. package/dist/feedback/feedback-labels.js +1 -0
  41. package/dist/feedback/feedback-labels.js.map +1 -1
  42. package/dist/feedback/feedback-menu-labels.js +3 -1
  43. package/dist/feedback/feedback-menu-labels.js.map +1 -1
  44. package/dist/feedback/feedback-menu.d.ts +31 -24
  45. package/dist/feedback/feedback-menu.js +16 -7
  46. package/dist/feedback/feedback-menu.js.map +1 -1
  47. package/dist/feedback/feedback-public-labels.d.ts +3 -2
  48. package/dist/feedback/feedback-public-labels.js +1 -1
  49. package/dist/feedback/feedback-public-labels.js.map +1 -1
  50. package/dist/feedback/feedback-public-list.js +1 -0
  51. package/dist/feedback/feedback-public-list.js.map +1 -1
  52. package/dist/feedback/feedback-public-types.d.ts +16 -3
  53. package/dist/feedback/feedback-record.d.ts +20 -3
  54. package/dist/feedback/feedback-record.js +15 -0
  55. package/dist/feedback/feedback-record.js.map +1 -1
  56. package/dist/feedback/feedback-replies.d.ts +13 -2
  57. package/dist/feedback/feedback-replies.js +67 -33
  58. package/dist/feedback/feedback-replies.js.map +1 -1
  59. package/dist/feedback/feedback-rework-entry-chip.d.ts +9 -0
  60. package/dist/feedback/feedback-rework-entry-chip.js +13 -0
  61. package/dist/feedback/feedback-rework-entry-chip.js.map +1 -0
  62. package/dist/feedback/feedback-row-detail.d.ts +31 -8
  63. package/dist/feedback/feedback-row-detail.js +31 -7
  64. package/dist/feedback/feedback-row-detail.js.map +1 -1
  65. package/dist/feedback/feedback-table-labels.js +1 -1
  66. package/dist/feedback/feedback-table-labels.js.map +1 -1
  67. package/dist/feedback/feedback-table.d.ts +5 -2
  68. package/dist/feedback/feedback-table.js +2 -2
  69. package/dist/feedback/feedback-table.js.map +1 -1
  70. package/dist/feedback/feedback-thread.js +3 -1
  71. package/dist/feedback/feedback-thread.js.map +1 -1
  72. package/dist/feedback/feedback-vocabulary.d.ts +9 -6
  73. package/dist/feedback/feedback-vocabulary.js +16 -1
  74. package/dist/feedback/feedback-vocabulary.js.map +1 -1
  75. package/dist/hooks/use-long-press.d.ts +30 -0
  76. package/dist/hooks/use-long-press.js +77 -0
  77. package/dist/hooks/use-long-press.js.map +1 -0
  78. package/dist/hooks/use-overlay-history.js +37 -3
  79. package/dist/hooks/use-overlay-history.js.map +1 -1
  80. package/dist/i18n/german.js +13 -5
  81. package/dist/i18n/german.js.map +1 -1
  82. package/dist/i18n/kit-labels.d.ts +3 -0
  83. package/dist/i18n/kit-labels.js.map +1 -1
  84. package/dist/i18n/locales/en.js +12 -5
  85. package/dist/i18n/locales/en.js.map +2 -2
  86. package/dist/i18n/locales/es.js +13 -5
  87. package/dist/i18n/locales/es.js.map +1 -1
  88. package/dist/i18n/locales/fr.js +13 -5
  89. package/dist/i18n/locales/fr.js.map +1 -1
  90. package/dist/i18n/locales/hu.js +13 -5
  91. package/dist/i18n/locales/hu.js.map +1 -1
  92. package/dist/i18n/locales/it.js +13 -5
  93. package/dist/i18n/locales/it.js.map +1 -1
  94. package/dist/i18n/locales/zh.js +13 -5
  95. package/dist/i18n/locales/zh.js.map +1 -1
  96. package/dist/i18n/shared-labels.js +1 -1
  97. package/dist/i18n/shared-labels.js.map +1 -1
  98. package/dist/pay/pay.js +1 -1
  99. package/dist/shell/app-shell.js +9 -0
  100. package/dist/shell/app-shell.js.map +1 -1
  101. package/dist/shell/fill-height.d.ts +38 -0
  102. package/dist/shell/fill-height.js +12 -0
  103. package/dist/shell/fill-height.js.map +1 -0
  104. package/dist/shell.d.ts +2 -0
  105. package/dist/shell.js +3 -0
  106. package/dist/shell.js.map +1 -1
  107. package/package.json +1 -1
  108. package/src/billing/billing-overview.ts +12 -2
  109. package/src/billing/pay-page.ts +1 -1
  110. package/src/components/autocomplete.tsx +5 -1
  111. package/src/components/combobox-core.tsx +20 -1
  112. package/src/components/combobox.tsx +9 -3
  113. package/src/components/data-table-labels.ts +7 -0
  114. package/src/components/data-table-mobile.tsx +193 -9
  115. package/src/components/data-table.tsx +141 -24
  116. package/src/components/inline-entity-combobox.tsx +12 -4
  117. package/src/components/picker-sheet.tsx +38 -1
  118. package/src/components/tabs.tsx +73 -3
  119. package/src/components/translation-review-parts.tsx +3 -2
  120. package/src/components/ui-labels.ts +2 -0
  121. package/src/feedback/feedback-comment-thread.tsx +39 -1
  122. package/src/feedback/feedback-labels.ts +3 -1
  123. package/src/feedback/feedback-menu-labels.ts +2 -0
  124. package/src/feedback/feedback-menu.tsx +48 -30
  125. package/src/feedback/feedback-public-labels.ts +4 -3
  126. package/src/feedback/feedback-public-list.tsx +4 -1
  127. package/src/feedback/feedback-public-types.ts +17 -3
  128. package/src/feedback/feedback-record.ts +34 -3
  129. package/src/feedback/feedback-replies.tsx +100 -31
  130. package/src/feedback/feedback-rework-entry-chip.tsx +18 -0
  131. package/src/feedback/feedback-row-detail.tsx +65 -12
  132. package/src/feedback/feedback-table-labels.ts +1 -1
  133. package/src/feedback/feedback-table.tsx +7 -4
  134. package/src/feedback/feedback-thread.tsx +5 -1
  135. package/src/feedback/feedback-vocabulary.ts +27 -6
  136. package/src/hooks/use-long-press.ts +126 -0
  137. package/src/hooks/use-overlay-history.ts +74 -3
  138. package/src/i18n/german.ts +10 -2
  139. package/src/i18n/kit-labels.tsx +3 -0
  140. package/src/i18n/locales/es.ts +10 -2
  141. package/src/i18n/locales/fr.ts +10 -2
  142. package/src/i18n/locales/hu.ts +10 -2
  143. package/src/i18n/locales/it.ts +10 -2
  144. package/src/i18n/locales/zh.ts +10 -2
  145. package/src/i18n/shared-labels.ts +1 -1
  146. package/src/shell/app-shell.tsx +9 -0
  147. package/src/shell/fill-height.tsx +45 -0
  148. package/src/shell.ts +3 -0
@@ -1,4 +1,4 @@
1
- import { Inbox, MessageSquare, Users } from "lucide-react";
1
+ import { Inbox, MessageSquare, User, Users } from "lucide-react";
2
2
  import type { UserAvatarBadge } from "../components/user-avatar";
3
3
  import { useKitLabels, type LabelOverride } from "../i18n/kit-labels";
4
4
  import { TopBarActionMenu, type TopBarMenuEntry } from "../shell/topbar-action-menu";
@@ -13,21 +13,26 @@ import { DEFAULT_FEEDBACK_MENU_LABELS } from "./feedback-menu-labels";
13
13
  /**
14
14
  * `feedbackMenu` — the top-bar feedback menu's own words (0.27.0). The four category rows
15
15
  * are `feedbackCategory`'s, shared with the badges and the dialog's picker. de-CH canon
16
- * (keksdose `de-CH.json`): `trigger` "Feedback senden", `myFeedback` "Mein Feedback",
17
- * `viewFeedback` "Feedback ansehen".
16
+ * (keksdose `de-CH.json`, §10.6): `trigger` "Feedback senden", `discussion`
17
+ * "Feedback-Diskussion", `myFeedback` "Mein Feedback", `inbox` "Feedback-Eingang".
18
18
  */
19
19
  export interface FeedbackMenuLabels {
20
20
  /** The trigger's accessible name, and the menu's. keksdose `feedback.send`. */
21
21
  trigger: string;
22
- /** The link to the reporter's own reports — everyone but an admin. keksdose
23
- * `feedback.my_title`. */
22
+ /** The link to the viewer's own reports — everyone's, an admin's too since 0.40
23
+ * (§10.2 decision 3). keksdose `feedback.my_title`. */
24
24
  myFeedback: string;
25
- /** The link to the inbox — an admin's one list link, in place of "My feedback".
26
- * keksdose `feedback.view_all`. */
27
- viewFeedback: string;
28
- /** The link to every public report, {@link FeedbackMenuProps.publicHref} (0.38,
29
- * docs/feedback-harmonization.md §9.2 decision 21). Optional, so a complete
30
- * `UiKitLabels` typed before 0.38 still compiles. de-CH "Alle Rückmeldungen". */
25
+ /** 0.40: the link to the public list, {@link FeedbackMenuProps.publicHref} (§10.2
26
+ * decision 3; "All feedback" before). de-CH "Feedback-Diskussion". */
27
+ discussion: string;
28
+ /** 0.40: the admin's link to the triage list, {@link FeedbackMenuProps.inboxHref}
29
+ * ("View feedback" before). de-CH "Feedback-Eingang". */
30
+ inbox: string;
31
+ /** @deprecated 0.40 — use {@link inbox}; removed in 0.41. Unused: the inbox row reads
32
+ * `inbox` (feedback §10.6). */
33
+ viewFeedback?: string;
34
+ /** @deprecated 0.40 — use {@link discussion}; removed in 0.41. Unused: the public row
35
+ * reads `discussion` (feedback §10.6). */
31
36
  allFeedback?: string;
32
37
  }
33
38
 
@@ -38,8 +43,8 @@ export interface FeedbackMenuProps {
38
43
  /** A category row was chosen — open the submit dialog on it (`useFeedbackSubmit`'s
39
44
  * `open`). Never called with `CRASH`: there is no row for it. */
40
45
  onFile: (category: FeedbackCategory) => void;
41
- /** The list link is "View feedback" → {@link inboxHref} INSTEAD of "My feedback" (§4.1,
42
- * keksdose `top-bar.tsx:205-214`). The admin check is the app's (kastlan's is
46
+ /** Adds the **Feedback inbox** row → {@link inboxHref} under "My feedback" (0.40, §10.2
47
+ * decision 11; until 0.39 it replaced it). The admin check is the app's (kastlan's is
43
48
  * company-scoped). Default `false`. */
44
49
  isAdmin?: boolean;
45
50
  /**
@@ -50,16 +55,16 @@ export interface FeedbackMenuProps {
50
55
  * `all-feedback`, `my-feedback`, `view-feedback`).
51
56
  */
52
57
  extraEntries?: TopBarMenuEntry[];
53
- /** The user's own reports, linked for everyone but an admin. Default `"/my-feedback"`
54
- * (§2.3). */
58
+ /** The viewer's own reports, linked for everyone (an admin too since 0.40). Default
59
+ * `"/my-feedback"` (§2.3). */
55
60
  myFeedbackHref?: string;
56
61
  /** The admin inbox, linked for an admin. Default `"/feedback"` (§2.3). */
57
62
  inboxHref?: string;
58
63
  /**
59
64
  * The public list (0.38, docs/feedback-harmonization.md §9.2 decision 21): when passed,
60
- * an **All feedback** row (`Users`, key `all-feedback`) above the list link, for
61
- * everyone — the admin too, who reads what the others read there. keksdose:
62
- * `"/all-feedback"`; "My feedback" stays as it is. Leave it out where the app has no
65
+ * a **Feedback discussion** row (`Users`, key `all-feedback`; "All feedback" until 0.40)
66
+ * first of the list links, for everyone — the admin too, who reads what the others read
67
+ * there. keksdose: `"/all-feedback"`. Leave it out where the app has no
63
68
  * public parts (kastlan) or the viewer may not see them (a Kurvenschmiede customer, a
64
69
  * demo — §9.2 decisions 7, 8).
65
70
  */
@@ -83,17 +88,19 @@ export interface FeedbackMenuProps {
83
88
  * the category badges' own glyphs), each calling `onFile(category)`;
84
89
  * 3. a divider, always;
85
90
  * 4. the app's `extraEntries`;
86
- * 5. with `publicHref` (0.38), **All feedback** → the public list, for everyone;
87
- * 6. ONE list link, `Inbox`: an admin's is "View feedback" → `/feedback`, everyone
88
- * else's "My feedback" → `/my-feedback` — keksdose's (`top-bar.tsx:205-214`, feedback
89
- * #331: admins get the management view, everyone else their own submissions).
91
+ * 5. the list links (0.40, §10.2 decision 11), in this order: **Feedback discussion**
92
+ * (`Users`, key `all-feedback`) → `publicHref`, when passed; **My feedback** (`User`,
93
+ * key `my-feedback`) → `/my-feedback`, for everyone, admins included; **Feedback
94
+ * inbox** (`Inbox`, key `view-feedback`) → `/feedback`, for an admin. An admin with
95
+ * public parts sees three, a member two; without them an admin two and anyone else
96
+ * one. Until 0.39 an admin had the inbox in place of their own list (feedback #331);
97
+ * Marcel's kk #419 gave them both.
90
98
  *
91
99
  * WHY A PRESET. Three apps had drawn three menus on the same primitive and they disagreed:
92
100
  * kastlan headed it "New submission", Kurvenschmiede "Send feedback" with a single link
93
101
  * that changed its words by role, keksdose had no heading and one link by role. keksdose
94
- * is the source (§2.4, §7.11): no heading, one link — the inbox for an admin, who reaches
95
- * their own reports there too — and the kit draws it, so the row order, the icons and the
96
- * words cannot drift again. Where the menu sits in the bar stays each app's.
102
+ * is the source (§2.4, §7.11): no heading, the list links by role — and the kit draws
103
+ * it, so the row order, the icons and the words cannot drift again. Where the menu sits in the bar stays each app's.
97
104
  *
98
105
  * `CRASH` has no row and never will: crash reports are filed by the error boundary with
99
106
  * the stack and a fingerprint (keksdose feedback #160), and a hand-filed "crash" would sort
@@ -136,17 +143,28 @@ export function FeedbackMenu({
136
143
  key: "all-feedback",
137
144
  to: publicHref,
138
145
  icon: <Users className="size-4" />,
139
- label: labels.allFeedback ?? DEFAULT_FEEDBACK_MENU_LABELS.allFeedback!,
146
+ label: labels.discussion,
140
147
  } satisfies TopBarMenuEntry,
141
148
  ]
142
149
  : []),
143
150
  {
144
151
  kind: "link",
145
- key: isAdmin ? "view-feedback" : "my-feedback",
146
- to: isAdmin ? inboxHref : myFeedbackHref,
147
- icon: <Inbox className="size-4" />,
148
- label: isAdmin ? labels.viewFeedback : labels.myFeedback,
152
+ key: "my-feedback",
153
+ to: myFeedbackHref,
154
+ icon: <User className="size-4" />,
155
+ label: labels.myFeedback,
149
156
  },
157
+ ...(isAdmin
158
+ ? [
159
+ {
160
+ kind: "link",
161
+ key: "view-feedback",
162
+ to: inboxHref,
163
+ icon: <Inbox className="size-4" />,
164
+ label: labels.inbox,
165
+ } satisfies TopBarMenuEntry,
166
+ ]
167
+ : []),
150
168
  ];
151
169
  return (
152
170
  <TopBarActionMenu
@@ -10,10 +10,11 @@
10
10
  * stay `feedbackPage`'s, so the two lists cannot disagree; the vote button's words are
11
11
  * `feedbackVote`'s, the thread's `feedbackComments`'.
12
12
  *
13
- * de-CH canon (§9.2, §9.5): `title` "Alle Rückmeldungen", `anonymous` "Eine Person".
13
+ * de-CH canon (§9.2, §9.5, §10.6): `title` "Feedback-Diskussion", `anonymous` "Eine Person".
14
14
  */
15
15
  export interface FeedbackPublicLabels {
16
- /** The page's heading — keksdose's `/all-feedback`, the menu's "All feedback". */
16
+ /** The page's heading — keksdose's `/all-feedback`, the menu's "Feedback discussion"
17
+ * (0.40, §10.2 decision 11; "All feedback" before). de-CH "Feedback-Diskussion". */
17
18
  title: string;
18
19
  /** The sort toggle's accessible name. */
19
20
  sort: string;
@@ -57,7 +58,7 @@ export interface FeedbackPublicLabels {
57
58
  }
58
59
 
59
60
  export const DEFAULT_FEEDBACK_PUBLIC_LABELS: FeedbackPublicLabels = {
60
- title: "All feedback",
61
+ title: "Feedback discussion",
61
62
  sort: "Sort",
62
63
  sortNewest: "Newest",
63
64
  sortVotes: "Most wanted",
@@ -455,7 +455,8 @@ export function FeedbackPublicDetail({
455
455
  />
456
456
  </div>
457
457
  <FeedbackDetailSection title={labels.body}>
458
- {/* Rework rounds stay in it — the author's own words (§9.4). */}
458
+ {/* Rework rounds stay in it — the author's own words (§9.4) — on a server before
459
+ server-kit 0.13; since then each is an entry in the conversation (§10.3). */}
459
460
  <FeedbackProse>{row.body}</FeedbackProse>
460
461
  </FeedbackDetailSection>
461
462
  <FeedbackDetailSection title={labels.conversation}>
@@ -464,6 +465,8 @@ export function FeedbackPublicDetail({
464
465
  comments={comments ?? []}
465
466
  loading={comments === undefined}
466
467
  isAdmin={isAdmin}
468
+ // 0.40 (§10.3): the author's rework entries are theirs — at the reading end.
469
+ isAuthor={row.mine}
467
470
  onComment={onComment}
468
471
  onHide={onHide}
469
472
  onDelete={onDelete}
@@ -11,14 +11,28 @@ import type { FeedbackCategory, FeedbackStatus } from "./feedback-vocabulary";
11
11
  * account, a loop's reply). The client never builds one from an id.
12
12
  */
13
13
 
14
- /** One answer of the admin's — `FeedbackReplyResponse`, §9.7. Oldest first. */
14
+ /** The two kinds of entry in a report's timeline (0.40, §10.2 decision 7): the admin's
15
+ * `reply`, and the author's `rework` — the report sent back with a note. */
16
+ export type FeedbackEntryKind = "reply" | "rework";
17
+
18
+ /** One answer of the admin's — `FeedbackReplyResponse`, §9.7 — or, since 0.40, one
19
+ * rework of the author's (§10.3, `kind: "rework"`). Oldest first. */
15
20
  export interface FeedbackReply {
16
21
  id: number;
17
22
  /** `null`: a loop's reply, a migrated `outcome`, or an erased admin — drawn as the
18
- * app's word for its admin ("Admin"). */
23
+ * app's word for its admin ("Admin"). On a rework entry `null` is an erased or
24
+ * unattributed author, drawn as "A user" (`feedbackPublic.anonymous`), never as the
25
+ * admin word. */
19
26
  author_name: string | null;
20
27
  body: string;
21
- /** The status this reply moved the report to; `null` = no status change. */
28
+ /** 0.40 (§10.3): `"reply"` (absent: a server before server-kit 0.13, all replies) or
29
+ * `"rework"`, the author's entry. */
30
+ kind?: FeedbackEntryKind;
31
+ /** 0.40: a rework entry's one file (`/api/v1/feedback/attachments/<key>`) — the admin
32
+ * and author views only; `null` or absent otherwise (never in the public view, §9.10). */
33
+ attachment_url?: string | null;
34
+ /** The status this reply moved the report to; `null` = no status change — and on a
35
+ * rework migrated out of the body (§10.4), whose move was never recorded. */
22
36
  status_to: FeedbackStatus | null;
23
37
  /** Where it moved from — the admin view only. */
24
38
  status_from?: FeedbackStatus | null;
@@ -179,11 +179,12 @@ export const FEEDBACK_PICKABLE_CATEGORIES: readonly FeedbackCategory[] = Object.
179
179
 
180
180
  /**
181
181
  * The statuses a report can be sent back for **rework** from (§3.4): everything the team
182
- * has answered — `IN_EVALUATION`, `NEEDS_LIVE_TEST`, `POSTPONED`, `DONE`, `WONT_DO`. On
183
- * these the server reopens the row when the body grows by a rework block; on `OPEN`,
184
- * `READY` and `IN_PROGRESS` there is nothing to send back yet.
182
+ * has answered — `IN_EVALUATION`, `NEEDS_LIVE_TEST`, `POSTPONED`, `DONE`, `WONT_DO` — and,
183
+ * since 0.40, `REWORK` itself (§10.2 decision 4: a second rework adds another entry). On
184
+ * `OPEN`, `READY` and `IN_PROGRESS` there is nothing to send back yet.
185
185
  */
186
186
  export const FEEDBACK_REWORKABLE_STATUSES: readonly FeedbackStatus[] = Object.freeze([
187
+ "REWORK",
187
188
  "IN_EVALUATION",
188
189
  "NEEDS_LIVE_TEST",
189
190
  "POSTPONED",
@@ -191,6 +192,18 @@ export const FEEDBACK_REWORKABLE_STATUSES: readonly FeedbackStatus[] = Object.fr
191
192
  "WONT_DO",
192
193
  ] as const);
193
194
 
195
+ /**
196
+ * The loops' queue (0.40, §10.2 decision 4; server-kit `QUEUE_STATUSES`): released for
197
+ * work (`READY`), sent back and released again (`REWORK`), and being worked
198
+ * (`IN_PROGRESS`). Kurvenschmiede's loop adds `OPEN` (decision 9) — its own difference,
199
+ * not this list's.
200
+ */
201
+ export const FEEDBACK_QUEUE_STATUSES: readonly FeedbackStatus[] = Object.freeze([
202
+ "READY",
203
+ "REWORK",
204
+ "IN_PROGRESS",
205
+ ] as const);
206
+
194
207
  /**
195
208
  * The statuses in which the AUTHOR may still edit title, body and category (§3.4,
196
209
  * keksdose `canAuthorEdit`, `feedback-page.tsx:398`): `OPEN`, `READY` and `IN_PROGRESS`.
@@ -320,6 +333,24 @@ export function reworkCount(body: string): number {
320
333
  return n;
321
334
  }
322
335
 
336
+ /**
337
+ * How many times a report has been sent back, since 0.40 (§10.3): its `kind: "rework"`
338
+ * entries when the row carries `replies`, else {@link reworkCount} off the body (a server
339
+ * before server-kit 0.13, whose reworks are still `--- REWORK … ---` blocks). What the
340
+ * Rework chip counts.
341
+ */
342
+ export function feedbackReworkCount(
343
+ row: { body?: string | null; replies?: readonly Pick<FeedbackReply, "kind">[] | null },
344
+ ): number {
345
+ if (row.replies) {
346
+ const entries = row.replies.filter((reply) => reply.kind === "rework").length;
347
+ // A row migrated by an app that answers `replies` but not yet `kind` keeps its body's
348
+ // count: no entry is a rework there, and the blocks are still in the body.
349
+ if (entries > 0) return entries;
350
+ }
351
+ return reworkCount(row.body ?? "");
352
+ }
353
+
323
354
  /**
324
355
  * A body split into the prose to render and the files to show beside it, in body order.
325
356
  *
@@ -1,5 +1,6 @@
1
1
  import { useState } from "react";
2
- import { BellOff } from "lucide-react";
2
+ import type { ReactNode } from "react";
3
+ import { BellOff, Paperclip } from "lucide-react";
3
4
  import { Select } from "../components/select";
4
5
  import type { CommitScope } from "../components/write-lock";
5
6
  import { useKitLabels, useKitLocale, type LabelOverride } from "../i18n/kit-labels";
@@ -8,9 +9,11 @@ import { formatDate } from "../lib/format";
8
9
  import { FeedbackStatusBadge, selectableFeedbackStatuses, type FeedbackStatus } from "./feedback-inbox";
9
10
  import { useFeedbackStatusLabels } from "./feedback-labels";
10
11
  import type { FeedbackReply } from "./feedback-public-types";
11
- import { FEEDBACK_DECIDED_STATUSES, FEEDBACK_REPLY_MAX } from "./feedback-record";
12
+ import { FEEDBACK_DECIDED_STATUSES, FEEDBACK_REPLY_MAX, attachmentName } from "./feedback-record";
12
13
  import { ChatComposer } from "./feedback-thread";
13
14
  import { DEFAULT_FEEDBACK_REPLIES_LABELS } from "./feedback-thread-parts-labels";
15
+ import { FeedbackReworkEntryChip } from "./feedback-rework-entry-chip";
16
+ import { DEFAULT_FEEDBACK_PUBLIC_LABELS } from "./feedback-public-labels";
14
17
 
15
18
  /**
16
19
  * The admin's answers as a list (docs/feedback-harmonization.md §9.7, decision 5): each
@@ -79,23 +82,53 @@ export interface FeedbackReplyTimelineProps {
79
82
  renderDate?: (iso: string) => string;
80
83
  /** Tag bodies and names `data-private` (a host's screen-share blur). Default true. */
81
84
  redact?: boolean;
85
+ /** 0.40: how a rework entry's file (`attachment_url`) is drawn under its text. Default:
86
+ * its name beside a paperclip — the file itself is in the row detail's Attachment
87
+ * section, fetched with the app's credentials. */
88
+ renderAttachment?: (url: string) => ReactNode;
82
89
  labels?: LabelOverride<FeedbackRepliesLabels>;
83
90
  className?: string;
84
91
  }
85
92
 
93
+ /** The status an entry moved the report to — "→ Done", with where from in the admin view.
94
+ * Nothing on an entry with no `status_to`: a status-less reply, or a rework migrated out
95
+ * of the body, whose move was never recorded (0.40, §10.3). */
96
+ function EntryStatus({ reply, statusChanged }: { reply: FeedbackReply; statusChanged: string }) {
97
+ if (!reply.status_to) return null;
98
+ return (
99
+ <span className="inline-flex min-w-0 max-w-full flex-wrap items-center gap-1" data-feedback-reply-status={reply.status_to}>
100
+ <span className="sr-only">{statusChanged}</span>
101
+ {reply.status_from && reply.status_from !== reply.status_to && (
102
+ <>
103
+ <FeedbackStatusBadge status={reply.status_from} />
104
+ <span aria-hidden>→</span>
105
+ </>
106
+ )}
107
+ {!reply.status_from && <span aria-hidden>→</span>}
108
+ <FeedbackStatusBadge status={reply.status_to} />
109
+ </span>
110
+ );
111
+ }
112
+
86
113
  /**
87
114
  * The replies, oldest first, down a rule: each with its author (`null` → "Admin"), its
88
115
  * time, the status it set ("→ Done"; the admin view adds where from) and its text. No
89
116
  * edit and no delete (§9.7): a correction is a new reply.
117
+ *
118
+ * 0.40 (§10.3): an author's **rework** entry (`kind: "rework"`) stands among them as the
119
+ * author's — their name (`null` → "A user", never "Admin"), the **Rework** chip, the
120
+ * status it moved the report to (none on a migrated one) and its file.
90
121
  */
91
122
  export function FeedbackReplyTimeline({
92
123
  replies,
93
124
  renderDate,
94
125
  redact = true,
126
+ renderAttachment,
95
127
  labels: labelsProp,
96
128
  className,
97
129
  }: FeedbackReplyTimelineProps) {
98
130
  const labels = useFeedbackRepliesLabels(labelsProp);
131
+ const anonymous = useKitLabels("feedbackPublic", DEFAULT_FEEDBACK_PUBLIC_LABELS).anonymous;
99
132
  const locale = useKitLocale();
100
133
  const priv = redact ? "" : undefined;
101
134
  if (replies.length === 0) {
@@ -105,40 +138,72 @@ export function FeedbackReplyTimeline({
105
138
  renderDate ? renderDate(iso) : formatDate(iso, { dateStyle: "medium", timeStyle: "short" }, { locale });
106
139
  return (
107
140
  <ol aria-label={labels.replies} className={cn("space-y-3 border-s-2 border-subtle ps-3", className)}>
108
- {replies.map((reply) => (
109
- <li key={reply.id} data-feedback-reply={reply.id} className="min-w-0 text-sm">
110
- <div className="flex flex-wrap items-center gap-x-2 gap-y-1 text-caption text-muted">
111
- <span data-private={priv} className="font-medium text-secondary">
112
- {reply.author_name ?? labels.admin}
113
- </span>
114
- <time dateTime={reply.created_at} className="tabular-nums">
115
- {dateText(reply.created_at)}
116
- </time>
117
- {reply.status_to && (
118
- <span className="inline-flex min-w-0 max-w-full flex-wrap items-center gap-1" data-feedback-reply-status={reply.status_to}>
119
- <span className="sr-only">{labels.statusChanged}</span>
120
- {reply.status_from && reply.status_from !== reply.status_to && (
121
- <>
122
- <FeedbackStatusBadge status={reply.status_from} />
123
- <span aria-hidden>→</span>
124
- </>
125
- )}
126
- {!reply.status_from && <span aria-hidden>→</span>}
127
- <FeedbackStatusBadge status={reply.status_to} />
141
+ {replies.map((reply) => {
142
+ const rework = reply.kind === "rework";
143
+ return (
144
+ <li
145
+ key={reply.id}
146
+ data-feedback-reply={reply.id}
147
+ data-feedback-entry-kind={rework ? "rework" : undefined}
148
+ className="min-w-0 text-sm"
149
+ >
150
+ <div className="flex flex-wrap items-center gap-x-2 gap-y-1 text-caption text-muted">
151
+ <span data-private={priv} className="font-medium text-secondary">
152
+ {reply.author_name ?? (rework ? anonymous : labels.admin)}
128
153
  </span>
154
+ {rework && <FeedbackReworkEntryChip />}
155
+ <time dateTime={reply.created_at} className="tabular-nums">
156
+ {dateText(reply.created_at)}
157
+ </time>
158
+ <EntryStatus reply={reply} statusChanged={labels.statusChanged} />
159
+ </div>
160
+ <p data-private={priv} dir="auto" className="mt-0.5 whitespace-pre-wrap break-words text-start text-primary">
161
+ {reply.body}
162
+ </p>
163
+ {rework && reply.attachment_url && (
164
+ <div className="mt-1 min-w-0" data-private={priv} data-feedback-entry-file="">
165
+ {renderAttachment ? (
166
+ renderAttachment(reply.attachment_url)
167
+ ) : (
168
+ <span className="inline-flex min-w-0 max-w-full items-center gap-1 text-xs text-secondary">
169
+ <Paperclip className="size-3.5 shrink-0" aria-hidden />
170
+ <span className="min-w-0 break-all">{attachmentName(reply.attachment_url)}</span>
171
+ </span>
172
+ )}
173
+ </div>
129
174
  )}
130
- </div>
131
- <p data-private={priv} dir="auto" className="mt-0.5 whitespace-pre-wrap break-words text-start text-primary">
132
- {reply.body}
133
- </p>
134
- </li>
135
- ))}
175
+ </li>
176
+ );
177
+ })}
136
178
  </ol>
137
179
  );
138
180
  }
139
181
 
140
182
  /* ── The composer ─────────────────────────────────────────────────────────── */
141
183
 
184
+ /**
185
+ * The status Select and Reply on one line, one height (keksdose live #417, Android, dark).
186
+ *
187
+ * A field and a button of the same padding are not the same height everywhere: on a touch
188
+ * screen the field's type grows to 16 px (FIELD_TOUCH_TEXT, so iOS does not zoom) and its
189
+ * box with it, while the button keeps its 14 px line — and at Large the button takes its
190
+ * 48 px touch floor, which the field has not. So the composer's action row stretches its
191
+ * two halves to one height and Reply fills it (Button's `stretch`, which is the kit's
192
+ * answer for a button beside a taller field). Scoped to this composer through the marks
193
+ * ChatComposer puts on its row: a host slot with an attachment preview must not drag the
194
+ * send button to the preview's 80 px.
195
+ *
196
+ * On a touch screen the "Ctrl+Enter to send" hint has no keyboard to speak of, and beside
197
+ * a field-height select it pushed Reply onto a line of its own on a phone — it is
198
+ * visually hidden there (still read in order). At Large the button's 48 px floor is
199
+ * taller than any field, here as in every form row of the kit.
200
+ */
201
+ const REPLY_ROW_ALIGN = cn(
202
+ "[&_[data-chat-composer-actions]]:items-stretch",
203
+ "[&_[data-chat-composer-send]]:self-stretch",
204
+ "pointer-coarse:[&_[data-chat-composer-hint]]:sr-only",
205
+ );
206
+
142
207
  export interface FeedbackReplyComposerProps {
143
208
  /** The report's status now — what "No change" keeps. */
144
209
  status: FeedbackStatus;
@@ -161,7 +226,9 @@ export interface FeedbackReplyComposerProps {
161
226
 
162
227
  /**
163
228
  * The admin's reply box (§9.7): the text, the **status** to set with it ("No change"
164
- * first, then every status — {@link selectableFeedbackStatuses}), and **Reply**. A
229
+ * first, then every status — {@link selectableFeedbackStatuses}, `REWORK` among them
230
+ * since 0.40: how an admin sends any report back, their own included, §10.2 decision 6),
231
+ * and **Reply**. A
165
232
  * {@link ChatComposer} underneath: Ctrl/⌘+Enter sends, the count runs to 8 000, a draft
166
233
  * typed while a reply is in flight stays.
167
234
  *
@@ -196,7 +263,7 @@ export function FeedbackReplyComposer({
196
263
  };
197
264
 
198
265
  return (
199
- <div className={cn("space-y-2", className)} data-feedback-reply-composer="">
266
+ <div className={cn("space-y-2", REPLY_ROW_ALIGN, className)} data-feedback-reply-composer="">
200
267
  {suggestedStatus && (
201
268
  <p className="flex items-start gap-1.5 text-xs text-secondary" data-feedback-nudge={suggestedStatus}>
202
269
  <BellOff className="mt-0.5 size-3.5 shrink-0 text-warning" aria-hidden />
@@ -211,8 +278,10 @@ export function FeedbackReplyComposer({
211
278
  showCount
212
279
  labels={{ field: labels.field, placeholder: labels.placeholder, send: labels.reply }}
213
280
  attachmentSlot={
281
+ // The kit's standard Select at field height (#417: "not invent new things where
282
+ // there are usable ones already") — `size="sm"` was a 28 px box beside the
283
+ // 38 px Reply. See REPLY_ROW_ALIGN for how the two keep one height.
214
284
  <Select
215
- size="sm"
216
285
  aria-label={labels.status}
217
286
  value={picked}
218
287
  disabled={pending}
@@ -0,0 +1,18 @@
1
+ import { Chip } from "../components/chip";
2
+ import { useKitLabels } from "../i18n/kit-labels";
3
+ import { DEFAULT_FEEDBACK_PAGE_LABELS } from "./feedback-table-labels";
4
+
5
+ /**
6
+ * The **Rework** chip on an author's rework entry (0.40, docs/feedback-harmonization.md
7
+ * §10.3) — the table's own `feedbackPage.reworkChip(1)` and tone, so the entry and the
8
+ * row's chip say the same. Drawn by `FeedbackReplyTimeline` and `FeedbackCommentThread`.
9
+ * Internal: not re-exported.
10
+ */
11
+ export function FeedbackReworkEntryChip({ className }: { className?: string }) {
12
+ const pageLabels = useKitLabels("feedbackPage", DEFAULT_FEEDBACK_PAGE_LABELS);
13
+ return (
14
+ <Chip size="sm" tone="warning" caps className={className} data-feedback-entry-rework="">
15
+ {pageLabels.reworkChip(1)}
16
+ </Chip>
17
+ );
18
+ }