@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
|
@@ -0,0 +1,284 @@
|
|
|
1
|
+
import { useState } from "react";
|
|
2
|
+
import type { ComponentPropsWithoutRef, ReactNode } from "react";
|
|
3
|
+
import { MonitorSmartphone } from "lucide-react";
|
|
4
|
+
|
|
5
|
+
import { useKitLabels, useKitLocale } from "../i18n/kit-labels";
|
|
6
|
+
import { cn } from "../lib/cn";
|
|
7
|
+
import { formatRelativeTime, toDate } from "../lib/format";
|
|
8
|
+
import type { DateInput } from "../lib/format";
|
|
9
|
+
import { AlertBanner } from "../components/alert-banner";
|
|
10
|
+
import { Chip } from "../components/chip";
|
|
11
|
+
import { hasMessage } from "../components/choice-parts";
|
|
12
|
+
import { DangerConfirm } from "../components/danger-confirm";
|
|
13
|
+
import { Skeleton } from "../components/skeleton";
|
|
14
|
+
import { Button, Card } from "../components/ui";
|
|
15
|
+
import { CARD_DESCRIPTION_CLASS, settle, useMounted } from "./account-parts";
|
|
16
|
+
import { SettingsCardTitle } from "../settings/settings-heading";
|
|
17
|
+
|
|
18
|
+
/* ── Labels ──────────────────────────────────────────────────────────────── */
|
|
19
|
+
|
|
20
|
+
/** Every string {@link SessionsSetting} renders — the `sessions` namespace of
|
|
21
|
+
* `<UiKitProvider labels>`, overridable per instance through `labels`. The confirm's
|
|
22
|
+
* Cancel is `dangerConfirm.cancel`. */
|
|
23
|
+
export interface SessionsLabels {
|
|
24
|
+
/** The card's title. */
|
|
25
|
+
title: string;
|
|
26
|
+
/** The line under it: what "sign out everywhere" is for. */
|
|
27
|
+
description: string;
|
|
28
|
+
/** The button that asks first. */
|
|
29
|
+
signOutEverywhere: string;
|
|
30
|
+
/** The question it asks: every session ends, this one too. */
|
|
31
|
+
signOutEverywherePrompt: string;
|
|
32
|
+
/** The button that answers it. */
|
|
33
|
+
confirmSignOutEverywhere: string;
|
|
34
|
+
/** The device list's accessible name. */
|
|
35
|
+
list: string;
|
|
36
|
+
/** While the list loads. */
|
|
37
|
+
loading: string;
|
|
38
|
+
/** A list with no rows. */
|
|
39
|
+
empty: string;
|
|
40
|
+
/** The chip on the session this page runs in. */
|
|
41
|
+
current: string;
|
|
42
|
+
/** A session whose device the server could not name. */
|
|
43
|
+
unknownDevice: string;
|
|
44
|
+
/** Given the last activity, formatted ("3 hours ago", "5 Oct 2026"). */
|
|
45
|
+
lastActive: (when: string) => string;
|
|
46
|
+
/** Given the address the session was last seen from. */
|
|
47
|
+
ip: (address: string) => string;
|
|
48
|
+
/** A row's button, visible text. */
|
|
49
|
+
revoke: string;
|
|
50
|
+
/** The same button's accessible name, given the device — it starts with `revoke`,
|
|
51
|
+
* the visible text, so voice control finds it by what it says. */
|
|
52
|
+
revokeItem: (device: string) => string;
|
|
53
|
+
/** A request failed. */
|
|
54
|
+
failed: string;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export const DEFAULT_SESSIONS_LABELS: SessionsLabels = {
|
|
58
|
+
title: "Sessions",
|
|
59
|
+
description: "Signed in on a device you no longer use, or don’t trust? Sign out everywhere.",
|
|
60
|
+
signOutEverywhere: "Sign out everywhere…",
|
|
61
|
+
signOutEverywherePrompt:
|
|
62
|
+
"This ends every session, including this one: you will be signed out on this device too, and sign in again from here.",
|
|
63
|
+
confirmSignOutEverywhere: "Sign out everywhere",
|
|
64
|
+
list: "Where you’re signed in",
|
|
65
|
+
loading: "Loading sessions…",
|
|
66
|
+
empty: "No sessions to show.",
|
|
67
|
+
current: "This device",
|
|
68
|
+
unknownDevice: "Unknown device",
|
|
69
|
+
lastActive: (when) => `Last active ${when}`,
|
|
70
|
+
ip: (address) => `IP ${address}`,
|
|
71
|
+
revoke: "Sign out",
|
|
72
|
+
revokeItem: (device) => `Sign out ${device}`,
|
|
73
|
+
failed: "That didn’t work. Please try again.",
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
/* ── Props ───────────────────────────────────────────────────────────────── */
|
|
77
|
+
|
|
78
|
+
/** What a session's `id` may be: the app's own key type. */
|
|
79
|
+
export type SessionId = string | number;
|
|
80
|
+
|
|
81
|
+
/** One stored session (kastlan's `GET /auth/sessions`). */
|
|
82
|
+
export interface SessionItem<Id extends SessionId = SessionId> {
|
|
83
|
+
id: Id;
|
|
84
|
+
/** The browser and system, as the server reads them ("Firefox on Windows"). Blank
|
|
85
|
+
* reads as `labels.unknownDevice`. */
|
|
86
|
+
device?: string | null;
|
|
87
|
+
/** The address it was last seen from. */
|
|
88
|
+
ip?: string | null;
|
|
89
|
+
/** A `Date`, an ISO string or epoch ms. */
|
|
90
|
+
lastActiveAt?: DateInput;
|
|
91
|
+
/** The session this page runs in: marked, and offered no "Sign out" of its own — the
|
|
92
|
+
* app's sign-out does that, and "Sign out everywhere" below. */
|
|
93
|
+
current?: boolean;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/** Which request failed — {@link SessionsSettingProps.describeError}'s second argument. */
|
|
97
|
+
export type SessionsAction = "sign-out-everywhere" | "revoke";
|
|
98
|
+
|
|
99
|
+
export interface SessionsSettingProps<Id extends SessionId = SessionId>
|
|
100
|
+
extends Omit<ComponentPropsWithoutRef<"div">, "children"> {
|
|
101
|
+
/**
|
|
102
|
+
* End every session (`POST /auth/logout`, auth §6.3) — this one included. Asked
|
|
103
|
+
* first; resolve once the server answered, then sign this page out: the card says
|
|
104
|
+
* that the device signs out too, and its own session is gone.
|
|
105
|
+
*/
|
|
106
|
+
onSignOutEverywhere: () => Promise<unknown> | void;
|
|
107
|
+
/**
|
|
108
|
+
* The stored sessions, where the app keeps them (kastlan's `user_sessions`, §6.3).
|
|
109
|
+
* Left out, the card is "Sign out everywhere" alone — keksdose and Kurvenschmiede keep
|
|
110
|
+
* a cut-off (`sessions_invalid_before`), not rows. `undefined` with `loading` while the
|
|
111
|
+
* first load runs.
|
|
112
|
+
*/
|
|
113
|
+
sessions?: readonly SessionItem<Id>[];
|
|
114
|
+
/** The list is loading. */
|
|
115
|
+
loading?: boolean;
|
|
116
|
+
/** End one other session (`DELETE /auth/sessions/{id}`). Left out, no row buttons.
|
|
117
|
+
* The row is busy until it settles; refresh `sessions` then. */
|
|
118
|
+
onRevoke?: (id: Id) => Promise<unknown> | void;
|
|
119
|
+
/** The app's own words for a failure, or `undefined` for the kit's `failed`. */
|
|
120
|
+
describeError?: (error: unknown, action: SessionsAction) => ReactNode | undefined;
|
|
121
|
+
/** The "Last active" text. Default: how long ago, in the provider's locale, and the
|
|
122
|
+
* date once it is more than a week back. */
|
|
123
|
+
formatLastActive?: (date: Date) => string;
|
|
124
|
+
/** Overrides the provider's locale for the default format. */
|
|
125
|
+
locale?: string;
|
|
126
|
+
labels?: Partial<SessionsLabels>;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* Where the account is signed in, and the way out of all of it
|
|
131
|
+
* (docs/user-admin-harmonization.md §6.3, §2.6).
|
|
132
|
+
*
|
|
133
|
+
* **"Sign out everywhere", in every app.** It ends every session, THIS one included —
|
|
134
|
+
* the server stamps a cut-off (`sessions_invalid_before`) that the page's own token is
|
|
135
|
+
* older than — so the confirm says so before it is pressed, and the app signs out once it
|
|
136
|
+
* resolves. A {@link DangerConfirm} in the warning tone: nothing is lost, but the person
|
|
137
|
+
* has to sign in again on every device. keksdose kept its ordinary sign-out device-only
|
|
138
|
+
* (its offline mirror is wiped on an explicit sign-out), which is why this is an action
|
|
139
|
+
* of its own, not a mode of that one.
|
|
140
|
+
*
|
|
141
|
+
* **The device list, where sessions are stored** (kastlan): device, address, last
|
|
142
|
+
* active, and "This device" on the current one, with "Sign out" on each other row — no
|
|
143
|
+
* confirm, since a session ended is a sign-in away from coming back, not data lost.
|
|
144
|
+
* Without `sessions` there is no list: the other apps keep a cut-off, not rows.
|
|
145
|
+
*
|
|
146
|
+
* Both requests are the app's; a failure is said under the card's action, in the app's
|
|
147
|
+
* words if `describeError` has them.
|
|
148
|
+
*/
|
|
149
|
+
export function SessionsSetting<Id extends SessionId = SessionId>({
|
|
150
|
+
onSignOutEverywhere,
|
|
151
|
+
sessions,
|
|
152
|
+
loading,
|
|
153
|
+
onRevoke,
|
|
154
|
+
describeError,
|
|
155
|
+
formatLastActive,
|
|
156
|
+
locale: localeProp,
|
|
157
|
+
labels: labelsProp,
|
|
158
|
+
className,
|
|
159
|
+
...rest
|
|
160
|
+
}: SessionsSettingProps<Id>) {
|
|
161
|
+
const labels = useKitLabels("sessions", DEFAULT_SESSIONS_LABELS, labelsProp);
|
|
162
|
+
const locale = useKitLocale(localeProp);
|
|
163
|
+
const [busyId, setBusyId] = useState<Id | null>(null);
|
|
164
|
+
const [failure, setFailure] = useState<ReactNode>(null);
|
|
165
|
+
const mounted = useMounted();
|
|
166
|
+
|
|
167
|
+
const describe = (error: unknown, action: SessionsAction): ReactNode => {
|
|
168
|
+
const own = describeError?.(error, action);
|
|
169
|
+
return hasMessage(own) ? own : labels.failed;
|
|
170
|
+
};
|
|
171
|
+
|
|
172
|
+
const lastActive = (value: DateInput): string | null => {
|
|
173
|
+
const date = toDate(value);
|
|
174
|
+
if (!date) return null;
|
|
175
|
+
return formatLastActive
|
|
176
|
+
? formatLastActive(date)
|
|
177
|
+
: formatRelativeTime(date, { locale, absoluteAfterDays: 7, absoluteStyle: "medium" });
|
|
178
|
+
};
|
|
179
|
+
|
|
180
|
+
const signOutEverywhere = () => {
|
|
181
|
+
setFailure(null);
|
|
182
|
+
// Rejected again after it is said, so the tile stays armed for a retry.
|
|
183
|
+
return settle(onSignOutEverywhere).catch((error: unknown) => {
|
|
184
|
+
if (mounted.current) setFailure(describe(error, "sign-out-everywhere"));
|
|
185
|
+
throw error;
|
|
186
|
+
});
|
|
187
|
+
};
|
|
188
|
+
|
|
189
|
+
const revoke = (id: Id) => {
|
|
190
|
+
if (!onRevoke || busyId !== null) return;
|
|
191
|
+
setBusyId(id);
|
|
192
|
+
setFailure(null);
|
|
193
|
+
settle(() => onRevoke(id)).then(
|
|
194
|
+
() => {
|
|
195
|
+
if (mounted.current) setBusyId(null);
|
|
196
|
+
},
|
|
197
|
+
(error: unknown) => {
|
|
198
|
+
if (!mounted.current) return;
|
|
199
|
+
setBusyId(null);
|
|
200
|
+
setFailure(describe(error, "revoke"));
|
|
201
|
+
},
|
|
202
|
+
);
|
|
203
|
+
};
|
|
204
|
+
|
|
205
|
+
const showList = sessions !== undefined || loading;
|
|
206
|
+
|
|
207
|
+
return (
|
|
208
|
+
<Card {...rest} className={cn("p-4 space-y-3", className)}>
|
|
209
|
+
<div>
|
|
210
|
+
<SettingsCardTitle>{labels.title}</SettingsCardTitle>
|
|
211
|
+
<div className={CARD_DESCRIPTION_CLASS}>{labels.description}</div>
|
|
212
|
+
</div>
|
|
213
|
+
{showList &&
|
|
214
|
+
(loading || sessions === undefined ? (
|
|
215
|
+
<Skeleton lines={2} label={labels.loading} />
|
|
216
|
+
) : sessions.length === 0 ? (
|
|
217
|
+
<p className={CARD_DESCRIPTION_CLASS}>{labels.empty}</p>
|
|
218
|
+
) : (
|
|
219
|
+
<ul aria-label={labels.list} className="divide-y divide-[var(--border)]">
|
|
220
|
+
{sessions.map((session) => {
|
|
221
|
+
const device = session.device?.trim() || labels.unknownDevice;
|
|
222
|
+
const when = lastActive(session.lastActiveAt);
|
|
223
|
+
const ip = session.ip?.trim();
|
|
224
|
+
return (
|
|
225
|
+
<li
|
|
226
|
+
key={session.id}
|
|
227
|
+
data-session-id={String(session.id)}
|
|
228
|
+
data-current={session.current || undefined}
|
|
229
|
+
className="flex items-center gap-3 py-2"
|
|
230
|
+
>
|
|
231
|
+
<MonitorSmartphone aria-hidden className="size-4 shrink-0 text-[var(--text-muted)]" />
|
|
232
|
+
<div className="min-w-0 flex-1">
|
|
233
|
+
<div className="flex flex-wrap items-center gap-x-2 gap-y-1">
|
|
234
|
+
<span className="min-w-0 break-words text-sm font-medium">{device}</span>
|
|
235
|
+
{session.current && (
|
|
236
|
+
<Chip size="sm" tone="brand">
|
|
237
|
+
{labels.current}
|
|
238
|
+
</Chip>
|
|
239
|
+
)}
|
|
240
|
+
</div>
|
|
241
|
+
{(when || ip) && (
|
|
242
|
+
<div className={CARD_DESCRIPTION_CLASS}>
|
|
243
|
+
{when && labels.lastActive(when)}
|
|
244
|
+
{when && ip && " · "}
|
|
245
|
+
{/* An address reads left to right in any page direction. */}
|
|
246
|
+
{ip && <span dir="ltr">{labels.ip(ip)}</span>}
|
|
247
|
+
</div>
|
|
248
|
+
)}
|
|
249
|
+
</div>
|
|
250
|
+
{onRevoke && !session.current && (
|
|
251
|
+
<Button
|
|
252
|
+
type="button"
|
|
253
|
+
size="sm"
|
|
254
|
+
variant="ghost"
|
|
255
|
+
commit
|
|
256
|
+
pending={busyId === session.id}
|
|
257
|
+
disabled={busyId !== null && busyId !== session.id}
|
|
258
|
+
aria-label={labels.revokeItem(device)}
|
|
259
|
+
onClick={() => revoke(session.id)}
|
|
260
|
+
>
|
|
261
|
+
{labels.revoke}
|
|
262
|
+
</Button>
|
|
263
|
+
)}
|
|
264
|
+
</li>
|
|
265
|
+
);
|
|
266
|
+
})}
|
|
267
|
+
</ul>
|
|
268
|
+
))}
|
|
269
|
+
<DangerConfirm
|
|
270
|
+
tone="warning"
|
|
271
|
+
commit
|
|
272
|
+
armLabel={labels.signOutEverywhere}
|
|
273
|
+
prompt={labels.signOutEverywherePrompt}
|
|
274
|
+
confirmLabel={labels.confirmSignOutEverywhere}
|
|
275
|
+
onConfirm={signOutEverywhere}
|
|
276
|
+
/>
|
|
277
|
+
{hasMessage(failure) && (
|
|
278
|
+
<AlertBanner tone="danger" size="sm" role="alert">
|
|
279
|
+
{failure}
|
|
280
|
+
</AlertBanner>
|
|
281
|
+
)}
|
|
282
|
+
</Card>
|
|
283
|
+
);
|
|
284
|
+
}
|