@10x-media/impersonation 0.1.0-beta.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/CHANGELOG.md +31 -0
- package/LICENSE +21 -0
- package/README.md +49 -0
- package/dist/access/filterTargets.js +51 -0
- package/dist/access/filterTargets.js.map +1 -0
- package/dist/auth/cookies.js +49 -0
- package/dist/auth/cookies.js.map +1 -0
- package/dist/auth/expiryCookies.js +59 -0
- package/dist/auth/expiryCookies.js.map +1 -0
- package/dist/auth/issue.js +126 -0
- package/dist/auth/issue.js.map +1 -0
- package/dist/auth/mode.js +69 -0
- package/dist/auth/mode.js.map +1 -0
- package/dist/auth/origin.js +55 -0
- package/dist/auth/origin.js.map +1 -0
- package/dist/auth/revoke.js +24 -0
- package/dist/auth/revoke.js.map +1 -0
- package/dist/auth/tenantCookie.js +74 -0
- package/dist/auth/tenantCookie.js.map +1 -0
- package/dist/endpoints/codes.d.ts +6 -0
- package/dist/endpoints/current.js +66 -0
- package/dist/endpoints/current.js.map +1 -0
- package/dist/endpoints/exit.js +242 -0
- package/dist/endpoints/exit.js.map +1 -0
- package/dist/endpoints/shared.js +76 -0
- package/dist/endpoints/shared.js.map +1 -0
- package/dist/endpoints/start.js +332 -0
- package/dist/endpoints/start.js.map +1 -0
- package/dist/endpoints/terminate.js +73 -0
- package/dist/endpoints/terminate.js.map +1 -0
- package/dist/exports/client.d.ts +9 -0
- package/dist/exports/client.js +10 -0
- package/dist/exports/i18n.d.ts +3 -0
- package/dist/exports/i18n.js +3 -0
- package/dist/exports/rsc.d.ts +4 -0
- package/dist/exports/rsc.js +4 -0
- package/dist/exports/types.d.ts +3 -0
- package/dist/exports/types.js +1 -0
- package/dist/getImpersonation.d.ts +49 -0
- package/dist/getImpersonation.js +78 -0
- package/dist/getImpersonation.js.map +1 -0
- package/dist/ids.js +27 -0
- package/dist/ids.js.map +1 -0
- package/dist/index.d.ts +20 -0
- package/dist/index.js +55 -0
- package/dist/index.js.map +1 -0
- package/dist/plugin/assertTargets.js +23 -0
- package/dist/plugin/assertTargets.js.map +1 -0
- package/dist/plugin/constants.d.ts +6 -0
- package/dist/plugin/constants.js +20 -0
- package/dist/plugin/constants.js.map +1 -0
- package/dist/plugin/decorateAuth.js +99 -0
- package/dist/plugin/decorateAuth.js.map +1 -0
- package/dist/plugin/lookup.js +23 -0
- package/dist/plugin/lookup.js.map +1 -0
- package/dist/plugin/normalizeOptions.js +83 -0
- package/dist/plugin/normalizeOptions.js.map +1 -0
- package/dist/plugin/registerAdmin.js +49 -0
- package/dist/plugin/registerAdmin.js.map +1 -0
- package/dist/plugin/registerCollection.js +183 -0
- package/dist/plugin/registerCollection.js.map +1 -0
- package/dist/plugin/registerEndpoints.js +19 -0
- package/dist/plugin/registerEndpoints.js.map +1 -0
- package/dist/plugin/registerHooks.js +59 -0
- package/dist/plugin/registerHooks.js.map +1 -0
- package/dist/plugin/registerRetention.js +57 -0
- package/dist/plugin/registerRetention.js.map +1 -0
- package/dist/plugin/registerTranslations.js +19 -0
- package/dist/plugin/registerTranslations.js.map +1 -0
- package/dist/plugin/registry.js +11 -0
- package/dist/plugin/registry.js.map +1 -0
- package/dist/plugin/startable.js +21 -0
- package/dist/plugin/startable.js.map +1 -0
- package/dist/session/close.js +65 -0
- package/dist/session/close.js.map +1 -0
- package/dist/session/closeStale.d.ts +11 -0
- package/dist/session/closeStale.js +65 -0
- package/dist/session/closeStale.js.map +1 -0
- package/dist/session/locks.js +32 -0
- package/dist/session/locks.js.map +1 -0
- package/dist/session/resolve.js +91 -0
- package/dist/session/resolve.js.map +1 -0
- package/dist/translations/ar.js +52 -0
- package/dist/translations/ar.js.map +1 -0
- package/dist/translations/de.js +52 -0
- package/dist/translations/de.js.map +1 -0
- package/dist/translations/en.js +52 -0
- package/dist/translations/en.js.map +1 -0
- package/dist/translations/es.js +52 -0
- package/dist/translations/es.js.map +1 -0
- package/dist/translations/fr.js +52 -0
- package/dist/translations/fr.js.map +1 -0
- package/dist/translations/id.js +52 -0
- package/dist/translations/id.js.map +1 -0
- package/dist/translations/index.d.ts +24 -0
- package/dist/translations/index.js +49 -0
- package/dist/translations/index.js.map +1 -0
- package/dist/translations/keys.d.ts +56 -0
- package/dist/translations/keys.js +56 -0
- package/dist/translations/keys.js.map +1 -0
- package/dist/translations/ko.js +52 -0
- package/dist/translations/ko.js.map +1 -0
- package/dist/translations/lookup.js +17 -0
- package/dist/translations/lookup.js.map +1 -0
- package/dist/translations/pt.js +52 -0
- package/dist/translations/pt.js.map +1 -0
- package/dist/translations/ru.js +52 -0
- package/dist/translations/ru.js.map +1 -0
- package/dist/translations/server.js +13 -0
- package/dist/translations/server.js.map +1 -0
- package/dist/translations/uk.js +52 -0
- package/dist/translations/uk.js.map +1 -0
- package/dist/translations/useTranslation.js +12 -0
- package/dist/translations/useTranslation.js.map +1 -0
- package/dist/translations/zh.js +52 -0
- package/dist/translations/zh.js.map +1 -0
- package/dist/types.d.ts +214 -0
- package/dist/types.js +11 -0
- package/dist/types.js.map +1 -0
- package/dist/ui/EndSessionMenuItem.d.ts +5 -0
- package/dist/ui/EndSessionMenuItem.js +45 -0
- package/dist/ui/EndSessionMenuItem.js.map +1 -0
- package/dist/ui/ImpersonateIcon.js +20 -0
- package/dist/ui/ImpersonateIcon.js.map +1 -0
- package/dist/ui/ImpersonationAction.d.ts +10 -0
- package/dist/ui/ImpersonationAction.js +48 -0
- package/dist/ui/ImpersonationAction.js.map +1 -0
- package/dist/ui/ImpersonationBar.d.ts +36 -0
- package/dist/ui/ImpersonationBar.js +357 -0
- package/dist/ui/ImpersonationBar.js.map +1 -0
- package/dist/ui/ImpersonationConfig.js +25 -0
- package/dist/ui/ImpersonationConfig.js.map +1 -0
- package/dist/ui/ImpersonationDocumentButton.d.ts +5 -0
- package/dist/ui/ImpersonationDocumentButton.js +85 -0
- package/dist/ui/ImpersonationDocumentButton.js.map +1 -0
- package/dist/ui/ImpersonationFrontendBanner.d.ts +41 -0
- package/dist/ui/ImpersonationFrontendBanner.js +58 -0
- package/dist/ui/ImpersonationFrontendBanner.js.map +1 -0
- package/dist/ui/ImpersonationFrontendExit.d.ts +17 -0
- package/dist/ui/ImpersonationFrontendExit.js +45 -0
- package/dist/ui/ImpersonationFrontendExit.js.map +1 -0
- package/dist/ui/ImpersonationProvider.d.ts +16 -0
- package/dist/ui/ImpersonationProvider.js +127 -0
- package/dist/ui/ImpersonationProvider.js.map +1 -0
- package/dist/ui/ImpersonationSwitcher.d.ts +21 -0
- package/dist/ui/ImpersonationSwitcher.js +251 -0
- package/dist/ui/ImpersonationSwitcher.js.map +1 -0
- package/dist/ui/ImpersonationUserCard.d.ts +31 -0
- package/dist/ui/ImpersonationUserCard.js +51 -0
- package/dist/ui/ImpersonationUserCard.js.map +1 -0
- package/dist/ui/StartConfirmModal.d.ts +28 -0
- package/dist/ui/StartConfirmModal.js +65 -0
- package/dist/ui/StartConfirmModal.js.map +1 -0
- package/dist/ui/api.js +66 -0
- package/dist/ui/api.js.map +1 -0
- package/dist/ui/barPlacement.js +107 -0
- package/dist/ui/barPlacement.js.map +1 -0
- package/dist/ui/frontendBanner.css +22 -0
- package/dist/ui/impersonation.css +244 -0
- package/dist/ui/useImpersonation.d.ts +43 -0
- package/dist/ui/useImpersonation.js +36 -0
- package/dist/ui/useImpersonation.js.map +1 -0
- package/package.json +126 -0
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
//#region src/ui/ImpersonationUserCard.d.ts
|
|
2
|
+
type ImpersonationUserCardProps = {
|
|
3
|
+
collectionSlug: string;
|
|
4
|
+
doc: Record<string, unknown>;
|
|
5
|
+
documentHref: string;
|
|
6
|
+
email?: string;
|
|
7
|
+
onOpenDrawer: () => void;
|
|
8
|
+
onSelect: () => void;
|
|
9
|
+
openDocumentLabel: string;
|
|
10
|
+
openDrawerLabel: string;
|
|
11
|
+
title: string;
|
|
12
|
+
};
|
|
13
|
+
/**
|
|
14
|
+
* Default switcher row. Title is `admin.useAsTitle`. Email is optional and
|
|
15
|
+
* omitted when it would duplicate the title. Hosts can copy this for a custom
|
|
16
|
+
* card (roles, avatars) built with `useImpersonation`.
|
|
17
|
+
*/
|
|
18
|
+
declare const ImpersonationUserCard: ({
|
|
19
|
+
collectionSlug,
|
|
20
|
+
doc,
|
|
21
|
+
documentHref,
|
|
22
|
+
email,
|
|
23
|
+
onOpenDrawer,
|
|
24
|
+
onSelect,
|
|
25
|
+
openDocumentLabel,
|
|
26
|
+
openDrawerLabel,
|
|
27
|
+
title
|
|
28
|
+
}: ImpersonationUserCardProps) => import("react/jsx-runtime").JSX.Element;
|
|
29
|
+
//#endregion
|
|
30
|
+
export { ImpersonationUserCard, ImpersonationUserCardProps };
|
|
31
|
+
//# sourceMappingURL=ImpersonationUserCard.d.ts.map
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { Button, EditIcon, ExternalLinkIcon } from "@payloadcms/ui";
|
|
3
|
+
import { jsx, jsxs } from "react/jsx-runtime";
|
|
4
|
+
//#region src/ui/ImpersonationUserCard.tsx
|
|
5
|
+
/**
|
|
6
|
+
* Default switcher row. Title is `admin.useAsTitle`. Email is optional and
|
|
7
|
+
* omitted when it would duplicate the title. Hosts can copy this for a custom
|
|
8
|
+
* card (roles, avatars) built with `useImpersonation`.
|
|
9
|
+
*/
|
|
10
|
+
const ImpersonationUserCard = ({ collectionSlug, doc, documentHref, email, onOpenDrawer, onSelect, openDocumentLabel, openDrawerLabel, title }) => /* @__PURE__ */ jsxs("div", {
|
|
11
|
+
className: "impersonation-card",
|
|
12
|
+
"data-collection": collectionSlug,
|
|
13
|
+
"data-id": String(doc.id ?? ""),
|
|
14
|
+
children: [/* @__PURE__ */ jsxs("button", {
|
|
15
|
+
className: "impersonation-card__pick",
|
|
16
|
+
onClick: onSelect,
|
|
17
|
+
type: "button",
|
|
18
|
+
children: [/* @__PURE__ */ jsx("span", {
|
|
19
|
+
className: "impersonation-card__title",
|
|
20
|
+
children: title
|
|
21
|
+
}), email ? /* @__PURE__ */ jsx("span", {
|
|
22
|
+
"aria-hidden": "true",
|
|
23
|
+
className: "impersonation-card__email",
|
|
24
|
+
children: email
|
|
25
|
+
}) : null]
|
|
26
|
+
}), /* @__PURE__ */ jsxs("div", {
|
|
27
|
+
className: "impersonation-card__actions",
|
|
28
|
+
children: [/* @__PURE__ */ jsx(Button, {
|
|
29
|
+
buttonStyle: "icon-label",
|
|
30
|
+
extraButtonProps: { "aria-label": openDrawerLabel },
|
|
31
|
+
icon: /* @__PURE__ */ jsx(EditIcon, {}),
|
|
32
|
+
iconStyle: "none",
|
|
33
|
+
margin: false,
|
|
34
|
+
onClick: onOpenDrawer,
|
|
35
|
+
size: "small"
|
|
36
|
+
}), /* @__PURE__ */ jsx(Button, {
|
|
37
|
+
buttonStyle: "icon-label",
|
|
38
|
+
el: "link",
|
|
39
|
+
extraButtonProps: { "aria-label": openDocumentLabel },
|
|
40
|
+
icon: /* @__PURE__ */ jsx(ExternalLinkIcon, {}),
|
|
41
|
+
iconStyle: "none",
|
|
42
|
+
margin: false,
|
|
43
|
+
size: "small",
|
|
44
|
+
to: documentHref
|
|
45
|
+
})]
|
|
46
|
+
})]
|
|
47
|
+
});
|
|
48
|
+
//#endregion
|
|
49
|
+
export { ImpersonationUserCard };
|
|
50
|
+
|
|
51
|
+
//# sourceMappingURL=ImpersonationUserCard.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ImpersonationUserCard.js","names":[],"sources":["../../src/ui/ImpersonationUserCard.tsx"],"sourcesContent":["'use client'\n\nimport { Button, EditIcon, ExternalLinkIcon } from '@payloadcms/ui'\n\nexport type ImpersonationUserCardProps = {\n\tcollectionSlug: string\n\tdoc: Record<string, unknown>\n\tdocumentHref: string\n\temail?: string\n\tonOpenDrawer: () => void\n\tonSelect: () => void\n\topenDocumentLabel: string\n\topenDrawerLabel: string\n\ttitle: string\n}\n\n/**\n * Default switcher row. Title is `admin.useAsTitle`. Email is optional and\n * omitted when it would duplicate the title. Hosts can copy this for a custom\n * card (roles, avatars) built with `useImpersonation`.\n */\nexport const ImpersonationUserCard = ({\n\tcollectionSlug,\n\tdoc,\n\tdocumentHref,\n\temail,\n\tonOpenDrawer,\n\tonSelect,\n\topenDocumentLabel,\n\topenDrawerLabel,\n\ttitle,\n}: ImpersonationUserCardProps) => (\n\t<div\n\t\tclassName=\"impersonation-card\"\n\t\tdata-collection={collectionSlug}\n\t\tdata-id={String(doc.id ?? '')}\n\t>\n\t\t<button className=\"impersonation-card__pick\" onClick={onSelect} type=\"button\">\n\t\t\t<span className=\"impersonation-card__title\">{title}</span>\n\t\t\t{email ? (\n\t\t\t\t<span aria-hidden=\"true\" className=\"impersonation-card__email\">\n\t\t\t\t\t{email}\n\t\t\t\t</span>\n\t\t\t) : null}\n\t\t</button>\n\t\t<div className=\"impersonation-card__actions\">\n\t\t\t<Button\n\t\t\t\tbuttonStyle=\"icon-label\"\n\t\t\t\textraButtonProps={{\n\t\t\t\t\t'aria-label': openDrawerLabel,\n\t\t\t\t}}\n\t\t\t\ticon={<EditIcon />}\n\t\t\t\ticonStyle=\"none\"\n\t\t\t\tmargin={false}\n\t\t\t\tonClick={onOpenDrawer}\n\t\t\t\tsize=\"small\"\n\t\t\t/>\n\t\t\t<Button\n\t\t\t\tbuttonStyle=\"icon-label\"\n\t\t\t\tel=\"link\"\n\t\t\t\textraButtonProps={{\n\t\t\t\t\t'aria-label': openDocumentLabel,\n\t\t\t\t}}\n\t\t\t\ticon={<ExternalLinkIcon />}\n\t\t\t\ticonStyle=\"none\"\n\t\t\t\tmargin={false}\n\t\t\t\tsize=\"small\"\n\t\t\t\tto={documentHref}\n\t\t\t/>\n\t\t</div>\n\t</div>\n)\n"],"mappings":";;;;;;;;;AAqBA,MAAa,yBAAyB,EACrC,gBACA,KACA,cACA,OACA,cACA,UACA,mBACA,iBACA,YAEA,qBAAC,OAAD;CACC,WAAU;CACV,mBAAiB;CACjB,WAAS,OAAO,IAAI,MAAM,EAAE;WAH7B,CAKC,qBAAC,UAAD;EAAQ,WAAU;EAA2B,SAAS;EAAU,MAAK;YAArE,CACC,oBAAC,QAAD;GAAM,WAAU;aAA6B;EAAY,CAAA,GACxD,QACA,oBAAC,QAAD;GAAM,eAAY;GAAO,WAAU;aACjC;EACI,CAAA,IACH,IACG;KACR,qBAAC,OAAD;EAAK,WAAU;YAAf,CACC,oBAAC,QAAD;GACC,aAAY;GACZ,kBAAkB,EACjB,cAAc,gBACf;GACA,MAAM,oBAAC,UAAD,CAAW,CAAA;GACjB,WAAU;GACV,QAAQ;GACR,SAAS;GACT,MAAK;EACL,CAAA,GACD,oBAAC,QAAD;GACC,aAAY;GACZ,IAAG;GACH,kBAAkB,EACjB,cAAc,kBACf;GACA,MAAM,oBAAC,kBAAD,CAAmB,CAAA;GACzB,WAAU;GACV,QAAQ;GACR,MAAK;GACL,IAAI;EACJ,CAAA,CACG;GACD"}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
//#region src/ui/StartConfirmModal.d.ts
|
|
2
|
+
type StartTarget = {
|
|
3
|
+
collection: string;
|
|
4
|
+
id: number | string;
|
|
5
|
+
label: string;
|
|
6
|
+
};
|
|
7
|
+
type StartConfirmModalProps = {
|
|
8
|
+
apiPath: string;
|
|
9
|
+
modalSlug: string;
|
|
10
|
+
reasonMode: 'off' | 'optional' | 'required';
|
|
11
|
+
target: null | StartTarget;
|
|
12
|
+
};
|
|
13
|
+
/**
|
|
14
|
+
* Payload `ConfirmationModal` for starting a session. Reason uses `TextInput`
|
|
15
|
+
* in the body when the host asked for optional or required notes.
|
|
16
|
+
*
|
|
17
|
+
* `key={generation}` remounts after a failed confirm so the native Loading
|
|
18
|
+
* state resets. Throw still keeps the modal open.
|
|
19
|
+
*/
|
|
20
|
+
declare const StartConfirmModal: ({
|
|
21
|
+
apiPath,
|
|
22
|
+
modalSlug,
|
|
23
|
+
reasonMode,
|
|
24
|
+
target
|
|
25
|
+
}: StartConfirmModalProps) => import("react/jsx-runtime").JSX.Element;
|
|
26
|
+
//#endregion
|
|
27
|
+
export { StartConfirmModal };
|
|
28
|
+
//# sourceMappingURL=StartConfirmModal.d.ts.map
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { keys } from "../translations/keys.js";
|
|
3
|
+
import { useTranslation as useTranslation$1 } from "../translations/useTranslation.js";
|
|
4
|
+
import { errorKey, goAfterSwitch, postImpersonation } from "./api.js";
|
|
5
|
+
import { useRef, useState } from "react";
|
|
6
|
+
import { ConfirmationModal, TextInput, toast } from "@payloadcms/ui";
|
|
7
|
+
import { Fragment, jsx, jsxs } from "react/jsx-runtime";
|
|
8
|
+
//#region src/ui/StartConfirmModal.tsx
|
|
9
|
+
/**
|
|
10
|
+
* Payload `ConfirmationModal` for starting a session. Reason uses `TextInput`
|
|
11
|
+
* in the body when the host asked for optional or required notes.
|
|
12
|
+
*
|
|
13
|
+
* `key={generation}` remounts after a failed confirm so the native Loading
|
|
14
|
+
* state resets. Throw still keeps the modal open.
|
|
15
|
+
*/
|
|
16
|
+
const StartConfirmModal = ({ apiPath, modalSlug, reasonMode, target }) => {
|
|
17
|
+
const { t } = useTranslation$1();
|
|
18
|
+
const [reason, setReason] = useState("");
|
|
19
|
+
const [generation, setGeneration] = useState(0);
|
|
20
|
+
const reasonRef = useRef(reason);
|
|
21
|
+
reasonRef.current = reason;
|
|
22
|
+
const targetRef = useRef(target);
|
|
23
|
+
targetRef.current = target;
|
|
24
|
+
const failConfirm = (message) => {
|
|
25
|
+
setGeneration((current) => current + 1);
|
|
26
|
+
throw new Error(message);
|
|
27
|
+
};
|
|
28
|
+
const onConfirm = async () => {
|
|
29
|
+
const next = targetRef.current;
|
|
30
|
+
if (!next) return;
|
|
31
|
+
if (reasonMode === "required" && !reasonRef.current.trim()) {
|
|
32
|
+
toast.error(t(keys.errorReasonRequired));
|
|
33
|
+
failConfirm("reason required");
|
|
34
|
+
}
|
|
35
|
+
const result = await postImpersonation(`${apiPath}/start`, {
|
|
36
|
+
collection: next.collection,
|
|
37
|
+
id: next.id,
|
|
38
|
+
reason: reasonMode === "off" ? void 0 : reasonRef.current
|
|
39
|
+
});
|
|
40
|
+
if (!result.ok) {
|
|
41
|
+
toast.error(result.error ? t(errorKey(result.error)) : t(keys.errorFailed));
|
|
42
|
+
failConfirm(result.error ?? "failed");
|
|
43
|
+
}
|
|
44
|
+
goAfterSwitch(result.redirect);
|
|
45
|
+
};
|
|
46
|
+
return /* @__PURE__ */ jsx(ConfirmationModal, {
|
|
47
|
+
body: /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsx("p", { children: target?.label }), reasonMode === "off" ? null : /* @__PURE__ */ jsx(TextInput, {
|
|
48
|
+
label: t(keys.reasonLabel),
|
|
49
|
+
onChange: (event) => setReason(event.target.value),
|
|
50
|
+
path: `${modalSlug}-reason`,
|
|
51
|
+
placeholder: reasonMode === "required" ? void 0 : t(keys.reasonPlaceholder),
|
|
52
|
+
required: reasonMode === "required",
|
|
53
|
+
value: reason
|
|
54
|
+
})] }),
|
|
55
|
+
className: "impersonation-confirm-modal",
|
|
56
|
+
confirmLabel: t(keys.confirm),
|
|
57
|
+
heading: t(keys.confirmTitle),
|
|
58
|
+
modalSlug,
|
|
59
|
+
onConfirm
|
|
60
|
+
}, generation);
|
|
61
|
+
};
|
|
62
|
+
//#endregion
|
|
63
|
+
export { StartConfirmModal };
|
|
64
|
+
|
|
65
|
+
//# sourceMappingURL=StartConfirmModal.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"StartConfirmModal.js","names":["useTranslation"],"sources":["../../src/ui/StartConfirmModal.tsx"],"sourcesContent":["'use client'\n\nimport { ConfirmationModal, TextInput, toast } from '@payloadcms/ui'\nimport { type ChangeEvent, useRef, useState } from 'react'\n\nimport { keys } from '../translations/keys'\nimport { useTranslation } from '../translations/useTranslation'\nimport { errorKey, goAfterSwitch, postImpersonation } from './api'\n\nexport type StartTarget = {\n\tcollection: string\n\tid: number | string\n\tlabel: string\n}\n\nexport type StartConfirmModalProps = {\n\tapiPath: string\n\tmodalSlug: string\n\treasonMode: 'off' | 'optional' | 'required'\n\ttarget: null | StartTarget\n}\n\n/**\n * Payload `ConfirmationModal` for starting a session. Reason uses `TextInput`\n * in the body when the host asked for optional or required notes.\n *\n * `key={generation}` remounts after a failed confirm so the native Loading\n * state resets. Throw still keeps the modal open.\n */\nexport const StartConfirmModal = ({\n\tapiPath,\n\tmodalSlug,\n\treasonMode,\n\ttarget,\n}: StartConfirmModalProps) => {\n\tconst { t } = useTranslation()\n\tconst [reason, setReason] = useState('')\n\tconst [generation, setGeneration] = useState(0)\n\tconst reasonRef = useRef(reason)\n\treasonRef.current = reason\n\tconst targetRef = useRef(target)\n\ttargetRef.current = target\n\n\tconst failConfirm = (message: string): never => {\n\t\tsetGeneration((current) => current + 1)\n\t\tthrow new Error(message)\n\t}\n\n\tconst onConfirm = async () => {\n\t\tconst next = targetRef.current\n\t\tif (!next) {\n\t\t\treturn\n\t\t}\n\t\tif (reasonMode === 'required' && !reasonRef.current.trim()) {\n\t\t\ttoast.error(t(keys.errorReasonRequired))\n\t\t\tfailConfirm('reason required')\n\t\t}\n\t\tconst result = await postImpersonation(`${apiPath}/start`, {\n\t\t\tcollection: next.collection,\n\t\t\tid: next.id,\n\t\t\treason: reasonMode === 'off' ? undefined : reasonRef.current,\n\t\t})\n\t\tif (!result.ok) {\n\t\t\ttoast.error(result.error ? t(errorKey(result.error)) : t(keys.errorFailed))\n\t\t\tfailConfirm(result.error ?? 'failed')\n\t\t}\n\t\tgoAfterSwitch(result.redirect)\n\t}\n\n\treturn (\n\t\t<ConfirmationModal\n\t\t\tbody={\n\t\t\t\t<>\n\t\t\t\t\t<p>{target?.label}</p>\n\t\t\t\t\t{reasonMode === 'off' ? null : (\n\t\t\t\t\t\t<TextInput\n\t\t\t\t\t\t\tlabel={t(keys.reasonLabel)}\n\t\t\t\t\t\t\tonChange={(event: ChangeEvent<HTMLInputElement>) => setReason(event.target.value)}\n\t\t\t\t\t\t\tpath={`${modalSlug}-reason`}\n\t\t\t\t\t\t\tplaceholder={reasonMode === 'required' ? undefined : t(keys.reasonPlaceholder)}\n\t\t\t\t\t\t\trequired={reasonMode === 'required'}\n\t\t\t\t\t\t\tvalue={reason}\n\t\t\t\t\t\t/>\n\t\t\t\t\t)}\n\t\t\t\t</>\n\t\t\t}\n\t\t\tclassName=\"impersonation-confirm-modal\"\n\t\t\tconfirmLabel={t(keys.confirm)}\n\t\t\theading={t(keys.confirmTitle)}\n\t\t\tkey={generation}\n\t\t\tmodalSlug={modalSlug}\n\t\t\tonConfirm={onConfirm}\n\t\t/>\n\t)\n}\n"],"mappings":";;;;;;;;;;;;;;;AA6BA,MAAa,qBAAqB,EACjC,SACA,WACA,YACA,aAC6B;CAC7B,MAAM,EAAE,MAAMA,iBAAe;CAC7B,MAAM,CAAC,QAAQ,aAAa,SAAS,EAAE;CACvC,MAAM,CAAC,YAAY,iBAAiB,SAAS,CAAC;CAC9C,MAAM,YAAY,OAAO,MAAM;CAC/B,UAAU,UAAU;CACpB,MAAM,YAAY,OAAO,MAAM;CAC/B,UAAU,UAAU;CAEpB,MAAM,eAAe,YAA2B;EAC/C,eAAe,YAAY,UAAU,CAAC;EACtC,MAAM,IAAI,MAAM,OAAO;CACxB;CAEA,MAAM,YAAY,YAAY;EAC7B,MAAM,OAAO,UAAU;EACvB,IAAI,CAAC,MACJ;EAED,IAAI,eAAe,cAAc,CAAC,UAAU,QAAQ,KAAK,GAAG;GAC3D,MAAM,MAAM,EAAE,KAAK,mBAAmB,CAAC;GACvC,YAAY,iBAAiB;EAC9B;EACA,MAAM,SAAS,MAAM,kBAAkB,GAAG,QAAQ,SAAS;GAC1D,YAAY,KAAK;GACjB,IAAI,KAAK;GACT,QAAQ,eAAe,QAAQ,KAAA,IAAY,UAAU;EACtD,CAAC;EACD,IAAI,CAAC,OAAO,IAAI;GACf,MAAM,MAAM,OAAO,QAAQ,EAAE,SAAS,OAAO,KAAK,CAAC,IAAI,EAAE,KAAK,WAAW,CAAC;GAC1E,YAAY,OAAO,SAAS,QAAQ;EACrC;EACA,cAAc,OAAO,QAAQ;CAC9B;CAEA,OACC,oBAAC,mBAAD;EACC,MACC,qBAAA,UAAA,EAAA,UAAA,CACC,oBAAC,KAAD,EAAA,UAAI,QAAQ,MAAS,CAAA,GACpB,eAAe,QAAQ,OACvB,oBAAC,WAAD;GACC,OAAO,EAAE,KAAK,WAAW;GACzB,WAAW,UAAyC,UAAU,MAAM,OAAO,KAAK;GAChF,MAAM,GAAG,UAAU;GACnB,aAAa,eAAe,aAAa,KAAA,IAAY,EAAE,KAAK,iBAAiB;GAC7E,UAAU,eAAe;GACzB,OAAO;EACP,CAAA,CAED,EAAA,CAAA;EAEH,WAAU;EACV,cAAc,EAAE,KAAK,OAAO;EAC5B,SAAS,EAAE,KAAK,YAAY;EAEjB;EACA;CACX,GAHK,UAGL;AAEH"}
|
package/dist/ui/api.js
ADDED
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { CLIENT_FETCH_TIMEOUT_MS } from "../plugin/constants.js";
|
|
3
|
+
import { keys } from "../translations/keys.js";
|
|
4
|
+
//#region src/ui/api.ts
|
|
5
|
+
const jsonHeaders = { "Content-Type": "application/json" };
|
|
6
|
+
const postImpersonation = async (path, body) => {
|
|
7
|
+
try {
|
|
8
|
+
const response = await fetch(path, {
|
|
9
|
+
body: JSON.stringify(body ?? {}),
|
|
10
|
+
credentials: "include",
|
|
11
|
+
headers: jsonHeaders,
|
|
12
|
+
method: "POST",
|
|
13
|
+
signal: AbortSignal.timeout(CLIENT_FETCH_TIMEOUT_MS)
|
|
14
|
+
});
|
|
15
|
+
let parsed = {};
|
|
16
|
+
try {
|
|
17
|
+
parsed = await response.json();
|
|
18
|
+
} catch {
|
|
19
|
+
parsed = {};
|
|
20
|
+
}
|
|
21
|
+
return {
|
|
22
|
+
error: parsed.error,
|
|
23
|
+
ok: response.ok,
|
|
24
|
+
redirect: parsed.redirect,
|
|
25
|
+
status: response.status
|
|
26
|
+
};
|
|
27
|
+
} catch {
|
|
28
|
+
return {
|
|
29
|
+
error: "failed",
|
|
30
|
+
ok: false,
|
|
31
|
+
status: 0
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
};
|
|
35
|
+
/** Reload when the redirect is the current path; `assign` would be a no-op. */
|
|
36
|
+
const goAfterSwitch = (redirect) => {
|
|
37
|
+
const next = redirect ?? "/";
|
|
38
|
+
if (new URL(next, window.location.origin).pathname === window.location.pathname) {
|
|
39
|
+
window.location.reload();
|
|
40
|
+
return;
|
|
41
|
+
}
|
|
42
|
+
window.location.assign(next);
|
|
43
|
+
};
|
|
44
|
+
const errorKey = (code) => {
|
|
45
|
+
return {
|
|
46
|
+
alreadyImpersonating: keys.errorAlreadyImpersonating,
|
|
47
|
+
failed: keys.errorFailed,
|
|
48
|
+
forbidden: keys.errorForbidden,
|
|
49
|
+
impersonatorGone: keys.errorImpersonatorGone,
|
|
50
|
+
impersonatorSessionExpired: keys.errorImpersonatorSessionExpired,
|
|
51
|
+
invalidBody: keys.errorInvalidBody,
|
|
52
|
+
notImpersonating: keys.errorNotImpersonating,
|
|
53
|
+
origin: keys.errorOrigin,
|
|
54
|
+
reasonRequired: keys.errorReasonRequired,
|
|
55
|
+
selfTarget: keys.errorSelfTarget,
|
|
56
|
+
targetNotFound: keys.errorTargetNotFound,
|
|
57
|
+
targetTrashed: keys.errorTargetTrashed,
|
|
58
|
+
targetUnverified: keys.errorTargetUnverified,
|
|
59
|
+
unsupportedAuth: keys.errorUnsupportedAuth,
|
|
60
|
+
unsupportedCollection: keys.errorUnsupportedCollection
|
|
61
|
+
}[code] ?? keys.errorFailed;
|
|
62
|
+
};
|
|
63
|
+
//#endregion
|
|
64
|
+
export { errorKey, goAfterSwitch, postImpersonation };
|
|
65
|
+
|
|
66
|
+
//# sourceMappingURL=api.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"api.js","names":[],"sources":["../../src/ui/api.ts"],"sourcesContent":["'use client'\n\nimport type { FailureCode } from '../endpoints/codes'\nimport { CLIENT_FETCH_TIMEOUT_MS } from '../plugin/constants'\nimport { keys } from '../translations/keys'\n\nexport type { FailureCode }\n\nconst jsonHeaders = { 'Content-Type': 'application/json' }\n\nexport const postImpersonation = async (\n\tpath: string,\n\tbody: unknown\n): Promise<{ error?: FailureCode; ok: boolean; redirect?: string; status: number }> => {\n\ttry {\n\t\tconst response = await fetch(path, {\n\t\t\tbody: JSON.stringify(body ?? {}),\n\t\t\tcredentials: 'include',\n\t\t\theaders: jsonHeaders,\n\t\t\tmethod: 'POST',\n\t\t\tsignal: AbortSignal.timeout(CLIENT_FETCH_TIMEOUT_MS),\n\t\t})\n\t\tlet parsed: { error?: FailureCode; redirect?: string } = {}\n\t\ttry {\n\t\t\tparsed = (await response.json()) as { error?: FailureCode; redirect?: string }\n\t\t} catch {\n\t\t\tparsed = {}\n\t\t}\n\t\treturn {\n\t\t\terror: parsed.error,\n\t\t\tok: response.ok,\n\t\t\tredirect: parsed.redirect,\n\t\t\tstatus: response.status,\n\t\t}\n\t} catch {\n\t\treturn { error: 'failed', ok: false, status: 0 }\n\t}\n}\n\n/** Reload when the redirect is the current path; `assign` would be a no-op. */\nexport const goAfterSwitch = (redirect?: string) => {\n\tconst next = redirect ?? '/'\n\tconst path = new URL(next, window.location.origin).pathname\n\tif (path === window.location.pathname) {\n\t\twindow.location.reload()\n\t\treturn\n\t}\n\twindow.location.assign(next)\n}\n\nexport const errorKey = (code: string) => {\n\tconst map: Record<string, (typeof keys)[keyof typeof keys]> = {\n\t\talreadyImpersonating: keys.errorAlreadyImpersonating,\n\t\tfailed: keys.errorFailed,\n\t\tforbidden: keys.errorForbidden,\n\t\timpersonatorGone: keys.errorImpersonatorGone,\n\t\timpersonatorSessionExpired: keys.errorImpersonatorSessionExpired,\n\t\tinvalidBody: keys.errorInvalidBody,\n\t\tnotImpersonating: keys.errorNotImpersonating,\n\t\torigin: keys.errorOrigin,\n\t\treasonRequired: keys.errorReasonRequired,\n\t\tselfTarget: keys.errorSelfTarget,\n\t\ttargetNotFound: keys.errorTargetNotFound,\n\t\ttargetTrashed: keys.errorTargetTrashed,\n\t\ttargetUnverified: keys.errorTargetUnverified,\n\t\tunsupportedAuth: keys.errorUnsupportedAuth,\n\t\tunsupportedCollection: keys.errorUnsupportedCollection,\n\t}\n\treturn map[code] ?? keys.errorFailed\n}\n"],"mappings":";;;;AAQA,MAAM,cAAc,EAAE,gBAAgB,mBAAmB;AAEzD,MAAa,oBAAoB,OAChC,MACA,SACsF;CACtF,IAAI;EACH,MAAM,WAAW,MAAM,MAAM,MAAM;GAClC,MAAM,KAAK,UAAU,QAAQ,CAAC,CAAC;GAC/B,aAAa;GACb,SAAS;GACT,QAAQ;GACR,QAAQ,YAAY,QAAQ,uBAAuB;EACpD,CAAC;EACD,IAAI,SAAqD,CAAC;EAC1D,IAAI;GACH,SAAU,MAAM,SAAS,KAAK;EAC/B,QAAQ;GACP,SAAS,CAAC;EACX;EACA,OAAO;GACN,OAAO,OAAO;GACd,IAAI,SAAS;GACb,UAAU,OAAO;GACjB,QAAQ,SAAS;EAClB;CACD,QAAQ;EACP,OAAO;GAAE,OAAO;GAAU,IAAI;GAAO,QAAQ;EAAE;CAChD;AACD;;AAGA,MAAa,iBAAiB,aAAsB;CACnD,MAAM,OAAO,YAAY;CAEzB,IADa,IAAI,IAAI,MAAM,OAAO,SAAS,MAAM,EAAE,aACtC,OAAO,SAAS,UAAU;EACtC,OAAO,SAAS,OAAO;EACvB;CACD;CACA,OAAO,SAAS,OAAO,IAAI;AAC5B;AAEA,MAAa,YAAY,SAAiB;CAkBzC,OAAO;EAhBN,sBAAsB,KAAK;EAC3B,QAAQ,KAAK;EACb,WAAW,KAAK;EAChB,kBAAkB,KAAK;EACvB,4BAA4B,KAAK;EACjC,aAAa,KAAK;EAClB,kBAAkB,KAAK;EACvB,QAAQ,KAAK;EACb,gBAAgB,KAAK;EACrB,YAAY,KAAK;EACjB,gBAAgB,KAAK;EACrB,eAAe,KAAK;EACpB,kBAAkB,KAAK;EACvB,iBAAiB,KAAK;EACtB,uBAAuB,KAAK;CAEpB,EAAE,SAAS,KAAK;AAC1B"}
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
//#region src/ui/barPlacement.ts
|
|
2
|
+
const STORAGE_KEY = "impersonation-bar";
|
|
3
|
+
const MARGIN = 12;
|
|
4
|
+
const EDGES = [
|
|
5
|
+
"top",
|
|
6
|
+
"top-right",
|
|
7
|
+
"right",
|
|
8
|
+
"bottom-right",
|
|
9
|
+
"bottom",
|
|
10
|
+
"bottom-left",
|
|
11
|
+
"left",
|
|
12
|
+
"top-left"
|
|
13
|
+
];
|
|
14
|
+
const isBarEdge = (value) => typeof value === "string" && EDGES.includes(value);
|
|
15
|
+
const readBarPlacement = () => {
|
|
16
|
+
if (typeof localStorage === "undefined") return {
|
|
17
|
+
collapsed: false,
|
|
18
|
+
edge: "right"
|
|
19
|
+
};
|
|
20
|
+
try {
|
|
21
|
+
const parsed = JSON.parse(localStorage.getItem(STORAGE_KEY) ?? "");
|
|
22
|
+
return {
|
|
23
|
+
collapsed: parsed.collapsed === true,
|
|
24
|
+
edge: isBarEdge(parsed.edge) ? parsed.edge : "right"
|
|
25
|
+
};
|
|
26
|
+
} catch {
|
|
27
|
+
return {
|
|
28
|
+
collapsed: false,
|
|
29
|
+
edge: "right"
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
};
|
|
33
|
+
const writeBarPlacement = (placement) => {
|
|
34
|
+
localStorage.setItem(STORAGE_KEY, JSON.stringify(placement));
|
|
35
|
+
};
|
|
36
|
+
/**
|
|
37
|
+
* Layout viewport without scrollbars. `position: fixed` resolves against it, so
|
|
38
|
+
* `window.innerWidth` would park a right-edge chip under the scrollbar.
|
|
39
|
+
*/
|
|
40
|
+
const readViewport = () => ({
|
|
41
|
+
height: document.documentElement.clientHeight,
|
|
42
|
+
width: document.documentElement.clientWidth
|
|
43
|
+
});
|
|
44
|
+
const anchorsFor = ({ header, size, viewport }) => {
|
|
45
|
+
const left = MARGIN;
|
|
46
|
+
const right = Math.max(left, viewport.width - size.width - MARGIN);
|
|
47
|
+
const top = header + MARGIN;
|
|
48
|
+
const bottom = Math.max(top, viewport.height - size.height - MARGIN);
|
|
49
|
+
const midY = Math.max(top, (viewport.height - size.height) / 2);
|
|
50
|
+
const midX = Math.max(MARGIN, (viewport.width - size.width) / 2);
|
|
51
|
+
return {
|
|
52
|
+
bottom: {
|
|
53
|
+
x: midX,
|
|
54
|
+
y: bottom
|
|
55
|
+
},
|
|
56
|
+
"bottom-left": {
|
|
57
|
+
x: left,
|
|
58
|
+
y: bottom
|
|
59
|
+
},
|
|
60
|
+
"bottom-right": {
|
|
61
|
+
x: right,
|
|
62
|
+
y: bottom
|
|
63
|
+
},
|
|
64
|
+
left: {
|
|
65
|
+
x: left,
|
|
66
|
+
y: midY
|
|
67
|
+
},
|
|
68
|
+
right: {
|
|
69
|
+
x: right,
|
|
70
|
+
y: midY
|
|
71
|
+
},
|
|
72
|
+
top: {
|
|
73
|
+
x: midX,
|
|
74
|
+
y: top
|
|
75
|
+
},
|
|
76
|
+
"top-left": {
|
|
77
|
+
x: left,
|
|
78
|
+
y: top
|
|
79
|
+
},
|
|
80
|
+
"top-right": {
|
|
81
|
+
x: right,
|
|
82
|
+
y: top
|
|
83
|
+
}
|
|
84
|
+
};
|
|
85
|
+
};
|
|
86
|
+
/** Project a release point along the drag so a flick lands in the corner it was thrown toward. */
|
|
87
|
+
const projectThrow = (point, velocity) => ({
|
|
88
|
+
x: point.x + velocity.x * 220,
|
|
89
|
+
y: point.y + velocity.y * 220
|
|
90
|
+
});
|
|
91
|
+
const nearestEdge = (point, anchors) => {
|
|
92
|
+
let best = "right";
|
|
93
|
+
let bestDistance = Number.POSITIVE_INFINITY;
|
|
94
|
+
for (const edge of EDGES) {
|
|
95
|
+
const anchor = anchors[edge];
|
|
96
|
+
const distance = (anchor.x - point.x) ** 2 + (anchor.y - point.y) ** 2;
|
|
97
|
+
if (distance < bestDistance) {
|
|
98
|
+
best = edge;
|
|
99
|
+
bestDistance = distance;
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
return best;
|
|
103
|
+
};
|
|
104
|
+
//#endregion
|
|
105
|
+
export { anchorsFor, nearestEdge, projectThrow, readBarPlacement, readViewport, writeBarPlacement };
|
|
106
|
+
|
|
107
|
+
//# sourceMappingURL=barPlacement.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"barPlacement.js","names":[],"sources":["../../src/ui/barPlacement.ts"],"sourcesContent":["export type BarEdge =\n\t| 'bottom'\n\t| 'bottom-left'\n\t| 'bottom-right'\n\t| 'left'\n\t| 'right'\n\t| 'top'\n\t| 'top-left'\n\t| 'top-right'\n\nexport type BarPlacement = {\n\tcollapsed: boolean\n\tedge: BarEdge\n}\n\nexport type Point = { x: number; y: number }\n\nconst STORAGE_KEY = 'impersonation-bar'\nconst MARGIN = 12\n\nconst EDGES: readonly BarEdge[] = [\n\t'top',\n\t'top-right',\n\t'right',\n\t'bottom-right',\n\t'bottom',\n\t'bottom-left',\n\t'left',\n\t'top-left',\n]\n\nexport const isBarEdge = (value: unknown): value is BarEdge =>\n\ttypeof value === 'string' && (EDGES as readonly string[]).includes(value)\n\nexport const readBarPlacement = (): BarPlacement => {\n\tif (typeof localStorage === 'undefined') {\n\t\treturn { collapsed: false, edge: 'right' }\n\t}\n\ttry {\n\t\tconst parsed = JSON.parse(localStorage.getItem(STORAGE_KEY) ?? '') as {\n\t\t\tcollapsed?: unknown\n\t\t\tedge?: unknown\n\t\t}\n\t\treturn {\n\t\t\tcollapsed: parsed.collapsed === true,\n\t\t\tedge: isBarEdge(parsed.edge) ? parsed.edge : 'right',\n\t\t}\n\t} catch {\n\t\treturn { collapsed: false, edge: 'right' }\n\t}\n}\n\nexport const writeBarPlacement = (placement: BarPlacement): void => {\n\tlocalStorage.setItem(STORAGE_KEY, JSON.stringify(placement))\n}\n\n/**\n * Layout viewport without scrollbars. `position: fixed` resolves against it, so\n * `window.innerWidth` would park a right-edge chip under the scrollbar.\n */\nexport const readViewport = (): { height: number; width: number } => ({\n\theight: document.documentElement.clientHeight,\n\twidth: document.documentElement.clientWidth,\n})\n\nexport const anchorsFor = ({\n\theader,\n\tsize,\n\tviewport,\n}: {\n\theader: number\n\tsize: { height: number; width: number }\n\tviewport: { height: number; width: number }\n}): Record<BarEdge, Point> => {\n\tconst left = MARGIN\n\tconst right = Math.max(left, viewport.width - size.width - MARGIN)\n\tconst top = header + MARGIN\n\tconst bottom = Math.max(top, viewport.height - size.height - MARGIN)\n\tconst midY = Math.max(top, (viewport.height - size.height) / 2)\n\tconst midX = Math.max(MARGIN, (viewport.width - size.width) / 2)\n\treturn {\n\t\tbottom: { x: midX, y: bottom },\n\t\t'bottom-left': { x: left, y: bottom },\n\t\t'bottom-right': { x: right, y: bottom },\n\t\tleft: { x: left, y: midY },\n\t\tright: { x: right, y: midY },\n\t\ttop: { x: midX, y: top },\n\t\t'top-left': { x: left, y: top },\n\t\t'top-right': { x: right, y: top },\n\t}\n}\n\n/** Project a release point along the drag so a flick lands in the corner it was thrown toward. */\nexport const projectThrow = (point: Point, velocity: Point): Point => ({\n\tx: point.x + velocity.x * 220,\n\ty: point.y + velocity.y * 220,\n})\n\nexport const nearestEdge = (point: Point, anchors: Record<BarEdge, Point>): BarEdge => {\n\tlet best: BarEdge = 'right'\n\tlet bestDistance = Number.POSITIVE_INFINITY\n\tfor (const edge of EDGES) {\n\t\tconst anchor = anchors[edge]\n\t\tconst distance = (anchor.x - point.x) ** 2 + (anchor.y - point.y) ** 2\n\t\tif (distance < bestDistance) {\n\t\t\tbest = edge\n\t\t\tbestDistance = distance\n\t\t}\n\t}\n\treturn best\n}\n"],"mappings":";AAiBA,MAAM,cAAc;AACpB,MAAM,SAAS;AAEf,MAAM,QAA4B;CACjC;CACA;CACA;CACA;CACA;CACA;CACA;CACA;AACD;AAEA,MAAa,aAAa,UACzB,OAAO,UAAU,YAAa,MAA4B,SAAS,KAAK;AAEzE,MAAa,yBAAuC;CACnD,IAAI,OAAO,iBAAiB,aAC3B,OAAO;EAAE,WAAW;EAAO,MAAM;CAAQ;CAE1C,IAAI;EACH,MAAM,SAAS,KAAK,MAAM,aAAa,QAAQ,WAAW,KAAK,EAAE;EAIjE,OAAO;GACN,WAAW,OAAO,cAAc;GAChC,MAAM,UAAU,OAAO,IAAI,IAAI,OAAO,OAAO;EAC9C;CACD,QAAQ;EACP,OAAO;GAAE,WAAW;GAAO,MAAM;EAAQ;CAC1C;AACD;AAEA,MAAa,qBAAqB,cAAkC;CACnE,aAAa,QAAQ,aAAa,KAAK,UAAU,SAAS,CAAC;AAC5D;;;;;AAMA,MAAa,sBAAyD;CACrE,QAAQ,SAAS,gBAAgB;CACjC,OAAO,SAAS,gBAAgB;AACjC;AAEA,MAAa,cAAc,EAC1B,QACA,MACA,eAK6B;CAC7B,MAAM,OAAO;CACb,MAAM,QAAQ,KAAK,IAAI,MAAM,SAAS,QAAQ,KAAK,QAAQ,MAAM;CACjE,MAAM,MAAM,SAAS;CACrB,MAAM,SAAS,KAAK,IAAI,KAAK,SAAS,SAAS,KAAK,SAAS,MAAM;CACnE,MAAM,OAAO,KAAK,IAAI,MAAM,SAAS,SAAS,KAAK,UAAU,CAAC;CAC9D,MAAM,OAAO,KAAK,IAAI,SAAS,SAAS,QAAQ,KAAK,SAAS,CAAC;CAC/D,OAAO;EACN,QAAQ;GAAE,GAAG;GAAM,GAAG;EAAO;EAC7B,eAAe;GAAE,GAAG;GAAM,GAAG;EAAO;EACpC,gBAAgB;GAAE,GAAG;GAAO,GAAG;EAAO;EACtC,MAAM;GAAE,GAAG;GAAM,GAAG;EAAK;EACzB,OAAO;GAAE,GAAG;GAAO,GAAG;EAAK;EAC3B,KAAK;GAAE,GAAG;GAAM,GAAG;EAAI;EACvB,YAAY;GAAE,GAAG;GAAM,GAAG;EAAI;EAC9B,aAAa;GAAE,GAAG;GAAO,GAAG;EAAI;CACjC;AACD;;AAGA,MAAa,gBAAgB,OAAc,cAA4B;CACtE,GAAG,MAAM,IAAI,SAAS,IAAI;CAC1B,GAAG,MAAM,IAAI,SAAS,IAAI;AAC3B;AAEA,MAAa,eAAe,OAAc,YAA6C;CACtF,IAAI,OAAgB;CACpB,IAAI,eAAe,OAAO;CAC1B,KAAK,MAAM,QAAQ,OAAO;EACzB,MAAM,SAAS,QAAQ;EACvB,MAAM,YAAY,OAAO,IAAI,MAAM,MAAM,KAAK,OAAO,IAAI,MAAM,MAAM;EACrE,IAAI,WAAW,cAAc;GAC5B,OAAO;GACP,eAAe;EAChB;CACD;CACA,OAAO;AACR"}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
:where(.impersonation-frontend-banner--default) {
|
|
2
|
+
align-items: center;
|
|
3
|
+
background: #f4f1ea;
|
|
4
|
+
color: #1c1915;
|
|
5
|
+
display: flex;
|
|
6
|
+
font:
|
|
7
|
+
14px / 1.4 system-ui,
|
|
8
|
+
sans-serif;
|
|
9
|
+
gap: 12px;
|
|
10
|
+
justify-content: space-between;
|
|
11
|
+
padding: 0.6rem 1rem;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
:where(.impersonation-frontend-banner--default .impersonation-frontend-banner__exit) {
|
|
15
|
+
background: transparent;
|
|
16
|
+
border: 1px solid currentColor;
|
|
17
|
+
border-radius: 4px;
|
|
18
|
+
color: inherit;
|
|
19
|
+
cursor: pointer;
|
|
20
|
+
font: inherit;
|
|
21
|
+
padding: 0.35rem 0.75rem;
|
|
22
|
+
}
|
|
@@ -0,0 +1,244 @@
|
|
|
1
|
+
.impersonation-bar {
|
|
2
|
+
align-items: center;
|
|
3
|
+
background: var(--theme-warning-50);
|
|
4
|
+
border-block-start: 1px solid var(--theme-warning-250);
|
|
5
|
+
bottom: 0;
|
|
6
|
+
color: var(--theme-warning-800);
|
|
7
|
+
display: flex;
|
|
8
|
+
gap: calc(var(--base) * 0.75);
|
|
9
|
+
inset-inline: 0;
|
|
10
|
+
justify-content: space-between;
|
|
11
|
+
padding-block: calc(var(--base) * 0.35);
|
|
12
|
+
padding-inline: var(--gutter-h, 1rem);
|
|
13
|
+
position: fixed;
|
|
14
|
+
z-index: var(--z-status, 10);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
html[data-theme="dark"] .impersonation-bar {
|
|
18
|
+
background: var(--theme-warning-100);
|
|
19
|
+
color: var(--theme-warning-800);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/* Follows Payload's nav column: `--nav-width` while it is open, animated like the grid. */
|
|
23
|
+
body:has(.template-default--nav-open) .impersonation-bar {
|
|
24
|
+
inset-inline-start: var(--nav-width);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
@media (prefers-reduced-motion: no-preference) {
|
|
28
|
+
.impersonation-bar {
|
|
29
|
+
transition: inset-inline-start var(--nav-trans-time) linear;
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
.impersonation-bar--quiet {
|
|
34
|
+
background: var(--theme-success-50);
|
|
35
|
+
border-block-start-color: var(--theme-success-250);
|
|
36
|
+
color: var(--theme-success-800);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
html[data-theme="dark"] .impersonation-bar--quiet {
|
|
40
|
+
background: var(--theme-success-100);
|
|
41
|
+
color: var(--theme-success-800);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
.impersonation-bar__meta {
|
|
45
|
+
display: flex;
|
|
46
|
+
flex-direction: column;
|
|
47
|
+
gap: calc(var(--base) * 0.15);
|
|
48
|
+
min-width: 0;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
.impersonation-bar__copy {
|
|
52
|
+
align-items: center;
|
|
53
|
+
display: flex;
|
|
54
|
+
flex-wrap: wrap;
|
|
55
|
+
gap: calc(var(--base) * 0.4);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
.impersonation-bar__time {
|
|
59
|
+
color: var(--theme-warning-800);
|
|
60
|
+
font-size: 0.875rem;
|
|
61
|
+
opacity: 0.92;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
.impersonation-bar--quiet .impersonation-bar__time {
|
|
65
|
+
color: var(--theme-success-800);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
.impersonation-chip__name {
|
|
69
|
+
max-width: 12rem;
|
|
70
|
+
overflow: hidden;
|
|
71
|
+
text-overflow: ellipsis;
|
|
72
|
+
white-space: nowrap;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
.impersonation-bar__actions {
|
|
76
|
+
align-items: center;
|
|
77
|
+
display: flex;
|
|
78
|
+
flex-shrink: 0;
|
|
79
|
+
gap: calc(var(--base) * 0.5);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
.impersonation-bar__link {
|
|
83
|
+
color: inherit;
|
|
84
|
+
font-size: 0.875rem;
|
|
85
|
+
text-decoration: underline;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
.impersonation-bar__icon {
|
|
89
|
+
align-items: center;
|
|
90
|
+
background: transparent;
|
|
91
|
+
border: 0;
|
|
92
|
+
color: inherit;
|
|
93
|
+
cursor: pointer;
|
|
94
|
+
display: inline-flex;
|
|
95
|
+
padding: calc(var(--base) * 0.2);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
.impersonation-bar__icon:focus-visible {
|
|
99
|
+
outline: 2px solid var(--theme-warning-800);
|
|
100
|
+
outline-offset: 2px;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
.impersonation-chip {
|
|
104
|
+
align-items: center;
|
|
105
|
+
background: var(--theme-warning-50);
|
|
106
|
+
border: 1px solid var(--theme-warning-250);
|
|
107
|
+
border-radius: 999px;
|
|
108
|
+
box-shadow: 0 8px 24px rgb(0 0 0 / 18%);
|
|
109
|
+
color: var(--theme-warning-800);
|
|
110
|
+
cursor: grab;
|
|
111
|
+
display: flex;
|
|
112
|
+
gap: calc(var(--base) * 0.25);
|
|
113
|
+
padding-block: calc(var(--base) * 0.3);
|
|
114
|
+
padding-inline: calc(var(--base) * 0.7) calc(var(--base) * 0.3);
|
|
115
|
+
position: fixed;
|
|
116
|
+
touch-action: none;
|
|
117
|
+
user-select: none;
|
|
118
|
+
z-index: calc(var(--z-nav, 20) + 30);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
html[data-theme="dark"] .impersonation-chip {
|
|
122
|
+
background: var(--theme-warning-100);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
.impersonation-chip--quiet {
|
|
126
|
+
background: var(--theme-success-50);
|
|
127
|
+
border-color: var(--theme-success-250);
|
|
128
|
+
color: var(--theme-success-800);
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
html[data-theme="dark"] .impersonation-chip--quiet {
|
|
132
|
+
background: var(--theme-success-100);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
.impersonation-bar--quiet .impersonation-bar__icon:focus-visible,
|
|
136
|
+
.impersonation-chip--quiet .impersonation-bar__icon:focus-visible {
|
|
137
|
+
outline-color: var(--theme-success-800);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
.impersonation-chip--dragging {
|
|
141
|
+
cursor: grabbing;
|
|
142
|
+
transition: none;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
.impersonation-chip:not(.impersonation-chip--dragging) {
|
|
146
|
+
transition:
|
|
147
|
+
left 280ms cubic-bezier(0.2, 0.8, 0.2, 1),
|
|
148
|
+
top 280ms cubic-bezier(0.2, 0.8, 0.2, 1);
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
@media (prefers-reduced-motion: reduce) {
|
|
152
|
+
.impersonation-chip:not(.impersonation-chip--dragging) {
|
|
153
|
+
transition: none;
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
.impersonation-switcher {
|
|
158
|
+
display: flex;
|
|
159
|
+
padding-block-end: var(--base);
|
|
160
|
+
flex-direction: column;
|
|
161
|
+
gap: var(--base);
|
|
162
|
+
min-height: calc(100vh - 10rem);
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
.impersonation-switcher__results {
|
|
166
|
+
display: flex;
|
|
167
|
+
flex: 1;
|
|
168
|
+
flex-direction: column;
|
|
169
|
+
gap: calc(var(--base) * 0.4);
|
|
170
|
+
isolation: isolate;
|
|
171
|
+
min-height: 0;
|
|
172
|
+
overflow: auto;
|
|
173
|
+
z-index: 0;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
.impersonation-card {
|
|
177
|
+
align-items: center;
|
|
178
|
+
background: var(--theme-elevation-50);
|
|
179
|
+
border: 1px solid var(--theme-elevation-150);
|
|
180
|
+
border-radius: var(--style-radius-m, 4px);
|
|
181
|
+
display: flex;
|
|
182
|
+
gap: calc(var(--base) * 0.35);
|
|
183
|
+
padding-block: calc(var(--base) * 0.35);
|
|
184
|
+
padding-inline: calc(var(--base) * 0.5);
|
|
185
|
+
width: 100%;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
.impersonation-card:hover {
|
|
189
|
+
background: var(--theme-elevation-100);
|
|
190
|
+
border-color: var(--theme-elevation-250);
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
.impersonation-card__pick {
|
|
194
|
+
align-items: flex-start;
|
|
195
|
+
background: none;
|
|
196
|
+
border: 0;
|
|
197
|
+
color: var(--theme-text);
|
|
198
|
+
cursor: pointer;
|
|
199
|
+
display: flex;
|
|
200
|
+
flex: 1;
|
|
201
|
+
flex-direction: column;
|
|
202
|
+
gap: 0.15rem;
|
|
203
|
+
min-width: 0;
|
|
204
|
+
padding: calc(var(--base) * 0.2);
|
|
205
|
+
text-align: start;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
.impersonation-card__pick:focus-visible {
|
|
209
|
+
outline: 2px solid var(--theme-elevation-800);
|
|
210
|
+
outline-offset: 2px;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
.impersonation-card__actions {
|
|
214
|
+
align-items: center;
|
|
215
|
+
display: flex;
|
|
216
|
+
flex-shrink: 0;
|
|
217
|
+
gap: calc(var(--base) * 0.15);
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
.impersonation-card__title {
|
|
221
|
+
font-weight: 600;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
.impersonation-card__email {
|
|
225
|
+
color: var(--theme-elevation-500);
|
|
226
|
+
font-size: 1rem;
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
.impersonation-switcher__empty {
|
|
230
|
+
color: var(--theme-elevation-500);
|
|
231
|
+
margin: 0;
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
.impersonation-switcher__pager {
|
|
235
|
+
align-items: center;
|
|
236
|
+
display: flex;
|
|
237
|
+
gap: calc(var(--base) * 0.4);
|
|
238
|
+
justify-content: flex-end;
|
|
239
|
+
margin-block-start: auto;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
.impersonation-confirm-modal .field-type {
|
|
243
|
+
width: 100%;
|
|
244
|
+
}
|