@eifi1/ui-kit 0.30.0 → 0.31.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.
Files changed (269) hide show
  1. package/README.md +42 -5
  2. package/dist/account/data-export-setting.js +3 -2
  3. package/dist/account/data-export-setting.js.map +1 -1
  4. package/dist/account/delete-account-setting.d.ts +5 -1
  5. package/dist/account/delete-account-setting.js +3 -2
  6. package/dist/account/delete-account-setting.js.map +1 -1
  7. package/dist/account/email-change-setting.js +4 -3
  8. package/dist/account/email-change-setting.js.map +1 -1
  9. package/dist/account/sessions-setting.js +3 -2
  10. package/dist/account/sessions-setting.js.map +1 -1
  11. package/dist/admin/admin-action-confirm.d.ts +5 -1
  12. package/dist/admin/admin-parts.d.ts +8 -1
  13. package/dist/admin/admin-parts.js +1 -1
  14. package/dist/admin/admin-parts.js.map +1 -1
  15. package/dist/admin/invitations-panel.d.ts +5 -1
  16. package/dist/admin/invitations-panel.js +3 -2
  17. package/dist/admin/invitations-panel.js.map +1 -1
  18. package/dist/admin/reviewer-scope-editor.d.ts +5 -1
  19. package/dist/admin/role-select.d.ts +5 -1
  20. package/dist/admin/transfer-ownership-dialog.d.ts +5 -1
  21. package/dist/admin/user-roster.d.ts +5 -1
  22. package/dist/auth/auth-errors.d.ts +24 -6
  23. package/dist/auth/auth-errors.js +7 -1
  24. package/dist/auth/auth-errors.js.map +1 -1
  25. package/dist/auth/register-form.d.ts +5 -1
  26. package/dist/auth/reset-password-form.d.ts +1 -1
  27. package/dist/auth/reset-password-form.js +2 -1
  28. package/dist/auth/reset-password-form.js.map +1 -1
  29. package/dist/auth/sign-in-form.d.ts +15 -1
  30. package/dist/auth/sign-in-form.js +12 -3
  31. package/dist/auth/sign-in-form.js.map +1 -1
  32. package/dist/auth/verify-email.d.ts +4 -0
  33. package/dist/auth/verify-email.js +3 -2
  34. package/dist/auth/verify-email.js.map +1 -1
  35. package/dist/chart.d.ts +5 -1
  36. package/dist/components/account-settings-labels.d.ts +12 -0
  37. package/dist/components/account-settings-labels.js +2 -1
  38. package/dist/components/account-settings-labels.js.map +1 -1
  39. package/dist/components/account-settings.d.ts +8 -2
  40. package/dist/components/account-settings.js +19 -11
  41. package/dist/components/account-settings.js.map +1 -1
  42. package/dist/components/amount-input.d.ts +5 -1
  43. package/dist/components/button-group.d.ts +5 -1
  44. package/dist/components/calculator.d.ts +5 -1
  45. package/dist/components/confirm-dialog.d.ts +5 -1
  46. package/dist/components/confirm-dialog.js +7 -1
  47. package/dist/components/confirm-dialog.js.map +1 -1
  48. package/dist/components/copy-button.d.ts +5 -1
  49. package/dist/components/danger-confirm.d.ts +5 -1
  50. package/dist/components/data-table.js +8 -1
  51. package/dist/components/data-table.js.map +1 -1
  52. package/dist/components/date-picker.js +9 -4
  53. package/dist/components/date-picker.js.map +1 -1
  54. package/dist/components/facing-pair.d.ts +5 -1
  55. package/dist/components/file-button.d.ts +5 -1
  56. package/dist/components/file-dropzone.d.ts +5 -1
  57. package/dist/components/form-actions.d.ts +5 -1
  58. package/dist/components/iban-input.d.ts +5 -1
  59. package/dist/components/language-select.d.ts +5 -1
  60. package/dist/components/legal-page.d.ts +5 -1
  61. package/dist/components/legal.d.ts +5 -1
  62. package/dist/components/money-field.d.ts +5 -1
  63. package/dist/components/number-field.d.ts +5 -1
  64. package/dist/components/number-input.d.ts +5 -1
  65. package/dist/components/numpad-sheet.d.ts +5 -1
  66. package/dist/components/passkeys-setting.js +2 -1
  67. package/dist/components/passkeys-setting.js.map +1 -1
  68. package/dist/components/phone-input.d.ts +5 -1
  69. package/dist/components/series-chart.d.ts +5 -1
  70. package/dist/components/settings-fields.d.ts +40 -3
  71. package/dist/components/settings-fields.js +20 -1
  72. package/dist/components/settings-fields.js.map +1 -1
  73. package/dist/components/share-card.d.ts +5 -1
  74. package/dist/components/text-link.d.ts +5 -1
  75. package/dist/components/time-input.d.ts +5 -1
  76. package/dist/components/ui.d.ts +5 -1
  77. package/dist/{delete-account-setting-CGVMckMw.d.ts → delete-account-setting-CpIhzxxZ.d.ts} +60 -23
  78. package/dist/demo/demo-banner.d.ts +51 -0
  79. package/dist/demo/demo-banner.js +97 -0
  80. package/dist/demo/demo-banner.js.map +1 -0
  81. package/dist/demo/demo-countdown.d.ts +58 -0
  82. package/dist/demo/demo-countdown.js +79 -0
  83. package/dist/demo/demo-countdown.js.map +1 -0
  84. package/dist/demo/demo-ended.d.ts +45 -0
  85. package/dist/demo/demo-ended.js +42 -0
  86. package/dist/demo/demo-ended.js.map +1 -0
  87. package/dist/demo/demo-labels.d.ts +53 -0
  88. package/dist/demo/demo-labels.js +29 -0
  89. package/dist/demo/demo-labels.js.map +1 -0
  90. package/dist/demo/demo-session.d.ts +30 -0
  91. package/dist/demo/demo-session.js +8 -0
  92. package/dist/demo/demo-session.js.map +1 -0
  93. package/dist/demo/demo-start.d.ts +54 -0
  94. package/dist/demo/demo-start.js +68 -0
  95. package/dist/demo/demo-start.js.map +1 -0
  96. package/dist/feedback/feedback-attachment.d.ts +5 -1
  97. package/dist/feedback/feedback-context.d.ts +5 -1
  98. package/dist/feedback/feedback-crash.d.ts +5 -1
  99. package/dist/feedback/feedback-dialog.d.ts +5 -1
  100. package/dist/feedback/feedback-inbox.d.ts +5 -1
  101. package/dist/feedback/feedback-inbox.js +12 -2
  102. package/dist/feedback/feedback-inbox.js.map +1 -1
  103. package/dist/feedback/feedback-labels.d.ts +5 -1
  104. package/dist/feedback/feedback-labels.js +1 -0
  105. package/dist/feedback/feedback-labels.js.map +1 -1
  106. package/dist/feedback/feedback-menu.d.ts +5 -1
  107. package/dist/feedback/feedback-record.d.ts +5 -1
  108. package/dist/feedback/feedback-record.js +2 -0
  109. package/dist/feedback/feedback-record.js.map +1 -1
  110. package/dist/feedback/feedback-row-detail.d.ts +5 -1
  111. package/dist/feedback/feedback-row-detail.js +7 -0
  112. package/dist/feedback/feedback-row-detail.js.map +1 -1
  113. package/dist/feedback/feedback-status-undo.d.ts +5 -1
  114. package/dist/feedback/feedback-submit.d.ts +5 -1
  115. package/dist/feedback/feedback-swipe.d.ts +6 -2
  116. package/dist/feedback/feedback-swipe.js.map +1 -1
  117. package/dist/feedback/feedback-table.d.ts +5 -1
  118. package/dist/feedback/feedback-thread.d.ts +5 -1
  119. package/dist/feedback.d.ts +5 -1
  120. package/dist/hooks/use-file-drop.d.ts +5 -1
  121. package/dist/i18n/defaults.d.ts +5 -1
  122. package/dist/i18n/defaults.js +6 -0
  123. package/dist/i18n/defaults.js.map +1 -1
  124. package/dist/i18n/german.d.ts +5 -1
  125. package/dist/i18n/german.js +85 -2
  126. package/dist/i18n/german.js.map +1 -1
  127. package/dist/i18n/kit-labels.d.ts +5 -1
  128. package/dist/i18n/kit-labels.js.map +1 -1
  129. package/dist/i18n/languages.d.ts +5 -1
  130. package/dist/i18n/locales/de-CH.d.ts +5 -1
  131. package/dist/i18n/locales/en.d.ts +5 -1
  132. package/dist/i18n/locales/es.d.ts +5 -1
  133. package/dist/i18n/locales/es.js +82 -2
  134. package/dist/i18n/locales/es.js.map +1 -1
  135. package/dist/i18n/locales/fr.d.ts +5 -1
  136. package/dist/i18n/locales/fr.js +83 -2
  137. package/dist/i18n/locales/fr.js.map +1 -1
  138. package/dist/i18n/locales/hu.d.ts +5 -1
  139. package/dist/i18n/locales/hu.js +83 -2
  140. package/dist/i18n/locales/hu.js.map +1 -1
  141. package/dist/i18n/locales/it.d.ts +5 -1
  142. package/dist/i18n/locales/it.js +82 -2
  143. package/dist/i18n/locales/it.js.map +1 -1
  144. package/dist/i18n/locales/zh.d.ts +5 -1
  145. package/dist/i18n/locales/zh.js +81 -2
  146. package/dist/i18n/locales/zh.js.map +1 -1
  147. package/dist/i18n/review.d.ts +5 -1
  148. package/dist/i18n/review.js +15 -1
  149. package/dist/i18n/review.js.map +1 -1
  150. package/dist/index.d.ts +25 -3
  151. package/dist/index.js +96 -0
  152. package/dist/index.js.map +1 -1
  153. package/dist/landing/access.d.ts +48 -0
  154. package/dist/landing/access.js +38 -0
  155. package/dist/landing/access.js.map +1 -0
  156. package/dist/landing/landing-actions.d.ts +140 -0
  157. package/dist/landing/landing-actions.js +39 -0
  158. package/dist/landing/landing-actions.js.map +1 -0
  159. package/dist/landing/landing-labels.d.ts +43 -0
  160. package/dist/landing/landing-labels.js +23 -0
  161. package/dist/landing/landing-labels.js.map +1 -0
  162. package/dist/landing/landing-sections.d.ts +218 -0
  163. package/dist/landing/landing-sections.js +167 -0
  164. package/dist/landing/landing-sections.js.map +1 -0
  165. package/dist/landing/page-seo.d.ts +99 -0
  166. package/dist/landing/page-seo.js +121 -0
  167. package/dist/landing/page-seo.js.map +1 -0
  168. package/dist/landing/public-header.d.ts +148 -0
  169. package/dist/landing/public-header.js +49 -0
  170. package/dist/landing/public-header.js.map +1 -0
  171. package/dist/landing/routing.d.ts +231 -0
  172. package/dist/landing/routing.js +84 -0
  173. package/dist/landing/routing.js.map +1 -0
  174. package/dist/rhf/fields.d.ts +5 -1
  175. package/dist/rhf/form.d.ts +5 -1
  176. package/dist/rhf.d.ts +5 -1
  177. package/dist/settings/settings-catalogue.d.ts +84 -0
  178. package/dist/settings/settings-catalogue.js +47 -0
  179. package/dist/settings/settings-catalogue.js.map +1 -0
  180. package/dist/settings/settings-context.d.ts +31 -0
  181. package/dist/settings/settings-context.js +11 -0
  182. package/dist/settings/settings-context.js.map +1 -0
  183. package/dist/settings/settings-heading.d.ts +50 -0
  184. package/dist/settings/settings-heading.js +27 -0
  185. package/dist/settings/settings-heading.js.map +1 -0
  186. package/dist/settings/settings-labels.d.ts +48 -0
  187. package/dist/settings/settings-labels.js +43 -0
  188. package/dist/settings/settings-labels.js.map +1 -0
  189. package/dist/settings/settings-layout.d.ts +77 -0
  190. package/dist/settings/settings-layout.js +242 -0
  191. package/dist/settings/settings-layout.js.map +1 -0
  192. package/dist/settings/settings-section.d.ts +133 -0
  193. package/dist/settings/settings-section.js +63 -0
  194. package/dist/settings/settings-section.js.map +1 -0
  195. package/dist/settings/use-account-language.d.ts +83 -0
  196. package/dist/settings/use-account-language.js +62 -0
  197. package/dist/settings/use-account-language.js.map +1 -0
  198. package/dist/settings/use-settings-focus.d.ts +34 -0
  199. package/dist/settings/use-settings-focus.js +77 -0
  200. package/dist/settings/use-settings-focus.js.map +1 -0
  201. package/dist/settings/use-settings-route.d.ts +118 -0
  202. package/dist/settings/use-settings-route.js +120 -0
  203. package/dist/settings/use-settings-route.js.map +1 -0
  204. package/dist/shell/app-shell.d.ts +11 -1
  205. package/dist/shell/app-shell.js +3 -2
  206. package/dist/shell/app-shell.js.map +1 -1
  207. package/dist/shell/top-bar-brand.d.ts +5 -1
  208. package/dist/shell.d.ts +15 -1
  209. package/dist/shell.js +56 -0
  210. package/dist/shell.js.map +1 -1
  211. package/dist/wizard/stepper-nav.d.ts +5 -1
  212. package/dist/wizard.d.ts +5 -1
  213. package/package.json +2 -1
  214. package/src/account/data-export-setting.tsx +3 -2
  215. package/src/account/delete-account-setting.tsx +3 -2
  216. package/src/account/email-change-setting.tsx +4 -7
  217. package/src/account/sessions-setting.tsx +3 -2
  218. package/src/admin/admin-parts.ts +9 -2
  219. package/src/admin/invitations-panel.tsx +11 -4
  220. package/src/auth/auth-errors.ts +36 -5
  221. package/src/auth/reset-password-form.tsx +6 -2
  222. package/src/auth/sign-in-form.tsx +34 -3
  223. package/src/auth/verify-email.tsx +14 -2
  224. package/src/components/account-settings-labels.ts +13 -0
  225. package/src/components/account-settings.tsx +33 -12
  226. package/src/components/confirm-dialog.tsx +20 -1
  227. package/src/components/data-table.tsx +8 -0
  228. package/src/components/date-picker.tsx +9 -4
  229. package/src/components/passkeys-setting.tsx +2 -1
  230. package/src/components/settings-fields.tsx +50 -1
  231. package/src/demo/demo-banner.tsx +159 -0
  232. package/src/demo/demo-countdown.ts +141 -0
  233. package/src/demo/demo-ended.tsx +89 -0
  234. package/src/demo/demo-labels.ts +78 -0
  235. package/src/demo/demo-session.ts +32 -0
  236. package/src/demo/demo-start.tsx +150 -0
  237. package/src/feedback/feedback-inbox.tsx +21 -8
  238. package/src/feedback/feedback-labels.ts +2 -1
  239. package/src/feedback/feedback-record.ts +10 -7
  240. package/src/feedback/feedback-row-detail.tsx +28 -4
  241. package/src/feedback/feedback-swipe.tsx +1 -1
  242. package/src/i18n/defaults.ts +6 -0
  243. package/src/i18n/german.ts +87 -0
  244. package/src/i18n/kit-labels.tsx +9 -0
  245. package/src/i18n/locales/es.ts +84 -0
  246. package/src/i18n/locales/fr.ts +85 -0
  247. package/src/i18n/locales/hu.ts +85 -0
  248. package/src/i18n/locales/it.ts +84 -0
  249. package/src/i18n/locales/zh.ts +82 -0
  250. package/src/i18n/review.ts +14 -0
  251. package/src/index.ts +98 -1
  252. package/src/landing/access.ts +97 -0
  253. package/src/landing/landing-actions.tsx +96 -0
  254. package/src/landing/landing-labels.ts +59 -0
  255. package/src/landing/landing-sections.tsx +337 -0
  256. package/src/landing/page-seo.ts +251 -0
  257. package/src/landing/public-header.tsx +120 -0
  258. package/src/landing/routing.tsx +226 -0
  259. package/src/settings/settings-catalogue.ts +132 -0
  260. package/src/settings/settings-context.ts +33 -0
  261. package/src/settings/settings-heading.tsx +65 -0
  262. package/src/settings/settings-labels.ts +87 -0
  263. package/src/settings/settings-layout.tsx +379 -0
  264. package/src/settings/settings-section.tsx +108 -0
  265. package/src/settings/use-account-language.ts +146 -0
  266. package/src/settings/use-settings-focus.ts +120 -0
  267. package/src/settings/use-settings-route.ts +258 -0
  268. package/src/shell/app-shell.tsx +10 -2
  269. package/src/shell.ts +51 -0
