@eifi1/ui-kit 0.29.0 → 0.31.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 +65 -6
- 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 +114 -0
- package/dist/account/data-export-setting.js.map +1 -0
- package/dist/account/delete-account-setting.d.ts +95 -0
- package/dist/account/delete-account-setting.js +200 -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 +278 -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 +147 -0
- package/dist/account/sessions-setting.js.map +1 -0
- package/dist/admin/admin-action-confirm.d.ts +95 -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 +95 -0
- package/dist/admin/invitations-panel.js +290 -0
- package/dist/admin/invitations-panel.js.map +1 -0
- package/dist/admin/reviewer-scope-editor.d.ts +95 -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 +95 -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 +95 -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 +95 -0
- package/dist/admin/user-roster.js +285 -0
- package/dist/admin/user-roster.js.map +1 -0
- package/dist/auth/auth-errors.d.ts +103 -6
- package/dist/auth/auth-errors.js +68 -2
- package/dist/auth/auth-errors.js.map +1 -1
- package/dist/auth/forgot-password-form.d.ts +10 -3
- package/dist/auth/forgot-password-form.js +6 -3
- package/dist/auth/forgot-password-form.js.map +1 -1
- package/dist/auth/register-form.d.ts +21 -12
- package/dist/auth/register-form.js +6 -4
- package/dist/auth/register-form.js.map +1 -1
- package/dist/auth/reset-password-form.d.ts +10 -3
- package/dist/auth/reset-password-form.js +6 -3
- package/dist/auth/reset-password-form.js.map +1 -1
- package/dist/auth/sign-in-form.d.ts +75 -9
- package/dist/auth/sign-in-form.js +105 -15
- package/dist/auth/sign-in-form.js.map +1 -1
- package/dist/auth/verify-email.d.ts +7 -2
- package/dist/auth/verify-email.js +44 -8
- package/dist/auth/verify-email.js.map +1 -1
- package/dist/chart.d.ts +20 -11
- 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 +24 -1
- package/dist/components/account-settings-labels.js +6 -2
- package/dist/components/account-settings-labels.js.map +1 -1
- package/dist/components/account-settings.d.ts +69 -8
- package/dist/components/account-settings.js +133 -25
- package/dist/components/account-settings.js.map +1 -1
- package/dist/components/amount-input.d.ts +21 -12
- package/dist/components/button-group.d.ts +21 -12
- package/dist/components/calculator.d.ts +21 -12
- package/dist/components/confirm-dialog.d.ts +21 -12
- package/dist/components/copy-button.d.ts +18 -9
- package/dist/components/danger-confirm.d.ts +21 -12
- package/dist/components/data-table.js +8 -1
- package/dist/components/data-table.js.map +1 -1
- package/dist/components/date-picker.js +9 -4
- package/dist/components/date-picker.js.map +1 -1
- package/dist/components/facing-pair.d.ts +21 -12
- package/dist/components/file-button.d.ts +21 -12
- package/dist/components/file-dropzone.d.ts +21 -12
- package/dist/components/form-actions.d.ts +21 -12
- package/dist/components/iban-input.d.ts +21 -12
- package/dist/components/language-select.d.ts +21 -12
- package/dist/components/legal-page.d.ts +21 -12
- package/dist/components/legal.d.ts +21 -12
- package/dist/components/money-field.d.ts +21 -12
- package/dist/components/number-field.d.ts +21 -12
- package/dist/components/number-input.d.ts +21 -12
- package/dist/components/numpad-sheet.d.ts +21 -12
- package/dist/components/passkeys-setting.js +2 -1
- package/dist/components/passkeys-setting.js.map +1 -1
- package/dist/components/phone-input.d.ts +21 -12
- package/dist/components/series-chart.d.ts +21 -12
- package/dist/components/settings-fields.d.ts +55 -13
- package/dist/components/settings-fields.js +20 -1
- package/dist/components/settings-fields.js.map +1 -1
- package/dist/components/share-card.d.ts +20 -11
- package/dist/components/text-link.d.ts +21 -12
- package/dist/components/time-input.d.ts +21 -12
- package/dist/components/ui.d.ts +17 -8
- package/dist/{register-form-V1OeNlMr.d.ts → delete-account-setting-DCFS5Vof.d.ts} +6612 -5612
- package/dist/demo/demo-banner.d.ts +51 -0
- package/dist/demo/demo-banner.js +97 -0
- package/dist/demo/demo-banner.js.map +1 -0
- package/dist/demo/demo-countdown.d.ts +58 -0
- package/dist/demo/demo-countdown.js +79 -0
- package/dist/demo/demo-countdown.js.map +1 -0
- package/dist/demo/demo-ended.d.ts +45 -0
- package/dist/demo/demo-ended.js +42 -0
- package/dist/demo/demo-ended.js.map +1 -0
- package/dist/demo/demo-labels.d.ts +53 -0
- package/dist/demo/demo-labels.js +29 -0
- package/dist/demo/demo-labels.js.map +1 -0
- package/dist/demo/demo-session.d.ts +30 -0
- package/dist/demo/demo-session.js +8 -0
- package/dist/demo/demo-session.js.map +1 -0
- package/dist/demo/demo-start.d.ts +54 -0
- package/dist/demo/demo-start.js +68 -0
- package/dist/demo/demo-start.js.map +1 -0
- package/dist/feedback/feedback-attachment.d.ts +21 -12
- package/dist/feedback/feedback-context.d.ts +21 -12
- package/dist/feedback/feedback-crash.d.ts +21 -12
- package/dist/feedback/feedback-dialog.d.ts +21 -12
- package/dist/feedback/feedback-inbox.d.ts +21 -12
- package/dist/feedback/feedback-inbox.js +12 -2
- package/dist/feedback/feedback-inbox.js.map +1 -1
- package/dist/feedback/feedback-labels.d.ts +21 -12
- package/dist/feedback/feedback-labels.js +1 -0
- package/dist/feedback/feedback-labels.js.map +1 -1
- package/dist/feedback/feedback-menu.d.ts +21 -12
- package/dist/feedback/feedback-record.d.ts +21 -12
- package/dist/feedback/feedback-record.js +2 -0
- package/dist/feedback/feedback-record.js.map +1 -1
- package/dist/feedback/feedback-row-detail.d.ts +21 -12
- package/dist/feedback/feedback-row-detail.js +7 -0
- package/dist/feedback/feedback-row-detail.js.map +1 -1
- package/dist/feedback/feedback-status-undo.d.ts +21 -12
- package/dist/feedback/feedback-submit.d.ts +21 -12
- package/dist/feedback/feedback-swipe.d.ts +22 -13
- package/dist/feedback/feedback-swipe.js.map +1 -1
- package/dist/feedback/feedback-table.d.ts +18 -9
- package/dist/feedback/feedback-thread.d.ts +21 -12
- package/dist/feedback.d.ts +21 -12
- 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 +21 -12
- package/dist/i18n/defaults.d.ts +21 -12
- package/dist/i18n/defaults.js +28 -0
- package/dist/i18n/defaults.js.map +1 -1
- package/dist/i18n/german.d.ts +21 -12
- package/dist/i18n/german.js +298 -12
- package/dist/i18n/german.js.map +1 -1
- package/dist/i18n/kit-labels.d.ts +14 -5
- package/dist/i18n/kit-labels.js.map +1 -1
- package/dist/i18n/languages.d.ts +15 -6
- package/dist/i18n/locales/de-CH.d.ts +21 -12
- package/dist/i18n/locales/en.d.ts +21 -12
- package/dist/i18n/locales/es.d.ts +21 -12
- package/dist/i18n/locales/es.js +287 -10
- package/dist/i18n/locales/es.js.map +1 -1
- package/dist/i18n/locales/fr.d.ts +21 -12
- package/dist/i18n/locales/fr.js +290 -7
- package/dist/i18n/locales/fr.js.map +1 -1
- package/dist/i18n/locales/hu.d.ts +21 -12
- package/dist/i18n/locales/hu.js +288 -12
- package/dist/i18n/locales/hu.js.map +1 -1
- package/dist/i18n/locales/it.d.ts +21 -12
- package/dist/i18n/locales/it.js +289 -7
- package/dist/i18n/locales/it.js.map +1 -1
- package/dist/i18n/locales/zh.d.ts +21 -12
- package/dist/i18n/locales/zh.js +279 -7
- package/dist/i18n/locales/zh.js.map +1 -1
- package/dist/i18n/review.d.ts +21 -12
- package/dist/i18n/review.js +50 -1
- package/dist/i18n/review.js.map +1 -1
- package/dist/index.d.ts +31 -4
- package/dist/index.js +148 -0
- package/dist/index.js.map +1 -1
- package/dist/landing/access.d.ts +48 -0
- package/dist/landing/access.js +38 -0
- package/dist/landing/access.js.map +1 -0
- package/dist/landing/landing-actions.d.ts +138 -0
- package/dist/landing/landing-actions.js +39 -0
- package/dist/landing/landing-actions.js.map +1 -0
- package/dist/landing/landing-labels.d.ts +43 -0
- package/dist/landing/landing-labels.js +23 -0
- package/dist/landing/landing-labels.js.map +1 -0
- package/dist/landing/landing-sections.d.ts +218 -0
- package/dist/landing/landing-sections.js +167 -0
- package/dist/landing/landing-sections.js.map +1 -0
- package/dist/landing/page-seo.d.ts +99 -0
- package/dist/landing/page-seo.js +121 -0
- package/dist/landing/page-seo.js.map +1 -0
- package/dist/landing/public-header.d.ts +148 -0
- package/dist/landing/public-header.js +49 -0
- package/dist/landing/public-header.js.map +1 -0
- package/dist/landing/routing.d.ts +218 -0
- package/dist/landing/routing.js +81 -0
- package/dist/landing/routing.js.map +1 -0
- package/dist/rhf/fields.d.ts +21 -12
- package/dist/rhf/form.d.ts +21 -12
- package/dist/rhf.d.ts +21 -12
- package/dist/settings/settings-catalogue.d.ts +84 -0
- package/dist/settings/settings-catalogue.js +47 -0
- package/dist/settings/settings-catalogue.js.map +1 -0
- package/dist/settings/settings-context.d.ts +31 -0
- package/dist/settings/settings-context.js +11 -0
- package/dist/settings/settings-context.js.map +1 -0
- package/dist/settings/settings-heading.d.ts +50 -0
- package/dist/settings/settings-heading.js +27 -0
- package/dist/settings/settings-heading.js.map +1 -0
- package/dist/settings/settings-labels.d.ts +41 -0
- package/dist/settings/settings-labels.js +38 -0
- package/dist/settings/settings-labels.js.map +1 -0
- package/dist/settings/settings-layout.d.ts +77 -0
- package/dist/settings/settings-layout.js +242 -0
- package/dist/settings/settings-layout.js.map +1 -0
- package/dist/settings/settings-section.d.ts +133 -0
- package/dist/settings/settings-section.js +63 -0
- package/dist/settings/settings-section.js.map +1 -0
- package/dist/settings/use-account-language.d.ts +83 -0
- package/dist/settings/use-account-language.js +62 -0
- package/dist/settings/use-account-language.js.map +1 -0
- package/dist/settings/use-settings-focus.d.ts +34 -0
- package/dist/settings/use-settings-focus.js +77 -0
- package/dist/settings/use-settings-focus.js.map +1 -0
- package/dist/settings/use-settings-route.d.ts +118 -0
- package/dist/settings/use-settings-route.js +120 -0
- package/dist/settings/use-settings-route.js.map +1 -0
- package/dist/shell/app-shell.d.ts +27 -12
- package/dist/shell/app-shell.js +3 -2
- package/dist/shell/app-shell.js.map +1 -1
- package/dist/shell/top-bar-brand.d.ts +21 -12
- package/dist/shell.d.ts +25 -6
- package/dist/shell.js +56 -0
- package/dist/shell.js.map +1 -1
- package/dist/wizard/stepper-nav.d.ts +21 -12
- package/dist/wizard.d.ts +21 -12
- package/package.json +2 -1
- package/src/account/account-parts.ts +67 -0
- package/src/account/data-export-setting.tsx +219 -0
- package/src/account/delete-account-setting.tsx +363 -0
- package/src/account/email-change-setting.tsx +454 -0
- package/src/account/sessions-setting.tsx +284 -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 +523 -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/auth-errors.ts +196 -8
- package/src/auth/forgot-password-form.tsx +21 -5
- package/src/auth/register-form.tsx +24 -6
- package/src/auth/reset-password-form.tsx +19 -5
- package/src/auth/sign-in-form.tsx +241 -38
- package/src/auth/verify-email.tsx +59 -8
- package/src/components/account-chips.tsx +64 -7
- package/src/components/account-settings-labels.ts +28 -1
- package/src/components/account-settings.tsx +231 -34
- package/src/components/data-table.tsx +8 -0
- package/src/components/date-picker.tsx +9 -4
- package/src/components/passkeys-setting.tsx +2 -1
- package/src/components/settings-fields.tsx +50 -1
- package/src/demo/demo-banner.tsx +159 -0
- package/src/demo/demo-countdown.ts +141 -0
- package/src/demo/demo-ended.tsx +89 -0
- package/src/demo/demo-labels.ts +78 -0
- package/src/demo/demo-session.ts +32 -0
- package/src/demo/demo-start.tsx +150 -0
- package/src/feedback/feedback-inbox.tsx +21 -8
- package/src/feedback/feedback-labels.ts +2 -1
- package/src/feedback/feedback-record.ts +10 -7
- package/src/feedback/feedback-row-detail.tsx +28 -4
- package/src/feedback/feedback-swipe.tsx +1 -1
- package/src/hooks/use-anchored-panel.ts +14 -3
- package/src/i18n/defaults.ts +28 -0
- package/src/i18n/german.ts +327 -5
- package/src/i18n/kit-labels.tsx +42 -0
- package/src/i18n/locales/es.ts +313 -3
- package/src/i18n/locales/fr.ts +316 -0
- package/src/i18n/locales/hu.ts +308 -5
- package/src/i18n/locales/it.ts +313 -0
- package/src/i18n/locales/zh.ts +286 -0
- package/src/i18n/review.ts +49 -0
- package/src/index.ts +184 -0
- package/src/landing/access.ts +97 -0
- package/src/landing/landing-actions.tsx +92 -0
- package/src/landing/landing-labels.ts +59 -0
- package/src/landing/landing-sections.tsx +337 -0
- package/src/landing/page-seo.ts +251 -0
- package/src/landing/public-header.tsx +120 -0
- package/src/landing/routing.tsx +209 -0
- package/src/settings/settings-catalogue.ts +132 -0
- package/src/settings/settings-context.ts +33 -0
- package/src/settings/settings-heading.tsx +65 -0
- package/src/settings/settings-labels.ts +75 -0
- package/src/settings/settings-layout.tsx +379 -0
- package/src/settings/settings-section.tsx +108 -0
- package/src/settings/use-account-language.ts +146 -0
- package/src/settings/use-settings-focus.ts +120 -0
- package/src/settings/use-settings-route.ts +258 -0
- package/src/shell/app-shell.tsx +10 -2
- package/src/shell.ts +45 -0
|
@@ -118,25 +118,55 @@ export function RoleChip<R extends string = string>({
|
|
|
118
118
|
* - `registered` — an allowlist row whose address has an account now (Kurvenschmiede).
|
|
119
119
|
* - `unverified` — the email address is not confirmed (keksdose).
|
|
120
120
|
* - `passwordChange` — must set a new password at the next sign-in (keksdose).
|
|
121
|
+
* - `deletion` — 0.30.0: the user asked for the account to be deleted
|
|
122
|
+
* (docs/user-admin-harmonization.md §2.1, §6.4). It is deactivated at once and erased
|
|
123
|
+
* on a date (`after_days`: keksdose, Kurvenschmiede) or by an operator (kastlan). The
|
|
124
|
+
* chip says the date when there is one ({@link AccountStateChipProps.date}), and
|
|
125
|
+
* "Deletion requested" when there is none. It stands IN PLACE of `inactive`: an
|
|
126
|
+
* account marked for deletion is always deactivated, and two chips would say it twice.
|
|
121
127
|
*
|
|
122
128
|
* An account can be several at once (inactive AND unverified): render one chip per
|
|
123
129
|
* state, as keksdose's state column does.
|
|
124
130
|
*/
|
|
125
|
-
export type AccountState =
|
|
131
|
+
export type AccountState =
|
|
132
|
+
| "active"
|
|
133
|
+
| "inactive"
|
|
134
|
+
| "invited"
|
|
135
|
+
| "registered"
|
|
136
|
+
| "unverified"
|
|
137
|
+
| "passwordChange"
|
|
138
|
+
| "deletion";
|
|
126
139
|
|
|
127
|
-
|
|
140
|
+
/**
|
|
141
|
+
* The `accountState` namespace. One word per state, plus the two forms of `deletion`.
|
|
142
|
+
*
|
|
143
|
+
* The 0.30 keys are OPTIONAL, like every key added to a namespace an app annotates
|
|
144
|
+
* (`const L: AccountStateLabels = {…}`): a new required key would be a compile error in
|
|
145
|
+
* each app on its next update. Left out, they fall back to the provider's, then English.
|
|
146
|
+
*/
|
|
147
|
+
export interface AccountStateLabels extends Record<Exclude<AccountState, "deletion">, string> {
|
|
148
|
+
/** 0.30.0: an account marked for deletion with no date — kastlan's `operator` mode,
|
|
149
|
+
* where an operator erases it (§6.4). "Deletion requested". */
|
|
150
|
+
deletion?: string;
|
|
151
|
+
/** 0.30.0: an account that will be erased on `date`, already formatted in the
|
|
152
|
+
* reader's locale — the `after_days` mode. "Deletion on 6 Nov 2026". */
|
|
153
|
+
deletionOn?: (date: string) => string;
|
|
154
|
+
}
|
|
128
155
|
|
|
129
|
-
export const DEFAULT_ACCOUNT_STATE_LABELS
|
|
156
|
+
export const DEFAULT_ACCOUNT_STATE_LABELS = {
|
|
130
157
|
active: "Active",
|
|
131
158
|
inactive: "Inactive",
|
|
132
159
|
invited: "Invited",
|
|
133
160
|
registered: "Registered",
|
|
134
161
|
unverified: "Unverified",
|
|
135
162
|
passwordChange: "Must change password",
|
|
136
|
-
|
|
163
|
+
deletion: "Deletion requested",
|
|
164
|
+
deletionOn: (date: string) => `Deletion on ${date}`,
|
|
165
|
+
} satisfies Required<AccountStateLabels>;
|
|
137
166
|
|
|
138
|
-
/** The default tone of each state: green for the normal case, red for switched off
|
|
139
|
-
* amber for something the user still has to do, blue for
|
|
167
|
+
/** The default tone of each state: green for the normal case, red for switched off
|
|
168
|
+
* (and for going, `deletion`), amber for something the user still has to do, blue for
|
|
169
|
+
* an open invitation. */
|
|
140
170
|
export const ACCOUNT_STATE_TONES: Readonly<Record<AccountState, ChipTone>> = {
|
|
141
171
|
active: "success",
|
|
142
172
|
inactive: "danger",
|
|
@@ -144,10 +174,22 @@ export const ACCOUNT_STATE_TONES: Readonly<Record<AccountState, ChipTone>> = {
|
|
|
144
174
|
registered: "neutral",
|
|
145
175
|
unverified: "warning",
|
|
146
176
|
passwordChange: "warning",
|
|
177
|
+
// Red like `inactive`, which it replaces: the account cannot sign in, and it is going.
|
|
178
|
+
deletion: "danger",
|
|
147
179
|
};
|
|
148
180
|
|
|
149
181
|
export interface AccountStateChipProps extends AccountChipLook {
|
|
150
182
|
state: AccountState;
|
|
183
|
+
/**
|
|
184
|
+
* 0.30.0, `state="deletion"` only: the day the account will be erased — the row's
|
|
185
|
+
* `deletion_scheduled_at` (§3.2, §6.4). Formatted as a date in the reader's locale
|
|
186
|
+
* ("Deletion on 6 Nov 2026"), through the provider's `formatDate` when the app set
|
|
187
|
+
* one, as {@link DateMark} does. Missing or `null` — an operator erases it, so there
|
|
188
|
+
* is no date — the chip says "Deletion requested". Ignored for every other state.
|
|
189
|
+
*/
|
|
190
|
+
date?: DateInput;
|
|
191
|
+
/** Over the provider's locale, for the `deletion` date. */
|
|
192
|
+
locale?: string;
|
|
151
193
|
/** Over {@link ACCOUNT_STATE_TONES} — Kurvenschmiede draws `inactive` neutral. */
|
|
152
194
|
tone?: ChipTone;
|
|
153
195
|
/** Over the label, for an app whose word for the state is its own ("Deactivated"). */
|
|
@@ -162,6 +204,8 @@ export interface AccountStateChipProps extends AccountChipLook {
|
|
|
162
204
|
*/
|
|
163
205
|
export function AccountStateChip({
|
|
164
206
|
state,
|
|
207
|
+
date,
|
|
208
|
+
locale: localeProp,
|
|
165
209
|
tone,
|
|
166
210
|
children,
|
|
167
211
|
labels: labelsProp,
|
|
@@ -175,10 +219,23 @@ export function AccountStateChip({
|
|
|
175
219
|
// An `undefined` in the prop is skipped (the provider's merge does that), so an
|
|
176
220
|
// optional value cannot blank a default.
|
|
177
221
|
const labels = useKitLabels("accountState", DEFAULT_ACCOUNT_STATE_LABELS, labelsProp);
|
|
222
|
+
const locale = useKitLocale(localeProp);
|
|
223
|
+
const fromProvider = useKitDateFormatter();
|
|
224
|
+
const when = state === "deletion" ? toDate(date ?? null) : null;
|
|
225
|
+
let text: ReactNode;
|
|
226
|
+
if (state !== "deletion") text = labels[state];
|
|
227
|
+
else if (when) {
|
|
228
|
+
// A date in a SENTENCE, so no weekday (keksdose's rule, KitDateFormatContext).
|
|
229
|
+
const day =
|
|
230
|
+
(fromProvider && fromProvider(toLocalIso(when), { unit: "day", source: "dateMark", locale, weekday: false })) ||
|
|
231
|
+
formatDate(when, "medium", { locale });
|
|
232
|
+
text = (labels.deletionOn ?? DEFAULT_ACCOUNT_STATE_LABELS.deletionOn)(day);
|
|
233
|
+
} else text = labels.deletion ?? DEFAULT_ACCOUNT_STATE_LABELS.deletion;
|
|
178
234
|
return (
|
|
179
235
|
<Chip
|
|
180
236
|
{...rest}
|
|
181
237
|
data-state={state}
|
|
238
|
+
data-date={when ? toLocalIso(when) : undefined}
|
|
182
239
|
tone={tone ?? ACCOUNT_STATE_TONES[state]}
|
|
183
240
|
size={size}
|
|
184
241
|
variant={variant}
|
|
@@ -186,7 +243,7 @@ export function AccountStateChip({
|
|
|
186
243
|
caps={caps}
|
|
187
244
|
className={className}
|
|
188
245
|
>
|
|
189
|
-
{children ??
|
|
246
|
+
{children ?? text}
|
|
190
247
|
</Chip>
|
|
191
248
|
);
|
|
192
249
|
}
|
|
@@ -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 {
|
|
@@ -35,6 +46,12 @@ export interface PasswordSettingLabels {
|
|
|
35
46
|
}
|
|
36
47
|
|
|
37
48
|
export interface TwoFactorSettingLabels {
|
|
49
|
+
/**
|
|
50
|
+
* Before 0.31 the card's one line, "<status>: On". Since 0.31 the setting's name is the
|
|
51
|
+
* card's {@link title} and the state stands beside it, so this is read only where no
|
|
52
|
+
* `title` is given at the same level — an app that translated `status` keeps its word
|
|
53
|
+
* as the title.
|
|
54
|
+
*/
|
|
38
55
|
status: string;
|
|
39
56
|
enabledText: string;
|
|
40
57
|
disabledText: string;
|
|
@@ -59,6 +76,12 @@ export interface TwoFactorSettingLabels {
|
|
|
59
76
|
secretHint?: string;
|
|
60
77
|
/** The copy button beside that key. */
|
|
61
78
|
copySecret?: string;
|
|
79
|
+
/**
|
|
80
|
+
* 0.31.0 (docs/settings-harmonization.md §3.7): the card's title, a heading inside a
|
|
81
|
+
* `SettingsLayout`. OPTIONAL for the reason `qrAlt` is. Unset, the same source's
|
|
82
|
+
* `status` stands in for it, then English.
|
|
83
|
+
*/
|
|
84
|
+
title?: string;
|
|
62
85
|
}
|
|
63
86
|
|
|
64
87
|
export interface PasskeysSettingLabels {
|
|
@@ -104,7 +127,7 @@ export interface PasskeysSettingLabels {
|
|
|
104
127
|
}
|
|
105
128
|
|
|
106
129
|
export interface AccountSettingsLabels {
|
|
107
|
-
profile: ProfileSettingLabels
|
|
130
|
+
profile: Required<ProfileSettingLabels>;
|
|
108
131
|
password: PasswordSettingLabels;
|
|
109
132
|
// `Required` here, optional on the section interfaces: the namespace is what a full
|
|
110
133
|
// catalogue (`@eifi1/ui-kit/i18n/<code>`, `missingKitLabels`) is written against, so
|
|
@@ -121,6 +144,9 @@ export const DEFAULT_ACCOUNT_SETTINGS_LABELS: AccountSettingsLabels = {
|
|
|
121
144
|
memberSince: "Member since",
|
|
122
145
|
displayName: "Display name",
|
|
123
146
|
save: "Save",
|
|
147
|
+
firstName: "First name",
|
|
148
|
+
lastName: "Last name",
|
|
149
|
+
nameRequired: "Enter both a first and a last name.",
|
|
124
150
|
},
|
|
125
151
|
password: {
|
|
126
152
|
title: "Change password",
|
|
@@ -146,6 +172,7 @@ export const DEFAULT_ACCOUNT_SETTINGS_LABELS: AccountSettingsLabels = {
|
|
|
146
172
|
qrAlt: "QR code",
|
|
147
173
|
secretHint: "Can’t scan it? Enter this key in the app instead:",
|
|
148
174
|
copySecret: "Copy key",
|
|
175
|
+
title: "Two-factor authentication",
|
|
149
176
|
},
|
|
150
177
|
passkeys: {
|
|
151
178
|
title: "Passkeys",
|
|
@@ -1,18 +1,22 @@
|
|
|
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,
|
|
11
13
|
PasswordStrengthMeter,
|
|
12
14
|
} from "./password-strength";
|
|
13
15
|
import type { PasswordStrengthMeterProps } from "./password-strength";
|
|
14
|
-
import {
|
|
16
|
+
import { useKitLabelOverrides, useKitLabels } from "../i18n/kit-labels";
|
|
17
|
+
import { Chip } from "./chip";
|
|
15
18
|
import { useAccountSettingsLabels } from "./account-settings-labels";
|
|
19
|
+
import { SettingsCardTitle } from "../settings/settings-heading";
|
|
16
20
|
import type {
|
|
17
21
|
PasswordSettingLabels,
|
|
18
22
|
ProfileSettingLabels,
|
|
@@ -40,40 +44,96 @@ export { DEFAULT_ACCOUNT_SETTINGS_LABELS } from "./account-settings-labels";
|
|
|
40
44
|
* `accountSettings` namespace, then English (see account-settings-labels.ts).
|
|
41
45
|
*/
|
|
42
46
|
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
}: {
|
|
47
|
+
/** What {@link ProfileSetting} hands `onSave` in its two-field form (0.30.0): both
|
|
48
|
+
* trimmed, 1–120 characters each — the `PATCH /auth/me` body's `first_name` /
|
|
49
|
+
* `last_name` (docs/user-admin-harmonization.md §6.1). */
|
|
50
|
+
export interface ProfileNameValues {
|
|
51
|
+
firstName: string;
|
|
52
|
+
lastName: string;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/** What both forms of {@link ProfileSetting} take. */
|
|
56
|
+
interface ProfileSettingCommonProps {
|
|
54
57
|
name?: string | null;
|
|
55
58
|
email?: string | null;
|
|
56
59
|
role?: ReactNode;
|
|
57
60
|
memberSince?: ReactNode;
|
|
58
|
-
value: string;
|
|
59
|
-
onChange: (value: string) => void;
|
|
60
|
-
onSave: () => void;
|
|
61
61
|
saving?: boolean;
|
|
62
62
|
/** Prop > `<UiKitProvider labels={{ accountSettings: { profile } }}>` > English. */
|
|
63
63
|
labels?: Partial<ProfileSettingLabels>;
|
|
64
|
-
|
|
64
|
+
/** The card's DOM id — a settings catalogue `anchor`, so `?focus=` can ring it (0.31). */
|
|
65
|
+
id?: string;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* The single display name, as before 0.30.0 — controlled through `value` / `onChange`,
|
|
70
|
+
* saved by `onSave()`. Kept unchanged for an app that has not moved to first and last
|
|
71
|
+
* name yet.
|
|
72
|
+
*/
|
|
73
|
+
export interface ProfileSettingDisplayNameProps extends ProfileSettingCommonProps {
|
|
74
|
+
value: string;
|
|
75
|
+
onChange: (value: string) => void;
|
|
76
|
+
onSave: () => void;
|
|
77
|
+
firstName?: undefined;
|
|
78
|
+
lastName?: undefined;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* First and last name (0.30.0, docs/user-admin-harmonization.md §6.1): the saved
|
|
83
|
+
* values in, the edited ones out through `onSave`. The card keeps the drafts itself.
|
|
84
|
+
*/
|
|
85
|
+
export interface ProfileSettingNamesProps extends ProfileSettingCommonProps {
|
|
86
|
+
/** The saved given name — `first_name`. `null` reads as empty. */
|
|
87
|
+
firstName: string | null;
|
|
88
|
+
/** The saved family name — `last_name`. `null` (a migrated account, §3.3) reads as
|
|
89
|
+
* empty, and Save then waits for one. */
|
|
90
|
+
lastName: string | null;
|
|
91
|
+
/**
|
|
92
|
+
* Save the two names (`PATCH /auth/me`, which also clears `name_incomplete`). Return
|
|
93
|
+
* a promise and Save is busy until it settles. Refresh `firstName` / `lastName` from
|
|
94
|
+
* the answer: the card compares its drafts with them, so Save turns off once the new
|
|
95
|
+
* names come back. A rejection keeps what was typed — the app says why (a toast), as
|
|
96
|
+
* for `PasswordSetting`.
|
|
97
|
+
*/
|
|
98
|
+
onSave: (values: ProfileNameValues) => void | Promise<unknown>;
|
|
99
|
+
value?: undefined;
|
|
100
|
+
onChange?: undefined;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
export type ProfileSettingProps = ProfileSettingDisplayNameProps | ProfileSettingNamesProps;
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* The profile section: avatar, address, role, member since, and the name.
|
|
107
|
+
*
|
|
108
|
+
* **Two forms.** Given `firstName` / `lastName` (0.30.0, §6.1) it edits first and last
|
|
109
|
+
* name in two fields, side by side where the card is wide enough — the order and the
|
|
110
|
+
* 1–120-character rule of `RegisterForm` (auth §3.1), so the name a person signed up
|
|
111
|
+
* with is edited the way it was typed. Without them it is the single `displayName`
|
|
112
|
+
* field it always was, `value` / `onChange` / `onSave()` unchanged, for an app that has
|
|
113
|
+
* not moved yet (kastlan split the one name at its first space on save; Kurvenschmiede
|
|
114
|
+
* built its own card instead).
|
|
115
|
+
*
|
|
116
|
+
* The two-field form owns its drafts, starting from the saved names and following them
|
|
117
|
+
* when they change; Save waits for a change, holds — saying why — while a name is
|
|
118
|
+
* blank, and is a `commit` (a {@link WriteLockProvider} locks it, keksdose's demo).
|
|
119
|
+
*/
|
|
120
|
+
export function ProfileSetting(props: ProfileSettingProps) {
|
|
121
|
+
const { name, email, role, memberSince, labels: labelsProp, id } = props;
|
|
65
122
|
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();
|
|
123
|
+
const names = props.firstName !== undefined || props.lastName !== undefined;
|
|
71
124
|
return (
|
|
72
|
-
<Card className="p-4 space-y-3">
|
|
125
|
+
<Card id={id} className="p-4 space-y-3">
|
|
73
126
|
<div className="flex items-center gap-3">
|
|
74
|
-
<UserAvatar
|
|
127
|
+
<UserAvatar
|
|
128
|
+
name={name}
|
|
129
|
+
email={email}
|
|
130
|
+
person={names ? { first: props.firstName, last: props.lastName } : undefined}
|
|
131
|
+
size="lg"
|
|
132
|
+
/>
|
|
75
133
|
<div className="min-w-0">
|
|
76
|
-
|
|
134
|
+
{/* A heading inside a SettingsLayout (0.31, docs/settings-harmonization.md
|
|
135
|
+
§3.7), the plain div it always was outside one. */}
|
|
136
|
+
<SettingsCardTitle>{labels.title}</SettingsCardTitle>
|
|
77
137
|
<div className="truncate font-mono text-xs text-[var(--text-muted)]">
|
|
78
138
|
{/* `email` was a required key that nothing rendered; it names the address
|
|
79
139
|
for a screen reader, which otherwise hears a bare string under a title. */}
|
|
@@ -88,6 +148,51 @@ export function ProfileSetting({
|
|
|
88
148
|
<span className="text-[var(--text-muted)]">{labels.memberSince}</span>
|
|
89
149
|
<span>{memberSince ?? "—"}</span>
|
|
90
150
|
</div>
|
|
151
|
+
{names ? (
|
|
152
|
+
<PersonNameFields
|
|
153
|
+
firstName={props.firstName ?? ""}
|
|
154
|
+
lastName={props.lastName ?? ""}
|
|
155
|
+
onSave={(props as ProfileSettingNamesProps).onSave}
|
|
156
|
+
saving={props.saving}
|
|
157
|
+
labels={labels}
|
|
158
|
+
/>
|
|
159
|
+
) : (
|
|
160
|
+
<DisplayNameField
|
|
161
|
+
name={name}
|
|
162
|
+
value={(props as ProfileSettingDisplayNameProps).value}
|
|
163
|
+
onChange={(props as ProfileSettingDisplayNameProps).onChange}
|
|
164
|
+
onSave={(props as ProfileSettingDisplayNameProps).onSave}
|
|
165
|
+
saving={props.saving}
|
|
166
|
+
labels={labels}
|
|
167
|
+
/>
|
|
168
|
+
)}
|
|
169
|
+
</Card>
|
|
170
|
+
);
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
/** The single-name form — exactly the 0.29 card's field and button. */
|
|
174
|
+
function DisplayNameField({
|
|
175
|
+
name,
|
|
176
|
+
value,
|
|
177
|
+
onChange,
|
|
178
|
+
onSave,
|
|
179
|
+
saving,
|
|
180
|
+
labels,
|
|
181
|
+
}: {
|
|
182
|
+
name?: string | null;
|
|
183
|
+
value: string;
|
|
184
|
+
onChange: (value: string) => void;
|
|
185
|
+
onSave: () => void;
|
|
186
|
+
saving?: boolean;
|
|
187
|
+
labels: Required<ProfileSettingLabels>;
|
|
188
|
+
}) {
|
|
189
|
+
const dirty = value.trim().length > 0 && value.trim() !== (name ?? "");
|
|
190
|
+
// Generated, not the literal "display-name" it was: two of these on one page (an
|
|
191
|
+
// admin editing someone else's profile beside their own) shared an id, and the
|
|
192
|
+
// second label pointed at the first field.
|
|
193
|
+
const inputId = useId();
|
|
194
|
+
return (
|
|
195
|
+
<>
|
|
91
196
|
<Input
|
|
92
197
|
id={inputId}
|
|
93
198
|
label={labels.displayName}
|
|
@@ -98,7 +203,84 @@ export function ProfileSetting({
|
|
|
98
203
|
<Button onClick={onSave} disabled={!dirty || saving}>
|
|
99
204
|
{labels.save}
|
|
100
205
|
</Button>
|
|
101
|
-
|
|
206
|
+
</>
|
|
207
|
+
);
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
/** The first-and-last form (0.30.0, §6.1). */
|
|
211
|
+
function PersonNameFields({
|
|
212
|
+
firstName,
|
|
213
|
+
lastName,
|
|
214
|
+
onSave,
|
|
215
|
+
saving,
|
|
216
|
+
labels,
|
|
217
|
+
}: {
|
|
218
|
+
firstName: string;
|
|
219
|
+
lastName: string;
|
|
220
|
+
onSave: (values: ProfileNameValues) => void | Promise<unknown>;
|
|
221
|
+
saving?: boolean;
|
|
222
|
+
labels: Required<ProfileSettingLabels>;
|
|
223
|
+
}) {
|
|
224
|
+
const [first, setFirst] = useState(firstName);
|
|
225
|
+
const [last, setLast] = useState(lastName);
|
|
226
|
+
// The saved names moved (the save came back, another tab renamed): the drafts follow
|
|
227
|
+
// them. During render, as DangerConfirm resets its fields, so no frame shows the old
|
|
228
|
+
// draft beside the new name.
|
|
229
|
+
const [saved, setSaved] = useState({ firstName, lastName });
|
|
230
|
+
if (saved.firstName !== firstName || saved.lastName !== lastName) {
|
|
231
|
+
setSaved({ firstName, lastName });
|
|
232
|
+
setFirst(firstName);
|
|
233
|
+
setLast(lastName);
|
|
234
|
+
}
|
|
235
|
+
const { pending, run } = usePromisePending();
|
|
236
|
+
const busy = Boolean(saving) || pending;
|
|
237
|
+
const valid = personNameOk(first) && personNameOk(last);
|
|
238
|
+
const dirty = first.trim() !== firstName.trim() || last.trim() !== lastName.trim();
|
|
239
|
+
|
|
240
|
+
const submit = (e: FormEvent) => {
|
|
241
|
+
e.preventDefault();
|
|
242
|
+
if (!valid || !dirty || busy) return;
|
|
243
|
+
// A rejection keeps the drafts; the app reports it, as PasswordSetting's does.
|
|
244
|
+
run(onSave({ firstName: first.trim(), lastName: last.trim() }), () => {});
|
|
245
|
+
};
|
|
246
|
+
|
|
247
|
+
return (
|
|
248
|
+
<form className="@container space-y-3" onSubmit={submit} noValidate>
|
|
249
|
+
{/* The order and the rule of RegisterForm's two fields (auth §3.1). */}
|
|
250
|
+
<div className="grid gap-3 @xs:grid-cols-2">
|
|
251
|
+
<Input
|
|
252
|
+
name="given-name"
|
|
253
|
+
autoComplete="given-name"
|
|
254
|
+
label={labels.firstName}
|
|
255
|
+
value={first}
|
|
256
|
+
onChange={(e) => setFirst(e.target.value)}
|
|
257
|
+
maxLength={PERSON_NAME_MAX_LENGTH}
|
|
258
|
+
readOnly={busy}
|
|
259
|
+
required
|
|
260
|
+
/>
|
|
261
|
+
<Input
|
|
262
|
+
name="family-name"
|
|
263
|
+
autoComplete="family-name"
|
|
264
|
+
label={labels.lastName}
|
|
265
|
+
value={last}
|
|
266
|
+
onChange={(e) => setLast(e.target.value)}
|
|
267
|
+
maxLength={PERSON_NAME_MAX_LENGTH}
|
|
268
|
+
readOnly={busy}
|
|
269
|
+
required
|
|
270
|
+
/>
|
|
271
|
+
</div>
|
|
272
|
+
<Button
|
|
273
|
+
type="submit"
|
|
274
|
+
commit
|
|
275
|
+
pending={busy}
|
|
276
|
+
// A blank name is a reason the person can act on; an unchanged one is not —
|
|
277
|
+
// there is nothing to save, and the button is simply off.
|
|
278
|
+
disabled={valid && !dirty}
|
|
279
|
+
disabledReason={valid ? undefined : labels.nameRequired}
|
|
280
|
+
>
|
|
281
|
+
{labels.save}
|
|
282
|
+
</Button>
|
|
283
|
+
</form>
|
|
102
284
|
);
|
|
103
285
|
}
|
|
104
286
|
|
|
@@ -117,6 +299,7 @@ export function PasswordSetting({
|
|
|
117
299
|
strength,
|
|
118
300
|
maxBytes,
|
|
119
301
|
labels: labelsProp,
|
|
302
|
+
id,
|
|
120
303
|
}: {
|
|
121
304
|
/** Called with the validated (current, new) pair; return a promise to auto-clear on success. */
|
|
122
305
|
onSubmit: (currentPassword: string, newPassword: string) => void | Promise<unknown>;
|
|
@@ -147,6 +330,8 @@ export function PasswordSetting({
|
|
|
147
330
|
maxBytes?: number;
|
|
148
331
|
/** Prop > `<UiKitProvider labels={{ accountSettings: { password } }}>` > English. */
|
|
149
332
|
labels?: Partial<PasswordSettingLabels>;
|
|
333
|
+
/** The card's DOM id — a settings catalogue `anchor`, so `?focus=` can ring it (0.31). */
|
|
334
|
+
id?: string;
|
|
150
335
|
}) {
|
|
151
336
|
const labels = useAccountSettingsLabels("password", labelsProp);
|
|
152
337
|
const strengthOptions = typeof strength === "object" ? strength : undefined;
|
|
@@ -189,8 +374,8 @@ export function PasswordSetting({
|
|
|
189
374
|
);
|
|
190
375
|
|
|
191
376
|
return (
|
|
192
|
-
<Card className="p-4 space-y-3">
|
|
193
|
-
<
|
|
377
|
+
<Card id={id} className="p-4 space-y-3">
|
|
378
|
+
<SettingsCardTitle>{labels.title}</SettingsCardTitle>
|
|
194
379
|
<Input type="password" autoComplete="current-password" label={labels.current} value={current} onChange={(e) => setCurrent(e.target.value)} />
|
|
195
380
|
{strength ? (
|
|
196
381
|
// One box for the field and its meter, so the card's `space-y-3` spaces the pair
|
|
@@ -257,6 +442,7 @@ export function TwoFactorSetting({
|
|
|
257
442
|
busy,
|
|
258
443
|
renderQr,
|
|
259
444
|
labels: labelsProp,
|
|
445
|
+
id,
|
|
260
446
|
}: {
|
|
261
447
|
enabled: boolean;
|
|
262
448
|
/** What the app's setup call returned; null before setup starts. */
|
|
@@ -270,8 +456,16 @@ export function TwoFactorSetting({
|
|
|
270
456
|
renderQr?: (otpauthUri: string) => ReactNode;
|
|
271
457
|
/** Prop > `<UiKitProvider labels={{ accountSettings: { twoFactor } }}>` > English. */
|
|
272
458
|
labels?: Partial<TwoFactorSettingLabels>;
|
|
459
|
+
/** The card's DOM id — a settings catalogue `anchor`, so `?focus=` can ring it (0.31). */
|
|
460
|
+
id?: string;
|
|
273
461
|
}) {
|
|
274
462
|
const labels = useAccountSettingsLabels("twoFactor", labelsProp);
|
|
463
|
+
// The title (0.31, docs/settings-harmonization.md §3.7) is new, and an app that
|
|
464
|
+
// translated this card before it named the setting in `status` — "2FA",
|
|
465
|
+
// "Zwei-Faktor-Authentifizierung". Each source's `title` first, then its `status`, so
|
|
466
|
+
// that app keeps its own word instead of meeting the English title.
|
|
467
|
+
const overrides = useKitLabelOverrides("accountSettings")?.twoFactor;
|
|
468
|
+
const title = labelsProp?.title ?? overrides?.title ?? labelsProp?.status ?? overrides?.status ?? labels.title;
|
|
275
469
|
const [code, setCode] = useState("");
|
|
276
470
|
const [password, setPassword] = useState("");
|
|
277
471
|
// Both code fields are a OneTimeCodeInput (0.22.0) — the field this card wrote by hand
|
|
@@ -285,17 +479,20 @@ export function TwoFactorSetting({
|
|
|
285
479
|
// pasted from an authenticator that groups its digits used to reach `onEnable`
|
|
286
480
|
// with the space in it.
|
|
287
481
|
// - one `code` state for both, as before.
|
|
288
|
-
// Only for the "Status: Enabled" composition — the punctuation between a field's
|
|
289
|
-
// name and its value is the language's (see `CommonLabels.fieldValue`).
|
|
290
|
-
const common = useKitLabels("common", DEFAULT_COMMON_LABELS);
|
|
291
482
|
const secret = setup
|
|
292
483
|
? (setup.secret ?? ("otpauthUri" in setup ? otpauthSecret(setup.otpauthUri) : null))
|
|
293
484
|
: null;
|
|
294
485
|
|
|
295
486
|
return (
|
|
296
|
-
<Card className="p-4 space-y-3">
|
|
297
|
-
|
|
298
|
-
|
|
487
|
+
<Card id={id} className="p-4 space-y-3">
|
|
488
|
+
{/* The card had only "Two-factor authentication: Off". A settings page outlines its
|
|
489
|
+
cards by their titles (§3.7), so the setting's name is the title now, and the
|
|
490
|
+
state stands beside it as a chip instead of repeating the name. */}
|
|
491
|
+
<div className="flex flex-wrap items-center justify-between gap-2">
|
|
492
|
+
<SettingsCardTitle>{title}</SettingsCardTitle>
|
|
493
|
+
<Chip size="sm" tone={enabled ? "success" : "neutral"}>
|
|
494
|
+
{enabled ? labels.enabledText : labels.disabledText}
|
|
495
|
+
</Chip>
|
|
299
496
|
</div>
|
|
300
497
|
|
|
301
498
|
{!enabled && !setup && (
|
|
@@ -1725,6 +1725,14 @@ export function DataTable<T>({
|
|
|
1725
1725
|
// flip stays instant.
|
|
1726
1726
|
interactive &&
|
|
1727
1727
|
"cursor-pointer transition-transform duration-100 active:scale-[0.995] active:bg-[var(--bg-active)] motion-reduce:transition-none motion-reduce:active:scale-100",
|
|
1728
|
+
// The keyboard's frame drawn INSIDE the card, as the desktop row's is (keksdose live
|
|
1729
|
+
// #394): the browser's own ring and tap highlight sit on the card's outer edge, and
|
|
1730
|
+
// three ancestors clip it — SwipeableRow's `overflow-hidden`, the card list, and
|
|
1731
|
+
// AppShell main's `overflow-x-clip` — so the frame went missing on one side. A press
|
|
1732
|
+
// keeps the fill and the squeeze above; an outline on every tap of a long list would
|
|
1733
|
+
// be louder than the acknowledgement it replaces.
|
|
1734
|
+
interactive &&
|
|
1735
|
+
"[-webkit-tap-highlight-color:transparent] focus-visible:outline-2 focus-visible:-outline-offset-2 focus-visible:outline-[var(--brand)]",
|
|
1728
1736
|
);
|
|
1729
1737
|
// The card's content is written once and worn by either tag below. Note that
|
|
1730
1738
|
// the primary cell is rendered RAW here, never through `linkColumn` — the card
|
|
@@ -588,9 +588,6 @@ function StepButton({
|
|
|
588
588
|
// px-3 would make it wider than it needs to be. twMerge lets these win.
|
|
589
589
|
"relative flex h-full w-10 shrink-0 items-center justify-center px-0 text-[var(--text-muted)]",
|
|
590
590
|
"hover:bg-[var(--bg-hover)] hover:text-[var(--text-secondary)]",
|
|
591
|
-
// Raised while hovered or focused, so the shared border and the focus ring are
|
|
592
|
-
// drawn on top of the neighbour they overlap by a pixel.
|
|
593
|
-
"hover:z-10 focus-visible:z-10",
|
|
594
591
|
// Dimmed ICON, not a dimmed box: at 40% opacity the whole button — border
|
|
595
592
|
// included — faded out of the group and read as a broken, detached control.
|
|
596
593
|
"disabled:cursor-not-allowed disabled:text-[var(--text-placeholder)] disabled:opacity-60 disabled:hover:bg-[var(--bg-surface)]",
|
|
@@ -1164,7 +1161,15 @@ export function DatePicker(props: DatePickerProps) {
|
|
|
1164
1161
|
"flex items-stretch [&>*:not(:first-child)]:-ms-px",
|
|
1165
1162
|
step && "[&_[role=combobox]]:rounded-s-none",
|
|
1166
1163
|
(step || today) && "[&_[role=combobox]]:rounded-e-none",
|
|
1167
|
-
|
|
1164
|
+
// The PART that is focused, pressed or invalid is raised over the neighbour it
|
|
1165
|
+
// overlaps by a pixel, so its blue (or red) border shows on all four sides
|
|
1166
|
+
// (keksdose live #394). `:focus-within`, not `:focus-visible`: a tap focuses
|
|
1167
|
+
// without it, as does the sheet handing focus back, and the next part — later in
|
|
1168
|
+
// the DOM — painted its grey left edge over the tapped one's right. Raising the
|
|
1169
|
+
// whole part (each is `relative`) keeps the floating label and the icon above
|
|
1170
|
+
// the trigger, which raising the trigger alone covered. Hover raises nothing: it
|
|
1171
|
+
// changes no border, and would let a hovered › cover a focused field's edge.
|
|
1172
|
+
"[&>*:focus-within]:z-20 [&>*:active]:z-20 [&>*:has([aria-invalid=true])]:z-10",
|
|
1168
1173
|
className,
|
|
1169
1174
|
)}
|
|
1170
1175
|
>
|
|
@@ -6,6 +6,7 @@ import { useKitLocale } from "../i18n/kit-labels";
|
|
|
6
6
|
import { Button, Card, EmptyState, Input } from "./ui";
|
|
7
7
|
import { Skeleton } from "./skeleton";
|
|
8
8
|
import { useAccountSettingsLabels } from "./account-settings-labels";
|
|
9
|
+
import { SettingsCardTitle } from "../settings/settings-heading";
|
|
9
10
|
import type { PasskeysSettingLabels } from "./account-settings-labels";
|
|
10
11
|
|
|
11
12
|
/** What a passkey's `id` may be: the app's own key type, string or numeric. */
|
|
@@ -243,7 +244,7 @@ export function PasskeysSetting<Id extends PasskeyId = PasskeyId>({
|
|
|
243
244
|
// `rest` is only `id` and `data-*`: everything else the props name is destructured.
|
|
244
245
|
<Card {...rest} className={cn("p-4 space-y-3", className)}>
|
|
245
246
|
<div>
|
|
246
|
-
<
|
|
247
|
+
<SettingsCardTitle>{labels.title}</SettingsCardTitle>
|
|
247
248
|
<div className="text-xs text-[var(--text-muted)]">
|
|
248
249
|
{mode === "alongside"
|
|
249
250
|
? labels.descriptionAlongside
|