@eifi1/ui-kit 0.28.0 → 0.29.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 +22 -4
- package/dist/auth/accept-invitation.d.ts +90 -0
- package/dist/auth/accept-invitation.js +117 -0
- package/dist/auth/accept-invitation.js.map +1 -0
- package/dist/auth/auth-errors.d.ts +60 -0
- package/dist/auth/auth-errors.js +32 -0
- package/dist/auth/auth-errors.js.map +1 -0
- package/dist/auth/complete-name-dialog.d.ts +71 -0
- package/dist/auth/complete-name-dialog.js +168 -0
- package/dist/auth/complete-name-dialog.js.map +1 -0
- package/dist/auth/email-tag.d.ts +60 -0
- package/dist/auth/email-tag.js +23 -0
- package/dist/auth/email-tag.js.map +1 -0
- package/dist/auth/forgot-password-form.d.ts +79 -0
- package/dist/auth/forgot-password-form.js +120 -0
- package/dist/auth/forgot-password-form.js.map +1 -0
- package/dist/auth/form-rules.d.ts +27 -0
- package/dist/auth/form-rules.js +23 -0
- package/dist/auth/form-rules.js.map +1 -0
- package/dist/auth/new-password-fields.d.ts +38 -0
- package/dist/auth/new-password-fields.js +54 -0
- package/dist/auth/new-password-fields.js.map +1 -0
- package/dist/auth/not-found-page.d.ts +57 -0
- package/dist/auth/not-found-page.js +40 -0
- package/dist/auth/not-found-page.js.map +1 -0
- package/dist/auth/register-form.d.ts +86 -0
- package/dist/auth/register-form.js +268 -0
- package/dist/auth/register-form.js.map +1 -0
- package/dist/auth/reset-password-form.d.ts +124 -0
- package/dist/auth/reset-password-form.js +164 -0
- package/dist/auth/reset-password-form.js.map +1 -0
- package/dist/auth/sign-in-form.d.ts +243 -0
- package/dist/auth/sign-in-form.js +345 -0
- package/dist/auth/sign-in-form.js.map +1 -0
- package/dist/auth/status-parts.d.ts +78 -0
- package/dist/auth/status-parts.js +83 -0
- package/dist/auth/status-parts.js.map +1 -0
- package/dist/auth/verify-email.d.ts +121 -0
- package/dist/auth/verify-email.js +207 -0
- package/dist/auth/verify-email.js.map +1 -0
- package/dist/chart.d.ts +17 -2
- package/dist/components/amount-input.d.ts +18 -2
- package/dist/components/button-group.d.ts +18 -2
- package/dist/components/calculator.d.ts +18 -2
- package/dist/components/confirm-dialog.d.ts +18 -2
- package/dist/components/copy-button.d.ts +18 -2
- package/dist/components/danger-confirm.d.ts +18 -2
- package/dist/components/facing-pair.d.ts +18 -2
- package/dist/components/file-button.d.ts +18 -2
- package/dist/components/file-dropzone.d.ts +18 -2
- package/dist/components/form-actions.d.ts +18 -2
- package/dist/components/iban-input.d.ts +18 -2
- package/dist/components/language-select.d.ts +18 -9
- package/dist/components/legal-page.d.ts +26 -4
- package/dist/components/legal-page.js +24 -5
- package/dist/components/legal-page.js.map +1 -1
- package/dist/components/legal.d.ts +18 -2
- package/dist/components/legal.js +49 -5
- package/dist/components/legal.js.map +1 -1
- package/dist/components/money-field.d.ts +18 -2
- package/dist/components/number-field.d.ts +18 -2
- package/dist/components/number-input.d.ts +18 -2
- package/dist/components/numpad-sheet.d.ts +18 -2
- package/dist/components/phone-input.d.ts +18 -2
- package/dist/components/series-chart.d.ts +18 -2
- package/dist/components/settings-fields.d.ts +15 -2
- package/dist/components/share-card.d.ts +18 -2
- package/dist/components/text-link.d.ts +18 -2
- package/dist/components/time-input.d.ts +18 -2
- package/dist/components/ui.d.ts +17 -1
- package/dist/components/user-avatar.d.ts +16 -2
- package/dist/components/user-avatar.js +7 -3
- package/dist/components/user-avatar.js.map +1 -1
- package/dist/feedback/feedback-attachment.d.ts +18 -2
- package/dist/feedback/feedback-context.d.ts +18 -2
- package/dist/feedback/feedback-crash.d.ts +18 -2
- package/dist/feedback/feedback-dialog.d.ts +18 -2
- package/dist/feedback/feedback-inbox.d.ts +18 -2
- package/dist/feedback/feedback-labels.d.ts +18 -2
- package/dist/feedback/feedback-menu.d.ts +18 -2
- package/dist/feedback/feedback-record.d.ts +18 -2
- package/dist/feedback/feedback-row-detail.d.ts +18 -2
- package/dist/feedback/feedback-status-undo.d.ts +18 -2
- package/dist/feedback/feedback-submit.d.ts +18 -2
- package/dist/feedback/feedback-swipe.d.ts +18 -2
- package/dist/feedback/feedback-table.d.ts +18 -2
- package/dist/feedback/feedback-thread.d.ts +18 -2
- package/dist/feedback.d.ts +18 -2
- package/dist/hooks/use-file-drop.d.ts +18 -2
- package/dist/i18n/defaults.d.ts +18 -2
- package/dist/i18n/defaults.js +18 -0
- package/dist/i18n/defaults.js.map +1 -1
- package/dist/i18n/german.d.ts +18 -2
- package/dist/i18n/german.js +145 -0
- package/dist/i18n/german.js.map +1 -1
- package/dist/i18n/kit-labels.d.ts +18 -2
- package/dist/i18n/kit-labels.js.map +1 -1
- package/dist/i18n/languages.d.ts +13 -201
- package/dist/i18n/locales/de-CH.d.ts +18 -2
- package/dist/i18n/locales/en.d.ts +18 -2
- package/dist/i18n/locales/es.d.ts +18 -2
- package/dist/i18n/locales/es.js +147 -3
- package/dist/i18n/locales/es.js.map +1 -1
- package/dist/i18n/locales/fr.d.ts +18 -2
- package/dist/i18n/locales/fr.js +147 -0
- package/dist/i18n/locales/fr.js.map +1 -1
- package/dist/i18n/locales/hu.d.ts +18 -2
- package/dist/i18n/locales/hu.js +149 -3
- package/dist/i18n/locales/hu.js.map +1 -1
- package/dist/i18n/locales/it.d.ts +18 -2
- package/dist/i18n/locales/it.js +146 -1
- package/dist/i18n/locales/it.js.map +1 -1
- package/dist/i18n/locales/zh.d.ts +18 -2
- package/dist/i18n/locales/zh.js +146 -1
- package/dist/i18n/locales/zh.js.map +1 -1
- package/dist/i18n/review.d.ts +18 -2
- package/dist/i18n/review.js +17 -7
- package/dist/i18n/review.js.map +1 -1
- package/dist/index.d.ts +13 -2
- package/dist/index.js +49 -1
- package/dist/index.js.map +1 -1
- package/dist/lib/person-name.d.ts +69 -0
- package/dist/lib/person-name.js +65 -0
- package/dist/lib/person-name.js.map +1 -0
- package/dist/{kit-labels-SBN0biPl.d.ts → register-form-V1OeNlMr.d.ts} +423 -6
- package/dist/rhf/fields.d.ts +18 -2
- package/dist/rhf/form.d.ts +18 -2
- package/dist/rhf.d.ts +18 -2
- package/dist/shell/app-shell.d.ts +18 -2
- package/dist/shell/company-switcher.d.ts +59 -0
- package/dist/shell/company-switcher.js +85 -0
- package/dist/shell/company-switcher.js.map +1 -0
- package/dist/shell/top-bar-brand.d.ts +18 -2
- package/dist/shell/topbar-action-menu.d.ts +1 -0
- package/dist/shell.d.ts +12 -2
- package/dist/wizard/stepper-nav.d.ts +18 -2
- package/dist/wizard.d.ts +18 -2
- package/package.json +1 -1
- package/src/auth/accept-invitation.tsx +227 -0
- package/src/auth/auth-errors.ts +105 -0
- package/src/auth/complete-name-dialog.tsx +253 -0
- package/src/auth/email-tag.ts +75 -0
- package/src/auth/forgot-password-form.tsx +213 -0
- package/src/auth/form-rules.ts +40 -0
- package/src/auth/new-password-fields.tsx +85 -0
- package/src/auth/not-found-page.tsx +98 -0
- package/src/auth/register-form.tsx +459 -0
- package/src/auth/reset-password-form.tsx +337 -0
- package/src/auth/sign-in-form.tsx +687 -0
- package/src/auth/status-parts.tsx +138 -0
- package/src/auth/verify-email.tsx +358 -0
- package/src/components/legal-page.tsx +35 -0
- package/src/components/legal.tsx +93 -7
- package/src/components/user-avatar.tsx +22 -4
- package/src/i18n/defaults.ts +18 -0
- package/src/i18n/german.ts +153 -0
- package/src/i18n/kit-labels.tsx +27 -0
- package/src/i18n/locales/es.ts +156 -3
- package/src/i18n/locales/fr.ts +157 -0
- package/src/i18n/locales/hu.ts +159 -3
- package/src/i18n/locales/it.ts +154 -1
- package/src/i18n/locales/zh.ts +149 -1
- package/src/i18n/review.ts +15 -6
- package/src/index.ts +63 -1
- package/src/lib/person-name.ts +157 -0
- package/src/shell/company-switcher.tsx +140 -0
package/src/components/legal.tsx
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { createContext, useContext } from "react";
|
|
2
|
-
import type { ReactNode } from "react";
|
|
2
|
+
import type { MouseEvent, ReactNode } from "react";
|
|
3
3
|
|
|
4
4
|
import { useKitLabels } from "../i18n/kit-labels";
|
|
5
5
|
import type { LabelOverride } from "../i18n/kit-labels";
|
|
@@ -66,6 +66,14 @@ export interface LegalOperator {
|
|
|
66
66
|
country: string;
|
|
67
67
|
/** The contact address the imprint and the privacy policy print. */
|
|
68
68
|
email: string;
|
|
69
|
+
/**
|
|
70
|
+
* The language whose version of the legal texts is BINDING ("de-CH"), a BCP 47 code
|
|
71
|
+
* (0.28.1, Marcel 2026-10-05). The lawyer reviews that one; the others are translations
|
|
72
|
+
* for convenience. With it, {@link LegalPage} shows a note on every page read in
|
|
73
|
+
* another language, and the terms' `language` section says which version prevails.
|
|
74
|
+
* Only the language subtag counts: "de-CH" and "de" are the same language here.
|
|
75
|
+
*/
|
|
76
|
+
bindingLanguage?: string;
|
|
69
77
|
}
|
|
70
78
|
|
|
71
79
|
/**
|
|
@@ -78,10 +86,42 @@ export interface LegalOperator {
|
|
|
78
86
|
*/
|
|
79
87
|
export type LegalOperatorText = Readonly<Record<keyof LegalOperator, string>>;
|
|
80
88
|
|
|
89
|
+
/** The language subtag of a BCP 47 code, lower-cased: "de-CH" → "de". */
|
|
90
|
+
export function baseLanguage(code: string): string {
|
|
91
|
+
return code.split(/[-_]/)[0]!.toLowerCase();
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
const languageNamers = new Map<string, Intl.DisplayNames | null>();
|
|
95
|
+
|
|
96
|
+
/** The name of a language in `locale` — "Deutsch", "German", "allemand", "德语" — from its
|
|
97
|
+
* language subtag, so "de-CH" is plain "German", not "Swiss High German". The code itself
|
|
98
|
+
* when the runtime cannot name it. */
|
|
99
|
+
function languageName(code: string, locale: string): string {
|
|
100
|
+
let namer = languageNamers.get(locale);
|
|
101
|
+
if (namer === undefined) {
|
|
102
|
+
try {
|
|
103
|
+
namer = new Intl.DisplayNames([locale], { type: "language" });
|
|
104
|
+
} catch {
|
|
105
|
+
namer = null;
|
|
106
|
+
}
|
|
107
|
+
languageNamers.set(locale, namer);
|
|
108
|
+
}
|
|
109
|
+
const base = baseLanguage(code);
|
|
110
|
+
try {
|
|
111
|
+
return namer?.of(base) ?? base;
|
|
112
|
+
} catch {
|
|
113
|
+
return base;
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
81
117
|
/** @internal The operator as the texts read it: its country named in `locale` (English
|
|
82
118
|
* without one — `countryName`'s rule, so an app that set no locale reads English). */
|
|
83
119
|
export function legalOperatorText(operator: LegalOperator, locale: string = "en"): LegalOperatorText {
|
|
84
|
-
return {
|
|
120
|
+
return {
|
|
121
|
+
...operator,
|
|
122
|
+
country: countryName(operator.country, locale),
|
|
123
|
+
bindingLanguage: operator.bindingLanguage ? languageName(operator.bindingLanguage, locale) : "",
|
|
124
|
+
};
|
|
85
125
|
}
|
|
86
126
|
|
|
87
127
|
// ── Labels ────────────────────────────────────────────────────────────────────
|
|
@@ -142,6 +182,16 @@ export interface LegalLabels {
|
|
|
142
182
|
beta: string;
|
|
143
183
|
privacy: string;
|
|
144
184
|
};
|
|
185
|
+
/**
|
|
186
|
+
* 0.28.1: on a page read in another language than the operator's `bindingLanguage` —
|
|
187
|
+
* `note` under the title, `show` on the button that switches to the binding version
|
|
188
|
+
* (when the app passes `onShowBindingLanguage`). `o.bindingLanguage` is the binding
|
|
189
|
+
* language's NAME in the reader's language ("German", "allemand").
|
|
190
|
+
*/
|
|
191
|
+
translation: {
|
|
192
|
+
note: (o: LegalOperatorText) => string;
|
|
193
|
+
show: (o: LegalOperatorText) => string;
|
|
194
|
+
};
|
|
145
195
|
/** The kit's sections (§4.3), by page and section key. */
|
|
146
196
|
sections: {
|
|
147
197
|
impressum: {
|
|
@@ -164,6 +214,8 @@ export interface LegalLabels {
|
|
|
164
214
|
warranty: LegalTextSectionLabels;
|
|
165
215
|
liability: LegalTextSectionLabels;
|
|
166
216
|
changes: LegalTextSectionLabels;
|
|
217
|
+
/** 0.28.1: which language's version prevails — `o.bindingLanguage`, named. */
|
|
218
|
+
language: LegalOperatorSectionLabels;
|
|
167
219
|
/** The place of jurisdiction is the operator's `{city} ({region}), {country}`. */
|
|
168
220
|
law: LegalOperatorSectionLabels;
|
|
169
221
|
};
|
|
@@ -189,6 +241,11 @@ export const DEFAULT_LEGAL_LABELS: LegalLabels = {
|
|
|
189
241
|
privacy:
|
|
190
242
|
"Closed beta. The legal wording below has not been reviewed by a lawyer yet and will be before a public launch. The technical descriptions — what is stored, where, and who can read it — describe what the software actually does today, and are meant to be checked against it.",
|
|
191
243
|
},
|
|
244
|
+
translation: {
|
|
245
|
+
note: (o) =>
|
|
246
|
+
`This is a translation for your convenience. The ${o.bindingLanguage} version is binding.`,
|
|
247
|
+
show: (o) => `Show the ${o.bindingLanguage} version`,
|
|
248
|
+
},
|
|
192
249
|
sections: {
|
|
193
250
|
impressum: {
|
|
194
251
|
operator: {
|
|
@@ -243,6 +300,11 @@ export const DEFAULT_LEGAL_LABELS: LegalLabels = {
|
|
|
243
300
|
title: "Changes to these terms",
|
|
244
301
|
body: "These terms may be updated as the service evolves. Material changes will be announced by email or in the app; continued use after a change means you accept it.",
|
|
245
302
|
},
|
|
303
|
+
language: {
|
|
304
|
+
title: "Language",
|
|
305
|
+
body: (o) =>
|
|
306
|
+
`These terms are written in ${o.bindingLanguage}. Translations into other languages are provided for convenience only; where a translation differs, the ${o.bindingLanguage} version prevails.`,
|
|
307
|
+
},
|
|
246
308
|
law: {
|
|
247
309
|
title: "Governing law",
|
|
248
310
|
body: (o) =>
|
|
@@ -375,6 +437,9 @@ export interface LegalLinksProps {
|
|
|
375
437
|
* links, so in a plain container — Kurvenschmiede's account-menu `<li>` — they ran
|
|
376
438
|
* together as "ImpressumDatenschutzerklärungNutz…" and overflowed). Default true. */
|
|
377
439
|
nav?: boolean;
|
|
440
|
+
/** Called on a click on any of the links, before it navigates — an account menu closes
|
|
441
|
+
* itself here (0.28.1, keksdose: it had to catch the click on a wrapping `<div>`). */
|
|
442
|
+
onLinkClick?: (event: MouseEvent<HTMLAnchorElement>) => void;
|
|
378
443
|
labels?: LabelOverride<LegalLabels>;
|
|
379
444
|
className?: string;
|
|
380
445
|
}
|
|
@@ -396,12 +461,19 @@ function defaultLinks(labels: LegalLabels): LegalLink[] {
|
|
|
396
461
|
* already hears three links, and "middle dot" between them is noise. They are flex items
|
|
397
462
|
* of the row like the links, so the row's gap spaces them evenly.
|
|
398
463
|
*/
|
|
399
|
-
export function LegalLinks({
|
|
464
|
+
export function LegalLinks({
|
|
465
|
+
links,
|
|
466
|
+
currentHref,
|
|
467
|
+
nav = true,
|
|
468
|
+
onLinkClick,
|
|
469
|
+
labels: labelsProp,
|
|
470
|
+
className,
|
|
471
|
+
}: LegalLinksProps) {
|
|
400
472
|
const labels = useLegalLabels(labelsProp);
|
|
401
473
|
const shown = links ?? defaultLinks(labels);
|
|
402
474
|
const items = shown.flatMap((link, i) => [
|
|
403
475
|
...(i > 0 ? [<span key={`sep-${link.href}`} aria-hidden="true">·</span>] : []),
|
|
404
|
-
<TextLink key={link.href} href={link.href} tone="muted" current={link.href === currentHref}>
|
|
476
|
+
<TextLink key={link.href} href={link.href} tone="muted" current={link.href === currentHref} onClick={onLinkClick}>
|
|
405
477
|
{link.label}
|
|
406
478
|
</TextLink>,
|
|
407
479
|
]);
|
|
@@ -425,6 +497,14 @@ export interface LegalFooterProps {
|
|
|
425
497
|
/** Marks that link `aria-current="page"`. {@link LegalPage}'s default footer passes
|
|
426
498
|
* its own page. */
|
|
427
499
|
currentHref?: string;
|
|
500
|
+
/**
|
|
501
|
+
* Default true: the footer is a `<footer>`. `false` draws a `<div>` — for a host that is
|
|
502
|
+
* a footer already: `AuthLayout`'s `footer` slot is one, and HTML forbids a footer inside
|
|
503
|
+
* a footer (0.28.1, kastlan). On an `AuthLayout` page the usual choice is simpler still:
|
|
504
|
+
* `footer={<LegalLinks nav={false} />}` with `footerLabel` set to the kit's `navLabel`
|
|
505
|
+
* ({@link useLegalLabels}).
|
|
506
|
+
*/
|
|
507
|
+
landmark?: boolean;
|
|
428
508
|
className?: string;
|
|
429
509
|
labels?: LabelOverride<LegalLabels>;
|
|
430
510
|
}
|
|
@@ -440,9 +520,10 @@ export interface LegalFooterProps {
|
|
|
440
520
|
* only. Its palette classes (`border-slate-200 bg-white dark:…`) became the surface and
|
|
441
521
|
* border tokens, so it follows the theme like the rest of the kit.
|
|
442
522
|
*/
|
|
443
|
-
export function LegalFooter({ children, links, currentHref, className, labels }: LegalFooterProps) {
|
|
523
|
+
export function LegalFooter({ children, links, currentHref, landmark = true, className, labels }: LegalFooterProps) {
|
|
524
|
+
const Root = landmark ? "footer" : "div";
|
|
444
525
|
return (
|
|
445
|
-
<
|
|
526
|
+
<Root className={cn("border-t border-[var(--border)] bg-[var(--bg-surface)]", className)}>
|
|
446
527
|
<div className="mx-auto max-w-5xl px-4 py-8 text-sm text-[var(--text-muted)]">
|
|
447
528
|
<LegalLinks
|
|
448
529
|
links={links}
|
|
@@ -452,7 +533,7 @@ export function LegalFooter({ children, links, currentHref, className, labels }:
|
|
|
452
533
|
/>
|
|
453
534
|
{children !== undefined && children !== null && <p className="mt-4 text-center">{children}</p>}
|
|
454
535
|
</div>
|
|
455
|
-
</
|
|
536
|
+
</Root>
|
|
456
537
|
);
|
|
457
538
|
}
|
|
458
539
|
|
|
@@ -526,6 +607,9 @@ export interface LegalLayoutProps {
|
|
|
526
607
|
* than the others (its technical sections describe running code).
|
|
527
608
|
*/
|
|
528
609
|
notice?: ReactNode;
|
|
610
|
+
/** 0.28.1: under the title, before the notice — {@link LegalPage}'s note that this
|
|
611
|
+
* page is a translation and which language is binding. */
|
|
612
|
+
translationNote?: ReactNode;
|
|
529
613
|
/** The sections — {@link LegalSection}s. */
|
|
530
614
|
children: ReactNode;
|
|
531
615
|
/** The three pages, linked at the end of the column with the current one marked.
|
|
@@ -543,6 +627,7 @@ export function LegalLayout({
|
|
|
543
627
|
title,
|
|
544
628
|
back,
|
|
545
629
|
notice,
|
|
630
|
+
translationNote,
|
|
546
631
|
children,
|
|
547
632
|
links,
|
|
548
633
|
currentHref,
|
|
@@ -554,6 +639,7 @@ export function LegalLayout({
|
|
|
554
639
|
<article className={cn("mx-auto w-full max-w-3xl px-4 py-12", className)}>
|
|
555
640
|
{back !== undefined && <div className="mb-4 text-sm">{back}</div>}
|
|
556
641
|
<Heading className="text-3xl font-bold tracking-tight break-words text-[var(--text-primary)]">{title}</Heading>
|
|
642
|
+
{translationNote !== undefined && translationNote !== null && <div className="mt-3">{translationNote}</div>}
|
|
557
643
|
{notice !== undefined && notice !== null && (
|
|
558
644
|
<AlertBanner tone="warning" role="note" className="mt-4">
|
|
559
645
|
{notice}
|
|
@@ -1,5 +1,8 @@
|
|
|
1
1
|
import type { ComponentPropsWithoutRef, ReactNode } from "react";
|
|
2
2
|
import { cn } from "../lib/cn";
|
|
3
|
+
import { personInitials } from "../lib/person-name";
|
|
4
|
+
import type { PersonName } from "../lib/person-name";
|
|
5
|
+
import { useKitLocale } from "../i18n/kit-labels";
|
|
3
6
|
import { StatusDot } from "./status-dot";
|
|
4
7
|
import type { StatusDotSize, StatusDotTone } from "./status-dot";
|
|
5
8
|
|
|
@@ -43,10 +46,23 @@ export interface UserAvatarBadge {
|
|
|
43
46
|
}
|
|
44
47
|
|
|
45
48
|
/** A `<span>`'s props, minus `children`: the content is the initials this computes
|
|
46
|
-
* from `name`/`email`, so there is nothing for a caller to put inside. */
|
|
49
|
+
* from `person`/`name`/`email`, so there is nothing for a caller to put inside. */
|
|
47
50
|
export interface UserAvatarProps extends Omit<ComponentPropsWithoutRef<"span">, "children"> {
|
|
48
51
|
name?: string | null;
|
|
49
52
|
email?: string | null;
|
|
53
|
+
/**
|
|
54
|
+
* The person's first and last name (0.29.0, docs/auth-harmonization.md §3.2) —
|
|
55
|
+
* preferred over `name` whenever either part is filled in. The initials follow the
|
|
56
|
+
* READER's language, the provider's `locale`: "AE" for Ada Example, "EA" in Hungarian
|
|
57
|
+
* (family name first), the family name's first character alone in Chinese. See
|
|
58
|
+
* {@link personInitials}.
|
|
59
|
+
*
|
|
60
|
+
* `name` stays the fallback for an API that still sends only `display_name`, and
|
|
61
|
+
* `email` after it: a person whose two parts are both blank falls through to them.
|
|
62
|
+
* A migrated user (the whole old display name in `first`, no `last`, §3.3) keeps the
|
|
63
|
+
* initials they had.
|
|
64
|
+
*/
|
|
65
|
+
person?: PersonName | null;
|
|
50
66
|
size?: keyof typeof AVATAR_SIZES;
|
|
51
67
|
/**
|
|
52
68
|
* A status dot in the top-end corner (the top-right in LTR, top-left in RTL), ringed
|
|
@@ -64,7 +80,9 @@ export interface UserAvatarProps extends Omit<ComponentPropsWithoutRef<"span">,
|
|
|
64
80
|
* A round initials avatar — the common user chip shared across apps (feedback
|
|
65
81
|
* #333). Purely presentational; wrap it in a button for the account menu trigger.
|
|
66
82
|
*/
|
|
67
|
-
export function UserAvatar({ name, email, size = "md", badge, className, ...rest }: UserAvatarProps) {
|
|
83
|
+
export function UserAvatar({ name, email, person, size = "md", badge, className, ...rest }: UserAvatarProps) {
|
|
84
|
+
const locale = useKitLocale();
|
|
85
|
+
const initials = (person ? personInitials(person, locale) : "") || avatarInitials(name, email);
|
|
68
86
|
if (badge) {
|
|
69
87
|
// A wrapper, because the chip itself is `aria-hidden` and the badge label must not
|
|
70
88
|
// be. `className` stays on the chip (a size override still sizes the circle); the
|
|
@@ -80,7 +98,7 @@ export function UserAvatar({ name, email, size = "md", badge, className, ...rest
|
|
|
80
98
|
className,
|
|
81
99
|
)}
|
|
82
100
|
>
|
|
83
|
-
{
|
|
101
|
+
{initials}
|
|
84
102
|
</span>
|
|
85
103
|
<StatusDot
|
|
86
104
|
ring
|
|
@@ -109,7 +127,7 @@ export function UserAvatar({ name, email, size = "md", badge, className, ...rest
|
|
|
109
127
|
className,
|
|
110
128
|
)}
|
|
111
129
|
>
|
|
112
|
-
{
|
|
130
|
+
{initials}
|
|
113
131
|
</span>
|
|
114
132
|
);
|
|
115
133
|
}
|
package/src/i18n/defaults.ts
CHANGED
|
@@ -61,6 +61,15 @@ import { DEFAULT_IMAGE_GRID_LABELS } from "../components/image-grid";
|
|
|
61
61
|
import { DEFAULT_LIGHTBOX_LABELS } from "../components/lightbox";
|
|
62
62
|
import { DEFAULT_WRITE_LOCK_LABELS } from "../components/write-lock";
|
|
63
63
|
import { DEFAULT_LEGAL_LABELS } from "../components/legal";
|
|
64
|
+
import { DEFAULT_SIGN_IN_LABELS } from "../auth/sign-in-form";
|
|
65
|
+
import { DEFAULT_REGISTER_LABELS } from "../auth/register-form";
|
|
66
|
+
import { DEFAULT_COMPLETE_NAME_LABELS } from "../auth/complete-name-dialog";
|
|
67
|
+
import { DEFAULT_FORGOT_PASSWORD_LABELS } from "../auth/forgot-password-form";
|
|
68
|
+
import { DEFAULT_RESET_PASSWORD_LABELS } from "../auth/reset-password-form";
|
|
69
|
+
import { DEFAULT_VERIFY_EMAIL_LABELS } from "../auth/verify-email";
|
|
70
|
+
import { DEFAULT_NOT_FOUND_LABELS } from "../auth/not-found-page";
|
|
71
|
+
import { DEFAULT_ACCEPT_INVITATION_LABELS } from "../auth/accept-invitation";
|
|
72
|
+
import { DEFAULT_COMPANY_SWITCHER_LABELS } from "../shell/company-switcher";
|
|
64
73
|
import { DEFAULT_ACCOUNT_STATE_LABELS } from "../components/account-chips";
|
|
65
74
|
import { DEFAULT_SHARE_CARD_LABELS } from "../components/share-card";
|
|
66
75
|
import { DEFAULT_REAUTH_DIALOG_LABELS } from "../components/reauth-dialog";
|
|
@@ -168,6 +177,15 @@ export const DEFAULT_UI_KIT_LABELS: UiKitLabels = {
|
|
|
168
177
|
serverWake: DEFAULT_SERVER_WAKE_LABELS,
|
|
169
178
|
translationReview: DEFAULT_TRANSLATION_REVIEW_LABELS,
|
|
170
179
|
legal: DEFAULT_LEGAL_LABELS,
|
|
180
|
+
signIn: DEFAULT_SIGN_IN_LABELS,
|
|
181
|
+
register: DEFAULT_REGISTER_LABELS,
|
|
182
|
+
completeName: DEFAULT_COMPLETE_NAME_LABELS,
|
|
183
|
+
forgotPassword: DEFAULT_FORGOT_PASSWORD_LABELS,
|
|
184
|
+
resetPassword: DEFAULT_RESET_PASSWORD_LABELS,
|
|
185
|
+
verifyEmail: DEFAULT_VERIFY_EMAIL_LABELS,
|
|
186
|
+
notFound: DEFAULT_NOT_FOUND_LABELS,
|
|
187
|
+
acceptInvitation: DEFAULT_ACCEPT_INVITATION_LABELS,
|
|
188
|
+
companySwitcher: DEFAULT_COMPANY_SWITCHER_LABELS,
|
|
171
189
|
characterCount: DEFAULT_CHARACTER_COUNT_LABELS,
|
|
172
190
|
countrySelect: DEFAULT_COUNTRY_SELECT_LABELS,
|
|
173
191
|
inlineEdit: DEFAULT_INLINE_EDIT_LABELS,
|
package/src/i18n/german.ts
CHANGED
|
@@ -779,6 +779,11 @@ export function germanLabels(numberLocale: string): UiKitLabels {
|
|
|
779
779
|
privacy:
|
|
780
780
|
"Geschlossene Beta. Die rechtlichen Formulierungen unten sind noch nicht anwaltlich geprüft; das geschieht vor einem öffentlichen Start. Die technischen Angaben — was gespeichert wird, wo, und wer es lesen kann — beschreiben, was die Software heute tatsächlich tut, und sind ausdrücklich dazu da, daran überprüft zu werden.",
|
|
781
781
|
},
|
|
782
|
+
translation: {
|
|
783
|
+
note: (o) =>
|
|
784
|
+
`Dies ist eine Übersetzung zu Ihrer Information. Massgebend ist die Fassung auf ${o.bindingLanguage}.`,
|
|
785
|
+
show: (o) => `Fassung auf ${o.bindingLanguage} anzeigen`,
|
|
786
|
+
},
|
|
782
787
|
sections: {
|
|
783
788
|
impressum: {
|
|
784
789
|
operator: {
|
|
@@ -833,6 +838,11 @@ export function germanLabels(numberLocale: string): UiKitLabels {
|
|
|
833
838
|
title: "Änderungen dieser Bedingungen",
|
|
834
839
|
body: "Diese Bedingungen können im Zuge der Weiterentwicklung des Dienstes angepasst werden. Wesentliche Änderungen werden per E-Mail oder in der App angekündigt; die weitere Nutzung nach einer Änderung gilt als Zustimmung.",
|
|
835
840
|
},
|
|
841
|
+
language: {
|
|
842
|
+
title: "Sprache",
|
|
843
|
+
body: (o) =>
|
|
844
|
+
`Diese Bedingungen sind auf ${o.bindingLanguage} verfasst. Übersetzungen in andere Sprachen dienen nur der Information; weicht eine Übersetzung ab, ist die Fassung auf ${o.bindingLanguage} massgebend.`,
|
|
845
|
+
},
|
|
836
846
|
law: {
|
|
837
847
|
title: "Anwendbares Recht",
|
|
838
848
|
body: (o) =>
|
|
@@ -841,6 +851,149 @@ export function germanLabels(numberLocale: string): UiKitLabels {
|
|
|
841
851
|
},
|
|
842
852
|
},
|
|
843
853
|
},
|
|
854
|
+
// 0.29.0 — docs/auth-harmonization.md: keksdose's reviewed de-CH wording (auth.*,
|
|
855
|
+
// verify_email, error.not_found_*, budget_share.join_*), kastlan's for the names and
|
|
856
|
+
// the 2FA prompt; written fresh where neither app had the sentence. Formal "Sie".
|
|
857
|
+
signIn: {
|
|
858
|
+
email: "E-Mail",
|
|
859
|
+
password: "Passwort",
|
|
860
|
+
submit: "Anmelden",
|
|
861
|
+
passkey: "Mit Passkey anmelden",
|
|
862
|
+
forgotPassword: "Passwort vergessen?",
|
|
863
|
+
noAccount: "Noch kein Konto?",
|
|
864
|
+
register: "Registrieren",
|
|
865
|
+
invalidCredentials: "Die E-Mail-Adresse oder das Passwort ist falsch.",
|
|
866
|
+
failed: "Anmeldung fehlgeschlagen. Bitte versuchen Sie es erneut.",
|
|
867
|
+
passkeyFailed: "Passkey-Anmeldung fehlgeschlagen.",
|
|
868
|
+
deactivatedHint: "Konto deaktiviert? Schreiben Sie an {contact}.",
|
|
869
|
+
tagHint: (taggedAddress) =>
|
|
870
|
+
`Mit ${taggedAddress} registriert? Verwenden Sie diese Adresse.`,
|
|
871
|
+
twoFactorTitle: "Zwei-Faktor-Authentifizierung",
|
|
872
|
+
twoFactorIntro: "Geben Sie den Code aus Ihrer Authenticator-App ein.",
|
|
873
|
+
code: "2FA-Code",
|
|
874
|
+
verify: "Bestätigen",
|
|
875
|
+
codeInvalid: "Ungültiger 2FA-Code.",
|
|
876
|
+
setPasswordTitle: "Neues Passwort vergeben",
|
|
877
|
+
setPasswordIntro:
|
|
878
|
+
"Für dieses Konto wurde ein neues Passwort angefordert. Vergeben Sie eines, dann geht es direkt weiter.",
|
|
879
|
+
newPassword: "Neues Passwort",
|
|
880
|
+
confirmPassword: "Neues Passwort wiederholen",
|
|
881
|
+
passwordMismatch: "Die Passwörter stimmen nicht überein.",
|
|
882
|
+
setPasswordSubmit: "Passwort vergeben und anmelden",
|
|
883
|
+
setPasswordFailed: "Das Passwort konnte nicht vergeben werden.",
|
|
884
|
+
expired: "Diese Anmeldung ist abgelaufen. Bitte melden Sie sich erneut an.",
|
|
885
|
+
backToSignIn: "Zurück zur Anmeldung",
|
|
886
|
+
},
|
|
887
|
+
register: {
|
|
888
|
+
firstName: "Vorname",
|
|
889
|
+
lastName: "Nachname",
|
|
890
|
+
email: "E-Mail",
|
|
891
|
+
emailTagUse: (address) => `${address} verwenden`,
|
|
892
|
+
emailTagHint:
|
|
893
|
+
"Viele Anbieter stellen name+kennung@… an dasselbe Postfach zu, damit Post von dieser App filter- und zuordenbar wird. Prüfen Sie vorher, ob Ihrer das tut: Angemeldet wird dann mit der markierten Adresse.",
|
|
894
|
+
invitedEmailHint: "Die Adresse, an die Ihre Einladung gesendet wurde.",
|
|
895
|
+
invitedTagNote: "Die markierte Adresse erhält eine eigene Bestätigungs-E-Mail.",
|
|
896
|
+
password: "Passwort",
|
|
897
|
+
confirmPassword: "Passwort wiederholen",
|
|
898
|
+
passwordMismatch: "Die Passwörter stimmen nicht überein.",
|
|
899
|
+
language: "Sprache",
|
|
900
|
+
submit: "Registrieren",
|
|
901
|
+
haveAccount: "Bereits registriert?",
|
|
902
|
+
signIn: "Anmelden",
|
|
903
|
+
emailTaken: "Ein Konto mit dieser E-Mail-Adresse existiert bereits.",
|
|
904
|
+
registrationClosed:
|
|
905
|
+
"Neue Konten gibt es nur auf Einladung. Bitten Sie den Betreiber, Ihre E-Mail-Adresse einzuladen.",
|
|
906
|
+
invitationInvalid: "Dieser Einladungslink ist ungültig.",
|
|
907
|
+
invitationExpired: "Diese Einladung ist abgelaufen. Bitten Sie um eine neue.",
|
|
908
|
+
failed: "Registrierung fehlgeschlagen. Bitte versuchen Sie es erneut.",
|
|
909
|
+
},
|
|
910
|
+
completeName: {
|
|
911
|
+
title: "Namen vervollständigen",
|
|
912
|
+
description:
|
|
913
|
+
"Vor- und Nachname werden jetzt getrennt erfasst. Prüfen Sie, was eingetragen ist, und ergänzen Sie, was fehlt.",
|
|
914
|
+
firstName: "Vorname",
|
|
915
|
+
lastName: "Nachname",
|
|
916
|
+
save: "Speichern",
|
|
917
|
+
later: "Später",
|
|
918
|
+
failed: "Ihr Name konnte nicht gespeichert werden. Bitte versuchen Sie es erneut.",
|
|
919
|
+
},
|
|
920
|
+
forgotPassword: {
|
|
921
|
+
title: "Passwort vergessen",
|
|
922
|
+
intro:
|
|
923
|
+
"Geben Sie die E-Mail-Adresse Ihres Kontos ein. Wir schicken Ihnen einen Link, mit dem Sie ein neues Passwort vergeben können.",
|
|
924
|
+
email: "E-Mail",
|
|
925
|
+
submit: "Link anfordern",
|
|
926
|
+
sent: (email) => `Falls es ein Konto zu ${email} gibt, ist der Link unterwegs.`,
|
|
927
|
+
sentHint: "Der Link gilt eine Stunde und funktioniert genau einmal. Sehen Sie auch im Spam-Ordner nach.",
|
|
928
|
+
backToSignIn: "Zurück zur Anmeldung",
|
|
929
|
+
error: "Anfrage fehlgeschlagen. Bitte versuchen Sie es später noch einmal.",
|
|
930
|
+
},
|
|
931
|
+
resetPassword: {
|
|
932
|
+
title: "Neues Passwort vergeben",
|
|
933
|
+
checking: "Link wird geprüft…",
|
|
934
|
+
intro: (email) => `Sie vergeben ein neues Passwort für ${email}.`,
|
|
935
|
+
newPassword: "Neues Passwort",
|
|
936
|
+
confirmPassword: "Neues Passwort wiederholen",
|
|
937
|
+
mismatch: "Die Passwörter stimmen nicht überein.",
|
|
938
|
+
submit: "Passwort speichern",
|
|
939
|
+
failed: "Das Passwort konnte nicht geändert werden.",
|
|
940
|
+
noToken: "Dieser Link ist unvollständig.",
|
|
941
|
+
invalid: "Dieser Link ist ungültig oder abgelaufen.",
|
|
942
|
+
invalidHint: "Links gelten eine Stunde und funktionieren nur einmal. Fordern Sie einfach einen neuen an.",
|
|
943
|
+
requestNew: "Neuen Link anfordern",
|
|
944
|
+
success: "Ihr Passwort ist geändert. Sie können sich jetzt damit anmelden.",
|
|
945
|
+
sessionsEnded: "Angemeldete Geräte wurden abgemeldet – dort müssen Sie sich neu anmelden.",
|
|
946
|
+
signIn: "Zur Anmeldung",
|
|
947
|
+
},
|
|
948
|
+
verifyEmail: {
|
|
949
|
+
title: "E-Mail-Adresse bestätigen",
|
|
950
|
+
verifying: "Wird bestätigt…",
|
|
951
|
+
verified: "Ihre E-Mail-Adresse ist bestätigt.",
|
|
952
|
+
continue: "Weiter",
|
|
953
|
+
invalid: "Dieser Bestätigungslink ist ungültig",
|
|
954
|
+
expired: "Dieser Bestätigungslink ist abgelaufen",
|
|
955
|
+
noToken: "Diesem Link fehlt das Bestätigungs-Token.",
|
|
956
|
+
invalidHint: "Der Link ist vermutlich abgelaufen oder wurde schon benutzt.",
|
|
957
|
+
requestInApp: "Sie können in der App einen neuen anfordern.",
|
|
958
|
+
requestHere: "Sie können hier einen neuen anfordern.",
|
|
959
|
+
resend: "Erneut senden",
|
|
960
|
+
resendIn: (seconds) => `In ${n(seconds)} s erneut senden`,
|
|
961
|
+
resent: "Bestätigungs-E-Mail gesendet",
|
|
962
|
+
resendError: "Bestätigungs-E-Mail konnte nicht gesendet werden",
|
|
963
|
+
banner: "Bitte bestätigen Sie Ihre E-Mail-Adresse.",
|
|
964
|
+
dismiss: "Später",
|
|
965
|
+
},
|
|
966
|
+
notFound: {
|
|
967
|
+
title: "Seite nicht gefunden",
|
|
968
|
+
body: "Diese Adresse gibt es (nicht mehr). Vielleicht ein Tippfehler – oder die Seite ist umgezogen.",
|
|
969
|
+
home: "Zur Startseite",
|
|
970
|
+
app: "Zurück zur App",
|
|
971
|
+
},
|
|
972
|
+
acceptInvitation: {
|
|
973
|
+
title: "Einladung annehmen",
|
|
974
|
+
// Not keksdose's "Trete bei…": first person, where this catalogue's progress
|
|
975
|
+
// words are impersonal ("Wird geladen…").
|
|
976
|
+
accepting: "Beitritt läuft…",
|
|
977
|
+
accepted: "Einladung angenommen.",
|
|
978
|
+
joined: (name) => `Sie sind ${name} beigetreten.`,
|
|
979
|
+
continue: "Weiter",
|
|
980
|
+
invalid: "Beitritt über diesen Link nicht möglich",
|
|
981
|
+
expired: "Diese Einladung ist abgelaufen",
|
|
982
|
+
noToken: "Diesem Link fehlt das Einladungs-Token.",
|
|
983
|
+
askAgain: "Bitten Sie die Person, die Sie eingeladen hat, um eine neue Einladung.",
|
|
984
|
+
signInFirst: "Melden Sie sich an, um diese Einladung anzunehmen",
|
|
985
|
+
signInHint: "Verwenden Sie das Konto, an das die Einladung gesendet wurde.",
|
|
986
|
+
signIn: "Anmelden",
|
|
987
|
+
register: "Registrieren",
|
|
988
|
+
},
|
|
989
|
+
companySwitcher: {
|
|
990
|
+
// "Firma", as kastlan's sign-up asks for the "Firmenname".
|
|
991
|
+
switchCompany: "Firma wechseln",
|
|
992
|
+
heading: "Firmen",
|
|
993
|
+
current: (name) => `Firma: ${name}`,
|
|
994
|
+
currentMark: "(aktuell)",
|
|
995
|
+
switching: "Firma wird gewechselt…",
|
|
996
|
+
},
|
|
844
997
|
characterCount: {
|
|
845
998
|
// "Zeichen" is the same in singular and plural.
|
|
846
999
|
count: (used, max) => `${n(used)} von ${n(max)} Zeichen`,
|
package/src/i18n/kit-labels.tsx
CHANGED
|
@@ -57,6 +57,15 @@ import type { ImageGridLabels } from "../components/image-grid";
|
|
|
57
57
|
import type { LightboxLabels } from "../components/lightbox";
|
|
58
58
|
import type { WriteLockLabels } from "../components/write-lock";
|
|
59
59
|
import type { LegalLabels } from "../components/legal";
|
|
60
|
+
import type { SignInLabels } from "../auth/sign-in-form";
|
|
61
|
+
import type { RegisterLabels } from "../auth/register-form";
|
|
62
|
+
import type { CompleteNameLabels } from "../auth/complete-name-dialog";
|
|
63
|
+
import type { ForgotPasswordLabels } from "../auth/forgot-password-form";
|
|
64
|
+
import type { ResetPasswordLabels } from "../auth/reset-password-form";
|
|
65
|
+
import type { VerifyEmailLabels } from "../auth/verify-email";
|
|
66
|
+
import type { NotFoundLabels } from "../auth/not-found-page";
|
|
67
|
+
import type { AcceptInvitationLabels } from "../auth/accept-invitation";
|
|
68
|
+
import type { CompanySwitcherLabels } from "../shell/company-switcher";
|
|
60
69
|
import type { AccountStateLabels } from "../components/account-chips";
|
|
61
70
|
import type { ShareCardLabels } from "../components/share-card";
|
|
62
71
|
import type { ReauthDialogLabels } from "../components/reauth-dialog";
|
|
@@ -323,6 +332,24 @@ export interface UiKitLabels {
|
|
|
323
332
|
* sections that are the same in every app, filled in with the app's operator
|
|
324
333
|
* (docs/legal-harmonization.md §4.3/§4.4). */
|
|
325
334
|
legal: LegalLabels;
|
|
335
|
+
/** 0.29.0: sign-in form: credentials, passkey, 2FA, forced new password (docs/auth-harmonization.md §5). */
|
|
336
|
+
signIn: SignInLabels;
|
|
337
|
+
/** 0.29.0: the sign-up form, with the +app address tag (§4.1, §4.5). */
|
|
338
|
+
register: RegisterLabels;
|
|
339
|
+
/** 0.29.0: asking a migrated account for first and last name once (§3.3). */
|
|
340
|
+
completeName: CompleteNameLabels;
|
|
341
|
+
/** 0.29.0: requesting a reset link (always the same answer). */
|
|
342
|
+
forgotPassword: ForgotPasswordLabels;
|
|
343
|
+
/** 0.29.0: choosing a new password from a reset link; not a sign-in (§6.3). */
|
|
344
|
+
resetPassword: ResetPasswordLabels;
|
|
345
|
+
/** 0.29.0: the verify-email page and the unverified banner. */
|
|
346
|
+
verifyEmail: VerifyEmailLabels;
|
|
347
|
+
/** 0.29.0: the 404 page. */
|
|
348
|
+
notFound: NotFoundLabels;
|
|
349
|
+
/** 0.29.0: accepting an invitation with an account (§4.4). */
|
|
350
|
+
acceptInvitation: AcceptInvitationLabels;
|
|
351
|
+
/** 0.29.0: kastlan's switcher between a user's companies (§5.3). */
|
|
352
|
+
companySwitcher: CompanySwitcherLabels;
|
|
326
353
|
/** 0.22.0: the screen-reader words of `Input` / `Textarea`'s `showCount` counter. */
|
|
327
354
|
characterCount: CharacterCountLabels;
|
|
328
355
|
/** 0.22.0: `CountrySelect`. Its list's "no results" and counts are `combobox`'s. */
|