@@ -0,0 +1,150 @@
1
+ import { useState } from "react";
2
+ import type { ComponentPropsWithoutRef } from "react";
3
+
4
+ import { cn } from "../lib/cn";
5
+ import { AlertBanner } from "../components/alert-banner";
6
+ import { Button, Spinner } from "../components/ui";
7
+ import { useNoIndex } from "../hooks/use-noindex";
8
+ import { authErrorCode, isRateLimited, retryAfterSeconds } from "../auth/auth-errors";
9
+ import { useOncePerKey } from "../auth/status-parts";
10
+ import { useAccessAction } from "../landing/access";
11
+ import type { AccessChoice } from "../landing/access";
12
+ import type { LandingLabels } from "../landing/landing-labels";
13
+ import { useDemoLabels } from "./demo-labels";
14
+ import type { DemoLabels } from "./demo-labels";
15
+
16
+ /** Why a start was refused, in the page's terms (§5.2). */
17
+ type Refusal =
18
+ | { reason: "rateLimited"; minutes?: number }
19
+ | { reason: "capacity" }
20
+ | { reason: "unavailable" }
21
+ | { reason: "failed" };
22
+
23
+ /**
24
+ * The server's answer to `POST /auth/demo-session`, read as a refusal (§5.1, §6.2):
25
+ * `demo_rate_limited` (429, with `Retry-After`), `demo_capacity` (429, without),
26
+ * `demo_disabled` (404) and `demo_not_ready` (503) by their codes; a bare 429 as the
27
+ * per-IP window too; anything else is a plain failure.
28
+ */
29
+ function refusalOf(error: unknown): Refusal {
30
+ const code = authErrorCode(error);
31
+ if (code === "demo_capacity") return { reason: "capacity" };
32
+ if (code === "demo_disabled" || code === "demo_not_ready") return { reason: "unavailable" };
33
+ if (code === "demo_rate_limited" || (code === undefined && isRateLimited(error))) {
34
+ const seconds = retryAfterSeconds(error);
35
+ return { reason: "rateLimited", minutes: seconds ? Math.ceil(seconds / 60) : undefined };
36
+ }
37
+ return { reason: "failed" };
38
+ }
39
+
40
+ function messageOf(refusal: Refusal, labels: DemoLabels): string {
41
+ switch (refusal.reason) {
42
+ case "rateLimited":
43
+ return labels.rateLimited(refusal.minutes);
44
+ case "capacity":
45
+ return labels.capacity;
46
+ case "unavailable":
47
+ return labels.unavailable;
48
+ case "failed":
49
+ return labels.failed;
50
+ }
51
+ }
52
+
53
+ /** The one start a mount makes. */
54
+ const ONCE = "demo";
55
+
56
+ export interface DemoStartProps<T> extends Omit<ComponentPropsWithoutRef<"div">, "children"> {
57
+ /**
58
+ * Create the demo: the app's `POST /auth/demo-session {locale}`. Resolve with what it
59
+ * answered (the `TokenResponse`, `refresh_token: null`, `expires_at`); reject with the
60
+ * client's error — the codes are read out of it. Called once per mount.
61
+ */
62
+ start: () => Promise<T>;
63
+ /** The demo exists: store the session (and, for kastlan, `kastlan.demoSession`) and
64
+ * replace to the app's home. The page keeps saying "Starting the demo…" meanwhile. */
65
+ onStarted: (session: T) => void;
66
+ /** "Request access" beside a refusal. */
67
+ access: AccessChoice;
68
+ /** "Back to the start page". Default `/`. */
69
+ backHref?: string;
70
+ /** `false` adds no `noindex` — for a preview. Default `true`: `/demo` is never indexed
71
+ * (§4.3). */
72
+ noIndex?: boolean;
73
+ labels?: Partial<DemoLabels>;
74
+ /** The access words (`landing` namespace) for this instance. */
75
+ landingLabels?: Partial<LandingLabels>;
76
+ }
77
+
78
+ /**
79
+ * The `/demo` page's content (docs/landing-demo-harmonization.md §5.2): it starts the
80
+ * demo ONCE — StrictMode's double mount included, keksdose's ref guard — and says
81
+ * "Starting the demo…" until the app has the session and has moved on.
82
+ *
83
+ * A refusal says why, in the visitor's words, and offers "Request access" and "Back to
84
+ * the start page":
85
+ *
86
+ * | code | says |
87
+ * |---|---|
88
+ * | `demo_rate_limited` (429) | "Too many demos from this network. Try again in N min." — N from `Retry-After` |
89
+ * | `demo_capacity` (429) | "The demo is full right now. Try again later." |
90
+ * | `demo_disabled` (404), `demo_not_ready` (503) | "The demo isn't available right now." |
91
+ * | anything else | "The demo could not be started. Please try again." |
92
+ *
93
+ * keksdose fell back to `/register` with a toast; registration is invitation-only now,
94
+ * so the way on is the access mail.
95
+ *
96
+ * Put it in the app's `AuthLayout`. A live demo session never reaches it: the `/demo`
97
+ * route sits in `RedirectIfAuthed` WITHOUT `allowDemo`, which continues the live demo at
98
+ * its resume target instead of starting a second one. A cold start is the kit's
99
+ * `ServerWakeNotice`'s, once the app's watcher counts this POST
100
+ * (`watchReadsAnd(/\/auth\/demo-session\b/)`).
101
+ */
102
+ export function DemoStart<T>({
103
+ start,
104
+ onStarted,
105
+ access,
106
+ backHref = "/",
107
+ noIndex = true,
108
+ labels: labelsProp,
109
+ landingLabels,
110
+ className,
111
+ ...rest
112
+ }: DemoStartProps<T>) {
113
+ const labels = useDemoLabels(labelsProp);
114
+ const accessLink = useAccessAction(access, landingLabels);
115
+ const [refusal, setRefusal] = useState<Refusal | null>(null);
116
+ useNoIndex(noIndex);
117
+ useOncePerKey<T>(ONCE, () => start(), (_key, result) => {
118
+ if (result.ok) onStarted(result.value);
119
+ else setRefusal(refusalOf(result.error));
120
+ });
121
+
122
+ return (
123
+ <div {...rest} className={cn("flex flex-col items-center gap-4 py-4 text-center", className)}>
124
+ {/* One live region, there from the start, so the refusal that replaces the
125
+ progress line is announced (a region inserted with its text often is not). */}
126
+ <div role="status" aria-live="polite" className="w-full">
127
+ {refusal === null ? (
128
+ <p className="flex items-center justify-center gap-2 text-sm text-[var(--text-secondary)]">
129
+ <Spinner label={null} className="size-5" />
130
+ {labels.starting}
131
+ </p>
132
+ ) : (
133
+ <AlertBanner tone={refusal.reason === "failed" ? "danger" : "warning"} className="text-start">
134
+ {messageOf(refusal, labels)}
135
+ </AlertBanner>
136
+ )}
137
+ </div>
138
+ {refusal !== null && (
139
+ <div className="flex flex-wrap justify-center gap-3">
140
+ <Button href={accessLink.href} variant="brand">
141
+ {accessLink.label}
142
+ </Button>
143
+ <Button href={backHref} variant="secondary">
144
+ {labels.backToStart}
145
+ </Button>
146
+ </div>
147
+ )}
148
+ </div>
149
+ );
150
+ }
@@ -4,6 +4,7 @@ import {
4
4
  Ban,
5
5
  Bug,
6
6
  CheckCircle2,
7
+ ClipboardCheck,
7
8
  CloudUpload,
8
9
  Eye,
9
10
  HelpCircle,
@@ -55,13 +56,16 @@ import { useFeedbackCategoryLabels, useFeedbackStatusLabels } from "./feedback-l
55
56
  * a `label` the caller passes still wins.
56
57
  */
57
58
 
58
- /** The seven states a report can be in.
59
+ /** The eight states a report can be in.
59
60
  *
60
- * Shared value for value across both apps on purpose: it is what lets one habit,
61
- * and one agent prompt, work on either repo. Four are a chain and three sit off
62
- * it — see {@link visibleFeedbackStatuses} for what that buys. */
61
+ * Shared value for value across the apps on purpose: it is what lets one habit,
62
+ * and one agent prompt, work on any repo. Five are a chain and three sit off
63
+ * it — see {@link visibleFeedbackStatuses} for what that buys. `READY` joined in 0.31
64
+ * (feedback contract §8.2): OPEN is "filed, not yet triaged", READY "released for
65
+ * implementation", and nobody works an OPEN row. */
63
66
  export type FeedbackStatus =
64
67
  | "OPEN"
68
+ | "READY"
65
69
  | "IN_PROGRESS"
66
70
  | "IN_EVALUATION"
67
71
  | "NEEDS_LIVE_TEST"
@@ -81,7 +85,7 @@ export type FeedbackCategory = "CRASH" | "BUG" | "IDEA" | "QUESTION" | "OTHER";
81
85
  * its own shape as well. Declaration order is chain order, and
82
86
  * {@link FEEDBACK_STATUS_ORDER} is derived from it rather than restated, because
83
87
  * a restated list cannot be checked for exhaustiveness: an eighth status would
84
- * break the build here and leave a literal seven long.
88
+ * break the build here and leave a literal eight long (as READY did in 0.31).
85
89
  */
86
90
  export const FEEDBACK_STATUS_META: Record<
87
91
  FeedbackStatus,
@@ -92,6 +96,15 @@ export const FEEDBACK_STATUS_META: Record<
92
96
  activeBg: "bg-[var(--bg-active)]",
93
97
  activeText: "text-[var(--text-primary)]",
94
98
  },
