@eifi1/ui-kit 0.28.1 → 0.30.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 +44 -4
- package/dist/account/account-parts.d.ts +21 -0
- package/dist/account/account-parts.js +40 -0
- package/dist/account/account-parts.js.map +1 -0
- package/dist/account/data-export-setting.d.ts +73 -0
- package/dist/account/data-export-setting.js +113 -0
- package/dist/account/data-export-setting.js.map +1 -0
- package/dist/account/delete-account-setting.d.ts +91 -0
- package/dist/account/delete-account-setting.js +199 -0
- package/dist/account/delete-account-setting.js.map +1 -0
- package/dist/account/email-change-setting.d.ts +136 -0
- package/dist/account/email-change-setting.js +277 -0
- package/dist/account/email-change-setting.js.map +1 -0
- package/dist/account/sessions-setting.d.ts +110 -0
- package/dist/account/sessions-setting.js +146 -0
- package/dist/account/sessions-setting.js.map +1 -0
- package/dist/admin/admin-action-confirm.d.ts +91 -0
- package/dist/admin/admin-action-confirm.js +276 -0
- package/dist/admin/admin-action-confirm.js.map +1 -0
- package/dist/admin/admin-action-log.d.ts +105 -0
- package/dist/admin/admin-action-log.js +153 -0
- package/dist/admin/admin-action-log.js.map +1 -0
- package/dist/admin/admin-parts.d.ts +40 -0
- package/dist/admin/admin-parts.js +45 -0
- package/dist/admin/admin-parts.js.map +1 -0
- package/dist/admin/invitations-panel.d.ts +91 -0
- package/dist/admin/invitations-panel.js +289 -0
- package/dist/admin/invitations-panel.js.map +1 -0
- package/dist/admin/reviewer-scope-editor.d.ts +91 -0
- package/dist/admin/reviewer-scope-editor.js +134 -0
- package/dist/admin/reviewer-scope-editor.js.map +1 -0
- package/dist/admin/role-select.d.ts +91 -0
- package/dist/admin/role-select.js +128 -0
- package/dist/admin/role-select.js.map +1 -0
- package/dist/admin/transfer-ownership-dialog.d.ts +91 -0
- package/dist/admin/transfer-ownership-dialog.js +83 -0
- package/dist/admin/transfer-ownership-dialog.js.map +1 -0
- package/dist/admin/user-roster.d.ts +91 -0
- package/dist/admin/user-roster.js +285 -0
- package/dist/admin/user-roster.js.map +1 -0
- 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 +139 -0
- package/dist/auth/auth-errors.js +92 -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 +86 -0
- package/dist/auth/forgot-password-form.js +123 -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 +91 -0
- package/dist/auth/register-form.js +270 -0
- package/dist/auth/register-form.js.map +1 -0
- package/dist/auth/reset-password-form.d.ts +131 -0
- package/dist/auth/reset-password-form.js +167 -0
- package/dist/auth/reset-password-form.js.map +1 -0
- package/dist/auth/sign-in-form.d.ts +295 -0
- package/dist/auth/sign-in-form.js +426 -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 +126 -0
- package/dist/auth/verify-email.js +243 -0
- package/dist/auth/verify-email.js.map +1 -0
- package/dist/chart.d.ts +25 -5
- package/dist/components/account-chips.d.ts +47 -7
- package/dist/components/account-chips.js +19 -3
- package/dist/components/account-chips.js.map +1 -1
- package/dist/components/account-settings-labels.d.ts +12 -1
- package/dist/components/account-settings-labels.js +4 -1
- package/dist/components/account-settings-labels.js.map +1 -1
- package/dist/components/account-settings.d.ts +61 -6
- package/dist/components/account-settings.js +115 -15
- package/dist/components/account-settings.js.map +1 -1
- package/dist/components/amount-input.d.ts +26 -5
- package/dist/components/button-group.d.ts +26 -5
- package/dist/components/calculator.d.ts +26 -5
- package/dist/components/confirm-dialog.d.ts +26 -5
- package/dist/components/copy-button.d.ts +23 -2
- package/dist/components/danger-confirm.d.ts +26 -5
- package/dist/components/facing-pair.d.ts +26 -5
- package/dist/components/file-button.d.ts +26 -5
- package/dist/components/file-dropzone.d.ts +26 -5
- package/dist/components/form-actions.d.ts +26 -5
- package/dist/components/iban-input.d.ts +26 -5
- package/dist/components/language-select.d.ts +26 -12
- package/dist/components/legal-page.d.ts +26 -5
- package/dist/components/legal.d.ts +26 -5
- package/dist/components/money-field.d.ts +26 -5
- package/dist/components/number-field.d.ts +26 -5
- package/dist/components/number-input.d.ts +26 -5
- package/dist/components/numpad-sheet.d.ts +26 -5
- package/dist/components/phone-input.d.ts +26 -5
- package/dist/components/series-chart.d.ts +26 -5
- package/dist/components/settings-fields.d.ts +25 -7
- package/dist/components/share-card.d.ts +25 -4
- package/dist/components/text-link.d.ts +26 -5
- package/dist/components/time-input.d.ts +26 -5
- package/dist/components/ui.d.ts +23 -2
- 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/{kit-labels-Du0uHBq4.d.ts → delete-account-setting-CGVMckMw.d.ts} +6654 -5302
- package/dist/feedback/feedback-attachment.d.ts +26 -5
- package/dist/feedback/feedback-context.d.ts +26 -5
- package/dist/feedback/feedback-crash.d.ts +26 -5
- package/dist/feedback/feedback-dialog.d.ts +26 -5
- package/dist/feedback/feedback-inbox.d.ts +26 -5
- package/dist/feedback/feedback-labels.d.ts +26 -5
- package/dist/feedback/feedback-menu.d.ts +26 -5
- package/dist/feedback/feedback-record.d.ts +26 -5
- package/dist/feedback/feedback-row-detail.d.ts +26 -5
- package/dist/feedback/feedback-status-undo.d.ts +26 -5
- package/dist/feedback/feedback-submit.d.ts +26 -5
- package/dist/feedback/feedback-swipe.d.ts +26 -5
- package/dist/feedback/feedback-table.d.ts +23 -2
- package/dist/feedback/feedback-thread.d.ts +26 -5
- package/dist/feedback.d.ts +26 -5
- package/dist/hooks/use-anchored-panel.d.ts +6 -2
- package/dist/hooks/use-anchored-panel.js +2 -1
- package/dist/hooks/use-anchored-panel.js.map +1 -1
- package/dist/hooks/use-file-drop.d.ts +26 -5
- package/dist/i18n/defaults.d.ts +26 -5
- package/dist/i18n/defaults.js +40 -0
- package/dist/i18n/defaults.js.map +1 -1
- package/dist/i18n/german.d.ts +26 -5
- package/dist/i18n/german.js +343 -2
- package/dist/i18n/german.js.map +1 -1
- package/dist/i18n/kit-labels.d.ts +23 -2
- package/dist/i18n/kit-labels.js.map +1 -1
- package/dist/i18n/languages.d.ts +21 -204
- package/dist/i18n/locales/de-CH.d.ts +26 -5
- package/dist/i18n/locales/en.d.ts +26 -5
- package/dist/i18n/locales/es.d.ts +26 -5
- package/dist/i18n/locales/es.js +337 -3
- package/dist/i18n/locales/es.js.map +1 -1
- package/dist/i18n/locales/fr.d.ts +26 -5
- package/dist/i18n/locales/fr.js +344 -2
- package/dist/i18n/locales/fr.js.map +1 -1
- package/dist/i18n/locales/hu.d.ts +26 -5
- package/dist/i18n/locales/hu.js +337 -3
- package/dist/i18n/locales/hu.js.map +1 -1
- package/dist/i18n/locales/it.d.ts +26 -5
- package/dist/i18n/locales/it.js +342 -2
- package/dist/i18n/locales/it.js.map +1 -1
- package/dist/i18n/locales/zh.d.ts +26 -5
- package/dist/i18n/locales/zh.js +333 -2
- package/dist/i18n/locales/zh.js.map +1 -1
- package/dist/i18n/review.d.ts +26 -5
- package/dist/i18n/review.js +45 -1
- package/dist/i18n/review.js.map +1 -1
- package/dist/index.d.ts +19 -3
- package/dist/index.js +89 -0
- 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/rhf/fields.d.ts +26 -5
- package/dist/rhf/form.d.ts +26 -5
- package/dist/rhf.d.ts +26 -5
- package/dist/shell/app-shell.d.ts +26 -5
- 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 +26 -5
- package/dist/shell/topbar-action-menu.d.ts +1 -0
- package/dist/shell.d.ts +20 -5
- package/dist/wizard/stepper-nav.d.ts +26 -5
- package/dist/wizard.d.ts +26 -5
- package/package.json +1 -1
- package/src/account/account-parts.ts +67 -0
- package/src/account/data-export-setting.tsx +218 -0
- package/src/account/delete-account-setting.tsx +362 -0
- package/src/account/email-change-setting.tsx +457 -0
- package/src/account/sessions-setting.tsx +283 -0
- package/src/admin/admin-action-confirm.tsx +495 -0
- package/src/admin/admin-action-log.tsx +297 -0
- package/src/admin/admin-parts.ts +82 -0
- package/src/admin/invitations-panel.tsx +516 -0
- package/src/admin/reviewer-scope-editor.tsx +234 -0
- package/src/admin/role-select.tsx +286 -0
- package/src/admin/transfer-ownership-dialog.tsx +172 -0
- package/src/admin/user-roster.tsx +573 -0
- package/src/auth/accept-invitation.tsx +227 -0
- package/src/auth/auth-errors.ts +262 -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 +229 -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 +477 -0
- package/src/auth/reset-password-form.tsx +351 -0
- package/src/auth/sign-in-form.tsx +859 -0
- package/src/auth/status-parts.tsx +138 -0
- package/src/auth/verify-email.tsx +409 -0
- package/src/components/account-chips.tsx +64 -7
- package/src/components/account-settings-labels.ts +15 -1
- package/src/components/account-settings.tsx +199 -23
- package/src/components/user-avatar.tsx +22 -4
- package/src/hooks/use-anchored-panel.ts +14 -3
- package/src/i18n/defaults.ts +40 -0
- package/src/i18n/german.ts +379 -0
- package/src/i18n/kit-labels.tsx +60 -0
- package/src/i18n/locales/es.ts +371 -1
- package/src/i18n/locales/fr.ts +379 -0
- package/src/i18n/locales/hu.ts +366 -1
- package/src/i18n/locales/it.ts +373 -0
- package/src/i18n/locales/zh.ts +343 -0
- package/src/i18n/review.ts +44 -0
- package/src/index.ts +146 -0
- package/src/lib/person-name.ts +157 -0
- package/src/shell/company-switcher.tsx +140 -0
|
@@ -20,8 +20,19 @@ export interface ProfileSettingLabels {
|
|
|
20
20
|
email: string;
|
|
21
21
|
role: string;
|
|
22
22
|
memberSince: string;
|
|
23
|
+
/** The single name field — the card without `firstName` / `lastName`. */
|
|
23
24
|
displayName: string;
|
|
24
25
|
save: string;
|
|
26
|
+
/**
|
|
27
|
+
* 0.30.0 (docs/user-admin-harmonization.md §6.1): the two fields that replace
|
|
28
|
+
* `displayName` once the card is given `firstName` / `lastName`. OPTIONAL, like
|
|
29
|
+
* `TwoFactorSettingLabels.qrAlt` and for its reason — this interface is annotated at
|
|
30
|
+
* call sites — and filled from the provider's `accountSettings.profile`, then English.
|
|
31
|
+
*/
|
|
32
|
+
firstName?: string;
|
|
33
|
+
lastName?: string;
|
|
34
|
+
/** Why Save is held while a name is blank: both are required (§3.1). */
|
|
35
|
+
nameRequired?: string;
|
|
25
36
|
}
|
|
26
37
|
|
|
27
38
|
export interface PasswordSettingLabels {
|
|
@@ -104,7 +115,7 @@ export interface PasskeysSettingLabels {
|
|
|
104
115
|
}
|
|
105
116
|
|
|
106
117
|
export interface AccountSettingsLabels {
|
|
107
|
-
profile: ProfileSettingLabels
|
|
118
|
+
profile: Required<ProfileSettingLabels>;
|
|
108
119
|
password: PasswordSettingLabels;
|
|
109
120
|
// `Required` here, optional on the section interfaces: the namespace is what a full
|
|
110
121
|
// catalogue (`@eifi1/ui-kit/i18n/<code>`, `missingKitLabels`) is written against, so
|
|
@@ -121,6 +132,9 @@ export const DEFAULT_ACCOUNT_SETTINGS_LABELS: AccountSettingsLabels = {
|
|
|
121
132
|
memberSince: "Member since",
|
|
122
133
|
displayName: "Display name",
|
|
123
134
|
save: "Save",
|
|
135
|
+
firstName: "First name",
|
|
136
|
+
lastName: "Last name",
|
|
137
|
+
nameRequired: "Enter both a first and a last name.",
|
|
124
138
|
},
|
|
125
139
|
password: {
|
|
126
140
|
title: "Change password",
|
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
import { useId, useState } from "react";
|
|
2
|
-
import type { KeyboardEvent, ReactNode } from "react";
|
|
2
|
+
import type { FormEvent, KeyboardEvent, ReactNode } from "react";
|
|
3
3
|
import { Button, Card, Input } from "./ui";
|
|
4
4
|
import { UserAvatar } from "./user-avatar";
|
|
5
5
|
import { CopyButton } from "./copy-button";
|
|
6
6
|
import { QrCode } from "./qr-code";
|
|
7
7
|
import { OneTimeCodeInput } from "./one-time-code-input";
|
|
8
|
+
import { usePromisePending } from "./danger-confirm";
|
|
9
|
+
import { personNameOk, PERSON_NAME_MAX_LENGTH } from "../auth/form-rules";
|
|
8
10
|
import {
|
|
9
11
|
DEFAULT_PASSWORD_STRENGTH_LABELS,
|
|
10
12
|
passwordByteLength,
|
|
@@ -40,38 +42,90 @@ export { DEFAULT_ACCOUNT_SETTINGS_LABELS } from "./account-settings-labels";
|
|
|
40
42
|
* `accountSettings` namespace, then English (see account-settings-labels.ts).
|
|
41
43
|
*/
|
|
42
44
|
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
}: {
|
|
45
|
+
/** What {@link ProfileSetting} hands `onSave` in its two-field form (0.30.0): both
|
|
46
|
+
* trimmed, 1–120 characters each — the `PATCH /auth/me` body's `first_name` /
|
|
47
|
+
* `last_name` (docs/user-admin-harmonization.md §6.1). */
|
|
48
|
+
export interface ProfileNameValues {
|
|
49
|
+
firstName: string;
|
|
50
|
+
lastName: string;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/** What both forms of {@link ProfileSetting} take. */
|
|
54
|
+
interface ProfileSettingCommonProps {
|
|
54
55
|
name?: string | null;
|
|
55
56
|
email?: string | null;
|
|
56
57
|
role?: ReactNode;
|
|
57
58
|
memberSince?: ReactNode;
|
|
58
|
-
value: string;
|
|
59
|
-
onChange: (value: string) => void;
|
|
60
|
-
onSave: () => void;
|
|
61
59
|
saving?: boolean;
|
|
62
60
|
/** Prop > `<UiKitProvider labels={{ accountSettings: { profile } }}>` > English. */
|
|
63
61
|
labels?: Partial<ProfileSettingLabels>;
|
|
64
|
-
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* The single display name, as before 0.30.0 — controlled through `value` / `onChange`,
|
|
66
|
+
* saved by `onSave()`. Kept unchanged for an app that has not moved to first and last
|
|
67
|
+
* name yet.
|
|
68
|
+
*/
|
|
69
|
+
export interface ProfileSettingDisplayNameProps extends ProfileSettingCommonProps {
|
|
70
|
+
value: string;
|
|
71
|
+
onChange: (value: string) => void;
|
|
72
|
+
onSave: () => void;
|
|
73
|
+
firstName?: undefined;
|
|
74
|
+
lastName?: undefined;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* First and last name (0.30.0, docs/user-admin-harmonization.md §6.1): the saved
|
|
79
|
+
* values in, the edited ones out through `onSave`. The card keeps the drafts itself.
|
|
80
|
+
*/
|
|
81
|
+
export interface ProfileSettingNamesProps extends ProfileSettingCommonProps {
|
|
82
|
+
/** The saved given name — `first_name`. `null` reads as empty. */
|
|
83
|
+
firstName: string | null;
|
|
84
|
+
/** The saved family name — `last_name`. `null` (a migrated account, §3.3) reads as
|
|
85
|
+
* empty, and Save then waits for one. */
|
|
86
|
+
lastName: string | null;
|
|
87
|
+
/**
|
|
88
|
+
* Save the two names (`PATCH /auth/me`, which also clears `name_incomplete`). Return
|
|
89
|
+
* a promise and Save is busy until it settles. Refresh `firstName` / `lastName` from
|
|
90
|
+
* the answer: the card compares its drafts with them, so Save turns off once the new
|
|
91
|
+
* names come back. A rejection keeps what was typed — the app says why (a toast), as
|
|
92
|
+
* for `PasswordSetting`.
|
|
93
|
+
*/
|
|
94
|
+
onSave: (values: ProfileNameValues) => void | Promise<unknown>;
|
|
95
|
+
value?: undefined;
|
|
96
|
+
onChange?: undefined;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
export type ProfileSettingProps = ProfileSettingDisplayNameProps | ProfileSettingNamesProps;
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* The profile section: avatar, address, role, member since, and the name.
|
|
103
|
+
*
|
|
104
|
+
* **Two forms.** Given `firstName` / `lastName` (0.30.0, §6.1) it edits first and last
|
|
105
|
+
* name in two fields, side by side where the card is wide enough — the order and the
|
|
106
|
+
* 1–120-character rule of `RegisterForm` (auth §3.1), so the name a person signed up
|
|
107
|
+
* with is edited the way it was typed. Without them it is the single `displayName`
|
|
108
|
+
* field it always was, `value` / `onChange` / `onSave()` unchanged, for an app that has
|
|
109
|
+
* not moved yet (kastlan split the one name at its first space on save; Kurvenschmiede
|
|
110
|
+
* built its own card instead).
|
|
111
|
+
*
|
|
112
|
+
* The two-field form owns its drafts, starting from the saved names and following them
|
|
113
|
+
* when they change; Save waits for a change, holds — saying why — while a name is
|
|
114
|
+
* blank, and is a `commit` (a {@link WriteLockProvider} locks it, keksdose's demo).
|
|
115
|
+
*/
|
|
116
|
+
export function ProfileSetting(props: ProfileSettingProps) {
|
|
117
|
+
const { name, email, role, memberSince, labels: labelsProp } = props;
|
|
65
118
|
const labels = useAccountSettingsLabels("profile", labelsProp);
|
|
66
|
-
const
|
|
67
|
-
// Generated, not the literal "display-name" it was: two of these on one page (an
|
|
68
|
-
// admin editing someone else's profile beside their own) shared an id, and the
|
|
69
|
-
// second label pointed at the first field.
|
|
70
|
-
const inputId = useId();
|
|
119
|
+
const names = props.firstName !== undefined || props.lastName !== undefined;
|
|
71
120
|
return (
|
|
72
121
|
<Card className="p-4 space-y-3">
|
|
73
122
|
<div className="flex items-center gap-3">
|
|
74
|
-
<UserAvatar
|
|
123
|
+
<UserAvatar
|
|
124
|
+
name={name}
|
|
125
|
+
email={email}
|
|
126
|
+
person={names ? { first: props.firstName, last: props.lastName } : undefined}
|
|
127
|
+
size="lg"
|
|
128
|
+
/>
|
|
75
129
|
<div className="min-w-0">
|
|
76
130
|
<div className="text-sm font-medium">{labels.title}</div>
|
|
77
131
|
<div className="truncate font-mono text-xs text-[var(--text-muted)]">
|
|
@@ -88,6 +142,51 @@ export function ProfileSetting({
|
|
|
88
142
|
<span className="text-[var(--text-muted)]">{labels.memberSince}</span>
|
|
89
143
|
<span>{memberSince ?? "—"}</span>
|
|
90
144
|
</div>
|
|
145
|
+
{names ? (
|
|
146
|
+
<PersonNameFields
|
|
147
|
+
firstName={props.firstName ?? ""}
|
|
148
|
+
lastName={props.lastName ?? ""}
|
|
149
|
+
onSave={(props as ProfileSettingNamesProps).onSave}
|
|
150
|
+
saving={props.saving}
|
|
151
|
+
labels={labels}
|
|
152
|
+
/>
|
|
153
|
+
) : (
|
|
154
|
+
<DisplayNameField
|
|
155
|
+
name={name}
|
|
156
|
+
value={(props as ProfileSettingDisplayNameProps).value}
|
|
157
|
+
onChange={(props as ProfileSettingDisplayNameProps).onChange}
|
|
158
|
+
onSave={(props as ProfileSettingDisplayNameProps).onSave}
|
|
159
|
+
saving={props.saving}
|
|
160
|
+
labels={labels}
|
|
161
|
+
/>
|
|
162
|
+
)}
|
|
163
|
+
</Card>
|
|
164
|
+
);
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
/** The single-name form — exactly the 0.29 card's field and button. */
|
|
168
|
+
function DisplayNameField({
|
|
169
|
+
name,
|
|
170
|
+
value,
|
|
171
|
+
onChange,
|
|
172
|
+
onSave,
|
|
173
|
+
saving,
|
|
174
|
+
labels,
|
|
175
|
+
}: {
|
|
176
|
+
name?: string | null;
|
|
177
|
+
value: string;
|
|
178
|
+
onChange: (value: string) => void;
|
|
179
|
+
onSave: () => void;
|
|
180
|
+
saving?: boolean;
|
|
181
|
+
labels: Required<ProfileSettingLabels>;
|
|
182
|
+
}) {
|
|
183
|
+
const dirty = value.trim().length > 0 && value.trim() !== (name ?? "");
|
|
184
|
+
// Generated, not the literal "display-name" it was: two of these on one page (an
|
|
185
|
+
// admin editing someone else's profile beside their own) shared an id, and the
|
|
186
|
+
// second label pointed at the first field.
|
|
187
|
+
const inputId = useId();
|
|
188
|
+
return (
|
|
189
|
+
<>
|
|
91
190
|
<Input
|
|
92
191
|
id={inputId}
|
|
93
192
|
label={labels.displayName}
|
|
@@ -98,7 +197,84 @@ export function ProfileSetting({
|
|
|
98
197
|
<Button onClick={onSave} disabled={!dirty || saving}>
|
|
99
198
|
{labels.save}
|
|
100
199
|
</Button>
|
|
101
|
-
|
|
200
|
+
</>
|
|
201
|
+
);
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
/** The first-and-last form (0.30.0, §6.1). */
|
|
205
|
+
function PersonNameFields({
|
|
206
|
+
firstName,
|
|
207
|
+
lastName,
|
|
208
|
+
onSave,
|
|
209
|
+
saving,
|
|
210
|
+
labels,
|
|
211
|
+
}: {
|
|
212
|
+
firstName: string;
|
|
213
|
+
lastName: string;
|
|
214
|
+
onSave: (values: ProfileNameValues) => void | Promise<unknown>;
|
|
215
|
+
saving?: boolean;
|
|
216
|
+
labels: Required<ProfileSettingLabels>;
|
|
217
|
+
}) {
|
|
218
|
+
const [first, setFirst] = useState(firstName);
|
|
219
|
+
const [last, setLast] = useState(lastName);
|
|
220
|
+
// The saved names moved (the save came back, another tab renamed): the drafts follow
|
|
221
|
+
// them. During render, as DangerConfirm resets its fields, so no frame shows the old
|
|
222
|
+
// draft beside the new name.
|
|
223
|
+
const [saved, setSaved] = useState({ firstName, lastName });
|
|
224
|
+
if (saved.firstName !== firstName || saved.lastName !== lastName) {
|
|
225
|
+
setSaved({ firstName, lastName });
|
|
226
|
+
setFirst(firstName);
|
|
227
|
+
setLast(lastName);
|
|
228
|
+
}
|
|
229
|
+
const { pending, run } = usePromisePending();
|
|
230
|
+
const busy = Boolean(saving) || pending;
|
|
231
|
+
const valid = personNameOk(first) && personNameOk(last);
|
|
232
|
+
const dirty = first.trim() !== firstName.trim() || last.trim() !== lastName.trim();
|
|
233
|
+
|
|
234
|
+
const submit = (e: FormEvent) => {
|
|
235
|
+
e.preventDefault();
|
|
236
|
+
if (!valid || !dirty || busy) return;
|
|
237
|
+
// A rejection keeps the drafts; the app reports it, as PasswordSetting's does.
|
|
238
|
+
run(onSave({ firstName: first.trim(), lastName: last.trim() }), () => {});
|
|
239
|
+
};
|
|
240
|
+
|
|
241
|
+
return (
|
|
242
|
+
<form className="@container space-y-3" onSubmit={submit} noValidate>
|
|
243
|
+
{/* The order and the rule of RegisterForm's two fields (auth §3.1). */}
|
|
244
|
+
<div className="grid gap-3 @xs:grid-cols-2">
|
|
245
|
+
<Input
|
|
246
|
+
name="given-name"
|
|
247
|
+
autoComplete="given-name"
|
|
248
|
+
label={labels.firstName}
|
|
249
|
+
value={first}
|
|
250
|
+
onChange={(e) => setFirst(e.target.value)}
|
|
251
|
+
maxLength={PERSON_NAME_MAX_LENGTH}
|
|
252
|
+
readOnly={busy}
|
|
253
|
+
required
|
|
254
|
+
/>
|
|
255
|
+
<Input
|
|
256
|
+
name="family-name"
|
|
257
|
+
autoComplete="family-name"
|
|
258
|
+
label={labels.lastName}
|
|
259
|
+
value={last}
|
|
260
|
+
onChange={(e) => setLast(e.target.value)}
|
|
261
|
+
maxLength={PERSON_NAME_MAX_LENGTH}
|
|
262
|
+
readOnly={busy}
|
|
263
|
+
required
|
|
264
|
+
/>
|
|
265
|
+
</div>
|
|
266
|
+
<Button
|
|
267
|
+
type="submit"
|
|
268
|
+
commit
|
|
269
|
+
pending={busy}
|
|
270
|
+
// A blank name is a reason the person can act on; an unchanged one is not —
|
|
271
|
+
// there is nothing to save, and the button is simply off.
|
|
272
|
+
disabled={valid && !dirty}
|
|
273
|
+
disabledReason={valid ? undefined : labels.nameRequired}
|
|
274
|
+
>
|
|
275
|
+
{labels.save}
|
|
276
|
+
</Button>
|
|
277
|
+
</form>
|
|
102
278
|
);
|
|
103
279
|
}
|
|
104
280
|
|
|
@@ -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
|
}
|
|
@@ -55,13 +55,21 @@ export interface AnchoredPanel {
|
|
|
55
55
|
/** Absolute floor: below this a list is useless, so overflow the margin instead. */
|
|
56
56
|
const FLOOR = 96;
|
|
57
57
|
|
|
58
|
+
/** How much more room above must offer, when neither side fits the preferred height,
|
|
59
|
+
* before the panel changes sides — so it never jumps for a few pixels' gain. */
|
|
60
|
+
const FLIP_GAIN = 48;
|
|
61
|
+
|
|
58
62
|
/**
|
|
59
63
|
* The pure half of {@link useAnchoredPanel}: where a panel goes, given the trigger's
|
|
60
64
|
* rect and the region actually on screen.
|
|
61
65
|
*
|
|
62
66
|
* Prefers below the trigger — the conventional direction, and the one that keeps the
|
|
63
|
-
* trigger's own value visible
|
|
64
|
-
*
|
|
67
|
+
* trigger's own value visible — whenever the panel's preferred height fits there. When it
|
|
68
|
+
* does not, the panel opens above if the full height fits there, or if above is clearly
|
|
69
|
+
* roomier (by {@link FLIP_GAIN}); below an unusable `minHeight` it flips to any roomier
|
|
70
|
+
* side, as before. It used to flip only below `minHeight`, so a select with 160–319 px
|
|
71
|
+
* under it opened downward as a cramped list with 600 px free above (keksdose live #384,
|
|
72
|
+
* the currency select of the new-budget form near the bottom of the screen). The
|
|
65
73
|
* returned `maxHeight` is what makes the flip sufficient rather than merely different:
|
|
66
74
|
* without it a tall panel placed above just runs off the top instead of the bottom.
|
|
67
75
|
*
|
|
@@ -77,7 +85,10 @@ export function anchoredPanelPlacement(
|
|
|
77
85
|
const viewBottom = viewport.top + viewport.height - margin;
|
|
78
86
|
const spaceBelow = viewBottom - (rect.bottom + gap);
|
|
79
87
|
const spaceAbove = rect.top - gap - viewTop;
|
|
80
|
-
const above =
|
|
88
|
+
const above =
|
|
89
|
+
spaceAbove > spaceBelow &&
|
|
90
|
+
(spaceBelow < minHeight ||
|
|
91
|
+
(spaceBelow < preferredHeight && (spaceAbove >= preferredHeight || spaceAbove - spaceBelow >= FLIP_GAIN)));
|
|
81
92
|
const space = Math.max(FLOOR, above ? spaceAbove : spaceBelow);
|
|
82
93
|
const maxHeight = Math.min(preferredHeight, space);
|
|
83
94
|
return {
|
package/src/i18n/defaults.ts
CHANGED
|
@@ -61,6 +61,26 @@ 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";
|
|
73
|
+
import { DEFAULT_EMAIL_CHANGE_LABELS } from "../account/email-change-setting";
|
|
74
|
+
import { DEFAULT_SESSIONS_LABELS } from "../account/sessions-setting";
|
|
75
|
+
import { DEFAULT_DELETE_ACCOUNT_LABELS } from "../account/delete-account-setting";
|
|
76
|
+
import { DEFAULT_DATA_EXPORT_LABELS } from "../account/data-export-setting";
|
|
77
|
+
import { DEFAULT_USER_ROSTER_LABELS } from "../admin/user-roster";
|
|
78
|
+
import { DEFAULT_ROLE_SELECT_LABELS } from "../admin/role-select";
|
|
79
|
+
import { DEFAULT_REVIEWER_SCOPE_LABELS } from "../admin/reviewer-scope-editor";
|
|
80
|
+
import { DEFAULT_ADMIN_ACTION_LABELS } from "../admin/admin-action-confirm";
|
|
81
|
+
import { DEFAULT_ADMIN_ACTION_LOG_LABELS } from "../admin/admin-action-log";
|
|
82
|
+
import { DEFAULT_TRANSFER_OWNERSHIP_LABELS } from "../admin/transfer-ownership-dialog";
|
|
83
|
+
import { DEFAULT_INVITATIONS_LABELS } from "../admin/invitations-panel";
|
|
64
84
|
import { DEFAULT_ACCOUNT_STATE_LABELS } from "../components/account-chips";
|
|
65
85
|
import { DEFAULT_SHARE_CARD_LABELS } from "../components/share-card";
|
|
66
86
|
import { DEFAULT_REAUTH_DIALOG_LABELS } from "../components/reauth-dialog";
|
|
@@ -168,6 +188,26 @@ export const DEFAULT_UI_KIT_LABELS: UiKitLabels = {
|
|
|
168
188
|
serverWake: DEFAULT_SERVER_WAKE_LABELS,
|
|
169
189
|
translationReview: DEFAULT_TRANSLATION_REVIEW_LABELS,
|
|
170
190
|
legal: DEFAULT_LEGAL_LABELS,
|
|
191
|
+
signIn: DEFAULT_SIGN_IN_LABELS,
|
|
192
|
+
register: DEFAULT_REGISTER_LABELS,
|
|
193
|
+
completeName: DEFAULT_COMPLETE_NAME_LABELS,
|
|
194
|
+
forgotPassword: DEFAULT_FORGOT_PASSWORD_LABELS,
|
|
195
|
+
resetPassword: DEFAULT_RESET_PASSWORD_LABELS,
|
|
196
|
+
verifyEmail: DEFAULT_VERIFY_EMAIL_LABELS,
|
|
197
|
+
notFound: DEFAULT_NOT_FOUND_LABELS,
|
|
198
|
+
acceptInvitation: DEFAULT_ACCEPT_INVITATION_LABELS,
|
|
199
|
+
companySwitcher: DEFAULT_COMPANY_SWITCHER_LABELS,
|
|
200
|
+
emailChange: DEFAULT_EMAIL_CHANGE_LABELS,
|
|
201
|
+
sessions: DEFAULT_SESSIONS_LABELS,
|
|
202
|
+
deleteAccount: DEFAULT_DELETE_ACCOUNT_LABELS,
|
|
203
|
+
dataExport: DEFAULT_DATA_EXPORT_LABELS,
|
|
204
|
+
userRoster: DEFAULT_USER_ROSTER_LABELS,
|
|
205
|
+
roleSelect: DEFAULT_ROLE_SELECT_LABELS,
|
|
206
|
+
reviewerScope: DEFAULT_REVIEWER_SCOPE_LABELS,
|
|
207
|
+
adminAction: DEFAULT_ADMIN_ACTION_LABELS,
|
|
208
|
+
adminActionLog: DEFAULT_ADMIN_ACTION_LOG_LABELS,
|
|
209
|
+
transferOwnership: DEFAULT_TRANSFER_OWNERSHIP_LABELS,
|
|
210
|
+
invitations: DEFAULT_INVITATIONS_LABELS,
|
|
171
211
|
characterCount: DEFAULT_CHARACTER_COUNT_LABELS,
|
|
172
212
|
countrySelect: DEFAULT_COUNTRY_SELECT_LABELS,
|
|
173
213
|
inlineEdit: DEFAULT_INLINE_EDIT_LABELS,
|