@eifi1/ui-kit 0.44.0 → 0.45.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.
- package/README.md +10 -6
- package/dist/admin/admin-action-log-labels.js +9 -0
- package/dist/admin/admin-action-log-labels.js.map +1 -1
- package/dist/admin/admin-action-log.d.ts +18 -1
- package/dist/admin/admin-action-log.js +25 -16
- package/dist/admin/admin-action-log.js.map +1 -1
- package/dist/admin/admin-area-labels.d.ts +6 -0
- package/dist/admin/admin-area-labels.js +1 -0
- package/dist/admin/admin-area-labels.js.map +1 -1
- package/dist/admin/mail-test-card-labels.d.ts +3 -0
- package/dist/admin/mail-test-card-labels.js +17 -0
- package/dist/admin/mail-test-card-labels.js.map +1 -0
- package/dist/admin/mail-test-card.d.ts +74 -0
- package/dist/admin/mail-test-card.js +80 -0
- package/dist/admin/mail-test-card.js.map +1 -0
- package/dist/components/data-table-mobile.js +17 -2
- package/dist/components/data-table-mobile.js.map +1 -1
- package/dist/components/data-table-shared.d.ts +2 -0
- package/dist/components/data-table-shared.js +8 -0
- package/dist/components/data-table-shared.js.map +1 -1
- package/dist/components/data-table.js +1 -6
- package/dist/components/data-table.js.map +1 -1
- package/dist/eslint/index.js +1 -1
- package/dist/i18n/defaults.js +9 -1
- package/dist/i18n/defaults.js.map +1 -1
- package/dist/i18n/german.js +40 -0
- package/dist/i18n/german.js.map +1 -1
- package/dist/i18n/kit-labels.d.ts +13 -0
- package/dist/i18n/kit-labels.js.map +1 -1
- package/dist/i18n/locales/en.js +57 -1
- package/dist/i18n/locales/en.js.map +3 -3
- package/dist/i18n/locales/es.js +40 -0
- package/dist/i18n/locales/es.js.map +1 -1
- package/dist/i18n/locales/fr.js +40 -0
- package/dist/i18n/locales/fr.js.map +1 -1
- package/dist/i18n/locales/hu.js +40 -0
- package/dist/i18n/locales/hu.js.map +1 -1
- package/dist/i18n/locales/it.js +40 -0
- package/dist/i18n/locales/it.js.map +1 -1
- package/dist/i18n/locales/zh.js +40 -0
- package/dist/i18n/locales/zh.js.map +1 -1
- package/dist/i18n/review.js +13 -1
- package/dist/i18n/review.js.map +1 -1
- package/dist/index.d.ts +2 -0
- package/dist/index.js +5 -0
- package/dist/index.js.map +1 -1
- package/dist/lib/format.d.ts +2 -1
- package/dist/lib/format.js +6 -1
- package/dist/lib/format.js.map +1 -1
- package/dist/offline/pwa-update-prompt-labels.d.ts +2 -0
- package/dist/offline/pwa-update-prompt-labels.js +9 -0
- package/dist/offline/pwa-update-prompt-labels.js.map +1 -0
- package/dist/offline/pwa-update-prompt.d.ts +32 -0
- package/dist/offline/pwa-update-prompt.js +36 -0
- package/dist/offline/pwa-update-prompt.js.map +1 -0
- package/dist/offline/query-persistence.d.ts +100 -0
- package/dist/offline/query-persistence.js +179 -0
- package/dist/offline/query-persistence.js.map +1 -0
- package/dist/offline/shared-computer-labels.d.ts +2 -0
- package/dist/offline/shared-computer-labels.js +9 -0
- package/dist/offline/shared-computer-labels.js.map +1 -0
- package/dist/offline/shared-computer.d.ts +44 -0
- package/dist/offline/shared-computer.js +51 -0
- package/dist/offline/shared-computer.js.map +1 -0
- package/dist/offline/sign-out-pending-dialog-labels.d.ts +2 -0
- package/dist/offline/sign-out-pending-dialog-labels.js +14 -0
- package/dist/offline/sign-out-pending-dialog-labels.js.map +1 -0
- package/dist/offline/sign-out-pending-dialog.d.ts +58 -0
- package/dist/offline/sign-out-pending-dialog.js +66 -0
- package/dist/offline/sign-out-pending-dialog.js.map +1 -0
- package/dist/offline.d.ts +8 -0
- package/dist/offline.js +24 -0
- package/dist/offline.js.map +1 -0
- package/dist/shell/admin-menu.d.ts +24 -0
- package/dist/shell/admin-menu.js +12 -3
- package/dist/shell/admin-menu.js.map +1 -1
- package/dist/shell.d.ts +2 -0
- package/dist/shell.js +2 -0
- package/dist/shell.js.map +1 -1
- package/dist/tour/tour-catalogue.d.ts +7 -1
- package/dist/tour/tour-catalogue.js.map +1 -1
- package/dist/tour/tour.js +4 -2
- package/dist/tour/tour.js.map +1 -1
- package/dist/tour/use-tour-progress.d.ts +99 -1
- package/dist/tour/use-tour-progress.js +228 -12
- package/dist/tour/use-tour-progress.js.map +1 -1
- package/dist/tour.d.ts +2 -0
- package/dist/tour.js +2 -0
- package/dist/tour.js.map +1 -1
- package/package.json +25 -1
- package/src/admin/admin-action-log-labels.ts +9 -0
- package/src/admin/admin-action-log.tsx +41 -5
- package/src/admin/admin-area-labels.ts +7 -0
- package/src/admin/mail-test-card-labels.ts +19 -0
- package/src/admin/mail-test-card.tsx +186 -0
- package/src/components/data-table-mobile.tsx +26 -3
- package/src/components/data-table-shared.tsx +18 -1
- package/src/components/data-table.tsx +1 -15
- package/src/i18n/defaults.ts +8 -0
- package/src/i18n/german.ts +46 -0
- package/src/i18n/kit-labels.tsx +13 -0
- package/src/i18n/locales/en.ts +12 -0
- package/src/i18n/locales/es.ts +46 -0
- package/src/i18n/locales/fr.ts +46 -0
- package/src/i18n/locales/hu.ts +42 -0
- package/src/i18n/locales/it.ts +46 -0
- package/src/i18n/locales/zh.ts +40 -0
- package/src/i18n/review.ts +12 -0
- package/src/index.ts +3 -0
- package/src/lib/format.ts +13 -2
- package/src/offline/pwa-update-prompt-labels.ts +9 -0
- package/src/offline/pwa-update-prompt.tsx +72 -0
- package/src/offline/query-persistence.ts +323 -0
- package/src/offline/shared-computer-labels.ts +8 -0
- package/src/offline/shared-computer.tsx +87 -0
- package/src/offline/sign-out-pending-dialog-labels.ts +21 -0
- package/src/offline/sign-out-pending-dialog.tsx +135 -0
- package/src/offline.ts +26 -0
- package/src/shell/admin-menu.tsx +40 -3
- package/src/shell.ts +4 -0
- package/src/tour/tour-catalogue.tsx +7 -1
- package/src/tour/tour.tsx +5 -2
- package/src/tour/use-tour-progress.ts +378 -12
- package/src/tour.ts +3 -0
|
@@ -72,7 +72,11 @@ export type AdminActionKind =
|
|
|
72
72
|
| "group_rename"
|
|
73
73
|
| "group_delete"
|
|
74
74
|
| "membership_add"
|
|
75
|
-
| "membership_role"
|
|
75
|
+
| "membership_role"
|
|
76
|
+
// 0.45 (admin §9.1.3): a group switched off or on again — kastlan's company PATCH
|
|
77
|
+
// (`is_active`). server-kit 0.15's `AdminAction.GROUP_DEACTIVATE` / `GROUP_REACTIVATE`.
|
|
78
|
+
| "group_deactivate"
|
|
79
|
+
| "group_reactivate";
|
|
76
80
|
|
|
77
81
|
/** Every {@link AdminActionKind}, in the contract's order. */
|
|
78
82
|
export const ADMIN_ACTION_KINDS: readonly AdminActionKind[] = [
|
|
@@ -98,6 +102,8 @@ export const ADMIN_ACTION_KINDS: readonly AdminActionKind[] = [
|
|
|
98
102
|
"group_delete",
|
|
99
103
|
"membership_add",
|
|
100
104
|
"membership_role",
|
|
105
|
+
"group_deactivate",
|
|
106
|
+
"group_reactivate",
|
|
101
107
|
];
|
|
102
108
|
|
|
103
109
|
/** The app's word for its groups (§11.5): Kurvenschmiede's teams, kastlan's companies,
|
|
@@ -105,11 +111,14 @@ export const ADMIN_ACTION_KINDS: readonly AdminActionKind[] = [
|
|
|
105
111
|
* actions are said in. */
|
|
106
112
|
export type AdminGroupNoun = "group" | "team" | "company" | "budget";
|
|
107
113
|
|
|
108
|
-
/** The kinds said in the noun's words: the five of 0.34,
|
|
114
|
+
/** The kinds said in the noun's words: the five of 0.34, `membership_remove`, and 0.45's
|
|
115
|
+
* `group_deactivate` / `group_reactivate`. */
|
|
109
116
|
type AdminGroupActionKind =
|
|
110
117
|
| "group_create"
|
|
111
118
|
| "group_rename"
|
|
112
119
|
| "group_delete"
|
|
120
|
+
| "group_deactivate"
|
|
121
|
+
| "group_reactivate"
|
|
113
122
|
| "membership_add"
|
|
114
123
|
| "membership_role"
|
|
115
124
|
| "membership_remove";
|
|
@@ -118,6 +127,8 @@ const GROUP_ACTION_KINDS: ReadonlySet<string> = new Set<AdminGroupActionKind>([
|
|
|
118
127
|
"group_create",
|
|
119
128
|
"group_rename",
|
|
120
129
|
"group_delete",
|
|
130
|
+
"group_deactivate",
|
|
131
|
+
"group_reactivate",
|
|
121
132
|
"membership_add",
|
|
122
133
|
"membership_role",
|
|
123
134
|
"membership_remove",
|
|
@@ -184,6 +195,13 @@ export interface AdminActionLogEntry {
|
|
|
184
195
|
group?: AdminGroupTarget | null;
|
|
185
196
|
/** The `detail` in the app's words — "Member → Admin", "12 items to Ben Example". */
|
|
186
197
|
detail?: ReactNode;
|
|
198
|
+
/**
|
|
199
|
+
* 0.45 (admin §9.4, Marcel 2026-10-19): a `group_rename` row's `detail.{from, to}` —
|
|
200
|
+
* kastlan's company rename logs both names. With both, the row reads "Company renamed
|
|
201
|
+
* from A to B" (`renamedFromTo`) in place of the target. A rename that logs `{}` (KS's
|
|
202
|
+
* team rename: never a name) leaves it out and reads "Team renamed" and the group.
|
|
203
|
+
*/
|
|
204
|
+
renamed?: { from?: string | null; to?: string | null } | null;
|
|
187
205
|
}
|
|
188
206
|
|
|
189
207
|
/** The `adminActionLog` namespace. */
|
|
@@ -206,6 +224,8 @@ export interface AdminActionLogLabels {
|
|
|
206
224
|
showAll: string;
|
|
207
225
|
/** A target name as a filter button — "Show only the actions on Ada Example". */
|
|
208
226
|
filterBy: (target: string) => string;
|
|
227
|
+
/** 0.45: after "Company renamed" when the row carries both names — "from A to B". */
|
|
228
|
+
renamedFromTo: (from: string, to: string) => string;
|
|
209
229
|
/** What each action is called, past tense, keyed by the vocabulary. An app adds its
|
|
210
230
|
* own here (`labels={{ actions: { plan: "Plan changed" } }}`). A word given here
|
|
211
231
|
* outranks the noun's in `groupActions` — so no catalogue holds the group kinds
|
|
@@ -223,6 +243,11 @@ interface AdminGroupActionLabels {
|
|
|
223
243
|
group_rename: string;
|
|
224
244
|
/** "Team deleted". */
|
|
225
245
|
group_delete: string;
|
|
246
|
+
/** "Company deactivated" (0.45, admin §9.1.3): the group switched off — its people
|
|
247
|
+
* locked out — not a person's `deactivate`. */
|
|
248
|
+
group_deactivate: string;
|
|
249
|
+
/** "Company reactivated" (0.45). */
|
|
250
|
+
group_reactivate: string;
|
|
226
251
|
/** "Added to the team" — said of the person, so it agrees with the account where a
|
|
227
252
|
* language agrees (es "Añadida", fr "Ajouté"). */
|
|
228
253
|
membership_add: string;
|
|
@@ -384,12 +409,22 @@ export function AdminActionLog({
|
|
|
384
409
|
aria-busy={loading || undefined}
|
|
385
410
|
className={cn("divide-y divide-subtle", loading && "opacity-60")}
|
|
386
411
|
>
|
|
387
|
-
{entries.map((entry) =>
|
|
412
|
+
{entries.map((entry) => {
|
|
413
|
+
const from = (entry.renamed?.from ?? "").trim();
|
|
414
|
+
const to = (entry.renamed?.to ?? "").trim();
|
|
415
|
+
const renamed = entry.action === "group_rename" && from !== "" && to !== "";
|
|
416
|
+
return (
|
|
388
417
|
<li key={entry.id} data-action={entry.action} className="space-y-0.5 py-2 first:pt-0 last:pb-0">
|
|
389
418
|
<div className="flex flex-wrap items-baseline justify-between gap-x-3 gap-y-0.5">
|
|
390
419
|
<div className="flex min-w-0 flex-wrap items-baseline gap-x-2 gap-y-0.5 text-sm">
|
|
391
420
|
<span className="font-medium text-primary">{actionText(entry.action)}</span>
|
|
392
|
-
{
|
|
421
|
+
{renamed ? (
|
|
422
|
+
<span data-private data-renamed className="min-w-0 break-words text-primary">
|
|
423
|
+
{labels.renamedFromTo(from, to)}
|
|
424
|
+
</span>
|
|
425
|
+
) : (
|
|
426
|
+
entry.target !== undefined && <span className="min-w-0 break-words">{targetNode(entry)}</span>
|
|
427
|
+
)}
|
|
393
428
|
{entry.group && (
|
|
394
429
|
<span data-group className="min-w-0 break-words text-xs text-muted">
|
|
395
430
|
{groupText(entry.group)}
|
|
@@ -410,7 +445,8 @@ export function AdminActionLog({
|
|
|
410
445
|
)}
|
|
411
446
|
</div>
|
|
412
447
|
</li>
|
|
413
|
-
|
|
448
|
+
);
|
|
449
|
+
})}
|
|
414
450
|
</ol>
|
|
415
451
|
)}
|
|
416
452
|
{paging && paging.total > 0 && (
|
|
@@ -32,6 +32,12 @@ export interface AdminAreaLabels {
|
|
|
32
32
|
/** The row's waiting count, for a reader ("Administration, 3 waiting"); the eye sees
|
|
33
33
|
* the number in the badge. */
|
|
34
34
|
waiting: (count: number) => string;
|
|
35
|
+
/**
|
|
36
|
+
* 0.45 (admin §9.3.4): a button's whole name with its count — "Platform, 2 waiting" —
|
|
37
|
+
* for an `IconButton` whose `badge` is an `AdminWaitingBadge`: the badge is decorative
|
|
38
|
+
* there, so the name carries the count. `label` is the button's own name.
|
|
39
|
+
*/
|
|
40
|
+
waitingLabel: (label: string, count: number) => string;
|
|
35
41
|
/** The 403 page's heading (§2.2 decision 8). */
|
|
36
42
|
forbiddenTitle: string;
|
|
37
43
|
/**
|
|
@@ -59,6 +65,7 @@ export interface AdminAreaLabels {
|
|
|
59
65
|
export const DEFAULT_ADMIN_AREA_LABELS: AdminAreaLabels = {
|
|
60
66
|
title: "Administration",
|
|
61
67
|
waiting: (count) => `${count} waiting`,
|
|
68
|
+
waitingLabel: (label, count) => `${label}, ${count} waiting`,
|
|
62
69
|
forbiddenTitle: "Only for administrators",
|
|
63
70
|
forbiddenBody: (appName) => `This is where administrators run ${appName}. Your account cannot open this page.`,
|
|
64
71
|
forbiddenHome: "Go to the start page",
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
// The English words of ./mail-test-card, in a module with no component code: the English
|
|
2
|
+
// catalogue (src/i18n/defaults.ts) imports them without the component, so `en` loads as one
|
|
3
|
+
// chunk like the other languages (0.34, KS U8). ./mail-test-card re-exports them.
|
|
4
|
+
|
|
5
|
+
import { englishRateLimited } from "../auth/auth-errors";
|
|
6
|
+
import type { MailTestCardLabels } from "./mail-test-card";
|
|
7
|
+
|
|
8
|
+
/** docs/admin-harmonization.md §9.2.4, word for word. */
|
|
9
|
+
export const DEFAULT_MAIL_TEST_CARD_LABELS: MailTestCardLabels = {
|
|
10
|
+
title: "Test mail",
|
|
11
|
+
description: "Sends one mail to your own address, through the transport this installation sends all its mail with.",
|
|
12
|
+
transport: (backend) => `Transport: ${backend}`,
|
|
13
|
+
action: "Send a test mail to me",
|
|
14
|
+
sent: (address) => `Sent to ${address}. Check the inbox.`,
|
|
15
|
+
console: "The console transport only logs mail; nothing was sent.",
|
|
16
|
+
failed: "Could not send. The server log has the provider’s answer.",
|
|
17
|
+
rateLimited: englishRateLimited,
|
|
18
|
+
keywords: "test mail email e-mail transport smtp resend console send deliver",
|
|
19
|
+
};
|
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
import { useState } from "react";
|
|
2
|
+
import type { ReactNode } from "react";
|
|
3
|
+
import { Send } from "lucide-react";
|
|
4
|
+
|
|
5
|
+
import { useKitLabels, type LabelOverride } from "../i18n/kit-labels";
|
|
6
|
+
import { isRateLimited, retryAfterSeconds } from "../auth/auth-errors";
|
|
7
|
+
import { AlertBanner } from "../components/alert-banner";
|
|
8
|
+
import { Button } from "../components/button";
|
|
9
|
+
import { Chip } from "../components/chip";
|
|
10
|
+
import { SettingsSection, type SettingsSectionProps } from "../settings/settings-section";
|
|
11
|
+
import { DEFAULT_MAIL_TEST_CARD_LABELS } from "./mail-test-card-labels";
|
|
12
|
+
|
|
13
|
+
/* ── Labels ──────────────────────────────────────────────────────────────── */
|
|
14
|
+
|
|
15
|
+
/** Every string {@link MailTestCard} renders — the `mailTestCard` namespace of
|
|
16
|
+
* `<UiKitProvider labels>`, overridable per instance through `labels`
|
|
17
|
+
* (docs/admin-harmonization.md §9.2.4). */
|
|
18
|
+
export interface MailTestCardLabels {
|
|
19
|
+
/** The card's title, and the app's catalogue entry's. */
|
|
20
|
+
title: string;
|
|
21
|
+
/** The sentence under it. */
|
|
22
|
+
description: string;
|
|
23
|
+
/** The transport's chip: "Transport: resend". `backend` is the server's `mail_backend`. */
|
|
24
|
+
transport: (backend: string) => string;
|
|
25
|
+
/** The button. */
|
|
26
|
+
action: string;
|
|
27
|
+
/** The answer said "sent" through a real transport. `address` is masked ("a…@example.com"). */
|
|
28
|
+
sent: (address: string) => string;
|
|
29
|
+
/** `mail_backend` is `console`: said whatever `sent` is — a console answers `true`. */
|
|
30
|
+
console: string;
|
|
31
|
+
/** Not sent, or the request failed. The provider's error stays in the server log. */
|
|
32
|
+
failed: string;
|
|
33
|
+
/** HTTP 429 — the app's limiter (server-kit's `MAIL_TEST_LIMIT`, 3 per 10 minutes);
|
|
34
|
+
* `seconds` from `Retry-After` when the response has one. */
|
|
35
|
+
rateLimited: (seconds?: number) => string;
|
|
36
|
+
/** For the app's catalogue entry (`keywords`); the card never reads it. */
|
|
37
|
+
keywords: string;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// Defined in ./mail-test-card-labels, which holds no component code.
|
|
41
|
+
export { DEFAULT_MAIL_TEST_CARD_LABELS };
|
|
42
|
+
|
|
43
|
+
/* ── The card ────────────────────────────────────────────────────────────── */
|
|
44
|
+
|
|
45
|
+
/** The answer of `POST /admin/mail/test` — server-kit 0.15's `MailTestResult`. */
|
|
46
|
+
export interface MailTestAnswer {
|
|
47
|
+
sent: boolean;
|
|
48
|
+
/** The transport that took it: `console`, `resend`, `smtp`, … */
|
|
49
|
+
mail_backend: string;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/** The card's default DOM id: its catalogue entry's `anchor`, the `?focus=` target. */
|
|
53
|
+
export const MAIL_TEST_ANCHOR = "admin-mail-test";
|
|
54
|
+
|
|
55
|
+
export interface MailTestCardProps
|
|
56
|
+
extends Omit<SettingsSectionProps, "anchor" | "title" | "description" | "children"> {
|
|
57
|
+
/** The installation's transport, as the admin page has it (`mail_backend`): the chip
|
|
58
|
+
* before a send. After one, the answer's. */
|
|
59
|
+
mailBackend: string;
|
|
60
|
+
/** The app's `POST /admin/mail/test` (kastlan `/platform/mail/test`), no body: it
|
|
61
|
+
* awaits the app's own send and answers `{sent, mail_backend}`. A rejection is read
|
|
62
|
+
* for a 429 (the limiter); anything else is "could not send". */
|
|
63
|
+
send: () => Promise<MailTestAnswer>;
|
|
64
|
+
/** The signed-in admin's own address — where the mail goes. Shown masked. */
|
|
65
|
+
address: string;
|
|
66
|
+
/** Default {@link MAIL_TEST_ANCHOR}; the app's catalogue entry names the same. */
|
|
67
|
+
anchor?: string;
|
|
68
|
+
/** The app's sentence after the kit's description. */
|
|
69
|
+
description?: ReactNode;
|
|
70
|
+
labels?: LabelOverride<MailTestCardLabels>;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* An address with its local part masked to its first character: "ada@example.com" →
|
|
75
|
+
* "a…@example.com". The domain stays, so the admin sees which mailbox to open.
|
|
76
|
+
*/
|
|
77
|
+
export function maskMailAddress(address: string): string {
|
|
78
|
+
const text = address.trim();
|
|
79
|
+
const at = text.lastIndexOf("@");
|
|
80
|
+
if (at <= 0) return text === "" ? "" : `${Array.from(text)[0]}…`;
|
|
81
|
+
return `${Array.from(text.slice(0, at))[0]}…${text.slice(at)}`;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
type Outcome =
|
|
85
|
+
| { kind: "sent"; backend: string }
|
|
86
|
+
| { kind: "console"; backend: string }
|
|
87
|
+
| { kind: "failed"; backend?: string }
|
|
88
|
+
| { kind: "limited"; seconds?: number };
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* "Send a test mail to me" (docs/admin-harmonization.md §9.2): a `SettingsSection` with a
|
|
92
|
+
* sentence, the transport as a chip, the button and the result line. One mail goes to the
|
|
93
|
+
* signed-in admin's own address, never another, through the app's own send.
|
|
94
|
+
*
|
|
95
|
+
* **The result line reads `mail_backend` first**: with `console` it says "nothing was sent"
|
|
96
|
+
* whatever `sent` is, since every console path answers `true` (§9.2 Today). Otherwise
|
|
97
|
+
* `sent` says "Sent to a…@example.com. Check the inbox." or "Could not send." — the
|
|
98
|
+
* provider's error stays in the server log (§9.2.7). A 429 says the limiter's line. The
|
|
99
|
+
* button is busy while the request runs; the line is a live region.
|
|
100
|
+
*
|
|
101
|
+
* Not a commit: the test writes nothing, so it is not under the write lock (a demo never
|
|
102
|
+
* reaches the admin area, and the server refuses it anyway). The app adds the catalogue
|
|
103
|
+
* entry `{ id, anchor: MAIL_TEST_ANCHOR, title, keywords }` from these labels.
|
|
104
|
+
*/
|
|
105
|
+
export function MailTestCard({
|
|
106
|
+
mailBackend,
|
|
107
|
+
send,
|
|
108
|
+
address,
|
|
109
|
+
anchor = MAIL_TEST_ANCHOR,
|
|
110
|
+
description,
|
|
111
|
+
labels: labelsProp,
|
|
112
|
+
...rest
|
|
113
|
+
}: MailTestCardProps) {
|
|
114
|
+
const labels = useKitLabels("mailTestCard", DEFAULT_MAIL_TEST_CARD_LABELS, labelsProp);
|
|
115
|
+
const [busy, setBusy] = useState(false);
|
|
116
|
+
const [outcome, setOutcome] = useState<Outcome | null>(null);
|
|
117
|
+
|
|
118
|
+
const run = async () => {
|
|
119
|
+
if (busy) return;
|
|
120
|
+
setBusy(true);
|
|
121
|
+
setOutcome(null);
|
|
122
|
+
try {
|
|
123
|
+
const answer = await send();
|
|
124
|
+
const backend = answer.mail_backend;
|
|
125
|
+
if (backend === "console") setOutcome({ kind: "console", backend });
|
|
126
|
+
else setOutcome(answer.sent ? { kind: "sent", backend } : { kind: "failed", backend });
|
|
127
|
+
} catch (error) {
|
|
128
|
+
setOutcome(isRateLimited(error) ? { kind: "limited", seconds: retryAfterSeconds(error) } : { kind: "failed" });
|
|
129
|
+
} finally {
|
|
130
|
+
setBusy(false);
|
|
131
|
+
}
|
|
132
|
+
};
|
|
133
|
+
|
|
134
|
+
const backend = outcome && "backend" in outcome && outcome.backend ? outcome.backend : mailBackend;
|
|
135
|
+
|
|
136
|
+
return (
|
|
137
|
+
<SettingsSection
|
|
138
|
+
{...rest}
|
|
139
|
+
anchor={anchor}
|
|
140
|
+
title={labels.title}
|
|
141
|
+
description={
|
|
142
|
+
description != null ? (
|
|
143
|
+
<>
|
|
144
|
+
{labels.description} {description}
|
|
145
|
+
</>
|
|
146
|
+
) : (
|
|
147
|
+
labels.description
|
|
148
|
+
)
|
|
149
|
+
}
|
|
150
|
+
>
|
|
151
|
+
<div className="space-y-2">
|
|
152
|
+
<div className="flex flex-wrap items-center gap-2">
|
|
153
|
+
<Button variant="secondary" pending={busy} onClick={() => void run()}>
|
|
154
|
+
<Send className="size-4" aria-hidden />
|
|
155
|
+
{labels.action}
|
|
156
|
+
</Button>
|
|
157
|
+
{backend && (
|
|
158
|
+
<Chip size="sm" data-slot="mail-backend">
|
|
159
|
+
{labels.transport(backend)}
|
|
160
|
+
</Chip>
|
|
161
|
+
)}
|
|
162
|
+
</div>
|
|
163
|
+
{outcome?.kind === "sent" && (
|
|
164
|
+
<AlertBanner variant="inline" block tone="success">
|
|
165
|
+
<span data-private>{labels.sent(maskMailAddress(address))}</span>
|
|
166
|
+
</AlertBanner>
|
|
167
|
+
)}
|
|
168
|
+
{outcome?.kind === "console" && (
|
|
169
|
+
<AlertBanner variant="inline" block tone="info">
|
|
170
|
+
{labels.console}
|
|
171
|
+
</AlertBanner>
|
|
172
|
+
)}
|
|
173
|
+
{outcome?.kind === "failed" && (
|
|
174
|
+
<AlertBanner variant="inline" block tone="danger">
|
|
175
|
+
{labels.failed}
|
|
176
|
+
</AlertBanner>
|
|
177
|
+
)}
|
|
178
|
+
{outcome?.kind === "limited" && (
|
|
179
|
+
<AlertBanner variant="inline" block tone="warning">
|
|
180
|
+
{labels.rateLimited(outcome.seconds)}
|
|
181
|
+
</AlertBanner>
|
|
182
|
+
)}
|
|
183
|
+
</div>
|
|
184
|
+
</SettingsSection>
|
|
185
|
+
);
|
|
186
|
+
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { Fragment, useEffect, useId, useMemo, useRef, useState } from "react";
|
|
2
|
-
import type { ReactNode } from "react";
|
|
2
|
+
import type { MouseEvent as ReactMouseEvent, PointerEvent as ReactPointerEvent, ReactNode } from "react";
|
|
3
3
|
import { ArrowDownWideNarrow, ArrowUpNarrowWide, ChevronDown, ChevronRight, Filter } from "lucide-react";
|
|
4
4
|
import { FOCUS_RING } from "./focus-ring";
|
|
5
5
|
import { IconButton } from "./icon-button";
|
|
@@ -15,7 +15,7 @@ import { DialogFrame } from "./dialog-frame";
|
|
|
15
15
|
import { Checkbox } from "./checkbox";
|
|
16
16
|
import { useLongPress } from "../hooks/use-long-press";
|
|
17
17
|
import type { DataTableLabels } from "./data-table-labels";
|
|
18
|
-
import { RowLink, cleanAttrs, columnLabel, fromOutside } from "./data-table-shared";
|
|
18
|
+
import { RowLink, cleanAttrs, columnLabel, fromOutside, fromOwnControl } from "./data-table-shared";
|
|
19
19
|
import type {
|
|
20
20
|
DataTableCellProps,
|
|
21
21
|
DataTableColumn,
|
|
@@ -383,7 +383,25 @@ export function MobileRow<T>({
|
|
|
383
383
|
// Attached whenever the table selects, the hook's `enabled` deciding whether a press
|
|
384
384
|
// arms: its pointerdown also clears a stale swallow latch, which must happen in the
|
|
385
385
|
// mode too, or a long press whose click never came would eat the first toggle.
|
|
386
|
-
|
|
386
|
+
//
|
|
387
|
+
// A press that starts on a control in a cell (or in a portal one opened) is that
|
|
388
|
+
// control's, not a way into the mode (keksdose live #428): it only clears the latch.
|
|
389
|
+
const press = select
|
|
390
|
+
? {
|
|
391
|
+
...longPress.handlers,
|
|
392
|
+
onPointerDown: (e: ReactPointerEvent<HTMLElement>) => {
|
|
393
|
+
if (fromOutside(e.target, e.currentTarget) || fromOwnControl(e.target, e.currentTarget)) {
|
|
394
|
+
longPress.consumeClick();
|
|
395
|
+
return;
|
|
396
|
+
}
|
|
397
|
+
longPress.handlers.onPointerDown(e);
|
|
398
|
+
},
|
|
399
|
+
onContextMenu: (e: ReactMouseEvent<HTMLElement>) => {
|
|
400
|
+
if (fromOutside(e.target, e.currentTarget) || fromOwnControl(e.target, e.currentTarget)) return;
|
|
401
|
+
longPress.handlers.onContextMenu(e);
|
|
402
|
+
},
|
|
403
|
+
}
|
|
404
|
+
: undefined;
|
|
387
405
|
const body = selecting ? (
|
|
388
406
|
// In the mode a card is a toggle — kk's register row: `role="button"` with
|
|
389
407
|
// `aria-pressed`, the box beside it only a picture of that state (the card is the
|
|
@@ -399,6 +417,8 @@ export function MobileRow<T>({
|
|
|
399
417
|
{...press}
|
|
400
418
|
onClick={(e) => {
|
|
401
419
|
if (!select!.selectable || fromOutside(e.target, e.currentTarget)) return;
|
|
420
|
+
// A tap on a control in a cell is that control's (keksdose live #428).
|
|
421
|
+
if (fromOwnControl(e.target, e.currentTarget)) return;
|
|
402
422
|
select!.onToggle(!select!.selected);
|
|
403
423
|
}}
|
|
404
424
|
onKeyDown={(e) => {
|
|
@@ -455,6 +475,9 @@ export function MobileRow<T>({
|
|
|
455
475
|
interactive
|
|
456
476
|
? (e) => {
|
|
457
477
|
if (fromOutside(e.target, e.currentTarget)) return;
|
|
478
|
+
// A tap on a control in a cell (a select, a button) is that
|
|
479
|
+
// control's, as on the desktop `<tr>` (keksdose live #428).
|
|
480
|
+
if (fromOwnControl(e.target, e.currentTarget)) return;
|
|
458
481
|
onRowClick!(row);
|
|
459
482
|
}
|
|
460
483
|
: undefined
|
|
@@ -147,6 +147,21 @@ export function fromOutside(target: EventTarget | null, row: HTMLElement): boole
|
|
|
147
147
|
return !(target instanceof Node) || !row.contains(target);
|
|
148
148
|
}
|
|
149
149
|
|
|
150
|
+
/**
|
|
151
|
+
* What counts as a control of its own inside a clickable row: a click or key press
|
|
152
|
+
* that lands on one of these belongs to it, not to the row's `onRowClick`. The row's
|
|
153
|
+
* own link (`rowHref`, marked `data-row-link`) is the exception — it IS the row.
|
|
154
|
+
*/
|
|
155
|
+
const OWN_CONTROL =
|
|
156
|
+
'a[href],button,input,select,textarea,label,summary,[role="button"],[role="link"],[role="checkbox"],[role="switch"],[role="menuitem"],[role="option"],[contenteditable=""],[contenteditable="true"],[tabindex]:not([tabindex="-1"])';
|
|
157
|
+
|
|
158
|
+
/** Whether an event on `row` started on a control nested inside it. */
|
|
159
|
+
export function fromOwnControl(target: EventTarget | null, row: HTMLElement): boolean {
|
|
160
|
+
if (!(target instanceof Element)) return false;
|
|
161
|
+
const hit = target.closest(OWN_CONTROL);
|
|
162
|
+
return !!hit && hit !== row && row.contains(hit) && !hit.hasAttribute("data-row-link");
|
|
163
|
+
}
|
|
164
|
+
|
|
150
165
|
/** A click the APP owns. Anything else — middle, ⌘/Ctrl, Shift, Alt — belongs to
|
|
151
166
|
* the browser, and the whole feature is not touching it. */
|
|
152
167
|
const isPlainLeftClick = (e: React.MouseEvent) =>
|
|
@@ -215,7 +230,9 @@ export function RowLink({
|
|
|
215
230
|
}
|
|
216
231
|
// Already cancelled above; this phase only decides what opens. A nested
|
|
217
232
|
// control that stopped propagation deliberately never reaches it, which is
|
|
218
|
-
// exactly how the row behaved before it became a link
|
|
233
|
+
// exactly how the row behaved before it became a link — and one that did not
|
|
234
|
+
// (a select in a phone card's cell, keksdose live #428) is still its own.
|
|
235
|
+
if (fromOwnControl(e.target, e.currentTarget)) return;
|
|
219
236
|
onActivate?.();
|
|
220
237
|
}}
|
|
221
238
|
>
|
|
@@ -10,6 +10,7 @@ import {
|
|
|
10
10
|
cleanAttrs,
|
|
11
11
|
columnLabel,
|
|
12
12
|
fromOutside,
|
|
13
|
+
fromOwnControl,
|
|
13
14
|
logicalAlign,
|
|
14
15
|
resolveDataTableLabels,
|
|
15
16
|
splitCellProps,
|
|
@@ -702,21 +703,6 @@ export const DEFAULT_DATA_TABLE_SORT_LABELS: DataTableSortLabels = {
|
|
|
702
703
|
|
|
703
704
|
const flipDir = (d: SortDir): SortDir => (d === "asc" ? "desc" : "asc");
|
|
704
705
|
|
|
705
|
-
/**
|
|
706
|
-
* What counts as a control of its own inside a clickable row: a click or key press
|
|
707
|
-
* that lands on one of these belongs to it, not to the row's `onRowClick`. The row's
|
|
708
|
-
* own link (`rowHref`, marked `data-row-link`) is the exception — it IS the row.
|
|
709
|
-
*/
|
|
710
|
-
const OWN_CONTROL =
|
|
711
|
-
'a[href],button,input,select,textarea,label,summary,[role="button"],[role="link"],[role="checkbox"],[role="switch"],[role="menuitem"],[role="option"],[contenteditable=""],[contenteditable="true"],[tabindex]:not([tabindex="-1"])';
|
|
712
|
-
|
|
713
|
-
/** Whether an event on `row` started on a control nested inside it. */
|
|
714
|
-
function fromOwnControl(target: EventTarget | null, row: HTMLElement): boolean {
|
|
715
|
-
if (!(target instanceof Element)) return false;
|
|
716
|
-
const hit = target.closest(OWN_CONTROL);
|
|
717
|
-
return !!hit && hit !== row && row.contains(hit) && !hit.hasAttribute("data-row-link");
|
|
718
|
-
}
|
|
719
|
-
|
|
720
706
|
/** Arrow/Home/End between the focusable rows of one `<tbody>` — the roving half of
|
|
721
707
|
* the keyboard-activatable rows (see the row's `onKeyDown`). */
|
|
722
708
|
function moveRowFocus(from: HTMLElement, key: string): boolean {
|
package/src/i18n/defaults.ts
CHANGED
|
@@ -100,6 +100,10 @@ import { DEFAULT_ROW_ACTIONS_LABELS } from "../components/row-actions-labels";
|
|
|
100
100
|
import { DEFAULT_APP_SHELL_MORE_LABELS } from "../shell/app-shell-labels";
|
|
101
101
|
import { DEFAULT_ACCOUNT_STATE_LABELS } from "../components/account-chips-labels";
|
|
102
102
|
import { DEFAULT_ADMIN_AREA_LABELS } from "../admin/admin-area-labels";
|
|
103
|
+
import { DEFAULT_MAIL_TEST_CARD_LABELS } from "../admin/mail-test-card-labels";
|
|
104
|
+
import { DEFAULT_SIGN_OUT_PENDING_DIALOG_LABELS } from "../offline/sign-out-pending-dialog-labels";
|
|
105
|
+
import { DEFAULT_PWA_UPDATE_PROMPT_LABELS } from "../offline/pwa-update-prompt-labels";
|
|
106
|
+
import { DEFAULT_SHARED_COMPUTER_LABELS } from "../offline/shared-computer-labels";
|
|
103
107
|
import { DEFAULT_TOP_BAR_PARTS_LABELS } from "../shell/top-bar-parts-labels";
|
|
104
108
|
import { DEFAULT_SHARE_CARD_LABELS } from "../components/share-card-labels";
|
|
105
109
|
import { DEFAULT_REAUTH_DIALOG_LABELS } from "../components/reauth-dialog-labels";
|
|
@@ -262,4 +266,8 @@ export const DEFAULT_UI_KIT_LABELS: UiKitLabels = {
|
|
|
262
266
|
notifications: DEFAULT_NOTIFICATIONS_LABELS,
|
|
263
267
|
adminArea: DEFAULT_ADMIN_AREA_LABELS,
|
|
264
268
|
topBarParts: DEFAULT_TOP_BAR_PARTS_LABELS,
|
|
269
|
+
mailTestCard: DEFAULT_MAIL_TEST_CARD_LABELS,
|
|
270
|
+
signOutPending: DEFAULT_SIGN_OUT_PENDING_DIALOG_LABELS,
|
|
271
|
+
pwaUpdate: DEFAULT_PWA_UPDATE_PROMPT_LABELS,
|
|
272
|
+
sharedComputer: DEFAULT_SHARED_COMPUTER_LABELS,
|
|
265
273
|
};
|
package/src/i18n/german.ts
CHANGED
|
@@ -1409,11 +1409,14 @@ export function germanLabels(numberLocale: string): UiKitLabels {
|
|
|
1409
1409
|
// 0.34 (§11.5): whole phrases per noun. `team` in Kurvenschmiede's own words ("Team
|
|
1410
1410
|
// angelegt", "aufnehmen"), `budget` in keksdose's, `company` with the string
|
|
1411
1411
|
// `actions.membership_remove` held until 0.34.
|
|
1412
|
+
renamedFromTo: (from, to) => `von ${from} in ${to}`,
|
|
1412
1413
|
groupActions: {
|
|
1413
1414
|
group: {
|
|
1414
1415
|
group_create: "Gruppe erstellt",
|
|
1415
1416
|
group_rename: "Gruppe umbenannt",
|
|
1416
1417
|
group_delete: "Gruppe gelöscht",
|
|
1418
|
+
group_deactivate: "Gruppe deaktiviert",
|
|
1419
|
+
group_reactivate: "Gruppe reaktiviert",
|
|
1417
1420
|
membership_add: "Zur Gruppe hinzugefügt",
|
|
1418
1421
|
membership_role: "Rolle in der Gruppe geändert",
|
|
1419
1422
|
membership_remove: "Aus der Gruppe entfernt",
|
|
@@ -1423,6 +1426,8 @@ export function germanLabels(numberLocale: string): UiKitLabels {
|
|
|
1423
1426
|
group_create: "Team angelegt",
|
|
1424
1427
|
group_rename: "Team umbenannt",
|
|
1425
1428
|
group_delete: "Team gelöscht",
|
|
1429
|
+
group_deactivate: "Team deaktiviert",
|
|
1430
|
+
group_reactivate: "Team reaktiviert",
|
|
1426
1431
|
membership_add: "Ins Team aufgenommen",
|
|
1427
1432
|
membership_role: "Rolle im Team geändert",
|
|
1428
1433
|
membership_remove: "Aus dem Team entfernt",
|
|
@@ -1432,6 +1437,8 @@ export function germanLabels(numberLocale: string): UiKitLabels {
|
|
|
1432
1437
|
group_create: "Unternehmen erstellt",
|
|
1433
1438
|
group_rename: "Unternehmen umbenannt",
|
|
1434
1439
|
group_delete: "Unternehmen gelöscht",
|
|
1440
|
+
group_deactivate: "Unternehmen deaktiviert",
|
|
1441
|
+
group_reactivate: "Unternehmen reaktiviert",
|
|
1435
1442
|
membership_add: "Zum Unternehmen hinzugefügt",
|
|
1436
1443
|
membership_role: "Rolle im Unternehmen geändert",
|
|
1437
1444
|
membership_remove: "Aus dem Unternehmen entfernt",
|
|
@@ -1441,6 +1448,8 @@ export function germanLabels(numberLocale: string): UiKitLabels {
|
|
|
1441
1448
|
group_create: "Budget erstellt",
|
|
1442
1449
|
group_rename: "Budget umbenannt",
|
|
1443
1450
|
group_delete: "Budget gelöscht",
|
|
1451
|
+
group_deactivate: "Budget deaktiviert",
|
|
1452
|
+
group_reactivate: "Budget reaktiviert",
|
|
1444
1453
|
membership_add: "Zum Budget hinzugefügt",
|
|
1445
1454
|
membership_role: "Rolle im Budget geändert",
|
|
1446
1455
|
membership_remove: "Aus dem Budget entfernt",
|
|
@@ -1892,9 +1901,21 @@ export function germanLabels(numberLocale: string): UiKitLabels {
|
|
|
1892
1901
|
// 0.42.0 — docs/admin-harmonization.md §3.1: the admin area's shared words. "Verwaltung"
|
|
1893
1902
|
// as settings §5 names the page; "mit Administratorrechten" rather than "für
|
|
1894
1903
|
// Administratoren", which would give the reader a gender (as `transferOwnership`).
|
|
1904
|
+
mailTestCard: {
|
|
1905
|
+
title: "Testmail",
|
|
1906
|
+
description: "Sendet eine Mail an Ihre eigene Adresse, über den Versand, mit dem diese Installation alle Mails verschickt.",
|
|
1907
|
+
transport: (backend) => `Versand: ${backend}`,
|
|
1908
|
+
action: "Testmail an mich senden",
|
|
1909
|
+
sent: (address) => `An ${address} gesendet. Bitte prüfen Sie den Posteingang.`,
|
|
1910
|
+
console: "Der Konsolen-Versand schreibt Mails nur ins Log; es wurde nichts gesendet.",
|
|
1911
|
+
failed: "Senden fehlgeschlagen. Die Antwort des Anbieters steht im Server-Log.",
|
|
1912
|
+
rateLimited,
|
|
1913
|
+
keywords: "testmail test e-mail mail versand transport smtp resend konsole senden zustellen",
|
|
1914
|
+
},
|
|
1895
1915
|
adminArea: {
|
|
1896
1916
|
title: "Verwaltung",
|
|
1897
1917
|
waiting: (count) => `${n(count)} offen`,
|
|
1918
|
+
waitingLabel: (label, count) => `${label}, ${n(count)} offen`,
|
|
1898
1919
|
forbiddenTitle: "Nur mit Administratorrechten",
|
|
1899
1920
|
forbiddenBody: (appName) => `Hier wird ${appName} verwaltet. Ihr Konto kann diese Seite nicht öffnen.`,
|
|
1900
1921
|
forbiddenHome: "Zur Startseite",
|
|
@@ -1966,5 +1987,30 @@ export function germanLabels(numberLocale: string): UiKitLabels {
|
|
|
1966
1987
|
billingOff: "Die Abrechnung ist lokal aus: Tarife ändern nichts",
|
|
1967
1988
|
},
|
|
1968
1989
|
},
|
|
1990
|
+
// 0.45.0 — docs/local-first-harmonization.md §1.6 (`@eifi1/ui-kit/offline`): keksdose's
|
|
1991
|
+
// reviewed words (nav.logout_queued_*, pwa.*) where it has the language.
|
|
1992
|
+
signOutPending: {
|
|
1993
|
+
title: "Einige Änderungen sind noch nicht auf dem Server",
|
|
1994
|
+
lost: (count) =>
|
|
1995
|
+
count === 1
|
|
1996
|
+
? `${n(count)} Änderung wartet noch auf die Synchronisierung. Beim Abmelden wird die Kopie auf diesem Gerät gelöscht – und das ist die einzige.`
|
|
1997
|
+
: `${n(count)} Änderungen warten noch auf die Synchronisierung. Beim Abmelden wird die Kopie auf diesem Gerät gelöscht – und das ist die einzige.`,
|
|
1998
|
+
kept: (count) =>
|
|
1999
|
+
count === 1
|
|
2000
|
+
? `${n(count)} Änderung wartet noch auf die Synchronisierung. Sie bleibt für Sie auf diesem Gerät und wird bei Ihrer nächsten Anmeldung hier synchronisiert.`
|
|
2001
|
+
: `${n(count)} Änderungen warten noch auf die Synchronisierung. Sie bleiben für Sie auf diesem Gerät und werden bei Ihrer nächsten Anmeldung hier synchronisiert.`,
|
|
2002
|
+
syncNow: "Jetzt synchronisieren",
|
|
2003
|
+
signOutAnyway: "Trotzdem abmelden",
|
|
2004
|
+
discardWaiting: "Wartende Änderungen verwerfen",
|
|
2005
|
+
cancel: "Abbrechen",
|
|
2006
|
+
},
|
|
2007
|
+
pwaUpdate: {
|
|
2008
|
+
updateAvailable: "Eine neue Version ist verfügbar.",
|
|
2009
|
+
reload: "Neu laden",
|
|
2010
|
+
},
|
|
2011
|
+
sharedComputer: {
|
|
2012
|
+
label: "Dies ist ein gemeinsam genutzter Computer",
|
|
2013
|
+
hint: "Auf diesem Gerät bleibt keine Kopie Ihrer Daten. Seiten öffnen sich dann nur mit einer Verbindung.",
|
|
2014
|
+
},
|
|
1969
2015
|
};
|
|
1970
2016
|
}
|
package/src/i18n/kit-labels.tsx
CHANGED
|
@@ -120,7 +120,11 @@ import type { SignChipLabels } from "../components/sign-chip";
|
|
|
120
120
|
import type { ColumnMapperLabels } from "../components/column-mapper";
|
|
121
121
|
import type { NotificationsLabels } from "../notifications/notifications-labels";
|
|
122
122
|
import type { AdminAreaLabels } from "../admin/admin-area-labels";
|
|
123
|
+
import type { MailTestCardLabels } from "../admin/mail-test-card";
|
|
123
124
|
import type { TopBarPartsLabels } from "../shell/top-bar-parts-labels";
|
|
125
|
+
import type { SignOutPendingDialogLabels } from "../offline/sign-out-pending-dialog";
|
|
126
|
+
import type { PwaUpdatePromptLabels } from "../offline/pwa-update-prompt";
|
|
127
|
+
import type { SharedComputerLabels } from "../offline/shared-computer";
|
|
124
128
|
|
|
125
129
|
/**
|
|
126
130
|
* EVERY string the kit renders, as one typed tree — and an optional provider that
|
|
@@ -487,6 +491,15 @@ export interface UiKitLabels {
|
|
|
487
491
|
/** 0.42.0: the top bar's shared parts — `TourMenu`, `SyncStatusIndicator`, `DevSwitchers`
|
|
488
492
|
* (docs/top-bar-harmonization.md §3.1). */
|
|
489
493
|
topBarParts: TopBarPartsLabels;
|
|
494
|
+
/** 0.45.0: "Send a test mail to me" (docs/admin-harmonization.md §9.2.4). */
|
|
495
|
+
mailTestCard: MailTestCardLabels;
|
|
496
|
+
/** 0.45.0: the sign-out question while changes wait to sync (`@eifi1/ui-kit/offline`,
|
|
497
|
+
* docs/local-first-harmonization.md §1.6). */
|
|
498
|
+
signOutPending: SignOutPendingDialogLabels;
|
|
499
|
+
/** 0.45.0: the service-worker update toast (local-first §1.5 D8). */
|
|
500
|
+
pwaUpdate: PwaUpdatePromptLabels;
|
|
501
|
+
/** 0.45.0: "This is a shared computer" on the sign-in form (local-first §1.5 D6). */
|
|
502
|
+
sharedComputer: SharedComputerLabels;
|
|
490
503
|
}
|
|
491
504
|
|
|
492
505
|
/**
|
package/src/i18n/locales/en.ts
CHANGED
|
@@ -220,6 +220,7 @@ export function uiKitLabelsEn(numberLocale = "en-US"): UiKitLabels {
|
|
|
220
220
|
adminArea: {
|
|
221
221
|
...d.adminArea,
|
|
222
222
|
waiting: (count) => `${n(count)} waiting`,
|
|
223
|
+
waitingLabel: (label, count) => `${label}, ${n(count)} waiting`,
|
|
223
224
|
tiles: { ...d.adminArea.tiles, usersNew: (count) => `${n(count)} new in 7 days` },
|
|
224
225
|
},
|
|
225
226
|
// 0.43.0: the tours page's counts (docs/tours-harmonization.md §3).
|
|
@@ -266,5 +267,16 @@ export function uiKitLabelsEn(numberLocale = "en-US"): UiKitLabels {
|
|
|
266
267
|
waitingChanges: (count) =>
|
|
267
268
|
count === 1 ? "1 change waiting for a plan" : `${n(count)} changes waiting for a plan`,
|
|
268
269
|
},
|
|
270
|
+
signOutPending: {
|
|
271
|
+
...d.signOutPending,
|
|
272
|
+
lost: (count) =>
|
|
273
|
+
count === 1
|
|
274
|
+
? d.signOutPending.lost(1)
|
|
275
|
+
: `${n(count)} changes are still waiting to sync. Logging out clears this device’s copy, and it is the only copy.`,
|
|
276
|
+
kept: (count) =>
|
|
277
|
+
count === 1
|
|
278
|
+
? d.signOutPending.kept(1)
|
|
279
|
+
: `${n(count)} changes are still waiting to sync. They stay on this device for you and sync the next time you log in here.`,
|
|
280
|
+
},
|
|
269
281
|
};
|
|
270
282
|
}
|