99
+ // Triaged and released for implementation (feedback contract §8.2, keksdose live
100
+ // #396): OPEN is "filed, nobody has looked yet", READY is "go". The brand tint at full
101
+ // strength, so it reads as the go-ahead beside OPEN's neutral fill and
102
+ // NEEDS_LIVE_TEST's muted brand.
103
+ READY: {
104
+ icon: ClipboardCheck,
105
+ activeBg: "bg-[var(--brand-bg)]",
106
+ activeText: "text-[var(--brand)]",
107
+ },
95
108
  IN_PROGRESS: {
96
109
  icon: Wrench,
97
110
  activeBg: "bg-[var(--warning-bg)]",
@@ -155,7 +168,7 @@ export const FEEDBACK_STATUS_ORDER = Object.keys(FEEDBACK_STATUS_META) as Feedba
155
168
  * through them to reach `DONE`. */
156
169
  const OFF_CHAIN: FeedbackStatus[] = ["NEEDS_LIVE_TEST", "POSTPONED", "WONT_DO"];
157
170
 
158
- const FORWARD_CHAIN: FeedbackStatus[] = ["OPEN", "IN_PROGRESS", "IN_EVALUATION", "DONE"];
171
+ const FORWARD_CHAIN: FeedbackStatus[] = ["OPEN", "READY", "IN_PROGRESS", "IN_EVALUATION", "DONE"];
159
172
 
160
173
  /** The next status along the chain, or null at the end (or off it, as `WONT_DO`
161
174
  * is). What a swipe-to-advance gesture commits. */
@@ -168,7 +181,7 @@ export function nextFeedbackStatus(current: FeedbackStatus): FeedbackStatus | nu
168
181
  * The steps that make sense from where a row currently stands.
169
182
  *
170
183
  * For the compact control in a table cell: it is a glanceable triage affordance,
171
- * and a row of seven icons in a cell is noise. One step forward, one step back
184
+ * and a row of eight icons in a cell is noise. One step forward, one step back
172
185
  * (so an item can be sent back for rework), and any of the three off-chain
173
186
  * verdicts — which is what makes those usable as verdicts at all.
174
187
  *
@@ -452,7 +465,7 @@ export function FeedbackStatusTransitions({
452
465
  ),
453
466
  // Both stay unclickable, but only the read-only case is DIMMED: the
454
467
  // current status is the one thing in the row that has to be legible
455
- // at a glance, and among seven pills a faded active one reads as
468
+ // at a glance, and among eight pills a faded active one reads as
456
469
  // "unavailable" rather than as "this is where the row stands".
457
470
  (!canEdit || active) && "cursor-default",
458
471
  !canEdit && "opacity-60",
@@ -24,7 +24,7 @@ import type { FeedbackCategory, FeedbackStatus } from "./feedback-inbox";
24
24
  * `feedbackStatus` — one word per status, keyed by the enum value so
25
25
  * `labels[row.status]` is the whole lookup.
26
26
  *
27
- * de-CH canon: OPEN "Offen" · IN_PROGRESS "In Bearbeitung" · IN_EVALUATION "Zur Prüfung"
27
+ * de-CH canon: OPEN "Offen" · READY "Bereit zur Umsetzung" · IN_PROGRESS "In Bearbeitung" · IN_EVALUATION "Zur Prüfung"
28
28
  * · NEEDS_LIVE_TEST "Live testen" · POSTPONED "Zurückgestellt" · DONE "Erledigt" ·
29
29
  * WONT_DO "Wird nicht umgesetzt".
30
30
  */
@@ -32,6 +32,7 @@ export type FeedbackStatusLabels = Record<FeedbackStatus, string>;
32
32
 
33
33
  export const DEFAULT_FEEDBACK_STATUS_LABELS: FeedbackStatusLabels = {
34
34
  OPEN: "Open",
35
+ READY: "Ready to implement",
35
36
  IN_PROGRESS: "In progress",
36
37
  IN_EVALUATION: "In evaluation",
37
38
  NEEDS_LIVE_TEST: "Test when live",
@@ -158,22 +158,25 @@ export const FEEDBACK_REWORKABLE_STATUSES: readonly FeedbackStatus[] = Object.fr
158
158
 
159
159
  /**
160
160
  * The statuses in which the AUTHOR may still edit title, body and category (§3.4,
161
- * keksdose `canAuthorEdit`, `feedback-page.tsx:398`): `OPEN` and `IN_PROGRESS`. Once the
162
- * team has answered, the description is what they answered and stays as it was; a
163
- * correction goes in as a rework.
161
+ * keksdose `canAuthorEdit`, `feedback-page.tsx:398`): `OPEN`, `READY` and `IN_PROGRESS`.
162
+ * Once the team has answered, the description is what they answered and stays as it
163
+ * was; a correction goes in as a rework.
164
164
  */
165
165
  export const FEEDBACK_AUTHOR_EDITABLE_STATUSES: readonly FeedbackStatus[] = Object.freeze([
166
166
  "OPEN",
167
+ "READY",
167
168
  "IN_PROGRESS",
168
169
  ] as const);
169
170
 
170
171
  /**
171
- * The two statuses that wait on the person triaging: answered and handed back for a
172
- * verdict, or answered and only checkable on a deployed build (keksdose
173
- * `AWAITING_VERDICT`, live #330). The phone's "Only what is waiting for you" toggle
174
- * narrows the inbox to exactly these (§4.3).
172
+ * The statuses that wait on the person triaging: filed and not yet triaged (`OPEN`,
173
+ * since READY took over "released for implementation" in 0.31, feedback contract §8.2),
174
+ * answered and handed back for a verdict, or answered and only checkable on a deployed
175
+ * build (keksdose `AWAITING_VERDICT`, live #330). The phone's "Only what is waiting for
176
+ * you" toggle narrows the inbox to exactly these (§4.3).
175
177
  */
176
178
  export const FEEDBACK_AWAITING_STATUSES: readonly FeedbackStatus[] = Object.freeze([
179
+ "OPEN",
177
180
  "IN_EVALUATION",
178
181
  "NEEDS_LIVE_TEST",
179
182
  ] as const);
@@ -12,6 +12,7 @@ import { formatDate } from "../lib/format";
12
12
  import { DEFAULT_MAX_ATTACHMENT_BYTES } from "./feedback-attachment";
13
13
  import { DEFAULT_FEEDBACK_DIALOG_LABELS } from "./feedback-dialog";
14
14
  import {
15
+ FeedbackCategoryBadge,
15
16
  FeedbackDetail,
16
17
  FeedbackDetailSection,
17
18
  FeedbackNoteEditor,
@@ -63,8 +64,9 @@ import type { FeedbackStatusChange } from "./feedback-status-undo";
63
64
  * path (`/accounts?p=2`): keksdose's `new URL(…)` throws on one and the section
64
65
  * vanished. kastlan already fell back to `context.route` (`feedbackRaw`, `:97`).
65
66
  *
66
- * The sections, in the contract's order: **Description** (the body; the author's
67
- * Edit while OPEN / IN_PROGRESS), **URL**, **Attachment**, **Outcome** (the admin's
67
+ * The sections, in the contract's order: **Subject** (the category and the title, §8.1;
68
+ * `showSubject={false}` leaves it out), **Description** (the body; the author's
69
+ * Edit while OPEN / READY / IN_PROGRESS), **URL**, **Attachment**, **Outcome** (the admin's
68
70
  * outcome editor, Rework, Open page), **Send for rework** (only while reworking) and
69
71
  * **Status**. No comment thread (§2.2: kastlan's comments fold into the body, §7.7).
70
72
  *
@@ -81,7 +83,11 @@ import type { FeedbackStatusChange } from "./feedback-status-undo";
81
83
  * canon (keksdose `de-CH.json`, ss never ß, "Nacharbeit" throughout — §7.14) on each key.
82
84
  */
83
85
  export interface FeedbackDetailLabels {
84
- /** Section 1's heading — keksdose's `feedback.body` (§7.3). The dialog's field asks
86
+ /** The first section's heading: what the report is called (feedback contract §8.1,
87
+ * keksdose live #395 — with the row expanded and the table scrolled, nothing said
88
+ * which entry was being answered). de-CH: "Betreff" */
89
+ subject: string;
90
+ /** The description section's heading — keksdose's `feedback.body` (§7.3). The dialog's field asks
85
91
  * "What happened? (optional)"; the description, once filed, is headed as what it is
86
92
  * — and as the author's "Edit description" calls it. de-CH: "Beschreibung" */
87
93
  body: string;
@@ -140,6 +146,7 @@ export interface FeedbackDetailLabels {
140
146
  }
141
147
 
142
148
  export const DEFAULT_FEEDBACK_DETAIL_LABELS: FeedbackDetailLabels = {
149
+ subject: "Subject",
143
150
  body: "Description",
144
151
  edit: "Edit",
145
152
  editDescription: "Edit description",
@@ -316,7 +323,7 @@ function AttachmentDownload({
316
323
  export interface FeedbackRowAccess {
317
324
  /** The viewer filed it (`viewerId === row.user_id`; never for an erased author). */
318
325
  isAuthor: boolean;
319
- /** "Edit description": the author, while the row is OPEN or IN_PROGRESS — on either
326
+ /** "Edit description": the author, while the row is OPEN, READY or IN_PROGRESS — on either
320
327
  * page (keksdose `canAuthorEdit`, `feedback-page.tsx:398`, is not gated on `mine`). */
321
328
  canEditDescription: boolean;
322
329
  /** The outcome editor: the admin on the inbox (`canEdit`). */
@@ -563,6 +570,12 @@ export interface FeedbackRowDetailProps {
563
570
  * BUDGET, and a person viewing one still owns their own reports.
564
571
  */
565
572
  commit?: boolean;
573
+ /**
574
+ * Open with the subject: the category badge and the title (feedback contract §8.1,
575
+ * keksdose live #395). On by default; `false` for a page that already shows the title
576
+ * beside the detail.
577
+ */
578
+ showSubject?: boolean;
566
579
  labels?: Partial<FeedbackDetailLabels>;
567
580
  className?: string;
568
581
  }
@@ -603,6 +616,7 @@ export function FeedbackRowDetail({
603
616
  reworkAttachment,
604
617
  renderDate,
605
618
  commit,
619
+ showSubject = true,
606
620
  labels: labelsProp,
607
621
  className,
608
622
  }: FeedbackRowDetailProps) {
@@ -653,6 +667,16 @@ export function FeedbackRowDetail({
653
667
 
654
668
  return (
655
669
  <FeedbackDetail className={className}>
670
+ {showSubject && (
671
+ <FeedbackDetailSection title={labels.subject}>
672
+ <p className="flex flex-wrap items-center gap-x-2 gap-y-1 text-sm" data-feedback-subject="">
673
+ <FeedbackCategoryBadge category={row.category} />
674
+ <span className="min-w-0 font-medium text-[var(--text-primary)] [overflow-wrap:anywhere]">
675
+ {row.title}
676
+ </span>
677
+ </p>
678
+ </FeedbackDetailSection>
679
+ )}
656
680
  <FeedbackDetailSection
657
681
  title={labels.body}
658
682
  action={
@@ -10,7 +10,7 @@ import type { FeedbackStatusChange, FeedbackStatusRow } from "./feedback-status-
10
10
  * (`feedback-page.tsx:990`, feedback #144 rework item 4: *"it is a queue, and triaging it
11
11
  * one tap-into-the-row at a time is the slow way"*), for every app.
12
12
  *
13
- * - `advance` — one step along the chain (`nextFeedbackStatus`: OPEN → IN_PROGRESS →
13
+ * - `advance` — one step along the chain (`nextFeedbackStatus`: OPEN → READY → IN_PROGRESS →
14
14
  * IN_EVALUATION → DONE), labelled with the status it goes to;
15
15
  * - `done` — straight to DONE, for a row that plainly needs no stop on the way;
16
16
  * - `wont_do` — dismiss as WONT_DO.
@@ -81,6 +81,9 @@ import { DEFAULT_ADMIN_ACTION_LABELS } from "../admin/admin-action-confirm";
81
81
  import { DEFAULT_ADMIN_ACTION_LOG_LABELS } from "../admin/admin-action-log";
82
82
  import { DEFAULT_TRANSFER_OWNERSHIP_LABELS } from "../admin/transfer-ownership-dialog";
83
83
  import { DEFAULT_INVITATIONS_LABELS } from "../admin/invitations-panel";
84
+ import { DEFAULT_SETTINGS_LABELS } from "../settings/settings-labels";
85
+ import { DEFAULT_LANDING_LABELS } from "../landing/landing-labels";
86
+ import { DEFAULT_DEMO_LABELS } from "../demo/demo-labels";
84
87
  import { DEFAULT_ACCOUNT_STATE_LABELS } from "../components/account-chips";
85
88
  import { DEFAULT_SHARE_CARD_LABELS } from "../components/share-card";
86
89
  import { DEFAULT_REAUTH_DIALOG_LABELS } from "../components/reauth-dialog";
@@ -208,6 +211,9 @@ export const DEFAULT_UI_KIT_LABELS: UiKitLabels = {
208
211
  adminActionLog: DEFAULT_ADMIN_ACTION_LOG_LABELS,
209
212
  transferOwnership: DEFAULT_TRANSFER_OWNERSHIP_LABELS,
210
213
  invitations: DEFAULT_INVITATIONS_LABELS,
214
+ settings: DEFAULT_SETTINGS_LABELS,
215
+ landing: DEFAULT_LANDING_LABELS,
216
+ demo: DEFAULT_DEMO_LABELS,
211
217
  characterCount: DEFAULT_CHARACTER_COUNT_LABELS,
212
218
  countrySelect: DEFAULT_COUNTRY_SELECT_LABELS,
213
219
  inlineEdit: DEFAULT_INLINE_EDIT_LABELS,
@@ -79,6 +79,7 @@ export function germanLabels(numberLocale: string): UiKitLabels {
79
79
  // written here with "ß" like the rest of this source; `swiss()` makes it "grösser".
80
80
  feedbackStatus: {
81
81
  OPEN: "Offen",
82
+ READY: "Bereit zur Umsetzung",
82
83
  IN_PROGRESS: "In Bearbeitung",
83
84
  IN_EVALUATION: "Zur Prüfung",
84
85
  NEEDS_LIVE_TEST: "Live testen",
@@ -140,6 +141,7 @@ export function germanLabels(numberLocale: string): UiKitLabels {
140
141
  phoneActions: "Feedback-Aktionen",
141
142
  },
142
143
  feedbackDetail: {
144
+ subject: "Betreff",
143
145
  body: "Beschreibung",
144
146
  edit: "Bearbeiten",
145
147
  editDescription: "Beschreibung bearbeiten",
@@ -198,6 +200,8 @@ export function germanLabels(numberLocale: string): UiKitLabels {
198
200
  qrAlt: "QR-Code für die Authenticator-App",
199
201
  secretHint: "Scannen nicht möglich? Stattdessen diesen Schlüssel in der App eingeben:",
200
202
  copySecret: "Schlüssel kopieren",
203
+ // 0.31.0: the card's title; the state ("Aktiv") stands beside it.
204
+ title: "Zwei-Faktor-Authentifizierung",
201
205
  },
202
206
  passkeys: {
203
207
  title: "Passkeys",
@@ -908,6 +912,8 @@ export function germanLabels(numberLocale: string): UiKitLabels {
908
912
  `${n(length)} Buchstaben und Ziffern. Bindestriche und Leerzeichen spielen keine Rolle.`,
909
913
  recoveryCodeInvalid: "Dieser Backup-Code ist ungültig oder wurde schon verwendet.",
910
914
  rateLimited,
915
+ // 0.31.0: the same words as `landing.requestAccess`.
916
+ requestAccess: "Zugang anfragen",
911
917
  },
912
918
  register: {
913
919
  firstName: "Vorname",
@@ -988,6 +994,7 @@ export function germanLabels(numberLocale: string): UiKitLabels {
988
994
  resendIn: (seconds) => `In ${n(seconds)} s erneut senden`,
989
995
  resent: "Bestätigungs-E-Mail gesendet",
990
996
  resendError: "Bestätigungs-E-Mail konnte nicht gesendet werden",
997
+ rateLimited,
991
998
  banner: "Bitte bestätigen Sie Ihre E-Mail-Adresse.",
992
999
  dismiss: "Später",
993
1000
  },
@@ -1363,5 +1370,85 @@ export function germanLabels(numberLocale: string): UiKitLabels {
1363
1370
  ? `Den ungeprüften Text in „${group}“ bestätigen?`
1364
1371
  : `Alle ${n(count)} ungeprüften Texte in „${group}“ bestätigen – auch die nicht angezeigten?`,
1365
1372
  },
1373
+ // 0.31.0 — docs/settings-harmonization.md §4.3 and docs/landing-demo-harmonization.md
1374
+ // §4–§5: the settings shell, the landing page's generic words and the demo. The group
1375
+ // names are the contracts' ("Darstellung", "Konto", "Sicherheit", "Benachrichtigungen",
1376
+ // "Daten"); the help lines name things as `accountSettings`, `sessions` and
1377
+ // `dataExport` do. Formal "Sie".
1378
+ settings: {
1379
+ title: "Einstellungen",
1380
+ search: "Einstellungen durchsuchen",
1381
+ searchPlaceholder: "Einstellungen durchsuchen…",
1382
+ results: "Passende Einstellungen",
1383
+ noMatches: (query) => `Keine Einstellung passt zu „${query}“.`,
1384
+ // As `commandPalette.clear`.
1385
+ clearSearch: "Suche löschen",
1386
+ back: "Zurück zu den Einstellungen",
1387
+ sections: "Einstellungsbereiche",
1388
+ // "Treffer" is the same in singular and plural.
1389
+ matchCount: (count) => `${n(count)} Treffer`,
1390
+ groups: {
1391
+ appearance: {
1392
+ title: "Darstellung",
1393
+ // "Theme", as `topBar.theme`.
1394
+ help: "Sprache, Theme und wie die App auf diesem Gerät aussieht.",
1395
+ },
1396
+ account: {
1397
+ title: "Konto",
1398
+ help: "Ihr Profil und Ihre E-Mail-Adresse.",
1399
+ },
1400
+ security: {
1401
+ title: "Sicherheit",
1402
+ help: "Wie Sie sich anmelden und wo Sie angemeldet sind.",
1403
+ },
1404
+ notifications: {
1405
+ title: "Benachrichtigungen",
1406
+ help: "Worüber Sie informiert werden, wann und auf welchen Geräten.",
1407
+ },
1408
+ data: {
1409
+ title: "Daten",
1410
+ help: "Ihre Daten exportieren oder zusammen mit Ihrem Konto löschen.",
1411
+ },
1412
+ },
1413
+ },
1414
+ landing: {
1415
+ signIn: "Anmelden",
1416
+ requestAccess: "Zugang anfragen",
1417
+ getStarted: "Jetzt starten",
1418
+ tryDemo: "Demo ausprobieren",
1419
+ openApp: "App öffnen",
1420
+ continueDemo: "Demo fortsetzen",
1421
+ beta: "Beta",
1422
+ // Plain text: a mail's subject and body lines, never HTML.
1423
+ accessSubject: (app) => (app ? `Zugang zu ${app}` : "Zugangsanfrage"),
1424
+ accessName: "Name:",
1425
+ // "Unternehmen", as `companySwitcher`.
1426
+ accessCompany: "Unternehmen:",
1427
+ accessUse: "Wofür Sie die App nutzen möchten:",
1428
+ },
1429
+ demo: {
1430
+ starting: "Demo wird gestartet…",
1431
+ rateLimited: (minutes) =>
1432
+ minutes
1433
+ ? `Zu viele Demos aus diesem Netzwerk. Versuchen Sie es in ${n(minutes)} min erneut.`
1434
+ : "Zu viele Demos aus diesem Netzwerk. Versuchen Sie es später erneut.",
1435
+ capacity: "Die Demo ist gerade ausgelastet. Versuchen Sie es später erneut.",
1436
+ unavailable: "Die Demo ist gerade nicht verfügbar.",
1437
+ failed: "Die Demo konnte nicht gestartet werden. Bitte versuchen Sie es erneut.",
1438
+ // As `legal.backHome`.
1439
+ backToStart: "Zurück zur Startseite",
1440
+ hoursLeft: (hours, minutes) => `Demo · noch ${n(hours)} h ${n(minutes)} min`,
1441
+ minutesLeft: (minutes) => `Demo · noch ${n(minutes)} min`,
1442
+ badge: "Demo",
1443
+ readOnly: "Sie sehen Beispieldaten. Änderungen sind nicht möglich.",
1444
+ sandbox: "Was Sie selbst anlegen, wird gelöscht, wenn die Demo endet.",
1445
+ details: "Demo-Details",
1446
+ writeLocked: "In der Demo nicht möglich.",
1447
+ endedTitle: "Die Demo ist beendet",
1448
+ endedReadOnly: "Die Beispieldaten werden regelmäßig zurückgesetzt.",
1449
+ endedSandbox:
1450
+ "Die Beispieldaten werden regelmäßig zurückgesetzt; was Sie in der Demo selbst angelegt haben, wird gelöscht.",
1451
+ restart: "Neue Demo starten",
1452
+ },
1366
1453
  };
1367
1454
  }
@@ -77,6 +77,9 @@ import type { AdminActionLabels } from "../admin/admin-action-confirm";
77
77
  import type { AdminActionLogLabels } from "../admin/admin-action-log";
78
78
  import type { TransferOwnershipLabels } from "../admin/transfer-ownership-dialog";
79
79
  import type { InvitationsLabels } from "../admin/invitations-panel";
80
+ import type { SettingsLabels } from "../settings/settings-labels";
81
+ import type { LandingLabels } from "../landing/landing-labels";
82
+ import type { DemoLabels } from "../demo/demo-labels";
80
83
  import type { AccountStateLabels } from "../components/account-chips";
81
84
  import type { ShareCardLabels } from "../components/share-card";
82
85
  import type { ReauthDialogLabels } from "../components/reauth-dialog";
@@ -383,6 +386,12 @@ export interface UiKitLabels {
383
386
  transferOwnership: TransferOwnershipLabels;
384
387
  /** 0.30.0: the invitations panel (§5). */
385
388
  invitations: InvitationsLabels;
389
+ /** 0.31.0: the settings and admin pages' shell (docs/settings-harmonization.md §4.3). */
390
+ settings: SettingsLabels;
391
+ /** 0.31.0: the public landing page's kit words (docs/landing-demo-harmonization.md §4). */
392
+ landing: LandingLabels;
393
+ /** 0.31.0: the demo's start, banner and end (§5). */
394
+ demo: DemoLabels;
386
395
  /** 0.22.0: the screen-reader words of `Input` / `Textarea`'s `showCount` counter. */
387
396
  characterCount: CharacterCountLabels;
388
397
  /** 0.22.0: `CountrySelect`. Its list's "no results" and counts are `combobox`'s. */