@eifi1/ui-kit 0.30.0 → 0.31.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +42 -5
- package/dist/account/data-export-setting.js +3 -2
- package/dist/account/data-export-setting.js.map +1 -1
- package/dist/account/delete-account-setting.d.ts +5 -1
- package/dist/account/delete-account-setting.js +3 -2
- package/dist/account/delete-account-setting.js.map +1 -1
- package/dist/account/email-change-setting.js +4 -3
- package/dist/account/email-change-setting.js.map +1 -1
- package/dist/account/sessions-setting.js +3 -2
- package/dist/account/sessions-setting.js.map +1 -1
- package/dist/admin/admin-action-confirm.d.ts +5 -1
- package/dist/admin/admin-parts.d.ts +8 -1
- package/dist/admin/admin-parts.js +1 -1
- package/dist/admin/admin-parts.js.map +1 -1
- package/dist/admin/invitations-panel.d.ts +5 -1
- package/dist/admin/invitations-panel.js +3 -2
- package/dist/admin/invitations-panel.js.map +1 -1
- package/dist/admin/reviewer-scope-editor.d.ts +5 -1
- package/dist/admin/role-select.d.ts +5 -1
- package/dist/admin/transfer-ownership-dialog.d.ts +5 -1
- package/dist/admin/user-roster.d.ts +5 -1
- package/dist/auth/auth-errors.d.ts +24 -6
- package/dist/auth/auth-errors.js +7 -1
- package/dist/auth/auth-errors.js.map +1 -1
- package/dist/auth/register-form.d.ts +5 -1
- package/dist/auth/reset-password-form.d.ts +1 -1
- package/dist/auth/reset-password-form.js +2 -1
- package/dist/auth/reset-password-form.js.map +1 -1
- package/dist/auth/sign-in-form.d.ts +15 -1
- package/dist/auth/sign-in-form.js +12 -3
- package/dist/auth/sign-in-form.js.map +1 -1
- package/dist/auth/verify-email.d.ts +4 -0
- package/dist/auth/verify-email.js +3 -2
- package/dist/auth/verify-email.js.map +1 -1
- package/dist/chart.d.ts +5 -1
- package/dist/components/account-settings-labels.d.ts +12 -0
- package/dist/components/account-settings-labels.js +2 -1
- package/dist/components/account-settings-labels.js.map +1 -1
- package/dist/components/account-settings.d.ts +8 -2
- package/dist/components/account-settings.js +19 -11
- package/dist/components/account-settings.js.map +1 -1
- package/dist/components/amount-input.d.ts +5 -1
- package/dist/components/button-group.d.ts +5 -1
- package/dist/components/calculator.d.ts +5 -1
- package/dist/components/confirm-dialog.d.ts +5 -1
- package/dist/components/confirm-dialog.js +7 -1
- package/dist/components/confirm-dialog.js.map +1 -1
- package/dist/components/copy-button.d.ts +5 -1
- package/dist/components/danger-confirm.d.ts +5 -1
- 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 +5 -1
- package/dist/components/file-button.d.ts +5 -1
- package/dist/components/file-dropzone.d.ts +5 -1
- package/dist/components/form-actions.d.ts +5 -1
- package/dist/components/iban-input.d.ts +5 -1
- package/dist/components/language-select.d.ts +5 -1
- package/dist/components/legal-page.d.ts +5 -1
- package/dist/components/legal.d.ts +5 -1
- package/dist/components/money-field.d.ts +5 -1
- package/dist/components/number-field.d.ts +5 -1
- package/dist/components/number-input.d.ts +5 -1
- package/dist/components/numpad-sheet.d.ts +5 -1
- package/dist/components/passkeys-setting.js +2 -1
- package/dist/components/passkeys-setting.js.map +1 -1
- package/dist/components/phone-input.d.ts +5 -1
- package/dist/components/series-chart.d.ts +5 -1
- package/dist/components/settings-fields.d.ts +40 -3
- package/dist/components/settings-fields.js +20 -1
- package/dist/components/settings-fields.js.map +1 -1
- package/dist/components/share-card.d.ts +5 -1
- package/dist/components/text-link.d.ts +5 -1
- package/dist/components/time-input.d.ts +5 -1
- package/dist/components/ui.d.ts +5 -1
- package/dist/{delete-account-setting-CGVMckMw.d.ts → delete-account-setting-CpIhzxxZ.d.ts} +60 -23
- 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 +5 -1
- package/dist/feedback/feedback-context.d.ts +5 -1
- package/dist/feedback/feedback-crash.d.ts +5 -1
- package/dist/feedback/feedback-dialog.d.ts +5 -1
- package/dist/feedback/feedback-inbox.d.ts +5 -1
- package/dist/feedback/feedback-inbox.js +12 -2
- package/dist/feedback/feedback-inbox.js.map +1 -1
- package/dist/feedback/feedback-labels.d.ts +5 -1
- package/dist/feedback/feedback-labels.js +1 -0
- package/dist/feedback/feedback-labels.js.map +1 -1
- package/dist/feedback/feedback-menu.d.ts +5 -1
- package/dist/feedback/feedback-record.d.ts +5 -1
- 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 +5 -1
- 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 +5 -1
- package/dist/feedback/feedback-submit.d.ts +5 -1
- package/dist/feedback/feedback-swipe.d.ts +6 -2
- package/dist/feedback/feedback-swipe.js.map +1 -1
- package/dist/feedback/feedback-table.d.ts +5 -1
- package/dist/feedback/feedback-thread.d.ts +5 -1
- package/dist/feedback.d.ts +5 -1
- package/dist/hooks/use-file-drop.d.ts +5 -1
- package/dist/i18n/defaults.d.ts +5 -1
- package/dist/i18n/defaults.js +6 -0
- package/dist/i18n/defaults.js.map +1 -1
- package/dist/i18n/german.d.ts +5 -1
- package/dist/i18n/german.js +85 -2
- package/dist/i18n/german.js.map +1 -1
- package/dist/i18n/kit-labels.d.ts +5 -1
- package/dist/i18n/kit-labels.js.map +1 -1
- package/dist/i18n/languages.d.ts +5 -1
- package/dist/i18n/locales/de-CH.d.ts +5 -1
- package/dist/i18n/locales/en.d.ts +5 -1
- package/dist/i18n/locales/es.d.ts +5 -1
- package/dist/i18n/locales/es.js +82 -2
- package/dist/i18n/locales/es.js.map +1 -1
- package/dist/i18n/locales/fr.d.ts +5 -1
- package/dist/i18n/locales/fr.js +83 -2
- package/dist/i18n/locales/fr.js.map +1 -1
- package/dist/i18n/locales/hu.d.ts +5 -1
- package/dist/i18n/locales/hu.js +83 -2
- package/dist/i18n/locales/hu.js.map +1 -1
- package/dist/i18n/locales/it.d.ts +5 -1
- package/dist/i18n/locales/it.js +82 -2
- package/dist/i18n/locales/it.js.map +1 -1
- package/dist/i18n/locales/zh.d.ts +5 -1
- package/dist/i18n/locales/zh.js +81 -2
- package/dist/i18n/locales/zh.js.map +1 -1
- package/dist/i18n/review.d.ts +5 -1
- package/dist/i18n/review.js +15 -1
- package/dist/i18n/review.js.map +1 -1
- package/dist/index.d.ts +25 -3
- package/dist/index.js +96 -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 +140 -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 +231 -0
- package/dist/landing/routing.js +84 -0
- package/dist/landing/routing.js.map +1 -0
- package/dist/rhf/fields.d.ts +5 -1
- package/dist/rhf/form.d.ts +5 -1
- package/dist/rhf.d.ts +5 -1
- 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 +48 -0
- package/dist/settings/settings-labels.js +43 -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 +11 -1
- 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 +5 -1
- package/dist/shell.d.ts +15 -1
- package/dist/shell.js +56 -0
- package/dist/shell.js.map +1 -1
- package/dist/wizard/stepper-nav.d.ts +5 -1
- package/dist/wizard.d.ts +5 -1
- package/package.json +2 -1
- package/src/account/data-export-setting.tsx +3 -2
- package/src/account/delete-account-setting.tsx +3 -2
- package/src/account/email-change-setting.tsx +4 -7
- package/src/account/sessions-setting.tsx +3 -2
- package/src/admin/admin-parts.ts +9 -2
- package/src/admin/invitations-panel.tsx +11 -4
- package/src/auth/auth-errors.ts +36 -5
- package/src/auth/reset-password-form.tsx +6 -2
- package/src/auth/sign-in-form.tsx +34 -3
- package/src/auth/verify-email.tsx +14 -2
- package/src/components/account-settings-labels.ts +13 -0
- package/src/components/account-settings.tsx +33 -12
- package/src/components/confirm-dialog.tsx +20 -1
- 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/i18n/defaults.ts +6 -0
- package/src/i18n/german.ts +87 -0
- package/src/i18n/kit-labels.tsx +9 -0
- package/src/i18n/locales/es.ts +84 -0
- package/src/i18n/locales/fr.ts +85 -0
- package/src/i18n/locales/hu.ts +85 -0
- package/src/i18n/locales/it.ts +84 -0
- package/src/i18n/locales/zh.ts +82 -0
- package/src/i18n/review.ts +14 -0
- package/src/index.ts +98 -1
- package/src/landing/access.ts +97 -0
- package/src/landing/landing-actions.tsx +96 -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 +226 -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 +87 -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 +51 -0
|
@@ -12,7 +12,8 @@ import { hasMessage } from "../components/choice-parts";
|
|
|
12
12
|
import { DangerConfirm } from "../components/danger-confirm";
|
|
13
13
|
import { Skeleton } from "../components/skeleton";
|
|
14
14
|
import { Button, Card } from "../components/ui";
|
|
15
|
-
import { CARD_DESCRIPTION_CLASS,
|
|
15
|
+
import { CARD_DESCRIPTION_CLASS, settle, useMounted } from "./account-parts";
|
|
16
|
+
import { SettingsCardTitle } from "../settings/settings-heading";
|
|
16
17
|
|
|
17
18
|
/* ── Labels ──────────────────────────────────────────────────────────────── */
|
|
18
19
|
|
|
@@ -206,7 +207,7 @@ export function SessionsSetting<Id extends SessionId = SessionId>({
|
|
|
206
207
|
return (
|
|
207
208
|
<Card {...rest} className={cn("p-4 space-y-3", className)}>
|
|
208
209
|
<div>
|
|
209
|
-
<
|
|
210
|
+
<SettingsCardTitle>{labels.title}</SettingsCardTitle>
|
|
210
211
|
<div className={CARD_DESCRIPTION_CLASS}>{labels.description}</div>
|
|
211
212
|
</div>
|
|
212
213
|
{showList &&
|
package/src/admin/admin-parts.ts
CHANGED
|
@@ -16,11 +16,18 @@ import { formatPersonName } from "../lib/person-name";
|
|
|
16
16
|
export type MaybePromise<T = unknown> = void | Promise<T>;
|
|
17
17
|
|
|
18
18
|
/** A person as the admin parts show one: the two name parts of the auth contract
|
|
19
|
-
* (§3.2), an older whole `name` as the fallback, and the address.
|
|
19
|
+
* (§3.2), an older whole `name` as the fallback, and the address. The name parts are
|
|
20
|
+
* read in either spelling — the kit's `first` / `last`, or server-kit's `PersonRef`
|
|
21
|
+
* as it serialises (`first_name` / `last_name`), so an app hands the server's object
|
|
22
|
+
* over as it came (keksdose's and kastlan's 0.30 adoptions mapped one to the other). */
|
|
20
23
|
export interface AdminPerson {
|
|
21
24
|
id?: string | number | null;
|
|
22
25
|
first?: string | null;
|
|
23
26
|
last?: string | null;
|
|
27
|
+
/** server-kit `PersonRef.first_name`; `first` wins when both are given. */
|
|
28
|
+
first_name?: string | null;
|
|
29
|
+
/** server-kit `PersonRef.last_name`; `last` wins when both are given. */
|
|
30
|
+
last_name?: string | null;
|
|
24
31
|
/** A whole name, for an API that still sends `display_name` only. */
|
|
25
32
|
name?: string | null;
|
|
26
33
|
email?: string | null;
|
|
@@ -39,7 +46,7 @@ export function hasMessage(node: ReactNode): boolean {
|
|
|
39
46
|
export function personLabel(person: AdminPerson | null | undefined, locale: string | undefined): string {
|
|
40
47
|
if (!person) return "";
|
|
41
48
|
return (
|
|
42
|
-
formatPersonName({ first: person.first, last: person.last }, locale) ||
|
|
49
|
+
formatPersonName({ first: person.first ?? person.first_name, last: person.last ?? person.last_name }, locale) ||
|
|
43
50
|
(person.name ?? "").trim() ||
|
|
44
51
|
(person.email ?? "").trim()
|
|
45
52
|
);
|
|
@@ -196,8 +196,10 @@ export interface InvitationsPanelProps<R extends string = string> {
|
|
|
196
196
|
onInvite?: (draft: InvitationDraft<R>) => MaybePromise<InvitationSentAnswer | void>;
|
|
197
197
|
/** Mints a new link and mails it. Left out: no resend button. */
|
|
198
198
|
onResend?: (invitation: InvitationRow<R>) => MaybePromise<InvitationSentAnswer | void>;
|
|
199
|
-
/** Revokes an open invitation (level `none`, §4.2: re-inviting undoes
|
|
200
|
-
*
|
|
199
|
+
/** Revokes an open or an expired invitation (level `none`, §4.2: re-inviting undoes
|
|
200
|
+
* it). An expired row can be resent, and since 0.31 also removed: in an app that
|
|
201
|
+
* keeps the rows it would otherwise stay listed for good (Kurvenschmiede's 0.30
|
|
202
|
+
* adoption). Left out: no revoke button. */
|
|
201
203
|
onRevoke?: (invitation: InvitationRow<R>) => MaybePromise;
|
|
202
204
|
/** The list is being fetched. */
|
|
203
205
|
loading?: boolean;
|
|
@@ -205,6 +207,10 @@ export interface InvitationsPanelProps<R extends string = string> {
|
|
|
205
207
|
describeError?: (error: unknown) => ReactNode | undefined;
|
|
206
208
|
/** Over the form: who can be invited and what happens, in the app's words. */
|
|
207
209
|
intro?: ReactNode;
|
|
210
|
+
/** The list's visible heading. Default: the `listTitle` label. `null` leaves it out,
|
|
211
|
+
* for a page whose card is already headed "Invitations" (Kurvenschmiede's 0.30
|
|
212
|
+
* adoption); the list keeps the label as its accessible name either way. */
|
|
213
|
+
listTitle?: ReactNode | null;
|
|
208
214
|
className?: string;
|
|
209
215
|
labels?: Partial<InvitationsLabels>;
|
|
210
216
|
}
|
|
@@ -233,6 +239,7 @@ export function InvitationsPanel<R extends string = string>({
|
|
|
233
239
|
loading = false,
|
|
234
240
|
describeError,
|
|
235
241
|
intro,
|
|
242
|
+
listTitle,
|
|
236
243
|
className,
|
|
237
244
|
labels: labelsProp,
|
|
238
245
|
}: InvitationsPanelProps<R>) {
|
|
@@ -418,7 +425,7 @@ export function InvitationsPanel<R extends string = string>({
|
|
|
418
425
|
)}
|
|
419
426
|
|
|
420
427
|
<div className="space-y-2">
|
|
421
|
-
<SectionLabel as="h4">{labels.listTitle}</SectionLabel>
|
|
428
|
+
{listTitle !== null && <SectionLabel as="h4">{listTitle ?? labels.listTitle}</SectionLabel>}
|
|
422
429
|
{loading && invitations.length === 0 ? (
|
|
423
430
|
<div className="flex justify-center py-3">
|
|
424
431
|
<Spinner label={labels.loading} />
|
|
@@ -432,7 +439,7 @@ export function InvitationsPanel<R extends string = string>({
|
|
|
432
439
|
const link = invitation.link ?? links[key];
|
|
433
440
|
const open = invitation.status === "open";
|
|
434
441
|
const resendable = onResend && (open || invitation.status === "expired");
|
|
435
|
-
const revocable = onRevoke && open;
|
|
442
|
+
const revocable = onRevoke && (open || invitation.status === "expired");
|
|
436
443
|
const scopeText = scopeLabel(invitation.scope);
|
|
437
444
|
const by =
|
|
438
445
|
typeof invitation.invited_by === "string" ? invitation.invited_by : nameOf(invitation.invited_by);
|
package/src/auth/auth-errors.ts
CHANGED
|
@@ -64,8 +64,31 @@ export type AccountErrorCode =
|
|
|
64
64
|
| "confirmation_mismatch"
|
|
65
65
|
| "password_incorrect";
|
|
66
66
|
|
|
67
|
-
/**
|
|
68
|
-
|
|
67
|
+
/**
|
|
68
|
+
* 0.31.0: the demo's coded refusals (docs/landing-demo-harmonization.md §6.2) —
|
|
69
|
+
* server-kit 0.5's `DemoErrorCode`, value for value, answered as `{detail, code}` like
|
|
70
|
+
* the others and read by the same {@link authErrorCode}.
|
|
71
|
+
*
|
|
72
|
+
* - `demo_disabled` — `404`: the demo switch is off (it is "not there", not forbidden).
|
|
73
|
+
* - `demo_rate_limited` — `429` with `Retry-After`: the per-IP window of demo starts.
|
|
74
|
+
* - `demo_capacity` — `429`, no `Retry-After`: the cap of live demo users.
|
|
75
|
+
* - `demo_not_ready` — `503`: the demo data has not been seeded yet.
|
|
76
|
+
* - `demo_read_only` — `403`, model R: any write by a demo user. The app's write lock
|
|
77
|
+
* already explains it, so it shows no toast (§5.4).
|
|
78
|
+
* - `demo_refused` — `403`, both models: an action a demo never may — a way in or out,
|
|
79
|
+
* mail, uploads, the account export (§6.4).
|
|
80
|
+
*/
|
|
81
|
+
export type DemoErrorCode =
|
|
82
|
+
| "demo_disabled"
|
|
83
|
+
| "demo_rate_limited"
|
|
84
|
+
| "demo_capacity"
|
|
85
|
+
| "demo_not_ready"
|
|
86
|
+
| "demo_read_only"
|
|
87
|
+
| "demo_refused";
|
|
88
|
+
|
|
89
|
+
/** Every code the kit reads: the sign-in refusals, the account ones (0.30.0) and the
|
|
90
|
+
* demo's (0.31.0). */
|
|
91
|
+
export type KitErrorCode = AuthErrorCode | AccountErrorCode | DemoErrorCode;
|
|
69
92
|
|
|
70
93
|
const CODES: ReadonlySet<string> = new Set<KitErrorCode>([
|
|
71
94
|
"invalid_credentials",
|
|
@@ -83,6 +106,12 @@ const CODES: ReadonlySet<string> = new Set<KitErrorCode>([
|
|
|
83
106
|
"confirmation_required",
|
|
84
107
|
"confirmation_mismatch",
|
|
85
108
|
"password_incorrect",
|
|
109
|
+
"demo_disabled",
|
|
110
|
+
"demo_rate_limited",
|
|
111
|
+
"demo_capacity",
|
|
112
|
+
"demo_not_ready",
|
|
113
|
+
"demo_read_only",
|
|
114
|
+
"demo_refused",
|
|
86
115
|
]);
|
|
87
116
|
|
|
88
117
|
type Bag = Record<string, unknown>;
|
|
@@ -102,8 +131,9 @@ function codeOfBody(body: unknown): KitErrorCode | undefined {
|
|
|
102
131
|
}
|
|
103
132
|
|
|
104
133
|
/**
|
|
105
|
-
* The code an error carries — an {@link AuthErrorCode},
|
|
106
|
-
* {@link AccountErrorCode} — or `undefined` when it
|
|
134
|
+
* The code an error carries — an {@link AuthErrorCode}, since 0.30.0 an
|
|
135
|
+
* {@link AccountErrorCode}, since 0.31.0 a {@link DemoErrorCode} — or `undefined` when it
|
|
136
|
+
* carries none.
|
|
107
137
|
*
|
|
108
138
|
* Reads the parsed response body wherever the apps' HTTP clients put it, without
|
|
109
139
|
* depending on any of them:
|
|
@@ -142,7 +172,8 @@ export function authErrorCode(err: unknown): KitErrorCode | undefined {
|
|
|
142
172
|
* The kit's `SignInForm` and `RegisterForm` call it on what their callbacks reject
|
|
143
173
|
* with, so an app's `onSubmit` can simply let its client's error through. See
|
|
144
174
|
* {@link authErrorCode} for the shapes it reads. Since 0.30.0 it knows the account codes
|
|
145
|
-
* too: `isAuthError(err, "last_admin")
|
|
175
|
+
* too: `isAuthError(err, "last_admin")`; since 0.31.0 the demo's:
|
|
176
|
+
* `isAuthError(err, "demo_read_only")`.
|
|
146
177
|
*/
|
|
147
178
|
export function isAuthError(err: unknown, code?: KitErrorCode): boolean {
|
|
148
179
|
const found = authErrorCode(err);
|
|
@@ -144,7 +144,7 @@ export interface ResetPasswordFormProps
|
|
|
144
144
|
forgotHref?: string;
|
|
145
145
|
/** The app's words for a failed save (the device offline), or `undefined` for the
|
|
146
146
|
* kit's: `rateLimited` for a bare `429` (0.30.0), else `failed`. Not asked for
|
|
147
|
-
* `token_invalid`, which
|
|
147
|
+
* `token_invalid` or `token_expired`, which are a state. */
|
|
148
148
|
describeError?: (error: unknown) => ReactNode | undefined;
|
|
149
149
|
/** The heading. Default `labels.title`; `null` draws none, for a page that puts it
|
|
150
150
|
* in `AuthLayout`'s `title` instead. */
|
|
@@ -299,7 +299,11 @@ export function ResetPasswordForm({
|
|
|
299
299
|
},
|
|
300
300
|
(error: unknown) => {
|
|
301
301
|
setPending(false);
|
|
302
|
-
|
|
302
|
+
// A link that died between the check and the save — used, or run out — is the
|
|
303
|
+
// dead-link state, under either code (0.31.1: keksdose mapped `token_expired`
|
|
304
|
+
// to it itself, `asDeadLink`).
|
|
305
|
+
const code = authErrorCode(error);
|
|
306
|
+
if (code === "token_invalid" || code === "token_expired") {
|
|
303
307
|
setPassword("");
|
|
304
308
|
setConfirm("");
|
|
305
309
|
setPhase({ token, phase: { kind: "invalid" } });
|
|
@@ -10,6 +10,9 @@ import { CurrentPasswordInput } from "../components/danger-confirm";
|
|
|
10
10
|
import { OneTimeCodeInput } from "../components/one-time-code-input";
|
|
11
11
|
import { TextLink } from "../components/text-link";
|
|
12
12
|
import { Button, Input } from "../components/ui";
|
|
13
|
+
import { accessAction } from "../landing/access";
|
|
14
|
+
import type { AccessChoice } from "../landing/access";
|
|
15
|
+
import { useLandingLabels } from "../landing/landing-labels";
|
|
13
16
|
import { authErrorCode, englishRateLimited, isRateLimited, retryAfterSeconds } from "./auth-errors";
|
|
14
17
|
import type { KitErrorCode } from "./auth-errors";
|
|
15
18
|
import { taggedEmail } from "./email-tag";
|
|
@@ -171,6 +174,9 @@ export interface SignInLabels {
|
|
|
171
174
|
* wait its `Retry-After` asks for, in seconds ({@link retryAfterSeconds}), or
|
|
172
175
|
* `undefined` (or `0`) without one. */
|
|
173
176
|
rateLimited?: (seconds?: number) => string;
|
|
177
|
+
/** 0.31.0: the link after `noAccount` for an invitation-only app (`access` of kind
|
|
178
|
+
* `request`) — a mail to support, in place of `register`. */
|
|
179
|
+
requestAccess?: string;
|
|
174
180
|
}
|
|
175
181
|
|
|
176
182
|
/** `Required`: every key, the 0.30 ones included, has its English here. */
|
|
@@ -208,6 +214,7 @@ export const DEFAULT_SIGN_IN_LABELS: Required<SignInLabels> = {
|
|
|
208
214
|
recoveryCodeHint: (length) => `${length} letters and digits. Dashes and spaces don’t matter.`,
|
|
209
215
|
recoveryCodeInvalid: "This backup code is not valid, or it has been used already.",
|
|
210
216
|
rateLimited: englishRateLimited,
|
|
217
|
+
requestAccess: "Request access",
|
|
211
218
|
};
|
|
212
219
|
|
|
213
220
|
/* ── Props ───────────────────────────────────────────────────────────────── */
|
|
@@ -252,6 +259,15 @@ export interface SignInFormProps extends Omit<ComponentPropsWithoutRef<"div">, "
|
|
|
252
259
|
forgotHref?: string;
|
|
253
260
|
/** "No account yet? Create account" goes here — `/register`. Left out, no line. */
|
|
254
261
|
registerHref?: string;
|
|
262
|
+
/**
|
|
263
|
+
* 0.31.0 (docs/landing-demo-harmonization.md §4.2): how a visitor gets an account —
|
|
264
|
+
* the same value the landing's `PublicHeader` and `Hero` take. `{kind: "request",
|
|
265
|
+
* email}` turns the line into "No account yet? Request access", a mail to support with
|
|
266
|
+
* the subject and body of `accessAction` (every app is invitation-only); `{kind:
|
|
267
|
+
* "register", href}` keeps "Create account", to `href`. Wins over `registerHref`; left
|
|
268
|
+
* out, `registerHref` works as before.
|
|
269
|
+
*/
|
|
270
|
+
access?: AccessChoice;
|
|
255
271
|
/** The email field's start value — the reset page ends on "Sign in" with the address
|
|
256
272
|
* filled in (§6.3). */
|
|
257
273
|
defaultEmail?: string;
|
|
@@ -408,6 +424,7 @@ export function SignInForm({
|
|
|
408
424
|
passkeyAutofill,
|
|
409
425
|
forgotHref,
|
|
410
426
|
registerHref,
|
|
427
|
+
access,
|
|
411
428
|
defaultEmail = "",
|
|
412
429
|
emailTag,
|
|
413
430
|
deactivatedContact,
|
|
@@ -424,6 +441,20 @@ export function SignInForm({
|
|
|
424
441
|
...rest
|
|
425
442
|
}: SignInFormProps) {
|
|
426
443
|
const labels = useKitLabels("signIn", DEFAULT_SIGN_IN_LABELS, labelsProp);
|
|
444
|
+
// The mail's subject and body are the landing's words (`landing` namespace), so the
|
|
445
|
+
// request reads the same from every page that offers it.
|
|
446
|
+
const landing = useLandingLabels();
|
|
447
|
+
const accountLink =
|
|
448
|
+
access === undefined
|
|
449
|
+
? registerHref === undefined
|
|
450
|
+
? null
|
|
451
|
+
: { href: registerHref, label: labels.register }
|
|
452
|
+
: access.kind === "register"
|
|
453
|
+
? { href: access.href, label: labels.register }
|
|
454
|
+
: {
|
|
455
|
+
href: accessAction(access, landing).href,
|
|
456
|
+
label: labels.requestAccess ?? DEFAULT_SIGN_IN_LABELS.requestAccess,
|
|
457
|
+
};
|
|
427
458
|
const [step, setStep] = useState<SignInStep>("credentials");
|
|
428
459
|
const [email, setEmail] = useState(defaultEmail);
|
|
429
460
|
const [password, setPassword] = useState("");
|
|
@@ -834,11 +865,11 @@ export function SignInForm({
|
|
|
834
865
|
</TextLink>
|
|
835
866
|
</p>
|
|
836
867
|
)}
|
|
837
|
-
{
|
|
868
|
+
{accountLink !== null && (
|
|
838
869
|
<p className="text-center text-sm text-[var(--text-secondary)]">
|
|
839
870
|
{labels.noAccount}{" "}
|
|
840
|
-
<TextLink href={
|
|
841
|
-
{
|
|
871
|
+
<TextLink href={accountLink.href} tone="primary">
|
|
872
|
+
{accountLink.label}
|
|
842
873
|
</TextLink>
|
|
843
874
|
</p>
|
|
844
875
|
)}
|
|
@@ -8,7 +8,7 @@ import { AlertBanner } from "../components/alert-banner";
|
|
|
8
8
|
import { LoadingState } from "../components/loading-state";
|
|
9
9
|
import { Button, EmptyState } from "../components/ui";
|
|
10
10
|
import { OUTCOME_CLASS, STATUS_HEADING_CLASS, isThenable, useOncePerKey } from "./status-parts";
|
|
11
|
-
import { authErrorCode } from "./auth-errors";
|
|
11
|
+
import { authErrorCode, englishRateLimited, isRateLimited, retryAfterSeconds } from "./auth-errors";
|
|
12
12
|
import type { AuthHeadingLevel } from "./status-parts";
|
|
13
13
|
|
|
14
14
|
/* ── Labels ──────────────────────────────────────────────────────────────── */
|
|
@@ -41,6 +41,10 @@ export interface VerifyEmailLabels {
|
|
|
41
41
|
resent: string;
|
|
42
42
|
/** Said (and shown) after a resend failed. */
|
|
43
43
|
resendError: string;
|
|
44
|
+
/** 0.31.1: a resend refused as throttled — HTTP 429 (`isRateLimited`) — given the
|
|
45
|
+
* `Retry-After` seconds, when the app's `describeError` has no words of its own
|
|
46
|
+
* (keksdose borrowed the sign-in namespace's). */
|
|
47
|
+
rateLimited?: (seconds?: number) => string;
|
|
44
48
|
/** The banner's message. */
|
|
45
49
|
banner: string;
|
|
46
50
|
/** The banner's ×: it defers, it does not close anything. */
|
|
@@ -62,6 +66,7 @@ export const DEFAULT_VERIFY_EMAIL_LABELS: VerifyEmailLabels = {
|
|
|
62
66
|
resendIn: (seconds) => `Send again in ${seconds} s`,
|
|
63
67
|
resent: "Confirmation email sent",
|
|
64
68
|
resendError: "Could not send the confirmation email",
|
|
69
|
+
rateLimited: englishRateLimited,
|
|
65
70
|
banner: "Please confirm your email address.",
|
|
66
71
|
dismiss: "Not now",
|
|
67
72
|
};
|
|
@@ -160,7 +165,14 @@ function ResendStatus({
|
|
|
160
165
|
labels: VerifyEmailLabels;
|
|
161
166
|
className?: string;
|
|
162
167
|
}) {
|
|
163
|
-
|
|
168
|
+
// The app's words first, then the throttle's, then the kit's catch-all.
|
|
169
|
+
const failed =
|
|
170
|
+
phase === "failed"
|
|
171
|
+
? (describeError?.(error) ??
|
|
172
|
+
(isRateLimited(error)
|
|
173
|
+
? (labels.rateLimited ?? englishRateLimited)(retryAfterSeconds(error))
|
|
174
|
+
: labels.resendError))
|
|
175
|
+
: "";
|
|
164
176
|
return (
|
|
165
177
|
<span
|
|
166
178
|
role="status"
|
|
@@ -46,6 +46,12 @@ export interface PasswordSettingLabels {
|
|
|
46
46
|
}
|
|
47
47
|
|
|
48
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
|
+
*/
|
|
49
55
|
status: string;
|
|
50
56
|
enabledText: string;
|
|
51
57
|
disabledText: string;
|
|
@@ -70,6 +76,12 @@ export interface TwoFactorSettingLabels {
|
|
|
70
76
|
secretHint?: string;
|
|
71
77
|
/** The copy button beside that key. */
|
|
72
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;
|
|
73
85
|
}
|
|
74
86
|
|
|
75
87
|
export interface PasskeysSettingLabels {
|
|
@@ -160,6 +172,7 @@ export const DEFAULT_ACCOUNT_SETTINGS_LABELS: AccountSettingsLabels = {
|
|
|
160
172
|
qrAlt: "QR code",
|
|
161
173
|
secretHint: "Can’t scan it? Enter this key in the app instead:",
|
|
162
174
|
copySecret: "Copy key",
|
|
175
|
+
title: "Two-factor authentication",
|
|
163
176
|
},
|
|
164
177
|
passkeys: {
|
|
165
178
|
title: "Passkeys",
|
|
@@ -13,8 +13,10 @@ import {
|
|
|
13
13
|
PasswordStrengthMeter,
|
|
14
14
|
} from "./password-strength";
|
|
15
15
|
import type { PasswordStrengthMeterProps } from "./password-strength";
|
|
16
|
-
import {
|
|
16
|
+
import { useKitLabelOverrides, useKitLabels } from "../i18n/kit-labels";
|
|
17
|
+
import { Chip } from "./chip";
|
|
17
18
|
import { useAccountSettingsLabels } from "./account-settings-labels";
|
|
19
|
+
import { SettingsCardTitle } from "../settings/settings-heading";
|
|
18
20
|
import type {
|
|
19
21
|
PasswordSettingLabels,
|
|
20
22
|
ProfileSettingLabels,
|
|
@@ -59,6 +61,8 @@ interface ProfileSettingCommonProps {
|
|
|
59
61
|
saving?: boolean;
|
|
60
62
|
/** Prop > `<UiKitProvider labels={{ accountSettings: { profile } }}>` > English. */
|
|
61
63
|
labels?: Partial<ProfileSettingLabels>;
|
|
64
|
+
/** The card's DOM id — a settings catalogue `anchor`, so `?focus=` can ring it (0.31). */
|
|
65
|
+
id?: string;
|
|
62
66
|
}
|
|
63
67
|
|
|
64
68
|
/**
|
|
@@ -114,11 +118,11 @@ export type ProfileSettingProps = ProfileSettingDisplayNameProps | ProfileSettin
|
|
|
114
118
|
* blank, and is a `commit` (a {@link WriteLockProvider} locks it, keksdose's demo).
|
|
115
119
|
*/
|
|
116
120
|
export function ProfileSetting(props: ProfileSettingProps) {
|
|
117
|
-
const { name, email, role, memberSince, labels: labelsProp } = props;
|
|
121
|
+
const { name, email, role, memberSince, labels: labelsProp, id } = props;
|
|
118
122
|
const labels = useAccountSettingsLabels("profile", labelsProp);
|
|
119
123
|
const names = props.firstName !== undefined || props.lastName !== undefined;
|
|
120
124
|
return (
|
|
121
|
-
<Card className="p-4 space-y-3">
|
|
125
|
+
<Card id={id} className="p-4 space-y-3">
|
|
122
126
|
<div className="flex items-center gap-3">
|
|
123
127
|
<UserAvatar
|
|
124
128
|
name={name}
|
|
@@ -127,7 +131,9 @@ export function ProfileSetting(props: ProfileSettingProps) {
|
|
|
127
131
|
size="lg"
|
|
128
132
|
/>
|
|
129
133
|
<div className="min-w-0">
|
|
130
|
-
|
|
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>
|
|
131
137
|
<div className="truncate font-mono text-xs text-[var(--text-muted)]">
|
|
132
138
|
{/* `email` was a required key that nothing rendered; it names the address
|
|
133
139
|
for a screen reader, which otherwise hears a bare string under a title. */}
|
|
@@ -293,6 +299,7 @@ export function PasswordSetting({
|
|
|
293
299
|
strength,
|
|
294
300
|
maxBytes,
|
|
295
301
|
labels: labelsProp,
|
|
302
|
+
id,
|
|
296
303
|
}: {
|
|
297
304
|
/** Called with the validated (current, new) pair; return a promise to auto-clear on success. */
|
|
298
305
|
onSubmit: (currentPassword: string, newPassword: string) => void | Promise<unknown>;
|
|
@@ -323,6 +330,8 @@ export function PasswordSetting({
|
|
|
323
330
|
maxBytes?: number;
|
|
324
331
|
/** Prop > `<UiKitProvider labels={{ accountSettings: { password } }}>` > English. */
|
|
325
332
|
labels?: Partial<PasswordSettingLabels>;
|
|
333
|
+
/** The card's DOM id — a settings catalogue `anchor`, so `?focus=` can ring it (0.31). */
|
|
334
|
+
id?: string;
|
|
326
335
|
}) {
|
|
327
336
|
const labels = useAccountSettingsLabels("password", labelsProp);
|
|
328
337
|
const strengthOptions = typeof strength === "object" ? strength : undefined;
|
|
@@ -365,8 +374,8 @@ export function PasswordSetting({
|
|
|
365
374
|
);
|
|
366
375
|
|
|
367
376
|
return (
|
|
368
|
-
<Card className="p-4 space-y-3">
|
|
369
|
-
<
|
|
377
|
+
<Card id={id} className="p-4 space-y-3">
|
|
378
|
+
<SettingsCardTitle>{labels.title}</SettingsCardTitle>
|
|
370
379
|
<Input type="password" autoComplete="current-password" label={labels.current} value={current} onChange={(e) => setCurrent(e.target.value)} />
|
|
371
380
|
{strength ? (
|
|
372
381
|
// One box for the field and its meter, so the card's `space-y-3` spaces the pair
|
|
@@ -433,6 +442,7 @@ export function TwoFactorSetting({
|
|
|
433
442
|
busy,
|
|
434
443
|
renderQr,
|
|
435
444
|
labels: labelsProp,
|
|
445
|
+
id,
|
|
436
446
|
}: {
|
|
437
447
|
enabled: boolean;
|
|
438
448
|
/** What the app's setup call returned; null before setup starts. */
|
|
@@ -446,8 +456,16 @@ export function TwoFactorSetting({
|
|
|
446
456
|
renderQr?: (otpauthUri: string) => ReactNode;
|
|
447
457
|
/** Prop > `<UiKitProvider labels={{ accountSettings: { twoFactor } }}>` > English. */
|
|
448
458
|
labels?: Partial<TwoFactorSettingLabels>;
|
|
459
|
+
/** The card's DOM id — a settings catalogue `anchor`, so `?focus=` can ring it (0.31). */
|
|
460
|
+
id?: string;
|
|
449
461
|
}) {
|
|
450
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;
|
|
451
469
|
const [code, setCode] = useState("");
|
|
452
470
|
const [password, setPassword] = useState("");
|
|
453
471
|
// Both code fields are a OneTimeCodeInput (0.22.0) — the field this card wrote by hand
|
|
@@ -461,17 +479,20 @@ export function TwoFactorSetting({
|
|
|
461
479
|
// pasted from an authenticator that groups its digits used to reach `onEnable`
|
|
462
480
|
// with the space in it.
|
|
463
481
|
// - one `code` state for both, as before.
|
|
464
|
-
// Only for the "Status: Enabled" composition — the punctuation between a field's
|
|
465
|
-
// name and its value is the language's (see `CommonLabels.fieldValue`).
|
|
466
|
-
const common = useKitLabels("common", DEFAULT_COMMON_LABELS);
|
|
467
482
|
const secret = setup
|
|
468
483
|
? (setup.secret ?? ("otpauthUri" in setup ? otpauthSecret(setup.otpauthUri) : null))
|
|
469
484
|
: null;
|
|
470
485
|
|
|
471
486
|
return (
|
|
472
|
-
<Card className="p-4 space-y-3">
|
|
473
|
-
|
|
474
|
-
|
|
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>
|
|
475
496
|
</div>
|
|
476
497
|
|
|
477
498
|
{!enabled && !setup && (
|
|
@@ -6,6 +6,7 @@ import { TypedConfirmField, typedMatches } from "./danger-confirm";
|
|
|
6
6
|
import type { TypedMatch } from "./danger-confirm";
|
|
7
7
|
import { DialogFrame } from "./dialog-frame";
|
|
8
8
|
import { Button } from "./ui";
|
|
9
|
+
import { useWriteLock } from "./write-lock";
|
|
9
10
|
|
|
10
11
|
/**
|
|
11
12
|
* The `confirmDialog` namespace of `<UiKitProvider labels>`: the two buttons' fallback
|
|
@@ -76,6 +77,14 @@ export interface ConfirmOptions {
|
|
|
76
77
|
* keeps spaces too — for a phrase like "DELETE" whose contract is "exactly this".
|
|
77
78
|
*/
|
|
78
79
|
typedMatch?: TypedMatch;
|
|
80
|
+
/**
|
|
81
|
+
* 0.31.1: the confirm button COMMITS, as `Button`'s `commit` does. Under a locked
|
|
82
|
+
* `WriteLockProvider` — read where `useConfirm` was called, not where the one dialog
|
|
83
|
+
* host sits — it is `aria-disabled` with the lock's reason, so a read-only page (a
|
|
84
|
+
* demo, a viewer's budget) can't confirm a delete through a dialog. One prop for the
|
|
85
|
+
* many confirm sites an app has (kastlan's 0.31 adoption counted ~23).
|
|
86
|
+
*/
|
|
87
|
+
commit?: boolean;
|
|
79
88
|
}
|
|
80
89
|
|
|
81
90
|
export type ConfirmFn = (options: ConfirmOptions) => Promise<boolean>;
|
|
@@ -97,12 +106,21 @@ export function useConfirm(): ConfirmFn {
|
|
|
97
106
|
if (!confirm) {
|
|
98
107
|
throw new Error("useConfirm() must be called below a <ConfirmProvider>.");
|
|
99
108
|
}
|
|
100
|
-
|
|
109
|
+
// The lock of the CALLER's place: the dialog host usually sits above the app's
|
|
110
|
+
// `WriteLockProvider`, so it could not read the lock itself.
|
|
111
|
+
const lock = useWriteLock();
|
|
112
|
+
return useCallback(
|
|
113
|
+
(options: ConfirmOptions) =>
|
|
114
|
+
confirm(options.commit && lock.locked ? ({ ...options, lockReason: lock.reason } as ConfirmOptions) : options),
|
|
115
|
+
[confirm, lock.locked, lock.reason],
|
|
116
|
+
);
|
|
101
117
|
}
|
|
102
118
|
|
|
103
119
|
interface Request extends ConfirmOptions {
|
|
104
120
|
id: number;
|
|
105
121
|
resolve: (answer: boolean) => void;
|
|
122
|
+
/** Set by {@link useConfirm} for a `commit` confirm under a locked provider. */
|
|
123
|
+
lockReason?: ReactNode;
|
|
106
124
|
}
|
|
107
125
|
|
|
108
126
|
export interface ConfirmProviderProps {
|
|
@@ -279,6 +297,7 @@ function ConfirmDialogView({
|
|
|
279
297
|
type="button"
|
|
280
298
|
variant={tone === "danger" ? "danger" : "primary"}
|
|
281
299
|
disabled={!matches}
|
|
300
|
+
disabledReason={request.lockReason}
|
|
282
301
|
onClick={() => {
|
|
283
302
|
accepted.current = true;
|
|
284
303
|
close();
|
|
@@ -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
|