@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.
Files changed (163) hide show
  1. package/CHANGELOG.md +31 -0
  2. package/LICENSE +21 -0
  3. package/README.md +49 -0
  4. package/dist/access/filterTargets.js +51 -0
  5. package/dist/access/filterTargets.js.map +1 -0
  6. package/dist/auth/cookies.js +49 -0
  7. package/dist/auth/cookies.js.map +1 -0
  8. package/dist/auth/expiryCookies.js +59 -0
  9. package/dist/auth/expiryCookies.js.map +1 -0
  10. package/dist/auth/issue.js +126 -0
  11. package/dist/auth/issue.js.map +1 -0
  12. package/dist/auth/mode.js +69 -0
  13. package/dist/auth/mode.js.map +1 -0
  14. package/dist/auth/origin.js +55 -0
  15. package/dist/auth/origin.js.map +1 -0
  16. package/dist/auth/revoke.js +24 -0
  17. package/dist/auth/revoke.js.map +1 -0
  18. package/dist/auth/tenantCookie.js +74 -0
  19. package/dist/auth/tenantCookie.js.map +1 -0
  20. package/dist/endpoints/codes.d.ts +6 -0
  21. package/dist/endpoints/current.js +66 -0
  22. package/dist/endpoints/current.js.map +1 -0
  23. package/dist/endpoints/exit.js +242 -0
  24. package/dist/endpoints/exit.js.map +1 -0
  25. package/dist/endpoints/shared.js +76 -0
  26. package/dist/endpoints/shared.js.map +1 -0
  27. package/dist/endpoints/start.js +332 -0
  28. package/dist/endpoints/start.js.map +1 -0
  29. package/dist/endpoints/terminate.js +73 -0
  30. package/dist/endpoints/terminate.js.map +1 -0
  31. package/dist/exports/client.d.ts +9 -0
  32. package/dist/exports/client.js +10 -0
  33. package/dist/exports/i18n.d.ts +3 -0
  34. package/dist/exports/i18n.js +3 -0
  35. package/dist/exports/rsc.d.ts +4 -0
  36. package/dist/exports/rsc.js +4 -0
  37. package/dist/exports/types.d.ts +3 -0
  38. package/dist/exports/types.js +1 -0
  39. package/dist/getImpersonation.d.ts +49 -0
  40. package/dist/getImpersonation.js +78 -0
  41. package/dist/getImpersonation.js.map +1 -0
  42. package/dist/ids.js +27 -0
  43. package/dist/ids.js.map +1 -0
  44. package/dist/index.d.ts +20 -0
  45. package/dist/index.js +55 -0
  46. package/dist/index.js.map +1 -0
  47. package/dist/plugin/assertTargets.js +23 -0
  48. package/dist/plugin/assertTargets.js.map +1 -0
  49. package/dist/plugin/constants.d.ts +6 -0
  50. package/dist/plugin/constants.js +20 -0
  51. package/dist/plugin/constants.js.map +1 -0
  52. package/dist/plugin/decorateAuth.js +99 -0
  53. package/dist/plugin/decorateAuth.js.map +1 -0
  54. package/dist/plugin/lookup.js +23 -0
  55. package/dist/plugin/lookup.js.map +1 -0
  56. package/dist/plugin/normalizeOptions.js +83 -0
  57. package/dist/plugin/normalizeOptions.js.map +1 -0
  58. package/dist/plugin/registerAdmin.js +49 -0
  59. package/dist/plugin/registerAdmin.js.map +1 -0
  60. package/dist/plugin/registerCollection.js +183 -0
  61. package/dist/plugin/registerCollection.js.map +1 -0
  62. package/dist/plugin/registerEndpoints.js +19 -0
  63. package/dist/plugin/registerEndpoints.js.map +1 -0
  64. package/dist/plugin/registerHooks.js +59 -0
  65. package/dist/plugin/registerHooks.js.map +1 -0
  66. package/dist/plugin/registerRetention.js +57 -0
  67. package/dist/plugin/registerRetention.js.map +1 -0
  68. package/dist/plugin/registerTranslations.js +19 -0
  69. package/dist/plugin/registerTranslations.js.map +1 -0
  70. package/dist/plugin/registry.js +11 -0
  71. package/dist/plugin/registry.js.map +1 -0
  72. package/dist/plugin/startable.js +21 -0
  73. package/dist/plugin/startable.js.map +1 -0
  74. package/dist/session/close.js +65 -0
  75. package/dist/session/close.js.map +1 -0
  76. package/dist/session/closeStale.d.ts +11 -0
  77. package/dist/session/closeStale.js +65 -0
  78. package/dist/session/closeStale.js.map +1 -0
  79. package/dist/session/locks.js +32 -0
  80. package/dist/session/locks.js.map +1 -0
  81. package/dist/session/resolve.js +91 -0
  82. package/dist/session/resolve.js.map +1 -0
  83. package/dist/translations/ar.js +52 -0
  84. package/dist/translations/ar.js.map +1 -0
  85. package/dist/translations/de.js +52 -0
  86. package/dist/translations/de.js.map +1 -0
  87. package/dist/translations/en.js +52 -0
  88. package/dist/translations/en.js.map +1 -0
  89. package/dist/translations/es.js +52 -0
  90. package/dist/translations/es.js.map +1 -0
  91. package/dist/translations/fr.js +52 -0
  92. package/dist/translations/fr.js.map +1 -0
  93. package/dist/translations/id.js +52 -0
  94. package/dist/translations/id.js.map +1 -0
  95. package/dist/translations/index.d.ts +24 -0
  96. package/dist/translations/index.js +49 -0
  97. package/dist/translations/index.js.map +1 -0
  98. package/dist/translations/keys.d.ts +56 -0
  99. package/dist/translations/keys.js +56 -0
  100. package/dist/translations/keys.js.map +1 -0
  101. package/dist/translations/ko.js +52 -0
  102. package/dist/translations/ko.js.map +1 -0
  103. package/dist/translations/lookup.js +17 -0
  104. package/dist/translations/lookup.js.map +1 -0
  105. package/dist/translations/pt.js +52 -0
  106. package/dist/translations/pt.js.map +1 -0
  107. package/dist/translations/ru.js +52 -0
  108. package/dist/translations/ru.js.map +1 -0
  109. package/dist/translations/server.js +13 -0
  110. package/dist/translations/server.js.map +1 -0
  111. package/dist/translations/uk.js +52 -0
  112. package/dist/translations/uk.js.map +1 -0
  113. package/dist/translations/useTranslation.js +12 -0
  114. package/dist/translations/useTranslation.js.map +1 -0
  115. package/dist/translations/zh.js +52 -0
  116. package/dist/translations/zh.js.map +1 -0
  117. package/dist/types.d.ts +214 -0
  118. package/dist/types.js +11 -0
  119. package/dist/types.js.map +1 -0
  120. package/dist/ui/EndSessionMenuItem.d.ts +5 -0
  121. package/dist/ui/EndSessionMenuItem.js +45 -0
  122. package/dist/ui/EndSessionMenuItem.js.map +1 -0
  123. package/dist/ui/ImpersonateIcon.js +20 -0
  124. package/dist/ui/ImpersonateIcon.js.map +1 -0
  125. package/dist/ui/ImpersonationAction.d.ts +10 -0
  126. package/dist/ui/ImpersonationAction.js +48 -0
  127. package/dist/ui/ImpersonationAction.js.map +1 -0
  128. package/dist/ui/ImpersonationBar.d.ts +36 -0
  129. package/dist/ui/ImpersonationBar.js +357 -0
  130. package/dist/ui/ImpersonationBar.js.map +1 -0
  131. package/dist/ui/ImpersonationConfig.js +25 -0
  132. package/dist/ui/ImpersonationConfig.js.map +1 -0
  133. package/dist/ui/ImpersonationDocumentButton.d.ts +5 -0
  134. package/dist/ui/ImpersonationDocumentButton.js +85 -0
  135. package/dist/ui/ImpersonationDocumentButton.js.map +1 -0
  136. package/dist/ui/ImpersonationFrontendBanner.d.ts +41 -0
  137. package/dist/ui/ImpersonationFrontendBanner.js +58 -0
  138. package/dist/ui/ImpersonationFrontendBanner.js.map +1 -0
  139. package/dist/ui/ImpersonationFrontendExit.d.ts +17 -0
  140. package/dist/ui/ImpersonationFrontendExit.js +45 -0
  141. package/dist/ui/ImpersonationFrontendExit.js.map +1 -0
  142. package/dist/ui/ImpersonationProvider.d.ts +16 -0
  143. package/dist/ui/ImpersonationProvider.js +127 -0
  144. package/dist/ui/ImpersonationProvider.js.map +1 -0
  145. package/dist/ui/ImpersonationSwitcher.d.ts +21 -0
  146. package/dist/ui/ImpersonationSwitcher.js +251 -0
  147. package/dist/ui/ImpersonationSwitcher.js.map +1 -0
  148. package/dist/ui/ImpersonationUserCard.d.ts +31 -0
  149. package/dist/ui/ImpersonationUserCard.js +51 -0
  150. package/dist/ui/ImpersonationUserCard.js.map +1 -0
  151. package/dist/ui/StartConfirmModal.d.ts +28 -0
  152. package/dist/ui/StartConfirmModal.js +65 -0
  153. package/dist/ui/StartConfirmModal.js.map +1 -0
  154. package/dist/ui/api.js +66 -0
  155. package/dist/ui/api.js.map +1 -0
  156. package/dist/ui/barPlacement.js +107 -0
  157. package/dist/ui/barPlacement.js.map +1 -0
  158. package/dist/ui/frontendBanner.css +22 -0
  159. package/dist/ui/impersonation.css +244 -0
  160. package/dist/ui/useImpersonation.d.ts +43 -0
  161. package/dist/ui/useImpersonation.js +36 -0
  162. package/dist/ui/useImpersonation.js.map +1 -0
  163. 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
+ }